this repo has no description
0
fork

Configure Feed

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

internal/tdtest: new table-driven test package

NOTE: tests are not yet implemented. This package is
tested by writing actual tests. If this ever were to be a public package.

Also note the TODOs at the top of tdtest.go

Signed-off-by: Marcel van Lohuizen <mpvl@gmail.com>
Change-Id: I23c13bb5d1bbe8d3fb1f43f0ca459878d4a818b6
Reviewed-on: https://review.gerrithub.io/c/cue-lang/cue/+/552155
Reviewed-by: Roger Peppe <rogpeppe@gmail.com>
Unity-Result: CUEcueckoo <cueckoo@cuelang.org>
TryBot-Result: CUEcueckoo <cueckoo@cuelang.org>

+633
+14
internal/cuetest/cuetest.go
··· 21 21 "os" 22 22 "regexp" 23 23 "testing" 24 + 25 + "cuelang.org/go/internal/tdtest" 24 26 ) 25 27 26 28 const ( ··· 93 95 return Long, nil 94 96 } 95 97 return false, fmt.Errorf("unknown condition %v", cond) 98 + } 99 + 100 + // T is an alias to tdtest.T 101 + type T = tdtest.T 102 + 103 + func init() { 104 + tdtest.UpdateTests = UpdateGoldenFiles 105 + } 106 + 107 + // Run creates a new table-driven test using the CUE testing defaults. 108 + func Run[TC any](t *testing.T, table []TC, fn func(t *T, tc *TC)) { 109 + tdtest.Run(t, table, fn) 96 110 } 97 111 98 112 // IssueSkip causes the test t to be skipped unless the issue identified
+175
internal/tdtest/tdtest.go
··· 1 + // Copyright 2023 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 tdtest provides support for table-driven testing. 16 + // 17 + // Features include automatically updating of test values, automatic error 18 + // message generation, and singling out single tests to run. 19 + // 20 + // Auto updating fields is only supported for fields that are scalar types: 21 + // string, bool, int*, and uint*. If the field is a string, the "actual" value 22 + // may be any Go value that can meaningfully be printed with fmt.Sprint. 23 + package tdtest 24 + 25 + import ( 26 + "fmt" 27 + "go/token" 28 + "reflect" 29 + "runtime" 30 + "strings" 31 + "testing" 32 + ) 33 + 34 + // TODO: 35 + // - make this a public package at some point. 36 + // - add tests. Maybe adding Examples is sufficient. 37 + // - use text-based modification, instead of astutil. The latter is too brittle. 38 + // - allow updating position-based, instead of named, fields. 39 + // - implement skip, maybe match 40 + // - make name field explicit, i.e. Name("name"), field tag, or tdtest.Name type. 41 + // - allow "skip" field. Again either SkipName("skip"), tag, or Skip type. 42 + // - allow for tdtest:"noupdate" field tag. 43 + // - should we derive names from field names? This would require always 44 + // loading the packages data upon error. Could be an option to disable, or 45 + // implicitly it would only be loaded if there is an error without message. 46 + // - Option: allow ignore field that lists a set of fields to not be tested 47 + // for that particular test case: ignore: tdtest.Ignore("want1", "want2") 48 + // 49 + 50 + // UpdateTests defines whether tests should be updated by default. 51 + // This can be overridden on an individual basis using T.Update. 52 + var UpdateTests = false 53 + 54 + // set is the set of tests to run. 55 + type set[TC any] struct { 56 + t *testing.T 57 + 58 + table []TC 59 + toRun []int 60 + 61 + updateEnabled bool 62 + file string 63 + info *info 64 + } 65 + 66 + // Run runs the given function for each (selected) element in the table. 67 + func Run[TC any](t *testing.T, table []TC, fn func(t *T, tc *TC)) { 68 + s := &set[TC]{ 69 + t: t, 70 + table: table, 71 + updateEnabled: UpdateTests, 72 + } 73 + for i := range s.table { 74 + name := fmt.Sprint(i) 75 + 76 + x := reflect.ValueOf(s.table[i]).FieldByName("name") 77 + if x.Kind() == reflect.String { 78 + name += "/" + x.String() 79 + } 80 + 81 + s.t.Run(name, func(t *testing.T) { 82 + tt := &T{ 83 + T: t, 84 + iter: i, 85 + infoSrc: s, 86 + updateEnabled: s.updateEnabled, 87 + } 88 + fn(tt, &s.table[i]) 89 + }) 90 + } 91 + if s.info != nil && s.info.needsUpdate { 92 + s.update() 93 + } 94 + } 95 + 96 + // T is a single test case representing an element in a table. 97 + // It embeds *testing.T, so all functions of testing.T are available. 98 + type T struct { 99 + *testing.T 100 + 101 + infoSrc interface{ getInfo(file string) *info } 102 + iter int // position in the table of the current subtest. 103 + 104 + updateEnabled bool 105 + } 106 + 107 + func (t *T) info(file string) *info { 108 + return t.infoSrc.getInfo(file) 109 + } 110 + 111 + func (t *T) getCallInfo() (*info, *callInfo) { 112 + _, file, line, ok := runtime.Caller(2) 113 + if !ok { 114 + t.Fatalf("could not update file for test %s", t.Name()) 115 + } 116 + info := t.info(file) 117 + return info, info.calls[token.Position{Filename: file, Line: line}] 118 + } 119 + 120 + // Equal compares two fields. 121 + // 122 + // For auto updating to work, field must reference a field in the test case 123 + // directly. 124 + func (t *T) Equal(actual, field any, msgAndArgs ...any) { 125 + t.Helper() 126 + 127 + switch { 128 + case field == actual: 129 + case t.updateEnabled: 130 + info, ci := t.getCallInfo() 131 + t.updateField(info, ci, actual) 132 + case len(msgAndArgs) == 0: 133 + t.Errorf("unexpected value:\ngot: %v;\nwant: %v", actual, field) 134 + default: 135 + format := msgAndArgs[0].(string) + ":\ngot: %v;\nwant: %v" 136 + args := append(msgAndArgs[1:], actual, field) 137 + t.Errorf(format, args...) 138 + } 139 + } 140 + 141 + // Update specifies whether to update the Go structs in case of discrepancies. 142 + // It overrides the default setting. 143 + func (t *T) Update(enable bool) { 144 + t.updateEnabled = enable 145 + } 146 + 147 + // Select species which tests to run. The test may be an int, in which case 148 + // it selects the table entry to run, or a string, which is matched against 149 + // the last path of the test. An empty list runs all tests. 150 + func (t *T) Select(tests ...any) { 151 + if len(tests) == 0 { 152 + return 153 + } 154 + 155 + t.Helper() 156 + 157 + name := t.Name() 158 + parts := strings.Split(name, "/") 159 + 160 + for _, n := range tests { 161 + switch n := n.(type) { 162 + case int: 163 + if n == t.iter { 164 + return 165 + } 166 + case string: 167 + if n == parts[len(parts)-1] { 168 + return 169 + } 170 + default: 171 + panic("unexpected type passed to Select") 172 + } 173 + } 174 + t.Skip("not selected") 175 + }
+42
internal/tdtest/tdtest_test.go
··· 1 + // Copyright 2023 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 tdtest_test 16 + 17 + import ( 18 + "testing" 19 + 20 + "cuelang.org/go/internal/tdtest" 21 + ) 22 + 23 + // TODO: write a proper test 24 + 25 + // NOTE: for debugging purposes. Do not remove. 26 + func TestX(t *testing.T) { 27 + t.Skip() 28 + 29 + type testCase struct { 30 + name string 31 + want string 32 + } 33 + _, cases := 1, []testCase{{ 34 + name: "foo", 35 + want: `foo`, 36 + }} 37 + 38 + tdtest.Run(t, cases, func(t *tdtest.T, tc *testCase) { 39 + t.Update(true) 40 + t.Equal("actual", tc.want) 41 + }) 42 + }
+402
internal/tdtest/update.go
··· 1 + // Copyright 2023 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 tdtest 16 + 17 + import ( 18 + "fmt" 19 + "go/ast" 20 + "go/format" 21 + "go/token" 22 + "go/types" 23 + "os" 24 + "reflect" 25 + "strconv" 26 + "strings" 27 + "sync" 28 + "testing" 29 + 30 + "golang.org/x/tools/go/ast/astutil" 31 + "golang.org/x/tools/go/packages" 32 + ) 33 + 34 + // info contains information needed to update files. 35 + type info struct { 36 + t *testing.T 37 + 38 + tcType reflect.Type 39 + 40 + needsUpdate bool // an updateable field has changed 41 + 42 + table *ast.CompositeLit // the table that is the source of the tests 43 + 44 + testPkg *packages.Package 45 + 46 + calls map[token.Position]*callInfo 47 + patches map[ast.Node]ast.Expr 48 + } 49 + 50 + type callInfo struct { 51 + ast *ast.CallExpr 52 + funcName string 53 + fieldName string 54 + } 55 + 56 + var ( 57 + once sync.Once 58 + pkgs []*packages.Package 59 + pkgsErr error 60 + ) 61 + 62 + func initPackages() ([]*packages.Package, error) { 63 + once.Do(func() { 64 + cfg := &packages.Config{ 65 + Mode: packages.NeedFiles | 66 + packages.NeedDeps | 67 + packages.NeedTypes | 68 + packages.NeedTypesInfo | 69 + packages.NeedSyntax, 70 + Tests: true, 71 + } 72 + 73 + pkgs, pkgsErr = packages.Load(cfg, ".") 74 + }) 75 + return pkgs, pkgsErr 76 + } 77 + 78 + func (s *set[T]) getInfo(file string) *info { 79 + if s.info != nil { 80 + return s.info 81 + } 82 + info := &info{ 83 + t: s.t, 84 + tcType: reflect.TypeOf(new(T)).Elem(), 85 + calls: make(map[token.Position]*callInfo), 86 + patches: make(map[ast.Node]ast.Expr), 87 + } 88 + s.info = info 89 + 90 + t := s.t 91 + 92 + pkgs, pkgsErr = initPackages() 93 + if pkgsErr != nil { 94 + t.Fatalf("load: %v\n", pkgsErr) 95 + } 96 + 97 + // Get package under test. 98 + f, pkg := findFileAndPackage(file, pkgs) 99 + if f == nil { 100 + t.Fatalf("failed to load package for file %s", file) 101 + } 102 + info.testPkg = pkg 103 + 104 + // TODO: not necessary at the moment, but this is tricky so leaving this in 105 + // so as to not to forget how to do it. 106 + // 107 + // for _, p := range pkg.Types.Imports() { 108 + // if p.Path() == "cuelang.org/go/internal/tdtest" { 109 + // info.thisPkg = p 110 + // } 111 + // } 112 + // if info.thisPkg == nil { 113 + // t.Fatalf("could not find test package") 114 + // } 115 + 116 + // Find function declaration of this test. 117 + var fn *ast.FuncDecl 118 + for _, d := range f.Decls { 119 + if fd, ok := d.(*ast.FuncDecl); ok && fd.Name.Name == t.Name() { 120 + fn = fd 121 + } 122 + } 123 + if fn == nil { 124 + t.Fatalf("could not find test %q in file %q", t.Name(), file) 125 + } 126 + 127 + // Find CompositLit table used for the test: 128 + // - find call to which CompositLit was passed, 129 + a := info.findCalls(fn.Body, "New", "Run") 130 + if len(a) != 1 { 131 + // TODO: allow more than one. 132 + t.Fatalf("only one Run or New function allowed per test") 133 + } 134 + 135 + // - analyse second argument of call, 136 + call := a[0].ast 137 + fset := info.testPkg.Fset 138 + ti := info.testPkg.TypesInfo 139 + ident, ok := call.Args[1].(*ast.Ident) 140 + if !ok { 141 + t.Fatalf("%v: arg 2 of %s must be a reference to the table", 142 + fset.Position(call.Args[1].Pos()), a[0].funcName) 143 + } 144 + def := ti.Uses[ident] 145 + pos := def.Pos() 146 + 147 + // - locate the CompositLit in the AST based on position. 148 + v, ok := findVar(pos, f).(*ast.CompositeLit) 149 + if !ok { 150 + // generics should avoid this. 151 + t.Fatalf("expected composite literal, found %T", v) 152 + } 153 + info.table = v 154 + 155 + // Find and index assertion calls. 156 + a = info.findCalls(fn.Body, "Equal") 157 + for _, x := range a { 158 + info.initFieldRef(x, f) 159 + } 160 + 161 + return info 162 + } 163 + 164 + // initFieldRef updates c with information about the field referenced 165 + // in its corresponding call: 166 + // - name of the field 167 + // - indexes the field based on filename and line number. 168 + func (i *info) initFieldRef(c *callInfo, f *ast.File) { 169 + call := c.ast 170 + t := i.t 171 + info := i.testPkg.TypesInfo 172 + fset := i.testPkg.Fset 173 + pos := fset.Position(call.Pos()) 174 + 175 + sel, ok := call.Args[1].(*ast.SelectorExpr) 176 + s := info.Selections[sel] 177 + if !ok || s == nil || s.Kind() != types.FieldVal { 178 + t.Fatalf("%v: arg 2 of %s must be a reference to a test case field", 179 + fset.Position(call.Args[1].Pos()), c.funcName) 180 + } 181 + 182 + obj := s.Obj() 183 + c.fieldName = obj.Name() 184 + if _, ok := i.tcType.FieldByName(c.fieldName); !ok { 185 + t.Fatalf("%v: could not find field %s", 186 + fset.Position(obj.Pos()), c.fieldName) 187 + } 188 + 189 + pos.Column = 0 190 + pos.Offset = 0 191 + i.calls[pos] = c 192 + } 193 + 194 + // findFileAndPackage locates the ast.File and package within the given slice 195 + // of packages, in which the given file is located. 196 + func findFileAndPackage(path string, pkgs []*packages.Package) (*ast.File, *packages.Package) { 197 + for _, p := range pkgs { 198 + for i, gf := range p.GoFiles { 199 + if gf == path { 200 + return p.Syntax[i], p 201 + } 202 + } 203 + } 204 + return nil, nil 205 + } 206 + 207 + const ( 208 + typeT = "*cuelang.org/go/internal/tdtest.T" 209 + tdtestParen = `("cuelang.org/go/internal/tdtest")` 210 + ) 211 + 212 + // findCalls finds all call expressions within a given block for functions 213 + // or methods defined within the tdtest package. 214 + func (i *info) findCalls(block *ast.BlockStmt, names ...string) []*callInfo { 215 + var a []*callInfo 216 + ast.Inspect(block, func(n ast.Node) bool { 217 + c, ok := n.(*ast.CallExpr) 218 + if !ok { 219 + return true 220 + } 221 + sel, ok := c.Fun.(*ast.SelectorExpr) 222 + if !ok { 223 + return true 224 + } 225 + 226 + // TODO: also test package. It would be better to test the equality 227 + // using the information in the types.Info/packages to ensure that 228 + // we really got the right function. 229 + info := i.testPkg.TypesInfo 230 + for _, name := range names { 231 + if sel.Sel.Name == name { 232 + if info.TypeOf(sel.X).String() == typeT { 233 + } else if ident, ok := sel.X.(*ast.Ident); !ok { 234 + return true // Run method. 235 + } else if id, ok := info.Uses[ident].(*types.PkgName); ok && strings.Contains(id.String(), tdtestParen) { 236 + } else { 237 + return true 238 + } 239 + ci := &callInfo{ 240 + funcName: name, 241 + ast: c, 242 + } 243 + a = append(a, ci) 244 + return true 245 + } 246 + } 247 + 248 + return true 249 + }) 250 + return a 251 + } 252 + 253 + func findVar(pos token.Pos, n ast.Node) (ret ast.Expr) { 254 + ast.Inspect(n, func(n ast.Node) bool { 255 + if as, ok := n.(*ast.AssignStmt); ok { 256 + for i, v := range as.Lhs { 257 + if v.Pos() == pos { 258 + ret = as.Rhs[i] 259 + } 260 + } 261 + return false 262 + } 263 + return true 264 + }) 265 + return ret 266 + } 267 + 268 + func (s *set[TC]) update() { 269 + info := s.info 270 + 271 + t := s.t 272 + fset := info.testPkg.Fset 273 + 274 + file := fset.Position(info.table.Pos()).Filename 275 + var f *ast.File 276 + for i, gof := range info.testPkg.GoFiles { 277 + if gof == file { 278 + f = info.testPkg.Syntax[i] 279 + } 280 + } 281 + if f == nil { 282 + t.Fatalf("file %s not in package", file) 283 + } 284 + 285 + // TODO: use text-based insertion instead: 286 + // - sort insertions and replacements on position in descending order. 287 + // - substitute textually. 288 + // 289 + // We are using Apply because this is supposed to give better handling of 290 + // comments. In practice this only works marginally better than not handling 291 + // positions at all. Probably a lost cause. 292 + astutil.Apply(f, func(c *astutil.Cursor) bool { 293 + n := c.Node() 294 + 295 + switch x := info.patches[n]; x.(type) { 296 + case nil: 297 + case *ast.KeyValueExpr: 298 + for { 299 + c.InsertAfter(x) 300 + x = info.patches[x] 301 + if x == nil { 302 + break 303 + } 304 + } 305 + default: 306 + c.Replace(x) 307 + } 308 + return true 309 + }, nil) 310 + 311 + // TODO: use tmp files? 312 + w, err := os.Create(file) 313 + if err != nil { 314 + t.Fatal(err) 315 + } 316 + defer w.Close() 317 + 318 + err = format.Node(w, fset, f) 319 + if err != nil { 320 + t.Fatal(err) 321 + } 322 + } 323 + 324 + func (t *T) updateField(info *info, ci *callInfo, newValue any) { 325 + info.needsUpdate = true 326 + 327 + fset := info.testPkg.Fset 328 + 329 + e, ok := info.table.Elts[t.iter].(*ast.CompositeLit) 330 + if !ok { 331 + t.Fatalf("not a composite literal") 332 + } 333 + 334 + isZero := false 335 + var value ast.Expr 336 + switch x := reflect.ValueOf(newValue); x.Kind() { 337 + default: 338 + s := fmt.Sprint(x) 339 + x = reflect.ValueOf(s) 340 + fallthrough 341 + case reflect.String: 342 + s := x.String() 343 + isZero = s == "" 344 + if !strings.ContainsRune(s, '`') && !isZero { 345 + s = fmt.Sprintf("`%s`", s) 346 + } else { 347 + s = strconv.Quote(s) 348 + } 349 + value = &ast.BasicLit{Kind: token.STRING, Value: s} 350 + case reflect.Bool: 351 + if b := x.Bool(); b { 352 + value = &ast.BasicLit{Kind: token.IDENT, Value: "true"} 353 + } else { 354 + value = &ast.BasicLit{Kind: token.IDENT, Value: "false"} 355 + isZero = true 356 + } 357 + case reflect.Int, reflect.Int64, reflect.Int32, reflect.Int8: 358 + i := x.Int() 359 + value = &ast.BasicLit{Kind: token.INT, 360 + Value: strconv.FormatInt(i, 10)} 361 + isZero = i == 0 362 + case reflect.Uint, reflect.Uint64, reflect.Uint32, reflect.Uint8: 363 + i := x.Uint() 364 + value = &ast.BasicLit{Kind: token.INT, 365 + Value: strconv.FormatUint(i, 10)} 366 + isZero = i == 0 367 + } 368 + 369 + for _, x := range e.Elts { 370 + kv, ok := x.(*ast.KeyValueExpr) 371 + if !ok { 372 + t.Fatalf("%v: elements must be key value pairs", 373 + fset.Position(kv.Pos())) 374 + } 375 + ident, ok := kv.Key.(*ast.Ident) 376 + if !ok { 377 + t.Fatalf("%v: key must be an identifier", 378 + fset.Position(kv.Pos())) 379 + } 380 + if ident.Name == ci.fieldName { 381 + info.patches[kv.Value] = value 382 + return 383 + } 384 + } 385 + 386 + if !isZero { 387 + kv := &ast.KeyValueExpr{ 388 + Key: &ast.Ident{Name: ci.fieldName}, 389 + Value: value, 390 + } 391 + if len(e.Elts) > 0 { 392 + var key ast.Node = e.Elts[len(e.Elts)-1] 393 + old := info.patches[key] 394 + if old != nil { 395 + info.patches[kv] = old 396 + } 397 + info.patches[key] = kv 398 + } else { 399 + info.patches[e] = &ast.CompositeLit{Elts: []ast.Expr{kv}} 400 + } 401 + } 402 + }