From edc8c9674f5628224be13ebf1d65d62f991fe33e Mon Sep 17 00:00:00 2001 From: Alex Burke Date: Tue, 8 Oct 2024 10:12:55 +0200 Subject: [PATCH] Rework python version handling associated testconfig path handling. A previous revision fixed an immediate problem with test path handling but knowingly left some things on the table - in particular a split between container based handling of Python 2 and the local execution of Python 3 (meaning a potentially inconsistent version relative to what the project considers officially supported). Address this entirely: rework the default behaviour of `make test` to container based execution and use a consistent baseline Python 3. In order to continue to support rapid local iteration, provide a separate `make unittest` target which will execute the test suite locally and is also used as a mechanism to run other supporting tools. The clean use of containers necessitated various changes that make path arguments within the testconfig non-overlapping and better isolated. Adjust all paths generated within the test suite to be always from the base root instead of relative the output directory. Opt to patch the makeconfig generator rather than fiddle with generateconfs again. While here also add support for an environment variable overried that allows execution of the test suite against arbitrary python 3 versions. --- .github/workflows/ci.yml | 11 +-- Makefile | 54 +++++++---- .../{Dockerfile.python2 => Dockerfile.py2} | 0 envhelp/docker/Dockerfile.py3 | 8 ++ envhelp/docker/Dockerfile.pyver | 9 ++ envhelp/dpython | 88 +++++++++++++++++ envhelp/lpython | 46 +++++++++ envhelp/makeconfig.py | 29 +++--- envhelp/python2 | 43 +-------- envhelp/python3 | 20 +--- mig/unittest/testcore.py | 27 ++---- tests/__init__.py | 10 ++ tests/support/__init__.py | 95 ++++++++++++++----- tests/support/_env.py | 11 +++ tests/support/suppconst.py | 30 +++++- tests/test_booleans.py | 9 +- tests/test_mig_shared_functionality_cat.py | 7 +- tests/test_mig_unittest_testcore.py | 5 +- 18 files changed, 352 insertions(+), 150 deletions(-) rename envhelp/docker/{Dockerfile.python2 => Dockerfile.py2} (100%) create mode 100644 envhelp/docker/Dockerfile.py3 create mode 100644 envhelp/docker/Dockerfile.pyver create mode 100755 envhelp/dpython create mode 100755 envhelp/lpython create mode 100644 tests/support/_env.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f86bfba88..95c6e4b56 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,7 +35,7 @@ jobs: make dependencies - name: Run tests run: | - make test + make unittest python3-rocky9ish: runs-on: ubuntu-22.04 @@ -51,7 +51,7 @@ jobs: make dependencies - name: Run tests run: | - make test + make unittest python3-rocky8ish: runs-on: ubuntu-20.04 @@ -67,7 +67,7 @@ jobs: make dependencies - name: Run tests run: | - make test + make unittest python2-latest: runs-on: ubuntu-latest @@ -80,8 +80,7 @@ jobs: uses: actions/checkout@v4 - name: Setup environment run: | - pip install --no-cache-dir -r requirements.txt -r local-requirements.txt + make PYTHON_BIN=python PY=2 dependencies - name: Run tests run: | - PYTHON_BIN=python ./envhelp/makeconfig test --python2 - MIG_ENV='local' python -m unittest discover -s tests/ + make PYTHON_BIN=python PY=2 unittest diff --git a/Makefile b/Makefile index 7e39f2149..1863a9ffc 100644 --- a/Makefile +++ b/Makefile @@ -1,11 +1,21 @@ ifndef MIG_ENV MIG_ENV = 'local' endif -ifeq ($(PY),2) + +ifndef PY + PY = 3 +endif + +LOCAL_PYTHON_BIN = './envhelp/lpython' + +ifdef PYTHON_BIN + LOCAL_PYTHON_BIN = $(PYTHON_BIN) +else ifeq ($(PY),2) PYTHON_BIN = './envhelp/python2' else PYTHON_BIN = './envhelp/python3' endif + ifeq ($(ALLDEPS),1) REQS_PATH = ./recommended.txt else @@ -17,8 +27,9 @@ info: @echo @echo "The following should help you get started:" @echo - @echo "'make test' - run the test suite" - @echo "'make PY=2 test' - run the test suite (python 2)" + @echo "'make test' - run the test suite (default python 3)" + @echo "'make PY=2 test' - run the test suite (default python 2)" + @echo "'make unittest' - execute tests locally for development" .PHONY: fmt fmt: @@ -26,12 +37,13 @@ ifneq ($(MIG_ENV),'local') @echo "unavailable outside local development environment" @exit 1 endif - $(PYTHON_BIN) -m autopep8 --ignore E402 -i + $(LOCAL_PYTHON_BIN) -m autopep8 --ignore E402 -i .PHONY: clean clean: @rm -f ./envhelp/py2.imageid - @rm -f ./envhelp/py3.depends + @rm -f ./envhelp/py3.imageid + @rm -f ./envhelp/local.depends .PHONY: distclean distclean: clean @@ -44,37 +56,41 @@ distclean: clean test: dependencies testconfig @$(PYTHON_BIN) -m unittest discover -s tests/ +.PHONY: unittest +unittest: dependencies testconfig + @$(LOCAL_PYTHON_BIN) -m unittest discover -s tests/ + .PHONY: dependencies -dependencies: ./envhelp/venv/pyvenv.cfg ./envhelp/py3.depends +ifeq ($(PY),2) +dependencies: ./envhelp/local.depends +else +dependencies: ./envhelp/venv/pyvenv.cfg ./envhelp/local.depends +endif .PHONY: testconfig testconfig: ./envhelp/output/testconfs ./envhelp/output/testconfs: - @./envhelp/makeconfig test --python2 + @./envhelp/makeconfig test --docker @./envhelp/makeconfig test - @mkdir -p ./envhelp/output/certs - @mkdir -p ./envhelp/output/state - @mkdir -p ./envhelp/output/state/log ifeq ($(MIG_ENV),'local') -./envhelp/py3.depends: $(REQS_PATH) local-requirements.txt +./envhelp/local.depends: $(REQS_PATH) local-requirements.txt else -./envhelp/py3.depends: $(REQS_PATH) +./envhelp/local.depends: $(REQS_PATH) endif - @rm -f ./envhelp/py3.depends - @echo "upgrading venv pip as required for some dependencies" - @./envhelp/venv/bin/pip3 install --upgrade pip @echo "installing dependencies from $(REQS_PATH)" - @./envhelp/venv/bin/pip3 install -r $(REQS_PATH) + @$(LOCAL_PYTHON_BIN) -m pip install -r $(REQS_PATH) ifeq ($(MIG_ENV),'local') @echo "" @echo "installing development dependencies" - @./envhelp/venv/bin/pip3 install -r local-requirements.txt + @$(LOCAL_PYTHON_BIN) -m pip install -r local-requirements.txt endif - @touch ./envhelp/py3.depends + @touch ./envhelp/local.depends ./envhelp/venv/pyvenv.cfg: @echo "provisioning environment" @/usr/bin/env python3 -m venv ./envhelp/venv - @rm -f ./envhelp/py3.depends + @rm -f ./envhelp/local.depends + @echo "upgrading venv pip as required for some dependencies" + @./envhelp/venv/bin/pip3 install --upgrade pip diff --git a/envhelp/docker/Dockerfile.python2 b/envhelp/docker/Dockerfile.py2 similarity index 100% rename from envhelp/docker/Dockerfile.python2 rename to envhelp/docker/Dockerfile.py2 diff --git a/envhelp/docker/Dockerfile.py3 b/envhelp/docker/Dockerfile.py3 new file mode 100644 index 000000000..c0aeeb9d6 --- /dev/null +++ b/envhelp/docker/Dockerfile.py3 @@ -0,0 +1,8 @@ +FROM python:3.9 + +WORKDIR /usr/src/app + +COPY requirements.txt local-requirements.txt ./ +RUN pip install --no-cache-dir -r requirements.txt -r local-requirements.txt + +CMD [ "python", "--version" ] diff --git a/envhelp/docker/Dockerfile.pyver b/envhelp/docker/Dockerfile.pyver new file mode 100644 index 000000000..e90a17ef8 --- /dev/null +++ b/envhelp/docker/Dockerfile.pyver @@ -0,0 +1,9 @@ +ARG pyver +FROM python:${pyver} + +WORKDIR /usr/src/app + +COPY requirements.txt local-requirements.txt ./ +RUN pip install --no-cache-dir -r requirements.txt -r local-requirements.txt + +CMD [ "python", "--version" ] diff --git a/envhelp/dpython b/envhelp/dpython new file mode 100755 index 000000000..cedd5f778 --- /dev/null +++ b/envhelp/dpython @@ -0,0 +1,88 @@ +#!/bin/sh +# +# --- BEGIN_HEADER --- +# +# dpython - wrapper to invoke a containerised python +# Copyright (C) 2003-2024 The MiG Project by the Science HPC Center at UCPH +# +# This file is part of MiG. +# +# MiG is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# MiG is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, +# USA. +# +# --- END_HEADER --- +# + +set -e + +SCRIPT_PATH=$(realpath "$0") +SCRIPT_BASE=$(dirname -- "$SCRIPT_PATH") +MIG_BASE=$(realpath "$SCRIPT_BASE/..") + +if [ -n "${PY}" ]; then + PYVER="$PY" + PYTHON_SUFFIX="py$PY" + DOCKER_FILE_SUFFIX="$PYTHON_SUFFIX" +elif [ -n "${PYVER}" ]; then + PY=3 + PYTHON_SUFFIX="pyver-$PYVER" + DOCKER_FILE_SUFFIX="pyver" +else + echo "No python version specified - please supply a PY env var" + exit 1 +fi + +DOCKER_FILE="$SCRIPT_BASE/docker/Dockerfile.$DOCKER_FILE_SUFFIX" +DOCKER_IMAGEID_FILE="$SCRIPT_BASE/$PYTHON_SUFFIX.imageid" + +# NOTE: portable dynamic lookup with docker as default and fallback to podman +DOCKER_BIN=$(command -v docker || command -v podman || echo "") +if [ -z "${DOCKER_BIN}" ]; then + echo "No docker binary found - cannot use for python $PY tests" + exit 1 +fi + +# default PYTHONPATH such that directly executing files in the repo "just works" +# NOTE: this is hard-coded to the mount point used within the container +PYTHONPATH='/usr/src/app' + +# default any variables for container development +MIG_ENV=${MIG_ENV:-'docker'} + +# determine if the image has changed +echo -n "validating python $PY container.. " + +# load a previously written docker image id if present +IMAGEID_STORED=$(cat "$DOCKER_IMAGEID_FILE" 2>/dev/null || echo "") + +IMAGEID=$(${DOCKER_BIN} build -f "$DOCKER_FILE" . -q --build-arg "pyver=$PYVER") +if [ "$IMAGEID" != "$IMAGEID_STORED" ]; then + echo "rebuilt for changes" + + # reset the image id so the next call finds no changes + echo "$IMAGEID" > "$DOCKER_IMAGEID_FILE" +else + echo "no changes needed" +fi + +echo "using image id $IMAGEID" + +# execute python2 within the image passing the supplied arguments + +${DOCKER_BIN} run -it --rm \ + --mount "type=bind,source=$MIG_BASE,target=/usr/src/app" \ + --env "PYTHONPATH=$PYTHONPATH" \ + --env "MIG_ENV=$MIG_ENV" \ + "$IMAGEID" python$PY $@ diff --git a/envhelp/lpython b/envhelp/lpython new file mode 100755 index 000000000..9d42a83d3 --- /dev/null +++ b/envhelp/lpython @@ -0,0 +1,46 @@ +#!/bin/sh +# +# --- BEGIN_HEADER --- +# +# python3 - wrapper to invoke a local python3 virtual environment +# Copyright (C) 2003-2024 The MiG Project by the Science HPC Center at UCPH +# +# This file is part of MiG. +# +# MiG is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# MiG is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, +# USA. +# +# --- END_HEADER --- +# + +set -e + +SCRIPT_PATH=$(realpath "$0") +SCRIPT_BASE=$(dirname -- "$SCRIPT_PATH") +MIG_BASE=$(realpath "$SCRIPT_BASE/..") + +PYTHON_BIN=${PYTHON_BIN:-"$SCRIPT_BASE/venv/bin/python3"} +if [ ! -f "${PYTHON_BIN}" ]; then + echo "No python binary found - perhaps the virtual env was not created" + exit 1 +fi + +# default PYTHONPATH such that directly executing files in the repo "just works" +PYTHONPATH=${PYTHONPATH:-"$MIG_BASE"} + +# default any variables for local development +MIG_ENV=${MIG_ENV:-'local'} + +PYTHONPATH="$PYTHONPATH" MIG_ENV="$MIG_ENV" "$PYTHON_BIN" "$@" diff --git a/envhelp/makeconfig.py b/envhelp/makeconfig.py index 703b62e07..0d1941b83 100644 --- a/envhelp/makeconfig.py +++ b/envhelp/makeconfig.py @@ -38,8 +38,9 @@ from mig.shared.install import MIG_BASE, generate_confs -_LOCAL_ENVHELP_OUTPUT_DIR = os.path.realpath( - os.path.join(os.path.dirname(__file__), "output")) +_LOCAL_MIG_BASE = os.path.normpath( + os.path.join(os.path.dirname(__file__), "..")) +_LOCAL_ENVHELP_OUTPUT_DIR = os.path.join(_LOCAL_MIG_BASE, "envhelp/output") _MAKECONFIG_ALLOWED = ["local", "test"] @@ -51,21 +52,27 @@ def _at(sequence, index=-1, default=None): return default -def write_testconfig(env_name, is_py2=False): - confs_name = 'confs' if env_name == 'local' else '%sconfs' % (env_name,) - confs_suffix = 'py2' if is_py2 else 'py3' +def write_testconfig(env_name, is_docker=False): + is_predefined = env_name == 'test' + confs_name = '%sconfs' % (env_name,) + if is_predefined: + confs_suffix = 'docker' if is_docker else 'local' + else: + confs_suffix = 'py3' overrides = { 'destination': os.path.join(_LOCAL_ENVHELP_OUTPUT_DIR, confs_name), 'destination_suffix': "-%s" % (confs_suffix,), } - # determine the paths by which we will access the various configured dirs - if is_py2: + # determine the paths b which we will access the various configured dirs + # the tests output directory - when invoked within + + if is_predefined and is_docker: env_mig_base = '/usr/src/app' else: - env_mig_base = MIG_BASE - conf_dir_path = os.path.join(env_mig_base, "envhelp/output") + env_mig_base = _LOCAL_MIG_BASE + conf_dir_path = os.path.join(env_mig_base, "tests/output") overrides.update(**{ 'mig_code': os.path.join(conf_dir_path, 'mig'), @@ -85,7 +92,7 @@ def write_testconfig(env_name, is_py2=False): def main_(argv): env_name = _at(argv, index=1, default='') - arg_is_py2 = '--python2' in argv + arg_is_docker = '--docker' in argv if env_name == '': raise RuntimeError( @@ -94,7 +101,7 @@ def main_(argv): raise RuntimeError('environment must be one of %s' % (_MAKECONFIG_ALLOWED,)) - write_testconfig(env_name, is_py2=arg_is_py2) + write_testconfig(env_name, is_docker=arg_is_docker) def main(argv=sys.argv): diff --git a/envhelp/python2 b/envhelp/python2 index c0c0351a5..d0b0d04a8 100755 --- a/envhelp/python2 +++ b/envhelp/python2 @@ -29,46 +29,5 @@ set -e SCRIPT_PATH=$(realpath "$0") SCRIPT_BASE=$(dirname -- "$SCRIPT_PATH") -DOCKER_BASE="$SCRIPT_BASE/docker" -DOCKER_IMAGEID_FILE="$SCRIPT_BASE/py2.imageid" -# NOTE: portable dynamic lookup with docker as default and fallback to podman -DOCKER_BIN=$(command -v docker || command -v podman || echo "") -if [ -z "${DOCKER_BIN}" ]; then - echo "No docker binary found - cannot use for python2 tests" - exit 1 -fi - -# default PYTHONPATH such that directly executing files in the repo "just works" -# NOTE: this is hard-coded to the mount point used within the container -PYTHONPATH='/usr/app/src' - -# default any variables for local development -MIG_ENV=${MIG_ENV:-'local'} - -# determine if the image has changed -echo -n "validating container.. " - -# load a previously written docker image id if present -IMAGEID_STORED=$(cat "$DOCKER_IMAGEID_FILE" 2>/dev/null || echo "") - -IMAGEID=$(${DOCKER_BIN} build -f "$DOCKER_BASE/Dockerfile.python2" . -q) -if [ "$IMAGEID" != "$IMAGEID_STORED" ]; then - echo "rebuilt for changes" - - # reset the image id so the next call finds no changes - echo "$IMAGEID" > "$DOCKER_IMAGEID_FILE" -else - echo "no changes needed" -fi - -echo "running with MIG_ENV='$MIG_ENV' under python 2" -echo - -# execute python2 within the image passing the supplied arguments - -${DOCKER_BIN} run -it --rm \ - --mount type=bind,source=.,target=/usr/src/app \ - --env "PYTHONPATH=$PYTHON_PATH" \ - --env "MIG_ENV=$MIG_ENV" \ - "$IMAGEID" python2 "$@" +PY=2 $SCRIPT_BASE/dpython "$@" diff --git a/envhelp/python3 b/envhelp/python3 index 309356a03..9584a6dc5 100755 --- a/envhelp/python3 +++ b/envhelp/python3 @@ -2,7 +2,7 @@ # # --- BEGIN_HEADER --- # -# python3 - wrap python3 virtual environment for testing +# python2 - wrap python2 docker container for testing # Copyright (C) 2003-2024 The MiG Project by the Science HPC Center at UCPH # # This file is part of MiG. @@ -29,21 +29,5 @@ set -e SCRIPT_PATH=$(realpath "$0") SCRIPT_BASE=$(dirname -- "$SCRIPT_PATH") -MIG_BASE=$(realpath "$SCRIPT_BASE/..") -PYTHON3_BIN="$SCRIPT_BASE/venv/bin/python3" -if [ ! -f "${PYTHON3_BIN}" ]; then - echo "No python3 binary found - perhaps the virtual env was not created" - exit 1 -fi - -# default PYTHONPATH such that directly executing files in the repo "just works" -PYTHONPATH=${PYTHONPATH:-"$MIG_BASE"} - -# default any variables for local development -MIG_ENV=${MIG_ENV:-'local'} - -echo "running with MIG_ENV='$MIG_ENV' under python 3" -echo - -PYTHONPATH="$PYTHONPATH" MIG_ENV="$MIG_ENV" "$PYTHON3_BIN" "$@" +PY=3 $SCRIPT_BASE/dpython "$@" diff --git a/mig/unittest/testcore.py b/mig/unittest/testcore.py index 34998b8db..8944fd45b 100644 --- a/mig/unittest/testcore.py +++ b/mig/unittest/testcore.py @@ -42,18 +42,17 @@ invisible_path, allow_script, brief_list -_LOCAL_MIG_BASE = '/usr/src/app' if PY2 else MIG_BASE # account for execution in container -_PYTHON_MAJOR = '2' if PY2 else '3' -_TEST_CONF_DIR = os.path.join(MIG_BASE, "envhelp/output/testconfs-py%s" % (_PYTHON_MAJOR,)) -_TEST_CONF_FILE = os.path.join(_TEST_CONF_DIR, "MiGserver.conf") +_TEST_CONF_FILE = os.environ['MIG_CONF'] +_TEST_CONF_DIR = os.path.dirname(_TEST_CONF_FILE) _TEST_CONF_SYMLINK = os.path.join(MIG_BASE, "envhelp/output/testconfs") def _assert_local_config(): try: - link_stat = os.lstat(_TEST_CONF_SYMLINK) - assert stat.S_ISLNK(link_stat.st_mode) - configdir_stat = os.stat(_TEST_CONF_DIR) + #link_stat = os.lstat(_TEST_CONF_SYMLINK) + #assert stat.S_ISLNK(link_stat.st_mode) + _test_conf_dir = os.path.dirname(_TEST_CONF_DIR) + configdir_stat = os.stat(_test_conf_dir) assert stat.S_ISDIR(configdir_stat.st_mode) config = ConfigParser() config.read([_TEST_CONF_FILE]) @@ -67,23 +66,16 @@ def _assert_local_config_global_values(config): for path in ('mig_path', 'certs_path', 'state_path'): path_value = config_global_values.get(path) - if not is_path_within(path_value, start=_LOCAL_MIG_BASE): + if not is_path_within(path_value, start=MIG_BASE): raise AssertionError('local config contains bad path: %s=%s' % (path, path_value)) return config_global_values -def main(_exit=sys.exit): +def main(configuration, _exit=sys.exit): config = _assert_local_config() config_global_values = _assert_local_config_global_values(config) - from mig.shared.conf import get_configuration_object - configuration = get_configuration_object(_TEST_CONF_FILE, skip_log=True, - disable_auth_log=True) - logging.basicConfig(filename=None, level=logging.INFO, - format="%(asctime)s %(levelname)s %(message)s") - configuration.logger = logging - print("Running unit test on shared core functions ..") short_alias = 'email' @@ -192,4 +184,5 @@ def main(_exit=sys.exit): _exit(0) if __name__ == "__main__": - main() + from mig.shared.conf import get_configuration_object + main(get_configuration_object()) diff --git a/tests/__init__.py b/tests/__init__.py index e69de29bb..bcec2ab8a 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -0,0 +1,10 @@ +def _print_identity(): + import os + import sys + python_version_string = sys.version.split(' ')[0] + mig_env = os.environ.get('MIG_ENV', 'local') + print("running with MIG_ENV='%s' under Python %s" % + (mig_env, python_version_string)) + print("") + +_print_identity() diff --git a/tests/support/__init__.py b/tests/support/__init__.py index 499ea005c..422182b4a 100644 --- a/tests/support/__init__.py +++ b/tests/support/__init__.py @@ -42,27 +42,30 @@ from tests.support.configsupp import FakeConfiguration from tests.support.suppconst import MIG_BASE, TEST_BASE, TEST_FIXTURE_DIR, \ - TEST_OUTPUT_DIR, TEST_DATA_DIR + TEST_DATA_DIR, TEST_OUTPUT_DIR, ENVHELP_OUTPUT_DIR -PY2 = (sys.version_info[0] == 2) +from tests.support._env import MIG_ENV, PY2 -# force defaults to a local environment -os.environ['MIG_ENV'] = 'local' +# Provide access to a configuration file for the active environment. -# expose the configured environment as a constant -MIG_ENV = os.environ['MIG_ENV'] - -if MIG_ENV == 'local': - # force testconfig as the conig file path - is_py2 = PY2 - _conf_dir_suffix = "-py%s" % ('2' if is_py2 else '3',) - _conf_dir = "testconfs%s" % (_conf_dir_suffix,) - _local_conf = os.path.join( - MIG_BASE, 'envhelp/output', _conf_dir, 'MiGserver.conf') +if MIG_ENV in ('local', 'docker'): + # force local testconfig + _output_dir = os.path.join(MIG_BASE, 'envhelp/output') + _conf_dir_name = "testconfs-%s" % (MIG_ENV,) + _conf_dir = os.path.join(_output_dir, _conf_dir_name) + _local_conf = os.path.join(_conf_dir, 'MiGserver.conf') _config_file = os.getenv('MIG_CONF', None) if _config_file is None: os.environ['MIG_CONF'] = _local_conf + # adjust the link through which confs are accessed to suit the environment + _conf_link = os.path.join(_output_dir, 'testconfs') + assert os.path.lexists(_conf_link) # it must already exist + os.remove(_conf_link) # blow it away + os.symlink(_conf_dir, _conf_link) # recreate it using the active MIG_BASE +else: + raise NotImplementedError() + # All MiG related code will at some point include bits from the mig module # namespace. Rather than have this knowledge spread through every test file, # make the sole responsbility of test files to find the support file and @@ -75,7 +78,10 @@ os.mkdir(TEST_OUTPUT_DIR) except EnvironmentError as enverr: if enverr.errno == errno.EEXIST: # FileExistsError - shutil.rmtree(TEST_OUTPUT_DIR) + try: + shutil.rmtree(TEST_OUTPUT_DIR) + except Exception as exc: + raise os.mkdir(TEST_OUTPUT_DIR) # Exports to expose at the top level from the support library. @@ -146,7 +152,11 @@ def tearDown(self): if os.path.islink(path): os.remove(path) elif os.path.isdir(path): - shutil.rmtree(path) + try: + shutil.rmtree(path) + except Exception as exc: + print(path) + raise elif os.path.exists(path): os.remove(path) else: @@ -164,6 +174,11 @@ def before_each(self): def _register_check(self, check_callable): self._cleanup_checks.append(check_callable) + def _register_path(self, cleanup_path): + assert os.path.isabs(cleanup_path) + self._cleanup_paths.add(cleanup_path) + return cleanup_path + def _reset_logging(self, stream): root_logger = logging.getLogger() root_handler = root_logger.handlers[0] @@ -188,11 +203,26 @@ def _provide_configuration(self): @property def configuration(self): """Init a fake configuration if not already done""" - if self._configuration is None: - configuration_to_make = self._provide_configuration() - self._configuration = self._make_configuration_instance( - configuration_to_make) - return self._configuration + + if self._configuration is not None: + return self._configuration + + configuration_to_make = self._provide_configuration() + configuration_instance = self._make_configuration_instance( + configuration_to_make) + + if configuration_to_make == 'testconfig': + # use the paths defined by the loaded configuration to create + # the directories which are expected to be present by the code + os.mkdir(self._register_path(configuration_instance.certs_path)) + os.mkdir(self._register_path(configuration_instance.state_path)) + log_path = os.path.join(configuration_instance.state_path, "log") + os.mkdir(self._register_path(log_path)) + + self._configuration = configuration_instance + + return configuration_instance + @property def logger(self): @@ -361,18 +391,31 @@ def fixturepath(relative_path): return tmp_path -def temppath(relative_path, test_case, ensure_dir=False, skip_clean=False, - skip_output_anchor=False): +def temppath(relative_path, test_case, ensure_dir=False, skip_clean=False): """Register relative_path as a temp path and schedule automatic clean up after unit tests unless skip_clean is set. Anchors the temp path in internal test output dir unless skip_output_anchor is set. Returns resulting temp path. """ assert isinstance(test_case, MigTestCase) - if not skip_output_anchor: - tmp_path = os.path.join(TEST_OUTPUT_DIR, relative_path) - else: + + if os.path.isabs(relative_path): + # the only permitted paths are those within the output directory set + # aside for execution of the test suite: this will be enforced below + # so effectively submit the supplied path for scrutiny tmp_path = relative_path + else: + tmp_path = os.path.join(TEST_OUTPUT_DIR, relative_path) + + # failsafe path checking that supplied paths are rooted within valid paths + is_tmp_path_within_safe_dir = False + for start in (ENVHELP_OUTPUT_DIR): + is_tmp_path_within_safe_dir = is_path_within(tmp_path, start=start) + if is_tmp_path_within_safe_dir: + break + if not is_tmp_path_within_safe_dir: + raise AssertionError("ABORT: corrupt test path=%s" % (tmp_path,)) + if ensure_dir: try: os.mkdir(tmp_path) diff --git a/tests/support/_env.py b/tests/support/_env.py new file mode 100644 index 000000000..2c71386a4 --- /dev/null +++ b/tests/support/_env.py @@ -0,0 +1,11 @@ +import os +import sys + +# expose the configured environment as a constant +MIG_ENV = os.environ.get('MIG_ENV', 'local') + +# force the chosen environment globally +os.environ['MIG_ENV'] = MIG_ENV + +# expose a boolean indicating whether we are executing on Python 2 +PY2 = (sys.version_info[0] == 2) diff --git a/tests/support/suppconst.py b/tests/support/suppconst.py index 15912e933..148303f0d 100644 --- a/tests/support/suppconst.py +++ b/tests/support/suppconst.py @@ -27,11 +27,33 @@ import os +from tests.support._env import MIG_ENV -# Use abspath for __file__ on Py2 -_SUPPORT_DIR = os.path.dirname(os.path.abspath(__file__)) -TEST_BASE = os.path.normpath(os.path.join(_SUPPORT_DIR, "..")) +if MIG_ENV == 'local': + # Use abspath for __file__ on Py2 + _SUPPORT_DIR = os.path.dirname(os.path.abspath(__file__)) +elif MIG_ENV == 'docker': + _SUPPORT_DIR = '/usr/src/app/tests/support' +else: + raise NotImplementedError("ABORT: unsupported environment: %s" % (MIG_ENV,)) + +MIG_BASE = os.path.realpath(os.path.join(_SUPPORT_DIR, "../..")) +TEST_BASE = os.path.join(MIG_BASE, "tests") TEST_DATA_DIR = os.path.join(TEST_BASE, "data") TEST_FIXTURE_DIR = os.path.join(TEST_BASE, "fixture") TEST_OUTPUT_DIR = os.path.join(TEST_BASE, "output") -MIG_BASE = os.path.realpath(os.path.join(TEST_BASE, "..")) +ENVHELP_DIR = os.path.join(MIG_BASE, "envhelp") +ENVHELP_OUTPUT_DIR = os.path.join(ENVHELP_DIR, "output") + + +if __name__ == '__main__': + def print_root_relative(prefix, path): + print("%s = /%s" % (prefix, os.path.relpath(path, MIG_BASE))) + + print("# base paths") + print("root=%s" % (MIG_BASE,)) + print("# envhelp paths") + print_root_relative("output", ENVHELP_OUTPUT_DIR) + print("# test paths") + print_root_relative("fixture", TEST_FIXTURE_DIR) + print_root_relative("output", TEST_OUTPUT_DIR) diff --git a/tests/test_booleans.py b/tests/test_booleans.py index 3c37c1ce3..5246197ee 100644 --- a/tests/test_booleans.py +++ b/tests/test_booleans.py @@ -1,11 +1,14 @@ from __future__ import print_function -import sys -from unittest import TestCase +from tests.support import MigTestCase, testmain -class TestBooleans(TestCase): +class TestBooleans(MigTestCase): def test_true(self): self.assertEqual(True, True) def test_false(self): self.assertEqual(False, False) + + +if __name__ == '__main__': + testmain() diff --git a/tests/test_mig_shared_functionality_cat.py b/tests/test_mig_shared_functionality_cat.py index b7edaab5a..e02af8896 100644 --- a/tests/test_mig_shared_functionality_cat.py +++ b/tests/test_mig_shared_functionality_cat.py @@ -75,19 +75,20 @@ def before_each(self): test_user_dir = os.path.join(conf_user_home, test_client_dir) # ensure a user db that includes our test user + conf_user_db_home = ensure_dirs_exist(self.configuration.user_db_home) - temppath(conf_user_db_home, self, skip_output_anchor=True) + temppath(conf_user_db_home, self) db_fixture, db_fixture_file = fixturefile('MiG-users.db--example', fixture_format='binary', include_path=True) test_db_file = temppath(fixturefile_normname('MiG-users.db--example', prefix=conf_user_db_home), - self, skip_output_anchor=True) + self) shutil.copyfile(db_fixture_file, test_db_file) # create the test user home directory self.test_user_dir = ensure_dirs_exist(test_user_dir) - temppath(self.test_user_dir, self, skip_output_anchor=True) + temppath(self.test_user_dir, self) self.test_environ = create_http_environ(self.configuration) def assertSingleOutputObject(self, output_objects, with_object_type=None): diff --git a/tests/test_mig_unittest_testcore.py b/tests/test_mig_unittest_testcore.py index a7621812a..b27a74d33 100644 --- a/tests/test_mig_unittest_testcore.py +++ b/tests/test_mig_unittest_testcore.py @@ -38,6 +38,9 @@ class MigUnittestTestcore(MigTestCase): + def _provide_configuration(self): + return 'testconfig' + def test_existing_main(self): def raise_on_error_exit(exit_code, identifying_message=None): if exit_code != 0: @@ -48,7 +51,7 @@ def raise_on_error_exit(exit_code, identifying_message=None): print("") # account for wrapped tests printing to console - testcore_main(_exit=raise_on_error_exit) + testcore_main(self.configuration, _exit=raise_on_error_exit) if __name__ == '__main__':