Skip to content

Functional test for run by line #11746

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
May 11, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions news/3 Code Health/11660.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Functional test for run by line functionality
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ const messageWithMessageTypes: MessageMapping<IInteractiveWindowMapping> & 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,
Expand Down
7 changes: 7 additions & 0 deletions src/datascience-ui/interactive-common/redux/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`, {});
}
Expand Down
2 changes: 2 additions & 0 deletions src/test/datascience/dataScienceIocContainer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -763,6 +764,7 @@ export class DataScienceIocContainer extends UnitTestIocContainer {
this.serviceManager.add<IInteractiveWindowListener>(IInteractiveWindowListener, IntellisenseProvider);
this.serviceManager.add<IInteractiveWindowListener>(IInteractiveWindowListener, AutoSaveService);
this.serviceManager.add<IInteractiveWindowListener>(IInteractiveWindowListener, GatherListener);
this.serviceManager.add<IInteractiveWindowListener>(IInteractiveWindowListener, NativeEditorRunByLineListener);
this.serviceManager.addSingleton<IPyWidgetMessageDispatcherFactory>(
IPyWidgetMessageDispatcherFactory,
IPyWidgetMessageDispatcherFactory
Expand Down
246 changes: 147 additions & 99 deletions src/test/datascience/debugger.functional.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand All @@ -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>(
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<IApplicationShell>();
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>(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>(IInteractiveWindowProvider);
ioc.get<IJupyterExecution>(IJupyterExecution);
ioc.get<IDebugLocationTracker>(IDebugLocationTracker);

await ioc.activate();
return ioc;
}

teardown(async () => {
for (const disposable of disposables) {
Expand Down Expand Up @@ -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<IApplicationShell>();
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>(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>(IInteractiveWindowProvider);
result.get<IJupyterExecution>(IJupyterExecution);
result.get<IDebugLocationTracker>(IDebugLocationTracker);
return result;
}

async function debugCell(
code: string,
breakpoint?: Range,
Expand Down Expand Up @@ -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>(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<void>();
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>(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<void>();
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();
}
);
});
23 changes: 19 additions & 4 deletions src/test/datascience/testHelpers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -136,9 +136,9 @@ async function testInnerLoop(
type: 'native' | 'interactive',
wrapper: ReactWrapper<any, Readonly<{}>, React.Component>
) => Promise<void>,
getIOC: () => DataScienceIocContainer
getIOC: () => Promise<DataScienceIocContainer>
) {
const ioc = getIOC();
const ioc = await getIOC();
const jupyterExecution = ioc.get<IJupyterExecution>(IJupyterExecution);
if (await jupyterExecution.isNotebookSupported()) {
addMockData(ioc, 'a=1\na', 1);
Expand All @@ -156,7 +156,7 @@ export function runDoubleTest(
type: 'native' | 'interactive',
wrapper: ReactWrapper<any, Readonly<{}>, React.Component>
) => Promise<void>,
getIOC: () => DataScienceIocContainer
getIOC: () => Promise<DataScienceIocContainer>
) {
// Just run the test twice. Originally mounted twice, but too hard trying to figure out disposing.
test(`${name} (interactive)`, async () =>
Expand All @@ -168,7 +168,7 @@ export function runDoubleTest(
export function runInteractiveTest(
name: string,
testFunc: (wrapper: ReactWrapper<any, Readonly<{}>, React.Component>) => Promise<void>,
getIOC: () => DataScienceIocContainer
getIOC: () => Promise<DataScienceIocContainer>
) {
// Run the test with just the interactive window
test(`${name} (interactive)`, async () =>
Expand All @@ -180,6 +180,21 @@ export function runInteractiveTest(
getIOC
));
}
export function runNativeTest(
name: string,
testFunc: (wrapper: ReactWrapper<any, Readonly<{}>, React.Component>) => Promise<void>,
getIOC: () => Promise<DataScienceIocContainer>
) {
// 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,
Expand Down
Loading