diff --git a/__tests__/fixtures/phpunit/phpunit-paths.xml b/__tests__/fixtures/phpunit/phpunit-paths.xml
new file mode 100644
index 0000000..46b0a16
--- /dev/null
+++ b/__tests__/fixtures/phpunit/phpunit-paths.xml
@@ -0,0 +1,23 @@
+
+
+
+
+ /home/runner/work/repo/src/Fake.php:42
+
+
+ /home/runner/work/repo/src/Other.php:10
+
+
+ at /home/runner/work/repo/src/Paren.php(123)
+
+
+ C:\repo\src\Win.php:77
+
+
+ at C:\repo\src\WinParen.php(88)
+
+
+ /home/runner/work/repo/tests/Sample.phpt:12
+
+
+
diff --git a/__tests__/phpunit-junit.test.ts b/__tests__/phpunit-junit.test.ts
index 03a4306..8075208 100644
--- a/__tests__/phpunit-junit.test.ts
+++ b/__tests__/phpunit-junit.test.ts
@@ -100,6 +100,67 @@ describe('phpunit-junit tests', () => {
}
})
+ 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))
diff --git a/src/parsers/phpunit-junit/phpunit-junit-parser.ts b/src/parsers/phpunit-junit/phpunit-junit-parser.ts
index 79e6056..c24e87d 100644
--- a/src/parsers/phpunit-junit/phpunit-junit-parser.ts
+++ b/src/parsers/phpunit-junit/phpunit-junit-parser.ts
@@ -2,7 +2,7 @@ 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 {getBasePath, normalizeFilePath} from '../../utils/path-utils'
import {
TestExecutionResult,
@@ -15,9 +15,12 @@ import {
export class PhpunitJunitParser implements TestParser {
readonly trackedFiles: Set
+ readonly trackedFilesList: string[]
+ private assumedWorkDir: string | undefined
constructor(readonly options: ParseOptions) {
- this.trackedFiles = new Set(options.trackedFiles.map(f => normalizeFilePath(f)))
+ this.trackedFilesList = options.trackedFiles.map(f => normalizeFilePath(f))
+ this.trackedFiles = new Set(this.trackedFilesList)
}
async parse(filePath: string, content: string): Promise {
@@ -127,16 +130,16 @@ export class PhpunitJunitParser implements TestParser {
}
const failure = failures[0]
- const details = failure._ ?? ''
+ 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 normalizedPath = normalizeFilePath(tc.$.file)
- if (this.trackedFiles.has(normalizedPath)) {
- filePath = normalizedPath
+ const relativePath = this.getRelativePath(tc.$.file)
+ if (this.trackedFiles.has(relativePath)) {
+ filePath = relativePath
}
if (tc.$.line) {
line = parseInt(tc.$.line)
@@ -153,7 +156,7 @@ export class PhpunitJunitParser implements TestParser {
}
let message: string | undefined
- if (failure.$) {
+ if (typeof failure !== 'string' && failure.$) {
message = failure.$.message
if (failure.$.type) {
message = message ? `${failure.$.type}: ${message}` : failure.$.type
@@ -174,17 +177,48 @@ export class PhpunitJunitParser implements TestParser {
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)}
+ 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
}
+
+ 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
+ }
+
+ 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
+ }
}