From 237f82b696725011beaa6a46bc75b6307efefc6f Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Mon, 31 Jul 2023 15:29:08 -0700 Subject: [PATCH] Fix UUID and disposing to resolve race condition (#21667) fixes https://github.com/microsoft/vscode-python/issues/21599 and https://github.com/microsoft/vscode-python/issues/21507 --- .../testing/testController/common/server.ts | 27 +- .../testing/testController/common/types.ts | 7 +- .../pytest/pytestDiscoveryAdapter.ts | 27 +- .../pytest/pytestExecutionAdapter.ts | 45 ++-- .../unittest/testDiscoveryAdapter.ts | 11 +- .../unittest/testExecutionAdapter.ts | 21 +- src/test/mocks/mockChildProcess.ts | 238 +++++++++++++++++ .../pytestDiscoveryAdapter.unit.test.ts | 76 ++++-- .../pytestExecutionAdapter.unit.test.ts | 118 +++++++-- .../testController/server.unit.test.ts | 248 +++++++++++++----- .../workspaceTestAdapter.unit.test.ts | 3 +- 11 files changed, 664 insertions(+), 157 deletions(-) create mode 100644 src/test/mocks/mockChildProcess.ts diff --git a/src/client/testing/testController/common/server.ts b/src/client/testing/testController/common/server.ts index f854371ffc35..564bd82f2ef6 100644 --- a/src/client/testing/testController/common/server.ts +++ b/src/client/testing/testController/common/server.ts @@ -3,10 +3,11 @@ import * as net from 'net'; import * as crypto from 'crypto'; -import { Disposable, Event, EventEmitter } from 'vscode'; +import { Disposable, Event, EventEmitter, TestRun } from 'vscode'; import * as path from 'path'; import { ExecutionFactoryCreateWithEnvironmentOptions, + ExecutionResult, IPythonExecutionFactory, SpawnOptions, } from '../../../common/process/types'; @@ -15,6 +16,7 @@ import { DataReceivedEvent, ITestServer, TestCommandOptions } from './types'; import { ITestDebugLauncher, LaunchOptions } from '../../common/types'; import { UNITTEST_PROVIDER } from '../../common/constants'; import { jsonRPCHeaders, jsonRPCContent, JSONRPC_UUID_HEADER } from './utils'; +import { createDeferred } from '../../../common/utils/async'; export class PythonTestServer implements ITestServer, Disposable { private _onDataReceived: EventEmitter = new EventEmitter(); @@ -140,7 +142,12 @@ export class PythonTestServer implements ITestServer, Disposable { return this._onDataReceived.event; } - async sendCommand(options: TestCommandOptions, runTestIdPort?: string, callback?: () => void): Promise { + async sendCommand( + options: TestCommandOptions, + runTestIdPort?: string, + runInstance?: TestRun, + callback?: () => void, + ): Promise { const { uuid } = options; const pythonPathParts: string[] = process.env.PYTHONPATH?.split(path.delimiter) ?? []; @@ -154,7 +161,7 @@ export class PythonTestServer implements ITestServer, Disposable { }; if (spawnOptions.extraVariables) spawnOptions.extraVariables.RUN_TEST_IDS_PORT = runTestIdPort; - const isRun = !options.testIds; + const isRun = runTestIdPort !== undefined; // Create the Python environment in which to execute the command. const creationOptions: ExecutionFactoryCreateWithEnvironmentOptions = { allowEnvironmentFetchExceptions: false, @@ -195,7 +202,19 @@ export class PythonTestServer implements ITestServer, Disposable { // This means it is running discovery traceLog(`Discovering unittest tests with arguments: ${args}\r\n`); } - await execService.exec(args, spawnOptions); + const deferred = createDeferred>(); + + const result = execService.execObservable(args, spawnOptions); + + runInstance?.token.onCancellationRequested(() => { + result?.proc?.kill(); + }); + result?.proc?.on('close', () => { + traceLog('Exec server closed.', uuid); + deferred.resolve({ stdout: '', stderr: '' }); + callback?.(); + }); + await deferred.promise; } } catch (ex) { this.uuids = this.uuids.filter((u) => u !== uuid); diff --git a/src/client/testing/testController/common/types.ts b/src/client/testing/testController/common/types.ts index d4e54951bfd7..16c0bd0e3cee 100644 --- a/src/client/testing/testController/common/types.ts +++ b/src/client/testing/testController/common/types.ts @@ -174,7 +174,12 @@ export interface ITestServer { readonly onDataReceived: Event; readonly onRunDataReceived: Event; readonly onDiscoveryDataReceived: Event; - sendCommand(options: TestCommandOptions, runTestIdsPort?: string, callback?: () => void): Promise; + sendCommand( + options: TestCommandOptions, + runTestIdsPort?: string, + runInstance?: TestRun, + callback?: () => void, + ): Promise; serverReady(): Promise; getPort(): number; createUUID(cwd: string): string; diff --git a/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts index b83224d4161b..810fae0fa11c 100644 --- a/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts +++ b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts @@ -4,13 +4,14 @@ import * as path from 'path'; import { Uri } from 'vscode'; import { ExecutionFactoryCreateWithEnvironmentOptions, + ExecutionResult, IPythonExecutionFactory, SpawnOptions, } from '../../../common/process/types'; import { IConfigurationService, ITestOutputChannel } from '../../../common/types'; import { createDeferred } from '../../../common/utils/async'; import { EXTENSION_ROOT_DIR } from '../../../constants'; -import { traceError, traceVerbose } from '../../../logging'; +import { traceVerbose } from '../../../logging'; import { DataReceivedEvent, DiscoveredTestPayload, @@ -48,7 +49,7 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { return discoveryPayload; } - async runPytestDiscovery(uri: Uri, executionFactory?: IPythonExecutionFactory): Promise { + async runPytestDiscovery(uri: Uri, executionFactory?: IPythonExecutionFactory): Promise { const deferred = createDeferred(); const relativePathToPytest = 'pythonFiles'; const fullPluginPath = path.join(EXTENSION_ROOT_DIR, relativePathToPytest); @@ -78,17 +79,15 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { }; const execService = await executionFactory?.createActivatedEnvironment(creationOptions); // delete UUID following entire discovery finishing. - execService - ?.exec(['-m', 'pytest', '-p', 'vscode_pytest', '--collect-only'].concat(pytestArgs), spawnOptions) - .then(() => { - this.testServer.deleteUUID(uuid); - return deferred.resolve(); - }) - .catch((err) => { - traceError(`Error while trying to run pytest discovery, \n${err}\r\n\r\n`); - this.testServer.deleteUUID(uuid); - return deferred.reject(err); - }); - return deferred.promise; + const deferredExec = createDeferred>(); + const execArgs = ['-m', 'pytest', '-p', 'vscode_pytest', '--collect-only'].concat(pytestArgs); + const result = execService?.execObservable(execArgs, spawnOptions); + + result?.proc?.on('close', () => { + deferredExec.resolve({ stdout: '', stderr: '' }); + this.testServer.deleteUUID(uuid); + deferred.resolve(); + }); + await deferredExec.promise; } } diff --git a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts index a75a6089627c..b05fa21fc046 100644 --- a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts +++ b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts @@ -15,6 +15,7 @@ import { } from '../common/types'; import { ExecutionFactoryCreateWithEnvironmentOptions, + ExecutionResult, IPythonExecutionFactory, SpawnOptions, } from '../../../common/process/types'; @@ -22,13 +23,7 @@ import { removePositionalFoldersAndFiles } from './arguments'; import { ITestDebugLauncher, LaunchOptions } from '../../common/types'; import { PYTEST_PROVIDER } from '../../common/constants'; import { EXTENSION_ROOT_DIR } from '../../../common/constants'; -import { startTestIdServer } from '../common/utils'; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -// (global as any).EXTENSION_ROOT_DIR = EXTENSION_ROOT_DIR; -/** - * Wrapper Class for pytest test execution.. - */ +import * as utils from '../common/utils'; export class PytestTestExecutionAdapter implements ITestExecutionAdapter { constructor( @@ -48,18 +43,20 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { ): Promise { const uuid = this.testServer.createUUID(uri.fsPath); traceVerbose(uri, testIds, debugBool); - const disposable = this.testServer.onRunDataReceived((e: DataReceivedEvent) => { + const disposedDataReceived = this.testServer.onRunDataReceived((e: DataReceivedEvent) => { if (runInstance) { this.resultResolver?.resolveExecution(JSON.parse(e.data), runInstance); } }); - try { - await this.runTestsNew(uri, testIds, uuid, debugBool, executionFactory, debugLauncher); - } finally { - this.testServer.deleteUUID(uuid); - disposable.dispose(); - // confirm with testing that this gets called (it must clean this up) - } + const dispose = function (testServer: ITestServer) { + testServer.deleteUUID(uuid); + disposedDataReceived.dispose(); + }; + runInstance?.token.onCancellationRequested(() => { + dispose(this.testServer); + }); + await this.runTestsNew(uri, testIds, uuid, runInstance, debugBool, executionFactory, debugLauncher); + // placeholder until after the rewrite is adopted // TODO: remove after adoption. const executionPayload: ExecutionTestPayload = { @@ -74,6 +71,7 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { uri: Uri, testIds: string[], uuid: string, + runInstance?: TestRun, debugBool?: boolean, executionFactory?: IPythonExecutionFactory, debugLauncher?: ITestDebugLauncher, @@ -124,7 +122,7 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { } traceLog(`Running PYTEST execution for the following test ids: ${testIds}`); - const pytestRunTestIdsPort = await startTestIdServer(testIds); + const pytestRunTestIdsPort = await utils.startTestIdServer(testIds); if (spawnOptions.extraVariables) spawnOptions.extraVariables.RUN_TEST_IDS_PORT = pytestRunTestIdsPort.toString(); @@ -143,6 +141,7 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { traceInfo(`Running DEBUG pytest with arguments: ${testArgs.join(' ')}\r\n`); await debugLauncher!.launchDebugger(launchOptions, () => { deferred.resolve(); + this.testServer.deleteUUID(uuid); }); } else { // combine path to run script with run args @@ -150,7 +149,19 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { const runArgs = [scriptPath, ...testArgs]; traceInfo(`Running pytests with arguments: ${runArgs.join(' ')}\r\n`); - await execService?.exec(runArgs, spawnOptions); + const deferredExec = createDeferred>(); + const result = execService?.execObservable(runArgs, spawnOptions); + + runInstance?.token.onCancellationRequested(() => { + result?.proc?.kill(); + }); + + result?.proc?.on('close', () => { + deferredExec.resolve({ stdout: '', stderr: '' }); + this.testServer.deleteUUID(uuid); + deferred.resolve(); + }); + await deferredExec.promise; } } catch (ex) { traceError(`Error while running tests: ${testIds}\r\n${ex}\r\n\r\n`); diff --git a/src/client/testing/testController/unittest/testDiscoveryAdapter.ts b/src/client/testing/testController/unittest/testDiscoveryAdapter.ts index 6deca55117ea..b49ac3dabd0e 100644 --- a/src/client/testing/testController/unittest/testDiscoveryAdapter.ts +++ b/src/client/testing/testController/unittest/testDiscoveryAdapter.ts @@ -46,12 +46,11 @@ export class UnittestTestDiscoveryAdapter implements ITestDiscoveryAdapter { const disposable = this.testServer.onDiscoveryDataReceived((e: DataReceivedEvent) => { this.resultResolver?.resolveDiscovery(JSON.parse(e.data)); }); - try { - await this.callSendCommand(options); - } finally { + + await this.callSendCommand(options, () => { this.testServer.deleteUUID(uuid); disposable.dispose(); - } + }); // placeholder until after the rewrite is adopted // TODO: remove after adoption. const discoveryPayload: DiscoveredTestPayload = { @@ -61,8 +60,8 @@ export class UnittestTestDiscoveryAdapter implements ITestDiscoveryAdapter { return discoveryPayload; } - private async callSendCommand(options: TestCommandOptions): Promise { - await this.testServer.sendCommand(options); + private async callSendCommand(options: TestCommandOptions, callback: () => void): Promise { + await this.testServer.sendCommand(options, undefined, undefined, callback); const discoveryPayload: DiscoveredTestPayload = { cwd: '', status: 'success' }; return discoveryPayload; } diff --git a/src/client/testing/testController/unittest/testExecutionAdapter.ts b/src/client/testing/testController/unittest/testExecutionAdapter.ts index 4cab941c2608..4cd392f93a43 100644 --- a/src/client/testing/testController/unittest/testExecutionAdapter.ts +++ b/src/client/testing/testController/unittest/testExecutionAdapter.ts @@ -37,18 +37,19 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { runInstance?: TestRun, ): Promise { const uuid = this.testServer.createUUID(uri.fsPath); - const disposable = this.testServer.onRunDataReceived((e: DataReceivedEvent) => { + const disposedDataReceived = this.testServer.onRunDataReceived((e: DataReceivedEvent) => { if (runInstance) { this.resultResolver?.resolveExecution(JSON.parse(e.data), runInstance); } }); - try { - await this.runTestsNew(uri, testIds, uuid, debugBool); - } finally { + const dispose = function () { + disposedDataReceived.dispose(); + }; + runInstance?.token.onCancellationRequested(() => { this.testServer.deleteUUID(uuid); - disposable.dispose(); - // confirm with testing that this gets called (it must clean this up) - } + dispose(); + }); + await this.runTestsNew(uri, testIds, uuid, runInstance, debugBool, dispose); const executionPayload: ExecutionTestPayload = { cwd: uri.fsPath, status: 'success', error: '' }; return executionPayload; } @@ -57,7 +58,9 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { uri: Uri, testIds: string[], uuid: string, + runInstance?: TestRun, debugBool?: boolean, + dispose?: () => void, ): Promise { const settings = this.configSettings.getSettings(uri); const { unittestArgs } = settings.testing; @@ -80,8 +83,10 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { const runTestIdsPort = await startTestIdServer(testIds); - await this.testServer.sendCommand(options, runTestIdsPort.toString(), () => { + await this.testServer.sendCommand(options, runTestIdsPort.toString(), runInstance, () => { + this.testServer.deleteUUID(uuid); deferred.resolve(); + dispose?.(); }); // placeholder until after the rewrite is adopted // TODO: remove after adoption. diff --git a/src/test/mocks/mockChildProcess.ts b/src/test/mocks/mockChildProcess.ts new file mode 100644 index 000000000000..c038c0f845ab --- /dev/null +++ b/src/test/mocks/mockChildProcess.ts @@ -0,0 +1,238 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { Serializable, SendHandle, MessageOptions } from 'child_process'; +import { Writable, Readable, Pipe } from 'stream'; +import { EventEmitter } from 'node:events'; + +export class MockChildProcess extends EventEmitter { + constructor(spawnfile: string, spawnargs: string[]) { + super(); + this.spawnfile = spawnfile; + this.spawnargs = spawnargs; + this.stdin = new Writable(); + this.stdout = new Readable(); + this.stderr = null; + this.channel = null; + this.stdio = [this.stdin, this.stdout, this.stdout, this.stderr, null]; + this.killed = false; + this.connected = false; + this.exitCode = null; + this.signalCode = null; + this.eventMap = new Map(); + } + + stdin: Writable | null; + + stdout: Readable | null; + + stderr: Readable | null; + + eventMap: Map; + + readonly channel?: Pipe | null | undefined; + + readonly stdio: [ + Writable | null, + // stdin + Readable | null, + // stdout + Readable | null, + // stderr + Readable | Writable | null | undefined, + // extra + Readable | Writable | null | undefined, // extra + ]; + + readonly killed: boolean; + + readonly pid?: number | undefined; + + readonly connected: boolean; + + readonly exitCode: number | null; + + readonly signalCode: NodeJS.Signals | null; + + readonly spawnargs: string[]; + + readonly spawnfile: string; + + signal?: NodeJS.Signals | number; + + send(message: Serializable, callback?: (error: Error | null) => void): boolean; + + send(message: Serializable, sendHandle?: SendHandle, callback?: (error: Error | null) => void): boolean; + + send( + message: Serializable, + sendHandle?: SendHandle, + options?: MessageOptions, + callback?: (error: Error | null) => void, + ): boolean; + + send( + message: Serializable, + _sendHandleOrCallback?: SendHandle | ((error: Error | null) => void), + _optionsOrCallback?: MessageOptions | ((error: Error | null) => void), + _callback?: (error: Error | null) => void, + ): boolean { + // Implementation of the send method + // For example, you might want to emit a 'message' event + this.stdout?.push(message.toString()); + return true; + } + + // eslint-disable-next-line class-methods-use-this + disconnect(): void { + /* noop */ + } + + // eslint-disable-next-line class-methods-use-this + unref(): void { + /* noop */ + } + + // eslint-disable-next-line class-methods-use-this + ref(): void { + /* noop */ + } + + addListener(event: 'close', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; + + addListener(event: 'disconnect', listener: () => void): this; + + addListener(event: 'error', listener: (err: Error) => void): this; + + addListener(event: 'exit', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; + + addListener(event: 'message', listener: (message: Serializable, sendHandle: SendHandle) => void): this; + + addListener(event: 'spawn', listener: () => void): this; + + addListener(event: string, listener: (...args: any[]) => void): this { + if (this.eventMap.has(event)) { + this.eventMap.get(event).push(listener); + } else { + this.eventMap.set(event, [listener]); + } + return this; + } + + emit(event: 'close', code: number | null, signal: NodeJS.Signals | null): boolean; + + emit(event: 'disconnect'): boolean; + + emit(event: 'error', err: Error): boolean; + + emit(event: 'exit', code: number | null, signal: NodeJS.Signals | null): boolean; + + emit(event: 'message', message: Serializable, sendHandle: SendHandle): boolean; + + emit(event: 'spawn', listener: () => void): boolean; + + emit(event: string | symbol, ...args: unknown[]): boolean { + if (this.eventMap.has(event.toString())) { + this.eventMap.get(event.toString()).forEach((listener: (arg0: unknown) => void) => { + const argsArray = Array.isArray(args) ? args : [args]; + listener(argsArray); + }); + } + return true; + } + + on(event: 'close', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; + + on(event: 'disconnect', listener: () => void): this; + + on(event: 'error', listener: (err: Error) => void): this; + + on(event: 'exit', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; + + on(event: 'message', listener: (message: Serializable, sendHandle: SendHandle) => void): this; + + on(event: 'spawn', listener: () => void): this; + + on(event: string, listener: (...args: any[]) => void): this { + if (this.eventMap.has(event)) { + this.eventMap.get(event).push(listener); + } else { + this.eventMap.set(event, [listener]); + } + return this; + } + + once(event: 'close', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; + + once(event: 'disconnect', listener: () => void): this; + + once(event: 'error', listener: (err: Error) => void): this; + + once(event: 'exit', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; + + once(event: 'message', listener: (message: Serializable, sendHandle: SendHandle) => void): this; + + once(event: 'spawn', listener: () => void): this; + + once(event: string, listener: (...args: any[]) => void): this { + if (this.eventMap.has(event)) { + this.eventMap.get(event).push(listener); + } else { + this.eventMap.set(event, [listener]); + } + return this; + } + + prependListener(event: 'close', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; + + prependListener(event: 'disconnect', listener: () => void): this; + + prependListener(event: 'error', listener: (err: Error) => void): this; + + prependListener(event: 'exit', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; + + prependListener(event: 'message', listener: (message: Serializable, sendHandle: SendHandle) => void): this; + + prependListener(event: 'spawn', listener: () => void): this; + + prependListener(event: string, listener: (...args: any[]) => void): this { + if (this.eventMap.has(event)) { + this.eventMap.get(event).push(listener); + } else { + this.eventMap.set(event, [listener]); + } + return this; + } + + prependOnceListener(event: 'close', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; + + prependOnceListener(event: 'disconnect', listener: () => void): this; + + prependOnceListener(event: 'error', listener: (err: Error) => void): this; + + prependOnceListener(event: 'exit', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; + + prependOnceListener(event: 'message', listener: (message: Serializable, sendHandle: SendHandle) => void): this; + + prependOnceListener(event: 'spawn', listener: () => void): this; + + prependOnceListener(event: string, listener: (...args: any[]) => void): this { + if (this.eventMap.has(event)) { + this.eventMap.get(event).push(listener); + } else { + this.eventMap.set(event, [listener]); + } + return this; + } + + trigger(event: string): Array { + if (this.eventMap.has(event)) { + return this.eventMap.get(event); + } + return []; + } + + kill(_signal?: NodeJS.Signals | number): boolean { + this.stdout?.destroy(); + return true; + } +} diff --git a/src/test/testing/testController/pytest/pytestDiscoveryAdapter.unit.test.ts b/src/test/testing/testController/pytest/pytestDiscoveryAdapter.unit.test.ts index 18212b2d1032..8ba7dd9a6f00 100644 --- a/src/test/testing/testController/pytest/pytestDiscoveryAdapter.unit.test.ts +++ b/src/test/testing/testController/pytest/pytestDiscoveryAdapter.unit.test.ts @@ -5,6 +5,7 @@ import * as assert from 'assert'; import { Uri } from 'vscode'; import * as typeMoq from 'typemoq'; import * as path from 'path'; +import { Observable } from 'rxjs/Observable'; import { IConfigurationService, ITestOutputChannel } from '../../../../client/common/types'; import { PytestTestDiscoveryAdapter } from '../../../../client/testing/testController/pytest/pytestDiscoveryAdapter'; import { ITestServer } from '../../../../client/testing/testController/common/types'; @@ -12,9 +13,11 @@ import { IPythonExecutionFactory, IPythonExecutionService, SpawnOptions, + Output, } from '../../../../client/common/process/types'; -import { createDeferred, Deferred } from '../../../../client/common/utils/async'; import { EXTENSION_ROOT_DIR } from '../../../../client/constants'; +import { MockChildProcess } from '../../../mocks/mockChildProcess'; +import { Deferred, createDeferred } from '../../../../client/common/utils/async'; suite('pytest test discovery adapter', () => { let testServer: typeMoq.IMock; @@ -29,6 +32,7 @@ suite('pytest test discovery adapter', () => { let expectedPath: string; let uri: Uri; let expectedExtraVariables: Record; + let mockProc: MockChildProcess; setup(() => { const mockExtensionRootDir = typeMoq.Mock.ofType(); @@ -66,32 +70,46 @@ suite('pytest test discovery adapter', () => { }), } as unknown) as IConfigurationService; - // set up exec factory - execFactory = typeMoq.Mock.ofType(); - execFactory - .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) - .returns(() => Promise.resolve(execService.object)); - - // set up exec service + // set up exec service with child process + mockProc = new MockChildProcess('', ['']); execService = typeMoq.Mock.ofType(); - deferred = createDeferred(); + const output = new Observable>(() => { + /* no op */ + }); execService - .setup((x) => x.exec(typeMoq.It.isAny(), typeMoq.It.isAny())) - .returns(() => { - deferred.resolve(); - return Promise.resolve({ stdout: '{}' }); - }); + .setup((x) => x.execObservable(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => ({ + proc: mockProc, + out: output, + dispose: () => { + /* no-body */ + }, + })); execService.setup((p) => ((p as unknown) as any).then).returns(() => undefined); outputChannel = typeMoq.Mock.ofType(); }); test('Discovery should call exec with correct basic args', async () => { + // set up exec mock + deferred = createDeferred(); + execFactory = typeMoq.Mock.ofType(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => { + deferred.resolve(); + return Promise.resolve(execService.object); + }); + adapter = new PytestTestDiscoveryAdapter(testServer.object, configService, outputChannel.object); - await adapter.discoverTests(uri, execFactory.object); - const expectedArgs = ['-m', 'pytest', '-p', 'vscode_pytest', '--collect-only', '.']; + adapter.discoverTests(uri, execFactory.object); + // add in await and trigger + await deferred.promise; + mockProc.trigger('close'); + // verification + const expectedArgs = ['-m', 'pytest', '-p', 'vscode_pytest', '--collect-only', '.']; execService.verify( (x) => - x.exec( + x.execObservable( expectedArgs, typeMoq.It.is((options) => { assert.deepEqual(options.extraVariables, expectedExtraVariables); @@ -108,16 +126,34 @@ suite('pytest test discovery adapter', () => { const expectedPathNew = path.join('other', 'path'); const configServiceNew: IConfigurationService = ({ getSettings: () => ({ - testing: { pytestArgs: ['.', 'abc', 'xyz'], cwd: expectedPathNew }, + testing: { + pytestArgs: ['.', 'abc', 'xyz'], + cwd: expectedPathNew, + }, }), } as unknown) as IConfigurationService; + // set up exec mock + deferred = createDeferred(); + execFactory = typeMoq.Mock.ofType(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => { + deferred.resolve(); + return Promise.resolve(execService.object); + }); + adapter = new PytestTestDiscoveryAdapter(testServer.object, configServiceNew, outputChannel.object); - await adapter.discoverTests(uri, execFactory.object); + adapter.discoverTests(uri, execFactory.object); + // add in await and trigger + await deferred.promise; + mockProc.trigger('close'); + + // verification const expectedArgs = ['-m', 'pytest', '-p', 'vscode_pytest', '--collect-only', '.', 'abc', 'xyz']; execService.verify( (x) => - x.exec( + x.execObservable( expectedArgs, typeMoq.It.is((options) => { assert.deepEqual(options.extraVariables, expectedExtraVariables); diff --git a/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts b/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts index 44116fd753b0..43b763f56e6c 100644 --- a/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts +++ b/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts @@ -6,11 +6,13 @@ import { TestRun, Uri } from 'vscode'; import * as typeMoq from 'typemoq'; import * as sinon from 'sinon'; import * as path from 'path'; +import { Observable } from 'rxjs/Observable'; import { IConfigurationService, ITestOutputChannel } from '../../../../client/common/types'; import { ITestServer } from '../../../../client/testing/testController/common/types'; import { IPythonExecutionFactory, IPythonExecutionService, + Output, SpawnOptions, } from '../../../../client/common/process/types'; import { createDeferred, Deferred } from '../../../../client/common/utils/async'; @@ -18,6 +20,7 @@ import { PytestTestExecutionAdapter } from '../../../../client/testing/testContr import { ITestDebugLauncher, LaunchOptions } from '../../../../client/testing/common/types'; import * as util from '../../../../client/testing/testController/common/utils'; import { EXTENSION_ROOT_DIR } from '../../../../client/constants'; +import { MockChildProcess } from '../../../mocks/mockChildProcess'; suite('pytest test execution adapter', () => { let testServer: typeMoq.IMock; @@ -29,8 +32,8 @@ suite('pytest test execution adapter', () => { let debugLauncher: typeMoq.IMock; (global as any).EXTENSION_ROOT_DIR = EXTENSION_ROOT_DIR; let myTestPath: string; - let startTestIdServerStub: sinon.SinonStub; - + let mockProc: MockChildProcess; + let utilsStub: sinon.SinonStub; setup(() => { testServer = typeMoq.Mock.ofType(); testServer.setup((t) => t.getPort()).returns(() => 12345); @@ -47,8 +50,24 @@ suite('pytest test execution adapter', () => { }), isTestExecution: () => false, } as unknown) as IConfigurationService; - execFactory = typeMoq.Mock.ofType(); + + // set up exec service with child process + mockProc = new MockChildProcess('', ['']); + const output = new Observable>(() => { + /* no op */ + }); execService = typeMoq.Mock.ofType(); + execService + .setup((x) => x.execObservable(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => ({ + proc: mockProc, + out: output, + dispose: () => { + /* no-body */ + }, + })); + execFactory = typeMoq.Mock.ofType(); + utilsStub = sinon.stub(util, 'startTestIdServer'); debugLauncher = typeMoq.Mock.ofType(); execFactory .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) @@ -66,7 +85,6 @@ suite('pytest test execution adapter', () => { deferred.resolve(); return Promise.resolve(); }); - startTestIdServerStub = sinon.stub(util, 'startTestIdServer').returns(Promise.resolve(54321)); execFactory.setup((p) => ((p as unknown) as any).then).returns(() => undefined); execService.setup((p) => ((p as unknown) as any).then).returns(() => undefined); @@ -77,10 +95,25 @@ suite('pytest test execution adapter', () => { sinon.restore(); }); test('startTestIdServer called with correct testIds', async () => { + const deferred2 = createDeferred(); + const deferred3 = createDeferred(); + execFactory = typeMoq.Mock.ofType(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => { + deferred2.resolve(); + return Promise.resolve(execService.object); + }); + utilsStub.callsFake(() => { + deferred3.resolve(); + return Promise.resolve(54321); + }); + const testRun = typeMoq.Mock.ofType(); + testRun.setup((t) => t.token).returns(() => ({ onCancellationRequested: () => undefined } as any)); const uri = Uri.file(myTestPath); const uuid = 'uuid123'; testServer - .setup((t) => t.onDiscoveryDataReceived(typeMoq.It.isAny(), typeMoq.It.isAny())) + .setup((t) => t.onRunDataReceived(typeMoq.It.isAny(), typeMoq.It.isAny())) .returns(() => ({ dispose: () => { /* no-body */ @@ -88,19 +121,38 @@ suite('pytest test execution adapter', () => { })); testServer.setup((t) => t.createUUID(typeMoq.It.isAny())).returns(() => uuid); const outputChannel = typeMoq.Mock.ofType(); - const testRun = typeMoq.Mock.ofType(); adapter = new PytestTestExecutionAdapter(testServer.object, configService, outputChannel.object); - const testIds = ['test1id', 'test2id']; - await adapter.runTests(uri, testIds, false, testRun.object, execFactory.object); + adapter.runTests(uri, testIds, false, testRun.object, execFactory.object); - sinon.assert.calledWithExactly(startTestIdServerStub, testIds); + // add in await and trigger + await deferred2.promise; + await deferred3.promise; + mockProc.trigger('close'); + + // assert + sinon.assert.calledWithExactly(utilsStub, testIds); }); test('pytest execution called with correct args', async () => { + const deferred2 = createDeferred(); + const deferred3 = createDeferred(); + execFactory = typeMoq.Mock.ofType(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => { + deferred2.resolve(); + return Promise.resolve(execService.object); + }); + utilsStub.callsFake(() => { + deferred3.resolve(); + return Promise.resolve(54321); + }); + const testRun = typeMoq.Mock.ofType(); + testRun.setup((t) => t.token).returns(() => ({ onCancellationRequested: () => undefined } as any)); const uri = Uri.file(myTestPath); const uuid = 'uuid123'; testServer - .setup((t) => t.onDiscoveryDataReceived(typeMoq.It.isAny(), typeMoq.It.isAny())) + .setup((t) => t.onRunDataReceived(typeMoq.It.isAny(), typeMoq.It.isAny())) .returns(() => ({ dispose: () => { /* no-body */ @@ -108,9 +160,12 @@ suite('pytest test execution adapter', () => { })); testServer.setup((t) => t.createUUID(typeMoq.It.isAny())).returns(() => uuid); const outputChannel = typeMoq.Mock.ofType(); - const testRun = typeMoq.Mock.ofType(); adapter = new PytestTestExecutionAdapter(testServer.object, configService, outputChannel.object); - await adapter.runTests(uri, [], false, testRun.object, execFactory.object); + adapter.runTests(uri, [], false, testRun.object, execFactory.object); + + await deferred2.promise; + await deferred3.promise; + mockProc.trigger('close'); const pathToPythonFiles = path.join(EXTENSION_ROOT_DIR, 'pythonFiles'); const pathToPythonScript = path.join(pathToPythonFiles, 'vscode_pytest', 'run_pytest_script.py'); @@ -123,7 +178,7 @@ suite('pytest test execution adapter', () => { // execService.verify((x) => x.exec(expectedArgs, typeMoq.It.isAny()), typeMoq.Times.once()); execService.verify( (x) => - x.exec( + x.execObservable( expectedArgs, typeMoq.It.is((options) => { assert.equal(options.extraVariables?.PYTHONPATH, expectedExtraVariables.PYTHONPATH); @@ -139,6 +194,21 @@ suite('pytest test execution adapter', () => { ); }); test('pytest execution respects settings.testing.cwd when present', async () => { + const deferred2 = createDeferred(); + const deferred3 = createDeferred(); + execFactory = typeMoq.Mock.ofType(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => { + deferred2.resolve(); + return Promise.resolve(execService.object); + }); + utilsStub.callsFake(() => { + deferred3.resolve(); + return Promise.resolve(54321); + }); + const testRun = typeMoq.Mock.ofType(); + testRun.setup((t) => t.token).returns(() => ({ onCancellationRequested: () => undefined } as any)); const newCwd = path.join('new', 'path'); configService = ({ getSettings: () => ({ @@ -149,7 +219,7 @@ suite('pytest test execution adapter', () => { const uri = Uri.file(myTestPath); const uuid = 'uuid123'; testServer - .setup((t) => t.onDiscoveryDataReceived(typeMoq.It.isAny(), typeMoq.It.isAny())) + .setup((t) => t.onRunDataReceived(typeMoq.It.isAny(), typeMoq.It.isAny())) .returns(() => ({ dispose: () => { /* no-body */ @@ -157,9 +227,12 @@ suite('pytest test execution adapter', () => { })); testServer.setup((t) => t.createUUID(typeMoq.It.isAny())).returns(() => uuid); const outputChannel = typeMoq.Mock.ofType(); - const testRun = typeMoq.Mock.ofType(); adapter = new PytestTestExecutionAdapter(testServer.object, configService, outputChannel.object); - await adapter.runTests(uri, [], false, testRun.object, execFactory.object); + adapter.runTests(uri, [], false, testRun.object, execFactory.object); + + await deferred2.promise; + await deferred3.promise; + mockProc.trigger('close'); const pathToPythonFiles = path.join(EXTENSION_ROOT_DIR, 'pythonFiles'); const pathToPythonScript = path.join(pathToPythonFiles, 'vscode_pytest', 'run_pytest_script.py'); @@ -172,7 +245,7 @@ suite('pytest test execution adapter', () => { execService.verify( (x) => - x.exec( + x.execObservable( expectedArgs, typeMoq.It.is((options) => { assert.equal(options.extraVariables?.PYTHONPATH, expectedExtraVariables.PYTHONPATH); @@ -188,10 +261,17 @@ suite('pytest test execution adapter', () => { ); }); test('Debug launched correctly for pytest', async () => { + const deferred3 = createDeferred(); + utilsStub.callsFake(() => { + deferred3.resolve(); + return Promise.resolve(54321); + }); + const testRun = typeMoq.Mock.ofType(); + testRun.setup((t) => t.token).returns(() => ({ onCancellationRequested: () => undefined } as any)); const uri = Uri.file(myTestPath); const uuid = 'uuid123'; testServer - .setup((t) => t.onDiscoveryDataReceived(typeMoq.It.isAny(), typeMoq.It.isAny())) + .setup((t) => t.onRunDataReceived(typeMoq.It.isAny(), typeMoq.It.isAny())) .returns(() => ({ dispose: () => { /* no-body */ @@ -199,9 +279,9 @@ suite('pytest test execution adapter', () => { })); testServer.setup((t) => t.createUUID(typeMoq.It.isAny())).returns(() => uuid); const outputChannel = typeMoq.Mock.ofType(); - const testRun = typeMoq.Mock.ofType(); adapter = new PytestTestExecutionAdapter(testServer.object, configService, outputChannel.object); await adapter.runTests(uri, [], true, testRun.object, execFactory.object, debugLauncher.object); + await deferred3.promise; debugLauncher.verify( (x) => x.launchDebugger( diff --git a/src/test/testing/testController/server.unit.test.ts b/src/test/testing/testController/server.unit.test.ts index 1131c26c6444..53c2b72e40f7 100644 --- a/src/test/testing/testController/server.unit.test.ts +++ b/src/test/testing/testController/server.unit.test.ts @@ -1,15 +1,24 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +/* eslint-disable @typescript-eslint/no-explicit-any */ import * as assert from 'assert'; import * as net from 'net'; import * as sinon from 'sinon'; import * as crypto from 'crypto'; import { OutputChannel, Uri } from 'vscode'; -import { IPythonExecutionFactory, IPythonExecutionService, SpawnOptions } from '../../../client/common/process/types'; +import { Observable } from 'rxjs'; +import * as typeMoq from 'typemoq'; +import { + IPythonExecutionFactory, + IPythonExecutionService, + Output, + SpawnOptions, +} from '../../../client/common/process/types'; import { PythonTestServer } from '../../../client/testing/testController/common/server'; import { ITestDebugLauncher } from '../../../client/testing/common/types'; -import { createDeferred } from '../../../client/common/utils/async'; +import { Deferred, createDeferred } from '../../../client/common/utils/async'; +import { MockChildProcess } from '../../mocks/mockChildProcess'; suite('Python Test Server', () => { const fakeUuid = 'fake-uuid'; @@ -18,10 +27,12 @@ suite('Python Test Server', () => { let stubExecutionService: IPythonExecutionService; let server: PythonTestServer; let sandbox: sinon.SinonSandbox; - let execArgs: string[]; - let spawnOptions: SpawnOptions; let v4Stub: sinon.SinonStub; let debugLauncher: ITestDebugLauncher; + let mockProc: MockChildProcess; + let execService: typeMoq.IMock; + let deferred: Deferred; + let execFactory = typeMoq.Mock.ofType(); setup(() => { sandbox = sinon.createSandbox(); @@ -29,27 +40,42 @@ suite('Python Test Server', () => { v4Stub.returns(fakeUuid); stubExecutionService = ({ - exec: (args: string[], spawnOptionsProvided: SpawnOptions) => { - execArgs = args; - spawnOptions = spawnOptionsProvided; - return Promise.resolve({ stdout: '', stderr: '' }); - }, + execObservable: () => Promise.resolve({ stdout: '', stderr: '' }), } as unknown) as IPythonExecutionService; stubExecutionFactory = ({ createActivatedEnvironment: () => Promise.resolve(stubExecutionService), } as unknown) as IPythonExecutionFactory; + + // set up exec service with child process + mockProc = new MockChildProcess('', ['']); + execService = typeMoq.Mock.ofType(); + const output = new Observable>(() => { + /* no op */ + }); + execService + .setup((x) => x.execObservable(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => ({ + proc: mockProc, + out: output, + dispose: () => { + /* no-body */ + }, + })); + execService.setup((p) => ((p as unknown) as any).then).returns(() => undefined); }); teardown(() => { sandbox.restore(); - execArgs = []; server.dispose(); }); test('sendCommand should add the port to the command being sent and add the correct extra spawn variables', async () => { const options = { - command: { script: 'myscript', args: ['-foo', 'foo'] }, + command: { + script: 'myscript', + args: ['-foo', 'foo'], + }, workspaceFolder: Uri.file('/foo/bar'), cwd: '/foo/bar', uuid: fakeUuid, @@ -59,17 +85,31 @@ suite('Python Test Server', () => { outputChannel: undefined, token: undefined, throwOnStdErr: true, - extraVariables: { PYTHONPATH: '/foo/bar', RUN_TEST_IDS_PORT: '56789' }, + extraVariables: { + PYTHONPATH: '/foo/bar', + RUN_TEST_IDS_PORT: '56789', + }, } as SpawnOptions; + const deferred2 = createDeferred(); + execFactory = typeMoq.Mock.ofType(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => { + deferred2.resolve(); + return Promise.resolve(execService.object); + }); - server = new PythonTestServer(stubExecutionFactory, debugLauncher); + server = new PythonTestServer(execFactory.object, debugLauncher); await server.serverReady(); - await server.sendCommand(options, '56789'); - const port = server.getPort(); + server.sendCommand(options, '56789'); + // add in await and trigger + await deferred2.promise; + mockProc.trigger('close'); - assert.deepStrictEqual(execArgs, ['myscript', '--port', `${port}`, '--uuid', fakeUuid, '-foo', 'foo']); - assert.deepStrictEqual(spawnOptions, expectedSpawnOptions); + const port = server.getPort(); + const expectedArgs = ['myscript', '--port', `${port}`, '--uuid', fakeUuid, '-foo', 'foo']; + execService.verify((x) => x.execObservable(expectedArgs, expectedSpawnOptions), typeMoq.Times.once()); }); test('sendCommand should write to an output channel if it is provided as an option', async () => { @@ -80,17 +120,31 @@ suite('Python Test Server', () => { }, } as OutputChannel; const options = { - command: { script: 'myscript', args: ['-foo', 'foo'] }, + command: { + script: 'myscript', + args: ['-foo', 'foo'], + }, workspaceFolder: Uri.file('/foo/bar'), cwd: '/foo/bar', uuid: fakeUuid, outChannel, }; + deferred = createDeferred(); + execFactory = typeMoq.Mock.ofType(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => { + deferred.resolve(); + return Promise.resolve(execService.object); + }); - server = new PythonTestServer(stubExecutionFactory, debugLauncher); + server = new PythonTestServer(execFactory.object, debugLauncher); await server.serverReady(); - await server.sendCommand(options); + server.sendCommand(options); + // add in await and trigger + await deferred.promise; + mockProc.trigger('close'); const port = server.getPort(); const expected = ['python', 'myscript', '--port', `${port}`, '--uuid', fakeUuid, '-foo', 'foo'].join(' '); @@ -99,13 +153,12 @@ suite('Python Test Server', () => { }); test('If script execution fails during sendCommand, an onDataReceived event should be fired with the "error" status', async () => { - let eventData: { status: string; errors: string[] }; + let eventData: { status: string; errors: string[] } | undefined; stubExecutionService = ({ - exec: () => { + execObservable: () => { throw new Error('Failed to execute'); }, } as unknown) as IPythonExecutionService; - const options = { command: { script: 'myscript', args: ['-foo', 'foo'] }, workspaceFolder: Uri.file('/foo/bar'), @@ -122,30 +175,43 @@ suite('Python Test Server', () => { await server.sendCommand(options); - assert.deepStrictEqual(eventData!.status, 'error'); - assert.deepStrictEqual(eventData!.errors, ['Failed to execute']); + assert.notEqual(eventData, undefined); + assert.deepStrictEqual(eventData?.status, 'error'); + assert.deepStrictEqual(eventData?.errors, ['Failed to execute']); }); test('If the server receives malformed data, it should display a log message, and not fire an event', async () => { let eventData: string | undefined; const client = new net.Socket(); - const deferred = createDeferred(); - const options = { command: { script: 'myscript', args: ['-foo', 'foo'] }, workspaceFolder: Uri.file('/foo/bar'), cwd: '/foo/bar', uuid: fakeUuid, }; - - stubExecutionService = ({ - exec: async () => { + mockProc = new MockChildProcess('', ['']); + const output = new Observable>(() => { + /* no op */ + }); + const stubExecutionService2 = ({ + execObservable: () => { client.connect(server.getPort()); - return Promise.resolve({ stdout: '', stderr: '' }); + return { + proc: mockProc, + out: output, + dispose: () => { + /* no-body */ + }, + }; }, } as unknown) as IPythonExecutionService; - server = new PythonTestServer(stubExecutionFactory, debugLauncher); + const stubExecutionFactory2 = ({ + createActivatedEnvironment: () => Promise.resolve(stubExecutionService2), + } as unknown) as IPythonExecutionFactory; + + deferred = createDeferred(); + server = new PythonTestServer(stubExecutionFactory2, debugLauncher); await server.serverReady(); server.onDataReceived(({ data }) => { eventData = data; @@ -161,16 +227,17 @@ suite('Python Test Server', () => { console.log('Socket connection error:', error); }); - await server.sendCommand(options); + server.sendCommand(options); + // add in await and trigger await deferred.promise; + mockProc.trigger('close'); + assert.deepStrictEqual(eventData, ''); }); test('If the server doesnt recognize the UUID it should ignore it', async () => { let eventData: string | undefined; const client = new net.Socket(); - const deferred = createDeferred(); - const options = { command: { script: 'myscript', args: ['-foo', 'foo'] }, workspaceFolder: Uri.file('/foo/bar'), @@ -178,14 +245,28 @@ suite('Python Test Server', () => { uuid: fakeUuid, }; - stubExecutionService = ({ - exec: async () => { + deferred = createDeferred(); + mockProc = new MockChildProcess('', ['']); + const output = new Observable>(() => { + /* no op */ + }); + const stubExecutionService2 = ({ + execObservable: () => { client.connect(server.getPort()); - return Promise.resolve({ stdout: '', stderr: '' }); + return { + proc: mockProc, + out: output, + dispose: () => { + /* no-body */ + }, + }; }, } as unknown) as IPythonExecutionService; - server = new PythonTestServer(stubExecutionFactory, debugLauncher); + const stubExecutionFactory2 = ({ + createActivatedEnvironment: () => Promise.resolve(stubExecutionService2), + } as unknown) as IPythonExecutionFactory; + server = new PythonTestServer(stubExecutionFactory2, debugLauncher); await server.serverReady(); server.onDataReceived(({ data }) => { eventData = data; @@ -201,7 +282,7 @@ suite('Python Test Server', () => { console.log('Socket connection error:', error); }); - await server.sendCommand(options); + server.sendCommand(options); await deferred.promise; assert.deepStrictEqual(eventData, ''); }); @@ -212,23 +293,34 @@ suite('Python Test Server', () => { test('Error if payload does not have a content length header', async () => { let eventData: string | undefined; const client = new net.Socket(); - const deferred = createDeferred(); - const options = { command: { script: 'myscript', args: ['-foo', 'foo'] }, workspaceFolder: Uri.file('/foo/bar'), cwd: '/foo/bar', uuid: fakeUuid, }; - - stubExecutionService = ({ - exec: async () => { + deferred = createDeferred(); + mockProc = new MockChildProcess('', ['']); + const output = new Observable>(() => { + /* no op */ + }); + const stubExecutionService2 = ({ + execObservable: () => { client.connect(server.getPort()); - return Promise.resolve({ stdout: '', stderr: '' }); + return { + proc: mockProc, + out: output, + dispose: () => { + /* no-body */ + }, + }; }, } as unknown) as IPythonExecutionService; - server = new PythonTestServer(stubExecutionFactory, debugLauncher); + const stubExecutionFactory2 = ({ + createActivatedEnvironment: () => Promise.resolve(stubExecutionService2), + } as unknown) as IPythonExecutionFactory; + server = new PythonTestServer(stubExecutionFactory2, debugLauncher); await server.serverReady(); server.onDataReceived(({ data }) => { eventData = data; @@ -244,7 +336,7 @@ suite('Python Test Server', () => { console.log('Socket connection error:', error); }); - await server.sendCommand(options); + server.sendCommand(options); await deferred.promise; assert.deepStrictEqual(eventData, ''); }); @@ -267,7 +359,6 @@ Request-uuid: UUID_HERE // Your test logic here let eventData: string | undefined; const client = new net.Socket(); - const deferred = createDeferred(); const options = { command: { script: 'myscript', args: ['-foo', 'foo'] }, @@ -275,15 +366,28 @@ Request-uuid: UUID_HERE cwd: '/foo/bar', uuid: fakeUuid, }; - - stubExecutionService = ({ - exec: async () => { + deferred = createDeferred(); + mockProc = new MockChildProcess('', ['']); + const output = new Observable>(() => { + /* no op */ + }); + const stubExecutionService2 = ({ + execObservable: () => { client.connect(server.getPort()); - return Promise.resolve({ stdout: '', stderr: '' }); + return { + proc: mockProc, + out: output, + dispose: () => { + /* no-body */ + }, + }; }, } as unknown) as IPythonExecutionService; + const stubExecutionFactory2 = ({ + createActivatedEnvironment: () => Promise.resolve(stubExecutionService2), + } as unknown) as IPythonExecutionFactory; - server = new PythonTestServer(stubExecutionFactory, debugLauncher); + server = new PythonTestServer(stubExecutionFactory2, debugLauncher); await server.serverReady(); const uuid = server.createUUID(); payload = payload.replace('UUID_HERE', uuid); @@ -301,7 +405,7 @@ Request-uuid: UUID_HERE console.log('Socket connection error:', error); }); - await server.sendCommand(options); + server.sendCommand(options); await deferred.promise; assert.deepStrictEqual(eventData, expectedResult); }); @@ -310,8 +414,29 @@ Request-uuid: UUID_HERE test('Calls run resolver if the result header is in the payload', async () => { let eventData: string | undefined; const client = new net.Socket(); - const deferred = createDeferred(); + deferred = createDeferred(); + mockProc = new MockChildProcess('', ['']); + const output = new Observable>(() => { + /* no op */ + }); + const stubExecutionService2 = ({ + execObservable: () => { + client.connect(server.getPort()); + return { + proc: mockProc, + out: output, + dispose: () => { + /* no-body */ + }, + }; + }, + } as unknown) as IPythonExecutionService; + + const stubExecutionFactory2 = ({ + createActivatedEnvironment: () => Promise.resolve(stubExecutionService2), + } as unknown) as IPythonExecutionFactory; + server = new PythonTestServer(stubExecutionFactory2, debugLauncher); const options = { command: { script: 'myscript', args: ['-foo', 'foo'] }, workspaceFolder: Uri.file('/foo/bar'), @@ -319,14 +444,6 @@ Request-uuid: UUID_HERE uuid: fakeUuid, }; - stubExecutionService = ({ - exec: async () => { - client.connect(server.getPort()); - return Promise.resolve({ stdout: '', stderr: '' }); - }, - } as unknown) as IPythonExecutionService; - - server = new PythonTestServer(stubExecutionFactory, debugLauncher); await server.serverReady(); const uuid = server.createUUID(); server.onRunDataReceived(({ data }) => { @@ -349,9 +466,8 @@ Request-uuid: ${uuid} console.log('Socket connection error:', error); }); - await server.sendCommand(options); + server.sendCommand(options); await deferred.promise; - console.log('event data', eventData); const expectedResult = '{"cwd": "path", "status": "success", "result": "xyz", "not_found": null, "error": null}'; assert.deepStrictEqual(eventData, expectedResult); diff --git a/src/test/testing/testController/workspaceTestAdapter.unit.test.ts b/src/test/testing/testController/workspaceTestAdapter.unit.test.ts index 5a2e48130746..41cd1bbd7ef2 100644 --- a/src/test/testing/testController/workspaceTestAdapter.unit.test.ts +++ b/src/test/testing/testController/workspaceTestAdapter.unit.test.ts @@ -164,8 +164,7 @@ suite('Workspace test adapter', () => { const buildErrorNodeOptionsStub = sinon.stub(util, 'buildErrorNodeOptions').returns(errorTestItemOptions); const testProvider = 'unittest'; - const abc = await workspaceTestAdapter.discoverTests(testController); - console.log(abc); + await workspaceTestAdapter.discoverTests(testController); sinon.assert.calledWithMatch(createErrorTestItemStub, sinon.match.any, sinon.match.any); sinon.assert.calledWithMatch(buildErrorNodeOptionsStub, Uri.parse('foo'), sinon.match.any, testProvider);