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
5 changes: 5 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ include = ["pywr_utils*"]
[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "--cov=pywr_utils --cov-report=term-missing --cov-report=xml"
markers = [
"unit: a single function/class in isolation, no DB/network/subprocess",
"integration: real internal components wired together via the code's own Python API, no mocks",
"e2e: drives the installed CLI entrypoint end to end",
]

[tool.coverage.report]
# Ratchet, not an aspirational target: set at today's measured coverage (99.23%, after a full unit/integration/e2e pass on model_runner.py) minus a small buffer for environment variance, so coverage can't silently regress further without failing CI. Raise this number as coverage genuinely improves.
Expand Down
20 changes: 20 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import sys

import pytest

BLOCKED_MODULES = ("pandas", "pywr")


@pytest.fixture
def block_pandas_and_pywr(monkeypatch):
"""Simulate a fresh install with neither pandas nor pywr present.

Setting `sys.modules[name] = None` is the standard idiom for this: Python's import system raises ImportError for any name mapped to None in sys.modules, without needing to intercept `__import__` or actually uninstall the package.
"""
for name in BLOCKED_MODULES:
monkeypatch.setitem(sys.modules, name, None)
for name in ("pywr_utils.cli", "pywr_utils.model_runner"):
monkeypatch.delitem(sys.modules, name, raising=False)
yield
for name in ("pywr_utils.cli", "pywr_utils.model_runner"):
monkeypatch.delitem(sys.modules, name, raising=False)
2 changes: 2 additions & 0 deletions tests/e2e/test_cli_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
from pywr_utils import model_runner
from pywr_utils.cli import main

pytestmark = pytest.mark.e2e


@pytest.fixture
def runner():
Expand Down
Original file line number Diff line number Diff line change
@@ -1,34 +1,13 @@
"""Regression tests for the model_runner lazy-import fix.
"""E2E tests for the model_runner lazy-import fix, driven through the Click entrypoint.

create-synthetic-model must not require pandas or pywr to be installed, since it only depends on model_creation (stdlib json/typing). Before the fix, cli.py imported model_runner (which needs both) at module load time, so every CLI invocation failed on a fresh install before pywr's own native build was ever reached.
create-synthetic-model must not require pandas or pywr to be installed, since it only depends on model_creation (stdlib json/typing). Before the fix, cli.py imported model_runner (which needs both) at module load time, so every CLI invocation failed on a fresh install before pywr's own native build was ever reached. See tests/unit/test_cli_lazy_import.py for the isolated import-only check.
"""
import importlib
import sys

import pytest
from click.testing import CliRunner

BLOCKED_MODULES = ("pandas", "pywr")


@pytest.fixture
def block_pandas_and_pywr(monkeypatch):
"""Simulate a fresh install with neither pandas nor pywr present.

Setting `sys.modules[name] = None` is the standard idiom for this: Python's import system raises ImportError for any name mapped to None in sys.modules, without needing to intercept `__import__` or actually uninstall the package.
"""
for name in BLOCKED_MODULES:
monkeypatch.setitem(sys.modules, name, None)
for name in ("pywr_utils.cli", "pywr_utils.model_runner"):
monkeypatch.delitem(sys.modules, name, raising=False)
yield
for name in ("pywr_utils.cli", "pywr_utils.model_runner"):
monkeypatch.delitem(sys.modules, name, raising=False)


def test_cli_module_imports_without_pandas_or_pywr(block_pandas_and_pywr):
module = importlib.import_module("pywr_utils.cli")
assert hasattr(module, "create_synthetic_model")
pytestmark = pytest.mark.e2e


def test_create_synthetic_model_command_without_pandas_or_pywr(tmp_path, block_pandas_and_pywr):
Expand Down
2 changes: 2 additions & 0 deletions tests/integration/test_incremental_sweep_failures.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
from pywr_utils import model_runner
from pywr_utils.model_runner import PywrFileRunner, run_incremental_sizes

pytestmark = pytest.mark.integration

SWEEP_ARGS = {"max_zones": 2, "zone_increment": 2, "transfer_increment": 2, "output_csv": "timings.csv"}


Expand Down
2 changes: 2 additions & 0 deletions tests/integration/test_model_runner_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
from pywr_utils.model_creation import SyntheticModelCreator
from pywr_utils.model_runner import PywrFileRunner, run_file, run_incremental_sizes

pytestmark = pytest.mark.integration

DEFAULT_SOLVER = "glpk"
ALTERNATIVE_SOLVER = "glpk-edge"

Expand Down
14 changes: 14 additions & 0 deletions tests/unit/test_cli_module_import_without_deps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
"""UNIT test for the model_runner lazy-import fix.

create-synthetic-model must not require pandas or pywr to be installed, since it only depends on model_creation (stdlib json/typing). Before the fix, cli.py imported model_runner (which needs both) at module load time, so every CLI invocation failed on a fresh install before pywr's own native build was ever reached. This checks only the import itself, in isolation from the Click entrypoint — see tests/e2e/test_cli_lazy_import.py for the command-level regression coverage.
"""
import importlib

import pytest

pytestmark = pytest.mark.unit


def test_cli_module_imports_without_pandas_or_pywr(block_pandas_and_pywr):
module = importlib.import_module("pywr_utils.cli")
assert hasattr(module, "create_synthetic_model")
2 changes: 2 additions & 0 deletions tests/unit/test_model_creation.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

from pywr_utils.model_creation import SyntheticModelCreator, create_synthetic_model

pytestmark = pytest.mark.unit

# One input produces three nodes: an input, a link and a demand. Each transfer adds one further link node.
NODES_PER_INPUT = 3

Expand Down
2 changes: 2 additions & 0 deletions tests/unit/test_terminal_graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

from pywr_utils.model_runner import _display_terminal_graph, _draw_line

pytestmark = pytest.mark.unit


def timing_frame(zones, transfers, setup_times):
return pd.DataFrame({"zones": zones, "transfers": transfers, "setup_time": setup_times})
Expand Down
Loading