diff --git a/news/3 Code Health/11660.md b/news/3 Code Health/11660.md new file mode 100644 index 000000000000..649eac927647 --- /dev/null +++ b/news/3 Code Health/11660.md @@ -0,0 +1 @@ +Functional test for run by line functionality \ No newline at end of file diff --git a/src/client/datascience/interactive-common/interactiveWindowTypes.ts b/src/client/datascience/interactive-common/interactiveWindowTypes.ts index 1fb5662ae433..6bb666e57ef4 100644 --- a/src/client/datascience/interactive-common/interactiveWindowTypes.ts +++ b/src/client/datascience/interactive-common/interactiveWindowTypes.ts @@ -127,7 +127,8 @@ export enum InteractiveWindowMessages { Step = 'step', Continue = 'continue', ShowContinue = 'show_continue', - ShowBreak = 'show_break' + ShowBreak = 'show_break', + ShowingIp = 'showing_ip' } export enum IPyWidgetMessages { @@ -607,4 +608,5 @@ export class IInteractiveWindowMapping { public [InteractiveWindowMessages.ShowBreak]: { frames: DebugProtocol.StackFrame[]; cell: ICell }; public [InteractiveWindowMessages.ShowContinue]: ICell; public [InteractiveWindowMessages.Step]: never | undefined; + public [InteractiveWindowMessages.ShowingIp]: never | undefined; } diff --git a/src/client/datascience/interactive-common/synchronization.ts b/src/client/datascience/interactive-common/synchronization.ts index 6a09a1601a13..ebf65a82f9dc 100644 --- a/src/client/datascience/interactive-common/synchronization.ts +++ b/src/client/datascience/interactive-common/synchronization.ts @@ -169,6 +169,7 @@ const messageWithMessageTypes: MessageMapping & Messa [InteractiveWindowMessages.SendInfo]: MessageType.other, [InteractiveWindowMessages.SettingsUpdated]: MessageType.other, [InteractiveWindowMessages.ShowBreak]: MessageType.other, + [InteractiveWindowMessages.ShowingIp]: MessageType.other, [InteractiveWindowMessages.ShowContinue]: MessageType.other, [InteractiveWindowMessages.ShowDataViewer]: MessageType.other, [InteractiveWindowMessages.ShowPlot]: MessageType.other, diff --git a/src/datascience-ui/interactive-common/redux/store.ts b/src/datascience-ui/interactive-common/redux/store.ts index 3ca5476e7ca6..044a17b3f384 100644 --- a/src/datascience-ui/interactive-common/redux/store.ts +++ b/src/datascience-ui/interactive-common/redux/store.ts @@ -204,6 +204,13 @@ function createTestMiddleware(): Redux.Middleware<{}, IStore> { sendMessage(InteractiveWindowMessages.ExecutionRendered, { ids: diff }); } + // Entering break state in a native cell + const prevBreak = prevState.main.cellVMs.find((cvm) => cvm.currentStack); + const newBreak = afterState.main.cellVMs.find((cvm) => cvm.currentStack); + if (prevBreak !== newBreak || !fastDeepEqual(prevBreak?.currentStack, newBreak?.currentStack)) { + sendMessage(InteractiveWindowMessages.ShowingIp); + } + if (action.type !== 'action.postOutgoingMessage') { sendMessage(`DISPATCHED_ACTION_${action.type}`, {}); } diff --git a/src/test/datascience/dataScienceIocContainer.ts b/src/test/datascience/dataScienceIocContainer.ts index af1458afdab5..065097dd165c 100644 --- a/src/test/datascience/dataScienceIocContainer.ts +++ b/src/test/datascience/dataScienceIocContainer.ts @@ -194,6 +194,7 @@ import { AutoSaveService } from '../../client/datascience/interactive-ipynb/auto import { NativeEditor } from '../../client/datascience/interactive-ipynb/nativeEditor'; import { NativeEditorCommandListener } from '../../client/datascience/interactive-ipynb/nativeEditorCommandListener'; import { NativeEditorOldWebView } from '../../client/datascience/interactive-ipynb/nativeEditorOldWebView'; +import { NativeEditorRunByLineListener } from '../../client/datascience/interactive-ipynb/nativeEditorRunByLineListener'; import { NativeEditorStorage } from '../../client/datascience/interactive-ipynb/nativeEditorStorage'; import { NativeEditorSynchronizer } from '../../client/datascience/interactive-ipynb/nativeEditorSynchronizer'; import { InteractiveWindow } from '../../client/datascience/interactive-window/interactiveWindow'; @@ -763,6 +764,7 @@ export class DataScienceIocContainer extends UnitTestIocContainer { this.serviceManager.add(IInteractiveWindowListener, IntellisenseProvider); this.serviceManager.add(IInteractiveWindowListener, AutoSaveService); this.serviceManager.add(IInteractiveWindowListener, GatherListener); + this.serviceManager.add(IInteractiveWindowListener, NativeEditorRunByLineListener); this.serviceManager.addSingleton( IPyWidgetMessageDispatcherFactory, IPyWidgetMessageDispatcherFactory diff --git a/src/test/datascience/debugger.functional.test.tsx b/src/test/datascience/debugger.functional.test.tsx index e49a62c9d936..3826e4f9eed6 100644 --- a/src/test/datascience/debugger.functional.test.tsx +++ b/src/test/datascience/debugger.functional.test.tsx @@ -7,7 +7,6 @@ import * as TypeMoq from 'typemoq'; import * as uuid from 'uuid/v4'; import { CodeLens, Disposable, Position, Range, SourceBreakpoint, Uri } from 'vscode'; import { CancellationToken } from 'vscode-jsonrpc'; -import * as vsls from 'vsls/vscode'; import { IApplicationShell, IDocumentManager } from '../../client/common/application/types'; import { RunByLine } from '../../client/common/experimentGroups'; @@ -23,11 +22,19 @@ import { IJupyterDebugService, IJupyterExecution } from '../../client/datascience/types'; +import { ImageButton } from '../../datascience-ui/react-common/imageButton'; import { DataScienceIocContainer } from './dataScienceIocContainer'; import { getInteractiveCellResults, getOrCreateInteractiveWindow } from './interactiveWindowTestHelpers'; import { MockDocument } from './mockDocument'; import { MockDocumentManager } from './mockDocumentManager'; -import { mountConnectedMainPanel, openVariableExplorer, waitForMessage } from './testHelpers'; +import { addCell, createNewEditor } from './nativeEditorTestHelpers'; +import { + getLastOutputCell, + openVariableExplorer, + runInteractiveTest, + runNativeTest, + waitForMessage +} from './testHelpers'; import { verifyVariables } from './variableTestHelpers'; //import { asyncDump } from '../common/asyncDump'; @@ -52,13 +59,45 @@ suite('DataScience Debugger tests', () => { }); setup(async () => { - ioc = createContainer(); + ioc = new DataScienceIocContainer(); + }); + + async function createIOC() { + ioc.registerDataScienceTypes(); jupyterDebuggerService = ioc.serviceManager.get( IJupyterDebugService, Identifiers.MULTIPLEXING_DEBUGSERVICE ); - return ioc.activate(); - }); + // Rebind the appshell so we can change what happens on an error + const dummyDisposable = { + dispose: () => { + return; + } + }; + const appShell = TypeMoq.Mock.ofType(); + appShell.setup((a) => a.showErrorMessage(TypeMoq.It.isAnyString())).returns((e) => (lastErrorMessage = e)); + appShell + .setup((a) => a.showInformationMessage(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve('')); + appShell + .setup((a) => a.showInformationMessage(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns((_a1: string, a2: string, _a3: string) => Promise.resolve(a2)); + appShell + .setup((a) => a.showSaveDialog(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(Uri.file('test.ipynb'))); + appShell.setup((a) => a.setStatusBarMessage(TypeMoq.It.isAny())).returns(() => dummyDisposable); + + ioc.serviceManager.rebindInstance(IApplicationShell, appShell.object); + + // Make sure the history provider and execution factory in the container is created (the extension does this on startup in the extension) + // This is necessary to get the appropriate live share services up and running. + ioc.get(IInteractiveWindowProvider); + ioc.get(IJupyterExecution); + ioc.get(IDebugLocationTracker); + + await ioc.activate(); + return ioc; + } teardown(async () => { for (const disposable of disposables) { @@ -89,42 +128,6 @@ suite('DataScience Debugger tests', () => { // asyncDump(); }); - function createContainer(): DataScienceIocContainer { - const result = new DataScienceIocContainer(); - result.registerDataScienceTypes(); - - // Rebind the appshell so we can change what happens on an error - const dummyDisposable = { - dispose: () => { - return; - } - }; - const appShell = TypeMoq.Mock.ofType(); - appShell.setup((a) => a.showErrorMessage(TypeMoq.It.isAnyString())).returns((e) => (lastErrorMessage = e)); - appShell - .setup((a) => a.showInformationMessage(TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns(() => Promise.resolve('')); - appShell - .setup((a) => a.showInformationMessage(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns((_a1: string, a2: string, _a3: string) => Promise.resolve(a2)); - appShell - .setup((a) => a.showSaveDialog(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(Uri.file('test.ipynb'))); - appShell.setup((a) => a.setStatusBarMessage(TypeMoq.It.isAny())).returns(() => dummyDisposable); - - result.serviceManager.rebindInstance(IApplicationShell, appShell.object); - - // Setup our webview panel - result.createWebView(() => mountConnectedMainPanel('interactive'), vsls.Role.None); - - // Make sure the history provider and execution factory in the container is created (the extension does this on startup in the extension) - // This is necessary to get the appropriate live share services up and running. - result.get(IInteractiveWindowProvider); - result.get(IJupyterExecution); - result.get(IDebugLocationTracker); - return result; - } - async function debugCell( code: string, breakpoint?: Range, @@ -238,63 +241,108 @@ suite('DataScience Debugger tests', () => { return []; } - test('Debug cell without breakpoint', async () => { - await debugCell('#%%\nprint("bar")'); - }); - test('Check variables', async () => { - ioc.setExperimentState(RunByLine.experiment, true); - await debugCell('#%%\nx = [4, 6]\nx = 5', undefined, undefined, false, () => { - const targetResult = { - name: 'x', - value: '[4, 6]', - supportsDataExplorer: true, - type: 'list', - size: 0, - shape: '', - count: 2, - truncated: false - }; - verifyVariables(ioc!.wrapper!, [targetResult]); - }); - }); - - test('Debug temporary file', async () => { - const code = '#%%\nprint("bar")'; - - // Create a dummy document with just this code - const docManager = ioc.get(IDocumentManager) as MockDocumentManager; - const fileName = 'Untitled-1'; - docManager.addDocument(code, fileName); - const mockDoc = docManager.textDocuments[0] as MockDocument; - mockDoc.forceUntitled(); - - // Start the jupyter server - const history = await getOrCreateInteractiveWindow(ioc); - const expectedBreakLine = 2; // 2 because of the 'breakpoint()' that gets added - - // Debug this code. We should either hit the breakpoint or stop on entry - const resultPromise = getInteractiveCellResults(ioc, ioc.wrapper!, async () => { - const breakPromise = createDeferred(); - disposables.push(jupyterDebuggerService!.onBreakpointHit(() => breakPromise.resolve())); - const targetUri = Uri.file(fileName); - const done = history.debugCode(code, targetUri.fsPath, 0, docManager.activeTextEditor); - await waitForPromise(Promise.race([done, breakPromise.promise]), 60000); - assert.ok(breakPromise.resolved, 'Breakpoint event did not fire'); - assert.ok(!lastErrorMessage, `Error occurred ${lastErrorMessage}`); - const stackFrames = await jupyterDebuggerService!.getStack(); - assert.ok(stackFrames, 'Stack trace not computable'); - assert.ok(stackFrames.length >= 1, 'Not enough frames'); - assert.equal(stackFrames[0].line, expectedBreakLine, 'Stopped on wrong line number'); - assert.ok( - stackFrames[0].source!.path!.includes('baz.py'), - 'Stopped on wrong file name. Name should have been saved' - ); - // Verify break location - await jupyterDebuggerService!.continue(); - }); + runInteractiveTest( + 'Debug cell without breakpoint', + async () => { + await debugCell('#%%\nprint("bar")'); + }, + createIOC + ); + runInteractiveTest( + 'Check variables', + async () => { + ioc.setExperimentState(RunByLine.experiment, true); + await debugCell('#%%\nx = [4, 6]\nx = 5', undefined, undefined, false, () => { + const targetResult = { + name: 'x', + value: '[4, 6]', + supportsDataExplorer: true, + type: 'list', + size: 0, + shape: '', + count: 2, + truncated: false + }; + verifyVariables(ioc!.wrapper!, [targetResult]); + }); + }, + createIOC + ); + + runInteractiveTest( + 'Debug temporary file', + async () => { + const code = '#%%\nprint("bar")'; + + // Create a dummy document with just this code + const docManager = ioc.get(IDocumentManager) as MockDocumentManager; + const fileName = 'Untitled-1'; + docManager.addDocument(code, fileName); + const mockDoc = docManager.textDocuments[0] as MockDocument; + mockDoc.forceUntitled(); + + // Start the jupyter server + const history = await getOrCreateInteractiveWindow(ioc); + const expectedBreakLine = 2; // 2 because of the 'breakpoint()' that gets added + + // Debug this code. We should either hit the breakpoint or stop on entry + const resultPromise = getInteractiveCellResults(ioc, ioc.wrapper!, async () => { + const breakPromise = createDeferred(); + disposables.push(jupyterDebuggerService!.onBreakpointHit(() => breakPromise.resolve())); + const targetUri = Uri.file(fileName); + const done = history.debugCode(code, targetUri.fsPath, 0, docManager.activeTextEditor); + await waitForPromise(Promise.race([done, breakPromise.promise]), 60000); + assert.ok(breakPromise.resolved, 'Breakpoint event did not fire'); + assert.ok(!lastErrorMessage, `Error occurred ${lastErrorMessage}`); + const stackFrames = await jupyterDebuggerService!.getStack(); + assert.ok(stackFrames, 'Stack trace not computable'); + assert.ok(stackFrames.length >= 1, 'Not enough frames'); + assert.equal(stackFrames[0].line, expectedBreakLine, 'Stopped on wrong line number'); + assert.ok( + stackFrames[0].source!.path!.includes('baz.py'), + 'Stopped on wrong file name. Name should have been saved' + ); + // Verify break location + await jupyterDebuggerService!.continue(); + }); - const cellResults = await resultPromise; - assert.ok(cellResults, 'No cell results after finishing debugging'); - await history.dispose(); - }); + const cellResults = await resultPromise; + assert.ok(cellResults, 'No cell results after finishing debugging'); + await history.dispose(); + }, + createIOC + ); + + runNativeTest( + 'Run by line', + async () => { + // Create an editor so something is listening to messages + await createNewEditor(ioc); + const wrapper = ioc.wrapper!; + + // Add a cell into the UI and wait for it to render and submit it. + await addCell(wrapper, ioc, 'a=1\na', true); + + // Step into this cell using the button + let cell = getLastOutputCell(wrapper, 'NativeCell'); + let ImageButtons = cell.find(ImageButton); + assert.equal(ImageButtons.length, 7, 'Cell buttons not found'); + const runByLineButton = ImageButtons.at(3); + // tslint:disable-next-line: no-any + assert.equal((runByLineButton.instance().props as any).tooltip, 'Run by line'); + + const promise = waitForMessage(ioc, InteractiveWindowMessages.ShowingIp); + runByLineButton.simulate('click'); + await promise; + + // We should be in the break state. See if buttons indicate that or not + cell = getLastOutputCell(wrapper, 'NativeCell'); + ImageButtons = cell.find(ImageButton); + assert.equal(ImageButtons.length, 4, 'Cell buttons wrong number'); + }, + () => { + ioc.setExperimentState(RunByLine.experiment, true); + return createIOC(); + } + ); }); diff --git a/src/test/datascience/testHelpers.tsx b/src/test/datascience/testHelpers.tsx index f5975473b6fc..2fcfaded5c6f 100644 --- a/src/test/datascience/testHelpers.tsx +++ b/src/test/datascience/testHelpers.tsx @@ -136,9 +136,9 @@ async function testInnerLoop( type: 'native' | 'interactive', wrapper: ReactWrapper, React.Component> ) => Promise, - getIOC: () => DataScienceIocContainer + getIOC: () => Promise ) { - const ioc = getIOC(); + const ioc = await getIOC(); const jupyterExecution = ioc.get(IJupyterExecution); if (await jupyterExecution.isNotebookSupported()) { addMockData(ioc, 'a=1\na', 1); @@ -156,7 +156,7 @@ export function runDoubleTest( type: 'native' | 'interactive', wrapper: ReactWrapper, React.Component> ) => Promise, - getIOC: () => DataScienceIocContainer + getIOC: () => Promise ) { // Just run the test twice. Originally mounted twice, but too hard trying to figure out disposing. test(`${name} (interactive)`, async () => @@ -168,7 +168,7 @@ export function runDoubleTest( export function runInteractiveTest( name: string, testFunc: (wrapper: ReactWrapper, React.Component>) => Promise, - getIOC: () => DataScienceIocContainer + getIOC: () => Promise ) { // Run the test with just the interactive window test(`${name} (interactive)`, async () => @@ -180,6 +180,21 @@ export function runInteractiveTest( getIOC )); } +export function runNativeTest( + name: string, + testFunc: (wrapper: ReactWrapper, React.Component>) => Promise, + getIOC: () => Promise +) { + // Run the test with just the native window + test(`${name} (native)`, async () => + testInnerLoop( + name, + 'native', + (ioc) => mountWebView(ioc, 'native'), + (_t, w) => testFunc(w), + getIOC + )); +} export function mountWebView( ioc: DataScienceIocContainer, diff --git a/src/test/datascience/variableexplorer.functional.test.tsx b/src/test/datascience/variableexplorer.functional.test.tsx index 68fcc0324884..abd7331b6bf0 100644 --- a/src/test/datascience/variableexplorer.functional.test.tsx +++ b/src/test/datascience/variableexplorer.functional.test.tsx @@ -158,7 +158,7 @@ value = 'hello world'`; verifyVariables(wrapper, targetVariables); }, () => { - return ioc; + return Promise.resolve(ioc); } ); @@ -253,7 +253,7 @@ value = 'hello world'`; verifyVariables(wrapper, targetVariables); }, () => { - return ioc; + return Promise.resolve(ioc); } ); @@ -339,7 +339,7 @@ myDict = {'a': 1}`; } }, () => { - return ioc; + return Promise.resolve(ioc); } ); @@ -460,7 +460,7 @@ Name: 0, dtype: float64`, } }, () => { - return ioc; + return Promise.resolve(ioc); } ); @@ -527,7 +527,7 @@ Name: 0, dtype: float64`, } }, () => { - return ioc; + return Promise.resolve(ioc); } ); });