#!/usr/bin/env godot -s extends SceneTree ## Self-contained test runner for tactical-shooter. ## No framework dependencies. ## ## Usage: ## godot --headless -s tests/runner.gd --path . ## ## Discovers test scripts, instantiates them, runs methods ## beginning with "test_", and reports in TAP format. func _init(): print("TAP version 14") print("1..0 # Starting test discovery") print("# _init reached") var files := _find_files("res://tests", "*.gd", ["runner.gd"]) print("# Found %d test file(s)" % files.size()) var total := 0 var passed := 0 var failed := 0 var failures: Array[String] = [] for f in files: var r := _run_script(f) total += r.total passed += r.passed failed += r.failed failures.append_array(r.failures) # Re-print header with real count print("1..%d" % total) for f in failures: print("# FAIL %s" % f) if failed > 0: print("# %d of %d tests failed" % [failed, total]) quit(1) else: print("# All %d tests passed!" % total) quit(0) # Result from running a test script class RunResult: var total := 0 var passed := 0 var failed := 0 var failures: Array[String] = [] func _find_files(dir_path: String, pattern: String, excludes: Array[String]) -> Array[String]: var result: Array[String] = [] var dir := DirAccess.open(dir_path) if dir == null: push_error("Cannot open directory: %s" % dir_path) return result dir.list_dir_begin() var fname := dir.get_next() while fname != "": if fname.ends_with(".gd") and not fname in excludes and not fname.begins_with("."): result.append(dir_path.path_join(fname)) fname = dir.get_next() dir.list_dir_end() return result func _run_script(path: String) -> RunResult: var r := RunResult.new() var script := load(path) if script == null: push_error("Cannot load: %s" % path) return r if not script.has_method("new"): push_error("Cannot instantiate: %s" % path) return r var obj = script.new() var methods := _get_test_methods(script) print("# Running: %s (%d tests)" % [path, methods.size()]) for m in methods: var test_name := m.trim_prefix("test_").replace("_", " ").capitalize() r.total += 1 var ok := true var err_msg := "" var callable = Callable(obj, m) var result = callable.call() if result is String and result.length() > 0: ok = false err_msg = result if ok: r.passed += 1 print("ok %d - %s" % [r.total, test_name]) else: r.failed += 1 print("not ok %d - %s" % [r.total, test_name]) var file_line := "(unknown)" if obj.has_method("_get_current_line"): file_line = str(obj._get_current_line()) r.failures.append("%s::%s: %s" % [path.get_file(), m, err_msg]) if not obj is RefCounted: obj.free() return r func _get_test_methods(script: Script) -> Array[String]: var result: Array[String] = [] var methods := script.get_script_method_list() for m in methods: var name := m["name"] as String if name.begins_with("test_") and name != "_init": result.append(name) return result