Adds tools for installing Android apps.

--
MOS_MIGRATED_REVID=94198797
diff --git a/tools/android/incremental_install_test.py b/tools/android/incremental_install_test.py
new file mode 100644
index 0000000..b8ea64f
--- /dev/null
+++ b/tools/android/incremental_install_test.py
@@ -0,0 +1,433 @@
+# Copyright 2015 Google Inc. 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._error = None
+    self.package_timestamp = None
+    self._last_package_timestamp = 0
+    self.shell_cmdlns = []
+
+  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]) as f:
+        content = f.read()
+      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, "w") as f:
+          f.write(content)
+      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 == "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")):
+        pass
+      elif shell_cmdln.startswith("dumpsys package "):
+        return self._CreatePopenMock(
+            0,
+            "lastUpdateTime=%s" % self.package_timestamp,
+            "")
+      elif shell_cmdln.startswith("rm"):
+        file_path = shell_cmdln.split()[2]
+        self.files.pop(file_path, None)
+      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)
+
+
+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, "w") as f:
+      f.write("\n".join([self._OLD_APP_PACKGE, self._APP_PACKAGE]))
+
+    # Write the local resource apk file.
+    with open(self._RESOURCE_APK, "w") as f:
+      f.write("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, "w") as f:
+      f.write(content)
+    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 _CallIncrementalInstall(self, incremental, start_app=False):
+    if 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,
+        output_marker=self._OUTPUT_MARKER,
+        adb_jobs=1,
+        start_app=start_app,
+        user_home_dir="/home/root")
+
+  def testUploadToPristineDevice(self):
+
+    self._CreateZip()
+
+    with open("dex1", "w") as f:
+      f.write("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.assertEquals(manifest, self._GetDeviceFile("dex/manifest"))
+    self.assertEquals("content1", self._GetDeviceFile("dex/ip1"))
+    self.assertEquals("content2", self._GetDeviceFile("dex/ip2"))
+    self.assertEquals("content3", self._GetDeviceFile("dex/ip3"))
+    self.assertEquals("resource apk", self._GetDeviceFile("resources.ap_"))
+
+  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.assertEquals("old content1", self._GetDeviceFile("dex/ip1"))
+    self.assertEquals("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.assertEquals("content1", self._GetDeviceFile("dex/ip1"))
+    self.assertEquals("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.assertEquals("content1", self._GetDeviceFile("dex/ip1"))
+    self.assertEquals("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", "0")
+    self._mock_adb.package_timestamp = "0"
+
+    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.assertEquals("content1", self._GetDeviceFile("dex/ip1"))
+    self.assertEquals("content2", self._GetDeviceFile("dex/ip2"))
+    self.assertEquals("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.assertEquals("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.assertEquals("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:
+      pass
+
+  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:
+      pass
+
+  def testInstallFailure(self):
+    self._mock_adb.SetError(0, "Failure", "", 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:
+      pass
+
+  def testStartApp(self):
+    # Based on testUploadToPristineDevice
+    self._CreateZip()
+
+    with open("dex1", "w") as f:
+      f.write("content3")
+
+    self._CreateLocalManifest(
+        "zip1 zp1 ip1 0",
+        "zip1 zp2 ip2 0",
+        "dex1 - ip3 0")
+
+    self._CallIncrementalInstall(incremental=False, start_app=True)
+
+    self.assertTrue(("monkey -p %s -c android.intent.category.LAUNCHER 1" %
+                     self._APP_PACKAGE) 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(255, "", error)
+      try:
+        self._CallIncrementalInstall(incremental=True)
+        self.fail("Should have quit if there were multiple devices.")
+      except SystemExit:
+        pass
+
+  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
+
+if __name__ == "__main__":
+  unittest.main()