From 4d6b488bb9c74e16b213276483daeefbcb9c4893 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 16 Jul 2024 13:21:20 +0200 Subject: [PATCH 01/53] feat: introduce server tasks --- packages/vitest/src/node/server-tasks.ts | 153 +++++++++++++++++++++++ 1 file changed, 153 insertions(+) create mode 100644 packages/vitest/src/node/server-tasks.ts diff --git a/packages/vitest/src/node/server-tasks.ts b/packages/vitest/src/node/server-tasks.ts new file mode 100644 index 000000000000..cecb7cc1c20b --- /dev/null +++ b/packages/vitest/src/node/server-tasks.ts @@ -0,0 +1,153 @@ +import type { Custom, File as FileTask, Suite as SuiteTask, TaskMeta, TaskResult, Test } from '@vitest/runner' +import { getFullName } from '../utils' +import type { WorkspaceProject } from './workspace' + +const tasksMap = new WeakMap< + Test | Custom | FileTask | SuiteTask, + TestCase | File | Suite +>() + +class Task { + #fullName: string | undefined + #project: WorkspaceProject + + constructor( + public readonly task: Test | Custom | FileTask | SuiteTask, + project: WorkspaceProject, + ) { + this.#project = project + } + + public project(): WorkspaceProject { + return this.#project + } + + public file(): File { + return tasksMap.get(this.task.file) as File + } + + public parent(): Suite | File { + const suite = this.task.suite + if (suite) { + return tasksMap.get(suite) as Suite + } + return this.file() + } + + public get name(): string { + return this.task.name + } + + public get fullName(): string { + if (this.#fullName === undefined) { + this.#fullName = getFullName(this.task, ' > ') + } + return this.#fullName + } + + public get id(): string { + return this.task.id + } + + public get location(): { line: number; column: number } | undefined { + return this.task.location + } +} + +export class TestCase extends Task { + declare public readonly task: Test | Custom + public readonly type = 'test' + #options: TaskOptions | undefined + + public get meta(): TaskMeta { + return this.task.meta + } + + public options(): TaskOptions { + if (this.#options === undefined) { + this.#options = buildOptions(this.task) + // mode is the only one that can change dinamically + this.#options.mode = this.task.mode + } + return this.#options + } + + public diagnostic(): TestDiagnostic { + const result = (this.task.result || {} as TaskResult) + return { + heap: result.heap, + duration: result.duration, + startTime: result.startTime, + retryCount: result.retryCount, + repeatCount: result.repeatCount, + } + } +} + +export abstract class SuiteImplementation extends Task { + declare public readonly task: SuiteTask | FileTask + + public children(): (Suite | TestCase)[] { + return this.task.tasks.map((task) => { + const taskInstance = tasksMap.get(task) + if (!taskInstance) { + throw new Error(`Task instance was not found for task ${task.id}`) + } + return taskInstance as Suite | TestCase + }) + } +} + +export class Suite extends SuiteImplementation { + declare public readonly task: SuiteTask + public readonly type = 'suite' + #options: TaskOptions | undefined + + public options(): TaskOptions { + if (this.#options === undefined) { + this.#options = buildOptions(this.task) + } + return this.#options + } +} + +export class File extends SuiteImplementation { + declare public readonly task: FileTask + public readonly type = 'file' + + public get moduleId(): string { + return this.task.filepath + } + + public get location(): undefined { + return undefined + } +} + +export interface TaskOptions { + each: boolean | undefined + concurrent: boolean | undefined + shuffle: boolean | undefined + retry: number | undefined + repeats: number | undefined + mode: 'run' | 'only' | 'skip' | 'todo' +} + +function buildOptions(task: Test | Custom | FileTask | SuiteTask): TaskOptions { + return { + each: task.each, + concurrent: task.concurrent, + shuffle: task.shuffle, + retry: task.retry, + repeats: task.repeats, + mode: task.mode, + } +} + +export interface TestDiagnostic { + heap: number | undefined + duration: number | undefined + startTime: number | undefined + retryCount: number | undefined + repeatCount: number | undefined +} From 0b642145032783d13ce624526898ff3208792a3c Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 16 Jul 2024 13:29:08 +0200 Subject: [PATCH 02/53] fix: implement result for the TestCase --- packages/vitest/src/node/server-tasks.ts | 36 ++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/packages/vitest/src/node/server-tasks.ts b/packages/vitest/src/node/server-tasks.ts index cecb7cc1c20b..4140d7295412 100644 --- a/packages/vitest/src/node/server-tasks.ts +++ b/packages/vitest/src/node/server-tasks.ts @@ -1,5 +1,6 @@ import type { Custom, File as FileTask, Suite as SuiteTask, TaskMeta, TaskResult, Test } from '@vitest/runner' import { getFullName } from '../utils' +import type { ParsedStack } from '../types' import type { WorkspaceProject } from './workspace' const tasksMap = new WeakMap< @@ -59,6 +60,22 @@ export class TestCase extends Task { public readonly type = 'test' #options: TaskOptions | undefined + public result(): TestResult | undefined { + const result = this.task.result + if (!result) { + return undefined + } + const state = result.state === 'fail' + ? 'failed' + : result.state === 'pass' + ? 'passed' + : 'skipped' + return { + state, + errors: result.errors as TestError[] | undefined, + } + } + public get meta(): TaskMeta { return this.task.meta } @@ -144,6 +161,25 @@ function buildOptions(task: Test | Custom | FileTask | SuiteTask): TaskOptions { } } +interface SerialisedError { + message: string + stack?: string + name: string + stacks?: ParsedStack[] + [key: string]: unknown +} + +interface TestError extends SerialisedError { + diff?: string + actual?: string + expected?: string +} + +interface TestResult { + state: 'passed' | 'failed' | 'skipped' + errors?: TestError[] +} + export interface TestDiagnostic { heap: number | undefined duration: number | undefined From 962d6b876e27797321b3d85b8fc1d3048903f4d6 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 16 Jul 2024 13:29:51 +0200 Subject: [PATCH 03/53] chore: allow dynamic mode in options for tests (allow `context.skip()`) --- packages/vitest/src/node/server-tasks.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/vitest/src/node/server-tasks.ts b/packages/vitest/src/node/server-tasks.ts index 4140d7295412..152f1706c9d2 100644 --- a/packages/vitest/src/node/server-tasks.ts +++ b/packages/vitest/src/node/server-tasks.ts @@ -83,9 +83,9 @@ export class TestCase extends Task { public options(): TaskOptions { if (this.#options === undefined) { this.#options = buildOptions(this.task) - // mode is the only one that can change dinamically - this.#options.mode = this.task.mode } + // mode is the only one that can change dinamically + this.#options.mode = this.task.mode return this.#options } From ea28932dd43c9dc734471ffc73d1c1a06075b8c5 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 16 Jul 2024 13:34:51 +0200 Subject: [PATCH 04/53] chore: don't support parent in a file --- packages/vitest/src/node/server-tasks.ts | 26 ++++++++++++++++-------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/packages/vitest/src/node/server-tasks.ts b/packages/vitest/src/node/server-tasks.ts index 152f1706c9d2..875944553be0 100644 --- a/packages/vitest/src/node/server-tasks.ts +++ b/packages/vitest/src/node/server-tasks.ts @@ -27,14 +27,6 @@ class Task { return tasksMap.get(this.task.file) as File } - public parent(): Suite | File { - const suite = this.task.suite - if (suite) { - return tasksMap.get(suite) as Suite - } - return this.file() - } - public get name(): string { return this.task.name } @@ -57,9 +49,17 @@ class Task { export class TestCase extends Task { declare public readonly task: Test | Custom - public readonly type = 'test' + public readonly type: 'test' | 'custom' = 'test' #options: TaskOptions | undefined + public parent(): Suite | File { + const suite = this.task.suite + if (suite) { + return tasksMap.get(suite) as Suite + } + return this.file() + } + public result(): TestResult | undefined { const result = this.task.result if (!result) { @@ -104,6 +104,14 @@ export class TestCase extends Task { export abstract class SuiteImplementation extends Task { declare public readonly task: SuiteTask | FileTask + public parent(): Suite | File { + const suite = this.task.suite + if (suite) { + return tasksMap.get(suite) as Suite + } + return this.file() + } + public children(): (Suite | TestCase)[] { return this.task.tasks.map((task) => { const taskInstance = tasksMap.get(task) From 503b5060a4a961d697a582e0a25355ef3ba246a6 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 16 Jul 2024 13:35:34 +0200 Subject: [PATCH 05/53] chore: make meta dynamic --- packages/vitest/src/node/server-tasks.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vitest/src/node/server-tasks.ts b/packages/vitest/src/node/server-tasks.ts index 875944553be0..9ee2058a40ae 100644 --- a/packages/vitest/src/node/server-tasks.ts +++ b/packages/vitest/src/node/server-tasks.ts @@ -76,7 +76,7 @@ export class TestCase extends Task { } } - public get meta(): TaskMeta { + public meta(): TaskMeta { return this.task.meta } From 73519b40f3677a2f8460c57adc88bffb23297107 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 16 Jul 2024 13:53:21 +0200 Subject: [PATCH 06/53] chore: cleanup --- packages/vitest/src/node/server-tasks.ts | 76 +++++++++++++++++++++++- 1 file changed, 75 insertions(+), 1 deletion(-) diff --git a/packages/vitest/src/node/server-tasks.ts b/packages/vitest/src/node/server-tasks.ts index 9ee2058a40ae..4a1a71027254 100644 --- a/packages/vitest/src/node/server-tasks.ts +++ b/packages/vitest/src/node/server-tasks.ts @@ -12,25 +12,45 @@ class Task { #fullName: string | undefined #project: WorkspaceProject + /** + * Task instance. + * @experimental Public task API is experimental and does not follow semver. + */ + public readonly task: Test | Custom | FileTask | SuiteTask + constructor( - public readonly task: Test | Custom | FileTask | SuiteTask, + task: Test | Custom | FileTask | SuiteTask, project: WorkspaceProject, ) { + this.task = task this.#project = project } + /** + * Current task's project. + * @experimental Public project API is experimental and does not follow semver. + */ public project(): WorkspaceProject { return this.#project } + /** + * Direct reference to the file task where the test or suite is defined. + */ public file(): File { return tasksMap.get(this.task.file) as File } + /** + * Name of the test or the suite. + */ public get name(): string { return this.task.name } + /** + * Full name of the test or the suite including all parent suites separated with `>`. + */ public get fullName(): string { if (this.#fullName === undefined) { this.#fullName = getFullName(this.task, ' > ') @@ -38,10 +58,19 @@ class Task { return this.#fullName } + /** + * Unique identifier. + * This ID is deterministic and will be the same for the same test across multiple runs. + * The ID is based on the file path and test position. + */ public get id(): string { return this.task.id } + /** + * Location in the file where the test or suite was defined. + * Locations are collected only if `includeTaskLocation` is enabled in the config. + */ public get location(): { line: number; column: number } | undefined { return this.task.location } @@ -52,6 +81,9 @@ export class TestCase extends Task { public readonly type: 'test' | 'custom' = 'test' #options: TaskOptions | undefined + /** + * Parent suite of the test. If test was called directly inside the file, the parent will be the file. + */ public parent(): Suite | File { const suite = this.task.suite if (suite) { @@ -60,6 +92,9 @@ export class TestCase extends Task { return this.file() } + /** + * Result of the test. Will be `undefined` if test is not finished yet or was just collected. + */ public result(): TestResult | undefined { const result = this.task.result if (!result) { @@ -76,10 +111,16 @@ export class TestCase extends Task { } } + /** + * Custom metadata that was attached to the test during its execution. + */ public meta(): TaskMeta { return this.task.meta } + /** + * Options that the test was initiated with. + */ public options(): TaskOptions { if (this.#options === undefined) { this.#options = buildOptions(this.task) @@ -89,6 +130,9 @@ export class TestCase extends Task { return this.#options } + /** + * Useful information about the test like duration, memory usage, etc. + */ public diagnostic(): TestDiagnostic { const result = (this.task.result || {} as TaskResult) return { @@ -104,6 +148,9 @@ export class TestCase extends Task { export abstract class SuiteImplementation extends Task { declare public readonly task: SuiteTask | FileTask + /** + * Parent suite. If suite was called directly inside the file, the parent will be the file. + */ public parent(): Suite | File { const suite = this.task.suite if (suite) { @@ -112,6 +159,9 @@ export abstract class SuiteImplementation extends Task { return this.file() } + /** + * An array of suites and tests that are part of this suite. + */ public children(): (Suite | TestCase)[] { return this.task.tasks.map((task) => { const taskInstance = tasksMap.get(task) @@ -121,6 +171,22 @@ export abstract class SuiteImplementation extends Task { return taskInstance as Suite | TestCase }) } + + /** + * An array of all tests that are part of this suite and its children. + */ + public tests(): TestCase[] { + const tests: TestCase[] = [] + for (const child of this.children()) { + if (child.type === 'test' || child.type === 'custom') { + tests.push(child as TestCase) + } + else { + tests.push(...(child as Suite).tests()) + } + } + return tests + } } export class Suite extends SuiteImplementation { @@ -128,6 +194,9 @@ export class Suite extends SuiteImplementation { public readonly type = 'suite' #options: TaskOptions | undefined + /** + * Options that suite was initiated with. + */ public options(): TaskOptions { if (this.#options === undefined) { this.#options = buildOptions(this.task) @@ -140,6 +209,11 @@ export class File extends SuiteImplementation { declare public readonly task: FileTask public readonly type = 'file' + /** + * This is usually an absolute UNIX file path. + * It can be a virtual id if the file is not on the disk. + * This value corresponds to Vite's `ModuleGraph` id. + */ public get moduleId(): string { return this.task.filepath } From e79abce1f6d63fe10eac55ab6d02cc6e9a9eb323 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 16 Jul 2024 14:06:45 +0200 Subject: [PATCH 07/53] refactor: better naming --- packages/vitest/src/node/server-tasks.ts | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/vitest/src/node/server-tasks.ts b/packages/vitest/src/node/server-tasks.ts index 4a1a71027254..373fa6ce04fa 100644 --- a/packages/vitest/src/node/server-tasks.ts +++ b/packages/vitest/src/node/server-tasks.ts @@ -5,7 +5,7 @@ import type { WorkspaceProject } from './workspace' const tasksMap = new WeakMap< Test | Custom | FileTask | SuiteTask, - TestCase | File | Suite + TestCase | TestFile | TestSuite >() class Task { @@ -37,8 +37,8 @@ class Task { /** * Direct reference to the file task where the test or suite is defined. */ - public file(): File { - return tasksMap.get(this.task.file) as File + public file(): TestFile { + return tasksMap.get(this.task.file) as TestFile } /** @@ -84,10 +84,10 @@ export class TestCase extends Task { /** * Parent suite of the test. If test was called directly inside the file, the parent will be the file. */ - public parent(): Suite | File { + public parent(): TestSuite | TestFile { const suite = this.task.suite if (suite) { - return tasksMap.get(suite) as Suite + return tasksMap.get(suite) as TestSuite } return this.file() } @@ -151,10 +151,10 @@ export abstract class SuiteImplementation extends Task { /** * Parent suite. If suite was called directly inside the file, the parent will be the file. */ - public parent(): Suite | File { + public parent(): TestSuite | TestFile { const suite = this.task.suite if (suite) { - return tasksMap.get(suite) as Suite + return tasksMap.get(suite) as TestSuite } return this.file() } @@ -162,13 +162,13 @@ export abstract class SuiteImplementation extends Task { /** * An array of suites and tests that are part of this suite. */ - public children(): (Suite | TestCase)[] { + public children(): (TestSuite | TestCase)[] { return this.task.tasks.map((task) => { const taskInstance = tasksMap.get(task) if (!taskInstance) { throw new Error(`Task instance was not found for task ${task.id}`) } - return taskInstance as Suite | TestCase + return taskInstance as TestSuite | TestCase }) } @@ -182,14 +182,14 @@ export abstract class SuiteImplementation extends Task { tests.push(child as TestCase) } else { - tests.push(...(child as Suite).tests()) + tests.push(...(child as TestSuite).tests()) } } return tests } } -export class Suite extends SuiteImplementation { +export class TestSuite extends SuiteImplementation { declare public readonly task: SuiteTask public readonly type = 'suite' #options: TaskOptions | undefined @@ -205,7 +205,7 @@ export class Suite extends SuiteImplementation { } } -export class File extends SuiteImplementation { +export class TestFile extends SuiteImplementation { declare public readonly task: FileTask public readonly type = 'file' From 83138c2f24a164499bd92f7ee5a4611907742717 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 16 Jul 2024 14:16:31 +0200 Subject: [PATCH 08/53] chore: make children and tests iterators --- packages/vitest/src/node/server-tasks.ts | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/packages/vitest/src/node/server-tasks.ts b/packages/vitest/src/node/server-tasks.ts index 373fa6ce04fa..adc796998279 100644 --- a/packages/vitest/src/node/server-tasks.ts +++ b/packages/vitest/src/node/server-tasks.ts @@ -162,30 +162,28 @@ export abstract class SuiteImplementation extends Task { /** * An array of suites and tests that are part of this suite. */ - public children(): (TestSuite | TestCase)[] { - return this.task.tasks.map((task) => { + public *children(): Iterable { + for (const task of this.task.tasks) { const taskInstance = tasksMap.get(task) if (!taskInstance) { throw new Error(`Task instance was not found for task ${task.id}`) } - return taskInstance as TestSuite | TestCase - }) + yield taskInstance as TestSuite | TestCase + } } /** * An array of all tests that are part of this suite and its children. */ - public tests(): TestCase[] { - const tests: TestCase[] = [] + public *tests(): Iterable { for (const child of this.children()) { if (child.type === 'test' || child.type === 'custom') { - tests.push(child as TestCase) + yield child as TestCase } else { - tests.push(...(child as TestSuite).tests()) + yield * (child as TestSuite).tests() } } - return tests } } From 1ac4469c78adf343801c1123544f7462f497677f Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 16 Jul 2024 14:22:38 +0200 Subject: [PATCH 09/53] chore: cleanup --- packages/vitest/src/node/server-tasks.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/vitest/src/node/server-tasks.ts b/packages/vitest/src/node/server-tasks.ts index adc796998279..afbab57aed5d 100644 --- a/packages/vitest/src/node/server-tasks.ts +++ b/packages/vitest/src/node/server-tasks.ts @@ -177,11 +177,11 @@ export abstract class SuiteImplementation extends Task { */ public *tests(): Iterable { for (const child of this.children()) { - if (child.type === 'test' || child.type === 'custom') { - yield child as TestCase + if (child.type === 'suite') { + yield * child.tests() } else { - yield * (child as TestSuite).tests() + yield child } } } From e22012bbb0a98827c360eaa85f890ced1822c13b Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 16 Jul 2024 14:53:48 +0200 Subject: [PATCH 10/53] chore: add findTest/findSuite methods --- packages/vitest/src/node/server-tasks.ts | 42 ++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/packages/vitest/src/node/server-tasks.ts b/packages/vitest/src/node/server-tasks.ts index afbab57aed5d..e4e27b6e5900 100644 --- a/packages/vitest/src/node/server-tasks.ts +++ b/packages/vitest/src/node/server-tasks.ts @@ -148,6 +148,48 @@ export class TestCase extends Task { export abstract class SuiteImplementation extends Task { declare public readonly task: SuiteTask | FileTask + /** + * Looks for a test by `name`. + * If `name` is a string, it will look for an exact match. + */ + public findTest(name: string | RegExp): TestCase | undefined { + if (typeof name === 'string') { + for (const test of this.tests()) { + if (test.name === name) { + return test + } + } + } + else { + for (const test of this.tests()) { + if (name.test(test.name)) { + return test + } + } + } + } + + /** + * Looks for a test by `name`. + * If `name` is a string, it will look for an exact match. + */ + public findSuite(name: string | RegExp): TestSuite | undefined { + if (typeof name === 'string') { + for (const suite of this.children()) { + if (suite.type === 'suite' && suite.name === name) { + return suite + } + } + } + else { + for (const suite of this.children()) { + if (suite.type === 'suite' && name.test(suite.name)) { + return suite + } + } + } + } + /** * Parent suite. If suite was called directly inside the file, the parent will be the file. */ From 03c96d0091a209bd411ceea546d2aa37e7a0be5c Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 16 Jul 2024 14:55:05 +0200 Subject: [PATCH 11/53] chore: cleanup --- packages/vitest/src/node/server-tasks.ts | 33 +++++++----------------- 1 file changed, 10 insertions(+), 23 deletions(-) diff --git a/packages/vitest/src/node/server-tasks.ts b/packages/vitest/src/node/server-tasks.ts index e4e27b6e5900..c6c077381653 100644 --- a/packages/vitest/src/node/server-tasks.ts +++ b/packages/vitest/src/node/server-tasks.ts @@ -153,18 +153,10 @@ export abstract class SuiteImplementation extends Task { * If `name` is a string, it will look for an exact match. */ public findTest(name: string | RegExp): TestCase | undefined { - if (typeof name === 'string') { - for (const test of this.tests()) { - if (test.name === name) { - return test - } - } - } - else { - for (const test of this.tests()) { - if (name.test(test.name)) { - return test - } + const isString = typeof name === 'string' + for (const test of this.tests()) { + if (test.name === name || (!isString && name.test(test.name))) { + return test } } } @@ -174,18 +166,13 @@ export abstract class SuiteImplementation extends Task { * If `name` is a string, it will look for an exact match. */ public findSuite(name: string | RegExp): TestSuite | undefined { - if (typeof name === 'string') { - for (const suite of this.children()) { - if (suite.type === 'suite' && suite.name === name) { - return suite - } + const isString = typeof name === 'string' + for (const suite of this.children()) { + if (suite.type !== 'suite') { + continue } - } - else { - for (const suite of this.children()) { - if (suite.type === 'suite' && name.test(suite.name)) { - return suite - } + if (suite.name === name || (!isString && name.test(suite.name))) { + return suite } } } From 1a74c2fb8e779a90ebdc06b20312cc8abbc21ff6 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 16 Jul 2024 15:03:59 +0200 Subject: [PATCH 12/53] feat: allow filtering tests by state --- packages/vitest/src/node/server-tasks.ts | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/packages/vitest/src/node/server-tasks.ts b/packages/vitest/src/node/server-tasks.ts index c6c077381653..d894ae1b7cc7 100644 --- a/packages/vitest/src/node/server-tasks.ts +++ b/packages/vitest/src/node/server-tasks.ts @@ -204,13 +204,24 @@ export abstract class SuiteImplementation extends Task { /** * An array of all tests that are part of this suite and its children. */ - public *tests(): Iterable { + public *tests(state?: TestResult['state'] | 'running'): Iterable { for (const child of this.children()) { if (child.type === 'suite') { - yield * child.tests() + yield * child.tests(state) } else { - yield child + if (state) { + const result = child.result() + if (!result && state === 'running') { + yield child + } + else if (result && result.state === state) { + yield child + } + } + else { + yield child + } } } } From df3e04bfe9647033663afebef9b9a9fc9c805535 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 16 Jul 2024 15:06:37 +0200 Subject: [PATCH 13/53] refactor: cleanup --- packages/vitest/src/node/server-tasks.ts | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/packages/vitest/src/node/server-tasks.ts b/packages/vitest/src/node/server-tasks.ts index d894ae1b7cc7..cda18b256286 100644 --- a/packages/vitest/src/node/server-tasks.ts +++ b/packages/vitest/src/node/server-tasks.ts @@ -209,20 +209,18 @@ export abstract class SuiteImplementation extends Task { if (child.type === 'suite') { yield * child.tests(state) } - else { - if (state) { - const result = child.result() - if (!result && state === 'running') { - yield child - } - else if (result && result.state === state) { - yield child - } + else if (state) { + const result = child.result() + if (!result && state === 'running') { + yield child } - else { + else if (result && result.state === state) { yield child } } + else { + yield child + } } } } From 0f74fcde364986896e314d02b95bf34ee6ca58c1 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 16 Jul 2024 15:46:13 +0200 Subject: [PATCH 14/53] refactor: make parent and file getters --- packages/vitest/src/node/server-tasks.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/vitest/src/node/server-tasks.ts b/packages/vitest/src/node/server-tasks.ts index cda18b256286..bf5371a1fef3 100644 --- a/packages/vitest/src/node/server-tasks.ts +++ b/packages/vitest/src/node/server-tasks.ts @@ -30,14 +30,14 @@ class Task { * Current task's project. * @experimental Public project API is experimental and does not follow semver. */ - public project(): WorkspaceProject { + public get project(): WorkspaceProject { return this.#project } /** * Direct reference to the file task where the test or suite is defined. */ - public file(): TestFile { + public get file(): TestFile { return tasksMap.get(this.task.file) as TestFile } @@ -84,12 +84,12 @@ export class TestCase extends Task { /** * Parent suite of the test. If test was called directly inside the file, the parent will be the file. */ - public parent(): TestSuite | TestFile { + public get parent(): TestSuite | TestFile { const suite = this.task.suite if (suite) { return tasksMap.get(suite) as TestSuite } - return this.file() + return this.file } /** @@ -180,12 +180,12 @@ export abstract class SuiteImplementation extends Task { /** * Parent suite. If suite was called directly inside the file, the parent will be the file. */ - public parent(): TestSuite | TestFile { + public get parent(): TestSuite | TestFile { const suite = this.task.suite if (suite) { return tasksMap.get(suite) as TestSuite } - return this.file() + return this.file } /** From b317187328c681f601bea50a55d848fdeb2855ea Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 16 Jul 2024 15:47:49 +0200 Subject: [PATCH 15/53] chore: add a note for function/getter --- packages/vitest/src/node/server-tasks.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/vitest/src/node/server-tasks.ts b/packages/vitest/src/node/server-tasks.ts index bf5371a1fef3..50bb0bc808c7 100644 --- a/packages/vitest/src/node/server-tasks.ts +++ b/packages/vitest/src/node/server-tasks.ts @@ -3,6 +3,10 @@ import { getFullName } from '../utils' import type { ParsedStack } from '../types' import type { WorkspaceProject } from './workspace' +// rule for function/getter +// getter is a readonly property that doesn't change in time +// method can return different objects depending on when it's called + const tasksMap = new WeakMap< Test | Custom | FileTask | SuiteTask, TestCase | TestFile | TestSuite From b3346b1cb91386ee9f112a5617a420c0c344b797 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 16 Jul 2024 15:58:00 +0200 Subject: [PATCH 16/53] refactor: make class shape predictable --- packages/vitest/src/node/server-tasks.ts | 114 +++++++++++------------ 1 file changed, 55 insertions(+), 59 deletions(-) diff --git a/packages/vitest/src/node/server-tasks.ts b/packages/vitest/src/node/server-tasks.ts index 50bb0bc808c7..b72738b110be 100644 --- a/packages/vitest/src/node/server-tasks.ts +++ b/packages/vitest/src/node/server-tasks.ts @@ -14,7 +14,6 @@ const tasksMap = new WeakMap< class Task { #fullName: string | undefined - #project: WorkspaceProject /** * Task instance. @@ -22,34 +21,40 @@ class Task { */ public readonly task: Test | Custom | FileTask | SuiteTask - constructor( - task: Test | Custom | FileTask | SuiteTask, - project: WorkspaceProject, - ) { - this.task = task - this.#project = project - } - /** * Current task's project. * @experimental Public project API is experimental and does not follow semver. */ - public get project(): WorkspaceProject { - return this.#project - } - + public readonly project: WorkspaceProject /** - * Direct reference to the file task where the test or suite is defined. + * Direct reference to the test file where the test or suite is defined. */ - public get file(): TestFile { - return tasksMap.get(this.task.file) as TestFile - } - + public readonly file: TestFile /** * Name of the test or the suite. */ - public get name(): string { - return this.task.name + public readonly name: string + /** + * Unique identifier. + * This ID is deterministic and will be the same for the same test across multiple runs. + * The ID is based on the file path and test position. + */ + public readonly id: string + /** + * Full name of the test or the suite including all parent suites separated with `>`. + */ + public readonly location: { line: number; column: number } | undefined + + constructor( + task: Test | Custom | FileTask | SuiteTask, + project: WorkspaceProject, + ) { + this.task = task + this.project = project + this.file = tasksMap.get(task.file) as TestFile + this.name = task.name + this.id = task.id + this.location = task.location } /** @@ -61,39 +66,27 @@ class Task { } return this.#fullName } - - /** - * Unique identifier. - * This ID is deterministic and will be the same for the same test across multiple runs. - * The ID is based on the file path and test position. - */ - public get id(): string { - return this.task.id - } - - /** - * Location in the file where the test or suite was defined. - * Locations are collected only if `includeTaskLocation` is enabled in the config. - */ - public get location(): { line: number; column: number } | undefined { - return this.task.location - } } export class TestCase extends Task { declare public readonly task: Test | Custom public readonly type: 'test' | 'custom' = 'test' #options: TaskOptions | undefined - /** - * Parent suite of the test. If test was called directly inside the file, the parent will be the file. + * Parent suite. If suite was called directly inside the file, the parent will be the file. */ - public get parent(): TestSuite | TestFile { + public readonly parent: TestSuite | TestFile + + constructor(task: SuiteTask | FileTask, project: WorkspaceProject) { + super(task, project) + const suite = this.task.suite if (suite) { - return tasksMap.get(suite) as TestSuite + this.parent = tasksMap.get(suite) as TestSuite + } + else { + this.parent = this.file } - return this.file } /** @@ -151,6 +144,22 @@ export class TestCase extends Task { export abstract class SuiteImplementation extends Task { declare public readonly task: SuiteTask | FileTask + /** + * Parent suite. If suite was called directly inside the file, the parent will be the file. + */ + public readonly parent: TestSuite | TestFile + + constructor(task: SuiteTask | FileTask, project: WorkspaceProject) { + super(task, project) + + const suite = this.task.suite + if (suite) { + this.parent = tasksMap.get(suite) as TestSuite + } + else { + this.parent = this.file + } + } /** * Looks for a test by `name`. @@ -181,17 +190,6 @@ export abstract class SuiteImplementation extends Task { } } - /** - * Parent suite. If suite was called directly inside the file, the parent will be the file. - */ - public get parent(): TestSuite | TestFile { - const suite = this.task.suite - if (suite) { - return tasksMap.get(suite) as TestSuite - } - return this.file - } - /** * An array of suites and tests that are part of this suite. */ @@ -248,18 +246,16 @@ export class TestSuite extends SuiteImplementation { export class TestFile extends SuiteImplementation { declare public readonly task: FileTask public readonly type = 'file' - /** * This is usually an absolute UNIX file path. * It can be a virtual id if the file is not on the disk. * This value corresponds to Vite's `ModuleGraph` id. */ - public get moduleId(): string { - return this.task.filepath - } + public readonly moduleId: string - public get location(): undefined { - return undefined + constructor(task: FileTask, project: WorkspaceProject) { + super(task, project) + this.moduleId = task.filepath } } From 53814cd544ec8a514d2c2f0e587e200bc35721ca Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 16 Jul 2024 16:01:14 +0200 Subject: [PATCH 17/53] chore: don't export SuiteImplementation --- packages/vitest/src/node/server-tasks.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vitest/src/node/server-tasks.ts b/packages/vitest/src/node/server-tasks.ts index b72738b110be..791b0e92573e 100644 --- a/packages/vitest/src/node/server-tasks.ts +++ b/packages/vitest/src/node/server-tasks.ts @@ -142,7 +142,7 @@ export class TestCase extends Task { } } -export abstract class SuiteImplementation extends Task { +abstract class SuiteImplementation extends Task { declare public readonly task: SuiteTask | FileTask /** * Parent suite. If suite was called directly inside the file, the parent will be the file. From f0f99ef11c185c263c9d7f87ba37a923ac81bd3e Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 16 Jul 2024 19:11:28 +0200 Subject: [PATCH 18/53] feat: task collection --- packages/vitest/src/node/server-tasks.ts | 155 +++++++++++++---------- 1 file changed, 86 insertions(+), 69 deletions(-) diff --git a/packages/vitest/src/node/server-tasks.ts b/packages/vitest/src/node/server-tasks.ts index 791b0e92573e..319bfb4fb71e 100644 --- a/packages/vitest/src/node/server-tasks.ts +++ b/packages/vitest/src/node/server-tasks.ts @@ -1,4 +1,11 @@ -import type { Custom, File as FileTask, Suite as SuiteTask, TaskMeta, TaskResult, Test } from '@vitest/runner' +import type { + Custom, + File as FileTask, + Suite as SuiteTask, + TaskMeta, + TaskResult, + Test, +} from '@vitest/runner' import { getFullName } from '../utils' import type { ParsedStack } from '../types' import type { WorkspaceProject } from './workspace' @@ -71,7 +78,10 @@ class Task { export class TestCase extends Task { declare public readonly task: Test | Custom public readonly type: 'test' | 'custom' = 'test' - #options: TaskOptions | undefined + /** + * Options that the test was initiated with. + */ + public readonly options: TaskOptions /** * Parent suite. If suite was called directly inside the file, the parent will be the file. */ @@ -87,6 +97,7 @@ export class TestCase extends Task { else { this.parent = this.file } + this.options = buildOptions(task) } /** @@ -115,18 +126,6 @@ export class TestCase extends Task { return this.task.meta } - /** - * Options that the test was initiated with. - */ - public options(): TaskOptions { - if (this.#options === undefined) { - this.#options = buildOptions(this.task) - } - // mode is the only one that can change dinamically - this.#options.mode = this.task.mode - return this.#options - } - /** * Useful information about the test like duration, memory usage, etc. */ @@ -142,74 +141,59 @@ export class TestCase extends Task { } } -abstract class SuiteImplementation extends Task { - declare public readonly task: SuiteTask | FileTask - /** - * Parent suite. If suite was called directly inside the file, the parent will be the file. - */ - public readonly parent: TestSuite | TestFile +class TaskCollection { + #task: SuiteTask | FileTask - constructor(task: SuiteTask | FileTask, project: WorkspaceProject) { - super(task, project) - - const suite = this.task.suite - if (suite) { - this.parent = tasksMap.get(suite) as TestSuite - } - else { - this.parent = this.file - } + constructor(task: SuiteTask | FileTask) { + this.#task = task } /** - * Looks for a test by `name`. - * If `name` is a string, it will look for an exact match. + * The same collection, but in an array form for easier manipulation. */ - public findTest(name: string | RegExp): TestCase | undefined { - const isString = typeof name === 'string' - for (const test of this.tests()) { - if (test.name === name || (!isString && name.test(test.name))) { - return test - } - } + array(): (TestCase | TestSuite)[] { + return Array.from(this) } /** - * Looks for a test by `name`. - * If `name` is a string, it will look for an exact match. + * Iterates over all tests and suites in the collection. */ - public findSuite(name: string | RegExp): TestSuite | undefined { - const isString = typeof name === 'string' - for (const suite of this.children()) { - if (suite.type !== 'suite') { - continue - } - if (suite.name === name || (!isString && name.test(suite.name))) { - return suite - } - } + *values(): IterableIterator { + return this[Symbol.iterator]() } /** - * An array of suites and tests that are part of this suite. + * Looks for a test or a suite by `name` inside that suite and its children. + * If `name` is a string, it will look for an exact match. */ - public *children(): Iterable { - for (const task of this.task.tasks) { - const taskInstance = tasksMap.get(task) - if (!taskInstance) { - throw new Error(`Task instance was not found for task ${task.id}`) + find(type: 'test', name: string | RegExp): TestCase | undefined + find(type: 'suite', name: string | RegExp): TestSuite | undefined + find(type: 'test' | 'suite', name: string | RegExp): TestCase | TestSuite | undefined + find(type: 'test' | 'suite', name: string | RegExp): TestCase | TestSuite | undefined { + const isString = typeof name === 'string' + for (const task of this) { + if (task.type === type) { + if (task.name === name || (!isString && name.test(task.name))) { + return task + } + if (task.type === 'suite') { + const result = task.children.find(type, name) + if (result) { + return result + } + } } - yield taskInstance as TestSuite | TestCase } } /** - * An array of all tests that are part of this suite and its children. + * Returns all tests that are part of this suite and its children. + * If you only need tests of the current suite, you can iterate over the collection directly and filter by type. */ - public *tests(state?: TestResult['state'] | 'running'): Iterable { - for (const child of this.children()) { + *tests(state?: TestResult['state'] | 'running'): IterableIterator { + for (const child of this) { if (child.type === 'suite') { - yield * child.tests(state) + yield * child.children.tests(state) } else if (state) { const result = child.result() @@ -225,21 +209,54 @@ abstract class SuiteImplementation extends Task { } } } + + *[Symbol.iterator](): IterableIterator { + for (const task of this.#task.tasks) { + const taskInstance = tasksMap.get(task) + if (!taskInstance) { + throw new Error(`Task instance was not found for task ${task.id}`) + } + yield taskInstance as TestSuite | TestCase + } + } +} + +abstract class SuiteImplementation extends Task { + declare public readonly task: SuiteTask | FileTask + /** + * Parent suite. If suite was called directly inside the file, the parent will be the file. + */ + public readonly parent: TestSuite | TestFile + /** + * Collection of suites and tests that are part of this suite. + */ + public readonly children: TaskCollection + + constructor(task: SuiteTask | FileTask, project: WorkspaceProject) { + super(task, project) + + const suite = this.task.suite + if (suite) { + this.parent = tasksMap.get(suite) as TestSuite + } + else { + this.parent = this.file + } + this.children = new TaskCollection(task) + } } export class TestSuite extends SuiteImplementation { declare public readonly task: SuiteTask public readonly type = 'suite' - #options: TaskOptions | undefined - /** * Options that suite was initiated with. */ - public options(): TaskOptions { - if (this.#options === undefined) { - this.#options = buildOptions(this.task) - } - return this.#options + public readonly options: TaskOptions + + constructor(task: SuiteTask, project: WorkspaceProject) { + super(task, project) + this.options = buildOptions(task) } } From e68ed5098472ee789ab268fe74f7ed17613095ef Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 16 Jul 2024 19:22:06 +0200 Subject: [PATCH 19/53] chore: fix find lookup --- packages/vitest/src/node/server-tasks.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/vitest/src/node/server-tasks.ts b/packages/vitest/src/node/server-tasks.ts index 319bfb4fb71e..8d57dc1934ae 100644 --- a/packages/vitest/src/node/server-tasks.ts +++ b/packages/vitest/src/node/server-tasks.ts @@ -176,11 +176,11 @@ class TaskCollection { if (task.name === name || (!isString && name.test(task.name))) { return task } - if (task.type === 'suite') { - const result = task.children.find(type, name) - if (result) { - return result - } + } + if (task.type === 'suite') { + const result = task.children.find(type, name) + if (result) { + return result } } } From 8cad39a9b8b6f7802e21c60ecd5a32afec1b96d3 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 16 Jul 2024 19:33:49 +0200 Subject: [PATCH 20/53] feat: push deep*, suites methods --- packages/vitest/src/node/server-tasks.ts | 76 ++++++++++++++++++++---- 1 file changed, 66 insertions(+), 10 deletions(-) diff --git a/packages/vitest/src/node/server-tasks.ts b/packages/vitest/src/node/server-tasks.ts index 8d57dc1934ae..e94009d0bbb6 100644 --- a/packages/vitest/src/node/server-tasks.ts +++ b/packages/vitest/src/node/server-tasks.ts @@ -163,13 +163,31 @@ class TaskCollection { } /** - * Looks for a test or a suite by `name` inside that suite and its children. + * Looks for a test or a suite by `name` only inside the current suite. * If `name` is a string, it will look for an exact match. */ find(type: 'test', name: string | RegExp): TestCase | undefined find(type: 'suite', name: string | RegExp): TestSuite | undefined find(type: 'test' | 'suite', name: string | RegExp): TestCase | TestSuite | undefined find(type: 'test' | 'suite', name: string | RegExp): TestCase | TestSuite | undefined { + const isString = typeof name === 'string' + for (const task of this) { + if (task.type === type) { + if (task.name === name || (!isString && name.test(task.name))) { + return task + } + } + } + } + + /** + * Looks for a test or a suite by `name` inside the current suite and its children. + * If `name` is a string, it will look for an exact match. + */ + deepFind(type: 'test', name: string | RegExp): TestCase | undefined + deepFind(type: 'suite', name: string | RegExp): TestSuite | undefined + deepFind(type: 'test' | 'suite', name: string | RegExp): TestCase | TestSuite | undefined + deepFind(type: 'test' | 'suite', name: string | RegExp): TestCase | TestSuite | undefined { const isString = typeof name === 'string' for (const task of this) { if (task.type === type) { @@ -178,7 +196,7 @@ class TaskCollection { } } if (task.type === 'suite') { - const result = task.children.find(type, name) + const result = task.children.deepFind(type, name) if (result) { return result } @@ -187,29 +205,62 @@ class TaskCollection { } /** - * Returns all tests that are part of this suite and its children. - * If you only need tests of the current suite, you can iterate over the collection directly and filter by type. + * Filters all tests that are part of this collection's suite and its children. */ - *tests(state?: TestResult['state'] | 'running'): IterableIterator { + *deepTests(state?: TestResult['state'] | 'running'): IterableIterator { for (const child of this) { if (child.type === 'suite') { - yield * child.children.tests(state) + yield * child.children.deepTests(state) } else if (state) { - const result = child.result() - if (!result && state === 'running') { + const testState = getTestState(child) + if (state === testState) { yield child } - else if (result && result.state === state) { + } + else { + yield child + } + } + } + + /** + * Filters only tests that are part of this collection. + */ + *tests(state?: TestResult['state'] | 'running'): IterableIterator { + for (const child of this) { + if (child.type === 'test') { + const testState = getTestState(child) + if (state === testState) { yield child } } - else { + } + } + + /** + * Filters only suites that are part of this collection. + */ + *suites(): IterableIterator { + for (const child of this) { + if (child.type === 'suite') { yield child } } } + /** + * Filters all suites that are part of this collection's suite and its children. + */ + *deepSuites(): IterableIterator { + for (const child of this) { + if (child.type === 'suite') { + yield child + yield * child.children.deepSuites() + } + } + } + *[Symbol.iterator](): IterableIterator { for (const task of this.#task.tasks) { const taskInstance = tasksMap.get(task) @@ -322,3 +373,8 @@ export interface TestDiagnostic { retryCount: number | undefined repeatCount: number | undefined } + +function getTestState(test: TestCase): TestResult['state'] | 'running' { + const result = test.result() + return result ? result.state : 'running' +} From d62a8a86ccaca14c58f0474d7ca4792bb0d03adb Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Wed, 17 Jul 2024 09:30:19 +0200 Subject: [PATCH 21/53] chore: don't return result if test is still running, diagnistic is available only if result is --- packages/vitest/src/node/server-tasks.ts | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/packages/vitest/src/node/server-tasks.ts b/packages/vitest/src/node/server-tasks.ts index e94009d0bbb6..527b2ff82565 100644 --- a/packages/vitest/src/node/server-tasks.ts +++ b/packages/vitest/src/node/server-tasks.ts @@ -3,7 +3,6 @@ import type { File as FileTask, Suite as SuiteTask, TaskMeta, - TaskResult, Test, } from '@vitest/runner' import { getFullName } from '../utils' @@ -105,7 +104,7 @@ export class TestCase extends Task { */ public result(): TestResult | undefined { const result = this.task.result - if (!result) { + if (!result || result.state === 'run') { return undefined } const state = result.state === 'fail' @@ -128,12 +127,17 @@ export class TestCase extends Task { /** * Useful information about the test like duration, memory usage, etc. + * Diagnostic is only available after the test has finished. */ - public diagnostic(): TestDiagnostic { - const result = (this.task.result || {} as TaskResult) + public diagnostic(): TestDiagnostic | undefined { + const result = this.task.result + // startTime should always be available if the test has properly finished + if (!result || result.state === 'run' || !result.startTime) { + return undefined + } return { heap: result.heap, - duration: result.duration, + duration: result.duration!, startTime: result.startTime, retryCount: result.retryCount, repeatCount: result.repeatCount, @@ -368,8 +372,8 @@ interface TestResult { export interface TestDiagnostic { heap: number | undefined - duration: number | undefined - startTime: number | undefined + duration: number + startTime: number retryCount: number | undefined repeatCount: number | undefined } From 1b40e06cb51e6662089842b4e8956209959507c0 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Wed, 17 Jul 2024 09:33:26 +0200 Subject: [PATCH 22/53] chore: improve types for test result --- packages/vitest/src/node/server-tasks.ts | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/packages/vitest/src/node/server-tasks.ts b/packages/vitest/src/node/server-tasks.ts index 527b2ff82565..ef9e20efe1a0 100644 --- a/packages/vitest/src/node/server-tasks.ts +++ b/packages/vitest/src/node/server-tasks.ts @@ -115,7 +115,7 @@ export class TestCase extends Task { return { state, errors: result.errors as TestError[] | undefined, - } + } as TestResult } /** @@ -365,9 +365,21 @@ interface TestError extends SerialisedError { expected?: string } -interface TestResult { - state: 'passed' | 'failed' | 'skipped' - errors?: TestError[] +type TestResult = TestResultPassed | TestResultFailed | TestResultSkipped + +interface TestResultPassed { + state: 'passed' + errors: undefined +} + +interface TestResultFailed { + state: 'failed' + errors: TestError[] +} + +interface TestResultSkipped { + state: 'skipped' + errors: undefined } export interface TestDiagnostic { From c6a3a34c16ea12b6576643f837bea4891d44de29 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Wed, 17 Jul 2024 09:36:56 +0200 Subject: [PATCH 23/53] chore: add flaky prop --- packages/vitest/src/node/server-tasks.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/vitest/src/node/server-tasks.ts b/packages/vitest/src/node/server-tasks.ts index ef9e20efe1a0..35a2b0562374 100644 --- a/packages/vitest/src/node/server-tasks.ts +++ b/packages/vitest/src/node/server-tasks.ts @@ -139,8 +139,9 @@ export class TestCase extends Task { heap: result.heap, duration: result.duration!, startTime: result.startTime, - retryCount: result.retryCount, - repeatCount: result.repeatCount, + retryCount: result.retryCount ?? 0, + repeatCount: result.repeatCount ?? 0, + flaky: !!result.retryCount && result.state === 'pass' && result.retryCount > 0, } } } @@ -386,8 +387,12 @@ export interface TestDiagnostic { heap: number | undefined duration: number startTime: number - retryCount: number | undefined - repeatCount: number | undefined + retryCount: number + repeatCount: number + /** + * If test passed on a second retry. + */ + flaky: boolean } function getTestState(test: TestCase): TestResult['state'] | 'running' { From ca14ffc335d60a62e3ab0261e7252155790e181d Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Wed, 17 Jul 2024 11:07:47 +0200 Subject: [PATCH 24/53] chore: export server tasks --- packages/vitest/src/node/server-tasks.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/vitest/src/node/server-tasks.ts b/packages/vitest/src/node/server-tasks.ts index 35a2b0562374..992d0187ef4c 100644 --- a/packages/vitest/src/node/server-tasks.ts +++ b/packages/vitest/src/node/server-tasks.ts @@ -352,7 +352,7 @@ function buildOptions(task: Test | Custom | FileTask | SuiteTask): TaskOptions { } } -interface SerialisedError { +export interface SerialisedError { message: string stack?: string name: string @@ -360,13 +360,13 @@ interface SerialisedError { [key: string]: unknown } -interface TestError extends SerialisedError { +export interface TestError extends SerialisedError { diff?: string actual?: string expected?: string } -type TestResult = TestResultPassed | TestResultFailed | TestResultSkipped +export type TestResult = TestResultPassed | TestResultFailed | TestResultSkipped interface TestResultPassed { state: 'passed' From 1ce77bff70656df52046957d399f8d63f14a2152 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Wed, 17 Jul 2024 11:25:16 +0200 Subject: [PATCH 25/53] feat: register server tasks --- packages/vitest/src/node/server-tasks.ts | 27 +++-- packages/vitest/src/node/state.ts | 33 ++++-- packages/ws-client/src/index.ts | 2 +- packages/ws-client/src/state.ts | 134 +++++++++++++++++++++++ 4 files changed, 178 insertions(+), 18 deletions(-) create mode 100644 packages/ws-client/src/state.ts diff --git a/packages/vitest/src/node/server-tasks.ts b/packages/vitest/src/node/server-tasks.ts index 992d0187ef4c..e92e18bf01dd 100644 --- a/packages/vitest/src/node/server-tasks.ts +++ b/packages/vitest/src/node/server-tasks.ts @@ -1,6 +1,7 @@ import type { Custom, File as FileTask, + Task as RunnerTask, Suite as SuiteTask, TaskMeta, Test, @@ -14,10 +15,14 @@ import type { WorkspaceProject } from './workspace' // method can return different objects depending on when it's called const tasksMap = new WeakMap< - Test | Custom | FileTask | SuiteTask, + RunnerTask, TestCase | TestFile | TestSuite >() +export function _experimental_getServerTask(task: RunnerTask) { + return tasksMap.get(task) +} + class Task { #fullName: string | undefined @@ -25,7 +30,7 @@ class Task { * Task instance. * @experimental Public task API is experimental and does not follow semver. */ - public readonly task: Test | Custom | FileTask | SuiteTask + public readonly task: RunnerTask /** * Current task's project. @@ -51,8 +56,8 @@ class Task { */ public readonly location: { line: number; column: number } | undefined - constructor( - task: Test | Custom | FileTask | SuiteTask, + protected constructor( + task: RunnerTask, project: WorkspaceProject, ) { this.task = task @@ -72,6 +77,12 @@ class Task { } return this.#fullName } + + static register(task: RunnerTask, project: WorkspaceProject) { + const state = new this(task, project) + tasksMap.set(task, state as TestCase | TestFile | TestSuite) + return state + } } export class TestCase extends Task { @@ -86,7 +97,7 @@ export class TestCase extends Task { */ public readonly parent: TestSuite | TestFile - constructor(task: SuiteTask | FileTask, project: WorkspaceProject) { + protected constructor(task: SuiteTask | FileTask, project: WorkspaceProject) { super(task, project) const suite = this.task.suite @@ -288,7 +299,7 @@ abstract class SuiteImplementation extends Task { */ public readonly children: TaskCollection - constructor(task: SuiteTask | FileTask, project: WorkspaceProject) { + protected constructor(task: SuiteTask | FileTask, project: WorkspaceProject) { super(task, project) const suite = this.task.suite @@ -310,7 +321,7 @@ export class TestSuite extends SuiteImplementation { */ public readonly options: TaskOptions - constructor(task: SuiteTask, project: WorkspaceProject) { + protected constructor(task: SuiteTask, project: WorkspaceProject) { super(task, project) this.options = buildOptions(task) } @@ -326,7 +337,7 @@ export class TestFile extends SuiteImplementation { */ public readonly moduleId: string - constructor(task: FileTask, project: WorkspaceProject) { + protected constructor(task: FileTask, project: WorkspaceProject) { super(task, project) this.moduleId = task.filepath } diff --git a/packages/vitest/src/node/state.ts b/packages/vitest/src/node/state.ts index 919af2bc6f0e..6e049c5c82bb 100644 --- a/packages/vitest/src/node/state.ts +++ b/packages/vitest/src/node/state.ts @@ -1,10 +1,9 @@ import type { File, Task, TaskResultPack } from '@vitest/runner' - -// can't import actual functions from utils, because it's incompatible with @vitest/browsers import { createFileTask } from '@vitest/runner/utils' import type { AggregateError as AggregateErrorPonyfill } from '../utils/base' import type { UserConsoleLog } from '../types/general' import type { WorkspaceProject } from './workspace' +import { TestCase, TestFile, TestSuite, _experimental_getServerTask } from './server-tasks' export function isAggregateError(err: unknown): err is AggregateErrorPonyfill { if (typeof AggregateError !== 'undefined' && err instanceof AggregateError) { @@ -14,7 +13,6 @@ export function isAggregateError(err: unknown): err is AggregateErrorPonyfill { return err instanceof Error && 'errors' in err } -// Note this file is shared for both node and browser, be aware to avoid node specific logic export class StateManager { filesMap = new Map() pathsSet: Set = new Set() @@ -98,7 +96,7 @@ export class StateManager { }) } - collectFiles(files: File[] = []) { + collectFiles(files: File[] = [], project: WorkspaceProject) { files.forEach((file) => { const existing = this.filesMap.get(file.filepath) || [] const otherProject = existing.filter( @@ -114,7 +112,7 @@ export class StateManager { } otherProject.push(file) this.filesMap.set(file.filepath, otherProject) - this.updateId(file) + this.updateId(file, project) }) } @@ -132,6 +130,7 @@ export class StateManager { project.config.name, ) fileTask.local = true + TestFile.register(fileTask, project) this.idMap.set(fileTask.id, fileTask) if (!files) { this.filesMap.set(path, [fileTask]) @@ -150,18 +149,33 @@ export class StateManager { }) } - updateId(task: Task) { + updateId(task: Task, project: WorkspaceProject) { if (this.idMap.get(task.id) === task) { return } + + if (task.type === 'suite' && 'filepath' in task) { + TestFile.register(task, project) + } + else if (task.type === 'suite') { + TestSuite.register(task, project) + } + else { + TestCase.register(task, project) + } + this.idMap.set(task.id, task) if (task.type === 'suite') { task.tasks.forEach((task) => { - this.updateId(task) + this.updateId(task, project) }) } } + _experimental_getServerTask(task: Task) { + return _experimental_getServerTask(task) + } + updateTasks(packs: TaskResultPack[]) { for (const [id, result, meta] of packs) { const task = this.idMap.get(id) @@ -192,9 +206,10 @@ export class StateManager { ).length } - cancelFiles(files: string[], root: string, projectName: string) { + cancelFiles(files: string[], project: WorkspaceProject) { this.collectFiles( - files.map(filepath => createFileTask(filepath, root, projectName)), + files.map(filepath => createFileTask(filepath, project.config.root, project.config.name)), + project, ) } } diff --git a/packages/ws-client/src/index.ts b/packages/ws-client/src/index.ts index 8301c19b719c..ecd65739426e 100644 --- a/packages/ws-client/src/index.ts +++ b/packages/ws-client/src/index.ts @@ -4,7 +4,7 @@ import { parse, stringify } from 'flatted' // eslint-disable-next-line no-restricted-imports import type { WebSocketEvents, WebSocketHandlers } from 'vitest' -import { StateManager } from '../../vitest/src/node/state' +import { StateManager } from './state' export * from '../../vitest/src/utils/tasks' diff --git a/packages/ws-client/src/state.ts b/packages/ws-client/src/state.ts new file mode 100644 index 000000000000..e00b92333d45 --- /dev/null +++ b/packages/ws-client/src/state.ts @@ -0,0 +1,134 @@ +import type { File, Task, TaskResultPack } from '@vitest/runner' +// eslint-disable-next-line no-restricted-imports +import type { UserConsoleLog } from 'vitest' + +// can't import actual functions from utils, because it's incompatible with @vitest/browsers +import { createFileTask } from '@vitest/runner/utils' + +// Note this file is shared for both node and browser, be aware to avoid node specific logic +export class StateManager { + filesMap = new Map() + pathsSet: Set = new Set() + idMap = new Map() + + getPaths() { + return Array.from(this.pathsSet) + } + + /** + * Return files that were running or collected. + */ + getFiles(keys?: string[]): File[] { + if (keys) { + return keys + .map(key => this.filesMap.get(key)!) + .flat() + .filter(file => file && !file.local) + } + return Array.from(this.filesMap.values()).flat().filter(file => !file.local) + } + + getFilepaths(): string[] { + return Array.from(this.filesMap.keys()) + } + + getFailedFilepaths() { + return this.getFiles() + .filter(i => i.result?.state === 'fail') + .map(i => i.filepath) + } + + collectPaths(paths: string[] = []) { + paths.forEach((path) => { + this.pathsSet.add(path) + }) + } + + collectFiles(files: File[] = []) { + files.forEach((file) => { + const existing = this.filesMap.get(file.filepath) || [] + const otherProject = existing.filter( + i => i.projectName !== file.projectName, + ) + const currentFile = existing.find( + i => i.projectName === file.projectName, + ) + // keep logs for the previous file because it should always be initiated before the collections phase + // which means that all logs are collected during the collection and not inside tests + if (currentFile) { + file.logs = currentFile.logs + } + otherProject.push(file) + this.filesMap.set(file.filepath, otherProject) + this.updateId(file) + }) + } + + // this file is reused by ws-client, and should not rely on heavy dependencies like workspace + clearFiles( + _project: { config: { name: string | undefined; root: string } }, + paths: string[] = [], + ) { + const project = _project + paths.forEach((path) => { + const files = this.filesMap.get(path) + const fileTask = createFileTask( + path, + project.config.root, + project.config.name || '', + ) + fileTask.local = true + this.idMap.set(fileTask.id, fileTask) + if (!files) { + this.filesMap.set(path, [fileTask]) + return + } + const filtered = files.filter( + file => file.projectName !== project.config.name, + ) + // always keep a File task, so we can associate logs with it + if (!filtered.length) { + this.filesMap.set(path, [fileTask]) + } + else { + this.filesMap.set(path, [...filtered, fileTask]) + } + }) + } + + updateId(task: Task) { + if (this.idMap.get(task.id) === task) { + return + } + this.idMap.set(task.id, task) + if (task.type === 'suite') { + task.tasks.forEach((task) => { + this.updateId(task) + }) + } + } + + updateTasks(packs: TaskResultPack[]) { + for (const [id, result, meta] of packs) { + const task = this.idMap.get(id) + if (task) { + task.result = result + task.meta = meta + // skipped with new PendingError + if (result?.state === 'skip') { + task.mode = 'skip' + } + } + } + } + + updateUserLog(log: UserConsoleLog) { + const task = log.taskId && this.idMap.get(log.taskId) + if (task) { + if (!task.logs) { + task.logs = [] + } + task.logs.push(log) + } + } +} From 95c6e917b14e9f42f3465c348babc2750fb17e03 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Wed, 17 Jul 2024 11:29:21 +0200 Subject: [PATCH 26/53] fix: correctly collect files --- packages/browser/src/node/rpc.ts | 2 +- packages/ui/client/composables/client/static.ts | 3 +-- packages/vitest/src/api/setup.ts | 4 ---- packages/vitest/src/api/types.ts | 1 - packages/vitest/src/node/core.ts | 2 +- packages/vitest/src/node/pools/rpc.ts | 2 +- packages/vitest/src/node/pools/typecheck.ts | 8 ++++---- packages/vitest/src/node/state.ts | 10 +++++----- 8 files changed, 13 insertions(+), 19 deletions(-) diff --git a/packages/browser/src/node/rpc.ts b/packages/browser/src/node/rpc.ts index 9db8a86490cc..17740981733d 100644 --- a/packages/browser/src/node/rpc.ts +++ b/packages/browser/src/node/rpc.ts @@ -70,7 +70,7 @@ export function setupBrowserRpc( ctx.state.catchError(error, type) }, async onCollected(files) { - ctx.state.collectFiles(files) + ctx.state.collectFiles(project, files) await ctx.report('onCollected', files) }, async onTaskUpdate(packs) { diff --git a/packages/ui/client/composables/client/static.ts b/packages/ui/client/composables/client/static.ts index cc32711984fa..21d8020d0441 100644 --- a/packages/ui/client/composables/client/static.ts +++ b/packages/ui/client/composables/client/static.ts @@ -9,7 +9,7 @@ import type { } from 'vitest' import { parse } from 'flatted' import { decompressSync, strFromU8 } from 'fflate' -import { StateManager } from '../../../../vitest/src/node/state' +import { StateManager } from '../../../../ws-client/src/state' interface HTMLReportMetadata { paths: string[] @@ -55,7 +55,6 @@ export function createStaticClient(): VitestClient { }, getTransformResult: asyncNoop, onDone: noop, - onCollected: asyncNoop, onTaskUpdate: noop, writeFile: asyncNoop, rerun: asyncNoop, diff --git a/packages/vitest/src/api/setup.ts b/packages/vitest/src/api/setup.ts index c837d44aba53..2f380a56d00a 100644 --- a/packages/vitest/src/api/setup.ts +++ b/packages/vitest/src/api/setup.ts @@ -46,10 +46,6 @@ export function setup(ctx: Vitest, _server?: ViteDevServer) { function setupClient(ws: WebSocket) { const rpc = createBirpc( { - async onCollected(files) { - ctx.state.collectFiles(files) - await ctx.report('onCollected', files) - }, async onTaskUpdate(packs) { ctx.state.updateTasks(packs) await ctx.report('onTaskUpdate', packs) diff --git a/packages/vitest/src/api/types.ts b/packages/vitest/src/api/types.ts index 14102be34761..60a3a48ef522 100644 --- a/packages/vitest/src/api/types.ts +++ b/packages/vitest/src/api/types.ts @@ -27,7 +27,6 @@ export interface TransformResultWithSource { } export interface WebSocketHandlers { - onCollected: (files?: File[]) => Promise onTaskUpdate: (packs: TaskResultPack[]) => void getFiles: () => File[] getTestFiles: () => Promise diff --git a/packages/vitest/src/node/core.ts b/packages/vitest/src/node/core.ts index e1967d78daa0..ed5a813edcf2 100644 --- a/packages/vitest/src/node/core.ts +++ b/packages/vitest/src/node/core.ts @@ -440,7 +440,7 @@ export class Vitest { files.forEach((file) => { file.logs?.forEach(log => this.state.updateUserLog(log)) }) - this.state.collectFiles(files) + this.state.collectFiles(project, files) } await this.report('onCollected', files).catch(noop) diff --git a/packages/vitest/src/node/pools/rpc.ts b/packages/vitest/src/node/pools/rpc.ts index 32196af49e72..36a2b369921d 100644 --- a/packages/vitest/src/node/pools/rpc.ts +++ b/packages/vitest/src/node/pools/rpc.ts @@ -76,7 +76,7 @@ export function createMethodsRPC(project: WorkspaceProject, options: MethodsOpti return ctx.report('onPathsCollected', paths) }, onCollected(files) { - ctx.state.collectFiles(files) + ctx.state.collectFiles(project, files) return ctx.report('onCollected', files) }, onAfterSuiteRun(meta) { diff --git a/packages/vitest/src/node/pools/typecheck.ts b/packages/vitest/src/node/pools/typecheck.ts index a88c29ae5912..b0ce984e0f49 100644 --- a/packages/vitest/src/node/pools/typecheck.ts +++ b/packages/vitest/src/node/pools/typecheck.ts @@ -61,7 +61,7 @@ export function createTypecheckPool(ctx: Vitest): ProcessPool { checker.setFiles(files) checker.onParseStart(async () => { - ctx.state.collectFiles(checker.getTestFiles()) + ctx.state.collectFiles(project, checker.getTestFiles()) await ctx.report('onCollected') }) @@ -80,7 +80,7 @@ export function createTypecheckPool(ctx: Vitest): ProcessPool { } await checker.collectTests() - ctx.state.collectFiles(checker.getTestFiles()) + ctx.state.collectFiles(project, checker.getTestFiles()) await ctx.report('onTaskUpdate', checker.getTestPacks()) await ctx.report('onCollected') @@ -107,7 +107,7 @@ export function createTypecheckPool(ctx: Vitest): ProcessPool { const checker = await createWorkspaceTypechecker(project, files) checker.setFiles(files) await checker.collectTests() - ctx.state.collectFiles(checker.getTestFiles()) + ctx.state.collectFiles(project, checker.getTestFiles()) await ctx.report('onCollected') } } @@ -135,7 +135,7 @@ export function createTypecheckPool(ctx: Vitest): ProcessPool { }) const triggered = await _p if (project.typechecker && !triggered) { - ctx.state.collectFiles(project.typechecker.getTestFiles()) + ctx.state.collectFiles(project, project.typechecker.getTestFiles()) await ctx.report('onCollected') await onParseEnd(project, project.typechecker.getResult()) continue diff --git a/packages/vitest/src/node/state.ts b/packages/vitest/src/node/state.ts index 6e049c5c82bb..dd9d2ab79af4 100644 --- a/packages/vitest/src/node/state.ts +++ b/packages/vitest/src/node/state.ts @@ -96,7 +96,7 @@ export class StateManager { }) } - collectFiles(files: File[] = [], project: WorkspaceProject) { + collectFiles(project: WorkspaceProject, files: File[] = []) { files.forEach((file) => { const existing = this.filesMap.get(file.filepath) || [] const otherProject = existing.filter( @@ -116,12 +116,10 @@ export class StateManager { }) } - // this file is reused by ws-client, and should not rely on heavy dependencies like workspace clearFiles( - _project: { config: { name: string | undefined; root: string } }, + project: WorkspaceProject, paths: string[] = [], ) { - const project = _project as WorkspaceProject paths.forEach((path) => { const files = this.filesMap.get(path) const fileTask = createFileTask( @@ -208,8 +206,10 @@ export class StateManager { cancelFiles(files: string[], project: WorkspaceProject) { this.collectFiles( - files.map(filepath => createFileTask(filepath, project.config.root, project.config.name)), project, + files.map(filepath => + createFileTask(filepath, project.config.root, project.config.name), + ), ) } } From 4fee8ac641ea0c9c3a22f6028a84b6adf5fb931e Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Wed, 17 Jul 2024 11:35:20 +0200 Subject: [PATCH 27/53] chore: fix type error --- packages/vitest/src/node/pools/forks.ts | 2 +- packages/vitest/src/node/pools/threads.ts | 2 +- packages/vitest/src/node/pools/vmForks.ts | 2 +- packages/vitest/src/node/pools/vmThreads.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/vitest/src/node/pools/forks.ts b/packages/vitest/src/node/pools/forks.ts index 69a8de2c706b..b7185520b886 100644 --- a/packages/vitest/src/node/pools/forks.ts +++ b/packages/vitest/src/node/pools/forks.ts @@ -137,7 +137,7 @@ export function createForksPool( && error instanceof Error && /The task has been cancelled/.test(error.message) ) { - ctx.state.cancelFiles(files, ctx.config.root, project.config.name) + ctx.state.cancelFiles(files, project) } else { throw error diff --git a/packages/vitest/src/node/pools/threads.ts b/packages/vitest/src/node/pools/threads.ts index c76109baf492..9613d47d198f 100644 --- a/packages/vitest/src/node/pools/threads.ts +++ b/packages/vitest/src/node/pools/threads.ts @@ -135,7 +135,7 @@ export function createThreadsPool( && error instanceof Error && /The task has been cancelled/.test(error.message) ) { - ctx.state.cancelFiles(files, ctx.config.root, project.config.name) + ctx.state.cancelFiles(files, project) } else { throw error diff --git a/packages/vitest/src/node/pools/vmForks.ts b/packages/vitest/src/node/pools/vmForks.ts index 46ea4c3c96b6..249da53e503c 100644 --- a/packages/vitest/src/node/pools/vmForks.ts +++ b/packages/vitest/src/node/pools/vmForks.ts @@ -147,7 +147,7 @@ export function createVmForksPool( && error instanceof Error && /The task has been cancelled/.test(error.message) ) { - ctx.state.cancelFiles(files, ctx.config.root, project.config.name) + ctx.state.cancelFiles(files, project) } else { throw error diff --git a/packages/vitest/src/node/pools/vmThreads.ts b/packages/vitest/src/node/pools/vmThreads.ts index ea245696db89..700eab6c0a60 100644 --- a/packages/vitest/src/node/pools/vmThreads.ts +++ b/packages/vitest/src/node/pools/vmThreads.ts @@ -141,7 +141,7 @@ export function createVmThreadsPool( && error instanceof Error && /The task has been cancelled/.test(error.message) ) { - ctx.state.cancelFiles(files, ctx.config.root, project.config.name) + ctx.state.cancelFiles(files, project) } else { throw error From 6f4f70b3b112851fabe0214f66798a71ab02abd8 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Wed, 17 Jul 2024 11:44:13 +0200 Subject: [PATCH 28/53] chore: cleanup --- packages/vitest/src/node/reporters/index.ts | 3 ++ packages/vitest/src/node/server-tasks.ts | 43 +++++++++++++-------- packages/vitest/src/node/state.ts | 6 ++- 3 files changed, 34 insertions(+), 18 deletions(-) diff --git a/packages/vitest/src/node/reporters/index.ts b/packages/vitest/src/node/reporters/index.ts index 37000f174bbb..b3d12c334bdb 100644 --- a/packages/vitest/src/node/reporters/index.ts +++ b/packages/vitest/src/node/reporters/index.ts @@ -28,6 +28,9 @@ export { } export type { BaseReporter, Reporter } +export { TestCase, TestFile, TestSuite } from '../server-tasks' +export type { TaskOptions, TestDiagnostic, TestError, SerialisedError, ReportedTask } from '../server-tasks' + export type { JsonAssertionResult, JsonTestResult, diff --git a/packages/vitest/src/node/server-tasks.ts b/packages/vitest/src/node/server-tasks.ts index e92e18bf01dd..49e62c27ae38 100644 --- a/packages/vitest/src/node/server-tasks.ts +++ b/packages/vitest/src/node/server-tasks.ts @@ -10,18 +10,15 @@ import { getFullName } from '../utils' import type { ParsedStack } from '../types' import type { WorkspaceProject } from './workspace' +// naming proposal: +// @vitest/runner: Task -> RunnerTask, Suite -> RunnerTestSuite, File -> RunnerTestFile, Test -> RunnerTestCase +// vitest/reporters: Task -> ReportedTask, Suite -> ReportedTestSuite, File -> ReportedTestFile, Test -> ReportedTestCase + // rule for function/getter // getter is a readonly property that doesn't change in time // method can return different objects depending on when it's called -const tasksMap = new WeakMap< - RunnerTask, - TestCase | TestFile | TestSuite ->() - -export function _experimental_getServerTask(task: RunnerTask) { - return tasksMap.get(task) -} +export type ReportedTask = TestCase | TestFile | TestSuite class Task { #fullName: string | undefined @@ -62,7 +59,7 @@ class Task { ) { this.task = task this.project = project - this.file = tasksMap.get(task.file) as TestFile + this.file = project.ctx.state.reportedTasksMap.get(task.file) as TestFile this.name = task.name this.id = task.id this.location = task.location @@ -79,8 +76,8 @@ class Task { } static register(task: RunnerTask, project: WorkspaceProject) { - const state = new this(task, project) - tasksMap.set(task, state as TestCase | TestFile | TestSuite) + const state = new this(task, project) as ReportedTask + storeTask(project, task, state) return state } } @@ -102,7 +99,7 @@ export class TestCase extends Task { const suite = this.task.suite if (suite) { - this.parent = tasksMap.get(suite) as TestSuite + this.parent = getReportedTask(project, suite) as TestSuite } else { this.parent = this.file @@ -159,9 +156,11 @@ export class TestCase extends Task { class TaskCollection { #task: SuiteTask | FileTask + #project: WorkspaceProject - constructor(task: SuiteTask | FileTask) { + constructor(task: SuiteTask | FileTask, project: WorkspaceProject) { this.#task = task + this.#project = project } /** @@ -279,7 +278,7 @@ class TaskCollection { *[Symbol.iterator](): IterableIterator { for (const task of this.#task.tasks) { - const taskInstance = tasksMap.get(task) + const taskInstance = getReportedTask(this.#project, task) if (!taskInstance) { throw new Error(`Task instance was not found for task ${task.id}`) } @@ -304,12 +303,12 @@ abstract class SuiteImplementation extends Task { const suite = this.task.suite if (suite) { - this.parent = tasksMap.get(suite) as TestSuite + this.parent = getReportedTask(project, suite) as TestSuite } else { this.parent = this.file } - this.children = new TaskCollection(task) + this.children = new TaskCollection(task, project) } } @@ -410,3 +409,15 @@ function getTestState(test: TestCase): TestResult['state'] | 'running' { const result = test.result() return result ? result.state : 'running' } + +function storeTask(project: WorkspaceProject, runnerTask: RunnerTask, reportedTask: ReportedTask) { + project.ctx.state.reportedTasksMap.set(runnerTask, reportedTask) +} + +function getReportedTask(project: WorkspaceProject, runnerTask: RunnerTask): ReportedTask { + const reportedTask = project.ctx.state.reportedTasksMap.get(runnerTask) + if (!reportedTask) { + throw new Error(`Task instance was not found for task ${runnerTask.id}`) + } + return reportedTask +} diff --git a/packages/vitest/src/node/state.ts b/packages/vitest/src/node/state.ts index dd9d2ab79af4..8514addf637d 100644 --- a/packages/vitest/src/node/state.ts +++ b/packages/vitest/src/node/state.ts @@ -3,7 +3,8 @@ import { createFileTask } from '@vitest/runner/utils' import type { AggregateError as AggregateErrorPonyfill } from '../utils/base' import type { UserConsoleLog } from '../types/general' import type { WorkspaceProject } from './workspace' -import { TestCase, TestFile, TestSuite, _experimental_getServerTask } from './server-tasks' +import type { ReportedTask } from './server-tasks' +import { TestCase, TestFile, TestSuite } from './server-tasks' export function isAggregateError(err: unknown): err is AggregateErrorPonyfill { if (typeof AggregateError !== 'undefined' && err instanceof AggregateError) { @@ -20,6 +21,7 @@ export class StateManager { taskFileMap = new WeakMap() errorsSet = new Set() processTimeoutCauses = new Set() + reportedTasksMap = new WeakMap() catchError(err: unknown, type: string): void { if (isAggregateError(err)) { @@ -171,7 +173,7 @@ export class StateManager { } _experimental_getServerTask(task: Task) { - return _experimental_getServerTask(task) + return this.reportedTasksMap.get(task) } updateTasks(packs: TaskResultPack[]) { From 91caca004996a20a77e2b55f93f95f19926a9d14 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Wed, 17 Jul 2024 11:45:16 +0200 Subject: [PATCH 29/53] chore: cleanup --- packages/vitest/src/node/server-tasks.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/vitest/src/node/server-tasks.ts b/packages/vitest/src/node/server-tasks.ts index 49e62c27ae38..c9b52e8f259b 100644 --- a/packages/vitest/src/node/server-tasks.ts +++ b/packages/vitest/src/node/server-tasks.ts @@ -20,12 +20,12 @@ import type { WorkspaceProject } from './workspace' export type ReportedTask = TestCase | TestFile | TestSuite -class Task { +class ReportedTaskImplementation { #fullName: string | undefined /** * Task instance. - * @experimental Public task API is experimental and does not follow semver. + * @experimental Public runner task API is experimental and does not follow semver. */ public readonly task: RunnerTask @@ -82,7 +82,7 @@ class Task { } } -export class TestCase extends Task { +export class TestCase extends ReportedTaskImplementation { declare public readonly task: Test | Custom public readonly type: 'test' | 'custom' = 'test' /** @@ -287,7 +287,7 @@ class TaskCollection { } } -abstract class SuiteImplementation extends Task { +abstract class SuiteImplementation extends ReportedTaskImplementation { declare public readonly task: SuiteTask | FileTask /** * Parent suite. If suite was called directly inside the file, the parent will be the file. From 881a248481c4e2492744acc4820be3464457cc63 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Wed, 17 Jul 2024 11:46:31 +0200 Subject: [PATCH 30/53] chore: cleanup --- packages/vitest/src/node/server-tasks.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vitest/src/node/server-tasks.ts b/packages/vitest/src/node/server-tasks.ts index c9b52e8f259b..1d83c3f72a85 100644 --- a/packages/vitest/src/node/server-tasks.ts +++ b/packages/vitest/src/node/server-tasks.ts @@ -49,7 +49,7 @@ class ReportedTaskImplementation { */ public readonly id: string /** - * Full name of the test or the suite including all parent suites separated with `>`. + * Location in the file where the test or suite is defined. */ public readonly location: { line: number; column: number } | undefined From 02a0067d22a399fbaf32b3e7333465a427c9f0d3 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Wed, 17 Jul 2024 14:42:55 +0200 Subject: [PATCH 31/53] refactor: move stuff around --- packages/utils/src/diff/index.ts | 10 +- packages/utils/src/error.ts | 31 +++--- packages/utils/src/index.ts | 2 + packages/utils/src/source-map.ts | 2 +- packages/utils/src/types.ts | 25 ++++- packages/vitest/src/node/error.ts | 2 +- packages/vitest/src/node/reporters/index.ts | 14 ++- .../reported-tasks.ts} | 96 ++++++++----------- packages/vitest/src/node/state.ts | 5 +- packages/vitest/src/node/types/config.ts | 4 +- test/core/test/serialize.test.ts | 26 ++--- 11 files changed, 121 insertions(+), 96 deletions(-) rename packages/vitest/src/node/{server-tasks.ts => reporters/reported-tasks.ts} (81%) diff --git a/packages/utils/src/diff/index.ts b/packages/utils/src/diff/index.ts index 82777b6c4de0..d6dc8a084a83 100644 --- a/packages/utils/src/diff/index.ts +++ b/packages/utils/src/diff/index.ts @@ -69,7 +69,7 @@ const FALLBACK_FORMAT_OPTIONS = { * @param options Diff options * @returns {string | null} a string diff */ -export function diff(a: any, b: any, options?: DiffOptions): string | null { +export function diff(a: any, b: any, options?: DiffOptions): string | undefined { if (Object.is(a, b)) { return '' } @@ -80,11 +80,11 @@ export function diff(a: any, b: any, options?: DiffOptions): string | null { if (aType === 'object' && typeof a.asymmetricMatch === 'function') { if (a.$$typeof !== Symbol.for('jest.asymmetricMatcher')) { // Do not know expected type of user-defined asymmetric matcher. - return null + return undefined } if (typeof a.getExpectedType !== 'function') { // For example, expect.anything() matches either null or undefined - return null + return undefined } expectedType = a.getExpectedType() // Primitive types boolean and number omit difference below. @@ -104,7 +104,7 @@ export function diff(a: any, b: any, options?: DiffOptions): string | null { } if (omitDifference) { - return null + return undefined } switch (aType) { @@ -234,7 +234,7 @@ export function printDiffOrStringify( expected: unknown, received: unknown, options?: DiffOptions, -): string | null { +): string | undefined { const { aAnnotation, bAnnotation } = normalizeDiffOptions(options) if ( diff --git a/packages/utils/src/error.ts b/packages/utils/src/error.ts index 2e0cf0c57152..9ff900208f3b 100644 --- a/packages/utils/src/error.ts +++ b/packages/utils/src/error.ts @@ -1,5 +1,6 @@ import { type DiffOptions, printDiffOrStringify } from './diff' import { format, stringify } from './display' +import type { TestError } from './types' // utils is bundled for any environment and might not support `Element` declare class Element { @@ -26,7 +27,7 @@ function getUnserializableMessage(err: unknown) { } // https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm -export function serializeError(val: any, seen: WeakMap = new WeakMap()): any { +export function serializeValue(val: any, seen: WeakMap = new WeakMap()): any { if (!val || typeof val === 'string') { return val } @@ -41,7 +42,7 @@ export function serializeError(val: any, seen: WeakMap = new WeakM } // cannot serialize immutables as immutables if (isImmutable(val)) { - return serializeError(val.toJSON(), seen) + return serializeValue(val.toJSON(), seen) } if ( val instanceof Promise @@ -56,7 +57,7 @@ export function serializeError(val: any, seen: WeakMap = new WeakM return `${val.toString()} ${format(val.sample)}` } if (typeof val.toJSON === 'function') { - return serializeError(val.toJSON(), seen) + return serializeValue(val.toJSON(), seen) } if (seen.has(val)) { @@ -69,7 +70,7 @@ export function serializeError(val: any, seen: WeakMap = new WeakM seen.set(val, clone) val.forEach((e, i) => { try { - clone[i] = serializeError(e, seen) + clone[i] = serializeValue(e, seen) } catch (err) { clone[i] = getUnserializableMessage(err) @@ -90,7 +91,7 @@ export function serializeError(val: any, seen: WeakMap = new WeakM return } try { - clone[key] = serializeError(val[key], seen) + clone[key] = serializeValue(val[key], seen) } catch (err) { // delete in case it has a setter from prototype that might throw @@ -104,18 +105,22 @@ export function serializeError(val: any, seen: WeakMap = new WeakM } } +export { serializeValue as serializeError } + function normalizeErrorMessage(message: string) { return message.replace(/__(vite_ssr_import|vi_import)_\d+__\./g, '') } export function processError( - err: any, + _err: any, diffOptions?: DiffOptions, seen: WeakSet = new WeakSet(), ): any { - if (!err || typeof err !== 'object') { - return { message: err } + if (!_err || typeof _err !== 'object') { + return { message: String(_err) } } + const err = _err as TestError + // stack is not serialized in worker communication // we stringify it first if (err.stack) { @@ -133,10 +138,14 @@ export function processError( ) { err.diff = printDiffOrStringify(err.actual, err.expected, { ...diffOptions, - ...err.diffOptions, + ...err.diffOptions as DiffOptions, }) } + if (err.diffOptions) { + err.diffOptions = undefined + } + if (typeof err.expected !== 'string') { err.expected = stringify(err.expected, 10) } @@ -163,10 +172,10 @@ export function processError( catch {} try { - return serializeError(err) + return serializeValue(err) } catch (e: any) { - return serializeError( + return serializeValue( new Error( `Failed to fully serialize error: ${e?.message}\nInner error message: ${err?.message}`, ), diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index dce716edf4b4..1caac8ff7c57 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -47,4 +47,6 @@ export type { Constructable, ParsedStack, ErrorWithDiff, + SerialisedError, + TestError, } from './types' diff --git a/packages/utils/src/source-map.ts b/packages/utils/src/source-map.ts index c549c290772e..f1cd145ef419 100644 --- a/packages/utils/src/source-map.ts +++ b/packages/utils/src/source-map.ts @@ -15,7 +15,7 @@ export interface StackTraceParserOptions { ignoreStackEntries?: (RegExp | string)[] getSourceMap?: (file: string) => unknown getFileName?: (id: string) => string - frameFilter?: (error: Error, frame: ParsedStack) => boolean | void + frameFilter?: (error: ErrorWithDiff, frame: ParsedStack) => boolean | void } const CHROME_IE_STACK_REGEXP = /^\s*at .*(?:\S:\d+|\(native\))/m diff --git a/packages/utils/src/types.ts b/packages/utils/src/types.ts index e5c249bceaa4..c2d98d2d62e5 100644 --- a/packages/utils/src/types.ts +++ b/packages/utils/src/types.ts @@ -32,8 +32,29 @@ export interface ParsedStack { column: number } -export interface ErrorWithDiff extends Error { - name: string +export interface SerialisedError { + message: string + stack?: string + name?: string + stacks?: ParsedStack[] + cause?: SerialisedError + [key: string]: unknown +} + +export interface TestError extends SerialisedError { + cause?: TestError + diff?: string + actual?: string + expected?: string +} + +/** + * @deprecated Use `TestError` instead + */ +export interface ErrorWithDiff { + message: string + name?: string + cause?: unknown nameStr?: string stack?: string stackStr?: string diff --git a/packages/vitest/src/node/error.ts b/packages/vitest/src/node/error.ts index d0f07767823d..6597dc902c22 100644 --- a/packages/vitest/src/node/error.ts +++ b/packages/vitest/src/node/error.ts @@ -306,7 +306,7 @@ function printModuleWarningForSourceCode(logger: Logger, path: string) { ) } -export function displayDiff(diff: string | null, console: Console) { +export function displayDiff(diff: string | undefined, console: Console) { if (diff) { console.error(`\n${diff}\n`) } diff --git a/packages/vitest/src/node/reporters/index.ts b/packages/vitest/src/node/reporters/index.ts index b3d12c334bdb..7bf686514626 100644 --- a/packages/vitest/src/node/reporters/index.ts +++ b/packages/vitest/src/node/reporters/index.ts @@ -28,8 +28,18 @@ export { } export type { BaseReporter, Reporter } -export { TestCase, TestFile, TestSuite } from '../server-tasks' -export type { TaskOptions, TestDiagnostic, TestError, SerialisedError, ReportedTask } from '../server-tasks' +export { TestCase, TestFile, TestSuite } from './reported-tasks' +export type { + TestCollection, + + TaskOptions, + TestDiagnostic, + + TestResult, + TestResultFailed, + TestResultPassed, + TestResultSkipped, +} from './reported-tasks' export type { JsonAssertionResult, diff --git a/packages/vitest/src/node/server-tasks.ts b/packages/vitest/src/node/reporters/reported-tasks.ts similarity index 81% rename from packages/vitest/src/node/server-tasks.ts rename to packages/vitest/src/node/reporters/reported-tasks.ts index 1d83c3f72a85..f1782b6d8f81 100644 --- a/packages/vitest/src/node/server-tasks.ts +++ b/packages/vitest/src/node/reporters/reported-tasks.ts @@ -1,24 +1,14 @@ import type { - Custom, - File as FileTask, + Custom as RunnerCustomCase, Task as RunnerTask, - Suite as SuiteTask, + Test as RunnerTestCase, + File as RunnerTestFile, + Suite as RunnerTestSuite, TaskMeta, - Test, } from '@vitest/runner' -import { getFullName } from '../utils' -import type { ParsedStack } from '../types' -import type { WorkspaceProject } from './workspace' - -// naming proposal: -// @vitest/runner: Task -> RunnerTask, Suite -> RunnerTestSuite, File -> RunnerTestFile, Test -> RunnerTestCase -// vitest/reporters: Task -> ReportedTask, Suite -> ReportedTestSuite, File -> ReportedTestFile, Test -> ReportedTestCase - -// rule for function/getter -// getter is a readonly property that doesn't change in time -// method can return different objects depending on when it's called - -export type ReportedTask = TestCase | TestFile | TestSuite +import type { TestError } from '@vitest/utils' +import { getFullName } from '../../utils/tasks' +import type { WorkspaceProject } from '../workspace' class ReportedTaskImplementation { #fullName: string | undefined @@ -34,20 +24,24 @@ class ReportedTaskImplementation { * @experimental Public project API is experimental and does not follow semver. */ public readonly project: WorkspaceProject + /** * Direct reference to the test file where the test or suite is defined. */ public readonly file: TestFile + /** * Name of the test or the suite. */ public readonly name: string + /** * Unique identifier. * This ID is deterministic and will be the same for the same test across multiple runs. * The ID is based on the file path and test position. */ public readonly id: string + /** * Location in the file where the test or suite is defined. */ @@ -76,25 +70,27 @@ class ReportedTaskImplementation { } static register(task: RunnerTask, project: WorkspaceProject) { - const state = new this(task, project) as ReportedTask + const state = new this(task, project) as TestCase | TestSuite | TestFile storeTask(project, task, state) return state } } export class TestCase extends ReportedTaskImplementation { - declare public readonly task: Test | Custom + declare public readonly task: RunnerTestCase | RunnerCustomCase public readonly type: 'test' | 'custom' = 'test' + /** * Options that the test was initiated with. */ public readonly options: TaskOptions + /** * Parent suite. If suite was called directly inside the file, the parent will be the file. */ public readonly parent: TestSuite | TestFile - protected constructor(task: SuiteTask | FileTask, project: WorkspaceProject) { + protected constructor(task: RunnerTestSuite | RunnerTestFile, project: WorkspaceProject) { super(task, project) const suite = this.task.suite @@ -154,11 +150,11 @@ export class TestCase extends ReportedTaskImplementation { } } -class TaskCollection { - #task: SuiteTask | FileTask +class TestCollection { + #task: RunnerTestSuite | RunnerTestFile #project: WorkspaceProject - constructor(task: SuiteTask | FileTask, project: WorkspaceProject) { + constructor(task: RunnerTestSuite | RunnerTestFile, project: WorkspaceProject) { this.#task = task this.#project = project } @@ -278,27 +274,27 @@ class TaskCollection { *[Symbol.iterator](): IterableIterator { for (const task of this.#task.tasks) { - const taskInstance = getReportedTask(this.#project, task) - if (!taskInstance) { - throw new Error(`Task instance was not found for task ${task.id}`) - } - yield taskInstance as TestSuite | TestCase + yield getReportedTask(this.#project, task) as TestSuite | TestCase } } } +export type { TestCollection } + abstract class SuiteImplementation extends ReportedTaskImplementation { - declare public readonly task: SuiteTask | FileTask + declare public readonly task: RunnerTestSuite | RunnerTestFile + /** * Parent suite. If suite was called directly inside the file, the parent will be the file. */ public readonly parent: TestSuite | TestFile + /** * Collection of suites and tests that are part of this suite. */ - public readonly children: TaskCollection + public readonly children: TestCollection - protected constructor(task: SuiteTask | FileTask, project: WorkspaceProject) { + protected constructor(task: RunnerTestSuite | RunnerTestFile, project: WorkspaceProject) { super(task, project) const suite = this.task.suite @@ -308,27 +304,29 @@ abstract class SuiteImplementation extends ReportedTaskImplementation { else { this.parent = this.file } - this.children = new TaskCollection(task, project) + this.children = new TestCollection(task, project) } } export class TestSuite extends SuiteImplementation { - declare public readonly task: SuiteTask + declare public readonly task: RunnerTestSuite public readonly type = 'suite' + /** * Options that suite was initiated with. */ public readonly options: TaskOptions - protected constructor(task: SuiteTask, project: WorkspaceProject) { + protected constructor(task: RunnerTestSuite, project: WorkspaceProject) { super(task, project) this.options = buildOptions(task) } } export class TestFile extends SuiteImplementation { - declare public readonly task: FileTask + declare public readonly task: RunnerTestFile public readonly type = 'file' + /** * This is usually an absolute UNIX file path. * It can be a virtual id if the file is not on the disk. @@ -336,7 +334,7 @@ export class TestFile extends SuiteImplementation { */ public readonly moduleId: string - protected constructor(task: FileTask, project: WorkspaceProject) { + protected constructor(task: RunnerTestFile, project: WorkspaceProject) { super(task, project) this.moduleId = task.filepath } @@ -351,7 +349,7 @@ export interface TaskOptions { mode: 'run' | 'only' | 'skip' | 'todo' } -function buildOptions(task: Test | Custom | FileTask | SuiteTask): TaskOptions { +function buildOptions(task: RunnerTestCase | RunnerCustomCase | RunnerTestFile | RunnerTestSuite): TaskOptions { return { each: task.each, concurrent: task.concurrent, @@ -362,33 +360,19 @@ function buildOptions(task: Test | Custom | FileTask | SuiteTask): TaskOptions { } } -export interface SerialisedError { - message: string - stack?: string - name: string - stacks?: ParsedStack[] - [key: string]: unknown -} - -export interface TestError extends SerialisedError { - diff?: string - actual?: string - expected?: string -} - export type TestResult = TestResultPassed | TestResultFailed | TestResultSkipped -interface TestResultPassed { +export interface TestResultPassed { state: 'passed' errors: undefined } -interface TestResultFailed { +export interface TestResultFailed { state: 'failed' errors: TestError[] } -interface TestResultSkipped { +export interface TestResultSkipped { state: 'skipped' errors: undefined } @@ -410,11 +394,11 @@ function getTestState(test: TestCase): TestResult['state'] | 'running' { return result ? result.state : 'running' } -function storeTask(project: WorkspaceProject, runnerTask: RunnerTask, reportedTask: ReportedTask) { +function storeTask(project: WorkspaceProject, runnerTask: RunnerTask, reportedTask: TestCase | TestSuite | TestFile) { project.ctx.state.reportedTasksMap.set(runnerTask, reportedTask) } -function getReportedTask(project: WorkspaceProject, runnerTask: RunnerTask): ReportedTask { +function getReportedTask(project: WorkspaceProject, runnerTask: RunnerTask): TestCase | TestSuite | TestFile { const reportedTask = project.ctx.state.reportedTasksMap.get(runnerTask) if (!reportedTask) { throw new Error(`Task instance was not found for task ${runnerTask.id}`) diff --git a/packages/vitest/src/node/state.ts b/packages/vitest/src/node/state.ts index 8514addf637d..6ca988a5930b 100644 --- a/packages/vitest/src/node/state.ts +++ b/packages/vitest/src/node/state.ts @@ -3,8 +3,7 @@ import { createFileTask } from '@vitest/runner/utils' import type { AggregateError as AggregateErrorPonyfill } from '../utils/base' import type { UserConsoleLog } from '../types/general' import type { WorkspaceProject } from './workspace' -import type { ReportedTask } from './server-tasks' -import { TestCase, TestFile, TestSuite } from './server-tasks' +import { TestCase, TestFile, TestSuite } from './reporters/reported-tasks' export function isAggregateError(err: unknown): err is AggregateErrorPonyfill { if (typeof AggregateError !== 'undefined' && err instanceof AggregateError) { @@ -21,7 +20,7 @@ export class StateManager { taskFileMap = new WeakMap() errorsSet = new Set() processTimeoutCauses = new Set() - reportedTasksMap = new WeakMap() + reportedTasksMap = new WeakMap() catchError(err: unknown, type: string): void { if (isAggregateError(err)) { diff --git a/packages/vitest/src/node/types/config.ts b/packages/vitest/src/node/types/config.ts index 57cf97cce725..360433863b7b 100644 --- a/packages/vitest/src/node/types/config.ts +++ b/packages/vitest/src/node/types/config.ts @@ -10,7 +10,7 @@ import type { } from '../reporters' import type { TestSequencerConstructor } from '../sequencers/types' import type { ChaiConfig } from '../../integrations/chai/config' -import type { Arrayable, ParsedStack } from '../../types/general' +import type { Arrayable, ErrorWithDiff, ParsedStack } from '../../types/general' import type { JSDOMOptions } from '../../types/jsdom-options' import type { HappyDOMOptions } from '../../types/happy-dom-options' import type { EnvironmentOptions } from '../../types/environment' @@ -620,7 +620,7 @@ export interface InlineConfig { * * Return `false` to omit the frame. */ - onStackTrace?: (error: Error, frame: ParsedStack) => boolean | void + onStackTrace?: (error: ErrorWithDiff, frame: ParsedStack) => boolean | void /** * Indicates if CSS files should be processed. diff --git a/test/core/test/serialize.test.ts b/test/core/test/serialize.test.ts index af2721376d44..387d8de7f631 100644 --- a/test/core/test/serialize.test.ts +++ b/test/core/test/serialize.test.ts @@ -1,15 +1,15 @@ // @vitest-environment jsdom -import { serializeError } from '@vitest/utils/error' +import { serializeValue } from '@vitest/utils/error' import { describe, expect, it } from 'vitest' describe('error serialize', () => { it('works', () => { - expect(serializeError(undefined)).toEqual(undefined) - expect(serializeError(null)).toEqual(null) - expect(serializeError('hi')).toEqual('hi') + expect(serializeValue(undefined)).toEqual(undefined) + expect(serializeValue(null)).toEqual(null) + expect(serializeValue('hi')).toEqual('hi') - expect(serializeError({ + expect(serializeValue({ foo: 'hi', promise: new Promise(() => {}), fn: () => {}, @@ -35,7 +35,7 @@ describe('error serialize', () => { error.whateverArray = [error, error] error.whateverArrayClone = error.whateverArray - expect(serializeError(error)).toMatchSnapshot() + expect(serializeValue(error)).toMatchSnapshot() }) it('Should handle object with getter/setter correctly', () => { @@ -51,7 +51,7 @@ describe('error serialize', () => { }, } - expect(serializeError(user)).toEqual({ + expect(serializeValue(user)).toEqual({ name: 'John', surname: 'Smith', fullName: 'John Smith', @@ -70,7 +70,7 @@ describe('error serialize', () => { Object.defineProperty(user, 'fullName', { enumerable: false, value: 'John Smith' }) - const serialized = serializeError(user) + const serialized = serializeValue(user) expect(serialized).not.toBe(user) expect(serialized).toEqual({ name: 'John', @@ -86,7 +86,7 @@ describe('error serialize', () => { // `MessagePort`, so the serialized error object should have been recreated as plain object. const error = new Error('test') - const serialized = serializeError(error) + const serialized = serializeValue(error) expect(Object.getPrototypeOf(serialized)).toBe(null) expect(serialized).toEqual({ constructor: 'Function', @@ -114,7 +114,7 @@ describe('error serialize', () => { }, }], }) - expect(serializeError(error)).toEqual({ + expect(serializeValue(error)).toEqual({ array: [ { name: ': name cannot be accessed', @@ -131,7 +131,7 @@ describe('error serialize', () => { it('can serialize DOMException', () => { const err = new DOMException('You failed', 'InvalidStateError') - expect(serializeError(err)).toMatchObject({ + expect(serializeValue(err)).toMatchObject({ NETWORK_ERR: 19, name: 'InvalidStateError', message: 'You failed', @@ -160,7 +160,7 @@ describe('error serialize', () => { immutableRecord, }) - expect(serializeError(error)).toMatchObject({ + expect(serializeValue(error)).toMatchObject({ stack: expect.stringContaining('Error: test'), immutableList: ['foo'], immutableRecord: { foo: 'bar' }, @@ -186,7 +186,7 @@ describe('error serialize', () => { }, } - const serialized = serializeError(error) + const serialized = serializeValue(error) expect(serialized).toEqual({ key: 'value', From ad420cb4eb0d1b7887a83bbcac3c5917dae2db62 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Wed, 17 Jul 2024 14:46:17 +0200 Subject: [PATCH 32/53] chore: cleanup --- packages/vitest/src/node/reporters/reported-tasks.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vitest/src/node/reporters/reported-tasks.ts b/packages/vitest/src/node/reporters/reported-tasks.ts index f1782b6d8f81..65696f8651e2 100644 --- a/packages/vitest/src/node/reporters/reported-tasks.ts +++ b/packages/vitest/src/node/reporters/reported-tasks.ts @@ -53,7 +53,7 @@ class ReportedTaskImplementation { ) { this.task = task this.project = project - this.file = project.ctx.state.reportedTasksMap.get(task.file) as TestFile + this.file = getReportedTask(project, task.file) as TestFile this.name = task.name this.id = task.id this.location = task.location From 47048d2982b7ad9fd63218207814b950f25f14ee Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Wed, 17 Jul 2024 16:24:29 +0200 Subject: [PATCH 33/53] chore: cleanup --- packages/utils/src/error.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/utils/src/error.ts b/packages/utils/src/error.ts index 9ff900208f3b..f675d949137c 100644 --- a/packages/utils/src/error.ts +++ b/packages/utils/src/error.ts @@ -142,10 +142,6 @@ export function processError( }) } - if (err.diffOptions) { - err.diffOptions = undefined - } - if (typeof err.expected !== 'string') { err.expected = stringify(err.expected, 10) } From 3c2a477958f91a97e86c9a110b17fc82b02e0cea Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Wed, 17 Jul 2024 16:26:57 +0200 Subject: [PATCH 34/53] refactor: remove `file` and `parent` from TestFile, add `at` and `size` to collection, add `diagnostic` to TestFile --- packages/vitest/rollup.config.js | 2 +- .../src/node/reporters/reported-tasks.ts | 111 ++++++++++++++---- packages/vitest/src/node/state.ts | 2 +- .../fixtures/reported-tasks/1_first.test.ts | 82 +++++++++++++ test/cli/test/reported-tasks.test.ts | 73 ++++++++++++ 5 files changed, 248 insertions(+), 22 deletions(-) create mode 100644 test/cli/fixtures/reported-tasks/1_first.test.ts create mode 100644 test/cli/test/reported-tasks.test.ts diff --git a/packages/vitest/rollup.config.js b/packages/vitest/rollup.config.js index 46e6417ae7a0..4652deaec581 100644 --- a/packages/vitest/rollup.config.js +++ b/packages/vitest/rollup.config.js @@ -95,7 +95,7 @@ const plugins = [ json(), commonjs(), esbuild({ - target: 'node14', + target: 'node18', }), ] diff --git a/packages/vitest/src/node/reporters/reported-tasks.ts b/packages/vitest/src/node/reporters/reported-tasks.ts index 65696f8651e2..13c00e8263a1 100644 --- a/packages/vitest/src/node/reporters/reported-tasks.ts +++ b/packages/vitest/src/node/reporters/reported-tasks.ts @@ -25,11 +25,6 @@ class ReportedTaskImplementation { */ public readonly project: WorkspaceProject - /** - * Direct reference to the test file where the test or suite is defined. - */ - public readonly file: TestFile - /** * Name of the test or the suite. */ @@ -53,7 +48,6 @@ class ReportedTaskImplementation { ) { this.task = task this.project = project - this.file = getReportedTask(project, task.file) as TestFile this.name = task.name this.id = task.id this.location = task.location @@ -80,6 +74,11 @@ export class TestCase extends ReportedTaskImplementation { declare public readonly task: RunnerTestCase | RunnerCustomCase public readonly type: 'test' | 'custom' = 'test' + /** + * Direct reference to the test file where the test or suite is defined. + */ + public readonly file: TestFile + /** * Options that the test was initiated with. */ @@ -93,6 +92,7 @@ export class TestCase extends ReportedTaskImplementation { protected constructor(task: RunnerTestSuite | RunnerTestFile, project: WorkspaceProject) { super(task, project) + this.file = getReportedTask(project, task.file) as TestFile const suite = this.task.suite if (suite) { this.parent = getReportedTask(project, suite) as TestSuite @@ -159,6 +159,20 @@ class TestCollection { this.#project = project } + at(index: number): TestCase | TestSuite | undefined { + if (index < 0) { + index = this.size + index + } + return getReportedTask(this.#project, this.#task.tasks[index]) as TestCase | TestSuite | undefined + } + + /** + * The number of tests and suites in the collection. + */ + get size(): number { + return this.#task.tasks.length + } + /** * The same collection, but in an array form for easier manipulation. */ @@ -240,12 +254,19 @@ class TestCollection { */ *tests(state?: TestResult['state'] | 'running'): IterableIterator { for (const child of this) { - if (child.type === 'test') { + if (child.type !== 'test') { + continue + } + + if (state) { const testState = getTestState(child) if (state === testState) { yield child } } + else { + yield child + } } } @@ -284,11 +305,6 @@ export type { TestCollection } abstract class SuiteImplementation extends ReportedTaskImplementation { declare public readonly task: RunnerTestSuite | RunnerTestFile - /** - * Parent suite. If suite was called directly inside the file, the parent will be the file. - */ - public readonly parent: TestSuite | TestFile - /** * Collection of suites and tests that are part of this suite. */ @@ -296,14 +312,6 @@ abstract class SuiteImplementation extends ReportedTaskImplementation { protected constructor(task: RunnerTestSuite | RunnerTestFile, project: WorkspaceProject) { super(task, project) - - const suite = this.task.suite - if (suite) { - this.parent = getReportedTask(project, suite) as TestSuite - } - else { - this.parent = this.file - } this.children = new TestCollection(task, project) } } @@ -312,6 +320,16 @@ export class TestSuite extends SuiteImplementation { declare public readonly task: RunnerTestSuite public readonly type = 'suite' + /** + * Direct reference to the test file where the test or suite is defined. + */ + public readonly file: TestFile + + /** + * Parent suite. If suite was called directly inside the file, the parent will be the file. + */ + public readonly parent: TestSuite | TestFile + /** * Options that suite was initiated with. */ @@ -319,12 +337,22 @@ export class TestSuite extends SuiteImplementation { protected constructor(task: RunnerTestSuite, project: WorkspaceProject) { super(task, project) + + this.file = getReportedTask(project, task.file) as TestFile + const suite = this.task.suite + if (suite) { + this.parent = getReportedTask(project, suite) as TestSuite + } + else { + this.parent = this.file + } this.options = buildOptions(task) } } export class TestFile extends SuiteImplementation { declare public readonly task: RunnerTestFile + declare public readonly location: undefined public readonly type = 'file' /** @@ -338,6 +366,25 @@ export class TestFile extends SuiteImplementation { super(task, project) this.moduleId = task.filepath } + + /** + * Useful information about the file like duration, memory usage, etc. + * If the file was not executed yet, all diagnostic values will return `0`. + */ + public diagnostic(): FileDiagnostic { + const setupDuration = this.task.setupDuration || 0 + const collectDuration = this.task.collectDuration || 0 + const prepareDuration = this.task.prepareDuration || 0 + const environmentSetupDuration = this.task.environmentLoad || 0 + const duration = this.task.result?.duration || 0 + return { + environmentSetupDuration, + prepareDuration, + collectDuration, + setupDuration, + duration, + } + } } export interface TaskOptions { @@ -389,6 +436,30 @@ export interface TestDiagnostic { flaky: boolean } +export interface FileDiagnostic { + /** + * The time it takes to import and initiate an environment. + */ + environmentSetupDuration: number + /** + * The time it takes Vitest to setup test harness (runner, mocks, etc.). + */ + prepareDuration: number + /** + * The time it takes to import the test file. + * This includes importing everything in the file and executing suite callbacks. + */ + collectDuration: number + /** + * The time it takes to import the setup file. + */ + setupDuration: number + /** + * Accumulated duration of all tests and hooks in the file. + */ + duration: number +} + function getTestState(test: TestCase): TestResult['state'] | 'running' { const result = test.result() return result ? result.state : 'running' diff --git a/packages/vitest/src/node/state.ts b/packages/vitest/src/node/state.ts index 6ca988a5930b..ddd598430bac 100644 --- a/packages/vitest/src/node/state.ts +++ b/packages/vitest/src/node/state.ts @@ -171,7 +171,7 @@ export class StateManager { } } - _experimental_getServerTask(task: Task) { + _experimental_getReportedEntity(task: Task) { return this.reportedTasksMap.get(task) } diff --git a/test/cli/fixtures/reported-tasks/1_first.test.ts b/test/cli/fixtures/reported-tasks/1_first.test.ts new file mode 100644 index 000000000000..66998a91bb34 --- /dev/null +++ b/test/cli/fixtures/reported-tasks/1_first.test.ts @@ -0,0 +1,82 @@ +import { describe, expect, it } from 'vitest' + +it('runs a test', () => { + expect(1).toBe(1) +}) + +it('fails a test', () => { + expect(1).toBe(2) +}) + +it('fails multiple times', () => { + expect.soft(1).toBe(2) + expect.soft(3).toBe(3) + expect.soft(2).toBe(3) +}) + +it('skips an option test', { skip: true }) +it.skip('skips a .modifier test') + +it('todos an option test', { todo: true }) +it.todo('todos a .modifier test') + +it('retries a test', { retry: 5 }, () => { + expect(1).toBe(2) +}) + +let counter = 0 +it('retries a test with success', { retry: 5 }, () => { + expect(counter++).toBe(2) +}) + +it('repeats a test', { repeats: 5 }, () => { + expect(1).toBe(2) +}) + +describe('a group', () => { + it('runs a test in a group', () => { + expect(1).toBe(1) + }) + + it('todos an option test in a group', { todo: true }) + + describe('a nested group', () => { + it('runs a test in a nested group', () => { + expect(1).toBe(1) + }) + + it('fails a test in a nested group', () => { + expect(1).toBe(2) + }) + + it.concurrent('runs first concurrent test in a nested group', () => { + expect(1).toBe(1) + }) + + it.concurrent('runs second concurrent test in a nested group', () => { + expect(1).toBe(1) + }) + }) +}) + +describe.shuffle('shuffled group', () => { + it('runs a test in a shuffled group', () => { + expect(1).toBe(1) + }) +}) + +describe.each([1])('each group %s', (groupValue) => { + it.each([2])('each test %s', (itValue) => { + expect(groupValue + itValue).toBe(3) + }) +}) + +it('regesters a metadata', (ctx) => { + ctx.task.meta.key = 'value' +}) + +declare module 'vitest' { + interface TaskMeta { + key: string + } +} diff --git a/test/cli/test/reported-tasks.test.ts b/test/cli/test/reported-tasks.test.ts new file mode 100644 index 000000000000..5c60566c088d --- /dev/null +++ b/test/cli/test/reported-tasks.test.ts @@ -0,0 +1,73 @@ +import { beforeAll, expect, it } from 'vitest' +import { resolve } from 'pathe' +import type { File } from 'vitest' +import type { StateManager } from 'vitest/src/node/state.js' +import type { WorkspaceProject } from 'vitest/node' +import { runVitest } from '../../test-utils' +import type { TestFile } from '../../../packages/vitest/src/node/reporters/reported-tasks' + +// const finishedFiles: File[] = [] +const collectedFiles: File[] = [] +let state: StateManager +let project: WorkspaceProject + +beforeAll(async () => { + const { ctx } = await runVitest({ + root: resolve(__dirname, '..', 'fixtures', 'reported-tasks'), + include: ['**/*.test.ts'], + reporters: [ + 'verbose', + { + // onFinished(files) { + // finishedFiles.push(...files || []) + // }, + onCollected(files) { + collectedFiles.push(...files || []) + }, + }, + ], + includeTaskLocation: true, + }) + state = ctx!.state + project = ctx!.getCoreWorkspaceProject() +}) + +it('correctly reports a file', async () => { + const files = state.getFiles() || [] + expect(files).toHaveLength(1) + + const testFile = state._experimental_getReportedEntity(files[0])! as TestFile + expect(testFile).toBeDefined() + // suite properties not available on file + expect(testFile).not.toHaveProperty('parent') + expect(testFile).not.toHaveProperty('options') + expect(testFile).not.toHaveProperty('file') + + expect(testFile.task).toBe(files[0]) + expect(testFile.fullName).toBe('1_first.test.ts') + expect(testFile.name).toBe('1_first.test.ts') + expect(testFile.id).toBe(files[0].id) + expect(testFile.location).toBeUndefined() + expect(testFile.moduleId).toBe(resolve('./fixtures/reported-tasks/1_first.test.ts')) + expect(testFile.project).toBe(project) + expect(testFile.children.size).toBe(14) + + const tests = [...testFile.children.tests()] + expect(tests).toHaveLength(11) + const deepTests = [...testFile.children.deepTests()] + expect(deepTests).toHaveLength(19) + + const suites = [...testFile.children.suites()] + expect(suites).toHaveLength(3) + const deepSuites = [...testFile.children.deepSuites()] + expect(deepSuites).toHaveLength(4) + + const diagnostic = testFile.diagnostic() + expect(diagnostic).toBeDefined() + expect(diagnostic.environmentSetupDuration).toBeGreaterThan(0) + expect(diagnostic.prepareDuration).toBeGreaterThan(0) + expect(diagnostic.collectDuration).toBeGreaterThan(0) + expect(diagnostic.duration).toBeGreaterThan(0) + // doesn't have a setup file + expect(diagnostic.setupDuration).toBe(0) +}) From a9d0dfe45f6feb781993bde5fa8062a1fa934ba0 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Wed, 17 Jul 2024 16:27:08 +0200 Subject: [PATCH 35/53] chore: export FileDiagnostic --- packages/vitest/src/node/reporters/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/vitest/src/node/reporters/index.ts b/packages/vitest/src/node/reporters/index.ts index 7bf686514626..c066a3126c76 100644 --- a/packages/vitest/src/node/reporters/index.ts +++ b/packages/vitest/src/node/reporters/index.ts @@ -34,6 +34,7 @@ export type { TaskOptions, TestDiagnostic, + FileDiagnostic, TestResult, TestResultFailed, From ad0bbc09167dadc90c3afe43ae97223f33a5f4ec Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Wed, 17 Jul 2024 17:28:51 +0200 Subject: [PATCH 36/53] test: add more tests --- packages/runner/src/collect.ts | 14 +- packages/runner/src/setup.ts | 3 +- .../src/node/reporters/reported-tasks.ts | 61 ++++-- .../fixtures/reported-tasks/1_first.test.ts | 6 +- test/cli/test/reported-tasks.test.ts | 176 +++++++++++++++++- 5 files changed, 225 insertions(+), 35 deletions(-) diff --git a/packages/runner/src/collect.ts b/packages/runner/src/collect.ts index 74c2d1230095..78fed2e4d038 100644 --- a/packages/runner/src/collect.ts +++ b/packages/runner/src/collect.ts @@ -1,4 +1,5 @@ import { processError } from '@vitest/utils/error' +import { toArray } from '@vitest/utils' import type { File, SuiteHooks } from './types/tasks' import type { VitestRunner } from './types/runner' import { @@ -34,11 +35,18 @@ export async function collectTests( clearCollectorContext(filepath, runner) try { - const setupStart = now() - await runSetupFiles(config, runner) + const setupFiles = toArray(config.setupFiles) + if (setupFiles.length) { + const setupStart = now() + await runSetupFiles(config, setupFiles, runner) + const setupEnd = now() + file.setupDuration = setupEnd - setupStart + } + else { + file.setupDuration = 0 + } const collectStart = now() - file.setupDuration = collectStart - setupStart await runner.importFile(filepath, 'collect') diff --git a/packages/runner/src/setup.ts b/packages/runner/src/setup.ts index ea3a129263e2..f6ea1d722f77 100644 --- a/packages/runner/src/setup.ts +++ b/packages/runner/src/setup.ts @@ -1,11 +1,10 @@ -import { toArray } from '@vitest/utils' import type { VitestRunner, VitestRunnerConfig } from './types/runner' export async function runSetupFiles( config: VitestRunnerConfig, + files: string[], runner: VitestRunner, ): Promise { - const files = toArray(config.setupFiles) if (config.sequence.setupFiles === 'parallel') { await Promise.all( files.map(async (fsPath) => { diff --git a/packages/vitest/src/node/reporters/reported-tasks.ts b/packages/vitest/src/node/reporters/reported-tasks.ts index 13c00e8263a1..3d35a3168fab 100644 --- a/packages/vitest/src/node/reporters/reported-tasks.ts +++ b/packages/vitest/src/node/reporters/reported-tasks.ts @@ -7,12 +7,10 @@ import type { TaskMeta, } from '@vitest/runner' import type { TestError } from '@vitest/utils' -import { getFullName } from '../../utils/tasks' +import { getTestName } from '../../utils/tasks' import type { WorkspaceProject } from '../workspace' class ReportedTaskImplementation { - #fullName: string | undefined - /** * Task instance. * @experimental Public runner task API is experimental and does not follow semver. @@ -25,11 +23,6 @@ class ReportedTaskImplementation { */ public readonly project: WorkspaceProject - /** - * Name of the test or the suite. - */ - public readonly name: string - /** * Unique identifier. * This ID is deterministic and will be the same for the same test across multiple runs. @@ -48,21 +41,10 @@ class ReportedTaskImplementation { ) { this.task = task this.project = project - this.name = task.name this.id = task.id this.location = task.location } - /** - * Full name of the test or the suite including all parent suites separated with `>`. - */ - public get fullName(): string { - if (this.#fullName === undefined) { - this.#fullName = getFullName(this.task, ' > ') - } - return this.#fullName - } - static register(task: RunnerTask, project: WorkspaceProject) { const state = new this(task, project) as TestCase | TestSuite | TestFile storeTask(project, task, state) @@ -71,6 +53,8 @@ class ReportedTaskImplementation { } export class TestCase extends ReportedTaskImplementation { + #fullName: string | undefined + declare public readonly task: RunnerTestCase | RunnerCustomCase public readonly type: 'test' | 'custom' = 'test' @@ -79,6 +63,11 @@ export class TestCase extends ReportedTaskImplementation { */ public readonly file: TestFile + /** + * Name of the test. + */ + public readonly name: string + /** * Options that the test was initiated with. */ @@ -92,6 +81,7 @@ export class TestCase extends ReportedTaskImplementation { protected constructor(task: RunnerTestSuite | RunnerTestFile, project: WorkspaceProject) { super(task, project) + this.name = task.name this.file = getReportedTask(project, task.file) as TestFile const suite = this.task.suite if (suite) { @@ -103,6 +93,16 @@ export class TestCase extends ReportedTaskImplementation { this.options = buildOptions(task) } + /** + * Full name of the test including all parent suites separated with `>`. + */ + public get fullName(): string { + if (this.#fullName === undefined) { + this.#fullName = getTestName(this.task, ' > ') + } + return this.#fullName + } + /** * Result of the test. Will be `undefined` if test is not finished yet or was just collected. */ @@ -317,9 +317,16 @@ abstract class SuiteImplementation extends ReportedTaskImplementation { } export class TestSuite extends SuiteImplementation { + #fullName: string | undefined + declare public readonly task: RunnerTestSuite public readonly type = 'suite' + /** + * Name of the test or the suite. + */ + public readonly name: string + /** * Direct reference to the test file where the test or suite is defined. */ @@ -338,6 +345,7 @@ export class TestSuite extends SuiteImplementation { protected constructor(task: RunnerTestSuite, project: WorkspaceProject) { super(task, project) + this.name = task.name this.file = getReportedTask(project, task.file) as TestFile const suite = this.task.suite if (suite) { @@ -348,6 +356,16 @@ export class TestSuite extends SuiteImplementation { } this.options = buildOptions(task) } + + /** + * Full name of the suite including all parent suites separated with `>`. + */ + public get fullName(): string { + if (this.#fullName === undefined) { + this.#fullName = getTestName(this.task, ' > ') + } + return this.#fullName + } } export class TestFile extends SuiteImplementation { @@ -411,7 +429,10 @@ export type TestResult = TestResultPassed | TestResultFailed | TestResultSkipped export interface TestResultPassed { state: 'passed' - errors: undefined + /** + * If test was retried successfully, errors will still be reported. + */ + errors: TestError[] | undefined } export interface TestResultFailed { diff --git a/test/cli/fixtures/reported-tasks/1_first.test.ts b/test/cli/fixtures/reported-tasks/1_first.test.ts index 66998a91bb34..c6fe098f8539 100644 --- a/test/cli/fixtures/reported-tasks/1_first.test.ts +++ b/test/cli/fixtures/reported-tasks/1_first.test.ts @@ -1,10 +1,12 @@ import { describe, expect, it } from 'vitest' -it('runs a test', () => { +it('runs a test', async () => { + await new Promise(r => setTimeout(r, 10)) expect(1).toBe(1) }) -it('fails a test', () => { +it('fails a test', async () => { + await new Promise(r => setTimeout(r, 10)) expect(1).toBe(2) }) diff --git a/test/cli/test/reported-tasks.test.ts b/test/cli/test/reported-tasks.test.ts index 5c60566c088d..a8d165ccb4bf 100644 --- a/test/cli/test/reported-tasks.test.ts +++ b/test/cli/test/reported-tasks.test.ts @@ -6,10 +6,13 @@ import type { WorkspaceProject } from 'vitest/node' import { runVitest } from '../../test-utils' import type { TestFile } from '../../../packages/vitest/src/node/reporters/reported-tasks' +const now = new Date() // const finishedFiles: File[] = [] const collectedFiles: File[] = [] let state: StateManager let project: WorkspaceProject +let files: File[] +let testFile: TestFile beforeAll(async () => { const { ctx } = await runVitest({ @@ -27,25 +30,26 @@ beforeAll(async () => { }, ], includeTaskLocation: true, + logHeapUsage: true, }) state = ctx!.state project = ctx!.getCoreWorkspaceProject() -}) - -it('correctly reports a file', async () => { - const files = state.getFiles() || [] + files = state.getFiles() expect(files).toHaveLength(1) - - const testFile = state._experimental_getReportedEntity(files[0])! as TestFile + testFile = state._experimental_getReportedEntity(files[0])! as TestFile expect(testFile).toBeDefined() +}) + +it('correctly reports a file', () => { // suite properties not available on file expect(testFile).not.toHaveProperty('parent') expect(testFile).not.toHaveProperty('options') expect(testFile).not.toHaveProperty('file') + expect(testFile).not.toHaveProperty('fullName') + expect(testFile).not.toHaveProperty('name') + expect(testFile.type).toBe('file') expect(testFile.task).toBe(files[0]) - expect(testFile.fullName).toBe('1_first.test.ts') - expect(testFile.name).toBe('1_first.test.ts') expect(testFile.id).toBe(files[0].id) expect(testFile.location).toBeUndefined() expect(testFile.moduleId).toBe(resolve('./fixtures/reported-tasks/1_first.test.ts')) @@ -71,3 +75,159 @@ it('correctly reports a file', async () => { // doesn't have a setup file expect(diagnostic.setupDuration).toBe(0) }) + +it('correctly reports a passed test', () => { + const passedTest = testFile.children.find('test', 'runs a test')! + expect(passedTest.type).toBe('test') + expect(passedTest.task).toBe(files[0].tasks[0]) + expect(passedTest.name).toBe('runs a test') + expect(passedTest.fullName).toBe('runs a test') + expect(passedTest.file).toBe(testFile) + expect(passedTest.parent).toBe(testFile) + expect(passedTest.options).toEqual({ + each: undefined, + concurrent: undefined, + shuffle: undefined, + retry: undefined, + repeats: undefined, + mode: 'run', + }) + expect(passedTest.meta()).toEqual({}) + + const result = passedTest.result()! + expect(result).toBeDefined() + expect(result.state).toBe('passed') + expect(result.errors).toBeUndefined() + + const diagnostic = passedTest.diagnostic()! + expect(diagnostic).toBeDefined() + expect(diagnostic.heap).toBeGreaterThan(0) + expect(diagnostic.duration).toBeGreaterThan(0) + expect(date(new Date(diagnostic.startTime))).toBe(date(now)) + expect(diagnostic.flaky).toBe(false) + expect(diagnostic.repeatCount).toBe(0) + expect(diagnostic.repeatCount).toBe(0) +}) + +it('correctly reports failed test', () => { + const passedTest = testFile.children.find('test', 'fails a test')! + expect(passedTest.type).toBe('test') + expect(passedTest.task).toBe(files[0].tasks[1]) + expect(passedTest.name).toBe('fails a test') + expect(passedTest.fullName).toBe('fails a test') + expect(passedTest.file).toBe(testFile) + expect(passedTest.parent).toBe(testFile) + expect(passedTest.options).toEqual({ + each: undefined, + concurrent: undefined, + shuffle: undefined, + retry: undefined, + repeats: undefined, + mode: 'run', + }) + expect(passedTest.meta()).toEqual({}) + + const result = passedTest.result()! + expect(result).toBeDefined() + expect(result.state).toBe('failed') + expect(result.errors).toHaveLength(1) + expect(result.errors![0]).toMatchObject({ + diff: expect.any(String), + message: 'expected 1 to be 2 // Object.is equality', + ok: false, + stack: expect.stringContaining('expected 1 to be 2 // Object.is equality'), + stacks: [ + { + column: 13, + file: resolve('./fixtures/reported-tasks/1_first.test.ts'), + line: 10, + method: '', + }, + ], + }) + + const diagnostic = passedTest.diagnostic()! + expect(diagnostic).toBeDefined() + expect(diagnostic.heap).toBeGreaterThan(0) + expect(diagnostic.duration).toBeGreaterThan(0) + expect(date(new Date(diagnostic.startTime))).toBe(date(now)) + expect(diagnostic.flaky).toBe(false) + expect(diagnostic.repeatCount).toBe(0) + expect(diagnostic.repeatCount).toBe(0) +}) + +it('correctly reports multiple failures', () => { + const testCase = testFile.children.find('test', 'fails multiple times')! + const result = testCase.result()! + expect(result).toBeDefined() + expect(result.state).toBe('failed') + expect(result.errors).toHaveLength(2) + expect(result.errors![0]).toMatchObject({ + message: 'expected 1 to be 2 // Object.is equality', + }) + expect(result.errors![1]).toMatchObject({ + message: 'expected 2 to be 3 // Object.is equality', + }) +}) + +it('correctly reports test assigned options', () => { + const testOptionSkip = testFile.children.find('test', 'skips an option test')! + expect(testOptionSkip.options.mode).toBe('skip') + const testModifierSkip = testFile.children.find('test', 'skips a .modifier test')! + expect(testModifierSkip.options.mode).toBe('skip') + + const testOptionTodo = testFile.children.find('test', 'todos an option test')! + expect(testOptionTodo.options.mode).toBe('todo') + const testModifierTodo = testFile.children.find('test', 'todos a .modifier test')! + expect(testModifierTodo.options.mode).toBe('todo') +}) + +it('correctly reports retried tests', () => { + const testRetry = testFile.children.find('test', 'retries a test')! + expect(testRetry.options.retry).toBe(5) + expect(testRetry.options.repeats).toBeUndefined() + expect(testRetry.result()!.state).toBe('failed') +}) + +it('correctly reports flaky tests', () => { + const testFlaky = testFile.children.find('test', 'retries a test with success')! + const diagnostic = testFlaky.diagnostic()! + expect(diagnostic.flaky).toBe(true) + expect(diagnostic.retryCount).toBe(2) + expect(diagnostic.repeatCount).toBe(0) + const result = testFlaky.result()! + expect(result.state).toBe('passed') + expect(result.errors).toHaveLength(2) +}) + +it('correctly reports repeated tests', () => { + const testRepeated = testFile.children.find('test', 'repeats a test')! + const diagnostic = testRepeated.diagnostic()! + expect(diagnostic.flaky).toBe(false) + expect(diagnostic.retryCount).toBe(0) + expect(diagnostic.repeatCount).toBe(5) + const result = testRepeated.result()! + expect(result.state).toBe('failed') + expect(result.errors).toHaveLength(6) +}) + +it('correctly passed down metadata', () => { + const testMetadata = testFile.children.find('test', 'regesters a metadata')! + const meta = testMetadata.meta() + expect(meta.key).toBe('value') +}) + +it('correctly finds test in nested suites', () => { + const oneNestedTest = testFile.children.deepFind('test', 'runs a test in a group')! + const oneTestedSuite = testFile.children.find('suite', 'a group')! + expect(oneNestedTest.parent).toEqual(oneTestedSuite) + + const twoNestedTest = testFile.children.deepFind('test', 'runs a test in a nested group')! + expect(twoNestedTest.parent).toEqual( + oneTestedSuite.children.find('suite', 'a nested group'), + ) +}) + +function date(time: Date) { + return `${time.getDate()}/${time.getMonth() + 1}/${time.getFullYear()}` +} From 7a2fdff7751f4bdbd561d0f7908f9205be8e493a Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Thu, 25 Jul 2024 16:25:04 +0200 Subject: [PATCH 37/53] chore: export reporter task types --- packages/vitest/src/public/node.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/vitest/src/public/node.ts b/packages/vitest/src/public/node.ts index 7373e0916550..0c4be2578472 100644 --- a/packages/vitest/src/public/node.ts +++ b/packages/vitest/src/public/node.ts @@ -48,6 +48,17 @@ export type { HTMLOptions } from '../node/reporters/html' export { isFileServingAllowed, createServer, parseAst, parseAstAsync } from 'vite' export type * as Vite from 'vite' +export { TestCase, TestFile, TestSuite } from '../node/reporters/reported-tasks' +export type { + TaskOptions, + TestDiagnostic, + FileDiagnostic, + TestResult, + TestResultPassed, + TestResultFailed, + TestResultSkipped, +} from '../node/reporters/reported-tasks' + export type { SequenceHooks, SequenceSetupFiles, From 93ea531d49d9c54f013d8343a9bc1c54dc7903d2 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Thu, 25 Jul 2024 16:31:49 +0200 Subject: [PATCH 38/53] refctor: remove deepFind, rename deepTests to allTests, deepSuites to allSuites --- .../src/node/reporters/reported-tasks.ts | 50 ++------------- packages/vitest/src/public/node.ts | 2 + test/cli/test/reported-tasks.test.ts | 63 +++++++++++-------- 3 files changed, 44 insertions(+), 71 deletions(-) diff --git a/packages/vitest/src/node/reporters/reported-tasks.ts b/packages/vitest/src/node/reporters/reported-tasks.ts index 3d35a3168fab..e44060f6dbc5 100644 --- a/packages/vitest/src/node/reporters/reported-tasks.ts +++ b/packages/vitest/src/node/reporters/reported-tasks.ts @@ -187,55 +187,13 @@ class TestCollection { return this[Symbol.iterator]() } - /** - * Looks for a test or a suite by `name` only inside the current suite. - * If `name` is a string, it will look for an exact match. - */ - find(type: 'test', name: string | RegExp): TestCase | undefined - find(type: 'suite', name: string | RegExp): TestSuite | undefined - find(type: 'test' | 'suite', name: string | RegExp): TestCase | TestSuite | undefined - find(type: 'test' | 'suite', name: string | RegExp): TestCase | TestSuite | undefined { - const isString = typeof name === 'string' - for (const task of this) { - if (task.type === type) { - if (task.name === name || (!isString && name.test(task.name))) { - return task - } - } - } - } - - /** - * Looks for a test or a suite by `name` inside the current suite and its children. - * If `name` is a string, it will look for an exact match. - */ - deepFind(type: 'test', name: string | RegExp): TestCase | undefined - deepFind(type: 'suite', name: string | RegExp): TestSuite | undefined - deepFind(type: 'test' | 'suite', name: string | RegExp): TestCase | TestSuite | undefined - deepFind(type: 'test' | 'suite', name: string | RegExp): TestCase | TestSuite | undefined { - const isString = typeof name === 'string' - for (const task of this) { - if (task.type === type) { - if (task.name === name || (!isString && name.test(task.name))) { - return task - } - } - if (task.type === 'suite') { - const result = task.children.deepFind(type, name) - if (result) { - return result - } - } - } - } - /** * Filters all tests that are part of this collection's suite and its children. */ - *deepTests(state?: TestResult['state'] | 'running'): IterableIterator { + *allTests(state?: TestResult['state'] | 'running'): IterableIterator { for (const child of this) { if (child.type === 'suite') { - yield * child.children.deepTests(state) + yield * child.children.allTests(state) } else if (state) { const testState = getTestState(child) @@ -284,11 +242,11 @@ class TestCollection { /** * Filters all suites that are part of this collection's suite and its children. */ - *deepSuites(): IterableIterator { + *allSuites(): IterableIterator { for (const child of this) { if (child.type === 'suite') { yield child - yield * child.children.deepSuites() + yield * child.children.allSuites() } } } diff --git a/packages/vitest/src/public/node.ts b/packages/vitest/src/public/node.ts index 0c4be2578472..d89495518264 100644 --- a/packages/vitest/src/public/node.ts +++ b/packages/vitest/src/public/node.ts @@ -50,6 +50,8 @@ export type * as Vite from 'vite' export { TestCase, TestFile, TestSuite } from '../node/reporters/reported-tasks' export type { + TestCollection, + TaskOptions, TestDiagnostic, FileDiagnostic, diff --git a/test/cli/test/reported-tasks.test.ts b/test/cli/test/reported-tasks.test.ts index a8d165ccb4bf..5c11eea3ba7c 100644 --- a/test/cli/test/reported-tasks.test.ts +++ b/test/cli/test/reported-tasks.test.ts @@ -4,7 +4,7 @@ import type { File } from 'vitest' import type { StateManager } from 'vitest/src/node/state.js' import type { WorkspaceProject } from 'vitest/node' import { runVitest } from '../../test-utils' -import type { TestFile } from '../../../packages/vitest/src/node/reporters/reported-tasks' +import type { TestCase, TestCollection, TestFile } from '../../../packages/vitest/src/node/reporters/reported-tasks' const now = new Date() // const finishedFiles: File[] = [] @@ -58,12 +58,12 @@ it('correctly reports a file', () => { const tests = [...testFile.children.tests()] expect(tests).toHaveLength(11) - const deepTests = [...testFile.children.deepTests()] + const deepTests = [...testFile.children.allTests()] expect(deepTests).toHaveLength(19) const suites = [...testFile.children.suites()] expect(suites).toHaveLength(3) - const deepSuites = [...testFile.children.deepSuites()] + const deepSuites = [...testFile.children.allSuites()] expect(deepSuites).toHaveLength(4) const diagnostic = testFile.diagnostic() @@ -77,7 +77,7 @@ it('correctly reports a file', () => { }) it('correctly reports a passed test', () => { - const passedTest = testFile.children.find('test', 'runs a test')! + const passedTest = findTest(testFile.children, 'runs a test') expect(passedTest.type).toBe('test') expect(passedTest.task).toBe(files[0].tasks[0]) expect(passedTest.name).toBe('runs a test') @@ -110,7 +110,7 @@ it('correctly reports a passed test', () => { }) it('correctly reports failed test', () => { - const passedTest = testFile.children.find('test', 'fails a test')! + const passedTest = findTest(testFile.children, 'fails a test') expect(passedTest.type).toBe('test') expect(passedTest.task).toBe(files[0].tasks[1]) expect(passedTest.name).toBe('fails a test') @@ -157,7 +157,7 @@ it('correctly reports failed test', () => { }) it('correctly reports multiple failures', () => { - const testCase = testFile.children.find('test', 'fails multiple times')! + const testCase = findTest(testFile.children, 'fails multiple times') const result = testCase.result()! expect(result).toBeDefined() expect(result.state).toBe('failed') @@ -171,26 +171,26 @@ it('correctly reports multiple failures', () => { }) it('correctly reports test assigned options', () => { - const testOptionSkip = testFile.children.find('test', 'skips an option test')! + const testOptionSkip = findTest(testFile.children, 'skips an option test') expect(testOptionSkip.options.mode).toBe('skip') - const testModifierSkip = testFile.children.find('test', 'skips a .modifier test')! + const testModifierSkip = findTest(testFile.children, 'skips a .modifier test') expect(testModifierSkip.options.mode).toBe('skip') - const testOptionTodo = testFile.children.find('test', 'todos an option test')! + const testOptionTodo = findTest(testFile.children, 'todos an option test') expect(testOptionTodo.options.mode).toBe('todo') - const testModifierTodo = testFile.children.find('test', 'todos a .modifier test')! + const testModifierTodo = findTest(testFile.children, 'todos a .modifier test') expect(testModifierTodo.options.mode).toBe('todo') }) it('correctly reports retried tests', () => { - const testRetry = testFile.children.find('test', 'retries a test')! + const testRetry = findTest(testFile.children, 'retries a test') expect(testRetry.options.retry).toBe(5) expect(testRetry.options.repeats).toBeUndefined() expect(testRetry.result()!.state).toBe('failed') }) it('correctly reports flaky tests', () => { - const testFlaky = testFile.children.find('test', 'retries a test with success')! + const testFlaky = findTest(testFile.children, 'retries a test with success') const diagnostic = testFlaky.diagnostic()! expect(diagnostic.flaky).toBe(true) expect(diagnostic.retryCount).toBe(2) @@ -201,7 +201,7 @@ it('correctly reports flaky tests', () => { }) it('correctly reports repeated tests', () => { - const testRepeated = testFile.children.find('test', 'repeats a test')! + const testRepeated = findTest(testFile.children, 'repeats a test') const diagnostic = testRepeated.diagnostic()! expect(diagnostic.flaky).toBe(false) expect(diagnostic.retryCount).toBe(0) @@ -212,22 +212,35 @@ it('correctly reports repeated tests', () => { }) it('correctly passed down metadata', () => { - const testMetadata = testFile.children.find('test', 'regesters a metadata')! + const testMetadata = findTest(testFile.children, 'regesters a metadata') const meta = testMetadata.meta() expect(meta.key).toBe('value') }) -it('correctly finds test in nested suites', () => { - const oneNestedTest = testFile.children.deepFind('test', 'runs a test in a group')! - const oneTestedSuite = testFile.children.find('suite', 'a group')! - expect(oneNestedTest.parent).toEqual(oneTestedSuite) - - const twoNestedTest = testFile.children.deepFind('test', 'runs a test in a nested group')! - expect(twoNestedTest.parent).toEqual( - oneTestedSuite.children.find('suite', 'a nested group'), - ) -}) - function date(time: Date) { return `${time.getDate()}/${time.getMonth() + 1}/${time.getFullYear()}` } + +function deepFind(children: TestCollection, name: string): TestCase | undefined { + for (const task of children) { + if (task.type === 'test') { + if (task.name === name) { + return task + } + } + if (task.type === 'suite') { + const result = deepFind(task.children, name) + if (result) { + return result + } + } + } +} + +function findTest(children: TestCollection, name: string): TestCase { + const testCase = deepFind(children, name) + if (!testCase) { + throw new Error(`Test "${name}" not found`) + } + return testCase +} From c67e90862c4e7b0eaffbea16fe6d94fd6125d24d Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Thu, 25 Jul 2024 16:35:08 +0200 Subject: [PATCH 39/53] chore: better error --- packages/vitest/src/node/reporters/reported-tasks.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/vitest/src/node/reporters/reported-tasks.ts b/packages/vitest/src/node/reporters/reported-tasks.ts index e44060f6dbc5..245409a860ec 100644 --- a/packages/vitest/src/node/reporters/reported-tasks.ts +++ b/packages/vitest/src/node/reporters/reported-tasks.ts @@ -159,6 +159,9 @@ class TestCollection { this.#project = project } + /** + * Test or a suite at a specific index in the array. + */ at(index: number): TestCase | TestSuite | undefined { if (index < 0) { index = this.size + index @@ -451,7 +454,7 @@ function storeTask(project: WorkspaceProject, runnerTask: RunnerTask, reportedTa function getReportedTask(project: WorkspaceProject, runnerTask: RunnerTask): TestCase | TestSuite | TestFile { const reportedTask = project.ctx.state.reportedTasksMap.get(runnerTask) if (!reportedTask) { - throw new Error(`Task instance was not found for task ${runnerTask.id}`) + throw new Error(`Task instance was not found for ${runnerTask.type} ${runnerTask.name}`) } return reportedTask } From 28c65b2bb4b34c9fa0b7b5378db4fc1261e6f4d1 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Thu, 25 Jul 2024 16:43:06 +0200 Subject: [PATCH 40/53] chore: cleanup --- packages/vitest/src/node/reporters/reported-tasks.ts | 7 ++++++- packages/vitest/src/node/state.ts | 2 +- test/cli/fixtures/reported-tasks/1_first.test.ts | 2 +- test/cli/test/reported-tasks.test.ts | 4 ++-- 4 files changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/vitest/src/node/reporters/reported-tasks.ts b/packages/vitest/src/node/reporters/reported-tasks.ts index 245409a860ec..aaf3f0826c91 100644 --- a/packages/vitest/src/node/reporters/reported-tasks.ts +++ b/packages/vitest/src/node/reporters/reported-tasks.ts @@ -391,13 +391,18 @@ export type TestResult = TestResultPassed | TestResultFailed | TestResultSkipped export interface TestResultPassed { state: 'passed' /** - * If test was retried successfully, errors will still be reported. + * Errors that were thrown during the test execution. + * + * **Note**: If test was retried successfully, errors will still be reported. */ errors: TestError[] | undefined } export interface TestResultFailed { state: 'failed' + /** + * Errors that were thrown during the test execution. + */ errors: TestError[] } diff --git a/packages/vitest/src/node/state.ts b/packages/vitest/src/node/state.ts index ddd598430bac..dc7b79fafe91 100644 --- a/packages/vitest/src/node/state.ts +++ b/packages/vitest/src/node/state.ts @@ -171,7 +171,7 @@ export class StateManager { } } - _experimental_getReportedEntity(task: Task) { + getReportedEntity(task: Task) { return this.reportedTasksMap.get(task) } diff --git a/test/cli/fixtures/reported-tasks/1_first.test.ts b/test/cli/fixtures/reported-tasks/1_first.test.ts index c6fe098f8539..7279c56df6e4 100644 --- a/test/cli/fixtures/reported-tasks/1_first.test.ts +++ b/test/cli/fixtures/reported-tasks/1_first.test.ts @@ -73,7 +73,7 @@ describe.each([1])('each group %s', (groupValue) => { }) }) -it('regesters a metadata', (ctx) => { +it('registers a metadata', (ctx) => { ctx.task.meta.key = 'value' }) diff --git a/test/cli/test/reported-tasks.test.ts b/test/cli/test/reported-tasks.test.ts index 5c11eea3ba7c..4f004de5e7ec 100644 --- a/test/cli/test/reported-tasks.test.ts +++ b/test/cli/test/reported-tasks.test.ts @@ -36,7 +36,7 @@ beforeAll(async () => { project = ctx!.getCoreWorkspaceProject() files = state.getFiles() expect(files).toHaveLength(1) - testFile = state._experimental_getReportedEntity(files[0])! as TestFile + testFile = state.getReportedEntity(files[0])! as TestFile expect(testFile).toBeDefined() }) @@ -212,7 +212,7 @@ it('correctly reports repeated tests', () => { }) it('correctly passed down metadata', () => { - const testMetadata = findTest(testFile.children, 'regesters a metadata') + const testMetadata = findTest(testFile.children, 'registers a metadata') const meta = testMetadata.meta() expect(meta.key).toBe('value') }) From 6763c0dde4f2b5116834eae68be8c2dd827046fe Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Thu, 25 Jul 2024 16:43:11 +0200 Subject: [PATCH 41/53] test: fix diff test --- .../core/test/__snapshots__/jest-expect.test.ts.snap | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/core/test/__snapshots__/jest-expect.test.ts.snap b/test/core/test/__snapshots__/jest-expect.test.ts.snap index a8aff8bc9627..85b30fbf13c6 100644 --- a/test/core/test/__snapshots__/jest-expect.test.ts.snap +++ b/test/core/test/__snapshots__/jest-expect.test.ts.snap @@ -3,7 +3,7 @@ exports[`asymmetric matcher error 1`] = ` { "actual": "hello", - "diff": null, + "diff": undefined, "expected": "StringContaining "xx"", "message": "expected 'hello' to deeply equal StringContaining "xx"", } @@ -12,7 +12,7 @@ exports[`asymmetric matcher error 1`] = ` exports[`asymmetric matcher error 2`] = ` { "actual": "hello", - "diff": null, + "diff": undefined, "expected": "StringNotContaining "ll"", "message": "expected 'hello' to deeply equal StringNotContaining "ll"", } @@ -200,7 +200,7 @@ exports[`asymmetric matcher error 13`] = ` exports[`asymmetric matcher error 14`] = ` { "actual": "hello", - "diff": null, + "diff": undefined, "expected": "StringMatching /xx/", "message": "expected 'hello' to deeply equal StringMatching /xx/", } @@ -222,7 +222,7 @@ exports[`asymmetric matcher error 15`] = ` exports[`asymmetric matcher error 16`] = ` { "actual": "hello", - "diff": null, + "diff": undefined, "expected": "StringContaining "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"", "message": "expected 'hello' to deeply equal StringContaining{…}", } @@ -231,7 +231,7 @@ exports[`asymmetric matcher error 16`] = ` exports[`asymmetric matcher error 17`] = ` { "actual": "hello", - "diff": null, + "diff": undefined, "expected": "StringContaining "xx"", "message": "expected error to match asymmetric matcher", } @@ -253,7 +253,7 @@ stringContainingCustom exports[`asymmetric matcher error 19`] = ` { "actual": "hello", - "diff": null, + "diff": undefined, "expected": "StringContaining "ll"", "message": "expected error not to match asymmetric matcher", } From 893f50bad7c0447f6bd156be49debab52a603bd1 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Thu, 25 Jul 2024 16:44:01 +0200 Subject: [PATCH 42/53] chore: cleanup --- packages/vitest/src/node/reporters/reported-tasks.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/vitest/src/node/reporters/reported-tasks.ts b/packages/vitest/src/node/reporters/reported-tasks.ts index aaf3f0826c91..2faaa1ccd57a 100644 --- a/packages/vitest/src/node/reporters/reported-tasks.ts +++ b/packages/vitest/src/node/reporters/reported-tasks.ts @@ -452,12 +452,12 @@ function getTestState(test: TestCase): TestResult['state'] | 'running' { return result ? result.state : 'running' } -function storeTask(project: WorkspaceProject, runnerTask: RunnerTask, reportedTask: TestCase | TestSuite | TestFile) { +function storeTask(project: WorkspaceProject, runnerTask: RunnerTask, reportedTask: TestCase | TestSuite | TestFile): void { project.ctx.state.reportedTasksMap.set(runnerTask, reportedTask) } function getReportedTask(project: WorkspaceProject, runnerTask: RunnerTask): TestCase | TestSuite | TestFile { - const reportedTask = project.ctx.state.reportedTasksMap.get(runnerTask) + const reportedTask = project.ctx.state.getReportedEntity(runnerTask) if (!reportedTask) { throw new Error(`Task instance was not found for ${runnerTask.type} ${runnerTask.name}`) } From 71d487927cf3b3ce7b127b01577200872c0e6159 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Thu, 25 Jul 2024 16:48:18 +0200 Subject: [PATCH 43/53] chore: export TestError/SerializedError --- packages/utils/src/index.ts | 2 +- packages/utils/src/types.ts | 6 +++--- packages/vitest/src/public/index.ts | 2 ++ 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 1caac8ff7c57..6b87a5c4fcb2 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -47,6 +47,6 @@ export type { Constructable, ParsedStack, ErrorWithDiff, - SerialisedError, + SerializedError, TestError, } from './types' diff --git a/packages/utils/src/types.ts b/packages/utils/src/types.ts index c2d98d2d62e5..8e3c3c229de8 100644 --- a/packages/utils/src/types.ts +++ b/packages/utils/src/types.ts @@ -32,16 +32,16 @@ export interface ParsedStack { column: number } -export interface SerialisedError { +export interface SerializedError { message: string stack?: string name?: string stacks?: ParsedStack[] - cause?: SerialisedError + cause?: SerializedError [key: string]: unknown } -export interface TestError extends SerialisedError { +export interface TestError extends SerializedError { cause?: TestError diff?: string actual?: string diff --git a/packages/vitest/src/public/index.ts b/packages/vitest/src/public/index.ts index 33f17ad0e200..b20c4f05243e 100644 --- a/packages/vitest/src/public/index.ts +++ b/packages/vitest/src/public/index.ts @@ -201,6 +201,8 @@ export type { AfterSuiteRunMeta, } from '../types/general' +export type { TestError, SerializedError } from '@vitest/utils' + /** @deprecated import from `vitest/environments` instead */ export type EnvironmentReturn = EnvironmentReturn_ /** @deprecated import from `vitest/environments` instead */ From b2609379c94b5af7d4b21742362d6f9534611901 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Thu, 25 Jul 2024 16:49:12 +0200 Subject: [PATCH 44/53] chore: cleanup --- test/cli/fixtures/reported-tasks/1_first.test.ts | 2 +- test/cli/test/reported-tasks.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/cli/fixtures/reported-tasks/1_first.test.ts b/test/cli/fixtures/reported-tasks/1_first.test.ts index 7279c56df6e4..1c52b5e3ee50 100644 --- a/test/cli/fixtures/reported-tasks/1_first.test.ts +++ b/test/cli/fixtures/reported-tasks/1_first.test.ts @@ -79,6 +79,6 @@ it('registers a metadata', (ctx) => { declare module 'vitest' { interface TaskMeta { - key: string + key?: string } } diff --git a/test/cli/test/reported-tasks.test.ts b/test/cli/test/reported-tasks.test.ts index 4f004de5e7ec..afcef7de361a 100644 --- a/test/cli/test/reported-tasks.test.ts +++ b/test/cli/test/reported-tasks.test.ts @@ -214,7 +214,7 @@ it('correctly reports repeated tests', () => { it('correctly passed down metadata', () => { const testMetadata = findTest(testFile.children, 'registers a metadata') const meta = testMetadata.meta() - expect(meta.key).toBe('value') + expect(meta).toHaveProperty('key', 'value') }) function date(time: Date) { From 864dad20d07575c3a3f13c3cf0fa78aa6b6072af Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 30 Jul 2024 10:26:36 +0200 Subject: [PATCH 45/53] feat: add public project API --- .../vitest/src/node/reported-workspace.ts | 70 +++++++++++++++++++ .../src/node/reporters/reported-tasks.ts | 51 ++++++++++++-- packages/vitest/src/node/state.ts | 10 +++ packages/vitest/src/node/types/config.ts | 18 +++-- packages/vitest/src/node/workspace.ts | 39 ++++------- packages/vitest/src/public/node.ts | 1 + 6 files changed, 156 insertions(+), 33 deletions(-) create mode 100644 packages/vitest/src/node/reported-workspace.ts diff --git a/packages/vitest/src/node/reported-workspace.ts b/packages/vitest/src/node/reported-workspace.ts new file mode 100644 index 000000000000..756dcf367b4a --- /dev/null +++ b/packages/vitest/src/node/reported-workspace.ts @@ -0,0 +1,70 @@ +import type { ProvidedContext } from '../types/general' +import type { ResolvedConfig, ResolvedProjectConfig, SerializedConfig } from './types/config' +import type { WorkspaceProject } from './workspace' +import type { Vitest } from './core' + +export class TestProject { + /** + * The global vitest instance. + * @experimental The public Vitest API is experimental and does not follow semver. + */ + public readonly vitest: Vitest + /** + * The workspace project this test project is associated with. + * @experimental The public Vitest API is experimental and does not follow semver. + */ + public readonly workspaceProject: WorkspaceProject + + /** + * Resolved project configuration. + */ + public readonly config: ResolvedProjectConfig + /** + * Resolved global configuration. If there are no workspace projects, this will be the same as `config`. + */ + public readonly globalConfig: ResolvedConfig + + private constructor(workspaceProject: WorkspaceProject) { + this.workspaceProject = workspaceProject + this.vitest = workspaceProject.ctx + this.globalConfig = workspaceProject.ctx.config + this.config = workspaceProject.config + } + + /** + * Serialized project configuration. This is the config that tests receive. + */ + public get serializedConfig(): SerializedConfig { + return this.workspaceProject.getSerializableConfig() + } + + /** + * The name of the project or an empty string if not set. + */ + public name(): string { + return this.workspaceProject.getName() + } + + /** + * Custom context provided to the project. + */ + public context(): ProvidedContext { + return this.workspaceProject.getProvidedContext() + } + + /** + * Provide a custom context to the project. This context will be available to tests once they run. + */ + public provide( + key: T, + value: ProvidedContext[T], + ): void { + this.workspaceProject.provide(key, value) + } + + static register(workspaceProject: WorkspaceProject): TestProject { + const project = new TestProject(workspaceProject) + workspaceProject.ctx.state.reportedProjectsMap.set(workspaceProject, project) + return project + } +} diff --git a/packages/vitest/src/node/reporters/reported-tasks.ts b/packages/vitest/src/node/reporters/reported-tasks.ts index 2faaa1ccd57a..56bdc232afec 100644 --- a/packages/vitest/src/node/reporters/reported-tasks.ts +++ b/packages/vitest/src/node/reporters/reported-tasks.ts @@ -9,6 +9,7 @@ import type { import type { TestError } from '@vitest/utils' import { getTestName } from '../../utils/tasks' import type { WorkspaceProject } from '../workspace' +import type { TestProject } from '../reported-workspace' class ReportedTaskImplementation { /** @@ -18,10 +19,10 @@ class ReportedTaskImplementation { public readonly task: RunnerTask /** - * Current task's project. + * The project assosiacted with the test or suite. * @experimental Public project API is experimental and does not follow semver. */ - public readonly project: WorkspaceProject + public readonly project: TestProject /** * Unique identifier. @@ -40,11 +41,14 @@ class ReportedTaskImplementation { project: WorkspaceProject, ) { this.task = task - this.project = project + this.project = project.ctx.state.getReportedProject(project) this.id = task.id this.location = task.location } + /** + * Creates a new reported task instance and stores it in the project's state for future use. + */ static register(task: RunnerTask, project: WorkspaceProject) { const state = new this(task, project) as TestCase | TestSuite | TestFile storeTask(project, task, state) @@ -122,6 +126,15 @@ export class TestCase extends ReportedTaskImplementation { } as TestResult } + /** + * Checks if the test passed successfully. + * If the test is not finished yet or was skipped, it will return `true`. + */ + public ok(): boolean { + const result = this.result() + return !result || result.state !== 'failed' + } + /** * Custom metadata that was attached to the test during its execution. */ @@ -389,6 +402,9 @@ function buildOptions(task: RunnerTestCase | RunnerCustomCase | RunnerTestFile | export type TestResult = TestResultPassed | TestResultFailed | TestResultSkipped export interface TestResultPassed { + /** + * The test passed successfully. + */ state: 'passed' /** * Errors that were thrown during the test execution. @@ -399,6 +415,9 @@ export interface TestResultPassed { } export interface TestResultFailed { + /** + * The test failed to execute. + */ state: 'failed' /** * Errors that were thrown during the test execution. @@ -407,15 +426,39 @@ export interface TestResultFailed { } export interface TestResultSkipped { + /** + * The test was skipped with `only`, `skip` or `todo` flag. + * You can see which one was used in the `mode` option. + */ state: 'skipped' + /** + * Skipped tests have no errors. + */ errors: undefined } export interface TestDiagnostic { + /** + * The amount of memory used by the test in bytes. + * This value is only available if the test was executed with `logHeapUsage` flag. + */ heap: number | undefined + /** + * The time it takes to execute the test in ms. + */ duration: number + /** + * The time in ms when the test started. + */ startTime: number + /** + * The amount of times the test was retried. + */ retryCount: number + /** + * The amount of times the test was repeated as configured by `repeats` option. + * This value can be lower if the test failed during the repeat and no `retry` is configured. + */ repeatCount: number /** * If test passed on a second retry. @@ -459,7 +502,7 @@ function storeTask(project: WorkspaceProject, runnerTask: RunnerTask, reportedTa function getReportedTask(project: WorkspaceProject, runnerTask: RunnerTask): TestCase | TestSuite | TestFile { const reportedTask = project.ctx.state.getReportedEntity(runnerTask) if (!reportedTask) { - throw new Error(`Task instance was not found for ${runnerTask.type} ${runnerTask.name}`) + throw new Error(`Task instance was not found for ${runnerTask.type} "${runnerTask.name}"`) } return reportedTask } diff --git a/packages/vitest/src/node/state.ts b/packages/vitest/src/node/state.ts index dc7b79fafe91..1c676d3affc1 100644 --- a/packages/vitest/src/node/state.ts +++ b/packages/vitest/src/node/state.ts @@ -4,6 +4,7 @@ import type { AggregateError as AggregateErrorPonyfill } from '../utils/base' import type { UserConsoleLog } from '../types/general' import type { WorkspaceProject } from './workspace' import { TestCase, TestFile, TestSuite } from './reporters/reported-tasks' +import { TestProject } from './reported-workspace' export function isAggregateError(err: unknown): err is AggregateErrorPonyfill { if (typeof AggregateError !== 'undefined' && err instanceof AggregateError) { @@ -21,6 +22,7 @@ export class StateManager { errorsSet = new Set() processTimeoutCauses = new Set() reportedTasksMap = new WeakMap() + reportedProjectsMap = new WeakMap() catchError(err: unknown, type: string): void { if (isAggregateError(err)) { @@ -175,6 +177,14 @@ export class StateManager { return this.reportedTasksMap.get(task) } + getReportedProject(project: WorkspaceProject) { + const reportedProject = this.reportedProjectsMap.get(project) + if (!reportedProject) { + return TestProject.register(project) + } + return reportedProject + } + updateTasks(packs: TaskResultPack[]) { for (const [id, result, meta] of packs) { const task = this.idMap.get(id) diff --git a/packages/vitest/src/node/types/config.ts b/packages/vitest/src/node/types/config.ts index 360433863b7b..bf866ec4fa8a 100644 --- a/packages/vitest/src/node/types/config.ts +++ b/packages/vitest/src/node/types/config.ts @@ -1019,9 +1019,7 @@ export interface ResolvedConfig minWorkers: number } -export type ProjectConfig = Omit< - UserConfig, - | 'sequencer' +type NonProjectOptions = | 'shard' | 'watch' | 'run' @@ -1029,7 +1027,6 @@ export type ProjectConfig = Omit< | 'update' | 'reporters' | 'outputFile' - | 'poolOptions' | 'teardownTimeout' | 'silent' | 'forceRerunTriggers' @@ -1047,11 +1044,17 @@ export type ProjectConfig = Omit< | 'slowTestThreshold' | 'inspect' | 'inspectBrk' - | 'deps' | 'coverage' | 'maxWorkers' | 'minWorkers' | 'fileParallelism' + +export type ProjectConfig = Omit< + UserConfig, + NonProjectOptions + | 'sequencer' + | 'deps' + | 'poolOptions' > & { sequencer?: Omit deps?: Omit @@ -1065,4 +1068,9 @@ export type ProjectConfig = Omit< } } +export type ResolvedProjectConfig = Omit< + ResolvedConfig, + NonProjectOptions +> + export type { UserWorkspaceConfig } from '../../public/config' diff --git a/packages/vitest/src/node/workspace.ts b/packages/vitest/src/node/workspace.ts index 749f8a8a069f..54a280f09efc 100644 --- a/packages/vitest/src/node/workspace.ts +++ b/packages/vitest/src/node/workspace.ts @@ -24,6 +24,7 @@ import { setup } from '../api/setup' import type { ProvidedContext } from '../types/general' import type { ResolvedConfig, + SerializedConfig, UserConfig, UserWorkspaceConfig, } from './types/config' @@ -86,6 +87,7 @@ export class WorkspaceProject { configOverride: Partial | undefined config!: ResolvedConfig + serializedConfig!: SerializedConfig server!: ViteDevServer vitenode!: ViteNodeServer runner!: ViteNodeRunner @@ -206,12 +208,6 @@ export class WorkspaceProject { return mod?.ssrTransformResult?.map || mod?.transformResult?.map } - getBrowserSourceMapModuleById( - id: string, - ): TransformResult['map'] | undefined { - return this.browser?.vite.moduleGraph.getModuleById(id)?.transformResult?.map - } - get reporters() { return this.ctx.reporters } @@ -385,6 +381,11 @@ export class WorkspaceProject { server.config, this.ctx.logger, ) + this.serializedConfig = serializeConfig( + this.config, + this.ctx.config, + server.config, + ) this.server = server @@ -404,28 +405,18 @@ export class WorkspaceProject { await this.initBrowserServer(this.server.config.configFile) } - isBrowserEnabled() { + isBrowserEnabled(): boolean { return isBrowserEnabled(this.config) } - getSerializableConfig(method: 'run' | 'collect' = 'run') { - // TODO: call `serializeConfig` only once - const config = deepMerge(serializeConfig( - this.config, - this.ctx.config, - this.server?.config, - ), (this.ctx.configOverride || {})) - - // disable heavy features when collecting because they are not needed - if (method === 'collect') { - if (this.config.browser.provider && this.config.browser.provider !== 'preview') { - config.browser.headless = true - } - config.snapshotSerializers = [] - config.diff = undefined + getSerializableConfig(): SerializedConfig { + if (!this.ctx.configOverride) { + return this.serializedConfig } - - return config + return deepMerge( + this.serializedConfig, + this.ctx.configOverride, + ) } close() { diff --git a/packages/vitest/src/public/node.ts b/packages/vitest/src/public/node.ts index d89495518264..d04ccb97fdd0 100644 --- a/packages/vitest/src/public/node.ts +++ b/packages/vitest/src/public/node.ts @@ -81,6 +81,7 @@ export type { UserConfig, ResolvedConfig, ProjectConfig, + ResolvedProjectConfig, UserWorkspaceConfig, RuntimeConfig, } from '../node/types/config' From b937226ee50fce81ee957bb49101dbcfd9e60856 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 30 Jul 2024 10:36:21 +0200 Subject: [PATCH 46/53] chore: cleanup --- packages/vitest/src/node/workspace.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/vitest/src/node/workspace.ts b/packages/vitest/src/node/workspace.ts index 54a280f09efc..7a9ac7a5171a 100644 --- a/packages/vitest/src/node/workspace.ts +++ b/packages/vitest/src/node/workspace.ts @@ -362,6 +362,11 @@ export class WorkspaceProject { project.server = ctx.server project.runner = ctx.runner project.config = ctx.config + project.serializedConfig = serializeConfig( + ctx.config, + ctx.config, + ctx.server.config, + ) return project } From 2fb38e596d3f44f455c9d5193361eb7b84893bc1 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 30 Jul 2024 10:37:04 +0200 Subject: [PATCH 47/53] chore: cleanup --- packages/vitest/src/node/reporters/reported-tasks.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/vitest/src/node/reporters/reported-tasks.ts b/packages/vitest/src/node/reporters/reported-tasks.ts index 56bdc232afec..a7e0ca9cf3ad 100644 --- a/packages/vitest/src/node/reporters/reported-tasks.ts +++ b/packages/vitest/src/node/reporters/reported-tasks.ts @@ -20,7 +20,6 @@ class ReportedTaskImplementation { /** * The project assosiacted with the test or suite. - * @experimental Public project API is experimental and does not follow semver. */ public readonly project: TestProject From 776576fe0acb2297964fbb3c296cb4d078921782 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 30 Jul 2024 10:39:07 +0200 Subject: [PATCH 48/53] chore: cleanup --- .../{reported-workspace.ts => reported-workspace-project.ts} | 2 +- packages/vitest/src/node/reporters/reported-tasks.ts | 2 +- packages/vitest/src/node/state.ts | 2 +- packages/vitest/src/public/node.ts | 1 + 4 files changed, 4 insertions(+), 3 deletions(-) rename packages/vitest/src/node/{reported-workspace.ts => reported-workspace-project.ts} (98%) diff --git a/packages/vitest/src/node/reported-workspace.ts b/packages/vitest/src/node/reported-workspace-project.ts similarity index 98% rename from packages/vitest/src/node/reported-workspace.ts rename to packages/vitest/src/node/reported-workspace-project.ts index 756dcf367b4a..a2b70af44ac2 100644 --- a/packages/vitest/src/node/reported-workspace.ts +++ b/packages/vitest/src/node/reported-workspace-project.ts @@ -53,7 +53,7 @@ export class TestProject { } /** - * Provide a custom context to the project. This context will be available to tests once they run. + * Provide a custom context to the project. This context will be available for tests once they run. */ public provide( key: T, diff --git a/packages/vitest/src/node/reporters/reported-tasks.ts b/packages/vitest/src/node/reporters/reported-tasks.ts index a7e0ca9cf3ad..ea5dff0562d0 100644 --- a/packages/vitest/src/node/reporters/reported-tasks.ts +++ b/packages/vitest/src/node/reporters/reported-tasks.ts @@ -9,7 +9,7 @@ import type { import type { TestError } from '@vitest/utils' import { getTestName } from '../../utils/tasks' import type { WorkspaceProject } from '../workspace' -import type { TestProject } from '../reported-workspace' +import type { TestProject } from '../reported-workspace-project' class ReportedTaskImplementation { /** diff --git a/packages/vitest/src/node/state.ts b/packages/vitest/src/node/state.ts index 1c676d3affc1..fc66bc25155c 100644 --- a/packages/vitest/src/node/state.ts +++ b/packages/vitest/src/node/state.ts @@ -4,7 +4,7 @@ import type { AggregateError as AggregateErrorPonyfill } from '../utils/base' import type { UserConsoleLog } from '../types/general' import type { WorkspaceProject } from './workspace' import { TestCase, TestFile, TestSuite } from './reporters/reported-tasks' -import { TestProject } from './reported-workspace' +import { TestProject } from './reported-workspace-project' export function isAggregateError(err: unknown): err is AggregateErrorPonyfill { if (typeof AggregateError !== 'undefined' && err instanceof AggregateError) { diff --git a/packages/vitest/src/public/node.ts b/packages/vitest/src/public/node.ts index d04ccb97fdd0..d0d1bcb5d613 100644 --- a/packages/vitest/src/public/node.ts +++ b/packages/vitest/src/public/node.ts @@ -49,6 +49,7 @@ export { isFileServingAllowed, createServer, parseAst, parseAstAsync } from 'vit export type * as Vite from 'vite' export { TestCase, TestFile, TestSuite } from '../node/reporters/reported-tasks' +export { TestProject } from '../node/reported-workspace-project' export type { TestCollection, From 05e2b0f0ff9f47e2b4aa9a208548bb8c601b3e02 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 30 Jul 2024 11:12:29 +0200 Subject: [PATCH 49/53] chore: cleanup --- .../src/node/reported-workspace-project.ts | 30 +++++++++++-------- .../src/node/reporters/reported-tasks.ts | 4 +-- packages/vitest/src/node/state.ts | 10 ------- packages/vitest/src/node/workspace.ts | 9 ++++-- test/cli/test/reported-tasks.test.ts | 2 +- 5 files changed, 28 insertions(+), 27 deletions(-) diff --git a/packages/vitest/src/node/reported-workspace-project.ts b/packages/vitest/src/node/reported-workspace-project.ts index a2b70af44ac2..bc89b5e3cd7f 100644 --- a/packages/vitest/src/node/reported-workspace-project.ts +++ b/packages/vitest/src/node/reported-workspace-project.ts @@ -19,23 +19,21 @@ export class TestProject { * Resolved project configuration. */ public readonly config: ResolvedProjectConfig + /** + * Serialized project configuration. This is the config that tests receive. + */ + public readonly serializedConfig: SerializedConfig /** * Resolved global configuration. If there are no workspace projects, this will be the same as `config`. */ public readonly globalConfig: ResolvedConfig - private constructor(workspaceProject: WorkspaceProject) { + constructor(workspaceProject: WorkspaceProject) { this.workspaceProject = workspaceProject this.vitest = workspaceProject.ctx this.globalConfig = workspaceProject.ctx.config this.config = workspaceProject.config - } - - /** - * Serialized project configuration. This is the config that tests receive. - */ - public get serializedConfig(): SerializedConfig { - return this.workspaceProject.getSerializableConfig() + this.serializedConfig = workspaceProject.serializedConfig } /** @@ -62,9 +60,17 @@ export class TestProject { this.workspaceProject.provide(key, value) } - static register(workspaceProject: WorkspaceProject): TestProject { - const project = new TestProject(workspaceProject) - workspaceProject.ctx.state.reportedProjectsMap.set(workspaceProject, project) - return project + public toJSON(): SerializedTestProject { + return { + name: this.name(), + serializedConfig: this.serializedConfig, + context: this.context(), + } } } + +interface SerializedTestProject { + name: string + serializedConfig: SerializedConfig + context: ProvidedContext +} diff --git a/packages/vitest/src/node/reporters/reported-tasks.ts b/packages/vitest/src/node/reporters/reported-tasks.ts index ea5dff0562d0..7e6a317e0270 100644 --- a/packages/vitest/src/node/reporters/reported-tasks.ts +++ b/packages/vitest/src/node/reporters/reported-tasks.ts @@ -9,7 +9,7 @@ import type { import type { TestError } from '@vitest/utils' import { getTestName } from '../../utils/tasks' import type { WorkspaceProject } from '../workspace' -import type { TestProject } from '../reported-workspace-project' +import { TestProject } from '../reported-workspace-project' class ReportedTaskImplementation { /** @@ -40,7 +40,7 @@ class ReportedTaskImplementation { project: WorkspaceProject, ) { this.task = task - this.project = project.ctx.state.getReportedProject(project) + this.project = project.testProject || (project.testProject = new TestProject(project)) this.id = task.id this.location = task.location } diff --git a/packages/vitest/src/node/state.ts b/packages/vitest/src/node/state.ts index fc66bc25155c..dc7b79fafe91 100644 --- a/packages/vitest/src/node/state.ts +++ b/packages/vitest/src/node/state.ts @@ -4,7 +4,6 @@ import type { AggregateError as AggregateErrorPonyfill } from '../utils/base' import type { UserConsoleLog } from '../types/general' import type { WorkspaceProject } from './workspace' import { TestCase, TestFile, TestSuite } from './reporters/reported-tasks' -import { TestProject } from './reported-workspace-project' export function isAggregateError(err: unknown): err is AggregateErrorPonyfill { if (typeof AggregateError !== 'undefined' && err instanceof AggregateError) { @@ -22,7 +21,6 @@ export class StateManager { errorsSet = new Set() processTimeoutCauses = new Set() reportedTasksMap = new WeakMap() - reportedProjectsMap = new WeakMap() catchError(err: unknown, type: string): void { if (isAggregateError(err)) { @@ -177,14 +175,6 @@ export class StateManager { return this.reportedTasksMap.get(task) } - getReportedProject(project: WorkspaceProject) { - const reportedProject = this.reportedProjectsMap.get(project) - if (!reportedProject) { - return TestProject.register(project) - } - return reportedProject - } - updateTasks(packs: TaskResultPack[]) { for (const [id, result, meta] of packs) { const task = this.idMap.get(id) diff --git a/packages/vitest/src/node/workspace.ts b/packages/vitest/src/node/workspace.ts index 7a9ac7a5171a..bf4d9ba07c73 100644 --- a/packages/vitest/src/node/workspace.ts +++ b/packages/vitest/src/node/workspace.ts @@ -1,6 +1,6 @@ import { promises as fs } from 'node:fs' -import { rm } from 'node:fs/promises' import { tmpdir } from 'node:os' +import { rm } from 'node:fs/promises' import fg from 'fast-glob' import mm from 'micromatch' import { @@ -38,6 +38,7 @@ import { MocksPlugins } from './plugins/mocks' import { CoverageTransform } from './plugins/coverageTransform' import { serializeConfig } from './config/serializeConfig' import type { Vitest } from './core' +import { TestProject } from './reported-workspace-project' interface InitializeProjectOptions extends UserWorkspaceConfig { workspaceConfigPath: string @@ -98,6 +99,8 @@ export class WorkspaceProject { testFilesList: string[] | null = null + public testProject!: TestProject + public readonly id = nanoid() public readonly tmpDir = join(tmpdir(), this.id) @@ -367,6 +370,7 @@ export class WorkspaceProject { ctx.config, ctx.server.config, ) + project.testProject = new TestProject(project) return project } @@ -391,6 +395,7 @@ export class WorkspaceProject { this.ctx.config, server.config, ) + this.testProject = new TestProject(this) this.server = server @@ -440,7 +445,7 @@ export class WorkspaceProject { private async clearTmpDir() { try { - await rm(this.tmpDir, { force: true, recursive: true }) + await rm(this.tmpDir, { recursive: true }) } catch {} } diff --git a/test/cli/test/reported-tasks.test.ts b/test/cli/test/reported-tasks.test.ts index afcef7de361a..716d9f96b3c9 100644 --- a/test/cli/test/reported-tasks.test.ts +++ b/test/cli/test/reported-tasks.test.ts @@ -53,7 +53,7 @@ it('correctly reports a file', () => { expect(testFile.id).toBe(files[0].id) expect(testFile.location).toBeUndefined() expect(testFile.moduleId).toBe(resolve('./fixtures/reported-tasks/1_first.test.ts')) - expect(testFile.project).toBe(project) + expect(testFile.project.workspaceProject).toBe(project) expect(testFile.children.size).toBe(14) const tests = [...testFile.children.tests()] From 34e2e810103217aeaa267ed598628255be210715 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 30 Jul 2024 11:23:16 +0200 Subject: [PATCH 50/53] chore: cleanup --- .../vitest/src/node/reported-workspace-project.ts | 12 +++++++----- packages/vitest/src/node/workspace.ts | 10 ++++++++-- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/packages/vitest/src/node/reported-workspace-project.ts b/packages/vitest/src/node/reported-workspace-project.ts index bc89b5e3cd7f..11ba36f95d45 100644 --- a/packages/vitest/src/node/reported-workspace-project.ts +++ b/packages/vitest/src/node/reported-workspace-project.ts @@ -19,10 +19,6 @@ export class TestProject { * Resolved project configuration. */ public readonly config: ResolvedProjectConfig - /** - * Serialized project configuration. This is the config that tests receive. - */ - public readonly serializedConfig: SerializedConfig /** * Resolved global configuration. If there are no workspace projects, this will be the same as `config`. */ @@ -33,7 +29,13 @@ export class TestProject { this.vitest = workspaceProject.ctx this.globalConfig = workspaceProject.ctx.config this.config = workspaceProject.config - this.serializedConfig = workspaceProject.serializedConfig + } + + /** + * Serialized project configuration. This is the config that tests receive. + */ + public get serializedConfig() { + return this.workspaceProject.getSerializableConfig() } /** diff --git a/packages/vitest/src/node/workspace.ts b/packages/vitest/src/node/workspace.ts index bf4d9ba07c73..48cb1d4f4f0f 100644 --- a/packages/vitest/src/node/workspace.ts +++ b/packages/vitest/src/node/workspace.ts @@ -420,11 +420,17 @@ export class WorkspaceProject { } getSerializableConfig(): SerializedConfig { + // TODO: serialize the config _once_ or when needed + const config = serializeConfig( + this.config, + this.ctx.config, + this.server.config, + ) if (!this.ctx.configOverride) { - return this.serializedConfig + return config } return deepMerge( - this.serializedConfig, + config, this.ctx.configOverride, ) } From 9850e889a260be163b265598d00bd4cfe33e54dd Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 30 Jul 2024 11:31:52 +0200 Subject: [PATCH 51/53] chore: cleanup --- packages/vitest/src/node/workspace.ts | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/packages/vitest/src/node/workspace.ts b/packages/vitest/src/node/workspace.ts index 48cb1d4f4f0f..40a6a21b5ed8 100644 --- a/packages/vitest/src/node/workspace.ts +++ b/packages/vitest/src/node/workspace.ts @@ -88,7 +88,6 @@ export class WorkspaceProject { configOverride: Partial | undefined config!: ResolvedConfig - serializedConfig!: SerializedConfig server!: ViteDevServer vitenode!: ViteNodeServer runner!: ViteNodeRunner @@ -365,11 +364,6 @@ export class WorkspaceProject { project.server = ctx.server project.runner = ctx.runner project.config = ctx.config - project.serializedConfig = serializeConfig( - ctx.config, - ctx.config, - ctx.server.config, - ) project.testProject = new TestProject(project) return project } @@ -390,11 +384,6 @@ export class WorkspaceProject { server.config, this.ctx.logger, ) - this.serializedConfig = serializeConfig( - this.config, - this.ctx.config, - server.config, - ) this.testProject = new TestProject(this) this.server = server From 8d87774fa35b699921033ccc66a246248cbea4f3 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 30 Jul 2024 11:35:52 +0200 Subject: [PATCH 52/53] chore: cleanup --- .../vitest/src/node/reported-workspace-project.ts | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/packages/vitest/src/node/reported-workspace-project.ts b/packages/vitest/src/node/reported-workspace-project.ts index 11ba36f95d45..0d22aa809d94 100644 --- a/packages/vitest/src/node/reported-workspace-project.ts +++ b/packages/vitest/src/node/reported-workspace-project.ts @@ -24,11 +24,17 @@ export class TestProject { */ public readonly globalConfig: ResolvedConfig + /** + * The name of the project or an empty string if not set. + */ + public readonly name: string + constructor(workspaceProject: WorkspaceProject) { this.workspaceProject = workspaceProject this.vitest = workspaceProject.ctx this.globalConfig = workspaceProject.ctx.config this.config = workspaceProject.config + this.name = workspaceProject.getName() } /** @@ -38,13 +44,6 @@ export class TestProject { return this.workspaceProject.getSerializableConfig() } - /** - * The name of the project or an empty string if not set. - */ - public name(): string { - return this.workspaceProject.getName() - } - /** * Custom context provided to the project. */ @@ -64,7 +63,7 @@ export class TestProject { public toJSON(): SerializedTestProject { return { - name: this.name(), + name: this.name, serializedConfig: this.serializedConfig, context: this.context(), } From a41da6b26707b0caef8883ddc85fdc971ca78d84 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 30 Jul 2024 13:04:14 +0200 Subject: [PATCH 53/53] chore: deprecate Custom, File, Suite, Test and Task types --- packages/vitest/src/public/index.ts | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/packages/vitest/src/public/index.ts b/packages/vitest/src/public/index.ts index b20c4f05243e..3b0293034179 100644 --- a/packages/vitest/src/public/index.ts +++ b/packages/vitest/src/public/index.ts @@ -2,6 +2,13 @@ import '../node/types/vite' import '../types/global' +import type { + Custom as Custom_, + File as File_, + Suite as Suite_, + Task as Task_, + Test as Test_, +} from '@vitest/runner' import type { CollectLineNumbers as CollectLineNumbers_, CollectLines as CollectLines_, @@ -120,16 +127,28 @@ export type RootAndTarget = RootAndTarget_ /** @deprecated import `TypeCheckContext` from `vitest/node` instead */ export type Context = Context_ +/** @deprecated use `RunnerTestSuite` instead */ +export type Suite = Suite_ +/** @deprecated use `RunnerTestFile` instead */ +export type File = File_ +/** @deprecated use `RunnerTestCase` instead */ +export type Test = Test_ +/** @deprecated use `RunnerCustomCase` instead */ +export type Custom = Custom_ +/** @deprecated use `RunnerTask` instead */ +export type Task = Task_ + export type { RunMode, TaskState, TaskBase, TaskResult, TaskResultPack, - Suite, - File, - Test, - Task, + Suite as RunnerTestSuite, + File as RunnerTestFile, + Test as RunnerTestCase, + Task as RunnerTask, + Custom as RunnerCustomCase, DoneCallback, TestFunction, TestOptions, @@ -144,7 +163,6 @@ export type { TestContext, TaskContext, ExtendedContext, - Custom, TaskCustomOptions, OnTestFailedHandler, TaskMeta,