mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-02 11:07:10 +00:00
adb uninstall answers Failure [DELETE_FAILED_INTERNAL_ERROR] both when the package was never installed and when it refuses to remove one, so the failure text cannot say which happened and the old code installed over the top either way, keeping the data clear-state was asked to drop. Ask pm path instead, and fall back to pm clear when the app is still there.
575 lines
18 KiB
Go
575 lines
18 KiB
Go
package android
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestWakeCommands(t *testing.T) {
|
|
want := [][]string{
|
|
{"svc", "power", "stayon", "true"},
|
|
{"input", "keyevent", "KEYCODE_WAKEUP"},
|
|
{"wm", "dismiss-keyguard"},
|
|
}
|
|
if got := wakeCommands(); !reflect.DeepEqual(got, want) {
|
|
t.Errorf("wakeCommands() = %v, want %v", got, want)
|
|
}
|
|
}
|
|
|
|
func TestParseAdbDevices_OnlineOnly(t *testing.T) {
|
|
output := `List of devices attached
|
|
emulator-5554 device
|
|
emulator-5556 offline
|
|
physical-abc device
|
|
`
|
|
|
|
got := parseAdbDevices(output)
|
|
want := []string{"emulator-5554", "physical-abc"}
|
|
if !reflect.DeepEqual(got, want) {
|
|
t.Fatalf("got %v, want %v", got, want)
|
|
}
|
|
}
|
|
|
|
func TestParseAdbDevices_Empty(t *testing.T) {
|
|
output := "List of devices attached\n\n"
|
|
|
|
got := parseAdbDevices(output)
|
|
if len(got) != 0 {
|
|
t.Fatalf("got %v, want empty", got)
|
|
}
|
|
}
|
|
|
|
func TestPickDevice_RequestedOnline(t *testing.T) {
|
|
serial, found, err := pickDevice("physical-abc", []string{"emulator-5554", "physical-abc"})
|
|
if err != nil || !found || serial != "physical-abc" {
|
|
t.Fatalf("got (%q, %v, %v), want (physical-abc, true, nil)", serial, found, err)
|
|
}
|
|
}
|
|
|
|
func TestPickDevice_RequestedNotConnected(t *testing.T) {
|
|
_, found, err := pickDevice("physical-abc", []string{"emulator-5554"})
|
|
if err == nil {
|
|
t.Fatal("expected error for a serial that is not connected")
|
|
}
|
|
if found {
|
|
t.Fatal("found must be false when the requested device is absent")
|
|
}
|
|
}
|
|
|
|
func TestPickDevice_NoRequestSingleDeviceUsesIt(t *testing.T) {
|
|
serial, found, err := pickDevice("", []string{"emulator-5554"})
|
|
if err != nil || !found || serial != "emulator-5554" {
|
|
t.Fatalf("got (%q, %v, %v), want (emulator-5554, true, nil)", serial, found, err)
|
|
}
|
|
}
|
|
|
|
func TestPickDevice_NoRequestMultipleDevicesErrors(t *testing.T) {
|
|
serial, found, err := pickDevice("", []string{"emulator-5554", "physical-abc"})
|
|
if err == nil {
|
|
t.Fatal("expected an error asking for --device when several devices are connected")
|
|
}
|
|
if found || serial != "" {
|
|
t.Fatalf("ambiguous selection must not pick a device, got (%q, %v)", serial, found)
|
|
}
|
|
}
|
|
|
|
func TestPickDevice_NoneConnectedFallsBackToAVD(t *testing.T) {
|
|
serial, found, err := pickDevice("", nil)
|
|
if err != nil || found || serial != "" {
|
|
t.Fatalf("got (%q, %v, %v), want (\"\", false, nil)", serial, found, err)
|
|
}
|
|
}
|
|
|
|
func TestParseAVDList_DropsInfoLines(t *testing.T) {
|
|
output := `INFO | Storing crashdata in: /tmp/x
|
|
Medium_Phone_API_36.0
|
|
sanderling_test
|
|
`
|
|
|
|
got := parseAVDList(output)
|
|
want := []string{"Medium_Phone_API_36.0", "sanderling_test"}
|
|
if !reflect.DeepEqual(got, want) {
|
|
t.Fatalf("got %v, want %v", got, want)
|
|
}
|
|
}
|
|
|
|
func TestPickAVD_ExplicitName(t *testing.T) {
|
|
got, err := pickAVD("Pixel_7", []string{"Pixel_7", "sanderling_test"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got != "Pixel_7" {
|
|
t.Fatalf("got %q, want Pixel_7", got)
|
|
}
|
|
}
|
|
|
|
func TestPickAVD_ExplicitMissing(t *testing.T) {
|
|
_, err := pickAVD("Nope", []string{"Pixel_7"})
|
|
if err == nil {
|
|
t.Fatal("expected error")
|
|
}
|
|
}
|
|
|
|
func TestPickAVD_SingleAvailable(t *testing.T) {
|
|
got, err := pickAVD("", []string{"sanderling_test"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got != "sanderling_test" {
|
|
t.Fatalf("got %q, want sanderling_test", got)
|
|
}
|
|
}
|
|
|
|
func TestPickAVD_AmbiguousWithoutHint(t *testing.T) {
|
|
_, err := pickAVD("", []string{"a", "b"})
|
|
if err == nil {
|
|
t.Fatal("expected error when multiple AVDs and no --avd")
|
|
}
|
|
}
|
|
|
|
func TestPickAVD_NoneAvailable(t *testing.T) {
|
|
_, err := pickAVD("", nil)
|
|
if err == nil {
|
|
t.Fatal("expected error when no AVDs exist")
|
|
}
|
|
}
|
|
|
|
// fakeSDK writes an SDK layout holding only the named tools ("emulator/emulator"),
|
|
// so a lookup test never resolves against the host's own SDK.
|
|
func fakeSDK(t *testing.T, tools ...string) string {
|
|
t.Helper()
|
|
root := t.TempDir()
|
|
for _, tool := range tools {
|
|
path := filepath.Join(root, filepath.FromSlash(tool))
|
|
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
|
t.Fatalf("mkdir %s: %v", filepath.Dir(path), err)
|
|
}
|
|
if err := os.WriteFile(path, nil, 0o755); err != nil {
|
|
t.Fatalf("write %s: %v", path, err)
|
|
}
|
|
}
|
|
return root
|
|
}
|
|
|
|
// isolateSDKLookup closes every route to the host's own SDK: an empty PATH, no
|
|
// root variables, a home with nothing under it, and the standard install
|
|
// locations replaced by the given roots. It returns the fake home directory.
|
|
func isolateSDKLookup(t *testing.T, roots ...string) string {
|
|
t.Helper()
|
|
home := t.TempDir()
|
|
t.Setenv("PATH", t.TempDir())
|
|
t.Setenv("ANDROID_HOME", "")
|
|
t.Setenv("ANDROID_SDK_ROOT", "")
|
|
t.Setenv("HOME", home)
|
|
original := standardSDKRoots
|
|
t.Cleanup(func() { standardSDKRoots = original })
|
|
standardSDKRoots = roots
|
|
return home
|
|
}
|
|
|
|
func TestAdbBinary_ResolvesUnderStandardSDKRootWithAndroidHomeUnset(t *testing.T) {
|
|
sdk := fakeSDK(t, "platform-tools/adb")
|
|
isolateSDKLookup(t, sdk)
|
|
|
|
adb, err := AdbBinary()
|
|
if err != nil {
|
|
t.Fatalf("AdbBinary: %v", err)
|
|
}
|
|
if want := filepath.Join(sdk, "platform-tools", "adb"); adb != want {
|
|
t.Errorf("AdbBinary() = %q, want %q", adb, want)
|
|
}
|
|
}
|
|
|
|
func TestEmulatorBinary_ResolvesUnderStandardSDKRootWithAndroidHomeUnset(t *testing.T) {
|
|
sdk := fakeSDK(t, "emulator/emulator")
|
|
isolateSDKLookup(t, sdk)
|
|
|
|
emulator, err := EmulatorBinary()
|
|
if err != nil {
|
|
t.Fatalf("EmulatorBinary: %v", err)
|
|
}
|
|
if want := filepath.Join(sdk, "emulator", "emulator"); emulator != want {
|
|
t.Errorf("EmulatorBinary() = %q, want %q", emulator, want)
|
|
}
|
|
}
|
|
|
|
func TestAdbBinary_UsesAndroidHome(t *testing.T) {
|
|
sdk := fakeSDK(t, "platform-tools/adb")
|
|
isolateSDKLookup(t)
|
|
t.Setenv("ANDROID_HOME", sdk)
|
|
|
|
adb, err := AdbBinary()
|
|
if err != nil {
|
|
t.Fatalf("AdbBinary: %v", err)
|
|
}
|
|
if want := filepath.Join(sdk, "platform-tools", "adb"); adb != want {
|
|
t.Errorf("AdbBinary() = %q, want %q", adb, want)
|
|
}
|
|
}
|
|
|
|
func TestAdbBinary_NoSDKAnywhereReportsWhereItLooked(t *testing.T) {
|
|
empty := t.TempDir()
|
|
home := isolateSDKLookup(t, empty)
|
|
|
|
_, err := AdbBinary()
|
|
if err == nil {
|
|
t.Fatal("expected an error with no SDK anywhere")
|
|
}
|
|
for _, want := range []string{
|
|
"adb not found: not on PATH",
|
|
filepath.Join(home, "Library", "Android", "sdk", "platform-tools", "adb"),
|
|
filepath.Join(empty, "platform-tools", "adb"),
|
|
"$ANDROID_HOME and $ANDROID_SDK_ROOT are unset",
|
|
"put adb on PATH, or point $ANDROID_HOME at an Android SDK that has platform-tools/adb",
|
|
} {
|
|
if !strings.Contains(err.Error(), want) {
|
|
t.Errorf("error %q missing %q", err, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAdbBinary_AndroidHomePointingNowhereIsNamed(t *testing.T) {
|
|
isolateSDKLookup(t, t.TempDir())
|
|
missing := filepath.Join(t.TempDir(), "no-such-sdk")
|
|
t.Setenv("ANDROID_HOME", missing)
|
|
|
|
_, err := AdbBinary()
|
|
if err == nil {
|
|
t.Fatal("expected an error when ANDROID_HOME points nowhere")
|
|
}
|
|
if want := "$ANDROID_HOME=" + missing + " does not exist"; !strings.Contains(err.Error(), want) {
|
|
t.Errorf("error %q must say %q rather than leaving it in the tried list", err, want)
|
|
}
|
|
}
|
|
|
|
func TestPathContains(t *testing.T) {
|
|
path := "/usr/bin:/opt/tools:/usr/local/bin"
|
|
if !pathContains(path, "/opt/tools") {
|
|
t.Error("expected /opt/tools in PATH")
|
|
}
|
|
if pathContains(path, "/nope") {
|
|
t.Error("did not expect /nope in PATH")
|
|
}
|
|
}
|
|
|
|
func TestParseForegroundPackage(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
dumpsys string
|
|
want string
|
|
}{
|
|
{
|
|
name: "mResumedActivity folio",
|
|
dumpsys: " Stack #0:\n mResumedActivity: ActivityRecord{a1b2c3 u0 app.folio/.MainActivity t42}\n",
|
|
want: "app.folio",
|
|
},
|
|
{
|
|
name: "topResumedActivity chrome",
|
|
dumpsys: "ResumedActivity: ActivityRecord{ff u0 com.android.chrome/com.google.android.apps.chrome.Main t9}\n topResumedActivity=ActivityRecord{ff u0 com.android.chrome/com.google.android.apps.chrome.Main}",
|
|
want: "com.android.chrome",
|
|
},
|
|
{
|
|
name: "launcher",
|
|
dumpsys: " mResumedActivity: ActivityRecord{x u0 com.google.android.apps.nexuslauncher/.NexusLauncherActivity t1}",
|
|
want: "com.google.android.apps.nexuslauncher",
|
|
},
|
|
{
|
|
name: "no resumed activity",
|
|
dumpsys: " some unrelated dumpsys output\n with no resumed line\n",
|
|
want: "",
|
|
},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
if got := parseForegroundPackage(tc.dumpsys); got != tc.want {
|
|
t.Errorf("parseForegroundPackage = %q, want %q", got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParseFocusedWindowPackage(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
dumpsys string
|
|
want string
|
|
}{
|
|
{
|
|
name: "folio focused",
|
|
dumpsys: " mCurrentFocus=Window{e00f63a u0 app.folio/app.folio.MainActivity}\n mFocusedApp=ActivityRecord{c0 u0 app.folio/.MainActivity t202}",
|
|
want: "app.folio",
|
|
},
|
|
{
|
|
name: "settings focused",
|
|
dumpsys: " mCurrentFocus=Window{709 u0 com.android.settings/com.android.settings.SubSettings}",
|
|
want: "com.android.settings",
|
|
},
|
|
{
|
|
name: "no focused window mid-launch",
|
|
dumpsys: " mCurrentFocus=null\n mFocusedApp=null",
|
|
want: "",
|
|
},
|
|
{
|
|
name: "no focus line",
|
|
dumpsys: " some unrelated dumpsys window output\n",
|
|
want: "",
|
|
},
|
|
{
|
|
name: "notification shade focused",
|
|
dumpsys: " mCurrentFocus=Window{885e289 u0 NotificationShade}",
|
|
want: "com.android.systemui",
|
|
},
|
|
{
|
|
name: "quick settings focused",
|
|
dumpsys: " mCurrentFocus=Window{abc u0 QuickSettings}",
|
|
want: "com.android.systemui",
|
|
},
|
|
{
|
|
name: "volume dialog focused",
|
|
dumpsys: " mCurrentFocus=Window{abc u0 VolumeUiDialog}",
|
|
want: "com.android.systemui",
|
|
},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
if got := parseFocusedWindowPackage(tc.dumpsys); got != tc.want {
|
|
t.Errorf("parseFocusedWindowPackage = %q, want %q", got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParseEnabledNavOverlay(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
listing string
|
|
want string
|
|
}{
|
|
{
|
|
name: "gesture nav active",
|
|
listing: "[ ] com.android.internal.systemui.navbar.threebutton\n[x] com.android.internal.systemui.navbar.gestural\n[ ] com.android.internal.systemui.navbar.transparent",
|
|
want: "com.android.internal.systemui.navbar.gestural",
|
|
},
|
|
{
|
|
name: "three-button active",
|
|
listing: "[x] com.android.internal.systemui.navbar.threebutton\n[ ] com.android.internal.systemui.navbar.gestural",
|
|
want: "com.android.internal.systemui.navbar.threebutton",
|
|
},
|
|
{
|
|
name: "two-button active",
|
|
listing: "[x] com.android.internal.systemui.navbar.twobutton\n[ ] com.android.internal.systemui.navbar.gestural",
|
|
want: "com.android.internal.systemui.navbar.twobutton",
|
|
},
|
|
{
|
|
name: "ignores enabled non-nav overlays",
|
|
listing: "[x] com.some.other.overlay\n[ ] com.android.internal.systemui.navbar.gestural",
|
|
want: "",
|
|
},
|
|
{
|
|
name: "no overlay enabled",
|
|
listing: "[ ] com.android.internal.systemui.navbar.gestural\n[ ] com.android.internal.systemui.navbar.threebutton",
|
|
want: "",
|
|
},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
if got := parseEnabledNavOverlay(tc.listing); got != tc.want {
|
|
t.Errorf("parseEnabledNavOverlay = %q, want %q", got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAntiFreezeCommands_DisablesFreezersAndExemptsDriver(t *testing.T) {
|
|
commands := antiFreezeCommands()
|
|
has := func(want ...string) bool {
|
|
return slices.ContainsFunc(commands, func(c []string) bool { return slices.Equal(c, want) })
|
|
}
|
|
for _, want := range [][]string{
|
|
{"device_config", "set_sync_disabled_for_tests", "persistent"},
|
|
{"device_config", "put", "activity_manager_native_boot", "use_freezer", "false"},
|
|
{"settings", "put", "global", "settings_enable_monitor_phantom_procs", "false"},
|
|
{"dumpsys", "deviceidle", "disable"},
|
|
} {
|
|
if !has(want...) {
|
|
t.Errorf("anti-freeze commands missing exact command %v", want)
|
|
}
|
|
}
|
|
|
|
// The freezer exemption must be one device_config command whose final
|
|
// argument lists every driver package, not just the package string
|
|
// appearing somewhere among the commands.
|
|
exemption := findCommand(commands, "device_config", "put", "activity_manager_native_boot", "freeze_exempt_inst_pkg")
|
|
if exemption == nil {
|
|
t.Fatalf("no freeze_exempt_inst_pkg command found in %v", commands)
|
|
}
|
|
value := exemption[len(exemption)-1]
|
|
for _, pkg := range driverPackages {
|
|
if !strings.Contains(value, pkg) {
|
|
t.Errorf("freeze_exempt_inst_pkg value %q missing driver package %q", value, pkg)
|
|
}
|
|
if !has("dumpsys", "deviceidle", "whitelist", "+"+pkg) {
|
|
t.Errorf("driver package %q not whitelisted from doze", pkg)
|
|
}
|
|
}
|
|
}
|
|
|
|
// findCommand returns the first command whose leading tokens equal prefix.
|
|
func findCommand(commands [][]string, prefix ...string) []string {
|
|
for _, c := range commands {
|
|
if len(c) >= len(prefix) && slices.Equal(c[:len(prefix)], prefix) {
|
|
return c
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func TestNavModeToRestore(t *testing.T) {
|
|
cases := map[string]string{
|
|
"com.android.internal.systemui.navbar.gestural": "com.android.internal.systemui.navbar.gestural",
|
|
"com.android.internal.systemui.navbar.twobutton": "com.android.internal.systemui.navbar.twobutton",
|
|
"com.android.internal.systemui.navbar.threebutton": "", // already 3-button: nothing to change
|
|
"": "", // unknown current mode: must not switch what cannot be restored
|
|
}
|
|
for original, want := range cases {
|
|
if got := navModeToRestore(original); got != want {
|
|
t.Errorf("navModeToRestore(%q) = %q, want %q", original, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// scriptedAdb puts an adb under a fake SDK root that logs each invocation's
|
|
// arguments and answers from replies, a `case "$*" in` body. SDK lookup is
|
|
// isolated onto that root, so ReinstallApp runs its real command sequence
|
|
// against the script and the log holds what reached adb.
|
|
func scriptedAdb(t *testing.T, replies string) string {
|
|
t.Helper()
|
|
root := t.TempDir()
|
|
log := filepath.Join(root, "adb.log")
|
|
adb := filepath.Join(root, "platform-tools", "adb")
|
|
if err := os.MkdirAll(filepath.Dir(adb), 0o755); err != nil {
|
|
t.Fatalf("mkdir %s: %v", filepath.Dir(adb), err)
|
|
}
|
|
script := "#!/bin/sh\necho \"$*\" >> " + log + "\ncase \"$*\" in\n" + replies + "\nesac\n"
|
|
if err := os.WriteFile(adb, []byte(script), 0o755); err != nil {
|
|
t.Fatalf("write %s: %v", adb, err)
|
|
}
|
|
isolateSDKLookup(t, root)
|
|
return log
|
|
}
|
|
|
|
func adbCalls(t *testing.T, log string) []string {
|
|
t.Helper()
|
|
contents, err := os.ReadFile(log)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return nil
|
|
}
|
|
t.Fatalf("read %s: %v", log, err)
|
|
}
|
|
return strings.Split(strings.TrimSpace(string(contents)), "\n")
|
|
}
|
|
|
|
const (
|
|
uninstallRefused = `"uninstall "*) echo "Failure [DELETE_FAILED_INTERNAL_ERROR]"; exit 1;;`
|
|
stillInstalled = `"shell pm path "*) echo "package:/data/app/app.example-1/base.apk";;`
|
|
notInstalled = `"shell pm path "*) exit 1;;`
|
|
installedSuccessfully = `"install "*) echo "Success";;`
|
|
)
|
|
|
|
func TestReinstallApp_RefusedUninstallClearsTheDataItLeftBehind(t *testing.T) {
|
|
log := scriptedAdb(t, strings.Join([]string{
|
|
uninstallRefused,
|
|
stillInstalled,
|
|
`"shell pm clear "*) echo "Success";;`,
|
|
installedSuccessfully,
|
|
}, "\n"))
|
|
output := &strings.Builder{}
|
|
|
|
if err := ReinstallApp(t.Context(), "", "app.example", "/tmp/app.apk", output); err != nil {
|
|
t.Fatalf("ReinstallApp: %v", err)
|
|
}
|
|
|
|
want := []string{
|
|
"uninstall app.example",
|
|
"shell pm path app.example",
|
|
"shell pm clear app.example",
|
|
"install -r /tmp/app.apk",
|
|
}
|
|
if got := adbCalls(t, log); !slices.Equal(got, want) {
|
|
t.Fatalf("adb calls = %v, want %v", got, want)
|
|
}
|
|
if !strings.Contains(output.String(), "pm clear") {
|
|
t.Errorf("output %q does not say the data was cleared some other way", output.String())
|
|
}
|
|
}
|
|
|
|
func TestReinstallApp_RefusedUninstallThatCannotBeClearedIsFatal(t *testing.T) {
|
|
log := scriptedAdb(t, strings.Join([]string{
|
|
uninstallRefused,
|
|
stillInstalled,
|
|
`"shell pm clear "*) echo "Failed"; exit 1;;`,
|
|
installedSuccessfully,
|
|
}, "\n"))
|
|
|
|
err := ReinstallApp(t.Context(), "", "app.example", "/tmp/app.apk", &strings.Builder{})
|
|
|
|
if err == nil {
|
|
t.Fatal("ReinstallApp reported success while app.example kept the data clear-state was asked to remove")
|
|
}
|
|
for _, want := range []string{"app.example", "DELETE_FAILED_INTERNAL_ERROR", "Failed"} {
|
|
if !strings.Contains(err.Error(), want) {
|
|
t.Errorf("error %q does not quote %q", err, want)
|
|
}
|
|
}
|
|
if slices.Contains(adbCalls(t, log), "install -r /tmp/app.apk") {
|
|
t.Error("installed over an app whose data survived, which is the reinstall reporting a clear it did not perform")
|
|
}
|
|
}
|
|
|
|
func TestReinstallApp_UninstallFailureWithNothingInstalledIsQuiet(t *testing.T) {
|
|
log := scriptedAdb(t, strings.Join([]string{
|
|
uninstallRefused,
|
|
notInstalled,
|
|
installedSuccessfully,
|
|
}, "\n"))
|
|
output := &strings.Builder{}
|
|
|
|
if err := ReinstallApp(t.Context(), "", "app.example", "/tmp/app.apk", output); err != nil {
|
|
t.Fatalf("ReinstallApp: %v", err)
|
|
}
|
|
|
|
calls := adbCalls(t, log)
|
|
if !slices.Contains(calls, "install -r /tmp/app.apk") {
|
|
t.Fatalf("adb calls = %v, want the install to go ahead: a first run has no app to uninstall", calls)
|
|
}
|
|
if slices.Contains(calls, "shell pm clear app.example") {
|
|
t.Errorf("adb calls = %v, want no data clear: there was no app holding data", calls)
|
|
}
|
|
if output.String() != "" {
|
|
t.Errorf("output = %q, want nothing: a first run has no app to uninstall", output.String())
|
|
}
|
|
}
|
|
|
|
func TestReinstallApp_SuccessfulUninstallNeedsNoFallback(t *testing.T) {
|
|
log := scriptedAdb(t, strings.Join([]string{
|
|
`"uninstall "*) echo "Success";;`,
|
|
stillInstalled,
|
|
`"shell pm clear "*) echo "Success";;`,
|
|
installedSuccessfully,
|
|
}, "\n"))
|
|
|
|
if err := ReinstallApp(t.Context(), "", "app.example", "/tmp/app.apk", &strings.Builder{}); err != nil {
|
|
t.Fatalf("ReinstallApp: %v", err)
|
|
}
|
|
|
|
want := []string{"uninstall app.example", "install -r /tmp/app.apk"}
|
|
if got := adbCalls(t, log); !slices.Equal(got, want) {
|
|
t.Fatalf("adb calls = %v, want %v", got, want)
|
|
}
|
|
}
|