Something went wrong. Try again.
bayes for days
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154"""Every tactic a formation can open on has a scenario that produces it.
A tactic reaches the scorer because some force's composition asked for it.`Bombard` sat in the vocabulary with a live formation - `Fire Support` - askingfor it every match, opened on `Engage` instead because the tactic was unbuilt,and nothing noticed. `Harass` was in the same state for a different reason: itwas built, and no scenario in the repository fielded a force that produced it.
Both were found by hand. This is the test that finds the next one, and it failson the **addition** rather than on the omission: a tactic added to the openingvocabulary without a scenario exercising it cannot land quietly.
**This proves a formation opens on a tactic, not that the tactic's behaviourever happens.** A scenario cannot force a behaviour the bot is correctlydeclining to choose - it can only create the conditions under which a tacticwould choose it. A passing row here means some force asked for the tactic, andsays nothing about whether what the tactic is *for* occurred in the match.
**What this does not cover.** `Break`, `Regroup` and `Withdraw` have`Assignment` variants and no `Opening` variant: they are the coordinator'sresponse to match *state* - damage, cohesion, a losing exchange - rather than towho is in the lance. A scenario can force an opening by composition; it cannotforce a transition, and a scenario that reliably produced the *situation* wouldbe a much weaker test. Those three have no scenario-level cover and this testdoes not pretend otherwise.
**A force of fewer than three machines cannot hold a formation** - `Formed::holds`refuses below that - so its opening never reaches the scorer. Counting everyforce would roughly double the sample with rows that decide nothing, which iswhy only `n >= 3` counts here.
**This is not enforced on commit, and believing otherwise is the failure it waswritten to prevent.** Classifying every scenario starts a container per suiteand costs about a minute, which is too much for a hook that fires on any `.py`edit, so the half that needs Docker runs only under `SDS_TACTIC_COVERAGE=1`:
SDS_TACTIC_COVERAGE=1 python3 -m unittest tests.test_tactic_coverage
The structural half - that `UNCOVERED` names openings which exist - is free andalways runs. **There is no periodic job in this repository to hang the rest on**today: no release script, no scheduled suite, nothing that runs the `#[ignore]`dwork. Until there is, this runs when somebody runs it, and a tactic added withno scenario will not be caught at the moment it lands."""
import osimport reimport subprocessimport unittestfrom pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
#: Set to run the half that classifies scenarios, which needs Docker and about a#: minute. Off by default so the harness suite stays fast and hermetic.COVERAGE = os.environ.get("SDS_TACTIC_COVERAGE")WHY_SKIPPED = "set SDS_TACTIC_COVERAGE=1 to classify scenarios (needs Docker, ~1 min)"
#: Openings a formation can ask for, read off `Opening` in `formation.rs` so a#: variant added there without a scenario fails this rather than being missed.OPENING_ENUM = re.compile(r"pub enum Opening \{(.*?)\n\}", re.S)
#: Openings with no scenario, each with the reason it has none. An entry here is#: a boundary and has to carry its argument; an empty reason is a hole.UNCOVERED: dict[str, str] = {}
def openings() -> set[str]: """The openings a formation may ask for, from the Rust enum itself.""" text = (ROOT / "crates/sds-core/src/formation.rs").read_text() body = OPENING_ENUM.search(text) assert body, "no `Opening` enum in formation.rs" found = set(re.findall(r"^ (\w+)", body.group(1), re.M)) # `Blocked` names a tactic nothing can reach, which is the opposite of a # tactic to write a scenario for. return {name.lower() for name in found - {"Blocked"}}
def largest_force(scenario: Path) -> int: """The biggest single force in a scenario, counted off its `Unit_` lines.""" sides: dict[str, int] = {} for line in scenario.read_text().splitlines(): named = re.match(r"Unit_(\w+?)_\d+=", line) if named: sides[named.group(1)] = sides.get(named.group(1), 0) + 1 return max(sides.values(), default=0)
def produced() -> dict[str, set[str]]: """Which scenario produces each opening, for forces that can hold one.
Classified by the same path production uses - `sds forces` loads the `.mms` through the bridge and asks `sds_core::formation`. A test that classified scenarios by a second route would be a test of the route. """ found: dict[str, set[str]] = {} for suite in sorted((ROOT / "scenarios").iterdir()): if not suite.is_dir() or not any(suite.glob("*.mms")): continue # A directory whose largest force is under three machines cannot hold a # formation, so classifying it starts a container to learn nothing. The # count is off the file rather than off the classifier, which is the one # thing about a scenario that can be read without loading it. if max(largest_force(f) for f in suite.glob("*.mms")) < 3: continue done = subprocess.run( ["./sds.sh", "forces", "--suite", str(suite)], cwd=ROOT, capture_output=True, text=True, ) for line in done.stdout.splitlines(): row = re.match( r"(\S+)\s+(?:North|South)\s+(\d+)\s+.*?\b" r"(engage|advance|harass|flank|entrench|bombard)\b", line, ) if row and int(row.group(2)) >= 3: found.setdefault(row.group(3), set()).add(row.group(1)) return found
class TestTacticCoverage(unittest.TestCase): @unittest.skipUnless(COVERAGE, WHY_SKIPPED) def test_every_opening_has_a_scenario_that_produces_it(self) -> None: seen = produced() missing = sorted(openings() - set(seen) - set(UNCOVERED)) self.assertEqual( missing, [], "no scenario fields a force that opens on these, so nothing " "exercises them: write one under `scenarios/tactics/`, or record " "why it cannot be written in `UNCOVERED` with its reason", )
def test_an_uncovered_opening_is_still_an_opening(self) -> None: """`UNCOVERED` cannot drift into naming something that does not exist.""" self.assertEqual(sorted(set(UNCOVERED) - openings()), [])
@unittest.skipUnless(COVERAGE, WHY_SKIPPED) def test_an_uncovered_opening_is_actually_uncovered(self) -> None: """An entry that a scenario now produces must be deleted, not kept.
A stale exemption is worse than a missing one: it says nothing tests this while something does, and the next person believes it. """ seen = produced() stale = sorted(name for name in UNCOVERED if name in seen) self.assertEqual(stale, [], "these are covered now; drop them from UNCOVERED")
if __name__ == "__main__": unittest.main()