"""The register: everything ever looked up, read back and made browsable. Nothing here touches a network or a receiver. Both caches are written by the tests and read by the code, which is the whole reason the reading lives in a module of its own. """ import json import tempfile import time from pathlib import Path import pytest from rich.console import Console from bandsaunter import register as reg from bandsaunter.browse import Browser def write_callsigns(path: Path, entries) -> Path: body = {} for one in entries: row = {"call": one["call"], "status": one.get("status", "found"), "fetched_at": one.get("fetched_at", 1_700_000_000.0), "version": 4} row.update({k: v for k, v in one.items() if k != "call"}) body[one["call"]] = row path.write_text(json.dumps(body)) return path def write_aircraft(path: Path, planes, routes=None) -> Path: path.write_text(json.dumps({"aircraft": {p["icao"]: p for p in planes}, "routes": routes or {}, "airports": {}})) return path SOME = [ {"call": "AB7IC", "name": "Thomas G Britton", "location": "Tucson, AZ", "street": "8410 E Brookwood Dr", "postcode": "85750-2468", "country": "United States", "district": "district 7", "grid": "DM42og", "oper_class": "EXTRA", "licence_type": "PERSON", "expires": "12/21/2033", "latitude": 32.2750431, "longitude": -110.8131236, "source": "callook.info"}, {"call": "VK4BLE", "country": "Australia", "status": "unlisted"}, {"call": "WQVF960", "country": "United States", "status": "service"}, ] # --------------------------------------------------------------------------- # Reading the caches # --------------------------------------------------------------------------- def test_a_callsign_comes_back_with_everything_that_was_known(tmp_path): where = write_callsigns(tmp_path / "c.json", SOME) entries = reg.load_callsigns(where) got = {e.title: e for e in entries} one = got["AB7IC"] assert one.what == "Thomas G Britton" assert one.where == "Tucson, AZ" assert one.position == pytest.approx((32.2750431, -110.8131236)) shown = dict(one.rows) for label in ("licensed to", "class", "licence", "street", "town", "postcode", "country", "district", "grid", "expires", "position", "status", "answered by", "looked up"): assert label in shown, label assert shown["street"] == "8410 E Brookwood Dr" assert shown["answered by"] == "callook.info" def test_a_callsign_nobody_could_place_is_kept_rather_than_dropped(tmp_path): """"Asked about and in no register" is a fact about a station. A list that quietly dropped them would make an evening of unlisted foreign stations look like an evening when nothing was looked up.""" entries = reg.load_callsigns(write_callsigns(tmp_path / "c.json", SOME)) assert {e.title for e in entries} == {"AB7IC", "VK4BLE", "WQVF960"} vk = next(e for e in entries if e.title == "VK4BLE") assert vk.position is None and not vk.mappable assert dict(vk.rows)["status"] == \ "asked about, in no register reachable from here" def test_a_status_says_what_it_means_rather_than_what_it_is_called(tmp_path): entries = reg.load_callsigns(write_callsigns(tmp_path / "c.json", SOME)) service = next(e for e in entries if e.title == "WQVF960") assert "not an amateur callsign" in dict(service.rows)["status"] def test_a_position_taken_from_the_grid_says_so(tmp_path): """A grid square is a few kilometres across and a licensed address is a street, so which one a coordinate came from matters.""" where = write_callsigns(tmp_path / "c.json", [ {"call": "G4ABC", "grid": "IO91", "latitude": 51.5, "longitude": -1.0, "from_grid": True}]) one = reg.load_callsigns(where)[0] assert "from the grid square" in dict(one.rows)["position"] def test_an_aircraft_comes_back_with_its_route(tmp_path): """The route is filed against the flight number and the airframe against the address, so the two halves have to be joined.""" where = write_aircraft( tmp_path / "f.json", [{"icao": "4CA2D3", "registration": "EI-DYM", "callsign": "RYR1234", "manufacturer": "Boeing", "model": "737-8AS", "type_code": "B738", "operator": "Ryanair", "country": "Ireland", "status": "found", "fetched_at": 1_700_000_000.0}], {"RYR1234": {"origin": "Dublin", "origin_code": "EIDW", "origin_lat": 53.4213, "origin_lon": -6.2701, "destination": "Stansted", "destination_code": "EGSS", "route_source": "a schedule service"}}) one = reg.load_aircraft(where)[0] assert one.title == "EI-DYM" assert one.what == "Boeing 737-8AS" assert one.where == "EIDW → EGSS" shown = dict(one.rows) assert shown["from"] == "Dublin (EIDW)" assert shown["to"] == "Stansted (EGSS)" assert shown["route from"] == "a schedule service" assert shown["ICAO address"] == "4CA2D3" assert one.position == pytest.approx((53.4213, -6.2701)) def test_an_aircraft_with_no_registration_is_shown_by_its_address(tmp_path): where = write_aircraft(tmp_path / "f.json", [{"icao": "ABCDEF", "status": "unlisted"}]) assert reg.load_aircraft(where)[0].title == "ABCDEF" def test_a_cache_that_is_not_there_is_no_entries_rather_than_an_error(tmp_path): assert reg.load_callsigns(tmp_path / "nope.json") == [] assert reg.load_aircraft(tmp_path / "nope.json") == [] def test_a_cache_full_of_rubbish_is_no_entries_rather_than_an_error(tmp_path): bad = tmp_path / "c.json" bad.write_text("{ not json at all") assert reg.load_callsigns(bad) == [] bad.write_text("[1, 2, 3]") assert reg.load_callsigns(bad) == [] def test_the_two_caches_are_the_ones_everything_else_writes(): from bandsaunter.callsign import _cache_path as calls from bandsaunter.flights import _cache_path as planes where = reg.cache_paths() assert where["callsigns"] == calls() assert where["aircraft"] == planes() # --------------------------------------------------------------------------- # Maps # --------------------------------------------------------------------------- def test_a_map_link_points_at_the_coordinates_and_says_nothing_else(tmp_path): """The one part of this that leaves the machine, so it carries as little as it can: a map does not need to be told whose licence it is looking at in order to show a place.""" one = reg.load_callsigns(write_callsigns(tmp_path / "c.json", SOME))[0] url = one.maps() assert url.startswith("https://www.google.com/maps/") assert "32.275043" in url and "-110.813124" in url assert "AB7IC" not in url assert "Britton" not in url and "Brookwood" not in url def test_nothing_to_point_at_is_no_link_rather_than_a_wrong_one(tmp_path): entries = reg.load_callsigns(write_callsigns(tmp_path / "c.json", SOME)) vk = next(e for e in entries if e.title == "VK4BLE") assert vk.maps() == "" and reg.maps_url(None) == "" # --------------------------------------------------------------------------- # Searching # --------------------------------------------------------------------------- def test_searching_looks_at_every_field_and_not_only_the_name(tmp_path): """The interesting question is usually not "which callsign" but "who was in Arizona", and that lives in the detail.""" entries = reg.load_callsigns(write_callsigns(tmp_path / "c.json", SOME)) assert [e.title for e in reg.search(entries, "tucson")] == ["AB7IC"] assert [e.title for e in reg.search(entries, "brookwood")] == ["AB7IC"] assert [e.title for e in reg.search(entries, "australia")] == ["VK4BLE"] assert [e.title for e in reg.search(entries, "EXTRA")] == ["AB7IC"] def test_every_word_has_to_match_so_a_search_can_be_narrowed(tmp_path): entries = reg.load_callsigns(write_callsigns(tmp_path / "c.json", SOME)) assert [e.title for e in reg.search(entries, "united states")] != [] assert reg.search(entries, "tucson australia") == [] def test_an_empty_search_is_everything(tmp_path): entries = reg.load_callsigns(write_callsigns(tmp_path / "c.json", SOME)) assert len(reg.search(entries, "")) == len(entries) assert len(reg.search(entries, " ")) == len(entries) # --------------------------------------------------------------------------- # Browsing it # --------------------------------------------------------------------------- @pytest.fixture def browser(tmp_path, monkeypatch): """A browser whose register reads caches the test wrote.""" calls = write_callsigns(tmp_path / "callsigns.json", SOME) planes = write_aircraft(tmp_path / "flights.json", [ {"icao": "4CA2D3", "registration": "EI-DYM", "manufacturer": "Boeing", "model": "737-8AS", "status": "found", "fetched_at": 1.7e9}]) monkeypatch.setattr(reg, "cache_paths", lambda: {"callsigns": calls, "aircraft": planes}) console = Console(width=116, height=30, force_terminal=False) return Browser(tmp_path, console=console) def _screen(browser) -> str: with browser.console.capture() as caught: browser.console.print(browser.render()) return caught.get() def test_c_opens_the_register_and_closes_it_again(browser): assert not browser.showing_register browser.handle("c") assert browser.showing_register assert "62" not in browser.message # it read the test's caches assert "3 callsigns" in browser.message browser.handle("c") assert not browser.showing_register browser.handle("c") browser.handle("escape") assert not browser.showing_register def test_the_register_shows_what_is_in_it(browser): browser.handle("c") shown = _screen(browser) assert "the register" in shown assert "AB7IC" in shown and "Thomas G Britton" in shown assert "Tucson, AZ" in shown # A date, because when something was looked up is part of the record. assert "2023-" in shown or "2026-" in shown or "2020-" in shown def test_the_detail_shows_every_field_for_the_current_one(browser): browser.handle("c") browser.reg_index = 0 shown = _screen(browser) for bit in ("8410 E Brookwood Dr", "85750-2468", "DM42og", "EXTRA", "12/21/2033", "callook.info"): assert bit in shown, bit def test_tab_switches_between_callsigns_and_aircraft(browser): browser.handle("c") assert browser.reg_kind == "callsigns" browser.handle("tab") assert browser.reg_kind == "aircraft" shown = _screen(browser) assert "EI-DYM" in shown and "Boeing 737-8AS" in shown browser.handle("tab") assert browser.reg_kind == "callsigns" def test_moving_about_stays_inside_the_list(browser): browser.handle("c") for _ in range(20): browser.handle("down") _screen(browser) # the panel clamps as it draws assert browser.reg_index == 2 # three entries for _ in range(20): browser.handle("up") assert browser.reg_index == 0 browser.handle("end") _screen(browser) assert browser.reg_index == 2 browser.handle("home") assert browser.reg_index == 0 def test_searching_in_the_register_narrows_the_list(browser): browser.handle("c") browser.handle("/") for ch in "tucson": browser.handle(ch) assert browser.reg_query == "tucson" browser.handle("enter") shown = _screen(browser) assert "AB7IC" in shown and "VK4BLE" not in shown assert "matching" in shown browser.handle("/") browser.handle("escape") assert browser.reg_query == "" def test_backspace_takes_a_letter_off_a_search(browser): browser.handle("c") browser.handle("/") for ch in "tucsonx": browser.handle(ch) browser.handle("backspace") assert browser.reg_query == "tucson" def test_g_opens_the_place_in_a_browser(browser, monkeypatch): import webbrowser asked = [] monkeypatch.setattr(webbrowser, "open", lambda url: asked.append(url) or True) browser.handle("c") browser.reg_index = 0 browser.handle("g") assert len(asked) == 1 assert "32.275043" in asked[0] and "maps" in asked[0] assert "opened AB7IC" in browser.message def test_g_on_something_with_no_position_says_so_rather_than_guessing( browser, monkeypatch): import webbrowser asked = [] monkeypatch.setattr(webbrowser, "open", lambda url: asked.append(url) or True) browser.handle("c") browser.handle("/") for ch in "australia": browser.handle(ch) browser.handle("enter") browser.handle("g") assert asked == [] assert "where this one is" in browser.message def test_a_machine_with_no_browser_prints_the_place_instead(browser, monkeypatch): """A headless box is the normal case for a receiver, and the coordinates are still the answer somebody wanted.""" import webbrowser monkeypatch.setattr(webbrowser, "open", lambda url: False) browser.handle("c") browser.reg_index = 0 browser.handle("g") assert "no browser to open" in browser.message assert "32.275043" in browser.message def test_a_browser_that_will_not_start_is_reported_rather_than_raising( browser, monkeypatch): import webbrowser def boom(url): raise RuntimeError("no display") monkeypatch.setattr(webbrowser, "open", boom) browser.handle("c") browser.reg_index = 0 assert browser.handle("g") is True assert "could not open a browser" in browser.message def test_q_still_quits_from_the_register(browser): browser.handle("c") assert browser.handle("q") is False def test_the_register_is_read_once_rather_than_on_every_frame(browser, monkeypatch): reads = [] real = reg.load_all monkeypatch.setattr(reg, "load_all", lambda *a, **k: reads.append(1) or real(*a, **k)) browser.handle("c") for _ in range(5): _screen(browser) browser.handle("down") assert len(reads) == 1 # And r asks again, for somebody who has just finished a scan. browser.handle("r") assert len(reads) == 2 assert "read the caches again" in browser.message def test_the_keys_page_says_the_register_is_there(browser): browser.handle("?") shown = _screen(browser) assert "every callsign and aircraft ever looked up" in shown # The register's own keys are named on the register's own screen, the # keys page being exactly as tall as an eighty-by-twenty-four terminal. browser.handle("?") # close the keys page browser.handle("c") browser.message = "" # the footer shows counts first assert "g map" in _screen(browser) assert "tab switch" in _screen(browser) def test_an_empty_register_says_so_rather_than_drawing_a_blank(tmp_path, monkeypatch): monkeypatch.setattr(reg, "cache_paths", lambda: {"callsigns": tmp_path / "none.json", "aircraft": tmp_path / "none.json"}) console = Console(width=100, height=24, force_terminal=False) browser = Browser(tmp_path, console=console) browser.handle("c") shown = _screen(browser) assert "nothing here" in shown assert browser.reg_current is None # And g on nothing at all does not fall over. assert browser.handle("g") is True