Initial commit for acts_tests test suite.

For the first step to decouple the ACTS test suite from the framework,
create a new acts_tests directory that holds all of the test cases and
user-contributed test utils.

The acts_tests test suite includes its own setup.py script that
installs the ACTS framework as well as any additional utils in
acts_contrib.

Note for reviewers: The only file to review is acts_tests/setup.py
All other files are mirrored from their original locations via symlink.

Bug: 151355642
Test: local setup, ACTS test runs
Change-Id: I3c4dca0606fa13d63741ce12adf75d053f5090d7
diff --git a/acts_tests/setup.py b/acts_tests/setup.py
new file mode 100755
index 0000000..a92d86f
--- /dev/null
+++ b/acts_tests/setup.py
@@ -0,0 +1,189 @@
+#!/usr/bin/env python3
+#
+# Copyright 2017 - The Android Open Source Project
+#
+# 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.
+
+import logging
+import os
+import shutil
+import subprocess
+import sys
+from distutils import cmd
+from distutils import log
+
+import setuptools
+from setuptools.command.install import install
+
+FRAMEWORK_DIR = 'acts_framework'
+
+acts_tests_dir = os.path.abspath(os.path.dirname(__file__))
+
+install_requires = [
+    # Future needs to have a newer version that contains urllib.
+    'future>=0.16.0',
+    # Latest version of mock (4.0.0b) causes a number of compatibility issues
+    # with ACTS unit tests (b/148695846, b/148814743)
+    'mock==3.0.5',
+    'numpy',
+    'pyserial',
+    'pyyaml>=5.1',
+    'protobuf>=3.11.3',
+    'requests',
+    'scapy',
+    'xlsxwriter',
+    'mobly>=1.10.0',
+]
+
+if sys.version_info < (3, ):
+    install_requires.append('enum34')
+    install_requires.append('statistics')
+    # "futures" is needed for py2 compatibility and it only works in 2.7
+    install_requires.append('futures')
+    install_requires.append('subprocess32')
+
+
+class ActsContribInstall(install):
+    """Custom installation of the acts_contrib package.
+
+    First installs the required ACTS framework via its own setup.py script,
+    before proceeding with the rest of the installation.
+
+    The installation requires the ACTS framework to exist under the
+    "acts_framework" directory.
+    """
+    def run(self):
+        acts_framework_dir = os.path.join(acts_tests_dir, FRAMEWORK_DIR)
+        if not os.path.isdir(acts_framework_dir):
+            logging.error('Cannot install ACTS framework. Framework dir '
+                          '"%s" not found' % acts_framework_dir)
+            exit(1)
+        acts_setup_bin = os.path.join(acts_framework_dir, 'setup.py')
+        if not os.path.isfile(acts_setup_bin):
+            logging.error('Cannot install ACTS framework. Setup script not '
+                          'found.')
+            exit(1)
+        command = [sys.executable, acts_setup_bin, 'install']
+        subprocess.check_call(command, cwd=acts_framework_dir)
+        super().run()
+
+
+class ActsContribInstallDependencies(cmd.Command):
+    """Installs only required packages
+
+    Installs all required packages for acts_contrib to work. Rather than using
+    the normal install system which creates links with the python egg, pip is
+    used to install the packages.
+    """
+
+    description = 'Install dependencies needed for acts_contrib packages.'
+    user_options = []
+
+    def initialize_options(self):
+        pass
+
+    def finalize_options(self):
+        pass
+
+    def run(self):
+        install_args = [sys.executable, '-m', 'pip', 'install']
+        subprocess.check_call(install_args + ['--upgrade', 'pip'])
+        required_packages = self.distribution.install_requires
+
+        for package in required_packages:
+            self.announce('Installing %s...' % package, log.INFO)
+            subprocess.check_call(install_args +
+                                  ['-v', '--no-cache-dir', package])
+
+        self.announce('Dependencies installed.')
+
+
+class ActsContribUninstall(cmd.Command):
+    """acts_contrib uninstaller.
+
+    Uninstalls acts_contrib from the current version of python. This will
+    attempt to import acts_contrib from any of the python egg locations. If it
+    finds an import it will use the modules file location to delete it. This is
+    repeated until acts_contrib can no longer be imported and thus is
+    uninstalled.
+    """
+
+    description = 'Uninstall acts_contrib from the local machine.'
+    user_options = []
+
+    def initialize_options(self):
+        pass
+
+    def finalize_options(self):
+        pass
+
+    def uninstall_acts_contrib_module(self, acts_contrib_module):
+        """Uninstalls acts_contrib from a module.
+
+        Args:
+            acts_contrib_module: The acts_contrib module to uninstall.
+        """
+        for acts_contrib_install_dir in acts_contrib_module.__path__:
+            self.announce('Deleting acts_contrib from: %s'
+                          % acts_contrib_install_dir, log.INFO)
+            shutil.rmtree(acts_contrib_install_dir)
+
+    def run(self):
+        """Entry point for the uninstaller."""
+        # Remove the working directory from the python path. This ensures that
+        # Source code is not accidentally targeted.
+        our_dir = os.path.abspath(os.path.dirname(__file__))
+        if our_dir in sys.path:
+            sys.path.remove(our_dir)
+
+        if os.getcwd() in sys.path:
+            sys.path.remove(os.getcwd())
+
+        try:
+            import acts_contrib as acts_contrib_module
+        except ImportError:
+            self.announce('acts_contrib is not installed, nothing to uninstall.',
+                          level=log.ERROR)
+            return
+
+        while acts_contrib_module:
+            self.uninstall_acts_contrib_module(acts_contrib_module)
+            try:
+                del sys.modules['acts_contrib']
+                import acts_contrib as acts_contrib_module
+            except ImportError:
+                acts_contrib_module = None
+
+        self.announce('Finished uninstalling acts_contrib.')
+
+
+def main():
+    os.chdir(acts_tests_dir)
+    packages = setuptools.find_packages(include=('acts_contrib*',))
+    setuptools.setup(name='acts_contrib',
+                     version='0.9',
+                     description='Android Comms Test Suite',
+                     license='Apache2.0',
+                     packages=packages,
+                     include_package_data=False,
+                     install_requires=install_requires,
+                     cmdclass={
+                         'install': ActsContribInstall,
+                         'install_deps': ActsContribInstallDependencies,
+                         'uninstall': ActsContribUninstall
+                     },
+                     url="http://www.android.com/")
+
+
+if __name__ == '__main__':
+    main()