diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 00234e148..4d621ffad 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -154,7 +154,7 @@ jobs: CONSTRUCTOR_SIGNTOOL_PATH: "C:/Program Files (x86)/Windows Kits/10/bin/10.0.26100.0/x86/signtool.exe" run: | rm -rf coverage.json - pytest -vv --cov=constructor --cov-branch tests/test_examples.py + pytest -vv --tb=long -rs --cov=constructor --cov-branch tests/test_examples.py coverage run --branch --append -m constructor -V coverage json - uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2 diff --git a/pyproject.toml b/pyproject.toml index f54eaac55..46a453ee0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -86,4 +86,5 @@ select = [ [tool.pytest.ini_options] markers = [ "examples", + "installer_types: restrict test to specific installer types (args: tuple of suffix strings)", ] diff --git a/tests/conftest.py b/tests/conftest.py index dff805cd3..a5d82283a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,11 +1,53 @@ import os import subprocess +import sys from pathlib import Path import pytest REPO_DIR = Path(__file__).parent.parent +def _platform_installer_types() -> list[str]: + """Return the installer type suffixes available on the current platform.""" + if sys.platform.startswith("win"): + return ["exe"] + elif sys.platform == "darwin": + return ["sh", "pkg"] + else: + return ["sh"] + +ALL_INSTALLER_TYPES = _platform_installer_types() + + +def pytest_generate_tests(metafunc): + """ + Automatically parametrize any test that declares an ``installer_type`` + fixture argument. + + If the test is decorated with ``@pytest.mark.installer_types("pkg", ...)`` + the parametrization is restricted to those types (intersected with what is + available on the current platform). Without the marker all platform types + are used. + + Note: this hook lives here for colocation with the tests; it should be + moved to ``conftest.py`` if this file grows or the hook is needed elsewhere. + """ + if "installer_type" not in metafunc.fixturenames: + return + + marker = metafunc.definition.get_closest_marker("installer_types") + if marker: + requested = list(marker.args) # marker.args is already the tuple of strings + types = [t for t in requested if t in ALL_INSTALLER_TYPES] + if not types: + pytest.skip( + f"No applicable installer types for this platform " + f"(requested: {requested}, available: {ALL_INSTALLER_TYPES})" + ) + else: + types = ALL_INSTALLER_TYPES + + metafunc.parametrize("installer_type", types) @pytest.fixture def self_signed_application_certificate_macos(tmp_path): diff --git a/tests/test_examples.py b/tests/test_examples.py index e2c09b166..4eac924f9 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -62,6 +62,9 @@ else: KEEP_ARTIFACTS_PATH = None +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- def _is_program_installed(partial_name: str) -> bool: """ @@ -391,17 +394,23 @@ def _run_installer( return process -def create_installer( +def build_installer( input_dir: Path, workspace: Path, conda_exe=CONSTRUCTOR_CONDA_EXE, debug=CONSTRUCTOR_DEBUG, - with_spaces=False, timeout=420, config_filename="construct.yaml", extra_constructor_args: Iterable[str] = None, **env_vars, -) -> Generator[tuple[Path, Path], None, None]: +) -> Path: + """ + Build the installer(s) for *input_dir* and return the output directory. + + This replaces the build half of the old ``create_installer`` generator. + Call ``get_installer`` afterwards to obtain the (installer, install_dir) + pair for a specific installer type. + """ if sys.platform.startswith("win") and conda_exe and _is_micromamba(conda_exe): pytest.skip("Micromamba is not supported on Windows yet.") @@ -425,8 +434,78 @@ def create_installer( cmd.extend(extra_constructor_args) _execute(cmd, timeout=timeout, **env_vars) + return output_dir + + +def get_installer( + output_dir: Path, + input_dir: Path, + installer_type: str, + workspace: Path, + with_spaces: bool = False, + config_filename: str = "construct.yaml", +) -> tuple[Path, Path]: + """ + Return the (installer, install_dir) pair for *installer_type* from + a previously built *output_dir*. + + Moves the installer artifact to ``KEEP_ARTIFACTS_PATH`` if configured. + """ + suffix = f".{installer_type}" + matches = [p for p in output_dir.iterdir() if p.suffix == suffix] + if not matches: + raise FileNotFoundError( + f"No {suffix!r} installer found in {output_dir}. " + f"Available: {[p.name for p in output_dir.iterdir()]}" + ) + installer = matches[0] install_dir_prefix = "i n s t a l l" if with_spaces else "install" + if installer_type == "pkg" and ON_CI: + install_dir = Path("~").expanduser() / calculate_install_dir( + input_dir / config_filename + ) + else: + install_dir = workspace / f"{install_dir_prefix}-{installer.stem}-{installer_type}" + + if KEEP_ARTIFACTS_PATH: + try: + shutil.move(str(installer), str(KEEP_ARTIFACTS_PATH)) + except shutil.Error: + # Some tests reuse the examples for different checks; ignore errors + pass + + return installer, install_dir + + +def create_installer( + input_dir: Path, + workspace: Path, + conda_exe=CONSTRUCTOR_CONDA_EXE, + debug=CONSTRUCTOR_DEBUG, + with_spaces=False, + timeout=420, + config_filename="construct.yaml", + extra_constructor_args: Iterable[str] = None, + **env_vars, +) -> Generator[tuple[Path, Path], None, None]: + """ + Legacy generator kept for tests that have not yet been parametrized + (e.g. tests that use ``next(create_installer(...))`` directly or that + contain complex per-type branching that is hard to split). + + New tests should use ``build_installer`` + ``get_installer`` instead. + """ + output_dir = build_installer( + input_dir, + workspace, + conda_exe=conda_exe, + debug=debug, + timeout=timeout, + config_filename=config_filename, + extra_constructor_args=extra_constructor_args, + **env_vars, + ) def _sort_by_extension(path): "Return shell installers first so they are run before the GUI ones" @@ -434,21 +513,15 @@ def _sort_by_extension(path): installers = (p for p in output_dir.iterdir() if p.suffix in (".exe", ".sh", ".pkg")) for installer in sorted(installers, key=_sort_by_extension): - if installer.suffix == ".pkg" and ON_CI: - install_dir = Path("~").expanduser() / calculate_install_dir( - input_dir / config_filename - ) - else: - install_dir = ( - workspace / f"{install_dir_prefix}-{installer.stem}-{installer.suffix[1:]}" - ) + _, install_dir = get_installer( + output_dir, + input_dir, + installer.suffix[1:], + workspace, + with_spaces=with_spaces, + config_filename=config_filename, + ) yield installer, install_dir - if KEEP_ARTIFACTS_PATH: - try: - shutil.move(str(installer), str(KEEP_ARTIFACTS_PATH)) - except shutil.Error: - # Some tests reuse the examples for different checks; ignore errors - pass @cache @@ -494,73 +567,86 @@ def platform_conda_exe(request, tmp_path) -> tuple[str, Path]: return platform, conda_exe -def test_example_customize_controls(tmp_path, request): +# --------------------------------------------------------------------------- +# Tests +# --------------------------------------------------------------------------- + +def test_example_customize_controls(tmp_path, request, installer_type): input_path = _example_path("customize_controls") - for installer, install_dir in create_installer(input_path, tmp_path): - _run_installer(input_path, installer, install_dir, request=request) + output_dir = build_installer(input_path, tmp_path) + installer, install_dir = get_installer(output_dir, input_path, installer_type, tmp_path) + _run_installer(input_path, installer, install_dir, request=request) -def test_example_customized_welcome_conclusion(tmp_path, request): +def test_example_customized_welcome_conclusion(tmp_path, request, installer_type): input_path = _example_path("customized_welcome_conclusion") - for installer, install_dir in create_installer(input_path, tmp_path): - _run_installer(input_path, installer, install_dir, request=request) + output_dir = build_installer(input_path, tmp_path) + installer, install_dir = get_installer(output_dir, input_path, installer_type, tmp_path) + _run_installer(input_path, installer, install_dir, request=request) @pytest.mark.parametrize("extra_pages", ("str", "list")) @pytest.mark.skipif(sys.platform != "win32", reason="Windows only") -def test_example_extra_pages_win(tmp_path, request, extra_pages, monkeypatch): +@pytest.mark.installer_types("exe") +def test_example_extra_pages_win(tmp_path, request, extra_pages, installer_type, monkeypatch): if extra_pages == "list": monkeypatch.setenv("POST_INSTALL_PAGES_LIST", "1") input_path = _example_path("exe_extra_pages") - for installer, install_dir in create_installer(input_path, tmp_path): - _run_installer(input_path, installer, install_dir, request=request) + output_dir = build_installer(input_path, tmp_path) + installer, install_dir = get_installer(output_dir, input_path, installer_type, tmp_path) + _run_installer(input_path, installer, install_dir, request=request) -def test_example_extra_envs(tmp_path, request): +def test_example_extra_envs(tmp_path, request, installer_type): input_path = _example_path("extra_envs") - for installer, install_dir in create_installer(input_path, tmp_path): - _run_installer(input_path, installer, install_dir, request=request, uninstall=False) - assert ( - "@EXPLICIT" in (install_dir / "conda-meta" / "initial-state.explicit.txt").read_text() - ) - for env in install_dir.glob("envs/*/conda-meta/"): - envtxt = env / "initial-state.explicit.txt" - assert envtxt.exists() - assert "@EXPLICIT" in envtxt.read_text() + output_dir = build_installer(input_path, tmp_path) + installer, install_dir = get_installer(output_dir, input_path, installer_type, tmp_path) + _run_installer(input_path, installer, install_dir, request=request, uninstall=False) + assert ( + "@EXPLICIT" in (install_dir / "conda-meta" / "initial-state.explicit.txt").read_text() + ) + for env in install_dir.glob("envs/*/conda-meta/"): + envtxt = env / "initial-state.explicit.txt" + assert envtxt.exists() + assert "@EXPLICIT" in envtxt.read_text() - if sys.platform.startswith("win"): - _run_uninstaller_exe(install_dir=install_dir) + if sys.platform.startswith("win"): + _run_uninstaller_exe(install_dir=install_dir) -def test_example_extra_files(tmp_path, request): +def test_example_extra_files(tmp_path, request, installer_type): input_path = _example_path("extra_files") - for installer, install_dir in create_installer(input_path, tmp_path, with_spaces=True): - _run_installer(input_path, installer, install_dir, request=request) + output_dir = build_installer(input_path, tmp_path, with_spaces=True) + installer, install_dir = get_installer( + output_dir, input_path, installer_type, tmp_path, with_spaces=True + ) + _run_installer(input_path, installer, install_dir, request=request) -def test_example_mirrored_channels(tmp_path, request): +def test_example_mirrored_channels(tmp_path, request, installer_type): input_path = _example_path("mirrored_channels") - for installer, install_dir in create_installer(input_path, tmp_path): - _run_installer(input_path, installer, install_dir, request=request, uninstall=False) - - expected_condarc = { - "channels": ["conda-forge"], - "mirrored_channels": { - "conda-forge": [ - "https://conda.anaconda.org/conda-forge", - "https://conda.anaconda.org/mirror1", - "https://conda.anaconda.org/mirror2", - ] - }, - } + output_dir = build_installer(input_path, tmp_path) + installer, install_dir = get_installer(output_dir, input_path, installer_type, tmp_path) + _run_installer(input_path, installer, install_dir, request=request, uninstall=False) + + expected_condarc = { + "channels": ["conda-forge"], + "mirrored_channels": { + "conda-forge": [ + "https://conda.anaconda.org/conda-forge", + "https://conda.anaconda.org/mirror1", + "https://conda.anaconda.org/mirror2", + ] + }, + } - condarc_file = install_dir / ".condarc" - assert condarc_file.exists() + condarc_file = install_dir / ".condarc" + assert condarc_file.exists() - with open(condarc_file) as file: - condarc_data = YAML().load(file) + with open(condarc_file) as file: + condarc_data = YAML().load(file) - assert condarc_data == expected_condarc + assert condarc_data == expected_condarc @pytest.mark.xfail( @@ -571,65 +657,77 @@ def test_example_mirrored_channels(tmp_path, request): reason="Known issue with conda-standalone<=23.10: shortcuts are created but not removed.", ) @pytest.mark.parametrize("example", ("miniforge", "miniforge-mamba2")) -def test_example_miniforge(tmp_path, request, example): +def test_example_miniforge(tmp_path, request, example, installer_type): input_path = _example_path(example) - for installer, install_dir in create_installer(input_path, tmp_path): - if installer.suffix == ".sh": - # try both batch and interactive installations - install_dirs = (install_dir / "batch", install_dir / "interactive") - installer_inputs = (None, f"\nyes\n{install_dir / 'interactive'}\nno\nno\n") - else: - install_dirs = (install_dir,) - installer_inputs = (None,) - for installer_input, install_dir in zip(installer_inputs, install_dirs): - _run_installer( - input_path, - installer, - install_dir, - installer_input=installer_input, - request=request, - # PKG installers use their own install path, so we can't check sentinels - # via `install_dir` - check_sentinels=installer.suffix != ".pkg", - uninstall=False, - ) - # Check that key metadata files are in place - assert install_dir.glob("conda-meta/*.json") - assert install_dir.glob("pkgs/cache/*.json") # enables offline installs - if installer.suffix == ".pkg" and ON_CI: - basename = "Miniforge3" if example == "miniforge" else "Miniforge3-mamba2" - _sentinel_file_checks(input_path, Path(os.environ["HOME"]) / basename) - if installer.suffix == ".exe": - for key in ("ProgramData", "AppData"): - start_menu_dir = Path( - os.environ[key], - "Microsoft/Windows/Start Menu/Programs/Miniforge3", - ) - if start_menu_dir.is_dir(): - assert list(start_menu_dir.glob("Miniforge*.lnk")) - break - else: - raise AssertionError("Could not find Start Menu folder for miniforge") - _run_uninstaller_exe(install_dir) - assert not list(start_menu_dir.glob("Miniforge*.lnk")) - - -def test_example_noconda(tmp_path, request): - input_path = _example_path("noconda") - for installer, install_dir in create_installer( - input_path, tmp_path, config_filename="constructor_input.yaml", with_spaces=True - ): + output_dir = build_installer(input_path, tmp_path) + installer, install_dir = get_installer(output_dir, input_path, installer_type, tmp_path) + + if installer.suffix == ".sh": + # try both batch and interactive installations + install_dirs = (install_dir / "batch", install_dir / "interactive") + installer_inputs = (None, f"\nyes\n{install_dir / 'interactive'}\nno\nno\n") + else: + install_dirs = (install_dir,) + installer_inputs = (None,) + + for installer_input, install_dir in zip(installer_inputs, install_dirs): _run_installer( input_path, installer, install_dir, - config_filename="constructor_input.yaml", + installer_input=installer_input, request=request, + # PKG installers use their own install path, so we can't check sentinels + # via `install_dir` + check_sentinels=installer.suffix != ".pkg", + uninstall=False, ) + # Check that key metadata files are in place + assert install_dir.glob("conda-meta/*.json") + assert install_dir.glob("pkgs/cache/*.json") # enables offline installs + if installer.suffix == ".pkg" and ON_CI: + basename = "Miniforge3" if example == "miniforge" else "Miniforge3-mamba2" + _sentinel_file_checks(input_path, Path(os.environ["HOME"]) / basename) + if installer.suffix == ".exe": + for key in ("ProgramData", "AppData"): + start_menu_dir = Path( + os.environ[key], + "Microsoft/Windows/Start Menu/Programs/Miniforge3", + ) + if start_menu_dir.is_dir(): + assert list(start_menu_dir.glob("Miniforge*.lnk")) + break + else: + raise AssertionError("Could not find Start Menu folder for miniforge") + _run_uninstaller_exe(install_dir) + assert not list(start_menu_dir.glob("Miniforge*.lnk")) + + +def test_example_noconda(tmp_path, request, installer_type): + input_path = _example_path("noconda") + output_dir = build_installer( + input_path, tmp_path, config_filename="constructor_input.yaml", with_spaces=True + ) + installer, install_dir = get_installer( + output_dir, + input_path, + installer_type, + tmp_path, + with_spaces=True, + config_filename="constructor_input.yaml", + ) + _run_installer( + input_path, + installer, + install_dir, + config_filename="constructor_input.yaml", + request=request, + ) @pytest.mark.skipif(sys.platform != "darwin", reason="macOS only") -def test_example_osxpkg(tmp_path, request): +@pytest.mark.installer_types("pkg") +def test_example_osxpkg(tmp_path, request, installer_type): input_path = _example_path("osxpkg") ownership_test_files_home = [ ".bash_profile", @@ -648,16 +746,17 @@ def test_example_osxpkg(tmp_path, request): # getpass.getuser is more reliable than os.getlogin: # https://docs.python.org/3/library/os.html#os.getlogin expected_owner = getpass.getuser() - for installer, install_dir in create_installer(input_path, tmp_path): - _run_installer(input_path, installer, install_dir, request=request) - expected = {} - found = {} - for file in ownership_test_files_home: - if not file.exists(): - continue - expected[file] = expected_owner - found[file] = file.owner() - assert expected == found + output_dir = build_installer(input_path, tmp_path) + installer, install_dir = get_installer(output_dir, input_path, installer_type, tmp_path) + _run_installer(input_path, installer, install_dir, request=request) + expected = {} + found = {} + for file in ownership_test_files_home: + if not file.exists(): + continue + expected[file] = expected_owner + found[file] = file.owner() + assert expected == found @pytest.mark.skipif(sys.platform != "darwin", reason="macOS only") @@ -745,10 +844,13 @@ def test_macos_signing(tmp_path, self_signed_application_certificate_macos): assert validated_signatures == components -def test_example_scripts(tmp_path, request): +def test_example_scripts(tmp_path, request, installer_type): input_path = _example_path("scripts") - for installer, install_dir in create_installer(input_path, tmp_path, with_spaces=True): - _run_installer(input_path, installer, install_dir, request=request) + output_dir = build_installer(input_path, tmp_path, with_spaces=True) + installer, install_dir = get_installer( + output_dir, input_path, installer_type, tmp_path, with_spaces=True + ) + _run_installer(input_path, installer, install_dir, request=request) @pytest.mark.skipif( @@ -758,44 +860,46 @@ def test_example_scripts(tmp_path, request): ), reason="menuinst v2 requires conda-standalone>=23.11.0; micromamba is not supported yet", ) -def test_example_shortcuts(tmp_path, request): +def test_example_shortcuts(tmp_path, request, installer_type): input_path = _example_path("shortcuts") - for installer, install_dir in create_installer(input_path, tmp_path): - _run_installer(input_path, installer, install_dir, request=request, uninstall=False) - # check that the shortcuts are created - if sys.platform == "win32": - for key in ("ProgramData", "AppData"): - start_menu = Path(os.environ[key]) / "Microsoft/Windows/Start Menu/Programs" - package_1 = start_menu / "Package 1" - anaconda = start_menu / "Anaconda3 (64-bit)" - if package_1.is_dir() and anaconda.is_dir(): - assert (package_1 / "A.lnk").is_file() - assert (package_1 / "B.lnk").is_file() - # The shortcut created from the 'base' env - # should not exist because we filtered it out in the YAML - # We do expect one shortcut from 'another_env' - assert not (anaconda / "Anaconda Prompt.lnk").is_file() - assert (anaconda / "Anaconda Prompt (another_env).lnk").is_file() - break - else: - raise AssertionError("No shortcuts found!") - _run_uninstaller_exe(install_dir) - assert not (package_1 / "A.lnk").is_file() - assert not (package_1 / "B.lnk").is_file() - elif sys.platform == "darwin": - applications = Path("~/Applications").expanduser() - print("Shortcuts found:", sorted(applications.glob("**/*.app"))) - assert (applications / "A.app").exists() - assert (applications / "B.app").exists() - elif sys.platform == "linux": - applications = Path("~/.local/share/applications").expanduser() - print("Shortcuts found:", sorted(applications.glob("**/*.desktop"))) - assert (applications / "package-1_a.desktop").exists() - assert (applications / "package-1_b.desktop").exists() + output_dir = build_installer(input_path, tmp_path) + installer, install_dir = get_installer(output_dir, input_path, installer_type, tmp_path) + _run_installer(input_path, installer, install_dir, request=request, uninstall=False) + # check that the shortcuts are created + if sys.platform == "win32": + for key in ("ProgramData", "AppData"): + start_menu = Path(os.environ[key]) / "Microsoft/Windows/Start Menu/Programs" + package_1 = start_menu / "Package 1" + anaconda = start_menu / "Anaconda3 (64-bit)" + if package_1.is_dir() and anaconda.is_dir(): + assert (package_1 / "A.lnk").is_file() + assert (package_1 / "B.lnk").is_file() + # The shortcut created from the 'base' env + # should not exist because we filtered it out in the YAML + # We do expect one shortcut from 'another_env' + assert not (anaconda / "Anaconda Prompt.lnk").is_file() + assert (anaconda / "Anaconda Prompt (another_env).lnk").is_file() + break + else: + raise AssertionError("No shortcuts found!") + _run_uninstaller_exe(install_dir) + assert not (package_1 / "A.lnk").is_file() + assert not (package_1 / "B.lnk").is_file() + elif sys.platform == "darwin": + applications = Path("~/Applications").expanduser() + print("Shortcuts found:", sorted(applications.glob("**/*.app"))) + assert (applications / "A.app").exists() + assert (applications / "B.app").exists() + elif sys.platform == "linux": + applications = Path("~/.local/share/applications").expanduser() + print("Shortcuts found:", sorted(applications.glob("**/*.desktop"))) + assert (applications / "package-1_a.desktop").exists() + assert (applications / "package-1_b.desktop").exists() @pytest.mark.skipif(sys.platform != "win32", reason="Windows only") -def test_example_signing(tmp_path, request): +@pytest.mark.installer_types("exe") +def test_example_signing(tmp_path, request, installer_type): input_path = _example_path("signing") cert_path = tmp_path / "self-signed-cert.pfx" cert_pwd = "1234" @@ -804,14 +908,17 @@ def test_example_signing(tmp_path, request): certificate_in_input_dir = input_path / "certificate.pfx" shutil.copy(str(cert_path), str(certificate_in_input_dir)) request.addfinalizer(lambda: certificate_in_input_dir.unlink()) - for installer, install_dir in create_installer( + output_dir = build_installer( input_path, tmp_path, with_spaces=True, CONSTRUCTOR_SIGNING_CERTIFICATE=str(cert_path), CONSTRUCTOR_PFX_CERTIFICATE_PASSWORD=cert_pwd, - ): - _run_installer(input_path, installer, install_dir, request=request) + ) + installer, install_dir = get_installer( + output_dir, input_path, installer_type, tmp_path, with_spaces=True + ) + _run_installer(input_path, installer, install_dir, request=request) @pytest.mark.skipif(sys.platform != "win32", reason="Windows only") @@ -823,7 +930,8 @@ def test_example_signing(tmp_path, request): "auth_method", os.environ.get("AZURE_SIGNTOOL_TEST_AUTH_METHODS", "token,secret").split(","), ) -def test_azure_signtool(tmp_path, request, monkeypatch, auth_method): +@pytest.mark.installer_types("exe") +def test_azure_signtool(tmp_path, request, monkeypatch, auth_method, installer_type): """Test signing installers with AzureSignTool. There are three ways to authenticate with Azure: tokens, secrets, and managed identities. @@ -844,88 +952,92 @@ def test_azure_signtool(tmp_path, request, monkeypatch, auth_method): else: pytest.skip(f"Unknown authentication method {auth_method}.") input_path = _example_path("azure_signtool") - for installer, install_dir in create_installer( - input_path, - tmp_path, - ): - _run_installer(input_path, installer, install_dir, request=request) + output_dir = build_installer(input_path, tmp_path) + installer, install_dir = get_installer(output_dir, input_path, installer_type, tmp_path) + _run_installer(input_path, installer, install_dir, request=request) -def test_example_use_channel_remap(tmp_path, request): +def test_example_use_channel_remap(tmp_path, request, installer_type): input_path = _example_path("use_channel_remap") - for installer, install_dir in create_installer(input_path, tmp_path): - _run_installer(input_path, installer, install_dir, request=request, uninstall=False) - p = subprocess.run( - [sys.executable, "-m", "conda", "list", "--prefix", install_dir, "--json"], - capture_output=True, - text=True, - ) - packages = json.loads(p.stdout) - for pkg in packages: - assert pkg["channel"] == "private_repo" + output_dir = build_installer(input_path, tmp_path) + installer, install_dir = get_installer(output_dir, input_path, installer_type, tmp_path) + _run_installer(input_path, installer, install_dir, request=request, uninstall=False) + p = subprocess.run( + [sys.executable, "-m", "conda", "list", "--prefix", install_dir, "--json"], + capture_output=True, + text=True, + ) + packages = json.loads(p.stdout) + for pkg in packages: + assert pkg["channel"] == "private_repo" -def test_example_from_existing_env(tmp_path, request): +def test_example_from_existing_env(tmp_path, request, installer_type): input_path = _example_path("from_existing_env") subprocess.check_call( [sys.executable, "-mconda", "create", "-p", tmp_path / "env", "-y", "python"] ) - for installer, install_dir in create_installer( + output_dir = build_installer( input_path, tmp_path, CONSTRUCTOR_TEST_EXISTING_ENV=str(tmp_path / "env"), - ): - _run_installer(input_path, installer, install_dir, request=request) - if installer.suffix == ".pkg" and not ON_CI: - return - for pkg in PrefixData(install_dir, pip_interop_enabled=True).iter_records(): - assert pkg["channel"] != "pypi" + ) + installer, install_dir = get_installer(output_dir, input_path, installer_type, tmp_path) + _run_installer(input_path, installer, install_dir, request=request) + if installer.suffix == ".pkg" and not ON_CI: + return + for pkg in PrefixData(install_dir, pip_interop_enabled=True).iter_records(): + assert pkg["channel"] != "pypi" -def test_example_from_env_txt(tmp_path, request): +def test_example_from_env_txt(tmp_path, request, installer_type): input_path = _example_path("from_env_txt") - for installer, install_dir in create_installer(input_path, tmp_path): - _run_installer(input_path, installer, install_dir, request=request) - if installer.suffix == ".pkg" and not ON_CI: - return - for pkg in PrefixData(install_dir, pip_interop_enabled=True).iter_records(): - assert pkg["channel"] != "pypi" + output_dir = build_installer(input_path, tmp_path) + installer, install_dir = get_installer(output_dir, input_path, installer_type, tmp_path) + _run_installer(input_path, installer, install_dir, request=request) + if installer.suffix == ".pkg" and not ON_CI: + return + for pkg in PrefixData(install_dir, pip_interop_enabled=True).iter_records(): + assert pkg["channel"] != "pypi" -def test_example_from_env_yaml(tmp_path, request): +def test_example_from_env_yaml(tmp_path, request, installer_type): input_path = _example_path("from_env_yaml") - for installer, install_dir in create_installer(input_path, tmp_path, timeout=600): - _run_installer(input_path, installer, install_dir, request=request) - if installer.suffix == ".pkg" and not ON_CI: - return - for pkg in PrefixData(install_dir, pip_interop_enabled=True).iter_records(): - assert pkg["channel"] != "pypi" + output_dir = build_installer(input_path, tmp_path, timeout=600) + installer, install_dir = get_installer(output_dir, input_path, installer_type, tmp_path) + _run_installer(input_path, installer, install_dir, request=request) + if installer.suffix == ".pkg" and not ON_CI: + return + for pkg in PrefixData(install_dir, pip_interop_enabled=True).iter_records(): + assert pkg["channel"] != "pypi" @pytest.mark.skipif(context.subdir != "linux-64", reason="Linux x64 only") -def test_example_from_explicit(tmp_path, request): +def test_example_from_explicit(tmp_path, request, installer_type): input_path = _example_path("from_explicit") - for installer, install_dir in create_installer(input_path, tmp_path): - _run_installer(input_path, installer, install_dir, request=request) - if installer.suffix == ".pkg" and not ON_CI: - return - out = subprocess.check_output( - [sys.executable, "-mconda", "list", "-p", install_dir, "--explicit", "--md5"], - text=True, - ) - expected = (input_path / "explicit_linux-64.txt").read_text() - # Filter comments - out = [line for line in out.split("\n") if not line.startswith("#")] - expected = [line for line in expected.split("\n") if not line.startswith("#")] - assert out == expected + output_dir = build_installer(input_path, tmp_path) + installer, install_dir = get_installer(output_dir, input_path, installer_type, tmp_path) + _run_installer(input_path, installer, install_dir, request=request) + if installer.suffix == ".pkg" and not ON_CI: + return + out = subprocess.check_output( + [sys.executable, "-mconda", "list", "-p", install_dir, "--explicit", "--md5"], + text=True, + ) + expected = (input_path / "explicit_linux-64.txt").read_text() + # Filter comments + out = [line for line in out.split("\n") if not line.startswith("#")] + expected = [line for line in expected.split("\n") if not line.startswith("#")] + assert out == expected -def test_register_envs(tmp_path, request): +def test_register_envs(tmp_path, request, installer_type): input_path = _example_path("register_envs") - for installer, install_dir in create_installer(input_path, tmp_path): - _run_installer(input_path, installer, install_dir, request=request) - environments_txt = Path("~/.conda/environments.txt").expanduser().read_text() - assert str(install_dir) not in environments_txt + output_dir = build_installer(input_path, tmp_path) + installer, install_dir = get_installer(output_dir, input_path, installer_type, tmp_path) + _run_installer(input_path, installer, install_dir, request=request) + environments_txt = Path("~/.conda/environments.txt").expanduser().read_text() + assert str(install_dir) not in environments_txt @pytest.mark.skipif(sys.platform != "darwin", reason="MacOS only") @@ -974,7 +1086,7 @@ def test_cross_osx_building(tmp_path): ], ) micromamba_arm64 = tmp_env / "bin" / "micromamba" - create_installer( + build_installer( input_path, tmp_path, conda_exe=micromamba_arm64, @@ -997,135 +1109,139 @@ def test_cross_build_example(tmp_path, platform_conda_exe): assert installer.exists() -def test_virtual_specs_failed(tmp_path, request): +def test_virtual_specs_failed(tmp_path, request, installer_type): input_path = _example_path("virtual_specs_failed") - for installer, install_dir in create_installer(input_path, tmp_path): - process = _run_installer( - input_path, - installer, - install_dir, - request=request, - check_subprocess=False, - uninstall=False, - ) - # This example is configured to fail due to unsatisfiable virtual specs - if installer.suffix == ".exe": - with pytest.raises(AssertionError, match="Failed to check virtual specs"): - _check_installer_log(install_dir) - continue - elif installer.suffix == ".pkg": - if not ON_CI: - continue - # The GUI does provide a better message with the min version and so on - # but on the CLI we fail with this one instead - msg = "Cannot install on volume" - else: - # The shell installer has its own Bash code for __glibc and __osx - # Other virtual specs like __cuda are checked by conda-standalone/micromamba - # and will fail with solver errors like PackagesNotFound etc - msg = "Installer requires" + output_dir = build_installer(input_path, tmp_path) + installer, install_dir = get_installer(output_dir, input_path, installer_type, tmp_path) + process = _run_installer( + input_path, + installer, + install_dir, + request=request, + check_subprocess=False, + uninstall=False, + ) + # This example is configured to fail due to unsatisfiable virtual specs + if installer.suffix == ".exe": + with pytest.raises(AssertionError, match="Failed to check virtual specs"): + _check_installer_log(install_dir) + elif installer.suffix == ".pkg": + if not ON_CI: + return + # The GUI does provide a better message with the min version and so on + # but on the CLI we fail with this one instead + assert process.returncode != 0 + assert "Cannot install on volume" in process.stdout + process.stderr + else: + # The shell installer has its own Bash code for __glibc and __osx + # Other virtual specs like __cuda are checked by conda-standalone/micromamba + # and will fail with solver errors like PackagesNotFound etc assert process.returncode != 0 - assert msg in process.stdout + process.stderr + assert "Installer requires" in process.stdout + process.stderr -def test_virtual_specs_ok(tmp_path, request): +def test_virtual_specs_ok(tmp_path, request, installer_type): input_path = _example_path("virtual_specs_ok") - for installer, install_dir in create_installer(input_path, tmp_path): - _run_installer( - input_path, - installer, - install_dir, - request=request, - check_subprocess=True, - uninstall=True, - ) + output_dir = build_installer(input_path, tmp_path) + installer, install_dir = get_installer(output_dir, input_path, installer_type, tmp_path) + _run_installer( + input_path, + installer, + install_dir, + request=request, + check_subprocess=True, + uninstall=True, + ) @pytest.mark.skipif(sys.platform.startswith("win"), reason="Unix only") -def test_virtual_specs_override(tmp_path, request, monkeypatch): +@pytest.mark.installer_types("sh") +def test_virtual_specs_override(tmp_path, request, monkeypatch, installer_type): input_path = _example_path("virtual_specs_failed") - for installer, install_dir in create_installer(input_path, tmp_path): - if installer.name.endswith(".pkg"): - continue - monkeypatch.setenv("CONDA_OVERRIDE_GLIBC", "20") - monkeypatch.setenv("CONDA_OVERRIDE_OSX", "30") - _run_installer( - input_path, - installer, - install_dir, - request=request, - check_subprocess=True, - uninstall=True, - ) + output_dir = build_installer(input_path, tmp_path) + installer, install_dir = get_installer(output_dir, input_path, installer_type, tmp_path) + monkeypatch.setenv("CONDA_OVERRIDE_GLIBC", "20") + monkeypatch.setenv("CONDA_OVERRIDE_OSX", "30") + _run_installer( + input_path, + installer, + install_dir, + request=request, + check_subprocess=True, + uninstall=True, + ) @pytest.mark.skipif(not ON_CI, reason="Run on CI only") @pytest.mark.parametrize("method", ("classic", "condabin", True, False)) -def test_initialization(tmp_path, request, monkeypatch, method): +def test_initialization(tmp_path, request, monkeypatch, method, installer_type): request.addfinalizer( lambda: subprocess.run([sys.executable, "-m", "conda", "init", "--reverse"]) ) monkeypatch.setenv("initialization_method", str(method).lower()) input_path = _example_path("initialization") initialize = method is not False - for installer, install_dir in create_installer(input_path, tmp_path): - if installer.suffix == ".sh" and initialize: - options = ["-c"] - elif installer.suffix == ".exe": - # GHA runs on an admin user account, but AllUsers (admin) installs - # do not add to PATH due to CVE-2022-26526, so force single user install - options = ["/AddToPath=1", "/InstallationType=JustMe"] - else: - options = [] - _run_installer( - input_path, - installer, - install_dir, - request=request, - check_subprocess=True, - uninstall=False, - options=options, - ) - if installer.suffix == ".exe": - try: - paths = [] - for root, keyname in ( - (winreg.HKEY_CURRENT_USER, r"Environment"), - ( - winreg.HKEY_LOCAL_MACHINE, - r"SYSTEM\CurrentControlSet\Control\Session Manager\Environment", - ), - ): - with winreg.OpenKey(root, keyname, 0, winreg.KEY_QUERY_VALUE) as key: - value = winreg.QueryValueEx(key, "PATH")[0] - paths += value.strip().split(os.pathsep) - if method == "condabin": - assert (str(install_dir / "condabin") in paths) == initialize - else: - assert (str(install_dir) in paths) == initialize - assert (str(install_dir / "Scripts") in paths) == initialize - assert (str(install_dir / "Library" / "bin") in paths) == initialize - - finally: - _run_uninstaller_exe(install_dir, check=True) - else: - # GHA's Ubuntu needs interactive, but macOS wants login :shrug: - login_flag = "-i" if sys.platform.startswith("linux") else "-l" - out = subprocess.check_output( - [os.environ.get("SHELL", "bash"), login_flag, "-c", "echo $PATH"], - text=True, - ) + output_dir = build_installer(input_path, tmp_path) + installer, install_dir = get_installer(output_dir, input_path, installer_type, tmp_path) + + if installer.suffix == ".sh" and initialize: + options = ["-c"] + elif installer.suffix == ".exe": + # GHA runs on an admin user account, but AllUsers (admin) installs + # do not add to PATH due to CVE-2022-26526, so force single user install + options = ["/AddToPath=1", "/InstallationType=JustMe"] + else: + options = [] + _run_installer( + input_path, + installer, + install_dir, + request=request, + check_subprocess=True, + uninstall=False, + options=options, + ) + if installer.suffix == ".exe": + try: + paths = [] + for root, keyname in ( + (winreg.HKEY_CURRENT_USER, r"Environment"), + ( + winreg.HKEY_LOCAL_MACHINE, + r"SYSTEM\CurrentControlSet\Control\Session Manager\Environment", + ), + ): + with winreg.OpenKey(root, keyname, 0, winreg.KEY_QUERY_VALUE) as key: + value = winreg.QueryValueEx(key, "PATH")[0] + paths += value.strip().split(os.pathsep) if method == "condabin": - assert ( - str(install_dir / "condabin") in out.strip().split(os.pathsep) - ) == initialize + assert (str(install_dir / "condabin") in paths) == initialize else: - assert (str(install_dir / "bin") in out.strip().split(os.pathsep)) == initialize + assert (str(install_dir) in paths) == initialize + assert (str(install_dir / "Scripts") in paths) == initialize + assert (str(install_dir / "Library" / "bin") in paths) == initialize + + finally: + _run_uninstaller_exe(install_dir, check=True) + else: + # GHA's Ubuntu needs interactive, but macOS wants login :shrug: + login_flag = "-i" if sys.platform.startswith("linux") else "-l" + out = subprocess.check_output( + [os.environ.get("SHELL", "bash"), login_flag, "-c", "echo $PATH"], + text=True, + ) + if method == "condabin": + assert ( + str(install_dir / "condabin") in out.strip().split(os.pathsep) + ) == initialize + else: + assert (str(install_dir / "bin") in out.strip().split(os.pathsep)) == initialize @pytest.mark.skipif(not ON_CI, reason="CI only") @pytest.mark.skipif(not sys.platform.startswith("win"), reason="Windows only") -def test_allusers_exe(tmp_path, request): +@pytest.mark.installer_types("exe") +def test_allusers_exe(tmp_path, request, installer_type): """Ensure that AllUsers installations have the correct permissions for built-in users, domain users, and authenticated users. @@ -1203,83 +1319,84 @@ def _get_dacl_information(filepath: Path) -> dict: return dacl_info input_path = _example_path("miniforge") - for installer, install_dir in create_installer(input_path, tmp_path): - _run_installer( - input_path, - installer, - install_dir, - request=request, - check_subprocess=True, - uninstall=False, - options=["/InstallationType=AllUsers"], - ) - - # Test the installation directory - dacl = _get_dacl_information(install_dir) - assert dacl["protected"], "Installation directory must not inherit permissions." - assert len(dacl["permissions"].keys()) > 0, ( - "Directory permission must include either domain or built-in users" - ) - for acct in SDDL_ABBREVIATIONS: - permissions = dacl["permissions"].get(acct) - if permissions is None: - continue - assert not permissions["write_access"], ( - f"Installation directory must not be writable by {acct}." - ) - if acct == "AU": - continue - assert permissions["generic_execute"] and permissions["generic_read"], ( - f"Installation directory must be readable and executable by {acct}" - ) + output_dir = build_installer(input_path, tmp_path) + installer, install_dir = get_installer(output_dir, input_path, installer_type, tmp_path) + _run_installer( + input_path, + installer, + install_dir, + request=request, + check_subprocess=True, + uninstall=False, + options=["/InstallationType=AllUsers"], + ) - # Test all files inside installation directory - incorrect_permissions = { - "protected": [], - "not_inherited": [], - "write_access": {acct: [] for acct in SDDL_ABBREVIATIONS}, - "bad_read_exec": {acct: [] for acct in SDDL_ABBREVIATIONS if acct != "AU"}, - "not_set": [], - } - for file in install_dir.glob("**/*"): - dacl = _get_dacl_information(file) - if dacl["protected"]: - incorrect_permissions["protected"].append(file) - if not dacl["inherited"]: - incorrect_permissions["not_inherited"].append(file) - if len(dacl["permissions"].keys()) == 0: - incorrect_permissions["not_set"].append(file) - continue - for acct, files in incorrect_permissions["write_access"].items(): - permissions = dacl["permissions"].get(acct) - if permissions is not None and permissions["write_access"]: - files.append(file) - for acct, files in incorrect_permissions["bad_read_exec"].items(): - permissions = dacl["permissions"].get(acct) - if permissions is not None and not ( - permissions["generic_execute"] and permissions["generic_read"] - ): - files.append(file) - assert incorrect_permissions["protected"] == [], ( - "Files must not be protected from inheriting permissions" - ) - assert incorrect_permissions["not_inherited"] == [], ( - "Files must inherit from installation directory" + # Test the installation directory + dacl = _get_dacl_information(install_dir) + assert dacl["protected"], "Installation directory must not inherit permissions." + assert len(dacl["permissions"].keys()) > 0, ( + "Directory permission must include either domain or built-in users" + ) + for acct in SDDL_ABBREVIATIONS: + permissions = dacl["permissions"].get(acct) + if permissions is None: + continue + assert not permissions["write_access"], ( + f"Installation directory must not be writable by {acct}." ) - assert incorrect_permissions["not_set"] == [], ( - "File permission must include either domain or built-in users" + if acct == "AU": + continue + assert permissions["generic_execute"] and permissions["generic_read"], ( + f"Installation directory must be readable and executable by {acct}" ) + + # Test all files inside installation directory + incorrect_permissions = { + "protected": [], + "not_inherited": [], + "write_access": {acct: [] for acct in SDDL_ABBREVIATIONS}, + "bad_read_exec": {acct: [] for acct in SDDL_ABBREVIATIONS if acct != "AU"}, + "not_set": [], + } + for file in install_dir.glob("**/*"): + dacl = _get_dacl_information(file) + if dacl["protected"]: + incorrect_permissions["protected"].append(file) + if not dacl["inherited"]: + incorrect_permissions["not_inherited"].append(file) + if len(dacl["permissions"].keys()) == 0: + incorrect_permissions["not_set"].append(file) + continue for acct, files in incorrect_permissions["write_access"].items(): - assert files == [], f"Files must not have write access for {acct}" + permissions = dacl["permissions"].get(acct) + if permissions is not None and permissions["write_access"]: + files.append(file) for acct, files in incorrect_permissions["bad_read_exec"].items(): - assert files == [], f"Files must have generic execute and read for {acct}" + permissions = dacl["permissions"].get(acct) + if permissions is not None and not ( + permissions["generic_execute"] and permissions["generic_read"] + ): + files.append(file) + assert incorrect_permissions["protected"] == [], ( + "Files must not be protected from inheriting permissions" + ) + assert incorrect_permissions["not_inherited"] == [], ( + "Files must inherit from installation directory" + ) + assert incorrect_permissions["not_set"] == [], ( + "File permission must include either domain or built-in users" + ) + for acct, files in incorrect_permissions["write_access"].items(): + assert files == [], f"Files must not have write access for {acct}" + for acct, files in incorrect_permissions["bad_read_exec"].items(): + assert files == [], f"Files must have generic execute and read for {acct}" @pytest.mark.xfail( CONDA_EXE == StandaloneExe.CONDA and not check_version(CONDA_EXE_VERSION, min_version="24.9.0"), reason="Pre-existing .condarc breaks installation", ) -def test_ignore_condarc_files(tmp_path, monkeypatch, request): +def test_ignore_condarc_files(tmp_path, monkeypatch, request, installer_type): # Create a bogus .condarc file that would result in errors if read. # conda searches inside XDG_CONFIG_HOME on all systems, which is a # a safer directory to monkeypatch, especially on Windows where patching @@ -1307,22 +1424,23 @@ def test_ignore_condarc_files(tmp_path, monkeypatch, request): construct_yaml = input_path / "construct.yaml" content = construct_yaml.read_text() construct_yaml.write_text(content.replace("name: NoCondaOptions", "name: NoCondaRC")) - for installer, install_dir in create_installer(input_path, tmp_path): - proc = _run_installer( - input_path, - installer, - install_dir, - request=request, - check_subprocess=True, - uninstall=True, - ) - if CONDA_EXE == StandaloneExe.MAMBA and installer.suffix == ".sh": - # micromamba loads the rc files even for constructor subcommands. - # This cannot be turned off with --no-rc, which causes four errors - # in stderr. If there are more, other micromamba calls have read - # the bogus .condarc file. - # pkg installers unfortunately do not output any errors into the log. - assert proc.stderr.count("Bad conversion of configurable") == 4 + output_dir = build_installer(input_path, tmp_path) + installer, install_dir = get_installer(output_dir, input_path, installer_type, tmp_path) + proc = _run_installer( + input_path, + installer, + install_dir, + request=request, + check_subprocess=True, + uninstall=True, + ) + if CONDA_EXE == StandaloneExe.MAMBA and installer.suffix == ".sh": + # micromamba loads the rc files even for constructor subcommands. + # This cannot be turned off with --no-rc, which causes four errors + # in stderr. If there are more, other micromamba calls have read + # the bogus .condarc file. + # pkg installers unfortunately do not output any errors into the log. + assert proc.stderr.count("Bad conversion of configurable") == 4 @pytest.mark.skipif( @@ -1340,19 +1458,22 @@ def test_ignore_condarc_files(tmp_path, monkeypatch, request): pytest.param(False, False, "user", id="remove user .condarc files"), ), ) +@pytest.mark.installer_types("exe") def test_uninstallation_standalone( monkeypatch, remove_user_data: bool, remove_caches: bool, remove_config_files: str | None, tmp_path: Path, + installer_type: str, ): recipe_path = _example_path("customize_controls") input_path = tmp_path / "input" shutil.copytree(str(recipe_path), str(input_path)) with open(input_path / "construct.yaml", "a") as construct: construct.write("uninstall_with_conda_exe: true\n") - installer, install_dir = next(create_installer(input_path, tmp_path)) + output_dir = build_installer(input_path, tmp_path) + installer, install_dir = get_installer(output_dir, input_path, installer_type, tmp_path) monkeypatch.setenv("USERPROFILE", str(tmp_path)) _run_installer( input_path, @@ -1403,62 +1524,66 @@ def test_uninstallation_standalone( shutil.rmtree(system_rc.parent) -def test_output_files(tmp_path): +def test_output_files(tmp_path, installer_type): input_path = _example_path("outputs") - for installer, _ in create_installer(input_path, tmp_path): - files_expected = [ - f"{installer.name}.md5", - f"{installer.name}.sha256", - "info.json", - "licenses.json", - "pkg-list.base.txt", - "pkg-list.py310.txt", - "lockfile.base.txt", - "lockfile.py310.txt", - ] - files_not_expected = [ - "pkg-list.py311.txt", - "lockfile.py311.txt", - ] - root_path = installer.parent - files_exist = [file for file in files_expected if (root_path / file).exists()] - assert sorted(files_exist) == sorted(files_expected) - files_exist = [file for file in files_not_expected if (root_path / file).exists()] - assert files_exist == [] + output_dir = build_installer(input_path, tmp_path) + installer, _ = get_installer(output_dir, input_path, installer_type, tmp_path) + files_expected = [ + f"{installer.name}.md5", + f"{installer.name}.sha256", + "info.json", + "licenses.json", + "pkg-list.base.txt", + "pkg-list.py310.txt", + "lockfile.base.txt", + "lockfile.py310.txt", + ] + files_not_expected = [ + "pkg-list.py311.txt", + "lockfile.py311.txt", + ] + root_path = installer.parent + files_exist = [file for file in files_expected if (root_path / file).exists()] + assert sorted(files_exist) == sorted(files_expected) + files_exist = [file for file in files_not_expected if (root_path / file).exists()] + assert files_exist == [] -def test_regressions(tmp_path, request): +def test_regressions(tmp_path, request, installer_type): input_path = _example_path("regressions") - for installer, install_dir in create_installer(input_path, tmp_path): - _run_installer( - input_path, - installer, - install_dir, - request=request, - check_subprocess=True, - uninstall=True, - ) + output_dir = build_installer(input_path, tmp_path) + installer, install_dir = get_installer(output_dir, input_path, installer_type, tmp_path) + _run_installer( + input_path, + installer, + install_dir, + request=request, + check_subprocess=True, + uninstall=True, + ) @pytest.mark.parametrize("no_registry", (0, 1)) @pytest.mark.skipif(not ON_CI, reason="CI only") @pytest.mark.skipif(not sys.platform.startswith("win"), reason="Windows only") -def test_not_in_installed_menu_list_(tmp_path, request, no_registry): +@pytest.mark.installer_types("exe") +def test_not_in_installed_menu_list_(tmp_path, request, no_registry, installer_type): """Verify the app is in the Installed Apps Menu (or not), based on the CLI arg '/NoRegistry'. If NoRegistry=0, we expect to find the installer in the Menu, otherwise not. """ input_path = _example_path("extra_files") # The specific example we use here is not important options = ["/InstallationType=JustMe", f"/NoRegistry={no_registry}"] - for installer, install_dir in create_installer(input_path, tmp_path): - _run_installer( - input_path, - installer, - install_dir, - request=request, - check_subprocess=True, - uninstall=False, - options=options, - ) + output_dir = build_installer(input_path, tmp_path) + installer, install_dir = get_installer(output_dir, input_path, installer_type, tmp_path) + _run_installer( + input_path, + installer, + install_dir, + request=request, + check_subprocess=True, + uninstall=False, + options=options, + ) # Use the installer file name for the registry search installer_file_name_parts = Path(installer).name.split("-") @@ -1491,11 +1616,11 @@ def test_not_in_installed_menu_list_(tmp_path, request, no_registry): pytest.param(False, id="without-conflict"), ), ) -def test_frozen_environment(tmp_path, request, has_conflict): +def test_frozen_environment(tmp_path, request, has_conflict, installer_type): example_path = _example_path("protected_base") input_path = tmp_path / "input" - context = pytest.raises(subprocess.CalledProcessError) if has_conflict else nullcontext() + ctx = pytest.raises(subprocess.CalledProcessError) if has_conflict else nullcontext() shutil.copytree(str(example_path), str(input_path)) @@ -1508,20 +1633,21 @@ def test_frozen_environment(tmp_path, request, has_conflict): with open(input_path / "construct.yaml", "w") as f: yaml.dump(config, f) - with context as c: - for installer, install_dir in create_installer(input_path, tmp_path): - _run_installer(input_path, installer, install_dir, request=request, uninstall=False) + with ctx as c: + output_dir = build_installer(input_path, tmp_path) + installer, install_dir = get_installer(output_dir, input_path, installer_type, tmp_path) + _run_installer(input_path, installer, install_dir, request=request, uninstall=False) - expected_frozen = { - install_dir / "conda-meta" / "frozen": config["freeze_base"]["conda"], - install_dir / "envs" / "env1" / "conda-meta" / "frozen": config["extra_envs"][ - "env1" - ]["freeze_env"]["conda"], - } + expected_frozen = { + install_dir / "conda-meta" / "frozen": config["freeze_base"]["conda"], + install_dir / "envs" / "env1" / "conda-meta" / "frozen": config["extra_envs"][ + "env1" + ]["freeze_env"]["conda"], + } - for frozen_path, expected_content in expected_frozen.items(): - assert frozen_path.is_file() - assert json.loads(frozen_path.read_text()) == expected_content + for frozen_path, expected_content in expected_frozen.items(): + assert frozen_path.is_file() + assert json.loads(frozen_path.read_text()) == expected_content if has_conflict: assert all(