vendor tsgo
This commit is contained in:
250
tools/tsgo/internal/testutil/baseline/baseline.go
Normal file
250
tools/tsgo/internal/testutil/baseline/baseline.go
Normal file
@@ -0,0 +1,250 @@
|
||||
package baseline
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/collections"
|
||||
"github.com/microsoft/typescript-go/internal/repo"
|
||||
"github.com/microsoft/typescript-go/internal/stringutil"
|
||||
"github.com/peter-evans/patience"
|
||||
)
|
||||
|
||||
type Options struct {
|
||||
Subfolder string
|
||||
IsSubmodule bool
|
||||
IsSubmoduleAccepted bool
|
||||
IsSubmoduleTriaged bool
|
||||
DiffFixupOld func(string) string
|
||||
DiffFixupNew func(string) string
|
||||
SkipDiffWithOld bool
|
||||
}
|
||||
|
||||
const NoContent = "<no content>"
|
||||
|
||||
func Run(t *testing.T, fileName string, actual string, opts Options) {
|
||||
origSubfolder := opts.Subfolder
|
||||
|
||||
{
|
||||
subfolder := opts.Subfolder
|
||||
if opts.IsSubmodule {
|
||||
subfolder = filepath.Join("submodule", subfolder)
|
||||
}
|
||||
|
||||
localPath := filepath.Join(localRoot, subfolder, fileName)
|
||||
referencePath := filepath.Join(referenceRoot, subfolder, fileName)
|
||||
|
||||
// Record this baseline for tracking unused baselines
|
||||
recordBaseline(t, filepath.Join(subfolder, fileName))
|
||||
|
||||
writeComparison(t, actual, localPath, referencePath, false)
|
||||
}
|
||||
|
||||
if !opts.IsSubmodule || opts.SkipDiffWithOld {
|
||||
// Not a submodule, no diffs.
|
||||
return
|
||||
}
|
||||
|
||||
submoduleReference := filepath.Join(submoduleReferenceRoot, fileName)
|
||||
submoduleExpected := readFileOrNoContent(submoduleReference)
|
||||
|
||||
const (
|
||||
submoduleFolder = "submodule"
|
||||
submoduleAcceptedFolder = "submoduleAccepted"
|
||||
submoduleTriagedFolder = "submoduleTriaged"
|
||||
)
|
||||
|
||||
diffFileName := fileName + ".diff"
|
||||
diffKey := origSubfolder + "/" + diffFileName
|
||||
isSubmoduleAccepted := opts.IsSubmoduleAccepted || submoduleAcceptedFileNames().Has(diffKey)
|
||||
isSubmoduleTriaged := opts.IsSubmoduleTriaged || submoduleTriagedFileNames().Has(diffKey)
|
||||
|
||||
if isSubmoduleAccepted && isSubmoduleTriaged {
|
||||
t.Fatalf("diff file %s/%s is in both submoduleAccepted and submoduleTriaged; it should only be in one", origSubfolder, diffFileName)
|
||||
}
|
||||
|
||||
var outRoot string
|
||||
switch {
|
||||
case isSubmoduleAccepted:
|
||||
outRoot = submoduleAcceptedFolder
|
||||
case isSubmoduleTriaged:
|
||||
outRoot = submoduleTriagedFolder
|
||||
default:
|
||||
outRoot = submoduleFolder
|
||||
}
|
||||
|
||||
allRoots := [3]string{submoduleFolder, submoduleAcceptedFolder, submoduleTriagedFolder}
|
||||
|
||||
diff := getBaselineDiff(t, actual, submoduleExpected, fileName, opts.DiffFixupOld, opts.DiffFixupNew)
|
||||
|
||||
for _, root := range allRoots {
|
||||
localPath := filepath.Join(localRoot, root, origSubfolder, diffFileName)
|
||||
referencePath := filepath.Join(referenceRoot, root, origSubfolder, diffFileName)
|
||||
|
||||
// Record this baseline for tracking unused baselines
|
||||
recordBaseline(t, filepath.Join(root, origSubfolder, diffFileName))
|
||||
|
||||
if root == outRoot {
|
||||
writeComparison(t, diff, localPath, referencePath, false)
|
||||
} else {
|
||||
writeComparison(t, NoContent, localPath, referencePath, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var submoduleAcceptedFileNames = sync.OnceValue(func() *collections.Set[string] {
|
||||
return readFileNameSet(filepath.Join(repo.TestDataPath(), "submoduleAccepted.txt"))
|
||||
})
|
||||
|
||||
var submoduleTriagedFileNames = sync.OnceValue(func() *collections.Set[string] {
|
||||
return readFileNameSet(filepath.Join(repo.TestDataPath(), "submoduleTriaged.txt"))
|
||||
})
|
||||
|
||||
func readFileNameSet(path string) *collections.Set[string] {
|
||||
var set collections.Set[string]
|
||||
|
||||
if content, err := os.ReadFile(path); err == nil {
|
||||
for line := range strings.SplitSeq(string(content), "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" || line[0] == '#' {
|
||||
continue
|
||||
}
|
||||
set.Add(line)
|
||||
}
|
||||
} else {
|
||||
panic(fmt.Sprintf("failed to read file %s: %v", path, err))
|
||||
}
|
||||
|
||||
return &set
|
||||
}
|
||||
|
||||
func readFileOrNoContent(fileName string) string {
|
||||
content, err := os.ReadFile(fileName)
|
||||
if err != nil {
|
||||
return NoContent
|
||||
}
|
||||
return string(content)
|
||||
}
|
||||
|
||||
func DiffText(oldName string, newName string, expected string, actual string) string {
|
||||
lines := patience.Diff(stringutil.SplitLines(expected), stringutil.SplitLines(actual))
|
||||
return patience.UnifiedDiffTextWithOptions(lines, patience.UnifiedDiffOptions{
|
||||
Precontext: 3,
|
||||
Postcontext: 3,
|
||||
SrcHeader: oldName,
|
||||
DstHeader: newName,
|
||||
})
|
||||
}
|
||||
|
||||
func getBaselineDiff(t *testing.T, actual string, expected string, fileName string, fixupOld func(string) string, fixupNew func(string) string) string {
|
||||
if fixupOld != nil {
|
||||
expected = fixupOld(expected)
|
||||
}
|
||||
if fixupNew != nil {
|
||||
actual = fixupNew(actual)
|
||||
}
|
||||
if actual == expected {
|
||||
return NoContent
|
||||
}
|
||||
s := DiffText("old."+fileName, "new."+fileName, expected, actual)
|
||||
|
||||
// If the diff is empty (just headers, no hunks), return NoContent
|
||||
if !strings.Contains(s, "@@") {
|
||||
return NoContent
|
||||
}
|
||||
|
||||
// Remove line numbers from unified diff headers; this avoids adding/deleting
|
||||
// lines in our baselines from causing knock-on header changes later in the diff.
|
||||
|
||||
aCurLine := 1
|
||||
bCurLine := 1
|
||||
s = fixUnifiedDiff.ReplaceAllStringFunc(s, func(match string) string {
|
||||
var aLine, aLineCount, bLine, bLineCount int
|
||||
if _, err := fmt.Sscanf(match, "@@ -%d,%d +%d,%d @@", &aLine, &aLineCount, &bLine, &bLineCount); err != nil {
|
||||
panic(fmt.Sprintf("failed to parse unified diff header: %v", err))
|
||||
}
|
||||
aDiff := aLine - aCurLine
|
||||
bDiff := bLine - bCurLine
|
||||
aCurLine = aLine
|
||||
bCurLine = bLine
|
||||
|
||||
// Keep surrounded by @@, to make GitHub's grammar happy.
|
||||
// https://github.com/textmate/diff.tmbundle/blob/0593bb775eab1824af97ef2172fd38822abd97d7/Syntaxes/Diff.plist#L68
|
||||
return fmt.Sprintf("@@= skipped -%d, +%d lines =@@", aDiff, bDiff)
|
||||
})
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
var fixUnifiedDiff = regexp.MustCompile(`@@ -\d+,\d+ \+\d+,\d+ @@`)
|
||||
|
||||
func RunAgainstSubmodule(t *testing.T, fileName string, actual string, opts Options) {
|
||||
// Record this baseline for tracking unused baselines
|
||||
recordBaseline(t, filepath.Join(opts.Subfolder, fileName))
|
||||
|
||||
local := filepath.Join(localRoot, opts.Subfolder, fileName)
|
||||
reference := filepath.Join(submoduleReferenceRoot, opts.Subfolder, fileName)
|
||||
writeComparison(t, actual, local, reference, true)
|
||||
}
|
||||
|
||||
func writeComparison(t *testing.T, actualContent string, local, reference string, comparingAgainstSubmodule bool) {
|
||||
if actualContent == "" {
|
||||
panic("the generated content was \"\". Return 'baseline.NoContent' if no baselining is required.")
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(local), 0o755); err != nil {
|
||||
t.Error(fmt.Errorf("failed to create directories for the local baseline file %s: %w", local, err))
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := os.Stat(local); err == nil {
|
||||
if err := os.Remove(local); err != nil {
|
||||
t.Error(fmt.Errorf("failed to remove the local baseline file %s: %w", local, err))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
expected := NoContent
|
||||
foundExpected := false
|
||||
if content, err := os.ReadFile(reference); err == nil {
|
||||
expected = string(content)
|
||||
foundExpected = true
|
||||
}
|
||||
|
||||
if expected != actualContent || actualContent == NoContent && foundExpected {
|
||||
if actualContent == NoContent {
|
||||
if err := os.WriteFile(local+".delete", []byte{}, 0o644); err != nil {
|
||||
t.Error(fmt.Errorf("failed to write the local baseline file %s: %w", local+".delete", err))
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if err := os.WriteFile(local, []byte(actualContent), 0o644); err != nil {
|
||||
t.Error(fmt.Errorf("failed to write the local baseline file %s: %w", local, err))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := os.Stat(reference); err != nil {
|
||||
if comparingAgainstSubmodule {
|
||||
t.Errorf("the baseline file %s does not exist in the TypeScript submodule", reference)
|
||||
} else {
|
||||
t.Errorf("new baseline created at %s.", local)
|
||||
}
|
||||
} else if comparingAgainstSubmodule {
|
||||
t.Errorf("the baseline file %s does not match the reference in the TypeScript submodule", reference)
|
||||
} else {
|
||||
t.Errorf("the baseline file %s has changed. (Run `hereby baseline-accept` if the new baseline is correct.)", reference)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
localRoot = filepath.Join(repo.TestDataPath(), "baselines", "local")
|
||||
referenceRoot = filepath.Join(repo.TestDataPath(), "baselines", "reference")
|
||||
submoduleReferenceRoot = filepath.Join(repo.TypeScriptSubmodulePath(), "tests", "baselines", "reference")
|
||||
)
|
||||
25
tools/tsgo/internal/testutil/baseline/baseline_test.go
Normal file
25
tools/tsgo/internal/testutil/baseline/baseline_test.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package baseline
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSubmoduleAcceptedFilesExist(t *testing.T) {
|
||||
t.Parallel()
|
||||
for name := range submoduleAcceptedFileNames().Keys() {
|
||||
if _, err := os.Stat(filepath.Join(referenceRoot, "submoduleAccepted", name)); err != nil {
|
||||
t.Errorf("submoduleAccepted.txt references %q, but the baseline file does not exist", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubmoduleTriagedFilesExist(t *testing.T) {
|
||||
t.Parallel()
|
||||
for name := range submoduleTriagedFileNames().Keys() {
|
||||
if _, err := os.Stat(filepath.Join(referenceRoot, "submoduleTriaged", name)); err != nil {
|
||||
t.Errorf("submoduleTriaged.txt references %q, but the baseline file does not exist", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
103
tools/tsgo/internal/testutil/baseline/testmain.go
Normal file
103
tools/tsgo/internal/testutil/baseline/testmain.go
Normal file
@@ -0,0 +1,103 @@
|
||||
package baseline
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"hash/fnv"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/collections"
|
||||
)
|
||||
|
||||
var (
|
||||
// recordedBaselines tracks all baseline file paths that were written during the test run.
|
||||
recordedBaselines collections.SyncSet[string]
|
||||
|
||||
// trackingInitialized is set to true when Track() is called.
|
||||
trackingInitialized bool
|
||||
|
||||
// trackingDir is the directory where tracking files should be written.
|
||||
// If non-empty, baseline tracking is enabled.
|
||||
// Set by Herebyfile.mjs when running full test suites with tracking enabled.
|
||||
trackingDir = os.Getenv("TSGO_BASELINE_TRACKING_DIR")
|
||||
)
|
||||
|
||||
// Track sets up baseline tracking and returns a cleanup function that writes the tracking file.
|
||||
// It should be called from TestMain using defer:
|
||||
//
|
||||
// func TestMain(m *testing.M) {
|
||||
// defer baseline.Track()()
|
||||
// m.Run()
|
||||
// }
|
||||
func Track() func() {
|
||||
trackingInitialized = true
|
||||
|
||||
if trackingDir == "" {
|
||||
return func() {}
|
||||
}
|
||||
|
||||
// Hash the entire call stack to create a unique filename per calling package.
|
||||
// This must be done in Track(), not in the deferred cleanup, because
|
||||
// the deferred function's call stack won't include the caller's info.
|
||||
var pcs [32]uintptr
|
||||
n := runtime.Callers(2, pcs[:]) // Skip Track and runtime.Callers
|
||||
h := fnv.New64a()
|
||||
frames := runtime.CallersFrames(pcs[:n])
|
||||
for {
|
||||
frame, more := frames.Next()
|
||||
h.Write([]byte(frame.File))
|
||||
if !more {
|
||||
break
|
||||
}
|
||||
}
|
||||
trackingPath := filepath.Join(trackingDir, fmt.Sprintf("%016x.txt", h.Sum64()))
|
||||
|
||||
return func() {
|
||||
// After tests complete, write the recorded baselines
|
||||
writeRecordedBaselines(trackingPath)
|
||||
}
|
||||
}
|
||||
|
||||
// recordBaseline adds a baseline file path to the recorded set.
|
||||
// The path should be relative to the baselines/reference directory.
|
||||
func recordBaseline(t testing.TB, relativePath string) {
|
||||
if trackingDir != "" {
|
||||
if !trackingInitialized {
|
||||
t.Error("baseline: package uses baselines but TestMain did not call baseline.Track(). " +
|
||||
"Please add a TestMain function with: defer baseline.Track()()")
|
||||
return
|
||||
}
|
||||
recordedBaselines.Add(relativePath)
|
||||
}
|
||||
}
|
||||
|
||||
// writeRecordedBaselines writes the list of recorded baseline files to a tracking file.
|
||||
func writeRecordedBaselines(trackingPath string) {
|
||||
if recordedBaselines.Size() == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if err := doWriteRecordedBaselines(trackingPath); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "baseline: failed to write tracking file %s: %v\n", trackingPath, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func doWriteRecordedBaselines(trackingPath string) error {
|
||||
f, err := os.Create(trackingPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
w := bufio.NewWriter(f)
|
||||
for baseline := range recordedBaselines.Keys() {
|
||||
if _, err := fmt.Fprintln(w, baseline); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return w.Flush()
|
||||
}
|
||||
Reference in New Issue
Block a user