From 190a89dd1b679f6fe45be178d2b4e5e158fd1abf Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Tue, 3 Aug 2021 10:56:43 -0700 Subject: [PATCH] Port unit tests from vscode-jupyter to ipynb extension #129446 --- extensions/ipynb/src/test/index.ts | 40 ++ extensions/ipynb/src/test/serializers.test.ts | 394 ++++++++++++++++++ scripts/test-integration.sh | 3 + 3 files changed, 437 insertions(+) create mode 100644 extensions/ipynb/src/test/index.ts create mode 100644 extensions/ipynb/src/test/serializers.test.ts diff --git a/extensions/ipynb/src/test/index.ts b/extensions/ipynb/src/test/index.ts new file mode 100644 index 00000000000..0099ddd699f --- /dev/null +++ b/extensions/ipynb/src/test/index.ts @@ -0,0 +1,40 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +const path = require('path'); +const testRunner = require('../../../../test/integration/electron/testrunner'); + +const options: any = { + ui: 'tdd', + color: true, + timeout: 60000 +}; + +// These integration tests is being run in multiple environments (electron, web, remote) +// so we need to set the suite name based on the environment as the suite name is used +// for the test results file name +let suite = ''; +if (process.env.VSCODE_BROWSER) { + suite = `${process.env.VSCODE_BROWSER} Browser Integration .ipynb Tests`; +} else if (process.env.REMOTE_VSCODE) { + suite = 'Remote Integration .ipynb Tests'; +} else { + suite = 'Integration .ipynb Tests'; +} + +if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY) { + options.reporter = 'mocha-multi-reporters'; + options.reporterOptions = { + reporterEnabled: 'spec, mocha-junit-reporter', + mochaJunitReporterReporterOptions: { + testsuitesTitle: `${suite} ${process.platform}`, + mochaFile: path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${process.arch}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) + } + }; +} + +testRunner.configure(options); + +export = testRunner; diff --git a/extensions/ipynb/src/test/serializers.test.ts b/extensions/ipynb/src/test/serializers.test.ts new file mode 100644 index 00000000000..740eecc38d5 --- /dev/null +++ b/extensions/ipynb/src/test/serializers.test.ts @@ -0,0 +1,394 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { nbformat } from '@jupyterlab/coreutils'; +import * as assert from 'assert'; +import * as vscode from 'vscode'; +import { jupyterNotebookModelToNotebookData } from '../deserializers'; + +function deepStripProperties(obj: any, props: string[]) { + for (let prop in obj) { + if (obj[prop]) { + delete obj[prop]; + } else if (typeof obj[prop] === 'object') { + deepStripProperties(obj[prop], props); + } + } +} + +suite('ipynb serializer', () => { + const base64EncodedImage = + 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mOUlZL6DwAB/wFSU1jVmgAAAABJRU5ErkJggg=='; + test('Deserialize', async () => { + const cells: nbformat.ICell[] = [ + { + cell_type: 'code', + execution_count: 10, + outputs: [], + source: 'print(1)', + metadata: {} + }, + { + cell_type: 'markdown', + source: '# HEAD', + metadata: {} + } + ]; + const notebook = jupyterNotebookModelToNotebookData({ cells }, 'python'); + assert.ok(notebook); + + const expectedCodeCell = new vscode.NotebookCellData(vscode.NotebookCellKind.Code, 'print(1)', 'python'); + expectedCodeCell.outputs = []; + expectedCodeCell.metadata = { custom: { metadata: {} } }; + expectedCodeCell.executionSummary = { executionOrder: 10 }; + + const expectedMarkdownCell = new vscode.NotebookCellData(vscode.NotebookCellKind.Markup, '# HEAD', 'markdown'); + expectedMarkdownCell.outputs = []; + expectedMarkdownCell.metadata = { + custom: { metadata: {} } + }; + + assert.deepStrictEqual(notebook.cells, [expectedCodeCell, expectedMarkdownCell]); + }); + + suite('Outputs', () => { + function validateCellOutputTranslation( + outputs: nbformat.IOutput[], + expectedOutputs: vscode.NotebookCellOutput[], + propertiesToExcludeFromComparison: string[] = [] + ) { + const cells: nbformat.ICell[] = [ + { + cell_type: 'code', + execution_count: 10, + outputs, + source: 'print(1)', + metadata: {} + } + ]; + const notebook = jupyterNotebookModelToNotebookData({ cells }, 'python'); + + // OutputItems contain an `id` property generated by VSC. + // Exclude that property when comparing. + const propertiesToExclude = propertiesToExcludeFromComparison.concat(['id']); + const actualOuts = notebook.cells[0].outputs; + deepStripProperties(actualOuts, propertiesToExclude); + deepStripProperties(expectedOutputs, propertiesToExclude); + assert.deepStrictEqual(actualOuts, expectedOutputs); + } + + test('Empty output', () => { + validateCellOutputTranslation([], []); + }); + + test('Stream output', () => { + validateCellOutputTranslation( + [ + { + output_type: 'stream', + name: 'stderr', + text: 'Error' + }, + { + output_type: 'stream', + name: 'stdout', + text: 'NoError' + } + ], + [ + new vscode.NotebookCellOutput([vscode.NotebookCellOutputItem.stderr('Error')], { + outputType: 'stream' + }), + new vscode.NotebookCellOutput([vscode.NotebookCellOutputItem.stdout('NoError')], { + outputType: 'stream' + }) + ] + ); + }); + + test('Streamed text with Ansi characters', async () => { + validateCellOutputTranslation( + [ + { + name: 'stderr', + text: '\u001b[K\u001b[33m✅ \u001b[0m Loading\n', + output_type: 'stream' + } + ], + [ + new vscode.NotebookCellOutput( + [vscode.NotebookCellOutputItem.stderr('\u001b[K\u001b[33m✅ \u001b[0m Loading\n')], + { + outputType: 'stream' + } + ) + ] + ); + }); + + test('Streamed text with angle bracket characters', async () => { + validateCellOutputTranslation( + [ + { + name: 'stderr', + text: '1 is < 2', + output_type: 'stream' + } + ], + [ + new vscode.NotebookCellOutput([vscode.NotebookCellOutputItem.stderr('1 is < 2')], { + outputType: 'stream' + }) + ] + ); + }); + + test('Streamed text with angle bracket characters and ansi chars', async () => { + validateCellOutputTranslation( + [ + { + name: 'stderr', + text: '1 is < 2\u001b[K\u001b[33m✅ \u001b[0m Loading\n', + output_type: 'stream' + } + ], + [ + new vscode.NotebookCellOutput( + [vscode.NotebookCellOutputItem.stderr('1 is < 2\u001b[K\u001b[33m✅ \u001b[0m Loading\n')], + { + outputType: 'stream' + } + ) + ] + ); + }); + + test('Error', async () => { + validateCellOutputTranslation( + [ + { + ename: 'Error Name', + evalue: 'Error Value', + traceback: ['stack1', 'stack2', 'stack3'], + output_type: 'error' + } + ], + [ + new vscode.NotebookCellOutput( + [ + vscode.NotebookCellOutputItem.error({ + name: 'Error Name', + message: 'Error Value', + stack: ['stack1', 'stack2', 'stack3'].join('\n') + }) + ], + { + outputType: 'error', + originalError: { + ename: 'Error Name', + evalue: 'Error Value', + traceback: ['stack1', 'stack2', 'stack3'], + output_type: 'error' + } + } + ) + ] + ); + }); + + ['display_data', 'execute_result'].forEach(output_type => { + suite(`Rich output for output_type = ${output_type}`, () => { + // Properties to exclude when comparing. + let propertiesToExcludeFromComparison: string[] = []; + setup(() => { + if (output_type === 'display_data') { + // With display_data the execution_count property will never exist in the output. + // We can ignore that (as it will never exist). + // But we leave it in the case of `output_type === 'execute_result'` + propertiesToExcludeFromComparison = ['execution_count', 'executionCount']; + } + }); + + test('Text mimeType output', async () => { + validateCellOutputTranslation( + [ + { + data: { + 'text/plain': 'Hello World!' + }, + output_type, + metadata: {}, + execution_count: 1 + } + ], + [ + new vscode.NotebookCellOutput( + [new vscode.NotebookCellOutputItem(Buffer.from('Hello World!', 'utf8'), 'text/plain')], + { + outputType: output_type, + metadata: {}, // display_data & execute_result always have metadata. + executionCount: 1 + } + ) + ], + propertiesToExcludeFromComparison + ); + }); + + test('png,jpeg images', async () => { + validateCellOutputTranslation( + [ + { + execution_count: 1, + data: { + 'image/png': base64EncodedImage, + 'image/jpeg': base64EncodedImage + }, + metadata: {}, + output_type + } + ], + [ + new vscode.NotebookCellOutput( + [ + new vscode.NotebookCellOutputItem(Buffer.from(base64EncodedImage, 'base64'), 'image/png'), + new vscode.NotebookCellOutputItem(Buffer.from(base64EncodedImage, 'base64'), 'image/jpeg') + ], + { + executionCount: 1, + outputType: output_type, + metadata: {} // display_data & execute_result always have metadata. + } + ) + ], + propertiesToExcludeFromComparison + ); + }); + + test('png image with a light background', async () => { + validateCellOutputTranslation( + [ + { + execution_count: 1, + data: { + 'image/png': base64EncodedImage + }, + metadata: { + needs_background: 'light' + }, + output_type + } + ], + [ + new vscode.NotebookCellOutput( + [new vscode.NotebookCellOutputItem(Buffer.from(base64EncodedImage, 'base64'), 'image/png')], + { + executionCount: 1, + metadata: { + needs_background: 'light' + }, + outputType: output_type + } + ) + ], + propertiesToExcludeFromComparison + ); + }); + + test('png image with a dark background', async () => { + validateCellOutputTranslation( + [ + { + execution_count: 1, + data: { + 'image/png': base64EncodedImage + }, + metadata: { + needs_background: 'dark' + }, + output_type + } + ], + [ + new vscode.NotebookCellOutput( + [new vscode.NotebookCellOutputItem(Buffer.from(base64EncodedImage, 'base64'), 'image/png')], + { + executionCount: 1, + metadata: { + needs_background: 'dark' + }, + outputType: output_type + } + ) + ], + propertiesToExcludeFromComparison + ); + }); + + test('png image with custom dimensions', async () => { + validateCellOutputTranslation( + [ + { + execution_count: 1, + data: { + 'image/png': base64EncodedImage + }, + metadata: { + 'image/png': { height: '111px', width: '999px' } + }, + output_type + } + ], + [ + new vscode.NotebookCellOutput( + [new vscode.NotebookCellOutputItem(Buffer.from(base64EncodedImage, 'base64'), 'image/png')], + { + executionCount: 1, + metadata: { + 'image/png': { height: '111px', width: '999px' } + }, + outputType: output_type + } + ) + ], + propertiesToExcludeFromComparison + ); + }); + + test('png allowed to scroll', async () => { + validateCellOutputTranslation( + [ + { + execution_count: 1, + data: { + 'image/png': base64EncodedImage + }, + metadata: { + unconfined: true, + 'image/png': { width: '999px' } + }, + output_type + } + ], + [ + new vscode.NotebookCellOutput( + [new vscode.NotebookCellOutputItem(Buffer.from(base64EncodedImage, 'base64'), 'image/png')], + { + executionCount: 1, + metadata: { + unconfined: true, + 'image/png': { width: '999px' } + }, + outputType: output_type + } + ) + ], + propertiesToExcludeFromComparison + ); + }); + }); + }); + }); +}); diff --git a/scripts/test-integration.sh b/scripts/test-integration.sh index 4e6115f1c7d..ba9b332d7cf 100755 --- a/scripts/test-integration.sh +++ b/scripts/test-integration.sh @@ -89,6 +89,9 @@ after_suite "$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_EXTRA_ARGS $(mktemp -d 2>/dev/null) --enable-proposed-api=vscode.git --extensionDevelopmentPath=$ROOT/extensions/git --extensionTestsPath=$ROOT/extensions/git/out/test $ALL_PLATFORMS_API_TESTS_EXTRA_ARGS after_suite +"$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_EXTRA_ARGS $(mktemp -d 2>/dev/null) --extensionDevelopmentPath=$ROOT/extensions/ipynb --extensionTestsPath=$ROOT/extensions/ipynb/out/test $ALL_PLATFORMS_API_TESTS_EXTRA_ARGS +after_suite + # Tests standalone (CommonJS)