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