From d8a13560c600faad55432bec6a1daa98387ea80e Mon Sep 17 00:00:00 2001 From: Ewout ter Hoeven Date: Tue, 3 Sep 2024 21:03:12 +0200 Subject: [PATCH] Add test script for GIS examples and run that in CI (#241) This PR adds a script to test all the GIS examples from the mesa-examples gis folder with pytest. It also adds a GitHub Actions CI workflow that runs that script continuously in CI. - tests/test_GIS_examples.py, lend from Mesa itself. - .github/workflows/examples.yml, also lend from Mesa - pyproject.toml: Adds optional dependencies to test the examples. --- .github/workflows/examples.yml | 40 ++++++++++++++++++++ pyproject.toml | 4 ++ tests/test_GIS_examples.py | 68 ++++++++++++++++++++++++++++++++++ 3 files changed, 112 insertions(+) create mode 100644 .github/workflows/examples.yml create mode 100644 tests/test_GIS_examples.py diff --git a/.github/workflows/examples.yml b/.github/workflows/examples.yml new file mode 100644 index 00000000..8fac9970 --- /dev/null +++ b/.github/workflows/examples.yml @@ -0,0 +1,40 @@ +name: Test GIS examples + +on: + push: + branches: + - main + - release** + - "**maintenance" + paths-ignore: + - '**.md' + pull_request: + paths-ignore: + - '**.md' + workflow_dispatch: + schedule: + - cron: '0 6 * * 1' + +jobs: + examples: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + cache: 'pip' + - name: Install uv + run: pip install uv + - name: Install Mesa + run: uv pip install --system .[examples] + - name: Checkout mesa-examples + uses: actions/checkout@v4 + with: + repository: projectmesa/mesa-examples + path: mesa-examples + - name: Test examples + run: | + cd mesa-examples + pytest -rA -Werror -Wdefault::FutureWarning test_gis_examples.py diff --git a/pyproject.toml b/pyproject.toml index df22e10b..1eb15065 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -69,6 +69,10 @@ docs = [ "myst_nb", "myst-parser", # Markdown in Sphinx ] +examples = [ + "pytest", + "momepy", +] [project.urls] homepage = "https://github.com/projectmesa/mesa-geo" diff --git a/tests/test_GIS_examples.py b/tests/test_GIS_examples.py new file mode 100644 index 00000000..0e041d35 --- /dev/null +++ b/tests/test_GIS_examples.py @@ -0,0 +1,68 @@ +import contextlib +import importlib +import os.path +import sys +import unittest + + +def classcase(name): + return "".join(x.capitalize() for x in name.replace("-", "_").split("_")) + + +@unittest.skip( + "Skipping TextExamples, because examples folder was moved. More discussion needed." +) +class TestExamples(unittest.TestCase): + """ + Test examples' models. This creates a model object and iterates it through + some steps. The idea is to get code coverage, rather than to test the + details of each example's model. + """ + + EXAMPLES = os.path.abspath(os.path.join(os.path.dirname(__file__), "../gis")) + + @contextlib.contextmanager + def active_example_dir(self, example): + "save and restore sys.path and sys.modules" + old_sys_path = sys.path[:] + old_sys_modules = sys.modules.copy() + old_cwd = os.getcwd() + example_path = os.path.abspath(os.path.join(self.EXAMPLES, example)) + try: + sys.path.insert(0, example_path) + os.chdir(example_path) + yield + finally: + os.chdir(old_cwd) + added = [m for m in sys.modules if m not in old_sys_modules] + for mod in added: + del sys.modules[mod] + sys.modules.update(old_sys_modules) + sys.path[:] = old_sys_path + + def test_examples(self): + for example in os.listdir(self.EXAMPLES): + if not os.path.isdir(os.path.join(self.EXAMPLES, example)): + continue + if hasattr(self, f"test_{example.replace('-', '_')}"): + # non-standard example; tested below + continue + + print(f"testing example {example!r}") + with self.active_example_dir(example): + try: + # model.py at the top level + mod = importlib.import_module("model") + server = importlib.import_module("server") + server.server.render_model() + except ImportError: + # /model.py + mod = importlib.import_module(f"{example.replace('-', '_')}.model") + server = importlib.import_module( + f"{example.replace('-', '_')}.server" + ) + server.server.render_model() + model_class = getattr(mod, classcase(example)) + model = model_class() + for _ in range(10): + model.step()