k8s.io/kubernetes@v1.29.3/test/list/main.go (about) 1 /* 2 Copyright 2016 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 // list all unit and ginkgo test names that will be run 18 package main 19 20 import ( 21 "encoding/json" 22 "flag" 23 "fmt" 24 "go/ast" 25 "go/parser" 26 "go/token" 27 "log" 28 "os" 29 "path/filepath" 30 "strconv" 31 "strings" 32 ) 33 34 var ( 35 dumpTree = flag.Bool("dump", false, "print AST") 36 dumpJSON = flag.Bool("json", false, "output test list as JSON") 37 warn = flag.Bool("warn", false, "print warnings") 38 ) 39 40 // Test holds test locations, package names, and test names. 41 type Test struct { 42 Loc string 43 Name string 44 TestName string 45 } 46 47 // collect extracts test metadata from a file. 48 // If src is nil, it reads filename for the code, otherwise it 49 // uses src (which may be a string, byte[], or io.Reader). 50 func collect(filename string, src interface{}) []Test { 51 // Create the AST by parsing src. 52 fset := token.NewFileSet() // positions are relative to fset 53 f, err := parser.ParseFile(fset, filename, src, parser.ParseComments) 54 if err != nil { 55 panic(err) 56 } 57 58 if *dumpTree { 59 ast.Print(fset, f) 60 } 61 62 tests := make([]Test, 0) 63 64 ast.Walk(makeWalker("[k8s.io]", fset, &tests), f) 65 66 // Unit tests are much simpler to enumerate! 67 if strings.HasSuffix(filename, "_test.go") { 68 packageName := f.Name.Name 69 dirName, _ := filepath.Split(filename) 70 if filepath.Base(dirName) != packageName && *warn { 71 log.Printf("Warning: strange path/package mismatch %s %s\n", filename, packageName) 72 } 73 testPath := "k8s.io/kubernetes/" + dirName[:len(dirName)-1] 74 for _, decl := range f.Decls { 75 funcdecl, ok := decl.(*ast.FuncDecl) 76 if !ok { 77 continue 78 } 79 name := funcdecl.Name.Name 80 if strings.HasPrefix(name, "Test") { 81 tests = append(tests, Test{fset.Position(funcdecl.Pos()).String(), testPath, name}) 82 } 83 } 84 } 85 86 return tests 87 } 88 89 // funcName converts a selectorExpr with two idents into a string, 90 // x.y -> "x.y" 91 func funcName(n ast.Expr) string { 92 if sel, ok := n.(*ast.SelectorExpr); ok { 93 if x, ok := sel.X.(*ast.Ident); ok { 94 return x.String() + "." + sel.Sel.String() 95 } 96 } 97 return "" 98 } 99 100 // isSprintf returns whether the given node is a call to fmt.Sprintf 101 func isSprintf(n ast.Expr) bool { 102 call, ok := n.(*ast.CallExpr) 103 return ok && funcName(call.Fun) == "fmt.Sprintf" && len(call.Args) != 0 104 } 105 106 type walker struct { 107 path string 108 fset *token.FileSet 109 tests *[]Test 110 vals map[string]string 111 } 112 113 func makeWalker(path string, fset *token.FileSet, tests *[]Test) *walker { 114 return &walker{path, fset, tests, make(map[string]string)} 115 } 116 117 // clone creates a new walker with the given string extending the path. 118 func (w *walker) clone(ext string) *walker { 119 return &walker{w.path + " " + ext, w.fset, w.tests, w.vals} 120 } 121 122 // firstArg attempts to statically determine the value of the first 123 // argument. It only handles strings, and converts any unknown values 124 // (fmt.Sprintf interpolations) into *. 125 func (w *walker) firstArg(n *ast.CallExpr) string { 126 if len(n.Args) == 0 { 127 return "" 128 } 129 var lit *ast.BasicLit 130 if isSprintf(n.Args[0]) { 131 return w.firstArg(n.Args[0].(*ast.CallExpr)) 132 } 133 lit, ok := n.Args[0].(*ast.BasicLit) 134 if ok && lit.Kind == token.STRING { 135 v, err := strconv.Unquote(lit.Value) 136 if err != nil { 137 panic(err) 138 } 139 if strings.Contains(v, "%") { 140 v = strings.Replace(v, "%d", "*", -1) 141 v = strings.Replace(v, "%v", "*", -1) 142 v = strings.Replace(v, "%s", "*", -1) 143 } 144 return v 145 } 146 if ident, ok := n.Args[0].(*ast.Ident); ok { 147 if val, ok := w.vals[ident.String()]; ok { 148 return val 149 } 150 } 151 if *warn { 152 log.Printf("Warning: dynamic arg value: %v\n", w.fset.Position(n.Args[0].Pos())) 153 } 154 return "*" 155 } 156 157 // describeName returns the first argument of a function if it's 158 // a Ginkgo-relevant function (Describe/SIGDescribe/Context), 159 // and the empty string otherwise. 160 func (w *walker) describeName(n *ast.CallExpr) string { 161 switch x := n.Fun.(type) { 162 case *ast.Ident: 163 if x.Name != "SIGDescribe" && x.Name != "Describe" && x.Name != "Context" { 164 return "" 165 } 166 default: 167 return "" 168 } 169 return w.firstArg(n) 170 } 171 172 // itName returns the first argument if it's a call to It(), else "". 173 func (w *walker) itName(n *ast.CallExpr) string { 174 if fun, ok := n.Fun.(*ast.Ident); ok && fun.Name == "It" { 175 return w.firstArg(n) 176 } 177 return "" 178 } 179 180 // Visit walks the AST, following Ginkgo context and collecting tests. 181 // See the documentation for ast.Walk for more details. 182 func (w *walker) Visit(n ast.Node) ast.Visitor { 183 switch x := n.(type) { 184 case *ast.CallExpr: 185 name := w.describeName(x) 186 if name != "" && len(x.Args) >= 2 { 187 // If calling (Kube)Describe/Context, make a new 188 // walker to recurse with the description added. 189 return w.clone(name) 190 } 191 name = w.itName(x) 192 if name != "" { 193 // We've found an It() call, the full test name 194 // can be determined now. 195 if w.path == "[k8s.io]" && *warn { 196 log.Printf("It without matching Describe: %s\n", w.fset.Position(n.Pos())) 197 } 198 *w.tests = append(*w.tests, Test{w.fset.Position(n.Pos()).String(), w.path, name}) 199 return nil // Stop walking 200 } 201 case *ast.AssignStmt: 202 // Attempt to track literals that might be used as 203 // arguments. This analysis is very unsound, and ignores 204 // both scope and program flow, but is sufficient for 205 // our minor use case. 206 ident, ok := x.Lhs[0].(*ast.Ident) 207 if ok { 208 if isSprintf(x.Rhs[0]) { 209 // x := fmt.Sprintf("something", args) 210 w.vals[ident.String()] = w.firstArg(x.Rhs[0].(*ast.CallExpr)) 211 } 212 if lit, ok := x.Rhs[0].(*ast.BasicLit); ok && lit.Kind == token.STRING { 213 // x := "a literal string" 214 v, err := strconv.Unquote(lit.Value) 215 if err != nil { 216 panic(err) 217 } 218 w.vals[ident.String()] = v 219 } 220 } 221 } 222 return w // Continue walking 223 } 224 225 type testList struct { 226 tests []Test 227 } 228 229 // handlePath walks the filesystem recursively, collecting tests 230 // from files with paths *e2e*.go and *_test.go, ignoring third_party 231 // and staging directories. 232 func (t *testList) handlePath(path string, info os.FileInfo, err error) error { 233 if err != nil { 234 return err 235 } 236 if strings.Contains(path, "third_party") || 237 strings.Contains(path, "staging") || 238 strings.Contains(path, "_output") { 239 return filepath.SkipDir 240 } 241 if strings.HasSuffix(path, ".go") && strings.Contains(path, "e2e") || 242 strings.HasSuffix(path, "_test.go") { 243 tests := collect(path, nil) 244 t.tests = append(t.tests, tests...) 245 } 246 return nil 247 } 248 249 func main() { 250 flag.Parse() 251 args := flag.Args() 252 if len(args) == 0 { 253 args = append(args, ".") 254 } 255 tests := testList{} 256 for _, arg := range args { 257 err := filepath.Walk(arg, tests.handlePath) 258 if err != nil { 259 log.Fatalf("Error walking: %v", err) 260 } 261 } 262 if *dumpJSON { 263 json, err := json.Marshal(tests.tests) 264 if err != nil { 265 log.Fatal(err) 266 } 267 fmt.Println(string(json)) 268 } else { 269 for _, t := range tests.tests { 270 fmt.Println(t) 271 } 272 } 273 }