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  ![remote test](remote_test.png)
    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).