Skip to content

Commit a9c2c02

Browse files
committed
test(multiple): expand tests and developer docs
* add DEVELOPER.md with basic install/testing info * add conftest.py with temp dir and misc fixtures * add tests for fixtures in conftest.py * add test for meson_build function * add PyGithub as test dep to setup.cfg * checkout modflow6 before testing in CI
1 parent 02933fa commit a9c2c02

7 files changed

Lines changed: 295 additions & 4 deletions

File tree

.github/workflows/ci.yml

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,20 +62,31 @@ jobs:
6262

6363
- name: Checkout repo
6464
uses: actions/checkout@v3
65+
with:
66+
path: modflow-devtools
67+
68+
- name: Checkout modflow6
69+
uses: actions/checkout@v3
70+
with:
71+
path: modflow6
6572

6673
- name: Setup Python
6774
uses: actions/setup-python@v4
6875
with:
6976
python-version: ${{ matrix.python }}
7077
cache: 'pip'
71-
cache-dependency-path: setup.cfg
78+
cache-dependency-path: modflow-devtools/setup.cfg
7279

7380
- name: Install Python packages
81+
working-directory: modflow-devtools
7482
run: |
7583
pip3 install .
7684
pip3 install ".[test]"
7785
7886
- name: Run tests
87+
working-directory: modflow-devtools
88+
env:
89+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
7990
run: pytest -v -n auto --durations 0
8091

8192
publish:

DEVELOPER.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# Developing `modflow-devtools`
2+
3+
This document provides guidance to set up a development environment and discusses conventions used in this project.
4+
5+
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
6+
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
7+
8+
- [Developing `modflow-devtools`](#developing-modflow-devtools)
9+
- [Installation](#installation)
10+
- [Testing](#testing)
11+
- [Environment variables](#environment-variables)
12+
- [Running the tests](#running-the-tests)
13+
14+
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
15+
16+
## Installation
17+
18+
To get started, first fork and clone this repository.
19+
20+
## Testing
21+
22+
This project uses [`pytest`](https://docs.pytest.org/en/latest/) and several plugins. [`PyGithub`](https://github.com/PyGithub/PyGithub) is used to communicate with the GitHub API.
23+
24+
### Environment variables
25+
26+
#### `GITHUB_TOKEN`
27+
28+
Tests require access to the GitHub API &mdash; in order to avoid rate limits, the tests attempt to authenticate with an access token. In C, this is the `GITHUB_TOKEN` provided by GitHub Actions. For local development a personal access token must be used. Setting the `GITHUB_TOKEN` variable manually will work, but the recommended approach is to use a `.env` file in your project root (the tests will automatically discover and use any environment variables configured here courtesy of [`pytest-dotenv`](https://github.com/quiqua/pytest-dotenv)).
29+
30+
#### `MODFLOW6_PATH`
31+
32+
By default, ths project's tests look for the `modflow6` repository side-by-side with `modflow-devtools` on the filesystem. The `MODFLOW6_PATH` variable is optional and can be used to configure a different location for the `modflow6` repo.
33+
34+
### Running the tests
35+
36+
To run the tests in parallel with verbose output, run from the project root:
37+
38+
```shell
39+
pytest -v -n auto
40+
```
41+
42+
### Writing new tests
43+
44+
Tests should follow a few conventions for ease of use and maintenance.
45+
46+
#### Temporary directories
47+
48+
If tests must write to disk, they should use `pytest`'s built-in `temp_dir` fixture or one of the scoped temporary directory fixtures defined in `conftest.py`.
49+
50+
#### Using the GitHub API
51+
52+
To access the GitHub API from a test case, just construct a `Github` object:
53+
54+
```python
55+
api = Github(environ.get("GITHUB_TOKEN"))
56+
```

modflow_devtools/build.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ def meson_build(
3434
print(f"Running: {cmd}")
3535
subprocess.run(cmd, shell=True, check=True)
3636

37-
cmd = "meson install -C builddir"
37+
cmd = f"meson install -C {bld_path}"
3838
if not quiet:
3939
print(f"Running: {cmd}")
4040
subprocess.run(cmd, shell=True, check=True)

modflow_devtools/test/conftest.py

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
from os import environ
2+
from pathlib import Path
3+
4+
import pytest
5+
from github import Github
6+
7+
proj_root = Path(__file__).parent.parent.parent.parent
8+
9+
10+
# keepable temporary directory fixtures for various scopes
11+
12+
13+
@pytest.fixture(scope="function")
14+
def tmpdir(tmpdir_factory, request) -> Path:
15+
node = (
16+
request.node.name.replace("/", "_")
17+
.replace("\\", "_")
18+
.replace(":", "_")
19+
)
20+
temp = Path(tmpdir_factory.mktemp(node))
21+
yield Path(temp)
22+
23+
keep = request.config.getoption("--keep")
24+
if keep:
25+
copytree(temp, Path(keep) / temp.name)
26+
27+
keep_failed = request.config.getoption("--keep-failed")
28+
if keep_failed and request.node.rep_call.failed:
29+
copytree(temp, Path(keep_failed) / temp.name)
30+
31+
32+
@pytest.fixture(scope="class")
33+
def class_tmpdir(tmpdir_factory, request) -> Path:
34+
assert (
35+
request.cls is not None
36+
), "Class-scoped temp dir fixture must be used on class"
37+
temp = Path(tmpdir_factory.mktemp(request.cls.__name__))
38+
yield temp
39+
40+
keep = request.config.getoption("--keep")
41+
if keep:
42+
copytree(temp, Path(keep) / temp.name)
43+
44+
45+
@pytest.fixture(scope="module")
46+
def module_tmpdir(tmpdir_factory, request) -> Path:
47+
temp = Path(tmpdir_factory.mktemp(request.module.__name__))
48+
yield temp
49+
50+
keep = request.config.getoption("--keep")
51+
if keep:
52+
copytree(temp, Path(keep) / temp.name)
53+
54+
55+
@pytest.fixture(scope="session")
56+
def session_tmpdir(tmpdir_factory, request) -> Path:
57+
temp = Path(tmpdir_factory.mktemp(request.session.name))
58+
yield temp
59+
60+
keep = request.config.getoption("--keep")
61+
if keep:
62+
copytree(temp, Path(keep) / temp.name)
63+
64+
65+
# misc fixtures
66+
67+
68+
@pytest.fixture
69+
def gh_api() -> Github:
70+
return Github(environ.get("GITHUB_TOKEN"))
71+
72+
73+
@pytest.fixture
74+
def modflow6_path() -> Path:
75+
return Path(environ.get("MODFLOW6_PATH", proj_root / "modflow6"))
76+
77+
78+
# pytest configuration hooks
79+
80+
81+
def pytest_addoption(parser):
82+
parser.addoption(
83+
"-K",
84+
"--keep",
85+
action="store",
86+
default=None,
87+
help="Move the contents of temporary test directories to correspondingly named subdirectories at the given "
88+
"location after tests complete. This option can be used to exclude test results from automatic cleanup, "
89+
"e.g. for manual inspection. The provided path is created if it does not already exist. An error is "
90+
"thrown if any matching files already exist.",
91+
)
92+
93+
parser.addoption(
94+
"--keep-failed",
95+
action="store",
96+
default=None,
97+
help="Move the contents of temporary test directories to correspondingly named subdirectories at the given "
98+
"location if the test case fails. This option automatically saves the outputs of failed tests in the "
99+
"given location. The path is created if it doesn't already exist. An error is thrown if files with the "
100+
"same names already exist in the given location.",
101+
)
Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,34 @@
1-
def test_meson_build():
2-
pass
1+
import platform
2+
3+
from modflow_devtools.build import meson_build
4+
5+
system = platform.system()
6+
7+
8+
def test_meson_build(modflow6_path, tmpdir):
9+
bld_path = tmpdir / "builddir"
10+
bin_path = tmpdir / "bin"
11+
lib_path = bin_path
12+
13+
meson_build(modflow6_path, bld_path, bin_path, bin_path, quiet=False)
14+
15+
# check build directory was populated
16+
assert (bld_path / "build.ninja").is_file()
17+
assert (bld_path / "src").is_dir()
18+
assert (bld_path / "meson-logs").is_dir()
19+
20+
# check binaries and libraries were created
21+
ext = ".exe" if system == "Windows" else ""
22+
for exe in ["mf6", "mf5to6", "zbud6"]:
23+
assert (bin_path / f"{exe}{ext}").is_file()
24+
assert (
25+
bin_path
26+
/ (
27+
"libmf6"
28+
+ (
29+
".so"
30+
if system == "Linux"
31+
else (".dylib" if system == "Darwin" else "")
32+
)
33+
)
34+
).is_file()
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import inspect
2+
from os import environ
3+
from pathlib import Path
4+
5+
import pytest
6+
7+
proj_root = Path(__file__).parent.parent.parent.parent
8+
9+
10+
# test environment variables
11+
12+
13+
def test_environment():
14+
assert environ.get("GITHUB_TOKEN")
15+
assert Path(environ.get("MODFLOW6_PATH", proj_root / "modflow6")).is_dir()
16+
17+
18+
# temporary directory fixtures
19+
20+
21+
def test_tmpdirs(tmpdir, module_tmpdir):
22+
# function-scoped temporary directory
23+
assert isinstance(tmpdir, Path)
24+
assert tmpdir.is_dir()
25+
assert inspect.currentframe().f_code.co_name in tmpdir.stem
26+
27+
# module-scoped temp dir (accessible to other tests in the script)
28+
assert module_tmpdir.is_dir()
29+
assert "test" in module_tmpdir.stem
30+
31+
32+
def test_function_scoped_tmpdir(tmpdir):
33+
assert isinstance(tmpdir, Path)
34+
assert tmpdir.is_dir()
35+
assert inspect.currentframe().f_code.co_name in tmpdir.stem
36+
37+
38+
@pytest.mark.parametrize("name", ["noslash", "forward/slash", "back\\slash"])
39+
def test_function_scoped_tmpdir_slash_in_name(tmpdir, name):
40+
assert isinstance(tmpdir, Path)
41+
assert tmpdir.is_dir()
42+
43+
# node name might have slashes if test function is parametrized
44+
# (e.g., test_function_scoped_tmpdir_slash_in_name[a/slash])
45+
replaced1 = name.replace("/", "_").replace("\\", "_").replace(":", "_")
46+
replaced2 = name.replace("/", "_").replace("\\", "__").replace(":", "_")
47+
assert (
48+
f"{inspect.currentframe().f_code.co_name}[{replaced1}]" in tmpdir.stem
49+
or f"{inspect.currentframe().f_code.co_name}[{replaced2}]"
50+
in tmpdir.stem
51+
)
52+
53+
54+
class TestClassScopedTmpdir:
55+
filename = "hello.txt"
56+
57+
@pytest.fixture(autouse=True)
58+
def setup(self, class_tmpdir):
59+
with open(class_tmpdir / self.filename, "w") as file:
60+
file.write("hello, class-scoped tmpdir")
61+
62+
def test_class_scoped_tmpdir(self, class_tmpdir):
63+
assert isinstance(class_tmpdir, Path)
64+
assert class_tmpdir.is_dir()
65+
assert self.__class__.__name__ in class_tmpdir.stem
66+
assert Path(class_tmpdir / self.filename).is_file()
67+
68+
69+
def test_module_scoped_tmpdir(module_tmpdir):
70+
assert isinstance(module_tmpdir, Path)
71+
assert module_tmpdir.is_dir()
72+
assert Path(inspect.getmodulename(__file__)).stem in module_tmpdir.name
73+
74+
75+
def test_session_scoped_tmpdir(session_tmpdir):
76+
assert isinstance(session_tmpdir, Path)
77+
assert session_tmpdir.is_dir()
78+
79+
80+
# test misc fixtures
81+
82+
83+
def test_github_api(gh_api):
84+
assert gh_api.get_user().login
85+
86+
87+
def test_modflow6_path(modflow6_path):
88+
assert modflow6_path.is_dir()
89+
assert (modflow6_path / "version.txt").is_file()

setup.cfg

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,10 @@ test =
5656
%(lint)s
5757
coverage
5858
flaky
59+
PyGithub
5960
pytest
6061
pytest-cov
62+
pytest-dotenv
6163
pytest-xdist
6264

6365
[flake8]

0 commit comments

Comments
 (0)