1// Copyright 2021 The Dawn Authors 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15// run-cts is a tool used to run the WebGPU CTS using the Dawn module for NodeJS 16package main 17 18import ( 19 "bytes" 20 "context" 21 "encoding/json" 22 "errors" 23 "flag" 24 "fmt" 25 "io" 26 "io/ioutil" 27 "math" 28 "net/http" 29 "os" 30 "os/exec" 31 "os/signal" 32 "path/filepath" 33 "regexp" 34 "runtime" 35 "strconv" 36 "strings" 37 "sync" 38 "syscall" 39 "time" 40 41 "github.com/mattn/go-colorable" 42 "github.com/mattn/go-isatty" 43) 44 45const ( 46 testTimeout = time.Minute 47) 48 49func main() { 50 if err := run(); err != nil { 51 fmt.Println(err) 52 os.Exit(1) 53 } 54} 55 56func showUsage() { 57 fmt.Println(` 58run-cts is a tool used to run the WebGPU CTS using the Dawn module for NodeJS 59 60Usage: 61 run-cts --dawn-node=<path to dawn.node> --cts=<path to WebGPU CTS> [test-query]`) 62 os.Exit(1) 63} 64 65var ( 66 colors bool 67 stdout io.Writer 68 mainCtx context.Context 69) 70 71type dawnNodeFlags []string 72 73func (f *dawnNodeFlags) String() string { 74 return fmt.Sprint(strings.Join(*f, "")) 75} 76 77func (f *dawnNodeFlags) Set(value string) error { 78 // Multiple flags must be passed in indivually: 79 // -flag=a=b -dawn_node_flag=c=d 80 *f = append(*f, value) 81 return nil 82} 83 84func makeMainCtx() context.Context { 85 ctx, cancel := context.WithCancel(context.Background()) 86 sigs := make(chan os.Signal, 1) 87 signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) 88 go func() { 89 sig := <-sigs 90 fmt.Printf("Signal received: %v\n", sig) 91 cancel() 92 }() 93 return ctx 94} 95 96func run() error { 97 mainCtx = makeMainCtx() 98 99 colors = os.Getenv("TERM") != "dumb" || 100 isatty.IsTerminal(os.Stdout.Fd()) || 101 isatty.IsCygwinTerminal(os.Stdout.Fd()) 102 if colors { 103 if _, disable := os.LookupEnv("NO_COLOR"); disable { 104 colors = false 105 } 106 } 107 108 backendDefault := "default" 109 if vkIcdFilenames := os.Getenv("VK_ICD_FILENAMES"); vkIcdFilenames != "" { 110 backendDefault = "vulkan" 111 } 112 113 var dawnNode, cts, node, npx, logFilename, backend string 114 var verbose, isolated, build bool 115 var numRunners int 116 var flags dawnNodeFlags 117 flag.StringVar(&dawnNode, "dawn-node", "", "path to dawn.node module") 118 flag.StringVar(&cts, "cts", "", "root directory of WebGPU CTS") 119 flag.StringVar(&node, "node", "", "path to node executable") 120 flag.StringVar(&npx, "npx", "", "path to npx executable") 121 flag.BoolVar(&verbose, "verbose", false, "print extra information while testing") 122 flag.BoolVar(&build, "build", true, "attempt to build the CTS before running") 123 flag.BoolVar(&isolated, "isolate", false, "run each test in an isolated process") 124 flag.BoolVar(&colors, "colors", colors, "enable / disable colors") 125 flag.IntVar(&numRunners, "j", runtime.NumCPU()/2, "number of concurrent runners. 0 runs serially") 126 flag.StringVar(&logFilename, "log", "", "path to log file of tests run and result") 127 flag.Var(&flags, "flag", "flag to pass to dawn-node as flag=value. multiple flags must be passed in individually") 128 flag.StringVar(&backend, "backend", backendDefault, "backend to use: default|null|webgpu|d3d11|d3d12|metal|vulkan|opengl|opengles."+ 129 " set to 'vulkan' if VK_ICD_FILENAMES environment variable is set, 'default' otherwise") 130 flag.Parse() 131 132 if colors { 133 stdout = colorable.NewColorableStdout() 134 } else { 135 stdout = colorable.NewNonColorable(os.Stdout) 136 } 137 138 // Check mandatory arguments 139 if dawnNode == "" || cts == "" { 140 showUsage() 141 } 142 if !isFile(dawnNode) { 143 return fmt.Errorf("'%v' is not a file", dawnNode) 144 } 145 if !isDir(cts) { 146 return fmt.Errorf("'%v' is not a directory", cts) 147 } 148 149 // Make paths absolute 150 for _, path := range []*string{&dawnNode, &cts} { 151 abs, err := filepath.Abs(*path) 152 if err != nil { 153 return fmt.Errorf("unable to get absolute path for '%v'", *path) 154 } 155 *path = abs 156 } 157 158 // The test query is the optional unnamed argument 159 query := "webgpu:*" 160 switch len(flag.Args()) { 161 case 0: 162 case 1: 163 query = flag.Args()[0] 164 default: 165 return fmt.Errorf("only a single query can be provided") 166 } 167 168 // Find node 169 if node == "" { 170 var err error 171 node, err = exec.LookPath("node") 172 if err != nil { 173 return fmt.Errorf("add node to PATH or specify with --node") 174 } 175 } 176 // Find npx 177 if npx == "" { 178 var err error 179 npx, err = exec.LookPath("npx") 180 if err != nil { 181 npx = "" 182 } 183 } 184 185 if backend != "default" { 186 fmt.Println("Forcing backend to", backend) 187 flags = append(flags, fmt.Sprint("dawn-backend=", backend)) 188 } 189 190 r := runner{ 191 numRunners: numRunners, 192 verbose: verbose, 193 node: node, 194 npx: npx, 195 dawnNode: dawnNode, 196 cts: cts, 197 flags: flags, 198 evalScript: func(main string) string { 199 return fmt.Sprintf(`require('./src/common/tools/setup-ts-in-node.js');require('./src/common/runtime/%v.ts');`, main) 200 }, 201 } 202 203 if logFilename != "" { 204 writer, err := os.Create(logFilename) 205 if err != nil { 206 return fmt.Errorf("failed to open log '%v': %w", logFilename, err) 207 } 208 defer writer.Close() 209 r.log = newLogger(writer) 210 } 211 212 cache := cache{} 213 cachePath := dawnNode + ".runcts.cache" 214 if err := cache.load(cachePath); err != nil && verbose { 215 fmt.Println("failed to load cache from", cachePath, err) 216 } 217 defer cache.save(cachePath) 218 219 // Scan the CTS source to determine the most recent change to the CTS source 220 mostRecentSourceChange, err := r.scanSourceTimestamps(verbose) 221 if err != nil { 222 return fmt.Errorf("failed to scan source files for modified timestamps: %w", err) 223 } 224 225 ctsNeedsRebuild := mostRecentSourceChange.After(cache.BuildTimestamp) || 226 !isDir(filepath.Join(r.cts, "out-node")) 227 if build { 228 if verbose { 229 fmt.Println("CTS needs rebuild:", ctsNeedsRebuild) 230 } 231 232 if npx != "" { 233 if ctsNeedsRebuild { 234 if err := r.buildCTS(verbose); err != nil { 235 return fmt.Errorf("failed to build CTS: %w", err) 236 } 237 cache.BuildTimestamp = mostRecentSourceChange 238 } 239 // Use the prebuilt CTS (instead of using the `setup-ts-in-node` transpiler) 240 r.evalScript = func(main string) string { 241 return fmt.Sprintf(`require('./out-node/common/runtime/%v.js');`, main) 242 } 243 } else { 244 fmt.Println("npx not found on PATH. Using runtime TypeScript transpilation (slow)") 245 } 246 } 247 248 if numRunners > 0 { 249 // Find all the test cases that match the given queries. 250 if err := r.gatherTestCases(query, verbose); err != nil { 251 return fmt.Errorf("failed to gather test cases: %w", err) 252 } 253 254 if isolated { 255 fmt.Println("Running in parallel isolated...") 256 fmt.Printf("Testing %d test cases...\n", len(r.testcases)) 257 return r.runParallelIsolated() 258 } 259 fmt.Println("Running in parallel with server...") 260 fmt.Printf("Testing %d test cases...\n", len(r.testcases)) 261 return r.runParallelWithServer() 262 } 263 264 fmt.Println("Running serially...") 265 return r.runSerially(query) 266} 267 268type logger struct { 269 writer io.Writer 270 idx int 271 resultByIndex map[int]result 272} 273 274// newLogger creates a new logger instance. 275func newLogger(writer io.Writer) logger { 276 return logger{writer, 0, map[int]result{}} 277} 278 279// logResult writes the test results to the log file in sequential order. 280// logResult should be called whenever a new test result becomes available. 281func (l *logger) logResults(res result) { 282 if l.writer == nil { 283 return 284 } 285 l.resultByIndex[res.index] = res 286 for { 287 logRes, ok := l.resultByIndex[l.idx] 288 if !ok { 289 break 290 } 291 fmt.Fprintf(l.writer, "%v [%v]\n", logRes.testcase, logRes.status) 292 l.idx++ 293 } 294} 295 296// Cache holds cached information between runs to optimize runs 297type cache struct { 298 BuildTimestamp time.Time 299} 300 301// load loads the cache information from the JSON file at path 302func (c *cache) load(path string) error { 303 f, err := os.Open(path) 304 if err != nil { 305 return err 306 } 307 defer f.Close() 308 return json.NewDecoder(f).Decode(c) 309} 310 311// save saves the cache information to the JSON file at path 312func (c *cache) save(path string) error { 313 f, err := os.Create(path) 314 if err != nil { 315 return err 316 } 317 defer f.Close() 318 return json.NewEncoder(f).Encode(c) 319} 320 321type runner struct { 322 numRunners int 323 verbose bool 324 node, npx, dawnNode, cts string 325 flags dawnNodeFlags 326 evalScript func(string) string 327 testcases []string 328 log logger 329} 330 331// scanSourceTimestamps scans all the .js and .ts files in all subdirectories of 332// r.cts, and returns the file with the most recent timestamp. 333func (r *runner) scanSourceTimestamps(verbose bool) (time.Time, error) { 334 if verbose { 335 start := time.Now() 336 fmt.Println("Scanning .js / .ts files for changes...") 337 defer func() { 338 fmt.Println("completed in", time.Since(start)) 339 }() 340 } 341 342 dir := filepath.Join(r.cts, "src") 343 344 mostRecentChange := time.Time{} 345 err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { 346 switch filepath.Ext(path) { 347 case ".ts", ".js": 348 if info.ModTime().After(mostRecentChange) { 349 mostRecentChange = info.ModTime() 350 } 351 } 352 return nil 353 }) 354 if err != nil { 355 return time.Time{}, err 356 } 357 return mostRecentChange, nil 358} 359 360// buildCTS calls `npx grunt run:build-out-node` in the CTS directory to compile 361// the TypeScript files down to JavaScript. Doing this once ahead of time can be 362// much faster than dynamically transpiling when there are many tests to run. 363func (r *runner) buildCTS(verbose bool) error { 364 if verbose { 365 start := time.Now() 366 fmt.Println("Building CTS...") 367 defer func() { 368 fmt.Println("completed in", time.Since(start)) 369 }() 370 } 371 372 cmd := exec.Command(r.npx, "grunt", "run:build-out-node") 373 cmd.Dir = r.cts 374 out, err := cmd.CombinedOutput() 375 if err != nil { 376 return fmt.Errorf("%w: %v", err, string(out)) 377 } 378 return nil 379} 380 381// gatherTestCases() queries the CTS for all test cases that match the given 382// query. On success, gatherTestCases() populates r.testcases. 383func (r *runner) gatherTestCases(query string, verbose bool) error { 384 if verbose { 385 start := time.Now() 386 fmt.Println("Gathering test cases...") 387 defer func() { 388 fmt.Println("completed in", time.Since(start)) 389 }() 390 } 391 392 args := append([]string{ 393 "-e", r.evalScript("cmdline"), 394 "--", // Start of arguments 395 // src/common/runtime/helper/sys.ts expects 'node file.js <args>' 396 // and slices away the first two arguments. When running with '-e', args 397 // start at 1, so just inject a dummy argument. 398 "dummy-arg", 399 "--list", 400 }, query) 401 402 cmd := exec.Command(r.node, args...) 403 cmd.Dir = r.cts 404 out, err := cmd.CombinedOutput() 405 if err != nil { 406 return fmt.Errorf("%w\n%v", err, string(out)) 407 } 408 409 tests := filterTestcases(strings.Split(string(out), "\n")) 410 r.testcases = tests 411 return nil 412} 413 414type portListener struct { 415 buffer strings.Builder 416 port chan int 417} 418 419func newPortListener() portListener { 420 return portListener{strings.Builder{}, make(chan int)} 421} 422 423var portRE = regexp.MustCompile(`\[\[(\d+)\]\]`) 424 425func (p *portListener) Write(data []byte) (n int, err error) { 426 if p.port != nil { 427 p.buffer.Write(data) 428 match := portRE.FindStringSubmatch(p.buffer.String()) 429 if len(match) == 2 { 430 port, err := strconv.Atoi(match[1]) 431 if err != nil { 432 return 0, err 433 } 434 p.port <- port 435 close(p.port) 436 p.port = nil 437 } 438 } 439 return len(data), nil 440} 441 442// runParallelWithServer() starts r.numRunners instances of the CTS server test 443// runner, and issues test run requests to those servers, concurrently. 444func (r *runner) runParallelWithServer() error { 445 // Create a chan of test indices. 446 // This will be read by the test runner goroutines. 447 caseIndices := make(chan int, len(r.testcases)) 448 for i := range r.testcases { 449 caseIndices <- i 450 } 451 close(caseIndices) 452 453 // Create a chan for the test results. 454 // This will be written to by the test runner goroutines. 455 results := make(chan result, len(r.testcases)) 456 457 // Spin up the test runner goroutines 458 wg := &sync.WaitGroup{} 459 for i := 0; i < r.numRunners; i++ { 460 wg.Add(1) 461 go func() { 462 defer wg.Done() 463 if err := r.runServer(caseIndices, results); err != nil { 464 results <- result{ 465 status: fail, 466 error: fmt.Errorf("Test server error: %w", err), 467 } 468 } 469 }() 470 } 471 472 r.streamResults(wg, results) 473 return nil 474} 475 476type redirectingWriter struct { 477 io.Writer 478} 479 480// runServer starts a test runner server instance, takes case indices from 481// caseIndices, and requests the server run the test with the given index. 482// The result of the test run is written to the results chan. 483// Once the caseIndices chan has been closed, the server is stopped and 484// runServer returns. 485func (r *runner) runServer(caseIndices <-chan int, results chan<- result) error { 486 var port int 487 var rw redirectingWriter 488 489 stopServer := func() {} 490 startServer := func() error { 491 args := []string{ 492 "-e", r.evalScript("server"), // Evaluate 'eval'. 493 "--", 494 // src/common/runtime/helper/sys.ts expects 'node file.js <args>' 495 // and slices away the first two arguments. When running with '-e', args 496 // start at 1, so just inject a dummy argument. 497 "dummy-arg", 498 // Actual arguments begin here 499 "--gpu-provider", r.dawnNode, 500 } 501 for _, f := range r.flags { 502 args = append(args, "--gpu-provider-flag", f) 503 } 504 505 ctx := mainCtx 506 cmd := exec.CommandContext(ctx, r.node, args...) 507 508 serverLog := &bytes.Buffer{} 509 510 pl := newPortListener() 511 512 cmd.Dir = r.cts 513 cmd.Stdout = io.MultiWriter(&rw, serverLog, &pl) 514 cmd.Stderr = io.MultiWriter(&rw, serverLog) 515 516 err := cmd.Start() 517 if err != nil { 518 return fmt.Errorf("failed to start test runner server: %v", err) 519 } 520 521 select { 522 case port = <-pl.port: 523 case <-time.After(time.Second * 10): 524 return fmt.Errorf("timeout waiting for server port:\n%v", serverLog.String()) 525 case <-ctx.Done(): 526 return ctx.Err() 527 } 528 529 return nil 530 } 531 stopServer = func() { 532 if port > 0 { 533 go http.Post(fmt.Sprintf("http://localhost:%v/terminate", port), "", &bytes.Buffer{}) 534 time.Sleep(time.Millisecond * 100) 535 port = 0 536 } 537 } 538 539 for idx := range caseIndices { 540 // Redirect the server log per test case 541 caseServerLog := &bytes.Buffer{} 542 rw.Writer = caseServerLog 543 544 if port == 0 { 545 if err := startServer(); err != nil { 546 return err 547 } 548 } 549 550 res := result{index: idx, testcase: r.testcases[idx]} 551 552 type Response struct { 553 Status string 554 Message string 555 } 556 postResp, err := http.Post(fmt.Sprintf("http://localhost:%v/run?%v", port, r.testcases[idx]), "", &bytes.Buffer{}) 557 if err != nil { 558 res.error = fmt.Errorf("server POST failure. Restarting server...") 559 res.status = fail 560 results <- res 561 stopServer() 562 continue 563 } 564 565 if postResp.StatusCode == http.StatusOK { 566 var resp Response 567 if err := json.NewDecoder(postResp.Body).Decode(&resp); err != nil { 568 res.error = fmt.Errorf("server response decode failure") 569 res.status = fail 570 results <- res 571 continue 572 } 573 574 switch resp.Status { 575 case "pass": 576 res.status = pass 577 res.message = resp.Message + caseServerLog.String() 578 case "warn": 579 res.status = warn 580 res.message = resp.Message + caseServerLog.String() 581 case "fail": 582 res.status = fail 583 res.message = resp.Message + caseServerLog.String() 584 case "skip": 585 res.status = skip 586 res.message = resp.Message + caseServerLog.String() 587 default: 588 res.status = fail 589 res.error = fmt.Errorf("unknown status: '%v'", resp.Status) 590 } 591 } else { 592 msg, err := ioutil.ReadAll(postResp.Body) 593 if err != nil { 594 msg = []byte(err.Error()) 595 } 596 res.status = fail 597 res.error = fmt.Errorf("server error: %v", string(msg)) 598 } 599 results <- res 600 } 601 602 stopServer() 603 return nil 604} 605 606// runParallelIsolated() calls the CTS command-line test runner to run each 607// testcase in a separate process. This reduces possibility of state leakage 608// between tests. 609// Up to r.numRunners tests will be run concurrently. 610func (r *runner) runParallelIsolated() error { 611 // Create a chan of test indices. 612 // This will be read by the test runner goroutines. 613 caseIndices := make(chan int, len(r.testcases)) 614 for i := range r.testcases { 615 caseIndices <- i 616 } 617 close(caseIndices) 618 619 // Create a chan for the test results. 620 // This will be written to by the test runner goroutines. 621 results := make(chan result, len(r.testcases)) 622 623 // Spin up the test runner goroutines 624 wg := &sync.WaitGroup{} 625 for i := 0; i < r.numRunners; i++ { 626 wg.Add(1) 627 go func() { 628 defer wg.Done() 629 for idx := range caseIndices { 630 res := r.runTestcase(r.testcases[idx]) 631 res.index = idx 632 results <- res 633 } 634 }() 635 } 636 637 r.streamResults(wg, results) 638 return nil 639} 640 641// streamResults reads from the chan 'results', printing the results in test-id 642// sequential order. Once the WaitGroup 'wg' is complete, streamResults() will 643// automatically close the 'results' chan. 644// Once all the results have been printed, a summary will be printed and the 645// function will return. 646func (r *runner) streamResults(wg *sync.WaitGroup, results chan result) { 647 // Create another goroutine to close the results chan when all the runner 648 // goroutines have finished. 649 start := time.Now() 650 var timeTaken time.Duration 651 go func() { 652 wg.Wait() 653 timeTaken = time.Since(start) 654 close(results) 655 }() 656 657 // Total number of tests, test counts binned by status 658 numTests, numByStatus := len(r.testcases), map[status]int{} 659 660 // Helper function for printing a progress bar. 661 lastStatusUpdate, animFrame := time.Now(), 0 662 updateProgress := func() { 663 printANSIProgressBar(animFrame, numTests, numByStatus) 664 animFrame++ 665 lastStatusUpdate = time.Now() 666 } 667 668 // Pull test results as they become available. 669 // Update the status counts, and print any failures (or all test results if --verbose) 670 progressUpdateRate := time.Millisecond * 10 671 if !colors { 672 // No colors == no cursor control. Reduce progress updates so that 673 // we're not printing endless progress bars. 674 progressUpdateRate = time.Second 675 } 676 677 for res := range results { 678 r.log.logResults(res) 679 680 numByStatus[res.status] = numByStatus[res.status] + 1 681 name := res.testcase 682 if r.verbose || res.error != nil || (res.status != pass && res.status != skip) { 683 fmt.Printf("%v - %v: %v\n", name, res.status, res.message) 684 if res.error != nil { 685 fmt.Println(res.error) 686 } 687 updateProgress() 688 } 689 if time.Since(lastStatusUpdate) > progressUpdateRate { 690 updateProgress() 691 } 692 } 693 printANSIProgressBar(animFrame, numTests, numByStatus) 694 695 // All done. Print final stats. 696 fmt.Printf(` 697Completed in %v 698 699pass: %v (%v) 700fail: %v (%v) 701skip: %v (%v) 702timeout: %v (%v) 703`, 704 timeTaken, 705 numByStatus[pass], percentage(numByStatus[pass], numTests), 706 numByStatus[fail], percentage(numByStatus[fail], numTests), 707 numByStatus[skip], percentage(numByStatus[skip], numTests), 708 numByStatus[timeout], percentage(numByStatus[timeout], numTests), 709 ) 710} 711 712// runSerially() calls the CTS test runner to run the test query in a single 713// process. 714func (r *runner) runSerially(query string) error { 715 start := time.Now() 716 result := r.runTestcase(query) 717 timeTaken := time.Since(start) 718 719 if r.verbose { 720 fmt.Println(result) 721 } 722 fmt.Println("Status:", result.status) 723 fmt.Println("Completed in", timeTaken) 724 return nil 725} 726 727// status is an enumerator of test result status 728type status string 729 730const ( 731 pass status = "pass" 732 warn status = "warn" 733 fail status = "fail" 734 skip status = "skip" 735 timeout status = "timeout" 736) 737 738// result holds the information about a completed test 739type result struct { 740 index int 741 testcase string 742 status status 743 message string 744 error error 745} 746 747// runTestcase() runs the CTS testcase with the given query, returning the test 748// result. 749func (r *runner) runTestcase(query string) result { 750 ctx, cancel := context.WithTimeout(mainCtx, testTimeout) 751 defer cancel() 752 753 args := []string{ 754 "-e", r.evalScript("cmdline"), // Evaluate 'eval'. 755 "--", 756 // src/common/runtime/helper/sys.ts expects 'node file.js <args>' 757 // and slices away the first two arguments. When running with '-e', args 758 // start at 1, so just inject a dummy argument. 759 "dummy-arg", 760 // Actual arguments begin here 761 "--gpu-provider", r.dawnNode, 762 "--verbose", 763 } 764 for _, f := range r.flags { 765 args = append(args, "--gpu-provider-flag", f) 766 } 767 args = append(args, query) 768 769 cmd := exec.CommandContext(ctx, r.node, args...) 770 cmd.Dir = r.cts 771 772 var buf bytes.Buffer 773 cmd.Stdout = &buf 774 cmd.Stderr = &buf 775 776 err := cmd.Run() 777 msg := buf.String() 778 switch { 779 case errors.Is(err, context.DeadlineExceeded): 780 return result{testcase: query, status: timeout, message: msg} 781 case strings.Contains(msg, "[fail]"): 782 return result{testcase: query, status: fail, message: msg} 783 case strings.Contains(msg, "[warn]"): 784 return result{testcase: query, status: warn, message: msg} 785 case strings.Contains(msg, "[skip]"): 786 return result{testcase: query, status: skip, message: msg} 787 case strings.Contains(msg, "[pass]"), err == nil: 788 return result{testcase: query, status: pass, message: msg} 789 } 790 return result{testcase: query, status: fail, message: fmt.Sprint(msg, err), error: err} 791} 792 793// filterTestcases returns in with empty strings removed 794func filterTestcases(in []string) []string { 795 out := make([]string, 0, len(in)) 796 for _, c := range in { 797 if c != "" { 798 out = append(out, c) 799 } 800 } 801 return out 802} 803 804// percentage returns the percentage of n out of total as a string 805func percentage(n, total int) string { 806 if total == 0 { 807 return "-" 808 } 809 f := float64(n) / float64(total) 810 return fmt.Sprintf("%.1f%c", f*100.0, '%') 811} 812 813// isDir returns true if the path resolves to a directory 814func isDir(path string) bool { 815 s, err := os.Stat(path) 816 if err != nil { 817 return false 818 } 819 return s.IsDir() 820} 821 822// isFile returns true if the path resolves to a file 823func isFile(path string) bool { 824 s, err := os.Stat(path) 825 if err != nil { 826 return false 827 } 828 return !s.IsDir() 829} 830 831// printANSIProgressBar prints a colored progress bar, providing realtime 832// information about the status of the CTS run. 833// Note: We'll want to skip this if !isatty or if we're running on windows. 834func printANSIProgressBar(animFrame int, numTests int, numByStatus map[status]int) { 835 const ( 836 barWidth = 50 837 838 escape = "\u001B[" 839 positionLeft = escape + "0G" 840 red = escape + "31m" 841 green = escape + "32m" 842 yellow = escape + "33m" 843 blue = escape + "34m" 844 magenta = escape + "35m" 845 cyan = escape + "36m" 846 white = escape + "37m" 847 reset = escape + "0m" 848 ) 849 850 animSymbols := []rune{'⣾', '⣽', '⣻', '⢿', '⡿', '⣟', '⣯', '⣷'} 851 blockSymbols := []rune{'▏', '▎', '▍', '▌', '▋', '▊', '▉'} 852 853 numBlocksPrinted := 0 854 855 fmt.Fprint(stdout, string(animSymbols[animFrame%len(animSymbols)]), " [") 856 animFrame++ 857 858 numFinished := 0 859 860 for _, ty := range []struct { 861 status status 862 color string 863 }{{pass, green}, {warn, yellow}, {skip, blue}, {timeout, yellow}, {fail, red}} { 864 num := numByStatus[ty.status] 865 numFinished += num 866 statusFrac := float64(num) / float64(numTests) 867 fNumBlocks := barWidth * statusFrac 868 fmt.Fprint(stdout, ty.color) 869 numBlocks := int(math.Ceil(fNumBlocks)) 870 if numBlocks > 1 { 871 fmt.Print(strings.Repeat(string("▉"), numBlocks)) 872 } 873 if numBlocks > 0 { 874 frac := fNumBlocks - math.Floor(fNumBlocks) 875 symbol := blockSymbols[int(math.Round(frac*float64(len(blockSymbols)-1)))] 876 fmt.Print(string(symbol)) 877 } 878 numBlocksPrinted += numBlocks 879 } 880 881 if barWidth > numBlocksPrinted { 882 fmt.Print(strings.Repeat(string(" "), barWidth-numBlocksPrinted)) 883 } 884 fmt.Fprint(stdout, reset) 885 fmt.Print("] ", percentage(numFinished, numTests)) 886 887 if colors { 888 // move cursor to start of line so the bar is overridden 889 fmt.Fprint(stdout, positionLeft) 890 } else { 891 // cannot move cursor, so newline 892 fmt.Println() 893 } 894} 895