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