grading-answers / tests /test_term_selection.py
giuseppecuccunm's picture
Widen sign-in form and write live grades to the HF bucket
4519f8e verified
Raw History Blame Contribute Delete
5.1 kB
"""The term cap must be even across concept classes, reproducible and nested."""
import term_selection as ts
CONCEPTS = [f"Concept {i}" for i in range(8)]
def _pool(indices: list[int], concepts: list[str] | None = None) -> dict[str, str]:
"""One term per (concept, index). Even indices only, so a later pool can
interleave odd ones and expose a name-ordered implementation."""
concepts = concepts or CONCEPTS
return {
f"term_{c}_{i:03d}": concept
for c, concept in enumerate(concepts)
for i in indices
}
BASE = _pool(list(range(0, 40, 2))) # 8 concepts x 20 terms
def test_cap_divides_evenly_across_concepts() -> None:
selection = ts.select_terms_by_concept(BASE, max_terms=40)
assert len(selection.terms) == 40
assert set(selection.per_concept.values()) == {5}
def test_cap_that_does_not_divide_evenly_differs_by_one() -> None:
selection = ts.select_terms_by_concept(BASE, max_terms=41)
counts = selection.per_concept
assert len(selection.terms) == 41
assert sum(counts.values()) == 41
assert max(counts.values()) - min(counts.values()) == 1
def test_a_small_concept_redistributes_its_slots() -> None:
pool = dict(BASE)
pool.update(_pool([0, 2], concepts=["Concept tiny"]))
selection = ts.select_terms_by_concept(pool, max_terms=40)
assert len(selection.terms) == 40
assert selection.per_concept["Concept tiny"] == 2
assert sum(selection.per_concept.values()) == 40
def test_shortfall_names_the_concept_that_ran_out() -> None:
pool = dict(BASE)
pool.update(_pool([0, 2], concepts=["Concept tiny"]))
selection = ts.select_terms_by_concept(pool, max_terms=40)
assert selection.shortfall_concepts() == ["Concept tiny"]
def test_a_cap_larger_than_the_pool_takes_everything_and_names_nobody() -> None:
selection = ts.select_terms_by_concept(BASE, max_terms=1000)
assert len(selection.terms) == len(BASE)
assert selection.shortfall_concepts() == []
def test_shortfall_is_empty_when_cap_exactly_matches_the_pool() -> None:
"""A cap equal to the pool size still took everything -- same as a cap
larger than the pool, or no cap at all -- so nothing was left out."""
pool = {f"small_{i:03d}": "Small" for i in range(20)}
pool.update({f"big_{i:03d}": "Big" for i in range(24)})
selection = ts.select_terms_by_concept(pool, max_terms=len(pool))
assert len(selection.terms) == len(pool)
assert selection.shortfall_concepts() == []
def test_selection_is_nested_across_cap_sizes() -> None:
"""This is what keeps grader verdicts valid when a batch is widened."""
larger = ts.select_terms_by_concept(BASE, max_terms=80).terms
for smaller_n in (8, 16, 40, 64):
smaller = ts.select_terms_by_concept(BASE, max_terms=smaller_n).terms
assert larger[:smaller_n] == smaller
def test_selection_is_nested_with_a_non_multiple_cap_and_an_undersized_concept() -> (
None
):
"""Slack redistribution -- what a small concept's leftover slots do -- is
where a refactor is most likely to break nesting, and that arithmetic is
only reachable with a cap that doesn't divide the concept count, over a
pool with an undersized concept."""
pool = dict(BASE)
pool.update(_pool([0, 2], concepts=["Concept tiny"]))
larger = ts.select_terms_by_concept(pool, max_terms=49).terms
smaller = ts.select_terms_by_concept(pool, max_terms=41).terms
assert larger[:41] == smaller
def test_result_does_not_depend_on_insertion_order() -> None:
shuffled = dict(reversed(list(BASE.items())))
assert ts.select_terms_by_concept(BASE, max_terms=40) == ts.select_terms_by_concept(
shuffled, max_terms=40
)
def test_adding_terms_keeps_most_of_the_existing_prefix() -> None:
"""Terms are ranked by hash of the term, not by position or name, so new
terms displace only their share. A name-ordered implementation would put
every odd index near the front and move 16 of the 40."""
before = set(ts.select_terms_by_concept(BASE, max_terms=40).terms)
grown = dict(BASE)
grown.update(_pool([1, 3]))
after = set(ts.select_terms_by_concept(grown, max_terms=40).terms)
assert len(before & after) >= 32
def test_no_cap_selects_every_term() -> None:
selection = ts.select_terms_by_concept(BASE)
assert len(selection.terms) == len(BASE)
assert selection.requested is None
assert selection.shortfall_concepts() == []
def test_a_cap_of_zero_selects_nothing() -> None:
selection = ts.select_terms_by_concept(BASE, max_terms=0)
assert selection.terms == ()
assert selection.per_concept == {}
def test_empty_pool() -> None:
selection = ts.select_terms_by_concept({}, max_terms=10)
assert selection.terms == ()
assert selection.available == {}
assert selection.shortfall_concepts() == []
def test_available_counts_the_whole_pool_not_the_sample() -> None:
selection = ts.select_terms_by_concept(BASE, max_terms=8)
assert set(selection.available.values()) == {20}
assert set(selection.per_concept.values()) == {1}