* * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\CodeCoverage; use function array_key_exists; use function array_keys; use function array_merge; use function array_unique; use function count; use function is_array; use function ksort; use SebastianBergmann\CodeCoverage\Driver\Driver; /** * @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage */ final class ProcessedCodeCoverageData { /** * Line coverage data. * An array of filenames, each having an array of linenumbers, each executable line having an array of testcase ids. * * @var array */ private $lineCoverage = []; /** * Function coverage data. * Maintains base format of raw data (@see https://xdebug.org/docs/code_coverage), but each 'hit' entry is an array * of testcase ids. * * @var array */ private $functionCoverage = []; public function initializeUnseenData(RawCodeCoverageData $rawData): void { foreach ($rawData->lineCoverage() as $file => $lines) { if (!isset($this->lineCoverage[$file])) { $this->lineCoverage[$file] = []; foreach ($lines as $k => $v) { $this->lineCoverage[$file][$k] = $v === Driver::LINE_NOT_EXECUTABLE ? null : []; } } } foreach ($rawData->functionCoverage() as $file => $functions) { foreach ($functions as $functionName => $functionData) { if (isset($this->functionCoverage[$file][$functionName])) { $this->initPreviouslySeenFunction($file, $functionName, $functionData); } else { $this->initPreviouslyUnseenFunction($file, $functionName, $functionData); } } } } public function markCodeAsExecutedByTestCase(string $testCaseId, RawCodeCoverageData $executedCode): void { foreach ($executedCode->lineCoverage() as $file => $lines) { foreach ($lines as $k => $v) { if ($v === Driver::LINE_EXECUTED) { $this->lineCoverage[$file][$k][] = $testCaseId; } } } foreach ($executedCode->functionCoverage() as $file => $functions) { foreach ($functions as $functionName => $functionData) { foreach ($functionData['branches'] as $branchId => $branchData) { if ($branchData['hit'] === Driver::BRANCH_HIT) { $this->functionCoverage[$file][$functionName]['branches'][$branchId]['hit'][] = $testCaseId; } } foreach ($functionData['paths'] as $pathId => $pathData) { if ($pathData['hit'] === Driver::BRANCH_HIT) { $this->functionCoverage[$file][$functionName]['paths'][$pathId]['hit'][] = $testCaseId; } } } } } public function setLineCoverage(array $lineCoverage): void { $this->lineCoverage = $lineCoverage; } public function lineCoverage(): array { ksort($this->lineCoverage); return $this->lineCoverage; } public function setFunctionCoverage(array $functionCoverage): void { $this->functionCoverage = $functionCoverage; } public function functionCoverage(): array { ksort($this->functionCoverage); return $this->functionCoverage; } public function coveredFiles(): array { ksort($this->lineCoverage); return array_keys($this->lineCoverage); } public function renameFile(string $oldFile, string $newFile): void { $this->lineCoverage[$newFile] = $this->lineCoverage[$oldFile]; if (isset($this->functionCoverage[$oldFile])) { $this->functionCoverage[$newFile] = $this->functionCoverage[$oldFile]; } unset($this->lineCoverage[$oldFile], $this->functionCoverage[$oldFile]); } public function merge(self $newData): void { foreach ($newData->lineCoverage as $file => $lines) { if (!isset($this->lineCoverage[$file])) { $this->lineCoverage[$file] = $lines; continue; } // we should compare the lines if any of two contains data $compareLineNumbers = array_unique( array_merge( array_keys($this->lineCoverage[$file]), array_keys($newData->lineCoverage[$file]) ) ); foreach ($compareLineNumbers as $line) { $thatPriority = $this->priorityForLine($newData->lineCoverage[$file], $line); $thisPriority = $this->priorityForLine($this->lineCoverage[$file], $line); if ($thatPriority > $thisPriority) { $this->lineCoverage[$file][$line] = $newData->lineCoverage[$file][$line]; } elseif ($thatPriority === $thisPriority && is_array($this->lineCoverage[$file][$line])) { $this->lineCoverage[$file][$line] = array_unique( array_merge($this->lineCoverage[$file][$line], $newData->lineCoverage[$file][$line]) ); } } } foreach ($newData->functionCoverage as $file => $functions) { if (!isset($this->functionCoverage[$file])) { $this->functionCoverage[$file] = $functions; continue; } foreach ($functions as $functionName => $functionData) { if (isset($this->functionCoverage[$file][$functionName])) { $this->initPreviouslySeenFunction($file, $functionName, $functionData); } else { $this->initPreviouslyUnseenFunction($file, $functionName, $functionData); } foreach ($functionData['branches'] as $branchId => $branchData) { $this->functionCoverage[$file][$functionName]['branches'][$branchId]['hit'] = array_unique(array_merge($this->functionCoverage[$file][$functionName]['branches'][$branchId]['hit'], $branchData['hit'])); } foreach ($functionData['paths'] as $pathId => $pathData) { $this->functionCoverage[$file][$functionName]['paths'][$pathId]['hit'] = array_unique(array_merge($this->functionCoverage[$file][$functionName]['paths'][$pathId]['hit'], $pathData['hit'])); } } } } /** * Determine the priority for a line. * * 1 = the line is not set * 2 = the line has not been tested * 3 = the line is dead code * 4 = the line has been tested * * During a merge, a higher number is better. */ private function priorityForLine(array $data, int $line): int { if (!array_key_exists($line, $data)) { return 1; } if (is_array($data[$line]) && count($data[$line]) === 0) { return 2; } if ($data[$line] === null) { return 3; } return 4; } /** * For a function we have never seen before, copy all data over and simply init the 'hit' array. */ private function initPreviouslyUnseenFunction(string $file, string $functionName, array $functionData): void { $this->functionCoverage[$file][$functionName] = $functionData; foreach (array_keys($functionData['branches']) as $branchId) { $this->functionCoverage[$file][$functionName]['branches'][$branchId]['hit'] = []; } foreach (array_keys($functionData['paths']) as $pathId) { $this->functionCoverage[$file][$functionName]['paths'][$pathId]['hit'] = []; } } /** * For a function we have seen before, only copy over and init the 'hit' array for any unseen branches and paths. * Techniques such as mocking and where the contents of a file are different vary during tests (e.g. compiling * containers) mean that the functions inside a file cannot be relied upon to be static. */ private function initPreviouslySeenFunction(string $file, string $functionName, array $functionData): void { foreach ($functionData['branches'] as $branchId => $branchData) { if (!isset($this->functionCoverage[$file][$functionName]['branches'][$branchId])) { $this->functionCoverage[$file][$functionName]['branches'][$branchId] = $branchData; $this->functionCoverage[$file][$functionName]['branches'][$branchId]['hit'] = []; } } foreach ($functionData['paths'] as $pathId => $pathData) { if (!isset($this->functionCoverage[$file][$functionName]['paths'][$pathId])) { $this->functionCoverage[$file][$functionName]['paths'][$pathId] = $pathData; $this->functionCoverage[$file][$functionName]['paths'][$pathId]['hit'] = []; } } } }