ProcessedCodeCoverageData.php 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267
  1. <?php declare(strict_types=1);
  2. /*
  3. * This file is part of phpunit/php-code-coverage.
  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. namespace SebastianBergmann\CodeCoverage;
  11. use function array_key_exists;
  12. use function array_keys;
  13. use function array_merge;
  14. use function array_unique;
  15. use function count;
  16. use function is_array;
  17. use function ksort;
  18. use SebastianBergmann\CodeCoverage\Driver\Driver;
  19. /**
  20. * @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
  21. */
  22. final class ProcessedCodeCoverageData
  23. {
  24. /**
  25. * Line coverage data.
  26. * An array of filenames, each having an array of linenumbers, each executable line having an array of testcase ids.
  27. *
  28. * @var array
  29. */
  30. private $lineCoverage = [];
  31. /**
  32. * Function coverage data.
  33. * Maintains base format of raw data (@see https://xdebug.org/docs/code_coverage), but each 'hit' entry is an array
  34. * of testcase ids.
  35. *
  36. * @var array
  37. */
  38. private $functionCoverage = [];
  39. public function initializeUnseenData(RawCodeCoverageData $rawData): void
  40. {
  41. foreach ($rawData->lineCoverage() as $file => $lines) {
  42. if (!isset($this->lineCoverage[$file])) {
  43. $this->lineCoverage[$file] = [];
  44. foreach ($lines as $k => $v) {
  45. $this->lineCoverage[$file][$k] = $v === Driver::LINE_NOT_EXECUTABLE ? null : [];
  46. }
  47. }
  48. }
  49. foreach ($rawData->functionCoverage() as $file => $functions) {
  50. foreach ($functions as $functionName => $functionData) {
  51. if (isset($this->functionCoverage[$file][$functionName])) {
  52. $this->initPreviouslySeenFunction($file, $functionName, $functionData);
  53. } else {
  54. $this->initPreviouslyUnseenFunction($file, $functionName, $functionData);
  55. }
  56. }
  57. }
  58. }
  59. public function markCodeAsExecutedByTestCase(string $testCaseId, RawCodeCoverageData $executedCode): void
  60. {
  61. foreach ($executedCode->lineCoverage() as $file => $lines) {
  62. foreach ($lines as $k => $v) {
  63. if ($v === Driver::LINE_EXECUTED) {
  64. $this->lineCoverage[$file][$k][] = $testCaseId;
  65. }
  66. }
  67. }
  68. foreach ($executedCode->functionCoverage() as $file => $functions) {
  69. foreach ($functions as $functionName => $functionData) {
  70. foreach ($functionData['branches'] as $branchId => $branchData) {
  71. if ($branchData['hit'] === Driver::BRANCH_HIT) {
  72. $this->functionCoverage[$file][$functionName]['branches'][$branchId]['hit'][] = $testCaseId;
  73. }
  74. }
  75. foreach ($functionData['paths'] as $pathId => $pathData) {
  76. if ($pathData['hit'] === Driver::BRANCH_HIT) {
  77. $this->functionCoverage[$file][$functionName]['paths'][$pathId]['hit'][] = $testCaseId;
  78. }
  79. }
  80. }
  81. }
  82. }
  83. public function setLineCoverage(array $lineCoverage): void
  84. {
  85. $this->lineCoverage = $lineCoverage;
  86. }
  87. public function lineCoverage(): array
  88. {
  89. ksort($this->lineCoverage);
  90. return $this->lineCoverage;
  91. }
  92. public function setFunctionCoverage(array $functionCoverage): void
  93. {
  94. $this->functionCoverage = $functionCoverage;
  95. }
  96. public function functionCoverage(): array
  97. {
  98. ksort($this->functionCoverage);
  99. return $this->functionCoverage;
  100. }
  101. public function coveredFiles(): array
  102. {
  103. ksort($this->lineCoverage);
  104. return array_keys($this->lineCoverage);
  105. }
  106. public function renameFile(string $oldFile, string $newFile): void
  107. {
  108. $this->lineCoverage[$newFile] = $this->lineCoverage[$oldFile];
  109. if (isset($this->functionCoverage[$oldFile])) {
  110. $this->functionCoverage[$newFile] = $this->functionCoverage[$oldFile];
  111. }
  112. unset($this->lineCoverage[$oldFile], $this->functionCoverage[$oldFile]);
  113. }
  114. public function removeFilesWithNoCoverage(): void
  115. {
  116. foreach ($this->lineCoverage as $file => $lines) {
  117. foreach ($lines as $line) {
  118. if (is_array($line) && !empty($line)) {
  119. continue 2;
  120. }
  121. }
  122. unset($file);
  123. }
  124. }
  125. public function merge(self $newData): void
  126. {
  127. foreach ($newData->lineCoverage as $file => $lines) {
  128. if (!isset($this->lineCoverage[$file])) {
  129. $this->lineCoverage[$file] = $lines;
  130. continue;
  131. }
  132. // we should compare the lines if any of two contains data
  133. $compareLineNumbers = array_unique(
  134. array_merge(
  135. array_keys($this->lineCoverage[$file]),
  136. array_keys($newData->lineCoverage[$file])
  137. )
  138. );
  139. foreach ($compareLineNumbers as $line) {
  140. $thatPriority = $this->priorityForLine($newData->lineCoverage[$file], $line);
  141. $thisPriority = $this->priorityForLine($this->lineCoverage[$file], $line);
  142. if ($thatPriority > $thisPriority) {
  143. $this->lineCoverage[$file][$line] = $newData->lineCoverage[$file][$line];
  144. } elseif ($thatPriority === $thisPriority && is_array($this->lineCoverage[$file][$line])) {
  145. $this->lineCoverage[$file][$line] = array_unique(
  146. array_merge($this->lineCoverage[$file][$line], $newData->lineCoverage[$file][$line])
  147. );
  148. }
  149. }
  150. }
  151. foreach ($newData->functionCoverage as $file => $functions) {
  152. if (!isset($this->functionCoverage[$file])) {
  153. $this->functionCoverage[$file] = $functions;
  154. continue;
  155. }
  156. foreach ($functions as $functionName => $functionData) {
  157. if (isset($this->functionCoverage[$file][$functionName])) {
  158. $this->initPreviouslySeenFunction($file, $functionName, $functionData);
  159. } else {
  160. $this->initPreviouslyUnseenFunction($file, $functionName, $functionData);
  161. }
  162. foreach ($functionData['branches'] as $branchId => $branchData) {
  163. $this->functionCoverage[$file][$functionName]['branches'][$branchId]['hit'] = array_unique(array_merge($this->functionCoverage[$file][$functionName]['branches'][$branchId]['hit'], $branchData['hit']));
  164. }
  165. foreach ($functionData['paths'] as $pathId => $pathData) {
  166. $this->functionCoverage[$file][$functionName]['paths'][$pathId]['hit'] = array_unique(array_merge($this->functionCoverage[$file][$functionName]['paths'][$pathId]['hit'], $pathData['hit']));
  167. }
  168. }
  169. }
  170. }
  171. /**
  172. * Determine the priority for a line.
  173. *
  174. * 1 = the line is not set
  175. * 2 = the line has not been tested
  176. * 3 = the line is dead code
  177. * 4 = the line has been tested
  178. *
  179. * During a merge, a higher number is better.
  180. */
  181. private function priorityForLine(array $data, int $line): int
  182. {
  183. if (!array_key_exists($line, $data)) {
  184. return 1;
  185. }
  186. if (is_array($data[$line]) && count($data[$line]) === 0) {
  187. return 2;
  188. }
  189. if ($data[$line] === null) {
  190. return 3;
  191. }
  192. return 4;
  193. }
  194. /**
  195. * For a function we have never seen before, copy all data over and simply init the 'hit' array.
  196. */
  197. private function initPreviouslyUnseenFunction(string $file, string $functionName, array $functionData): void
  198. {
  199. $this->functionCoverage[$file][$functionName] = $functionData;
  200. foreach (array_keys($functionData['branches']) as $branchId) {
  201. $this->functionCoverage[$file][$functionName]['branches'][$branchId]['hit'] = [];
  202. }
  203. foreach (array_keys($functionData['paths']) as $pathId) {
  204. $this->functionCoverage[$file][$functionName]['paths'][$pathId]['hit'] = [];
  205. }
  206. }
  207. /**
  208. * For a function we have seen before, only copy over and init the 'hit' array for any unseen branches and paths.
  209. * Techniques such as mocking and where the contents of a file are different vary during tests (e.g. compiling
  210. * containers) mean that the functions inside a file cannot be relied upon to be static.
  211. */
  212. private function initPreviouslySeenFunction(string $file, string $functionName, array $functionData): void
  213. {
  214. foreach ($functionData['branches'] as $branchId => $branchData) {
  215. if (!isset($this->functionCoverage[$file][$functionName]['branches'][$branchId])) {
  216. $this->functionCoverage[$file][$functionName]['branches'][$branchId] = $branchData;
  217. $this->functionCoverage[$file][$functionName]['branches'][$branchId]['hit'] = [];
  218. }
  219. }
  220. foreach ($functionData['paths'] as $pathId => $pathData) {
  221. if (!isset($this->functionCoverage[$file][$functionName]['paths'][$pathId])) {
  222. $this->functionCoverage[$file][$functionName]['paths'][$pathId] = $pathData;
  223. $this->functionCoverage[$file][$functionName]['paths'][$pathId]['hit'] = [];
  224. }
  225. }
  226. }
  227. }