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 ```