github.com/nya3jp/tast@v0.0.0-20230601000426-85c8e4d83a9b/docs/codelab_4.md (about) 1 # Tast Codelab: Remote tests (go/tast-codelab-4) 2 3 > This document assumes that you've already gone through [Codelab #1]. 4 5 This codelab follows the creation of a remote Tast test and a [gRPC](https://grpc.io) service 6 used by the test. 7 8 [Codelab #1]: codelab_1.md 9 10 11 ## Background 12 13 In Tast our reference point is the DUT, so anything not running on the DUT is 14 considered to be remote in the Tast context. So remote tests are tests which do 15 not run on the DUT itself but on a remote host. They are needed when a test 16 needs to reboot the DUT during the test execution for any reason. Alternatively 17 a remote fixture can be used when reboots are only needed in a preparation step 18 for the test (e.g. enrollment, resetting hardware etc.). The remote host can 19 communicate with the DUT via ssh or gRPC services during the test, which are 20 used to execute code on the DUT itself. 21 22  23 24 Unless ssh calls are enough for the test, the remote test will consist of two 25 parts: 26 27 1. The remote part which includes all the initialization steps and calls to 28 gRPC services. 29 2. One or more gRPC services which execute the required code on 30 the DUT, e.g. for login, the general test logic and cleaning up the DUT. 31 32 33 ## gRPC services 34 35 The gRPC services are created using [protocol buffers](https://developers.google.com/protocol-buffers) 36 to define their signatures, their respective request and response message types. 37 We create a proto file with the definition in the folder in 38 `tast-tests/src/go.chromium.org/tast-tests/cros/services/cros` that corresponds to the test. 39 So if you need a service for a policy test the folder used will be `policy`. 40 In this example we will create a service with a method that checks if the 41 timezone set on the DUT matches a given timezone. So we need one method 42 `TestSystemTimezone()` which performs the check. The request message type for 43 the method will be `TestSystemTimezoneRequest` and contains a timezone string. 44 As the method doesn't return anything except for an error when it fails the 45 response message type is `google.protobuf.Empty`. 46 As `option` we will also define a `go_package` of which this service should be 47 part of. 48 49 ``` 50 syntax = "proto3"; 51 52 package tast.cros.policy; 53 54 import "google/protobuf/empty.proto"; 55 56 option go_package = "go.chromium.org/tast-tests/cros/services/cros/policy"; 57 58 // SystemTimezoneService provides a function to test the system timezone. 59 service SystemTimezoneService { 60 rpc TestSystemTimezone(TestSystemTimezoneRequest) returns (google.protobuf.Empty) {} 61 } 62 63 message TestSystemTimezoneRequest { 64 string Timezone = 1; 65 } 66 ``` 67 68 In order to generate the go code for this service we add its proto file to the 69 `gen.go` file in the same folder. 70 71 ```go 72 // Package policy provides the PolicyService 73 package policy 74 75 // Run the following command in CrOS chroot to regenerate protocol buffer bindings: 76 // 77 // ~/trunk/src/platform/tast/tools/go.sh generate go.chromium.org/tast-tests/cros/services/cros/policy 78 //go:generate protoc -I . --go_out=plugins=grpc:../../../../.. system_timezone.proto 79 ``` 80 81 Then we run 82 `~/trunk/src/platform/tast/tools/go.sh generate go.chromium.org/tast-tests/cros/services/cros/folder` 83 as stated in the `gen.go` file. This will generate a `system_timezone.pb.go` file 84 which contains the go code for the service. 85 86 NOTE: When committing a remote test with gRPC services, make sure to also submit the 87 generated files. 88 89 ## Local service implementation 90 91 Next we need to implement what the `TestSystemTimezone` method is actually 92 doing on the DUT. The service implementation is placed in the same folder as 93 respective local tests. So again if you are writing a remote policy test, all 94 created services for that test go in the 95 `src/go.chromium.org/tast-tests/cros/local/bundles/cros/policy` folder. 96 97 For the implementation we need to import the respective package we assigned 98 our service to as well as the `grpc` package. As we use an empty response 99 in our service we also need the `protobuf/ptypes/empty` package. 100 101 ```go 102 import ( 103 ... 104 "github.com/golang/protobuf/ptypes/empty" 105 "google.golang.org/grpc" 106 107 pb "go.chromium.org/tast-tests/cros/services/cros/policy" 108 ... 109 ) 110 ``` 111 112 In the `init()` function we only add a service instead of a test: 113 114 ```go 115 func init() { 116 testing.AddService(&testing.Service{ 117 Register: func(srv *grpc.Server, s *testing.ServiceState) { 118 pb.RegisterSystemTimezoneServiceServer(srv, &SystemTimezoneService{s: s}) 119 }, 120 }) 121 } 122 ``` 123 124 Then we need a struct to hold our service: 125 126 ```go 127 // SystemTimezoneService implements tast.cros.policy.SystemTimezoneService. 128 type SystemTimezoneService struct { 129 s *testing.ServiceState 130 } 131 ``` 132 133 And finally we need an implementation for all methods defined in our service. 134 All methods return their respective response message and an error as second 135 return value which needs to be checked by the caller. 136 In this case this is just the `TestSystemTimezone()` method which returns 137 a reference to `empty.Empty{}` and an error if the check fails. 138 These methods are the part of the remote test that get executed on the DUT, 139 so we can put most of our test logic in these: 140 141 ```go 142 func (c *SystemTimezoneService) TestSystemTimezone(ctx context.Context, req *pb.TestSystemTimezoneRequest) (*empty.Empty, error) { 143 144 if err := upstart.RestartJob(ctx, "ui"); err != nil { 145 return nil, errors.Wrap(err, "failed to log out") 146 } 147 148 // Wait until the timezone is set. 149 if err := testing.Poll(ctx, func(ctx context.Context) error { 150 151 out, err := os.Readlink("/var/lib/timezone/localtime") 152 if err != nil { 153 return errors.Wrap(err, "failed to get the timezone") 154 } 155 outStr := strings.TrimSpace(string(out)) 156 157 if !strings.Contains(outStr, req.Timezone) { 158 return errors.Errorf("unexpected timezone: got %q; want %q", outStr, req.Timezone) 159 } 160 161 return nil 162 163 }, &testing.PollOptions{ 164 Timeout: 30 * time.Second, 165 }); err != nil { 166 return nil, err 167 } 168 169 return &empty.Empty{}, nil 170 } 171 ``` 172 173 In this example the `TestSystemTimezone()` method will poll the currently set 174 timezone on the console and compare it to the timezone in the input parameter. 175 The function always returns a reference to `empty.Empty{}` as request response 176 and when the timezones don't match until the polling timeout is reached it 177 also returns an error. The error indicates that the timezone on the DUT is not 178 the expected timezone. 179 180 The full implementation of the local service can be found 181 [here](https://osscs.corp.google.com/chromiumos/chromiumos/codesearch/+/03b1d8d64180a8e6f5c073c683cd3ba070adf1f8:src/platform/tast-tests/src/go.chromium.org/tast-tests/cros/local/bundles/cros/policy/system_timezone_service.go) 182 183 ## Remote test 184 185 The remote tests themselves are put into their respective folder in 186 `tast-tests/src/go.chromium.org/tast-tests/cros/remote/bundles/cros`, so in the case of a policy test they are placed in 187 `tast-tests/src/go.chromium.org/tast-tests/cros/remote/bundles/cros/policy`. 188 With the generated service we can now implement the remote part of the test. 189 For that we import all packages containing services we need as well as the 190 `rpc` package. For tests involving enrollment we also need the 191 `remote/policyutil` package to power wash the device. 192 193 ```go 194 import ( 195 ... 196 "go.chromium.org/tast-tests/cros/remote/policyutil" 197 "go.chromium.org/tast/core/rpc" 198 ps "go.chromium.org/tast-tests/cros/services/cros/policy" 199 ... 200 ) 201 ``` 202 203 In the `init()` function we add the services we want to use as `ServiceDeps`. 204 If the test performs an enrollment then we also add it to the enrollment group. 205 206 ```go 207 func init() { 208 testing.AddTest(&testing.Test{ 209 Func: SystemTimezone, 210 Desc: "Just getting the time in a certain timezone", 211 Contacts: []string{ 212 "googler@google.com", // Test author 213 "googler-team@google.com", 214 }, 215 Attr: []string{"group:enrollment"}, 216 SoftwareDeps: []string{"chrome"}, 217 ServiceDeps: []string{"tast.cros.policy.PolicyService", "tast.cros.policy.SystemTimezoneService"}, 218 Timeout: 7 * time.Minute, 219 }) 220 } 221 ``` 222 223 In the test function we then establish a gRPC connection to the DUT. We 224 create a client instance of our service on the connection and then we can call 225 the methods of the service. 226 227 ```go 228 func SystemTimezone(ctx context.Context, s *testing.State) { 229 ... 230 231 // Establish RPC connection to the DUT. 232 cl, err := rpc.Dial(ctx, s.DUT(), s.RPCHint()) 233 if err != nil { 234 s.Fatal("Failed to connect to the RPC service on the DUT: ", err) 235 } 236 defer cl.Close(ctx) 237 238 // Create client instance of the SystemTimezone service. 239 psc := ps.NewSystemTimezoneServiceClient(cl.Conn) 240 241 // Use the TestSystemTimezone method of the SystemTimezone service 242 // to check if the timezone was set correctly by the policy. 243 if _, err = psc.TestSystemTimezone(ctx, &ps.TestSystemTimezoneRequest{ 244 Timezone: "Europe/Berlin", 245 }); err != nil { 246 s.Error("Failed to set SystemTimezone policy : ", err) 247 } 248 ``` 249 250 The full implementation of the remote test can be found 251 [here](https://osscs.corp.google.com/chromiumos/chromiumos/codesearch/+/03b1d8d64180a8e6f5c073c683cd3ba070adf1f8:src/platform/tast-tests/src/go.chromium.org/tast-tests/cros/remote/bundles/cros/policy/system_timezone.go).