Files
TheTree_mock/tests/environment/test_p07_weather_restart_1_write.gd

211 lines
12 KiB
GDScript

extends SceneTree
## P07-01 F7, writer phase. Plays the minute script, writes ordinary save files
## mid-rain, just before and after the rain end and just before and after the
## day 1 draw, and hands the exact save packages to the reader suite
## (test_p07_weather_restart_2_read.gd), which runs as a separate engine process.
## The fog scene (WX7) writes saves around the 07:00 fog mana boundary and the
## fog end, reloads each one in this process and continues it, and hands those
## packages to the reader too.
## The handoff is the declared generated report res://artifacts/p07-01/restart-handoff.json
## (tools/regression/suite-policy.json fixed_generated_report_routes).
const Fixture := preload("res://tests/helpers/p07_weather_restart_fixture.gd")
var _checks := 0
var _failures := 0
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
print("P07-01: restart writer")
var fixtures := Fixture.new(_expect)
var straight := fixtures.new_fixture()
var straight_runtime: Variant = straight[&"runtime"]
var held_guest: Variant = straight_runtime.guest_service.get_guest(Fixture.HELD_GUEST)
var held := {"hold": {}, "reason": &"", "after": {}}
fixtures.play(straight, Fixture.FIRST_MINUTE, Fixture.LAST_MINUTE, func(minute: int) -> void:
if (held["hold"] as Dictionary).is_empty():
held["hold"] = straight_runtime.weather.get_rain_hold(Fixture.HELD_GUEST)
if StringName(held["reason"]).is_empty():
held["reason"] = held_guest.exit_reason
if minute == 363:
_expect(straight_runtime.guest_service.get_guest(Fixture.HELD_GUEST) != null and held_guest.state == GuestRuntime.State.LEAVING
and straight_runtime.weather.get_rain_hold(Fixture.HELD_GUEST) == Fixture.HOLD, "at 06:03 the held guest is still present and leaving with its hold")
if minute == 364:
_expect(straight_runtime.guest_service.get_guest(Fixture.HELD_GUEST) == null and straight_runtime.weather.get_rain_holds().is_empty(),
"at 06:04 the held guest is gone and its hold left with it")
if minute - 1 in Fixture.SAVE_MINUTES:
held["after"][minute] = fixtures.state_bytes(straight_runtime))
# The hold only moves the planned exit; every other current rule still applies,
# so in this world without facilities the guest leaves for lack of one.
_expect(held["hold"] == Fixture.HOLD and held_guest.planned_exit_at == 602 and held["reason"] == &"NO_SUITABLE_FACILITY",
"the guest due at 06:02 was held to 10:02 and still left by the current rules (%s)" % String(held["reason"]))
_expect(straight[&"draws"][&"count"] == Fixture.DRAW_MINUTES.size() and straight_runtime.weather.get_last_draw_day() == 1, "the uninterrupted script draws day 1 once")
var saving := fixtures.new_fixture()
var saving_runtime: Variant = saving[&"runtime"]
var saved_states := {}
var saved_paths := {}
fixtures.play(saving, Fixture.FIRST_MINUTE, Fixture.LAST_MINUTE, func(minute: int) -> void:
if minute in Fixture.SAVE_MINUTES:
if minute == 363:
_expect(saving_runtime.guest_service.get_guest(Fixture.HELD_GUEST) != null and saving_runtime.weather.get_rain_holds() == {String(Fixture.HELD_GUEST): Fixture.HOLD}
and saving_runtime.guest_service.get_guest(Fixture.HELD_GUEST).planned_exit_at == 602, "minute 363 saves the hold of the present held guest")
if minute == 364:
_expect(saving_runtime.guest_service.get_guest(Fixture.HELD_GUEST) == null and saving_runtime.weather.get_rain_holds().is_empty(), "minute 364 saves just after the held guest left, without its hold")
if minute == 500:
_expect(saving_runtime.weather.get_kind() == &"rain", "minute 500 saves mid-rain")
var path := Fixture.WRITER_SAVE % minute
saved_states[minute] = fixtures.state_bytes(saving_runtime)
_expect(bool(saving_runtime.save_to_path(path).get(&"ok", false)), "save file at minute %d" % minute)
saved_paths[minute] = path)
_expect(fixtures.state_bytes(saving_runtime) == fixtures.state_bytes(straight_runtime), "writing the save files leaves the weather state as without saving")
var final_state := fixtures.state_bytes(straight_runtime)
for minute: int in Fixture.SAVE_MINUTES:
var reload := fixtures.new_fixture(false)
var reload_runtime: Variant = reload[&"runtime"]
_expect(bool(reload_runtime.load_from_path(str(saved_paths.get(minute, Fixture.WRITER_SAVE % minute))).get(&"ok", false))
and fixtures.state_bytes(reload_runtime) == saved_states.get(minute, PackedByteArray()) and reload[&"clock"].absolute_minute == minute,
"this process reloads the save file of minute %d exactly" % minute)
if minute == 363:
_expect(reload_runtime.weather.get_rain_holds() == {String(Fixture.HELD_GUEST): Fixture.HOLD} and reload_runtime.guest_service.get_guest(Fixture.HELD_GUEST).planned_exit_at == 602,
"the reloaded minute 363 save keeps the one hold and the extended plan")
fixtures.play(reload, minute, minute + 1, Callable())
_expect(fixtures.state_bytes(reload_runtime) == held["after"].get(minute + 1, PackedByteArray()),
"one minute after the minute %d reload equals the uninterrupted run (no second extension)" % minute)
fixtures.play(reload, minute + 1, Fixture.LAST_MINUTE, Callable())
_expect(fixtures.state_bytes(reload_runtime) == final_state, "the minute %d reload continues to the uninterrupted final state" % minute)
fixtures.free_fixture(reload)
var saves := _packages(Fixture.SAVE_MINUTES, saved_paths, saved_states, Fixture.WRITER_SAVE)
var after := {}
for minute: int in held["after"]:
var bytes: PackedByteArray = held["after"][minute]
after[str(minute)] = {"sha256": Fixture.sha256(bytes), "base64": Marshalls.raw_to_base64(bytes)}
var fog := _fog_scene(fixtures)
var handoff := {
"schema": Fixture.HANDOFF_SCHEMA,
"writer_pid": OS.get_process_id(),
"writer_source_sha256": FileAccess.get_sha256(Fixture.WRITER_SOURCE),
"seed": Fixture.SEED,
"last_minute": Fixture.LAST_MINUTE,
"straight_final_state_sha256": Fixture.sha256(final_state),
"straight_final_state_base64": Marshalls.raw_to_base64(final_state),
"after_states": after,
"saves": saves,
"fog": fog,
}
DirAccess.make_dir_recursive_absolute(Fixture.HANDOFF_PATH.get_base_dir())
var file := FileAccess.open(Fixture.HANDOFF_PATH, FileAccess.WRITE)
_expect(file != null, "handoff file opens for writing")
if file != null:
file.store_string(JSON.stringify(handoff, "\t") + "\n")
file.close()
print("P07_RESTART_WRITER pid=%d handoff_sha256=%s" % [OS.get_process_id(), FileAccess.get_sha256(Fixture.HANDOFF_PATH)])
fixtures.free_fixture(straight)
fixtures.free_fixture(saving)
await process_frame
if _failures == 0:
print("PASS: %d checks" % _checks)
else:
push_error("FAIL: %d of %d checks failed" % [_failures, _checks])
quit(_failures)
## WX7 fog scene: the uninterrupted run, the run that writes the save files, and
## a same-process reload of every save file continued to the end.
func _fog_scene(fixtures: Variant) -> Dictionary:
var straight: Dictionary = fixtures.new_fixture(false, Fixture.FOG_SEED)
var runtime: Variant = straight[&"runtime"]
_expect(runtime.weather.get_kind() == &"mana_fog" and int(runtime.weather.get_today_draw().get("end", -1)) == 600, "seed 1592 opens the fog scene with mana fog 06:00-10:00")
var seen := {"intimacy": {}, "rises": 0, "fog_rises": 0, "rises_ok": true, "flows": 0, "fog_flows": 0, "flows_ok": true, "after": {}}
fixtures.play(straight, Fixture.FIRST_MINUTE, Fixture.FOG_LAST_MINUTE, func(minute: int) -> void:
var fog_now: bool = runtime.weather.get_kind() == &"mana_fog"
var last: Dictionary = seen["intimacy"]
for guest_id: StringName in Fixture.FOG_ADULTS.values():
var guest: Variant = runtime.guest_service.get_guest(guest_id)
if guest == null:
continue
var now := int(guest.needs[&"intimacy"])
if last.has(guest_id) and now < 100:
seen["rises"] += 1
seen["rises_ok"] = seen["rises_ok"] and now - int(last[guest_id]) == (4 if fog_now else 3)
if fog_now:
seen["fog_rises"] += 1
last[guest_id] = now
if minute % 10 == 0:
seen["flows"] += 1
seen["flows_ok"] = seen["flows_ok"] and int(runtime.mana_reserve.get_last_flow().get(&"generation", -1)) == (24 if fog_now else 20)
if fog_now:
seen["fog_flows"] += 1
if minute - 1 in Fixture.FOG_SAVE_MINUTES:
seen["after"][minute] = fixtures.state_bytes(runtime))
_expect(seen["rises_ok"] and int(seen["fog_rises"]) >= 3 and int(seen["rises"]) - int(seen["fog_rises"]) >= 2,
"adult-verified intimacy rises by 4 a minute in fog and 3 after it (%d rises, %d in fog)" % [int(seen["rises"]), int(seen["fog_rises"])])
_expect(seen["flows_ok"] and int(seen["fog_flows"]) >= 20 and int(seen["flows"]) > int(seen["fog_flows"]),
"each 10-minute mana boundary adds 24 in fog and 20 after it (%d boundaries, %d in fog)" % [int(seen["flows"]), int(seen["fog_flows"])])
var saving: Dictionary = fixtures.new_fixture(false, Fixture.FOG_SEED)
var saving_runtime: Variant = saving[&"runtime"]
var states := {}
var paths := {}
fixtures.play(saving, Fixture.FIRST_MINUTE, Fixture.FOG_LAST_MINUTE, func(minute: int) -> void:
if minute in Fixture.FOG_SAVE_MINUTES:
var path := Fixture.FOG_WRITER_SAVE % minute
states[minute] = fixtures.state_bytes(saving_runtime)
_expect(bool(saving_runtime.save_to_path(path).get(&"ok", false)), "fog save file at minute %d" % minute)
paths[minute] = path)
var final_state: PackedByteArray = fixtures.state_bytes(runtime)
_expect(fixtures.state_bytes(saving_runtime) == final_state, "writing the fog save files leaves the state as without saving")
for minute: int in Fixture.FOG_SAVE_MINUTES:
var reload: Dictionary = fixtures.new_fixture(false, Fixture.FOG_SEED)
var reload_runtime: Variant = reload[&"runtime"]
_expect(bool(reload_runtime.load_from_path(str(paths.get(minute, Fixture.FOG_WRITER_SAVE % minute))).get(&"ok", false))
and fixtures.state_bytes(reload_runtime) == states.get(minute, PackedByteArray()) and reload[&"clock"].absolute_minute == minute,
"this process reloads the fog save file of minute %d exactly" % minute)
fixtures.play(reload, minute, minute + 1, Callable())
_expect(fixtures.state_bytes(reload_runtime) == seen["after"].get(minute + 1, PackedByteArray()),
"one minute after the minute %d fog reload equals the uninterrupted run (the fog yield and intimacy apply once)" % minute)
fixtures.play(reload, minute + 1, Fixture.FOG_LAST_MINUTE, Callable())
_expect(fixtures.state_bytes(reload_runtime) == final_state, "the minute %d fog reload continues to the uninterrupted final state" % minute)
fixtures.free_fixture(reload)
var after := {}
for minute: int in seen["after"]:
var bytes: PackedByteArray = seen["after"][minute]
after[str(minute)] = {"sha256": Fixture.sha256(bytes), "base64": Marshalls.raw_to_base64(bytes)}
var saves := _packages(Fixture.FOG_SAVE_MINUTES, paths, states, Fixture.FOG_WRITER_SAVE)
fixtures.free_fixture(straight)
fixtures.free_fixture(saving)
return {
"seed": Fixture.FOG_SEED,
"last_minute": Fixture.FOG_LAST_MINUTE,
"straight_final_state_sha256": Fixture.sha256(final_state),
"straight_final_state_base64": Marshalls.raw_to_base64(final_state),
"after_states": after,
"saves": saves,
}
## The exact save packages and the compared state at each save minute.
func _packages(minutes: Array[int], paths: Dictionary, states: Dictionary, path_format: String) -> Array:
var saves: Array = []
for minute: int in minutes:
var directory := ProjectSettings.globalize_path(str(paths.get(minute, path_format % minute)))
_expect(DirAccess.dir_exists_absolute(directory) and DirAccess.get_directories_at(directory).is_empty(), "save package %s holds only files" % (path_format % minute))
var files := {}
for name: String in DirAccess.get_files_at(directory):
var bytes := FileAccess.get_file_as_bytes(directory.path_join(name))
files[name] = {"sha256": Fixture.sha256(bytes), "bytes": bytes.size(), "base64": Marshalls.raw_to_base64(bytes)}
_expect(files.has("manifest.json") and files.has("world.bin"), "save package %s has its manifest and world" % (path_format % minute))
var state: PackedByteArray = states.get(minute, PackedByteArray())
saves.append({"minute": minute, "files": files, "state_sha256": Fixture.sha256(state), "state_base64": Marshalls.raw_to_base64(state)})
return saves
func _expect(condition: bool, message: String) -> void:
_checks += 1
if not condition:
_failures += 1
push_error(message)