this repo has no description
0
fork

Configure Feed

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

internal/cuetxtar: merge permuteCount into permute directive

Replace the separate @test(permuteCount, N) directive with a count=
option embedded in @test(permute, count=N). This ensures the count is
always co-located with the permute directive and is auto-filled by
CUE_UPDATE=1.

For the decl form (@test(permute) as a struct declaration), count= is
placed on the permute directive itself. For the field form, count= is
placed on the first annotated field in the group. The count= (empty)
placeholder is filled by CUE_UPDATE=1; a non-empty value is verified
and updated by CUE_UPDATE=force.

Remove permuteCount handling from the inline runner and update all
test files accordingly.

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

+171 -203
+3 -6
cue/testdata/basicrewrite/019_resolved_self-reference_cycles.txtar
··· 4 4 # mutual struct cycles with two (nodes.t2) and three (nodes.t3) participants. 5 5 -- in.cue -- 6 6 math: { 7 - @test(permute) 7 + @test(permute, count=6) 8 8 a: b - 100 9 9 b: a + 100 10 10 b: 200 11 11 @test(eq, {a: 100, b: 200}) 12 - @test(permuteCount, 6) 13 12 } 14 13 15 14 a: 100 @test(eq, 100) ··· 28 27 nodes: t1: p2: s1: {a: 1} & s1 @test(eq, {a: 1}) 29 28 // nodes.t2: two-node mutual cycle; field order must not affect the result. 30 29 nodes: t2: { 31 - @test(permute) 30 + @test(permute, count=2) 32 31 s1: s2 & {a: 1} 33 32 s2: s1 & {b: 2} 34 33 @test(eq, {s1: {a: 1, b: 2}, s2: {b: 2, a: 1}}) 35 - @test(permuteCount, 2) 36 34 } 37 35 // nodes.t3: three-node mutual cycle; all 6 orderings must yield equal results. 38 36 nodes: t3: { 39 - @test(permute) 37 + @test(permute, count=6) 40 38 s1: s2 & {a: 1} 41 39 s2: s3 & {b: 2} 42 40 s3: s1 & {c: 3} 43 41 @test(eq, {s1: {a: 1, b: 2, c: 3}, s2: {b: 2, c: 3, a: 1}, s3: {c: 3, a: 1, b: 2}}) 44 - @test(permuteCount, 6) 45 42 } 46 43 47 44 -- out/eval/stats --
+1 -2
cue/testdata/compile/alias.txtar
··· 1 1 -- in.cue -- 2 2 dynamic: { 3 - @test(permute) 4 - @test(permuteCount, 6) 3 + @test(permute, count=6) 5 4 6 5 X=(a): 1 7 6 x: X
+2 -4
cue/testdata/cycle/compbottom.txtar
··· 30 30 #message: #"^(message: (?P<message>.*))?$"# 31 31 32 32 p1: { 33 - @test(permute) 34 - @test(permuteCount, 24) 33 + @test(permute, count=24) 35 34 36 35 X: "message: hello" 37 36 #aux: { ··· 57 56 #userHostPort: #"^((?P<userinfo>[[:alnum:]]*)@)?(?P<host>[[:alnum:].]+)$"# 58 57 59 58 p1: { 60 - @test(permute) 61 - @test(permuteCount, 120) 59 + @test(permute, count=120) 62 60 63 61 Y: { 64 62 userinfo: "user"
+1 -2
cue/testdata/cycle/compbottom2.txtar
··· 102 102 } 103 103 104 104 oneDirectionalBrokenConflictSuccess: { 105 - @test(permute) 106 - @test(permuteCount, 6) 105 + @test(permute, count=6) 107 106 b: foo: "" 108 107 a: { if b.foo == _|_ { bar: "" } } 109 108 b: { if a.bar == _|_ { new: "" } }
+18 -21
cue/testdata/cycle/compbottomnofinal.txtar
··· 29 29 #userHostPort: #"^(:(?P<port>\d+))?$"# 30 30 31 31 p1: { 32 - @test(permute) 33 - @test(permuteCount, 2) 32 + @test(permute, count=2) 34 33 #Y: regexp.FindNamedSubmatch(#userHostPort, #X.port) 35 34 36 35 #X: { ··· 42 41 } 43 42 } 44 43 @test(eq, { 45 - #Y: _|_ @test(err, code=incomplete, contains="#Y: undefined field: port", pos=[32:50]) 46 - #X: _|_ @test(err, code=incomplete, contains="#X.port: cyclic reference to field port", pos=[35:4]) 44 + #Y: _|_ @test(err, code=incomplete, contains="#Y: undefined field: port", pos=[31:50]) 45 + #X: _|_ @test(err, code=incomplete, contains="#X.port: cyclic reference to field port", pos=[34:4]) 47 46 }) 48 47 } 49 48 } ··· 52 51 #userHostPort: #"^(:(?P<port>\d+))?$"# 53 52 54 53 p1: { 55 - @test(permute) 56 - @test(permuteCount, 6) 54 + @test(permute, count=6) 57 55 58 56 #Y: regexp.FindNamedSubmatch(#userHostPort, #X.port) 59 57 ··· 75 73 } 76 74 } 77 75 @test(eq, { 78 - #Y: _|_ @test(err, code=incomplete, contains="medium.p1.#Y: undefined field: port", pos=[56:50]) 76 + #Y: _|_ @test(err, code=incomplete, contains="medium.p1.#Y: undefined field: port", pos=[54:50]) 79 77 Y: {} 80 - #X: _|_ @test(err, code=incomplete, contains="medium.p1.#X.port: cyclic reference to field port", pos=[68:4]) 78 + #X: _|_ @test(err, code=incomplete, contains="medium.p1.#X.port: cyclic reference to field port", pos=[66:4]) 81 79 }) 82 80 } 83 81 } ··· 86 84 #userHostPort: #"^((?P<userinfo>[[:alnum:]]*)@)?(?P<host>[[:alnum:].]+)(:(?P<port>\d+))?$"# 87 85 88 86 p1: { 89 - @test(permute) 90 - @test(permuteCount, 120) 87 + @test(permute, count=120) 91 88 92 89 Y: { 93 90 userinfo: "user" ··· 130 127 @test(eq, { 131 128 Y: { 132 129 userinfo: "user" 133 - host: _|_ @test(err, code=incomplete, contains="large.p1.Y.host: undefined field: host", pos=[120:13]) 130 + host: _|_ @test(err, code=incomplete, contains="large.p1.Y.host: undefined field: host", pos=[117:13]) 134 131 } 135 - X: _|_ @test(err, code=incomplete, contains="large.p1.X: undefined field: port", pos=[95:33]) 132 + X: _|_ @test(err, code=incomplete, contains="large.p1.X: undefined field: port", pos=[92:33]) 136 133 #X: { 137 134 _|_ 138 135 userinfo: "user@" 139 136 host: "mod.test" 140 137 } 141 - #Y: _|_ @test(err, code=incomplete, contains="large.p1.X: undefined field: port", pos=[95:33]) 138 + #Y: _|_ @test(err, code=incomplete, contains="large.p1.X: undefined field: port", pos=[92:33]) 142 139 }) 143 140 } 144 141 } ··· 148 145 [incomplete] minimal.b.port: cyclic reference to field port: 149 146 ./in.cue:19:3 150 147 [incomplete] small.p1.#Y: undefined field: port: 151 - ./in.cue:32:50 148 + ./in.cue:31:50 152 149 [incomplete] small.p1.#X.port: cyclic reference to field port: 153 - ./in.cue:35:4 150 + ./in.cue:34:4 154 151 [incomplete] medium.p1.#Y: undefined field: port: 155 - ./in.cue:56:50 152 + ./in.cue:54:50 156 153 [incomplete] medium.p1.#X.port: cyclic reference to field port: 157 - ./in.cue:68:4 154 + ./in.cue:66:4 158 155 [incomplete] large.p1.Y.host: undefined field: host: 159 - ./in.cue:120:13 156 + ./in.cue:117:13 160 157 [incomplete] large.p1.X: undefined field: port: 161 - ./in.cue:95:33 158 + ./in.cue:92:33 162 159 [incomplete] large.p1.#X.port: cyclic reference to field port: 163 - ./in.cue:107:4 160 + ./in.cue:104:4 164 161 [incomplete] large.p1.X: undefined field: port: 165 - ./in.cue:95:33 162 + ./in.cue:92:33 166 163 -- out/todo.txt -- 167 164 minimal.a/b: different error code in v2 (cycle) and v3 (incomplete). 168 165 small.p1: v2 #Y succeeds with port="", v3 #Y errors; error codes differ.
+1 -2
cue/testdata/cycle/constraints.txtar
··· 209 209 } 210 210 p2: { 211 211 // TODO(eval): fix permute or eval: error messages differ based on path in the two cases. 212 - // @test(permute) 213 - // @test(permuteCount, 2) 212 + // @test(permute, count=2) 214 213 T: b: D 215 214 D: a?: T 216 215 @test(eq, {
+4 -5
cue/testdata/cycle/freeze.txtar
··· 15 15 // This test would fail in the old evaluator, depending on ordering. 16 16 comprehension: t1: _ 17 17 comprehension: t1: ok:{ 18 - @test(permute) 19 - @test(permuteCount) 18 + @test(permute, count=) 20 19 21 20 x: y: {} 22 21 // order of comprehensions matter in old evaluator. ··· 109 108 } 110 109 -- out/errors.txt -- 111 110 [eval] comprehension.t2.err.a: adding field xq not allowed as field set was already referenced: 112 - ./comprehension.cue:24:13 111 + ./comprehension.cue:23:13 113 112 [eval] comprehension.t3.err.a: adding field xq not allowed as field set was already referenced: 114 - ./comprehension.cue:36:13 113 + ./comprehension.cue:35:13 115 114 [eval] comprehension.moreSpecific.err.a.x: adding field z not allowed as field set was already referenced: 116 - ./comprehension.cue:74:9 115 + ./comprehension.cue:73:9 117 116 -- out/eval/stats -- 118 117 Leaks: 4 119 118 Freed: 68
+1 -1
cue/testdata/definitions/fields.txtar
··· 224 224 // either allow for an error variant that only "allows" all fields but does not 225 225 // define them as optional top, or rethink the closedness model altogether. 226 226 err: t1: { 227 - @test(permute) 227 + @test(permute, count=6) 228 228 #D: _ & {a: 2} 229 229 a: #D 230 230 a: disallowed: 1 @test(err, code=eval, contains="field not allowed", pos=[0:5])
+15 -16
cue/testdata/disjunctions/edge.txtar
··· 35 35 // resolve x to a concrete value. As `x` is defined outside the 36 36 // disjunction, and the disjunction cannot be resolved without it, `x` 37 37 // cannot be evaluated to a concrete value, resulting in an error. 38 - @test(permute) 38 + @test(permute, count=6) 39 39 x: c.e 40 40 c: d: x 41 41 c: {d: 1, e: 2} | {d : 2, e: 2} ··· 47 47 // be disambiguated as a result of computing x. This means c.e cannot 48 48 // be concrete at the time of evaluation. 49 49 // NOTE: not so obvious. It clearly works, and arguably it should. 50 - @test(permute) 51 - @test(permuteCount, 6) 50 + @test(permute, count=6) 52 51 x: div(c.e, 2) 53 52 c: d: x 54 53 c: {d: 1, e: 2} | {d : 2, e: 3} ··· 57 56 err3: { 58 57 // Here it is clear that d will not disambiguate the disjunction at c. 59 58 // So c.e. cannot be resolved. 60 - @test(permute) 59 + @test(permute, count=6) 61 60 x: c.e @test(err, code=incomplete, contains="unresolved disjunction", pos=[0:6]) 62 61 c: d: 1 | 2 | x 63 62 c: {d: 1, e: 2} | {d : 2, e: 3} 64 63 @test(eq, { 65 - x: _|_ @test(err, code=incomplete, contains="unresolved disjunction", pos=[55:6]) 64 + x: _|_ @test(err, code=incomplete, contains="unresolved disjunction", pos=[54:6]) 66 65 c: {d: 1, e: 2} | {d: 2, e: 3} 67 66 }) 68 67 } ··· 72 71 err1: {x: 2, c: {d: 2, e: 2}} 73 72 err2: {x: 1, c: {d: 1, e: 2}} 74 73 err3: { 75 - x: _|_ @test(err, code=incomplete, contains="unresolved disjunction {d:1,e:2} | {d:2,e:3} (type struct)", pos=[55:6]) 74 + x: _|_ @test(err, code=incomplete, contains="unresolved disjunction {d:1,e:2} | {d:2,e:3} (type struct)", pos=[54:6]) 76 75 c: {d: 1, e: 2} | {d: 2, e: 3} 77 76 } 78 77 }) ··· 174 173 } 175 174 -- out/errors.txt -- 176 175 [incomplete] cyclicalInsertInDisjunction.err3.x: unresolved disjunction {d:1,e:2} | {d:2,e:3} (type struct): 177 - ./in.cue:55:6 176 + ./in.cue:54:6 178 177 [incomplete] issue4251_notC.#NotC._must: non-concrete value string in operand to !=: 179 - ./in.cue:105:17 180 - ./in.cue:103:11 181 - ./in.cue:104:3 178 + ./in.cue:104:17 179 + ./in.cue:102:11 180 + ./in.cue:103:3 182 181 [incomplete] issue4251_notB.#NotB._must: non-concrete value string in operand to !=: 183 - ./in.cue:116:17 184 - ./in.cue:114:11 185 - ./in.cue:115:3 182 + ./in.cue:115:17 183 + ./in.cue:113:11 184 + ./in.cue:114:3 186 185 [incomplete] issue4251_notB_reversed.#NotB._must: non-concrete value string in operand to !=: 187 - ./in.cue:126:17 188 - ./in.cue:124:11 189 - ./in.cue:125:3 186 + ./in.cue:125:17 187 + ./in.cue:123:11 188 + ./in.cue:124:3 190 189 -- out/eval/stats -- 191 190 Leaks: 24 192 191 Freed: 175
+4 -5
cue/testdata/eval/disjunctions.txtar
··· 61 61 } 62 62 63 63 t10: p1: { 64 - // @test(permute) 65 - // @test(permuteCount, 120) // TODO(inline): we could reduce this by grouping fields. 64 + // @test(permute, count=120) // TODO(inline): we could reduce this by grouping fields. 66 65 schema: test 67 66 schema: string | {name: string} 68 67 #A: {string | {name: string}} ··· 144 143 j: i.bar 145 144 @test(eq, { 146 145 i: null | {bar: 2} 147 - j: _|_ @test(err, code=incomplete, contains="d100.j: unresolved disjunction null | {bar:2} (type (null|struct))", pos=[143:5]) 146 + j: _|_ @test(err, code=incomplete, contains="d100.j: unresolved disjunction null | {bar:2} (type (null|struct))", pos=[142:5]) 148 147 }) 149 148 } 150 149 ··· 456 455 // a string interpolation references an optional field inside a disjunction. 457 456 // https://cuelang.org/issue/3899 458 457 issue3899: reduced: { 459 - @test(permute) 458 + @test(permute, count=2) 460 459 b: c + "" | string 461 460 c?: string 462 461 @test(eq, {b: string, c?: string}) ··· 545 544 ./in.cue:15:4 546 545 ./in.cue:15:27 547 546 [incomplete] d100.j: unresolved disjunction null | {bar:2} (type (null|struct)): 548 - ./in.cue:143:5 547 + ./in.cue:142:5 549 548 [incomplete] issue3899.full.#Schema.deployImage: invalid non-ground value string (must be concrete string): 550 549 ./issue3899.cue:18:18 551 550 -- out/todo.txt --
+8 -9
cue/testdata/eval/dynamic_field.txtar
··· 33 33 @test(eq, {x: "foo", foo: {}}) 34 34 } 35 35 chainedResolution: x: { 36 - @test(permute) 37 - @test(permuteCount, 6) 36 + @test(permute, count=6) 38 37 (x.baz): "bar" 39 38 (x.foo): "baz" 40 39 baz: "foo" ··· 117 116 ./in.cue:14:31 118 117 ./in.cue:14:35 119 118 [incomplete] noCycleError.foo.baz.#ID: invalid interpolation: non-concrete value string (type string): 120 - ./in.cue:47:8 121 - ./in.cue:47:11 119 + ./in.cue:46:8 120 + ./in.cue:46:11 122 121 noCycleError.foo.bar.entries: key value of dynamic field must be concrete, found _|_(noCycleError.foo.baz.#ID: invalid interpolation: non-concrete value string (type string)): 123 - ./in.cue:49:22 122 + ./in.cue:48:22 124 123 [incomplete] noCycleError.foo.bar.#ID: invalid interpolation: non-concrete value string (type string): 125 - ./in.cue:47:8 126 - ./in.cue:47:11 124 + ./in.cue:46:8 125 + ./in.cue:46:11 127 126 [incomplete] noCycleError.foo.baz.#ID: invalid interpolation: non-concrete value string (type string): 128 - ./in.cue:47:8 129 - ./in.cue:47:11 127 + ./in.cue:46:8 128 + ./in.cue:46:11 130 129 -- out/eval/stats -- 131 130 Leaks: 4 132 131 Freed: 74
+2 -2
cue/testdata/eval/notify.txtar
··· 10 10 ) 11 11 } 12 12 t2: { 13 - @test(permute) 13 + @test(permute, count=2) 14 14 d: d.a 15 15 d: a: a: 2|1 16 16 @test(err, code=eval, contains="empty disjunction", ··· 19 19 ) 20 20 } 21 21 t3: { 22 - @test(permute) 22 + @test(permute, count=6) 23 23 b: a 24 24 b: (1|2) | 2 25 25 a: 1 | 2
+5 -5
cue/testdata/eval/required.txtar
··· 11 11 } 12 12 13 13 unify: t1: { 14 - @test(permute) 14 + @test(permute, count=2) 15 15 a!: int 16 16 a: int 17 17 @test(eq, {a: int}) ··· 43 43 44 44 reference: { 45 45 toWithinDefinition: { 46 - @test(permute) 46 + @test(permute, count=2) 47 47 #Foo: a!: int 48 48 b: #Foo.a @test(err, code=incomplete, contains="required field missing: %s", args=[a], pos=[0:11]) 49 49 } 50 50 toFieldFromDefinition: { 51 - @test(permute) 51 + @test(permute, count=6) 52 52 #Foo: a!: int 53 53 b: #Foo 54 54 c: b.a @test(err, code=incomplete, contains="required field missing: %s", args=[a], pos=[0:8]) 55 55 } 56 56 toNonDefinition: { 57 - @test(permute) 57 + @test(permute, count=2) 58 58 x: y!: _ 59 59 b: x.y @test(err, code=incomplete, contains="required field missing: %s", args=[y], pos=[0:8]) 60 60 } 61 61 toConcrete: { 62 - @test(permute) 62 + @test(permute, count=2) 63 63 a!: 1 64 64 b: a + 1 @test(err, code=incomplete, contains="required field missing: %s", args=[a], pos=[0:6]) 65 65 }
+5 -9
cue/testdata/inlinetest/permute.txtar
··· 6 6 // ── Container-level @test(permute): permutes all fields of `in` ── 7 7 8 8 threeFieldPermute: { 9 - @test(permute) 9 + @test(permute, count=6) 10 10 a: b + c 11 11 b: 1 12 12 c: 2 ··· 15 15 b: 1 16 16 c: 2 17 17 }) 18 - @test(permuteCount, 6) // 3 fields = 3! = 6 permutations 19 18 } 20 19 21 20 twoFieldPermute: { 22 - @test(permute) 21 + @test(permute, count=2) 23 22 x: y + 1 24 23 y: 5 25 24 @test(eq, { 26 25 x: 6 27 26 y: 5 28 27 }) 29 - @test(permuteCount, 2) // 2 fields = 2! = 2 permutations 30 28 } 31 29 32 30 // ── Decl attr on `in` directly: equivalent syntax ── 33 31 34 32 inDeclPermute: { 35 - @test(permute) 33 + @test(permute, count=6) 36 34 p: q + r 37 35 q: 3 38 36 r: 7 ··· 41 39 q: 3 42 40 r: 7 43 41 }) 44 - @test(permuteCount, 6) // 3 fields = 3! = 6 permutations 45 42 } 46 43 47 44 // ── Self-contained concrete struct: trivially order-invariant ── 48 45 49 46 concretePermute: { 50 47 x: { 51 - @test(permute) 48 + @test(permute, count=6) 52 49 alpha: 1 53 50 beta: 2 54 51 gamma: 3 55 52 } 56 53 y: { 57 - @test(permute) 54 + @test(permute, count=6) 58 55 alpha: 1 59 56 beta: 2 60 57 gamma: 3 ··· 71 68 gamma: 3 72 69 } 73 70 }) 74 - @test(permuteCount, 12) // 2 structs * 3! = 12 permutations 75 71 } 76 72 -- out/compile -- 77 73 --- in.cue
+6 -30
cue/testdata/readme.md
··· 303 303 x: y + 1 @test(permute) 304 304 y: 2 @test(permute) 305 305 @test(eq, {x: 3, y: 2}) 306 - @test(permuteCount, 2) 307 306 } 308 307 309 308 // Decl attribute form: permute all fields in the struct. 309 + // The optional count=N option asserts the number of permutations (N!). 310 + // count= (empty) is a fill-in placeholder auto-updated by CUE_UPDATE=1. 310 311 permuteStruct: { 311 - @test(permute) 312 + @test(permute, count=6) 312 313 a: b + c 313 314 b: 1 314 315 c: 2 315 316 @test(eq, {a: 3, b: 1, c: 2}) 316 - @test(permuteCount, 6) 317 - } 318 - ``` 319 - 320 - ### `permuteCount` — verify permutation count 321 - 322 - ```cue 323 - permuteStruct: { 324 - a: b + c @test(permute) 325 - b: 1 @test(permute) 326 - c: 2 @test(permute) 327 - @test(eq, {a: 3, b: 1, c: 2}) 328 - @test(permuteCount, 6) 329 317 } 330 318 ``` 331 319 332 - Placed alongside `@test(permute)` in the same struct, asserts the count for 333 - that permutation group. May also be placed at the test-root level to assert 334 - the **total** count across all groups within the root: 335 - 336 - ```cue 337 - concretePermute: { 338 - x: { @test(permute); alpha: 1, beta: 2, gamma: 3 } 339 - y: { @test(permute); alpha: 1, beta: 2, gamma: 3 } 340 - @test(eq, {x: {alpha: 1, beta: 2, gamma: 3}, y: {alpha: 1, beta: 2, gamma: 3}}) 341 - @test(permuteCount, 12) // 2 structs × 3! = 12 342 - } 343 - ``` 344 - 345 - `@test(permuteCount)` (no argument) is a fill-in placeholder. Auto-updated 346 - by `CUE_UPDATE=1`; `CUE_UPDATE=force` overwrites an existing non-empty count. 320 + The `count=N` option is supported only in the **decl attribute form** (where 321 + `@test(permute)` is a standalone declaration inside the struct, not a field 322 + attribute). `CUE_UPDATE=1` inserts or updates the count automatically. 347 323 348 324 ### `todo` — expected-to-fail wrapper 349 325
+1 -2
cue/testdata/scalars/embed.txtar
··· 139 139 // regardless of which field is declared first. The @test(permute) exercises all 140 140 // declaration orders to confirm order-independence. 141 141 merged: t1: { 142 - @test(permute) 143 - @test(permuteCount, 6) 142 + @test(permute, count=6) 144 143 145 144 #fn: { #in2: _, _ } 146 145 #fn: { #in2: c1: string, #in2.c1 }
+10 -10
doc/specs/inline-test-attributes/spec.md
··· 701 701 702 702 --- 703 703 704 - ### Requirement: `permuteCount` directive 705 - The `permuteCount` directive asserts the total number of permutations that were executed for the test case root. It is placed alongside `@test(permute)` (either in the same struct as a decl attr, or on the parent struct). When `CUE_UPDATE=1` is set, the count is auto-filled if empty or replaced if it differs. 704 + ### Requirement: `permute` with `count=` option 705 + The `count=N` option on a decl-form `@test(permute)` asserts the number of permutations executed for that group. When `CUE_UPDATE=1` is set, the count is auto-filled if empty (`count=`) or replaced if it differs. 706 706 707 707 ```cue 708 708 in: { 709 - @test(permute) 709 + @test(permute, count=2) 710 710 a: 1, b: a+1 711 - @test(permuteCount, 2) 712 711 } 713 712 ``` 714 713 715 - For multiple permutation groups within one test root, `@test(permuteCount, N)` at the root asserts the total across all groups: 714 + For multiple permutation groups, each struct has its own `count=N`: 716 715 717 716 ```cue 718 717 multiPermute: { 719 - x: { @test(permute); alpha: 1, beta: 2, gamma: 3 } 720 - y: { @test(permute); alpha: 1, beta: 2, gamma: 3 } 721 - @test(permuteCount, 12) // 2 groups × 3! = 12 total 718 + x: { @test(permute, count=6); alpha: 1, beta: 2, gamma: 3 } 719 + y: { @test(permute, count=6); alpha: 1, beta: 2, gamma: 3 } 722 720 } 723 721 ``` 724 722 723 + The `count=` option is only supported in the **decl attribute form** (where `@test(permute)` is a standalone declaration inside the struct). It is not available in the field attribute form. 724 + 725 725 #### Scenario: Permutation count matches 726 - - **WHEN** `@test(permuteCount, 6)` is declared and exactly 6 permutations ran 726 + - **WHEN** `@test(permute, count=6)` is declared and exactly 6 permutations ran 727 727 - **THEN** the test passes 728 728 729 729 #### Scenario: Permutation count mismatch 730 - - **WHEN** `@test(permuteCount, 6)` is declared but only 2 permutations ran 730 + - **WHEN** `@test(permute, count=6)` is declared but only 2 permutations ran 731 731 - **THEN** the test fails reporting the count mismatch 732 732 733 733 ---
-2
internal/cuetxtar/inline.go
··· 788 788 // @test(todo) is handled at the loop level in runInline; no-op here. 789 789 case "permute": 790 790 // Handled by permute-group collection in runInline; no-op here. 791 - case "permuteCount": 792 - // Handled by checkPermuteCount after permutations run; no-op here. 793 791 case "shareID": 794 792 // @test(shareID=name) annotations appear on fields within @test(eq, {...}) 795 793 // bodies; sharing is verified by runShareIDChecks in runInline — no-op here.
+1 -1
internal/cuetxtar/inline_attr.go
··· 175 175 } 176 176 default: 177 177 switch result.directive { 178 - case "err", "todo", "skip", "shareID": 178 + case "err", "todo", "skip", "shareID", "permute": 179 179 // These directives parse their own flags elsewhere. 180 180 default: 181 181 return result, fmt.Errorf("@test(%s): unknown flag %q", result.directive, kv.Key())
+78 -56
internal/cuetxtar/permute.go
··· 35 35 // 1. Field attribute: @test(permute) on individual fields collects those fields 36 36 // into a group under their parent struct. Only the marked fields are permuted. 37 37 // 38 - // 2. Decl attribute: @test(permute) as a declaration inside a struct means 39 - // "permute all fields within this struct." 38 + // 2. Decl attribute: @test(permute[, count=N]) as a declaration inside a struct 39 + // means "permute all fields within this struct." The optional count=N asserts 40 + // the number of permutations (N!); count= (empty) is a fill-in placeholder 41 + // that CUE_UPDATE=1 fills automatically. 40 42 func (r *inlineRunner) runInlinePermutes(t testing.TB, rootPath cue.Path, records []attrRecord, version string) { 41 43 type permuteGroup struct { 42 44 parentPath cue.Path 43 - fields []string // nil means permute all fields 45 + fields []string // nil means permute all fields 46 + permutePA parsedTestAttr // permute directive (decl form); used for count 47 + hasPA bool // true when permutePA is set (decl form) 44 48 } 45 49 var permuteGroups []permuteGroup 46 50 parentSeen := map[string]int{} // parentPath.String() → index in permuteGroups ··· 54 58 continue 55 59 } 56 60 if rec.isDeclAttr { 57 - // Decl form: @test(permute) inside a struct → permute all 61 + // Decl form: @test(permute[, count=N]) inside a struct → permute all 58 62 // fields in that struct. The path points to the struct itself. 59 63 key := rec.path.String() 60 64 if _, ok := parentSeen[key]; !ok { 61 65 parentSeen[key] = len(permuteGroups) 62 - permuteGroups = append(permuteGroups, permuteGroup{rec.path, nil}) 66 + permuteGroups = append(permuteGroups, permuteGroup{ 67 + parentPath: rec.path, 68 + permutePA: pa, 69 + hasPA: true, 70 + }) 63 71 } 64 72 continue 65 73 } 66 74 67 75 // Field form: @test(permute) on a field → collect into parent group. 76 + // The first field in the group holds the count= option. 68 77 sels := rec.path.Selectors() 69 78 if len(sels) < 2 { 70 79 continue // top-level field permute not supported in inline form ··· 76 85 permuteGroups[idx].fields = append(permuteGroups[idx].fields, fieldName) 77 86 } else { 78 87 parentSeen[key] = len(permuteGroups) 79 - permuteGroups = append(permuteGroups, permuteGroup{parentPath, []string{fieldName}}) 88 + permuteGroups = append(permuteGroups, permuteGroup{ 89 + parentPath: parentPath, 90 + fields: []string{fieldName}, 91 + permutePA: pa, // first field holds the count= option 92 + hasPA: true, 93 + }) 80 94 } 81 95 } 82 96 } 83 97 84 - totalPerms := 0 85 98 for _, group := range permuteGroups { 86 99 groupPerms := r.runPermuteAssertion(t, group.parentPath, group.fields) 87 - // @test(permuteCount, N) placed alongside @test(permute) in the same 88 - // struct is checked here with the per-group count. 89 - if groupPerms > 0 { 90 - r.checkPermuteCount(t, group.parentPath, records, version, groupPerms) 91 - } 92 - totalPerms += groupPerms 93 - } 94 - // Also check @test(permuteCount, N) at the root with the total across all 95 - // groups — but only when the root path is not itself one of the group paths 96 - // (to avoid double-checking single-group cases where the permuted struct IS 97 - // the root). 98 - if totalPerms > 0 { 99 - rootStr := rootPath.String() 100 - rootIsGroup := false 101 - for _, group := range permuteGroups { 102 - if group.parentPath.String() == rootStr { 103 - rootIsGroup = true 104 - break 105 - } 106 - } 107 - if !rootIsGroup { 108 - r.checkPermuteCount(t, rootPath, records, version, totalPerms) 100 + if groupPerms > 0 && group.hasPA { 101 + r.checkPermuteCount(t, group.parentPath, group.permutePA, groupPerms) 109 102 } 110 103 } 111 104 } 112 105 113 - // checkPermuteCount verifies or auto-updates a @test(permuteCount, N) directive 114 - // at path after all permutations for a group have run. When CUE_UPDATE=1, the 115 - // count is filled or replaced with the actual value. 116 - func (r *inlineRunner) checkPermuteCount(t testing.TB, path cue.Path, records []attrRecord, version string, actualCount int) { 106 + // checkPermuteCount verifies or auto-updates the count= option inside a 107 + // @test(permute, count=N) directive after all permutations for a group have 108 + // run. When CUE_UPDATE=1, the count is filled or replaced with the actual value. 109 + func (r *inlineRunner) checkPermuteCount(t testing.TB, path cue.Path, pa parsedTestAttr, actualCount int) { 117 110 t.Helper() 118 - directives := selectActiveDirectives(records, path, version) 119 - for _, pa := range directives { 120 - if pa.directive != "permuteCount" { 121 - continue 122 - } 123 - if len(pa.raw.Fields) < 2 { 124 - // Bare @test(permuteCount) — fill with actual count. 125 - if cuetest.UpdateGoldenFiles { 126 - r.enqueueInlineFill(pa, fmt.Sprintf("@test(permuteCount, %d)", actualCount)) 127 - } 128 - return 111 + // Find the count= option. 112 + countVal := "" 113 + hasCount := false 114 + for _, f := range pa.raw.Fields[1:] { 115 + if f.Key() == "count" { 116 + hasCount = true 117 + countVal = f.Value() 118 + break 129 119 } 130 - expectedStr := pa.raw.Fields[1].Value() 131 - expected, err := strconv.Atoi(expectedStr) 132 - if err != nil { 133 - t.Errorf("path %s: @test(permuteCount, %q): cannot parse as integer", path, expectedStr) 134 - return 120 + } 121 + if !hasCount { 122 + // No count= option yet — add it when updating. 123 + if cuetest.UpdateGoldenFiles { 124 + r.enqueueInlineFill(pa, buildPermuteAttr(pa, actualCount)) 135 125 } 136 - if expected == actualCount { 137 - return // matches 126 + return 127 + } 128 + if countVal == "" { 129 + // Empty count= placeholder — fill with actual count. 130 + if cuetest.UpdateGoldenFiles { 131 + r.enqueueInlineFill(pa, buildPermuteAttr(pa, actualCount)) 138 132 } 139 - if cuetest.UpdateGoldenFiles || cuetest.ForceUpdateGoldenFiles { 140 - r.enqueueInlineFill(pa, fmt.Sprintf("@test(permuteCount, %d)", actualCount)) 141 - return 133 + return 134 + } 135 + expected, err := strconv.Atoi(countVal) 136 + if err != nil { 137 + t.Errorf("path %s: @test(permute, count=%q): cannot parse as integer", path, countVal) 138 + return 139 + } 140 + if expected == actualCount { 141 + return // matches 142 + } 143 + if cuetest.UpdateGoldenFiles || cuetest.ForceUpdateGoldenFiles { 144 + r.enqueueInlineFill(pa, buildPermuteAttr(pa, actualCount)) 145 + return 146 + } 147 + t.Errorf("path %s: @test(permute, count=...): got %d permutations, want %d", path, actualCount, expected) 148 + } 149 + 150 + // buildPermuteAttr constructs the replacement text for a @test(permute, count=N) 151 + // directive, preserving any version suffix and non-count options. 152 + func buildPermuteAttr(pa parsedTestAttr, count int) string { 153 + directive := "permute" 154 + if pa.version != "" { 155 + directive += ":" + pa.version 156 + } 157 + var extras []string 158 + for _, f := range pa.raw.Fields[1:] { 159 + if f.Key() == "count" { 160 + continue // replaced below 142 161 } 143 - t.Errorf("path %s: @test(permuteCount): got %d permutations, want %d", path, actualCount, expected) 144 - return 162 + extras = append(extras, f.Text()) 145 163 } 164 + if len(extras) > 0 { 165 + return fmt.Sprintf("@test(%s, %s, count=%d)", directive, strings.Join(extras, ", "), count) 166 + } 167 + return fmt.Sprintf("@test(%s, count=%d)", directive, count) 146 168 } 147 169 148 170 // runPermuteAssertion evaluates all N! field-order permutations of the struct
+5 -13
internal/cuetxtar/testdata/inline/permute.txtar
··· 9 9 a: b + 1 @test(permute) 10 10 b: 2 @test(permute) 11 11 c: 99 12 - @test(permuteCount) 13 12 } @test(eq, {a: 3, b: 2, c: 99}) 14 13 15 14 // Single permute field: nothing to permute, silently skipped. 16 15 single: { 17 16 a: 1 @test(permute) 18 17 b: 2 19 - @test(permuteCount) // NOTE: Permute count not set because it skips. 20 18 } @test(eq, {a: 1, b: 2}) 21 19 22 20 all: { 23 - @test(permute) 21 + @test(permute, count=) 24 22 a: 1 25 23 b: 2 26 24 c: 3 27 25 d: 4 28 - @test(permuteCount) 29 26 @test(eq, {a: 1, b: 2, c: 3, d: 4}) 30 27 } 31 28 ··· 35 32 // a negative case for permute. 36 33 toposort: { 37 34 src: { 38 - @test(permute) 39 - @test(permuteCount) 35 + @test(permute, count=) 40 36 a: 1 41 37 b: 2 42 38 } ··· 47 43 -- out/update/test.cue -- 48 44 // Two-field permute: a=b+1 and b=2 are permuted; c=99 is not. 49 45 myStruct: { 50 - a: b + 1 @test(permute) 46 + a: b + 1 @test(permute, count=2) 51 47 b: 2 @test(permute) 52 48 c: 99 53 - @test(permuteCount, 2) 54 49 } @test(eq, {a: 3, b: 2, c: 99}) 55 50 56 51 // Single permute field: nothing to permute, silently skipped. 57 52 single: { 58 53 a: 1 @test(permute) 59 54 b: 2 60 - @test(permuteCount) // NOTE: Permute count not set because it skips. 61 55 } @test(eq, {a: 1, b: 2}) 62 56 63 57 all: { 64 - @test(permute) 58 + @test(permute, count=24) 65 59 a: 1 66 60 b: 2 67 61 c: 3 68 62 d: 4 69 - @test(permuteCount, 24) 70 63 @test(eq, {a: 1, b: 2, c: 3, d: 4}) 71 64 } 72 65 ··· 76 69 // a negative case for permute. 77 70 toposort: { 78 71 src: { 79 - @test(permute) 80 - @test(permuteCount, 2) 72 + @test(permute, count=2) 81 73 a: 1 82 74 b: 2 83 75 }