Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/test/simulation/notebookEdits.stest.ts
13388 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 assert from 'assert';
7
import type { NotebookCell } from 'vscode';
8
import { IAlternativeNotebookContentService } from '../../src/platform/notebook/common/alternativeContent';
9
import { ITestingServicesAccessor, TestingServiceCollection } from '../../src/platform/test/node/services';
10
import { SimulationAlternativeNotebookContentService } from '../../src/platform/test/node/simulationWorkspaceServices';
11
import { NotebookCellData, NotebookCellKind } from '../../src/vscodeTypes';
12
import { ssuite, stest } from '../base/stest';
13
import { simulatePanelCodeMapper } from './panelCodeMapperSimulator';
14
import { assertWorkspaceEdit, fromFixture, toFile } from './stestUtil';
15
import { EditTestStrategy, IScenario } from './types';
16
17
18
export function notebookCellToCellData(cell: NotebookCell): NotebookCellData {
19
const cellData = new NotebookCellData(cell.kind, cell.document.getText(), cell.document.languageId);
20
cellData.metadata = cell.metadata;
21
cellData.executionSummary = cell.executionSummary;
22
if (cell.outputs.length) {
23
cellData.outputs = [...cell.outputs];
24
}
25
return cellData;
26
}
27
28
(['xml', 'json', 'text'] as const).forEach(format => {
29
function onBeforeStart(accessor: ITestingServicesAccessor) {
30
const altContentService = accessor.get<IAlternativeNotebookContentService>(IAlternativeNotebookContentService) as SimulationAlternativeNotebookContentService;
31
altContentService.format = format;
32
}
33
function simulatePanelCodeMapperEx(
34
testingServiceCollection: TestingServiceCollection,
35
scenario: IScenario
36
): Promise<void> {
37
scenario.onBeforeStart = onBeforeStart;
38
return simulatePanelCodeMapper(testingServiceCollection, scenario, EditTestStrategy.Edits);
39
}
40
41
ssuite({ title: `notebookEdits`, subtitle: `modification - ${format}`, location: 'panel' }, () => {
42
stest({ description: 'code cell modification', language: 'python' }, async (testingServiceCollection) => {
43
const file = fromFixture('notebook/edits/single.ipynb');
44
return simulatePanelCodeMapperEx(testingServiceCollection, {
45
files: [file],
46
queries: [
47
{
48
file: 'single.ipynb',
49
activeCell: 0,
50
selection: [0, 0, 0, 0],
51
query: 'Please add a docstring to the circle_area function describing its purpose and what it returns.',
52
validate: async (outcome, workspace, accessor) => {
53
const notebookDocument = workspace.getNotebookDocuments()[0];
54
if (!notebookDocument) {
55
assert.fail('no notebook document');
56
}
57
58
assertWorkspaceEdit(outcome);
59
assert.equal(notebookDocument.cellCount, 1);
60
const cell = notebookDocument.cellAt(0);
61
assert.ok(cell.document.getText().toLowerCase().indexOf('"""') > 0, `docstring not found in ${cell.document.getText()}`);
62
}
63
},
64
]
65
});
66
});
67
68
stest({ description: 'code cell insertion', language: 'python' }, async (testingServiceCollection) => {
69
const file = fromFixture('notebook/edits/single.ipynb');
70
return simulatePanelCodeMapperEx(testingServiceCollection, {
71
files: [file],
72
queries: [
73
{
74
file: 'single.ipynb',
75
activeCell: 0,
76
selection: [0, 0, 0, 0],
77
query: 'Please add a new cell to test the function.',
78
validate: async (outcome, workspace, accessor) => {
79
const notebookDocument = workspace.getNotebookDocuments()[0];
80
if (!notebookDocument) {
81
assert.fail('no notebook document');
82
}
83
84
assertWorkspaceEdit(outcome);
85
assert.equal(notebookDocument.cellCount, 2);
86
}
87
}
88
]
89
});
90
});
91
92
93
stest({ description: 'code cell modification, plotting', language: 'python' }, async (testingServiceCollection) => {
94
const file = fromFixture('notebook/edits/plot.ipynb');
95
return simulatePanelCodeMapperEx(testingServiceCollection, {
96
files: [file],
97
queries: [
98
{
99
file: 'plot.ipynb',
100
activeCell: 0,
101
selection: [0, 0, 0, 0],
102
query: 'Please update the code to also include a scatter plot of the same data on the same figure, using red markers',
103
validate: async (outcome, workspace, accessor) => {
104
const notebookDocument = workspace.getNotebookDocuments()[0];
105
if (!notebookDocument) {
106
assert.fail('no notebook document');
107
}
108
109
assertWorkspaceEdit(outcome);
110
assert.equal(notebookDocument.cellCount, 1);
111
assert.ok(notebookDocument.cellAt(0).document.getText().includes('plt.scatter'), 'scatter plot added');
112
113
}
114
}
115
]
116
});
117
});
118
119
stest({ description: 'code cell modification, convert Point2D code to Point3D', language: 'python' }, async (testingServiceCollection) => {
120
const file = fromFixture('notebook/edits/point.ipynb');
121
return simulatePanelCodeMapperEx(testingServiceCollection, {
122
files: [file],
123
queries: [
124
{
125
file: 'point.ipynb',
126
activeCell: 0,
127
selection: [0, 0, 0, 0],
128
query: 'Convert the code in Point2D to a Point3D class',
129
validate: async (outcome, workspace, accessor) => {
130
const notebookDocument = workspace.getNotebookDocuments()[0];
131
if (!notebookDocument) {
132
assert.fail('no notebook document');
133
}
134
135
assertWorkspaceEdit(outcome);
136
assert.equal(notebookDocument.cellCount, 2);
137
assert.ok(notebookDocument.cellAt(0).document.getText().includes('class Point3D'), 'Point3D class not found');
138
assert.ok(notebookDocument.cellAt(1).document.getText().includes('distance_from_origin(point: Point3D)') || notebookDocument.cellAt(1).document.getText().includes(`distance_from_origin(point: 'Point3D')`), 'distance_from_origin not updated');
139
}
140
}
141
]
142
});
143
});
144
145
// stest({ description: 'code cell refactoring, plotly code -> matplotlib', language: 'python' }, async (testingServiceCollection) => {
146
// const file = fromFixture('notebook/edits/plotly_to_matplotlib.ipynb');
147
// return simulatePanelCodeMapperEx(testingServiceCollection, {
148
// files: [file],
149
// queries: [
150
// {
151
// file: 'plotly_to_matplotlib.ipynb',
152
// activeCell: 0,
153
// selection: [0, 0, 0, 0],
154
// query: 'Refactor the code so that purchases are stored in a dictionary keyed by customer_id. Each value should be a list of (product_id, quantity, price). Then update any code that computes total spend and ensure the plotting is done using matplotlibRefactor the code to use matplotlib instead of plotly for the plots.',
155
// validate: async (outcome, workspace, accessor) => {
156
// const notebookDocument = workspace.getNotebookDocuments()[0];
157
// if (!notebookDocument) {
158
// assert.fail('no notebook document');
159
// }
160
161
// assertWorkspaceEdit(outcome);
162
163
// const firstImportCell = notebookDocument.getCells().find(c => c.document.getText().includes('import pandas'));
164
// assert.ok(firstImportCell?.document.getText().includes('import matplotlib'), `Should contain 'import matplotlib' statements: ${firstImportCell?.document.getText()}`);
165
// assert.ok(!firstImportCell?.document.getText().includes('import plotly.express'), `Should not contain 'import plotly.express' statements: ${firstImportCell?.document.getText()}`);
166
// assert.ok(!firstImportCell?.document.getText().includes('import plotly.graph'), `Should not contain 'import plotly.graph' statements: ${firstImportCell?.document.getText()}`);
167
// assert.ok(notebookDocument.getCells().some(c => c.document.getText().includes('plt.')), `Should contain 'plt.plot' statements`);
168
// }
169
// }
170
// ]
171
// });
172
// });
173
174
stest({ description: 'cell refactoring, plot refactoring', language: 'python' }, async (testingServiceCollection) => {
175
const file = fromFixture('notebook/edits/data_visualization.ipynb');
176
return simulatePanelCodeMapperEx(testingServiceCollection, {
177
files: [file],
178
queries: [
179
{
180
file: 'data_visualization.ipynb',
181
activeCell: 0,
182
selection: [0, 0, 0, 0],
183
query: 'Modify the plot function to add a new parameter title. This parameter should allow users to set a custom title for the plot. Add titles to all sales plots.',
184
validate: async (outcome, workspace, accessor) => {
185
const notebookDocument = workspace.getNotebookDocuments()[0];
186
if (!notebookDocument) {
187
assert.fail('no notebook document');
188
}
189
190
assertWorkspaceEdit(outcome);
191
192
assert.ok(notebookDocument.cellAt(5).document.getText().includes('title'), `Should contain 'title' statements: ${notebookDocument.cellAt(5).document.getText()}`);
193
assert.ok(notebookDocument.cellAt(7).document.getText().includes('title'), `Should contain 'title' statements: ${notebookDocument.cellAt(7).document.getText()}`);
194
assert.ok(notebookDocument.cellAt(9).document.getText().includes('title'), `Should contain 'title' statements: ${notebookDocument.cellAt(9).document.getText()}`);
195
196
}
197
}
198
]
199
});
200
});
201
202
// stest.skip({ description: 'remove single print statement from large notebook cell', language: 'python' }, async (testingServiceCollection) => {
203
// const file = fromFixture('notebook/edits/large_cell.ipynb');
204
// return simulatePanelCodeMapperEx(testingServiceCollection, {
205
// files: [file],
206
// queries: [
207
// {
208
// file: 'large_cell.ipynb',
209
// activeCell: 0,
210
// selection: [0, 0, 0, 0],
211
// query: 'Remove the print statement',
212
// validate: async (outcome, workspace, accessor) => {
213
// const notebookDocument = workspace.getNotebookDocuments()[0];
214
// if (!notebookDocument) {
215
// assert.fail('no notebook document');
216
// }
217
218
// assertWorkspaceEdit(outcome);
219
220
// assert.ok(!notebookDocument.cellAt(1).document.getText().includes('print'), `Should not contain 'print' statements: ${notebookDocument.cellAt(1).document.getText()}`);
221
// }
222
// }
223
// ]
224
// });
225
// });
226
227
stest({ description: 'new code cells in empty notebook', language: 'python' }, async (testingServiceCollection) => {
228
const file = fromFixture('notebook/edits/empty.ipynb');
229
return simulatePanelCodeMapperEx(testingServiceCollection, {
230
files: [file],
231
queries: [
232
{
233
file: 'empty.ipynb',
234
activeCell: 0,
235
selection: [0, 0, 0, 0],
236
query: 'Please add a new code cell that imports pandas and numpy.',
237
validate: async (outcome, workspace, accessor) => {
238
const notebookDocument = workspace.getNotebookDocuments()[0];
239
if (!notebookDocument) {
240
assert.fail('no notebook document');
241
}
242
243
assertWorkspaceEdit(outcome);
244
245
assert.ok(notebookDocument.cellAt(0).document.getText().includes('import pandas'), 'pandas not imported');
246
assert.ok(notebookDocument.cellAt(0).document.getText().includes('import numpy'), 'numpy not imported');
247
}
248
}
249
]
250
});
251
});
252
253
stest({ description: 'new julia code cells in empty notebook', language: 'julia' }, async (testingServiceCollection) => {
254
const file = fromFixture('notebook/edits/empty_julia.ipynb');
255
return simulatePanelCodeMapperEx(testingServiceCollection, {
256
files: [file],
257
queries: [
258
{
259
file: 'empty_julia.ipynb',
260
activeCell: 0,
261
selection: [0, 0, 0, 0],
262
query: 'Please add a new Julia code cell that calculates the factorial of a given number.',
263
validate: async (outcome, workspace, accessor) => {
264
const notebookDocument = workspace.getNotebookDocuments()[0];
265
if (!notebookDocument) {
266
assert.fail('no notebook document');
267
}
268
269
assertWorkspaceEdit(outcome);
270
271
assert.ok(notebookDocument.cellAt(0).document.languageId === 'julia', 'cell is not julia');
272
}
273
}
274
]
275
});
276
});
277
278
stest({ description: 'notebook code cell deletion', language: 'python' }, async (testingServiceCollection) => {
279
const file = fromFixture('notebook/edits/multicells.ipynb');
280
return simulatePanelCodeMapperEx(testingServiceCollection, {
281
files: [file],
282
queries: [
283
{
284
file: 'multicells.ipynb',
285
activeCell: 0,
286
selection: [0, 0, 0, 0],
287
query: 'Please remove the last code cell from the notebook.',
288
validate: async (outcome, workspace, accessor) => {
289
const notebookDocument = workspace.getNotebookDocuments()[0];
290
if (!notebookDocument) {
291
assert.fail('no notebook document');
292
}
293
294
assertWorkspaceEdit(outcome);
295
296
assert.ok(notebookDocument.cellCount === 2, 'Should have 2 cells remaining after deletion');
297
}
298
}
299
]
300
});
301
});
302
303
stest({ description: 're-organize python imports to top of the notebook', language: 'python' }, async (testingServiceCollection) => {
304
const file = fromFixture('notebook/edits/data_visualization_2.ipynb');
305
return simulatePanelCodeMapperEx(testingServiceCollection, {
306
files: [file],
307
queries: [
308
{
309
file: 'data_visualization_2.ipynb',
310
activeCell: 0,
311
selection: [0, 0, 0, 0],
312
query: 'Please move all import statements to the top of the notebook.',
313
validate: async (outcome, workspace, accessor) => {
314
const notebookDocument = workspace.getNotebookDocuments()[0];
315
if (!notebookDocument) {
316
assert.fail('no notebook document');
317
}
318
319
assertWorkspaceEdit(outcome);
320
321
const firstCodeCell = notebookDocument.getCells().filter(cell => cell.kind === NotebookCellKind.Code)[0];
322
assert.ok(firstCodeCell, 'no code cells');
323
assert.ok(firstCodeCell.document.getText().includes('import pandas as pd'), 'pandas not imported');
324
assert.ok(firstCodeCell.document.getText().includes('import matplotlib.pyplot as plt'), 'matplotlib not imported');
325
assert.ok(firstCodeCell.document.getText().includes('import seaborn as sns'), 'seaborn not imported');
326
}
327
}
328
]
329
});
330
});
331
332
stest({ description: 'Insert markdown cells explaining code', language: 'python' }, async (testingServiceCollection) => {
333
const file = fromFixture('notebook/edits/github.ipynb');
334
return simulatePanelCodeMapperEx(testingServiceCollection, {
335
files: [file],
336
queries: [
337
{
338
file: 'github.ipynb',
339
activeCell: 0,
340
selection: [0, 0, 0, 0],
341
query: 'I do not understand the code in the entire notebook, please add Markdown cells and comments clearly explaining the the output and the analysis performed by the code.',
342
validate: async (outcome, workspace, accessor) => {
343
const notebookDocument = workspace.getNotebookDocuments()[0];
344
if (!notebookDocument) {
345
assert.fail('no notebook document');
346
}
347
348
assertWorkspaceEdit(outcome);
349
350
const markdownCells = notebookDocument.getCells().filter(cell => cell.kind === NotebookCellKind.Markup);
351
assert.ok(markdownCells.length > 0, 'no markdown cells added');
352
353
assert.ok(markdownCells.some(md => md.document.getText().toLowerCase().includes('filter issues') || md.document.getText().toLowerCase().includes('filtered issues')), `Should have a markdown cell with 'filter issues'`);
354
assert.ok(markdownCells.some(md => md.document.getText().toLowerCase().includes('assignee')), `Should have a markdown cell with 'assignee'`);
355
assert.ok(markdownCells.some(md => md.document.getText().toLowerCase().includes('label')), `Should have a markdown cell with 'label'`);
356
357
}
358
}
359
]
360
});
361
});
362
363
stest({ description: 'code cell modification & insertion', language: 'python' }, async (testingServiceCollection) => {
364
const file = fromFixture('notebook/edits/multicells.ipynb');
365
return simulatePanelCodeMapperEx(testingServiceCollection, {
366
files: [file],
367
queries: [
368
{
369
file: 'multicells.ipynb',
370
activeCell: 0,
371
selection: [0, 0, 0, 0],
372
query: 'Please convert the numeric lists into NumPy arrays. Then create a new cell below the existing cells that plots the distribution of sepal lengths using matplotlib. Use any style you like for the plot.',
373
expectedIntent: 'edit',
374
validate: async (outcome, workspace, accessor) => {
375
const notebookDocument = workspace.getNotebookDocuments()[0];
376
if (!notebookDocument) {
377
assert.fail('no notebook document');
378
}
379
380
assertWorkspaceEdit(outcome);
381
382
assert.ok(notebookDocument.cellCount === 3, 'Should have 2 cells remaining after deletion');
383
}
384
},
385
]
386
});
387
});
388
389
stest({ description: 'code cell modification & deletion', language: 'python' }, async (testingServiceCollection) => {
390
const file = fromFixture('notebook/edits/multicells.ipynb');
391
return simulatePanelCodeMapperEx(testingServiceCollection, {
392
files: [file],
393
queries: [
394
{
395
file: 'multicells.ipynb',
396
activeCell: 0,
397
selection: [0, 0, 0, 0],
398
query: 'Please delete the last cell.',
399
expectedIntent: 'edit',
400
validate: async (outcome, workspace, accessor) => {
401
const notebookDocument = workspace.getNotebookDocuments()[0];
402
if (!notebookDocument) {
403
assert.fail('no notebook document');
404
}
405
406
assertWorkspaceEdit(outcome);
407
408
assert.ok(notebookDocument.cellCount === 2, 'Should have 2 cells remaining after deletion');
409
}
410
},
411
]
412
});
413
});
414
415
stest({ description: 'code cell modification with removal of unused imports', language: 'python' }, async (testingServiceCollection) => {
416
const file = fromFixture('notebook/edits/imports.ipynb');
417
return simulatePanelCodeMapperEx(testingServiceCollection, {
418
files: [file],
419
queries: [
420
{
421
file: 'imports.ipynb',
422
activeCell: 0,
423
selection: [0, 0, 0, 0],
424
query: 'Please delete unused imports.',
425
expectedIntent: 'edit',
426
validate: async (outcome, workspace, accessor) => {
427
const notebookDocument = workspace.getNotebookDocuments()[0];
428
if (!notebookDocument) {
429
assert.fail('no notebook document');
430
}
431
432
assertWorkspaceEdit(outcome);
433
434
// `import os` should be removed
435
notebookDocument.getCells().forEach(cell => {
436
assert.strictEqual(cell.document.getText().includes('import os'), false);
437
});
438
}
439
},
440
]
441
});
442
});
443
444
stest({ description: 'code cell re-ordering', language: 'python' }, async (testingServiceCollection) => {
445
const file = fromFixture('notebook/edits/reorder.ipynb');
446
return simulatePanelCodeMapperEx(testingServiceCollection, {
447
files: [file],
448
queries: [
449
{
450
file: 'reorder.ipynb',
451
activeCell: 0,
452
selection: [0, 0, 0, 0],
453
query: 'Please change order of the cells to ensure cell with imports are on top.',
454
expectedIntent: 'edit',
455
validate: async (outcome, workspace, accessor) => {
456
const notebookDocument = workspace.getNotebookDocuments()[0];
457
if (!notebookDocument) {
458
assert.fail('no notebook document');
459
}
460
461
assertWorkspaceEdit(outcome);
462
463
// First cell will contain imports and second cell print statement
464
assert.strictEqual(notebookDocument.cellCount, 2);
465
assert.strictEqual(notebookDocument.cellAt(0).document.getText().includes('import sys'), true);
466
assert.strictEqual(notebookDocument.cellAt(1).document.getText().includes('print'), true);
467
}
468
},
469
]
470
});
471
});
472
473
474
stest({ description: 'code cell refactoring, modification, insertion & delection of cells', language: 'python' }, async (testingServiceCollection) => {
475
const file = fromFixture('notebook/edits/matplotlib_to_plotly.ipynb');
476
return simulatePanelCodeMapperEx(testingServiceCollection, {
477
files: [file],
478
queries: [
479
{
480
file: 'matplotlib_to_plotly.ipynb',
481
activeCell: 0,
482
selection: [0, 0, 0, 0],
483
query: 'Replace Matplotlib with Plotly for the plots, remove redundant cells, remove print statements, reorder the second Markdown cell, and add a new code cell at the bottom with a pie chart of species counts. Add Markdown cells before each plot cell to describe the plot and the data.',
484
expectedIntent: 'edit',
485
validate: async (outcome, workspace, accessor) => {
486
const notebookDocument = workspace.getNotebookDocuments()[0];
487
if (!notebookDocument) {
488
assert.fail('no notebook document');
489
}
490
491
assertWorkspaceEdit(outcome);
492
493
// Initially 1 markdowncell and 3 code cells with a plot in each.
494
// After updates we should have at least 5 code cells with plots & 5 markdown cells.
495
const markdownCells = notebookDocument.getCells().filter(c => c.kind === NotebookCellKind.Markup);
496
const codeCells = notebookDocument.getCells().filter(c => c.kind === NotebookCellKind.Code);
497
498
assert.ok(markdownCells.length > 1, `Should have at least 2 markdown cells, got ${markdownCells.length}`);
499
assert.ok(codeCells.some(c => c.document.getText().includes('pie')), `Should have a code cell with a pie chart, got ${codeCells.map(c => c.document.getText()).join(',')}`);
500
}
501
},
502
]
503
});
504
});
505
});
506
507
ssuite({ title: 'notebookEdits', subtitle: `bug reports - ${format}`, location: 'panel' }, () => {
508
stest({ description: 'Issue #13868' }, async (testingServiceCollection) => {
509
try {
510
await simulatePanelCodeMapperEx(testingServiceCollection, {
511
files: [
512
toFile({
513
fileName: 'multiFile/issue-13868/data.csv',
514
fileContents: [
515
'Duration,Pulse,Maxpulse,Calories\n',
516
'60,110,130,409.1\n',
517
'60,117,145,479.0\n',
518
'60,103,135,340.0\n',
519
'45,109,175,282.4\n',
520
'45,117,148,406.0\n',
521
'60,102,127,300.0\n',
522
'60,110,136,374.0\n',
523
'45,104,134,253.3\n',
524
'30,109,133,195.1\n',
525
'60,98,124,269.0\n',
526
'60,103,147,329.3\n',
527
'60,100,120,250.7\n',
528
'60,106,128,345.3\n',
529
'60,104,132,379.3\n',
530
'60,98,123,275.0\n',
531
'60,98,120,215.2\n',
532
'60,100,120,300.0\n'
533
].join('')
534
}),
535
],
536
queries: [
537
{
538
file: undefined,
539
selection: undefined,
540
query: 'create a new notebook to analyze #file:data.csv ',
541
validate: async (outcome, workspace, accessor) => {
542
assertWorkspaceEdit(outcome);
543
// assert.strictEqual((await getWorkspaceDiagnostics(accessor, workspace, 'tsc')).filter(d => d.kind === 'syntactic').length, 0);
544
}
545
}
546
]
547
});
548
} catch (ex: unknown) {
549
assert.fail((ex as Error).message);
550
}
551
});
552
});
553
});
554