blob: 5eee83fcad2ae21c7345900301f871c5c02c213c [file] [log] [blame]
# pylint: disable=g-direct-third-party-import
# Copyright 2015 The Bazel Authors. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Unit tests for stubify_incremental_install."""
import os
import unittest
import zipfile
from tools.android import incremental_install
from third_party.py import mock
class MockAdb(object):
"""Mocks the Android ADB binary."""
def __init__(self):
# Map of file name -> contents.
self.files = {}
self.split_apks = set()
self._error = None
self.package_timestamp = None
self._last_package_timestamp = 1
self.shell_cmdlns = []
self.abi = "armeabi-v7a"
def Exec(self, args):
if self._error:
error_info, arg = self._error # pylint: disable=unpacking-non-sequence
if not arg or arg in args:
return self._CreatePopenMock(*error_info)
returncode = 0
stdout = ""
stderr = ""
cmd = args[1]
if cmd == "push":
# "/test/adb push local remote"
with open(args[2], "rb") as f:
content = f.read().decode("utf-8")
self.files[args[3]] = content
elif cmd == "pull":
# "/test/adb pull remote local"
remote = args[2]
local = args[3]
content = self.files.get(remote)
if content is not None:
with open(local, "wb") as f:
f.write(content.encode("utf-8"))
else:
returncode = 1
stderr = "remote object '%s' does not exist\n" % remote
elif cmd == "install":
self.package_timestamp = self._last_package_timestamp
self._last_package_timestamp += 1
return self._CreatePopenMock(0, "Success", "")
elif cmd == "install-multiple":
if args[3] == "-p":
with open(args[5], "rb") as f:
content = f.read().decode("utf-8")
self.split_apks.add(content)
else:
self.package_timestamp = self._last_package_timestamp
self._last_package_timestamp += 1
return self._CreatePopenMock(0, "Success", "")
elif cmd == "uninstall":
self._CreatePopenMock(0, "Success", "")
self.split_apks = set()
self.package_timestamp = None
elif cmd == "shell":
# "/test/adb shell ..."
# mkdir, rm, am (application manager), or monkey
shell_cmdln = args[2]
self.shell_cmdlns.append(shell_cmdln)
if shell_cmdln.startswith(("mkdir", "am", "monkey", "input")):
pass
elif shell_cmdln.startswith("dumpsys package "):
if self.package_timestamp is not None:
timestamp = "firstInstallTime=%s" % self.package_timestamp
else:
timestamp = ""
return self._CreatePopenMock(0, timestamp, "")
elif shell_cmdln.startswith("rm"):
file_path = shell_cmdln.split()[2]
self.files.pop(file_path, None)
elif shell_cmdln.startswith("getprop ro.product.cpu.abi"):
return self._CreatePopenMock(0, self.abi, "")
else:
raise Exception("Unknown shell command line: %s" % shell_cmdln)
# Return a mock subprocess.Popen object
return self._CreatePopenMock(returncode, stdout, stderr)
def _CreatePopenMock(self, returncode, stdout, stderr):
return mock.Mock(
returncode=returncode, communicate=lambda: (stdout, stderr))
def SetError(self, returncode, stdout, stderr, for_arg=None):
self._error = ((returncode, stdout, stderr), for_arg)
def SetAbi(self, abi):
self.abi = abi
class IncrementalInstallTest(unittest.TestCase):
"""Unit tests for incremental install."""
_DEXMANIFEST = "dexmanifest.txt"
_ADB_PATH = "/test/adb"
_OUTPUT_MARKER = "full_deploy_marker"
_APK = "myapp_incremental.apk"
_RESOURCE_APK = "incremental.ap_"
_STUB_DATAFILE = "stub_application_data.txt"
_OLD_APP_PACKGE = "old.app.package"
_APP_PACKAGE = "new.app.package"
_EXEC_ROOT = "."
def setUp(self):
os.chdir(os.environ["TEST_TMPDIR"])
self._mock_adb = MockAdb()
# Write the stub datafile which contains the package name of the app.
with open(self._STUB_DATAFILE, "wb") as f:
f.write(("\n".join([self._OLD_APP_PACKGE, self._APP_PACKAGE]))
.encode("utf-8"))
# Write the local resource apk file.
with open(self._RESOURCE_APK, "wb") as f:
f.write(b"resource apk")
# Mock out subprocess.Popen to use our mock adb.
self._popen_patch = mock.patch.object(incremental_install, "subprocess")
self._popen = self._popen_patch.start().Popen
self._popen.side_effect = lambda args, **kwargs: self._mock_adb.Exec(args)
def tearDown(self):
self._popen_patch.stop()
def _CreateZip(self, name="zip1", *files):
if not files:
files = [("zp1", "content1"), ("zp2", "content2")]
with zipfile.ZipFile(name, "w") as z:
for f, content in files:
z.writestr(f, content)
def _CreateLocalManifest(self, *lines):
content = "\n".join(lines)
with open(self._DEXMANIFEST, "wb") as f:
f.write(content.encode("utf-8"))
return content
def _CreateRemoteManifest(self, *lines):
self._PutDeviceFile("dex/manifest", "\n".join(lines))
def _GetDeviceAppPath(self, f):
return os.path.join(
incremental_install.DEVICE_DIRECTORY, self._APP_PACKAGE, f)
def _GetDeviceFile(self, f):
return self._mock_adb.files[self._GetDeviceAppPath(f)]
def _PutDeviceFile(self, f, content):
self._mock_adb.files[self._GetDeviceAppPath(f)] = content
def _DeleteDeviceFile(self, f):
self._mock_adb.files.pop(self._GetDeviceAppPath(f), None)
def _CallIncrementalInstall(self, incremental, native_libs=None,
split_main_apk=None, split_apks=None,
start_type="no"):
if split_main_apk:
apk = split_main_apk
elif incremental:
apk = None
else:
apk = self._APK
incremental_install.IncrementalInstall(
adb_path=self._ADB_PATH,
execroot=self._EXEC_ROOT,
stub_datafile=self._STUB_DATAFILE,
dexmanifest=self._DEXMANIFEST,
apk=apk,
resource_apk=self._RESOURCE_APK,
split_main_apk=split_main_apk,
split_apks=split_apks,
native_libs=native_libs,
output_marker=self._OUTPUT_MARKER,
adb_jobs=1,
start_type=start_type,
user_home_dir="/home/root")
def testUploadToPristineDevice(self):
self._CreateZip()
with open("dex1", "wb") as f:
f.write(b"content3")
manifest = self._CreateLocalManifest(
"zip1 zp1 ip1 0",
"zip1 zp2 ip2 0",
"dex1 - ip3 0")
self._CallIncrementalInstall(incremental=False)
resources_checksum_path = self._GetDeviceAppPath("resources_checksum")
self.assertTrue(resources_checksum_path in self._mock_adb.files)
self.assertEqual(manifest, self._GetDeviceFile("dex/manifest"))
self.assertEqual("content1", self._GetDeviceFile("dex/ip1"))
self.assertEqual("content2", self._GetDeviceFile("dex/ip2"))
self.assertEqual("content3", self._GetDeviceFile("dex/ip3"))
self.assertEqual("resource apk", self._GetDeviceFile("resources.ap_"))
def testSplitInstallToPristineDevice(self):
with open("split1", "wb") as f:
f.write(b"split_content1")
with open("main", "wb") as f:
f.write(b"main_Content")
self._CallIncrementalInstall(
incremental=False, split_main_apk="main", split_apks=["split1"])
self.assertEqual(set(["split_content1"]), self._mock_adb.split_apks)
def testSplitInstallUnchanged(self):
with open("split1", "wb") as f:
f.write(b"split_content1")
with open("main", "wb") as f:
f.write(b"main_Content")
self._CallIncrementalInstall(
incremental=False, split_main_apk="main", split_apks=["split1"])
self.assertEqual(set(["split_content1"]), self._mock_adb.split_apks)
self._mock_adb.split_apks = set()
self._CallIncrementalInstall(
incremental=False, split_main_apk="main", split_apks=["split1"])
self.assertEqual(set([]), self._mock_adb.split_apks)
def testSplitInstallChanges(self):
with open("split1", "wb") as f:
f.write(b"split_content1")
with open("main", "wb") as f:
f.write(b"main_Content")
self._CallIncrementalInstall(
incremental=False, split_main_apk="main", split_apks=["split1"])
self.assertEqual(set(["split_content1"]), self._mock_adb.split_apks)
with open("split1", "wb") as f:
f.write(b"split_content2")
self._mock_adb.split_apks = set()
self._CallIncrementalInstall(
incremental=False, split_main_apk="main", split_apks=["split1"])
self.assertEqual(set(["split_content2"]), self._mock_adb.split_apks)
def testMissingNativeManifestWithIncrementalInstall(self):
self._CreateZip()
with open("liba.so", "wb") as f:
f.write(b"liba_1")
# Upload a library to the device.
native_libs = ["armeabi-v7a:liba.so"]
self._CallIncrementalInstall(incremental=False, native_libs=native_libs)
self.assertEqual("liba_1", self._GetDeviceFile("native/liba.so"))
# Delete the manifest, overwrite the library and check that even an
# incremental install straightens things out.
self._PutDeviceFile("native/liba.so", "GARBAGE")
self._CallIncrementalInstall(incremental=False, native_libs=native_libs)
self.assertEqual("liba_1", self._GetDeviceFile("native/liba.so"))
def testNonIncrementalInstallOverwritesNativeLibs(self):
self._CreateZip()
with open("liba.so", "wb") as f:
f.write(b"liba_1")
# Upload a library to the device.
native_libs = ["armeabi-v7a:liba.so"]
self._CallIncrementalInstall(incremental=False, native_libs=native_libs)
self.assertEqual("liba_1", self._GetDeviceFile("native/liba.so"))
# Change a library on the device. Incremental install should not replace the
# changed file, because it only checks the manifest.
self._PutDeviceFile("native/liba.so", "GARBAGE")
self._CallIncrementalInstall(incremental=True, native_libs=native_libs)
self.assertEqual("GARBAGE", self._GetDeviceFile("native/liba.so"))
# However, a full install should overwrite it.
self._CallIncrementalInstall(incremental=False, native_libs=native_libs)
self.assertEqual("liba_1", self._GetDeviceFile("native/liba.so"))
def testNativeAbiCompatibility(self):
self._CreateZip()
with open("liba.so", "wb") as f:
f.write(b"liba")
native_libs = ["armeabi:liba.so"]
self._mock_adb.SetAbi("arm64-v8a")
self._CallIncrementalInstall(incremental=False, native_libs=native_libs)
self.assertEqual("liba", self._GetDeviceFile("native/liba.so"))
def testUploadNativeLibs(self):
self._CreateZip()
with open("liba.so", "wb") as f:
f.write(b"liba_1")
with open("libb.so", "wb") as f:
f.write(b"libb_1")
native_libs = ["armeabi-v7a:liba.so", "armeabi-v7a:libb.so"]
self._CallIncrementalInstall(incremental=False, native_libs=native_libs)
self.assertEqual("liba_1", self._GetDeviceFile("native/liba.so"))
self.assertEqual("libb_1", self._GetDeviceFile("native/libb.so"))
# Change a library
with open("libb.so", "wb") as f:
f.write(b"libb_2")
self._CallIncrementalInstall(incremental=True, native_libs=native_libs)
self.assertEqual("libb_2", self._GetDeviceFile("native/libb.so"))
# Delete a library
self._CallIncrementalInstall(
incremental=True, native_libs=["armeabi-v7a:liba.so"])
self.assertFalse(
self._GetDeviceAppPath("native/libb.so") in self._mock_adb.files)
# Add the deleted library back
self._CallIncrementalInstall(incremental=True, native_libs=native_libs)
self.assertEqual("libb_2", self._GetDeviceFile("native/libb.so"))
def testUploadWithOneChangedFile(self):
# Existing manifest from a previous install.
self._CreateRemoteManifest(
"zip1 zp1 ip1 0",
"zip1 zp2 ip2 1")
# Existing files from a previous install.
self._PutDeviceFile("dex/ip1", "old content1")
self._PutDeviceFile("dex/ip2", "old content2")
self._PutDeviceFile("install_timestamp", "0")
self._mock_adb.package_timestamp = "0"
self._CreateZip()
# Updated dex manifest.
self._CreateLocalManifest(
"zip1 zp1 ip1 0",
"zip1 zp2 ip2 2")
self._CallIncrementalInstall(incremental=True)
# This is a bit of a dishonest test: the local content for "ip1" is
# "content1" and the remote content for it is "old content1", but
# the checksums for that file are the same in the local and remote manifest.
# We just want to make sure that only one file was updated, so to
# distinguish that we force the local and remote content to be different but
# keep the checksum the same.
self.assertEqual("old content1", self._GetDeviceFile("dex/ip1"))
self.assertEqual("content2", self._GetDeviceFile("dex/ip2"))
def testFullUploadWithOneChangedFile(self):
# Existing manifest from a previous install.
self._CreateRemoteManifest(
"zip1 zp1 ip1 0",
"zip1 zp2 ip2 1")
self._PutDeviceFile("dex/ip1", "old content1")
self._PutDeviceFile("dex/ip2", "old content2")
self._PutDeviceFile("install_timestamp", "0")
self._mock_adb.package_timestamp = "0"
self._CreateZip()
self._CreateLocalManifest(
"zip1 zp1 ip1 0",
"zip1 zp2 ip2 2")
self._CallIncrementalInstall(incremental=False)
# Even though the checksums for ip1 were the same, the file still got
# updated. This is a bit of a dishonest test because the local and remote
# content for ip1 were different, but their checksums were the same.
self.assertEqual("content1", self._GetDeviceFile("dex/ip1"))
self.assertEqual("content2", self._GetDeviceFile("dex/ip2"))
def testUploadWithNewFile(self):
self._CreateRemoteManifest("zip1 zp1 ip1 0")
self._PutDeviceFile("dex/ip1", "content1")
self._PutDeviceFile("install_timestamp", "0")
self._mock_adb.package_timestamp = "0"
self._CreateLocalManifest(
"zip1 zp1 ip1 0",
"zip1 zp2 ip2 1")
self._CreateZip()
self._CallIncrementalInstall(incremental=True)
self.assertEqual("content1", self._GetDeviceFile("dex/ip1"))
self.assertEqual("content2", self._GetDeviceFile("dex/ip2"))
def testDeletesFile(self):
self._CreateRemoteManifest(
"zip1 zp1 ip1 0",
"zip1 zip2 ip2 1")
self._PutDeviceFile("dex/ip1", "content1")
self._PutDeviceFile("dex/ip2", "content2")
self._PutDeviceFile("install_timestamp", "1")
self._mock_adb.package_timestamp = "1"
self._CreateZip("zip1", ("zp1", "content1"))
self._CreateLocalManifest("zip1 zp1 ip1 0")
self.assertTrue(self._GetDeviceAppPath("dex/ip2") in self._mock_adb.files)
self._CallIncrementalInstall(incremental=True)
self.assertFalse(self._GetDeviceAppPath("dex/ip2") in self._mock_adb.files)
def testNothingToUpdate(self):
self._CreateRemoteManifest(
"zip1 zp1 ip1 0",
"zip1 zip2 ip2 1",
"dex1 - ip3 0")
self._PutDeviceFile("dex/ip1", "content1")
self._PutDeviceFile("dex/ip2", "content2")
self._PutDeviceFile("dex/ip3", "content3")
self._PutDeviceFile("install_timestamp", "0")
self._mock_adb.package_timestamp = "0"
self._CreateZip()
self._CreateLocalManifest(
"zip1 zp1 ip1 0",
"zip1 zip2 ip2 1",
"dex1 - ip3 0")
self._CallIncrementalInstall(incremental=True)
self.assertEqual("content1", self._GetDeviceFile("dex/ip1"))
self.assertEqual("content2", self._GetDeviceFile("dex/ip2"))
self.assertEqual("content3", self._GetDeviceFile("dex/ip3"))
def testNoResourcesToUpdate(self):
self._CreateRemoteManifest("zip1 zp1 ip1 0")
self._PutDeviceFile("dex/ip1", "content1")
# The local file is actually "resource apk", but the checksum on the device
# for the resources file is set to be the same as the checksum for the
# local file so that we can confirm that it was not updated.
self._PutDeviceFile("resources.ap_", "resources")
self._PutDeviceFile("resources_checksum",
incremental_install.Checksum(self._RESOURCE_APK))
self._PutDeviceFile("install_timestamp", "0")
self._mock_adb.package_timestamp = "0"
self._CreateZip()
self._CreateLocalManifest("zip1 zp1 ip1 0")
self._CallIncrementalInstall(incremental=True)
self.assertEqual("resources", self._GetDeviceFile("resources.ap_"))
def testUpdateResources(self):
self._CreateRemoteManifest("zip1 zp1 ip1 0")
self._PutDeviceFile("dex/ip1", "content1")
self._PutDeviceFile("resources.ap_", "resources")
self._PutDeviceFile("resources_checksum", "checksum")
self._PutDeviceFile("install_timestamp", "0")
self._mock_adb.package_timestamp = "0"
self._CreateZip()
self._CreateLocalManifest("zip1 zp1 ip1 0")
self._CallIncrementalInstall(incremental=True)
self.assertEqual("resource apk", self._GetDeviceFile("resources.ap_"))
def testNoDevice(self):
self._mock_adb.SetError(1, "", "device not found")
try:
self._CallIncrementalInstall(incremental=True)
self.fail("Should have quit if there is no device")
except SystemExit as e:
# make sure it's the right SystemExit reason
self.assertTrue("Device not found" in str(e))
def testUnauthorizedDevice(self):
self._mock_adb.SetError(1, "", "device unauthorized. Please check the "
"confirmation dialog on your device")
try:
self._CallIncrementalInstall(incremental=True)
self.fail("Should have quit if the device is unauthorized.")
except SystemExit as e:
# make sure it's the right SystemExit reason
self.assertTrue("Device unauthorized." in str(e))
def testInstallFailure(self):
self._mock_adb.SetError(0, "Failure", "INSTALL_FAILED", for_arg="install")
self._CreateZip()
self._CreateLocalManifest("zip1 zp1 ip1 0")
try:
self._CallIncrementalInstall(incremental=False)
self.fail("Should have quit if the install failed.")
except SystemExit as e:
# make sure it's the right SystemExit reason
self.assertTrue("Failure" in str(e))
def testStartCold(self):
# Based on testUploadToPristineDevice
self._CreateZip()
with open("dex1", "wb") as f:
f.write(b"content3")
self._CreateLocalManifest(
"zip1 zp1 ip1 0",
"zip1 zp2 ip2 0",
"dex1 - ip3 0")
self._CallIncrementalInstall(incremental=False, start_type="cold")
self.assertTrue(("monkey -p %s -c android.intent.category.LAUNCHER 1" %
self._APP_PACKAGE) in self._mock_adb.shell_cmdlns)
def testDebugStart(self):
self._CreateZip()
with open("dex1", "wb") as f:
f.write(b"content3")
self._CreateLocalManifest(
"zip1 zp1 ip1 0",
"zip1 zp2 ip2 0",
"dex1 - ip3 0")
self._CallIncrementalInstall(incremental=False, start_type="debug")
enable_debug_cmd = ("am set-debug-app -w --persistent %s" %
self._APP_PACKAGE)
start_app_cmd = ("monkey -p %s -c android.intent.category.LAUNCHER 1" %
self._APP_PACKAGE)
self.assertTrue(enable_debug_cmd in self._mock_adb.shell_cmdlns)
self.assertTrue(start_app_cmd in self._mock_adb.shell_cmdlns)
def testColdStop(self):
self._CreateRemoteManifest(
"zip1 zp1 ip1 0",
"zip1 zip2 ip2 1",
"dex1 - ip3 0")
self._PutDeviceFile("dex/ip1", "content1")
self._PutDeviceFile("dex/ip2", "content2")
self._PutDeviceFile("dex/ip3", "content3")
self._PutDeviceFile("install_timestamp", "0")
self._mock_adb.package_timestamp = "0"
self._CreateZip()
self._CreateLocalManifest(
"zip1 zp1 ip1 0",
"zip1 zip2 ip2 1",
"dex1 - ip3 0")
self._CallIncrementalInstall(incremental=True, start_type="cold")
stop_cmd = "am force-stop %s" % self._APP_PACKAGE
self.assertTrue(stop_cmd in self._mock_adb.shell_cmdlns)
def testWarmStop(self):
self._CreateRemoteManifest(
"zip1 zp1 ip1 0",
"zip1 zip2 ip2 1",
"dex1 - ip3 0")
self._PutDeviceFile("dex/ip1", "content1")
self._PutDeviceFile("dex/ip2", "content2")
self._PutDeviceFile("dex/ip3", "content3")
self._PutDeviceFile("install_timestamp", "0")
self._mock_adb.package_timestamp = "0"
self._CreateZip()
self._CreateLocalManifest(
"zip1 zp1 ip1 0",
"zip1 zip2 ip2 1",
"dex1 - ip3 0")
self._CallIncrementalInstall(incremental=True, start_type="warm")
background_cmd = "input keyevent KEYCODE_APP_SWITCH"
stop_cmd = "am kill %s" % self._APP_PACKAGE
self.assertTrue(background_cmd in self._mock_adb.shell_cmdlns)
self.assertTrue(stop_cmd in self._mock_adb.shell_cmdlns)
def testMultipleDevicesError(self):
errors = [
"more than one device and emulator",
"more than one device",
"more than one emulator",
]
for error in errors:
self._mock_adb.SetError(1, "", error)
try:
self._CallIncrementalInstall(incremental=True)
self.fail("Should have quit if there were multiple devices.")
except SystemExit as e:
# make sure it's the right SystemExit reason
self.assertTrue("Try specifying a device serial" in str(e))
def testIncrementalInstallOnPristineDevice(self):
self._CreateZip()
self._CreateLocalManifest(
"zip1 zp1 ip1 0",
"zip1 zip2 ip2 1",
"dex1 - ip3 0")
try:
self._CallIncrementalInstall(incremental=True)
self.fail("Should have quit for incremental install on pristine device")
except SystemExit:
pass
def testIncrementalInstallWithWrongInstallTimestamp(self):
self._CreateRemoteManifest(
"zip1 zp1 ip1 0",
"zip1 zip2 ip2 1",
"dex1 - ip3 0")
self._PutDeviceFile("dex/ip1", "content1")
self._PutDeviceFile("dex/ip2", "content2")
self._PutDeviceFile("dex/ip3", "content3")
self._mock_adb.package_timestamp = "WRONG"
self._CreateZip()
self._CreateLocalManifest(
"zip1 zp1 ip1 0",
"zip1 zip2 ip2 1",
"dex1 - ip3 0")
try:
self._CallIncrementalInstall(incremental=True)
self.fail("Should have quit if install timestamp is wrong")
except SystemExit:
pass
def testSdkTooOld(self):
self._mock_adb.SetError(
0, "INSTALL_FAILED_OLDER_SDK", "", for_arg="install")
self._CreateZip()
self._CreateLocalManifest("zip1 zp1 ip1 0")
try:
self._CallIncrementalInstall(incremental=False)
self.fail("Should have quit if the SDK is too old.")
except SystemExit as e:
# make sure it's the right SystemExit reason
self.assertTrue("minSdkVersion" in str(e))
if __name__ == "__main__":
unittest.main()