Path: blob/main/extensions/ipynb/src/test/serializers.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 { jupyterCellOutputToCellOutput, jupyterNotebookModelToNotebookData } from '../deserializers';10import { createMarkdownCellFromNotebookCell, getCellMetadata } from '../serializers';1112function deepStripProperties(obj: any, props: string[]) {13for (const prop in obj) {14if (obj[prop]) {15delete obj[prop];16} else if (typeof obj[prop] === 'object') {17deepStripProperties(obj[prop], props);18}19}20}21suite(`ipynb serializer`, () => {22let disposables: vscode.Disposable[] = [];23setup(() => {24disposables = [];25});26teardown(async () => {27disposables.forEach(d => d.dispose());28disposables = [];29sinon.restore();30});3132const base64EncodedImage =33'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mOUlZL6DwAB/wFSU1jVmgAAAABJRU5ErkJggg==';34test('Deserialize', async () => {35const cells: nbformat.ICell[] = [36{37cell_type: 'code',38execution_count: 10,39outputs: [],40source: 'print(1)',41metadata: {}42},43{44cell_type: 'code',45outputs: [],46source: 'print(2)',47metadata: {}48},49{50cell_type: 'markdown',51source: '# HEAD',52metadata: {}53}54];55const notebook = jupyterNotebookModelToNotebookData({ cells }, 'python');56assert.ok(notebook);5758const expectedCodeCell = new vscode.NotebookCellData(vscode.NotebookCellKind.Code, 'print(1)', 'python');59expectedCodeCell.outputs = [];60expectedCodeCell.metadata = { execution_count: 10, metadata: {} };61expectedCodeCell.executionSummary = { executionOrder: 10 };6263const expectedCodeCell2 = new vscode.NotebookCellData(vscode.NotebookCellKind.Code, 'print(2)', 'python');64expectedCodeCell2.outputs = [];65expectedCodeCell2.metadata = { execution_count: null, metadata: {} };66expectedCodeCell2.executionSummary = {};6768const expectedMarkdownCell = new vscode.NotebookCellData(vscode.NotebookCellKind.Markup, '# HEAD', 'markdown');69expectedMarkdownCell.outputs = [];70expectedMarkdownCell.metadata = {71metadata: {}72};7374assert.deepStrictEqual(notebook.cells, [expectedCodeCell, expectedCodeCell2, expectedMarkdownCell]);75});767778test('Serialize', async () => {79const markdownCell = new vscode.NotebookCellData(vscode.NotebookCellKind.Markup, '# header1', 'markdown');80markdownCell.metadata = {81attachments: {82'image.png': {83'image/png': 'abc'84}85},86id: '123',87metadata: {88foo: 'bar'89}90};9192const cellMetadata = getCellMetadata({ cell: markdownCell });93assert.deepStrictEqual(cellMetadata, {94id: '123',95metadata: {96foo: 'bar',97},98attachments: {99'image.png': {100'image/png': 'abc'101}102}103});104105const markdownCell2 = new vscode.NotebookCellData(vscode.NotebookCellKind.Markup, '# header1', 'markdown');106markdownCell2.metadata = {107id: '123',108metadata: {109foo: 'bar'110},111attachments: {112'image.png': {113'image/png': 'abc'114}115}116};117118const nbMarkdownCell = createMarkdownCellFromNotebookCell(markdownCell);119const nbMarkdownCell2 = createMarkdownCellFromNotebookCell(markdownCell2);120assert.deepStrictEqual(nbMarkdownCell, nbMarkdownCell2);121122assert.deepStrictEqual(nbMarkdownCell, {123cell_type: 'markdown',124source: ['# header1'],125metadata: {126foo: 'bar',127},128attachments: {129'image.png': {130'image/png': 'abc'131}132},133id: '123'134});135});136137suite('Outputs', () => {138function validateCellOutputTranslation(139outputs: nbformat.IOutput[],140expectedOutputs: vscode.NotebookCellOutput[],141propertiesToExcludeFromComparison: string[] = []142) {143const cells: nbformat.ICell[] = [144{145cell_type: 'code',146execution_count: 10,147outputs,148source: 'print(1)',149metadata: {}150}151];152const notebook = jupyterNotebookModelToNotebookData({ cells }, 'python');153154// OutputItems contain an `id` property generated by VSC.155// Exclude that property when comparing.156const propertiesToExclude = propertiesToExcludeFromComparison.concat(['id']);157const actualOuts = notebook.cells[0].outputs;158deepStripProperties(actualOuts, propertiesToExclude);159deepStripProperties(expectedOutputs, propertiesToExclude);160assert.deepStrictEqual(actualOuts, expectedOutputs);161}162163test('Empty output', () => {164validateCellOutputTranslation([], []);165});166167test('Stream output', () => {168validateCellOutputTranslation(169[170{171output_type: 'stream',172name: 'stderr',173text: 'Error'174},175{176output_type: 'stream',177name: 'stdout',178text: 'NoError'179}180],181[182new vscode.NotebookCellOutput([vscode.NotebookCellOutputItem.stderr('Error')], {183outputType: 'stream'184}),185new vscode.NotebookCellOutput([vscode.NotebookCellOutputItem.stdout('NoError')], {186outputType: 'stream'187})188]189);190});191test('Stream output and line endings', () => {192validateCellOutputTranslation(193[194{195output_type: 'stream',196name: 'stdout',197text: [198'Line1\n',199'\n',200'Line3\n',201'Line4'202]203}204],205[206new vscode.NotebookCellOutput([vscode.NotebookCellOutputItem.stdout('Line1\n\nLine3\nLine4')], {207outputType: 'stream'208})209]210);211validateCellOutputTranslation(212[213{214output_type: 'stream',215name: 'stdout',216text: [217'Hello\n',218'Hello\n',219'Hello\n',220'Hello\n',221'Hello\n',222'Hello\n'223]224}225],226[227new vscode.NotebookCellOutput([vscode.NotebookCellOutputItem.stdout('Hello\nHello\nHello\nHello\nHello\nHello\n')], {228outputType: 'stream'229})230]231);232});233test('Multi-line Stream output', () => {234validateCellOutputTranslation(235[236{237name: 'stdout',238output_type: 'stream',239text: [240'Epoch 1/5\n',241'...\n',242'Epoch 2/5\n',243'...\n',244'Epoch 3/5\n',245'...\n',246'Epoch 4/5\n',247'...\n',248'Epoch 5/5\n',249'...\n'250]251}252],253[254new vscode.NotebookCellOutput([vscode.NotebookCellOutputItem.stdout(['Epoch 1/5\n',255'...\n',256'Epoch 2/5\n',257'...\n',258'Epoch 3/5\n',259'...\n',260'Epoch 4/5\n',261'...\n',262'Epoch 5/5\n',263'...\n'].join(''))], {264outputType: 'stream'265})266]267);268});269270test('Multi-line Stream output (last empty line should not be saved in ipynb)', () => {271validateCellOutputTranslation(272[273{274name: 'stderr',275output_type: 'stream',276text: [277'Epoch 1/5\n',278'...\n',279'Epoch 2/5\n',280'...\n',281'Epoch 3/5\n',282'...\n',283'Epoch 4/5\n',284'...\n',285'Epoch 5/5\n',286'...\n'287]288}289],290[291new vscode.NotebookCellOutput([vscode.NotebookCellOutputItem.stderr(['Epoch 1/5\n',292'...\n',293'Epoch 2/5\n',294'...\n',295'Epoch 3/5\n',296'...\n',297'Epoch 4/5\n',298'...\n',299'Epoch 5/5\n',300'...\n',301// This last empty line should not be saved in ipynb.302'\n'].join(''))], {303outputType: 'stream'304})305]306);307});308309test('Streamed text with Ansi characters', async () => {310validateCellOutputTranslation(311[312{313name: 'stderr',314text: '\u001b[K\u001b[33m✅ \u001b[0m Loading\n',315output_type: 'stream'316}317],318[319new vscode.NotebookCellOutput(320[vscode.NotebookCellOutputItem.stderr('\u001b[K\u001b[33m✅ \u001b[0m Loading\n')],321{322outputType: 'stream'323}324)325]326);327});328329test('Streamed text with angle bracket characters', async () => {330validateCellOutputTranslation(331[332{333name: 'stderr',334text: '1 is < 2',335output_type: 'stream'336}337],338[339new vscode.NotebookCellOutput([vscode.NotebookCellOutputItem.stderr('1 is < 2')], {340outputType: 'stream'341})342]343);344});345346test('Streamed text with angle bracket characters and ansi chars', async () => {347validateCellOutputTranslation(348[349{350name: 'stderr',351text: '1 is < 2\u001b[K\u001b[33m✅ \u001b[0m Loading\n',352output_type: 'stream'353}354],355[356new vscode.NotebookCellOutput(357[vscode.NotebookCellOutputItem.stderr('1 is < 2\u001b[K\u001b[33m✅ \u001b[0m Loading\n')],358{359outputType: 'stream'360}361)362]363);364});365366test('Error', async () => {367validateCellOutputTranslation(368[369{370ename: 'Error Name',371evalue: 'Error Value',372traceback: ['stack1', 'stack2', 'stack3'],373output_type: 'error'374}375],376[377new vscode.NotebookCellOutput(378[379vscode.NotebookCellOutputItem.error({380name: 'Error Name',381message: 'Error Value',382stack: ['stack1', 'stack2', 'stack3'].join('\n')383})384],385{386outputType: 'error',387originalError: {388ename: 'Error Name',389evalue: 'Error Value',390traceback: ['stack1', 'stack2', 'stack3'],391output_type: 'error'392}393}394)395]396);397});398399['display_data', 'execute_result'].forEach(output_type => {400suite(`Rich output for output_type = ${output_type}`, () => {401// Properties to exclude when comparing.402let propertiesToExcludeFromComparison: string[] = [];403setup(() => {404if (output_type === 'display_data') {405// With display_data the execution_count property will never exist in the output.406// We can ignore that (as it will never exist).407// But we leave it in the case of `output_type === 'execute_result'`408propertiesToExcludeFromComparison = ['execution_count', 'executionCount'];409}410});411412test('Text mimeType output', async () => {413validateCellOutputTranslation(414[415{416data: {417'text/plain': 'Hello World!'418},419output_type,420metadata: {},421execution_count: 1422}423],424[425new vscode.NotebookCellOutput(426[new vscode.NotebookCellOutputItem(Buffer.from('Hello World!', 'utf8'), 'text/plain')],427{428outputType: output_type,429metadata: {}, // display_data & execute_result always have metadata.430executionCount: 1431}432)433],434propertiesToExcludeFromComparison435);436});437438test('png,jpeg images', async () => {439validateCellOutputTranslation(440[441{442execution_count: 1,443data: {444'image/png': base64EncodedImage,445'image/jpeg': base64EncodedImage446},447metadata: {},448output_type449}450],451[452new vscode.NotebookCellOutput(453[454new vscode.NotebookCellOutputItem(Buffer.from(base64EncodedImage, 'base64'), 'image/png'),455new vscode.NotebookCellOutputItem(Buffer.from(base64EncodedImage, 'base64'), 'image/jpeg')456],457{458executionCount: 1,459outputType: output_type,460metadata: {} // display_data & execute_result always have metadata.461}462)463],464propertiesToExcludeFromComparison465);466});467468test('png image with a light background', async () => {469validateCellOutputTranslation(470[471{472execution_count: 1,473data: {474'image/png': base64EncodedImage475},476metadata: {477needs_background: 'light'478},479output_type480}481],482[483new vscode.NotebookCellOutput(484[new vscode.NotebookCellOutputItem(Buffer.from(base64EncodedImage, 'base64'), 'image/png')],485{486executionCount: 1,487metadata: {488needs_background: 'light'489},490outputType: output_type491}492)493],494propertiesToExcludeFromComparison495);496});497498test('png image with a dark background', async () => {499validateCellOutputTranslation(500[501{502execution_count: 1,503data: {504'image/png': base64EncodedImage505},506metadata: {507needs_background: 'dark'508},509output_type510}511],512[513new vscode.NotebookCellOutput(514[new vscode.NotebookCellOutputItem(Buffer.from(base64EncodedImage, 'base64'), 'image/png')],515{516executionCount: 1,517metadata: {518needs_background: 'dark'519},520outputType: output_type521}522)523],524propertiesToExcludeFromComparison525);526});527528test('png image with custom dimensions', async () => {529validateCellOutputTranslation(530[531{532execution_count: 1,533data: {534'image/png': base64EncodedImage535},536metadata: {537'image/png': { height: '111px', width: '999px' }538},539output_type540}541],542[543new vscode.NotebookCellOutput(544[new vscode.NotebookCellOutputItem(Buffer.from(base64EncodedImage, 'base64'), 'image/png')],545{546executionCount: 1,547metadata: {548'image/png': { height: '111px', width: '999px' }549},550outputType: output_type551}552)553],554propertiesToExcludeFromComparison555);556});557558test('png allowed to scroll', async () => {559validateCellOutputTranslation(560[561{562execution_count: 1,563data: {564'image/png': base64EncodedImage565},566metadata: {567unconfined: true,568'image/png': { width: '999px' }569},570output_type571}572],573[574new vscode.NotebookCellOutput(575[new vscode.NotebookCellOutputItem(Buffer.from(base64EncodedImage, 'base64'), 'image/png')],576{577executionCount: 1,578metadata: {579unconfined: true,580'image/png': { width: '999px' }581},582outputType: output_type583}584)585],586propertiesToExcludeFromComparison587);588});589});590});591});592593suite('Output Order', () => {594test('Verify order of outputs', async () => {595const dataAndExpectedOrder: { output: nbformat.IDisplayData; expectedMimeTypesOrder: string[] }[] = [596{597output: {598data: {599'application/vnd.vegalite.v4+json': 'some json',600'text/html': '<a>Hello</a>'601},602metadata: {},603output_type: 'display_data'604},605expectedMimeTypesOrder: ['application/vnd.vegalite.v4+json', 'text/html']606},607{608output: {609data: {610'application/vnd.vegalite.v4+json': 'some json',611'application/javascript': 'some js',612'text/plain': 'some text',613'text/html': '<a>Hello</a>'614},615metadata: {},616output_type: 'display_data'617},618expectedMimeTypesOrder: [619'application/vnd.vegalite.v4+json',620'text/html',621'application/javascript',622'text/plain'623]624},625{626output: {627data: {628'application/vnd.vegalite.v4+json': '', // Empty, should give preference to other mimetypes.629'application/javascript': 'some js',630'text/plain': 'some text',631'text/html': '<a>Hello</a>'632},633metadata: {},634output_type: 'display_data'635},636expectedMimeTypesOrder: [637'text/html',638'application/javascript',639'text/plain',640'application/vnd.vegalite.v4+json'641]642},643{644output: {645data: {646'text/plain': 'some text',647'text/html': '<a>Hello</a>'648},649metadata: {},650output_type: 'display_data'651},652expectedMimeTypesOrder: ['text/html', 'text/plain']653},654{655output: {656data: {657'application/javascript': 'some js',658'text/plain': 'some text'659},660metadata: {},661output_type: 'display_data'662},663expectedMimeTypesOrder: ['application/javascript', 'text/plain']664},665{666output: {667data: {668'image/svg+xml': 'some svg',669'text/plain': 'some text'670},671metadata: {},672output_type: 'display_data'673},674expectedMimeTypesOrder: ['image/svg+xml', 'text/plain']675},676{677output: {678data: {679'text/latex': 'some latex',680'text/plain': 'some text'681},682metadata: {},683output_type: 'display_data'684},685expectedMimeTypesOrder: ['text/latex', 'text/plain']686},687{688output: {689data: {690'application/vnd.jupyter.widget-view+json': 'some widget',691'text/plain': 'some text'692},693metadata: {},694output_type: 'display_data'695},696expectedMimeTypesOrder: ['application/vnd.jupyter.widget-view+json', 'text/plain']697},698{699output: {700data: {701'text/plain': 'some text',702'image/svg+xml': 'some svg',703'image/png': 'some png'704},705metadata: {},706output_type: 'display_data'707},708expectedMimeTypesOrder: ['image/png', 'image/svg+xml', 'text/plain']709}710];711712dataAndExpectedOrder.forEach(({ output, expectedMimeTypesOrder }) => {713const sortedOutputs = jupyterCellOutputToCellOutput(output);714const mimeTypes = sortedOutputs.items.map((item) => item.mime).join(',');715assert.equal(mimeTypes, expectedMimeTypesOrder.join(','));716});717});718});719});720721722