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 }