diff --git a/.bumpversion.cfg b/.bumpversion.cfg index d459ac0..2daa7b7 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 2.3.3 +current_version = 2.4.0 tag = False commit = False diff --git a/CHANGELOG.md b/CHANGELOG.md index e600589..8b09c2b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +# v 2.4.0 (2022-09-07) +Changes in this release: +- Add fixture to change the Inmanta state dir to a writable location for the current user. +- Add a new '--no-strict-deps-check' option to run pytest-inmanta using the legacy check on requirements. +By default the new strict check of core will be used. + # v 2.3.3 (2022-05-18) Changes in this release: - Fix enum test parameters registered after pytest has loaded pytest-inmanta plugin. diff --git a/Jenkinsfile-integration-tests-iso3 b/Jenkinsfile-integration-tests-iso3 deleted file mode 100644 index 0850ac8..0000000 --- a/Jenkinsfile-integration-tests-iso3 +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Test the pytest-inmanta version used by the ISO3 product (pytest-inmanta<1.5.0) against - * the corresponding inmanta-core version. - */ - pipeline { - agent any - triggers { - cron(BRANCH_NAME == "master" ? "H H(2-5) * * *": "") - } - - environment { - REPO_NAME="pytest-inmanta" - PIP_INDEX_URL="https://artifacts.internal.inmanta.com/inmanta/dev" - PIP_PRE="true" - } - - options { - disableConcurrentBuilds() - checkoutToSubdirectory(env.REPO_NAME) - skipDefaultCheckout() - buildDiscarder(logRotator(numToKeepStr: '30')) - } - - stages { - stage("Setup") { - steps { - deleteDir() - dir(env.REPO_NAME) { - checkout scm - script { - sh ''' - rm -rf ${WORKSPACE}/env - python3 -m venv ${WORKSPACE}/env - ${WORKSPACE}/env/bin/python3 -m pip install -U setuptools pip - - # Versions used by ISO3 - ${WORKSPACE}/env/bin/python3 -m pip install -U "pytest-inmanta<1.5.0" "inmanta~=2020.4.7" - ''' - } - } - } - } - - stage("Run tests") { - steps { - dir(env.REPO_NAME) { - sh ''' - ${WORKSPACE}/env/bin/python3 -m pytest --junitxml=junit.xml --log-cli-level DEBUG -s -vvv tests --basetemp=${WORKSPACE}/tmp - ''' - } - } - } - } - - post { - always { - junit "${env.REPO_NAME}/junit.xml" - } - } -} diff --git a/README.md b/README.md index 6dda23a..6024295 100644 --- a/README.md +++ b/README.md @@ -173,7 +173,7 @@ The following options are available. module to a temporary libs directory. It allows testing the current module against specific versions of dependent modules. Using this option can speed up the tests, because the module dependencies are not downloaded multiple times. * `--module-repo`: location to download modules from, overrides `INMANTA_MODULE_REPO`. The default value is the inmanta github organisation. - For versions of inmanta-core that support v2 modules, the repo accepts the format "[:]" with "type" the repository type as + For versions of inmanta-core that support v2 modules, the repo accepts the format "[\:]\" with "type" the repository type as defined in the project config documentation. If type is omitted, git is assumed. Multiple repos can be passed by space-separating them or by passing the parameter multiple times. * `--install-mode`: install mode to use for modules downloaded during this test, overrides `INMANTA_INSTALL_MODE`. @@ -181,6 +181,7 @@ The following options are available. When not using this option during the testing of plugins with the `project.get_plugin_function` method, it's possible that the module's `plugin/__init__.py` is loaded multiple times, which can cause issues when it has side effects, as they are executed multiple times as well. + * `--no-strict-deps-check`: option to run pytest-inmanta using the legacy check(less strict) on requirements. By default the new strict will be used. Use the generic pytest options `--log-cli-level` to show Inmanta logger to see any setup or cleanup warnings. For example, `--log-cli-level=INFO` diff --git a/examples/inmanta-module-testmodulev2conflict1/MANIFEST.in b/examples/inmanta-module-testmodulev2conflict1/MANIFEST.in new file mode 100644 index 0000000..6a98ec9 --- /dev/null +++ b/examples/inmanta-module-testmodulev2conflict1/MANIFEST.in @@ -0,0 +1,4 @@ +include inmanta_plugins/testmodulev2conflict1/setup.cfg +recursive-include inmanta_plugins/testmodulev2conflict1/model *.cf +graft inmanta_plugins/testmodulev2conflict1/files +graft inmanta_plugins/testmodulev2conflict1/templates diff --git a/examples/inmanta-module-testmodulev2conflict1/inmanta_plugins/testmodulev2conflict1/__init__.py b/examples/inmanta-module-testmodulev2conflict1/inmanta_plugins/testmodulev2conflict1/__init__.py new file mode 100644 index 0000000..27fc562 --- /dev/null +++ b/examples/inmanta-module-testmodulev2conflict1/inmanta_plugins/testmodulev2conflict1/__init__.py @@ -0,0 +1,11 @@ +""" + Copyright 2021 Inmanta + Contact: code@inmanta.com + License: Apache 2.0 +""" +from inmanta.plugins import plugin + + +@plugin +def myplugin(x: "int") -> "int": + return x diff --git a/examples/inmanta-module-testmodulev2conflict1/model/_init.cf b/examples/inmanta-module-testmodulev2conflict1/model/_init.cf new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/inmanta-module-testmodulev2conflict1/model/_init.cf @@ -0,0 +1 @@ + diff --git a/examples/inmanta-module-testmodulev2conflict1/pyproject.toml b/examples/inmanta-module-testmodulev2conflict1/pyproject.toml new file mode 100644 index 0000000..9787c3b --- /dev/null +++ b/examples/inmanta-module-testmodulev2conflict1/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["setuptools", "wheel"] +build-backend = "setuptools.build_meta" diff --git a/examples/inmanta-module-testmodulev2conflict1/setup.cfg b/examples/inmanta-module-testmodulev2conflict1/setup.cfg new file mode 100644 index 0000000..1045f68 --- /dev/null +++ b/examples/inmanta-module-testmodulev2conflict1/setup.cfg @@ -0,0 +1,14 @@ +[metadata] +name = inmanta-module-testmodulev2conflict1 +freeze_recursive = False +freeze_operator = ~= +version = 0.1 +license = Test License + +[options] +install_requires = + inmanta-module-std + lorem~=0.0.1 +zip_safe = False +include_package_data = True +packages = find_namespace: diff --git a/examples/inmanta-module-testmodulev2conflict2/MANIFEST.in b/examples/inmanta-module-testmodulev2conflict2/MANIFEST.in new file mode 100644 index 0000000..8516475 --- /dev/null +++ b/examples/inmanta-module-testmodulev2conflict2/MANIFEST.in @@ -0,0 +1,4 @@ +include inmanta_plugins/testmodulev2conflict2/setup.cfg +recursive-include inmanta_plugins/testmodulev2conflict2/model *.cf +graft inmanta_plugins/testmodulev2conflict2/files +graft inmanta_plugins/testmodulev2conflict2/templates diff --git a/examples/inmanta-module-testmodulev2conflict2/inmanta_plugins/testmodulev2conflict2/__init__.py b/examples/inmanta-module-testmodulev2conflict2/inmanta_plugins/testmodulev2conflict2/__init__.py new file mode 100644 index 0000000..27fc562 --- /dev/null +++ b/examples/inmanta-module-testmodulev2conflict2/inmanta_plugins/testmodulev2conflict2/__init__.py @@ -0,0 +1,11 @@ +""" + Copyright 2021 Inmanta + Contact: code@inmanta.com + License: Apache 2.0 +""" +from inmanta.plugins import plugin + + +@plugin +def myplugin(x: "int") -> "int": + return x diff --git a/examples/inmanta-module-testmodulev2conflict2/model/_init.cf b/examples/inmanta-module-testmodulev2conflict2/model/_init.cf new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/inmanta-module-testmodulev2conflict2/model/_init.cf @@ -0,0 +1 @@ + diff --git a/examples/inmanta-module-testmodulev2conflict2/pyproject.toml b/examples/inmanta-module-testmodulev2conflict2/pyproject.toml new file mode 100644 index 0000000..9787c3b --- /dev/null +++ b/examples/inmanta-module-testmodulev2conflict2/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["setuptools", "wheel"] +build-backend = "setuptools.build_meta" diff --git a/examples/inmanta-module-testmodulev2conflict2/setup.cfg b/examples/inmanta-module-testmodulev2conflict2/setup.cfg new file mode 100644 index 0000000..e1d82d1 --- /dev/null +++ b/examples/inmanta-module-testmodulev2conflict2/setup.cfg @@ -0,0 +1,14 @@ +[metadata] +name = inmanta-module-testmodulev2conflict2 +freeze_recursive = False +freeze_operator = ~= +version = 0.1 +license = Test License + +[options] +install_requires = + inmanta-module-std + lorem~=0.1.1 +zip_safe = False +include_package_data = True +packages = find_namespace: diff --git a/examples/test_conflict_dependencies/files/.gitkeep b/examples/test_conflict_dependencies/files/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/examples/test_conflict_dependencies/model/_init.cf b/examples/test_conflict_dependencies/model/_init.cf new file mode 100644 index 0000000..e69de29 diff --git a/examples/test_conflict_dependencies/module.yml b/examples/test_conflict_dependencies/module.yml new file mode 100644 index 0000000..aa2164b --- /dev/null +++ b/examples/test_conflict_dependencies/module.yml @@ -0,0 +1,8 @@ +author: Inmanta +author_email: code@inmanta.com +description: module that test what happens when its dependencies conflict +license: ASL 2.0 +copyright: 2021 Inmanta +name: test_conflict_dependencies +version: 0.0.1 +compiler_version: 2019.3 diff --git a/examples/test_conflict_dependencies/plugins/__init__.py b/examples/test_conflict_dependencies/plugins/__init__.py new file mode 100644 index 0000000..0e7ead4 --- /dev/null +++ b/examples/test_conflict_dependencies/plugins/__init__.py @@ -0,0 +1,17 @@ +""" + Copyright 2021 Inmanta + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + Contact: code@inmanta.com +""" diff --git a/examples/test_conflict_dependencies/requirements.dev.txt b/examples/test_conflict_dependencies/requirements.dev.txt new file mode 100644 index 0000000..3db865a --- /dev/null +++ b/examples/test_conflict_dependencies/requirements.dev.txt @@ -0,0 +1,7 @@ +pytest-inmanta +flake8 +flake8-black +flake8-isort>3.0.0 +flake8-copyright +isort +black diff --git a/examples/test_conflict_dependencies/requirements.txt b/examples/test_conflict_dependencies/requirements.txt new file mode 100644 index 0000000..1ee09ae --- /dev/null +++ b/examples/test_conflict_dependencies/requirements.txt @@ -0,0 +1,2 @@ +inmanta-module-testmodulev2conflict1 +inmanta-module-testmodulev2conflict2 diff --git a/examples/test_conflict_dependencies/templates/.gitkeep b/examples/test_conflict_dependencies/templates/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/examples/test_conflict_dependencies/tests/test_basics.py b/examples/test_conflict_dependencies/tests/test_basics.py new file mode 100644 index 0000000..65e8053 --- /dev/null +++ b/examples/test_conflict_dependencies/tests/test_basics.py @@ -0,0 +1,21 @@ +""" + Copyright 2021 Inmanta + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + Contact: code@inmanta.com +""" + + +def test_basics(project): + project.compile("import test_conflict_dependencies") diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..e69de29 diff --git a/pytest_inmanta/parameters.py b/pytest_inmanta/parameters.py index f6a25ab..f5a30df 100644 --- a/pytest_inmanta/parameters.py +++ b/pytest_inmanta/parameters.py @@ -63,6 +63,18 @@ group=param_group, ) +inm_no_strict_deps_check = BooleanTestParameter( + argument="--no-strict-deps-check", + environment_variable="INMANTA_NO_STRICT_DEPS_CHECKS", + usage=( + "Tell pytest-inmanta to run without using the deps check after module installation." + " When using the dependency check, an error is raised if there are conflicting requirements" + " when disabling the check, the less strict legacy behavior is used instead." + ), + default=False, + group=param_group, +) + # This is the legacy module repo option # TODO remove this in next major version bump diff --git a/pytest_inmanta/plugin.py b/pytest_inmanta/plugin.py index 7f12c24..54eb132 100644 --- a/pytest_inmanta/plugin.py +++ b/pytest_inmanta/plugin.py @@ -43,7 +43,9 @@ import inmanta.ast from inmanta import compiler, config, const, module, plugins, protocol -from inmanta.agent import cache, handler +from inmanta.agent import cache +from inmanta.agent import config as inmanta_config +from inmanta.agent import handler from inmanta.agent import io as agent_io from inmanta.agent.handler import HandlerContext, ResourceHandler from inmanta.const import ResourceState @@ -55,14 +57,23 @@ from inmanta.resources import Resource if typing.TYPE_CHECKING: + # Local type stub for mypy that works with both pytest < 7 and pytest >=7 + # https://docs.pytest.org/en/7.1.x/_modules/_pytest/legacypath.html#TempdirFactory + import py from inmanta.agent.io.local import IOBase + class TempdirFactory: + def mktemp(self, path: str) -> py.path.local: + ... + + from .handler import DATA from .parameters import ( inm_install_mode, inm_mod_in_place, inm_mod_repo, inm_no_load_plugins, + inm_no_strict_deps_check, inm_venv, ) from .test_parameter import ParameterNotSetException, TestParameterRegistry @@ -125,7 +136,7 @@ def inmanta_plugins( @pytest.fixture() def project( - project_shared: "Project", capsys: "CaptureFixture" + project_shared: "Project", capsys: "CaptureFixture", set_inmanta_state_dir: None ) -> typing.Iterator["Project"]: DATA.clear() project_shared.clean() @@ -136,7 +147,9 @@ def project( @pytest.fixture() def project_no_plugins( - project_shared_no_plugins: "Project", capsys: "CaptureFixture" + project_shared_no_plugins: "Project", + capsys: "CaptureFixture", + set_inmanta_state_dir: None, ) -> typing.Iterator["Project"]: warnings.warn( DeprecationWarning( @@ -269,7 +282,9 @@ def project_factory(request: pytest.FixtureRequest) -> typing.Callable[[], "Proj def create_project(**kwargs: object): load_plugins = not inm_no_load_plugins.resolve(request.config) + no_strict_deps_check = inm_no_strict_deps_check.resolve(request.config) extended_kwargs: typing.Dict[str, object] = { + "no_strict_deps_check": no_strict_deps_check, "load_plugins": load_plugins, "env_path": env_dir, **kwargs, @@ -593,6 +608,7 @@ def __init__( project_dir: str, env_path: str, load_plugins: typing.Optional[bool] = True, + no_strict_deps_check: typing.Optional[bool] = False, ) -> None: """ :param project_dir: Directory containing the Inmanta project. @@ -601,6 +617,7 @@ def __init__( """ self._test_project_dir = project_dir self._env_path = env_path + self.no_strict_deps_check = no_strict_deps_check self._stdout: typing.Optional[str] = None self._stderr: typing.Optional[str] = None self.types: typing.Optional[typing.Dict[str, inmanta.ast.Type]] = None @@ -651,7 +668,7 @@ def init(self, capsys: "CaptureFixture") -> None: def _create_project_and_load(self, model: str) -> module.Project: """ - This method doesn the following: + This method does the following: * Add the given model file to the Inmanta project * Install the module dependencies * Load the project @@ -666,12 +683,20 @@ def _create_project_and_load(self, model: str) -> module.Project: module.Project.__init__ ) # The venv_path parameter only exists on ISO5+ + extra_kwargs_init = ( {"venv_path": self._env_path} if "venv_path" in signature_init.parameters.keys() else {} ) - test_project = module.Project(self._test_project_dir, **extra_kwargs_init) + + if "strict_deps_check" in signature_init.parameters.keys(): + extra_kwargs_init["strict_deps_check"] = not self.no_strict_deps_check + + test_project = module.Project( + self._test_project_dir, + **extra_kwargs_init, + ) ProjectLoader.load(test_project) @@ -1130,3 +1155,19 @@ def finalize_handler(self, handler: ResourceHandler) -> None: def finalize_all_handlers(self) -> None: for handler_instance in self._handlers: self.finalize_handler(handler_instance) + + +@pytest.fixture(scope="function") +def inmanta_state_dir(tmpdir_factory: "TempdirFactory") -> Iterator[str]: + """ + This fixture can be overridden in the conftest of any individual project + in order to set the Inmanta state directory at the desired level. + """ + inmanta_state_dir = tmpdir_factory.mktemp("inmanta_state_dir") + yield str(inmanta_state_dir) + inmanta_state_dir.remove() + + +@pytest.fixture +def set_inmanta_state_dir(inmanta_state_dir: str) -> None: + inmanta_config.state_dir.set(inmanta_state_dir) diff --git a/requirements.txt b/requirements.txt index 66bb7af..bb02024 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,6 @@ inmanta-dev-dependencies==1.76.0; python_version <= '3.6' -inmanta-dev-dependencies==2.17.0; python_version > '3.6' +inmanta-dev-dependencies==2.34.0; python_version > '3.6' inmanta-core -pydantic==1.9.0 +pydantic==1.9.2; python_version <= '3.6' +pydantic==1.10.2; python_version > '3.6' pyyaml==6.0 diff --git a/setup.cfg b/setup.cfg index c661822..8a21c15 100644 --- a/setup.cfg +++ b/setup.cfg @@ -26,6 +26,8 @@ copyright-check=True copyright-author=Inmanta # C errors are not selected by default, so add them to the selection select = E,F,W,C,BLK,I +# make sure projects in examples dir are never considered as separate projects rather than depending on how check is invoked +black-config=pyproject.toml [isort] profile=black diff --git a/setup.py b/setup.py index 44588bb..18c74b7 100644 --- a/setup.py +++ b/setup.py @@ -12,7 +12,7 @@ setup( name="pytest-inmanta", - version="2.3.3", + version="2.4.0", description=( "A py.test plugin providing fixtures to simplify inmanta modules testing." ), diff --git a/tests/conftest.py b/tests/conftest.py index 3ba20bb..62109d5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -46,6 +46,8 @@ def deactive_venv(): old_os_path = os.environ.get("PATH", "") old_prefix = sys.prefix old_path = sys.path + old_meta_path = sys.meta_path.copy() + old_path_hooks = sys.path_hooks.copy() old_pythonpath = os.environ.get("PYTHONPATH", None) old_os_venv: Optional[str] = os.environ.get("VIRTUAL_ENV", None) old_working_set = pkg_resources.working_set @@ -55,6 +57,13 @@ def deactive_venv(): os.environ["PATH"] = old_os_path sys.prefix = old_prefix sys.path = old_path + # reset sys.meta_path because it might contain finders for editable installs, make sure to keep the same object + sys.meta_path.clear() + sys.meta_path.extend(old_meta_path) + sys.path_hooks.clear() + sys.path_hooks.extend(old_path_hooks) + # Clear cache for sys.path_hooks + sys.path_importer_cache.clear() pkg_resources.working_set = old_working_set # Restore PYTHONPATH if old_pythonpath is not None: diff --git a/tests/test_basic_example_v2.py b/tests/test_basic_example_v2.py index 9de698a..92e9d91 100644 --- a/tests/test_basic_example_v2.py +++ b/tests/test_basic_example_v2.py @@ -16,13 +16,9 @@ Contact: code@inmanta.com """ import logging -import os -import subprocess # Note: These tests only function when the pytest output is not modified by plugins such as pytest-sugar! -import tempfile -from importlib.abc import Loader -from typing import Iterator, Optional, Tuple +from typing import Iterator import pytest @@ -43,68 +39,22 @@ def testmodulev2_venv(pytestconfig) -> Iterator[env.VirtualEnv]: """ Yields a Python environment with testmodulev2 installed in it. """ - with tempfile.TemporaryDirectory() as venv_dir: - # set up environment - venv: env.VirtualEnv = env.VirtualEnv(env_path=venv_dir) - venv.init_env() - venv_unset_python_path(venv) - # install test module into environment - subprocess.check_call( - [ - venv.python_path, - "-m", - "inmanta.app", - "-X", - "module", - "install", - str(pytestconfig.rootpath / "examples" / "testmodulev2"), - ], - ) + with utils.module_v2_venv( + pytestconfig.rootpath / "examples" / "testmodulev2" + ) as venv: yield venv -def venv_unset_python_path(venv: env.VirtualEnv) -> None: - """ - Workaround for pypa/build#405: unset PYTHONPATH because it's not required in this case and it triggers a bug in build - """ - sitecustomize_existing: Optional[ - Tuple[Optional[str], Loader] - ] = env.ActiveEnv.get_module_file("sitecustomize") - # inherit from existing sitecustomize.py - sitecustomize_inherit: str - if sitecustomize_existing is not None and sitecustomize_existing[0] is not None: - with open(sitecustomize_existing[0], "r") as fd: - sitecustomize_inherit = fd.read() - else: - sitecustomize_inherit = "" - with open(os.path.join(venv.site_packages_dir, "sitecustomize.py"), "a") as fd: - fd.write( - f""" -{sitecustomize_inherit} - -import os - -if "PYTHONPATH" in os.environ: - del os.environ["PYTHONPATH"] - """.strip() - ) - - @pytest.fixture(scope="function") def testmodulev2_venv_active( - deactive_venv, testmodulev2_venv + deactive_venv: None, + testmodulev2_venv: env.VirtualEnv, ) -> Iterator[env.VirtualEnv]: """ Activates a Python environment with testmodulev2 installed in it for the currently running process. """ - with tempfile.TemporaryDirectory() as tmpdir: - # Create a unique function-scoped venv dir to prevent caching issues with inmanta_plugins' submodule_search_locations - unique_env_dir: str = os.path.join(tmpdir, ".env") - os.symlink(testmodulev2_venv.env_path, unique_env_dir) - unique_env: env.VirtualEnv = env.VirtualEnv(env_path=unique_env_dir) - unique_env.use_virtual_env() - yield unique_env - utils.unload_modules_for_path(unique_env.site_packages_dir) + with utils.activate_venv(testmodulev2_venv) as venv: + yield venv def test_basic_example(testdir, caplog, testmodulev2_venv_active): diff --git a/tests/test_dependencies.py b/tests/test_dependencies.py index 6d68ed1..70b73cf 100644 --- a/tests/test_dependencies.py +++ b/tests/test_dependencies.py @@ -20,6 +20,8 @@ import os import tempfile +import pytest + import pytest_inmanta.plugin import utils from inmanta import env @@ -54,3 +56,55 @@ def test_transitive_v2_dependencies(examples_v2_package_index, pytestconfig, tes result.assert_outcomes(passed=1) finally: utils.unload_modules_for_path(venv.site_packages_dir) + + +@pytest.mark.parametrize( + "no_strict_deps_check, error_msg", + [ + (True, "CompilerException"), + (False, "ConflictingRequirements"), + ], +) +def test_conflicing_dependencies( + examples_v2_package_index, pytestconfig, testdir, no_strict_deps_check, error_msg +): + """ + when using the pytest-inmanta without specifying the --no-strict-deps-check, the constraints + of the installed modules/packages are verified and if a conflict is detected a ConflictingRequirement + error is raised. + when using pytest-inmanta with --no-strict-deps-check option, + the legacy check on the constraints is done. If the installed modules are not compatible + a CompilerException is raised. In the used example for this test, + test_conflict_dependencies(v1 module) requires inmanta-module-testmodulev2conflict1 and + inmanta-module-testmodulev2conflict2. The later two are incompatible as one requires lorem 0.0.1 + and the other one 0.1.1. + """ + # set working directory to allow in-place with all example modules + pytest_inmanta.plugin.CURDIR = str( + pytestconfig.rootpath / "examples" / "test_conflict_dependencies" + ) + testdir.copy_example("test_conflict_dependencies") + + with tempfile.TemporaryDirectory() as venv_dir: + # set up environment + venv: env.VirtualEnv = env.VirtualEnv(env_path=venv_dir) + try: + venv.use_virtual_env() + + # run tests + result = testdir.runpytest_inprocess( + "tests/test_basics.py", + *(["--no-strict-deps-check"] if no_strict_deps_check else []), + "--use-module-in-place", + # add pip index containing examples packages as module repo + "--module_repo", + f"package:{examples_v2_package_index}", + # include configured pip index for inmanta-module-std and lorem + "--module_repo", + "package:" + + os.environ.get("PIP_INDEX_URL", "package:https://pypi.org/simple"), + ) + result.assert_outcomes(errors=1) + assert error_msg in "\n".join(result.outlines) + finally: + utils.unload_modules_for_path(venv.site_packages_dir) diff --git a/tests/utils.py b/tests/utils.py index 923b47e..72eb559 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -16,10 +16,86 @@ Contact: code@inmanta.com """ +import contextlib import importlib +import os +import subprocess import sys +import tempfile +from importlib.abc import Loader from types import ModuleType -from typing import Optional, Sequence +from typing import Iterator, Optional, Sequence, Tuple + +from inmanta import env + + +@contextlib.contextmanager +def module_v2_venv(module_path: str) -> Iterator[env.VirtualEnv]: + """ + Yields a Python environment with the given module installed in it. + """ + with tempfile.TemporaryDirectory() as venv_dir: + # set up environment + venv: env.VirtualEnv = env.VirtualEnv(env_path=venv_dir) + venv.init_env() + venv_unset_python_path(venv) + # install test module into environment + subprocess.check_call( + [ + venv.python_path, + "-m", + "inmanta.app", + "-X", + "module", + "install", + module_path, + ], + ) + yield venv + + +@contextlib.contextmanager +def activate_venv(venv: env.VirtualEnv) -> Iterator[env.VirtualEnv]: + """ + Activates a given Python environment for the currently running process. To prevent + """ + with tempfile.TemporaryDirectory() as tmpdir: + # Create a unique venv dir to prevent caching issues with inmanta_plugins' submodule_search_locations + unique_env_dir: str = os.path.join(tmpdir, ".env") + os.symlink(venv.env_path, unique_env_dir) + unique_env: env.VirtualEnv = env.VirtualEnv(env_path=unique_env_dir) + unique_env.use_virtual_env() + try: + yield unique_env + finally: + unload_modules_for_path(unique_env.site_packages_dir) + + +def venv_unset_python_path(venv: env.VirtualEnv) -> None: + """ + Workaround for pypa/build#405: unset PYTHONPATH because it's not required in this case and it triggers a bug in build + """ + sitecustomize_existing: Optional[ + Tuple[Optional[str], Loader] + ] = env.ActiveEnv.get_module_file("sitecustomize") + # inherit from existing sitecustomize.py + sitecustomize_inherit: str + if sitecustomize_existing is not None and sitecustomize_existing[0] is not None: + with open(sitecustomize_existing[0], "r") as fd: + sitecustomize_inherit = fd.read() + else: + sitecustomize_inherit = "" + with open(os.path.join(venv.site_packages_dir, "sitecustomize.py"), "a") as fd: + fd.write( + f""" +{sitecustomize_inherit} + +import os + +if "PYTHONPATH" in os.environ: + del os.environ["PYTHONPATH"] + """.strip() + ) def unload_modules_for_path(path: str) -> None: