From 324a2c389c4f137d9d76c116d40feb5c85f97074 Mon Sep 17 00:00:00 2001 From: Danny Hermes Date: Mon, 3 Oct 2016 12:57:15 -0700 Subject: [PATCH] Running restricted set of unit tests on Travis PR. --- scripts/run_unit_tests.py | 53 +++++++++++++++++--------- scripts/script_utils.py | 78 ++++++++++++++++++++++++++++++++++++++- tox.ini | 1 + 3 files changed, 113 insertions(+), 19 deletions(-) diff --git a/scripts/run_unit_tests.py b/scripts/run_unit_tests.py index 5be8fdbf6d70..4254f2f8e6b9 100644 --- a/scripts/run_unit_tests.py +++ b/scripts/run_unit_tests.py @@ -26,6 +26,12 @@ import subprocess import sys +from script_utils import check_output +from script_utils import get_changed_packages +from script_utils import in_travis +from script_utils import in_travis_pr +from script_utils import travis_branch + PROJECT_ROOT = os.path.abspath( os.path.join(os.path.dirname(__file__), '..')) @@ -44,24 +50,6 @@ UNSET_SENTINEL = object() # Sentinel for argparser -def check_output(*args): - """Run a command on the operation system. - - :type args: tuple - :param args: Keyword arguments to pass to ``subprocess.check_output``. - - :rtype: str - :returns: The raw STDOUT from the command (converted from bytes - if necessary). - """ - cmd_output = subprocess.check_output(args) - # On Python 3, this returns bytes (from STDOUT), so we - # convert to a string. - cmd_output = cmd_output.decode('utf-8') - # Also strip the output since it usually has a trailing newline. - return cmd_output.strip() - - def get_package_directories(): """Get a list of directories containing sub-packages. @@ -83,6 +71,30 @@ def get_package_directories(): return result +def get_travis_directories(package_list): + """Get list of packages that need to be tested on Travis CI. + + See: https://travis-ci.com/ + + If the current Travis build is for a pull request (PR), this will + limit the directories to the ones impacted by the PR. Otherwise + it will just test all package directories. + + :type package_list: list + :param package_list: The list of **all** valid packages with unit tests. + + :rtype: list + :returns: A list of all package directories where tests + need to be run. + """ + if in_travis_pr(): + pr_against_branch = travis_branch() + return get_changed_packages('HEAD', pr_against_branch, + package_list) + else: + return package_list + + def verify_packages(subset, all_packages): """Verify that a subset of packages are among all packages. @@ -107,6 +119,9 @@ def get_test_packages(): Filters the package list in the following order: * Check command line for packages passed in as positional arguments + * Check if in Travis, then limit the subset based on changes + in a Pull Request ("push" builds to branches may not have + any filtering) * Just use all packages :rtype: list @@ -120,6 +135,8 @@ def get_test_packages(): if args.packages is not UNSET_SENTINEL: verify_packages(args.packages, all_packages) return sorted(args.packages) + elif in_travis(): + return get_travis_directories(all_packages) else: return all_packages diff --git a/scripts/script_utils.py b/scripts/script_utils.py index 160117ee0cd6..711d2bd54475 100644 --- a/scripts/script_utils.py +++ b/scripts/script_utils.py @@ -15,6 +15,7 @@ """Common helpers for testing scripts.""" import os +import subprocess LOCAL_REMOTE_ENV = 'GOOGLE_CLOUD_TESTING_REMOTE' @@ -81,5 +82,80 @@ def travis_branch(): :rtype: str :returns: The name of the branch the current pull request is changed against. + :raises: :class:`~exceptions.OSError` if the ``TRAVIS_BRANCH_ENV`` + environment variable isn't set during a pull request + build. """ - return os.getenv(TRAVIS_BRANCH_ENV) + try: + return os.environ[TRAVIS_BRANCH_ENV] + except KeyError: + msg = ('Pull request build does not have an ' + 'associated branch set (via %s)') % (TRAVIS_BRANCH_ENV,) + raise OSError(msg) + + +def check_output(*args): + """Run a command on the operation system. + + :type args: tuple + :param args: Arguments to pass to ``subprocess.check_output``. + + :rtype: str + :returns: The raw STDOUT from the command (converted from bytes + if necessary). + """ + cmd_output = subprocess.check_output(args) + # On Python 3, this returns bytes (from STDOUT), so we + # convert to a string. + cmd_output = cmd_output.decode('utf-8') + # Also strip the output since it usually has a trailing newline. + return cmd_output.strip() + + +def rootname(filename): + """Get the root directory that a file is contained in. + + :type filename: str + :param filename: The path / name of a file. + + :rtype: str + :returns: The root directory containing the file. + """ + if os.path.sep not in filename: + return '' + else: + file_root, _ = filename.split(os.path.sep, 1) + return file_root + + +def get_changed_packages(blob_name1, blob_name2, package_list): + """Get a list of packages which have changed between two changesets. + + :type blob_name1: str + :param blob_name1: The name of a commit hash or branch name or other + ``git`` artifact. + + :type blob_name2: str + :param blob_name2: The name of a commit hash or branch name or other + ``git`` artifact. + + :type package_list: list + :param package_list: The list of **all** valid packages with unit tests. + + :rtype: list + :returns: A list of all package directories that have changed + between ``blob_name1`` and ``blob_name2``. Starts + with a list of valid packages (``package_list``) + and filters out the unchanged directories. + """ + changed_files = check_output( + 'git', 'diff', '--name-only', blob_name1, blob_name2) + changed_files = changed_files.split('\n') + + result = set() + for filename in changed_files: + file_root = rootname(filename) + if file_root in package_list: + result.add(file_root) + + return sorted(result) diff --git a/tox.ini b/tox.ini index d165d7b7621e..c0783749c4c4 100644 --- a/tox.ini +++ b/tox.ini @@ -121,6 +121,7 @@ skip_install = py34: True py35: True isolated-cover: True +passenv = TRAVIS* [testenv:isolated-cover] commands =