227 lines
11 KiB
GDScript
227 lines
11 KiB
GDScript
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("== Credential-Guard (Karte t_1457e935): git wird NIE interaktiv ==")
|
|
_check(EditorUpdater.looks_like_auth_error(
|
|
"fatal: could not read Username for 'http://127.0.0.1:13020': No such device or address"),
|
|
"auth-Erkennung: could-not-read-Username")
|
|
_check(EditorUpdater.looks_like_auth_error(
|
|
"remote: Unauthorized\nfatal: Authentication failed for 'http://x/'"),
|
|
"auth-Erkennung: Authentication failed / Unauthorized")
|
|
_check(EditorUpdater.looks_like_auth_error("fatal: could not read Username; terminal prompts disabled"),
|
|
"auth-Erkennung: terminal prompts disabled")
|
|
_check(not EditorUpdater.looks_like_auth_error(
|
|
"fatal: unable to access 'http://127.0.0.1:1/': Connection refused"),
|
|
"auth-Erkennung: Connection refused ist KEIN Auth-Fehler")
|
|
var spawn := EditorUpdater.spawn_argv("git", "/projekt", PackedStringArray(["ls-remote", "origin"]))
|
|
if OS.has_feature("linux"):
|
|
_check(str(spawn[0]) == "/usr/bin/env", "argv: POSIX-Spawn über /usr/bin/env")
|
|
_check(spawn.has("GIT_TERMINAL_PROMPT=0"), "argv: GIT_TERMINAL_PROMPT=0 (kein Terminal-Prompt)")
|
|
_check(spawn.has("GIT_ASKPASS=/bin/true"), "argv: GIT_ASKPASS=/bin/true (überstimmt geerbtes SSH_ASKPASS)")
|
|
_check(spawn.has("-c") and spawn.has("credential.helper=") and spawn.has("credential.helper=store"),
|
|
"argv: Helper-Reset + ausschließlich store")
|
|
_check(spawn.has("core.askPass=/bin/true"), "argv: core.askPass-Fallback gesetzt")
|
|
var hat_home := false
|
|
for eintrag in spawn:
|
|
if str(eintrag).begins_with("HOME="):
|
|
hat_home = true
|
|
_check(hat_home, "argv: HOME aus der Umgebung mitgegeben (kein Hardcode)")
|
|
_check(str(spawn[spawn.size() - 2]) == "ls-remote" and str(spawn[spawn.size() - 1]) == "origin",
|
|
"argv: originale git-Argumente bleiben hinten")
|
|
|
|
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)")
|