Small fixes
This commit is contained in:
parent
54fbcb1ea6
commit
083f8a982e
24 changed files with 1505 additions and 79 deletions
130
finder/test_shutdown.py
Normal file
130
finder/test_shutdown.py
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
"""Tests for cooperative Ctrl+C shutdown across the scrapers."""
|
||||
|
||||
import signal
|
||||
import threading
|
||||
import time
|
||||
|
||||
import pytest
|
||||
|
||||
import http_client
|
||||
import scraper
|
||||
import shutdown
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _clean_stop_flag():
|
||||
"""Every test starts (and leaves) the global stop flag cleared."""
|
||||
shutdown.reset()
|
||||
yield
|
||||
shutdown.reset()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# The flag primitive
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_request_and_reset_toggle_the_flag():
|
||||
assert shutdown.stop_requested() is False
|
||||
shutdown.request_stop()
|
||||
assert shutdown.stop_requested() is True
|
||||
shutdown.reset()
|
||||
assert shutdown.stop_requested() is False
|
||||
|
||||
|
||||
def test_sleep_returns_immediately_once_stop_requested():
|
||||
shutdown.request_stop()
|
||||
started = time.monotonic()
|
||||
shutdown.sleep(30) # would hang for 30s without the early wake
|
||||
assert time.monotonic() - started < 1.0
|
||||
|
||||
|
||||
def test_sleep_wakes_when_stop_requested_from_another_thread():
|
||||
def _trip():
|
||||
time.sleep(0.05)
|
||||
shutdown.request_stop()
|
||||
|
||||
waker = threading.Thread(target=_trip)
|
||||
waker.start()
|
||||
started = time.monotonic()
|
||||
shutdown.sleep(30)
|
||||
waker.join()
|
||||
assert time.monotonic() - started < 1.0
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Signal handlers: first signal requests stop, second forces a hard exit
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_signal_handler_requests_stop_then_escalates():
|
||||
previous = signal.getsignal(signal.SIGINT)
|
||||
try:
|
||||
shutdown.install_signal_handlers()
|
||||
handler = signal.getsignal(signal.SIGINT)
|
||||
assert callable(handler)
|
||||
|
||||
# First Ctrl+C: graceful stop requested, no exception.
|
||||
handler(signal.SIGINT, None)
|
||||
assert shutdown.stop_requested() is True
|
||||
|
||||
# Second Ctrl+C: escalate to a hard interrupt.
|
||||
with pytest.raises(KeyboardInterrupt):
|
||||
handler(signal.SIGINT, None)
|
||||
finally:
|
||||
signal.signal(signal.SIGINT, previous)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# HTTP layer bails out instead of issuing more requests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_fetch_with_retry_returns_none_without_touching_client():
|
||||
class _ExplodingClient:
|
||||
def get(self, *_a, **_k): # pragma: no cover - must never be called
|
||||
raise AssertionError("no request should be made after shutdown")
|
||||
|
||||
shutdown.request_stop()
|
||||
assert http_client.fetch_with_retry(_ExplodingClient(), "http://x") is None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# A source loop stops at the next outcode boundary
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _londonish_prop(outcode):
|
||||
return {"id": outcode, "Postcode": f"{outcode} 0HD", "price": 100000, "Bedrooms": 2}
|
||||
|
||||
|
||||
def test_scrape_rightmove_stops_after_signal(monkeypatch):
|
||||
visited = []
|
||||
|
||||
class _DummyClient:
|
||||
def close(self):
|
||||
pass
|
||||
|
||||
monkeypatch.setattr(scraper, "make_client", lambda: _DummyClient())
|
||||
monkeypatch.setattr(scraper, "resolve_outcode_id", lambda _client, oc: oc)
|
||||
monkeypatch.setattr(scraper, "DELAY_BETWEEN_OUTCODES", 0)
|
||||
|
||||
def fake_search(_client, _oid, outcode, *_a, **_k):
|
||||
visited.append(outcode)
|
||||
# Operator hits Ctrl+C while the first outcode is being processed.
|
||||
shutdown.request_stop()
|
||||
return [_londonish_prop(outcode)]
|
||||
|
||||
monkeypatch.setattr(scraper, "rightmove_search_outcode", fake_search)
|
||||
|
||||
results = {source: [] for source in scraper.SOURCE_ORDER}
|
||||
errors: list[str] = []
|
||||
scraper._scrape_rightmove(
|
||||
["SW9", "E1", "BR1"], pc_index=None, results=results,
|
||||
errors=errors, max_properties_per_source=None,
|
||||
)
|
||||
|
||||
# Only the first outcode ran; the loop bailed before the next boundary.
|
||||
assert visited == ["SW9"]
|
||||
assert len(results["rightmove"]) == 1
|
||||
assert errors == []
|
||||
Loading…
Add table
Add a link
Reference in a new issue