Skip to content

Commit 71bf20d

Browse files
authored
Mock jupyter apis such that they can be faked to have tests run more reliably (microsoft#3853)
* Implement jupyter session wrapping * Fix remote test to not leave a dangling session * Fix dispose problems in functional tests * Add news entry * Fix guid regex
1 parent 6e0e28d commit 71bf20d

30 files changed

+1661
-429
lines changed

news/3 Code Health/3556.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add mock of Jupyter API to allow functional tests to run more quickly and more consistently.

src/client/datascience/editor-integration/codewatcher.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import * as localize from '../../common/utils/localize';
1010
import { captureTelemetry } from '../../telemetry';
1111
import { generateCellRanges } from '../cellFactory';
1212
import { Commands, Telemetry } from '../constants';
13-
import { JupyterInstallError } from '../jupyterInstallError';
13+
import { JupyterInstallError } from '../jupyter/jupyterInstallError';
1414
import { ICodeWatcher, IHistoryProvider } from '../types';
1515

1616
@injectable()

src/client/datascience/history.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,12 @@ import { EXTENSION_ROOT_DIR } from '../common/constants';
2525
import { ContextKey } from '../common/contextKey';
2626
import { IFileSystem } from '../common/platform/types';
2727
import { IConfigurationService, IDisposableRegistry, ILogger } from '../common/types';
28+
import { createDeferred } from '../common/utils/async';
2829
import * as localize from '../common/utils/localize';
2930
import { IInterpreterService } from '../interpreter/contracts';
3031
import { captureTelemetry, sendTelemetryEvent } from '../telemetry';
3132
import { EditorContexts, HistoryMessages, Settings, Telemetry } from './constants';
32-
import { JupyterInstallError } from './jupyterInstallError';
33+
import { JupyterInstallError } from './jupyter/jupyterInstallError';
3334
import {
3435
CellState,
3536
ICell,
@@ -112,6 +113,14 @@ export class History implements IWebPanelMessageListener, IHistory {
112113
// Start a status item
113114
const status = this.setStatus(localize.DataScience.executingCode());
114115

116+
// Create a deferred object that will wait until the status is disposed
117+
const finishedAddingCode = createDeferred<void>();
118+
const actualDispose = status.dispose;
119+
status.dispose = () => {
120+
finishedAddingCode.resolve();
121+
actualDispose();
122+
};
123+
115124
try {
116125

117126
// Make sure we're loaded first.
@@ -151,6 +160,9 @@ export class History implements IWebPanelMessageListener, IHistory {
151160
// Indicate executing until this cell is done.
152161
status.dispose();
153162
});
163+
164+
// Wait for the cell to finish
165+
await finishedAddingCode.promise;
154166
}
155167
} catch (err) {
156168
status.dispose();

src/client/datascience/historycommandlistener.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -141,13 +141,14 @@ export class HistoryCommandListener implements IDataScienceCommandListener {
141141

142142
// When all done, show a notice that it completed.
143143
const openQuestion = localize.DataScience.exportOpenQuestion();
144-
this.applicationShell.showInformationMessage(localize.DataScience.exportDialogComplete().format(uri.fsPath), openQuestion).then((str: string | undefined) => {
145-
if (str === openQuestion) {
146-
// If the user wants to, open the notebook they just generated.
147-
this.jupyterExecution.spawnNotebook(uri.fsPath).ignoreErrors();
148-
}
149-
});
150-
144+
if (uri && uri.fsPath) {
145+
this.applicationShell.showInformationMessage(localize.DataScience.exportDialogComplete().format(uri.fsPath), openQuestion).then((str: string | undefined) => {
146+
if (str === openQuestion) {
147+
// If the user wants to, open the notebook they just generated.
148+
this.jupyterExecution.spawnNotebook(uri.fsPath).ignoreErrors();
149+
}
150+
});
151+
}
151152
}
152153
}
153154
}

src/client/datascience/jupyterConnectError.ts renamed to src/client/datascience/jupyter/jupyterConnectError.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT License.
33
'use strict';
4-
import '../common/extensions';
4+
import '../../common/extensions';
55

66
export class JupyterConnectError extends Error {
77
constructor(message: string, stderr?: string) {

src/client/datascience/jupyterConnection.ts renamed to src/client/datascience/jupyter/jupyterConnection.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,15 @@
44
import * as path from 'path';
55
import { CancellationToken, Disposable } from 'vscode-jsonrpc';
66

7-
import { CancellationError } from '../common/cancellation';
8-
import { IFileSystem } from '../common/platform/types';
9-
import { ObservableExecutionResult, Output } from '../common/process/types';
10-
import { IConfigurationService, ILogger } from '../common/types';
11-
import { createDeferred, Deferred } from '../common/utils/async';
12-
import * as localize from '../common/utils/localize';
13-
import { IServiceContainer } from '../ioc/types';
7+
import { CancellationError } from '../../common/cancellation';
8+
import { IFileSystem } from '../../common/platform/types';
9+
import { ObservableExecutionResult, Output } from '../../common/process/types';
10+
import { IConfigurationService, ILogger } from '../../common/types';
11+
import { createDeferred, Deferred } from '../../common/utils/async';
12+
import * as localize from '../../common/utils/localize';
13+
import { IServiceContainer } from '../../ioc/types';
1414
import { JupyterConnectError } from './jupyterConnectError';
15-
import { IConnection } from './types';
15+
import { IConnection } from '../types';
1616

1717
const UrlPatternRegEx = /(https?:\/\/[^\s]+)/ ;
1818
const HttpPattern = /https?:\/\//;

src/client/datascience/jupyterExecution.ts renamed to src/client/datascience/jupyter/jupyterExecution.ts

Lines changed: 34 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -10,33 +10,34 @@ import { URL } from 'url';
1010
import * as uuid from 'uuid/v4';
1111
import { CancellationToken, Disposable } from 'vscode-jsonrpc';
1212

13-
import { Cancellation, CancellationError } from '../common/cancellation';
14-
import { IS_WINDOWS } from '../common/platform/constants';
15-
import { IFileSystem, TemporaryDirectory } from '../common/platform/types';
13+
import { Cancellation, CancellationError } from '../../common/cancellation';
14+
import { IS_WINDOWS } from '../../common/platform/constants';
15+
import { IFileSystem, TemporaryDirectory } from '../../common/platform/types';
1616
import {
1717
ExecutionResult,
1818
IProcessService,
1919
IProcessServiceFactory,
2020
IPythonExecutionFactory,
2121
ObservableExecutionResult,
2222
SpawnOptions
23-
} from '../common/process/types';
24-
import { IAsyncDisposableRegistry, IDisposableRegistry, ILogger } from '../common/types';
25-
import * as localize from '../common/utils/localize';
26-
import { noop } from '../common/utils/misc';
27-
import { EXTENSION_ROOT_DIR } from '../constants';
23+
} from '../../common/process/types';
24+
import { IAsyncDisposableRegistry, IDisposableRegistry, ILogger } from '../../common/types';
25+
import * as localize from '../../common/utils/localize';
26+
import { noop } from '../../common/utils/misc';
27+
import { EXTENSION_ROOT_DIR } from '../../constants';
2828
import {
2929
ICondaService,
3030
IInterpreterService,
3131
IKnownSearchPathsForInterpreters,
3232
InterpreterType,
3333
PythonInterpreter
34-
} from '../interpreter/contracts';
35-
import { IServiceContainer } from '../ioc/types';
36-
import { captureTelemetry } from '../telemetry';
37-
import { Telemetry } from './constants';
34+
} from '../../interpreter/contracts';
35+
import { IServiceContainer } from '../../ioc/types';
36+
import { captureTelemetry } from '../../telemetry';
37+
import { Telemetry } from '../constants';
3838
import { JupyterConnection, JupyterServerInfo } from './jupyterConnection';
39-
import { IConnection, IJupyterExecution, IJupyterKernelSpec, INotebookServer } from './types';
39+
import { IConnection, IJupyterExecution, IJupyterKernelSpec, INotebookServer, IJupyterSessionManager } from '../types';
40+
import { JupyterKernelSpec } from './jupyterKernelSpec';
4041

4142
const CheckJupyterRegEx = IS_WINDOWS ? /^jupyter?\.exe$/ : /^jupyter?$/;
4243
const NotebookCommand = 'notebook';
@@ -45,33 +46,6 @@ const KernelSpecCommand = 'kernelspec';
4546
const KernelCreateCommand = 'ipykernel';
4647
const PyKernelOutputRegEx = /.*\s+(.+)$/m;
4748
const KernelSpecOutputRegEx = /^\s*(\S+)\s+(\S+)$/;
48-
const IsGuidRegEx = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
49-
50-
class JupyterKernelSpec implements IJupyterKernelSpec {
51-
public name: string;
52-
public language: string;
53-
public path: string;
54-
public specFile: string | undefined;
55-
constructor(specModel: Kernel.ISpecModel, file?: string) {
56-
this.name = specModel.name;
57-
this.language = specModel.language;
58-
this.path = specModel.argv.length > 0 ? specModel.argv[0] : '';
59-
this.specFile = file;
60-
}
61-
public dispose = async () => {
62-
if (this.specFile &&
63-
IsGuidRegEx.test(path.basename(path.dirname(this.specFile)))) {
64-
// There is more than one location for the spec file directory
65-
// to be cleaned up. If one fails, the other likely deleted it already.
66-
try {
67-
await fs.remove(path.dirname(this.specFile));
68-
} catch {
69-
noop();
70-
}
71-
this.specFile = undefined;
72-
}
73-
}
74-
}
7549

7650
// JupyterCommand objects represent some process that can be launched that should be guaranteed to work because it
7751
// was found by testing it previously
@@ -155,21 +129,22 @@ export class JupyterExecution implements IJupyterExecution, Disposable {
155129
private usablePythonInterpreter: PythonInterpreter | undefined;
156130

157131
constructor(@inject(IPythonExecutionFactory) private executionFactory: IPythonExecutionFactory,
158-
@inject(ICondaService) private condaService: ICondaService,
159-
@inject(IInterpreterService) private interpreterService: IInterpreterService,
160-
@inject(IProcessServiceFactory) private processServiceFactory: IProcessServiceFactory,
161-
@inject(IKnownSearchPathsForInterpreters) private knownSearchPaths: IKnownSearchPathsForInterpreters,
162-
@inject(ILogger) private logger: ILogger,
163-
@inject(IDisposableRegistry) private disposableRegistry: IDisposableRegistry,
164-
@inject(IAsyncDisposableRegistry) private asyncRegistry: IAsyncDisposableRegistry,
165-
@inject(IFileSystem) private fileSystem: IFileSystem,
166-
@inject(IServiceContainer) private serviceContainer: IServiceContainer) {
132+
@inject(ICondaService) private condaService: ICondaService,
133+
@inject(IInterpreterService) private interpreterService: IInterpreterService,
134+
@inject(IProcessServiceFactory) private processServiceFactory: IProcessServiceFactory,
135+
@inject(IKnownSearchPathsForInterpreters) private knownSearchPaths: IKnownSearchPathsForInterpreters,
136+
@inject(ILogger) private logger: ILogger,
137+
@inject(IDisposableRegistry) private disposableRegistry: IDisposableRegistry,
138+
@inject(IAsyncDisposableRegistry) private asyncRegistry: IAsyncDisposableRegistry,
139+
@inject(IFileSystem) private fileSystem: IFileSystem,
140+
@inject(IJupyterSessionManager) private sessionManager: IJupyterSessionManager,
141+
@inject(IServiceContainer) private serviceContainer: IServiceContainer) {
167142
this.processServicePromise = this.processServiceFactory.create();
168-
this.disposableRegistry.push(this.interpreterService.onDidChangeInterpreter(this.onSettingsChanged));
143+
this.disposableRegistry.push(this.interpreterService.onDidChangeInterpreter(() => this.onSettingsChanged()));
169144
this.disposableRegistry.push(this);
170145
}
171146

172-
public dispose = () => {
147+
public dispose() {
173148
// Clear our usableJupyterInterpreter
174149
this.usablePythonInterpreter = undefined;
175150
this.commands = {};
@@ -241,7 +216,6 @@ export class JupyterExecution implements IJupyterExecution, Disposable {
241216

242217
// Try to connect to our jupyter process
243218
const result = this.serviceContainer.get<INotebookServer>(INotebookServer);
244-
this.disposableRegistry.push(result);
245219
await result.connect(connection, kernelSpec, cancelToken, workingDir);
246220
return result;
247221
} catch (err) {
@@ -298,7 +272,7 @@ export class JupyterExecution implements IJupyterExecution, Disposable {
298272
}
299273

300274
// Now enumerate them again
301-
const enumerator = connection ? () => this.getActiveKernelSpecs(connection) : () => this.enumerateSpecs(cancelToken);
275+
const enumerator = connection ? () => this.sessionManager.getActiveKernelSpecs(connection) : () => this.enumerateSpecs(cancelToken);
302276

303277
// Then find our match
304278
return this.findSpecMatch(enumerator);
@@ -365,8 +339,10 @@ export class JupyterExecution implements IJupyterExecution, Disposable {
365339
const launchResult = await notebookCommand.execObservable(args, { throwOnStdErr: false, encoding: 'utf8', token: cancelToken });
366340

367341
// Make sure this process gets cleaned up. We might be canceled before the connection finishes.
368-
if (launchResult) {
369-
this.asyncRegistry.push({ dispose: () => Promise.resolve(launchResult.dispose()) });
342+
if (launchResult && cancelToken) {
343+
cancelToken.onCancellationRequested(() => {
344+
launchResult.dispose();
345+
});
370346
}
371347

372348
// Wait for the connection information on this result
@@ -475,10 +451,10 @@ export class JupyterExecution implements IJupyterExecution, Disposable {
475451
return undefined;
476452
}
477453

478-
private onSettingsChanged = (): Promise<void> => {
454+
private onSettingsChanged() {
479455
// Do the same thing as dispose so that we regenerate
480456
// all of our commands
481-
return Promise.resolve(this.dispose());
457+
this.dispose();
482458
}
483459

484460
private async addMatchingSpec(bestInterpreter: PythonInterpreter, cancelToken?: CancellationToken): Promise<void> {
@@ -684,32 +660,7 @@ export class JupyterExecution implements IJupyterExecution, Disposable {
684660
return bestSpec;
685661
}
686662

687-
private getActiveKernelSpecs = async (connection: IConnection): Promise<IJupyterKernelSpec[]> => {
688-
// Use our connection to create a session manager
689-
const serverSettings = ServerConnection.makeSettings(
690-
{
691-
baseUrl: connection.baseUrl,
692-
token: connection.token,
693-
pageUrl: '',
694-
// A web socket is required to allow token authentication (what if there is no token authentication?)
695-
wsUrl: connection.baseUrl.replace('http', 'ws'),
696-
init: { cache: 'no-store', credentials: 'same-origin' }
697-
});
698-
const sessionManager = new SessionManager({ serverSettings: serverSettings });
699-
700-
// Ask the session manager to refresh its list of kernel specs.
701-
await sessionManager.refreshSpecs();
702-
703-
// Enumerate all of the kernel specs, turning each into a JupyterKernelSpec
704-
const kernelspecs = sessionManager.specs && sessionManager.specs.kernelspecs ? sessionManager.specs.kernelspecs : {};
705-
const keys = Object.keys(kernelspecs);
706-
return keys.map(k => {
707-
const spec = kernelspecs[k];
708-
return new JupyterKernelSpec(spec);
709-
});
710-
}
711-
712-
private async readSpec(kernelSpecOutputLine: string): Promise<JupyterKernelSpec | undefined> {
663+
private async readSpec(kernelSpecOutputLine: string) : Promise<JupyterKernelSpec | undefined> {
713664
const match = KernelSpecOutputRegEx.exec(kernelSpecOutputLine);
714665
if (match && match !== null && match.length > 2) {
715666
// Second match should be our path to the kernel spec

src/client/datascience/jupyterExporter.ts renamed to src/client/datascience/jupyter/jupyterExporter.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@ import * as uuid from 'uuid/v4';
77

88
import * as os from 'os';
99
import * as path from 'path';
10-
import { IWorkspaceService } from '../common/application/types';
11-
import { IFileSystem } from '../common/platform/types';
12-
import { ILogger } from '../common/types';
13-
import * as localize from '../common/utils/localize';
14-
import { noop } from '../common/utils/misc';
15-
import { CodeSnippits, RegExpValues } from './constants';
16-
import { CellState, ICell, IJupyterExecution, INotebookExporter, ISysInfo } from './types';
10+
import { IWorkspaceService } from '../../common/application/types';
11+
import { IFileSystem } from '../../common/platform/types';
12+
import { ILogger } from '../../common/types';
13+
import * as localize from '../../common/utils/localize';
14+
import { noop } from '../../common/utils/misc';
15+
import { CodeSnippits, RegExpValues } from '../constants';
16+
import { CellState, ICell, IJupyterExecution, INotebookExporter, ISysInfo } from '../types';
1717

1818
@injectable()
1919
export class JupyterExporter implements INotebookExporter {

0 commit comments

Comments
 (0)