Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add regression test for dev dependencies #778

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
216 changes: 214 additions & 2 deletions tests/test_regression.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -242,3 +246,211 @@ 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(
[False, True], # dev_deps_are_empty
[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_are_empty: bool,
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 ""
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_are_empty,dev_deps,filter_cats,extras",
dev_deps_and_extras_cli_regression,
ids=[
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(
monkeypatch: "pytest.MonkeyPatch",
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],
):
"""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),
"--platform=linux-64",
"--kind=explicit",
f"--filename-template={filename_template}",
]

# 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
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 = (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
)
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}"
)
Loading