github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/cmd/testfilter/main.go (about)

     1  // Copyright 2019 The Cockroach Authors.
     2  //
     3  // Use of this software is governed by the Business Source License
     4  // included in the file licenses/BSL.txt.
     5  //
     6  // As of the Change Date specified in that file, in accordance with
     7  // the Business Source License, use of this software will be governed
     8  // by the Apache License, Version 2.0, included in the file
     9  // licenses/APL.txt.
    10  
    11  // testfilter is a utility to manipulate JSON streams in [test2json] format.
    12  // Standard input is read and each line starting with `{` and ending with `}`
    13  // parsed (and expected to parse successfully). Lines not matching this pattern
    14  // are classified as output not related to the test and, depending on the args
    15  // passed to `testfilter`, are passed through or removed. The arguments available
    16  // are `--mode=(strip|omit|convert)`, where:
    17  //
    18  // strip: omit output for non-failing tests, pass everything else through. In
    19  //   particular, non-test output and tests that never terminate are passed through.
    20  // omit: print only failing tests. Note that test2json does not close scopes for
    21  //   tests that are running in parallel (in the same package) with a "foreground"
    22  //   test that panics, so it will pass through *only* the one foreground test.
    23  //   Note also that package scopes are omitted; test2json does not reliably close
    24  //   them on panic/Exit anyway.
    25  // convert:
    26  //   no filtering is performed, but any test2json input is translated back into
    27  //   its pure Go test framework text representation. This is useful for output
    28  //   intended for human eyes.
    29  //
    30  // [test2json]: https://golang.org/cmd/test2json/
    31  package main
    32  
    33  import (
    34  	"bufio"
    35  	"encoding/json"
    36  	"flag"
    37  	"fmt"
    38  	"io"
    39  	"os"
    40  	"strings"
    41  	"time"
    42  
    43  	"github.com/cockroachdb/errors"
    44  )
    45  
    46  const modeUsage = `strip:
    47    omit output for non-failing tests, but print run/pass/skip events for all tests
    48  omit:
    49    only emit failing tests
    50  convert:
    51    don't perform any filtering, simply convert the json back to original test format'
    52  `
    53  
    54  type modeT byte
    55  
    56  const (
    57  	modeStrip modeT = iota
    58  	modeOmit
    59  	modeConvert
    60  )
    61  
    62  func (m *modeT) Set(s string) error {
    63  	switch s {
    64  	case "strip":
    65  		*m = modeStrip
    66  	case "omit":
    67  		*m = modeOmit
    68  	case "convert":
    69  		*m = modeConvert
    70  	default:
    71  		return errors.New("unsupported mode")
    72  	}
    73  	return nil
    74  }
    75  
    76  func (m *modeT) String() string {
    77  	switch *m {
    78  	case modeStrip:
    79  		return "strip"
    80  	case modeOmit:
    81  		return "omit"
    82  	case modeConvert:
    83  		return "convert"
    84  	default:
    85  		return "unknown"
    86  	}
    87  }
    88  
    89  var modeVar = modeStrip
    90  
    91  func init() {
    92  	flag.Var(&modeVar, "mode", modeUsage)
    93  }
    94  
    95  type testEvent struct {
    96  	Time    time.Time // encodes as an RFC3339-format string
    97  	Action  string
    98  	Package string
    99  	Test    string
   100  	Elapsed float64 // seconds
   101  	Output  string
   102  }
   103  
   104  func main() {
   105  	flag.Parse()
   106  	if err := filter(os.Stdin, os.Stdout, modeVar); err != nil {
   107  		fmt.Fprintln(os.Stderr, err)
   108  		os.Exit(1)
   109  	}
   110  }
   111  
   112  func filter(in io.Reader, out io.Writer, mode modeT) error {
   113  	scanner := bufio.NewScanner(in)
   114  	type tup struct {
   115  		pkg  string
   116  		test string
   117  	}
   118  	type ent struct {
   119  		first, last     string // RUN and (SKIP|PASS|FAIL)
   120  		strings.Builder        // output
   121  	}
   122  	m := map[tup]*ent{}
   123  	ev := &testEvent{}
   124  	var n int               // number of JSON lines parsed
   125  	var passFailLine string // catch common error of piping non-json test output in
   126  	for scanner.Scan() {
   127  		line := scanner.Text() // has no EOL marker
   128  		if len(line) <= 2 || line[0] != '{' || line[len(line)-1] != '}' {
   129  			// Not test2json output, pass it through except in `omit` mode.
   130  			// It's important that we still see build errors etc when running
   131  			// in -mode=strip.
   132  			if passFailLine == "" && (strings.Contains(line, "PASS") || strings.Contains(line, "FAIL")) {
   133  				passFailLine = line
   134  			}
   135  			if mode != modeOmit {
   136  				fmt.Fprintln(out, line)
   137  			}
   138  			continue
   139  		}
   140  		*ev = testEvent{}
   141  		if err := json.Unmarshal([]byte(line), ev); err != nil {
   142  			return err
   143  		}
   144  		n++
   145  
   146  		if mode == modeConvert {
   147  			if ev.Action == "output" {
   148  				fmt.Fprint(out, ev.Output)
   149  			}
   150  			continue
   151  		}
   152  
   153  		if ev.Test == "" {
   154  			// Skip all package output when omitting. Unfortunately package
   155  			// events aren't always well-formed. For example, if a test panics,
   156  			// the package will never receive a fail event (arguably a bug), so
   157  			// it's not trivial to print only failing packages. Besides, there's
   158  			// not much package output typically (only init functions), so not
   159  			// worth getting fancy about.
   160  			if mode == modeOmit {
   161  				continue
   162  			}
   163  		}
   164  		key := tup{ev.Package, ev.Test}
   165  		buf := m[key]
   166  		if buf == nil {
   167  			buf = &ent{first: line}
   168  			m[key] = buf
   169  		}
   170  		if _, err := fmt.Fprintln(buf, line); err != nil {
   171  			return err
   172  		}
   173  		switch ev.Action {
   174  		case "pass", "skip", "fail":
   175  			buf.last = line
   176  			delete(m, key)
   177  			if ev.Action == "fail" {
   178  				fmt.Fprint(out, buf.String())
   179  			} else if mode == modeStrip {
   180  				// Output only the start and end of test so that we preserve the
   181  				// timing information. However, the output is omitted.
   182  				fmt.Fprintln(out, buf.first)
   183  				fmt.Fprintln(out, buf.last)
   184  			}
   185  		case "run", "pause", "cont", "bench", "output":
   186  		default:
   187  			// We must have parsed some JSON that wasn't a testData.
   188  			return fmt.Errorf("unknown input: %s", line)
   189  		}
   190  	}
   191  	// Some scopes might still be open. To the best of my knowledge, this is due
   192  	// to a panic/premature exit of a test binary. In that case, it seems that
   193  	// neither is the package scope closed, nor the scopes for any tests that
   194  	// were running in parallel, so we pass that through if stripping, but not
   195  	// when omitting.
   196  	if mode == modeStrip {
   197  		for key := range m {
   198  			fmt.Fprintln(out, m[key].String())
   199  		}
   200  	}
   201  	// TODO(tbg): would like to return an error here for sanity, but the
   202  	// JSON just isn't well-formed all the time. For example, at the time
   203  	// of writing, here's a repro:
   204  	// make benchshort PKG=./pkg/bench BENCHES=BenchmarkIndexJoin 2>&1 | \
   205  	// testfilter -mode=strip
   206  	// Interestingly it works once we remove the `log.Scope(b).Close` in
   207  	// that test. Adding TESTFLAGS=-v doesn't matter apparently.
   208  	// if len(m) != 0 {
   209  	// 	return fmt.Errorf("%d tests did not terminate (a package likely exited prematurely)", len(m))
   210  	// }
   211  	if mode != modeConvert && n == 0 && passFailLine != "" {
   212  		// Without this, if the input to this command wasn't even JSON, we would
   213  		// pass. That's a mistake we should avoid at all costs. Note that even
   214  		// `go test -run - ./some/pkg` produces n>0 due to the start/pass events
   215  		// for the package, so if we're here then 100% something weird is going
   216  		// on.
   217  		return fmt.Errorf("not a single test was parsed, but detected test output: %s", passFailLine)
   218  	}
   219  	return nil
   220  }