Skip to content

Commit 5ae9f96

Browse files
authored
Functional test for run by line (microsoft#11746)
* Functional test for run by line * Fix linter
1 parent 798ba83 commit 5ae9f96

8 files changed

Lines changed: 185 additions & 109 deletions

File tree

news/3 Code Health/11660.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Functional test for run by line functionality

src/client/datascience/interactive-common/interactiveWindowTypes.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,8 @@ export enum InteractiveWindowMessages {
127127
Step = 'step',
128128
Continue = 'continue',
129129
ShowContinue = 'show_continue',
130-
ShowBreak = 'show_break'
130+
ShowBreak = 'show_break',
131+
ShowingIp = 'showing_ip'
131132
}
132133

133134
export enum IPyWidgetMessages {
@@ -607,4 +608,5 @@ export class IInteractiveWindowMapping {
607608
public [InteractiveWindowMessages.ShowBreak]: { frames: DebugProtocol.StackFrame[]; cell: ICell };
608609
public [InteractiveWindowMessages.ShowContinue]: ICell;
609610
public [InteractiveWindowMessages.Step]: never | undefined;
611+
public [InteractiveWindowMessages.ShowingIp]: never | undefined;
610612
}

src/client/datascience/interactive-common/synchronization.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@ const messageWithMessageTypes: MessageMapping<IInteractiveWindowMapping> & Messa
169169
[InteractiveWindowMessages.SendInfo]: MessageType.other,
170170
[InteractiveWindowMessages.SettingsUpdated]: MessageType.other,
171171
[InteractiveWindowMessages.ShowBreak]: MessageType.other,
172+
[InteractiveWindowMessages.ShowingIp]: MessageType.other,
172173
[InteractiveWindowMessages.ShowContinue]: MessageType.other,
173174
[InteractiveWindowMessages.ShowDataViewer]: MessageType.other,
174175
[InteractiveWindowMessages.ShowPlot]: MessageType.other,

src/datascience-ui/interactive-common/redux/store.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,13 @@ function createTestMiddleware(): Redux.Middleware<{}, IStore> {
204204
sendMessage(InteractiveWindowMessages.ExecutionRendered, { ids: diff });
205205
}
206206

207+
// Entering break state in a native cell
208+
const prevBreak = prevState.main.cellVMs.find((cvm) => cvm.currentStack);
209+
const newBreak = afterState.main.cellVMs.find((cvm) => cvm.currentStack);
210+
if (prevBreak !== newBreak || !fastDeepEqual(prevBreak?.currentStack, newBreak?.currentStack)) {
211+
sendMessage(InteractiveWindowMessages.ShowingIp);
212+
}
213+
207214
if (action.type !== 'action.postOutgoingMessage') {
208215
sendMessage(`DISPATCHED_ACTION_${action.type}`, {});
209216
}

src/test/datascience/dataScienceIocContainer.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,7 @@ import { AutoSaveService } from '../../client/datascience/interactive-ipynb/auto
194194
import { NativeEditor } from '../../client/datascience/interactive-ipynb/nativeEditor';
195195
import { NativeEditorCommandListener } from '../../client/datascience/interactive-ipynb/nativeEditorCommandListener';
196196
import { NativeEditorOldWebView } from '../../client/datascience/interactive-ipynb/nativeEditorOldWebView';
197+
import { NativeEditorRunByLineListener } from '../../client/datascience/interactive-ipynb/nativeEditorRunByLineListener';
197198
import { NativeEditorStorage } from '../../client/datascience/interactive-ipynb/nativeEditorStorage';
198199
import { NativeEditorSynchronizer } from '../../client/datascience/interactive-ipynb/nativeEditorSynchronizer';
199200
import { InteractiveWindow } from '../../client/datascience/interactive-window/interactiveWindow';
@@ -763,6 +764,7 @@ export class DataScienceIocContainer extends UnitTestIocContainer {
763764
this.serviceManager.add<IInteractiveWindowListener>(IInteractiveWindowListener, IntellisenseProvider);
764765
this.serviceManager.add<IInteractiveWindowListener>(IInteractiveWindowListener, AutoSaveService);
765766
this.serviceManager.add<IInteractiveWindowListener>(IInteractiveWindowListener, GatherListener);
767+
this.serviceManager.add<IInteractiveWindowListener>(IInteractiveWindowListener, NativeEditorRunByLineListener);
766768
this.serviceManager.addSingleton<IPyWidgetMessageDispatcherFactory>(
767769
IPyWidgetMessageDispatcherFactory,
768770
IPyWidgetMessageDispatcherFactory

src/test/datascience/debugger.functional.test.tsx

Lines changed: 147 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import * as TypeMoq from 'typemoq';
77
import * as uuid from 'uuid/v4';
88
import { CodeLens, Disposable, Position, Range, SourceBreakpoint, Uri } from 'vscode';
99
import { CancellationToken } from 'vscode-jsonrpc';
10-
import * as vsls from 'vsls/vscode';
1110

1211
import { IApplicationShell, IDocumentManager } from '../../client/common/application/types';
1312
import { RunByLine } from '../../client/common/experimentGroups';
@@ -23,11 +22,19 @@ import {
2322
IJupyterDebugService,
2423
IJupyterExecution
2524
} from '../../client/datascience/types';
25+
import { ImageButton } from '../../datascience-ui/react-common/imageButton';
2626
import { DataScienceIocContainer } from './dataScienceIocContainer';
2727
import { getInteractiveCellResults, getOrCreateInteractiveWindow } from './interactiveWindowTestHelpers';
2828
import { MockDocument } from './mockDocument';
2929
import { MockDocumentManager } from './mockDocumentManager';
30-
import { mountConnectedMainPanel, openVariableExplorer, waitForMessage } from './testHelpers';
30+
import { addCell, createNewEditor } from './nativeEditorTestHelpers';
31+
import {
32+
getLastOutputCell,
33+
openVariableExplorer,
34+
runInteractiveTest,
35+
runNativeTest,
36+
waitForMessage
37+
} from './testHelpers';
3138
import { verifyVariables } from './variableTestHelpers';
3239

3340
//import { asyncDump } from '../common/asyncDump';
@@ -52,13 +59,45 @@ suite('DataScience Debugger tests', () => {
5259
});
5360

5461
setup(async () => {
55-
ioc = createContainer();
62+
ioc = new DataScienceIocContainer();
63+
});
64+
65+
async function createIOC() {
66+
ioc.registerDataScienceTypes();
5667
jupyterDebuggerService = ioc.serviceManager.get<IJupyterDebugService>(
5768
IJupyterDebugService,
5869
Identifiers.MULTIPLEXING_DEBUGSERVICE
5970
);
60-
return ioc.activate();
61-
});
71+
// Rebind the appshell so we can change what happens on an error
72+
const dummyDisposable = {
73+
dispose: () => {
74+
return;
75+
}
76+
};
77+
const appShell = TypeMoq.Mock.ofType<IApplicationShell>();
78+
appShell.setup((a) => a.showErrorMessage(TypeMoq.It.isAnyString())).returns((e) => (lastErrorMessage = e));
79+
appShell
80+
.setup((a) => a.showInformationMessage(TypeMoq.It.isAny(), TypeMoq.It.isAny()))
81+
.returns(() => Promise.resolve(''));
82+
appShell
83+
.setup((a) => a.showInformationMessage(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()))
84+
.returns((_a1: string, a2: string, _a3: string) => Promise.resolve(a2));
85+
appShell
86+
.setup((a) => a.showSaveDialog(TypeMoq.It.isAny()))
87+
.returns(() => Promise.resolve(Uri.file('test.ipynb')));
88+
appShell.setup((a) => a.setStatusBarMessage(TypeMoq.It.isAny())).returns(() => dummyDisposable);
89+
90+
ioc.serviceManager.rebindInstance<IApplicationShell>(IApplicationShell, appShell.object);
91+
92+
// Make sure the history provider and execution factory in the container is created (the extension does this on startup in the extension)
93+
// This is necessary to get the appropriate live share services up and running.
94+
ioc.get<IInteractiveWindowProvider>(IInteractiveWindowProvider);
95+
ioc.get<IJupyterExecution>(IJupyterExecution);
96+
ioc.get<IDebugLocationTracker>(IDebugLocationTracker);
97+
98+
await ioc.activate();
99+
return ioc;
100+
}
62101

63102
teardown(async () => {
64103
for (const disposable of disposables) {
@@ -89,42 +128,6 @@ suite('DataScience Debugger tests', () => {
89128
// asyncDump();
90129
});
91130

92-
function createContainer(): DataScienceIocContainer {
93-
const result = new DataScienceIocContainer();
94-
result.registerDataScienceTypes();
95-
96-
// Rebind the appshell so we can change what happens on an error
97-
const dummyDisposable = {
98-
dispose: () => {
99-
return;
100-
}
101-
};
102-
const appShell = TypeMoq.Mock.ofType<IApplicationShell>();
103-
appShell.setup((a) => a.showErrorMessage(TypeMoq.It.isAnyString())).returns((e) => (lastErrorMessage = e));
104-
appShell
105-
.setup((a) => a.showInformationMessage(TypeMoq.It.isAny(), TypeMoq.It.isAny()))
106-
.returns(() => Promise.resolve(''));
107-
appShell
108-
.setup((a) => a.showInformationMessage(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()))
109-
.returns((_a1: string, a2: string, _a3: string) => Promise.resolve(a2));
110-
appShell
111-
.setup((a) => a.showSaveDialog(TypeMoq.It.isAny()))
112-
.returns(() => Promise.resolve(Uri.file('test.ipynb')));
113-
appShell.setup((a) => a.setStatusBarMessage(TypeMoq.It.isAny())).returns(() => dummyDisposable);
114-
115-
result.serviceManager.rebindInstance<IApplicationShell>(IApplicationShell, appShell.object);
116-
117-
// Setup our webview panel
118-
result.createWebView(() => mountConnectedMainPanel('interactive'), vsls.Role.None);
119-
120-
// Make sure the history provider and execution factory in the container is created (the extension does this on startup in the extension)
121-
// This is necessary to get the appropriate live share services up and running.
122-
result.get<IInteractiveWindowProvider>(IInteractiveWindowProvider);
123-
result.get<IJupyterExecution>(IJupyterExecution);
124-
result.get<IDebugLocationTracker>(IDebugLocationTracker);
125-
return result;
126-
}
127-
128131
async function debugCell(
129132
code: string,
130133
breakpoint?: Range,
@@ -238,63 +241,108 @@ suite('DataScience Debugger tests', () => {
238241
return [];
239242
}
240243

241-
test('Debug cell without breakpoint', async () => {
242-
await debugCell('#%%\nprint("bar")');
243-
});
244-
test('Check variables', async () => {
245-
ioc.setExperimentState(RunByLine.experiment, true);
246-
await debugCell('#%%\nx = [4, 6]\nx = 5', undefined, undefined, false, () => {
247-
const targetResult = {
248-
name: 'x',
249-
value: '[4, 6]',
250-
supportsDataExplorer: true,
251-
type: 'list',
252-
size: 0,
253-
shape: '',
254-
count: 2,
255-
truncated: false
256-
};
257-
verifyVariables(ioc!.wrapper!, [targetResult]);
258-
});
259-
});
260-
261-
test('Debug temporary file', async () => {
262-
const code = '#%%\nprint("bar")';
263-
264-
// Create a dummy document with just this code
265-
const docManager = ioc.get<IDocumentManager>(IDocumentManager) as MockDocumentManager;
266-
const fileName = 'Untitled-1';
267-
docManager.addDocument(code, fileName);
268-
const mockDoc = docManager.textDocuments[0] as MockDocument;
269-
mockDoc.forceUntitled();
270-
271-
// Start the jupyter server
272-
const history = await getOrCreateInteractiveWindow(ioc);
273-
const expectedBreakLine = 2; // 2 because of the 'breakpoint()' that gets added
274-
275-
// Debug this code. We should either hit the breakpoint or stop on entry
276-
const resultPromise = getInteractiveCellResults(ioc, ioc.wrapper!, async () => {
277-
const breakPromise = createDeferred<void>();
278-
disposables.push(jupyterDebuggerService!.onBreakpointHit(() => breakPromise.resolve()));
279-
const targetUri = Uri.file(fileName);
280-
const done = history.debugCode(code, targetUri.fsPath, 0, docManager.activeTextEditor);
281-
await waitForPromise(Promise.race([done, breakPromise.promise]), 60000);
282-
assert.ok(breakPromise.resolved, 'Breakpoint event did not fire');
283-
assert.ok(!lastErrorMessage, `Error occurred ${lastErrorMessage}`);
284-
const stackFrames = await jupyterDebuggerService!.getStack();
285-
assert.ok(stackFrames, 'Stack trace not computable');
286-
assert.ok(stackFrames.length >= 1, 'Not enough frames');
287-
assert.equal(stackFrames[0].line, expectedBreakLine, 'Stopped on wrong line number');
288-
assert.ok(
289-
stackFrames[0].source!.path!.includes('baz.py'),
290-
'Stopped on wrong file name. Name should have been saved'
291-
);
292-
// Verify break location
293-
await jupyterDebuggerService!.continue();
294-
});
244+
runInteractiveTest(
245+
'Debug cell without breakpoint',
246+
async () => {
247+
await debugCell('#%%\nprint("bar")');
248+
},
249+
createIOC
250+
);
251+
runInteractiveTest(
252+
'Check variables',
253+
async () => {
254+
ioc.setExperimentState(RunByLine.experiment, true);
255+
await debugCell('#%%\nx = [4, 6]\nx = 5', undefined, undefined, false, () => {
256+
const targetResult = {
257+
name: 'x',
258+
value: '[4, 6]',
259+
supportsDataExplorer: true,
260+
type: 'list',
261+
size: 0,
262+
shape: '',
263+
count: 2,
264+
truncated: false
265+
};
266+
verifyVariables(ioc!.wrapper!, [targetResult]);
267+
});
268+
},
269+
createIOC
270+
);
271+
272+
runInteractiveTest(
273+
'Debug temporary file',
274+
async () => {
275+
const code = '#%%\nprint("bar")';
276+
277+
// Create a dummy document with just this code
278+
const docManager = ioc.get<IDocumentManager>(IDocumentManager) as MockDocumentManager;
279+
const fileName = 'Untitled-1';
280+
docManager.addDocument(code, fileName);
281+
const mockDoc = docManager.textDocuments[0] as MockDocument;
282+
mockDoc.forceUntitled();
283+
284+
// Start the jupyter server
285+
const history = await getOrCreateInteractiveWindow(ioc);
286+
const expectedBreakLine = 2; // 2 because of the 'breakpoint()' that gets added
287+
288+
// Debug this code. We should either hit the breakpoint or stop on entry
289+
const resultPromise = getInteractiveCellResults(ioc, ioc.wrapper!, async () => {
290+
const breakPromise = createDeferred<void>();
291+
disposables.push(jupyterDebuggerService!.onBreakpointHit(() => breakPromise.resolve()));
292+
const targetUri = Uri.file(fileName);
293+
const done = history.debugCode(code, targetUri.fsPath, 0, docManager.activeTextEditor);
294+
await waitForPromise(Promise.race([done, breakPromise.promise]), 60000);
295+
assert.ok(breakPromise.resolved, 'Breakpoint event did not fire');
296+
assert.ok(!lastErrorMessage, `Error occurred ${lastErrorMessage}`);
297+
const stackFrames = await jupyterDebuggerService!.getStack();
298+
assert.ok(stackFrames, 'Stack trace not computable');
299+
assert.ok(stackFrames.length >= 1, 'Not enough frames');
300+
assert.equal(stackFrames[0].line, expectedBreakLine, 'Stopped on wrong line number');
301+
assert.ok(
302+
stackFrames[0].source!.path!.includes('baz.py'),
303+
'Stopped on wrong file name. Name should have been saved'
304+
);
305+
// Verify break location
306+
await jupyterDebuggerService!.continue();
307+
});
295308

296-
const cellResults = await resultPromise;
297-
assert.ok(cellResults, 'No cell results after finishing debugging');
298-
await history.dispose();
299-
});
309+
const cellResults = await resultPromise;
310+
assert.ok(cellResults, 'No cell results after finishing debugging');
311+
await history.dispose();
312+
},
313+
createIOC
314+
);
315+
316+
runNativeTest(
317+
'Run by line',
318+
async () => {
319+
// Create an editor so something is listening to messages
320+
await createNewEditor(ioc);
321+
const wrapper = ioc.wrapper!;
322+
323+
// Add a cell into the UI and wait for it to render and submit it.
324+
await addCell(wrapper, ioc, 'a=1\na', true);
325+
326+
// Step into this cell using the button
327+
let cell = getLastOutputCell(wrapper, 'NativeCell');
328+
let ImageButtons = cell.find(ImageButton);
329+
assert.equal(ImageButtons.length, 7, 'Cell buttons not found');
330+
const runByLineButton = ImageButtons.at(3);
331+
// tslint:disable-next-line: no-any
332+
assert.equal((runByLineButton.instance().props as any).tooltip, 'Run by line');
333+
334+
const promise = waitForMessage(ioc, InteractiveWindowMessages.ShowingIp);
335+
runByLineButton.simulate('click');
336+
await promise;
337+
338+
// We should be in the break state. See if buttons indicate that or not
339+
cell = getLastOutputCell(wrapper, 'NativeCell');
340+
ImageButtons = cell.find(ImageButton);
341+
assert.equal(ImageButtons.length, 4, 'Cell buttons wrong number');
342+
},
343+
() => {
344+
ioc.setExperimentState(RunByLine.experiment, true);
345+
return createIOC();
346+
}
347+
);
300348
});

src/test/datascience/testHelpers.tsx

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -136,9 +136,9 @@ async function testInnerLoop(
136136
type: 'native' | 'interactive',
137137
wrapper: ReactWrapper<any, Readonly<{}>, React.Component>
138138
) => Promise<void>,
139-
getIOC: () => DataScienceIocContainer
139+
getIOC: () => Promise<DataScienceIocContainer>
140140
) {
141-
const ioc = getIOC();
141+
const ioc = await getIOC();
142142
const jupyterExecution = ioc.get<IJupyterExecution>(IJupyterExecution);
143143
if (await jupyterExecution.isNotebookSupported()) {
144144
addMockData(ioc, 'a=1\na', 1);
@@ -156,7 +156,7 @@ export function runDoubleTest(
156156
type: 'native' | 'interactive',
157157
wrapper: ReactWrapper<any, Readonly<{}>, React.Component>
158158
) => Promise<void>,
159-
getIOC: () => DataScienceIocContainer
159+
getIOC: () => Promise<DataScienceIocContainer>
160160
) {
161161
// Just run the test twice. Originally mounted twice, but too hard trying to figure out disposing.
162162
test(`${name} (interactive)`, async () =>
@@ -168,7 +168,7 @@ export function runDoubleTest(
168168
export function runInteractiveTest(
169169
name: string,
170170
testFunc: (wrapper: ReactWrapper<any, Readonly<{}>, React.Component>) => Promise<void>,
171-
getIOC: () => DataScienceIocContainer
171+
getIOC: () => Promise<DataScienceIocContainer>
172172
) {
173173
// Run the test with just the interactive window
174174
test(`${name} (interactive)`, async () =>
@@ -180,6 +180,21 @@ export function runInteractiveTest(
180180
getIOC
181181
));
182182
}
183+
export function runNativeTest(
184+
name: string,
185+
testFunc: (wrapper: ReactWrapper<any, Readonly<{}>, React.Component>) => Promise<void>,
186+
getIOC: () => Promise<DataScienceIocContainer>
187+
) {
188+
// Run the test with just the native window
189+
test(`${name} (native)`, async () =>
190+
testInnerLoop(
191+
name,
192+
'native',
193+
(ioc) => mountWebView(ioc, 'native'),
194+
(_t, w) => testFunc(w),
195+
getIOC
196+
));
197+
}
183198

184199
export function mountWebView(
185200
ioc: DataScienceIocContainer,

0 commit comments

Comments
 (0)