oss.indeed.com/go/go-opine@v1.3.0/internal/gotest/result.go (about)

     1  package gotest
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"strings"
     7  	"time"
     8  )
     9  
    10  // resultKey identifies a result.
    11  type resultKey struct {
    12  	Package string
    13  	Test    string
    14  }
    15  
    16  // result is a test result. The result is for either a single test or for
    17  // a package, in which case Key.Test is empty.
    18  type result struct {
    19  	Key     resultKey
    20  	Outcome string
    21  	Output  string
    22  	Elapsed time.Duration
    23  }
    24  
    25  // resultAccepter accepts results.
    26  type resultAccepter interface {
    27  	Accept(res result) error
    28  }
    29  
    30  // multiResultAccepter accepts results and forwards them on to zero or
    31  // more downstream result accepters.
    32  type multiResultAccepter struct {
    33  	accepters []resultAccepter
    34  }
    35  
    36  var _ resultAccepter = (*multiResultAccepter)(nil)
    37  
    38  func newMultiResultAccepter(accepter ...resultAccepter) *multiResultAccepter {
    39  	return &multiResultAccepter{accepters: accepter}
    40  }
    41  
    42  // Accept forwards the result to the downstream resultAccepters. If any
    43  // resultAccepter returns an error processing stops immediately and that
    44  // error is returned to the caller.
    45  func (m multiResultAccepter) Accept(res result) error {
    46  	for _, accepter := range m.accepters {
    47  		if err := accepter.Accept(res); err != nil {
    48  			return err
    49  		}
    50  	}
    51  	return nil
    52  }
    53  
    54  // resultAggregator is an eventAccepter that aggregates events for the same
    55  // test or package into results. Completed results are passed to the
    56  // resultAccepter.
    57  type resultAggregator struct {
    58  	to     resultAccepter
    59  	events map[resultKey][]event
    60  	err    error
    61  }
    62  
    63  func newResultAggregator(to resultAccepter) *resultAggregator {
    64  	return &resultAggregator{
    65  		to:     to,
    66  		events: make(map[resultKey][]event),
    67  	}
    68  }
    69  
    70  // Accept adds an event to the internal state and provides any result
    71  // completed by the event to the resultAccepter.
    72  //
    73  // If the resultAccepter returns an error the resultAggregator will enter
    74  // an error state causing the current accept and all subsequent accepts to
    75  // fail. This error will also be returned by Close.
    76  func (a *resultAggregator) Accept(e event) error {
    77  	if a.err != nil {
    78  		return fmt.Errorf("permanent error state: %w", a.err)
    79  	}
    80  
    81  	rk := resultKey{
    82  		Package: e.Package,
    83  		Test:    e.Test,
    84  	}
    85  
    86  	if !isTestOrPackageComplete(e.Action) {
    87  		a.events[rk] = append(a.events[rk], e)
    88  		return nil
    89  	}
    90  
    91  	var output strings.Builder
    92  	for _, prevEvent := range a.events[rk] {
    93  		output.WriteString(prevEvent.Output)
    94  	}
    95  	delete(a.events, rk)
    96  	output.WriteString(e.Output)
    97  
    98  	res := result{
    99  		Key:     rk,
   100  		Outcome: e.Action,
   101  		Output:  output.String(),
   102  		Elapsed: time.Duration(e.Elapsed * float64(time.Second)),
   103  	}
   104  	if err := a.to.Accept(res); err != nil {
   105  		a.setErr(err)
   106  		return a.err
   107  	}
   108  
   109  	return nil
   110  }
   111  
   112  // CheckAllEventsConsumed checks that all events are consumed and that
   113  // no error occurred in any Accept.
   114  func (a *resultAggregator) CheckAllEventsConsumed() error {
   115  	if a.err == nil && len(a.events) > 0 {
   116  		a.setErr(errors.New("not all events were consumed"))
   117  	}
   118  	return a.err
   119  }
   120  
   121  // setErr puts the resultAggregator into a permanent error state.
   122  func (a *resultAggregator) setErr(err error) {
   123  	a.err = err
   124  	a.events = nil
   125  }
   126  
   127  // resultPackageGrouper accepts results, groups them by package, and
   128  // forwards all results for a package when it completes.
   129  //
   130  // This is necessary because by default Go will run tests from different
   131  // packages at the same time. If the output of each result is printed
   132  // immediately it will cause confusion regarding which package each test
   133  // is in. For example take the following output:
   134  //
   135  //     === RUN   Test_Cmd_optionLog
   136  //     --- PASS: Test_Cmd_optionLog (0.01s)
   137  //     PASS
   138  //     ok  	oss.indeed.com/go/go-opine/internal/run	(cached)
   139  //
   140  // The only way you can tell the Test_Cmd_optionLog package is
   141  // oss.indeed.com/go/go-opine/internal/run is by the fact that the package
   142  // output is printed immediately after the test output.
   143  //
   144  // !!WARNING!! This struct relies on the the final result of a package
   145  // being the "package result" (i.e. the result that has only a package
   146  // and no test). If you filter results before providing them to a
   147  // resultPackageGrouper make sure you do not filter out the package result
   148  // for any test result you previously provided. Otherwise Close will return
   149  // an error about results remaining.
   150  type resultPackageGrouper struct {
   151  	to         resultAccepter
   152  	pkgResults map[string][]result
   153  	err        error
   154  }
   155  
   156  var _ resultAccepter = (*resultPackageGrouper)(nil)
   157  
   158  func newResultPackageGrouper(to resultAccepter) *resultPackageGrouper {
   159  	return &resultPackageGrouper{
   160  		to:         to,
   161  		pkgResults: make(map[string][]result),
   162  	}
   163  }
   164  
   165  // Accept adds the result to the resultPackageGrouper internal state and,
   166  // if the result is a "package result", forwards all buffered test results
   167  // and the package result onward.
   168  //
   169  // If the resultAccepter returns an error the resultPackageGrouper will enter
   170  // an error state causing the current accept and all subsequent accepts to
   171  // fail. This error will also be returned by Close.
   172  func (r *resultPackageGrouper) Accept(res result) error {
   173  	if r.err != nil {
   174  		return fmt.Errorf("permanent error state: %w", r.err)
   175  	}
   176  
   177  	r.pkgResults[res.Key.Package] = append(r.pkgResults[res.Key.Package], res)
   178  
   179  	if !isPackageComplete(res) {
   180  		return nil
   181  	}
   182  
   183  	if err := r.forward(r.pkgResults[res.Key.Package]...); err != nil {
   184  		return err
   185  	}
   186  	delete(r.pkgResults, res.Key.Package)
   187  
   188  	return nil
   189  }
   190  
   191  // CheckAllEventsConsumed checks that all results are consumed and that
   192  // no error occurred in any Accept.
   193  func (r *resultPackageGrouper) CheckAllResultsConsumed() error {
   194  	if r.err == nil && len(r.pkgResults) > 0 {
   195  		r.setErr(errors.New("not all results were consumed"))
   196  	}
   197  	return r.err
   198  }
   199  
   200  // forward passes zero or more results on to the resultAccepter. If the
   201  // resultAccepter returns an error for any result processing stops, setErr
   202  // is called to put the resultPackageGrouper in a permanent error state, and
   203  // the error is returned.
   204  func (r *resultPackageGrouper) forward(results ...result) error {
   205  	for _, res := range results {
   206  		if err := r.to.Accept(res); err != nil {
   207  			r.setErr(err)
   208  			return r.err
   209  		}
   210  	}
   211  	return nil
   212  }
   213  
   214  // setErr puts the resultPackageGrouper into a permanent error state.
   215  func (r *resultPackageGrouper) setErr(err error) {
   216  	r.err = err
   217  	r.pkgResults = nil
   218  }
   219  
   220  // isTestOrPackageComplete returns true iff the provided event.Action
   221  // represents the completion of test or package.
   222  func isTestOrPackageComplete(action string) bool {
   223  	return action == "pass" || action == "fail" || action == "skip"
   224  }
   225  
   226  // isPackageComplete returns true iff the provided result represents
   227  // the completion of a package.
   228  func isPackageComplete(res result) bool {
   229  	return res.Key.Test == ""
   230  }