Files
meshcore-bot/tests/unit/test_geocode_city_variants.py
agessaman 79f408f962 refactor(utils): one body each for geocode_city and geocode_zipcode, sync and async
geocode_city and geocode_city_sync were two 300-line copies, and geocode_zipcode and geocode_zipcode_sync two more, differing only in await. Each now has one body written as a generator that yields its Nominatim requests; _run_geocode_steps_sync and _run_geocode_steps_async perform them with the matching rate-limited calls and send back the result or throw back the exception, so the body's own error handling (bare excepts included) sees what a direct call would. The async variant keeps its own rate limiter path.

The city copies had drifted on one rule, kept behind async_rules: the async variant always reverse-geocodes an unqualified city's first hit to judge whether it is an obscure place in the default country, with a looser name match, and reuses that lookup for address info; the sync variant only checks when address info is requested. tests/unit/test_geocode_city_variants.py pins both. A side-by-side run of the old and new module over 3,840 city cases and 960 zip cases (scripted results, failures and caches, both variants, with and without address info) matched on return values, Nominatim call sequence and cache calls; flipping any one rule branch produced differences.
2026-10-01 11:10:09 -07:00

103 lines
3.5 KiB
Python

"""geocode_city and geocode_city_sync share one body but keep their drifted rule.
The async variant always reverse-geocodes an unqualified city's first hit to
decide whether it is an obscure place in the default country; the sync variant
only does so when address info was requested. Unifying them is a behavior
change and belongs in its own PR.
"""
import asyncio
import configparser
from unittest.mock import MagicMock, patch
import modules.utils as utils
def _bot():
bot = MagicMock()
config = configparser.ConfigParser()
config.add_section("Weather")
config.set("Weather", "default_state", "WA")
bot.config = config
bot.db_manager.get_cached_geocoding.return_value = (None, None)
bot.db_manager.get_cached_json.return_value = None
return bot
def _location(lat, lon):
loc = MagicMock()
loc.latitude, loc.longitude = lat, lon
return loc
def _fakes(calls):
# "Snoqualmie" alone resolves to an obscure US township; with the default
# state it resolves to the real city.
def geocode(bot, query, timeout=10):
calls.append(("geocode", query))
return _location(1.0, 1.0) if query == "Snoqualmie" else _location(2.0, 2.0)
def reverse(bot, coords, timeout=10):
calls.append(("reverse", coords))
result = MagicMock()
result.raw = {"address": {"country": "United States", "country_code": "us",
"town": "Snoqualmie Township", "type": "township"}}
return result
async def ageocode(bot, query, timeout=10):
return geocode(bot, query, timeout)
async def areverse(bot, coords, timeout=10):
return reverse(bot, coords, timeout)
return geocode, reverse, ageocode, areverse
def _patched(calls):
geocode, reverse, ageocode, areverse = _fakes(calls)
return (
patch.object(utils, "rate_limited_nominatim_geocode_sync", geocode),
patch.object(utils, "rate_limited_nominatim_reverse_sync", reverse),
patch.object(utils, "rate_limited_nominatim_geocode", ageocode),
patch.object(utils, "rate_limited_nominatim_reverse", areverse),
)
def test_async_skips_an_obscure_first_hit_without_address_info():
calls = []
p1, p2, p3, p4 = _patched(calls)
with p1, p2, p3, p4:
lat, lon, info = asyncio.run(utils.geocode_city(_bot(), "Snoqualmie"))
assert (lat, lon, info) == (2.0, 2.0, None)
assert calls[:2] == [("geocode", "Snoqualmie"), ("reverse", "1.0, 1.0")]
def test_sync_takes_the_first_hit_without_address_info():
calls = []
p1, p2, p3, p4 = _patched(calls)
with p1, p2, p3, p4:
lat, lon, info = utils.geocode_city_sync(_bot(), "Snoqualmie")
assert (lat, lon, info) == (1.0, 1.0, None)
assert calls == [("geocode", "Snoqualmie")]
def test_sync_checks_the_first_hit_when_address_info_is_requested():
calls = []
p1, p2, p3, p4 = _patched(calls)
with p1, p2, p3, p4:
lat, lon, _ = utils.geocode_city_sync(_bot(), "Snoqualmie", include_address_info=True)
assert (lat, lon) == (2.0, 2.0)
assert calls[1] == ("reverse", "1.0, 1.0")
def test_geocoder_errors_reach_the_shared_body_as_they_would_a_direct_call():
bot = _bot()
def failing(bot, query, timeout=10):
raise RuntimeError("nominatim down")
with patch.object(utils, "rate_limited_nominatim_geocode_sync", failing):
assert utils.geocode_zipcode_sync(bot, "98101") == (None, None)
bot.logger.error.assert_called_once()
assert "nominatim down" in bot.logger.error.call_args.args[0]