blob: 87f48b282fdde606c014d8b9ab94a854e10290ec [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 (
"bytes"
"os"
"path/filepath"
"reflect"
"strings"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
)
func newTestInstance() *BaseInstance {
build, _ := newMockBuild()
return &BaseInstance{Build: build, reconnectInterval: defaultReconnectInterval}
}
func TestInstanceHandle(t *testing.T) {
// Instance loading automatically loads the build, so we need to stub it out
realNewBuild := NewBuild
NewBuild = newMockBuild
defer func() { NewBuild = realNewBuild }()
instance := newTestInstance()
instance.Launcher = &QemuLauncher{Pid: 404, TmpDir: "/some/dir"}
instance.Connector = NewSSHConnector(nil, "somehost", 123, "keyfile", "/tmp/path")
handle, err := instance.Handle()
if err != nil {
t.Fatalf("error creating handle: %s", err)
}
defer handle.Release()
handle2, err := instance.Handle()
if handle.Serialize() != handle2.Serialize() {
handle2.Release()
t.Fatalf("same instance returned different handles: %q, %q", handle, handle2)
}
// Note: we don't serialize here because that is covered by handle tests
reloadedInstance, err := loadInstanceFromHandle(handle, true)
if err != nil {
t.Fatalf("error loading instance from handle: %s", err)
}
got, ok := reloadedInstance.(*BaseInstance)
if !ok {
t.Fatalf("incorrect instance type")
}
if diff := cmp.Diff(instance, got, cmpopts.IgnoreUnexported(BaseInstance{},
SSHConnector{}, QemuLauncher{}, mockBuild{})); diff != "" {
t.Fatalf("incorrect data in reloaded instance (-want +got):\n%s", diff)
}
// Ensure that handle is released and invalid upon stopping
if err := instance.Stop(); err != nil {
t.Fatalf("error stopping instance: %s", err)
}
reloadedInstance, err = loadInstanceFromHandle(handle, true)
if err == nil {
t.Fatalf("unexpected success reloading instance from released handle: %q", handle)
}
}
func TestInstanceLoadHandleFromOldVersion(t *testing.T) {
// Instance loading automatically loads the build, so we need to stub it out
realNewBuild := NewBuild
NewBuild = newMockBuild
defer func() { NewBuild = realNewBuild }()
// Create a handle file mimicking an earlier version. We assume for
// simplicity's sake that old versions of a given Connector type will
// always be a subset of the new type.
json := `{"LauncherType": "QemuLauncher", "Launcher": {"Pid": 123}}`
handlePath := filepath.Join(t.TempDir(), "some.handle")
if err := os.WriteFile(handlePath, []byte(json), 0o600); err != nil {
t.Fatalf("error writing handle: %s", err)
}
handle := Handle(handlePath)
if _, err := loadInstanceFromHandle(handle, true); err == nil {
t.Fatalf("unexpectedly succeeded loading incomplete instance")
}
instance, err := loadInstanceFromHandle(handle, false)
if err != nil {
t.Fatalf("error loading instance from handle: %s", err)
}
if instance.(*BaseInstance).Launcher.(*QemuLauncher).Pid != 123 {
t.Fatalf("incorrect launcher pid found: %+v", instance)
}
}
func TestInstanceRepeatedStart(t *testing.T) {
i := newTestInstance()
i.Launcher = NewMockLauncher(t)
if err := i.Start(); err != nil {
t.Fatalf("Error starting instance: %s", err)
}
if err := i.Start(); err == nil {
t.Fatalf("Expected error starting already-running instance but succeeded")
}
}
func TestInstanceStartWithLauncherFailure(t *testing.T) {
i := newTestInstance()
launcher := NewMockLauncher(t)
launcher.shouldFailToStart = true
i.Launcher = launcher
if err := i.Start(); err == nil {
t.Fatalf("expected launcher failure but succeeded")
}
}
func TestInstanceStartWithLauncherEarlyExit(t *testing.T) {
i := newTestInstance()
// Only the first connection will fail, but no retries should be made
// because the launcher will have died.
launcher := NewMockLauncher(t)
launcher.shouldFailToConnectCount = 1
launcher.shouldExitEarly = true
i.Launcher = launcher
if err := i.Start(); err == nil {
t.Fatalf("expected launcher failure but succeeded")
}
if running, _ := i.Launcher.IsRunning(); running {
t.Fatalf("expected launcher to have been killed, but it is running")
}
if !i.Connector.(*mockConnector).CleanedUp {
t.Fatalf("connector not cleaned up")
}
}
func TestInstanceStartWithTemporaryConnectorFailure(t *testing.T) {
i := newTestInstance()
launcher := NewMockLauncher(t)
launcher.shouldFailToConnectCount = maxInitialConnectAttempts - 1
i.Launcher = launcher
i.reconnectInterval = 1 * time.Millisecond
if err := i.Start(); err != nil {
t.Fatalf("Expected connector to succeed after reconnection attempt")
}
if err := i.Stop(); err != nil {
t.Fatalf("Error stopping instance: %s", err)
}
if running, _ := i.Launcher.IsRunning(); running {
t.Fatalf("expected launcher to have been killed, but it is running")
}
if !i.Connector.(*mockConnector).CleanedUp {
t.Fatalf("connector not cleaned up")
}
}
func TestInstanceStartWithPermanentConnectorFailure(t *testing.T) {
i := newTestInstance()
launcher := NewMockLauncher(t)
launcher.shouldFailToConnectCount = maxInitialConnectAttempts
i.Launcher = launcher
i.reconnectInterval = 1 * time.Millisecond
if err := i.Start(); err == nil {
t.Fatalf("Expected connector failure but succeeded")
}
if running, _ := i.Launcher.IsRunning(); running {
t.Fatalf("expected launcher to have been killed, but it is running")
}
if !i.Connector.(*mockConnector).CleanedUp {
t.Fatalf("connector not cleaned up")
}
}
func TestInstanceStartWithTemporaryCommandFailure(t *testing.T) {
i := newTestInstance()
launcher := NewMockLauncher(t)
launcher.shouldFailToConnectCount = maxInitialConnectAttempts - 1
i.Launcher = launcher
i.reconnectInterval = 1 * time.Millisecond
if err := i.Start(); err != nil {
t.Fatalf("Expected to succeed after command re-execution attempt")
}
if err := i.Stop(); err != nil {
t.Fatalf("Error stopping instance: %s", err)
}
if running, _ := i.Launcher.IsRunning(); running {
t.Fatalf("expected launcher to have been killed, but it is running")
}
if !i.Connector.(*mockConnector).CleanedUp {
t.Fatalf("connector not cleaned up")
}
}
func TestInstanceStartWithPermanentCommandFailure(t *testing.T) {
i := newTestInstance()
launcher := NewMockLauncher(t)
launcher.shouldFailToConnectCount = maxInitialConnectAttempts + 1
i.Launcher = launcher
i.reconnectInterval = 1 * time.Millisecond
if err := i.Start(); err == nil {
t.Fatalf("Expected command execution failure but succeeded")
}
if running, _ := i.Launcher.IsRunning(); running {
t.Fatalf("expected launcher to have been killed, but it is running")
}
if !i.Connector.(*mockConnector).CleanedUp {
t.Fatalf("connector not cleaned up")
}
}
func TestInstance(t *testing.T) {
i := newTestInstance()
i.Launcher = NewMockLauncher(t)
if err := i.Start(); err != nil {
t.Fatalf("Error starting instance: %s", err)
}
fuzzers := i.ListFuzzers()
if !reflect.DeepEqual(fuzzers, []string{"foo/bar", "fail/nopid", "fail/notfound",
"cff/fuzzer", "cff/broken"}) {
t.Fatalf("incorrect fuzzer list: %v", fuzzers)
}
var outBuf bytes.Buffer
if err := i.RunFuzzer(&outBuf, "foo/bar", "", "arg1", "arg2"); err != nil {
t.Fatalf("Error running fuzzer: %s", err)
}
out := outBuf.String()
// Just a basic check here, more are done in fuzzer_tests
if !strings.Contains(out, "fuchsia-pkg://fuchsia.com/foo#meta/bar.cm") {
t.Fatalf("fuzzer output missing package: %q", out)
}
if err := i.RunFuzzer(&outBuf, "invalid/fuzzer", ""); err == nil {
t.Fatalf("expected error when running invalid fuzzer")
}
tmpDir := t.TempDir()
remotePath := "data/path/to/remoteFile"
if err := i.Get("foo/bar", remotePath, tmpDir); err != nil {
t.Fatalf("Error getting from instance: %s", err)
}
expected := []transferCmd{
{"/tmp/fuzz_ctl/fuchsia.com/foo/bar/path/to/remoteFile",
tmpDir}}
got := i.Connector.(*mockConnector).PathsGot
if !reflect.DeepEqual(got, expected) {
t.Fatalf("incorrect file get list: %v", got)
}
tmpFile := filepath.Join(tmpDir, "secrets.ini")
if err := os.WriteFile(tmpFile, []byte("shh"), 0o600); err != nil {
t.Fatalf("error writing file: %s", err)
}
remotePath = "data/path/to/otherFile"
if err := i.Put("foo/bar", tmpFile, remotePath); err != nil {
t.Fatalf("Error putting to instance: %s", err)
}
expected = []transferCmd{{tmpFile,
"/tmp/fuzz_ctl/fuchsia.com/foo/bar/path/to/otherFile"}}
got = i.Connector.(*mockConnector).PathsPut
if !reflect.DeepEqual(got, expected) {
t.Fatalf("incorrect file put list: %v", got)
}
var logBuf bytes.Buffer
if err := i.GetLogs(&logBuf); err != nil {
t.Fatalf("error getting instance logs: %s", err)
}
log := logBuf.String()
if log != "system log\n" {
t.Fatalf("unexpected instance log: %q", log)
}
if err := i.Stop(); err != nil {
t.Fatalf("Error stopping instance: %s", err)
}
if running, _ := i.Launcher.IsRunning(); running {
t.Fatalf("expected launcher to have been killed, but it is running")
}
if !i.Connector.(*mockConnector).CleanedUp {
t.Fatalf("Connector not cleaned up when instance stopped")
}
}
func TestInstanceRunFuzzerWithArtifactFetch(t *testing.T) {
i := newTestInstance()
i.Launcher = NewMockLauncher(t)
if err := i.Start(); err != nil {
t.Fatalf("Error starting instance: %s", err)
}
hostArtifactDir := t.TempDir()
var outBuf bytes.Buffer
args := []string{"-artifact_prefix=data/wow/x", "data/corpus"}
if err := i.RunFuzzer(&outBuf, "foo/bar", hostArtifactDir, args...); err != nil {
t.Fatalf("Error running fuzzer: %s", err)
}
out := outBuf.String()
if !strings.Contains(out, filepath.Join(hostArtifactDir, "xcrash-1312")) {
t.Fatalf("fuzzer output missing host artifact path: %q", out)
}
expected := []transferCmd{{"/tmp/fuzz_ctl/fuchsia.com/foo/bar/wow/xcrash-1312",
hostArtifactDir}}
got := i.Connector.(*mockConnector).PathsGot
if !reflect.DeepEqual(got, expected) {
t.Fatalf("incorrect file get list: %v", got)
}
if err := i.Stop(); err != nil {
t.Fatalf("Error stopping instance: %s", err)
}
if running, _ := i.Launcher.IsRunning(); running {
t.Fatalf("expected launcher to have been killed, but it is running")
}
}