Skip to content

Commit 3107a6b

Browse files
committed
Describe Codecov plugin
1 parent 3ff8bb4 commit 3107a6b

7 files changed

Lines changed: 577 additions & 8 deletions

File tree

.vitepress/config.mts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ gtag('config', 'G-VYGDN3X0PR');`],
9696
{ text: 'Bench', link: '/docs/plugins/bench.md' },
9797
{ text: '\#[Test]', link: '/docs/plugins/test.md' },
9898
{ text: 'Lifecycle', link: '/docs/plugins/lifecycle.md' },
99+
{ text: 'Codecov', link: '/docs/plugins/codecov.md' },
99100
{ text: 'Convention', link: '/docs/plugins/convention.md' },
100101
{ text: 'Filter', link: '/docs/plugins/filter.md' },
101102
],
@@ -159,6 +160,7 @@ gtag('config', 'G-VYGDN3X0PR');`],
159160
{ text: 'Bench', link: '/ru/docs/plugins/bench.md' },
160161
{ text: '\#[Test]', link: '/ru/docs/plugins/test.md' },
161162
{ text: 'Lifecycle', link: '/ru/docs/plugins/lifecycle.md' },
163+
{ text: 'Codecov', link: '/ru/docs/plugins/codecov.md' },
162164
{ text: 'Convention', link: '/ru/docs/plugins/convention.md' },
163165
{ text: 'Filter', link: '/ru/docs/plugins/filter.md' },
164166
],

.vitepress/signature.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -242,9 +242,11 @@ function renderEnumRefHtml(md: MarkdownIt, rawFqn: string, locale?: LocaleConfig
242242
return `<span class="${cls}">${displayHtml}${tooltip}</span>`
243243
}
244244

245-
// No registry info at all
246-
const shortDisplay = stripNamespaceShort(enumPart) + '::' + casePart
247-
return `<code>${escapeHtml(shortDisplay)}</code>`
245+
// No registry info — show FQN tooltip like <class> fallback
246+
const enumShort = stripNamespaceShort(enumPart)
247+
const shortDisplay = enumShort + '::' + casePart
248+
const fqnTooltip = `<span class="func-ref-tooltip"><code class="func-ref-tooltip-sig vp-code">${escapeHtml(rawFqn)}</code></span>`
249+
return `<span class="class-ref func-ref vp-code">${escapeHtml(shortDisplay)}${fqnTooltip}</span>`
248250
}
249251

250252
// No :: — just enum class reference, delegate to <class> rendering

CLAUDE.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ ru/ # Russian locale (same structure)
3737
**Cross-references in text:**
3838
- Use `<plugin>Name</plugin>` when referencing a Testo plugin by name (e.g., `<plugin>Assert</plugin>`)
3939
- Use `<class>\FQN</class>` when referencing PHP classes and interfaces (e.g., `<class>\Testo\Event\Test\TestFinished</class>`)
40-
- Use `<enum>\FQN</enum>` when referencing PHP enums (e.g., `<enum>\Testo\Codecov\Config\CoverageLevel</enum>`)
40+
- Use `<enum>\FQN</enum>` when referencing PHP enums (e.g., `<enum>\Testo\Codecov\Config\CoverageLevel</enum>`). For specific enum cases use `<enum>\FQN::Case</enum>` (e.g., `<enum>\Testo\Codecov\Config\CoverageMode::Always</enum>`) — do NOT use backtick-wrapped `CoverageMode::Always` in prose
4141
- Use `<attr>\FQN</attr>` for any PHP attributes — user-facing (`<attr>\Testo\Retry</attr>`) and meta-attributes (`<attr>\Testo\Pipeline\Attribute\FallbackInterceptor</attr>`)
4242
- Use `<func>\FQN::method()</func>` for methods (e.g., `<func>\Testo\Assert::same()</func>`)
4343
- Do NOT use plain markdown links (`[Assert](./plugins/assert.md)`) when these tags are available
@@ -261,12 +261,15 @@ For referencing PHP enums inline. Renders the short enum name (without namespace
261261
**Syntax:**
262262
```html
263263
<enum>\Testo\Codecov\Config\CoverageLevel</enum>
264+
<enum>\Testo\Codecov\Config\CoverageMode::Always</enum>
264265
```
265266

266-
Renders as `CoverageLevel` styled as inline code. On hover, shows a tooltip with the full enum name. If a corresponding `<signature name="enum \...">` block exists with `h > 0`, the reference is a clickable link.
267+
The first form renders as `CoverageLevel` — same as `<class>`. The second form renders as `CoverageMode::Always` with a tooltip showing the enum's short description and the case description. If a corresponding `<signature name="enum \...">` block exists with `h > 0`, the reference is a clickable link.
267268

268269
**Behavior:**
269-
- Same display rules as `<class>`: namespace stripped, tooltip with full FQN
270+
- Enum reference (no `::`): namespace stripped, tooltip with full FQN and `<short>` description
271+
- Enum case reference (with `::`): tooltip shows enum signature, enum `<short>`, case name, and case description
272+
- Always use `<enum>` tags for enum values in prose — never backtick-wrapped names like `` `CoverageMode::Always` ``
270273
- Locale-aware: EN pages reference EN signatures, RU pages reference RU signatures
271274

272275
## Class References (`<class>`)

blog/beta-testo.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,7 @@ The public API has stabilized, but there are still a few things to finish:
261261
- Fine-tune minor things in benchmarks and internals.
262262
- Organizational matters like splitting the monorepo and finishing the documentation.
263263
264-
Code coverage and mocks might also make it to the release, but no promises.
264+
<plugin>Code coverage</plugin> and mocks might also make it to the release, but no promises.
265265
266266
You can help by testing and providing feedback to make the release as smooth as possible.
267267
Head to [GitHub Issues](https://github.com/php-testo/testo/issues) with ideas, questions, and problems. Let's figure it out together!

docs/plugins/codecov.md

Lines changed: 282 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,282 @@
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.

ru/blog/beta-testo.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,7 @@ Results for viaDivision:
261261
- Допилить незначительные вещи в бенчах и internal, чтобы было совсем хорошо.
262262
- Всякие организационные моменты, вроде "раскидать монорепу" и дописать документацию.
263263
264-
Codecov и Mocks, возможно, тоже подъедут к релизу, но это уже не точно.
264+
<plugin>Codecov</plugin> и Mocks, возможно, тоже подъедут к релизу, но это уже не точно.
265265
266266
Вы же, в свою очередь, можете помочь с тестированием и фидбеком, чтобы релиз был максимально гладким и безболезненным.
267267
Приходите в [GitHub Issues](https://github.com/php-testo/testo/issues) или [Telegram-чат](https://t.me/spiralphp/10863) с идеями, вопросами и проблемами — будем разбираться вместе!

0 commit comments

Comments
 (0)