Skip to content
Merged
Show file tree
Hide file tree
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
231 changes: 231 additions & 0 deletions autotest/test_programs.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
from pathlib import Path

import pytest

from modflow_devtools.programs import (
_DEFAULT_CACHE,
ProgramCache,
ProgramDistribution,
ProgramMetadata,
ProgramRegistry,
ProgramSourceConfig,
ProgramSourceRepo,
Expand Down Expand Up @@ -344,3 +348,230 @@ def test_installation_metadata_integration(self):

# Clean up
cache.clear()


class TestExeFieldResolution:
"""Test executable path resolution logic."""

def test_distribution_level_exe_takes_precedence(self):
"""Test that distribution-level exe overrides program-level."""
metadata = ProgramMetadata(
exe="bin/program", # Program-level
dists=[
ProgramDistribution(
name="linux",
asset="linux.zip",
exe="custom/path/to/program", # Distribution-level
),
ProgramDistribution(
name="win64",
asset="win64.zip",
exe="custom/path/to/program.exe", # Distribution-level
),
],
)

# Distribution-level should be used
assert metadata.get_exe_path("program", "linux") == "custom/path/to/program"
assert metadata.get_exe_path("program", "win64") == "custom/path/to/program.exe"

def test_program_level_exe_fallback(self):
"""Test that program-level exe is used when no distribution match."""
metadata = ProgramMetadata(
exe="bin/program", # Program-level
dists=[
ProgramDistribution(
name="linux",
asset="linux.zip",
# No exe specified
),
ProgramDistribution(
name="win64",
asset="win64.zip",
# No exe specified
),
],
)

# Program-level should be used
assert metadata.get_exe_path("program", "linux") == "bin/program"
# Should auto-add .exe on Windows
assert metadata.get_exe_path("program", "win64") == "bin/program.exe"

def test_default_exe_path(self):
"""Test default exe path when neither level specifies."""
metadata = ProgramMetadata(
# No program-level exe
dists=[
ProgramDistribution(
name="linux",
asset="linux.zip",
# No distribution-level exe
),
ProgramDistribution(
name="win64",
asset="win64.zip",
# No distribution-level exe
),
],
)

# Should default to bin/{program_name}
assert metadata.get_exe_path("myprogram", "linux") == "bin/myprogram"
# Should auto-add .exe on Windows
assert metadata.get_exe_path("myprogram", "win64") == "bin/myprogram.exe"

def test_windows_exe_extension_handling(self):
"""Test automatic .exe extension on Windows platforms."""
metadata = ProgramMetadata(
dists=[
ProgramDistribution(
name="win64",
asset="win64.zip",
exe="mfnwt", # No .exe extension
),
],
)

# Should auto-add .exe
assert metadata.get_exe_path("mfnwt", "win64") == "mfnwt.exe"

# Should not double-add if already present
metadata2 = ProgramMetadata(
dists=[
ProgramDistribution(
name="win64",
asset="win64.zip",
exe="mfnwt.exe", # Already has .exe
),
],
)
assert metadata2.get_exe_path("mfnwt", "win64") == "mfnwt.exe"

def test_mixed_exe_field_usage(self):
"""Test mixed usage: some distributions with exe, some without."""
metadata = ProgramMetadata(
exe="default/path/program", # Program-level fallback
dists=[
ProgramDistribution(
name="linux",
asset="linux.zip",
exe="linux-specific/bin/program", # Has distribution-level
),
ProgramDistribution(
name="mac",
asset="mac.zip",
# No distribution-level, should use program-level
),
ProgramDistribution(
name="win64",
asset="win64.zip",
exe="win64-specific/bin/program.exe", # Has distribution-level
),
],
)

# Linux uses distribution-level
assert metadata.get_exe_path("program", "linux") == "linux-specific/bin/program"
# Mac uses program-level fallback
assert metadata.get_exe_path("program", "mac") == "default/path/program"
# Windows uses distribution-level
assert metadata.get_exe_path("program", "win64") == "win64-specific/bin/program.exe"

def test_nonexistent_platform_uses_fallback(self):
"""Test that non-matching platform uses program-level or default."""
metadata = ProgramMetadata(
exe="bin/program",
dists=[
ProgramDistribution(
name="linux",
asset="linux.zip",
exe="linux/bin/program",
),
],
)

# Requesting win64 when only linux has distribution-specific exe
# Should fall back to program-level
assert metadata.get_exe_path("program", "win64") == "bin/program.exe"


class TestForceSemantics:
"""Test force flag semantics for sync and install."""

def test_sync_force_flag(self):
"""Test that sync --force re-downloads even if cached."""
# Clear cache first
_DEFAULT_CACHE.clear()

config = ProgramSourceConfig.load()

# Get a source that we know exists (modflow6)
if "modflow6" not in config.sources:
pytest.skip("modflow6 source not configured")

source = config.sources["modflow6"]

# First sync (should download)
result1 = source.sync(
ref=source.refs[0] if source.refs else None, force=False, verbose=False
)

# Check if sync succeeded (it might fail if no registry available)
if not result1.synced:
pytest.skip(f"Sync failed: {result1.failed}")

# Verify it's cached
ref = source.refs[0] if source.refs else None
assert _DEFAULT_CACHE.has(source.name, ref)

# Second sync without force (should skip)
result2 = source.sync(ref=ref, force=False, verbose=False)
assert len(result2.skipped) > 0

# Third sync with force (should re-download)
result3 = source.sync(ref=ref, force=True, verbose=False)
assert len(result3.synced) > 0

# Clean up
_DEFAULT_CACHE.clear()

def test_install_force_does_not_sync(self):
"""Test that install --force does not re-sync registry."""
from modflow_devtools.programs import ProgramManager

# This is more of a design verification test
# We verify that the install method signature has force parameter
# and that it's documented to not sync

manager = ProgramManager()

# Check install method has force parameter
import inspect

sig = inspect.signature(manager.install)
assert "force" in sig.parameters

# Check that force parameter is documented correctly
# The docstring should mention that force doesn't re-sync
docstring = manager.install.__doc__
if docstring:
# This is a basic check - in reality the behavior is tested
# through integration tests
assert docstring is not None

def test_sync_and_install_independence(self):
"""Test that sync cache and install state are independent."""
from modflow_devtools.programs import ProgramCache

cache = ProgramCache()

# Registry cache is separate from installation metadata
# Registry cache: ~/.cache/modflow-devtools/programs/registries/
# Install metadata: ~/.cache/modflow-devtools/programs/metadata/

assert cache.registries_dir != cache.metadata_dir

# Verify paths are different
assert "registries" in str(cache.registries_dir)
assert "metadata" in str(cache.metadata_dir)
Loading