test-reporter/__tests__/open-test-reporting.test.ts
Piotr Mionskowski eedd088b6d Add support for open-test-reporting format
Add a new parser for the open-test-reporting format developed by the
JUnit team (https://github.com/ota4j-team/open-test-reporting).

This format is a modern, framework-agnostic XML-based test reporting
standard that supports rich metadata including tags, attachments, and
infrastructure information.

Features:
- Auto-detection of both XML format variants:
  - Hierarchical format (h:execution) - tree-structured results
  - Event-based format (e:events) - streaming/real-time results
- ISO 8601 duration parsing (e.g., PT1.234S)
- Status mapping: SUCCESSFUL, SKIPPED, ABORTED, FAILED, ERRORED
- Error message extraction from failed tests
- Proper XML namespace handling

Files added:
- src/parsers/open-test-reporting/open-test-reporting-types.ts
- src/parsers/open-test-reporting/open-test-reporting-parser.ts
- __tests__/open-test-reporting.test.ts (20 tests)
- __tests__/fixtures/open-test-reporting/*.xml
2025-12-05 09:43:46 +01:00

344 lines
15 KiB
TypeScript

import * as fs from 'fs'
import * as path from 'path'
import {OpenTestReportingParser} from '../src/parsers/open-test-reporting/open-test-reporting-parser'
import {ParseOptions} from '../src/test-parser'
import {getReport} from '../src/report/get-report'
import {normalizeFilePath} from '../src/utils/path-utils'
describe('open-test-reporting tests', () => {
const defaultOpts: ParseOptions = {
parseErrors: true,
trackedFiles: []
}
describe('hierarchy format', () => {
it('parses hierarchy format correctly', async () => {
const fixturePath = path.join(__dirname, 'fixtures', 'open-test-reporting', 'hierarchy.xml')
const filePath = normalizeFilePath(path.relative(__dirname, fixturePath))
const fileContent = fs.readFileSync(fixturePath, {encoding: 'utf8'})
const parser = new OpenTestReportingParser(defaultOpts)
const result = await parser.parse(filePath, fileContent)
// 3 suites: UserServiceTest, PaymentServiceTest, EmptySuite
expect(result.suites.length).toBe(3)
// UserServiceTest: 3 tests (2 passed, 1 skipped)
const userSuite = result.suites.find(s => s.name === 'com.example.UserServiceTest')
expect(userSuite).toBeDefined()
expect(userSuite!.tests).toBe(3)
expect(userSuite!.passed).toBe(2)
expect(userSuite!.skipped).toBe(1)
expect(userSuite!.failed).toBe(0)
// PaymentServiceTest: 4 tests (3 passed, 1 failed) with nested groups
const paymentSuite = result.suites.find(s => s.name === 'com.example.PaymentServiceTest')
expect(paymentSuite).toBeDefined()
expect(paymentSuite!.tests).toBe(4)
expect(paymentSuite!.passed).toBe(3)
expect(paymentSuite!.failed).toBe(1)
// Check groups exist
expect(paymentSuite!.groups.length).toBe(2)
const validationGroup = paymentSuite!.groups.find(g => g.name === 'ValidationTests')
const processingGroup = paymentSuite!.groups.find(g => g.name === 'ProcessingTests')
expect(validationGroup).toBeDefined()
expect(processingGroup).toBeDefined()
expect(validationGroup!.tests.length).toBe(2)
expect(processingGroup!.tests.length).toBe(2)
// EmptySuite: 0 tests
const emptySuite = result.suites.find(s => s.name === 'com.example.EmptySuite')
expect(emptySuite).toBeDefined()
expect(emptySuite!.tests).toBe(0)
})
it('parses duration correctly', async () => {
const fixturePath = path.join(__dirname, 'fixtures', 'open-test-reporting', 'hierarchy.xml')
const filePath = normalizeFilePath(path.relative(__dirname, fixturePath))
const fileContent = fs.readFileSync(fixturePath, {encoding: 'utf8'})
const parser = new OpenTestReportingParser(defaultOpts)
const result = await parser.parse(filePath, fileContent)
// UserServiceTest has duration PT1.234S = 1234ms
const userSuite = result.suites.find(s => s.name === 'com.example.UserServiceTest')
expect(userSuite!.time).toBeCloseTo(1234, 0)
// Check individual test times
const group = userSuite!.groups[0]
const creationTest = group.tests.find(t => t.name === 'testUserCreation')
expect(creationTest!.time).toBeCloseTo(123, 0) // PT0.123S
})
it('extracts error message from failed tests', async () => {
const fixturePath = path.join(__dirname, 'fixtures', 'open-test-reporting', 'hierarchy.xml')
const filePath = normalizeFilePath(path.relative(__dirname, fixturePath))
const fileContent = fs.readFileSync(fixturePath, {encoding: 'utf8'})
const parser = new OpenTestReportingParser(defaultOpts)
const result = await parser.parse(filePath, fileContent)
const paymentSuite = result.suites.find(s => s.name === 'com.example.PaymentServiceTest')
const processingGroup = paymentSuite!.groups.find(g => g.name === 'ProcessingTests')
const timeoutTest = processingGroup!.tests.find(t => t.name === 'testPaymentTimeout')
expect(timeoutTest!.result).toBe('failed')
expect(timeoutTest!.error).toBeDefined()
expect(timeoutTest!.error!.message).toContain('AssertionFailedError')
})
it('handles empty hierarchy file', async () => {
const fixturePath = path.join(__dirname, 'fixtures', 'open-test-reporting', 'empty.xml')
const filePath = normalizeFilePath(path.relative(__dirname, fixturePath))
const fileContent = fs.readFileSync(fixturePath, {encoding: 'utf8'})
const parser = new OpenTestReportingParser(defaultOpts)
const result = await parser.parse(filePath, fileContent)
expect(result.tests).toBe(0)
expect(result.suites.length).toBe(0)
expect(result.result).toBe('success')
})
})
describe('events format', () => {
it('parses events format correctly', async () => {
const fixturePath = path.join(__dirname, 'fixtures', 'open-test-reporting', 'events.xml')
const filePath = normalizeFilePath(path.relative(__dirname, fixturePath))
const fileContent = fs.readFileSync(fixturePath, {encoding: 'utf8'})
const parser = new OpenTestReportingParser(defaultOpts)
const result = await parser.parse(filePath, fileContent)
// 2 suites: CalculatorTest, StringUtilsTest
expect(result.suites.length).toBe(2)
// CalculatorTest: 4 tests
const calcSuite = result.suites.find(s => s.name === 'com.example.CalculatorTest')
expect(calcSuite).toBeDefined()
expect(calcSuite!.tests).toBe(4)
expect(calcSuite!.passed).toBe(2)
expect(calcSuite!.failed).toBe(1)
expect(calcSuite!.skipped).toBe(1)
// StringUtilsTest: 2 tests in TrimTests group
const stringSuite = result.suites.find(s => s.name === 'com.example.StringUtilsTest')
expect(stringSuite).toBeDefined()
expect(stringSuite!.tests).toBe(2)
expect(stringSuite!.passed).toBe(2)
// Check TrimTests group
const trimGroup = stringSuite!.groups.find(g => g.name === 'TrimTests')
expect(trimGroup).toBeDefined()
expect(trimGroup!.tests.length).toBe(2)
})
it('calculates duration from timestamps', async () => {
const fixturePath = path.join(__dirname, 'fixtures', 'open-test-reporting', 'events.xml')
const filePath = normalizeFilePath(path.relative(__dirname, fixturePath))
const fileContent = fs.readFileSync(fixturePath, {encoding: 'utf8'})
const parser = new OpenTestReportingParser(defaultOpts)
const result = await parser.parse(filePath, fileContent)
const calcSuite = result.suites.find(s => s.name === 'com.example.CalculatorTest')
// testAddition: 11:00:00.100 to 11:00:00.200 = 100ms
const addTest = calcSuite!.groups[0].tests.find(t => t.name === 'testAddition')
expect(addTest!.time).toBe(100)
})
it('extracts error message from failed events', async () => {
const fixturePath = path.join(__dirname, 'fixtures', 'open-test-reporting', 'events.xml')
const filePath = normalizeFilePath(path.relative(__dirname, fixturePath))
const fileContent = fs.readFileSync(fixturePath, {encoding: 'utf8'})
const parser = new OpenTestReportingParser(defaultOpts)
const result = await parser.parse(filePath, fileContent)
const calcSuite = result.suites.find(s => s.name === 'com.example.CalculatorTest')
const divTest = calcSuite!.groups[0].tests.find(t => t.name === 'testDivision')
expect(divTest!.result).toBe('failed')
expect(divTest!.error).toBeDefined()
expect(divTest!.error!.message).toContain('ArithmeticException')
})
})
describe('format auto-detection', () => {
it('auto-detects hierarchy format', async () => {
const fixturePath = path.join(__dirname, 'fixtures', 'open-test-reporting', 'hierarchy.xml')
const filePath = normalizeFilePath(path.relative(__dirname, fixturePath))
const fileContent = fs.readFileSync(fixturePath, {encoding: 'utf8'})
const parser = new OpenTestReportingParser(defaultOpts)
const result = await parser.parse(filePath, fileContent)
// Should parse successfully with hierarchy format
expect(result.suites.length).toBe(3)
})
it('auto-detects events format', async () => {
const fixturePath = path.join(__dirname, 'fixtures', 'open-test-reporting', 'events.xml')
const filePath = normalizeFilePath(path.relative(__dirname, fixturePath))
const fileContent = fs.readFileSync(fixturePath, {encoding: 'utf8'})
const parser = new OpenTestReportingParser(defaultOpts)
const result = await parser.parse(filePath, fileContent)
// Should parse successfully with events format
expect(result.suites.length).toBe(2)
})
it('throws error for invalid format', async () => {
const invalidXml = '<?xml version="1.0"?><unknown><root/></unknown>'
const parser = new OpenTestReportingParser(defaultOpts)
await expect(parser.parse('invalid.xml', invalidXml)).rejects.toThrow(
'Unknown open-test-reporting format'
)
})
it('throws error for invalid XML', async () => {
const invalidXml = 'not valid xml at all'
const parser = new OpenTestReportingParser(defaultOpts)
await expect(parser.parse('invalid.xml', invalidXml)).rejects.toThrow('Invalid XML')
})
})
describe('status mapping', () => {
it('maps SUCCESSFUL to success', async () => {
const fixturePath = path.join(__dirname, 'fixtures', 'open-test-reporting', 'hierarchy.xml')
const filePath = normalizeFilePath(path.relative(__dirname, fixturePath))
const fileContent = fs.readFileSync(fixturePath, {encoding: 'utf8'})
const parser = new OpenTestReportingParser(defaultOpts)
const result = await parser.parse(filePath, fileContent)
const userSuite = result.suites.find(s => s.name === 'com.example.UserServiceTest')
const creationTest = userSuite!.groups[0].tests.find(t => t.name === 'testUserCreation')
expect(creationTest!.result).toBe('success')
})
it('maps SKIPPED to skipped', async () => {
const fixturePath = path.join(__dirname, 'fixtures', 'open-test-reporting', 'hierarchy.xml')
const filePath = normalizeFilePath(path.relative(__dirname, fixturePath))
const fileContent = fs.readFileSync(fixturePath, {encoding: 'utf8'})
const parser = new OpenTestReportingParser(defaultOpts)
const result = await parser.parse(filePath, fileContent)
const userSuite = result.suites.find(s => s.name === 'com.example.UserServiceTest')
const updateTest = userSuite!.groups[0].tests.find(t => t.name === 'testUserUpdate')
expect(updateTest!.result).toBe('skipped')
})
it('maps FAILED to failed', async () => {
const fixturePath = path.join(__dirname, 'fixtures', 'open-test-reporting', 'hierarchy.xml')
const filePath = normalizeFilePath(path.relative(__dirname, fixturePath))
const fileContent = fs.readFileSync(fixturePath, {encoding: 'utf8'})
const parser = new OpenTestReportingParser(defaultOpts)
const result = await parser.parse(filePath, fileContent)
const paymentSuite = result.suites.find(s => s.name === 'com.example.PaymentServiceTest')
const processingGroup = paymentSuite!.groups.find(g => g.name === 'ProcessingTests')
const timeoutTest = processingGroup!.tests.find(t => t.name === 'testPaymentTimeout')
expect(timeoutTest!.result).toBe('failed')
})
})
describe('report generation', () => {
it('generates report from hierarchy format', async () => {
const fixturePath = path.join(__dirname, 'fixtures', 'open-test-reporting', 'hierarchy.xml')
const outputPath = path.join(__dirname, '__outputs__', 'open-test-reporting-hierarchy.md')
const filePath = normalizeFilePath(path.relative(__dirname, fixturePath))
const fileContent = fs.readFileSync(fixturePath, {encoding: 'utf8'})
const parser = new OpenTestReportingParser(defaultOpts)
const result = await parser.parse(filePath, fileContent)
const report = getReport([result])
fs.mkdirSync(path.dirname(outputPath), {recursive: true})
fs.writeFileSync(outputPath, report)
// Verify report contains expected content
expect(report).toContain('UserServiceTest')
expect(report).toContain('PaymentServiceTest')
expect(report).toContain('testPaymentTimeout')
})
it('generates report from events format', async () => {
const fixturePath = path.join(__dirname, 'fixtures', 'open-test-reporting', 'events.xml')
const outputPath = path.join(__dirname, '__outputs__', 'open-test-reporting-events.md')
const filePath = normalizeFilePath(path.relative(__dirname, fixturePath))
const fileContent = fs.readFileSync(fixturePath, {encoding: 'utf8'})
const parser = new OpenTestReportingParser(defaultOpts)
const result = await parser.parse(filePath, fileContent)
const report = getReport([result])
fs.mkdirSync(path.dirname(outputPath), {recursive: true})
fs.writeFileSync(outputPath, report)
// Verify report contains expected content
expect(report).toContain('CalculatorTest')
expect(report).toContain('StringUtilsTest')
expect(report).toContain('testDivision')
})
it('result matches snapshot for hierarchy format', async () => {
const fixturePath = path.join(__dirname, 'fixtures', 'open-test-reporting', 'hierarchy.xml')
const filePath = normalizeFilePath(path.relative(__dirname, fixturePath))
const fileContent = fs.readFileSync(fixturePath, {encoding: 'utf8'})
const parser = new OpenTestReportingParser(defaultOpts)
const result = await parser.parse(filePath, fileContent)
expect(result).toMatchSnapshot()
})
it('result matches snapshot for events format', async () => {
const fixturePath = path.join(__dirname, 'fixtures', 'open-test-reporting', 'events.xml')
const filePath = normalizeFilePath(path.relative(__dirname, fixturePath))
const fileContent = fs.readFileSync(fixturePath, {encoding: 'utf8'})
const parser = new OpenTestReportingParser(defaultOpts)
const result = await parser.parse(filePath, fileContent)
expect(result).toMatchSnapshot()
})
})
describe('duration parsing', () => {
it('parses seconds only format (PT0.123S)', async () => {
// This is tested through hierarchy.xml which uses PT0.123S format
const fixturePath = path.join(__dirname, 'fixtures', 'open-test-reporting', 'hierarchy.xml')
const filePath = normalizeFilePath(path.relative(__dirname, fixturePath))
const fileContent = fs.readFileSync(fixturePath, {encoding: 'utf8'})
const parser = new OpenTestReportingParser(defaultOpts)
const result = await parser.parse(filePath, fileContent)
const userSuite = result.suites.find(s => s.name === 'com.example.UserServiceTest')
// PT0.123S = 123ms
const creationTest = userSuite!.groups[0].tests.find(t => t.name === 'testUserCreation')
expect(creationTest!.time).toBeCloseTo(123, 0)
})
it('handles missing duration', async () => {
// EmptySuite has PT0S duration
const fixturePath = path.join(__dirname, 'fixtures', 'open-test-reporting', 'hierarchy.xml')
const filePath = normalizeFilePath(path.relative(__dirname, fixturePath))
const fileContent = fs.readFileSync(fixturePath, {encoding: 'utf8'})
const parser = new OpenTestReportingParser(defaultOpts)
const result = await parser.parse(filePath, fileContent)
const emptySuite = result.suites.find(s => s.name === 'com.example.EmptySuite')
expect(emptySuite!.time).toBe(0)
})
})
})