From b8cb3f519be272ca810f8186820205723a209d38 Mon Sep 17 00:00:00 2001 From: Matt Clay Date: Thu, 11 May 2017 19:05:21 +0800 Subject: [PATCH] Detect and fix environment tampering in tests. --- .../targets/setup_mysql_db/tasks/main.yml | 19 +++- .../setup_mysql_db/vars/Ubuntu-16-py3.yml | 6 ++ test/runner/lib/executor.py | 88 +++++++++++++++++++ 3 files changed, 109 insertions(+), 4 deletions(-) create mode 100644 test/integration/targets/setup_mysql_db/vars/Ubuntu-16-py3.yml diff --git a/test/integration/targets/setup_mysql_db/tasks/main.yml b/test/integration/targets/setup_mysql_db/tasks/main.yml index fc986f4d4d1..be605816d7f 100644 --- a/test/integration/targets/setup_mysql_db/tasks/main.yml +++ b/test/integration/targets/setup_mysql_db/tasks/main.yml @@ -17,13 +17,24 @@ # along with Ansible. If not, see . # ============================================================ +- name: python 2 + set_fact: + python_suffix: "" + when: ansible_python_version | version_compare('3', '<') + +- name: python 3 + set_fact: + python_suffix: "-py3" + when: ansible_python_version | version_compare('3', '>=') + - include_vars: '{{ item }}' with_first_found: - files: - - '{{ ansible_distribution }}-{{ ansible_distribution_major_version }}.yml' - - '{{ ansible_os_family }}-{{ ansible_distribution_major_version }}.yml' - - '{{ ansible_distribution }}.yml' - - '{{ ansible_os_family }}.yml' + - '{{ ansible_distribution }}-{{ ansible_distribution_major_version }}{{ python_suffix }}.yml' + - '{{ ansible_os_family }}-{{ ansible_distribution_major_version }}{{ python_suffix }}.yml' + - '{{ ansible_distribution }}{{ python_suffix }}.yml' + - '{{ ansible_os_family }}{{ python_suffix }}.yml' + - 'default{{ python_suffix }}.yml' paths: '../vars' - name: install mysqldb_test rpm dependencies diff --git a/test/integration/targets/setup_mysql_db/vars/Ubuntu-16-py3.yml b/test/integration/targets/setup_mysql_db/vars/Ubuntu-16-py3.yml new file mode 100644 index 00000000000..bffbf588956 --- /dev/null +++ b/test/integration/targets/setup_mysql_db/vars/Ubuntu-16-py3.yml @@ -0,0 +1,6 @@ +mysql_service: 'mysql' + +mysql_packages: + - mysql-server + - python3-mysqldb + - bzip2 diff --git a/test/runner/lib/executor.py b/test/runner/lib/executor.py index e7148a0ce8e..e033eef1f26 100644 --- a/test/runner/lib/executor.py +++ b/test/runner/lib/executor.py @@ -2,6 +2,7 @@ from __future__ import absolute_import, print_function +import json import os import re import tempfile @@ -48,6 +49,8 @@ from lib.util import ( make_dirs, is_shippable, is_binary_file, + find_executable, + raw_command, ) from lib.test import ( @@ -518,6 +521,10 @@ def command_integration_filtered(args, targets): cloud_environment = get_cloud_environment(args, target) + original_environment = EnvironmentDescription() + + display.info('>>> Environment Description\n%s' % original_environment, verbosity=3) + try: while tries: tries -= 1 @@ -538,11 +545,16 @@ def command_integration_filtered(args, targets): if cloud_environment: cloud_environment.on_failure(target, tries) + if not original_environment.validate(target.name, throw=False): + raise + if not tries: raise display.warning('Retrying test target "%s" with maximum verbosity.' % target.name) display.verbosity = args.verbosity = 6 + + original_environment.validate(target.name, throw=True) except: display.notice('To resume at this test target, use the option: --start-at %s' % target.name) @@ -1135,6 +1147,82 @@ def get_integration_remote_filter(args, targets): return exclude +class EnvironmentDescription(object): + """Description of current running environment.""" + def __init__(self): + """Initialize snapshot of environment configuration.""" + versions = [''] + versions += SUPPORTED_PYTHON_VERSIONS + versions += list(set(v.split('.')[0] for v in SUPPORTED_PYTHON_VERSIONS)) + + python_paths = dict((v, find_executable('python%s' % v, required=False)) for v in sorted(versions)) + python_versions = dict((v, self.get_version([python_paths[v], '-V'])) for v in sorted(python_paths) if python_paths[v]) + + pip_paths = dict((v, find_executable('pip%s' % v, required=False)) for v in sorted(versions)) + pip_versions = dict((v, self.get_version([pip_paths[v], '--version'])) for v in sorted(pip_paths) if pip_paths[v]) + pip_interpreters = dict((v, self.get_shebang(pip_paths[v])) for v in sorted(pip_paths) if pip_paths[v]) + + self.data = dict( + python_paths=python_paths, + python_versions=python_versions, + pip_paths=pip_paths, + pip_versions=pip_versions, + pip_interpreters=pip_interpreters, + ) + + def __str__(self): + """ + :rtype: str + """ + return json.dumps(self.data, sort_keys=True, indent=4) + + def validate(self, target_name, throw): + """ + :type target_name: str + :type throw: bool + :rtype: bool + """ + current = EnvironmentDescription() + + original_json = str(self) + current_json = str(current) + + if original_json == current_json: + return True + + message = ('Test target "%s" has changed the test environment!\n' + 'If these changes are necessary, they must be reverted before the test finishes.\n' + '>>> Original Environment\n' + '%s\n' + '>>> Current Environment\n' + '%s' % (target_name, original_json, current_json)) + + if throw: + raise ApplicationError(message) + + display.error(message) + + return False + + @staticmethod + def get_version(command): + """ + :type command: list[str] + :rtype: str + """ + stdout, stderr = raw_command(command, capture=True) + return (stdout or '').strip() + (stderr or '').strip() + + @staticmethod + def get_shebang(path): + """ + :type path: str + :rtype: str + """ + with open(path) as script_fd: + return script_fd.readline() + + class NoChangesDetected(ApplicationWarning): """Exception when change detection was performed, but no changes were found.""" def __init__(self):