From dff213cc615cccc6ec6e96b090ca043d7706fd6c Mon Sep 17 00:00:00 2001 From: Steven Wroblewski Date: Thu, 1 Oct 2026 13:23:01 +0200 Subject: [PATCH] =?UTF-8?q?Testdateien:=20L=C3=B6schen=20nur=20unter=20use?= =?UTF-8?q?r://tests,=20Altlasten=20r=C3=A4umen,=20Wortlaut?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit user://tests liegt im echten Benutzerordner des Spiels, neben Spielstand, Schlüssel und Aufzeichnungen des Spielers. TestFiles.remove_dir löschte jeden Pfad, den es bekam; ein falsches Argument hätte echte Spielerdateien gelöscht. Tests, die früh mit einem Laufzeitfehler abbrachen, ließen ihren Ordner zurück (es lagen 15 solche Ordner im Benutzerordner). - remove_dir vereinfacht den Pfad (simplify_path) und verweigert alles, was nicht user://tests oder darunter liegt, mit push_error und ohne zu löschen. Die Wurzel steht in der Konstante TestFiles.ROOT, die auch temporary_dir benutzt. - run_tests.gd entfernt user://tests zu Beginn von _run_all, vor dem Fehlerzähler, aber nur wenn der Ordner existiert, weil Godot beim Auflisten eines fehlenden Ordners einen Fehler meldet. - CLAUDE.md sagt jetzt, dass die Tests unter user://tests schreiben und nie die echten Dateien des Spielers anfassen. Das Verweigern wurde einmal von Hand geprüft, mit einem Wegwerfskript auf selbst angelegten Ordnern: Ohne Schutz löschte remove_dir sie, mit Schutz bleiben sie samt Datei stehen und es erscheint ein Fehler, auch für einen Geschwisterordner user://tests_probe und für user://tests/../…. Danach waren die 15 Altlasten verschwunden und die Ausgabe fehlerfrei, auch ohne vorhandenen Ordner user://tests. Co-Authored-By: Claude Sonnet 5.5 --- CLAUDE.md | 2 +- game/tests/run_tests.gd | 5 +++++ game/tests/support/test_files.gd | 27 +++++++++++++++++++-------- 3 files changed, 25 insertions(+), 9 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index a7aaeec..d3e8f7c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -39,7 +39,7 @@ Drei Wächter prüfen das. `core/clippy.toml` sperrt Pfade wie `std::collections - GDScript mit Typangaben schreiben (`var x := …`, Rückgabetypen an jeder Funktion). - Eingabeaktionen legt `game/scripts/input_setup.gd` an, nicht `project.godot`. - Alle Texte, die Spieler sehen, stehen in `game/i18n/texts.csv` (Deutsch und Englisch) und werden über `tr()` geholt. -- Tests, die Dateien schreiben, nutzen `TestFiles.temporary_dir()`, nie den echten Benutzerordner. +- Tests, die Dateien schreiben, nutzen `TestFiles.temporary_dir()` (Ordner unter `user://tests`), nie die echten Dateien des Spielers. - Godot erzeugt beim Import zu jedem Skript eine `.uid`-Datei. Sie gehört ins Archiv. Der Ordner `game/.godot/` gehört nicht hinein. ## Prüfen diff --git a/game/tests/run_tests.gd b/game/tests/run_tests.gd index 5a0ea89..b9cc7d0 100644 --- a/game/tests/run_tests.gd +++ b/game/tests/run_tests.gd @@ -44,6 +44,11 @@ func _initialize() -> void: func _run_all() -> void: + # Remove what runs that stopped early left behind, before the error + # counter starts. Only if the folder exists, because Godot reports an + # error when it lists a missing one. + if DirAccess.dir_exists_absolute(TestFiles.ROOT): + TestFiles.remove_dir(TestFiles.ROOT) var counter := ErrorCounter.new() OS.add_logger(counter) var passed := 0 diff --git a/game/tests/support/test_files.gd b/game/tests/support/test_files.gd index 09d77d3..55fd5bd 100644 --- a/game/tests/support/test_files.gd +++ b/game/tests/support/test_files.gd @@ -1,18 +1,29 @@ class_name TestFiles extends RefCounted -## Temporary folders below user://tests for tests that write files, so that -## tests never touch the player's real key, save or recordings. +## Temporary folders below user://tests for tests that write files. That +## folder lies inside the game's real user folder, next to the player's own +## key, save and recordings, so nothing outside it may be deleted here. + +## The only folder the tests write to and remove_dir deletes from. +const ROOT := "user://tests" static func temporary_dir(label: String) -> String: - var path := "user://tests".path_join("%s-%d" % [label, Time.get_ticks_usec()]) + var path := ROOT.path_join("%s-%d" % [label, Time.get_ticks_usec()]) DirAccess.make_dir_recursive_absolute(path) return path +## Deletes the folder `path` with everything in it. Refuses every path that is +## not ROOT or below it, with an error and without deleting anything: a wrong +## argument must never delete the player's real files. static func remove_dir(path: String) -> void: - for file in DirAccess.get_files_at(path): - DirAccess.remove_absolute(path.path_join(file)) - for directory in DirAccess.get_directories_at(path): - remove_dir(path.path_join(directory)) - DirAccess.remove_absolute(path) + var clean := path.simplify_path() + if clean != ROOT and not clean.begins_with(ROOT + "/"): + push_error("TestFiles.remove_dir refuses %s: it is not %s or below it" % [path, ROOT]) + return + for file in DirAccess.get_files_at(clean): + DirAccess.remove_absolute(clean.path_join(file)) + for directory in DirAccess.get_directories_at(clean): + remove_dir(clean.path_join(directory)) + DirAccess.remove_absolute(clean)