github.com/nya3jp/tast@v0.0.0-20230601000426-85c8e4d83a9b/docs/codelab_1.md (about)

     1  # Tast Codelab #1: platform.DateFormat (go/tast-codelab-1)
     2  
     3  This codelab walks through the creation of a short Tast test that checks that
     4  the `date` command prints the expected output when executed with various
     5  arguments. In doing so, we'll learn:
     6  
     7  *   how to define new tests
     8  *   how to test multiple cases without repeating code
     9  *   how to run external commands
    10  *   how to report errors
    11  
    12  We probably wouldn't want to actually test this, since the `date` command is
    13  likely very stable by this point and any regressions in it would hopefully be
    14  caught long before reaching ChromeOS. Since there's a cost in writing, running,
    15  and maintaining tests, we want to focus on areas where we'll get the most
    16  benefit.
    17  
    18  ## Boring boilerplate
    19  
    20  To start out, we'll create a file at
    21  `src/platform/tast-tests/src/go.chromium.org/tast-tests/cros/local/bundles/cros/platform/date_format.go`
    22  containing the standard copyright header, the name of the package that this file
    23  belongs to, and an `import` block listing the packages that we're using:
    24  
    25  ```go
    26  // Copyright <copyright_year> The ChromiumOS Authors
    27  // Use of this source code is governed by a BSD-style license that can be
    28  // found in the LICENSE file.
    29  
    30  package platform
    31  
    32  import (
    33  	"context"
    34  	"strings"
    35  
    36  	"go.chromium.org/tast-tests/cros/common/testexec"
    37  	"go.chromium.org/tast/core/shutil"
    38  	"go.chromium.org/tast/core/testing"
    39  )
    40  ```
    41  
    42  To keep tests from becoming hard to find, we favor using one of the existing
    43  [test category packages] in the `cros` local test bundle; `platform` seems like
    44  a good fit. If we were to add many more tests for the `date` command later, it
    45  would be easy to introduce a new `date` package for them then.
    46  
    47  If you configure your editor to run [goimports] whenever you save Go files, then
    48  you generally don't need to worry about managing imports of standard Go
    49  packages, but you'll still need to add Tast-specific dependencies (i.e. packages
    50  beginning with `chromiumos/`) yourself.
    51  
    52  [test category packages]: https://chromium.googlesource.com/chromiumos/platform/tast-tests/+/HEAD/src/go.chromium.org/tast-tests/cros/local/bundles/cros
    53  [goimports]: https://godoc.org/golang.org/x/tools/cmd/goimports
    54  
    55  ## Test metadata
    56  
    57  Next, we add an `init` function containing a single [testing.AddTest] call that
    58  registers our test:
    59  
    60  ```go
    61  func init() {
    62  	testing.AddTest(&testing.Test{
    63  		Func: DateFormat,
    64  		Desc: "Checks that the date command prints dates as expected",
    65  		Contacts: []string{
    66  			"my-team@chromium.org", // Owning team mailing list
    67  			"me@chromium.org",      // Optional test contact
    68  		},
    69  		Attr: []string{"group:mainline", "informational"},
    70  		BugComponent: "b:1234",
    71  	})
    72  }
    73  ```
    74  
    75  `init` functions run automatically before all other code in the package. We pass
    76  a pointer to a [testing.Test] struct to `testing.AddTest`; this contains our
    77  test's metadata.
    78  
    79  The `Func` field contains the main test function that we'll define, i.e. the
    80  entry point into the test. The function's name is also used to derive the test's
    81  name; since our test is in the `platform` package, it will be named
    82  `platform.DateFormat`. We don't include words like `Check`, `Test`, or `Verify`
    83  the test's name: we already know that it's a test, after all.
    84  
    85  `Desc` is a short, human-readable phrase describing the test, and `Contacts`
    86  lists the email addresses of people and mailing lists that are responsible for
    87  the test.
    88  
    89  `Attr` contains free-form strings naming this test's [attributes].
    90  `group:mainline` indicates that this test is in [the mainline group], the
    91  default group for functional tests. `informational` indicates that this test is
    92  non-critical, i.e. it won't run on the ChromeOS Commit Queue or on the
    93  Pre-Flight Queue (PFQ) builders that are used to integrate new versions of
    94  Chrome or Android into the OS. All [new mainline tests] (internal link) should
    95  start out with the `informational` attribute until they've been proven to be
    96  stable. If a test is not dependent on physical HW and can be run on x86 VMs,
    97  please also specify `group:hw_agnostic`.
    98  
    99  `BugComponent` contains a string representing the owning team's primary bug component.
   100  For buganizer, please use a "`b:`" prefix and specify the component ID (i.e., "`b:12345`"). For crbug, please use a "`crbug:`" prefix followed by component path (i.e., "`crbug:UI>Shell>Launcher`").
   101  
   102  [testing.AddTest]: https://godoc.org/chromium.googlesource.com/chromiumos/platform/tast.git/src/go.chromium.org/tast/core/testing#AddTest
   103  [testing.Test]: https://godoc.org/chromium.googlesource.com/chromiumos/platform/tast.git/src/go.chromium.org/tast/core/testing#Test
   104  [attributes]: https://chromium.googlesource.com/chromiumos/platform/tast/+/HEAD/docs/test_attributes.md
   105  [the mainline group]: https://chromium.googlesource.com/chromiumos/platform/tast/+/HEAD/docs/test_attributes.md
   106  [new mainline tests]: https://chrome-internal.googlesource.com/chromeos/chromeos-admin/+/HEAD/doc/tast_add_test.md
   107  
   108  ## Test function
   109  
   110  Next comes the signature of our main `DateFormat` test function:
   111  
   112  ```go
   113  func DateFormat(ctx context.Context, s *testing.State) {
   114  ```
   115  
   116  All Tast test functions receive [context.Context] and [testing.State] arguments;
   117  by convention, these are named `ctx` and `s`. The `Context` is used primarily to
   118  carry a deadline that represents the test's timeout, while the `State` is used
   119  to fetch test-related information at runtime and to report log messages or
   120  errors.
   121  
   122  [context.Context]: https://golang.org/pkg/context/
   123  [testing.State]: https://godoc.org/chromium.googlesource.com/chromiumos/platform/tast.git/src/go.chromium.org/tast/core/testing#State
   124  
   125  ## Test cases
   126  
   127  Within the test function, we need to run the `date` command with various
   128  arguments and compare its actual output against the corresponding expected
   129  output. One way to do this would be be repeating code for each test case, but
   130  that would result in a lot of duplication and make future changes harder. A
   131  common approach used in Go code for cases like this is to iterate over an array
   132  of anonymous structures, each of which contains a test case:
   133  
   134  ```go
   135  	for _, tc := range []struct {
   136  		date string // value to pass via --date flag
   137  		spec string // spec to pass in "+"-prefixed arg
   138  		exp  string // expected UTC output (minus trailing newline)
   139  	}{
   140  		{"2004-02-29 16:21:42 +0100", "%Y-%m-%d %H:%M:%S", "2004-02-29 15:21:42"},
   141  		{"Sun, 29 Feb 2004 16:21:42 -0800", "%Y-%m-%d %H:%M:%S", "2004-03-01 00:21:42"},
   142  	} {
   143  		// Test body will go here.
   144  	}
   145  ```
   146  
   147  The syntax can be confusing at first, so let's break it down.
   148  
   149  First, we start a `for` loop. We use the double-assignment form of `range`,
   150  which provides an index and a value for each element in a slice. We're not
   151  interested in the index, so we ignore it (by assigning to underscore) and copy
   152  each element to a `tc` (for "test case") value. There's a convention in Go code
   153  to use [short names] for variables that have a limited scope, like this one.
   154  
   155  The next part of the loop construct explains what we're iterating over: a slice
   156  of structs, each of which contains three string fields. We document each field's
   157  purpose using an end-of-line comment. Single-line or multi-line comments
   158  typically consist of full sentences, but it's fine to use phrases for short
   159  end-of-line comments like these.
   160  
   161  If we were going to use this struct multiple times, we would give it a name
   162  using a `type` declaration, but since we're only using it within the loop here,
   163  it's simpler to keep it anonymous.
   164  
   165  Next, we provide the slice's values: a comma-separated list of struct literals.
   166  Since we're providing all of the struct fields, we can omit the field names.
   167  
   168  Finally, we provide a block containing the loop body. We'll discuss that in the
   169  next section.
   170  
   171  [short names]: https://talks.golang.org/2014/names.slide
   172  
   173  ## Loop body
   174  
   175  ```go
   176  		cmd := testexec.CommandContext(ctx, "date", "--utc", "--date="+tc.date, "+"+tc.spec)
   177  		if out, err := cmd.Output(testexec.DumpLogOnError); err != nil {
   178  			s.Errorf("%q failed: %v", shutil.EscapeSlice(cmd.Args), err)
   179  		} else if outs := strings.TrimRight(string(out), "\n"); outs != tc.exp {
   180  			s.Errorf("%q printed %q; want %q", shutil.EscapeSlice(cmd.Args), outs, tc.exp)
   181  		}
   182  ```
   183  
   184  First, we declare a `testexec.Cmd` named `cmd` that will be used to execute the
   185  `date` command with the appropriate arguments for this test case. The [testexec]
   186  package is similar to the standard [exec] package but provides a few
   187  Tast-specific niceties.
   188  
   189  After that, we run the command synchronously to completion using `Output`,
   190  getting back its stdout as a `[]byte`, along with an `error` value that is
   191  non-nil if the process didn't run successfully. We pass
   192  `testexec.DumpLogToError`, which is an option instructing the `Cmd` to log
   193  likely-useful information like stderr if the process fails.
   194  
   195  We use the assignment form of `if`, which lets us perform an assignment before
   196  testing a boolean condition. If a non-nil `error` was returned, then we report a
   197  test error. We use `Errorf` so we can provide a `printf`-like format string, and
   198  we include both the quoted command and the error that was returned. The
   199  `Something failed: <error with more details>` form is recommended for error
   200  messages in Tast tests for consistency.
   201  
   202  Finally, we add an `else if` that calls `strings.TrimRight` to trim a trailing
   203  newline from `out` (which is still in scope here), and compare the resulting
   204  string against the test case's expected output. If they don't match, then we
   205  report another error using `Errorf`.
   206  
   207  In the error messages above, we use the [shutil] package to escape the command
   208  that we ran so it's easier to copy-and-paste to run manually. Since we're
   209  logging strings that were produced by an outside command and that may contain
   210  spaces, we use `%q` so they'll be quoted automatically. The `<foo>
   211  printed/produced/= <bar>; want <baz>` form is also common in Go unit tests and
   212  recommended in Tast.
   213  
   214  [testexec]: https://godoc.org/chromium.googlesource.com/chromiumos/platform/tast-tests.git/src/go.chromium.org/tast-tests/cros/common/testexec
   215  [exec]: https://golang.org/pkg/os/exec/
   216  [shutil]: https://godoc.org/chromium.googlesource.com/chromiumos/platform/tast.git/src/go.chromium.org/tast/core/shutil
   217  
   218  ## Wrapping it up
   219  
   220  After closing our loop and the test function, we're done!
   221  
   222  ```go
   223  	}
   224  }
   225  ```
   226  
   227  If the test reports one or more errors in the loop, it fails. If no errors have
   228  been reported by the time that the test function returns, then the test passes.
   229  
   230  The test can be run using a command like `tast -verbose run <DUT>
   231  platform.DateFormat`. See the [Running Tests] document for more information.
   232  
   233  If you want to see how a more-complicated test is written, check out
   234  [Codelab #2].
   235  
   236  [Running Tests]: https://chromium.googlesource.com/chromiumos/platform/tast/+/HEAD/docs/running_tests.md
   237  [Codelab #2]: codelab_2.md
   238  
   239  ## Full code
   240  
   241  Here's a full listing of the test's code:
   242  
   243  ```go
   244  // Copyright 2019 The ChromiumOS Authors
   245  // Use of this source code is governed by a BSD-style license that can be
   246  // found in the LICENSE file.
   247  
   248  package platform
   249  
   250  import (
   251  	"context"
   252  	"strings"
   253  
   254  	"go.chromium.org/tast-tests/cros/common/testexec"
   255  	"go.chromium.org/tast/core/shutil"
   256  	"go.chromium.org/tast/core/testing"
   257  )
   258  
   259  func init() {
   260  	testing.AddTest(&testing.Test{
   261  		Func: DateFormat,
   262  		Desc: "Checks that the date command prints dates as expected",
   263  		Contacts: []string{
   264  			"me@chromium.org",         // Test author
   265  			"tast-users@chromium.org", // Backup mailing list
   266  		},
   267  		Attr: []string{"group:mainline", "informational"},
   268  	})
   269  }
   270  
   271  func DateFormat(ctx context.Context, s *testing.State) {
   272  	for _, tc := range []struct {
   273  		date string // value to pass via --date flag
   274  		spec string // spec to pass in "+"-prefixed arg
   275  		exp  string // expected UTC output (minus trailing newline)
   276  	}{
   277  		{"2004-02-29 16:21:42 +0100", "%Y-%m-%d %H:%M:%S", "2004-02-29 15:21:42"},
   278  		{"Sun, 29 Feb 2004 16:21:42 -0800", "%Y-%m-%d %H:%M:%S", "2004-03-01 00:21:42"},
   279  	} {
   280  		cmd := testexec.CommandContext(ctx, "date", "--utc", "--date="+tc.date, "+"+tc.spec)
   281  		if out, err := cmd.Output(testexec.DumpLogOnError); err != nil {
   282  			s.Errorf("%q failed: %v", shutil.EscapeSlice(cmd.Args), err)
   283  		} else if outs := strings.TrimRight(string(out), "\n"); outs != tc.exp {
   284  			s.Errorf("%q printed %q; want %q", shutil.EscapeSlice(cmd.Args), outs, tc.exp)
   285  		}
   286  	}
   287  }
   288  ```