diff --git a/extensions/notebook/src/common/utils.ts b/extensions/notebook/src/common/utils.ts index 3f7cd5c80f..adec32d6a4 100644 --- a/extensions/notebook/src/common/utils.ts +++ b/extensions/notebook/src/common/utils.ts @@ -75,7 +75,7 @@ export function executeStreamedCommand(cmd: string, options: childProcess.SpawnO if (code === 0) { resolve(); } else { - reject(localize('executeCommandProcessExited', "Process exited with with error code: {0}. StdErr Output: {1}", code, stdErrLog)); + reject(new Error(localize('executeCommandProcessExited', "Process exited with error code: {0}. StdErr Output: {1}", code, stdErrLog))); } }); diff --git a/extensions/notebook/src/extension.ts b/extensions/notebook/src/extension.ts index ba51586f75..45877940d5 100644 --- a/extensions/notebook/src/extension.ts +++ b/extensions/notebook/src/extension.ts @@ -54,12 +54,12 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi }); }); })); - extensionContext.subscriptions.push(vscode.commands.registerCommand('_notebook.command.new', (context?: azdata.ConnectedContext) => { + extensionContext.subscriptions.push(vscode.commands.registerCommand('_notebook.command.new', async (context?: azdata.ConnectedContext) => { let connectionProfile: azdata.IConnectionProfile = undefined; if (context && context.connectionProfile) { connectionProfile = context.connectionProfile; } - newNotebook(connectionProfile); + return newNotebook(connectionProfile); })); extensionContext.subscriptions.push(vscode.commands.registerCommand('notebook.command.open', async () => { await openNotebook(); @@ -142,10 +142,10 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi }; } -function newNotebook(connectionProfile: azdata.IConnectionProfile) { - let title = findNextUntitledEditorName(); - let untitledUri = vscode.Uri.parse(`untitled:${title}`); - let options: azdata.nb.NotebookShowOptions = connectionProfile ? { +async function newNotebook(connectionProfile: azdata.IConnectionProfile): Promise { + const title = findNextUntitledEditorName(); + const untitledUri = vscode.Uri.parse(`untitled:${title}`); + const options: azdata.nb.NotebookShowOptions = connectionProfile ? { viewColumn: null, preserveFocus: true, preview: null, @@ -153,11 +153,7 @@ function newNotebook(connectionProfile: azdata.IConnectionProfile) { connectionProfile: connectionProfile, defaultKernel: null } : null; - azdata.nb.showNotebookDocument(untitledUri, options).then(success => { - - }, (err: Error) => { - vscode.window.showErrorMessage(err.message); - }); + return azdata.nb.showNotebookDocument(untitledUri, options); } function findNextUntitledEditorName(): string { diff --git a/extensions/notebook/src/test/common/testUtils.ts b/extensions/notebook/src/test/common/testUtils.ts index 68531f3497..0c53be86a5 100644 --- a/extensions/notebook/src/test/common/testUtils.ts +++ b/extensions/notebook/src/test/common/testUtils.ts @@ -17,3 +17,7 @@ export async function assertThrowsAsync(fn: () => Promise, msg: string): Pr assert.throws(f, msg); } } + +export async function sleep(ms: number): Promise<{}> { + return new Promise(resolve => setTimeout(resolve, ms)); +} diff --git a/extensions/notebook/src/test/common/utils.test.ts b/extensions/notebook/src/test/common/utils.test.ts index 2cd05a1044..5cba304823 100644 --- a/extensions/notebook/src/test/common/utils.test.ts +++ b/extensions/notebook/src/test/common/utils.test.ts @@ -10,6 +10,9 @@ import * as os from 'os'; import * as path from 'path'; import * as utils from '../../common/utils'; import { MockOutputChannel } from './stubs'; +import * as vscode from 'vscode'; +import * as azdata from 'azdata'; +import { sleep } from './testUtils'; describe('Utils Tests', function () { @@ -129,12 +132,190 @@ describe('Utils Tests', function () { it('different lengths', () => { const random = ['1.0.0', '42', '100.0', '0.1', '1.0.1']; - const randomSorted = ['0.1', '1.0.0', '1.0.1', '42', '100.0'] + const randomSorted = ['0.1', '1.0.0', '1.0.1', '42', '100.0']; should(utils.sortPackageVersions(random)).deepEqual(randomSorted); }); }); - describe('getClusterEndpoints', () => { + describe('executeBufferedCommand', () => { + it('runs successfully', async () => { + await utils.executeBufferedCommand('echo hello', {}, new MockOutputChannel()); + }); + + it('errors correctly with invalid command', async () => { + await should(utils.executeBufferedCommand('invalidcommand', {}, new MockOutputChannel())).be.rejected(); + }); + }); + + describe('executeStreamedCommand', () => { + + it('runs successfully', async () => { + await utils.executeStreamedCommand('echo hello', {}, new MockOutputChannel()); + }); + + it('errors correctly with invalid command', async () => { + await should(utils.executeStreamedCommand('invalidcommand', {}, new MockOutputChannel())).be.rejected(); + }); + }); + + describe('isEditorTitleFree', () => { + afterEach( async () => { + await vscode.commands.executeCommand('workbench.action.closeActiveEditor'); + }); + + it('title is free', () => { + should(utils.isEditorTitleFree('MyTitle')).be.true(); + }); + + it('title is not free with text document sharing name', async () => { + const editorTitle = 'Untitled-1'; + should(utils.isEditorTitleFree(editorTitle)).be.true('Title should be free before opening text document'); + await vscode.workspace.openTextDocument(); + should(utils.isEditorTitleFree(editorTitle)).be.false('Title should not be free after opening text document'); + }); + + it('title is not free with notebook document sharing name', async () => { + const editorTitle = 'MyUntitledNotebook'; + should(utils.isEditorTitleFree(editorTitle)).be.true('Title should be free before opening notebook'); + await azdata.nb.showNotebookDocument(vscode.Uri.parse(`untitled:${editorTitle}`)); + should(utils.isEditorTitleFree('MyUntitledNotebook')).be.false('Title should not be free after opening notebook'); + }); + + it('title is not free with notebook document sharing name created through command', async () => { + const editorTitle = 'Notebook-0'; + should(utils.isEditorTitleFree(editorTitle)).be.true('Title should be free before opening notebook'); + await vscode.commands.executeCommand('_notebook.command.new'); + should(utils.isEditorTitleFree(editorTitle)).be.false('Title should not be free after opening notebook'); + }); + }); + + describe('getClusterEndpoints', () => { + const baseServerInfo: azdata.ServerInfo = { + serverMajorVersion: -1, + serverMinorVersion: -1, + serverReleaseVersion: -1, + engineEditionId: -1, + serverVersion: '', + serverLevel: '', + serverEdition: '', + isCloud: false, + azureVersion: -1, + osVersion: '', + options: {} + }; + it('empty endpoints does not error', () => { + const serverInfo = Object.assign({}, baseServerInfo); + serverInfo.options['clusterEndpoints'] = []; + should(utils.getClusterEndpoints(serverInfo).length).equal(0); + }); + + it('endpoints without endpoint field are created successfully', () => { + const serverInfo = Object.assign({}, baseServerInfo); + const ipAddress = 'localhost'; + const port = '123'; + serverInfo.options['clusterEndpoints'] = [{ ipAddress: ipAddress, port: port }]; + const endpoints = utils.getClusterEndpoints(serverInfo); + should(endpoints.length).equal(1); + should(endpoints[0].endpoint).equal('https://localhost:123'); + }); + + it('endpoints with endpoint field are created successfully', () => { + const endpoint = 'https://myActualEndpoint:8080'; + const serverInfo = Object.assign({}, baseServerInfo); + serverInfo.options['clusterEndpoints'] = [{ endpoint: endpoint, ipAddress: 'localhost', port: '123' }]; + const endpoints = utils.getClusterEndpoints(serverInfo); + should(endpoints.length).equal(1); + should(endpoints[0].endpoint).equal(endpoint); + }); + }); + + describe('getHostAndPortFromEndpoint', () => { + it('valid endpoint is parsed correctly', () => { + const host = 'localhost'; + const port = '123'; + const hostAndIp = utils.getHostAndPortFromEndpoint(`https://${host}:${port}`); + should(hostAndIp).deepEqual({ host: host, port: port }); + }); + + it('invalid endpoint is returned as is', () => { + const host = 'localhost'; + const hostAndIp = utils.getHostAndPortFromEndpoint(`https://${host}`); + should(hostAndIp).deepEqual({ host: host, port: undefined }); + }); + }); + + describe('exists', () => { + it('runs as expected', async () => { + const filename = path.join(os.tmpdir(), `NotebookUtilsTest_${uuid.v4()}`); + try { + should(await utils.exists(filename)).be.false(); + await fs.writeFile(filename, ''); + should(await utils.exists(filename)).be.true(); + } finally { + try { + await fs.unlink(filename); + } catch { /* no-op */ } + } + }); + }); + + describe('getIgnoreSslVerificationConfigSetting', () => { + it('runs as expected', async () => { + should(utils.getIgnoreSslVerificationConfigSetting()).be.true(); + }); + }); + + describe('debounce', () => { + class DebounceTest { + public fnCalled = 0; + public getterCalled = 0; + + @utils.debounce(100) + fn(): void { + this.fnCalled++; + } + + @utils.debounce(100) + get getter(): number { + this.getterCalled++; + return -1; + } + } + + it('decorates function correctly', async () => { + const debounceTestObj = new DebounceTest(); + debounceTestObj.fn(); + debounceTestObj.fn(); + await sleep(500); + should(debounceTestObj.fnCalled).equal(1); + debounceTestObj.fn(); + debounceTestObj.fn(); + await sleep(500); + should(debounceTestObj.fnCalled).equal(2); + }); + + it('decorates getter correctly', async () => { + const debounceTestObj = new DebounceTest(); + let getterValue = debounceTestObj.getter; + getterValue = debounceTestObj.getter; + await sleep(500); + should(debounceTestObj.getterCalled).equal(1); + getterValue = debounceTestObj.getter; + getterValue = debounceTestObj.getter; + await sleep(500); + should(debounceTestObj.getterCalled).equal(2); + should(getterValue).be.undefined(); + }); + + it('decorating setter not supported', async () => { + should(() => { + class UnsupportedTest { + @utils.debounce(100) + set setter(value: number) { } + } + new UnsupportedTest(); + }).throw(); + }); }); }); diff --git a/extensions/notebook/src/test/model/sessionManager.test.ts b/extensions/notebook/src/test/model/sessionManager.test.ts index c6bea1deba..844fc22f09 100644 --- a/extensions/notebook/src/test/model/sessionManager.test.ts +++ b/extensions/notebook/src/test/model/sessionManager.test.ts @@ -108,7 +108,7 @@ describe('Jupyter Session', function (): void { beforeEach(() => { mockJupyterSession = TypeMoq.Mock.ofType(SessionStub); - session = new JupyterSession(mockJupyterSession.object, undefined); + session = new JupyterSession(mockJupyterSession.object, undefined, true); }); it('should always be able to change kernels', function (): void { @@ -145,7 +145,7 @@ describe('Jupyter Session', function (): void { kernel = session.kernel; // Then I expect it to have the ID, and only be called once should(kernel.id).equal('id'); - mockJupyterSession.verify(s => s.kernel, TypeMoq.Times.exactly(2)); + mockJupyterSession.verify(s => s.kernel, TypeMoq.Times.exactly(1)); }); it('should send name in changeKernel request', async function (): Promise {