Spaces:
Sleeping
Sleeping
File size: 6,529 Bytes
b8930b3 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 | """Tests for tuning/held-out split selection (``eval.splits``).
The property that matters is that the split is a function of *which documents
are cached*, not of the order they happen to be read in. A positional rule
("the first 100 files") would silently reassign documents between tuning and
held-out when the glob order changes, when the cache is regenerated on another
machine, or when new documents are added -- turning a held-out claim into an
in-sample one with no visible failure. These tests pin the id-set semantics.
"""
from __future__ import annotations
import json
import random
from pathlib import Path
from typing import Any
import pytest
from eval.score import build_report, format_report
from eval.splits import SplitError, describe, load_tuning_ids, manifest_path, select
from tests.test_eval_revalidate import _clean_document, _real_entry
def _write_manifest(tmp_path: Path, dataset: str, ids: list[str]) -> Path:
path = manifest_path(dataset, splits_dir=tmp_path)
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps({"dataset": dataset, "ids": ids}), encoding="utf-8")
return path
def _entries(ids: list[str]) -> list[dict[str, Any]]:
return [{"id": i} for i in ids]
# ---------------------------------------------------------------------------
# Selection semantics
# ---------------------------------------------------------------------------
def test_all_returns_every_entry(tmp_path: Path) -> None:
entries = _entries(["a", "b", "c"])
assert select(entries, "all", dataset="d", splits_dir=tmp_path) == entries
def test_tuning_and_heldout_partition_the_cache(tmp_path: Path) -> None:
"""Every cached document lands in exactly one of the two splits."""
_write_manifest(tmp_path, "d", ["a", "b"])
entries = _entries(["a", "b", "c", "d"])
tuning = select(entries, "tuning", dataset="d", splits_dir=tmp_path)
heldout = select(entries, "heldout", dataset="d", splits_dir=tmp_path)
assert [e["id"] for e in tuning] == ["a", "b"]
assert [e["id"] for e in heldout] == ["c", "d"]
assert len(tuning) + len(heldout) == len(entries)
assert not {e["id"] for e in tuning} & {e["id"] for e in heldout}
def test_split_is_independent_of_read_order(tmp_path: Path) -> None:
"""Shuffling the cache does not move a single document between splits."""
_write_manifest(tmp_path, "d", ["a", "c", "e"])
ids = ["a", "b", "c", "d", "e", "f"]
baseline = {
name: {e["id"] for e in select(_entries(ids), name, dataset="d", splits_dir=tmp_path)}
for name in ("tuning", "heldout")
}
rng = random.Random(0)
for _ in range(20):
shuffled = ids[:]
rng.shuffle(shuffled)
for name in ("tuning", "heldout"):
got = {e["id"] for e in select(_entries(shuffled), name, dataset="d",
splits_dir=tmp_path)}
assert got == baseline[name], (name, shuffled)
def test_selection_preserves_input_order(tmp_path: Path) -> None:
"""Surviving entries keep their relative order, so reports are stable."""
_write_manifest(tmp_path, "d", ["c", "a"])
entries = _entries(["a", "b", "c"])
assert [e["id"] for e in select(entries, "tuning", dataset="d", splits_dir=tmp_path)] == [
"a",
"c",
]
def test_manifest_membership_ignores_absent_ids(tmp_path: Path) -> None:
"""A manifest id not present in the cache simply selects nothing extra."""
_write_manifest(tmp_path, "d", ["a", "ghost"])
entries = _entries(["a", "b"])
tuning = select(entries, "tuning", dataset="d", splits_dir=tmp_path)
assert [e["id"] for e in tuning] == ["a"]
# ---------------------------------------------------------------------------
# Failure modes -- a split must never resolve silently to the wrong thing
# ---------------------------------------------------------------------------
def test_missing_manifest_raises_with_guidance(tmp_path: Path) -> None:
with pytest.raises(SplitError, match="No tuning manifest"):
select(_entries(["a"]), "tuning", dataset="nope", splits_dir=tmp_path)
def test_unknown_split_name_raises(tmp_path: Path) -> None:
with pytest.raises(SplitError, match="Unknown split"):
select(_entries(["a"]), "train", dataset="d", splits_dir=tmp_path)
def test_empty_selection_raises_rather_than_reporting_nothing(tmp_path: Path) -> None:
"""A held-out split on an unexpanded cache is an error, not an empty report."""
_write_manifest(tmp_path, "d", ["a", "b"])
with pytest.raises(SplitError, match="selected 0 of"):
select(_entries(["a", "b"]), "heldout", dataset="d", splits_dir=tmp_path)
def test_malformed_manifest_raises(tmp_path: Path) -> None:
path = manifest_path("d", splits_dir=tmp_path)
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text("{not json", encoding="utf-8")
with pytest.raises(SplitError, match="not valid JSON"):
load_tuning_ids("d", splits_dir=tmp_path)
def test_manifest_without_ids_raises(tmp_path: Path) -> None:
path = manifest_path("d", splits_dir=tmp_path)
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps({"dataset": "d", "ids": []}), encoding="utf-8")
with pytest.raises(SplitError, match="declares no 'ids'"):
load_tuning_ids("d", splits_dir=tmp_path)
# ---------------------------------------------------------------------------
# Reporting
# ---------------------------------------------------------------------------
def test_describe_marks_tuning_as_contaminated() -> None:
assert "not independent" in describe("tuning", 100, 361)
assert "HELD OUT" in describe("heldout", 261, 361)
assert "mixes tuning and held-out" in describe("all", 361, 361)
def test_report_header_states_the_split(tmp_path: Path) -> None:
"""A rendered report names its split and the size it was drawn from."""
from eval.cache import write_entry
cache = tmp_path / "cache"
splits = tmp_path / "splits"
for name in ("keep", "drop"):
write_entry(
cache, "d", _real_entry(name, _clean_document(), gold={"total": "11.00"})
)
_write_manifest(splits, "d", ["keep"])
report = build_report(
"d", cache_base=cache, split="heldout", splits_dir=splits, revalidate=True
)
text = format_report(report)
assert report.n == 1
assert report.n_cached == 2
assert "Split: heldout (1 of 2 cached)" in text
assert "HELD OUT" in text
|