Skip to content

Commit

Permalink
feat: add commands to run tests depending on changed files (#1078)
Browse files Browse the repository at this point in the history
  • Loading branch information
sheremet-va committed Apr 3, 2022
1 parent 885b25d commit 3c965d4
Show file tree
Hide file tree
Showing 7 changed files with 126 additions and 0 deletions.
9 changes: 9 additions & 0 deletions docs/config/index.md
Expand Up @@ -422,3 +422,12 @@ Format options for snapshot testing.
- **Default:** `test`

Overrides Vite mode.

### changed

- **Type**: `boolean | string`
- **Default**: false

Run tests only against changed files. If no value is provided, it will run tests against uncomitted changes (includes staged and unstaged).

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 change: 1 addition & 0 deletions docs/guide/index.md
Expand Up @@ -118,6 +118,7 @@ You can specify additional CLI options like `--port` or `--https`. For a full li
| `--environment <env>` | Runner environment (default: `node`) |
| `--passWithNoTests` | Pass when no tests found |
| `--allowOnly` | Allow tests and suites that are marked as `only` (default: false in CI, true otherwise) |
| `--changed [since]` | Run tests that are affected by the changed files (default: false)
| `-h, --help` | Display available CLI options |

## Examples
Expand Down
1 change: 1 addition & 0 deletions packages/vitest/src/node/cli.ts
Expand Up @@ -33,6 +33,7 @@ cli
.option('--environment <env>', 'runner environment (default: node)')
.option('--passWithNoTests', 'pass when no tests found')
.option('--allowOnly', 'Allow tests and suites that are marked as only (default: !process.env.CI)')
.option('--changed [since]', 'Run tests that are affected by the changed files (default: false)')
.help()

cli
Expand Down
3 changes: 3 additions & 0 deletions packages/vitest/src/node/config.ts
Expand Up @@ -134,5 +134,8 @@ export function resolveConfig(
if (!resolved.reporters.length)
resolved.reporters.push('default')

if (resolved.changed)
resolved.passWithNoTests ??= true

return resolved
}
13 changes: 13 additions & 0 deletions packages/vitest/src/node/core.ts
Expand Up @@ -15,6 +15,7 @@ import type { WorkerPool } from './pool'
import { StateManager } from './state'
import { resolveConfig } from './config'
import { printError } from './error'
import { VitestGit } from './git'

const WATCHER_DEBOUNCE = 100
const CLOSE_TIMEOUT = 1_000
Expand Down Expand Up @@ -163,6 +164,18 @@ export class Vitest {
}

async filterTestsBySource(tests: string[]) {
if (this.config.changed && !this.config.related) {
const vitestGit = new VitestGit(this.config.root)
const related = await vitestGit.findChangedFiles({
changedSince: this.config.changed,
})
if (!related) {
this.error(c.red('Could not find Git root. Have you initialized git with `git init`?\n'))
process.exit(1)
}
this.config.related = Array.from(new Set(related))
}

const related = this.config.related
if (!related)
return tests
Expand Down
92 changes: 92 additions & 0 deletions packages/vitest/src/node/git.ts
@@ -0,0 +1,92 @@
import { resolve } from 'pathe'
import { execa } from 'execa'
import type { ExecaReturnValue } from 'execa'

export interface GitOptions {
changedSince?: string | boolean
}

export class VitestGit {
private root!: string

constructor(private cwd: string) {}

private async resolveFilesWithGitCommand(
args: string[],
): Promise<string[]> {
let result: ExecaReturnValue

try {
result = await execa('git', args, { cwd: this.root })
}
catch (e: any) {
e.message = e.stderr

throw e
}

return result.stdout
.split('\n')
.filter(s => s !== '')
.map(changedPath => resolve(this.root, changedPath))
}

async findChangedFiles(options: GitOptions) {
const root = await this.getRoot(this.cwd)
if (!root)
return null

this.root = root

const changedSince = options.changedSince
if (typeof changedSince === 'string') {
const [committed, staged, unstaged] = await Promise.all([
this.getFilesSince(changedSince),
this.getStagedFiles(),
this.getUnstagedFiles(),
])
return [...committed, ...staged, ...unstaged]
}
const [staged, unstaged] = await Promise.all([
this.getStagedFiles(),
this.getUnstagedFiles(),
])
return [...staged, ...unstaged]
}

private getFilesSince(hash: string) {
return this.resolveFilesWithGitCommand(
['diff', '--name-only', `${hash}...HEAD`],
)
}

private getStagedFiles() {
return this.resolveFilesWithGitCommand(
['diff', '--cached', '--name-only'],
)
}

private getUnstagedFiles() {
return this.resolveFilesWithGitCommand(
[
'ls-files',
'--other',
'--modified',
'--exclude-standard',
],
)
}

async getRoot(cwd: string) {
const options = ['rev-parse', '--show-cdup']

try {
const result = await execa('git', options, { cwd })

return resolve(cwd, result.stdout)
}
catch {
return null
}
}
}
7 changes: 7 additions & 0 deletions packages/vitest/src/types/config.ts
Expand Up @@ -329,6 +329,13 @@ export interface UserConfig extends InlineConfig {
* @default 'test'
*/
mode?: string

/**
* Runs tests that are affected by the changes in the repository, or between specified branch or commit hash
* Requires initialized git repository
* @default false
*/
changed?: boolean | string
}

export interface ResolvedConfig extends Omit<Required<UserConfig>, 'config' | 'filters' | 'coverage' | 'testNamePattern' | 'related' | 'api' | 'reporters'> {
Expand Down

0 comments on commit 3c965d4

Please sign in to comment.