From 94bc0d955926247605aa0e6526ae9868aa996cfd Mon Sep 17 00:00:00 2001 From: Charles Gagnon Date: Tue, 16 Jun 2020 13:06:44 -0700 Subject: [PATCH] Fix open notebook bug (#10930) * Fix open notebook bug * cleanup * clean up spaces --- extensions/notebook/src/common/appContext.ts | 4 + .../notebook/src/common/notebookUtils.ts | 238 +++++++++--------- extensions/notebook/src/common/utils.ts | 17 ++ extensions/notebook/src/extension.ts | 21 +- .../notebook/src/jupyter/serverInstance.ts | 3 +- .../src/test/common/notebookUtils.test.ts | 53 +++- .../src/test/common/querybookUtils.test.ts | 22 -- .../notebook/src/test/common/testUtils.ts | 9 + .../notebook/src/test/common/utils.test.ts | 15 +- 9 files changed, 213 insertions(+), 169 deletions(-) delete mode 100644 extensions/notebook/src/test/common/querybookUtils.test.ts diff --git a/extensions/notebook/src/common/appContext.ts b/extensions/notebook/src/common/appContext.ts index df03917215..1a806bc37d 100644 --- a/extensions/notebook/src/common/appContext.ts +++ b/extensions/notebook/src/common/appContext.ts @@ -5,6 +5,7 @@ import * as vscode from 'vscode'; import { ApiWrapper } from './apiWrapper'; +import { NotebookUtils } from './notebookUtils'; /** * Global context for the application @@ -12,8 +13,11 @@ import { ApiWrapper } from './apiWrapper'; export class AppContext { private serviceMap: Map = new Map(); + public readonly notebookUtils: NotebookUtils; + constructor(public readonly extensionContext: vscode.ExtensionContext, public readonly apiWrapper: ApiWrapper) { this.apiWrapper = apiWrapper || new ApiWrapper(); + this.notebookUtils = new NotebookUtils(apiWrapper); } public getService(serviceName: string): T { diff --git a/extensions/notebook/src/common/notebookUtils.ts b/extensions/notebook/src/common/notebookUtils.ts index e70084fd8a..666ea80d33 100644 --- a/extensions/notebook/src/common/notebookUtils.ts +++ b/extensions/notebook/src/common/notebookUtils.ts @@ -4,11 +4,11 @@ *--------------------------------------------------------------------------------------------*/ import * as azdata from 'azdata'; -import * as crypto from 'crypto'; import * as os from 'os'; import * as vscode from 'vscode'; import * as nls from 'vscode-nls'; import { getErrorMessage, isEditorTitleFree } from '../common/utils'; +import { ApiWrapper } from './apiWrapper'; const localize = nls.loadMessageBundle(); @@ -16,152 +16,140 @@ const JUPYTER_NOTEBOOK_PROVIDER = 'jupyter'; const msgSampleCodeDataFrame = localize('msgSampleCodeDataFrame', "This sample code loads the file into a data frame and shows the first 10 results."); const noNotebookVisible = localize('noNotebookVisible', "No notebook editor is active"); -/** - * Creates a random token per https://nodejs.org/api/crypto.html#crypto_crypto_randombytes_size_callback. - * Defaults to 24 bytes, which creates a 48-char hex string - */ -export function getRandomToken(size: number = 24): Promise { - return new Promise((resolve, reject) => { - crypto.randomBytes(size, (err, buffer) => { - if (err) { - reject(err); +export class NotebookUtils { + + constructor(private _apiWrapper: ApiWrapper) { } + + public async newNotebook(connectionProfile?: azdata.IConnectionProfile): Promise { + const title = this.findNextUntitledEditorName(); + const untitledUri = vscode.Uri.parse(`untitled:${title}`); + const options: azdata.nb.NotebookShowOptions = connectionProfile ? { + viewColumn: null, + preserveFocus: true, + preview: null, + providerId: null, + connectionProfile: connectionProfile, + defaultKernel: null + } : null; + return azdata.nb.showNotebookDocument(untitledUri, options); + } + + private findNextUntitledEditorName(): string { + let nextVal = 0; + // Note: this will go forever if it's coded wrong, or you have infinite Untitled notebooks! + while (true) { + let title = `Notebook-${nextVal}`; + if (isEditorTitleFree(title)) { + return title; } - let token = buffer.toString('hex'); - resolve(token); - }); - }); -} - -export 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, - providerId: null, - connectionProfile: connectionProfile, - defaultKernel: null - } : null; - return azdata.nb.showNotebookDocument(untitledUri, options); -} - -function findNextUntitledEditorName(): string { - let nextVal = 0; - // Note: this will go forever if it's coded wrong, or you have infinite Untitled notebooks! - while (true) { - let title = `Notebook-${nextVal}`; - if (isEditorTitleFree(title)) { - return title; + nextVal++; } - nextVal++; } -} -export async function openNotebook(): Promise { - try { - let filter: { [key: string]: Array } = {}; - // TODO support querying valid notebook file types - filter[localize('notebookFiles', "Notebooks")] = ['ipynb']; - let file = await vscode.window.showOpenDialog({ - filters: filter - }); - if (file) { - let doc = await vscode.workspace.openTextDocument(file[0]); - vscode.window.showTextDocument(doc); + public async openNotebook(): Promise { + try { + let filter: { [key: string]: Array } = {}; + // TODO support querying valid notebook file types + filter[localize('notebookFiles', "Notebooks")] = ['ipynb']; + let file = await this._apiWrapper.showOpenDialog({ + filters: filter + }); + if (file && file.length > 0) { + await azdata.nb.showNotebookDocument(file[0]); + } + } catch (err) { + this._apiWrapper.showErrorMessage(getErrorMessage(err)); } - } catch (err) { - vscode.window.showErrorMessage(getErrorMessage(err)); } -} -export async function runActiveCell(): Promise { - try { - let notebook = azdata.nb.activeNotebookEditor; - if (notebook) { - await notebook.runCell(); - } else { - throw new Error(noNotebookVisible); + public async runActiveCell(): Promise { + try { + let notebook = azdata.nb.activeNotebookEditor; + if (notebook) { + await notebook.runCell(); + } else { + throw new Error(noNotebookVisible); + } + } catch (err) { + vscode.window.showErrorMessage(getErrorMessage(err)); } - } catch (err) { - vscode.window.showErrorMessage(getErrorMessage(err)); } -} -export async function clearActiveCellOutput(): Promise { - try { - let notebook = azdata.nb.activeNotebookEditor; - if (notebook) { - await notebook.clearOutput(); - } else { - throw new Error(noNotebookVisible); + public async clearActiveCellOutput(): Promise { + try { + let notebook = azdata.nb.activeNotebookEditor; + if (notebook) { + await notebook.clearOutput(); + } else { + throw new Error(noNotebookVisible); + } + } catch (err) { + vscode.window.showErrorMessage(getErrorMessage(err)); } - } catch (err) { - vscode.window.showErrorMessage(getErrorMessage(err)); } -} -export async function runAllCells(startCell?: azdata.nb.NotebookCell, endCell?: azdata.nb.NotebookCell): Promise { - try { - let notebook = azdata.nb.activeNotebookEditor; - if (notebook) { - await notebook.runAllCells(startCell, endCell); - } else { - throw new Error(noNotebookVisible); + public async runAllCells(startCell?: azdata.nb.NotebookCell, endCell?: azdata.nb.NotebookCell): Promise { + try { + let notebook = azdata.nb.activeNotebookEditor; + if (notebook) { + await notebook.runAllCells(startCell, endCell); + } else { + throw new Error(noNotebookVisible); + } + } catch (err) { + vscode.window.showErrorMessage(getErrorMessage(err)); } - } catch (err) { - vscode.window.showErrorMessage(getErrorMessage(err)); } -} -export async function addCell(cellType: azdata.nb.CellType): Promise { - try { - let notebook = azdata.nb.activeNotebookEditor; - if (notebook) { - await notebook.edit((editBuilder: azdata.nb.NotebookEditorEdit) => { - // TODO should prompt and handle cell placement - editBuilder.insertCell({ - cell_type: cellType, - source: '' + public async addCell(cellType: azdata.nb.CellType): Promise { + try { + let notebook = azdata.nb.activeNotebookEditor; + if (notebook) { + await notebook.edit((editBuilder: azdata.nb.NotebookEditorEdit) => { + // TODO should prompt and handle cell placement + editBuilder.insertCell({ + cell_type: cellType, + source: '' + }); }); - }); - } else { - throw new Error(noNotebookVisible); + } else { + throw new Error(noNotebookVisible); + } + } catch (err) { + vscode.window.showErrorMessage(getErrorMessage(err)); } - } catch (err) { - vscode.window.showErrorMessage(getErrorMessage(err)); } -} -export async function analyzeNotebook(oeContext?: azdata.ObjectExplorerContext): Promise { - // Ensure we get a unique ID for the notebook. For now we're using a different prefix to the built-in untitled files - // to handle this. We should look into improving this in the future - let title = findNextUntitledEditorName(); - let untitledUri = vscode.Uri.parse(`untitled:${title}`); + public async analyzeNotebook(oeContext?: azdata.ObjectExplorerContext): Promise { + // Ensure we get a unique ID for the notebook. For now we're using a different prefix to the built-in untitled files + // to handle this. We should look into improving this in the future + let title = this.findNextUntitledEditorName(); + let untitledUri = vscode.Uri.parse(`untitled:${title}`); - let editor = await azdata.nb.showNotebookDocument(untitledUri, { - connectionProfile: oeContext ? oeContext.connectionProfile : undefined, - providerId: JUPYTER_NOTEBOOK_PROVIDER, - preview: false, - defaultKernel: { - name: 'pysparkkernel', - display_name: 'PySpark', - language: 'python' - } - }); - if (oeContext && oeContext.nodeInfo && oeContext.nodeInfo.nodePath) { - // Get the file path after '/HDFS' - let hdfsPath: string = oeContext.nodeInfo.nodePath.substring(oeContext.nodeInfo.nodePath.indexOf('/HDFS') + '/HDFS'.length); - if (hdfsPath.length > 0) { - let analyzeCommand = '#' + msgSampleCodeDataFrame + os.EOL + 'df = (spark.read.option("inferSchema", "true")' - + os.EOL + '.option("header", "true")' + os.EOL + '.csv("{0}"))' + os.EOL + 'df.show(10)'; + let editor = await azdata.nb.showNotebookDocument(untitledUri, { + connectionProfile: oeContext ? oeContext.connectionProfile : undefined, + providerId: JUPYTER_NOTEBOOK_PROVIDER, + preview: false, + defaultKernel: { + name: 'pysparkkernel', + display_name: 'PySpark', + language: 'python' + } + }); + if (oeContext && oeContext.nodeInfo && oeContext.nodeInfo.nodePath) { + // Get the file path after '/HDFS' + let hdfsPath: string = oeContext.nodeInfo.nodePath.substring(oeContext.nodeInfo.nodePath.indexOf('/HDFS') + '/HDFS'.length); + if (hdfsPath.length > 0) { + let analyzeCommand = '#' + msgSampleCodeDataFrame + os.EOL + 'df = (spark.read.option("inferSchema", "true")' + + os.EOL + '.option("header", "true")' + os.EOL + '.csv("{0}"))' + os.EOL + 'df.show(10)'; - editor.edit(editBuilder => { - editBuilder.insertCell({ - cell_type: 'code', - source: analyzeCommand.replace('{0}', hdfsPath) - }, 0); - }); + editor.edit(editBuilder => { + editBuilder.insertCell({ + cell_type: 'code', + source: analyzeCommand.replace('{0}', hdfsPath) + }, 0); + }); + } } } } diff --git a/extensions/notebook/src/common/utils.ts b/extensions/notebook/src/common/utils.ts index c253cc4959..65f668d247 100644 --- a/extensions/notebook/src/common/utils.ts +++ b/extensions/notebook/src/common/utils.ts @@ -8,6 +8,7 @@ import * as fs from 'fs-extra'; import * as nls from 'vscode-nls'; import * as vscode from 'vscode'; import * as azdata from 'azdata'; +import * as crypto from 'crypto'; import { notebookLanguages } from './constants'; const localize = nls.loadMessageBundle(); @@ -300,3 +301,19 @@ function decorate(decorator: (fn: Function, key: string) => Function): Function export function getDropdownValue(dropdown: azdata.DropDownComponent): string { return (typeof dropdown.value === 'string') ? dropdown.value : dropdown.value.name; } + +/** + * Creates a random token per https://nodejs.org/api/crypto.html#crypto_crypto_randombytes_size_callback. + * Defaults to 24 bytes, which creates a 48-char hex string + */ +export async function getRandomToken(size: number = 24): Promise { + return new Promise((resolve, reject) => { + crypto.randomBytes(size, (err, buffer) => { + if (err) { + reject(err); + } + let token = buffer.toString('hex'); + resolve(token); + }); + }); +} diff --git a/extensions/notebook/src/extension.ts b/extensions/notebook/src/extension.ts index 4bf531b47c..4d6791c885 100644 --- a/extensions/notebook/src/extension.ts +++ b/extensions/notebook/src/extension.ts @@ -16,7 +16,6 @@ import { CellType } from './contracts/content'; import { NotebookUriHandler } from './protocol/notebookUriHandler'; import { BookTreeViewProvider } from './book/bookTreeView'; import { NavigationProviders } from './common/constants'; -import { newNotebook, openNotebook, runActiveCell, runAllCells, clearActiveCellOutput, addCell, analyzeNotebook } from './common/notebookUtils'; const localize = nls.loadMessageBundle(); @@ -26,6 +25,7 @@ let controller: JupyterController; type ChooseCellType = { label: string, id: CellType }; export async function activate(extensionContext: vscode.ExtensionContext): Promise { + const appContext = new AppContext(extensionContext, new ApiWrapper()); const createBookPath: string = path.posix.join(extensionContext.extensionPath, 'resources', 'notebooks', 'JupyterBooksCreate.ipynb'); extensionContext.subscriptions.push(vscode.commands.registerCommand('bookTreeView.openBook', (bookPath: string, openAsUntitled: boolean, urlToOpen?: string) => openAsUntitled ? providedBookTreeViewProvider.openBook(bookPath, urlToOpen, true) : bookTreeViewProvider.openBook(bookPath, urlToOpen, true))); extensionContext.subscriptions.push(vscode.commands.registerCommand('bookTreeView.openNotebook', (resource) => bookTreeViewProvider.openNotebook(resource))); @@ -56,19 +56,19 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi if (context && context.connectionProfile) { connectionProfile = context.connectionProfile; } - return newNotebook(connectionProfile); + return appContext.notebookUtils.newNotebook(connectionProfile); })); extensionContext.subscriptions.push(vscode.commands.registerCommand('notebook.command.open', async () => { - await openNotebook(); + await appContext.notebookUtils.openNotebook(); })); extensionContext.subscriptions.push(vscode.commands.registerCommand('notebook.command.runactivecell', async () => { - await runActiveCell(); + await appContext.notebookUtils.runActiveCell(); })); extensionContext.subscriptions.push(vscode.commands.registerCommand('notebook.command.runallcells', async () => { - await runAllCells(); + await appContext.notebookUtils.runAllCells(); })); extensionContext.subscriptions.push(vscode.commands.registerCommand('notebook.command.clearactivecellresult', async () => { - await clearActiveCellOutput(); + await appContext.notebookUtils.clearActiveCellOutput(); })); extensionContext.subscriptions.push(vscode.commands.registerCommand('notebook.command.addcell', async () => { let cellType: CellType; @@ -91,17 +91,17 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi return; } if (cellType) { - await addCell(cellType); + await appContext.notebookUtils.addCell(cellType); } })); extensionContext.subscriptions.push(vscode.commands.registerCommand('notebook.command.addcode', async () => { - await addCell('code'); + await appContext.notebookUtils.addCell('code'); })); extensionContext.subscriptions.push(vscode.commands.registerCommand('notebook.command.addtext', async () => { - await addCell('markdown'); + await appContext.notebookUtils.addCell('markdown'); })); extensionContext.subscriptions.push(vscode.commands.registerCommand('notebook.command.analyzeNotebook', async (explorerContext: azdata.ObjectExplorerContext) => { - await analyzeNotebook(explorerContext); + await appContext.notebookUtils.analyzeNotebook(explorerContext); })); extensionContext.subscriptions.push(vscode.window.registerUriHandler(new NotebookUriHandler())); @@ -110,7 +110,6 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi await vscode.commands.executeCommand('vscode.open', vscode.Uri.parse(urlToOpen)); })); - let appContext = new AppContext(extensionContext, new ApiWrapper()); controller = new JupyterController(appContext); let result = await controller.activate(); if (!result) { diff --git a/extensions/notebook/src/jupyter/serverInstance.ts b/extensions/notebook/src/jupyter/serverInstance.ts index fc4f54c8e3..86a435dae8 100644 --- a/extensions/notebook/src/jupyter/serverInstance.ts +++ b/extensions/notebook/src/jupyter/serverInstance.ts @@ -16,7 +16,6 @@ import { IServerInstance } from './common'; import { JupyterServerInstallation } from './jupyterServerInstallation'; import * as utils from '../common/utils'; import * as constants from '../common/constants'; -import * as notebookUtils from '../common/notebookUtils'; import * as ports from '../common/ports'; const NotebookConfigFilename = 'jupyter_notebook_config.py'; @@ -241,7 +240,7 @@ export class PerFolderServerInstance implements IServerInstance { let notebookDirectory = this.getNotebookDirectory(); // Find a port in a given range. If run into trouble, try another port inside the given range let port = await ports.strictFindFreePort(new ports.StrictPortFindOptions(defaultPort, defaultPort + 1000)); - let token = await notebookUtils.getRandomToken(); + let token = await utils.getRandomToken(); this._uri = vscode.Uri.parse(`http://localhost:${port}/?token=${token}`); this._port = port.toString(); let startCommand = `"${this.options.install.pythonExecutable}" -m jupyter notebook --no-browser --no-mathjax --notebook-dir "${notebookDirectory}" --port=${port} --NotebookApp.token=${token}`; diff --git a/extensions/notebook/src/test/common/notebookUtils.test.ts b/extensions/notebook/src/test/common/notebookUtils.test.ts index 6462c8ca78..e0f424d443 100644 --- a/extensions/notebook/src/test/common/notebookUtils.test.ts +++ b/extensions/notebook/src/test/common/notebookUtils.test.ts @@ -4,15 +4,29 @@ *--------------------------------------------------------------------------------------------*/ import * as azdata from 'azdata'; -import { newNotebook } from '../../common/notebookUtils'; +import { NotebookUtils } from '../../common/notebookUtils'; import * as should from 'should'; import * as vscode from 'vscode'; +import * as TypeMoq from 'typemoq'; +import * as os from 'os'; +import * as path from 'path'; +import * as uuid from 'uuid'; +import { promises as fs } from 'fs'; +import { ApiWrapper } from '../../common/apiWrapper'; +import { tryDeleteFile } from './testUtils'; -describe('notebookUtils Tests', async function (): Promise { - describe('newNotebook', async function (): Promise { +describe('notebookUtils Tests', function (): void { + let notebookUtils: NotebookUtils; + let apiWrapperMock: TypeMoq.IMock; + before(function (): void { + apiWrapperMock = TypeMoq.Mock.ofInstance(new ApiWrapper()); + notebookUtils = new NotebookUtils(apiWrapperMock.object); + }); + + describe('newNotebook', function (): void { it('Should open a new notebook successfully', async function (): Promise { should(azdata.nb.notebookDocuments.length).equal(0, 'There should be not any open Notebook documents'); - await newNotebook(undefined); + await notebookUtils.newNotebook(undefined); should(azdata.nb.notebookDocuments.length).equal(1, 'There should be exactly 1 open Notebook document'); await vscode.commands.executeCommand('workbench.action.closeActiveEditor'); should(azdata.nb.notebookDocuments.length).equal(0, 'There should be not any open Notebook documents'); @@ -20,12 +34,12 @@ describe('notebookUtils Tests', async function (): Promise { it('Opening an untitled editor after closing should re-use previous untitled name', async function (): Promise { should(azdata.nb.notebookDocuments.length).equal(0, 'There should be not any open Notebook documents'); - await newNotebook(undefined); + await notebookUtils.newNotebook(undefined); should(azdata.nb.notebookDocuments.length).equal(1, 'There should be exactly 1 open Notebook document'); should(azdata.nb.notebookDocuments[0].fileName).equal('Notebook-0', 'The first Untitled Notebook should have an index of 0'); await vscode.commands.executeCommand('workbench.action.closeActiveEditor'); should(azdata.nb.notebookDocuments.length).equal(0, 'There should be not any open Notebook documents'); - await newNotebook(undefined); + await notebookUtils.newNotebook(undefined); should(azdata.nb.notebookDocuments.length).equal(1, 'There should be exactly 1 open Notebook document after second opening'); should(azdata.nb.notebookDocuments[0].fileName).equal('Notebook-0', 'The first Untitled Notebook should have an index of 0 after closing first Untitled Notebook'); await vscode.commands.executeCommand('workbench.action.closeActiveEditor'); @@ -33,9 +47,9 @@ describe('notebookUtils Tests', async function (): Promise { it('Untitled Name index should increase', async function (): Promise { should(azdata.nb.notebookDocuments.length).equal(0, 'There should be not any open Notebook documents'); - await newNotebook(undefined); + await notebookUtils.newNotebook(undefined); should(azdata.nb.notebookDocuments.length).equal(1, 'There should be exactly 1 open Notebook document'); - const secondNotebook = await newNotebook(undefined); + const secondNotebook = await notebookUtils.newNotebook(undefined); should(azdata.nb.notebookDocuments.length).equal(2, 'There should be exactly 2 open Notebook documents'); should(secondNotebook.document.fileName).equal('Notebook-1', 'The second Untitled Notebook should have an index of 1'); await vscode.commands.executeCommand('workbench.action.closeActiveEditor'); @@ -43,4 +57,27 @@ describe('notebookUtils Tests', async function (): Promise { should(azdata.nb.notebookDocuments.length).equal(0, 'There should be not any open Notebook documents'); }); }); + + describe('openNotebook', function () { + it('opens a Notebook successfully', async function (): Promise { + const notebookPath = path.join(os.tmpdir(), `OpenNotebookTest_${uuid.v4()}.ipynb`); + const notebookUri = vscode.Uri.file(notebookPath); + try { + await fs.writeFile(notebookPath, ''); + apiWrapperMock.setup(x => x.showOpenDialog(TypeMoq.It.isAny())).returns(() => Promise.resolve([notebookUri])); + await notebookUtils.openNotebook(); + should(azdata.nb.notebookDocuments.find(doc => doc.fileName === notebookUri.fsPath)).not.be.undefined(); + await vscode.commands.executeCommand('workbench.action.closeActiveEditor'); + } finally { + tryDeleteFile(notebookPath); + } + }); + + it('shows error if unexpected error is thrown', async function (): Promise { + apiWrapperMock.setup(x => x.showOpenDialog(TypeMoq.It.isAny())).throws(new Error('Unexpected error')); + apiWrapperMock.setup(x => x.showErrorMessage(TypeMoq.It.isAny())).returns(() => Promise.resolve('')); + await notebookUtils.openNotebook(); + apiWrapperMock.verify(x => x.showErrorMessage(TypeMoq.It.isAny()), TypeMoq.Times.once()); + }); + }); }); diff --git a/extensions/notebook/src/test/common/querybookUtils.test.ts b/extensions/notebook/src/test/common/querybookUtils.test.ts deleted file mode 100644 index c29729f69e..0000000000 --- a/extensions/notebook/src/test/common/querybookUtils.test.ts +++ /dev/null @@ -1,22 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as should from 'should'; -import 'mocha'; - -import * as notebookUtils from '../../common/notebookUtils'; - -describe('Random Token', () => { - it('Should have default length and be hex only', async function (): Promise { - - let token = await notebookUtils.getRandomToken(); - should(token).have.length(48); - let validChars = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f']; - for (let i = 0; i < token.length; i++) { - let char = token.charAt(i); - should(validChars.indexOf(char)).be.greaterThan(-1); - } - }); -}); diff --git a/extensions/notebook/src/test/common/testUtils.ts b/extensions/notebook/src/test/common/testUtils.ts index 0c53be86a5..c9eebccd86 100644 --- a/extensions/notebook/src/test/common/testUtils.ts +++ b/extensions/notebook/src/test/common/testUtils.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; +import { promises as fs } from 'fs'; export async function assertThrowsAsync(fn: () => Promise, msg: string): Promise { let f = () => { @@ -21,3 +22,11 @@ export async function assertThrowsAsync(fn: () => Promise, msg: string): Pr export async function sleep(ms: number): Promise<{}> { return new Promise(resolve => setTimeout(resolve, ms)); } + +export async function tryDeleteFile(path: string): Promise { + try { + await fs.unlink(path); + } catch { + console.warn(`Could not delete file ${path}`); + } +} diff --git a/extensions/notebook/src/test/common/utils.test.ts b/extensions/notebook/src/test/common/utils.test.ts index 5cba304823..e7bf8f78f8 100644 --- a/extensions/notebook/src/test/common/utils.test.ts +++ b/extensions/notebook/src/test/common/utils.test.ts @@ -160,7 +160,7 @@ describe('Utils Tests', function () { }); describe('isEditorTitleFree', () => { - afterEach( async () => { + afterEach(async () => { await vscode.commands.executeCommand('workbench.action.closeActiveEditor'); }); @@ -318,4 +318,17 @@ describe('Utils Tests', function () { }).throw(); }); }); + + describe('getRandomToken', function (): void { + it('Should have default length and be hex only', async function (): Promise { + + let token = await utils.getRandomToken(); + should(token).have.length(48); + let validChars = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f']; + for (let i = 0; i < token.length; i++) { + let char = token.charAt(i); + should(validChars.indexOf(char)).be.greaterThan(-1); + } + }); + }); });