Path: blob/main/extensions/ipynb/src/test/clearOutputs.test.ts
3292 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import * as sinon from 'sinon';6import type * as nbformat from '@jupyterlab/nbformat';7import * as assert from 'assert';8import * as vscode from 'vscode';9import { jupyterNotebookModelToNotebookData } from '../deserializers';10import { activate } from '../notebookModelStoreSync';111213suite(`ipynb Clear Outputs`, () => {14const disposables: vscode.Disposable[] = [];15const context = { subscriptions: disposables } as vscode.ExtensionContext;16setup(() => {17disposables.length = 0;18activate(context);19});20teardown(async () => {21disposables.forEach(d => d.dispose());22disposables.length = 0;23sinon.restore();24await vscode.commands.executeCommand('workbench.action.closeAllEditors');25});2627test.skip('Clear outputs after opening Notebook', async () => {28const cells: nbformat.ICell[] = [29{30cell_type: 'code',31execution_count: 10,32outputs: [{ output_type: 'stream', name: 'stdout', text: ['Hello'] }],33source: 'print(1)',34metadata: {}35},36{37cell_type: 'code',38outputs: [],39source: 'print(2)',40metadata: {}41},42{43cell_type: 'markdown',44source: '# HEAD',45metadata: {}46}47];48const notebook = jupyterNotebookModelToNotebookData({ cells }, 'python');4950const notebookDocumentPromise = vscode.workspace.openNotebookDocument('jupyter-notebook', notebook);51await raceTimeout(notebookDocumentPromise, 5000, () => {52throw new Error('Timeout waiting for notebook to open');53});54const notebookDocument = await notebookDocumentPromise;55await raceTimeout(vscode.window.showNotebookDocument(notebookDocument), 20000, () => {56throw new Error('Timeout waiting for notebook to open');57});5859assert.strictEqual(notebookDocument.cellCount, 3);60assert.strictEqual(notebookDocument.cellAt(0).metadata.execution_count, 10);61assert.strictEqual(notebookDocument.cellAt(1).metadata.execution_count, null);62assert.strictEqual(notebookDocument.cellAt(2).metadata.execution_count, undefined);6364// Clear all outputs65await raceTimeout(vscode.commands.executeCommand('notebook.clearAllCellsOutputs'), 5000, () => {66throw new Error('Timeout waiting for notebook to clear outputs');67});6869// Wait for all changes to be applied, could take a few ms.70const verifyMetadataChanges = () => {71assert.strictEqual(notebookDocument.cellAt(0).metadata.execution_count, null);72assert.strictEqual(notebookDocument.cellAt(1).metadata.execution_count, null);73assert.strictEqual(notebookDocument.cellAt(2).metadata.execution_count, undefined);74};7576vscode.workspace.onDidChangeNotebookDocument(() => verifyMetadataChanges(), undefined, disposables);7778await new Promise<void>((resolve, reject) => {79const interval = setInterval(() => {80try {81verifyMetadataChanges();82clearInterval(interval);83resolve();84} catch {85// Ignore86}87}, 50);88disposables.push({ dispose: () => clearInterval(interval) });89const timeout = setTimeout(() => {90try {91verifyMetadataChanges();92resolve();93} catch (ex) {94reject(ex);95}96}, 1000);97disposables.push({ dispose: () => clearTimeout(timeout) });98});99});100101102// test('Serialize', async () => {103// const markdownCell = new vscode.NotebookCellData(vscode.NotebookCellKind.Markup, '# header1', 'markdown');104// markdownCell.metadata = {105// attachments: {106// 'image.png': {107// 'image/png': 'abc'108// }109// },110// id: '123',111// metadata: {112// foo: 'bar'113// }114// };115116// const cellMetadata = getCellMetadata({ cell: markdownCell });117// assert.deepStrictEqual(cellMetadata, {118// id: '123',119// metadata: {120// foo: 'bar',121// },122// attachments: {123// 'image.png': {124// 'image/png': 'abc'125// }126// }127// });128129// const markdownCell2 = new vscode.NotebookCellData(vscode.NotebookCellKind.Markup, '# header1', 'markdown');130// markdownCell2.metadata = {131// id: '123',132// metadata: {133// foo: 'bar'134// },135// attachments: {136// 'image.png': {137// 'image/png': 'abc'138// }139// }140// };141142// const nbMarkdownCell = createMarkdownCellFromNotebookCell(markdownCell);143// const nbMarkdownCell2 = createMarkdownCellFromNotebookCell(markdownCell2);144// assert.deepStrictEqual(nbMarkdownCell, nbMarkdownCell2);145146// assert.deepStrictEqual(nbMarkdownCell, {147// cell_type: 'markdown',148// source: ['# header1'],149// metadata: {150// foo: 'bar',151// },152// attachments: {153// 'image.png': {154// 'image/png': 'abc'155// }156// },157// id: '123'158// });159// });160161// suite('Outputs', () => {162// function validateCellOutputTranslation(163// outputs: nbformat.IOutput[],164// expectedOutputs: vscode.NotebookCellOutput[],165// propertiesToExcludeFromComparison: string[] = []166// ) {167// const cells: nbformat.ICell[] = [168// {169// cell_type: 'code',170// execution_count: 10,171// outputs,172// source: 'print(1)',173// metadata: {}174// }175// ];176// const notebook = jupyterNotebookModelToNotebookData({ cells }, 'python');177178// // OutputItems contain an `id` property generated by VSC.179// // Exclude that property when comparing.180// const propertiesToExclude = propertiesToExcludeFromComparison.concat(['id']);181// const actualOuts = notebook.cells[0].outputs;182// deepStripProperties(actualOuts, propertiesToExclude);183// deepStripProperties(expectedOutputs, propertiesToExclude);184// assert.deepStrictEqual(actualOuts, expectedOutputs);185// }186187// test('Empty output', () => {188// validateCellOutputTranslation([], []);189// });190191// test('Stream output', () => {192// validateCellOutputTranslation(193// [194// {195// output_type: 'stream',196// name: 'stderr',197// text: 'Error'198// },199// {200// output_type: 'stream',201// name: 'stdout',202// text: 'NoError'203// }204// ],205// [206// new vscode.NotebookCellOutput([vscode.NotebookCellOutputItem.stderr('Error')], {207// outputType: 'stream'208// }),209// new vscode.NotebookCellOutput([vscode.NotebookCellOutputItem.stdout('NoError')], {210// outputType: 'stream'211// })212// ]213// );214// });215// test('Stream output and line endings', () => {216// validateCellOutputTranslation(217// [218// {219// output_type: 'stream',220// name: 'stdout',221// text: [222// 'Line1\n',223// '\n',224// 'Line3\n',225// 'Line4'226// ]227// }228// ],229// [230// new vscode.NotebookCellOutput([vscode.NotebookCellOutputItem.stdout('Line1\n\nLine3\nLine4')], {231// outputType: 'stream'232// })233// ]234// );235// validateCellOutputTranslation(236// [237// {238// output_type: 'stream',239// name: 'stdout',240// text: [241// 'Hello\n',242// 'Hello\n',243// 'Hello\n',244// 'Hello\n',245// 'Hello\n',246// 'Hello\n'247// ]248// }249// ],250// [251// new vscode.NotebookCellOutput([vscode.NotebookCellOutputItem.stdout('Hello\nHello\nHello\nHello\nHello\nHello\n')], {252// outputType: 'stream'253// })254// ]255// );256// });257// test('Multi-line Stream output', () => {258// validateCellOutputTranslation(259// [260// {261// name: 'stdout',262// output_type: 'stream',263// text: [264// 'Epoch 1/5\n',265// '...\n',266// 'Epoch 2/5\n',267// '...\n',268// 'Epoch 3/5\n',269// '...\n',270// 'Epoch 4/5\n',271// '...\n',272// 'Epoch 5/5\n',273// '...\n'274// ]275// }276// ],277// [278// new vscode.NotebookCellOutput([vscode.NotebookCellOutputItem.stdout(['Epoch 1/5\n',279// '...\n',280// 'Epoch 2/5\n',281// '...\n',282// 'Epoch 3/5\n',283// '...\n',284// 'Epoch 4/5\n',285// '...\n',286// 'Epoch 5/5\n',287// '...\n'].join(''))], {288// outputType: 'stream'289// })290// ]291// );292// });293294// test('Multi-line Stream output (last empty line should not be saved in ipynb)', () => {295// validateCellOutputTranslation(296// [297// {298// name: 'stderr',299// output_type: 'stream',300// text: [301// 'Epoch 1/5\n',302// '...\n',303// 'Epoch 2/5\n',304// '...\n',305// 'Epoch 3/5\n',306// '...\n',307// 'Epoch 4/5\n',308// '...\n',309// 'Epoch 5/5\n',310// '...\n'311// ]312// }313// ],314// [315// new vscode.NotebookCellOutput([vscode.NotebookCellOutputItem.stderr(['Epoch 1/5\n',316// '...\n',317// 'Epoch 2/5\n',318// '...\n',319// 'Epoch 3/5\n',320// '...\n',321// 'Epoch 4/5\n',322// '...\n',323// 'Epoch 5/5\n',324// '...\n',325// // This last empty line should not be saved in ipynb.326// '\n'].join(''))], {327// outputType: 'stream'328// })329// ]330// );331// });332333// test('Streamed text with Ansi characters', async () => {334// validateCellOutputTranslation(335// [336// {337// name: 'stderr',338// text: '\u001b[K\u001b[33m✅ \u001b[0m Loading\n',339// output_type: 'stream'340// }341// ],342// [343// new vscode.NotebookCellOutput(344// [vscode.NotebookCellOutputItem.stderr('\u001b[K\u001b[33m✅ \u001b[0m Loading\n')],345// {346// outputType: 'stream'347// }348// )349// ]350// );351// });352353// test('Streamed text with angle bracket characters', async () => {354// validateCellOutputTranslation(355// [356// {357// name: 'stderr',358// text: '1 is < 2',359// output_type: 'stream'360// }361// ],362// [363// new vscode.NotebookCellOutput([vscode.NotebookCellOutputItem.stderr('1 is < 2')], {364// outputType: 'stream'365// })366// ]367// );368// });369370// test('Streamed text with angle bracket characters and ansi chars', async () => {371// validateCellOutputTranslation(372// [373// {374// name: 'stderr',375// text: '1 is < 2\u001b[K\u001b[33m✅ \u001b[0m Loading\n',376// output_type: 'stream'377// }378// ],379// [380// new vscode.NotebookCellOutput(381// [vscode.NotebookCellOutputItem.stderr('1 is < 2\u001b[K\u001b[33m✅ \u001b[0m Loading\n')],382// {383// outputType: 'stream'384// }385// )386// ]387// );388// });389390// test('Error', async () => {391// validateCellOutputTranslation(392// [393// {394// ename: 'Error Name',395// evalue: 'Error Value',396// traceback: ['stack1', 'stack2', 'stack3'],397// output_type: 'error'398// }399// ],400// [401// new vscode.NotebookCellOutput(402// [403// vscode.NotebookCellOutputItem.error({404// name: 'Error Name',405// message: 'Error Value',406// stack: ['stack1', 'stack2', 'stack3'].join('\n')407// })408// ],409// {410// outputType: 'error',411// originalError: {412// ename: 'Error Name',413// evalue: 'Error Value',414// traceback: ['stack1', 'stack2', 'stack3'],415// output_type: 'error'416// }417// }418// )419// ]420// );421// });422423// ['display_data', 'execute_result'].forEach(output_type => {424// suite(`Rich output for output_type = ${output_type}`, () => {425// // Properties to exclude when comparing.426// let propertiesToExcludeFromComparison: string[] = [];427// setup(() => {428// if (output_type === 'display_data') {429// // With display_data the execution_count property will never exist in the output.430// // We can ignore that (as it will never exist).431// // But we leave it in the case of `output_type === 'execute_result'`432// propertiesToExcludeFromComparison = ['execution_count', 'executionCount'];433// }434// });435436// test('Text mimeType output', async () => {437// validateCellOutputTranslation(438// [439// {440// data: {441// 'text/plain': 'Hello World!'442// },443// output_type,444// metadata: {},445// execution_count: 1446// }447// ],448// [449// new vscode.NotebookCellOutput(450// [new vscode.NotebookCellOutputItem(Buffer.from('Hello World!', 'utf8'), 'text/plain')],451// {452// outputType: output_type,453// metadata: {}, // display_data & execute_result always have metadata.454// executionCount: 1455// }456// )457// ],458// propertiesToExcludeFromComparison459// );460// });461462// test('png,jpeg images', async () => {463// validateCellOutputTranslation(464// [465// {466// execution_count: 1,467// data: {468// 'image/png': base64EncodedImage,469// 'image/jpeg': base64EncodedImage470// },471// metadata: {},472// output_type473// }474// ],475// [476// new vscode.NotebookCellOutput(477// [478// new vscode.NotebookCellOutputItem(Buffer.from(base64EncodedImage, 'base64'), 'image/png'),479// new vscode.NotebookCellOutputItem(Buffer.from(base64EncodedImage, 'base64'), 'image/jpeg')480// ],481// {482// executionCount: 1,483// outputType: output_type,484// metadata: {} // display_data & execute_result always have metadata.485// }486// )487// ],488// propertiesToExcludeFromComparison489// );490// });491492// test('png image with a light background', async () => {493// validateCellOutputTranslation(494// [495// {496// execution_count: 1,497// data: {498// 'image/png': base64EncodedImage499// },500// metadata: {501// needs_background: 'light'502// },503// output_type504// }505// ],506// [507// new vscode.NotebookCellOutput(508// [new vscode.NotebookCellOutputItem(Buffer.from(base64EncodedImage, 'base64'), 'image/png')],509// {510// executionCount: 1,511// metadata: {512// needs_background: 'light'513// },514// outputType: output_type515// }516// )517// ],518// propertiesToExcludeFromComparison519// );520// });521522// test('png image with a dark background', async () => {523// validateCellOutputTranslation(524// [525// {526// execution_count: 1,527// data: {528// 'image/png': base64EncodedImage529// },530// metadata: {531// needs_background: 'dark'532// },533// output_type534// }535// ],536// [537// new vscode.NotebookCellOutput(538// [new vscode.NotebookCellOutputItem(Buffer.from(base64EncodedImage, 'base64'), 'image/png')],539// {540// executionCount: 1,541// metadata: {542// needs_background: 'dark'543// },544// outputType: output_type545// }546// )547// ],548// propertiesToExcludeFromComparison549// );550// });551552// test('png image with custom dimensions', async () => {553// validateCellOutputTranslation(554// [555// {556// execution_count: 1,557// data: {558// 'image/png': base64EncodedImage559// },560// metadata: {561// 'image/png': { height: '111px', width: '999px' }562// },563// output_type564// }565// ],566// [567// new vscode.NotebookCellOutput(568// [new vscode.NotebookCellOutputItem(Buffer.from(base64EncodedImage, 'base64'), 'image/png')],569// {570// executionCount: 1,571// metadata: {572// 'image/png': { height: '111px', width: '999px' }573// },574// outputType: output_type575// }576// )577// ],578// propertiesToExcludeFromComparison579// );580// });581582// test('png allowed to scroll', async () => {583// validateCellOutputTranslation(584// [585// {586// execution_count: 1,587// data: {588// 'image/png': base64EncodedImage589// },590// metadata: {591// unconfined: true,592// 'image/png': { width: '999px' }593// },594// output_type595// }596// ],597// [598// new vscode.NotebookCellOutput(599// [new vscode.NotebookCellOutputItem(Buffer.from(base64EncodedImage, 'base64'), 'image/png')],600// {601// executionCount: 1,602// metadata: {603// unconfined: true,604// 'image/png': { width: '999px' }605// },606// outputType: output_type607// }608// )609// ],610// propertiesToExcludeFromComparison611// );612// });613// });614// });615// });616617// suite('Output Order', () => {618// test('Verify order of outputs', async () => {619// const dataAndExpectedOrder: { output: nbformat.IDisplayData; expectedMimeTypesOrder: string[] }[] = [620// {621// output: {622// data: {623// 'application/vnd.vegalite.v4+json': 'some json',624// 'text/html': '<a>Hello</a>'625// },626// metadata: {},627// output_type: 'display_data'628// },629// expectedMimeTypesOrder: ['application/vnd.vegalite.v4+json', 'text/html']630// },631// {632// output: {633// data: {634// 'application/vnd.vegalite.v4+json': 'some json',635// 'application/javascript': 'some js',636// 'text/plain': 'some text',637// 'text/html': '<a>Hello</a>'638// },639// metadata: {},640// output_type: 'display_data'641// },642// expectedMimeTypesOrder: [643// 'application/vnd.vegalite.v4+json',644// 'text/html',645// 'application/javascript',646// 'text/plain'647// ]648// },649// {650// output: {651// data: {652// 'application/vnd.vegalite.v4+json': '', // Empty, should give preference to other mimetypes.653// 'application/javascript': 'some js',654// 'text/plain': 'some text',655// 'text/html': '<a>Hello</a>'656// },657// metadata: {},658// output_type: 'display_data'659// },660// expectedMimeTypesOrder: [661// 'text/html',662// 'application/javascript',663// 'text/plain',664// 'application/vnd.vegalite.v4+json'665// ]666// },667// {668// output: {669// data: {670// 'text/plain': 'some text',671// 'text/html': '<a>Hello</a>'672// },673// metadata: {},674// output_type: 'display_data'675// },676// expectedMimeTypesOrder: ['text/html', 'text/plain']677// },678// {679// output: {680// data: {681// 'application/javascript': 'some js',682// 'text/plain': 'some text'683// },684// metadata: {},685// output_type: 'display_data'686// },687// expectedMimeTypesOrder: ['application/javascript', 'text/plain']688// },689// {690// output: {691// data: {692// 'image/svg+xml': 'some svg',693// 'text/plain': 'some text'694// },695// metadata: {},696// output_type: 'display_data'697// },698// expectedMimeTypesOrder: ['image/svg+xml', 'text/plain']699// },700// {701// output: {702// data: {703// 'text/latex': 'some latex',704// 'text/plain': 'some text'705// },706// metadata: {},707// output_type: 'display_data'708// },709// expectedMimeTypesOrder: ['text/latex', 'text/plain']710// },711// {712// output: {713// data: {714// 'application/vnd.jupyter.widget-view+json': 'some widget',715// 'text/plain': 'some text'716// },717// metadata: {},718// output_type: 'display_data'719// },720// expectedMimeTypesOrder: ['application/vnd.jupyter.widget-view+json', 'text/plain']721// },722// {723// output: {724// data: {725// 'text/plain': 'some text',726// 'image/svg+xml': 'some svg',727// 'image/png': 'some png'728// },729// metadata: {},730// output_type: 'display_data'731// },732// expectedMimeTypesOrder: ['image/png', 'image/svg+xml', 'text/plain']733// }734// ];735736// dataAndExpectedOrder.forEach(({ output, expectedMimeTypesOrder }) => {737// const sortedOutputs = jupyterCellOutputToCellOutput(output);738// const mimeTypes = sortedOutputs.items.map((item) => item.mime).join(',');739// assert.equal(mimeTypes, expectedMimeTypesOrder.join(','));740// });741// });742// });743});744745function raceTimeout<T>(promise: Thenable<T>, timeout: number, onTimeout?: () => void): Promise<T | undefined> {746let promiseResolve: ((value: T | undefined) => void) | undefined = undefined;747748const timer = setTimeout(() => {749promiseResolve?.(undefined);750onTimeout?.();751}, timeout);752753return Promise.race([754Promise.resolve(promise).then(755result => {756clearTimeout(timer);757return result;758},759err => {760clearTimeout(timer);761throw err;762}763),764new Promise<T | undefined>(resolve => promiseResolve = resolve)765]);766}767768769