Skip to content

Commit 3fe7057

Browse files
authored
new inactive discovery logic (#20566)
New pytest code in a currently inactive state.
1 parent bf4091e commit 3fe7057

File tree

6 files changed

+221
-21
lines changed

6 files changed

+221
-21
lines changed

src/client/testing/testController/common/server.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,12 @@ export class PythonTestServer implements ITestServer, Disposable {
7171
return (this.server.address() as net.AddressInfo).port;
7272
}
7373

74+
public createUUID(cwd: string): string {
75+
const uuid = crypto.randomUUID();
76+
this.uuids.set(uuid, cwd);
77+
return uuid;
78+
}
79+
7480
public dispose(): void {
7581
this.server.close();
7682
this._onDataReceived.dispose();
@@ -81,15 +87,13 @@ export class PythonTestServer implements ITestServer, Disposable {
8187
}
8288

8389
async sendCommand(options: TestCommandOptions): Promise<void> {
84-
const uuid = crypto.randomUUID();
90+
const uuid = this.createUUID(options.cwd);
8591
const spawnOptions: SpawnOptions = {
8692
token: options.token,
8793
cwd: options.cwd,
8894
throwOnStdErr: true,
8995
};
9096

91-
this.uuids.set(uuid, options.cwd);
92-
9397
// Create the Python environment in which to execute the command.
9498
const creationOptions: ExecutionFactoryCreateWithEnvironmentOptions = {
9599
allowEnvironmentFetchExceptions: false,

src/client/testing/testController/common/types.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,17 @@ export type TestCommandOptions = {
151151
testIds?: string[];
152152
};
153153

154+
export type TestCommandOptionsPytest = {
155+
workspaceFolder: Uri;
156+
cwd: string;
157+
commandStr: string;
158+
token?: CancellationToken;
159+
outChannel?: OutputChannel;
160+
debugBool?: boolean;
161+
testIds?: string[];
162+
env: { [key: string]: string | undefined };
163+
};
164+
154165
/**
155166
* Interface describing the server that will send test commands to the Python side, and process responses.
156167
*
@@ -161,10 +172,14 @@ export interface ITestServer {
161172
readonly onDataReceived: Event<DataReceivedEvent>;
162173
sendCommand(options: TestCommandOptions): Promise<void>;
163174
serverReady(): Promise<void>;
175+
getPort(): number;
176+
createUUID(cwd: string): string;
164177
}
165178

166179
export interface ITestDiscoveryAdapter {
180+
// ** Uncomment second line and comment out first line to use the new discovery method.
167181
discoverTests(uri: Uri): Promise<DiscoveredTestPayload>;
182+
// discoverTests(uri: Uri, executionFactory: IPythonExecutionFactory): Promise<DiscoveredTestPayload>
168183
}
169184

170185
// interface for execution/runner adapter

src/client/testing/testController/controller.ts

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,10 @@ import {
3939
ITestExecutionAdapter,
4040
} from './common/types';
4141
import { UnittestTestDiscoveryAdapter } from './unittest/testDiscoveryAdapter';
42-
import { WorkspaceTestAdapter } from './workspaceTestAdapter';
4342
import { UnittestTestExecutionAdapter } from './unittest/testExecutionAdapter';
43+
import { PytestTestDiscoveryAdapter } from './pytest/pytestDiscoveryAdapter';
44+
import { PytestTestExecutionAdapter } from './pytest/pytestExecutionAdapter';
45+
import { WorkspaceTestAdapter } from './workspaceTestAdapter';
4446
import { ITestDebugLauncher } from '../common/types';
4547

4648
// Types gymnastics to make sure that sendTriggerTelemetry only accepts the correct types.
@@ -141,7 +143,6 @@ export class PythonTestController implements ITestController, IExtensionSingleAc
141143
});
142144
return this.refreshTestData(undefined, { forceRefresh: true });
143145
};
144-
145146
this.pythonTestServer = new PythonTestServer(this.pythonExecFactory, this.debugLauncher);
146147
}
147148

@@ -161,10 +162,8 @@ export class PythonTestController implements ITestController, IExtensionSingleAc
161162
executionAdapter = new UnittestTestExecutionAdapter(this.pythonTestServer, this.configSettings);
162163
testProvider = UNITTEST_PROVIDER;
163164
} else {
164-
// TODO: PYTEST DISCOVERY ADAPTER
165-
// this is a placeholder for now
166-
discoveryAdapter = new UnittestTestDiscoveryAdapter(this.pythonTestServer, { ...this.configSettings });
167-
executionAdapter = new UnittestTestExecutionAdapter(this.pythonTestServer, this.configSettings);
165+
discoveryAdapter = new PytestTestDiscoveryAdapter(this.pythonTestServer, { ...this.configSettings });
166+
executionAdapter = new PytestTestExecutionAdapter(this.pythonTestServer, this.configSettings);
168167
testProvider = PYTEST_PROVIDER;
169168
}
170169

@@ -224,18 +223,30 @@ export class PythonTestController implements ITestController, IExtensionSingleAc
224223
this.refreshingStartedEvent.fire();
225224
if (uri) {
226225
const settings = this.configSettings.getSettings(uri);
226+
traceVerbose(`Testing: Refreshing test data for ${uri.fsPath}`);
227227
if (settings.testing.pytestEnabled) {
228-
traceVerbose(`Testing: Refreshing test data for ${uri.fsPath}`);
229-
230228
// Ensure we send test telemetry if it gets disabled again
231229
this.sendTestDisabledTelemetry = true;
232-
230+
// ** uncomment ~231 - 241 to NEW new test discovery mechanism
231+
// const workspace = this.workspaceService.getWorkspaceFolder(uri);
232+
// traceVerbose(`Discover tests for workspace name: ${workspace?.name} - uri: ${uri.fsPath}`);
233+
// const testAdapter =
234+
// this.testAdapters.get(uri) || (this.testAdapters.values().next().value as WorkspaceTestAdapter);
235+
// testAdapter.discoverTests(
236+
// this.testController,
237+
// this.refreshCancellation.token,
238+
// this.testAdapters.size > 1,
239+
// this.workspaceService.workspaceFile?.fsPath,
240+
// this.pythonExecFactory,
241+
// );
242+
// uncomment ~243 to use OLD test discovery mechanism
233243
await this.pytest.refreshTestData(this.testController, uri, this.refreshCancellation.token);
234244
} else if (settings.testing.unittestEnabled) {
235-
// TODO: Use new test discovery mechanism
236-
// traceVerbose(`Testing: Refreshing test data for ${uri.fsPath}`);
245+
// ** Ensure we send test telemetry if it gets disabled again
246+
this.sendTestDisabledTelemetry = true;
247+
// uncomment ~248 - 258 to NEW new test discovery mechanism
237248
// const workspace = this.workspaceService.getWorkspaceFolder(uri);
238-
// console.warn(`Discover tests for workspace name: ${workspace?.name} - uri: ${uri.fsPath}`);
249+
// traceVerbose(`Discover tests for workspace name: ${workspace?.name} - uri: ${uri.fsPath}`);
239250
// const testAdapter =
240251
// this.testAdapters.get(uri) || (this.testAdapters.values().next().value as WorkspaceTestAdapter);
241252
// testAdapter.discoverTests(
@@ -244,9 +255,7 @@ export class PythonTestController implements ITestController, IExtensionSingleAc
244255
// this.testAdapters.size > 1,
245256
// this.workspaceService.workspaceFile?.fsPath,
246257
// );
247-
// // Ensure we send test telemetry if it gets disabled again
248-
// this.sendTestDisabledTelemetry = true;
249-
// comment below 229 to run the new way and uncomment above 212 ~ 227
258+
// uncomment ~260 to use OLD test discovery mechanism
250259
await this.unittest.refreshTestData(this.testController, uri, this.refreshCancellation.token);
251260
} else {
252261
if (this.sendTestDisabledTelemetry) {
@@ -375,7 +384,7 @@ export class PythonTestController implements ITestController, IExtensionSingleAc
375384
);
376385
}
377386
if (settings.testing.unittestEnabled) {
378-
// potentially sqeeze in the new exeuction way here?
387+
// potentially squeeze in the new execution way here?
379388
sendTelemetryEvent(EventName.UNITTEST_RUN, undefined, {
380389
tool: 'unittest',
381390
debugging: request.profile?.kind === TestRunProfileKind.Debug,
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
import * as path from 'path';
4+
import { Uri } from 'vscode';
5+
import {
6+
ExecutionFactoryCreateWithEnvironmentOptions,
7+
IPythonExecutionFactory,
8+
SpawnOptions,
9+
} from '../../../common/process/types';
10+
import { IConfigurationService } from '../../../common/types';
11+
import { createDeferred, Deferred } from '../../../common/utils/async';
12+
import { EXTENSION_ROOT_DIR } from '../../../constants';
13+
import { traceVerbose } from '../../../logging';
14+
import { DataReceivedEvent, DiscoveredTestPayload, ITestDiscoveryAdapter, ITestServer } from '../common/types';
15+
16+
/**
17+
* Wrapper class for unittest test discovery. This is where we call `runTestCommand`. #this seems incorrectly copied
18+
*/
19+
export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter {
20+
private deferred: Deferred<DiscoveredTestPayload> | undefined;
21+
22+
private cwd: string | undefined;
23+
24+
constructor(public testServer: ITestServer, public configSettings: IConfigurationService) {
25+
testServer.onDataReceived(this.onDataReceivedHandler, this);
26+
}
27+
28+
public onDataReceivedHandler({ cwd, data }: DataReceivedEvent): void {
29+
if (this.deferred && cwd === this.cwd) {
30+
const testData: DiscoveredTestPayload = JSON.parse(data);
31+
32+
this.deferred.resolve(testData);
33+
this.deferred = undefined;
34+
}
35+
}
36+
37+
// ** Old version of discover tests.
38+
discoverTests(uri: Uri): Promise<DiscoveredTestPayload> {
39+
traceVerbose(uri);
40+
this.deferred = createDeferred<DiscoveredTestPayload>();
41+
return this.deferred.promise;
42+
}
43+
// Uncomment this version of the function discoverTests to use the new discovery method.
44+
// public async discoverTests(uri: Uri, executionFactory: IPythonExecutionFactory): Promise<DiscoveredTestPayload> {
45+
// const settings = this.configSettings.getSettings(uri);
46+
// const { pytestArgs } = settings.testing;
47+
// traceVerbose(pytestArgs);
48+
49+
// this.cwd = uri.fsPath;
50+
// return this.runPytestDiscovery(uri, executionFactory);
51+
// }
52+
53+
async runPytestDiscovery(uri: Uri, executionFactory: IPythonExecutionFactory): Promise<DiscoveredTestPayload> {
54+
if (!this.deferred) {
55+
this.deferred = createDeferred<DiscoveredTestPayload>();
56+
const relativePathToPytest = 'pythonFiles';
57+
const fullPluginPath = path.join(EXTENSION_ROOT_DIR, relativePathToPytest);
58+
const uuid = this.testServer.createUUID(uri.fsPath);
59+
const settings = this.configSettings.getSettings(uri);
60+
const { pytestArgs } = settings.testing;
61+
62+
const pythonPathParts: string[] = process.env.PYTHONPATH?.split(path.delimiter) ?? [];
63+
const pythonPathCommand = [fullPluginPath, ...pythonPathParts].join(path.delimiter);
64+
65+
const spawnOptions: SpawnOptions = {
66+
cwd: uri.fsPath,
67+
throwOnStdErr: true,
68+
extraVariables: {
69+
PYTHONPATH: pythonPathCommand,
70+
TEST_UUID: uuid.toString(),
71+
TEST_PORT: this.testServer.getPort().toString(),
72+
},
73+
};
74+
75+
// Create the Python environment in which to execute the command.
76+
const creationOptions: ExecutionFactoryCreateWithEnvironmentOptions = {
77+
allowEnvironmentFetchExceptions: false,
78+
resource: uri,
79+
};
80+
const execService = await executionFactory.createActivatedEnvironment(creationOptions);
81+
82+
try {
83+
execService.exec(
84+
['-m', 'pytest', '-p', 'vscode_pytest', '--collect-only'].concat(pytestArgs),
85+
spawnOptions,
86+
);
87+
} catch (ex) {
88+
console.error(ex);
89+
}
90+
}
91+
return this.deferred.promise;
92+
}
93+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
import * as path from 'path';
5+
import { Uri } from 'vscode';
6+
import { IConfigurationService } from '../../../common/types';
7+
import { createDeferred, Deferred } from '../../../common/utils/async';
8+
import { EXTENSION_ROOT_DIR } from '../../../constants';
9+
import {
10+
DataReceivedEvent,
11+
ExecutionTestPayload,
12+
ITestExecutionAdapter,
13+
ITestServer,
14+
TestCommandOptions,
15+
TestExecutionCommand,
16+
} from '../common/types';
17+
18+
/**
19+
* Wrapper Class for unittest test execution. This is where we call `runTestCommand`?
20+
*/
21+
22+
export class PytestTestExecutionAdapter implements ITestExecutionAdapter {
23+
private deferred: Deferred<ExecutionTestPayload> | undefined;
24+
25+
private cwd: string | undefined;
26+
27+
constructor(public testServer: ITestServer, public configSettings: IConfigurationService) {
28+
testServer.onDataReceived(this.onDataReceivedHandler, this);
29+
}
30+
31+
public onDataReceivedHandler({ cwd, data }: DataReceivedEvent): void {
32+
if (this.deferred && cwd === this.cwd) {
33+
const testData: ExecutionTestPayload = JSON.parse(data);
34+
35+
this.deferred.resolve(testData);
36+
this.deferred = undefined;
37+
}
38+
}
39+
40+
public async runTests(uri: Uri, testIds: string[], debugBool?: boolean): Promise<ExecutionTestPayload> {
41+
if (!this.deferred) {
42+
const settings = this.configSettings.getSettings(uri);
43+
const { unittestArgs } = settings.testing;
44+
45+
const command = buildExecutionCommand(unittestArgs);
46+
this.cwd = uri.fsPath;
47+
48+
const options: TestCommandOptions = {
49+
workspaceFolder: uri,
50+
command,
51+
cwd: this.cwd,
52+
debugBool,
53+
testIds,
54+
};
55+
56+
this.deferred = createDeferred<ExecutionTestPayload>();
57+
58+
// send test command to server
59+
// server fire onDataReceived event once it gets response
60+
this.testServer.sendCommand(options);
61+
}
62+
return this.deferred.promise;
63+
}
64+
}
65+
66+
function buildExecutionCommand(args: string[]): TestExecutionCommand {
67+
const executionScript = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'unittestadapter', 'execution.py');
68+
69+
return {
70+
script: executionScript,
71+
args: ['--udiscovery', ...args],
72+
};
73+
}

src/client/testing/testController/workspaceTestAdapter.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,7 @@ export class WorkspaceTestAdapter {
196196
return Promise.resolve();
197197
}
198198

199+
// add `executionFactory?: IPythonExecutionFactory,` to the function for new pytest method
199200
public async discoverTests(
200201
testController: TestController,
201202
token?: CancellationToken,
@@ -216,8 +217,13 @@ export class WorkspaceTestAdapter {
216217

217218
let rawTestData;
218219
try {
220+
// ** First line is old way, section with if statement below is new way.
219221
rawTestData = await this.discoveryAdapter.discoverTests(this.workspaceUri);
220-
222+
// if (executionFactory !== undefined) {
223+
// rawTestData = await this.discoveryAdapter.discoverTests(this.workspaceUri, executionFactory);
224+
// } else {
225+
// traceVerbose('executionFactory is undefined');
226+
// }
221227
deferred.resolve();
222228
} catch (ex) {
223229
sendTelemetryEvent(EventName.UNITTEST_DISCOVERY_DONE, undefined, { tool: this.testProvider, failed: true });
@@ -352,6 +358,7 @@ function populateTestTree(
352358
testItem.canResolveChildren = false;
353359
testItem.range = range;
354360
testItem.tags = [RunTestTag, DebugTestTag];
361+
355362
testRoot!.children.add(testItem);
356363
// add to our map
357364
wstAdapter.runIdToTestItem.set(child.runID, testItem);
@@ -365,7 +372,6 @@ function populateTestTree(
365372

366373
node.canResolveChildren = true;
367374
node.tags = [RunTestTag, DebugTestTag];
368-
369375
testRoot!.children.add(node);
370376
}
371377
populateTestTree(testController, child, node, wstAdapter, token);

0 commit comments

Comments
 (0)