# test_phase3b_library.gd # Headless tests for Phase 3b Asset Library (no pixel rendering): # 1. StickmanLibrary.scan() returns 3 entries (test/basic/break) sorted by # display name; empty stickman_name falls back to the filename basename. # 2. Corrupt / body-parts-less .stk files are skipped (scan dir override). # 3. make_entry() valid path -> name+data; missing/corrupt path -> {}. # 4. PropLibrary.get_entries() -> 4 templates; get_default_id() == "crate". # 5. StageSpawner registry ids == [ground, ramp, step, prop, stickman, area]. # 6. _spawn_prop honors selected_prop_id (crate polygon / ball circle). # 7. _spawn_stickman honors selected_stickman_path (basic.stk spawns non-null). # 8. ThumbnailCache key/png formatting. # 9. AssetSelector pagination math (PAGE_SIZE == 12; page_bounds slicing). # 10. Scene load checks (sandbox_stage.tscn + asset_selector.tscn parse/load). # # Run with: # & "C:\Godot4\Godot_v4.7.1-stable_win64_console.exe" --headless --script res://tests/test_phase3b_library.gd --path . # # Prints PASS/FAIL per assertion and exits 0 on all PASS, 1 on any FAIL. extends SceneTree const STICKMAN_LIBRARY := preload("res://scripts/stickman_library.gd") const PROP_LIBRARY := preload("res://scripts/prop_library.gd") const THUMBNAIL_CACHE := preload("res://scripts/thumbnails/thumbnail_cache.gd") const ASSET_SELECTOR := preload("res://scripts/asset_selector.gd") const STAGE_SPAWNER := preload("res://scripts/stage_spawner.gd") const PROP_BLOCK := preload("res://scripts/prop_block.gd") const RIG := preload("res://scripts/stickman_rig.gd") const STAGE_SCENE := preload("res://scenes/sandbox_stage.tscn") const ASSET_SELECTOR_SCENE := preload("res://scenes/asset_selector.tscn") const TEST_DIR := "user://phase3b_test" var _checks := 0 var _failures := 0 func _initialize() -> void: call_deferred("_run") func _run() -> void: print("") print("========================================================") print(" PHASE 3b ASSET LIBRARY TEST (headless)") print("========================================================") _test_scan() _test_corrupt_skip() _test_make_entry() _test_prop_library() _test_spawner_registry() _test_spawn_prop() _test_spawn_stickman() _test_thumbnail_cache() _test_pagination() _test_scene_loads() print("--------------------------------------------------------") if _failures == 0: print("RESULT: ALL PASSED (%d assertions, 0 failures)" % _checks) quit(0) else: print("RESULT: %d FAILURE(S) out of %d assertions" % [_failures, _checks]) quit(1) func _test_scan() -> void: print("") print("--- StickmanLibrary.scan() ---") var lib = STICKMAN_LIBRARY.new() var entries: Array[Dictionary] = lib.scan() _check(entries.size() == 3, "scan returns 3 entries (got %d)" % entries.size()) var names: Array[String] = [] for e: Dictionary in entries: names.append(String(e["name"])) _check(names == ["Basic", "break", "test"], "entries sorted by display name (got %s)" % str(names)) _check(String(_by_path(entries, "basic.stk")["name"]) == "Basic", "basic.stk display name is 'Basic' (stickman_name)") _check(String(_by_path(entries, "test.stk")["name"]) == "test", "test.stk falls back to filename basename") _check(String(_by_path(entries, "break.stk")["name"]) == "break", "break.stk falls back to filename basename") _check(not _by_path(entries, "basic.stk").is_empty(), "basic.stk entry has data") var found: Dictionary = lib.find_by_path("res://stickmen/basic.stk") _check(not found.is_empty() and String(found["name"]) == "Basic", "find_by_path resolves basic.stk") func _test_corrupt_skip() -> void: print("") print("--- Corrupt / body-parts-less .stk skipped ---") _ensure_test_dir() _write_file(TEST_DIR + "/good.stk", "{\"stickman_name\":\"Good\",\"body_parts\":{}}") _write_file(TEST_DIR + "/corrupt.stk", "{ not valid json !!!") _write_file(TEST_DIR + "/nobody.stk", "{\"version\":\"1.5\"}") var lib = STICKMAN_LIBRARY.new() var entries: Array[Dictionary] = lib.scan(TEST_DIR) _check(entries.size() == 1, "scan skips corrupt + body-parts-less files (got %d)" % entries.size()) if entries.size() == 1: _check(String(entries[0]["name"]) == "Good", "only the valid file survives (name='%s')" % String(entries[0]["name"])) _clean_test_dir() func _test_make_entry() -> void: print("") print("--- make_entry() ---") var lib = STICKMAN_LIBRARY.new() var entry := lib.make_entry("res://stickmen/basic.stk") _check(not entry.is_empty(), "make_entry on valid path returns an entry") _check(String(entry["name"]) == "Basic", "make_entry name == 'Basic'") _check(not (entry["data"] as Dictionary).is_empty(), "make_entry carries parsed data") _check(lib.make_entry("res://stickmen/does_not_exist.stk").is_empty(), "make_entry on missing path returns {}") _ensure_test_dir() _write_file(TEST_DIR + "/bad.stk", "nope nope") _check(lib.make_entry(TEST_DIR + "/bad.stk").is_empty(), "make_entry on corrupt file returns {}") _clean_test_dir() func _test_prop_library() -> void: print("") print("--- PropLibrary ---") var entries: Array[Dictionary] = PROP_LIBRARY.get_entries() _check(entries.size() == 4, "4 prop templates (got %d)" % entries.size()) _check(PROP_LIBRARY.get_default_id() == "crate", "default prop id == 'crate'") var ids: Array[String] = PROP_LIBRARY.get_ids() _check(ids == ["crate", "ball", "plank", "triangle"], "prop ids in order (got %s)" % str(ids)) var crate: Dictionary = PROP_LIBRARY.get_entry("crate") _check(int(crate["material_preset"]) == PROP_BLOCK.MaterialPreset.WOOD, "crate material preset == WOOD") _check(String(crate["material_label"]) == "Wood", "crate material label == 'Wood'") var ball: Dictionary = PROP_LIBRARY.get_entry("ball") _check(int(ball["material_preset"]) == PROP_BLOCK.MaterialPreset.RUBBER, "ball material preset == RUBBER") _check(PROP_LIBRARY.get_entry("nope").is_empty(), "unknown prop id returns {}") func _test_spawner_registry() -> void: print("") print("--- StageSpawner registry ---") var world := Node2D.new() root.add_child(world) var spawner = STAGE_SPAWNER.new(world) var ids: Array[String] = spawner.get_spawnable_ids() _check(ids == ["ground", "ramp", "step", "prop", "stickman", "area"], "registry ids == [ground, ramp, step, prop, stickman, area] (got %s)" % str(ids)) _check(not ids.has("crate") and not ids.has("ball"), "crate/ball registry entries removed") _check(spawner.get_selected_prop_id() == "crate", "default selected_prop_id == 'crate'") _check(spawner.get_selected_stickman_path() == "res://stickmen/test.stk", "default selected_stickman_path == test.stk") world.queue_free() func _test_spawn_prop() -> void: print("") print("--- _spawn_prop honors selected_prop_id ---") var world := Node2D.new() root.add_child(world) var spawner = STAGE_SPAWNER.new(world) var crate: Node2D = spawner.spawn("prop", Vector2.ZERO) _check(crate != null, "prop spawns (default crate)") _check(crate is PROP_BLOCK, "default prop is a PropBlock") _check((crate as PROP_BLOCK).shape_type == PROP_BLOCK.ShapeType.POLYGON, "default crate is a POLYGON prop") spawner.selected_prop_id = "ball" var ball: Node2D = spawner.spawn("prop", Vector2(100, 0)) _check(ball != null, "ball prop spawns after selected_prop_id = 'ball'") _check((ball as PROP_BLOCK).shape_type == PROP_BLOCK.ShapeType.CIRCLE, "ball prop is a CIRCLE prop") world.queue_free() func _test_spawn_stickman() -> void: print("") print("--- _spawn_stickman honors selected_stickman_path ---") var world := Node2D.new() root.add_child(world) var spawner = STAGE_SPAWNER.new(world) spawner.selected_stickman_path = "res://stickmen/basic.stk" var rig: Node2D = spawner.spawn("stickman", Vector2.ZERO) _check(rig != null, "stickman spawns from basic.stk") _check(rig is RIG, "basic.stk spawns a StickmanRig") world.queue_free() func _test_thumbnail_cache() -> void: print("") print("--- ThumbnailCache key/png formatting ---") var cache = THUMBNAIL_CACHE.new() var mtime: int = FileAccess.get_modified_time("res://stickmen/test.stk") var key := cache.stickman_key("res://stickmen/test.stk") _check(key == "test_%d" % mtime, "stickman_key embeds basename + mtime (got '%s')" % key) _check(cache.stickman_png(key) == "user://thumbnails/stickmen/" + key + ".png", "stickman_png path formatting (got '%s')" % cache.stickman_png(key)) _check(cache.prop_png("crate") == "user://thumbnails/props/crate_v" + str(THUMBNAIL_CACHE.PROP_VERSION) + ".png", "prop_png embeds id + version (got '%s')" % cache.prop_png("crate")) _check(THUMBNAIL_CACHE.PROP_VERSION == 1, "PROP_VERSION == 1") _check(cache.load_png("user://does_not_exist_phase3b.png") == null, "load_png on missing file returns null") func _test_pagination() -> void: print("") print("--- AssetSelector pagination ---") _check(ASSET_SELECTOR.PAGE_SIZE == 12, "PAGE_SIZE == 12 (got %d)" % ASSET_SELECTOR.PAGE_SIZE) _check(ASSET_SELECTOR.COLUMNS == 4 and ASSET_SELECTOR.ROWS == 3, "COLUMNS=4, ROWS=3") var b0: Dictionary = ASSET_SELECTOR.page_bounds(20, 0) _check(int(b0["start"]) == 0 and int(b0["end"]) == 12 and int(b0["page_count"]) == 2, "page 0 slices [0,12) of 20, page_count=2") var b1: Dictionary = ASSET_SELECTOR.page_bounds(20, 1) _check(int(b1["start"]) == 12 and int(b1["end"]) == 20, "page 1 slices [12,20) of 20") var b4: Dictionary = ASSET_SELECTOR.page_bounds(4, 0) _check(int(b4["start"]) == 0 and int(b4["end"]) == 4 and int(b4["page_count"]) == 1, "4 entries -> single page") func _test_scene_loads() -> void: print("") print("--- Scene load checks ---") _check(load("res://scenes/sandbox_stage.tscn") != null, "sandbox_stage.tscn loads") _check(load("res://scenes/asset_selector.tscn") != null, "asset_selector.tscn loads") var selector: AssetSelector = ASSET_SELECTOR_SCENE.instantiate() as AssetSelector _check(selector != null, "asset_selector.tscn instantiates as AssetSelector") root.add_child(selector) _check(selector._title_label != null and selector._grid != null, "asset_selector unique-name markers resolve") _check(selector._browse_button != null and selector._refresh_button != null, "asset_selector footer buttons resolve") selector.queue_free() await process_frame var stage: Node2D = STAGE_SCENE.instantiate() root.add_child(stage) _check(stage._selector != null, "stage builds the AssetSelector") _check(stage._stickman_library != null, "stage builds the StickmanLibrary") _check(stage._stickman_thumb != null and stage._prop_thumb != null, "stage builds the thumbnail renderers") _check(stage._spawner.get_spawnable_ids() == ["ground", "ramp", "step", "prop", "stickman", "area"], "stage spawner registry excludes crate/ball") stage.queue_free() await process_frame func _by_path(entries: Array[Dictionary], suffix: String) -> Dictionary: for e: Dictionary in entries: if String(e["path"]).ends_with(suffix): return e return {} func _ensure_test_dir() -> void: DirAccess.make_dir_recursive_absolute(TEST_DIR) func _write_file(path: String, text: String) -> void: var f := FileAccess.open(path, FileAccess.WRITE) if f != null: f.store_string(text) f.close() func _clean_test_dir() -> void: var dir := DirAccess.open(TEST_DIR) if dir != null: dir.list_dir_begin() var fname := dir.get_next() while fname != "": if not dir.current_is_dir(): DirAccess.remove_absolute(TEST_DIR + "/" + fname) fname = dir.get_next() dir.list_dir_end() DirAccess.remove_absolute(TEST_DIR) func _check(condition: bool, message: String) -> void: _checks += 1 if condition: print("PASS: " + message) else: _failures += 1 print("FAIL: " + message)