forked from DonJayamanne/pythonVSCode
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
/
xUnitParser.ts
183 lines (166 loc) · 6.24 KB
/
xUnitParser.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
import { inject, injectable } from 'inversify';
import { IFileSystem } from '../../common/platform/types';
import { FlattenedTestFunction, IXUnitParser, TestFunction, TestResult, Tests, TestStatus, TestSummary } from './types';
type TestSuiteResult = {
$: {
errors: string;
failures: string;
name: string;
skips: string;
skip: string;
tests: string;
time: string;
};
testcase: TestCaseResult[];
};
type TestCaseResult = {
$: {
classname: string;
file: string;
line: string;
name: string;
time: string;
};
failure: {
_: string;
$: { message: string; type: string };
}[];
error: {
_: string;
$: { message: string; type: string };
}[];
skipped: {
_: string;
$: { message: string; type: string };
}[];
};
// tslint:disable-next-line:no-any
function getSafeInt(value: string, defaultValue: any = 0): number {
const num = parseInt(value, 10);
if (isNaN(num)) {
return defaultValue;
}
return num;
}
@injectable()
export class XUnitParser implements IXUnitParser {
constructor(@inject(IFileSystem) private readonly fs: IFileSystem) {}
// Update "tests" with the results parsed from the given file.
public async updateResultsFromXmlLogFile(tests: Tests, outputXmlFile: string) {
const data = await this.fs.readFile(outputXmlFile);
const parserResult = await parseXML(data);
const junitResults = getJunitResults(parserResult);
if (junitResults) {
updateTests(tests, junitResults);
}
}
}
// An async wrapper around xml2js.parseString().
// tslint:disable-next-line:no-any
async function parseXML(data: string): Promise<any> {
const xml2js = await import('xml2js');
// tslint:disable-next-line:no-any
return new Promise<any>((resolve, reject) => {
// tslint:disable-next-line:no-any
xml2js.parseString(data, (error: Error, result: any) => {
if (error) {
return reject(error);
}
return resolve(result);
});
});
}
// Return the actual test results from the given data.
// tslint:disable-next-line:no-any
function getJunitResults(parserResult: any): TestSuiteResult | undefined {
// This is the newer JUnit XML format (e.g. pytest 5.1 and later).
const fullResults = parserResult as { testsuites: { testsuite: TestSuiteResult[] } };
if (!fullResults.testsuites) {
return (parserResult as { testsuite: TestSuiteResult }).testsuite;
}
const junitSuites = fullResults.testsuites.testsuite;
if (!Array.isArray(junitSuites)) {
throw Error('bad JUnit XML data');
}
if (junitSuites.length === 0) {
return;
}
if (junitSuites.length > 1) {
throw Error('got multiple XML results');
}
return junitSuites[0];
}
// Update "tests" with the given results.
function updateTests(tests: Tests, testSuiteResult: TestSuiteResult) {
updateSummary(tests.summary, testSuiteResult);
if (!Array.isArray(testSuiteResult.testcase)) {
return;
}
// Update the results for each test.
// Previously unknown tests are ignored.
testSuiteResult.testcase.forEach((testcase: TestCaseResult) => {
const testFunc = findTestFunction(tests.testFunctions, testcase.$.classname, testcase.$.name);
if (testFunc) {
updateResultInfo(testFunc, testcase);
updateResultStatus(testFunc, testcase);
} else {
// Possible we're dealing with nosetests, where the file name isn't returned to us
// When dealing with nose tests
// It is possible to have a test file named x in two separate test sub directories and have same functions/classes
// And unforutnately xunit log doesn't ouput the filename
// result = tests.testFunctions.find(fn => fn.testFunction.name === testcase.$.name &&
// fn.parentTestSuite && fn.parentTestSuite.name === testcase.$.classname);
// Look for failed file test
const fileTest = testcase.$.file && tests.testFiles.find((file) => file.nameToRun === testcase.$.file);
if (fileTest && testcase.error) {
updateResultStatus(fileTest, testcase);
}
}
});
}
// Update the summary with the information in the given results.
function updateSummary(summary: TestSummary, testSuiteResult: TestSuiteResult) {
summary.errors = getSafeInt(testSuiteResult.$.errors);
summary.failures = getSafeInt(testSuiteResult.$.failures);
summary.skipped = getSafeInt(testSuiteResult.$.skips ? testSuiteResult.$.skips : testSuiteResult.$.skip);
const testCount = getSafeInt(testSuiteResult.$.tests);
summary.passed = testCount - summary.failures - summary.skipped - summary.errors;
}
function findTestFunction(
candidates: FlattenedTestFunction[],
className: string,
funcName: string
): TestFunction | undefined {
const xmlClassName = className.replace(/\(\)/g, '').replace(/\.\./g, '.').replace(/\.\./g, '.').replace(/\.+$/, '');
const flattened = candidates.find((fn) => fn.xmlClassName === xmlClassName && fn.testFunction.name === funcName);
if (!flattened) {
return;
}
return flattened.testFunction;
}
function updateResultInfo(result: TestResult, testCase: TestCaseResult) {
result.file = testCase.$.file;
result.line = getSafeInt(testCase.$.line, null);
result.time = parseFloat(testCase.$.time);
}
function updateResultStatus(result: TestResult, testCase: TestCaseResult) {
if (testCase.error) {
result.status = TestStatus.Error;
result.passed = false;
result.message = testCase.error[0].$.message;
result.traceback = testCase.error[0]._;
} else if (testCase.failure) {
result.status = TestStatus.Fail;
result.passed = false;
result.message = testCase.failure[0].$.message;
result.traceback = testCase.failure[0]._;
} else if (testCase.skipped) {
result.status = TestStatus.Skipped;
result.passed = undefined;
result.message = testCase.skipped[0].$.message;
result.traceback = '';
} else {
result.status = TestStatus.Pass;
result.passed = true;
}
}