From db6ec84c599eae2a10e262d960de9130b2073a2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ari=20Perkki=C3=B6?= Date: Thu, 14 Nov 2024 15:05:57 +0200 Subject: [PATCH] refactor: move `onTaskUpdate` to its own `TaskParser` --- packages/vitest/src/node/reporters/summary.ts | 153 +++++------------ .../vitest/src/node/reporters/task-parser.ts | 87 ++++++++++ pnpm-lock.yaml | 3 + .../task-parser-tests/example-1.test.ts | 40 +++++ .../task-parser-tests/example-2.test.ts | 40 +++++ test/reporters/package.json | 1 + test/reporters/tests/task-parser.test.ts | 157 ++++++++++++++++++ 7 files changed, 371 insertions(+), 110 deletions(-) create mode 100644 packages/vitest/src/node/reporters/task-parser.ts create mode 100644 test/reporters/fixtures/task-parser-tests/example-1.test.ts create mode 100644 test/reporters/fixtures/task-parser-tests/example-2.test.ts create mode 100644 test/reporters/tests/task-parser.test.ts diff --git a/packages/vitest/src/node/reporters/summary.ts b/packages/vitest/src/node/reporters/summary.ts index bb30200be44ff..25030392d4a7c 100644 --- a/packages/vitest/src/node/reporters/summary.ts +++ b/packages/vitest/src/node/reporters/summary.ts @@ -1,11 +1,13 @@ -import type { Custom, File, Task, TaskResultPack, Test } from '@vitest/runner' +import type { Custom, File, Test } from '@vitest/runner' import type { Vitest } from '../core' import type { Reporter } from '../types/reporter' +import type { HookOptions } from './task-parser' import { getTests } from '@vitest/runner/utils' import c from 'tinyrainbow' import { F_POINTER, F_TREE_NODE_END, F_TREE_NODE_MIDDLE } from './renderers/figures' import { formatProjectName, formatTime, formatTimeString, padSummaryTitle } from './renderers/utils' import { WindowRenderer } from './renderers/windowedRenderer' +import { TaskParser } from './task-parser' const DURATION_UPDATE_INTERVAL_MS = 100 const FINISHED_TEST_CLEANUP_TIME_MS = 1_000 @@ -23,14 +25,6 @@ interface Counter { todo: number } -interface HookOptions { - name: string - file: File - id: File['id'] | Test['id'] - type: Task['type'] - -} - interface SlowTask { name: string visible: boolean @@ -50,8 +44,7 @@ interface RunningTest extends Pick { * Reporter extension that renders summary and forwards all other logs above itself. * Intended to be used by other reporters, not as a standalone reporter. */ -export class SummaryReporter implements Reporter { - private ctx!: Vitest +export class SummaryReporter extends TaskParser implements Reporter { private options!: Options private renderer!: WindowRenderer @@ -98,66 +91,6 @@ export class SummaryReporter implements Reporter { this.suites.total = (paths || []).length } - onTaskUpdate(packs: TaskResultPack[]) { - const startingTestFiles: File[] = [] - const finishedTestFiles: File[] = [] - - const startingTests: (Test | Custom)[] = [] - const finishedTests: (Test | Custom)[] = [] - - const startingHooks: HookOptions[] = [] - const endingHooks: HookOptions[] = [] - - for (const pack of packs) { - const task = this.ctx.state.idMap.get(pack[0]) - - if (task?.type === 'suite' && 'filepath' in task && task.result?.state) { - if (task?.result?.state === 'run') { - startingTestFiles.push(task) - } - else { - // Skipped tests are not reported, do it manually - for (const test of getTests(task)) { - if (!test.result || test.result?.state === 'skip') { - finishedTests.push(test) - } - } - - finishedTestFiles.push(task.file) - } - } - - if (task?.type === 'test' || task?.type === 'custom') { - if (task.result?.state === 'run') { - startingTests.push(task) - } - else if (task.result?.hooks?.afterEach !== 'run') { - finishedTests.push(task) - } - } - - if (task?.result?.hooks) { - for (const [hook, state] of Object.entries(task.result.hooks)) { - if (state === 'run') { - startingHooks.push({ name: hook, file: task.file, id: task.id, type: task.type }) - } - else { - endingHooks.push({ name: hook, file: task.file, id: task.id, type: task.type }) - } - } - } - } - - endingHooks.forEach(hook => this.onHookEnd(hook)) - finishedTests.forEach(test => this.onTestFinished(test)) - finishedTestFiles.forEach(file => this.onTestFileFinished(file)) - - startingTestFiles.forEach(file => this.onTestFilePrepare(file)) - startingTests.forEach(test => this.onTestStart(test)) - startingHooks.forEach(hook => this.onHookStart(hook), - ) - } - onWatcherRerun() { this.runningTests.clear() this.finishedTests.clear() @@ -177,7 +110,7 @@ export class SummaryReporter implements Reporter { clearInterval(this.durationInterval) } - private onTestFilePrepare(file: File) { + onTestFilePrepare(file: File) { if (this.allFinishedTests.has(file.id) || this.runningTests.has(file.id)) { return } @@ -202,40 +135,7 @@ export class SummaryReporter implements Reporter { this.maxParallelTests = Math.max(this.maxParallelTests, this.runningTests.size) } - private getTestStats(test: Test | Custom) { - const file = test.file - let stats = this.runningTests.get(file.id) - - if (!stats) { - // It's possible that that test finished before it's preparation was even reported - this.onTestFilePrepare(test.file) - stats = this.runningTests.get(file.id)! - - // It's also possible that this update came after whole test file was reported as finished - if (!stats) { - return - } - } - - return stats - } - - private getHookStats({ file, id, type }: HookOptions) { - // Track slow running hooks only on verbose mode - if (!this.options.verbose) { - return - } - - const stats = this.runningTests.get(file.id) - - if (!stats) { - return - } - - return type === 'suite' ? stats : stats?.tests.get(id) - } - - private onHookStart(options: HookOptions) { + onHookStart(options: HookOptions) { const stats = this.getHookStats(options) if (!stats) { @@ -258,7 +158,7 @@ export class SummaryReporter implements Reporter { hook.onFinish = () => clearTimeout(timeout) } - private onHookEnd(options: HookOptions) { + onHookEnd(options: HookOptions) { const stats = this.getHookStats(options) if (stats?.hook?.name !== options.name) { @@ -269,7 +169,7 @@ export class SummaryReporter implements Reporter { stats.hook.visible = false } - private onTestStart(test: Test | Custom) { + onTestStart(test: Test | Custom) { // Track slow running tests only on verbose mode if (!this.options.verbose) { return @@ -300,7 +200,7 @@ export class SummaryReporter implements Reporter { stats.tests.set(test.id, slowTest) } - private onTestFinished(test: Test | Custom) { + onTestFinished(test: Test | Custom) { const stats = this.getTestStats(test) if (!stats) { @@ -324,7 +224,7 @@ export class SummaryReporter implements Reporter { } } - private onTestFileFinished(file: File) { + onTestFileFinished(file: File) { if (this.allFinishedTests.has(file.id)) { return } @@ -362,6 +262,39 @@ export class SummaryReporter implements Reporter { } } + private getTestStats(test: Test | Custom) { + const file = test.file + let stats = this.runningTests.get(file.id) + + if (!stats) { + // It's possible that that test finished before it's preparation was even reported + this.onTestFilePrepare(test.file) + stats = this.runningTests.get(file.id)! + + // It's also possible that this update came after whole test file was reported as finished + if (!stats) { + return + } + } + + return stats + } + + private getHookStats({ file, id, type }: HookOptions) { + // Track slow running hooks only on verbose mode + if (!this.options.verbose) { + return + } + + const stats = this.runningTests.get(file.id) + + if (!stats) { + return + } + + return type === 'suite' ? stats : stats?.tests.get(id) + } + private createSummary() { const summary = [''] diff --git a/packages/vitest/src/node/reporters/task-parser.ts b/packages/vitest/src/node/reporters/task-parser.ts new file mode 100644 index 0000000000000..3859594dc44a1 --- /dev/null +++ b/packages/vitest/src/node/reporters/task-parser.ts @@ -0,0 +1,87 @@ +import type { Custom, File, Task, TaskResultPack, Test } from '@vitest/runner' +import type { Vitest } from '../core' +import { getTests } from '@vitest/runner/utils' + +export interface HookOptions { + name: string + file: File + id: File['id'] | Test['id'] + type: Task['type'] +} + +export class TaskParser { + ctx!: Vitest + + onInit(ctx: Vitest) { + this.ctx = ctx + } + + onHookStart(_options: HookOptions) {} + onHookEnd(_options: HookOptions) {} + + onTestStart(_test: Test | Custom) {} + onTestFinished(_test: Test | Custom) {} + + onTestFilePrepare(_file: File) {} + onTestFileFinished(_file: File) {} + + onTaskUpdate(packs: TaskResultPack[]) { + const startingTestFiles: File[] = [] + const finishedTestFiles: File[] = [] + + const startingTests: (Test | Custom)[] = [] + const finishedTests: (Test | Custom)[] = [] + + const startingHooks: HookOptions[] = [] + const endingHooks: HookOptions[] = [] + + for (const pack of packs) { + const task = this.ctx.state.idMap.get(pack[0]) + + if (task?.type === 'suite' && 'filepath' in task && task.result?.state) { + if (task?.result?.state === 'run') { + startingTestFiles.push(task) + } + else { + // Skipped tests are not reported, do it manually + for (const test of getTests(task)) { + if (!test.result || test.result?.state === 'skip') { + finishedTests.push(test) + } + } + + finishedTestFiles.push(task.file) + } + } + + if (task?.type === 'test' || task?.type === 'custom') { + if (task.result?.state === 'run') { + startingTests.push(task) + } + else if (task.result?.hooks?.afterEach !== 'run') { + finishedTests.push(task) + } + } + + if (task?.result?.hooks) { + for (const [hook, state] of Object.entries(task.result.hooks)) { + if (state === 'run') { + startingHooks.push({ name: hook, file: task.file, id: task.id, type: task.type }) + } + else { + endingHooks.push({ name: hook, file: task.file, id: task.id, type: task.type }) + } + } + } + } + + endingHooks.forEach(hook => this.onHookEnd(hook)) + finishedTests.forEach(test => this.onTestFinished(test)) + finishedTestFiles.forEach(file => this.onTestFileFinished(file)) + + startingTestFiles.forEach(file => this.onTestFilePrepare(file)) + startingTests.forEach(test => this.onTestStart(test)) + startingHooks.forEach(hook => this.onHookStart(hook), + ) + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b2b3a03136811..6bf1d64a0f93a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1303,6 +1303,9 @@ importers: test/reporters: devDependencies: + '@vitest/runner': + specifier: workspace:* + version: link:../../packages/runner flatted: specifier: ^3.2.9 version: 3.2.9 diff --git a/test/reporters/fixtures/task-parser-tests/example-1.test.ts b/test/reporters/fixtures/task-parser-tests/example-1.test.ts new file mode 100644 index 0000000000000..1f77707a7eae5 --- /dev/null +++ b/test/reporters/fixtures/task-parser-tests/example-1.test.ts @@ -0,0 +1,40 @@ +import { beforeAll, beforeEach, afterEach, afterAll, test, describe } from "vitest"; +import { setTimeout } from "node:timers/promises"; + +beforeAll(async () => { + await setTimeout(100); +}); + +afterAll(async () => { + await setTimeout(100); +}); + +describe("some suite", async () => { + beforeEach(async () => { + await setTimeout(100); + }); + + test("some test", async () => { + await setTimeout(100); + }); + + afterEach(async () => { + await setTimeout(100); + }); +}); + +test("Fast test 1", () => { + // +}); + +test.skip("Skipped test 1", () => { + // +}); + +test.concurrent("parallel slow tests 1.1", async () => { + await setTimeout(100); +}); + +test.concurrent("parallel slow tests 1.2", async () => { + await setTimeout(100); +}); diff --git a/test/reporters/fixtures/task-parser-tests/example-2.test.ts b/test/reporters/fixtures/task-parser-tests/example-2.test.ts new file mode 100644 index 0000000000000..55ba3fe883e50 --- /dev/null +++ b/test/reporters/fixtures/task-parser-tests/example-2.test.ts @@ -0,0 +1,40 @@ +import { beforeAll, beforeEach, afterEach, afterAll, test, describe } from "vitest"; +import { setTimeout } from "node:timers/promises"; + +beforeAll(async () => { + await setTimeout(100); +}); + +afterAll(async () => { + await setTimeout(100); +}); + +describe("some suite", async () => { + beforeEach(async () => { + await setTimeout(100); + }); + + test("some test", async () => { + await setTimeout(100); + }); + + afterEach(async () => { + await setTimeout(100); + }); +}); + +test("Fast test 1", () => { + // +}); + +test.skip("Skipped test 1", () => { + // +}); + +test.concurrent("parallel slow tests 2.1", async () => { + await setTimeout(100); +}); + +test.concurrent("parallel slow tests 2.2", async () => { + await setTimeout(100); +}); diff --git a/test/reporters/package.json b/test/reporters/package.json index ed03c0a4fa2da..901834b98d025 100644 --- a/test/reporters/package.json +++ b/test/reporters/package.json @@ -6,6 +6,7 @@ "test": "vitest" }, "devDependencies": { + "@vitest/runner": "workspace:*", "flatted": "^3.2.9", "pkg-reporter": "./reportPkg/", "vitest": "workspace:*", diff --git a/test/reporters/tests/task-parser.test.ts b/test/reporters/tests/task-parser.test.ts new file mode 100644 index 0000000000000..51d7954e3f22a --- /dev/null +++ b/test/reporters/tests/task-parser.test.ts @@ -0,0 +1,157 @@ +import type { File, Test } from '@vitest/runner' +import type { WorkspaceSpec } from 'vitest/node' +import type { Reporter } from 'vitest/reporters' +import type { HookOptions } from '../../../packages/vitest/src/node/reporters/task-parser' +import { expect, test } from 'vitest' +import { TaskParser } from '../../../packages/vitest/src/node/reporters/task-parser' +import { runVitest } from '../../test-utils' + +test('tasks are reported in correct order', async () => { + const reporter = new TaskReporter() + + const { stdout, stderr } = await runVitest({ + config: false, + include: ['./fixtures/task-parser-tests/*.test.ts'], + fileParallelism: false, + reporters: [reporter], + sequence: { sequencer: Sorter }, + }) + + expect(stdout).toBe('') + expect(stderr).toBe('') + + expect(reporter.calls).toMatchInlineSnapshot(` + [ + "|fixtures/task-parser-tests/example-1.test.ts| start", + "|fixtures/task-parser-tests/example-1.test.ts| beforeAll start (suite)", + "|fixtures/task-parser-tests/example-1.test.ts| beforeAll end (suite)", + "|fixtures/task-parser-tests/example-1.test.ts| beforeAll end (suite)", + "|fixtures/task-parser-tests/example-1.test.ts| start", + "|fixtures/task-parser-tests/example-1.test.ts| RUN some test", + "|fixtures/task-parser-tests/example-1.test.ts| beforeEach start (test)", + "|fixtures/task-parser-tests/example-1.test.ts| beforeEach end (test)", + "|fixtures/task-parser-tests/example-1.test.ts| RUN some test", + "|fixtures/task-parser-tests/example-1.test.ts| beforeEach end (test)", + "|fixtures/task-parser-tests/example-1.test.ts| afterEach start (test)", + "|fixtures/task-parser-tests/example-1.test.ts| beforeEach end (test)", + "|fixtures/task-parser-tests/example-1.test.ts| afterEach end (test)", + "|fixtures/task-parser-tests/example-1.test.ts| beforeAll end (suite)", + "|fixtures/task-parser-tests/example-1.test.ts| afterAll end (suite)", + "|fixtures/task-parser-tests/example-1.test.ts| beforeEach end (test)", + "|fixtures/task-parser-tests/example-1.test.ts| afterEach end (test)", + "|fixtures/task-parser-tests/example-1.test.ts| beforeEach end (test)", + "|fixtures/task-parser-tests/example-1.test.ts| beforeEach end (test)", + "|fixtures/task-parser-tests/example-1.test.ts| DONE some test", + "|fixtures/task-parser-tests/example-1.test.ts| DONE Fast test 1", + "|fixtures/task-parser-tests/example-1.test.ts| RUN parallel slow tests 1.1", + "|fixtures/task-parser-tests/example-1.test.ts| RUN parallel slow tests 1.2", + "|fixtures/task-parser-tests/example-1.test.ts| beforeEach end (test)", + "|fixtures/task-parser-tests/example-1.test.ts| afterEach end (test)", + "|fixtures/task-parser-tests/example-1.test.ts| beforeEach end (test)", + "|fixtures/task-parser-tests/example-1.test.ts| afterEach end (test)", + "|fixtures/task-parser-tests/example-1.test.ts| beforeAll end (suite)", + "|fixtures/task-parser-tests/example-1.test.ts| DONE parallel slow tests 1.1", + "|fixtures/task-parser-tests/example-1.test.ts| DONE parallel slow tests 1.2", + "|fixtures/task-parser-tests/example-1.test.ts| start", + "|fixtures/task-parser-tests/example-1.test.ts| afterAll start (suite)", + "|fixtures/task-parser-tests/example-1.test.ts| beforeAll end (suite)", + "|fixtures/task-parser-tests/example-1.test.ts| afterAll end (suite)", + "|fixtures/task-parser-tests/example-1.test.ts| DONE Skipped test 1", + "|fixtures/task-parser-tests/example-1.test.ts| finish", + "|fixtures/task-parser-tests/example-2.test.ts| start", + "|fixtures/task-parser-tests/example-2.test.ts| beforeAll start (suite)", + "|fixtures/task-parser-tests/example-2.test.ts| beforeAll end (suite)", + "|fixtures/task-parser-tests/example-2.test.ts| beforeAll end (suite)", + "|fixtures/task-parser-tests/example-2.test.ts| start", + "|fixtures/task-parser-tests/example-2.test.ts| RUN some test", + "|fixtures/task-parser-tests/example-2.test.ts| beforeEach start (test)", + "|fixtures/task-parser-tests/example-2.test.ts| beforeEach end (test)", + "|fixtures/task-parser-tests/example-2.test.ts| RUN some test", + "|fixtures/task-parser-tests/example-2.test.ts| beforeEach end (test)", + "|fixtures/task-parser-tests/example-2.test.ts| afterEach start (test)", + "|fixtures/task-parser-tests/example-2.test.ts| beforeEach end (test)", + "|fixtures/task-parser-tests/example-2.test.ts| afterEach end (test)", + "|fixtures/task-parser-tests/example-2.test.ts| beforeAll end (suite)", + "|fixtures/task-parser-tests/example-2.test.ts| afterAll end (suite)", + "|fixtures/task-parser-tests/example-2.test.ts| beforeEach end (test)", + "|fixtures/task-parser-tests/example-2.test.ts| afterEach end (test)", + "|fixtures/task-parser-tests/example-2.test.ts| beforeEach end (test)", + "|fixtures/task-parser-tests/example-2.test.ts| beforeEach end (test)", + "|fixtures/task-parser-tests/example-2.test.ts| DONE some test", + "|fixtures/task-parser-tests/example-2.test.ts| DONE Fast test 1", + "|fixtures/task-parser-tests/example-2.test.ts| RUN parallel slow tests 2.1", + "|fixtures/task-parser-tests/example-2.test.ts| RUN parallel slow tests 2.2", + "|fixtures/task-parser-tests/example-2.test.ts| beforeEach end (test)", + "|fixtures/task-parser-tests/example-2.test.ts| afterEach end (test)", + "|fixtures/task-parser-tests/example-2.test.ts| beforeEach end (test)", + "|fixtures/task-parser-tests/example-2.test.ts| afterEach end (test)", + "|fixtures/task-parser-tests/example-2.test.ts| beforeAll end (suite)", + "|fixtures/task-parser-tests/example-2.test.ts| DONE parallel slow tests 2.1", + "|fixtures/task-parser-tests/example-2.test.ts| DONE parallel slow tests 2.2", + "|fixtures/task-parser-tests/example-2.test.ts| start", + "|fixtures/task-parser-tests/example-2.test.ts| afterAll start (suite)", + "|fixtures/task-parser-tests/example-2.test.ts| beforeAll end (suite)", + "|fixtures/task-parser-tests/example-2.test.ts| afterAll end (suite)", + "|fixtures/task-parser-tests/example-2.test.ts| DONE Skipped test 1", + "|fixtures/task-parser-tests/example-2.test.ts| finish", + ] + `) +}) + +class TaskReporter extends TaskParser implements Reporter { + calls: string[] = [] + + // @ts-expect-error -- not sure why + onInit(ctx) { + super.onInit(ctx) + } + + onTestFilePrepare(file: File) { + this.calls.push(`|${file.name}| start`) + } + + onTestFileFinished(file: File) { + this.calls.push(`|${file.name}| finish`) + } + + onTestStart(test: Test) { + this.calls.push(`|${test.file.name}| RUN ${test.name}`) + } + + onTestFinished(test: Test) { + this.calls.push(`|${test.file.name}| DONE ${test.name}`) + } + + onHookStart(options: HookOptions) { + this.calls.push(`|${options.file.name}| ${options.name} start (${options.type})`) + } + + onHookEnd(options: HookOptions) { + this.calls.push(`|${options.file.name}| ${options.name} end (${options.type})`) + } +} + +class Sorter { + sort(files: WorkspaceSpec[]) { + return files.sort((a, b) => { + const idA = Number.parseInt( + a.moduleId.match(/example-(\d*)\.test\.ts/)![1], + ) + const idB = Number.parseInt( + b.moduleId.match(/example-(\d*)\.test\.ts/)![1], + ) + + if (idA > idB) { + return 1 + } + if (idA < idB) { + return -1 + } + return 0 + }) + } + + shard(files: WorkspaceSpec[]) { + return files + } +}