Skip to content

Commit

Permalink
Refactor junitxml parser (#45)
Browse files Browse the repository at this point in the history
* Refactor junitxml parser

* Refactor

* Refactor

* Refactor

* Refactor
  • Loading branch information
int128 authored Oct 19, 2024
1 parent a4620e6 commit 09ac14d
Show file tree
Hide file tree
Showing 2 changed files with 93 additions and 76 deletions.
118 changes: 64 additions & 54 deletions src/junitxml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,120 +22,130 @@ export const parseTestReportFiles = async (testReportFiles: string[]): Promise<T
return testFiles
}

const parseTestReportFilesToJunitXml = async (testReportFiles: string[]): Promise<JunitXml[]> => {
const junitXmls: JunitXml[] = []
core.startGroup(`Parsing ${testReportFiles.length} test report files`)
for (const testReportFile of testReportFiles) {
core.info(`Parsing the test report: ${testReportFile}`)
const xml = await fs.readFile(testReportFile)
const junitXml = parseJunitXml(xml)
junitXmls.push(junitXml)
export const groupTestCasesByTestFile = (testCases: TestCase[]): TestFile[] => {
const testFiles = new Map<string, TestFile>()
for (const testCase of testCases) {
const currentTestFile = testFiles.get(testCase.filename) ?? {
filename: testCase.filename,
totalTime: 0,
totalTestCases: 0,
}
currentTestFile.totalTime += testCase.time
currentTestFile.totalTestCases++
testFiles.set(testCase.filename, currentTestFile)
}
core.endGroup()
return junitXmls
return [...testFiles.values()]
}

export type TestCase = {
filename: string
time: number
}

export const findTestCasesFromJunitXml = (junitXml: JunitXml): TestCase[] => {
const testCases: TestCase[] = []
const visit = (testSuite: TestSuite): void => {
for (const testCase of testSuite.testcase ?? []) {
testCases.push(testCase)
function* visit(testSuite: JunitXmlTestSuite): Generator<TestCase> {
for (const junitXmlTestCase of testSuite.testcase ?? []) {
yield {
filename: path.normalize(junitXmlTestCase['@_file']),
time: junitXmlTestCase['@_time'],
}
}
for (const nestedTestSuite of testSuite.testsuite ?? []) {
visit(nestedTestSuite)
}
}

const root = junitXml.testsuites?.testsuite ?? junitXml.testsuite ?? []
const testCases: TestCase[] = []
for (const testSuite of root) {
visit(testSuite)
for (const testCase of visit(testSuite)) {
testCases.push(testCase)
}
}
return testCases
}

export const groupTestCasesByTestFile = (testCases: TestCase[]): TestFile[] => {
const testFiles = new Map<string, TestFile>()
for (const testCase of testCases) {
const testFilename = path.normalize(testCase['@_file'])
const currentTestFile = testFiles.get(testFilename) ?? {
filename: testFilename,
totalTime: 0,
totalTestCases: 0,
}
currentTestFile.totalTime += testCase['@_time']
currentTestFile.totalTestCases++
testFiles.set(testFilename, currentTestFile)
const parseTestReportFilesToJunitXml = async (testReportFiles: string[]): Promise<JunitXml[]> => {
const junitXmls: JunitXml[] = []
core.startGroup(`Parsing ${testReportFiles.length} test report files`)
for (const testReportFile of testReportFiles) {
core.info(`Parsing the test report: ${testReportFile}`)
const xml = await fs.readFile(testReportFile)
const junitXml = parseJunitXml(xml)
junitXmls.push(junitXml)
}
return [...testFiles.values()]
core.endGroup()
return junitXmls
}

type JunitXml = {
testsuites?: {
testsuite?: TestSuite[]
testsuite?: JunitXmlTestSuite[]
}
testsuite?: TestSuite[]
testsuite?: JunitXmlTestSuite[]
}

function assertJunitXml(x: unknown): asserts x is JunitXml {
assert(typeof x === 'object', 'root element must be an object')
assert(x != null, 'root element must not be null')
assert(typeof x === 'object', 'Root document must be an object')
assert(x != null, 'Root document must not be null')

if ('testsuites' in x) {
assert(typeof x.testsuites === 'object', 'element testsuites must be an object')
assert(x.testsuites != null, 'element testsuites must not be null')
assert(typeof x.testsuites === 'object', 'Element <testsuites> must be an object')
assert(x.testsuites != null, 'Element <testsuites> must not be null')

if ('testsuite' in x.testsuites) {
assert(Array.isArray(x.testsuites.testsuite), 'element testsuite must be an array')
assert(Array.isArray(x.testsuites.testsuite), 'Element <testsuite> must be an array')
for (const testsuite of x.testsuites.testsuite) {
assertTestSuite(testsuite)
}
}
}

if ('testsuite' in x) {
assert(Array.isArray(x.testsuite), 'element testsuite must be an array')
assert(Array.isArray(x.testsuite), 'Element <testsuite> must be an array')
for (const testsuite of x.testsuite) {
assertTestSuite(testsuite)
}
}
}

type TestSuite = {
testsuite?: TestSuite[]
testcase?: TestCase[]
type JunitXmlTestSuite = {
testsuite?: JunitXmlTestSuite[]
testcase?: JunitXmlTestCase[]
}

function assertTestSuite(x: unknown): asserts x is TestSuite {
assert(typeof x === 'object', 'element testsuite must be an object')
assert(x != null, 'element testsuite must not be null')
function assertTestSuite(x: unknown): asserts x is JunitXmlTestSuite {
assert(typeof x === 'object', 'Element <testsuite> must be an object')
assert(x != null, 'Element <testsuite> must not be null')
if ('testsuite' in x) {
assert(Array.isArray(x.testsuite), 'element testsuite must be an array')
assert(Array.isArray(x.testsuite), 'Element <testsuite> must be an array')
for (const testsuite of x.testsuite) {
assertTestSuite(testsuite)
}
}
if ('testcase' in x) {
assert(Array.isArray(x.testcase), 'element testcase must be an array')
assert(Array.isArray(x.testcase), 'Element <testcase> must be an array')
for (const testcase of x.testcase) {
assertTestCase(testcase)
}
}
}

export type TestCase = {
type JunitXmlTestCase = {
'@_name': string
'@_time': number
'@_file': string
}

function assertTestCase(x: unknown): asserts x is TestCase {
assert(typeof x === 'object', 'element testcase must be an object')
assert(x != null, 'element testcase must not be null')
assert('@_name' in x, 'element testcase must have name attribute')
assert(typeof x['@_name'] === 'string', 'name attribute of testcase must be a string')
assert('@_time' in x, 'element testcase must have time attribute')
assert(typeof x['@_time'] === 'number', 'time attribute of testcase must be a number')
assert('@_file' in x, 'element testcase must have file attribute')
assert(typeof x['@_file'] === 'string', 'file attribute of testcase must be a string')
function assertTestCase(x: unknown): asserts x is JunitXmlTestCase {
assert(typeof x === 'object', 'Element <testcase> must be an object')
assert(x != null, 'Element <testcase> must not be null')
assert('@_name' in x, 'Element <testcase> must have "name" attribute')
assert(typeof x['@_name'] === 'string', 'name attribute of <testcase> must be a string')
assert('@_time' in x, 'Element <testcase> must have "time" attribute')
assert(typeof x['@_time'] === 'number', 'time attribute of <testcase> must be a number')
assert('@_file' in x, 'Element <testcase> must have "file" attribute')
assert(typeof x['@_file'] === 'string', 'file attribute of <testcase> must be a string')
}

export const parseJunitXml = (xml: string | Buffer): JunitXml => {
Expand Down
51 changes: 29 additions & 22 deletions tests/junitxml.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,40 +47,47 @@ describe('findTestCasesFromJunitXml', () => {
],
}
expect(findTestCasesFromJunitXml(junitXml)).toEqual<TestCase[]>([
{ '@_name': 'test1', '@_time': 1, '@_file': 'file1' },
{ '@_name': 'test2', '@_time': 2, '@_file': 'file2' },
{ '@_name': 'test3', '@_time': 3, '@_file': 'file1' },
{ '@_name': 'test4', '@_time': 4, '@_file': 'file2' },
{ '@_name': 'test5', '@_time': 5, '@_file': 'file3' },
{ filename: 'file1', time: 1 },
{ filename: 'file2', time: 2 },
{ filename: 'file1', time: 3 },
{ filename: 'file2', time: 4 },
{ filename: 'file3', time: 5 },
])
})

it('should normalize file paths', () => {
const junitXml = {
testsuite: [
{
testcase: [
{ '@_name': 'test1', '@_time': 1, '@_file': 'file1' },
{ '@_name': 'test2', '@_time': 2, '@_file': './file2' },
{ '@_name': 'test3', '@_time': 3, '@_file': './file1' },
],
},
],
}
expect(findTestCasesFromJunitXml(junitXml)).toEqual<TestCase[]>([
{ filename: 'file1', time: 1 },
{ filename: 'file2', time: 2 },
{ filename: 'file1', time: 3 },
])
})
})

describe('groupTestCasesByTestFile', () => {
it('should group test cases by file', () => {
const testCases: TestCase[] = [
{ '@_name': 'test1', '@_time': 1, '@_file': 'file1' },
{ '@_name': 'test2', '@_time': 2, '@_file': 'file2' },
{ '@_name': 'test3', '@_time': 3, '@_file': 'file1' },
{ '@_name': 'test4', '@_time': 4, '@_file': 'file2' },
{ '@_name': 'test5', '@_time': 5, '@_file': 'file3' },
{ filename: 'file1', time: 1 },
{ filename: 'file2', time: 2 },
{ filename: 'file1', time: 3 },
{ filename: 'file2', time: 4 },
{ filename: 'file3', time: 5 },
]
expect(groupTestCasesByTestFile(testCases)).toEqual([
{ filename: 'file1', totalTime: 4, totalTestCases: 2 },
{ filename: 'file2', totalTime: 6, totalTestCases: 2 },
{ filename: 'file3', totalTime: 5, totalTestCases: 1 },
])
})

it('should normalize file paths', () => {
const testCases: TestCase[] = [
{ '@_name': 'test1', '@_time': 1, '@_file': 'file1' },
{ '@_name': 'test2', '@_time': 2, '@_file': './file2' },
{ '@_name': 'test3', '@_time': 3, '@_file': './file1' },
]
expect(groupTestCasesByTestFile(testCases)).toEqual([
{ filename: 'file1', totalTime: 4, totalTestCases: 2 },
{ filename: 'file2', totalTime: 2, totalTestCases: 1 },
])
})
})

0 comments on commit 09ac14d

Please sign in to comment.