• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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