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