diff --git a/src/kernels/execution/cellExecutionCreator.ts b/src/kernels/execution/cellExecutionCreator.ts index 89905c0f1ff..43af71fd738 100644 --- a/src/kernels/execution/cellExecutionCreator.ts +++ b/src/kernels/execution/cellExecutionCreator.ts @@ -8,7 +8,8 @@ import { NotebookCell, NotebookCellExecution, NotebookCellOutput, - NotebookCellOutputItem + NotebookCellOutputItem, + TextDocument } from 'vscode'; import { IKernelController } from '../types'; @@ -80,10 +81,10 @@ export class NotebookCellExecutionWrapper implements NotebookCellExecution { * Class for mapping cells to an instance of a NotebookCellExecution object */ export class CellExecutionCreator { - private static _map = new Map(); + private static _map = new WeakMap(); static getOrCreate(cell: NotebookCell, controller: IKernelController) { let cellExecution: NotebookCellExecutionWrapper | undefined; - const key = cell.document.uri.toString(); + const key = cell.document; cellExecution = this.get(cell); if (!cellExecution) { cellExecution = CellExecutionCreator.create(key, cell, controller); @@ -106,11 +107,11 @@ export class CellExecutionCreator { return cellExecution; } static get(cell: NotebookCell) { - const key = cell.document.uri.toString(); + const key = cell.document; return CellExecutionCreator._map.get(key); } - private static create(key: string, cell: NotebookCell, controller: IKernelController) { + private static create(key: TextDocument, cell: NotebookCell, controller: IKernelController) { const result = new NotebookCellExecutionWrapper( controller.createNotebookCellExecution(cell), controller.id, diff --git a/src/kernels/kernelCrashMonitor.unit.test.ts b/src/kernels/kernelCrashMonitor.unit.test.ts new file mode 100644 index 00000000000..6fe12d33006 --- /dev/null +++ b/src/kernels/kernelCrashMonitor.unit.test.ts @@ -0,0 +1,166 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import * as fakeTimers from '@sinonjs/fake-timers'; +import { KernelMessage } from '@jupyterlab/services'; +import { anything, instance, mock, verify, when } from 'ts-mockito'; +import { Disposable, EventEmitter, NotebookCell } from 'vscode'; +import { IApplicationShell } from '../platform/common/application/types'; +import { disposeAllDisposables } from '../platform/common/helpers'; +import { IDisposable } from '../platform/common/types'; +import { createKernelController, TestNotebookDocument } from '../test/datascience/notebook/executionHelper'; +import { KernelCrashMonitor } from './kernelCrashMonitor'; +import { + IKernel, + IKernelConnectionSession, + IKernelController, + IKernelProvider, + INotebookKernelExecution, + LocalKernelSpecConnectionMetadata, + RemoteKernelSpecConnectionMetadata +} from './types'; +import { assert } from 'chai'; +import { DataScience } from '../platform/common/utils/localize'; +import { createOutputWithErrorMessageForDisplay } from '../platform/errors/errorUtils'; +import { getDisplayNameOrNameOfKernelConnection } from './helpers'; + +suite('Kernel Crash Monitor', () => { + let kernelProvider: IKernelProvider; + const disposables: IDisposable[] = []; + let kernel: IKernel; + let appShell: IApplicationShell; + let kernelCrashMonitor: KernelCrashMonitor; + let onKernelStatusChanged: EventEmitter<{ + status: KernelMessage.Status; + kernel: IKernel; + }>; + let onDidStartKernel: EventEmitter; + let kernelExecution: INotebookKernelExecution; + let onPreExecute: EventEmitter; + let cell: NotebookCell; + let kernelSession: IKernelConnectionSession; + let notebook: TestNotebookDocument; + let controller: IKernelController; + let clock: fakeTimers.InstalledClock; + let remoteKernelSpec = RemoteKernelSpecConnectionMetadata.create({ + id: 'remote', + baseUrl: '1', + kernelSpec: { + argv: [], + display_name: 'remote', + executable: '', + name: 'remote' + }, + serverId: '1' + }); + let localKernelSpec = LocalKernelSpecConnectionMetadata.create({ + id: 'local', + kernelSpec: { + argv: [], + display_name: 'remote', + executable: '', + name: 'remote' + } + }); + setup(async () => { + kernelProvider = mock(); + kernel = mock(); + appShell = mock(); + kernelExecution = mock(); + kernelSession = mock(); + onKernelStatusChanged = new EventEmitter<{ + status: KernelMessage.Status; + kernel: IKernel; + }>(); + onDidStartKernel = new EventEmitter(); + onPreExecute = new EventEmitter(); + notebook = new TestNotebookDocument(); + cell = await notebook.appendCodeCell('1234'); + controller = createKernelController('1'); + disposables.push(onDidStartKernel); + disposables.push(onKernelStatusChanged); + disposables.push(onPreExecute); + when(kernel.dispose()).thenResolve(); + when(kernel.disposed).thenReturn(false); + when(kernel.controller).thenReturn(controller); + when(kernel.disposing).thenReturn(false); + when(kernel.session).thenReturn(instance(kernelSession)); + when(kernelSession.kind).thenReturn('localRaw'); + when(appShell.showErrorMessage(anything())).thenResolve(); + + when(kernelProvider.onDidStartKernel).thenReturn(onDidStartKernel.event); + when(kernelProvider.onKernelStatusChanged).thenReturn(onKernelStatusChanged.event); + when(kernelProvider.getOrCreate(anything(), anything())).thenReturn(instance(kernel)); + when(kernelProvider.getKernelExecution(anything())).thenReturn(instance(kernelExecution)); + when(kernelExecution.onPreExecute).thenReturn(onPreExecute.event); + + kernelCrashMonitor = new KernelCrashMonitor(disposables, instance(appShell), instance(kernelProvider)); + clock = fakeTimers.install(); + disposables.push(new Disposable(() => clock.uninstall())); + }); + teardown(() => disposeAllDisposables(disposables)); + + test('Error message displayed and Cell output updated with error message (raw kernel)', async () => { + when(kernelSession.kind).thenReturn('localRaw'); + when(kernel.kernelConnectionMetadata).thenReturn(localKernelSpec); + // Ensure we have a kernel and have started a cell. + kernelCrashMonitor.activate(); + onDidStartKernel.fire(instance(kernel)); + onPreExecute.fire(cell); + const execution = controller.createNotebookCellExecution(cell); + execution.start(); + + const expectedErrorMessage = Buffer.from( + createOutputWithErrorMessageForDisplay(DataScience.kernelCrashedDueToCodeInCurrentOrPreviousCell()) + ?.items[0]!.data! + ).toString(); + + when(kernel.status).thenReturn('dead'); + onKernelStatusChanged.fire({ status: 'dead', kernel: instance(kernel) }); + await clock.runAllAsync(); + + verify( + appShell.showErrorMessage( + DataScience.kernelDiedWithoutError().format(getDisplayNameOrNameOfKernelConnection(localKernelSpec)) + ) + ).once(); + + assert.strictEqual(cell.outputs.length, 1); + assert.strictEqual(cell.outputs[0].items.length, 1); + const outputItem = cell.outputs[0].items[0]; + assert.include(Buffer.from(outputItem.data).toString(), expectedErrorMessage); + }); + test('Error message displayed and Cell output updated with error message (jupyter kernel)', async () => { + when(kernelSession.kind).thenReturn('localJupyter'); + when(kernel.kernelConnectionMetadata).thenReturn(remoteKernelSpec); + // Ensure we have a kernel and have started a cell. + kernelCrashMonitor.activate(); + onDidStartKernel.fire(instance(kernel)); + onPreExecute.fire(cell); + const execution = controller.createNotebookCellExecution(cell); + execution.start(); + + const expectedErrorMessage = Buffer.from( + createOutputWithErrorMessageForDisplay(DataScience.kernelCrashedDueToCodeInCurrentOrPreviousCell()) + ?.items[0]!.data! + ).toString(); + + when(kernel.status).thenReturn('autorestarting'); + when(kernelSession.status).thenReturn('autorestarting'); + onKernelStatusChanged.fire({ status: 'dead', kernel: instance(kernel) }); + await clock.runAllAsync(); + + verify( + appShell.showErrorMessage( + DataScience.kernelDiedWithoutErrorAndAutoRestarting().format( + getDisplayNameOrNameOfKernelConnection(remoteKernelSpec) + ) + ) + ).once(); + + assert.strictEqual(cell.outputs.length, 1); + assert.strictEqual(cell.outputs[0].items.length, 1); + const outputItem = cell.outputs[0].items[0]; + assert.include(Buffer.from(outputItem.data).toString(), expectedErrorMessage); + }); +}); diff --git a/src/notebooks/controllers/kernelConnector.unit.test.ts b/src/notebooks/controllers/kernelConnector.unit.test.ts new file mode 100644 index 00000000000..4cf32952c0d --- /dev/null +++ b/src/notebooks/controllers/kernelConnector.unit.test.ts @@ -0,0 +1,201 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { assert } from 'chai'; +import { anything, deepEqual, instance, mock, verify, when } from 'ts-mockito'; +import { NotebookDocument, Uri } from 'vscode'; +import { DisplayOptions } from '../../kernels/displayOptions'; +import { KernelDeadError } from '../../kernels/errors/kernelDeadError'; +import { IDataScienceErrorHandler } from '../../kernels/errors/types'; +import { getDisplayNameOrNameOfKernelConnection } from '../../kernels/helpers'; +import { ITrustedKernelPaths } from '../../kernels/raw/finder/types'; +import { + IKernel, + IKernelConnectionSession, + IKernelController, + IKernelProvider, + KernelInterpreterDependencyResponse, + PythonKernelConnectionMetadata +} from '../../kernels/types'; +import { IApplicationShell, ICommandManager } from '../../platform/common/application/types'; +import { disposeAllDisposables } from '../../platform/common/helpers'; +import { IDisposable } from '../../platform/common/types'; +import { DataScience } from '../../platform/common/utils/localize'; +import { IServiceContainer } from '../../platform/ioc/types'; +import { createKernelController, TestNotebookDocument } from '../../test/datascience/notebook/executionHelper'; +import { KernelConnector } from './kernelConnector'; + +suite('Kernel Connector', () => { + const pythonConnection = PythonKernelConnectionMetadata.create({ + id: 'python', + interpreter: { + id: 'id', + sysPrefix: '', + uri: Uri.file('python') + }, + kernelSpec: { + argv: ['python'], + display_name: '', + executable: '', + language: 'python', + name: 'python' + } + }); + let serviceContainer: IServiceContainer; + let kernelProvider: IKernelProvider; + let trustedKernels: ITrustedKernelPaths; + let notebook: NotebookDocument; + const disposables: IDisposable[] = []; + let controller: IKernelController; + let kernel: IKernel; + let errorHandler: IDataScienceErrorHandler; + let kernelSession: IKernelConnectionSession; + let appShell: IApplicationShell; + let commandManager: ICommandManager; + let pythonKernelSpec = PythonKernelConnectionMetadata.create({ + id: 'python', + interpreter: { + id: 'id', + sysPrefix: '', + uri: Uri.file('python') + }, + kernelSpec: { + argv: [], + display_name: 'python', + executable: '', + name: 'python' + } + }); + setup(() => { + serviceContainer = mock(); + kernelProvider = mock(); + trustedKernels = mock(); + errorHandler = mock(); + kernelSession = mock(); + appShell = mock(); + commandManager = mock(); + kernel = mock(); + (instance(kernel) as any).then = undefined; + notebook = new TestNotebookDocument(); + (instance(kernelSession) as any).then = undefined; + + when(kernel.dispose()).thenResolve(); + when(kernel.start(anything())).thenResolve(instance(kernelSession)); + when(kernel.kernelConnectionMetadata).thenReturn(pythonKernelSpec); + when(trustedKernels.isTrusted(anything())).thenReturn(true); + when(serviceContainer.get(IKernelProvider)).thenReturn(instance(kernelProvider)); + when(serviceContainer.get(ITrustedKernelPaths)).thenReturn(instance(trustedKernels)); + when(serviceContainer.get(IApplicationShell)).thenReturn(instance(appShell)); + when(serviceContainer.get(ICommandManager)).thenReturn(instance(commandManager)); + when(serviceContainer.get(IDataScienceErrorHandler)).thenReturn( + instance(errorHandler) + ); + when(kernelProvider.getOrCreate(anything(), anything())).thenReturn(instance(kernel)); + controller = createKernelController(pythonConnection.id); + }); + teardown(() => disposeAllDisposables(disposables)); + test('Can start a kernel', async () => { + when(kernel.status).thenReturn('idle'); + + await KernelConnector.connectToNotebookKernel( + pythonConnection, + instance(serviceContainer), + { + controller, + notebook, + resource: notebook.uri + }, + new DisplayOptions(false), + disposables, + 'jupyterExtension' + ); + }); + test('Throws an error if we fail to start the kernel', async () => { + when(kernel.status).thenReturn('idle'); + when(kernel.start(anything())).thenThrow(new Error('Failed to Start Kernel')); + when(errorHandler.handleKernelError(anything(), anything(), anything(), anything(), anything())).thenResolve( + KernelInterpreterDependencyResponse.failed + ); + const result = KernelConnector.connectToNotebookKernel( + pythonConnection, + instance(serviceContainer), + { + controller, + notebook, + resource: notebook.uri + }, + new DisplayOptions(false), + disposables, + 'jupyterExtension' + ); + + await assert.isRejected(result, 'Failed to Start Kernel'); + }); + test('Display modal dialog for dead kernel and verify kernel is restart when the kernel is dead (user choses to restart)', async () => { + when(kernel.status).thenReturn('dead'); + when(kernel.restart()).thenResolve(); + when(errorHandler.handleKernelError(anything(), anything(), anything(), anything(), anything())).thenResolve( + KernelInterpreterDependencyResponse.failed + ); + when(appShell.showErrorMessage(anything(), anything(), anything(), anything())).thenReturn( + Promise.resolve(DataScience.restartKernel()) + ); + await KernelConnector.connectToNotebookKernel( + pythonConnection, + instance(serviceContainer), + { + controller, + notebook, + resource: notebook.uri + }, + new DisplayOptions(false), + disposables, + 'jupyterExtension' + ); + + verify(kernel.restart()).once(); + verify( + appShell.showErrorMessage( + DataScience.cannotRunCellKernelIsDead().format( + getDisplayNameOrNameOfKernelConnection(pythonKernelSpec) + ), + deepEqual({ modal: true }), + anything(), + anything() + ) + ).once(); + }); + test('Display modal dialog for dead kernel and verify kernel is not restarted when the kernel is dead (user does not restart)', async () => { + when(kernel.status).thenReturn('dead'); + when(kernel.restart()).thenResolve(); + when(errorHandler.handleKernelError(anything(), anything(), anything(), anything(), anything())).thenResolve( + KernelInterpreterDependencyResponse.failed + ); + when(appShell.showErrorMessage(anything(), anything(), anything(), anything())).thenResolve(); + const result = KernelConnector.connectToNotebookKernel( + pythonConnection, + instance(serviceContainer), + { + controller, + notebook, + resource: notebook.uri + }, + new DisplayOptions(false), + disposables, + 'jupyterExtension' + ); + + await assert.isRejected(result, new KernelDeadError(pythonKernelSpec).message); + verify(kernel.restart()).never(); + verify( + appShell.showErrorMessage( + DataScience.cannotRunCellKernelIsDead().format( + getDisplayNameOrNameOfKernelConnection(pythonKernelSpec) + ), + deepEqual({ modal: true }), + anything(), + anything() + ) + ).once(); + }); +}); diff --git a/src/test/datascience/notebook/executionHelper.ts b/src/test/datascience/notebook/executionHelper.ts index 92b62aec377..d1972896f79 100644 --- a/src/test/datascience/notebook/executionHelper.ts +++ b/src/test/datascience/notebook/executionHelper.ts @@ -2,6 +2,7 @@ // Licensed under the MIT License. /* eslint-disable @typescript-eslint/no-explicit-any */ +import { instance, mock, when } from 'ts-mockito'; import { CancellationToken, NotebookCell, @@ -129,7 +130,7 @@ export class TestNotebookDocument implements NotebookDocument { language: string = PYTHON_LANGUAGE, metadata: { readonly [key: string]: any } = {} ): Promise { - const textDoc = await workspace.openTextDocument({ language, content }); + const textDoc = await createTextDocument({ language, content }); // eslint-disable-next-line @typescript-eslint/no-use-before-define const cell = new TestNotebookCell(this, textDoc, NotebookCellKind.Code, 'text/plain', metadata); this.cells.push(cell); @@ -141,7 +142,7 @@ export class TestNotebookDocument implements NotebookDocument { language: string = PYTHON_LANGUAGE, metadata: { readonly [key: string]: any } = {} ): Promise { - const textDoc = await workspace.openTextDocument({ language, content }); + const textDoc = await createTextDocument({ language, content }); // eslint-disable-next-line @typescript-eslint/no-use-before-define const cell = new TestNotebookCell(this, textDoc, NotebookCellKind.Code, 'text/plain', metadata); this.cells.splice(index, 0, cell); @@ -151,7 +152,7 @@ export class TestNotebookDocument implements NotebookDocument { content: string, metadata: { readonly [key: string]: any } = {} ): Promise { - const textDoc = await workspace.openTextDocument({ language: 'markdown', content }); + const textDoc = await createTextDocument({ language: 'markdown', content }); // eslint-disable-next-line @typescript-eslint/no-use-before-define const cell = new TestNotebookCell(this, textDoc, NotebookCellKind.Markup, 'text/markdown', metadata); this.cells.push(cell); @@ -162,7 +163,7 @@ export class TestNotebookDocument implements NotebookDocument { content: string, metadata: { readonly [key: string]: any } = {} ): Promise { - const textDoc = await workspace.openTextDocument({ language: 'markdown', content }); + const textDoc = await createTextDocument({ language: 'markdown', content }); // eslint-disable-next-line @typescript-eslint/no-use-before-define const cell = new TestNotebookCell(this, textDoc, NotebookCellKind.Markup, 'text/markdown', metadata); this.cells.splice(index, 0, cell); @@ -182,6 +183,17 @@ export class TestNotebookDocument implements NotebookDocument { } } +async function createTextDocument({ language, content }: { language: string; content: string }) { + let textDoc = await workspace.openTextDocument({ language, content }); + if (textDoc) { + return textDoc; + } + textDoc = mock(); + when(textDoc.languageId).thenReturn(language); + when(textDoc.getText()).thenReturn(content); + (instance(textDoc) as any).then = undefined; + return instance(textDoc); +} export class TestNotebookCell implements NotebookCell { public get index(): number { return this.notebook.cells.findIndex((c) => c === this); diff --git a/src/test/datascience/notebook/kernelCrashes.vscode.test.ts b/src/test/datascience/notebook/kernelCrashes.vscode.test.ts index 4c0ec7deb27..171f3f83b71 100644 --- a/src/test/datascience/notebook/kernelCrashes.vscode.test.ts +++ b/src/test/datascience/notebook/kernelCrashes.vscode.test.ts @@ -36,7 +36,6 @@ import { runAllCellsInActiveNotebook, waitForKernelToGetAutoSelected, defaultNotebookTestTimeout, - waitForExecutionCompletedWithoutChangesToExecutionCount, getCellOutputs, getDefaultKernelConnection } from './helper.node'; @@ -206,60 +205,6 @@ suite('VSCode Notebook Kernel Error Handling - @kernelCore', function () { traceInfo(`Ended Test (completed) ${this.currentTest?.title}`); }); suiteTeardown(() => closeNotebooksAndCleanUpAfterTests(disposables)); - suite('Jupyter Kernels', () => { - setup(function () { - if (!IS_REMOTE_NATIVE_TEST() && !IS_NON_RAW_NATIVE_TEST()) { - return this.skip(); - } - }); - test('Ensure kernel is automatically restarted by jupyter & we get a status of restarting & autorestarting when kernel dies while executing a cell', async function () { - const cell1 = await notebook.appendCodeCell('print("123412341234")'); - const cell2 = await notebook.appendCodeCell(codeToKillKernel); - - await Promise.all([ - cellExecutionHandler([cell1], notebook, controller), - waitForExecutionCompletedSuccessfully(cell1) - ]); - const kernel = kernelProvider.get(notebook)!; - const restartingEventFired = createDeferred(); - const autoRestartingEventFired = createDeferred(); - - kernel.onStatusChanged((status) => { - if (status === 'restarting') { - restartingEventFired.resolve(); - } - if (status === 'autorestarting') { - autoRestartingEventFired.resolve(); - } - }); - // Run cell that will kill the kernel. - await Promise.all([ - cellExecutionHandler([cell2], notebook, controller), - waitForExecutionCompletedSuccessfully(cell2) - ]); - - // Confirm we get the terminating & dead events. - // Kernel must die immediately, lets just wait for 10s. - await Promise.race([ - Promise.all([restartingEventFired, autoRestartingEventFired]), - sleep(10_000).then(() => Promise.reject(new Error('Did not fail'))) - ]); - - // Verify we have output in the cell to indicate the cell crashed. - await waitForCondition( - async () => { - const output = getCellOutputs(cell2); - return ( - output.includes(kernelCrashFailureMessageInCell) && - output.includes('https://aka.ms/vscodeJupyterKernelCrash') - ); - }, - defaultNotebookTestTimeout, - () => `Cell did not have kernel crash output, the output is = ${getCellOutputs(cell2)}` - ); - }); - }); - suite('Raw Kernels', () => { setup(function () { if (IS_REMOTE_NATIVE_TEST() || IS_NON_RAW_NATIVE_TEST()) { @@ -323,79 +268,6 @@ suite('VSCode Notebook Kernel Error Handling - @kernelCore', function () { () => `Cell did not have kernel crash output, the output is = ${getCellOutputs(cell2)}` ); } - test('Ensure we get an error message & a status of terminating & dead when kernel dies while executing a cell', async function () { - await runAndFailWithKernelCrash(); - }); - test('Ensure we get a modal prompt to restart kernel when running cells against a dead kernel', async function () { - await runAndFailWithKernelCrash(); - const cell3 = await notebook.appendCodeCell('print("123412341234")'); - const kernel = kernelProvider.get(notebook)!; - const expectedErrorMessage = DataScience.cannotRunCellKernelIsDead().format( - getDisplayNameOrNameOfKernelConnection(kernel.kernelConnectionMetadata) - ); - const restartPrompt = await hijackPrompt( - 'showErrorMessage', - { - exactMatch: expectedErrorMessage - }, - { result: DataScience.restartKernel(), clickImmediately: true }, - disposables - ); - - await sleep(5_000); - // Confirm we get a prompt to restart the kernel, and it gets restarted. - // & also confirm the cell completes execution with an execution count of 1 (thats how we tell kernel restarted). - await Promise.all([ - restartPrompt.displayed, - cellExecutionHandler([cell3], notebook, controller), - waitForExecutionCompletedSuccessfully(cell3) - ]); - // If execution order is 1, then we know the kernel restarted. - assert.strictEqual(cell3.executionSummary?.executionOrder, 1); - }); - test('Ensure we get a modal prompt to restart kernel when running cells against a dead kernel (dismiss and run again)', async function () { - await runAndFailWithKernelCrash(); - const cell3 = await notebook.appendCodeCell('print("123412341234")'); - const kernel = kernelProvider.get(notebook)!; - const expectedErrorMessage = DataScience.cannotRunCellKernelIsDead().format( - getDisplayNameOrNameOfKernelConnection(kernel.kernelConnectionMetadata) - ); - let restartPrompt = await hijackPrompt( - 'showErrorMessage', - { - exactMatch: expectedErrorMessage - }, - { dismissPrompt: true }, - disposables - ); - // Confirm we get a prompt to restart the kernel - await Promise.all([ - restartPrompt.displayed, - cellExecutionHandler([cell3], notebook, controller), - waitForExecutionCompletedWithoutChangesToExecutionCount(cell3) - ]); - - restartPrompt.dispose(); - - // Running cell again should display the prompt and restart the kernel. - restartPrompt = await hijackPrompt( - 'showErrorMessage', - { - exactMatch: expectedErrorMessage - }, - { result: DataScience.restartKernel(), clickImmediately: true }, - disposables - ); - // Confirm we get a prompt to restart the kernel, and it gets restarted. - // & also confirm the cell completes execution with an execution count of 1 (thats how we tell kernel restarted). - await Promise.all([ - restartPrompt.displayed, - cellExecutionHandler([cell3], notebook, controller), - waitForExecutionCompletedSuccessfully(cell3) - ]); - // If execution order is 1, then we know the kernel restarted. - assert.strictEqual(cell3.executionSummary?.executionOrder, 1); - }); test('Ensure we get an error displayed in cell output and prompt when user has a file named random.py next to the ipynb file', async function () { await runAndFailWithKernelCrash(); const cell3 = await notebook.appendCodeCell('print("123412341234")');