diff --git a/package-lock.json b/package-lock.json index bcd06599a795..1d8f357e0cd9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19068,9 +19068,9 @@ } }, "ts-mockito": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/ts-mockito/-/ts-mockito-2.3.1.tgz", - "integrity": "sha512-chcKw0sTApwJxTyKhzbWxI4BTUJ6RStZKUVh2/mfwYqFS09PYy5pvdXZwG35QSkqT5pkdXZlYKBX196RRvEZdQ==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-mockito/-/ts-mockito-2.5.0.tgz", + "integrity": "sha512-b3qUeMfghRq5k5jw3xNJcnU9RKhqKnRn0k9v9QkN+YpuawrFuMIiGwzFZCpdi5MHy26o7YPnK8gag2awURl3nA==", "dev": true, "requires": { "lodash": "^4.17.5" diff --git a/package.json b/package.json index 67e539502a11..3f64489147ed 100644 --- a/package.json +++ b/package.json @@ -3132,7 +3132,7 @@ "terser-webpack-plugin": "^2.3.2", "transform-loader": "^0.2.4", "ts-loader": "^5.3.0", - "ts-mockito": "^2.3.1", + "ts-mockito": "^2.5.0", "ts-node": "^8.3.0", "tsconfig-paths-webpack-plugin": "^3.2.0", "tslint": "^5.20.1", diff --git a/src/client/datascience/raw-kernel/rawFuture.ts b/src/client/datascience/raw-kernel/rawFuture.ts new file mode 100644 index 000000000000..79b67156ddf1 --- /dev/null +++ b/src/client/datascience/raw-kernel/rawFuture.ts @@ -0,0 +1,155 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import { Kernel, KernelMessage } from '@jupyterlab/services'; +import { createDeferred, Deferred } from '../../common/utils/async'; +import { noop } from '../../common/utils/misc'; + +/* +RawFuture represents the IFuture interface that JupyterLab services returns from functions like executeRequest. +It provides an interface for getting updates on the status of the request such as reply messages or io messages +*/ +export class RawFuture< + REQUEST extends KernelMessage.IShellControlMessage, + REPLY extends KernelMessage.IShellControlMessage +> implements Kernel.IFuture { + public isDisposed: boolean = false; + public msg: REQUEST; + + private donePromise: Deferred; + private stdIn: (msg: KernelMessage.IStdinMessage) => void | PromiseLike = noop; + private ioPub: (msg: KernelMessage.IIOPubMessage) => void | PromiseLike = noop; + private reply: (msg: REPLY) => void | PromiseLike = noop; + private replyMessage: REPLY | undefined; + private disposeOnDone: boolean; + private idleSeen: boolean; + + constructor(msg: REQUEST, disposeOnDone: boolean) { + this.msg = msg; + this.donePromise = createDeferred(); + this.disposeOnDone = disposeOnDone; + this.idleSeen = false; + } + + get done(): Promise { + return this.donePromise.promise; + } + + // Message handlers that can be hooked up to for message notifications + get onStdin(): (msg: KernelMessage.IStdinMessage) => void | PromiseLike { + return this.stdIn; + } + + set onStdin(handler: (msg: KernelMessage.IStdinMessage) => void | PromiseLike) { + this.stdIn = handler; + } + + get onIOPub(): (msg: KernelMessage.IIOPubMessage) => void | PromiseLike { + return this.ioPub; + } + + set onIOPub(cb: (msg: KernelMessage.IIOPubMessage) => void | PromiseLike) { + this.ioPub = cb; + } + get onReply(): (msg: REPLY) => void | PromiseLike { + return this.reply; + } + + set onReply(handler: (msg: REPLY) => void | PromiseLike) { + this.reply = handler; + } + + // Handle a new message passed from the kernel + public async handleMessage(message: KernelMessage.IMessage): Promise { + switch (message.channel) { + case 'stdin': + await this.handleStdIn(message as KernelMessage.IStdinMessage); + break; + case 'iopub': + await this.handleIOPub(message as KernelMessage.IIOPubMessage); + break; + case 'control': + case 'shell': + await this.handleShellControl(message as KernelMessage.IShellControlMessage); + break; + default: + break; + } + } + + public dispose(): void { + if (!this.isDisposed) { + // First clear out our handlers + this.stdIn = noop; + this.ioPub = noop; + this.reply = noop; + + // Reject our done promise + this.donePromise.reject(new Error('Disposed Future')); + this.isDisposed = true; + } + } + + // RAWKERNEL: Not Implemented + public registerMessageHook(_hook: (msg: KernelMessage.IIOPubMessage) => boolean | PromiseLike): void { + throw new Error('Not yet implemented'); + } + public removeMessageHook(_hook: (msg: KernelMessage.IIOPubMessage) => boolean | PromiseLike): void { + throw new Error('Not yet implemented'); + } + public sendInputReply(_content: KernelMessage.IInputReplyMsg['content']): void { + throw new Error('Not yet implemented'); + } + + // Private Functions + + // Functions for handling specific message types + private async handleStdIn(message: KernelMessage.IStdinMessage): Promise { + // Call our handler for stdin, might just be noop + // RAWKERNEL: same channel type string != 'stdin' cast issue + // tslint:disable-next-line:no-any + await this.stdIn(message); + } + + private async handleIOPub(message: KernelMessage.IIOPubMessage): Promise { + // RAWKERNEL: Check hooks process first? + // tslint:disable-next-line:no-any + await this.ioPub(message); + + // If we get an idle status message and a reply then we are done + if (KernelMessage.isStatusMsg(message) && message.content.execution_state === 'idle') { + this.idleSeen = true; + + if (this.replyMessage) { + this.handleDone(); + } + } + } + + private async handleShellControl(message: KernelMessage.IShellControlMessage): Promise { + if (message.channel === this.msg.channel && message.parent_header) { + const parentHeader = message.parent_header as KernelMessage.IHeader; + if (parentHeader.msg_id === this.msg.header.msg_id) { + await this.handleReply(message as REPLY); + } + } + } + + private async handleReply(message: REPLY): Promise { + await this.reply(message); + + this.replyMessage = message; + + // If we've gotten an idle status message we are done now + if (this.idleSeen) { + this.handleDone(); + } + } + + private handleDone(): void { + this.donePromise.resolve(this.replyMessage); + + if (this.disposeOnDone) { + this.dispose(); + } + } +} diff --git a/src/client/datascience/raw-kernel/rawKernel.ts b/src/client/datascience/raw-kernel/rawKernel.ts new file mode 100644 index 000000000000..5e204b637f72 --- /dev/null +++ b/src/client/datascience/raw-kernel/rawKernel.ts @@ -0,0 +1,268 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import { Kernel, KernelMessage, ServerConnection } from '@jupyterlab/services'; +import { JSONObject } from '@phosphor/coreutils'; +import { ISignal } from '@phosphor/signaling'; +import * as uuid from 'uuid/v4'; +import { IJMPConnection, IJMPConnectionInfo } from '../types'; +import { RawFuture } from './rawFuture'; + +/* +RawKernel class represents the mapping from the JupyterLab services IKernel interface +to a raw IPython kernel running on the local machine. RawKernel is in charge of taking +input request, translating them, sending them to an IPython kernel over ZMQ, then passing back the messages +*/ +export class RawKernel implements Kernel.IKernel { + // IKernel properties + get terminated(): ISignal { + throw new Error('Not yet implemented'); + } + get statusChanged(): ISignal { + throw new Error('Not yet implemented'); + } + get iopubMessage(): ISignal { + throw new Error('Not yet implemented'); + } + get unhandledMessage(): ISignal { + throw new Error('Not yet implemented'); + } + get anyMessage(): ISignal { + throw new Error('Not yet implemented'); + } + get serverSettings(): ServerConnection.ISettings { + throw new Error('Not yet implemented'); + } + + // IKernelConnection properties + get id(): string { + throw new Error('Not yet implemented'); + } + get name(): string { + throw new Error('Not yet implemented'); + } + get model(): Kernel.IModel { + throw new Error('Not yet implemented'); + } + get username(): string { + throw new Error('Not yet implemented'); + } + get clientId(): string { + throw new Error('Not yet implemented'); + } + get status(): Kernel.Status { + throw new Error('Not yet implemented'); + } + get info(): KernelMessage.IInfoReply | null { + throw new Error('Not yet implemented'); + } + get isReady(): boolean { + throw new Error('Not yet implemented'); + } + get ready(): Promise { + throw new Error('Not yet implemented'); + } + get handleComms(): boolean { + throw new Error('Not yet implemented'); + } + + public isDisposed: boolean = false; + private jmpConnection: IJMPConnection; + private sessionId: string | undefined; + + // Keep track of all of our active futures + private futures = new Map< + string, + RawFuture + >(); + + // JMP connection should be injected, but no need to yet until it actually exists + constructor(connection: IJMPConnection) { + this.jmpConnection = connection; + } + + public async connect(connectInfo: IJMPConnectionInfo) { + this.sessionId = uuid(); + await this.jmpConnection.connect(connectInfo, this.sessionId); + this.jmpConnection.subscribe(msg => { + this.handleMessage(msg).ignoreErrors(); + }); + } + + public requestExecute( + content: KernelMessage.IExecuteRequestMsg['content'], + disposeOnDone?: boolean, + _metadata?: JSONObject + ): Kernel.IShellFuture { + if (this.jmpConnection && this.sessionId) { + // Build our execution message + // Silent is supposed to be options, but in my testing the message was not passing + // correctly without it, so specifying it here with default false + const executeOptions: KernelMessage.IOptions = { + session: this.sessionId, + channel: 'shell', + msgType: 'execute_request', + username: 'vscode', + content: { ...content, silent: content.silent || false } + }; + const executeMessage = KernelMessage.createMessage(executeOptions); + + // Send off our message to our jmp connection + this.jmpConnection.sendMessage(executeMessage); + + // Create a future to watch for reply messages + const newFuture = new RawFuture( + executeMessage, + disposeOnDone || true + ); + this.futures.set( + newFuture.msg.header.msg_id, + newFuture as RawFuture + ); + + // Set our future to remove itself when disposed + const oldDispose = newFuture.dispose.bind(newFuture); + newFuture.dispose = () => { + this.futures.delete(newFuture.msg.header.msg_id); + return oldDispose(); + }; + + return newFuture; + } + + // RAWKERNEL: What should we do here? Throw? + // Probably should not get here if session is not available + throw new Error('No session available?'); + } + + // On dispose close down our connection and get rid of saved futures + public dispose(): void { + if (!this.isDisposed) { + if (this.jmpConnection) { + this.jmpConnection.dispose(); + } + + // Dispose of all our outstanding futures + this.futures.forEach(future => { + future.dispose(); + }); + this.futures.clear(); + + this.isDisposed = true; + } + } + public shutdown(): Promise { + throw new Error('Not yet implemented'); + } + public getSpec(): Promise { + throw new Error('Not yet implemented'); + } + public sendShellMessage( + _msg: KernelMessage.IShellMessage, + _expectReply?: boolean, + _disposeOnDone?: boolean + ): Kernel.IShellFuture> { + throw new Error('Not yet implemented'); + } + public sendControlMessage( + _msg: KernelMessage.IControlMessage, + _expectReply?: boolean, + _disposeOnDone?: boolean + ): Kernel.IControlFuture> { + throw new Error('Not yet implemented'); + } + public reconnect(): Promise { + throw new Error('Not yet implemented'); + } + public interrupt(): Promise { + throw new Error('Not yet implemented'); + } + public restart(): Promise { + throw new Error('Not yet implemented'); + } + public requestKernelInfo(): Promise { + throw new Error('Not yet implemented'); + } + public requestComplete( + _content: KernelMessage.ICompleteRequestMsg['content'] + ): Promise { + throw new Error('Not yet implemented'); + } + public requestInspect( + _content: KernelMessage.IInspectRequestMsg['content'] + ): Promise { + throw new Error('Not yet implemented'); + } + public requestHistory( + _content: KernelMessage.IHistoryRequestMsg['content'] + ): Promise { + throw new Error('Not yet implemented'); + } + public requestDebug( + _content: KernelMessage.IDebugRequestMsg['content'], + _disposeOnDone?: boolean + ): Kernel.IControlFuture { + throw new Error('Not yet implemented'); + } + public requestIsComplete( + _content: KernelMessage.IIsCompleteRequestMsg['content'] + ): Promise { + throw new Error('Not yet implemented'); + } + public requestCommInfo( + _content: KernelMessage.ICommInfoRequestMsg['content'] + ): Promise { + throw new Error('Not yet implemented'); + } + public sendInputReply(_content: KernelMessage.IInputReplyMsg['content']): void { + throw new Error('Not yet implemented'); + } + public connectToComm(_targetName: string, _commId?: string): Kernel.IComm { + throw new Error('Not yet implemented'); + } + public registerCommTarget( + _targetName: string, + _callback: (comm: Kernel.IComm, _msg: KernelMessage.ICommOpenMsg) => void | PromiseLike + ): void { + throw new Error('Not yet implemented'); + } + public removeCommTarget( + _targetName: string, + _callback: (comm: Kernel.IComm, _msg: KernelMessage.ICommOpenMsg) => void | PromiseLike + ): void { + throw new Error('Not yet implemented'); + } + public registerMessageHook( + _msgId: string, + _hook: (_msg: KernelMessage.IIOPubMessage) => boolean | PromiseLike + ): void { + throw new Error('Not yet implemented'); + } + public removeMessageHook( + _msgId: string, + _hook: (_msg: KernelMessage.IIOPubMessage) => boolean | PromiseLike + ): void { + throw new Error('Not yet implemented'); + } + + // Handle a new message arriving from JMP connection + private async handleMessage(message: KernelMessage.IMessage): Promise { + // RAWKERNEL: display_data messages can route based on their id here first + + // Look up in our future list and see if a future needs to be updated on this message + if (message.parent_header) { + const parentHeader = message.parent_header as KernelMessage.IHeader; + const parentFuture = this.futures.get(parentHeader.msg_id); + + if (parentFuture) { + // Let the parent future message handle it here + await parentFuture.handleMessage(message); + } else { + if (message.header.session === this.sessionId && message.channel !== 'iopub') { + // RAWKERNEL: emit unhandled + } + } + } + + // RAWKERNEL: Handle general IOpub messages + } +} diff --git a/src/test/datascience/execution.unit.test.ts b/src/test/datascience/execution.unit.test.ts index e9f8fd4f2977..861d8b2efdff 100644 --- a/src/test/datascience/execution.unit.test.ts +++ b/src/test/datascience/execution.unit.test.ts @@ -769,7 +769,7 @@ suite('Jupyter Execution', async () => { when(interpreterService.getInterpreterDetails(match('/foo/baz/python.exe'))).thenResolve(missingKernelPython); when(interpreterService.getInterpreterDetails(match('/bar/baz/python.exe'))).thenResolve(missingNotebookPython); when(interpreterService.getInterpreterDetails(argThat(o => !o.includes || !o.includes('python')))).thenReject( - 'Unknown interpreter' + ('Unknown interpreter' as any) as Error ); if (runInDocker) { when(fileSystem.readFile('/proc/self/cgroup')).thenResolve('hello docker world'); diff --git a/src/test/datascience/interactiveWindowCommandListener.unit.test.ts b/src/test/datascience/interactiveWindowCommandListener.unit.test.ts index 45b0bafc04d1..da3ed170ce08 100644 --- a/src/test/datascience/interactiveWindowCommandListener.unit.test.ts +++ b/src/test/datascience/interactiveWindowCommandListener.unit.test.ts @@ -107,7 +107,7 @@ suite('Interactive window command listener', async () => { // Setup defaults when(interpreterService.onDidChangeInterpreter).thenReturn(dummyEvent.event); when(interpreterService.getInterpreterDetails(argThat(o => !o.includes || !o.includes('python')))).thenReject( - 'Unknown interpreter' + ('Unknown interpreter' as any) as Error ); // Service container needs logger, file system, and config service diff --git a/src/test/datascience/jupyter/jupyterSession.unit.test.ts b/src/test/datascience/jupyter/jupyterSession.unit.test.ts index 0789f5ad70c0..1c52edb028ca 100644 --- a/src/test/datascience/jupyter/jupyterSession.unit.test.ts +++ b/src/test/datascience/jupyter/jupyterSession.unit.test.ts @@ -1,8 +1,14 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { ContentsManager, Kernel, ServerConnection, Session, SessionManager } from '@jupyterlab/services'; +import { + ContentsManager, + Kernel, + KernelMessage, + ServerConnection, + Session, + SessionManager +} from '@jupyterlab/services'; import { DefaultKernel } from '@jupyterlab/services/lib/kernel/default'; -import { KernelFutureHandler } from '@jupyterlab/services/lib/kernel/future'; import { DefaultSession } from '@jupyterlab/services/lib/session/default'; import { ISignal, Signal } from '@phosphor/commands/node_modules/@phosphor/signaling'; import { assert } from 'chai'; @@ -321,7 +327,9 @@ suite('Data Science - JupyterSession', () => { setup(executeUserCode); async function executeUserCode() { - const future = mock(KernelFutureHandler); + const future = mock< + Kernel.IFuture + >(); // tslint:disable-next-line: no-any when(future.done).thenReturn(Promise.resolve(undefined as any)); // tslint:disable-next-line: no-any diff --git a/src/test/datascience/raw-kernel/rawFuture.unit.test.ts b/src/test/datascience/raw-kernel/rawFuture.unit.test.ts new file mode 100644 index 000000000000..bc1c2532d199 --- /dev/null +++ b/src/test/datascience/raw-kernel/rawFuture.unit.test.ts @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import { KernelMessage } from '@jupyterlab/services'; +import { expect } from 'chai'; +import * as uuid from 'uuid/v4'; +import { RawFuture } from '../../../client/datascience/raw-kernel/rawFuture'; + +// tslint:disable: max-func-body-length +suite('Data Science - RawFuture', () => { + let rawFuture: RawFuture; + let executeMessage: KernelMessage.IExecuteRequestMsg; + let sessionID: string; + + setup(() => { + sessionID = uuid(); + // Create an execute request message + const executeOptions: KernelMessage.IOptions = { + session: sessionID, + channel: 'shell', + msgType: 'execute_request', + username: 'vscode', + content: { code: 'print("hello world")' } + }; + executeMessage = KernelMessage.createMessage(executeOptions); + rawFuture = new RawFuture(executeMessage, true); + }); + + test('Check our reply message channel', async () => { + const replyOptions: KernelMessage.IOptions = { + channel: 'shell', + session: sessionID, + msgType: 'execute_reply', + content: { status: 'ok', execution_count: 1, payload: [], user_expressions: {} } + }; + const replyMessage = KernelMessage.createMessage(replyOptions); + replyMessage.parent_header = executeMessage.header; + + // Verify that the reply message matches the one we sent + rawFuture.onReply = msg => { + expect(msg.header.msg_id).to.equal(replyMessage.header.msg_id); + }; + + await rawFuture.handleMessage(replyMessage); + + // Now take the same message and mangle the parent header, + // This message should not be sent as it doesn't match the request + replyMessage.header.msg_id = uuid(); + replyMessage.parent_header.msg_id = 'junk'; + + await rawFuture.handleMessage(replyMessage); + }); + + test('Check our IOPub message channel', async () => { + const ioPubMessageOptions: KernelMessage.IOptions = { + session: sessionID, + msgType: 'stream', + channel: 'iopub', + content: { name: 'stdout', text: 'hello' } + }; + const ioPubMessage = KernelMessage.createMessage(ioPubMessageOptions); + ioPubMessage.parent_header = executeMessage.header; + + // Verify that the iopub message matches the one we sent + rawFuture.onIOPub = msg => { + expect(msg.header.msg_id).to.equal(ioPubMessage.header.msg_id); + }; + + await rawFuture.handleMessage(ioPubMessage); + }); +}); diff --git a/src/test/datascience/raw-kernel/rawKernel.unit.test.ts b/src/test/datascience/raw-kernel/rawKernel.unit.test.ts new file mode 100644 index 000000000000..2489eebf1ecc --- /dev/null +++ b/src/test/datascience/raw-kernel/rawKernel.unit.test.ts @@ -0,0 +1,95 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import { KernelMessage } from '@jupyterlab/services'; +import { assert, expect } from 'chai'; +import { anything, deepEqual, instance, mock, verify, when } from 'ts-mockito'; +import { RawKernel } from '../../../client/datascience/raw-kernel/rawKernel'; +import { IJMPConnection, IJMPConnectionInfo } from '../../../client/datascience/types'; + +// tslint:disable: max-func-body-length +suite('Data Science - RawKernel', () => { + let rawKernel: RawKernel; + let jmpConnection: IJMPConnection; + let connectInfo: IJMPConnectionInfo; + + setup(() => { + jmpConnection = mock(); + when(jmpConnection.connect(anything(), anything())).thenResolve(); + when(jmpConnection.subscribe(anything())).thenReturn(); + rawKernel = new RawKernel(instance(jmpConnection)); + + connectInfo = { + version: 0, + transport: 'tcp', + ip: '127.0.0.1', + shell_port: 55196, + iopub_port: 55197, + stdin_port: 55198, + hb_port: 55200, + control_port: 55199, + signature_scheme: 'hmac-sha256', + key: 'adaf9032-487d222a85026db284c3d5e7' + }; + }); + + test('RawKernel connect should connect and subscribe to JMP', async () => { + await rawKernel.connect(connectInfo); + verify(jmpConnection.connect(deepEqual(connectInfo), anything())).once(); + verify(jmpConnection.subscribe(anything())).once(); + }); + + test('RawKernel dispose should dispose the jmp', async () => { + when(jmpConnection.dispose()).thenReturn(); + + await rawKernel.connect(connectInfo); + + // Dispose our kernel + rawKernel.dispose(); + + verify(jmpConnection.dispose()).once(); + assert.isTrue(rawKernel.isDisposed); + }); + + test('RawKernel requestExecute should pass a valid execute message to JMP', async () => { + when(jmpConnection.sendMessage(anything())).thenReturn(); + + await rawKernel.connect(connectInfo); + + const code = 'print("hello world")'; + const executeContent: KernelMessage.IExecuteRequestMsg['content'] = { + code + }; + const future = rawKernel.requestExecute(executeContent, true, undefined); + + // Verify that we sent a message to jmp + verify(jmpConnection.sendMessage(anything())).once(); + + // We don't need a detailed verification on the jmp message sent, as that same + // message is set in the future which we can examine now + expect(future.msg.header.msg_type).to.equal('execute_request'); + expect(future.msg.channel).to.equal('shell'); + expect(future.msg.content.code).to.equal(code); + }); + + test('RawKernel dispose should also dispose of any futures', async () => { + when(jmpConnection.sendMessage(anything())).thenReturn(); + when(jmpConnection.dispose()).thenReturn(); + + await rawKernel.connect(connectInfo); + + const code = 'print("hello world")'; + const executeContent: KernelMessage.IExecuteRequestMsg['content'] = { + code + }; + const future = rawKernel.requestExecute(executeContent, true, undefined); + future.done.catch(reason => { + const error = reason as Error; + expect(error.message).to.equal('Disposed Future'); + }); + + // Dispose the rawKernel, the done promise on the future should reject with an Error + rawKernel.dispose(); + + expect(future.isDisposed).to.equal(true, 'Future was not disposed on RawKernel dispose'); + }); +}); diff --git a/src/test/linters/linterCommands.unit.test.ts b/src/test/linters/linterCommands.unit.test.ts index 3595f6e6837b..4f7259fd41d7 100644 --- a/src/test/linters/linterCommands.unit.test.ts +++ b/src/test/linters/linterCommands.unit.test.ts @@ -12,6 +12,7 @@ import { CommandManager } from '../../client/common/application/commandManager'; import { DocumentManager } from '../../client/common/application/documentManager'; import { IApplicationShell, ICommandManager, IDocumentManager } from '../../client/common/application/types'; import { Commands } from '../../client/common/constants'; +import { Product } from '../../client/common/types'; import { ServiceContainer } from '../../client/ioc/container'; import { LinterCommands } from '../../client/linters/linterCommands'; import { LinterManager } from '../../client/linters/linterManager'; @@ -163,6 +164,6 @@ suite('Linting - Linter Commands', () => { verify(shell.showWarningMessage(anything(), 'Yes', 'No')).once(); const quickPickOptions = capture(shell.showQuickPick).last()[1]; expect(quickPickOptions).to.deep.equal(expectedQuickPickOptions); - verify(manager.setActiveLintersAsync(deepEqual(['Three']), anything())).once(); + verify(manager.setActiveLintersAsync(deepEqual([('Three' as any) as Product]), anything())).once(); }); }); diff --git a/src/test/testing/pytest/services/discoveryService.unit.test.ts b/src/test/testing/pytest/services/discoveryService.unit.test.ts index fb2d71a7ffe5..8e9907a31a3a 100644 --- a/src/test/testing/pytest/services/discoveryService.unit.test.ts +++ b/src/test/testing/pytest/services/discoveryService.unit.test.ts @@ -91,11 +91,13 @@ suite('Unit Tests - PyTest - Discovery', () => { throw new Error('Unrecognized directory'); }; when(argsService.getTestFolders(deepEqual(options.args))).thenReturn(directories); - when(helper.mergeTests(deepEqual(['Result A', 'Result B']))).thenReturn('mergedTests' as any); + when(helper.mergeTests(deepEqual([('Result A' as any) as Tests, ('Result B' as any) as Tests]))).thenReturn( + 'mergedTests' as any + ); const tests = await discoveryService.discoverTests(options); - verify(helper.mergeTests(deepEqual(['Result A', 'Result B']))).once(); + verify(helper.mergeTests(deepEqual([('Result A' as any) as Tests, ('Result B' as any) as Tests]))).once(); expect(tests).equal('mergedTests'); }); test('Build collection arguments', async () => {