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__':