チラシの裏

Vitestのrelated/JestのfindRelatedTestsを深ぼる

vitest relatedはVitestの命令の1つです。指定したファイルに関連するテストを自動で実行してくれます。Jestでも–findRelatedTestsというオプションで同様の動作ができます。

これは特定のファイルの依存を色々確認できるということです。どうやって依存を取得しているかを深ぼれば面白そうだったので、調べてみました。


とりあえず上記のサブコマンドのエントリーポイントまで愚直に見ていきます。

まずは package.jsonbin を見ると vitest.mjs となっています。

106 107 108
"bin": { "vitest": "./vitest.mjs" },

vitest.mjs はシンプルに ./dist/cli.js を読み込んでいます。

1 2
#!/usr/bin/env node import './dist/cli.js'

vitestの設定を見るとrollupを使ってビルドをしているので、rollup.config.jsを確認すると、おそらくsrc/node/cli.tsが実際の処理になっていそうです。

21
'cli': 'src/node/cli.ts',

で、最終的にsrc/node/cli/cac.tsに行き着きます。

1 2 3
import { createCLI } from './cli/cac' createCLI().parse()

cacはCLIアプリのフレームワーク的なものですね。

relatedのサブコマンドの定義は runRelated を呼び出す様になっており。

162 163 164
cli .command('related [...filters]', undefined, options) .action(runRelated)

runRelated は下記のような定義です。

229 230 231 232 233
async function runRelated(relatedFiles: string[] | string, argv: CliOptions): Promise<void> { argv.related = relatedFiles argv.passWithNoTests ??= true await start('test', [], argv) }

つまりは、 argv.related にファイル情報を入れている状態で、 start関数を呼び出している事がわかります。startも同ファイルに定義されており、src/node/cli/cli-api.tsstartVitest関数を呼び出す様になっています。

270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295
async function start(mode: VitestRunMode, cliFilters: string[], options: CliOptions): Promise<void> { try { process.title = 'node (vitest)' } catch {} try { const { startVitest } = await import('./cli-api') const ctx = await startVitest(mode, cliFilters.map(normalize), normalizeCliOptions(cliFilters, options)) if (!ctx.shouldKeepServer()) { await ctx.exit() } } catch (e) { const { divider } = await import('../reporters/renderers/utils') console.error(`\n${c.red(divider(c.bold(c.inverse(' Startup Error '))))}`) console.error(e) console.error('\n\n') if (process.exitCode == null) { process.exitCode = 1 } process.exit() } }


上記のファイルを追いかけると最終的に src/node/core.ts にたどり着きます。名前からしても重要そうですね。

start といういかにもなメソッドがあるためそれを読んでいきます。おそらく対象のファイルを取得する処理があります。this.specifications.getRelevantTestSpecificationsです。

543 544 545 546 547 548 549 550 551 552 553
async start(filters?: string[]): Promise<TestRunResult> { try { await this.initCoverageProvider() await this.coverageProvider?.clean(this.config.coverage.clean) } finally { await this.report('onInit', this) } this.filenamePattern = filters && filters?.length > 0 ? filters : undefined const files = await this.specifications.getRelevantTestSpecifications(filters)

該当の処理はfilterTestsBySourceを呼んでいるだけです。

35 36 37 38 39
public async getRelevantTestSpecifications(filters: string[] = []): Promise<TestSpecification[]> { return this.filterTestsBySource( await this.globTestSpecifications(filters), ) }

そちらの処理を見ると、config.relatedを利用している部分があります。

122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166
private async filterTestsBySource(specs: TestSpecification[]): Promise<TestSpecification[]> { if (this.vitest.config.changed && !this.vitest.config.related) { const { VitestGit } = await import('./git') const vitestGit = new VitestGit(this.vitest.config.root) const related = await vitestGit.findChangedFiles({ changedSince: this.vitest.config.changed, }) if (!related) { process.exitCode = 1 throw new GitNotFoundError() } this.vitest.config.related = Array.from(new Set(related)) } const related = this.vitest.config.related if (!related) { return specs } const forceRerunTriggers = this.vitest.config.forceRerunTriggers if (forceRerunTriggers.length && mm(related, forceRerunTriggers).length) { return specs } // don't run anything if no related sources are found // if we are in watch mode, we want to process all tests if (!this.vitest.config.watch && !related.length) { return [] } const testGraphs = await Promise.all( specs.map(async (spec) => { const deps = await this.getTestDependencies(spec) return [spec, deps] as const }), ) const runningTests: TestSpecification[] = [] for (const [specification, deps] of testGraphs) { // if deps or the test itself were changed if (related.some(path => path === specification.moduleId || deps.has(path))) { runningTests.push(specification) } }

下記の処理が関連ファイルかどうかを判定しているものっぽいですね。 getTestDependencies て該当のファイルの依存関係を取得し、その依存関係の中に関連ファイルのファイルがあるかどうかを判定している様です。

152 153 154 155 156 157 158 159 160 161 162 163 164 165 166
const testGraphs = await Promise.all( specs.map(async (spec) => { const deps = await this.getTestDependencies(spec) return [spec, deps] as const }), ) const runningTests: TestSpecification[] = [] for (const [specification, deps] of testGraphs) { // if deps or the test itself were changed if (related.some(path => path === specification.moduleId || deps.has(path))) { runningTests.push(specification) } }

getTestDependencies は下記のような処理です。

171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198
private async getTestDependencies(spec: TestSpecification, deps = new Set<string>()): Promise<Set<string>> { const addImports = async (project: TestProject, filepath: string) => { if (deps.has(filepath)) { return } deps.add(filepath) const mod = project.vite.moduleGraph.getModuleById(filepath) const transformed = mod?.ssrTransformResult || await project.vitenode.transformRequest(filepath) if (!transformed) { return } const dependencies = [...transformed.deps || [], ...transformed.dynamicDeps || []] await Promise.all(dependencies.map(async (dep) => { const fsPath = dep.startsWith('/@fs/') ? dep.slice(isWindows ? 5 : 4) : join(project.config.root, dep) if (!fsPath.includes('node_modules') && !deps.has(fsPath) && existsSync(fsPath)) { await addImports(project, fsPath) } })) } await addImports(spec.project, spec.moduleId) deps.delete(spec.moduleId) return deps }

project.vite.moduleGraph.getModuleByIdproject.vitenode.transformRequest というメソッドが気になりますね。これらのAPIを利用すれば依存関係がいい感じに取得できそうです。


まずは project.vite を見てみます。viteViteDevServer というvite側のインスタンスを持っているようです。Viteではおなじみの createDevServer で得られるインスタンスですね。Viteのドキュメントでも説明されています。

moduleGraphは下記のような説明となっています。

Module graph that tracks the import relationships, url to file mapping and hmr state.

インポート関係、urlとファイルのマッピング、hmrの状態を追跡するモジュールグラフ。

/** * Module graph that tracks the import relationships, url to file mapping * and hmr state. */ moduleGraph: ModuleGraph

まぁ名前のとおりではありますが、モジュールのグラフを追跡できるものですね。


transformRequestを確認してみます。

検索してみると下記にたどり着きます。

67
export function transformRequest(

コードを読んでみると最終的にはmoduleGraphが登場します。戻り値にdepsdynamicDepsがありますしね。


まとめると、viteの内部的には該当のファイル(モジュール)の依存を追うdepsなりdynamicDepsという物があり、それを利用して依存しているファイルを確認し自動でテストを実行しているようです。