281 lines
10 KiB
GDScript
281 lines
10 KiB
GDScript
extends GutTest
|
|
|
|
const MANAGER_SCRIPT := preload("res://simulation/SimulationManager.gd")
|
|
|
|
var manager: Node
|
|
|
|
|
|
func before_each() -> void:
|
|
manager = MANAGER_SCRIPT.new()
|
|
add_child_autofree(manager)
|
|
|
|
|
|
func test_canonical_manifest_round_trips_defensively() -> void:
|
|
var local_state: SimulationStateRecord = manager.create_state_record()
|
|
var regional_facade := RegionalSimulationFacade.create_jajce_route()
|
|
var manifest := SimulationSaveManifest.create(local_state, regional_facade)
|
|
assert_not_null(manifest)
|
|
|
|
var saved := manifest.to_dictionary()
|
|
var expected_fields := [
|
|
"local_checksum",
|
|
"local_state",
|
|
"regional_checksum",
|
|
"regional_state",
|
|
"schema",
|
|
"schema_version",
|
|
]
|
|
var saved_fields := saved.keys()
|
|
saved_fields.sort()
|
|
assert_eq(saved_fields, expected_fields)
|
|
assert_eq(saved["schema"], SimulationSaveManifest.SCHEMA_NAME)
|
|
assert_eq(saved["schema_version"], SimulationSaveManifest.SCHEMA_VERSION)
|
|
assert_eq(
|
|
saved["local_checksum"],
|
|
SimulationSaveManifest._checksum_dictionary(saved["local_state"]),
|
|
)
|
|
assert_eq(
|
|
saved["regional_checksum"],
|
|
SimulationSaveManifest._checksum_dictionary(saved["regional_state"]),
|
|
)
|
|
|
|
var restored := SimulationSaveManifest.from_dictionary(saved)
|
|
var restored_json := SimulationSaveManifest.from_json(manifest.to_json())
|
|
assert_not_null(restored)
|
|
assert_not_null(restored_json)
|
|
assert_eq(restored.to_dictionary(), saved)
|
|
assert_eq(restored.checksum(), manifest.checksum())
|
|
assert_eq(restored_json.checksum(), manifest.checksum())
|
|
assert_not_same(manifest.get_local_state(), local_state)
|
|
assert_not_same(manifest.get_regional_facade(), regional_facade)
|
|
|
|
local_state.simulation["tick_count"] = 99
|
|
var copied_local := manifest.get_local_state()
|
|
copied_local.simulation["tick_count"] = 77
|
|
assert_eq(int(manifest.get_local_state().simulation["tick_count"]), 0)
|
|
var copied_regional := manifest.get_regional_facade().to_dictionary()
|
|
copied_regional["last_processed_tick"] = 88
|
|
assert_eq(manifest.get_regional_facade().get_last_processed_tick(), 0)
|
|
|
|
|
|
func test_parser_rejects_missing_extra_and_wrong_manifest_fields() -> void:
|
|
var valid := _valid_manifest_dictionary()
|
|
for field: String in SimulationSaveManifest.MANIFEST_FIELDS:
|
|
var missing := valid.duplicate(true)
|
|
missing.erase(field)
|
|
assert_null(SimulationSaveManifest.from_dictionary(missing), "Missing %s" % field)
|
|
|
|
var extra := valid.duplicate(true)
|
|
extra["unexpected"] = true
|
|
assert_null(SimulationSaveManifest.from_dictionary(extra))
|
|
|
|
var wrong_schema := valid.duplicate(true)
|
|
wrong_schema["schema"] = &"the_steward.save_manifest"
|
|
assert_null(SimulationSaveManifest.from_dictionary(wrong_schema))
|
|
var wrong_version := valid.duplicate(true)
|
|
wrong_version["schema_version"] = "1"
|
|
assert_null(SimulationSaveManifest.from_dictionary(wrong_version))
|
|
var wrong_float_version := valid.duplicate(true)
|
|
wrong_float_version["schema_version"] = 1.0
|
|
assert_null(SimulationSaveManifest.from_dictionary(wrong_float_version))
|
|
var wrong_local := valid.duplicate(true)
|
|
wrong_local["local_state"] = []
|
|
assert_null(SimulationSaveManifest.from_dictionary(wrong_local))
|
|
var wrong_regional := valid.duplicate(true)
|
|
wrong_regional["regional_state"] = "regional"
|
|
assert_null(SimulationSaveManifest.from_dictionary(wrong_regional))
|
|
var wrong_local_checksum := valid.duplicate(true)
|
|
wrong_local_checksum["local_checksum"] = 1
|
|
assert_null(SimulationSaveManifest.from_dictionary(wrong_local_checksum))
|
|
var wrong_regional_checksum := valid.duplicate(true)
|
|
wrong_regional_checksum["regional_checksum"] = []
|
|
assert_null(SimulationSaveManifest.from_dictionary(wrong_regional_checksum))
|
|
|
|
assert_null(SimulationSaveManifest.from_json("[]"))
|
|
assert_null(SimulationSaveManifest.from_json("not json"))
|
|
|
|
|
|
func test_nested_tampering_and_checksum_mismatches_fail_closed() -> void:
|
|
var valid := _valid_manifest_dictionary()
|
|
var local_checksum_mismatch := valid.duplicate(true)
|
|
local_checksum_mismatch["local_checksum"] = "0".repeat(64)
|
|
assert_null(SimulationSaveManifest.from_dictionary(local_checksum_mismatch))
|
|
|
|
var regional_checksum_mismatch := valid.duplicate(true)
|
|
regional_checksum_mismatch["regional_checksum"] = "f".repeat(64)
|
|
assert_null(SimulationSaveManifest.from_dictionary(regional_checksum_mismatch))
|
|
|
|
var tampered_local := valid.duplicate(true)
|
|
tampered_local["local_state"]["simulation"]["seed"] = 4004
|
|
assert_null(SimulationSaveManifest.from_dictionary(tampered_local))
|
|
|
|
var tampered_regional := valid.duplicate(true)
|
|
tampered_regional["regional_state"]["last_processed_tick"] = 1
|
|
assert_null(SimulationSaveManifest.from_dictionary(tampered_regional))
|
|
|
|
var nested_extra := valid.duplicate(true)
|
|
nested_extra["local_state"]["unexpected"] = true
|
|
nested_extra["local_checksum"] = SimulationSaveManifest._checksum_dictionary(
|
|
nested_extra["local_state"]
|
|
)
|
|
assert_null(SimulationSaveManifest.from_dictionary(nested_extra))
|
|
assert_null(SimulationSaveManifest.from_json(JSON.stringify(nested_extra)))
|
|
|
|
|
|
func test_local_seed_and_tick_require_exact_integer_types() -> void:
|
|
var valid := _valid_manifest_dictionary()
|
|
var invalid_boundaries: Array[Dictionary] = [
|
|
{"field": "seed", "value": "1337"},
|
|
{"field": "seed", "value": 1337.5},
|
|
{"field": "tick_count", "value": "0"},
|
|
{"field": "tick_count", "value": 0.5},
|
|
]
|
|
|
|
for invalid_boundary: Dictionary in invalid_boundaries:
|
|
var field := String(invalid_boundary["field"])
|
|
var value: Variant = invalid_boundary["value"]
|
|
var malformed := valid.duplicate(true)
|
|
malformed["local_state"]["simulation"][field] = value
|
|
malformed["local_checksum"] = SimulationSaveManifest._checksum_dictionary(
|
|
malformed["local_state"]
|
|
)
|
|
assert_null(
|
|
SimulationSaveManifest.from_dictionary(malformed),
|
|
"Dictionary parser should reject %s=%s" % [field, value],
|
|
)
|
|
assert_null(
|
|
SimulationSaveManifest.from_json(JSON.stringify(malformed)),
|
|
"JSON parser should reject %s=%s" % [field, value],
|
|
)
|
|
|
|
var local_state: SimulationStateRecord = manager.create_state_record()
|
|
local_state.simulation[field] = value
|
|
assert_null(
|
|
SimulationSaveManifest.create(
|
|
local_state, RegionalSimulationFacade.create_jajce_route()
|
|
),
|
|
"Manifest creation should reject %s=%s" % [field, value],
|
|
)
|
|
|
|
|
|
func test_json_parser_rejects_nonnumeric_regional_ledger_values() -> void:
|
|
var string_ledger := _valid_manifest_dictionary()
|
|
_set_regional_stockpile_value(
|
|
string_ledger["regional_state"],
|
|
RegionalSimulationFacade.SETTLEMENT_TRAVNIK,
|
|
"4",
|
|
)
|
|
assert_null(SimulationSaveManifest.from_json(JSON.stringify(string_ledger)))
|
|
|
|
var boolean_ledger := _valid_manifest_dictionary()
|
|
_set_regional_stockpile_value(
|
|
boolean_ledger["regional_state"],
|
|
RegionalSimulationFacade.SETTLEMENT_JAJCE,
|
|
false,
|
|
)
|
|
var canonical_regional_state: Dictionary = boolean_ledger["regional_state"].duplicate(true)
|
|
_set_regional_stockpile_value(
|
|
canonical_regional_state,
|
|
RegionalSimulationFacade.SETTLEMENT_JAJCE,
|
|
0.0,
|
|
)
|
|
boolean_ledger["regional_checksum"] = SimulationSaveManifest._checksum_dictionary(
|
|
canonical_regional_state
|
|
)
|
|
assert_null(SimulationSaveManifest.from_json(JSON.stringify(boolean_ledger)))
|
|
|
|
|
|
func test_world_location_and_tick_must_match_the_regional_boundary() -> void:
|
|
var valid := _valid_manifest_dictionary()
|
|
|
|
var wrong_world := valid.duplicate(true)
|
|
wrong_world["local_state"]["simulation"]["world_id"] = "regional_elsewhere"
|
|
wrong_world["local_checksum"] = SimulationSaveManifest._checksum_dictionary(
|
|
wrong_world["local_state"]
|
|
)
|
|
assert_null(SimulationSaveManifest.from_dictionary(wrong_world))
|
|
|
|
var wrong_location := valid.duplicate(true)
|
|
wrong_location["local_state"]["simulation"]["location_id"] = String(
|
|
RegionalSimulationFacade.LOCATION_TRAVNIK
|
|
)
|
|
wrong_location["local_checksum"] = SimulationSaveManifest._checksum_dictionary(
|
|
wrong_location["local_state"]
|
|
)
|
|
assert_null(SimulationSaveManifest.from_dictionary(wrong_location))
|
|
|
|
var wrong_seed := valid.duplicate(true)
|
|
wrong_seed["local_state"]["simulation"]["seed"] = (
|
|
RegionalSimulationFacade.DEFAULT_WORLD_SEED + 1
|
|
)
|
|
wrong_seed["local_checksum"] = SimulationSaveManifest._checksum_dictionary(
|
|
wrong_seed["local_state"]
|
|
)
|
|
assert_null(SimulationSaveManifest.from_dictionary(wrong_seed))
|
|
|
|
var wrong_regional_seed := valid.duplicate(true)
|
|
wrong_regional_seed["regional_state"]["world_seed"] = (
|
|
RegionalSimulationFacade.DEFAULT_WORLD_SEED + 1
|
|
)
|
|
wrong_regional_seed["regional_checksum"] = SimulationSaveManifest._checksum_dictionary(
|
|
wrong_regional_seed["regional_state"]
|
|
)
|
|
assert_null(SimulationSaveManifest.from_dictionary(wrong_regional_seed))
|
|
|
|
var wrong_tick := valid.duplicate(true)
|
|
wrong_tick["local_state"]["simulation"]["tick_count"] = 1
|
|
wrong_tick["local_checksum"] = SimulationSaveManifest._checksum_dictionary(
|
|
wrong_tick["local_state"]
|
|
)
|
|
assert_null(SimulationSaveManifest.from_dictionary(wrong_tick))
|
|
assert_null(
|
|
SimulationSaveManifest.create(
|
|
manager.create_state_record(),
|
|
RegionalSimulationFacade.create_jajce_route(
|
|
RegionalSimulationFacade.DEFAULT_WORLD_SEED, 1
|
|
)
|
|
)
|
|
)
|
|
assert_null(
|
|
SimulationSaveManifest.create(
|
|
manager.create_state_record(),
|
|
RegionalSimulationFacade.create_jajce_route(
|
|
RegionalSimulationFacade.DEFAULT_WORLD_SEED + 1, 0
|
|
)
|
|
)
|
|
)
|
|
var negative_seed_local: SimulationStateRecord = manager.create_state_record()
|
|
negative_seed_local.simulation["seed"] = -42
|
|
assert_not_null(
|
|
SimulationSaveManifest.create(
|
|
negative_seed_local, RegionalSimulationFacade.create_jajce_route(-42, 0)
|
|
)
|
|
)
|
|
|
|
manager.simulate_tick()
|
|
var synchronized := SimulationSaveManifest.create(
|
|
manager.create_state_record(),
|
|
RegionalSimulationFacade.create_jajce_route(RegionalSimulationFacade.DEFAULT_WORLD_SEED, 1)
|
|
)
|
|
assert_not_null(synchronized)
|
|
assert_eq(synchronized.get_regional_facade().get_last_processed_tick(), 1)
|
|
assert_eq(int(synchronized.get_local_state().simulation["tick_count"]), 1)
|
|
|
|
|
|
func _valid_manifest_dictionary() -> Dictionary:
|
|
var manifest := SimulationSaveManifest.create(
|
|
manager.create_state_record(), RegionalSimulationFacade.create_jajce_route()
|
|
)
|
|
assert_not_null(manifest)
|
|
return manifest.to_dictionary()
|
|
|
|
|
|
func _set_regional_stockpile_value(
|
|
regional_state: Dictionary, settlement_id: StringName, value: Variant
|
|
) -> void:
|
|
for settlement: Dictionary in regional_state["caravan_service"]["world_state"]["settlements"]:
|
|
if StringName(settlement["settlement_id"]) == settlement_id:
|
|
settlement["stockpile_ledger"][String(RegionalSimulationFacade.TRADE_GOODS)] = value
|
|
return
|
|
assert_true(false, "Expected regional settlement '%s'" % settlement_id)
|