mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-03-16 11:03:09 -04:00
638 lines
15 KiB
TypeScript
638 lines
15 KiB
TypeScript
/*---------------------------------------------------------------------------------------------
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
|
*--------------------------------------------------------------------------------------------*/
|
|
|
|
import * as nbformat from '@jupyterlab/nbformat';
|
|
import * as assert from 'assert';
|
|
import * as vscode from 'vscode';
|
|
import { jupyterCellOutputToCellOutput, 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('Stream output and line endings', () => {
|
|
validateCellOutputTranslation(
|
|
[
|
|
{
|
|
output_type: 'stream',
|
|
name: 'stdout',
|
|
text: [
|
|
'Line1\n',
|
|
'\n',
|
|
'Line3\n',
|
|
'Line4'
|
|
]
|
|
}
|
|
],
|
|
[
|
|
new vscode.NotebookCellOutput([vscode.NotebookCellOutputItem.stdout('Line1\n\nLine3\nLine4')], {
|
|
outputType: 'stream'
|
|
})
|
|
]
|
|
);
|
|
validateCellOutputTranslation(
|
|
[
|
|
{
|
|
output_type: 'stream',
|
|
name: 'stdout',
|
|
text: [
|
|
'Hello\n',
|
|
'Hello\n',
|
|
'Hello\n',
|
|
'Hello\n',
|
|
'Hello\n',
|
|
'Hello\n'
|
|
]
|
|
}
|
|
],
|
|
[
|
|
new vscode.NotebookCellOutput([vscode.NotebookCellOutputItem.stdout('Hello\nHello\nHello\nHello\nHello\nHello\n')], {
|
|
outputType: 'stream'
|
|
})
|
|
]
|
|
);
|
|
});
|
|
test('Multi-line Stream output', () => {
|
|
validateCellOutputTranslation(
|
|
[
|
|
{
|
|
name: 'stdout',
|
|
output_type: 'stream',
|
|
text: [
|
|
'Epoch 1/5\n',
|
|
'...\n',
|
|
'Epoch 2/5\n',
|
|
'...\n',
|
|
'Epoch 3/5\n',
|
|
'...\n',
|
|
'Epoch 4/5\n',
|
|
'...\n',
|
|
'Epoch 5/5\n',
|
|
'...\n'
|
|
]
|
|
}
|
|
],
|
|
[
|
|
new vscode.NotebookCellOutput([vscode.NotebookCellOutputItem.stdout(['Epoch 1/5\n',
|
|
'...\n',
|
|
'Epoch 2/5\n',
|
|
'...\n',
|
|
'Epoch 3/5\n',
|
|
'...\n',
|
|
'Epoch 4/5\n',
|
|
'...\n',
|
|
'Epoch 5/5\n',
|
|
'...\n'].join(''))], {
|
|
outputType: 'stream'
|
|
})
|
|
]
|
|
);
|
|
});
|
|
|
|
test('Multi-line Stream output (last empty line should not be saved in ipynb)', () => {
|
|
validateCellOutputTranslation(
|
|
[
|
|
{
|
|
name: 'stderr',
|
|
output_type: 'stream',
|
|
text: [
|
|
'Epoch 1/5\n',
|
|
'...\n',
|
|
'Epoch 2/5\n',
|
|
'...\n',
|
|
'Epoch 3/5\n',
|
|
'...\n',
|
|
'Epoch 4/5\n',
|
|
'...\n',
|
|
'Epoch 5/5\n',
|
|
'...\n'
|
|
]
|
|
}
|
|
],
|
|
[
|
|
new vscode.NotebookCellOutput([vscode.NotebookCellOutputItem.stderr(['Epoch 1/5\n',
|
|
'...\n',
|
|
'Epoch 2/5\n',
|
|
'...\n',
|
|
'Epoch 3/5\n',
|
|
'...\n',
|
|
'Epoch 4/5\n',
|
|
'...\n',
|
|
'Epoch 5/5\n',
|
|
'...\n',
|
|
// This last empty line should not be saved in ipynb.
|
|
'\n'].join(''))], {
|
|
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
|
|
);
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
suite('Output Order', () => {
|
|
test('Verify order of outputs', async () => {
|
|
const dataAndExpectedOrder: { output: nbformat.IDisplayData; expectedMimeTypesOrder: string[] }[] = [
|
|
{
|
|
output: {
|
|
data: {
|
|
'application/vnd.vegalite.v4+json': 'some json',
|
|
'text/html': '<a>Hello</a>'
|
|
},
|
|
metadata: {},
|
|
output_type: 'display_data'
|
|
},
|
|
expectedMimeTypesOrder: ['application/vnd.vegalite.v4+json', 'text/html']
|
|
},
|
|
{
|
|
output: {
|
|
data: {
|
|
'application/vnd.vegalite.v4+json': 'some json',
|
|
'application/javascript': 'some js',
|
|
'text/plain': 'some text',
|
|
'text/html': '<a>Hello</a>'
|
|
},
|
|
metadata: {},
|
|
output_type: 'display_data'
|
|
},
|
|
expectedMimeTypesOrder: [
|
|
'application/vnd.vegalite.v4+json',
|
|
'text/html',
|
|
'application/javascript',
|
|
'text/plain'
|
|
]
|
|
},
|
|
{
|
|
output: {
|
|
data: {
|
|
'application/vnd.vegalite.v4+json': '', // Empty, should give preference to other mimetypes.
|
|
'application/javascript': 'some js',
|
|
'text/plain': 'some text',
|
|
'text/html': '<a>Hello</a>'
|
|
},
|
|
metadata: {},
|
|
output_type: 'display_data'
|
|
},
|
|
expectedMimeTypesOrder: [
|
|
'text/html',
|
|
'application/javascript',
|
|
'text/plain',
|
|
'application/vnd.vegalite.v4+json'
|
|
]
|
|
},
|
|
{
|
|
output: {
|
|
data: {
|
|
'text/plain': 'some text',
|
|
'text/html': '<a>Hello</a>'
|
|
},
|
|
metadata: {},
|
|
output_type: 'display_data'
|
|
},
|
|
expectedMimeTypesOrder: ['text/html', 'text/plain']
|
|
},
|
|
{
|
|
output: {
|
|
data: {
|
|
'application/javascript': 'some js',
|
|
'text/plain': 'some text'
|
|
},
|
|
metadata: {},
|
|
output_type: 'display_data'
|
|
},
|
|
expectedMimeTypesOrder: ['application/javascript', 'text/plain']
|
|
},
|
|
{
|
|
output: {
|
|
data: {
|
|
'image/svg+xml': 'some svg',
|
|
'text/plain': 'some text'
|
|
},
|
|
metadata: {},
|
|
output_type: 'display_data'
|
|
},
|
|
expectedMimeTypesOrder: ['image/svg+xml', 'text/plain']
|
|
},
|
|
{
|
|
output: {
|
|
data: {
|
|
'text/latex': 'some latex',
|
|
'text/plain': 'some text'
|
|
},
|
|
metadata: {},
|
|
output_type: 'display_data'
|
|
},
|
|
expectedMimeTypesOrder: ['text/latex', 'text/plain']
|
|
},
|
|
{
|
|
output: {
|
|
data: {
|
|
'application/vnd.jupyter.widget-view+json': 'some widget',
|
|
'text/plain': 'some text'
|
|
},
|
|
metadata: {},
|
|
output_type: 'display_data'
|
|
},
|
|
expectedMimeTypesOrder: ['application/vnd.jupyter.widget-view+json', 'text/plain']
|
|
},
|
|
{
|
|
output: {
|
|
data: {
|
|
'text/plain': 'some text',
|
|
'image/svg+xml': 'some svg',
|
|
'image/png': 'some png'
|
|
},
|
|
metadata: {},
|
|
output_type: 'display_data'
|
|
},
|
|
expectedMimeTypesOrder: ['image/png', 'image/svg+xml', 'text/plain']
|
|
}
|
|
];
|
|
|
|
dataAndExpectedOrder.forEach(({ output, expectedMimeTypesOrder }) => {
|
|
const sortedOutputs = jupyterCellOutputToCellOutput(output);
|
|
const mimeTypes = sortedOutputs.items.map((item) => item.mime).join(',');
|
|
assert.equal(mimeTypes, expectedMimeTypesOrder.join(','));
|
|
});
|
|
});
|
|
});
|
|
});
|