From f679d39f4f5c4a5df326d41dada80da95f94f586 Mon Sep 17 00:00:00 2001 From: Marcel van Lohuizen Date: Tue, 28 Apr 2026 16:31:45 +0200 Subject: [PATCH] tools/fix: add inline @test framework MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add inline test framework in fixall_test.go that runs @test assertions on both pre-fix and post-fix archives to verify fixes preserve semantics. The idea: @test attributes get copied by the fix the the new files as is. By testing both in and out, we ensure that the code generated by fix is correct. Add @test(eq) assertions to aliasv2 for field aliases, value aliases, and __self handling for demonstration. Fix compile errors in aliasv2.txtar: add missing variable definitions (x, name) for dynamic fields, add references to unused aliases (Z, OptField, ReqField, X, DynInterpAlias), fix Inner.Leaf.value to Inner.deep.value. Signed-off-by: Marcel van Lohuizen Change-Id: Id935408d9665283df4020ded2ca9d30f29037a07 Reviewed-on: https://cue.gerrithub.io/c/cue-lang/cue/+/1236308 Reviewed-by: Daniel Martí TryBot-Result: CUEcueckoo Unity-Result: CUE porcuepine --- tools/fix/fixall_test.go | 172 +++++++++++++++++++++++++++++++ tools/fix/testdata/aliasv2.txtar | 109 +++++++++++--------- 2 files changed, 235 insertions(+), 46 deletions(-) diff --git a/tools/fix/fixall_test.go b/tools/fix/fixall_test.go index 9c834ca71..866d9eb81 100644 --- a/tools/fix/fixall_test.go +++ b/tools/fix/fixall_test.go @@ -15,13 +15,16 @@ package fix import ( + "bytes" "fmt" + "slices" "strings" "testing" "cuelang.org/go/cue/format" "cuelang.org/go/internal/cuetxtar" "cuelang.org/go/mod/modfile" + "golang.org/x/tools/txtar" ) func TestInstances(t *testing.T) { @@ -44,19 +47,188 @@ func TestInstances(t *testing.T) { } err := Instances(a, opts...) t.WriteErrors(err) + + // Collect formatted fixed files for golden output and inline testing. + fixedFiles := make(map[string][]byte) for _, b := range a { // Output module file if it exists and was potentially modified if b.ModuleFile != nil { if data, err := modfile.Format(b.ModuleFile); err == nil { fmt.Fprintln(t, "---", "cue.mod/module.cue") fmt.Fprint(t, string(data)) + fixedFiles["cue.mod/module.cue"] = data } } for _, f := range b.Files { b, _ := format.Node(f) fmt.Fprintln(t, "---", t.Rel(f.Filename)) fmt.Fprint(t, string(b)) + fixedFiles[t.Rel(f.Filename)] = b } } + + // If any input CUE file has @test annotations, verify that the + // assertions pass on both the original and the fixed output. + // This ensures the fix is semantics-preserving. + runInlineTests(t.T, t.Archive, t.Dir, fixedFiles) + }) +} + +// runInlineTests runs @test assertions on both the original archive and +// on a post-fix archive built from fixedFiles. Skipped if the archive +// has no @test annotations (making this a no-op for existing tests). +func runInlineTests(t *testing.T, archive *txtar.Archive, dir string, fixedFiles map[string][]byte) { + t.Helper() + + if !archiveHasTestAttrs(archive) { + return + } + + // Run @test assertions on the original (pre-fix) archive. + t.Run("pre-fix", func(t *testing.T) { + cap := &cuetxtar.FailCapture{TB: t} + runner := cuetxtar.NewInlineRunnerCapture(t, nil, archive, dir, cap) + runner.Run() + if cap.Failed() { + t.Errorf("@test assertions failed on original (pre-fix) input:\n%s", cap.Messages()) + } + }) + + // Build a post-fix archive with the fixed CUE files. + postFixArchive := buildPostFixArchive(archive, fixedFiles) + + // Run @test assertions on the fixed output. + t.Run("post-fix", func(t *testing.T) { + cap := &cuetxtar.FailCapture{TB: t} + runner := cuetxtar.NewInlineRunnerCapture(t, nil, postFixArchive, dir, cap) + runner.Run() + if cap.Failed() { + t.Errorf("@test assertions failed on fixed output:\n%s", cap.Messages()) + } + }) +} + +// TestFixSemanticsDetectsBreak verifies that runInlineTests catches a +// deliberately broken fix. It provides a pre-fix archive with @test +// annotations and a fixedFiles map where the __reclose wrapper is +// missing, then asserts that the post-fix @test assertions fail. +func TestFixSemanticsDetectsBreak(t *testing.T) { + // Pre-fix archive: old semantics (no explicitopen), embedding + // propagates closedness from #A. + archive := txtar.Parse([]byte(` +#no-coverage + +-- cue.mod/module.cue -- +module: "test.example" +language: version: "v0.15.0" + +-- in.cue -- +package foo + +#A: a: int + +X: { + #A + b: int +} + +tests: { + t1: err: X & {c: 1} @test(err, code=eval, contains="field not allowed", pos=[0:16]) +} +`)) + + dir := t.TempDir() + + // Correct fix: __reclose preserves closedness in new semantics. + t.Run("correct-fix", func(t *testing.T) { + correctFixed := map[string][]byte{ + "in.cue": []byte(`@experiment(explicitopen) + +package foo + +#A: a: int + +X: __reclose({ + #A... + b: int +}) + +tests: { + t1: err: X & {c: 1} @test(err, code=eval, contains="field not allowed", pos=[0:16]) +} +`), + } + runInlineTests(t, archive, dir, correctFixed) + }) + + // Broken fix: __reclose is missing, X becomes open, t1 should fail. + t.Run("broken-fix", func(t *testing.T) { + brokenFixed := map[string][]byte{ + "in.cue": []byte(`@experiment(explicitopen) + +package foo + +#A: a: int + +X: { + #A... + b: int +} + +tests: { + t1: err: X & {c: 1} @test(err, code=eval, contains="field not allowed", pos=[0:16]) +} +`), + } + + // We expect the post-fix sub-test to fail. Wrap in a helper + // that captures and verifies the failure. + var postFixFailed bool + t.Run("post-fix", func(t *testing.T) { + postFixArchive := buildPostFixArchive(archive, brokenFixed) + cap := &cuetxtar.FailCapture{TB: t} + runner := cuetxtar.NewInlineRunnerCapture(t, nil, postFixArchive, dir, cap) + runner.Run() + if cap.Failed() { + postFixFailed = true + t.Logf("correctly detected broken fix:\n%s", cap.Messages()) + } + }) + if !postFixFailed { + t.Fatal("expected broken fix to fail @test assertions, but it passed") + } + }) +} + +// archiveHasTestAttrs reports whether any CUE file in the archive contains +// an @test( attribute. +func archiveHasTestAttrs(a *txtar.Archive) bool { + for _, f := range a.Files { + if strings.HasSuffix(f.Name, ".cue") && bytes.Contains(f.Data, []byte("@test(")) { + return true + } + } + return false +} + +// buildPostFixArchive constructs a new txtar archive with the fixed CUE file +// contents, preserving all other files (module.cue, non-CUE files) and the +// archive comment. Output sections (out/*) are stripped since the inline +// runner doesn't need them. +func buildPostFixArchive(orig *txtar.Archive, fixedFiles map[string][]byte) *txtar.Archive { + result := &txtar.Archive{ + Comment: orig.Comment, + Files: slices.Clone(orig.Files), + } + // Replace CUE files and module.cue with fixed versions. + for i, f := range result.Files { + if data, ok := fixedFiles[f.Name]; ok { + result.Files[i] = txtar.File{Name: f.Name, Data: data} + } + } + // Strip out/* sections — they're for golden-file comparison, not evaluation. + result.Files = slices.DeleteFunc(result.Files, func(f txtar.File) bool { + return strings.HasPrefix(f.Name, "out/") }) + return result } diff --git a/tools/fix/testdata/aliasv2.txtar b/tools/fix/testdata/aliasv2.txtar index 816123634..59dc36d19 100644 --- a/tools/fix/testdata/aliasv2.txtar +++ b/tools/fix/testdata/aliasv2.txtar @@ -1,5 +1,6 @@ #exp:aliasv2 +#no-coverage -- cue.mod/module.cue -- module: "test.example" @@ -12,7 +13,7 @@ package foo // Simple field alias X=a: { foo: 1 - bar: X.foo + 2 + bar: X.foo + 2 @test(eq, 3) } // Multiple aliases @@ -20,8 +21,9 @@ Y=b: { data: 42 } Z=c: { - val: Y.data + val: Y.data @test(eq, 42) } +zref: Z.val @test(eq, 42) -- b.cue -- package foo @@ -31,9 +33,9 @@ outer: { Leaf=deep: { value: 1 } - ref: Leaf.value + ref: Leaf.value @test(eq, 1) } - ref2: Inner.Leaf.value + ref2: Inner.deep.value @test(eq, 1) } -- c.cue -- package foo @@ -42,10 +44,12 @@ package foo OptField=a?: { value: 1 } +optRef: OptField.value ReqField=b!: { value: 2 } +reqRef: ReqField.value -- d.cue -- // Already has postfix alias experiment, so do not do any changes. @experiment(aliasv2) @@ -55,6 +59,7 @@ package foo a~X: { foo: 1 } +ref: X.foo -- e.cue -- package foo @@ -73,6 +78,9 @@ schema: { -- f.cue -- package foo +x: "key" +name: "dyn" + // Dynamic fields with old alias syntax DynAlias=(x): { field: 1 @@ -90,6 +98,7 @@ DynInterpAlias=("\(name)"): { val: 3 ref: InterpAlias.val } +dynRef: DynInterpAlias.val // Just the interpolation InterpAlias="\(name)": { val: 3 @@ -100,22 +109,22 @@ package foo // Value aliases - old syntax X={...} // Should convert to let with self -foo: X={ - x: X.a - y: X.b +valAlias: X={ + x: X.a @test(eq, 1) + y: X.b @test(eq, 2) a: 1 b: 2 } -bar: Y={ - data: Y.x + 10 +computed: Y={ + data: Y.x + 10 @test(eq, 15) x: 5 } // Nested value alias -outer: { +nested: { inner: Z={ - value: Z.n * 2 + value: Z.n * 2 @test(eq, 6) n: 3 } } @@ -123,11 +132,11 @@ outer: { // Multiple fields with value aliases multi: { first: A={ - val: A.base * 2 + val: A.base * 2 @test(eq, 20) base: 10 } second: B={ - val: B.base * 3 + val: B.base * 3 @test(eq, 60) base: 20 } } @@ -141,7 +150,7 @@ package foo // Field named self in same struct samestruct: X={ - i: X.self + i: X.self @test(eq, 42) self: 42 } @@ -149,7 +158,7 @@ samestruct: X={ enclosingfield: { self: 42 inner: X={ - i: X.a + i: X.a @test(eq, 1) a: 1 } } @@ -158,9 +167,9 @@ enclosingfield: { enclosinglet: { let self = {z: 99} inner: X={ - i: X.a + i: X.a @test(eq, 1) a: 1 - j: self.z + j: self.z @test(eq, 99) } } @@ -169,9 +178,9 @@ aliassamescope: { self=bar: { data: 1 } - baz: self.data - foo: X={ - i: X.a + baz: self.data @test(eq, 1) + valAlias: X={ + i: X.a @test(eq, 1) a: 1 } } @@ -181,10 +190,10 @@ aliasenclosing: { self=bar: { data: 1 } - baz: self.data + baz: self.data @test(eq, 1) inner: { - foo: X={ - i: X.a + valAlias: X={ + i: X.a @test(eq, 1) a: 1 } } @@ -203,7 +212,7 @@ package foo // Simple field alias a~(X): { foo: 1 - bar: X.foo + 2 + bar: X.foo + 2 @test(eq, 3) } // Multiple aliases @@ -211,8 +220,9 @@ b~(Y): { data: 42 } c~(Z): { - val: Y.data + val: Y.data @test(eq, 42) } +zref: Z.val @test(eq, 42) --- b.cue @experiment(aliasv2) @@ -224,9 +234,9 @@ outer: { deep~(Leaf): { value: 1 } - ref: Leaf.value + ref: Leaf.value @test(eq, 1) } - ref2: Inner.Leaf.value + ref2: Inner.deep.value @test(eq, 1) } --- c.cue @experiment(aliasv2) @@ -237,9 +247,11 @@ package foo a~(OptField)?: { value: 1 } +optRef: OptField.value b~(ReqField)!: { value: 2 } +reqRef: ReqField.value --- d.cue // Already has postfix alias experiment, so do not do any changes. @experiment(aliasv2) @@ -249,6 +261,7 @@ package foo a~(X): { foo: 1 } +ref: X.foo --- e.cue @experiment(aliasv2) @@ -271,6 +284,9 @@ schema: { package foo +x: "key" +name: "dyn" + // Dynamic fields with old alias syntax (x)~(DynAlias): { field: 1 @@ -288,6 +304,7 @@ package foo val: 3 ref: InterpAlias.val } +dynRef: DynInterpAlias.val // Just the interpolation "\(name)"~(InterpAlias): { val: 3 @@ -300,25 +317,25 @@ package foo // Value aliases - old syntax X={...} // Should convert to let with self -foo: { +valAlias: { let X = self - x: X.a - y: X.b + x: X.a @test(eq, 1) + y: X.b @test(eq, 2) a: 1 b: 2 } -bar: { +computed: { let Y = self - data: Y.x + 10 + data: Y.x + 10 @test(eq, 15) x: 5 } // Nested value alias -outer: { +nested: { inner: { let Z = self - value: Z.n * 2 + value: Z.n * 2 @test(eq, 6) n: 3 } } @@ -327,12 +344,12 @@ outer: { multi: { first: { let A = self - val: A.base * 2 + val: A.base * 2 @test(eq, 20) base: 10 } second: { let B = self - val: B.base * 3 + val: B.base * 3 @test(eq, 60) base: 20 } } @@ -349,7 +366,7 @@ package foo // Field named self in same struct samestruct: { let X = __self - i: X.self + i: X.self @test(eq, 42) self: 42 } @@ -358,7 +375,7 @@ enclosingfield: { self: 42 inner: { let X = __self - i: X.a + i: X.a @test(eq, 1) a: 1 } } @@ -368,9 +385,9 @@ enclosinglet: { let self = {z: 99} inner: { let X = __self - i: X.a + i: X.a @test(eq, 1) a: 1 - j: self.z + j: self.z @test(eq, 99) } } @@ -379,10 +396,10 @@ aliassamescope: { bar~(self): { data: 1 } - baz: self.data - foo: { + baz: self.data @test(eq, 1) + valAlias: { let X = __self - i: X.a + i: X.a @test(eq, 1) a: 1 } } @@ -392,11 +409,11 @@ aliasenclosing: { bar~(self): { data: 1 } - baz: self.data + baz: self.data @test(eq, 1) inner: { - foo: { + valAlias: { let X = __self - i: X.a + i: X.a @test(eq, 1) a: 1 } } -- 2.51.2