|  | // 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" | 
|  | "io/ioutil" | 
|  | "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 := ioutil.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, "devmgr.log-to-debuglog=true", "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"}, | 
|  | }) | 
|  | instance := distro.Create(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 | 
|  | } |