Skip to content

Commit

Permalink
fix: print unhandled errors to tets results tab
Browse files Browse the repository at this point in the history
  • Loading branch information
sheremet-va committed May 2, 2024
1 parent 1a11227 commit 12d1093
Show file tree
Hide file tree
Showing 6 changed files with 98 additions and 81 deletions.
2 changes: 1 addition & 1 deletion src/api/rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export interface VitestPool extends VitestPoolMethods {
export interface VitestEvents {
onConsoleLog: (log: UserConsoleLog) => void
onTaskUpdate: (task: TaskResultPack[]) => void
onFinished: (files?: File[], errors?: unknown[], collecting?: boolean) => void
onFinished: (files: File[], unhandledError: string, collecting?: boolean) => void
onCollected: (files?: File[], collecting?: boolean) => void
onWatcherStart: (files?: File[], errors?: unknown[], collecting?: boolean) => void
onWatcherRerun: (files: string[], trigger?: string, collecting?: boolean) => void
Expand Down
1 change: 1 addition & 0 deletions src/extension.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as vscode from 'vscode'
import './polyfills'
import { gte } from 'semver'
import { version } from '../package.json'
import { getConfig, testControllerId } from './config'
Expand Down
11 changes: 11 additions & 0 deletions src/polyfills.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
if (!Promise.withResolvers) {
Promise.withResolvers = function withResolvers<T>() {
let a: (v: T | PromiseLike<T>) => void
let b: (r?: any) => void
const c = new this<T>((resolve, reject) => {
a = resolve
b = reject
})
return { resolve: a!, reject: b!, promise: c }
}
}
133 changes: 58 additions & 75 deletions src/runner/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import * as vscode from 'vscode'
import { getTasks } from '@vitest/ws-client'
import type { ErrorWithDiff, File, ParsedStack, Task, TaskResult } from 'vitest'
import { basename, normalize, relative } from 'pathe'
import { type TestData, TestFile, TestFolder, getTestData } from '../testTreeData'
import { TestFile, TestFolder, getTestData } from '../testTreeData'
import type { TestTree } from '../testTree'
import type { VitestFolderAPI } from '../api'
import { log } from '../log'
Expand All @@ -15,15 +15,13 @@ import { coverageContext, readCoverageReport } from '../coverage'

export class TestRunner extends vscode.Disposable {
private continuousRequests = new Set<vscode.TestRunRequest>()
private simpleTestRunRequest: vscode.TestRunRequest | null = null

// TODO: doesn't support "projects" - run every project because Vitest doesn't support
// granular filters yet (coming in Vitest 1.4.1)
private testRunsByFile = new Map<string, vscode.TestRun>()
private testRunsByRequest = new WeakMap<vscode.TestRunRequest, vscode.TestRun[]>()
private simpleTestRunRequest: vscode.TestRunRequest | undefined

private _onRequestsExhausted = new vscode.EventEmitter<void>()

private testRun: vscode.TestRun | undefined
private testRunDefer: PromiseWithResolvers<void> | undefined

constructor(
private readonly controller: vscode.TestController,
private readonly tree: TestTree,
Expand All @@ -32,9 +30,9 @@ export class TestRunner extends vscode.Disposable {
) {
super(() => {
api.clearListeners()
this.testRunsByFile.forEach(run => run.end())
this.testRunsByFile.clear()
this.simpleTestRunRequest = null
this.testRun?.end()
this.testRun = undefined
this.simpleTestRunRequest = undefined
this.continuousRequests.clear()
this.api.cancelRun()
this._onRequestsExhausted.dispose()
Expand All @@ -49,7 +47,7 @@ export class TestRunner extends vscode.Disposable {
log.error('Cannot find task during onTaskUpdate', testId)
return
}
const testRun = this.getTestRunByTestItem(test)
const testRun = this.testRun
// there is no test run for collected tests
if (!testRun)
return
Expand All @@ -71,7 +69,7 @@ export class TestRunner extends vscode.Disposable {
log.error(`Test data not found for "${task.name}"`)
return
}
const testRun = this.getTestRunByTestItem(test)
const testRun = this.testRun
if (!testRun)
return
if (task.mode === 'skip' || task.mode === 'todo')
Expand All @@ -81,7 +79,11 @@ export class TestRunner extends vscode.Disposable {
})
})

api.onFinished(async (files = [], _, collecting) => {
api.onFinished(async (files = [], unhandledError, collecting) => {
const testRun = this.testRun
if (!testRun)
return

try {
if (!collecting)
await this.reportCoverage(files)
Expand All @@ -90,29 +92,24 @@ export class TestRunner extends vscode.Disposable {
showVitestError(`Failed to report coverage. ${err.message}`, err)
}

const finishedRuns = new Set<vscode.TestRun>()

files.forEach((file) => {
const testItem = this.tree.getTestItemByTask(file)
const data = testItem && getTestData(testItem) as TestFile | undefined
const testRun = data && this.getTestRunByData(data)
if (testRun && data) {
if (testItem)
this.markResult(testRun, testItem, file.result, file)
this.testRunsByFile.delete(file.filepath)
if (!finishedRuns.has(testRun)) {
finishedRuns.add(testRun)
testRun.end()
}
}
})

if (unhandledError)
testRun.appendOutput(formatTestOutput(unhandledError))

this.endTestRun()
})

api.onConsoleLog(({ content, taskId }) => {
const testItem = taskId ? tree.getTestItemByTaskId(taskId) : undefined
const testRun = testItem && this.getTestRunByTestItem(testItem)
const testRun = this.testRun
if (testRun) {
testRun.appendOutput(
content.replace(/(?<!\r)\n/g, '\r\n'),
formatTestOutput(content),
undefined,
testItem,
)
Expand All @@ -127,15 +124,17 @@ export class TestRunner extends vscode.Disposable {
await this.debug.enable(this.api)

const testItems = request.include?.length
? partitionTestFileItems(this.tree, request.include)
? partitionTestFileItems(request.include)
: this.tree.getAllFileItems().map(item => [item, []] as [vscode.TestItem, never[]])

this.simpleTestRunRequest = request
token.onCancellationRequested(() => {
this.debug.disable(this.api)
this.endRequestRuns(request)
this.simpleTestRunRequest = null
this.endTestRun()
this.simpleTestRunRequest = undefined
this.api.cancelRun()
// just in case it gets stuck
this.testRunDefer?.resolve()
})

// we need to run tests one file at a time, so we partition them
Expand All @@ -160,27 +159,26 @@ export class TestRunner extends vscode.Disposable {

await this.debug.disable(this.api)

this.simpleTestRunRequest = null
this.simpleTestRunRequest = undefined
this._onRequestsExhausted.fire()
}

private endRequestRuns(request: vscode.TestRunRequest) {
const runs = this.testRunsByRequest.get(request)
if (!runs)
return
runs.forEach(run => run.end())
this.testRunsByRequest.delete(request)
private endTestRun() {
this.testRun?.end()
this.testRunDefer?.resolve()
this.testRun = undefined
this.testRunDefer = undefined
}

private async watchContinuousTests(request: vscode.TestRunRequest, token: vscode.CancellationToken) {
this.continuousRequests.add(request)

token.onCancellationRequested(() => {
this.continuousRequests.delete(request)
this.endRequestRuns(request)
if (!this.continuousRequests.size) {
this._onRequestsExhausted.fire()
this.api.unwatchTests()
this.endTestRun()
}
})

Expand Down Expand Up @@ -227,18 +225,22 @@ export class TestRunner extends vscode.Disposable {
this.simpleTestRunRequest = request

token.onCancellationRequested(() => {
this.endRequestRuns(request)
this.simpleTestRunRequest = null
this.endTestRun()
this.simpleTestRunRequest = undefined
this.api.cancelRun()
})

await this.runTestItems(request, request.include || [])

this.simpleTestRunRequest = null
this.simpleTestRunRequest = undefined
this._onRequestsExhausted.fire()
}

private async runTestItems(request: vscode.TestRunRequest, tests: readonly vscode.TestItem[]) {
await this.testRunDefer?.promise

this.testRunDefer = Promise.withResolvers()

const runTests = (files?: string[], testNamePatern?: string) =>
'updateSnapshots' in request
? this.api.updateSnapshots(files, testNamePatern)
Expand All @@ -260,21 +262,6 @@ export class TestRunner extends vscode.Disposable {
}
}

private getTestRunByTestItem(data: vscode.TestItem) {
return this.getTestRunByData(getTestData(data))
}

private getTestRunByData(data: TestData): vscode.TestRun | null {
if (data instanceof TestFolder)
return null
if (data instanceof TestFile)
return this.testRunsByFile.get(data.filepath) || null

if ('file' in data)
return this.getTestRunByData(data.file)
return null
}

private isFileIncluded(file: string, include: readonly vscode.TestItem[] | vscode.TestItemCollection) {
for (const _item of include) {
const item = 'id' in _item ? _item : _item[1]
Expand Down Expand Up @@ -321,43 +308,40 @@ export class TestRunner extends vscode.Disposable {
)
}

private async startTestRun(files: string[], primaryRun?: vscode.TestRun, primaryRequest?: vscode.TestRunRequest) {
private async startTestRun(files: string[], primaryRequest?: vscode.TestRunRequest) {
const request = primaryRequest || this.simpleTestRunRequest || this.createContinuousRequest()

if (!request)
return

if (this.testRun) {
await this.testRunDefer?.promise
this.endTestRun()
}

const name = files.length > 1
? undefined
: relative(this.api.workspaceFolder.uri.fsPath, files[0])

const run = primaryRun || this.controller.createTestRun(request, name)
const testRunsByRequest = this.testRunsByRequest.get(request) || []
this.testRunsByRequest.set(request, [...testRunsByRequest, run])
const run = this.testRun = this.controller.createTestRun(request, name)

for (const file of files) {
if (file[file.length - 1] === '/') {
const files = this.getTestFilesInFolder(file)
this.startTestRun(files, run, request)
this.startTestRun(files, request)
continue
}

// during test collection, we don't have test runs
if (request.include && !this.isFileIncluded(file, request.include))
continue

const testRun = this.testRunsByFile.get(file)
if (testRun)
continue

const testItems = this.tree.getFileTestItems(file)
function enqueue(test: vscode.TestItem) {
run.enqueued(test)
test.children.forEach(enqueue)
}
testItems.forEach(test => enqueue(test))

this.testRunsByFile.set(file, run)
}
}

Expand All @@ -371,15 +355,10 @@ export class TestRunner extends vscode.Disposable {

const coverage = readCoverageReport(reportsDirectory)

const runs = new Set<vscode.TestRun>()

const promises = files.map(async (file) => {
const testItem = this.tree.getTestItemByTask(file)
const testRun = testItem && this.getTestRunByTestItem(testItem)
if (testRun && !runs.has(testRun)) {
runs.add(testRun)
const promises = files.map(async () => {
const testRun = this.testRun
if (testRun)
await coverageContext.applyJson(testRun, coverage)
}
})

await Promise.all(promises)
Expand Down Expand Up @@ -494,7 +473,7 @@ function getFolderFiles(folder: vscode.TestItem): vscode.TestItem[] {
return files
}

function partitionTestFileItems(tree: TestTree, tests: readonly vscode.TestItem[]) {
function partitionTestFileItems(tests: readonly vscode.TestItem[]) {
const fileItems = new Map<vscode.TestItem, vscode.TestItem[]>()

for (const testItem of tests) {
Expand Down Expand Up @@ -560,3 +539,7 @@ function formatTestPattern(tests: readonly vscode.TestItem[]) {
return undefined
return patterns.join('|')
}

function formatTestOutput(output: string) {
return output.replace(/(?<!\r)\n/g, '\r\n')
}
6 changes: 3 additions & 3 deletions src/testTree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,11 +223,11 @@ export class TestTree extends vscode.Disposable {
}
}

public getTestItemByTaskId(taskId: string): vscode.TestItem | null {
public getTestItemByTaskId(taskId: string): vscode.TestItem | undefined {
const testItem = this.flatTestItems.get(taskId)
if (!testItem)
return null
return testItem || null
return undefined
return testItem || undefined
}

public getTestItemByTask(task: Task): vscode.TestItem | null {
Expand Down
26 changes: 24 additions & 2 deletions src/worker/reporter.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { nextTick } from 'node:process'
import { Writable } from 'node:stream'
import { Console } from 'node:console'
import { parseErrorStacktrace } from '@vitest/utils/source-map'
import type { BirpcReturn } from 'birpc'
import type { File, Reporter, TaskResultPack, UserConsoleLog, Vitest as VitestCore } from 'vitest'
Expand Down Expand Up @@ -57,10 +59,30 @@ export class VSCodeReporter implements Reporter {
this.rpc.onTaskUpdate(this.meta.id, packs)
}

onFinished(files?: File[], errors?: unknown[]) {
async onFinished(files?: File[], errors: unknown[] = this.ctx.state.getUnhandledErrors()) {
const collecting = this.collecting

let output = ''
if (errors.length) {
const writable = new Writable({
write(chunk, _encoding, callback) {
output += String(chunk)
callback()
},
})
const _console = this.ctx.logger.console
const errorStream = this.ctx.logger.errorStream
const outputStream = this.ctx.logger.outputStream
this.ctx.logger.errorStream = writable as any
this.ctx.logger.outputStream = writable as any
this.ctx.logger.console = new Console(writable, writable)
await this.ctx.logger.printUnhandledErrors(errors)
this.ctx.logger.console = _console
this.ctx.logger.errorStream = errorStream
this.ctx.logger.outputStream = outputStream
}
nextTick(() => {
this.rpc.onFinished(this.meta.id, files, errors, collecting)
this.rpc.onFinished(this.meta.id, files || [], output, collecting)
})
}

Expand Down

0 comments on commit 12d1093

Please sign in to comment.