Skip to content

Commit

Permalink
move tests to autotest/, remove BIN_PATH testing env var, simplify te…
Browse files Browse the repository at this point in the history
…sts, add aliases, update docs
  • Loading branch information
wpbonelli committed Nov 10, 2023
1 parent 6cc044c commit cdd5eb8
Show file tree
Hide file tree
Showing 21 changed files with 210 additions and 165 deletions.
9 changes: 3 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ jobs:
fail-fast: false
matrix:
os: [ ubuntu-22.04, macos-12, windows-2022 ]
python: [ 3.8, 3.9, "3.10", "3.11" ]
python: [ 3.8, 3.9, "3.10", "3.11", "3.12" ]
env:
GCC_V: 11
steps:
Expand Down Expand Up @@ -159,11 +159,9 @@ jobs:
run: python ci_build_files.py

- name: Run local tests
working-directory: modflow-devtools
working-directory: modflow-devtools/autotest
env:
BIN_PATH: ~/.local/bin/modflow
REPOS_PATH: ${{ github.workspace }}
GITHUB_TOKEN: ${{ github.token }}
# use --dist loadfile to so tests requiring pytest-virtualenv run on the same worker
run: pytest -v -n auto --dist loadfile --durations 0 --ignore modflow_devtools/test/test_download.py

Expand All @@ -172,9 +170,8 @@ jobs:
# to avoid rate limits (1000 rqs / hour / repository)
# https://docs.github.com/en/actions/learn-github-actions/usage-limits-billing-and-administration#usage-limits
if: runner.os == 'Linux' && matrix.python == '3.8'
working-directory: modflow-devtools
working-directory: modflow-devtools/autotest
env:
BIN_PATH: ~/.local/bin/modflow
REPOS_PATH: ${{ github.workspace }}
GITHUB_TOKEN: ${{ github.token }}
run: pytest -v -n auto --durations 0 modflow_devtools/test/test_download.py
10 changes: 3 additions & 7 deletions DEVELOPER.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,37 +38,33 @@ This repository's tests use [`pytest`](https://docs.pytest.org/en/latest/) and s

This repository's tests expect a few environment variables:

- `BIN_PATH`: path to MODFLOW 6 and related executables
- `REPOS_PATH`: the path to MODFLOW 6 example model repositories
- `GITHUB_TOKEN`: a GitHub authentication token

These may be set manually, but the recommended approach is to configure environment variables in a `.env` file in the project root, for instance:

```
BIN_PATH=/path/to/modflow/executables
REPOS_PATH=/path/to/repos
GITHUB_TOKEN=yourtoken...
```

The tests use [`pytest-dotenv`](https://github.com/quiqua/pytest-dotenv) to detect and load variables from this file.

**Note:** at minimum, the tests require that the `mf6` (or `mf6.exe` on Windows) executable is present in `BIN_PATH`.

### Running the tests

Tests should be run from the project root. To run the tests in parallel with verbose output:
Tests should be run from the `autotest` directory. To run the tests in parallel with verbose output:

```shell
pytest -v -n auto
```

### Writing new tests

Tests should follow a few conventions for ease of use and maintenance.
Tests follow a few conventions for ease of use and maintenance.

#### Temporary directories

Tests which must write to disk should use `pytest`'s built-in `temp_dir` fixture or one of this package's own scoped temporary directory fixtures.
Tests which must write to disk use `pytest`'s built-in `temp_dir` fixture or one of this package's own scoped temporary directory fixtures.

## Releasing

Expand Down
File renamed without changes.
File renamed without changes.
8 changes: 8 additions & 0 deletions autotest/pytest.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[pytest]
addopts = -ra --color=yes
python_files =
test_*.py
*_test*.py
markers =
slow: tests that don't complete in a few seconds
meta: run by other tests (e.g. testing fixtures)
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from pathlib import Path

import pytest

from modflow_devtools.build import meson_build
from modflow_devtools.markers import requires_pkg

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import pytest
from flaky import flaky

from modflow_devtools.download import (
download_and_unzip,
download_artifact,
Expand Down
38 changes: 38 additions & 0 deletions autotest/test_executables.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import subprocess
import sys
from pathlib import Path
from shutil import which

import pytest

from modflow_devtools.executables import Executables
from modflow_devtools.misc import get_suffixes, add_sys_path

ext, _ = get_suffixes(sys.platform)
exe_stem = "pytest"
exe_path = Path(which(exe_stem))
bin_path = exe_path.parent
exe = f"{exe_stem}{ext}"


@pytest.fixture
def exes():
with add_sys_path(bin_path):
yield Executables(**{exe_stem: bin_path / exe})


def test_access(exes):
# support both attribute and dictionary style access
assert exes.pytest == exes["pytest"] == exe_path


def test_get_version(exes):
ver_str = Executables.get_version(exes.pytest)
version = (
subprocess.check_output([f"{exes.pytest}", "-v"])
.decode("utf-8")
.split(":")[1]
.strip()
)
assert ver_str == version
assert int(ver_str[0].split(".")[0]) >= 6
File renamed without changes.
59 changes: 59 additions & 0 deletions autotest/test_markers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
from os import environ
from platform import python_version, system
from shutil import which

from packaging.version import Version

from modflow_devtools.markers import *

exe = "pytest"


@requires_exe(exe)
def test_require_exe():
assert which(exe)
require_exe(exe)
require_program(exe)


exes = [exe, "python"]


@require_exe(*exes)
def test_require_exe_multiple():
assert all(which(exe) for exe in exes)


@requires_pkg("pytest")
def test_requires_pkg():
import numpy

assert numpy is not None


@requires_pkg("pytest", "pluggy")
def test_requires_pkg_multiple():
import pluggy
import pytest

assert pluggy is not None and pytest is not None


@requires_platform("Windows")
def test_requires_platform():
assert system() == "Windows"


@excludes_platform("Darwin", ci_only=True)
def test_requires_platform_ci_only():
if "CI" in environ:
assert system() != "Darwin"


py_ver = python_version()


@pytest.mark.parametrize("version", ["3.12", "3.11"])
def test_requires_python(version):
if Version(py_ver) >= Version(version):
assert requires_python(version)
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from typing import List

import pytest
from conftest import project_root_path

from modflow_devtools.misc import (
get_model_paths,
get_namefile_paths,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from platform import system

import pytest

from modflow_devtools.ostags import (
OSTag,
get_binary_suffixes,
Expand Down
83 changes: 33 additions & 50 deletions modflow_devtools/test/test_zip.py → autotest/test_zip.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,91 +2,74 @@
import shutil
import sys
import zipfile
from os import environ
from pathlib import Path
from pprint import pprint
from shutil import which
from zipfile import ZipFile

import pytest

from modflow_devtools.markers import excludes_platform
from modflow_devtools.misc import get_suffixes, set_dir
from modflow_devtools.zip import MFZipFile

_bin_path = Path(environ.get("BIN_PATH")).expanduser().absolute()
_ext, _ = get_suffixes(sys.platform)


@pytest.fixture(scope="module")
def empty_archive(module_tmpdir) -> Path:
# https://stackoverflow.com/a/25195628/6514033
data = b"PK\x05\x06\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
path = module_tmpdir / "empty.zip"

with open(path, "wb") as zip:
zip.write(data)

return path


@pytest.fixture(scope="module")
def nonempty_archive(module_tmpdir) -> Path:
if not _bin_path.is_dir():
pytest.skip(f"BIN_PATH ({_bin_path}) is not a directory")

zip_path = module_tmpdir / "nonempty.zip"
txt_path = module_tmpdir / "hw.txt"
exe_path = _bin_path / f"mf6{_ext}"

# create a zip file with a text file and an executable
shutil.copy(exe_path, module_tmpdir)
with open(txt_path, "w") as f:
f.write("hello world")

with set_dir(module_tmpdir):
zip = MFZipFile(zip_path.name, "w")
zip.write(txt_path.name, compress_type=zipfile.ZIP_DEFLATED)
zip.write(exe_path.name, compress_type=zipfile.ZIP_DEFLATED)
zip.close()

return zip_path
ext, _ = get_suffixes(sys.platform)
exe_stem = "pytest"
exe_path = Path(which(exe_stem))
bin_path = exe_path.parent
exe_name = f"{exe_stem}{ext}"


def test_compressall(function_tmpdir):
zip_file = function_tmpdir / "output.zip"
input_dir = function_tmpdir / "input"
input_dir.mkdir()

with open(input_dir / "data.txt", "w") as f:
f.write("hello world")

MFZipFile.compressall(str(zip_file), dir_pths=str(input_dir))

pprint(list(function_tmpdir.iterdir()))
assert zip_file.exists()

output_dir = function_tmpdir / "output"
output_dir.mkdir()

ZipFile(zip_file).extractall(path=str(output_dir))

pprint(list(output_dir.iterdir()))
assert (output_dir / "data.txt").is_file()


@pytest.fixture(scope="module")
def empty_archive(module_tmpdir) -> Path:
# https://stackoverflow.com/a/25195628/6514033
data = b"PK\x05\x06\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
path = module_tmpdir / "empty.zip"
with open(path, "wb") as zip:
zip.write(data)
return path


def test_extractall_empty(empty_archive, function_tmpdir):
zf = MFZipFile(empty_archive, "r")
zf.extractall(str(function_tmpdir))

assert not any(function_tmpdir.iterdir())


@pytest.fixture(scope="module")
def archive(module_tmpdir) -> Path:
zip_path = module_tmpdir / "nonempty.zip"
shutil.copy(exe_path, module_tmpdir)
with set_dir(module_tmpdir):
zip = MFZipFile(zip_path.name, "w")
zip.write(exe_path.name, compress_type=zipfile.ZIP_DEFLATED)
zip.close()
return zip_path


@pytest.mark.parametrize("mf", [True, False])
@excludes_platform("Windows")
def test_preserves_execute_permission(function_tmpdir, nonempty_archive, mf):
zip = MFZipFile(nonempty_archive) if mf else ZipFile(nonempty_archive)
def test_extractall_preserves_execute_permission(function_tmpdir, archive, mf):
zip = MFZipFile(archive) if mf else ZipFile(archive)
zip.extractall(path=str(function_tmpdir))

exe_path = function_tmpdir / f"mf6{_ext}"

assert exe_path.is_file()
assert os.access(exe_path, os.X_OK) == mf
path = function_tmpdir / exe_name
assert path.is_file()
assert os.access(path, os.X_OK) == mf
9 changes: 8 additions & 1 deletion docs/md/markers.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,4 +78,11 @@ Both these markers accept a `ci_only` flag, which indicates whether the policy s
Markers are also provided to ping network resources and skip if unavailable:

- `@requires_github`: skips if `github.com` is unreachable
- `@requires_spatial_reference`: skips if `spatialreference.org` is unreachable
- `@requires_spatial_reference`: skips if `spatialreference.org` is unreachable

## Aliases

All markers are aliased to imperative mood, e.g. `require_github`. Some have other aliases as well:

`requires_pkg` -> `require[s]_package`
`requires_exe` -> `require[s]_program`
2 changes: 1 addition & 1 deletion modflow_devtools/download.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ def get_response_json():
return json.loads(resp.read().decode())
except urllib.error.HTTPError as err:
if err.code == 401 and os.environ.get("GITHUB_TOKEN"):
raise ValueError("GITHUB_TOKEN env is invalid") from err
raise ValueError("GITHUB_TOKEN is invalid") from err
elif err.code == 403 and "rate limit exceeded" in err.reason:
raise ValueError(
f"use GITHUB_TOKEN env to bypass rate limit ({err})"
Expand Down
Loading

0 comments on commit cdd5eb8

Please sign in to comment.