Skip to content

Commit

Permalink
refactor(tests): simplify test utilities per convention that tests ar…
Browse files Browse the repository at this point in the history
…e run from autotest folder (#1586)
  • Loading branch information
wpbonelli committed Oct 14, 2022
1 parent 9dbb88c commit 7973c70
Show file tree
Hide file tree
Showing 18 changed files with 42 additions and 222 deletions.
24 changes: 6 additions & 18 deletions DEVELOPER.md
Expand Up @@ -321,36 +321,24 @@ def test_with_data(tmpdir, example_data_path):

This is preferable to manually handling relative paths as if the location of the example data changes in the future, only a single fixture in `conftest.py` will need to be updated rather than every test case individually.

An equivalent function `get_example_data_path(path=None)` is also provided in `conftest.py`. This is useful to dynamically generate data for test parametrization. (Due to a [longstanding `pytest` limitation](https://github.com/pytest-dev/pytest/issues/349), fixtures cannot be used to generate test parameters.) This function accepts a path hint, taken as the path to the current test file, but will try to locate the example data even if the current file is not provided.

```python
import pytest
from autotest.conftest import get_example_data_path

# current test script can be provided (or not)

@pytest.mark.parametrize("current_path", [__file__, None])
def test_get_example_data_path(current_path):
parts = get_example_data_path(current_path).parts
assert (parts[-1] == "data" and
parts[-2] == "examples" and
parts[-3] == "flopy")
```
An equivalent function `get_example_data_path()` is also provided in `conftest.py`. This is useful to dynamically generate data for test parametrization. (Due to a [longstanding `pytest` limitation](https://github.com/pytest-dev/pytest/issues/349), fixtures cannot be used to generate test parameters.)

#### Locating the project root

A similar `get_project_root_path(path=None)` function is also provided, doing what it says on the tin:
A similar `get_project_root_path()` function is also provided, doing what it says on the tin:

```python
from autotest.conftest import get_project_root_path, get_example_data_path

def test_get_paths():
example_data = get_example_data_path(__file__)
project_root = get_project_root_path(__file__)
example_data = get_example_data_path()
project_root = get_project_root_path()

assert example_data.parent.parent == project_root
```

Note that this function expects tests to be run from the `autotest` directory, as mentioned above.

#### Conditionally skipping tests

Several `pytest` markers are provided to conditionally skip tests based on executable availability, Python package environment or operating system.
Expand Down
106 changes: 12 additions & 94 deletions autotest/conftest.py
Expand Up @@ -24,98 +24,16 @@
# misc utilities


def get_project_root_path(path=None) -> Path:
"""
Infers the path to the project root given the path to the current working directory.
The current working location must be somewhere in the project, below the project root.
This function aims to work whether invoked from the autotests directory, the examples
directory, the flopy module directory, or any subdirectories of these. GitHub Actions
CI runners, local `act` runners for GitHub CI, and local environments are supported.
This function can be modified to support other flopy testing environments if needed.
Parameters
----------
path : the path to the current working directory
Returns
-------
The absolute path to the project root
"""
def get_project_root_path() -> Path:
return Path(__file__).parent.parent

cwd = Path(path) if path is not None else Path(os.getcwd())

def backtrack_or_raise():
tries = [1]
if is_in_ci():
tries.append(2)
for t in tries:
parts = cwd.parts[0 : cwd.parts.index("flopy") + t]
pth = Path(*parts)
if (
next(iter([p for p in pth.glob("setup.cfg")]), None)
is not None
):
return pth
raise Exception(
f"Can't infer location of project root from {cwd} "
f"(run from project root, flopy module, examples, or autotest)"
)

if cwd.name == "autotest":
# we're in top-level autotest folder
return cwd.parent
elif "autotest" in cwd.parts and cwd.parts.index(
"autotest"
) > cwd.parts.index("flopy"):
# we're somewhere inside autotests
parts = cwd.parts[0 : cwd.parts.index("autotest")]
return Path(*parts)
elif "examples" in cwd.parts and cwd.parts.index(
"examples"
) > cwd.parts.index("flopy"):
# we're somewhere inside examples folder
parts = cwd.parts[0 : cwd.parts.index("examples")]
return Path(*parts)
elif "flopy" in cwd.parts:
if cwd.parts.count("flopy") >= 2:
# we're somewhere inside the project or flopy module
return backtrack_or_raise()
elif cwd.parts.count("flopy") == 1:
if cwd.name == "flopy":
# we're in project root
return cwd
elif cwd.name == ".working":
# we're in local `act` github actions runner
return backtrack_or_raise()
else:
raise Exception(
f"Can't infer location of project root from {cwd}"
f"(run from project root, flopy module, examples, or autotest)"
)
else:
raise Exception(
f"Can't infer location of project root from {cwd}"
f"(run from project root, flopy module, examples, or autotest)"
)

def get_example_data_path() -> Path:
return get_project_root_path() / "examples" / "data"

def get_example_data_path(path=None) -> Path:
"""
Gets the absolute path to example models and data.
The path argument is a hint, interpreted as
the current working location.
"""
return get_project_root_path(path) / "examples" / "data"


def get_flopy_data_path(path=None) -> Path:
"""
Gets the absolute path to flopy module data.
The path argument is a hint, interpreted as
the current working location.
"""
return get_project_root_path(path) / "flopy" / "data"
def get_flopy_data_path() -> Path:
return get_project_root_path() / "flopy" / "data"


def get_current_branch() -> str:
Expand Down Expand Up @@ -264,18 +182,18 @@ def excludes_branch(branch):


@pytest.fixture(scope="session")
def project_root_path(request) -> Path:
return get_project_root_path(request.session.path)
def project_root_path() -> Path:
return get_project_root_path()


@pytest.fixture(scope="session")
def example_data_path(request) -> Path:
return get_example_data_path(request.session.path)
def example_data_path() -> Path:
return get_example_data_path()


@pytest.fixture(scope="session")
def flopy_data_path(request) -> Path:
return get_flopy_data_path(request.session.path)
def flopy_data_path() -> Path:
return get_flopy_data_path()


@pytest.fixture(scope="session")
Expand Down
2 changes: 1 addition & 1 deletion autotest/regression/test_mfnwt.py
Expand Up @@ -9,7 +9,7 @@

def get_nfnwt_namfiles():
# build list of name files to try and load
nwtpth = get_example_data_path(__file__) / "mf2005_test"
nwtpth = get_example_data_path() / "mf2005_test"
namfiles = []
m = Modflow("test", version="mfnwt")
for namfile in nwtpth.rglob("*.nam"):
Expand Down
2 changes: 1 addition & 1 deletion autotest/regression/test_modflow.py
Expand Up @@ -167,7 +167,7 @@ def test_gage(tmpdir, example_data_path):
), f'new and original gage file "{f}" are not binary equal.'


__example_data_path = get_example_data_path(Path(__file__))
__example_data_path = get_example_data_path()


@requires_exe("mf2005")
Expand Down
2 changes: 1 addition & 1 deletion autotest/test_binaryfile.py
Expand Up @@ -160,7 +160,7 @@ def test_get_headfile_precision(example_data_path):
assert precision == 'double'


_example_data_path = get_example_data_path(__file__)
_example_data_path = get_example_data_path()


@pytest.mark.parametrize("path", [str(p) for p in [
Expand Down
2 changes: 1 addition & 1 deletion autotest/test_cbc_full3D.py
Expand Up @@ -9,7 +9,7 @@
from flopy.modflow import Modflow
from flopy.utils import CellBudgetFile

example_data_path = get_example_data_path(Path(__file__))
example_data_path = get_example_data_path()
mf2005_paths = [
str(example_data_path / "freyberg"),
]
Expand Down
47 changes: 6 additions & 41 deletions autotest/test_conftest.py
Expand Up @@ -80,62 +80,27 @@ def test_session_scoped_tmpdir(session_tmpdir):
# misc utilities


def test_get_project_root_path_from_autotest():
cwd = Path(__file__).parent
root = get_project_root_path(cwd)
def test_get_project_root_path():
root = get_project_root_path()

assert root.is_dir()
assert root.name == "flopy"

contents = [p.name for p in root.glob("*")]
assert (
"autotest" in contents
and "examples" in contents
and "README.md" in contents
)


def test_get_project_root_path_from_project_root():
cwd = Path(__file__).parent.parent
root = get_project_root_path(cwd)

assert root.is_dir()
assert root.name == "flopy"

contents = [p.name for p in root.glob("*")]
assert (
"autotest" in contents
and "examples" in contents
and "README.md" in contents
)


@pytest.mark.parametrize("relative_path", ["", "utils", "mf6/utils"])
def test_get_project_root_path_from_within_flopy_module(relative_path):
cwd = Path(__file__).parent.parent / "flopy" / Path(relative_path)
root = get_project_root_path(cwd)

assert root.is_dir()
assert root.name == "flopy"

contents = [p.name for p in root.glob("*")]
assert (
"autotest" in contents
and "examples" in contents
and "README.md" in contents
)


def test_get_paths():
example_data = get_example_data_path(__file__)
project_root = get_project_root_path(__file__)
example_data = get_example_data_path()
project_root = get_project_root_path()

assert example_data.parent.parent == project_root


@pytest.mark.parametrize("current_path", [__file__, None])
def test_get_example_data_path(current_path):
parts = get_example_data_path(current_path).parts
def test_get_example_data_path():
parts = get_example_data_path().parts
assert (
parts[-3] == "flopy"
and parts[-2] == "examples"
Expand Down
2 changes: 1 addition & 1 deletion autotest/test_example_notebooks.py
Expand Up @@ -5,7 +5,7 @@


def get_example_notebooks(exclude=None):
prjroot = get_project_root_path(__file__)
prjroot = get_project_root_path()
nbpaths = [str(p) for p in (prjroot / "examples" / "FAQ").glob("*.ipynb")]
nbpaths += [
str(p) for p in (prjroot / "examples" / "Notebooks").glob("*.ipynb")
Expand Down
2 changes: 1 addition & 1 deletion autotest/test_example_scripts.py
Expand Up @@ -7,7 +7,7 @@


def get_example_scripts(exclude=None):
prjroot = get_project_root_path(__file__)
prjroot = get_project_root_path()

# sort to appease pytest-xdist: all workers must collect identically ordered sets of tests
return sorted(
Expand Down
2 changes: 1 addition & 1 deletion autotest/test_export.py
Expand Up @@ -62,7 +62,7 @@ def shp_paths(d) -> List[Path]:


def namfiles():
mf2005_path = get_example_data_path(__file__) / "mf2005_test"
mf2005_path = get_example_data_path() / "mf2005_test"
return [str(p) for p in Path(mf2005_path).rglob("*.nam")]


Expand Down
2 changes: 1 addition & 1 deletion autotest/test_get_modflow.py
Expand Up @@ -17,7 +17,7 @@
from flopy.utils import get_modflow

rate_limit_msg = "rate limit exceeded"
flopy_dir = get_project_root_path(__file__)
flopy_dir = get_project_root_path()
get_modflow_script = flopy_dir / "flopy" / "utils" / "get_modflow.py"


Expand Down
2 changes: 1 addition & 1 deletion autotest/test_mfnwt.py
Expand Up @@ -33,7 +33,7 @@ def analytical_water_table_solution(h1, h2, z, R, K, L, x):


def fnwt_model_files(pattern):
path = get_example_data_path(Path(__file__)) / "nwt_test"
path = get_example_data_path() / "nwt_test"
return [
os.path.join(path, f) for f in os.listdir(path) if f.endswith(pattern)
]
Expand Down
6 changes: 3 additions & 3 deletions autotest/test_modflow.py
Expand Up @@ -538,12 +538,12 @@ def test_read_usgs_model_reference(tmpdir, model_reference_path):


def mf2005_model_namfiles():
path = get_example_data_path(Path(__file__)) / "mf2005_test"
path = get_example_data_path() / "mf2005_test"
return [str(p) for p in path.glob("*.nam")]


def parameters_model_namfiles():
path = get_example_data_path(Path(__file__)) / "parameters"
path = get_example_data_path() / "parameters"
skip = ["twrip.nam", "twrip_upw.nam"] # TODO: why do these fail?
return [str(p) for p in path.glob("*.nam") if p.name not in skip]

Expand Down Expand Up @@ -731,7 +731,7 @@ def test_mflist_add_record():
np.testing.assert_array_equal(wel.stress_period_data[1], check1)


__mf2005_test_path = get_example_data_path(Path(__file__)) / "mf2005_test"
__mf2005_test_path = get_example_data_path() / "mf2005_test"


@pytest.mark.parametrize(
Expand Down
2 changes: 1 addition & 1 deletion autotest/test_mp6.py
Expand Up @@ -35,7 +35,7 @@ def copy_modpath_files(source, model_ws, baseName):
and os.path.isfile(os.path.join(source, file))
]
for file in files:
src = str(get_example_data_path(__file__) / "mp6" / file)
src = str(get_example_data_path() / "mp6" / file)
dst = os.path.join(model_ws, file)
print(f"copying {src} -> {dst}")
shutil.copy(src, dst)
Expand Down
2 changes: 1 addition & 1 deletion autotest/test_seawat.py
Expand Up @@ -196,7 +196,7 @@ def test_seawat2_henry(tmpdir):
def swt4_namfiles():
return [
str(p)
for p in (get_example_data_path(__file__) / "swtv4_test").rglob(
for p in (get_example_data_path() / "swtv4_test").rglob(
"*.nam"
)
]
Expand Down
2 changes: 1 addition & 1 deletion autotest/test_sfr.py
Expand Up @@ -916,7 +916,7 @@ def test_isfropt_icalc(tmpdir, example_data_path, isfropt, icalc):
)


__example_data_path = get_example_data_path(Path(__file__))
__example_data_path = get_example_data_path()


@requires_exe("mf2005dbl")
Expand Down
2 changes: 1 addition & 1 deletion autotest/test_usg.py
Expand Up @@ -389,7 +389,7 @@ def test_flat_array_to_util3d_usg(tmpdir, freyberg_usg_model_path):
"fpth",
[
str(p)
for p in (get_example_data_path(Path(__file__)) / "mfusg_test").rglob(
for p in (get_example_data_path() / "mfusg_test").rglob(
"*.nam"
)
],
Expand Down

0 comments on commit 7973c70

Please sign in to comment.