| // Copyright 2020 The Fuchsia Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| package fuzz | |
| import ( | |
| "flag" | |
| "fmt" | |
| "os" | |
| "os/exec" | |
| "path/filepath" | |
| "strconv" | |
| "strings" | |
| "testing" | |
| "time" | |
| ) | |
| // Note: In order to avoid needing to mock out exec.Cmd or to have external | |
| // program(s) act as mock subprocesses, we implement the subprocess mocks here | |
| // in the main test binary (see TestDoProcessMock) and spawn a copy of ourself | |
| // as a subprocess whenever mockCommand is called. A special environment | |
| // variable is set to trigger the mocking behavior, and the first non-flag | |
| // command-line argument is used as the command name so we know which specific | |
| // behavior to emulate. Despite being misleadingly named process_test.go (which | |
| // is necessary so that we can invoke TestDoProcessMock in the subprocess), | |
| // this file is actually mostly process mocking. | |
| // Drop-in replacement for exec.Command for use during testing. | |
| func mockCommand(command string, args ...string) *exec.Cmd { | |
| // Call ourself as the subprocess so we can mock behavior as appropriate | |
| argv := []string{"-logtostderr", "-test.run=TestDoProcessMock", "--", command} | |
| argv = append(argv, args...) | |
| cmd := exec.Command(os.Args[0], argv...) | |
| cmd.Env = os.Environ() | |
| cmd.Env = append(cmd.Env, "MOCK_PROCESS=yes") | |
| return cmd | |
| } | |
| // This is the actual implementation of mocked subprocesses. | |
| // The name is a little confusing because it has to have the Test prefix | |
| func TestDoProcessMock(t *testing.T) { | |
| if os.Getenv("MOCK_PROCESS") != "yes" { | |
| t.Skip("Not in a subprocess") | |
| } | |
| cmd, args := flag.Arg(0), flag.Args()[1:] | |
| // Check all args for a special invalid path being passed along | |
| for _, arg := range args { | |
| if strings.Contains(arg, invalidPath) { | |
| fmt.Println("file not found") | |
| os.Exit(1) | |
| } | |
| } | |
| var out string | |
| var exitCode int | |
| var stayAlive bool | |
| switch filepath.Base(cmd) { | |
| case "echo": | |
| out = strings.Join(args, " ") + "\n" | |
| exitCode = 0 | |
| case FakeQemuFailing: | |
| out = "qemu error message" | |
| exitCode = 1 | |
| case FakeQemuSlow: | |
| stayAlive = true | |
| out = "welcome to qemu for turtles 🐢" | |
| exitCode = 0 | |
| case "cp", "fvm", "zbi": | |
| // These utilities already had their args checked for invalid paths | |
| // above, so at this point it's a no-op | |
| exitCode = 0 | |
| case "qemu-system-x86_64", "qemu-system-aarch64": | |
| stayAlive = true | |
| out = fmt.Sprintf("'%s'\n", successfulBootMarker) | |
| exitCode = 0 | |
| case "ps": | |
| // This is kind of a mess. We can't rely on ps to reflect the killed | |
| // state of a child process that we don't wait for if we, the parent | |
| // process, are still alive, because it will become a zombie and still | |
| // show up in the process list. This hacks around that by filtering out | |
| // zombie processes and then patching in an appropriate exit code. | |
| // Request an explicit output format to normalize different flavors of `ps`: | |
| args = append([]string{"-ostate="}, args...) | |
| cmd := exec.Command(cmd, args...) | |
| psOut, err := cmd.Output() | |
| if err != nil { | |
| if cmderr, ok := err.(*exec.ExitError); ok { | |
| exitCode = cmderr.ExitCode() | |
| } else { | |
| t.Fatalf("Error proxying ps call: %s", err) | |
| } | |
| } | |
| if strings.HasPrefix(string(psOut), "Z") { | |
| exitCode = 1 | |
| } | |
| case "symbolize": | |
| if err := fakeSymbolize(os.Stdin, os.Stdout); err != nil { | |
| t.Fatalf("failed during scan: %s", err) | |
| } | |
| exitCode = 0 | |
| default: | |
| exitCode = 127 | |
| } | |
| os.Stdout.WriteString(out) | |
| if stayAlive { | |
| // Simulate a long-running process | |
| time.Sleep(10 * time.Second) | |
| } | |
| os.Exit(exitCode) | |
| } | |
| // This tests the subprocess mocking itself | |
| func TestProcessMocking(t *testing.T) { | |
| // Enable subprocess mocking | |
| ExecCommand = mockCommand | |
| defer func() { ExecCommand = exec.Command }() | |
| cmd := NewCommand("echo", "it", "works") | |
| out, err := cmd.Output() | |
| if err != nil { | |
| t.Fatalf("Error running mock command: %s", err) | |
| } | |
| if string(out) != "it works\n" { | |
| t.Fatalf("Mock command returned unexpected output: %q", out) | |
| } | |
| cmd = NewCommand("ps", "-p", strconv.Itoa(os.Getpid())) | |
| out, err = cmd.Output() | |
| if err != nil { | |
| t.Fatalf("Error running mock command: %s (%q)", err, out) | |
| } | |
| } |