Skip to content

Commit eae03ed

Browse files
authored
Add functional test for debugging jupyter cells (microsoft#6510)
* Preliminary idea * More logging * Actual responses coming back * Get debugger basic test working Fix all the Promise.race(sleep) code to cancel the timer, as this timer can hang a test * Fix restart problems Add breakpoint test * Add breakpoint in another file test Add functional requirements so can find ptvsd on test machine * Add news entries * Fix logging to a file and a linter issue * Add back logToFile but commented out. * Fix functional tests and rework how waitForPromise behaves
1 parent 16e0af5 commit eae03ed

File tree

16 files changed

+678
-177
lines changed

16 files changed

+678
-177
lines changed

build/functional-test-requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,5 @@ numpy
55
matplotlib
66
pandas
77
livelossplot
8+
# This is temporary until these bits are ready
9+
git+https://s3h4llbub6kmnxk53k53ricbg4n7s6tif3ls6qsywersiroezaea@ptvsd.visualstudio.com/ptvsd-pr/_git/ptvsd-pr@ptvsd_jupyter_cells

news/2 Fixes/6502md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix debugging to work on restarting the jupyter kernel.

news/3 Code Health/6377.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Functional test for debugging jupyter cells.

src/client/common/utils/async.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,28 @@
33

44
'use strict';
55

6-
export async function sleep(timeout: number) : Promise<number> {
6+
export async function sleep(timeout: number): Promise<number> {
77
return new Promise<number>((resolve) => {
88
setTimeout(() => resolve(timeout), timeout);
99
});
1010
}
1111

12+
export function waitForPromise<T>(promise: Promise<T>, timeout: number): Promise<T | null> {
13+
// Set a timer that will resolve with null
14+
return new Promise<T | null>((resolve, reject) => {
15+
const timer = setTimeout(() => resolve(null), timeout);
16+
promise.then(result => {
17+
// When the promise resolves, make sure to clear the timer or
18+
// the timer may stick around causing tests to wait
19+
clearTimeout(timer);
20+
resolve(result);
21+
}).catch(e => {
22+
clearTimeout(timer);
23+
reject(e);
24+
});
25+
});
26+
}
27+
1228
//======================
1329
// Deferred
1430

src/client/datascience/editor-integration/cellhashprovider.ts

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,22 @@ import {
1414
} from 'vscode';
1515

1616
import { IDebugService, IDocumentManager } from '../../common/application/types';
17+
import { traceError } from '../../common/logger';
1718
import { IConfigurationService } from '../../common/types';
1819
import { noop } from '../../common/utils/misc';
1920
import { CellMatcher } from '../cellMatcher';
2021
import { splitMultilineString } from '../common';
2122
import { Identifiers } from '../constants';
2223
import { InteractiveWindowMessages, SysInfoReason } from '../interactive-window/interactiveWindowTypes';
23-
import { ICell, ICellHash, ICellHashListener, ICellHashProvider, IFileHashes, IInteractiveWindowListener, INotebookExecutionLogger } from '../types';
24+
import {
25+
ICell,
26+
ICellHash,
27+
ICellHashListener,
28+
ICellHashProvider,
29+
IFileHashes,
30+
IInteractiveWindowListener,
31+
INotebookExecutionLogger
32+
} from '../types';
2433

2534
interface IRangedCellHash extends ICellHash {
2635
code: string;
@@ -73,6 +82,7 @@ export class CellHashProvider implements ICellHashProvider, IInteractiveWindowLi
7382
const reason = payload.type as SysInfoReason;
7483
if (reason !== SysInfoReason.Interrupt) {
7584
this.hashes.clear();
85+
this.executionCount = 0;
7686
}
7787
}
7888
break;
@@ -92,14 +102,19 @@ export class CellHashProvider implements ICellHashProvider, IInteractiveWindowLi
92102
}
93103

94104
public async preExecute(cell: ICell, silent: boolean): Promise<void> {
95-
if (!silent) {
96-
// When the user adds new code, we know the execution count is increasing
97-
this.executionCount += 1;
98-
99-
// Skip hash on unknown file though
100-
if (cell.file !== Identifiers.EmptyFileName) {
101-
return this.addCellHash(cell, this.executionCount);
105+
try {
106+
if (!silent) {
107+
// When the user adds new code, we know the execution count is increasing
108+
this.executionCount += 1;
109+
110+
// Skip hash on unknown file though
111+
if (cell.file !== Identifiers.EmptyFileName) {
112+
await this.addCellHash(cell, this.executionCount);
113+
}
102114
}
115+
} catch (exc) {
116+
// Don't let exceptions in a preExecute mess up normal operation
117+
traceError(exc);
103118
}
104119
}
105120

@@ -195,7 +210,7 @@ export class CellHashProvider implements ICellHashProvider, IInteractiveWindowLi
195210
}
196211

197212
// Compute the runtime line and adjust our cell/stripped source for debugging
198-
const runtimeLine = this.adjustForDebugging(cell, stripped, startOffset, endOffset);
213+
const runtimeLine = this.adjustRuntimeForDebugging(cell, stripped, startOffset, endOffset);
199214
const hashedCode = stripped.join('');
200215
const realCode = doc.getText(new Range(new Position(cell.line, 0), endLine.rangeIncludingLineBreak.end));
201216

@@ -245,7 +260,7 @@ export class CellHashProvider implements ICellHashProvider, IInteractiveWindowLi
245260
}
246261
}
247262

248-
private adjustForDebugging(cell: ICell, source: string[], cellStartOffset: number, cellEndOffset: number): number {
263+
private adjustRuntimeForDebugging(cell: ICell, source: string[], cellStartOffset: number, cellEndOffset: number): number {
249264
if (this.debugService.activeDebugSession && this.configService.getSettings().datascience.stopOnFirstLineWhileDebugging) {
250265
// See if any breakpoints in any cell that's already run or in the cell we're about to run
251266
const anyExisting = this.debugService.breakpoints.filter(b => {

src/client/datascience/interactive-window/intellisense/baseIntellisenseProvider.ts

Lines changed: 23 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import { IWorkspaceService } from '../../../common/application/types';
2020
import { CancellationError } from '../../../common/cancellation';
2121
import { traceWarning } from '../../../common/logger';
2222
import { IFileSystem, TemporaryFile } from '../../../common/platform/types';
23-
import { createDeferred, Deferred, sleep } from '../../../common/utils/async';
23+
import { createDeferred, Deferred, waitForPromise } from '../../../common/utils/async';
2424
import { Identifiers, Settings } from '../../constants';
2525
import { IInteractiveWindowListener, IInteractiveWindowProvider, IJupyterExecution } from '../../types';
2626
import {
@@ -43,8 +43,8 @@ export abstract class BaseIntellisenseProvider implements IInteractiveWindowList
4343

4444
private documentPromise: Deferred<IntellisenseDocument> | undefined;
4545
private temporaryFile: TemporaryFile | undefined;
46-
private postEmitter: EventEmitter<{message: string; payload: any}> = new EventEmitter<{message: string; payload: any}>();
47-
private cancellationSources : Map<string, CancellationTokenSource> = new Map<string, CancellationTokenSource>();
46+
private postEmitter: EventEmitter<{ message: string; payload: any }> = new EventEmitter<{ message: string; payload: any }>();
47+
private cancellationSources: Map<string, CancellationTokenSource> = new Map<string, CancellationTokenSource>();
4848

4949
constructor(
5050
@unmanaged() private workspaceService: IWorkspaceService,
@@ -60,7 +60,7 @@ export abstract class BaseIntellisenseProvider implements IInteractiveWindowList
6060
}
6161
}
6262

63-
public get postMessage(): Event<{message: string; payload: any}> {
63+
public get postMessage(): Event<{ message: string; payload: any }> {
6464
return this.postEmitter.event;
6565
}
6666

@@ -116,7 +116,7 @@ export abstract class BaseIntellisenseProvider implements IInteractiveWindowList
116116
}
117117
}
118118

119-
protected getDocument(resource?: Uri) : Promise<IntellisenseDocument> {
119+
protected getDocument(resource?: Uri): Promise<IntellisenseDocument> {
120120
if (!this.documentPromise) {
121121
this.documentPromise = createDeferred<IntellisenseDocument>();
122122

@@ -142,17 +142,17 @@ export abstract class BaseIntellisenseProvider implements IInteractiveWindowList
142142
}
143143

144144
protected abstract get isActive(): boolean;
145-
protected abstract provideCompletionItems(position: monacoEditor.Position, context: monacoEditor.languages.CompletionContext, cellId: string, token: CancellationToken) : Promise<monacoEditor.languages.CompletionList>;
146-
protected abstract provideHover(position: monacoEditor.Position, cellId: string, token: CancellationToken) : Promise<monacoEditor.languages.Hover>;
147-
protected abstract provideSignatureHelp(position: monacoEditor.Position, context: monacoEditor.languages.SignatureHelpContext, cellId: string, token: CancellationToken) : Promise<monacoEditor.languages.SignatureHelp>;
148-
protected abstract handleChanges(originalFile: string | undefined, document: IntellisenseDocument, changes: TextDocumentContentChangeEvent[]) : Promise<void>;
145+
protected abstract provideCompletionItems(position: monacoEditor.Position, context: monacoEditor.languages.CompletionContext, cellId: string, token: CancellationToken): Promise<monacoEditor.languages.CompletionList>;
146+
protected abstract provideHover(position: monacoEditor.Position, cellId: string, token: CancellationToken): Promise<monacoEditor.languages.Hover>;
147+
protected abstract provideSignatureHelp(position: monacoEditor.Position, context: monacoEditor.languages.SignatureHelpContext, cellId: string, token: CancellationToken): Promise<monacoEditor.languages.SignatureHelp>;
148+
protected abstract handleChanges(originalFile: string | undefined, document: IntellisenseDocument, changes: TextDocumentContentChangeEvent[]): Promise<void>;
149149

150-
private dispatchMessage<M extends IInteractiveWindowMapping, T extends keyof M>(_message: T, payload: any, handler: (args : M[T]) => void) {
150+
private dispatchMessage<M extends IInteractiveWindowMapping, T extends keyof M>(_message: T, payload: any, handler: (args: M[T]) => void) {
151151
const args = payload as M[T];
152152
handler.bind(this)(args);
153153
}
154154

155-
private postResponse<M extends IInteractiveWindowMapping, T extends keyof M>(type: T, payload?: M[T]) : void {
155+
private postResponse<M extends IInteractiveWindowMapping, T extends keyof M>(type: T, payload?: M[T]): void {
156156
const response = payload as any;
157157
if (response && response.id) {
158158
const cancelSource = this.cancellationSources.get(response.id);
@@ -161,7 +161,7 @@ export abstract class BaseIntellisenseProvider implements IInteractiveWindowList
161161
this.cancellationSources.delete(response.id);
162162
}
163163
}
164-
this.postEmitter.fire({message: type.toString(), payload});
164+
this.postEmitter.fire({ message: type.toString(), payload });
165165
}
166166

167167
private handleCancel(request: ICancelIntellisenseRequest) {
@@ -181,11 +181,11 @@ export abstract class BaseIntellisenseProvider implements IInteractiveWindowList
181181
// Combine all of the results together.
182182
this.postTimedResponse(
183183
[this.provideCompletionItems(request.position, request.context, request.cellId, cancelSource.token),
184-
this.provideJupyterCompletionItems(request.position, request.context, cancelSource.token)],
184+
this.provideJupyterCompletionItems(request.position, request.context, cancelSource.token)],
185185
InteractiveWindowMessages.ProvideCompletionItemsResponse,
186186
(c) => {
187187
const list = this.combineCompletions(c);
188-
return {list, requestId: request.requestId};
188+
return { list, requestId: request.requestId };
189189
}
190190
);
191191
}
@@ -198,20 +198,20 @@ export abstract class BaseIntellisenseProvider implements IInteractiveWindowList
198198
InteractiveWindowMessages.ProvideHoverResponse,
199199
(h) => {
200200
if (h && h[0]) {
201-
return { hover: h[0]!, requestId: request.requestId};
201+
return { hover: h[0]!, requestId: request.requestId };
202202
} else {
203203
return { hover: { contents: [] }, requestId: request.requestId };
204204
}
205205
});
206206
}
207207

208-
private async provideJupyterCompletionItems(position: monacoEditor.Position, _context: monacoEditor.languages.CompletionContext, cancelToken: CancellationToken) : Promise<monacoEditor.languages.CompletionList> {
208+
private async provideJupyterCompletionItems(position: monacoEditor.Position, _context: monacoEditor.languages.CompletionContext, cancelToken: CancellationToken): Promise<monacoEditor.languages.CompletionList> {
209209
try {
210210
const activeServer = await this.jupyterExecution.getServer(await this.interactiveWindowProvider.getNotebookOptions());
211211
const document = await this.getDocument();
212212
if (activeServer && document) {
213213
const code = document.getEditCellContent();
214-
const lines = code.splitLines({trim: false, removeEmptyEntries: false});
214+
const lines = code.splitLines({ trim: false, removeEmptyEntries: false });
215215
const offsetInCode = lines.reduce((a: number, c: string, i: number) => {
216216
if (i < position.lineNumber - 1) {
217217
return a + c.length + 1;
@@ -226,7 +226,7 @@ export abstract class BaseIntellisenseProvider implements IInteractiveWindowList
226226
const baseOffset = document.getEditCellOffset();
227227
const basePosition = document.positionAt(baseOffset);
228228
const startPosition = document.positionAt(jupyterResults.cursor.start + baseOffset);
229-
const endPosition = document.positionAt(jupyterResults.cursor.end + baseOffset);
229+
const endPosition = document.positionAt(jupyterResults.cursor.end + baseOffset);
230230
const range: monacoEditor.IRange = {
231231
startLineNumber: startPosition.line + 1 - basePosition.line, // monaco is 1 based
232232
startColumn: startPosition.character + 1,
@@ -254,27 +254,18 @@ export abstract class BaseIntellisenseProvider implements IInteractiveWindowList
254254

255255
private postTimedResponse<R, M extends IInteractiveWindowMapping, T extends keyof M>(promises: Promise<R>[], message: T, formatResponse: (val: (R | null)[]) => M[T]) {
256256
// Time all of the promises to make sure they don't take too long
257-
const timed = promises.map(p => Promise.race([p, sleep(Settings.IntellisenseTimeout)]));
257+
const timed = promises.map(p => waitForPromise(p, Settings.IntellisenseTimeout));
258258

259259
// Wait for all of of the timings.
260260
const all = Promise.all(timed);
261261
all.then(r => {
262-
263-
// Check all of the results. If they timed out, turn into
264-
// a null so formatResponse can post the empty result.
265-
const nulled = r.map(v => {
266-
if (v === Settings.IntellisenseTimeout) {
267-
return null;
268-
}
269-
return v as R;
270-
});
271-
this.postResponse(message, formatResponse(nulled));
262+
this.postResponse(message, formatResponse(r));
272263
}).catch(_e => {
273264
this.postResponse(message, formatResponse([null]));
274265
});
275266
}
276267

277-
private combineCompletions(list: (monacoEditor.languages.CompletionList | null)[]) : monacoEditor.languages.CompletionList {
268+
private combineCompletions(list: (monacoEditor.languages.CompletionList | null)[]): monacoEditor.languages.CompletionList {
278269
// Note to self. We're eliminating duplicates ourselves. The alternative would be to
279270
// have more than one intellisense provider at the monaco editor level and return jupyter
280271
// results independently. Maybe we switch to this when jupyter resides on the react side.
@@ -303,9 +294,9 @@ export abstract class BaseIntellisenseProvider implements IInteractiveWindowList
303294
InteractiveWindowMessages.ProvideSignatureHelpResponse,
304295
(s) => {
305296
if (s && s[0]) {
306-
return { signatureHelp: s[0]!, requestId: request.requestId};
297+
return { signatureHelp: s[0]!, requestId: request.requestId };
307298
} else {
308-
return {signatureHelp: { signatures: [], activeParameter: 0, activeSignature: 0 }, requestId: request.requestId};
299+
return { signatureHelp: { signatures: [], activeParameter: 0, activeSignature: 0 }, requestId: request.requestId };
309300
}
310301
});
311302
}

0 commit comments

Comments
 (0)