arc: add anroid-installer

Design Doc: go/ptdnbss-new-design
These script will ba used by push_to_device, board_specific_setup and
chromeos-base/android-{container,vm}-* ebuilds.

BUG=b:147893231, b:123380475
TEST=python3 ./android_installer_unittest.py
TEST=cros lint --py3 *.py

Cq-Depend: chrome-internal:3174029
Change-Id: I99ba6366c37fb1aee756fcb86ab9b489cad10a01
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/platform2/+/2306850
Tested-by: Boleyn Su <boleynsu@chromium.org>
Reviewed-by: Hidehiko Abe <hidehiko@chromium.org>
Commit-Queue: Boleyn Su <boleynsu@chromium.org>
Auto-Submit: Boleyn Su <boleynsu@chromium.org>
diff --git a/arc/android-installer/android_installer.py b/arc/android-installer/android_installer.py
new file mode 100755
index 0000000..1e3584a
--- /dev/null
+++ b/arc/android-installer/android_installer.py
@@ -0,0 +1,154 @@
+#!/usr/bin/env python3
+# Copyright 2020 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""Android Installer
+
+This script will be called by push_to_device, chromeos-base/android-{container,
+vm}-* ebuilds and board_specific_setup.
+"""
+
+from __future__ import print_function
+
+import argparse
+import enum
+import sys
+from typing import Dict, List
+
+
+class AndroidInstallerCaller(enum.Enum):
+  """Android Installer Caller"""
+  PUSH_TO_DEVICE = 0
+  EBUILD_SRC_COMPILE = 1
+  EBUILD_SRC_INSTALL = 2
+  EBUILD_SRC_TEST = 3
+  BOARD_SPECIFIC_SETUP = 4
+  BOARD_SPECIFIC_SETUP_TEST = 5
+
+  @staticmethod
+  def allowed_callers() -> List[str]:
+    return [name.lower() for name in
+            AndroidInstallerCaller.__members__.keys()]
+
+  @staticmethod
+  # TODO(boleynsu): annotate the return type once we are using python>3.6
+  def from_str(caller: str):
+    return AndroidInstallerCaller[caller.upper()]
+
+
+
+class AndroidInstaller:
+  """Android Installer"""
+
+  # env variables used by Android Installer
+  env: Dict[str, str]
+  # use flags used by Android Installer
+  use: Dict[str, bool]
+  # caller of Android Installer
+  caller: AndroidInstallerCaller
+
+  def __init__(self, argv: List[str]) -> None:
+    """Parse the arguments."""
+
+    parser = argparse.ArgumentParser(
+        usage=
+        """
+        Example:
+          %(prog)s --env=ROOT=/build/rammus-arc-r --use=-cheets_local_img \\
+            --caller push_to_device
+
+          %(prog)s --env ROOT=/build/rammus-arc-r --env PV=9999 \\
+            --env KEY_WITH_NO_VALUE= --use +cheets_local_img \\
+            --use +chromeos-base/chromeos-cheets:android-container-pi \\
+            --caller ebuild_src_compile
+        Note:
+          The dash used in --use is special so we must use --use=-use_flag
+          instead of --use -use_flag
+        """
+    )
+    parser.add_argument('--env', action='append',
+                        help='the format is KEY=VALUE')
+    parser.add_argument('--use', action='append',
+                        help='the format is +use_flag or -use_flag')
+    parser.add_argument(
+        '--caller', choices=AndroidInstallerCaller.allowed_callers(),
+        help='the caller of this script')
+
+    args = parser.parse_args(args=argv)
+
+    self.env = dict()
+    if args.env:
+      for e in args.env:
+        kv = e.split('=', 1)
+        if len(kv) < 2:
+          raise ValueError(
+              'Invalid --env %s argument.' % e +
+              ' = is missing. For a key with no value, please use --env KEY=')
+        key, value = kv
+        if not key:
+          raise ValueError(
+              'Invalid --env %s argument.' % e +
+              ' The key should not be empty.')
+        # The later argument will overwrite the former one.
+        self.env[key] = value
+
+    self.use = dict()
+    if args.use:
+      for u in args.use:
+        key = u[1:]
+        if u[0] == '+':
+          value = True
+        elif u[0] == '-':
+          value = False
+        else:
+          raise ValueError(
+              'Invalid --use %s argument.' % u +
+              ' The first character should be + or -.')
+        # The later argument will overwrite the former one.
+        self.use[key] = value
+
+    if not args.caller:
+      raise ValueError('--caller must be specified')
+    self.caller = AndroidInstallerCaller.from_str(args.caller)
+
+  def main(self) -> None:
+    if self.caller in [AndroidInstallerCaller.PUSH_TO_DEVICE,
+                       AndroidInstallerCaller.EBUILD_SRC_COMPILE]:
+      self.ebuild_src_compile()
+    if self.caller in [AndroidInstallerCaller.PUSH_TO_DEVICE,
+                       AndroidInstallerCaller.EBUILD_SRC_TEST]:
+      self.ebuild_src_test()
+    if self.caller in [AndroidInstallerCaller.PUSH_TO_DEVICE,
+                       AndroidInstallerCaller.EBUILD_SRC_INSTALL]:
+      self.ebuild_src_install()
+    if self.caller in [AndroidInstallerCaller.PUSH_TO_DEVICE,
+                       AndroidInstallerCaller.BOARD_SPECIFIC_SETUP]:
+      self.board_specific_setup()
+    if self.caller in [AndroidInstallerCaller.PUSH_TO_DEVICE,
+                       AndroidInstallerCaller.BOARD_SPECIFIC_SETUP_TEST]:
+      self.board_specific_setup_test()
+
+  def ebuild_src_compile(self) -> None:
+    # TODO(boleynsu): implement this
+    raise NotImplementedError()
+
+  def ebuild_src_test(self) -> None:
+    # TODO(boleynsu): implement this
+    raise NotImplementedError()
+
+  def ebuild_src_install(self) -> None:
+    # TODO(boleynsu): implement this
+    raise NotImplementedError()
+
+  def board_specific_setup(self) -> None:
+    # TODO(boleynsu): implement this
+    raise NotImplementedError()
+
+  def board_specific_setup_test(self) -> None:
+    # TODO(boleynsu): implement this
+    raise NotImplementedError()
+
+
+if __name__ == '__main__':
+  AndroidInstaller(sys.argv[1:]).main()
diff --git a/arc/android-installer/android_installer_unittest.py b/arc/android-installer/android_installer_unittest.py
new file mode 100644
index 0000000..29df107
--- /dev/null
+++ b/arc/android-installer/android_installer_unittest.py
@@ -0,0 +1,101 @@
+# Copyright 2020 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""Unit tests for android_installer.py"""
+
+import unittest
+import unittest.mock
+
+import android_installer
+
+
+class AndroidInstallerCallerTest(unittest.TestCase):
+  """Unit tests for AndroidInstallerCaller"""
+
+  def test_from_str(self):
+    self.assertRaises(KeyError,
+                      lambda: android_installer.AndroidInstallerCaller.from_str(
+                          'invalid_caller'))
+
+
+class AndroidInstallerTest(unittest.TestCase):
+  """Unit tests for AndroidInstaller"""
+
+  def test_env(self):
+    # Test --env with '=' in its value
+    self.assertEqual(android_installer.AndroidInstaller(
+        ['--env', 'a=b=c', '--caller', 'ebuild_src_compile']).env, {'a': 'b=c'})
+    # Test --env with no value
+    self.assertEqual(android_installer.AndroidInstaller(
+        ['--env', 'a=', '--caller', 'ebuild_src_compile']).env, {'a': ''})
+    # Test --env with no =
+    self.assertRaises(ValueError, lambda: android_installer.AndroidInstaller(
+        ['--env', 'a', '--caller', 'ebuild_src_compile']))
+    # Test --env with empty key
+    self.assertRaises(ValueError, lambda: android_installer.AndroidInstaller(
+        ['--env', '=a', '--caller', 'ebuild_src_compile']))
+    # Test that the later argument will overwrite the former one.
+    self.assertEqual(android_installer.AndroidInstaller(
+        ['--env', 'a=1', '--env', 'a=2', '--caller', 'ebuild_src_compile']).env,
+                     {'a': '2'})
+    # Test when no --env is given.
+    self.assertEqual(android_installer.AndroidInstaller(
+        ['--caller', 'ebuild_src_compile']).env, {})
+
+
+  def test_use(self):
+    # Test enabling use flag
+    self.assertEqual(android_installer.AndroidInstaller(
+        ['--use', '+x', '--caller', 'ebuild_src_compile']).use, {'x': True})
+    # Test disabling use flag
+    self.assertEqual(android_installer.AndroidInstaller(
+        ['--use=-x', '--caller', 'ebuild_src_compile']).use, {'x': False})
+    # Test invalid use flag
+    self.assertRaises(ValueError, lambda: android_installer.AndroidInstaller(
+        ['--use', '+x', '--use=*x', '--caller', 'ebuild_src_compile']))
+    # Test that the later argument will overwrite the former one.
+    self.assertEqual(android_installer.AndroidInstaller(
+        ['--use', '+x', '--use=-x', '--caller', 'ebuild_src_compile']).use,
+                     {'x': False})
+    # Test when no use flags are given.
+    self.assertEqual(android_installer.AndroidInstaller(
+        ['--caller', 'ebuild_src_compile']).use, {})
+
+  def test_caller(self):
+    # Test --caller ebuild_src_compile
+    self.assertEqual(
+        android_installer.AndroidInstaller(
+            ['--caller', 'ebuild_src_compile']).caller,
+        android_installer.AndroidInstallerCaller.EBUILD_SRC_COMPILE)
+    # Test invalid caller
+    self.assertRaises(SystemExit, lambda: android_installer.AndroidInstaller(
+        ['--caller', 'invalid_caller']))
+    # Test when caller is not specified
+    self.assertRaises(ValueError, lambda: android_installer.AndroidInstaller(
+        []))
+
+  def test_main(self):
+    all_fn = ['ebuild_src_compile', 'ebuild_src_install', 'ebuild_src_test',
+              'board_specific_setup', 'board_specific_setup_test']
+
+    def test_called(caller, called_fn):
+      mock = unittest.mock.Mock(android_installer.AndroidInstaller)
+      mock.caller = android_installer.AndroidInstallerCaller.from_str(caller)
+      android_installer.AndroidInstaller.main(mock)
+      for fn in all_fn:
+        if fn in called_fn:
+          mock.__getattr__(fn).assert_called_once()
+        else:
+          mock.__getattr__(fn).assert_not_called()
+
+    for caller in android_installer.AndroidInstallerCaller.allowed_callers():
+      if caller != 'push_to_device':
+        self.assertIn(caller, all_fn)
+        test_called(caller, [caller])
+
+    test_called('push_to_device', all_fn)
+
+
+if __name__ == '__main__':
+  unittest.main()