diff --git a/__tests__/__outputs__/phpunit-phpcheckstyle-results.md b/__tests__/__outputs__/phpunit-phpcheckstyle-results.md index 6966c55..2797b04 100644 --- a/__tests__/__outputs__/phpunit-phpcheckstyle-results.md +++ b/__tests__/__outputs__/phpunit-phpcheckstyle-results.md @@ -62,9 +62,9 @@ ### ❌ OtherTest ``` ❌ testOther - PHPUnit\Framework\ExpectationFailedException + OtherTest::testOther ❌ testException - PHPUnit\Framework\ExpectationFailedException + OtherTest::testException ✅ testEmpty ✅ testSwitchCaseNeedBreak ``` diff --git a/__tests__/__outputs__/phpunit-test-results.md b/__tests__/__outputs__/phpunit-test-results.md index 67f8258..9a599fa 100644 --- a/__tests__/__outputs__/phpunit-test-results.md +++ b/__tests__/__outputs__/phpunit-test-results.md @@ -13,9 +13,9 @@ ### ❌ CLI Arguments ``` ❌ targeting-traits-with-coversclass-attribute-is-deprecated.phpt - PHPUnit\Framework\PhptAssertionFailedError + targeting-traits-with-coversclass-attribute-is-deprecated.phptFailed asserting that string matches format description. ❌ targeting-traits-with-usesclass-attribute-is-deprecated.phpt - PHPUnit\Framework\PhptAssertionFailedError + targeting-traits-with-usesclass-attribute-is-deprecated.phptFailed asserting that string matches format description. ``` ### ✅ PHPUnit\Event\CollectingDispatcherTest ``` diff --git a/__tests__/__snapshots__/phpunit-junit.test.ts.snap b/__tests__/__snapshots__/phpunit-junit.test.ts.snap index d48d941..fbfaf76 100644 --- a/__tests__/__snapshots__/phpunit-junit.test.ts.snap +++ b/__tests__/__snapshots__/phpunit-junit.test.ts.snap @@ -315,7 +315,7 @@ Failed asserting that 19 matches expected 20. /workspace/phpcheckstyle/test/OtherTest.php:24", "line": 12, - "message": "PHPUnit\\Framework\\ExpectationFailedException", + "message": "OtherTest::testOther", "path": undefined, }, "name": "testOther", @@ -330,7 +330,7 @@ Failed asserting that 0 matches expected 1. /workspace/phpcheckstyle/test/OtherTest.php:40", "line": 31, - "message": "PHPUnit\\Framework\\ExpectationFailedException", + "message": "OtherTest::testException", "path": undefined, }, "name": "testException", @@ -572,7 +572,7 @@ TestRunResult { /home/matteo/OSS/phpunit/src/TextUI/TestRunner.php:62 /home/matteo/OSS/phpunit/src/TextUI/Application.php:200", "line": undefined, - "message": "PHPUnit\\Framework\\PhptAssertionFailedError", + "message": "targeting-traits-with-coversclass-attribute-is-deprecated.phptFailed asserting that string matches format description.", "path": undefined, }, "name": "targeting-traits-with-coversclass-attribute-is-deprecated.phpt", @@ -609,7 +609,7 @@ TestRunResult { /home/matteo/OSS/phpunit/src/TextUI/TestRunner.php:62 /home/matteo/OSS/phpunit/src/TextUI/Application.php:200", "line": undefined, - "message": "PHPUnit\\Framework\\PhptAssertionFailedError", + "message": "targeting-traits-with-usesclass-attribute-is-deprecated.phptFailed asserting that string matches format description.", "path": undefined, }, "name": "targeting-traits-with-usesclass-attribute-is-deprecated.phpt", diff --git a/__tests__/fixtures/phpunit/phpunit-no-message-attr.xml b/__tests__/fixtures/phpunit/phpunit-no-message-attr.xml new file mode 100644 index 0000000..d528d3e --- /dev/null +++ b/__tests__/fixtures/phpunit/phpunit-no-message-attr.xml @@ -0,0 +1,20 @@ + + + + + Symfony\Component\VarDumper\Tests\Caster\DOMCasterTest::testCastModernDocumentType +TypeError: Cannot assign DOMNodeList to property Dom\Node::$childNodes of type Dom\NodeList + +/home/runner/work/php-latest-builds/php-latest-builds/src/Symfony/Component/VarDumper/Caster/DOMCaster.php:190 +/home/runner/work/php-latest-builds/php-latest-builds/src/Symfony/Component/VarDumper/Cloner/AbstractCloner.php:379 +/home/runner/work/php-latest-builds/php-latest-builds/src/Symfony/Component/VarDumper/Cloner/VarCloner.php:127 +/home/runner/work/php-latest-builds/php-latest-builds/src/Symfony/Component/VarDumper/Cloner/AbstractCloner.php:318 +/home/runner/work/php-latest-builds/php-latest-builds/src/Symfony/Component/VarDumper/Test/VarDumperTestTrait.php:79 +/home/runner/work/php-latest-builds/php-latest-builds/src/Symfony/Component/VarDumper/Test/VarDumperTestTrait.php:63 +/home/runner/work/php-latest-builds/php-latest-builds/src/Symfony/Component/VarDumper/Tests/Caster/DOMCasterTest.php:235 +/home/runner/work/php-latest-builds/php-latest-builds/.phpunit/phpunit-12-0/phpunit:104 +/home/runner/work/php-latest-builds/php-latest-builds/vendor/symfony/phpunit-bridge/bin/simple-phpunit.php:458 +/home/runner/work/php-latest-builds/php-latest-builds/vendor/symfony/phpunit-bridge/bin/simple-phpunit:13 + + + diff --git a/__tests__/phpunit-message-extraction.test.ts b/__tests__/phpunit-message-extraction.test.ts new file mode 100644 index 0000000..f8ee19a --- /dev/null +++ b/__tests__/phpunit-message-extraction.test.ts @@ -0,0 +1,105 @@ +import * as fs from 'fs' +import * as path from 'path' + +import {PhpunitJunitParser} from '../src/parsers/phpunit-junit/phpunit-junit-parser.js' +import {ParseOptions} from '../src/test-parser.js' +import {normalizeFilePath} from '../src/utils/path-utils.js' + +import {fileURLToPath} from 'url' +import {dirname} from 'path' +const __filename = fileURLToPath(import.meta.url) +const __dirname = dirname(__filename) + +describe('phpunit-junit parser - message extraction', () => { + it('extracts message from first line of error details when message attribute is not present', async () => { + const fixturePath = path.join(__dirname, 'fixtures', 'phpunit', 'phpunit-no-message-attr.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 failed test + const suite = result.suites.find(s => s.name === 'DOMCasterTest') + expect(suite).toBeDefined() + + const tests = suite!.groups.flatMap(g => g.tests) + const failedTest = tests.find(t => t.name === 'testCastModernDocumentType') + expect(failedTest).toBeDefined() + expect(failedTest!.result).toBe('failed') + expect(failedTest!.error).toBeDefined() + + // Verify that the message is extracted from the first line of error details + expect(failedTest!.error!.message).toBe( + 'TypeError: Cannot assign DOMNodeList to property Dom\\Node::$childNodes of type Dom\\NodeList' + ) + + // Verify that full details are still captured + expect(failedTest!.error!.details).toContain('TypeError: Cannot assign DOMNodeList to property Dom\\Node::$childNodes of type Dom\\NodeList') + expect(failedTest!.error!.details).toContain('/home/runner/work/php-latest-builds/php-latest-builds/src/Symfony/Component/VarDumper/Caster/DOMCaster.php:190') + }) + + it('prefers message attribute when present', 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) + + // Find the failed test that has a message attribute + const authSuite = result.suites.find(s => s.name === 'Tests.Authentication') + expect(authSuite).toBeDefined() + + const tests = authSuite!.groups.flatMap(g => g.tests) + const failedTest = tests.find(t => t.name === 'testCase9') + expect(failedTest).toBeDefined() + expect(failedTest!.result).toBe('failed') + expect(failedTest!.error).toBeDefined() + + // When message attribute is present, use it (with type prepended) + expect(failedTest!.error!.message).toBe('AssertionError: Assertion error message') + }) + + it('uses type as message when neither message attribute nor details are available', async () => { + // Create a minimal XML with only type attribute + const xmlContent = ` + + + + + + +` + + const opts: ParseOptions = { + parseErrors: true, + trackedFiles: [] + } + + const parser = new PhpunitJunitParser(opts) + const result = await parser.parse('test.xml', xmlContent) + + const suite = result.suites.find(s => s.name === 'TestSuite') + expect(suite).toBeDefined() + + const tests = suite!.groups.flatMap(g => g.tests) + const failedTest = tests.find(t => t.name === 'testError') + expect(failedTest).toBeDefined() + expect(failedTest!.result).toBe('failed') + expect(failedTest!.error).toBeDefined() + + // When only type is available, use it as message + expect(failedTest!.error!.message).toBe('RuntimeException') + }) +}) diff --git a/src/parsers/phpunit-junit/phpunit-junit-parser.ts b/src/parsers/phpunit-junit/phpunit-junit-parser.ts index 4886fc1..af479fa 100644 --- a/src/parsers/phpunit-junit/phpunit-junit-parser.ts +++ b/src/parsers/phpunit-junit/phpunit-junit-parser.ts @@ -156,13 +156,42 @@ export class PhpunitJunitParser implements TestParser { } let message: string | undefined + + // First, try to extract message attribute (with type prepended if available) if (typeof failure !== 'string' && failure.$) { - message = failure.$.message - if (failure.$.type) { - message = message ? `${failure.$.type}: ${message}` : failure.$.type + if (failure.$.message) { + message = failure.$.type ? `${failure.$.type}: ${failure.$.message}` : failure.$.message + } else if (failure.$.type && !details) { + // Only use type alone if there's no details to extract from + message = failure.$.type } } + // If no message attribute, try to extract from details + if (!message && details) { + const typePrefix = typeof failure !== 'string' && failure.$?.type ? `${failure.$.type}: ` : '' + const lines = details.split(/\r?\n/) + for (const line of lines) { + const trimmedLine = line.trim() + // Check if line starts with type prefix (e.g., "TypeError: ") + if (typePrefix && trimmedLine.startsWith(typePrefix)) { + message = trimmedLine + break + } + } + + // If no error message pattern found, use first line + if (!message) { + const firstLine = lines[0].trim() + message = firstLine || undefined + } + } + + // If still no message and type is available, use it as last resort + if (!message && typeof failure !== 'string' && failure.$?.type) { + message = failure.$.type + } + return { path: filePath, line,