feat: add pytest junit parser and simple test

This commit is contained in:
Martin Fillafer 2023-03-13 19:03:20 +01:00
parent e9fa2f582c
commit a007309f5d
6 changed files with 214 additions and 17 deletions

View file

@ -132,6 +132,7 @@ jobs:
# flutter-json # flutter-json
# java-junit # java-junit
# jest-junit # jest-junit
# pytest-junit
# mocha-json # mocha-json
reporter: '' reporter: ''
@ -264,6 +265,7 @@ Some heuristic was necessary to figure out the mapping between the line in the s
It will create test results in Junit XML format which can be then processed by this action. It will create test results in Junit XML format which can be then processed by this action.
You can use the following example configuration in `package.json`: You can use the following example configuration in `package.json`:
```json ```json
{
"scripts": { "scripts": {
"test": "jest --ci --reporters=default --reporters=jest-Junit" "test": "jest --ci --reporters=default --reporters=jest-Junit"
}, },
@ -280,6 +282,7 @@ You can use the following example configuration in `package.json`:
"classNameTemplate": "{classname}", "classNameTemplate": "{classname}",
"titleTemplate": "{title}" "titleTemplate": "{title}"
} }
}
``` ```
Configuration of `uniqueOutputName`, `suiteNameTemplate`, `classNameTemplate`, `titleTemplate` is important for proper visualization of test results. Configuration of `uniqueOutputName`, `suiteNameTemplate`, `classNameTemplate`, `titleTemplate` is important for proper visualization of test results.
@ -294,9 +297,11 @@ Configuration of `uniqueOutputName`, `suiteNameTemplate`, `classNameTemplate`, `
You can use the following example configuration in `package.json`: You can use the following example configuration in `package.json`:
```json ```json
{
"scripts": { "scripts": {
"test": "mocha --reporter json > test-results.json" "test": "mocha --reporter json > test-results.json"
} }
}
``` ```
Test processing might fail if any of your tests write anything on standard output. Test processing might fail if any of your tests write anything on standard output.

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<testsuites>
<testsuite name="pytest" errors="0" failures="0" skipped="0" tests="1" time="0.178"
timestamp="2023-03-08T11:47:09.377238" hostname="0e634555ad5c">
<testcase classname="product_changes.tests.first_test.MyTestCase" name="test_something" time="0.133"/>
</testsuite>
</testsuites>

View file

@ -0,0 +1,24 @@
import * as fs from 'fs'
import * as path from 'path'
import {PytestJunitParser} from '../src/parsers/pytest-junit/pytest-junit-parser'
import {ParseOptions} from '../src/test-parser'
import {normalizeFilePath} from '../src/utils/path-utils'
describe('pytest-junit tests', () => {
it('test with one successful test', async () => {
const fixturePath = path.join(__dirname, 'fixtures', 'external', 'pytest', 'single-case.xml')
const filePath = normalizeFilePath(path.relative(__dirname, fixturePath))
const fileContent = fs.readFileSync(fixturePath, {encoding: 'utf8'})
const opts: ParseOptions = {
parseErrors: true,
trackedFiles: []
}
const parser = new PytestJunitParser(opts)
const result = await parser.parse(filePath, fileContent)
expect(result.tests).toBe(1)
expect(result.result).toBe('success')
})
})

View file

@ -14,6 +14,7 @@ import {DartJsonParser} from './parsers/dart-json/dart-json-parser'
import {DotnetTrxParser} from './parsers/dotnet-trx/dotnet-trx-parser' import {DotnetTrxParser} from './parsers/dotnet-trx/dotnet-trx-parser'
import {JavaJunitParser} from './parsers/java-junit/java-junit-parser' import {JavaJunitParser} from './parsers/java-junit/java-junit-parser'
import {JestJunitParser} from './parsers/jest-junit/jest-junit-parser' import {JestJunitParser} from './parsers/jest-junit/jest-junit-parser'
import {PytestJunitParser} from './parsers/pytest-junit/pytest-junit-parser'
import {MochaJsonParser} from './parsers/mocha-json/mocha-json-parser' import {MochaJsonParser} from './parsers/mocha-json/mocha-json-parser'
import {normalizeDirPath, normalizeFilePath} from './utils/path-utils' import {normalizeDirPath, normalizeFilePath} from './utils/path-utils'
@ -214,6 +215,8 @@ class TestReporter {
return new JavaJunitParser(options) return new JavaJunitParser(options)
case 'jest-junit': case 'jest-junit':
return new JestJunitParser(options) return new JestJunitParser(options)
case 'pytest-junit':
return new PytestJunitParser(options)
case 'mocha-json': case 'mocha-json':
return new MochaJsonParser(options) return new MochaJsonParser(options)
default: default:

View file

@ -0,0 +1,124 @@
import {ParseOptions, TestParser} from '../../test-parser'
import {parseStringPromise} from 'xml2js'
import {JunitReport, TestCase, TestSuite} from './pytest-junit-types'
import {getExceptionSource} from '../../utils/node-utils'
import {getBasePath, normalizeFilePath} from '../../utils/path-utils'
import {
TestExecutionResult,
TestRunResult,
TestSuiteResult,
TestGroupResult,
TestCaseResult,
TestCaseError
} from '../../test-results'
export class PytestJunitParser implements TestParser {
assumedWorkDir: string | undefined
constructor(readonly options: ParseOptions) {}
async parse(path: string, content: string): Promise<TestRunResult> {
const ju = await this.getJunitReport(path, content)
return this.getTestRunResult(path, ju)
}
private async getJunitReport(path: string, content: string): Promise<JunitReport> {
try {
return (await parseStringPromise(content)) as JunitReport
} catch (e) {
throw new Error(`Invalid XML at ${path}\n\n${e}`)
}
}
private getTestRunResult(path: string, junit: JunitReport): TestRunResult {
const suites: TestSuiteResult[] =
junit.testsuites.testsuite === undefined
? []
: junit.testsuites.testsuite.map(ts => {
const name = ts.$.name.trim()
const time = parseFloat(ts.$.time) * 1000
return new TestSuiteResult(name, this.getGroups(ts), time)
})
const time =
junit.testsuites.$ === undefined
? suites.reduce((sum, suite) => sum + suite.time, 0)
: parseFloat(junit.testsuites.$.time) * 1000
return new TestRunResult(path, suites, time)
}
private getGroups(suite: TestSuite): TestGroupResult[] {
if (!suite.testcase) {
return []
}
const groups: {describe: string; tests: TestCase[]}[] = []
for (const tc of suite.testcase) {
let grp = groups.find(g => g.describe === tc.$.classname)
if (grp === undefined) {
grp = {describe: tc.$.classname, tests: []}
groups.push(grp)
}
grp.tests.push(tc)
}
return groups.map(grp => {
const tests = grp.tests.map(tc => {
const name = tc.$.name.trim()
const result = this.getTestCaseResult(tc)
const time = parseFloat(tc.$.time) * 1000
const error = this.getTestCaseError(tc)
return new TestCaseResult(name, result, time, error)
})
return new TestGroupResult(grp.describe, tests)
})
}
private getTestCaseResult(test: TestCase): TestExecutionResult {
if (test.failure) return 'failed'
if (test.skipped) return 'skipped'
return 'success'
}
private getTestCaseError(tc: TestCase): TestCaseError | undefined {
if (!this.options.parseErrors || !tc.failure) {
return undefined
}
const details = tc.failure[0]
let path
let line
const src = getExceptionSource(details, this.options.trackedFiles, file => this.getRelativePath(file))
if (src) {
path = src.path
line = src.line
}
return {
path,
line,
details
}
}
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 {
return (
this.options.workDir ??
this.assumedWorkDir ??
(this.assumedWorkDir = getBasePath(path, this.options.trackedFiles))
)
}
}

View file

@ -0,0 +1,34 @@
export interface JunitReport {
testsuites: TestSuites
}
export interface TestSuites {
$?: {
time: string
}
testsuite?: TestSuite[]
}
export interface TestSuite {
$: {
name: string
tests: string
errors: string
failures: string
skipped: string
time: string
timestamp?: Date
}
testcase?: TestCase[]
}
export interface TestCase {
$: {
classname: string
file?: string
name: string
time: string
}
failure?: string[]
skipped?: string[]
}