From d627e209dd99ba17557ef6ba2e4b2c52ddf6fa97 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Thu, 14 Mar 2024 15:54:40 +0100 Subject: [PATCH] feat: add a flag to include test location in tasks (#5342) --- docs/config/index.md | 13 +++++ packages/browser/src/client/runner.ts | 33 ++++++++++++- packages/runner/src/collect.ts | 2 +- packages/runner/src/run.ts | 2 +- packages/runner/src/suite.ts | 55 ++++++++++++++++++++-- packages/runner/src/types/runner.ts | 1 + packages/runner/src/types/tasks.ts | 4 ++ packages/vitest/src/api/setup.ts | 6 +++ packages/vitest/src/api/types.ts | 1 + packages/vitest/src/node/cli/cli-config.ts | 1 + packages/vitest/src/node/workspace.ts | 1 + packages/vitest/src/public/utils.ts | 4 ++ packages/vitest/src/types/config.ts | 7 +++ test/public-api/fixtures/vitest.config.ts | 1 + test/public-api/package.json | 3 +- test/public-api/tests/runner.spec.ts | 23 +++++++-- 16 files changed, 145 insertions(+), 12 deletions(-) create mode 100644 test/public-api/fixtures/vitest.config.ts diff --git a/docs/config/index.md b/docs/config/index.md index e9bf7d5730d2..9dde7a667390 100644 --- a/docs/config/index.md +++ b/docs/config/index.md @@ -2108,3 +2108,16 @@ Disabling this option might [improve performance](/guide/improving-performance) ::: tip You can disable isolation for specific pools by using [`poolOptions`](#pooloptions) property. ::: + +### includeTaskLocation 1.4.0+ {#includeTaskLocation} + +- **Type:** `boolean` +- **Default:** `false` + +Should `location` property be included when Vitest API receives tasks in [reporters](#reporters). If you have a lot of tests, this might cause a small performance regression. + +The `location` property has `column` and `line` values that correspond to the `test` or `describe` position in the original file. + +::: tip +This option has no effect if you do not use custom code that relies on this. +::: diff --git a/packages/browser/src/client/runner.ts b/packages/browser/src/client/runner.ts index 63a9be827640..1725b93883d2 100644 --- a/packages/browser/src/client/runner.ts +++ b/packages/browser/src/client/runner.ts @@ -55,7 +55,13 @@ export function createBrowserRunner( } } - onCollected = (files: File[]): unknown => { + onCollected = async (files: File[]): Promise => { + if (this.config.includeTaskLocation) { + try { + await updateFilesLocations(files) + } + catch (_) {} + } return rpc().onCollected(files) } @@ -107,3 +113,28 @@ export async function initiateRunner() { cachedRunner = runner return runner } + +async function updateFilesLocations(files: File[]) { + const { loadSourceMapUtils } = await importId('vitest/utils') as typeof import('vitest/utils') + const { TraceMap, originalPositionFor } = await loadSourceMapUtils() + + const promises = files.map(async (file) => { + const result = await rpc().getBrowserFileSourceMap(file.filepath) + if (!result) + return null + const traceMap = new TraceMap(result as any) + function updateLocation(task: Task) { + if (task.location) { + const { line, column } = originalPositionFor(traceMap, task.location) + if (line != null && column != null) + task.location = { line, column: column + 1 } + } + if ('tasks' in task) + task.tasks.forEach(updateLocation) + } + file.tasks.forEach(updateLocation) + return null + }) + + await Promise.all(promises) +} diff --git a/packages/runner/src/collect.ts b/packages/runner/src/collect.ts index a32501b67a2c..0a9a69bb540d 100644 --- a/packages/runner/src/collect.ts +++ b/packages/runner/src/collect.ts @@ -28,7 +28,7 @@ export async function collectTests(paths: string[], runner: VitestRunner): Promi projectName: config.name, } - clearCollectorContext(runner) + clearCollectorContext(filepath, runner) try { const setupStart = now() diff --git a/packages/runner/src/run.ts b/packages/runner/src/run.ts index 4087ab3b29fb..5155621fb129 100644 --- a/packages/runner/src/run.ts +++ b/packages/runner/src/run.ts @@ -393,7 +393,7 @@ export async function startTests(paths: string[], runner: VitestRunner) { const files = await collectTests(paths, runner) - runner.onCollected?.(files) + await runner.onCollected?.(files) await runner.onBeforeRunFiles?.(files) await runFiles(files, runner) diff --git a/packages/runner/src/suite.ts b/packages/runner/src/suite.ts index e3ae628cbfce..0f5feb6b787d 100644 --- a/packages/runner/src/suite.ts +++ b/packages/runner/src/suite.ts @@ -1,4 +1,5 @@ import { format, isObject, objDisplay, objectAttr } from '@vitest/utils' +import { parseSingleStack } from '@vitest/utils/source-map' import type { Custom, CustomAPI, File, Fixtures, RunMode, Suite, SuiteAPI, SuiteCollector, SuiteFactory, SuiteHooks, Task, TaskCustomOptions, Test, TestAPI, TestFunction, TestOptions } from './types' import type { VitestRunner } from './types/runner' import { createChainable } from './utils/chain' @@ -25,19 +26,25 @@ export const it = test let runner: VitestRunner let defaultSuite: SuiteCollector +let currentTestFilepath: string export function getDefaultSuite() { return defaultSuite } +export function getTestFilepath() { + return currentTestFilepath +} + export function getRunner() { return runner } -export function clearCollectorContext(currentRunner: VitestRunner) { +export function clearCollectorContext(filepath: string, currentRunner: VitestRunner) { if (!defaultSuite) defaultSuite = currentRunner.config.sequence.shuffle ? suite.shuffle('') : currentRunner.config.sequence.concurrent ? suite.concurrent('') : suite('') runner = currentRunner + currentTestFilepath = filepath collectorContext.tasks.length = 0 defaultSuite.clear() collectorContext.currentSuite = defaultSuite @@ -103,7 +110,7 @@ function createSuiteCollector(name: string, factory: SuiteFactory = () => { }, m let suite: Suite - initSuite() + initSuite(true) const task = function (name = '', options: TaskCustomOptions = {}) { const task: Custom = { @@ -140,6 +147,17 @@ function createSuiteCollector(name: string, factory: SuiteFactory = () => { }, m )) } + if (runner.config.includeTaskLocation) { + const limit = Error.stackTraceLimit + // custom can be called from any place, let's assume the limit is 10 stacks + Error.stackTraceLimit = 10 + const error = new Error('stacktrace').stack! + Error.stackTraceLimit = limit + const stack = findStackTrace(error) + if (stack) + task.location = stack + } + tasks.push(task) return task } @@ -183,7 +201,7 @@ function createSuiteCollector(name: string, factory: SuiteFactory = () => { }, m getHooks(suite)[name].push(...fn as any) } - function initSuite() { + function initSuite(includeLocation: boolean) { if (typeof suiteOptions === 'number') suiteOptions = { timeout: suiteOptions } @@ -199,13 +217,27 @@ function createSuiteCollector(name: string, factory: SuiteFactory = () => { }, m projectName: '', } + if (runner && includeLocation && runner.config.includeTaskLocation) { + const limit = Error.stackTraceLimit + Error.stackTraceLimit = 5 + const error = new Error('stacktrace').stack! + Error.stackTraceLimit = limit + const stack = parseSingleStack(error.split('\n')[5]) + if (stack) { + suite.location = { + line: stack.line, + column: stack.column, + } + } + } + setHooks(suite, createSuiteHooks()) } function clear() { tasks.length = 0 factoryQueue.length = 0 - initSuite() + initSuite(false) } async function collect(file?: File) { @@ -397,3 +429,18 @@ function formatTemplateString(cases: any[], args: any[]): any[] { } return res } + +function findStackTrace(error: string) { + // first line is the error message + // and the first 3 stacks are always from the collector + const lines = error.split('\n').slice(4) + for (const line of lines) { + const stack = parseSingleStack(line) + if (stack && stack.file === getTestFilepath()) { + return { + line: stack.line, + column: stack.column, + } + } + } +} diff --git a/packages/runner/src/types/runner.ts b/packages/runner/src/types/runner.ts index 6822c4903c1c..ce4d18f7a942 100644 --- a/packages/runner/src/types/runner.ts +++ b/packages/runner/src/types/runner.ts @@ -33,6 +33,7 @@ export interface VitestRunnerConfig { testTimeout: number hookTimeout: number retry: number + includeTaskLocation?: boolean diffOptions?: DiffOptions } diff --git a/packages/runner/src/types/tasks.ts b/packages/runner/src/types/tasks.ts index e2ceddfe7a50..afb822cb1e4f 100644 --- a/packages/runner/src/types/tasks.ts +++ b/packages/runner/src/types/tasks.ts @@ -18,6 +18,10 @@ export interface TaskBase { result?: TaskResult retry?: number repeats?: number + location?: { + line: number + column: number + } } export interface TaskPopulated extends TaskBase { diff --git a/packages/vitest/src/api/setup.ts b/packages/vitest/src/api/setup.ts index fd5e15df4e39..9abd0b32b1af 100644 --- a/packages/vitest/src/api/setup.ts +++ b/packages/vitest/src/api/setup.ts @@ -113,6 +113,12 @@ export function setup(vitestOrWorkspace: Vitest | WorkspaceProject, _server?: Vi getConfig() { return vitestOrWorkspace.config }, + async getBrowserFileSourceMap(id) { + if (!('ctx' in vitestOrWorkspace)) + return undefined + const mod = vitestOrWorkspace.browser?.moduleGraph.getModuleById(id) + return mod?.transformResult?.map + }, async getTransformResult(id) { const result: TransformResultWithSource | null | undefined = await ctx.vitenode.transformRequest(id) if (result) { diff --git a/packages/vitest/src/api/types.ts b/packages/vitest/src/api/types.ts index b59c6dd151ff..4b47bedc7ed2 100644 --- a/packages/vitest/src/api/types.ts +++ b/packages/vitest/src/api/types.ts @@ -20,6 +20,7 @@ export interface WebSocketHandlers { resolveSnapshotPath: (testPath: string) => string resolveSnapshotRawPath: (testPath: string, rawPath: string) => string getModuleGraph: (id: string) => Promise + getBrowserFileSourceMap: (id: string) => Promise getTransformResult: (id: string) => Promise readSnapshotFile: (id: string) => Promise readTestFile: (id: string) => Promise diff --git a/packages/vitest/src/node/cli/cli-config.ts b/packages/vitest/src/node/cli/cli-config.ts index afb1ec4dc17e..622fb13f50a7 100644 --- a/packages/vitest/src/node/cli/cli-config.ts +++ b/packages/vitest/src/node/cli/cli-config.ts @@ -615,4 +615,5 @@ export const cliOptionsConfig: VitestCLIOptions = { poolMatchGlobs: null, deps: null, name: null, + includeTaskLocation: null, } diff --git a/packages/vitest/src/node/workspace.ts b/packages/vitest/src/node/workspace.ts index 0a973f9f00e6..b73f438ce4fa 100644 --- a/packages/vitest/src/node/workspace.ts +++ b/packages/vitest/src/node/workspace.ts @@ -382,6 +382,7 @@ export class WorkspaceProject { inspect: this.ctx.config.inspect, inspectBrk: this.ctx.config.inspectBrk, alias: [], + includeTaskLocation: this.config.includeTaskLocation ?? this.ctx.config.includeTaskLocation, }, this.ctx.configOverride || {} as any) as ResolvedConfig } diff --git a/packages/vitest/src/public/utils.ts b/packages/vitest/src/public/utils.ts index 7030fbb320bf..7947f1bff4f9 100644 --- a/packages/vitest/src/public/utils.ts +++ b/packages/vitest/src/public/utils.ts @@ -1 +1,5 @@ export * from '@vitest/utils' + +export function loadSourceMapUtils() { + return import('@vitest/utils/source-map') +} diff --git a/packages/vitest/src/types/config.ts b/packages/vitest/src/types/config.ts index 594ed36eae35..3dc2d42ef633 100644 --- a/packages/vitest/src/types/config.ts +++ b/packages/vitest/src/types/config.ts @@ -715,6 +715,13 @@ export interface InlineConfig { * @default false */ disableConsoleIntercept?: boolean + + /** + * Include "location" property inside the test definition + * + * @default false + */ + includeTaskLocation?: boolean } export interface TypecheckConfig { diff --git a/test/public-api/fixtures/vitest.config.ts b/test/public-api/fixtures/vitest.config.ts new file mode 100644 index 000000000000..56004c9f9e06 --- /dev/null +++ b/test/public-api/fixtures/vitest.config.ts @@ -0,0 +1 @@ +export default {} \ No newline at end of file diff --git a/test/public-api/package.json b/test/public-api/package.json index 36116b4d3ea6..3d4808f6a8eb 100644 --- a/test/public-api/package.json +++ b/test/public-api/package.json @@ -3,7 +3,8 @@ "type": "module", "private": true, "scripts": { - "test": "vitest" + "test": "vitest", + "fixtures": "vitest --root ./fixtures" }, "devDependencies": { "@vitest/browser": "workspace:*", diff --git a/test/public-api/tests/runner.spec.ts b/test/public-api/tests/runner.spec.ts index da2820fe1b15..e8160d4aa7f9 100644 --- a/test/public-api/tests/runner.spec.ts +++ b/test/public-api/tests/runner.spec.ts @@ -15,9 +15,10 @@ it.each([ headless: true, }, }, -] as UserConfig[])('passes down metadata when $name', async (config) => { +] as UserConfig[])('passes down metadata when $name', { timeout: 60_000, retry: 3 }, async (config) => { const taskUpdate: TaskResultPack[] = [] const finishedFiles: File[] = [] + const collectedFiles: File[] = [] const { vitest, stdout, stderr } = await runVitest({ root: resolve(__dirname, '..', 'fixtures'), include: ['**/*.spec.ts'], @@ -30,8 +31,12 @@ it.each([ onFinished(files) { finishedFiles.push(...files || []) }, + onCollected(files) { + collectedFiles.push(...files || []) + }, }, ], + includeTaskLocation: true, ...config, }) @@ -69,7 +74,17 @@ it.each([ expect(files[0].meta).toEqual(suiteMeta) expect(files[0].tasks[0].meta).toEqual(testMeta) -}, { - timeout: 60_000, - retry: 3, + + expect(finishedFiles[0].tasks[0].location).toEqual({ + line: 14, + column: 1, + }) + expect(collectedFiles[0].tasks[0].location).toEqual({ + line: 14, + column: 1, + }) + expect(files[0].tasks[0].location).toEqual({ + line: 14, + column: 1, + }) })