Skip to content

Commit 4627487

Browse files
m-messerclaude
andcommitted
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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017VXb8aZqgFBjoeuuddjW6r
1 parent 30569c5 commit 4627487

6 files changed

Lines changed: 87 additions & 5 deletions

File tree

.github/workflows/test.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,11 @@ jobs:
2424
uses: r-lib/actions/setup-pandoc@v2
2525
- name: Linting Checks
2626
run: |
27-
poetry run black .
28-
poetry run isort --check-only in2lambda docs
29-
poetry run pydocstyle --convention=google in2lambda
27+
poetry run black --check .
28+
poetry run isort --check-only in2lambda docs tests
29+
poetry run pydocstyle --convention=google in2lambda tests
3030
- name: pytest
31-
run: poetry run pytest --cov-report=xml:coverage.xml --cov=in2lambda --doctest-modules in2lambda
31+
run: poetry run pytest --cov-report=xml:coverage.xml --cov=in2lambda
3232
- name: Upload coverage to Codecov
3333
uses: codecov/codecov-action@v3
3434
with:

in2lambda/api/visibility_status.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from enum import Enum
44

5+
56
class VisibilityStatus(Enum):
67
"""Enum representing the visibility status of a question or set."""
78

in2lambda/json_convert/json_convert.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,10 @@ def converter(
9898

9999
# Output file
100100
filename = (
101-
"question_" + str(i).zfill(3) + "_" + re.sub(r'[^\w\-_.]', '_', output['title'].strip())
101+
"question_"
102+
+ str(i).zfill(3)
103+
+ "_"
104+
+ re.sub(r"[^\w\-_.]", "_", output["title"].strip())
102105
)
103106

104107
# write questions into directory

pyproject.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,11 @@ ignore_missing_imports = true
6060
[tool.isort]
6161
profile = "black"
6262

63+
[tool.pytest.ini_options]
64+
# Collect both the unit tests in tests/ and the doctests embedded in the package.
65+
testpaths = ["tests", "in2lambda"]
66+
addopts = "--doctest-modules"
67+
6368
[build-system]
6469
requires = ["poetry-core"]
6570
build-backend = "poetry.core.masonry.api"

tests/conftest.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
"""Shared pytest fixtures for the in2lambda test suite."""
2+
3+
import os
4+
5+
import pytest
6+
7+
import in2lambda
8+
9+
10+
@pytest.fixture(scope="session")
11+
def filters_dir() -> str:
12+
"""Absolute path to the packaged ``filters`` directory.
13+
14+
Each filter ships a self-contained ``example.tex`` used by the end-to-end tests.
15+
"""
16+
return os.path.join(os.path.dirname(in2lambda.__file__), "filters")

tests/test_runner.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
"""End-to-end tests for :func:`in2lambda.main.runner` across the built-in filters.
2+
3+
Each built-in filter ships a self-contained ``example.tex`` that exercises the
4+
document structure it targets. These tests run every filter over its own example
5+
and check both the in-memory :class:`~in2lambda.api.set.Set` and the JSON/ZIP
6+
files written to disk.
7+
"""
8+
9+
import json
10+
import os
11+
12+
import pytest
13+
14+
from in2lambda.api.set import Set
15+
from in2lambda.main import runner
16+
17+
BUILTIN_FILTERS = ["PartsSepSol", "PartsOneSol", "PartPartSolSol", "PartSolPartSol"]
18+
19+
20+
@pytest.mark.parametrize("filter_name", BUILTIN_FILTERS)
21+
def test_runner_returns_populated_set(filter_name: str, filters_dir: str) -> None:
22+
"""Every filter turns its example into a Set with at least one usable question."""
23+
result = runner(os.path.join(filters_dir, filter_name, "example.tex"), filter_name)
24+
25+
assert isinstance(result, Set)
26+
assert result.questions, f"{filter_name} produced no questions"
27+
for question in result.questions:
28+
# A question is only useful if it has top-level text or at least one part.
29+
assert question.main_text or question.parts
30+
31+
32+
@pytest.mark.parametrize("filter_name", BUILTIN_FILTERS)
33+
def test_runner_writes_importable_json(
34+
filter_name: str, filters_dir: str, tmp_path
35+
) -> None:
36+
"""Passing an output directory produces the Lambda Feedback set/ dir and zip."""
37+
out_dir = tmp_path / "out"
38+
result = runner(
39+
os.path.join(filters_dir, filter_name, "example.tex"),
40+
filter_name,
41+
str(out_dir),
42+
)
43+
44+
set_dir = out_dir / "set"
45+
assert set_dir.is_dir()
46+
assert (out_dir / "set.zip").is_file()
47+
48+
set_json = json.loads((set_dir / "set_set.json").read_text())
49+
assert set_json["name"] == "set"
50+
51+
question_files = sorted(set_dir.glob("question_*.json"))
52+
assert len(question_files) == len(result.questions)
53+
for question_file in question_files:
54+
question_json = json.loads(question_file.read_text())
55+
assert question_json["title"]
56+
assert "masterContent" in question_json
57+
assert "parts" in question_json

0 commit comments

Comments
 (0)