blob: 0728a0c35b057f81c525d7a9ab071fb206ad5a10 [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 main
import (
"context"
"flag"
"fmt"
"io"
"log"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"syscall"
"testing"
"go.fuchsia.dev/fuchsia/tools/lib/color"
"go.fuchsia.dev/fuchsia/tools/lib/logger"
"go.fuchsia.dev/fuchsia/tools/sdk-tools/sdkcommon"
)
// test argument pointing to the directory containing the testdata directory.
// This is configured in the BUILD.gn file.
var testrootFlag = flag.String("testroot", "", "Root directory of the files needed to execute the test.")
// Context with a logger used to test.
func testingContext() context.Context {
flags := log.Ltime | log.Lshortfile
log := logger.NewLogger(logger.DebugLevel, color.NewColor(color.ColorAuto), os.Stdout, os.Stderr, "fserve_test ")
log.SetFlags(flags)
return logger.WithLogger(context.Background(), log)
}
// See exec_test.go for details, but effectively this runs the function called TestHelperProcess passing
// the args.
func helperCommandForFServe(command string, s ...string) (cmd *exec.Cmd) {
cs := []string{"-test.run=TestFakeFServe", "--"}
cs = append(cs, command)
cs = append(cs, s...)
cmd = exec.Command(os.Args[0], cs...)
// Set this in the enviroment, so we can control the result.
cmd.Env = append(os.Environ(), "GO_WANT_HELPER_PROCESS=1")
return cmd
}
func TestKillServers(t *testing.T) {
ctx := testingContext()
ExecCommand = helperCommandForFServe
findProcess = mockedFindProcess
defer func() {
ExecCommand = exec.Command
findProcess = defaultFindProcess
}()
// Test no existing servers
os.Setenv("FSERVE_TEST_NO_SERVERS", "1")
if err := killServers(ctx, ""); err != nil {
t.Fatal(err)
}
// Test existing servers
os.Setenv("FSERVE_TEST_NO_SERVERS", "0")
if err := killServers(ctx, ""); err != nil {
t.Fatal(err)
}
if err := killServers(ctx, "8083"); err != nil {
t.Fatal(err)
}
os.Setenv("FSERVE_TEST_PGREP_ERROR", "1")
err := killServers(ctx, "")
if err == nil {
t.Fatal("Expected error running pgrep, got no error.")
}
expected := "Error running pgrep: Expected error\n"
actual := fmt.Sprintf("%v", err)
if expected != actual {
t.Fatalf("[%v], got [%v]", expected, actual)
}
os.Setenv("FSERVE_TEST_PGREP_ERROR", "0")
os.Setenv("FSERVE_TEST_PS_ERROR", "1")
err = killServers(ctx, "")
if err == nil {
t.Fatal("Expected error running ps, got no error.")
}
expected = "Error running ps: Expected error\n"
actual = fmt.Sprintf("%v", err)
if expected != actual {
t.Fatalf("[%v], got [%v]", expected, actual)
}
}
func TestStartServer(t *testing.T) {
testSDK := sdkcommon.SDKProperties{
DataPath: "/fake",
}
repoPath := "/fake/repo/path"
repoPort := "8083"
ExecCommand = helperCommandForFServe
syscallWait4 = mockWait4NoError
defer func() {
ExecCommand = exec.Command
syscallWait4 = defaultsyscallWait4
}()
if _, err := startServer(testSDK, repoPath, repoPort); err != nil {
t.Fatal(err)
}
syscallWait4 = mockWait4WithError
if _, err := startServer(testSDK, repoPath, repoPort); err != nil {
expected := "Server started then exited with code 1"
actual := fmt.Sprintf("%v", err)
if expected != actual {
t.Fatalf("[%v], got [%v]", expected, actual)
}
} else {
t.Fatal("Expected error starting server, got no error.")
}
}
func TestDownloadImageIfNeeded(t *testing.T) {
testSDK := sdkcommon.SDKProperties{
DataPath: t.TempDir(),
}
ctx := testingContext()
ExecCommand = helperCommandForFServe
sdkcommon.ExecCommand = helperCommandForFServe
sdkcommon.ExecLookPath = func(cmd string) (string, error) { return filepath.Join("mocked", cmd), nil }
defer func() {
ExecCommand = exec.Command
sdkcommon.ExecCommand = exec.Command
sdkcommon.ExecLookPath = exec.LookPath
}()
version := "any-version"
bucket := "test-bucket"
srcPath := "gs://test-bucket/path/on/GCS/theImage.tgz"
imageFilename := "theImage.tgz"
executable, _ := os.Executable()
fmt.Fprintf(os.Stderr, "Running test executable %v\n", executable)
fmt.Fprintf(os.Stderr, "testrootFlag value is %v\n", *testrootFlag)
testrootPath := filepath.Join(filepath.Dir(executable), *testrootFlag)
os.Setenv("FSERVE_TEST_TESTROOT", testrootPath)
if err := downloadImageIfNeeded(ctx, testSDK, version, bucket, srcPath, imageFilename); err != nil {
t.Fatal(err)
}
// Run the test again, and it should skip the download
os.Setenv("FSERVE_TEST_ASSERT_NO_DOWNLOAD", "1")
if err := downloadImageIfNeeded(ctx, testSDK, version, bucket, srcPath, imageFilename); err != nil {
t.Fatal(err)
}
}
func TestDownloadImageIfNeededCopiedFails(t *testing.T) {
testSDK := sdkcommon.SDKProperties{
DataPath: t.TempDir(),
}
ctx := testingContext()
ExecCommand = helperCommandForFServe
sdkcommon.ExecCommand = helperCommandForFServe
sdkcommon.ExecLookPath = func(cmd string) (string, error) { return filepath.Join("mocked", cmd), nil }
defer func() {
ExecCommand = exec.Command
sdkcommon.ExecCommand = exec.Command
sdkcommon.ExecLookPath = exec.LookPath
}()
version := "any-version"
bucket := "test-bucket"
srcPath := "gs://test-bucket/path/on/GCS/theImage.tgz"
imageFilename := "theImage.tgz"
// Run the test again, and it should skip the download
os.Setenv("FSERVE_TEST_ASSERT_NO_DOWNLOAD", "")
os.Setenv("FSERVE_TEST_COPY_FAILS", "1")
if err := downloadImageIfNeeded(ctx, testSDK, version, bucket, srcPath, imageFilename); err != nil {
destPath := filepath.Join(testSDK.DataPath, imageFilename)
expected := fmt.Sprintf("Could not copy image from %v to %v: BucketNotFoundException: 404 %v bucket does not exist.: exit status 2",
srcPath, destPath, srcPath)
actual := fmt.Sprintf("%v", err)
if expected != actual {
t.Fatalf("[%v], got [%v]", expected, actual)
}
} else {
t.Fatal("Expected error downloading, got no error.")
}
}
const resolvedAddr = "fe80::c0ff:eee:fe00:4444%en0"
func TestSetPackageSource(t *testing.T) {
testSDK := sdkcommon.SDKProperties{
DataPath: t.TempDir(),
}
homeDir := filepath.Join(testSDK.DataPath, "_TEMP_HOME")
if err := os.MkdirAll(homeDir, 0755); err != nil {
t.Fatal(err)
}
ctx := testingContext()
ExecCommand = helperCommandForFServe
sdkcommon.ExecCommand = helperCommandForFServe
sdkcommon.GetUserHomeDir = func() (string, error) { return homeDir, nil }
sdkcommon.GetUsername = func() (string, error) { return "testuser", nil }
sdkcommon.GetHostname = func() (string, error) { return "testhost", nil }
defer func() {
ExecCommand = exec.Command
sdkcommon.ExecCommand = exec.Command
sdkcommon.GetUserHomeDir = sdkcommon.DefaultGetUserHomeDir
sdkcommon.GetUsername = sdkcommon.DefaultGetUsername
sdkcommon.GetHostname = sdkcommon.DefaultGetHostname
}()
repoPort := "8083"
deviceName := ""
deviceIP := resolvedAddr
sshConfig := ""
privateKey := ""
name := "devhost"
if err := setPackageSource(ctx, testSDK, repoPort, name, deviceName, deviceIP, sshConfig, privateKey); err != nil {
t.Fatal(err)
}
deviceIP = "10.10.0.1"
if err := setPackageSource(ctx, testSDK, repoPort, name, deviceName, deviceIP, sshConfig, privateKey); err != nil {
t.Fatal(err)
}
deviceIP = ""
deviceName = "test-device"
if err := setPackageSource(ctx, testSDK, repoPort, name, deviceName, deviceIP, sshConfig, privateKey); err != nil {
t.Fatal(err)
}
deviceIP = ""
deviceName = "test-device"
sshConfig = "custom-sshconfig"
os.Setenv("FSERVE_TEST_USE_CUSTOM_SSH_CONFIG", "1")
if err := setPackageSource(ctx, testSDK, repoPort, name, deviceName, deviceIP, sshConfig, privateKey); err != nil {
t.Fatal(err)
}
deviceIP = ""
deviceName = "test-device"
sshConfig = ""
privateKey = "private-key"
os.Setenv("FSERVE_TEST_USE_CUSTOM_SSH_CONFIG", "")
os.Setenv("FSERVE_TEST_USE_PRIVATE_KEY", "1")
if err := setPackageSource(ctx, testSDK, repoPort, name, deviceName, deviceIP, sshConfig, privateKey); err != nil {
t.Fatal(err)
}
}
/*
This "test" is used to mock the command line tools invoked by fserve.
The method "helperCommandForFServe" replaces exec.Command and runs
this test inplace of the command.
This approach to mocking out executables is based on exec_test.go.
*/
func TestFakeFServe(t *testing.T) {
t.Helper()
if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" {
return
}
defer os.Exit(0)
args := os.Args
for len(args) > 0 {
if args[0] == "--" {
args = args[1:]
break
}
args = args[1:]
}
if len(args) == 0 {
fmt.Fprintf(os.Stderr, "No command\n")
os.Exit(2)
}
// Check the command line
cmd, args := args[0], args[1:]
switch filepath.Base(cmd) {
case "device-finder":
fakeDeviceFinder(args)
case "pgrep":
fakePgrep(args)
case "ps":
fakePS(args)
case "pm":
fakePM(args)
case "gsutil":
fakeGSUtil(args)
case "ssh":
fakeSSH(args)
case "ssh-keygen":
fakeSSHKeyGen(args)
default:
fmt.Fprintf(os.Stderr, "Unexpected command %v", cmd)
os.Exit(1)
}
}
func fakeDeviceFinder(args []string) {
expected := []string{}
expectedResolveArgs := []string{"resolve", "-device-limit", "1", "-ipv4=false", "test-device"}
if args[0] == "resolve" {
expected = expectedResolveArgs
fmt.Println(resolvedAddr)
}
ok := len(expected) == len(args)
for i := range args {
if strings.Contains(expected[i], "*") {
expectedPattern := regexp.MustCompile(expected[i])
ok = ok && expectedPattern.MatchString(args[i])
} else {
ok = ok && args[i] == expected[i]
}
}
if !ok {
fmt.Fprintf(os.Stderr, "unexpected ssh args %v exepected %v", args, expected)
os.Exit(1)
}
}
func fakeSSH(args []string) {
expected := []string{}
expectedHostConnection := []string{}
expectedSetSource := []string{}
privateKeyArgs := []string{"-i", "private-key"}
sshConfigMatch := "/.*/sshconfig"
if os.Getenv("FSERVE_TEST_USE_CUSTOM_SSH_CONFIG") != "" {
sshConfigMatch = "custom-sshconfig"
}
sshConfigArgs := []string{"-F", sshConfigMatch}
hostaddr := "fe80::c0ff:eeee:fefe:c000%eth1"
expectedURL := "http://[fe80::c0ff:eeee:fefe:c000%25eth1]:8083/config.json"
targetaddr := resolvedAddr
targetIndex := 2
expectedHostConnection = append(expectedHostConnection, sshConfigArgs...)
expectedSetSource = append(expectedSetSource, sshConfigArgs...)
if os.Getenv("FSERVE_TEST_USE_PRIVATE_KEY") != "" {
targetIndex = 4
expectedHostConnection = append(expectedHostConnection, privateKeyArgs...)
expectedSetSource = append(expectedSetSource, privateKeyArgs...)
}
if args[targetIndex] == resolvedAddr {
hostaddr = "fe80::c0ff:eeee:fefe:c000%eth1"
expectedURL = "http://[fe80::c0ff:eeee:fefe:c000%25eth1]:8083/config.json"
} else {
targetaddr = args[targetIndex]
hostaddr = "10.10.1.12"
expectedURL = "http://10.10.1.12:8083/config.json"
}
expectedHostConnection = append(expectedHostConnection, targetaddr, "echo", "$SSH_CONNECTION")
expectedSetSource = append(expectedSetSource, targetaddr, "amber_ctl", "add_src", "-n", "devhost", "-f", expectedURL)
if args[len(args)-1] == "$SSH_CONNECTION" {
expected = expectedHostConnection
fmt.Printf("%v 54545 fe80::c00f:f0f0:eeee:cccc 22\n", hostaddr)
} else if args[len(args)-1] == expectedURL {
expected = expectedSetSource
}
ok := len(args) == len(expected)
if ok {
for i := range args {
if strings.Contains(expected[i], "*") {
expectedPattern := regexp.MustCompile(expected[i])
ok = ok && expectedPattern.MatchString(args[i])
} else {
ok = ok && args[i] == expected[i]
}
}
}
if !ok {
fmt.Fprintf(os.Stderr, "unexpected ssh args %v expected %v", args, expected)
os.Exit(1)
}
}
func fakeSSHKeyGen(args []string) {
expected := []string{}
if len(args) == 0 {
fmt.Fprintf(os.Stderr, "Expected arguments to ssh-keygen\n")
os.Exit(1)
}
switch args[0] {
case "-P":
expected = []string{"-P", "", "-t", "ed25519", "-f", "/.*/_TEMP_HOME/.ssh/fuchsia_ed25519", "-C", "testuser@testhost generated by Fuchsia GN SDK"}
case "-y":
expected = []string{"-y", "-f", "/.*/_TEMP_HOME/.ssh/fuchsia_ed25519"}
fmt.Println("ssh-ed25519 AAAAC3NzaC1lTESTNTE5AAAAILxVYY7Q++kWUCmlfK1B6JQ9FPRaee05Te/PSHWVTeST testuser@test-host generated by Fuchsia GN SDK")
}
ok := len(args) == len(expected)
if ok {
for i := range args {
if strings.Contains(expected[i], "*") {
expectedPattern := regexp.MustCompile(expected[i])
ok = ok && expectedPattern.MatchString(args[i])
} else {
ok = ok && args[i] == expected[i]
}
}
}
if !ok {
fmt.Fprintf(os.Stderr, "unexpected ssh-keygen args %v. Expected %v", args, expected)
os.Exit(1)
}
}
func fakeGSUtil(args []string) {
expected := []string{}
expectedLS := []string{"ls", "gs://test-bucket/path/on/GCS/theImage.tgz"}
expectsedCP := []string{"cp", "gs://test-bucket/path/on/GCS/theImage.tgz", "/.*/theImage.tgz"}
if len(args) == 0 {
fmt.Fprintf(os.Stderr, "Expected arguments to gsutil\n")
os.Exit(1)
}
switch args[0] {
case "ls":
expected = expectedLS
case "cp":
if os.Getenv("FSERVE_TEST_ASSERT_NO_DOWNLOAD") != "" {
fmt.Fprintf(os.Stderr, "Unexpected call to gsutil cp: %v\n", args)
os.Exit(1)
}
if os.Getenv("FSERVE_TEST_COPY_FAILS") != "" {
fmt.Fprintf(os.Stderr, "BucketNotFoundException: 404 %v bucket does not exist.", args[1])
os.Exit(2)
}
expected = expectsedCP
// Copy the test data to the expected path.
testRoot := os.Getenv("FSERVE_TEST_TESTROOT")
if testRoot != "" {
testdata := filepath.Join(testRoot, "testdata", "testdata.tgz")
if err := copyFile(testdata, args[2]); err != nil {
fmt.Fprintf(os.Stderr, "Error linking testdata: %v\n", err)
os.Exit(1)
}
}
}
ok := len(args) == len(expected)
if ok {
for i := range args {
if strings.Contains(expected[i], "*") {
expectedPattern := regexp.MustCompile(expected[i])
ok = ok && expectedPattern.MatchString(args[i])
} else {
ok = ok && args[i] == expected[i]
}
}
}
if !ok {
fmt.Fprintf(os.Stderr, "unexpected gsutil args %v. Expected %v", args, expected)
os.Exit(1)
}
}
func fakePM(args []string) {
expected := []string{"serve", "-repo", "/fake/repo/path", "-l", ":8083"}
ok := len(args) == len(expected)
if ok {
for i := range args {
ok = ok && args[i] == expected[i]
}
}
if !ok {
fmt.Fprintf(os.Stderr, "unexpected pm args %v. Expected %v", args, expected)
os.Exit(1)
}
}
func fakePgrep(args []string) {
if os.Getenv("FSERVE_TEST_PGREP_ERROR") == "1" {
fmt.Fprintf(os.Stderr, "Expected error\n")
os.Exit(1)
}
if args[0] == "pm" {
if os.Getenv("FSERVE_TEST_NO_SERVERS") == "1" {
os.Exit(0)
} else {
// return 3 pm instances
fmt.Printf(`1000
2000
3000`)
os.Exit(0)
}
}
fmt.Fprintf(os.Stderr, "unexpected pgrep args %v", args)
os.Exit(1)
}
func fakePS(args []string) {
if os.Getenv("FSERVE_TEST_PS_ERROR") == "1" {
fmt.Fprintf(os.Stderr, "Expected error\n")
os.Exit(1)
}
fmt.Println(" PID TTY STAT TIME COMMAND")
for _, arg := range args {
switch arg {
case "1000":
// some internal process
fmt.Println("1000 ? I< 0:00 [tpm_dev_wq]")
case "2000":
// pm on port 8083
fmt.Println("2000 pts/0 Sl 0:00 /sdk/path/tools/x64/pm serve -repo /home/developer/.fuchsia/packages/amber-files -l :8083")
case "3000":
// pm on port 8084
fmt.Println("3000 pts/0 Sl 0:00 /sdk/path/tools/x64/pm serve -repo /home/developer/.fuchsia/packages/amber-files -l :8084 -q")
}
}
}
type testProcess struct {
Pid int
}
func (proc testProcess) Kill() error {
// This mock only kills pid == 2000
if proc.Pid != 2000 {
return fmt.Errorf("Unexpected pid %v in Kill", proc.Pid)
}
return nil
}
func mockedFindProcess(pid int) (osProcess, error) {
proc := testProcess{Pid: pid}
return &proc, nil
}
func mockWait4NoError(pid int, wstatus *syscall.WaitStatus, flags int, usage *syscall.Rusage) (int, error) {
return 0, nil
}
func mockWait4WithError(pid int, wstatus *syscall.WaitStatus, flags int, usage *syscall.Rusage) (int, error) {
// set wstatus to exited with code 1.
*wstatus = syscall.WaitStatus(0x100)
return 0, nil
}
func copyFile(src, dst string) error {
in, err := os.Open(src)
if err != nil {
return err
}
defer in.Close()
out, err := os.Create(dst)
if err != nil {
return err
}
defer out.Close()
_, err = io.Copy(out, in)
if err != nil {
return err
}
return out.Close()
}