this repo has no description
0
fork

Configure Feed

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

internal/cuetxtar: emit @test(err,...) annotation for error fields in eq fill

When CUE_UPDATE=1 fills an @test(pass) or
@test(eq) body and a struct field evaluates to
an error, the emitted expression now includes a
@test(err, code=..., contains="...", pos=[])
annotation on that field. This removes the need
to hand-annotate error fields after fill: the
error code and message are populated immediately;
pos=[] is left as a placeholder that a second
CUE_UPDATE=1 pass fills in automatically.

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

+147 -29
+12 -2
internal/cuetxtar/astcmp.go
··· 44 44 // innerAttrText is the raw text of the inner @test(err,...) attribute 45 45 // (used to locate it within the outer @test(eq,...) source text). 46 46 posWriteback func(innerAttrText string, positions []token.Pos) 47 + // formatPos, when non-nil, formats a token.Pos for error messages. 48 + // When nil, positions are formatted as line:col. 49 + formatPos func(token.Pos) string 50 + } 51 + 52 + func (c *cmpCtx) fmtPos(p token.Pos) string { 53 + if c.formatPos != nil { 54 + return c.formatPos(p) 55 + } 56 + return fmt.Sprintf("%d:%d", p.Line(), p.Column()) 47 57 } 48 58 49 59 // astCompare compares a parsed CUE AST expression against an evaluated ··· 678 688 if len(positions) != len(ea.pos) { 679 689 var got []string 680 690 for _, p := range positions { 681 - got = append(got, fmt.Sprintf("%d:%d", p.Line(), p.Column())) 691 + got = append(got, c.fmtPos(p)) 682 692 } 683 693 msg := formatPosCountMismatch("@test(err, pos=...)", len(positions), len(ea.pos)) 684 694 return pathErr(path, "%s %v", msg, got) ··· 710 720 if !found { 711 721 var got []string 712 722 for _, p := range positions { 713 - got = append(got, fmt.Sprintf("%d:%d", p.Line(), p.Column())) 723 + got = append(got, c.fmtPos(p)) 714 724 } 715 725 wantLine := c.baseLine + exp.deltaLine 716 726 return pathErr(path, "@test(err, pos=...): no match for expected position %d:%d in %v",
+2 -1
internal/cuetxtar/inline.go
··· 791 791 case "": 792 792 // Empty placeholder @test() — fill with actual value when CUE_UPDATE=1. 793 793 if cuetest.UpdateGoldenFiles { 794 - r.enqueueInlineFill(pa, r.formatCoverAttr(val)) 794 + r.enqueueInlineFill(pa, r.formatCoverAttr(val, pa.srcFileName)) 795 795 } 796 796 default: 797 797 t.Errorf("path %s: unknown @test directive %q", path, pa.directive) ··· 880 880 posWriteback: func(innerAttrText string, positions []token.Pos) { 881 881 r.enqueueNestedPosWrite(pa, innerAttrText, positions) 882 882 }, 883 + formatPos: func(p token.Pos) string { return r.formatPosSpec(p, 0, pa.srcFileName) }, 883 884 } 884 885 cmpErr := ctx.astCmp(cue.Path{}, expr, val) 885 886 if cmpErr == nil {
+88 -22
internal/cuetxtar/inline_format.go
··· 27 27 28 28 "cuelang.org/go/cue" 29 29 "cuelang.org/go/cue/ast" 30 + cueerrors "cuelang.org/go/cue/errors" 30 31 "cuelang.org/go/cue/format" 31 32 "cuelang.org/go/cue/literal" 33 + "cuelang.org/go/cue/token" 32 34 "cuelang.org/go/internal/core/adt" 33 35 "cuelang.org/go/internal/core/debug" 34 36 "cuelang.org/go/internal/cuetest" 35 37 "cuelang.org/go/internal/value" 36 38 ) 37 39 40 + // eqWriter bundles the runner and source file name for writing @test(eq, ...) 41 + // bodies. Methods on eqWriter access r.formatPosSpec and the opCtx directly, 42 + // eliminating the need to thread a formatPos closure through every call. 43 + type eqWriter struct { 44 + r *inlineRunner 45 + srcFileName string 46 + opCtx *adt.OpContext 47 + } 48 + 49 + func (w *eqWriter) formatPos(p token.Pos) string { 50 + return w.r.formatPosSpec(p, 0, w.srcFileName) 51 + } 52 + 38 53 // eqFillAttr builds an @test(eq, <value>[, at=<atStr>]) attribute for fill/force-update. 39 54 func (r *inlineRunner) eqFillAttr(v cue.Value, atStr string, pa parsedTestAttr) string { 40 55 if r.isError(v) { 41 56 return "@test(err)" 42 57 } 43 - return r.eqFillAttrStr(r.formatValue(v), atStr, pa) 58 + return r.eqFillAttrStr(r.formatValue(v, pa.srcFileName), atStr, pa) 44 59 } 45 60 46 61 // eqFillAttrStr builds @test(eq, <exprStr>[, at=<atStr>]). ··· 90 105 } 91 106 92 107 // eqCompactThreshold is the maximum byte length of a compact struct expression 93 - // before eqWriteValue switches to multi-line (one field per line) form. 108 + // before writeValue switches to multi-line (one field per line) form. 94 109 const eqCompactThreshold = 40 95 110 96 111 // formatValue returns a human-readable CUE string for a value. ··· 98 113 // form exceeds eqCompactThreshold are returned in multi-line form with 99 114 // recursive indentation; eqFillAttrStr handles re-indentation relative to the 100 115 // source attribute line. 101 - func (r *inlineRunner) formatValue(v cue.Value) string { 116 + func (r *inlineRunner) formatValue(v cue.Value, srcFileName string) string { 102 117 var b strings.Builder 103 - eqWriteValue(value.OpContext(v), &b, v, "\t") 118 + w := &eqWriter{r: r, srcFileName: srcFileName, opCtx: value.OpContext(v)} 119 + w.writeValue(&b, v, "\t") 104 120 return b.String() 105 121 } 106 122 107 - // eqWriteValue writes a CUE value to b in @test(eq, ...) body notation. 123 + // writeValue writes a CUE value to b in @test(eq, ...) body notation. 108 124 // 109 125 // nestedIndent controls multi-line formatting for struct values: when non-empty, 110 126 // a struct whose compact form exceeds eqCompactThreshold is written in ··· 119 135 // - adt.Disjunction values are emitted as *d1 | d2 (with * for defaults) 120 136 // instead of being collapsed to the default by cue.Final(). 121 137 // - adt.Conjunction values are emitted as c1 & c2. 122 - func eqWriteValue(opCtx *adt.OpContext, b *strings.Builder, v cue.Value, nestedIndent string) { 138 + func (w *eqWriter) writeValue(b *strings.Builder, v cue.Value, nestedIndent string) { 123 139 tv := v.Core() 124 140 vx := tv.V.DerefValue() 125 141 126 142 switch bv := vx.BaseValue.(type) { 127 143 case *adt.Disjunction: 128 - eqWriteDisjunction(opCtx, b, bv) 144 + w.writeDisjunction(b, bv) 129 145 return 130 146 case *adt.Conjunction: 131 - eqWriteConjunction(opCtx, b, bv) 147 + w.writeConjunction(b, bv) 132 148 return 133 149 } 134 150 ··· 143 159 if nestedIndent != "" { 144 160 // Multi-line mode: use compact if it fits, else recurse one level deeper. 145 161 var compact strings.Builder 146 - eqWriteStruct(opCtx, &compact, vx, "") 162 + w.writeStruct(&compact, vx, "") 147 163 if compact.Len() <= eqCompactThreshold { 148 164 b.WriteString(compact.String()) 149 165 return 150 166 } 151 167 } 152 - eqWriteStruct(opCtx, b, vx, nestedIndent) 168 + w.writeStruct(b, vx, nestedIndent) 153 169 return 154 170 } 155 171 ··· 157 173 // This avoids the confusing let-containing struct that v.Syntax(Final()) 158 174 // generates when it tries to make the expression self-contained. 159 175 // astCmp requires _|_ to match only error values. 176 + // writeStruct adds the @test(err, ...) annotation after _|_ for field arcs. 160 177 if v.Err() != nil { 161 178 b.WriteString("_|_") 162 179 return ··· 174 191 } 175 192 } 176 193 177 - // eqWriteDisjunction emits disjuncts as *d1 | d2 | d3 (defaults first with *). 178 - func eqWriteDisjunction(opCtx *adt.OpContext, b *strings.Builder, dj *adt.Disjunction) { 194 + // writeDisjunction emits disjuncts as *d1 | d2 | d3 (defaults first with *). 195 + func (w *eqWriter) writeDisjunction(b *strings.Builder, dj *adt.Disjunction) { 179 196 for i, v := range dj.Values { 180 197 if i > 0 { 181 198 b.WriteString(" | ") ··· 183 200 if i < dj.NumDefaults { 184 201 b.WriteByte('*') 185 202 } 186 - eqWriteValue(opCtx, b, value.Make(opCtx, v), "") 203 + w.writeValue(b, value.Make(w.opCtx, v), "") 187 204 } 188 205 } 189 206 190 - // eqWriteConjunction emits conjuncts as c1 & c2 & c3. 191 - func eqWriteConjunction(opCtx *adt.OpContext, b *strings.Builder, conj *adt.Conjunction) { 207 + // writeConjunction emits conjuncts as c1 & c2 & c3. 208 + func (w *eqWriter) writeConjunction(b *strings.Builder, conj *adt.Conjunction) { 192 209 for i, v := range conj.Values { 193 210 if i > 0 { 194 211 b.WriteString(" & ") 195 212 } 196 - eqWriteValue(opCtx, b, value.Make(opCtx, v), "") 213 + w.writeValue(b, value.Make(w.opCtx, v), "") 197 214 } 198 215 } 199 216 200 - // eqWriteStruct emits a struct using _foo$pkg notation for hidden-field labels. 217 + // isLeafError reports whether writeValue would render v as bare _|_ 218 + // (i.e. an error that is neither a struct with child arcs nor a list). 219 + // Used by writeStruct to decide whether to append a @test(err, ...) annotation. 220 + func isLeafError(v cue.Value) bool { 221 + if v.Err() == nil { 222 + return false 223 + } 224 + tv := v.Core() 225 + vx := tv.V.DerefValue() 226 + k := v.IncompleteKind() 227 + return !((k == cue.StructKind || len(vx.Arcs) > 0) && k != cue.ListKind) 228 + } 229 + 230 + // writeErrAnnotation appends a @test(err, code=..., contains="...", pos=[...]) 231 + // annotation for a leaf error arc value. code=, contains=, and pos= are all 232 + // filled immediately from the actual error so the annotation is stable after a 233 + // single CUE_UPDATE=1 pass. 234 + func (w *eqWriter) writeErrAnnotation(b *strings.Builder, v cue.Value) { 235 + tv := v.Core() 236 + if tv.V == nil { 237 + return 238 + } 239 + bot := tv.V.DerefValue().Bottom() 240 + if bot == nil { 241 + return 242 + } 243 + b.WriteString(" @test(err, code=") 244 + b.WriteString(bot.Code.String()) 245 + if bot.Err != nil { 246 + fmt.Fprintf(b, ", contains=%q", bot.Err.Error()) 247 + } 248 + // Fill positions immediately so the annotation is stable in one pass. 249 + positions := cueerrors.Positions(v.Err()) 250 + b.WriteString(", pos=[") 251 + for i, p := range positions { 252 + if i > 0 { 253 + b.WriteString(", ") 254 + } 255 + b.WriteString(w.formatPos(p)) 256 + } 257 + b.WriteString("])") 258 + } 259 + 260 + // writeStruct emits a struct using _foo$pkg notation for hidden-field labels. 201 261 // 202 262 // nestedIndent controls layout: 203 263 // - "" (compact): fields are separated by ", ". 204 264 // - non-empty: each field is preceded by "\n"+nestedIndent; the closing "}" 205 265 // is preceded by "\n"+nestedIndent[:-1] (one tab less). Field values 206 266 // receive nestedIndent+"\t" so they can recurse one level deeper. 207 - func eqWriteStruct(opCtx *adt.OpContext, b *strings.Builder, vx *adt.Vertex, nestedIndent string) { 267 + func (w *eqWriter) writeStruct(b *strings.Builder, vx *adt.Vertex, nestedIndent string) { 208 268 b.WriteByte('{') 209 269 first := true 210 270 for _, arc := range vx.Arcs { ··· 215 275 // module path (e.g. "mod.test/pkg") which cannot be encoded as a 216 276 // valid CUE identifier in the $pkg suffix notation. 217 277 if arc.Label.IsHidden() { 218 - pkg := arc.Label.PkgID(opCtx) 278 + pkg := arc.Label.PkgID(w.opCtx) 219 279 if pkg != "_" && !strings.HasPrefix(pkg, ":") { 220 280 continue 221 281 } ··· 226 286 b.WriteString(", ") 227 287 } 228 288 first = false 229 - eqWriteLabel(opCtx, b, arc.Label, arc.ArcType) 289 + eqWriteLabel(w.opCtx, b, arc.Label, arc.ArcType) 230 290 b.WriteString(": ") 231 - eqWriteValue(opCtx, b, value.Make(opCtx, arc), nestedIndent+"\t") 291 + arcVal := value.Make(w.opCtx, arc) 292 + w.writeValue(b, arcVal, nestedIndent+"\t") 293 + // For leaf error fields, append a @test(err, ...) annotation with 294 + // actual error details and immediately-filled positions. 295 + if isLeafError(arcVal) { 296 + w.writeErrAnnotation(b, arcVal) 297 + } 232 298 } 233 299 if nestedIndent != "" && !first { 234 300 b.WriteString("\n" + nestedIndent[:len(nestedIndent)-1]) ··· 240 306 // For hidden labels the $pkg qualifier is included when the field is 241 307 // package-scoped (pkg != "_"). Callers must ensure the label's PkgID is 242 308 // either "_" or colon-prefixed (inline package) before calling — see 243 - // the skip guard in eqWriteStruct. 309 + // the skip guard in writeStruct. 244 310 func eqWriteLabel(opCtx *adt.OpContext, b *strings.Builder, f adt.Feature, arcType adt.ArcType) { 245 311 if f.IsHidden() { 246 312 name := f.IdentString(opCtx)
+1 -1
internal/cuetxtar/inlinerunner_test.go
··· 69 69 }, 70 70 { 71 71 // @test(err) inside @test(eq) body can carry filled pos= specs. 72 - // pos=[3:5] means absolute line 3, col 5 (baseLine=0 convention). 72 + // pos=[4:5, 1:8] uses absolute line numbers (baseLine=0 convention). 73 73 name: "eq with nested @test(err, pos=) passes", 74 74 archive: "-- test.cue --\na: {b: int}\nc: a & {\n\tb: 100\n\td: a.b + 3\n} @test(eq, {b: 100, d: _|_ @test(err, code=incomplete, pos=[4:5, 1:8])})\n", 75 75 },
+20
internal/cuetxtar/inlinetxtar_test.go
··· 151 151 staleMsg: "out/update/ sections present but update produces same result as input (run with CUE_UPDATE=1 to remove)", 152 152 }) 153 153 154 + // --- Plain run on update output --- 155 + // When update changed the source, run the updated files as a fresh test to 156 + // verify the update output is itself a passing test. This replaces the need 157 + // for separate *_run.txtar files that duplicate the update output as input. 158 + updateOutputPasses := updateIdentical // trivially true when nothing changed 159 + if !updateIdentical { 160 + capUR := &cuetxtar.FailCapture{TB: t} 161 + withUpdateMode(false, false, func() { 162 + runner := cuetxtar.NewInlineRunnerCapture(t, nil, cloneTxtarArchive(update1), t.TempDir(), capUR) 163 + runner.Run() 164 + }) 165 + if errs := capUR.Messages(); errs != "" { 166 + t.Errorf("plain run of update output produced errors:\n%s", errs) 167 + } else { 168 + updateOutputPasses = true 169 + } 170 + } 171 + 154 172 // --- Force update (CUE_UPDATE=force) --- 155 173 // out/force/ sections: present only when force produces a different result than update. 156 174 ··· 178 196 var statusLines []string 179 197 if updateIdentical { 180 198 statusLines = append(statusLines, "update: identical to input") 199 + } else if updateOutputPasses { 200 + statusLines = append(statusLines, "update: output passes run") 181 201 } 182 202 if !forceDiffers { 183 203 statusLines = append(statusLines, "force: identical to update")
+1 -1
internal/cuetxtar/permute.go
··· 232 232 } 233 233 t.Errorf("path %s: @test(permute): ordering [%s] produces a different result\ngot: %s\nwant: %s", 234 234 structPath, strings.Join(permNames, ", "), 235 - r.formatValue(permVal), r.formatValue(baseline)) 235 + r.formatValue(permVal, ""), r.formatValue(baseline, "")) 236 236 } 237 237 return 238 238 }
+19
internal/cuetxtar/testdata/inline/eq_fill_err_annotation.txtar
··· 1 + # Tests that CUE_UPDATE=1 emits a @test(err, code=..., contains="...", pos=[...]) 2 + # annotation for error-valued fields when filling a bare @test(eq) body. 3 + # Positions are filled immediately so the output is stable after a single pass. 4 + -- in/test.cue -- 5 + x: { 6 + a: int 7 + b: a + 3 8 + } @test(eq) 9 + -- out/update/test.cue -- 10 + x: { 11 + a: int 12 + b: a + 3 13 + } @test(eq, { 14 + a: int 15 + b: _|_ @test(err, code=incomplete, contains="x.b: non-concrete value int in operand to +", pos=[3:5, 2:5]) 16 + }) 17 + -- out/status.txt -- 18 + update: output passes run 19 + force: identical to update
+1
internal/cuetxtar/testdata/inline/pos_fill.txtar
··· 40 40 }) 41 41 } 42 42 -- out/status.txt -- 43 + update: output passes run 43 44 force: identical to update
+1
internal/cuetxtar/testdata/inline/seed.txtar
··· 3 3 -- out/update/test.cue -- 4 4 a: 1 @test(eq, 1) 5 5 -- out/status.txt -- 6 + update: output passes run 6 7 force: identical to update
+2 -2
internal/cuetxtar/writeback.go
··· 96 96 // formatCoverAttr returns the @test attribute text to insert for a field whose 97 97 // evaluated value is v. For non-error values this is @test(eq, <value>). 98 98 // For error values it falls back to a bare @test(err) placeholder. 99 - func (r *inlineRunner) formatCoverAttr(v cue.Value) string { 99 + func (r *inlineRunner) formatCoverAttr(v cue.Value, srcFileName string) string { 100 100 if r.isError(v) { 101 101 return "@test(err)" 102 102 } 103 - return fmt.Sprintf("@test(eq, %s)", r.formatValue(v)) 103 + return fmt.Sprintf("@test(eq, %s)", r.formatValue(v, srcFileName)) 104 104 }