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