mirror of
https://github.com/dorny/test-reporter.git
synced 2026-02-01 02:45:22 -08:00
Support for the PHPUnit dialect of JUnit
Refactor PHPUnit support into separate phpunit-junit parser Instead of modifying the Java JUnit parser, this creates a dedicated PHPUnit parser that properly handles PHPUnit's nested testsuite elements. This keeps the parsers cleanly separated and allows for future PHPUnit- specific features. Co-Authored-By: Matteo Beccati <matteo@beccati.com> Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
10
README.md
10
README.md
@@ -20,6 +20,7 @@ This [Github Action](https://github.com/features/actions) displays test results
|
|||||||
- Java / [JUnit](https://junit.org/)
|
- Java / [JUnit](https://junit.org/)
|
||||||
- JavaScript / [JEST](https://jestjs.io/) / [Mocha](https://mochajs.org/)
|
- JavaScript / [JEST](https://jestjs.io/) / [Mocha](https://mochajs.org/)
|
||||||
- Python / [pytest](https://docs.pytest.org/en/stable/) / [unittest](https://docs.python.org/3/library/unittest.html)
|
- Python / [pytest](https://docs.pytest.org/en/stable/) / [unittest](https://docs.python.org/3/library/unittest.html)
|
||||||
|
- PHP / [PHPUnit](https://phpunit.de/)
|
||||||
- Ruby / [RSpec](https://rspec.info/)
|
- Ruby / [RSpec](https://rspec.info/)
|
||||||
- Swift / xUnit
|
- Swift / xUnit
|
||||||
|
|
||||||
@@ -147,6 +148,7 @@ jobs:
|
|||||||
# java-junit
|
# java-junit
|
||||||
# jest-junit
|
# jest-junit
|
||||||
# mocha-json
|
# mocha-json
|
||||||
|
# phpunit-junit
|
||||||
# python-xunit
|
# python-xunit
|
||||||
# rspec-json
|
# rspec-json
|
||||||
# swift-xunit
|
# swift-xunit
|
||||||
@@ -314,6 +316,14 @@ This is due to the fact Java stack traces don't contain a full path to the sourc
|
|||||||
Some heuristic was necessary to figure out the mapping between the line in the stack trace and an actual source file.
|
Some heuristic was necessary to figure out the mapping between the line in the stack trace and an actual source file.
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>phpunit-junit</summary>
|
||||||
|
|
||||||
|
[PHPUnit](https://phpunit.de/) can generate JUnit XML via CLI:
|
||||||
|
`phpunit --log-junit reports/phpunit-junit.xml`
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>jest-junit</summary>
|
<summary>jest-junit</summary>
|
||||||
|
|
||||||
|
|||||||
38
__tests__/__outputs__/phpunit-test-results.md
Normal file
38
__tests__/__outputs__/phpunit-test-results.md
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|

|
||||||
|
## ❌ <a id="user-content-r0" href="#r0">fixtures/phpunit/phpunit.xml</a>
|
||||||
|
**12** tests were completed in **148ms** with **10** passed, **2** failed and **0** skipped.
|
||||||
|
|Test suite|Passed|Failed|Skipped|Time|
|
||||||
|
|:---|---:|---:|---:|---:|
|
||||||
|
|[CLI Arguments](#r0s0)||2❌||140ms|
|
||||||
|
|[PHPUnit\Event\CollectingDispatcherTest](#r0s1)|2✅|||4ms|
|
||||||
|
|[PHPUnit\Event\DeferringDispatcherTest](#r0s2)|4✅|||3ms|
|
||||||
|
|[PHPUnit\Event\DirectDispatcherTest](#r0s3)|4✅|||1ms|
|
||||||
|
### ❌ <a id="user-content-r0s0" href="#r0s0">CLI Arguments</a>
|
||||||
|
```
|
||||||
|
❌ targeting-traits-with-coversclass-attribute-is-deprecated.phpt
|
||||||
|
PHPUnit\Framework\PhptAssertionFailedError
|
||||||
|
❌ targeting-traits-with-usesclass-attribute-is-deprecated.phpt
|
||||||
|
PHPUnit\Framework\PhptAssertionFailedError
|
||||||
|
```
|
||||||
|
### ✅ <a id="user-content-r0s1" href="#r0s1">PHPUnit\Event\CollectingDispatcherTest</a>
|
||||||
|
```
|
||||||
|
PHPUnit.Event.CollectingDispatcherTest
|
||||||
|
✅ testHasNoCollectedEventsWhenFlushedImmediatelyAfterCreation
|
||||||
|
✅ testCollectsDispatchedEventsUntilFlushed
|
||||||
|
```
|
||||||
|
### ✅ <a id="user-content-r0s2" href="#r0s2">PHPUnit\Event\DeferringDispatcherTest</a>
|
||||||
|
```
|
||||||
|
PHPUnit.Event.DeferringDispatcherTest
|
||||||
|
✅ testCollectsEventsUntilFlush
|
||||||
|
✅ testFlushesCollectedEvents
|
||||||
|
✅ testSubscriberCanBeRegistered
|
||||||
|
✅ testTracerCanBeRegistered
|
||||||
|
```
|
||||||
|
### ✅ <a id="user-content-r0s3" href="#r0s3">PHPUnit\Event\DirectDispatcherTest</a>
|
||||||
|
```
|
||||||
|
PHPUnit.Event.DirectDispatcherTest
|
||||||
|
✅ testDispatchesEventToKnownSubscribers
|
||||||
|
✅ testDispatchesEventToTracers
|
||||||
|
✅ testRegisterRejectsUnknownSubscriber
|
||||||
|
✅ testDispatchRejectsUnknownEventType
|
||||||
|
```
|
||||||
188
__tests__/__snapshots__/phpunit-junit.test.ts.snap
Normal file
188
__tests__/__snapshots__/phpunit-junit.test.ts.snap
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||||
|
|
||||||
|
exports[`phpunit-junit tests report from phpunit test results matches snapshot 1`] = `
|
||||||
|
TestRunResult {
|
||||||
|
"path": "fixtures/phpunit/phpunit.xml",
|
||||||
|
"suites": [
|
||||||
|
TestSuiteResult {
|
||||||
|
"groups": [
|
||||||
|
TestGroupResult {
|
||||||
|
"name": "PHPUnit.Event.CollectingDispatcherTest",
|
||||||
|
"tests": [
|
||||||
|
TestCaseResult {
|
||||||
|
"error": undefined,
|
||||||
|
"name": "testHasNoCollectedEventsWhenFlushedImmediatelyAfterCreation",
|
||||||
|
"result": "success",
|
||||||
|
"time": 1.441,
|
||||||
|
},
|
||||||
|
TestCaseResult {
|
||||||
|
"error": undefined,
|
||||||
|
"name": "testCollectsDispatchedEventsUntilFlushed",
|
||||||
|
"result": "success",
|
||||||
|
"time": 2.815,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"name": "PHPUnit\\Event\\CollectingDispatcherTest",
|
||||||
|
"totalTime": 4.256,
|
||||||
|
},
|
||||||
|
TestSuiteResult {
|
||||||
|
"groups": [
|
||||||
|
TestGroupResult {
|
||||||
|
"name": "PHPUnit.Event.DeferringDispatcherTest",
|
||||||
|
"tests": [
|
||||||
|
TestCaseResult {
|
||||||
|
"error": undefined,
|
||||||
|
"name": "testCollectsEventsUntilFlush",
|
||||||
|
"result": "success",
|
||||||
|
"time": 1.6720000000000002,
|
||||||
|
},
|
||||||
|
TestCaseResult {
|
||||||
|
"error": undefined,
|
||||||
|
"name": "testFlushesCollectedEvents",
|
||||||
|
"result": "success",
|
||||||
|
"time": 0.661,
|
||||||
|
},
|
||||||
|
TestCaseResult {
|
||||||
|
"error": undefined,
|
||||||
|
"name": "testSubscriberCanBeRegistered",
|
||||||
|
"result": "success",
|
||||||
|
"time": 0.33399999999999996,
|
||||||
|
},
|
||||||
|
TestCaseResult {
|
||||||
|
"error": undefined,
|
||||||
|
"name": "testTracerCanBeRegistered",
|
||||||
|
"result": "success",
|
||||||
|
"time": 0.262,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"name": "PHPUnit\\Event\\DeferringDispatcherTest",
|
||||||
|
"totalTime": 2.928,
|
||||||
|
},
|
||||||
|
TestSuiteResult {
|
||||||
|
"groups": [
|
||||||
|
TestGroupResult {
|
||||||
|
"name": "PHPUnit.Event.DirectDispatcherTest",
|
||||||
|
"tests": [
|
||||||
|
TestCaseResult {
|
||||||
|
"error": undefined,
|
||||||
|
"name": "testDispatchesEventToKnownSubscribers",
|
||||||
|
"result": "success",
|
||||||
|
"time": 0.17,
|
||||||
|
},
|
||||||
|
TestCaseResult {
|
||||||
|
"error": undefined,
|
||||||
|
"name": "testDispatchesEventToTracers",
|
||||||
|
"result": "success",
|
||||||
|
"time": 0.248,
|
||||||
|
},
|
||||||
|
TestCaseResult {
|
||||||
|
"error": undefined,
|
||||||
|
"name": "testRegisterRejectsUnknownSubscriber",
|
||||||
|
"result": "success",
|
||||||
|
"time": 0.257,
|
||||||
|
},
|
||||||
|
TestCaseResult {
|
||||||
|
"error": undefined,
|
||||||
|
"name": "testDispatchRejectsUnknownEventType",
|
||||||
|
"result": "success",
|
||||||
|
"time": 0.11900000000000001,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"name": "PHPUnit\\Event\\DirectDispatcherTest",
|
||||||
|
"totalTime": 0.794,
|
||||||
|
},
|
||||||
|
TestSuiteResult {
|
||||||
|
"groups": [
|
||||||
|
TestGroupResult {
|
||||||
|
"name": "",
|
||||||
|
"tests": [
|
||||||
|
TestCaseResult {
|
||||||
|
"error": {
|
||||||
|
"details": "targeting-traits-with-coversclass-attribute-is-deprecated.phptFailed asserting that string matches format description.
|
||||||
|
--- Expected
|
||||||
|
+++ Actual
|
||||||
|
@@ @@
|
||||||
|
PHPUnit Started (PHPUnit 11.2-g0c2333363 using PHP 8.2.17 (cli) on Linux)
|
||||||
|
Test Runner Configured
|
||||||
|
Test Suite Loaded (1 test)
|
||||||
|
+Test Runner Triggered Warning (No code coverage driver available)
|
||||||
|
Event Facade Sealed
|
||||||
|
Test Runner Started
|
||||||
|
Test Suite Sorted
|
||||||
|
@@ @@
|
||||||
|
Test Preparation Started (PHPUnit\\DeprecatedAnnotationsTestFixture\\TraitTargetedWithCoversClassTest::testSomething)
|
||||||
|
Test Prepared (PHPUnit\\DeprecatedAnnotationsTestFixture\\TraitTargetedWithCoversClassTest::testSomething)
|
||||||
|
Test Passed (PHPUnit\\DeprecatedAnnotationsTestFixture\\TraitTargetedWithCoversClassTest::testSomething)
|
||||||
|
-Test Runner Triggered Deprecation (Targeting a trait such as PHPUnit\\TestFixture\\CoveredTrait with #[CoversClass] is deprecated, please refactor your test to use #[CoversTrait] instead.)
|
||||||
|
Test Finished (PHPUnit\\DeprecatedAnnotationsTestFixture\\TraitTargetedWithCoversClassTest::testSomething)
|
||||||
|
Test Suite Finished (PHPUnit\\DeprecatedAnnotationsTestFixture\\TraitTargetedWithCoversClassTest, 1 test)
|
||||||
|
Test Runner Execution Finished
|
||||||
|
Test Runner Finished
|
||||||
|
-PHPUnit Finished (Shell Exit Code: 0)
|
||||||
|
+PHPUnit Finished (Shell Exit Code: 1)
|
||||||
|
|
||||||
|
/home/matteo/OSS/phpunit/tests/end-to-end/metadata/targeting-traits-with-coversclass-attribute-is-deprecated.phpt:28
|
||||||
|
/home/matteo/OSS/phpunit/src/Framework/TestSuite.php:369
|
||||||
|
/home/matteo/OSS/phpunit/src/TextUI/TestRunner.php:62
|
||||||
|
/home/matteo/OSS/phpunit/src/TextUI/Application.php:200",
|
||||||
|
"line": undefined,
|
||||||
|
"message": "PHPUnit\\Framework\\PhptAssertionFailedError",
|
||||||
|
"path": undefined,
|
||||||
|
},
|
||||||
|
"name": "targeting-traits-with-coversclass-attribute-is-deprecated.phpt",
|
||||||
|
"result": "failed",
|
||||||
|
"time": 68.151,
|
||||||
|
},
|
||||||
|
TestCaseResult {
|
||||||
|
"error": {
|
||||||
|
"details": "targeting-traits-with-usesclass-attribute-is-deprecated.phptFailed asserting that string matches format description.
|
||||||
|
--- Expected
|
||||||
|
+++ Actual
|
||||||
|
@@ @@
|
||||||
|
PHPUnit Started (PHPUnit 11.2-g0c2333363 using PHP 8.2.17 (cli) on Linux)
|
||||||
|
Test Runner Configured
|
||||||
|
Test Suite Loaded (1 test)
|
||||||
|
+Test Runner Triggered Warning (No code coverage driver available)
|
||||||
|
Event Facade Sealed
|
||||||
|
Test Runner Started
|
||||||
|
Test Suite Sorted
|
||||||
|
@@ @@
|
||||||
|
Test Preparation Started (PHPUnit\\DeprecatedAnnotationsTestFixture\\TraitTargetedWithUsesClassTest::testSomething)
|
||||||
|
Test Prepared (PHPUnit\\DeprecatedAnnotationsTestFixture\\TraitTargetedWithUsesClassTest::testSomething)
|
||||||
|
Test Passed (PHPUnit\\DeprecatedAnnotationsTestFixture\\TraitTargetedWithUsesClassTest::testSomething)
|
||||||
|
-Test Runner Triggered Deprecation (Targeting a trait such as PHPUnit\\TestFixture\\CoveredTrait with #[UsesClass] is deprecated, please refactor your test to use #[UsesTrait] instead.)
|
||||||
|
Test Finished (PHPUnit\\DeprecatedAnnotationsTestFixture\\TraitTargetedWithUsesClassTest::testSomething)
|
||||||
|
Test Suite Finished (PHPUnit\\DeprecatedAnnotationsTestFixture\\TraitTargetedWithUsesClassTest, 1 test)
|
||||||
|
Test Runner Execution Finished
|
||||||
|
Test Runner Finished
|
||||||
|
-PHPUnit Finished (Shell Exit Code: 0)
|
||||||
|
+PHPUnit Finished (Shell Exit Code: 1)
|
||||||
|
|
||||||
|
/home/matteo/OSS/phpunit/tests/end-to-end/metadata/targeting-traits-with-usesclass-attribute-is-deprecated.phpt:28
|
||||||
|
/home/matteo/OSS/phpunit/src/Framework/TestSuite.php:369
|
||||||
|
/home/matteo/OSS/phpunit/src/TextUI/TestRunner.php:62
|
||||||
|
/home/matteo/OSS/phpunit/src/TextUI/Application.php:200",
|
||||||
|
"line": undefined,
|
||||||
|
"message": "PHPUnit\\Framework\\PhptAssertionFailedError",
|
||||||
|
"path": undefined,
|
||||||
|
},
|
||||||
|
"name": "targeting-traits-with-usesclass-attribute-is-deprecated.phpt",
|
||||||
|
"result": "failed",
|
||||||
|
"time": 64.268,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"name": "CLI Arguments",
|
||||||
|
"totalTime": 140.397,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"totalTime": undefined,
|
||||||
|
}
|
||||||
|
`;
|
||||||
2
__tests__/fixtures/empty/phpunit-empty.xml
Normal file
2
__tests__/fixtures/empty/phpunit-empty.xml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<testsuites/>
|
||||||
79
__tests__/fixtures/phpunit/phpunit.xml
Normal file
79
__tests__/fixtures/phpunit/phpunit.xml
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<testsuites>
|
||||||
|
<testsuite name="CLI Arguments" tests="12" assertions="12" errors="0" failures="2" skipped="0" time="0.140397">
|
||||||
|
<testcase name="targeting-traits-with-coversclass-attribute-is-deprecated.phpt" file="/home/matteo/OSS/phpunit/tests/end-to-end/metadata/targeting-traits-with-coversclass-attribute-is-deprecated.phpt" assertions="1" time="0.068151">
|
||||||
|
<failure type="PHPUnit\Framework\PhptAssertionFailedError">targeting-traits-with-coversclass-attribute-is-deprecated.phptFailed asserting that string matches format description.
|
||||||
|
--- Expected
|
||||||
|
+++ Actual
|
||||||
|
@@ @@
|
||||||
|
PHPUnit Started (PHPUnit 11.2-g0c2333363 using PHP 8.2.17 (cli) on Linux)
|
||||||
|
Test Runner Configured
|
||||||
|
Test Suite Loaded (1 test)
|
||||||
|
+Test Runner Triggered Warning (No code coverage driver available)
|
||||||
|
Event Facade Sealed
|
||||||
|
Test Runner Started
|
||||||
|
Test Suite Sorted
|
||||||
|
@@ @@
|
||||||
|
Test Preparation Started (PHPUnit\DeprecatedAnnotationsTestFixture\TraitTargetedWithCoversClassTest::testSomething)
|
||||||
|
Test Prepared (PHPUnit\DeprecatedAnnotationsTestFixture\TraitTargetedWithCoversClassTest::testSomething)
|
||||||
|
Test Passed (PHPUnit\DeprecatedAnnotationsTestFixture\TraitTargetedWithCoversClassTest::testSomething)
|
||||||
|
-Test Runner Triggered Deprecation (Targeting a trait such as PHPUnit\TestFixture\CoveredTrait with #[CoversClass] is deprecated, please refactor your test to use #[CoversTrait] instead.)
|
||||||
|
Test Finished (PHPUnit\DeprecatedAnnotationsTestFixture\TraitTargetedWithCoversClassTest::testSomething)
|
||||||
|
Test Suite Finished (PHPUnit\DeprecatedAnnotationsTestFixture\TraitTargetedWithCoversClassTest, 1 test)
|
||||||
|
Test Runner Execution Finished
|
||||||
|
Test Runner Finished
|
||||||
|
-PHPUnit Finished (Shell Exit Code: 0)
|
||||||
|
+PHPUnit Finished (Shell Exit Code: 1)
|
||||||
|
|
||||||
|
/home/matteo/OSS/phpunit/tests/end-to-end/metadata/targeting-traits-with-coversclass-attribute-is-deprecated.phpt:28
|
||||||
|
/home/matteo/OSS/phpunit/src/Framework/TestSuite.php:369
|
||||||
|
/home/matteo/OSS/phpunit/src/TextUI/TestRunner.php:62
|
||||||
|
/home/matteo/OSS/phpunit/src/TextUI/Application.php:200</failure>
|
||||||
|
</testcase>
|
||||||
|
<testcase name="targeting-traits-with-usesclass-attribute-is-deprecated.phpt" file="/home/matteo/OSS/phpunit/tests/end-to-end/metadata/targeting-traits-with-usesclass-attribute-is-deprecated.phpt" assertions="1" time="0.064268">
|
||||||
|
<failure type="PHPUnit\Framework\PhptAssertionFailedError">targeting-traits-with-usesclass-attribute-is-deprecated.phptFailed asserting that string matches format description.
|
||||||
|
--- Expected
|
||||||
|
+++ Actual
|
||||||
|
@@ @@
|
||||||
|
PHPUnit Started (PHPUnit 11.2-g0c2333363 using PHP 8.2.17 (cli) on Linux)
|
||||||
|
Test Runner Configured
|
||||||
|
Test Suite Loaded (1 test)
|
||||||
|
+Test Runner Triggered Warning (No code coverage driver available)
|
||||||
|
Event Facade Sealed
|
||||||
|
Test Runner Started
|
||||||
|
Test Suite Sorted
|
||||||
|
@@ @@
|
||||||
|
Test Preparation Started (PHPUnit\DeprecatedAnnotationsTestFixture\TraitTargetedWithUsesClassTest::testSomething)
|
||||||
|
Test Prepared (PHPUnit\DeprecatedAnnotationsTestFixture\TraitTargetedWithUsesClassTest::testSomething)
|
||||||
|
Test Passed (PHPUnit\DeprecatedAnnotationsTestFixture\TraitTargetedWithUsesClassTest::testSomething)
|
||||||
|
-Test Runner Triggered Deprecation (Targeting a trait such as PHPUnit\TestFixture\CoveredTrait with #[UsesClass] is deprecated, please refactor your test to use #[UsesTrait] instead.)
|
||||||
|
Test Finished (PHPUnit\DeprecatedAnnotationsTestFixture\TraitTargetedWithUsesClassTest::testSomething)
|
||||||
|
Test Suite Finished (PHPUnit\DeprecatedAnnotationsTestFixture\TraitTargetedWithUsesClassTest, 1 test)
|
||||||
|
Test Runner Execution Finished
|
||||||
|
Test Runner Finished
|
||||||
|
-PHPUnit Finished (Shell Exit Code: 0)
|
||||||
|
+PHPUnit Finished (Shell Exit Code: 1)
|
||||||
|
|
||||||
|
/home/matteo/OSS/phpunit/tests/end-to-end/metadata/targeting-traits-with-usesclass-attribute-is-deprecated.phpt:28
|
||||||
|
/home/matteo/OSS/phpunit/src/Framework/TestSuite.php:369
|
||||||
|
/home/matteo/OSS/phpunit/src/TextUI/TestRunner.php:62
|
||||||
|
/home/matteo/OSS/phpunit/src/TextUI/Application.php:200</failure>
|
||||||
|
</testcase>
|
||||||
|
<testsuite name="PHPUnit\Event\CollectingDispatcherTest" file="/home/matteo/OSS/phpunit/tests/unit/Event/Dispatcher/CollectingDispatcherTest.php" tests="2" assertions="2" errors="0" failures="0" skipped="0" time="0.004256">
|
||||||
|
<testcase name="testHasNoCollectedEventsWhenFlushedImmediatelyAfterCreation" file="/home/matteo/OSS/phpunit/tests/unit/Event/Dispatcher/CollectingDispatcherTest.php" line="20" class="PHPUnit\Event\CollectingDispatcherTest" classname="PHPUnit.Event.CollectingDispatcherTest" assertions="1" time="0.001441"/>
|
||||||
|
<testcase name="testCollectsDispatchedEventsUntilFlushed" file="/home/matteo/OSS/phpunit/tests/unit/Event/Dispatcher/CollectingDispatcherTest.php" line="27" class="PHPUnit\Event\CollectingDispatcherTest" classname="PHPUnit.Event.CollectingDispatcherTest" assertions="1" time="0.002815"/>
|
||||||
|
</testsuite>
|
||||||
|
<testsuite name="PHPUnit\Event\DeferringDispatcherTest" file="/home/matteo/OSS/phpunit/tests/unit/Event/Dispatcher/DeferringDispatcherTest.php" tests="4" assertions="4" errors="0" failures="0" skipped="0" time="0.002928">
|
||||||
|
<testcase name="testCollectsEventsUntilFlush" file="/home/matteo/OSS/phpunit/tests/unit/Event/Dispatcher/DeferringDispatcherTest.php" line="22" class="PHPUnit\Event\DeferringDispatcherTest" classname="PHPUnit.Event.DeferringDispatcherTest" assertions="1" time="0.001672"/>
|
||||||
|
<testcase name="testFlushesCollectedEvents" file="/home/matteo/OSS/phpunit/tests/unit/Event/Dispatcher/DeferringDispatcherTest.php" line="35" class="PHPUnit\Event\DeferringDispatcherTest" classname="PHPUnit.Event.DeferringDispatcherTest" assertions="1" time="0.000661"/>
|
||||||
|
<testcase name="testSubscriberCanBeRegistered" file="/home/matteo/OSS/phpunit/tests/unit/Event/Dispatcher/DeferringDispatcherTest.php" line="53" class="PHPUnit\Event\DeferringDispatcherTest" classname="PHPUnit.Event.DeferringDispatcherTest" assertions="1" time="0.000334"/>
|
||||||
|
<testcase name="testTracerCanBeRegistered" file="/home/matteo/OSS/phpunit/tests/unit/Event/Dispatcher/DeferringDispatcherTest.php" line="69" class="PHPUnit\Event\DeferringDispatcherTest" classname="PHPUnit.Event.DeferringDispatcherTest" assertions="1" time="0.000262"/>
|
||||||
|
</testsuite>
|
||||||
|
<testsuite name="PHPUnit\Event\DirectDispatcherTest" file="/home/matteo/OSS/phpunit/tests/unit/Event/Dispatcher/DirectDispatcherTest.php" tests="4" assertions="4" errors="0" failures="0" skipped="0" time="0.000794">
|
||||||
|
<testcase name="testDispatchesEventToKnownSubscribers" file="/home/matteo/OSS/phpunit/tests/unit/Event/Dispatcher/DirectDispatcherTest.php" line="24" class="PHPUnit\Event\DirectDispatcherTest" classname="PHPUnit.Event.DirectDispatcherTest" assertions="1" time="0.000170"/>
|
||||||
|
<testcase name="testDispatchesEventToTracers" file="/home/matteo/OSS/phpunit/tests/unit/Event/Dispatcher/DirectDispatcherTest.php" line="43" class="PHPUnit\Event\DirectDispatcherTest" classname="PHPUnit.Event.DirectDispatcherTest" assertions="1" time="0.000248"/>
|
||||||
|
<testcase name="testRegisterRejectsUnknownSubscriber" file="/home/matteo/OSS/phpunit/tests/unit/Event/Dispatcher/DirectDispatcherTest.php" line="62" class="PHPUnit\Event\DirectDispatcherTest" classname="PHPUnit.Event.DirectDispatcherTest" assertions="1" time="0.000257"/>
|
||||||
|
<testcase name="testDispatchRejectsUnknownEventType" file="/home/matteo/OSS/phpunit/tests/unit/Event/Dispatcher/DirectDispatcherTest.php" line="73" class="PHPUnit\Event\DirectDispatcherTest" classname="PHPUnit.Event.DirectDispatcherTest" assertions="1" time="0.000119"/>
|
||||||
|
</testsuite>
|
||||||
|
</testsuite>
|
||||||
|
</testsuites>
|
||||||
102
__tests__/phpunit-junit.test.ts
Normal file
102
__tests__/phpunit-junit.test.ts
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
import * as fs from 'fs'
|
||||||
|
import * as path from 'path'
|
||||||
|
|
||||||
|
import {PhpunitJunitParser} from '../src/parsers/phpunit-junit/phpunit-junit-parser'
|
||||||
|
import {ParseOptions} from '../src/test-parser'
|
||||||
|
import {getReport} from '../src/report/get-report'
|
||||||
|
import {normalizeFilePath} from '../src/utils/path-utils'
|
||||||
|
|
||||||
|
describe('phpunit-junit tests', () => {
|
||||||
|
it('produces empty test run result when there are no test cases', async () => {
|
||||||
|
const fixturePath = path.join(__dirname, 'fixtures', 'empty', 'phpunit-empty.xml')
|
||||||
|
const filePath = normalizeFilePath(path.relative(__dirname, fixturePath))
|
||||||
|
const fileContent = fs.readFileSync(fixturePath, {encoding: 'utf8'})
|
||||||
|
|
||||||
|
const opts: ParseOptions = {
|
||||||
|
parseErrors: true,
|
||||||
|
trackedFiles: []
|
||||||
|
}
|
||||||
|
|
||||||
|
const parser = new PhpunitJunitParser(opts)
|
||||||
|
const result = await parser.parse(filePath, fileContent)
|
||||||
|
expect(result.tests).toBe(0)
|
||||||
|
expect(result.result).toBe('success')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('report from phpunit test results matches snapshot', async () => {
|
||||||
|
const fixturePath = path.join(__dirname, 'fixtures', 'phpunit', 'phpunit.xml')
|
||||||
|
const outputPath = path.join(__dirname, '__outputs__', 'phpunit-test-results.md')
|
||||||
|
const filePath = normalizeFilePath(path.relative(__dirname, fixturePath))
|
||||||
|
const fileContent = fs.readFileSync(fixturePath, {encoding: 'utf8'})
|
||||||
|
|
||||||
|
const opts: ParseOptions = {
|
||||||
|
parseErrors: true,
|
||||||
|
trackedFiles: []
|
||||||
|
}
|
||||||
|
|
||||||
|
const parser = new PhpunitJunitParser(opts)
|
||||||
|
const result = await parser.parse(filePath, fileContent)
|
||||||
|
expect(result).toMatchSnapshot()
|
||||||
|
|
||||||
|
const report = getReport([result])
|
||||||
|
fs.mkdirSync(path.dirname(outputPath), {recursive: true})
|
||||||
|
fs.writeFileSync(outputPath, report)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('parses nested test suites correctly', async () => {
|
||||||
|
const fixturePath = path.join(__dirname, 'fixtures', 'phpunit', 'phpunit.xml')
|
||||||
|
const filePath = normalizeFilePath(path.relative(__dirname, fixturePath))
|
||||||
|
const fileContent = fs.readFileSync(fixturePath, {encoding: 'utf8'})
|
||||||
|
|
||||||
|
const opts: ParseOptions = {
|
||||||
|
parseErrors: true,
|
||||||
|
trackedFiles: []
|
||||||
|
}
|
||||||
|
|
||||||
|
const parser = new PhpunitJunitParser(opts)
|
||||||
|
const result = await parser.parse(filePath, fileContent)
|
||||||
|
|
||||||
|
// Should have 4 test suites (3 nested ones plus the parent with direct testcases)
|
||||||
|
expect(result.suites.length).toBe(4)
|
||||||
|
|
||||||
|
// Verify suite names
|
||||||
|
const suiteNames = result.suites.map(s => s.name)
|
||||||
|
expect(suiteNames).toContain('PHPUnit\\Event\\CollectingDispatcherTest')
|
||||||
|
expect(suiteNames).toContain('PHPUnit\\Event\\DeferringDispatcherTest')
|
||||||
|
expect(suiteNames).toContain('PHPUnit\\Event\\DirectDispatcherTest')
|
||||||
|
expect(suiteNames).toContain('CLI Arguments')
|
||||||
|
|
||||||
|
// Verify total test count
|
||||||
|
expect(result.tests).toBe(12)
|
||||||
|
expect(result.passed).toBe(10)
|
||||||
|
expect(result.failed).toBe(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('extracts error details from failures', async () => {
|
||||||
|
const fixturePath = path.join(__dirname, 'fixtures', 'phpunit', 'phpunit.xml')
|
||||||
|
const filePath = normalizeFilePath(path.relative(__dirname, fixturePath))
|
||||||
|
const fileContent = fs.readFileSync(fixturePath, {encoding: 'utf8'})
|
||||||
|
|
||||||
|
const opts: ParseOptions = {
|
||||||
|
parseErrors: true,
|
||||||
|
trackedFiles: []
|
||||||
|
}
|
||||||
|
|
||||||
|
const parser = new PhpunitJunitParser(opts)
|
||||||
|
const result = await parser.parse(filePath, fileContent)
|
||||||
|
|
||||||
|
// Find the CLI Arguments suite which has failures
|
||||||
|
const cliSuite = result.suites.find(s => s.name === 'CLI Arguments')
|
||||||
|
expect(cliSuite).toBeDefined()
|
||||||
|
|
||||||
|
// Get the failed tests
|
||||||
|
const failedTests = cliSuite!.groups.flatMap(g => g.tests).filter(t => t.result === 'failed')
|
||||||
|
expect(failedTests.length).toBe(2)
|
||||||
|
|
||||||
|
// Verify error details are captured
|
||||||
|
for (const test of failedTests) {
|
||||||
|
expect(test.error).toBeDefined()
|
||||||
|
expect(test.error!.details).toContain('Failed asserting that string matches format description')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -32,6 +32,7 @@ inputs:
|
|||||||
- java-junit
|
- java-junit
|
||||||
- jest-junit
|
- jest-junit
|
||||||
- mocha-json
|
- mocha-json
|
||||||
|
- phpunit-junit
|
||||||
- python-xunit
|
- python-xunit
|
||||||
- rspec-json
|
- rspec-json
|
||||||
- swift-xunit
|
- swift-xunit
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {GolangJsonParser} from './parsers/golang-json/golang-json-parser'
|
|||||||
import {JavaJunitParser} from './parsers/java-junit/java-junit-parser'
|
import {JavaJunitParser} from './parsers/java-junit/java-junit-parser'
|
||||||
import {JestJunitParser} from './parsers/jest-junit/jest-junit-parser'
|
import {JestJunitParser} from './parsers/jest-junit/jest-junit-parser'
|
||||||
import {MochaJsonParser} from './parsers/mocha-json/mocha-json-parser'
|
import {MochaJsonParser} from './parsers/mocha-json/mocha-json-parser'
|
||||||
|
import {PhpunitJunitParser} from './parsers/phpunit-junit/phpunit-junit-parser'
|
||||||
import {PythonXunitParser} from './parsers/python-xunit/python-xunit-parser'
|
import {PythonXunitParser} from './parsers/python-xunit/python-xunit-parser'
|
||||||
import {RspecJsonParser} from './parsers/rspec-json/rspec-json-parser'
|
import {RspecJsonParser} from './parsers/rspec-json/rspec-json-parser'
|
||||||
import {SwiftXunitParser} from './parsers/swift-xunit/swift-xunit-parser'
|
import {SwiftXunitParser} from './parsers/swift-xunit/swift-xunit-parser'
|
||||||
@@ -271,6 +272,8 @@ class TestReporter {
|
|||||||
return new JestJunitParser(options)
|
return new JestJunitParser(options)
|
||||||
case 'mocha-json':
|
case 'mocha-json':
|
||||||
return new MochaJsonParser(options)
|
return new MochaJsonParser(options)
|
||||||
|
case 'phpunit-junit':
|
||||||
|
return new PhpunitJunitParser(options)
|
||||||
case 'python-xunit':
|
case 'python-xunit':
|
||||||
return new PythonXunitParser(options)
|
return new PythonXunitParser(options)
|
||||||
case 'rspec-json':
|
case 'rspec-json':
|
||||||
|
|||||||
190
src/parsers/phpunit-junit/phpunit-junit-parser.ts
Normal file
190
src/parsers/phpunit-junit/phpunit-junit-parser.ts
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
import {ParseOptions, TestParser} from '../../test-parser'
|
||||||
|
import {parseStringPromise} from 'xml2js'
|
||||||
|
|
||||||
|
import {PhpunitReport, SingleSuiteReport, TestCase, TestSuite} from './phpunit-junit-types'
|
||||||
|
import {normalizeFilePath} from '../../utils/path-utils'
|
||||||
|
|
||||||
|
import {
|
||||||
|
TestExecutionResult,
|
||||||
|
TestRunResult,
|
||||||
|
TestSuiteResult,
|
||||||
|
TestGroupResult,
|
||||||
|
TestCaseResult,
|
||||||
|
TestCaseError
|
||||||
|
} from '../../test-results'
|
||||||
|
|
||||||
|
export class PhpunitJunitParser implements TestParser {
|
||||||
|
readonly trackedFiles: Set<string>
|
||||||
|
|
||||||
|
constructor(readonly options: ParseOptions) {
|
||||||
|
this.trackedFiles = new Set(options.trackedFiles.map(f => normalizeFilePath(f)))
|
||||||
|
}
|
||||||
|
|
||||||
|
async parse(filePath: string, content: string): Promise<TestRunResult> {
|
||||||
|
const reportOrSuite = await this.getPhpunitReport(filePath, content)
|
||||||
|
const isReport = (reportOrSuite as PhpunitReport).testsuites !== undefined
|
||||||
|
|
||||||
|
// XML might contain:
|
||||||
|
// - multiple suites under <testsuites> root node
|
||||||
|
// - single <testsuite> as root node
|
||||||
|
let report: PhpunitReport
|
||||||
|
if (isReport) {
|
||||||
|
report = reportOrSuite as PhpunitReport
|
||||||
|
} else {
|
||||||
|
// Make it behave the same way as if suite was inside <testsuites> root node
|
||||||
|
const suite = (reportOrSuite as SingleSuiteReport).testsuite
|
||||||
|
report = {
|
||||||
|
testsuites: {
|
||||||
|
$: {time: suite.$.time},
|
||||||
|
testsuite: [suite]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.getTestRunResult(filePath, report)
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getPhpunitReport(filePath: string, content: string): Promise<PhpunitReport | SingleSuiteReport> {
|
||||||
|
try {
|
||||||
|
return await parseStringPromise(content)
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error(`Invalid XML at ${filePath}\n\n${e}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getTestRunResult(filePath: string, report: PhpunitReport): TestRunResult {
|
||||||
|
const suites: TestSuiteResult[] = []
|
||||||
|
this.collectSuites(suites, report.testsuites.testsuite ?? [])
|
||||||
|
|
||||||
|
const seconds = parseFloat(report.testsuites.$?.time ?? '')
|
||||||
|
const time = isNaN(seconds) ? undefined : seconds * 1000
|
||||||
|
return new TestRunResult(filePath, suites, time)
|
||||||
|
}
|
||||||
|
|
||||||
|
private collectSuites(results: TestSuiteResult[], testsuites: TestSuite[]): void {
|
||||||
|
for (const ts of testsuites) {
|
||||||
|
// Recursively process nested test suites first (depth-first)
|
||||||
|
if (ts.testsuite) {
|
||||||
|
this.collectSuites(results, ts.testsuite)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only add suites that have direct test cases
|
||||||
|
// This avoids adding container suites that only hold nested suites
|
||||||
|
if (ts.testcase && ts.testcase.length > 0) {
|
||||||
|
const name = ts.$.name.trim()
|
||||||
|
const time = parseFloat(ts.$.time) * 1000
|
||||||
|
results.push(new TestSuiteResult(name, this.getGroups(ts), time))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getGroups(suite: TestSuite): TestGroupResult[] {
|
||||||
|
if (!suite.testcase || suite.testcase.length === 0) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
const groups: {name: string; tests: TestCase[]}[] = []
|
||||||
|
for (const tc of suite.testcase) {
|
||||||
|
// Use classname (PHPUnit style) for grouping
|
||||||
|
// If classname matches suite name, use empty string to avoid redundancy
|
||||||
|
const className = tc.$.classname ?? tc.$.class ?? ''
|
||||||
|
const groupName = className === suite.$.name ? '' : className
|
||||||
|
let grp = groups.find(g => g.name === groupName)
|
||||||
|
if (grp === undefined) {
|
||||||
|
grp = {name: groupName, tests: []}
|
||||||
|
groups.push(grp)
|
||||||
|
}
|
||||||
|
grp.tests.push(tc)
|
||||||
|
}
|
||||||
|
|
||||||
|
return groups.map(grp => {
|
||||||
|
const tests = grp.tests.map(tc => {
|
||||||
|
const name = tc.$.name.trim()
|
||||||
|
const result = this.getTestCaseResult(tc)
|
||||||
|
const time = parseFloat(tc.$.time) * 1000
|
||||||
|
const error = this.getTestCaseError(tc)
|
||||||
|
return new TestCaseResult(name, result, time, error)
|
||||||
|
})
|
||||||
|
return new TestGroupResult(grp.name, tests)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private getTestCaseResult(test: TestCase): TestExecutionResult {
|
||||||
|
if (test.failure || test.error) return 'failed'
|
||||||
|
if (test.skipped) return 'skipped'
|
||||||
|
return 'success'
|
||||||
|
}
|
||||||
|
|
||||||
|
private getTestCaseError(tc: TestCase): TestCaseError | undefined {
|
||||||
|
if (!this.options.parseErrors) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
// We process <error> and <failure> the same way
|
||||||
|
const failures = tc.failure ?? tc.error
|
||||||
|
if (!failures || failures.length === 0) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const failure = failures[0]
|
||||||
|
const details = failure._ ?? ''
|
||||||
|
|
||||||
|
// PHPUnit provides file path directly in testcase attributes
|
||||||
|
let filePath: string | undefined
|
||||||
|
let line: number | undefined
|
||||||
|
|
||||||
|
if (tc.$.file) {
|
||||||
|
const normalizedPath = normalizeFilePath(tc.$.file)
|
||||||
|
if (this.trackedFiles.has(normalizedPath)) {
|
||||||
|
filePath = normalizedPath
|
||||||
|
}
|
||||||
|
if (tc.$.line) {
|
||||||
|
line = parseInt(tc.$.line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If file not in tracked files, try to extract from error details
|
||||||
|
if (!filePath && details) {
|
||||||
|
const extracted = this.extractFileAndLine(details)
|
||||||
|
if (extracted) {
|
||||||
|
filePath = extracted.filePath
|
||||||
|
line = extracted.line
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let message: string | undefined
|
||||||
|
if (failure.$) {
|
||||||
|
message = failure.$.message
|
||||||
|
if (failure.$.type) {
|
||||||
|
message = message ? `${failure.$.type}: ${message}` : failure.$.type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
path: filePath,
|
||||||
|
line,
|
||||||
|
details,
|
||||||
|
message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extractFileAndLine(details: string): {filePath: string; line: number} | undefined {
|
||||||
|
// PHPUnit stack traces typically have format: /path/to/file.php:123
|
||||||
|
const lines = details.split(/\r?\n/)
|
||||||
|
|
||||||
|
for (const str of lines) {
|
||||||
|
// Match patterns like /path/to/file.php:123 or at /path/to/file.php(123)
|
||||||
|
const match = str.match(/([^\s:()]+\.php):(\d+)|([^\s:()]+\.php)\((\d+)\)/)
|
||||||
|
if (match) {
|
||||||
|
const path = match[1] ?? match[3]
|
||||||
|
const lineStr = match[2] ?? match[4]
|
||||||
|
const normalizedPath = normalizeFilePath(path)
|
||||||
|
if (this.trackedFiles.has(normalizedPath)) {
|
||||||
|
return {filePath: normalizedPath, line: parseInt(lineStr)}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
52
src/parsers/phpunit-junit/phpunit-junit-types.ts
Normal file
52
src/parsers/phpunit-junit/phpunit-junit-types.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
export interface PhpunitReport {
|
||||||
|
testsuites: TestSuites
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SingleSuiteReport {
|
||||||
|
testsuite: TestSuite
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TestSuites {
|
||||||
|
$?: {
|
||||||
|
time?: string
|
||||||
|
}
|
||||||
|
testsuite?: TestSuite[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TestSuite {
|
||||||
|
$: {
|
||||||
|
name: string
|
||||||
|
tests?: string
|
||||||
|
assertions?: string
|
||||||
|
errors?: string
|
||||||
|
failures?: string
|
||||||
|
skipped?: string
|
||||||
|
time: string
|
||||||
|
file?: string
|
||||||
|
}
|
||||||
|
testcase?: TestCase[]
|
||||||
|
testsuite?: TestSuite[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TestCase {
|
||||||
|
$: {
|
||||||
|
name: string
|
||||||
|
class?: string
|
||||||
|
classname?: string
|
||||||
|
file?: string
|
||||||
|
line?: string
|
||||||
|
assertions?: string
|
||||||
|
time: string
|
||||||
|
}
|
||||||
|
failure?: Failure[]
|
||||||
|
error?: Failure[]
|
||||||
|
skipped?: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Failure {
|
||||||
|
_: string
|
||||||
|
$?: {
|
||||||
|
type?: string
|
||||||
|
message?: string
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user