this repo has no description
0
fork

Configure Feed

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

internal/cuetxtar: add args= and suberr= to @test(err, ...)

Add two new sub-options to the err
directive:

args=[v1, v2, ...]
Checks that the error's Msg() args
include all listed strings.
Order-independent subset check:
extra actual args are allowed.
Useful for verifying type names in
conflict errors regardless of arg
order across implementations.

suberr=(...)
Matches one sub-error in a
multi-error value (e.g. failed
disjunction). Multiple suberr=
entries match order-independently.
Two-pass: pos= specs matched first,
then remaining by contains=.
pos=[] triggers write-back on
CUE_UPDATE=1.

Add TestCheckMsgArgs to inline_test.go
covering subset semantics, order-
independence, extra-args allowed,
and missing-arg failure.

Update 010_lists.txtar and
issue3330.txtar with args= and
suberr= annotations.

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

+172 -49
+4 -4
cue/testdata/basicrewrite/010_lists.txtar
··· 2 2 list: [1, 2, 3] @test(eq, [1, 2, 3]) 3 3 index: [1, 2, 3][1] @test(eq, 2) 4 4 unify: [1, 2, 3] & [_, 2, 3] @test(eq, [1, 2, 3]) 5 - e: [] & 4 @test(err, code=eval, pos=[0:4 0:9], contains="conflicting values") 5 + e: [] & 4 @test(err, code=eval, pos=[0:4 0:9], contains="conflicting values", args=[list, int]) 6 6 7 7 // No "contains" as message may vary by implementation. 8 8 e2: [3]["d"] @test(err, code=eval, pos=[0:9]) 9 - e3: [3][-1] @test(err, code=eval, pos=[0:5 0:9], contains="invalid index") 10 - e4: [1, 2, ...>=4 & <=5] & [1, 2, 4, 8] @test(err, code=eval, pos=[0:21 0:38], contains="invalid value") 11 - e5: [1, 2, 4, 8] & [1, 2, ...>=4 & <=5] @test(err, code=eval, pos=[0:36 0:15], contains="invalid value") 9 + e3: [3][-1] @test(err, code=eval, pos=[0:5 0:9], contains="invalid index", args=[-1]) 10 + e4: [1, 2, ...>=4 & <=5] & [1, 2, 4, 8] @test(err, code=eval, pos=[0:21 0:38], contains="invalid value", args=[8, <=5]) 11 + e5: [1, 2, 4, 8] & [1, 2, ...>=4 & <=5] @test(err, code=eval, pos=[0:36 0:15], contains="invalid value", args=[8, <=5]) 12 12 -- out/errors.txt -- 13 13 [eval] e: conflicting values [] and 4 (mismatched types list and int): 14 14 ./in.cue:4:4
+1 -1
cue/testdata/eval/issue3330.txtar
··· 21 21 #empty: {} 22 22 x: null | { n: 3 } 23 23 x: #empty & { n: 3 } @test(err, code=eval, contains="empty disjunction", 24 - suberr=(pos=[-1:5 0:5 0:14], contains="conflicting values"), 24 + suberr=(pos=[-1:5 0:5 0:14], contains="conflicting values", args=[struct, null]), 25 25 suberr=(pos=[-1:13 0:15], contains="not allowed")) 26 26 out: len(x) @test(err, code=eval) 27 27 }
+10 -35
cue/testdata/eval/issue500.txtar
··· 1 - #todo:inline: medium — imported builtin validators; needs cmpBuiltinExpr path 2 1 -- in.cue -- 3 2 package x 4 3 5 4 import "strings" 6 5 import "list" 7 6 8 - a: strings.Join(strings.Split("test", "")[1:], "") 7 + a: strings.Join(strings.Split("test", "")[1:], "") @test(eq, "est") 9 8 10 - b: strings.Join(["t", "e", "s", "t"][1:], "") 9 + b: strings.Join(["t", "e", "s", "t"][1:], "") @test(eq, "est") 11 10 12 - c: ["t", "e", "s", "t"][1:] 13 - d: strings.Join(c, "") 11 + c: ["t", "e", "s", "t"][1:] @test(eq, ["e", "s", "t"]) 12 + d: strings.Join(c, "") @test(eq, "est") 14 13 15 - e: strings.Join(list.Concat([["a"], ["b"]]), "") 14 + e: strings.Join(list.Concat([["a"], ["b"]]), "") @test(eq, "ab") 16 15 17 - f: list.Concat([["a"], ["b"]]) 18 - g: strings.Join(f, "") 16 + f: list.Concat([["a"], ["b"]]) @test(eq, ["a", "b"]) 17 + g: strings.Join(f, "") @test(eq, "ab") 19 18 20 - h: strings.Join(list.Repeat(["a"], 3), "") 19 + h: strings.Join(list.Repeat(["a"], 3), "") @test(eq, "aaa") 21 20 22 - i: list.Repeat(["b"], 3) 23 - j: strings.Join(i, "") 21 + i: list.Repeat(["b"], 3) @test(eq, ["b", "b", "b"]) 22 + j: strings.Join(i, "") @test(eq, "bbb") 24 23 -- out/eval/stats -- 25 24 Leaks: 2 26 25 Freed: 36 ··· 31 30 Unifications: 38 32 31 Conjuncts: 45 33 32 Disjuncts: 42 34 - -- out/evalalpha -- 35 - (struct){ 36 - a: (string){ "est" } 37 - b: (string){ "est" } 38 - c: (#list){ 39 - 0: (string){ "e" } 40 - 1: (string){ "s" } 41 - 2: (string){ "t" } 42 - } 43 - d: (string){ "est" } 44 - e: (string){ "ab" } 45 - f: (#list){ 46 - 0: (string){ "a" } 47 - 1: (string){ "b" } 48 - } 49 - g: (string){ "ab" } 50 - h: (string){ "aaa" } 51 - i: (#list){ 52 - 0: (string){ "b" } 53 - 1: (string){ "b" } 54 - 2: (string){ "b" } 55 - } 56 - j: (string){ "bbb" } 57 - } 58 33 -- out/compile -- 59 34 --- in.cue 60 35 {
+77 -8
internal/cuetxtar/inline_err.go
··· 19 19 package cuetxtar 20 20 21 21 import ( 22 + "errors" 22 23 "fmt" 23 24 "os" 24 25 "slices" ··· 79 80 // suberrs holds expected sub-error specs for multi-error (list) values. 80 81 // Each entry is matched order-independently against errors.Errors(val.Err()). 81 82 suberrs []*errArgs 83 + // msgArgs holds expected fmt.Sprint representations of Msg() args to check 84 + // order-independently against the error's Msg() arguments. 85 + msgArgs []string 82 86 } 83 87 84 88 // matchesCode reports whether the given error code satisfies the codes ··· 135 139 ea.pos = specs 136 140 ea.posSet = true 137 141 case kv.Key() == "suberr": 138 - inner := strings.TrimSpace(kv.Value()) 139 - if !strings.HasPrefix(inner, "(") || !strings.HasSuffix(inner, ")") { 140 - return ea, fmt.Errorf("@test(err, suberr=...): value must be wrapped in (...), got %q", inner) 142 + raw := strings.TrimSpace(kv.Value()) 143 + inner, err := trimSurrounding(raw, '(', ')') 144 + if err != nil { 145 + return ea, fmt.Errorf("@test(err, suberr=...): %w", err) 141 146 } 142 - inner = inner[1 : len(inner)-1] 143 147 // Reuse parseErrArgs by building a synthetic "err, <inner>" attr body. 144 148 syntheticAttr := internal.ParseAttrBody(token.NoPos, "err, "+inner) 145 149 subEA, err := parseErrArgs(syntheticAttr) ··· 147 151 return ea, fmt.Errorf("@test(err, suberr=...): %w", err) 148 152 } 149 153 ea.suberrs = append(ea.suberrs, &subEA) 154 + case kv.Key() == "args": 155 + args, err := parseArgsList(kv.Value()) 156 + if err != nil { 157 + return ea, fmt.Errorf("@test(err, args=...): %w", err) 158 + } 159 + ea.msgArgs = args 150 160 } 151 161 } 152 162 return ea, nil 153 163 } 154 164 165 + // trimSurrounding checks that s is wrapped in the given left and right 166 + // delimiter bytes and returns the inner content. It returns an error if 167 + // either delimiter is missing. 168 + func trimSurrounding(s string, left, right byte) (string, error) { 169 + if len(s) < 2 || s[0] != left || s[len(s)-1] != right { 170 + return "", fmt.Errorf("expected %c...%c, got %q", left, right, s) 171 + } 172 + return s[1 : len(s)-1], nil 173 + } 174 + 155 175 // parseParenList parses a balanced parenthesized pipe-separated list like 156 176 // (path1|path2|path3), returning ["path1","path2","path3"]. 157 177 // The input may or may not include the outer parentheses. ··· 180 200 // absLine is the 1-indexed line in the named file. 181 201 func parsePosSpecs(s string) ([]posSpec, error) { 182 202 s = strings.TrimSpace(s) 183 - if !strings.HasPrefix(s, "[") || !strings.HasSuffix(s, "]") { 184 - return nil, fmt.Errorf("pos= value must be enclosed in square brackets, got %q", s) 203 + inner, err := trimSurrounding(s, '[', ']') 204 + if err != nil { 205 + return nil, fmt.Errorf("error parsing pos= value: %w", err) 185 206 } 186 - s = s[1 : len(s)-1] 207 + s = inner 187 208 var specs []posSpec 188 209 for _, p := range strings.Fields(s) { 189 210 parts := strings.SplitN(p, ":", 3) ··· 218 239 return specs, nil 219 240 } 220 241 242 + // parseArgsList parses a bracket-enclosed, comma-separated list of arg strings, 243 + // e.g. "[list, int]" → ["list", "int"]. 244 + func parseArgsList(s string) ([]string, error) { 245 + inner, err := trimSurrounding(s, '[', ']') 246 + if err != nil { 247 + return nil, fmt.Errorf("error parsing args: %w", err) 248 + } 249 + var result []string 250 + for tok := range strings.SplitSeq(inner, ",") { 251 + tok = strings.TrimSpace(tok) 252 + if tok != "" { 253 + result = append(result, tok) 254 + } 255 + } 256 + return result, nil 257 + } 258 + 221 259 // matchesErrSpec reports whether act satisfies all discriminating constraints 222 260 // in ea. It is a pure predicate — it never calls t.Errorf. 223 261 // ··· 280 318 msg := r.errorMessage(val) 281 319 if !strings.Contains(msg, ea.contains) { 282 320 t.Errorf("path %s: expected error message to contain %q, got %q", path, ea.contains, msg) 321 + } 322 + } 323 + // Validate Msg() args (order-independent). 324 + if len(ea.msgArgs) > 0 { 325 + var e cueerrors.Error 326 + if errors.As(val.Err(), &e) { 327 + checkMsgArgs(t, path, e, ea.msgArgs, "@test(err, args=...)") 283 328 } 284 329 } 285 330 // Validate error positions. ··· 441 486 if needWriteback { 442 487 r.enqueueSubErrPosWrites(pa, posUpdates) 443 488 } 489 + // Validate Msg() args for matched pairs (order-independent). 490 + for _, p := range pairs { 491 + if len(p.exp.msgArgs) > 0 { 492 + checkMsgArgs(t, path, p.act, p.exp.msgArgs, "@test(err, suberr=...)") 493 + } 494 + } 444 495 } 445 496 446 497 // enqueueSubErrPosWrites applies all sub-error position updates atomically to ··· 454 505 if p.Filename() == "" || p.Filename() == pa.srcFileName { 455 506 return fmt.Sprintf("%d:%d", p.Line()-pa.baseLine, p.Column()) 456 507 } 457 - return fmt.Sprintf("%s:%d:%d", r.relFilename(p.Filename()), p.Line(), p.Column()) 508 + return p.String() 458 509 } 459 510 460 511 func (r *inlineRunner) enqueueSubErrPosWrites(pa parsedTestAttr, updates []posUpdate) { ··· 541 592 // cueerrors.Error (primary position first, then input positions sorted). 542 593 // Unlike cueerrors.Positions, this works on an individual error rather than 543 594 // potentially a list (where cueerrors.Positions only sees the first element). 595 + // 596 + // TODO: is this a bug in cueerrors.Positions()? 544 597 func positionsFromSingleError(e cueerrors.Error) []token.Pos { 545 598 var a []token.Pos 546 599 if p := e.Position(); p.File() != nil { ··· 743 796 return err.Error() 744 797 } 745 798 return "" 799 + } 800 + 801 + // checkMsgArgs checks that the Msg() args of e include all strings in expected 802 + // (matched via fmt.Sprint, order-independent). directive is used in error messages. 803 + func checkMsgArgs(t testing.TB, path cue.Path, e cueerrors.Error, expected []string, directive string) { 804 + t.Helper() 805 + _, actualArgs := e.Msg() 806 + for _, exp := range expected { 807 + if !slices.ContainsFunc(actualArgs, func(a any) bool { return fmt.Sprint(a) == exp }) { 808 + var actual []string 809 + for _, a := range actualArgs { 810 + actual = append(actual, fmt.Sprint(a)) 811 + } 812 + t.Errorf("path %s: %s: args: expected %q in Msg() args, got %v", path, directive, exp, actual) 813 + } 814 + } 746 815 } 747 816 748 817 // findDescendantError walks val looking for any descendant with an error
+80 -1
internal/cuetxtar/inline_test.go
··· 14 14 package cuetxtar 15 15 16 16 import ( 17 + "fmt" 17 18 "slices" 18 19 "strings" 19 20 "testing" ··· 22 23 "cuelang.org/go/cue/ast" 23 24 "cuelang.org/go/cue/cuecontext" 24 25 "cuelang.org/go/cue/parser" 26 + "cuelang.org/go/cue/token" 25 27 "golang.org/x/tools/txtar" 26 28 ) 29 + 30 + // stubError implements cueerrors.Error with a fixed Msg() return. 31 + type stubError struct { 32 + format string 33 + args []any 34 + } 35 + 36 + func (s stubError) Position() token.Pos { return token.NoPos } 37 + func (s stubError) InputPositions() []token.Pos { return nil } 38 + func (s stubError) Error() string { return fmt.Sprintf(s.format, s.args...) } 39 + func (s stubError) Path() []string { return nil } 40 + func (s stubError) Msg() (string, []any) { return s.format, s.args } 41 + func TestCheckMsgArgs(t *testing.T) { 42 + path := testMakePath("field") 43 + tests := []struct { 44 + name string 45 + args []any // actual Msg() args 46 + expected []string // expected strings passed to checkMsgArgs 47 + wantFail bool 48 + }{ 49 + { 50 + name: "exact match passes", 51 + args: []any{"list", "int"}, 52 + expected: []string{"list", "int"}, 53 + }, 54 + { 55 + name: "subset passes — one of two args", 56 + args: []any{"list", "int"}, 57 + expected: []string{"list"}, 58 + }, 59 + { 60 + name: "subset passes — other arg", 61 + args: []any{"list", "int"}, 62 + expected: []string{"int"}, 63 + }, 64 + { 65 + name: "order-independent — reversed", 66 + args: []any{"list", "int"}, 67 + expected: []string{"int", "list"}, 68 + }, 69 + { 70 + name: "extra actual args are allowed", 71 + args: []any{"list", "int", "extra"}, 72 + expected: []string{"list", "int"}, 73 + }, 74 + { 75 + name: "missing expected arg fails", 76 + args: []any{"list"}, 77 + expected: []string{"list", "int"}, 78 + wantFail: true, 79 + }, 80 + { 81 + name: "empty expected always passes", 82 + args: []any{"list", "int"}, 83 + expected: nil, 84 + }, 85 + { 86 + name: "no actual args with non-empty expected fails", 87 + args: nil, 88 + expected: []string{"list"}, 89 + wantFail: true, 90 + }, 91 + } 92 + for _, tc := range tests { 93 + t.Run(tc.name, func(t *testing.T) { 94 + rec := &failCapture{TB: t} 95 + checkMsgArgs(rec, path, stubError{args: tc.args}, tc.expected, "@test(err, args=...)") 96 + if rec.failed != tc.wantFail { 97 + if tc.wantFail { 98 + t.Errorf("expected failure but checkMsgArgs passed") 99 + } else { 100 + t.Errorf("unexpected failure:\n%s", rec.msgs.String()) 101 + } 102 + } 103 + }) 104 + } 105 + } 27 106 28 107 // testMakePath creates a CUE path from a dot-separated string for test use. 29 108 func testMakePath(s string) cue.Path { ··· 372 451 // reports an error when two paths do NOT share the same vertex. 373 452 // This cannot be expressed as a txtar inline test (we can't annotate "should 374 453 // fail"), so it is tested here by calling runShareIDChecks directly with a 375 - // todoCapture that captures errors without propagating to the parent test. 454 + // failCapture that captures errors without propagating to the parent test. 376 455 func TestRunShareIDChecks_Negative(t *testing.T) { 377 456 ctx := cuecontext.New() 378 457 r := &inlineRunner{}