Skip to content

Commit a62f179

Browse files
authored
tests: print debugging info when timing out (#3190)
## Problem It's not always clear why tests hang and timeout. ## Solution Print pending UI elements before stopping the test. This is a more general solution over requiring tests to specify all responses up-front which is also viable.
1 parent 3089d02 commit a62f179

File tree

3 files changed

+47
-14
lines changed

3 files changed

+47
-14
lines changed

src/test/globalSetup.test.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import { activateExtension } from '../shared/utilities/vsCodeUtils'
2222
import { FakeExtensionContext, FakeMemento } from './fakeExtensionContext'
2323
import { TestLogger } from './testLogger'
2424
import * as testUtil from './testUtil'
25-
import { getTestWindow, resetTestWindow } from './shared/vscode/window'
25+
import { printPendingUiElements, getTestWindow, resetTestWindow } from './shared/vscode/window'
2626

2727
const testReportDir = join(__dirname, '../../../.test-reports')
2828
const testLogOutput = join(testReportDir, 'testLog.log')
@@ -80,14 +80,18 @@ beforeEach(async function () {
8080
event.dispose()
8181
reject(error)
8282
})
83+
84+
// Set a hard time limit per-test so CI doesn't hang
85+
// Mocha's `timeout` method isn't used because we want to emit a custom message
86+
setTimeout(() => {
87+
const message = `Test length exceeded max duration\n${printPendingUiElements()}`
88+
reject(new Error(message))
89+
}, maxTestDuration)
8390
}),
8491
])
8592
}
8693
}
8794

88-
// Set a hard time limit per-test so CI doesn't hang
89-
this.currentTest?.timeout(maxTestDuration)
90-
9195
// Enable telemetry features for tests. The metrics won't actually be posted.
9296
globals.telemetry.telemetryEnabled = true
9397
globals.telemetry.clearRecords()

src/test/shared/vscode/message.ts

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ export class TestMessage<T extends vscode.MessageItem = vscode.MessageItem> {
5858
}
5959

6060
public get visible() {
61-
return this._showing && this.message
61+
return this._showing && !!this.message
6262
}
6363

6464
public get detail() {
@@ -140,7 +140,11 @@ export class TestMessage<T extends vscode.MessageItem = vscode.MessageItem> {
140140
}
141141

142142
public printDebug(message: string | RegExp = this.message, severity = this.severity) {
143-
return `[${severity}]: ${typeof message === 'string' ? message : message.source}`
143+
return [
144+
`severity: ${severity}`,
145+
`message: ${message}`,
146+
`items: ${this.options?.items?.map(i => i.title).join(', ') || '[no items]'}`,
147+
].join(', ')
144148
}
145149

146150
public dispose(): void {
@@ -166,7 +170,7 @@ export class TestMessage<T extends vscode.MessageItem = vscode.MessageItem> {
166170
throw new Error('Attempted to select from a disposed message')
167171
}
168172
if (!this.options?.items || this.options.items.length === 0) {
169-
throw new Error(`Could not find the specified item: ${item}. Message has no items: ${this.message}`)
173+
throw new Error(`Could not find the specified item "${item}". Message has no items: ${this.message}`)
170174
}
171175

172176
const selected =
@@ -175,8 +179,7 @@ export class TestMessage<T extends vscode.MessageItem = vscode.MessageItem> {
175179
: this.options?.items?.find(i => i === item)
176180

177181
if (!selected) {
178-
const items = this.options?.items?.map(i => i.title)?.join('\n')
179-
throw new Error(`Could not find the specified item: ${item}. Current items:\n${items}`)
182+
throw new Error(`Could not find the specified item "${item}" on message: ${this.printDebug()}`)
180183
}
181184

182185
this._selected = selected
@@ -243,11 +246,11 @@ export class TestMessage<T extends vscode.MessageItem = vscode.MessageItem> {
243246
}
244247

245248
interface OpenDialogOptions extends vscode.OpenDialogOptions {
246-
readonly type: 'open'
249+
readonly type: 'Open'
247250
}
248251

249252
interface SaveDialogOptions extends vscode.SaveDialogOptions {
250-
readonly type: 'save'
253+
readonly type: 'Save'
251254
}
252255

253256
type FileSystemDialogOptions = OpenDialogOptions | SaveDialogOptions
@@ -294,7 +297,7 @@ export class TestFileSystemDialog {
294297
}
295298

296299
public get acceptButtonLabel() {
297-
if (this.options.type === 'save') {
300+
if (this.options.type === 'Save') {
298301
return this.options.saveLabel
299302
} else {
300303
return this.options.openLabel
@@ -343,12 +346,20 @@ export class TestFileSystemDialog {
343346
})
344347
}
345348

349+
public printDebug() {
350+
return [
351+
`type: ${this.options.type}`,
352+
`title: ${this.title ?? '[no title]'}`,
353+
`acceptButtonLabel: ${this.acceptButtonLabel ?? '[no label]'}`,
354+
].join(', ')
355+
}
356+
346357
public static createOpenSaveDialogFn(
347358
fs: vscode.FileSystem,
348359
callback?: (dialog: TestFileSystemDialog) => void
349360
): Window['showOpenDialog'] {
350361
return async (options?: vscode.OpenDialogOptions) => {
351-
const dialog = new TestFileSystemDialog([], { type: 'open', ...options })
362+
const dialog = new TestFileSystemDialog([], { type: 'Open', ...options })
352363

353364
return new Promise<vscode.Uri[] | undefined>(resolve => {
354365
dialog.onDidAcceptItem(item => resolve(item instanceof vscode.Uri ? [item] : item))
@@ -363,7 +374,7 @@ export class TestFileSystemDialog {
363374
callback?: (dialog: TestFileSystemDialog) => void
364375
): Window['showSaveDialog'] {
365376
return async (options?: vscode.SaveDialogOptions) => {
366-
const dialog = new TestFileSystemDialog([], { type: 'save', ...options })
377+
const dialog = new TestFileSystemDialog([], { type: 'Save', ...options })
367378

368379
return new Promise<vscode.Uri | undefined>(resolve => {
369380
dialog.onDidAcceptItem(item => resolve(Array.isArray(item) ? item[0] : item))

src/test/shared/vscode/window.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -432,6 +432,24 @@ export function assertNoErrorMessages() {
432432
}
433433
}
434434

435+
export function printPendingUiElements(window = getTestWindow()) {
436+
const parts: string[] = []
437+
const messages = window.shownMessages.filter(m => m.visible)
438+
const dialogs = window.shownDialogs.filter(d => d.visible)
439+
440+
if (messages.length > 0) {
441+
parts.push('Messages:', ...messages.map(m => ` ${m.printDebug()}`))
442+
}
443+
if (dialogs.length > 0) {
444+
parts.push('File System Dialogs:', ...dialogs.map(d => ` ${d.printDebug()}`))
445+
}
446+
if (window.activeQuickInput?.visible) {
447+
parts.push('Quick Inputs: ', ` ${window.activeQuickInput.title}`)
448+
}
449+
450+
return parts.length > 0 ? ['Pending UI Elements:', ...parts].join('\n') : '[No Pending UI Elements Found]'
451+
}
452+
435453
type TestEventEmitter<T> = vscode.EventEmitter<T> & {
436454
readonly onError: vscode.Event<{ event: T; error: unknown }>
437455
}

0 commit comments

Comments
 (0)