gotest.tools/gotestsum@v1.11.0/cmd/rerunfails.go (about)

     1  package cmd
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"os"
     7  	"regexp"
     8  	"sort"
     9  	"strings"
    10  
    11  	"gotest.tools/gotestsum/testjson"
    12  )
    13  
    14  type rerunOpts struct {
    15  	runFlag string
    16  	pkg     string
    17  }
    18  
    19  func (o rerunOpts) Args() []string {
    20  	var result []string
    21  	if o.runFlag != "" {
    22  		result = append(result, o.runFlag)
    23  	}
    24  	if o.pkg != "" {
    25  		result = append(result, o.pkg)
    26  	}
    27  	return result
    28  }
    29  
    30  func newRerunOptsFromTestCase(tc testjson.TestCase) rerunOpts {
    31  	return rerunOpts{
    32  		runFlag: goTestRunFlagForTestCase(tc.Test),
    33  		pkg:     tc.Package,
    34  	}
    35  }
    36  
    37  type testCaseFilter func([]testjson.TestCase) []testjson.TestCase
    38  
    39  func rerunFailsFilter(o *options) testCaseFilter {
    40  	if o.rerunFailsRunRootCases {
    41  		return func(tcs []testjson.TestCase) []testjson.TestCase {
    42  			var result []testjson.TestCase
    43  			for _, tc := range tcs {
    44  				if !tc.Test.IsSubTest() {
    45  					result = append(result, tc)
    46  				}
    47  			}
    48  			return result
    49  		}
    50  	}
    51  	return testjson.FilterFailedUnique
    52  }
    53  
    54  func rerunFailed(ctx context.Context, opts *options, scanConfig testjson.ScanConfig) error {
    55  	ctx, cancel := context.WithCancel(ctx)
    56  	defer cancel()
    57  	tcFilter := rerunFailsFilter(opts)
    58  
    59  	rec := newFailureRecorderFromExecution(scanConfig.Execution)
    60  	for attempts := 0; rec.count() > 0 && attempts < opts.rerunFailsMaxAttempts; attempts++ {
    61  		testjson.PrintSummary(opts.stdout, scanConfig.Execution, testjson.SummarizeNone)
    62  		opts.stdout.Write([]byte("\n")) // nolint: errcheck
    63  
    64  		nextRec := newFailureRecorder(scanConfig.Handler)
    65  		for _, tc := range tcFilter(rec.failures) {
    66  			goTestProc, err := startGoTestFn(ctx, "", goTestCmdArgs(opts, newRerunOptsFromTestCase(tc)))
    67  			if err != nil {
    68  				return err
    69  			}
    70  
    71  			cfg := testjson.ScanConfig{
    72  				RunID:     attempts + 1,
    73  				Stdout:    goTestProc.stdout,
    74  				Stderr:    goTestProc.stderr,
    75  				Handler:   nextRec,
    76  				Execution: scanConfig.Execution,
    77  				Stop:      cancel,
    78  			}
    79  			if _, err := testjson.ScanTestOutput(cfg); err != nil {
    80  				return err
    81  			}
    82  			exitErr := goTestProc.cmd.Wait()
    83  			if exitErr != nil {
    84  				nextRec.lastErr = exitErr
    85  			}
    86  			if err := hasErrors(exitErr, scanConfig.Execution); err != nil {
    87  				return err
    88  			}
    89  		}
    90  		rec = nextRec
    91  	}
    92  	return rec.lastErr
    93  }
    94  
    95  // startGoTestFn is a shim for testing
    96  var startGoTestFn = startGoTest
    97  
    98  func hasErrors(err error, exec *testjson.Execution) error {
    99  	switch {
   100  	case len(exec.Errors()) > 0:
   101  		return fmt.Errorf("rerun aborted because previous run had errors")
   102  	// Exit code 0 and 1 are expected.
   103  	case ExitCodeWithDefault(err) > 1:
   104  		return fmt.Errorf("unexpected go test exit code: %v", err)
   105  	case exec.HasPanic():
   106  		return fmt.Errorf("rerun aborted because previous run had a suspected panic and some test may not have run")
   107  	default:
   108  		return nil
   109  	}
   110  }
   111  
   112  type failureRecorder struct {
   113  	testjson.EventHandler
   114  	failures []testjson.TestCase
   115  	lastErr  error
   116  }
   117  
   118  func newFailureRecorder(handler testjson.EventHandler) *failureRecorder {
   119  	return &failureRecorder{EventHandler: handler}
   120  }
   121  
   122  func newFailureRecorderFromExecution(exec *testjson.Execution) *failureRecorder {
   123  	return &failureRecorder{failures: exec.Failed()}
   124  }
   125  
   126  func (r *failureRecorder) Event(event testjson.TestEvent, execution *testjson.Execution) error {
   127  	if !event.PackageEvent() && event.Action == testjson.ActionFail {
   128  		pkg := execution.Package(event.Package)
   129  		tc := pkg.LastFailedByName(event.Test)
   130  		r.failures = append(r.failures, tc)
   131  	}
   132  	return r.EventHandler.Event(event, execution)
   133  }
   134  
   135  func (r *failureRecorder) count() int {
   136  	return len(r.failures)
   137  }
   138  
   139  func goTestRunFlagForTestCase(test testjson.TestName) string {
   140  	if test.IsSubTest() {
   141  		parts := strings.Split(string(test), "/")
   142  		var sb strings.Builder
   143  		sb.WriteString("-test.run=")
   144  		for i, p := range parts {
   145  			if i > 0 {
   146  				sb.WriteByte('/')
   147  			}
   148  			sb.WriteByte('^')
   149  			sb.WriteString(regexp.QuoteMeta(p))
   150  			sb.WriteByte('$')
   151  		}
   152  		return sb.String()
   153  	}
   154  	return "-test.run=^" + regexp.QuoteMeta(test.Name()) + "$"
   155  }
   156  
   157  func writeRerunFailsReport(opts *options, exec *testjson.Execution) error {
   158  	if opts.rerunFailsMaxAttempts == 0 || opts.rerunFailsReportFile == "" {
   159  		return nil
   160  	}
   161  
   162  	type testCaseCounts struct {
   163  		total  int
   164  		failed int
   165  	}
   166  
   167  	names := []string{}
   168  	results := map[string]testCaseCounts{}
   169  	for _, failure := range exec.Failed() {
   170  		name := failure.Package + "." + failure.Test.Name()
   171  		if _, ok := results[name]; ok {
   172  			continue
   173  		}
   174  		names = append(names, name)
   175  
   176  		pkg := exec.Package(failure.Package)
   177  		counts := testCaseCounts{}
   178  
   179  		for _, tc := range pkg.Failed {
   180  			if tc.Test == failure.Test {
   181  				counts.total++
   182  				counts.failed++
   183  			}
   184  		}
   185  		for _, tc := range pkg.Passed {
   186  			if tc.Test == failure.Test {
   187  				counts.total++
   188  			}
   189  		}
   190  		// Skipped tests are not counted, but presumably skipped tests can not fail
   191  		results[name] = counts
   192  	}
   193  
   194  	fh, err := os.Create(opts.rerunFailsReportFile)
   195  	if err != nil {
   196  		return err
   197  	}
   198  
   199  	sort.Strings(names)
   200  	for _, name := range names {
   201  		counts := results[name]
   202  		fmt.Fprintf(fh, "%s: %d runs, %d failures\n", name, counts.total, counts.failed)
   203  	}
   204  	return nil
   205  }