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