mirror of
https://github.com/dorny/test-reporter.git
synced 2025-12-17 06:47:09 +01:00
feat: support pytest trace back parsing
This commit is contained in:
parent
a007309f5d
commit
7442569c25
5 changed files with 163 additions and 28 deletions
12
__tests__/fixtures/external/pytest/report-tb-short.xml
vendored
Normal file
12
__tests__/fixtures/external/pytest/report-tb-short.xml
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<testsuites>
|
||||||
|
<testsuite name="pytest" errors="0" failures="1" skipped="0" tests="1" time="0.316"
|
||||||
|
timestamp="2023-03-10T11:26:51.659606" hostname="c29b94e3532a">
|
||||||
|
<testcase classname="product_changes.tests.first_test.MyTestCase" name="test_something" time="0.075">
|
||||||
|
<failure message="assert False">mnt/extra-addons/product_changes/tests/first_test.py:6: in test_something
|
||||||
|
assert False
|
||||||
|
E assert False
|
||||||
|
</failure>
|
||||||
|
</testcase>
|
||||||
|
</testsuite>
|
||||||
|
</testsuites>
|
||||||
|
|
@ -21,4 +21,24 @@ describe('pytest-junit tests', () => {
|
||||||
expect(result.tests).toBe(1)
|
expect(result.tests).toBe(1)
|
||||||
expect(result.result).toBe('success')
|
expect(result.result).toBe('success')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('test failure with trace back', async () => {
|
||||||
|
const fixturePath = path.join(__dirname, 'fixtures', 'external', 'pytest', 'report-tb-short.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('failed')
|
||||||
|
expect(result.failedSuites[0].failedGroups[0].failedTests[0].error).toMatchObject({
|
||||||
|
line: 6,
|
||||||
|
message: 'assert False'
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
112
dist/index.js
generated
vendored
112
dist/index.js
generated
vendored
|
|
@ -264,6 +264,7 @@ const dart_json_parser_1 = __nccwpck_require__(4528);
|
||||||
const dotnet_trx_parser_1 = __nccwpck_require__(2664);
|
const dotnet_trx_parser_1 = __nccwpck_require__(2664);
|
||||||
const java_junit_parser_1 = __nccwpck_require__(676);
|
const java_junit_parser_1 = __nccwpck_require__(676);
|
||||||
const jest_junit_parser_1 = __nccwpck_require__(1113);
|
const jest_junit_parser_1 = __nccwpck_require__(1113);
|
||||||
|
const pytest_junit_parser_1 = __nccwpck_require__(2842);
|
||||||
const mocha_json_parser_1 = __nccwpck_require__(6043);
|
const mocha_json_parser_1 = __nccwpck_require__(6043);
|
||||||
const path_utils_1 = __nccwpck_require__(4070);
|
const path_utils_1 = __nccwpck_require__(4070);
|
||||||
const github_utils_1 = __nccwpck_require__(3522);
|
const github_utils_1 = __nccwpck_require__(3522);
|
||||||
|
|
@ -426,6 +427,8 @@ class TestReporter {
|
||||||
return new java_junit_parser_1.JavaJunitParser(options);
|
return new java_junit_parser_1.JavaJunitParser(options);
|
||||||
case 'jest-junit':
|
case 'jest-junit':
|
||||||
return new jest_junit_parser_1.JestJunitParser(options);
|
return new jest_junit_parser_1.JestJunitParser(options);
|
||||||
|
case 'pytest-junit':
|
||||||
|
return new pytest_junit_parser_1.PytestJunitParser(options);
|
||||||
case 'mocha-json':
|
case 'mocha-json':
|
||||||
return new mocha_json_parser_1.MochaJsonParser(options);
|
return new mocha_json_parser_1.MochaJsonParser(options);
|
||||||
default:
|
default:
|
||||||
|
|
@ -1392,6 +1395,115 @@ class MochaJsonParser {
|
||||||
exports.MochaJsonParser = MochaJsonParser;
|
exports.MochaJsonParser = MochaJsonParser;
|
||||||
|
|
||||||
|
|
||||||
|
/***/ }),
|
||||||
|
|
||||||
|
/***/ 2842:
|
||||||
|
/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) {
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||||
|
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||||
|
return new (P || (P = Promise))(function (resolve, reject) {
|
||||||
|
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||||
|
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||||
|
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||||
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||||
|
});
|
||||||
|
};
|
||||||
|
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
||||||
|
exports.PytestJunitParser = void 0;
|
||||||
|
const xml2js_1 = __nccwpck_require__(6189);
|
||||||
|
const test_results_1 = __nccwpck_require__(2768);
|
||||||
|
class PytestJunitParser {
|
||||||
|
constructor(options) {
|
||||||
|
this.options = options;
|
||||||
|
}
|
||||||
|
parse(path, content) {
|
||||||
|
return __awaiter(this, void 0, void 0, function* () {
|
||||||
|
const ju = yield this.getJunitReport(path, content);
|
||||||
|
return this.getTestRunResult(path, ju);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
getJunitReport(path, content) {
|
||||||
|
return __awaiter(this, void 0, void 0, function* () {
|
||||||
|
try {
|
||||||
|
return (yield (0, xml2js_1.parseStringPromise)(content));
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
throw new Error(`Invalid XML at ${path}\n\n${e}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
getTestRunResult(path, junit) {
|
||||||
|
const suites = junit.testsuites.testsuite === undefined
|
||||||
|
? []
|
||||||
|
: junit.testsuites.testsuite.map(ts => {
|
||||||
|
const name = ts.$.name.trim();
|
||||||
|
const time = parseFloat(ts.$.time) * 1000;
|
||||||
|
return new test_results_1.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 test_results_1.TestRunResult(path, suites, time);
|
||||||
|
}
|
||||||
|
getGroups(suite) {
|
||||||
|
if (!suite.testcase) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const groups = [];
|
||||||
|
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 test_results_1.TestCaseResult(name, result, time, error);
|
||||||
|
});
|
||||||
|
return new test_results_1.TestGroupResult(grp.describe, tests);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
getTestCaseResult(test) {
|
||||||
|
if (test.failure)
|
||||||
|
return 'failed';
|
||||||
|
if (test.skipped)
|
||||||
|
return 'skipped';
|
||||||
|
return 'success';
|
||||||
|
}
|
||||||
|
getTestCaseError(tc) {
|
||||||
|
if (!this.options.parseErrors || !tc.failure) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const failure = tc.failure[0];
|
||||||
|
const details = typeof failure === 'object' ? failure._ : failure;
|
||||||
|
return Object.assign(Object.assign({}, this.errorSource(details)), { details });
|
||||||
|
}
|
||||||
|
errorSource(details) {
|
||||||
|
const lines = details.split('\n').map(line => line.trim());
|
||||||
|
const [path, pos] = lines[0].split(':');
|
||||||
|
const line = Number.parseInt(pos);
|
||||||
|
if (path && Number.isFinite(line)) {
|
||||||
|
return {
|
||||||
|
path,
|
||||||
|
line,
|
||||||
|
message: lines[1]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
exports.PytestJunitParser = PytestJunitParser;
|
||||||
|
|
||||||
|
|
||||||
/***/ }),
|
/***/ }),
|
||||||
|
|
||||||
/***/ 5867:
|
/***/ 5867:
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,6 @@ import {ParseOptions, TestParser} from '../../test-parser'
|
||||||
import {parseStringPromise} from 'xml2js'
|
import {parseStringPromise} from 'xml2js'
|
||||||
|
|
||||||
import {JunitReport, TestCase, TestSuite} from './pytest-junit-types'
|
import {JunitReport, TestCase, TestSuite} from './pytest-junit-types'
|
||||||
import {getExceptionSource} from '../../utils/node-utils'
|
|
||||||
import {getBasePath, normalizeFilePath} from '../../utils/path-utils'
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
TestExecutionResult,
|
TestExecutionResult,
|
||||||
|
|
@ -88,37 +86,28 @@ export class PytestJunitParser implements TestParser {
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
const details = tc.failure[0]
|
const failure = tc.failure[0]
|
||||||
let path
|
const details = typeof failure === 'object' ? failure._ : failure
|
||||||
let line
|
|
||||||
|
|
||||||
const src = getExceptionSource(details, this.options.trackedFiles, file => this.getRelativePath(file))
|
|
||||||
if (src) {
|
|
||||||
path = src.path
|
|
||||||
line = src.line
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
path,
|
...this.errorSource(details),
|
||||||
line,
|
|
||||||
details
|
details
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private getRelativePath(path: string): string {
|
private errorSource(details: string): {path: string; line: number; message: string} | undefined {
|
||||||
path = normalizeFilePath(path)
|
const lines = details.split('\n').map(line => line.trim())
|
||||||
const workDir = this.getWorkDir(path)
|
const [path, pos] = lines[0].split(':')
|
||||||
if (workDir !== undefined && path.startsWith(workDir)) {
|
const line = Number.parseInt(pos)
|
||||||
path = path.substr(workDir.length)
|
|
||||||
|
if (path && Number.isFinite(line)) {
|
||||||
|
return {
|
||||||
|
path,
|
||||||
|
line,
|
||||||
|
message: lines[1]
|
||||||
}
|
}
|
||||||
return path
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private getWorkDir(path: string): string | undefined {
|
return undefined
|
||||||
return (
|
|
||||||
this.options.workDir ??
|
|
||||||
this.assumedWorkDir ??
|
|
||||||
(this.assumedWorkDir = getBasePath(path, this.options.trackedFiles))
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import {Failure} from '../java-junit/java-junit-types'
|
||||||
|
|
||||||
export interface JunitReport {
|
export interface JunitReport {
|
||||||
testsuites: TestSuites
|
testsuites: TestSuites
|
||||||
}
|
}
|
||||||
|
|
@ -29,6 +31,6 @@ export interface TestCase {
|
||||||
name: string
|
name: string
|
||||||
time: string
|
time: string
|
||||||
}
|
}
|
||||||
failure?: string[]
|
failure?: string | Failure[]
|
||||||
skipped?: string[]
|
skipped?: string[]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue