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
4 changes: 1 addition & 3 deletions .github/workflows/commit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,6 @@ jobs:
${{ runner.os }}-${{ hashFiles('flopy/utils/get_modflow.py') }}

- name: Install Modflow executables
working-directory: ./autotest
run: |
mkdir -p $HOME/.local/bin
get-modflow $HOME/.local/bin
Expand Down Expand Up @@ -241,7 +240,6 @@ jobs:
${{ runner.os }}-${{ hashFiles('flopy/utils/get_modflow.py') }}

- name: Install Modflow executables
working-directory: ./autotest
run: |
mkdir -p $HOME/.local/bin
get-modflow $HOME/.local/bin
Expand Down Expand Up @@ -359,4 +357,4 @@ jobs:
uses: codecov/codecov-action@v2.1.0
with:
directory: ./autotest
file: coverage.xml
file: coverage.xml
27 changes: 17 additions & 10 deletions DEVELOPER.md
Original file line number Diff line number Diff line change
Expand Up @@ -342,9 +342,9 @@ def test_get_paths():

#### Conditionally skipping tests

Several `pytest` markers are provided to conditionally skip tests based on executable availability or operating system.
Several `pytest` markers are provided to conditionally skip tests based on executable availability, Python package environment or operating system.

To skip tests if an executable is not available on the path:
To skip tests if one or more executables are not available on the path:

```python
from shutil import which
Expand All @@ -353,19 +353,26 @@ from autotest.conftest import requires_exe
@requires_exe("mf6")
def test_mf6():
assert which("mf6")

@requires_exe("mf6", "mp7")
def test_mf6_and_mp7():
assert which("mf6")
assert which("mp7")
```

A variant for multiple executables is also provided:
To skip tests if one or more Python packages are not available:

```python
from shutil import which
from autotest.conftest import requires_exes
from autotest.conftest import requires_pkg

exes = ["mfusg", "mfnwt"]
@requires_pkg("pandas")
def test_needs_pandas():
import pandas as pd

@requires_exes(exes)
def test_mfusg_and_mfnwt():
assert all(which(exe) for exe in exes)
@requires_pkg("pandas", "shapefile")
def test_needs_pandas():
import pandas as pd
from shapefile import Reader
```

To mark tests requiring or incompatible with particular operating systems:
Expand Down Expand Up @@ -430,4 +437,4 @@ act -W .github/workflows/ci.yml -j build

The `-n` flag can be used to execute a dry run, which doesn't run anything, just evaluates workflow, job and step definitions. See the [docs](https://github.com/nektos/act#example-commands) for more.

**Note:** `act` can only run Linux-based container definitions, so Mac or Windows workflows or matrix OS entries will be skipped.
**Note:** `act` can only run Linux-based container definitions, so Mac or Windows workflows or matrix OS entries will be skipped.
120 changes: 97 additions & 23 deletions autotest/conftest.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import os
import pkg_resources
import socket
import subprocess
import sys
from os import environ
from os.path import basename, normpath
from pathlib import Path
from platform import system
from shutil import copytree, which
from subprocess import PIPE, Popen
from typing import List, Optional
from urllib import request
from warnings import warn
Expand Down Expand Up @@ -114,23 +116,12 @@ def get_current_branch() -> str:
return basename(normpath(ref)).lower()

# otherwise ask git about it
try:
b = subprocess.Popen(
("git", "status"),
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
).communicate()[0]

if isinstance(b, bytes):
b = b.decode("utf-8")

for line in b.splitlines():
if "On branch" in line:
return line.replace("On branch ", "").rstrip().lower()
except:
raise ValueError(
"Could not determine current branch. Is git installed?"
)
if not which("git"):
raise RuntimeError("'git' required to determine current branch")
stdout, stderr, code = run_cmd("git", "rev-parse", "--abbrev-ref", "HEAD")
if code == 0 and stdout:
return stdout.strip().lower()
raise ValueError(f"Could not determine current branch: {stderr}")


def is_connected(hostname):
Expand Down Expand Up @@ -172,15 +163,39 @@ def is_github_rate_limited() -> Optional[bool]:
return None


def requires_exes(exes):
_has_exe_cache = {}
_has_pkg_cache = {}

def has_exe(exe):
if exe not in _has_exe_cache:
_has_exe_cache[exe] = bool(which(exe))
return _has_exe_cache[exe]

def has_pkg(pkg):
if pkg not in _has_pkg_cache:
try:
_has_pkg_cache[pkg] = bool(pkg_resources.get_distribution(pkg))
except pkg_resources.DistributionNotFound:
_has_pkg_cache[pkg] = False
return _has_pkg_cache[pkg]


def requires_exe(*exes):
missing = {exe for exe in exes if not has_exe(exe)}
return pytest.mark.skipif(
any(which(exe) is None for exe in exes),
reason=f"requires executables: {', '.join(exes)}",
missing,
reason=f"missing executable{'s' if len(missing) != 1 else ''}: " +
", ".join(missing),
)


def requires_exe(exe):
return requires_exes([exe])
def requires_pkg(*pkgs):
missing = {pkg for pkg in pkgs if not has_pkg(pkg)}
return pytest.mark.skipif(
missing,
reason=f"missing package{'s' if len(missing) != 1 else ''}: " +
", ".join(missing),
)


def requires_platform(platform, ci_only=False):
Expand Down Expand Up @@ -388,3 +403,62 @@ def pytest_runtest_setup(item):
is_profiletest = any(item.iter_markers(name="profile"))
if (is_profiletest and not should_profile) or (not is_profiletest and should_profile):
pytest.skip()


def pytest_report_header(config):
"""Header for pytest to show versions of packages."""
processed = set()
flopy_pkg = pkg_resources.get_distribution("flopy")
lines = []
items = []
for pkg in flopy_pkg.requires():
name = pkg.name
processed.add(name)
try:
version = pkg_resources.get_distribution(name).version
items.append(f"{name}-{version}")
except pkg_resources.DistributionNotFound:
items.append(f"{name} (not found)")
lines.append("required packages: " + ", ".join(items))
installed = []
not_found = []
for pkg in flopy_pkg.requires(["optional"]):
name = pkg.name
if name in processed:
continue
processed.add(name)
try:
version = pkg_resources.get_distribution(name).version
installed.append(f"{name}-{version}")
except pkg_resources.DistributionNotFound:
not_found.append(name)
if installed:
lines.append("optional packages: " + ", ".join(installed))
if not_found:
lines.append("optional packages not found: " + ", ".join(not_found))
return "\n".join(lines)


# functions to run commands and scripts

def run_cmd(*args, verbose=False, **kwargs):
"""Run any command, return tuple (stdout, stderr, returncode)."""
args = [str(g) for g in args]
if verbose:
print("running: " + " ".join(args))
p = Popen(args, stdout=PIPE, stderr=PIPE, **kwargs)
stdout, stderr = p.communicate()
stdout = stdout.decode()
stderr = stderr.decode()
returncode = p.returncode
if verbose:
print(f"stdout:\n{stdout}")
print(f"stderr:\n{stderr}")
print(f"returncode: {returncode}")
return stdout, stderr, returncode


def run_py_script(script, *args, verbose=False):
"""Run a Python script, return tuple (stdout, stderr, returncode)."""
return run_cmd(
sys.executable, script, *args, verbose=verbose, cwd=Path(script).parent)
10 changes: 5 additions & 5 deletions autotest/regression/test_lgr.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,18 @@
import pytest

import flopy
from autotest.conftest import requires_exe
from autotest.conftest import requires_exe, requires_pkg


@requires_exe("mflgr")
@requires_pkg("pymake")
@pytest.mark.regression
def test_simplelgr(tmpdir, example_data_path):
mflgr_v2_ex3_path = example_data_path / "mflgr_v2" / "ex3"

pytest.importorskip("pymake")
"""Test load and write of distributed MODFLOW-LGR example problem."""
import pymake

# Test load and write of distributed MODFLOW-LGR example problem
mflgr_v2_ex3_path = example_data_path / "mflgr_v2" / "ex3"

ws = tmpdir / mflgr_v2_ex3_path.stem
shutil.copytree(mflgr_v2_ex3_path, ws)

Expand Down
Loading