OXCなんかで、TypeScriptのテストケースを参照して、カバレッジを取ってることは知ってた。
けど、
./tests/baselines/reference/
にある.errors.txt
はどう読み解くのが正解なのか?- TSC自体はどのAPIを使ってテストしているのか?
みたいなことが、なんとな〜くしかわかってなかった。
今回はそれを明らかにしたい。
./tests/baselines/reference
配下においてあるものをベースラインと呼んでる。
https://github.com/microsoft/TypeScript/blob/main/CONTRIBUTING.md#managing-the-baselines
テストの足回り
- TypeScriptのリポジトリで
npm run
すると、npm run test
が存在することがわかる npm run test
は、hereby runtests-parallel
を実行するdoRunTestsParallel()
という別のタスクが呼ばれてる- これは、依存してるタスクを解決してから、
runConsoleTests(testRunner, "min", runInParallel)
を呼ぶ - 依存タスクとは、
tests()
というタスクとgenerateLibs()
というタスク tests()
タスクは、テストランナーをビルドするもの./src/testRunner
配下にあるのがその一式で、ビルドされるとrun.js
というファイルになるらしい- その
run.js
を使って、テストコマンドがchild_process.spawn()
されてく
実際にはもう少し色々な準備があるけど、だいたいこんな感じか。
./src/testRunner/runner.ts
で呼んでるstartTestEnvironment()
がエントリーポイントらしい。
startTestEnvironment()
- https://github.com/microsoft/TypeScript/blob/dd1e258ba56f1b511879372c857fb625de3dec4a/src/testRunner/runner.ts#L277
- テスト実行を並列化するためのあれこれはさておき
- ここにある
beginTests()
が呼ばれる
beginTest()
は、runTests(runners)
が実体runners
は、事前にテストの設定から導出されるもので、RunnerBase
クラスの配列になってるRunnerBase
は、./src/harness/runnerbase.ts
で定義されてる- コンパイラのテストとして、
RunnerBase
をextend
してるCompilerBaselineRunner
というやつが、2種類用意されるrunners.push(new CompilerBaselineRunner(CompilerTestType.Conformance))
runners.push(new CompilerBaselineRunner(CompilerTestType.Regressions))
- https://github.com/microsoft/TypeScript/blob/dd1e258ba56f1b511879372c857fb625de3dec4a/src/testRunner/runner.ts#L210-L212
runTests(runners)
でそれぞれを実行していく- まずは
runner.enumerateTestFiles()
でテストケースの列挙- テストケース名、衝突しそうやなって思ってたけど、ここで重複チェックをやってた
- それぞれ
runner.initializeTests()
していく
つぎはCompilerBaselineRunner
の詳細へ。
CompilerBaselineRunner
- コンパイラー関連では、2種類用意されてた
CompilerTestType.Conformance
はtests/cases/conformance
に対応CompilerTestType.Regressions
はtests/cases/compiler
に対応- https://github.com/microsoft/TypeScript/blob/dd1e258ba56f1b511879372c857fb625de3dec4a/src/testRunner/compilerRunner.ts#L34
enumerateTestFiles()
- それぞれのディレクトリから、
.tsx?
ファイルを取ってきてリストを返すだけ
- それぞれのディレクトリから、
initializeTests()
- テストファイルそれぞれに対して、
checkTestCodeOutput(vpath.normalizeSeparators(file), CompilerTest.getConfigurations(file))
- テストファイルそれぞれに対して、
CompilerTest.getConfigurations(file)
- https://github.com/microsoft/TypeScript/blob/dd1e258ba56f1b511879372c857fb625de3dec4a/src/testRunner/compilerRunner.ts#L261
- どういうファイル(ソースコード)に対して、どういう設定でコンパイルするかを調べる
const settings = TestCaseParser.extractCompilerSettings(content)
- https://github.com/microsoft/TypeScript/blob/dd1e258ba56f1b511879372c857fb625de3dec4a/src/harness/harnessIO.ts#L1288
- テストファイルそれぞれを解析して、コンパイラの設定を抽出する
- 各テストファイルは行頭に
// @lib: esnext
や// @filename foo.ts
みたいなコメントが書いてある
const configurations = getFileBasedTestConfigurations(settings, CompilerTest.varyBy)
- https://github.com/microsoft/TypeScript/blob/dd1e258ba56f1b511879372c857fb625de3dec4a/src/harness/harnessIO.ts#L1221
- テストのコメントは、基本的には
@key: value
だが、たまに@key: value1,value2
みたいなのがあり、これがバリエーション - https://github.com/microsoft/TypeScript/blob/dd1e258ba56f1b511879372c857fb625de3dec4a/tests/cases/compiler/exportEmptyObjectBindingPattern.ts
- ソース文字列と設定がわかったので、
checkTestCodeOutput()
- バリエーションに応じて、または単発の
runSuite(fileName, test)
- これ自体が
mocha
のdescribe()
内で呼ばれる
- バリエーションに応じて、または単発の
runSuite(fileName, test, configuration?)
- https://github.com/microsoft/TypeScript/blob/dd1e258ba56f1b511879372c857fb625de3dec4a/src/testRunner/compilerRunner.ts#L88
payload = TestCaseParser.makeUnitsFromTest(test.content, test.file)
して- それを
new CompilerTest(fileName, payload, configuration)
CompilerTest
クラスの様々なメソッドでもって、mocha
のit()
を呼んでいく
makeUnitsFromTest()
- https://github.com/microsoft/TypeScript/blob/dd1e258ba56f1b511879372c857fb625de3dec4a/src/harness/harnessIO.ts#L1308
@filename
コメントがあったら分割してモジュールっぽく扱うFilename
とかfileName
とか、表記揺れしてるのがずっと気になってたけど、ここでtoLowerCase()
されてた- 愚直にファイルを改行で割って、1行ずつ精査していってた・・・
- シンボリックリンクまで処理してるけどそんなことあるんや
runSuite()
でテストしてることは6種類- エラー報告:
compilerTest.verifyDiagnostics()
- モジュール解決:
compilerTest.verifyModuleResolution()
- ソースマップの中身:
compilerTest.verifySourceMapRecord()
- JSへの変換結果:
compilerTest.verifyJavaScriptOutput()
- ソースマップ:
compilerTest.verifySourceMapOutput()
- 型とシンボル:
compilerTest.verifyTypesAndSymbols()
- エラー報告:
このverifyDiagnostics()
が一番知りたかったやつ。CompilerTest
をみてく。
CompilerTest
- https://github.com/microsoft/TypeScript/blob/dd1e258ba56f1b511879372c857fb625de3dec4a/src/testRunner/compilerRunner.ts#L128
constructor()
の時点で、this.result = Compiler.compileFiles()
してて、それを後から参照してるverifyDiagnostics()
も、this.result.diagnostics
を渡して、Compiler.doErrorBaseline()
というものでチェックしてるだけ
Compiler
はただのnamespace
だった。
Compiler#compileFiles()
- https://github.com/microsoft/TypeScript/blob/dd1e258ba56f1b511879372c857fb625de3dec4a/src/harness/harnessIO.ts#L377
- いろいろやってるが、
compiler.compileFiles()
が実体っぽい compiler.compileFiles()
- https://github.com/microsoft/TypeScript/blob/dd1e258ba56f1b511879372c857fb625de3dec4a/src/harness/compilerImpl.ts#L244
createProgram()
とか、getPreEmitDiagnostics()
とか、見慣れたAPIがいろいろ使われてる
- コンパイル結果は、
CompilationResult
クラスにして返す- https://github.com/microsoft/TypeScript/blob/dd1e258ba56f1b511879372c857fb625de3dec4a/src/harness/compilerImpl.ts#L55
getPreEmitDiagnostics()
で取得したdiagnostics: Diagnostic[]
が渡されてる
getPreEmitDiagnostics()
には、さまざまなエラーが含まれてるgetSyntacticDiagnostics()
とgetSemanticDiagnostics()
ももちろん入ってる
Compiler#doErrorBaseline()
CompilationResult
に渡しておいたdiagnostics
を渡して実行する- https://github.com/microsoft/TypeScript/blob/dd1e258ba56f1b511879372c857fb625de3dec4a/src/harness/harnessIO.ts#L715
Baseline.runBaseline(baselinePath.replace(/\.tsx?$/, ".errors.txt"), !errors || (errors.length === 0) ? null : getErrorBaseline(inputFiles, errors, pretty))
- 第1引数のパスはテストケース名なので、拡張子を
.errors.txt
に変えて、./tests/baselines/reference
にあるスナップショットへのパス - 第2引数は、取得した
Diagnostic
を文字列に整形したもの- https://github.com/microsoft/TypeScript/blob/dd1e258ba56f1b511879372c857fb625de3dec4a/src/harness/harnessIO.ts#L540
- かなり頑張って整形してる・・・
- https://github.com/microsoft/TypeScript/blob/dd1e258ba56f1b511879372c857fb625de3dec4a/src/harness/harnessIO.ts#L578
- まずサマリがあり、そのあとにファイルごとの詳細が並ぶ
"!!! " + ts.diagnosticCategoryName(error) + " TS" + error.code + ": " + s
みたいなテンプレの産地はここ- こうしてできあがったのが、あの
.errors.txt
というわけ
このBaseline
もただのnamespace
だった。
Baseline#runBaseline()
- https://github.com/microsoft/TypeScript/blob/dd1e258ba56f1b511879372c857fb625de3dec4a/src/harness/harnessIO.ts#L1559
- ついさっき取得した
Diagnostic[]
を文字列化したものをactualとして、expectと比較を行い、その結果を記録する compareToBaseline()
- https://github.com/microsoft/TypeScript/blob/dd1e258ba56f1b511879372c857fb625de3dec4a/src/harness/harnessIO.ts#L1482
- compareと言ってるものの、特に比較はしていなそう
- 意味深なTODOコメントが残ってたけど、issueはclosedだった: https://github.com/microsoft/TypeScript/issues/18217
readFile()
してexpectな文字列を取得してるのみ
writeComparison()
- https://github.com/microsoft/TypeScript/blob/dd1e258ba56f1b511879372c857fb625de3dec4a/src/harness/harnessIO.ts#L1505
- 実際に比較して、差分があったら
throw
してるのはこっち - はじめての場合は
<no content>
というプレースホルダーになってる
スナップショットと差分があったらthrow
するというだけ。
まとめ
- TSCのテストは、
src/testRunner
とsrc/harness
でがっつり実装されてる - 内部的には
mocha
を使ってがんばってる - コンパイラーに関しては2パターンをテスト
tests/cases/compiler
: Regressionstests/cases/conformance
: Conformance
- 内容は、
tests/baselines/reference
にある各種ファイルとのスナップショットテスト.errors.txt
だけでなく、.symbols
や.types
のほか、emitされた.js
など
.errors.txt
は、Diagnostic[]
を文字列化したもの- テストケースは
@filename
コメントで複数ファイルから構成されることもある - その場合も全ファイル分をまとめたスナップショットになる
- どのファイルでどのエラーが出たか、ちゃんとわかるようになってる
- テストケースは
だいたい予想してた通りでよかった。