extends SceneTree # Headless test: run with # godot --headless -s res://tests/test_setup_file_identity.gd # A setup's id and its FILENAME are not the same thing. res://PlanetSetups/ # holds authored resources named however their author named them, and shipped # content already contains a mismatch: ResearchLabSetup.tres holds setup_id # "research_lab_1". # # The editor's save and delete both rebuilt the path from the id, which # produced two bugs that looked unrelated: # - deleting such a setup looked for a file that does not exist, refused, # and reported nothing useful. It was always the same entry, so it read # as "the first item in the list cannot be deleted" # - saving such a setup wrote a SECOND file under the derived name and left # the original in place, so the pool then held two setups claiming one id # and the stale one could still be served # A third bug hid behind those: rescan() re-read through Godot's resource # CACHE, so even a correctly written file came back with its pre-save values. # All three present identically to a player - "my change was not saved". # # Covers: # A. shipped content really does contain an id/filename mismatch, so this is # guarding a live case (checked read-only - see the note on isolation) # B. saving a mismatched setup writes back to ITS file and creates no twin # C. the edit is what the registry serves afterwards, cache and all # D. such a setup can be deleted # E. a brand-new setup still lands at PlanetSetups/.tres # # ISOLATION: every mutation happens on a scratch file this test creates, never # on shipped content. The suite runs tests in parallel against one project # directory, and deleting a real setup mid-run - even briefly, even restored # afterwards - would make some other test fail for reasons of its own. const SCRATCH_FILE := "res://PlanetSetups/_identity_scratch_file.tres" const SCRATCH_ID := "_identity_scratch_id" const DERIVED_PATH := "res://PlanetSetups/_identity_scratch_id.tres" const NEW_ID := "test_identity_new" const NEW_PATH := "res://PlanetSetups/test_identity_new.tres" var editor: Node var elapsed: float = 0.0 var stage: int = 0 func _initialize() -> void: _cleanup() editor = load("res://surface/PlanetSetupEditor.tscn").instantiate() root.add_child(editor) func _process(delta: float) -> bool: elapsed += delta if stage != 0: return false if elapsed < 0.15: return false stage = 1 # --- A. shipped content has a mismatch (read-only) --------------------- var shipped_mismatch: String = "" for setup in PlanetSetups.all(): var path: String = String(setup.resource_path) if path != "" and String(setup.setup_id) != path.get_file().get_basename(): shipped_mismatch = "%s holds id '%s'" % [path.get_file(), setup.setup_id] break if shipped_mismatch == "": print("note: no shipped setup currently has an id/filename mismatch; the scratch case below still covers the behaviour") else: print("shipped mismatch present: %s" % shipped_mismatch) # A setup whose FILE NAME deliberately differs from its id. var scratch: Resource = load("res://surface/PlanetSetup.gd").new() scratch.setup_id = SCRATCH_ID scratch.display_name = "Identity Scratch" scratch.surface_profile_id = "rocky" scratch.research_point_reward = 1 if ResourceSaver.save(scratch, SCRATCH_FILE) != OK: return _fail("could not write the scratch setup") PlanetSetups.rescan() if PlanetSetups.path_for_id(SCRATCH_ID) != SCRATCH_FILE: return _fail("the registry resolved '%s' to %s, not the file it lives in (%s)" \ % [SCRATCH_ID, PlanetSetups.path_for_id(SCRATCH_ID), SCRATCH_FILE]) print("'%s' lives in %s - id and filename disagree" % [SCRATCH_ID, SCRATCH_FILE.get_file()]) # --- B/C. edit, save, and see the edit served ------------------------- if not editor.load_setup(SCRATCH_ID): return _fail("the editor could not open '%s'" % SCRATCH_ID) var before: int = int(editor.current_setup.research_point_reward) editor._on_reward_pressed() var chosen: int = int(editor.current_setup.research_point_reward) if chosen == before: return _fail("cycling the reward did not change it") var outcome: Dictionary = editor.save_setup() if not outcome.ok: return _fail("saving failed: %s" % outcome.message) if FileAccess.file_exists(ProjectSettings.globalize_path(DERIVED_PATH)): return _fail("saving wrote a twin at %s instead of back to the file it was loaded from" % DERIVED_PATH) PlanetSetups.rescan() var served: Resource = PlanetSetups.find_by_id(SCRATCH_ID) if served == null: return _fail("'%s' vanished from the pool after saving" % SCRATCH_ID) if int(served.research_point_reward) != chosen: return _fail("saved reward %d but the pool still serves %d - the rescan read the resource cache rather than the file" \ % [chosen, int(served.research_point_reward)]) var duplicates: int = 0 for setup in PlanetSetups.all(): if String(setup.setup_id) == SCRATCH_ID: duplicates += 1 if duplicates != 1: return _fail("the pool holds %d setups all claiming id '%s'" % [duplicates, SCRATCH_ID]) print("reward %d -> %d saved back into %s, no twin, and the pool serves the edit" \ % [before, chosen, SCRATCH_FILE.get_file()]) # --- E. a NEW setup still lands under its id -------------------------- editor.new_setup(NEW_ID, "Identity New", "rocky") editor.add_enemy("chaser", Vector2(20.0, 30.0)) if not editor.save_setup().ok: return _fail("saving a brand-new setup failed") if not FileAccess.file_exists(ProjectSettings.globalize_path(NEW_PATH)): return _fail("a new setup did not land at %s" % NEW_PATH) PlanetSetups.rescan() if not editor.delete_setup(NEW_ID).ok: return _fail("could not delete the new setup") print("a new setup still saves to PlanetSetups/.tres, and deletes again") # --- D. the mismatched setup can be deleted --------------------------- PlanetSetups.rescan() var deleted: Dictionary = editor.delete_setup(SCRATCH_ID) if not deleted.ok: return _fail("deleting '%s' failed: %s" % [SCRATCH_ID, deleted.message]) if FileAccess.file_exists(ProjectSettings.globalize_path(SCRATCH_FILE)): return _fail("delete reported success but %s is still there" % SCRATCH_FILE) print("a setup whose filename differs from its id can be deleted") _cleanup() print("TEST_SETUP_FILE_IDENTITY: PASS") quit(0) return true func _cleanup() -> void: for path in [SCRATCH_FILE, DERIVED_PATH, NEW_PATH]: var absolute: String = ProjectSettings.globalize_path(path) if FileAccess.file_exists(absolute): DirAccess.remove_absolute(absolute) if FileAccess.file_exists(absolute + ".uid"): DirAccess.remove_absolute(absolute + ".uid") PlanetSetups.rescan() func _fail(message: String) -> bool: _cleanup() print("TEST_SETUP_FILE_IDENTITY: FAIL (%s)" % message) quit(1) return true