Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/ipynb/src/test/notebookModelStoreSync.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 assert from 'assert';
7
import * as sinon from 'sinon';
8
import { CancellationTokenSource, Disposable, EventEmitter, ExtensionContext, NotebookCellKind, NotebookDocumentChangeEvent, NotebookDocumentWillSaveEvent, NotebookEdit, NotebookRange, TextDocumentSaveReason, workspace, type CancellationToken, type NotebookCell, type NotebookDocument, type WorkspaceEdit, type WorkspaceEditMetadata } from 'vscode';
9
import { activate } from '../notebookModelStoreSync';
10
11
suite(`Notebook Model Store Sync`, () => {
12
let disposables: Disposable[] = [];
13
let onDidChangeNotebookDocument: EventEmitter<NotebookDocumentChangeEvent>;
14
let onWillSaveNotebookDocument: AsyncEmitter<NotebookDocumentWillSaveEvent>;
15
let notebook: NotebookDocument;
16
let token: CancellationTokenSource;
17
let editsApplied: WorkspaceEdit[] = [];
18
let pendingPromises: Promise<void>[] = [];
19
let cellMetadataUpdates: NotebookEdit[] = [];
20
let applyEditStub: sinon.SinonStub<[edit: WorkspaceEdit, metadata?: WorkspaceEditMetadata | undefined], Thenable<boolean>>;
21
setup(() => {
22
disposables = [];
23
notebook = {
24
notebookType: '',
25
metadata: {}
26
} as NotebookDocument;
27
token = new CancellationTokenSource();
28
disposables.push(token);
29
sinon.stub(notebook, 'notebookType').get(() => 'jupyter-notebook');
30
applyEditStub = sinon.stub(workspace, 'applyEdit').callsFake((edit: WorkspaceEdit) => {
31
editsApplied.push(edit);
32
return Promise.resolve(true);
33
});
34
const context = { subscriptions: [] as Disposable[] } as ExtensionContext;
35
onDidChangeNotebookDocument = new EventEmitter<NotebookDocumentChangeEvent>();
36
disposables.push(onDidChangeNotebookDocument);
37
onWillSaveNotebookDocument = new AsyncEmitter<NotebookDocumentWillSaveEvent>();
38
39
sinon.stub(NotebookEdit, 'updateCellMetadata').callsFake((index, metadata) => {
40
const edit = (NotebookEdit.updateCellMetadata as any).wrappedMethod.call(NotebookEdit, index, metadata);
41
cellMetadataUpdates.push(edit);
42
return edit;
43
}
44
);
45
sinon.stub(workspace, 'onDidChangeNotebookDocument').callsFake(cb =>
46
onDidChangeNotebookDocument.event(cb)
47
);
48
sinon.stub(workspace, 'onWillSaveNotebookDocument').callsFake(cb =>
49
onWillSaveNotebookDocument.event(cb)
50
);
51
activate(context);
52
});
53
teardown(async () => {
54
await Promise.allSettled(pendingPromises);
55
editsApplied = [];
56
pendingPromises = [];
57
cellMetadataUpdates = [];
58
disposables.forEach(d => d.dispose());
59
disposables = [];
60
sinon.restore();
61
});
62
63
test('Empty cell will not result in any updates', async () => {
64
const e: NotebookDocumentChangeEvent = {
65
notebook,
66
metadata: undefined,
67
contentChanges: [],
68
cellChanges: []
69
};
70
71
onDidChangeNotebookDocument.fire(e);
72
73
assert.strictEqual(editsApplied.length, 0);
74
});
75
test('Adding cell for non Jupyter Notebook will not result in any updates', async () => {
76
sinon.stub(notebook, 'notebookType').get(() => 'some-other-type');
77
const cell: NotebookCell = {
78
document: {} as any,
79
executionSummary: {},
80
index: 0,
81
kind: NotebookCellKind.Code,
82
metadata: {},
83
notebook,
84
outputs: []
85
};
86
const e: NotebookDocumentChangeEvent = {
87
notebook,
88
metadata: undefined,
89
contentChanges: [
90
{
91
range: new NotebookRange(0, 0),
92
removedCells: [],
93
addedCells: [cell]
94
}
95
],
96
cellChanges: []
97
};
98
99
onDidChangeNotebookDocument.fire(e);
100
101
assert.strictEqual(editsApplied.length, 0);
102
assert.strictEqual(cellMetadataUpdates.length, 0);
103
});
104
test('Adding cell to nbformat 4.2 notebook will result in adding empty metadata', async () => {
105
sinon.stub(notebook, 'metadata').get(() => ({ nbformat: 4, nbformat_minor: 2 }));
106
const cell: NotebookCell = {
107
document: {} as any,
108
executionSummary: {},
109
index: 0,
110
kind: NotebookCellKind.Code,
111
metadata: {},
112
notebook,
113
outputs: []
114
};
115
const e: NotebookDocumentChangeEvent = {
116
notebook,
117
metadata: undefined,
118
contentChanges: [
119
{
120
range: new NotebookRange(0, 0),
121
removedCells: [],
122
addedCells: [cell]
123
}
124
],
125
cellChanges: []
126
};
127
128
onDidChangeNotebookDocument.fire(e);
129
130
assert.strictEqual(editsApplied.length, 1);
131
assert.strictEqual(cellMetadataUpdates.length, 1);
132
const newMetadata = cellMetadataUpdates[0].newCellMetadata;
133
assert.deepStrictEqual(newMetadata, { execution_count: null, metadata: {} });
134
});
135
test('Added cell will have a cell id if nbformat is 4.5', async () => {
136
sinon.stub(notebook, 'metadata').get(() => ({ nbformat: 4, nbformat_minor: 5 }));
137
const cell: NotebookCell = {
138
document: {} as any,
139
executionSummary: {},
140
index: 0,
141
kind: NotebookCellKind.Code,
142
metadata: {},
143
notebook,
144
outputs: []
145
};
146
const e: NotebookDocumentChangeEvent = {
147
notebook,
148
metadata: undefined,
149
contentChanges: [
150
{
151
range: new NotebookRange(0, 0),
152
removedCells: [],
153
addedCells: [cell]
154
}
155
],
156
cellChanges: []
157
};
158
159
onDidChangeNotebookDocument.fire(e);
160
161
assert.strictEqual(editsApplied.length, 1);
162
assert.strictEqual(cellMetadataUpdates.length, 1);
163
const newMetadata = cellMetadataUpdates[0].newCellMetadata || {};
164
assert.strictEqual(Object.keys(newMetadata).length, 3);
165
assert.deepStrictEqual(newMetadata.execution_count, null);
166
assert.deepStrictEqual(newMetadata.metadata, {});
167
assert.ok(newMetadata.id);
168
});
169
test('Do not add cell id if one already exists', async () => {
170
sinon.stub(notebook, 'metadata').get(() => ({ nbformat: 4, nbformat_minor: 5 }));
171
const cell: NotebookCell = {
172
document: {} as any,
173
executionSummary: {},
174
index: 0,
175
kind: NotebookCellKind.Code,
176
metadata: {
177
id: '1234'
178
},
179
notebook,
180
outputs: []
181
};
182
const e: NotebookDocumentChangeEvent = {
183
notebook,
184
metadata: undefined,
185
contentChanges: [
186
{
187
range: new NotebookRange(0, 0),
188
removedCells: [],
189
addedCells: [cell]
190
}
191
],
192
cellChanges: []
193
};
194
195
onDidChangeNotebookDocument.fire(e);
196
197
assert.strictEqual(editsApplied.length, 1);
198
assert.strictEqual(cellMetadataUpdates.length, 1);
199
const newMetadata = cellMetadataUpdates[0].newCellMetadata || {};
200
assert.strictEqual(Object.keys(newMetadata).length, 3);
201
assert.deepStrictEqual(newMetadata.execution_count, null);
202
assert.deepStrictEqual(newMetadata.metadata, {});
203
assert.strictEqual(newMetadata.id, '1234');
204
});
205
test('Do not perform any updates if cell id and metadata exists', async () => {
206
sinon.stub(notebook, 'metadata').get(() => ({ nbformat: 4, nbformat_minor: 5 }));
207
const cell: NotebookCell = {
208
document: {} as any,
209
executionSummary: {},
210
index: 0,
211
kind: NotebookCellKind.Code,
212
metadata: {
213
id: '1234',
214
metadata: {}
215
},
216
notebook,
217
outputs: []
218
};
219
const e: NotebookDocumentChangeEvent = {
220
notebook,
221
metadata: undefined,
222
contentChanges: [
223
{
224
range: new NotebookRange(0, 0),
225
removedCells: [],
226
addedCells: [cell]
227
}
228
],
229
cellChanges: []
230
};
231
232
onDidChangeNotebookDocument.fire(e);
233
234
assert.strictEqual(editsApplied.length, 0);
235
assert.strictEqual(cellMetadataUpdates.length, 0);
236
});
237
test('Store language id in custom metadata, whilst preserving existing metadata', async () => {
238
sinon.stub(notebook, 'metadata').get(() => ({
239
nbformat: 4, nbformat_minor: 5,
240
metadata: {
241
language_info: { name: 'python' }
242
}
243
}));
244
const cell: NotebookCell = {
245
document: {
246
languageId: 'javascript'
247
} as any,
248
executionSummary: {},
249
index: 0,
250
kind: NotebookCellKind.Code,
251
metadata: {
252
id: '1234',
253
metadata: {
254
collapsed: true, scrolled: true
255
}
256
},
257
notebook,
258
outputs: []
259
};
260
const e: NotebookDocumentChangeEvent = {
261
notebook,
262
metadata: undefined,
263
contentChanges: [],
264
cellChanges: [
265
{
266
cell,
267
document: {
268
languageId: 'javascript'
269
} as any,
270
metadata: undefined,
271
outputs: undefined,
272
executionSummary: undefined
273
}
274
]
275
};
276
277
onDidChangeNotebookDocument.fire(e);
278
279
assert.strictEqual(editsApplied.length, 1);
280
assert.strictEqual(cellMetadataUpdates.length, 1);
281
const newMetadata = cellMetadataUpdates[0].newCellMetadata || {};
282
assert.strictEqual(Object.keys(newMetadata).length, 3);
283
assert.deepStrictEqual(newMetadata.execution_count, null);
284
assert.deepStrictEqual(newMetadata.metadata, { collapsed: true, scrolled: true, vscode: { languageId: 'javascript' } });
285
assert.strictEqual(newMetadata.id, '1234');
286
});
287
test('No changes when language is javascript', async () => {
288
sinon.stub(notebook, 'metadata').get(() => ({
289
nbformat: 4, nbformat_minor: 5,
290
metadata: {
291
language_info: { name: 'javascript' }
292
}
293
}));
294
const cell: NotebookCell = {
295
document: {
296
languageId: 'javascript'
297
} as any,
298
executionSummary: {},
299
index: 0,
300
kind: NotebookCellKind.Code,
301
metadata: {
302
id: '1234',
303
metadata: {
304
collapsed: true, scrolled: true
305
}
306
},
307
notebook,
308
outputs: []
309
};
310
const e: NotebookDocumentChangeEvent = {
311
notebook,
312
metadata: undefined,
313
contentChanges: [],
314
cellChanges: [
315
{
316
cell,
317
document: undefined,
318
metadata: undefined,
319
outputs: undefined,
320
executionSummary: undefined
321
}
322
]
323
};
324
325
onDidChangeNotebookDocument.fire(e);
326
327
assert.strictEqual(editsApplied.length, 0);
328
assert.strictEqual(cellMetadataUpdates.length, 0);
329
});
330
test('Remove language from metadata when cell language matches kernel language', async () => {
331
sinon.stub(notebook, 'metadata').get(() => ({
332
nbformat: 4, nbformat_minor: 5,
333
metadata: {
334
language_info: { name: 'javascript' }
335
}
336
}));
337
const cell: NotebookCell = {
338
document: {
339
languageId: 'javascript'
340
} as any,
341
executionSummary: {},
342
index: 0,
343
kind: NotebookCellKind.Code,
344
metadata: {
345
id: '1234',
346
metadata: {
347
vscode: { languageId: 'python' },
348
collapsed: true, scrolled: true
349
}
350
},
351
notebook,
352
outputs: []
353
};
354
const e: NotebookDocumentChangeEvent = {
355
notebook,
356
metadata: undefined,
357
contentChanges: [],
358
cellChanges: [
359
{
360
cell,
361
document: {
362
languageId: 'javascript'
363
} as any,
364
metadata: undefined,
365
outputs: undefined,
366
executionSummary: undefined
367
}
368
]
369
};
370
371
onDidChangeNotebookDocument.fire(e);
372
373
assert.strictEqual(editsApplied.length, 1);
374
assert.strictEqual(cellMetadataUpdates.length, 1);
375
const newMetadata = cellMetadataUpdates[0].newCellMetadata || {};
376
assert.strictEqual(Object.keys(newMetadata).length, 3);
377
assert.deepStrictEqual(newMetadata.execution_count, null);
378
assert.deepStrictEqual(newMetadata.metadata, { collapsed: true, scrolled: true });
379
assert.strictEqual(newMetadata.id, '1234');
380
});
381
test('Update language in metadata', async () => {
382
sinon.stub(notebook, 'metadata').get(() => ({
383
nbformat: 4, nbformat_minor: 5,
384
metadata: {
385
language_info: { name: 'javascript' }
386
}
387
}));
388
const cell: NotebookCell = {
389
document: {
390
languageId: 'powershell'
391
} as any,
392
executionSummary: {},
393
index: 0,
394
kind: NotebookCellKind.Code,
395
metadata: {
396
id: '1234',
397
metadata: {
398
vscode: { languageId: 'python' },
399
collapsed: true, scrolled: true
400
}
401
},
402
notebook,
403
outputs: []
404
};
405
const e: NotebookDocumentChangeEvent = {
406
notebook,
407
metadata: undefined,
408
contentChanges: [],
409
cellChanges: [
410
{
411
cell,
412
document: {
413
languageId: 'powershell'
414
} as any,
415
metadata: undefined,
416
outputs: undefined,
417
executionSummary: undefined
418
}
419
]
420
};
421
422
onDidChangeNotebookDocument.fire(e);
423
424
assert.strictEqual(editsApplied.length, 1);
425
assert.strictEqual(cellMetadataUpdates.length, 1);
426
const newMetadata = cellMetadataUpdates[0].newCellMetadata || {};
427
assert.strictEqual(Object.keys(newMetadata).length, 3);
428
assert.deepStrictEqual(newMetadata.execution_count, null);
429
assert.deepStrictEqual(newMetadata.metadata, { collapsed: true, scrolled: true, vscode: { languageId: 'powershell' } });
430
assert.strictEqual(newMetadata.id, '1234');
431
});
432
433
test('Will save event without any changes', async () => {
434
await onWillSaveNotebookDocument.fireAsync({ notebook, reason: TextDocumentSaveReason.Manual }, token.token);
435
});
436
test('Wait for pending updates to complete when saving', async () => {
437
let resolveApplyEditPromise: (value: boolean) => void;
438
const promise = new Promise<boolean>((resolve) => resolveApplyEditPromise = resolve);
439
applyEditStub.restore();
440
sinon.stub(workspace, 'applyEdit').callsFake((edit: WorkspaceEdit) => {
441
editsApplied.push(edit);
442
return promise;
443
});
444
445
const cell: NotebookCell = {
446
document: {} as any,
447
executionSummary: {},
448
index: 0,
449
kind: NotebookCellKind.Code,
450
metadata: {},
451
notebook,
452
outputs: []
453
};
454
const e: NotebookDocumentChangeEvent = {
455
notebook,
456
metadata: undefined,
457
contentChanges: [
458
{
459
range: new NotebookRange(0, 0),
460
removedCells: [],
461
addedCells: [cell]
462
}
463
],
464
cellChanges: []
465
};
466
467
onDidChangeNotebookDocument.fire(e);
468
469
assert.strictEqual(editsApplied.length, 1);
470
assert.strictEqual(cellMetadataUpdates.length, 1);
471
472
// Try to save.
473
let saveCompleted = false;
474
const saved = onWillSaveNotebookDocument.fireAsync({
475
notebook,
476
reason: TextDocumentSaveReason.Manual
477
}, token.token);
478
saved.finally(() => saveCompleted = true);
479
await new Promise((resolve) => setTimeout(resolve, 10));
480
481
// Verify we have not yet completed saving.
482
assert.strictEqual(saveCompleted, false);
483
484
resolveApplyEditPromise!(true);
485
await new Promise((resolve) => setTimeout(resolve, 1));
486
487
// Should have completed saving.
488
saved.finally(() => saveCompleted = true);
489
});
490
491
interface IWaitUntil {
492
token: CancellationToken;
493
waitUntil(thenable: Promise<unknown>): void;
494
}
495
496
interface IWaitUntil {
497
token: CancellationToken;
498
waitUntil(thenable: Promise<unknown>): void;
499
}
500
type IWaitUntilData<T> = Omit<Omit<T, 'waitUntil'>, 'token'>;
501
502
class AsyncEmitter<T extends IWaitUntil> {
503
private listeners: ((d: T) => void)[] = [];
504
get event(): (listener: (e: T) => any, thisArgs?: any, disposables?: Disposable[]) => Disposable {
505
506
return (listener, thisArgs, _disposables) => {
507
this.listeners.push(listener.bind(thisArgs));
508
return {
509
dispose: () => {
510
//
511
}
512
};
513
};
514
}
515
dispose() {
516
this.listeners = [];
517
}
518
async fireAsync(data: IWaitUntilData<T>, token: CancellationToken): Promise<void> {
519
if (!this.listeners.length) {
520
return;
521
}
522
523
const promises: Promise<unknown>[] = [];
524
this.listeners.forEach(cb => {
525
const event = {
526
...data,
527
token,
528
waitUntil: (thenable: Promise<WorkspaceEdit>) => {
529
promises.push(thenable);
530
}
531
} as T;
532
cb(event);
533
});
534
535
await Promise.all(promises);
536
}
537
}
538
});
539
540