From 9300f193e5d2a2dcfe4e48ff766743a5d7c00939 Mon Sep 17 00:00:00 2001 From: Ben Mares Date: Fri, 21 Feb 2025 13:00:32 +0100 Subject: [PATCH 1/2] Add regression test for dev dependencies --- tests/test_regression.py | 211 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 209 insertions(+), 2 deletions(-) diff --git a/tests/test_regression.py b/tests/test_regression.py index 4d1ef4ac2..0a30bf995 100644 --- a/tests/test_regression.py +++ b/tests/test_regression.py @@ -2,17 +2,21 @@ break the functionality of conda-lock. This is a regression test suite.""" import io +import itertools import logging import shutil +import sys import textwrap from pathlib import Path from textwrap import dedent -from typing import List, Union +from typing import List, Optional, Union import pytest -from conda_lock.conda_lock import run_lock +from click.testing import CliRunner + +from conda_lock.conda_lock import main, run_lock from conda_lock.invoke_conda import _stderr_to_log, is_micromamba from conda_lock.lookup import DEFAULT_MAPPING_URL from conda_lock.models.lock_spec import VersionedDependency @@ -242,3 +246,206 @@ def test_stderr_to_log_gh770( assert ( record.message == expected_message ), f"Expected message '{expected_message}' but got '{record.message}'" + + +@pytest.fixture +def categories_environment_files(tmp_path: Path) -> List[Path]: + """Create test environment files with dependencies in different categories. + + We set up three environment files corresponding to three categories: + - main containing tzcode + - dev containing pixi + - mm containing micromamba + """ + # Main environment file (no category specified = main) + main_content = """ + channels: + - conda-forge + dependencies: + - tzcode + """ + main_file = tmp_path / "environment.yml" + main_file.write_text(textwrap.dedent(main_content)) + + # Dev environment file + dev_content = """ + channels: + - conda-forge + category: dev + dependencies: + - pixi + """ + dev_file = tmp_path / "environment-dev.yml" + dev_file.write_text(textwrap.dedent(dev_content)) + + # Custom extra environment file + mm_content = """ + channels: + - conda-forge + category: mm + dependencies: + - micromamba + """ + mm_file = tmp_path / "environment-mm.yml" + mm_file.write_text(textwrap.dedent(mm_content)) + + return [main_file, dev_file, mm_file] + + +dev_deps_and_extras_cli_regression = list( + itertools.product( + [None, True, False], # dev_deps + [False, True], # filter_cats + [[], ["dev"], ["mm"], ["dev", "mm"]], # extras + ) +) + + +def make_dev_deps_and_extras_cli_regression_id( + dev_deps: Optional[bool], filter_cats: bool, extras: List[str] +) -> str: + dev = ( + "" + if dev_deps is None + else "--dev-dependencies" + if dev_deps + else "--no-dev-dependencies" + ) + filter = "--filter-categories" if filter_cats else "" + extra = "--category=" + ",".join(extras) if extras else "" + nonempty_args = [arg for arg in [dev, filter, extra] if arg] + return "_".join(nonempty_args) or "no_args" + + +@pytest.mark.parametrize( + "dev_deps,filter_cats,extras", + dev_deps_and_extras_cli_regression, + ids=[ + make_dev_deps_and_extras_cli_regression_id(d, f, e) + for d, f, e in dev_deps_and_extras_cli_regression + ], +) +def test_dev_deps_and_extras_cli_regression( + monkeypatch: "pytest.MonkeyPatch", + categories_environment_files: List[Path], + mamba_exe: Path, + capsys: "pytest.CaptureFixture[str]", + dev_deps: Optional[bool], + filter_cats: bool, + extras: List[str], +): + """Test conda-lock's handling of dev dependencies, category filtering, and extras. + + This test verifies: + 1. The {dev-dependencies} template variable in filenames correctly reflects the dev + dependencies setting: + - "true" when dev_deps is None (default) or True + - "false" when dev_deps is False + + 2. Package inclusion based on categories and CLI options: + - Main category (tzcode) is always included + - Dev category (pixi) is included when: + * dev_deps is None or True (default behavior), or + * "dev" is in extras + - Custom category (micromamba) is included only when "mm" is in extras + + 3. File generation: + - Exactly one output file is generated + - Output filename correctly uses the {dev-dependencies} template variable + + Test Parameters: + dev_deps: Controls --dev-dependencies flag + None: Default behavior (same as True) + True: --dev-dependencies + False: --no-dev-dependencies + + filter_cats: Controls --filter-categories flag + True: Enable category filtering + False: Default behavior + + extras: Controls which extra categories to include via --category + []: No extras + ["dev"]: Include dev category + ["mm"]: Include custom category + ["dev", "mm"]: Include both categories + + The test matrix covers all combinations of these parameters (24 test cases) + to ensure consistent behavior across different CLI option combinations. + """ + # Create output directory + output_dir = categories_environment_files[0].parent / "output" + output_dir.mkdir() + + # Create a filename template using the {dev-dependencies} variable + filename_template = "conda-lock-{dev-dependencies}.lock" + + # Build the command arguments + args = [ + "lock", + "--conda", + str(mamba_exe), + "-p", + "linux-64", + "-k", + "explicit", + "--filename-template", + filename_template, + ] + + # Add all environment files + for env_file in categories_environment_files: + args.extend(["-f", str(env_file)]) + + # Add optional arguments based on the test case + if dev_deps is not None: + args.append("--dev-dependencies" if dev_deps else "--no-dev-dependencies") + if filter_cats: + args.append("--filter-categories") + for extra in extras: + args.extend(["--category", extra]) + + # Run the command from the output directory + monkeypatch.chdir(output_dir) + runner = CliRunner(mix_stderr=False) + with capsys.disabled(): + result = runner.invoke(main, args, catch_exceptions=False) + print(result.stdout, file=sys.stdout) + print(result.stderr, file=sys.stderr) + assert result.exit_code == 0 + + # Verify exactly one output file was generated + output_files = list(output_dir.glob("*")) + assert ( + len(output_files) == 1 + ), f"Expected exactly one output file, found {len(output_files)}" + output_file = output_files[0] + + # Verify the filename matches the expected dev-dependencies value + expected_dev_str = "true" if dev_deps in (None, True) else "false" + expected_filename = f"conda-lock-{expected_dev_str}.lock" + assert ( + output_file.name == expected_filename + ), f"Expected filename {expected_filename}, got {output_file.name}" + + # Read the file contents + content = output_file.read_text() + assert "tzcode" in content, "Main category dependency should always be present" + + # Check for dev category dependency + should_have_dev_category = ( + (dev_deps is None or dev_deps is True) # dev_dependencies is True by default + or "dev" in extras + ) + does_have_dev_category = "pixi" in content + assert does_have_dev_category == should_have_dev_category, ( + f"Dev category in lockfile: {does_have_dev_category}, " + f"Expected: {should_have_dev_category}" + ) + + # Check for custom extra category dependency + should_have_mm_category = "mm" in extras + does_have_mm_category = "micromamba" in content + assert does_have_mm_category == should_have_mm_category, ( + f"Custom category (mm) in lockfile: {does_have_mm_category}, " + f"Expected: {should_have_mm_category}" + ) From 59e379b465a2a2a2f0db6eb6e86de6485b26c01c Mon Sep 17 00:00:00 2001 From: Ben Mares Date: Sun, 23 Feb 2025 15:11:32 +0100 Subject: [PATCH 2/2] Add test case when dev deps are empty --- tests/test_regression.py | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/tests/test_regression.py b/tests/test_regression.py index 0a30bf995..9a76c5e97 100644 --- a/tests/test_regression.py +++ b/tests/test_regression.py @@ -294,6 +294,7 @@ def categories_environment_files(tmp_path: Path) -> List[Path]: dev_deps_and_extras_cli_regression = list( itertools.product( + [False, True], # dev_deps_are_empty [None, True, False], # dev_deps [False, True], # filter_cats [[], ["dev"], ["mm"], ["dev", "mm"]], # extras @@ -302,7 +303,10 @@ def categories_environment_files(tmp_path: Path) -> List[Path]: def make_dev_deps_and_extras_cli_regression_id( - dev_deps: Optional[bool], filter_cats: bool, extras: List[str] + dev_deps_are_empty: bool, + dev_deps: Optional[bool], + filter_cats: bool, + extras: List[str], ) -> str: dev = ( "" @@ -313,16 +317,17 @@ def make_dev_deps_and_extras_cli_regression_id( ) filter = "--filter-categories" if filter_cats else "" extra = "--category=" + ",".join(extras) if extras else "" - nonempty_args = [arg for arg in [dev, filter, extra] if arg] + empty_dev_deps = "empty-dev-deps" if dev_deps_are_empty else "" + nonempty_args = [arg for arg in [dev, filter, extra, empty_dev_deps] if arg] return "_".join(nonempty_args) or "no_args" @pytest.mark.parametrize( - "dev_deps,filter_cats,extras", + "dev_deps_are_empty,dev_deps,filter_cats,extras", dev_deps_and_extras_cli_regression, ids=[ - make_dev_deps_and_extras_cli_regression_id(d, f, e) - for d, f, e in dev_deps_and_extras_cli_regression + make_dev_deps_and_extras_cli_regression_id(dde, d, f, e) + for dde, d, f, e in dev_deps_and_extras_cli_regression ], ) def test_dev_deps_and_extras_cli_regression( @@ -330,6 +335,7 @@ def test_dev_deps_and_extras_cli_regression( categories_environment_files: List[Path], mamba_exe: Path, capsys: "pytest.CaptureFixture[str]", + dev_deps_are_empty: bool, dev_deps: Optional[bool], filter_cats: bool, extras: List[str], @@ -384,16 +390,15 @@ def test_dev_deps_and_extras_cli_regression( "lock", "--conda", str(mamba_exe), - "-p", - "linux-64", - "-k", - "explicit", - "--filename-template", - filename_template, + "--platform=linux-64", + "--kind=explicit", + f"--filename-template={filename_template}", ] - # Add all environment files + # Add environment files for env_file in categories_environment_files: + if dev_deps_are_empty and env_file.name == "environment-dev.yml": + continue args.extend(["-f", str(env_file)]) # Add optional arguments based on the test case @@ -432,7 +437,7 @@ def test_dev_deps_and_extras_cli_regression( assert "tzcode" in content, "Main category dependency should always be present" # Check for dev category dependency - should_have_dev_category = ( + should_have_dev_category = (not dev_deps_are_empty) and ( (dev_deps is None or dev_deps is True) # dev_dependencies is True by default or "dev" in extras )