blob: cafa00d3dbad55d14863885973076b3e9c17dd3f [file] [view] [edit]
# Tast Codelab #1: platform.DateFormat (go/tast-codelab-1)
This codelab walks through the creation of a short Tast test that checks that
the `date` command prints the expected output when executed with various
arguments. In doing so, we'll learn:
* how to define new tests
* how to test multiple cases without repeating code
* how to run external commands
* how to report errors
We probably wouldn't want to actually test this, since the `date` command is
likely very stable by this point and any regressions in it would hopefully be
caught long before reaching Chrome OS. Since there's a cost in writing, running,
and maintaining tests, we want to focus on areas where we'll get the most
benefit.
## Boring boilerplate
To start out, we'll create a file at
`src/platform/tast-tests/src/chromiumos/tast/local/bundles/cros/platform/date_format.go`
containing the standard copyright header, the name of the package that this file
belongs to, and an `import` block listing the packages that we're using:
```go
// Copyright 2019 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package platform
import (
"context"
"strings"
"chromiumos/tast/common/testexec"
"chromiumos/tast/shutil"
"chromiumos/tast/testing"
)
```
To keep tests from becoming hard to find, we favor using one of the existing
[test category packages] in the `cros` local test bundle; `platform` seems like
a good fit. If we were to add many more tests for the `date` command later, it
would be easy to introduce a new `date` package for them then.
If you configure your editor to run [goimports] whenever you save Go files, then
you generally don't need to worry about managing imports of standard Go
packages, but you'll still need to add Tast-specific dependencies (i.e. packages
beginning with `chromiumos/`) yourself.
[test category packages]: https://chromium.googlesource.com/chromiumos/platform/tast-tests/+/HEAD/src/chromiumos/tast/local/bundles/cros
[goimports]: https://godoc.org/golang.org/x/tools/cmd/goimports
## Test metadata
Next, we add an `init` function containing a single [testing.AddTest] call that
registers our test:
```go
func init() {
testing.AddTest(&testing.Test{
Func: DateFormat,
Desc: "Checks that the date command prints dates as expected",
Contacts: []string{
"me@chromium.org", // Test author
"tast-users@chromium.org", // Backup mailing list
},
Attr: []string{"group:mainline", "informational"},
})
}
```
`init` functions run automatically before all other code in the package. We pass
a pointer to a [testing.Test] struct to `testing.AddTest`; this contains our
test's metadata.
The `Func` field contains the main test function that we'll define, i.e. the
entry point into the test. The function's name is also used to derive the test's
name; since our test is in the `platform` package, it will be named
`platform.DateFormat`. We don't include words like `Check`, `Test`, or `Verify`
the test's name: we already know that it's a test, after all.
`Desc` is a short, human-readable phrase describing the test, and `Contacts`
lists the email addresses of people and mailing lists that are responsible for
the test.
`Attr` contains free-form strings naming this test's [attributes].
`group:mainline` indicates that this test is in [the mainline group], the
default group for functional tests. `informational` indicates that this test is
non-critical, i.e. it won't run on the Chrome OS Commit Queue or on the
Pre-Flight Queue (PFQ) builders that are used to integrate new versions of
Chrome or Android into the OS. All [new mainline tests] (internal link) should
start out with the `informational` attribute until they've been proven to be
stable.
[testing.AddTest]: https://godoc.org/chromium.googlesource.com/chromiumos/platform/tast.git/src/chromiumos/tast/testing#AddTest
[testing.Test]: https://godoc.org/chromium.googlesource.com/chromiumos/platform/tast.git/src/chromiumos/tast/testing#Test
[attributes]: https://chromium.googlesource.com/chromiumos/platform/tast/+/HEAD/docs/test_attributes.md
[the mainline group]: https://chromium.googlesource.com/chromiumos/platform/tast/+/HEAD/docs/test_attributes.md
[new mainline tests]: https://chrome-internal.googlesource.com/chromeos/chromeos-admin/+/HEAD/doc/tast_add_test.md
## Test function
Next comes the signature of our main `DateFormat` test function:
```go
func DateFormat(ctx context.Context, s *testing.State) {
```
All Tast test functions receive [context.Context] and [testing.State] arguments;
by convention, these are named `ctx` and `s`. The `Context` is used primarily to
carry a deadline that represents the test's timeout, while the `State` is used
to fetch test-related information at runtime and to report log messages or
errors.
[context.Context]: https://golang.org/pkg/context/
[testing.State]: https://godoc.org/chromium.googlesource.com/chromiumos/platform/tast.git/src/chromiumos/tast/testing#State
## Test cases
Within the test function, we need to run the `date` command with various
arguments and compare its actual output against the corresponding expected
output. One way to do this would be be repeating code for each test case, but
that would result in a lot of duplication and make future changes harder. A
common approach used in Go code for cases like this is to iterate over an array
of anonymous structures, each of which contains a test case:
```go
for _, tc := range []struct {
date string // value to pass via --date flag
spec string // spec to pass in "+"-prefixed arg
exp string // expected UTC output (minus trailing newline)
}{
{"2004-02-29 16:21:42 +0100", "%Y-%m-%d %H:%M:%S", "2004-02-29 15:21:42"},
{"Sun, 29 Feb 2004 16:21:42 -0800", "%Y-%m-%d %H:%M:%S", "2004-03-01 00:21:42"},
} {
// Test body will go here.
}
```
The syntax can be confusing at first, so let's break it down.
First, we start a `for` loop. We use the double-assignment form of `range`,
which provides an index and a value for each element in a slice. We're not
interested in the index, so we ignore it (by assigning to underscore) and copy
each element to a `tc` (for "test case") value. There's a convention in Go code
to use [short names] for variables that have a limited scope, like this one.
The next part of the loop construct explains what we're iterating over: a slice
of structs, each of which contains three string fields. We document each field's
purpose using an end-of-line comment. Single-line or multi-line comments
typically consist of full sentences, but it's fine to use phrases for short
end-of-line comments like these.
If we were going to use this struct multiple times, we would give it a name
using a `type` declaration, but since we're only using it within the loop here,
it's simpler to keep it anonymous.
Next, we provide the slice's values: a comma-separated list of struct literals.
Since we're providing all of the struct fields, we can omit the field names.
Finally, we provide a block containing the loop body. We'll discuss that in the
next section.
[short names]: https://talks.golang.org/2014/names.slide
## Loop body
```go
cmd := testexec.CommandContext(ctx, "date", "--utc", "--date="+tc.date, "+"+tc.spec)
if out, err := cmd.Output(testexec.DumpLogOnError); err != nil {
s.Errorf("%q failed: %v", shutil.EscapeSlice(cmd.Args), err)
} else if outs := strings.TrimRight(string(out), "\n"); outs != tc.exp {
s.Errorf("%q printed %q; want %q", shutil.EscapeSlice(cmd.Args), outs, tc.exp)
}
```
First, we declare a `testexec.Cmd` named `cmd` that will be used to execute the
`date` command with the appropriate arguments for this test case. The [testexec]
package is similar to the standard [exec] package but provides a few
Tast-specific niceties.
After that, we run the command synchronously to completion using `Output`,
getting back its stdout as a `[]byte`, along with an `error` value that is
non-nil if the process didn't run successfully. We pass
`testexec.DumpLogToError`, which is an option instructing the `Cmd` to log
likely-useful information like stderr if the process fails.
We use the assignment form of `if`, which lets us perform an assignment before
testing a boolean condition. If a non-nil `error` was returned, then we report a
test error. We use `Errorf` so we can provide a `printf`-like format string, and
we include both the quoted command and the error that was returned. The
`Something failed: <error with more details>` form is recommended for error
messages in Tast tests for consistency.
Finally, we add an `else if` that calls `strings.TrimRight` to trim a trailing
newline from `out` (which is still in scope here), and compare the resulting
string against the test case's expected output. If they don't match, then we
report another error using `Errorf`.
In the error messages above, we use the [shutil] package to escape the command
that we ran so it's easier to copy-and-paste to run manually. Since we're
logging strings that were produced by an outside command and that may contain
spaces, we use `%q` so they'll be quoted automatically. The `<foo>
printed/produced/= <bar>; want <baz>` form is also common in Go unit tests and
recommended in Tast.
[testexec]: https://godoc.org/chromium.googlesource.com/chromiumos/platform/tast-tests.git/src/chromiumos/tast/common/testexec
[exec]: https://golang.org/pkg/os/exec/
[shutil]: https://godoc.org/chromium.googlesource.com/chromiumos/platform/tast.git/src/chromiumos/tast/shutil
## Wrapping it up
After closing our loop and the test function, we're done!
```go
}
}
```
If the test reports one or more errors in the loop, it fails. If no errors have
been reported by the time that the test function returns, then the test passes.
The test can be run using a command like `tast -verbose run <DUT>
platform.DateFormat`. See the [Running Tests] document for more information.
If you want to see how a more-complicated test is written, check out
[Codelab #2].
[Running Tests]: https://chromium.googlesource.com/chromiumos/platform/tast/+/HEAD/docs/running_tests.md
[Codelab #2]: codelab_2.md
## Full code
Here's a full listing of the test's code:
```go
// Copyright 2019 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package platform
import (
"context"
"strings"
"chromiumos/tast/common/testexec"
"chromiumos/tast/shutil"
"chromiumos/tast/testing"
)
func init() {
testing.AddTest(&testing.Test{
Func: DateFormat,
Desc: "Checks that the date command prints dates as expected",
Contacts: []string{
"me@chromium.org", // Test author
"tast-users@chromium.org", // Backup mailing list
},
Attr: []string{"group:mainline", "informational"},
})
}
func DateFormat(ctx context.Context, s *testing.State) {
for _, tc := range []struct {
date string // value to pass via --date flag
spec string // spec to pass in "+"-prefixed arg
exp string // expected UTC output (minus trailing newline)
}{
{"2004-02-29 16:21:42 +0100", "%Y-%m-%d %H:%M:%S", "2004-02-29 15:21:42"},
{"Sun, 29 Feb 2004 16:21:42 -0800", "%Y-%m-%d %H:%M:%S", "2004-03-01 00:21:42"},
} {
cmd := testexec.CommandContext(ctx, "date", "--utc", "--date="+tc.date, "+"+tc.spec)
if out, err := cmd.Output(testexec.DumpLogOnError); err != nil {
s.Errorf("%q failed: %v", shutil.EscapeSlice(cmd.Args), err)
} else if outs := strings.TrimRight(string(out), "\n"); outs != tc.exp {
s.Errorf("%q printed %q; want %q", shutil.EscapeSlice(cmd.Args), outs, tc.exp)
}
}
}
```