M2-Karte t_b84af40f: '▶ Karte testen' (Editor-Seite). Großer deutsch beschrifteter Button oben + Menü-Eintrag: exportiert die AKTUELLE Map (auch ungespeichert) nach user://testrun/<name>.svmap über dieselbe SvmapIO-Validierung wie das Speichern, sucht den scopedvr-client (Klon neben dem Editor ODER client_dir in user://scopedvr_editor.cfg, dokumentiert im README-Abschnitt 'Karte testen') und startet ihn als EIGENEN Prozess (godot --path <client> -- --testmap <map> --mock-master) — Editor bleibt offen, Spiel beendet sich selbst sauber per ESC. Ungültige Map → Klartext-Block statt Start. TestRun-Klasse komplett static + headless testbar. Tests: tests/test_testrun.gd (14 Checks: Temp-Export-Roundtrip, ungültige Map blockiert, Client-Suche inkl. Konfig-Vorrang, Godot-Findung, exakte Argumentliste); run_tests.sh + parse_check.sh um test_testrun/test_run.gd erweitert — volle Suite 220 Checks grün. GODOT-Pfad-Default in tests/run_testmap_e2e.sh des Clients jetzt auto-erkennend (Sly-PC-Pfad → ~/bin/godot4 → PATH).

This commit is contained in:
hermes-godot-vr 2026-10-15 15:36:06 +00:00
parent 595d1217d9
commit 0eae5f56ad
8 changed files with 303 additions and 2 deletions

View file

@ -259,3 +259,34 @@ Mehrfachauswahl + Gruppieren + Gruppen-Verschieben, auf dem UX-Fundament
- **Desktop-Smoke:** `godot … -- --groups-demo [ordner] [--groups-demo-quit]` - **Desktop-Smoke:** `godot … -- --groups-demo [ordner] [--groups-demo-quit]`
fährt Mehrfachauswahl → Gruppieren → gemeinsames Ziehen/Drehen → fährt Mehrfachauswahl → Gruppieren → gemeinsames Ziehen/Drehen →
Undo programmatisch durch und speichert Screenshots. Undo programmatisch durch und speichert Screenshots.
## Karte testen (M2, Karte t_b84af40f — FERTIG)
Button „▶ Karte testen“ oben (auch im Datei-Menü) startet die AKTUELLE Map —
auch ungespeicherte! — sofort als Spiel in einem EIGENEN Fenster. Der Editor
bleibt offen. Im Spiel: WASD laufen, Maus umsehen, Shift sprinten, Leertaste
springen, ESC beendet das Test-Fenster sauber (kein Prozess-Kill nötig).
- **Temp-Export (`src/editor/test_run.gd`):** Die Map wird nach
`user://testrun/<name>.svmap` exportiert (deterministischer Name aus dem
Map-Namen, erneuter Testlauf überschreibt) — über dieselbe
`SvmapIO.save_to_file`-Validierung wie das echte Speichern. Ist die Map
noch nicht lauffähig (z. B. Spawn-Zonen fehlen), kommt der Klartext-Block
mit den fehlenden Punkten, GESTARTET wird nichts.
- **Client-Suche (erste Treffer-Zeile gewinnt):**
1. Konfigurationsdatei `user://scopedvr_editor.cfg`:
```ini
[test]
client_dir="/pfad/zu/scopedvr-client"
godot_path="/pfad/zu/godot" ; nur nötig, wenn der Editor NICHT über Godot läuft
```
2. Klon neben dem Editor: `<Editor-Ordner>/../scopedvr-client`
(erkennbar an `project.godot`).
- **Start:** Der Client läuft als eigener Godot-Prozess
(`godot --path <client> -- --testmap <map.svmap> --mock-master`) — kein
Master, kein Match-Server, alles lokal. Godot-Binary = das, unter dem der
Editor selbst läuft.
- **Tests:** `tests/test_testrun.gd` (14 Checks: Temp-Export mit Roundtrip,
ungültige Map blockiert, Client-Suche inkl. Konfig-Vorrang, Godot-Findung,
exakte Argumentliste). Parse-Check umfasst `test_run.gd`.

91
src/editor/test_run.gd Normal file
View file

@ -0,0 +1,91 @@
class_name TestRun
extends RefCounted
## „▶ Karte testen" (Karte t_b84af40f): Export der AKTUELLEN Map nach Temp
## + Start des scopedvr-client als EIGENES Fenster im Testlauf-Modus
## (--testmap <pfad>). Der Editor bleibt offen — der Client ist ein eigener
## Prozess; das Spiel beendet sich selbst sauber (ESC = „Zurück zum Editor").
##
## Client-Suche (erste Treffer-Zeile gewinnt):
## 1. Konfigurationsdatei user://scopedvr_editor.cfg, Abschnitt [test]:
## client_dir="/pfad/zu/scopedvr-client"
## 2. Klon neben dem Editor: <Editor-Projektordner>/../scopedvr-client
## (erkennbar an project.godot im Ordner).
##
## Godot-Binary für den Start: das Binary, unter dem der Editor selbst läuft
## (OS.get_executable_path — Sly startet den Editor über den Godot-Editor,
## also kann DAS auch den Client starten). Läuft der Editor irgendwann als
## exportierte App, muss [test] godot_path gesetzt werden.
##
## Alles static + testbar ohne UI (tests/test_testrun.gd).
const CFG_PATH := "user://scopedvr_editor.cfg"
const TESTRUN_DIR := "user://testrun"
static func load_cfg() -> ConfigFile:
var cfg := ConfigFile.new()
if FileAccess.file_exists(CFG_PATH):
cfg.load(CFG_PATH) # Fehler → Default-Logik greift weiter unten.
return cfg
## Liefert den Client-Projektordner (mit project.godot) oder "".
static func find_client(project_dir: String, cfg: ConfigFile = null) -> String:
var candidates: Array = []
if cfg != null:
var configured := str(cfg.get_value("test", "client_dir", ""))
# Explizit gesetzter Pfad gewinnt, sobald der ORDNER existiert
# (bewusst ohne project.godot-Check — wer ihn konfiguriert, weiß es besser).
if not configured.is_empty() and DirAccess.dir_exists_absolute(configured):
return configured
var parent := project_dir.simplify_path().get_base_dir()
candidates.append(parent.path_join("scopedvr-client"))
for candidate in candidates:
var dir := str(candidate).simplify_path()
if FileAccess.file_exists(dir.path_join("project.godot")):
return dir
return ""
## Godot-Binary zum Starten des Clients (Editor-Binary oder Konfiguration).
static func find_godot(cfg: ConfigFile = null) -> String:
var exe := OS.get_executable_path()
if exe.get_file().to_lower().contains("godot"):
return exe
if cfg != null:
return str(cfg.get_value("test", "godot_path", ""))
return ""
## Deterministischer Temp-Pfad aus dem Map-Namen (erneuter Testlauf
## überschreibt die alte Export-Datei — kein Müll-Berg).
static func temp_map_path(doc: Dictionary) -> String:
var raw := str(doc.get("info", {}).get("name", "")).strip_edges()
if raw.is_empty():
raw = "karte"
var safe := ""
for ch in raw:
var c := ch.to_lower()
if (c >= "a" and c <= "z") or (c >= "0" and c <= "9") or ch == "_" or ch == "-" or ch == " ":
safe += ch
else:
safe += "_"
safe = safe.strip_edges().replace(" ", "_").rstrip("_")
if safe.is_empty():
safe = "karte"
return TESTRUN_DIR.path_join(safe + ".svmap")
## Exportiert das AKTUELLE Dokument (auch ungespeicherte Maps!) nach Temp.
## Rückgabe: {ok, errors, path} — path ist globalisiert (für den Client-Prozess).
static func export_temp_map(doc: Dictionary) -> Dictionary:
var rel := temp_map_path(doc)
DirAccess.make_dir_recursive_absolute(ProjectSettings.globalize_path(TESTRUN_DIR))
var result := SvmapIO.save_to_file(rel, doc)
result["path"] = ProjectSettings.globalize_path(rel)
return result
## Argumentliste für den Client-Start (getestet: exakt diese Form).
static func launch_args(client_dir: String, map_path: String) -> PackedStringArray:
return PackedStringArray(["--path", client_dir, "--", "--testmap", map_path, "--mock-master"])

View file

@ -0,0 +1 @@
uid://cd3gmuvpf5ldv

View file

@ -94,6 +94,9 @@ var check_btn: Button
var check_dialog: AcceptDialog var check_dialog: AcceptDialog
var check_text: Label var check_text: Label
# Karte testen (Karte t_b84af40f)
var test_btn: Button
# Dialoge # Dialoge
var new_dialog: AcceptDialog var new_dialog: AcceptDialog
var open_dialog: FileDialog var open_dialog: FileDialog
@ -150,6 +153,8 @@ func _build_ui() -> void:
popup.add_item("Speichern (Strg+S)", 2) popup.add_item("Speichern (Strg+S)", 2)
popup.add_item("Speichern unter…", 3) popup.add_item("Speichern unter…", 3)
popup.add_separator() popup.add_separator()
popup.add_item("▶ Karte testen", 5)
popup.add_separator()
popup.add_item("Beenden", 4) popup.add_item("Beenden", 4)
popup.id_pressed.connect(_on_menu_action) popup.id_pressed.connect(_on_menu_action)
top_h.add_child(file_menu) top_h.add_child(file_menu)
@ -176,6 +181,14 @@ func _build_ui() -> void:
check_btn.pressed.connect(_on_check_map) check_btn.pressed.connect(_on_check_map)
top_h.add_child(check_btn) top_h.add_child(check_btn)
# Karte testen (Karte t_b84af40f): Map SOFORT als Spiel zum Durchlaufen.
test_btn = Button.new()
test_btn.text = "▶ Karte testen"
test_btn.custom_minimum_size = Vector2(190, 40)
test_btn.tooltip_text = "Startet die Map SOFORT als Spiel zum Durchlaufen — eigenes Fenster, der Editor bleibt offen. ESC im Spiel = zurück zum Editor."
test_btn.pressed.connect(_on_test_run)
top_h.add_child(test_btn)
var title_label := Label.new() var title_label := Label.new()
title_label.text = " %s" % APP_TITLE title_label.text = " %s" % APP_TITLE
title_label.add_theme_font_size_override("font_size", 20) title_label.add_theme_font_size_override("font_size", 20)
@ -756,6 +769,54 @@ func _on_menu_action(id: int) -> void:
save_dialog.popup_centered() save_dialog.popup_centered()
4: # Beenden 4: # Beenden
get_tree().quit() get_tree().quit()
5: # Karte testen
_on_test_run()
## „▶ Karte testen" (Karte t_b84af40f): aktuelle Map (auch ungespeichert!)
## nach Temp exportieren und den scopedvr-client als EIGENES Fenster starten.
## Der Editor bleibt offen; das Spiel beendet sich selbst sauber (ESC).
func _on_test_run() -> void:
if doc.is_empty():
_set_status("Erst eine Map anlegen (Datei → Neue Map), dann testen.", true)
return
var cfg := TestRun.load_cfg()
var client_dir := TestRun.find_client(ProjectSettings.globalize_path("res://"), cfg)
if client_dir.is_empty():
_show_errors("Karte testen — das Spiel wurde nicht gefunden:", [
"Der ScopedVR-Client (Ordner „scopedvr-client“) wurde nicht neben dem Editor gefunden.",
"So geht es: Den Client-Ordner neben den Editor-Ordner legen (gleicher Überordner),",
"ODER in der Datei scopedvr_editor.cfg im Ordner „ScopedVR Map Editor“ (Godot-App-Daten) eintragen:",
" [test]",
" client_dir=\"/pfad/zu/scopedvr-client\"",
" godot_path=\"/pfad/zu/godot\" (nur nötig, wenn der Editor NICHT über Godot gestartet wird)",
])
_set_status("Karte testen: Spiel nicht gefunden — siehe Fenster.", true)
return
var godot := TestRun.find_godot(cfg)
if godot.is_empty():
_show_errors("Karte testen — Godot-Programm nicht gefunden:", [
"Der Editor läuft gerade nicht über ein Godot-Programm, deshalb weiß er nicht,",
"wie er das Spiel-Fenster starten soll. Trage in scopedvr_editor.cfg ein:",
" [test]",
" godot_path=\"/pfad/zu/godot\"",
])
_set_status("Karte testen: Godot-Programm nicht gefunden — siehe Fenster.", true)
return
var exported := TestRun.export_temp_map(doc)
if not bool(exported["ok"]):
_show_errors("Karte testen — die Map ist noch nicht lauffähig:", exported["errors"])
_set_status("Testlauf nicht möglich — siehe Fenster für die fehlenden Punkte (z. B. Spawn-Zonen).", true)
return
var map_name := str(doc.get("info", {}).get("name", "?"))
var args := TestRun.launch_args(client_dir, str(exported["path"]))
var pid := OS.create_process(godot, args)
if pid <= 0:
_show_errors("Karte testen — Start fehlgeschlagen:", [
"Das Spiel-Fenster konnte nicht gestartet werden (Programm: %s)." % godot])
_set_status("Karte testen: Start fehlgeschlagen.", true)
return
_set_status("▶ Karte „%s“ läuft im Test-Fenster — WASD laufen, ESC zurück zum Editor. Der Editor bleibt offen." % map_name)
func _on_new_map_confirmed() -> void: func _on_new_map_confirmed() -> void:

116
tests/test_testrun.gd Normal file
View file

@ -0,0 +1,116 @@
extends SceneTree
## Editor-Tests (headless): Karte t_b84af40f — „▶ Karte testen" Verkabelung.
## Ausführen: godot --headless --path . --script res://tests/test_testrun.gd
## Exit 0 = alles grün. Läuft OHNE UI (TestRun/SvmapIO/SvmapDocument):
## Temp-Export (auch ungespeicherte Maps), Client-Suche, Godot-Binär-Findung,
## Argument-Aufbau — der eigentliche Prozess-Start ist Desktop-Sache
## (run_testmap_e2e.sh im CLIENT deckt die other-Seite ab).
var failures := 0
var checks := 0
const TEST_DIR := "user://test_testrun"
func _init() -> void:
_cleanup()
_run_tests()
_cleanup()
if failures == 0:
print("ALL OK: %d checks" % checks)
quit(0)
else:
print("FAILED: %d/%d checks" % [failures, checks])
quit(1)
func _check(cond: bool, label: String) -> void:
checks += 1
if cond:
print(" ok: %s" % label)
else:
failures += 1
print(" FAIL: %s" % label)
func _cleanup() -> void:
_cleanup_dir(ProjectSettings.globalize_path(TEST_DIR))
var rel := TestRun.temp_map_path({"info": {"name": "Testmap"}})
var p := ProjectSettings.globalize_path(rel)
if FileAccess.file_exists(p):
DirAccess.remove_absolute(p)
func _cleanup_dir(base: String) -> void:
if not DirAccess.dir_exists_absolute(base):
return
var d := DirAccess.open(base)
if d == null:
return
for file in d.get_files():
DirAccess.remove_absolute(base.path_join(file))
for sub in d.get_directories():
_cleanup_dir(base.path_join(sub))
DirAccess.remove_absolute(base)
func _run_tests() -> void:
print("== Temp-Map-Pfad ==")
var path := TestRun.temp_map_path({"info": {"name": "Offenes Gelände 2!"}})
_check(str(path).begins_with("user://testrun/"), "Temp-Pfad liegt unter user://testrun/")
_check(str(path).ends_with("Offenes_Gel_nde_2.svmap"), "Map-Name bereinigt (Sonderzeichen → _, kein trailing _)")
path = TestRun.temp_map_path({})
_check(str(path).ends_with("karte.svmap"), "fehlender Name → Fallback „karte.svmap“")
print("== Temp-Export (ungespeicherte Map) ==")
var doc := SvmapDocument.create_new("Testkarte", 256.0, "mittag")
var exported := TestRun.export_temp_map(doc)
_check(bool(exported["ok"]), "neue Map exportiert (ohne je gespeichert zu haben)")
var global_path := str(exported["path"])
_check(FileAccess.file_exists(global_path), "Temp-Datei existiert auf Disk")
var back := SvmapIO.load_from_file(global_path)
_check(bool(back["ok"]), "Temp-Datei lädt wieder (Prüfsumme gültig — voller Roundtrip)")
_check(str(back["data"]["info"]["name"]) == "Testkarte", "Inhalt identisch")
print("== Temp-Export (ungültige Map → Klartext-Block) ==")
var broken: Dictionary = doc.duplicate(true)
broken["spawn_zones"] = {"0": {"points": []}, "1": {"points": []}}
var failed := TestRun.export_temp_map(broken)
_check(not bool(failed["ok"]), "ungültige Map wird NICHT exportiert")
_check((failed["errors"] as Array).size() > 0, "Fehlerliste fürs Dialog-Fenster vorhanden")
print("== Client-Suche ==")
var base := ProjectSettings.globalize_path(TEST_DIR)
DirAccess.make_dir_recursive_absolute(base.path_join("mein-editor-projekt"))
DirAccess.make_dir_recursive_absolute(base.path_join("scopedvr-client"))
var f := FileAccess.open(base.path_join("scopedvr-client/project.godot"), FileAccess.WRITE)
f.store_string("[application]\n")
# 2) Nachbar-Klon wird gefunden:
var found := TestRun.find_client(base.path_join("mein-editor-projekt"), ConfigFile.new())
_check(found == base.path_join("scopedvr-client"),
"Nachbar-Klon ../scopedvr-client gefunden")
# 1) Konfiguration gewinnt (Ordner existiert, ohne project.godot-Check):
var cfg := ConfigFile.new()
cfg.set_value("test", "client_dir", base.path_join("irgendwo"))
DirAccess.make_dir_recursive_absolute(base.path_join("irgendwo"))
var found2 := TestRun.find_client(base.path_join("mein-editor-projekt"), cfg)
_check(found2 == base.path_join("irgendwo"), "Konfig-Pfad hat Vorrang (Ordner existiert)")
# 3) Gar nichts da:
DirAccess.remove_absolute(base.path_join("scopedvr-client/project.godot"))
var found3 := TestRun.find_client(base.path_join("mein-editor-projekt"), ConfigFile.new())
_check(found3 == "", "kein Client → leerer String (Fehler-Dialog greift)")
print("== Godot-Binär-Findung ==")
var exe := TestRun.find_godot(ConfigFile.new())
_check(not exe.is_empty() and exe.get_file().to_lower().contains("godot"),
"Editor läuft über Godot → dasselbe Binary zum Starten")
print("== Argument-Aufbau ==")
var args := TestRun.launch_args("/pfad/client", "/tmp/x.svmap")
var want := ["--path", "/pfad/client", "--", "--testmap", "/tmp/x.svmap", "--mock-master"]
var ok := args.size() == want.size()
if ok:
for i in want.size():
if str(args[i]) != want[i]:
ok = false
_check(ok, "launch_args exakt: --path … -- --testmap … --mock-master")

View file

@ -0,0 +1 @@
uid://dhycopnavsglm

View file

@ -3,7 +3,7 @@
cd "$(dirname "$0")/.." cd "$(dirname "$0")/.."
GODOT=~/bin/godot4 GODOT=~/bin/godot4
fail=0 fail=0
for f in src/ui/main.gd src/editor/library_browser.gd src/editor/object_ops.gd \ for f in src/ui/main.gd src/editor/test_run.gd src/editor/library_browser.gd src/editor/object_ops.gd \
src/editor/interaction.gd tools/gen_thumbs.gd tests/test_library.gd; do src/editor/interaction.gd tools/gen_thumbs.gd tests/test_library.gd; do
out=$("$GODOT" --headless --path . --check-only --script "res://$f" 2>&1 | grep -iv "godot engine\|^$" || true) out=$("$GODOT" --headless --path . --check-only --script "res://$f" 2>&1 | grep -iv "godot engine\|^$" || true)
if [ -n "$out" ]; then if [ -n "$out" ]; then

View file

@ -3,7 +3,7 @@
cd "$(dirname "$0")/.." cd "$(dirname "$0")/.."
GODOT=~/bin/godot4 GODOT=~/bin/godot4
rc=0 rc=0
for t in test_ux test_editor_svmap test_terrain test_library test_spawns; do for t in test_ux test_editor_svmap test_terrain test_library test_spawns test_testrun; do
"$GODOT" --headless --path . --script "res://tests/$t.gd" > "/tmp/$t.log" 2>&1 "$GODOT" --headless --path . --script "res://tests/$t.gd" > "/tmp/$t.log" 2>&1
code=$? code=$?
line=$(grep -E "ALL OK|FAILED" "/tmp/$t.log" | tail -1) line=$(grep -E "ALL OK|FAILED" "/tmp/$t.log" | tail -1)