this repo has no description
0
fork

Configure Feed

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

internal/cuetxtar: split inline.go

Pure mechanical reorganization: no logic changes.

Split inline.go into focused files:

- inline_attr.go: @test attribute parsing,
AST extraction, and inline-mode detection
(medium scrutiny — no evaluator interaction)
- inline_format.go: value formatting for
@test(eq) bodies and @test(debug) output
(low scrutiny — changes are visible in
test output)
- inline_shareid.go: @test(shareID=...) vertex-
sharing assertions (high scrutiny — verifies
evaluator internals)
- inline.go: core runner, execution pipeline,
and assertion dispatch (highest scrutiny)

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

+1229 -1138
+1 -1138
internal/cuetxtar/inline.go
··· 39 39 "os" 40 40 "path/filepath" 41 41 "slices" 42 - "strconv" 43 42 "strings" 44 43 "testing" 45 44 ··· 51 50 "cuelang.org/go/cue/cuecontext" 52 51 cueerrors "cuelang.org/go/cue/errors" 53 52 "cuelang.org/go/cue/format" 54 - "cuelang.org/go/cue/literal" 55 53 "cuelang.org/go/cue/load" 56 54 "cuelang.org/go/cue/parser" 57 55 "cuelang.org/go/internal" 58 - "cuelang.org/go/internal/core/adt" 59 - "cuelang.org/go/internal/core/debug" 60 56 "cuelang.org/go/internal/cuetdtest" 61 57 "cuelang.org/go/internal/cuetest" 62 - "cuelang.org/go/internal/value" 63 58 ) 64 59 65 60 // RunInlineTests iterates over txtar archives in dir, detects inline-assertion ··· 139 134 ir.r.runArchive() 140 135 } 141 136 142 - // ───────────────────────────────────────────────────────────────────────────── 143 - // Section 1: Attribute Parsing Utilities 144 - // ───────────────────────────────────────────────────────────────────────────── 145 - 146 - // parsedTestAttr holds the result of parsing a single @test(...) attribute. 147 - type parsedTestAttr struct { 148 - // directive is the primary directive name, e.g. "eq", "err", "kind", 149 - // "closed", "leq", "skip", "permute", "file", "desc". 150 - directive string 151 - 152 - // version is the optional version suffix from directive:vN, e.g. "v3". 153 - // Empty means unversioned. 154 - version string 155 - 156 - // raw is the parsed internal.Attr for accessing remaining arguments. 157 - raw *internal.Attr 158 - 159 - // For "err" directives, parsed sub-options are stored here. 160 - errArgs *errArgs 161 - 162 - // isTodo marks directives produced by the "todo" version qualifier 163 - // (e.g. @test(eq:todo, X)). These run as expected-to-fail: failures 164 - // are logged rather than reported as errors; a pass emits a warning. 165 - isTodo bool 166 - 167 - // todoPriority is the p= value from a :todo directive, e.g. "1" for p=1. 168 - // 0 is the highest priority; empty means no priority specified. 169 - todoPriority string 170 - 171 - // isIncorrect marks a directive carrying the "incorrect" positional flag 172 - // (e.g. @test(eq, 3, incorrect) or @test(err, code=eval, incorrect)). 173 - // Applicable to any assertion directive. The assertion documents current 174 - // known-incorrect behavior: a pass is suppressed (logs a NOTE), but a 175 - // failure still propagates as a real test failure so that changes to the 176 - // incorrect value are always detected. 177 - isIncorrect bool 178 - 179 - // hint is an optional message printed when the assertion fails 180 - // (from hint="..."). Intended as guidance for automated tools 181 - // such as AI assistants reviewing test failures. 182 - hint string 183 - 184 - // srcAttr is the original AST attribute node (needed for CUE_UPDATE write-back). 185 - srcAttr *ast.Attribute 186 - 187 - // srcFileName is the archive .cue file name containing this attribute, 188 - // e.g. "in.cue" (needed to locate the file for CUE_UPDATE write-back). 189 - srcFileName string 190 - 191 - // baseLine is the effective 1-indexed line of the field carrying this 192 - // attribute in the stripped-and-formatted output. It may differ from 193 - // srcAttr.Pos().Line() when earlier @test attributes on preceding fields 194 - // contain embedded newlines in their bodies (which are stripped before 195 - // formatting, collapsing those extra lines). 196 - baseLine int 197 - } 198 - 199 - // parseTestAttr parses the body of a @test(...) attribute node. 200 - // It returns a parsedTestAttr for each logical directive in the attribute. 201 - // A single @test(...) contains exactly one directive (the first positional 202 - // argument or the key of the first key=value pair). 203 - func parseTestAttr(astAttr *ast.Attribute) (parsedTestAttr, error) { 204 - if name := astAttr.Name(); name != "test" { 205 - return parsedTestAttr{}, fmt.Errorf("not a @test attribute: @%s", name) 206 - } 207 - 208 - attr := internal.ParseAttr(astAttr) 209 - if attr.Err != nil { 210 - return parsedTestAttr{}, attr.Err 211 - } 212 - 213 - result := parsedTestAttr{ 214 - raw: attr, 215 - srcAttr: astAttr, 216 - } 217 - 218 - if len(attr.Fields) == 0 || (len(attr.Fields) == 1 && attr.Fields[0] == internal.KeyValue{}) { 219 - // @test() — empty placeholder or bare marker. 220 - result.directive = "" 221 - return result, nil 222 - } 223 - 224 - // The first field determines the directive. 225 - // Case 1: key=value form like desc="hello", shareID=name — directive is the key. 226 - // Case 2: positional form like eq, err, kind — directive (with optional :vN suffix) is the value. 227 - f0 := attr.Fields[0] 228 - if f0.Key() != "" { 229 - dir := f0.Key() 230 - // Key-based directives may carry a version suffix: "shareID" → directive="shareID", version="v3". 231 - if idx := strings.LastIndex(dir, ":"); idx >= 0 { 232 - result.directive = dir[:idx] 233 - result.version = dir[idx+1:] 234 - } else { 235 - result.directive = dir 236 - } 237 - } else { 238 - // May have version suffix: "eq:v3" → directive="eq", version="v3". 239 - dir := f0.Value() 240 - if idx := strings.LastIndex(dir, ":"); idx >= 0 { 241 - result.directive = dir[:idx] 242 - result.version = dir[idx+1:] 243 - } else { 244 - result.directive = dir 245 - } 246 - } 247 - 248 - // Parse directive-specific sub-options. 249 - switch result.directive { 250 - case "err": 251 - ea, err := parseErrArgs(attr) 252 - if err != nil { 253 - return result, err 254 - } 255 - result.errArgs = &ea 256 - } 257 - 258 - // Extract universal flags and reject unknown key= flags. 259 - // Positional args (kv.Key() == "") are accepted by directives as needed. 260 - // Directives with their own flag parsers (err, todo, skip, shareID) are 261 - // responsible for validating their own flags. 262 - for _, kv := range attr.Fields[1:] { 263 - switch kv.Key() { 264 - case "hint": 265 - result.hint = kv.Value() 266 - case "p": 267 - // p= is a universal priority flag (e.g. p=1 on err:todo). 268 - result.todoPriority = kv.Value() 269 - case "": 270 - // Positional arg — check for universal flags. 271 - if kv.Value() == "incorrect" { 272 - result.isIncorrect = true 273 - } 274 - case "at": 275 - // at= is accepted by eq, err, and shareID; each validates it 276 - // in its own handler. 277 - switch result.directive { 278 - case "eq", "err", "shareID": 279 - // Validated in their respective handlers. 280 - default: 281 - return result, fmt.Errorf("@test(%s): unknown flag %q", result.directive, kv.Key()) 282 - } 283 - default: 284 - switch result.directive { 285 - case "err", "todo", "skip", "shareID": 286 - // These directives parse their own flags elsewhere. 287 - default: 288 - return result, fmt.Errorf("@test(%s): unknown flag %q", result.directive, kv.Key()) 289 - } 290 - } 291 - } 292 - 293 - return result, nil 294 - } 295 - 296 - // logHint logs hint as an additional note following a test failure. 297 - // Call immediately after t.Errorf when pa.hint is set. 298 - func logHint(t testing.TB, hint string) { 299 - if hint != "" { 300 - t.Helper() 301 - t.Log("hint:", hint) 302 - } 303 - } 304 - 305 - // ───────────────────────────────────────────────────────────────────────────── 306 - // Section 2: AST Extraction and Stripping 307 - // ───────────────────────────────────────────────────────────────────────────── 308 - 309 - // attrRecord associates a parsed @test attribute with its location in the 310 - // evaluated CUE value. 311 - type attrRecord struct { 312 - // path is the full CUE path to the field carrying this attribute. 313 - path cue.Path 314 - 315 - // parsed is the parsed directive from this attribute. 316 - parsed parsedTestAttr 317 - 318 - // parseErr is non-nil when parseTestAttr failed. The runner reports the 319 - // error as a test failure and skips running the directive. 320 - parseErr error 321 - 322 - // fileLevel is true when this record comes from a file-level (top-level) 323 - // decl attribute rather than a field attribute or struct-level decl attribute. 324 - // A file-level @test(eq, VALUE) checks the entire file's evaluated value. 325 - fileLevel bool 326 - 327 - // isDeclAttr is true when this record comes from a decl attribute inside 328 - // a struct (as opposed to a field attribute). For @test(permute), this 329 - // distinction matters: a decl attr means "permute all fields within this 330 - // struct" whereas a field attr means "this field participates in 331 - // permutation within its parent struct." 332 - isDeclAttr bool 333 - } 334 - 335 - // extractTestAttrs walks ast.File and: 336 - // 1. Collects all @test(...) attributes from field attrs, struct decl attrs, 337 - // and file-level decl attrs. 338 - // 2. Removes them from the AST (in-place). 339 - // 3. Preserves all non-@test attributes. 340 - // 341 - // Returns the collected records. 342 - // File-level decl attributes produce records with an empty path and 343 - // fileLevel=true; these check the entire file's evaluated value. 344 - func extractTestAttrs(f *ast.File, fileName string) []attrRecord { 345 - var records []attrRecord 346 - 347 - // hidPkg is the package scope for hidden fields in this file. 348 - // Inline-compiled sources use ":" + pkgname; anonymous files use "_". 349 - hidPkg := ":" + f.PackageName() 350 - if hidPkg == ":" { 351 - hidPkg = "_" 352 - } 353 - 354 - // appendErrRecord records a @test attribute whose parseTestAttr call failed. 355 - // The runner reports all parseErr records as test failures before running 356 - // any assertions. 357 - appendErrRecord := func(attr *ast.Attribute, path cue.Path, isDeclAttr, fileLevel bool, err error) { 358 - records = append(records, attrRecord{ 359 - path: path, 360 - parsed: parsedTestAttr{srcAttr: attr, srcFileName: fileName}, 361 - parseErr: err, 362 - isDeclAttr: isDeclAttr, 363 - fileLevel: fileLevel, 364 - }) 365 - } 366 - 367 - // walkField records @test field attrs, then recurses into the field's 368 - // struct value (if any). Attributes are NOT stripped from the AST so that 369 - // the evaluated value is built from the original source; CUE ignores 370 - // attributes during evaluation, so positions reference original source lines. 371 - var walkField func(field *ast.Field, path cue.Path) 372 - 373 - // walkStruct records @test decl attrs and recurses into all sub-fields. 374 - var walkStruct func(sl *ast.StructLit, path cue.Path) 375 - 376 - // scanUnreachableTests scans expr for @test attributes that appear inside 377 - // struct literals which are binary-expression operands. Such attributes are 378 - // never visited by walkField/walkStruct and would be silently ignored; 379 - // they are reported here as parse errors so the test suite fails loudly. 380 - // 381 - // inOperand must be true when expr is already inside a binary-expression 382 - // operand (meaning all @test within it are unreachable regardless of depth). 383 - var scanUnreachableTests func(expr ast.Expr, path cue.Path, inOperand bool) 384 - 385 - walkField = func(field *ast.Field, path cue.Path) { 386 - fieldBaseLine := field.Pos().Line() 387 - 388 - for _, a := range field.Attrs { 389 - k, _ := a.Split() 390 - if k != "test" { 391 - continue 392 - } 393 - pa, err := parseTestAttr(a) 394 - if err != nil { 395 - appendErrRecord(a, path, false, false, err) 396 - continue 397 - } 398 - pa.baseLine = fieldBaseLine 399 - pa.srcFileName = fileName 400 - records = append(records, attrRecord{ 401 - path: path, 402 - parsed: pa, 403 - }) 404 - } 405 - 406 - if sl, ok := field.Value.(*ast.StructLit); ok { 407 - walkStruct(sl, path) 408 - } else { 409 - scanUnreachableTests(field.Value, path, false) 410 - } 411 - } 412 - 413 - walkStruct = func(sl *ast.StructLit, path cue.Path) { 414 - // Use the opening brace line as the base for decl-level @test pos= specs. 415 - // File-level @test attrs (no braces) use their own line as baseLine (see below). 416 - structBaseLine := sl.Lbrace.Line() 417 - 418 - for _, elt := range sl.Elts { 419 - switch e := elt.(type) { 420 - case *ast.Attribute: 421 - k, _ := e.Split() 422 - if k != "test" { 423 - continue 424 - } 425 - pa, err := parseTestAttr(e) 426 - if err != nil { 427 - appendErrRecord(e, path, true, false, err) 428 - continue 429 - } 430 - pa.baseLine = structBaseLine 431 - pa.srcFileName = fileName 432 - records = append(records, attrRecord{ 433 - path: path, 434 - parsed: pa, 435 - isDeclAttr: true, 436 - }) 437 - 438 - case *ast.Field: 439 - subPath := appendPath(path, e.Label, hidPkg) 440 - if subPath.Err() == nil { 441 - walkField(e, subPath) 442 - } else { 443 - // Non-static label (e.g. string interpolation): cannot 444 - // register a path from the AST. Still process 445 - // @test(shareID=name, at=sel) where at= is a non-integer 446 - // field name giving the resolved key, so the shareID check 447 - // can look it up in the evaluated value. 448 - for _, a := range e.Attrs { 449 - k, _ := a.Split() 450 - if k != "test" { 451 - continue 452 - } 453 - pa, err := parseTestAttr(a) 454 - if err != nil { 455 - appendErrRecord(a, path, false, false, err) 456 - continue 457 - } 458 - if pa.directive != "shareID" || len(pa.raw.Fields) == 0 { 459 - continue 460 - } 461 - // Require a non-integer at= giving the resolved field name. 462 - // applyShareIDAt (called in collectDirectShareIDs) will 463 - // append it to the parent path stored here. 464 - if !hasShareIDAtSel(pa) { 465 - continue // no usable at=sel for dynamic key 466 - } 467 - pa.baseLine = a.Pos().Line() 468 - pa.srcFileName = fileName 469 - // Store the parent path; applyShareIDAt in 470 - // collectDirectShareIDs appends the at= selector. 471 - records = append(records, attrRecord{ 472 - path: path, 473 - parsed: pa, 474 - }) 475 - } 476 - } 477 - 478 - case *ast.EmbedDecl: 479 - // Bare embeddings (e.g. `A & {f: v @test(...)}`) are not 480 - // walked by walkField/walkStruct, so any @test inside them 481 - // would be silently ignored. Scan and report them as errors. 482 - scanUnreachableTests(e.Expr, path, false) 483 - } 484 - } 485 - } 486 - 487 - scanUnreachableTests = func(expr ast.Expr, path cue.Path, inOperand bool) { 488 - switch e := expr.(type) { 489 - case *ast.BinaryExpr: 490 - scanUnreachableTests(e.X, path, true) 491 - scanUnreachableTests(e.Y, path, true) 492 - case *ast.ParenExpr: 493 - scanUnreachableTests(e.X, path, inOperand) 494 - case *ast.StructLit: 495 - if !inOperand { 496 - return // reachable; handled by walkField/walkStruct 497 - } 498 - for _, elt := range e.Elts { 499 - switch elt := elt.(type) { 500 - case *ast.Attribute: 501 - k, _ := elt.Split() 502 - if k == "test" { 503 - appendErrRecord(elt, path, true, false, fmt.Errorf( 504 - "@test inside a struct literal that is a binary-expression operand "+ 505 - "(e.g. X & {@test(...)}) is not reachable by the test runner; "+ 506 - "place @test after the field value (e.g. f: X & {...} @test(...)) "+ 507 - "or as a decl attribute in the enclosing struct")) 508 - } 509 - case *ast.Field: 510 - for _, a := range elt.Attrs { 511 - k, _ := a.Split() 512 - if k == "test" { 513 - appendErrRecord(a, path, false, false, fmt.Errorf( 514 - "@test on a field inside a struct literal that is a binary-expression operand "+ 515 - "(e.g. X & {f: v @test(...)}) is not reachable by the test runner; "+ 516 - "place @test after the enclosing field value (e.g. f: X & {...} @test(...))")) 517 - } 518 - } 519 - scanUnreachableTests(elt.Value, path, true) 520 - } 521 - } 522 - } 523 - } 524 - 525 - // Handle file-level decl attributes (@test as a top-level declaration). 526 - for _, decl := range f.Decls { 527 - if a, ok := decl.(*ast.Attribute); ok { 528 - k, _ := a.Split() 529 - if k == "test" { 530 - pa, err := parseTestAttr(a) 531 - if err != nil { 532 - appendErrRecord(a, cue.Path{}, false, true, err) 533 - continue 534 - } 535 - pa.baseLine = a.Pos().Line() 536 - pa.srcFileName = fileName 537 - records = append(records, attrRecord{ 538 - path: cue.Path{}, 539 - parsed: pa, 540 - fileLevel: true, 541 - }) 542 - } 543 - } 544 - } 545 - 546 - for _, decl := range f.Decls { 547 - switch d := decl.(type) { 548 - case *ast.Field: 549 - fieldPath := appendPath(cue.Path{}, d.Label, hidPkg) 550 - if fieldPath.Err() == nil { 551 - walkField(d, fieldPath) 552 - } 553 - case *ast.EmbedDecl: 554 - // Top-level bare embeddings are also not walked by walkField, so 555 - // scan them for unreachable @test attributes. 556 - scanUnreachableTests(d.Expr, cue.Path{}, false) 557 - } 558 - } 559 - 560 - return records 561 - } 562 - 563 - // identStr returns the string form of an AST label that is a simple identifier 564 - // or string literal. Returns empty string for complex labels. 565 - func identStr(label ast.Label) string { 566 - switch l := label.(type) { 567 - case *ast.Ident: 568 - return l.Name 569 - case *ast.BasicLit: 570 - // Strip quotes for string labels. 571 - s := l.Value 572 - if len(s) >= 2 && s[0] == '"' { 573 - s = s[1 : len(s)-1] 574 - } 575 - return s 576 - } 577 - return "" 578 - } 579 - 580 - // labelSelector returns the cue.Selector for an AST label, correctly handling 581 - // hidden fields (_foo) in two contexts: 582 - // 583 - // - Source-file context: pass hidPkg = ":" + f.PackageName() (or "_" for 584 - // anonymous packages). The package scope is applied to all hidden idents. 585 - // - Eq-body context: pass hidPkg = "". The caller signals that the name may 586 - // carry a $pkg suffix (e.g. "_foo$mypkg"), which is stripped and converted 587 - // to ":" + pkgname. Without a suffix, "_" is used (anonymous hidden field). 588 - func labelSelector(label ast.Label, hidPkg string) cue.Selector { 589 - if ident, ok := label.(*ast.Ident); ok && internal.IsHidden(ident.Name) { 590 - name := ident.Name 591 - pkg := hidPkg 592 - if pkg == "" { 593 - if i := strings.IndexByte(name, '$'); i >= 0 { 594 - pkg = ":" + name[i+1:] 595 - name = name[:i] 596 - } else { 597 - pkg = "_" 598 - } 599 - } 600 - return cue.Hid(name, pkg) 601 - } 602 - return cue.Label(label) 603 - } 604 - 605 - // appendPath appends a selector for label to path. 606 - // hidPkg is forwarded to labelSelector; see its documentation. 607 - func appendPath(base cue.Path, label ast.Label, hidPkg string) cue.Path { 608 - return base.Append(labelSelector(label, hidPkg)) 609 - } 610 - 611 - // parseAtPath parses an at= selector string into a cue.Path. 612 - // Unlike cue.ParsePath, it handles hidden field names with a $pkg qualifier 613 - // (e.g. "_foo$pkg" → cue.Hid("_foo", ":pkg"), matching the same syntax 614 - // accepted inside @test(eq, {...}) bodies). Dotted paths are split on "." and 615 - // each segment is processed independently, so "a._foo$pkg.b" works correctly. 616 - func parseAtPath(at string) (cue.Path, error) { 617 - // Fast path: if there are no hidden-field indicators, delegate directly. 618 - if !strings.Contains(at, "_") { 619 - p := cue.ParsePath(at) 620 - return p, p.Err() 621 - } 622 - var sels []cue.Selector 623 - for _, seg := range strings.Split(at, ".") { 624 - if internal.IsHidden(seg) { 625 - name := seg 626 - pkg := "_" 627 - if i := strings.IndexByte(name, '$'); i >= 0 { 628 - pkg = ":" + name[i+1:] 629 - name = name[:i] 630 - } 631 - sels = append(sels, cue.Hid(name, pkg)) 632 - } else { 633 - p := cue.ParsePath(seg) 634 - if err := p.Err(); err != nil { 635 - return cue.Path{}, err 636 - } 637 - sels = append(sels, p.Selectors()...) 638 - } 639 - } 640 - return cue.MakePath(sels...), nil 641 - } 642 - 643 - // applyShareIDAt applies the at= field from a @test(shareID=...) attribute 644 - // to base. For integer at=N it appends a list index; for a non-integer at=sel 645 - // it parses sel as a CUE path and appends its selectors. Returns base 646 - // unchanged if no at= is present or if the at= value cannot be parsed. 647 - func applyShareIDAt(base cue.Path, pa parsedTestAttr) cue.Path { 648 - for _, kv := range pa.raw.Fields[1:] { 649 - if kv.Key() != "at" { 650 - continue 651 - } 652 - val := kv.Value() 653 - if n, err := strconv.Atoi(val); err == nil { 654 - return base.Append(cue.Index(n)) 655 - } 656 - if p, err := parseAtPath(val); err == nil { 657 - return base.Append(p.Selectors()...) 658 - } 659 - break 660 - } 661 - return base 662 - } 663 - 664 - // hasShareIDAtSel reports whether pa contains at= with a non-integer value, 665 - // i.e. a field-name selector for use with dynamic-key fields. 666 - func hasShareIDAtSel(pa parsedTestAttr) bool { 667 - for _, kv := range pa.raw.Fields[1:] { 668 - if kv.Key() != "at" { 669 - continue 670 - } 671 - val := kv.Value() 672 - if val == "" { 673 - return false 674 - } 675 - _, err := strconv.Atoi(val) 676 - return err != nil 677 - } 678 - return false 679 - } 680 - 681 - // ───────────────────────────────────────────────────────────────────────────── 682 - // Section 3: Mode Detection 683 - // ───────────────────────────────────────────────────────────────────────────── 684 - 685 - // isInlineMode parses the CUE files in the archive (AST only) and returns true 686 - // if any @test(...) attribute is found anywhere in any CUE file — as a field 687 - // attribute, a decl attribute inside a struct at any nesting depth, or a 688 - // file-level decl attribute. No compilation is required. 689 - func isInlineMode(archive *txtar.Archive) bool { 690 - for _, f := range archive.Files { 691 - if !strings.HasSuffix(f.Name, ".cue") { 692 - continue 693 - } 694 - af, err := parser.ParseFile(f.Name, f.Data) 695 - if err != nil { 696 - continue 697 - } 698 - if declsHaveTestAttrs(af.Decls) { 699 - return true 700 - } 701 - } 702 - return false 703 - } 704 - 705 - // declsHaveTestAttrs recursively searches decls for any @test(...) attribute, 706 - // descending into struct-valued fields and comprehension bodies at any depth. 707 - func declsHaveTestAttrs(decls []ast.Decl) bool { 708 - for _, decl := range decls { 709 - switch d := decl.(type) { 710 - case *ast.Attribute: 711 - if k, _ := d.Split(); k == "test" { 712 - return true 713 - } 714 - case *ast.Field: 715 - for _, a := range d.Attrs { 716 - if k, _ := a.Split(); k == "test" { 717 - return true 718 - } 719 - } 720 - if sl, ok := d.Value.(*ast.StructLit); ok { 721 - if declsHaveTestAttrs(sl.Elts) { 722 - return true 723 - } 724 - } 725 - case *ast.Comprehension: 726 - if sl, ok := d.Value.(*ast.StructLit); ok { 727 - if declsHaveTestAttrs(sl.Elts) { 728 - return true 729 - } 730 - } 731 - } 732 - } 733 - return false 734 - } 735 - 736 - // ───────────────────────────────────────────────────────────────────────────── 737 137 // Section 4: Core Test Runner (inline mode) 738 138 // ───────────────────────────────────────────────────────────────────────────── 739 139 ··· 855 255 ctx := r.cueContext() 856 256 val, allRecords, compileErr := r.buildValue(ctx, nil) 857 257 if compileErr != nil { 858 - r.t.Fatalf("inline: CUE compile error: %v", compileErr) 258 + r.t.Fatalf("inline: CUE compile error:\n%s", cueerrors.Details(compileErr, nil)) 859 259 return 860 260 } 861 261 ··· 1447 847 } 1448 848 } 1449 849 1450 - // eqFillAttr builds an @test(eq, <value>[, at=<atStr>]) attribute for fill/force-update. 1451 - func (r *inlineRunner) eqFillAttr(v cue.Value, atStr string, pa parsedTestAttr) string { 1452 - if r.isError(v) { 1453 - return "@test(err)" 1454 - } 1455 - return r.eqFillAttrStr(r.formatValue(v), atStr, pa) 1456 - } 1457 - 1458 - // eqFillAttrStr builds @test(eq, <exprStr>[, at=<atStr>]). 1459 - // For multi-line expressions, if the compact single-line form is < 20 chars it 1460 - // is used directly; otherwise lines after the first are re-indented using the 1461 - // leading whitespace of the source line containing the @test attribute (the 1462 - // same offset trick as formatDebugAttr, but without an extra tab because 1463 - // format.Node already carries one tab of relative indentation). 1464 - func (r *inlineRunner) eqFillAttrStr(exprStr, atStr string, pa parsedTestAttr) string { 1465 - if strings.Contains(exprStr, "\n") { 1466 - if compact := compactCUEExpr(exprStr); len(compact) < 20 { 1467 - exprStr = compact 1468 - } else { 1469 - indent := r.attrLineIndent(pa) 1470 - exprStr = strings.ReplaceAll(exprStr, "\n", "\n"+indent) 1471 - } 1472 - } 1473 - if atStr != "" { 1474 - return fmt.Sprintf("@test(eq, %s, at=%s)", exprStr, atStr) 1475 - } 1476 - return fmt.Sprintf("@test(eq, %s)", exprStr) 1477 - } 1478 - 1479 - // compactCUEExpr collapses a multi-line CUE expression produced by format.Node 1480 - // into a single line. It handles struct and list literals by joining their 1481 - // tab-indented field lines with ", ". Only safe for shallow (non-nested) 1482 - // structures; deeply-nested values will exceed the 20-char threshold and use 1483 - // the indented form instead. 1484 - func compactCUEExpr(s string) string { 1485 - lines := strings.Split(s, "\n") 1486 - parts := make([]string, 0, len(lines)) 1487 - for _, line := range lines { 1488 - trimmed := strings.TrimLeft(line, "\t") 1489 - if trimmed != "" { 1490 - parts = append(parts, trimmed) 1491 - } 1492 - } 1493 - if len(parts) < 2 { 1494 - return strings.Join(parts, "") 1495 - } 1496 - open, close_ := parts[0], parts[len(parts)-1] 1497 - middle := parts[1 : len(parts)-1] 1498 - if (open == "{" || open == "[") && len(middle) > 0 { 1499 - return open + strings.Join(middle, ", ") + close_ 1500 - } 1501 - return strings.Join(parts, " ") 1502 - } 1503 - 1504 - // formatValue returns a human-readable CUE string for a value. 1505 - // Routes through the Vertex export path (via cue.Final()) to avoid internal 1506 - // _#def wrapping, then re-enables optional fields (value?: T) so the 1507 - // formatted expression round-trips through astCmp. 1508 - func (r *inlineRunner) formatValue(v cue.Value) string { 1509 - var b strings.Builder 1510 - eqWriteValue(value.OpContext(v), &b, v) 1511 - return b.String() 1512 - } 1513 - 1514 - // eqWriteValue writes a CUE value to b in @test(eq, ...) body notation. 1515 - // 1516 - // Compared with v.Syntax() + cue.Final() + format.Node: 1517 - // - Hidden fields use _foo$pkg notation (matching astcmp.go conventions). 1518 - // - adt.Disjunction values are emitted as *d1 | d2 (with * for defaults) 1519 - // instead of being collapsed to the default by cue.Final(). 1520 - // - adt.Conjunction values are emitted as c1 & c2. 1521 - func eqWriteValue(opCtx *adt.OpContext, b *strings.Builder, v cue.Value) { 1522 - tv := v.Core() 1523 - vx := tv.V.DerefValue() 1524 - 1525 - switch bv := vx.BaseValue.(type) { 1526 - case *adt.Disjunction: 1527 - eqWriteDisjunction(opCtx, b, bv) 1528 - return 1529 - case *adt.Conjunction: 1530 - eqWriteConjunction(opCtx, b, bv) 1531 - return 1532 - } 1533 - 1534 - // Use struct emission if the kind is struct OR if there are arcs — the 1535 - // latter handles error vertices that still carry child fields (e.g. a 1536 - // struct with one bad field: the parent vertex is _|_ but its arcs hold 1537 - // the successfully-evaluated sibling fields). 1538 - if v.IncompleteKind() == cue.StructKind || len(vx.Arcs) > 0 { 1539 - eqWriteStruct(opCtx, b, vx) 1540 - return 1541 - } 1542 - 1543 - // Scalar values and lists: fall back to the standard syntax formatter. 1544 - // cue.Final() resolves defaults and avoids _#def wrapping. 1545 - syn := v.Syntax(cue.Docs(false), cue.Final(), cue.Optional(true)) 1546 - stripComments(syn) 1547 - bs, err := format.Node(syn, format.Simplify()) 1548 - if err != nil { 1549 - fmt.Fprintf(b, "%#v", v) 1550 - } else { 1551 - b.Write(bs) 1552 - } 1553 - } 1554 - 1555 - // eqWriteDisjunction emits disjuncts as *d1 | d2 | d3 (defaults first with *). 1556 - func eqWriteDisjunction(opCtx *adt.OpContext, b *strings.Builder, dj *adt.Disjunction) { 1557 - for i, v := range dj.Values { 1558 - if i > 0 { 1559 - b.WriteString(" | ") 1560 - } 1561 - if i < dj.NumDefaults { 1562 - b.WriteByte('*') 1563 - } 1564 - eqWriteValue(opCtx, b, value.Make(opCtx, v)) 1565 - } 1566 - } 1567 - 1568 - // eqWriteConjunction emits conjuncts as c1 & c2 & c3. 1569 - func eqWriteConjunction(opCtx *adt.OpContext, b *strings.Builder, conj *adt.Conjunction) { 1570 - for i, v := range conj.Values { 1571 - if i > 0 { 1572 - b.WriteString(" & ") 1573 - } 1574 - eqWriteValue(opCtx, b, value.Make(opCtx, v)) 1575 - } 1576 - } 1577 - 1578 - // eqWriteStruct emits a struct, using _foo$pkg notation for hidden-field labels. 1579 - func eqWriteStruct(opCtx *adt.OpContext, b *strings.Builder, vx *adt.Vertex) { 1580 - b.WriteByte('{') 1581 - first := true 1582 - for _, arc := range vx.Arcs { 1583 - if arc.ArcType == adt.ArcNotPresent || arc.Label.IsLet() { 1584 - continue 1585 - } 1586 - // Skip hidden fields from external packages: their PkgID is a full 1587 - // module path (e.g. "mod.test/pkg") which cannot be encoded as a 1588 - // valid CUE identifier in the $pkg suffix notation. 1589 - if arc.Label.IsHidden() { 1590 - pkg := arc.Label.PkgID(opCtx) 1591 - if pkg != "_" && !strings.HasPrefix(pkg, ":") { 1592 - continue 1593 - } 1594 - } 1595 - if !first { 1596 - b.WriteString(", ") 1597 - } 1598 - first = false 1599 - eqWriteLabel(opCtx, b, arc.Label, arc.ArcType) 1600 - b.WriteString(": ") 1601 - eqWriteValue(opCtx, b, value.Make(opCtx, arc)) 1602 - } 1603 - b.WriteByte('}') 1604 - } 1605 - 1606 - // eqWriteLabel writes a field label. 1607 - // For hidden labels the $pkg qualifier is included when the field is 1608 - // package-scoped (pkg != "_"). Callers must ensure the label's PkgID is 1609 - // either "_" or colon-prefixed (inline package) before calling — see 1610 - // the skip guard in eqWriteStruct. 1611 - func eqWriteLabel(opCtx *adt.OpContext, b *strings.Builder, f adt.Feature, arcType adt.ArcType) { 1612 - if f.IsHidden() { 1613 - name := f.IdentString(opCtx) 1614 - pkg := f.PkgID(opCtx) 1615 - if pkg != "_" { 1616 - // PkgID returns ":pkgname" for inline sources; convert to "$pkgname". 1617 - b.WriteString(name + "$" + strings.TrimPrefix(pkg, ":")) 1618 - } else { 1619 - b.WriteString(name) 1620 - } 1621 - } else { 1622 - b.WriteString(f.SelectorString(opCtx)) 1623 - } 1624 - b.WriteString(arcType.Suffix()) 1625 - } 1626 - 1627 - // stripComments removes all comment groups from every node in the AST. 1628 - // Error nodes produced by v.Syntax() carry line comments like 1629 - // "// path: error message"; if left in, the // sequence inside an 1630 - // @test(eq, ...) attribute body would be parsed as a CUE comment and 1631 - // consume the closing ), corrupting the attribute syntax. 1632 - func stripComments(node ast.Node) { 1633 - ast.Walk(node, func(n ast.Node) bool { 1634 - ast.SetComments(n, nil) 1635 - return true 1636 - }, nil) 1637 - } 1638 - 1639 - // eqBodySupportedDirectives lists the @test directive names that are 1640 - // intentionally processed by astCmp when they appear inside an @test(eq, ...) 1641 - // body (as field-level attributes or struct-level decl attributes). 1642 - // Any other directive has no effect there. 1643 - var eqBodySupportedDirectives = map[string]bool{ 1644 - "final": true, // field-level and struct-level: resolve default before comparing 1645 - "ignore": true, // field-level: skip eq descent; field need not exist 1646 - "err": true, // field-level: check that value is an error 1647 - "shareID": true, // field-level: sharing assertion (handled by extractShareIDsFromEqExpr) 1648 - "checkOrder": true, // struct-level decl: require fields in declaration order 1649 - } 1650 - 1651 - // reportEqBodyTestAttrs walks the expected expression of an @test(eq, ...) 1652 - // body and reports any @test field attributes that have no effect there. 1653 - // Directives listed in eqBodySupportedDirectives are intentionally processed 1654 - // by astCmp and are excluded from the error. 1655 - func reportEqBodyTestAttrs(t testing.TB, path cue.Path, expr ast.Node) { 1656 - t.Helper() 1657 - ast.Walk(expr, func(n ast.Node) bool { 1658 - f, ok := n.(*ast.Field) 1659 - if !ok { 1660 - return true 1661 - } 1662 - for _, a := range f.Attrs { 1663 - k, _ := a.Split() 1664 - if k != "test" { 1665 - continue 1666 - } 1667 - pa, err := parseTestAttr(a) 1668 - if err != nil { 1669 - continue 1670 - } 1671 - if eqBodySupportedDirectives[pa.directive] { 1672 - continue 1673 - } 1674 - t.Errorf("path %s: @test(%s) in @test(eq, ...) body has no effect; place it as a field attribute on the actual value", path, pa.directive) 1675 - } 1676 - return true 1677 - }, nil) 1678 - } 1679 - 1680 850 // runLeqInline checks that val is subsumed by the constraint in pa. 1681 851 func (r *inlineRunner) runLeqInline(t testing.TB, path cue.Path, val cue.Value, pa parsedTestAttr) { 1682 852 t.Helper() ··· 1826 996 } 1827 997 } 1828 998 1829 - // runDebugCheckInline checks the debug printer output of val against the 1830 - // expected string in the @test(debugCheck, "...") attribute. 1831 - // When CUE_UPDATE modes are active, enqueues a write-back. 1832 - func (r *inlineRunner) runDebugCheckInline(t testing.TB, path cue.Path, val cue.Value, pa parsedTestAttr) { 1833 - t.Helper() 1834 - name := pa.raw.Fields[0].Value() // preserves any :vN version suffix 1835 - if len(pa.raw.Fields) < 2 { 1836 - // Empty @test(debugCheck) — fill placeholder. 1837 - if cuetest.UpdateGoldenFiles { 1838 - actual := r.debugPrinterOutput(val) 1839 - r.enqueueInlineFill(pa, r.formatDebugAttr(name, actual, pa)) 1840 - } 1841 - return 1842 - } 1843 - expected := pa.raw.Fields[1].Value() 1844 - actual := r.debugPrinterOutput(val) 1845 - match := normalizeLines(actual) == normalizeLines(expected) 1846 - if match && !cuetest.ForceUpdateGoldenFiles { 1847 - return 1848 - } 1849 - if cuetest.ForceUpdateGoldenFiles || cuetest.UpdateGoldenFiles { 1850 - r.enqueueInlineFill(pa, r.formatDebugAttr(name, actual, pa)) 1851 - return 1852 - } 1853 - if !match { 1854 - t.Errorf("path %s: @test(debugCheck) mismatch:\ngot: %q\nwant: %q", path, actual, expected) 1855 - logHint(t, pa.hint) 1856 - } 1857 - } 1858 - 1859 - // runDebugOutputInline captures the debug printer output of val as an 1860 - // informational annotation (@test(debug, ...)). Unlike debugCheck, a 1861 - // mismatch does not fail the test — it only logs and auto-updates when 1862 - // CUE_UPDATE is active. 1863 - func (r *inlineRunner) runDebugOutputInline(t testing.TB, path cue.Path, val cue.Value, pa parsedTestAttr) { 1864 - t.Helper() 1865 - name := pa.raw.Fields[0].Value() // preserves any :vN version suffix 1866 - actual := r.debugPrinterOutput(val) 1867 - if len(pa.raw.Fields) < 2 { 1868 - // Empty @test(debug) — fill placeholder. 1869 - if cuetest.UpdateGoldenFiles { 1870 - r.enqueueInlineFill(pa, r.formatDebugAttr(name, actual, pa)) 1871 - } 1872 - return 1873 - } 1874 - expected := pa.raw.Fields[1].Value() 1875 - match := normalizeLines(actual) == normalizeLines(expected) 1876 - if match && !cuetest.ForceUpdateGoldenFiles { 1877 - return 1878 - } 1879 - // Always auto-update on mismatch (informational, not an assertion). 1880 - if cuetest.ForceUpdateGoldenFiles || cuetest.UpdateGoldenFiles { 1881 - r.enqueueInlineFill(pa, r.formatDebugAttr(name, actual, pa)) 1882 - return 1883 - } 1884 - if !match { 1885 - t.Logf("path %s: @test(debug) changed:\ngot: %q\nwant: %q", path, actual, expected) 1886 - } 1887 - } 1888 - 1889 - // formatDebugAttr returns the @test(name, ...) attribute text for a debug value. 1890 - func (r *inlineRunner) formatDebugAttr(name, actual string, pa parsedTestAttr) string { 1891 - actual = strings.TrimRight(actual, "\n") 1892 - n := strings.Count(r.attrLineIndent(pa), "\t") 1893 - actual = literal.String.WithOptionalTabIndent(n + 1).Quote(actual) 1894 - return fmt.Sprintf("@test(%s, %s)", name, actual) 1895 - } 1896 - 1897 - // attrLineIndent returns the leading whitespace on the source line that 1898 - // contains pa's @test attribute. Used to compute the indentation level 1899 - // for multi-line debug attribute values. 1900 - func (r *inlineRunner) attrLineIndent(pa parsedTestAttr) string { 1901 - offset := pa.srcAttr.Pos().Offset() 1902 - for _, f := range r.archive.Files { 1903 - if f.Name != pa.srcFileName { 1904 - continue 1905 - } 1906 - data := f.Data 1907 - start := offset 1908 - for start > 0 && data[start-1] != '\n' { 1909 - start-- 1910 - } 1911 - end := start 1912 - for end < offset && data[end] == '\t' { 1913 - end++ 1914 - } 1915 - return string(data[start:end]) 1916 - } 1917 - return "" 1918 - } 1919 - 1920 - // debugPrinterOutput returns the standard debug-printer representation of val, 1921 - // equivalent to what appears in out/eval golden sections. 1922 - // Absolute file paths from module-aware loading are normalized to relative. 1923 - func (r *inlineRunner) debugPrinterOutput(val cue.Value) string { 1924 - c := val.Core() 1925 - if c.V == nil { 1926 - return "" 1927 - } 1928 - out := debug.NodeString(c.R, c.V, nil) 1929 - if r.dir != "" { 1930 - out = strings.ReplaceAll(out, filepath.ToSlash(r.dir)+"/", "") 1931 - } 1932 - return out 1933 - } 1934 - 1935 - // normalizeLines trims trailing whitespace from each line and strips any 1936 - // trailing blank lines, for use in debug: textual comparison. 1937 - func normalizeLines(s string) string { 1938 - lines := strings.Split(s, "\n") 1939 - for i, line := range lines { 1940 - lines[i] = strings.TrimRight(line, " \t") 1941 - } 1942 - for len(lines) > 0 && lines[len(lines)-1] == "" { 1943 - lines = lines[:len(lines)-1] 1944 - } 1945 - return strings.Join(lines, "\n") 1946 - } 1947 - 1948 999 // attrHasSkip reports whether the raw attribute body contains a skip:<ver> arg 1949 1000 // at position 2 or later. Returns the version string (e.g. "v3") and true 1950 1001 // when a skip arg is found; returns "", false otherwise. ··· 1971 1022 newAttrText: newAttrText, 1972 1023 }) 1973 1024 } 1974 - 1975 - // ───────────────────────────────────────────────────────────────────────────── 1976 - // Section 8: shareID — vertex sharing assertions 1977 - // ───────────────────────────────────────────────────────────────────────────── 1978 - 1979 - // extractShareIDsFromEqExpr walks the struct literal of an @test(eq, STRUCT) 1980 - // body and collects all @test(shareID=name) annotations on fields. 1981 - // basePath is the CUE path of the @test(eq) attribute; field paths in the 1982 - // struct are appended to it. version is the active evaluator version name 1983 - // used for version-specific share groups (@test(shareID=name)). 1984 - // Returns a map from shareID name to the absolute paths of fields in that group. 1985 - func extractShareIDsFromEqExpr(expr ast.Expr, basePath cue.Path, version string) map[string][]cue.Path { 1986 - s, ok := expr.(*ast.StructLit) 1987 - if !ok { 1988 - return nil 1989 - } 1990 - var result map[string][]cue.Path 1991 - for _, d := range s.Elts { 1992 - f, ok := d.(*ast.Field) 1993 - if !ok { 1994 - continue 1995 - } 1996 - for _, a := range f.Attrs { 1997 - if k, _ := a.Split(); k != "test" { 1998 - continue 1999 - } 2000 - pa, err := parseTestAttr(a) 2001 - if err != nil || pa.directive != "shareID" { 2002 - continue 2003 - } 2004 - // Version filter: skip if a non-matching version is specified. 2005 - if pa.version != "" && pa.version != version { 2006 - continue 2007 - } 2008 - if len(pa.raw.Fields) == 0 { 2009 - continue 2010 - } 2011 - shareIDName := pa.raw.Fields[0].Value() 2012 - if shareIDName == "" { 2013 - continue 2014 - } 2015 - fieldPath := applyShareIDAt(basePath.Append(labelSelector(f.Label, "")), pa) 2016 - if result == nil { 2017 - result = make(map[string][]cue.Path) 2018 - } 2019 - result[shareIDName] = append(result[shareIDName], fieldPath) 2020 - } 2021 - } 2022 - return result 2023 - } 2024 - 2025 - // collectShareIDsForRoot builds a map of shareID name → CUE paths by scanning 2026 - // all attrRecords within rootPath in two ways: 2027 - // 2028 - // 1. Direct @test(shareID=name) field attributes in the source — each record 2029 - // with directive "shareID" contributes its rec.path to the named group. 2030 - // 2031 - // 2. @test(shareID=name) annotations on fields inside @test(eq, STRUCT) bodies 2032 - // — the struct is parsed and fields carrying shareID annotations are mapped 2033 - // to their absolute paths (basePath + fieldLabel). 2034 - func (r *inlineRunner) collectShareIDsForRoot(records []attrRecord, rootPath cue.Path, version string) map[string][]cue.Path { 2035 - var shareGroups map[string][]cue.Path 2036 - add := func(id string, p cue.Path) { 2037 - if shareGroups == nil { 2038 - shareGroups = make(map[string][]cue.Path) 2039 - } 2040 - shareGroups[id] = append(shareGroups[id], p) 2041 - } 2042 - 2043 - // Track processed eq attrs by (fileName, offset) to avoid double-counting. 2044 - type attrKey struct { 2045 - file string 2046 - offset int 2047 - } 2048 - seenEq := make(map[attrKey]bool) 2049 - 2050 - for _, rec := range records { 2051 - if !pathHasPrefix(rec.path, rootPath) { 2052 - continue 2053 - } 2054 - pa := rec.parsed 2055 - // Version filter: skip directives targeting a different version. 2056 - if pa.version != "" && pa.version != version { 2057 - continue 2058 - } 2059 - 2060 - switch pa.directive { 2061 - case "shareID": 2062 - // Direct field attribute: @test(shareID=name) on a source field. 2063 - // Optional at=N sub-option selects list element N within the field. 2064 - if len(pa.raw.Fields) == 0 { 2065 - continue 2066 - } 2067 - shareIDName := pa.raw.Fields[0].Value() 2068 - if shareIDName == "" { 2069 - continue 2070 - } 2071 - add(shareIDName, applyShareIDAt(rec.path, pa)) 2072 - 2073 - case "eq": 2074 - // Eq body: extract @test(shareID=name) from fields in the struct literal. 2075 - if len(pa.raw.Fields) < 2 { 2076 - continue 2077 - } 2078 - key := attrKey{file: pa.srcFileName, offset: pa.srcAttr.Pos().Offset()} 2079 - if seenEq[key] { 2080 - continue 2081 - } 2082 - seenEq[key] = true 2083 - eqExpr, err := parser.ParseExpr("shareID", pa.raw.Fields[1].Text()) 2084 - if err != nil { 2085 - continue 2086 - } 2087 - for id, paths := range extractShareIDsFromEqExpr(eqExpr, rec.path, version) { 2088 - for _, p := range paths { 2089 - add(id, p) 2090 - } 2091 - } 2092 - } 2093 - } 2094 - return shareGroups 2095 - } 2096 - 2097 - // collectDirectShareIDs builds a shareID group map from direct @test(shareID=name) 2098 - // field attributes across ALL records at any nesting depth (no root filtering). 2099 - // This is used for cross-root sharing assertions where fields from different 2100 - // roots share a vertex. Eq-body sharing is handled per-root by 2101 - // collectShareIDsForRoot. 2102 - func (r *inlineRunner) collectDirectShareIDs(records []attrRecord, version string) map[string][]cue.Path { 2103 - var shareGroups map[string][]cue.Path 2104 - for _, rec := range records { 2105 - if rec.fileLevel { 2106 - continue 2107 - } 2108 - 2109 - pa := rec.parsed 2110 - if pa.version != "" && pa.version != version { 2111 - continue 2112 - } 2113 - if pa.directive != "shareID" { 2114 - continue 2115 - } 2116 - if len(pa.raw.Fields) == 0 { 2117 - continue 2118 - } 2119 - shareIDName := pa.raw.Fields[0].Value() 2120 - if shareIDName == "" { 2121 - continue 2122 - } 2123 - if shareGroups == nil { 2124 - shareGroups = make(map[string][]cue.Path) 2125 - } 2126 - shareGroups[shareIDName] = append(shareGroups[shareIDName], applyShareIDAt(rec.path, pa)) 2127 - } 2128 - return shareGroups 2129 - } 2130 - 2131 - // runShareIDChecks verifies that all paths in each shareID group dereference to 2132 - // the same canonical *adt.Vertex, confirming that the CUE evaluator shares the 2133 - // vertex rather than copying it. 2134 - func (r *inlineRunner) runShareIDChecks(t testing.TB, fileVal cue.Value, shareGroups map[string][]cue.Path) { 2135 - t.Helper() 2136 - for id, paths := range shareGroups { 2137 - if len(paths) < 2 { 2138 - continue // need at least two to assert sharing 2139 - } 2140 - firstVal := fileVal.LookupPath(paths[0]) 2141 - firstCore := firstVal.Core() 2142 - if firstCore.V == nil { 2143 - t.Errorf("@test(shareID=%s): path %s: not found in evaluated value", id, paths[0]) 2144 - continue 2145 - } 2146 - derefFirst := firstCore.V.DerefValue() 2147 - for _, p := range paths[1:] { 2148 - otherVal := fileVal.LookupPath(p) 2149 - otherCore := otherVal.Core() 2150 - if otherCore.V == nil { 2151 - t.Errorf("@test(shareID=%s): path %s: not found in evaluated value", id, p) 2152 - continue 2153 - } 2154 - derefOther := otherCore.V.DerefValue() 2155 - if derefFirst != derefOther { 2156 - t.Errorf("@test(shareID=%s): %s and %s are not shared (different vertices)", 2157 - id, paths[0], p) 2158 - } 2159 - } 2160 - } 2161 - }
+627
internal/cuetxtar/inline_attr.go
··· 1 + // Copyright 2026 CUE Authors 2 + // 3 + // Licensed under the Apache License, Version 2.0 (the "License"); 4 + // you may not use this file except in compliance with the License. 5 + // You may obtain a copy of the License at 6 + // 7 + // http://www.apache.org/licenses/LICENSE-2.0 8 + // 9 + // Unless required by applicable law or agreed to in writing, software 10 + // distributed under the License is distributed on an "AS IS" BASIS, 11 + // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 + // See the License for the specific language governing permissions and 13 + // limitations under the License. 14 + 15 + package cuetxtar 16 + 17 + // This file implements @test attribute parsing, AST extraction, and inline-mode 18 + // detection. These components are self-contained and medium-risk: they parse 19 + // CUE source and attribute syntax but do not interact with the evaluator. 20 + 21 + import ( 22 + "fmt" 23 + "strconv" 24 + "strings" 25 + "testing" 26 + 27 + "golang.org/x/tools/txtar" 28 + 29 + "cuelang.org/go/cue" 30 + "cuelang.org/go/cue/ast" 31 + "cuelang.org/go/cue/parser" 32 + "cuelang.org/go/internal" 33 + ) 34 + 35 + // ───────────────────────────────────────────────────────────────────────────── 36 + // Section 1: Attribute Parsing Utilities 37 + // ───────────────────────────────────────────────────────────────────────────── 38 + 39 + // parsedTestAttr holds the result of parsing a single @test(...) attribute. 40 + type parsedTestAttr struct { 41 + // directive is the primary directive name, e.g. "eq", "err", "kind", 42 + // "closed", "leq", "skip", "permute", "file", "desc". 43 + directive string 44 + 45 + // version is the optional version suffix from directive:vN, e.g. "v3". 46 + // Empty means unversioned. 47 + version string 48 + 49 + // raw is the parsed internal.Attr for accessing remaining arguments. 50 + raw *internal.Attr 51 + 52 + // For "err" directives, parsed sub-options are stored here. 53 + errArgs *errArgs 54 + 55 + // isTodo marks directives produced by the "todo" version qualifier 56 + // (e.g. @test(eq:todo, X)). These run as expected-to-fail: failures 57 + // are logged rather than reported as errors; a pass emits a warning. 58 + isTodo bool 59 + 60 + // todoPriority is the p= value from a :todo directive, e.g. "1" for p=1. 61 + // 0 is the highest priority; empty means no priority specified. 62 + todoPriority string 63 + 64 + // isIncorrect marks a directive carrying the "incorrect" positional flag 65 + // (e.g. @test(eq, 3, incorrect) or @test(err, code=eval, incorrect)). 66 + // Applicable to any assertion directive. The assertion documents current 67 + // known-incorrect behavior: a pass is suppressed (logs a NOTE), but a 68 + // failure still propagates as a real test failure so that changes to the 69 + // incorrect value are always detected. 70 + isIncorrect bool 71 + 72 + // hint is an optional message printed when the assertion fails 73 + // (from hint="..."). Intended as guidance for automated tools 74 + // such as AI assistants reviewing test failures. 75 + hint string 76 + 77 + // srcAttr is the original AST attribute node (needed for CUE_UPDATE write-back). 78 + srcAttr *ast.Attribute 79 + 80 + // srcFileName is the archive .cue file name containing this attribute, 81 + // e.g. "in.cue" (needed to locate the file for CUE_UPDATE write-back). 82 + srcFileName string 83 + 84 + // baseLine is the effective 1-indexed line of the field carrying this 85 + // attribute in the stripped-and-formatted output. It may differ from 86 + // srcAttr.Pos().Line() when earlier @test attributes on preceding fields 87 + // contain embedded newlines in their bodies (which are stripped before 88 + // formatting, collapsing those extra lines). 89 + baseLine int 90 + } 91 + 92 + // parseTestAttr parses the body of a @test(...) attribute node. 93 + // It returns a parsedTestAttr for each logical directive in the attribute. 94 + // A single @test(...) contains exactly one directive (the first positional 95 + // argument or the key of the first key=value pair). 96 + func parseTestAttr(astAttr *ast.Attribute) (parsedTestAttr, error) { 97 + if name := astAttr.Name(); name != "test" { 98 + return parsedTestAttr{}, fmt.Errorf("not a @test attribute: @%s", name) 99 + } 100 + 101 + attr := internal.ParseAttr(astAttr) 102 + if attr.Err != nil { 103 + return parsedTestAttr{}, attr.Err 104 + } 105 + 106 + result := parsedTestAttr{ 107 + raw: attr, 108 + srcAttr: astAttr, 109 + } 110 + 111 + if len(attr.Fields) == 0 || (len(attr.Fields) == 1 && attr.Fields[0] == internal.KeyValue{}) { 112 + // @test() — empty placeholder or bare marker. 113 + result.directive = "" 114 + return result, nil 115 + } 116 + 117 + // The first field determines the directive. 118 + // Case 1: key=value form like desc="hello", shareID=name — directive is the key. 119 + // Case 2: positional form like eq, err, kind — directive (with optional :vN suffix) is the value. 120 + f0 := attr.Fields[0] 121 + if f0.Key() != "" { 122 + dir := f0.Key() 123 + // Key-based directives may carry a version suffix: "shareID" → directive="shareID", version="v3". 124 + if idx := strings.LastIndex(dir, ":"); idx >= 0 { 125 + result.directive = dir[:idx] 126 + result.version = dir[idx+1:] 127 + } else { 128 + result.directive = dir 129 + } 130 + } else { 131 + // May have version suffix: "eq:v3" → directive="eq", version="v3". 132 + dir := f0.Value() 133 + if idx := strings.LastIndex(dir, ":"); idx >= 0 { 134 + result.directive = dir[:idx] 135 + result.version = dir[idx+1:] 136 + } else { 137 + result.directive = dir 138 + } 139 + } 140 + 141 + // Parse directive-specific sub-options. 142 + switch result.directive { 143 + case "err": 144 + ea, err := parseErrArgs(attr) 145 + if err != nil { 146 + return result, err 147 + } 148 + result.errArgs = &ea 149 + } 150 + 151 + // Extract universal flags and reject unknown key= flags. 152 + // Positional args (kv.Key() == "") are accepted by directives as needed. 153 + // Directives with their own flag parsers (err, todo, skip, shareID) are 154 + // responsible for validating their own flags. 155 + for _, kv := range attr.Fields[1:] { 156 + switch kv.Key() { 157 + case "hint": 158 + result.hint = kv.Value() 159 + case "p": 160 + // p= is a universal priority flag (e.g. p=1 on err:todo). 161 + result.todoPriority = kv.Value() 162 + case "": 163 + // Positional arg — check for universal flags. 164 + if kv.Value() == "incorrect" { 165 + result.isIncorrect = true 166 + } 167 + case "at": 168 + // at= is accepted by eq, err, and shareID; each validates it 169 + // in its own handler. 170 + switch result.directive { 171 + case "eq", "err", "shareID": 172 + // Validated in their respective handlers. 173 + default: 174 + return result, fmt.Errorf("@test(%s): unknown flag %q", result.directive, kv.Key()) 175 + } 176 + default: 177 + switch result.directive { 178 + case "err", "todo", "skip", "shareID": 179 + // These directives parse their own flags elsewhere. 180 + default: 181 + return result, fmt.Errorf("@test(%s): unknown flag %q", result.directive, kv.Key()) 182 + } 183 + } 184 + } 185 + 186 + return result, nil 187 + } 188 + 189 + // logHint logs hint as an additional note following a test failure. 190 + // Call immediately after t.Errorf when pa.hint is set. 191 + func logHint(t testing.TB, hint string) { 192 + if hint != "" { 193 + t.Helper() 194 + t.Log("hint:", hint) 195 + } 196 + } 197 + 198 + // ───────────────────────────────────────────────────────────────────────────── 199 + // Section 2: AST Extraction and Stripping 200 + // ───────────────────────────────────────────────────────────────────────────── 201 + 202 + // attrRecord associates a parsed @test attribute with its location in the 203 + // evaluated CUE value. 204 + type attrRecord struct { 205 + // path is the full CUE path to the field carrying this attribute. 206 + path cue.Path 207 + 208 + // parsed is the parsed directive from this attribute. 209 + parsed parsedTestAttr 210 + 211 + // parseErr is non-nil when parseTestAttr failed. The runner reports the 212 + // error as a test failure and skips running the directive. 213 + parseErr error 214 + 215 + // fileLevel is true when this record comes from a file-level (top-level) 216 + // decl attribute rather than a field attribute or struct-level decl attribute. 217 + // A file-level @test(eq, VALUE) checks the entire file's evaluated value. 218 + fileLevel bool 219 + 220 + // isDeclAttr is true when this record comes from a decl attribute inside 221 + // a struct (as opposed to a field attribute). For @test(permute), this 222 + // distinction matters: a decl attr means "permute all fields within this 223 + // struct" whereas a field attr means "this field participates in 224 + // permutation within its parent struct." 225 + isDeclAttr bool 226 + } 227 + 228 + // extractTestAttrs walks ast.File and: 229 + // 1. Collects all @test(...) attributes from field attrs, struct decl attrs, 230 + // and file-level decl attrs. 231 + // 2. Removes them from the AST (in-place). 232 + // 3. Preserves all non-@test attributes. 233 + // 234 + // Returns the collected records. 235 + // File-level decl attributes produce records with an empty path and 236 + // fileLevel=true; these check the entire file's evaluated value. 237 + func extractTestAttrs(f *ast.File, fileName string) []attrRecord { 238 + var records []attrRecord 239 + 240 + // hidPkg is the package scope for hidden fields in this file. 241 + // Inline-compiled sources use ":" + pkgname; anonymous files use "_". 242 + hidPkg := ":" + f.PackageName() 243 + if hidPkg == ":" { 244 + hidPkg = "_" 245 + } 246 + 247 + // appendErrRecord records a @test attribute whose parseTestAttr call failed. 248 + // The runner reports all parseErr records as test failures before running 249 + // any assertions. 250 + appendErrRecord := func(attr *ast.Attribute, path cue.Path, isDeclAttr, fileLevel bool, err error) { 251 + records = append(records, attrRecord{ 252 + path: path, 253 + parsed: parsedTestAttr{srcAttr: attr, srcFileName: fileName}, 254 + parseErr: err, 255 + isDeclAttr: isDeclAttr, 256 + fileLevel: fileLevel, 257 + }) 258 + } 259 + 260 + // walkField records @test field attrs, then recurses into the field's 261 + // struct value (if any). Attributes are NOT stripped from the AST so that 262 + // the evaluated value is built from the original source; CUE ignores 263 + // attributes during evaluation, so positions reference original source lines. 264 + var walkField func(field *ast.Field, path cue.Path) 265 + 266 + // walkStruct records @test decl attrs and recurses into all sub-fields. 267 + var walkStruct func(sl *ast.StructLit, path cue.Path) 268 + 269 + // scanUnreachableTests scans expr for @test attributes that appear inside 270 + // struct literals which are binary-expression operands. Such attributes are 271 + // never visited by walkField/walkStruct and would be silently ignored; 272 + // they are reported here as parse errors so the test suite fails loudly. 273 + // 274 + // inOperand must be true when expr is already inside a binary-expression 275 + // operand (meaning all @test within it are unreachable regardless of depth). 276 + var scanUnreachableTests func(expr ast.Expr, path cue.Path, inOperand bool) 277 + 278 + walkField = func(field *ast.Field, path cue.Path) { 279 + fieldBaseLine := field.Pos().Line() 280 + 281 + for _, a := range field.Attrs { 282 + k, _ := a.Split() 283 + if k != "test" { 284 + continue 285 + } 286 + pa, err := parseTestAttr(a) 287 + if err != nil { 288 + appendErrRecord(a, path, false, false, err) 289 + continue 290 + } 291 + pa.baseLine = fieldBaseLine 292 + pa.srcFileName = fileName 293 + records = append(records, attrRecord{ 294 + path: path, 295 + parsed: pa, 296 + }) 297 + } 298 + 299 + if sl, ok := field.Value.(*ast.StructLit); ok { 300 + walkStruct(sl, path) 301 + } else { 302 + scanUnreachableTests(field.Value, path, false) 303 + } 304 + } 305 + 306 + walkStruct = func(sl *ast.StructLit, path cue.Path) { 307 + // Use the opening brace line as the base for decl-level @test pos= specs. 308 + // File-level @test attrs (no braces) use their own line as baseLine (see below). 309 + structBaseLine := sl.Lbrace.Line() 310 + 311 + for _, elt := range sl.Elts { 312 + switch e := elt.(type) { 313 + case *ast.Attribute: 314 + k, _ := e.Split() 315 + if k != "test" { 316 + continue 317 + } 318 + pa, err := parseTestAttr(e) 319 + if err != nil { 320 + appendErrRecord(e, path, true, false, err) 321 + continue 322 + } 323 + pa.baseLine = structBaseLine 324 + pa.srcFileName = fileName 325 + records = append(records, attrRecord{ 326 + path: path, 327 + parsed: pa, 328 + isDeclAttr: true, 329 + }) 330 + 331 + case *ast.Field: 332 + subPath := appendPath(path, e.Label, hidPkg) 333 + if subPath.Err() == nil { 334 + walkField(e, subPath) 335 + } else { 336 + // Non-static label (e.g. string interpolation): cannot 337 + // register a path from the AST. Still process 338 + // @test(shareID=name, at=sel) where at= is a non-integer 339 + // field name giving the resolved key, so the shareID check 340 + // can look it up in the evaluated value. 341 + for _, a := range e.Attrs { 342 + k, _ := a.Split() 343 + if k != "test" { 344 + continue 345 + } 346 + pa, err := parseTestAttr(a) 347 + if err != nil { 348 + appendErrRecord(a, path, false, false, err) 349 + continue 350 + } 351 + if pa.directive != "shareID" || len(pa.raw.Fields) == 0 { 352 + continue 353 + } 354 + // Require a non-integer at= giving the resolved field name. 355 + // applyShareIDAt (called in collectDirectShareIDs) will 356 + // append it to the parent path stored here. 357 + if !hasShareIDAtSel(pa) { 358 + continue // no usable at=sel for dynamic key 359 + } 360 + pa.baseLine = a.Pos().Line() 361 + pa.srcFileName = fileName 362 + // Store the parent path; applyShareIDAt in 363 + // collectDirectShareIDs appends the at= selector. 364 + records = append(records, attrRecord{ 365 + path: path, 366 + parsed: pa, 367 + }) 368 + } 369 + } 370 + 371 + case *ast.EmbedDecl: 372 + // Bare embeddings (e.g. `A & {f: v @test(...)}`) are not 373 + // walked by walkField/walkStruct, so any @test inside them 374 + // would be silently ignored. Scan and report them as errors. 375 + scanUnreachableTests(e.Expr, path, false) 376 + } 377 + } 378 + } 379 + 380 + scanUnreachableTests = func(expr ast.Expr, path cue.Path, inOperand bool) { 381 + switch e := expr.(type) { 382 + case *ast.BinaryExpr: 383 + scanUnreachableTests(e.X, path, true) 384 + scanUnreachableTests(e.Y, path, true) 385 + case *ast.ParenExpr: 386 + scanUnreachableTests(e.X, path, inOperand) 387 + case *ast.StructLit: 388 + if !inOperand { 389 + return // reachable; handled by walkField/walkStruct 390 + } 391 + for _, elt := range e.Elts { 392 + switch elt := elt.(type) { 393 + case *ast.Attribute: 394 + k, _ := elt.Split() 395 + if k == "test" { 396 + appendErrRecord(elt, path, true, false, fmt.Errorf( 397 + "@test inside a struct literal that is a binary-expression operand "+ 398 + "(e.g. X & {@test(...)}) is not reachable by the test runner; "+ 399 + "place @test after the field value (e.g. f: X & {...} @test(...)) "+ 400 + "or as a decl attribute in the enclosing struct")) 401 + } 402 + case *ast.Field: 403 + for _, a := range elt.Attrs { 404 + k, _ := a.Split() 405 + if k == "test" { 406 + appendErrRecord(a, path, false, false, fmt.Errorf( 407 + "@test on a field inside a struct literal that is a binary-expression operand "+ 408 + "(e.g. X & {f: v @test(...)}) is not reachable by the test runner; "+ 409 + "place @test after the enclosing field value (e.g. f: X & {...} @test(...))")) 410 + } 411 + } 412 + scanUnreachableTests(elt.Value, path, true) 413 + } 414 + } 415 + } 416 + } 417 + 418 + // Handle file-level decl attributes (@test as a top-level declaration). 419 + for _, decl := range f.Decls { 420 + if a, ok := decl.(*ast.Attribute); ok { 421 + k, _ := a.Split() 422 + if k == "test" { 423 + pa, err := parseTestAttr(a) 424 + if err != nil { 425 + appendErrRecord(a, cue.Path{}, false, true, err) 426 + continue 427 + } 428 + pa.baseLine = a.Pos().Line() 429 + pa.srcFileName = fileName 430 + records = append(records, attrRecord{ 431 + path: cue.Path{}, 432 + parsed: pa, 433 + fileLevel: true, 434 + }) 435 + } 436 + } 437 + } 438 + 439 + for _, decl := range f.Decls { 440 + switch d := decl.(type) { 441 + case *ast.Field: 442 + fieldPath := appendPath(cue.Path{}, d.Label, hidPkg) 443 + if fieldPath.Err() == nil { 444 + walkField(d, fieldPath) 445 + } 446 + case *ast.EmbedDecl: 447 + // Top-level bare embeddings are also not walked by walkField, so 448 + // scan them for unreachable @test attributes. 449 + scanUnreachableTests(d.Expr, cue.Path{}, false) 450 + } 451 + } 452 + 453 + return records 454 + } 455 + 456 + // identStr returns the string form of an AST label that is a simple identifier 457 + // or string literal. Returns empty string for complex labels. 458 + func identStr(label ast.Label) string { 459 + switch l := label.(type) { 460 + case *ast.Ident: 461 + return l.Name 462 + case *ast.BasicLit: 463 + // Strip quotes for string labels. 464 + s := l.Value 465 + if len(s) >= 2 && s[0] == '"' { 466 + s = s[1 : len(s)-1] 467 + } 468 + return s 469 + } 470 + return "" 471 + } 472 + 473 + // labelSelector returns the cue.Selector for an AST label, correctly handling 474 + // hidden fields (_foo) in two contexts: 475 + // 476 + // - Source-file context: pass hidPkg = ":" + f.PackageName() (or "_" for 477 + // anonymous packages). The package scope is applied to all hidden idents. 478 + // - Eq-body context: pass hidPkg = "". The caller signals that the name may 479 + // carry a $pkg suffix (e.g. "_foo$mypkg"), which is stripped and converted 480 + // to ":" + pkgname. Without a suffix, "_" is used (anonymous hidden field). 481 + func labelSelector(label ast.Label, hidPkg string) cue.Selector { 482 + if ident, ok := label.(*ast.Ident); ok && internal.IsHidden(ident.Name) { 483 + name := ident.Name 484 + pkg := hidPkg 485 + if pkg == "" { 486 + if i := strings.IndexByte(name, '$'); i >= 0 { 487 + pkg = ":" + name[i+1:] 488 + name = name[:i] 489 + } else { 490 + pkg = "_" 491 + } 492 + } 493 + return cue.Hid(name, pkg) 494 + } 495 + return cue.Label(label) 496 + } 497 + 498 + // appendPath appends a selector for label to path. 499 + // hidPkg is forwarded to labelSelector; see its documentation. 500 + func appendPath(base cue.Path, label ast.Label, hidPkg string) cue.Path { 501 + return base.Append(labelSelector(label, hidPkg)) 502 + } 503 + 504 + // parseAtPath parses an at= selector string into a cue.Path. 505 + // Unlike cue.ParsePath, it handles hidden field names with a $pkg qualifier 506 + // (e.g. "_foo$pkg" → cue.Hid("_foo", ":pkg"), matching the same syntax 507 + // accepted inside @test(eq, {...}) bodies). Dotted paths are split on "." and 508 + // each segment is processed independently, so "a._foo$pkg.b" works correctly. 509 + func parseAtPath(at string) (cue.Path, error) { 510 + // Fast path: if there are no hidden-field indicators, delegate directly. 511 + if !strings.Contains(at, "_") { 512 + p := cue.ParsePath(at) 513 + return p, p.Err() 514 + } 515 + var sels []cue.Selector 516 + for _, seg := range strings.Split(at, ".") { 517 + if internal.IsHidden(seg) { 518 + name := seg 519 + pkg := "_" 520 + if i := strings.IndexByte(name, '$'); i >= 0 { 521 + pkg = ":" + name[i+1:] 522 + name = name[:i] 523 + } 524 + sels = append(sels, cue.Hid(name, pkg)) 525 + } else { 526 + p := cue.ParsePath(seg) 527 + if err := p.Err(); err != nil { 528 + return cue.Path{}, err 529 + } 530 + sels = append(sels, p.Selectors()...) 531 + } 532 + } 533 + return cue.MakePath(sels...), nil 534 + } 535 + 536 + // applyShareIDAt applies the at= field from a @test(shareID=...) attribute 537 + // to base. For integer at=N it appends a list index; for a non-integer at=sel 538 + // it parses sel as a CUE path and appends its selectors. Returns base 539 + // unchanged if no at= is present or if the at= value cannot be parsed. 540 + func applyShareIDAt(base cue.Path, pa parsedTestAttr) cue.Path { 541 + for _, kv := range pa.raw.Fields[1:] { 542 + if kv.Key() != "at" { 543 + continue 544 + } 545 + val := kv.Value() 546 + if n, err := strconv.Atoi(val); err == nil { 547 + return base.Append(cue.Index(n)) 548 + } 549 + if p, err := parseAtPath(val); err == nil { 550 + return base.Append(p.Selectors()...) 551 + } 552 + break 553 + } 554 + return base 555 + } 556 + 557 + // hasShareIDAtSel reports whether pa contains at= with a non-integer value, 558 + // i.e. a field-name selector for use with dynamic-key fields. 559 + func hasShareIDAtSel(pa parsedTestAttr) bool { 560 + for _, kv := range pa.raw.Fields[1:] { 561 + if kv.Key() != "at" { 562 + continue 563 + } 564 + val := kv.Value() 565 + if val == "" { 566 + return false 567 + } 568 + _, err := strconv.Atoi(val) 569 + return err != nil 570 + } 571 + return false 572 + } 573 + 574 + // ───────────────────────────────────────────────────────────────────────────── 575 + // Section 3: Mode Detection 576 + // ───────────────────────────────────────────────────────────────────────────── 577 + 578 + // isInlineMode parses the CUE files in the archive (AST only) and returns true 579 + // if any @test(...) attribute is found anywhere in any CUE file — as a field 580 + // attribute, a decl attribute inside a struct at any nesting depth, or a 581 + // file-level decl attribute. No compilation is required. 582 + func isInlineMode(archive *txtar.Archive) bool { 583 + for _, f := range archive.Files { 584 + if !strings.HasSuffix(f.Name, ".cue") { 585 + continue 586 + } 587 + af, err := parser.ParseFile(f.Name, f.Data) 588 + if err != nil { 589 + continue 590 + } 591 + if declsHaveTestAttrs(af.Decls) { 592 + return true 593 + } 594 + } 595 + return false 596 + } 597 + 598 + // declsHaveTestAttrs recursively searches decls for any @test(...) attribute, 599 + // descending into struct-valued fields and comprehension bodies at any depth. 600 + func declsHaveTestAttrs(decls []ast.Decl) bool { 601 + for _, decl := range decls { 602 + switch d := decl.(type) { 603 + case *ast.Attribute: 604 + if k, _ := d.Split(); k == "test" { 605 + return true 606 + } 607 + case *ast.Field: 608 + for _, a := range d.Attrs { 609 + if k, _ := a.Split(); k == "test" { 610 + return true 611 + } 612 + } 613 + if sl, ok := d.Value.(*ast.StructLit); ok { 614 + if declsHaveTestAttrs(sl.Elts) { 615 + return true 616 + } 617 + } 618 + case *ast.Comprehension: 619 + if sl, ok := d.Value.(*ast.StructLit); ok { 620 + if declsHaveTestAttrs(sl.Elts) { 621 + return true 622 + } 623 + } 624 + } 625 + } 626 + return false 627 + }
+385
internal/cuetxtar/inline_format.go
··· 1 + // Copyright 2026 CUE Authors 2 + // 3 + // Licensed under the Apache License, Version 2.0 (the "License"); 4 + // you may not use this file except in compliance with the License. 5 + // You may obtain a copy of the License at 6 + // 7 + // http://www.apache.org/licenses/LICENSE-2.0 8 + // 9 + // Unless required by applicable law or agreed to in writing, software 10 + // distributed under the License is distributed on an "AS IS" BASIS, 11 + // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 + // See the License for the specific language governing permissions and 13 + // limitations under the License. 14 + 15 + package cuetxtar 16 + 17 + // This file implements value formatting for @test(eq, ...) bodies and 18 + // @test(debug, ...) / @test(debugCheck, ...) output capture. 19 + // Changes here are low-risk: their effect is immediately visible in test 20 + // output and golden files. 21 + 22 + import ( 23 + "fmt" 24 + "path/filepath" 25 + "strings" 26 + "testing" 27 + 28 + "cuelang.org/go/cue" 29 + "cuelang.org/go/cue/ast" 30 + "cuelang.org/go/cue/format" 31 + "cuelang.org/go/cue/literal" 32 + "cuelang.org/go/internal/core/adt" 33 + "cuelang.org/go/internal/core/debug" 34 + "cuelang.org/go/internal/cuetest" 35 + "cuelang.org/go/internal/value" 36 + ) 37 + 38 + // eqFillAttr builds an @test(eq, <value>[, at=<atStr>]) attribute for fill/force-update. 39 + func (r *inlineRunner) eqFillAttr(v cue.Value, atStr string, pa parsedTestAttr) string { 40 + if r.isError(v) { 41 + return "@test(err)" 42 + } 43 + return r.eqFillAttrStr(r.formatValue(v), atStr, pa) 44 + } 45 + 46 + // eqFillAttrStr builds @test(eq, <exprStr>[, at=<atStr>]). 47 + // For multi-line expressions, if the compact single-line form is < 20 chars it 48 + // is used directly; otherwise lines after the first are re-indented using the 49 + // leading whitespace of the source line containing the @test attribute (the 50 + // same offset trick as formatDebugAttr, but without an extra tab because 51 + // format.Node already carries one tab of relative indentation). 52 + func (r *inlineRunner) eqFillAttrStr(exprStr, atStr string, pa parsedTestAttr) string { 53 + if strings.Contains(exprStr, "\n") { 54 + if compact := compactCUEExpr(exprStr); len(compact) < 20 { 55 + exprStr = compact 56 + } else { 57 + indent := r.attrLineIndent(pa) 58 + exprStr = strings.ReplaceAll(exprStr, "\n", "\n"+indent) 59 + } 60 + } 61 + if atStr != "" { 62 + return fmt.Sprintf("@test(eq, %s, at=%s)", exprStr, atStr) 63 + } 64 + return fmt.Sprintf("@test(eq, %s)", exprStr) 65 + } 66 + 67 + // compactCUEExpr collapses a multi-line CUE expression produced by format.Node 68 + // into a single line. It handles struct and list literals by joining their 69 + // tab-indented field lines with ", ". Only safe for shallow (non-nested) 70 + // structures; deeply-nested values will exceed the 20-char threshold and use 71 + // the indented form instead. 72 + func compactCUEExpr(s string) string { 73 + lines := strings.Split(s, "\n") 74 + parts := make([]string, 0, len(lines)) 75 + for _, line := range lines { 76 + trimmed := strings.TrimLeft(line, "\t") 77 + if trimmed != "" { 78 + parts = append(parts, trimmed) 79 + } 80 + } 81 + if len(parts) < 2 { 82 + return strings.Join(parts, "") 83 + } 84 + open, close_ := parts[0], parts[len(parts)-1] 85 + middle := parts[1 : len(parts)-1] 86 + if (open == "{" || open == "[") && len(middle) > 0 { 87 + return open + strings.Join(middle, ", ") + close_ 88 + } 89 + return strings.Join(parts, " ") 90 + } 91 + 92 + // formatValue returns a human-readable CUE string for a value. 93 + // Routes through the Vertex export path (via cue.Final()) to avoid internal 94 + // _#def wrapping, then re-enables optional fields (value?: T) so the 95 + // formatted expression round-trips through astCmp. 96 + func (r *inlineRunner) formatValue(v cue.Value) string { 97 + var b strings.Builder 98 + eqWriteValue(value.OpContext(v), &b, v) 99 + return b.String() 100 + } 101 + 102 + // eqWriteValue writes a CUE value to b in @test(eq, ...) body notation. 103 + // 104 + // Compared with v.Syntax() + cue.Final() + format.Node: 105 + // - Hidden fields use _foo$pkg notation (matching astcmp.go conventions). 106 + // - adt.Disjunction values are emitted as *d1 | d2 (with * for defaults) 107 + // instead of being collapsed to the default by cue.Final(). 108 + // - adt.Conjunction values are emitted as c1 & c2. 109 + func eqWriteValue(opCtx *adt.OpContext, b *strings.Builder, v cue.Value) { 110 + tv := v.Core() 111 + vx := tv.V.DerefValue() 112 + 113 + switch bv := vx.BaseValue.(type) { 114 + case *adt.Disjunction: 115 + eqWriteDisjunction(opCtx, b, bv) 116 + return 117 + case *adt.Conjunction: 118 + eqWriteConjunction(opCtx, b, bv) 119 + return 120 + } 121 + 122 + // Use struct emission if the kind is struct OR if there are arcs — the 123 + // latter handles error vertices that still carry child fields (e.g. a 124 + // struct with one bad field: the parent vertex is _|_ but its arcs hold 125 + // the successfully-evaluated sibling fields). 126 + if v.IncompleteKind() == cue.StructKind || len(vx.Arcs) > 0 { 127 + eqWriteStruct(opCtx, b, vx) 128 + return 129 + } 130 + 131 + // Scalar values and lists: fall back to the standard syntax formatter. 132 + // cue.Final() resolves defaults and avoids _#def wrapping. 133 + syn := v.Syntax(cue.Docs(false), cue.Final(), cue.Optional(true)) 134 + stripComments(syn) 135 + bs, err := format.Node(syn, format.Simplify()) 136 + if err != nil { 137 + fmt.Fprintf(b, "%#v", v) 138 + } else { 139 + b.Write(bs) 140 + } 141 + } 142 + 143 + // eqWriteDisjunction emits disjuncts as *d1 | d2 | d3 (defaults first with *). 144 + func eqWriteDisjunction(opCtx *adt.OpContext, b *strings.Builder, dj *adt.Disjunction) { 145 + for i, v := range dj.Values { 146 + if i > 0 { 147 + b.WriteString(" | ") 148 + } 149 + if i < dj.NumDefaults { 150 + b.WriteByte('*') 151 + } 152 + eqWriteValue(opCtx, b, value.Make(opCtx, v)) 153 + } 154 + } 155 + 156 + // eqWriteConjunction emits conjuncts as c1 & c2 & c3. 157 + func eqWriteConjunction(opCtx *adt.OpContext, b *strings.Builder, conj *adt.Conjunction) { 158 + for i, v := range conj.Values { 159 + if i > 0 { 160 + b.WriteString(" & ") 161 + } 162 + eqWriteValue(opCtx, b, value.Make(opCtx, v)) 163 + } 164 + } 165 + 166 + // eqWriteStruct emits a struct, using _foo$pkg notation for hidden-field labels. 167 + func eqWriteStruct(opCtx *adt.OpContext, b *strings.Builder, vx *adt.Vertex) { 168 + b.WriteByte('{') 169 + first := true 170 + for _, arc := range vx.Arcs { 171 + if arc.ArcType == adt.ArcNotPresent || arc.Label.IsLet() { 172 + continue 173 + } 174 + // Skip hidden fields from external packages: their PkgID is a full 175 + // module path (e.g. "mod.test/pkg") which cannot be encoded as a 176 + // valid CUE identifier in the $pkg suffix notation. 177 + if arc.Label.IsHidden() { 178 + pkg := arc.Label.PkgID(opCtx) 179 + if pkg != "_" && !strings.HasPrefix(pkg, ":") { 180 + continue 181 + } 182 + } 183 + if !first { 184 + b.WriteString(", ") 185 + } 186 + first = false 187 + eqWriteLabel(opCtx, b, arc.Label, arc.ArcType) 188 + b.WriteString(": ") 189 + eqWriteValue(opCtx, b, value.Make(opCtx, arc)) 190 + } 191 + b.WriteByte('}') 192 + } 193 + 194 + // eqWriteLabel writes a field label. 195 + // For hidden labels the $pkg qualifier is included when the field is 196 + // package-scoped (pkg != "_"). Callers must ensure the label's PkgID is 197 + // either "_" or colon-prefixed (inline package) before calling — see 198 + // the skip guard in eqWriteStruct. 199 + func eqWriteLabel(opCtx *adt.OpContext, b *strings.Builder, f adt.Feature, arcType adt.ArcType) { 200 + if f.IsHidden() { 201 + name := f.IdentString(opCtx) 202 + pkg := f.PkgID(opCtx) 203 + if pkg != "_" { 204 + // PkgID returns ":pkgname" for inline sources; convert to "$pkgname". 205 + b.WriteString(name + "$" + strings.TrimPrefix(pkg, ":")) 206 + } else { 207 + b.WriteString(name) 208 + } 209 + } else { 210 + b.WriteString(f.SelectorString(opCtx)) 211 + } 212 + b.WriteString(arcType.Suffix()) 213 + } 214 + 215 + // stripComments removes all comment groups from every node in the AST. 216 + // Error nodes produced by v.Syntax() carry line comments like 217 + // "// path: error message"; if left in, the // sequence inside an 218 + // @test(eq, ...) attribute body would be parsed as a CUE comment and 219 + // consume the closing ), corrupting the attribute syntax. 220 + func stripComments(node ast.Node) { 221 + ast.Walk(node, func(n ast.Node) bool { 222 + ast.SetComments(n, nil) 223 + return true 224 + }, nil) 225 + } 226 + 227 + // eqBodySupportedDirectives lists the @test directive names that are 228 + // intentionally processed by astCmp when they appear inside an @test(eq, ...) 229 + // body (as field-level attributes or struct-level decl attributes). 230 + // Any other directive has no effect there. 231 + var eqBodySupportedDirectives = map[string]bool{ 232 + "final": true, // field-level and struct-level: resolve default before comparing 233 + "ignore": true, // field-level: skip eq descent; field need not exist 234 + "err": true, // field-level: check that value is an error 235 + "shareID": true, // field-level: sharing assertion (handled by extractShareIDsFromEqExpr) 236 + "checkOrder": true, // struct-level decl: require fields in declaration order 237 + } 238 + 239 + // reportEqBodyTestAttrs walks the expected expression of an @test(eq, ...) 240 + // body and reports any @test field attributes that have no effect there. 241 + // Directives listed in eqBodySupportedDirectives are intentionally processed 242 + // by astCmp and are excluded from the error. 243 + func reportEqBodyTestAttrs(t testing.TB, path cue.Path, expr ast.Node) { 244 + t.Helper() 245 + ast.Walk(expr, func(n ast.Node) bool { 246 + f, ok := n.(*ast.Field) 247 + if !ok { 248 + return true 249 + } 250 + for _, a := range f.Attrs { 251 + k, _ := a.Split() 252 + if k != "test" { 253 + continue 254 + } 255 + pa, err := parseTestAttr(a) 256 + if err != nil { 257 + continue 258 + } 259 + if eqBodySupportedDirectives[pa.directive] { 260 + continue 261 + } 262 + t.Errorf("path %s: @test(%s) in @test(eq, ...) body has no effect; place it as a field attribute on the actual value", path, pa.directive) 263 + } 264 + return true 265 + }, nil) 266 + } 267 + 268 + // runDebugCheckInline checks the debug printer output of val against the 269 + // expected string in the @test(debugCheck, "...") attribute. 270 + // When CUE_UPDATE modes are active, enqueues a write-back. 271 + func (r *inlineRunner) runDebugCheckInline(t testing.TB, path cue.Path, val cue.Value, pa parsedTestAttr) { 272 + t.Helper() 273 + name := pa.raw.Fields[0].Value() // preserves any :vN version suffix 274 + if len(pa.raw.Fields) < 2 { 275 + // Empty @test(debugCheck) — fill placeholder. 276 + if cuetest.UpdateGoldenFiles { 277 + actual := r.debugPrinterOutput(val) 278 + r.enqueueInlineFill(pa, r.formatDebugAttr(name, actual, pa)) 279 + } 280 + return 281 + } 282 + expected := pa.raw.Fields[1].Value() 283 + actual := r.debugPrinterOutput(val) 284 + match := normalizeLines(actual) == normalizeLines(expected) 285 + if match && !cuetest.ForceUpdateGoldenFiles { 286 + return 287 + } 288 + if cuetest.ForceUpdateGoldenFiles || cuetest.UpdateGoldenFiles { 289 + r.enqueueInlineFill(pa, r.formatDebugAttr(name, actual, pa)) 290 + return 291 + } 292 + if !match { 293 + t.Errorf("path %s: @test(debugCheck) mismatch:\ngot: %q\nwant: %q", path, actual, expected) 294 + logHint(t, pa.hint) 295 + } 296 + } 297 + 298 + // runDebugOutputInline captures the debug printer output of val as an 299 + // informational annotation (@test(debug, ...)). Unlike debugCheck, a 300 + // mismatch does not fail the test — it only logs and auto-updates when 301 + // CUE_UPDATE is active. 302 + func (r *inlineRunner) runDebugOutputInline(t testing.TB, path cue.Path, val cue.Value, pa parsedTestAttr) { 303 + t.Helper() 304 + name := pa.raw.Fields[0].Value() // preserves any :vN version suffix 305 + actual := r.debugPrinterOutput(val) 306 + if len(pa.raw.Fields) < 2 { 307 + // Empty @test(debug) — fill placeholder. 308 + if cuetest.UpdateGoldenFiles { 309 + r.enqueueInlineFill(pa, r.formatDebugAttr(name, actual, pa)) 310 + } 311 + return 312 + } 313 + expected := pa.raw.Fields[1].Value() 314 + match := normalizeLines(actual) == normalizeLines(expected) 315 + if match && !cuetest.ForceUpdateGoldenFiles { 316 + return 317 + } 318 + // Always auto-update on mismatch (informational, not an assertion). 319 + if cuetest.ForceUpdateGoldenFiles || cuetest.UpdateGoldenFiles { 320 + r.enqueueInlineFill(pa, r.formatDebugAttr(name, actual, pa)) 321 + return 322 + } 323 + if !match { 324 + t.Logf("path %s: @test(debug) changed:\ngot: %q\nwant: %q", path, actual, expected) 325 + } 326 + } 327 + 328 + // formatDebugAttr returns the @test(name, ...) attribute text for a debug value. 329 + func (r *inlineRunner) formatDebugAttr(name, actual string, pa parsedTestAttr) string { 330 + actual = strings.TrimRight(actual, "\n") 331 + n := strings.Count(r.attrLineIndent(pa), "\t") 332 + actual = literal.String.WithOptionalTabIndent(n + 1).Quote(actual) 333 + return fmt.Sprintf("@test(%s, %s)", name, actual) 334 + } 335 + 336 + // attrLineIndent returns the leading whitespace on the source line that 337 + // contains pa's @test attribute. Used to compute the indentation level 338 + // for multi-line debug attribute values. 339 + func (r *inlineRunner) attrLineIndent(pa parsedTestAttr) string { 340 + offset := pa.srcAttr.Pos().Offset() 341 + for _, f := range r.archive.Files { 342 + if f.Name != pa.srcFileName { 343 + continue 344 + } 345 + data := f.Data 346 + start := offset 347 + for start > 0 && data[start-1] != '\n' { 348 + start-- 349 + } 350 + end := start 351 + for end < offset && data[end] == '\t' { 352 + end++ 353 + } 354 + return string(data[start:end]) 355 + } 356 + return "" 357 + } 358 + 359 + // debugPrinterOutput returns the standard debug-printer representation of val, 360 + // equivalent to what appears in out/eval golden sections. 361 + // Absolute file paths from module-aware loading are normalized to relative. 362 + func (r *inlineRunner) debugPrinterOutput(val cue.Value) string { 363 + c := val.Core() 364 + if c.V == nil { 365 + return "" 366 + } 367 + out := debug.NodeString(c.R, c.V, nil) 368 + if r.dir != "" { 369 + out = strings.ReplaceAll(out, filepath.ToSlash(r.dir)+"/", "") 370 + } 371 + return out 372 + } 373 + 374 + // normalizeLines trims trailing whitespace from each line and strips any 375 + // trailing blank lines, for use in debug: textual comparison. 376 + func normalizeLines(s string) string { 377 + lines := strings.Split(s, "\n") 378 + for i, line := range lines { 379 + lines[i] = strings.TrimRight(line, " \t") 380 + } 381 + for len(lines) > 0 && lines[len(lines)-1] == "" { 382 + lines = lines[:len(lines)-1] 383 + } 384 + return strings.Join(lines, "\n") 385 + }
+216
internal/cuetxtar/inline_shareid.go
··· 1 + // Copyright 2026 CUE Authors 2 + // 3 + // Licensed under the Apache License, Version 2.0 (the "License"); 4 + // you may not use this file except in compliance with the License. 5 + // You may obtain a copy of the License at 6 + // 7 + // http://www.apache.org/licenses/LICENSE-2.0 8 + // 9 + // Unless required by applicable law or agreed to in writing, software 10 + // distributed under the License is distributed on an "AS IS" BASIS, 11 + // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 + // See the License for the specific language governing permissions and 13 + // limitations under the License. 14 + 15 + package cuetxtar 16 + 17 + // This file implements @test(shareID=...) vertex-sharing assertions. 18 + // This is isolated here because it is high-scrutiny: it verifies evaluator 19 + // internals (vertex identity) and any logic change could silently allow 20 + // regressions in structure sharing. 21 + 22 + import ( 23 + "testing" 24 + 25 + "cuelang.org/go/cue" 26 + "cuelang.org/go/cue/ast" 27 + "cuelang.org/go/cue/parser" 28 + ) 29 + 30 + // ───────────────────────────────────────────────────────────────────────────── 31 + // Section 8: shareID — vertex sharing assertions 32 + // ───────────────────────────────────────────────────────────────────────────── 33 + 34 + // extractShareIDsFromEqExpr walks the struct literal of an @test(eq, STRUCT) 35 + // body and collects all @test(shareID=name) annotations on fields. 36 + // basePath is the CUE path of the @test(eq) attribute; field paths in the 37 + // struct are appended to it. version is the active evaluator version name 38 + // used for version-specific share groups (@test(shareID=name)). 39 + // Returns a map from shareID name to the absolute paths of fields in that group. 40 + func extractShareIDsFromEqExpr(expr ast.Expr, basePath cue.Path, version string) map[string][]cue.Path { 41 + s, ok := expr.(*ast.StructLit) 42 + if !ok { 43 + return nil 44 + } 45 + var result map[string][]cue.Path 46 + for _, d := range s.Elts { 47 + f, ok := d.(*ast.Field) 48 + if !ok { 49 + continue 50 + } 51 + for _, a := range f.Attrs { 52 + if k, _ := a.Split(); k != "test" { 53 + continue 54 + } 55 + pa, err := parseTestAttr(a) 56 + if err != nil || pa.directive != "shareID" { 57 + continue 58 + } 59 + // Version filter: skip if a non-matching version is specified. 60 + if pa.version != "" && pa.version != version { 61 + continue 62 + } 63 + if len(pa.raw.Fields) == 0 { 64 + continue 65 + } 66 + shareIDName := pa.raw.Fields[0].Value() 67 + if shareIDName == "" { 68 + continue 69 + } 70 + fieldPath := applyShareIDAt(basePath.Append(labelSelector(f.Label, "")), pa) 71 + if result == nil { 72 + result = make(map[string][]cue.Path) 73 + } 74 + result[shareIDName] = append(result[shareIDName], fieldPath) 75 + } 76 + } 77 + return result 78 + } 79 + 80 + // collectShareIDsForRoot builds a map of shareID name → CUE paths by scanning 81 + // all attrRecords within rootPath in two ways: 82 + // 83 + // 1. Direct @test(shareID=name) field attributes in the source — each record 84 + // with directive "shareID" contributes its rec.path to the named group. 85 + // 86 + // 2. @test(shareID=name) annotations on fields inside @test(eq, STRUCT) bodies 87 + // — the struct is parsed and fields carrying shareID annotations are mapped 88 + // to their absolute paths (basePath + fieldLabel). 89 + func (r *inlineRunner) collectShareIDsForRoot(records []attrRecord, rootPath cue.Path, version string) map[string][]cue.Path { 90 + var shareGroups map[string][]cue.Path 91 + add := func(id string, p cue.Path) { 92 + if shareGroups == nil { 93 + shareGroups = make(map[string][]cue.Path) 94 + } 95 + shareGroups[id] = append(shareGroups[id], p) 96 + } 97 + 98 + // Track processed eq attrs by (fileName, offset) to avoid double-counting. 99 + type attrKey struct { 100 + file string 101 + offset int 102 + } 103 + seenEq := make(map[attrKey]bool) 104 + 105 + for _, rec := range records { 106 + if !pathHasPrefix(rec.path, rootPath) { 107 + continue 108 + } 109 + pa := rec.parsed 110 + // Version filter: skip directives targeting a different version. 111 + if pa.version != "" && pa.version != version { 112 + continue 113 + } 114 + 115 + switch pa.directive { 116 + case "shareID": 117 + // Direct field attribute: @test(shareID=name) on a source field. 118 + // Optional at=N sub-option selects list element N within the field. 119 + if len(pa.raw.Fields) == 0 { 120 + continue 121 + } 122 + shareIDName := pa.raw.Fields[0].Value() 123 + if shareIDName == "" { 124 + continue 125 + } 126 + add(shareIDName, applyShareIDAt(rec.path, pa)) 127 + 128 + case "eq": 129 + // Eq body: extract @test(shareID=name) from fields in the struct literal. 130 + if len(pa.raw.Fields) < 2 { 131 + continue 132 + } 133 + key := attrKey{file: pa.srcFileName, offset: pa.srcAttr.Pos().Offset()} 134 + if seenEq[key] { 135 + continue 136 + } 137 + seenEq[key] = true 138 + eqExpr, err := parser.ParseExpr("shareID", pa.raw.Fields[1].Text()) 139 + if err != nil { 140 + continue 141 + } 142 + for id, paths := range extractShareIDsFromEqExpr(eqExpr, rec.path, version) { 143 + for _, p := range paths { 144 + add(id, p) 145 + } 146 + } 147 + } 148 + } 149 + return shareGroups 150 + } 151 + 152 + // collectDirectShareIDs builds a shareID group map from direct @test(shareID=name) 153 + // field attributes across ALL records at any nesting depth (no root filtering). 154 + // This is used for cross-root sharing assertions where fields from different 155 + // roots share a vertex. Eq-body sharing is handled per-root by 156 + // collectShareIDsForRoot. 157 + func (r *inlineRunner) collectDirectShareIDs(records []attrRecord, version string) map[string][]cue.Path { 158 + var shareGroups map[string][]cue.Path 159 + for _, rec := range records { 160 + if rec.fileLevel { 161 + continue 162 + } 163 + 164 + pa := rec.parsed 165 + if pa.version != "" && pa.version != version { 166 + continue 167 + } 168 + if pa.directive != "shareID" { 169 + continue 170 + } 171 + if len(pa.raw.Fields) == 0 { 172 + continue 173 + } 174 + shareIDName := pa.raw.Fields[0].Value() 175 + if shareIDName == "" { 176 + continue 177 + } 178 + if shareGroups == nil { 179 + shareGroups = make(map[string][]cue.Path) 180 + } 181 + shareGroups[shareIDName] = append(shareGroups[shareIDName], applyShareIDAt(rec.path, pa)) 182 + } 183 + return shareGroups 184 + } 185 + 186 + // runShareIDChecks verifies that all paths in each shareID group dereference to 187 + // the same canonical *adt.Vertex, confirming that the CUE evaluator shares the 188 + // vertex rather than copying it. 189 + func (r *inlineRunner) runShareIDChecks(t testing.TB, fileVal cue.Value, shareGroups map[string][]cue.Path) { 190 + t.Helper() 191 + for id, paths := range shareGroups { 192 + if len(paths) < 2 { 193 + continue // need at least two to assert sharing 194 + } 195 + firstVal := fileVal.LookupPath(paths[0]) 196 + firstCore := firstVal.Core() 197 + if firstCore.V == nil { 198 + t.Errorf("@test(shareID=%s): path %s: not found in evaluated value", id, paths[0]) 199 + continue 200 + } 201 + derefFirst := firstCore.V.DerefValue() 202 + for _, p := range paths[1:] { 203 + otherVal := fileVal.LookupPath(p) 204 + otherCore := otherVal.Core() 205 + if otherCore.V == nil { 206 + t.Errorf("@test(shareID=%s): path %s: not found in evaluated value", id, p) 207 + continue 208 + } 209 + derefOther := otherCore.V.DerefValue() 210 + if derefFirst != derefOther { 211 + t.Errorf("@test(shareID=%s): %s and %s are not shared (different vertices)", 212 + id, paths[0], p) 213 + } 214 + } 215 + } 216 + }