From 09ac14d29f2fc1e9a3cb5a7fbe85cae28e7e298d Mon Sep 17 00:00:00 2001 From: Hidetake Iwata Date: Sat, 19 Oct 2024 14:44:46 +0900 Subject: [PATCH] Refactor junitxml parser (#45) * Refactor junitxml parser * Refactor * Refactor * Refactor * Refactor --- src/junitxml.ts | 118 ++++++++++++++++++++++------------------- tests/junitxml.test.ts | 51 ++++++++++-------- 2 files changed, 93 insertions(+), 76 deletions(-) diff --git a/src/junitxml.ts b/src/junitxml.ts index 1fccf33..b4464ac 100644 --- a/src/junitxml.ts +++ b/src/junitxml.ts @@ -22,69 +22,79 @@ export const parseTestReportFiles = async (testReportFiles: string[]): Promise => { - 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() + 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 { + 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() - 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 => { + 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 must be an object') + assert(x.testsuites != null, 'Element 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 must be an array') for (const testsuite of x.testsuites.testsuite) { assertTestSuite(testsuite) } @@ -92,50 +102,50 @@ function assertJunitXml(x: unknown): asserts x is JunitXml { } if ('testsuite' in x) { - assert(Array.isArray(x.testsuite), 'element testsuite must be an array') + assert(Array.isArray(x.testsuite), 'Element 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 must be an object') + assert(x != null, 'Element 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 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 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 must be an object') + assert(x != null, 'Element must not be null') + assert('@_name' in x, 'Element must have "name" attribute') + assert(typeof x['@_name'] === 'string', 'name attribute of must be a string') + assert('@_time' in x, 'Element must have "time" attribute') + assert(typeof x['@_time'] === 'number', 'time attribute of must be a number') + assert('@_file' in x, 'Element must have "file" attribute') + assert(typeof x['@_file'] === 'string', 'file attribute of must be a string') } export const parseJunitXml = (xml: string | Buffer): JunitXml => { diff --git a/tests/junitxml.test.ts b/tests/junitxml.test.ts index d8a87f6..6696ebc 100644 --- a/tests/junitxml.test.ts +++ b/tests/junitxml.test.ts @@ -47,11 +47,30 @@ describe('findTestCasesFromJunitXml', () => { ], } expect(findTestCasesFromJunitXml(junitXml)).toEqual([ - { '@_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([ + { filename: 'file1', time: 1 }, + { filename: 'file2', time: 2 }, + { filename: 'file1', time: 3 }, ]) }) }) @@ -59,11 +78,11 @@ describe('findTestCasesFromJunitXml', () => { 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 }, @@ -71,16 +90,4 @@ describe('groupTestCasesByTestFile', () => { { 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 }, - ]) - }) })