1/* Copyright (c) 2015, Google Inc. 2 * 3 * Permission to use, copy, modify, and/or distribute this software for any 4 * purpose with or without fee is hereby granted, provided that the above 5 * copyright notice and this permission notice appear in all copies. 6 * 7 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 10 * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION 12 * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN 13 * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ 14 15package main 16 17import ( 18 "bufio" 19 "bytes" 20 "encoding/json" 21 "flag" 22 "fmt" 23 "math/rand" 24 "os" 25 "os/exec" 26 "path" 27 "runtime" 28 "strconv" 29 "strings" 30 "sync" 31 "syscall" 32 "time" 33) 34 35// TODO(davidben): Link tests with the malloc shim and port -malloc-test to this runner. 36 37var ( 38 useValgrind = flag.Bool("valgrind", false, "If true, run code under valgrind") 39 useCallgrind = flag.Bool("callgrind", false, "If true, run code under valgrind to generate callgrind traces.") 40 useGDB = flag.Bool("gdb", false, "If true, run BoringSSL code under gdb") 41 useSDE = flag.Bool("sde", false, "If true, run BoringSSL code under Intel's SDE for each supported chip") 42 sdePath = flag.String("sde-path", "sde", "The path to find the sde binary.") 43 buildDir = flag.String("build-dir", "build", "The build directory to run the tests from.") 44 numWorkers = flag.Int("num-workers", runtime.NumCPU(), "Runs the given number of workers when testing.") 45 jsonOutput = flag.String("json-output", "", "The file to output JSON results to.") 46 mallocTest = flag.Int64("malloc-test", -1, "If non-negative, run each test with each malloc in turn failing from the given number onwards.") 47 mallocTestDebug = flag.Bool("malloc-test-debug", false, "If true, ask each test to abort rather than fail a malloc. This can be used with a specific value for --malloc-test to identity the malloc failing that is causing problems.") 48) 49 50type test struct { 51 args []string 52 // cpu, if not empty, contains an Intel CPU code to simulate. Run 53 // `sde64 -help` to get a list of these codes. 54 cpu string 55} 56 57type result struct { 58 Test test 59 Passed bool 60 Error error 61} 62 63// testOutput is a representation of Chromium's JSON test result format. See 64// https://www.chromium.org/developers/the-json-test-results-format 65type testOutput struct { 66 Version int `json:"version"` 67 Interrupted bool `json:"interrupted"` 68 PathDelimiter string `json:"path_delimiter"` 69 SecondsSinceEpoch float64 `json:"seconds_since_epoch"` 70 NumFailuresByType map[string]int `json:"num_failures_by_type"` 71 Tests map[string]testResult `json:"tests"` 72} 73 74type testResult struct { 75 Actual string `json:"actual"` 76 Expected string `json:"expected"` 77 IsUnexpected bool `json:"is_unexpected"` 78} 79 80// sdeCPUs contains a list of CPU code that we run all tests under when *useSDE 81// is true. 82var sdeCPUs = []string{ 83 "p4p", // Pentium4 Prescott 84 "mrm", // Merom 85 "pnr", // Penryn 86 "nhm", // Nehalem 87 "wsm", // Westmere 88 "snb", // Sandy Bridge 89 "ivb", // Ivy Bridge 90 "hsw", // Haswell 91 "bdw", // Broadwell 92 "skx", // Skylake Server 93 "skl", // Skylake Client 94 "cnl", // Cannonlake 95 "knl", // Knights Landing 96 "slt", // Saltwell 97 "slm", // Silvermont 98 "glm", // Goldmont 99} 100 101func newTestOutput() *testOutput { 102 return &testOutput{ 103 Version: 3, 104 PathDelimiter: ".", 105 SecondsSinceEpoch: float64(time.Now().UnixNano()) / float64(time.Second/time.Nanosecond), 106 NumFailuresByType: make(map[string]int), 107 Tests: make(map[string]testResult), 108 } 109} 110 111func (t *testOutput) addResult(name, result string) { 112 if _, found := t.Tests[name]; found { 113 panic(name) 114 } 115 t.Tests[name] = testResult{ 116 Actual: result, 117 Expected: "PASS", 118 IsUnexpected: result != "PASS", 119 } 120 t.NumFailuresByType[result]++ 121} 122 123func (t *testOutput) writeTo(name string) error { 124 file, err := os.Create(name) 125 if err != nil { 126 return err 127 } 128 defer file.Close() 129 out, err := json.MarshalIndent(t, "", " ") 130 if err != nil { 131 return err 132 } 133 _, err = file.Write(out) 134 return err 135} 136 137func valgrindOf(dbAttach bool, path string, args ...string) *exec.Cmd { 138 valgrindArgs := []string{"--error-exitcode=99", "--track-origins=yes", "--leak-check=full", "--quiet"} 139 if dbAttach { 140 valgrindArgs = append(valgrindArgs, "--db-attach=yes", "--db-command=xterm -e gdb -nw %f %p") 141 } 142 valgrindArgs = append(valgrindArgs, path) 143 valgrindArgs = append(valgrindArgs, args...) 144 145 return exec.Command("valgrind", valgrindArgs...) 146} 147 148func callgrindOf(path string, args ...string) *exec.Cmd { 149 valgrindArgs := []string{"-q", "--tool=callgrind", "--dump-instr=yes", "--collect-jumps=yes", "--callgrind-out-file=" + *buildDir + "/callgrind/callgrind.out.%p"} 150 valgrindArgs = append(valgrindArgs, path) 151 valgrindArgs = append(valgrindArgs, args...) 152 153 return exec.Command("valgrind", valgrindArgs...) 154} 155 156func gdbOf(path string, args ...string) *exec.Cmd { 157 xtermArgs := []string{"-e", "gdb", "--args"} 158 xtermArgs = append(xtermArgs, path) 159 xtermArgs = append(xtermArgs, args...) 160 161 return exec.Command("xterm", xtermArgs...) 162} 163 164func sdeOf(cpu, path string, args ...string) *exec.Cmd { 165 sdeArgs := []string{"-" + cpu, "--", path} 166 sdeArgs = append(sdeArgs, args...) 167 return exec.Command(*sdePath, sdeArgs...) 168} 169 170type moreMallocsError struct{} 171 172func (moreMallocsError) Error() string { 173 return "child process did not exhaust all allocation calls" 174} 175 176var errMoreMallocs = moreMallocsError{} 177 178func runTestOnce(test test, mallocNumToFail int64) (passed bool, err error) { 179 prog := path.Join(*buildDir, test.args[0]) 180 args := test.args[1:] 181 var cmd *exec.Cmd 182 if *useValgrind { 183 cmd = valgrindOf(false, prog, args...) 184 } else if *useCallgrind { 185 cmd = callgrindOf(prog, args...) 186 } else if *useGDB { 187 cmd = gdbOf(prog, args...) 188 } else if *useSDE { 189 cmd = sdeOf(test.cpu, prog, args...) 190 } else { 191 cmd = exec.Command(prog, args...) 192 } 193 var outBuf bytes.Buffer 194 cmd.Stdout = &outBuf 195 cmd.Stderr = &outBuf 196 if mallocNumToFail >= 0 { 197 cmd.Env = os.Environ() 198 cmd.Env = append(cmd.Env, "MALLOC_NUMBER_TO_FAIL="+strconv.FormatInt(mallocNumToFail, 10)) 199 if *mallocTestDebug { 200 cmd.Env = append(cmd.Env, "MALLOC_ABORT_ON_FAIL=1") 201 } 202 cmd.Env = append(cmd.Env, "_MALLOC_CHECK=1") 203 } 204 205 if err := cmd.Start(); err != nil { 206 return false, err 207 } 208 if err := cmd.Wait(); err != nil { 209 if exitError, ok := err.(*exec.ExitError); ok { 210 if exitError.Sys().(syscall.WaitStatus).ExitStatus() == 88 { 211 return false, errMoreMallocs 212 } 213 } 214 fmt.Print(string(outBuf.Bytes())) 215 return false, err 216 } 217 218 // Account for Windows line-endings. 219 stdout := bytes.Replace(outBuf.Bytes(), []byte("\r\n"), []byte("\n"), -1) 220 221 if bytes.HasSuffix(stdout, []byte("PASS\n")) && 222 (len(stdout) == 5 || stdout[len(stdout)-6] == '\n') { 223 return true, nil 224 } 225 226 // Also accept a googletest-style pass line. This is left here in 227 // transition until the tests are all converted and this script made 228 // unnecessary. 229 if bytes.Contains(stdout, []byte("\n[ PASSED ]")) { 230 return true, nil 231 } 232 233 fmt.Print(string(outBuf.Bytes())) 234 return false, nil 235} 236 237func runTest(test test) (bool, error) { 238 if *mallocTest < 0 { 239 return runTestOnce(test, -1) 240 } 241 242 for mallocNumToFail := int64(*mallocTest); ; mallocNumToFail++ { 243 if passed, err := runTestOnce(test, mallocNumToFail); err != errMoreMallocs { 244 if err != nil { 245 err = fmt.Errorf("at malloc %d: %s", mallocNumToFail, err) 246 } 247 return passed, err 248 } 249 } 250} 251 252// shortTestName returns the short name of a test. Except for evp_test and 253// cipher_test, it assumes that any argument which ends in .txt is a path to a 254// data file and not relevant to the test's uniqueness. 255func shortTestName(test test) string { 256 var args []string 257 for _, arg := range test.args { 258 if test.args[0] == "crypto/evp/evp_test" || test.args[0] == "crypto/cipher_extra/cipher_test" || test.args[0] == "crypto/cipher_extra/aead_test" || !strings.HasSuffix(arg, ".txt") || strings.HasPrefix(arg, "--gtest_filter=") { 259 args = append(args, arg) 260 } 261 } 262 return strings.Join(args, " ") + test.cpuMsg() 263} 264 265// setWorkingDirectory walks up directories as needed until the current working 266// directory is the top of a BoringSSL checkout. 267func setWorkingDirectory() { 268 for i := 0; i < 64; i++ { 269 if _, err := os.Stat("BUILDING.md"); err == nil { 270 return 271 } 272 os.Chdir("..") 273 } 274 275 panic("Couldn't find BUILDING.md in a parent directory!") 276} 277 278func parseTestConfig(filename string) ([]test, error) { 279 in, err := os.Open(filename) 280 if err != nil { 281 return nil, err 282 } 283 defer in.Close() 284 285 decoder := json.NewDecoder(in) 286 var testArgs [][]string 287 if err := decoder.Decode(&testArgs); err != nil { 288 return nil, err 289 } 290 291 var result []test 292 for _, args := range testArgs { 293 result = append(result, test{args: args}) 294 } 295 return result, nil 296} 297 298func worker(tests <-chan test, results chan<- result, done *sync.WaitGroup) { 299 defer done.Done() 300 for test := range tests { 301 passed, err := runTest(test) 302 results <- result{test, passed, err} 303 } 304} 305 306func (t test) cpuMsg() string { 307 if len(t.cpu) == 0 { 308 return "" 309 } 310 311 return fmt.Sprintf(" (for CPU %q)", t.cpu) 312} 313 314func (t test) getGTestShards() ([]test, error) { 315 if *numWorkers == 1 || len(t.args) != 1 { 316 return []test{t}, nil 317 } 318 319 // Only shard the three GTest-based tests. 320 if t.args[0] != "crypto/crypto_test" && t.args[0] != "ssl/ssl_test" && t.args[0] != "decrepit/decrepit_test" { 321 return []test{t}, nil 322 } 323 324 prog := path.Join(*buildDir, t.args[0]) 325 cmd := exec.Command(prog, "--gtest_list_tests") 326 var stdout bytes.Buffer 327 cmd.Stdout = &stdout 328 if err := cmd.Start(); err != nil { 329 return nil, err 330 } 331 if err := cmd.Wait(); err != nil { 332 return nil, err 333 } 334 335 var group string 336 var tests []string 337 scanner := bufio.NewScanner(&stdout) 338 for scanner.Scan() { 339 line := scanner.Text() 340 341 // Remove the parameter comment and trailing space. 342 if idx := strings.Index(line, "#"); idx >= 0 { 343 line = line[:idx] 344 } 345 line = strings.TrimSpace(line) 346 if len(line) == 0 { 347 continue 348 } 349 350 if line[len(line)-1] == '.' { 351 group = line 352 continue 353 } 354 355 if len(group) == 0 { 356 return nil, fmt.Errorf("found test case %q without group", line) 357 } 358 tests = append(tests, group+line) 359 } 360 361 const testsPerShard = 20 362 if len(tests) <= testsPerShard { 363 return []test{t}, nil 364 } 365 366 // Slow tests which process large test vector files tend to be grouped 367 // together, so shuffle the order. 368 shuffled := make([]string, len(tests)) 369 perm := rand.Perm(len(tests)) 370 for i, j := range perm { 371 shuffled[i] = tests[j] 372 } 373 374 var shards []test 375 for i := 0; i < len(shuffled); i += testsPerShard { 376 n := len(shuffled) - i 377 if n > testsPerShard { 378 n = testsPerShard 379 } 380 shard := t 381 shard.args = []string{shard.args[0], "--gtest_filter=" + strings.Join(shuffled[i:i+n], ":")} 382 shards = append(shards, shard) 383 } 384 385 return shards, nil 386} 387 388func main() { 389 flag.Parse() 390 setWorkingDirectory() 391 392 testCases, err := parseTestConfig("util/all_tests.json") 393 if err != nil { 394 fmt.Printf("Failed to parse input: %s\n", err) 395 os.Exit(1) 396 } 397 398 var wg sync.WaitGroup 399 tests := make(chan test, *numWorkers) 400 results := make(chan result, *numWorkers) 401 402 for i := 0; i < *numWorkers; i++ { 403 wg.Add(1) 404 go worker(tests, results, &wg) 405 } 406 407 go func() { 408 for _, test := range testCases { 409 if *useSDE { 410 // SDE generates plenty of tasks and gets slower 411 // with additional sharding. 412 for _, cpu := range sdeCPUs { 413 testForCPU := test 414 testForCPU.cpu = cpu 415 tests <- testForCPU 416 } 417 } else { 418 shards, err := test.getGTestShards() 419 if err != nil { 420 fmt.Printf("Error listing tests: %s\n", err) 421 os.Exit(1) 422 } 423 for _, shard := range shards { 424 tests <- shard 425 } 426 } 427 } 428 close(tests) 429 430 wg.Wait() 431 close(results) 432 }() 433 434 testOutput := newTestOutput() 435 var failed []test 436 for testResult := range results { 437 test := testResult.Test 438 args := test.args 439 440 fmt.Printf("%s%s\n", strings.Join(args, " "), test.cpuMsg()) 441 name := shortTestName(test) 442 if testResult.Error != nil { 443 fmt.Printf("%s failed to complete: %s\n", args[0], testResult.Error) 444 failed = append(failed, test) 445 testOutput.addResult(name, "CRASHED") 446 } else if !testResult.Passed { 447 fmt.Printf("%s failed to print PASS on the last line.\n", args[0]) 448 failed = append(failed, test) 449 testOutput.addResult(name, "FAIL") 450 } else { 451 testOutput.addResult(name, "PASS") 452 } 453 } 454 455 if *jsonOutput != "" { 456 if err := testOutput.writeTo(*jsonOutput); err != nil { 457 fmt.Fprintf(os.Stderr, "Error: %s\n", err) 458 } 459 } 460 461 if len(failed) > 0 { 462 fmt.Printf("\n%d of %d tests failed:\n", len(failed), len(testCases)) 463 for _, test := range failed { 464 fmt.Printf("\t%s%s\n", strings.Join(test.args, " "), test.cpuMsg()) 465 } 466 os.Exit(1) 467 } 468 469 fmt.Printf("\nAll tests passed!\n") 470} 471