github.com/rclone/rclone@v1.66.1-0.20240517100346-7b89735ae726/fstest/test_all/run.go (about) 1 // Run a test 2 3 package main 4 5 import ( 6 "bytes" 7 "context" 8 "fmt" 9 "go/build" 10 "io" 11 "log" 12 "os" 13 "os/exec" 14 "path" 15 "regexp" 16 "runtime" 17 "sort" 18 "strconv" 19 "strings" 20 "sync" 21 "time" 22 23 "github.com/rclone/rclone/fs" 24 "github.com/rclone/rclone/fstest/testserver" 25 ) 26 27 // Control concurrency per backend if required 28 var ( 29 oneOnlyMu sync.Mutex 30 oneOnly = map[string]*sync.Mutex{} 31 ) 32 33 // Run holds info about a running test 34 // 35 // A run just runs one command line, but it can be run multiple times 36 // if retries are needed. 37 type Run struct { 38 // Config 39 Remote string // name of the test remote 40 Backend string // name of the backend 41 Path string // path to the source directory 42 FastList bool // add -fast-list to tests 43 Short bool // add -short 44 NoRetries bool // don't retry if set 45 OneOnly bool // only run test for this backend at once 46 NoBinary bool // set to not build a binary 47 SizeLimit int64 // maximum test file size 48 Ignore map[string]struct{} 49 ListRetries int // -list-retries if > 0 50 ExtraTime float64 // multiply the timeout by this 51 // Internals 52 CmdLine []string 53 CmdString string 54 Try int 55 err error 56 output []byte 57 FailedTests []string 58 RunFlag string 59 LogDir string // directory to place the logs 60 TrialName string // name/log file name of current trial 61 TrialNames []string // list of all the trials 62 } 63 64 // Runs records multiple Run objects 65 type Runs []*Run 66 67 // Sort interface 68 func (rs Runs) Len() int { return len(rs) } 69 func (rs Runs) Swap(i, j int) { rs[i], rs[j] = rs[j], rs[i] } 70 func (rs Runs) Less(i, j int) bool { 71 a, b := rs[i], rs[j] 72 if a.Backend < b.Backend { 73 return true 74 } else if a.Backend > b.Backend { 75 return false 76 } 77 if a.Remote < b.Remote { 78 return true 79 } else if a.Remote > b.Remote { 80 return false 81 } 82 if a.Path < b.Path { 83 return true 84 } else if a.Path > b.Path { 85 return false 86 } 87 if !a.FastList && b.FastList { 88 return true 89 } else if a.FastList && !b.FastList { 90 return false 91 } 92 return false 93 } 94 95 // dumpOutput prints the error output 96 func (r *Run) dumpOutput() { 97 log.Println("------------------------------------------------------------") 98 log.Printf("---- %q ----", r.CmdString) 99 log.Println(string(r.output)) 100 log.Println("------------------------------------------------------------") 101 } 102 103 // trie for storing runs 104 type trie map[string]trie 105 106 // turn a trie into multiple regexp matches 107 // 108 // We can't ever have a / in a regexp as it doesn't work. 109 func match(current trie) []string { 110 var names []string 111 var parts []string 112 for name, value := range current { 113 matchName := "^" + name + "$" 114 if len(value) == 0 { 115 names = append(names, name) 116 } else { 117 for _, part := range match(value) { 118 parts = append(parts, matchName+"/"+part) 119 } 120 } 121 } 122 sort.Strings(names) 123 if len(names) > 1 { 124 parts = append(parts, "^("+strings.Join(names, "|")+")$") 125 } else if len(names) == 1 { 126 parts = append(parts, "^"+names[0]+"$") 127 } 128 sort.Strings(parts) 129 return parts 130 } 131 132 // This converts a slice of test names into a regexp which matches 133 // them. 134 func testsToRegexp(tests []string) string { 135 var split = trie{} 136 // Make a trie showing which parts are used at each level 137 for _, test := range tests { 138 var parent = split 139 for _, name := range strings.Split(test, "/") { 140 current := parent[name] 141 if current == nil { 142 current = trie{} 143 parent[name] = current 144 } 145 parent = current 146 } 147 } 148 parts := match(split) 149 return strings.Join(parts, "|") 150 } 151 152 var failRe = regexp.MustCompile(`(?m)^\s*--- FAIL: (Test.*?) \(`) 153 154 // findFailures looks for all the tests which failed 155 func (r *Run) findFailures() { 156 oldFailedTests := r.FailedTests 157 r.FailedTests = nil 158 excludeParents := map[string]struct{}{} 159 ignored := 0 160 for _, matches := range failRe.FindAllSubmatch(r.output, -1) { 161 failedTest := string(matches[1]) 162 // Skip any ignored failures 163 if _, found := r.Ignore[failedTest]; found { 164 ignored++ 165 } else { 166 r.FailedTests = append(r.FailedTests, failedTest) 167 } 168 // Find all the parents of this test 169 parts := strings.Split(failedTest, "/") 170 for i := len(parts) - 1; i >= 1; i-- { 171 excludeParents[strings.Join(parts[:i], "/")] = struct{}{} 172 } 173 } 174 // Exclude the parents 175 var newTests = r.FailedTests[:0] 176 for _, failedTest := range r.FailedTests { 177 if _, excluded := excludeParents[failedTest]; !excluded { 178 newTests = append(newTests, failedTest) 179 } 180 } 181 r.FailedTests = newTests 182 if len(r.FailedTests) == 0 && ignored > 0 { 183 log.Printf("%q - Found %d ignored errors only - marking as good", r.CmdString, ignored) 184 r.err = nil 185 r.dumpOutput() 186 return 187 } 188 if len(r.FailedTests) != 0 { 189 r.RunFlag = testsToRegexp(r.FailedTests) 190 } else { 191 r.RunFlag = "" 192 } 193 if r.passed() && len(r.FailedTests) != 0 { 194 log.Printf("%q - Expecting no errors but got: %v", r.CmdString, r.FailedTests) 195 r.dumpOutput() 196 } else if !r.passed() && len(r.FailedTests) == 0 { 197 log.Printf("%q - Expecting errors but got none: %v", r.CmdString, r.FailedTests) 198 r.dumpOutput() 199 r.FailedTests = oldFailedTests 200 } 201 } 202 203 // nextCmdLine returns the next command line 204 func (r *Run) nextCmdLine() []string { 205 CmdLine := r.CmdLine 206 if r.RunFlag != "" { 207 CmdLine = append(CmdLine, "-test.run", r.RunFlag) 208 } 209 return CmdLine 210 } 211 212 // trial runs a single test 213 func (r *Run) trial() { 214 CmdLine := r.nextCmdLine() 215 CmdString := toShell(CmdLine) 216 msg := fmt.Sprintf("%q - Starting (try %d/%d)", CmdString, r.Try, *maxTries) 217 log.Println(msg) 218 logName := path.Join(r.LogDir, r.TrialName) 219 out, err := os.Create(logName) 220 if err != nil { 221 log.Fatalf("Couldn't create log file: %v", err) 222 } 223 defer func() { 224 err := out.Close() 225 if err != nil { 226 log.Fatalf("Failed to close log file: %v", err) 227 } 228 }() 229 _, _ = fmt.Fprintln(out, msg) 230 231 // Early exit if --try-run 232 if *dryRun { 233 log.Printf("Not executing as --dry-run: %v", CmdLine) 234 _, _ = fmt.Fprintln(out, "--dry-run is set - not running") 235 return 236 } 237 238 // Start the test server if required 239 finish, err := testserver.Start(r.Remote) 240 if err != nil { 241 log.Printf("%s: Failed to start test server: %v", r.Remote, err) 242 _, _ = fmt.Fprintf(out, "%s: Failed to start test server: %v\n", r.Remote, err) 243 r.err = err 244 return 245 } 246 defer finish() 247 248 // Internal buffer 249 var b bytes.Buffer 250 multiOut := io.MultiWriter(out, &b) 251 252 cmd := exec.Command(CmdLine[0], CmdLine[1:]...) 253 cmd.Stderr = multiOut 254 cmd.Stdout = multiOut 255 cmd.Dir = r.Path 256 start := time.Now() 257 r.err = cmd.Run() 258 r.output = b.Bytes() 259 duration := time.Since(start) 260 r.findFailures() 261 if r.passed() { 262 msg = fmt.Sprintf("%q - Finished OK in %v (try %d/%d)", CmdString, duration, r.Try, *maxTries) 263 } else { 264 msg = fmt.Sprintf("%q - Finished ERROR in %v (try %d/%d): %v: Failed %v", CmdString, duration, r.Try, *maxTries, r.err, r.FailedTests) 265 } 266 log.Println(msg) 267 _, _ = fmt.Fprintln(out, msg) 268 } 269 270 // passed returns true if the test passed 271 func (r *Run) passed() bool { 272 return r.err == nil 273 } 274 275 // GOPATH returns the current GOPATH 276 func GOPATH() string { 277 gopath := os.Getenv("GOPATH") 278 if gopath == "" { 279 gopath = build.Default.GOPATH 280 } 281 return gopath 282 } 283 284 // BinaryName turns a package name into a binary name 285 func (r *Run) BinaryName() string { 286 binary := path.Base(r.Path) + ".test" 287 if runtime.GOOS == "windows" { 288 binary += ".exe" 289 } 290 return binary 291 } 292 293 // BinaryPath turns a package name into a binary path 294 func (r *Run) BinaryPath() string { 295 return path.Join(r.Path, r.BinaryName()) 296 } 297 298 // PackagePath returns the path to the package 299 func (r *Run) PackagePath() string { 300 return path.Join(GOPATH(), "src", r.Path) 301 } 302 303 // MakeTestBinary makes the binary we will run 304 func (r *Run) MakeTestBinary() { 305 binary := r.BinaryPath() 306 binaryName := r.BinaryName() 307 log.Printf("%s: Making test binary %q", r.Path, binaryName) 308 CmdLine := []string{"go", "test", "-c"} 309 if *race { 310 CmdLine = append(CmdLine, "-race") 311 } 312 if *dryRun { 313 log.Printf("Not executing: %v", CmdLine) 314 return 315 } 316 cmd := exec.Command(CmdLine[0], CmdLine[1:]...) 317 cmd.Dir = r.Path 318 err := cmd.Run() 319 if err != nil { 320 log.Fatalf("Failed to make test binary: %v", err) 321 } 322 if _, err := os.Stat(binary); err != nil { 323 log.Fatalf("Couldn't find test binary %q", binary) 324 } 325 } 326 327 // RemoveTestBinary removes the binary made in makeTestBinary 328 func (r *Run) RemoveTestBinary() { 329 if *dryRun { 330 return 331 } 332 binary := r.BinaryPath() 333 err := os.Remove(binary) // Delete the binary when finished 334 if err != nil { 335 log.Printf("Error removing test binary %q: %v", binary, err) 336 } 337 } 338 339 // Name returns the run name as a file name friendly string 340 func (r *Run) Name() string { 341 ns := []string{ 342 r.Backend, 343 strings.ReplaceAll(r.Path, "/", "."), 344 r.Remote, 345 } 346 if r.FastList { 347 ns = append(ns, "fastlist") 348 } 349 ns = append(ns, fmt.Sprintf("%d", r.Try)) 350 s := strings.Join(ns, "-") 351 s = strings.ReplaceAll(s, ":", "") 352 return s 353 } 354 355 // Init the Run 356 func (r *Run) Init() { 357 prefix := "-test." 358 if r.NoBinary { 359 prefix = "-" 360 r.CmdLine = []string{"go", "test"} 361 } else { 362 r.CmdLine = []string{"./" + r.BinaryName()} 363 } 364 testTimeout := *timeout 365 if r.ExtraTime > 0 { 366 testTimeout = time.Duration(float64(testTimeout) * r.ExtraTime) 367 } 368 r.CmdLine = append(r.CmdLine, prefix+"v", prefix+"timeout", testTimeout.String(), "-remote", r.Remote) 369 listRetries := *listRetries 370 if r.ListRetries > 0 { 371 listRetries = r.ListRetries 372 } 373 if listRetries > 0 { 374 r.CmdLine = append(r.CmdLine, "-list-retries", fmt.Sprint(listRetries)) 375 } 376 r.Try = 1 377 ci := fs.GetConfig(context.Background()) 378 if *verbose { 379 r.CmdLine = append(r.CmdLine, "-verbose") 380 ci.LogLevel = fs.LogLevelDebug 381 } 382 if *runOnly != "" { 383 r.CmdLine = append(r.CmdLine, prefix+"run", *runOnly) 384 } 385 if r.FastList { 386 r.CmdLine = append(r.CmdLine, "-fast-list") 387 } 388 if r.Short { 389 r.CmdLine = append(r.CmdLine, "-short") 390 } 391 if r.SizeLimit > 0 { 392 r.CmdLine = append(r.CmdLine, "-size-limit", strconv.FormatInt(r.SizeLimit, 10)) 393 } 394 r.CmdString = toShell(r.CmdLine) 395 } 396 397 // Logs returns all the log names 398 func (r *Run) Logs() []string { 399 return r.TrialNames 400 } 401 402 // FailedTestsCSV returns the failed tests as a comma separated string, limiting the number 403 func (r *Run) FailedTestsCSV() string { 404 const maxTests = 5 405 ts := r.FailedTests 406 if len(ts) > maxTests { 407 ts = ts[:maxTests:maxTests] 408 ts = append(ts, fmt.Sprintf("… (%d more)", len(r.FailedTests)-maxTests)) 409 } 410 return strings.Join(ts, ", ") 411 } 412 413 // Run runs all the trials for this test 414 func (r *Run) Run(LogDir string, result chan<- *Run) { 415 if r.OneOnly { 416 oneOnlyMu.Lock() 417 mu := oneOnly[r.Backend] 418 if mu == nil { 419 mu = new(sync.Mutex) 420 oneOnly[r.Backend] = mu 421 } 422 oneOnlyMu.Unlock() 423 mu.Lock() 424 defer mu.Unlock() 425 } 426 r.Init() 427 r.LogDir = LogDir 428 for r.Try = 1; r.Try <= *maxTries; r.Try++ { 429 r.TrialName = r.Name() + ".txt" 430 r.TrialNames = append(r.TrialNames, r.TrialName) 431 log.Printf("Starting run with log %q", r.TrialName) 432 r.trial() 433 if r.passed() || r.NoRetries { 434 break 435 } 436 } 437 if !r.passed() { 438 r.dumpOutput() 439 } 440 result <- r 441 }