fix(runtime): make session revisions atomic
This commit is contained in:
@@ -13,7 +13,7 @@ func _init(initial_state: FirstSessionState = null) -> void:
|
||||
|
||||
func reset_transient(reason: StringName = &"reset") -> Dictionary:
|
||||
state.reset_transient()
|
||||
return _success([{ "kind": Contracts.EVENT_RUN_CLEARED, "reason": reason }], false)
|
||||
return _success([{ "kind": Contracts.EVENT_RUN_CLEARED, "reason": reason }])
|
||||
|
||||
func open_new_session() -> Dictionary:
|
||||
state.reset_transient()
|
||||
|
||||
@@ -79,7 +79,6 @@ func set_screen(value: StringName) -> void:
|
||||
legacy_page = mapped_page
|
||||
|
||||
func reset_transient() -> void:
|
||||
revision += 1
|
||||
front_choice = ""
|
||||
rear_choice = ""
|
||||
selected_recruits.clear()
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
extends SceneTree
|
||||
|
||||
const Contracts = preload("res://domain/p9_1/contracts.gd")
|
||||
const FirstSessionServiceScript = preload("res://domain/p9_1/session_service.gd")
|
||||
const FirstSessionStateScript = preload("res://domain/p9_1/session_state.gd")
|
||||
|
||||
var failures: Array[String] = []
|
||||
var successful_commands := 0
|
||||
var rejected_commands := 0
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("run_test")
|
||||
|
||||
|
||||
func run_test() -> void:
|
||||
_test_success_matrix()
|
||||
_test_rejection_matrix()
|
||||
_test_exact_guild_ids()
|
||||
|
||||
if failures.is_empty():
|
||||
print("ABG_P9_1_FOUNDATION_ATOMICITY_OK success=%d rejected=%d exact_guild_ids=2 legacy_leaks=0" % [successful_commands, rejected_commands])
|
||||
quit(0)
|
||||
else:
|
||||
for failure in failures:
|
||||
push_error(failure)
|
||||
quit(1)
|
||||
|
||||
|
||||
func _test_success_matrix() -> void:
|
||||
_success(&"reset_transient")
|
||||
_success(&"open_new_session")
|
||||
_success(&"toggle_recruit", ["Hearthguard"], {"screen": Contracts.SCREEN_RECRUIT_SELECTION})
|
||||
_success(&"review_formation", [], _formation_fields(Contracts.SCREEN_RECRUIT_SELECTION))
|
||||
_success(&"revise_formation", [], {"screen": Contracts.SCREEN_FORMATION_REVIEW})
|
||||
_success(&"commit_formation", [], _formation_fields(Contracts.SCREEN_FORMATION_REVIEW))
|
||||
_success(&"begin_compat_first_battle", [], {"front_choice": "Hearthguard", "rear_choice": "Farstring"})
|
||||
_success(&"open_adaptation", [], {"screen": Contracts.SCREEN_RESULT_1, "first_reward_committed": true})
|
||||
_success(&"open_guild_response", [], {"screen": Contracts.SCREEN_RESULT_2, "adaptation_committed": true})
|
||||
_success(&"open_route_board", [], {"screen": Contracts.SCREEN_RESULT_3, "guild_committed": true})
|
||||
_success(&"open_growth", [], {"screen": Contracts.SCREEN_RESULT_4, "route_committed": true, "route_reward_committed": true})
|
||||
_success(&"commit_reward", [&"battle_1", 20, Contracts.SCREEN_RESULT_1], {"screen": Contracts.SCREEN_RESULT_1})
|
||||
_success(&"choose_adaptation", ["EQ-002"], {"screen": Contracts.SCREEN_ADAPTATION})
|
||||
_success(&"commit_adaptation", [], {"screen": Contracts.SCREEN_ADAPTATION, "adaptation_choice": "EQ-002"})
|
||||
_success(&"choose_guild_response", ["retain_loan"], {"screen": Contracts.SCREEN_GUILD_RESPONSE, "coin": 20})
|
||||
_success(&"commit_guild_response", [], _guild_fields("retain_loan"))
|
||||
_success(&"choose_route", ["R6-01"], {"screen": Contracts.SCREEN_ROUTE_BOARD})
|
||||
_success(&"review_route", [], {"screen": Contracts.SCREEN_ROUTE_BOARD, "route_choice": "R6-01"})
|
||||
_success(&"cancel_route_review", [], {"screen": Contracts.SCREEN_ROUTE_REVIEW})
|
||||
_success(&"commit_route", [], {"screen": Contracts.SCREEN_ROUTE_REVIEW, "route_choice": "R6-01"})
|
||||
_success(&"choose_growth", ["Hearthguard"], _growth_fields(Contracts.SCREEN_GROWTH))
|
||||
_success(&"review_growth", [], _growth_fields(Contracts.SCREEN_GROWTH, "Hearthguard"))
|
||||
_success(&"cancel_growth_review", [], _growth_fields(Contracts.SCREEN_GROWTH_REVIEW))
|
||||
_success(&"commit_growth", [], _growth_fields(Contracts.SCREEN_GROWTH_REVIEW, "Hearthguard"))
|
||||
_success(&"commit_impact", [Contracts.ENCOUNTER_1, 0, &"atomicity", {"delta": 0}], _battle_fields(Contracts.SCREEN_BATTLE_1, 0, true))
|
||||
_success(&"resolve_impact", [Contracts.ENCOUNTER_1, 2], _battle_fields(Contracts.SCREEN_BATTLE_1, 2, true))
|
||||
_success(&"resolve_compat_impact", [Contracts.ENCOUNTER_1, 2], _battle_fields(Contracts.SCREEN_BATTLE_1, 2, false))
|
||||
_success(&"complete_phase", [Contracts.ENCOUNTER_1, 0], _battle_fields(Contracts.SCREEN_BATTLE_1, 0, true))
|
||||
|
||||
|
||||
func _test_rejection_matrix() -> void:
|
||||
_reject(&"toggle_recruit", ["Hearthguard"])
|
||||
_reject(&"review_formation")
|
||||
_reject(&"revise_formation")
|
||||
_reject(&"commit_formation")
|
||||
_reject(&"begin_compat_first_battle")
|
||||
_reject(&"open_adaptation")
|
||||
_reject(&"open_guild_response")
|
||||
_reject(&"open_route_board")
|
||||
_reject(&"open_growth")
|
||||
_reject(&"commit_reward", [&"battle_1", 20, Contracts.SCREEN_RESULT_1])
|
||||
_reject(&"choose_adaptation", ["EQ-002"])
|
||||
_reject(&"commit_adaptation")
|
||||
_reject(&"choose_guild_response", ["retain_loan"])
|
||||
_reject(&"commit_guild_response")
|
||||
_reject(&"choose_route", ["R6-01"])
|
||||
_reject(&"review_route")
|
||||
_reject(&"cancel_route_review")
|
||||
_reject(&"commit_route")
|
||||
_reject(&"choose_growth", ["Hearthguard"])
|
||||
_reject(&"review_growth")
|
||||
_reject(&"cancel_growth_review")
|
||||
_reject(&"commit_growth")
|
||||
_reject(&"commit_impact", [Contracts.ENCOUNTER_1, 0, &"atomicity", {}])
|
||||
_reject(&"resolve_impact", [Contracts.ENCOUNTER_1, 2])
|
||||
_reject(&"resolve_compat_impact", [Contracts.ENCOUNTER_1, 2])
|
||||
_reject(&"complete_phase", [Contracts.ENCOUNTER_1, 0])
|
||||
|
||||
|
||||
func _test_exact_guild_ids() -> void:
|
||||
for guild_id in Contracts.GUILD_RESPONSE_IDS:
|
||||
var service = _service({"screen": Contracts.SCREEN_GUILD_RESPONSE, "coin": 20})
|
||||
var before: Dictionary = service.snapshot()
|
||||
_assert_success("choose_guild_response:%s" % guild_id, service, service.callv(&"choose_guild_response", [guild_id]), before)
|
||||
before = service.snapshot()
|
||||
_assert_success("commit_guild_response:%s" % guild_id, service, service.callv(&"commit_guild_response", []), before)
|
||||
expect(service.state.guild_response == guild_id, "%s remains the committed exact Guild ID" % guild_id)
|
||||
if guild_id == "retain_loan":
|
||||
expect(service.state.added_rear.is_empty(), "retain_loan does not add a Recruit")
|
||||
else:
|
||||
expect(not service.state.added_rear.is_empty(), "add_recruit adds the mapped Recruit")
|
||||
for legacy_id in ["retain_item", "recruit_rear"]:
|
||||
var service = _service({"screen": Contracts.SCREEN_GUILD_RESPONSE, "coin": 20})
|
||||
var before: Dictionary = service.snapshot()
|
||||
var result: Dictionary = service.choose_guild_response(legacy_id)
|
||||
_assert_rejected("legacy_guild_id:%s" % legacy_id, service, before, result)
|
||||
expect(service.state.guild_response.is_empty(), "%s does not leak into state" % legacy_id)
|
||||
|
||||
|
||||
func _success(method: StringName, args: Array = [], fields: Dictionary = {}) -> void:
|
||||
var service = _service(fields)
|
||||
var before: Dictionary = service.snapshot()
|
||||
var result: Dictionary = service.callv(method, args)
|
||||
_assert_success(String(method), service, result, before)
|
||||
|
||||
|
||||
func _reject(method: StringName, args: Array = [], fields: Dictionary = {}) -> void:
|
||||
var service = _service(fields)
|
||||
var before: Dictionary = service.snapshot()
|
||||
var result: Dictionary = service.callv(method, args)
|
||||
_assert_rejected(String(method), service, before, result)
|
||||
|
||||
|
||||
func _assert_success(label: String, service, result: Dictionary, before: Dictionary) -> void:
|
||||
var after: Dictionary = service.snapshot()
|
||||
expect(result.ok, "%s succeeds" % label)
|
||||
expect(after.revision == before.revision + 1, "%s revision delta is exactly 1, got %d" % [label, after.revision - before.revision])
|
||||
expect(result.snapshot == after, "%s returns the committed snapshot" % label)
|
||||
if result.ok:
|
||||
successful_commands += 1
|
||||
|
||||
|
||||
func _assert_rejected(label: String, service, before: Dictionary, result: Dictionary) -> void:
|
||||
var after: Dictionary = service.snapshot()
|
||||
expect(not result.ok, "%s rejects" % label)
|
||||
expect(after.revision == before.revision, "%s preserves revision" % label)
|
||||
expect(after == before, "%s preserves the complete snapshot" % label)
|
||||
expect(result.snapshot == before, "%s returns the unchanged snapshot" % label)
|
||||
expect(result.events.is_empty(), "%s emits no event" % label)
|
||||
if not result.ok:
|
||||
rejected_commands += 1
|
||||
|
||||
|
||||
func _service(fields: Dictionary = {}):
|
||||
var service = FirstSessionServiceScript.new(FirstSessionStateScript.new())
|
||||
for field in fields:
|
||||
service.state.set(field, fields[field])
|
||||
return service
|
||||
|
||||
|
||||
func _formation_fields(screen: StringName) -> Dictionary:
|
||||
var selected: Array[String] = ["Hearthguard", "Farstring"]
|
||||
return {"screen": screen, "selected_recruits": selected, "front_choice": "Hearthguard", "rear_choice": "Farstring"}
|
||||
|
||||
|
||||
func _guild_fields(guild_id: String) -> Dictionary:
|
||||
return {"screen": Contracts.SCREEN_GUILD_RESPONSE, "coin": 20, "guild_response": guild_id, "adaptation_choice": "EQ-002", "front_choice": "Hearthguard", "rear_choice": "Farstring"}
|
||||
|
||||
|
||||
func _growth_fields(screen: StringName, member_id := "") -> Dictionary:
|
||||
return {"screen": screen, "front_choice": "Hearthguard", "rear_choice": "Farstring", "growth_member_id": member_id}
|
||||
|
||||
|
||||
func _battle_fields(screen: StringName, phase: int, playing: bool) -> Dictionary:
|
||||
return {"screen": screen, "front_choice": "Hearthguard", "rear_choice": "Farstring", "battle_phase": phase, "battle_playing": playing}
|
||||
|
||||
|
||||
func expect(condition: bool, message: String) -> void:
|
||||
if not condition:
|
||||
failures.append("P9.1 foundation atomicity: %s" % message)
|
||||
Reference in New Issue
Block a user