dawn.node: Make run-cts --print-stdout print the run's stdout/err
Add a new muxWriter and use this for stdout writing to ensure that stdout printing does not tear. Bug: dawn:1123 Change-Id: Iec1ba16e5524c11bfc00ba38b3e4de3c06627fb1 Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/85366 Reviewed-by: Corentin Wallez <cwallez@chromium.org> Kokoro: Kokoro <noreply+kokoro@google.com> Commit-Queue: Ben Clayton <bclayton@google.com>
This commit is contained in:
parent
8c5cb62438
commit
48585f3c15
|
@ -30,7 +30,6 @@ import (
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
|
||||||
"runtime"
|
"runtime"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
@ -66,7 +65,6 @@ Usage:
|
||||||
|
|
||||||
var (
|
var (
|
||||||
colors bool
|
colors bool
|
||||||
stdout io.Writer
|
|
||||||
mainCtx context.Context
|
mainCtx context.Context
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -130,7 +128,7 @@ func run() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
var dawnNode, cts, node, npx, resultsPath, expectationsPath, logFilename, backend string
|
var dawnNode, cts, node, npx, resultsPath, expectationsPath, logFilename, backend string
|
||||||
var verbose, isolated, build bool
|
var printStdout, verbose, isolated, build bool
|
||||||
var numRunners int
|
var numRunners int
|
||||||
var flags dawnNodeFlags
|
var flags dawnNodeFlags
|
||||||
flag.StringVar(&dawnNode, "dawn-node", "", "path to dawn.node module")
|
flag.StringVar(&dawnNode, "dawn-node", "", "path to dawn.node module")
|
||||||
|
@ -139,6 +137,7 @@ func run() error {
|
||||||
flag.StringVar(&npx, "npx", "", "path to npx executable")
|
flag.StringVar(&npx, "npx", "", "path to npx executable")
|
||||||
flag.StringVar(&resultsPath, "output", "", "path to write test results file")
|
flag.StringVar(&resultsPath, "output", "", "path to write test results file")
|
||||||
flag.StringVar(&expectationsPath, "expect", "", "path to expectations file")
|
flag.StringVar(&expectationsPath, "expect", "", "path to expectations file")
|
||||||
|
flag.BoolVar(&printStdout, "print-stdout", false, "print the stdout and stderr from each test runner server")
|
||||||
flag.BoolVar(&verbose, "verbose", false, "print extra information while testing")
|
flag.BoolVar(&verbose, "verbose", false, "print extra information while testing")
|
||||||
flag.BoolVar(&build, "build", true, "attempt to build the CTS before running")
|
flag.BoolVar(&build, "build", true, "attempt to build the CTS before running")
|
||||||
flag.BoolVar(&isolated, "isolate", false, "run each test in an isolated process")
|
flag.BoolVar(&isolated, "isolate", false, "run each test in an isolated process")
|
||||||
|
@ -150,11 +149,14 @@ func run() error {
|
||||||
" set to 'vulkan' if VK_ICD_FILENAMES environment variable is set, 'default' otherwise")
|
" set to 'vulkan' if VK_ICD_FILENAMES environment variable is set, 'default' otherwise")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
|
// Create a thread-safe, color supporting stdout wrapper.
|
||||||
|
var stdout io.WriteCloser
|
||||||
if colors {
|
if colors {
|
||||||
stdout = colorable.NewColorableStdout()
|
stdout = newMuxWriter(colorable.NewColorableStdout())
|
||||||
} else {
|
} else {
|
||||||
stdout = colorable.NewNonColorable(os.Stdout)
|
stdout = newMuxWriter(colorable.NewNonColorable(os.Stdout))
|
||||||
}
|
}
|
||||||
|
defer stdout.Close() // Required to flush the mux chan
|
||||||
|
|
||||||
// Check mandatory arguments
|
// Check mandatory arguments
|
||||||
if dawnNode == "" || cts == "" {
|
if dawnNode == "" || cts == "" {
|
||||||
|
@ -201,7 +203,7 @@ func run() error {
|
||||||
|
|
||||||
// Forward the backend to use, if specified.
|
// Forward the backend to use, if specified.
|
||||||
if backend != "default" {
|
if backend != "default" {
|
||||||
fmt.Println("Forcing backend to", backend)
|
fmt.Fprintln(stdout, "Forcing backend to", backend)
|
||||||
flags = append(flags, fmt.Sprint("dawn-backend=", backend))
|
flags = append(flags, fmt.Sprint("dawn-backend=", backend))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -218,17 +220,19 @@ func run() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
r := runner{
|
r := runner{
|
||||||
numRunners: numRunners,
|
numRunners: numRunners,
|
||||||
verbose: verbose,
|
printStdout: printStdout,
|
||||||
node: node,
|
verbose: verbose,
|
||||||
npx: npx,
|
node: node,
|
||||||
dawnNode: dawnNode,
|
npx: npx,
|
||||||
cts: cts,
|
dawnNode: dawnNode,
|
||||||
flags: flags,
|
cts: cts,
|
||||||
results: testcaseStatuses{},
|
flags: flags,
|
||||||
|
results: testcaseStatuses{},
|
||||||
evalScript: func(main string) string {
|
evalScript: func(main string) string {
|
||||||
return fmt.Sprintf(`require('./src/common/tools/setup-ts-in-node.js');require('./src/common/runtime/%v.ts');`, main)
|
return fmt.Sprintf(`require('./src/common/tools/setup-ts-in-node.js');require('./src/common/runtime/%v.ts');`, main)
|
||||||
},
|
},
|
||||||
|
stdout: stdout,
|
||||||
}
|
}
|
||||||
|
|
||||||
if logFilename != "" {
|
if logFilename != "" {
|
||||||
|
@ -243,7 +247,7 @@ func run() error {
|
||||||
cache := cache{}
|
cache := cache{}
|
||||||
cachePath := dawnNode + ".runcts.cache"
|
cachePath := dawnNode + ".runcts.cache"
|
||||||
if err := cache.load(cachePath); err != nil && verbose {
|
if err := cache.load(cachePath); err != nil && verbose {
|
||||||
fmt.Println("failed to load cache from", cachePath, err)
|
fmt.Fprintln(stdout, "failed to load cache from", cachePath, err)
|
||||||
}
|
}
|
||||||
defer cache.save(cachePath)
|
defer cache.save(cachePath)
|
||||||
|
|
||||||
|
@ -257,7 +261,7 @@ func run() error {
|
||||||
!isDir(filepath.Join(r.cts, "out-node"))
|
!isDir(filepath.Join(r.cts, "out-node"))
|
||||||
if build {
|
if build {
|
||||||
if verbose {
|
if verbose {
|
||||||
fmt.Println("CTS needs rebuild:", ctsNeedsRebuild)
|
fmt.Fprintln(stdout, "CTS needs rebuild:", ctsNeedsRebuild)
|
||||||
}
|
}
|
||||||
|
|
||||||
if npx != "" {
|
if npx != "" {
|
||||||
|
@ -272,7 +276,7 @@ func run() error {
|
||||||
return fmt.Sprintf(`require('./out-node/common/runtime/%v.js');`, main)
|
return fmt.Sprintf(`require('./out-node/common/runtime/%v.js');`, main)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
fmt.Println("npx not found on PATH. Using runtime TypeScript transpilation (slow)")
|
fmt.Fprintln(stdout, "npx not found on PATH. Using runtime TypeScript transpilation (slow)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -292,20 +296,20 @@ func run() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
if isolated {
|
if isolated {
|
||||||
fmt.Println("Running in parallel isolated...")
|
fmt.Fprintln(stdout, "Running in parallel isolated...")
|
||||||
fmt.Printf("Testing %d test cases...\n", len(r.testcases))
|
fmt.Fprintf(stdout, "Testing %d test cases...\n", len(r.testcases))
|
||||||
if err := r.runParallelIsolated(); err != nil {
|
if err := r.runParallelIsolated(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
fmt.Println("Running in parallel with server...")
|
fmt.Fprintln(stdout, "Running in parallel with server...")
|
||||||
fmt.Printf("Testing %d test cases...\n", len(r.testcases))
|
fmt.Fprintf(stdout, "Testing %d test cases...\n", len(r.testcases))
|
||||||
if err := r.runParallelWithServer(); err != nil {
|
if err := r.runParallelWithServer(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
fmt.Println("Running serially...")
|
fmt.Fprintln(stdout, "Running serially...")
|
||||||
if err := r.runSerially(query); err != nil {
|
if err := r.runSerially(query); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -375,6 +379,7 @@ func (c *cache) save(path string) error {
|
||||||
|
|
||||||
type runner struct {
|
type runner struct {
|
||||||
numRunners int
|
numRunners int
|
||||||
|
printStdout bool
|
||||||
verbose bool
|
verbose bool
|
||||||
node, npx, dawnNode, cts string
|
node, npx, dawnNode, cts string
|
||||||
flags dawnNodeFlags
|
flags dawnNodeFlags
|
||||||
|
@ -383,6 +388,7 @@ type runner struct {
|
||||||
expectations testcaseStatuses
|
expectations testcaseStatuses
|
||||||
results testcaseStatuses
|
results testcaseStatuses
|
||||||
log logger
|
log logger
|
||||||
|
stdout io.WriteCloser
|
||||||
}
|
}
|
||||||
|
|
||||||
// scanSourceTimestamps scans all the .js and .ts files in all subdirectories of
|
// scanSourceTimestamps scans all the .js and .ts files in all subdirectories of
|
||||||
|
@ -390,9 +396,9 @@ type runner struct {
|
||||||
func (r *runner) scanSourceTimestamps(verbose bool) (time.Time, error) {
|
func (r *runner) scanSourceTimestamps(verbose bool) (time.Time, error) {
|
||||||
if verbose {
|
if verbose {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
fmt.Println("Scanning .js / .ts files for changes...")
|
fmt.Fprintln(r.stdout, "Scanning .js / .ts files for changes...")
|
||||||
defer func() {
|
defer func() {
|
||||||
fmt.Println("completed in", time.Since(start))
|
fmt.Fprintln(r.stdout, "completed in", time.Since(start))
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -420,9 +426,9 @@ func (r *runner) scanSourceTimestamps(verbose bool) (time.Time, error) {
|
||||||
func (r *runner) buildCTS(verbose bool) error {
|
func (r *runner) buildCTS(verbose bool) error {
|
||||||
if verbose {
|
if verbose {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
fmt.Println("Building CTS...")
|
fmt.Fprintln(r.stdout, "Building CTS...")
|
||||||
defer func() {
|
defer func() {
|
||||||
fmt.Println("completed in", time.Since(start))
|
fmt.Fprintln(r.stdout, "completed in", time.Since(start))
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -440,9 +446,9 @@ func (r *runner) buildCTS(verbose bool) error {
|
||||||
func (r *runner) gatherTestCases(query string, verbose bool) error {
|
func (r *runner) gatherTestCases(query string, verbose bool) error {
|
||||||
if verbose {
|
if verbose {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
fmt.Println("Gathering test cases...")
|
fmt.Fprintln(r.stdout, "Gathering test cases...")
|
||||||
defer func() {
|
defer func() {
|
||||||
fmt.Println("completed in", time.Since(start))
|
fmt.Fprintln(r.stdout, "completed in", time.Since(start))
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -468,30 +474,80 @@ func (r *runner) gatherTestCases(query string, verbose bool) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// portListener implements io.Writer, monitoring written messages until a port
|
||||||
|
// is printed between '[[' ']]'. Once the port has been found, the parsed
|
||||||
|
// port number is written to the 'port' chan, and all subsequent writes are
|
||||||
|
// forwarded to writer.
|
||||||
type portListener struct {
|
type portListener struct {
|
||||||
|
writer io.Writer
|
||||||
buffer strings.Builder
|
buffer strings.Builder
|
||||||
port chan int
|
port chan int
|
||||||
}
|
}
|
||||||
|
|
||||||
func newPortListener() portListener {
|
func newPortListener(w io.Writer) portListener {
|
||||||
return portListener{strings.Builder{}, make(chan int)}
|
return portListener{w, strings.Builder{}, make(chan int)}
|
||||||
}
|
}
|
||||||
|
|
||||||
var portRE = regexp.MustCompile(`\[\[(\d+)\]\]`)
|
|
||||||
|
|
||||||
func (p *portListener) Write(data []byte) (n int, err error) {
|
func (p *portListener) Write(data []byte) (n int, err error) {
|
||||||
if p.port != nil {
|
if p.port != nil {
|
||||||
p.buffer.Write(data)
|
p.buffer.Write(data)
|
||||||
match := portRE.FindStringSubmatch(p.buffer.String())
|
str := p.buffer.String()
|
||||||
if len(match) == 2 {
|
|
||||||
port, err := strconv.Atoi(match[1])
|
idx := strings.Index(str, "[[")
|
||||||
if err != nil {
|
if idx < 0 {
|
||||||
return 0, err
|
// Still waiting for the opening '[['
|
||||||
}
|
return len(data), nil
|
||||||
p.port <- port
|
|
||||||
close(p.port)
|
|
||||||
p.port = nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
str = str[idx+2:] // skip past '[['
|
||||||
|
idx = strings.Index(str, "]]")
|
||||||
|
if idx < 0 {
|
||||||
|
// Still waiting for the closing ']]'
|
||||||
|
return len(data), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
port, err := strconv.Atoi(str[:idx])
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Port found. Write it to the chan, and close the chan.
|
||||||
|
p.port <- port
|
||||||
|
close(p.port)
|
||||||
|
p.port = nil
|
||||||
|
|
||||||
|
str = strings.TrimRight(str[idx+2:], " \n")
|
||||||
|
if len(str) == 0 {
|
||||||
|
return len(data), nil
|
||||||
|
}
|
||||||
|
// Write out trailing text after the ']]'
|
||||||
|
return p.writer.Write([]byte(str))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Port has been found. Just forward the rest of the data to p.writer
|
||||||
|
return p.writer.Write(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// prefixWriter is an io.Writer that prefixes each write with a prefix string
|
||||||
|
type prefixWriter struct {
|
||||||
|
prefix string
|
||||||
|
writer io.Writer
|
||||||
|
midLine bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *prefixWriter) Write(data []byte) (int, error) {
|
||||||
|
lines := strings.Split(string(data), "\n")
|
||||||
|
buf := strings.Builder{}
|
||||||
|
for i, line := range lines {
|
||||||
|
if line == "" && i == len(lines)-1 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
buf.WriteString(p.prefix)
|
||||||
|
buf.WriteString(line)
|
||||||
|
buf.WriteString("\n")
|
||||||
|
}
|
||||||
|
if _, err := p.writer.Write([]byte(buf.String())); err != nil {
|
||||||
|
return 0, err
|
||||||
}
|
}
|
||||||
return len(data), nil
|
return len(data), nil
|
||||||
}
|
}
|
||||||
|
@ -514,10 +570,11 @@ func (r *runner) runParallelWithServer() error {
|
||||||
// Spin up the test runner goroutines
|
// Spin up the test runner goroutines
|
||||||
wg := &sync.WaitGroup{}
|
wg := &sync.WaitGroup{}
|
||||||
for i := 0; i < r.numRunners; i++ {
|
for i := 0; i < r.numRunners; i++ {
|
||||||
|
id := i
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
if err := r.runServer(caseIndices, results); err != nil {
|
if err := r.runServer(id, caseIndices, results); err != nil {
|
||||||
results <- result{
|
results <- result{
|
||||||
status: fail,
|
status: fail,
|
||||||
error: fmt.Errorf("Test server error: %w", err),
|
error: fmt.Errorf("Test server error: %w", err),
|
||||||
|
@ -530,18 +587,14 @@ func (r *runner) runParallelWithServer() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type redirectingWriter struct {
|
|
||||||
io.Writer
|
|
||||||
}
|
|
||||||
|
|
||||||
// runServer starts a test runner server instance, takes case indices from
|
// runServer starts a test runner server instance, takes case indices from
|
||||||
// caseIndices, and requests the server run the test with the given index.
|
// caseIndices, and requests the server run the test with the given index.
|
||||||
// The result of the test run is written to the results chan.
|
// The result of the test run is written to the results chan.
|
||||||
// Once the caseIndices chan has been closed, the server is stopped and
|
// Once the caseIndices chan has been closed, the server is stopped and
|
||||||
// runServer returns.
|
// runServer returns.
|
||||||
func (r *runner) runServer(caseIndices <-chan int, results chan<- result) error {
|
func (r *runner) runServer(id int, caseIndices <-chan int, results chan<- result) error {
|
||||||
var port int
|
var port int
|
||||||
var rw redirectingWriter
|
testCaseLog := &bytes.Buffer{}
|
||||||
|
|
||||||
stopServer := func() {}
|
stopServer := func() {}
|
||||||
startServer := func() error {
|
startServer := func() error {
|
||||||
|
@ -562,13 +615,20 @@ func (r *runner) runServer(caseIndices <-chan int, results chan<- result) error
|
||||||
ctx := mainCtx
|
ctx := mainCtx
|
||||||
cmd := exec.CommandContext(ctx, r.node, args...)
|
cmd := exec.CommandContext(ctx, r.node, args...)
|
||||||
|
|
||||||
serverLog := &bytes.Buffer{}
|
writer := io.Writer(testCaseLog)
|
||||||
|
if r.printStdout {
|
||||||
|
pw := &prefixWriter{
|
||||||
|
prefix: fmt.Sprintf("[%d] ", id),
|
||||||
|
writer: r.stdout,
|
||||||
|
}
|
||||||
|
writer = io.MultiWriter(pw, writer)
|
||||||
|
}
|
||||||
|
|
||||||
pl := newPortListener()
|
pl := newPortListener(writer)
|
||||||
|
|
||||||
cmd.Dir = r.cts
|
cmd.Dir = r.cts
|
||||||
cmd.Stdout = io.MultiWriter(&rw, serverLog, &pl)
|
cmd.Stdout = &pl
|
||||||
cmd.Stderr = io.MultiWriter(&rw, serverLog)
|
cmd.Stderr = &pl
|
||||||
|
|
||||||
err := cmd.Start()
|
err := cmd.Start()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -577,9 +637,10 @@ func (r *runner) runServer(caseIndices <-chan int, results chan<- result) error
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case port = <-pl.port:
|
case port = <-pl.port:
|
||||||
|
return nil // success
|
||||||
case <-time.After(time.Second * 10):
|
case <-time.After(time.Second * 10):
|
||||||
return fmt.Errorf("timeout waiting for server port:\n%v", serverLog.String())
|
return fmt.Errorf("timeout waiting for server port:\n%v", pl.buffer.String())
|
||||||
case <-ctx.Done():
|
case <-ctx.Done(): // cancelled
|
||||||
return ctx.Err()
|
return ctx.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -594,9 +655,7 @@ func (r *runner) runServer(caseIndices <-chan int, results chan<- result) error
|
||||||
}
|
}
|
||||||
|
|
||||||
for idx := range caseIndices {
|
for idx := range caseIndices {
|
||||||
// Redirect the server log per test case
|
testCaseLog.Reset() // Clear the log for this test case
|
||||||
caseServerLog := &bytes.Buffer{}
|
|
||||||
rw.Writer = caseServerLog
|
|
||||||
|
|
||||||
if port == 0 {
|
if port == 0 {
|
||||||
if err := startServer(); err != nil {
|
if err := startServer(); err != nil {
|
||||||
|
@ -631,16 +690,16 @@ func (r *runner) runServer(caseIndices <-chan int, results chan<- result) error
|
||||||
switch resp.Status {
|
switch resp.Status {
|
||||||
case "pass":
|
case "pass":
|
||||||
res.status = pass
|
res.status = pass
|
||||||
res.message = resp.Message + caseServerLog.String()
|
res.message = resp.Message + testCaseLog.String()
|
||||||
case "warn":
|
case "warn":
|
||||||
res.status = warn
|
res.status = warn
|
||||||
res.message = resp.Message + caseServerLog.String()
|
res.message = resp.Message + testCaseLog.String()
|
||||||
case "fail":
|
case "fail":
|
||||||
res.status = fail
|
res.status = fail
|
||||||
res.message = resp.Message + caseServerLog.String()
|
res.message = resp.Message + testCaseLog.String()
|
||||||
case "skip":
|
case "skip":
|
||||||
res.status = skip
|
res.status = skip
|
||||||
res.message = resp.Message + caseServerLog.String()
|
res.message = resp.Message + testCaseLog.String()
|
||||||
default:
|
default:
|
||||||
res.status = fail
|
res.status = fail
|
||||||
res.error = fmt.Errorf("unknown status: '%v'", resp.Status)
|
res.error = fmt.Errorf("unknown status: '%v'", resp.Status)
|
||||||
|
@ -717,7 +776,7 @@ func (r *runner) streamResults(wg *sync.WaitGroup, results chan result) {
|
||||||
// Helper function for printing a progress bar.
|
// Helper function for printing a progress bar.
|
||||||
lastStatusUpdate, animFrame := time.Now(), 0
|
lastStatusUpdate, animFrame := time.Now(), 0
|
||||||
updateProgress := func() {
|
updateProgress := func() {
|
||||||
printANSIProgressBar(animFrame, numTests, numByExpectedStatus)
|
fmt.Fprint(r.stdout, ansiProgressBar(animFrame, numTests, numByExpectedStatus))
|
||||||
animFrame++
|
animFrame++
|
||||||
lastStatusUpdate = time.Now()
|
lastStatusUpdate = time.Now()
|
||||||
}
|
}
|
||||||
|
@ -744,24 +803,32 @@ func (r *runner) streamResults(wg *sync.WaitGroup, results chan result) {
|
||||||
if r.verbose ||
|
if r.verbose ||
|
||||||
res.error != nil ||
|
res.error != nil ||
|
||||||
(exStatus.status != pass && exStatus.status != skip && !exStatus.expected) {
|
(exStatus.status != pass && exStatus.status != skip && !exStatus.expected) {
|
||||||
fmt.Printf("%v - %v: %v", name, res.status, res.message)
|
buf := &bytes.Buffer{}
|
||||||
|
fmt.Fprint(buf, statusColor[res.status])
|
||||||
|
if res.message != "" {
|
||||||
|
fmt.Fprintf(buf, "%v - %v:\n%v", name, res.status, res.message)
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(buf, "%v - %v", name, res.status)
|
||||||
|
}
|
||||||
if expected != "" {
|
if expected != "" {
|
||||||
fmt.Printf(" [%v -> %v]", expected, res.status)
|
fmt.Fprintf(buf, " [%v -> %v]", expected, res.status)
|
||||||
}
|
}
|
||||||
fmt.Println()
|
fmt.Fprintln(buf)
|
||||||
if res.error != nil {
|
if res.error != nil {
|
||||||
fmt.Println(res.error)
|
fmt.Fprintln(buf, res.error)
|
||||||
}
|
}
|
||||||
|
fmt.Fprint(buf, ansiReset)
|
||||||
|
fmt.Fprint(r.stdout, buf.String())
|
||||||
updateProgress()
|
updateProgress()
|
||||||
}
|
}
|
||||||
if time.Since(lastStatusUpdate) > progressUpdateRate {
|
if time.Since(lastStatusUpdate) > progressUpdateRate {
|
||||||
updateProgress()
|
updateProgress()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
printANSIProgressBar(animFrame, numTests, numByExpectedStatus)
|
fmt.Fprint(r.stdout, ansiProgressBar(animFrame, numTests, numByExpectedStatus))
|
||||||
|
|
||||||
// All done. Print final stats.
|
// All done. Print final stats.
|
||||||
fmt.Printf("\nCompleted in %v\n", timeTaken)
|
fmt.Fprintf(r.stdout, "\nCompleted in %v\n", timeTaken)
|
||||||
|
|
||||||
var numExpectedByStatus map[status]int
|
var numExpectedByStatus map[status]int
|
||||||
if r.expectations != nil {
|
if r.expectations != nil {
|
||||||
|
@ -788,22 +855,22 @@ func (r *runner) streamResults(wg *sync.WaitGroup, results chan result) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Print(bold, statusColor[s])
|
fmt.Fprint(r.stdout, bold, statusColor[s])
|
||||||
fmt.Print(alignRight(strings.ToUpper(string(s))+": ", 10))
|
fmt.Fprint(r.stdout, alignRight(strings.ToUpper(string(s))+": ", 10))
|
||||||
fmt.Print(ansiReset)
|
fmt.Fprint(r.stdout, ansiReset)
|
||||||
if numByStatus > 0 {
|
if numByStatus > 0 {
|
||||||
fmt.Print(bold)
|
fmt.Fprint(r.stdout, bold)
|
||||||
}
|
}
|
||||||
fmt.Print(alignLeft(numByStatus, 10))
|
fmt.Fprint(r.stdout, alignLeft(numByStatus, 10))
|
||||||
fmt.Print(ansiReset)
|
fmt.Fprint(r.stdout, ansiReset)
|
||||||
fmt.Print(alignRight("("+percentage(numByStatus, numTests)+")", 6))
|
fmt.Fprint(r.stdout, alignRight("("+percentage(numByStatus, numTests)+")", 6))
|
||||||
|
|
||||||
if diffFromExpected != 0 {
|
if diffFromExpected != 0 {
|
||||||
fmt.Print(bold, " [")
|
fmt.Fprint(r.stdout, bold, " [")
|
||||||
fmt.Printf("%+d", diffFromExpected)
|
fmt.Fprintf(r.stdout, "%+d", diffFromExpected)
|
||||||
fmt.Print(ansiReset, "]")
|
fmt.Fprint(r.stdout, ansiReset, "]")
|
||||||
}
|
}
|
||||||
fmt.Println()
|
fmt.Fprintln(r.stdout)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -817,10 +884,10 @@ func (r *runner) runSerially(query string) error {
|
||||||
timeTaken := time.Since(start)
|
timeTaken := time.Since(start)
|
||||||
|
|
||||||
if r.verbose {
|
if r.verbose {
|
||||||
fmt.Println(result)
|
fmt.Fprintln(r.stdout, result)
|
||||||
}
|
}
|
||||||
fmt.Println("Status:", result.status)
|
fmt.Fprintln(r.stdout, "Status:", result.status)
|
||||||
fmt.Println("Completed in", timeTaken)
|
fmt.Fprintln(r.stdout, "Completed in", timeTaken)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -968,10 +1035,10 @@ func alignRight(val interface{}, width int) string {
|
||||||
return strings.Repeat(" ", padding) + s
|
return strings.Repeat(" ", padding) + s
|
||||||
}
|
}
|
||||||
|
|
||||||
// printANSIProgressBar prints a colored progress bar, providing realtime
|
// ansiProgressBar returns a string with an ANSI-colored progress bar, providing
|
||||||
// information about the status of the CTS run.
|
// realtime information about the status of the CTS run.
|
||||||
// Note: We'll want to skip this if !isatty or if we're running on windows.
|
// Note: We'll want to skip this if !isatty or if we're running on windows.
|
||||||
func printANSIProgressBar(animFrame int, numTests int, numByExpectedStatus map[expectedStatus]int) {
|
func ansiProgressBar(animFrame int, numTests int, numByExpectedStatus map[expectedStatus]int) string {
|
||||||
const barWidth = 50
|
const barWidth = 50
|
||||||
|
|
||||||
animSymbols := []rune{'⣾', '⣽', '⣻', '⢿', '⡿', '⣟', '⣯', '⣷'}
|
animSymbols := []rune{'⣾', '⣽', '⣻', '⢿', '⡿', '⣟', '⣯', '⣷'}
|
||||||
|
@ -979,7 +1046,8 @@ func printANSIProgressBar(animFrame int, numTests int, numByExpectedStatus map[e
|
||||||
|
|
||||||
numBlocksPrinted := 0
|
numBlocksPrinted := 0
|
||||||
|
|
||||||
fmt.Fprint(stdout, string(animSymbols[animFrame%len(animSymbols)]), " [")
|
buf := &strings.Builder{}
|
||||||
|
fmt.Fprint(buf, string(animSymbols[animFrame%len(animSymbols)]), " [")
|
||||||
animFrame++
|
animFrame++
|
||||||
|
|
||||||
numFinished := 0
|
numFinished := 0
|
||||||
|
@ -995,20 +1063,20 @@ func printANSIProgressBar(animFrame int, numTests int, numByExpectedStatus map[e
|
||||||
numFinished += num
|
numFinished += num
|
||||||
statusFrac := float64(num) / float64(numTests)
|
statusFrac := float64(num) / float64(numTests)
|
||||||
fNumBlocks := barWidth * statusFrac
|
fNumBlocks := barWidth * statusFrac
|
||||||
fmt.Fprint(stdout, color)
|
fmt.Fprint(buf, color)
|
||||||
numBlocks := int(math.Ceil(fNumBlocks))
|
numBlocks := int(math.Ceil(fNumBlocks))
|
||||||
if expected {
|
if expected {
|
||||||
if numBlocks > 1 {
|
if numBlocks > 1 {
|
||||||
fmt.Print(strings.Repeat(string("░"), numBlocks))
|
fmt.Fprint(buf, strings.Repeat(string("░"), numBlocks))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if numBlocks > 1 {
|
if numBlocks > 1 {
|
||||||
fmt.Print(strings.Repeat(string("▉"), numBlocks))
|
fmt.Fprint(buf, strings.Repeat(string("▉"), numBlocks))
|
||||||
}
|
}
|
||||||
if numBlocks > 0 {
|
if numBlocks > 0 {
|
||||||
frac := fNumBlocks - math.Floor(fNumBlocks)
|
frac := fNumBlocks - math.Floor(fNumBlocks)
|
||||||
symbol := blockSymbols[int(math.Round(frac*float64(len(blockSymbols)-1)))]
|
symbol := blockSymbols[int(math.Round(frac*float64(len(blockSymbols)-1)))]
|
||||||
fmt.Print(string(symbol))
|
fmt.Fprint(buf, string(symbol))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
numBlocksPrinted += numBlocks
|
numBlocksPrinted += numBlocks
|
||||||
|
@ -1016,18 +1084,20 @@ func printANSIProgressBar(animFrame int, numTests int, numByExpectedStatus map[e
|
||||||
}
|
}
|
||||||
|
|
||||||
if barWidth > numBlocksPrinted {
|
if barWidth > numBlocksPrinted {
|
||||||
fmt.Print(strings.Repeat(string(" "), barWidth-numBlocksPrinted))
|
fmt.Fprint(buf, strings.Repeat(string(" "), barWidth-numBlocksPrinted))
|
||||||
}
|
}
|
||||||
fmt.Fprint(stdout, ansiReset)
|
fmt.Fprint(buf, ansiReset)
|
||||||
fmt.Print("] ", percentage(numFinished, numTests))
|
fmt.Fprint(buf, "] ", percentage(numFinished, numTests))
|
||||||
|
|
||||||
if colors {
|
if colors {
|
||||||
// move cursor to start of line so the bar is overridden
|
// move cursor to start of line so the bar is overridden
|
||||||
fmt.Fprint(stdout, positionLeft)
|
fmt.Fprint(buf, positionLeft)
|
||||||
} else {
|
} else {
|
||||||
// cannot move cursor, so newline
|
// cannot move cursor, so newline
|
||||||
fmt.Println()
|
fmt.Fprintln(buf)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return buf.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// testcaseStatus is a pair of testcase name and result status
|
// testcaseStatus is a pair of testcase name and result status
|
||||||
|
@ -1164,3 +1234,38 @@ func thisDir() string {
|
||||||
}
|
}
|
||||||
return filepath.Dir(file)
|
return filepath.Dir(file)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type muxWriter struct {
|
||||||
|
data chan []byte
|
||||||
|
err chan error
|
||||||
|
}
|
||||||
|
|
||||||
|
// muxWriter returns a thread-safe io.WriteCloser, that writes to w
|
||||||
|
func newMuxWriter(w io.Writer) *muxWriter {
|
||||||
|
m := muxWriter{
|
||||||
|
data: make(chan []byte, 256),
|
||||||
|
err: make(chan error, 1),
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
defer close(m.err)
|
||||||
|
for data := range m.data {
|
||||||
|
_, err := w.Write(data)
|
||||||
|
if err != nil {
|
||||||
|
m.err <- err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m.err <- nil
|
||||||
|
}()
|
||||||
|
return &m
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *muxWriter) Write(data []byte) (n int, err error) {
|
||||||
|
w.data <- append([]byte{}, data...)
|
||||||
|
return len(data), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *muxWriter) Close() error {
|
||||||
|
close(w.data)
|
||||||
|
return <-w.err
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue