Files
gta6/tools/extract/tests/test_park_config.py
T
knsoandClaude Opus 5 3c74202b5e fix(extract): Chunk-Ausgabe lauf-stabil, Eckenprüfung in beide Richtungen
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>
2026-09-16 13:43:01 +02:00

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)