# 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()