blob: de1b7302d219feff78f73d4bf601ab9397cb0dbd [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 bootservertest includes common code to write smoke tests for
// bootserver_old.
package bootservertest
import (
"bufio"
"context"
"flag"
"io"
"os"
"os/exec"
"path/filepath"
"regexp"
"runtime"
"strings"
"testing"
"time"
"go.fuchsia.dev/fuchsia/tools/emulator"
"go.fuchsia.dev/fuchsia/tools/emulator/emulatortest"
fvdpb "go.fuchsia.dev/fuchsia/tools/virtual_device/proto"
)
var hostDir = map[string]string{"arm64": "host_arm64", "amd64": "host_x64"}[runtime.GOARCH]
func testDataDir() string {
base := filepath.Join("..", "..", "..", "..")
c, err := os.ReadFile(filepath.Join(base, ".fx-build-dir"))
if err != nil {
return ""
}
return filepath.Join(base, strings.TrimSpace(string(c)), hostDir, "test_data")
}
// TestDataDir is the location to test data files.
//
// - In "go test" mode, it's relative to this directory.
// - In "fx test" mode, it's relative to the build directory (out/default).
var TestDataDir = flag.String("test_data_dir", testDataDir(), "Path to test_data/; only used in GN build")
// DefaultNodename is the default nodename given to an target with the default
// QEMU MAC address.
const DefaultNodename = "swarm-donut-petri-acre"
// ToolPath returns the full path to a tool.
func ToolPath(t *testing.T, name string) string {
p, err := filepath.Abs(filepath.Join(*TestDataDir, "bootserver_tools", name))
if err != nil {
t.Fatal(err)
}
return p
}
// CmdWithOutput returns a command and returns stdout.
func CmdWithOutput(t *testing.T, name string, arg ...string) []byte {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
cmd := exec.CommandContext(ctx, name, arg...)
out, err := cmd.Output()
if err != nil {
t.Errorf("%s failed %s, err=%s", name, out, err)
return nil
}
return out
}
// LogMatch is one pattern to search for.
type LogMatch struct {
Pattern string
ShouldMatch bool
}
func matchPattern(t *testing.T, pattern string, reader *bufio.Reader) bool {
for {
line, err := reader.ReadString('\n')
if err != nil {
if err == io.EOF {
t.Logf("matchPattern(%q): EOF", pattern)
return false
}
t.Fatal(err)
}
if strings.Contains(line, pattern) {
t.Logf("matchPattern(%q): true", pattern)
return true
}
t.Logf("matchPattern(%q): ignored %q", pattern, line)
}
}
// CmdSearchLog searches for a patterns in stderr.
func CmdSearchLog(t *testing.T, logPatterns []LogMatch, name string, arg ...string) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
cmd := exec.CommandContext(ctx, name, arg...)
cmderr, err := cmd.StderrPipe()
if err != nil {
t.Errorf("Failed to stderr %s", err)
}
readerErr := bufio.NewReader(cmderr)
if err := cmd.Start(); err != nil {
t.Errorf("Failed to start %s", err)
}
found := false
for _, logPattern := range logPatterns {
match := matchPattern(t, logPattern.Pattern, readerErr)
if match != logPattern.ShouldMatch {
found = false
t.Errorf("Log pattern \"%s\" mismatch. Expected - %t, actual - %t",
logPattern.Pattern, logPattern.ShouldMatch, match)
break
}
found = true
}
if err := cmd.Wait(); err != nil {
t.Logf("Failed to wait on task %s", err)
}
if ctx.Err() == context.DeadlineExceeded {
t.Errorf("%s timed out err=%s", name, ctx.Err())
} else if !found {
t.Errorf("%s failed to match logs", name)
} else {
t.Logf("%s worked as expected", name)
}
}
// AttemptPaveNoBind attempts to initiate a pave.
func AttemptPaveNoBind(t *testing.T, shouldWork bool) {
// Get the node ipv6 address.
out := CmdWithOutput(t, ToolPath(t, "netls"))
// Extract the ipv6 from the netls output
regexString := DefaultNodename + ` \((?P<ipv6>.*)\)`
match := regexp.MustCompile(regexString).FindStringSubmatch(string(out))
if len(match) != 2 {
t.Errorf("Node %s not found in netls output - %s", DefaultNodename, out)
return
}
var logPattern []LogMatch
if shouldWork {
paveWorksPattern := []LogMatch{
{"Sending request to ", true},
{"Received request from ", true},
{"Proceeding with nodename ", true},
{"Transfer starts", true},
}
logPattern = paveWorksPattern
} else {
paveFailsPattern := []LogMatch{
{"Sending request to ", true},
{"Received request from ", false},
{"Proceeding with nodename ", false},
{"Transfer starts", false},
}
logPattern = paveFailsPattern
}
CmdSearchLog(
t, logPattern,
ToolPath(t, "bootserver"), "--fvm", "\"dummy.blk\"",
"--no-bind", "-a", match[1], "-1", "--fail-fast")
}
// StartQemu starts a QEMU instance with the given kernel commandline args.
func StartQemu(t *testing.T, appendCmdline []string, modeString string) *emulatortest.Instance {
if testing.Short() {
t.Skip("skipping test in short mode.")
}
distro := emulatortest.UnpackFrom(t, *TestDataDir, emulator.DistributionParams{
Emulator: emulator.Qemu,
})
arch := distro.TargetCPU()
device := emulator.DefaultVirtualDevice(string(arch))
device.KernelArgs = append(device.KernelArgs, appendCmdline...)
device.KernelArgs = append(device.KernelArgs, "zircon.nodename="+DefaultNodename)
// To run locally on linux, you need to configure a tuntap interface:
// $ sudo ip add mode tap qemu user `whoami`
// $ sudo ip link set qemu up
device.Hw.NetworkDevices = append(device.Hw.NetworkDevices, &fvdpb.Netdev{
Id: "qemu",
Kind: "tap",
Device: &fvdpb.Device{Model: "virtio-net-pci"},
})
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
instance := distro.CreateContext(ctx, device)
instance.Start()
// Make sure netsvc in expected mode.
instance.WaitForLogMessage("netsvc: running in " + modeString + " mode")
// Make sure netsvc is booted.
instance.WaitForLogMessage("netsvc: start")
return instance
}