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