|
| 1 | +--- |
| 2 | +outline: [2, 4] |
| 3 | +llms: true |
| 4 | +llms_description: "Code coverage collection during tests. CodecovPlugin configuration with CoverageLevel (Line/Branch/Path), CoverageMode (IfAvailable/Always/Never), CLI flags --coverage/--no-coverage. Clover and Cobertura XML reports. #[Covers] and #[CoversNothing] attributes for per-test coverage control. Source filtering via FinderConfig." |
| 5 | +--- |
| 6 | + |
| 7 | +# Code Coverage |
| 8 | + |
| 9 | +The plugin collects code coverage data during test execution and generates reports in standard formats. Reports can be used in CI services (Codecov.io, SonarQube, GitHub Actions) and in IDEs — for example, PhpStorm can display coverage directly in your code from a Clover report. |
| 10 | + |
| 11 | +<plugin-info name="Codecov" class="\Testo\Codecov\CodecovPlugin" /> |
| 12 | + |
| 13 | +## Requirements |
| 14 | + |
| 15 | +One of the following PHP extensions is required: |
| 16 | + |
| 17 | +- **[PCOV](https://github.com/krakjoe/pcov)** — lightweight, fast, line coverage only. |
| 18 | +- **[XDebug](https://xdebug.org/)** ≥ 3.0 with `coverage` mode enabled (`xdebug.mode=coverage`). |
| 19 | + |
| 20 | +When both extensions are available, Testo prefers PCOV due to its lower overhead. If neither extension is installed, behavior depends on the plugin's activation mode (<enum>\Testo\Codecov\Config\CoverageMode</enum>). |
| 21 | + |
| 22 | +::: question Which extension is better — PCOV or XDebug? |
| 23 | +PCOV is faster and easier to set up, but only supports line coverage. XDebug is required for branch and path analysis. If <enum>\Testo\Codecov\Config\CoverageLevel::Line</enum> is sufficient for your needs — use PCOV. |
| 24 | +::: |
| 25 | + |
| 26 | +## Setup |
| 27 | + |
| 28 | +Register <class>\Testo\Codecov\CodecovPlugin</class> in the `plugins` section of your configuration: |
| 29 | + |
| 30 | +::: code-group |
| 31 | +```php [Application level] |
| 32 | +return new ApplicationConfig( |
| 33 | + src: ['src'], |
| 34 | + //... |
| 35 | + plugins: [ |
| 36 | + new CodecovPlugin( |
| 37 | + level: CoverageLevel::Line, |
| 38 | + reports: [ |
| 39 | + new CloverReport(__DIR__ . '/clover.xml', 'MyProject'), |
| 40 | + new CoberturaReport(__DIR__ . '/cobertura.xml'), |
| 41 | + ], |
| 42 | + ), |
| 43 | + ], |
| 44 | +); |
| 45 | +``` |
| 46 | +```php [Test Suite level] |
| 47 | +return new ApplicationConfig( |
| 48 | + src: ['src'], |
| 49 | + suites: [ |
| 50 | + new SuiteConfig( |
| 51 | + // ... |
| 52 | + plugins: [ |
| 53 | + new CodecovPlugin( |
| 54 | + reports: [ |
| 55 | + new CloverReport(__DIR__ . '/clover.xml', 'MyProject'), |
| 56 | + ], |
| 57 | + ), |
| 58 | + ], |
| 59 | + ), |
| 60 | + ], |
| 61 | +); |
| 62 | +``` |
| 63 | +::: |
| 64 | + |
| 65 | +At the application level, coverage is collected across all test suites. At the test suite level — only for that specific suite. Reports are generated after tests complete. Coverage is filtered to files matching the `src` parameter from <class>\Testo\Application\Config\ApplicationConfig</class>. |
| 66 | + |
| 67 | +<signature h="3" name="new \Testo\Codecov\CodecovPlugin(CoverageLevel $level = CoverageLevel::Line, CoverageMode $collect = CoverageMode::IfAvailable, array $testTypes = [TestType::Test, TestType::TestInline], array $reports = [])"> |
| 68 | +<short>Configures code coverage collection: analysis depth, activation mode, and report formats.</short> |
| 69 | +<param name="$level">Coverage analysis depth. Defaults to <enum>\Testo\Codecov\Config\CoverageLevel::Line</enum>.</param> |
| 70 | +<param name="$collect">Default activation mode. CLI flags (`--coverage`, `--no-coverage`) take priority over this value.</param> |
| 71 | +<param name="$testTypes">Test types to collect coverage for. Coverage collection adds overhead to each run, so benchmarks are excluded by default — otherwise performance measurements would be skewed. By default, coverage is collected only for regular tests and inline tests (<enum>\Testo\Core\Value\TestType::Test</enum>, <enum>\Testo\Core\Value\TestType::TestInline</enum>). An empty array means all types. Accepts <enum>\Testo\Core\Value\TestType</enum> cases or custom string identifiers.</param> |
| 72 | +<param name="$reports">Report generators to run after all tests complete. Each element must implement the <class>\Testo\Codecov\Report\CoverageReport</class> interface.</param> |
| 73 | +</signature> |
| 74 | + |
| 75 | +<signature h="3" name="enum \Testo\Codecov\Config\CoverageLevel"> |
| 76 | +<short>Defines the depth of coverage analysis. Each successive level includes all data from the previous one.</short> |
| 77 | +<description> |
| 78 | +Each successive level increases analysis overhead. PCOV only supports `Line` — when a deeper level is requested, it silently falls back to `Line`. |
| 79 | + |
| 80 | +**Example.** Consider the following code: |
| 81 | + |
| 82 | +```php |
| 83 | +function greet(bool $loud, bool $formal): string |
| 84 | +{ |
| 85 | + $greeting = $formal ? 'Good day' : 'Hi'; // 2 branches |
| 86 | + return $loud ? strtoupper($greeting) : $greeting; // 2 branches |
| 87 | +} |
| 88 | +``` |
| 89 | + |
| 90 | +A test calling `greet(true, true)`: |
| 91 | + |
| 92 | +- **Line** — 100%: both lines executed. |
| 93 | +- **Branch** — 50%: 2 of 4 branches taken. |
| 94 | +- **Path** — 25%: 1 of 4 paths followed (true+true, true+false, false+true, false+false). |
| 95 | +</description> |
| 96 | +<case name="Line">Which source lines were executed. Supported by both PCOV and XDebug.</case> |
| 97 | +<case name="Branch">Line + which branches (`if/else`, `switch`, `?:`, `??`) were taken. XDebug only.</case> |
| 98 | +<case name="Path">Branch + which complete execution paths through each function were followed. XDebug only.</case> |
| 99 | +</signature> |
| 100 | + |
| 101 | +<signature h="3" name="enum \Testo\Codecov\Config\CoverageMode"> |
| 102 | +<short>Controls whether coverage is collected.</short> |
| 103 | +<description> |
| 104 | +The default behavior is set by the `collect` parameter of the <class>\Testo\Codecov\CodecovPlugin</class> constructor, and CLI flags can override it at runtime. This means the plugin can safely remain in `testo.php` across all environments — on CI without PCOV/XDebug, tests will run normally, just without reports. |
| 105 | +</description> |
| 106 | +<case name="IfAvailable">**Default.** Coverage is collected if an extension is available and configured, otherwise silently skipped.</case> |
| 107 | +<case name="Always">Coverage is mandatory. If no extension is installed, tests will fail with a <class>\Testo\Codecov\Exception\CoverageDriverNotAvailable</class> exception. Set by the `--coverage` CLI flag.</case> |
| 108 | +<case name="Never">Coverage is fully disabled, zero overhead. Set by the `--no-coverage` CLI flag.</case> |
| 109 | +</signature> |
| 110 | + |
| 111 | +::: question What happens if no coverage extension is installed? |
| 112 | +It depends on the activation mode. By default, <enum>\Testo\Codecov\Config\CoverageMode::IfAvailable</enum> is used — the plugin silently skips coverage collection and tests run without it. If you run with the `--coverage` flag, the mode switches to <enum>\Testo\Codecov\Config\CoverageMode::Always</enum>, and tests will fail with a <class>\Testo\Codecov\Exception\CoverageDriverNotAvailable</class> exception. |
| 113 | +::: |
| 114 | + |
| 115 | +### Reports |
| 116 | + |
| 117 | +All built-in report generators implement the <class>\Testo\Codecov\Report\CoverageReport</class> interface. You can implement it to add your own output format. |
| 118 | + |
| 119 | +<signature h="4" name="new \Testo\Codecov\Report\CloverReport(string $outputPath, string $projectName = '')"> |
| 120 | +<short>Generates a Clover XML report.</short> |
| 121 | +<description> |
| 122 | +The format contains `<file>`, `<line>`, and `<metrics>` elements — line-level statement coverage only. Branch and path data is not included, as the format does not support it. |
| 123 | + |
| 124 | +Compatible with: Codecov.io, SonarQube, Atlassian Clover. |
| 125 | +</description> |
| 126 | +<param name="$outputPath">Absolute path to the output XML file.</param> |
| 127 | +<param name="$projectName">Project name in the `<project>` element. Defaults to an empty string.</param> |
| 128 | +<example> |
| 129 | +```php |
| 130 | +new CloverReport(__DIR__ . '/clover.xml', 'MyProject') |
| 131 | +``` |
| 132 | +</example> |
| 133 | +</signature> |
| 134 | + |
| 135 | +<signature h="4" name="new \Testo\Codecov\Report\CoberturaReport(string $outputPath, string $sourceRoot = '')"> |
| 136 | +<short>Generates a Cobertura XML report.</short> |
| 137 | +<description> |
| 138 | +Files are grouped into `<package>` elements by directory, with relative paths from `sourceRoot`. |
| 139 | + |
| 140 | +When branch data is available (<enum>\Testo\Codecov\Config\CoverageLevel::Branch</enum> or higher): |
| 141 | + |
| 142 | +- `branch-rate`, `branches-covered`, `branches-valid` are populated at all levels (coverage, package, class). |
| 143 | +- Lines with branch points get `branch="true"` and `condition-coverage="50% (1/2)"` attributes. |
| 144 | + |
| 145 | +Without branch data, all branch attributes are `0`. |
| 146 | + |
| 147 | +Compatible with: GitHub Actions, GitLab CI, Jenkins. |
| 148 | +</description> |
| 149 | +<param name="$outputPath">Absolute path to the output XML file.</param> |
| 150 | +<param name="$sourceRoot">Source root for relative file paths. Defaults to `getcwd()`.</param> |
| 151 | +<example> |
| 152 | +```php |
| 153 | +new CoberturaReport(__DIR__ . '/cobertura.xml') |
| 154 | +``` |
| 155 | +</example> |
| 156 | +</signature> |
| 157 | + |
| 158 | +## Coverage Control |
| 159 | + |
| 160 | +The `src` parameter in the <class>\Testo\Application\Config\ApplicationConfig</class> configuration defines the global set of files included in coverage. The <attr>\Testo\Codecov\Covers</attr> and <attr>\Testo\Codecov\CoversNothing</attr> attributes allow fine-grained control over coverage for individual tests. |
| 161 | + |
| 162 | +### Global Filter |
| 163 | + |
| 164 | +Includes and excludes are supported via <class>\Testo\Application\Config\FinderConfig</class>: |
| 165 | + |
| 166 | +```php |
| 167 | +return new ApplicationConfig( |
| 168 | + src: new FinderConfig( |
| 169 | + include: ['src'], |
| 170 | + exclude: ['src/Generated'], |
| 171 | + ), |
| 172 | + // ... |
| 173 | +); |
| 174 | +``` |
| 175 | + |
| 176 | +::: tip |
| 177 | +Include only the directories you need in `src` to filter out unnecessary files before they are even loaded. This gives the best performance. |
| 178 | +::: |
| 179 | + |
| 180 | +<signature h="3" name="#[\Testo\Codecov\Covers(string $classOrFunction, ?string $method = null)]"> |
| 181 | +<short>Restricts which source code counts toward coverage for this test.</short> |
| 182 | +<description> |
| 183 | +Only lines belonging to the specified classes, traits, enums, methods, or functions will be included in the report. Everything else is discarded. The attribute is repeatable: multiple <attr>\Testo\Codecov\Covers</attr> on the same test are combined. |
| 184 | + |
| 185 | +When placed on a class, applies to all tests within it. |
| 186 | +</description> |
| 187 | +<param name="$classOrFunction">Fully qualified class, trait, enum (`UserService::class`, `Cacheable::class`, `OrderStatus::class`), or function name (`'App\Helpers\format_name'`).</param> |
| 188 | +<param name="$method">Method name within the class, trait, or enum. When specified, only lines of that method are included, not the entire entity.</param> |
| 189 | +<example> |
| 190 | +Coverage for a class, trait, or enum — all executable lines: |
| 191 | + |
| 192 | +```php |
| 193 | +#[Covers(UserService::class)] |
| 194 | +public function testCreateUser(): void { ... } |
| 195 | + |
| 196 | +#[Covers(Cacheable::class)] |
| 197 | +public function testCacheableBehavior(): void { ... } |
| 198 | + |
| 199 | +#[Covers(OrderStatus::class)] |
| 200 | +public function testOrderStatusTransitions(): void { ... } |
| 201 | +``` |
| 202 | +</example> |
| 203 | +<example> |
| 204 | +Coverage for a specific method — works with classes, traits, and enums: |
| 205 | + |
| 206 | +```php |
| 207 | +#[Covers(UserService::class, 'create')] |
| 208 | +public function testCreateUser(): void { ... } |
| 209 | + |
| 210 | +#[Covers(Cacheable::class, 'invalidate')] |
| 211 | +public function testCacheInvalidation(): void { ... } |
| 212 | + |
| 213 | +#[Covers(OrderStatus::class, 'canTransitionTo')] |
| 214 | +public function testStatusTransition(): void { ... } |
| 215 | +``` |
| 216 | +</example> |
| 217 | +<example> |
| 218 | +Multiple targets — coverage is combined: |
| 219 | + |
| 220 | +```php |
| 221 | +#[Covers(UserService::class)] |
| 222 | +#[Covers(UserRepository::class, 'findById')] |
| 223 | +public function testCreateUser(): void { ... } |
| 224 | +``` |
| 225 | +</example> |
| 226 | +</signature> |
| 227 | + |
| 228 | +<signature h="3" name="#[\Testo\Codecov\CoversNothing]"> |
| 229 | +<short>Excludes a test from coverage statistics.</short> |
| 230 | +<description> |
| 231 | +The test runs as usual, but the coverage driver is not started — zero overhead, no data is collected or included in reports. Useful for smoke tests and integration checks that touch a lot of code but shouldn't skew your coverage picture. |
| 232 | + |
| 233 | +When placed on a class, applies to all tests within it. |
| 234 | +</description> |
| 235 | +<example> |
| 236 | +```php |
| 237 | +#[CoversNothing] |
| 238 | +public function smokeTest(): void |
| 239 | +{ |
| 240 | + // Test runs, but coverage is not collected |
| 241 | + $response = $this->app->get('/health'); |
| 242 | + Assert::same(200, $response->statusCode); |
| 243 | +} |
| 244 | +``` |
| 245 | +</example> |
| 246 | +</signature> |
| 247 | + |
| 248 | +### Attribute Priority |
| 249 | + |
| 250 | +Coverage attributes are resolved layer by layer: the method is checked first, then the class. If the method has any coverage attribute, class-level attributes are ignored entirely. This allows overriding behavior in subclasses: |
| 251 | + |
| 252 | +```php |
| 253 | +#[CoversNothing] |
| 254 | +abstract class BaseIntegrationTest |
| 255 | +{ |
| 256 | + // By default, all tests in subclasses skip coverage |
| 257 | +} |
| 258 | + |
| 259 | +#[Covers(PaymentService::class)] |
| 260 | +final class PaymentServiceTest extends BaseIntegrationTest |
| 261 | +{ |
| 262 | + // This subclass overrides the behavior — coverage is collected |
| 263 | + public function testCharge(): void { ... } |
| 264 | +} |
| 265 | +``` |
| 266 | + |
| 267 | +::: warning |
| 268 | +Using <attr>\Testo\Codecov\Covers</attr> and <attr>\Testo\Codecov\CoversNothing</attr> on the **same level** is an error. Testo will throw an exception identifying the conflicting test. Different levels (e.g., <attr>\Testo\Codecov\CoversNothing</attr> on the parent class and <attr>\Testo\Codecov\Covers</attr> on the child) are valid. |
| 269 | +::: |
| 270 | + |
| 271 | +### Metadata |
| 272 | + |
| 273 | +Coverage data for each test is attached to the <class>\Testo\Core\Context\TestResult</class> metadata under the <class>\Testo\Codecov\Result\CoverageResult</class> key: |
| 274 | + |
| 275 | +```php |
| 276 | +use Testo\Codecov\Result\CoverageResult; |
| 277 | + |
| 278 | +$coverage = $testResult->getAttribute(CoverageResult::class); |
| 279 | +// CoverageResult|null |
| 280 | +``` |
| 281 | + |
| 282 | +This allows you to implement custom logic based on coverage data before results are reflected in the reports. |
0 commit comments