diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 70ca413..7941bc9 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -26,7 +26,7 @@ RUN python -m pip install --upgrade pip COPY . . # install devcontainer requirements -RUN pip install -e .[dev] +RUN pip install -e .[dev,test] # install docs requirements RUN pip install --no-cache-dir -r docs/requirements.txt diff --git a/.github/workflows/.python-version b/.github/workflows/.python-version new file mode 100644 index 0000000..e4fba21 --- /dev/null +++ b/.github/workflows/.python-version @@ -0,0 +1 @@ +3.12 diff --git a/.github/workflows/tests-pytest.yml b/.github/workflows/tests-pytest.yml new file mode 100644 index 0000000..5c4a066 --- /dev/null +++ b/.github/workflows/tests-pytest.yml @@ -0,0 +1,51 @@ +name: Pytest + +on: [push, pull_request, workflow_call] + +jobs: + pytest: + runs-on: ubuntu-latest + permissions: + # Gives the action the necessary permissions for publishing new + # comments in pull requests. + pull-requests: write + # Gives the action the necessary permissions for pushing data to the + # python-coverage-comment-action branch, and for editing existing + # comments (to avoid publishing multiple comments in the same PR) + contents: write + steps: + - name: Check out code + uses: actions/checkout@v4 + + - name: Install system packages + run: | + sudo apt-get update -y + sudo apt-get install -y gettext + + - uses: actions/setup-python@v5 + with: + python-version-file: .github/workflows/.python-version + cache: pip + cache-dependency-path: "**/pyproject.toml" + + - name: Install Python dependencies + run: pip install -e .[test] + + - name: Run setup + run: ./bin/init.sh + + - name: Run tests + run: ./tests/pytest/run.sh + + - name: Upload coverage report + uses: actions/upload-artifact@v4 + with: + name: coverage-report + path: pems/static/coverage + + - name: Coverage comment + uses: py-cov-action/python-coverage-comment-action@v3 + with: + GITHUB_TOKEN: ${{ github.token }} + MINIMUM_GREEN: 90 + MINIMUM_ORANGE: 80 diff --git a/.vscode/settings.json b/.vscode/settings.json index 1698f7b..d34973e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -21,6 +21,9 @@ "editor.defaultFormatter": "ms-python.black-formatter" }, "python.languageServer": "Pylance", + "python.testing.pytestArgs": ["tests/pytest"], + "python.testing.pytestEnabled": true, + "python.testing.unittestEnabled": false, "workbench.editorAssociations": { "*.db": "sqlite-viewer.option" } diff --git a/pems/core/__init__.py b/pems/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pems/core/middleware.py b/pems/core/middleware.py new file mode 100644 index 0000000..284c2ee --- /dev/null +++ b/pems/core/middleware.py @@ -0,0 +1,19 @@ +""" +The core application: middleware definitions for request/response cycle. +""" + +from django.http import HttpResponse + +HEALTHCHECK_PATH = "/healthcheck" + + +class Healthcheck: + """Middleware intercepts and accepts /healthcheck requests.""" + + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + if request.path == HEALTHCHECK_PATH: + return HttpResponse("Healthy", content_type="text/plain") + return self.get_response(request) diff --git a/pems/settings.py b/pems/settings.py index d034edf..d834a1f 100644 --- a/pems/settings.py +++ b/pems/settings.py @@ -36,6 +36,7 @@ def _filter_empty(ls): MIDDLEWARE = [ "django.middleware.security.SecurityMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", + "pems.core.middleware.Healthcheck", "django.middleware.common.CommonMiddleware", "django.middleware.csrf.CsrfViewMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", diff --git a/pyproject.toml b/pyproject.toml index 82f7ddc..1ddcf1e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,6 +20,14 @@ dev = [ "pre-commit" ] +test = [ + "coverage", + "pytest", + "pytest-django", + "pytest-mock", + "pytest-socket", +] + [project.urls] Code = "https://github.com/compilerla/pems" Homepage = "https://compilerla.github.io/pems/" @@ -34,6 +42,14 @@ line-length = 127 target-version = ['py312'] include = '\.pyi?$' +[tool.coverage.run] +branch = true +relative_files = true +source = ["pems"] + +[tool.pytest.ini_options] +DJANGO_SETTINGS_MODULE = "pems.settings" + [tool.setuptools.packages.find] include = ["pems*"] namespaces = false diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/pytest/__init__.py b/tests/pytest/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/pytest/conftest.py b/tests/pytest/conftest.py new file mode 100644 index 0000000..b4e630a --- /dev/null +++ b/tests/pytest/conftest.py @@ -0,0 +1,5 @@ +from pytest_socket import disable_socket + + +def pytest_runtest_setup(): + disable_socket() diff --git a/tests/pytest/core/__init__.py b/tests/pytest/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/pytest/core/test_middleware_healthcheck.py b/tests/pytest/core/test_middleware_healthcheck.py new file mode 100644 index 0000000..1519fa0 --- /dev/null +++ b/tests/pytest/core/test_middleware_healthcheck.py @@ -0,0 +1,6 @@ +from pems.core.middleware import HEALTHCHECK_PATH + + +def test_healthcheck(client): + response = client.get(HEALTHCHECK_PATH) + assert response.status_code == 200 diff --git a/tests/pytest/run.sh b/tests/pytest/run.sh new file mode 100755 index 0000000..a1fbf69 --- /dev/null +++ b/tests/pytest/run.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +set -eu + +# run normal pytests +coverage run -m pytest + +# clean out old coverage results +rm -rf pems/static/coverage + +coverage html --directory pems/static/coverage