Notebooks: Adding Change Kernel API, 3 Integration Tests (#5287)

* Notebook change kernel

* Fix notifying of k change too many times add tests

* Fix broken unit test

* Deal with comment
This commit is contained in:
Chris LaFreniere
2019-06-03 14:49:40 -07:00
committed by GitHub
parent 4b6214c9a4
commit 8d70544374
10 changed files with 130 additions and 17 deletions

View File

@@ -10,7 +10,7 @@ import * as assert from 'assert';
import * as azdata from 'azdata'; import * as azdata from 'azdata';
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import { context } from './testContext'; import { context } from './testContext';
import { sqlNotebookContent, writeNotebookToFile, sqlKernelMetadata, getFileName, pySparkNotebookContent, pySpark3KernelMetadata, pythonKernelMetadata, sqlNotebookMultipleCellsContent, notebookContentForCellLanguageTest } from './notebook.util'; import { sqlNotebookContent, writeNotebookToFile, sqlKernelMetadata, getFileName, pySparkNotebookContent, pySpark3KernelMetadata, pythonKernelMetadata, sqlNotebookMultipleCellsContent, notebookContentForCellLanguageTest, sqlKernelSpec, pythonKernelSpec, pySpark3KernelSpec } from './notebook.util';
import { getBdcServer, getConfigValue, EnvironmentVariable_PYTHON_PATH } from './testConfig'; import { getBdcServer, getConfigValue, EnvironmentVariable_PYTHON_PATH } from './testConfig';
import { connectToServer } from './utils'; import { connectToServer } from './utils';
import * as fs from 'fs'; import * as fs from 'fs';
@@ -56,6 +56,18 @@ if (context.RunTest) {
test('python language test', async function () { test('python language test', async function () {
await (new NotebookTester()).pythonLanguageTest(this.test.title); await (new NotebookTester()).pythonLanguageTest(this.test.title);
}); });
test('Change kernel different provider SQL to Python to SQL', async function () {
await (new NotebookTester()).sqlNbChangeKernelDifferentProviderTest(this.test.title);
});
test('Change kernel different provider Python to SQL to Python', async function () {
await (new NotebookTester()).pythonChangeKernelDifferentProviderTest(this.test.title);
});
test('Change kernel same provider Python to PySpark3 to Python', async function () {
await (new NotebookTester()).pythonChangeKernelSameProviderTest(this.test.title);
});
} }
if (process.env['RUN_PYSPARK_TEST'] === '1') { if (process.env['RUN_PYSPARK_TEST'] === '1') {
@@ -154,6 +166,48 @@ class NotebookTester {
assert(actualOutput2[0] === '1', `Expected result: 1, Actual: '${actualOutput2[0]}'`); assert(actualOutput2[0] === '1', `Expected result: 1, Actual: '${actualOutput2[0]}'`);
} }
async sqlNbChangeKernelDifferentProviderTest(title: string): Promise<void> {
let notebook = await this.openNotebook(sqlNotebookContent, sqlKernelMetadata, title);
assert(notebook.document.providerId === 'sql', `Expected providerId to be sql, Actual: ${notebook.document.providerId}`);
assert(notebook.document.kernelSpec.name === 'SQL', `Expected first kernel name: SQL, Actual: ${notebook.document.kernelSpec.name}`);
let kernelChanged = await notebook.changeKernel(pythonKernelSpec);
assert(notebook.document.providerId === 'jupyter', `Expected providerId to be jupyter, Actual: ${notebook.document.providerId}`);
assert(kernelChanged && notebook.document.kernelSpec.name === 'python3', `Expected second kernel name: python3, Actual: ${notebook.document.kernelSpec.name}`);
kernelChanged = await notebook.changeKernel(sqlKernelSpec);
assert(notebook.document.providerId === 'sql', `Expected providerId to be sql, Actual: ${notebook.document.providerId}`);
assert(kernelChanged && notebook.document.kernelSpec.name === 'SQL', `Expected third kernel name: SQL, Actual: ${notebook.document.kernelSpec.name}`);
}
async pythonChangeKernelDifferentProviderTest(title: string): Promise<void> {
let notebook = await this.openNotebook(pySparkNotebookContent, pythonKernelMetadata, title);
assert(notebook.document.providerId === 'jupyter', `Expected providerId to be jupyter, Actual: ${notebook.document.providerId}`);
assert(notebook.document.kernelSpec.name === 'python3', `Expected first kernel name: python3, Actual: ${notebook.document.kernelSpec.name}`);
let kernelChanged = await notebook.changeKernel(sqlKernelSpec);
assert(notebook.document.providerId === 'sql', `Expected providerId to be sql, Actual: ${notebook.document.providerId}`);
assert(kernelChanged && notebook.document.kernelSpec.name === 'SQL', `Expected second kernel name: SQL, Actual: ${notebook.document.kernelSpec.name}`);
kernelChanged = await notebook.changeKernel(pythonKernelSpec);
assert(notebook.document.providerId === 'jupyter', `Expected providerId to be jupyter, Actual: ${notebook.document.providerId}`);
assert(kernelChanged && notebook.document.kernelSpec.name === 'python3', `Expected third kernel name: python3, Actual: ${notebook.document.kernelSpec.name}`);
}
async pythonChangeKernelSameProviderTest(title: string): Promise<void> {
let notebook = await this.openNotebook(pySparkNotebookContent, pythonKernelMetadata, title);
assert(notebook.document.providerId === 'jupyter', `Expected providerId to be jupyter, Actual: ${notebook.document.providerId}`);
assert(notebook.document.kernelSpec.name === 'python3', `Expected first kernel name: python3, Actual: ${notebook.document.kernelSpec.name}`);
let kernelChanged = await notebook.changeKernel(pySpark3KernelSpec);
assert(notebook.document.providerId === 'jupyter', `Expected providerId to be jupyter, Actual: ${notebook.document.providerId}`);
assert(kernelChanged && notebook.document.kernelSpec.name === 'pyspark3kernel', `Expected second kernel name: pyspark3kernel, Actual: ${notebook.document.kernelSpec.name}`);
kernelChanged = await notebook.changeKernel(pythonKernelSpec);
assert(notebook.document.providerId === 'jupyter', `Expected providerId to be jupyter, Actual: ${notebook.document.providerId}`);
assert(kernelChanged && notebook.document.kernelSpec.name === 'python3', `Expected third kernel name: python3, Actual: ${notebook.document.kernelSpec.name}`);
}
async scalaLanguageTest(title: string): Promise<void> { async scalaLanguageTest(title: string): Promise<void> {
let language = 'scala'; let language = 'scala';
await this.cellLanguageTest(notebookContentForCellLanguageTest, title + this.invocationCount++, language, { await this.cellLanguageTest(notebookContentForCellLanguageTest, title + this.invocationCount++, language, {

View File

@@ -134,6 +134,11 @@ export const pySpark3KernelMetadata = {
} }
}; };
export const pySpark3KernelSpec = {
name: 'pyspark3',
display_name: 'PySpark3'
};
export const sqlKernelMetadata = { export const sqlKernelMetadata = {
'kernelspec': { 'kernelspec': {
'name': 'SQL', 'name': 'SQL',
@@ -141,6 +146,11 @@ export const sqlKernelMetadata = {
} }
}; };
export const sqlKernelSpec: azdata.nb.IKernelSpec = {
name: 'SQL',
display_name: 'SQL'
};
export const pythonKernelMetadata = { export const pythonKernelMetadata = {
'kernelspec': { 'kernelspec': {
'name': 'python3', 'name': 'python3',
@@ -148,6 +158,11 @@ export const pythonKernelMetadata = {
} }
}; };
export const pythonKernelSpec: azdata.nb.IKernelSpec = {
name: 'python3',
display_name: 'Python 3'
};
export function writeNotebookToFile(pythonNotebook: azdata.nb.INotebookContents, testName: string): vscode.Uri { export function writeNotebookToFile(pythonNotebook: azdata.nb.INotebookContents, testName: string): vscode.Uri {
let fileName = getFileName(testName); let fileName = getFileName(testName);
let notebookContentString = JSON.stringify(pythonNotebook); let notebookContentString = JSON.stringify(pythonNotebook);

View File

@@ -4363,6 +4363,11 @@ declare module 'azdata' {
* @return A promise that resolves with a value indicating if the outputs are cleared or not. * @return A promise that resolves with a value indicating if the outputs are cleared or not.
*/ */
clearAllOutputs(): Thenable<boolean>; clearAllOutputs(): Thenable<boolean>;
/**
* Changes the Notebook's kernel. Thenable will resolve only after kernel change is complete.
*/
changeKernel(kernel: IKernelSpec): Thenable<boolean>;
} }
export interface NotebookCell { export interface NotebookCell {

View File

@@ -163,6 +163,10 @@ export class ExtHostNotebookEditor implements azdata.nb.NotebookEditor, IDisposa
return this._proxy.$clearAllOutputs(this._id); return this._proxy.$clearAllOutputs(this._id);
} }
public changeKernel(kernel: azdata.nb.IKernelSpec): Thenable<boolean> {
return this._proxy.$changeKernel(this._id, kernel);
}
public edit(callback: (editBuilder: azdata.nb.NotebookEditorEdit) => void, options?: { undoStopBefore: boolean; undoStopAfter: boolean; }): Thenable<boolean> { public edit(callback: (editBuilder: azdata.nb.NotebookEditorEdit) => void, options?: { undoStopBefore: boolean; undoStopAfter: boolean; }): Thenable<boolean> {
if (this._disposed) { if (this._disposed) {
return Promise.reject(new Error('NotebookEditor#edit not possible on closed editors')); return Promise.reject(new Error('NotebookEditor#edit not possible on closed editors'));

View File

@@ -36,11 +36,14 @@ import { localize } from 'vs/nls';
class MainThreadNotebookEditor extends Disposable { class MainThreadNotebookEditor extends Disposable {
private _contentChangedEmitter = new Emitter<NotebookContentChange>(); private _contentChangedEmitter = new Emitter<NotebookContentChange>();
public readonly contentChanged: Event<NotebookContentChange> = this._contentChangedEmitter.event; public readonly contentChanged: Event<NotebookContentChange> = this._contentChangedEmitter.event;
private _providerInfo: IProviderInfo; private _providerId: string = '';
private _providers: string[] = [];
constructor(public readonly editor: INotebookEditor) { constructor(public readonly editor: INotebookEditor) {
super(); super();
editor.modelReady.then(model => { editor.modelReady.then(model => {
this._providerId = model.providerId;
this._register(model.contentChanged((e) => this._contentChangedEmitter.fire(e))); this._register(model.contentChanged((e) => this._contentChangedEmitter.fire(e)));
this._register(model.kernelChanged((e) => { this._register(model.kernelChanged((e) => {
let changeEvent: NotebookContentChange = { let changeEvent: NotebookContentChange = {
@@ -48,9 +51,12 @@ class MainThreadNotebookEditor extends Disposable {
}; };
this._contentChangedEmitter.fire(changeEvent); this._contentChangedEmitter.fire(changeEvent);
})); }));
this._register(model.onProviderIdChange((provider) => {
this._providerId = provider;
}));
}); });
editor.notebookParams.providerInfo.then(info => { editor.notebookParams.providerInfo.then(info => {
this._providerInfo = info; this._providers = info.providers;
}); });
} }
@@ -67,11 +73,11 @@ class MainThreadNotebookEditor extends Disposable {
} }
public get providerId(): string { public get providerId(): string {
return this._providerInfo ? this._providerInfo.providerId : undefined; return this._providerId;
} }
public get providers(): string[] { public get providers(): string[] {
return this._providerInfo ? this._providerInfo.providers : []; return this._providers;
} }
public get cells(): ICellModel[] { public get cells(): ICellModel[] {
@@ -306,7 +312,7 @@ class MainThreadNotebookDocumentAndEditorStateComputer extends Disposable {
export class MainThreadNotebookDocumentsAndEditors extends Disposable implements MainThreadNotebookDocumentsAndEditorsShape { export class MainThreadNotebookDocumentsAndEditors extends Disposable implements MainThreadNotebookDocumentsAndEditorsShape {
private _proxy: ExtHostNotebookDocumentsAndEditorsShape; private _proxy: ExtHostNotebookDocumentsAndEditorsShape;
private _notebookEditors = new Map<string, MainThreadNotebookEditor>(); private _notebookEditors = new Map<string, MainThreadNotebookEditor>();
private _modelToDisposeMap = new Map<string, IDisposable>(); private _modelToDisposeMap = new Map<string, IDisposable[]>();
constructor( constructor(
extHostContext: IExtHostContext, extHostContext: IExtHostContext,
@IUntitledEditorService private _untitledEditorService: IUntitledEditorService, @IUntitledEditorService private _untitledEditorService: IUntitledEditorService,
@@ -385,6 +391,14 @@ export class MainThreadNotebookDocumentsAndEditors extends Disposable implements
return editor.clearAllOutputs(); return editor.clearAllOutputs();
} }
$changeKernel(id: string, kernel: azdata.nb.IKernelSpec): Promise<boolean> {
let editor = this.getEditor(id);
if (!editor) {
return Promise.reject(disposed(`TextEditor(${id})`));
}
return this.doChangeKernel(editor, kernel.display_name);
}
//#endregion //#endregion
private async doOpenEditor(resource: UriComponents, options: INotebookShowOptions): Promise<string> { private async doOpenEditor(resource: UriComponents, options: INotebookShowOptions): Promise<string> {
@@ -500,9 +514,11 @@ export class MainThreadNotebookDocumentsAndEditors extends Disposable implements
return; return;
} }
removedDocuments.forEach(removedDoc => { removedDocuments.forEach(removedDoc => {
let listener = this._modelToDisposeMap.get(removedDoc.toString()); let listeners = this._modelToDisposeMap.get(removedDoc.toString());
if (listener) { if (listeners && listeners.length) {
listener.dispose(); listeners.forEach(listener => {
listener.dispose();
});
this._modelToDisposeMap.delete(removedDoc.toString()); this._modelToDisposeMap.delete(removedDoc.toString());
} }
}); });
@@ -514,9 +530,9 @@ export class MainThreadNotebookDocumentsAndEditors extends Disposable implements
} }
addedEditors.forEach(editor => { addedEditors.forEach(editor => {
let modelUrl = editor.uri; let modelUrl = editor.uri;
this._modelToDisposeMap.set(editor.uri.toString(), editor.contentChanged((e) => { this._modelToDisposeMap.set(editor.uri.toString(), [editor.contentChanged((e) => {
this._proxy.$acceptModelChanged(modelUrl, this._toNotebookChangeData(e, editor)); this._proxy.$acceptModelChanged(modelUrl, this._toNotebookChangeData(e, editor));
})); })]);
}); });
} }
@@ -591,4 +607,15 @@ export class MainThreadNotebookDocumentsAndEditors extends Disposable implements
} }
return notebookCells; return notebookCells;
} }
private async doChangeKernel(editor: MainThreadNotebookEditor, displayName: string): Promise<boolean> {
let listeners = this._modelToDisposeMap.get(editor.id);
editor.model.changeKernel(displayName);
return new Promise((resolve) => {
listeners.push(editor.model.kernelChanged((kernel) => {
resolve(true);
}));
this._modelToDisposeMap.set(editor.id, listeners);
});
}
} }

View File

@@ -900,6 +900,7 @@ export interface MainThreadNotebookDocumentsAndEditorsShape extends IDisposable
$runCell(id: string, cellUri: UriComponents): Promise<boolean>; $runCell(id: string, cellUri: UriComponents): Promise<boolean>;
$runAllCells(id: string): Promise<boolean>; $runAllCells(id: string): Promise<boolean>;
$clearAllOutputs(id: string): Promise<boolean>; $clearAllOutputs(id: string): Promise<boolean>;
$changeKernel(id: string, kernel: azdata.nb.IKernelInfo): Promise<boolean>;
} }
export interface ExtHostExtensionManagementShape { export interface ExtHostExtensionManagementShape {

View File

@@ -264,8 +264,11 @@ export class ClientSession implements IClientSession {
private async updateCachedKernelSpec(): Promise<void> { private async updateCachedKernelSpec(): Promise<void> {
this._cachedKernelSpec = undefined; this._cachedKernelSpec = undefined;
let kernel = this.kernel; let kernel = this.kernel;
if (kernel && kernel.isReady) { if (kernel) {
this._cachedKernelSpec = await this.kernel.getSpec(); await kernel.ready;
if (kernel.isReady) {
this._cachedKernelSpec = await kernel.getSpec();
}
} }
} }

View File

@@ -332,6 +332,11 @@ export interface INotebookModel {
*/ */
readonly contentChanged: Event<NotebookContentChange>; readonly contentChanged: Event<NotebookContentChange>;
/**
* Event fired on notebook provider change
*/
readonly onProviderIdChange: Event<string>;
/** /**
* The trusted mode of the Notebook * The trusted mode of the Notebook
*/ */

View File

@@ -457,10 +457,6 @@ export class NotebookModel extends Disposable implements INotebookModel {
} }
} }
this._onClientSessionReady.fire(clientSession); this._onClientSessionReady.fire(clientSession);
this._kernelChangedEmitter.fire({
oldValue: undefined,
newValue: clientSession.kernel
});
} }
} }

View File

@@ -96,6 +96,9 @@ export class NotebookModelStub implements INotebookModel {
get onValidConnectionSelected(): Event<boolean> { get onValidConnectionSelected(): Event<boolean> {
throw new Error('method not implemented.'); throw new Error('method not implemented.');
} }
get onProviderIdChange(): Event<string> {
throw new Error('method not impelemented.');
}
toJSON(): nb.INotebookContents { toJSON(): nb.INotebookContents {
throw new Error('Method not implemented.'); throw new Error('Method not implemented.');
} }