1// Copyright 2019 The ChromiumOS Authors 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5package main 6 7import ( 8 "bytes" 9 "fmt" 10 "io" 11 "io/ioutil" 12 "os" 13 "os/exec" 14 "path/filepath" 15 "regexp" 16 "strings" 17 "sync" 18 "syscall" 19 "testing" 20 "time" 21) 22 23const ( 24 mainCc = "main.cc" 25 clangAndroid = "./clang" 26 clangTidyAndroid = "./clang-tidy" 27 clangX86_64 = "./x86_64-cros-linux-gnu-clang" 28 gccX86_64 = "./x86_64-cros-linux-gnu-gcc" 29 gccX86_64Eabi = "./x86_64-cros-eabi-gcc" 30 gccArmV7 = "./armv7m-cros-linux-gnu-gcc" 31 gccArmV7Eabi = "./armv7m-cros-eabi-gcc" 32 gccArmV8 = "./armv8m-cros-linux-gnu-gcc" 33 gccArmV8Eabi = "./armv8m-cros-eabi-gcc" 34) 35 36type testContext struct { 37 t *testing.T 38 wd string 39 tempDir string 40 env []string 41 cfg *config 42 lastCmd *command 43 cmdCount int 44 cmdMock func(cmd *command, stdin io.Reader, stdout io.Writer, stderr io.Writer) error 45 stdinBuffer bytes.Buffer 46 stdoutBuffer bytes.Buffer 47 stderrBuffer bytes.Buffer 48 49 umaskRestoreAction func() 50} 51 52// We have some tests which modify our umask, and other tests which depend upon the value of our 53// umask remaining consistent. This lock serializes those. Please use `NoteTestWritesToUmask()` and 54// `NoteTestDependsOnUmask()` on `testContext` rather than using this directly. 55var umaskModificationLock sync.RWMutex 56 57func withTestContext(t *testing.T, work func(ctx *testContext)) { 58 t.Parallel() 59 tempDir, err := ioutil.TempDir("", "compiler_wrapper") 60 if err != nil { 61 t.Fatalf("Unable to create the temp dir. Error: %s", err) 62 } 63 defer os.RemoveAll(tempDir) 64 65 ctx := testContext{ 66 t: t, 67 wd: tempDir, 68 tempDir: tempDir, 69 env: nil, 70 cfg: &config{}, 71 } 72 ctx.updateConfig(&config{}) 73 74 defer ctx.maybeReleaseUmaskDependency() 75 work(&ctx) 76} 77 78var _ env = (*testContext)(nil) 79 80func (ctx *testContext) umask(mask int) (oldmask int) { 81 if ctx.umaskRestoreAction == nil { 82 panic("Umask operations requested in test without declaring a umask dependency") 83 } 84 return syscall.Umask(mask) 85} 86 87func (ctx *testContext) setArbitraryClangArtifactsDir() string { 88 d := filepath.Join(ctx.tempDir, "cros-artifacts") 89 ctx.env = append(ctx.env, crosArtifactsEnvVar+"="+d) 90 return d 91} 92 93func (ctx *testContext) initUmaskDependency(lockFn func(), unlockFn func()) { 94 if ctx.umaskRestoreAction != nil { 95 // Use a panic so we get a backtrace. 96 panic("Multiple notes of a test depending on the value of `umask` given -- tests " + 97 "are only allowed up to one.") 98 } 99 100 lockFn() 101 ctx.umaskRestoreAction = unlockFn 102} 103 104func (ctx *testContext) maybeReleaseUmaskDependency() { 105 if ctx.umaskRestoreAction != nil { 106 ctx.umaskRestoreAction() 107 } 108} 109 110// Note that the test depends on a stable value for the process' umask. 111func (ctx *testContext) NoteTestReadsFromUmask() { 112 ctx.initUmaskDependency(umaskModificationLock.RLock, umaskModificationLock.RUnlock) 113} 114 115// Note that the test modifies the process' umask. This implies a dependency on the process' umask, 116// so it's an error to call both NoteTestWritesToUmask and NoteTestReadsFromUmask from the same 117// test. 118func (ctx *testContext) NoteTestWritesToUmask() { 119 ctx.initUmaskDependency(umaskModificationLock.Lock, umaskModificationLock.Unlock) 120} 121 122func (ctx *testContext) getenv(key string) (string, bool) { 123 for i := len(ctx.env) - 1; i >= 0; i-- { 124 entry := ctx.env[i] 125 if strings.HasPrefix(entry, key+"=") { 126 return entry[len(key)+1:], true 127 } 128 } 129 return "", false 130} 131 132func (ctx *testContext) environ() []string { 133 return ctx.env 134} 135 136func (ctx *testContext) getwd() string { 137 return ctx.wd 138} 139 140func (ctx *testContext) stdin() io.Reader { 141 return &ctx.stdinBuffer 142} 143 144func (ctx *testContext) stdout() io.Writer { 145 return &ctx.stdoutBuffer 146} 147 148func (ctx *testContext) stdoutString() string { 149 return ctx.stdoutBuffer.String() 150} 151 152func (ctx *testContext) stderr() io.Writer { 153 return &ctx.stderrBuffer 154} 155 156func (ctx *testContext) stderrString() string { 157 return ctx.stderrBuffer.String() 158} 159 160func (ctx *testContext) run(cmd *command, stdin io.Reader, stdout io.Writer, stderr io.Writer) error { 161 ctx.cmdCount++ 162 ctx.lastCmd = cmd 163 if ctx.cmdMock != nil { 164 return ctx.cmdMock(cmd, stdin, stdout, stderr) 165 } 166 return nil 167} 168 169func (ctx *testContext) runWithTimeout(cmd *command, duration time.Duration) error { 170 return ctx.exec(cmd) 171} 172 173func (ctx *testContext) exec(cmd *command) error { 174 ctx.cmdCount++ 175 ctx.lastCmd = cmd 176 if ctx.cmdMock != nil { 177 return ctx.cmdMock(cmd, ctx.stdin(), ctx.stdout(), ctx.stderr()) 178 } 179 return nil 180} 181 182func (ctx *testContext) must(exitCode int) *command { 183 if exitCode != 0 { 184 ctx.t.Fatalf("expected no error, but got exit code %d. Stderr: %s", 185 exitCode, ctx.stderrString()) 186 } 187 return ctx.lastCmd 188} 189 190func (ctx *testContext) mustFail(exitCode int) string { 191 if exitCode == 0 { 192 ctx.t.Fatalf("expected an error, but got none") 193 } 194 return ctx.stderrString() 195} 196 197func (ctx *testContext) updateConfig(cfg *config) { 198 *ctx.cfg = *cfg 199} 200 201func (ctx *testContext) newCommand(path string, args ...string) *command { 202 // Create an empty wrapper at the given path. 203 // Needed as we are resolving symlinks which stats the wrapper file. 204 ctx.writeFile(path, "") 205 return &command{ 206 Path: path, 207 Args: args, 208 } 209} 210 211func (ctx *testContext) writeFile(fullFileName string, fileContent string) { 212 if !filepath.IsAbs(fullFileName) { 213 fullFileName = filepath.Join(ctx.tempDir, fullFileName) 214 } 215 if err := os.MkdirAll(filepath.Dir(fullFileName), 0777); err != nil { 216 ctx.t.Fatal(err) 217 } 218 if err := ioutil.WriteFile(fullFileName, []byte(fileContent), 0777); err != nil { 219 ctx.t.Fatal(err) 220 } 221} 222 223func (ctx *testContext) symlink(oldname string, newname string) { 224 if !filepath.IsAbs(oldname) { 225 oldname = filepath.Join(ctx.tempDir, oldname) 226 } 227 if !filepath.IsAbs(newname) { 228 newname = filepath.Join(ctx.tempDir, newname) 229 } 230 if err := os.MkdirAll(filepath.Dir(newname), 0777); err != nil { 231 ctx.t.Fatal(err) 232 } 233 if err := os.Symlink(oldname, newname); err != nil { 234 ctx.t.Fatal(err) 235 } 236} 237 238func (ctx *testContext) readAllString(r io.Reader) string { 239 if r == nil { 240 return "" 241 } 242 bytes, err := ioutil.ReadAll(r) 243 if err != nil { 244 ctx.t.Fatal(err) 245 } 246 return string(bytes) 247} 248 249func verifyPath(cmd *command, expectedRegex string) error { 250 compiledRegex := regexp.MustCompile(matchFullString(expectedRegex)) 251 if !compiledRegex.MatchString(cmd.Path) { 252 return fmt.Errorf("path does not match %s. Actual %s", expectedRegex, cmd.Path) 253 } 254 return nil 255} 256 257func verifyArgCount(cmd *command, expectedCount int, expectedRegex string) error { 258 compiledRegex := regexp.MustCompile(matchFullString(expectedRegex)) 259 count := 0 260 for _, arg := range cmd.Args { 261 if compiledRegex.MatchString(arg) { 262 count++ 263 } 264 } 265 if count != expectedCount { 266 return fmt.Errorf("expected %d matches for arg %s. All args: %s", 267 expectedCount, expectedRegex, cmd.Args) 268 } 269 return nil 270} 271 272func verifyArgOrder(cmd *command, expectedRegexes ...string) error { 273 compiledRegexes := []*regexp.Regexp{} 274 for _, regex := range expectedRegexes { 275 compiledRegexes = append(compiledRegexes, regexp.MustCompile(matchFullString(regex))) 276 } 277 expectedArgIndex := 0 278 for _, arg := range cmd.Args { 279 if expectedArgIndex == len(compiledRegexes) { 280 break 281 } else if compiledRegexes[expectedArgIndex].MatchString(arg) { 282 expectedArgIndex++ 283 } 284 } 285 if expectedArgIndex != len(expectedRegexes) { 286 return fmt.Errorf("expected args %s in order. All args: %s", 287 expectedRegexes, cmd.Args) 288 } 289 return nil 290} 291 292func verifyEnvUpdate(cmd *command, expectedRegex string) error { 293 compiledRegex := regexp.MustCompile(matchFullString(expectedRegex)) 294 for _, update := range cmd.EnvUpdates { 295 if compiledRegex.MatchString(update) { 296 return nil 297 } 298 } 299 return fmt.Errorf("expected at least one match for env update %s. All env updates: %s", 300 expectedRegex, cmd.EnvUpdates) 301} 302 303func verifyNoEnvUpdate(cmd *command, expectedRegex string) error { 304 compiledRegex := regexp.MustCompile(matchFullString(expectedRegex)) 305 updates := cmd.EnvUpdates 306 for _, update := range updates { 307 if compiledRegex.MatchString(update) { 308 return fmt.Errorf("expected no match for env update %s. All env updates: %s", 309 expectedRegex, cmd.EnvUpdates) 310 } 311 } 312 return nil 313} 314 315func hasInternalError(stderr string) bool { 316 return strings.Contains(stderr, "Internal error") 317} 318 319func verifyInternalError(stderr string) error { 320 if !hasInternalError(stderr) { 321 return fmt.Errorf("expected an internal error. Got: %s", stderr) 322 } 323 if ok, _ := regexp.MatchString(`\w+.go:\d+`, stderr); !ok { 324 return fmt.Errorf("expected a source line reference. Got: %s", stderr) 325 } 326 return nil 327} 328 329func verifyNonInternalError(stderr string, expectedRegex string) error { 330 if hasInternalError(stderr) { 331 return fmt.Errorf("expected a non internal error. Got: %s", stderr) 332 } 333 if ok, _ := regexp.MatchString(`\w+.go:\d+`, stderr); ok { 334 return fmt.Errorf("expected no source line reference. Got: %s", stderr) 335 } 336 if ok, _ := regexp.MatchString(matchFullString(expectedRegex), strings.TrimSpace(stderr)); !ok { 337 return fmt.Errorf("expected stderr matching %s. Got: %s", expectedRegex, stderr) 338 } 339 return nil 340} 341 342func matchFullString(regex string) string { 343 return "^" + regex + "$" 344} 345 346func newExitCodeError(exitCode int) error { 347 // It's actually hard to create an error that represents a command 348 // with exit code. Using a real command instead. 349 tmpCmd := exec.Command("/bin/sh", "-c", fmt.Sprintf("exit %d", exitCode)) 350 return tmpCmd.Run() 351} 352