DevPlace CI / test (push) Failing after 44m45s
Continuous Delivery could never be turned off once a crop was planted. Add a one-shot per-plot "stop after harvest" flag (POST /game/plot/stop, Devii tool game_plot_stop, game_plots.stop_after_harvest), cleared by the next plant, autoreplant claim or Refactor. Autoreplant now also respects crop locks through the shared _crop_lock predicate. Plan and measurements in happyplanting.md. Also includes the pending community sync, node network and related work in the tree; the full suite passed (4999) with all of it. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
127 lines
5.1 KiB
Python
127 lines
5.1 KiB
Python
# retoor <retoor@molodetz.nl>
|
|
|
|
import gzip
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from devplacepy.services.nodes import geo
|
|
from devplacepy.services.nodes.geo import GeoDatabase, GeoError, build, pack
|
|
|
|
ASN_ROWS = [
|
|
("1.0.0.0", "1.0.0.255", 13335, "Cloudflare, Inc."),
|
|
("8.8.8.0", "8.8.8.255", 15169, "Google LLC"),
|
|
("20.0.0.0", "20.0.0.255", 64500, "Home Fiber BV"),
|
|
("2001:db8::", "2001:db8::ffff", 64501, "Mobile Wireless NV"),
|
|
]
|
|
COUNTRY_ROWS = [
|
|
("1.0.0.0", "1.0.0.255", "AU"),
|
|
("8.8.8.0", "8.8.8.255", "US"),
|
|
("20.0.0.0", "20.0.0.255", "NL"),
|
|
("2001:db8::", "2001:db8::ffff", "ZZ"),
|
|
]
|
|
|
|
|
|
def lines(rows):
|
|
return [",".join(f'"{part}"' if "," in str(part) else str(part) for part in row) for row in rows]
|
|
|
|
|
|
def make(tmp_path, monkeypatch, checks=()):
|
|
monkeypatch.setattr(geo, "SPEC", geo.GeoSpec(min_asn_rows=1, min_country_rows=1, checks=checks))
|
|
target = tmp_path / "geo.db"
|
|
built = build(lines(ASN_ROWS), lines(COUNTRY_ROWS), target, "2026-10")
|
|
geo.install(built, target)
|
|
return GeoDatabase(target)
|
|
|
|
|
|
def test_pack_normalises_v4_mapped_and_rejects_garbage():
|
|
assert pack("::ffff:8.8.8.8") == pack("8.8.8.8")
|
|
assert pack("[2001:db8::1]")[0] == 6
|
|
with pytest.raises(ValueError):
|
|
pack("nope")
|
|
|
|
|
|
def test_lookup_hits_first_last_and_middle_of_a_range(tmp_path, monkeypatch):
|
|
database = make(tmp_path, monkeypatch)
|
|
for address in ("1.0.0.0", "1.0.0.77", "1.0.0.255"):
|
|
found = database.lookup(address)
|
|
assert (found.asn, found.country, found.asn_class) == (13335, "AU", "datacenter")
|
|
|
|
|
|
def test_lookup_between_and_outside_ranges_is_unknown(tmp_path, monkeypatch):
|
|
database = make(tmp_path, monkeypatch)
|
|
for address in ("1.0.1.0", "0.255.255.255", "9.9.9.9", "255.255.255.255"):
|
|
assert database.lookup(address).asn_class == "unknown"
|
|
|
|
|
|
def test_ipv6_and_zz_country(tmp_path, monkeypatch):
|
|
database = make(tmp_path, monkeypatch)
|
|
found = database.lookup("2001:db8::1234")
|
|
assert found.asn == 64501 and found.country == "" and found.asn_class == "mobile"
|
|
assert database.lookup("2001:db9::1").asn == 0
|
|
|
|
|
|
def test_classification_of_a_residential_range(tmp_path, monkeypatch):
|
|
assert make(tmp_path, monkeypatch).lookup("20.0.0.9").asn_class == "residential"
|
|
|
|
|
|
def test_missing_file_and_bad_input_degrade_to_unknown(tmp_path):
|
|
database = GeoDatabase(tmp_path / "absent.db")
|
|
assert not database.available()
|
|
assert database.lookup("8.8.8.8").asn_class == "unknown"
|
|
assert database.status()["available"] is False
|
|
assert GeoDatabase(tmp_path / "absent.db").lookup("").asn_class == "unknown"
|
|
|
|
|
|
def test_a_too_small_dataset_is_refused_and_leaves_no_file(tmp_path, monkeypatch):
|
|
monkeypatch.setattr(geo, "SPEC", geo.GeoSpec(min_asn_rows=10, min_country_rows=10, checks=()))
|
|
target = tmp_path / "geo.db"
|
|
with pytest.raises(GeoError):
|
|
build(lines(ASN_ROWS), lines(COUNTRY_ROWS), target, "2026-10")
|
|
assert not target.exists() and not target.with_suffix(".tmp").exists()
|
|
|
|
|
|
def test_a_failed_sanity_check_refuses_and_keeps_the_current_database(tmp_path, monkeypatch):
|
|
current = make(tmp_path, monkeypatch)
|
|
before = current.path.read_bytes()
|
|
monkeypatch.setattr(geo, "SPEC", geo.GeoSpec(min_asn_rows=1, min_country_rows=1, checks=(("8.8.8.8", 99999),)))
|
|
with pytest.raises(GeoError):
|
|
build(lines(ASN_ROWS), lines(COUNTRY_ROWS), tmp_path / "geo.db", "2026-11")
|
|
assert current.path.read_bytes() == before
|
|
|
|
|
|
def test_the_row_cap_is_enforced(tmp_path, monkeypatch):
|
|
monkeypatch.setattr(geo, "SPEC", geo.GeoSpec(max_rows=2, min_asn_rows=1, min_country_rows=1, checks=()))
|
|
with pytest.raises(GeoError):
|
|
build(lines(ASN_ROWS), lines(COUNTRY_ROWS), tmp_path / "geo.db", "2026-10")
|
|
|
|
|
|
def test_malformed_rows_are_skipped_not_fatal(tmp_path, monkeypatch):
|
|
monkeypatch.setattr(geo, "SPEC", geo.GeoSpec(min_asn_rows=1, min_country_rows=1, checks=()))
|
|
asn = lines(ASN_ROWS) + ["garbage", "a,b,c,d", "1.2.3.4,1.2.3.5,notanumber,x"]
|
|
built = build(asn, lines(COUNTRY_ROWS), tmp_path / "geo.db", "2026-10")
|
|
assert built["asn_rows"] == len(ASN_ROWS)
|
|
|
|
|
|
def test_status_reports_metadata_and_attribution(tmp_path, monkeypatch):
|
|
status = make(tmp_path, monkeypatch).status()
|
|
assert status["available"] and status["month"] == "2026-10" and "DB-IP" in status["attribution"]
|
|
|
|
|
|
def test_months_walk_back_over_a_year_boundary():
|
|
from datetime import datetime, timezone
|
|
|
|
assert geo.months(datetime(2026, 1, 15, tzinfo=timezone.utc)) == ["2026-01", "2025-12", "2025-11"]
|
|
|
|
|
|
def test_build_from_files_reads_gzip(tmp_path, monkeypatch):
|
|
monkeypatch.setattr(geo, "SPEC", geo.GeoSpec(min_asn_rows=1, min_country_rows=1, checks=()))
|
|
asn_file, country_file = tmp_path / "a.csv.gz", tmp_path / "c.csv.gz"
|
|
with gzip.open(asn_file, "wt") as handle:
|
|
handle.write("\n".join(lines(ASN_ROWS)))
|
|
with gzip.open(country_file, "wt") as handle:
|
|
handle.write("\n".join(lines(COUNTRY_ROWS)))
|
|
built = geo.build_from_files(asn_file, country_file, tmp_path / "geo.db", "2026-10")
|
|
geo.install(built, tmp_path / "geo.db")
|
|
assert GeoDatabase(tmp_path / "geo.db").lookup("8.8.8.8").asn == 15169
|