Files
gamedev-the-steward/tests/unit/test_regional_scheduler.gd
T
2026-08-12 20:50:44 +02:00

214 lines
8.4 KiB
GDScript

extends GutTest
func test_explicit_job_order_is_due_phase_entity_sequence() -> void:
var scheduler := RegionalJobScheduler.new()
assert_true(scheduler.schedule(ScheduledJobRecord.create(&"z", &"work", 8, 0, &"b", 2)))
assert_true(scheduler.schedule(ScheduledJobRecord.create(&"d", &"work", 7, 2, &"a", 3)))
assert_true(scheduler.schedule(ScheduledJobRecord.create(&"c", &"work", 7, 1, &"z", 1)))
assert_true(scheduler.schedule(ScheduledJobRecord.create(&"b", &"work", 7, 1, &"a", 9)))
assert_true(scheduler.schedule(ScheduledJobRecord.create(&"a", &"work", 7, 1, &"a", 4)))
assert_eq(_job_ids(scheduler.get_all_sorted()), [&"a", &"b", &"c", &"d", &"z"])
assert_eq(scheduler.get_due_count(6), 0)
assert_eq(scheduler.get_due_count(7), 4)
assert_eq(_job_ids(scheduler.take_due(7, 2)), [&"a", &"b"])
assert_eq(_job_ids(scheduler.take_due(7)), [&"c", &"d"])
assert_eq(_job_ids(scheduler.take_due(8)), [&"z"])
assert_true(scheduler.is_empty())
func test_budgets_one_eight_and_unlimited_preserve_order_and_checksum() -> void:
var budget_one := _run_fixture(1)
var budget_eight := _run_fixture(8)
var unlimited := _run_fixture(RegionalJobScheduler.UNLIMITED_BUDGET)
assert_eq(budget_one["order"], budget_eight["order"])
assert_eq(budget_one["order"], unlimited["order"])
assert_eq(budget_one["checksum"], budget_eight["checksum"])
assert_eq(budget_one["checksum"], unlimited["checksum"])
assert_eq(budget_one["order"].size(), 37)
assert_eq(budget_one["remaining"], 0)
assert_eq(budget_eight["remaining"], 0)
assert_eq(unlimited["remaining"], 0)
func test_dynamic_jobs_interleave_identically_across_execution_budgets() -> void:
var budget_one := _run_dynamic_fixture(1)
var budget_eight := _run_dynamic_fixture(8)
var unlimited := _run_dynamic_fixture(RegionalJobScheduler.UNLIMITED_BUDGET)
assert_eq(budget_one, [&"parent", &"child", &"tail"])
assert_eq(budget_eight, budget_one)
assert_eq(unlimited, budget_one)
func test_restore_mid_due_queue_continues_without_drop_or_reorder() -> void:
var uninterrupted := _fixture_scheduler()
var expected: Array[StringName] = []
while not uninterrupted.is_empty():
expected.append_array(_job_ids(uninterrupted.take_due(100, 3)))
var interrupted := _fixture_scheduler()
var actual := _job_ids(interrupted.take_due(100, 5))
var saved := interrupted.to_dictionary()
var restored := RegionalJobScheduler.from_dictionary(saved)
assert_not_null(restored)
assert_eq(restored.to_dictionary(), saved)
while not restored.is_empty():
actual.append_array(_job_ids(restored.take_due(100, 2)))
assert_eq(actual, expected)
assert_eq(actual.size(), 37)
assert_eq(actual.duplicate().reduce(_unique_id_count, {} as Dictionary).size(), 37)
assert_eq(restored.get_cursor()["executed_count"], 37)
func test_schedule_cancel_dedupe_and_repeating_jobs_preserve_queue_contract() -> void:
var scheduler := RegionalJobScheduler.new()
var repeating := scheduler.schedule_new(
&"weather#0", &"weather", 2, 0, &"jajce", {}, &"weather:jajce", 3
)
assert_not_null(repeating)
assert_null(
scheduler.schedule_new(&"duplicate", &"weather", 2, 0, &"jajce", {}, &"weather:jajce", 3)
)
assert_eq(_job_ids(scheduler.take_due(2, 1)), [&"weather#0"])
assert_true(scheduler.has_job(&"weather#1"))
assert_eq(scheduler.get_by_id(&"weather#1").get_due_tick(), 5)
assert_eq(scheduler.get_by_id(&"weather#1").get_occurrence(), 1)
assert_eq(_job_ids(scheduler.take_due(100, 2)), [&"weather#1", &"weather#2"])
assert_true(scheduler.has_job(&"weather#3"))
assert_not_null(scheduler.cancel(&"weather#3"))
assert_true(scheduler.is_empty())
assert_null(scheduler.cancel(&"missing"))
var duplicate_sequence := ScheduledJobRecord.create(&"one", &"work", 9, 0, &"a", 40)
assert_true(scheduler.schedule(duplicate_sequence))
assert_false(scheduler.schedule(ScheduledJobRecord.create(&"two", &"work", 10, 0, &"b", 40)))
func test_keyed_random_is_order_independent_and_key_sensitive() -> void:
var first := KeyedRandom.value_u32(1337, &"caravan", &"caravan_a", 4)
var unrelated := KeyedRandom.value_u32(1337, &"weather", &"jajce", 99)
var repeated := KeyedRandom.value_u32(1337, &"caravan", &"caravan_a", 4)
assert_eq(first, repeated)
assert_ne(first, unrelated)
assert_ne(first, KeyedRandom.value_u32(1338, &"caravan", &"caravan_a", 4))
assert_ne(first, KeyedRandom.value_u32(1337, &"caravan", &"caravan_b", 4))
assert_ne(first, KeyedRandom.value_u32(1337, &"caravan", &"caravan_a", 5))
assert_true(KeyedRandom.value_float(1337, &"caravan", &"a", 1) >= 0.0)
assert_true(KeyedRandom.value_float(1337, &"caravan", &"a", 1) < 1.0)
assert_true(KeyedRandom.range_int(1337, &"caravan", &"a", 1, 3, 7) in range(3, 8))
assert_true(KeyedRandom.chance(1337, &"caravan", &"a", 1, 1.0))
assert_false(KeyedRandom.chance(1337, &"caravan", &"a", 1, 0.0))
func test_analytical_elapsed_tick_updates_match_stepwise_results() -> void:
var stepwise_linear := 4.0
for _tick in range(17):
stepwise_linear = clampf(stepwise_linear + 0.75, 0.0, 20.0)
assert_almost_eq(
AnalyticalRegionalUpdates.linear_value(4.0, 0.75, 17, 0.0, 20.0), stepwise_linear, 0.000001
)
var stepwise_exponential := 100.0
for _tick in range(24):
stepwise_exponential *= 0.98
assert_almost_eq(
AnalyticalRegionalUpdates.exponential_value(100.0, 0.98, 24), stepwise_exponential, 0.000001
)
assert_eq(AnalyticalRegionalUpdates.periodic_occurrences(5, 3, 4), 0)
assert_eq(AnalyticalRegionalUpdates.periodic_occurrences(5, 3, 14), 4)
var transfer := AnalyticalRegionalUpdates.transfer_amount(12.0, 9.0, 2.0, 3, 0.25)
assert_eq(transfer["sent"], 6.0)
assert_eq(transfer["received"], 4.5)
assert_eq(transfer["remaining_available"], 6.0)
assert_eq(transfer["remaining_demand"], 4.5)
func test_record_and_scheduler_parsers_reject_invalid_state_transactionally() -> void:
var valid := ScheduledJobRecord.create(
&"job", &"work", 3, 0, &"entity", 1, {&"z": &"value", &"a": {&"b": 2, &"a": 1}}
)
assert_eq(
ScheduledJobRecord.from_dictionary(valid.to_dictionary()).to_dictionary(),
valid.to_dictionary()
)
assert_eq(valid.get_payload(), {"a": {"a": 1, "b": 2}, "z": "value"})
var invalid := valid.to_dictionary()
invalid["due_tick"] = -1
assert_null(ScheduledJobRecord.from_dictionary(invalid))
var scheduler := RegionalJobScheduler.new()
assert_true(scheduler.schedule(valid))
var before := scheduler.to_dictionary()
var malformed := before.duplicate(true)
malformed["jobs"].append(before["jobs"][0].duplicate(true))
assert_false(scheduler.restore_from_dictionary(malformed))
assert_eq(scheduler.to_dictionary(), before)
assert_eq(_job_ids(scheduler.take_due(3, 1)), [&"job"])
assert_false(scheduler.schedule(ScheduledJobRecord.create(&"past", &"work", 2, 0, &"a", 2)))
func _run_fixture(budget: int) -> Dictionary:
var scheduler := _fixture_scheduler()
var order: Array[StringName] = []
var checksum := 146959810
while not scheduler.is_empty():
var batch := scheduler.take_due(100, budget)
assert_false(batch.is_empty())
for job in batch:
order.append(job.get_job_id())
checksum = int((checksum * 16777619 + job.get_stable_sequence()) & 0x7FFFFFFF)
return {"order": order, "checksum": checksum, "remaining": scheduler.size()}
func _run_dynamic_fixture(budget: int) -> Array[StringName]:
var scheduler := RegionalJobScheduler.new()
assert_not_null(scheduler.schedule_new(&"parent", &"work", 5, 0, &"a"))
assert_not_null(scheduler.schedule_new(&"tail", &"work", 5, 2, &"z"))
var order: Array[StringName] = []
while not scheduler.is_empty():
var executed := scheduler.execute_due(
5,
budget,
func(job: ScheduledJobRecord) -> void:
order.append(job.get_job_id())
if job.get_job_id() == &"parent":
assert_not_null(scheduler.schedule_new(&"child", &"work", 5, 1, &"b"))
)
assert_false(executed.is_empty())
return order
func _fixture_scheduler() -> RegionalJobScheduler:
var scheduler := RegionalJobScheduler.new()
for index in range(37):
var due_tick := 5 + (index * 7) % 11
var phase := (index * 5) % 3
var entity_id := StringName("entity_%02d" % ((index * 13) % 9))
var job := ScheduledJobRecord.create(
StringName("job_%02d" % index),
&"fixture",
due_tick,
phase,
entity_id,
index,
{"delta": index + 1}
)
assert_true(scheduler.schedule(job))
return scheduler
func _job_ids(jobs: Array[ScheduledJobRecord]) -> Array[StringName]:
var result: Array[StringName] = []
for job in jobs:
result.append(job.get_job_id())
return result
func _unique_id_count(accumulator: Dictionary, job_id: StringName) -> Dictionary:
accumulator[job_id] = true
return accumulator