spaceship/tests/test_setup_file_identity.gd
Loïc Wisniewski 6e13b6397c Take planets by killing what defends them
#211 conquest is the fight now: a planet registers the garrison its setup
authors, each defender's own death counts against it, and the last one takes
the world. The capture points - stand in a ring, wait out a timer, once per
ring - are gone, along with the editor palette that placed them.
#212 conquest opens a dialog on the surface naming the research points
recovered; the award travels with the signal, after it is banked.
#209 the prologue's charted sectors go with the breach, so the map the
survivors keep is of where they now are - and "chart 30 sectors" is no
longer complete before they have flown anywhere.
#218 enemy variety is gated on the FLEET, not on wave strength: a starting
fleet meets chasers only at any wave number, and each vessel past three
opens one more kind.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019AJMdB7iuHuLFZM1aBu3GD
2026-09-05 13:31:25 +02:00

157 lines
6.6 KiB
GDScript

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/<id>.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/<id>.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