1// Copyright 2019 The Chromium OS Authors. All rights reserved.
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	"testing"
18)
19
20const (
21	mainCc           = "main.cc"
22	clangAndroid     = "./clang"
23	clangTidyAndroid = "./clang-tidy"
24	clangX86_64      = "./x86_64-cros-linux-gnu-clang"
25	gccX86_64        = "./x86_64-cros-linux-gnu-gcc"
26	gccX86_64Eabi    = "./x86_64-cros-eabi-gcc"
27	gccArmV7         = "./armv7m-cros-linux-gnu-gcc"
28	gccArmV7Eabi     = "./armv7m-cros-eabi-gcc"
29	gccArmV8         = "./armv8m-cros-linux-gnu-gcc"
30	gccArmV8Eabi     = "./armv8m-cros-eabi-gcc"
31)
32
33type testContext struct {
34	t            *testing.T
35	wd           string
36	tempDir      string
37	env          []string
38	cfg          *config
39	inputCmd     *command
40	lastCmd      *command
41	cmdCount     int
42	cmdMock      func(cmd *command, stdin io.Reader, stdout io.Writer, stderr io.Writer) error
43	stdinBuffer  bytes.Buffer
44	stdoutBuffer bytes.Buffer
45	stderrBuffer bytes.Buffer
46}
47
48func withTestContext(t *testing.T, work func(ctx *testContext)) {
49	t.Parallel()
50	tempDir, err := ioutil.TempDir("", "compiler_wrapper")
51	if err != nil {
52		t.Fatalf("Unable to create the temp dir. Error: %s", err)
53	}
54	defer os.RemoveAll(tempDir)
55
56	ctx := testContext{
57		t:       t,
58		wd:      tempDir,
59		tempDir: tempDir,
60		env:     nil,
61		cfg:     &config{},
62	}
63	ctx.updateConfig(&config{})
64
65	work(&ctx)
66}
67
68var _ env = (*testContext)(nil)
69
70func (ctx *testContext) getenv(key string) (string, bool) {
71	for i := len(ctx.env) - 1; i >= 0; i-- {
72		entry := ctx.env[i]
73		if strings.HasPrefix(entry, key+"=") {
74			return entry[len(key)+1:], true
75		}
76	}
77	return "", false
78}
79
80func (ctx *testContext) environ() []string {
81	return ctx.env
82}
83
84func (ctx *testContext) getwd() string {
85	return ctx.wd
86}
87
88func (ctx *testContext) stdin() io.Reader {
89	return &ctx.stdinBuffer
90}
91
92func (ctx *testContext) stdout() io.Writer {
93	return &ctx.stdoutBuffer
94}
95
96func (ctx *testContext) stdoutString() string {
97	return ctx.stdoutBuffer.String()
98}
99
100func (ctx *testContext) stderr() io.Writer {
101	return &ctx.stderrBuffer
102}
103
104func (ctx *testContext) stderrString() string {
105	return ctx.stderrBuffer.String()
106}
107
108func (ctx *testContext) run(cmd *command, stdin io.Reader, stdout io.Writer, stderr io.Writer) error {
109	ctx.cmdCount++
110	ctx.lastCmd = cmd
111	if ctx.cmdMock != nil {
112		return ctx.cmdMock(cmd, stdin, stdout, stderr)
113	}
114	return nil
115}
116
117func (ctx *testContext) exec(cmd *command) error {
118	ctx.cmdCount++
119	ctx.lastCmd = cmd
120	if ctx.cmdMock != nil {
121		return ctx.cmdMock(cmd, ctx.stdin(), ctx.stdout(), ctx.stderr())
122	}
123	return nil
124}
125
126func (ctx *testContext) must(exitCode int) *command {
127	if exitCode != 0 {
128		ctx.t.Fatalf("expected no error, but got exit code %d. Stderr: %s",
129			exitCode, ctx.stderrString())
130	}
131	return ctx.lastCmd
132}
133
134func (ctx *testContext) mustFail(exitCode int) string {
135	if exitCode == 0 {
136		ctx.t.Fatalf("expected an error, but got none")
137	}
138	return ctx.stderrString()
139}
140
141func (ctx *testContext) updateConfig(cfg *config) {
142	*ctx.cfg = *cfg
143	ctx.cfg.newWarningsDir = filepath.Join(ctx.tempDir, "fatal_clang_warnings")
144	ctx.cfg.triciumNitsDir = filepath.Join(ctx.tempDir, "tricium_nits")
145	ctx.cfg.crashArtifactsDir = filepath.Join(ctx.tempDir, "clang_crash_diagnostics")
146}
147
148func (ctx *testContext) newCommand(path string, args ...string) *command {
149	// Create an empty wrapper at the given path.
150	// Needed as we are resolving symlinks which stats the wrapper file.
151	ctx.writeFile(path, "")
152	return &command{
153		Path: path,
154		Args: args,
155	}
156}
157
158func (ctx *testContext) writeFile(fullFileName string, fileContent string) {
159	if !filepath.IsAbs(fullFileName) {
160		fullFileName = filepath.Join(ctx.tempDir, fullFileName)
161	}
162	if err := os.MkdirAll(filepath.Dir(fullFileName), 0777); err != nil {
163		ctx.t.Fatal(err)
164	}
165	if err := ioutil.WriteFile(fullFileName, []byte(fileContent), 0777); err != nil {
166		ctx.t.Fatal(err)
167	}
168}
169
170func (ctx *testContext) symlink(oldname string, newname string) {
171	if !filepath.IsAbs(oldname) {
172		oldname = filepath.Join(ctx.tempDir, oldname)
173	}
174	if !filepath.IsAbs(newname) {
175		newname = filepath.Join(ctx.tempDir, newname)
176	}
177	if err := os.MkdirAll(filepath.Dir(newname), 0777); err != nil {
178		ctx.t.Fatal(err)
179	}
180	if err := os.Symlink(oldname, newname); err != nil {
181		ctx.t.Fatal(err)
182	}
183}
184
185func (ctx *testContext) readAllString(r io.Reader) string {
186	if r == nil {
187		return ""
188	}
189	bytes, err := ioutil.ReadAll(r)
190	if err != nil {
191		ctx.t.Fatal(err)
192	}
193	return string(bytes)
194}
195
196func verifyPath(cmd *command, expectedRegex string) error {
197	compiledRegex := regexp.MustCompile(matchFullString(expectedRegex))
198	if !compiledRegex.MatchString(cmd.Path) {
199		return fmt.Errorf("path does not match %s. Actual %s", expectedRegex, cmd.Path)
200	}
201	return nil
202}
203
204func verifyArgCount(cmd *command, expectedCount int, expectedRegex string) error {
205	compiledRegex := regexp.MustCompile(matchFullString(expectedRegex))
206	count := 0
207	for _, arg := range cmd.Args {
208		if compiledRegex.MatchString(arg) {
209			count++
210		}
211	}
212	if count != expectedCount {
213		return fmt.Errorf("expected %d matches for arg %s. All args: %s",
214			expectedCount, expectedRegex, cmd.Args)
215	}
216	return nil
217}
218
219func verifyArgOrder(cmd *command, expectedRegexes ...string) error {
220	compiledRegexes := []*regexp.Regexp{}
221	for _, regex := range expectedRegexes {
222		compiledRegexes = append(compiledRegexes, regexp.MustCompile(matchFullString(regex)))
223	}
224	expectedArgIndex := 0
225	for _, arg := range cmd.Args {
226		if expectedArgIndex == len(compiledRegexes) {
227			break
228		} else if compiledRegexes[expectedArgIndex].MatchString(arg) {
229			expectedArgIndex++
230		}
231	}
232	if expectedArgIndex != len(expectedRegexes) {
233		return fmt.Errorf("expected args %s in order. All args: %s",
234			expectedRegexes, cmd.Args)
235	}
236	return nil
237}
238
239func verifyEnvUpdate(cmd *command, expectedRegex string) error {
240	compiledRegex := regexp.MustCompile(matchFullString(expectedRegex))
241	for _, update := range cmd.EnvUpdates {
242		if compiledRegex.MatchString(update) {
243			return nil
244		}
245	}
246	return fmt.Errorf("expected at least one match for env update %s. All env updates: %s",
247		expectedRegex, cmd.EnvUpdates)
248}
249
250func verifyNoEnvUpdate(cmd *command, expectedRegex string) error {
251	compiledRegex := regexp.MustCompile(matchFullString(expectedRegex))
252	updates := cmd.EnvUpdates
253	for _, update := range updates {
254		if compiledRegex.MatchString(update) {
255			return fmt.Errorf("expected no match for env update %s. All env updates: %s",
256				expectedRegex, cmd.EnvUpdates)
257		}
258	}
259	return nil
260}
261
262func hasInternalError(stderr string) bool {
263	return strings.Contains(stderr, "Internal error")
264}
265
266func verifyInternalError(stderr string) error {
267	if !hasInternalError(stderr) {
268		return fmt.Errorf("expected an internal error. Got: %s", stderr)
269	}
270	if ok, _ := regexp.MatchString(`\w+.go:\d+`, stderr); !ok {
271		return fmt.Errorf("expected a source line reference. Got: %s", stderr)
272	}
273	return nil
274}
275
276func verifyNonInternalError(stderr string, expectedRegex string) error {
277	if hasInternalError(stderr) {
278		return fmt.Errorf("expected a non internal error. Got: %s", stderr)
279	}
280	if ok, _ := regexp.MatchString(`\w+.go:\d+`, stderr); ok {
281		return fmt.Errorf("expected no source line reference. Got: %s", stderr)
282	}
283	if ok, _ := regexp.MatchString(matchFullString(expectedRegex), strings.TrimSpace(stderr)); !ok {
284		return fmt.Errorf("expected stderr matching %s. Got: %s", expectedRegex, stderr)
285	}
286	return nil
287}
288
289func matchFullString(regex string) string {
290	return "^" + regex + "$"
291}
292
293func newExitCodeError(exitCode int) error {
294	// It's actually hard to create an error that represents a command
295	// with exit code. Using a real command instead.
296	tmpCmd := exec.Command("/bin/sh", "-c", fmt.Sprintf("exit %d", exitCode))
297	return tmpCmd.Run()
298}
299