dff213cc61
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 <noreply@anthropic.com>
88 lines
2.6 KiB
GDScript
88 lines
2.6 KiB
GDScript
extends SceneTree
|
|
## Runs every test script in res://tests whose file name starts with "test_".
|
|
## Every method whose name starts with "test_" is one test. A test fails if a
|
|
## check fails or if an error is reported while it runs, for example a call
|
|
## on a null value. Exits with code 1 if any test failed or if no test ran,
|
|
## so that a renamed test folder or misnamed tests cannot pass as a clean run.
|
|
##
|
|
## From the repository root:
|
|
## godot --headless --path game -s res://tests/run_tests.gd
|
|
|
|
const TEST_DIR := "res://tests"
|
|
|
|
|
|
## Counts the errors that are reported while tests run. Warnings do not count.
|
|
class ErrorCounter extends Logger:
|
|
var _mutex := Mutex.new()
|
|
var _errors := 0
|
|
|
|
func errors() -> int:
|
|
_mutex.lock()
|
|
var count := _errors
|
|
_mutex.unlock()
|
|
return count
|
|
|
|
func _log_error(
|
|
_function: String,
|
|
_file: String,
|
|
_line: int,
|
|
_code: String,
|
|
_rationale: String,
|
|
_editor_notify: bool,
|
|
error_type: int,
|
|
_script_backtraces: Array[ScriptBacktrace]
|
|
) -> void:
|
|
if error_type == ERROR_TYPE_WARNING:
|
|
return
|
|
_mutex.lock()
|
|
_errors += 1
|
|
_mutex.unlock()
|
|
|
|
|
|
func _initialize() -> void:
|
|
_run_all.call_deferred()
|
|
|
|
|
|
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
|
|
var failed := 0
|
|
var files := DirAccess.get_files_at(TEST_DIR)
|
|
files.sort()
|
|
for file in files:
|
|
if not (file.begins_with("test_") and file.ends_with(".gd")):
|
|
continue
|
|
var script: GDScript = load(TEST_DIR.path_join(file))
|
|
if script == null or not script.can_instantiate():
|
|
failed += 1
|
|
print("FAIL %s the script does not load; see the error above" % file)
|
|
continue
|
|
for method in script.get_script_method_list():
|
|
var test_name: String = method["name"]
|
|
if not test_name.begins_with("test_"):
|
|
continue
|
|
var test: TestCase = script.new()
|
|
test.tree = self
|
|
var errors_before := counter.errors()
|
|
await test.call(test_name)
|
|
var errors := counter.errors() - errors_before
|
|
if errors > 0:
|
|
test.failures.append("%d error(s) reported while the test ran; see above" % errors)
|
|
if test.failures.is_empty():
|
|
passed += 1
|
|
print("ok %s %s" % [file, test_name])
|
|
else:
|
|
failed += 1
|
|
print("FAIL %s %s" % [file, test_name])
|
|
for failure in test.failures:
|
|
print(" " + failure)
|
|
OS.remove_logger(counter)
|
|
print("%d passed, %d failed" % [passed, failed])
|
|
quit(1 if failed > 0 or passed == 0 else 0)
|