this repo has no description
0
fork

Configure Feed

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

internal/cuetxtar: add hint= flag and unknown-flag validation to @test directives

All @test directives that can fail now accept a
guidance="hint" key that is logged (via t.Log) after
each assertion failure. Intended as guidance for
automated tools such as AI assistants.

Unknown key= flags now cause a parse error for
directives that do not have their own flag parser
(eq, leq, kind, closed, debugCheck). Directives
with custom parsers (err, todo, skip, shareID)
validate their own flags; err now also rejects
unknown keys.

Also:
- pos= in parsePosSpecs now uses strings.SplitSeq
for consistency with parseArgsList
- Clarify at= subPA copy: comment why any=false
and at field is intentionally omitted
- Update v0.7errorprop.txtar to inline @test format

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

+190 -94
+13 -2
CLAUDE.md
··· 126 126 the `out/eval/stats` section (promote v3 stats from `out/evalalpha/stats` if 127 127 needed). 128 128 129 - 7. **Add `out/errors.txt`** if any errors e xist in the test (leave empty initially; 129 + 7. **Add `out/errors.txt`** if any errors exist in the test (leave empty initially; 130 130 `CUE_UPDATE=1` fills it in automatically). 131 131 132 132 8. **Add `out/todo.txt`** if there are noteworthy differences ··· 145 145 10. **DO NOT** introduce any flags in new @test(err) directives. Only maintainers 146 146 of the CUE project should do so. 147 147 148 - 11. **@test(eq, ...) placement**: Prefer placing the eq directive attribute 148 + 11. **`hint=` flag**: Any `@test(...)` directive may carry a `hint="..."` flag. 149 + When a test fails, the runner logs the hint text as an additional note. 150 + **If you (as an AI) encounter a test failure on a field carrying `hint="..."`, 151 + read that text before diagnosing or fixing the failure.** The hint may explain 152 + known evaluator differences, version-specific behavior, or why the expected value is 153 + correct despite appearances. Example: 154 + ``` 155 + a: c: 1 @test(err, code=eval, contains="field not allowed", 156 + hint="v3 only reports the direct definition position; see out/todo.txt") 157 + ``` 158 + 159 + 12. **@test(eq, ...) placement**: Prefer placing the eq directive attribute 149 160 either directly after a field for single field test, or as a field decl 150 161 at the end of a struct of a test that is struct based. Do NOT place `@test` 151 162 as a field attribute after a closing `}` — e.g., `} @test(eq, ...)` is
+12 -68
cue/testdata/eval/v0.7errorprop.txtar
··· 1 1 // NOTE: do not add more tests to this file, as it may obfuscate the test case. 2 - #todo:inline: medium — multiple error cases; annotate with @test(err) per field 3 2 -- in.cue -- 4 3 a: #A 5 - a: c: 1 6 - #A: {} 4 + a: c: 1 @test(err, code=eval, 5 + contains="field not allowed", 6 + pos=[in.cue:2:4], 7 + hint="more positions may be allowed, see out/todo.txt for details") 8 + #A: {} @test(eq, {}) 9 + -- out/errors.txt -- 10 + [eval] a.c: field not allowed: 11 + ./in.cue:2:4 7 12 -- out/eval/stats -- 8 13 Leaks: 0 9 14 Freed: 4 ··· 14 19 Unifications: 4 15 20 Conjuncts: 6 16 21 Disjuncts: 5 17 - -- out/evalalpha -- 18 - Errors: 19 - a.c: field not allowed: 20 - ./in.cue:2:4 21 - 22 - Result: 23 - (_|_){ 24 - // [eval] 25 - a: (_|_){ 26 - // [eval] 27 - c: (_|_){ 28 - // [eval] a.c: field not allowed: 29 - // ./in.cue:2:4 30 - } 31 - } 32 - #A: (#struct){ 33 - } 34 - } 35 - -- diff/-out/evalalpha<==>+out/eval -- 36 - diff old new 37 - --- old 38 - +++ new 39 - @@ -1,8 +1,6 @@ 40 - Errors: 41 - a.c: field not allowed: 42 - - ./in.cue:1:4 43 - ./in.cue:2:4 44 - - ./in.cue:3:5 45 - 46 - Result: 47 - (_|_){ 48 - @@ -11,9 +9,7 @@ 49 - // [eval] 50 - c: (_|_){ 51 - // [eval] a.c: field not allowed: 52 - - // ./in.cue:1:4 53 - // ./in.cue:2:4 54 - - // ./in.cue:3:5 55 - } 56 - } 57 - #A: (#struct){ 58 - -- diff/todo/p2 -- 59 - Missing error positions. 60 - -- out/eval -- 61 - Errors: 62 - a.c: field not allowed: 63 - ./in.cue:1:4 64 - ./in.cue:2:4 65 - ./in.cue:3:5 66 - 67 - Result: 68 - (_|_){ 69 - // [eval] 70 - a: (_|_){ 71 - // [eval] 72 - c: (_|_){ 73 - // [eval] a.c: field not allowed: 74 - // ./in.cue:1:4 75 - // ./in.cue:2:4 76 - // ./in.cue:3:5 77 - } 78 - } 79 - #A: (#struct){ 80 - } 81 - } 82 22 -- out/compile -- 83 23 --- in.cue 84 24 { ··· 88 28 } 89 29 #A: {} 90 30 } 31 + -- out/todo.txt -- 32 + v2 reports additional error positions (in.cue:1:4 and in.cue:3:5) for the "field 33 + not allowed" error on a.c. v3 only reports the direct field definition position 34 + (in.cue:2:4).
+23
doc/specs/inline-test-attributes/spec.md
··· 78 78 79 79 --- 80 80 81 + ### Requirement: `guidance=` universal flag 82 + Any `@test(...)` directive that can produce a test failure MAY carry a `guidance="..."` key-value flag. When an assertion fails, the runner logs the guidance string as an additional note after the failure message. This is intended to provide context for automated tools (such as AI assistants) that inspect test failures, and for human readers who need background on why the expected value was chosen. 83 + 84 + `guidance=` is purely informational — it has no effect on whether a test passes or fails. It does not modify any comparison or matching logic. It is silently accepted by all directives that report failures (`eq`, `leq`, `err`, `kind`, `closed`, `debugCheck`). 85 + 86 + ```cue 87 + a: c: 1 @test(err, code=eval, 88 + contains="field not allowed", 89 + guidance="v3 only reports the direct definition position; see out/todo.txt") 90 + ``` 91 + 92 + When an AI tool encounters a test failure on a field that carries `guidance="..."`, it SHOULD read the guidance text before attempting to diagnose or fix the failure. The guidance may explain known evaluator differences, link to a tracking issue, or describe why the expected value is correct despite appearances. 93 + 94 + #### Scenario: guidance= is logged on failure 95 + - **WHEN** `@test(eq, 42, guidance="check the evaluator cycle")` is declared and the field evaluates to `43` 96 + - **THEN** the test fails AND the runner logs `hint: check the evaluator cycle` immediately after the failure message 97 + 98 + #### Scenario: guidance= has no effect on passing test 99 + - **WHEN** `@test(eq, 42, guidance="...")` is declared and the field evaluates to `42` 100 + - **THEN** the test passes; the guidance string is not logged 101 + 102 + --- 103 + 81 104 ### Requirement: `eq` directive 82 105 The `eq` directive SHALL assert equality between the evaluated value at the annotated field and an expected CUE expression. The comparison is performed by walking the expected expression as a parsed AST and comparing it structurally against the evaluated value — the expected expression is never compiled, which prevents evaluator bugs from masking mismatches. 83 106
+41 -2
internal/cuetxtar/inline.go
··· 159 159 // are logged rather than reported as errors; a pass emits a warning. 160 160 isTodo bool 161 161 162 + // hint is an optional message printed when the assertion fails 163 + // (from hint="..."). Intended as guidance for automated tools 164 + // such as AI assistants reviewing test failures. 165 + hint string 166 + 162 167 // srcAttr is the original AST attribute node (needed for CUE_UPDATE write-back). 163 168 srcAttr *ast.Attribute 164 169 ··· 234 239 result.errArgs = &ea 235 240 } 236 241 242 + // Extract the universal guidance= flag and reject unknown key= flags. 243 + // Positional args (kv.Key() == "") are accepted by directives as needed. 244 + // Directives with their own flag parsers (err, todo, skip, shareID) are 245 + // responsible for validating their own flags. 246 + for _, kv := range parsed.Fields[1:] { 247 + switch kv.Key() { 248 + case "hint": 249 + result.hint = kv.Value() 250 + case "": 251 + // Positional arg — accepted. 252 + default: 253 + switch result.directive { 254 + case "err", "todo", "skip", "shareID": 255 + // These directives parse their own flags elsewhere. 256 + default: 257 + return result, fmt.Errorf("@test(%s): unknown flag %q", result.directive, kv.Key()) 258 + } 259 + } 260 + } 261 + 237 262 return result, nil 263 + } 264 + 265 + // logHint logs hint as an additional note following a test failure. 266 + // Call immediately after t.Errorf when pa.hint is set. 267 + func logHint(t testing.TB, hint string) { 268 + if hint != "" { 269 + t.Helper() 270 + t.Log("hint:", hint) 271 + } 238 272 } 239 273 240 274 // ───────────────────────────────────────────────────────────────────────────── ··· 1147 1181 // Report the failure (unless already annotated with a skip). 1148 1182 if !hasSkip { 1149 1183 t.Errorf("path %s: %v", path, cmpErr) 1184 + logHint(t, pa.hint) 1150 1185 } 1151 1186 } 1152 1187 ··· 1177 1212 t.Errorf("path %s: @test(leq, ...): cannot compile constraint: %v", path, constraint.Err()) 1178 1213 return 1179 1214 } 1180 - r.runLeqAssertion(t, path, val, constraint) 1215 + r.runLeqAssertion(t, path, val, constraint, pa.hint) 1181 1216 } 1182 1217 1183 1218 // runLeqAssertion asserts that val is subsumed by constraint (constraint ⊑ val, i.e. val is at least as specific). 1184 - func (r *inlineRunner) runLeqAssertion(t testing.TB, path cue.Path, val, constraint cue.Value) { 1219 + func (r *inlineRunner) runLeqAssertion(t testing.TB, path cue.Path, val, constraint cue.Value, hint string) { 1185 1220 t.Helper() 1186 1221 if err := constraint.Subsume(val); err != nil { 1187 1222 t.Errorf("path %s: @test(leq): value %v is not subsumed by constraint %v: %v", path, val, constraint, err) 1223 + logHint(t, hint) 1188 1224 } 1189 1225 } 1190 1226 ··· 1212 1248 } 1213 1249 if gotKind != expectedKind { 1214 1250 t.Errorf("path %s: @test(kind=%s): got kind %v, want %v", path, expectedStr, gotKind, expectedKind) 1251 + logHint(t, pa.hint) 1215 1252 } 1216 1253 } 1217 1254 ··· 1255 1292 got := val.IsClosed() 1256 1293 if got != expected { 1257 1294 t.Errorf("path %s: @test(closed): got closed=%v, want %v", path, got, expected) 1295 + logHint(t, pa.hint) 1258 1296 } 1259 1297 } 1260 1298 ··· 1285 1323 } 1286 1324 if !match { 1287 1325 t.Errorf("path %s: @test(debugCheck) mismatch:\ngot: %q\nwant: %q", path, actual, expected) 1326 + logHint(t, pa.hint) 1288 1327 } 1289 1328 } 1290 1329
+31 -12
internal/cuetxtar/inline_err.go
··· 75 75 // pos lists expected error positions as (deltaLine:col) pairs relative to 76 76 // the line containing the @test attribute. 77 77 pos []posSpec 78 - // posSet is true when pos= was 79 - // explicitly provided (including pos=[] to assert no positions). 78 + // posSet is true when pos= was explicitly provided (including pos=[] to 79 + // assert no positions). 80 80 posSet bool 81 81 // suberrs holds expected sub-error specs for multi-error (list) values. 82 82 // Each entry is matched order-independently against errors.Errors(val.Err()). ··· 154 154 return ea, fmt.Errorf("@test(err, args=...): %w", err) 155 155 } 156 156 ea.msgArgs = args 157 + case kv.Key() == "hint": 158 + // hint= is a universal flag handled at the parsedTestAttr level; skip here. 159 + case kv.Key() == "": 160 + // Positional arg (e.g. "any"); already handled above. 161 + default: 162 + return ea, fmt.Errorf("@test(err): unknown flag %q", kv.Key()) 157 163 } 158 164 } 159 165 return ea, nil ··· 203 209 } 204 210 s = inner 205 211 var specs []posSpec 206 - for _, p := range strings.Split(s, ",") { 212 + for p := range strings.SplitSeq(s, ",") { 207 213 p = strings.TrimSpace(p) 208 214 if p == "" { 209 215 continue ··· 289 295 // Bare @test(err) — just check that the value is an error. 290 296 if !r.isError(val) { 291 297 t.Errorf("path %s: expected error, got non-error value", path) 298 + logHint(t, pa.hint) 292 299 } 293 300 return 294 301 } ··· 310 317 subPA.errArgs = &errArgs{ 311 318 codes: ea.codes, 312 319 contains: ea.contains, 313 - any: false, 320 + any: false, // don't cascade any= to the sub-check 314 321 posSet: ea.posSet, 315 322 pos: ea.pos, 316 323 suberrs: ea.suberrs, 317 324 msgArgs: ea.msgArgs, 325 + // at is intentionally omitted: we already navigated to the sub-path. 318 326 } 319 327 r.runErrAssertion(t, subFullPath, subVal, subPA) 320 328 return ··· 329 337 found := r.findDescendantError(val, ea) 330 338 if !found { 331 339 t.Errorf("path %s: expected a descendant error with code=%v, none found", path, ea.codes) 340 + logHint(t, pa.hint) 332 341 } 333 342 return 334 343 } 335 344 336 345 if !r.isError(val) { 337 346 t.Errorf("path %s: expected error, got non-error value", path) 347 + logHint(t, pa.hint) 338 348 return 339 349 } 340 350 ··· 343 353 gotCode := r.errorCode(val) 344 354 if !ea.matchesCode(gotCode) { 345 355 t.Errorf("path %s: expected error code %v, got %q", path, ea.codes, gotCode) 356 + logHint(t, pa.hint) 346 357 } 347 358 } 348 359 // Validate error message contains. ··· 350 361 msg := r.errorMessage(val) 351 362 if !strings.Contains(msg, ea.contains) { 352 363 t.Errorf("path %s: expected error message to contain %q, got %q", path, ea.contains, msg) 364 + logHint(t, pa.hint) 353 365 } 354 366 } 355 367 // Validate Msg() args (order-independent). 356 368 if len(ea.msgArgs) > 0 { 357 369 var e cueerrors.Error 358 370 if errors.As(val.Err(), &e) { 359 - checkMsgArgs(t, path, e, ea.msgArgs, "@test(err, args=...)") 371 + checkMsgArgs(t, path, e, ea.msgArgs, "@test(err, args=...)", pa.hint) 360 372 } 361 373 } 362 374 // Validate error positions. ··· 401 413 for i, a := range actual { 402 414 t.Logf(" actual[%d]: %s", i, a.Error()) 403 415 } 416 + logHint(t, pa.hint) 404 417 return 405 418 } 406 419 ··· 461 474 desc = fmt.Sprintf("code=%v", exp.codes) 462 475 } 463 476 t.Errorf("path %s: @test(err, suberr=...): no sub-error matched %q", path, desc) 477 + logHint(t, pa.hint) 464 478 } 465 479 } 466 480 // Report pass-1 specs that also failed to match. ··· 470 484 if !expMatched[i] && exp.posSet && len(exp.pos) > 0 { 471 485 if exp.contains == "" { 472 486 t.Errorf("path %s: @test(err, suberr=...): no sub-error matched pos=%v", path, exp.pos) 487 + logHint(t, pa.hint) 473 488 continue 474 489 } 475 490 found := false ··· 478 493 continue 479 494 } 480 495 found = true 481 - r.reportPosMismatch(t, path, "@test(err, suberr=...)", positionsFromSingleError(act), exp.pos, pa.baseLine) 496 + r.reportPosMismatch(t, path, "@test(err, suberr=...)", positionsFromSingleError(act), exp.pos, pa.baseLine, pa.hint) 482 497 break 483 498 } 484 499 if !found { 485 500 t.Errorf("path %s: @test(err, suberr=...): no sub-error matched pos=%v contains=%q", 486 501 path, exp.pos, exp.contains) 502 + logHint(t, pa.hint) 487 503 } 488 504 } 489 505 } ··· 513 529 continue 514 530 } 515 531 // Report mismatch. 516 - r.reportPosMismatch(t, path, "@test(err, suberr=...)", positions, p.exp.pos, pa.baseLine) 532 + r.reportPosMismatch(t, path, "@test(err, suberr=...)", positions, p.exp.pos, pa.baseLine, pa.hint) 517 533 } 518 534 if needWriteback { 519 535 r.enqueueSubErrPosWrites(pa, posUpdates) ··· 521 537 // Validate Msg() args for matched pairs (order-independent). 522 538 for _, p := range pairs { 523 539 if len(p.exp.msgArgs) > 0 { 524 - checkMsgArgs(t, path, p.act, p.exp.msgArgs, "@test(err, suberr=...)") 540 + checkMsgArgs(t, path, p.act, p.exp.msgArgs, "@test(err, suberr=...)", pa.hint) 525 541 } 526 542 } 527 543 } ··· 690 706 // expected specs. directive is included verbatim in each error message. 691 707 // If counts differ, only the count error is reported. Otherwise each 692 708 // unmatched expected spec is reported individually (order-independent). 693 - func (r *inlineRunner) reportPosMismatch(t testing.TB, path cue.Path, directive string, positions []token.Pos, specs []posSpec, baseLine int) { 709 + func (r *inlineRunner) reportPosMismatch(t testing.TB, path cue.Path, directive string, positions []token.Pos, specs []posSpec, baseLine int, hint string) { 694 710 t.Helper() 695 711 if len(positions) != len(specs) { 696 712 t.Errorf("path %s: %s", path, formatPosCountMismatch(directive, len(positions), len(specs))) 697 713 for _, p := range positions { 698 714 t.Logf(" actual: %d:%d", p.Line(), p.Column()) 699 715 } 716 + logHint(t, hint) 700 717 return 701 718 } 702 719 used := make([]bool, len(positions)) ··· 721 738 for _, p := range positions { 722 739 t.Logf(" actual: %d:%d", p.Line(), p.Column()) 723 740 } 741 + logHint(t, hint) 724 742 } 725 743 } 726 744 } ··· 752 770 return 753 771 } 754 772 755 - r.reportPosMismatch(t, path, "@test(err, pos=...)", positions, expected, pa.baseLine) 773 + r.reportPosMismatch(t, path, "@test(err, pos=...)", positions, expected, pa.baseLine, pa.hint) 756 774 } 757 775 758 776 // enqueuePosWrite formats positions as pos specs and enqueues a write-back ··· 766 784 for i, p := range positions { 767 785 parts[i] = r.formatPosSpec(p, pa) 768 786 } 769 - newPosStr := strings.Join(parts, " ") 787 + newPosStr := strings.Join(parts, ", ") 770 788 771 789 old := pa.srcAttr.Text 772 790 start := strings.Index(old, "pos=[") ··· 882 900 883 901 // checkMsgArgs checks that the Msg() args of e include all strings in expected 884 902 // (matched via fmt.Sprint, order-independent). directive is used in error messages. 885 - func checkMsgArgs(t testing.TB, path cue.Path, e cueerrors.Error, expected []string, directive string) { 903 + func checkMsgArgs(t testing.TB, path cue.Path, e cueerrors.Error, expected []string, directive string, hint string) { 886 904 t.Helper() 887 905 _, actualArgs := e.Msg() 888 906 for _, exp := range expected { ··· 892 910 actual = append(actual, fmt.Sprint(a)) 893 911 } 894 912 t.Errorf("path %s: %s: args: expected %q in Msg() args, got %v", path, directive, exp, actual) 913 + logHint(t, hint) 895 914 } 896 915 } 897 916 }
+70 -10
internal/cuetxtar/inline_test.go
··· 92 92 for _, tc := range tests { 93 93 t.Run(tc.name, func(t *testing.T) { 94 94 rec := &failCapture{TB: t} 95 - checkMsgArgs(rec, path, stubError{args: tc.args}, tc.expected, "@test(err, args=...)") 95 + checkMsgArgs(rec, path, stubError{args: tc.args}, tc.expected, "@test(err, args=...)", "") 96 96 if rec.failed != tc.wantFail { 97 97 if tc.wantFail { 98 98 t.Errorf("expected failure but checkMsgArgs passed") ··· 159 159 {deltaLine: 0, col: 5}, 160 160 {fileName: "fixture.cue", absLine: 1, col: 13}, 161 161 }, 162 + }, 163 + { 164 + name: "mixed relative and absolute whitespace-only is rejected", 165 + input: "[0:5 fixture.cue:1:13]", 166 + wantErr: true, 162 167 }, 163 168 { 164 169 name: "missing brackets", ··· 384 389 // Positions: 1:4 (the 1) and 1:8 (the 2). 385 390 // baseLine=1, deltaLine=0 → expected line 1. 386 391 name: "field attr pos relative same line", 387 - archive: "-- test.cue --\nx: 1 & 2 @test(err, pos=[0:4 0:8])\n", 392 + archive: "-- test.cue --\nx: 1 & 2 @test(err, pos=[0:4, 0:8])\n", 388 393 }, 389 394 { 390 395 // Field-attribute on a struct with conflict below. ··· 395 400 // line 4: } 396 401 // baseLine=1 (field x on line 1), deltas: 1→line 2, 2→line 3. 397 402 name: "field attr pos relative below", 398 - archive: "-- test.cue --\nx: {\n\ta: 1\n\ta: 2\n} @test(err, pos=[1:5 2:5])\n", 403 + archive: "-- test.cue --\nx: {\n\ta: 1\n\ta: 2\n} @test(err, pos=[1:5, 2:5])\n", 399 404 }, 400 405 { 401 406 // Decl-attribute form inside a struct. 402 407 // Original source: 403 408 // line 1: x: { 404 - // line 2: @test(err, pos=[1:5 2:5]) 409 + // line 2: @test(err, pos=[1:5, 2:5]) 405 410 // line 3: a: 1 406 411 // line 4: a: 2 407 412 // line 5: } ··· 413 418 // baseLine = sl.Lbrace.Line() - 0 = 1 (the "{" on line 1). 414 419 // deltaLine=1 → line 2 (a: 1), deltaLine=2 → line 3 (a: 2). 415 420 name: "decl attr pos relative", 416 - archive: "-- test.cue --\nx: {\n\t@test(err, pos=[1:5 2:5])\n\ta: 1\n\ta: 2\n}\n", 421 + archive: "-- test.cue --\nx: {\n\t@test(err, pos=[1:5, 2:5])\n\ta: 1\n\ta: 2\n}\n", 417 422 }, 418 423 { 419 424 // Decl-attribute at file-level with a conflict. 420 425 // Original source: 421 - // line 1: @test(err, pos=[0:4 0:8]) 426 + // line 1: @test(err, pos=[0:4, 0:8]) 422 427 // line 2: x: 1 & 2 423 428 // After stripping the @test (line 1 removed): 424 429 // line 1: x: 1 & 2 ··· 426 431 // deltaLine=0 → line 1 (x: 1 & 2). 427 432 // Positions: 1:4 and 1:8. 428 433 name: "file-level decl attr pos relative", 429 - archive: "-- test.cue --\n@test(err, pos=[0:4 0:8])\nx: 1 & 2\n", 434 + archive: "-- test.cue --\n@test(err, pos=[0:4, 0:8])\nx: 1 & 2\n", 430 435 }, 431 436 { 432 437 // Multiple fields: second field's baseLine accounts for 433 438 // the stripped @test on the first field. 434 439 // Original: 435 440 // line 1: x: 1 @test(eq, 1) 436 - // line 2: y: 1 & 2 @test(err, pos=[0:4 0:8]) 441 + // line 2: y: 1 & 2 @test(err, pos=[0:4, 0:8]) 437 442 // After stripping (both on same line, no extra newlines): 438 443 // line 1: x: 1 439 444 // line 2: y: 1 & 2 440 445 // baseLine for y = 2, deltaLine=0 → line 2. 441 446 name: "field attr after prior field attr", 442 - archive: "-- test.cue --\nx: 1 @test(eq, 1)\ny: 1 & 2 @test(err, pos=[0:4 0:8])\n", 447 + archive: "-- test.cue --\nx: 1 @test(eq, 1)\ny: 1 & 2 @test(err, pos=[0:4, 0:8])\n", 443 448 }, 444 449 { 445 450 // Absolute position form: filename:absLine:col. 446 451 // After stripping, "test.cue" has "x: 1 & 2" on line 1. 447 452 name: "absolute pos form", 448 - archive: "-- test.cue --\nx: 1 & 2 @test(err, pos=[test.cue:1:4 test.cue:1:8])\n", 453 + archive: "-- test.cue --\nx: 1 & 2 @test(err, pos=[test.cue:1:4, test.cue:1:8])\n", 449 454 }, 450 455 } 451 456 for _, tt := range tests { ··· 606 611 } 607 612 }) 608 613 } 614 + 615 + func TestHintFlag(t *testing.T) { 616 + parseAttr := func(src string) (parsedTestAttr, error) { 617 + f, err := parser.ParseFile("test.cue", src) 618 + if err != nil { 619 + return parsedTestAttr{}, err 620 + } 621 + field := f.Decls[0].(*ast.Field) 622 + return parseTestAttr(field.Attrs[0]) 623 + } 624 + 625 + t.Run("hint= is parsed into pa.hint", func(t *testing.T) { 626 + pa, err := parseAttr(`x: 1 @test(eq, 42, hint="fix the evaluator")`) 627 + if err != nil { 628 + t.Fatalf("unexpected error: %v", err) 629 + } 630 + if pa.hint != "fix the evaluator" { 631 + t.Errorf("got hint=%q, want %q", pa.hint, "fix the evaluator") 632 + } 633 + }) 634 + 635 + t.Run("hint= works on err directive", func(t *testing.T) { 636 + pa, err := parseAttr(`x: 1 @test(err, code=eval, hint="check the cycle")`) 637 + if err != nil { 638 + t.Fatalf("unexpected error: %v", err) 639 + } 640 + if pa.hint != "check the cycle" { 641 + t.Errorf("got hint=%q, want %q", pa.hint, "check the cycle") 642 + } 643 + }) 644 + 645 + t.Run("unknown flag rejected for eq", func(t *testing.T) { 646 + _, err := parseAttr(`x: 1 @test(eq, 42, foo="bar")`) 647 + if err == nil || !strings.Contains(err.Error(), "unknown flag") { 648 + t.Errorf("expected unknown flag error, got: %v", err) 649 + } 650 + }) 651 + 652 + t.Run("unknown flag rejected for err", func(t *testing.T) { 653 + _, err := parseAttr(`x: 1 @test(err, unknownKey=x)`) 654 + if err == nil || !strings.Contains(err.Error(), "unknown flag") { 655 + t.Errorf("expected unknown flag error, got: %v", err) 656 + } 657 + }) 658 + 659 + t.Run("no hint= gives empty hint", func(t *testing.T) { 660 + pa, err := parseAttr(`x: 1 @test(eq, 42)`) 661 + if err != nil { 662 + t.Fatalf("unexpected error: %v", err) 663 + } 664 + if pa.hint != "" { 665 + t.Errorf("expected empty hint, got %q", pa.hint) 666 + } 667 + }) 668 + }