Path: blob/main/extensions/copilot/test/simulation/notebookEdits.stest.ts
13388 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import assert from 'assert';6import type { NotebookCell } from 'vscode';7import { IAlternativeNotebookContentService } from '../../src/platform/notebook/common/alternativeContent';8import { ITestingServicesAccessor, TestingServiceCollection } from '../../src/platform/test/node/services';9import { SimulationAlternativeNotebookContentService } from '../../src/platform/test/node/simulationWorkspaceServices';10import { NotebookCellData, NotebookCellKind } from '../../src/vscodeTypes';11import { ssuite, stest } from '../base/stest';12import { simulatePanelCodeMapper } from './panelCodeMapperSimulator';13import { assertWorkspaceEdit, fromFixture, toFile } from './stestUtil';14import { EditTestStrategy, IScenario } from './types';151617export function notebookCellToCellData(cell: NotebookCell): NotebookCellData {18const cellData = new NotebookCellData(cell.kind, cell.document.getText(), cell.document.languageId);19cellData.metadata = cell.metadata;20cellData.executionSummary = cell.executionSummary;21if (cell.outputs.length) {22cellData.outputs = [...cell.outputs];23}24return cellData;25}2627(['xml', 'json', 'text'] as const).forEach(format => {28function onBeforeStart(accessor: ITestingServicesAccessor) {29const altContentService = accessor.get<IAlternativeNotebookContentService>(IAlternativeNotebookContentService) as SimulationAlternativeNotebookContentService;30altContentService.format = format;31}32function simulatePanelCodeMapperEx(33testingServiceCollection: TestingServiceCollection,34scenario: IScenario35): Promise<void> {36scenario.onBeforeStart = onBeforeStart;37return simulatePanelCodeMapper(testingServiceCollection, scenario, EditTestStrategy.Edits);38}3940ssuite({ title: `notebookEdits`, subtitle: `modification - ${format}`, location: 'panel' }, () => {41stest({ description: 'code cell modification', language: 'python' }, async (testingServiceCollection) => {42const file = fromFixture('notebook/edits/single.ipynb');43return simulatePanelCodeMapperEx(testingServiceCollection, {44files: [file],45queries: [46{47file: 'single.ipynb',48activeCell: 0,49selection: [0, 0, 0, 0],50query: 'Please add a docstring to the circle_area function describing its purpose and what it returns.',51validate: async (outcome, workspace, accessor) => {52const notebookDocument = workspace.getNotebookDocuments()[0];53if (!notebookDocument) {54assert.fail('no notebook document');55}5657assertWorkspaceEdit(outcome);58assert.equal(notebookDocument.cellCount, 1);59const cell = notebookDocument.cellAt(0);60assert.ok(cell.document.getText().toLowerCase().indexOf('"""') > 0, `docstring not found in ${cell.document.getText()}`);61}62},63]64});65});6667stest({ description: 'code cell insertion', language: 'python' }, async (testingServiceCollection) => {68const file = fromFixture('notebook/edits/single.ipynb');69return simulatePanelCodeMapperEx(testingServiceCollection, {70files: [file],71queries: [72{73file: 'single.ipynb',74activeCell: 0,75selection: [0, 0, 0, 0],76query: 'Please add a new cell to test the function.',77validate: async (outcome, workspace, accessor) => {78const notebookDocument = workspace.getNotebookDocuments()[0];79if (!notebookDocument) {80assert.fail('no notebook document');81}8283assertWorkspaceEdit(outcome);84assert.equal(notebookDocument.cellCount, 2);85}86}87]88});89});909192stest({ description: 'code cell modification, plotting', language: 'python' }, async (testingServiceCollection) => {93const file = fromFixture('notebook/edits/plot.ipynb');94return simulatePanelCodeMapperEx(testingServiceCollection, {95files: [file],96queries: [97{98file: 'plot.ipynb',99activeCell: 0,100selection: [0, 0, 0, 0],101query: 'Please update the code to also include a scatter plot of the same data on the same figure, using red markers',102validate: async (outcome, workspace, accessor) => {103const notebookDocument = workspace.getNotebookDocuments()[0];104if (!notebookDocument) {105assert.fail('no notebook document');106}107108assertWorkspaceEdit(outcome);109assert.equal(notebookDocument.cellCount, 1);110assert.ok(notebookDocument.cellAt(0).document.getText().includes('plt.scatter'), 'scatter plot added');111112}113}114]115});116});117118stest({ description: 'code cell modification, convert Point2D code to Point3D', language: 'python' }, async (testingServiceCollection) => {119const file = fromFixture('notebook/edits/point.ipynb');120return simulatePanelCodeMapperEx(testingServiceCollection, {121files: [file],122queries: [123{124file: 'point.ipynb',125activeCell: 0,126selection: [0, 0, 0, 0],127query: 'Convert the code in Point2D to a Point3D class',128validate: async (outcome, workspace, accessor) => {129const notebookDocument = workspace.getNotebookDocuments()[0];130if (!notebookDocument) {131assert.fail('no notebook document');132}133134assertWorkspaceEdit(outcome);135assert.equal(notebookDocument.cellCount, 2);136assert.ok(notebookDocument.cellAt(0).document.getText().includes('class Point3D'), 'Point3D class not found');137assert.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');138}139}140]141});142});143144// stest({ description: 'code cell refactoring, plotly code -> matplotlib', language: 'python' }, async (testingServiceCollection) => {145// const file = fromFixture('notebook/edits/plotly_to_matplotlib.ipynb');146// return simulatePanelCodeMapperEx(testingServiceCollection, {147// files: [file],148// queries: [149// {150// file: 'plotly_to_matplotlib.ipynb',151// activeCell: 0,152// selection: [0, 0, 0, 0],153// 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.',154// validate: async (outcome, workspace, accessor) => {155// const notebookDocument = workspace.getNotebookDocuments()[0];156// if (!notebookDocument) {157// assert.fail('no notebook document');158// }159160// assertWorkspaceEdit(outcome);161162// const firstImportCell = notebookDocument.getCells().find(c => c.document.getText().includes('import pandas'));163// assert.ok(firstImportCell?.document.getText().includes('import matplotlib'), `Should contain 'import matplotlib' statements: ${firstImportCell?.document.getText()}`);164// assert.ok(!firstImportCell?.document.getText().includes('import plotly.express'), `Should not contain 'import plotly.express' statements: ${firstImportCell?.document.getText()}`);165// assert.ok(!firstImportCell?.document.getText().includes('import plotly.graph'), `Should not contain 'import plotly.graph' statements: ${firstImportCell?.document.getText()}`);166// assert.ok(notebookDocument.getCells().some(c => c.document.getText().includes('plt.')), `Should contain 'plt.plot' statements`);167// }168// }169// ]170// });171// });172173stest({ description: 'cell refactoring, plot refactoring', language: 'python' }, async (testingServiceCollection) => {174const file = fromFixture('notebook/edits/data_visualization.ipynb');175return simulatePanelCodeMapperEx(testingServiceCollection, {176files: [file],177queries: [178{179file: 'data_visualization.ipynb',180activeCell: 0,181selection: [0, 0, 0, 0],182query: '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.',183validate: async (outcome, workspace, accessor) => {184const notebookDocument = workspace.getNotebookDocuments()[0];185if (!notebookDocument) {186assert.fail('no notebook document');187}188189assertWorkspaceEdit(outcome);190191assert.ok(notebookDocument.cellAt(5).document.getText().includes('title'), `Should contain 'title' statements: ${notebookDocument.cellAt(5).document.getText()}`);192assert.ok(notebookDocument.cellAt(7).document.getText().includes('title'), `Should contain 'title' statements: ${notebookDocument.cellAt(7).document.getText()}`);193assert.ok(notebookDocument.cellAt(9).document.getText().includes('title'), `Should contain 'title' statements: ${notebookDocument.cellAt(9).document.getText()}`);194195}196}197]198});199});200201// stest.skip({ description: 'remove single print statement from large notebook cell', language: 'python' }, async (testingServiceCollection) => {202// const file = fromFixture('notebook/edits/large_cell.ipynb');203// return simulatePanelCodeMapperEx(testingServiceCollection, {204// files: [file],205// queries: [206// {207// file: 'large_cell.ipynb',208// activeCell: 0,209// selection: [0, 0, 0, 0],210// query: 'Remove the print statement',211// validate: async (outcome, workspace, accessor) => {212// const notebookDocument = workspace.getNotebookDocuments()[0];213// if (!notebookDocument) {214// assert.fail('no notebook document');215// }216217// assertWorkspaceEdit(outcome);218219// assert.ok(!notebookDocument.cellAt(1).document.getText().includes('print'), `Should not contain 'print' statements: ${notebookDocument.cellAt(1).document.getText()}`);220// }221// }222// ]223// });224// });225226stest({ description: 'new code cells in empty notebook', language: 'python' }, async (testingServiceCollection) => {227const file = fromFixture('notebook/edits/empty.ipynb');228return simulatePanelCodeMapperEx(testingServiceCollection, {229files: [file],230queries: [231{232file: 'empty.ipynb',233activeCell: 0,234selection: [0, 0, 0, 0],235query: 'Please add a new code cell that imports pandas and numpy.',236validate: async (outcome, workspace, accessor) => {237const notebookDocument = workspace.getNotebookDocuments()[0];238if (!notebookDocument) {239assert.fail('no notebook document');240}241242assertWorkspaceEdit(outcome);243244assert.ok(notebookDocument.cellAt(0).document.getText().includes('import pandas'), 'pandas not imported');245assert.ok(notebookDocument.cellAt(0).document.getText().includes('import numpy'), 'numpy not imported');246}247}248]249});250});251252stest({ description: 'new julia code cells in empty notebook', language: 'julia' }, async (testingServiceCollection) => {253const file = fromFixture('notebook/edits/empty_julia.ipynb');254return simulatePanelCodeMapperEx(testingServiceCollection, {255files: [file],256queries: [257{258file: 'empty_julia.ipynb',259activeCell: 0,260selection: [0, 0, 0, 0],261query: 'Please add a new Julia code cell that calculates the factorial of a given number.',262validate: async (outcome, workspace, accessor) => {263const notebookDocument = workspace.getNotebookDocuments()[0];264if (!notebookDocument) {265assert.fail('no notebook document');266}267268assertWorkspaceEdit(outcome);269270assert.ok(notebookDocument.cellAt(0).document.languageId === 'julia', 'cell is not julia');271}272}273]274});275});276277stest({ description: 'notebook code cell deletion', language: 'python' }, async (testingServiceCollection) => {278const file = fromFixture('notebook/edits/multicells.ipynb');279return simulatePanelCodeMapperEx(testingServiceCollection, {280files: [file],281queries: [282{283file: 'multicells.ipynb',284activeCell: 0,285selection: [0, 0, 0, 0],286query: 'Please remove the last code cell from the notebook.',287validate: async (outcome, workspace, accessor) => {288const notebookDocument = workspace.getNotebookDocuments()[0];289if (!notebookDocument) {290assert.fail('no notebook document');291}292293assertWorkspaceEdit(outcome);294295assert.ok(notebookDocument.cellCount === 2, 'Should have 2 cells remaining after deletion');296}297}298]299});300});301302stest({ description: 're-organize python imports to top of the notebook', language: 'python' }, async (testingServiceCollection) => {303const file = fromFixture('notebook/edits/data_visualization_2.ipynb');304return simulatePanelCodeMapperEx(testingServiceCollection, {305files: [file],306queries: [307{308file: 'data_visualization_2.ipynb',309activeCell: 0,310selection: [0, 0, 0, 0],311query: 'Please move all import statements to the top of the notebook.',312validate: async (outcome, workspace, accessor) => {313const notebookDocument = workspace.getNotebookDocuments()[0];314if (!notebookDocument) {315assert.fail('no notebook document');316}317318assertWorkspaceEdit(outcome);319320const firstCodeCell = notebookDocument.getCells().filter(cell => cell.kind === NotebookCellKind.Code)[0];321assert.ok(firstCodeCell, 'no code cells');322assert.ok(firstCodeCell.document.getText().includes('import pandas as pd'), 'pandas not imported');323assert.ok(firstCodeCell.document.getText().includes('import matplotlib.pyplot as plt'), 'matplotlib not imported');324assert.ok(firstCodeCell.document.getText().includes('import seaborn as sns'), 'seaborn not imported');325}326}327]328});329});330331stest({ description: 'Insert markdown cells explaining code', language: 'python' }, async (testingServiceCollection) => {332const file = fromFixture('notebook/edits/github.ipynb');333return simulatePanelCodeMapperEx(testingServiceCollection, {334files: [file],335queries: [336{337file: 'github.ipynb',338activeCell: 0,339selection: [0, 0, 0, 0],340query: '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.',341validate: async (outcome, workspace, accessor) => {342const notebookDocument = workspace.getNotebookDocuments()[0];343if (!notebookDocument) {344assert.fail('no notebook document');345}346347assertWorkspaceEdit(outcome);348349const markdownCells = notebookDocument.getCells().filter(cell => cell.kind === NotebookCellKind.Markup);350assert.ok(markdownCells.length > 0, 'no markdown cells added');351352assert.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'`);353assert.ok(markdownCells.some(md => md.document.getText().toLowerCase().includes('assignee')), `Should have a markdown cell with 'assignee'`);354assert.ok(markdownCells.some(md => md.document.getText().toLowerCase().includes('label')), `Should have a markdown cell with 'label'`);355356}357}358]359});360});361362stest({ description: 'code cell modification & insertion', language: 'python' }, async (testingServiceCollection) => {363const file = fromFixture('notebook/edits/multicells.ipynb');364return simulatePanelCodeMapperEx(testingServiceCollection, {365files: [file],366queries: [367{368file: 'multicells.ipynb',369activeCell: 0,370selection: [0, 0, 0, 0],371query: '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.',372expectedIntent: 'edit',373validate: async (outcome, workspace, accessor) => {374const notebookDocument = workspace.getNotebookDocuments()[0];375if (!notebookDocument) {376assert.fail('no notebook document');377}378379assertWorkspaceEdit(outcome);380381assert.ok(notebookDocument.cellCount === 3, 'Should have 2 cells remaining after deletion');382}383},384]385});386});387388stest({ description: 'code cell modification & deletion', language: 'python' }, async (testingServiceCollection) => {389const file = fromFixture('notebook/edits/multicells.ipynb');390return simulatePanelCodeMapperEx(testingServiceCollection, {391files: [file],392queries: [393{394file: 'multicells.ipynb',395activeCell: 0,396selection: [0, 0, 0, 0],397query: 'Please delete the last cell.',398expectedIntent: 'edit',399validate: async (outcome, workspace, accessor) => {400const notebookDocument = workspace.getNotebookDocuments()[0];401if (!notebookDocument) {402assert.fail('no notebook document');403}404405assertWorkspaceEdit(outcome);406407assert.ok(notebookDocument.cellCount === 2, 'Should have 2 cells remaining after deletion');408}409},410]411});412});413414stest({ description: 'code cell modification with removal of unused imports', language: 'python' }, async (testingServiceCollection) => {415const file = fromFixture('notebook/edits/imports.ipynb');416return simulatePanelCodeMapperEx(testingServiceCollection, {417files: [file],418queries: [419{420file: 'imports.ipynb',421activeCell: 0,422selection: [0, 0, 0, 0],423query: 'Please delete unused imports.',424expectedIntent: 'edit',425validate: async (outcome, workspace, accessor) => {426const notebookDocument = workspace.getNotebookDocuments()[0];427if (!notebookDocument) {428assert.fail('no notebook document');429}430431assertWorkspaceEdit(outcome);432433// `import os` should be removed434notebookDocument.getCells().forEach(cell => {435assert.strictEqual(cell.document.getText().includes('import os'), false);436});437}438},439]440});441});442443stest({ description: 'code cell re-ordering', language: 'python' }, async (testingServiceCollection) => {444const file = fromFixture('notebook/edits/reorder.ipynb');445return simulatePanelCodeMapperEx(testingServiceCollection, {446files: [file],447queries: [448{449file: 'reorder.ipynb',450activeCell: 0,451selection: [0, 0, 0, 0],452query: 'Please change order of the cells to ensure cell with imports are on top.',453expectedIntent: 'edit',454validate: async (outcome, workspace, accessor) => {455const notebookDocument = workspace.getNotebookDocuments()[0];456if (!notebookDocument) {457assert.fail('no notebook document');458}459460assertWorkspaceEdit(outcome);461462// First cell will contain imports and second cell print statement463assert.strictEqual(notebookDocument.cellCount, 2);464assert.strictEqual(notebookDocument.cellAt(0).document.getText().includes('import sys'), true);465assert.strictEqual(notebookDocument.cellAt(1).document.getText().includes('print'), true);466}467},468]469});470});471472473stest({ description: 'code cell refactoring, modification, insertion & delection of cells', language: 'python' }, async (testingServiceCollection) => {474const file = fromFixture('notebook/edits/matplotlib_to_plotly.ipynb');475return simulatePanelCodeMapperEx(testingServiceCollection, {476files: [file],477queries: [478{479file: 'matplotlib_to_plotly.ipynb',480activeCell: 0,481selection: [0, 0, 0, 0],482query: '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.',483expectedIntent: 'edit',484validate: async (outcome, workspace, accessor) => {485const notebookDocument = workspace.getNotebookDocuments()[0];486if (!notebookDocument) {487assert.fail('no notebook document');488}489490assertWorkspaceEdit(outcome);491492// Initially 1 markdowncell and 3 code cells with a plot in each.493// After updates we should have at least 5 code cells with plots & 5 markdown cells.494const markdownCells = notebookDocument.getCells().filter(c => c.kind === NotebookCellKind.Markup);495const codeCells = notebookDocument.getCells().filter(c => c.kind === NotebookCellKind.Code);496497assert.ok(markdownCells.length > 1, `Should have at least 2 markdown cells, got ${markdownCells.length}`);498assert.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(',')}`);499}500},501]502});503});504});505506ssuite({ title: 'notebookEdits', subtitle: `bug reports - ${format}`, location: 'panel' }, () => {507stest({ description: 'Issue #13868' }, async (testingServiceCollection) => {508try {509await simulatePanelCodeMapperEx(testingServiceCollection, {510files: [511toFile({512fileName: 'multiFile/issue-13868/data.csv',513fileContents: [514'Duration,Pulse,Maxpulse,Calories\n',515'60,110,130,409.1\n',516'60,117,145,479.0\n',517'60,103,135,340.0\n',518'45,109,175,282.4\n',519'45,117,148,406.0\n',520'60,102,127,300.0\n',521'60,110,136,374.0\n',522'45,104,134,253.3\n',523'30,109,133,195.1\n',524'60,98,124,269.0\n',525'60,103,147,329.3\n',526'60,100,120,250.7\n',527'60,106,128,345.3\n',528'60,104,132,379.3\n',529'60,98,123,275.0\n',530'60,98,120,215.2\n',531'60,100,120,300.0\n'532].join('')533}),534],535queries: [536{537file: undefined,538selection: undefined,539query: 'create a new notebook to analyze #file:data.csv ',540validate: async (outcome, workspace, accessor) => {541assertWorkspaceEdit(outcome);542// assert.strictEqual((await getWorkspaceDiagnostics(accessor, workspace, 'tsc')).filter(d => d.kind === 'syntactic').length, 0);543}544}545]546});547} catch (ex: unknown) {548assert.fail((ex as Error).message);549}550});551});552});553554