Skip to content

Commit aaf9c6c

Browse files
authored
Update pytest parser to handle pytest 4.1 (microsoft#4098)
- Handles stdout produced by pytest in the 4.0 and 4.1 series - update test labels to match pytest versions - add test data specific to 4.1+
1 parent f4320fd commit aaf9c6c

File tree

3 files changed

+774
-63
lines changed

3 files changed

+774
-63
lines changed

news/2 Fixes/4099.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Handle stdout changes with updates to pytest 4.1.x series (without breaking 4.0.x series parsing).

src/client/unittests/pytest/services/parserService.ts

Lines changed: 58 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,10 @@
44
import { inject, injectable } from 'inversify';
55
import * as os from 'os';
66
import * as path from 'path';
7+
import '../../../common/extensions';
78
import { convertFileToPackage, extractBetweenDelimiters } from '../../common/testUtils';
89
import { ITestsHelper, ITestsParser, ParserOptions, TestFile, TestFunction, Tests, TestSuite } from '../../common/types';
910

10-
const DELIMITER = '\'';
11-
1211
@injectable()
1312
export class TestsParser implements ITestsParser {
1413

@@ -38,15 +37,15 @@ export class TestsParser implements ITestsParser {
3837

3938
const trimmedLine: string = line.trim();
4039

41-
if (trimmedLine.startsWith('<Package \'')) {
40+
if (trimmedLine.startsWith('<Package ')) {
4241
// Process the previous lines.
4342
this.parsePyTestModuleCollectionResult(options.cwd, logOutputLines, testFiles, parentNodes, packagePrefix);
4443
logOutputLines = [''];
4544

4645
packagePrefix = this.extractPackageName(trimmedLine, options.cwd);
4746
}
4847

49-
if (trimmedLine.startsWith('<Module \'') || index === lines.length - 1) {
48+
if (trimmedLine.startsWith('<Module ') || index === lines.length - 1) {
5049
// Process the previous lines.
5150
this.parsePyTestModuleCollectionResult(options.cwd, logOutputLines, testFiles, parentNodes, packagePrefix);
5251
logOutputLines = [''];
@@ -120,7 +119,7 @@ export class TestsParser implements ITestsParser {
120119
* @param rootDir Value is pytest's `--rootdir=` parameter.
121120
*/
122121
private extractPackageName(packageLine: string, rootDir: string): string {
123-
const packagePath: string = extractBetweenDelimiters(packageLine, DELIMITER, DELIMITER);
122+
const packagePath: string = extractBetweenDelimiters(packageLine, '<Package ', '>').trimQuotes();
124123
let packageName: string = path.normalize(packagePath);
125124
const tmpRoot: string = path.normalize(rootDir);
126125

@@ -149,10 +148,11 @@ export class TestsParser implements ITestsParser {
149148

150149
lines.forEach(line => {
151150
const trimmedLine = line.trim();
152-
let name: string = extractBetweenDelimiters(trimmedLine, DELIMITER, DELIMITER);
151+
let name: string = '';
153152
const indent = line.indexOf('<');
154153

155-
if (trimmedLine.startsWith('<Module \'')) {
154+
if (trimmedLine.startsWith('<Module ')) {
155+
name = extractBetweenDelimiters(trimmedLine, '<Module ', '>').trimQuotes();
156156
if (packagePrefix && packagePrefix.length > 0) {
157157
name = packagePrefix.concat('/', name);
158158
}
@@ -169,24 +169,39 @@ export class TestsParser implements ITestsParser {
169169

170170
const parentNode = this.findParentOfCurrentItem(indent, parentNodes);
171171

172-
if (parentNode && trimmedLine.startsWith('<Class \'') || trimmedLine.startsWith('<UnitTestCase \'')) {
173-
const isUnitTest = trimmedLine.startsWith('<UnitTestCase \'');
172+
if (parentNode && trimmedLine.startsWith('<Class ') || trimmedLine.startsWith('<UnitTestCase ')) {
173+
const isUnitTest = trimmedLine.startsWith('<UnitTestCase ');
174+
if (isUnitTest) {
175+
name = extractBetweenDelimiters(trimmedLine, '<UnitTestCase ', '>');
176+
} else {
177+
name = extractBetweenDelimiters(trimmedLine, '<Class ', '>');
178+
}
179+
name = name.trimQuotes();
180+
174181
const rawName = `${parentNode!.item.nameToRun}::${name}`;
175182
const xmlName = `${parentNode!.item.xmlName}.${name}`;
176183
const testSuite: TestSuite = { name: name, nameToRun: rawName, functions: [], suites: [], isUnitTest: isUnitTest, isInstance: false, xmlName: xmlName, time: 0 };
177184
parentNode!.item.suites.push(testSuite);
178185
parentNodes.push({ indent: indent, item: testSuite });
179186
return;
180187
}
181-
if (parentNode && trimmedLine.startsWith('<Instance \'')) {
188+
if (parentNode && trimmedLine.startsWith('<Instance ')) {
189+
name = extractBetweenDelimiters(trimmedLine, '<Instance ', '>').trimQuotes();
182190
// tslint:disable-next-line:prefer-type-cast
183191
const suite = (parentNode!.item as TestSuite);
184192
// suite.rawName = suite.rawName + '::()';
185193
// suite.xmlName = suite.xmlName + '.()';
186194
suite.isInstance = true;
187195
return;
188196
}
189-
if (parentNode && trimmedLine.startsWith('<TestCaseFunction \'') || trimmedLine.startsWith('<Function \'')) {
197+
if (parentNode && trimmedLine.startsWith('<TestCaseFunction ') || trimmedLine.startsWith('<Function ')) {
198+
if (trimmedLine.startsWith('<Function ')) {
199+
name = extractBetweenDelimiters(trimmedLine, '<Function ', '>');
200+
} else {
201+
name = extractBetweenDelimiters(trimmedLine, '<TestCaseFunction ', '>');
202+
}
203+
name = name.trimQuotes();
204+
190205
const rawName = `${parentNode!.item.nameToRun}::${name}`;
191206
const fn: TestFunction = { name: name, nameToRun: rawName, time: 0 };
192207
parentNode!.item.functions.push(fn);
@@ -209,37 +224,37 @@ export class TestsParser implements ITestsParser {
209224
}
210225
}
211226

212-
/* Sample output from pytest --collect-only
213-
<Module 'test_another.py'>
214-
<Class 'Test_CheckMyApp'>
215-
<Instance '()'>
216-
<Function 'test_simple_check'>
217-
<Function 'test_complex_check'>
218-
<Module 'test_one.py'>
219-
<UnitTestCase 'Test_test1'>
220-
<TestCaseFunction 'test_A'>
221-
<TestCaseFunction 'test_B'>
222-
<Module 'test_two.py'>
223-
<UnitTestCase 'Test_test1'>
224-
<TestCaseFunction 'test_A2'>
225-
<TestCaseFunction 'test_B2'>
226-
<Module 'testPasswords/test_Pwd.py'>
227-
<UnitTestCase 'Test_Pwd'>
228-
<TestCaseFunction 'test_APwd'>
229-
<TestCaseFunction 'test_BPwd'>
230-
<Module 'testPasswords/test_multi.py'>
231-
<Class 'Test_CheckMyApp'>
227+
/* Sample output from pytest --collect-only
228+
<Module 'test_another.py'>
229+
<Class 'Test_CheckMyApp'>
230+
<Instance '()'>
231+
<Function 'test_simple_check'>
232+
<Function 'test_complex_check'>
233+
<Module 'test_one.py'>
234+
<UnitTestCase 'Test_test1'>
235+
<TestCaseFunction 'test_A'>
236+
<TestCaseFunction 'test_B'>
237+
<Module 'test_two.py'>
238+
<UnitTestCase 'Test_test1'>
239+
<TestCaseFunction 'test_A2'>
240+
<TestCaseFunction 'test_B2'>
241+
<Module 'testPasswords/test_Pwd.py'>
242+
<UnitTestCase 'Test_Pwd'>
243+
<TestCaseFunction 'test_APwd'>
244+
<TestCaseFunction 'test_BPwd'>
245+
<Module 'testPasswords/test_multi.py'>
246+
<Class 'Test_CheckMyApp'>
247+
<Instance '()'>
248+
<Function 'test_simple_check'>
249+
<Function 'test_complex_check'>
250+
<Class 'Test_NestedClassA'>
232251
<Instance '()'>
233-
<Function 'test_simple_check'>
234-
<Function 'test_complex_check'>
235-
<Class 'Test_NestedClassA'>
252+
<Function 'test_nested_class_methodB'>
253+
<Class 'Test_nested_classB_Of_A'>
236254
<Instance '()'>
237-
<Function 'test_nested_class_methodB'>
238-
<Class 'Test_nested_classB_Of_A'>
239-
<Instance '()'>
240-
<Function 'test_d'>
241-
<Function 'test_username'>
242-
<Function 'test_parametrized_username[one]'>
243-
<Function 'test_parametrized_username[two]'>
244-
<Function 'test_parametrized_username[three]'>
245-
*/
255+
<Function 'test_d'>
256+
<Function 'test_username'>
257+
<Function 'test_parametrized_username[one]'>
258+
<Function 'test_parametrized_username[two]'>
259+
<Function 'test_parametrized_username[three]'>
260+
*/

0 commit comments

Comments
 (0)