2025-10-15 10:12:44 +03:00

212 lines
6.7 KiB
Go

package baseline
import (
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
"sync"
"testing"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/collections"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/core"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/repo"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/stringutil"
"github.com/peter-evans/patience"
)
type Options struct {
Subfolder string
IsSubmodule bool
IsSubmoduleAccepted bool
DiffFixupOld 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)
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"
)
diffFileName := fileName + ".diff"
isSubmoduleAccepted := opts.IsSubmoduleAccepted || submoduleAcceptedFileNames().Has(origSubfolder+"/"+diffFileName)
outRoot := core.IfElse(isSubmoduleAccepted, submoduleAcceptedFolder, submoduleFolder)
unusedOutRoot := core.IfElse(isSubmoduleAccepted, submoduleFolder, submoduleAcceptedFolder)
{
localPath := filepath.Join(localRoot, outRoot, origSubfolder, diffFileName)
referencePath := filepath.Join(referenceRoot, outRoot, origSubfolder, diffFileName)
diff := getBaselineDiff(t, actual, submoduleExpected, fileName, opts.DiffFixupOld)
writeComparison(t, diff, localPath, referencePath, false)
}
// Delete the other diff file if it exists
{
localPath := filepath.Join(localRoot, unusedOutRoot, origSubfolder, diffFileName)
referencePath := filepath.Join(referenceRoot, unusedOutRoot, origSubfolder, diffFileName)
writeComparison(t, NoContent, localPath, referencePath, false)
}
}
var submoduleAcceptedFileNames = sync.OnceValue(func() *collections.Set[string] {
var set collections.Set[string]
submoduleAccepted := filepath.Join(repo.TestDataPath, "submoduleAccepted.txt")
if content, err := os.ReadFile(submoduleAccepted); 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 submodule accepted file: %v", 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) string {
if fixupOld != nil {
expected = fixupOld(expected)
}
if actual == expected {
return NoContent
}
s := DiffText("old."+fileName, "new."+fileName, expected, actual)
// 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) {
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")
)