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/)
|
||||
- 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)
|
||||
- PHP / [PHPUnit](https://phpunit.de/)
|
||||
- Ruby / [RSpec](https://rspec.info/)
|
||||
- Swift / xUnit
|
||||
|
||||
@@ -147,6 +148,7 @@ jobs:
|
||||
# java-junit
|
||||
# jest-junit
|
||||
# mocha-json
|
||||
# phpunit-junit
|
||||
# python-xunit
|
||||
# rspec-json
|
||||
# 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.
|
||||
</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>
|
||||
<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
|
||||
- jest-junit
|
||||
- mocha-json
|
||||
- phpunit-junit
|
||||
- python-xunit
|
||||
- rspec-json
|
||||
- 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 {JestJunitParser} from './parsers/jest-junit/jest-junit-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 {RspecJsonParser} from './parsers/rspec-json/rspec-json-parser'
|
||||
import {SwiftXunitParser} from './parsers/swift-xunit/swift-xunit-parser'
|
||||
@@ -271,6 +272,8 @@ class TestReporter {
|
||||
return new JestJunitParser(options)
|
||||
case 'mocha-json':
|
||||
return new MochaJsonParser(options)
|
||||
case 'phpunit-junit':
|
||||
return new PhpunitJunitParser(options)
|
||||
case 'python-xunit':
|
||||
return new PythonXunitParser(options)
|
||||
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