package harnessutil import ( "context" "fmt" "io" "io/fs" "maps" "os" "path/filepath" "regexp" "slices" "strconv" "strings" "sync" "testing" "testing/fstest" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/ast" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/bundled" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/collections" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/compiler" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/core" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/outputpaths" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/parser" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/repo" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/sourcemap" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/testutil" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/tsoptions" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/tspath" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/vfs" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/vfs/vfstest" ) // Posix-style path to additional test libraries const testLibFolder = "/.lib" const FakeTSVersion = "FakeTSVersion" type TestFile struct { UnitName string Content string } // This maps a compiler setting to its string value, after splitting by commas, // handling inclusions and exclusions, and deduplicating. // For example, if a test file contains: // // // @target: esnext, es2015 // // Then the map will map "target" to "esnext", and another map will map "target" to "es2015". type TestConfiguration = map[string]string type NamedTestConfiguration struct { Name string Config TestConfiguration } type HarnessOptions struct { UseCaseSensitiveFileNames bool BaselineFile string IncludeBuiltFile string FileName string LibFiles []string NoImplicitReferences bool CurrentDirectory string Symlink string Link string NoTypesAndSymbols bool FullEmitPaths bool ReportDiagnostics bool CaptureSuggestions bool TypescriptVersion string } func CompileFiles( t *testing.T, inputFiles []*TestFile, otherFiles []*TestFile, testConfig TestConfiguration, tsconfig *tsoptions.ParsedCommandLine, currentDirectory string, symlinks map[string]string, ) *CompilationResult { var compilerOptions *core.CompilerOptions if tsconfig != nil { compilerOptions = tsconfig.ParsedConfig.CompilerOptions.Clone() } if compilerOptions == nil { compilerOptions = &core.CompilerOptions{} } // Set default options for tests if compilerOptions.NewLine == core.NewLineKindNone { compilerOptions.NewLine = core.NewLineKindCRLF } if compilerOptions.SkipDefaultLibCheck == core.TSUnknown { compilerOptions.SkipDefaultLibCheck = core.TSTrue } compilerOptions.NoErrorTruncation = core.TSTrue harnessOptions := HarnessOptions{UseCaseSensitiveFileNames: true, CurrentDirectory: currentDirectory} // Parse harness and compiler options from the test configuration if testConfig != nil { setOptionsFromTestConfig(t, testConfig, compilerOptions, &harnessOptions, currentDirectory) } return CompileFilesEx(t, inputFiles, otherFiles, &harnessOptions, compilerOptions, currentDirectory, symlinks, tsconfig) } func CompileFilesEx( t *testing.T, inputFiles []*TestFile, otherFiles []*TestFile, harnessOptions *HarnessOptions, compilerOptions *core.CompilerOptions, currentDirectory string, symlinks map[string]string, tsconfig *tsoptions.ParsedCommandLine, ) *CompilationResult { var programFileNames []string for _, file := range inputFiles { fileName := tspath.GetNormalizedAbsolutePath(file.UnitName, currentDirectory) if !tspath.FileExtensionIs(fileName, tspath.ExtensionJson) { programFileNames = append(programFileNames, fileName) } } // !!! Note: lib files are not going to be in `built/local`. // In addition, not all files that used to be in `built/local` are going to exist. // Files from built\local that are requested by test "@includeBuiltFiles" to be in the context. // Treat them as library files, so include them in build, but not in baselines. // if harnessOptions.includeBuiltFile != "" { // programFileNames = append(programFileNames, tspath.CombinePaths(builtFolder, harnessOptions.includeBuiltFile)) // } // Performance optimization; avoid copying in the /.lib folder if the test doesn't need it. includeLibDir := core.Some(inputFiles, func(file *TestFile) bool { return strings.Contains(file.Content, testLibFolder+"/") }) // Files from testdata\lib that are requested by "@libFiles" if len(harnessOptions.LibFiles) > 0 { for _, libFile := range harnessOptions.LibFiles { if libFile == "lib.d.ts" && compilerOptions.NoLib != core.TSTrue { // We used to override lib with a custom lib.d.ts for some reason. Skip this unless it becomes necessary. continue } programFileNames = append(programFileNames, tspath.CombinePaths(testLibFolder, libFile)) includeLibDir = true } } if includeLibDir { repo.SkipIfNoTypeScriptSubmodule(t) } // !!! // ts.assign(options, ts.convertToOptionsWithAbsolutePaths(options, path => ts.getNormalizedAbsolutePath(path, currentDirectory))); if compilerOptions.OutDir != "" { compilerOptions.OutDir = tspath.GetNormalizedAbsolutePath(compilerOptions.OutDir, currentDirectory) } if compilerOptions.Project != "" { compilerOptions.Project = tspath.GetNormalizedAbsolutePath(compilerOptions.Project, currentDirectory) } if compilerOptions.RootDir != "" { compilerOptions.RootDir = tspath.GetNormalizedAbsolutePath(compilerOptions.RootDir, currentDirectory) } if compilerOptions.TsBuildInfoFile != "" { compilerOptions.TsBuildInfoFile = tspath.GetNormalizedAbsolutePath(compilerOptions.TsBuildInfoFile, currentDirectory) } if compilerOptions.BaseUrl != "" { compilerOptions.BaseUrl = tspath.GetNormalizedAbsolutePath(compilerOptions.BaseUrl, currentDirectory) } if compilerOptions.DeclarationDir != "" { compilerOptions.DeclarationDir = tspath.GetNormalizedAbsolutePath(compilerOptions.DeclarationDir, currentDirectory) } for i, rootDir := range compilerOptions.RootDirs { compilerOptions.RootDirs[i] = tspath.GetNormalizedAbsolutePath(rootDir, currentDirectory) } for i, typeRoot := range compilerOptions.TypeRoots { compilerOptions.TypeRoots[i] = tspath.GetNormalizedAbsolutePath(typeRoot, currentDirectory) } // Create fake FS for testing testfs := map[string]any{} for _, file := range inputFiles { fileName := tspath.GetNormalizedAbsolutePath(file.UnitName, currentDirectory) testfs[fileName] = &fstest.MapFile{ Data: []byte(file.Content), } } for _, file := range otherFiles { fileName := tspath.GetNormalizedAbsolutePath(file.UnitName, currentDirectory) testfs[fileName] = &fstest.MapFile{ Data: []byte(file.Content), } } for src, target := range symlinks { srcFileName := tspath.GetNormalizedAbsolutePath(src, currentDirectory) targetFileName := tspath.GetNormalizedAbsolutePath(target, currentDirectory) testfs[srcFileName] = vfstest.Symlink(targetFileName) } if includeLibDir { maps.Copy(testfs, testLibFolderMap()) } fs := vfstest.FromMap(testfs, harnessOptions.UseCaseSensitiveFileNames) fs = bundled.WrapFS(fs) fs = NewOutputRecorderFS(fs) host := createCompilerHost(fs, bundled.LibPath(), currentDirectory) var configFile *tsoptions.TsConfigSourceFile var errors []*ast.Diagnostic if tsconfig != nil { configFile = tsconfig.ConfigFile errors = tsconfig.Errors } result := compileFilesWithHost(host, &tsoptions.ParsedCommandLine{ ParsedConfig: &core.ParsedOptions{ CompilerOptions: compilerOptions, FileNames: programFileNames, }, ConfigFile: configFile, Errors: errors, }, harnessOptions) result.Symlinks = symlinks result.Trace = host.tracer.String() result.Repeat = func(testConfig TestConfiguration) *CompilationResult { newHarnessOptions := *harnessOptions newCompilerOptions := compilerOptions.Clone() setOptionsFromTestConfig(t, testConfig, newCompilerOptions, &newHarnessOptions, currentDirectory) return CompileFilesEx(t, inputFiles, otherFiles, &newHarnessOptions, newCompilerOptions, currentDirectory, symlinks, tsconfig) } return result } var testLibFolderMap = sync.OnceValue(func() map[string]any { testfs := make(map[string]any) libfs := os.DirFS(filepath.Join(repo.TypeScriptSubmodulePath, "tests", "lib")) err := fs.WalkDir(libfs, ".", func(path string, d fs.DirEntry, err error) error { if err != nil { return err } if d.IsDir() { return nil } content, err := fs.ReadFile(libfs, path) if err != nil { return err } testfs[testLibFolder+"/"+path] = &fstest.MapFile{ Data: content, } return nil }) if err != nil { panic(fmt.Sprintf("Failed to read lib dir: %v", err)) } return testfs }) func SetCompilerOptionsFromTestConfig(t *testing.T, testConfig TestConfiguration, compilerOptions *core.CompilerOptions, currentDirectory string) { for name, value := range testConfig { if name == "typescriptversion" { continue } commandLineOption := getCommandLineOption(name) if commandLineOption != nil { parsedValue := getOptionValue(t, commandLineOption, value, currentDirectory) errors := tsoptions.ParseCompilerOptions(commandLineOption.Name, parsedValue, compilerOptions) if len(errors) > 0 { t.Fatalf("Error parsing value '%s' for compiler option '%s'.", value, commandLineOption.Name) } } } } func setOptionsFromTestConfig(t *testing.T, testConfig TestConfiguration, compilerOptions *core.CompilerOptions, harnessOptions *HarnessOptions, currentDirectory string) { for name, value := range testConfig { if name == "typescriptversion" { continue } commandLineOption := getCommandLineOption(name) if commandLineOption != nil { parsedValue := getOptionValue(t, commandLineOption, value, currentDirectory) errors := tsoptions.ParseCompilerOptions(commandLineOption.Name, parsedValue, compilerOptions) if len(errors) > 0 { t.Fatalf("Error parsing value '%s' for compiler option '%s'.", value, commandLineOption.Name) } continue } harnessOption := getHarnessOption(name) if harnessOption != nil { parsedValue := getOptionValue(t, harnessOption, value, currentDirectory) parseHarnessOption(t, harnessOption.Name, parsedValue, harnessOptions) continue } t.Fatalf("Unknown compiler option '%s'.", name) } } var compilerOptions = core.Concatenate( tsoptions.OptionsDeclarations, []*tsoptions.CommandLineOption{ { Name: "allowNonTsExtensions", Kind: tsoptions.CommandLineOptionTypeBoolean, }, { Name: "noErrorTruncation", Kind: tsoptions.CommandLineOptionTypeBoolean, }, { Name: "suppressOutputPathCheck", Kind: tsoptions.CommandLineOptionTypeBoolean, }, { Name: "noCheck", Kind: tsoptions.CommandLineOptionTypeBoolean, }, }, ) var harnessCommandLineOptions = []*tsoptions.CommandLineOption{ { Name: "useCaseSensitiveFileNames", Kind: tsoptions.CommandLineOptionTypeBoolean, }, { Name: "baselineFile", Kind: tsoptions.CommandLineOptionTypeString, }, { Name: "includeBuiltFile", Kind: tsoptions.CommandLineOptionTypeString, }, { Name: "fileName", Kind: tsoptions.CommandLineOptionTypeString, }, { Name: "libFiles", Kind: tsoptions.CommandLineOptionTypeList, }, { Name: "noImplicitReferences", Kind: tsoptions.CommandLineOptionTypeBoolean, }, { Name: "currentDirectory", Kind: tsoptions.CommandLineOptionTypeString, }, { Name: "symlink", Kind: tsoptions.CommandLineOptionTypeString, }, { Name: "link", Kind: tsoptions.CommandLineOptionTypeString, }, { Name: "noTypesAndSymbols", Kind: tsoptions.CommandLineOptionTypeBoolean, }, // Emitted js baseline will print full paths for every output file { Name: "fullEmitPaths", Kind: tsoptions.CommandLineOptionTypeBoolean, }, // used to enable error collection in `transpile` baselines { Name: "reportDiagnostics", Kind: tsoptions.CommandLineOptionTypeBoolean, }, // Adds suggestion diagnostics to error baselines { Name: "captureSuggestions", Kind: tsoptions.CommandLineOptionTypeBoolean, }, } func getHarnessOption(name string) *tsoptions.CommandLineOption { return core.Find(harnessCommandLineOptions, func(option *tsoptions.CommandLineOption) bool { return strings.EqualFold(option.Name, name) }) } func parseHarnessOption(t *testing.T, key string, value any, harnessOptions *HarnessOptions) { switch key { case "useCaseSensitiveFileNames": harnessOptions.UseCaseSensitiveFileNames = value.(bool) case "baselineFile": harnessOptions.BaselineFile = value.(string) case "includeBuiltFile": harnessOptions.IncludeBuiltFile = value.(string) case "fileName": harnessOptions.FileName = value.(string) case "libFiles": harnessOptions.LibFiles = make([]string, 0, len(value.([]any))) for _, v := range value.([]any) { harnessOptions.LibFiles = append(harnessOptions.LibFiles, v.(string)) } case "noImplicitReferences": harnessOptions.NoImplicitReferences = value.(bool) case "currentDirectory": harnessOptions.CurrentDirectory = value.(string) case "symlink": harnessOptions.Symlink = value.(string) case "link": harnessOptions.Link = value.(string) case "noTypesAndSymbols": harnessOptions.NoTypesAndSymbols = value.(bool) case "fullEmitPaths": harnessOptions.FullEmitPaths = value.(bool) case "reportDiagnostics": harnessOptions.ReportDiagnostics = value.(bool) case "captureSuggestions": harnessOptions.CaptureSuggestions = value.(bool) case "typescriptVersion": harnessOptions.TypescriptVersion = value.(string) default: t.Fatalf("Unknown harness option '%s'.", key) } } var deprecatedModuleResolution []string = []string{"node", "classic", "node10"} func getOptionValue(t *testing.T, option *tsoptions.CommandLineOption, value string, cwd string) tsoptions.CompilerOptionsValue { switch option.Kind { case tsoptions.CommandLineOptionTypeString: if option.IsFilePath { return tspath.GetNormalizedAbsolutePath(value, cwd) } return value case tsoptions.CommandLineOptionTypeNumber: numVal, err := strconv.Atoi(value) if err != nil { t.Fatalf("Value for option '%s' must be a number, got: %v", option.Name, value) } return numVal case tsoptions.CommandLineOptionTypeBoolean: switch strings.ToLower(value) { case "true": return true case "false": return false default: t.Fatalf("Value for option '%s' must be a boolean, got: %v", option.Name, value) } case tsoptions.CommandLineOptionTypeEnum: enumVal, ok := option.EnumMap().Get(strings.ToLower(value)) if !ok { t.Fatalf("Value for option '%s' must be one of %s, got: %v", option.Name, strings.Join(slices.Collect(option.EnumMap().Keys()), ","), value) } return enumVal case tsoptions.CommandLineOptionTypeList, tsoptions.CommandLineOptionTypeListOrElement: listVal, errors := tsoptions.ParseListTypeOption(option, value) if option.Elements().IsFilePath { return core.Map(listVal, func(item any) any { return tspath.GetNormalizedAbsolutePath(item.(string), cwd) }) } if len(errors) > 0 { t.Fatalf("Unknown value '%s' for compiler option '%s'", value, option.Name) } return listVal case tsoptions.CommandLineOptionTypeObject: t.Fatalf("Object type options like '%s' are not supported", option.Name) } return nil } type cachedCompilerHost struct { compiler.CompilerHost tracer *TracerForBaselining } var sourceFileCache collections.SyncMap[SourceFileCacheKey, *ast.SourceFile] type SourceFileCacheKey struct { opts ast.SourceFileParseOptions text string scriptKind core.ScriptKind } func GetSourceFileCacheKey(opts ast.SourceFileParseOptions, text string, scriptKind core.ScriptKind) SourceFileCacheKey { return SourceFileCacheKey{ opts: opts, text: text, scriptKind: scriptKind, } } func (h *cachedCompilerHost) GetSourceFile(opts ast.SourceFileParseOptions) *ast.SourceFile { text, ok := h.FS().ReadFile(opts.FileName) if !ok { return nil } scriptKind := core.GetScriptKindFromFileName(opts.FileName) if scriptKind == core.ScriptKindUnknown { panic("Unknown script kind for file " + opts.FileName) } key := GetSourceFileCacheKey(opts, text, scriptKind) if cached, ok := sourceFileCache.Load(key); ok { return cached } sourceFile := parser.ParseSourceFile(opts, text, scriptKind) result, _ := sourceFileCache.LoadOrStore(key, sourceFile) return result } type TracerForBaselining struct { opts tspath.ComparePathsOptions packageJsonCache map[tspath.Path]bool builder *strings.Builder } func NewTracerForBaselining(opts tspath.ComparePathsOptions, builder *strings.Builder) *TracerForBaselining { return &TracerForBaselining{ opts: opts, packageJsonCache: make(map[tspath.Path]bool), builder: builder, } } func (t *TracerForBaselining) Trace(msg string) { t.TraceWithWriter(t.builder, msg, true) } func (t *TracerForBaselining) TraceWithWriter(w io.Writer, msg string, usePackageJsonCache bool) { fmt.Fprintln(w, t.sanitizeTrace(msg, usePackageJsonCache)) } func (t *TracerForBaselining) sanitizeTrace(msg string, usePackageJsonCache bool) string { // Version if str := strings.Replace(msg, "'"+core.Version()+"'", "'"+FakeTSVersion+"'", 1); str != msg { return str } // caching of fs in trace to be replaces with non caching version if str := strings.TrimSuffix(msg, "' does not exist according to earlier cached lookups."); str != msg { file := strings.TrimPrefix(str, "File '") if usePackageJsonCache { filePath := tspath.ToPath(file, t.opts.CurrentDirectory, t.opts.UseCaseSensitiveFileNames) if _, has := t.packageJsonCache[filePath]; has { return msg } else { t.packageJsonCache[filePath] = false } } return fmt.Sprintf("File '%s' does not exist.", file) } if str := strings.TrimSuffix(msg, "' exists according to earlier cached lookups."); str != msg { file := strings.TrimPrefix(str, "File '") if usePackageJsonCache { filePath := tspath.ToPath(file, t.opts.CurrentDirectory, t.opts.UseCaseSensitiveFileNames) if _, has := t.packageJsonCache[filePath]; has { return msg } else { t.packageJsonCache[filePath] = true } } return fmt.Sprintf("Found 'package.json' at '%s'.", file) } if usePackageJsonCache { if str := strings.TrimSuffix(msg, "' does not exist."); str != msg { file := strings.TrimPrefix(str, "File '") filePath := tspath.ToPath(file, t.opts.CurrentDirectory, t.opts.UseCaseSensitiveFileNames) if _, has := t.packageJsonCache[filePath]; !has { t.packageJsonCache[filePath] = false return msg } else { return fmt.Sprintf("File '%s' does not exist according to earlier cached lookups.", file) } } if str := strings.TrimPrefix(msg, "Found 'package.json' at '"); str != msg { file := strings.TrimSuffix(str, "'.") filePath := tspath.ToPath(file, t.opts.CurrentDirectory, t.opts.UseCaseSensitiveFileNames) if _, has := t.packageJsonCache[filePath]; !has { t.packageJsonCache[filePath] = true return msg } else { return fmt.Sprintf("File '%s' exists according to earlier cached lookups.", file) } } } return msg } func (t *TracerForBaselining) String() string { return t.builder.String() } func (t *TracerForBaselining) Reset() { t.packageJsonCache = make(map[tspath.Path]bool) } func createCompilerHost(fs vfs.FS, defaultLibraryPath string, currentDirectory string) *cachedCompilerHost { tracer := NewTracerForBaselining(tspath.ComparePathsOptions{ UseCaseSensitiveFileNames: fs.UseCaseSensitiveFileNames(), CurrentDirectory: currentDirectory, }, &strings.Builder{}) return &cachedCompilerHost{ CompilerHost: compiler.NewCompilerHost(currentDirectory, fs, defaultLibraryPath, nil, tracer.Trace), tracer: tracer, } } func compileFilesWithHost( host compiler.CompilerHost, config *tsoptions.ParsedCommandLine, harnessOptions *HarnessOptions, ) *CompilationResult { // !!! // if (compilerOptions.project || !rootFiles || rootFiles.length === 0) { // const project = readProject(host.parseConfigHost, compilerOptions.project, compilerOptions); // if (project) { // if (project.errors && project.errors.length > 0) { // return new CompilationResult(host, compilerOptions, /*program*/ undefined, /*result*/ undefined, project.errors); // } // if (project.config) { // rootFiles = project.config.fileNames; // compilerOptions = project.config.options; // } // } // delete compilerOptions.project; // } // !!! Need `getPreEmitDiagnostics` program for this // pre-emit/post-emit error comparison requires declaration emit twice, which can be slow. If it's unlikely to flag any error consistency issues // and if the test is running `skipLibCheck` - an indicator that we want the tets to run quickly - skip the before/after error comparison, too // skipErrorComparison := len(rootFiles) >= 100 || options.SkipLibCheck == core.TSTrue && options.Declaration == core.TSTrue // var preProgram *compiler.Program // if !skipErrorComparison { // preProgram = ts.createProgram({ rootNames: rootFiles || [], options: { ...compilerOptions, configFile: compilerOptions.configFile, traceResolution: false }, host, typeScriptVersion }) // } // let preErrors = preProgram && ts.getPreEmitDiagnostics(preProgram); // if (preProgram && harnessOptions.captureSuggestions) { // preErrors = ts.concatenate(preErrors, ts.flatMap(preProgram.getSourceFiles(), f => preProgram.getSuggestionDiagnostics(f))); // } // const program = ts.createProgram({ rootNames: rootFiles || [], options: compilerOptions, host, harnessOptions.typeScriptVersion }); // const emitResult = program.emit(); // let postErrors = ts.getPreEmitDiagnostics(program); // !!! Need `getSuggestionDiagnostics` for this // if (harnessOptions.captureSuggestions) { // postErrors = ts.concatenate(postErrors, ts.flatMap(program.getSourceFiles(), f => program.getSuggestionDiagnostics(f))); // } // const longerErrors = ts.length(preErrors) > postErrors.length ? preErrors : postErrors; // const shorterErrors = longerErrors === preErrors ? postErrors : preErrors; // const errors = preErrors && (preErrors.length !== postErrors.length) ? [ // ...shorterErrors!, // ts.addRelatedInfo( // ts.createCompilerDiagnostic({ // category: ts.DiagnosticCategory.Error, // code: -1, // key: "-1", // message: `Pre-emit (${preErrors.length}) and post-emit (${postErrors.length}) diagnostic counts do not match! This can indicate that a semantic _error_ was added by the emit resolver - such an error may not be reflected on the command line or in the editor, but may be captured in a baseline here!`, // }), // ts.createCompilerDiagnostic({ // category: ts.DiagnosticCategory.Error, // code: -1, // key: "-1", // message: `The excess diagnostics are:`, // }), // ...ts.filter(longerErrors!, p => !ts.some(shorterErrors, p2 => ts.compareDiagnostics(p, p2) === ts.Comparison.EqualTo)), // ), // ] : postErrors; ctx := context.Background() program := createProgram(host, config) var diagnostics []*ast.Diagnostic diagnostics = append(diagnostics, program.GetProgramDiagnostics()...) diagnostics = append(diagnostics, program.GetSyntacticDiagnostics(ctx, nil)...) diagnostics = append(diagnostics, program.GetSemanticDiagnostics(ctx, nil)...) diagnostics = append(diagnostics, program.GetGlobalDiagnostics(ctx)...) if config.CompilerOptions().GetEmitDeclarations() { diagnostics = append(diagnostics, program.GetDeclarationDiagnostics(ctx, nil)...) } if harnessOptions.CaptureSuggestions { diagnostics = append(diagnostics, program.GetSuggestionDiagnostics(ctx, nil)...) } emitResult := program.Emit(ctx, compiler.EmitOptions{}) return newCompilationResult(config.CompilerOptions(), program, emitResult, diagnostics, harnessOptions) } type CompilationResult struct { Diagnostics []*ast.Diagnostic Result *compiler.EmitResult Program *compiler.Program Options *core.CompilerOptions HarnessOptions *HarnessOptions JS collections.OrderedMap[string, *TestFile] DTS collections.OrderedMap[string, *TestFile] Maps collections.OrderedMap[string, *TestFile] Symlinks map[string]string Repeat func(TestConfiguration) *CompilationResult outputs []*TestFile inputs []*TestFile inputsAndOutputs collections.OrderedMap[string, *CompilationOutput] Trace string } type CompilationOutput struct { Inputs []*TestFile JS *TestFile DTS *TestFile Map *TestFile } func newCompilationResult( options *core.CompilerOptions, program *compiler.Program, result *compiler.EmitResult, diagnostics []*ast.Diagnostic, harnessOptions *HarnessOptions, ) *CompilationResult { if program != nil { options = program.Options() } c := &CompilationResult{ Diagnostics: diagnostics, Result: result, Program: program, Options: options, HarnessOptions: harnessOptions, } fs := program.Host().FS().(*OutputRecorderFS) if fs != nil && program != nil { // Corsa, unlike Strada, can use multiple threads for emit. As a result, the order of outputs is non-deterministic. // To make the order deterministic, we sort the outputs by the order of the inputs. var js, dts, maps collections.OrderedMap[string, *TestFile] for _, document := range fs.Outputs() { if tspath.HasJSFileExtension(document.UnitName) || tspath.HasJSONFileExtension(document.UnitName) { js.Set(document.UnitName, document) } else if tspath.IsDeclarationFileName(document.UnitName) { dts.Set(document.UnitName, document) } else if tspath.FileExtensionIs(document.UnitName, ".map") { maps.Set(document.UnitName, document) } } // using the order from the inputs, populate the outputs for _, sourceFile := range program.GetSourceFiles() { input := &TestFile{UnitName: sourceFile.FileName(), Content: sourceFile.Text()} c.inputs = append(c.inputs, input) if !tspath.IsDeclarationFileName(sourceFile.FileName()) { extname := outputpaths.GetOutputExtension(sourceFile.FileName(), options.Jsx) outputs := &CompilationOutput{ Inputs: []*TestFile{input}, JS: js.GetOrZero(c.getOutputPath(sourceFile.FileName(), extname)), DTS: dts.GetOrZero(c.getOutputPath(sourceFile.FileName(), tspath.GetDeclarationEmitExtensionForPath(sourceFile.FileName()))), Map: maps.GetOrZero(c.getOutputPath(sourceFile.FileName(), extname+".map")), } c.inputsAndOutputs.Set(sourceFile.FileName(), outputs) if outputs.JS != nil { c.inputsAndOutputs.Set(outputs.JS.UnitName, outputs) c.JS.Set(outputs.JS.UnitName, outputs.JS) js.Delete(outputs.JS.UnitName) c.outputs = append(c.outputs, outputs.JS) } if outputs.DTS != nil { c.inputsAndOutputs.Set(outputs.DTS.UnitName, outputs) c.DTS.Set(outputs.DTS.UnitName, outputs.DTS) dts.Delete(outputs.DTS.UnitName) c.outputs = append(c.outputs, outputs.DTS) } if outputs.Map != nil { c.inputsAndOutputs.Set(outputs.Map.UnitName, outputs) c.Maps.Set(outputs.Map.UnitName, outputs.Map) maps.Delete(outputs.Map.UnitName) c.outputs = append(c.outputs, outputs.Map) } } } // add any unhandled outputs, ordered by unit name for _, document := range slices.SortedFunc(js.Values(), compareTestFiles) { c.JS.Set(document.UnitName, document) } for _, document := range slices.SortedFunc(dts.Values(), compareTestFiles) { c.DTS.Set(document.UnitName, document) } for _, document := range slices.SortedFunc(maps.Values(), compareTestFiles) { c.Maps.Set(document.UnitName, document) } } return c } func compareTestFiles(a *TestFile, b *TestFile) int { return strings.Compare(a.UnitName, b.UnitName) } func (c *CompilationResult) getOutputPath(path string, ext string) string { path = tspath.ResolvePath(c.Program.GetCurrentDirectory(), path) var outDir string if ext == ".d.ts" || ext == ".d.mts" || ext == ".d.cts" || (strings.HasSuffix(ext, ".ts") && strings.Contains(ext, ".d.")) { outDir = c.Options.DeclarationDir if outDir == "" { outDir = c.Options.OutDir } } else { outDir = c.Options.OutDir } if outDir != "" { common := c.Program.CommonSourceDirectory() if common != "" { path = tspath.GetRelativePathFromDirectory(common, path, tspath.ComparePathsOptions{ UseCaseSensitiveFileNames: c.Program.UseCaseSensitiveFileNames(), CurrentDirectory: c.Program.GetCurrentDirectory(), }) path = tspath.CombinePaths(tspath.ResolvePath(c.Program.GetCurrentDirectory(), c.Options.OutDir), path) } } return tspath.ChangeExtension(path, ext) } func (r *CompilationResult) FS() vfs.FS { return r.Program.Host().FS() } func (r *CompilationResult) GetNumberOfJSFiles(includeJson bool) int { if includeJson { return r.JS.Size() } count := 0 for file := range r.JS.Values() { if !tspath.FileExtensionIs(file.UnitName, tspath.ExtensionJson) { count++ } } return count } func (c *CompilationResult) Inputs() []*TestFile { return c.inputs } func (c *CompilationResult) Outputs() []*TestFile { return c.outputs } func (c *CompilationResult) GetInputsAndOutputsForFile(path string) *CompilationOutput { return c.inputsAndOutputs.GetOrZero(tspath.ResolvePath(c.Program.GetCurrentDirectory(), path)) } func (c *CompilationResult) GetInputsForFile(path string) []*TestFile { outputs := c.GetInputsAndOutputsForFile(path) if outputs != nil { return outputs.Inputs } return nil } func (c *CompilationResult) GetOutput(path string, kind string /*"js" | "dts" | "map"*/) *TestFile { outputs := c.GetInputsAndOutputsForFile(path) if outputs != nil { switch kind { case "js": return outputs.JS case "dts": return outputs.DTS case "map": return outputs.Map } } return nil } func (c *CompilationResult) GetSourceMapRecord() string { if c.Result == nil || len(c.Result.SourceMaps) == 0 { return "" } var sourceMapRecorder writerAggregator for _, sourceMapData := range c.Result.SourceMaps { var prevSourceFile *ast.SourceFile var currentFile *TestFile if tspath.IsDeclarationFileName(sourceMapData.GeneratedFile) { currentFile = c.DTS.GetOrZero(sourceMapData.GeneratedFile) } else { currentFile = c.JS.GetOrZero(sourceMapData.GeneratedFile) } sourceMapSpanWriter := newSourceMapSpanWriter(&sourceMapRecorder, sourceMapData.SourceMap, currentFile) mapper := sourcemap.DecodeMappings(sourceMapData.SourceMap.Mappings) for decodedSourceMapping := range mapper.Values() { var currentSourceFile *ast.SourceFile if decodedSourceMapping.IsSourceMapping() { currentSourceFile = c.Program.GetSourceFile(sourceMapData.InputSourceFileNames[decodedSourceMapping.SourceIndex]) } if currentSourceFile != prevSourceFile { if currentSourceFile != nil { sourceMapSpanWriter.recordNewSourceFileSpan(decodedSourceMapping, currentSourceFile.Text()) } prevSourceFile = currentSourceFile } else { sourceMapSpanWriter.recordSourceMapSpan(decodedSourceMapping) } } sourceMapSpanWriter.close() } return sourceMapRecorder.String() } func createProgram(host compiler.CompilerHost, config *tsoptions.ParsedCommandLine) *compiler.Program { var singleThreaded core.Tristate if testutil.TestProgramIsSingleThreaded() { singleThreaded = core.TSTrue } programOptions := compiler.ProgramOptions{ Config: config, Host: host, SingleThreaded: singleThreaded, } program := compiler.NewProgram(programOptions) return program } func EnumerateFiles(folder string, testRegex *regexp.Regexp, recursive bool) ([]string, error) { files, err := listFiles(folder, testRegex, recursive) if err != nil { return nil, err } return core.Map(files, tspath.NormalizeSlashes), nil } func listFiles(path string, spec *regexp.Regexp, recursive bool) ([]string, error) { return listFilesWorker(spec, recursive, path) } func listFilesWorker(spec *regexp.Regexp, recursive bool, folder string) ([]string, error) { folder = tspath.GetNormalizedAbsolutePath(folder, repo.TestDataPath) entries, err := os.ReadDir(folder) if err != nil { return nil, err } var paths []string for _, entry := range entries { path := tspath.NormalizePath(filepath.Join(folder, entry.Name())) if !entry.IsDir() { if spec == nil || spec.MatchString(path) { paths = append(paths, path) } } else if recursive { subPaths, err := listFilesWorker(spec, recursive, path) if err != nil { return nil, err } paths = append(paths, subPaths...) } } return paths, nil } func getFileBasedTestConfigurationDescription(config TestConfiguration) string { var output strings.Builder keys := slices.Sorted(maps.Keys(config)) for i, key := range keys { if i > 0 { output.WriteString(",") } fmt.Fprintf(&output, "%s=%s", key, strings.ToLower(config[key])) } return output.String() } func GetFileBasedTestConfigurations(t *testing.T, settings map[string]string, varyByOptions map[string]struct{}) []*NamedTestConfiguration { var optionEntries [][]string // Each element slice has the option name as the first element, and the values as the rest variationCount := 1 nonVaryingOptions := make(map[string]string) for option, value := range settings { if _, ok := varyByOptions[option]; ok { entries := splitOptionValues(t, value, option) if len(entries) > 1 { variationCount *= len(entries) if variationCount > 25 { t.Fatal("Provided test options exceeded the maximum number of variations") } optionEntries = append(optionEntries, append([]string{option}, entries...)) } else if len(entries) == 1 { nonVaryingOptions[option] = entries[0] } } else { // Variation is not supported for the option nonVaryingOptions[option] = value } } var configurations []*NamedTestConfiguration if len(optionEntries) > 0 { // Merge varying and non-varying options varyingConfigurations := computeFileBasedTestConfigurationVariations(variationCount, optionEntries) for _, varyingConfig := range varyingConfigurations { description := getFileBasedTestConfigurationDescription(varyingConfig) maps.Copy(varyingConfig, nonVaryingOptions) configurations = append(configurations, &NamedTestConfiguration{description, varyingConfig}) } } else if len(nonVaryingOptions) > 0 { // Only non-varying options configurations = append(configurations, &NamedTestConfiguration{"", nonVaryingOptions}) } return configurations } // Splits a string value into an array of strings, each corresponding to a unique value for the given option. // Also handles the `*` value, which includes all possible values for the option, and exclusions using `-` or `!`. // ``` // // splitOptionValues("esnext, es2015, es6", "target") => ["esnext", "es2015"] // splitOptionValues("*", "strict") => ["true", "false"] // splitOptionValues("*, -true", "strict") => ["false"] // // ``` func splitOptionValues(t *testing.T, value string, option string) []string { if len(value) == 0 { return nil } star := false var includes []string var excludes []string for _, s := range strings.Split(value, ",") { s = strings.TrimSpace(s) if len(s) == 0 { continue } if s == "*" { star = true } else if strings.HasPrefix(s, "-") || strings.HasPrefix(s, "!") { excludes = append(excludes, s[1:]) } else { includes = append(includes, s) } } if len(includes) == 0 && !star && len(excludes) == 0 { return nil } // Dedupe the variations by their normalized values variations := make(map[tsoptions.CompilerOptionsValue]string) // add (and deduplicate) all included entries for _, include := range includes { value := getValueOfOptionString(t, option, include) if _, ok := variations[value]; !ok { variations[value] = include } } allValues := getAllValuesForOption(option) if star && len(allValues) > 0 { // add all entries for _, include := range allValues { value := getValueOfOptionString(t, option, include) if _, ok := variations[value]; !ok { variations[value] = include } } } // remove all excluded entries for _, exclude := range excludes { value := getValueOfOptionString(t, option, exclude) delete(variations, value) } if len(variations) == 0 { panic(fmt.Sprintf("Variations in test option '@%s' resulted in an empty set.", option)) } return slices.Collect(maps.Values(variations)) } func getValueOfOptionString(t *testing.T, option string, value string) tsoptions.CompilerOptionsValue { optionDecl := getCommandLineOption(option) if optionDecl == nil { t.Fatalf("Unknown option '%s'", option) } // TODO(gabritto): remove this when we deprecate the tests containing those option values if optionDecl.Name == "moduleResolution" && slices.Contains(deprecatedModuleResolution, strings.ToLower(value)) { return value } return getOptionValue(t, optionDecl, value, "/") } func getCommandLineOption(option string) *tsoptions.CommandLineOption { return core.Find(compilerOptions, func(optionDecl *tsoptions.CommandLineOption) bool { return strings.EqualFold(optionDecl.Name, option) }) } func getAllValuesForOption(option string) []string { optionDecl := getCommandLineOption(option) if optionDecl == nil { return nil } switch optionDecl.Kind { case tsoptions.CommandLineOptionTypeEnum: return slices.Collect(optionDecl.EnumMap().Keys()) case tsoptions.CommandLineOptionTypeBoolean: return []string{"true", "false"} } return nil } func computeFileBasedTestConfigurationVariations(variationCount int, optionEntries [][]string) []TestConfiguration { configurations := make([]TestConfiguration, 0, variationCount) computeFileBasedTestConfigurationVariationsWorker(&configurations, optionEntries, 0, make(map[string]string)) return configurations } func computeFileBasedTestConfigurationVariationsWorker( configurations *[]TestConfiguration, optionEntries [][]string, index int, variationState TestConfiguration, ) { if index >= len(optionEntries) { *configurations = append(*configurations, maps.Clone(variationState)) return } optionKey := optionEntries[index][0] entries := optionEntries[index][1:] for _, entry := range entries { // set or overwrite the variation, then compute the next variation variationState[optionKey] = entry computeFileBasedTestConfigurationVariationsWorker(configurations, optionEntries, index+1, variationState) } } func GetConfigNameFromFileName(filename string) string { basenameLower := strings.ToLower(tspath.GetBaseFileName(filename)) if basenameLower == "tsconfig.json" || basenameLower == "jsconfig.json" { return basenameLower } return "" }