From 4627487fdd31e078c407ab8e52c8ab41d967563a Mon Sep 17 00:00:00 2001 From: Marcus Messer Date: Mon, 31 Aug 2026 11:03:30 +0100 Subject: [PATCH] test: add pytest suite alongside doctests Adds a real tests/ suite so the project no longer relies on doctests alone: - tests/test_runner.py runs each built-in filter over its own example.tex end to end, asserting on the returned Set and on the JSON/ZIP written to disk. - [tool.pytest.ini_options] collects both tests/ and the package doctests, so a bare `pytest` covers everything. - CI: `black .` -> `black --check .` (no longer silently reformats), and isort/pydocstyle now also cover tests/. Applies black to two pre-existing files (visibility_status.py, json_convert.py) that were not clean under `black --check`. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_017VXb8aZqgFBjoeuuddjW6r --- .github/workflows/test.yml | 8 ++-- in2lambda/api/visibility_status.py | 1 + in2lambda/json_convert/json_convert.py | 5 ++- pyproject.toml | 5 +++ tests/conftest.py | 16 ++++++++ tests/test_runner.py | 57 ++++++++++++++++++++++++++ 6 files changed, 87 insertions(+), 5 deletions(-) create mode 100644 tests/conftest.py create mode 100644 tests/test_runner.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c0288e3..d2e798f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -24,11 +24,11 @@ jobs: uses: r-lib/actions/setup-pandoc@v2 - name: Linting Checks run: | - poetry run black . - poetry run isort --check-only in2lambda docs - poetry run pydocstyle --convention=google in2lambda + poetry run black --check . + poetry run isort --check-only in2lambda docs tests + poetry run pydocstyle --convention=google in2lambda tests - name: pytest - run: poetry run pytest --cov-report=xml:coverage.xml --cov=in2lambda --doctest-modules in2lambda + run: poetry run pytest --cov-report=xml:coverage.xml --cov=in2lambda - name: Upload coverage to Codecov uses: codecov/codecov-action@v3 with: diff --git a/in2lambda/api/visibility_status.py b/in2lambda/api/visibility_status.py index 541c97d..c0295dd 100644 --- a/in2lambda/api/visibility_status.py +++ b/in2lambda/api/visibility_status.py @@ -2,6 +2,7 @@ from enum import Enum + class VisibilityStatus(Enum): """Enum representing the visibility status of a question or set.""" diff --git a/in2lambda/json_convert/json_convert.py b/in2lambda/json_convert/json_convert.py index 73b49eb..dd85f23 100644 --- a/in2lambda/json_convert/json_convert.py +++ b/in2lambda/json_convert/json_convert.py @@ -98,7 +98,10 @@ def converter( # Output file filename = ( - "question_" + str(i).zfill(3) + "_" + re.sub(r'[^\w\-_.]', '_', output['title'].strip()) + "question_" + + str(i).zfill(3) + + "_" + + re.sub(r"[^\w\-_.]", "_", output["title"].strip()) ) # write questions into directory diff --git a/pyproject.toml b/pyproject.toml index 7929d25..e1b4022 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -60,6 +60,11 @@ ignore_missing_imports = true [tool.isort] profile = "black" +[tool.pytest.ini_options] +# Collect both the unit tests in tests/ and the doctests embedded in the package. +testpaths = ["tests", "in2lambda"] +addopts = "--doctest-modules" + [build-system] requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..095b418 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,16 @@ +"""Shared pytest fixtures for the in2lambda test suite.""" + +import os + +import pytest + +import in2lambda + + +@pytest.fixture(scope="session") +def filters_dir() -> str: + """Absolute path to the packaged ``filters`` directory. + + Each filter ships a self-contained ``example.tex`` used by the end-to-end tests. + """ + return os.path.join(os.path.dirname(in2lambda.__file__), "filters") diff --git a/tests/test_runner.py b/tests/test_runner.py new file mode 100644 index 0000000..df31429 --- /dev/null +++ b/tests/test_runner.py @@ -0,0 +1,57 @@ +"""End-to-end tests for :func:`in2lambda.main.runner` across the built-in filters. + +Each built-in filter ships a self-contained ``example.tex`` that exercises the +document structure it targets. These tests run every filter over its own example +and check both the in-memory :class:`~in2lambda.api.set.Set` and the JSON/ZIP +files written to disk. +""" + +import json +import os + +import pytest + +from in2lambda.api.set import Set +from in2lambda.main import runner + +BUILTIN_FILTERS = ["PartsSepSol", "PartsOneSol", "PartPartSolSol", "PartSolPartSol"] + + +@pytest.mark.parametrize("filter_name", BUILTIN_FILTERS) +def test_runner_returns_populated_set(filter_name: str, filters_dir: str) -> None: + """Every filter turns its example into a Set with at least one usable question.""" + result = runner(os.path.join(filters_dir, filter_name, "example.tex"), filter_name) + + assert isinstance(result, Set) + assert result.questions, f"{filter_name} produced no questions" + for question in result.questions: + # A question is only useful if it has top-level text or at least one part. + assert question.main_text or question.parts + + +@pytest.mark.parametrize("filter_name", BUILTIN_FILTERS) +def test_runner_writes_importable_json( + filter_name: str, filters_dir: str, tmp_path +) -> None: + """Passing an output directory produces the Lambda Feedback set/ dir and zip.""" + out_dir = tmp_path / "out" + result = runner( + os.path.join(filters_dir, filter_name, "example.tex"), + filter_name, + str(out_dir), + ) + + set_dir = out_dir / "set" + assert set_dir.is_dir() + assert (out_dir / "set.zip").is_file() + + set_json = json.loads((set_dir / "set_set.json").read_text()) + assert set_json["name"] == "set" + + question_files = sorted(set_dir.glob("question_*.json")) + assert len(question_files) == len(result.questions) + for question_file in question_files: + question_json = json.loads(question_file.read_text()) + assert question_json["title"] + assert "masterContent" in question_json + assert "parts" in question_json