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

Configure Feed

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

feat: add commands to run tests depending on changed files (#1078)

authored by

Vladimir and committed by
GitHub
(Apr 3, 2022, 4:09 PM +0800) 3c965d4e 885b25d4

+126
+9
docs/config/index.md
··· 422 422 - **Default:** `test` 423 423 424 424 Overrides Vite mode. 425 + 426 + ### changed 427 + 428 + - **Type**: `boolean | string` 429 + - **Default**: false 430 + 431 + Run tests only against changed files. If no value is provided, it will run tests against uncomitted changes (includes staged and unstaged). 432 + 433 + To run tests against changes made in last commit, you can use `--changed HEAD~1`. You can also pass commit hash or branch name.
+1
docs/guide/index.md
··· 118 118 | `--environment <env>` | Runner environment (default: `node`) | 119 119 | `--passWithNoTests` | Pass when no tests found | 120 120 | `--allowOnly` | Allow tests and suites that are marked as `only` (default: false in CI, true otherwise) | 121 + | `--changed [since]` | Run tests that are affected by the changed files (default: false) 121 122 | `-h, --help` | Display available CLI options | 122 123 123 124 ## Examples
+1
packages/vitest/src/node/cli.ts
··· 33 33 .option('--environment <env>', 'runner environment (default: node)') 34 34 .option('--passWithNoTests', 'pass when no tests found') 35 35 .option('--allowOnly', 'Allow tests and suites that are marked as only (default: !process.env.CI)') 36 + .option('--changed [since]', 'Run tests that are affected by the changed files (default: false)') 36 37 .help() 37 38 38 39 cli
+3
packages/vitest/src/node/config.ts
··· 134 134 if (!resolved.reporters.length) 135 135 resolved.reporters.push('default') 136 136 137 + if (resolved.changed) 138 + resolved.passWithNoTests ??= true 139 + 137 140 return resolved 138 141 }
+13
packages/vitest/src/node/core.ts
··· 15 15 import { StateManager } from './state' 16 16 import { resolveConfig } from './config' 17 17 import { printError } from './error' 18 + import { VitestGit } from './git' 18 19 19 20 const WATCHER_DEBOUNCE = 100 20 21 const CLOSE_TIMEOUT = 1_000 ··· 163 164 } 164 165 165 166 async filterTestsBySource(tests: string[]) { 167 + if (this.config.changed && !this.config.related) { 168 + const vitestGit = new VitestGit(this.config.root) 169 + const related = await vitestGit.findChangedFiles({ 170 + changedSince: this.config.changed, 171 + }) 172 + if (!related) { 173 + this.error(c.red('Could not find Git root. Have you initialized git with `git init`?\n')) 174 + process.exit(1) 175 + } 176 + this.config.related = Array.from(new Set(related)) 177 + } 178 + 166 179 const related = this.config.related 167 180 if (!related) 168 181 return tests
+92
packages/vitest/src/node/git.ts
··· 1 + import { resolve } from 'pathe' 2 + import { execa } from 'execa' 3 + import type { ExecaReturnValue } from 'execa' 4 + 5 + export interface GitOptions { 6 + changedSince?: string | boolean 7 + } 8 + 9 + export class VitestGit { 10 + private root!: string 11 + 12 + constructor(private cwd: string) {} 13 + 14 + private async resolveFilesWithGitCommand( 15 + args: string[], 16 + ): Promise<string[]> { 17 + let result: ExecaReturnValue 18 + 19 + try { 20 + result = await execa('git', args, { cwd: this.root }) 21 + } 22 + catch (e: any) { 23 + e.message = e.stderr 24 + 25 + throw e 26 + } 27 + 28 + return result.stdout 29 + .split('\n') 30 + .filter(s => s !== '') 31 + .map(changedPath => resolve(this.root, changedPath)) 32 + } 33 + 34 + async findChangedFiles(options: GitOptions) { 35 + const root = await this.getRoot(this.cwd) 36 + if (!root) 37 + return null 38 + 39 + this.root = root 40 + 41 + const changedSince = options.changedSince 42 + if (typeof changedSince === 'string') { 43 + const [committed, staged, unstaged] = await Promise.all([ 44 + this.getFilesSince(changedSince), 45 + this.getStagedFiles(), 46 + this.getUnstagedFiles(), 47 + ]) 48 + return [...committed, ...staged, ...unstaged] 49 + } 50 + const [staged, unstaged] = await Promise.all([ 51 + this.getStagedFiles(), 52 + this.getUnstagedFiles(), 53 + ]) 54 + return [...staged, ...unstaged] 55 + } 56 + 57 + private getFilesSince(hash: string) { 58 + return this.resolveFilesWithGitCommand( 59 + ['diff', '--name-only', `${hash}...HEAD`], 60 + ) 61 + } 62 + 63 + private getStagedFiles() { 64 + return this.resolveFilesWithGitCommand( 65 + ['diff', '--cached', '--name-only'], 66 + ) 67 + } 68 + 69 + private getUnstagedFiles() { 70 + return this.resolveFilesWithGitCommand( 71 + [ 72 + 'ls-files', 73 + '--other', 74 + '--modified', 75 + '--exclude-standard', 76 + ], 77 + ) 78 + } 79 + 80 + async getRoot(cwd: string) { 81 + const options = ['rev-parse', '--show-cdup'] 82 + 83 + try { 84 + const result = await execa('git', options, { cwd }) 85 + 86 + return resolve(cwd, result.stdout) 87 + } 88 + catch { 89 + return null 90 + } 91 + } 92 + }
+7
packages/vitest/src/types/config.ts
··· 329 329 * @default 'test' 330 330 */ 331 331 mode?: string 332 + 333 + /** 334 + * Runs tests that are affected by the changes in the repository, or between specified branch or commit hash 335 + * Requires initialized git repository 336 + * @default false 337 + */ 338 + changed?: boolean | string 332 339 } 333 340 334 341 export interface ResolvedConfig extends Omit<Required<UserConfig>, 'config' | 'filters' | 'coverage' | 'testNamePattern' | 'related' | 'api' | 'reporters'> {