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みたいなのがあり、これがバリエーション - バリエーションは複数の
@keyから構成される可能性があり、倍々で増える - 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コメントで複数ファイルから構成されることもある - その場合も全ファイル分をまとめたスナップショットになる
- どのファイルでどのエラーが出たか、ちゃんとわかるようになってる
- テストケースは
だいたい予想してた通りでよかった。
TSCのためのスナップショットというところの理解度がふわっとしてたけど、そこが解消されてよかった。
@filenameでは.ts(x)だけが書かれてるわけではなくて、
package.jsonREADME.md.css.js.map.d.ts,.d.mts- etc…
などなどほんとうにいろんな種類のファイルが配置されてる。
ファイルパスもWindowsみたいなのも混じってるし、壊れたファイルも、拡張子のないファイルまでもある。
個人的には、TS(X)パーサーのためのテストケース集として使いたいなら、事前に精査が必要だという結論。