blob: a12a7eb9486f96df4b0af7d515e39f41f4b96dff [file] [log] [blame]
package machine
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"net/url"
"os"
"path/filepath"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"go.skia.org/infra/go/executil"
"go.skia.org/infra/go/httputils"
"go.skia.org/infra/go/metrics2"
"go.skia.org/infra/go/testutils"
"go.skia.org/infra/go/testutils/noop"
"go.skia.org/infra/machine/go/machine"
"go.skia.org/infra/machine/go/machine/change/source/mocks"
sinkMocks "go.skia.org/infra/machine/go/machine/event/sink/mocks"
"go.skia.org/infra/machine/go/machineserver/rpc"
"go.skia.org/infra/machine/go/test_machine_monitor/adb"
"go.skia.org/infra/machine/go/test_machine_monitor/ios"
"go.skia.org/infra/machine/go/test_machine_monitor/ssh"
)
const (
// For example values, see adb_test.go
getPropPlaceholder = "Placeholder get prop response"
dumpSysBatteryPlaceholder = "Placeholder dumpsys battery response"
dumpSysThermalPlaceholder = "Placeholder dumpsys thermal response"
// This formatting matters because it is processed in adb.go
adbUptimePlaceholder = "123.4 567.8"
iOSVersionPlaceholder = "99.9.9"
iOSDeviceTypePlaceholder = "iPhone88,8"
testUserIP = "root@skia-foobar-01"
// This example was taken directly from a production ChromeOS device
sampleChromeOSlsbrelease = `CHROMEOS_DEVSERVER=
CHROMEOS_RELEASE_APPID={9A3BE5D2-C3DC-4AE6-9943-E2C113895DC5}
CHROMEOS_RELEASE_BOARD=octopus-signed-mp-v23keys
CHROMEOS_RELEASE_BRANCH_NUMBER=56
CHROMEOS_RELEASE_BUILDER_PATH=octopus-release/R89-13729.56.0
CHROMEOS_RELEASE_BUILD_NUMBER=13729
CHROMEOS_RELEASE_BUILD_TYPE=Official Build
CHROMEOS_RELEASE_CHROME_MILESTONE=89
CHROMEOS_RELEASE_DESCRIPTION=13729.56.0 (Official Build) stable-channel octopus
CHROMEOS_RELEASE_KEYSET=mp-v23
CHROMEOS_RELEASE_NAME=Chrome OS
CHROMEOS_RELEASE_PATCH_NUMBER=0
CHROMEOS_RELEASE_TRACK=stable-channel
CHROMEOS_RELEASE_UNIBUILD=1
CHROMEOS_RELEASE_VERSION=13729.56.0
DEVICETYPE=CHROMEBOOK
GOOGLE_RELEASE=13729.56.0`
// This example was taken directly from a production ChromeOS device
sampleChromeOSUptime = "1234.5 5678.9"
machineID = "skia-rpi2-rack4-shelf1-001"
adbGetStateSuccess = "device"
adbShellGetUptimeSuccess = "27 8218793.04"
adbShellGetPropSuccess = "[ro.product.manufacturer]: [asus]"
adbShellDumpSysBattery = "This is dumpsys output."
versionForTest = "some-version-string-for-testing-purposes"
maintenanceModeMessage = "This is a note about why the machine was put in maintenance mode."
machineServerHost = "https://machines.skia.org"
)
func TestTryInterrogatingAndroidDevice_DeviceAttached_Success(t *testing.T) {
ctx := executil.FakeTestsContext(
"Test_FakeExe_AdbGetState_Success",
"Test_FakeExe_ADBUptime_ReturnsPlaceholder",
"Test_FakeExe_AdbShellGetProp_ReturnsPlaceholder",
"Test_FakeExe_RawDumpSysBattery_ReturnsPlaceholder",
"Test_FakeExe_RawDumpSysThermal_ReturnsPlaceholder",
)
m := &Machine{adb: adb.New()}
actual, err := m.tryInterrogatingAndroidDevice(ctx)
require.NoError(t, err)
assert.Equal(t, machine.Android{
GetProp: getPropPlaceholder,
DumpsysBattery: dumpSysBatteryPlaceholder,
DumpsysThermalService: dumpSysThermalPlaceholder,
Uptime: 123 * time.Second,
}, actual)
}
func TestTryInterrogatingAndroidDevice_UptimeFails_DeviceConsideredNotAttached(t *testing.T) {
ctx := executil.FakeTestsContext(
"Test_FakeExe_ExitCodeOne",
)
m := &Machine{adb: adb.New()}
_, err := m.tryInterrogatingAndroidDevice(ctx)
require.Error(t, err)
}
func TestTryInterrogatingAndroidDevice_ThermalFails_PartialSuccess(t *testing.T) {
ctx := executil.FakeTestsContext(
"Test_FakeExe_AdbGetState_Success",
"Test_FakeExe_ADBUptime_ReturnsPlaceholder",
"Test_FakeExe_AdbShellGetProp_ReturnsPlaceholder",
"Test_FakeExe_RawDumpSysBattery_ReturnsPlaceholder",
"Test_FakeExe_ExitCodeOne",
)
m := &Machine{adb: adb.New()}
actual, err := m.tryInterrogatingAndroidDevice(ctx)
require.NoError(t, err)
assert.Equal(t, machine.Android{
GetProp: getPropPlaceholder,
DumpsysBattery: dumpSysBatteryPlaceholder,
Uptime: 123 * time.Second,
}, actual)
}
func TestTryInterrogatingChromeOS_DeviceReachable_Success(t *testing.T) {
ctx := executil.FakeTestsContext(
"Test_FakeExe_SSHUptime_ReturnsPlaceholder",
"Test_FakeExe_SSHLSBRelease_ReturnsPlaceholder",
)
sshFile := filepath.Join(t.TempDir(), "test.json")
m := &Machine{
ssh: ssh.ExeImpl{},
sshMachineLocation: sshFile,
description: machine.Description{SSHUserIP: testUserIP},
}
actual, err := m.tryInterrogatingChromeOSDevice(ctx)
require.NoError(t, err)
assert.Equal(t, machine.ChromeOS{
Channel: "stable-channel",
Milestone: "89",
ReleaseVersion: "13729.56.0",
Uptime: 1234500 * time.Millisecond,
}, actual)
require.FileExists(t, sshFile)
b, err := os.ReadFile(sshFile)
require.NoError(t, err)
const expected = `{
"Comment": "This file is written to by test_machine_monitor. Do not edit by hand.",
"user_ip": "root@skia-foobar-01"
}
`
assert.Equal(t, expected, string(b))
}
func TestTryInterrogatingChromeOS_CatLSBReleaseFails_DeviceConsideredUnattached(t *testing.T) {
ctx := executil.FakeTestsContext(
"Test_FakeExe_SSHUptime_ReturnsPlaceholder",
"Test_FakeExe_ExitCodeOne", // pretend LSBRelease failed
)
m := &Machine{ssh: ssh.ExeImpl{}, description: machine.Description{SSHUserIP: testUserIP}}
_, err := m.tryInterrogatingChromeOSDevice(ctx)
require.Error(t, err)
}
func TestTryInterrogatingChromeOS_NoSSHUserIP_ReturnFalse(t *testing.T) {
ctx := executil.FakeTestsContext() // Any exe call will panic
m := &Machine{ssh: ssh.ExeImpl{}}
_, err := m.tryInterrogatingChromeOSDevice(ctx)
require.Error(t, err)
}
func TestTryInterrogatingChromeOS_UptimeFails_ReturnFalse(t *testing.T) {
ctx := executil.FakeTestsContext(
"Test_FakeExe_ExitCodeOne", // pretend uptime fails
)
m := &Machine{ssh: ssh.ExeImpl{}, description: machine.Description{SSHUserIP: testUserIP}}
_, err := m.tryInterrogatingChromeOSDevice(ctx)
require.Error(t, err)
}
func TestTryInterrogatingChromeOS_NoChromeOSData_AssumesNotAttached(t *testing.T) {
ctx := executil.FakeTestsContext(
"Test_FakeExe_SSHLSBRelease_ReturnsNonChromeOS",
)
m := &Machine{ssh: ssh.ExeImpl{}, description: machine.Description{SSHUserIP: testUserIP}}
_, err := m.tryInterrogatingChromeOSDevice(ctx)
require.Error(t, err)
}
func TestInterrogate_NoDeviceAttached_Success(t *testing.T) {
ctx := executil.FakeTestsContext(
"Test_FakeExe_ExitCodeOne", // No Android device
"Test_FakeExe_ExitCodeOne", // No iOS device
)
m := &Machine{
adb: adb.New(),
ios: ios.New(),
MachineID: "some-machine",
Version: "some-version",
runningTask: true,
startSwarming: true,
startTime: time.Date(2021, time.September, 2, 2, 2, 2, 2, time.UTC),
interrogateTimer: noop.Float64SummaryMetric{},
}
actual, err := m.interrogate(ctx)
require.NoError(t, err)
assert.Equal(t, machine.Event{
EventType: machine.EventTypeRawState,
LaunchedSwarming: true,
RunningSwarmingTask: true,
Host: machine.Host{
Name: "some-machine",
Version: "some-version",
StartTime: time.Date(2021, time.September, 2, 2, 2, 2, 2, time.UTC),
},
}, actual)
}
func TestInterrogate_AndroidDeviceAttached_Success(t *testing.T) {
ctx := executil.FakeTestsContext(
"Test_FakeExe_AdbGetState_Success",
"Test_FakeExe_ADBUptime_ReturnsPlaceholder",
"Test_FakeExe_AdbShellGetProp_ReturnsPlaceholder",
"Test_FakeExe_RawDumpSysBattery_ReturnsPlaceholder",
"Test_FakeExe_RawDumpSysThermal_ReturnsPlaceholder",
)
m := &Machine{
adb: adb.New(),
MachineID: "some-machine",
Version: "some-version",
runningTask: true,
startSwarming: true,
startTime: time.Date(2021, time.September, 2, 2, 2, 2, 2, time.UTC),
interrogateTimer: noop.Float64SummaryMetric{},
description: machine.Description{
AttachedDevice: machine.AttachedDeviceAdb,
},
}
actual, err := m.interrogate(ctx)
require.NoError(t, err)
assert.Equal(t, machine.Event{
EventType: machine.EventTypeRawState,
LaunchedSwarming: true,
RunningSwarmingTask: true,
Host: machine.Host{
Name: "some-machine",
Version: "some-version",
StartTime: time.Date(2021, time.September, 2, 2, 2, 2, 2, time.UTC),
},
Android: machine.Android{
GetProp: getPropPlaceholder,
DumpsysBattery: dumpSysBatteryPlaceholder,
DumpsysThermalService: dumpSysThermalPlaceholder,
Uptime: 123 * time.Second,
},
}, actual)
}
func goodIOSInterrogationResult(timePlaceholder time.Time) (*Machine, machine.Event) {
m := &Machine{
adb: adb.New(),
ios: ios.New(),
MachineID: "some-machine",
Version: "some-version",
runningTask: true,
startSwarming: true,
startTime: timePlaceholder,
interrogateTimer: noop.Float64SummaryMetric{},
description: machine.Description{
AttachedDevice: machine.AttachedDeviceIOS,
},
}
expected := machine.Event{
EventType: machine.EventTypeRawState,
LaunchedSwarming: true,
RunningSwarmingTask: true,
Host: machine.Host{
Name: "some-machine",
Version: "some-version",
StartTime: timePlaceholder,
},
IOS: machine.IOS{
OSVersion: iOSVersionPlaceholder,
DeviceType: iOSDeviceTypePlaceholder,
Battery: 33,
},
}
return m, expected
}
// Just make sure it can get into tryInterrogatingIOSDevice(), and test success while we're at it.
// Tests that call tryInterrogatingIOSDevice() directly cover the other cases.
func TestInterrogate_IOSDeviceAttached_Success(t *testing.T) {
ctx := executil.FakeTestsContext(
"Test_FakeExe_IDeviceInfo_ReturnsDeviceType",
"Test_FakeExe_IDeviceInfo_ReturnsOSVersion",
"Test_FakeExe_IDeviceInfo_ReturnsGoodBatteryLevel",
)
timePlaceholder := time.Date(2021, time.September, 2, 2, 2, 2, 2, time.UTC)
m, expected := goodIOSInterrogationResult(timePlaceholder)
actual, err := m.interrogate(ctx)
require.NoError(t, err)
assert.Equal(t, expected, actual)
}
func TestInterrogate_IOSDeviceAttachedButBatteryCallFails_StillSuccess(t *testing.T) {
ctx := executil.FakeTestsContext(
"Test_FakeExe_IDeviceInfo_ReturnsDeviceType",
"Test_FakeExe_IDeviceInfo_ReturnsOSVersion",
"Test_FakeExe_ExitCodeOne", // battery level fails
)
timePlaceholder := time.Date(2021, time.September, 2, 2, 2, 2, 2, time.UTC)
m, expected := goodIOSInterrogationResult(timePlaceholder)
expected.IOS.Battery = machine.BadBatteryLevel // Battery value should reflect failure.
actual, err := m.interrogate(ctx)
require.NoError(t, err)
assert.Equal(t, expected, actual)
}
func TestTryInterrogatingIOSDevice_OSVersionFails_Fails(t *testing.T) {
ctx := executil.FakeTestsContext(
"Test_FakeExe_IDeviceInfo_ReturnsDeviceType",
"Test_FakeExe_ExitCodeOne", // OS version check goes kaboom.
"Test_FakeExe_IDeviceInfo_ReturnsGoodBatteryLevel",
)
m := &Machine{ios: ios.New()}
_, err := m.tryInterrogatingIOSDevice(ctx)
require.Error(t, err)
}
func TestTryInterrogatingIOSDevice_DeviceTypeFails_DeviceConsideredUnattached(t *testing.T) {
ctx := executil.FakeTestsContext(
"Test_FakeExe_ExitCodeOne", // Device-type check fails.
)
m := &Machine{ios: ios.New()}
_, err := m.tryInterrogatingIOSDevice(ctx)
require.Error(t, err)
}
func TestInterrogate_ChromeOSDeviceAttached_Success(t *testing.T) {
ctx := executil.FakeTestsContext(
"Test_FakeExe_SSHUptime_ReturnsPlaceholder",
"Test_FakeExe_SSHLSBRelease_ReturnsPlaceholder",
// We found a device, no need to check for adb
)
sshFile := filepath.Join(t.TempDir(), "test.json")
m := &Machine{
ssh: ssh.ExeImpl{},
sshMachineLocation: sshFile,
MachineID: "some-machine",
description: machine.Description{
SSHUserIP: testUserIP,
AttachedDevice: machine.AttachedDeviceSSH,
},
Version: "some-version",
runningTask: true,
startSwarming: true,
startTime: time.Date(2021, time.September, 2, 2, 2, 2, 2, time.UTC),
interrogateTimer: noop.Float64SummaryMetric{},
}
actual, err := m.interrogate(ctx)
require.NoError(t, err)
assert.Equal(t, machine.Event{
EventType: machine.EventTypeRawState,
LaunchedSwarming: true,
RunningSwarmingTask: true,
Host: machine.Host{
Name: "some-machine",
Version: "some-version",
StartTime: time.Date(2021, time.September, 2, 2, 2, 2, 2, time.UTC),
},
ChromeOS: machine.ChromeOS{
Channel: "stable-channel",
Milestone: "89",
ReleaseVersion: "13729.56.0",
Uptime: 1234500 * time.Millisecond,
},
}, actual)
require.FileExists(t, sshFile)
b, err := os.ReadFile(sshFile)
require.NoError(t, err)
const expected = `{
"Comment": "This file is written to by test_machine_monitor. Do not edit by hand.",
"user_ip": "root@skia-foobar-01"
}
`
assert.Equal(t, expected, string(b))
}
func TestRebootDevice_AndroidDeviceAttached_Success(t *testing.T) {
ctx := executil.FakeTestsContext(
"Test_FakeExe_AdbReboot_Success",
)
m := &Machine{
adb: adb.New(),
description: machine.Description{
Dimensions: machine.SwarmingDimensions{
machine.DimAndroidDevices: []string{"sprout"},
},
},
}
require.NoError(t, m.RebootDevice(ctx))
assert.Equal(t, 1, executil.FakeCommandsReturned(ctx))
}
func TestRebootDevice_AndroidDeviceAttached_ErrOnNonZeroExitCode(t *testing.T) {
ctx := executil.FakeTestsContext(
"Test_FakeExe_Reboot_NonZeroExitCode",
"Test_FakeExe_ReconnectOffline_Success",
"Test_FakeExe_Reboot_NonZeroExitCode",
)
m := &Machine{
adb: adb.New(),
description: machine.Description{
Dimensions: machine.SwarmingDimensions{
machine.DimAndroidDevices: []string{"sprout"},
},
},
}
require.Error(t, m.RebootDevice(ctx))
assert.Equal(t, 3, executil.FakeCommandsReturned(ctx))
}
func TestRebootDevice_NoErrorIfNoDevicesAttached(t *testing.T) {
ctx := executil.FakeTestsContext() // Any exe call will panic
m := &Machine{
description: machine.Description{},
}
require.NoError(t, m.RebootDevice(ctx))
}
func TestRebootDevice_IOSDeviceAttached_Success(t *testing.T) {
ctx := executil.FakeTestsContext("Test_FakeExe_IDeviceDiagnosticsReboot_Success")
m := &Machine{
ios: ios.New(),
description: machine.Description{
Dimensions: machine.SwarmingDimensions{
machine.DimOS: []string{"iOS", "iOS-1.2.3"},
},
},
}
require.NoError(t, m.RebootDevice(ctx))
assert.Equal(t, 1, executil.FakeCommandsReturned(ctx)) // Make sure it was really called.
}
func TestRebootDevice_ChromeOSDeviceAttached_Success(t *testing.T) {
ctx := executil.FakeTestsContext(
"Test_FakeExe_SSHReboot_Success",
)
m := &Machine{
ssh: ssh.ExeImpl{},
description: machine.Description{
SSHUserIP: testUserIP,
},
}
require.NoError(t, m.RebootDevice(ctx))
assert.Equal(t, 1, executil.FakeCommandsReturned(ctx))
}
func TestRebootDevice_ChromeOSDeviceAttached_ErrOnNonZeroExitCode(t *testing.T) {
ctx := executil.FakeTestsContext(
"Test_FakeExe_ExitCodeOne",
)
m := &Machine{
ssh: ssh.ExeImpl{},
description: machine.Description{
SSHUserIP: testUserIP,
},
}
require.Error(t, m.RebootDevice(ctx))
assert.Equal(t, 1, executil.FakeCommandsReturned(ctx))
}
func TestInterrogateAndSend_InterrogateSuccessful_EmitsEventViaSink(t *testing.T) {
ctx := context.Background()
start := time.Date(2020, time.May, 1, 0, 0, 0, 0, time.UTC)
expectedEvent := machine.Event{
EventType: "raw_state",
Android: machine.Android{
GetProp: adbShellGetPropSuccess,
DumpsysBattery: adbShellDumpSysBattery,
DumpsysThermalService: adbShellDumpSysBattery,
Uptime: 27 * time.Second,
},
Host: machine.Host{
Name: "my-test-bot-001",
StartTime: start,
},
}
eventSink := &sinkMocks.Sink{}
eventSink.On("Send", testutils.AnyContext, expectedEvent).Return(nil)
desc := machine.NewDescription(ctx)
desc.AttachedDevice = machine.AttachedDeviceAdb
// Create a Machine instance.
m := &Machine{
httpSink: eventSink,
startTime: start,
description: desc,
adb: adb.New(),
interrogateTimer: metrics2.GetFloat64SummaryMetric("bot_config_machine_interrogate_timer", map[string]string{"machine": machineID}),
MachineID: "my-test-bot-001",
}
ctx = executil.WithFakeTests(ctx,
"Test_FakeExe_AdbGetState_Success",
"Test_FakeExe_AdbShellGetUptime_Success",
"Test_FakeExe_AdbShellGetProp_Success",
"Test_FakeExe_RawDumpSys_Success",
"Test_FakeExe_RawDumpSys_Success",
)
err := m.interrogateAndSend(ctx)
require.NoError(t, err)
eventSink.AssertExpectations(t)
}
func TestInterrogateAndSend_AdbFailsToTalkToDevice_EmptyEventsSentToServer(t *testing.T) {
ctx := context.Background()
start := time.Date(2020, time.May, 1, 0, 0, 0, 0, time.UTC)
expectedEvent := machine.Event{
EventType: "raw_state",
Android: machine.Android{
GetProp: "",
DumpsysBattery: "",
DumpsysThermalService: "",
},
Host: machine.Host{
Name: "my-test-bot-001",
StartTime: start,
},
}
eventSink := &sinkMocks.Sink{}
eventSink.On("Send", testutils.AnyContext, expectedEvent).Return(nil)
desc := machine.NewDescription(ctx)
desc.AttachedDevice = machine.AttachedDeviceAdb
// Create a Machine instance.
m := &Machine{
httpSink: eventSink,
startTime: start,
description: desc,
adb: adb.New(),
interrogateTimer: metrics2.GetFloat64SummaryMetric("bot_config_machine_interrogate_timer", map[string]string{"machine": machineID}),
interrogateAndSendFailures: metrics2.GetCounter("test_machine_monitor_interrogate_and_send_errors", map[string]string{"machine": "my-test-bot-001"}),
MachineID: "my-test-bot-001",
}
ctx = executil.WithFakeTests(ctx,
"Test_FakeExe_AdbFail",
"Test_FakeExe_AdbFail",
)
err := m.interrogateAndSend(ctx)
require.NoError(t, err)
eventSink.AssertExpectations(t)
}
func TestStartInterrogation_TriggerInterrogationChannel_InterrogationIsDone(t *testing.T) {
ctx := context.Background()
// Successful calls for a single loop of interrogating an ADB device.
ctx = executil.WithFakeTests(ctx,
"Test_FakeExe_AdbGetState_Success",
"Test_FakeExe_AdbShellGetUptime_Success",
"Test_FakeExe_AdbShellGetProp_Success",
"Test_FakeExe_RawDumpSys_Success",
"Test_FakeExe_RawDumpSys_Success",
)
cancelCtx, cancel := context.WithCancel(ctx)
// Other tests confirm the value being sent is valid, in this case we just
// want to cancel the context so the startInterrogateLoop exits.
httpSink := &sinkMocks.Sink{}
httpSink.On("Send", testutils.AnyContext, mock.Anything).Return(nil).Run(func(args mock.Arguments) {
cancel()
}).Return(nil)
desc := machine.NewDescription(ctx)
desc.AttachedDevice = machine.AttachedDeviceAdb
// Create a Machine instance.
triggerInterrogationCh := make(chan bool, 1)
m := &Machine{
httpSink: httpSink,
description: desc,
adb: adb.New(),
interrogateTimer: metrics2.GetFloat64SummaryMetric("bot_config_machine_interrogate_timer", map[string]string{"machine": machineID}),
MachineID: "my-test-bot-001",
triggerInterrogationCh: triggerInterrogationCh,
}
// trigger an interrogation right away.
triggerInterrogationCh <- true
m.startInterrogateLoop(cancelCtx)
// The test will fail by timeout if startInterrogateLoop fails to exit on
// context cancellation.
}
func Test_FakeExe_AdbShellGetUptime_Success(t *testing.T) {
if !executil.IsCallingFakeCommand() {
return
}
// Check the input arguments to make sure they were as expected.
args := executil.OriginalArgs()
require.Equal(t, []string{"adb", "shell", "cat", "/proc/uptime"}, args)
fmt.Print(adbShellGetUptimeSuccess)
os.Exit(0)
}
func Test_FakeExe_AdbState_Success(t *testing.T) {
if !executil.IsCallingFakeCommand() {
return
}
// Check the input arguments to make sure they were as expected.
args := executil.OriginalArgs()
require.Equal(t, []string{"adb", "get-state"}, args)
fmt.Print(adbGetStateSuccess)
os.Exit(0)
}
func Test_FakeExe_AdbShellGetProp_Success(t *testing.T) {
if !executil.IsCallingFakeCommand() {
return
}
// Check the input arguments to make sure they were as expected.
args := executil.OriginalArgs()
require.Equal(t, []string{"adb", "shell", "getprop"}, args)
fmt.Print(adbShellGetPropSuccess)
os.Exit(0)
}
func Test_FakeExe_RawDumpSys_Success(t *testing.T) {
if !executil.IsCallingFakeCommand() {
return
}
fmt.Print(adbShellDumpSysBattery)
os.Exit(0)
}
func Test_FakeExe_ADBUptime_ReturnsPlaceholder(t *testing.T) {
if !executil.IsCallingFakeCommand() {
return
}
// Check the input arguments to make sure they were as expected.
args := executil.OriginalArgs()
require.Equal(t, []string{"adb", "shell", "cat", "/proc/uptime"}, args)
fmt.Print(adbUptimePlaceholder)
os.Exit(0)
}
func Test_FakeExe_AdbShellGetProp_ReturnsPlaceholder(t *testing.T) {
if !executil.IsCallingFakeCommand() {
return
}
// Check the input arguments to make sure they were as expected.
args := executil.OriginalArgs()
require.Equal(t, []string{"adb", "shell", "getprop"}, args)
fmt.Print(getPropPlaceholder)
os.Exit(0)
}
func Test_FakeExe_RawDumpSysBattery_ReturnsPlaceholder(t *testing.T) {
if !executil.IsCallingFakeCommand() {
return
}
// Check the input arguments to make sure they were as expected.
args := executil.OriginalArgs()
require.Equal(t, []string{"adb", "shell", "dumpsys", "battery"}, args)
fmt.Print(dumpSysBatteryPlaceholder)
os.Exit(0)
}
func Test_FakeExe_RawDumpSysThermal_ReturnsPlaceholder(t *testing.T) {
if !executil.IsCallingFakeCommand() {
return
}
// Check the input arguments to make sure they were as expected.
args := executil.OriginalArgs()
require.Equal(t, []string{"adb", "shell", "dumpsys", "thermalservice"}, args)
fmt.Print(dumpSysThermalPlaceholder)
os.Exit(0)
}
func Test_FakeExe_IDeviceInfo_ReturnsDeviceType(t *testing.T) {
if !executil.IsCallingFakeCommand() {
return
}
require.Equal(t, []string{"ideviceinfo", "-k", "ProductType"}, executil.OriginalArgs())
fmt.Fprintln(os.Stderr, iOSDeviceTypePlaceholder)
os.Exit(0)
}
func Test_FakeExe_IDeviceInfo_ReturnsOSVersion(t *testing.T) {
if !executil.IsCallingFakeCommand() {
return
}
require.Equal(t, []string{"ideviceinfo", "-k", "ProductVersion"}, executil.OriginalArgs())
fmt.Fprintln(os.Stderr, iOSVersionPlaceholder)
os.Exit(0)
}
func Test_FakeExe_IDeviceInfo_ReturnsGoodBatteryLevel(t *testing.T) {
if !executil.IsCallingFakeCommand() {
return
}
require.Equal(t, []string{"ideviceinfo", "--domain", "com.apple.mobile.battery", "-k", "BatteryCurrentCapacity"}, executil.OriginalArgs())
fmt.Fprintln(os.Stderr, "33")
os.Exit(0)
}
func Test_FakeExe_IDeviceDiagnosticsReboot_Success(t *testing.T) {
if !executil.IsCallingFakeCommand() {
return
}
require.Equal(t, []string{"idevicediagnostics", "restart"}, executil.OriginalArgs())
os.Exit(0)
}
func Test_FakeExe_SSHLSBRelease_ReturnsPlaceholder(t *testing.T) {
if !executil.IsCallingFakeCommand() {
return
}
// Check the input arguments to make sure they were as expected.
args := executil.OriginalArgs()
require.Contains(t, args, "ssh")
require.Contains(t, args, testUserIP)
require.Contains(t, args, "/etc/lsb-release")
fmt.Print(sampleChromeOSlsbrelease)
os.Exit(0)
}
func Test_FakeExe_SSHLSBRelease_ReturnsNonChromeOS(t *testing.T) {
if !executil.IsCallingFakeCommand() {
return
}
// Check the input arguments to make sure they were as expected.
args := executil.OriginalArgs()
require.Contains(t, args, "ssh")
require.Contains(t, args, testUserIP)
require.Contains(t, args, "/etc/lsb-release")
fmt.Print("FOO=bar")
os.Exit(0)
}
func Test_FakeExe_SSHUptime_ReturnsPlaceholder(t *testing.T) {
if !executil.IsCallingFakeCommand() {
return
}
// Check the input arguments to make sure they were as expected.
args := executil.OriginalArgs()
require.Contains(t, args, "ssh")
require.Contains(t, args, testUserIP)
require.Contains(t, args, "/proc/uptime")
fmt.Print(sampleChromeOSUptime)
os.Exit(0)
}
func Test_FakeExe_ExitCodeOne(t *testing.T) {
if !executil.IsCallingFakeCommand() {
return
}
os.Exit(1)
}
func Test_FakeExe_AdbGetState_Success(t *testing.T) {
if !executil.IsCallingFakeCommand() {
return
}
// Check the input arguments to make sure they were as expected.
args := executil.OriginalArgs()
require.Equal(t, []string{"adb", "get-state"}, args)
_, _ = fmt.Fprintf(os.Stderr, "device")
// Force exit so we don't get PASS in the output.
os.Exit(0)
}
func Test_FakeExe_AdbReboot_Success(t *testing.T) {
if !executil.IsCallingFakeCommand() {
return
}
// Check the input arguments to make sure they were as expected.
args := executil.OriginalArgs()
require.Equal(t, []string{"adb", "reboot"}, args)
// Force exit so we don't get PASS in the output.
os.Exit(0)
}
func Test_FakeExe_Reboot_NonZeroExitCode(t *testing.T) {
if !executil.IsCallingFakeCommand() {
return
}
_, _ = fmt.Fprintf(os.Stderr, "error: no devices/emulators found")
os.Exit(127)
}
func Test_FakeExe_ReconnectOffline_Success(t *testing.T) {
if !executil.IsCallingFakeCommand() {
return
}
// Check the input arguments to make sure they were as expected.
args := executil.OriginalArgs()
require.Equal(t, []string{"adb", "reconnect", "offline"}, args)
// Force exit so we don't get PASS in the output.
os.Exit(0)
}
func Test_FakeExe_SSHReboot_Success(t *testing.T) {
if !executil.IsCallingFakeCommand() {
return
}
// Check the input arguments to make sure they were as expected.
args := executil.OriginalArgs()
require.Contains(t, args, "ssh")
require.Contains(t, args, testUserIP)
require.Contains(t, args, "reboot")
// Force exit so we don't get PASS in the output.
os.Exit(0)
}
// setupLocalServerWithCallback sets up a local HTTP server where the provided
// 'cb' function will be used to serve all requests. A *url.URL is returned with
// the scheme and host configured to point at the local HTTP server.
func setupLocalServerWithCallback(t *testing.T, cb http.HandlerFunc) (*url.URL, *bool, *http.Client) {
t.Helper()
called := false
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
cb(w, r)
called = true
}))
t.Cleanup(func() {
ts.Close()
})
u, err := url.Parse(ts.URL)
u.Path = urlExpansionRegex.ReplaceAllLiteralString(rpc.MachineDescriptionURL, machineID)
require.NoError(t, err)
httpClient := httputils.DefaultClientConfig().With2xxOnly().WithoutRetries().Client()
return u, &called, httpClient
}
func TestRetrieveDescription_EndpointReturnsError_DescriptionIsNotUpdated(t *testing.T) {
u, called, client := setupLocalServerWithCallback(t, func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "error", http.StatusInternalServerError)
})
m := &Machine{
client: client,
machineDescriptionURL: u.String(),
}
err := m.retrieveDescription(context.Background())
require.Error(t, err)
require.True(t, *called)
require.Equal(t, machine.Description{}, m.description)
}
func TestRetrieveDescription_EndpointReturnsInvalidJSON_DescriptionIsNotUpdated(t *testing.T) {
u, called, client := setupLocalServerWithCallback(t, func(w http.ResponseWriter, r *http.Request) {
_, err := fmt.Fprint(w, "} not valid json {")
require.NoError(t, err)
})
m := &Machine{
client: client,
machineDescriptionURL: u.String(),
descriptionWatchArrivalCounter: metrics2.GetCounter("bot_config_machine_description_watch_arrival", map[string]string{"machine": machineID}),
}
m.descriptionWatchArrivalCounter.Reset()
err := m.retrieveDescription(context.Background())
require.Error(t, err)
require.True(t, *called)
require.Equal(t, machine.Description{}, m.description)
require.Equal(t, int64(0), m.descriptionWatchArrivalCounter.Get())
}
func TestRetrieveDescription_EndpointReturnsNewDescription_DescriptionIsUpdated(t *testing.T) {
desc := machine.Description{}
var capturedRequest *http.Request
u, called, client := setupLocalServerWithCallback(t, func(w http.ResponseWriter, r *http.Request) {
capturedRequest = r
err := json.NewEncoder(w).Encode(desc)
require.NoError(t, err)
})
m := &Machine{
client: client,
machineDescriptionURL: u.String(),
descriptionWatchArrivalCounter: metrics2.GetCounter("bot_config_machine_description_watch_arrival", map[string]string{"machine": machineID}),
}
m.descriptionWatchArrivalCounter.Reset()
err := m.retrieveDescription(context.Background())
require.NoError(t, err)
require.Equal(t, u.Path, capturedRequest.URL.Path)
require.True(t, *called)
require.Equal(t, desc, m.description)
require.Equal(t, int64(1), m.descriptionWatchArrivalCounter.Get())
}
func TestStartDescriptionWatch_ChannelIsClosed_FunctionExits(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
// Cancel the context so startDescriptionWatch will return.
cancel()
ch := make(chan interface{})
var readOnlyCh <-chan interface{} = ch
changeSource := &mocks.Source{}
changeSource.On("Start", testutils.AnyContext).Return(readOnlyCh)
m := &Machine{
sseChangeSource: changeSource,
}
m.startDescriptionWatch(ctx)
close(ch)
// Test will never exit on failure.
}
func getMachineWithHomeDir(t *testing.T) *Machine {
return &Machine{
MachineID: machineID,
homeDir: t.TempDir(),
description: machine.Description{
AttachedDevice: machine.AttachedDeviceNone,
},
interrogateTimer: metrics2.GetFloat64SummaryMetric("just_a_test"),
}
}
func writeQuarantineFile(m *Machine, t *testing.T) string {
// Write quarantine file.
quarantineFile := filepath.Join(m.homeDir, fmt.Sprintf("%s.force_quarantine", machineID))
err := ioutil.WriteFile(quarantineFile, []byte("test"), 0666)
require.NoError(t, err)
// Test the test, confirm the file exists.
_, err = os.Stat(quarantineFile)
require.NoError(t, err)
return quarantineFile
}
func TestMachineCheckForForcedQuarantine_FileExists_FileIsDeletedAndReturnsTrue(t *testing.T) {
m := getMachineWithHomeDir(t)
filename := writeQuarantineFile(m, t)
require.True(t, m.checkForForcedQuarantine())
// Confirm the file was removed.
_, err := os.Stat(filepath.Join(m.homeDir, filename))
require.True(t, os.IsNotExist(err))
}
func TestMachineCheckForForcedQuarantine_FileDoesNotExists_ReturnsFalse(t *testing.T) {
m := getMachineWithHomeDir(t)
require.False(t, m.checkForForcedQuarantine())
}
func TestInterrogate_ForceQuarantineFileDoesNotExists_ReturnsEventWithForceQuarantineTrue(t *testing.T) {
m := getMachineWithHomeDir(t)
event, err := m.interrogate(context.Background())
require.NoError(t, err)
require.False(t, event.ForcedQuarantine)
}
func TestInterrogate_ForceQuarantineFileExists_ReturnsEventWithForceQuarantineTrue(t *testing.T) {
m := getMachineWithHomeDir(t)
_ = writeQuarantineFile(m, t)
event, err := m.interrogate(context.Background())
require.NoError(t, err)
require.True(t, event.ForcedQuarantine)
}
func TestIsAvailable_NilMachine_ReturnsFalse(t *testing.T) {
var m *Machine
require.False(t, m.IsAvailable())
}
func TestIsAvailable_AvailableMachine_ReturnsTrue(t *testing.T) {
m := &Machine{
description: machine.Description{
MaintenanceMode: "",
IsQuarantined: false,
Recovering: "",
},
}
require.True(t, m.IsAvailable())
}