@descryy/runtime-test-runner 0.4.12 → 0.4.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bounded-file.d.ts +18 -0
- package/dist/bounded-file.d.ts.map +1 -0
- package/dist/bounded-file.js +77 -0
- package/dist/bounded-file.js.map +1 -0
- package/dist/child-environment.d.ts +12 -0
- package/dist/child-environment.d.ts.map +1 -0
- package/dist/child-environment.js +124 -0
- package/dist/child-environment.js.map +1 -0
- package/dist/coverage-capture.d.ts +23 -0
- package/dist/coverage-capture.d.ts.map +1 -0
- package/dist/coverage-capture.js +165 -0
- package/dist/coverage-capture.js.map +1 -0
- package/dist/executed-coverage.d.ts +45 -0
- package/dist/executed-coverage.d.ts.map +1 -0
- package/dist/executed-coverage.js +143 -0
- package/dist/executed-coverage.js.map +1 -0
- package/dist/go-cover-profile.d.ts +29 -0
- package/dist/go-cover-profile.d.ts.map +1 -0
- package/dist/go-cover-profile.js +162 -0
- package/dist/go-cover-profile.js.map +1 -0
- package/dist/go-test.d.ts +29 -2
- package/dist/go-test.d.ts.map +1 -1
- package/dist/go-test.js +149 -30
- package/dist/go-test.js.map +1 -1
- package/dist/graph-mapping.d.ts +1 -0
- package/dist/graph-mapping.d.ts.map +1 -1
- package/dist/graph-mapping.js +78 -1
- package/dist/graph-mapping.js.map +1 -1
- package/dist/index.d.ts +21 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -1
- package/dist/index.js.map +1 -1
- package/dist/lexers/c-family.d.ts +4 -0
- package/dist/lexers/c-family.d.ts.map +1 -0
- package/dist/lexers/c-family.js +221 -0
- package/dist/lexers/c-family.js.map +1 -0
- package/dist/lexers/chars.d.ts +7 -0
- package/dist/lexers/chars.d.ts.map +1 -0
- package/dist/lexers/chars.js +49 -0
- package/dist/lexers/chars.js.map +1 -0
- package/dist/lexers/index.d.ts +7 -0
- package/dist/lexers/index.d.ts.map +1 -0
- package/dist/lexers/index.js +5 -0
- package/dist/lexers/index.js.map +1 -0
- package/dist/lexers/python.d.ts +3 -0
- package/dist/lexers/python.d.ts.map +1 -0
- package/dist/lexers/python.js +101 -0
- package/dist/lexers/python.js.map +1 -0
- package/dist/quality/annotations.d.ts +14 -0
- package/dist/quality/annotations.d.ts.map +1 -0
- package/dist/quality/annotations.js +147 -0
- package/dist/quality/annotations.js.map +1 -0
- package/dist/quality/assertion-kinds.d.ts +18 -0
- package/dist/quality/assertion-kinds.d.ts.map +1 -0
- package/dist/quality/assertion-kinds.js +25 -0
- package/dist/quality/assertion-kinds.js.map +1 -0
- package/dist/quality/chains.d.ts +25 -0
- package/dist/quality/chains.d.ts.map +1 -0
- package/dist/quality/chains.js +129 -0
- package/dist/quality/chains.js.map +1 -0
- package/dist/quality/common.d.ts +31 -0
- package/dist/quality/common.d.ts.map +1 -0
- package/dist/quality/common.js +211 -0
- package/dist/quality/common.js.map +1 -0
- package/dist/quality/csharp-family.d.ts +9 -0
- package/dist/quality/csharp-family.d.ts.map +1 -0
- package/dist/quality/csharp-family.js +560 -0
- package/dist/quality/csharp-family.js.map +1 -0
- package/dist/quality/detect.d.ts +3 -0
- package/dist/quality/detect.d.ts.map +1 -0
- package/dist/quality/detect.js +348 -0
- package/dist/quality/detect.js.map +1 -0
- package/dist/quality/durations.d.ts +4 -0
- package/dist/quality/durations.d.ts.map +1 -0
- package/dist/quality/durations.js +177 -0
- package/dist/quality/durations.js.map +1 -0
- package/dist/quality/expressions.d.ts +7 -0
- package/dist/quality/expressions.d.ts.map +1 -0
- package/dist/quality/expressions.js +123 -0
- package/dist/quality/expressions.js.map +1 -0
- package/dist/quality/facts.d.ts +5 -0
- package/dist/quality/facts.d.ts.map +1 -0
- package/dist/quality/facts.js +183 -0
- package/dist/quality/facts.js.map +1 -0
- package/dist/quality/failure-of.d.ts +4 -0
- package/dist/quality/failure-of.d.ts.map +1 -0
- package/dist/quality/failure-of.js +233 -0
- package/dist/quality/failure-of.js.map +1 -0
- package/dist/quality/frameworks.d.ts +5 -0
- package/dist/quality/frameworks.d.ts.map +1 -0
- package/dist/quality/frameworks.js +20 -0
- package/dist/quality/frameworks.js.map +1 -0
- package/dist/quality/go-family.d.ts +6 -0
- package/dist/quality/go-family.d.ts.map +1 -0
- package/dist/quality/go-family.js +393 -0
- package/dist/quality/go-family.js.map +1 -0
- package/dist/quality/index.d.ts +10 -0
- package/dist/quality/index.d.ts.map +1 -0
- package/dist/quality/index.js +19 -0
- package/dist/quality/index.js.map +1 -0
- package/dist/quality/java-family.d.ts +12 -0
- package/dist/quality/java-family.d.ts.map +1 -0
- package/dist/quality/java-family.js +529 -0
- package/dist/quality/java-family.js.map +1 -0
- package/dist/quality/js-assertions.d.ts +11 -0
- package/dist/quality/js-assertions.d.ts.map +1 -0
- package/dist/quality/js-assertions.js +297 -0
- package/dist/quality/js-assertions.js.map +1 -0
- package/dist/quality/js-family.d.ts +3 -0
- package/dist/quality/js-family.d.ts.map +1 -0
- package/dist/quality/js-family.js +470 -0
- package/dist/quality/js-family.js.map +1 -0
- package/dist/quality/python-family.d.ts +6 -0
- package/dist/quality/python-family.d.ts.map +1 -0
- package/dist/quality/python-family.js +786 -0
- package/dist/quality/python-family.js.map +1 -0
- package/dist/quality/scope.d.ts +26 -0
- package/dist/quality/scope.d.ts.map +1 -0
- package/dist/quality/scope.js +33 -0
- package/dist/quality/scope.js.map +1 -0
- package/dist/quality/selectors.d.ts +14 -0
- package/dist/quality/selectors.d.ts.map +1 -0
- package/dist/quality/selectors.js +137 -0
- package/dist/quality/selectors.js.map +1 -0
- package/dist/quality/statuses.d.ts +7 -0
- package/dist/quality/statuses.d.ts.map +1 -0
- package/dist/quality/statuses.js +143 -0
- package/dist/quality/statuses.js.map +1 -0
- package/dist/quality/stream.d.ts +23 -0
- package/dist/quality/stream.d.ts.map +1 -0
- package/dist/quality/stream.js +194 -0
- package/dist/quality/stream.js.map +1 -0
- package/dist/quality/types.d.ts +88 -0
- package/dist/quality/types.d.ts.map +1 -0
- package/dist/quality/types.js +2 -0
- package/dist/quality/types.js.map +1 -0
- package/dist/run-timeout.d.ts +25 -0
- package/dist/run-timeout.d.ts.map +1 -0
- package/dist/run-timeout.js +52 -0
- package/dist/run-timeout.js.map +1 -0
- package/dist/runner-wrappers.d.ts +74 -0
- package/dist/runner-wrappers.d.ts.map +1 -0
- package/dist/runner-wrappers.js +604 -0
- package/dist/runner-wrappers.js.map +1 -0
- package/dist/swift-xctest.d.ts +15 -2
- package/dist/swift-xctest.d.ts.map +1 -1
- package/dist/swift-xctest.js +143 -20
- package/dist/swift-xctest.js.map +1 -1
- package/dist/test-runner-collector.d.ts +56 -6
- package/dist/test-runner-collector.d.ts.map +1 -1
- package/dist/test-runner-collector.js +265 -113
- package/dist/test-runner-collector.js.map +1 -1
- package/dist/testgen/csharp-facts.d.ts +3 -0
- package/dist/testgen/csharp-facts.d.ts.map +1 -0
- package/dist/testgen/csharp-facts.js +306 -0
- package/dist/testgen/csharp-facts.js.map +1 -0
- package/dist/testgen/csharp-profile.d.ts +14 -0
- package/dist/testgen/csharp-profile.d.ts.map +1 -0
- package/dist/testgen/csharp-profile.js +233 -0
- package/dist/testgen/csharp-profile.js.map +1 -0
- package/dist/testgen/declarations.d.ts +10 -0
- package/dist/testgen/declarations.d.ts.map +1 -0
- package/dist/testgen/declarations.js +110 -0
- package/dist/testgen/declarations.js.map +1 -0
- package/dist/testgen/facts-common.d.ts +33 -0
- package/dist/testgen/facts-common.d.ts.map +1 -0
- package/dist/testgen/facts-common.js +76 -0
- package/dist/testgen/facts-common.js.map +1 -0
- package/dist/testgen/failure.d.ts +4 -0
- package/dist/testgen/failure.d.ts.map +1 -0
- package/dist/testgen/failure.js +106 -0
- package/dist/testgen/failure.js.map +1 -0
- package/dist/testgen/frameworks.d.ts +6 -0
- package/dist/testgen/frameworks.d.ts.map +1 -0
- package/dist/testgen/frameworks.js +516 -0
- package/dist/testgen/frameworks.js.map +1 -0
- package/dist/testgen/go-facts.d.ts +11 -0
- package/dist/testgen/go-facts.d.ts.map +1 -0
- package/dist/testgen/go-facts.js +237 -0
- package/dist/testgen/go-facts.js.map +1 -0
- package/dist/testgen/go-profile.d.ts +6 -0
- package/dist/testgen/go-profile.d.ts.map +1 -0
- package/dist/testgen/go-profile.js +155 -0
- package/dist/testgen/go-profile.js.map +1 -0
- package/dist/testgen/index.d.ts +28 -0
- package/dist/testgen/index.d.ts.map +1 -0
- package/dist/testgen/index.js +21 -0
- package/dist/testgen/index.js.map +1 -0
- package/dist/testgen/java-build.d.ts +13 -0
- package/dist/testgen/java-build.d.ts.map +1 -0
- package/dist/testgen/java-build.js +250 -0
- package/dist/testgen/java-build.js.map +1 -0
- package/dist/testgen/java-facts.d.ts +3 -0
- package/dist/testgen/java-facts.d.ts.map +1 -0
- package/dist/testgen/java-facts.js +349 -0
- package/dist/testgen/java-facts.js.map +1 -0
- package/dist/testgen/java-profile.d.ts +13 -0
- package/dist/testgen/java-profile.d.ts.map +1 -0
- package/dist/testgen/java-profile.js +194 -0
- package/dist/testgen/java-profile.js.map +1 -0
- package/dist/testgen/languages.d.ts +11 -0
- package/dist/testgen/languages.d.ts.map +1 -0
- package/dist/testgen/languages.js +63 -0
- package/dist/testgen/languages.js.map +1 -0
- package/dist/testgen/lexer.d.ts +19 -0
- package/dist/testgen/lexer.d.ts.map +1 -0
- package/dist/testgen/lexer.js +257 -0
- package/dist/testgen/lexer.js.map +1 -0
- package/dist/testgen/mutant-core.d.ts +19 -0
- package/dist/testgen/mutant-core.d.ts.map +1 -0
- package/dist/testgen/mutant-core.js +39 -0
- package/dist/testgen/mutant-core.js.map +1 -0
- package/dist/testgen/mutants-lang.d.ts +6 -0
- package/dist/testgen/mutants-lang.d.ts.map +1 -0
- package/dist/testgen/mutants-lang.js +291 -0
- package/dist/testgen/mutants-lang.js.map +1 -0
- package/dist/testgen/mutants.d.ts +9 -0
- package/dist/testgen/mutants.d.ts.map +1 -0
- package/dist/testgen/mutants.js +148 -0
- package/dist/testgen/mutants.js.map +1 -0
- package/dist/testgen/paths.d.ts +32 -0
- package/dist/testgen/paths.d.ts.map +1 -0
- package/dist/testgen/paths.js +211 -0
- package/dist/testgen/paths.js.map +1 -0
- package/dist/testgen/process.d.ts +19 -0
- package/dist/testgen/process.d.ts.map +1 -0
- package/dist/testgen/process.js +89 -0
- package/dist/testgen/process.js.map +1 -0
- package/dist/testgen/profile-failure.d.ts +7 -0
- package/dist/testgen/profile-failure.d.ts.map +1 -0
- package/dist/testgen/profile-failure.js +29 -0
- package/dist/testgen/profile-failure.js.map +1 -0
- package/dist/testgen/python-facts.d.ts +11 -0
- package/dist/testgen/python-facts.d.ts.map +1 -0
- package/dist/testgen/python-facts.js +380 -0
- package/dist/testgen/python-facts.js.map +1 -0
- package/dist/testgen/python-profile.d.ts +4 -0
- package/dist/testgen/python-profile.d.ts.map +1 -0
- package/dist/testgen/python-profile.js +344 -0
- package/dist/testgen/python-profile.js.map +1 -0
- package/dist/testgen/source-facts.d.ts +30 -0
- package/dist/testgen/source-facts.d.ts.map +1 -0
- package/dist/testgen/source-facts.js +497 -0
- package/dist/testgen/source-facts.js.map +1 -0
- package/dist/testgen/stubs.d.ts +4 -0
- package/dist/testgen/stubs.d.ts.map +1 -0
- package/dist/testgen/stubs.js +66 -0
- package/dist/testgen/stubs.js.map +1 -0
- package/dist/testgen/toolchain.d.ts +23 -0
- package/dist/testgen/toolchain.d.ts.map +1 -0
- package/dist/testgen/toolchain.js +201 -0
- package/dist/testgen/toolchain.js.map +1 -0
- package/dist/testgen/types.d.ts +67 -0
- package/dist/testgen/types.d.ts.map +1 -0
- package/dist/testgen/types.js +2 -0
- package/dist/testgen/types.js.map +1 -0
- package/dist/v8-coverage.d.ts +21 -0
- package/dist/v8-coverage.d.ts.map +1 -0
- package/dist/v8-coverage.js +217 -0
- package/dist/v8-coverage.js.map +1 -0
- package/dist/vitest-reporter.d.ts +46 -13
- package/dist/vitest-reporter.d.ts.map +1 -1
- package/dist/vitest-reporter.js +119 -26
- package/dist/vitest-reporter.js.map +1 -1
- package/dist/vitest-version.d.ts +21 -0
- package/dist/vitest-version.d.ts.map +1 -0
- package/dist/vitest-version.js +98 -0
- package/dist/vitest-version.js.map +1 -0
- package/package.json +14 -8
- package/reporters/junit/classes/DescryJUnitListener.class +0 -0
- package/reporters/junit/classes/META-INF/services/org.junit.platform.launcher.TestExecutionListener +1 -0
- package/reporters/minitest/descry_minitest_reporter.rb +150 -0
- package/reporters/phpunit/DescryPhpUnitExtension.php +209 -0
- package/reporters/pytest/descry_pytest_plugin.py +397 -0
- package/reporters/rspec/descry_rspec_formatter.rb +175 -0
- package/reporters/xunit/bin/Release/netstandard2.0/Descry.TestLogger.deps.json +203 -0
- package/reporters/xunit/bin/Release/netstandard2.0/Descry.TestLogger.dll +0 -0
- package/reporters/xunit/bin/Release/netstandard2.0/Descry.TestLogger.pdb +0 -0
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
<?php declare(strict_types=1);
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* §17's PHPUnit reporter — a runner extension writing the same NDJSON
|
|
5
|
+
* envelope every other reporter in this package writes (RT-109).
|
|
6
|
+
*
|
|
7
|
+
* ## Why an extension and not the JUnit XML or TestDox output
|
|
8
|
+
*
|
|
9
|
+
* PHPUnit emits JUnit XML, and RT-088 measured what that format costs:
|
|
10
|
+
* `@Disabled` and an aborted assumption collapse into an identical bare
|
|
11
|
+
* `<skipped>`. PHPUnit has the same problem with more to lose —
|
|
12
|
+
* `markTestSkipped()` and `markTestIncomplete()` are different facts about
|
|
13
|
+
* coverage and the XML writes both as `<skipped>`.
|
|
14
|
+
*
|
|
15
|
+
* PHPUnit's **event system** is its own structured stream. It also removes
|
|
16
|
+
* the shape RT-088 warned about generally: a document written at the end of
|
|
17
|
+
* the run produces nothing at all if the run is killed.
|
|
18
|
+
*
|
|
19
|
+
* `TestListener` is not an option here and should not be looked for: it was
|
|
20
|
+
* removed in PHPUnit 10 and replaced by this.
|
|
21
|
+
*
|
|
22
|
+
* ## The four outcomes, and the one that needs a snapshot
|
|
23
|
+
*
|
|
24
|
+
* `MarkedIncomplete` -> `not-implemented`. PHPUnit's own word for it is
|
|
25
|
+
* "incomplete", meaning the test is a placeholder for behaviour that is not
|
|
26
|
+
* written. That is `test.todo` exactly.
|
|
27
|
+
*
|
|
28
|
+
* `Errored` and `Failed` are both `test:fail`, named apart in the payload:
|
|
29
|
+
* an assertion that did not hold and an exception that escaped are different
|
|
30
|
+
* things to go and look at, and PHPUnit is the only layer that knows which
|
|
31
|
+
* happened.
|
|
32
|
+
*
|
|
33
|
+
* `Skipped` is the one that needs care, because PHPUnit reaches it two ways
|
|
34
|
+
* — a `#[Requires*]` attribute the runner evaluated *before* the test, and
|
|
35
|
+
* `markTestSkipped()` *inside* the body. Measured: `Test\Prepared` fires only
|
|
36
|
+
* for the second. So a `Skipped` with a preceding `Prepared` ran real code
|
|
37
|
+
* and bailed on an assumption (`precondition-unmet`, JUnit's `ABORTED`), and
|
|
38
|
+
* one without never started (`disabled`).
|
|
39
|
+
*
|
|
40
|
+
* That is the same shape RSpec needed (its metadata is mutated by execution)
|
|
41
|
+
* and pytest needed (its markers are only readable at collection). Three
|
|
42
|
+
* runners, three different mechanisms, one rule: **the distinction is read
|
|
43
|
+
* from the runner's own event order, never parsed out of a message string.**
|
|
44
|
+
*
|
|
45
|
+
* ## The envelope version
|
|
46
|
+
*
|
|
47
|
+
* Declared before the first event, as a number: `{"version":"1"}` is read as
|
|
48
|
+
* a broken reporter rather than a matching one. Keep in step with
|
|
49
|
+
* `ENVELOPE_VERSION` in `test-runner-collector.ts` — that file is the
|
|
50
|
+
* authority, not this comment.
|
|
51
|
+
*/
|
|
52
|
+
|
|
53
|
+
use PHPUnit\Event\Test\Errored;
|
|
54
|
+
use PHPUnit\Event\Test\ErroredSubscriber;
|
|
55
|
+
use PHPUnit\Event\Test\Failed;
|
|
56
|
+
use PHPUnit\Event\Test\FailedSubscriber;
|
|
57
|
+
use PHPUnit\Event\Test\MarkedIncomplete;
|
|
58
|
+
use PHPUnit\Event\Test\MarkedIncompleteSubscriber;
|
|
59
|
+
use PHPUnit\Event\Test\Passed;
|
|
60
|
+
use PHPUnit\Event\Test\PassedSubscriber;
|
|
61
|
+
use PHPUnit\Event\Test\Prepared;
|
|
62
|
+
use PHPUnit\Event\Test\PreparedSubscriber;
|
|
63
|
+
use PHPUnit\Event\Test\Skipped;
|
|
64
|
+
use PHPUnit\Event\Test\SkippedSubscriber;
|
|
65
|
+
use PHPUnit\Event\TestRunner\Started as RunnerStarted;
|
|
66
|
+
use PHPUnit\Event\TestRunner\StartedSubscriber as RunnerStartedSubscriber;
|
|
67
|
+
use PHPUnit\Runner\Extension\Extension;
|
|
68
|
+
use PHPUnit\Runner\Extension\Facade;
|
|
69
|
+
use PHPUnit\Runner\Extension\ParameterCollection;
|
|
70
|
+
use PHPUnit\TextUI\Configuration\Configuration;
|
|
71
|
+
|
|
72
|
+
final class DescryEmitter
|
|
73
|
+
{
|
|
74
|
+
/** Must equal ENVELOPE_VERSION in test-runner-collector.ts. A mismatch is refused there, loudly. */
|
|
75
|
+
public const ENVELOPE_VERSION = 1;
|
|
76
|
+
|
|
77
|
+
/** Test ids that reached `Prepared`, i.e. whose body actually started. */
|
|
78
|
+
private static array $prepared = [];
|
|
79
|
+
|
|
80
|
+
public static function envelope(): void
|
|
81
|
+
{
|
|
82
|
+
self::write(['type' => 'descry:envelope', 'data' => ['version' => self::ENVELOPE_VERSION]]);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
public static function started(Prepared $event): void
|
|
86
|
+
{
|
|
87
|
+
self::$prepared[$event->test()->id()] = true;
|
|
88
|
+
self::write(['type' => 'test:start', 'data' => self::common($event)]);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
public static function passed(Passed $event): void
|
|
92
|
+
{
|
|
93
|
+
self::write(['type' => 'test:pass', 'data' => self::common($event)]);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
public static function failed(Failed $event): void
|
|
97
|
+
{
|
|
98
|
+
$data = self::common($event);
|
|
99
|
+
$data['details'] = ['error' => ['message' => $event->throwable()->asString()]];
|
|
100
|
+
self::write(['type' => 'test:fail', 'data' => $data]);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
public static function errored(Errored $event): void
|
|
104
|
+
{
|
|
105
|
+
$data = self::common($event);
|
|
106
|
+
// Named apart from a failed assertion. PHPUnit is the only layer that
|
|
107
|
+
// knows which of the two happened, and they send a developer to
|
|
108
|
+
// different places.
|
|
109
|
+
$data['details'] = ['error' => ['message' => $event->throwable()->asString(), 'failureType' => 'error']];
|
|
110
|
+
self::write(['type' => 'test:fail', 'data' => $data]);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
public static function skipped(Skipped $event): void
|
|
114
|
+
{
|
|
115
|
+
$data = self::common($event);
|
|
116
|
+
$data['skip'] = true;
|
|
117
|
+
// Read from the event order, never from the message. A `#[Requires*]`
|
|
118
|
+
// attribute is evaluated before the test and produces no `Prepared`;
|
|
119
|
+
// `markTestSkipped()` runs inside the body, which necessarily did.
|
|
120
|
+
$data['skipReason'] = isset(self::$prepared[$event->test()->id()]) ? 'precondition-unmet' : 'disabled';
|
|
121
|
+
$data['message'] = $event->message();
|
|
122
|
+
self::write(['type' => 'test:pass', 'data' => $data]);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
public static function incomplete(MarkedIncomplete $event): void
|
|
126
|
+
{
|
|
127
|
+
$data = self::common($event);
|
|
128
|
+
$data['skip'] = true;
|
|
129
|
+
// PHPUnit's own word is "incomplete": a placeholder for behaviour
|
|
130
|
+
// that is not written. node:test's `todo`, not its `skip`.
|
|
131
|
+
$data['skipReason'] = 'not-implemented';
|
|
132
|
+
$data['message'] = $event->throwable()->message();
|
|
133
|
+
self::write(['type' => 'test:pass', 'data' => $data]);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* `name` is the method (with a data-set suffix for a data provider);
|
|
138
|
+
* `qualifiedName` is `Class::method`, which is what a graph resolves
|
|
139
|
+
* against. Both come off `TestMethod`, so nothing is stripped or
|
|
140
|
+
* reconstructed downstream.
|
|
141
|
+
*/
|
|
142
|
+
private static function common($event): array
|
|
143
|
+
{
|
|
144
|
+
$test = $event->test();
|
|
145
|
+
$data = ['name' => $test->name()];
|
|
146
|
+
|
|
147
|
+
if ($test instanceof \PHPUnit\Event\Code\TestMethod) {
|
|
148
|
+
// `id()` is `Class::method` plus a data-provider suffix when
|
|
149
|
+
// there is one; `name()` is the bare method and drops it. The
|
|
150
|
+
// display name keeps the suffix so two data sets are two
|
|
151
|
+
// results, and `qualifiedName` is the declaring method so both
|
|
152
|
+
// resolve to one graph node. Neither is reconstructed by string
|
|
153
|
+
// surgery on the other beyond removing the class prefix `id()`
|
|
154
|
+
// itself put there.
|
|
155
|
+
$prefix = $test->className() . '::';
|
|
156
|
+
$data['name'] = str_starts_with($test->id(), $prefix) ? substr($test->id(), strlen($prefix)) : $test->name();
|
|
157
|
+
$data['qualifiedName'] = $prefix . $test->methodName();
|
|
158
|
+
$data['file'] = $test->file();
|
|
159
|
+
$data['line'] = $test->line();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return $data;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* **The leading newline is not cosmetic**, and it was measured on pytest
|
|
167
|
+
* rather than guessed at here: a runner that writes progress characters
|
|
168
|
+
* with no trailing newline puts a report on the same line as one, and
|
|
169
|
+
* the line stops being JSON. A collector then reads every result as
|
|
170
|
+
* noise while the starts arrive intact — a reporter that looks healthy
|
|
171
|
+
* and reports nothing. PHPUnit's default progress output does exactly
|
|
172
|
+
* that (`.`, `F`, `S`, `I`, in columns of 63).
|
|
173
|
+
*/
|
|
174
|
+
private static function write(array $object): void
|
|
175
|
+
{
|
|
176
|
+
fwrite(STDOUT, "\n" . json_encode($object, JSON_UNESCAPED_SLASHES) . "\n");
|
|
177
|
+
fflush(STDOUT);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
final class DescryPhpUnitExtension implements Extension
|
|
182
|
+
{
|
|
183
|
+
public function bootstrap(Configuration $configuration, Facade $facade, ParameterCollection $parameters): void
|
|
184
|
+
{
|
|
185
|
+
$facade->registerSubscribers(
|
|
186
|
+
new class implements RunnerStartedSubscriber {
|
|
187
|
+
public function notify(RunnerStarted $event): void { DescryEmitter::envelope(); }
|
|
188
|
+
},
|
|
189
|
+
new class implements PreparedSubscriber {
|
|
190
|
+
public function notify(Prepared $event): void { DescryEmitter::started($event); }
|
|
191
|
+
},
|
|
192
|
+
new class implements PassedSubscriber {
|
|
193
|
+
public function notify(Passed $event): void { DescryEmitter::passed($event); }
|
|
194
|
+
},
|
|
195
|
+
new class implements FailedSubscriber {
|
|
196
|
+
public function notify(Failed $event): void { DescryEmitter::failed($event); }
|
|
197
|
+
},
|
|
198
|
+
new class implements ErroredSubscriber {
|
|
199
|
+
public function notify(Errored $event): void { DescryEmitter::errored($event); }
|
|
200
|
+
},
|
|
201
|
+
new class implements SkippedSubscriber {
|
|
202
|
+
public function notify(Skipped $event): void { DescryEmitter::skipped($event); }
|
|
203
|
+
},
|
|
204
|
+
new class implements MarkedIncompleteSubscriber {
|
|
205
|
+
public function notify(MarkedIncomplete $event): void { DescryEmitter::incomplete($event); }
|
|
206
|
+
},
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
"""§17's pytest reporter — a plugin writing the same NDJSON envelope
|
|
2
|
+
``reporter.ts``, ``jest-reporter.ts``, ``vitest-reporter.ts`` and
|
|
3
|
+
``DescryJUnitListener.java`` write (RT-107).
|
|
4
|
+
|
|
5
|
+
## Why a plugin and not ``--junit-xml``
|
|
6
|
+
|
|
7
|
+
pytest can emit JUnit XML, and it is the wrong source for the same reason
|
|
8
|
+
RT-088 gave for JUnit itself: the XML collapses distinct outcomes into one
|
|
9
|
+
bare ``<skipped>``. pytest has **more** to lose than JUnit does — ``skip``,
|
|
10
|
+
``skipif``, ``xfail`` and a runtime ``pytest.skip()`` are four different
|
|
11
|
+
facts about coverage, and the report format keeps two of them apart at best.
|
|
12
|
+
|
|
13
|
+
``pytest_runtest_logreport`` is pytest's own structured reporting hook. It
|
|
14
|
+
receives the outcome pytest itself computed, so nothing here re-derives a
|
|
15
|
+
verdict from printed text.
|
|
16
|
+
|
|
17
|
+
## The four outcomes, and where each one is *read* rather than inferred
|
|
18
|
+
|
|
19
|
+
``skipReason`` is set here, never downstream, for the same reason
|
|
20
|
+
``isSuite`` is: only the reporter knows what its runner can tell apart.
|
|
21
|
+
|
|
22
|
+
=========================== ========================== ====================
|
|
23
|
+
what the developer wrote what it means for coverage ``skipReason``
|
|
24
|
+
=========================== ========================== ====================
|
|
25
|
+
``@pytest.mark.skip`` switched off deliberately ``disabled``
|
|
26
|
+
``@pytest.mark.skipif(c)`` the environment said no ``precondition-unmet``
|
|
27
|
+
``@pytest.mark.xfail`` known-broken, not absent ``not-implemented``
|
|
28
|
+
``pytest.skip()`` at runtime ran real code, then bailed ``precondition-unmet``
|
|
29
|
+
=========================== ========================== ====================
|
|
30
|
+
|
|
31
|
+
``xfail`` -> ``not-implemented`` is the one worth arguing. The vocabulary
|
|
32
|
+
defines it as "declared as absent rather than switched off", and an ``xfail``
|
|
33
|
+
test is written, does run, and is declared not to work yet — which is what
|
|
34
|
+
``test.todo`` means on node:test, and is not what ``test.skip`` means. The
|
|
35
|
+
alternative reading (``disabled``) would put ``xfail`` in the same bucket as
|
|
36
|
+
``skip``, and the whole reason this reporter exists is that those two answer
|
|
37
|
+
"is this code covered" differently.
|
|
38
|
+
|
|
39
|
+
A runtime ``pytest.skip()`` gets ``precondition-unmet`` and not
|
|
40
|
+
``unspecified``: it is JUnit's ``ABORTED`` exactly — the test body executed
|
|
41
|
+
and bailed on an assumption.
|
|
42
|
+
|
|
43
|
+
## Which marker applied is read at collection, not parsed from the report
|
|
44
|
+
|
|
45
|
+
``report.longrepr`` for a skip is a ``(path, lineno, reason)`` tuple whose
|
|
46
|
+
reason is a *string* like ``"Skipped: no database"``. Distinguishing
|
|
47
|
+
``skip`` from ``skipif`` by parsing it would be a heuristic over prose. So
|
|
48
|
+
markers are read off the item in ``pytest_collection_modifyitems``, where
|
|
49
|
+
pytest hands over the real ``Mark`` objects, and looked up by ``nodeid``.
|
|
50
|
+
|
|
51
|
+
The same pass reads ``module.__name__``, the class and the original function
|
|
52
|
+
name, so ``qualifiedName`` is what pytest knows rather than a nodeid this
|
|
53
|
+
plugin reformatted into something that looks like a dotted path.
|
|
54
|
+
|
|
55
|
+
## Setup and teardown failures are real failures
|
|
56
|
+
|
|
57
|
+
A fixture that raises produces ``failed`` on the ``setup`` phase and the test
|
|
58
|
+
body never runs. Reported as ``test:fail``: it is the single signal that the
|
|
59
|
+
test did not execute, and dropping it to make the phase handling uniform
|
|
60
|
+
would trade a real failure for a silence. The ``call`` phase is what
|
|
61
|
+
produces passes, so a passed ``setup``/``teardown`` is not forwarded — the
|
|
62
|
+
same asymmetry the translator applies to node:test containers and the JUnit
|
|
63
|
+
listener applies to its own.
|
|
64
|
+
|
|
65
|
+
## The envelope version
|
|
66
|
+
|
|
67
|
+
Declared before the first event, as a **number**: ``{"version": "1"}`` is
|
|
68
|
+
read as a broken reporter rather than a matching one. Keep in step with
|
|
69
|
+
``ENVELOPE_VERSION`` in ``test-runner-collector.ts`` — that file is the
|
|
70
|
+
authority, not this docstring.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
import json
|
|
74
|
+
import os
|
|
75
|
+
import sys
|
|
76
|
+
import threading
|
|
77
|
+
|
|
78
|
+
import pytest
|
|
79
|
+
|
|
80
|
+
#: Must equal ENVELOPE_VERSION in test-runner-collector.ts. A mismatch is refused there, loudly.
|
|
81
|
+
ENVELOPE_VERSION = 1
|
|
82
|
+
|
|
83
|
+
#: nodeid -> what collection could see about this item. Populated once, read per report.
|
|
84
|
+
_COLLECTED: dict = {}
|
|
85
|
+
|
|
86
|
+
#: Set from the config so writes can step outside pytest's own output capture.
|
|
87
|
+
_CAPTURE_MANAGER = None
|
|
88
|
+
|
|
89
|
+
_COVERAGE_DIRECTORY_VARIABLE = "DESCRY_COVERAGE_DIR"
|
|
90
|
+
_COVERAGE_ROOT_VARIABLE = "DESCRY_COVERAGE_ROOT"
|
|
91
|
+
_EXCLUDED_SEGMENTS = frozenset(
|
|
92
|
+
{"site-packages", "dist-packages", "node_modules", ".venv", "venv", "vendor", "__pycache__", ".tox", ".git"}
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
_RECORDER = None
|
|
96
|
+
_TEST_FILES = set()
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class _LineRecorder:
|
|
100
|
+
def __init__(self, root):
|
|
101
|
+
self.root = root
|
|
102
|
+
self.executed = {}
|
|
103
|
+
self.mechanism = None
|
|
104
|
+
self.tool_id = None
|
|
105
|
+
self.notes = []
|
|
106
|
+
self.relative_cache = {}
|
|
107
|
+
self.own_file = os.path.realpath(__file__)
|
|
108
|
+
|
|
109
|
+
def relative(self, filename):
|
|
110
|
+
cached = self.relative_cache.get(filename)
|
|
111
|
+
if cached is not None:
|
|
112
|
+
return cached
|
|
113
|
+
result = ""
|
|
114
|
+
try:
|
|
115
|
+
if filename.endswith(".py") and os.path.isabs(filename):
|
|
116
|
+
real = os.path.realpath(filename)
|
|
117
|
+
prefix = self.root + os.sep
|
|
118
|
+
if real.startswith(prefix) and real != self.own_file:
|
|
119
|
+
parts = real[len(prefix):].split(os.sep)
|
|
120
|
+
if not _EXCLUDED_SEGMENTS.intersection(parts):
|
|
121
|
+
result = "/".join(parts)
|
|
122
|
+
except (OSError, ValueError):
|
|
123
|
+
result = ""
|
|
124
|
+
self.relative_cache[filename] = result
|
|
125
|
+
return result
|
|
126
|
+
|
|
127
|
+
def lines_of(self, relative):
|
|
128
|
+
lines = self.executed.get(relative)
|
|
129
|
+
if lines is None:
|
|
130
|
+
lines = self.executed[relative] = set()
|
|
131
|
+
return lines
|
|
132
|
+
|
|
133
|
+
def on_line(self, code, line_number):
|
|
134
|
+
relative = self.relative(code.co_filename)
|
|
135
|
+
if relative and line_number > 0:
|
|
136
|
+
self.lines_of(relative).add(line_number)
|
|
137
|
+
return sys.monitoring.DISABLE
|
|
138
|
+
|
|
139
|
+
def global_trace(self, frame, event, argument):
|
|
140
|
+
relative = self.relative(frame.f_code.co_filename)
|
|
141
|
+
if not relative:
|
|
142
|
+
return None
|
|
143
|
+
lines = self.lines_of(relative)
|
|
144
|
+
|
|
145
|
+
def local_trace(local_frame, local_event, local_argument):
|
|
146
|
+
if local_event == "line":
|
|
147
|
+
lines.add(local_frame.f_lineno)
|
|
148
|
+
return local_trace
|
|
149
|
+
|
|
150
|
+
return local_trace
|
|
151
|
+
|
|
152
|
+
def start_monitoring(self):
|
|
153
|
+
monitoring = getattr(sys, "monitoring", None)
|
|
154
|
+
if monitoring is None:
|
|
155
|
+
return False
|
|
156
|
+
for tool_id in dict.fromkeys((monitoring.COVERAGE_ID, 3, 4, 2, 5)):
|
|
157
|
+
try:
|
|
158
|
+
monitoring.use_tool_id(tool_id, "descry-coverage")
|
|
159
|
+
except ValueError:
|
|
160
|
+
continue
|
|
161
|
+
monitoring.register_callback(tool_id, monitoring.events.LINE, self.on_line)
|
|
162
|
+
monitoring.set_events(tool_id, monitoring.events.LINE)
|
|
163
|
+
self.mechanism = "monitoring"
|
|
164
|
+
self.tool_id = tool_id
|
|
165
|
+
if tool_id != monitoring.COVERAGE_ID:
|
|
166
|
+
self.notes.append(
|
|
167
|
+
"sys.monitoring COVERAGE_ID was taken by another tool, so tool id %d was used" % tool_id
|
|
168
|
+
)
|
|
169
|
+
return True
|
|
170
|
+
self.notes.append("every sys.monitoring tool id in use, fell back to sys.settrace")
|
|
171
|
+
return False
|
|
172
|
+
|
|
173
|
+
def start_settrace(self):
|
|
174
|
+
if sys.gettrace() is not None:
|
|
175
|
+
raise RuntimeError("another sys.settrace tracer (a debugger or coverage tool) is already active")
|
|
176
|
+
sys.settrace(self.global_trace)
|
|
177
|
+
threading.settrace(self.global_trace)
|
|
178
|
+
self.mechanism = "settrace"
|
|
179
|
+
self.notes.append("recorded with sys.settrace, which slows the test run noticeably")
|
|
180
|
+
|
|
181
|
+
def start(self):
|
|
182
|
+
if not self.start_monitoring():
|
|
183
|
+
self.start_settrace()
|
|
184
|
+
|
|
185
|
+
def stop(self):
|
|
186
|
+
if self.mechanism == "monitoring":
|
|
187
|
+
monitoring = sys.monitoring
|
|
188
|
+
monitoring.set_events(self.tool_id, 0)
|
|
189
|
+
monitoring.register_callback(self.tool_id, monitoring.events.LINE, None)
|
|
190
|
+
monitoring.free_tool_id(self.tool_id)
|
|
191
|
+
elif self.mechanism == "settrace":
|
|
192
|
+
sys.settrace(None)
|
|
193
|
+
threading.settrace(None)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _coverage_directory():
|
|
197
|
+
return os.environ.get(_COVERAGE_DIRECTORY_VARIABLE) or None
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _write_coverage_file(payload):
|
|
201
|
+
directory = _coverage_directory()
|
|
202
|
+
if directory is None:
|
|
203
|
+
return
|
|
204
|
+
pid = os.getpid()
|
|
205
|
+
temporary = os.path.join(directory, ".coverage-%d.tmp" % pid)
|
|
206
|
+
with open(temporary, "w", encoding="utf-8") as handle:
|
|
207
|
+
json.dump(payload, handle)
|
|
208
|
+
os.replace(temporary, os.path.join(directory, "coverage-%d.json" % pid))
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _write_unavailable(reason):
|
|
212
|
+
try:
|
|
213
|
+
_write_coverage_file({"unavailable": reason})
|
|
214
|
+
except Exception:
|
|
215
|
+
return
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _start_recording():
|
|
219
|
+
global _RECORDER
|
|
220
|
+
root = os.environ.get(_COVERAGE_ROOT_VARIABLE)
|
|
221
|
+
if _RECORDER is not None or _coverage_directory() is None or not root:
|
|
222
|
+
return
|
|
223
|
+
recorder = _LineRecorder(os.path.realpath(root))
|
|
224
|
+
try:
|
|
225
|
+
recorder.start()
|
|
226
|
+
except Exception as error:
|
|
227
|
+
_write_unavailable("%s: %s" % (type(error).__name__, error))
|
|
228
|
+
return
|
|
229
|
+
_RECORDER = recorder
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _finish_recording():
|
|
233
|
+
global _RECORDER
|
|
234
|
+
recorder, _RECORDER = _RECORDER, None
|
|
235
|
+
if recorder is None:
|
|
236
|
+
return
|
|
237
|
+
try:
|
|
238
|
+
recorder.stop()
|
|
239
|
+
files = {
|
|
240
|
+
relative: sorted(lines)
|
|
241
|
+
for relative, lines in recorder.executed.items()
|
|
242
|
+
if relative not in _TEST_FILES and relative.rsplit("/", 1)[-1] != "conftest.py"
|
|
243
|
+
}
|
|
244
|
+
_write_coverage_file({"tool": recorder.mechanism, "notes": recorder.notes, "files": files})
|
|
245
|
+
except Exception as error:
|
|
246
|
+
_write_unavailable("%s: %s" % (type(error).__name__, error))
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _note_test_files(items):
|
|
250
|
+
recorder = _RECORDER
|
|
251
|
+
if recorder is None:
|
|
252
|
+
return
|
|
253
|
+
for item in items:
|
|
254
|
+
path = getattr(item, "path", None)
|
|
255
|
+
filename = str(path) if path is not None else str(item.fspath)
|
|
256
|
+
relative = recorder.relative(filename)
|
|
257
|
+
if relative:
|
|
258
|
+
_TEST_FILES.add(relative)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
@pytest.hookimpl(tryfirst=True)
|
|
262
|
+
def pytest_load_initial_conftests(early_config, parser, args):
|
|
263
|
+
_start_recording()
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _emit(line: str) -> None:
|
|
267
|
+
"""Write one NDJSON line to the real stdout.
|
|
268
|
+
|
|
269
|
+
Two hazards, both measured rather than anticipated.
|
|
270
|
+
|
|
271
|
+
**Capture.** pytest captures stdout at the *file-descriptor* level by
|
|
272
|
+
default (``--capture=fd``), which swallows a plain ``print`` from a hook
|
|
273
|
+
as readily as one from a test body. The capture manager's own context is
|
|
274
|
+
the supported way out, and is what pytest's terminal reporter uses for
|
|
275
|
+
the same purpose.
|
|
276
|
+
|
|
277
|
+
**The leading newline is not cosmetic.** pytest's progress characters
|
|
278
|
+
are written with no trailing newline, so a report emitted from
|
|
279
|
+
``pytest_runtest_logreport`` lands on the same line::
|
|
280
|
+
|
|
281
|
+
.{"type": "test:pass", ...}
|
|
282
|
+
F{"type": "test:fail", ...}
|
|
283
|
+
|
|
284
|
+
That line is not JSON. The collector would read every result as noise
|
|
285
|
+
and keep only ``test:start`` — measured exactly that way on the first
|
|
286
|
+
run of this plugin: eight starts, zero outcomes, and no error anywhere,
|
|
287
|
+
because unparseable stdout is *supposed* to be ignorable. A reporter
|
|
288
|
+
that silently reports only starts is the shape §17 exists to ban, so the
|
|
289
|
+
fix is to always begin on a fresh line rather than to depend on a
|
|
290
|
+
verbosity setting where it happens not to bite.
|
|
291
|
+
"""
|
|
292
|
+
text = "\n" + line + "\n"
|
|
293
|
+
if _CAPTURE_MANAGER is None:
|
|
294
|
+
sys.stdout.write(text)
|
|
295
|
+
sys.stdout.flush()
|
|
296
|
+
return
|
|
297
|
+
with _CAPTURE_MANAGER.global_and_fixture_disabled():
|
|
298
|
+
sys.stdout.write(text)
|
|
299
|
+
sys.stdout.flush()
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def pytest_configure(config):
|
|
303
|
+
global _CAPTURE_MANAGER
|
|
304
|
+
_CAPTURE_MANAGER = config.pluginmanager.getplugin("capturemanager")
|
|
305
|
+
_start_recording()
|
|
306
|
+
_emit(json.dumps({"type": "descry:envelope", "data": {"version": ENVELOPE_VERSION}}))
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def pytest_collection_modifyitems(items):
|
|
310
|
+
"""Read the markers and the real names off each item, once."""
|
|
311
|
+
_note_test_files(items)
|
|
312
|
+
for item in items:
|
|
313
|
+
markers = {mark.name for mark in item.iter_markers()}
|
|
314
|
+
module = getattr(item, "module", None)
|
|
315
|
+
cls = getattr(item, "cls", None)
|
|
316
|
+
parts = [getattr(module, "__name__", None), getattr(cls, "__name__", None), getattr(item, "originalname", None) or item.name]
|
|
317
|
+
location = getattr(item, "location", (None, None, None))
|
|
318
|
+
_COLLECTED[item.nodeid] = {
|
|
319
|
+
"markers": markers,
|
|
320
|
+
# `originalname` is the declaring function for a parameterised
|
|
321
|
+
# case, whose `name` carries its arguments. Both are emitted so
|
|
322
|
+
# the translator is never left to strip anything.
|
|
323
|
+
"qualifiedName": ".".join(p for p in parts if p),
|
|
324
|
+
"file": _absolute_file(item, location[0]),
|
|
325
|
+
# pytest's `location` line is 0-based; every other reporter in
|
|
326
|
+
# this envelope emits 1-based, which is what an editor opens.
|
|
327
|
+
"line": None if location[1] is None else location[1] + 1,
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def _absolute_file(item, relative):
|
|
332
|
+
path = getattr(item, "path", None) or getattr(item, "fspath", None)
|
|
333
|
+
return str(path) if path is not None else relative
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def _common(report) -> dict:
|
|
337
|
+
collected = _COLLECTED.get(report.nodeid, {})
|
|
338
|
+
data = {"name": report.nodeid.split("::")[-1]}
|
|
339
|
+
for key in ("qualifiedName", "file", "line"):
|
|
340
|
+
value = collected.get(key)
|
|
341
|
+
if value is not None:
|
|
342
|
+
data[key] = value
|
|
343
|
+
return data
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _skip_reason(report) -> str:
|
|
347
|
+
markers = _COLLECTED.get(report.nodeid, {}).get("markers", set())
|
|
348
|
+
# `wasxfail` is set by pytest on any report an xfail marker governed,
|
|
349
|
+
# including one raised by `pytest.xfail()` at runtime, so it is checked
|
|
350
|
+
# before the markers rather than after.
|
|
351
|
+
if hasattr(report, "wasxfail") or "xfail" in markers:
|
|
352
|
+
return "not-implemented"
|
|
353
|
+
if "skipif" in markers:
|
|
354
|
+
return "precondition-unmet"
|
|
355
|
+
if "skip" in markers:
|
|
356
|
+
return "disabled"
|
|
357
|
+
# No marker: `pytest.skip()` or `pytest.importorskip()` inside the test
|
|
358
|
+
# body. It ran real code and bailed on an assumption — JUnit's ABORTED.
|
|
359
|
+
return "precondition-unmet"
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def pytest_runtest_logreport(report):
|
|
363
|
+
if report.when == "setup" and report.outcome == "passed":
|
|
364
|
+
_emit(json.dumps({"type": "test:start", "data": _common(report)}))
|
|
365
|
+
return
|
|
366
|
+
|
|
367
|
+
if report.outcome == "skipped":
|
|
368
|
+
data = _common(report)
|
|
369
|
+
data["skip"] = True
|
|
370
|
+
data["skipReason"] = _skip_reason(report)
|
|
371
|
+
if report.longreprtext:
|
|
372
|
+
data["message"] = report.longreprtext
|
|
373
|
+
_emit(json.dumps({"type": "test:pass", "data": data}))
|
|
374
|
+
return
|
|
375
|
+
|
|
376
|
+
if report.outcome == "failed":
|
|
377
|
+
data = _common(report)
|
|
378
|
+
data["details"] = {"error": {"message": report.longreprtext}}
|
|
379
|
+
if report.when != "call":
|
|
380
|
+
# A fixture failure. Named in the payload rather than collapsed,
|
|
381
|
+
# because "the test failed" and "the test never ran" are
|
|
382
|
+
# different facts and only this reporter can tell them apart.
|
|
383
|
+
data["details"]["error"]["failureType"] = f"{report.when}Error"
|
|
384
|
+
_emit(json.dumps({"type": "test:fail", "data": data}))
|
|
385
|
+
return
|
|
386
|
+
|
|
387
|
+
if report.when == "call" and report.outcome == "passed":
|
|
388
|
+
data = _common(report)
|
|
389
|
+
if report.duration is not None:
|
|
390
|
+
data["details"] = {"duration_ms": report.duration * 1000}
|
|
391
|
+
_emit(json.dumps({"type": "test:pass", "data": data}))
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
@pytest.hookimpl(trylast=True)
|
|
395
|
+
def pytest_unconfigure(config): # noqa: ARG001 - pytest's signature
|
|
396
|
+
_finish_recording()
|
|
397
|
+
_COLLECTED.clear()
|