diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3f624afd7b..c1ecbaf8e4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -73,7 +73,9 @@ jobs: # Installing gotestsum is super slow on Windows. if: ${{ matrix.os != 'windows-latest' }} - - run: npx hereby test:all ${RACE_FLAG:+"$RACE_FLAG"} ${NOEMBED_FLAG:+"$NOEMBED_FLAG"} + - name: Tests + id: test + run: npx hereby test:all ${RACE_FLAG:+"$RACE_FLAG"} ${NOEMBED_FLAG:+"$NOEMBED_FLAG"} env: RACE_FLAG: ${{ (matrix.race && '--race') || '' }} NOEMBED_FLAG: ${{ (matrix.noembed && '--noembed') || '' }} @@ -81,6 +83,13 @@ jobs: - run: git add . - run: git diff --staged --exit-code --stat + - name: Print baseline diff on failure + if: ${{ failure() && steps.test.conclusion == 'failure' }} + run: | + npx hereby baseline-accept + git add testdata/baselines/reference + git diff --staged --exit-code + lint: runs-on: ubuntu-latest steps: diff --git a/.gitignore b/.gitignore index 0e0d1ecb79..0f19d678d5 100644 --- a/.gitignore +++ b/.gitignore @@ -173,6 +173,9 @@ go.work.sum # Benchmarking results *.txt +# Re-add baseline references +!testdata/baselines/reference/**/*.txt + # Local baselines testdata/baselines/local testdata/baselines/tmp diff --git a/internal/core/compileroptions.go b/internal/core/compileroptions.go index 98417c4216..bbae3d05f8 100644 --- a/internal/core/compileroptions.go +++ b/internal/core/compileroptions.go @@ -19,6 +19,7 @@ type CompilerOptions struct { EmitDeclarationOnly Tristate `json:"emitDeclarationOnly"` EmitBOM Tristate `json:"emitBOM"` DownlevelIteration Tristate `json:"downlevelIteration"` + Declaration Tristate `json:"declaration"` ESModuleInterop Tristate `json:"esModuleInterop"` ExactOptionalPropertyTypes Tristate `json:"exactOptionalPropertyTypes"` ExperimentalDecorators Tristate `json:"experimentalDecorators"` @@ -32,6 +33,7 @@ type CompilerOptions struct { ModuleDetection ModuleDetectionKind `json:"moduleDetectionKind"` NewLine NewLineKind `json:"newLine"` NoEmit Tristate `json:"noEmit"` + NoErrorTruncation Tristate `json:"noErrorTruncation"` NoFallthroughCasesInSwitch Tristate `json:"noFallthroughCasesInSwitch"` NoImplicitAny Tristate `json:"noImplicitAny"` NoImplicitThis Tristate `json:"noImplicitThis"` @@ -45,6 +47,7 @@ type CompilerOptions struct { ResolveJsonModule Tristate `json:"resolveJsonModule"` ResolvePackageJsonExports Tristate `json:"resolvePackageJsonExports"` ResolvePackageJsonImports Tristate `json:"resolvePackageJsonImports"` + SkipLibCheck Tristate `json:"skipLibCheck"` Strict Tristate `json:"strict"` StrictBindCallApply Tristate `json:"strictBindCallApply"` StrictBuiltinIteratorReturn Tristate `json:"strictBuiltinIteratorReturn"` @@ -251,8 +254,9 @@ func (m ModuleResolutionKind) String() string { type NewLineKind int32 const ( - NewLineKindCRLF NewLineKind = 0 - NewLineKindLF NewLineKind = 1 + NewLineKindNone NewLineKind = 0 + NewLineKindCRLF NewLineKind = 1 + NewLineKindLF NewLineKind = 2 ) func (newLine NewLineKind) GetNewLineCharacter() string { diff --git a/internal/diagnosticwriter/diagnosticwriter.go b/internal/diagnosticwriter/diagnosticwriter.go index 05f77a8aaf..d9bff725c2 100644 --- a/internal/diagnosticwriter/diagnosticwriter.go +++ b/internal/diagnosticwriter/diagnosticwriter.go @@ -358,7 +358,7 @@ func WriteFormatDiagnostic(output io.Writer, diagnostic *ast.Diagnostic, formatO fmt.Fprintf(output, "%s(%d,%d): ", relativeFileName, line+1, character+1) } - fmt.Fprintf(output, "%s TS%d: ", diagnostic.Category().String(), diagnostic.Code()) + fmt.Fprintf(output, "%s TS%d: ", diagnostic.Category().Name(), diagnostic.Code()) WriteFlattenedDiagnosticMessage(output, diagnostic, formatOpts.NewLine) fmt.Fprint(output, formatOpts.NewLine) } diff --git a/internal/testutil/harnessutil/harnessutil.go b/internal/testutil/harnessutil/harnessutil.go new file mode 100644 index 0000000000..64303db613 --- /dev/null +++ b/internal/testutil/harnessutil/harnessutil.go @@ -0,0 +1,430 @@ +package harnessutil + +import ( + "fmt" + "maps" + "os" + "path/filepath" + "regexp" + "slices" + "strings" + "testing/fstest" + + "github.com/microsoft/typescript-go/internal/ast" + "github.com/microsoft/typescript-go/internal/bundled" + "github.com/microsoft/typescript-go/internal/compiler" + "github.com/microsoft/typescript-go/internal/core" + "github.com/microsoft/typescript-go/internal/repo" + "github.com/microsoft/typescript-go/internal/tspath" + "github.com/microsoft/typescript-go/internal/vfs" + "github.com/microsoft/typescript-go/internal/vfs/vfstest" +) + +type TestFile struct { + UnitName string + Content string + FileOptions map[string]string +} + +type CompileFilesResult struct { + Diagnostics []*ast.Diagnostic + Program *compiler.Program + // !!! +} + +// This maps a compiler setting to its string value, after splitting by commas, +// handling includions 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 harnessOptions struct { + useCaseSensitiveFileNames bool + includeBuiltFile string + baselineFile string + libFiles []string + noTypesAndSymbols bool + captureSuggestions bool +} + +func CompileFiles( + inputFiles []*TestFile, + otherFiles []*TestFile, + rawHarnessConfig TestConfiguration, + compilerOptions *core.CompilerOptions, + currentDirectory string, + symlinks any, +) *CompileFilesResult { + // originalCurrentDirectory := currentDirectory + var options core.CompilerOptions + if compilerOptions != nil { + options = *compilerOptions + } + harnessOptions := getHarnessOptions(rawHarnessConfig) + + var typescriptVersion string + + // Parse settings + if rawHarnessConfig != nil { // !!! Review after tsconfig parsing: why do we need this if we've already parsed ts config options in `NewCompilerTest`? + setCompilerOptionsFromHarnessConfig(rawHarnessConfig, &options) + typescriptVersion = rawHarnessConfig["typescriptVersion"] + } + + useCaseSensitiveFileNames := true // !!! Get this from harness options; default to true + 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)) + // } + + // !!! This won't work until we have the actual lib files + // // Files from tests\lib that are requested by "@libFiles" + // if len(harnessOptions.libFiles) > 0 { + // for _, libFile := range harnessOptions.libFiles { + // programFileNames = append(programFileNames, tspath.CombinePaths(testLibFolder, libFile)) + // } + // } + + // !!! + // docs := append(inputFiles, otherFiles...) // !!! Convert to `TextDocument` + // const fs = vfs.createFromFileSystem(IO, !useCaseSensitiveFileNames, { documents: docs, cwd: currentDirectory }); + // if (symlinks) { + // fs.apply(symlinks); + // } + + // ts.assign(options, ts.convertToOptionsWithAbsolutePaths(options, path => ts.getNormalizedAbsolutePath(path, currentDirectory))); + + // !!! Port vfs usage closer to original + + // Create fake FS for testing + // Note: the code below assumes a single root, since an FS in Go always has a single root. + testfs := fstest.MapFS{} + for _, file := range inputFiles { + fileName := tspath.GetNormalizedAbsolutePath(file.UnitName, currentDirectory) + rootLen := tspath.GetRootLength(fileName) + fileName = fileName[rootLen:] + testfs[fileName] = &fstest.MapFile{ + Data: []byte(file.Content), + } + } + for _, file := range otherFiles { + fileName := tspath.GetNormalizedAbsolutePath(file.UnitName, currentDirectory) + rootLen := tspath.GetRootLength(fileName) + fileName = fileName[rootLen:] + testfs[fileName] = &fstest.MapFile{ + Data: []byte(file.Content), + } + } + + fs := vfstest.FromMapFS(testfs, useCaseSensitiveFileNames) + fs = bundled.WrapFS(fs) + + host := createCompilerHost(fs, &options, currentDirectory) + result := compileFilesWithHost(host, programFileNames, &options, typescriptVersion, harnessOptions.captureSuggestions) + + return result +} + +func getHarnessOptions(harnessConfig TestConfiguration) harnessOptions { + // !!! Implement this once we have command line options + // !!! Split and trim `libFiles` by comma here + return harnessOptions{} +} + +func setCompilerOptionsFromHarnessConfig(harnessConfig TestConfiguration, options *core.CompilerOptions) { + for name, value := range harnessConfig { + if value == "" { + panic(fmt.Sprintf("Cannot have undefined value for compiler option '%s'", name)) + } + if name == "typescriptversion" { + continue + } + + // !!! Implement this once we have command line options + // const option = getCommandLineOption(name); + // if (option) { + // const errors: ts.Diagnostic[] = []; + // options[option.name] = optionValue(option, value, errors); + // if (errors.length > 0) { + // throw new Error(`Unknown value '${value}' for compiler option '${name}'.`); + // } + // } + // else { + // throw new Error(`Unknown compiler option '${name}'.`); + // } + // !!! Validate that all options present in harness config are either compiler or harness options + } +} + +func createCompilerHost(fs vfs.FS, options *core.CompilerOptions, currentDirectory string) compiler.CompilerHost { + return compiler.NewCompilerHost(options, currentDirectory, fs) +} + +func compileFilesWithHost( + host compiler.CompilerHost, + rootFiles []string, + options *core.CompilerOptions, + typescriptVersion string, + captureSuggestions bool, +) *CompileFilesResult { + // !!! + // 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; + // } + + // establish defaults (aligns with old harness) + if options.NewLine == core.NewLineKindNone { + options.NewLine = core.NewLineKindCRLF + } + // !!! + // if options.SkipDefaultLibCheck == core.TSUnknown { + // options.SkipDefaultLibCheck = core.TSTrue + // } + if options.NoErrorTruncation == core.TSUnknown { + options.NoErrorTruncation = core.TSTrue + } + + // 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 { + // !!! Need actual program for this + // preProgram = ts.createProgram({ rootNames: rootFiles || [], options: { ...compilerOptions, configFile: compilerOptions.configFile, traceResolution: false }, host, typeScriptVersion }) + // } + // let preErrors = preProgram && ts.getPreEmitDiagnostics(preProgram); + // if (preProgram && captureSuggestions) { + // preErrors = ts.concatenate(preErrors, ts.flatMap(preProgram.getSourceFiles(), f => preProgram.getSuggestionDiagnostics(f))); + // } + + // const program = ts.createProgram({ rootNames: rootFiles || [], options: compilerOptions, host, typeScriptVersion }); + // const emitResult = program.emit(); + // let postErrors = ts.getPreEmitDiagnostics(program); + // if (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; + program := createProgram(host, options) + var diagnostics []*ast.Diagnostic + diagnostics = append(diagnostics, program.GetSyntacticDiagnostics(nil)...) + diagnostics = append(diagnostics, program.GetBindDiagnostics(nil)...) + diagnostics = append(diagnostics, program.GetSemanticDiagnostics(nil)...) + diagnostics = append(diagnostics, program.GetGlobalDiagnostics()...) + return &CompileFilesResult{ + Diagnostics: diagnostics, + Program: program, + } +} + +// !!! Temporary while we don't have the real `createProgram` +func createProgram(host compiler.CompilerHost, options *core.CompilerOptions) *compiler.Program { + programOptions := compiler.ProgramOptions{ + RootPath: "/", // Include all files while we don't have a way to specify root files + Host: host, + Options: options, + DefaultLibraryPath: bundled.LibPath(), + } + 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 := 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, config[key]) + } + return output.String() +} + +func GetFileBasedTestConfigurations(settings map[string]string, option []string) []TestConfiguration { + var optionEntries [][]string + variationCount := 1 + for _, optionKey := range option { + value, ok := settings[optionKey] + if ok { + entries := splitOptionValues(value, optionKey) + if len(entries) > 0 { + variationCount *= len(entries) + if variationCount > 25 { + panic("Provided test options exceeded the maximum number of variations: " + strings.Join(option, ", ")) + } + optionEntries = append(optionEntries, []string{optionKey, value}) + } + } + } + + if len(optionEntries) == 0 { + return nil + } + + return computeFileBasedTestConfigurationVariations(variationCount, optionEntries) +} + +func splitOptionValues(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.ToLower(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) + } + } + + // do nothing if the setting has no variations + if len(includes) <= 1 && !star && len(excludes) == 0 { + return nil + } + + // !!! We should dedupe the variations by their normalized values instead of by name + variations := make(map[string]struct{}) + + // add (and deduplicate) all included entries + for _, include := range includes { + // value := getValueOfSetting(setting, include) + variations[include] = struct{}{} + } + + allValues := getAllValuesForOption(option) + if star && len(allValues) > 0 { + // add all entries + for _, value := range allValues { + variations[value] = struct{}{} + } + } + + // remove all excluded entries + for _, exclude := range excludes { + delete(variations, exclude) + } + + if len(variations) == 0 { + panic(fmt.Sprintf("Variations in test option '@%s' resulted in an empty set.", option)) + } + return slices.Collect(maps.Keys(variations)) +} + +func getAllValuesForOption(option string) []string { + // !!! + 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) + } +} diff --git a/internal/testutil/runner/compiler_runner.go b/internal/testutil/runner/compiler_runner.go new file mode 100644 index 0000000000..1832cfe793 --- /dev/null +++ b/internal/testutil/runner/compiler_runner.go @@ -0,0 +1,290 @@ +package runner + +import ( + "fmt" + "maps" + "os" + "path/filepath" + "regexp" + "slices" + "strings" + "testing" + + "github.com/microsoft/typescript-go/internal/core" + "github.com/microsoft/typescript-go/internal/repo" + "github.com/microsoft/typescript-go/internal/testutil/baseline" + "github.com/microsoft/typescript-go/internal/testutil/harnessutil" + "github.com/microsoft/typescript-go/internal/testutil/tsbaseline" + "github.com/microsoft/typescript-go/internal/tspath" +) + +var ( + compilerBaselineRegex = regexp.MustCompile(`\.tsx?$`) + requireStr = "require(" + referencesRegex = regexp.MustCompile(`reference\spath`) +) + +var ( + // Posix-style path to sources under test + srcFolder = "/.src" + // Posix-style path to the TypeScript compiler build outputs (including tsc.js, lib.d.ts, etc.) + builtFolder = "/.ts" + // Posix-style path to additional test libraries + testLibFolder = "/.lib" +) + +type CompilerTestType int + +const ( + TestTypeConformance CompilerTestType = iota + TestTypeRegression +) + +func (t *CompilerTestType) String() string { + if *t == TestTypeRegression { + return "compiler" + } + return "conformance" +} + +type CompilerBaselineRunner struct { + testFiles []string + basePath string + testSuitName string +} + +var _ Runner = (*CompilerBaselineRunner)(nil) + +func NewCompilerBaselineRunner(testType CompilerTestType) *CompilerBaselineRunner { + testSuitName := testType.String() + basePath := "tests/cases/" + testSuitName + return &CompilerBaselineRunner{ + basePath: basePath, + testSuitName: testSuitName, + } +} + +func (r *CompilerBaselineRunner) EnumerateTestFiles() []string { + if len(r.testFiles) > 0 { + return r.testFiles + } + files, err := harnessutil.EnumerateFiles(r.basePath, compilerBaselineRegex, true) + if err != nil { + panic("Could not read compiler test files: " + err.Error()) + } + r.testFiles = files + return files +} + +func (r *CompilerBaselineRunner) RunTests(t *testing.T) { + files := r.EnumerateTestFiles() + for _, filename := range files { + r.runTest(t, filename) + } +} + +var compilerVaryBy []string // !!! Add this when we have real compiler options parsing + +func (r *CompilerBaselineRunner) runTest(t *testing.T, filename string) { + test := getCompilerFileBasedTest(filename) + basename := tspath.GetBaseFileName(filename) + if len(test.configurations) > 0 { + for _, config := range test.configurations { + description := harnessutil.GetFileBasedTestConfigurationDescription(config) + t.Run(basename+description, func(t *testing.T) { r.runSingleConfigTest(t, test, config) }) + } + } else { + t.Run(basename, func(t *testing.T) { r.runSingleConfigTest(t, test, nil) }) + } +} + +func (r *CompilerBaselineRunner) runSingleConfigTest(t *testing.T, test *compilerFileBasedTest, config harnessutil.TestConfiguration) { + t.Parallel() + payload := makeUnitsFromTest(test.content, test.filename) + compilerTest := newCompilerTest(test.filename, &payload, config) + + compilerTest.verifyDiagnostics(t, r.testSuitName) + compilerTest.verifyTypesAndSymbols(t, r.testSuitName) + // !!! Verify all baselines +} + +type compilerFileBasedTest struct { + filename string + content string + configurations []harnessutil.TestConfiguration +} + +func getCompilerFileBasedTest(filename string) *compilerFileBasedTest { + bytes, err := os.ReadFile(filename) + if err != nil { + panic("Could not read test file: " + err.Error()) + } + content := string(bytes) + settings := extractCompilerSettings(content) + configurations := harnessutil.GetFileBasedTestConfigurations(settings, compilerVaryBy) + return &compilerFileBasedTest{ + filename: filename, + content: content, + configurations: configurations, + } +} + +var localBasePath = filepath.Join(repo.TestDataPath, "baselines", "local") + +func cleanUpLocalCompilerTests(testType CompilerTestType) { + localPath := filepath.Join(localBasePath, testType.String()) + err := os.RemoveAll(localPath) + if err != nil { + panic("Could not clean up local compiler tests: " + err.Error()) + } +} + +type compilerTest struct { + filename string + basename string + configuredName string // name with configuration description, e.g. `file` + options *core.CompilerOptions + result *harnessutil.CompileFilesResult + tsConfigFiles []*harnessutil.TestFile + toBeCompiled []*harnessutil.TestFile // equivalent to the files that will be passed on the command line + otherFiles []*harnessutil.TestFile // equivalent to other files on the file system not directly passed to the compiler (ie things that are referenced by other files) + hasNonDtsFiles bool +} + +type testCaseContentWithConfig struct { + testCaseContent + configuration harnessutil.TestConfiguration +} + +func newCompilerTest(filename string, testContent *testCaseContent, configuration harnessutil.TestConfiguration) *compilerTest { + basename := tspath.GetBaseFileName(filename) + configuredName := basename + if configuration != nil { + // Compute name with configuration description, e.g. `filename(target=esnext).ts` + var configNameBuilder strings.Builder + keys := slices.Sorted(maps.Keys(configuration)) + for i, key := range keys { + if i > 0 { + configNameBuilder.WriteRune(',') + } + fmt.Fprintf(&configNameBuilder, "%s=%s", strings.ToLower(key), strings.ToLower(configuration[key])) + } + configName := configNameBuilder.String() + if len(configName) > 0 { + extname := tspath.GetAnyExtensionFromPath(basename, nil, false) + extensionlessBasename := basename[:len(basename)-len(extname)] + configuredName = fmt.Sprintf("%s(%s)%s", extensionlessBasename, configName, extname) + } + } + + testCaseContentWithConfig := testCaseContentWithConfig{ + testCaseContent: *testContent, + configuration: configuration, + } + + harnessConfig := testCaseContentWithConfig.configuration + currentDirectory := harnessConfig["currentDirectory"] + if currentDirectory == "" { + currentDirectory = srcFolder + } + + units := testCaseContentWithConfig.testUnitData + var toBeCompiled []*harnessutil.TestFile + var otherFiles []*harnessutil.TestFile + var tsConfigOptions *core.CompilerOptions + hasNonDtsFiles := core.Some(units, func(unit *testUnit) bool { return !tspath.FileExtensionIs(unit.name, tspath.ExtensionDts) }) + // var tsConfigFiles []*harnessutil.TestFile // !!! + if testCaseContentWithConfig.tsConfig != nil { + // !!! + } else { + baseUrl, ok := harnessConfig["baseUrl"] + if ok && !tspath.IsRootedDiskPath(baseUrl) { + harnessConfig["baseUrl"] = tspath.GetNormalizedAbsolutePath(baseUrl, currentDirectory) + } + + lastUnit := units[len(units)-1] + // We need to assemble the list of input files for the compiler and other related files on the 'filesystem' (ie in a multi-file test) + // If the last file in a test uses require or a triple slash reference we'll assume all other files will be brought in via references, + // otherwise, assume all files are just meant to be in the same compilation session without explicit references to one another. + + if testCaseContentWithConfig.configuration["noImplicitReferences"] != "" || + strings.Contains(lastUnit.content, requireStr) || + referencesRegex.MatchString(lastUnit.content) { + toBeCompiled = append(toBeCompiled, createHarnessTestFile(lastUnit, currentDirectory)) + for _, unit := range units[:len(units)-1] { + otherFiles = append(otherFiles, createHarnessTestFile(unit, currentDirectory)) + } + } else { + toBeCompiled = core.Map(units, func(unit *testUnit) *harnessutil.TestFile { return createHarnessTestFile(unit, currentDirectory) }) + } + } + + if tsConfigOptions != nil && tsConfigOptions.ConfigFilePath != "" { + // tsConfigOptions.configFile!.fileName = tsConfigOptions.configFilePath; // !!! + } + + result := harnessutil.CompileFiles( + toBeCompiled, + otherFiles, + harnessConfig, + tsConfigOptions, + currentDirectory, + testCaseContentWithConfig.symlinks, + ) + + return &compilerTest{ + filename: filename, + basename: basename, + configuredName: configuredName, + // options: result.options, // !!! + result: result, + // tsConfigFiles: tsConfigFiles, // !!! + toBeCompiled: toBeCompiled, + otherFiles: otherFiles, + hasNonDtsFiles: hasNonDtsFiles, + } +} + +func (c *compilerTest) verifyDiagnostics(t *testing.T, suiteName string) { + // pretty := c.result.options.pretty + pretty := false // !!! Add `pretty` to compiler options + files := core.Concatenate(c.tsConfigFiles, core.Concatenate(c.toBeCompiled, c.otherFiles)) + tsbaseline.DoErrorBaseline(t, c.configuredName, files, c.result.Diagnostics, pretty, suiteName) +} + +func (c *compilerTest) verifyTypesAndSymbols(t *testing.T, suiteName string) { + // !!! Needs harness settings parsing + // const noTypesAndSymbols = this.harnessSettings.noTypesAndSymbols && + // this.harnessSettings.noTypesAndSymbols.toLowerCase() === "true"; + // if (noTypesAndSymbols) { + // return; + // } + program := c.result.Program + allFiles := core.Filter( + core.Concatenate(c.toBeCompiled, c.otherFiles), + func(f *harnessutil.TestFile) bool { + return program.GetSourceFile(f.UnitName) != nil + }, + ) + + header := tspath.GetRelativePathFromDirectory(repo.TestDataPath, c.filename, tspath.ComparePathsOptions{}) + tsbaseline.DoTypeAndSymbolBaseline( + t, + c.configuredName, + header, + program, + allFiles, + baseline.Options{Subfolder: suiteName}, + false, + false, + len(c.result.Diagnostics) > 0, + ) +} + +func createHarnessTestFile(unit *testUnit, currentDirectory string) *harnessutil.TestFile { + return &harnessutil.TestFile{ + UnitName: tspath.GetNormalizedAbsolutePath(unit.name, currentDirectory), + Content: unit.content, + FileOptions: unit.fileOptions, + } +} diff --git a/internal/testutil/runner/compiler_runner_test.go b/internal/testutil/runner/compiler_runner_test.go new file mode 100644 index 0000000000..51180a968f --- /dev/null +++ b/internal/testutil/runner/compiler_runner_test.go @@ -0,0 +1,27 @@ +package runner + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/bundled" +) + +func TestCompilerBaselines(t *testing.T) { + t.Parallel() + + if !bundled.Embedded { + // Without embedding, we'd need to read all of the lib files out from disk into the MapFS. + // Just skip this for now. + t.Skip("bundled files are not embedded") + } + + testTypes := []CompilerTestType{TestTypeRegression, TestTypeConformance} + for _, testType := range testTypes { + t.Run(testType.String(), func(t *testing.T) { + t.Parallel() + cleanUpLocalCompilerTests(testType) + runner := NewCompilerBaselineRunner(testType) + runner.RunTests(t) + }) + } +} diff --git a/internal/testutil/runner/runner.go b/internal/testutil/runner/runner.go new file mode 100644 index 0000000000..28147fbfd0 --- /dev/null +++ b/internal/testutil/runner/runner.go @@ -0,0 +1,17 @@ +package runner + +import "testing" + +type Runner interface { + EnumerateTestFiles() []string + RunTests(t *testing.T) +} + +func runTests(t *testing.T, runners []Runner) { + // !!! + // const seen = new Map(); + // const dupes: [string, string][] = []; + for _, runner := range runners { + runner.RunTests(t) + } +} diff --git a/internal/testutil/runner/test_case_parser.go b/internal/testutil/runner/test_case_parser.go index 1d731df3d2..fe5d2d51e0 100644 --- a/internal/testutil/runner/test_case_parser.go +++ b/internal/testutil/runner/test_case_parser.go @@ -10,19 +10,22 @@ import ( var lineDelimiter = regexp.MustCompile("\r?\n") -// all the necessary information to set the right compiler settings -type compilerSettings map[string]string +// This maps a compiler setting to its value as written in the test file. For example, if a test file contains: +// +// // @target: esnext, es2015 +// +// Then the map will map "target" to "esnext, es2015" +type rawCompilerSettings map[string]string // All the necessary information to turn a multi file test into useful units for later compilation type testUnit struct { content string name string - fileOptions map[string]string + fileOptions rawCompilerSettings originalFilePath string } type testCaseContent struct { - settings compilerSettings testUnitData []*testUnit tsConfig any // !!! tsConfigFileUnitData any // !!! @@ -33,10 +36,7 @@ type testCaseContent struct { var optionRegex = regexp.MustCompile(`(?m)^\/{2}\s*@(\w+)\s*:\s*([^\r\n]*)`) // multiple matches on multiple lines // Given a test file containing // @FileName directives, return an array of named units of code to be added to an existing compiler instance -func makeUnitsFromTest(code string, fileName string, settings compilerSettings) testCaseContent { - if settings == nil { - settings = extractCompilerSettings(code) - } +func makeUnitsFromTest(code string, fileName string) testCaseContent { // List of all the subfiles we've parsed out var testUnits []*testUnit @@ -163,16 +163,15 @@ func makeUnitsFromTest(code string, fileName string, settings compilerSettings) // } return testCaseContent{ - settings: settings, testUnitData: testUnits, } } -func extractCompilerSettings(content string) compilerSettings { +func extractCompilerSettings(content string) rawCompilerSettings { opts := make(map[string]string) for _, match := range optionRegex.FindAllStringSubmatch(content, -1) { - opts[match[1]] = strings.TrimSpace(match[2]) + opts[strings.ToLower(match[1])] = strings.TrimSpace(match[2]) } return opts diff --git a/internal/testutil/runner/test_case_parser_test.go b/internal/testutil/runner/test_case_parser_test.go index 89ed21e1fa..ae829ede93 100644 --- a/internal/testutil/runner/test_case_parser_test.go +++ b/internal/testutil/runner/test_case_parser_test.go @@ -35,11 +35,6 @@ function bar() { return "b"; }`, originalFilePath: "simpleTest.ts", } testContent := testCaseContent{ - settings: map[string]string{ - "strict": "true", - "noEmit": "true", - "filename": "secondFile.ts", - }, testUnitData: []*testUnit{testUnit1, testUnit2}, tsConfig: nil, tsConfigFileUnitData: nil, @@ -47,7 +42,7 @@ function bar() { return "b"; }`, } assert.DeepEqual( t, - makeUnitsFromTest(code, "simpleTest.ts", nil), + makeUnitsFromTest(code, "simpleTest.ts"), testContent, cmp.AllowUnexported(testCaseContent{}, testUnit{})) } diff --git a/internal/testutil/tsbaseline/error_baseline.go b/internal/testutil/tsbaseline/error_baseline.go index c69bf5c1a0..1172f08808 100644 --- a/internal/testutil/tsbaseline/error_baseline.go +++ b/internal/testutil/tsbaseline/error_baseline.go @@ -13,6 +13,7 @@ import ( "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/diagnosticwriter" "github.com/microsoft/typescript-go/internal/testutil/baseline" + "github.com/microsoft/typescript-go/internal/testutil/harnessutil" "github.com/microsoft/typescript-go/internal/tspath" "gotest.tools/v3/assert" "gotest.tools/v3/assert/cmp" @@ -25,18 +26,12 @@ var formatOpts = &diagnosticwriter.FormattingOptions{ NewLine: harnessNewLine, } -type TestFile struct { - unitName string - content string - fileOptions map[string]string -} - var ( diagnosticsLocationPrefix = regexp.MustCompile(`(?im)^(lib.*\.d\.ts)\(\d+,\d+\)`) diagnosticsLocationPattern = regexp.MustCompile(`(?i)(lib.*\.d\.ts):\d+:\d+`) ) -func DoErrorBaseline(t *testing.T, baselinePath string, inputFiles []*TestFile, errors []*ast.Diagnostic, pretty bool) { +func DoErrorBaseline(t *testing.T, baselinePath string, inputFiles []*harnessutil.TestFile, errors []*ast.Diagnostic, pretty bool, subfolder string) { baselinePath = tsExtension.ReplaceAllString(baselinePath, ".errors.txt") var errorBaseline string if len(errors) > 0 { @@ -44,7 +39,7 @@ func DoErrorBaseline(t *testing.T, baselinePath string, inputFiles []*TestFile, } else { errorBaseline = baseline.NoContent } - baseline.Run(t, baselinePath, errorBaseline, baseline.Options{}) + baseline.Run(t, baselinePath, errorBaseline, baseline.Options{Subfolder: subfolder}) } func minimalDiagnosticsToString(diagnostics []*ast.Diagnostic, pretty bool) string { @@ -57,7 +52,7 @@ func minimalDiagnosticsToString(diagnostics []*ast.Diagnostic, pretty bool) stri return output.String() } -func getErrorBaseline(t *testing.T, inputFiles []*TestFile, diagnostics []*ast.Diagnostic, pretty bool) string { +func getErrorBaseline(t *testing.T, inputFiles []*harnessutil.TestFile, diagnostics []*ast.Diagnostic, pretty bool) string { t.Helper() outputLines := iterateErrorBaseline(t, inputFiles, diagnostics, pretty) @@ -73,7 +68,7 @@ func getErrorBaseline(t *testing.T, inputFiles []*TestFile, diagnostics []*ast.D return strings.Join(outputLines, "") } -func iterateErrorBaseline(t *testing.T, inputFiles []*TestFile, inputDiagnostics []*ast.Diagnostic, pretty bool) []string { +func iterateErrorBaseline(t *testing.T, inputFiles []*harnessutil.TestFile, inputDiagnostics []*ast.Diagnostic, pretty bool) []string { t.Helper() diagnostics := slices.Clone(inputDiagnostics) slices.SortFunc(diagnostics, compiler.CompareDiagnostics) @@ -104,7 +99,7 @@ func iterateErrorBaseline(t *testing.T, inputFiles []*TestFile, inputDiagnostics if len(line) < 0 { continue } - out := fmt.Sprintf("!!! %s TS%d: %s", diag.Category().String(), diag.Code(), line) + out := fmt.Sprintf("!!! %s TS%d: %s", diag.Category().Name(), diag.Code(), line) errLines = append(errLines, out) } @@ -157,19 +152,19 @@ func iterateErrorBaseline(t *testing.T, inputFiles []*TestFile, inputDiagnostics // 'merge' the lines of each input file with any errors associated with it dupeCase := map[string]int{} - nonEmptyFiles := core.Filter(inputFiles, func(f *TestFile) bool { return len(f.content) > 0 }) + nonEmptyFiles := core.Filter(inputFiles, func(f *harnessutil.TestFile) bool { return len(f.Content) > 0 }) for _, inputFile := range nonEmptyFiles { // Filter down to the errors in the file fileErrors := core.Filter(diagnostics, func(e *ast.Diagnostic) bool { return e.File() != nil && - tspath.ComparePaths(removeTestPathPrefixes(e.File().FileName(), false), removeTestPathPrefixes(inputFile.unitName, false), tspath.ComparePathsOptions{}) == 0 + tspath.ComparePaths(removeTestPathPrefixes(e.File().FileName(), false), removeTestPathPrefixes(inputFile.UnitName, false), tspath.ComparePathsOptions{}) == 0 }) // Header fmt.Fprintf(&outputLines, "%s==== %s (%d errors) ====", newLine(), - removeTestPathPrefixes(inputFile.unitName, false), + removeTestPathPrefixes(inputFile.UnitName, false), len(fileErrors), ) @@ -177,15 +172,15 @@ func iterateErrorBaseline(t *testing.T, inputFiles []*TestFile, inputDiagnostics markedErrorCount := 0 // For each line, emit the line followed by any error squiggles matching this line - lineStarts := core.ComputeLineStarts(inputFile.content) - lines := lineDelimiter.Split(inputFile.content, -1) + lineStarts := core.ComputeLineStarts(inputFile.Content) + lines := lineDelimiter.Split(inputFile.Content, -1) for lineIndex, line := range lines { thisLineStart := int(lineStarts[lineIndex]) var nextLineStart int // On the last line of the file, fake the next line start number so that we handle errors on the last character of the file correctly if lineIndex == len(lines)-1 { - nextLineStart = len(inputFile.content) + nextLineStart = len(inputFile.Content) } else { nextLineStart = int(lineStarts[lineIndex+1]) } @@ -219,8 +214,8 @@ func iterateErrorBaseline(t *testing.T, inputFiles []*TestFile, inputDiagnostics } // Verify we didn't miss any errors in this file - assert.Check(t, cmp.Equal(markedErrorCount, len(fileErrors)), "count of errors in "+inputFile.unitName) - _, isDupe := dupeCase[sanitizeTestFilePath(inputFile.unitName)] + assert.Check(t, cmp.Equal(markedErrorCount, len(fileErrors)), "count of errors in "+inputFile.UnitName) + _, isDupe := dupeCase[sanitizeTestFilePath(inputFile.UnitName)] result = append(result, outputLines.String()) if isDupe { // Case-duplicated files on a case-insensitive build will have errors reported in both the dupe and the original diff --git a/internal/testutil/tsbaseline/symbol_baseline.go b/internal/testutil/tsbaseline/symbol_baseline.go index 1276e448a2..c69c84f499 100644 --- a/internal/testutil/tsbaseline/symbol_baseline.go +++ b/internal/testutil/tsbaseline/symbol_baseline.go @@ -13,6 +13,7 @@ import ( "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/scanner" "github.com/microsoft/typescript-go/internal/testutil/baseline" + "github.com/microsoft/typescript-go/internal/testutil/harnessutil" "github.com/microsoft/typescript-go/internal/tspath" ) @@ -27,7 +28,7 @@ func DoTypeAndSymbolBaseline( baselinePath string, header string, program *compiler.Program, - allFiles []*TestFile, + allFiles []*harnessutil.TestFile, opts baseline.Options, skipTypeBaselines bool, skipSymbolBaselines bool, @@ -50,9 +51,10 @@ func DoTypeAndSymbolBaseline( fullWalker := newTypeWriterWalker(program, hasErrorBaseline) - t.Run("type", func(t *testing.T) { - checkBaselines(t, baselinePath, allFiles, fullWalker, header, opts, false /*isSymbolBaseline*/) - }) + // !!! Enable type baselines once it's implemented + // t.Run("type", func(t *testing.T) { + // checkBaselines(t, baselinePath, allFiles, fullWalker, header, opts, false /*isSymbolBaseline*/) + // }) t.Run("symbol", func(t *testing.T) { checkBaselines(t, baselinePath, allFiles, fullWalker, header, opts, true /*isSymbolBaseline*/) }) @@ -61,7 +63,7 @@ func DoTypeAndSymbolBaseline( func checkBaselines( t *testing.T, baselinePath string, - allFiles []*TestFile, + allFiles []*harnessutil.TestFile, fullWalker *typeWriterWalker, header string, opts baseline.Options, @@ -74,7 +76,7 @@ func checkBaselines( } func generateBaseline( - allFiles []*TestFile, + allFiles []*harnessutil.TestFile, fullWalker *typeWriterWalker, header string, isSymbolBaseline bool, @@ -125,14 +127,14 @@ func generateBaseline( return result.String() } -func iterateBaseline(allFiles []*TestFile, fullWalker *typeWriterWalker, isSymbolBaseline bool) []string { +func iterateBaseline(allFiles []*harnessutil.TestFile, fullWalker *typeWriterWalker, isSymbolBaseline bool) []string { var baselines []string for _, file := range allFiles { - unitName := file.unitName + unitName := file.UnitName var typeLines strings.Builder typeLines.WriteString("=== " + unitName + " ===\r\n") - codeLines := codeLinesRegexp.Split(file.content, -1) + codeLines := codeLinesRegexp.Split(file.Content, -1) var results []*typeWriterResult if isSymbolBaseline { results = fullWalker.getSymbols(unitName) diff --git a/testdata/baselines/reference/compiler/simpleTestMultiFile.errors.txt b/testdata/baselines/reference/compiler/simpleTestMultiFile.errors.txt new file mode 100644 index 0000000000..87a0d9fe1c --- /dev/null +++ b/testdata/baselines/reference/compiler/simpleTestMultiFile.errors.txt @@ -0,0 +1,13 @@ +/src/bar.ts(1,7): error TS2322: Type '1' is not assignable to type 'string'. +/src/foo.ts(1,7): error TS2322: Type '""' is not assignable to type 'number'. + + +==== /src/foo.ts (1 errors) ==== + const x: number = ""; + ~ +!!! error TS2322: Type '""' is not assignable to type 'number'. + +==== /src/bar.ts (1 errors) ==== + const y: string = 1; + ~ +!!! error TS2322: Type '1' is not assignable to type 'string'. \ No newline at end of file diff --git a/testdata/baselines/reference/compiler/simpleTestMultiFile.symbols b/testdata/baselines/reference/compiler/simpleTestMultiFile.symbols new file mode 100644 index 0000000000..1958e1c9f5 --- /dev/null +++ b/testdata/baselines/reference/compiler/simpleTestMultiFile.symbols @@ -0,0 +1,10 @@ +//// [tests/cases/compiler/simpleTestMultiFile.ts] //// + +=== /src/foo.ts === +const x: number = ""; +>x : Symbol(x, Decl(foo.ts, 0, 5)) + +=== /src/bar.ts === +const y: string = 1; +>y : Symbol(y, Decl(bar.ts, 0, 5)) + diff --git a/testdata/baselines/reference/compiler/simpleTestSingleFile.errors.txt b/testdata/baselines/reference/compiler/simpleTestSingleFile.errors.txt new file mode 100644 index 0000000000..922cf75aa3 --- /dev/null +++ b/testdata/baselines/reference/compiler/simpleTestSingleFile.errors.txt @@ -0,0 +1,7 @@ +/simpleTestSingleFile.ts(1,7): error TS2322: Type '""' is not assignable to type 'number'. + + +==== /simpleTestSingleFile.ts (1 errors) ==== + const x: number = ""; + ~ +!!! error TS2322: Type '""' is not assignable to type 'number'. \ No newline at end of file diff --git a/testdata/baselines/reference/compiler/simpleTestSingleFile.symbols b/testdata/baselines/reference/compiler/simpleTestSingleFile.symbols new file mode 100644 index 0000000000..da688950b2 --- /dev/null +++ b/testdata/baselines/reference/compiler/simpleTestSingleFile.symbols @@ -0,0 +1,6 @@ +//// [tests/cases/compiler/simpleTestSingleFile.ts] //// + +=== /simpleTestSingleFile.ts === +const x: number = ""; +>x : Symbol(x, Decl(simpleTestSingleFile.ts, 0, 5)) + diff --git a/testdata/baselines/reference/conformance/simpleTest.symbols b/testdata/baselines/reference/conformance/simpleTest.symbols new file mode 100644 index 0000000000..fb284745fb --- /dev/null +++ b/testdata/baselines/reference/conformance/simpleTest.symbols @@ -0,0 +1,5 @@ +//// [tests/cases/conformance/simpleTest.ts] //// + +=== /simpleTest.ts === + +1 + 2; diff --git a/testdata/tests/cases/compiler/simpleTestMultiFile.ts b/testdata/tests/cases/compiler/simpleTestMultiFile.ts new file mode 100644 index 0000000000..6ba1c61aac --- /dev/null +++ b/testdata/tests/cases/compiler/simpleTestMultiFile.ts @@ -0,0 +1,5 @@ +// @filename: /src/foo.ts +const x: number = ""; + +// @filename: /src/bar.ts +const y: string = 1; \ No newline at end of file diff --git a/testdata/tests/cases/compiler/simpleTestSingleFile.ts b/testdata/tests/cases/compiler/simpleTestSingleFile.ts new file mode 100644 index 0000000000..262da12169 --- /dev/null +++ b/testdata/tests/cases/compiler/simpleTestSingleFile.ts @@ -0,0 +1 @@ +const x: number = ""; \ No newline at end of file diff --git a/testdata/tests/cases/conformance/simpleTest.ts b/testdata/tests/cases/conformance/simpleTest.ts new file mode 100644 index 0000000000..167665d097 --- /dev/null +++ b/testdata/tests/cases/conformance/simpleTest.ts @@ -0,0 +1 @@ +1 + 2; \ No newline at end of file