From 631192007df382ffbc39444cb1281120ee4f2f54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliv=C3=A9r=20Falvai?= Date: Fri, 29 Nov 2024 09:19:43 +0100 Subject: [PATCH] Split ADB-related code into own package (#55) * Split ADB-related code into own package * Add tests --- adb/adb.go | 102 +++++++++++++++++++++++++++++++++++ adb/adb_test.go | 61 +++++++++++++++++++++ emuinstaller/install_test.go | 69 ++---------------------- main.go | 89 ++++-------------------------- test/fakes.go | 68 +++++++++++++++++++++++ 5 files changed, 245 insertions(+), 144 deletions(-) create mode 100644 adb/adb.go create mode 100644 adb/adb_test.go create mode 100644 test/fakes.go diff --git a/adb/adb.go b/adb/adb.go new file mode 100644 index 0000000..8199416 --- /dev/null +++ b/adb/adb.go @@ -0,0 +1,102 @@ +package adb + +import ( + "bufio" + "fmt" + "path/filepath" + "regexp" + "strings" + + "github.com/bitrise-io/go-utils/v2/command" + "github.com/bitrise-io/go-utils/v2/log" +) + +type ADB struct { + androidHome string + cmdFactory command.Factory + logger log.Logger +} + +func New(androidHome string, cmdFactory command.Factory, logger log.Logger) ADB { + return ADB{ + androidHome: androidHome, + cmdFactory: cmdFactory, + logger: logger, + } +} + +const DeviceStateConnected = "device" + +// Key: device serial number +// Value: device state +type Devices map[string]string + +// Devices returns a map of connected Android devices and their states. +func (a *ADB) Devices() (Devices, error) { + cmd := a.cmdFactory.Create( + filepath.Join(a.androidHome, "platform-tools", "adb"), + []string{"devices"}, + nil, + ) + out, err := cmd.RunAndReturnTrimmedCombinedOutput() + if err != nil { + a.logger.Printf(out) + return map[string]string{}, fmt.Errorf("adb devices: %s", err) + } + + a.logger.Debugf("$ %s", cmd.PrintableCommandArgs()) + a.logger.Debugf("%s", out) + + // List of devices attached + // emulator-5554 device + deviceListItemPattern := `^(?Pemulator-\d*)[\s+](?P.*)` + deviceListItemRegexp := regexp.MustCompile(deviceListItemPattern) + + deviceStateMap := map[string]string{} + + scanner := bufio.NewScanner(strings.NewReader(out)) + for scanner.Scan() { + line := scanner.Text() + matches := deviceListItemRegexp.FindStringSubmatch(line) + if len(matches) == 3 { + serial := matches[1] + state := matches[2] + + deviceStateMap[serial] = state + } + + } + if scanner.Err() != nil { + return map[string]string{}, fmt.Errorf("scan adb devices output: %s", err) + } + + return deviceStateMap, nil +} + +// FindNewDevice returns the serial number of a newly connected device compared +// to the previous state of running devices. +// If no new device is found, an empty string is returned. +func (a *ADB) FindNewDevice(previousDeviceState Devices) (string, error) { + devicesNow, err := a.Devices() + if err != nil { + return "", err + } + + newDeviceSerial := "" + for serial := range devicesNow { + _, found := previousDeviceState[serial] + if !found { + newDeviceSerial = serial + break + } + } + + if len(newDeviceSerial) > 0 { + state := devicesNow[newDeviceSerial] + if state == DeviceStateConnected { + return newDeviceSerial, nil + } + } + + return "", nil +} diff --git a/adb/adb_test.go b/adb/adb_test.go new file mode 100644 index 0000000..e632998 --- /dev/null +++ b/adb/adb_test.go @@ -0,0 +1,61 @@ +package adb + +import ( + "testing" + + "github.com/bitrise-io/go-utils/v2/log" + "github.com/bitrise-steplib/steps-avd-manager/test" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestFindNewDevice(t *testing.T) { + androidHome := "/fake/android/home" + logger := log.NewLogger() + + tests := []struct { + name string + previousDevices Devices + adbOutput string + expectedSerial string + }{ + { + name: "no new device", + previousDevices: Devices{ + "emulator-5554": "device", + }, + adbOutput: "List of devices attached\nemulator-5554\tdevice\n", + expectedSerial: "", + }, + { + name: "new device connected", + previousDevices: Devices{ + "emulator-5554": "device", + }, + adbOutput: "List of devices attached\nemulator-5554\tdevice\nemulator-5556\tdevice\n", + expectedSerial: "emulator-5556", + }, + { + name: "new device not connected", + previousDevices: Devices{ + "emulator-5554": "device", + }, + adbOutput: "List of devices attached\nemulator-5554\tdevice\nemulator-5556\toffline\n", + expectedSerial: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmdFactory := test.FakeCommandFactory{ + Stdout: tt.adbOutput, + ExitCode: 0, + } + adb := New(androidHome, cmdFactory, logger) + + newDevice, err := adb.FindNewDevice(tt.previousDevices) + require.NoError(t, err) + assert.Equal(t, tt.expectedSerial, newDevice) + }) + } +} diff --git a/emuinstaller/install_test.go b/emuinstaller/install_test.go index 1c48fd7..ab0ae4a 100644 --- a/emuinstaller/install_test.go +++ b/emuinstaller/install_test.go @@ -1,12 +1,11 @@ package emuinstaller import ( - "fmt" "os" "path/filepath" - "strings" "testing" + "github.com/bitrise-steplib/steps-avd-manager/test" "github.com/bitrise-io/go-utils/v2/command" "github.com/bitrise-io/go-utils/v2/env" "github.com/stretchr/testify/require" @@ -75,9 +74,9 @@ This program is a derivative of the QEMU CPU emulator (www.qemu.org). for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - cmdFactory := fakeCommandFactory{ - stdout: tt.versionOutput, - exitCode: 0, + cmdFactory := test.FakeCommandFactory{ + Stdout: tt.versionOutput, + ExitCode: 0, } installer := EmuInstaller{ @@ -155,63 +154,3 @@ func TestBackupEmuDir(t *testing.T) { }) } } - -type fakeCommandFactory struct { - stdout string - exitCode int -} - -func (f fakeCommandFactory) Create(name string, args []string, _ *command.Opts) command.Command { - return fakeCommand{ - command: fmt.Sprintf("%s %s", name, strings.Join(args, " ")), - stdout: f.stdout, - exitCode: f.exitCode, - } -} - -type fakeCommand struct { - command string - stdout string - stderr string - exitCode int -} - -func (c fakeCommand) PrintableCommandArgs() string { - return c.command -} - -func (c fakeCommand) Run() error { - if c.exitCode != 0 { - return fmt.Errorf("exit code %d", c.exitCode) - } - return nil -} - -func (c fakeCommand) RunAndReturnExitCode() (int, error) { - if c.exitCode != 0 { - return c.exitCode, fmt.Errorf("exit code %d", c.exitCode) - } - return c.exitCode, nil -} - -func (c fakeCommand) RunAndReturnTrimmedOutput() (string, error) { - if c.exitCode != 0 { - return "", fmt.Errorf("exit code %d", c.exitCode) - } - return c.stdout, nil -} - -func (c fakeCommand) RunAndReturnTrimmedCombinedOutput() (string, error) { - if c.exitCode != 0 { - return "", fmt.Errorf("exit code %d", c.exitCode) - } - return fmt.Sprintf("%s%s", c.stdout, c.stderr), nil -} - -func (c fakeCommand) Start() error { - return nil -} - -func (c fakeCommand) Wait() error { - return nil -} diff --git a/main.go b/main.go index e23a0d1..d26327e 100755 --- a/main.go +++ b/main.go @@ -1,12 +1,10 @@ package main import ( - "bufio" "bytes" "fmt" "os" "path/filepath" - "regexp" "strings" "time" @@ -21,6 +19,7 @@ import ( v2log "github.com/bitrise-io/go-utils/v2/log" "github.com/bitrise-io/go-utils/v2/retryhttp" "github.com/bitrise-io/go-utils/v2/system" + "github.com/bitrise-steplib/steps-avd-manager/adb" "github.com/bitrise-steplib/steps-avd-manager/emuinstaller" "github.com/kballard/go-shellquote" ) @@ -52,43 +51,6 @@ const ( emuBuildNumberPreinstalled = "preinstalled" ) -func runningDeviceInfos(androidHome string) (map[string]string, error) { - cmd := command.New(filepath.Join(androidHome, "platform-tools", "adb"), "devices") - out, err := cmd.RunAndReturnTrimmedCombinedOutput() - if err != nil { - log.Printf(err.Error()) - return map[string]string{}, fmt.Errorf("command failed, error: %s", err) - } - - log.Debugf("$ %s", cmd.PrintableCommandArgs()) - log.Debugf("%s", out) - - // List of devices attached - // emulator-5554 device - deviceListItemPattern := `^(?Pemulator-\d*)[\s+](?P.*)` - deviceListItemRegexp := regexp.MustCompile(deviceListItemPattern) - - deviceStateMap := map[string]string{} - - scanner := bufio.NewScanner(strings.NewReader(out)) - for scanner.Scan() { - line := scanner.Text() - matches := deviceListItemRegexp.FindStringSubmatch(line) - if len(matches) == 3 { - serial := matches[1] - state := matches[2] - - deviceStateMap[serial] = state - } - - } - if scanner.Err() != nil { - return map[string]string{}, fmt.Errorf("scanner failed, error: %s", err) - } - - return deviceStateMap, nil -} - func failf(msg string, args ...interface{}) { log.Errorf(msg, args...) @@ -102,38 +64,6 @@ func failf(msg string, args ...interface{}) { os.Exit(1) } -func currentlyStartedDeviceSerial(alreadyRunningDeviceInfos, currentlyRunningDeviceInfos map[string]string) string { - startedSerial := "" - - for serial := range currentlyRunningDeviceInfos { - _, found := alreadyRunningDeviceInfos[serial] - if !found { - startedSerial = serial - break - } - } - - if len(startedSerial) > 0 { - state := currentlyRunningDeviceInfos[startedSerial] - if state == "device" { - return startedSerial - } - } - - return "" -} - -func queryNewDeviceSerial(androidHome string, runningDevices map[string]string) (string, error) { - currentRunningDevices, err := runningDeviceInfos(androidHome) - if err != nil { - return "", fmt.Errorf("failed to check running devices: %s", err) - } - - serial := currentlyStartedDeviceSerial(runningDevices, currentRunningDevices) - - return serial, nil -} - type phase struct { name string command *command.Model @@ -148,6 +78,9 @@ func validateConfig(cfg config) error { } func main() { + cmdFactory := v2command.NewFactory(env.NewRepository()) + logger := v2log.NewLogger() + var cfg config if err := stepconf.Parse(&cfg); err != nil { failf("Couldn't parse step inputs: %s", err) @@ -170,7 +103,8 @@ func main() { } androidHome := androidSdk.GetAndroidHome() - runningDevices, err := runningDeviceInfos(androidHome) + adbClient := adb.New(androidHome, cmdFactory, logger) + runningDevicesBeforeBoot, err := adbClient.Devices() if err != nil { failf("Failed to check running devices, error: %s", err) } @@ -200,8 +134,6 @@ func main() { } if cfg.EmulatorBuildNumber != emuBuildNumberPreinstalled { - cmdFactory := v2command.NewFactory(env.NewRepository()) - logger := v2log.NewLogger() httpClient := retryhttp.NewClient(logger) emuInstaller := emuinstaller.NewEmuInstaller(androidHome, cmdFactory, logger, httpClient) if err := emuInstaller.Install(cfg.EmulatorBuildNumber); err != nil { @@ -224,7 +156,6 @@ func main() { ) } - phases = append(phases, []phase{ { "Installing system image package", @@ -275,7 +206,7 @@ func main() { } args = append(args, startCustomFlags...) - serial := startEmulator(emulatorPath, args, androidHome, runningDevices, 1) + serial := startEmulator(adbClient, emulatorPath, args, androidHome, runningDevicesBeforeBoot, 1) if err := tools.ExportEnvironmentWithEnvman("BITRISE_EMULATOR_SERIAL", serial); err != nil { log.Warnf("Failed to export environment (BITRISE_EMULATOR_SERIAL), error: %s", err) @@ -285,7 +216,7 @@ func main() { log.Donef("- Done") } -func startEmulator(emulatorPath string, args []string, androidHome string, runningDevices map[string]string, attempt int) string { +func startEmulator(adbClient adb.ADB, emulatorPath string, args []string, androidHome string, runningDevices map[string]string, attempt int) string { var output bytes.Buffer deviceStartCmd := command.New(emulatorPath, args...).SetStdout(&output).SetStderr(&output) @@ -332,7 +263,7 @@ waitLoop: failf(errorMsg) case <-deviceCheckTicker.C: var err error - serial, err = queryNewDeviceSerial(androidHome, runningDevices) + serial, err = adbClient.FindNewDevice(runningDevices) if err != nil { failf("Error: %s", err) } else if serial != "" { @@ -357,7 +288,7 @@ waitLoop: timeoutTimer.Stop() deviceCheckTicker.Stop() if retry { - return startEmulator(emulatorPath, args, androidHome, runningDevices, attempt+1) + return startEmulator(adbClient, emulatorPath, args, androidHome, runningDevices, attempt+1) } return serial } diff --git a/test/fakes.go b/test/fakes.go new file mode 100644 index 0000000..5b71235 --- /dev/null +++ b/test/fakes.go @@ -0,0 +1,68 @@ +package test + +import ( + "fmt" + "strings" + + "github.com/bitrise-io/go-utils/v2/command" +) + +type FakeCommandFactory struct { + Stdout string + ExitCode int +} + +func (f FakeCommandFactory) Create(name string, args []string, _ *command.Opts) command.Command { + return fakeCommand{ + command: fmt.Sprintf("%s %s", name, strings.Join(args, " ")), + stdout: f.Stdout, + exitCode: f.ExitCode, + } +} + +type fakeCommand struct { + command string + stdout string + stderr string + exitCode int +} + +func (c fakeCommand) PrintableCommandArgs() string { + return c.command +} + +func (c fakeCommand) Run() error { + if c.exitCode != 0 { + return fmt.Errorf("exit code %d", c.exitCode) + } + return nil +} + +func (c fakeCommand) RunAndReturnExitCode() (int, error) { + if c.exitCode != 0 { + return c.exitCode, fmt.Errorf("exit code %d", c.exitCode) + } + return c.exitCode, nil +} + +func (c fakeCommand) RunAndReturnTrimmedOutput() (string, error) { + if c.exitCode != 0 { + return "", fmt.Errorf("exit code %d", c.exitCode) + } + return c.stdout, nil +} + +func (c fakeCommand) RunAndReturnTrimmedCombinedOutput() (string, error) { + if c.exitCode != 0 { + return "", fmt.Errorf("exit code %d", c.exitCode) + } + return fmt.Sprintf("%s%s", c.stdout, c.stderr), nil +} + +func (c fakeCommand) Start() error { + return nil +} + +func (c fakeCommand) Wait() error { + return nil +}