blob: 2b4ac5751250fdee31a75385d0b3c4292ea7cac0 [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"
"errors"
"flag"
"fmt"
"io"
"log"
"os"
"os/exec"
"path/filepath"
"regexp"
"runtime"
"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.")
const hostaddr = "fe80::c0ff:eeee:fefe:c000%eth1"
type testSDKProperties struct {
dataPath string
expectCustomSSHConfig bool
expectPrivateKey bool
expectedSSHArgs [][]string
}
func (testSDK testSDKProperties) GetToolsDir() (string, error) {
return "fake-tools", nil
}
func (testSDK testSDKProperties) GetSDKDataPath() string {
return testSDK.dataPath
}
func (testSDK testSDKProperties) GetAvailableImages(version string, bucket string) ([]sdkcommon.GCSImage, error) {
return []sdkcommon.GCSImage{}, nil
}
func (testSDK testSDKProperties) GetAddressByName(deviceName string) (string, error) {
return "::1", nil
}
func (testSDK testSDKProperties) GetDefaultPackageRepoDir() (string, error) {
return filepath.Join(testSDK.dataPath, "default-target-name", "packages", "amber-files"), nil
}
func (testSDK testSDKProperties) RunSSHCommand(targetAddress string, sshConfig string, privateKey string, verbose bool, sshArgs []string) (string, error) {
if testSDK.expectCustomSSHConfig && sshConfig == "" {
return "", errors.New("Expected custom ssh config file")
}
if testSDK.expectPrivateKey && privateKey == "" {
return "", errors.New("Expected private key file")
}
expectedArgs := []string{}
for _, args := range testSDK.expectedSSHArgs {
if sshArgs[0] == args[0] {
expectedArgs = args
break
}
}
ok := len(expectedArgs) == len(sshArgs)
if ok {
for i, expected := range expectedArgs {
if !ok {
return "", fmt.Errorf("unexpected ssh args[%v] %v expected[%v] %v",
len(sshArgs), sshArgs, len(expectedArgs), expectedArgs)
}
if strings.Contains(expected, "*") {
expectedPattern := regexp.MustCompile(expected)
ok = expectedPattern.MatchString(sshArgs[i])
} else {
ok = expected == sshArgs[i]
}
}
}
if sshArgs[0] == "echo" {
return fmt.Sprintf("%v 54545 fe80::c00f:f0f0:eeee:cccc 22\n", hostaddr), nil
}
return "", nil
}
// 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 := testSDKProperties{
dataPath: "/fake",
}
repoPath := "/fake/repo/path"
repoPort := "8083"
ExecCommand = helperCommandForFServe
defer func() {
ExecCommand = exec.Command
syscallWait4 = defaultsyscallWait4
}()
tests := []struct {
syscallWait4 func(pid int, wstatus *syscall.WaitStatus, flags int, usage *syscall.Rusage) (int, error)
expectedError string
logLevel logger.LogLevel
expectedArgs []string
}{
{syscallWait4: mockWait4NoError,
expectedError: "",
logLevel: logger.WarningLevel,
expectedArgs: []string{"serve", "-q", "-repo", "/fake/repo/path", "-l", ":8083"},
},
{syscallWait4: mockWait4NoError,
expectedError: "",
logLevel: logger.DebugLevel,
expectedArgs: []string{"serve", "-repo", "/fake/repo/path", "-l", ":8083"},
},
{syscallWait4: mockWait4WithError,
expectedError: "Server started then exited with code 1",
logLevel: logger.WarningLevel,
},
}
for i, test := range tests {
t.Run(fmt.Sprintf("TestStartServer case %d", i), func(t *testing.T) {
syscallWait4 = test.syscallWait4
level = test.logLevel
os.Setenv("TEST_LOGLEVEL", level.String())
cmd, err := startServer(testSDK, repoPath, repoPort)
if err != nil {
actual := fmt.Sprintf("%v", err)
if test.expectedError != actual {
t.Errorf("Actual error [%v] did not match expected [%v]", actual, test.expectedError)
}
} else if test.expectedError != "" {
t.Errorf("Expected error %v, but got no error", test.expectedError)
} else {
actual := cmd.Args[4:]
ok := len(actual) == len(test.expectedArgs)
if ok {
for i, arg := range test.expectedArgs {
if arg != actual[i] {
ok = false
break
}
}
}
if !ok {
t.Errorf("pm args %v do not match expected %v", actual, test.expectedArgs)
}
}
})
syscallWait4 = defaultsyscallWait4
}
}
func TestDownloadImageIfNeeded(t *testing.T) {
testSDK := testSDKProperties{
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"
repoPath, err := testSDK.GetDefaultPackageRepoDir()
if err != nil {
t.Fatal(err)
}
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, repoPath); 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, repoPath); err != nil {
t.Fatal(err)
}
}
func TestDownloadImageIfNeededCopiedFails(t *testing.T) {
testSDK := testSDKProperties{
dataPath: "/fake",
}
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"
repoPath, err := testSDK.GetDefaultPackageRepoDir()
if err != nil {
t.Fatal(err)
}
// 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, repoPath); err != nil {
destPath := filepath.Join(testSDK.GetSDKDataPath(), 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 := testSDKProperties{
dataPath: t.TempDir(),
}
homeDir := filepath.Join(testSDK.GetSDKDataPath(), "_TEMP_HOME")
if err := os.MkdirAll(homeDir, 0o700); 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
}()
tests := []struct {
repoPort string
targetAddress string
sshConfig string
name string
privateKey string
expectedSSHArgs [][]string
}{
{
repoPort: "8083",
targetAddress: resolvedAddr,
sshConfig: "",
privateKey: "",
name: "devhost",
expectedSSHArgs: [][]string{
{"echo", "$SSH_CONNECTION"},
{"amber_ctl", "add_src", "-n", "devhost", "-f", "http://[fe80::c0ff:eeee:fefe:c000%25eth1]:8083/config.json"},
},
},
{
repoPort: "8083",
targetAddress: resolvedAddr,
sshConfig: "custom-sshconfig",
privateKey: "",
name: "devhost",
expectedSSHArgs: [][]string{
{"echo", "$SSH_CONNECTION"},
{"amber_ctl", "add_src", "-n", "devhost", "-f", "http://[fe80::c0ff:eeee:fefe:c000%25eth1]:8083/config.json"},
},
},
{
repoPort: "8083",
targetAddress: resolvedAddr,
sshConfig: "",
privateKey: "private-key",
name: "devhost",
expectedSSHArgs: [][]string{
{"echo", "$SSH_CONNECTION"},
{"amber_ctl", "add_src", "-n", "devhost", "-f", "http://[fe80::c0ff:eeee:fefe:c000%25eth1]:8083/config.json"},
},
},
}
for _, test := range tests {
testSDK := testSDKProperties{expectedSSHArgs: test.expectedSSHArgs,
expectCustomSSHConfig: test.sshConfig != "",
expectPrivateKey: test.privateKey != ""}
if err := setPackageSource(ctx, testSDK, test.repoPort, test.name, test.targetAddress, test.sshConfig, test.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 "pgrep":
fakePgrep(args)
case "ps":
fakePS(args)
case "pm":
fakePM(args)
case "gsutil":
fakeGSUtil(args)
default:
fmt.Fprintf(os.Stderr, "Unexpected command %v", cmd)
os.Exit(1)
}
}
func fakeGSUtil(args []string) {
expected := []string{}
expectedLS := []string{"ls", "gs://test-bucket/path/on/GCS/theImage.tgz"}
expectedCP := []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 = expectedCP
// Copy the test data to the expected path.
testRoot := os.Getenv("FSERVE_TEST_TESTROOT")
testdata := filepath.Join(testRoot, "testdata", "testdata.tgz")
if !sdkcommon.FileExists(testdata) {
testdata = filepath.Join("..", "testdata", "testdata.tgz")
}
if err := os.MkdirAll(filepath.Dir(args[2]), 0755); err != nil {
fmt.Fprintf(os.Stderr, "Error getting mkdir temp dir: %v\n", err)
os.Exit(1)
}
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"}
logLevel := os.Getenv("TEST_LOGLEVEL")
// only debug and trace have non-quiet mode.
if logLevel != "debug" && logLevel != "trace" {
expected = append(expected, "-q")
}
expected = append(expected, "-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\n", 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" {
// mac exits with 1
if runtime.GOOS == "darwin" {
os.Exit(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()
}