ProcessedCodeCoverageData.php 10.0 KB

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