Spaces:
Sleeping
Sleeping
| """The Space -> client contract: what clients are promised. | |
| Two kinds of test here, and the split is the point. | |
| The first kind pins the WORDING. Those sentences are the fallback path for | |
| cv_results rows stored before the Space shipped the numbers, and the iOS app | |
| parses them with regexes; rewording one is allowed, but it has to be a | |
| deliberate act that breaks a test, not a silent degrade to raw text. | |
| The second kind pins the AGREEMENT between the sentence and the numbers behind | |
| it -- that every metric a fighter dict states in English is exactly what | |
| contract.py renders from `_meta.measured`. That is the invariant the whole | |
| module exists for: if it holds, there is nothing left to parse. | |
| """ | |
| import re | |
| import pytest | |
| import contract | |
| import pose_features as pf | |
| from test_trackers import two_fighter_poses | |
| # --- the sentences ------------------------------------------------------------ | |
| RENDERINGS = [ | |
| ("pressure", | |
| {"label": "pressuring", "net_sw_per_10s": 1.234}, | |
| "pressuring (net +1.2 SW/10s)"), | |
| ("pressure", | |
| {"label": "unclear (camera moves with the fighters)", "net_sw_per_10s": -0.04}, | |
| "unclear (camera moves with the fighters) (net -0.0 SW/10s)"), | |
| ("guard_by_range", | |
| {"close_pct": 62.4, "long_pct": 91.0, "drop_pp": 28.6}, | |
| "both hands up 62% in the pocket vs 91% at range"), | |
| ("guard_by_hand", | |
| {"lead_up_pct": 88.0, "rear_up_pct": 54.4}, | |
| "lead hand up 88% / rear hand up 54%"), | |
| ("guard_by_hand", | |
| {"left_up_pct": 88.0, "right_up_pct": 54.4}, | |
| "left hand up 88% / right hand up 54%"), | |
| ("pocket_work", | |
| {"entries": 23, "median_dwell_s": 1.8, "entries_per_min": 12.0}, | |
| "23 trips into range (12/min, median stay 1.8s)"), | |
| ("level_changes", | |
| {"count": 7, "per_min": 14.0}, | |
| "7 level changes (14/min)"), | |
| ("level_changes", | |
| {"count": 1, "per_min": 2.0}, | |
| "1 level change (2/min)"), | |
| ("distance_control", | |
| {"i_closed_pct": 28.6, "they_closed_pct": 57.1, | |
| "mutual_pct": 14.3, "n_entries": 7}, | |
| "closed the distance on 29% of entries " | |
| "(opponent 57%, mutual 14%; n=7)"), | |
| ("vs_opponent", | |
| {"hands_up_mine": 74.0, "hands_up_theirs": 89.0, | |
| "pocket_guard_mine": 62.0, "pocket_guard_theirs": 80.0}, | |
| "hands up 74% vs their 89%; " | |
| "both hands up in the pocket 62% vs their 80%"), | |
| ("vs_opponent", | |
| {"hands_up_mine": 74.0, "hands_up_theirs": 89.0}, | |
| "hands up 74% vs their 89%"), | |
| ("range_profile", | |
| {"close_pct": 30, "mid_pct": 45, "long_pct": 25}, | |
| "close 30% / mid 45% / long 25%"), | |
| ("stance", | |
| {"orthodox_pct": 70.6, "southpaw_pct": 29.4}, | |
| "orthodox 71% / southpaw 29%"), | |
| ("stance_matchup", | |
| {"closed_pct": 60.4, "open_pct": 39.6}, | |
| "closed 60% / open 40%"), | |
| ("weight_side", | |
| {"label": "rear-loaded", "front_pct": 20.0, | |
| "centered_pct": 30.0, "rear_pct": 50.0}, | |
| "rear-loaded (weight over front foot 20% / centered 30% / rear foot 50%)"), | |
| ("weight_side", | |
| {"label": "rear-loaded", "front_pct": 20.0, "centered_pct": 30.0, | |
| "rear_pct": 50.0, "throwing": contract.weight_throwing("onto_front", 35.0)}, | |
| "rear-loaded (weight over front foot 20% / centered 30% / rear foot 50%); " | |
| "drives onto the front foot when throwing (35%)"), | |
| ("weight_side", | |
| {"label": "front-heavy", "front_pct": 55.0, "centered_pct": 30.0, | |
| "rear_pct": 15.0, "throwing": contract.weight_throwing("off_front", 40.0)}, | |
| "front-heavy (weight over front foot 55% / centered 30% / rear foot 15%); " | |
| "stays OFF the front foot when throwing (40%)"), | |
| ] | |
| def test_wording_is_pinned(key, values, expected): | |
| assert contract.render(key, values) == expected | |
| def test_an_unmeasured_read_says_why_instead_of_guessing(): | |
| assert (contract.render("stance", {"unknown": "feet rarely visible"}) | |
| == "unknown (feet rarely visible)") | |
| def test_render_all_skips_what_was_never_measured(): | |
| out = contract.render_all({"stance": None, "range_profile": | |
| {"close_pct": 10, "mid_pct": 60, "long_pct": 30}}) | |
| assert out == {"range_profile": "close 10% / mid 60% / long 30%"} | |
| # --- the legacy parse --------------------------------------------------------- | |
| # Transcribed from hitanalyze/Models/CVAnalysis.swift. Those regexes read rows | |
| # stored before the numbers shipped, so they have to keep matching what the | |
| # Space says today -- a reworded sentence would leave old and new rows | |
| # rendering differently in the same history list. | |
| IOS_REGEXES = { | |
| # The advancing/retreating clause is gone from what the Space says today, | |
| # but rows stored before that still carry it -- so both halves are | |
| # optional and the app keeps reading either. Matches the Swift regex in | |
| # CVFighterStats.PressureRead.init(raw:). | |
| "pressure": r"^(.+?)\s*\((?:net ([+-][\d.]+) SW/10s)?(?:; )?" | |
| r"(?:advancing ([\d.]+)% / retreating ([\d.]+)%)?\)", | |
| "range_profile": r"close ([\d.]+)% / mid ([\d.]+)% / long ([\d.]+)%", | |
| "stance": r"^([a-z]+) ([\d.]+)% / ([a-z]+) ([\d.]+)%$", | |
| "stance_matchup": r"^([a-z]+) ([\d.]+)% / ([a-z]+) ([\d.]+)%$", | |
| "weight_side": r"^(.+?) \(weight over front foot ([\d.]+)% / centered ([\d.]+)% " | |
| r"/ rear foot ([\d.]+)%\)(?:; (.+))?$", | |
| } | |
| # Metrics that shipped AFTER the values did. No stored row has ever carried | |
| # their sentence, so there is nothing legacy to parse: a client reads | |
| # `_meta.measured` and falls back to showing the sentence as plain text. Listed | |
| # explicitly so a new metric can't quietly skip the legacy-parse question. | |
| VALUES_ONLY = {"guard_by_range", "guard_by_hand", "pocket_work", | |
| "distance_control", "vs_opponent", "level_changes"} | |
| def test_the_ios_fallback_regexes_still_read_the_sentence(key, values, expected): | |
| if key in VALUES_ONLY: | |
| pytest.skip(f"{key} is values-only: no stored row carries its sentence") | |
| assert re.search(IOS_REGEXES[key], expected), \ | |
| f"{key} no longer matches the app's legacy parse: {expected!r}" | |
| def test_every_rendered_metric_is_parseable_or_declared_values_only(): | |
| assert set(IOS_REGEXES) | VALUES_ONLY == set(contract.RENDERERS) | |
| assert not (set(IOS_REGEXES) & VALUES_ONLY) | |
| # --- punches ------------------------------------------------------------------ | |
| def parse_like_ios(line): | |
| """CVPunchEvent.init(logLine:), transcribed.""" | |
| parts = line.split(" ") | |
| assert len(parts) >= 3 | |
| minutes, seconds = parts[0].split(":") | |
| out = {"t": float(minutes) * 60 + float(seconds), "hand": parts[1]} | |
| rest = parts[2:] | |
| if rest[0].startswith("("): | |
| out["role"] = rest[0].strip("()") | |
| rest = rest[1:] | |
| else: | |
| out["role"] = None | |
| out["type"] = rest[0] | |
| out["conf"] = float(rest[1]) if len(rest) > 1 else None | |
| return out | |
| def test_a_punch_reads_the_same_from_its_values_or_its_line(role, kind, conf): | |
| e = contract.punch_event(65.34, "left", role, kind, conf) | |
| parsed = parse_like_ios(e["line"]) | |
| assert parsed["hand"] == e["hand"] | |
| assert parsed["role"] == e["role"] | |
| assert parsed["type"] == e["type"] | |
| assert parsed["conf"] == e["conf"] | |
| assert parsed["t"] == pytest.approx(e["t"], abs=0.05) | |
| def test_an_unclear_call_reports_no_confidence(): | |
| # There is no confidence to report in a type the classifier or the geometry | |
| # veto refused to name. | |
| e = contract.punch_event(5.3, "right", "rear", "unclear", 0.4) | |
| assert e["conf"] is None | |
| assert e["line"] == "0:05.3 right (rear) unclear" | |
| def test_the_punch_clock_survives_the_minute_boundary(): | |
| assert contract.clock_tenths(59.96) == "1:00.0" | |
| assert contract.clock_tenths(0.0) == "0:00.0" | |
| # --- errors ------------------------------------------------------------------- | |
| def test_every_failure_has_a_code_a_heading_and_a_body(): | |
| for code in contract.ERRORS: | |
| report = contract.error_report(code, seconds=200, limit=120) | |
| heading, _, body = report.partition("\n") | |
| assert heading.startswith("### ") and len(heading) > 4 | |
| assert body.strip(), f"{code} has no body" | |
| def test_the_headings_the_app_falls_back_to_are_unique(): | |
| headings = [h for h, _ in contract.ERRORS.values()] | |
| assert len(set(headings)) == len(headings) | |
| def test_a_failure_carries_its_reason_but_no_fighters(): | |
| feats = contract.error_features("seed_failed") | |
| assert [k for k in feats if not k.startswith("_")] == [] | |
| meta = feats["_meta"] | |
| assert meta["error"]["code"] == "seed_failed" | |
| assert meta["error"]["message"] == contract.ERRORS["seed_failed"][1] | |
| assert meta["contract_version"] == contract.CONTRACT_VERSION | |
| def test_clip_too_long_states_both_numbers(): | |
| report = contract.error_report("clip_too_long", seconds=200, limit=120) | |
| assert "~200s" in report and "120s" in report | |
| # --- output slots ------------------------------------------------------------- | |
| def test_report_and_features_lead_the_output_slots(): | |
| # A client can't look slots up by name until it has read the features | |
| # payload the names live in, so those two are positional by definition. | |
| assert contract.OUTPUT_NAMES[:2] == ("report", "features") | |
| assert len(set(contract.OUTPUT_NAMES)) == len(contract.OUTPUT_NAMES) | |
| def test_the_stamp_tells_a_client_what_it_is_reading(): | |
| meta = contract.stamp({}) | |
| assert meta["contract_version"] == contract.CONTRACT_VERSION | |
| assert meta["outputs"] == list(contract.OUTPUT_NAMES) | |
| # --- the agreement ------------------------------------------------------------ | |
| def test_every_sentence_is_rendered_from_the_numbers_beside_it(): | |
| """The invariant the module exists for. | |
| Whatever a fighter dict says in English about a measured read, `_meta | |
| .measured` carries the values it was rendered from -- so a client reads | |
| values, and the two can never disagree. Edit a sentence anywhere but | |
| contract.py and this fails.""" | |
| poses = two_fighter_poses(250.0, 750.0, n=120) | |
| feats, _overlay = pf._features_from_poses(poses, 30.0, ("Fighter A", "Fighter B")) | |
| assert feats, "the synthetic clip should analyze" | |
| measured = feats["_meta"]["measured"] | |
| fighters = [k for k in feats if not k.startswith("_")] | |
| assert set(measured) == set(fighters) | |
| for name in fighters: | |
| for key in contract.RENDERERS: | |
| assert key in measured[name], f"{name} states {key} with no values" | |
| assert feats[name][key] == contract.render(key, measured[name][key]), \ | |
| f"{name}.{key} was written somewhere other than contract.py" | |