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 "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 initialWd == "/proc/self/cwd" { 230 t.Fatalf("Working directory should never be %q when env is unset", initialWd) 231 } 232 233 defer func() { 234 if err := os.Chdir(initialWd); err != nil { 235 t.Errorf("Changing back to %q failed: %v", initialWd, err) 236 } 237 }() 238 239 tempDir, err := ioutil.TempDir("", "wrapper_env_test") 240 if err != nil { 241 t.Fatalf("Failed making temp dir: %v", err) 242 } 243 244 // Nothing we can do if this breaks, unfortunately. 245 defer os.RemoveAll(tempDir) 246 247 tempDirLink := tempDir + ".symlink" 248 if err := os.Symlink(tempDir, tempDirLink); err != nil { 249 t.Fatalf("Failed creating symlink %q => %q: %v", tempDirLink, tempDir, err) 250 } 251 252 if err := os.Chdir(tempDir); err != nil { 253 t.Fatalf("Failed chdir'ing to tempdir at %q: %v", tempDirLink, err) 254 } 255 256 if err := os.Setenv(envPwd, tempDirLink); err != nil { 257 t.Fatalf("Failed setting pwd to tempdir at %q: %v", tempDirLink, err) 258 } 259 260 // Ensure that we don't resolve symlinks if they're present in our CWD somehow, except for 261 // /proc/self/cwd, which tells us nothing about where we are. 262 env, err := newProcessEnv() 263 if err != nil { 264 t.Fatalf("Failed making a new env: %v", err) 265 } 266 267 if wd := env.getwd(); wd != tempDirLink { 268 t.Errorf("Environment setup had a wd of %q; wanted %q", wd, tempDirLink) 269 } 270 271 const cwdLink = "/proc/self/cwd" 272 if err := os.Setenv(envPwd, cwdLink); err != nil { 273 t.Fatalf("Failed setting pwd to /proc/self/cwd: %v", err) 274 } 275 276 env, err = newProcessEnv() 277 if err != nil { 278 t.Fatalf("Failed making a new env: %v", err) 279 } 280 281 if wd := env.getwd(); wd != tempDir { 282 t.Errorf("Environment setup had a wd of %q; wanted %q", cwdLink, tempDir) 283 } 284} 285 286func execEcho(ctx *testContext, cmd *command) { 287 env := &processEnv{} 288 err := env.exec(createEcho(ctx, cmd)) 289 if err != nil { 290 os.Stderr.WriteString(err.Error()) 291 } 292 os.Exit(1) 293} 294 295func forkAndReadEcho(ctx *testContext) []string { 296 testBin, err := os.Executable() 297 if err != nil { 298 ctx.t.Fatalf("unable to read the executable: %s", err) 299 } 300 301 subCmd := exec.Command(testBin, "-internalexececho", "-test.run="+ctx.t.Name()) 302 output, err := subCmd.CombinedOutput() 303 if err != nil { 304 ctx.t.Fatalf("error calling test binary again for exec: %s", err) 305 } 306 return strings.Split(string(output), "\n") 307} 308 309func runAndEcho(ctx *testContext, cmd *command) []string { 310 env, err := newProcessEnv() 311 if err != nil { 312 ctx.t.Fatalf("creation of process env failed: %s", err) 313 } 314 buffer := bytes.Buffer{} 315 if err := env.run(createEcho(ctx, cmd), nil, &buffer, &buffer); err != nil { 316 ctx.t.Fatalf("run failed: %s", err) 317 } 318 return strings.Split(buffer.String(), "\n") 319} 320 321func createEcho(ctx *testContext, cmd *command) *command { 322 content := ` 323/bin/echo "$0" "$@" 324/usr/bin/env 325` 326 fullPath := filepath.Join(ctx.tempDir, cmd.Path) 327 ctx.writeFile(fullPath, content) 328 // Note: Using a self executable wrapper does not work due to a race condition 329 // on unix systems. See https://github.com/golang/go/issues/22315 330 return &command{ 331 Path: "bash", 332 Args: append([]string{fullPath}, cmd.Args...), 333 EnvUpdates: cmd.EnvUpdates, 334 } 335} 336