github.com/mmirolim/gtr@v0.3.0/testrunner.go (about) 1 package main 2 3 import ( 4 "context" 5 "fmt" 6 "log" 7 "os" 8 "runtime" 9 "sort" 10 "strconv" 11 "strings" 12 ) 13 14 var _ Task = (*GoTestRunner)(nil) 15 16 // Strategy interface defines provider of tests for the testrunner 17 type Strategy interface { 18 CoverageEnabled() bool 19 TestsToRun(context.Context) (runAll bool, tests, subTests []string, err error) 20 } 21 22 // GoTestRunner runs go tests 23 type GoTestRunner struct { 24 strategy Strategy 25 cmd CommandCreator 26 args string 27 log *log.Logger 28 } 29 30 // NewGoTestRunner creates test runner 31 // strategy to use 32 // cmd creator 33 // args to runner 34 // logger for runner 35 func NewGoTestRunner( 36 strategy Strategy, 37 cmd CommandCreator, 38 args string, 39 logger *log.Logger, 40 ) *GoTestRunner { 41 return &GoTestRunner{ 42 strategy: strategy, 43 cmd: cmd, 44 args: args, 45 log: logger, 46 } 47 } 48 49 // ID returns Task ID 50 func (tr *GoTestRunner) ID() string { 51 return "GoTestRunner" 52 } 53 54 // Run method implements Task interface 55 // runs go tests 56 func (tr *GoTestRunner) Run(ctx context.Context) (string, error) { 57 runAll, tests, subTests, err := tr.strategy.TestsToRun(ctx) 58 if err != nil { 59 if err == ErrBuildFailed { 60 return "Build Failed", nil 61 } 62 return "", fmt.Errorf("strategy error %v", err) 63 } 64 if len(tests) == 0 && len(subTests) == 0 { 65 return "No test found to run", nil 66 } 67 68 var listArg []string 69 pkgPaths := map[string][]string{} 70 for _, tname := range tests { 71 id := strings.LastIndexByte(tname, '.') 72 pkgPath := tname[:id] 73 if pkgPath == "" { 74 pkgPath = "." 75 } 76 pkgPaths[pkgPath] = append(pkgPaths[pkgPath], tname[id+1:]) 77 } 78 79 // run tests 80 // do not wait process to finish 81 // in case of console blocking programs 82 // -vet=off to improve speed 83 // TODO handle run all 84 msg := "" 85 testParams := []string{"test", "-v", "-vet", "off", "-failfast", 86 "-cpu", strconv.Itoa(runtime.GOMAXPROCS(0))} 87 88 logStrList(tr.log, "Tests to run", tests, true) 89 if len(subTests) > 0 { 90 logStrList(tr.log, "Subtests to run", subTests, true) 91 } 92 var pkgList, testNames []string 93 for k, pkgtests := range pkgPaths { 94 pkgList = append(pkgList, k) 95 testNames = append(testNames, pkgtests...) 96 } 97 testsFormated := tr.joinTestAndSubtest(testNames, subTests) 98 var cmd CommandExecutor 99 // TODO refactor 100 if runAll { 101 if tr.strategy.CoverageEnabled() { 102 testParams = append(testParams, "-coverprofile") 103 testParams = append(testParams, "coverage_profile") 104 } 105 testParams = append(testParams, "-run") 106 testParams = append(testParams, testsFormated) 107 testParams = append(testParams, listArg...) 108 if len(tr.args) > 0 { 109 testParams = append(testParams, "-args") 110 testParams = append(testParams, tr.args) 111 } 112 cmd = tr.cmd(ctx, "go", testParams...) 113 tr.log.Println(">>", strings.Join(cmd.GetArgs(), " ")) 114 115 cmd.SetStdout(os.Stdout) 116 cmd.SetStderr(os.Stderr) 117 cmd.SetEnv(os.Environ()) 118 cmd.Run() 119 } else { 120 // run cmd for each test and skip subtests to have separation between tests 121 OUTER: 122 for pkg, pkgtests := range pkgPaths { 123 for _, tname := range pkgtests { 124 // run all tests 125 testParams := []string{"test", "-v", "-vet", "off", 126 "-cpu", strconv.Itoa(runtime.GOMAXPROCS(0))} 127 128 if tr.strategy.CoverageEnabled() { 129 testParams = append(testParams, "-coverprofile") 130 testParams = append(testParams, fmt.Sprintf(".gtr/%s.%s", 131 strings.ReplaceAll(pkg, "/", "_"), 132 tname)) 133 } 134 135 testParams = append(testParams, "-run") 136 testParams = append(testParams, tname+"$") // test 137 if tr.strategy.CoverageEnabled() { 138 // for all packages 139 testParams = append(testParams, "./...") 140 } else { 141 // package 142 testParams = append(testParams, pkg) 143 } 144 if len(tr.args) > 0 { 145 testParams = append(testParams, "-args") 146 // test binary args 147 testParams = append(testParams, tr.args) 148 } 149 cmd = tr.cmd(ctx, "go", testParams...) 150 tr.log.Println(">>", strings.Join(cmd.GetArgs(), " ")) 151 152 cmd.SetStdout(os.Stdout) 153 cmd.SetStderr(os.Stderr) 154 cmd.SetEnv(os.Environ()) 155 cmd.Run() 156 if !cmd.Success() { 157 // stop on failed test 158 break OUTER 159 } 160 } 161 } 162 } 163 164 if cmd.Success() { 165 msg = "Tests PASS: " + testsFormated 166 tr.log.Println("\033[32mTests PASS\033[39m") 167 } else { 168 msg = "Tests FAIL: " + testsFormated 169 tr.log.Println("\033[31mTests FAIL\033[39m") 170 } 171 return msg, nil 172 } 173 174 // joinTestAndSubtest joins and format tests according to go test -run arg format 175 func (tr *GoTestRunner) joinTestAndSubtest(tests, subTests []string) string { 176 sort.Strings(tests) 177 sort.Strings(subTests) 178 out := strings.Join(tests, "$|") 179 if len(out) > 0 { 180 out += "$" 181 } 182 for i := range subTests { 183 subTests[i] = strings.ReplaceAll(subTests[i], " ", "_") 184 } 185 if len(subTests) != 0 { 186 out += "/(" + strings.Join(subTests, "|") + ")" 187 } 188 return out 189 } 190 191 func logStrList(log *log.Logger, title string, tests []string, toSort bool) { 192 var out []string 193 if toSort { 194 out = make([]string, len(tests)) 195 copy(out[0:], tests) 196 sort.Strings(out) 197 } else { 198 out = tests 199 } 200 201 log.Println("=============") // output for debug 202 log.Println(title) 203 for i := range out { 204 log.Printf("-> %+v\n", out[i]) // output for debug 205 } 206 log.Println("=============") 207 }