From dd2e03a5df16161639ea89a1cf1bf65b276df996 Mon Sep 17 00:00:00 2001 From: Marcel van Lohuizen Date: Wed, 15 Apr 2026 12:08:43 +0200 Subject: [PATCH] internal/cuetxtar: add txtar test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Go tests are rather hard to read and thus it is easy to miss whether they actually cover issues. We add a txtar suite for testing update and force modes as well as being able to test *negative* tests and error reporting. The idea is that most Go tests can move to this framework. We just add tests for core functionality now, but intend convert more down the line. NOTE: looked into using the fs.FS functionality of txtar, but most of the code modifies the archives, So it wasn't all that beneficial. Signed-off-by: Marcel van Lohuizen Change-Id: Ib6ddf804a3801b60ed446bb5e49982f60a9306cb Reviewed-on: https://cue.gerrithub.io/c/cue-lang/cue/+/1235495 TryBot-Result: CUEcueckoo Unity-Result: CUE porcuepine Reviewed-by: Daniel Martí --- internal/cuetxtar/inline.go | 59 ++- internal/cuetxtar/inlinetxtar_test.go | 403 ++++++++++++++++++ internal/cuetxtar/permute.go | 6 +- internal/cuetxtar/testdata/inline/fail.txtar | 8 + .../testdata/inline/pos_correct.txtar | 22 + .../cuetxtar/testdata/inline/pos_fill.txtar | 43 ++ .../testdata/inline/pos_force_overwrite.txtar | 41 ++ internal/cuetxtar/testdata/inline/seed.txtar | 6 + 8 files changed, 580 insertions(+), 8 deletions(-) create mode 100644 internal/cuetxtar/inlinetxtar_test.go create mode 100644 internal/cuetxtar/testdata/inline/fail.txtar create mode 100644 internal/cuetxtar/testdata/inline/pos_correct.txtar create mode 100644 internal/cuetxtar/testdata/inline/pos_fill.txtar create mode 100644 internal/cuetxtar/testdata/inline/pos_force_overwrite.txtar create mode 100644 internal/cuetxtar/testdata/inline/seed.txtar diff --git a/internal/cuetxtar/inline.go b/internal/cuetxtar/inline.go index b4af9b88a..27006e95c 100644 --- a/internal/cuetxtar/inline.go +++ b/internal/cuetxtar/inline.go @@ -130,6 +130,13 @@ func NewInlineRunner(t *testing.T, m *cuetdtest.M, archive *txtar.Archive, dir s return &InlineRunner{r: &inlineRunner{t: t, m: m, archive: archive, dir: dir}} } +// NewInlineRunnerCapture creates an InlineRunner that captures assertion +// failures into cap without propagating them to t. Infrastructure errors +// (compile errors, write-back failures) still propagate through t. +func NewInlineRunnerCapture(t *testing.T, m *cuetdtest.M, archive *txtar.Archive, dir string, cap *FailCapture) *InlineRunner { + return &InlineRunner{r: &inlineRunner{t: t, m: m, archive: archive, dir: dir, errSink: cap}} +} + // Run executes all inline test cases in the archive. func (ir *InlineRunner) Run() { ir.r.runArchive() @@ -141,6 +148,7 @@ func (ir *InlineRunner) Run() { // inlineRunner handles execution of a single txtar archive in inline mode. type inlineRunner struct { t *testing.T + errSink testing.TB // if non-nil, assertion errors go here instead of the sub-test t m *cuetdtest.M archive *txtar.Archive dir string @@ -157,6 +165,24 @@ type inlineRunner struct { pendingInlineFillWrites []inlineFillWrite } +// sinkOrSub returns the errSink if set, otherwise the given sub-test t. +// Used in runArchive to route assertion errors in capture mode. +func (r *inlineRunner) sinkOrSub(sub *testing.T) testing.TB { + if r.errSink != nil { + return r.errSink + } + return sub +} + +// sinkOrT returns the errSink if set, otherwise r.t. +// Used for assertions that run outside a per-root sub-test. +func (r *inlineRunner) sinkOrT() testing.TB { + if r.errSink != nil { + return r.errSink + } + return r.t +} + // failCapture wraps *testing.T and captures failures without propagating them. // It is used for @test(todo) XFAIL mode: all directives run, but failures are // logged rather than reported as test errors. @@ -180,6 +206,29 @@ func (c *failCapture) Errorf(format string, args ...any) { fmt.Fprintf(&c.msgs, format+"\n", args...) } +// FailCapture wraps a testing.TB and captures Error/Errorf calls without +// propagating failures to the testing framework. All other testing.TB methods +// delegate to the embedded TB. Use with NewInlineRunnerCapture. +type FailCapture struct { + testing.TB + msgs strings.Builder +} + +func (c *FailCapture) Error(args ...any) { + fmt.Fprintln(&c.msgs, args...) +} + +func (c *FailCapture) Errorf(format string, args ...any) { + fmt.Fprintf(&c.msgs, format, args...) + fmt.Fprintln(&c.msgs) +} + +// Failed reports whether any errors were captured. +func (c *FailCapture) Failed() bool { return c.msgs.Len() > 0 } + +// Messages returns the accumulated error text. +func (c *FailCapture) Messages() string { return c.msgs.String() } + // runDirectivesForPath runs all directives for a single path, handling // @test(skip) and @test(todo) according to their semantics: // - skip directives fire first so t.Skip() is called before other assertions @@ -187,7 +236,7 @@ func (c *failCapture) Errorf(format string, args ...any) { // // path is used both as the argument to runDirective and in log messages; // an empty path is displayed as "(file level)". -func (r *inlineRunner) runDirectivesForPath(t *testing.T, path cue.Path, val cue.Value, directives []parsedTestAttr) { +func (r *inlineRunner) runDirectivesForPath(t testing.TB, path cue.Path, val cue.Value, directives []parsedTestAttr) { for _, pa := range directives { if pa.directive == "skip" { r.runDirective(t, path, val, pa) @@ -301,7 +350,7 @@ func (r *inlineRunner) runArchive() { } seenFilePaths[rec.path.String()] = true directives := selectActiveDirectives(allRecords, rec.path, version) - r.runDirectivesForPath(r.t, cue.Path{}, val, directives) + r.runDirectivesForPath(r.sinkOrT(), cue.Path{}, val, directives) } } @@ -331,7 +380,7 @@ func (r *inlineRunner) runArchive() { root := root name := r.subTestName(root, allRecords) r.t.Run(name, func(t *testing.T) { - r.runInline(t, root, val, allRecords) + r.runInline(r.sinkOrSub(t), root, val, allRecords) }) } @@ -341,7 +390,7 @@ func (r *inlineRunner) runArchive() { // over all records and check after all subtests run. version := r.versionName() if fileShareGroups := r.collectDirectShareIDs(allRecords, version); len(fileShareGroups) > 0 { - r.runShareIDChecks(r.t, val, fileShareGroups) + r.runShareIDChecks(r.sinkOrT(), val, fileShareGroups) } // After all subtests complete, write back any pending updates. @@ -631,7 +680,7 @@ func selectActiveDirectives(records []attrRecord, path cue.Path, version string) // ───────────────────────────────────────────────────────────────────────────── // runInline runs assertions for an inline-form test-case root. -func (r *inlineRunner) runInline(t *testing.T, root testCaseRoot, fileVal cue.Value, records []attrRecord) { +func (r *inlineRunner) runInline(t testing.TB, root testCaseRoot, fileVal cue.Value, records []attrRecord) { t.Helper() version := r.versionName() diff --git a/internal/cuetxtar/inlinetxtar_test.go b/internal/cuetxtar/inlinetxtar_test.go new file mode 100644 index 000000000..cd6bc5bd3 --- /dev/null +++ b/internal/cuetxtar/inlinetxtar_test.go @@ -0,0 +1,403 @@ +// Copyright 2026 CUE Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cuetxtar_test + +// This file tests the CUE_UPDATE=1 / CUE_UPDATE=force write-back behavior of +// the inline runner. Each testdata/inline/*.txtar file describes an update +// scenario: the in/ sections are the input, out/run/errors.txt captures errors +// from a plain run (no update), out/update/ sections are the expected state +// after two CUE_UPDATE=1 passes (for idempotency; omitted when identical to +// input), out/force/ sections are the expected state after CUE_UPDATE=force +// (omitted when identical to update), and out/status.txt records which results +// were identical (proof that the framework actually ran). +// +// CUE_UPDATE=1 adds missing out/ sections (for new tests) but validates +// existing ones without overwriting them. CUE_UPDATE=force fully regenerates +// all out/ sections, and is required when existing section content changes. + +import ( + "bytes" + "fmt" + "os" + "path/filepath" + "slices" + "strings" + "testing" + + "golang.org/x/tools/txtar" + + "cuelang.org/go/internal/cuetest" + "cuelang.org/go/internal/cuetxtar" +) + +// TestInlineUpdate runs txtar-driven update tests from testdata/inline/. +// Each test verifies that running the inline runner with CUE_UPDATE=1 and/or +// CUE_UPDATE=force produces the expected output, and that a second update pass +// is idempotent. +// +// Testdata format: +// +// -- in/test.cue -- +// ... CUE source with @test directives that may need updating ... +// +// -- out/run/errors.txt -- +// ... errors from a plain run (no update); absent if the run passes ... +// +// -- out/update/test.cue -- +// ... expected source after two CUE_UPDATE=1 passes (idempotent); +// ... absent when update produces no change (i.e. identical to input) ... +// +// -- out/force/test.cue -- +// ... expected source after CUE_UPDATE=force; absent when identical to update ... +// +// -- out/status.txt -- +// ... "update: identical" and/or "force: identical" lines (proof of run) ... +func TestInlineUpdate(t *testing.T) { + matches, err := filepath.Glob("testdata/inline/*.txtar") + if err != nil { + t.Fatal(err) + } + if len(matches) == 0 { + t.Fatal("testdata/inline: no *.txtar files found") + } + for _, path := range matches { + name := strings.TrimSuffix(filepath.Base(path), ".txtar") + t.Run(name, func(t *testing.T) { + runUpdateTest(t, path) + }) + } +} + +func runUpdateTest(t *testing.T, filePath string) { + t.Helper() + outer, err := txtar.ParseFile(filePath) + if err != nil { + t.Fatalf("parse %s: %v", filePath, err) + } + + // Build input archive from in/ sections. + inputArchive := extractTxtarSections(outer, "in/") + if len(inputArchive.Files) == 0 { + t.Fatalf("%s: no in/ sections", filePath) + } + + // ctx holds the values that are constant across all applySection calls. + ctx := §ionCtx{ + t: t, + outerForce: cuetest.ForceUpdateGoldenFiles, + outerUpdate: cuetest.UpdateGoldenFiles, + outer: outer, + filePath: filePath, + } + // --- Plain run (no update mode) --- + + capR := &cuetxtar.FailCapture{TB: t} + withUpdateMode(false, false, func() { + runner := cuetxtar.NewInlineRunnerCapture(t, nil, cloneTxtarArchive(inputArchive), t.TempDir(), capR) + runner.Run() + }) + runErrors := capR.Messages() + ctx.apply(section{ + prefix: "out/run/errors.txt", + content: singleFile(runErrors), + shouldExist: runErrors != "", + }) + + // --- Regular update (CUE_UPDATE=1) --- + + // Pass 1: run with update mode enabled; capture assertion errors. + update1 := cloneTxtarArchive(inputArchive) + cap1 := &cuetxtar.FailCapture{TB: t} + withUpdateMode(true, false, func() { + runner := cuetxtar.NewInlineRunnerCapture(t, nil, update1, t.TempDir(), cap1) + runner.Run() + }) + + // Pass 2: run again from the pass-1 result to verify idempotency. + update2 := cloneTxtarArchive(update1) + cap2 := &cuetxtar.FailCapture{TB: t} + withUpdateMode(true, false, func() { + runner := cuetxtar.NewInlineRunnerCapture(t, nil, update2, t.TempDir(), cap2) + runner.Run() + }) + + // Both passes must produce the same archive and errors. + if diff := txtarFileDiff(update1, update2); diff != "" { + t.Errorf("CUE_UPDATE=1 is not idempotent (archive):\n%s", diff) + } + if cap1.Messages() != cap2.Messages() { + t.Errorf("CUE_UPDATE=1 is not idempotent (errors):\ngot (pass1):\n%swant (pass2):\n%s", + cap1.Messages(), cap2.Messages()) + } + + // out/update/ sections: omitted when update is identical to input. + updateIdentical := txtarFileDiff(inputArchive, update1) == "" + ctx.apply(section{ + prefix: "out/update/", + content: update1, + shouldExist: !updateIdentical, + staleMsg: "out/update/ sections present but update produces same result as input (run with CUE_UPDATE=1 to remove)", + }) + + // --- Force update (CUE_UPDATE=force) --- + // out/force/ sections: present only when force produces a different result than update. + + forceArchive := cloneTxtarArchive(inputArchive) + capF := &cuetxtar.FailCapture{TB: t} + withUpdateMode(true, true, func() { + runner := cuetxtar.NewInlineRunnerCapture(t, nil, forceArchive, t.TempDir(), capF) + runner.Run() + }) + forceDiffers := txtarFileDiff(update1, forceArchive) != "" || cap1.Messages() != capF.Messages() + forceContent := cloneTxtarArchive(forceArchive) + if msg := capF.Messages(); msg != "" { + forceContent.Files = append(forceContent.Files, txtar.File{Name: "errors.txt", Data: []byte(msg)}) + } + ctx.apply(section{ + prefix: "out/force/", + content: forceContent, + shouldExist: forceDiffers, + staleMsg: "out/force/ sections present but force produces same result as update (run with CUE_UPDATE=1 to remove)", + }) + + // --- Status summary --- + // out/status.txt records which results were identical to their predecessor, + // serving as proof that the framework actually exercised those paths. + var statusLines []string + if updateIdentical { + statusLines = append(statusLines, "update: identical to input") + } + if !forceDiffers { + statusLines = append(statusLines, "force: identical to update") + } + var statusContent string + if len(statusLines) > 0 { + statusContent = strings.Join(statusLines, "\n") + "\n" + } + ctx.apply(section{ + prefix: "out/status.txt", + content: singleFile(statusContent), + shouldExist: statusContent != "", + }) + + // Write the outer testdata file if any sections were updated. + if ctx.modified { + if err := os.WriteFile(filePath, txtar.Format(outer), 0o644); err != nil { + t.Errorf("update %s: %v", filePath, err) + } + } +} + +// singleFile wraps content in a single-file archive with an empty file name. +// Used for sections where the prefix is the full file name (e.g. "out/status.txt"). +func singleFile(content string) *txtar.Archive { + return &txtar.Archive{Files: []txtar.File{{Data: []byte(content)}}} +} + +// sectionCtx holds the values that are constant across all apply calls within +// a single runUpdateTest invocation, and accumulates whether any section was +// modified. +type sectionCtx struct { + t *testing.T + outerForce bool + outerUpdate bool + outer *txtar.Archive + filePath string + modified bool +} + +// section describes one output section group to manage. +type section struct { + prefix string + content *txtar.Archive + shouldExist bool + staleMsg string // optional; ctx.filePath is prepended automatically +} + +// apply manages the lifecycle of one output section group in ctx.outer and +// sets ctx.modified if the archive was changed. +// +// - outerForce: write if shouldExist, remove otherwise; always sets modified. +// - outerUpdate: write if missing and shouldExist; validate if present and +// shouldExist; remove if present but !shouldExist. +// - default: validate if shouldExist; log staleMsg (if non-empty) when the +// section exists but should not. +func (ctx *sectionCtx) apply(s section) { + ctx.t.Helper() + exists := hasTxtarSectionPrefix(ctx.outer, s.prefix) + switch { + case ctx.outerForce: + if s.shouldExist { + replaceTxtarSections(ctx.outer, s.prefix, s.content) + } else { + removeTxtarSectionPrefix(ctx.outer, s.prefix) + } + ctx.modified = true + case ctx.outerUpdate: + switch { + case s.shouldExist && !exists: + replaceTxtarSections(ctx.outer, s.prefix, s.content) + ctx.modified = true + case s.shouldExist && exists: + compareTxtarSections(ctx.t, ctx.outer, s.prefix, s.content, ctx.filePath) + case !s.shouldExist && exists: + removeTxtarSectionPrefix(ctx.outer, s.prefix) + ctx.modified = true + } + default: + if s.shouldExist { + compareTxtarSections(ctx.t, ctx.outer, s.prefix, s.content, ctx.filePath) + } else if exists && s.staleMsg != "" { + ctx.t.Error(ctx.filePath + ": " + s.staleMsg) + } + } +} + +// withUpdateMode temporarily sets cuetest.UpdateGoldenFiles and +// cuetest.ForceUpdateGoldenFiles for the duration of fn, then restores them. +func withUpdateMode(update, force bool, fn func()) { + origUpdate := cuetest.UpdateGoldenFiles + origForce := cuetest.ForceUpdateGoldenFiles + defer func() { + cuetest.UpdateGoldenFiles = origUpdate + cuetest.ForceUpdateGoldenFiles = origForce + }() + cuetest.UpdateGoldenFiles = update + cuetest.ForceUpdateGoldenFiles = force + fn() +} + +// extractTxtarSections returns a new archive built from files in ar whose names +// start with the given prefix; the prefix is stripped from each file name. +func extractTxtarSections(ar *txtar.Archive, prefix string) *txtar.Archive { + var files []txtar.File + for _, f := range ar.Files { + if name, ok := strings.CutPrefix(f.Name, prefix); ok { + files = append(files, txtar.File{Name: name, Data: bytes.Clone(f.Data)}) + } + } + return &txtar.Archive{Files: files} +} + +// cloneTxtarArchive returns a deep copy of ar. +func cloneTxtarArchive(ar *txtar.Archive) *txtar.Archive { + clone := &txtar.Archive{Comment: bytes.Clone(ar.Comment)} + for _, f := range ar.Files { + clone.Files = append(clone.Files, txtar.File{Name: f.Name, Data: bytes.Clone(f.Data)}) + } + return clone +} + +// hasTxtarSectionPrefix reports whether ar has any file whose name starts with prefix. +func hasTxtarSectionPrefix(ar *txtar.Archive, prefix string) bool { + return slices.ContainsFunc(ar.Files, func(f txtar.File) bool { + return strings.HasPrefix(f.Name, prefix) + }) +} + +// removeTxtarSectionPrefix removes all files from ar whose names start with prefix. +func removeTxtarSectionPrefix(ar *txtar.Archive, prefix string) { + ar.Files = slices.DeleteFunc(ar.Files, func(f txtar.File) bool { + return strings.HasPrefix(f.Name, prefix) + }) +} + +// replaceTxtarSections sets all files under prefix in ar to match src: existing +// files with the prefix that are not in src are removed, files that are in src +// are updated in place, and new src files are appended at the end. +func replaceTxtarSections(ar *txtar.Archive, prefix string, src *txtar.Archive) { + // Build the set of names (with prefix) that src provides. + srcByName := make(map[string][]byte, len(src.Files)) + for _, sf := range src.Files { + srcByName[prefix+sf.Name] = sf.Data + } + + var result []txtar.File + inserted := make(map[string]bool) + for _, f := range ar.Files { + if strings.HasPrefix(f.Name, prefix) { + if data, ok := srcByName[f.Name]; ok { + result = append(result, txtar.File{Name: f.Name, Data: data}) + inserted[f.Name] = true + } + // else: stale file — drop it + } else { + result = append(result, f) + } + } + // Append any src files not yet inserted (new sections with no prior entry). + for _, sf := range src.Files { + name := prefix + sf.Name + if !inserted[name] { + result = append(result, txtar.File{Name: name, Data: sf.Data}) + } + } + ar.Files = result +} + +// compareTxtarSections checks that files in src match the sections in ar that +// have the given prefix. hint is the file path used in error messages. +func compareTxtarSections(t *testing.T, ar *txtar.Archive, prefix string, src *txtar.Archive, hint string) { + t.Helper() + want := make(map[string]string) + for _, f := range ar.Files { + if name, ok := strings.CutPrefix(f.Name, prefix); ok { + want[name] = string(f.Data) + } + } + got := make(map[string]string) + for _, f := range src.Files { + got[f.Name] = string(f.Data) + } + for name, g := range got { + w, ok := want[name] + if !ok { + t.Errorf("%s: %s%s: not in testdata (run with CUE_UPDATE=1 to add)\ngot:\n%s", hint, prefix, name, g) + continue + } + if g != w { + t.Errorf("%s: %s%s: mismatch\ngot:\n%swant:\n%s", hint, prefix, name, g, w) + } + } + for name := range want { + if _, ok := got[name]; !ok { + t.Errorf("%s: %s%s: in testdata but not produced by runner", hint, prefix, name) + } + } +} + +// txtarFileDiff returns a human-readable description of file-by-file differences +// between archive a and archive b. Returns "" if they are identical. +func txtarFileDiff(a, b *txtar.Archive) string { + aMap := make(map[string]string) + for _, f := range a.Files { + aMap[f.Name] = string(f.Data) + } + var diffs []string + for _, f := range b.Files { + ac, ok := aMap[f.Name] + if !ok { + diffs = append(diffs, fmt.Sprintf("file %s: only in second archive", f.Name)) + } else if ac != string(f.Data) { + diffs = append(diffs, fmt.Sprintf("file %s: differs\ngot (pass1):\n%swant (pass2):\n%s", + f.Name, ac, string(f.Data))) + } + delete(aMap, f.Name) + } + for name := range aMap { + diffs = append(diffs, fmt.Sprintf("file %s: only in first archive", name)) + } + return strings.Join(diffs, "\n") +} diff --git a/internal/cuetxtar/permute.go b/internal/cuetxtar/permute.go index 203a627ad..d12e0105a 100644 --- a/internal/cuetxtar/permute.go +++ b/internal/cuetxtar/permute.go @@ -37,7 +37,7 @@ import ( // // 2. Decl attribute: @test(permute) as a declaration inside a struct means // "permute all fields within this struct." -func (r *inlineRunner) runInlinePermutes(t *testing.T, rootPath cue.Path, records []attrRecord, version string) { +func (r *inlineRunner) runInlinePermutes(t testing.TB, rootPath cue.Path, records []attrRecord, version string) { type permuteGroup struct { parentPath cue.Path fields []string // nil means permute all fields @@ -113,7 +113,7 @@ func (r *inlineRunner) runInlinePermutes(t *testing.T, rootPath cue.Path, record // checkPermuteCount verifies or auto-updates a @test(permuteCount, N) directive // at path after all permutations for a group have run. When CUE_UPDATE=1, the // count is filled or replaced with the actual value. -func (r *inlineRunner) checkPermuteCount(t *testing.T, path cue.Path, records []attrRecord, version string, actualCount int) { +func (r *inlineRunner) checkPermuteCount(t testing.TB, path cue.Path, records []attrRecord, version string, actualCount int) { t.Helper() directives := selectActiveDirectives(records, path, version) for _, pa := range directives { @@ -151,7 +151,7 @@ func (r *inlineRunner) checkPermuteCount(t *testing.T, path cue.Path, records [] // Uses Heap's algorithm to enumerate permutations without allocating N! slices. // Returns the total number of permutations evaluated (N!), or 0 if skipped // (fewer than 2 permutable fields). -func (r *inlineRunner) runPermuteAssertion(t *testing.T, structPath cue.Path, fieldNames []string) int { +func (r *inlineRunner) runPermuteAssertion(t testing.TB, structPath cue.Path, fieldNames []string) int { t.Helper() if r.cueFiles == nil { return 0 diff --git a/internal/cuetxtar/testdata/inline/fail.txtar b/internal/cuetxtar/testdata/inline/fail.txtar new file mode 100644 index 000000000..b75471a41 --- /dev/null +++ b/internal/cuetxtar/testdata/inline/fail.txtar @@ -0,0 +1,8 @@ +-- in/test.cue -- +a: 1 @test(eq, 100) +-- out/run/errors.txt -- +path a: expected 100, got 1 +-- out/force/test.cue -- +a: 1 @test(eq, 1) +-- out/status.txt -- +update: identical to input diff --git a/internal/cuetxtar/testdata/inline/pos_correct.txtar b/internal/cuetxtar/testdata/inline/pos_correct.txtar new file mode 100644 index 000000000..5c3eb8393 --- /dev/null +++ b/internal/cuetxtar/testdata/inline/pos_correct.txtar @@ -0,0 +1,22 @@ +# Tests that a test with already-correct positions is left unchanged by both +# CUE_UPDATE=1 and CUE_UPDATE=force. Covers different @test(err) placement forms: +# field attribute, nested field inside struct, and nested inside @test(eq). +-- in/test.cue -- +x: 100 & 200 @test(err, code=eval, pos=[0:4, 0:10]) + +y: { + b: 100 & 200 @test(err, code=eval, pos=[0:5, 0:11]) +} + +z: { + b: int + c: b & "foo" + + @test(eq, { + b: int + c: _|_ @test(err, code=eval, pos=[8:5, 9:5, 9:9]) + }) +} +-- out/status.txt -- +update: identical to input +force: identical to update diff --git a/internal/cuetxtar/testdata/inline/pos_fill.txtar b/internal/cuetxtar/testdata/inline/pos_fill.txtar new file mode 100644 index 000000000..9073c3758 --- /dev/null +++ b/internal/cuetxtar/testdata/inline/pos_fill.txtar @@ -0,0 +1,43 @@ +# Tests that pos=[] placeholders are filled with actual error positions on +# CUE_UPDATE=1. The input has a conflict error (1 & 2) with an empty pos=[] +# placeholder; after update, the placeholder is replaced with the two source +# positions of the conflicting values. Force update (CUE_UPDATE=force) also +# fills placeholders, producing the same result. +-- in/test.cue -- +x: 100 & 200 @test(err, code=eval, pos=[]) + +y: { + b: 100 & 200 @test(err, code=eval, pos=[]) +} + +z: { + b: int + c: b & "foo" + + @test(eq, { + b: int + c: _|_ @test(err, code=eval, pos=[]) + }) +} +-- out/run/errors.txt -- +path x: @test(err, pos=...): got 2 position(s), want 0; extra positions are often acceptable, and after confirming they are relevant to this error you can add them to pos=[...] +path y.b: @test(err, pos=...): got 2 position(s), want 0; extra positions are often acceptable, and after confirming they are relevant to this error you can add them to pos=[...] +path z: c: @test(err, pos=...): got 3 position(s), want 0; extra positions are often acceptable, and after confirming they are relevant to this error you can add them to pos=[...] [8:5 9:5 9:9] +-- out/update/test.cue -- +x: 100 & 200 @test(err, code=eval, pos=[0:4, 0:10]) + +y: { + b: 100 & 200 @test(err, code=eval, pos=[0:5, 0:11]) +} + +z: { + b: int + c: b & "foo" + + @test(eq, { + b: int + c: _|_ @test(err, code=eval, pos=[8:5, 9:5, 9:9]) + }) +} +-- out/status.txt -- +force: identical to update diff --git a/internal/cuetxtar/testdata/inline/pos_force_overwrite.txtar b/internal/cuetxtar/testdata/inline/pos_force_overwrite.txtar new file mode 100644 index 000000000..b998d3b8a --- /dev/null +++ b/internal/cuetxtar/testdata/inline/pos_force_overwrite.txtar @@ -0,0 +1,41 @@ +# Tests that CUE_UPDATE=force overwrites wrong (non-placeholder) positions, while +# CUE_UPDATE=1 leaves them unchanged and reports a position mismatch error. +# The input has pos=[0:4] but the error has two positions; force update corrects it. +-- in/test.cue -- +x: 100 & 200 @test(err, code=eval, pos=[0:4, 0:999999]) + +y: { + b: 100 & 200 @test(err, code=eval, pos=[0:5, 0:99999]) +} + +z: { + b: int + c: b & "foo" + + @test(eq, { + b: int + c: _|_ @test(err, code=eval, pos=[8:5, 9:5, 9:99999]) + }) +} +-- out/run/errors.txt -- +path x: @test(err, pos=...): unmatched position 1:999999 (spec 0:999999); actual positions: +path y.b: @test(err, pos=...): unmatched position 4:99999 (spec 0:99999); actual positions: +path z: c: @test(err, pos=...): no match for expected position 9:99999 in [8:5 9:5 9:9] +-- out/status.txt -- +update: identical to input +-- out/force/test.cue -- +x: 100 & 200 @test(err, code=eval, pos=[0:4, 0:10]) + +y: { + b: 100 & 200 @test(err, code=eval, pos=[0:5, 0:11]) +} + +z: { + b: int + c: b & "foo" + + @test(eq, { + b: int + c: _|_ @test(err, code=eval, pos=[8:5, 9:5, 9:9]) + }) +} diff --git a/internal/cuetxtar/testdata/inline/seed.txtar b/internal/cuetxtar/testdata/inline/seed.txtar new file mode 100644 index 000000000..26f215831 --- /dev/null +++ b/internal/cuetxtar/testdata/inline/seed.txtar @@ -0,0 +1,6 @@ +-- in/test.cue -- +a: 1 @test(eq) +-- out/update/test.cue -- +a: 1 @test(eq, 1) +-- out/status.txt -- +force: identical to update -- 2.51.2