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

test: replace conftest parametrization with devtools #67

Merged
merged 3 commits into from
Mar 12, 2025
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
37 changes: 5 additions & 32 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,11 @@ on:
schedule:
- cron: '0 8 * * *' # run at 8 AM UTC (12 am PST)
push:
branches:
- main
- develop
- 'release*'
pull_request:
branches: [main, develop]

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:

std_setup:
Expand All @@ -24,7 +22,7 @@ jobs:
steps:

- name: Checkout repo
uses: actions/checkout@v3
uses: actions/checkout@v4

- name: Setup uv
uses: astral-sh/setup-uv@v5
Expand Down Expand Up @@ -158,13 +156,7 @@ jobs:
shell: bash
steps:
- name: Checkout repo
uses: actions/checkout@v3

- name: Checkout mf6-examples
uses: actions/checkout@v4
with:
repository: MODFLOW-ORG/modflow6-examples
path: modflow6-examples

- name: Setup uv
uses: astral-sh/setup-uv@v5
Expand All @@ -175,25 +167,6 @@ jobs:
- name: Install
run: uv sync --all-extras

- name: Install Python dependencies
run: |
uv pip install git+https://git@github.com/Deltares/xmipy@develop
uv pip install git+https://git@github.com/MODFLOW-ORG/modflow-devtools@develop
uv pip install git+https://git@github.com/modflowpy/flopy@develop

- name: update flopy for mf6-examples
run: uv run python -m flopy.mf6.utils.generate_classes --ref develop --no-backup

- name: Install modflow6 for modflow6-examples
uses: modflowpy/install-modflow-action@v1
with:
path: ${{ github.workspace }}/modflow6-examples/autotest

- name: Build mf6-examples
working-directory: modflow6-examples/autotest
run: |
uv run pytest -v -n=auto --init test_scripts.py -k "not synthetic-valley"

- name: Install modflow6 nightly build
uses: modflowpy/install-modflow-action@v1
with:
Expand All @@ -203,4 +176,4 @@ jobs:
- name: Run autotests
working-directory: ./autotest
shell: bash -l {0}
run: uv run pytest -v -n=auto test_mf6_examples.py --mf6-examples-path=../modflow6-examples/examples
run: uv run pytest -v -n auto test_mf6_examples.py
6 changes: 3 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ jobs:
steps:

- name: Checkout release branch
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
fetch-depth: 0

Expand Down Expand Up @@ -131,7 +131,7 @@ jobs:
steps:

- name: Checkout repo
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
ref: main

Expand Down Expand Up @@ -179,7 +179,7 @@ jobs:
steps:

- name: Checkout main branch
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
ref: main

Expand Down
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -137,4 +137,7 @@ dmypy.json
**.dylib

# uv lockfile
uv.lock
uv.lock

# temporary test folders
autotest/temp/**
121 changes: 0 additions & 121 deletions autotest/conftest.py
Original file line number Diff line number Diff line change
@@ -1,122 +1 @@
from itertools import groupby
from os import linesep
from pathlib import Path
from tempfile import gettempdir

import pytest
from filelock import FileLock
from modflow_devtools.download import download_and_unzip

# import modflow-devtools fixtures
pytest_plugins = ["modflow_devtools.fixtures"]


__mf6_examples = "mf6_examples"
__mf6_examples_path = Path(gettempdir()) / __mf6_examples
__mf6_examples_lock = FileLock(Path(gettempdir()) / f"{__mf6_examples}.lock")


def get_mf6_examples_path() -> Path:
# use file lock so mf6 distribution is downloaded once,
# even when tests are run in parallel with pytest-xdist
__mf6_examples_lock.acquire()
try:
if not __mf6_examples_path.is_dir():
__mf6_examples_path.mkdir(exist_ok=True)
download_and_unzip(
url="https://github.com/MODFLOW-ORG/modflow6-examples/releases/download/current/mf6examples.zip",
path=__mf6_examples_path,
verbose=True,
)
return __mf6_examples_path
finally:
__mf6_examples_lock.release()


def pytest_addoption(parser):
parser.addoption("--mf6-examples-path", action="store", default=None)


def is_nested(namfile) -> bool:
p = Path(namfile)
if not p.is_file() or not p.name.endswith(".nam"):
raise ValueError(f"Expected a namfile path, got {p}")
return p.parent.parent.name != __mf6_examples


def pytest_generate_tests(metafunc):
# examples to skip:
# - ex-gwtgwt-mt3dms-p10: https://github.com/MODFLOW-ORG/modflow6/pull/1008
option_value = metafunc.config.option.mf6_examples_path
t = metafunc.fixturenames
if "mf6_example_namfiles" in metafunc.fixturenames and option_value is not None:
mf6_examples_path = Path(option_value)
global __mf6_examples
__mf6_examples = str(mf6_examples_path.name)
else:
mf6_examples_path = get_mf6_examples_path()

# grouping...
exclude = [
"ex-gwt-gwtgwt-mt3dms-p10",
"mp7-p02",
"mp7-p04",
"synthetic-valley",
]
namfiles = [
str(p)
for p in mf6_examples_path.rglob("mfsim.nam")
if not any(e in str(p) for e in exclude)
]

# parametrization by model
# - single namfile per test case
# - no coupling (only first model in each simulation subdir is used)
key = "mf6_example_namfile"
if key in metafunc.fixturenames:
metafunc.parametrize(key, sorted(namfiles))

# parametrization by simulation
# - each test case gets an ordered list of 1+ namfiles
# - models can be coupled (run in order provided, sharing workspace)
key = "mf6_example_namfiles"
if key in metafunc.fixturenames:
simulations = []

def simulation_name_from_model_path(p):
p = Path(p)
return p.parent.parent.name if is_nested(p) else p.parent.name

for model_name, model_namfiles in groupby(
namfiles, key=simulation_name_from_model_path
):
models = []
model_namfiles = list(model_namfiles)
if len(model_namfiles) > 1:
# trap gwf models as first set of models
idxs = [ix for ix, _ in enumerate(model_namfiles) if "gwf" in _]
if len(idxs) > 0:
for ix in idxs[::-1]:
models.append(model_namfiles.pop(ix))

# sort remaining models in alphabetical order (gwe < gwt < prt)
models += list(sorted(model_namfiles))
simulations.append(models)
print(
f"Simulation {model_name} has {len(models)} model(s):\n"
f"{linesep.join(model_namfiles)}"
)

def simulation_name_from_model_namfiles(mnams):
try:
namfile = next(iter(mnams), None)
except TypeError:
namfile = None
if namfile is None:
pytest.skip("No namfiles (expected ordered collection)")
namfile = Path(namfile)
return (
namfile.parent.parent if is_nested(namfile) else namfile.parent
).name

metafunc.parametrize(key, simulations, ids=simulation_name_from_model_namfiles)
67 changes: 18 additions & 49 deletions autotest/test_mf6_examples.py
Original file line number Diff line number Diff line change
@@ -1,55 +1,24 @@
from pathlib import Path
from shutil import copytree

import modflow_devtools.models as models
import pytest

from autotest.conftest import is_nested
from modflowapi import run_simulation

examples = models.get_examples()
pytestmark = pytest.mark.mf6
dll = "libmf6"


def test_mf6_example_simulations(function_tmpdir, mf6_example_namfiles):
"""
MF6 examples parametrized by simulation. `mf6_example_namfiles` is a list
of models to run in order provided. Coupled models share the same tempdir

Parameters
----------
function_tmpdir: function-scoped temporary directory fixture
mf6_example_namfiles: ordered list of namfiles for 1+ coupled models
"""
if len(mf6_example_namfiles) == 0:
pytest.skip("No namfiles (expected ordered collection)")
namfile = Path(mf6_example_namfiles[0])

nested = is_nested(namfile)
function_tmpdir = Path(function_tmpdir / "workspace")

copytree(
src=namfile.parent.parent if nested else namfile.parent, dst=function_tmpdir
)

def callback(sim, step):
pass

def run_models():
# run models in order received (should be alphabetical,
# so gwf precedes gwt)
for namfile in mf6_example_namfiles:
namfile_path = Path(namfile).resolve()
model_path = namfile_path.parent

# working directory must be named according to namefile's parent
# (e.g. 'mf6gwf') because coupled models refer to each other with
# relative paths
wrkdir = (
Path(function_tmpdir / model_path.name) if nested else function_tmpdir
)
try:
run_simulation(dll, wrkdir, callback, verbose=True)
except Exception as e:
raise Exception(e)

run_models()
skip = [
# https://github.com/MODFLOW-ORG/modflowapi/issues/53
"ex-prt-mp7-p02",
"ex-prt-mp7-p04",
]


@pytest.mark.parametrize("example_name", examples.keys())
def test_example(function_tmpdir, example_name):
if example_name in skip:
pytest.skip(f"Skipping example {example_name}")
for model_name in examples[example_name]:
model_relpath = model_name.rpartition(example_name + "/")[-1]
model_workspace = function_tmpdir / model_relpath
models.copy_to(model_workspace, model_name, verbose=True)
run_simulation(dll, model_workspace, lambda sim, step: None, verbose=True)
Loading