[READ-ONLY] Mirror of https://github.com/vitest-dev/vitest. Next generation testing framework powered by Vite. vitest.dev
test testing-tools vite
12

Configure Feed

Select the types of activity you want to include in your feed.

feat: add a flag to include test location in tasks (#5342)

authored by

Vladimir and committed by
GitHub
(Mar 14, 2024, 3:54 PM +0100) d627e209 0bea2247

+145 -12
+13
docs/config/index.md
··· 2108 2108 ::: tip 2109 2109 You can disable isolation for specific pools by using [`poolOptions`](#pooloptions) property. 2110 2110 ::: 2111 + 2112 + ### includeTaskLocation <Badge type="info">1.4.0+</Badge> {#includeTaskLocation} 2113 + 2114 + - **Type:** `boolean` 2115 + - **Default:** `false` 2116 + 2117 + 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. 2118 + 2119 + The `location` property has `column` and `line` values that correspond to the `test` or `describe` position in the original file. 2120 + 2121 + ::: tip 2122 + This option has no effect if you do not use custom code that relies on this. 2123 + :::
+2 -1
test/public-api/package.json
··· 3 3 "type": "module", 4 4 "private": true, 5 5 "scripts": { 6 - "test": "vitest" 6 + "test": "vitest", 7 + "fixtures": "vitest --root ./fixtures" 7 8 }, 8 9 "devDependencies": { 9 10 "@vitest/browser": "workspace:*",
+1 -1
packages/runner/src/collect.ts
··· 28 28 projectName: config.name, 29 29 } 30 30 31 - clearCollectorContext(runner) 31 + clearCollectorContext(filepath, runner) 32 32 33 33 try { 34 34 const setupStart = now()
+1 -1
packages/runner/src/run.ts
··· 393 393 394 394 const files = await collectTests(paths, runner) 395 395 396 - runner.onCollected?.(files) 396 + await runner.onCollected?.(files) 397 397 await runner.onBeforeRunFiles?.(files) 398 398 399 399 await runFiles(files, runner)
+51 -4
packages/runner/src/suite.ts
··· 1 1 import { format, isObject, objDisplay, objectAttr } from '@vitest/utils' 2 + import { parseSingleStack } from '@vitest/utils/source-map' 2 3 import type { Custom, CustomAPI, File, Fixtures, RunMode, Suite, SuiteAPI, SuiteCollector, SuiteFactory, SuiteHooks, Task, TaskCustomOptions, Test, TestAPI, TestFunction, TestOptions } from './types' 3 4 import type { VitestRunner } from './types/runner' 4 5 import { createChainable } from './utils/chain' ··· 25 26 26 27 let runner: VitestRunner 27 28 let defaultSuite: SuiteCollector 29 + let currentTestFilepath: string 28 30 29 31 export function getDefaultSuite() { 30 32 return defaultSuite 33 + } 34 + 35 + export function getTestFilepath() { 36 + return currentTestFilepath 31 37 } 32 38 33 39 export function getRunner() { 34 40 return runner 35 41 } 36 42 37 - export function clearCollectorContext(currentRunner: VitestRunner) { 43 + export function clearCollectorContext(filepath: string, currentRunner: VitestRunner) { 38 44 if (!defaultSuite) 39 45 defaultSuite = currentRunner.config.sequence.shuffle ? suite.shuffle('') : currentRunner.config.sequence.concurrent ? suite.concurrent('') : suite('') 40 46 runner = currentRunner 47 + currentTestFilepath = filepath 41 48 collectorContext.tasks.length = 0 42 49 defaultSuite.clear() 43 50 collectorContext.currentSuite = defaultSuite ··· 103 110 104 111 let suite: Suite 105 112 106 - initSuite() 113 + initSuite(true) 107 114 108 115 const task = function (name = '', options: TaskCustomOptions = {}) { 109 116 const task: Custom = { ··· 138 145 withFixtures(handler, context), 139 146 options?.timeout ?? runner.config.testTimeout, 140 147 )) 148 + } 149 + 150 + if (runner.config.includeTaskLocation) { 151 + const limit = Error.stackTraceLimit 152 + // custom can be called from any place, let's assume the limit is 10 stacks 153 + Error.stackTraceLimit = 10 154 + const error = new Error('stacktrace').stack! 155 + Error.stackTraceLimit = limit 156 + const stack = findStackTrace(error) 157 + if (stack) 158 + task.location = stack 141 159 } 142 160 143 161 tasks.push(task) ··· 183 201 getHooks(suite)[name].push(...fn as any) 184 202 } 185 203 186 - function initSuite() { 204 + function initSuite(includeLocation: boolean) { 187 205 if (typeof suiteOptions === 'number') 188 206 suiteOptions = { timeout: suiteOptions } 189 207 ··· 199 217 projectName: '', 200 218 } 201 219 220 + if (runner && includeLocation && runner.config.includeTaskLocation) { 221 + const limit = Error.stackTraceLimit 222 + Error.stackTraceLimit = 5 223 + const error = new Error('stacktrace').stack! 224 + Error.stackTraceLimit = limit 225 + const stack = parseSingleStack(error.split('\n')[5]) 226 + if (stack) { 227 + suite.location = { 228 + line: stack.line, 229 + column: stack.column, 230 + } 231 + } 232 + } 233 + 202 234 setHooks(suite, createSuiteHooks()) 203 235 } 204 236 205 237 function clear() { 206 238 tasks.length = 0 207 239 factoryQueue.length = 0 208 - initSuite() 240 + initSuite(false) 209 241 } 210 242 211 243 async function collect(file?: File) { ··· 396 428 res.push(oneCase) 397 429 } 398 430 return res 431 + } 432 + 433 + function findStackTrace(error: string) { 434 + // first line is the error message 435 + // and the first 3 stacks are always from the collector 436 + const lines = error.split('\n').slice(4) 437 + for (const line of lines) { 438 + const stack = parseSingleStack(line) 439 + if (stack && stack.file === getTestFilepath()) { 440 + return { 441 + line: stack.line, 442 + column: stack.column, 443 + } 444 + } 445 + } 399 446 }
+1
test/public-api/fixtures/vitest.config.ts
··· 1 + export default {}
+19 -4
test/public-api/tests/runner.spec.ts
··· 15 15 headless: true, 16 16 }, 17 17 }, 18 - ] as UserConfig[])('passes down metadata when $name', async (config) => { 18 + ] as UserConfig[])('passes down metadata when $name', { timeout: 60_000, retry: 3 }, async (config) => { 19 19 const taskUpdate: TaskResultPack[] = [] 20 20 const finishedFiles: File[] = [] 21 + const collectedFiles: File[] = [] 21 22 const { vitest, stdout, stderr } = await runVitest({ 22 23 root: resolve(__dirname, '..', 'fixtures'), 23 24 include: ['**/*.spec.ts'], ··· 30 31 onFinished(files) { 31 32 finishedFiles.push(...files || []) 32 33 }, 34 + onCollected(files) { 35 + collectedFiles.push(...files || []) 36 + }, 33 37 }, 34 38 ], 39 + includeTaskLocation: true, 35 40 ...config, 36 41 }) 37 42 ··· 69 74 70 75 expect(files[0].meta).toEqual(suiteMeta) 71 76 expect(files[0].tasks[0].meta).toEqual(testMeta) 72 - }, { 73 - timeout: 60_000, 74 - retry: 3, 77 + 78 + expect(finishedFiles[0].tasks[0].location).toEqual({ 79 + line: 14, 80 + column: 1, 81 + }) 82 + expect(collectedFiles[0].tasks[0].location).toEqual({ 83 + line: 14, 84 + column: 1, 85 + }) 86 + expect(files[0].tasks[0].location).toEqual({ 87 + line: 14, 88 + column: 1, 89 + }) 75 90 })
+32 -1
packages/browser/src/client/runner.ts
··· 55 55 } 56 56 } 57 57 58 - onCollected = (files: File[]): unknown => { 58 + onCollected = async (files: File[]): Promise<unknown> => { 59 + if (this.config.includeTaskLocation) { 60 + try { 61 + await updateFilesLocations(files) 62 + } 63 + catch (_) {} 64 + } 59 65 return rpc().onCollected(files) 60 66 } 61 67 ··· 106 112 runner.config.diffOptions = diffOptions 107 113 cachedRunner = runner 108 114 return runner 115 + } 116 + 117 + async function updateFilesLocations(files: File[]) { 118 + const { loadSourceMapUtils } = await importId('vitest/utils') as typeof import('vitest/utils') 119 + const { TraceMap, originalPositionFor } = await loadSourceMapUtils() 120 + 121 + const promises = files.map(async (file) => { 122 + const result = await rpc().getBrowserFileSourceMap(file.filepath) 123 + if (!result) 124 + return null 125 + const traceMap = new TraceMap(result as any) 126 + function updateLocation(task: Task) { 127 + if (task.location) { 128 + const { line, column } = originalPositionFor(traceMap, task.location) 129 + if (line != null && column != null) 130 + task.location = { line, column: column + 1 } 131 + } 132 + if ('tasks' in task) 133 + task.tasks.forEach(updateLocation) 134 + } 135 + file.tasks.forEach(updateLocation) 136 + return null 137 + }) 138 + 139 + await Promise.all(promises) 109 140 }
+1
packages/runner/src/types/runner.ts
··· 33 33 testTimeout: number 34 34 hookTimeout: number 35 35 retry: number 36 + includeTaskLocation?: boolean 36 37 diffOptions?: DiffOptions 37 38 } 38 39
+4
packages/runner/src/types/tasks.ts
··· 18 18 result?: TaskResult 19 19 retry?: number 20 20 repeats?: number 21 + location?: { 22 + line: number 23 + column: number 24 + } 21 25 } 22 26 23 27 export interface TaskPopulated extends TaskBase {
+6
packages/vitest/src/api/setup.ts
··· 113 113 getConfig() { 114 114 return vitestOrWorkspace.config 115 115 }, 116 + async getBrowserFileSourceMap(id) { 117 + if (!('ctx' in vitestOrWorkspace)) 118 + return undefined 119 + const mod = vitestOrWorkspace.browser?.moduleGraph.getModuleById(id) 120 + return mod?.transformResult?.map 121 + }, 116 122 async getTransformResult(id) { 117 123 const result: TransformResultWithSource | null | undefined = await ctx.vitenode.transformRequest(id) 118 124 if (result) {
+1
packages/vitest/src/api/types.ts
··· 20 20 resolveSnapshotPath: (testPath: string) => string 21 21 resolveSnapshotRawPath: (testPath: string, rawPath: string) => string 22 22 getModuleGraph: (id: string) => Promise<ModuleGraphData> 23 + getBrowserFileSourceMap: (id: string) => Promise<TransformResult['map'] | undefined> 23 24 getTransformResult: (id: string) => Promise<TransformResultWithSource | undefined> 24 25 readSnapshotFile: (id: string) => Promise<string | null> 25 26 readTestFile: (id: string) => Promise<string | null>
+1
packages/vitest/src/node/workspace.ts
··· 382 382 inspect: this.ctx.config.inspect, 383 383 inspectBrk: this.ctx.config.inspectBrk, 384 384 alias: [], 385 + includeTaskLocation: this.config.includeTaskLocation ?? this.ctx.config.includeTaskLocation, 385 386 }, this.ctx.configOverride || {} as any) as ResolvedConfig 386 387 } 387 388
+4
packages/vitest/src/public/utils.ts
··· 1 1 export * from '@vitest/utils' 2 + 3 + export function loadSourceMapUtils() { 4 + return import('@vitest/utils/source-map') 5 + }
+7
packages/vitest/src/types/config.ts
··· 715 715 * @default false 716 716 */ 717 717 disableConsoleIntercept?: boolean 718 + 719 + /** 720 + * Include "location" property inside the test definition 721 + * 722 + * @default false 723 + */ 724 + includeTaskLocation?: boolean 718 725 } 719 726 720 727 export interface TypecheckConfig {
+1
packages/vitest/src/node/cli/cli-config.ts
··· 615 615 poolMatchGlobs: null, 616 616 deps: null, 617 617 name: null, 618 + includeTaskLocation: null, 618 619 }