Merge pull request #422 from mbeccati/phpunit-support

This commit is contained in:
Jozef Izso
2025-12-29 14:59:02 +01:00
committed by GitHub
16 changed files with 2040 additions and 0 deletions

View File

@@ -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>

View File

@@ -0,0 +1,30 @@
![Tests failed](https://img.shields.io/badge/tests-8%20passed%2C%201%20failed-critical)
|Report|Passed|Failed|Skipped|Time|
|:---|---:|---:|---:|---:|
|[fixtures/external/phpunit/junit-basic.xml](#user-content-r0)|8 ✅|1 ❌||16s|
## ❌ <a id="user-content-r0" href="#user-content-r0">fixtures/external/phpunit/junit-basic.xml</a>
**9** tests were completed in **16s** with **8** passed, **1** failed and **0** skipped.
|Test suite|Passed|Failed|Skipped|Time|
|:---|---:|---:|---:|---:|
|[Tests.Authentication](#user-content-r0s0)|2 ✅|1 ❌||9s|
|[Tests.Authentication.Login](#user-content-r0s1)|3 ✅|||4s|
|[Tests.Registration](#user-content-r0s2)|3 ✅|||7s|
### ❌ <a id="user-content-r0s0" href="#user-content-r0s0">Tests.Authentication</a>
```
✅ testCase7
✅ testCase8
❌ testCase9
AssertionError: Assertion error message
```
### ✅ <a id="user-content-r0s1" href="#user-content-r0s1">Tests.Authentication.Login</a>
```
✅ testCase4
✅ testCase5
✅ testCase6
```
### ✅ <a id="user-content-r0s2" href="#user-content-r0s2">Tests.Registration</a>
```
✅ testCase1
✅ testCase2
✅ testCase3
```

View File

@@ -0,0 +1,88 @@
![Tests failed](https://img.shields.io/badge/tests-28%20passed%2C%202%20failed-critical)
|Report|Passed|Failed|Skipped|Time|
|:---|---:|---:|---:|---:|
|[fixtures/external/phpunit/phpcheckstyle-phpunit.xml](#user-content-r0)|28 ✅|2 ❌||41ms|
## ❌ <a id="user-content-r0" href="#user-content-r0">fixtures/external/phpunit/phpcheckstyle-phpunit.xml</a>
**30** tests were completed in **41ms** with **28** passed, **2** failed and **0** skipped.
|Test suite|Passed|Failed|Skipped|Time|
|:---|---:|---:|---:|---:|
|[CommentsTest](#user-content-r0s0)|3 ✅|||7ms|
|[DeprecationTest](#user-content-r0s1)|1 ✅|||1ms|
|[GoodTest](#user-content-r0s2)|4 ✅|||5ms|
|[IndentationTest](#user-content-r0s3)|8 ✅|||8ms|
|[MetricsTest](#user-content-r0s4)|1 ✅|||4ms|
|[NamingTest](#user-content-r0s5)|2 ✅|||3ms|
|[OptimizationTest](#user-content-r0s6)|1 ✅|||1ms|
|[OtherTest](#user-content-r0s7)|2 ✅|2 ❌||7ms|
|[PHPTagsTest](#user-content-r0s8)|2 ✅|||1ms|
|[ProhibitedTest](#user-content-r0s9)|1 ✅|||1ms|
|[StrictCompareTest](#user-content-r0s10)|1 ✅|||2ms|
|[UnusedTest](#user-content-r0s11)|2 ✅|||2ms|
### ✅ <a id="user-content-r0s0" href="#user-content-r0s0">CommentsTest</a>
```
✅ testGoodDoc
✅ testComments
✅ testTODOs
```
### ✅ <a id="user-content-r0s1" href="#user-content-r0s1">DeprecationTest</a>
```
✅ testDeprecations
```
### ✅ <a id="user-content-r0s2" href="#user-content-r0s2">GoodTest</a>
```
✅ testGood
✅ testDoWhile
✅ testAnonymousFunction
✅ testException
```
### ✅ <a id="user-content-r0s3" href="#user-content-r0s3">IndentationTest</a>
```
✅ testTabIndentation
✅ testSpaceIndentation
✅ testSpaceIndentationArray
✅ testGoodSpaceIndentationArray
✅ testGoodIndentationNewLine
✅ testGoodIndentationSpaces
✅ testBadSpaces
✅ testBadSpaceAfterControl
```
### ✅ <a id="user-content-r0s4" href="#user-content-r0s4">MetricsTest</a>
```
✅ testMetrics
```
### ✅ <a id="user-content-r0s5" href="#user-content-r0s5">NamingTest</a>
```
✅ testNaming
✅ testFunctionNaming
```
### ✅ <a id="user-content-r0s6" href="#user-content-r0s6">OptimizationTest</a>
```
✅ testTextAfterClosingTag
```
### ❌ <a id="user-content-r0s7" href="#user-content-r0s7">OtherTest</a>
```
❌ testOther
PHPUnit\Framework\ExpectationFailedException
❌ testException
PHPUnit\Framework\ExpectationFailedException
✅ testEmpty
✅ testSwitchCaseNeedBreak
```
### ✅ <a id="user-content-r0s8" href="#user-content-r0s8">PHPTagsTest</a>
```
✅ testTextAfterClosingTag
✅ testClosingTagNotNeeded
```
### ✅ <a id="user-content-r0s9" href="#user-content-r0s9">ProhibitedTest</a>
```
✅ testProhibited
```
### ✅ <a id="user-content-r0s10" href="#user-content-r0s10">StrictCompareTest</a>
```
✅ testStrictCompare
```
### ✅ <a id="user-content-r0s11" href="#user-content-r0s11">UnusedTest</a>
```
✅ testGoodUnused
✅ testBadUnused
```

View File

@@ -0,0 +1,41 @@
![Tests failed](https://img.shields.io/badge/tests-10%20passed%2C%202%20failed-critical)
|Report|Passed|Failed|Skipped|Time|
|:---|---:|---:|---:|---:|
|[fixtures/phpunit/phpunit.xml](#user-content-r0)|10 ✅|2 ❌||148ms|
## ❌ <a id="user-content-r0" href="#user-content-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](#user-content-r0s0)||2 ❌||140ms|
|[PHPUnit\Event\CollectingDispatcherTest](#user-content-r0s1)|2 ✅|||4ms|
|[PHPUnit\Event\DeferringDispatcherTest](#user-content-r0s2)|4 ✅|||3ms|
|[PHPUnit\Event\DirectDispatcherTest](#user-content-r0s3)|4 ✅|||1ms|
### ❌ <a id="user-content-r0s0" href="#user-content-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="#user-content-r0s1">PHPUnit\Event\CollectingDispatcherTest</a>
```
PHPUnit.Event.CollectingDispatcherTest
✅ testHasNoCollectedEventsWhenFlushedImmediatelyAfterCreation
✅ testCollectsDispatchedEventsUntilFlushed
```
### ✅ <a id="user-content-r0s2" href="#user-content-r0s2">PHPUnit\Event\DeferringDispatcherTest</a>
```
PHPUnit.Event.DeferringDispatcherTest
✅ testCollectsEventsUntilFlush
✅ testFlushesCollectedEvents
✅ testSubscriberCanBeRegistered
✅ testTracerCanBeRegistered
```
### ✅ <a id="user-content-r0s3" href="#user-content-r0s3">PHPUnit\Event\DirectDispatcherTest</a>
```
PHPUnit.Event.DirectDispatcherTest
✅ testDispatchesEventToKnownSubscribers
✅ testDispatchesEventToTracers
✅ testRegisterRejectsUnknownSubscriber
✅ testDispatchRejectsUnknownEventType
```

View File

@@ -0,0 +1,628 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`phpunit-junit tests report from junit-basic.xml matches snapshot 1`] = `
TestRunResult {
"path": "fixtures/external/phpunit/junit-basic.xml",
"suites": [
TestSuiteResult {
"groups": [
TestGroupResult {
"name": "",
"tests": [
TestCaseResult {
"error": undefined,
"name": "testCase1",
"result": "success",
"time": 2113.871,
},
TestCaseResult {
"error": undefined,
"name": "testCase2",
"result": "success",
"time": 1051,
},
TestCaseResult {
"error": undefined,
"name": "testCase3",
"result": "success",
"time": 3441,
},
],
},
],
"name": "Tests.Registration",
"totalTime": 6605.870999999999,
},
TestSuiteResult {
"groups": [
TestGroupResult {
"name": "",
"tests": [
TestCaseResult {
"error": undefined,
"name": "testCase4",
"result": "success",
"time": 2244,
},
TestCaseResult {
"error": undefined,
"name": "testCase5",
"result": "success",
"time": 781,
},
TestCaseResult {
"error": undefined,
"name": "testCase6",
"result": "success",
"time": 1331,
},
],
},
],
"name": "Tests.Authentication.Login",
"totalTime": 4356,
},
TestSuiteResult {
"groups": [
TestGroupResult {
"name": "",
"tests": [
TestCaseResult {
"error": undefined,
"name": "testCase7",
"result": "success",
"time": 2508,
},
TestCaseResult {
"error": undefined,
"name": "testCase8",
"result": "success",
"time": 1230.8159999999998,
},
TestCaseResult {
"error": {
"details": "",
"line": undefined,
"message": "AssertionError: Assertion error message",
"path": undefined,
},
"name": "testCase9",
"result": "failed",
"time": 982,
},
],
},
],
"name": "Tests.Authentication",
"totalTime": 9076.816,
},
],
"totalTime": 15682.687,
}
`;
exports[`phpunit-junit tests report from phpcheckstyle-phpunit.xml matches snapshot 1`] = `
TestRunResult {
"path": "fixtures/external/phpunit/phpcheckstyle-phpunit.xml",
"suites": [
TestSuiteResult {
"groups": [
TestGroupResult {
"name": "",
"tests": [
TestCaseResult {
"error": undefined,
"name": "testGoodDoc",
"result": "success",
"time": 5.093,
},
TestCaseResult {
"error": undefined,
"name": "testComments",
"result": "success",
"time": 0.921,
},
TestCaseResult {
"error": undefined,
"name": "testTODOs",
"result": "success",
"time": 0.6880000000000001,
},
],
},
],
"name": "CommentsTest",
"totalTime": 6.702,
},
TestSuiteResult {
"groups": [
TestGroupResult {
"name": "",
"tests": [
TestCaseResult {
"error": undefined,
"name": "testDeprecations",
"result": "success",
"time": 0.9740000000000001,
},
],
},
],
"name": "DeprecationTest",
"totalTime": 0.9740000000000001,
},
TestSuiteResult {
"groups": [
TestGroupResult {
"name": "",
"tests": [
TestCaseResult {
"error": undefined,
"name": "testGood",
"result": "success",
"time": 2.6470000000000002,
},
TestCaseResult {
"error": undefined,
"name": "testDoWhile",
"result": "success",
"time": 1.0219999999999998,
},
TestCaseResult {
"error": undefined,
"name": "testAnonymousFunction",
"result": "success",
"time": 0.8,
},
TestCaseResult {
"error": undefined,
"name": "testException",
"result": "success",
"time": 0.888,
},
],
},
],
"name": "GoodTest",
"totalTime": 5.357,
},
TestSuiteResult {
"groups": [
TestGroupResult {
"name": "",
"tests": [
TestCaseResult {
"error": undefined,
"name": "testTabIndentation",
"result": "success",
"time": 0.857,
},
TestCaseResult {
"error": undefined,
"name": "testSpaceIndentation",
"result": "success",
"time": 0.929,
},
TestCaseResult {
"error": undefined,
"name": "testSpaceIndentationArray",
"result": "success",
"time": 0.975,
},
TestCaseResult {
"error": undefined,
"name": "testGoodSpaceIndentationArray",
"result": "success",
"time": 1.212,
},
TestCaseResult {
"error": undefined,
"name": "testGoodIndentationNewLine",
"result": "success",
"time": 0.859,
},
TestCaseResult {
"error": undefined,
"name": "testGoodIndentationSpaces",
"result": "success",
"time": 0.78,
},
TestCaseResult {
"error": undefined,
"name": "testBadSpaces",
"result": "success",
"time": 1.1199999999999999,
},
TestCaseResult {
"error": undefined,
"name": "testBadSpaceAfterControl",
"result": "success",
"time": 0.9219999999999999,
},
],
},
],
"name": "IndentationTest",
"totalTime": 7.654,
},
TestSuiteResult {
"groups": [
TestGroupResult {
"name": "",
"tests": [
TestCaseResult {
"error": undefined,
"name": "testMetrics",
"result": "success",
"time": 4.146999999999999,
},
],
},
],
"name": "MetricsTest",
"totalTime": 4.146999999999999,
},
TestSuiteResult {
"groups": [
TestGroupResult {
"name": "",
"tests": [
TestCaseResult {
"error": undefined,
"name": "testNaming",
"result": "success",
"time": 1.426,
},
TestCaseResult {
"error": undefined,
"name": "testFunctionNaming",
"result": "success",
"time": 1.271,
},
],
},
],
"name": "NamingTest",
"totalTime": 2.697,
},
TestSuiteResult {
"groups": [
TestGroupResult {
"name": "",
"tests": [
TestCaseResult {
"error": undefined,
"name": "testTextAfterClosingTag",
"result": "success",
"time": 0.9940000000000001,
},
],
},
],
"name": "OptimizationTest",
"totalTime": 0.9940000000000001,
},
TestSuiteResult {
"groups": [
TestGroupResult {
"name": "",
"tests": [
TestCaseResult {
"error": {
"details": "OtherTest::testOther
We expect 20 warnings
Failed asserting that 19 matches expected 20.
/workspace/phpcheckstyle/test/OtherTest.php:24",
"line": 12,
"message": "PHPUnit\\Framework\\ExpectationFailedException",
"path": undefined,
},
"name": "testOther",
"result": "failed",
"time": 5.2509999999999994,
},
TestCaseResult {
"error": {
"details": "OtherTest::testException
We expect 1 error
Failed asserting that 0 matches expected 1.
/workspace/phpcheckstyle/test/OtherTest.php:40",
"line": 31,
"message": "PHPUnit\\Framework\\ExpectationFailedException",
"path": undefined,
},
"name": "testException",
"result": "failed",
"time": 0.751,
},
TestCaseResult {
"error": undefined,
"name": "testEmpty",
"result": "success",
"time": 0.42700000000000005,
},
TestCaseResult {
"error": undefined,
"name": "testSwitchCaseNeedBreak",
"result": "success",
"time": 0.901,
},
],
},
],
"name": "OtherTest",
"totalTime": 7.329,
},
TestSuiteResult {
"groups": [
TestGroupResult {
"name": "",
"tests": [
TestCaseResult {
"error": undefined,
"name": "testTextAfterClosingTag",
"result": "success",
"time": 0.641,
},
TestCaseResult {
"error": undefined,
"name": "testClosingTagNotNeeded",
"result": "success",
"time": 0.631,
},
],
},
],
"name": "PHPTagsTest",
"totalTime": 1.272,
},
TestSuiteResult {
"groups": [
TestGroupResult {
"name": "",
"tests": [
TestCaseResult {
"error": undefined,
"name": "testProhibited",
"result": "success",
"time": 0.9380000000000001,
},
],
},
],
"name": "ProhibitedTest",
"totalTime": 0.9380000000000001,
},
TestSuiteResult {
"groups": [
TestGroupResult {
"name": "",
"tests": [
TestCaseResult {
"error": undefined,
"name": "testStrictCompare",
"result": "success",
"time": 1.578,
},
],
},
],
"name": "StrictCompareTest",
"totalTime": 1.578,
},
TestSuiteResult {
"groups": [
TestGroupResult {
"name": "",
"tests": [
TestCaseResult {
"error": undefined,
"name": "testGoodUnused",
"result": "success",
"time": 0.94,
},
TestCaseResult {
"error": undefined,
"name": "testBadUnused",
"result": "success",
"time": 0.895,
},
],
},
],
"name": "UnusedTest",
"totalTime": 1.835,
},
],
"totalTime": undefined,
}
`;
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,
}
`;

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="UTF-8"?>
<testsuites/>

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
This is a basic JUnit-style XML example to highlight the basis structure.
Example by Testmo. Copyright 2023 Testmo GmbH. All rights reserved.
Testmo test management software - https://www.testmo.com/
-->
<testsuites time="15.682687">
<testsuite name="Tests.Registration" time="6.605871">
<testcase name="testCase1" classname="Tests.Registration" time="2.113871" />
<testcase name="testCase2" classname="Tests.Registration" time="1.051" />
<testcase name="testCase3" classname="Tests.Registration" time="3.441" />
</testsuite>
<testsuite name="Tests.Authentication" time="9.076816">
<testsuite name="Tests.Authentication.Login" time="4.356">
<testcase name="testCase4" classname="Tests.Authentication.Login" time="2.244" />
<testcase name="testCase5" classname="Tests.Authentication.Login" time="0.781" />
<testcase name="testCase6" classname="Tests.Authentication.Login" time="1.331" />
</testsuite>
<testcase name="testCase7" classname="Tests.Authentication" time="2.508" />
<testcase name="testCase8" classname="Tests.Authentication" time="1.230816" />
<testcase name="testCase9" classname="Tests.Authentication" time="0.982">
<failure message="Assertion error message" type="AssertionError">
<!-- Call stack printed here -->
</failure>
</testcase>
</testsuite>
</testsuites>

View File

@@ -0,0 +1,212 @@
<?xml version="1.0" encoding="UTF-8"?>
<testsuites>
<testsuite name="/workspace/phpcheckstyle/phpunit.xml" tests="30" assertions="117" errors="0" failures="2" skipped="0" time="0.041478">
<testsuite name="PHPUnitTestSuite" tests="30" assertions="117" errors="0" failures="2" skipped="0" time="0.041478">
<testsuite name="CommentsTest" file="/workspace/phpcheckstyle/test/CommentsTest.php" tests="3" assertions="12" errors="0" failures="0" skipped="0" time="0.006702">
<testcase name="testGoodDoc" file="/workspace/phpcheckstyle/test/CommentsTest.php" line="12" class="CommentsTest" classname="CommentsTest" assertions="4" time="0.005093"/>
<testcase name="testComments" file="/workspace/phpcheckstyle/test/CommentsTest.php" line="30" class="CommentsTest" classname="CommentsTest" assertions="4" time="0.000921">
<system-out>File "./test/sample/bad_comments.php" warning, line 4 - Avoid Shell/Perl like comments.
File "./test/sample/bad_comments.php" warning, line 6 - The class Comments must have a docblock comment.
File "./test/sample/bad_comments.php" warning, line 10 - The function testComment must have a docblock comment.
File "./test/sample/bad_comments.php" warning, line 18 - The function testComment returns a value and must include @returns in its docblock.
File "./test/sample/bad_comments.php" warning, line 18 - The function testComment parameters must match those in its docblock @param.
File "./test/sample/bad_comments.php" warning, line 18 - The function testComment throws an exception and must include @throws in its docblock.
</system-out>
</testcase>
<testcase name="testTODOs" file="/workspace/phpcheckstyle/test/CommentsTest.php" line="48" class="CommentsTest" classname="CommentsTest" assertions="4" time="0.000688">
<system-out>File "./test/sample/todo.php" warning, line 3 - TODO: The todo message.
</system-out>
</testcase>
</testsuite>
<testsuite name="DeprecationTest" file="/workspace/phpcheckstyle/test/DeprecationTest.php" tests="1" assertions="4" errors="0" failures="0" skipped="0" time="0.000974">
<testcase name="testDeprecations" file="/workspace/phpcheckstyle/test/DeprecationTest.php" line="12" class="DeprecationTest" classname="DeprecationTest" assertions="4" time="0.000974">
<system-out>File "./test/sample/bad_deprecation.php" warning, line 17 - split is deprecated since PHP 5.3. explode($pattern, $string) or preg_split('@'.$pattern.'@', $string) must be used instead.
File "./test/sample/bad_deprecation.php" warning, line 19 - ereg is deprecated since PHP 5.3. preg_match('@'.$pattern.'@', $string) must be used instead.
File "./test/sample/bad_deprecation.php" warning, line 21 - session_register is deprecated since PHP 5.3. $_SESSION must be used instead.
File "./test/sample/bad_deprecation.php" warning, line 23 - mysql_db_query is deprecated since PHP 5.3. mysql_select_db and mysql_query must be used instead.
File "./test/sample/bad_deprecation.php" warning, line 25 - $HTTP_GET_VARS is deprecated since PHP 5.3. $_GET must be used instead.
</system-out>
</testcase>
</testsuite>
<testsuite name="GoodTest" file="/workspace/phpcheckstyle/test/GoodTest.php" tests="4" assertions="16" errors="0" failures="0" skipped="0" time="0.005357">
<testcase name="testGood" file="/workspace/phpcheckstyle/test/GoodTest.php" line="12" class="GoodTest" classname="GoodTest" assertions="4" time="0.002647"/>
<testcase name="testDoWhile" file="/workspace/phpcheckstyle/test/GoodTest.php" line="32" class="GoodTest" classname="GoodTest" assertions="4" time="0.001022"/>
<testcase name="testAnonymousFunction" file="/workspace/phpcheckstyle/test/GoodTest.php" line="50" class="GoodTest" classname="GoodTest" assertions="4" time="0.000800"/>
<testcase name="testException" file="/workspace/phpcheckstyle/test/GoodTest.php" line="68" class="GoodTest" classname="GoodTest" assertions="4" time="0.000888"/>
</testsuite>
<testsuite name="IndentationTest" file="/workspace/phpcheckstyle/test/IndentationTest.php" tests="8" assertions="32" errors="0" failures="0" skipped="0" time="0.007654">
<testcase name="testTabIndentation" file="/workspace/phpcheckstyle/test/IndentationTest.php" line="12" class="IndentationTest" classname="IndentationTest" assertions="4" time="0.000857">
<system-out>File "./test/sample/bad_indentation.php" warning, line 8 - Whitespace indentation must not be used.
File "./test/sample/bad_indentation.php" warning, line 15 - Whitespace indentation must not be used.
File "./test/sample/bad_indentation.php" warning, line 17 - Whitespace indentation must not be used.
File "./test/sample/bad_indentation.php" warning, line 18 - Whitespace indentation must not be used.
File "./test/sample/bad_indentation.php" warning, line 19 - Whitespace indentation must not be used.
File "./test/sample/bad_indentation.php" warning, line 20 - Whitespace indentation must not be used.
</system-out>
</testcase>
<testcase name="testSpaceIndentation" file="/workspace/phpcheckstyle/test/IndentationTest.php" line="30" class="IndentationTest" classname="IndentationTest" assertions="4" time="0.000929">
<system-out>File "./test/sample/bad_indentation.php" warning, line 10 - Tab indentation must not be used.
File "./test/sample/bad_indentation.php" warning, line 10 - The indentation level must be 4 but was 1.
File "./test/sample/bad_indentation.php" warning, line 13 - Tab indentation must not be used.
File "./test/sample/bad_indentation.php" warning, line 13 - The indentation level must be 4 but was 1.
File "./test/sample/bad_indentation.php" warning, line 15 - The indentation level must be 8 but was 4.
File "./test/sample/bad_indentation.php" warning, line 16 - Tab indentation must not be used.
File "./test/sample/bad_indentation.php" warning, line 16 - The indentation level must be 8 but was 1.
File "./test/sample/bad_indentation.php" warning, line 17 - The indentation level must be 8 but was 3.
File "./test/sample/bad_indentation.php" warning, line 18 - The indentation level must be 8 but was 5.
File "./test/sample/bad_indentation.php" warning, line 19 - The indentation level must be 8 but was 6.
File "./test/sample/bad_indentation.php" warning, line 20 - The indentation level must be 4 but was 1.
</system-out>
</testcase>
<testcase name="testSpaceIndentationArray" file="/workspace/phpcheckstyle/test/IndentationTest.php" line="51" class="IndentationTest" classname="IndentationTest" assertions="4" time="0.000975">
<system-out>File "./test/sample/bad_indentation_array.php" warning, line 10 - Tab indentation must not be used.
File "./test/sample/bad_indentation_array.php" warning, line 10 - The indentation level must be 4 but was 1.
File "./test/sample/bad_indentation_array.php" warning, line 13 - Tab indentation must not be used.
File "./test/sample/bad_indentation_array.php" warning, line 13 - The indentation level must be 4 but was 1.
File "./test/sample/bad_indentation_array.php" warning, line 16 - The indentation level must be 12 but was 8.
File "./test/sample/bad_indentation_array.php" warning, line 24 - The indentation level must be 12 but was 8.
File "./test/sample/bad_indentation_array.php" warning, line 29 - The indentation level must be 8 but was 12.
File "./test/sample/bad_indentation_array.php" warning, line 15 - Undeclared or unused variable: $aVar.
File "./test/sample/bad_indentation_array.php" warning, line 19 - Undeclared or unused variable: $bVar.
File "./test/sample/bad_indentation_array.php" warning, line 23 - Undeclared or unused variable: $cVar.
File "./test/sample/bad_indentation_array.php" warning, line 27 - Undeclared or unused variable: $dVar.
</system-out>
</testcase>
<testcase name="testGoodSpaceIndentationArray" file="/workspace/phpcheckstyle/test/IndentationTest.php" line="72" class="IndentationTest" classname="IndentationTest" assertions="4" time="0.001212"/>
<testcase name="testGoodIndentationNewLine" file="/workspace/phpcheckstyle/test/IndentationTest.php" line="93" class="IndentationTest" classname="IndentationTest" assertions="4" time="0.000859"/>
<testcase name="testGoodIndentationSpaces" file="/workspace/phpcheckstyle/test/IndentationTest.php" line="116" class="IndentationTest" classname="IndentationTest" assertions="4" time="0.000780"/>
<testcase name="testBadSpaces" file="/workspace/phpcheckstyle/test/IndentationTest.php" line="137" class="IndentationTest" classname="IndentationTest" assertions="4" time="0.001120">
<system-out>File "./test/sample/bad_spaces.php" warning, line 17 - Whitespace must follow ,.
File "./test/sample/bad_spaces.php" warning, line 17 - Whitespace must precede {.
File "./test/sample/bad_spaces.php" warning, line 19 - Whitespace must follow if.
File "./test/sample/bad_spaces.php" warning, line 23 - Whitespace must precede =.
File "./test/sample/bad_spaces.php" warning, line 23 - Whitespace must follow =.
File "./test/sample/bad_spaces.php" warning, line 23 - Whitespace must precede +.
File "./test/sample/bad_spaces.php" warning, line 23 - Whitespace must follow +.
File "./test/sample/bad_spaces.php" info, line 25 - Whitespace must not precede ,.
File "./test/sample/bad_spaces.php" info, line 26 - Whitespace must not follow !.
</system-out>
</testcase>
<testcase name="testBadSpaceAfterControl" file="/workspace/phpcheckstyle/test/IndentationTest.php" line="155" class="IndentationTest" classname="IndentationTest" assertions="4" time="0.000922">
<system-out>File "./test/sample/bad_space_after_control.php" warning, line 19 - Whitespace must not follow if.
</system-out>
</testcase>
</testsuite>
<testsuite name="MetricsTest" file="/workspace/phpcheckstyle/test/MetricsTest.php" tests="1" assertions="4" errors="0" failures="0" skipped="0" time="0.004147">
<testcase name="testMetrics" file="/workspace/phpcheckstyle/test/MetricsTest.php" line="12" class="MetricsTest" classname="MetricsTest" assertions="4" time="0.004147">
<system-out>File "./test/sample/bad_metrics.php" warning, line 21 - The function testMetrics's number of parameters (6) must not exceed 4.
File "./test/sample/bad_metrics.php" info, line 55 - Line is too long. [233/160]
File "./test/sample/bad_metrics.php" warning, line 21 - The Cyclomatic Complexity of function testMetrics is too high. [15/10]
File "./test/sample/bad_metrics.php" warning, line 244 - The testMetrics function body length is too long. [223/200]
</system-out>
</testcase>
</testsuite>
<testsuite name="NamingTest" file="/workspace/phpcheckstyle/test/NamingTest.php" tests="2" assertions="8" errors="0" failures="0" skipped="0" time="0.002697">
<testcase name="testNaming" file="/workspace/phpcheckstyle/test/NamingTest.php" line="12" class="NamingTest" classname="NamingTest" assertions="4" time="0.001426">
<system-out>File "./test/sample/_bad_naming.php" error, line 11 - Constant _badly_named_constant name should follow the pattern /^[A-Z][A-Z0-9_]*$/.
File "./test/sample/_bad_naming.php" error, line 13 - Constant bad_CONST name should follow the pattern /^[A-Z][A-Z0-9_]*$/.
File "./test/sample/_bad_naming.php" warning, line 17 - Top level variable $XXX name should follow the pattern /^[a-z_][a-zA-Z0-9]*$/.
File "./test/sample/_bad_naming.php" warning, line 20 - Variable x name length is too short.
File "./test/sample/_bad_naming.php" error, line 28 - Class badlynamedclass name should follow the pattern /^[A-Z][a-zA-Z0-9_]*$/.
File "./test/sample/_bad_naming.php" warning, line 32 - Member variable $YYY name should follow the pattern /^[a-z_][a-zA-Z0-9]*$/.
File "./test/sample/_bad_naming.php" warning, line 37 - The constructor name must be __construct().
File "./test/sample/_bad_naming.php" error, line 44 - Function Badlynamedfunction name should follow the pattern /^[a-z][a-zA-Z0-9]*$/.
File "./test/sample/_bad_naming.php" warning, line 47 - Local variable $ZZZ name should follow the pattern /^[a-z_][a-zA-Z0-9]*$/.
File "./test/sample/_bad_naming.php" error, line 54 - Protected function Badlynamedfunction2 name should follow the pattern /^[a-z][a-zA-Z0-9]*$/.
File "./test/sample/_bad_naming.php" error, line 61 - Private function badlynamedfunction3 name should follow the pattern /^_[a-z][a-zA-Z0-9]*$/.
File "./test/sample/_bad_naming.php" error, line 70 - Interface _badlynamedinterface name should follow the pattern /^[A-Z][a-zA-Z0-9_]*$/.
File "./test/sample/_bad_naming.php" error, line 75 - File _bad_naming.php name should follow the pattern /^[a-zA-Z][a-zA-Z0-9._]*$/.
</system-out>
</testcase>
<testcase name="testFunctionNaming" file="/workspace/phpcheckstyle/test/NamingTest.php" line="32" class="NamingTest" classname="NamingTest" assertions="4" time="0.001271"/>
</testsuite>
<testsuite name="OptimizationTest" file="/workspace/phpcheckstyle/test/OptimizationTest.php" tests="1" assertions="4" errors="0" failures="0" skipped="0" time="0.000994">
<testcase name="testTextAfterClosingTag" file="/workspace/phpcheckstyle/test/OptimizationTest.php" line="12" class="OptimizationTest" classname="OptimizationTest" assertions="4" time="0.000994">
<system-out>File "./test/sample/bad_optimisation.php" warning, line 18 - count function must not be used inside a loop.
File "./test/sample/bad_optimisation.php" warning, line 23 - count function must not be used inside a loop.
</system-out>
</testcase>
</testsuite>
<testsuite name="OtherTest" file="/workspace/phpcheckstyle/test/OtherTest.php" tests="4" assertions="13" errors="0" failures="2" skipped="0" time="0.007329">
<testcase name="testOther" file="/workspace/phpcheckstyle/test/OtherTest.php" line="12" class="OtherTest" classname="OtherTest" assertions="4" time="0.005251">
<failure type="PHPUnit\Framework\ExpectationFailedException">OtherTest::testOther
We expect 20 warnings
Failed asserting that 19 matches expected 20.
/workspace/phpcheckstyle/test/OtherTest.php:24</failure>
<system-out>File "./test/sample/bad_other.php" warning, line 17 - All arguments with default values must be at the end of the block or statement.
File "./test/sample/bad_other.php" warning, line 21 - Errors must not be silenced when calling a function.
File "./test/sample/bad_other.php" warning, line 23 - Prefer single-quoted strings when you don't need string interpolation.
File "./test/sample/bad_other.php" warning, line 23 - Encapsed variables must not be used inside a string.
File "./test/sample/bad_other.php" warning, line 23 - Encapsed variables must not be used inside a string.
File "./test/sample/bad_other.php" warning, line 23 - Prefer single-quoted strings when you don't need string interpolation.
File "./test/sample/bad_other.php" warning, line 37 - TODO: Show todos
File "./test/sample/bad_other.php" warning, line 40 - Avoid empty statements (;;).
File "./test/sample/bad_other.php" warning, line 42 - Boolean operators (&amp;&amp;) must be used instead of logical operators (AND).
File "./test/sample/bad_other.php" warning, line 42 - Empty if block.
File "./test/sample/bad_other.php" warning, line 48 - Heredoc syntax must not be used.
File "./test/sample/bad_other.php" warning, line 52 - The statement if must contain its code within a {} block.
File "./test/sample/bad_other.php" warning, line 54 - Consider using a strict comparison operator instead of ==.
File "./test/sample/bad_other.php" warning, line 54 - The statement while must contain its code within a {} block.
File "./test/sample/bad_other.php" warning, line 66 - The switch statement must have a default case.
File "./test/sample/bad_other.php" warning, line 79 - The default case of a switch statement must be located after all other cases.
File "./test/sample/bad_other.php" warning, line 93 - Unary operators (++ or --) must not be used inside a control statement
File "./test/sample/bad_other.php" warning, line 95 - Assigments (=) must not be used inside a control statement.
File "./test/sample/bad_other.php" warning, line 106 - File ./test/sample/bad_other.php must not have multiple class declarations.
</system-out>
</testcase>
<testcase name="testException" file="/workspace/phpcheckstyle/test/OtherTest.php" line="31" class="OtherTest" classname="OtherTest" assertions="1" time="0.000751">
<failure type="PHPUnit\Framework\ExpectationFailedException">OtherTest::testException
We expect 1 error
Failed asserting that 0 matches expected 1.
/workspace/phpcheckstyle/test/OtherTest.php:40</failure>
</testcase>
<testcase name="testEmpty" file="/workspace/phpcheckstyle/test/OtherTest.php" line="50" class="OtherTest" classname="OtherTest" assertions="4" time="0.000427">
<system-out>File "./test/sample/empty.php" warning, line 1 - The file ./test/sample/empty.php is empty.
</system-out>
</testcase>
<testcase name="testSwitchCaseNeedBreak" file="/workspace/phpcheckstyle/test/OtherTest.php" line="69" class="OtherTest" classname="OtherTest" assertions="4" time="0.000901">
<system-out>File "./test/sample/switch_multi_case.php" warning, line 10 - The case statement must contain a break.
</system-out>
</testcase>
</testsuite>
<testsuite name="PHPTagsTest" file="/workspace/phpcheckstyle/test/PHPTagsTest.php" tests="2" assertions="8" errors="0" failures="0" skipped="0" time="0.001272">
<testcase name="testTextAfterClosingTag" file="/workspace/phpcheckstyle/test/PHPTagsTest.php" line="12" class="PHPTagsTest" classname="PHPTagsTest" assertions="4" time="0.000641">
<system-out>File "./test/sample/bad_php_tags_text_after_end.php" warning, line 9 - A PHP close tag must not be included at the end of the file.
</system-out>
</testcase>
<testcase name="testClosingTagNotNeeded" file="/workspace/phpcheckstyle/test/PHPTagsTest.php" line="30" class="PHPTagsTest" classname="PHPTagsTest" assertions="4" time="0.000631">
<system-out>File "./test/sample/bad_php_tags_end_not_needed.php" warning, line 1 - PHP tag should be at the beginning of the line.
</system-out>
</testcase>
</testsuite>
<testsuite name="ProhibitedTest" file="/workspace/phpcheckstyle/test/ProhibitedTest.php" tests="1" assertions="4" errors="0" failures="0" skipped="0" time="0.000938">
<testcase name="testProhibited" file="/workspace/phpcheckstyle/test/ProhibitedTest.php" line="13" class="ProhibitedTest" classname="ProhibitedTest" assertions="4" time="0.000938">
<system-out>File "./test/sample/bad_prohibited.php" warning, line 18 - The function exec must not be called.
File "./test/sample/bad_prohibited.php" warning, line 20 - Token T_PRINT must not be used.
</system-out>
</testcase>
</testsuite>
<testsuite name="StrictCompareTest" file="/workspace/phpcheckstyle/test/StrictCompareTest.php" tests="1" assertions="4" errors="0" failures="0" skipped="0" time="0.001578">
<testcase name="testStrictCompare" file="/workspace/phpcheckstyle/test/StrictCompareTest.php" line="12" class="StrictCompareTest" classname="StrictCompareTest" assertions="4" time="0.001578">
<system-out>File "./test/sample/bad_strictcompare.php" warning, line 14 - Consider using a strict comparison operator instead of ==.
File "./test/sample/bad_strictcompare.php" warning, line 19 - Consider using a strict comparison operator instead of !=.
File "./test/sample/bad_strictcompare.php" warning, line 24 - Consider using a strict comparison operator instead of ==.
File "./test/sample/bad_strictcompare.php" warning, line 29 - Consider using a strict comparison operator instead of ==.
</system-out>
</testcase>
</testsuite>
<testsuite name="UnusedTest" file="/workspace/phpcheckstyle/test/UnusedTest.php" tests="2" assertions="8" errors="0" failures="0" skipped="0" time="0.001835">
<testcase name="testGoodUnused" file="/workspace/phpcheckstyle/test/UnusedTest.php" line="13" class="UnusedTest" classname="UnusedTest" assertions="4" time="0.000940"/>
<testcase name="testBadUnused" file="/workspace/phpcheckstyle/test/UnusedTest.php" line="32" class="UnusedTest" classname="UnusedTest" assertions="4" time="0.000895">
<system-out>File "./test/sample/bad_unused.php" warning, line 23 - Function _testUnused has unused code after RETURN.
File "./test/sample/bad_unused.php" warning, line 27 - The function _testUnused parameter $b is not used.
File "./test/sample/bad_unused.php" warning, line 18 - Unused private function: _testUnused.
File "./test/sample/bad_unused.php" warning, line 20 - Undeclared or unused variable: $c.
</system-out>
</testcase>
</testsuite>
</testsuite>
</testsuite>
</testsuites>

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<testsuites>
<testsuite name="SampleSuite" tests="6" failures="6" time="0.006">
<testcase name="testFailure" classname="SampleSuite" file="/home/runner/work/repo/src/Fake.php" line="42" time="0.001">
<failure type="Exception" message="Boom">/home/runner/work/repo/src/Fake.php:42</failure>
</testcase>
<testcase name="testStringFailure" classname="SampleSuite" file="/home/runner/work/repo/src/Other.php" line="10" time="0.001">
<failure>/home/runner/work/repo/src/Other.php:10</failure>
</testcase>
<testcase name="testParenFailure" classname="SampleSuite" file="/home/runner/work/repo/src/Paren.php" line="123" time="0.001">
<failure>at /home/runner/work/repo/src/Paren.php(123)</failure>
</testcase>
<testcase name="testWindowsFailure" classname="SampleSuite" file="C:\repo\src\Win.php" line="77" time="0.001">
<failure>C:\repo\src\Win.php:77</failure>
</testcase>
<testcase name="testWindowsParenFailure" classname="SampleSuite" file="C:\repo\src\WinParen.php" line="88" time="0.001">
<failure>at C:\repo\src\WinParen.php(88)</failure>
</testcase>
<testcase name="testPhptFailure" classname="SampleSuite" file="/home/runner/work/repo/tests/Sample.phpt" line="12" time="0.001">
<failure>/home/runner/work/repo/tests/Sample.phpt:12</failure>
</testcase>
</testsuite>
</testsuites>

View 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>

View File

@@ -0,0 +1,347 @@
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')
}
})
it('maps absolute paths to tracked files for annotations', async () => {
const fixturePath = path.join(__dirname, 'fixtures', 'phpunit', 'phpunit-paths.xml')
const filePath = normalizeFilePath(path.relative(__dirname, fixturePath))
const fileContent = fs.readFileSync(fixturePath, {encoding: 'utf8'})
const opts: ParseOptions = {
parseErrors: true,
trackedFiles: [
'src/Fake.php',
'src/Other.php',
'src/Paren.php',
'src/Win.php',
'src/WinParen.php',
'tests/Sample.phpt'
]
}
const parser = new PhpunitJunitParser(opts)
const result = await parser.parse(filePath, fileContent)
const suite = result.suites.find(s => s.name === 'SampleSuite')
expect(suite).toBeDefined()
const tests = suite!.groups.flatMap(g => g.tests)
const fileFailure = tests.find(t => t.name === 'testFailure')
expect(fileFailure).toBeDefined()
expect(fileFailure!.error).toBeDefined()
expect(fileFailure!.error!.path).toBe('src/Fake.php')
expect(fileFailure!.error!.line).toBe(42)
const stringFailure = tests.find(t => t.name === 'testStringFailure')
expect(stringFailure).toBeDefined()
expect(stringFailure!.error).toBeDefined()
expect(stringFailure!.error!.path).toBe('src/Other.php')
expect(stringFailure!.error!.line).toBe(10)
const parenFailure = tests.find(t => t.name === 'testParenFailure')
expect(parenFailure).toBeDefined()
expect(parenFailure!.error).toBeDefined()
expect(parenFailure!.error!.path).toBe('src/Paren.php')
expect(parenFailure!.error!.line).toBe(123)
const windowsFailure = tests.find(t => t.name === 'testWindowsFailure')
expect(windowsFailure).toBeDefined()
expect(windowsFailure!.error).toBeDefined()
expect(windowsFailure!.error!.path).toBe('src/Win.php')
expect(windowsFailure!.error!.line).toBe(77)
const windowsParenFailure = tests.find(t => t.name === 'testWindowsParenFailure')
expect(windowsParenFailure).toBeDefined()
expect(windowsParenFailure!.error).toBeDefined()
expect(windowsParenFailure!.error!.path).toBe('src/WinParen.php')
expect(windowsParenFailure!.error!.line).toBe(88)
const phptFailure = tests.find(t => t.name === 'testPhptFailure')
expect(phptFailure).toBeDefined()
expect(phptFailure!.error).toBeDefined()
expect(phptFailure!.error!.path).toBe('tests/Sample.phpt')
expect(phptFailure!.error!.line).toBe(12)
})
it('parses junit-basic.xml with nested suites and failure', async () => {
const fixturePath = path.join(__dirname, 'fixtures', 'external', 'phpunit', 'junit-basic.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)
// Verify test counts
expect(result.tests).toBe(9)
expect(result.passed).toBe(8)
expect(result.failed).toBe(1)
expect(result.result).toBe('failed')
// Verify suites - should have Tests.Registration, Tests.Authentication.Login, and Tests.Authentication
expect(result.suites.length).toBe(3)
const suiteNames = result.suites.map(s => s.name)
expect(suiteNames).toContain('Tests.Registration')
expect(suiteNames).toContain('Tests.Authentication.Login')
expect(suiteNames).toContain('Tests.Authentication')
// Verify the Registration suite has 3 tests
const registrationSuite = result.suites.find(s => s.name === 'Tests.Registration')
expect(registrationSuite).toBeDefined()
const registrationTests = registrationSuite!.groups.flatMap(g => g.tests)
expect(registrationTests.length).toBe(3)
// Verify the Authentication suite has 3 direct tests (not counting nested suite)
const authSuite = result.suites.find(s => s.name === 'Tests.Authentication')
expect(authSuite).toBeDefined()
const authTests = authSuite!.groups.flatMap(g => g.tests)
expect(authTests.length).toBe(3)
// Verify the Login nested suite has 3 tests
const loginSuite = result.suites.find(s => s.name === 'Tests.Authentication.Login')
expect(loginSuite).toBeDefined()
const loginTests = loginSuite!.groups.flatMap(g => g.tests)
expect(loginTests.length).toBe(3)
// Verify failure is captured
const failedTest = authTests.find(t => t.name === 'testCase9')
expect(failedTest).toBeDefined()
expect(failedTest!.result).toBe('failed')
expect(failedTest!.error).toBeDefined()
expect(failedTest!.error!.message).toBe('AssertionError: Assertion error message')
})
it('parses phpcheckstyle-phpunit.xml with deeply nested suites', async () => {
const fixturePath = path.join(__dirname, 'fixtures', 'external', 'phpunit', 'phpcheckstyle-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)
// Verify test counts from the XML: tests="30", failures="2"
expect(result.tests).toBe(30)
expect(result.passed).toBe(28)
expect(result.failed).toBe(2)
expect(result.result).toBe('failed')
// Verify the number of test suites extracted (leaf suites with testcases)
// CommentsTest, DeprecationTest, GoodTest, IndentationTest, MetricsTest,
// NamingTest, OptimizationTest, OtherTest, PHPTagsTest, ProhibitedTest,
// StrictCompareTest, UnusedTest = 12 suites
expect(result.suites.length).toBe(12)
const suiteNames = result.suites.map(s => s.name)
expect(suiteNames).toContain('CommentsTest')
expect(suiteNames).toContain('GoodTest')
expect(suiteNames).toContain('IndentationTest')
expect(suiteNames).toContain('OtherTest')
})
it('extracts test data from phpcheckstyle-phpunit.xml', async () => {
const fixturePath = path.join(__dirname, 'fixtures', 'external', 'phpunit', 'phpcheckstyle-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 CommentsTest suite
const commentsSuite = result.suites.find(s => s.name === 'CommentsTest')
expect(commentsSuite).toBeDefined()
// Verify tests are extracted correctly
const tests = commentsSuite!.groups.flatMap(g => g.tests)
expect(tests.length).toBe(3)
const testGoodDoc = tests.find(t => t.name === 'testGoodDoc')
expect(testGoodDoc).toBeDefined()
expect(testGoodDoc!.result).toBe('success')
})
it('captures failure details from phpcheckstyle-phpunit.xml', async () => {
const fixturePath = path.join(__dirname, 'fixtures', 'external', 'phpunit', 'phpcheckstyle-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 OtherTest suite which has failures
const otherSuite = result.suites.find(s => s.name === 'OtherTest')
expect(otherSuite).toBeDefined()
const failedTests = otherSuite!.groups.flatMap(g => g.tests).filter(t => t.result === 'failed')
expect(failedTests.length).toBe(2)
// Verify failure details
const testOther = failedTests.find(t => t.name === 'testOther')
expect(testOther).toBeDefined()
expect(testOther!.error).toBeDefined()
expect(testOther!.error!.details).toContain('We expect 20 warnings')
expect(testOther!.error!.details).toContain('Failed asserting that 19 matches expected 20')
const testException = failedTests.find(t => t.name === 'testException')
expect(testException).toBeDefined()
expect(testException!.error).toBeDefined()
expect(testException!.error!.details).toContain('We expect 1 error')
})
it('report from junit-basic.xml matches snapshot', async () => {
const fixturePath = path.join(__dirname, 'fixtures', 'external', 'phpunit', 'junit-basic.xml')
const outputPath = path.join(__dirname, '__outputs__', 'phpunit-junit-basic-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('report from phpcheckstyle-phpunit.xml matches snapshot', async () => {
const fixturePath = path.join(__dirname, 'fixtures', 'external', 'phpunit', 'phpcheckstyle-phpunit.xml')
const outputPath = path.join(__dirname, '__outputs__', 'phpunit-phpcheckstyle-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)
})
})

View File

@@ -32,6 +32,7 @@ inputs:
- java-junit
- jest-junit
- mocha-json
- phpunit-junit
- python-xunit
- rspec-json
- swift-xunit

238
dist/index.js generated vendored
View File

@@ -277,6 +277,7 @@ const golang_json_parser_1 = __nccwpck_require__(5162);
const java_junit_parser_1 = __nccwpck_require__(8342);
const jest_junit_parser_1 = __nccwpck_require__(1042);
const mocha_json_parser_1 = __nccwpck_require__(5402);
const phpunit_junit_parser_1 = __nccwpck_require__(2674);
const python_xunit_parser_1 = __nccwpck_require__(6578);
const rspec_json_parser_1 = __nccwpck_require__(9768);
const swift_xunit_parser_1 = __nccwpck_require__(7330);
@@ -494,6 +495,8 @@ class TestReporter {
return new jest_junit_parser_1.JestJunitParser(options);
case 'mocha-json':
return new mocha_json_parser_1.MochaJsonParser(options);
case 'phpunit-junit':
return new phpunit_junit_parser_1.PhpunitJunitParser(options);
case 'python-xunit':
return new python_xunit_parser_1.PythonXunitParser(options);
case 'rspec-json':
@@ -1666,6 +1669,241 @@ class MochaJsonParser {
exports.MochaJsonParser = MochaJsonParser;
/***/ }),
/***/ 2674:
/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => {
"use strict";
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.PhpunitJunitParser = void 0;
const xml2js_1 = __nccwpck_require__(758);
const path_utils_1 = __nccwpck_require__(9132);
const test_results_1 = __nccwpck_require__(613);
class PhpunitJunitParser {
options;
trackedFiles;
trackedFilesList;
assumedWorkDir;
constructor(options) {
this.options = options;
this.trackedFilesList = options.trackedFiles.map(f => (0, path_utils_1.normalizeFilePath)(f));
this.trackedFiles = new Set(this.trackedFilesList);
}
async parse(filePath, content) {
const reportOrSuite = await this.getPhpunitReport(filePath, content);
const isReport = reportOrSuite.testsuites !== undefined;
// XML might contain:
// - multiple suites under <testsuites> root node
// - single <testsuite> as root node
let report;
if (isReport) {
report = reportOrSuite;
}
else {
// Make it behave the same way as if suite was inside <testsuites> root node
const suite = reportOrSuite.testsuite;
report = {
testsuites: {
$: { time: suite.$.time },
testsuite: [suite]
}
};
}
return this.getTestRunResult(filePath, report);
}
async getPhpunitReport(filePath, content) {
try {
return await (0, xml2js_1.parseStringPromise)(content);
}
catch (e) {
throw new Error(`Invalid XML at ${filePath}\n\n${e}`);
}
}
getTestRunResult(filePath, report) {
const suites = [];
this.collectSuites(suites, report.testsuites.testsuite ?? []);
const seconds = parseFloat(report.testsuites.$?.time ?? '');
const time = isNaN(seconds) ? undefined : seconds * 1000;
return new test_results_1.TestRunResult(filePath, suites, time);
}
collectSuites(results, testsuites) {
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 test_results_1.TestSuiteResult(name, this.getGroups(ts), time));
}
}
}
getGroups(suite) {
if (!suite.testcase || suite.testcase.length === 0) {
return [];
}
const groups = [];
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 test_results_1.TestCaseResult(name, result, time, error);
});
return new test_results_1.TestGroupResult(grp.name, tests);
});
}
getTestCaseResult(test) {
if (test.failure || test.error)
return 'failed';
if (test.skipped)
return 'skipped';
return 'success';
}
getTestCaseError(tc) {
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 = typeof failure === 'string' ? failure : failure._ ?? '';
// PHPUnit provides file path directly in testcase attributes
let filePath;
let line;
if (tc.$.file) {
const relativePath = this.getRelativePath(tc.$.file);
if (this.trackedFiles.has(relativePath)) {
filePath = relativePath;
}
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;
if (typeof failure !== 'string' && failure.$) {
message = failure.$.message;
if (failure.$.type) {
message = message ? `${failure.$.type}: ${message}` : failure.$.type;
}
}
return {
path: filePath,
line,
details,
message
};
}
extractFileAndLine(details) {
// 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 matchColon = str.match(/((?:[A-Za-z]:)?[^\s:()]+?\.(?:php|phpt)):(\d+)/);
if (matchColon) {
const relativePath = this.getRelativePath(matchColon[1]);
if (this.trackedFiles.has(relativePath)) {
return { filePath: relativePath, line: parseInt(matchColon[2]) };
}
}
const matchParen = str.match(/((?:[A-Za-z]:)?[^\s:()]+?\.(?:php|phpt))\((\d+)\)/);
if (matchParen) {
const relativePath = this.getRelativePath(matchParen[1]);
if (this.trackedFiles.has(relativePath)) {
return { filePath: relativePath, line: parseInt(matchParen[2]) };
}
}
}
return undefined;
}
/**
* Converts an absolute file path to a relative path by stripping the working directory prefix.
*
* @param path - The absolute file path from PHPUnit output (e.g., `/home/runner/work/repo/src/Test.php`)
* @returns The relative path (e.g., `src/Test.php`) if a working directory can be determined,
* otherwise returns the normalized original path
*/
getRelativePath(path) {
path = (0, path_utils_1.normalizeFilePath)(path);
const workDir = this.getWorkDir(path);
if (workDir !== undefined && path.startsWith(workDir)) {
path = path.substr(workDir.length);
}
return path;
}
/**
* Determines the working directory prefix to strip from absolute file paths.
*
* The working directory is resolved using the following priority:
*
* 1. **Explicit configuration** - If `options.workDir` is set, it takes precedence.
* This allows users to explicitly specify the working directory.
*
* 2. **Cached assumption** - If we've previously determined a working directory
* (`assumedWorkDir`) and the current path starts with it, we reuse that value.
* This avoids redundant computation for subsequent paths.
*
* 3. **Heuristic detection** - Uses `getBasePath()` to find the common prefix between
* the absolute path and the list of tracked files in the repository. For example:
* - Absolute path: `/home/runner/work/repo/src/Test.php`
* - Tracked file: `src/Test.php`
* - Detected workDir: `/home/runner/work/repo/`
*
* Once detected, the working directory is cached in `assumedWorkDir` for efficiency.
*
* @param path - The normalized absolute file path to analyze
* @returns The working directory prefix (with trailing slash), or `undefined` if it cannot be determined
*
* @example
* // With tracked file 'src/Foo.php' and path '/home/runner/work/repo/src/Foo.php'
* // Returns: '/home/runner/work/repo/'
*/
getWorkDir(path) {
if (this.options.workDir) {
return this.options.workDir;
}
if (this.assumedWorkDir && path.startsWith(this.assumedWorkDir)) {
return this.assumedWorkDir;
}
const basePath = (0, path_utils_1.getBasePath)(path, this.trackedFilesList);
if (basePath !== undefined) {
this.assumedWorkDir = basePath;
}
return basePath;
}
}
exports.PhpunitJunitParser = PhpunitJunitParser;
/***/ }),
/***/ 6578:

View File

@@ -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':

View File

@@ -0,0 +1,258 @@
import {ParseOptions, TestParser} from '../../test-parser'
import {parseStringPromise} from 'xml2js'
import {PhpunitReport, SingleSuiteReport, TestCase, TestSuite} from './phpunit-junit-types'
import {getBasePath, normalizeFilePath} from '../../utils/path-utils'
import {
TestExecutionResult,
TestRunResult,
TestSuiteResult,
TestGroupResult,
TestCaseResult,
TestCaseError
} from '../../test-results'
export class PhpunitJunitParser implements TestParser {
readonly trackedFiles: Set<string>
readonly trackedFilesList: string[]
private assumedWorkDir: string | undefined
constructor(readonly options: ParseOptions) {
this.trackedFilesList = options.trackedFiles.map(f => normalizeFilePath(f))
this.trackedFiles = new Set(this.trackedFilesList)
}
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 = typeof failure === 'string' ? failure : failure._ ?? ''
// PHPUnit provides file path directly in testcase attributes
let filePath: string | undefined
let line: number | undefined
if (tc.$.file) {
const relativePath = this.getRelativePath(tc.$.file)
if (this.trackedFiles.has(relativePath)) {
filePath = relativePath
}
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 (typeof failure !== 'string' && 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 matchColon = str.match(/((?:[A-Za-z]:)?[^\s:()]+?\.(?:php|phpt)):(\d+)/)
if (matchColon) {
const relativePath = this.getRelativePath(matchColon[1])
if (this.trackedFiles.has(relativePath)) {
return {filePath: relativePath, line: parseInt(matchColon[2])}
}
}
const matchParen = str.match(/((?:[A-Za-z]:)?[^\s:()]+?\.(?:php|phpt))\((\d+)\)/)
if (matchParen) {
const relativePath = this.getRelativePath(matchParen[1])
if (this.trackedFiles.has(relativePath)) {
return {filePath: relativePath, line: parseInt(matchParen[2])}
}
}
}
return undefined
}
/**
* Converts an absolute file path to a relative path by stripping the working directory prefix.
*
* @param path - The absolute file path from PHPUnit output (e.g., `/home/runner/work/repo/src/Test.php`)
* @returns The relative path (e.g., `src/Test.php`) if a working directory can be determined,
* otherwise returns the normalized original path
*/
private getRelativePath(path: string): string {
path = normalizeFilePath(path)
const workDir = this.getWorkDir(path)
if (workDir !== undefined && path.startsWith(workDir)) {
path = path.substr(workDir.length)
}
return path
}
/**
* Determines the working directory prefix to strip from absolute file paths.
*
* The working directory is resolved using the following priority:
*
* 1. **Explicit configuration** - If `options.workDir` is set, it takes precedence.
* This allows users to explicitly specify the working directory.
*
* 2. **Cached assumption** - If we've previously determined a working directory
* (`assumedWorkDir`) and the current path starts with it, we reuse that value.
* This avoids redundant computation for subsequent paths.
*
* 3. **Heuristic detection** - Uses `getBasePath()` to find the common prefix between
* the absolute path and the list of tracked files in the repository. For example:
* - Absolute path: `/home/runner/work/repo/src/Test.php`
* - Tracked file: `src/Test.php`
* - Detected workDir: `/home/runner/work/repo/`
*
* Once detected, the working directory is cached in `assumedWorkDir` for efficiency.
*
* @param path - The normalized absolute file path to analyze
* @returns The working directory prefix (with trailing slash), or `undefined` if it cannot be determined
*
* @example
* // With tracked file 'src/Foo.php' and path '/home/runner/work/repo/src/Foo.php'
* // Returns: '/home/runner/work/repo/'
*/
private getWorkDir(path: string): string | undefined {
if (this.options.workDir) {
return this.options.workDir
}
if (this.assumedWorkDir && path.startsWith(this.assumedWorkDir)) {
return this.assumedWorkDir
}
const basePath = getBasePath(path, this.trackedFilesList)
if (basePath !== undefined) {
this.assumedWorkDir = basePath
}
return basePath
}
}

View 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
}
}