blob: 56bd0df4a8b8020c53c74da70dc0016b4822a3e6 [file] [log] [blame]
// 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"
"io"
"math/rand"
"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 {
// For ps, pass through directly
if command == "ps" {
return exec.Command(command, args...)
}
// Call ourself as the subprocess so we can mock behavior as appropriate
argv := []string{"-logtostderr", "-test.run=TestDoProcessMock", "--", command}
argv = append(argv, args...)
selfPath, _ := filepath.Abs(os.Args[0])
cmd := exec.Command(selfPath, argv...)
cmd.Env = os.Environ()
cmd.Env = append(cmd.Env, "MOCK_PROCESS=yes")
// Ensure the subprocess CPRNG is seeded uniquely (but deterministically)
cmd.Env = append(cmd.Env, fmt.Sprintf("RAND_SEED=%d", rand.Int63()))
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")
}
seed, err := strconv.ParseInt(os.Getenv("RAND_SEED"), 10, 64)
if err != nil {
t.Fatalf("Invalid CPRNG seed: %s", err)
}
rand.Seed(seed)
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
// The following utilities already had their args checked for invalid paths
// above, so at this point we just need to touch an expected output file.
// The contents are randomized to allow for simple change detection.
case "cp":
touchRandomFile(t, args[len(args)-1])
exitCode = 0
case "fvm":
touchRandomFile(t, args[0])
exitCode = 0
case "zbi":
found := false
for j, arg := range args[:len(args)-1] {
if arg == "-o" {
touchRandomFile(t, args[j+1])
found = true
}
}
if !found {
t.Fatalf("No output specified in zbi command args: %s", args)
}
exitCode = 0
case "qemu-system-x86_64", "qemu-system-aarch64":
stayAlive = true
var logFile string
// Check for a logfile specified in a serial parameter
for j, arg := range args[:len(args)-1] {
if arg != "-serial" {
continue
}
parts := strings.SplitN(args[j+1], ":", 2)
if parts[0] == "file" {
logFile = parts[1]
}
}
logWriter := os.Stdout
if logFile != "" {
outFile, err := os.Create(logFile)
if err != nil {
t.Fatalf("error creating qemu log file: %s", err)
}
defer outFile.Close()
logWriter = outFile
}
io.WriteString(logWriter, "early boot\n")
io.WriteString(logWriter, successfulBootMarker+"\n")
// Output >100KB of additional data to ensure we handle large logs correctly
filler := strings.Repeat("data", 1024/4)
for j := 0; j < 100; j++ {
logLine := fmt.Sprintf("%d: %s", j, filler)
io.WriteString(logWriter, logLine)
}
// Write a final success marker
io.WriteString(logWriter, lateBootMessage+"\n")
exitCode = 0
case "symbolizer":
if err := fakeSymbolize(os.Stdin, os.Stdout); err != nil {
t.Fatalf("failed during scan: %s", err)
}
exitCode = 0
case "ffx":
// Consume any leading flags and their parameters
i := 0
params := make(map[string]string)
for i < len(args)-1 {
if !strings.HasPrefix(args[i], "-") {
break
}
params[args[i]] = args[i+1]
i += 2
}
// Validate presence of target parameter, but then ignore it
if _, ok := params["--target"]; !ok {
t.Fatalf("ffx call missing target parameter")
}
// Validate isolate dir exists
if isolateDir, ok := params["--isolate-dir"]; ok {
if !fileExists(isolateDir) {
t.Fatalf("isolate-dir %s doesn't exist", isolateDir)
}
} else {
t.Fatalf("ffx call missing isolate-dir parameter")
}
command := args[i]
args = args[i+1:]
switch command {
case "config":
if args[0] != "set" {
// Return an error, but avoid emitting what looks like a test
// failure, because we know that TestFfxRunInvalidCommand will
// call this and echo the output.
out = fmt.Sprintf("invalid ffx config command: %s", args)
exitCode = 1
}
case "daemon":
if args[0] != "stop" {
t.Fatalf("invalid ffx daemon command: %s", args)
}
case "target":
if args[0] != "add" && args[0] != "remove" {
t.Fatalf("invalid ffx target command: %s", args)
}
case "fuzz":
stdout, outputDir := getFfxFuzzOutput(t, args)
out = stdout
// Perform any side-effect actions
switch args[0] {
case "minimize", "run":
// Write fake artifact to output directory
artifactDir := filepath.Join(outputDir, "artifacts")
if err := os.Mkdir(artifactDir, os.ModeDir|0o700); err != nil {
t.Fatalf("error making artifact dir: %s", err)
}
artifactPath := filepath.Join(artifactDir, "crash-1312")
if err := os.WriteFile(artifactPath, []byte("data"), 0o600); err != nil {
t.Fatalf("error writing artifact: %s", err)
}
case "fetch", "merge":
// Fetch a fake live corpus into place
corpusDir := filepath.Join(outputDir, "corpus")
if err := os.Mkdir(corpusDir, os.ModeDir|0o700); err != nil {
t.Fatalf("error making live corpus dir: %s", err)
}
touchRandomFile(t, filepath.Join(corpusDir, "a"))
touchRandomFile(t, filepath.Join(corpusDir, "b"))
}
exitCode = 0
default:
t.Fatalf("invalid ffx command: %s", command)
}
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)
}
}