1<?php
2/*
3 * This file is part of the php-code-coverage package.
4 *
5 * (c) Sebastian Bergmann <sebastian@phpunit.de>
6 *
7 * For the full copyright and license information, please view the LICENSE
8 * file that was distributed with this source code.
9 */
10
11namespace SebastianBergmann\CodeCoverage\Driver;
12
13use SebastianBergmann\CodeCoverage\RuntimeException;
14
15/**
16 * Driver for PHPDBG's code coverage functionality.
17 *
18 * @codeCoverageIgnore
19 */
20class PHPDBG implements Driver
21{
22    /**
23     * Constructor.
24     */
25    public function __construct()
26    {
27        if (PHP_SAPI !== 'phpdbg') {
28            throw new RuntimeException(
29                'This driver requires the PHPDBG SAPI'
30            );
31        }
32
33        if (!function_exists('phpdbg_start_oplog')) {
34            throw new RuntimeException(
35                'This build of PHPDBG does not support code coverage'
36            );
37        }
38    }
39
40    /**
41     * Start collection of code coverage information.
42     *
43     * @param bool $determineUnusedAndDead
44     */
45    public function start($determineUnusedAndDead = true)
46    {
47        phpdbg_start_oplog();
48    }
49
50    /**
51     * Stop collection of code coverage information.
52     *
53     * @return array
54     */
55    public function stop()
56    {
57        static $fetchedLines = [];
58
59        $dbgData = phpdbg_end_oplog();
60
61        if ($fetchedLines == []) {
62            $sourceLines = phpdbg_get_executable();
63        } else {
64            $newFiles = array_diff(
65                get_included_files(),
66                array_keys($fetchedLines)
67            );
68
69            if ($newFiles) {
70                $sourceLines = phpdbg_get_executable(
71                    ['files' => $newFiles]
72                );
73            } else {
74                $sourceLines = [];
75            }
76        }
77
78        foreach ($sourceLines as $file => $lines) {
79            foreach ($lines as $lineNo => $numExecuted) {
80                $sourceLines[$file][$lineNo] = self::LINE_NOT_EXECUTED;
81            }
82        }
83
84        $fetchedLines = array_merge($fetchedLines, $sourceLines);
85
86        return $this->detectExecutedLines($fetchedLines, $dbgData);
87    }
88
89    /**
90     * Convert phpdbg based data into the format CodeCoverage expects
91     *
92     * @param array $sourceLines
93     * @param array $dbgData
94     *
95     * @return array
96     */
97    private function detectExecutedLines(array $sourceLines, array $dbgData)
98    {
99        foreach ($dbgData as $file => $coveredLines) {
100            foreach ($coveredLines as $lineNo => $numExecuted) {
101                // phpdbg also reports $lineNo=0 when e.g. exceptions get thrown.
102                // make sure we only mark lines executed which are actually executable.
103                if (isset($sourceLines[$file][$lineNo])) {
104                    $sourceLines[$file][$lineNo] = self::LINE_EXECUTED;
105                }
106            }
107        }
108
109        return $sourceLines;
110    }
111}
112