github.com/nya3jp/tast@v0.0.0-20230601000426-85c8e4d83a9b/src/go.chromium.org/tast/core/internal/bundle/bundle.go (about)

     1  // Copyright 2020 The ChromiumOS Authors
     2  // Use of this source code is governed by a BSD-style license that can be
     3  // found in the LICENSE file.
     4  
     5  package bundle
     6  
     7  import (
     8  	"context"
     9  	"encoding/json"
    10  	"io"
    11  	"strings"
    12  	"time"
    13  
    14  	"go.chromium.org/tast/core/dut"
    15  	"go.chromium.org/tast/core/errors"
    16  	"go.chromium.org/tast/core/internal/bundle/legacyjson"
    17  	"go.chromium.org/tast/core/internal/command"
    18  	"go.chromium.org/tast/core/internal/testcontext"
    19  	"go.chromium.org/tast/core/internal/testing"
    20  )
    21  
    22  const (
    23  	statusSuccess     = 0 // bundle ran successfully
    24  	statusError       = 1 // unclassified runtime error was encountered
    25  	statusBadArgs     = 2 // bad command-line flags or other args were supplied
    26  	statusBadTests    = 3 // errors in test registration (bad names, missing test functions, etc.)
    27  	statusBadPatterns = 4 // one or more bad test patterns were passed to the bundle
    28  	_                 = 5 // deprecated
    29  )
    30  
    31  // Delegate injects functions as a part of test bundle framework implementation.
    32  type Delegate struct {
    33  	// TestHook is called before each test in the test bundle if it is not nil.
    34  	// The returned closure is executed after the test if it is not nil.
    35  	TestHook func(context.Context, *testing.TestHookState) func(context.Context, *testing.TestHookState)
    36  
    37  	// RunHook is called at the beginning of a bundle execution if it is not nil.
    38  	// The returned closure is executed at the end if it is not nil.
    39  	// In case of errors, no test in the test bundle will run.
    40  	RunHook func(context.Context) (func(context.Context) error, error)
    41  
    42  	// Ready is called at the beginning of a bundle execution if it is not
    43  	// nil and -waituntilready is set to true (default).
    44  	// systemTestsTimeout is the timeout for waiting for system services
    45  	// to be ready in seconds.
    46  	// Local test bundles can specify a function to wait for the DUT to be
    47  	// ready for tests to run. It is recommended to write informational
    48  	// messages with testing.ContextLog to let the user know the reason for
    49  	// the delay. In case of errors, no test in the test bundle will run.
    50  	// This field has an effect only for local test bundles.
    51  	Ready func(ctx context.Context, systemTestsTimeout time.Duration) error
    52  
    53  	// BeforeReboot is called before every reboot if it is not nil.
    54  	// This field has an effect only for remote test bundles.
    55  	BeforeReboot func(ctx context.Context, d *dut.DUT) error
    56  
    57  	// BeforeDownload is called before the framework attempts to download
    58  	// external data files if it is not nil.
    59  	//
    60  	// Test bundles can install this hook to recover from possible network
    61  	// outage caused by previous tests. Note that it is called only when
    62  	// the framework needs to download one or more external data files.
    63  	//
    64  	// Since no specific timeout is set to ctx, do remember to set a
    65  	// reasonable timeout at the beginning of the hook to avoid blocking
    66  	// for long time.
    67  	BeforeDownload func(ctx context.Context)
    68  }
    69  
    70  // run reads a JSON-marshaled BundleArgs struct from stdin and performs the requested action.
    71  // Default arguments may be specified via args, which will also be updated from stdin.
    72  // The caller should exit with the returned status code.
    73  func run(ctx context.Context, clArgs []string, stdin io.Reader, stdout, stderr io.Writer, scfg *StaticConfig) int {
    74  	args, err := readArgs(clArgs, stderr)
    75  	if err != nil {
    76  		return command.WriteError(stderr, err)
    77  	}
    78  
    79  	if errs := scfg.registry.Errors(); len(errs) > 0 {
    80  		es := make([]string, len(errs))
    81  		for i, err := range errs {
    82  			es[i] = err.Error()
    83  		}
    84  		err := command.NewStatusErrorf(statusBadTests, "error(s) in registered tests: %v", strings.Join(es, ", "))
    85  		return command.WriteError(stderr, err)
    86  	}
    87  
    88  	switch args.mode {
    89  	case modeDumpTests:
    90  		tests, err := testsToRun(scfg, nil)
    91  		if err != nil {
    92  			return command.WriteError(stderr, err)
    93  		}
    94  		switch args.dumpFormat {
    95  		case dumpFormatLegacyJSON:
    96  			if err := writeTestsAsLegacyJSON(stdout, tests); err != nil {
    97  				return command.WriteError(stderr, err)
    98  			}
    99  			return statusSuccess
   100  		case dumpFormatProto:
   101  			if err := testing.WriteTestsAsProto(stdout, tests); err != nil {
   102  				return command.WriteError(stderr, err)
   103  			}
   104  		default:
   105  			return command.WriteError(stderr, errors.Errorf("invalid dump format %v", args.dumpFormat))
   106  		}
   107  		return statusSuccess
   108  	case modeRPC:
   109  		if err := RunRPCServer(stdin, stdout, scfg); err != nil {
   110  			return command.WriteError(stderr, err)
   111  		}
   112  		return statusSuccess
   113  	case modeRPCTCP:
   114  		port := args.port
   115  		handshakeReq := args.handshake
   116  		if err := RunRPCServerTCP(port, handshakeReq, stdin, stdout, stderr, scfg); err != nil {
   117  			return command.WriteError(stderr, err)
   118  		}
   119  		return statusSuccess
   120  	default:
   121  		return command.WriteError(stderr, command.NewStatusErrorf(statusBadArgs, "invalid mode %v", args.mode))
   122  	}
   123  }
   124  
   125  func writeTestsAsLegacyJSON(w io.Writer, tests []*testing.TestInstance) error {
   126  	var infos []*legacyjson.EntityWithRunnabilityInfo
   127  	for _, test := range tests {
   128  		// If we encounter errors while checking test dependencies,
   129  		// treat the test as not skipped. When we actually try to
   130  		// run the test later, it will fail with errors.
   131  		var skipReason string
   132  		if reasons, err := test.Deps().Check(nil); err == nil && len(reasons) > 0 {
   133  			skipReason = strings.Join(append([]string(nil), reasons...), ", ")
   134  		}
   135  		infos = append(infos, &legacyjson.EntityWithRunnabilityInfo{
   136  			EntityInfo: *legacyjson.MustEntityInfoFromProto(test.EntityProto()),
   137  			SkipReason: skipReason,
   138  		})
   139  	}
   140  	return json.NewEncoder(w).Encode(infos)
   141  }
   142  
   143  // StaticConfig contains configurations unique to a test bundle.
   144  //
   145  // The supplied functions are used to provide customizations that apply to all
   146  // entities in a test bundle. They may contain bundle-specific code.
   147  type StaticConfig struct {
   148  	// registry is a registry to be used to find entities.
   149  	registry *testing.Registry
   150  	// runHook is run at the beginning of the entire series of tests if non-nil.
   151  	// The returned closure is executed after the entire series of tests if not nil.
   152  	runHook func(context.Context, time.Duration) (func(context.Context) error, error)
   153  	// testHook is run before each test if non-nil.
   154  	// If this function panics or reports errors, the precondition (if any)
   155  	// will not be prepared and the test function will not run.
   156  	// The returned closure is executed after a test if not nil.
   157  	testHook func(context.Context, *testing.TestHookState) func(context.Context, *testing.TestHookState)
   158  	// beforeReboot is run before every reboot if non-nil.
   159  	// The function must not call DUT.Reboot() or it will cause infinite recursion.
   160  	beforeReboot func(context.Context, *dut.DUT) error
   161  	// beforeDownload is run before downloading external data files if non-nil.
   162  	beforeDownload func(context.Context)
   163  	// defaultTestTimeout contains the default maximum time allotted to each test.
   164  	// It is only used if testing.Test.Timeout is unset.
   165  	defaultTestTimeout time.Duration
   166  }
   167  
   168  // NewStaticConfig constructs StaticConfig from given parameters.
   169  func NewStaticConfig(reg *testing.Registry, defaultTestTimeout time.Duration, d Delegate) *StaticConfig {
   170  	return &StaticConfig{
   171  		registry: reg,
   172  		runHook: func(ctx context.Context, systemTestsTimeout time.Duration) (func(context.Context) error, error) {
   173  			pd, ok := testcontext.PrivateDataFromContext(ctx)
   174  			if !ok {
   175  				panic("BUG: PrivateData not available in run hook")
   176  			}
   177  			if d.Ready != nil && pd.WaitUntilReady {
   178  				ctxWithTimeout := ctx
   179  				if pd.WaitUntilReadyTimeout > 0 {
   180  					var cancel context.CancelFunc
   181  					ctxWithTimeout, cancel = context.WithTimeout(ctx, pd.WaitUntilReadyTimeout)
   182  					defer cancel()
   183  				}
   184  				if err := d.Ready(ctxWithTimeout, systemTestsTimeout); err != nil {
   185  					return nil, err
   186  				}
   187  			}
   188  			if d.RunHook == nil {
   189  				return nil, nil
   190  			}
   191  			return d.RunHook(ctx)
   192  		},
   193  		testHook:           d.TestHook,
   194  		beforeReboot:       d.BeforeReboot,
   195  		beforeDownload:     d.BeforeDownload,
   196  		defaultTestTimeout: defaultTestTimeout,
   197  	}
   198  }