this repo has no description
0
fork

Configure Feed

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

tools/fix: add inline @test framework

Add inline test framework in fixall_test.go that
runs @test assertions on both pre-fix and
post-fix archives to verify fixes preserve
semantics.

The idea: @test attributes get copied by the fix
the the new files as is.
By testing both in and out, we ensure that the
code generated by fix is correct.

Add @test(eq) assertions to aliasv2
for field aliases, value aliases, and __self
handling for demonstration.

Fix compile errors in aliasv2.txtar: add missing
variable definitions (x, name) for dynamic
fields, add references to unused aliases (Z,
OptField, ReqField, X, DynInterpAlias), fix
Inner.Leaf.value to Inner.deep.value.

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

+235 -46
+172
tools/fix/fixall_test.go
··· 15 15 package fix 16 16 17 17 import ( 18 + "bytes" 18 19 "fmt" 20 + "slices" 19 21 "strings" 20 22 "testing" 21 23 22 24 "cuelang.org/go/cue/format" 23 25 "cuelang.org/go/internal/cuetxtar" 24 26 "cuelang.org/go/mod/modfile" 27 + "golang.org/x/tools/txtar" 25 28 ) 26 29 27 30 func TestInstances(t *testing.T) { ··· 44 47 } 45 48 err := Instances(a, opts...) 46 49 t.WriteErrors(err) 50 + 51 + // Collect formatted fixed files for golden output and inline testing. 52 + fixedFiles := make(map[string][]byte) 47 53 for _, b := range a { 48 54 // Output module file if it exists and was potentially modified 49 55 if b.ModuleFile != nil { 50 56 if data, err := modfile.Format(b.ModuleFile); err == nil { 51 57 fmt.Fprintln(t, "---", "cue.mod/module.cue") 52 58 fmt.Fprint(t, string(data)) 59 + fixedFiles["cue.mod/module.cue"] = data 53 60 } 54 61 } 55 62 for _, f := range b.Files { 56 63 b, _ := format.Node(f) 57 64 fmt.Fprintln(t, "---", t.Rel(f.Filename)) 58 65 fmt.Fprint(t, string(b)) 66 + fixedFiles[t.Rel(f.Filename)] = b 59 67 } 60 68 } 69 + 70 + // If any input CUE file has @test annotations, verify that the 71 + // assertions pass on both the original and the fixed output. 72 + // This ensures the fix is semantics-preserving. 73 + runInlineTests(t.T, t.Archive, t.Dir, fixedFiles) 61 74 }) 62 75 } 76 + 77 + // runInlineTests runs @test assertions on both the original archive and 78 + // on a post-fix archive built from fixedFiles. Skipped if the archive 79 + // has no @test annotations (making this a no-op for existing tests). 80 + func runInlineTests(t *testing.T, archive *txtar.Archive, dir string, fixedFiles map[string][]byte) { 81 + t.Helper() 82 + 83 + if !archiveHasTestAttrs(archive) { 84 + return 85 + } 86 + 87 + // Run @test assertions on the original (pre-fix) archive. 88 + t.Run("pre-fix", func(t *testing.T) { 89 + cap := &cuetxtar.FailCapture{TB: t} 90 + runner := cuetxtar.NewInlineRunnerCapture(t, nil, archive, dir, cap) 91 + runner.Run() 92 + if cap.Failed() { 93 + t.Errorf("@test assertions failed on original (pre-fix) input:\n%s", cap.Messages()) 94 + } 95 + }) 96 + 97 + // Build a post-fix archive with the fixed CUE files. 98 + postFixArchive := buildPostFixArchive(archive, fixedFiles) 99 + 100 + // Run @test assertions on the fixed output. 101 + t.Run("post-fix", func(t *testing.T) { 102 + cap := &cuetxtar.FailCapture{TB: t} 103 + runner := cuetxtar.NewInlineRunnerCapture(t, nil, postFixArchive, dir, cap) 104 + runner.Run() 105 + if cap.Failed() { 106 + t.Errorf("@test assertions failed on fixed output:\n%s", cap.Messages()) 107 + } 108 + }) 109 + } 110 + 111 + // TestFixSemanticsDetectsBreak verifies that runInlineTests catches a 112 + // deliberately broken fix. It provides a pre-fix archive with @test 113 + // annotations and a fixedFiles map where the __reclose wrapper is 114 + // missing, then asserts that the post-fix @test assertions fail. 115 + func TestFixSemanticsDetectsBreak(t *testing.T) { 116 + // Pre-fix archive: old semantics (no explicitopen), embedding 117 + // propagates closedness from #A. 118 + archive := txtar.Parse([]byte(` 119 + #no-coverage 120 + 121 + -- cue.mod/module.cue -- 122 + module: "test.example" 123 + language: version: "v0.15.0" 124 + 125 + -- in.cue -- 126 + package foo 127 + 128 + #A: a: int 129 + 130 + X: { 131 + #A 132 + b: int 133 + } 134 + 135 + tests: { 136 + t1: err: X & {c: 1} @test(err, code=eval, contains="field not allowed", pos=[0:16]) 137 + } 138 + `)) 139 + 140 + dir := t.TempDir() 141 + 142 + // Correct fix: __reclose preserves closedness in new semantics. 143 + t.Run("correct-fix", func(t *testing.T) { 144 + correctFixed := map[string][]byte{ 145 + "in.cue": []byte(`@experiment(explicitopen) 146 + 147 + package foo 148 + 149 + #A: a: int 150 + 151 + X: __reclose({ 152 + #A... 153 + b: int 154 + }) 155 + 156 + tests: { 157 + t1: err: X & {c: 1} @test(err, code=eval, contains="field not allowed", pos=[0:16]) 158 + } 159 + `), 160 + } 161 + runInlineTests(t, archive, dir, correctFixed) 162 + }) 163 + 164 + // Broken fix: __reclose is missing, X becomes open, t1 should fail. 165 + t.Run("broken-fix", func(t *testing.T) { 166 + brokenFixed := map[string][]byte{ 167 + "in.cue": []byte(`@experiment(explicitopen) 168 + 169 + package foo 170 + 171 + #A: a: int 172 + 173 + X: { 174 + #A... 175 + b: int 176 + } 177 + 178 + tests: { 179 + t1: err: X & {c: 1} @test(err, code=eval, contains="field not allowed", pos=[0:16]) 180 + } 181 + `), 182 + } 183 + 184 + // We expect the post-fix sub-test to fail. Wrap in a helper 185 + // that captures and verifies the failure. 186 + var postFixFailed bool 187 + t.Run("post-fix", func(t *testing.T) { 188 + postFixArchive := buildPostFixArchive(archive, brokenFixed) 189 + cap := &cuetxtar.FailCapture{TB: t} 190 + runner := cuetxtar.NewInlineRunnerCapture(t, nil, postFixArchive, dir, cap) 191 + runner.Run() 192 + if cap.Failed() { 193 + postFixFailed = true 194 + t.Logf("correctly detected broken fix:\n%s", cap.Messages()) 195 + } 196 + }) 197 + if !postFixFailed { 198 + t.Fatal("expected broken fix to fail @test assertions, but it passed") 199 + } 200 + }) 201 + } 202 + 203 + // archiveHasTestAttrs reports whether any CUE file in the archive contains 204 + // an @test( attribute. 205 + func archiveHasTestAttrs(a *txtar.Archive) bool { 206 + for _, f := range a.Files { 207 + if strings.HasSuffix(f.Name, ".cue") && bytes.Contains(f.Data, []byte("@test(")) { 208 + return true 209 + } 210 + } 211 + return false 212 + } 213 + 214 + // buildPostFixArchive constructs a new txtar archive with the fixed CUE file 215 + // contents, preserving all other files (module.cue, non-CUE files) and the 216 + // archive comment. Output sections (out/*) are stripped since the inline 217 + // runner doesn't need them. 218 + func buildPostFixArchive(orig *txtar.Archive, fixedFiles map[string][]byte) *txtar.Archive { 219 + result := &txtar.Archive{ 220 + Comment: orig.Comment, 221 + Files: slices.Clone(orig.Files), 222 + } 223 + // Replace CUE files and module.cue with fixed versions. 224 + for i, f := range result.Files { 225 + if data, ok := fixedFiles[f.Name]; ok { 226 + result.Files[i] = txtar.File{Name: f.Name, Data: data} 227 + } 228 + } 229 + // Strip out/* sections — they're for golden-file comparison, not evaluation. 230 + result.Files = slices.DeleteFunc(result.Files, func(f txtar.File) bool { 231 + return strings.HasPrefix(f.Name, "out/") 232 + }) 233 + return result 234 + }
+63 -46
tools/fix/testdata/aliasv2.txtar
··· 1 1 2 2 #exp:aliasv2 3 + #no-coverage 3 4 4 5 -- cue.mod/module.cue -- 5 6 module: "test.example" ··· 12 13 // Simple field alias 13 14 X=a: { 14 15 foo: 1 15 - bar: X.foo + 2 16 + bar: X.foo + 2 @test(eq, 3) 16 17 } 17 18 18 19 // Multiple aliases ··· 20 21 data: 42 21 22 } 22 23 Z=c: { 23 - val: Y.data 24 + val: Y.data @test(eq, 42) 24 25 } 26 + zref: Z.val @test(eq, 42) 25 27 -- b.cue -- 26 28 package foo 27 29 ··· 31 33 Leaf=deep: { 32 34 value: 1 33 35 } 34 - ref: Leaf.value 36 + ref: Leaf.value @test(eq, 1) 35 37 } 36 - ref2: Inner.Leaf.value 38 + ref2: Inner.deep.value @test(eq, 1) 37 39 } 38 40 -- c.cue -- 39 41 package foo ··· 42 44 OptField=a?: { 43 45 value: 1 44 46 } 47 + optRef: OptField.value 45 48 46 49 ReqField=b!: { 47 50 value: 2 48 51 } 52 + reqRef: ReqField.value 49 53 -- d.cue -- 50 54 // Already has postfix alias experiment, so do not do any changes. 51 55 @experiment(aliasv2) ··· 55 59 a~X: { 56 60 foo: 1 57 61 } 62 + ref: X.foo 58 63 -- e.cue -- 59 64 package foo 60 65 ··· 73 78 -- f.cue -- 74 79 package foo 75 80 81 + x: "key" 82 + name: "dyn" 83 + 76 84 // Dynamic fields with old alias syntax 77 85 DynAlias=(x): { 78 86 field: 1 ··· 90 98 val: 3 91 99 ref: InterpAlias.val 92 100 } 101 + dynRef: DynInterpAlias.val 93 102 // Just the interpolation 94 103 InterpAlias="\(name)": { 95 104 val: 3 ··· 100 109 101 110 // Value aliases - old syntax X={...} 102 111 // Should convert to let with self 103 - foo: X={ 104 - x: X.a 105 - y: X.b 112 + valAlias: X={ 113 + x: X.a @test(eq, 1) 114 + y: X.b @test(eq, 2) 106 115 a: 1 107 116 b: 2 108 117 } 109 118 110 - bar: Y={ 111 - data: Y.x + 10 119 + computed: Y={ 120 + data: Y.x + 10 @test(eq, 15) 112 121 x: 5 113 122 } 114 123 115 124 // Nested value alias 116 - outer: { 125 + nested: { 117 126 inner: Z={ 118 - value: Z.n * 2 127 + value: Z.n * 2 @test(eq, 6) 119 128 n: 3 120 129 } 121 130 } ··· 123 132 // Multiple fields with value aliases 124 133 multi: { 125 134 first: A={ 126 - val: A.base * 2 135 + val: A.base * 2 @test(eq, 20) 127 136 base: 10 128 137 } 129 138 second: B={ 130 - val: B.base * 3 139 + val: B.base * 3 @test(eq, 60) 131 140 base: 20 132 141 } 133 142 } ··· 141 150 142 151 // Field named self in same struct 143 152 samestruct: X={ 144 - i: X.self 153 + i: X.self @test(eq, 42) 145 154 self: 42 146 155 } 147 156 ··· 149 158 enclosingfield: { 150 159 self: 42 151 160 inner: X={ 152 - i: X.a 161 + i: X.a @test(eq, 1) 153 162 a: 1 154 163 } 155 164 } ··· 158 167 enclosinglet: { 159 168 let self = {z: 99} 160 169 inner: X={ 161 - i: X.a 170 + i: X.a @test(eq, 1) 162 171 a: 1 163 - j: self.z 172 + j: self.z @test(eq, 99) 164 173 } 165 174 } 166 175 ··· 169 178 self=bar: { 170 179 data: 1 171 180 } 172 - baz: self.data 173 - foo: X={ 174 - i: X.a 181 + baz: self.data @test(eq, 1) 182 + valAlias: X={ 183 + i: X.a @test(eq, 1) 175 184 a: 1 176 185 } 177 186 } ··· 181 190 self=bar: { 182 191 data: 1 183 192 } 184 - baz: self.data 193 + baz: self.data @test(eq, 1) 185 194 inner: { 186 - foo: X={ 187 - i: X.a 195 + valAlias: X={ 196 + i: X.a @test(eq, 1) 188 197 a: 1 189 198 } 190 199 } ··· 203 212 // Simple field alias 204 213 a~(X): { 205 214 foo: 1 206 - bar: X.foo + 2 215 + bar: X.foo + 2 @test(eq, 3) 207 216 } 208 217 209 218 // Multiple aliases ··· 211 220 data: 42 212 221 } 213 222 c~(Z): { 214 - val: Y.data 223 + val: Y.data @test(eq, 42) 215 224 } 225 + zref: Z.val @test(eq, 42) 216 226 --- b.cue 217 227 @experiment(aliasv2) 218 228 ··· 224 234 deep~(Leaf): { 225 235 value: 1 226 236 } 227 - ref: Leaf.value 237 + ref: Leaf.value @test(eq, 1) 228 238 } 229 - ref2: Inner.Leaf.value 239 + ref2: Inner.deep.value @test(eq, 1) 230 240 } 231 241 --- c.cue 232 242 @experiment(aliasv2) ··· 237 247 a~(OptField)?: { 238 248 value: 1 239 249 } 250 + optRef: OptField.value 240 251 b~(ReqField)!: { 241 252 value: 2 242 253 } 254 + reqRef: ReqField.value 243 255 --- d.cue 244 256 // Already has postfix alias experiment, so do not do any changes. 245 257 @experiment(aliasv2) ··· 249 261 a~(X): { 250 262 foo: 1 251 263 } 264 + ref: X.foo 252 265 --- e.cue 253 266 @experiment(aliasv2) 254 267 ··· 271 284 272 285 package foo 273 286 287 + x: "key" 288 + name: "dyn" 289 + 274 290 // Dynamic fields with old alias syntax 275 291 (x)~(DynAlias): { 276 292 field: 1 ··· 288 304 val: 3 289 305 ref: InterpAlias.val 290 306 } 307 + dynRef: DynInterpAlias.val 291 308 // Just the interpolation 292 309 "\(name)"~(InterpAlias): { 293 310 val: 3 ··· 300 317 301 318 // Value aliases - old syntax X={...} 302 319 // Should convert to let with self 303 - foo: { 320 + valAlias: { 304 321 let X = self 305 - x: X.a 306 - y: X.b 322 + x: X.a @test(eq, 1) 323 + y: X.b @test(eq, 2) 307 324 a: 1 308 325 b: 2 309 326 } 310 327 311 - bar: { 328 + computed: { 312 329 let Y = self 313 - data: Y.x + 10 330 + data: Y.x + 10 @test(eq, 15) 314 331 x: 5 315 332 } 316 333 317 334 // Nested value alias 318 - outer: { 335 + nested: { 319 336 inner: { 320 337 let Z = self 321 - value: Z.n * 2 338 + value: Z.n * 2 @test(eq, 6) 322 339 n: 3 323 340 } 324 341 } ··· 327 344 multi: { 328 345 first: { 329 346 let A = self 330 - val: A.base * 2 347 + val: A.base * 2 @test(eq, 20) 331 348 base: 10 332 349 } 333 350 second: { 334 351 let B = self 335 - val: B.base * 3 352 + val: B.base * 3 @test(eq, 60) 336 353 base: 20 337 354 } 338 355 } ··· 349 366 // Field named self in same struct 350 367 samestruct: { 351 368 let X = __self 352 - i: X.self 369 + i: X.self @test(eq, 42) 353 370 self: 42 354 371 } 355 372 ··· 358 375 self: 42 359 376 inner: { 360 377 let X = __self 361 - i: X.a 378 + i: X.a @test(eq, 1) 362 379 a: 1 363 380 } 364 381 } ··· 368 385 let self = {z: 99} 369 386 inner: { 370 387 let X = __self 371 - i: X.a 388 + i: X.a @test(eq, 1) 372 389 a: 1 373 - j: self.z 390 + j: self.z @test(eq, 99) 374 391 } 375 392 } 376 393 ··· 379 396 bar~(self): { 380 397 data: 1 381 398 } 382 - baz: self.data 383 - foo: { 399 + baz: self.data @test(eq, 1) 400 + valAlias: { 384 401 let X = __self 385 - i: X.a 402 + i: X.a @test(eq, 1) 386 403 a: 1 387 404 } 388 405 } ··· 392 409 bar~(self): { 393 410 data: 1 394 411 } 395 - baz: self.data 412 + baz: self.data @test(eq, 1) 396 413 inner: { 397 - foo: { 414 + valAlias: { 398 415 let X = __self 399 - i: X.a 416 + i: X.a @test(eq, 1) 400 417 a: 1 401 418 } 402 419 }