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
# java-junit
# jest-junit
# pytest-junit
# mocha-json
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.
You can use the following example configuration in `package.json`:
```json
{
"scripts": {
"test": "jest --ci --reporters=default --reporters=jest-Junit"
},
@ -280,6 +282,7 @@ You can use the following example configuration in `package.json`:
"classNameTemplate": "{classname}",
"titleTemplate": "{title}"
}
}
```
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`:
```json
{
"scripts": {
"test": "mocha --reporter json > test-results.json"
}
}
```
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 {JavaJunitParser} from './parsers/java-junit/java-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 {normalizeDirPath, normalizeFilePath} from './utils/path-utils'
@ -214,6 +215,8 @@ class TestReporter {
return new JavaJunitParser(options)
case 'jest-junit':
return new JestJunitParser(options)
case 'pytest-junit':
return new PytestJunitParser(options)
case 'mocha-json':
return new MochaJsonParser(options)
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[]
}