XdebugDriver.php 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162
  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\Driver;
  11. use const XDEBUG_CC_BRANCH_CHECK;
  12. use const XDEBUG_CC_DEAD_CODE;
  13. use const XDEBUG_CC_UNUSED;
  14. use const XDEBUG_FILTER_CODE_COVERAGE;
  15. use const XDEBUG_PATH_INCLUDE;
  16. use function explode;
  17. use function extension_loaded;
  18. use function getenv;
  19. use function in_array;
  20. use function ini_get;
  21. use function phpversion;
  22. use function version_compare;
  23. use function xdebug_get_code_coverage;
  24. use function xdebug_info;
  25. use function xdebug_set_filter;
  26. use function xdebug_start_code_coverage;
  27. use function xdebug_stop_code_coverage;
  28. use SebastianBergmann\CodeCoverage\Data\RawCodeCoverageData;
  29. use SebastianBergmann\CodeCoverage\Filter;
  30. /**
  31. * @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
  32. *
  33. * @see https://xdebug.org/docs/code_coverage#xdebug_get_code_coverage
  34. *
  35. * @psalm-type XdebugLinesCoverageType = array<int, int>
  36. * @psalm-type XdebugBranchCoverageType = array{
  37. * op_start: int,
  38. * op_end: int,
  39. * line_start: int,
  40. * line_end: int,
  41. * hit: int,
  42. * out: array<int, int>,
  43. * out_hit: array<int, int>,
  44. * }
  45. * @psalm-type XdebugPathCoverageType = array{
  46. * path: array<int, int>,
  47. * hit: int,
  48. * }
  49. * @psalm-type XdebugFunctionCoverageType = array{
  50. * branches: array<int, XdebugBranchCoverageType>,
  51. * paths: array<int, XdebugPathCoverageType>,
  52. * }
  53. * @psalm-type XdebugFunctionsCoverageType = array<string, XdebugFunctionCoverageType>
  54. * @psalm-type XdebugPathAndBranchesCoverageType = array{
  55. * lines: XdebugLinesCoverageType,
  56. * functions: XdebugFunctionsCoverageType,
  57. * }
  58. * @psalm-type XdebugCodeCoverageWithoutPathCoverageType = array<string, XdebugLinesCoverageType>
  59. * @psalm-type XdebugCodeCoverageWithPathCoverageType = array<string, XdebugPathAndBranchesCoverageType>
  60. */
  61. final class XdebugDriver extends Driver
  62. {
  63. /**
  64. * @throws XdebugNotAvailableException
  65. * @throws XdebugNotEnabledException
  66. */
  67. public function __construct(Filter $filter)
  68. {
  69. $this->ensureXdebugIsAvailable();
  70. $this->ensureXdebugCodeCoverageFeatureIsEnabled();
  71. if (!$filter->isEmpty()) {
  72. xdebug_set_filter(
  73. XDEBUG_FILTER_CODE_COVERAGE,
  74. XDEBUG_PATH_INCLUDE,
  75. $filter->files(),
  76. );
  77. }
  78. }
  79. public function canCollectBranchAndPathCoverage(): bool
  80. {
  81. return true;
  82. }
  83. public function canDetectDeadCode(): bool
  84. {
  85. return true;
  86. }
  87. public function start(): void
  88. {
  89. $flags = XDEBUG_CC_UNUSED;
  90. if ($this->detectsDeadCode() || $this->collectsBranchAndPathCoverage()) {
  91. $flags |= XDEBUG_CC_DEAD_CODE;
  92. }
  93. if ($this->collectsBranchAndPathCoverage()) {
  94. $flags |= XDEBUG_CC_BRANCH_CHECK;
  95. }
  96. xdebug_start_code_coverage($flags);
  97. }
  98. public function stop(): RawCodeCoverageData
  99. {
  100. $data = xdebug_get_code_coverage();
  101. xdebug_stop_code_coverage();
  102. if ($this->collectsBranchAndPathCoverage()) {
  103. /* @var XdebugCodeCoverageWithPathCoverageType $data */
  104. return RawCodeCoverageData::fromXdebugWithPathCoverage($data);
  105. }
  106. /* @var XdebugCodeCoverageWithoutPathCoverageType $data */
  107. return RawCodeCoverageData::fromXdebugWithoutPathCoverage($data);
  108. }
  109. public function nameAndVersion(): string
  110. {
  111. return 'Xdebug ' . phpversion('xdebug');
  112. }
  113. /**
  114. * @throws XdebugNotAvailableException
  115. */
  116. private function ensureXdebugIsAvailable(): void
  117. {
  118. if (!extension_loaded('xdebug')) {
  119. throw new XdebugNotAvailableException;
  120. }
  121. }
  122. /**
  123. * @throws XdebugNotEnabledException
  124. */
  125. private function ensureXdebugCodeCoverageFeatureIsEnabled(): void
  126. {
  127. if (version_compare(phpversion('xdebug'), '3.1', '>=')) {
  128. if (!in_array('coverage', xdebug_info('mode'), true)) {
  129. throw new XdebugNotEnabledException;
  130. }
  131. return;
  132. }
  133. $mode = getenv('XDEBUG_MODE');
  134. if ($mode === false || $mode === '') {
  135. $mode = ini_get('xdebug.mode');
  136. }
  137. if ($mode === false ||
  138. !in_array('coverage', explode(',', $mode), true)) {
  139. throw new XdebugNotEnabledException;
  140. }
  141. }
  142. }