├── .github └── workflows │ └── test.yml ├── LICENSE ├── README.md ├── dataloc ├── README.md ├── dataloc.go ├── dataloc_test.go └── example_test.go ├── go.mod └── go.sum /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | 14 | - name: Set up Go 15 | uses: actions/setup-go@v4 16 | with: 17 | go-version: stable 18 | 19 | - name: Build 20 | run: go build -v ./... 21 | 22 | - name: Test 23 | run: go test -v ./... 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Hironao OTSUBO 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-testutil 2 | 3 | [![Test](https://github.com/motemen/go-testutil/actions/workflows/test.yml/badge.svg)](https://github.com/motemen/go-testutil/actions/workflows/test.yml) 4 | [![PkgGoDev](https://pkg.go.dev/badge/github.com/motemen/go-testutil)](https://pkg.go.dev/github.com/motemen/go-testutil) 5 | 6 | go-testutil is a (going to be) collection of testing libraries. 7 | 8 | * [dataloc](./dataloc): provides functionality to find the source code location of table-driven test cases 9 | -------------------------------------------------------------------------------- /dataloc/README.md: -------------------------------------------------------------------------------- 1 | # dataloc 2 | 3 | [![PkgGoDev](https://pkg.go.dev/badge/github.com/motemen/go-testutil/dataloc)](https://pkg.go.dev/github.com/motemen/go-testutil/dataloc) 4 | 5 | Package dataloc provides functionality to find the source code location of table-driven test cases. 6 | 7 | ## Example 8 | 9 | ~~~go 10 | import ( 11 | "fmt" 12 | 13 | "github.com/motemen/go-testutil/dataloc" 14 | ) 15 | 16 | func Example() { 17 | testcases := []struct { 18 | name string 19 | a, b int 20 | sum int 21 | }{ 22 | { 23 | name: "100+200", 24 | a: 100, 25 | b: 200, 26 | sum: -1, 27 | }, 28 | { 29 | name: "1+1", 30 | a: 1, 31 | b: 1, 32 | sum: 99, 33 | }, 34 | } 35 | 36 | for _, testcase := range testcases { 37 | if expected, got := testcase.sum, testcase.a+testcase.b; got != expected { 38 | fmt.Printf("expected %d but got %d, test case at %s\n", expected, got, dataloc.L(testcase.name)) 39 | } 40 | } 41 | 42 | // Output: 43 | // expected -1 but got 300, test case at example_test.go:15 44 | // expected 99 but got 2, test case at example_test.go:21 45 | } 46 | ~~~ 47 | 48 | -------------------------------------------------------------------------------- /dataloc/dataloc.go: -------------------------------------------------------------------------------- 1 | // Package dataloc provides functionality to find the source code location of 2 | // table-driven test cases. 3 | package dataloc 4 | 5 | import ( 6 | "fmt" 7 | "go/ast" 8 | "go/parser" 9 | "go/token" 10 | "log" 11 | "os" 12 | "path/filepath" 13 | "runtime" 14 | "strconv" 15 | ) 16 | 17 | // L returns the source code location of the test case identified by its name. 18 | // It attempts runtime source code analysis to find the location 19 | // by using the expression passed to dataloc.L(). 20 | // So some restrictions apply: 21 | // - The function must be invoked as "dataloc.L". 22 | // - The argument must be an expression of the form "dataloc.L(testcase.key)" 23 | // , where "testcase" is a variable declared as "for _, testcase := range testcases" 24 | // , and "testcases" is a slice of a struct type 25 | // , whose "key" field is a string which is passsed to L(). 26 | // - or "dataloc.L(key)" 27 | // , where key is a variable declared as "for key, value := range testcases" 28 | // , and "testcases" is a map of string to any type 29 | // , and "key" is the string which is passed to L(). 30 | // 31 | // See Example. 32 | func L(name string) string { 33 | s, _ := loc(name) 34 | return s 35 | } 36 | 37 | func loc(value string) (string, error) { 38 | _, file, line, _ := runtime.Caller(2) 39 | 40 | cwd, err := os.Getwd() 41 | if err != nil { 42 | return "", err 43 | } 44 | file, err = filepath.Rel(cwd, file) 45 | if err != nil { 46 | return "", err 47 | } 48 | 49 | fset := token.NewFileSet() 50 | f, err := parser.ParseFile(fset, file, nil, 0) 51 | if err != nil { 52 | return "", err 53 | } 54 | 55 | // [ t ↦ expr ] for "type t struct{ ... }" 56 | objToTypeDecl := make(map[*ast.Object]ast.Expr) 57 | // [ v ↦ expr ] for "v := ..." 58 | objToVarInit := make(map[*ast.Object]ast.Expr) 59 | // [ v ↦ expr ] for "for k, v := range expr" 60 | objToRangeExprForValue := make(map[*ast.Object]ast.Expr) 61 | // [ k ↦ expr ] for "for k, v := range expr" 62 | objToRangeExprForKey := make(map[*ast.Object]ast.Expr) 63 | 64 | ast.Inspect(f, func(n ast.Node) bool { 65 | if rangeStmt, ok := n.(*ast.RangeStmt); ok { 66 | if ident, ok := rangeStmt.Value.(*ast.Ident); ok { 67 | objToRangeExprForValue[ident.Obj] = rangeStmt.X 68 | } 69 | if ident, ok := rangeStmt.Key.(*ast.Ident); ok { 70 | objToRangeExprForKey[ident.Obj] = rangeStmt.X 71 | } 72 | } else if decl, ok := n.(ast.Decl); ok { 73 | if genDecl, ok := decl.(*ast.GenDecl); ok { 74 | if genDecl.Tok == token.VAR { 75 | for _, spec := range genDecl.Specs { 76 | if valueSpec, ok := spec.(*ast.ValueSpec); ok { 77 | for i, name := range valueSpec.Names { 78 | if i < len(valueSpec.Values)-1 { 79 | objToVarInit[name.Obj] = valueSpec.Values[i] 80 | } 81 | } 82 | } 83 | } 84 | } else if genDecl.Tok == token.TYPE { 85 | for _, spec := range genDecl.Specs { 86 | if typeSpec, ok := spec.(*ast.TypeSpec); ok { 87 | objToTypeDecl[typeSpec.Name.Obj] = typeSpec.Type 88 | } 89 | } 90 | } 91 | } 92 | } else if assignStmt, ok := n.(*ast.AssignStmt); ok { 93 | for i, expr := range assignStmt.Lhs { 94 | if ident, ok := expr.(*ast.Ident); ok { 95 | if len(assignStmt.Lhs) == len(assignStmt.Rhs) { 96 | objToVarInit[ident.Obj] = assignStmt.Rhs[i] 97 | } else if len(assignStmt.Rhs) == 1 { 98 | objToVarInit[ident.Obj] = assignStmt.Rhs[0] 99 | } else { 100 | debugf("unreachable: len(assignStmt.Lhs)=%d, len(assignStmt.Rhs)=%d", len(assignStmt.Lhs), len(assignStmt.Rhs)) 101 | } 102 | } 103 | } 104 | } 105 | 106 | return true 107 | }) 108 | 109 | loc := "(unknown)" 110 | ast.Inspect(f, func(n ast.Node) bool { 111 | if n == nil { 112 | return false 113 | } 114 | 115 | pos := fset.Position(n.Pos()) 116 | if pos.Line != line { 117 | return true 118 | } 119 | 120 | // for example: 121 | // testcases := []struct{}{...} 122 | // for _, testdata := range testcases { 123 | // dataloc.L(testdata.name) 124 | // } 125 | if call, ok := isMethodCall(n, "dataloc", "L"); ok { 126 | arg := call.Args[0] 127 | // ident = testdata, key = name 128 | if ident, key, ok := isSelector(arg); ok { 129 | // expr = testcases 130 | if expr, ok := objToRangeExprForValue[ident.Obj]; ok { 131 | if testcasesIdent, ok := expr.(*ast.Ident); ok { 132 | // testcasesExpr = []struct{}{...} 133 | testcasesExpr := objToVarInit[testcasesIdent.Obj] 134 | node := findTestCaseItem(testcasesExpr, key, value, objToTypeDecl) 135 | if node != nil { 136 | pos := fset.Position(node.Pos()) 137 | loc = fmt.Sprintf("%s:%d", pos.Filename, pos.Line) 138 | return false 139 | } 140 | } 141 | } 142 | } else if ident, ok := arg.(*ast.Ident); ok { 143 | // for k, v := range testcases { 144 | // dataloc.L(k) 145 | // } 146 | if expr, ok := objToRangeExprForKey[ident.Obj]; ok { 147 | if testcasesIdent, ok := expr.(*ast.Ident); ok { 148 | testcasesExpr := objToVarInit[testcasesIdent.Obj] 149 | node := findTestCaseItem(testcasesExpr, ident.Name, value, objToTypeDecl) 150 | if node != nil { 151 | pos := fset.Position(node.Pos()) 152 | loc = fmt.Sprintf("%s:%d", pos.Filename, pos.Line) 153 | return false 154 | } 155 | } 156 | } 157 | } 158 | } 159 | 160 | return true 161 | }) 162 | 163 | return loc, nil 164 | } 165 | 166 | func isMethodCall(n ast.Node, obj, fun string) (*ast.CallExpr, bool) { 167 | if call, ok := n.(*ast.CallExpr); ok { 168 | if ident, name, ok := isSelector(call.Fun); ok { 169 | if ident.Name == obj && name == fun { 170 | return call, true 171 | } 172 | } 173 | } 174 | return nil, false 175 | } 176 | 177 | func isSelector(n ast.Node) (*ast.Ident, string, bool) { 178 | if sel, ok := n.(*ast.SelectorExpr); ok { 179 | if ident, ok := sel.X.(*ast.Ident); ok { 180 | return ident, sel.Sel.Name, true 181 | } 182 | } 183 | return nil, "", false 184 | } 185 | 186 | func findTestCaseItem(init ast.Expr, key, value string, objToTypeDecl map[*ast.Object]ast.Expr) ast.Node { 187 | testcases, ok := init.(*ast.CompositeLit) 188 | if !ok { 189 | return nil 190 | } 191 | 192 | var testcaseType ast.Expr 193 | if t, ok := testcases.Type.(*ast.ArrayType); ok { 194 | testcaseType = t.Elt 195 | if ident, ok := testcaseType.(*ast.Ident); ok { 196 | testcaseType = objToTypeDecl[ident.Obj] 197 | if testcaseType == nil { 198 | logf("could not resolve type of %s", ident.Name) 199 | return nil 200 | } 201 | } 202 | } else if m, ok := testcases.Type.(*ast.MapType); ok { 203 | testcaseType = m 204 | } else { 205 | // testcases should be an array eg. 206 | // testcases := []testcase{ ... } 207 | // or a map eg. 208 | // testcases := map[string]testcase{ ... } 209 | debugf("unexpected testcase type: %#v", testcases.Type) 210 | return nil 211 | } 212 | 213 | for _, testcase := range testcases.Elts { 214 | if kv, ok := testcase.(*ast.KeyValueExpr); ok { 215 | if basic, ok := kv.Key.(*ast.BasicLit); ok { 216 | if isStringLiteral(basic, value) { 217 | return kv 218 | } 219 | } 220 | } 221 | 222 | testcase, ok := testcase.(*ast.CompositeLit) 223 | if !ok { 224 | // testcase should be a struct literal eg. 225 | // { name: "foo", ... } 226 | // or 227 | // { "foo", ... } 228 | continue 229 | } 230 | 231 | for i, field := range testcase.Elts { 232 | if kv, ok := field.(*ast.KeyValueExpr); ok { 233 | // { : , ... } 234 | if ident, ok := kv.Key.(*ast.Ident); ok { 235 | if ident.Name == key { 236 | if isStringLiteral(kv.Value, value) { 237 | return testcase 238 | } 239 | } 240 | } 241 | } else if basic, ok := field.(*ast.BasicLit); ok { 242 | // { , ...} 243 | if findStructFieldIndex(testcaseType, key) == i { 244 | if isStringLiteral(basic, value) { 245 | return testcase 246 | } 247 | } 248 | } 249 | } 250 | } 251 | 252 | return nil 253 | } 254 | 255 | func isStringLiteral(n ast.Expr, s string) bool { 256 | lit, ok := n.(*ast.BasicLit) 257 | if !ok { 258 | return false 259 | } 260 | if lit.Kind != token.STRING { 261 | return false 262 | } 263 | return lit.Value == strconv.Quote(s) 264 | } 265 | 266 | func findStructFieldIndex(t ast.Expr, name string) int { 267 | typ, ok := t.(*ast.StructType) 268 | if !ok { 269 | return -1 270 | } 271 | 272 | for i, field := range typ.Fields.List { 273 | for _, ident := range field.Names { 274 | if ident.Name == name { 275 | return i 276 | } 277 | } 278 | } 279 | 280 | return -1 281 | } 282 | 283 | func logf(format string, args ...interface{}) { 284 | log.Printf(format, args...) 285 | } 286 | 287 | const debug = false 288 | 289 | func debugf(format string, args ...interface{}) { 290 | if debug { 291 | log.Printf("debug: "+format, args...) 292 | } 293 | } 294 | -------------------------------------------------------------------------------- /dataloc/dataloc_test.go: -------------------------------------------------------------------------------- 1 | package dataloc_test 2 | 3 | import ( 4 | "fmt" 5 | "runtime" 6 | "testing" 7 | 8 | // calling by dataloc.L() is important; L() without package name won't work 9 | "github.com/motemen/go-testutil/dataloc" 10 | ) 11 | 12 | var file = "dataloc_test.go" 13 | 14 | var varWithoutRHS string 15 | 16 | func __line__() int { 17 | _, _, line, _ := runtime.Caller(1) 18 | return line 19 | } 20 | 21 | func TestL_caseTypeInsideFunc(t *testing.T) { 22 | type testcaseInsideFunc struct { 23 | name string 24 | line int 25 | } 26 | 27 | tests := []testcaseInsideFunc{ 28 | {name: "keyed", line: __line__()}, 29 | {"unkeyed", __line__()}, 30 | } 31 | 32 | for _, test := range tests { 33 | t.Run(test.name, func(t *testing.T) { 34 | if got, expected := dataloc.L(test.name), fmt.Sprintf("%s:%d", file, test.line); got != expected { 35 | t.Errorf("expected %q, got %q", expected, got) 36 | } 37 | }) 38 | } 39 | } 40 | 41 | type testcaseOutsideFunc struct { 42 | line int 43 | description string 44 | } 45 | 46 | func TestL_caseTypeOutsideFunc(t *testing.T) { 47 | tests := []testcaseOutsideFunc{ 48 | {description: "keyed", line: __line__()}, 49 | {__line__(), "unkeyed"}, 50 | } 51 | 52 | for _, test := range tests { 53 | t.Run(test.description, func(t *testing.T) { 54 | if got, expected := dataloc.L(test.description), fmt.Sprintf("%s:%d", file, test.line); got != expected { 55 | t.Errorf("expected %q, got %q", expected, got) 56 | } 57 | }) 58 | } 59 | } 60 | 61 | func TestL_caseTypeInline(t *testing.T) { 62 | tests := []struct { 63 | name string 64 | line int 65 | }{ 66 | {name: "keyed", line: __line__()}, 67 | {"unkeyed", __line__()}, 68 | } 69 | 70 | for _, test := range tests { 71 | t.Run(test.name, func(t *testing.T) { 72 | if got, expected := dataloc.L(test.name), fmt.Sprintf("%s:%d", file, test.line); got != expected { 73 | t.Errorf("expected %q, got %q", expected, got) 74 | } 75 | }) 76 | } 77 | } 78 | 79 | func TestL_caseTypeMap(t *testing.T) { 80 | tests := map[string]struct { 81 | line int 82 | }{ 83 | "test1": {line: __line__()}, 84 | "test2": {line: __line__()}, 85 | } 86 | 87 | for name, test := range tests { 88 | t.Run(name, func(t *testing.T) { 89 | if got, expected := dataloc.L(name), fmt.Sprintf("%s:%d", file, test.line); got != expected { 90 | t.Errorf("expected %q, got %q", expected, got) 91 | } 92 | }) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /dataloc/example_test.go: -------------------------------------------------------------------------------- 1 | package dataloc_test 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/motemen/go-testutil/dataloc" 7 | ) 8 | 9 | func Example() { 10 | testcases := []struct { 11 | name string 12 | a, b int 13 | sum int 14 | }{ 15 | { 16 | name: "100+200", 17 | a: 100, 18 | b: 200, 19 | sum: -1, 20 | }, 21 | { 22 | name: "1+1", 23 | a: 1, 24 | b: 1, 25 | sum: 99, 26 | }, 27 | } 28 | 29 | for _, testcase := range testcases { 30 | if expected, got := testcase.sum, testcase.a+testcase.b; got != expected { 31 | fmt.Printf("expected %d but got %d, test case at %s\n", expected, got, dataloc.L(testcase.name)) 32 | } 33 | } 34 | 35 | // Output: 36 | // expected -1 but got 300, test case at example_test.go:15 37 | // expected 99 but got 2, test case at example_test.go:21 38 | } 39 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/motemen/go-testutil 2 | 3 | go 1.20 4 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/motemen/go-testutil/af6add1c10c867049bd72433e0bae44eec8e3ab2/go.sum --------------------------------------------------------------------------------