Skip to content
2 changes: 1 addition & 1 deletion src/client/repl/nativeRepl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export class NativeRepl implements Disposable {
}

dispose(): void {
this.disposables.forEach((d) => d.dispose());
this.disposables.forEach((d) => d?.dispose());
}

/**
Expand Down
9 changes: 5 additions & 4 deletions src/client/repl/replController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,13 @@ export function createReplController(
const exec = controller.createNotebookCellExecution(cell);
exec.start(Date.now());

const result = await server.execute(cell.document.getText());
const code = cell.document.getText();
const result = await server.execute(code);

if (result?.output) {
exec.replaceOutput([
new vscode.NotebookCellOutput([vscode.NotebookCellOutputItem.text(result.output, 'text/plain')]),
]);
const stdoutItem = vscode.NotebookCellOutputItem.stdout(result.output);
const output = new vscode.NotebookCellOutput([stdoutItem], {});
exec.replaceOutput([output]);
// TODO: Properly update via NotebookCellOutputItem.error later.
}

Expand Down
63 changes: 63 additions & 0 deletions src/test/repl/nativeRepl.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import * as TypeMoq from 'typemoq';
import * as sinon from 'sinon';
import { reset, when } from 'ts-mockito';
import * as vscode from 'vscode';
import { Disposable, EventEmitter, NotebookDocument, Uri } from 'vscode';
import { expect } from 'chai';

Expand All @@ -16,6 +18,7 @@ import * as PythonServer from '../../client/repl/pythonServer';
import * as vscodeWorkspaceApis from '../../client/common/vscodeApis/workspaceApis';
import * as replController from '../../client/repl/replController';
import { executeCommand } from '../../client/common/vscodeApis/commandApis';
import { mockedVSCodeNamespaces } from '../vscode-mock';

suite('REPL - Native REPL', () => {
let interpreterService: TypeMoq.IMock<IInterpreterService>;
Expand Down Expand Up @@ -114,6 +117,66 @@ suite('REPL - Native REPL', () => {
expect(createReplControllerStub.calledOnce).to.be.true;
});

test('createReplController should publish stdout notebook output for REPL execution', async () => {
const mockServer = {
interrupt: sinon.stub(),
execute: sinon.stub().resolves({ status: true, output: 'hello\nworld' }),
dispose: sinon.stub(),
} as any;
const createPythonServerStub = sinon.stub(PythonServer, 'createPythonServer').returns(mockServer as any);

const replaceOutputStub = sinon.stub();
const execStub = {
start: sinon.stub(),
replaceOutput: replaceOutputStub,
end: sinon.stub(),
} as any;

const createNotebookCellExecutionStub = sinon.stub().returns(execStub);
const mockNotebookController = ({
id: 'mockController',
dispose: sinon.stub(),
updateNotebookAffinity: sinon.stub(),
createNotebookCellExecution: createNotebookCellExecutionStub,
supportedLanguages: [] as string[],
description: '',
interruptHandler: undefined,
executeHandler: undefined,
} as any) as vscode.NotebookController;

when(
mockedVSCodeNamespaces.notebooks!.createNotebookController('pythonREPL', 'jupyter-notebook', 'Python REPL'),
).thenReturn(mockNotebookController);
createReplControllerStub.restore();

const disposables: Disposable[] = [];
try {
const controller = replController.createReplController('python', disposables, '/cwd');

const mockTextDocument = { getText: sinon.stub().returns('print("hi")') } as any;
const mockCell = { document: mockTextDocument } as any;
await (controller.executeHandler as any)([mockCell]);

expect(createPythonServerStub.calledOnce).to.be.true;
expect(mockServer.execute.calledOnceWithExactly('print("hi")')).to.be.true;
expect(createNotebookCellExecutionStub.calledOnce).to.be.true;
expect(replaceOutputStub.calledOnce).to.be.true;

const outputs = replaceOutputStub.firstCall.args[0] as vscode.NotebookCellOutput[];
expect(outputs).to.have.lengthOf(1);
const output = outputs[0];
expect(output.items).to.have.lengthOf(1);
expect(output.items[0].mime).to.equal('application/vnd.code.notebook.stdout');
expect((output.items[0] as any).metadata).to.be.undefined;
expect(output.metadata).to.deep.equal({});
const outputText = Buffer.from((output.items[0] as any).data).toString();
expect(outputText).to.equal('hello\nworld');
} finally {
disposables.forEach((disposable) => disposable.dispose());
reset(mockedVSCodeNamespaces.notebooks!);
}
});

test('watchNotebookClosed should clean up resources when notebook is closed', async () => {
const notebookCloseEmitter = new EventEmitter<NotebookDocument>();
sinon.stub(vscodeWorkspaceApis, 'onDidCloseNotebookDocument').callsFake((handler) => {
Expand Down
13 changes: 13 additions & 0 deletions src/test/vscode-mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,19 @@ export function initialize() {
generateMock('scm');
generateMock('notebooks');

// Notebook output helpers used by REPL tests.
(mockedVSCode as any).NotebookCellOutputItem = {
stdout: (value: string) => ({ mime: 'application/vnd.code.notebook.stdout', data: Buffer.from(value) }),
};
(mockedVSCode as any).NotebookCellOutput = class {
public items: any[];
public metadata?: any;
constructor(items: any[], metadata?: any) {
this.items = items;
this.metadata = metadata;
}
};

// Use mock clipboard fo testing purposes.
const clipboard = new MockClipboard();
when(mockedVSCodeNamespaces.env!.clipboard).thenReturn(clipboard);
Expand Down