Download tests/test_progress.py from wzender/mathly: direct link, hf CLI and curl.
- Browser
- Download file 6.22 kB
-
https://huggingface.co/spaces/wzender/mathly/resolve/main/tests/test_progress.py
- Command line
-
hf download hf://spaces/wzender/mathly/tests/test_progress.py
-
curl -L -o test_progress.py https://huggingface.co/spaces/wzender/mathly/resolve/main/tests/test_progress.py
6.22 kB
| import asyncio | |
| import json | |
| import re | |
| import shutil | |
| import subprocess | |
| import urllib.parse | |
| from pathlib import Path | |
| import httpx | |
| import pytest | |
| from app.auth import AuthUnavailable | |
| from app.main import create_app | |
| from scan_support import OTHER, OWNER, ScanAuth | |
| MATH_JS = Path(__file__).resolve().parent.parent / "static" / "math.js" | |
| class ProgressAuth(ScanAuth): | |
| """ScanAuth with an in-memory math_progress behind the PostgREST calls app/progress.py makes.""" | |
| def __init__(self): | |
| self.rows: dict[str, dict] = {} | |
| def _admin(self, path, method="GET", body=None, prefer=""): | |
| table, _, query = path.partition("?") | |
| assert table == "/rest/v1/math_progress" | |
| q = dict(urllib.parse.parse_qsl(query)) | |
| if method == "POST": | |
| assert "merge-duplicates" in prefer and q.get("on_conflict") == "user_id" | |
| self.rows[body["user_id"]] = dict(body) | |
| return None | |
| user = q["user_id"].removeprefix("eq.") | |
| return [{"data": self.rows[user]["data"]}] if user in self.rows else [] | |
| def client(app, user=None): | |
| headers = {"Authorization": f"Bearer {user}"} if user else {} | |
| return httpx.AsyncClient(transport=httpx.ASGITransport(app=app), base_url="http://test", headers=headers) | |
| def test_progress_is_kept_per_account(): | |
| auth = ProgressAuth() | |
| app = create_app(auth=auth) | |
| async def check(): | |
| async with client(app) as c: | |
| assert (await c.get("/api/math/progress")).status_code == 401 | |
| async with client(app, OWNER) as owner, client(app, OTHER) as other: | |
| assert (await owner.get("/api/math/progress")).json() == {"progress": {}} | |
| mine = {"done": {"s1": "2026-11-01T10:00:00Z"}, "updated": 1} | |
| assert (await owner.put("/api/math/progress", json={"progress": mine})).json() == {"ok": True} | |
| assert (await owner.get("/api/math/progress")).json() == {"progress": mine} | |
| assert (await other.get("/api/math/progress")).json() == {"progress": {}} | |
| assert (await owner.put("/api/math/progress", json={"progress": [1]})).status_code == 400 | |
| assert (await owner.put("/api/math/progress", content=b"not json")).status_code == 400 | |
| big = {"progress": {"x": "a" * 1_000_001}} | |
| assert (await owner.put("/api/math/progress", json=big)).status_code == 413 | |
| asyncio.run(check()) | |
| def test_missing_table_answers_503(): | |
| class NoTable(ProgressAuth): | |
| def _admin(self, *a, **k): | |
| raise AuthUnavailable("supabase answered 404: relation math_progress does not exist") | |
| app = create_app(auth=NoTable()) | |
| async def check(): | |
| async with client(app, OWNER) as c: | |
| assert (await c.get("/api/math/progress")).status_code == 503 | |
| assert (await c.put("/api/math/progress", json={"progress": {}})).status_code == 503 | |
| asyncio.run(check()) | |
| def merge(a, b): | |
| """Run mergeProgress from static/math.js in Node.""" | |
| source = MATH_JS.read_text(encoding="utf-8") | |
| fn = re.search(r"const isObj=.*?\n(function mergeProgress\(a,b\)\{.*?\n\})", source, re.S) | |
| script = fn.group(0) + f"\nprocess.stdout.write(JSON.stringify(mergeProgress({json.dumps(a)},{json.dumps(b)})));" | |
| return json.loads(subprocess.run(["node", "-e", script], capture_output=True, text=True, check=True).stdout) | |
| def test_merge_keeps_the_work_of_both_devices(): | |
| phone = {"updated": 200, "done": {"a": "2026-11-02", "b": "2026-11-02"}, "gold": {"b": "2026-11-02"}, | |
| "solved": {"c": [0, 2]}, "tries": {"c": 3}, "days": {"2026-11-02": 4}, "comets": ["pi"], | |
| "slips": [{"at": "2026-11-02T09:00", "id": "c", "q": 1, "m": "sign"}], | |
| "at": {"c": 2}, "run": {"c": 1}, "seen": {"a": "2026-11-02"}, "cometDay": "2026-11-02"} | |
| laptop = {"updated": 100, "done": {"a": "2026-11-01", "d": "2026-11-01"}, "solved": {"c": [1]}, | |
| "tries": {"c": 5}, "days": {"2026-11-01": 2, "2026-11-02": 1}, "comets": ["pi", "e"], | |
| "slips": [{"at": "2026-11-01T09:00", "id": "c", "q": 0, "m": "sign"}], | |
| "at": {"c": 1, "d": 0}, "run": {"c": 4}, "seen": {"a": "2026-11-03"}, | |
| "quizzes": [{"at": "2026-11-01T08:00", "n": 5}]} | |
| for out in (merge(phone, laptop), merge(laptop, phone)): | |
| assert out["done"] == {"a": "2026-11-01", "b": "2026-11-02", "d": "2026-11-01"} | |
| assert out["gold"] == {"b": "2026-11-02"} | |
| assert out["solved"] == {"c": [0, 1, 2]} | |
| assert out["tries"] == {"c": 5} | |
| assert out["days"] == {"2026-11-01": 2, "2026-11-02": 4} | |
| assert sorted(out["comets"]) == ["e", "pi"] | |
| assert [s["at"] for s in out["slips"]] == ["2026-11-01T09:00", "2026-11-02T09:00"] | |
| assert out["at"] == {"c": 2, "d": 0} and out["run"] == {"c": 1} # where the student is: the newer copy | |
| assert out["seen"] == {"a": "2026-11-03"} and out["cometDay"] == "2026-11-02" | |
| assert out["quizzes"] == [{"at": "2026-11-01T08:00", "n": 5}] and out["updated"] == 200 | |
| alone = merge({}, phone) # a device with nothing yet takes the server's copy as it is | |
| assert alone == merge(phone, {}) and {k: alone[k] for k in phone} == phone | |
| def test_merge_keeps_the_most_worked_example_openings_and_the_latest_time(): | |
| phone = {"updated": 200, "examples": {"a": {"n": 2, "last": "2026-11-02T10:00:00Z"}}} | |
| laptop = {"updated": 100, "examples": {"a": {"n": 3, "last": "2026-11-01T10:00:00Z"}, "b": {"n": 1, "last": "2026-11-01T11:00:00Z"}}} | |
| for out in (merge(phone, laptop), merge(laptop, phone)): | |
| assert out["examples"] == {"a": {"n": 3, "last": "2026-11-02T10:00:00Z"}, "b": {"n": 1, "last": "2026-11-01T11:00:00Z"}} | |
| assert merge({}, phone)["examples"] == phone["examples"] | |
| def test_merge_keeps_the_most_wrong_answers_per_skill(): | |
| phone = {"updated": 200, "wrongs": {"a": 2, "b": 1}} | |
| laptop = {"updated": 100, "wrongs": {"a": 5}} | |
| for out in (merge(phone, laptop), merge(laptop, phone)): | |
| assert out["wrongs"] == {"a": 5, "b": 1} | |