Map-Editor Selbst-Update (t_d95f087a): Start-Check + Banner + Jetzt-updaten + Pruef-Button — EditorUpdater (src/editor/updater.gd): stiller Check beim Start (git ls-remote origin/main vs. lokaler HEAD, keep-alive-leicht), Bernstei-Banner „🔄 Update verfügbar: alt → neu“ mit Knopf „Jetzt updaten“, Datei-Menü-ID 7 „🔄 Updates prüfen“ mit Statuszeilen-Ergebnis (Update verfügbar / auf dem neusten Stand / Nicht erreichbar). Update-Ausführung: Dirty-Schutz via _request_lossy (derselbe deutsche Dialog wie Strg+O/N, Abbrechen bricht ab), dann git pull --ff-only (Tunnel + credential store) → Godot-Import-Lauf (--headless --import) → Neustart. Import-Ausgabe wird geparst: Parse-/SCRIPT-Fehler trotz Exit-Code 0 = Abbruch mit Meldung, KEIN Neustart (Lektion 17.10. class_name-Cache). Restart plattformneutral: cfg [update] restart_command > systemd-User-Unit map-editor auto-erkennung (systemd-run --on-active=3s Kick im eigenen Scope, überlebt den Unit-Neustart) > Selbst-Restart (dieselbe Godot-Binary, --editor --path). Keine Linux-Pfade hartkodiert; git_command aus cfg möglich. Offline: kein Banner, kein Fehler-Geballer. Ein-Hintergrund-Thread je Richtung (check/update) mit Busy-Guard, Thread-join über call_deferred-Ordnung. Tests: neue Suite test_updater (37 Checks: Check divergiert/gleich/unreachable/kein-Repo, Banner-Texte, Pull gegen echte Temp-Klone, Import-Abbruch-Pfad inkl. Exit-0-Falle, Restart-Auflösung), Suite 13/13 ALL OK (997 Checks). docs/tasten.md: Abschnitt „Updates“; H-Overlay-Zeile.
This commit is contained in:
parent
2065ef2252
commit
f2b4f6a824
8 changed files with 607 additions and 1 deletions
|
|
@ -81,6 +81,35 @@ nichts geht versehentlich verloren.
|
||||||
| **Strg+O** | Map öffnen |
|
| **Strg+O** | Map öffnen |
|
||||||
| **Strg+N** | Neue Map |
|
| **Strg+N** | Neue Map |
|
||||||
|
|
||||||
|
## Updates (Datei-Menü)
|
||||||
|
|
||||||
|
Der Editor aktualisiert sich selbst aus seinem Git-Klon (Karte t_d95f087a) —
|
||||||
|
kein Programmdownload, nur „Versionsstände holen“:
|
||||||
|
|
||||||
|
| Weg | Wirkung |
|
||||||
|
| --- | --- |
|
||||||
|
| **Beim Start** | Still prüfen. Gibt es eine neuere Version, erscheint eine Bernstei-Leiste unter der Menüleiste: **„🔄 Update verfügbar: `<alt>` → `<neu>`“** |
|
||||||
|
| **Banner → „🔄 Jetzt updaten“** | Installiert das Update: neue Version laden (git pull), Import-Lauf, dann startet der Editor von selbst neu. Ungespeicherte Änderungen werden vorher abgefragt (derselbe Dialog wie bei „Neue Map“/„Öffnen“ — **Abbrechen** bricht das Update ab) |
|
||||||
|
| **Datei → „🔄 Updates prüfen“** | Gleicher Check auf Wunsch, mit Ergebnis-Meldung in der Statuszeile: „Update verfügbar“ / „Du bist auf dem neusten Stand (`<sha>`)“ / „Nicht erreichbar“ |
|
||||||
|
|
||||||
|
- **Offline / Tunnel weg:** kein Banner, keine Meldung — der Editor bleibt
|
||||||
|
einfach so, wie er ist. Prüfen kann man trotzdem: die Meldung sagt es dann
|
||||||
|
in der Statuszeile.
|
||||||
|
- **Fehlgeschlagener Import:** bricht das Update mit Klartext-Meldung ab —
|
||||||
|
der Editor läuft UNVERÄNDERT weiter (kein Neustart in einen kaputten Stand,
|
||||||
|
Lektion 17.10. class_name-Cache).
|
||||||
|
- **Neustart:** Der Editor startet sich selbst neu (dasselbe Godot-Programm,
|
||||||
|
derselbe Projektordner, wieder im Editor-Modus). Wer den Editor als
|
||||||
|
systemd-User-Unit betreibt, setzt in `scopedvr_editor.cfg` (Godot-App-Daten)
|
||||||
|
Abschnitt `[update]` einen `restart_command` — z. B.
|
||||||
|
`restart_command="systemd-run --user --on-active=3s systemctl --user restart map-editor"`.
|
||||||
|
Ohne Eintrag wird unter Linux automatisch erkannt, ob die Unit
|
||||||
|
`map-editor.service` existiert; sonst Selbst-Restart.
|
||||||
|
- **Konfiguration** (alle optional, `user://scopedvr_editor.cfg`, Abschnitt
|
||||||
|
`[update]`): `restart_command` (eigener Neustart-Befehl), `git_command`
|
||||||
|
(eigener git-Pfad). Der Import-Lauf nutzt dieselbe Godot-Binary wie der
|
||||||
|
Editor selbst (bzw. `[test] godot_path`).
|
||||||
|
|
||||||
## Kartengröße ändern (Datei-Menü)
|
## Kartengröße ändern (Datei-Menü)
|
||||||
|
|
||||||
„Kartengröße ändern…“ (Datei-Menü) öffnet einen Dialog mit Live-Vorschau:
|
„Kartengröße ändern…“ (Datei-Menü) öffnet einen Dialog mit Live-Vorschau:
|
||||||
|
|
|
||||||
182
src/editor/updater.gd
Normal file
182
src/editor/updater.gd
Normal file
|
|
@ -0,0 +1,182 @@
|
||||||
|
class_name EditorUpdater
|
||||||
|
extends RefCounted
|
||||||
|
## Editor-Selbstupdate (Karte t_d95f087a) — Update-Check über Git, Update-
|
||||||
|
## Ausführung (git pull + Godot-Import-Lauf) und plattformneutrale Restart-
|
||||||
|
## Auflösung. Alles static und ohne UI (tests/test_updater.gd).
|
||||||
|
##
|
||||||
|
## Regeln (Karte + Sly-Ansatz „das Programm sich selbst updaten lassen“):
|
||||||
|
## - Transport = Git (Klon des Editors), KEIN Binary-Download.
|
||||||
|
## - Keine Linux-Pfade hartkodiert: git (default "git") und ein optionaler
|
||||||
|
## Restart-Befehl kommen aus user://scopedvr_editor.cfg, Abschnitt [update]
|
||||||
|
## (git_command / restart_command). Ohne Konfiguration: unter Linux wird
|
||||||
|
## erkannt, ob eine systemd-User-Unit „map-editor“ existiert — dann klingelt
|
||||||
|
## ein Transient-Timer-Kick (systemd-run) den Unit-Neustart (der Kick läuft
|
||||||
|
## im EIGENEN Scope und überlebt die Stop-Phase). Sonst: der Editor startet
|
||||||
|
## sich selbst neu (mode "self", self_restart_args).
|
||||||
|
## - Import-Lauf-Lektion 17.10.: ein --import-Lauf kann mit Exit-Code 0
|
||||||
|
## ENDEN und trotzdem Parse-/Script-Fehler enthalten (class_name-Cache).
|
||||||
|
## Die Ausgabe wird deshalb zusätzlich gescannt — bei Fehlern wird NICHT
|
||||||
|
## neu gestartet (Abbruch mit Meldung), sonst würde Sly einen Editor ohne
|
||||||
|
## Map-Ansicht sehen.
|
||||||
|
## - Offline / Tunnel weg → status "unreachable" → die UI bleibt still
|
||||||
|
## (kein Banner, kein Fehler-Geballer).
|
||||||
|
|
||||||
|
const BRANCH := "main"
|
||||||
|
const UNIT_NAME := "map-editor"
|
||||||
|
|
||||||
|
|
||||||
|
## Ein Git-Kommando im Projektordner ausführen. Rückgabe {ok, code, output}.
|
||||||
|
static func git_run(git_exe: String, project_dir: String, args: PackedStringArray) -> Dictionary:
|
||||||
|
var full := PackedStringArray(["-C", project_dir])
|
||||||
|
full.append_array(args)
|
||||||
|
var output: Array = []
|
||||||
|
var code := OS.execute(git_exe, full, output, true)
|
||||||
|
var text := ""
|
||||||
|
for line in output:
|
||||||
|
text += str(line)
|
||||||
|
return {"ok": code == 0, "code": code, "output": text}
|
||||||
|
|
||||||
|
|
||||||
|
## Git-Binary: Konfiguration (user://scopedvr_editor.cfg → [update]
|
||||||
|
## git_command) gewinnt, sonst das Standard-"git" (PATH).
|
||||||
|
static func git_exe(cfg: ConfigFile = null) -> String:
|
||||||
|
if cfg != null:
|
||||||
|
var custom := str(cfg.get_value("update", "git_command", ""))
|
||||||
|
if not custom.is_empty():
|
||||||
|
return custom
|
||||||
|
return "git"
|
||||||
|
|
||||||
|
|
||||||
|
## Lokaler HEAD-SHA. "" = kein Git-Repository (oder git nicht gefunden).
|
||||||
|
static func head_sha(git_exe: String, project_dir: String) -> String:
|
||||||
|
var r := git_run(git_exe, project_dir, PackedStringArray(["rev-parse", "HEAD"]))
|
||||||
|
if not bool(r["ok"]):
|
||||||
|
return ""
|
||||||
|
var text := str(r["output"]).strip_edges()
|
||||||
|
if text.is_empty():
|
||||||
|
return ""
|
||||||
|
return text.split("\n")[0].strip_edges()
|
||||||
|
|
||||||
|
|
||||||
|
## Remote-SHA per git ls-remote (cheap, lesend — keine lokalen Referenzen
|
||||||
|
## verändert). "" = nicht erreichbar / Branch fehlt.
|
||||||
|
static func remote_sha(git_exe: String, project_dir: String, branch: String) -> String:
|
||||||
|
var r := git_run(git_exe, project_dir, PackedStringArray([
|
||||||
|
"ls-remote", "origin", "refs/heads/%s" % branch]))
|
||||||
|
if not bool(r["ok"]):
|
||||||
|
return ""
|
||||||
|
var text := str(r["output"]).strip_edges()
|
||||||
|
if text.is_empty():
|
||||||
|
return ""
|
||||||
|
return text.split("\n")[0].strip_edges().split("\t")[0]
|
||||||
|
|
||||||
|
|
||||||
|
## Update-Check: lokaler HEAD vs. origin/main.
|
||||||
|
## Rückgabe {status, local, remote, error} — status:
|
||||||
|
## "update_available" | "up_to_date" | "unreachable" | "error"
|
||||||
|
## "error" = kein Repo/git nicht da (Editor-spezifisches Problem)
|
||||||
|
## "unreachable"= Origin nicht erreichbar (offline, Tunnel weg)
|
||||||
|
static func check_update(git_exe: String, project_dir: String) -> Dictionary:
|
||||||
|
var local := head_sha(git_exe, project_dir)
|
||||||
|
if local.is_empty():
|
||||||
|
return {"status": "error", "local": "", "remote": "",
|
||||||
|
"error": "Kein Git-Repository (oder git nicht gefunden)."}
|
||||||
|
var remote := remote_sha(git_exe, project_dir, BRANCH)
|
||||||
|
if remote.is_empty():
|
||||||
|
return {"status": "unreachable", "local": local, "remote": "",
|
||||||
|
"error": "Origin nicht erreichbar (offline oder Tunnel weg)."}
|
||||||
|
if local == remote:
|
||||||
|
return {"status": "up_to_date", "local": local, "remote": remote, "error": ""}
|
||||||
|
return {"status": "update_available", "local": local, "remote": remote, "error": ""}
|
||||||
|
|
||||||
|
|
||||||
|
## Kurzer SHA für Anzeigen (7 Zeichen, wie git log --oneline).
|
||||||
|
static func short_sha(sha: String) -> String:
|
||||||
|
return sha.substr(0, 7)
|
||||||
|
|
||||||
|
|
||||||
|
## Banner-Text (pure, testbar).
|
||||||
|
static func banner_text(local_sha: String, remote_sha: String) -> String:
|
||||||
|
return "🔄 Update verfügbar: %s → %s" % [short_sha(local_sha), short_sha(remote_sha)]
|
||||||
|
|
||||||
|
|
||||||
|
## Update ausführen: git pull --ff-only (Klon zieht nur nach, dreht nichts
|
||||||
|
## um). Rückgabe {ok, output, errors}.
|
||||||
|
static func pull(git_exe: String, project_dir: String) -> Dictionary:
|
||||||
|
var r := git_run(git_exe, project_dir, PackedStringArray(["pull", "--ff-only"]))
|
||||||
|
var errors: Array = []
|
||||||
|
if not bool(r["ok"]):
|
||||||
|
errors.append("git pull --ff-only fehlgeschlagen (Exit-Code %s):" % str(r["code"]))
|
||||||
|
var text := str(r["output"]).strip_edges()
|
||||||
|
if not text.is_empty():
|
||||||
|
errors.append(text)
|
||||||
|
else:
|
||||||
|
errors.append("(keine Ausgabe — git nicht gefunden oder Verbindung sofort weg)")
|
||||||
|
return {"ok": bool(r["ok"]), "output": str(r["output"]), "errors": errors}
|
||||||
|
|
||||||
|
|
||||||
|
## Import-Lauf: <godot> --headless --path <projekt> --import. Muss NACH dem
|
||||||
|
## Pull laufen (neue class_name-Dateien in den Cache), FEHLSCHLAG = Update-
|
||||||
|
## Abbruch OHNE Neustart (Lektion 17.10.). Rückgabe {ok, errors, output}.
|
||||||
|
static func run_import(godot_exe: String, project_dir: String) -> Dictionary:
|
||||||
|
var args := PackedStringArray(["--headless", "--path", project_dir, "--import"])
|
||||||
|
var output: Array = []
|
||||||
|
var code := OS.execute(godot_exe, args, output, true)
|
||||||
|
var errors := import_errors(output)
|
||||||
|
if code != 0:
|
||||||
|
errors.append("Import-Lauf fehlgeschlagen (Exit-Code %s)" % str(code))
|
||||||
|
return {"ok": errors.is_empty(), "errors": errors, "output": _output_text(output)}
|
||||||
|
|
||||||
|
|
||||||
|
## Pure Prüfung (testbar): enthält die Import-Ausgabe Parse-/Script-Fehler?
|
||||||
|
## Exit-Code 0 allein beweist NICHT einen sauberen Import (Lektion 17.10.).
|
||||||
|
static func import_errors(output: Array) -> Array:
|
||||||
|
var errors: Array = []
|
||||||
|
for line in output:
|
||||||
|
var text := str(line)
|
||||||
|
if text.contains("Parse Error") or text.contains("SCRIPT ERROR"):
|
||||||
|
errors.append(text.strip_edges())
|
||||||
|
return errors
|
||||||
|
|
||||||
|
|
||||||
|
static func _output_text(output: Array) -> String:
|
||||||
|
var text := ""
|
||||||
|
for line in output:
|
||||||
|
text += str(line)
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
## Restart-Auflösung (plattformneutral, getestet):
|
||||||
|
## 1. cfg [update] restart_command gesetzt → mode "command", argv = an
|
||||||
|
## Leerzeichen gesplittet. (z. B. systemd-Unit-Kick, Batch-Skript — jede
|
||||||
|
## Plattform setzt hier ihren eigenen Befehl ein.)
|
||||||
|
## 2. Linux + systemd-User-Unit „map-editor“ existiert → Transient-Timer-
|
||||||
|
## Kick (systemd-run, 3 s): der Kick läuft in einem EIGENEN Scope und
|
||||||
|
## wird beim Stoppen des Editors (Unit-Neustart) nicht mitgekillt.
|
||||||
|
## 3. Sonst: mode "self" — der Editor startet sich selbst neu (gleiche
|
||||||
|
## Godot-Binary, --editor --path <projekt>) und beendet sich danach.
|
||||||
|
static func resolve_restart(cfg: ConfigFile = null) -> Dictionary:
|
||||||
|
if cfg != null:
|
||||||
|
var cmd := str(cfg.get_value("update", "restart_command", ""))
|
||||||
|
if not cmd.is_empty():
|
||||||
|
return {"mode": "command", "argv": PackedStringArray(cmd.split(" ", false))}
|
||||||
|
if OS.has_feature("linux"):
|
||||||
|
var probe: Array = []
|
||||||
|
var code := OS.execute("systemctl", PackedStringArray([
|
||||||
|
"--user", "list-unit-files", UNIT_NAME + ".service", "--no-legend"]),
|
||||||
|
probe, true)
|
||||||
|
var text := ""
|
||||||
|
for line in probe:
|
||||||
|
text += str(line)
|
||||||
|
if code == 0 and not text.strip_edges().is_empty():
|
||||||
|
return {"mode": "command", "argv": PackedStringArray([
|
||||||
|
"systemd-run", "--user", "--on-active=3s",
|
||||||
|
"systemctl", "--user", "restart", UNIT_NAME + ".service"])}
|
||||||
|
return {"mode": "self", "argv": PackedStringArray()}
|
||||||
|
|
||||||
|
|
||||||
|
## Argumente für den Selbst-Restart: dieselbe Godot-Binary öffnet denselben
|
||||||
|
## Projektordner im Editor-Modus („--editor“ ist nötig — ein nacktes
|
||||||
|
## „--path“ würde das Projekt als SPIEL starten, nicht als Editor).
|
||||||
|
static func self_restart_args(project_dir: String) -> PackedStringArray:
|
||||||
|
return PackedStringArray(["--editor", "--path", project_dir])
|
||||||
1
src/editor/updater.gd.uid
Normal file
1
src/editor/updater.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://nxdbipruop30
|
||||||
|
|
@ -57,6 +57,7 @@ static func text() -> String:
|
||||||
["Strg+Umschalt+S", "Map speichern unter …"],
|
["Strg+Umschalt+S", "Map speichern unter …"],
|
||||||
["Strg+O", "Map öffnen"],
|
["Strg+O", "Map öffnen"],
|
||||||
["Strg+N", "Neue Map"],
|
["Strg+N", "Neue Map"],
|
||||||
|
["Datei → 🔄 Updates prüfen", "Selbst-Update: prüft auf neue Version; Banner → „Jetzt updaten“ (lädt, importiert, startet neu)"],
|
||||||
]))
|
]))
|
||||||
out += _header("Werkzeuge & Tests")
|
out += _header("Werkzeuge & Tests")
|
||||||
out += _table(_rows([
|
out += _table(_rows([
|
||||||
|
|
|
||||||
193
src/ui/main.gd
193
src/ui/main.gd
|
|
@ -132,6 +132,16 @@ var help_overlay_title: Label
|
||||||
var help_overlay_text: RichTextLabel
|
var help_overlay_text: RichTextLabel
|
||||||
var tool_label: Label # permanenter Werkzeugmodus + Flug-Hint
|
var tool_label: Label # permanenter Werkzeugmodus + Flug-Hint
|
||||||
|
|
||||||
|
# Editor-Selbstupdate (Karte t_d95f087a): Start-Check + Banner + „Jetzt updaten“
|
||||||
|
var update_banner: PanelContainer # Leiste „🔄 Update verfügbar …“ (versteckt)
|
||||||
|
var update_label: Label
|
||||||
|
var update_btn: Button # „Jetzt updaten“ im Banner
|
||||||
|
var check_thread: Thread # Update-Check im Hintergrund (Start/Menü)
|
||||||
|
var update_thread: Thread # Update-Ausführung (pull + Import-Lauf)
|
||||||
|
var _update_check_busy := false
|
||||||
|
var _update_running := false
|
||||||
|
var _update_result: Dictionary = {} # letzter Check (Status/Banner-Nachfrage)
|
||||||
|
|
||||||
# Dialoge
|
# Dialoge
|
||||||
var new_dialog: AcceptDialog
|
var new_dialog: AcceptDialog
|
||||||
var open_dialog: FileDialog
|
var open_dialog: FileDialog
|
||||||
|
|
@ -153,6 +163,10 @@ func _ready() -> void:
|
||||||
_update_tool_label()
|
_update_tool_label()
|
||||||
_start_with_new_map()
|
_start_with_new_map()
|
||||||
_maybe_start_demo_mode()
|
_maybe_start_demo_mode()
|
||||||
|
# Selbst-Update (Karte t_d95f087a): STILLER Start-Check im Hintergrund —
|
||||||
|
# nur bei „Update verfügbar“ erscheint das Banner; offline/Tunnel weg
|
||||||
|
# bleibt alles ruhig (kein Banner, keine Meldung).
|
||||||
|
_startup_update_check()
|
||||||
|
|
||||||
|
|
||||||
func _load_manifest() -> Dictionary:
|
func _load_manifest() -> Dictionary:
|
||||||
|
|
@ -181,6 +195,7 @@ func _build_ui() -> void:
|
||||||
top_h.add_theme_constant_override("separation", 10)
|
top_h.add_theme_constant_override("separation", 10)
|
||||||
top.add_child(top_h)
|
top.add_child(top_h)
|
||||||
vbox.add_child(top)
|
vbox.add_child(top)
|
||||||
|
_update_banner_build(vbox)
|
||||||
|
|
||||||
var file_menu := MenuButton.new()
|
var file_menu := MenuButton.new()
|
||||||
file_menu.text = "Datei"
|
file_menu.text = "Datei"
|
||||||
|
|
@ -196,6 +211,7 @@ func _build_ui() -> void:
|
||||||
popup.add_separator()
|
popup.add_separator()
|
||||||
popup.add_item("▶ Karte testen", 5)
|
popup.add_item("▶ Karte testen", 5)
|
||||||
popup.add_separator()
|
popup.add_separator()
|
||||||
|
popup.add_item("🔄 Updates prüfen", 7)
|
||||||
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)
|
||||||
|
|
@ -993,6 +1009,8 @@ func _on_menu_action(id: int) -> void:
|
||||||
_on_test_run()
|
_on_test_run()
|
||||||
6: # Kartengröße ändern (R3, t_c863b93b)
|
6: # Kartengröße ändern (R3, t_c863b93b)
|
||||||
_open_resize_dialog()
|
_open_resize_dialog()
|
||||||
|
7: # Updates prüfen (Selbst-Update, t_d95f087a)
|
||||||
|
_on_update_check_menu()
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------- Kartengröße ändern (R3) --
|
# ------------------------------------------------- Kartengröße ändern (R3) --
|
||||||
|
|
@ -1059,6 +1077,181 @@ func _replace_doc_in_place(result: Dictionary) -> void:
|
||||||
doc[key] = source
|
doc[key] = source
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------- Selbst-Update (t_d95f087a) ----
|
||||||
|
|
||||||
|
## Banner „🔄 Update verfügbar“ (unaufdringliche Leiste unter der Top-Bar) —
|
||||||
|
## nur sichtbar, wenn ein Check einen Update findet. Offline bleibt alles still.
|
||||||
|
func _update_banner_build(parent: Container) -> void:
|
||||||
|
update_banner = PanelContainer.new()
|
||||||
|
var style := StyleBoxFlat.new()
|
||||||
|
style.bg_color = Color(0.36, 0.25, 0.06, 0.97) # gedämpftes Bernstein
|
||||||
|
style.border_color = Color(0.95, 0.72, 0.15)
|
||||||
|
style.set_border_width_all(2)
|
||||||
|
style.set_content_margin_all(8)
|
||||||
|
style.set_corner_radius_all(6)
|
||||||
|
update_banner.add_theme_stylebox_override("panel", style)
|
||||||
|
var h := HBoxContainer.new()
|
||||||
|
h.add_theme_constant_override("separation", 14)
|
||||||
|
update_banner.add_child(h)
|
||||||
|
update_label = Label.new()
|
||||||
|
update_label.add_theme_font_size_override("font_size", 17)
|
||||||
|
h.add_child(update_label)
|
||||||
|
var spacer := Control.new()
|
||||||
|
spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
h.add_child(spacer)
|
||||||
|
update_btn = Button.new()
|
||||||
|
update_btn.text = "🔄 Jetzt updaten"
|
||||||
|
update_btn.custom_minimum_size = Vector2(190, 40)
|
||||||
|
update_btn.tooltip_text = "Lädt die neue Version (git pull), importiert sie und startet den Editor neu. Bei ungespeicherten Änderungen fragt der Editor vorher."
|
||||||
|
update_btn.pressed.connect(_on_update_now)
|
||||||
|
h.add_child(update_btn)
|
||||||
|
update_banner.visible = false
|
||||||
|
parent.add_child(update_banner)
|
||||||
|
|
||||||
|
|
||||||
|
## Still Start-Check (nur Banner bei „Update verfügbar“ — offline: nichts).
|
||||||
|
func _startup_update_check() -> void:
|
||||||
|
_updater_check_async(false)
|
||||||
|
|
||||||
|
|
||||||
|
## Datei-Menü „🔄 Updates prüfen“ (ID 7): gleicher Check mit Ergebnis-Meldung.
|
||||||
|
func _on_update_check_menu() -> void:
|
||||||
|
if _update_check_busy:
|
||||||
|
_set_status("Update-Check läuft schon…")
|
||||||
|
return
|
||||||
|
_set_status("Prüfe auf Updates…")
|
||||||
|
_updater_check_async(true)
|
||||||
|
|
||||||
|
|
||||||
|
func _updater_check_async(report: bool) -> void:
|
||||||
|
_update_check_busy = true
|
||||||
|
check_thread = Thread.new()
|
||||||
|
check_thread.start(_updater_check_thread.bind(report))
|
||||||
|
|
||||||
|
|
||||||
|
func _updater_check_thread(report: bool) -> void:
|
||||||
|
var result := EditorUpdater.check_update(
|
||||||
|
EditorUpdater.git_exe(TestRun.load_cfg()),
|
||||||
|
ProjectSettings.globalize_path("res://"))
|
||||||
|
_update_result = result
|
||||||
|
call_deferred("_updater_check_done", result, report)
|
||||||
|
|
||||||
|
|
||||||
|
func _updater_check_done(result: Dictionary, report: bool) -> void:
|
||||||
|
_update_check_busy = false
|
||||||
|
_join(check_thread)
|
||||||
|
check_thread = null
|
||||||
|
match str(result.get("status")):
|
||||||
|
"update_available":
|
||||||
|
_show_update_banner(str(result["local"]), str(result["remote"]))
|
||||||
|
if report:
|
||||||
|
_set_status("🔄 Update verfügbar: %s → %s — im Banner „Jetzt updaten“ klicken." % [
|
||||||
|
EditorUpdater.short_sha(str(result["local"])),
|
||||||
|
EditorUpdater.short_sha(str(result["remote"]))])
|
||||||
|
"up_to_date":
|
||||||
|
if report:
|
||||||
|
_set_status("Du bist auf dem neusten Stand (%s)." % EditorUpdater.short_sha(str(result["local"])))
|
||||||
|
"unreachable":
|
||||||
|
if report:
|
||||||
|
_set_status("Update-Check nicht erreichbar (offline oder Tunnel weg) — später nochmal probieren.", true)
|
||||||
|
_:
|
||||||
|
if report:
|
||||||
|
_set_status("Update-Check fehlgeschlagen: %s" % str(result.get("error", "")), true)
|
||||||
|
|
||||||
|
|
||||||
|
func _show_update_banner(local_sha: String, remote_sha: String) -> void:
|
||||||
|
update_label.text = EditorUpdater.banner_text(local_sha, remote_sha)
|
||||||
|
update_banner.visible = true
|
||||||
|
|
||||||
|
|
||||||
|
## Thread sauber einsammeln (Ergebnis kam per call_deferred — der Thread ist
|
||||||
|
## längst fertig, join ist nur Ordnung).
|
||||||
|
func _join(thread: Thread) -> void:
|
||||||
|
if thread != null and thread.is_started():
|
||||||
|
thread.wait_to_finish()
|
||||||
|
|
||||||
|
|
||||||
|
## Banner-Knopf „Jetzt updaten“: Dirty-Schutz wie Strg+O/Strg+N (derselbe
|
||||||
|
## deutsche Dialog — „Abbrechen“ bricht das Update ab), dann git pull +
|
||||||
|
## Import-Lauf + Neustart. Import-Fehler = Abbruch mit Meldung, KEIN Neustart
|
||||||
|
## (Lektion 17.10.: sonst sieht Sly einen Editor ohne Map-Ansicht).
|
||||||
|
func _on_update_now() -> void:
|
||||||
|
if _update_running:
|
||||||
|
_set_status("Ein Update läuft schon…")
|
||||||
|
return
|
||||||
|
_request_lossy(_start_update)
|
||||||
|
|
||||||
|
|
||||||
|
func _start_update() -> void:
|
||||||
|
_update_running = true
|
||||||
|
update_btn.disabled = true
|
||||||
|
_set_status("Update läuft — neue Version wird geladen (git pull)…")
|
||||||
|
update_thread = Thread.new()
|
||||||
|
update_thread.start(_updater_run_thread)
|
||||||
|
|
||||||
|
|
||||||
|
func _updater_run_thread() -> void:
|
||||||
|
var project_dir := ProjectSettings.globalize_path("res://")
|
||||||
|
var cfg := TestRun.load_cfg()
|
||||||
|
var pull := EditorUpdater.pull(EditorUpdater.git_exe(cfg), project_dir)
|
||||||
|
if not bool(pull["ok"]):
|
||||||
|
call_deferred("_update_finished", false, "Update abgebrochen — git pull fehlgeschlagen:", pull["errors"])
|
||||||
|
return
|
||||||
|
call_deferred("_set_status", "Update geladen — Version wird importiert (Godot-Import-Lauf)…")
|
||||||
|
var godot := TestRun.find_godot(cfg)
|
||||||
|
if godot.is_empty():
|
||||||
|
call_deferred("_update_finished", false, "Update abgebrochen — kein Godot-Programm für den Import-Lauf:", [
|
||||||
|
"Der Editor läuft nicht über ein Godot-Binary und es ist kein godot_path gesetzt.",
|
||||||
|
"Trage in scopedvr_editor.cfg (Godot-App-Daten) ein:",
|
||||||
|
" [test]",
|
||||||
|
" godot_path=\"/pfad/zu/godot\"",
|
||||||
|
])
|
||||||
|
return
|
||||||
|
var imp := EditorUpdater.run_import(godot, project_dir)
|
||||||
|
if not bool(imp["ok"]):
|
||||||
|
call_deferred("_update_finished", false, "Update ABGEBROCHEN — der Import ist fehlgeschlagen, es wird NICHT neu gestartet:", imp["errors"])
|
||||||
|
return
|
||||||
|
call_deferred("_update_finished", true, "", [])
|
||||||
|
|
||||||
|
|
||||||
|
func _update_finished(ok: bool, title: String, details: Array) -> void:
|
||||||
|
_update_running = false
|
||||||
|
update_btn.disabled = false
|
||||||
|
_join(update_thread)
|
||||||
|
update_thread = null
|
||||||
|
if not ok:
|
||||||
|
update_banner.visible = true
|
||||||
|
_show_errors(title, details)
|
||||||
|
_set_status("Update fehlgeschlagen — der Editor bleibt unverändert (es ist nichts kaputt gemacht).", true)
|
||||||
|
return
|
||||||
|
var restart := EditorUpdater.resolve_restart(TestRun.load_cfg())
|
||||||
|
var project_dir := ProjectSettings.globalize_path("res://")
|
||||||
|
var pid := -1
|
||||||
|
if str(restart["mode"]) == "command":
|
||||||
|
var argv: PackedStringArray = restart["argv"]
|
||||||
|
pid = OS.create_process(str(argv[0]), argv.slice(1))
|
||||||
|
else:
|
||||||
|
var godot := TestRun.find_godot(TestRun.load_cfg())
|
||||||
|
if godot.is_empty():
|
||||||
|
_show_errors("Update installiert — Neustart nicht möglich:", [
|
||||||
|
"Der neue Stand ist geladen, aber das Godot-Programm für den Neustart ist unbekannt.",
|
||||||
|
"Editor einfach einmal selbst neu starten.",
|
||||||
|
])
|
||||||
|
_set_status("Update installiert — bitte den Editor einmal selbst neu starten.")
|
||||||
|
return
|
||||||
|
pid = OS.create_process(godot, EditorUpdater.self_restart_args(project_dir))
|
||||||
|
if pid <= 0:
|
||||||
|
_show_errors("Update installiert — Neustart fehlgeschlagen:", [
|
||||||
|
"Der neue Stand ist geladen, aber der Neustart wurde vom System abgelehnt.",
|
||||||
|
"Editor einfach einmal selbst neu starten.",
|
||||||
|
])
|
||||||
|
_set_status("Update installiert — bitte den Editor einmal selbst neu starten.")
|
||||||
|
return
|
||||||
|
_set_status("Update installiert (%s) — Editor startet neu…" % EditorUpdater.short_sha(str(_update_result.get("remote", ""))))
|
||||||
|
await get_tree().create_timer(1.0).timeout
|
||||||
|
get_tree().quit()
|
||||||
|
|
||||||
|
|
||||||
## „▶ Karte testen" (Karte t_b84af40f): aktuelle Map (auch ungespeichert!)
|
## „▶ Karte testen" (Karte t_b84af40f): aktuelle Map (auch ungespeichert!)
|
||||||
## nach Temp exportieren und den scopedvr-client als EIGENES Fenster starten.
|
## nach Temp exportieren und den scopedvr-client als EIGENES Fenster starten.
|
||||||
## Der Editor bleibt offen; das Spiel beendet sich selbst sauber (ESC).
|
## Der Editor bleibt offen; das Spiel beendet sich selbst sauber (ESC).
|
||||||
|
|
|
||||||
199
tests/test_updater.gd
Normal file
199
tests/test_updater.gd
Normal file
|
|
@ -0,0 +1,199 @@
|
||||||
|
extends SceneTree
|
||||||
|
## Editor-Tests (headless): Selbst-Update — Karte t_d95f087a.
|
||||||
|
## Ausführen: godot --headless --path . --script res://tests/test_updater.gd
|
||||||
|
## Exit 0 = alles grün. Läuft OHNE UI (EditorUpdater + echte git-Repos in
|
||||||
|
## user://test_updater):
|
||||||
|
## 1. Check-Funktion mit ECHTEN Repos: gleich (up_to_date), divergiert
|
||||||
|
## (update_available), nicht erreichbar (unreachable) — kein Mock, echte
|
||||||
|
## git-Prozesse gegen ein Bare-Repo / gegen einen abgelehnten Port.
|
||||||
|
## 2. Banner-Logik (Text, kurze SHAs).
|
||||||
|
## 3. Import-Abbruch-Pfad: faked Import-Fehler (Exit-Code != 0 UND Parse-
|
||||||
|
## -Error-Schreiber mit Exit 0) → kein ok, kein Restart (Restart wird nur
|
||||||
|
## NACH ok angestoßen — hier wird bewusst NUR der ok-Pfad geprüft).
|
||||||
|
## 4. Restart-Auflösung: cfg-Kommando gewinnt, Default ist self/command.
|
||||||
|
|
||||||
|
var failures := 0
|
||||||
|
var checks := 0
|
||||||
|
|
||||||
|
const TEST_DIR_PREFIX := "user://test_updater"
|
||||||
|
var base := ""
|
||||||
|
var work := ""
|
||||||
|
|
||||||
|
|
||||||
|
func _init() -> void:
|
||||||
|
_cleanup_old()
|
||||||
|
base = ProjectSettings.globalize_path(
|
||||||
|
TEST_DIR_PREFIX + "_" + str(Time.get_ticks_msec()))
|
||||||
|
DirAccess.make_dir_recursive_absolute(base)
|
||||||
|
_run_tests()
|
||||||
|
_cleanup_dir(base) # best effort — git-Pack-Dateien können read-only sein
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
## Best effort: alte Läufe aufräumen (git macht Pack-Dateien read-only —
|
||||||
|
## dann bleibt der Ordner liegen; der LAUF nutzt deshalb IMMER ein frisches,
|
||||||
|
## Zeit-stempel-basiertes Verzeichnis und hängt nie an Überbleibseln).
|
||||||
|
func _cleanup_old() -> void:
|
||||||
|
var root := DirAccess.open(ProjectSettings.globalize_path("user://"))
|
||||||
|
if root == null:
|
||||||
|
return
|
||||||
|
for name in root.get_directories():
|
||||||
|
if name.begins_with("test_updater"):
|
||||||
|
_cleanup_dir(ProjectSettings.globalize_path("user://").path_join(name))
|
||||||
|
|
||||||
|
|
||||||
|
func _cleanup_dir(base_dir: String) -> void:
|
||||||
|
if not DirAccess.dir_exists_absolute(base_dir):
|
||||||
|
return
|
||||||
|
var d := DirAccess.open(base_dir)
|
||||||
|
if d == null:
|
||||||
|
return
|
||||||
|
for file in d.get_files():
|
||||||
|
DirAccess.remove_absolute(base_dir.path_join(file))
|
||||||
|
for sub in d.get_directories():
|
||||||
|
_cleanup_dir(base_dir.path_join(sub))
|
||||||
|
DirAccess.remove_absolute(base_dir)
|
||||||
|
|
||||||
|
|
||||||
|
func _git(dir: String, args: Array) -> Dictionary:
|
||||||
|
var packed := PackedStringArray()
|
||||||
|
for a in args:
|
||||||
|
packed.append(str(a))
|
||||||
|
return EditorUpdater.git_run("git", dir, packed)
|
||||||
|
|
||||||
|
|
||||||
|
func _write_file(path: String, text: String) -> void:
|
||||||
|
var f := FileAccess.open(path, FileAccess.WRITE)
|
||||||
|
f.store_string(text)
|
||||||
|
f.close()
|
||||||
|
|
||||||
|
|
||||||
|
func _run_tests() -> void:
|
||||||
|
var git := "git"
|
||||||
|
|
||||||
|
# --- echte Repos aufbauen: origin.git (bare) + work (Klon mit main) ---
|
||||||
|
var origin := base.path_join("origin.git")
|
||||||
|
_check(bool(_git(base, ["init", "--bare", "--initial-branch=main", origin])["ok"]),
|
||||||
|
"Setup: Bare-Repo origin.git angelegt")
|
||||||
|
work = base.path_join("work")
|
||||||
|
_check(bool(_git(base, ["init", "--initial-branch=main", work])["ok"]),
|
||||||
|
"Setup: Arbeits-Klon angelegt")
|
||||||
|
_check(bool(_git(work, ["remote", "add", "origin", origin])["ok"]),
|
||||||
|
"Setup: remote origin gesetzt")
|
||||||
|
_git(work, ["config", "user.email", "test@example.local"])
|
||||||
|
_git(work, ["config", "user.name", "Update-Test"])
|
||||||
|
_write_file(work.path_join("a.txt"), "Version 1\n")
|
||||||
|
_check(bool(_git(work, ["add", "a.txt"])["ok"]), "Setup: Datei gestagt")
|
||||||
|
_check(bool(_git(work, ["commit", "-m", "v1", "a.txt"])["ok"]), "Setup: Commit v1")
|
||||||
|
_check(bool(_git(work, ["push", "-u", "origin", "main"])["ok"]),
|
||||||
|
"Setup: main nach origin gepusht")
|
||||||
|
|
||||||
|
var local_sha := EditorUpdater.head_sha(git, work)
|
||||||
|
_check(local_sha.length() == 40, "head_sha: 40-Zeichen-SHA gelesen")
|
||||||
|
|
||||||
|
print("== Check: auf dem neusten Stand ==")
|
||||||
|
var up := EditorUpdater.check_update(git, work)
|
||||||
|
_check(str(up["status"]) == "up_to_date", "gleiches SHA → up_to_date")
|
||||||
|
_check(str(up["local"]) == local_sha and str(up["remote"]) == local_sha,
|
||||||
|
"beide SHAs befüllt und identisch")
|
||||||
|
|
||||||
|
print("== Check: Update verfügbar ==")
|
||||||
|
# Fremder Klon pusht einen neuen Commit nach origin — work bleibt hinten.
|
||||||
|
var other := base.path_join("other")
|
||||||
|
_check(bool(_git(base, ["clone", "-q", origin, other])["ok"]), "Setup: zweiter Klon")
|
||||||
|
_git(other, ["config", "user.email", "test@example.local"])
|
||||||
|
_git(other, ["config", "user.name", "Update-Test"])
|
||||||
|
_write_file(other.path_join("b.txt"), "neue Version\n")
|
||||||
|
_git(other, ["add", "b.txt"])
|
||||||
|
_git(other, ["commit", "-m", "v2", "b.txt"])
|
||||||
|
_check(bool(_git(other, ["push", "origin", "main"])["ok"]), "Setup: v2 gepusht")
|
||||||
|
var avail := EditorUpdater.check_update(git, work)
|
||||||
|
_check(str(avail["status"]) == "update_available", "origin voraus → update_available")
|
||||||
|
_check(str(avail["local"]) == local_sha, "lokaler SHA bleibt (Pull ist NICHT Teil des Checks)")
|
||||||
|
_check(str(avail["remote"]) != local_sha, "remote SHA ist der neue")
|
||||||
|
|
||||||
|
print("== Banner-Logik ==")
|
||||||
|
var text := EditorUpdater.banner_text(local_sha, str(avail["remote"]))
|
||||||
|
_check(text.contains("Update verfügbar"), "Banner-Text enthält „Update verfügbar“")
|
||||||
|
_check(text.contains(local_sha.substr(0, 7)), "Banner-Text enthält kurzen ALT-SHA")
|
||||||
|
_check(text.contains(EditorUpdater.short_sha(str(avail["remote"]))), "Banner-Text enthält kurzen NEU-SHA")
|
||||||
|
_check(EditorUpdater.short_sha(local_sha).length() == 7, "short_sha = 7 Zeichen")
|
||||||
|
|
||||||
|
print("== Check: nicht erreichbar ==")
|
||||||
|
# Port 1 (bewusst zu) → Verbindung sofort abgelehnt = der Offline-Pfad.
|
||||||
|
_git(work, ["remote", "set-url", "origin", "http://127.0.0.1:1/nicht-da.git"])
|
||||||
|
var dead := EditorUpdater.check_update(git, work)
|
||||||
|
_check(str(dead["status"]) == "unreachable", "Origin tot → unreachable (still, kein Fehler)")
|
||||||
|
_check(str(dead["local"]) == local_sha, "lokaler SHA trotzdem bekannt")
|
||||||
|
# Falsches Binary → error (kein Repo-Check möglich)
|
||||||
|
var bad := EditorUpdater.check_update("gibt-es-nicht-xyz", work)
|
||||||
|
_check(str(bad["status"]) == "error", "git nicht gefunden → error (nicht unreachable)")
|
||||||
|
|
||||||
|
print("== Pull ==")
|
||||||
|
_git(work, ["remote", "set-url", "origin", origin])
|
||||||
|
var pulled := EditorUpdater.pull(git, work)
|
||||||
|
_check(bool(pulled["ok"]), "pull --ff-only erfolgreich (v2 wird geladen)")
|
||||||
|
_check(EditorUpdater.head_sha(git, work) == str(avail["remote"]), "nach Pull ist work auf v2")
|
||||||
|
_git(work, ["remote", "set-url", "origin", "http://127.0.0.1:1/nicht-da.git"])
|
||||||
|
var dead_pull := EditorUpdater.pull(git, work)
|
||||||
|
_check(not bool(dead_pull["ok"]), "pull gegen toten Origin schlägt fehl")
|
||||||
|
_check((dead_pull["errors"] as Array).size() > 0, "Pull-Fehlerliste fürs Dialog-Fenster")
|
||||||
|
_git(work, ["remote", "set-url", "origin", origin])
|
||||||
|
|
||||||
|
print("== Import-Abbruch-Pfad ==")
|
||||||
|
_check(EditorUpdater.import_errors([]).is_empty(), "leere Import-Ausgabe = keine Fehler")
|
||||||
|
_check(EditorUpdater.import_errors(["Eine normale Zeile"]).is_empty(), "normale Ausgabe = keine Fehler")
|
||||||
|
var errs := EditorUpdater.import_errors(["Parse Error: Identifier not declared in the current scope."])
|
||||||
|
_check(errs.size() == 1, "Parse Error wird erkannt (Exit-Code-0-Falle, Lektion 17.10.)")
|
||||||
|
errs = EditorUpdater.import_errors(["SCRIPT ERROR: blub", "und weiter"])
|
||||||
|
_check(errs.size() == 1, "SCRIPT ERROR wird erkannt")
|
||||||
|
# Echter Run mit Exit-Code != 0 (Benutzung von "false": endet mit 1).
|
||||||
|
if FileAccess.file_exists("/bin/false"):
|
||||||
|
var failed := EditorUpdater.run_import("/bin/false", work)
|
||||||
|
_check(not bool(failed["ok"]), "Import mit Exit-Code 1 = NICHT ok (kein Restart)")
|
||||||
|
_check((failed["errors"] as Array).size() > 0, "Import-Fehlerliste befüllt")
|
||||||
|
# Echter Run, Exit-Code 0, ABER Parse-Error in der Ausgabe (die 17.10.-Falle):
|
||||||
|
if FileAccess.file_exists("/bin/sh"):
|
||||||
|
var fake := base.path_join("fake_godot.sh")
|
||||||
|
_write_file(fake, "#!/bin/sh\necho \"Parse Error: Identifier not declared.\"\nexit 0\n")
|
||||||
|
var code := OS.execute("chmod", PackedStringArray(["+x", fake]))
|
||||||
|
if code == 0:
|
||||||
|
var trap := EditorUpdater.run_import(fake, work)
|
||||||
|
_check(not bool(trap["ok"]), "Import „grün“ MIT Parse-Error = NICHT ok (kein Restart!)")
|
||||||
|
|
||||||
|
print("== Restart-Auflösung ==")
|
||||||
|
var cfg := ConfigFile.new()
|
||||||
|
cfg.set_value("update", "restart_command", "echo hallo welt")
|
||||||
|
var resolved := EditorUpdater.resolve_restart(cfg)
|
||||||
|
_check(str(resolved["mode"]) == "command", "cfg-Kommando gewinnt (mode command)")
|
||||||
|
var argv: PackedStringArray = resolved["argv"]
|
||||||
|
var want := ["echo", "hallo", "welt"]
|
||||||
|
var ok := argv.size() == want.size()
|
||||||
|
if ok:
|
||||||
|
for i in want.size():
|
||||||
|
if str(argv[i]) != want[i]:
|
||||||
|
ok = false
|
||||||
|
_check(ok, "Kommando sauber an Leerzeichen gesplittet")
|
||||||
|
var resolved2 := EditorUpdater.resolve_restart(ConfigFile.new())
|
||||||
|
_check(str(resolved2["mode"]) == "self" or str(resolved2["mode"]) == "command",
|
||||||
|
"ohne cfg: plausibler Default (self ODER systemd-Unit erkannt)")
|
||||||
|
if str(resolved2["mode"]) == "self":
|
||||||
|
_check((resolved2["argv"] as PackedStringArray).is_empty(),
|
||||||
|
"mode self: UI baut die Selbst-Restart-Argumente selbst")
|
||||||
|
var self_args := EditorUpdater.self_restart_args("/pfad/zum/klon")
|
||||||
|
_check(str(self_args[0]) == "--editor" and str(self_args[2]) == "/pfad/zum/klon",
|
||||||
|
"Selbst-Restart: --editor --path <klon> (sonst startet das Projekt als SPIEL)")
|
||||||
1
tests/test_updater.gd.uid
Normal file
1
tests/test_updater.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://cfxxi8pjnihka
|
||||||
|
|
@ -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_shortcuts test_editor_svmap test_terrain test_library test_glb_view test_spawns test_testrun test_bauen test_resize_dialog test_gui_smoke test_rmb_vorrang; do
|
for t in test_ux test_shortcuts test_editor_svmap test_terrain test_library test_glb_view test_spawns test_testrun test_bauen test_resize_dialog test_updater test_gui_smoke test_rmb_vorrang; 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)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue