blob: 3af4854178c84c51605ae3203df8806c7cc67044 [file] [log] [blame]
// Copyright 2021 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 fstar_integration
import (
"bytes"
"context"
"encoding/json"
"fmt"
"os"
"os/exec"
"os/signal"
"os/user"
"path/filepath"
"strings"
"syscall"
"testing"
"github.com/google/go-cmp/cmp"
"go.fuchsia.dev/fuchsia/tools/botanist/constants"
"go.fuchsia.dev/fuchsia/tools/lib/ffxutil"
"go.fuchsia.dev/fuchsia/tools/sdk-tools/sdkcommon"
)
type deviceInfo struct {
Label string `json:"label"`
Child []deviceInfoChild `json:"child"`
}
type deviceInfoChild struct {
Label string `json:"label"`
// Use a interface because the value can be either bool, int, or string but will always be
// a string when the label is set to "target", the only label we care about.
Value interface{} `json:"value"`
}
const (
targetLabel = "target"
itemLabel = "name"
)
type toolsPath struct {
ffxPath string
ffxInstance *ffxutil.FFXInstance
ffxConfigPath string // Used to make ffx isolated.
fconfigPath string
fsshPath string
}
func verifyFFXTargetShowOutputWithDeviceName(output []byte, expectedDeviceName string) (bool, error) {
var fullDeviceInfo []deviceInfo
err := json.Unmarshal(output, &fullDeviceInfo)
if err != nil {
return false, err
}
for _, ele := range fullDeviceInfo {
if ele.Label == targetLabel {
for _, item := range ele.Child {
if item.Label == itemLabel {
return item.Value.(string) == expectedDeviceName, nil
}
}
}
}
return false, fmt.Errorf("output json from 'ffx target show --json' is missing the target name label")
}
// findSubDir searches the root dir and returns the first path that matches dirName.
func findSubDir(root, dirName string) string {
filePath := ""
filepath.WalkDir(root,
func(path string, info os.DirEntry, err error) error {
if err != nil {
return err
}
if info.IsDir() && info.Name() == dirName {
filePath = path
}
return nil
})
return filePath
}
// setUp sets up the environment, finds the paths for the host tools, and creates
// an isolated ffx instance.
func setUp(t *testing.T) (toolsPath, error) {
var tools toolsPath
ex, err := os.Executable()
if err != nil {
return tools, err
}
exDir := filepath.Dir(ex)
runtimeDir := findSubDir(exDir, "fstar_runtime_deps")
if len(runtimeDir) == 0 {
return tools, fmt.Errorf("cannot find fstar_runtime_deps binary from %s", exDir)
}
hostToolsDir := filepath.Join(runtimeDir, "host_tools")
if _, err := os.Stat(hostToolsDir); os.IsNotExist(err) {
return tools, err
}
tools.ffxPath = filepath.Join(hostToolsDir, "ffx")
if _, err := os.Stat(tools.ffxPath); os.IsNotExist(err) {
return tools, fmt.Errorf("unable to find ffx: %w", err)
}
tools.fsshPath = filepath.Join(hostToolsDir, "fssh")
if _, err := os.Stat(tools.fsshPath); os.IsNotExist(err) {
return tools, fmt.Errorf("unable to find fssh: %w", err)
}
tools.fconfigPath = filepath.Join(hostToolsDir, "fconfig")
if _, err := os.Stat(tools.fconfigPath); os.IsNotExist(err) {
return tools, fmt.Errorf("unable to find fconfig: %w", err)
}
testOutDir := filepath.Join(t.TempDir(), "fssh_test")
// Create a new isolated ffx instance.
tools.ffxInstance, err = ffxutil.NewFFXInstance(tools.ffxPath, "", os.Environ(), os.Getenv(constants.NodenameEnvKey), os.Getenv(constants.SSHKeyEnvKey), testOutDir)
if err != nil {
return tools, fmt.Errorf("unable to create new ffx instance %w", err)
}
tools.ffxConfigPath = filepath.Join(testOutDir, "ffx_config.json")
if _, err := os.Stat(tools.ffxConfigPath); os.IsNotExist(err) {
return tools, fmt.Errorf("ffx config path does not exist: %v", err)
}
return tools, nil
}
func resetBuffers(stdout, stderr *bytes.Buffer) {
stdout.Reset()
stderr.Reset()
}
func TestFSSH(t *testing.T) {
// In order to make sure the test cleans up the ffx instance, we capture SIGTERM until
// we ensured the ffx instance is cleaned up. This is needed in a scenario where the test may exit
// unexpectedly in infra (similar to pressing Ctrl-C when running this test locally).
ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGTERM, syscall.SIGINT)
defer cancel()
tools, err := setUp(t)
t.Cleanup(func() {
os.Unsetenv(sdkcommon.FFXIsolatedEnvKey)
if tools.ffxInstance != nil {
if err := tools.ffxInstance.Stop(); err != nil {
t.Logf("FFX didn't stop the running daemon %s", err)
}
}
})
if err != nil {
t.Fatalf("unable to setup environment: %s", err)
}
deviceName := os.Getenv(constants.NodenameEnvKey)
var stdoutBuf []byte
stdout := bytes.NewBuffer(stdoutBuf)
var stderrBuf []byte
stderr := bytes.NewBuffer(stderrBuf)
tools.ffxInstance.SetStdoutStderr(stdout, stderr)
// Get the device IP.
t.Logf("Getting the device IP")
err = tools.ffxInstance.List(ctx, "--format", "a", deviceName)
if err != nil {
t.Fatalf("ffx target list returned unexpected error: %s", err)
}
ffxTargetListStderr := strings.TrimSpace(stderr.String())
deviceIP := strings.TrimSpace(stdout.String())
// sdkcommon.go runs ffx with this configuration (using the --config flag) to ensure
// that we are using the isolated ffx instance.
// TODO(fxbug.dev/88287): Migrate to use flag when sdkcommon.go supports it.
os.Setenv(sdkcommon.FFXIsolatedEnvKey, tools.ffxConfigPath)
if ffxTargetListStderr != "" {
t.Fatalf("ffx target list returned unexpected output to stderr: %s", ffxTargetListStderr)
}
if deviceIP == "" {
t.Fatalf("device ip is empty")
}
t.Logf("Using device name: %s and device ip: %s", deviceName, deviceIP)
// Set the default device in fconfig.
fconfigSetDeviceArgs := []string{"set-device", deviceName, "--default"}
t.Logf("Setting the default device by running: %s %s", tools.fconfigPath, fconfigSetDeviceArgs)
cmd := exec.Command(tools.fconfigPath, fconfigSetDeviceArgs...)
_, err = cmd.Output()
if err != nil {
t.Errorf("fconfig returned unexpected error: %s", err)
}
usr, err := user.Current()
if err != nil {
t.Fatalf("unable to get users home dir: %s", err)
}
expectedGetAllDeviceConfig := sdkcommon.DeviceConfig{
DeviceName: deviceName,
Bucket: "fuchsia",
Image: "",
DeviceIP: deviceIP,
SSHPort: "22",
PackageRepo: filepath.Join(usr.HomeDir, ".fuchsia", deviceName, "packages", "amber-files"),
PackagePort: "8083",
IsDefault: true,
Discoverable: true,
}
// Get default device information from fconfig.
fconfigGetAllArgs := []string{"get-all", deviceName}
t.Logf("Getting the default device information by running: %s %s", tools.fconfigPath, fconfigGetAllArgs)
cmd = exec.Command(tools.fconfigPath, fconfigGetAllArgs...)
fconfigGetAllOutput, err := cmd.Output()
var deviceConfig sdkcommon.DeviceConfig
if err := json.Unmarshal(fconfigGetAllOutput, &deviceConfig); err != nil {
t.Fatalf("unable to unmarshal device config from fconfig get-all: %s", err)
}
if diff := cmp.Diff(expectedGetAllDeviceConfig, deviceConfig); diff != "" {
t.Errorf("fconfig get-all mismatch (-want +got):\n%s", diff)
}
// fssh into the default device from fconfig.
fsshArgs := []string{"-private-key", os.Getenv(constants.SSHKeyEnvKey), "echo", "\"Hello World\""}
t.Logf("SSH'ing into the default device by running: %s %s", tools.fsshPath, fsshArgs)
cmd = exec.Command(tools.fsshPath, fsshArgs...)
sshOutput, err := cmd.Output()
if err != nil {
t.Fatalf("fssh returned unexpected error: %s", err)
}
sshOutputTrimmed := strings.TrimSpace(string(sshOutput))
expectedOutput := "Hello World"
if sshOutputTrimmed != expectedOutput {
t.Errorf("fssh output doesn't match; Got: %s, want: %s", sshOutputTrimmed, expectedOutput)
}
// Reset the stdout and stderr buffers as they were previously used.
resetBuffers(stdout, stderr)
ffxTargetShowArgs := []string{"target", "show", "--json"}
t.Logf("Trying to get target information by running: ffx %s", ffxTargetShowArgs)
err = tools.ffxInstance.Run(ctx, ffxTargetShowArgs...)
if err != nil {
t.Fatalf("ffx target show returned unexpected error: %s", err)
}
ffxTargetShowErr := strings.TrimSpace(stderr.String())
if ffxTargetShowErr != "" {
t.Fatalf("ffx target show returned unexpected output to stderr: %s", ffxTargetShowErr)
}
isValid, err := verifyFFXTargetShowOutputWithDeviceName(stdout.Bytes(), deviceName)
if err != nil {
t.Errorf("verifyFFXTargetShowOutputWithDeviceName(%#v): got err %s", stdout.String(), err)
}
if !isValid {
t.Errorf("verifyFFXTargetShowOutputWithDeviceName(%#v): output doesn't contain the expected device name %s", stdout.String(), deviceName)
}
}