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)