214 lines
8.4 KiB
GDScript
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
|