xref: /aosp_15_r20/external/toolchain-utils/compiler_wrapper/env_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	"context"
10	"errors"
11	"flag"
12	"io/ioutil"
13	"os"
14	"os/exec"
15	"path"
16	"path/filepath"
17	"strings"
18	"testing"
19	"time"
20)
21
22// Attention: The tests in this file execute the test binary again with the `-run` flag.
23// This is needed as they want to test an `exec`, which terminates the test process.
24var internalexececho = flag.Bool("internalexececho", false, "internal flag used for tests that exec")
25
26func TestProcessEnvExecPathAndArgs(t *testing.T) {
27	withTestContext(t, func(ctx *testContext) {
28		if *internalexececho {
29			execEcho(ctx, &command{
30				Path: "some_binary",
31				Args: []string{"arg1", "arg2"},
32			})
33			return
34		}
35		logLines := forkAndReadEcho(ctx)
36		if !strings.HasSuffix(logLines[0], "/some_binary arg1 arg2") {
37			t.Errorf("incorrect path or args: %s", logLines[0])
38		}
39	})
40}
41
42func TestProcessEnvExecAddEnv(t *testing.T) {
43	withTestContext(t, func(ctx *testContext) {
44		if *internalexececho {
45			execEcho(ctx, &command{
46				Path:       "some_binary",
47				EnvUpdates: []string{"ABC=xyz"},
48			})
49			return
50		}
51
52		logLines := forkAndReadEcho(ctx)
53		for _, ll := range logLines {
54			if ll == "ABC=xyz" {
55				return
56			}
57		}
58		t.Errorf("could not find new env variable: %s", logLines)
59	})
60}
61
62func TestProcessEnvExecUpdateEnv(t *testing.T) {
63	if os.Getenv("PATH") == "" {
64		t.Fatal("no PATH environment variable found!")
65	}
66	withTestContext(t, func(ctx *testContext) {
67		if *internalexececho {
68			execEcho(ctx, &command{
69				Path:       "some_binary",
70				EnvUpdates: []string{"PATH=xyz"},
71			})
72			return
73		}
74		logLines := forkAndReadEcho(ctx)
75		for _, ll := range logLines {
76			if ll == "PATH=xyz" {
77				return
78			}
79		}
80		t.Errorf("could not find updated env variable: %s", logLines)
81	})
82}
83
84func TestProcessEnvExecDeleteEnv(t *testing.T) {
85	if os.Getenv("PATH") == "" {
86		t.Fatal("no PATH environment variable found!")
87	}
88	withTestContext(t, func(ctx *testContext) {
89		if *internalexececho {
90			execEcho(ctx, &command{
91				Path:       "some_binary",
92				EnvUpdates: []string{"PATH="},
93			})
94			return
95		}
96		logLines := forkAndReadEcho(ctx)
97		for _, ll := range logLines {
98			if strings.HasPrefix(ll, "PATH=") {
99				t.Errorf("path env was not removed: %s", ll)
100			}
101		}
102	})
103}
104
105func TestProcessEnvRunCmdPathAndArgs(t *testing.T) {
106	withTestContext(t, func(ctx *testContext) {
107		cmd := &command{
108			Path: "some_binary",
109			Args: []string{"arg1", "arg2"},
110		}
111		logLines := runAndEcho(ctx, cmd)
112		if !strings.HasSuffix(logLines[0], "/some_binary arg1 arg2") {
113			t.Errorf("incorrect path or args: %s", logLines[0])
114		}
115	})
116}
117
118func TestProcessEnvRunCmdAddEnv(t *testing.T) {
119	withTestContext(t, func(ctx *testContext) {
120		cmd := &command{
121			Path:       "some_binary",
122			EnvUpdates: []string{"ABC=xyz"},
123		}
124		logLines := runAndEcho(ctx, cmd)
125		for _, ll := range logLines {
126			if ll == "ABC=xyz" {
127				return
128			}
129		}
130		t.Errorf("could not find new env variable: %s", logLines)
131	})
132}
133
134func TestProcessEnvRunCmdUpdateEnv(t *testing.T) {
135	withTestContext(t, func(ctx *testContext) {
136		if os.Getenv("PATH") == "" {
137			t.Fatal("no PATH environment variable found!")
138		}
139		cmd := &command{
140			Path:       "some_binary",
141			EnvUpdates: []string{"PATH=xyz"},
142		}
143		logLines := runAndEcho(ctx, cmd)
144		for _, ll := range logLines {
145			if ll == "PATH=xyz" {
146				return
147			}
148		}
149		t.Errorf("could not find updated env variable: %s", logLines)
150	})
151}
152
153func TestProcessEnvRunCmdDeleteEnv(t *testing.T) {
154	withTestContext(t, func(ctx *testContext) {
155		if os.Getenv("PATH") == "" {
156			t.Fatal("no PATH environment variable found!")
157		}
158		cmd := &command{
159			Path:       "some_binary",
160			EnvUpdates: []string{"PATH="},
161		}
162		logLines := runAndEcho(ctx, cmd)
163		for _, ll := range logLines {
164			if strings.HasPrefix(ll, "PATH=") {
165				t.Errorf("path env was not removed: %s", ll)
166			}
167		}
168	})
169}
170
171func TestRunWithTimeoutRunsTheGivenProcess(t *testing.T) {
172	withTestContext(t, func(ctx *testContext) {
173		env, err := newProcessEnv()
174		if err != nil {
175			t.Fatalf("Unexpected error making new process env: %v", err)
176		}
177
178		tempFile := path.Join(ctx.tempDir, "some_file")
179		cmd := &command{
180			Path: "touch",
181			Args: []string{tempFile},
182		}
183		if err := env.runWithTimeout(cmd, time.Second*120); err != nil {
184			t.Fatalf("Unexpected error touch'ing %q: %v", tempFile, err)
185		}
186
187		// This should be fine, since `touch` should've created the file.
188		if _, err := os.Stat(tempFile); err != nil {
189			t.Errorf("Stat'ing temp file at %q failed: %v", tempFile, err)
190		}
191	})
192}
193
194func TestRunWithTimeoutReturnsErrorOnTimeout(t *testing.T) {
195	withTestContext(t, func(ctx *testContext) {
196		env, err := newProcessEnv()
197		if err != nil {
198			t.Fatalf("Unexpected error making new process env: %v", err)
199		}
200
201		cmd := &command{
202			Path: "sleep",
203			Args: []string{"30"},
204		}
205
206		err = env.runWithTimeout(cmd, 100*time.Millisecond)
207		if !errors.Is(err, context.DeadlineExceeded) {
208			t.Errorf("Expected context.DeadlineExceeded after `sleep` timed out; got error: %v", err)
209		}
210	})
211}
212
213func TestNewProcessEnvResolvesPwdAwayProperly(t *testing.T) {
214	// This test cannot be t.Parallel(), since it modifies our environment.
215	const envPwd = "PWD"
216
217	oldEnvPwd := os.Getenv(envPwd)
218	defer func() {
219		if oldEnvPwd == "" {
220			os.Unsetenv(envPwd)
221		} else {
222			os.Setenv(envPwd, oldEnvPwd)
223		}
224	}()
225
226	os.Unsetenv(envPwd)
227
228	initialWd, err := os.Getwd()
229	if err != nil {
230		t.Fatalf("Failed getting working directory: %v", err)
231	}
232	if initialWd == "/proc/self/cwd" {
233		t.Fatalf("Working directory should never be %q when env is unset", initialWd)
234	}
235
236	defer func() {
237		if err := os.Chdir(initialWd); err != nil {
238			t.Errorf("Changing back to %q failed: %v", initialWd, err)
239		}
240	}()
241
242	tempDir, err := ioutil.TempDir("", "wrapper_env_test")
243	if err != nil {
244		t.Fatalf("Failed making temp dir: %v", err)
245	}
246
247	// Nothing we can do if this breaks, unfortunately.
248	defer os.RemoveAll(tempDir)
249
250	tempDirLink := tempDir + ".symlink"
251	if err := os.Symlink(tempDir, tempDirLink); err != nil {
252		t.Fatalf("Failed creating symlink %q => %q: %v", tempDirLink, tempDir, err)
253	}
254
255	if err := os.Chdir(tempDir); err != nil {
256		t.Fatalf("Failed chdir'ing to tempdir at %q: %v", tempDirLink, err)
257	}
258
259	if err := os.Setenv(envPwd, tempDirLink); err != nil {
260		t.Fatalf("Failed setting pwd to tempdir at %q: %v", tempDirLink, err)
261	}
262
263	// Ensure that we don't resolve symlinks if they're present in our CWD somehow, except for
264	// /proc/self/cwd, which tells us nothing about where we are.
265	env, err := newProcessEnv()
266	if err != nil {
267		t.Fatalf("Failed making a new env: %v", err)
268	}
269
270	if wd := env.getwd(); wd != tempDirLink {
271		t.Errorf("Environment setup had a wd of %q; wanted %q", wd, tempDirLink)
272	}
273
274	const cwdLink = "/proc/self/cwd"
275	if err := os.Setenv(envPwd, cwdLink); err != nil {
276		t.Fatalf("Failed setting pwd to /proc/self/cwd: %v", err)
277	}
278
279	env, err = newProcessEnv()
280	if err != nil {
281		t.Fatalf("Failed making a new env: %v", err)
282	}
283
284	if wd := env.getwd(); wd != tempDir {
285		t.Errorf("Environment setup had a wd of %q; wanted %q", cwdLink, tempDir)
286	}
287}
288
289func execEcho(ctx *testContext, cmd *command) {
290	env := &processEnv{}
291	err := env.exec(createEcho(ctx, cmd))
292	if err != nil {
293		os.Stderr.WriteString(err.Error())
294	}
295	os.Exit(1)
296}
297
298func forkAndReadEcho(ctx *testContext) []string {
299	testBin, err := os.Executable()
300	if err != nil {
301		ctx.t.Fatalf("unable to read the executable: %s", err)
302	}
303
304	subCmd := exec.Command(testBin, "-internalexececho", "-test.run="+ctx.t.Name())
305	output, err := subCmd.CombinedOutput()
306	if err != nil {
307		ctx.t.Fatalf("error calling test binary again for exec: %s", err)
308	}
309	return strings.Split(string(output), "\n")
310}
311
312func runAndEcho(ctx *testContext, cmd *command) []string {
313	env, err := newProcessEnv()
314	if err != nil {
315		ctx.t.Fatalf("creation of process env failed: %s", err)
316	}
317	buffer := bytes.Buffer{}
318	if err := env.run(createEcho(ctx, cmd), nil, &buffer, &buffer); err != nil {
319		ctx.t.Fatalf("run failed: %s", err)
320	}
321	return strings.Split(buffer.String(), "\n")
322}
323
324func createEcho(ctx *testContext, cmd *command) *command {
325	content := `
326/bin/echo "$0" "$@"
327/usr/bin/env
328`
329	fullPath := filepath.Join(ctx.tempDir, cmd.Path)
330	ctx.writeFile(fullPath, content)
331	// Note: Using a self executable wrapper does not work due to a race condition
332	// on unix systems. See https://github.com/golang/go/issues/22315
333	return &command{
334		Path:       "bash",
335		Args:       append([]string{fullPath}, cmd.Args...),
336		EnvUpdates: cmd.EnvUpdates,
337	}
338}
339