this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

internal/cuetxtar: support shareID assertions in inline tests

This introduces the @test(shareID=<id>) directive
to inline assertions, verifying that two target
paths evaluate to the same underlying *adt.Vertex
without copying (structural sharing).

Signed-off-by: Marcel van Lohuizen <mpvl@gmail.com>
Change-Id: I907da15b35812684290b0ecd1840ca28276cac65
Reviewed-on: https://cue.gerrithub.io/c/cue-lang/cue/+/1234695
Reviewed-by: Daniel Martí <mvdan@mvdan.cc>
Unity-Result: CUE porcuepine <cue.porcuepine@gmail.com>
TryBot-Result: CUEcueckoo <cueckoo@cuelang.org>

+504 -335
+6 -110
cue/testdata/eval/issue494.txtar
··· 1 - #todo:inline: easy — general eval; straightforward @test(eq, ...) for concrete fields 2 1 -- in.cue -- 3 2 _Q: [{pos: 0}, {pos: 1}] 4 3 5 4 a: [rn=string]: _Q[0:len(a[rn])] 6 - a: ben: [{}] 5 + a: ben: [{}] @test(eq, [{pos: 0}]) 7 6 8 7 b: [rn=string]: _Q[0:1] 9 - b: ben: [{}] 8 + b: ben: [{}] @test(eq, [{pos: 0}]) 10 9 11 10 c: [rn=string]: [...{l: len(a[rn])}] 12 - c: ben: [{}] 11 + c: ben: [{}] @test(eq, [{l: 1}]) 13 12 14 - #d: [rn=string]: [...{pos: uint}] & _Q[0:len(#d[rn])] 15 - #d: ben: [{}] 13 + #d: {[rn=string]: [...{pos: uint}] & _Q[0:len(#d[rn])]} @test(shareID=D) 14 + #d: ben: [{}] @test(eq, [{pos: 0}]) 16 15 17 - d: #d 16 + d: #d @test(shareID=D) 18 17 -- out/eval/stats -- 19 18 Leaks: 0 20 19 Freed: 26 ··· 25 24 Unifications: 26 26 25 Conjuncts: 56 27 26 Disjuncts: 28 28 - -- out/evalalpha -- 29 - (struct){ 30 - _Q: (#list){ 31 - 0: (struct){ 32 - pos: (int){ 0 } 33 - } 34 - 1: (struct){ 35 - pos: (int){ 1 } 36 - } 37 - } 38 - a: (struct){ 39 - ben: (#list){ 40 - 0: (struct){ 41 - pos: (int){ 0 } 42 - } 43 - } 44 - } 45 - b: (struct){ 46 - ben: (#list){ 47 - 0: (struct){ 48 - pos: (int){ 0 } 49 - } 50 - } 51 - } 52 - c: (struct){ 53 - ben: (#list){ 54 - 0: (struct){ 55 - l: (int){ 1 } 56 - } 57 - } 58 - } 59 - #d: (#struct){ 60 - ben: (#list){ 61 - 0: (#struct){ 62 - pos: (int){ 0 } 63 - } 64 - } 65 - } 66 - d: ~(#d) 67 - } 68 - -- diff/-out/evalalpha<==>+out/eval -- 69 - diff old new 70 - --- old 71 - +++ new 72 - @@ -35,11 +35,5 @@ 73 - } 74 - } 75 - } 76 - - d: (#struct){ 77 - - ben: (#list){ 78 - - 0: (#struct){ 79 - - pos: (int){ 0 } 80 - - } 81 - - } 82 - - } 83 - + d: ~(#d) 84 - } 85 - -- out/eval -- 86 - (struct){ 87 - _Q: (#list){ 88 - 0: (struct){ 89 - pos: (int){ 0 } 90 - } 91 - 1: (struct){ 92 - pos: (int){ 1 } 93 - } 94 - } 95 - a: (struct){ 96 - ben: (#list){ 97 - 0: (struct){ 98 - pos: (int){ 0 } 99 - } 100 - } 101 - } 102 - b: (struct){ 103 - ben: (#list){ 104 - 0: (struct){ 105 - pos: (int){ 0 } 106 - } 107 - } 108 - } 109 - c: (struct){ 110 - ben: (#list){ 111 - 0: (struct){ 112 - l: (int){ 1 } 113 - } 114 - } 115 - } 116 - #d: (#struct){ 117 - ben: (#list){ 118 - 0: (#struct){ 119 - pos: (int){ 0 } 120 - } 121 - } 122 - } 123 - d: (#struct){ 124 - ben: (#list){ 125 - 0: (#struct){ 126 - pos: (int){ 0 } 127 - } 128 - } 129 - } 130 - } 131 27 -- out/compile -- 132 28 --- in.cue 133 29 {
+6 -110
cue/testdata/fulleval/018_recursive_evaluation_within_list.txtar
··· 1 1 #name: recursive evaluation within list 2 2 #evalFull 3 - #todo:inline: easy — full evaluation; use @test(eq, ...) for concrete results 4 3 -- in.cue -- 5 - l: [a] 6 - a: b & {c: "t"} 4 + l: [a] @test(eq, [{c: "t", d: "t"}]) @test(shareID=L, at=0) 5 + a: b & {c: "t"} @test(eq, {c: "t", d: "t"}) @test(shareID=L) 7 6 b: { 8 7 d: c 9 8 c: string 10 - } 11 - l1: [a1] 12 - a1: b1 & {c: "t"} 9 + } @test(eq, {d: string, c: string}) 10 + l1: [a1] @test(eq, [{c: "t", d: "st"}]) @test(shareID=L1, at=0) 11 + a1: b1 & {c: "t"} @test(eq, {c: "t", d: "st"}) @test(shareID=L1) 13 12 b1: { 14 - d: "s" + c 13 + d: "s" + c @test(err, code=incomplete) 15 14 c: string 16 15 } 17 16 -- out/compile -- ··· 48 47 Unifications: 21 49 48 Conjuncts: 40 50 49 Disjuncts: 31 51 - -- out/evalalpha -- 52 - (struct){ 53 - l: (#list){ 54 - 0: ~(a) 55 - } 56 - a: (struct){ 57 - c: (string){ "t" } 58 - d: (string){ "t" } 59 - } 60 - b: (struct){ 61 - d: (string){ string } 62 - c: (string){ string } 63 - } 64 - l1: (#list){ 65 - 0: ~(a1) 66 - } 67 - a1: (struct){ 68 - c: (string){ "t" } 69 - d: (string){ "st" } 70 - } 71 - b1: (struct){ 72 - d: (_|_){ 73 - // [incomplete] b1.d: non-concrete value string in operand to +: 74 - // ./in.cue:10:5 75 - // ./in.cue:11:5 76 - } 77 - c: (string){ string } 78 - } 79 - } 80 - -- diff/-out/evalalpha<==>+out/eval -- 81 - diff old new 82 - --- old 83 - +++ new 84 - @@ -1,13 +1,10 @@ 85 - (struct){ 86 - l: (#list){ 87 - - 0: (struct){ 88 - - d: (string){ "t" } 89 - - c: (string){ "t" } 90 - - } 91 - + 0: ~(a) 92 - } 93 - a: (struct){ 94 - + c: (string){ "t" } 95 - d: (string){ "t" } 96 - - c: (string){ "t" } 97 - } 98 - b: (struct){ 99 - d: (string){ string } 100 - @@ -14,14 +11,11 @@ 101 - c: (string){ string } 102 - } 103 - l1: (#list){ 104 - - 0: (struct){ 105 - - d: (string){ "st" } 106 - - c: (string){ "t" } 107 - - } 108 - + 0: ~(a1) 109 - } 110 - a1: (struct){ 111 - + c: (string){ "t" } 112 - d: (string){ "st" } 113 - - c: (string){ "t" } 114 - } 115 - b1: (struct){ 116 - d: (_|_){ 117 - -- diff/todo/p3 -- 118 - Reordering 119 - -- out/eval -- 120 - (struct){ 121 - l: (#list){ 122 - 0: (struct){ 123 - d: (string){ "t" } 124 - c: (string){ "t" } 125 - } 126 - } 127 - a: (struct){ 128 - d: (string){ "t" } 129 - c: (string){ "t" } 130 - } 131 - b: (struct){ 132 - d: (string){ string } 133 - c: (string){ string } 134 - } 135 - l1: (#list){ 136 - 0: (struct){ 137 - d: (string){ "st" } 138 - c: (string){ "t" } 139 - } 140 - } 141 - a1: (struct){ 142 - d: (string){ "st" } 143 - c: (string){ "t" } 144 - } 145 - b1: (struct){ 146 - d: (_|_){ 147 - // [incomplete] b1.d: non-concrete value string in operand to +: 148 - // ./in.cue:10:5 149 - // ./in.cue:11:5 150 - } 151 - c: (string){ string } 152 - } 153 - }
+20 -80
cue/testdata/fulleval/022_references_from_template_to_concrete.txtar
··· 1 - #name: references from template to concrete 2 - #evalFull 3 - #todo:inline: easy — full evaluation; use @test(eq, ...) for concrete results 4 1 -- in.cue -- 5 - res: [t] 6 - t: [X=string]: { 7 - a: c + b.str 8 - b: str: string 9 - c: "X" 10 - } 11 - t: x: {b: str: "DDDD"} 2 + // res is a list containing t; res[0] shares t's vertex. 3 + res: [t] @test(eq, [{x: {a: "XDDDD", b: str: "DDDD", c: "X"}}]) @test(shareID=T, at=0) 4 + t: { 5 + [X=string]: { 6 + a: c + b.str 7 + b: str: string 8 + c: "X" 9 + } 10 + x: {b: str: "DDDD"} @test(eq, {a: "XDDDD", b: str: "DDDD", c: "X"}) 11 + } @test(shareID=T) 12 + -- out/eval/stats -- 13 + Leaks: 0 14 + Freed: 14 15 + Reused: 7 16 + Allocs: 7 17 + Retain: 3 18 + 19 + Unifications: 14 20 + Conjuncts: 21 21 + Disjuncts: 15 12 22 -- out/compile -- 13 23 --- in.cue 14 24 { ··· 23 33 } 24 34 c: "X" 25 35 } 26 - } 27 - t: { 28 36 x: { 29 37 b: { 30 38 str: "DDDD" ··· 32 40 } 33 41 } 34 42 } 35 - -- out/eval/stats -- 36 - Leaks: 0 37 - Freed: 14 38 - Reused: 7 39 - Allocs: 7 40 - Retain: 3 41 - 42 - Unifications: 14 43 - Conjuncts: 21 44 - Disjuncts: 15 45 - -- out/evalalpha -- 46 - (struct){ 47 - res: (#list){ 48 - 0: ~(t) 49 - } 50 - t: (struct){ 51 - x: (struct){ 52 - b: (struct){ 53 - str: (string){ "DDDD" } 54 - } 55 - a: (string){ "XDDDD" } 56 - c: (string){ "X" } 57 - } 58 - } 59 - } 60 - -- diff/-out/evalalpha<==>+out/eval -- 61 - diff old new 62 - --- old 63 - +++ new 64 - @@ -1,14 +1,6 @@ 65 - (struct){ 66 - res: (#list){ 67 - - 0: (struct){ 68 - - x: (struct){ 69 - - b: (struct){ 70 - - str: (string){ "DDDD" } 71 - - } 72 - - a: (string){ "XDDDD" } 73 - - c: (string){ "X" } 74 - - } 75 - - } 76 - + 0: ~(t) 77 - } 78 - t: (struct){ 79 - x: (struct){ 80 - -- out/eval -- 81 - (struct){ 82 - res: (#list){ 83 - 0: (struct){ 84 - x: (struct){ 85 - b: (struct){ 86 - str: (string){ "DDDD" } 87 - } 88 - a: (string){ "XDDDD" } 89 - c: (string){ "X" } 90 - } 91 - } 92 - } 93 - t: (struct){ 94 - x: (struct){ 95 - b: (struct){ 96 - str: (string){ "DDDD" } 97 - } 98 - a: (string){ "XDDDD" } 99 - c: (string){ "X" } 100 - } 101 - } 102 - }
+11 -10
cue/testdata/inlinetest/sharing.txtar
··· 3 3 // @test(shareID=name) on a field within an eq struct asserts that the 4 4 // corresponding path in evaluate(in) shares the same canonical adt.Vertex as 5 5 // every other field carrying the same name in this test case. 6 + // 7 + // The value of a field annotated with @test(shareID=name) in an eq body is 8 + // ignored for the eq comparison — it is documentation only. The sharing 9 + // check is performed independently by runShareIDChecks. 6 10 7 - // ── basic: field reference creates vertex sharing ── 11 + // -- basic: field reference creates vertex sharing -- 8 12 // In CUE, b: a makes b an alias of a at the ADT level. After evaluation, 9 13 // in.b.DerefValue() == in.a.DerefValue(), so the shareID assertion passes. 10 14 11 15 basicSharing: { 12 - @test(skip, why="sharing checks not implemented yet") 13 16 a: {x: 1, y: 2} 14 17 b: a 15 18 @test(eq, { 16 19 a: { x: 1, y: 2 } @test(shareID=A) 17 - b: a @test(shareID=A) 20 + b: { x: 1, y: 2 } @test(shareID=A) 18 21 }) 19 22 } 20 23 21 - // ── three-way sharing ───────────────────── 24 + // -- three-way sharing ---------------------- 22 25 // g is the canonical node; p and q are both references to g. 23 26 24 27 threeWaySharing: { 25 - @test(skip, why="sharing checks not implemented yet") 26 28 g: {n: 42} 27 29 p: g 28 30 q: g 29 31 @test(eq, { 30 32 g: { n: 42 } @test(shareID=G) 31 - p: g @test(shareID=G) 32 - q: g @test(shareID=G) 33 + p: { n: 42 } @test(shareID=G) 34 + q: { n: 42 } @test(shareID=G) 33 35 }) 34 36 } 35 37 36 - // ── versioned shareID: @test(shareID:v3=name) applies only under v3 ── 38 + // -- versioned shareID: @test(shareID:v3=name) applies only under v3 -- 37 39 // The unversioned group "common" covers all evaluator versions; the versioned 38 40 // group "v3group" is only checked when running under v3. 39 41 40 42 versionedSharing: { 41 - @test(skip, why="sharing checks not implemented yet") 42 43 a: {z: 1} 43 44 b: a 44 45 @test(eq, { 45 46 a: { z: 1 } @test(shareID=common) 46 - b: a @test(shareID=common) 47 + b: { z: 1 } @test(shareID=common) 47 48 }) 48 49 } 49 50 -- out/compile --
+13 -3
internal/core/adt/eval_test.go
··· 34 34 "cuelang.org/go/internal/core/runtime" 35 35 "cuelang.org/go/internal/cuedebug" 36 36 "cuelang.org/go/internal/cueexperiment" 37 + "cuelang.org/go/internal/cuetdtest" 37 38 "cuelang.org/go/internal/cuetest" 38 39 "cuelang.org/go/internal/cuetxtar" 39 40 _ "cuelang.org/go/pkg" ··· 279 280 language: version: "v0.15.0" 280 281 281 282 -- in.cue -- 282 - s1: !="b" & =~"c" @test(eq, =~"c") 283 - s2: !=null @test(eq, !=null) 283 + // s1: !="b" & =~"c" @test(eq, =~"c") 284 + // s2: !=null @test(eq, !=null) 285 + a: 1 @test(shareID=A) 286 + b: c: a @test(shareID=A) 284 287 ` 285 288 286 289 if strings.HasSuffix(strings.TrimSpace(in), ".cue --") { 287 290 t.Skip() 288 291 } 289 292 293 + m := &cuetdtest.M{ 294 + Flags: cuedebug.Config{ 295 + Sharing: true, // Comment out to test without sharing. 296 + // LogEval: 1, // Uncomment to enable evaluator logging. 297 + }, 298 + } 299 + 290 300 archive := txtar.Parse([]byte(in)) 291 - runner := cuetxtar.NewInlineRunner(t, nil, archive, t.TempDir()) 301 + runner := cuetxtar.NewInlineRunner(t, m, archive, t.TempDir()) 292 302 runner.Run() 293 303 } 294 304
+16 -1
internal/cuetxtar/astcmp.go
··· 219 219 var patterns []expectedPattern 220 220 checkOrder := false 221 221 allFinal := false 222 + seenShareIDs := make(map[string]bool) 222 223 223 224 for _, d := range s.Elts { 224 225 switch d := d.(type) { ··· 239 240 lets = append(lets, expectedLet{name: d.Ident.Name, expr: d.Expr}) 240 241 case *ast.Field: 241 242 // Separate non-@test attributes from @test attributes. 242 - // Detect @test(final), @test(ignore), and @test(err) on individual fields. 243 + // Detect @test(final), @test(ignore), @test(err), and @test(shareID=name) 244 + // on individual fields. 243 245 var nonTestAttrs []*ast.Attribute 244 246 isFinal := false 245 247 isIgnore := false ··· 257 259 errCk = pa.errArgs 258 260 if errCk == nil { 259 261 errCk = &errArgs{} // bare @test(err) 262 + } 263 + case "shareID": 264 + // The first field with a given shareID name runs the eq check 265 + // normally so every value-expr pair is checked at least once. 266 + // Subsequent fields with the same shareID name skip the eq 267 + // check; sharing is verified separately by runShareIDChecks. 268 + if len(pa.raw.Fields) > 0 { 269 + name := pa.raw.Fields[0].Value() 270 + if seenShareIDs[name] { 271 + isIgnore = true 272 + } else { 273 + seenShareIDs[name] = true 274 + } 260 275 } 261 276 } 262 277 }
+44
internal/cuetxtar/astcmp_test.go
··· 832 832 t.Errorf("error %q does not contain %q", err.Error(), wantErr) 833 833 } 834 834 } 835 + 836 + // TestShareIDInEqBody verifies the first-occurrence eq-check rule for 837 + // @test(shareID=name) fields in an eq struct body. 838 + func TestShareIDInEqBody(t *testing.T) { 839 + t.Run("first occurrence runs eq check, mismatch fails", func(t *testing.T) { 840 + // The first field with shareID=A has value {x: 99} but the actual 841 + // value of "a" is {x: 1}. The eq check must run and fail. 842 + expr := parseExpr(t, `{ 843 + a: {x: 99} @test(shareID=A) 844 + b: {x: 1} @test(shareID=A) 845 + }`) 846 + val := compileVal(t, `{a: {x: 1}, b: {x: 1}}`) 847 + err := astCompare(expr, val.LookupPath(cue.MakePath())) 848 + if err == nil { 849 + t.Error("expected eq check to fail for first shareID occurrence, but it passed") 850 + } 851 + }) 852 + 853 + t.Run("second occurrence skips eq check, mismatch ok", func(t *testing.T) { 854 + // First field matches; second has wrong value but is skipped. 855 + expr := parseExpr(t, `{ 856 + a: {x: 1} @test(shareID=A) 857 + b: {x: 99} @test(shareID=A) 858 + }`) 859 + val := compileVal(t, `{a: {x: 1}, b: {x: 1}}`) 860 + err := astCompare(expr, val.LookupPath(cue.MakePath())) 861 + if err != nil { 862 + t.Errorf("expected second shareID occurrence to skip eq check, but got: %v", err) 863 + } 864 + }) 865 + 866 + t.Run("identifier value in second occurrence is skipped", func(t *testing.T) { 867 + // Second occurrence uses 'a' as a documentation reference; it is skipped. 868 + expr := parseExpr(t, `{ 869 + a: {x: 1} @test(shareID=A) 870 + b: a @test(shareID=A) 871 + }`) 872 + val := compileVal(t, `{a: {x: 1}, b: {x: 1}}`) 873 + err := astCompare(expr, val.LookupPath(cue.MakePath())) 874 + if err != nil { 875 + t.Errorf("identifier as second shareID value should be skipped, but got: %v", err) 876 + } 877 + }) 878 + }
+241 -7
internal/cuetxtar/inline.go
··· 665 665 pendingInlineFillWrites []inlineFillWrite 666 666 } 667 667 668 - // todoCapture wraps *testing.T and captures failures without propagating them. 668 + // failCapture wraps *testing.T and captures failures without propagating them. 669 669 // It is used for @test(todo) XFAIL mode: all directives run, but failures are 670 670 // logged rather than reported as test errors. 671 671 // 672 - // todoCapture embeds *testing.T to satisfy the testing.TB interface (via the 672 + // failCapture embeds *testing.T to satisfy the testing.TB interface (via the 673 673 // promoted unexported private() method). Only Errorf and Error are overridden; 674 674 // all other methods delegate to the embedded T. 675 - type todoCapture struct { 676 - *testing.T 675 + type failCapture struct { 676 + testing.TB 677 677 failed bool 678 678 msgs strings.Builder 679 679 } 680 680 681 - func (c *todoCapture) Error(args ...any) { 681 + func (c *failCapture) Error(args ...any) { 682 682 c.failed = true 683 683 fmt.Fprintln(&c.msgs, args...) 684 684 } 685 685 686 - func (c *todoCapture) Errorf(format string, args ...any) { 686 + func (c *failCapture) Errorf(format string, args ...any) { 687 687 c.failed = true 688 688 fmt.Fprintf(&c.msgs, format+"\n", args...) 689 689 } ··· 729 729 suffix += fmt.Sprintf(" why=%q", todoWhy) 730 730 } 731 731 if hasTodo { 732 - cap := &todoCapture{T: t} 732 + cap := &failCapture{TB: t} 733 733 for _, pa := range directives { 734 734 if pa.directive == "skip" || pa.directive == "todo" { 735 735 continue ··· 833 833 r.t.Run(name, func(t *testing.T) { 834 834 r.runInline(t, root, val, allRecords) 835 835 }) 836 + } 837 + 838 + // Run file-level shareID checks: @test(shareID=...) annotations at any 839 + // nesting depth may form groups spanning different roots. These cross-root 840 + // sharing assertions cannot be detected per-root, so we collect them once 841 + // over all records and check after all subtests run. 842 + version := r.versionName() 843 + if fileShareGroups := r.collectDirectShareIDs(allRecords, version); len(fileShareGroups) > 0 { 844 + r.runShareIDChecks(r.t, val, fileShareGroups) 836 845 } 837 846 838 847 // After all subtests complete, write back any pending updates. ··· 1141 1150 r.runDirectivesForPath(t, rec.path, fieldVal, directives) 1142 1151 } 1143 1152 1153 + // Check @test(shareID=...) vertex-sharing assertions collected from eq bodies. 1154 + shareGroups := r.collectShareIDsForRoot(records, rootPath, version) 1155 + if len(shareGroups) > 0 { 1156 + r.runShareIDChecks(t, fileVal, shareGroups) 1157 + } 1158 + 1144 1159 // Handle @test(permute) field attributes. 1145 1160 r.runInlinePermutes(t, rootPath, records, version) 1146 1161 } ··· 1196 1211 // Handled by permute-group collection in runInline; no-op here. 1197 1212 case "permuteCount": 1198 1213 // Handled by checkPermuteCount after permutations run; no-op here. 1214 + case "shareID": 1215 + // @test(shareID=name) annotations appear on fields within @test(eq, {...}) 1216 + // bodies; sharing is verified by runShareIDChecks in runInline — no-op here. 1199 1217 case "desc": 1200 1218 // @test(desc="...") is a human-readable description annotation — no assertions. 1201 1219 case "": ··· 1770 1788 newAttrText: newAttrText, 1771 1789 }) 1772 1790 } 1791 + 1792 + // ───────────────────────────────────────────────────────────────────────────── 1793 + // Section 8: shareID — vertex sharing assertions 1794 + // ───────────────────────────────────────────────────────────────────────────── 1795 + 1796 + // extractShareIDsFromEqExpr walks the struct literal of an @test(eq, STRUCT) 1797 + // body and collects all @test(shareID=name) annotations on fields. 1798 + // basePath is the CUE path of the @test(eq) attribute; field paths in the 1799 + // struct are appended to it. version is the active evaluator version name 1800 + // used for version-specific share groups (@test(shareID:v3=name)). 1801 + // Returns a map from shareID name to the absolute paths of fields in that group. 1802 + func extractShareIDsFromEqExpr(expr ast.Expr, basePath cue.Path, version string) map[string][]cue.Path { 1803 + s, ok := expr.(*ast.StructLit) 1804 + if !ok { 1805 + return nil 1806 + } 1807 + var result map[string][]cue.Path 1808 + for _, d := range s.Elts { 1809 + f, ok := d.(*ast.Field) 1810 + if !ok { 1811 + continue 1812 + } 1813 + for _, a := range f.Attrs { 1814 + if k, _ := a.Split(); k != "test" { 1815 + continue 1816 + } 1817 + pa, err := parseTestAttr(a) 1818 + if err != nil || pa.directive != "shareID" { 1819 + continue 1820 + } 1821 + // Version filter: skip if a non-matching version is specified. 1822 + if pa.version != "" && pa.version != version { 1823 + continue 1824 + } 1825 + if len(pa.raw.Fields) == 0 { 1826 + continue 1827 + } 1828 + shareIDName := pa.raw.Fields[0].Value() 1829 + if shareIDName == "" { 1830 + continue 1831 + } 1832 + fieldPath := basePath.Append(cue.Label(f.Label)) 1833 + for _, kv := range pa.raw.Fields[1:] { 1834 + if kv.Key() == "at" { 1835 + if n, err := strconv.Atoi(kv.Value()); err == nil { 1836 + fieldPath = fieldPath.Append(cue.Index(n)) 1837 + } 1838 + break 1839 + } 1840 + } 1841 + if result == nil { 1842 + result = make(map[string][]cue.Path) 1843 + } 1844 + result[shareIDName] = append(result[shareIDName], fieldPath) 1845 + } 1846 + } 1847 + return result 1848 + } 1849 + 1850 + // collectShareIDsForRoot builds a map of shareID name → CUE paths by scanning 1851 + // all attrRecords within rootPath in two ways: 1852 + // 1853 + // 1. Direct @test(shareID=name) field attributes in the source — each record 1854 + // with directive "shareID" contributes its rec.path to the named group. 1855 + // 1856 + // 2. @test(shareID=name) annotations on fields inside @test(eq, STRUCT) bodies 1857 + // — the struct is parsed and fields carrying shareID annotations are mapped 1858 + // to their absolute paths (basePath + fieldLabel). 1859 + func (r *inlineRunner) collectShareIDsForRoot(records []attrRecord, rootPath cue.Path, version string) map[string][]cue.Path { 1860 + var shareGroups map[string][]cue.Path 1861 + add := func(id string, p cue.Path) { 1862 + if shareGroups == nil { 1863 + shareGroups = make(map[string][]cue.Path) 1864 + } 1865 + shareGroups[id] = append(shareGroups[id], p) 1866 + } 1867 + 1868 + // Track processed eq attrs by (fileName, offset) to avoid double-counting. 1869 + type attrKey struct { 1870 + file string 1871 + offset int 1872 + } 1873 + seenEq := make(map[attrKey]bool) 1874 + 1875 + for _, rec := range records { 1876 + if !pathHasPrefix(rec.path, rootPath) { 1877 + continue 1878 + } 1879 + pa := rec.parsed 1880 + // Version filter: skip directives targeting a different version. 1881 + if pa.version != "" && pa.version != version { 1882 + continue 1883 + } 1884 + 1885 + switch pa.directive { 1886 + case "shareID": 1887 + // Direct field attribute: @test(shareID=name) on a source field. 1888 + // Optional at=N sub-option selects list element N within the field. 1889 + if len(pa.raw.Fields) == 0 { 1890 + continue 1891 + } 1892 + shareIDName := pa.raw.Fields[0].Value() 1893 + if shareIDName == "" { 1894 + continue 1895 + } 1896 + p := rec.path 1897 + for _, kv := range pa.raw.Fields[1:] { 1898 + if kv.Key() == "at" { 1899 + n, err := strconv.Atoi(kv.Value()) 1900 + if err == nil { 1901 + p = p.Append(cue.Index(n)) 1902 + } 1903 + break 1904 + } 1905 + } 1906 + add(shareIDName, p) 1907 + 1908 + case "eq": 1909 + // Eq body: extract @test(shareID=name) from fields in the struct literal. 1910 + if len(pa.raw.Fields) < 2 { 1911 + continue 1912 + } 1913 + key := attrKey{file: pa.srcFileName, offset: pa.srcAttr.Pos().Offset()} 1914 + if seenEq[key] { 1915 + continue 1916 + } 1917 + seenEq[key] = true 1918 + eqExpr, err := parser.ParseExpr("shareID", pa.raw.Fields[1].Text()) 1919 + if err != nil { 1920 + continue 1921 + } 1922 + for id, paths := range extractShareIDsFromEqExpr(eqExpr, rec.path, version) { 1923 + for _, p := range paths { 1924 + add(id, p) 1925 + } 1926 + } 1927 + } 1928 + } 1929 + return shareGroups 1930 + } 1931 + 1932 + // collectDirectShareIDs builds a shareID group map from direct @test(shareID=name) 1933 + // field attributes across ALL records at any nesting depth (no root filtering). 1934 + // This is used for cross-root sharing assertions where fields from different 1935 + // roots share a vertex. Eq-body sharing is handled per-root by 1936 + // collectShareIDsForRoot. 1937 + func (r *inlineRunner) collectDirectShareIDs(records []attrRecord, version string) map[string][]cue.Path { 1938 + var shareGroups map[string][]cue.Path 1939 + for _, rec := range records { 1940 + if rec.fileLevel { 1941 + continue 1942 + } 1943 + 1944 + pa := rec.parsed 1945 + if pa.version != "" && pa.version != version { 1946 + continue 1947 + } 1948 + if pa.directive != "shareID" { 1949 + continue 1950 + } 1951 + if len(pa.raw.Fields) == 0 { 1952 + continue 1953 + } 1954 + shareIDName := pa.raw.Fields[0].Value() 1955 + if shareIDName == "" { 1956 + continue 1957 + } 1958 + p := rec.path 1959 + for _, kv := range pa.raw.Fields[1:] { 1960 + if kv.Key() == "at" { 1961 + n, err := strconv.Atoi(kv.Value()) 1962 + if err == nil { 1963 + p = p.Append(cue.Index(n)) 1964 + } 1965 + break 1966 + } 1967 + } 1968 + if shareGroups == nil { 1969 + shareGroups = make(map[string][]cue.Path) 1970 + } 1971 + shareGroups[shareIDName] = append(shareGroups[shareIDName], p) 1972 + } 1973 + return shareGroups 1974 + } 1975 + 1976 + // runShareIDChecks verifies that all paths in each shareID group dereference to 1977 + // the same canonical *adt.Vertex, confirming that the CUE evaluator shares the 1978 + // vertex rather than copying it. 1979 + func (r *inlineRunner) runShareIDChecks(t testing.TB, fileVal cue.Value, shareGroups map[string][]cue.Path) { 1980 + t.Helper() 1981 + for id, paths := range shareGroups { 1982 + if len(paths) < 2 { 1983 + continue // need at least two to assert sharing 1984 + } 1985 + firstVal := fileVal.LookupPath(paths[0]) 1986 + firstCore := firstVal.Core() 1987 + if firstCore.V == nil { 1988 + t.Errorf("@test(shareID=%s): path %s: not found in evaluated value", id, paths[0]) 1989 + continue 1990 + } 1991 + derefFirst := firstCore.V.DerefValue() 1992 + for _, p := range paths[1:] { 1993 + otherVal := fileVal.LookupPath(p) 1994 + otherCore := otherVal.Core() 1995 + if otherCore.V == nil { 1996 + t.Errorf("@test(shareID=%s): path %s: not found in evaluated value", id, p) 1997 + continue 1998 + } 1999 + derefOther := otherCore.V.DerefValue() 2000 + if derefFirst != derefOther { 2001 + t.Errorf("@test(shareID=%s): %s and %s are not shared (different vertices)", 2002 + id, paths[0], p) 2003 + } 2004 + } 2005 + } 2006 + }
+63 -14
internal/cuetxtar/inline_test.go
··· 4 4 // you may not use this file except in compliance with the License. 5 5 // You may obtain a copy of the License at 6 6 // 7 - // http://www.apache.org/licenses/LICENSE-2.0 7 + // http://www.apache.org/licenses/LICENSE-2.0 8 8 // 9 9 // Unless required by applicable law or agreed to in writing, software 10 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 12 // See the License for the specific language governing permissions and 13 13 // limitations under the License. 14 - 15 14 package cuetxtar 16 15 17 16 import ( 18 17 "slices" 19 18 "strings" 20 19 "testing" 21 - 22 - "golang.org/x/tools/txtar" 23 20 24 21 "cuelang.org/go/cue" 25 22 "cuelang.org/go/cue/ast" 23 + "cuelang.org/go/cue/cuecontext" 26 24 "cuelang.org/go/cue/parser" 25 + "golang.org/x/tools/txtar" 27 26 ) 28 27 29 28 // testMakePath creates a CUE path from a dot-separated string for test use. ··· 38 37 } 39 38 return cue.MakePath(sels...) 40 39 } 41 - 42 40 func TestParsePosSpecs(t *testing.T) { 43 41 tests := []struct { 44 42 name string ··· 99 97 wantErr: true, 100 98 }, 101 99 } 102 - 103 100 for _, tt := range tests { 104 101 t.Run(tt.name, func(t *testing.T) { 105 102 got, err := parsePosSpecs(tt.input) ··· 124 121 }) 125 122 } 126 123 } 127 - 128 124 func TestFindPermFieldsAtPath(t *testing.T) { 129 125 tests := []struct { 130 126 name string ··· 169 165 wantCount: 0, 170 166 }, 171 167 } 172 - 173 168 for _, tt := range tests { 174 169 t.Run(tt.name, func(t *testing.T) { 175 170 f, err := parser.ParseFile("test.cue", tt.src) 176 171 if err != nil { 177 172 t.Fatalf("parse: %v", err) 178 173 } 179 - 180 174 structLit, indices := findPermFieldsAtPath(f, testMakePath(tt.path), tt.fieldNames) 181 - 182 175 if tt.wantCount == 0 { 183 176 if len(indices) != 0 { 184 177 t.Errorf("want 0 indices, got %d", len(indices)) ··· 211 204 }) 212 205 } 213 206 } 214 - 215 207 func makeRec(path string, directive, version string) attrRecord { 216 208 return attrRecord{ 217 209 path: testMakePath(path), ··· 221 213 }, 222 214 } 223 215 } 224 - 225 216 func TestSelectActiveDirectives(t *testing.T) { 226 217 tests := []struct { 227 218 name string ··· 275 266 wantDirs: []string{"eq", "err"}, 276 267 }, 277 268 } 278 - 279 269 for _, tt := range tests { 280 270 t.Run(tt.name, func(t *testing.T) { 281 271 got := selectActiveDirectives(tt.records, testMakePath(tt.path), tt.version) ··· 369 359 archive: "-- test.cue --\nx: 1 & 2 @test(err, pos=[test.cue:1:4 test.cue:1:8])\n", 370 360 }, 371 361 } 372 - 373 362 for _, tt := range tests { 374 363 t.Run(tt.name, func(t *testing.T) { 375 364 archive := txtar.Parse([]byte(tt.archive)) ··· 378 367 }) 379 368 } 380 369 } 370 + 371 + // TestRunShareIDChecks_Negative verifies that runShareIDChecks correctly 372 + // reports an error when two paths do NOT share the same vertex. 373 + // This cannot be expressed as a txtar inline test (we can't annotate "should 374 + // fail"), so it is tested here by calling runShareIDChecks directly with a 375 + // todoCapture that captures errors without propagating to the parent test. 376 + func TestRunShareIDChecks_Negative(t *testing.T) { 377 + ctx := cuecontext.New() 378 + r := &inlineRunner{} 379 + t.Run("shared vertices pass", func(t *testing.T) { 380 + // b: a creates vertex sharing; both paths should deref to the same node. 381 + v := ctx.CompileString("a: {x: 1}\nb: a") 382 + rec := &failCapture{TB: t} 383 + r.runShareIDChecks(rec, v, map[string][]cue.Path{ 384 + "AB": {cue.MakePath(cue.Str("a")), cue.MakePath(cue.Str("b"))}, 385 + }) 386 + if rec.failed { 387 + t.Errorf("expected shared vertices to pass, got errors:\n%s", rec.msgs.String()) 388 + } 389 + }) 390 + t.Run("independent structs fail", func(t *testing.T) { 391 + // a and b are independently defined; they must not share a vertex. 392 + v := ctx.CompileString("a: {x: 1}\nb: {x: 1}") 393 + rec := &failCapture{TB: t} 394 + r.runShareIDChecks(rec, v, map[string][]cue.Path{ 395 + "AB": {cue.MakePath(cue.Str("a")), cue.MakePath(cue.Str("b"))}, 396 + }) 397 + if !rec.failed { 398 + t.Errorf("expected independent structs to fail shareID check, but it passed") 399 + } 400 + }) 401 + t.Run("list element via at=0 shared passes", func(t *testing.T) { 402 + // l: [a] makes l[0] the same vertex as a. 403 + v := ctx.CompileString("a: {x: 1}\nl: [a]") 404 + rec := &failCapture{TB: t} 405 + r.runShareIDChecks(rec, v, map[string][]cue.Path{ 406 + "EL": { 407 + cue.MakePath(cue.Str("l"), cue.Index(0)), 408 + cue.MakePath(cue.Str("a")), 409 + }, 410 + }) 411 + if rec.failed { 412 + t.Errorf("expected l[0] and a to be shared, got errors:\n%s", rec.msgs.String()) 413 + } 414 + }) 415 + t.Run("list element literal not shared fails", func(t *testing.T) { 416 + // l: [{x: 1}] is a literal; no sharing with a. 417 + v := ctx.CompileString("a: {x: 1}\nl: [{x: 1}]") 418 + rec := &failCapture{TB: t} 419 + r.runShareIDChecks(rec, v, map[string][]cue.Path{ 420 + "EL": { 421 + cue.MakePath(cue.Str("l"), cue.Index(0)), 422 + cue.MakePath(cue.Str("a")), 423 + }, 424 + }) 425 + if !rec.failed { 426 + t.Errorf("expected list literal element to fail shareID check, but it passed") 427 + } 428 + }) 429 + }
+84
internal/cuetxtar/inlinerunner_test.go
··· 217 217 }) 218 218 } 219 219 } 220 + 221 + // TestInlineRunner_ShareID verifies @test(shareID=name) vertex-sharing 222 + // assertions for the passing (positive) cases. Negative cases (asserting that 223 + // the check correctly rejects non-shared vertices) are in inline_test.go as 224 + // TestRunShareIDChecks_Negative, which calls runShareIDChecks directly with a 225 + // non-propagating failRecorder. 226 + func TestInlineRunner_ShareID(t *testing.T) { 227 + run := func(t *testing.T, archiveStr string) { 228 + t.Helper() 229 + archive := txtar.Parse([]byte(archiveStr)) 230 + runner := cuetxtar.NewInlineRunner(t, nil, archive, t.TempDir()) 231 + runner.Run() 232 + } 233 + 234 + t.Run("sharing passes when p and q reference g", func(t *testing.T) { 235 + run(t, `-- test.cue -- 236 + g: {x: 1, y: 2} 237 + p: g @test(shareID=V) 238 + q: g @test(shareID=V) 239 + `) 240 + }) 241 + 242 + t.Run("sharing passes inside eq body", func(t *testing.T) { 243 + run(t, `-- test.cue -- 244 + root: { 245 + a: {x: 1} 246 + b: a 247 + @test(eq, { 248 + a: {x: 1} @test(shareID=AB) 249 + b: a @test(shareID=AB) 250 + }) 251 + } 252 + `) 253 + }) 254 + 255 + t.Run("single annotated path is silently skipped", func(t *testing.T) { 256 + // A group with a single path needs no second vertex to compare against. 257 + run(t, `-- test.cue -- 258 + a: {x: 1} @test(shareID=SOLO) 259 + `) 260 + }) 261 + 262 + t.Run("sharing passes with at=0 for list element", func(t *testing.T) { 263 + // l[0] is the same vertex as a because l: [a]. 264 + run(t, `-- test.cue -- 265 + a: {x: 1} 266 + l: [a] @test(shareID=EL, at=0) 267 + m: a @test(shareID=EL) 268 + `) 269 + }) 270 + 271 + t.Run("sharing passes inside eq body with at=0", func(t *testing.T) { 272 + run(t, `-- test.cue -- 273 + root: { 274 + a: {x: 1} 275 + l: [a] 276 + @test(eq, { 277 + a: {x: 1} @test(shareID=AB) 278 + l: [a] @test(shareID=AB, at=0) 279 + }) 280 + } 281 + `) 282 + }) 283 + 284 + t.Run("sharing passes for nested fields across roots", func(t *testing.T) { 285 + // @test(shareID=...) at depth > 1 participates in cross-root groups. 286 + // outer1.inner and outer2.inner both reference g, so they share a vertex. 287 + run(t, `-- test.cue -- 288 + g: {x: 1} 289 + outer1: {inner: g @test(shareID=V)} 290 + outer2: {inner: g @test(shareID=V)} 291 + `) 292 + }) 293 + 294 + t.Run("sharing passes across different depths", func(t *testing.T) { 295 + // A top-level field and a nested field may share the same vertex and 296 + // participate in the same shareID group despite being at different depths. 297 + run(t, `-- test.cue -- 298 + g: {x: 1} @test(shareID=V) 299 + p: g @test(shareID=V) 300 + outer: {q: g @test(shareID=V)} 301 + `) 302 + }) 303 + }