blob: 62343a4ee9b66186e39a22b13a9ec24bc5fbbb57 [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...)
cmd := exec.Command(os.Args[0], 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
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)
}
}