scopedvr-map-editor/tests/test_model_sources.gd
scopedvr-operator ab173ac8aa T2 Ueberall installierbar (Operator-Push): Modelle aus mitgeliefertem models/-Ordner
Worker hatte die Arbeit fertig, aber NICHT committet/gepusht (3. Fall heute) —
aus dem Karten-Arbeitsordner gerettet und hier gepusht.

- GlbMeshStore.quellen(): Reihenfolge EINE Stelle — Setting -> Sibling-Repos ->
  mitgelieferter Ordner models/ direkt neben der App (dedupliziert, typisiert)
- Klartext-Meldung FEHLT_TEXT statt stillem Platzhalter; Notfall-Modus: Terrain
  und Bauteile laufen ohne GLBs
- neu: tools/sync_models.sh (fuellt models/, --check), utils/modelle_status.gd
  (Autoload ModelleStatus), tools/probe_gen_map.gd
- neu: README_INSTALL.md (85 Zeilen, Schritt fuer Schritt + §5 Lizenzhinweis:
  ITHappy-Rohdaten NICHT weitergeben, App fuer Slys eigene Rechner)
- export_presets.cfg (eigenstaendiger Linux-/Windows-Export), .gitignore:
  models/ + export/ bleiben draussen (Lizenz + Groesse)
- Tests: test_model_sources (26/27; die rote ist umgebungsabhaengig:
  'lager_ist_leer' erwartet fehlende Sibling-Repos)

Belegt: Import 0 Fehler, gui_smoke 92, ui_layout 71, playtest_spawn 56.
2026-10-20 09:43:05 +00:00

229 lines
10 KiB
GDScript

extends SceneTree
## Editor-Tests (headless): Karte t_12cf94fe — Modell-Quellen-Reihenfolge,
## mitgelieferter models/-Ordner, Klartext-Banner, Notfall-Modus.
## Ausführen: godot --headless --path . --script res://tests/test_model_sources.gd
## Exit 0 = alles grün. Prüft:
## 1. quellen(): Reihenfolge Einstellungs-Pfad → Sibling-Defaults → models/,
## Duplikate gefiltert, ungültiger Settings-Wert fällt auf die Vorgabe.
## 2. probe_source_fuer(oid): erster EXISTIERENDER Kandidat gewinnt, ohne
## Parse (billiger FileAccess-Check), leerer String ohne Fund.
## 3. fehl_text(): Klartext-Meldung enthält models/-Pfad + sync_models.sh.
## 4. lager_ist_leer(): true ohne GLB-Quellen, false sobald eine Quelle eine
## GLB hat (auch rekursiv verschachtelt).
## 5. Notfall-Modus: fehlende OID → fehlt()=true, mesh_fuer()=null (Platzhalter
## ist Sache des view_builders — hier nur der Store-Vertrag).
## 6. models_pfad(): endet auf /models, neben der App (res://-Wurzel).
##
## Die Tests arbeiten auf TEMP-Ordnern und ProjectSettings im Speicher —
## sie berühren KEINE echten Dateien im Repo.
var failures := 0
var checks := 0
func _init() -> void:
# Vorgabe-Zustand: keine Einstellungs-Quellen (Sly-PCs ohne Siblings sind
# der Normalfall dieser Karte).
if ProjectSettings.has_setting(GlbMeshStore.QUELLEN_SETTING):
ProjectSettings.set_setting(GlbMeshStore.QUELLEN_SETTING, PackedStringArray())
GlbMeshStore.cache_leeren()
_test_quellen_reihenfolge()
_test_probe_source()
_test_fehl_text()
_test_lager_ist_leer()
_test_notfall_modus()
_test_models_pfad()
if failures == 0:
print("ALL OK: %d checks" % checks)
quit(0)
else:
print("FAILED: %d/%d checks" % [failures, checks])
quit(1)
func _ok(bedingung: bool, name: String, detail := "") -> void:
checks += 1
if bedingung:
print(" ok: %s" % name)
else:
failures += 1
print(" FEHLER: %s%s" % [name, " — " + detail if detail != "" else ""])
func _temp_ordner(na: String) -> String:
var p := OS.get_environment("TMPDIR")
if p.is_empty():
p = "/tmp"
var out := p.path_join("t12cf94fe_" + na + "_" + str(randi() % 100000))
DirAccess.make_dir_recursive_absolute(out)
return out
## 1. Reihenfolge: (1) Settings-Pfad, (2) Defaults, (3) models/ — Duplikate raus.
func _test_quellen_reihenfolge() -> void:
# Ohne Setting: Defaults + models/ am Ende, keine Duplikate.
ProjectSettings.set_setting(GlbMeshStore.QUELLEN_SETTING, PackedStringArray())
var q: Array = GlbMeshStore.quellen()
_ok(q.size() == 3, "quellen: ohne Setting = 2 Defaults + models/", str(q))
_ok(q[0] == GlbMeshStore.DEFAULT_QUELLEN[0] and q[1] == GlbMeshStore.DEFAULT_QUELLEN[1],
"quellen: Defaults zuerst (bisheriges Verhalten)", str(q))
_ok(q[2] == GlbMeshStore.MODELS_DIR, "quellen: models/ zuletzt", str(q))
# Mit Setting: der gesetzte Pfad gewinnt (erster Treffer).
ProjectSettings.set_setting(GlbMeshStore.QUELLEN_SETTING,
PackedStringArray(["/eigener/pfad", q[0] as String]))
var q2: Array = GlbMeshStore.quellen()
_ok(q2[0] == "/eigener/pfad", "quellen: gesetzter Pfad gewinnt", str(q2))
_ok(q2.find(GlbMeshStore.MODELS_DIR) == q2.size() - 1,
"quellen: models/ bleibt am Ende", str(q2))
_ok(q2.size() == 4, "quellen: Duplikat Default[0] gefiltert", str(q2))
# Leere/whitespace Einträge fliegen raus.
ProjectSettings.set_setting(GlbMeshStore.QUELLEN_SETTING,
PackedStringArray(["", " ", "/x"]))
var q3: Array = GlbMeshStore.quellen()
_ok(q3[0] == "/x", "quellen: leere Einträge gefiltert", str(q3))
# Ungültiger Settings-Wert (kein Array) → fällt auf Vorgabe-Verhalten.
ProjectSettings.set_setting(GlbMeshStore.QUELLEN_SETTING, "kein-array")
var q4: Array = GlbMeshStore.quellen()
_ok(q4.size() >= 3 and q4[0] == GlbMeshStore.DEFAULT_QUELLEN[0],
"quellen: ungültiger Wert → Vorgabe", str(q4))
ProjectSettings.set_setting(GlbMeshStore.QUELLEN_SETTING, PackedStringArray())
## 2. probe_source_fuer: erster EXISTIERENDER Pfad, Reihenfolge respektiert,
## verschachtelter Registry-Pfad VOR flacher Alt-Ablage, kein Parse.
## Der verschachtelte Test nutzt eine echte Registry-OID (sonst erzeugt
## _kandidaten keinen verschachtelten Kandidaten-Pfad).
func _test_probe_source() -> void:
var basis := _temp_ordner("probe")
var a := basis.path_join("a_objekte")
var b := basis.path_join("b_objekte")
var models := basis.path_join("models")
# Echte OID mit verschachteltem glb-Pfad aus der Registry holen.
var manifest: Dictionary = {}
var parsed: Variant = JSON.parse_string(
FileAccess.get_file_as_string(GlbMeshStore.MANIFEST_PATH))
if parsed is Dictionary:
manifest = (parsed as Dictionary).get("objects", {})
var oid := ""
var sub := ""
for key: String in manifest:
var eintrag: Variant = manifest[key]
if eintrag is Dictionary:
var glb := str((eintrag as Dictionary).get("glb", ""))
if glb.begins_with("res://"):
oid = key
sub = glb.substr(6)
break
if oid.is_empty():
print(" SKIP: kein OID mit res://-glb in der Registry — verschachtelter Pfad nicht testbar")
return
DirAccess.make_dir_recursive_absolute(a.path_join(sub.get_base_dir()))
DirAccess.make_dir_recursive_absolute(b)
DirAccess.make_dir_recursive_absolute(models)
# (1) verschachtelt in a (Registry-Pfad), (2) flach in b, (3) models/.
var glb_a := a.path_join(sub)
var glb_b := b.path_join(oid + ".glb")
var glb_m := models.path_join(oid + ".glb")
for p: String in [glb_a, glb_b, glb_m]:
var f := FileAccess.open(p, FileAccess.WRITE)
f.store_8(0) # Inhalt egal — probe nur file_exists, kein Parse
f.close()
ProjectSettings.set_setting(GlbMeshStore.QUELLEN_SETTING, PackedStringArray(
[_relativ(basis, a), _relativ(basis, b), _relativ(basis, models)]))
GlbMeshStore.cache_leeren()
_ok(GlbMeshStore.probe_source_fuer(oid).simplify_path() == glb_a.simplify_path(),
"probe: verschachtelter Registry-Pfad in Quelle 1 gewinnt",
GlbMeshStore.probe_source_fuer(oid))
_ok(GlbMeshStore.probe_source_fuer(oid).contains("/a_objekte/"),
"probe: Pfad enthält Quelle 1", GlbMeshStore.probe_source_fuer(oid))
# Nur models/ hat die Datei → Pfad aus models/.
var oid2 := "nur_models_oid"
var glb_m2 := models.path_join(oid2 + ".glb")
var f2 := FileAccess.open(glb_m2, FileAccess.WRITE)
f2.store_8(0)
f2.close()
_ok(GlbMeshStore.probe_source_fuer(oid2).simplify_path() == glb_m2.simplify_path(),
"probe: models/-Pfad gefunden, wenn nur dort vorhanden",
GlbMeshStore.probe_source_fuer(oid2))
_ok(GlbMeshStore.probe_source_fuer(oid2).contains("/models/"),
"probe: Pfad enthält /models/", GlbMeshStore.probe_source_fuer(oid2))
_ok(GlbMeshStore.probe_source_fuer("gaenzlich_fehlt") == "",
"probe: leerer String ohne Fund")
_ok(GlbMeshStore.probe_source_fuer("") == "", "probe: leere OID → leerer String")
ProjectSettings.set_setting(GlbMeshStore.QUELLEN_SETTING, PackedStringArray())
GlbMeshStore.cache_leeren()
## 3. Klartext-Meldung: models/-Pfad + sync_models.sh-Verweis, mehrzeilig.
func _test_fehl_text() -> void:
var text := GlbMeshStore.fehl_text()
_ok(text.contains(GlbMeshStore.models_pfad()), "fehl_text: models/-Pfad drin")
_ok(text.contains("sync_models.sh"), "fehl_text: sync_models.sh-Verweis drin")
_ok(text.contains("Notfall-Modus"), "fehl_text: Notfall-Modus erwähnt")
_ok(text.contains("\n"), "fehl_text: mehrzeilig (klar lesbar)")
_ok(not text.contains("%s"), "fehl_text: kein unfüllter Platzhalter")
## 4. lager_ist_leer: ohne Quellen true; false, sobald eine Quelle (auch
## verschachtelt) eine GLB trägt. models/ zählt als Quelle.
func _test_lager_ist_leer() -> void:
var basis := _temp_ordner("leer")
ProjectSettings.set_setting(GlbMeshStore.QUELLEN_SETTING, PackedStringArray())
GlbMeshStore.cache_leeren()
_ok(GlbMeshStore.lager_ist_leer(),
"lager_ist_leer: true ohne GLB-Quellen (Testumgebung hat keine Siblings)")
var tief := basis.path_join("tief/verschachtelt")
DirAccess.make_dir_recursive_absolute(tief)
var f := FileAccess.open(tief.path_join("eins.glb"), FileAccess.WRITE)
f.store_8(0)
f.close()
ProjectSettings.set_setting(GlbMeshStore.QUELLEN_SETTING,
PackedStringArray([_relativ(basis, basis)]))
_ok(not GlbMeshStore.lager_ist_leer(),
"lager_ist_leer: false mit verschachtelter GLB in Quelle")
ProjectSettings.set_setting(GlbMeshStore.QUELLEN_SETTING, PackedStringArray())
GlbMeshStore.cache_leeren()
## 5. Notfall-Modus: fehlende OID → fehlt()=true, mesh_fuer()=null. Ohne Modelle
## darf der Store nicht crashen (Platzhalter übernimmt der view_builder).
func _test_notfall_modus() -> void:
ProjectSettings.set_setting(GlbMeshStore.QUELLEN_SETTING, PackedStringArray())
GlbMeshStore.cache_leeren()
_ok(GlbMeshStore.fehlt("notfall_test_oid"), "notfall: fehlt()=true ohne Quellen")
_ok(GlbMeshStore.mesh_fuer("notfall_test_oid") == null,
"notfall: mesh_fuer()=null ohne Quellen (view_builder zeigt Platzhalter)")
_ok(GlbMeshStore.probe_source_fuer("notfall_test_oid") == "",
"notfall: probe_source_fuer() leer ohne Quellen")
## 6. models_pfad: direkt neben der App (res://-Wurzel), endet auf /models.
func _test_models_pfad() -> void:
var p := GlbMeshStore.models_pfad()
_ok(p.ends_with("/models"), "models_pfad: endet auf /models", p)
_ok(p.begins_with(ProjectSettings.globalize_path("res://")),
"models_pfad: neben der App (res://-Wurzel)", p)
_ok(p == ProjectSettings.globalize_path("res://").path_join("models"),
"models_pfad: exakt res://-Wurzel/models", p)
## Relativer Pfad (vom res://-Wurzel aus) — Quellen sind basis-relativ.
## (_basis wird ignoriert: die Funktion rechnet IMMER von der res://-Wurzel,
## wie _relativ_zu in test_glb_view.gd — sonst zeigt der Pfad in den falschen
## Ordner, wenn _kandidaten ihn wieder an die Wurzel hängt.)
static func _relativ(_basis: String, pfad: String) -> String:
var von := ProjectSettings.globalize_path("res://").simplify_path()
var n := pfad.simplify_path()
var v: PackedStringArray = von.split("/", false)
var teile_n: PackedStringArray = n.split("/", false)
var i := 0
while i < v.size() and i < teile_n.size() and v[i] == teile_n[i]:
i += 1
var out := PackedStringArray()
for _j: int in range(i, v.size()):
out.append("..")
for k: int in range(i, teile_n.size()):
out.append(teile_n[k])
if out.is_empty():
return "."
return "/".join(out)