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  }