"""Regression coverage for every intel-text format ocr.py understands. Each format below was added incrementally in response to a real screenshot/paste the user hit, and at least one of them (the ': ' grammar colliding with the 'Type#id:' header shape) has already regressed silently once because there was no test suite to catch it. One test per format, named after what it covers, so a future change that breaks an old format fails loudly and specifically instead of being noticed (or not) days later. """ from fenigma import ocr from fenigma.models import Coord, TargetType def test_standard_target_and_rp_blocks(): text = """ Target#5 Spotted. 088, 12.10km from Spotter#1 . Reference Point Alpha: Bearing 094 from Spotter#1 Distance 13.26km from Spotter#2 . AmmoCache#3: Bearing 217 & Distance 10.48km from AmmoCache#2 """ info = ocr.parse_text(text) assert (TargetType.UNKNOWN, "5") in info.targets raw, clues, coord, shell, requested_time = info.targets[(TargetType.UNKNOWN, "5")] assert clues == [ocr.Clue(reference="Spotter#1", bearing_deg=88.0, distance_km=12.1)] # "AmmoCache" is an alias for SupplyCache, both in the header and in # a reference to an existing one. assert (TargetType.SUPPLY_CACHE, "3") in info.targets raw, clues, coord, shell, requested_time = info.targets[(TargetType.SUPPLY_CACHE, "3")] assert clues == [ocr.Clue(reference="SupplyCache#2", bearing_deg=217.0, distance_km=10.48)] assert "Alpha" in info.reference_points def test_calibration_target_line(): text = "IRON NEST location - H3 5:5\nTARGET COORDINATES:\nTarget is at- Q4 4:2" info = ocr.parse_text(text) assert info.nest_coord == Coord("H", 3, 5, 5) assert info.targets[(TargetType.UNKNOWN, "1")][2] == Coord("Q", 4, 4, 2) def test_destroyed_reports_digit_and_letter_id(): text = "SupplyCache#2 Destroyed. Additional Requisition Granted.\nDirect Hit! HostileTank#3 Destroyed." info = ocr.parse_text(text) assert info.destroyed == {(TargetType.SUPPLY_CACHE, "2"), (TargetType.HOSTILE_TANK, "3")} def test_train_arrival_intel(): text = """ARRIVAL STATION: Valle de Mula MainStation: J6 0:4 . Estimated arrival: T=10:16:50 . TRACK ALIGNMENT: Rail line runs straight. Bearing 090 from MainStation. . FINAL APPROACH: Waypoint A - 6.00km from station: T=10:06:50 Waypoint B - 4.00km from station: T=10:10:10 """ info = ocr.parse_text(text) assert "MainStation" in info.reference_points assert info.reference_points["MainStation"][2] == Coord("J", 6, 0, 4) assert "Waypoint A" in info.reference_points _, clues, coord = info.reference_points["Waypoint A"] assert clues == [ocr.Clue(reference="MainStation", bearing_deg=90.0, distance_km=6.0)] def test_enemy_multiword_name_becomes_its_own_target_type(): text = """Enemy Signal Station: Distance 4.40km from Spotter#1 Distance 5.96km from Spotter#2 . Enemy Assembly Area: Bearing 034 from Enemy Signal Station """ info = ocr.parse_text(text) assert (TargetType.ENEMY, "SignalStation") in info.targets assert (TargetType.ENEMY, "AssemblyArea") in info.targets _, clues, *_ = info.targets[(TargetType.ENEMY, "AssemblyArea")] assert clues == [ocr.Clue(reference="Enemy#SignalStation", bearing_deg=34.0)] def test_enemy_destroyed_report(): text = "Priority target Enemy Signal Station Destroyed, +25 Requisition." info = ocr.parse_text(text) assert (TargetType.ENEMY, "SignalStation") in info.destroyed def test_listening_post_and_coastal_battery(): """'Coastal Battery' is an alias for HostileArtillery (see _TYPE_WORD_ALIASES), not its own TargetType, it's just the fixed-emplacement flavor of the same thing.""" text = """Listening Post#1 at K6 7:8 audio reports on: Coastal Battery#2: Distance 6.28km South-East from Listening Post#1 """ info = ocr.parse_text(text) assert info.reference_points["ListeningPost#1"][2] == Coord("K", 6, 7, 8) assert (TargetType.HOSTILE_ARTILLERY, "2") in info.targets _, clues, *_ = info.targets[(TargetType.HOSTILE_ARTILLERY, "2")] assert clues == [ocr.Clue(reference="ListeningPost#1", bearing_deg=135.0, distance_km=6.28)] def test_marine_garrison_fire_support_request(): text = """Marine Garrison#1 pinned! SMK Shells requested on J6 8:3 Requested before - T10:31:41 - """ info = ocr.parse_text(text) assert (TargetType.MARINE_GARRISON, "1") in info.targets raw, clues, coord, shell, requested_time = info.targets[(TargetType.MARINE_GARRISON, "1")] assert coord == Coord("J", 6, 8, 3) from fenigma.shells import Shell assert shell is Shell.SMK assert requested_time == "T10:31:41" def test_multiword_rp_names_with_bold_markup(): text = """Reference Point The Mole: Bearing 100° from Spotter#1 Bearing 048° from Spotter#2 . Reference Point Dockmaster's House: Distance 6.14km from The Mole """ info = ocr.parse_text(text) assert "TheMole" in info.reference_points assert "DockmastersHouse" in info.reference_points _, clues, _ = info.reference_points["DockmastersHouse"] assert clues == [ocr.Clue(reference="TheMole", distance_km=6.14)] def test_bare_name_header_becomes_a_target_not_an_rp(): text = """HMS Rockingham: Distance 4.65km from Spotter#2 """ info = ocr.parse_text(text) assert (TargetType.UNKNOWN, "HMSRockingham") in info.targets assert not info.reference_points def test_bold_coordinate_spans_are_not_corrupted_by_name_squashing(): """A real regression: squashing multi-word bold spans into single tokens (for names) once also mangled multi-word COORD spans like 'C9 7:9' into garbage ('C979'), because nothing distinguished the two cases. Guarded here permanently.""" text = """IRON NEST - A2 9:8 . OBSERVATION ASSETS: Spotter#1 - C9 7:9 Spotter#2 - E5 5:0 """ info = ocr.parse_text(text) assert info.nest_coord == Coord("A", 2, 9, 8) assert info.spotters == {1: Coord("C", 9, 7, 9), 2: Coord("E", 5, 5, 0)} def test_forward_observer_reports_triangulate_without_leaving_ephemeral_entities(): text = """FO#5 Audio report on HMS Rockingham: 2.24km From I8 6:9 . . . - - - FO#4 Eyes on HMS Rockingham: 087° From G7 6:7 . . . - - - FO Eyes on HMS Rockingham: 099° From C9 1:2 . . . """ info = ocr.parse_text(text) assert (TargetType.UNKNOWN, "HMSRockingham") in info.targets _, clues, coord, *_ = info.targets[(TargetType.UNKNOWN, "HMSRockingham")] # solved immediately via a scratch board, no Clues referencing an FO # persist, and no FO ever leaks in as a real reference point. assert clues == [] assert coord is not None assert not info.reference_points def test_ref_colon_value_clue_grammar(): """'Spotter#2: 4.04km' has the exact same 'Word#digits:' shape as a real block header ('AmmoCache#3:'), a real regression: it hijacked the block before any clues could attach to the entity above it.""" text = """Enemy Assembly Area: Spotter#2: 4.04km Spotter#3: 298° """ info = ocr.parse_text(text) assert (TargetType.ENEMY, "AssemblyArea") in info.targets _, clues, *_ = info.targets[(TargetType.ENEMY, "AssemblyArea")] assert clues == [ ocr.Clue(reference="Spotter#2", distance_km=4.04), ocr.Clue(reference="Spotter#3", bearing_deg=298.0), ] def test_compass_word_bearings_carry_a_tolerance(): text = """Enemy Field Command: Spotter#1: West Spotter#2: North-West Spotter#3: North Northwest """ info = ocr.parse_text(text) _, clues, *_ = info.targets[(TargetType.ENEMY, "FieldCommand")] by_ref = {c.reference: c for c in clues} assert by_ref["Spotter#1"].bearing_deg == 270.0 assert by_ref["Spotter#2"].bearing_deg == 315.0 assert by_ref["Spotter#3"].bearing_deg == 337.5 for clue in clues: assert clue.bearing_tolerance_deg == 11.25 def test_grid_only_coord_no_sub_position(): text = "Enemy Signal Station:\n Reported active in grid D10" info = ocr.parse_text(text) _, clues, coord, *_ = info.targets[(TargetType.ENEMY, "SignalStation")] assert coord == Coord("D", 10, 5, 5)