Path: blob/main/src/vs/workbench/contrib/notebook/test/browser/notebookExecutionStateService.test.ts
3296 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 { AsyncIterableObject, DeferredPromise } from '../../../../../base/common/async.js';7import { CancellationToken } from '../../../../../base/common/cancellation.js';8import { Event } from '../../../../../base/common/event.js';9import { DisposableStore } from '../../../../../base/common/lifecycle.js';10import { URI } from '../../../../../base/common/uri.js';11import { mock } from '../../../../../base/test/common/mock.js';12import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';13import { PLAINTEXT_LANGUAGE_ID } from '../../../../../editor/common/languages/modesRegistry.js';14import { IMenu, IMenuService } from '../../../../../platform/actions/common/actions.js';15import { ExtensionIdentifier } from '../../../../../platform/extensions/common/extensions.js';16import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js';17import { insertCellAtIndex } from '../../browser/controller/cellOperations.js';18import { NotebookExecutionService } from '../../browser/services/notebookExecutionServiceImpl.js';19import { NotebookExecutionStateService } from '../../browser/services/notebookExecutionStateServiceImpl.js';20import { NotebookKernelService } from '../../browser/services/notebookKernelServiceImpl.js';21import { NotebookViewModel } from '../../browser/viewModel/notebookViewModelImpl.js';22import { NotebookTextModel } from '../../common/model/notebookTextModel.js';23import { CellEditType, CellKind, CellUri, IOutputDto, NotebookCellMetadata, NotebookExecutionState } from '../../common/notebookCommon.js';24import { CellExecutionUpdateType, INotebookExecutionService } from '../../common/notebookExecutionService.js';25import { INotebookExecutionStateService, NotebookExecutionType } from '../../common/notebookExecutionStateService.js';26import { INotebookKernel, INotebookKernelService, VariablesResult } from '../../common/notebookKernelService.js';27import { INotebookLoggingService } from '../../common/notebookLoggingService.js';28import { INotebookService } from '../../common/notebookService.js';29import { setupInstantiationService, withTestNotebook as _withTestNotebook } from './testNotebookEditor.js';3031suite('NotebookExecutionStateService', () => {3233let instantiationService: TestInstantiationService;34let kernelService: INotebookKernelService;35let disposables: DisposableStore;36let testNotebookModel: NotebookTextModel | undefined;3738teardown(() => {39disposables.dispose();40});4142ensureNoDisposablesAreLeakedInTestSuite();4344setup(function () {4546disposables = new DisposableStore();4748instantiationService = setupInstantiationService(disposables);4950instantiationService.stub(INotebookService, new class extends mock<INotebookService>() {51override onDidAddNotebookDocument = Event.None;52override onWillRemoveNotebookDocument = Event.None;53override getNotebookTextModels() { return []; }54override getNotebookTextModel(uri: URI): NotebookTextModel | undefined {55return testNotebookModel;56}57});5859instantiationService.stub(IMenuService, new class extends mock<IMenuService>() {60override createMenu() {61return new class extends mock<IMenu>() {62override onDidChange = Event.None;63override getActions() { return []; }64override dispose() { }65};66}67});68instantiationService.stub(INotebookLoggingService, new class extends mock<INotebookLoggingService>() {69override debug(category: string, output: string): void {70//71}72});7374kernelService = disposables.add(instantiationService.createInstance(NotebookKernelService));75instantiationService.set(INotebookKernelService, kernelService);76instantiationService.set(INotebookExecutionService, disposables.add(instantiationService.createInstance(NotebookExecutionService)));77instantiationService.set(INotebookExecutionStateService, disposables.add(instantiationService.createInstance(NotebookExecutionStateService)));78});7980async function withTestNotebook(cells: [string, string, CellKind, IOutputDto[], NotebookCellMetadata][], callback: (viewModel: NotebookViewModel, textModel: NotebookTextModel, disposables: DisposableStore) => void | Promise<void>) {81return _withTestNotebook(cells, (editor, viewModel) => callback(viewModel, viewModel.notebookDocument, disposables));82}8384function testCancelOnDelete(expectedCancels: number, implementsInterrupt: boolean) {85return withTestNotebook([], async (viewModel, _document, disposables) => {86testNotebookModel = viewModel.notebookDocument;8788let cancels = 0;89const kernel = new class extends TestNotebookKernel {90implementsInterrupt = implementsInterrupt;9192constructor() {93super({ languages: ['javascript'] });94}9596override async executeNotebookCellsRequest(): Promise<void> { }9798override async cancelNotebookCellExecution(_uri: URI, handles: number[]): Promise<void> {99cancels += handles.length;100}101};102disposables.add(kernelService.registerKernel(kernel));103kernelService.selectKernelForNotebook(kernel, viewModel.notebookDocument);104105const executionStateService: INotebookExecutionStateService = instantiationService.get(INotebookExecutionStateService);106107// Should cancel executing and pending cells, when kernel does not implement interrupt108const cell = disposables.add(insertCellAtIndex(viewModel, 0, 'var c = 3', 'javascript', CellKind.Code, {}, [], true, true));109const cell2 = disposables.add(insertCellAtIndex(viewModel, 1, 'var c = 3', 'javascript', CellKind.Code, {}, [], true, true));110const cell3 = disposables.add(insertCellAtIndex(viewModel, 2, 'var c = 3', 'javascript', CellKind.Code, {}, [], true, true));111insertCellAtIndex(viewModel, 3, 'var c = 3', 'javascript', CellKind.Code, {}, [], true, true); // Not deleted112const exe = executionStateService.createCellExecution(viewModel.uri, cell.handle); // Executing113exe.confirm();114exe.update([{ editType: CellExecutionUpdateType.ExecutionState, executionOrder: 1 }]);115const exe2 = executionStateService.createCellExecution(viewModel.uri, cell2.handle); // Pending116exe2.confirm();117executionStateService.createCellExecution(viewModel.uri, cell3.handle); // Unconfirmed118assert.strictEqual(cancels, 0);119viewModel.notebookDocument.applyEdits([{120editType: CellEditType.Replace, index: 0, count: 3, cells: []121}], true, undefined, () => undefined, undefined, false);122assert.strictEqual(cancels, expectedCancels);123});124125}126127// TODO@roblou Could be a test just for NotebookExecutionListeners, which can be a standalone contribution128test('cancel execution when cell is deleted', async function () {129return testCancelOnDelete(3, false);130});131132test('cancel execution when cell is deleted in interrupt-type kernel', async function () {133return testCancelOnDelete(1, true);134});135136test('fires onDidChangeCellExecution when cell is completed while deleted', async function () {137return withTestNotebook([], async (viewModel, _document, disposables) => {138testNotebookModel = viewModel.notebookDocument;139140const kernel = new TestNotebookKernel();141disposables.add(kernelService.registerKernel(kernel));142kernelService.selectKernelForNotebook(kernel, viewModel.notebookDocument);143144const executionStateService: INotebookExecutionStateService = instantiationService.get(INotebookExecutionStateService);145const cell = insertCellAtIndex(viewModel, 0, 'var c = 3', 'javascript', CellKind.Code, {}, [], true, true);146const exe = executionStateService.createCellExecution(viewModel.uri, cell.handle);147148let didFire = false;149disposables.add(executionStateService.onDidChangeExecution(e => {150if (e.type === NotebookExecutionType.cell) {151didFire = !e.changed;152}153}));154155viewModel.notebookDocument.applyEdits([{156editType: CellEditType.Replace, index: 0, count: 1, cells: []157}], true, undefined, () => undefined, undefined, false);158exe.complete({});159assert.strictEqual(didFire, true);160});161});162163test('does not fire onDidChangeCellExecution for output updates', async function () {164return withTestNotebook([], async (viewModel, _document, disposables) => {165testNotebookModel = viewModel.notebookDocument;166167const kernel = new TestNotebookKernel();168disposables.add(kernelService.registerKernel(kernel));169kernelService.selectKernelForNotebook(kernel, viewModel.notebookDocument);170171const executionStateService: INotebookExecutionStateService = instantiationService.get(INotebookExecutionStateService);172const cell = disposables.add(insertCellAtIndex(viewModel, 0, 'var c = 3', 'javascript', CellKind.Code, {}, [], true, true));173const exe = executionStateService.createCellExecution(viewModel.uri, cell.handle);174175let didFire = false;176disposables.add(executionStateService.onDidChangeExecution(e => {177if (e.type === NotebookExecutionType.cell) {178didFire = true;179}180}));181182exe.update([{ editType: CellExecutionUpdateType.OutputItems, items: [], outputId: '1' }]);183assert.strictEqual(didFire, false);184exe.update([{ editType: CellExecutionUpdateType.ExecutionState, executionOrder: 123 }]);185assert.strictEqual(didFire, true);186exe.complete({});187});188});189190// #142466191test('getCellExecution and onDidChangeCellExecution', async function () {192return withTestNotebook([], async (viewModel, _document, disposables) => {193testNotebookModel = viewModel.notebookDocument;194195const kernel = new TestNotebookKernel();196disposables.add(kernelService.registerKernel(kernel));197kernelService.selectKernelForNotebook(kernel, viewModel.notebookDocument);198199const executionStateService: INotebookExecutionStateService = instantiationService.get(INotebookExecutionStateService);200const cell = disposables.add(insertCellAtIndex(viewModel, 0, 'var c = 3', 'javascript', CellKind.Code, {}, [], true, true));201202const deferred = new DeferredPromise<void>();203disposables.add(executionStateService.onDidChangeExecution(e => {204if (e.type === NotebookExecutionType.cell) {205const cellUri = CellUri.generate(e.notebook, e.cellHandle);206const exe = executionStateService.getCellExecution(cellUri);207assert.ok(exe);208assert.strictEqual(e.notebook.toString(), exe.notebook.toString());209assert.strictEqual(e.cellHandle, exe.cellHandle);210211assert.strictEqual(exe.notebook.toString(), e.changed?.notebook.toString());212assert.strictEqual(exe.cellHandle, e.changed?.cellHandle);213214deferred.complete();215}216}));217218executionStateService.createCellExecution(viewModel.uri, cell.handle);219220return deferred.p;221});222});223test('getExecution and onDidChangeExecution', async function () {224return withTestNotebook([], async (viewModel, _document, disposables) => {225testNotebookModel = viewModel.notebookDocument;226227const kernel = new TestNotebookKernel();228disposables.add(kernelService.registerKernel(kernel));229kernelService.selectKernelForNotebook(kernel, viewModel.notebookDocument);230231const eventRaisedWithExecution: boolean[] = [];232const executionStateService: INotebookExecutionStateService = instantiationService.get(INotebookExecutionStateService);233executionStateService.onDidChangeExecution(e => eventRaisedWithExecution.push(e.type === NotebookExecutionType.notebook && !!e.changed), this, disposables);234235const deferred = new DeferredPromise<void>();236disposables.add(executionStateService.onDidChangeExecution(e => {237if (e.type === NotebookExecutionType.notebook) {238const exe = executionStateService.getExecution(viewModel.uri);239assert.ok(exe);240assert.strictEqual(e.notebook.toString(), exe.notebook.toString());241assert.ok(e.affectsNotebook(viewModel.uri));242assert.deepStrictEqual(eventRaisedWithExecution, [true]);243deferred.complete();244}245}));246247executionStateService.createExecution(viewModel.uri);248249return deferred.p;250});251});252253test('getExecution and onDidChangeExecution 2', async function () {254return withTestNotebook([], async (viewModel, _document, disposables) => {255testNotebookModel = viewModel.notebookDocument;256257const kernel = new TestNotebookKernel();258disposables.add(kernelService.registerKernel(kernel));259kernelService.selectKernelForNotebook(kernel, viewModel.notebookDocument);260261const executionStateService: INotebookExecutionStateService = instantiationService.get(INotebookExecutionStateService);262263const deferred = new DeferredPromise<void>();264const expectedNotebookEventStates: (NotebookExecutionState | undefined)[] = [NotebookExecutionState.Unconfirmed, NotebookExecutionState.Pending, NotebookExecutionState.Executing, undefined];265executionStateService.onDidChangeExecution(e => {266if (e.type === NotebookExecutionType.notebook) {267const expectedState = expectedNotebookEventStates.shift();268if (typeof expectedState === 'number') {269const exe = executionStateService.getExecution(viewModel.uri);270assert.ok(exe);271assert.strictEqual(e.notebook.toString(), exe.notebook.toString());272assert.strictEqual(e.changed?.state, expectedState);273} else {274assert.ok(e.changed === undefined);275}276277assert.ok(e.affectsNotebook(viewModel.uri));278if (expectedNotebookEventStates.length === 0) {279deferred.complete();280}281}282}, this, disposables);283284const execution = executionStateService.createExecution(viewModel.uri);285execution.confirm();286execution.begin();287execution.complete();288289return deferred.p;290});291});292293test('force-cancel works for Cell Execution', async function () {294return withTestNotebook([], async (viewModel, _document, disposables) => {295testNotebookModel = viewModel.notebookDocument;296297const kernel = new TestNotebookKernel();298disposables.add(kernelService.registerKernel(kernel));299kernelService.selectKernelForNotebook(kernel, viewModel.notebookDocument);300301const executionStateService: INotebookExecutionStateService = instantiationService.get(INotebookExecutionStateService);302const cell = disposables.add(insertCellAtIndex(viewModel, 0, 'var c = 3', 'javascript', CellKind.Code, {}, [], true, true));303executionStateService.createCellExecution(viewModel.uri, cell.handle);304const exe = executionStateService.getCellExecution(cell.uri);305assert.ok(exe);306307executionStateService.forceCancelNotebookExecutions(viewModel.uri);308const exe2 = executionStateService.getCellExecution(cell.uri);309assert.strictEqual(exe2, undefined);310});311});312test('force-cancel works for Notebook Execution', async function () {313return withTestNotebook([], async (viewModel, _document, disposables) => {314testNotebookModel = viewModel.notebookDocument;315316const kernel = new TestNotebookKernel();317disposables.add(kernelService.registerKernel(kernel));318kernelService.selectKernelForNotebook(kernel, viewModel.notebookDocument);319const eventRaisedWithExecution: boolean[] = [];320321const executionStateService: INotebookExecutionStateService = instantiationService.get(INotebookExecutionStateService);322executionStateService.onDidChangeExecution(e => eventRaisedWithExecution.push(e.type === NotebookExecutionType.notebook && !!e.changed), this, disposables);323executionStateService.createExecution(viewModel.uri);324const exe = executionStateService.getExecution(viewModel.uri);325assert.ok(exe);326assert.deepStrictEqual(eventRaisedWithExecution, [true]);327328executionStateService.forceCancelNotebookExecutions(viewModel.uri);329const exe2 = executionStateService.getExecution(viewModel.uri);330assert.deepStrictEqual(eventRaisedWithExecution, [true, false]);331assert.strictEqual(exe2, undefined);332});333});334test('force-cancel works for Cell and Notebook Execution', async function () {335return withTestNotebook([], async (viewModel, _document, disposables) => {336testNotebookModel = viewModel.notebookDocument;337338const kernel = new TestNotebookKernel();339disposables.add(kernelService.registerKernel(kernel));340kernelService.selectKernelForNotebook(kernel, viewModel.notebookDocument);341342const executionStateService: INotebookExecutionStateService = instantiationService.get(INotebookExecutionStateService);343executionStateService.createExecution(viewModel.uri);344executionStateService.createExecution(viewModel.uri);345const cellExe = executionStateService.getExecution(viewModel.uri);346const exe = executionStateService.getExecution(viewModel.uri);347assert.ok(cellExe);348assert.ok(exe);349350executionStateService.forceCancelNotebookExecutions(viewModel.uri);351const cellExe2 = executionStateService.getExecution(viewModel.uri);352const exe2 = executionStateService.getExecution(viewModel.uri);353assert.strictEqual(cellExe2, undefined);354assert.strictEqual(exe2, undefined);355});356});357});358359class TestNotebookKernel implements INotebookKernel {360id: string = 'test';361label: string = '';362viewType = '*';363onDidChange = Event.None;364extension: ExtensionIdentifier = new ExtensionIdentifier('test');365localResourceRoot: URI = URI.file('/test');366description?: string | undefined;367detail?: string | undefined;368preloadUris: URI[] = [];369preloadProvides: string[] = [];370supportedLanguages: string[] = [];371async executeNotebookCellsRequest(): Promise<void> { }372async cancelNotebookCellExecution(uri: URI, cellHandles: number[]): Promise<void> { }373provideVariables(notebookUri: URI, parentId: number | undefined, kind: 'named' | 'indexed', start: number, token: CancellationToken): AsyncIterableObject<VariablesResult> {374return AsyncIterableObject.EMPTY;375}376377constructor(opts?: { languages?: string[]; id?: string }) {378this.supportedLanguages = opts?.languages ?? [PLAINTEXT_LANGUAGE_ID];379if (opts?.id) {380this.id = opts?.id;381}382}383}384385386