diff --git a/src/CodeCoverage.php b/src/CodeCoverage.php index 3933be6af..f87a15aba 100644 --- a/src/CodeCoverage.php +++ b/src/CodeCoverage.php @@ -348,17 +348,29 @@ public function append(array $data, $id = null, bool $append = true, $linesToBeC $this->tests[$id] = ['size' => $size, 'status' => $status]; - foreach ($data as $file => $lines) { + foreach ($data as $file => $fileData) { if (!$this->filter->isFile($file)) { continue; } - foreach ($lines as $k => $v) { - if ($v === Driver::LINE_EXECUTED) { - if (empty($this->data[$file][$k]) || !\in_array($id, $this->data[$file][$k])) { - $this->data[$file][$k][] = $id; + foreach ($fileData['lines'] as $line => $lineCoverage) { + if ($lineCoverage === Driver::LINE_EXECUTED) { + $this->addCoverageLinePathCovered($file, $line, true); + $this->addCoverageLineTest($file, $line, $id); + } + } + + foreach ($fileData['functions'] as $function => $functionCoverage) { + foreach ($functionCoverage['branches'] as $branch => $branchCoverage) { + if (($branchCoverage['hit'] ?? 0) === 1) { + $this->addCoverageBranchHit($file, $function, $branch, $branchCoverage['hit'] ?? 0); + $this->addCoverageBranchTest($file, $function, $branch, $id); } } + + foreach ($functionCoverage['paths'] as $path => $pathCoverage) { + $this->addCoveragePathHit($file, $function, $path, $pathCoverage['hit'] ?? 0); + } } } @@ -376,10 +388,13 @@ public function merge(self $that): void \array_merge($this->filter->getWhitelistedFiles(), $that->filter()->getWhitelistedFiles()) ); - foreach ($that->data as $file => $lines) { - if (!isset($this->data[$file])) { + $thisData = $this->getData(); + $thatData = $that->getData(); + + foreach ($thatData as $file => $fileData) { + if (!isset($thisData[$file])) { if (!$this->filter->isFiltered($file)) { - $this->data[$file] = $lines; + $thisData[$file] = $fileData; } continue; @@ -388,27 +403,30 @@ public function merge(self $that): void // we should compare the lines if any of two contains data $compareLineNumbers = \array_unique( \array_merge( - \array_keys($this->data[$file]), - \array_keys($that->data[$file]) + \array_keys($thisData[$file]['lines']), + \array_keys($thatData[$file]['lines']) // can this be $fileData? ) ); foreach ($compareLineNumbers as $line) { - $thatPriority = $this->getLinePriority($that->data[$file], $line); - $thisPriority = $this->getLinePriority($this->data[$file], $line); + $thatPriority = $this->getLinePriority($thatData[$file]['lines'], $line); + $thisPriority = $this->getLinePriority($thisData[$file]['lines'], $line); if ($thatPriority > $thisPriority) { - $this->data[$file][$line] = $that->data[$file][$line]; - } elseif ($thatPriority === $thisPriority && \is_array($this->data[$file][$line])) { - $this->data[$file][$line] = \array_unique( - \array_merge($this->data[$file][$line], $that->data[$file][$line]) + $thisData[$file]['lines'][$line] = $thatData[$file]['lines'][$line]; + } elseif ($thatPriority === $thisPriority && \is_array($thisData[$file]['lines'][$line])) { + if ($line['pathCovered'] === true) { + $thisData[$file]['lines'][$line]['pathCovered'] = $line['pathCovered']; + } + $thisData[$file]['lines'][$line] = \array_unique( + \array_merge($thisData[$file]['lines'][$line], $thatData[$file]['lines'][$line]) ); } } } - $this->tests = \array_merge($this->tests, $that->getTests()); - $this->report = null; + $this->tests = \array_merge($this->tests, $that->getTests()); + $this->setData($thisData); } public function setCacheTokens(bool $flag): void @@ -493,12 +511,9 @@ public function setDetermineBranchCoverage(bool $flag): void * * During a merge, a higher number is better. * - * @param array $data - * @param int $line - * * @return int */ - private function getLinePriority($data, $line) + private function getLinePriority(array $data, int $line) { if (!\array_key_exists($line, $data)) { return 1; @@ -533,7 +548,10 @@ private function applyCoversAnnotationFilter(array &$data, $linesToBeCovered, ar throw new MissingCoversAnnotationException; } - $data = []; + $data = [ + 'lines' => [], + 'functions' => [], + ]; return; } @@ -544,7 +562,7 @@ private function applyCoversAnnotationFilter(array &$data, $linesToBeCovered, ar if ($this->checkForUnintentionallyCoveredCode && (!$this->currentId instanceof TestCase || - (!$this->currentId->isMedium() && !$this->currentId->isLarge()))) { + (!$this->currentId->isMedium() && !$this->currentId->isLarge()))) { $this->performUnintentionallyCoveredCodeCheck($data, $linesToBeCovered, $linesToBeUsed); } @@ -556,7 +574,11 @@ private function applyCoversAnnotationFilter(array &$data, $linesToBeCovered, ar foreach (\array_keys($data) as $filename) { $_linesToBeCovered = \array_flip($linesToBeCovered[$filename]); - $data[$filename] = \array_intersect_key($data[$filename], $_linesToBeCovered); + + $data[$filename]['lines'] = \array_intersect_key( + $data[$filename], + $_linesToBeCovered + ); } } @@ -580,22 +602,208 @@ private function applyIgnoredLinesFilter(array &$data): void } foreach ($this->getLinesToBeIgnored($filename) as $line) { - unset($data[$filename][$line]); + unset($data[$filename]['lines'][$line]); } } } private function initializeFilesThatAreSeenTheFirstTime(array $data): void { - foreach ($data as $file => $lines) { - if (!isset($this->data[$file]) && $this->filter->isFile($file)) { - $this->data[$file] = []; + foreach ($data as $file => $fileData) { + if (isset($this->data[$file]) || !$this->filter->isFile($file)) { + continue; + } + $this->initializeFileCoverageData($file); - foreach ($lines as $k => $v) { - $this->data[$file][$k] = $v === -2 ? null : []; + // If this particular line is identified as not covered, mark it as null + foreach ($fileData['lines'] as $lineNumber => $flag) { + if ($flag === Driver::LINE_NOT_EXECUTABLE) { + $this->data[$file]['lines'][$lineNumber] = null; } } + + foreach ($fileData['functions'] as $functionName => $functionData) { + // @todo - should this have a helper to merge covered paths? + $this->data[$file]['paths'][$functionName] = $functionData['paths']; + + foreach ($functionData['branches'] as $branchIndex => $branchData) { + $this->addCoverageBranchHit($file, $functionName, $branchIndex, $branchData['hit']); + $this->addCoverageBranchLineStart($file, $functionName, $branchIndex, $branchData['line_start']); + $this->addCoverageBranchLineEnd($file, $functionName, $branchIndex, $branchData['line_end']); + + for ($curLine = $branchData['line_start']; $curLine < $branchData['line_end']; $curLine++) { + if (isset($this->data[$file]['lines'][$curLine])) { + $this->addCoverageLinePathCovered($file, $curLine, (bool) $branchData['hit']); + } + } + } + } + } + } + + private function initializeFileCoverageData(string $file): void + { + if (!isset($this->data[$file]) && $this->filter->isFile($file)) { + $this->data[$file] = [ + 'lines' => [], + 'branches' => [], + 'paths' => [], + ]; + } + } + + private function addCoverageLinePathCovered(string $file, int $lineNumber, bool $isCovered): void + { + $this->initializeFileCoverageData($file); + + // Initialize the data coverage array for this line + if (!isset($this->data[$file]['lines'][$lineNumber])) { + $this->data[$file]['lines'][$lineNumber] = [ + 'pathCovered' => false, + 'tests' => [], + ]; + } + + $this->data[$file]['lines'][$lineNumber]['pathCovered'] = $isCovered; + } + + private function addCoverageLineTest(string $file, int $lineNumber, string $testId): void + { + $this->initializeFileCoverageData($file); + + // Initialize the data coverage array for this line + if (!isset($this->data[$file]['lines'][$lineNumber])) { + $this->data[$file]['lines'][$lineNumber] = [ + 'pathCovered' => false, + 'tests' => [], + ]; + } + + if (!\in_array($testId, $this->data[$file]['lines'][$lineNumber]['tests'], true)) { + $this->data[$file]['lines'][$lineNumber]['tests'][] = $testId; + } + } + + private function addCoverageBranchHit(string $file, string $functionName, int $branchIndex, int $hit): void + { + $this->initializeFileCoverageData($file); + + if (!\array_key_exists($functionName, $this->data[$file]['branches'])) { + $this->data[$file]['branches'][$functionName] = []; + } + + if (!\array_key_exists($branchIndex, $this->data[$file]['branches'][$functionName])) { + $this->data[$file]['branches'][$functionName][$branchIndex] = [ + 'hit' => 0, + 'line_start' => 0, + 'line_end' => 0, + 'tests' => [], + ]; } + + $this->data[$file]['branches'][$functionName][$branchIndex]['hit'] = \max( + $this->data[$file]['branches'][$functionName][$branchIndex]['hit'], + $hit + ); + } + + private function addCoverageBranchLineStart( + string $file, + string $functionName, + int $branchIndex, + int $lineStart + ): void { + $this->initializeFileCoverageData($file); + + if (!\array_key_exists($functionName, $this->data[$file]['branches'])) { + $this->data[$file]['branches'][$functionName] = []; + } + + if (!\array_key_exists($branchIndex, $this->data[$file]['branches'][$functionName])) { + $this->data[$file]['branches'][$functionName][$branchIndex] = [ + 'hit' => 0, + 'line_start' => 0, + 'line_end' => 0, + 'tests' => [], + ]; + } + + $this->data[$file]['branches'][$functionName][$branchIndex]['line_start'] = $lineStart; + } + + private function addCoverageBranchLineEnd( + string $file, + string $functionName, + int $branchIndex, + int $lineEnd + ): void { + $this->initializeFileCoverageData($file); + + if (!\array_key_exists($functionName, $this->data[$file]['branches'])) { + $this->data[$file]['branches'][$functionName] = []; + } + + if (!\array_key_exists($branchIndex, $this->data[$file]['branches'][$functionName])) { + $this->data[$file]['branches'][$functionName][$branchIndex] = [ + 'hit' => 0, + 'line_start' => 0, + 'line_end' => 0, + 'tests' => [], + ]; + } + + $this->data[$file]['branches'][$functionName][$branchIndex]['line_end'] = $lineEnd; + } + + private function addCoverageBranchTest( + string $file, + string $functionName, + int $branchIndex, + string $testId + ): void { + $this->initializeFileCoverageData($file); + + if (!\array_key_exists($functionName, $this->data[$file]['branches'])) { + $this->data[$file]['branches'][$functionName] = []; + } + + if (!\array_key_exists($branchIndex, $this->data[$file]['branches'][$functionName])) { + $this->data[$file]['branches'][$functionName][$branchIndex] = [ + 'hit' => 0, + 'line_start' => 0, + 'line_end' => 0, + 'tests' => [], + ]; + } + + if (!\in_array($testId, $this->data[$file]['branches'][$functionName][$branchIndex]['tests'], true)) { + $this->data[$file]['branches'][$functionName][$branchIndex]['tests'][] = $testId; + } + } + + private function addCoveragePathHit( + string $file, + string $functionName, + int $pathId, + int $hit + ): void { + $this->initializeFileCoverageData($file); + + if (!\array_key_exists($functionName, $this->data[$file]['paths'])) { + $this->data[$file]['paths'][$functionName] = []; + } + + if (!\array_key_exists($pathId, $this->data[$file]['paths'][$functionName])) { + $this->data[$file]['paths'][$functionName][$pathId] = [ + 'hit' => 0, + 'path' => [], + ]; + } + + $this->data[$file]['paths'][$functionName][$pathId]['hit'] = \max( + $this->data[$file]['paths'][$functionName][$pathId]['hit'], + $hit + ); } /** @@ -619,13 +827,17 @@ private function addUncoveredFilesFromWhitelist(): void continue; } - $data[$uncoveredFile] = []; + $data[$uncoveredFile] = [ + 'lines' => [], + 'functions' => [], + ]; $lines = \count(\file($uncoveredFile)); - for ($i = 1; $i <= $lines; $i++) { - $data[$uncoveredFile][$i] = Driver::LINE_NOT_EXECUTED; + for ($line = 1; $line <= $lines; $line++) { + $data[$uncoveredFile]['lines'][$line] = Driver::LINE_NOT_EXECUTED; } + // @todo - do the same here with functions and paths } $this->append($data, 'UNCOVERED_FILES_FROM_WHITELIST'); @@ -817,10 +1029,10 @@ private function performUnintentionallyCoveredCodeCheck(array &$data, array $lin $unintentionallyCoveredUnits = []; - foreach ($data as $file => $_data) { - foreach ($_data as $line => $flag) { - if ($flag === 1 && !isset($allowedLines[$file][$line])) { - $unintentionallyCoveredUnits[] = $this->wizard->lookup($file, $line); + foreach ($data as $file => $fileData) { + foreach ($fileData['lines'] as $lineNumber => $flag) { + if ($flag === 1 && !isset($allowedLines[$file][$lineNumber])) { + $unintentionallyCoveredUnits[] = $this->wizard->lookup($file, $lineNumber); } } } diff --git a/src/Driver/PHPDBG.php b/src/Driver/PHPDBG.php index 42053af00..ee110732f 100644 --- a/src/Driver/PHPDBG.php +++ b/src/Driver/PHPDBG.php @@ -1,5 +1,5 @@ @@ -101,8 +101,8 @@ private function detectExecutedLines(array $sourceLines, array $dbgData): array foreach ($sourceLines as $file => $lines) { $sourceLines[$file] = [ - 'lines' => $lines, - 'functions' => [] + 'lines' => $lines, + 'functions' => [], ]; } diff --git a/src/Driver/Xdebug.php b/src/Driver/Xdebug.php index 7b7a716cd..7a97cc8a2 100644 --- a/src/Driver/Xdebug.php +++ b/src/Driver/Xdebug.php @@ -89,6 +89,7 @@ private function cleanup(array $data): array if (!isset($data[$file]['lines'])) { $data[$file] = ['lines' => $data[$file]]; } + if (!isset($data[$file]['functions'])) { $data[$file]['functions'] = []; }