emit.py normalisiert jede ausgegebene Fläche und sortiert die Listen nach der Ausgabestruktur selbst. unary_union() in apply_street() gab das kartenweite Straßenpolygon mit anderem Ringstartpunkt und an anderer Listenstelle zurück und änderte damit 88 Chunkdateien, in denen sich geometrisch nichts bewegt hatte (symmetrische Differenz 0,00 m²). Belagsarten und Mittellinien behalten ihre Reihenfolge: overview.py malt die Beläge in Listenreihenfolge übereinander, und vergeLampPoints() hängt an Reihenfolge und Laufrichtung der Mittellinien — Sortieren verschöbe dort 1.848 von 3.921 Laternen auf die andere Straßenseite. check_corners() misst gegen den Straßenrand statt gegen die Straßenfläche. Alle vier Ecken liegen in der Fahrbahn, ihr Flächenabstand ist 0,000 m; die alte Prüfung schlug deshalb nur an, wenn eine Straße sich zurückzieht. Eine nach innen verbreiterte Straße hätte apply_park() still auf das veraltete Viereck zurückgeschnitten. Die Parkwege werden am Parkviereck abgeschnitten — die runden Kappen von LineString.buffer() lagen mit 95,96 m² darüber hinaus, rund 41 m² davon sichtbar als Betonzungen auf den umgebenden Belägen. load_elements() prüft den OSM-Abzug so streng wie die Parkdatei, mit Dateiname statt nacktem KeyError. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
119 lines
4.6 KiB
Python
119 lines
4.6 KiB
Python
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from leonida.park import Anchor, Keepout, ParkError, load_elements, load_park, parse_park
|
|
|
|
DATA = Path(__file__).resolve().parents[1] / "data"
|
|
|
|
VALID = """
|
|
osm = "osm.json"
|
|
|
|
[fit]
|
|
real = { sw = [25.78, -80.14], se = [25.78, -80.13], nw = [25.79, -80.14] }
|
|
game = { sw = [0, 0], se = [100, 0], ne = [100, 200], nw = [0, 200] }
|
|
scale_u = 1.0
|
|
offset_u = 5
|
|
|
|
[[anchor]]
|
|
name = "Ort"
|
|
real = [25.785, -80.135]
|
|
game = [50, 100]
|
|
|
|
[[keepout]]
|
|
name = "ort"
|
|
at = [50, 100]
|
|
rotation = 45
|
|
bounds = { min_x = -10, max_x = 10, min_z = -5, max_z = 5 }
|
|
|
|
[paths]
|
|
main_width = 6
|
|
width = 3
|
|
service_width = 6
|
|
|
|
[buildings]
|
|
default_height = 4
|
|
|
|
[street]
|
|
from = [10, 0]
|
|
to = [10, -100]
|
|
width = 12
|
|
remove_buildings = [[12, -50]]
|
|
alley_cut = [[0, -1], [5, -1], [5, -99], [0, -99]]
|
|
"""
|
|
|
|
|
|
def parse(text: str, tmp_path: Path):
|
|
(tmp_path / "osm.json").write_text('{"elements": []}', encoding="utf-8")
|
|
return parse_park(text, tmp_path, "park.toml")
|
|
|
|
|
|
def test_valid_file_parses_into_a_config(tmp_path):
|
|
config = parse(VALID, tmp_path)
|
|
assert config.osm_path == tmp_path / "osm.json"
|
|
assert config.real == {"sw": (25.78, -80.14), "se": (25.78, -80.13), "nw": (25.79, -80.14)}
|
|
assert config.game["ne"] == (100.0, 200.0)
|
|
assert (config.scale_u, config.offset_u) == (1.0, 5.0)
|
|
assert config.anchors == (Anchor("Ort", (25.785, -80.135), (50.0, 100.0)),)
|
|
assert config.keepouts == (Keepout("ort", (50.0, 100.0), 45.0, (-10.0, 10.0, -5.0, 5.0)),)
|
|
assert (config.main_width, config.path_width, config.service_width) == (6.0, 3.0, 6.0)
|
|
assert config.default_height == 4.0
|
|
assert (config.street.start, config.street.end, config.street.width) == ((10.0, 0.0), (10.0, -100.0), 12.0)
|
|
assert config.street.remove_buildings == ((12.0, -50.0),)
|
|
assert config.street.alley_cut[1] == (5.0, -1.0)
|
|
|
|
|
|
@pytest.mark.parametrize("old, new, message", [
|
|
("offset_u = 5\n", "", "Feld 'fit.offset_u' fehlt"),
|
|
("offset_u = 5", "offest_u = 5", "Feld 'fit.offest_u' gibt es nicht"),
|
|
("width = 3\n", 'width = "3"\n', "Feld 'paths.width' muss eine Zahl"),
|
|
("main_width = 6", "main_width = 0", "Feld 'paths.main_width' muss eine Zahl größer als 0"),
|
|
("default_height = 4", "default_height = -1", "Feld 'buildings.default_height' muss eine Zahl größer als 0"),
|
|
("min_x = -10, max_x = 10", "min_x = 10, max_x = -10", "min muss kleiner als max"),
|
|
("game = [50, 100]", "game = [50]", "Feld 'anchor[1].game' muss ein Punkt"),
|
|
("[5, -1], [5, -99]", "[5, -99], [5, -1]", "ohne sich schneidende Kanten"),
|
|
('osm = "osm.json"', 'osm = "fehlt.json"', "uv run python osm.py"),
|
|
])
|
|
def test_form_errors_abort_with_file_and_field(tmp_path, old, new, message):
|
|
assert old in VALID
|
|
with pytest.raises(ParkError, match="park.toml") as err:
|
|
parse(VALID.replace(old, new, 1), tmp_path)
|
|
assert message in str(err.value)
|
|
|
|
|
|
def test_missing_park_file_aborts(tmp_path):
|
|
with pytest.raises(ParkError, match="fehlt"):
|
|
load_park(tmp_path / "flamingo-park.toml")
|
|
|
|
|
|
def test_committed_park_file_loads():
|
|
config = load_park(DATA / "flamingo-park.toml")
|
|
assert config.osm_path.exists()
|
|
assert [k.name for k in config.keepouts] == ["hood-yard", "tribuene"]
|
|
assert (config.scale_u, config.offset_u) == (1.0, 6.25)
|
|
assert len(config.street.alley_cut) == 12
|
|
assert len(load_elements(config.osm_path)) > 0
|
|
|
|
|
|
@pytest.mark.parametrize("text, message", [
|
|
("{ abgeschnitten", "kein gültiges JSON"),
|
|
('{"license": "ODbL 1.0"}', "Feld 'elements' fehlt oder ist keine Liste"),
|
|
('{"elements": {"1": {}}}', "Feld 'elements' fehlt oder ist keine Liste"),
|
|
('{"elements": [42]}', "elements[1] braucht eine Tabelle 'tags'"),
|
|
('{"elements": [{"id": 7, "geometry": [[25.78, -80.14], [25.79, -80.14]]}]}',
|
|
"elements[1] (id 7) braucht eine Tabelle 'tags'"),
|
|
('{"elements": [{"id": 7, "tags": {"highway": "footway"}}]}',
|
|
"elements[1] (id 7) braucht 'geometry' als Liste von Punkten"),
|
|
('{"elements": [{"id": 7, "tags": {}, "geometry": [[25.78]]}]}',
|
|
"elements[1] (id 7) braucht 'geometry' als Liste von Punkten"),
|
|
])
|
|
def test_a_broken_osm_dump_aborts_with_the_file_name(tmp_path, text, message):
|
|
"""osm.py wird von Hand aufgerufen; ein alter oder halb geschriebener Abzug ist der Normalfall,
|
|
nicht die Ausnahme. Er soll denselben ParkError mit Dateinamen geben wie die Parkdatei, statt
|
|
als KeyError irgendwo in build_layout() zu landen."""
|
|
path = tmp_path / "flamingo-park.json"
|
|
path.write_text(text, encoding="utf-8")
|
|
with pytest.raises(ParkError, match="flamingo-park.json") as err:
|
|
load_elements(path)
|
|
assert message in str(err.value)
|