## Problem Kattis and USACO had zero offline test coverage — no fixtures, no conftest routers, and no entries in the test matrix. The `precision` field and error paths were also unverified across all platforms. ## Solution Add HTML fixtures for both platforms and wire up `httpx.AsyncClient.get` routers in `conftest.py` following the existing CSES/CodeChef pattern. Extend the test matrix from 12 to 23 parametrized cases. Add a dedicated test for the Kattis contest-vs-slug fallback path (verifying `contest_url` and `standings_url`), three parametrized metadata error cases, and a targeted assertion that `extract_precision` returns a non-`None` float for problems with floating-point tolerance hints. Closes #281.
139 lines
5.1 KiB
Python
139 lines
5.1 KiB
Python
import pytest
|
|
|
|
from scrapers.models import (
|
|
ContestListResult,
|
|
MetadataResult,
|
|
TestsResult,
|
|
)
|
|
|
|
MATRIX = {
|
|
"cses": {
|
|
"metadata": ("introductory_problems",),
|
|
"tests": ("introductory_problems",),
|
|
"contests": tuple(),
|
|
},
|
|
"atcoder": {
|
|
"metadata": ("abc100",),
|
|
"tests": ("abc100",),
|
|
"contests": tuple(),
|
|
},
|
|
"codeforces": {
|
|
"metadata": ("1550",),
|
|
"tests": ("1550",),
|
|
"contests": tuple(),
|
|
},
|
|
"codechef": {
|
|
"metadata": ("START209D",),
|
|
"tests": ("START209D",),
|
|
"contests": tuple(),
|
|
},
|
|
"kattis": {
|
|
"metadata": ("hello",),
|
|
"tests": ("hello",),
|
|
"contests": tuple(),
|
|
},
|
|
"usaco": {
|
|
"metadata": ("dec24_gold",),
|
|
"tests": ("dec24_gold",),
|
|
"contests": tuple(),
|
|
},
|
|
}
|
|
|
|
|
|
@pytest.mark.parametrize("scraper", MATRIX.keys())
|
|
@pytest.mark.parametrize("mode", ["metadata", "tests", "contests"])
|
|
def test_scraper_offline_fixture_matrix(run_scraper_offline, scraper, mode):
|
|
args = MATRIX[scraper][mode]
|
|
rc, objs = run_scraper_offline(scraper, mode, *args)
|
|
assert rc in (0, 1), f"Bad exit code {rc}"
|
|
assert objs, f"No JSON output for {scraper}:{mode}"
|
|
|
|
if mode == "metadata":
|
|
model = MetadataResult.model_validate(objs[-1])
|
|
assert model.success is True
|
|
assert model.url
|
|
assert len(model.problems) >= 1
|
|
assert all(isinstance(p.id, str) and p.id for p in model.problems)
|
|
elif mode == "contests":
|
|
model = ContestListResult.model_validate(objs[-1])
|
|
assert model.success is True
|
|
assert len(model.contests) >= 1
|
|
else:
|
|
assert len(objs) >= 1, "No test objects returned"
|
|
validated_any = False
|
|
for obj in objs:
|
|
if "success" in obj and "tests" in obj and "problem_id" in obj:
|
|
tr = TestsResult.model_validate(obj)
|
|
assert tr.problem_id != ""
|
|
assert isinstance(tr.tests, list)
|
|
assert hasattr(tr, "combined"), "Missing combined field"
|
|
assert tr.combined is not None, "combined field is None"
|
|
assert hasattr(tr.combined, "input"), "combined missing input"
|
|
assert hasattr(tr.combined, "expected"), "combined missing expected"
|
|
assert isinstance(tr.combined.input, str), "combined.input not string"
|
|
assert isinstance(tr.combined.expected, str), (
|
|
"combined.expected not string"
|
|
)
|
|
assert hasattr(tr, "multi_test"), "Missing multi_test field"
|
|
assert isinstance(tr.multi_test, bool), "multi_test not boolean"
|
|
validated_any = True
|
|
else:
|
|
assert "problem_id" in obj
|
|
assert "tests" in obj and isinstance(obj["tests"], list)
|
|
assert (
|
|
"timeout_ms" in obj and "memory_mb" in obj and "interactive" in obj
|
|
)
|
|
assert "combined" in obj, "Missing combined field in raw JSON"
|
|
assert isinstance(obj["combined"], dict), "combined not a dict"
|
|
assert "input" in obj["combined"], "combined missing input key"
|
|
assert "expected" in obj["combined"], "combined missing expected key"
|
|
assert isinstance(obj["combined"]["input"], str), (
|
|
"combined.input not string"
|
|
)
|
|
assert isinstance(obj["combined"]["expected"], str), (
|
|
"combined.expected not string"
|
|
)
|
|
assert "multi_test" in obj, "Missing multi_test field in raw JSON"
|
|
assert isinstance(obj["multi_test"], bool), "multi_test not boolean"
|
|
assert "precision" in obj, "Missing precision field in raw JSON"
|
|
assert obj["precision"] is None or isinstance(
|
|
obj["precision"], float
|
|
), "precision must be None or float"
|
|
validated_any = True
|
|
assert validated_any, "No valid tests payloads validated"
|
|
|
|
|
|
def test_kattis_contest_metadata(run_scraper_offline):
|
|
rc, objs = run_scraper_offline("kattis", "metadata", "open2024")
|
|
assert rc == 0
|
|
assert objs
|
|
model = MetadataResult.model_validate(objs[-1])
|
|
assert model.success is True
|
|
assert len(model.problems) == 2
|
|
assert model.contest_url != ""
|
|
assert model.standings_url != ""
|
|
|
|
|
|
def test_usaco_precision_extracted(run_scraper_offline):
|
|
rc, objs = run_scraper_offline("usaco", "tests", "dec24_gold")
|
|
assert rc == 0
|
|
precisions = [obj["precision"] for obj in objs if "problem_id" in obj]
|
|
assert any(p is not None for p in precisions), (
|
|
"Expected at least one problem with precision"
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"scraper,contest_id",
|
|
[
|
|
("cses", "nonexistent_category_xyz"),
|
|
("usaco", "badformat"),
|
|
("kattis", "nonexistent_problem_xyz"),
|
|
],
|
|
)
|
|
def test_scraper_metadata_error(run_scraper_offline, scraper, contest_id):
|
|
rc, objs = run_scraper_offline(scraper, "metadata", contest_id)
|
|
assert rc == 1
|
|
assert objs
|
|
assert objs[-1].get("success") is False
|
|
assert objs[-1].get("error")
|