From 379a7f4f5a0491964c7896834f3f326412888585 Mon Sep 17 00:00:00 2001
From: Matt Clay <matt@mystile.com>
Date: Tue, 18 Sep 2018 08:37:14 -0700
Subject: [PATCH] Fix ansible-test unit test execution. (#45772)

* Fix ansible-test units requirements install.
* Run unit tests as unprivileged user under Docker.
---
 test/runner/lib/config.py     |  7 +++++++
 test/runner/lib/delegation.py | 23 +++++++++++++++++++++++
 test/runner/lib/executor.py   | 19 +++++++++++++++----
 test/runner/test.py           |  4 ++++
 tox.ini                       |  1 +
 5 files changed, 50 insertions(+), 4 deletions(-)

diff --git a/test/runner/lib/config.py b/test/runner/lib/config.py
index 0cc34f21d34..eeb179cc398 100644
--- a/test/runner/lib/config.py
+++ b/test/runner/lib/config.py
@@ -237,6 +237,13 @@ class UnitsConfig(TestConfig):
 
         self.collect_only = args.collect_only  # type: bool
 
+        self.requirements_mode = args.requirements_mode if 'requirements_mode' in args else ''
+
+        if self.requirements_mode == 'only':
+            self.requirements = True
+        elif self.requirements_mode == 'skip':
+            self.requirements = False
+
 
 class CoverageConfig(EnvironmentConfig):
     """Configuration for the coverage command."""
diff --git a/test/runner/lib/delegation.py b/test/runner/lib/delegation.py
index b75bb97bb60..8392fc59a79 100644
--- a/test/runner/lib/delegation.py
+++ b/test/runner/lib/delegation.py
@@ -275,6 +275,29 @@ def delegate_docker(args, exclude, require, integration_targets):
             if isinstance(args, UnitsConfig) and not args.python:
                 cmd += ['--python', 'default']
 
+            # run unit tests unprivileged to prevent stray writes to the source tree
+            if isinstance(args, UnitsConfig):
+                writable_dirs = [
+                    '/root/ansible/lib/ansible.egg-info',
+                    '/root/ansible/.pytest_cache',
+                ]
+
+                docker_exec(args, test_id, ['mkdir', '-p'] + writable_dirs)
+                docker_exec(args, test_id, ['chmod', '777'] + writable_dirs)
+
+                docker_exec(args, test_id, ['find', '/root/ansible/test/results/', '-type', 'd', '-exec', 'chmod', '777', '{}', '+'])
+
+                docker_exec(args, test_id, ['chmod', '755', '/root'])
+                docker_exec(args, test_id, ['chmod', '644', '/root/ansible/%s' % args.metadata_path])
+
+                docker_exec(args, test_id, ['useradd', 'pytest', '--create-home'])
+
+                docker_exec(args, test_id, cmd + ['--requirements-mode', 'only'], options=cmd_options)
+
+                cmd += ['--requirements-mode', 'skip']
+
+                cmd_options += ['--user', 'pytest']
+
             try:
                 docker_exec(args, test_id, cmd, options=cmd_options)
             finally:
diff --git a/test/runner/lib/executor.py b/test/runner/lib/executor.py
index 7a01081316e..ded10d0e959 100644
--- a/test/runner/lib/executor.py
+++ b/test/runner/lib/executor.py
@@ -12,6 +12,7 @@ import time
 import textwrap
 import functools
 import pipes
+import sys
 import hashlib
 
 import lib.pytar
@@ -49,6 +50,8 @@ from lib.util import (
     raw_command,
     get_coverage_path,
     get_available_port,
+    generate_pip_command,
+    find_python,
 )
 
 from lib.docker_util import (
@@ -148,9 +151,10 @@ def create_shell_command(command):
     return cmd
 
 
-def install_command_requirements(args):
+def install_command_requirements(args, python_version=None):
     """
     :type args: EnvironmentConfig
+    :type python_version: str | None
     """
     generate_egg_info(args)
 
@@ -168,7 +172,10 @@ def install_command_requirements(args):
         if args.junit:
             packages.append('junit-xml')
 
-    pip = args.pip_command
+    if not python_version:
+        python_version = args.python_version
+
+    pip = generate_pip_command(find_python(python_version))
 
     commands = [generate_pip_install(pip, args.command, packages=packages)]
 
@@ -1133,8 +1140,6 @@ def command_units(args):
     if args.delegate:
         raise Delegate(require=changes)
 
-    install_command_requirements(args)
-
     version_commands = []
 
     for version in SUPPORTED_PYTHON_VERSIONS:
@@ -1142,6 +1147,9 @@ def command_units(args):
         if args.python and version != args.python_version:
             continue
 
+        if args.requirements_mode != 'skip':
+            install_command_requirements(args, version)
+
         env = ansible_environment(args)
 
         cmd = [
@@ -1167,6 +1175,9 @@ def command_units(args):
 
         version_commands.append((version, cmd, env))
 
+    if args.requirements_mode == 'only':
+        sys.exit()
+
     for version, command, env in version_commands:
         display.info('Unit test with Python %s' % version)
 
diff --git a/test/runner/test.py b/test/runner/test.py
index b8e4d1f87b7..1529dd3ee1e 100755
--- a/test/runner/test.py
+++ b/test/runner/test.py
@@ -335,6 +335,10 @@ def parse_args():
                        action='store_true',
                        help='collect tests but do not execute them')
 
+    units.add_argument('--requirements-mode',
+                       choices=('only', 'skip'),
+                       help=argparse.SUPPRESS)
+
     add_extra_docker_options(units, integration=False)
 
     sanity = subparsers.add_parser('sanity',
diff --git a/tox.ini b/tox.ini
index f460eb31394..33522366c38 100644
--- a/tox.ini
+++ b/tox.ini
@@ -21,6 +21,7 @@ passenv =
 
 [pytest]
 xfail_strict = true
+cache_dir = .pytest_cache
 
 [flake8]
 # These are things that the devs don't agree make the code more readable