228 lines
7.6 KiB
Go
228 lines
7.6 KiB
Go
package testrunner
|
|
|
|
import (
|
|
"regexp"
|
|
"slices"
|
|
"strings"
|
|
|
|
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/ast"
|
|
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/core"
|
|
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/parser"
|
|
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/scanner"
|
|
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/testutil/harnessutil"
|
|
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/tsoptions"
|
|
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/tsoptions/tsoptionstest"
|
|
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/tspath"
|
|
)
|
|
|
|
var lineDelimiter = regexp.MustCompile("\r?\n")
|
|
|
|
// 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
|
|
}
|
|
|
|
type testCaseContent struct {
|
|
testUnitData []*testUnit
|
|
tsConfig *tsoptions.ParsedCommandLine
|
|
tsConfigFileUnitData *testUnit
|
|
symlinks map[string]string
|
|
}
|
|
|
|
// Regex for parsing options in the format "@Alpha: Value of any sort"
|
|
var optionRegex = regexp.MustCompile(`(?m)^\/{2}\s*@(\w+)\s*:\s*([^\r\n]*)`)
|
|
|
|
// Regex for parsing @link option
|
|
var linkRegex = regexp.MustCompile(`(?m)^\/{2}\s*@link\s*:\s*([^\r\n]*)\s*->\s*([^\r\n]*)`)
|
|
|
|
// File-specific directives used by fourslash tests
|
|
var fourslashDirectives = []string{"emitthisfile"}
|
|
|
|
// 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) testCaseContent {
|
|
testUnits, symlinks, currentDirectory, _, _ := ParseTestFilesAndSymlinks(
|
|
code,
|
|
fileName,
|
|
func(filename string, content string, fileOptions map[string]string) (*testUnit, error) {
|
|
return &testUnit{content: content, name: filename}, nil
|
|
},
|
|
)
|
|
if currentDirectory == "" {
|
|
currentDirectory = srcFolder
|
|
}
|
|
|
|
// unit tests always list files explicitly
|
|
allFiles := make(map[string]string)
|
|
for _, data := range testUnits {
|
|
allFiles[tspath.GetNormalizedAbsolutePath(data.name, currentDirectory)] = data.content
|
|
}
|
|
parseConfigHost := tsoptionstest.NewVFSParseConfigHost(allFiles, currentDirectory, true /*useCaseSensitiveFileNames*/)
|
|
|
|
// check if project has tsconfig.json in the list of files
|
|
var tsConfig *tsoptions.ParsedCommandLine
|
|
var tsConfigFileUnitData *testUnit
|
|
for i, data := range testUnits {
|
|
if harnessutil.GetConfigNameFromFileName(data.name) != "" {
|
|
configFileName := tspath.GetNormalizedAbsolutePath(data.name, currentDirectory)
|
|
path := tspath.ToPath(data.name, parseConfigHost.GetCurrentDirectory(), parseConfigHost.Vfs.UseCaseSensitiveFileNames())
|
|
configJson := parser.ParseSourceFile(ast.SourceFileParseOptions{
|
|
FileName: configFileName,
|
|
Path: path,
|
|
}, data.content, core.ScriptKindJSON)
|
|
tsConfigSourceFile := &tsoptions.TsConfigSourceFile{
|
|
SourceFile: configJson,
|
|
}
|
|
configDir := tspath.GetDirectoryPath(configFileName)
|
|
tsConfig = tsoptions.ParseJsonSourceFileConfigFileContent(
|
|
tsConfigSourceFile,
|
|
parseConfigHost,
|
|
configDir,
|
|
nil, /*existingOptions*/
|
|
configFileName,
|
|
nil, /*resolutionStack*/
|
|
nil, /*extraFileExtensions*/
|
|
nil /*extendedConfigCache*/)
|
|
tsConfigFileUnitData = data
|
|
|
|
// delete tsconfig file entry from the list
|
|
testUnits = slices.Delete(testUnits, i, i+1)
|
|
break
|
|
}
|
|
}
|
|
|
|
return testCaseContent{
|
|
testUnitData: testUnits,
|
|
tsConfig: tsConfig,
|
|
tsConfigFileUnitData: tsConfigFileUnitData,
|
|
symlinks: symlinks,
|
|
}
|
|
}
|
|
|
|
// Given a test file containing // @FileName and // @symlink directives,
|
|
// return an array of named units of code to be added to an existing compiler instance,
|
|
// along with a map of symlinks and the current directory.
|
|
func ParseTestFilesAndSymlinks[T any](
|
|
code string,
|
|
fileName string,
|
|
parseFile func(filename string, content string, fileOptions map[string]string) (T, error),
|
|
) (units []T, symlinks map[string]string, currentDir string, globalOptions map[string]string, e error) {
|
|
// List of all the subfiles we've parsed out
|
|
var testUnits []T
|
|
|
|
lines := lineDelimiter.Split(code, -1)
|
|
|
|
// Stuff related to the subfile we're parsing
|
|
var currentFileContent strings.Builder
|
|
var currentFileName string
|
|
var currentDirectory string
|
|
var parseError error
|
|
currentFileOptions := make(map[string]string)
|
|
symlinks = make(map[string]string)
|
|
globalOptions = make(map[string]string)
|
|
|
|
for _, line := range lines {
|
|
ok := parseSymlinkFromTest(line, symlinks)
|
|
if ok {
|
|
continue
|
|
}
|
|
if testMetaData := optionRegex.FindStringSubmatch(line); testMetaData != nil {
|
|
// Comment line, check for global/file @options and record them
|
|
metaDataName := strings.ToLower(testMetaData[1])
|
|
metaDataValue := strings.TrimSpace(testMetaData[2])
|
|
if metaDataName == "currentdirectory" {
|
|
currentDirectory = metaDataValue
|
|
}
|
|
if metaDataName != "filename" {
|
|
if slices.Contains(fourslashDirectives, metaDataName) {
|
|
// File-specific option
|
|
currentFileOptions[metaDataName] = metaDataValue
|
|
} else {
|
|
// Global option
|
|
if existingValue, ok := globalOptions[metaDataName]; ok && existingValue != metaDataValue {
|
|
// !!! This would break existing submodule tests
|
|
// panic("Duplicate global option: " + metaDataName)
|
|
}
|
|
globalOptions[metaDataName] = metaDataValue
|
|
}
|
|
continue
|
|
}
|
|
|
|
// New metadata statement after having collected some code to go with the previous metadata
|
|
if currentFileName != "" {
|
|
// Store result file
|
|
newTestFile, e := parseFile(currentFileName, currentFileContent.String(), currentFileOptions)
|
|
if e != nil {
|
|
parseError = e
|
|
break
|
|
}
|
|
testUnits = append(testUnits, newTestFile)
|
|
|
|
// Reset local data
|
|
currentFileContent.Reset()
|
|
currentFileName = metaDataValue
|
|
currentFileOptions = make(map[string]string)
|
|
} else {
|
|
// First metadata marker in the file
|
|
currentFileName = strings.TrimSpace(testMetaData[2])
|
|
if currentFileContent.Len() != 0 && scanner.SkipTrivia(currentFileContent.String(), 0) != currentFileContent.Len() {
|
|
panic("Non-comment test content appears before the first '// @Filename' directive")
|
|
}
|
|
currentFileContent.Reset()
|
|
}
|
|
} else {
|
|
// Subfile content line
|
|
// Append to the current subfile content, inserting a newline if needed
|
|
if currentFileContent.Len() != 0 {
|
|
// End-of-line
|
|
currentFileContent.WriteRune('\n')
|
|
}
|
|
currentFileContent.WriteString(line)
|
|
}
|
|
}
|
|
|
|
// normalize the fileName for the single file case
|
|
if len(testUnits) == 0 && len(currentFileName) == 0 {
|
|
currentFileName = tspath.GetBaseFileName(fileName)
|
|
}
|
|
|
|
// if there are no parse errors so far, parse the rest of the file
|
|
if parseError == nil {
|
|
// EOF, push whatever remains
|
|
newTestFile2, e := parseFile(currentFileName, currentFileContent.String(), currentFileOptions)
|
|
|
|
parseError = e
|
|
testUnits = append(testUnits, newTestFile2)
|
|
}
|
|
|
|
return testUnits, symlinks, currentDirectory, globalOptions, parseError
|
|
}
|
|
|
|
func extractCompilerSettings(content string) rawCompilerSettings {
|
|
opts := make(map[string]string)
|
|
|
|
for _, match := range optionRegex.FindAllStringSubmatch(content, -1) {
|
|
opts[strings.ToLower(match[1])] = strings.TrimSuffix(strings.TrimSpace(match[2]), ";")
|
|
}
|
|
|
|
return opts
|
|
}
|
|
|
|
func parseSymlinkFromTest(line string, symlinks map[string]string) bool {
|
|
linkMetaData := linkRegex.FindStringSubmatch(line)
|
|
if len(linkMetaData) == 0 {
|
|
return false
|
|
}
|
|
|
|
symlinks[strings.TrimSpace(linkMetaData[2])] = strings.TrimSpace(linkMetaData[1])
|
|
return true
|
|
}
|