From 5f3442aba28cd5498c85a990a1ab3fa34b78f562 Mon Sep 17 00:00:00 2001 From: mzhubail Date: Tue, 27 Aug 2024 09:48:36 +0300 Subject: [PATCH 01/28] Allow passing test location in cli * Add function to Parse filters + filter interface. Goal is to Parse filters and pass to `interpretTaskModes`. Attempt to parse each filter with the format `:`, while keeping mind that some files might have `:` in their filename. Throw an error when line range is provided. Note that there is an edge case when a file has `:` and `-` in its filename. * Add `FileSpec` type In `globTestSpecs`, the filters will be matched against the files, and if there is a location in the filter, it will be included with its file, in a `FileSpec: { path, testLocations? }`. I thought it would be a good idea to include the files with the location in the same object, instead of having them in their own attribute in the config and then matching based on the path or something. But I ended up having to modify types everywhere to get this to work. I also had to modify `groupFilesByEnv` to include the `testLocations` in its output, and also modify pools to pass the test locations to `runFiles`. - Move the filter parsing inside `globTestSpecs` and revert related type changes * Report disabled `includeTaskLocation` Add and handle new error `IncludeTasklocationDisabledError` * Report file not found if a test location was specified - The reason for this is that we're using a more strict matching that requires full path to be specified * Report wrong line number - Got rid of the confusing warning. Current behavior is that if user specifies a file with and without line numbers, it will not have an effect (e.g. `test.ts:3 test.ts` will match on line 3 only, not all files). --- packages/runner/src/collect.ts | 10 +- packages/runner/src/run.ts | 13 +- packages/runner/src/types/runner.ts | 5 + packages/runner/src/utils/collect.ts | 102 ++-- packages/vitest/src/node/cli/cli-api.ts | 55 +- packages/vitest/src/node/core.ts | 45 +- packages/vitest/src/node/errors.ts | 8 + packages/vitest/src/node/pools/forks.ts | 13 +- packages/vitest/src/node/pools/threads.ts | 14 +- packages/vitest/src/node/pools/vmForks.ts | 11 +- packages/vitest/src/node/pools/vmThreads.ts | 12 +- packages/vitest/src/node/project.ts | 7 +- packages/vitest/src/node/spec.ts | 3 + packages/vitest/src/node/workspace.ts | 494 ++++++++++++++++++ packages/vitest/src/runtime/runBaseTests.ts | 5 +- packages/vitest/src/runtime/runVmTests.ts | 6 +- packages/vitest/src/runtime/workers/base.ts | 12 +- packages/vitest/src/runtime/workers/vm.ts | 12 +- packages/vitest/src/typecheck/collect.ts | 2 + packages/vitest/src/types/worker.ts | 3 +- packages/vitest/src/utils/test-helpers.ts | 14 +- .../fixtures/location-filters/basic.test.ts | 21 + .../location-filters/custom.config.ts | 9 + .../location-filters/describe-error.test.ts | 9 + .../fixtures/location-filters/fail.config.ts | 7 + .../fixtures/location-filters/math.test.ts | 9 + .../no-task-location.config.ts | 10 + .../location-filters/top-level-error.test.ts | 1 + .../location-filters/vitest.config.ts | 14 + test/cli/test/location-filters.test.ts | 154 ++++++ test/config/tsconfig.vitest-temp.json | 114 ++++ 31 files changed, 1116 insertions(+), 78 deletions(-) create mode 100644 packages/vitest/src/node/workspace.ts create mode 100644 test/cli/fixtures/location-filters/basic.test.ts create mode 100644 test/cli/fixtures/location-filters/custom.config.ts create mode 100644 test/cli/fixtures/location-filters/describe-error.test.ts create mode 100644 test/cli/fixtures/location-filters/fail.config.ts create mode 100644 test/cli/fixtures/location-filters/math.test.ts create mode 100644 test/cli/fixtures/location-filters/no-task-location.config.ts create mode 100644 test/cli/fixtures/location-filters/top-level-error.test.ts create mode 100644 test/cli/fixtures/location-filters/vitest.config.ts create mode 100644 test/cli/test/location-filters.test.ts create mode 100644 test/config/tsconfig.vitest-temp.json diff --git a/packages/runner/src/collect.ts b/packages/runner/src/collect.ts index 1c9354cfe107..f0f4eaabc1b3 100644 --- a/packages/runner/src/collect.ts +++ b/packages/runner/src/collect.ts @@ -1,4 +1,4 @@ -import type { VitestRunner } from './types/runner' +import type { FileSpec, VitestRunner } from './types/runner' import type { File, SuiteHooks } from './types/tasks' import { toArray } from '@vitest/utils' import { processError } from '@vitest/utils/error' @@ -20,14 +20,17 @@ import { const now = globalThis.performance ? globalThis.performance.now.bind(globalThis.performance) : Date.now export async function collectTests( - paths: string[], + specs: string[] | FileSpec[], runner: VitestRunner, ): Promise { const files: File[] = [] const config = runner.config - for (const filepath of paths) { + for (const spec of specs) { + const filepath = typeof spec === 'string' ? spec : spec.filepath + const testLocations = typeof spec === 'string' ? undefined : spec.testLocations + const file = createFileTask(filepath, config.root, config.name, runner.pool) runner.onCollectStart?.(file) @@ -97,6 +100,7 @@ export async function collectTests( interpretTaskModes( file, config.testNamePattern, + testLocations, hasOnlyTasks, false, config.allowOnly, diff --git a/packages/runner/src/run.ts b/packages/runner/src/run.ts index b7b03e34ac1d..78721efc65af 100644 --- a/packages/runner/src/run.ts +++ b/packages/runner/src/run.ts @@ -1,6 +1,6 @@ import type { Awaitable } from '@vitest/utils' import type { DiffOptions } from '@vitest/utils/diff' -import type { VitestRunner } from './types/runner' +import type { FileSpec, VitestRunner } from './types/runner' import type { Custom, File, @@ -498,10 +498,11 @@ export async function runFiles(files: File[], runner: VitestRunner): Promise { +export async function startTests(specs: FileSpec[], runner: VitestRunner): Promise { + const paths = specs.map(f => f.filepath) await runner.onBeforeCollect?.(paths) - const files = await collectTests(paths, runner) + const files = await collectTests(specs, runner) await runner.onCollected?.(files) await runner.onBeforeRunFiles?.(files) @@ -515,10 +516,12 @@ export async function startTests(paths: string[], runner: VitestRunner): Promise return files } -async function publicCollect(paths: string[], runner: VitestRunner): Promise { +async function publicCollect(specs: string[] | FileSpec[], runner: VitestRunner): Promise { + const paths = specs.map(f => typeof f === 'string' ? f : f.filepath) + await runner.onBeforeCollect?.(paths) - const files = await collectTests(paths, runner) + const files = await collectTests(specs, runner) await runner.onCollected?.(files) return files diff --git a/packages/runner/src/types/runner.ts b/packages/runner/src/types/runner.ts index ade94a143d1d..9fb34109303f 100644 --- a/packages/runner/src/types/runner.ts +++ b/packages/runner/src/types/runner.ts @@ -40,6 +40,11 @@ export interface VitestRunnerConfig { diffOptions?: DiffOptions } +export interface FileSpec { + filepath: string + testLocations: number[] | undefined +} + export type VitestRunnerImportSource = 'collect' | 'setup' export interface VitestRunnerConstructor { diff --git a/packages/runner/src/utils/collect.ts b/packages/runner/src/utils/collect.ts index bd72b99e66b8..520654092e0a 100644 --- a/packages/runner/src/utils/collect.ts +++ b/packages/runner/src/utils/collect.ts @@ -6,53 +6,95 @@ import { relative } from 'pathe' * If any tasks been marked as `only`, mark all other tasks as `skip`. */ export function interpretTaskModes( - suite: Suite, + file: Suite, namePattern?: string | RegExp, + testLocations?: number[] | undefined, onlyMode?: boolean, parentIsOnly?: boolean, allowOnly?: boolean, ): void { - const suiteIsOnly = parentIsOnly || suite.mode === 'only' + const matchedLocations: number[] = [] - suite.tasks.forEach((t) => { - // Check if either the parent suite or the task itself are marked as included - const includeTask = suiteIsOnly || t.mode === 'only' - if (onlyMode) { - if (t.type === 'suite' && (includeTask || someTasksAreOnly(t))) { - // Don't skip this suite - if (t.mode === 'only') { + const traverseSuite = (suite: Suite) => { + const suiteIsOnly = parentIsOnly || suite.mode === 'only' + + suite.tasks.forEach((t) => { + // Check if either the parent suite or the task itself are marked as included + const includeTask = suiteIsOnly || t.mode === 'only' + if (onlyMode) { + if (t.type === 'suite' && (includeTask || someTasksAreOnly(t))) { + // Don't skip this suite + if (t.mode === 'only') { + checkAllowOnly(t, allowOnly) + t.mode = 'run' + } + } + else if (t.mode === 'run' && !includeTask) { + t.mode = 'skip' + } + else if (t.mode === 'only') { checkAllowOnly(t, allowOnly) t.mode = 'run' } } - else if (t.mode === 'run' && !includeTask) { - t.mode = 'skip' + if (t.type === 'test') { + if (namePattern && !getTaskFullName(t).match(namePattern)) { + t.mode = 'skip' + } + + // Match test location against provided locations, only run if present + // in `testLocations`. Note: if `includeTaskLocations` is not enabled, + // all test will be skipped. + if (testLocations !== undefined && testLocations.length !== 0) { + if (t.location && testLocations?.includes(t.location.line)) { + t.mode = 'run' + matchedLocations.push(t.location.line) + } + else { + t.mode = 'skip' + } + } } - else if (t.mode === 'only') { - checkAllowOnly(t, allowOnly) - t.mode = 'run' + else if (t.type === 'suite') { + if (t.mode === 'skip') { + skipAllTasks(t) + } + else { + traverseSuite(t) + // interpretTaskModes(t, namePattern, locationFilters, onlyMode, includeTask, allowOnly) + } } - } - if (t.type === 'test') { - if (namePattern && !getTaskFullName(t).match(namePattern)) { - t.mode = 'skip' + }) + + // if all subtasks are skipped, mark as skip + if (suite.mode === 'run') { + if (suite.tasks.length && suite.tasks.every(i => i.mode !== 'run')) { + suite.mode = 'skip' } } - else if (t.type === 'suite') { - if (t.mode === 'skip') { - skipAllTasks(t) - } - else { - interpretTaskModes(t, namePattern, onlyMode, includeTask, allowOnly) + } + + traverseSuite(file) + + const nonMatching = testLocations?.filter(loc => !matchedLocations.includes(loc)) + if (nonMatching && nonMatching.length !== 0) { + const message = nonMatching.length === 1 + ? `line ${nonMatching[0]}` + : `lines ${nonMatching.join(', ')}` + + if (file.result === undefined) { + file.result = { + state: 'fail', + errors: [], } } - }) - - // if all subtasks are skipped, mark as skip - if (suite.mode === 'run') { - if (suite.tasks.length && suite.tasks.every(i => i.mode !== 'run')) { - suite.mode = 'skip' + if (file.result.errors === undefined) { + file.result.errors = [] } + + file.result.errors.push( + processError(new Error(`No test found in ${file.name} in ${message}`)), + ) } } diff --git a/packages/vitest/src/node/cli/cli-api.ts b/packages/vitest/src/node/cli/cli-api.ts index e36d07432768..9727124913f8 100644 --- a/packages/vitest/src/node/cli/cli-api.ts +++ b/packages/vitest/src/node/cli/cli-api.ts @@ -8,8 +8,9 @@ import { mkdirSync, writeFileSync } from 'node:fs' import { getNames, getTests } from '@vitest/runner/utils' import { dirname, relative, resolve } from 'pathe' import { CoverageProviderMap } from '../../integrations/coverage' +import { groupBy } from '../../utils/base' import { createVitest } from '../create' -import { FilesNotFoundError, GitNotFoundError } from '../errors' +import { FilesNotFoundError, GitNotFoundError, IncludeTaskLocationDisabledError } from '../errors' import { registerConsoleShortcuts } from '../stdin' export interface CliOptions extends UserConfig { @@ -103,6 +104,11 @@ export async function startVitest( return ctx } + if (e instanceof IncludeTaskLocationDisabledError) { + ctx.logger.printError(e, { verbose: false }) + return ctx + } + process.exitCode = 1 ctx.logger.printError(e, { fullStack: true, type: 'Unhandled Error' }) ctx.logger.error('\n\n') @@ -278,6 +284,53 @@ export function formatCollectedAsString(files: File[]) { }).flat() } +export function parseFilter(f: string) { + const colonIndex = f.indexOf(':') + if (colonIndex === -1) { + return { filename: f } + } + + const [parsedFilename, lineNumber] = [ + f.substring(0, colonIndex), + f.substring(colonIndex + 1), + ] + + if (lineNumber.match(/^\d+$/)) { + return { + filename: parsedFilename, + lineNumber: Number.parseInt(lineNumber), + } + } + else if (lineNumber.includes('-')) { + throw new Error('Range line numbers are not allowed') + } + else { + return { filename: f } + } +} + +interface Filter { + filename: string + lineNumber?: undefined | number +} + +export function groupFilters(filters: Filter[]) { + const groupedFilters_ = groupBy(filters, f => f.filename) + const groupedFilters = Object.fromEntries(Object.entries(groupedFilters_) + .map((entry) => { + const [filename, filters] = entry + const testLocations = filters.map(f => f.lineNumber) + + return [ + filename, + testLocations.filter(l => l !== undefined) as number[], + ] + }), + ) + + return groupedFilters +} + const envPackageNames: Record< Exclude, string diff --git a/packages/vitest/src/node/core.ts b/packages/vitest/src/node/core.ts index 4d54e6b763df..3e7413cc9bcb 100644 --- a/packages/vitest/src/node/core.ts +++ b/packages/vitest/src/node/core.ts @@ -11,6 +11,7 @@ import type { ResolvedConfig, UserConfig, VitestRunMode } from './types/config' import type { CoverageProvider } from './types/coverage' import type { Reporter } from './types/reporter' import { existsSync, promises as fs, readFileSync } from 'node:fs' +import { resolve } from 'node:path' import { getTasks, hasFailed } from '@vitest/runner/utils' import { SnapshotManager } from '@vitest/snapshot/manager' import { noop, slash, toArray } from '@vitest/utils' @@ -25,8 +26,9 @@ import { getCoverageProvider } from '../integrations/coverage' import { distDir } from '../paths' import { wildcardPatternToRegExp } from '../utils/base' import { VitestCache } from './cache' +import { groupFilters, parseFilter } from './cli/cli-api' import { resolveConfig } from './config/resolveConfig' -import { FilesNotFoundError, GitNotFoundError } from './errors' +import { FilesNotFoundError, GitNotFoundError, IncludeTaskLocationDisabledError } from './errors' import { Logger } from './logger' import { VitestPackageInstaller } from './packageInstaller' import { createPool } from './pool' @@ -418,6 +420,13 @@ export class Vitest { await this.globTestFiles(filters), ) + if ( + !this.config.includeTaskLocation + && files.some(spec => spec.testLocations && spec.testLocations.length !== 0) + ) { + throw new IncludeTaskLocationDisabledError() + } + // if run with --changed, don't exit if no tests are found if (!files.length) { // Report coverage for uncovered files @@ -1144,19 +1153,47 @@ export class Vitest { public async globTestSpecs(filters: string[] = []) { const files: TestSpecification[] = [] + const parsedFilters = filters.map(f => parseFilter(f)) + const testLocations = groupFilters(parsedFilters.map( + f => ({ ...f, filename: resolve(f.filename) }), + )) + + // Key is file and val sepcifies whether we have matched this file with testLocation + const testLocHasMatch: { [f: string]: boolean } = {} + await Promise.all(this.projects.map(async (project) => { - const { testFiles, typecheckTestFiles } = await project.globTestFiles(filters) + const { testFiles, typecheckTestFiles } = await project.globTestFiles( + parsedFilters.map(f => f.filename), + ) + testFiles.forEach((file) => { - const spec = project.createSpecification(file) + const loc = testLocations[file] + testLocHasMatch[file] = true + + const spec = project.createSpecification(file, undefined, loc) this.ensureSpecCached(spec) files.push(spec) }) typecheckTestFiles.forEach((file) => { - const spec = project.createSpecification(file, 'typescript') + const loc = testLocations[file] + testLocHasMatch[file] = true + + const spec = project.createSpecification(file, 'typescript', loc) this.ensureSpecCached(spec) files.push(spec) }) })) + + Object.entries(testLocations).forEach(([filepath, loc]) => { + if (loc.length !== 0 && !testLocHasMatch[filepath]) { + const rel = relative(this.config.dir || this.config.root, filepath) + + this.logger.printError(new Error(`Couldn\'t find file "${rel}".\n` + + 'Note when specifying the test location you have to specify the full test filename.', + )) + } + }) + return files as WorkspaceSpec[] } diff --git a/packages/vitest/src/node/errors.ts b/packages/vitest/src/node/errors.ts index 296458134d67..dd1841a19f4f 100644 --- a/packages/vitest/src/node/errors.ts +++ b/packages/vitest/src/node/errors.ts @@ -13,3 +13,11 @@ export class GitNotFoundError extends Error { super('Could not find Git root. Have you initialized git with `git init`?') } } + +export class IncludeTaskLocationDisabledError extends Error { + code = 'VITEST_INCLUDE_TASK_LOCATION_DISABLED' + + constructor() { + super('Recieved line number filters while `includeTaskLocation` option is disabled') + } +} diff --git a/packages/vitest/src/node/pools/forks.ts b/packages/vitest/src/node/pools/forks.ts index 65bd2b0e9f33..0b61039e2655 100644 --- a/packages/vitest/src/node/pools/forks.ts +++ b/packages/vitest/src/node/pools/forks.ts @@ -101,11 +101,13 @@ export function createForksPool( async function runFiles( project: TestProject, config: SerializedConfig, - files: string[], + files: FileSpec[], environment: ContextTestEnvironment, invalidates: string[] = [], ) { - ctx.state.clearFiles(project, files) + const paths = files.map(f => f.filepath) + ctx.state.clearFiles(project, paths) + const { channel, cleanup } = createChildProcessChannel(project) const workerId = ++id const data: ContextRPC = { @@ -129,7 +131,7 @@ export function createForksPool( && /Failed to terminate worker/.test(error.message) ) { ctx.state.addProcessTimeoutCause( - `Failed to terminate worker while running ${files.join(', ')}.`, + `Failed to terminate worker while running ${paths.join(', ')}.`, ) } // Intentionally cancelled @@ -138,7 +140,7 @@ export function createForksPool( && error instanceof Error && /The task has been cancelled/.test(error.message) ) { - ctx.state.cancelFiles(files, project) + ctx.state.cancelFiles(paths, project) } else { throw error @@ -263,11 +265,10 @@ export function createForksPool( // Always run environments isolated between each other await pool.recycleWorkers() - const filenames = files.map(f => f.file) await runFiles( files[0].project, getConfig(files[0].project), - filenames, + files.map(f => f.file), files[0].environment, invalidates, ) diff --git a/packages/vitest/src/node/pools/threads.ts b/packages/vitest/src/node/pools/threads.ts index 26cc9d27d1b6..71f5e024688c 100644 --- a/packages/vitest/src/node/pools/threads.ts +++ b/packages/vitest/src/node/pools/threads.ts @@ -1,3 +1,4 @@ +import type { FileSpec } from '@vitest/runner/types/runner' import type { Options as TinypoolOptions } from 'tinypool' import type { RunnerRPC, RuntimeRPC } from '../../types/rpc' import type { ContextTestEnvironment } from '../../types/worker' @@ -95,11 +96,13 @@ export function createThreadsPool( async function runFiles( project: TestProject, config: SerializedConfig, - files: string[], + files: FileSpec[], environment: ContextTestEnvironment, invalidates: string[] = [], ) { - ctx.state.clearFiles(project, files) + const paths = files.map(f => f.filepath) + ctx.state.clearFiles(project, paths) + const { workerPort, port } = createWorkerChannel(project) const workerId = ++id const data: WorkerContext = { @@ -124,7 +127,7 @@ export function createThreadsPool( && /Failed to terminate worker/.test(error.message) ) { ctx.state.addProcessTimeoutCause( - `Failed to terminate worker while running ${files.join( + `Failed to terminate worker while running ${paths.join( ', ', )}. \nSee https://vitest.dev/guide/common-errors.html#failed-to-terminate-worker for troubleshooting.`, ) @@ -135,7 +138,7 @@ export function createThreadsPool( && error instanceof Error && /The task has been cancelled/.test(error.message) ) { - ctx.state.cancelFiles(files, project) + ctx.state.cancelFiles(paths, project) } else { throw error @@ -259,11 +262,10 @@ export function createThreadsPool( // Always run environments isolated between each other await pool.recycleWorkers() - const filenames = files.map(f => f.file) await runFiles( files[0].project, getConfig(files[0].project), - filenames, + files.map(f => f.file), files[0].environment, invalidates, ) diff --git a/packages/vitest/src/node/pools/vmForks.ts b/packages/vitest/src/node/pools/vmForks.ts index 70443a069853..ed5a0e08de49 100644 --- a/packages/vitest/src/node/pools/vmForks.ts +++ b/packages/vitest/src/node/pools/vmForks.ts @@ -1,3 +1,4 @@ +import type { FileSpec } from '@vitest/runner/types/runner' import type { TinypoolChannel, Options as TinypoolOptions } from 'tinypool' import type { RunnerRPC, RuntimeRPC } from '../../types/rpc' import type { ContextRPC, ContextTestEnvironment } from '../../types/worker' @@ -109,11 +110,13 @@ export function createVmForksPool( async function runFiles( project: TestProject, config: SerializedConfig, - files: string[], + files: FileSpec[], environment: ContextTestEnvironment, invalidates: string[] = [], ) { - ctx.state.clearFiles(project, files) + const paths = files.map(f => f.filepath) + ctx.state.clearFiles(project, paths) + const { channel, cleanup } = createChildProcessChannel(project) const workerId = ++id const data: ContextRPC = { @@ -137,7 +140,7 @@ export function createVmForksPool( && /Failed to terminate worker/.test(error.message) ) { ctx.state.addProcessTimeoutCause( - `Failed to terminate worker while running ${files.join(', ')}.`, + `Failed to terminate worker while running ${paths.join(', ')}.`, ) } // Intentionally cancelled @@ -146,7 +149,7 @@ export function createVmForksPool( && error instanceof Error && /The task has been cancelled/.test(error.message) ) { - ctx.state.cancelFiles(files, project) + ctx.state.cancelFiles(paths, project) } else { throw error diff --git a/packages/vitest/src/node/pools/vmThreads.ts b/packages/vitest/src/node/pools/vmThreads.ts index 3587dbc4fac0..68e2620e82d0 100644 --- a/packages/vitest/src/node/pools/vmThreads.ts +++ b/packages/vitest/src/node/pools/vmThreads.ts @@ -100,11 +100,13 @@ export function createVmThreadsPool( async function runFiles( project: TestProject, config: SerializedConfig, - files: string[], + files: FileSpec[], environment: ContextTestEnvironment, invalidates: string[] = [], ) { - ctx.state.clearFiles(project, files) + const paths = files.map(f => f.filepath) + ctx.state.clearFiles(project, paths) + const { workerPort, port } = createWorkerChannel(project) const workerId = ++id const data: WorkerContext = { @@ -112,7 +114,7 @@ export function createVmThreadsPool( worker, port: workerPort, config, - files, + files: paths, invalidates, environment, workerId, @@ -129,7 +131,7 @@ export function createVmThreadsPool( && /Failed to terminate worker/.test(error.message) ) { ctx.state.addProcessTimeoutCause( - `Failed to terminate worker while running ${files.join( + `Failed to terminate worker while running ${paths.join( ', ', )}. \nSee https://vitest.dev/guide/common-errors.html#failed-to-terminate-worker for troubleshooting.`, ) @@ -140,7 +142,7 @@ export function createVmThreadsPool( && error instanceof Error && /The task has been cancelled/.test(error.message) ) { - ctx.state.cancelFiles(files, project) + ctx.state.cancelFiles(paths, project) } else { throw error diff --git a/packages/vitest/src/node/project.ts b/packages/vitest/src/node/project.ts index e25c03992735..fce4f51b954b 100644 --- a/packages/vitest/src/node/project.ts +++ b/packages/vitest/src/node/project.ts @@ -131,11 +131,16 @@ export class TestProject { * Creates a new test specification. Specifications describe how to run tests. * @param moduleId The file path */ - public createSpecification(moduleId: string, pool?: string): TestSpecification { + public createSpecification( + moduleId: string, + pool?: string, + testLocations?: number[] | undefined, + ): TestSpecification { return new TestSpecification( this, moduleId, pool || getFilePoolName(this, moduleId), + testLocations, ) } diff --git a/packages/vitest/src/node/spec.ts b/packages/vitest/src/node/spec.ts index 96e12a8580fd..a076c2be0253 100644 --- a/packages/vitest/src/node/spec.ts +++ b/packages/vitest/src/node/spec.ts @@ -19,6 +19,7 @@ export class TestSpecification { public readonly project: TestProject public readonly moduleId: string public readonly pool: Pool + public readonly testLocations: number[] | undefined // public readonly location: WorkspaceSpecLocation | undefined constructor( @@ -26,6 +27,7 @@ export class TestSpecification { moduleId: string, pool: Pool, // location?: WorkspaceSpecLocation | undefined, + testLocations: number[] | undefined, ) { this[0] = project this[1] = moduleId @@ -33,6 +35,7 @@ export class TestSpecification { this.project = project this.moduleId = moduleId this.pool = pool + this.testLocations = testLocations // this.location = location } diff --git a/packages/vitest/src/node/workspace.ts b/packages/vitest/src/node/workspace.ts new file mode 100644 index 000000000000..7867b513e599 --- /dev/null +++ b/packages/vitest/src/node/workspace.ts @@ -0,0 +1,494 @@ +import type { + TransformResult, + ViteDevServer, + InlineConfig as ViteInlineConfig, +} from 'vite' +import type { Typechecker } from '../typecheck/typechecker' +import type { ProvidedContext } from '../types/general' +import type { Vitest } from './core' +import type { GlobalSetupFile } from './globalSetup' +import type { WorkspaceSpec as DeprecatedWorkspaceSpec } from './pool' +import type { BrowserServer } from './types/browser' +import type { + ResolvedConfig, + SerializedConfig, + UserConfig, + UserWorkspaceConfig, +} from './types/config' +import { promises as fs } from 'node:fs' +import { rm } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { deepMerge, nanoid } from '@vitest/utils' +import fg from 'fast-glob' +import mm from 'micromatch' +import { + dirname, + isAbsolute, + join, + relative, + resolve, + toNamespacedPath, +} from 'pathe' +import { ViteNodeRunner } from 'vite-node/client' +import { ViteNodeServer } from 'vite-node/server' +import { setup } from '../api/setup' +import { isBrowserEnabled, resolveConfig } from './config/resolveConfig' +import { serializeConfig } from './config/serializeConfig' +import { loadGlobalSetupFiles } from './globalSetup' +import { CoverageTransform } from './plugins/coverageTransform' +import { MocksPlugins } from './plugins/mocks' +import { WorkspaceVitestPlugin } from './plugins/workspace' +import { TestProject } from './reported-workspace-project' +import { TestSpecification } from './spec' +import { createViteServer } from './vite' + +interface InitializeProjectOptions extends UserWorkspaceConfig { + workspaceConfigPath: string + extends?: string +} + +export async function initializeProject( + workspacePath: string | number, + ctx: Vitest, + options: InitializeProjectOptions, +) { + const project = new WorkspaceProject(workspacePath, ctx, options) + + const root + = options.root + || (typeof workspacePath === 'number' + ? undefined + : workspacePath.endsWith('/') + ? workspacePath + : dirname(workspacePath)) + + const configFile = options.extends + ? resolve(dirname(options.workspaceConfigPath), options.extends) + : typeof workspacePath === 'number' || workspacePath.endsWith('/') + ? false + : workspacePath + + const config: ViteInlineConfig = { + ...options, + root, + configFile, + // this will make "mode": "test" | "benchmark" inside defineConfig + mode: options.test?.mode || options.mode || ctx.config.mode, + plugins: [ + ...(options.plugins || []), + WorkspaceVitestPlugin(project, { ...options, root, workspacePath }), + ], + } + + await createViteServer(config) + + return project +} + +export class WorkspaceProject { + configOverride: Partial | undefined + + config!: ResolvedConfig + server!: ViteDevServer + vitenode!: ViteNodeServer + runner!: ViteNodeRunner + browser?: BrowserServer + typechecker?: Typechecker + + closingPromise: Promise | undefined + + testFilesList: string[] | null = null + typecheckFilesList: string[] | null = null + + public testProject!: TestProject + + public readonly id = nanoid() + public readonly tmpDir = join(tmpdir(), this.id) + + private _globalSetups: GlobalSetupFile[] | undefined + private _provided: ProvidedContext = {} as any + + constructor( + public path: string | number, + public ctx: Vitest, + public options?: InitializeProjectOptions, + ) {} + + getName(): string { + return this.config.name || '' + } + + isCore() { + return this.ctx.getCoreWorkspaceProject() === this + } + + provide( + key: T, + value: ProvidedContext[T], + ) { + try { + structuredClone(value) + } + catch (err) { + throw new Error( + `Cannot provide "${key}" because it's not serializable.`, + { + cause: err, + }, + ) + } + (this._provided as any)[key] = value + } + + getProvidedContext(): ProvidedContext { + if (this.isCore()) { + return this._provided + } + // globalSetup can run even if core workspace is not part of the test run + // so we need to inherit its provided context + return { + ...this.ctx.getCoreWorkspaceProject().getProvidedContext(), + ...this._provided, + } + } + + public createSpec( + moduleId: string, + pool: string, + testLocations?: number[] | undefined, + ): DeprecatedWorkspaceSpec { + return new TestSpecification(this, moduleId, pool, testLocations) as DeprecatedWorkspaceSpec + } + + async initializeGlobalSetup() { + if (this._globalSetups) { + return + } + + this._globalSetups = await loadGlobalSetupFiles( + this.runner, + this.config.globalSetup, + ) + + for (const globalSetupFile of this._globalSetups) { + const teardown = await globalSetupFile.setup?.({ + provide: (key, value) => this.provide(key, value), + config: this.config, + }) + if (teardown == null || !!globalSetupFile.teardown) { + continue + } + if (typeof teardown !== 'function') { + throw new TypeError( + `invalid return value in globalSetup file ${globalSetupFile.file}. Must return a function`, + ) + } + globalSetupFile.teardown = teardown + } + } + + async teardownGlobalSetup() { + if (!this._globalSetups) { + return + } + for (const globalSetupFile of [...this._globalSetups].reverse()) { + await globalSetupFile.teardown?.() + } + } + + get logger() { + return this.ctx.logger + } + + // it's possible that file path was imported with different queries (?raw, ?url, etc) + getModulesByFilepath(file: string) { + const set + = this.server.moduleGraph.getModulesByFile(file) + || this.browser?.vite.moduleGraph.getModulesByFile(file) + return set || new Set() + } + + getModuleById(id: string) { + return ( + this.server.moduleGraph.getModuleById(id) + || this.browser?.vite.moduleGraph.getModuleById(id) + ) + } + + getSourceMapModuleById(id: string): TransformResult['map'] | undefined { + const mod = this.server.moduleGraph.getModuleById(id) + return mod?.ssrTransformResult?.map || mod?.transformResult?.map + } + + get reporters() { + return this.ctx.reporters + } + + async globTestFiles(filters: string[] = []) { + const dir = this.config.dir || this.config.root + + const { include, exclude, includeSource } = this.config + const typecheck = this.config.typecheck + + const [testFiles, typecheckTestFiles] = await Promise.all([ + typecheck.enabled && typecheck.only + ? [] + : this.globAllTestFiles(include, exclude, includeSource, dir), + typecheck.enabled + ? (this.typecheckFilesList || this.globFiles(typecheck.include, typecheck.exclude, dir)) + : [], + ]) + + this.typecheckFilesList = typecheckTestFiles + + return { + testFiles: this.filterFiles( + testFiles, + filters, + dir, + ), + typecheckTestFiles: this.filterFiles( + typecheckTestFiles, + filters, + dir, + ), + } + } + + async globAllTestFiles( + include: string[], + exclude: string[], + includeSource: string[] | undefined, + cwd: string, + ) { + if (this.testFilesList) { + return this.testFilesList + } + + const testFiles = await this.globFiles(include, exclude, cwd) + + if (includeSource?.length) { + const files = await this.globFiles(includeSource, exclude, cwd) + + await Promise.all( + files.map(async (file) => { + try { + const code = await fs.readFile(file, 'utf-8') + if (this.isInSourceTestFile(code)) { + testFiles.push(file) + } + } + catch { + return null + } + }), + ) + } + + this.testFilesList = testFiles + + return testFiles + } + + isTestFile(id: string) { + return this.testFilesList && this.testFilesList.includes(id) + } + + isTypecheckFile(id: string) { + return this.typecheckFilesList && this.typecheckFilesList.includes(id) + } + + async globFiles(include: string[], exclude: string[], cwd: string) { + const globOptions: fg.Options = { + dot: true, + cwd, + ignore: exclude, + } + + const files = await fg(include, globOptions) + return files.map(file => resolve(cwd, file)) + } + + async isTargetFile(id: string, source?: string): Promise { + const relativeId = relative(this.config.dir || this.config.root, id) + if (mm.isMatch(relativeId, this.config.exclude)) { + return false + } + if (mm.isMatch(relativeId, this.config.include)) { + return true + } + if ( + this.config.includeSource?.length + && mm.isMatch(relativeId, this.config.includeSource) + ) { + source = source || (await fs.readFile(id, 'utf-8')) + return this.isInSourceTestFile(source) + } + return false + } + + isInSourceTestFile(code: string) { + return code.includes('import.meta.vitest') + } + + filterFiles(testFiles: string[], filters: string[], dir: string) { + if (filters.length && process.platform === 'win32') { + filters = filters.map(f => toNamespacedPath(f)) + } + + if (filters.length) { + return testFiles.filter((t) => { + const testFile = relative(dir, t).toLocaleLowerCase() + return filters.some((f) => { + // if filter is a full file path, we should include it if it's in the same folder + if (isAbsolute(f) && t.startsWith(f)) { + return true + } + + const relativePath = f.endsWith('/') + ? join(relative(dir, f), '/') + : relative(dir, f) + return ( + testFile.includes(f.toLocaleLowerCase()) + || testFile.includes(relativePath.toLocaleLowerCase()) + ) + }) + }) + } + + return testFiles + } + + async initBrowserServer(configFile: string | undefined) { + if (!this.isBrowserEnabled()) { + return + } + await this.ctx.packageInstaller.ensureInstalled('@vitest/browser', this.config.root, this.ctx.version) + const { createBrowserServer } = await import('@vitest/browser') + await this.browser?.close() + const browser = await createBrowserServer( + this, + configFile, + [...MocksPlugins()], + [CoverageTransform(this.ctx)], + ) + this.browser = browser + if (this.config.browser.ui) { + setup(this.ctx, browser.vite) + } + } + + static createBasicProject(ctx: Vitest) { + const project = new WorkspaceProject( + ctx.config.name || ctx.config.root, + ctx, + ) + project.vitenode = ctx.vitenode + project.server = ctx.server + project.runner = ctx.runner + project.config = ctx.config + for (const _providedKey in ctx.config.provide) { + const providedKey = _providedKey as keyof ProvidedContext + // type is very strict here, so we cast it to any + (project.provide as (key: string, value: unknown) => void)( + providedKey, + ctx.config.provide[providedKey], + ) + } + project.testProject = new TestProject(project) + return project + } + + static async createCoreProject(ctx: Vitest) { + const project = WorkspaceProject.createBasicProject(ctx) + await project.initBrowserServer(ctx.server.config.configFile) + return project + } + + async setServer(options: UserConfig, server: ViteDevServer) { + this.config = resolveConfig( + this.ctx.mode, + { + ...options, + coverage: this.ctx.config.coverage, + }, + server.config, + this.ctx.logger, + ) + for (const _providedKey in this.config.provide) { + const providedKey = _providedKey as keyof ProvidedContext + // type is very strict here, so we cast it to any + (this.provide as (key: string, value: unknown) => void)( + providedKey, + this.config.provide[providedKey], + ) + } + + this.testProject = new TestProject(this) + + this.server = server + + this.vitenode = new ViteNodeServer(server, this.config.server) + const node = this.vitenode + this.runner = new ViteNodeRunner({ + root: server.config.root, + base: server.config.base, + fetchModule(id: string) { + return node.fetchModule(id) + }, + resolveId(id: string, importer?: string) { + return node.resolveId(id, importer) + }, + }) + + await this.initBrowserServer(this.server.config.configFile) + } + + isBrowserEnabled(): boolean { + return isBrowserEnabled(this.config) + } + + 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 config + } + return deepMerge( + config, + this.ctx.configOverride, + ) + } + + close() { + if (!this.closingPromise) { + this.closingPromise = Promise.all( + [ + this.server.close(), + this.typechecker?.stop(), + this.browser?.close(), + this.clearTmpDir(), + ].filter(Boolean), + ).then(() => (this._provided = {} as any)) + } + return this.closingPromise + } + + private async clearTmpDir() { + try { + await rm(this.tmpDir, { recursive: true }) + } + catch {} + } + + async initBrowserProvider() { + if (!this.isBrowserEnabled()) { + return + } + await this.browser?.initBrowserProvider() + } +} diff --git a/packages/vitest/src/runtime/runBaseTests.ts b/packages/vitest/src/runtime/runBaseTests.ts index e8dcd4ee1009..df800f578e92 100644 --- a/packages/vitest/src/runtime/runBaseTests.ts +++ b/packages/vitest/src/runtime/runBaseTests.ts @@ -1,3 +1,4 @@ +import type { FileSpec } from '@vitest/runner/types/runner' import type { ResolvedTestEnvironment } from '../types/environment' import type { SerializedConfig } from './config' import type { VitestExecutor } from './execute' @@ -17,7 +18,7 @@ import { getWorkerState, resetModules } from './utils' // browser shouldn't call this! export async function run( method: 'run' | 'collect', - files: string[], + files: FileSpec[], config: SerializedConfig, environment: ResolvedTestEnvironment, executor: VitestExecutor, @@ -61,7 +62,7 @@ export async function run( resetModules(workerState.moduleCache, true) } - workerState.filepath = file + workerState.filepath = file.file if (method === 'run') { await startTests([file], runner) diff --git a/packages/vitest/src/runtime/runVmTests.ts b/packages/vitest/src/runtime/runVmTests.ts index f195ca90d716..8d0df083700e 100644 --- a/packages/vitest/src/runtime/runVmTests.ts +++ b/packages/vitest/src/runtime/runVmTests.ts @@ -1,3 +1,4 @@ +import type { FileSpec } from '@vitest/runner/types/runner' import type { SerializedConfig } from './config' import type { VitestExecutor } from './execute' import { createRequire } from 'node:module' @@ -21,7 +22,8 @@ import { getWorkerState } from './utils' export async function run( method: 'run' | 'collect', - files: string[], + // TODO consider type + files: FileSpec[], config: SerializedConfig, executor: VitestExecutor, ): Promise { @@ -85,7 +87,7 @@ export async function run( const { vi } = VitestIndex for (const file of files) { - workerState.filepath = file + workerState.filepath = file.filepath if (method === 'run') { await startTests([file], runner) diff --git a/packages/vitest/src/runtime/workers/base.ts b/packages/vitest/src/runtime/workers/base.ts index ecd7fa901236..a8824ef8cdd3 100644 --- a/packages/vitest/src/runtime/workers/base.ts +++ b/packages/vitest/src/runtime/workers/base.ts @@ -30,15 +30,23 @@ export async function runBaseTests(method: 'run' | 'collect', state: WorkerGloba moduleCache.delete(`mock:${fsPath}`) }) } - ctx.files.forEach(i => state.moduleCache.delete(i)) + ctx.files.forEach(i => state.moduleCache.delete( + typeof i === 'string' ? i : i.filepath, + )) const [executor, { run }] = await Promise.all([ startViteNode({ state, requestStubs: getDefaultRequestStubs() }), import('../runBaseTests'), ]) + const fileSpecs = ctx.files.map(f => + typeof f === 'string' + ? { filepath: f, testLocations: undefined } + : f, + ) + await run( method, - ctx.files, + fileSpecs, ctx.config, { environment: state.environment, options: ctx.environment.options }, executor, diff --git a/packages/vitest/src/runtime/workers/vm.ts b/packages/vitest/src/runtime/workers/vm.ts index 023701b019d6..dd3df091ef73 100644 --- a/packages/vitest/src/runtime/workers/vm.ts +++ b/packages/vitest/src/runtime/workers/vm.ts @@ -87,9 +87,19 @@ export async function runVmTests(method: 'run' | 'collect', state: WorkerGlobalS const { run } = (await executor.importExternalModule( entryFile, )) as typeof import('../runVmTests') + const fileSpecs = ctx.files.map(f => + typeof f === 'string' + ? { filepath: f, testLocations: undefined } + : f, + ) try { - await run(method, ctx.files, ctx.config, executor) + await run( + method, + fileSpecs, + ctx.config, + executor, + ) } finally { await vm.teardown?.() diff --git a/packages/vitest/src/typecheck/collect.ts b/packages/vitest/src/typecheck/collect.ts index c9139493a576..f6b798c50ce8 100644 --- a/packages/vitest/src/typecheck/collect.ts +++ b/packages/vitest/src/typecheck/collect.ts @@ -212,6 +212,8 @@ export async function collectTests( interpretTaskModes( file, ctx.config.testNamePattern, + // TODO: make sure you're not screwing stuff up + undefined, hasOnly, false, ctx.config.allowOnly, diff --git a/packages/vitest/src/types/worker.ts b/packages/vitest/src/types/worker.ts index 2c8b9a0fc0c5..0358e046a518 100644 --- a/packages/vitest/src/types/worker.ts +++ b/packages/vitest/src/types/worker.ts @@ -1,4 +1,5 @@ import type { CancelReason, Task } from '@vitest/runner' +import type { FileSpec } from '@vitest/runner/types/runner' import type { BirpcReturn } from 'birpc' import type { ModuleCacheMap, ViteNodeResolveId } from 'vite-node' import type { SerializedConfig } from '../runtime/config' @@ -26,7 +27,7 @@ export interface ContextRPC { workerId: number config: SerializedConfig projectName: string - files: string[] + files: string[] | FileSpec[] environment: ContextTestEnvironment providedContext: Record invalidates?: string[] diff --git a/packages/vitest/src/utils/test-helpers.ts b/packages/vitest/src/utils/test-helpers.ts index 2d9c44d994b1..b343d154eff6 100644 --- a/packages/vitest/src/utils/test-helpers.ts +++ b/packages/vitest/src/utils/test-helpers.ts @@ -31,9 +31,10 @@ export async function groupFilesByEnv( ) { const filesWithEnv = await Promise.all( files.map(async (spec) => { - const file = spec.moduleId + const filepath = spec.moduleId + const { testLocations } = spec const project = spec.project - const code = await fs.readFile(file, 'utf-8') + const code = await fs.readFile(filepath, 'utf-8') // 1. Check for control comments in the file let env = code.match(/@(?:vitest|jest)-environment\s+([\w-]+)\b/)?.[1] @@ -41,7 +42,7 @@ export async function groupFilesByEnv( if (!env) { for (const [glob, target] of project.config.environmentMatchGlobs || []) { - if (mm.isMatch(file, glob, { cwd: project.config.root })) { + if (mm.isMatch(filepath, glob, { cwd: project.config.root })) { env = target break } @@ -52,7 +53,7 @@ export async function groupFilesByEnv( const transformMode = getTransformMode( project.config.testTransformMode, - file, + filepath, ) let envOptionsJson = code.match(/@(?:vitest|jest)-environment-options\s+(.+)/)?.[1] @@ -71,7 +72,10 @@ export async function groupFilesByEnv( : null, } return { - file, + file: { + filepath, + testLocations, + }, project, environment, } diff --git a/test/cli/fixtures/location-filters/basic.test.ts b/test/cli/fixtures/location-filters/basic.test.ts new file mode 100644 index 000000000000..37f68c1bd622 --- /dev/null +++ b/test/cli/fixtures/location-filters/basic.test.ts @@ -0,0 +1,21 @@ +import { describe, expect, it } from 'vitest' + +describe('basic suite', () => { + describe('inner suite', () => { + it('some test', () => { + expect(1).toBe(1) + }) + + it('another test', () => { + expect(1).toBe(1) + }) + }) + + it('basic test', () => { + expect(1).toBe(1) + }) +}) + +it('outside test', () => { + expect(1).toBe(1) +}) diff --git a/test/cli/fixtures/location-filters/custom.config.ts b/test/cli/fixtures/location-filters/custom.config.ts new file mode 100644 index 000000000000..841db817e7b8 --- /dev/null +++ b/test/cli/fixtures/location-filters/custom.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + include: ['basic.test.ts', 'math.test.ts'], + name: 'custom', + includeTaskLocation: true, + }, +}) diff --git a/test/cli/fixtures/location-filters/describe-error.test.ts b/test/cli/fixtures/location-filters/describe-error.test.ts new file mode 100644 index 000000000000..3238cceabdb6 --- /dev/null +++ b/test/cli/fixtures/location-filters/describe-error.test.ts @@ -0,0 +1,9 @@ +import { describe, expect, it } from 'vitest'; + +describe('describe error', () => { + throw new Error('describe error') + + it('wont run', () => { + expect(true).toBe(true) + }) +}) \ No newline at end of file diff --git a/test/cli/fixtures/location-filters/fail.config.ts b/test/cli/fixtures/location-filters/fail.config.ts new file mode 100644 index 000000000000..16d254a9aaab --- /dev/null +++ b/test/cli/fixtures/location-filters/fail.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + include: ['top-level-error.test.ts', 'describe-error.test.ts'], + }, +}) diff --git a/test/cli/fixtures/location-filters/math.test.ts b/test/cli/fixtures/location-filters/math.test.ts new file mode 100644 index 000000000000..c39601812ac5 --- /dev/null +++ b/test/cli/fixtures/location-filters/math.test.ts @@ -0,0 +1,9 @@ +import { expect, it } from 'vitest' + +it('1 plus 1', () => { + expect(1 + 1).toBe(2) +}) + +it('failing test', () => { + expect(1 + 1).toBe(3) +}) diff --git a/test/cli/fixtures/location-filters/no-task-location.config.ts b/test/cli/fixtures/location-filters/no-task-location.config.ts new file mode 100644 index 000000000000..6ef28d4e2315 --- /dev/null +++ b/test/cli/fixtures/location-filters/no-task-location.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + include: ['basic.test.ts', 'math.test.ts'], + name: 'no task location', + includeTaskLocation: false, + }, +}) + diff --git a/test/cli/fixtures/location-filters/top-level-error.test.ts b/test/cli/fixtures/location-filters/top-level-error.test.ts new file mode 100644 index 000000000000..59922e3fd360 --- /dev/null +++ b/test/cli/fixtures/location-filters/top-level-error.test.ts @@ -0,0 +1 @@ +throw new Error('top level error') diff --git a/test/cli/fixtures/location-filters/vitest.config.ts b/test/cli/fixtures/location-filters/vitest.config.ts new file mode 100644 index 000000000000..10b4610e41b7 --- /dev/null +++ b/test/cli/fixtures/location-filters/vitest.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + include: ['basic.test.ts', 'math.test.ts'], + browser: { + name: 'chromium', + provider: 'playwright', + headless: true, + api: 7523, + }, + includeTaskLocation: true, + }, +}) diff --git a/test/cli/test/location-filters.test.ts b/test/cli/test/location-filters.test.ts new file mode 100644 index 000000000000..d6bd333ab199 --- /dev/null +++ b/test/cli/test/location-filters.test.ts @@ -0,0 +1,154 @@ +import { describe, expect, test } from 'vitest' +import { runVitestCli } from '../../test-utils' + +describe('location filter with list command', () => { + test('finds test at correct line number', async () => { + const { stdout, stderr } = await runVitestCli( + 'list', + '-r=./fixtures/location-filters', + '--config=custom.config.ts', + 'basic.test.ts:5', + ) + + expect(stdout).toMatchInlineSnapshot(` + "[custom] basic.test.ts > basic suite > inner suite > some test + " + `) + expect(stderr).toEqual('') + }) + + test('reports not found test', async () => { + const { stdout, stderr } = await runVitestCli( + 'list', + '-r=./fixtures/location-filters', + '--config=custom.config.ts', + 'basic.test.ts:99', + ) + + expect(stdout).toEqual('') + expect(stderr).toMatchInlineSnapshot(` + "Error: No test found in basic.test.ts in line 99 + " + `) + }) + + test('reports multiple not found tests', async () => { + const { stdout, stderr } = await runVitestCli( + 'list', + '-r=./fixtures/location-filters', + '--config=custom.config.ts', + 'basic.test.ts:5', + 'basic.test.ts:12', + 'basic.test.ts:99', + ) + + expect(stdout).toEqual('') + expect(stderr).toMatchInlineSnapshot(` + "Error: No test found in basic.test.ts in lines 12, 99 + " + `) + }) + + test('errors if range location is provided', async () => { + const { stdout, stderr } = await runVitestCli( + 'list', + '-r=./fixtures/location-filters', + 'a/file/that/doesnt/exit:10-15', + ) + + expect(stdout).toEqual('') + expect(stderr).toContain('Collect Error') + expect(stderr).toContain('RangeLocationFilterProvidedError') + }) + + test('erorrs if includeTaskLocation is not enabled', async () => { + const { stdout, stderr } = await runVitestCli( + 'list', + '-r=./fixtures/location-filters', + '--config=no-task-location.config.ts', + 'a/file/that/doesnt/exist:5', + ) + + expect(stdout).toEqual('') + expect(stderr).toContain('Collect Error') + expect(stderr).toContain('IncludeTaskLocationDisabledError') + }) +}) + +describe('location filter with run command', () => { + test('finds test at correct line number', async () => { + const { stdout, stderr } = await runVitestCli( + 'run', + '-r=./fixtures/location-filters', + '--config=vitest.config.ts', + 'basic.test.ts:6', + // 'basic.test.ts:15', + // 'math.test.ts:3', + ) + + expect(`${stdout} ${stderr}`).toEqual('') + + expect(stdout).toMatchInlineSnapshot(` + "[custom] basic.test.ts > basic suite > inner suite > some test + " + `) + expect(stderr).toEqual('') + }) + + // test('reports not found test', async () => { + // const { stdout, stderr } = await runVitestCli( + // 'list', + // '-r=./fixtures/location-filters', + // '--config=custom.config.ts', + // 'basic.test.ts:99', + // ) + // + // expect(stdout).toEqual('') + // expect(stderr).toMatchInlineSnapshot(` + // "Error: No test found in basic.test.ts in line 99 + // " + // `) + // }) + // + // test('reports multiple not found tests', async () => { + // const { stdout, stderr } = await runVitestCli( + // 'list', + // '-r=./fixtures/location-filters', + // '--config=custom.config.ts', + // 'basic.test.ts:5', + // 'basic.test.ts:12', + // 'basic.test.ts:99', + // ) + // + // expect(stdout).toEqual('') + // expect(stderr).toMatchInlineSnapshot(` + // "Error: No test found in basic.test.ts in lines 12, 99 + // " + // `) + // }) + // + // test('errors if range location is provided', async () => { + // const { stdout, stderr } = await runVitestCli( + // 'list', + // '-r=./fixtures/location-filters', + // 'a/file/that/doesnt/exit:10-15', + // ) + // + // expect(stdout).toEqual('') + // expect(stderr).toContain('Collect Error') + // expect(stderr).toContain('RangeLocationFilterProvidedError') + // }) + // + // test('erorrs if includeTaskLocation is not enabled', async () => { + // const { stdout, stderr } = await runVitestCli( + // 'list', + // '-r=./fixtures/location-filters', + // '--config=no-task-location.config.ts', + // 'a/file/that/doesnt/exist:5', + // ) + // + // expect(stdout).toEqual('') + // expect(stderr).toContain('Collect Error') + // expect(stderr).toContain('IncludeTaskLocationDisabledError') + // }) +}) diff --git a/test/config/tsconfig.vitest-temp.json b/test/config/tsconfig.vitest-temp.json new file mode 100644 index 000000000000..083b46b4c817 --- /dev/null +++ b/test/config/tsconfig.vitest-temp.json @@ -0,0 +1,114 @@ +{ + "compilerOptions": { + "target": "ESNext", + "lib": [ + "DOM", + "ESNext" + ], + "module": "ESNext", + "moduleResolution": "Bundler", + "paths": { + "@vitest/ws-client": [ + "./packages/ws-client/src/index.ts" + ], + "@vitest/ui": [ + "./packages/ui/node/index.ts" + ], + "@vitest/pretty-format": [ + "./packages/pretty-format/src/index.ts" + ], + "@vitest/utils": [ + "./packages/utils/src/index.ts" + ], + "@vitest/utils/*": [ + "./packages/utils/src/*" + ], + "@vitest/spy": [ + "./packages/spy/src/index.ts" + ], + "@vitest/snapshot": [ + "./packages/snapshot/src/index.ts" + ], + "@vitest/snapshot/*": [ + "./packages/snapshot/src/*" + ], + "@vitest/expect": [ + "./packages/expect/src/index.ts" + ], + "@vitest/mocker": [ + "./packages/mocker/src/index.ts" + ], + "@vitest/mocker/node": [ + "./packages/mocker/src/node/index.ts" + ], + "@vitest/mocker/browser": [ + "./packages/mocker/src/browser/index.ts" + ], + "@vitest/runner": [ + "./packages/runner/src/index.ts" + ], + "@vitest/runner/*": [ + "./packages/runner/src/*" + ], + "@vitest/browser": [ + "./packages/browser/src/node/index.ts" + ], + "@vitest/browser/client": [ + "./packages/browser/src/client/client.ts" + ], + "~/*": [ + "./packages/ui/client/*" + ], + "vitest": [ + "./packages/vitest/src/public/index.ts" + ], + "vitest/globals": [ + "./packages/vitest/globals.d.ts" + ], + "vitest/node": [ + "./packages/vitest/src/public/node.ts" + ], + "vitest/execute": [ + "./packages/vitest/src/public/execute.ts" + ], + "vitest/config": [ + "./packages/vitest/src/public/config.ts" + ], + "vitest/coverage": [ + "./packages/vitest/src/public/coverage.ts" + ], + "vitest/browser": [ + "./packages/vitest/src/public/browser.ts" + ], + "vitest/runners": [ + "./packages/vitest/src/public/runners.ts" + ], + "vite-node": [ + "./packages/vite-node/src/index.ts" + ], + "vite-node/client": [ + "./packages/vite-node/src/client.ts" + ], + "vite-node/server": [ + "./packages/vite-node/src/server.ts" + ], + "vite-node/utils": [ + "./packages/vite-node/src/utils.ts" + ] + }, + "strict": true, + "declaration": true, + "noEmit": true, + "esModuleInterop": true, + "skipLibCheck": true, + "types": [ + "vite/client" + ], + "emitDeclarationOnly": false, + "incremental": true, + "tsBuildInfoFile": "/home/mzh/Repositories/vitest/packages/vitest/dist/chunks/tsconfig.tmp.tsbuildinfo" + }, + "exclude": [ + "**/dist/**" + ] +} \ No newline at end of file From 82ecbaa85bb5c597c31a4f2c0f5f1520eb353e99 Mon Sep 17 00:00:00 2001 From: mzhubail Date: Wed, 23 Oct 2024 16:46:39 +0300 Subject: [PATCH 02/28] Start with the test cases - Changed resolve code in `globTestSpecs`. Rationale is to better match what's happening in `filterFiles` of `node/workspace.ts`. - Attempted to test in case `includeTaskLocation` is disabled. But it seems to me that the code for throwing the error is not reached when the test case is executed. idk what to do about this. - Providing ranges is not tested yet. Code needs to be changed. --- packages/vitest/src/node/cli/cli-api.ts | 2 +- packages/vitest/src/node/core.ts | 5 +- .../fixtures/list/no-task-location.config.ts | 10 +++ test/cli/test/list.test.ts | 87 +++++++++++++++++++ 4 files changed, 101 insertions(+), 3 deletions(-) create mode 100644 test/cli/fixtures/list/no-task-location.config.ts diff --git a/packages/vitest/src/node/cli/cli-api.ts b/packages/vitest/src/node/cli/cli-api.ts index 9727124913f8..04b8a8a79ab5 100644 --- a/packages/vitest/src/node/cli/cli-api.ts +++ b/packages/vitest/src/node/cli/cli-api.ts @@ -302,7 +302,7 @@ export function parseFilter(f: string) { } } else if (lineNumber.includes('-')) { - throw new Error('Range line numbers are not allowed') + throw new Error(`Range line numbers are not allowed: ${f}`) } else { return { filename: f } diff --git a/packages/vitest/src/node/core.ts b/packages/vitest/src/node/core.ts index 3e7413cc9bcb..c9bf990323c5 100644 --- a/packages/vitest/src/node/core.ts +++ b/packages/vitest/src/node/core.ts @@ -1153,9 +1153,10 @@ export class Vitest { public async globTestSpecs(filters: string[] = []) { const files: TestSpecification[] = [] + const dir = this.config.dir || this.config.root const parsedFilters = filters.map(f => parseFilter(f)) const testLocations = groupFilters(parsedFilters.map( - f => ({ ...f, filename: resolve(f.filename) }), + f => ({ ...f, filename: resolve(dir, f.filename) }), )) // Key is file and val sepcifies whether we have matched this file with testLocation @@ -1186,7 +1187,7 @@ export class Vitest { Object.entries(testLocations).forEach(([filepath, loc]) => { if (loc.length !== 0 && !testLocHasMatch[filepath]) { - const rel = relative(this.config.dir || this.config.root, filepath) + const rel = relative(dir, filepath) this.logger.printError(new Error(`Couldn\'t find file "${rel}".\n` + 'Note when specifying the test location you have to specify the full test filename.', diff --git a/test/cli/fixtures/list/no-task-location.config.ts b/test/cli/fixtures/list/no-task-location.config.ts new file mode 100644 index 000000000000..6ef28d4e2315 --- /dev/null +++ b/test/cli/fixtures/list/no-task-location.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + include: ['basic.test.ts', 'math.test.ts'], + name: 'no task location', + includeTaskLocation: false, + }, +}) + diff --git a/test/cli/test/list.test.ts b/test/cli/test/list.test.ts index ad3c8a03267b..8559f356908b 100644 --- a/test/cli/test/list.test.ts +++ b/test/cli/test/list.test.ts @@ -231,6 +231,93 @@ test('ignores watch flag', async () => { `) }) +test('file not found mentions strict matching for location filters', async () => { + const { stdout, stderr } = await runVitestCli( + 'list', + '-r=./fixtures/list', + 'a/file/that/doesnt/exit:10', + ) + + expect(stderr).toMatchSnapshot() + expect(stdout).toEqual('') +}) + +test('location filter finds test at correct line number', async () => { + const { stdout, stderr } = await runVitestCli( + 'list', + '-r=./fixtures/list', + '--config=custom.config.ts', + 'basic.test.ts:5', + ) + + expect(stdout).toMatchInlineSnapshot(` + "[custom] basic.test.ts > basic suite > inner suite > some test + " + `) + expect(stderr).toEqual('') +}) + +test('location filter reports not found test', async () => { + const { stdout, stderr } = await runVitestCli( + 'list', + '-r=./fixtures/list', + '--config=custom.config.ts', + 'basic.test.ts:99', + ) + + expect(stdout).toEqual('') + expect(stderr).toMatchInlineSnapshot(` + "Error: No test found in basic.test.ts in line 99 + " + `) +}) + +test('location filter reports multiple not found tests', async () => { + const { stdout, stderr } = await runVitestCli( + 'list', + '-r=./fixtures/list', + '--config=custom.config.ts', + 'basic.test.ts:5', + 'basic.test.ts:12', + 'basic.test.ts:99', + ) + + expect(stdout).toEqual('') + expect(stderr).toMatchInlineSnapshot(` + "Error: No test found in basic.test.ts in lines 12, 99 + " + `) +}) + +// Will do after feedback +// test('throw error when range number is provided', async () => { +// const { stdout, stderr } = await runVitestCli( +// 'list', +// '-r=./fixtures/list', +// 'a/file/that/doesnt/exit:10-15' +// ) +// +// // Just to see output +// expect(stdout + '\n' + stderr).toEqual('') +// +// // expect(stderr).toMatchSnapshot() +// // expect(stdout).toEqual('') +// }) +// + +// DOESN'T WORK +// test('erorr if location filter provided without enabling includeTaskLocation', async () => { +// const { stdout, stderr } = await runVitestCli( +// 'list', +// '-r=./fixtures/list', +// '--config=no-task-location.config.ts', +// 'basic.test.ts:5' +// ) +// +// // Just to see output +// expect(stdout + '\n' + stderr).toEqual('') +// }) + function relative(stdout: string) { return stdout.replace(new RegExp(slash(process.cwd()), 'gi'), '') } From f66d5335f7deb014b041bdae17b35e1d5688d611 Mon Sep 17 00:00:00 2001 From: mzhubail Date: Wed, 30 Oct 2024 12:06:16 +0300 Subject: [PATCH 03/28] Wrap up error handling work - Error handling: Added a try-catch for the to catch the new errors, and with that I moved `collect` code of `cac.ts` to its own function in `cli-api.ts`. The reason for this is to have error handling in list command too - Move includeTaskLocation checking where it belongs - Test cases working for now - Minor code readability changes in `parseFilter` --- packages/vitest/src/node/cli/cac.ts | 23 +------ packages/vitest/src/node/cli/cli-api.ts | 60 ++++++++++++++++--- packages/vitest/src/node/core.ts | 15 ++--- packages/vitest/src/node/errors.ts | 8 +++ test/cli/test/__snapshots__/list.test.ts.snap | 16 +++++ test/cli/test/list.test.ts | 55 ++++++++--------- 6 files changed, 114 insertions(+), 63 deletions(-) diff --git a/packages/vitest/src/node/cli/cac.ts b/packages/vitest/src/node/cli/cac.ts index d1c8e78b103d..94ff798382f8 100644 --- a/packages/vitest/src/node/cli/cac.ts +++ b/packages/vitest/src/node/cli/cac.ts @@ -1,5 +1,5 @@ import type { VitestRunMode } from '../types/config' -import type { CliOptions } from './cli-api' +import { collectAndProcess, type CliOptions } from './cli-api' import type { CLIOption, CLIOptions as CLIOptionsConfig } from './cli-config' import { toArray } from '@vitest/utils' import cac, { type CAC, type Command } from 'cac' @@ -7,6 +7,7 @@ import { normalize } from 'pathe' import c from 'tinyrainbow' import { version } from '../../../package.json' with { type: 'json' } import { benchCliOptionsConfig, cliOptionsConfig, collectCliOptionsConfig } from './cli-config' +import { IncludeTaskLocationDisabledError, RangeLocationFilterProvidedError } from '../errors' function addCommand(cli: CAC | Command, name: string, option: CLIOption) { const commandName = option.alias || name @@ -306,25 +307,7 @@ async function collect(mode: VitestRunMode, cliFilters: string[], options: CliOp watch: false, run: true, }) - if (!options.filesOnly) { - const { tests, errors } = await ctx.collect(cliFilters.map(normalize)) - - if (errors.length) { - console.error('\nThere were unhandled errors during test collection') - errors.forEach(e => console.error(e)) - console.error('\n\n') - await ctx.close() - return - } - - processCollected(ctx, tests, options) - } - else { - const files = await ctx.listFiles(cliFilters.map(normalize)) - outputFileList(files, options) - } - - await ctx.close() + collectAndProcess(ctx, options, cliFilters) } catch (e) { const { divider } = await import('../reporters/renderers/utils') diff --git a/packages/vitest/src/node/cli/cli-api.ts b/packages/vitest/src/node/cli/cli-api.ts index 04b8a8a79ab5..acb75147200b 100644 --- a/packages/vitest/src/node/cli/cli-api.ts +++ b/packages/vitest/src/node/cli/cli-api.ts @@ -10,8 +10,9 @@ import { dirname, relative, resolve } from 'pathe' import { CoverageProviderMap } from '../../integrations/coverage' import { groupBy } from '../../utils/base' import { createVitest } from '../create' -import { FilesNotFoundError, GitNotFoundError, IncludeTaskLocationDisabledError } from '../errors' +import { FilesNotFoundError, GitNotFoundError, IncludeTaskLocationDisabledError, RangeLocationFilterProvidedError } from '../errors' import { registerConsoleShortcuts } from '../stdin' +import { normalize } from 'node:path' export interface CliOptions extends UserConfig { /** @@ -104,7 +105,10 @@ export async function startVitest( return ctx } - if (e instanceof IncludeTaskLocationDisabledError) { + if ( + e instanceof IncludeTaskLocationDisabledError + || e instanceof RangeLocationFilterProvidedError + ) { ctx.logger.printError(e, { verbose: false }) return ctx } @@ -165,6 +169,44 @@ export async function prepareVitest( return ctx } +export async function collectAndProcess( + ctx: Vitest, + options: CliOptions, + cliFilters: string[], +) { + try { + if (!options.filesOnly) { + const { tests, errors } = await ctx.collect(cliFilters.map(normalize)) + + if (errors.length) { + console.error('\nThere were unhandled errors during test collection') + errors.forEach(e => console.error(e)) + console.error('\n\n') + await ctx.close() + return + } + + processCollected(ctx, tests, options) + } + else { + const files = await ctx.listFiles(cliFilters.map(normalize)) + outputFileList(files, options) + } + + await ctx.close() + } catch (e) { + if ( + e instanceof IncludeTaskLocationDisabledError + || e instanceof RangeLocationFilterProvidedError + ) { + ctx.logger.printError(e, { verbose: false }) + return + } + + await ctx.close() + } +} + export function processCollected(ctx: Vitest, files: File[], options: CliOptions) { let errorsPrinted = false @@ -284,15 +326,15 @@ export function formatCollectedAsString(files: File[]) { }).flat() } -export function parseFilter(f: string) { - const colonIndex = f.indexOf(':') +export function parseFilter(filter: string): Filter { + const colonIndex = filter.indexOf(':') if (colonIndex === -1) { - return { filename: f } + return { filename: filter } } const [parsedFilename, lineNumber] = [ - f.substring(0, colonIndex), - f.substring(colonIndex + 1), + filter.substring(0, colonIndex), + filter.substring(colonIndex + 1), ] if (lineNumber.match(/^\d+$/)) { @@ -302,10 +344,10 @@ export function parseFilter(f: string) { } } else if (lineNumber.includes('-')) { - throw new Error(`Range line numbers are not allowed: ${f}`) + throw new RangeLocationFilterProvidedError(filter) } else { - return { filename: f } + return { filename: filter } } } diff --git a/packages/vitest/src/node/core.ts b/packages/vitest/src/node/core.ts index c9bf990323c5..b77587feedf1 100644 --- a/packages/vitest/src/node/core.ts +++ b/packages/vitest/src/node/core.ts @@ -420,13 +420,6 @@ export class Vitest { await this.globTestFiles(filters), ) - if ( - !this.config.includeTaskLocation - && files.some(spec => spec.testLocations && spec.testLocations.length !== 0) - ) { - throw new IncludeTaskLocationDisabledError() - } - // if run with --changed, don't exit if no tests are found if (!files.length) { // Report coverage for uncovered files @@ -1159,6 +1152,14 @@ export class Vitest { f => ({ ...f, filename: resolve(dir, f.filename) }), )) + // Require includeTaskLocation when a location filter is passed + if ( + !this.config.includeTaskLocation + && parsedFilters.some(f => f.lineNumber !== undefined) + ) { + throw new IncludeTaskLocationDisabledError() + } + // Key is file and val sepcifies whether we have matched this file with testLocation const testLocHasMatch: { [f: string]: boolean } = {} diff --git a/packages/vitest/src/node/errors.ts b/packages/vitest/src/node/errors.ts index dd1841a19f4f..e16ecf7e6747 100644 --- a/packages/vitest/src/node/errors.ts +++ b/packages/vitest/src/node/errors.ts @@ -21,3 +21,11 @@ export class IncludeTaskLocationDisabledError extends Error { super('Recieved line number filters while `includeTaskLocation` option is disabled') } } + +export class RangeLocationFilterProvidedError extends Error { + code = 'VITEST_RANGE_LOCATION_FILTER_PROVIDED' + + constructor(filter: string) { + super(`Location filters are not allowed: ${filter}`) + } +} diff --git a/test/cli/test/__snapshots__/list.test.ts.snap b/test/cli/test/__snapshots__/list.test.ts.snap index efc6f0b467ad..41a49e2c3612 100644 --- a/test/cli/test/__snapshots__/list.test.ts.snap +++ b/test/cli/test/__snapshots__/list.test.ts.snap @@ -59,6 +59,22 @@ math.test.ts > failing test " `; +exports[`file not found mentions strict matching for location filters 1`] = ` +"Error: Couldn't find file "a/file/that/doesnt/exit". +Note when specifying the test location you have to specify the full test filename. +" +`; + +exports[`ignore filter when range location is provided 1`] = ` +"basic.test.ts > basic suite > inner suite > some test +basic.test.ts > basic suite > inner suite > another test +basic.test.ts > basic suite > basic test +basic.test.ts > outside test +math.test.ts > 1 plus 1 +math.test.ts > failing test +" +`; + exports[`json output shows error 1`] = ` "Error: top level error ❯ top-level-error.test.ts:1:7 diff --git a/test/cli/test/list.test.ts b/test/cli/test/list.test.ts index 8559f356908b..fc2ee36fcda8 100644 --- a/test/cli/test/list.test.ts +++ b/test/cli/test/list.test.ts @@ -235,6 +235,7 @@ test('file not found mentions strict matching for location filters', async () => const { stdout, stderr } = await runVitestCli( 'list', '-r=./fixtures/list', + '--config=custom.config.ts', 'a/file/that/doesnt/exit:10', ) @@ -289,34 +290,34 @@ test('location filter reports multiple not found tests', async () => { `) }) -// Will do after feedback -// test('throw error when range number is provided', async () => { -// const { stdout, stderr } = await runVitestCli( -// 'list', -// '-r=./fixtures/list', -// 'a/file/that/doesnt/exit:10-15' -// ) -// -// // Just to see output -// expect(stdout + '\n' + stderr).toEqual('') -// -// // expect(stderr).toMatchSnapshot() -// // expect(stdout).toEqual('') -// }) -// +test('error if range location is provided', async () => { + const { stdout, stderr } = await runVitestCli( + 'list', + '-r=./fixtures/list', + 'a/file/that/doesnt/exit:10-15' + ) + + expect(stdout).toEqual('') + expect(stderr).toMatchInlineSnapshot(` + "Error: Location filters are not allowed: a/file/that/doesnt/exit:10-15 + " + `) +}) -// DOESN'T WORK -// test('erorr if location filter provided without enabling includeTaskLocation', async () => { -// const { stdout, stderr } = await runVitestCli( -// 'list', -// '-r=./fixtures/list', -// '--config=no-task-location.config.ts', -// 'basic.test.ts:5' -// ) -// -// // Just to see output -// expect(stdout + '\n' + stderr).toEqual('') -// }) +test('erorr if location filter provided without enabling includeTaskLocation', async () => { + const { stdout, stderr } = await runVitestCli( + 'list', + '-r=./fixtures/list', + '--config=no-task-location.config.ts', + 'a/file/that/doesnt/exist:5' + ) + + expect(stdout).toEqual('') + expect(stderr).toMatchInlineSnapshot(` + "Error: Recieved line number filters while \`includeTaskLocation\` option is disabled + " + `) +}) function relative(stdout: string) { return stdout.replace(new RegExp(slash(process.cwd()), 'gi'), '') From 41ca27102d2a3a2234a2c02c5fd5144d4b055e3b Mon Sep 17 00:00:00 2001 From: mzhubail Date: Wed, 30 Oct 2024 14:03:08 +0300 Subject: [PATCH 04/28] lint --- packages/vitest/src/node/cli/cac.ts | 5 ++--- packages/vitest/src/node/cli/cli-api.ts | 5 +++-- test/cli/test/list.test.ts | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/vitest/src/node/cli/cac.ts b/packages/vitest/src/node/cli/cac.ts index 94ff798382f8..8d3c3175c444 100644 --- a/packages/vitest/src/node/cli/cac.ts +++ b/packages/vitest/src/node/cli/cac.ts @@ -1,13 +1,12 @@ import type { VitestRunMode } from '../types/config' -import { collectAndProcess, type CliOptions } from './cli-api' import type { CLIOption, CLIOptions as CLIOptionsConfig } from './cli-config' import { toArray } from '@vitest/utils' import cac, { type CAC, type Command } from 'cac' import { normalize } from 'pathe' import c from 'tinyrainbow' import { version } from '../../../package.json' with { type: 'json' } +import { type CliOptions, collectAndProcess } from './cli-api' import { benchCliOptionsConfig, cliOptionsConfig, collectCliOptionsConfig } from './cli-config' -import { IncludeTaskLocationDisabledError, RangeLocationFilterProvidedError } from '../errors' function addCommand(cli: CAC | Command, name: string, option: CLIOption) { const commandName = option.alias || name @@ -301,7 +300,7 @@ async function collect(mode: VitestRunMode, cliFilters: string[], options: CliOp catch {} try { - const { prepareVitest, processCollected, outputFileList } = await import('./cli-api') + const { prepareVitest } = await import('./cli-api') const ctx = await prepareVitest(mode, { ...normalizeCliOptions(options), watch: false, diff --git a/packages/vitest/src/node/cli/cli-api.ts b/packages/vitest/src/node/cli/cli-api.ts index acb75147200b..fa0ad972a76b 100644 --- a/packages/vitest/src/node/cli/cli-api.ts +++ b/packages/vitest/src/node/cli/cli-api.ts @@ -5,6 +5,7 @@ import type { Vitest, VitestOptions } from '../core' import type { WorkspaceSpec } from '../pool' import type { UserConfig, VitestEnvironment, VitestRunMode } from '../types/config' import { mkdirSync, writeFileSync } from 'node:fs' +import { normalize } from 'node:path' import { getNames, getTests } from '@vitest/runner/utils' import { dirname, relative, resolve } from 'pathe' import { CoverageProviderMap } from '../../integrations/coverage' @@ -12,7 +13,6 @@ import { groupBy } from '../../utils/base' import { createVitest } from '../create' import { FilesNotFoundError, GitNotFoundError, IncludeTaskLocationDisabledError, RangeLocationFilterProvidedError } from '../errors' import { registerConsoleShortcuts } from '../stdin' -import { normalize } from 'node:path' export interface CliOptions extends UserConfig { /** @@ -194,7 +194,8 @@ export async function collectAndProcess( } await ctx.close() - } catch (e) { + } + catch (e) { if ( e instanceof IncludeTaskLocationDisabledError || e instanceof RangeLocationFilterProvidedError diff --git a/test/cli/test/list.test.ts b/test/cli/test/list.test.ts index fc2ee36fcda8..a8058af6f317 100644 --- a/test/cli/test/list.test.ts +++ b/test/cli/test/list.test.ts @@ -294,7 +294,7 @@ test('error if range location is provided', async () => { const { stdout, stderr } = await runVitestCli( 'list', '-r=./fixtures/list', - 'a/file/that/doesnt/exit:10-15' + 'a/file/that/doesnt/exit:10-15', ) expect(stdout).toEqual('') @@ -309,7 +309,7 @@ test('erorr if location filter provided without enabling includeTaskLocation', a 'list', '-r=./fixtures/list', '--config=no-task-location.config.ts', - 'a/file/that/doesnt/exist:5' + 'a/file/that/doesnt/exist:5', ) expect(stdout).toEqual('') From 6a3343d0b59d709c6a3d9da86dae136ec6a606d4 Mon Sep 17 00:00:00 2001 From: mzhubail Date: Wed, 30 Oct 2024 14:57:38 +0300 Subject: [PATCH 05/28] Fixes --- packages/runner/src/run.ts | 4 ++-- packages/vitest/src/node/pools/forks.ts | 1 + packages/vitest/src/node/pools/vmThreads.ts | 1 + packages/vitest/src/node/spec.ts | 3 +-- packages/vitest/src/runtime/inspector.ts | 4 +++- packages/vitest/src/runtime/runBaseTests.ts | 2 +- 6 files changed, 9 insertions(+), 6 deletions(-) diff --git a/packages/runner/src/run.ts b/packages/runner/src/run.ts index 78721efc65af..4ef1498d94c2 100644 --- a/packages/runner/src/run.ts +++ b/packages/runner/src/run.ts @@ -498,8 +498,8 @@ export async function runFiles(files: File[], runner: VitestRunner): Promise { - const paths = specs.map(f => f.filepath) +export async function startTests(specs: string[] | FileSpec[], runner: VitestRunner): Promise { + const paths = specs.map(f => typeof f === 'string' ? f : f.filepath) await runner.onBeforeCollect?.(paths) const files = await collectTests(specs, runner) diff --git a/packages/vitest/src/node/pools/forks.ts b/packages/vitest/src/node/pools/forks.ts index 0b61039e2655..34c68b4c41a5 100644 --- a/packages/vitest/src/node/pools/forks.ts +++ b/packages/vitest/src/node/pools/forks.ts @@ -1,3 +1,4 @@ +import type { FileSpec } from '@vitest/runner/types/runner' import type { TinypoolChannel, Options as TinypoolOptions } from 'tinypool' import type { RunnerRPC, RuntimeRPC } from '../../types/rpc' import type { ContextRPC, ContextTestEnvironment } from '../../types/worker' diff --git a/packages/vitest/src/node/pools/vmThreads.ts b/packages/vitest/src/node/pools/vmThreads.ts index 68e2620e82d0..3192f6aed127 100644 --- a/packages/vitest/src/node/pools/vmThreads.ts +++ b/packages/vitest/src/node/pools/vmThreads.ts @@ -1,3 +1,4 @@ +import type { FileSpec } from '@vitest/runner/types/runner' import type { Options as TinypoolOptions } from 'tinypool' import type { RunnerRPC, RuntimeRPC } from '../../types/rpc' import type { ContextTestEnvironment } from '../../types/worker' diff --git a/packages/vitest/src/node/spec.ts b/packages/vitest/src/node/spec.ts index a076c2be0253..4f78cb6125ef 100644 --- a/packages/vitest/src/node/spec.ts +++ b/packages/vitest/src/node/spec.ts @@ -26,8 +26,7 @@ export class TestSpecification { project: TestProject, moduleId: string, pool: Pool, - // location?: WorkspaceSpecLocation | undefined, - testLocations: number[] | undefined, + testLocations?: number[] | undefined, ) { this[0] = project this[1] = moduleId diff --git a/packages/vitest/src/runtime/inspector.ts b/packages/vitest/src/runtime/inspector.ts index a6ecea20a387..84e605f279e0 100644 --- a/packages/vitest/src/runtime/inspector.ts +++ b/packages/vitest/src/runtime/inspector.ts @@ -28,7 +28,9 @@ export function setupInspect(ctx: ContextRPC) { ) if (config.inspectBrk) { - const firstTestFile = ctx.files[0] + const firstTestFile = typeof ctx.files[0] === 'string' + ? ctx.files[0] + : ctx.files[0].filepath // Stop at first test file if (firstTestFile) { diff --git a/packages/vitest/src/runtime/runBaseTests.ts b/packages/vitest/src/runtime/runBaseTests.ts index df800f578e92..aef7b4e7d965 100644 --- a/packages/vitest/src/runtime/runBaseTests.ts +++ b/packages/vitest/src/runtime/runBaseTests.ts @@ -62,7 +62,7 @@ export async function run( resetModules(workerState.moduleCache, true) } - workerState.filepath = file.file + workerState.filepath = file.filepath if (method === 'run') { await startTests([file], runner) From f26364115de808b88866291661fa0736c359863e Mon Sep 17 00:00:00 2001 From: mzhubail Date: Sun, 10 Nov 2024 13:54:30 +0300 Subject: [PATCH 06/28] Walk back some error handling changes --- packages/vitest/src/node/cli/cac.ts | 22 ++++++++++++-- packages/vitest/src/node/cli/cli-api.ts | 39 ------------------------- test/cli/test/list.test.ts | 12 +++----- 3 files changed, 24 insertions(+), 49 deletions(-) diff --git a/packages/vitest/src/node/cli/cac.ts b/packages/vitest/src/node/cli/cac.ts index 8d3c3175c444..ea8de23717f0 100644 --- a/packages/vitest/src/node/cli/cac.ts +++ b/packages/vitest/src/node/cli/cac.ts @@ -5,7 +5,7 @@ import cac, { type CAC, type Command } from 'cac' import { normalize } from 'pathe' import c from 'tinyrainbow' import { version } from '../../../package.json' with { type: 'json' } -import { type CliOptions, collectAndProcess } from './cli-api' +import { type CliOptions, outputFileList, processCollected } from './cli-api' import { benchCliOptionsConfig, cliOptionsConfig, collectCliOptionsConfig } from './cli-config' function addCommand(cli: CAC | Command, name: string, option: CLIOption) { @@ -306,7 +306,25 @@ async function collect(mode: VitestRunMode, cliFilters: string[], options: CliOp watch: false, run: true, }) - collectAndProcess(ctx, options, cliFilters) + if (!options.filesOnly) { + const { tests, errors } = await ctx.collect(cliFilters.map(normalize)) + + if (errors.length) { + console.error('\nThere were unhandled errors during test collection') + errors.forEach(e => console.error(e)) + console.error('\n\n') + await ctx.close() + return + } + + processCollected(ctx, tests, options) + } + else { + const files = await ctx.listFiles(cliFilters.map(normalize)) + outputFileList(files, options) + } + + await ctx.close() } catch (e) { const { divider } = await import('../reporters/renderers/utils') diff --git a/packages/vitest/src/node/cli/cli-api.ts b/packages/vitest/src/node/cli/cli-api.ts index fa0ad972a76b..0b41c5a4c4bc 100644 --- a/packages/vitest/src/node/cli/cli-api.ts +++ b/packages/vitest/src/node/cli/cli-api.ts @@ -169,45 +169,6 @@ export async function prepareVitest( return ctx } -export async function collectAndProcess( - ctx: Vitest, - options: CliOptions, - cliFilters: string[], -) { - try { - if (!options.filesOnly) { - const { tests, errors } = await ctx.collect(cliFilters.map(normalize)) - - if (errors.length) { - console.error('\nThere were unhandled errors during test collection') - errors.forEach(e => console.error(e)) - console.error('\n\n') - await ctx.close() - return - } - - processCollected(ctx, tests, options) - } - else { - const files = await ctx.listFiles(cliFilters.map(normalize)) - outputFileList(files, options) - } - - await ctx.close() - } - catch (e) { - if ( - e instanceof IncludeTaskLocationDisabledError - || e instanceof RangeLocationFilterProvidedError - ) { - ctx.logger.printError(e, { verbose: false }) - return - } - - await ctx.close() - } -} - export function processCollected(ctx: Vitest, files: File[], options: CliOptions) { let errorsPrinted = false diff --git a/test/cli/test/list.test.ts b/test/cli/test/list.test.ts index a8058af6f317..5b699f86535a 100644 --- a/test/cli/test/list.test.ts +++ b/test/cli/test/list.test.ts @@ -298,10 +298,8 @@ test('error if range location is provided', async () => { ) expect(stdout).toEqual('') - expect(stderr).toMatchInlineSnapshot(` - "Error: Location filters are not allowed: a/file/that/doesnt/exit:10-15 - " - `) + expect(stderr).toContain('Collect Error') + expect(stderr).toContain('RangeLocationFilterProvidedError') }) test('erorr if location filter provided without enabling includeTaskLocation', async () => { @@ -313,10 +311,8 @@ test('erorr if location filter provided without enabling includeTaskLocation', a ) expect(stdout).toEqual('') - expect(stderr).toMatchInlineSnapshot(` - "Error: Recieved line number filters while \`includeTaskLocation\` option is disabled - " - `) + expect(stderr).toContain('Collect Error') + expect(stderr).toContain('IncludeTaskLocationDisabledError') }) function relative(stdout: string) { From 8c0e1f7e07007755cbbd38d92fc88ec6dfedcca0 Mon Sep 17 00:00:00 2001 From: mzhubail Date: Sun, 10 Nov 2024 15:21:22 +0300 Subject: [PATCH 07/28] Fix nasty recursion bug --- packages/runner/src/utils/collect.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/runner/src/utils/collect.ts b/packages/runner/src/utils/collect.ts index 520654092e0a..d9f595c0e4b8 100644 --- a/packages/runner/src/utils/collect.ts +++ b/packages/runner/src/utils/collect.ts @@ -15,7 +15,7 @@ export function interpretTaskModes( ): void { const matchedLocations: number[] = [] - const traverseSuite = (suite: Suite) => { + const traverseSuite = (suite: Suite, parentIsOnly?: boolean) => { const suiteIsOnly = parentIsOnly || suite.mode === 'only' suite.tasks.forEach((t) => { @@ -60,8 +60,7 @@ export function interpretTaskModes( skipAllTasks(t) } else { - traverseSuite(t) - // interpretTaskModes(t, namePattern, locationFilters, onlyMode, includeTask, allowOnly) + traverseSuite(t, includeTask) } } }) @@ -74,7 +73,7 @@ export function interpretTaskModes( } } - traverseSuite(file) + traverseSuite(file, parentIsOnly) const nonMatching = testLocations?.filter(loc => !matchedLocations.includes(loc)) if (nonMatching && nonMatching.length !== 0) { From e126427a1179888e8775efd1f37ee809a98e30e6 Mon Sep 17 00:00:00 2001 From: mzhubail Date: Sun, 10 Nov 2024 16:03:42 +0300 Subject: [PATCH 08/28] Fixes - Expand on error description - Add private marker - Remove TODO - Remove obsolete snapshot - Add `filter.ts` - Other changes --- packages/vitest/src/node/cli/cac.ts | 4 +- packages/vitest/src/node/cli/cli-api.ts | 49 ------------------- packages/vitest/src/node/cli/filter.ts | 49 +++++++++++++++++++ packages/vitest/src/node/core.ts | 2 +- packages/vitest/src/node/errors.ts | 3 +- packages/vitest/src/node/spec.ts | 1 + packages/vitest/src/runtime/runVmTests.ts | 1 - packages/vitest/src/typecheck/collect.ts | 1 - test/cli/test/__snapshots__/list.test.ts.snap | 10 ---- 9 files changed, 55 insertions(+), 65 deletions(-) create mode 100644 packages/vitest/src/node/cli/filter.ts diff --git a/packages/vitest/src/node/cli/cac.ts b/packages/vitest/src/node/cli/cac.ts index ea8de23717f0..d1c8e78b103d 100644 --- a/packages/vitest/src/node/cli/cac.ts +++ b/packages/vitest/src/node/cli/cac.ts @@ -1,11 +1,11 @@ import type { VitestRunMode } from '../types/config' +import type { CliOptions } from './cli-api' import type { CLIOption, CLIOptions as CLIOptionsConfig } from './cli-config' import { toArray } from '@vitest/utils' import cac, { type CAC, type Command } from 'cac' import { normalize } from 'pathe' import c from 'tinyrainbow' import { version } from '../../../package.json' with { type: 'json' } -import { type CliOptions, outputFileList, processCollected } from './cli-api' import { benchCliOptionsConfig, cliOptionsConfig, collectCliOptionsConfig } from './cli-config' function addCommand(cli: CAC | Command, name: string, option: CLIOption) { @@ -300,7 +300,7 @@ async function collect(mode: VitestRunMode, cliFilters: string[], options: CliOp catch {} try { - const { prepareVitest } = await import('./cli-api') + const { prepareVitest, processCollected, outputFileList } = await import('./cli-api') const ctx = await prepareVitest(mode, { ...normalizeCliOptions(options), watch: false, diff --git a/packages/vitest/src/node/cli/cli-api.ts b/packages/vitest/src/node/cli/cli-api.ts index 0b41c5a4c4bc..5fa78c8ed5a0 100644 --- a/packages/vitest/src/node/cli/cli-api.ts +++ b/packages/vitest/src/node/cli/cli-api.ts @@ -5,11 +5,9 @@ import type { Vitest, VitestOptions } from '../core' import type { WorkspaceSpec } from '../pool' import type { UserConfig, VitestEnvironment, VitestRunMode } from '../types/config' import { mkdirSync, writeFileSync } from 'node:fs' -import { normalize } from 'node:path' import { getNames, getTests } from '@vitest/runner/utils' import { dirname, relative, resolve } from 'pathe' import { CoverageProviderMap } from '../../integrations/coverage' -import { groupBy } from '../../utils/base' import { createVitest } from '../create' import { FilesNotFoundError, GitNotFoundError, IncludeTaskLocationDisabledError, RangeLocationFilterProvidedError } from '../errors' import { registerConsoleShortcuts } from '../stdin' @@ -288,53 +286,6 @@ export function formatCollectedAsString(files: File[]) { }).flat() } -export function parseFilter(filter: string): Filter { - const colonIndex = filter.indexOf(':') - if (colonIndex === -1) { - return { filename: filter } - } - - const [parsedFilename, lineNumber] = [ - filter.substring(0, colonIndex), - filter.substring(colonIndex + 1), - ] - - if (lineNumber.match(/^\d+$/)) { - return { - filename: parsedFilename, - lineNumber: Number.parseInt(lineNumber), - } - } - else if (lineNumber.includes('-')) { - throw new RangeLocationFilterProvidedError(filter) - } - else { - return { filename: filter } - } -} - -interface Filter { - filename: string - lineNumber?: undefined | number -} - -export function groupFilters(filters: Filter[]) { - const groupedFilters_ = groupBy(filters, f => f.filename) - const groupedFilters = Object.fromEntries(Object.entries(groupedFilters_) - .map((entry) => { - const [filename, filters] = entry - const testLocations = filters.map(f => f.lineNumber) - - return [ - filename, - testLocations.filter(l => l !== undefined) as number[], - ] - }), - ) - - return groupedFilters -} - const envPackageNames: Record< Exclude, string diff --git a/packages/vitest/src/node/cli/filter.ts b/packages/vitest/src/node/cli/filter.ts new file mode 100644 index 000000000000..82dbbc2addd3 --- /dev/null +++ b/packages/vitest/src/node/cli/filter.ts @@ -0,0 +1,49 @@ +import { groupBy } from '../../utils/base' +import { RangeLocationFilterProvidedError } from '../errors' + +export function parseFilter(filter: string): Filter { + const colonIndex = filter.indexOf(':') + if (colonIndex === -1) { + return { filename: filter } + } + + const [parsedFilename, lineNumber] = [ + filter.substring(0, colonIndex), + filter.substring(colonIndex + 1), + ] + + if (lineNumber.match(/^\d+$/)) { + return { + filename: parsedFilename, + lineNumber: Number.parseInt(lineNumber), + } + } + else if (lineNumber.includes('-')) { + throw new RangeLocationFilterProvidedError(filter) + } + else { + return { filename: filter } + } +} + +interface Filter { + filename: string + lineNumber?: undefined | number +} + +export function groupFilters(filters: Filter[]) { + const groupedFilters_ = groupBy(filters, f => f.filename) + const groupedFilters = Object.fromEntries(Object.entries(groupedFilters_) + .map((entry) => { + const [filename, filters] = entry + const testLocations = filters.map(f => f.lineNumber) + + return [ + filename, + testLocations.filter(l => l !== undefined) as number[], + ] + }), + ) + + return groupedFilters +} diff --git a/packages/vitest/src/node/core.ts b/packages/vitest/src/node/core.ts index b77587feedf1..d37f5a39c81b 100644 --- a/packages/vitest/src/node/core.ts +++ b/packages/vitest/src/node/core.ts @@ -26,7 +26,7 @@ import { getCoverageProvider } from '../integrations/coverage' import { distDir } from '../paths' import { wildcardPatternToRegExp } from '../utils/base' import { VitestCache } from './cache' -import { groupFilters, parseFilter } from './cli/cli-api' +import { groupFilters, parseFilter } from './cli/filter' import { resolveConfig } from './config/resolveConfig' import { FilesNotFoundError, GitNotFoundError, IncludeTaskLocationDisabledError } from './errors' import { Logger } from './logger' diff --git a/packages/vitest/src/node/errors.ts b/packages/vitest/src/node/errors.ts index e16ecf7e6747..ea7681c7ad0d 100644 --- a/packages/vitest/src/node/errors.ts +++ b/packages/vitest/src/node/errors.ts @@ -26,6 +26,7 @@ export class RangeLocationFilterProvidedError extends Error { code = 'VITEST_RANGE_LOCATION_FILTER_PROVIDED' constructor(filter: string) { - super(`Location filters are not allowed: ${filter}`) + super(`Found "-" in location filter ${filter}. Note that range location filters ` + + `are not supported. Consider specifying the exact line numbers of your tests.`) } } diff --git a/packages/vitest/src/node/spec.ts b/packages/vitest/src/node/spec.ts index 4f78cb6125ef..c89fdba2e5bc 100644 --- a/packages/vitest/src/node/spec.ts +++ b/packages/vitest/src/node/spec.ts @@ -20,6 +20,7 @@ export class TestSpecification { public readonly moduleId: string public readonly pool: Pool public readonly testLocations: number[] | undefined + /** @private */ // public readonly location: WorkspaceSpecLocation | undefined constructor( diff --git a/packages/vitest/src/runtime/runVmTests.ts b/packages/vitest/src/runtime/runVmTests.ts index 8d0df083700e..47178911bc0e 100644 --- a/packages/vitest/src/runtime/runVmTests.ts +++ b/packages/vitest/src/runtime/runVmTests.ts @@ -22,7 +22,6 @@ import { getWorkerState } from './utils' export async function run( method: 'run' | 'collect', - // TODO consider type files: FileSpec[], config: SerializedConfig, executor: VitestExecutor, diff --git a/packages/vitest/src/typecheck/collect.ts b/packages/vitest/src/typecheck/collect.ts index f6b798c50ce8..4b1ae34ddfa1 100644 --- a/packages/vitest/src/typecheck/collect.ts +++ b/packages/vitest/src/typecheck/collect.ts @@ -212,7 +212,6 @@ export async function collectTests( interpretTaskModes( file, ctx.config.testNamePattern, - // TODO: make sure you're not screwing stuff up undefined, hasOnly, false, diff --git a/test/cli/test/__snapshots__/list.test.ts.snap b/test/cli/test/__snapshots__/list.test.ts.snap index 41a49e2c3612..d62c03773997 100644 --- a/test/cli/test/__snapshots__/list.test.ts.snap +++ b/test/cli/test/__snapshots__/list.test.ts.snap @@ -65,16 +65,6 @@ Note when specifying the test location you have to specify the full test filenam " `; -exports[`ignore filter when range location is provided 1`] = ` -"basic.test.ts > basic suite > inner suite > some test -basic.test.ts > basic suite > inner suite > another test -basic.test.ts > basic suite > basic test -basic.test.ts > outside test -math.test.ts > 1 plus 1 -math.test.ts > failing test -" -`; - exports[`json output shows error 1`] = ` "Error: top level error ❯ top-level-error.test.ts:1:7 From 2319d984c106f119a9e6ec9cfdf0d985e5fcd638 Mon Sep 17 00:00:00 2001 From: mzhubail Date: Sun, 10 Nov 2024 16:20:11 +0300 Subject: [PATCH 09/28] something --- packages/vitest/src/node/spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vitest/src/node/spec.ts b/packages/vitest/src/node/spec.ts index c89fdba2e5bc..0088c445c52c 100644 --- a/packages/vitest/src/node/spec.ts +++ b/packages/vitest/src/node/spec.ts @@ -19,8 +19,8 @@ export class TestSpecification { public readonly project: TestProject public readonly moduleId: string public readonly pool: Pool - public readonly testLocations: number[] | undefined /** @private */ + public readonly testLocations: number[] | undefined // public readonly location: WorkspaceSpecLocation | undefined constructor( From 3d71d0dbd2c683d69b11bb72f87e1e970955e809 Mon Sep 17 00:00:00 2001 From: mzhubail Date: Wed, 20 Nov 2024 14:03:23 +0300 Subject: [PATCH 10/28] Add type export --- packages/runner/src/types.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/runner/src/types.ts b/packages/runner/src/types.ts index 48a72003b7b6..6cbc309aad63 100644 --- a/packages/runner/src/types.ts +++ b/packages/runner/src/types.ts @@ -1,5 +1,6 @@ export type { CancelReason, + FileSpec, VitestRunner, VitestRunnerConfig, VitestRunnerConstructor, From fa0b568502530c0c610503be28401bd7c54c6359 Mon Sep 17 00:00:00 2001 From: mzhubail Date: Thu, 21 Nov 2024 08:17:48 +0300 Subject: [PATCH 11/28] Comment out test case --- test/cli/test/location-filters.test.ts | 154 ++++++++++++------------- 1 file changed, 77 insertions(+), 77 deletions(-) diff --git a/test/cli/test/location-filters.test.ts b/test/cli/test/location-filters.test.ts index d6bd333ab199..3a07f159f3a0 100644 --- a/test/cli/test/location-filters.test.ts +++ b/test/cli/test/location-filters.test.ts @@ -75,80 +75,80 @@ describe('location filter with list command', () => { }) }) -describe('location filter with run command', () => { - test('finds test at correct line number', async () => { - const { stdout, stderr } = await runVitestCli( - 'run', - '-r=./fixtures/location-filters', - '--config=vitest.config.ts', - 'basic.test.ts:6', - // 'basic.test.ts:15', - // 'math.test.ts:3', - ) - - expect(`${stdout} ${stderr}`).toEqual('') - - expect(stdout).toMatchInlineSnapshot(` - "[custom] basic.test.ts > basic suite > inner suite > some test - " - `) - expect(stderr).toEqual('') - }) - - // test('reports not found test', async () => { - // const { stdout, stderr } = await runVitestCli( - // 'list', - // '-r=./fixtures/location-filters', - // '--config=custom.config.ts', - // 'basic.test.ts:99', - // ) - // - // expect(stdout).toEqual('') - // expect(stderr).toMatchInlineSnapshot(` - // "Error: No test found in basic.test.ts in line 99 - // " - // `) - // }) - // - // test('reports multiple not found tests', async () => { - // const { stdout, stderr } = await runVitestCli( - // 'list', - // '-r=./fixtures/location-filters', - // '--config=custom.config.ts', - // 'basic.test.ts:5', - // 'basic.test.ts:12', - // 'basic.test.ts:99', - // ) - // - // expect(stdout).toEqual('') - // expect(stderr).toMatchInlineSnapshot(` - // "Error: No test found in basic.test.ts in lines 12, 99 - // " - // `) - // }) - // - // test('errors if range location is provided', async () => { - // const { stdout, stderr } = await runVitestCli( - // 'list', - // '-r=./fixtures/location-filters', - // 'a/file/that/doesnt/exit:10-15', - // ) - // - // expect(stdout).toEqual('') - // expect(stderr).toContain('Collect Error') - // expect(stderr).toContain('RangeLocationFilterProvidedError') - // }) - // - // test('erorrs if includeTaskLocation is not enabled', async () => { - // const { stdout, stderr } = await runVitestCli( - // 'list', - // '-r=./fixtures/location-filters', - // '--config=no-task-location.config.ts', - // 'a/file/that/doesnt/exist:5', - // ) - // - // expect(stdout).toEqual('') - // expect(stderr).toContain('Collect Error') - // expect(stderr).toContain('IncludeTaskLocationDisabledError') - // }) -}) +// describe('location filter with run command', () => { +// test('finds test at correct line number', async () => { +// const { stdout, stderr } = await runVitestCli( +// 'run', +// '-r=./fixtures/location-filters', +// '--config=vitest.config.ts', +// 'basic.test.ts:6', +// // 'basic.test.ts:15', +// // 'math.test.ts:3', +// ) +// +// expect(`${stdout} ${stderr}`).toEqual('') +// +// expect(stdout).toMatchInlineSnapshot(` +// "[custom] basic.test.ts > basic suite > inner suite > some test +// " +// `) +// expect(stderr).toEqual('') +// }) +// +// // test('reports not found test', async () => { +// // const { stdout, stderr } = await runVitestCli( +// // 'list', +// // '-r=./fixtures/location-filters', +// // '--config=custom.config.ts', +// // 'basic.test.ts:99', +// // ) +// // +// // expect(stdout).toEqual('') +// // expect(stderr).toMatchInlineSnapshot(` +// // "Error: No test found in basic.test.ts in line 99 +// // " +// // `) +// // }) +// // +// // test('reports multiple not found tests', async () => { +// // const { stdout, stderr } = await runVitestCli( +// // 'list', +// // '-r=./fixtures/location-filters', +// // '--config=custom.config.ts', +// // 'basic.test.ts:5', +// // 'basic.test.ts:12', +// // 'basic.test.ts:99', +// // ) +// // +// // expect(stdout).toEqual('') +// // expect(stderr).toMatchInlineSnapshot(` +// // "Error: No test found in basic.test.ts in lines 12, 99 +// // " +// // `) +// // }) +// // +// // test('errors if range location is provided', async () => { +// // const { stdout, stderr } = await runVitestCli( +// // 'list', +// // '-r=./fixtures/location-filters', +// // 'a/file/that/doesnt/exit:10-15', +// // ) +// // +// // expect(stdout).toEqual('') +// // expect(stderr).toContain('Collect Error') +// // expect(stderr).toContain('RangeLocationFilterProvidedError') +// // }) +// // +// // test('erorrs if includeTaskLocation is not enabled', async () => { +// // const { stdout, stderr } = await runVitestCli( +// // 'list', +// // '-r=./fixtures/location-filters', +// // '--config=no-task-location.config.ts', +// // 'a/file/that/doesnt/exist:5', +// // ) +// // +// // expect(stdout).toEqual('') +// // expect(stderr).toContain('Collect Error') +// // expect(stderr).toContain('IncludeTaskLocationDisabledError') +// // }) +// }) From 2981c1ba4bf7cd3e8834c104a9f332c93318c96d Mon Sep 17 00:00:00 2001 From: mzhubail Date: Thu, 21 Nov 2024 08:21:42 +0300 Subject: [PATCH 12/28] Remove temp file I don't know how this got over here. --- test/config/tsconfig.vitest-temp.json | 114 -------------------------- 1 file changed, 114 deletions(-) delete mode 100644 test/config/tsconfig.vitest-temp.json diff --git a/test/config/tsconfig.vitest-temp.json b/test/config/tsconfig.vitest-temp.json deleted file mode 100644 index 083b46b4c817..000000000000 --- a/test/config/tsconfig.vitest-temp.json +++ /dev/null @@ -1,114 +0,0 @@ -{ - "compilerOptions": { - "target": "ESNext", - "lib": [ - "DOM", - "ESNext" - ], - "module": "ESNext", - "moduleResolution": "Bundler", - "paths": { - "@vitest/ws-client": [ - "./packages/ws-client/src/index.ts" - ], - "@vitest/ui": [ - "./packages/ui/node/index.ts" - ], - "@vitest/pretty-format": [ - "./packages/pretty-format/src/index.ts" - ], - "@vitest/utils": [ - "./packages/utils/src/index.ts" - ], - "@vitest/utils/*": [ - "./packages/utils/src/*" - ], - "@vitest/spy": [ - "./packages/spy/src/index.ts" - ], - "@vitest/snapshot": [ - "./packages/snapshot/src/index.ts" - ], - "@vitest/snapshot/*": [ - "./packages/snapshot/src/*" - ], - "@vitest/expect": [ - "./packages/expect/src/index.ts" - ], - "@vitest/mocker": [ - "./packages/mocker/src/index.ts" - ], - "@vitest/mocker/node": [ - "./packages/mocker/src/node/index.ts" - ], - "@vitest/mocker/browser": [ - "./packages/mocker/src/browser/index.ts" - ], - "@vitest/runner": [ - "./packages/runner/src/index.ts" - ], - "@vitest/runner/*": [ - "./packages/runner/src/*" - ], - "@vitest/browser": [ - "./packages/browser/src/node/index.ts" - ], - "@vitest/browser/client": [ - "./packages/browser/src/client/client.ts" - ], - "~/*": [ - "./packages/ui/client/*" - ], - "vitest": [ - "./packages/vitest/src/public/index.ts" - ], - "vitest/globals": [ - "./packages/vitest/globals.d.ts" - ], - "vitest/node": [ - "./packages/vitest/src/public/node.ts" - ], - "vitest/execute": [ - "./packages/vitest/src/public/execute.ts" - ], - "vitest/config": [ - "./packages/vitest/src/public/config.ts" - ], - "vitest/coverage": [ - "./packages/vitest/src/public/coverage.ts" - ], - "vitest/browser": [ - "./packages/vitest/src/public/browser.ts" - ], - "vitest/runners": [ - "./packages/vitest/src/public/runners.ts" - ], - "vite-node": [ - "./packages/vite-node/src/index.ts" - ], - "vite-node/client": [ - "./packages/vite-node/src/client.ts" - ], - "vite-node/server": [ - "./packages/vite-node/src/server.ts" - ], - "vite-node/utils": [ - "./packages/vite-node/src/utils.ts" - ] - }, - "strict": true, - "declaration": true, - "noEmit": true, - "esModuleInterop": true, - "skipLibCheck": true, - "types": [ - "vite/client" - ], - "emitDeclarationOnly": false, - "incremental": true, - "tsBuildInfoFile": "/home/mzh/Repositories/vitest/packages/vitest/dist/chunks/tsconfig.tmp.tsbuildinfo" - }, - "exclude": [ - "**/dist/**" - ] -} \ No newline at end of file From 65eda1918dc66689f42efccf2bb2cbd0019ecd63 Mon Sep 17 00:00:00 2001 From: mzhubail Date: Thu, 21 Nov 2024 08:53:17 +0300 Subject: [PATCH 13/28] Remove workspace.ts - rebase issues --- packages/vitest/src/node/workspace.ts | 494 -------------------------- 1 file changed, 494 deletions(-) delete mode 100644 packages/vitest/src/node/workspace.ts diff --git a/packages/vitest/src/node/workspace.ts b/packages/vitest/src/node/workspace.ts deleted file mode 100644 index 7867b513e599..000000000000 --- a/packages/vitest/src/node/workspace.ts +++ /dev/null @@ -1,494 +0,0 @@ -import type { - TransformResult, - ViteDevServer, - InlineConfig as ViteInlineConfig, -} from 'vite' -import type { Typechecker } from '../typecheck/typechecker' -import type { ProvidedContext } from '../types/general' -import type { Vitest } from './core' -import type { GlobalSetupFile } from './globalSetup' -import type { WorkspaceSpec as DeprecatedWorkspaceSpec } from './pool' -import type { BrowserServer } from './types/browser' -import type { - ResolvedConfig, - SerializedConfig, - UserConfig, - UserWorkspaceConfig, -} from './types/config' -import { promises as fs } from 'node:fs' -import { rm } from 'node:fs/promises' -import { tmpdir } from 'node:os' -import { deepMerge, nanoid } from '@vitest/utils' -import fg from 'fast-glob' -import mm from 'micromatch' -import { - dirname, - isAbsolute, - join, - relative, - resolve, - toNamespacedPath, -} from 'pathe' -import { ViteNodeRunner } from 'vite-node/client' -import { ViteNodeServer } from 'vite-node/server' -import { setup } from '../api/setup' -import { isBrowserEnabled, resolveConfig } from './config/resolveConfig' -import { serializeConfig } from './config/serializeConfig' -import { loadGlobalSetupFiles } from './globalSetup' -import { CoverageTransform } from './plugins/coverageTransform' -import { MocksPlugins } from './plugins/mocks' -import { WorkspaceVitestPlugin } from './plugins/workspace' -import { TestProject } from './reported-workspace-project' -import { TestSpecification } from './spec' -import { createViteServer } from './vite' - -interface InitializeProjectOptions extends UserWorkspaceConfig { - workspaceConfigPath: string - extends?: string -} - -export async function initializeProject( - workspacePath: string | number, - ctx: Vitest, - options: InitializeProjectOptions, -) { - const project = new WorkspaceProject(workspacePath, ctx, options) - - const root - = options.root - || (typeof workspacePath === 'number' - ? undefined - : workspacePath.endsWith('/') - ? workspacePath - : dirname(workspacePath)) - - const configFile = options.extends - ? resolve(dirname(options.workspaceConfigPath), options.extends) - : typeof workspacePath === 'number' || workspacePath.endsWith('/') - ? false - : workspacePath - - const config: ViteInlineConfig = { - ...options, - root, - configFile, - // this will make "mode": "test" | "benchmark" inside defineConfig - mode: options.test?.mode || options.mode || ctx.config.mode, - plugins: [ - ...(options.plugins || []), - WorkspaceVitestPlugin(project, { ...options, root, workspacePath }), - ], - } - - await createViteServer(config) - - return project -} - -export class WorkspaceProject { - configOverride: Partial | undefined - - config!: ResolvedConfig - server!: ViteDevServer - vitenode!: ViteNodeServer - runner!: ViteNodeRunner - browser?: BrowserServer - typechecker?: Typechecker - - closingPromise: Promise | undefined - - testFilesList: string[] | null = null - typecheckFilesList: string[] | null = null - - public testProject!: TestProject - - public readonly id = nanoid() - public readonly tmpDir = join(tmpdir(), this.id) - - private _globalSetups: GlobalSetupFile[] | undefined - private _provided: ProvidedContext = {} as any - - constructor( - public path: string | number, - public ctx: Vitest, - public options?: InitializeProjectOptions, - ) {} - - getName(): string { - return this.config.name || '' - } - - isCore() { - return this.ctx.getCoreWorkspaceProject() === this - } - - provide( - key: T, - value: ProvidedContext[T], - ) { - try { - structuredClone(value) - } - catch (err) { - throw new Error( - `Cannot provide "${key}" because it's not serializable.`, - { - cause: err, - }, - ) - } - (this._provided as any)[key] = value - } - - getProvidedContext(): ProvidedContext { - if (this.isCore()) { - return this._provided - } - // globalSetup can run even if core workspace is not part of the test run - // so we need to inherit its provided context - return { - ...this.ctx.getCoreWorkspaceProject().getProvidedContext(), - ...this._provided, - } - } - - public createSpec( - moduleId: string, - pool: string, - testLocations?: number[] | undefined, - ): DeprecatedWorkspaceSpec { - return new TestSpecification(this, moduleId, pool, testLocations) as DeprecatedWorkspaceSpec - } - - async initializeGlobalSetup() { - if (this._globalSetups) { - return - } - - this._globalSetups = await loadGlobalSetupFiles( - this.runner, - this.config.globalSetup, - ) - - for (const globalSetupFile of this._globalSetups) { - const teardown = await globalSetupFile.setup?.({ - provide: (key, value) => this.provide(key, value), - config: this.config, - }) - if (teardown == null || !!globalSetupFile.teardown) { - continue - } - if (typeof teardown !== 'function') { - throw new TypeError( - `invalid return value in globalSetup file ${globalSetupFile.file}. Must return a function`, - ) - } - globalSetupFile.teardown = teardown - } - } - - async teardownGlobalSetup() { - if (!this._globalSetups) { - return - } - for (const globalSetupFile of [...this._globalSetups].reverse()) { - await globalSetupFile.teardown?.() - } - } - - get logger() { - return this.ctx.logger - } - - // it's possible that file path was imported with different queries (?raw, ?url, etc) - getModulesByFilepath(file: string) { - const set - = this.server.moduleGraph.getModulesByFile(file) - || this.browser?.vite.moduleGraph.getModulesByFile(file) - return set || new Set() - } - - getModuleById(id: string) { - return ( - this.server.moduleGraph.getModuleById(id) - || this.browser?.vite.moduleGraph.getModuleById(id) - ) - } - - getSourceMapModuleById(id: string): TransformResult['map'] | undefined { - const mod = this.server.moduleGraph.getModuleById(id) - return mod?.ssrTransformResult?.map || mod?.transformResult?.map - } - - get reporters() { - return this.ctx.reporters - } - - async globTestFiles(filters: string[] = []) { - const dir = this.config.dir || this.config.root - - const { include, exclude, includeSource } = this.config - const typecheck = this.config.typecheck - - const [testFiles, typecheckTestFiles] = await Promise.all([ - typecheck.enabled && typecheck.only - ? [] - : this.globAllTestFiles(include, exclude, includeSource, dir), - typecheck.enabled - ? (this.typecheckFilesList || this.globFiles(typecheck.include, typecheck.exclude, dir)) - : [], - ]) - - this.typecheckFilesList = typecheckTestFiles - - return { - testFiles: this.filterFiles( - testFiles, - filters, - dir, - ), - typecheckTestFiles: this.filterFiles( - typecheckTestFiles, - filters, - dir, - ), - } - } - - async globAllTestFiles( - include: string[], - exclude: string[], - includeSource: string[] | undefined, - cwd: string, - ) { - if (this.testFilesList) { - return this.testFilesList - } - - const testFiles = await this.globFiles(include, exclude, cwd) - - if (includeSource?.length) { - const files = await this.globFiles(includeSource, exclude, cwd) - - await Promise.all( - files.map(async (file) => { - try { - const code = await fs.readFile(file, 'utf-8') - if (this.isInSourceTestFile(code)) { - testFiles.push(file) - } - } - catch { - return null - } - }), - ) - } - - this.testFilesList = testFiles - - return testFiles - } - - isTestFile(id: string) { - return this.testFilesList && this.testFilesList.includes(id) - } - - isTypecheckFile(id: string) { - return this.typecheckFilesList && this.typecheckFilesList.includes(id) - } - - async globFiles(include: string[], exclude: string[], cwd: string) { - const globOptions: fg.Options = { - dot: true, - cwd, - ignore: exclude, - } - - const files = await fg(include, globOptions) - return files.map(file => resolve(cwd, file)) - } - - async isTargetFile(id: string, source?: string): Promise { - const relativeId = relative(this.config.dir || this.config.root, id) - if (mm.isMatch(relativeId, this.config.exclude)) { - return false - } - if (mm.isMatch(relativeId, this.config.include)) { - return true - } - if ( - this.config.includeSource?.length - && mm.isMatch(relativeId, this.config.includeSource) - ) { - source = source || (await fs.readFile(id, 'utf-8')) - return this.isInSourceTestFile(source) - } - return false - } - - isInSourceTestFile(code: string) { - return code.includes('import.meta.vitest') - } - - filterFiles(testFiles: string[], filters: string[], dir: string) { - if (filters.length && process.platform === 'win32') { - filters = filters.map(f => toNamespacedPath(f)) - } - - if (filters.length) { - return testFiles.filter((t) => { - const testFile = relative(dir, t).toLocaleLowerCase() - return filters.some((f) => { - // if filter is a full file path, we should include it if it's in the same folder - if (isAbsolute(f) && t.startsWith(f)) { - return true - } - - const relativePath = f.endsWith('/') - ? join(relative(dir, f), '/') - : relative(dir, f) - return ( - testFile.includes(f.toLocaleLowerCase()) - || testFile.includes(relativePath.toLocaleLowerCase()) - ) - }) - }) - } - - return testFiles - } - - async initBrowserServer(configFile: string | undefined) { - if (!this.isBrowserEnabled()) { - return - } - await this.ctx.packageInstaller.ensureInstalled('@vitest/browser', this.config.root, this.ctx.version) - const { createBrowserServer } = await import('@vitest/browser') - await this.browser?.close() - const browser = await createBrowserServer( - this, - configFile, - [...MocksPlugins()], - [CoverageTransform(this.ctx)], - ) - this.browser = browser - if (this.config.browser.ui) { - setup(this.ctx, browser.vite) - } - } - - static createBasicProject(ctx: Vitest) { - const project = new WorkspaceProject( - ctx.config.name || ctx.config.root, - ctx, - ) - project.vitenode = ctx.vitenode - project.server = ctx.server - project.runner = ctx.runner - project.config = ctx.config - for (const _providedKey in ctx.config.provide) { - const providedKey = _providedKey as keyof ProvidedContext - // type is very strict here, so we cast it to any - (project.provide as (key: string, value: unknown) => void)( - providedKey, - ctx.config.provide[providedKey], - ) - } - project.testProject = new TestProject(project) - return project - } - - static async createCoreProject(ctx: Vitest) { - const project = WorkspaceProject.createBasicProject(ctx) - await project.initBrowserServer(ctx.server.config.configFile) - return project - } - - async setServer(options: UserConfig, server: ViteDevServer) { - this.config = resolveConfig( - this.ctx.mode, - { - ...options, - coverage: this.ctx.config.coverage, - }, - server.config, - this.ctx.logger, - ) - for (const _providedKey in this.config.provide) { - const providedKey = _providedKey as keyof ProvidedContext - // type is very strict here, so we cast it to any - (this.provide as (key: string, value: unknown) => void)( - providedKey, - this.config.provide[providedKey], - ) - } - - this.testProject = new TestProject(this) - - this.server = server - - this.vitenode = new ViteNodeServer(server, this.config.server) - const node = this.vitenode - this.runner = new ViteNodeRunner({ - root: server.config.root, - base: server.config.base, - fetchModule(id: string) { - return node.fetchModule(id) - }, - resolveId(id: string, importer?: string) { - return node.resolveId(id, importer) - }, - }) - - await this.initBrowserServer(this.server.config.configFile) - } - - isBrowserEnabled(): boolean { - return isBrowserEnabled(this.config) - } - - 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 config - } - return deepMerge( - config, - this.ctx.configOverride, - ) - } - - close() { - if (!this.closingPromise) { - this.closingPromise = Promise.all( - [ - this.server.close(), - this.typechecker?.stop(), - this.browser?.close(), - this.clearTmpDir(), - ].filter(Boolean), - ).then(() => (this._provided = {} as any)) - } - return this.closingPromise - } - - private async clearTmpDir() { - try { - await rm(this.tmpDir, { recursive: true }) - } - catch {} - } - - async initBrowserProvider() { - if (!this.isBrowserEnabled()) { - return - } - await this.browser?.initBrowserProvider() - } -} From cfe6504b0c5be40e9fd83cad7db34d3fdc98875d Mon Sep 17 00:00:00 2001 From: mzhubail Date: Thu, 21 Nov 2024 08:53:43 +0300 Subject: [PATCH 14/28] Change `FileSpec` imports --- 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 +- packages/vitest/src/runtime/runBaseTests.ts | 2 +- packages/vitest/src/runtime/runVmTests.ts | 2 +- packages/vitest/src/types/worker.ts | 3 +-- 7 files changed, 7 insertions(+), 8 deletions(-) diff --git a/packages/vitest/src/node/pools/forks.ts b/packages/vitest/src/node/pools/forks.ts index 34c68b4c41a5..a1315899c596 100644 --- a/packages/vitest/src/node/pools/forks.ts +++ b/packages/vitest/src/node/pools/forks.ts @@ -1,4 +1,4 @@ -import type { FileSpec } from '@vitest/runner/types/runner' +import type { FileSpec } from '@vitest/runner' import type { TinypoolChannel, Options as TinypoolOptions } from 'tinypool' import type { RunnerRPC, RuntimeRPC } from '../../types/rpc' import type { ContextRPC, ContextTestEnvironment } from '../../types/worker' diff --git a/packages/vitest/src/node/pools/threads.ts b/packages/vitest/src/node/pools/threads.ts index 71f5e024688c..8b3ef5674bbd 100644 --- a/packages/vitest/src/node/pools/threads.ts +++ b/packages/vitest/src/node/pools/threads.ts @@ -1,4 +1,4 @@ -import type { FileSpec } from '@vitest/runner/types/runner' +import type { FileSpec } from '@vitest/runner' import type { Options as TinypoolOptions } from 'tinypool' import type { RunnerRPC, RuntimeRPC } from '../../types/rpc' import type { ContextTestEnvironment } from '../../types/worker' diff --git a/packages/vitest/src/node/pools/vmForks.ts b/packages/vitest/src/node/pools/vmForks.ts index ed5a0e08de49..dd4e2ec4a909 100644 --- a/packages/vitest/src/node/pools/vmForks.ts +++ b/packages/vitest/src/node/pools/vmForks.ts @@ -1,4 +1,4 @@ -import type { FileSpec } from '@vitest/runner/types/runner' +import type { FileSpec } from '@vitest/runner' import type { TinypoolChannel, Options as TinypoolOptions } from 'tinypool' import type { RunnerRPC, RuntimeRPC } from '../../types/rpc' import type { ContextRPC, ContextTestEnvironment } from '../../types/worker' diff --git a/packages/vitest/src/node/pools/vmThreads.ts b/packages/vitest/src/node/pools/vmThreads.ts index 3192f6aed127..bbdbbf7c00cb 100644 --- a/packages/vitest/src/node/pools/vmThreads.ts +++ b/packages/vitest/src/node/pools/vmThreads.ts @@ -1,4 +1,4 @@ -import type { FileSpec } from '@vitest/runner/types/runner' +import type { FileSpec } from '@vitest/runner' import type { Options as TinypoolOptions } from 'tinypool' import type { RunnerRPC, RuntimeRPC } from '../../types/rpc' import type { ContextTestEnvironment } from '../../types/worker' diff --git a/packages/vitest/src/runtime/runBaseTests.ts b/packages/vitest/src/runtime/runBaseTests.ts index aef7b4e7d965..b15e5767970a 100644 --- a/packages/vitest/src/runtime/runBaseTests.ts +++ b/packages/vitest/src/runtime/runBaseTests.ts @@ -1,4 +1,4 @@ -import type { FileSpec } from '@vitest/runner/types/runner' +import type { FileSpec } from '@vitest/runner' import type { ResolvedTestEnvironment } from '../types/environment' import type { SerializedConfig } from './config' import type { VitestExecutor } from './execute' diff --git a/packages/vitest/src/runtime/runVmTests.ts b/packages/vitest/src/runtime/runVmTests.ts index 47178911bc0e..bc22d124e72a 100644 --- a/packages/vitest/src/runtime/runVmTests.ts +++ b/packages/vitest/src/runtime/runVmTests.ts @@ -1,4 +1,4 @@ -import type { FileSpec } from '@vitest/runner/types/runner' +import type { FileSpec } from '@vitest/runner' import type { SerializedConfig } from './config' import type { VitestExecutor } from './execute' import { createRequire } from 'node:module' diff --git a/packages/vitest/src/types/worker.ts b/packages/vitest/src/types/worker.ts index 0358e046a518..ee712b2b60c2 100644 --- a/packages/vitest/src/types/worker.ts +++ b/packages/vitest/src/types/worker.ts @@ -1,5 +1,4 @@ -import type { CancelReason, Task } from '@vitest/runner' -import type { FileSpec } from '@vitest/runner/types/runner' +import type { CancelReason, FileSpec, Task } from '@vitest/runner' import type { BirpcReturn } from 'birpc' import type { ModuleCacheMap, ViteNodeResolveId } from 'vite-node' import type { SerializedConfig } from '../runtime/config' From f80b6ca8203ee7ac0d471137971ea2b845d747c5 Mon Sep 17 00:00:00 2001 From: mzhubail Date: Thu, 21 Nov 2024 09:14:06 +0300 Subject: [PATCH 15/28] Remove unnecessary changes --- packages/vitest/src/node/pools/forks.ts | 3 ++- packages/vitest/src/node/pools/threads.ts | 5 +++-- packages/vitest/src/node/spec.ts | 1 + 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/vitest/src/node/pools/forks.ts b/packages/vitest/src/node/pools/forks.ts index a1315899c596..26172d6c2158 100644 --- a/packages/vitest/src/node/pools/forks.ts +++ b/packages/vitest/src/node/pools/forks.ts @@ -266,10 +266,11 @@ export function createForksPool( // Always run environments isolated between each other await pool.recycleWorkers() + const filenames = files.map(f => f.file) await runFiles( files[0].project, getConfig(files[0].project), - files.map(f => f.file), + filenames, files[0].environment, invalidates, ) diff --git a/packages/vitest/src/node/pools/threads.ts b/packages/vitest/src/node/pools/threads.ts index 8b3ef5674bbd..b1c8786163c9 100644 --- a/packages/vitest/src/node/pools/threads.ts +++ b/packages/vitest/src/node/pools/threads.ts @@ -1,4 +1,4 @@ -import type { FileSpec } from '@vitest/runner' +import type { FileSpec } from '@vitest/runner/types/runner' import type { Options as TinypoolOptions } from 'tinypool' import type { RunnerRPC, RuntimeRPC } from '../../types/rpc' import type { ContextTestEnvironment } from '../../types/worker' @@ -262,10 +262,11 @@ export function createThreadsPool( // Always run environments isolated between each other await pool.recycleWorkers() + const filenames = files.map(f => f.file) await runFiles( files[0].project, getConfig(files[0].project), - files.map(f => f.file), + filenames, files[0].environment, invalidates, ) diff --git a/packages/vitest/src/node/spec.ts b/packages/vitest/src/node/spec.ts index 0088c445c52c..9c74a8ca87d1 100644 --- a/packages/vitest/src/node/spec.ts +++ b/packages/vitest/src/node/spec.ts @@ -28,6 +28,7 @@ export class TestSpecification { moduleId: string, pool: Pool, testLocations?: number[] | undefined, + // location?: WorkspaceSpecLocation | undefined, ) { this[0] = project this[1] = moduleId From f2cd06889e59036a1af086a6ebea0c7aa8502724 Mon Sep 17 00:00:00 2001 From: mzhubail Date: Thu, 21 Nov 2024 11:20:53 +0300 Subject: [PATCH 16/28] Modify testing setup --- test/cli/fixtures/location-filters/custom.config.ts | 9 --------- test/cli/fixtures/location-filters/fail.config.ts | 7 ------- .../location-filters/math-with-dashes-in-name.test.ts | 9 +++++++++ test/cli/fixtures/location-filters/math.test.ts | 4 ++-- .../fixtures/location-filters/no-task-location.config.ts | 2 -- test/cli/fixtures/location-filters/vitest.config.ts | 7 ------- test/cli/test/location-filters.test.ts | 5 +---- 7 files changed, 12 insertions(+), 31 deletions(-) delete mode 100644 test/cli/fixtures/location-filters/custom.config.ts delete mode 100644 test/cli/fixtures/location-filters/fail.config.ts create mode 100644 test/cli/fixtures/location-filters/math-with-dashes-in-name.test.ts diff --git a/test/cli/fixtures/location-filters/custom.config.ts b/test/cli/fixtures/location-filters/custom.config.ts deleted file mode 100644 index 841db817e7b8..000000000000 --- a/test/cli/fixtures/location-filters/custom.config.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { defineConfig } from 'vitest/config' - -export default defineConfig({ - test: { - include: ['basic.test.ts', 'math.test.ts'], - name: 'custom', - includeTaskLocation: true, - }, -}) diff --git a/test/cli/fixtures/location-filters/fail.config.ts b/test/cli/fixtures/location-filters/fail.config.ts deleted file mode 100644 index 16d254a9aaab..000000000000 --- a/test/cli/fixtures/location-filters/fail.config.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { defineConfig } from 'vitest/config' - -export default defineConfig({ - test: { - include: ['top-level-error.test.ts', 'describe-error.test.ts'], - }, -}) diff --git a/test/cli/fixtures/location-filters/math-with-dashes-in-name.test.ts b/test/cli/fixtures/location-filters/math-with-dashes-in-name.test.ts new file mode 100644 index 000000000000..4727c76eacc4 --- /dev/null +++ b/test/cli/fixtures/location-filters/math-with-dashes-in-name.test.ts @@ -0,0 +1,9 @@ +import { expect, it } from 'vitest' + +it('1 plus 1', () => { + expect(1 + 1).toBe(2) +}) + +it('2 plus 2', () => { + expect(2 + 2).toBe(4) +}) diff --git a/test/cli/fixtures/location-filters/math.test.ts b/test/cli/fixtures/location-filters/math.test.ts index c39601812ac5..4727c76eacc4 100644 --- a/test/cli/fixtures/location-filters/math.test.ts +++ b/test/cli/fixtures/location-filters/math.test.ts @@ -4,6 +4,6 @@ it('1 plus 1', () => { expect(1 + 1).toBe(2) }) -it('failing test', () => { - expect(1 + 1).toBe(3) +it('2 plus 2', () => { + expect(2 + 2).toBe(4) }) diff --git a/test/cli/fixtures/location-filters/no-task-location.config.ts b/test/cli/fixtures/location-filters/no-task-location.config.ts index 6ef28d4e2315..f4d0e5f76d3b 100644 --- a/test/cli/fixtures/location-filters/no-task-location.config.ts +++ b/test/cli/fixtures/location-filters/no-task-location.config.ts @@ -2,8 +2,6 @@ import { defineConfig } from 'vitest/config' export default defineConfig({ test: { - include: ['basic.test.ts', 'math.test.ts'], - name: 'no task location', includeTaskLocation: false, }, }) diff --git a/test/cli/fixtures/location-filters/vitest.config.ts b/test/cli/fixtures/location-filters/vitest.config.ts index 10b4610e41b7..42676ddb8355 100644 --- a/test/cli/fixtures/location-filters/vitest.config.ts +++ b/test/cli/fixtures/location-filters/vitest.config.ts @@ -2,13 +2,6 @@ import { defineConfig } from 'vitest/config' export default defineConfig({ test: { - include: ['basic.test.ts', 'math.test.ts'], - browser: { - name: 'chromium', - provider: 'playwright', - headless: true, - api: 7523, - }, includeTaskLocation: true, }, }) diff --git a/test/cli/test/location-filters.test.ts b/test/cli/test/location-filters.test.ts index 3a07f159f3a0..b18ae6adb3a7 100644 --- a/test/cli/test/location-filters.test.ts +++ b/test/cli/test/location-filters.test.ts @@ -6,12 +6,11 @@ describe('location filter with list command', () => { const { stdout, stderr } = await runVitestCli( 'list', '-r=./fixtures/location-filters', - '--config=custom.config.ts', 'basic.test.ts:5', ) expect(stdout).toMatchInlineSnapshot(` - "[custom] basic.test.ts > basic suite > inner suite > some test + "basic.test.ts > basic suite > inner suite > some test " `) expect(stderr).toEqual('') @@ -21,7 +20,6 @@ describe('location filter with list command', () => { const { stdout, stderr } = await runVitestCli( 'list', '-r=./fixtures/location-filters', - '--config=custom.config.ts', 'basic.test.ts:99', ) @@ -36,7 +34,6 @@ describe('location filter with list command', () => { const { stdout, stderr } = await runVitestCli( 'list', '-r=./fixtures/location-filters', - '--config=custom.config.ts', 'basic.test.ts:5', 'basic.test.ts:12', 'basic.test.ts:99', From 52d5e3b4a928a3d3c1b8c6ce847de8629f3ec1c2 Mon Sep 17 00:00:00 2001 From: mzhubail Date: Thu, 21 Nov 2024 12:08:50 +0300 Subject: [PATCH 17/28] Add run command tests --- test/cli/test/location-filters.test.ts | 153 +++++++++++++------------ 1 file changed, 77 insertions(+), 76 deletions(-) diff --git a/test/cli/test/location-filters.test.ts b/test/cli/test/location-filters.test.ts index b18ae6adb3a7..a516d37bca32 100644 --- a/test/cli/test/location-filters.test.ts +++ b/test/cli/test/location-filters.test.ts @@ -72,80 +72,81 @@ describe('location filter with list command', () => { }) }) -// describe('location filter with run command', () => { -// test('finds test at correct line number', async () => { -// const { stdout, stderr } = await runVitestCli( -// 'run', -// '-r=./fixtures/location-filters', -// '--config=vitest.config.ts', -// 'basic.test.ts:6', -// // 'basic.test.ts:15', -// // 'math.test.ts:3', -// ) -// -// expect(`${stdout} ${stderr}`).toEqual('') -// -// expect(stdout).toMatchInlineSnapshot(` -// "[custom] basic.test.ts > basic suite > inner suite > some test -// " -// `) -// expect(stderr).toEqual('') -// }) +describe('location filter with run command', () => { + test('handles file with a dash in the name', async () => { + const { stdout, stderr } = await runVitestCli( + 'run', + '-r=./fixtures/location-filters', + 'math-with-dashes-in-name.test.ts:3', + ) + + expect(stdout).contain('1 passed') + expect(stdout).contain('1 skipped') + expect(stderr).toEqual('') + }) + + test('reports not found test', async () => { + const { stdout, stderr } = await runVitestCli( + 'run', + '-r=./fixtures/location-filters', + 'basic.test.ts:99', + ) + + expect(stdout).toContain('4 skipped') + expect(stderr).toContain('Error: No test found in basic.test.ts in line 99') + }) + + test('reports multiple not found tests', async () => { + const { stdout, stderr } = await runVitestCli( + 'run', + '-r=./fixtures/location-filters', + 'basic.test.ts:5', + 'basic.test.ts:12', + 'basic.test.ts:99', + ) + + expect(stdout).toContain('4 skipped') + expect(stderr).toContain('Error: No test found in basic.test.ts in lines 12, 99') + }) + + test('errors if range location is provided', async () => { + const { stderr } = await runVitestCli( + 'run', + '-r=./fixtures/location-filters', + 'a/file/that/doesnt/exit:10-15', + ) + + expect(stderr).toContain('Error: Found "-"') + }) + + test('errors if includeTaskLocation is not enabled', async () => { + const { stderr } = await runVitestCli( + 'run', + '-r=./fixtures/location-filters', + '--config=no-task-location.config.ts', + 'a/file/that/doesnt/exist:5', + ) + + expect(stderr).toMatchInlineSnapshot(` + "Error: Recieved line number filters while \`includeTaskLocation\` option is disabled + " + `) + }) + + test('fails on part of filename with location filter', async () => { + const { stdout, stderr } = await runVitestCli( + 'run', + '-r=./fixtures/location-filters', + 'math:999', + ) + + expect(stdout).contain('math.test.ts') + expect(stdout).contain('math-with-dashes-in-name.test.ts') + expect(stderr).toMatchInlineSnapshot(` + "Error: Couldn't find file "math". Ignoring test location filter. + Note when specifying the test location you have to specify the full test filename. + " + `) + }) +}) // -// // test('reports not found test', async () => { -// // const { stdout, stderr } = await runVitestCli( -// // 'list', -// // '-r=./fixtures/location-filters', -// // '--config=custom.config.ts', -// // 'basic.test.ts:99', -// // ) -// // -// // expect(stdout).toEqual('') -// // expect(stderr).toMatchInlineSnapshot(` -// // "Error: No test found in basic.test.ts in line 99 -// // " -// // `) -// // }) -// // -// // test('reports multiple not found tests', async () => { -// // const { stdout, stderr } = await runVitestCli( -// // 'list', -// // '-r=./fixtures/location-filters', -// // '--config=custom.config.ts', -// // 'basic.test.ts:5', -// // 'basic.test.ts:12', -// // 'basic.test.ts:99', -// // ) -// // -// // expect(stdout).toEqual('') -// // expect(stderr).toMatchInlineSnapshot(` -// // "Error: No test found in basic.test.ts in lines 12, 99 -// // " -// // `) -// // }) -// // -// // test('errors if range location is provided', async () => { -// // const { stdout, stderr } = await runVitestCli( -// // 'list', -// // '-r=./fixtures/location-filters', -// // 'a/file/that/doesnt/exit:10-15', -// // ) -// // -// // expect(stdout).toEqual('') -// // expect(stderr).toContain('Collect Error') -// // expect(stderr).toContain('RangeLocationFilterProvidedError') -// // }) -// // -// // test('erorrs if includeTaskLocation is not enabled', async () => { -// // const { stdout, stderr } = await runVitestCli( -// // 'list', -// // '-r=./fixtures/location-filters', -// // '--config=no-task-location.config.ts', -// // 'a/file/that/doesnt/exist:5', -// // ) -// // -// // expect(stdout).toEqual('') -// // expect(stderr).toContain('Collect Error') -// // expect(stderr).toContain('IncludeTaskLocationDisabledError') -// // }) -// }) From eb05faa8b05c454f6ad3173e8dbd4a3143ef2dad Mon Sep 17 00:00:00 2001 From: mzhubail Date: Thu, 21 Nov 2024 11:21:05 +0300 Subject: [PATCH 18/28] Change file not found error In `globTestSpecs`, test locations are matched with their test file by full path (`globTestFiles` returns full path). I think we should keep it this way because:(A) matching multiple files doesn't make much sense -- for example, `fileA.test.ts` might have test at line 21, but `fileB.test.ts` won't; and (B) this feature will be used in command line mostly, where entering full path is easy with autocomplete. Issue arises when location filter (let's say `file`) doesn't match a file by full path. Previously, I thought about dropping files `fileA.test.ts` and `fileB.test.ts`, and running the rest of the files but implementing this doesn't seem straightforward to me. For now, I'll be throwing error. --- packages/vitest/src/node/cli/cli-api.ts | 3 ++- packages/vitest/src/node/core.ts | 10 ++++------ packages/vitest/src/node/errors.ts | 9 +++++++++ test/cli/test/location-filters.test.ts | 7 +++---- 4 files changed, 18 insertions(+), 11 deletions(-) diff --git a/packages/vitest/src/node/cli/cli-api.ts b/packages/vitest/src/node/cli/cli-api.ts index 5fa78c8ed5a0..a0fb736c518c 100644 --- a/packages/vitest/src/node/cli/cli-api.ts +++ b/packages/vitest/src/node/cli/cli-api.ts @@ -9,7 +9,7 @@ import { getNames, getTests } from '@vitest/runner/utils' import { dirname, relative, resolve } from 'pathe' import { CoverageProviderMap } from '../../integrations/coverage' import { createVitest } from '../create' -import { FilesNotFoundError, GitNotFoundError, IncludeTaskLocationDisabledError, RangeLocationFilterProvidedError } from '../errors' +import { FilesNotFoundError, GitNotFoundError, IncludeTaskLocationDisabledError, LocationFilterFileNotFoundError, RangeLocationFilterProvidedError } from '../errors' import { registerConsoleShortcuts } from '../stdin' export interface CliOptions extends UserConfig { @@ -106,6 +106,7 @@ export async function startVitest( if ( e instanceof IncludeTaskLocationDisabledError || e instanceof RangeLocationFilterProvidedError + || e instanceof LocationFilterFileNotFoundError ) { ctx.logger.printError(e, { verbose: false }) return ctx diff --git a/packages/vitest/src/node/core.ts b/packages/vitest/src/node/core.ts index d37f5a39c81b..61e980a479f2 100644 --- a/packages/vitest/src/node/core.ts +++ b/packages/vitest/src/node/core.ts @@ -28,7 +28,7 @@ import { wildcardPatternToRegExp } from '../utils/base' import { VitestCache } from './cache' import { groupFilters, parseFilter } from './cli/filter' import { resolveConfig } from './config/resolveConfig' -import { FilesNotFoundError, GitNotFoundError, IncludeTaskLocationDisabledError } from './errors' +import { FilesNotFoundError, GitNotFoundError, IncludeTaskLocationDisabledError, LocationFilterFileNotFoundError } from './errors' import { Logger } from './logger' import { VitestPackageInstaller } from './packageInstaller' import { createPool } from './pool' @@ -1188,11 +1188,9 @@ export class Vitest { Object.entries(testLocations).forEach(([filepath, loc]) => { if (loc.length !== 0 && !testLocHasMatch[filepath]) { - const rel = relative(dir, filepath) - - this.logger.printError(new Error(`Couldn\'t find file "${rel}".\n` - + 'Note when specifying the test location you have to specify the full test filename.', - )) + throw new LocationFilterFileNotFoundError( + relative(dir, filepath), + ) } }) diff --git a/packages/vitest/src/node/errors.ts b/packages/vitest/src/node/errors.ts index ea7681c7ad0d..4e0a703713c3 100644 --- a/packages/vitest/src/node/errors.ts +++ b/packages/vitest/src/node/errors.ts @@ -14,6 +14,15 @@ export class GitNotFoundError extends Error { } } +export class LocationFilterFileNotFoundError extends Error { + code = 'VITEST_LOCATION_FILTER_FILE_NOT_FOUND' + + constructor(filename: string) { + super(`Couldn\'t find file ${filename}. Note when specifying the test ` + + 'location you have to specify the full test filename.') + } +} + export class IncludeTaskLocationDisabledError extends Error { code = 'VITEST_INCLUDE_TASK_LOCATION_DISABLED' diff --git a/test/cli/test/location-filters.test.ts b/test/cli/test/location-filters.test.ts index a516d37bca32..5ede942ccc24 100644 --- a/test/cli/test/location-filters.test.ts +++ b/test/cli/test/location-filters.test.ts @@ -140,11 +140,10 @@ describe('location filter with run command', () => { 'math:999', ) - expect(stdout).contain('math.test.ts') - expect(stdout).contain('math-with-dashes-in-name.test.ts') + expect(stdout).not.contain('math.test.ts') + expect(stdout).not.contain('math-with-dashes-in-name.test.ts') expect(stderr).toMatchInlineSnapshot(` - "Error: Couldn't find file "math". Ignoring test location filter. - Note when specifying the test location you have to specify the full test filename. + "Error: Couldn't find file math. Note when specifying the test location you have to specify the full test filename. " `) }) From 68debe6f1f43c29a168267c0964d009bb8234fb8 Mon Sep 17 00:00:00 2001 From: mzhubail Date: Thu, 21 Nov 2024 12:27:19 +0300 Subject: [PATCH 19/28] Add a couple of test cases --- test/cli/test/location-filters.test.ts | 28 +++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/test/cli/test/location-filters.test.ts b/test/cli/test/location-filters.test.ts index 5ede942ccc24..bfaddeb2daa2 100644 --- a/test/cli/test/location-filters.test.ts +++ b/test/cli/test/location-filters.test.ts @@ -16,6 +16,20 @@ describe('location filter with list command', () => { expect(stderr).toEqual('') }) + test('handles file with a dash in the name', async () => { + const { stdout, stderr } = await runVitestCli( + 'list', + '-r=./fixtures/location-filters', + 'math-with-dashes-in-name.test.ts:3', + ) + + expect(stdout).toMatchInlineSnapshot(` + "math-with-dashes-in-name.test.ts > 1 plus 1 + " + `) + expect(stderr).toEqual('') + }) + test('reports not found test', async () => { const { stdout, stderr } = await runVitestCli( 'list', @@ -73,6 +87,19 @@ describe('location filter with list command', () => { }) describe('location filter with run command', () => { + test('finds test at correct line number', async () => { + const { stdout, stderr } = await runVitestCli( + 'run', + '-r=./fixtures/location-filters', + '--config=vitest.config.ts', + 'math.test.ts:3', + ) + + expect(stdout).contain('1 passed') + expect(stdout).contain('1 skipped') + expect(stderr).toEqual('') + }) + test('handles file with a dash in the name', async () => { const { stdout, stderr } = await runVitestCli( 'run', @@ -148,4 +175,3 @@ describe('location filter with run command', () => { `) }) }) -// From 8ead31c1bb7dab3dbbb643e8183ae3df5ea5d062 Mon Sep 17 00:00:00 2001 From: mzhubail Date: Thu, 21 Nov 2024 12:42:44 +0300 Subject: [PATCH 20/28] Remove duplicate tests from `list.test.ts` --- test/cli/test/__snapshots__/list.test.ts.snap | 6 -- test/cli/test/list.test.ts | 84 ------------------- 2 files changed, 90 deletions(-) diff --git a/test/cli/test/__snapshots__/list.test.ts.snap b/test/cli/test/__snapshots__/list.test.ts.snap index d62c03773997..efc6f0b467ad 100644 --- a/test/cli/test/__snapshots__/list.test.ts.snap +++ b/test/cli/test/__snapshots__/list.test.ts.snap @@ -59,12 +59,6 @@ math.test.ts > failing test " `; -exports[`file not found mentions strict matching for location filters 1`] = ` -"Error: Couldn't find file "a/file/that/doesnt/exit". -Note when specifying the test location you have to specify the full test filename. -" -`; - exports[`json output shows error 1`] = ` "Error: top level error ❯ top-level-error.test.ts:1:7 diff --git a/test/cli/test/list.test.ts b/test/cli/test/list.test.ts index 5b699f86535a..ad3c8a03267b 100644 --- a/test/cli/test/list.test.ts +++ b/test/cli/test/list.test.ts @@ -231,90 +231,6 @@ test('ignores watch flag', async () => { `) }) -test('file not found mentions strict matching for location filters', async () => { - const { stdout, stderr } = await runVitestCli( - 'list', - '-r=./fixtures/list', - '--config=custom.config.ts', - 'a/file/that/doesnt/exit:10', - ) - - expect(stderr).toMatchSnapshot() - expect(stdout).toEqual('') -}) - -test('location filter finds test at correct line number', async () => { - const { stdout, stderr } = await runVitestCli( - 'list', - '-r=./fixtures/list', - '--config=custom.config.ts', - 'basic.test.ts:5', - ) - - expect(stdout).toMatchInlineSnapshot(` - "[custom] basic.test.ts > basic suite > inner suite > some test - " - `) - expect(stderr).toEqual('') -}) - -test('location filter reports not found test', async () => { - const { stdout, stderr } = await runVitestCli( - 'list', - '-r=./fixtures/list', - '--config=custom.config.ts', - 'basic.test.ts:99', - ) - - expect(stdout).toEqual('') - expect(stderr).toMatchInlineSnapshot(` - "Error: No test found in basic.test.ts in line 99 - " - `) -}) - -test('location filter reports multiple not found tests', async () => { - const { stdout, stderr } = await runVitestCli( - 'list', - '-r=./fixtures/list', - '--config=custom.config.ts', - 'basic.test.ts:5', - 'basic.test.ts:12', - 'basic.test.ts:99', - ) - - expect(stdout).toEqual('') - expect(stderr).toMatchInlineSnapshot(` - "Error: No test found in basic.test.ts in lines 12, 99 - " - `) -}) - -test('error if range location is provided', async () => { - const { stdout, stderr } = await runVitestCli( - 'list', - '-r=./fixtures/list', - 'a/file/that/doesnt/exit:10-15', - ) - - expect(stdout).toEqual('') - expect(stderr).toContain('Collect Error') - expect(stderr).toContain('RangeLocationFilterProvidedError') -}) - -test('erorr if location filter provided without enabling includeTaskLocation', async () => { - const { stdout, stderr } = await runVitestCli( - 'list', - '-r=./fixtures/list', - '--config=no-task-location.config.ts', - 'a/file/that/doesnt/exist:5', - ) - - expect(stdout).toEqual('') - expect(stderr).toContain('Collect Error') - expect(stderr).toContain('IncludeTaskLocationDisabledError') -}) - function relative(stdout: string) { return stdout.replace(new RegExp(slash(process.cwd()), 'gi'), '') } From 1607250f5fb235ca43d57ae8f9944ce2a974122e Mon Sep 17 00:00:00 2001 From: mzhubail Date: Thu, 21 Nov 2024 12:46:23 +0300 Subject: [PATCH 21/28] Remove more unused stuff --- test/cli/fixtures/list/no-task-location.config.ts | 10 ---------- .../fixtures/location-filters/describe-error.test.ts | 9 --------- .../fixtures/location-filters/top-level-error.test.ts | 1 - 3 files changed, 20 deletions(-) delete mode 100644 test/cli/fixtures/list/no-task-location.config.ts delete mode 100644 test/cli/fixtures/location-filters/describe-error.test.ts delete mode 100644 test/cli/fixtures/location-filters/top-level-error.test.ts diff --git a/test/cli/fixtures/list/no-task-location.config.ts b/test/cli/fixtures/list/no-task-location.config.ts deleted file mode 100644 index 6ef28d4e2315..000000000000 --- a/test/cli/fixtures/list/no-task-location.config.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { defineConfig } from 'vitest/config' - -export default defineConfig({ - test: { - include: ['basic.test.ts', 'math.test.ts'], - name: 'no task location', - includeTaskLocation: false, - }, -}) - diff --git a/test/cli/fixtures/location-filters/describe-error.test.ts b/test/cli/fixtures/location-filters/describe-error.test.ts deleted file mode 100644 index 3238cceabdb6..000000000000 --- a/test/cli/fixtures/location-filters/describe-error.test.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { describe, expect, it } from 'vitest'; - -describe('describe error', () => { - throw new Error('describe error') - - it('wont run', () => { - expect(true).toBe(true) - }) -}) \ No newline at end of file diff --git a/test/cli/fixtures/location-filters/top-level-error.test.ts b/test/cli/fixtures/location-filters/top-level-error.test.ts deleted file mode 100644 index 59922e3fd360..000000000000 --- a/test/cli/fixtures/location-filters/top-level-error.test.ts +++ /dev/null @@ -1 +0,0 @@ -throw new Error('top level error') From 0952978c132b3d997d1202d34b36739f380905db Mon Sep 17 00:00:00 2001 From: mzhubail Date: Thu, 21 Nov 2024 13:32:34 +0300 Subject: [PATCH 22/28] One more test --- test/cli/test/location-filters.test.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test/cli/test/location-filters.test.ts b/test/cli/test/location-filters.test.ts index bfaddeb2daa2..2e19b18a8a25 100644 --- a/test/cli/test/location-filters.test.ts +++ b/test/cli/test/location-filters.test.ts @@ -84,6 +84,18 @@ describe('location filter with list command', () => { expect(stderr).toContain('Collect Error') expect(stderr).toContain('IncludeTaskLocationDisabledError') }) + + test('fails on part of filename with location filter', async () => { + const { stdout, stderr } = await runVitestCli( + 'list', + '-r=./fixtures/location-filters', + 'math:999', + ) + + expect(stdout).toEqual('') + expect(stderr).toContain('Collect Error') + expect(stderr).toContain('LocationFilterFileNotFoundError') + }) }) describe('location filter with run command', () => { From 21fd0a9e9c696c5336a7efc15812472781dc0dc2 Mon Sep 17 00:00:00 2001 From: mzhubail Date: Thu, 21 Nov 2024 14:06:17 +0300 Subject: [PATCH 23/28] Resolve against `process.cwd()` --- packages/vitest/src/node/core.ts | 10 ++-- test/cli/test/location-filters.test.ts | 71 ++++++++++++++------------ 2 files changed, 42 insertions(+), 39 deletions(-) diff --git a/packages/vitest/src/node/core.ts b/packages/vitest/src/node/core.ts index 61e980a479f2..90b20d4abc6a 100644 --- a/packages/vitest/src/node/core.ts +++ b/packages/vitest/src/node/core.ts @@ -1146,12 +1146,8 @@ export class Vitest { public async globTestSpecs(filters: string[] = []) { const files: TestSpecification[] = [] - const dir = this.config.dir || this.config.root + const dir = process.cwd() const parsedFilters = filters.map(f => parseFilter(f)) - const testLocations = groupFilters(parsedFilters.map( - f => ({ ...f, filename: resolve(dir, f.filename) }), - )) - // Require includeTaskLocation when a location filter is passed if ( !this.config.includeTaskLocation @@ -1160,6 +1156,10 @@ export class Vitest { throw new IncludeTaskLocationDisabledError() } + const testLocations = groupFilters(parsedFilters.map( + f => ({ ...f, filename: resolve(dir, f.filename) }), + )) + // Key is file and val sepcifies whether we have matched this file with testLocation const testLocHasMatch: { [f: string]: boolean } = {} diff --git a/test/cli/test/location-filters.test.ts b/test/cli/test/location-filters.test.ts index 2e19b18a8a25..87a436e8dece 100644 --- a/test/cli/test/location-filters.test.ts +++ b/test/cli/test/location-filters.test.ts @@ -1,12 +1,14 @@ import { describe, expect, test } from 'vitest' import { runVitestCli } from '../../test-utils' +const fixturePath = './fixtures/location-filters' + describe('location filter with list command', () => { test('finds test at correct line number', async () => { const { stdout, stderr } = await runVitestCli( 'list', - '-r=./fixtures/location-filters', - 'basic.test.ts:5', + `-r=${fixturePath}`, + `${fixturePath}/basic.test.ts:5`, ) expect(stdout).toMatchInlineSnapshot(` @@ -19,8 +21,8 @@ describe('location filter with list command', () => { test('handles file with a dash in the name', async () => { const { stdout, stderr } = await runVitestCli( 'list', - '-r=./fixtures/location-filters', - 'math-with-dashes-in-name.test.ts:3', + `-r=${fixturePath}`, + `${fixturePath}/math-with-dashes-in-name.test.ts:3`, ) expect(stdout).toMatchInlineSnapshot(` @@ -33,8 +35,8 @@ describe('location filter with list command', () => { test('reports not found test', async () => { const { stdout, stderr } = await runVitestCli( 'list', - '-r=./fixtures/location-filters', - 'basic.test.ts:99', + `-r=${fixturePath}`, + `${fixturePath}/basic.test.ts:99`, ) expect(stdout).toEqual('') @@ -47,10 +49,10 @@ describe('location filter with list command', () => { test('reports multiple not found tests', async () => { const { stdout, stderr } = await runVitestCli( 'list', - '-r=./fixtures/location-filters', - 'basic.test.ts:5', - 'basic.test.ts:12', - 'basic.test.ts:99', + `-r=${fixturePath}`, + `${fixturePath}/basic.test.ts:5`, + `${fixturePath}/basic.test.ts:12`, + `${fixturePath}/basic.test.ts:99`, ) expect(stdout).toEqual('') @@ -63,8 +65,8 @@ describe('location filter with list command', () => { test('errors if range location is provided', async () => { const { stdout, stderr } = await runVitestCli( 'list', - '-r=./fixtures/location-filters', - 'a/file/that/doesnt/exit:10-15', + `-r=${fixturePath}`, + `${fixturePath}/a/file/that/doesnt/exit:10-15`, ) expect(stdout).toEqual('') @@ -75,9 +77,9 @@ describe('location filter with list command', () => { test('erorrs if includeTaskLocation is not enabled', async () => { const { stdout, stderr } = await runVitestCli( 'list', - '-r=./fixtures/location-filters', + `-r=${fixturePath}`, '--config=no-task-location.config.ts', - 'a/file/that/doesnt/exist:5', + `${fixturePath}/a/file/that/doesnt/exist:5`, ) expect(stdout).toEqual('') @@ -88,8 +90,8 @@ describe('location filter with list command', () => { test('fails on part of filename with location filter', async () => { const { stdout, stderr } = await runVitestCli( 'list', - '-r=./fixtures/location-filters', - 'math:999', + `-r=${fixturePath}`, + `math:999`, ) expect(stdout).toEqual('') @@ -102,11 +104,12 @@ describe('location filter with run command', () => { test('finds test at correct line number', async () => { const { stdout, stderr } = await runVitestCli( 'run', - '-r=./fixtures/location-filters', - '--config=vitest.config.ts', - 'math.test.ts:3', + `-r=${fixturePath}`, + `${fixturePath}/math.test.ts:3`, ) + // expect(`${stdout}\n--------------------\n${stderr}`).toEqual('') + expect(stdout).contain('1 passed') expect(stdout).contain('1 skipped') expect(stderr).toEqual('') @@ -115,8 +118,8 @@ describe('location filter with run command', () => { test('handles file with a dash in the name', async () => { const { stdout, stderr } = await runVitestCli( 'run', - '-r=./fixtures/location-filters', - 'math-with-dashes-in-name.test.ts:3', + `-r=${fixturePath}`, + `${fixturePath}/math-with-dashes-in-name.test.ts:3`, ) expect(stdout).contain('1 passed') @@ -127,8 +130,8 @@ describe('location filter with run command', () => { test('reports not found test', async () => { const { stdout, stderr } = await runVitestCli( 'run', - '-r=./fixtures/location-filters', - 'basic.test.ts:99', + `-r=${fixturePath}`, + `${fixturePath}/basic.test.ts:99`, ) expect(stdout).toContain('4 skipped') @@ -138,10 +141,10 @@ describe('location filter with run command', () => { test('reports multiple not found tests', async () => { const { stdout, stderr } = await runVitestCli( 'run', - '-r=./fixtures/location-filters', - 'basic.test.ts:5', - 'basic.test.ts:12', - 'basic.test.ts:99', + `-r=${fixturePath}`, + `${fixturePath}/basic.test.ts:5`, + `${fixturePath}/basic.test.ts:12`, + `${fixturePath}/basic.test.ts:99`, ) expect(stdout).toContain('4 skipped') @@ -151,8 +154,8 @@ describe('location filter with run command', () => { test('errors if range location is provided', async () => { const { stderr } = await runVitestCli( 'run', - '-r=./fixtures/location-filters', - 'a/file/that/doesnt/exit:10-15', + `-r=${fixturePath}`, + `${fixturePath}/a/file/that/doesnt/exit:10-15`, ) expect(stderr).toContain('Error: Found "-"') @@ -161,9 +164,9 @@ describe('location filter with run command', () => { test('errors if includeTaskLocation is not enabled', async () => { const { stderr } = await runVitestCli( 'run', - '-r=./fixtures/location-filters', - '--config=no-task-location.config.ts', - 'a/file/that/doesnt/exist:5', + `-r=${fixturePath}`, + `--config=no-task-location.config.ts`, + `${fixturePath}/a/file/that/doesnt/exist:5`, ) expect(stderr).toMatchInlineSnapshot(` @@ -175,8 +178,8 @@ describe('location filter with run command', () => { test('fails on part of filename with location filter', async () => { const { stdout, stderr } = await runVitestCli( 'run', - '-r=./fixtures/location-filters', - 'math:999', + `-r=${fixturePath}`, + `math:999`, ) expect(stdout).not.contain('math.test.ts') From e3487ad3b177abf976bcbf362c1695942844a70d Mon Sep 17 00:00:00 2001 From: mzhubail Date: Tue, 26 Nov 2024 09:19:44 +0300 Subject: [PATCH 24/28] Fix windows --- packages/vitest/src/node/core.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/vitest/src/node/core.ts b/packages/vitest/src/node/core.ts index 90b20d4abc6a..0a90e0d9ddac 100644 --- a/packages/vitest/src/node/core.ts +++ b/packages/vitest/src/node/core.ts @@ -1148,6 +1148,7 @@ export class Vitest { const files: TestSpecification[] = [] const dir = process.cwd() const parsedFilters = filters.map(f => parseFilter(f)) + // Require includeTaskLocation when a location filter is passed if ( !this.config.includeTaskLocation @@ -1157,7 +1158,7 @@ export class Vitest { } const testLocations = groupFilters(parsedFilters.map( - f => ({ ...f, filename: resolve(dir, f.filename) }), + f => ({ ...f, filename: slash(resolve(dir, f.filename)) }), )) // Key is file and val sepcifies whether we have matched this file with testLocation From 237f718b3bd804b728f2afbd4a4032e1d6bbd39a Mon Sep 17 00:00:00 2001 From: mzhubail Date: Tue, 26 Nov 2024 09:53:54 +0300 Subject: [PATCH 25/28] Make range line detection stricter - Previous implementation failed on windows (drive letter detected as range location `D:/...`) - Add related test cases too --- packages/vitest/src/node/cli/filter.ts | 4 +-- .../location-filters/math:colon-dash.test.ts | 9 +++++++ test/cli/test/location-filters.test.ts | 26 +++++++++++++++++++ 3 files changed, 37 insertions(+), 2 deletions(-) create mode 100644 test/cli/fixtures/location-filters/math:colon-dash.test.ts diff --git a/packages/vitest/src/node/cli/filter.ts b/packages/vitest/src/node/cli/filter.ts index 82dbbc2addd3..0fcc577e5a3f 100644 --- a/packages/vitest/src/node/cli/filter.ts +++ b/packages/vitest/src/node/cli/filter.ts @@ -2,7 +2,7 @@ import { groupBy } from '../../utils/base' import { RangeLocationFilterProvidedError } from '../errors' export function parseFilter(filter: string): Filter { - const colonIndex = filter.indexOf(':') + const colonIndex = filter.lastIndexOf(':') if (colonIndex === -1) { return { filename: filter } } @@ -18,7 +18,7 @@ export function parseFilter(filter: string): Filter { lineNumber: Number.parseInt(lineNumber), } } - else if (lineNumber.includes('-')) { + else if (lineNumber.match(/^\d+-\d+$/)) { throw new RangeLocationFilterProvidedError(filter) } else { diff --git a/test/cli/fixtures/location-filters/math:colon-dash.test.ts b/test/cli/fixtures/location-filters/math:colon-dash.test.ts new file mode 100644 index 000000000000..4727c76eacc4 --- /dev/null +++ b/test/cli/fixtures/location-filters/math:colon-dash.test.ts @@ -0,0 +1,9 @@ +import { expect, it } from 'vitest' + +it('1 plus 1', () => { + expect(1 + 1).toBe(2) +}) + +it('2 plus 2', () => { + expect(2 + 2).toBe(4) +}) diff --git a/test/cli/test/location-filters.test.ts b/test/cli/test/location-filters.test.ts index 87a436e8dece..094e84a717a8 100644 --- a/test/cli/test/location-filters.test.ts +++ b/test/cli/test/location-filters.test.ts @@ -32,6 +32,20 @@ describe('location filter with list command', () => { expect(stderr).toEqual('') }) + test('handles file with a colon and dash in the name', async () => { + const { stdout, stderr } = await runVitestCli( + 'list', + `-r=${fixturePath}`, + `${fixturePath}/math:colon-dash.test.ts:3`, + ) + + expect(stdout).toMatchInlineSnapshot(` + "math:colon-dash.test.ts > 1 plus 1 + " + `) + expect(stderr).toEqual('') + }) + test('reports not found test', async () => { const { stdout, stderr } = await runVitestCli( 'list', @@ -127,6 +141,18 @@ describe('location filter with run command', () => { expect(stderr).toEqual('') }) + test('handles file with a colon and dash in the name', async () => { + const { stdout, stderr } = await runVitestCli( + 'run', + `-r=${fixturePath}`, + `${fixturePath}/math:colon-dash.test.ts:3`, + ) + + expect(stdout).contain('1 passed') + expect(stdout).contain('1 skipped') + expect(stderr).toEqual('') + }) + test('reports not found test', async () => { const { stdout, stderr } = await runVitestCli( 'run', From dbce1893a352da113946cd55378ccdcba3229cd6 Mon Sep 17 00:00:00 2001 From: mzhubail Date: Tue, 26 Nov 2024 10:12:44 +0300 Subject: [PATCH 26/28] Remove file with a colon in name - Modify test case to use a file that doesn't exist instead --- .../location-filters/math:colon-dash.test.ts | 9 ---- test/cli/test/location-filters.test.ts | 49 +++++++++---------- 2 files changed, 23 insertions(+), 35 deletions(-) delete mode 100644 test/cli/fixtures/location-filters/math:colon-dash.test.ts diff --git a/test/cli/fixtures/location-filters/math:colon-dash.test.ts b/test/cli/fixtures/location-filters/math:colon-dash.test.ts deleted file mode 100644 index 4727c76eacc4..000000000000 --- a/test/cli/fixtures/location-filters/math:colon-dash.test.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { expect, it } from 'vitest' - -it('1 plus 1', () => { - expect(1 + 1).toBe(2) -}) - -it('2 plus 2', () => { - expect(2 + 2).toBe(4) -}) diff --git a/test/cli/test/location-filters.test.ts b/test/cli/test/location-filters.test.ts index 094e84a717a8..0873b3963cac 100644 --- a/test/cli/test/location-filters.test.ts +++ b/test/cli/test/location-filters.test.ts @@ -32,20 +32,6 @@ describe('location filter with list command', () => { expect(stderr).toEqual('') }) - test('handles file with a colon and dash in the name', async () => { - const { stdout, stderr } = await runVitestCli( - 'list', - `-r=${fixturePath}`, - `${fixturePath}/math:colon-dash.test.ts:3`, - ) - - expect(stdout).toMatchInlineSnapshot(` - "math:colon-dash.test.ts > 1 plus 1 - " - `) - expect(stderr).toEqual('') - }) - test('reports not found test', async () => { const { stdout, stderr } = await runVitestCli( 'list', @@ -88,6 +74,18 @@ describe('location filter with list command', () => { expect(stderr).toContain('RangeLocationFilterProvidedError') }) + test('parses file with a colon and dash in the name correctly', async () => { + const { stdout, stderr } = await runVitestCli( + 'list', + `-r=${fixturePath}`, + `${fixturePath}/:a/file/that/doesn-t/exit:10`, + ) + + expect(stdout).toEqual('') + // shouldn't get a range location error + expect(stderr).not.toContain('Error: Found "-"') + }) + test('erorrs if includeTaskLocation is not enabled', async () => { const { stdout, stderr } = await runVitestCli( 'list', @@ -141,18 +139,6 @@ describe('location filter with run command', () => { expect(stderr).toEqual('') }) - test('handles file with a colon and dash in the name', async () => { - const { stdout, stderr } = await runVitestCli( - 'run', - `-r=${fixturePath}`, - `${fixturePath}/math:colon-dash.test.ts:3`, - ) - - expect(stdout).contain('1 passed') - expect(stdout).contain('1 skipped') - expect(stderr).toEqual('') - }) - test('reports not found test', async () => { const { stdout, stderr } = await runVitestCli( 'run', @@ -187,6 +173,17 @@ describe('location filter with run command', () => { expect(stderr).toContain('Error: Found "-"') }) + test('parses file with a colon and dash in the name correctly', async () => { + const { stderr } = await runVitestCli( + 'run', + `-r=${fixturePath}`, + `${fixturePath}/:a/file/that/doesn-t/exit:10`, + ) + + // shouldn't get a range location error + expect(stderr).not.toContain('Error: Found "-"') + }) + test('errors if includeTaskLocation is not enabled', async () => { const { stderr } = await runVitestCli( 'run', From beed8082087c22b5e5e7d2d6947e909b4f660e18 Mon Sep 17 00:00:00 2001 From: mzhubail Date: Tue, 26 Nov 2024 15:52:34 +0300 Subject: [PATCH 27/28] Add doc --- docs/guide/filtering.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/docs/guide/filtering.md b/docs/guide/filtering.md index d2931fcc2adb..11feb0bc6c05 100644 --- a/docs/guide/filtering.md +++ b/docs/guide/filtering.md @@ -24,6 +24,22 @@ basic/foo.test.ts You can also use the `-t, --testNamePattern ` option to filter tests by full name. This can be helpful when you want to filter by the name defined within a file rather than the filename itself. +You can also specify the test by filename and line number: + +```bash +$ vitest basic/foo.test.ts:10 +``` + +::: warning +Note that you have to specify the full filename, and specify the exact line number, i.e. you can't do + +```bash +$ vitest foo:10 +$ vitest basic/foo.test.ts:10-25 +``` + +::: + ## Specifying a Timeout You can optionally pass a timeout in milliseconds as a third argument to tests. The default is [5 seconds](/config/#testtimeout). From a6cae3cfdd96f7697064a39a4307f1504958e5eb Mon Sep 17 00:00:00 2001 From: Vladimir Date: Sun, 1 Dec 2024 23:34:29 +0100 Subject: [PATCH 28/28] docs: add vitest 2.2 mention --- docs/guide/filtering.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/guide/filtering.md b/docs/guide/filtering.md index 11feb0bc6c05..5cdfef156d44 100644 --- a/docs/guide/filtering.md +++ b/docs/guide/filtering.md @@ -24,7 +24,7 @@ basic/foo.test.ts You can also use the `-t, --testNamePattern ` option to filter tests by full name. This can be helpful when you want to filter by the name defined within a file rather than the filename itself. -You can also specify the test by filename and line number: +Since Vitest 2.2, you can also specify the test by filename and line number: ```bash $ vitest basic/foo.test.ts:10 @@ -37,7 +37,6 @@ Note that you have to specify the full filename, and specify the exact line numb $ vitest foo:10 $ vitest basic/foo.test.ts:10-25 ``` - ::: ## Specifying a Timeout