From 6ec5855af2ef1c614e9d682b7b938043b4485d75 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Fri, 3 Jun 2022 14:32:14 +1000 Subject: [PATCH] Fixes to IW debugging with breakpoints (#10263) --- .../debugger/jupyter/debugCellControllers.ts | 13 +- .../debugger/jupyter/kernelDebugAdapter.ts | 151 ++- .../debugger/kernelDebugAdapterBase.ts | 108 +- src/kernels/debugger/types.ts | 4 +- src/kernels/variables/debuggerVariables.ts | 5 +- src/notebooks/debugger/helper.ts | 4 +- src/notebooks/debugger/kernelDebugAdapter.ts | 59 +- .../codeExecution/codeExecutionHelper.node.ts | 16 +- .../interactiveDebugging.vscode.common.ts | 612 ++++++++++ .../interactiveDebugging.vscode.test.ts | 613 +--------- .../interactiveWindow.vscode.test.ts | 1017 ++++++++--------- 11 files changed, 1369 insertions(+), 1233 deletions(-) create mode 100644 src/test/datascience/interactiveDebugging.vscode.common.ts diff --git a/src/interactive-window/debugger/jupyter/debugCellControllers.ts b/src/interactive-window/debugger/jupyter/debugCellControllers.ts index 86278fb255d..c8482b130d6 100644 --- a/src/interactive-window/debugger/jupyter/debugCellControllers.ts +++ b/src/interactive-window/debugger/jupyter/debugCellControllers.ts @@ -26,17 +26,20 @@ export class DebugCellController implements IDebuggingDelegate { public async willSendEvent(_msg: DebugProtocolMessage): Promise { return false; } + private debugCellDumped?: Promise; public async willSendRequest(request: DebugProtocol.Request): Promise { const metadata = getInteractiveCellMetadata(this.debugCell); if (request.command === 'setBreakpoints' && metadata && metadata.generatedCode && !this.cellDumpInvoked) { - this.cellDumpInvoked = true; - await cellDebugSetup(this.kernel, this.debugAdapter); + if (!this.debugCellDumped) { + this.debugCellDumped = cellDebugSetup(this.kernel, this.debugAdapter); + } + await this.debugCellDumped; } if (request.command === 'configurationDone' && metadata && metadata.generatedCode) { - if (!this.cellDumpInvoked) { - this.cellDumpInvoked = true; - await cellDebugSetup(this.kernel, this.debugAdapter); + if (!this.debugCellDumped) { + this.debugCellDumped = cellDebugSetup(this.kernel, this.debugAdapter); } + await this.debugCellDumped; this._ready.resolve(); } } diff --git a/src/interactive-window/debugger/jupyter/kernelDebugAdapter.ts b/src/interactive-window/debugger/jupyter/kernelDebugAdapter.ts index 84be8d727ca..90d7f325de4 100644 --- a/src/interactive-window/debugger/jupyter/kernelDebugAdapter.ts +++ b/src/interactive-window/debugger/jupyter/kernelDebugAdapter.ts @@ -10,12 +10,20 @@ import { DebugProtocol } from 'vscode-debugprotocol'; import { IJupyterSession, IKernel } from '../../../kernels/types'; import { IPlatformService } from '../../../platform/common/platform/types'; import { IDumpCellResponse, IDebugLocationTrackerFactory } from '../../../kernels/debugger/types'; -import { traceError, traceInfoIfCI } from '../../../platform/logging'; +import { traceError, traceInfo, traceInfoIfCI } from '../../../platform/logging'; import { getInteractiveCellMetadata } from '../../../interactive-window/helpers'; import { KernelDebugAdapterBase } from '../../../kernels/debugger/kernelDebugAdapterBase'; +import { InteractiveCellMetadata } from '../../editor-integration/types'; export class KernelDebugAdapter extends KernelDebugAdapterBase { private readonly debugLocationTracker?: DebugAdapterTracker; + private readonly cellToDebugFileSortedInReverseOrderByLineNumber: { + debugFilePath: string; + interactiveWindow: Uri; + lineOffset: number; + metadata: InteractiveCellMetadata; + }[] = []; + constructor( session: DebugSession, notebookDocument: NotebookDocument, @@ -72,24 +80,143 @@ export class KernelDebugAdapter extends KernelDebugAdapterBase { throw new Error('Not an interactive window cell'); } try { - const response = await this.session.customRequest('dumpCell', { - code: (metadata.generatedCode?.code || cell.document.getText()).replace(/\r\n/g, '\n') - }); + const code = (metadata.generatedCode?.code || cell.document.getText()).replace(/\r\n/g, '\n'); + const response = await this.session.customRequest('dumpCell', { code }); + + // We know jupyter will strip out leading white spaces, hence take that into account. + const lines = metadata.generatedCode!.realCode.splitLines({ trim: false, removeEmptyEntries: false }); + const indexOfFirstNoneEmptyLine = lines.findIndex((line) => line.trim().length); + console.error(indexOfFirstNoneEmptyLine); const norm = path.normalize((response as IDumpCellResponse).sourcePath); - this.fileToCell.set(norm, { - uri: Uri.parse(metadata.interactive.uristring), - lineOffset: - metadata.interactive.lineIndex + - (metadata.generatedCode?.lineOffsetRelativeToIndexOfFirstLineInCell || 0) - }); - this.cellToFile.set(Uri.parse(metadata.interactive.uristring), { - path: norm, + this.fileToCell.set(norm, Uri.parse(metadata.interactive.uristring)); + + // If this cell doesn't have a cell marker, then + // Jupyter will strip out any leading whitespace. + // Take that into account. + let numberOfStrippedLines = 0; + if (metadata.generatedCode && !metadata.generatedCode.hasCellMarker) { + numberOfStrippedLines = metadata.generatedCode.firstNonBlankLineIndex; + } + this.cellToDebugFileSortedInReverseOrderByLineNumber.push({ + debugFilePath: norm, + interactiveWindow: Uri.parse(metadata.interactive.uristring), + metadata, lineOffset: + numberOfStrippedLines + metadata.interactive.lineIndex + (metadata.generatedCode?.lineOffsetRelativeToIndexOfFirstLineInCell || 0) }); + // Order cells in reverse order. + this.cellToDebugFileSortedInReverseOrderByLineNumber.sort( + (a, b) => b.metadata.interactive.lineIndex - a.metadata.interactive.lineIndex + ); } catch (err) { traceError(`Failed to dump cell for ${cell.index} with code ${metadata.interactive.originalSource}`, err); } } + protected override translateDebuggerFileToRealFile( + source: DebugProtocol.Source | undefined, + lines?: { line?: number; endLine?: number; lines?: number[] } + ) { + if (!source || !source.path || !lines || (typeof lines.line !== 'number' && !Array.isArray(lines.lines))) { + return; + } + // Find the cell that matches this line in the IW file by mapping the debugFilePath to the IW file. + const cell = this.cellToDebugFileSortedInReverseOrderByLineNumber.find( + (item) => item.debugFilePath === source.path + ); + if (!cell) { + return; + } + source.name = path.basename(cell.interactiveWindow.path); + source.path = cell.interactiveWindow.toString(); + if (typeof lines?.endLine === 'number') { + lines.endLine = lines.endLine + (cell.lineOffset || 0); + } + if (typeof lines?.line === 'number') { + lines.line = lines.line + (cell.lineOffset || 0); + } + if (lines?.lines && Array.isArray(lines?.lines)) { + lines.lines = lines?.lines.map((line) => line + (cell.lineOffset || 0)); + } + } + protected override translateRealFileToDebuggerFile( + source: DebugProtocol.Source | undefined, + lines?: { line?: number; endLine?: number; lines?: number[] } + ) { + if (!source || !source.path || !lines || (typeof lines.line !== 'number' && !Array.isArray(lines.lines))) { + return; + } + const startLine = lines.line || lines.lines![0]; + // Find the cell that matches this line in the IW file by mapping the debugFilePath to the IW file. + const cell = this.cellToDebugFileSortedInReverseOrderByLineNumber.find( + (item) => startLine >= item.metadata.interactive.lineIndex + 1 + ); + if (!cell) { + return; + } + source.path = cell.debugFilePath; + if (typeof lines?.endLine === 'number') { + lines.endLine = lines.endLine - (cell.lineOffset || 0); + } + if (typeof lines?.line === 'number') { + lines.line = lines.line - (cell.lineOffset || 0); + } + if (lines?.lines && Array.isArray(lines?.lines)) { + lines.lines = lines?.lines.map((line) => line - (cell.lineOffset || 0)); + } + } + + protected override async sendRequestToJupyterSession(message: DebugProtocol.ProtocolMessage) { + if (this.jupyterSession.disposed || this.jupyterSession.status === 'dead') { + traceInfo(`Skipping sending message ${message.type} because session is disposed`); + return; + } + + const request = message as unknown as DebugProtocol.SetBreakpointsRequest; + if (request.type === 'request' && request.command === 'setBreakpoints') { + const sortedLines = (request.arguments.lines || []).concat( + (request.arguments.breakpoints || []).map((bp) => bp.line) + ); + const startLine = sortedLines.length ? sortedLines[0] : undefined; + // Find the cell that matches this line in the IW file by mapping the debugFilePath to the IW file. + const cell = startLine + ? this.cellToDebugFileSortedInReverseOrderByLineNumber.find( + (item) => startLine >= item.metadata.interactive.lineIndex + 1 + ) + : undefined; + if (cell) { + const clonedRequest: typeof request = JSON.parse(JSON.stringify(request)); + if (request.arguments.lines) { + request.arguments.lines = request.arguments.lines.filter( + (line) => line <= cell.metadata.generatedCode!.endLine + ); + } + if (request.arguments.breakpoints) { + request.arguments.breakpoints = request.arguments.breakpoints.filter( + (bp) => bp.line <= cell.metadata.generatedCode!.endLine + ); + } + if (sortedLines.filter((line) => line > cell.metadata.generatedCode!.endLine).length) { + // Find all the lines that don't belong to this cell & add breakpoints for those as well + // However do that separately as they belong to different files. + await this.setBreakpoints({ + source: clonedRequest.arguments.source, + breakpoints: clonedRequest.arguments.breakpoints?.filter( + (bp) => bp.line > cell.metadata.generatedCode!.endLine + ), + lines: clonedRequest.arguments.lines?.filter( + (line) => line > cell.metadata.generatedCode!.endLine + ) + }); + } + } + } + + return super.sendRequestToJupyterSession(message); + } + + protected getDumpFilesForDeletion() { + return this.cellToDebugFileSortedInReverseOrderByLineNumber.map((item) => item.debugFilePath); + } } diff --git a/src/kernels/debugger/kernelDebugAdapterBase.ts b/src/kernels/debugger/kernelDebugAdapterBase.ts index 2a21b4ba6cc..8d0ac5ce8fd 100644 --- a/src/kernels/debugger/kernelDebugAdapterBase.ts +++ b/src/kernels/debugger/kernelDebugAdapterBase.ts @@ -34,15 +34,15 @@ import { IDebugInfoResponse } from './types'; import { sendTelemetryEvent } from '../../telemetry'; -import { traceError, traceInfo, traceInfoIfCI, traceVerbose } from '../../platform/logging'; +import { traceError, traceInfo, traceInfoIfCI, traceVerbose, traceWarning } from '../../platform/logging'; import { assertIsDebugConfig, isShortNamePath, shortNameMatchesLongName, getMessageSourceAndHookIt } from '../../notebooks/debugger/helper'; -import { ResourceMap } from '../../platform/vscode-path/map'; import { IDisposable } from '../../platform/common/types'; +import { executeSilently } from '../helpers'; /** * For info on the custom requests implemented by jupyter see: @@ -50,17 +50,7 @@ import { IDisposable } from '../../platform/common/types'; * https://jupyter-client.readthedocs.io/en/stable/messaging.html#additions-to-the-dap */ export abstract class KernelDebugAdapterBase implements DebugAdapter, IKernelDebugAdapter, IDisposable { - protected readonly fileToCell = new Map< - string, - { - uri: Uri; - lineOffset?: number; - } - >(); - protected readonly cellToFile = new ResourceMap<{ - path: string; - lineOffset?: number; - }>(); + protected readonly fileToCell = new Map(); private readonly sendMessage = new EventEmitter(); private readonly endSession = new EventEmitter(); private readonly configuration: IKernelDebugAdapterConfig; @@ -210,6 +200,7 @@ export abstract class KernelDebugAdapterBase implements DebugAdapter, IKernelDeb } dispose() { + this.deleteDumpedFiles().catch((ex) => traceWarning('Error deleting temporary debug files.', ex)); this.disposables.forEach((d) => d.dispose()); } @@ -233,9 +224,6 @@ export abstract class KernelDebugAdapterBase implements DebugAdapter, IKernelDeb ); } protected abstract dumpCell(index: number): Promise; - public getSourcePath(filePath: Uri) { - return this.cellToFile.get(filePath)?.path; - } private async debugInfo(): Promise { const response = await this.session.customRequest('debugInfo'); @@ -268,29 +256,13 @@ export abstract class KernelDebugAdapterBase implements DebugAdapter, IKernelDeb return undefined; } - private async sendRequestToJupyterSession(message: DebugProtocol.ProtocolMessage) { + protected async sendRequestToJupyterSession(message: DebugProtocol.ProtocolMessage) { if (this.jupyterSession.disposed || this.jupyterSession.status === 'dead') { traceInfo(`Skipping sending message ${message.type} because session is disposed`); return; } // map Source paths from VS Code to Ipykernel temp files - getMessageSourceAndHookIt(message, (source, lines?: { line?: number; endLine?: number; lines?: number[] }) => { - if (source && source.path) { - const mapping = this.cellToFile.get(Uri.parse(source.path)); - if (mapping) { - source.path = mapping.path; - if (typeof lines?.endLine === 'number') { - lines.endLine = lines.endLine - (mapping.lineOffset || 0); - } - if (typeof lines?.line === 'number') { - lines.line = lines.line - (mapping.lineOffset || 0); - } - if (lines?.lines && Array.isArray(lines?.lines)) { - lines.lines = lines?.lines.map((line) => line - (mapping.lineOffset || 0)); - } - } - } - }); + getMessageSourceAndHookIt(message, this.translateRealFileToDebuggerFile.bind(this)); this.trace('to kernel', JSON.stringify(message)); if (message.type === 'request') { @@ -307,27 +279,7 @@ export abstract class KernelDebugAdapterBase implements DebugAdapter, IKernelDeb control.onReply = (msg) => { const message = msg.content as DebugProtocol.ProtocolMessage; - getMessageSourceAndHookIt( - message, - (source, lines?: { line?: number; endLine?: number; lines?: number[] }) => { - if (source && source.path) { - const mapping = this.fileToCell.get(source.path) ?? this.lookupCellByLongName(source.path); - if (mapping) { - source.name = path.basename(mapping.uri.path); - source.path = mapping.uri.toString(); - if (typeof lines?.endLine === 'number') { - lines.endLine = lines.endLine + (mapping.lineOffset || 0); - } - if (typeof lines?.line === 'number') { - lines.line = lines.line + (mapping.lineOffset || 0); - } - if (lines?.lines && Array.isArray(lines?.lines)) { - lines.lines = lines?.lines.map((line) => line + (mapping.lineOffset || 0)); - } - } - } - } - ); + getMessageSourceAndHookIt(message, this.translateDebuggerFileToRealFile.bind(this)); this.trace('response', JSON.stringify(message)); this.sendMessage.fire(message); @@ -350,4 +302,50 @@ export abstract class KernelDebugAdapterBase implements DebugAdapter, IKernelDeb traceError(`Unknown message type to send ${message.type}`); } } + protected translateDebuggerFileToRealFile( + source: DebugProtocol.Source | undefined, + _lines?: { line?: number; endLine?: number; lines?: number[] } + ) { + if (source && source.path) { + const mapping = this.fileToCell.get(source.path) ?? this.lookupCellByLongName(source.path); + if (mapping) { + source.name = path.basename(mapping.path); + source.path = mapping.toString(); + } + } + } + protected abstract translateRealFileToDebuggerFile( + source: DebugProtocol.Source | undefined, + _lines?: { line?: number; endLine?: number; lines?: number[] } + ): void; + + protected abstract getDumpFilesForDeletion(): string[]; + private async deleteDumpedFiles() { + const fileValues = this.getDumpFilesForDeletion(); + // Need to have our Jupyter Session and some dumpCell files to delete + if (this.jupyterSession && fileValues.length) { + // Create our python string of file names + const fileListString = fileValues + .map((filePath) => { + // escape Windows path separators again for python + return '"' + filePath.replace(/\\/g, '\\\\') + '"'; + }) + .join(','); + + // Insert into our delete snippet + const deleteFilesCode = `import os +_VSCODE_fileList = [${fileListString}] +for file in _VSCODE_fileList: + try: + os.remove(file) + except: + pass +del _VSCODE_fileList`; + + return executeSilently(this.jupyterSession, deleteFilesCode, { + traceErrors: true, + traceErrorsMessage: 'Error deleting temporary debugging files' + }); + } + } } diff --git a/src/kernels/debugger/types.ts b/src/kernels/debugger/types.ts index 70c0c7c307d..52a2e8a633b 100644 --- a/src/kernels/debugger/types.ts +++ b/src/kernels/debugger/types.ts @@ -13,8 +13,7 @@ import { Event, NotebookCell, NotebookDocument, - NotebookEditor, - Uri + NotebookEditor } from 'vscode'; import { IFileGeneratedCodes } from '../../interactive-window/editor-integration/types'; @@ -72,7 +71,6 @@ export interface IKernelDebugAdapter extends DebugAdapter { onDidEndSession: Event; dumpAllCells(): Promise; getConfiguration(): IKernelDebugAdapterConfig; - getSourcePath(filePath: Uri): string | undefined; } export const IDebuggingManager = Symbol('IDebuggingManager'); diff --git a/src/kernels/variables/debuggerVariables.ts b/src/kernels/variables/debuggerVariables.ts index 61a9492186d..e3a8b2059bf 100644 --- a/src/kernels/variables/debuggerVariables.ts +++ b/src/kernels/variables/debuggerVariables.ts @@ -26,6 +26,7 @@ import { } from './types'; import { convertDebugProtocolVariableToIJupyterVariable, DataViewableTypes } from './helpers'; import { IFileSystem } from '../../platform/common/platform/types'; +import { noop } from '../../platform/common/utils/misc'; const KnownExcludedVariables = new Set(['In', 'Out', 'exit', 'quit']); const MaximumRowChunkSizeForDebugger = 100; @@ -452,7 +453,9 @@ export class DebuggerVariables // Call variables if (scopesResponse) { scopesResponse.scopes.forEach((scope: DebugProtocol.Scope) => { - void session.customRequest('variables', { variablesReference: scope.variablesReference }); + session + .customRequest('variables', { variablesReference: scope.variablesReference }) + .then(noop, noop); }); this.refreshEventEmitter.fire(); diff --git a/src/notebooks/debugger/helper.ts b/src/notebooks/debugger/helper.ts index 53bac4097e0..4493f549fe3 100644 --- a/src/notebooks/debugger/helper.ts +++ b/src/notebooks/debugger/helper.ts @@ -78,9 +78,7 @@ export function getMessageSourceAndHookIt( case 'setBreakpoints': // Keep track of the original source to be passed for other hooks. const originalSource = { ...(request.arguments as DebugProtocol.SetBreakpointsArguments).source }; - sourceHook((request.arguments as DebugProtocol.SetBreakpointsArguments).source); - // We pass a copy of the original source, as only the original object as the unaltered source. - sourceHook({ ...originalSource }, request.arguments); + sourceHook((request.arguments as DebugProtocol.SetBreakpointsArguments).source, request.arguments); const breakpoints = (request.arguments as DebugProtocol.SetBreakpointsArguments).breakpoints; if (breakpoints && Array.isArray(breakpoints)) { breakpoints.forEach((bk) => { diff --git a/src/notebooks/debugger/kernelDebugAdapter.ts b/src/notebooks/debugger/kernelDebugAdapter.ts index 6b9e384699b..44c67a037e6 100644 --- a/src/notebooks/debugger/kernelDebugAdapter.ts +++ b/src/notebooks/debugger/kernelDebugAdapter.ts @@ -7,16 +7,10 @@ import * as path from '../../platform/vscode-path/path'; import { IDumpCellResponse } from '../../kernels/debugger/types'; import { traceError } from '../../platform/logging'; import { KernelDebugAdapterBase } from '../../kernels/debugger/kernelDebugAdapterBase'; -import { executeSilently } from '../../kernels/helpers'; +import { DebugProtocol } from 'vscode-debugprotocol'; export class KernelDebugAdapter extends KernelDebugAdapterBase { - public override dispose() { - super.dispose(); - // On dispose, delete our temp cell files - this.deleteDumpCells().catch(() => { - traceError('Error deleting temporary debug files.'); - }); - } + private readonly cellToFile = new Map(); // Dump content of given cell into a tmp file and return path to file. protected override async dumpCell(index: number): Promise { @@ -26,44 +20,25 @@ export class KernelDebugAdapter extends KernelDebugAdapterBase { code: cell.document.getText().replace(/\r\n/g, '\n') }); const norm = path.normalize((response as IDumpCellResponse).sourcePath); - this.fileToCell.set(norm, { - uri: cell.document.uri - }); - this.cellToFile.set(cell.document.uri, { - path: norm - }); + this.fileToCell.set(norm, cell.document.uri); + this.cellToFile.set(cell.document.uri.toString(), norm); } catch (err) { traceError(err); } } - - // Use our jupyter session to delete all the cells - private async deleteDumpCells() { - const fileValues = [...this.cellToFile.values()]; - // Need to have our Jupyter Session and some dumpCell files to delete - if (this.jupyterSession && fileValues.length) { - // Create our python string of file names - const fileListString = fileValues - .map((filePath) => { - // escape Windows path separators again for python - return '"' + filePath.path.replace(/\\/g, '\\\\') + '"'; - }) - .join(','); - - // Insert into our delete snippet - const deleteFilesCode = `import os -_VSCODE_fileList = [${fileListString}] -for file in _VSCODE_fileList: - try: - os.remove(file) - except: - pass -del _VSCODE_fileList`; - - return executeSilently(this.jupyterSession, deleteFilesCode, { - traceErrors: true, - traceErrorsMessage: 'Error deleting temporary debugging files' - }); + protected translateRealFileToDebuggerFile( + source: DebugProtocol.Source | undefined, + _lines?: { line?: number; endLine?: number; lines?: number[] } + ) { + if (source && source.path) { + const mapping = this.cellToFile.get(source.path); + if (mapping) { + source.path = mapping; + } } } + + protected getDumpFilesForDeletion() { + return Array.from(this.cellToFile.values()); + } } diff --git a/src/platform/terminals/codeExecution/codeExecutionHelper.node.ts b/src/platform/terminals/codeExecution/codeExecutionHelper.node.ts index 7e439fa122c..5ac5ee7f996 100644 --- a/src/platform/terminals/codeExecution/codeExecutionHelper.node.ts +++ b/src/platform/terminals/codeExecution/codeExecutionHelper.node.ts @@ -66,7 +66,21 @@ export class CodeExecutionHelper extends CodeExecutionHelperBase { const result = await normalizeOutput.promise; const object = JSON.parse(result); - return parse(object.normalized); + const normalizedLines = parse(object.normalized); + // Python will remove leading empty spaces, add them back. + const indexOfFirstNonEmptyLineInOriginalCode = code + .splitLines({ trim: true, removeEmptyEntries: false }) + .findIndex((line) => line.length); + const indexOfFirstNonEmptyLineInNormalizedCode = normalizedLines + .splitLines({ trim: true, removeEmptyEntries: false }) + .findIndex((line) => line.length); + if (indexOfFirstNonEmptyLineInOriginalCode > indexOfFirstNonEmptyLineInNormalizedCode) { + // Some white space has been trimmed, add them back. + const trimmedLineCount = + indexOfFirstNonEmptyLineInOriginalCode - indexOfFirstNonEmptyLineInNormalizedCode; + return `${'\n'.repeat(trimmedLineCount)}${normalizedLines}`; + } + return normalizedLines; } catch (ex) { traceError(ex, 'Python: Failed to normalize code for execution in terminal'); return code; diff --git a/src/test/datascience/interactiveDebugging.vscode.common.ts b/src/test/datascience/interactiveDebugging.vscode.common.ts new file mode 100644 index 00000000000..2f23009fd7d --- /dev/null +++ b/src/test/datascience/interactiveDebugging.vscode.common.ts @@ -0,0 +1,612 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { assert } from 'chai'; +import * as sinon from 'sinon'; +import * as vscode from 'vscode'; +import { traceInfo } from '../../platform/logging'; +import { IDisposable } from '../../platform/common/types'; +import { InteractiveWindowProvider } from '../../interactive-window/interactiveWindowProvider'; +import { IExtensionTestApi, waitForCondition } from '../common'; +import { initialize, IS_REMOTE_NATIVE_TEST } from '../initialize.node'; +import { + submitFromPythonFile, + submitFromPythonFileUsingCodeWatcher, + waitForCodeLenses, + waitForLastCellToComplete +} from './helpers.node'; +import { closeNotebooksAndCleanUpAfterTests, defaultNotebookTestTimeout, getCellOutputs } from './notebook/helper'; +import { ITestWebviewHost } from './testInterfaces'; +import { waitForVariablesToMatch } from './variableView/variableViewHelpers'; +import { ITestVariableViewProvider } from './variableView/variableViewTestInterfaces'; +import { IInteractiveWindowProvider } from '../../interactive-window/types'; +import { Commands } from '../../platform/common/constants'; +import { IVariableViewProvider } from '../../webviews/extension-side/variablesView/types'; +import { pythonIWKernelDebugAdapter } from '../../kernels/debugger/constants'; + +export type DebuggerType = 'VSCodePythonDebugger' | 'JupyterProtocolDebugger'; + +export function sharedIWDebuggerTests( + this: Mocha.Suite, + options: { + startJupyterServer: (notebook?: vscode.NotebookDocument) => Promise; + suiteSetup?: (debuggerType: DebuggerType) => Promise; + captureScreenShot?: (title: string) => Promise; + } +) { + const debuggerTypes: DebuggerType[] = ['VSCodePythonDebugger', 'JupyterProtocolDebugger']; + debuggerTypes.forEach((debuggerType) => { + suite(`Debugging with ${debuggerType}`, async function () { + this.timeout(120_000); + let api: IExtensionTestApi; + const disposables: IDisposable[] = []; + let interactiveWindowProvider: InteractiveWindowProvider; + let variableViewProvider: ITestVariableViewProvider; + let debugAdapterTracker: vscode.DebugAdapterTracker | undefined; + const tracker: vscode.DebugAdapterTrackerFactory = { + createDebugAdapterTracker: function ( + _session: vscode.DebugSession + ): vscode.ProviderResult { + return debugAdapterTracker; + } + }; + suiteSetup(async function () { + if (IS_REMOTE_NATIVE_TEST() && debuggerType === 'VSCodePythonDebugger') { + return this.skip(); + } + if (options.suiteSetup) { + await options.suiteSetup(debuggerType); + } + }); + suiteTeardown(() => vscode.commands.executeCommand('workbench.debug.viewlet.action.removeAllBreakpoints')); + setup(async function () { + if (IS_REMOTE_NATIVE_TEST() && debuggerType === 'VSCodePythonDebugger') { + return this.skip(); + } + traceInfo(`Start Test ${this.currentTest?.title}`); + api = await initialize(); + if (IS_REMOTE_NATIVE_TEST() && debuggerType === 'VSCodePythonDebugger') { + await options.startJupyterServer(); + } + await vscode.commands.executeCommand('workbench.debug.viewlet.action.removeAllBreakpoints'); + disposables.push(vscode.debug.registerDebugAdapterTrackerFactory('python', tracker)); + disposables.push(vscode.debug.registerDebugAdapterTrackerFactory(pythonIWKernelDebugAdapter, tracker)); + interactiveWindowProvider = api.serviceManager.get(IInteractiveWindowProvider); + traceInfo(`Start Test (completed) ${this.currentTest?.title}`); + const coreVariableViewProvider = api.serviceContainer.get(IVariableViewProvider); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + variableViewProvider = coreVariableViewProvider as any as ITestVariableViewProvider; // Cast to expose the test interfaces + }); + teardown(async function () { + // Make sure that debugging is shut down + await vscode.commands.executeCommand('workbench.action.debug.stop'); + await vscode.commands.executeCommand('workbench.action.debug.disconnect'); + await waitForCondition( + async () => { + return vscode.debug.activeDebugSession === undefined; + }, + defaultNotebookTestTimeout, + `Unable to stop debug session on test teardown` + ); + + traceInfo(`Ended Test ${this.currentTest?.title}`); + if (this.currentTest?.isFailed() && options.captureScreenShot) { + await options.captureScreenShot(this.currentTest?.title); + } + sinon.restore(); + debugAdapterTracker = undefined; + await closeNotebooksAndCleanUpAfterTests(disposables); + }); + + test('Debug a cell from a python file', async () => { + // Run a cell to get IW open + const source = 'print(42)'; + const { activeInteractiveWindow, untitledPythonFile } = await submitFromPythonFile( + interactiveWindowProvider, + source, + disposables + ); + await waitForLastCellToComplete(activeInteractiveWindow); + + // Add some more text + const editor = vscode.window.visibleTextEditors.find((e) => e.document.uri === untitledPythonFile.uri); + assert.ok(editor, `Couldn't find python file`); + await editor?.edit((b) => { + b.insert(new vscode.Position(1, 0), '\n# %%\n\n\nprint(43)'); + }); + + let codeLenses = await waitForCodeLenses(untitledPythonFile.uri, Commands.DebugCell); + let stopped = false; + let stoppedOnLine5 = false; + debugAdapterTracker = { + onDidSendMessage: (message) => { + if (message.event == 'stopped') { + stopped = true; + } + if (message.command == 'stackTrace' && !stoppedOnLine5) { + stoppedOnLine5 = message.body.stackFrames[0].line == 5; + } + } + }; + + // Try debugging the cell + assert.ok(codeLenses, `No code lenses found`); + assert.equal(codeLenses.length, 3, `Wrong number of code lenses found`); + const args = codeLenses[2].command!.arguments || []; + void vscode.commands.executeCommand(codeLenses[2].command!.command, ...args); + + // Wait for breakpoint to be hit + await waitForCondition( + async () => { + return vscode.debug.activeDebugSession != undefined && stopped; + }, + defaultNotebookTestTimeout, + `Never hit stop event when waiting for debug cell` + ); + + // Verify we are on the 'print(43)' line (might take a second for UI to update after stop event) + await waitForCondition( + async () => { + return stoppedOnLine5; + }, + defaultNotebookTestTimeout, + `Cursor did not move to expected line when hitting breakpoint` + ); + }); + + test('Run a cell and step into breakpoint', async function () { + // Define the function + const source = 'def foo():\n print("foo")'; + const { activeInteractiveWindow, untitledPythonFile } = await submitFromPythonFile( + interactiveWindowProvider, + source, + disposables + ); + await waitForLastCellToComplete(activeInteractiveWindow); + + // Add some more text + const editor = vscode.window.visibleTextEditors.find((e) => e.document.uri === untitledPythonFile.uri); + assert.ok(editor, `Couldn't find python file`); + await editor?.edit((b) => { + b.insert(new vscode.Position(2, 0), '\n# %%\nfoo()'); + }); + + let codeLenses = await waitForCodeLenses(untitledPythonFile.uri, Commands.DebugCell); + + // Insert a breakpoint on line 2 + vscode.debug.addBreakpoints([ + new vscode.SourceBreakpoint( + new vscode.Location(untitledPythonFile.uri, new vscode.Position(1, 0)), + true + ) + ]); + + let stopped = false; + let stoppedOnBreakpoint = false; + debugAdapterTracker = { + onDidSendMessage: (message) => { + if (message.event == 'stopped') { + stopped = true; + } + if ( + message.command == 'stackTrace' && + !stoppedOnBreakpoint && + message.body.stackFrames.length + ) { + stoppedOnBreakpoint = message.body.stackFrames[0].line == 2; + } + } + }; + + // Try debugging the cell + assert.ok(codeLenses, `No code lenses found`); + assert.equal(codeLenses.length, 3, `Wrong number of code lenses found`); + let args = codeLenses[2].command!.arguments || []; + void vscode.commands.executeCommand(codeLenses[2].command!.command, ...args); + + // Wait for breakpoint to be hit + await waitForCondition( + async () => { + return vscode.debug.activeDebugSession != undefined && stopped; + }, + defaultNotebookTestTimeout, + `Never hit stop event when waiting for debug cell` + ); + + codeLenses = await waitForCodeLenses(untitledPythonFile.uri, Commands.DebugContinue); + stopped = false; + // Continue and wait for stopped. + args = codeLenses[0].command!.arguments || []; + void vscode.commands.executeCommand(codeLenses[0].command!.command, ...args); + await waitForCondition( + async () => { + return stoppedOnBreakpoint && stopped; + }, + defaultNotebookTestTimeout, + `Did not hit breakpoint during continue` + ); + }); + + test('Update variables during stepping', async () => { + // Define the function + const source = 'print(42)'; + const { activeInteractiveWindow, untitledPythonFile } = await submitFromPythonFile( + interactiveWindowProvider, + source, + disposables + ); + await waitForLastCellToComplete(activeInteractiveWindow); + + // Add some more text + const editor = vscode.window.visibleTextEditors.find((e) => e.document.uri === untitledPythonFile.uri); + assert.ok(editor, `Couldn't find python file`); + await editor?.edit((b) => { + b.insert(new vscode.Position(1, 0), '\n# %%\nx = 1\nx = 2\nx = 3\nx'); + }); + + let codeLenses = await waitForCodeLenses(untitledPythonFile.uri, Commands.DebugCell); + let stopped = false; + debugAdapterTracker = { + onDidSendMessage: (message) => { + if (message.event == 'stopped') { + stopped = true; + } + } + }; + + // Try debugging the cell + assert.ok(codeLenses, `No code lenses found`); + assert.equal(codeLenses.length, 3, `Wrong number of code lenses found`); + let args = codeLenses[2].command!.arguments || []; + void vscode.commands.executeCommand(codeLenses[2].command!.command, ...args); + + // Wait for breakpoint to be hit + await waitForCondition( + async () => { + return vscode.debug.activeDebugSession != undefined && stopped; + }, + defaultNotebookTestTimeout, + `Never hit stop event when waiting for debug cell` + ); + + // Wait to get the step over code lens + codeLenses = await waitForCodeLenses(untitledPythonFile.uri, Commands.DebugStepOver); + // Step once + stopped = false; + // Continue and wait for stopped. + args = codeLenses[2].command!.arguments || []; + void vscode.commands.executeCommand(codeLenses[2].command!.command, ...args); + await waitForCondition( + async () => { + return stopped; + }, + defaultNotebookTestTimeout, + `Did not do first step` + ); + + // Send the command to open the view + await vscode.commands.executeCommand(Commands.OpenVariableView); + + // Aquire the variable view from the provider + const coreVariableView = await variableViewProvider.activeVariableView; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const variableView = coreVariableView as any as ITestWebviewHost; + + // Parse the HTML for our expected variables + let expectedVariables = [{ name: 'x', type: 'int', length: '', value: '1' }]; + await waitForVariablesToMatch(expectedVariables, variableView); + + stopped = false; + // Continue and wait for stopped. + args = codeLenses[2].command!.arguments || []; + void vscode.commands.executeCommand(codeLenses[2].command!.command, ...args); + await waitForCondition( + async () => { + return stopped; + }, + defaultNotebookTestTimeout, + `Did not do second step` + ); + + expectedVariables = [{ name: 'x', type: 'int', length: '', value: '2' }]; + await waitForVariablesToMatch(expectedVariables, variableView); + + stopped = false; + // Continue and wait for stopped. + args = codeLenses[2].command!.arguments || []; + void vscode.commands.executeCommand(codeLenses[2].command!.command, ...args); + await waitForCondition( + async () => { + return stopped; + }, + defaultNotebookTestTimeout, + `Did not do third step` + ); + + expectedVariables = [{ name: 'x', type: 'int', length: '', value: '3' }]; + await waitForVariablesToMatch(expectedVariables, variableView); + }); + + test('Run a cell and stop in the middle', async () => { + // Define the function + const source = 'print(42)'; + const { activeInteractiveWindow, untitledPythonFile } = await submitFromPythonFile( + interactiveWindowProvider, + source, + disposables + ); + await waitForLastCellToComplete(activeInteractiveWindow); + + // Add some more text + const editor = vscode.window.visibleTextEditors.find((e) => e.document.uri === untitledPythonFile.uri); + assert.ok(editor, `Couldn't find python file`); + await editor?.edit((b) => { + b.insert( + new vscode.Position(1, 0), + `\n# %%\nimport time\nwhile(True):\n print(1)\n time.sleep(.1)\nprint('finished')` + ); + }); + + let codeLenses = await waitForCodeLenses(untitledPythonFile.uri, Commands.DebugCell); + let stopped = false; + debugAdapterTracker = { + onDidSendMessage: (message) => { + if (message.event == 'stopped') { + stopped = true; + } + } + }; + + // Try debugging the cell + assert.ok(codeLenses, `No code lenses found`); + assert.equal(codeLenses.length, 3, `Wrong number of code lenses found`); + let args = codeLenses[2].command!.arguments || []; + void vscode.commands.executeCommand(codeLenses[2].command!.command, ...args); + + // Wait for breakpoint to be hit + await waitForCondition( + async () => { + return vscode.debug.activeDebugSession != undefined && stopped; + }, + defaultNotebookTestTimeout, + `Never hit stop event when waiting for debug cell` + ); + + // Now we should have a stop command (second one) + codeLenses = await waitForCodeLenses(untitledPythonFile.uri, Commands.DebugStop); + const lastCell = await waitForLastCellToComplete(activeInteractiveWindow, -1, true); + const outputs = getCellOutputs(lastCell); + assert.isFalse(outputs.includes('finished'), 'Cell finished during a stop'); + }); + + test('Correctly handle leading spaces in a code cell we are debugging', async () => { + // First just get our window up and started + const source = 'c = 50\n'; + const { activeInteractiveWindow, untitledPythonFile } = await submitFromPythonFile( + interactiveWindowProvider, + source, + disposables + ); + await waitForLastCellToComplete(activeInteractiveWindow); + + // Next add some code lines with leading spaces, we are going to debug this cell and we want to end up on the + // correct starting line + const leadingSpacesSource = `# %% + + + + +a = 100 +b = 200 +`; + const editor = vscode.window.visibleTextEditors.find((e) => e.document.uri === untitledPythonFile.uri); + assert.ok(editor, `Couldn't find python file`); + await editor?.edit((edit) => { + edit.insert(new vscode.Position(2, 0), leadingSpacesSource); + }); + + let codeLenses = await waitForCodeLenses(untitledPythonFile.uri, Commands.DebugCell); + let stopped = false; + let stoppedOnLine = false; + debugAdapterTracker = { + onDidSendMessage: (message) => { + if (message.event == 'stopped') { + stopped = true; + } + if (message.command == 'stackTrace' && !stoppedOnLine) { + stoppedOnLine = message.body.stackFrames[0].line == 7; + } + } + }; + + // Try debugging the cell + assert.ok(codeLenses, `No code lenses found`); + assert.equal(codeLenses.length, 3, `Wrong number of code lenses found`); + let args = codeLenses[2].command!.arguments || []; + void vscode.commands.executeCommand(codeLenses[2].command!.command, ...args); + + // Wait for breakpoint to be hit + await waitForCondition( + async () => { + return vscode.debug.activeDebugSession != undefined && stopped; + }, + defaultNotebookTestTimeout, + `Never hit stop event when waiting for debug cell` + ); + + // Verify we are on the 'a = 100' line (might take a second for UI to update after stop event) + await waitForCondition( + async () => { + return stoppedOnLine; + }, + defaultNotebookTestTimeout, + `Cursor did not move to expected line when hitting breakpoint` + ); + }); + test('Correctly handle leading spaces in a previously run code cell', async () => { + // Define the function with some leading spaces and run it (don't debug it) + const source = ` + + + + +def foo(): + x = 10`; + const { activeInteractiveWindow, untitledPythonFile } = await submitFromPythonFile( + interactiveWindowProvider, + source, + disposables + ); + await waitForLastCellToComplete(activeInteractiveWindow); + + // Add some more text + const editor = vscode.window.visibleTextEditors.find((e) => e.document.uri === untitledPythonFile.uri); + assert.ok(editor, `Couldn't find python file`); + await editor?.edit((b) => { + b.insert(new vscode.Position(8, 0), '\n# %%\nfoo()'); + }); + + let codeLenses = await waitForCodeLenses(untitledPythonFile.uri, Commands.DebugCell); + let stopped = false; + let stoppedOnLine = false; + let targetLine = 9; + debugAdapterTracker = { + onDidSendMessage: (message) => { + if (message.event == 'stopped') { + stopped = true; + } + if (message.command == 'stackTrace' && !stoppedOnLine) { + stoppedOnLine = message.body.stackFrames[0].line == targetLine; + } + } + }; + + assert.ok(codeLenses, `No code lenses found`); + assert.equal(codeLenses.length, 3, `Wrong number of code lenses found`); + let args = codeLenses[2].command!.arguments || []; + void vscode.commands.executeCommand(codeLenses[2].command!.command, ...args); + + // Wait for breakpoint to be hit + await waitForCondition( + async () => { + return vscode.debug.activeDebugSession != undefined && stopped; + }, + defaultNotebookTestTimeout, + `Never hit stop event when waiting for debug cell` + ); + + // Verify that we hit the correct line + await waitForCondition( + async () => { + return stoppedOnLine; + }, + defaultNotebookTestTimeout, + `Cursor did not move to expected line when hitting breakpoint` + ); + + // Perform a step into + stopped = false; + stoppedOnLine = false; + targetLine = 7; + + void vscode.commands.executeCommand('workbench.action.debug.stepInto'); + await waitForCondition( + async () => { + return stopped; + }, + defaultNotebookTestTimeout, + `Did not stop on step into` + ); + + // Verify that we hit the correct line + await waitForCondition( + async () => { + return stoppedOnLine; + }, + defaultNotebookTestTimeout, + `Cursor did not move to expected line when hitting stepping into` + ); + }); + + test('Step into a previous cell', async () => { + // Need a function and a call to the function + const source = ` +# %% +def foo(): + x = 10 + +# %% +foo() +`; + const { activeInteractiveWindow, untitledPythonFile } = await submitFromPythonFileUsingCodeWatcher( + source, + disposables + ); + await waitForLastCellToComplete(activeInteractiveWindow, 2); + + let codeLenses = await waitForCodeLenses(untitledPythonFile.uri, Commands.DebugCell); + let stopped = false; + let stoppedOnLine = false; + let targetLine = 7; + debugAdapterTracker = { + onDidSendMessage: (message) => { + if (message.event == 'stopped') { + stopped = true; + } + if (message.command == 'stackTrace' && !stoppedOnLine) { + stoppedOnLine = message.body.stackFrames[0].line == targetLine; + } + } + }; + + const debugCellCodeLenses = codeLenses.filter((c) => c.command?.command === Commands.DebugCell); + const debugCellCodeLens = debugCellCodeLenses[1]; + let args = debugCellCodeLens.command!.arguments || []; + void vscode.commands.executeCommand(debugCellCodeLens.command!.command, ...args); + + // Wait for breakpoint to be hit + await waitForCondition( + async () => { + return vscode.debug.activeDebugSession != undefined && stopped; + }, + defaultNotebookTestTimeout, + `Never hit stop event when waiting for debug cell` + ); + + // Verify that we hit the correct line + await waitForCondition( + async () => { + return stoppedOnLine; + }, + defaultNotebookTestTimeout, + `Cursor did not move to expected line when hitting breakpoint` + ); + + // Perform a step into + stopped = false; + stoppedOnLine = false; + targetLine = 4; + + void vscode.commands.executeCommand('workbench.action.debug.stepInto'); + await waitForCondition( + async () => { + return stopped; + }, + defaultNotebookTestTimeout, + `Did not stop on step into` + ); + + // Verify that we hit the correct line + await waitForCondition( + async () => { + return stoppedOnLine; + }, + defaultNotebookTestTimeout, + `Cursor did not move to expected line when hitting stepping into` + ); + }); + }); + }); +} diff --git a/src/test/datascience/interactiveDebugging.vscode.test.ts b/src/test/datascience/interactiveDebugging.vscode.test.ts index f1df2ab05ff..737af206497 100644 --- a/src/test/datascience/interactiveDebugging.vscode.test.ts +++ b/src/test/datascience/interactiveDebugging.vscode.test.ts @@ -3,577 +3,46 @@ 'use strict'; -import { assert } from 'chai'; -import * as sinon from 'sinon'; -import * as vscode from 'vscode'; -import { traceInfo } from '../../platform/logging'; -import { IDisposable } from '../../platform/common/types'; -import { InteractiveWindowProvider } from '../../interactive-window/interactiveWindowProvider'; -import { captureScreenShot, IExtensionTestApi, waitForCondition } from '../common.node'; -import { initialize, IS_REMOTE_NATIVE_TEST } from '../initialize.node'; -import { - submitFromPythonFile, - submitFromPythonFileUsingCodeWatcher, - waitForCodeLenses, - waitForLastCellToComplete -} from './helpers.node'; -import { closeNotebooksAndCleanUpAfterTests, defaultNotebookTestTimeout, getCellOutputs } from './notebook/helper.node'; -import { ITestWebviewHost } from './testInterfaces'; -import { waitForVariablesToMatch } from './variableView/variableViewHelpers'; -import { ITestVariableViewProvider } from './variableView/variableViewTestInterfaces'; -import { IInteractiveWindowProvider } from '../../interactive-window/types'; -import { Commands } from '../../platform/common/constants'; -import { IVariableViewProvider } from '../../webviews/extension-side/variablesView/types'; -import { pythonIWKernelDebugAdapter } from '../../kernels/debugger/constants'; - -suite('Interactive window debugging', async function () { - this.timeout(120_000); - let api: IExtensionTestApi; - const disposables: IDisposable[] = []; - let interactiveWindowProvider: InteractiveWindowProvider; - let variableViewProvider: ITestVariableViewProvider; - let debugAdapterTracker: vscode.DebugAdapterTracker | undefined; - const tracker: vscode.DebugAdapterTrackerFactory = { - createDebugAdapterTracker: function ( - _session: vscode.DebugSession - ): vscode.ProviderResult { - return debugAdapterTracker; - } - }; - - setup(async function () { - if (IS_REMOTE_NATIVE_TEST()) { - return this.skip(); - } - traceInfo(`Start Test ${this.currentTest?.title}`); - api = await initialize(); - disposables.push(vscode.debug.registerDebugAdapterTrackerFactory('python', tracker)); - disposables.push(vscode.debug.registerDebugAdapterTrackerFactory(pythonIWKernelDebugAdapter, tracker)); - interactiveWindowProvider = api.serviceManager.get(IInteractiveWindowProvider); - traceInfo(`Start Test (completed) ${this.currentTest?.title}`); - const coreVariableViewProvider = api.serviceContainer.get(IVariableViewProvider); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - variableViewProvider = coreVariableViewProvider as any as ITestVariableViewProvider; // Cast to expose the test interfaces - }); - teardown(async function () { - // Make sure that debugging is shut down - await vscode.commands.executeCommand('workbench.action.debug.stop'); - await vscode.commands.executeCommand('workbench.action.debug.disconnect'); - await waitForCondition( - async () => { - return vscode.debug.activeDebugSession === undefined; - }, - defaultNotebookTestTimeout, - `Unable to stop debug session on test teardown` - ); - - traceInfo(`Ended Test ${this.currentTest?.title}`); - if (this.currentTest?.isFailed()) { - await captureScreenShot(this.currentTest?.title); - } - sinon.restore(); - debugAdapterTracker = undefined; - await closeNotebooksAndCleanUpAfterTests(disposables); - }); - - test('Debug a cell from a python file', async () => { - // Run a cell to get IW open - const source = 'print(42)'; - const { activeInteractiveWindow, untitledPythonFile } = await submitFromPythonFile( - interactiveWindowProvider, - source, - disposables - ); - await waitForLastCellToComplete(activeInteractiveWindow); - - // Add some more text - const editor = vscode.window.visibleTextEditors.find((e) => e.document.uri === untitledPythonFile.uri); - assert.ok(editor, `Couldn't find python file`); - await editor?.edit((b) => { - b.insert(new vscode.Position(1, 0), '\n# %%\n\n\nprint(43)'); - }); - - let codeLenses = await waitForCodeLenses(untitledPythonFile.uri, Commands.DebugCell); - let stopped = false; - let stoppedOnLine5 = false; - debugAdapterTracker = { - onDidSendMessage: (message) => { - if (message.event == 'stopped') { - stopped = true; - } - if (message.command == 'stackTrace' && !stoppedOnLine5) { - stoppedOnLine5 = message.body.stackFrames[0].line == 5; - } - } - }; - - // Try debugging the cell - assert.ok(codeLenses, `No code lenses found`); - assert.equal(codeLenses.length, 3, `Wrong number of code lenses found`); - const args = codeLenses[2].command!.arguments || []; - void vscode.commands.executeCommand(codeLenses[2].command!.command, ...args); - - // Wait for breakpoint to be hit - await waitForCondition( - async () => { - return vscode.debug.activeDebugSession != undefined && stopped; - }, - defaultNotebookTestTimeout, - `Never hit stop event when waiting for debug cell` - ); - - // Verify we are on the 'print(43)' line (might take a second for UI to update after stop event) - await waitForCondition( - async () => { - return stoppedOnLine5; - }, - defaultNotebookTestTimeout, - `Cursor did not move to expected line when hitting breakpoint` - ); - }); - - test('Run a cell and step into breakpoint', async function () { - // Define the function - const source = 'def foo():\n print("foo")'; - const { activeInteractiveWindow, untitledPythonFile } = await submitFromPythonFile( - interactiveWindowProvider, - source, - disposables - ); - await waitForLastCellToComplete(activeInteractiveWindow); - - // Add some more text - const editor = vscode.window.visibleTextEditors.find((e) => e.document.uri === untitledPythonFile.uri); - assert.ok(editor, `Couldn't find python file`); - await editor?.edit((b) => { - b.insert(new vscode.Position(2, 0), '\n# %%\nfoo()'); - }); - - let codeLenses = await waitForCodeLenses(untitledPythonFile.uri, Commands.DebugCell); - - // Insert a breakpoint on line 2 - vscode.debug.addBreakpoints([ - new vscode.SourceBreakpoint(new vscode.Location(untitledPythonFile.uri, new vscode.Position(1, 0)), true) - ]); - - let stopped = false; - let stoppedOnBreakpoint = false; - debugAdapterTracker = { - onDidSendMessage: (message) => { - if (message.event == 'stopped') { - stopped = true; - } - if (message.command == 'stackTrace' && !stoppedOnBreakpoint && message.body.stackFrames.length) { - stoppedOnBreakpoint = message.body.stackFrames[0].line == 2; - } - } - }; - - // Try debugging the cell - assert.ok(codeLenses, `No code lenses found`); - assert.equal(codeLenses.length, 3, `Wrong number of code lenses found`); - let args = codeLenses[2].command!.arguments || []; - void vscode.commands.executeCommand(codeLenses[2].command!.command, ...args); - - // Wait for breakpoint to be hit - await waitForCondition( - async () => { - return vscode.debug.activeDebugSession != undefined && stopped; - }, - defaultNotebookTestTimeout, - `Never hit stop event when waiting for debug cell` - ); - - codeLenses = await waitForCodeLenses(untitledPythonFile.uri, Commands.DebugContinue); - stopped = false; - // Continue and wait for stopped. - args = codeLenses[0].command!.arguments || []; - void vscode.commands.executeCommand(codeLenses[0].command!.command, ...args); - await waitForCondition( - async () => { - return stoppedOnBreakpoint && stopped; - }, - defaultNotebookTestTimeout, - `Did not hit breakpoint during continue` - ); - }); - - test('Update variables during stepping', async () => { - // Define the function - const source = 'print(42)'; - const { activeInteractiveWindow, untitledPythonFile } = await submitFromPythonFile( - interactiveWindowProvider, - source, - disposables - ); - await waitForLastCellToComplete(activeInteractiveWindow); - - // Add some more text - const editor = vscode.window.visibleTextEditors.find((e) => e.document.uri === untitledPythonFile.uri); - assert.ok(editor, `Couldn't find python file`); - await editor?.edit((b) => { - b.insert(new vscode.Position(1, 0), '\n# %%\nx = 1\nx = 2\nx = 3\nx'); - }); - - let codeLenses = await waitForCodeLenses(untitledPythonFile.uri, Commands.DebugCell); - let stopped = false; - debugAdapterTracker = { - onDidSendMessage: (message) => { - if (message.event == 'stopped') { - stopped = true; - } - } - }; - - // Try debugging the cell - assert.ok(codeLenses, `No code lenses found`); - assert.equal(codeLenses.length, 3, `Wrong number of code lenses found`); - let args = codeLenses[2].command!.arguments || []; - void vscode.commands.executeCommand(codeLenses[2].command!.command, ...args); - - // Wait for breakpoint to be hit - await waitForCondition( - async () => { - return vscode.debug.activeDebugSession != undefined && stopped; - }, - defaultNotebookTestTimeout, - `Never hit stop event when waiting for debug cell` - ); - - // Wait to get the step over code lens - codeLenses = await waitForCodeLenses(untitledPythonFile.uri, Commands.DebugStepOver); - // Step once - stopped = false; - // Continue and wait for stopped. - args = codeLenses[2].command!.arguments || []; - void vscode.commands.executeCommand(codeLenses[2].command!.command, ...args); - await waitForCondition( - async () => { - return stopped; - }, - defaultNotebookTestTimeout, - `Did not do first step` - ); - - // Send the command to open the view - await vscode.commands.executeCommand(Commands.OpenVariableView); - - // Aquire the variable view from the provider - const coreVariableView = await variableViewProvider.activeVariableView; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const variableView = coreVariableView as any as ITestWebviewHost; - - // Parse the HTML for our expected variables - let expectedVariables = [{ name: 'x', type: 'int', length: '', value: '1' }]; - await waitForVariablesToMatch(expectedVariables, variableView); - - stopped = false; - // Continue and wait for stopped. - args = codeLenses[2].command!.arguments || []; - void vscode.commands.executeCommand(codeLenses[2].command!.command, ...args); - await waitForCondition( - async () => { - return stopped; - }, - defaultNotebookTestTimeout, - `Did not do second step` - ); - - expectedVariables = [{ name: 'x', type: 'int', length: '', value: '2' }]; - await waitForVariablesToMatch(expectedVariables, variableView); - - stopped = false; - // Continue and wait for stopped. - args = codeLenses[2].command!.arguments || []; - void vscode.commands.executeCommand(codeLenses[2].command!.command, ...args); - await waitForCondition( - async () => { - return stopped; - }, - defaultNotebookTestTimeout, - `Did not do third step` - ); - - expectedVariables = [{ name: 'x', type: 'int', length: '', value: '3' }]; - await waitForVariablesToMatch(expectedVariables, variableView); - }); - - test('Run a cell and stop in the middle', async () => { - // Define the function - const source = 'print(42)'; - const { activeInteractiveWindow, untitledPythonFile } = await submitFromPythonFile( - interactiveWindowProvider, - source, - disposables - ); - await waitForLastCellToComplete(activeInteractiveWindow); - - // Add some more text - const editor = vscode.window.visibleTextEditors.find((e) => e.document.uri === untitledPythonFile.uri); - assert.ok(editor, `Couldn't find python file`); - await editor?.edit((b) => { - b.insert( - new vscode.Position(1, 0), - `\n# %%\nimport time\nwhile(True):\n print(1)\n time.sleep(.1)\nprint('finished')` +import * as path from '../../platform/vscode-path/path'; +import * as fs from 'fs-extra'; +import { EXTENSION_ROOT_DIR } from '../../platform/constants.node'; +import { DebuggerType, sharedIWDebuggerTests } from './interactiveDebugging.vscode.common'; +import { startJupyterServer } from './notebook/helper.node'; +import { captureScreenShot } from '../common.node'; + +/* eslint-disable @typescript-eslint/no-explicit-any, no-invalid-this */ +suite('Interactive Window Debugging', function () { + const settingsFile = path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'datascience', '.vscode', 'settings.json'); + async function enableJupyterDebugger(debuggerType: DebuggerType) { + const enable = debuggerType === 'JupyterProtocolDebugger'; + const settingFileContents = fs.readFileSync(settingsFile).toString(); + if (enable && settingFileContents.includes(`"jupyter.forceIPyKernelDebugger": true`)) { + return; + } else if (enable && settingFileContents.includes(`"jupyter.forceIPyKernelDebugger": false`)) { + fs.writeFileSync( + settingsFile, + settingFileContents.replace( + `"jupyter.forceIPyKernelDebugger": false`, + `"jupyter.forceIPyKernelDebugger": true` + ) ); - }); - - let codeLenses = await waitForCodeLenses(untitledPythonFile.uri, Commands.DebugCell); - let stopped = false; - debugAdapterTracker = { - onDidSendMessage: (message) => { - if (message.event == 'stopped') { - stopped = true; - } - } - }; - - // Try debugging the cell - assert.ok(codeLenses, `No code lenses found`); - assert.equal(codeLenses.length, 3, `Wrong number of code lenses found`); - let args = codeLenses[2].command!.arguments || []; - void vscode.commands.executeCommand(codeLenses[2].command!.command, ...args); - - // Wait for breakpoint to be hit - await waitForCondition( - async () => { - return vscode.debug.activeDebugSession != undefined && stopped; - }, - defaultNotebookTestTimeout, - `Never hit stop event when waiting for debug cell` - ); - - // Now we should have a stop command (second one) - codeLenses = await waitForCodeLenses(untitledPythonFile.uri, Commands.DebugStop); - const lastCell = await waitForLastCellToComplete(activeInteractiveWindow, -1, true); - const outputs = getCellOutputs(lastCell); - assert.isFalse(outputs.includes('finished'), 'Cell finished during a stop'); - }); - - test('Correctly handle leading spaces in a code cell we are debugging', async () => { - // First just get our window up and started - const source = 'c = 50\n'; - const { activeInteractiveWindow, untitledPythonFile } = await submitFromPythonFile( - interactiveWindowProvider, - source, - disposables - ); - await waitForLastCellToComplete(activeInteractiveWindow); - - // Next add some code lines with leading spaces, we are going to debug this cell and we want to end up on the - // correct starting line - const leadingSpacesSource = `# %% - - - - -a = 100 -b = 200 -`; - const editor = vscode.window.visibleTextEditors.find((e) => e.document.uri === untitledPythonFile.uri); - assert.ok(editor, `Couldn't find python file`); - await editor?.edit((edit) => { - edit.insert(new vscode.Position(2, 0), leadingSpacesSource); - }); - - let codeLenses = await waitForCodeLenses(untitledPythonFile.uri, Commands.DebugCell); - let stopped = false; - let stoppedOnLine = false; - debugAdapterTracker = { - onDidSendMessage: (message) => { - if (message.event == 'stopped') { - stopped = true; - } - if (message.command == 'stackTrace' && !stoppedOnLine) { - stoppedOnLine = message.body.stackFrames[0].line == 7; - } - } - }; - - // Try debugging the cell - assert.ok(codeLenses, `No code lenses found`); - assert.equal(codeLenses.length, 3, `Wrong number of code lenses found`); - let args = codeLenses[2].command!.arguments || []; - void vscode.commands.executeCommand(codeLenses[2].command!.command, ...args); - - // Wait for breakpoint to be hit - await waitForCondition( - async () => { - return vscode.debug.activeDebugSession != undefined && stopped; - }, - defaultNotebookTestTimeout, - `Never hit stop event when waiting for debug cell` - ); - - // Verify we are on the 'a = 100' line (might take a second for UI to update after stop event) - await waitForCondition( - async () => { - return stoppedOnLine; - }, - defaultNotebookTestTimeout, - `Cursor did not move to expected line when hitting breakpoint` - ); - }); - test('Correctly handle leading spaces in a previously run code cell', async () => { - // Define the function with some leading spaces and run it (don't debug it) - const source = ` - - - - -def foo(): - x = 10`; - const { activeInteractiveWindow, untitledPythonFile } = await submitFromPythonFile( - interactiveWindowProvider, - source, - disposables - ); - await waitForLastCellToComplete(activeInteractiveWindow); - - // Add some more text - const editor = vscode.window.visibleTextEditors.find((e) => e.document.uri === untitledPythonFile.uri); - assert.ok(editor, `Couldn't find python file`); - await editor?.edit((b) => { - b.insert(new vscode.Position(8, 0), '\n# %%\nfoo()'); - }); - - let codeLenses = await waitForCodeLenses(untitledPythonFile.uri, Commands.DebugCell); - let stopped = false; - let stoppedOnLine = false; - let targetLine = 9; - debugAdapterTracker = { - onDidSendMessage: (message) => { - if (message.event == 'stopped') { - stopped = true; - } - if (message.command == 'stackTrace' && !stoppedOnLine) { - stoppedOnLine = message.body.stackFrames[0].line == targetLine; - } - } - }; - - assert.ok(codeLenses, `No code lenses found`); - assert.equal(codeLenses.length, 3, `Wrong number of code lenses found`); - let args = codeLenses[2].command!.arguments || []; - void vscode.commands.executeCommand(codeLenses[2].command!.command, ...args); - - // Wait for breakpoint to be hit - await waitForCondition( - async () => { - return vscode.debug.activeDebugSession != undefined && stopped; - }, - defaultNotebookTestTimeout, - `Never hit stop event when waiting for debug cell` - ); - - // Verify that we hit the correct line - await waitForCondition( - async () => { - return stoppedOnLine; - }, - defaultNotebookTestTimeout, - `Cursor did not move to expected line when hitting breakpoint` - ); - - // Perform a step into - stopped = false; - stoppedOnLine = false; - targetLine = 7; - - void vscode.commands.executeCommand('workbench.action.debug.stepInto'); - await waitForCondition( - async () => { - return stopped; - }, - defaultNotebookTestTimeout, - `Did not stop on step into` - ); - - // Verify that we hit the correct line - await waitForCondition( - async () => { - return stoppedOnLine; - }, - defaultNotebookTestTimeout, - `Cursor did not move to expected line when hitting stepping into` - ); - }); - - test('Step into a previous cell', async () => { - // Need a function and a call to the function - const source = ` -# %% -def foo(): - x = 10 - -# %% -foo() -`; - const { activeInteractiveWindow, untitledPythonFile } = await submitFromPythonFileUsingCodeWatcher( - source, - disposables - ); - await waitForLastCellToComplete(activeInteractiveWindow, 2); - - let codeLenses = await waitForCodeLenses(untitledPythonFile.uri, Commands.DebugCell); - let stopped = false; - let stoppedOnLine = false; - let targetLine = 7; - debugAdapterTracker = { - onDidSendMessage: (message) => { - if (message.event == 'stopped') { - stopped = true; - } - if (message.command == 'stackTrace' && !stoppedOnLine) { - stoppedOnLine = message.body.stackFrames[0].line == targetLine; - } - } - }; - - const debugCellCodeLenses = codeLenses.filter((c) => c.command?.command === Commands.DebugCell); - const debugCellCodeLens = debugCellCodeLenses[1]; - let args = debugCellCodeLens.command!.arguments || []; - void vscode.commands.executeCommand(debugCellCodeLens.command!.command, ...args); - - // Wait for breakpoint to be hit - await waitForCondition( - async () => { - return vscode.debug.activeDebugSession != undefined && stopped; - }, - defaultNotebookTestTimeout, - `Never hit stop event when waiting for debug cell` - ); - - // Verify that we hit the correct line - await waitForCondition( - async () => { - return stoppedOnLine; - }, - defaultNotebookTestTimeout, - `Cursor did not move to expected line when hitting breakpoint` - ); - - // Perform a step into - stopped = false; - stoppedOnLine = false; - targetLine = 4; - - void vscode.commands.executeCommand('workbench.action.debug.stepInto'); - await waitForCondition( - async () => { - return stopped; - }, - defaultNotebookTestTimeout, - `Did not stop on step into` - ); - - // Verify that we hit the correct line - await waitForCondition( - async () => { - return stoppedOnLine; - }, - defaultNotebookTestTimeout, - `Cursor did not move to expected line when hitting stepping into` - ); - }); + return; + } else if (enable && !settingFileContents.includes(`"jupyter.forceIPyKernelDebugger": true`)) { + throw new Error('Unable to update settings file'); + } else if (!enable && settingFileContents.includes(`"jupyter.forceIPyKernelDebugger": true`)) { + fs.writeFileSync( + settingsFile, + settingFileContents.replace( + `"jupyter.forceIPyKernelDebugger": true`, + `"jupyter.forceIPyKernelDebugger": false` + ) + ); + return; + } else if (!enable && settingFileContents.includes(`"jupyter.forceIPyKernelDebugger": false`)) { + return; + } else if (!enable && !settingFileContents.includes(`"jupyter.forceIPyKernelDebugger": true`)) { + throw new Error('Unable to update settings file'); + } + } + sharedIWDebuggerTests.bind(this)({ startJupyterServer, suiteSetup: enableJupyterDebugger, captureScreenShot }); }); diff --git a/src/test/datascience/interactiveWindow.vscode.test.ts b/src/test/datascience/interactiveWindow.vscode.test.ts index 2b0f7cf1454..b1dea1b6016 100644 --- a/src/test/datascience/interactiveWindow.vscode.test.ts +++ b/src/test/datascience/interactiveWindow.vscode.test.ts @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import * as path from '../../platform/vscode-path/path'; -import * as fs from 'fs-extra'; import { assert } from 'chai'; import * as sinon from 'sinon'; import * as vscode from 'vscode'; @@ -45,262 +43,204 @@ import { areInterpreterPathsSame } from '../../platform/pythonEnvironments/info/ import { IPythonApiProvider } from '../../platform/api/types'; import { isEqual } from '../../platform/vscode-path/resources'; import { PythonEnvironment } from '../../platform/pythonEnvironments/info'; -import { EXTENSION_ROOT_DIR } from '../../platform/constants.node'; - -type DebuggerType = 'VSCodePythonDebugger' | 'JupyterProtocolDebugger'; - -// See issue: https://github.com/microsoft/vscode-jupyter/issues/10258 -const debuggerTypes: DebuggerType[] = ['VSCodePythonDebugger']; -debuggerTypes.forEach((debuggerType) => { - suite(`Interactive window debugger using ${debuggerType}`, async function () { - this.timeout(120_000); - let api: IExtensionTestApi; - const disposables: IDisposable[] = []; - let interactiveWindowProvider: InteractiveWindowProvider; - let venNoKernelPath: vscode.Uri; - let venvKernelPath: vscode.Uri; - let pythonApiProvider: IPythonApiProvider; - let originalActiveInterpreter: PythonEnvironment | undefined; - const settingsFile = path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'datascience', '.vscode', 'settings.json'); - function enableJupyterDebugger(enable: boolean) { - const settingFileContents = fs.readFileSync(settingsFile).toString(); - if (enable && settingFileContents.includes(`"jupyter.forceIPyKernelDebugger": true`)) { - return; - } else if (enable && settingFileContents.includes(`"jupyter.forceIPyKernelDebugger": false`)) { - fs.writeFileSync( - settingsFile, - settingFileContents.replace( - `"jupyter.forceIPyKernelDebugger": false`, - `"jupyter.forceIPyKernelDebugger": true` - ) - ); - return; - } else if (enable && !settingFileContents.includes(`"jupyter.forceIPyKernelDebugger": true`)) { - throw new Error('Unable to update settings file'); - } else if (!enable && settingFileContents.includes(`"jupyter.forceIPyKernelDebugger": true`)) { - fs.writeFileSync( - settingsFile, - settingFileContents.replace( - `"jupyter.forceIPyKernelDebugger": true`, - `"jupyter.forceIPyKernelDebugger": false` - ) - ); - return; - } else if (!enable && settingFileContents.includes(`"jupyter.forceIPyKernelDebugger": false`)) { - return; - } else if (!enable && !settingFileContents.includes(`"jupyter.forceIPyKernelDebugger": true`)) { - throw new Error('Unable to update settings file'); - } + +suite(`Interactive window`, async function () { + this.timeout(120_000); + let api: IExtensionTestApi; + const disposables: IDisposable[] = []; + let interactiveWindowProvider: InteractiveWindowProvider; + let venNoKernelPath: vscode.Uri; + let venvKernelPath: vscode.Uri; + let pythonApiProvider: IPythonApiProvider; + let originalActiveInterpreter: PythonEnvironment | undefined; + setup(async function () { + traceInfo(`Start Test ${this.currentTest?.title}`); + api = await initialize(); + if (IS_REMOTE_NATIVE_TEST()) { + await startJupyterServer(); } - suiteSetup(function () { - if (IS_REMOTE_NATIVE_TEST() && debuggerType === 'VSCodePythonDebugger') { - return this.skip(); - } - enableJupyterDebugger(debuggerType === 'JupyterProtocolDebugger'); - }); - suiteTeardown(() => enableJupyterDebugger(false)); - setup(async function () { - if (IS_REMOTE_NATIVE_TEST() && debuggerType === 'VSCodePythonDebugger') { - return this.skip(); - } - traceInfo(`Start Test ${this.currentTest?.title}`); - api = await initialize(); - if (IS_REMOTE_NATIVE_TEST() && debuggerType === 'VSCodePythonDebugger') { - await startJupyterServer(); - } - interactiveWindowProvider = api.serviceManager.get(IInteractiveWindowProvider); - pythonApiProvider = api.serviceManager.get(IPythonApiProvider); - traceInfo(`Start Test (completed) ${this.currentTest?.title}`); - }); - teardown(async function () { - traceInfo(`Ended Test ${this.currentTest?.title}`); - if (this.currentTest?.isFailed()) { - // For a flaky interrupt test. - await captureScreenShot(`Interactive-Tests-${this.currentTest?.title}`); - } - sinon.restore(); - await closeNotebooksAndCleanUpAfterTests(disposables); - }); + interactiveWindowProvider = api.serviceManager.get(IInteractiveWindowProvider); + pythonApiProvider = api.serviceManager.get(IPythonApiProvider); + traceInfo(`Start Test (completed) ${this.currentTest?.title}`); + }); + teardown(async function () { + traceInfo(`Ended Test ${this.currentTest?.title}`); + if (this.currentTest?.isFailed()) { + // For a flaky interrupt test. + await captureScreenShot(`Interactive-Tests-${this.currentTest?.title}`); + } + sinon.restore(); + await closeNotebooksAndCleanUpAfterTests(disposables); + }); - test('Execute cell from Python file', async () => { - const source = 'print(42)'; - const { activeInteractiveWindow } = await submitFromPythonFile( - interactiveWindowProvider, - source, - disposables - ); - const notebookDocument = vscode.workspace.notebookDocuments.find( - (doc) => doc.uri.toString() === activeInteractiveWindow?.notebookUri?.toString() + test('Execute cell from Python file', async () => { + const source = 'print(42)'; + const { activeInteractiveWindow } = await submitFromPythonFile(interactiveWindowProvider, source, disposables); + const notebookDocument = vscode.workspace.notebookDocuments.find( + (doc) => doc.uri.toString() === activeInteractiveWindow?.notebookUri?.toString() + ); + const notebookControllerManager = + api.serviceManager.get(INotebookControllerManager); + + // Ensure we picked up the active interpreter for use as the kernel + const interpreterService = await api.serviceManager.get(IInterpreterService); + + // Give it a bit to warm up + await sleep(500); + + const controller = notebookDocument + ? notebookControllerManager.getSelectedNotebookController(notebookDocument) + : undefined; + if (!IS_REMOTE_NATIVE_TEST()) { + const activeInterpreter = await interpreterService.getActiveInterpreter(); + assert.ok( + areInterpreterPathsSame(controller?.connection.interpreter?.uri, activeInterpreter?.uri), + `Controller does not match active interpreter for ${getDisplayPath(notebookDocument?.uri)}` ); - const notebookControllerManager = - api.serviceManager.get(INotebookControllerManager); - - // Ensure we picked up the active interpreter for use as the kernel - const interpreterService = await api.serviceManager.get(IInterpreterService); - - // Give it a bit to warm up - await sleep(500); - - const controller = notebookDocument - ? notebookControllerManager.getSelectedNotebookController(notebookDocument) - : undefined; - if (!IS_REMOTE_NATIVE_TEST()) { - const activeInterpreter = await interpreterService.getActiveInterpreter(); - assert.ok( - areInterpreterPathsSame(controller?.connection.interpreter?.uri, activeInterpreter?.uri), - `Controller does not match active interpreter for ${getDisplayPath(notebookDocument?.uri)}` - ); - } + } + // Verify sys info cell + const firstCell = notebookDocument?.cellAt(0); + assert.ok(firstCell?.metadata.isInteractiveWindowMessageCell, 'First cell should be sys info cell'); + assert.equal(firstCell?.kind, vscode.NotebookCellKind.Markup, 'First cell should be markdown cell'); + + // Verify executed cell input and output + const secondCell = notebookDocument?.cellAt(1); + const actualSource = secondCell?.document.getText(); + assert.equal(actualSource, source, `Executed cell has unexpected source code`); + await waitForExecutionCompletedSuccessfully(secondCell!); + await waitForTextOutput(secondCell!, '42'); + }); + test('__file__ exists even after restarting a kernel', async function () { + // Ensure we click `Yes` when prompted to restart the kernel. + disposables.push(await clickOKForRestartPrompt()); + + const source = 'print(__file__)'; + const { activeInteractiveWindow, untitledPythonFile } = await submitFromPythonFile( + interactiveWindowProvider, + source, + disposables + ); + const notebookDocument = vscode.workspace.notebookDocuments.find( + (doc) => doc.uri.toString() === activeInteractiveWindow?.notebookUri?.toString() + )!; + const notebookControllerManager = + api.serviceManager.get(INotebookControllerManager); + // Ensure we picked up the active interpreter for use as the kernel + const interpreterService = await api.serviceManager.get(IInterpreterService); + + // Give it a bit to warm up + await sleep(500); + + const controller = notebookDocument + ? notebookControllerManager.getSelectedNotebookController(notebookDocument) + : undefined; + if (!IS_REMOTE_NATIVE_TEST()) { + const activeInterpreter = await interpreterService.getActiveInterpreter(); + assert.ok( + areInterpreterPathsSame(controller?.connection.interpreter?.uri, activeInterpreter?.uri), + `Controller does not match active interpreter for ${getDisplayPath(notebookDocument?.uri)}` + ); + } + async function verifyCells() { // Verify sys info cell - const firstCell = notebookDocument?.cellAt(0); + const firstCell = notebookDocument.cellAt(0); assert.ok(firstCell?.metadata.isInteractiveWindowMessageCell, 'First cell should be sys info cell'); assert.equal(firstCell?.kind, vscode.NotebookCellKind.Markup, 'First cell should be markdown cell'); // Verify executed cell input and output - const secondCell = notebookDocument?.cellAt(1); - const actualSource = secondCell?.document.getText(); + const secondCell = notebookDocument.cellAt(1); + const actualSource = secondCell.document.getText(); assert.equal(actualSource, source, `Executed cell has unexpected source code`); await waitForExecutionCompletedSuccessfully(secondCell!); - await waitForTextOutput(secondCell!, '42'); - }); - test('__file__ exists even after restarting a kernel', async function () { - // Ensure we click `Yes` when prompted to restart the kernel. - disposables.push(await clickOKForRestartPrompt()); - - const source = 'print(__file__)'; - const { activeInteractiveWindow, untitledPythonFile } = await submitFromPythonFile( - interactiveWindowProvider, - source, - disposables - ); - const notebookDocument = vscode.workspace.notebookDocuments.find( - (doc) => doc.uri.toString() === activeInteractiveWindow?.notebookUri?.toString() - )!; - const notebookControllerManager = - api.serviceManager.get(INotebookControllerManager); - // Ensure we picked up the active interpreter for use as the kernel - const interpreterService = await api.serviceManager.get(IInterpreterService); - - // Give it a bit to warm up - await sleep(500); - - const controller = notebookDocument - ? notebookControllerManager.getSelectedNotebookController(notebookDocument) - : undefined; - if (!IS_REMOTE_NATIVE_TEST()) { - const activeInterpreter = await interpreterService.getActiveInterpreter(); - assert.ok( - areInterpreterPathsSame(controller?.connection.interpreter?.uri, activeInterpreter?.uri), - `Controller does not match active interpreter for ${getDisplayPath(notebookDocument?.uri)}` - ); - } - async function verifyCells() { - // Verify sys info cell - const firstCell = notebookDocument.cellAt(0); - assert.ok(firstCell?.metadata.isInteractiveWindowMessageCell, 'First cell should be sys info cell'); - assert.equal(firstCell?.kind, vscode.NotebookCellKind.Markup, 'First cell should be markdown cell'); - - // Verify executed cell input and output - const secondCell = notebookDocument.cellAt(1); - const actualSource = secondCell.document.getText(); - assert.equal(actualSource, source, `Executed cell has unexpected source code`); - await waitForExecutionCompletedSuccessfully(secondCell!); - } - - await verifyCells(); - - // CLear all cells - await vscode.commands.executeCommand('jupyter.interactive.clearAllCells'); - await waitForCondition(async () => notebookDocument.cellCount === 0, 5_000, 'Cells not cleared'); - - // Restart kernel - const kernelProvider = api.serviceContainer.get(IKernelProvider); - const kernel = kernelProvider.get(notebookDocument.uri); - const handler = createEventHandler(kernel!, 'onRestarted', disposables); - await vscode.commands.executeCommand('jupyter.restartkernel'); - // Wait for restart to finish - await handler.assertFiredExactly(1, defaultNotebookTestTimeout); - await activeInteractiveWindow.addCode(source, untitledPythonFile.uri, 0); - await waitForCondition( - async () => notebookDocument.cellCount > 1, - defaultNotebookTestTimeout, - 'Code not executed' - ); - - await verifyCells(); - }); - test('Execute cell from input box', async () => { - // Create new interactive window - const activeInteractiveWindow = await createStandaloneInteractiveWindow(interactiveWindowProvider); - const notebook = await waitForInteractiveWindow(activeInteractiveWindow); - - // Add code to the input box - await insertIntoInputEditor('print("foo")'); - - // Run the code in the input box - await vscode.commands.executeCommand('interactive.execute'); - - assert.ok(notebook !== undefined, 'No interactive window found'); - await waitForCondition( - async () => { - return notebook.cellCount > 1; - }, - defaultNotebookTestTimeout, - 'Cell never added' - ); + } - // Inspect notebookDocument for output - const index = notebook!.cellCount - 1; - const cell = notebook!.cellAt(index); - await waitForTextOutput(cell, 'foo'); - }); + await verifyCells(); + + // CLear all cells + await vscode.commands.executeCommand('jupyter.interactive.clearAllCells'); + await waitForCondition(async () => notebookDocument.cellCount === 0, 5_000, 'Cells not cleared'); + + // Restart kernel + const kernelProvider = api.serviceContainer.get(IKernelProvider); + const kernel = kernelProvider.get(notebookDocument.uri); + const handler = createEventHandler(kernel!, 'onRestarted', disposables); + await vscode.commands.executeCommand('jupyter.restartkernel'); + // Wait for restart to finish + await handler.assertFiredExactly(1, defaultNotebookTestTimeout); + await activeInteractiveWindow.addCode(source, untitledPythonFile.uri, 0); + await waitForCondition( + async () => notebookDocument.cellCount > 1, + defaultNotebookTestTimeout, + 'Code not executed' + ); + + await verifyCells(); + }); + test('Execute cell from input box', async () => { + // Create new interactive window + const activeInteractiveWindow = await createStandaloneInteractiveWindow(interactiveWindowProvider); + const notebook = await waitForInteractiveWindow(activeInteractiveWindow); + + // Add code to the input box + await insertIntoInputEditor('print("foo")'); + + // Run the code in the input box + await vscode.commands.executeCommand('interactive.execute'); + + assert.ok(notebook !== undefined, 'No interactive window found'); + await waitForCondition( + async () => { + return notebook.cellCount > 1; + }, + defaultNotebookTestTimeout, + 'Cell never added' + ); + + // Inspect notebookDocument for output + const index = notebook!.cellCount - 1; + const cell = notebook!.cellAt(index); + await waitForTextOutput(cell, 'foo'); + }); - test('Clear output', async function () { - // Test failing after using python insiders. Not getting expected - // output - // https://github.com/microsoft/vscode-jupyter/issues/7580 - this.skip(); - const text = `from IPython.display import clear_output + test('Clear output', async function () { + // Test failing after using python insiders. Not getting expected + // output + // https://github.com/microsoft/vscode-jupyter/issues/7580 + this.skip(); + const text = `from IPython.display import clear_output for i in range(10): clear_output() print("Hello World {0}!".format(i)) `; - const { activeInteractiveWindow } = await submitFromPythonFile( - interactiveWindowProvider, - text, - disposables - ); - const cell = await waitForLastCellToComplete(activeInteractiveWindow); - await waitForTextOutput(cell!, 'Hello World 9!'); - }); - - test('Clear input box', async () => { - const text = '42'; - // Create interactive window with no owner - await createStandaloneInteractiveWindow(interactiveWindowProvider); - await insertIntoInputEditor(text); - - // Clear input and verify - assert.ok( - vscode.window.activeTextEditor?.document.getText() === text, - 'Text not inserted into input editor' - ); - await vscode.commands.executeCommand('interactive.input.clear'); - assert.ok(vscode.window.activeTextEditor?.document.getText() === '', 'Text not cleared from input editor'); - - // Undo - await vscode.commands.executeCommand('undo'); + const { activeInteractiveWindow } = await submitFromPythonFile(interactiveWindowProvider, text, disposables); + const cell = await waitForLastCellToComplete(activeInteractiveWindow); + await waitForTextOutput(cell!, 'Hello World 9!'); + }); - // Verify input box contents were restored - assert.ok( - vscode.window.activeTextEditor?.document.getText() === text, - 'Text not restored to input editor after undo' - ); - }); + test('Clear input box', async () => { + const text = '42'; + // Create interactive window with no owner + await createStandaloneInteractiveWindow(interactiveWindowProvider); + await insertIntoInputEditor(text); + + // Clear input and verify + assert.ok(vscode.window.activeTextEditor?.document.getText() === text, 'Text not inserted into input editor'); + await vscode.commands.executeCommand('interactive.input.clear'); + assert.ok(vscode.window.activeTextEditor?.document.getText() === '', 'Text not cleared from input editor'); + + // Undo + await vscode.commands.executeCommand('undo'); + + // Verify input box contents were restored + assert.ok( + vscode.window.activeTextEditor?.document.getText() === text, + 'Text not restored to input editor after undo' + ); + }); - test('LiveLossPlot', async () => { - const code = `from time import sleep + test('LiveLossPlot', async () => { + const code = `from time import sleep import numpy as np from livelossplot import PlotLosses @@ -315,94 +255,94 @@ for i in range(10): }) liveplot.draw() sleep(0.1)`; - const interactiveWindow = await createStandaloneInteractiveWindow(interactiveWindowProvider); - await insertIntoInputEditor(code); - await vscode.commands.executeCommand('interactive.execute'); - const codeCell = await waitForLastCellToComplete(interactiveWindow); - const output = codeCell?.outputs[0]; - assert.ok(output?.items[0].mime === 'image/png', 'No png output found'); - assert.ok( - output?.metadata?.outputType === 'display_data', - `Expected metadata.outputType to be 'display_data' but got ${output?.metadata?.outputType}` - ); - }); - - // Create 3 cells. Last cell should update the second - test('Update display data', async () => { - // Create cell 1 - const interactiveWindow = await createStandaloneInteractiveWindow(interactiveWindowProvider); - await insertIntoInputEditor('dh = display(display_id=True)'); - await vscode.commands.executeCommand('interactive.execute'); - - // Create cell 2 - await insertIntoInputEditor('dh.display("Hello")'); - await vscode.commands.executeCommand('interactive.execute'); - const secondCell = await waitForLastCellToComplete(interactiveWindow); - await waitForTextOutput(secondCell!, "'Hello'"); - - // Create cell 3 - await insertIntoInputEditor('dh.update("Goodbye")'); - await vscode.commands.executeCommand('interactive.execute'); - // Last cell output is empty - const thirdCell = await waitForLastCellToComplete(interactiveWindow); - assert.equal(thirdCell?.outputs.length, 0, 'Third cell should not have any outputs'); - // Second cell output is updated - await waitForTextOutput(secondCell!, "'Goodbye'"); - }); + const interactiveWindow = await createStandaloneInteractiveWindow(interactiveWindowProvider); + await insertIntoInputEditor(code); + await vscode.commands.executeCommand('interactive.execute'); + const codeCell = await waitForLastCellToComplete(interactiveWindow); + const output = codeCell?.outputs[0]; + assert.ok(output?.items[0].mime === 'image/png', 'No png output found'); + assert.ok( + output?.metadata?.outputType === 'display_data', + `Expected metadata.outputType to be 'display_data' but got ${output?.metadata?.outputType}` + ); + }); - test('Cells with errors cancel execution for others', async () => { - const source = - '# %%\nprint(1)\n# %%\nimport time\ntime.sleep(1)\nraise Exception("foo")\n# %%\nprint(2)\n# %%\nprint(3)'; - const { activeInteractiveWindow } = await submitFromPythonFileUsingCodeWatcher(source, disposables); - const notebookDocument = vscode.workspace.notebookDocuments.find( - (doc) => doc.uri.toString() === activeInteractiveWindow?.notebookUri?.toString() - ); + // Create 3 cells. Last cell should update the second + test('Update display data', async () => { + // Create cell 1 + const interactiveWindow = await createStandaloneInteractiveWindow(interactiveWindowProvider); + await insertIntoInputEditor('dh = display(display_id=True)'); + await vscode.commands.executeCommand('interactive.execute'); + + // Create cell 2 + await insertIntoInputEditor('dh.display("Hello")'); + await vscode.commands.executeCommand('interactive.execute'); + const secondCell = await waitForLastCellToComplete(interactiveWindow); + await waitForTextOutput(secondCell!, "'Hello'"); + + // Create cell 3 + await insertIntoInputEditor('dh.update("Goodbye")'); + await vscode.commands.executeCommand('interactive.execute'); + // Last cell output is empty + const thirdCell = await waitForLastCellToComplete(interactiveWindow); + assert.equal(thirdCell?.outputs.length, 0, 'Third cell should not have any outputs'); + // Second cell output is updated + await waitForTextOutput(secondCell!, "'Goodbye'"); + }); - await waitForCondition( - async () => { - return notebookDocument?.cellCount == 5; - }, - defaultNotebookTestTimeout, - `Cells should be added` - ); - const [, , secondCell, thirdCell, fourthCell] = notebookDocument!.getCells(); - // Other remaining cells will also fail with errors. - await Promise.all([ - waitForExecutionCompletedWithErrors(secondCell!), - waitForExecutionCompletedWithErrors(thirdCell!, undefined, false), - waitForExecutionCompletedWithErrors(fourthCell!, undefined, false) - ]); - }); + test('Cells with errors cancel execution for others', async () => { + const source = + '# %%\nprint(1)\n# %%\nimport time\ntime.sleep(1)\nraise Exception("foo")\n# %%\nprint(2)\n# %%\nprint(3)'; + const { activeInteractiveWindow } = await submitFromPythonFileUsingCodeWatcher(source, disposables); + const notebookDocument = vscode.workspace.notebookDocuments.find( + (doc) => doc.uri.toString() === activeInteractiveWindow?.notebookUri?.toString() + ); + + await waitForCondition( + async () => { + return notebookDocument?.cellCount == 5; + }, + defaultNotebookTestTimeout, + `Cells should be added` + ); + const [, , secondCell, thirdCell, fourthCell] = notebookDocument!.getCells(); + // Other remaining cells will also fail with errors. + await Promise.all([ + waitForExecutionCompletedWithErrors(secondCell!), + waitForExecutionCompletedWithErrors(thirdCell!, undefined, false), + waitForExecutionCompletedWithErrors(fourthCell!, undefined, false) + ]); + }); - test('Multiple interactive windows', async () => { - const settings = vscode.workspace.getConfiguration('jupyter', null); - await settings.update('interactiveWindowMode', 'multiple'); - const window1 = await interactiveWindowProvider.getOrCreate(undefined); - const window2 = await interactiveWindowProvider.getOrCreate(undefined); - assert.notEqual( - window1.notebookUri?.toString(), - window2.notebookUri?.toString(), - 'Two windows were not created in multiple mode' - ); - }); + test('Multiple interactive windows', async () => { + const settings = vscode.workspace.getConfiguration('jupyter', null); + await settings.update('interactiveWindowMode', 'multiple'); + const window1 = await interactiveWindowProvider.getOrCreate(undefined); + const window2 = await interactiveWindowProvider.getOrCreate(undefined); + assert.notEqual( + window1.notebookUri?.toString(), + window2.notebookUri?.toString(), + 'Two windows were not created in multiple mode' + ); + }); - test('Dispose test', async () => { - const interactiveWindow = await interactiveWindowProvider.getOrCreate(undefined); - await interactiveWindow.dispose(); - const interactiveWindow2 = await interactiveWindowProvider.getOrCreate(undefined); - assert.ok( - interactiveWindow.notebookUri?.toString() !== interactiveWindow2.notebookUri?.toString(), - 'Disposing is not removing the active interactive window' - ); - }); + test('Dispose test', async () => { + const interactiveWindow = await interactiveWindowProvider.getOrCreate(undefined); + await interactiveWindow.dispose(); + const interactiveWindow2 = await interactiveWindowProvider.getOrCreate(undefined); + assert.ok( + interactiveWindow.notebookUri?.toString() !== interactiveWindow2.notebookUri?.toString(), + 'Disposing is not removing the active interactive window' + ); + }); - test('Leading and trailing empty lines in #%% cell are trimmed', async () => { - const actualCode = ` print('foo') + test('Leading and trailing empty lines in #%% cell are trimmed', async () => { + const actualCode = ` print('foo') print('bar')`; - const codeWithWhitespace = ` # %% + const codeWithWhitespace = ` # %% @@ -412,255 +352,254 @@ ${actualCode} `; - traceInfoIfCI('Before submitting'); - const { activeInteractiveWindow: interactiveWindow } = await submitFromPythonFile( - interactiveWindowProvider, - codeWithWhitespace, - disposables - ); - traceInfoIfCI('After submitting'); - const lastCell = await waitForLastCellToComplete(interactiveWindow); - const actualCellText = lastCell.document.getText(); - assert.equal(actualCellText, actualCode); - }); + traceInfoIfCI('Before submitting'); + const { activeInteractiveWindow: interactiveWindow } = await submitFromPythonFile( + interactiveWindowProvider, + codeWithWhitespace, + disposables + ); + traceInfoIfCI('After submitting'); + const lastCell = await waitForLastCellToComplete(interactiveWindow); + const actualCellText = lastCell.document.getText(); + assert.equal(actualCellText, actualCode); + }); - test('Run current file in interactive window (with cells)', async () => { - const { activeInteractiveWindow } = await runNewPythonFile( - interactiveWindowProvider, - '#%%\na=1\nprint(a)\n#%%\nb=2\nprint(b)\n', - disposables - ); + test('Run current file in interactive window (with cells)', async () => { + const { activeInteractiveWindow } = await runNewPythonFile( + interactiveWindowProvider, + '#%%\na=1\nprint(a)\n#%%\nb=2\nprint(b)\n', + disposables + ); - await waitForLastCellToComplete(activeInteractiveWindow); + await waitForLastCellToComplete(activeInteractiveWindow); - const notebookDocument = vscode.workspace.notebookDocuments.find( - (doc) => doc.uri.toString() === activeInteractiveWindow?.notebookUri?.toString() - ); + const notebookDocument = vscode.workspace.notebookDocuments.find( + (doc) => doc.uri.toString() === activeInteractiveWindow?.notebookUri?.toString() + ); - // Should have two cells in the interactive window - assert.equal(notebookDocument?.cellCount, 3, `Running a whole file did not split cells`); + // Should have two cells in the interactive window + assert.equal(notebookDocument?.cellCount, 3, `Running a whole file did not split cells`); - // Make sure it output something - notebookDocument?.getCells().forEach((c, i) => { - if (c.document.uri.scheme === 'vscode-notebook-cell' && c.kind == vscode.NotebookCellKind.Code) { - assertHasTextOutputInVSCode(c, `${i}`); - } - }); + // Make sure it output something + notebookDocument?.getCells().forEach((c, i) => { + if (c.document.uri.scheme === 'vscode-notebook-cell' && c.kind == vscode.NotebookCellKind.Code) { + assertHasTextOutputInVSCode(c, `${i}`); + } }); + }); - test('Run current file in interactive window (without cells)', async () => { - const { activeInteractiveWindow } = await runNewPythonFile( - interactiveWindowProvider, - 'a=1\nprint(a)\nb=2\nprint(b)\n', - disposables - ); + test('Run current file in interactive window (without cells)', async () => { + const { activeInteractiveWindow } = await runNewPythonFile( + interactiveWindowProvider, + 'a=1\nprint(a)\nb=2\nprint(b)\n', + disposables + ); - await waitForLastCellToComplete(activeInteractiveWindow); + await waitForLastCellToComplete(activeInteractiveWindow); - const notebookDocument = vscode.workspace.notebookDocuments.find( - (doc) => doc.uri.toString() === activeInteractiveWindow?.notebookUri?.toString() - ); + const notebookDocument = vscode.workspace.notebookDocuments.find( + (doc) => doc.uri.toString() === activeInteractiveWindow?.notebookUri?.toString() + ); - // Should have two cells in the interactive window - assert.equal(notebookDocument?.cellCount, 2, `Running a file should use one cell`); + // Should have two cells in the interactive window + assert.equal(notebookDocument?.cellCount, 2, `Running a file should use one cell`); - // Wait for output to appear - await waitForTextOutput(notebookDocument!.cellAt(1), '1\n2'); - }); + // Wait for output to appear + await waitForTextOutput(notebookDocument!.cellAt(1), '1\n2'); + }); - test('Raising an exception from within a function has a stack trace', async function () { - const { activeInteractiveWindow } = await runNewPythonFile( - interactiveWindowProvider, - '# %%\ndef raiser():\n raise Exception("error")\n# %%\nraiser()', - disposables - ); - const lastCell = await waitForLastCellToComplete(activeInteractiveWindow, 2, true); + test('Raising an exception from within a function has a stack trace', async function () { + const { activeInteractiveWindow } = await runNewPythonFile( + interactiveWindowProvider, + '# %%\ndef raiser():\n raise Exception("error")\n# %%\nraiser()', + disposables + ); + const lastCell = await waitForLastCellToComplete(activeInteractiveWindow, 2, true); + + // Wait for the outputs to be available. + await waitForCondition( + async () => lastCell.outputs.length > 0 && lastCell.outputs[0].items.length > 0, + defaultNotebookTestTimeout, + 'Outputs not available' + ); + + // Parse the last cell's error output + const errorOutput = translateCellErrorOutput(lastCell.outputs[0]); + assert.ok(errorOutput, 'No error output found'); + assert.equal(errorOutput.traceback.length, 5, 'Traceback wrong size'); + + // Convert to html for easier parsing + const ansiToHtml = require('ansi-to-html') as typeof import('ansi-to-html'); + const converter = new ansiToHtml(); + const html = converter.toHtml(errorOutput.traceback.join('\n')); + + // Should be three hrefs for the two lines in the call stack + const hrefs = html.match(/ lastCell.outputs.length > 0 && lastCell.outputs[0].items.length > 0, - defaultNotebookTestTimeout, - 'Outputs not available' - ); + test('Raising an exception from system code has a stack trace', async function () { + const { activeInteractiveWindow } = await runNewPythonFile( + interactiveWindowProvider, + `# %%\n${IPYTHON_VERSION_CODE}# %%\nimport pathlib as pathlib\nx = pathlib.Path()\ny = None\nx.joinpath(y, "Foo")`, + disposables + ); + const lastCell = await waitForLastCellToComplete(activeInteractiveWindow, 2, true); + + // Wait for the outputs to be available. + await waitForCondition( + async () => lastCell.outputs.length > 0 && lastCell.outputs[0].items.length > 0, + defaultNotebookTestTimeout, + 'Outputs not available' + ); + + const ipythonVersionCell = activeInteractiveWindow.notebookDocument?.cellAt(lastCell.index - 1); + const ipythonVersion = parseInt(getTextOutputValue(ipythonVersionCell!.outputs[0])); + + // Parse the last cell's error output + const errorOutput = translateCellErrorOutput(lastCell.outputs[0]); + assert.ok(errorOutput, 'No error output found'); + + // Convert to html for easier parsing + const ansiToHtml = require('ansi-to-html') as typeof import('ansi-to-html'); + const converter = new ansiToHtml(); + const html = converter.toHtml(errorOutput.traceback.join('\n')); + + // Should be more than 3 hrefs if ipython 8 or not + const hrefs = html.match(/= 8) { + assert.isAtLeast(hrefs?.length, 4, 'Wrong number of hrefs found in traceback for IPython 8'); + } else { + assert.isAtLeast(hrefs?.length, 1, 'Wrong number of hrefs found in traceback for IPython 7 or earlier'); + } + }); - // Parse the last cell's error output - const errorOutput = translateCellErrorOutput(lastCell.outputs[0]); - assert.ok(errorOutput, 'No error output found'); - assert.equal(errorOutput.traceback.length, 5, 'Traceback wrong size'); - - // Convert to html for easier parsing - const ansiToHtml = require('ansi-to-html') as typeof import('ansi-to-html'); - const converter = new ansiToHtml(); - const html = converter.toHtml(errorOutput.traceback.join('\n')); - - // Should be three hrefs for the two lines in the call stack - const hrefs = html.match(/ { + const { activeInteractiveWindow } = await runNewPythonFile( + interactiveWindowProvider, + '# %% [markdown]\n# # HEADER\n# **bold**\nprint(1)', + disposables + ); + const lastCell = await waitForLastCellToComplete(activeInteractiveWindow, 1, true); + + // Wait for the outputs to be available. + await waitForCondition( + async () => lastCell.outputs.length > 0 && lastCell.outputs[0].items.length > 0, + defaultNotebookTestTimeout, + 'Outputs not available' + ); + + // Parse the last cell's output + await waitForTextOutput(lastCell, '1'); + }); + + async function preSwitch() { + const pythonApi = await pythonApiProvider.getApi(); + await pythonApi.refreshInterpreters({ clearCache: true }); + const interpreterService = api.serviceContainer.get(IInterpreterService); + const interpreters = await interpreterService.getInterpreters(); + const venvNoKernelInterpreter = interpreters.find((i) => getFilePath(i.uri).includes('.venvnokernel')); + const venvKernelInterpreter = interpreters.find((i) => getFilePath(i.uri).includes('.venvkernel')); + + if (!venvNoKernelInterpreter || !venvKernelInterpreter) { + throw new Error( + `Unable to find matching kernels. List of kernels is ${interpreters + .map((i) => getFilePath(i.uri)) + .join('\n')}` + ); + } + venNoKernelPath = venvNoKernelInterpreter.uri; + venvKernelPath = venvKernelInterpreter.uri; + originalActiveInterpreter = await interpreterService.getActiveInterpreter(); + + // No kernel should not have ipykernel in it yet, but we need two, so install it. + await installIPyKernel(venNoKernelPath.fsPath); + assert.ok(originalActiveInterpreter, `No active interpreter when running switch test`); + } + async function postSwitch() { + await uninstallIPyKernel(venNoKernelPath.fsPath); + await setActiveInterpreter(pythonApiProvider, undefined, originalActiveInterpreter?.uri); + } + test('Switching active interpreter on a python file changes kernel in use', async function () { + // Virtual environments are not available in conda + if (IS_CONDA_TEST() || IS_REMOTE_NATIVE_TEST()) { + this.skip(); + } + await preSwitch(); - test('Raising an exception from system code has a stack trace', async function () { - const { activeInteractiveWindow } = await runNewPythonFile( + try { + const interpreterService = await api.serviceManager.get(IInterpreterService); + const activeInterpreter = await interpreterService.getActiveInterpreter(); + const { activeInteractiveWindow, untitledPythonFile } = await runNewPythonFile( interactiveWindowProvider, - `# %%\n${IPYTHON_VERSION_CODE}# %%\nimport pathlib as pathlib\nx = pathlib.Path()\ny = None\nx.joinpath(y, "Foo")`, + 'import sys\nprint(sys.executable)', disposables ); - const lastCell = await waitForLastCellToComplete(activeInteractiveWindow, 2, true); + await waitForLastCellToComplete(activeInteractiveWindow, 1, true); + let notebookDocument = vscode.workspace.notebookDocuments.find( + (doc) => doc.uri.toString() === activeInteractiveWindow?.notebookUri?.toString() + )!; + const notebookControllerManager = + api.serviceManager.get(INotebookControllerManager); + // Ensure we picked up the active interpreter for use as the kernel - // Wait for the outputs to be available. - await waitForCondition( - async () => lastCell.outputs.length > 0 && lastCell.outputs[0].items.length > 0, - defaultNotebookTestTimeout, - 'Outputs not available' + let controller = notebookDocument + ? notebookControllerManager.getSelectedNotebookController(notebookDocument) + : undefined; + assert.ok( + areInterpreterPathsSame(controller?.connection.interpreter?.uri, activeInterpreter?.uri), + `Controller does not match active interpreter for ${getDisplayPath(notebookDocument?.uri)} - active: ${ + activeInterpreter?.uri + } controller: ${controller?.connection.interpreter?.uri}` ); - const ipythonVersionCell = activeInteractiveWindow.notebookDocument?.cellAt(lastCell.index - 1); - const ipythonVersion = parseInt(getTextOutputValue(ipythonVersionCell!.outputs[0])); - - // Parse the last cell's error output - const errorOutput = translateCellErrorOutput(lastCell.outputs[0]); - assert.ok(errorOutput, 'No error output found'); - - // Convert to html for easier parsing - const ansiToHtml = require('ansi-to-html') as typeof import('ansi-to-html'); - const converter = new ansiToHtml(); - const html = converter.toHtml(errorOutput.traceback.join('\n')); - - // Should be more than 3 hrefs if ipython 8 or not - const hrefs = html.match(/= 8) { - assert.isAtLeast(hrefs?.length, 4, 'Wrong number of hrefs found in traceback for IPython 8'); + // Now switch the active interpreter to the other path + if (isEqual(activeInterpreter?.uri, venNoKernelPath)) { + await setActiveInterpreter(pythonApiProvider, untitledPythonFile.uri, venvKernelPath); } else { - assert.isAtLeast(hrefs?.length, 1, 'Wrong number of hrefs found in traceback for IPython 7 or earlier'); + await setActiveInterpreter(pythonApiProvider, untitledPythonFile.uri, venNoKernelPath); } - }); - test('Running a cell with markdown and code runs two cells', async () => { - const { activeInteractiveWindow } = await runNewPythonFile( - interactiveWindowProvider, - '# %% [markdown]\n# # HEADER\n# **bold**\nprint(1)', - disposables - ); - const lastCell = await waitForLastCellToComplete(activeInteractiveWindow, 1, true); + // Close the interactive window and recreate it + await closeInteractiveWindow(activeInteractiveWindow); - // Wait for the outputs to be available. - await waitForCondition( - async () => lastCell.outputs.length > 0 && lastCell.outputs[0].items.length > 0, - defaultNotebookTestTimeout, - 'Outputs not available' - ); + // Run again and make sure it uses the new interpreter + const newIW = await runCurrentFile(interactiveWindowProvider, untitledPythonFile); + await waitForLastCellToComplete(newIW, 1, true); - // Parse the last cell's output - await waitForTextOutput(lastCell, '1'); - }); + // Make sure it's a new window + assert.notEqual(newIW, activeInteractiveWindow, `New IW was not created`); - async function preSwitch() { - const pythonApi = await pythonApiProvider.getApi(); - await pythonApi.refreshInterpreters({ clearCache: true }); - const interpreterService = api.serviceContainer.get(IInterpreterService); - const interpreters = await interpreterService.getInterpreters(); - const venvNoKernelInterpreter = interpreters.find((i) => getFilePath(i.uri).includes('.venvnokernel')); - const venvKernelInterpreter = interpreters.find((i) => getFilePath(i.uri).includes('.venvkernel')); - - if (!venvNoKernelInterpreter || !venvKernelInterpreter) { - throw new Error( - `Unable to find matching kernels. List of kernels is ${interpreters - .map((i) => getFilePath(i.uri)) - .join('\n')}` - ); - } - venNoKernelPath = venvNoKernelInterpreter.uri; - venvKernelPath = venvKernelInterpreter.uri; - originalActiveInterpreter = await interpreterService.getActiveInterpreter(); + // Get the controller + notebookDocument = vscode.workspace.notebookDocuments.find( + (doc) => doc.uri.toString() === newIW?.notebookUri?.toString() + )!; + controller = notebookDocument + ? notebookControllerManager.getSelectedNotebookController(notebookDocument) + : undefined; - // No kernel should not have ipykernel in it yet, but we need two, so install it. - await installIPyKernel(venNoKernelPath.fsPath); - assert.ok(originalActiveInterpreter, `No active interpreter when running switch test`); - } - async function postSwitch() { - await uninstallIPyKernel(venNoKernelPath.fsPath); - await setActiveInterpreter(pythonApiProvider, undefined, originalActiveInterpreter?.uri); + // Controller path should not be the same as the old active interpreter + assert.isFalse( + areInterpreterPathsSame(controller?.connection.interpreter?.uri, activeInterpreter?.uri), + `Controller should not match active interpreter for ${getDisplayPath( + notebookDocument?.uri + )} after changing active interpreter` + ); + } finally { + await postSwitch(); } - test('Switching active interpreter on a python file changes kernel in use', async function () { - // Virtual environments are not available in conda - if (IS_CONDA_TEST() || IS_REMOTE_NATIVE_TEST()) { - this.skip(); - } - await preSwitch(); - - try { - const interpreterService = await api.serviceManager.get(IInterpreterService); - const activeInterpreter = await interpreterService.getActiveInterpreter(); - const { activeInteractiveWindow, untitledPythonFile } = await runNewPythonFile( - interactiveWindowProvider, - 'import sys\nprint(sys.executable)', - disposables - ); - await waitForLastCellToComplete(activeInteractiveWindow, 1, true); - let notebookDocument = vscode.workspace.notebookDocuments.find( - (doc) => doc.uri.toString() === activeInteractiveWindow?.notebookUri?.toString() - )!; - const notebookControllerManager = - api.serviceManager.get(INotebookControllerManager); - // Ensure we picked up the active interpreter for use as the kernel - - let controller = notebookDocument - ? notebookControllerManager.getSelectedNotebookController(notebookDocument) - : undefined; - assert.ok( - areInterpreterPathsSame(controller?.connection.interpreter?.uri, activeInterpreter?.uri), - `Controller does not match active interpreter for ${getDisplayPath( - notebookDocument?.uri - )} - active: ${activeInterpreter?.uri} controller: ${controller?.connection.interpreter?.uri}` - ); - - // Now switch the active interpreter to the other path - if (isEqual(activeInterpreter?.uri, venNoKernelPath)) { - await setActiveInterpreter(pythonApiProvider, untitledPythonFile.uri, venvKernelPath); - } else { - await setActiveInterpreter(pythonApiProvider, untitledPythonFile.uri, venNoKernelPath); - } - - // Close the interactive window and recreate it - await closeInteractiveWindow(activeInteractiveWindow); - - // Run again and make sure it uses the new interpreter - const newIW = await runCurrentFile(interactiveWindowProvider, untitledPythonFile); - await waitForLastCellToComplete(newIW, 1, true); - - // Make sure it's a new window - assert.notEqual(newIW, activeInteractiveWindow, `New IW was not created`); - - // Get the controller - notebookDocument = vscode.workspace.notebookDocuments.find( - (doc) => doc.uri.toString() === newIW?.notebookUri?.toString() - )!; - controller = notebookDocument - ? notebookControllerManager.getSelectedNotebookController(notebookDocument) - : undefined; - - // Controller path should not be the same as the old active interpreter - assert.isFalse( - areInterpreterPathsSame(controller?.connection.interpreter?.uri, activeInterpreter?.uri), - `Controller should not match active interpreter for ${getDisplayPath( - notebookDocument?.uri - )} after changing active interpreter` - ); - } finally { - await postSwitch(); - } - }); - - // todo@joyceerhl - // test('Verify CWD', () => { }); - // test('Multiple executes go to last active window', async () => { }); - // test('Per file', async () => { }); - // test('Per file asks and changes titles', async () => { }); - // test('Debug cell with leading newlines', () => { }); - // test('Debug cell with multiple function definitions', () => { }); - // test('Should skip empty cells from #%% file or input box', () => { }); - // test('Export', () => { }); }); + + // todo@joyceerhl + // test('Verify CWD', () => { }); + // test('Multiple executes go to last active window', async () => { }); + // test('Per file', async () => { }); + // test('Per file asks and changes titles', async () => { }); + // test('Debug cell with leading newlines', () => { }); + // test('Debug cell with multiple function definitions', () => { }); + // test('Should skip empty cells from #%% file or input box', () => { }); + // test('Export', () => { }); });