Files
TheTree_mock/tests/economy/test_p05_funds_restart_2_read.gd
T

120 lines
6.5 KiB
GDScript

extends SceneTree
## P05-02 F6, reader phase. Runs after the writer suite
## (test_p05_funds_restart_1_write.gd) as a separate engine process, loads each
## save package the writer wrote, continues the day script and requires the same
## ledger and daily closes as the uninterrupted run: no double rank tax, interest
## or top-up across mid-day, pre-00:00 and post-00:00 restarts. A changed package
## is refused.
const Fixture := preload("res://tests/helpers/p05_funds_restart_fixture.gd")
var _checks := 0
var _failures := 0
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
print("P05-02: restart reader")
var handoff := _read_handoff()
if not handoff.is_empty():
_check_restarts(handoff)
await process_frame
if _failures == 0:
print("PASS: %d checks" % _checks)
else:
push_error("FAIL: %d of %d checks failed" % [_failures, _checks])
quit(_failures)
func _read_handoff() -> Dictionary:
_expect(FileAccess.file_exists(Fixture.HANDOFF_PATH), "the writer phase left its handoff (run test_p05_funds_restart_1_write.gd first)")
if not FileAccess.file_exists(Fixture.HANDOFF_PATH):
return {}
var parsed: Variant = JSON.parse_string(FileAccess.get_file_as_string(Fixture.HANDOFF_PATH))
_expect(parsed is Dictionary and str((parsed as Dictionary).get("schema", "")) == Fixture.HANDOFF_SCHEMA, "handoff schema")
if not parsed is Dictionary or str((parsed as Dictionary).get("schema", "")) != Fixture.HANDOFF_SCHEMA:
return {}
var handoff: Dictionary = parsed
var writer_pid := int(handoff.get("writer_pid", 0))
_expect(writer_pid > 0 and writer_pid != OS.get_process_id(), "a separate engine process wrote the saves")
_expect(str(handoff.get("writer_source_sha256", "")) == FileAccess.get_sha256(Fixture.WRITER_SOURCE), "the handoff comes from the current writer suite")
_expect(int(handoff.get("seed", -1)) == Fixture.SEED and int(handoff.get("last_minute", -1)) == Fixture.LAST_MINUTE, "same fixture and day script")
print("P05_RESTART_READER pid=%d writer_pid=%d handoff_sha256=%s" % [OS.get_process_id(), writer_pid, FileAccess.get_sha256(Fixture.HANDOFF_PATH)])
return handoff
func _check_restarts(handoff: Dictionary) -> void:
var fixtures := Fixture.new(_expect)
var straight := fixtures.new_fixture()
fixtures.play(straight, 0, Fixture.LAST_MINUTE, Callable())
var straight_runtime: Variant = straight[&"runtime"]
var straight_economy := fixtures.economy_bytes(straight_runtime)
_expect(Fixture.sha256(straight_economy) == str(handoff.get("straight_economy_sha256", "")), "the uninterrupted day script gives the same ledger in both processes")
var writer_days: Variant = bytes_to_var(Marshalls.base64_to_raw(str(handoff.get("straight_days_base64", ""))))
_expect(fixtures.days_equal(straight_runtime, writer_days), "the uninterrupted day script gives the same daily closes in both processes")
var saves: Array = handoff.get("saves", [])
var minutes: Array[int] = []
for save: Variant in saves:
minutes.append(int((save as Dictionary).get("minute", -1)) if save is Dictionary else -1)
_expect(minutes == Fixture.SAVE_MINUTES, "the writer saved mid-day, before and after the day 2 00:00 close")
for save: Variant in saves:
if not save is Dictionary:
continue
var minute := int((save as Dictionary).get("minute", -1))
var path := Fixture.READER_SAVE % minute
if not _materialize(path, (save as Dictionary).get("files", {}), false):
continue
var resumed := fixtures.new_fixture()
var runtime: Variant = resumed[&"runtime"]
_expect(bool(runtime.load_from_path(path).get(&"ok", false)), "this process loads the save written at minute %d" % minute)
_expect(int(resumed[&"clock"].absolute_minute) == minute, "the loaded save resumes at minute %d" % minute)
fixtures.play(resumed, minute, Fixture.LAST_MINUTE, Callable())
_expect(fixtures.economy_bytes(runtime) == straight_economy, "resuming at minute %d gives exactly the uninterrupted ledger" % minute)
_expect(fixtures.days_equal(runtime, writer_days), "resuming at minute %d gives the same daily closes, fund reports and arrears" % minute)
# Day 1: tax and interest; day 2: interest; day 3: tax and interest.
_expect(fixtures.category_count(runtime, [FinancePolicy.RANK_TAX_CATEGORY, FinancePolicy.LOAN_INTEREST_CATEGORY]) == 5, "minute %d: each tax and interest accrual is posted once" % minute)
_expect(fixtures.category_count(runtime, [FinancePolicy.TOP_UP_CATEGORY]) == 1, "minute %d: the day 2 top-up is posted once" % minute)
_expect(runtime.economy_service.get_balance(Fixture.FRONT) == 35 and runtime.economy_service.get_balance(Fixture.BACK) == 695, "minute %d: front 35 and hidden 695" % minute)
fixtures.free_fixture(resumed)
if not saves.is_empty() and saves[0] is Dictionary and _materialize(Fixture.READER_TAMPERED_SAVE, (saves[0] as Dictionary).get("files", {}), true):
var fixture := fixtures.new_fixture()
var runtime: Variant = fixture[&"runtime"]
var before := fixtures.economy_bytes(runtime)
_expect(not bool(runtime.load_from_path(Fixture.READER_TAMPERED_SAVE).get(&"ok", true)), "a changed save package is refused")
_expect(fixtures.economy_bytes(runtime) == before and int(fixture[&"clock"].absolute_minute) == 360, "a refused load changes no Gold and no time")
fixtures.free_fixture(fixture)
fixtures.free_fixture(straight)
# Writes the writer's exact package bytes into this process's own profile.
func _materialize(path: String, files: Variant, change_world: bool) -> bool:
var directory := ProjectSettings.globalize_path(path)
_expect(files is Dictionary and (files as Dictionary).has("manifest.json") and (files as Dictionary).has("world.bin"), "handoff package %s is complete" % path)
if not files is Dictionary:
return false
_expect(DirAccess.make_dir_recursive_absolute(directory) == OK, "package directory %s" % path)
for name: String in (files as Dictionary):
var pin: Dictionary = (files as Dictionary)[name]
var bytes := Marshalls.base64_to_raw(str(pin.get("base64", "")))
_expect(Fixture.sha256(bytes) == str(pin.get("sha256", "")) and bytes.size() == int(pin.get("bytes", -1)), "handoff bytes of %s are exact" % name)
if change_world and name == "world.bin" and not bytes.is_empty():
bytes[bytes.size() - 1] = bytes[bytes.size() - 1] ^ 0xff
var file := FileAccess.open(directory.path_join(name), FileAccess.WRITE)
_expect(file != null, "write %s into %s" % [name, path])
if file == null:
return false
file.store_buffer(bytes)
file.close()
return true
func _expect(condition: bool, message: String) -> void:
_checks += 1
if not condition:
_failures += 1
push_error(message)