Skip to content

Commit a2544d1

Browse files
authored
Correct unittest discovery for n-depth test source trees (#2066)
* Include full relative namespace to each test selected - for file-only test select - for suite-only test select
1 parent b769f55 commit a2544d1

File tree

9 files changed

+290
-37
lines changed

9 files changed

+290
-37
lines changed

news/2 Fixes/2044.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Store testId for files & suites during unittest discovery

pythonFiles/PythonTools/visualstudio_py_testlauncher.py

Lines changed: 21 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
# Python Tools for Visual Studio
22
# Copyright(c) Microsoft Corporation
33
# All rights reserved.
4-
#
4+
#
55
# Licensed under the Apache License, Version 2.0 (the License); you may not use
66
# this file except in compliance with the License. You may obtain a copy of the
77
# License at http://www.apache.org/licenses/LICENSE-2.0
8-
#
8+
#
99
# THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS
1010
# OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY
1111
# IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
1212
# MERCHANTABLITY OR NON-INFRINGEMENT.
13-
#
13+
#
1414
# See the Apache Version 2.0 License for specific language governing
1515
# permissions and limitations under the License.
1616

@@ -43,11 +43,11 @@ def __init__(self, old_out, is_stdout):
4343
def flush(self):
4444
if self.old_out:
4545
self.old_out.flush()
46-
46+
4747
def writelines(self, lines):
4848
for line in lines:
4949
self.write(line)
50-
50+
5151
@property
5252
def encoding(self):
5353
return 'utf8'
@@ -58,13 +58,13 @@ def write(self, value):
5858
self.old_out.write(value)
5959
# flush immediately, else things go wonky and out of order
6060
self.flush()
61-
61+
6262
def isatty(self):
6363
return True
6464

6565
def next(self):
6666
pass
67-
67+
6868
@property
6969
def name(self):
7070
if self.is_stdout:
@@ -84,7 +84,7 @@ def write(self, data):
8484
_channel.send_event('stdout' if self.is_stdout else 'stderr', content=data)
8585
self.buffer.write(data)
8686

87-
def flush(self):
87+
def flush(self):
8888
self.buffer.flush()
8989

9090
def truncate(self, pos = None):
@@ -137,7 +137,7 @@ def startTest(self, test):
137137
super(VsTestResult, self).startTest(test)
138138
if _channel is not None:
139139
_channel.send_event(
140-
name='start',
140+
name='start',
141141
test = test.id()
142142
)
143143

@@ -177,7 +177,7 @@ def sendResult(self, test, outcome, trace = None):
177177
tb = ''.join(formatted)
178178
message = str(trace[1])
179179
_channel.send_event(
180-
name='result',
180+
name='result',
181181
outcome=outcome,
182182
traceback = tb,
183183
message = message,
@@ -196,7 +196,7 @@ def stopTests():
196196
class ExitCommand(Exception):
197197
pass
198198

199-
def signal_handler(signal, frame):
199+
def signal_handler(signal, frame):
200200
raise ExitCommand()
201201

202202
def main():
@@ -223,7 +223,7 @@ def main():
223223

224224
if opts.debug:
225225
from ptvsd.visualstudio_py_debugger import DONT_DEBUG, DEBUG_ENTRYPOINTS, get_code
226-
226+
227227
sys.path[0] = os.getcwd()
228228
if opts.result_port:
229229
try:
@@ -243,7 +243,7 @@ def main():
243243

244244
pass
245245
elif opts.mixed_mode:
246-
# For mixed-mode attach, there's no ptvsd and hence no wait_for_attach(),
246+
# For mixed-mode attach, there's no ptvsd and hence no wait_for_attach(),
247247
# so we have to use Win32 API in a loop to do the same thing.
248248
from time import sleep
249249
from ctypes import windll, c_char
@@ -278,43 +278,44 @@ def main():
278278
opts.up = 'test*.py'
279279
tests = unittest.defaultTestLoader.discover(opts.us, opts.up)
280280
else:
281-
# loadTestsFromNames doesn't work well (with duplicate file names or class names)
281+
# loadTestsFromNames doesn't work well (with duplicate file names or class names)
282282
# Easier approach is find the test suite and use that for running
283283
loader = unittest.TestLoader()
284284
# opts.us will be passed in
285285
suites = loader.discover(opts.us, pattern=os.path.basename(opts.testFile))
286286
suite = None
287-
tests = None
287+
tests = None
288288
if opts.tests is None:
289289
# Run everything in the test file
290290
tests = suites
291291
else:
292292
# Run a specific test class or test method
293-
for suite in suites._tests:
294-
for cls in suite._tests:
293+
for test_suite in suites._tests:
294+
for cls in test_suite._tests:
295295
try:
296296
for m in cls._tests:
297297
testId = m.id()
298298
if testId.startswith(opts.tests[0]):
299299
suite = cls
300+
break
300301
if testId == opts.tests[0]:
301302
tests = m
302303
break
303304
except Exception as err:
304-
errorMessage = traceback.format_exception()
305+
errorMessage = traceback.format_exception()
305306
pass
306307
if tests is None:
307308
tests = suite
308309
if tests is None and suite is None:
309310
_channel.send_event(
310-
name='error',
311+
name='error',
311312
outcome='',
312313
traceback = '',
313314
message = 'Failed to identify the test',
314315
test = ''
315316
)
316317
if opts.uvInt is None:
317-
opts.uvInt = 0
318+
opts.uvInt = 0
318319
if opts.uf is not None:
319320
runner = unittest.TextTestRunner(verbosity=opts.uvInt, resultclass=VsTestResult, failfast=True)
320321
else:

src/client/unittests/common/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ export type TestRunOptions = {
2424
debug?: boolean;
2525
};
2626

27+
export type UnitTestParserOptions = TestDiscoveryOptions & { startDirectory: string };
28+
2729
export type TestFolder = TestResult & {
2830
name: string;
2931
testFiles: TestFile[];

src/client/unittests/main.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ export class UnitTestManagementService implements IUnitTestManagementService, Di
9191
if (this.testResultDisplay) {
9292
this.testResultDisplay.enabled = false;
9393
}
94+
// tslint:disable-next-line:no-suspicious-comment
9495
// TODO: Why are we disposing, what happens when tests are enabled.
9596
if (this.workspaceTestManagerService) {
9697
this.workspaceTestManagerService.dispose();

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

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33

44
import { inject, injectable } from 'inversify';
55
import * as path from 'path';
6-
import { ITestsHelper, ITestsParser, TestDiscoveryOptions, TestFile, TestFunction, Tests, TestStatus } from '../../common/types';
7-
8-
type UnitTestParserOptions = TestDiscoveryOptions & { startDirectory: string };
6+
import { ITestsHelper, ITestsParser, TestFile,
7+
TestFunction, Tests, TestStatus,
8+
UnitTestParserOptions } from '../../common/types';
99

1010
@injectable()
1111
export class TestsParser implements ITestsParser {
@@ -60,6 +60,7 @@ export class TestsParser implements ITestsParser {
6060
const paths = testIdParts.slice(0, testIdParts.length - 2);
6161
const filePath = `${path.join(rootDirectory, ...paths)}.py`;
6262
const functionName = testIdParts.pop()!;
63+
const suiteToRun = testIdParts.join('.');
6364
const className = testIdParts.pop()!;
6465

6566
// Check if we already have this test file
@@ -70,25 +71,25 @@ export class TestsParser implements ITestsParser {
7071
fullPath: filePath,
7172
functions: [],
7273
suites: [],
73-
nameToRun: `${className}.${functionName}`,
74+
nameToRun: `${suiteToRun}.${functionName}`,
7475
xmlName: '',
7576
status: TestStatus.Idle,
7677
time: 0
7778
};
7879
testFiles.push(testFile);
7980
}
8081

81-
// Check if we already have this test file
82-
const classNameToRun = className;
83-
let testSuite = testFile.suites.find(cls => cls.nameToRun === classNameToRun);
82+
// Check if we already have this suite
83+
// nameToRun = testId - method name
84+
let testSuite = testFile.suites.find(cls => cls.nameToRun === suiteToRun);
8485
if (!testSuite) {
8586
testSuite = {
8687
name: className,
8788
functions: [],
8889
suites: [],
8990
isUnitTest: true,
9091
isInstance: false,
91-
nameToRun: `${path.parse(filePath).name}.${classNameToRun}`,
92+
nameToRun: suiteToRun,
9293
xmlName: '',
9394
status: TestStatus.Idle,
9495
time: 0

src/client/unittests/unittest/socketServer.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export class UnitTestSocketServer extends EventEmitter implements IUnitTestSocke
2929
this.server = undefined;
3030
}
3131
}
32-
public start(options: { port?: number, host?: string } = { port: 0, host: 'localhost' }): Promise<number> {
32+
public start(options: { port?: number; host?: string } = { port: 0, host: 'localhost' }): Promise<number> {
3333
this.startedDef = createDeferred<number>();
3434
this.server = net.createServer(this.connectionListener.bind(this));
3535
this.server!.maxConnections = MaxConnections;

src/test/unittests/unittest/unittest.discovery.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ suite('Unit Tests - unittest - discovery with mocked process output', () => {
8787
assert.equal(tests.testFiles.length, 1, 'Incorrect number of test files');
8888
assert.equal(tests.testFunctions.length, 3, 'Incorrect number of test functions');
8989
assert.equal(tests.testSuites.length, 1, 'Incorrect number of test suites');
90-
assert.equal(tests.testFiles.some(t => t.name === 'test_one.py' && t.nameToRun === 'Test_test1.test_A'), true, 'Test File not found');
90+
assert.equal(tests.testFiles.some(t => t.name === 'test_one.py' && t.nameToRun === 'test_one.Test_test1.test_A'), true, 'Test File not found');
9191
});
9292

9393
test('Discover Tests', async () => {
@@ -110,8 +110,8 @@ suite('Unit Tests - unittest - discovery with mocked process output', () => {
110110
assert.equal(tests.testFiles.length, 2, 'Incorrect number of test files');
111111
assert.equal(tests.testFunctions.length, 9, 'Incorrect number of test functions');
112112
assert.equal(tests.testSuites.length, 3, 'Incorrect number of test suites');
113-
assert.equal(tests.testFiles.some(t => t.name === 'test_unittest_one.py' && t.nameToRun === 'Test_test1.test_A'), true, 'Test File not found');
114-
assert.equal(tests.testFiles.some(t => t.name === 'test_unittest_two.py' && t.nameToRun === 'Test_test2.test_A2'), true, 'Test File not found');
113+
assert.equal(tests.testFiles.some(t => t.name === 'test_unittest_one.py' && t.nameToRun === 'test_unittest_one.Test_test1.test_A'), true, 'Test File not found');
114+
assert.equal(tests.testFiles.some(t => t.name === 'test_unittest_two.py' && t.nameToRun === 'test_unittest_two.Test_test2.test_A2'), true, 'Test File not found');
115115
});
116116

117117
test('Discover Tests (pattern = *_test_*.py)', async () => {
@@ -127,7 +127,7 @@ suite('Unit Tests - unittest - discovery with mocked process output', () => {
127127
assert.equal(tests.testFiles.length, 1, 'Incorrect number of test files');
128128
assert.equal(tests.testFunctions.length, 2, 'Incorrect number of test functions');
129129
assert.equal(tests.testSuites.length, 1, 'Incorrect number of test suites');
130-
assert.equal(tests.testFiles.some(t => t.name === 'unittest_three_test.py' && t.nameToRun === 'Test_test3.test_A'), true, 'Test File not found');
130+
assert.equal(tests.testFiles.some(t => t.name === 'unittest_three_test.py' && t.nameToRun === 'unittest_three_test.Test_test3.test_A'), true, 'Test File not found');
131131
});
132132

133133
test('Setting cwd should return tests', async () => {

0 commit comments

Comments
 (0)