571 lines
24 KiB
GDScript
571 lines
24 KiB
GDScript
extends SceneTree
|
||
## Headless-тест часа: старт, спавн, буфер, таймауты, модалки, отправка,
|
||
## выплата, атаки и полный час на ускоренном времени.
|
||
## Запуск: godot --headless --path . --script tests/test_hour.gd
|
||
|
||
const HD := preload("res://scripts/core/hour_director.gd")
|
||
const AD := preload("res://scripts/systems/attack_director.gd")
|
||
|
||
var _checks: int = 0
|
||
var _fails: int = 0
|
||
var _case: String = ""
|
||
var _game = null
|
||
var _hd = null
|
||
var _flip: Dictionary = {}
|
||
var _hour_report: Dictionary = {}
|
||
var _hour_events: int = 0
|
||
var _frozen_events: int = 0
|
||
var _name_seq: int = 0
|
||
|
||
|
||
func _init() -> void:
|
||
await process_frame
|
||
_game = root.get_node_or_null("Game")
|
||
if _game == null:
|
||
_fail("autoload Game недоступен")
|
||
_finish()
|
||
return
|
||
_game.start_new_run(1)
|
||
_hd = HD.new()
|
||
_test_start_hour()
|
||
_test_spawn_and_buffer()
|
||
_test_receive()
|
||
_test_timeouts()
|
||
_test_failed_file_cancel()
|
||
_test_modal_freeze()
|
||
_test_send_payout()
|
||
_test_auto_send()
|
||
_test_required_counts()
|
||
_test_attacks()
|
||
_test_connection_failure()
|
||
_test_full_hour_timescale()
|
||
_finish()
|
||
|
||
|
||
func _test_start_hour() -> void:
|
||
_set_case("start_hour")
|
||
_game.start_new_run(1)
|
||
_hd.start_hour(1)
|
||
_eq(_hd.hour, 1, "hour = 1")
|
||
_eq(_hd.time_left, 300.0, "длительность часа 300 с")
|
||
_ok(_hd.running, "час запущен")
|
||
_eq(_hd.active_packets.size(), 0, "активных пакетов нет")
|
||
_eq(_hd.active_requests.size(), 0, "активных запросов нет")
|
||
_eq(_hd._buffer_limit(), 4, "буфер 4 на старте")
|
||
_approx(_hd._recv_interval(), 12.0, 0.0001, "recv_interval часа 1")
|
||
_eq(_hd._send_interval(), 22.0, "send_interval часа 1")
|
||
_eq(_hd._max_requests(), 2, "max_requests часа 1")
|
||
_eq(_hd.slots.max_slots(), 2, "2 слота соединений")
|
||
_ok(_hd.attack_director != null, "AttackDirector создан")
|
||
_eq(_hd.attack_director.active_attacks.size(), 0, "атаки сброшены")
|
||
_approx(_hd.channel_load(), 0.0, 0.0001, "idle-канал не нагружен")
|
||
_eq(_hd._type_pool(), ["text"], "час 1: открыт только text (sound с часа 2)")
|
||
|
||
|
||
func _test_spawn_and_buffer() -> void:
|
||
_set_case("spawn/buffer")
|
||
_hd.start_hour(1)
|
||
var ids: Array[String] = []
|
||
for i in range(4):
|
||
var id := str(_hd.spawn_packet_now("text", "tcp"))
|
||
ids.append(id)
|
||
_ok(id != "", "пакет %d заспавнен" % i)
|
||
_eq(_hd.active_packets.size(), 4, "буфер заполнен до 4")
|
||
_eq(_hd._buffer_used(), 4, "buffer_used = 4")
|
||
_approx(_hd.channel_load(), 0.0, 0.0001, "waiting-пакеты не грузят канал")
|
||
_eq(_hd._active_weights().size(), 0, "active_weights пуст без loading/sending")
|
||
var packet = _hd.active_packets[ids[0]]
|
||
_approx(float(packet.timer_total), 30.0, 0.0001, "таймер пакета часа 1: 30 с (grace 1.5)")
|
||
_eq(str(packet.protocol), "tcp", "протокол tcp")
|
||
_eq(str(packet.state), "waiting", "состояние waiting")
|
||
|
||
# Полный буфер останавливает обычный спавн по таймеру.
|
||
_frozen_events = 0
|
||
_hd.spawn_frozen.connect(_on_spawn_frozen)
|
||
_hd._recv_interval_current = 0.1
|
||
_hd.spawn_recv_accum = 0.0
|
||
_hd.tick(0.1)
|
||
_eq(_hd.active_packets.size(), 4, "при полном буфере таймер не спавнит")
|
||
_ok(_frozen_events > 0, "сигнал spawn_frozen(buffer_full)")
|
||
|
||
# Прямой спавн = переполнение: вытесняется самый старый, штраф по типу.
|
||
var sla_before: float = _game.sla
|
||
var oldest_id: String = ids[0]
|
||
var new_id := str(_hd.spawn_packet_now("sound", "udp"))
|
||
_eq(_hd.active_packets.size(), 4, "размер буфера не превышен")
|
||
_ok(new_id != "", "новый пакет добавлен")
|
||
_ok(not _hd.active_packets.has(oldest_id), "самый старый пакет вытеснен")
|
||
_approx(_game.sla, sla_before - 2.0, 0.001, "переполнение: вытеснен text → −2 SLA")
|
||
# Штраф зависит от типа вытесненного пакета (п.13).
|
||
_hd.start_hour(2)
|
||
var sound_id := str(_hd.spawn_packet_now("sound", "tcp"))
|
||
for i in range(3):
|
||
_hd.spawn_packet_now("text", "tcp")
|
||
sla_before = _game.sla
|
||
_hd.spawn_packet_now("text", "tcp")
|
||
_ok(not _hd.active_packets.has(sound_id), "sound вытеснен из буфера")
|
||
_approx(_game.sla, sla_before - 4.0, 0.001, "вытеснение sound → −4 SLA")
|
||
|
||
_hd.start_hour(2)
|
||
var video_id := str(_hd.spawn_packet_now("video", "tcp"))
|
||
for i in range(3):
|
||
_hd.spawn_packet_now("text", "tcp")
|
||
sla_before = _game.sla
|
||
_hd.spawn_packet_now("text", "tcp")
|
||
_ok(not _hd.active_packets.has(video_id), "video вытеснен из буфера")
|
||
_approx(_game.sla, sla_before - 16.0, 0.001, "вытеснение video → −16 SLA")
|
||
# Прибираем пакеты, чтобы не мешали следующим кейсам.
|
||
for id in _hd.active_packets.keys():
|
||
_hd._remove_packet(str(id))
|
||
|
||
|
||
func _test_receive() -> void:
|
||
_set_case("receive")
|
||
_hd.start_hour(1)
|
||
_hd._recv_interval_current = 9999.0
|
||
var folder_id := _folder_id()
|
||
_ok(not folder_id.is_empty(), "есть папка для приёма")
|
||
var files_before: int = _game.registry.files.size()
|
||
var money_before: int = _game.money
|
||
var sla_before: float = _game.sla
|
||
var packet_id := str(_hd.spawn_packet_now("text", "tcp"))
|
||
var expected_name := str(_hd.active_packets[packet_id].file_name)
|
||
_ok(_hd.receive_packet(packet_id, folder_id), "пакет принят в загрузку")
|
||
var packet = _hd.active_packets.get(packet_id)
|
||
_ok(packet != null and str(packet.state) == "loading", "state loading")
|
||
_approx(_hd.channel_load(), 6.0, 0.0001, "loading text грузит канал на weight 6")
|
||
for i in range(60):
|
||
_hd.tick(0.1)
|
||
if not _hd.active_packets.has(packet_id):
|
||
break
|
||
_ok(not _hd.active_packets.has(packet_id), "пакет ушёл из буфера")
|
||
_eq(_game.registry.files.size(), files_before + 1, "файл создан в реестре")
|
||
_ok(_game.registry.find_by_name(expected_name) != null, "имя файла сохранено без лишнего суффикса")
|
||
# Прерванная загрузка возвращает резерв папки.
|
||
var packet2 := str(_hd.spawn_packet_now("text", "tcp"))
|
||
_ok(_hd.receive_packet(packet2, folder_id), "вторая загрузка начата")
|
||
var folder = _game.registry.folders.get(folder_id)
|
||
_ok(folder != null and int(folder.reserved) >= 1, "папка зарезервирована под загрузку")
|
||
_hd._remove_packet(packet2)
|
||
_ok(folder != null and int(folder.reserved) == 0, "прерванная загрузка сняла резерв")
|
||
_eq(_game.money, money_before + 2, "recv_fee text = +2 $")
|
||
_approx(_game.sla, sla_before + 0.5, 0.001, "+0.5 SLA за приём")
|
||
|
||
|
||
func _test_timeouts() -> void:
|
||
_set_case("timeouts")
|
||
_hd.start_hour(1)
|
||
_hd._recv_interval_current = 9999.0
|
||
var sla_before: float = _game.sla
|
||
var packet_id := str(_hd.spawn_packet_now("text", "tcp"))
|
||
_hd.active_packets[packet_id].time_left = 0.05
|
||
_hd.tick(0.1)
|
||
_ok(not _hd.active_packets.has(packet_id), "пакет удалён по таймауту")
|
||
_approx(_game.sla, sla_before - 2.0, 0.001, "таймаут text-пакета: −2 SLA")
|
||
# Фото/видео штрафуются строже (п.13).
|
||
_game.sla = 200.0
|
||
var photo_id := str(_hd.spawn_packet_now("photo", "tcp"))
|
||
_hd.active_packets[photo_id].time_left = 0.05
|
||
_hd.tick(0.1)
|
||
_approx(_game.sla, 192.0, 0.001, "таймаут photo: −8 SLA")
|
||
_game.sla = 200.0
|
||
var video_id := str(_hd.spawn_packet_now("video", "tcp"))
|
||
_hd.active_packets[video_id].time_left = 0.05
|
||
_hd.tick(0.1)
|
||
_approx(_game.sla, 184.0, 0.001, "таймаут video: −16 SLA")
|
||
|
||
_hd.start_hour(1)
|
||
_game.sla = 200.0
|
||
sla_before = _game.sla
|
||
var req_id := str(_hd._make_request([], "udp", false, "", ""))
|
||
var req = _hd.active_requests[req_id]
|
||
req.time_left = 0.05
|
||
var failed_before: int = _hd._failed
|
||
_hd.tick(0.1)
|
||
_ok(not _hd.active_requests.has(req_id), "запрос удалён по таймауту")
|
||
_approx(_game.sla, sla_before - 12.0, 0.001, "таймаут запроса: −(10+2×hour)")
|
||
_eq(_hd._failed, failed_before + 1, "провал учтён")
|
||
|
||
|
||
func _test_failed_file_cancel() -> void:
|
||
_set_case("failed_file")
|
||
_hd.start_hour(1)
|
||
_hd._recv_interval_current = 9999.0
|
||
_game.money = 1000
|
||
_game.sla = 100.0
|
||
var file = _new_file("Сбойный", "text", _folder_id())
|
||
_ok(file != null, "файл для сбоя создан")
|
||
if file == null:
|
||
return
|
||
var req_id := str(_hd._make_request([file], "udp", false, "", ""))
|
||
_ok(_hd.attach_file(req_id, str(file.id)), "файл прикреплён")
|
||
_game.registry.mark_failed(str(file.id))
|
||
var money_before: int = _game.money
|
||
var sla_before: float = _game.sla
|
||
_hd.tick(0.1)
|
||
_ok(not _hd.active_requests.has(req_id), "запрос отменён при failed-файле (D06)")
|
||
_eq(_game.money, money_before - 15, "failed-файл: −15 $")
|
||
_approx(_game.sla, sla_before - 10.0, 0.001, "failed-файл: −10 SLA")
|
||
|
||
|
||
func _test_modal_freeze() -> void:
|
||
_set_case("modal")
|
||
_hd.start_hour(1)
|
||
_hd._recv_interval_current = 0.1
|
||
_hd.spawn_recv_accum = 0.0
|
||
var packet_id := str(_hd.spawn_packet_now("text", "tcp"))
|
||
var req_id := str(_hd._make_request([], "udp", false, "", ""))
|
||
_hd.modal_open = true
|
||
var packet = _hd.active_packets[packet_id]
|
||
var req = _hd.active_requests[req_id]
|
||
var packet_time: float = packet.time_left
|
||
var req_time: float = req.time_left
|
||
var packets_before: int = _hd.active_packets.size()
|
||
for i in range(10):
|
||
_hd.tick(0.1)
|
||
_approx(packet.time_left, packet_time, 0.0001, "таймер пакета заморожен модалкой")
|
||
_approx(req.time_left, req_time - 1.0, 0.001, "таймер запроса идёт при модалке")
|
||
_eq(_hd.active_packets.size(), packets_before, "спавн приёма остановлен")
|
||
_hd.modal_open = false
|
||
_hd._remove_packet(packet_id)
|
||
_hd._cleanup_request(req_id, false)
|
||
|
||
|
||
func _test_send_payout() -> void:
|
||
_set_case("send/payout")
|
||
_hd.start_hour(1)
|
||
_hd._recv_interval_current = 9999.0
|
||
_game.sla = 150.0
|
||
var folder_id := _folder_id()
|
||
var file = _new_file("Отправка", "text", folder_id)
|
||
_ok(file != null, "файл для отправки создан")
|
||
if file == null:
|
||
return
|
||
var req_id := str(_hd._make_request([file], "tcp", false, "", ""))
|
||
var req = _hd.active_requests[req_id]
|
||
_ok(_hd.attach_file(req_id, str(file.id)), "файл прикреплён")
|
||
_ok(not req.sending, "TCP без соединения ещё не отправляется")
|
||
req.connected = true
|
||
var money_before: int = _game.money
|
||
var sla_before: float = _game.sla
|
||
_ok(_hd.start_send(req_id), "отправка стартовала")
|
||
_ok(req.sending, "отправка началась сразу, без окна-заглушки")
|
||
_ok(not req.minigame_active, "флаг minigame_active не поднимается")
|
||
_ok(_hd._minigame == null, "MiniGame не создаётся")
|
||
_ok(not _hd.modal_open, "модалка не открывается")
|
||
_approx(req.quality, 1.0, 0.0001, "quality = 1.0 без мини-игры")
|
||
_approx(_hd.channel_load(), 6.0, 0.0001, "sending text грузит канал на weight 6")
|
||
_ok(float(req.send_duration) > 0.0, "send_duration > 0")
|
||
for i in range(200):
|
||
_hd.tick(0.1)
|
||
if not _hd.active_requests.has(req_id):
|
||
break
|
||
_ok(not _hd.active_requests.has(req_id), "запрос завершён")
|
||
_ok(_game.money > money_before, "выплата начислена")
|
||
_approx(_game.sla, sla_before + 1.0, 0.001, "+1 SLA за отправку")
|
||
_eq(_hd._completed, 1, "completed = 1")
|
||
_ok(float(file.cooldown_until) > 0.0, "cooldown файла после отправки (D02)")
|
||
_ok(str(file.attached_request).is_empty(), "файл откреплён")
|
||
|
||
|
||
func _test_attacks() -> void:
|
||
_set_case("attacks")
|
||
_eq(AD.attacks_for_hour(1), 0, "час 1: атак нет (junk с часа 2)")
|
||
_eq(AD.attacks_for_hour(2), 1, "час 2: 1 атака")
|
||
_eq(AD.attacks_for_hour(4), 2, "час 4: 2 атаки")
|
||
_eq(AD.attacks_for_hour(6), 3, "час 6: 3 атаки")
|
||
_eq(AD.attacks_for_hour(8), 4, "час 8: 4 атаки")
|
||
_eq(AD.attacks_for_hour(9), 4, "час 9: 4 атаки")
|
||
|
||
# Час 1: расписание пустое, junk ещё заперт — атак нет.
|
||
_game.hour = 1
|
||
_hd.start_hour(1)
|
||
var ad1 = _hd.attack_director
|
||
_eq(ad1._schedule.size(), 0, "час 1: расписание без атак")
|
||
_ok(str(ad1.spawn_attack("junk")).is_empty(), "час 1: junk закрыт")
|
||
|
||
# junk/ddos открыты с часа 2, phishing — с 3, ransomware — с 4 (п.16).
|
||
_game.hour = 2
|
||
_hd.start_hour(2)
|
||
var ad = _hd.attack_director
|
||
ad.reset(2)
|
||
_game.sla = 150.0
|
||
var money_before: int = _game.money
|
||
var sla_before: float = _game.sla
|
||
var junk_id := str(ad.spawn_attack("junk"))
|
||
_ok(junk_id != "", "мусор заспавнен")
|
||
_ok(ad.active_attacks.has(junk_id), "мусор в активных")
|
||
_ok(ad.destroy(junk_id), "мусор уничтожен")
|
||
_eq(_game.money, money_before + 5, "мусор: +5 $")
|
||
_approx(_game.sla, sla_before + 1.0, 0.001, "мусор: +1 SLA")
|
||
|
||
ad.reset(2)
|
||
var ddos_id := str(ad.spawn_attack("ddos"))
|
||
_ok(ddos_id != "", "DDoS заспавнен")
|
||
var cards := int(ad.active_attacks[ddos_id].get("cards", 0))
|
||
_ok(cards >= 8 and cards <= 12, "DDoS: 8–12 карточек")
|
||
money_before = _game.money
|
||
_ok(ad.hold_shield(0.8), "первое удержание щита")
|
||
_ok(ad.hold_shield(0.8), "второе удержание щита")
|
||
_ok(not ad.active_attacks.has(ddos_id), "1.5 с щита отражают DDoS")
|
||
_eq(_game.money, money_before + 30, "DDoS: +30 $")
|
||
|
||
_game.hour = 3
|
||
_hd.start_hour(3)
|
||
var file = _new_file("Фишинг", "text", _folder_id())
|
||
_ok(file != null, "файл для фишинга")
|
||
ad.reset(3)
|
||
var phish_id := str(ad.spawn_attack("phishing"))
|
||
_ok(phish_id != "", "фишинг заспавнен")
|
||
var phish_req := str(ad.active_attacks[phish_id].get("request_id", ""))
|
||
_ok(not phish_req.is_empty(), "фишинг создал фейковый запрос")
|
||
_ok(_hd.active_requests.has(phish_req), "фейковый запрос в часа")
|
||
money_before = _game.money
|
||
_ok(_hd.reject_request(phish_req), "фишинг отклонён")
|
||
_ok(not _hd.active_requests.has(phish_req), "фейковый запрос убран")
|
||
_ok(not ad.active_attacks.has(phish_id), "атака фишинга закрыта")
|
||
_ok(_game.money >= money_before + 20, "фишинг: награда без штрафа")
|
||
|
||
_game.hour = 4
|
||
_hd.start_hour(4)
|
||
ad.reset(4)
|
||
var rw_id := str(ad.spawn_attack("ransomware"))
|
||
_ok(rw_id != "", "шифровальщик заспавнен")
|
||
var packet_id := str(ad.active_attacks[rw_id].get("packet_id", ""))
|
||
_ok(_hd.active_packets.has(packet_id), "пакет .exe в приёме")
|
||
money_before = _game.money
|
||
_ok(_hd.destroy_attack_packet(packet_id), "пакет-шифровальщик уничтожен")
|
||
_eq(_game.money, money_before + 25, "шифровальщик: +25 $")
|
||
_ok(not _hd.active_packets.has(packet_id), "пакет удалён")
|
||
|
||
|
||
func _test_auto_send() -> void:
|
||
_set_case("auto_send")
|
||
_game.hour = 1
|
||
_hd.start_hour(1)
|
||
_hd._recv_interval_current = 9999.0
|
||
_game.sla = 150.0
|
||
var folder_id := _folder_id()
|
||
# UDP: последний прикреплённый файл сразу запускает отправку.
|
||
var udp_file = _new_file("Авто-UDP", "text", folder_id)
|
||
_ok(udp_file != null, "UDP: файл создан")
|
||
if udp_file != null:
|
||
var udp_id := str(_hd._make_request([udp_file], "udp", false, "", ""))
|
||
var udp_req = _hd.active_requests[udp_id]
|
||
_ok(_hd.attach_file(udp_id, str(udp_file.id)), "UDP: файл прикреплён")
|
||
_ok(udp_req.sending, "UDP: отправка началась без клика")
|
||
_ok(not udp_req.minigame_active, "UDP: minigame_active не поднимается")
|
||
_ok(_hd._minigame == null, "UDP: MiniGame не создаётся")
|
||
_ok(not _hd.modal_open, "UDP: модалка не осталась")
|
||
_ok(float(udp_req.send_duration) > 0.0, "UDP: send_duration задан")
|
||
var progress_before: float = float(udp_req.send_progress)
|
||
_hd.tick(0.1)
|
||
_ok(float(udp_req.send_progress) > progress_before, "UDP: прогресс отправки идёт")
|
||
for i in range(200):
|
||
_hd.tick(0.1)
|
||
if not _hd.active_requests.has(udp_id):
|
||
break
|
||
_ok(not _hd.active_requests.has(udp_id), "UDP: запрос отправлен и закрыт")
|
||
|
||
# TCP: отправка стартует сразу после успешного соединения.
|
||
var tcp_file = _new_file("Авто-TCP", "text", folder_id)
|
||
_ok(tcp_file != null, "TCP: файл создан")
|
||
if tcp_file != null:
|
||
var tcp_id := str(_hd._make_request([tcp_file], "tcp", false, "", ""))
|
||
var tcp_req = _hd.active_requests[tcp_id]
|
||
_ok(_hd.attach_file(tcp_id, str(tcp_file.id)), "TCP: файл прикреплён")
|
||
_ok(not tcp_req.sending, "TCP: до соединения отправки нет")
|
||
_ok(not _hd.start_send(tcp_id), "TCP: start_send до соединения → false")
|
||
_ok(_hd.connect_request(tcp_id), "TCP: соединение открыто")
|
||
var node = _hd._connection_games.get(tcp_id)
|
||
_hd._on_connection_finished(true, tcp_id)
|
||
_ok(tcp_req.connected, "TCP: соединение успешно")
|
||
_ok(tcp_req.sending, "TCP: отправка началась после соединения")
|
||
_ok(not _hd.modal_open, "TCP: модалка соединения закрыта")
|
||
if node != null and is_instance_valid(node):
|
||
node.free()
|
||
_hd._cleanup_request(tcp_id, true)
|
||
|
||
# UDP-потеря: guard разрешает один повтор и не зацикливает отправку.
|
||
var retry_file = _new_file("Авто-UDP-retry", "text", folder_id)
|
||
_ok(retry_file != null, "UDP-повтор: файл создан")
|
||
if retry_file != null:
|
||
var retry_id := str(_hd._make_request([retry_file], "udp", false, "", ""))
|
||
var retry_req = _hd.active_requests[retry_id]
|
||
var saved_rng = _hd.rng
|
||
_hd.rng = AlwaysZeroRng.new()
|
||
_ok(_hd.attach_file(retry_id, str(retry_file.id)), "UDP-повтор: файл прикреплён")
|
||
_hd.rng = saved_rng
|
||
_ok(retry_req.sending, "UDP-повтор: отправка началась после одного повтора")
|
||
_ok(_hd._minigame == null, "UDP-повтор: MiniGame не создаётся")
|
||
_hd._cleanup_request(retry_id, true)
|
||
|
||
# Открытая ConnectionGame (другого запроса) блокирует старт отправки.
|
||
var modal_id := str(_hd._make_request([], "udp", false, "", ""))
|
||
var modal_req = _hd.active_requests[modal_id]
|
||
_ok(modal_req != null and modal_req.is_ready(), "модальный кейс: запрос готов")
|
||
_hd._push_modal()
|
||
_ok(_hd.modal_open, "модалка открыта")
|
||
_ok(not _hd.start_send(modal_id), "modal_open=true → start_send false")
|
||
_ok(not modal_req.sending, "при открытой модалке отправка не началась")
|
||
_hd._pop_modal()
|
||
_ok(not _hd.modal_open, "модалка закрыта")
|
||
_ok(_hd.start_send(modal_id), "после закрытия модалки start_send true")
|
||
_ok(modal_req.sending, "отправка началась после закрытия модалки")
|
||
_hd._cleanup_request(modal_id, true)
|
||
|
||
|
||
## RNG, который всегда «теряет» UDP-пакеты — проверка guard'а от зацикливания.
|
||
class AlwaysZeroRng:
|
||
func randf() -> float:
|
||
return 0.0
|
||
|
||
|
||
func _test_required_counts() -> void:
|
||
_set_case("required_count")
|
||
_hd.start_hour(1)
|
||
_hd._recv_interval_current = 9999.0
|
||
for i in range(30):
|
||
_eq(_hd._roll_required_count(), 1, "час 1: всегда 1 файл")
|
||
var folder_id := _folder_id()
|
||
for i in range(3):
|
||
_new_file("Счёт-1", "text", folder_id)
|
||
for i in range(12):
|
||
var rid := str(_hd.spawn_request_now())
|
||
_ok(rid != "", "час 1: запрос создан")
|
||
if rid.is_empty():
|
||
continue
|
||
_eq(_hd.active_requests[rid].required.size(), 1, "час 1: required.size() == 1")
|
||
_hd._cleanup_request(rid, true)
|
||
|
||
_hd.start_hour(2)
|
||
_hd._recv_interval_current = 9999.0
|
||
var saw_two := false
|
||
for i in range(60):
|
||
var count: int = int(_hd._roll_required_count())
|
||
_ok(count >= 1 and count <= 2, "час 2: required 1..2")
|
||
if count == 2:
|
||
saw_two = true
|
||
_ok(saw_two, "час 2: распределение даёт 2 файла")
|
||
|
||
_hd.start_hour(3)
|
||
_hd._recv_interval_current = 9999.0
|
||
var seen: Dictionary = {}
|
||
for i in range(120):
|
||
var count3: int = int(_hd._roll_required_count())
|
||
_ok(count3 >= 1 and count3 <= 3, "час 3+: required 1..3")
|
||
seen[count3] = true
|
||
_ok(seen.size() >= 2, "час 3+: встречаются разные размеры")
|
||
|
||
|
||
|
||
func _test_connection_failure() -> void:
|
||
_set_case("connection_fail")
|
||
_hd.start_hour(1)
|
||
_hd._recv_interval_current = 9999.0
|
||
var packet_id := str(_hd.spawn_packet_now("text", "tcp"))
|
||
_ok(_hd.connect_packet(packet_id), "соединение открыто")
|
||
_eq(_hd.slots.used_count(), 1, "слот занят на время соединения")
|
||
var node = _hd._connection_games.get(packet_id)
|
||
_hd._on_connection_finished(false, packet_id)
|
||
_eq(_hd.slots.used_count(), 0, "провал соединения освобождает слот")
|
||
_ok(not _hd.modal_open, "провал закрывает модалку")
|
||
if _hd.active_packets.has(packet_id):
|
||
_eq(str(_hd.active_packets[packet_id].state), "waiting", "пакет остаётся waiting")
|
||
if node != null and is_instance_valid(node):
|
||
node.free()
|
||
# Повторное «Соединиться» создаёт новую панель.
|
||
_ok(_hd.connect_packet(packet_id), "после провала можно соединиться снова")
|
||
_hd._on_connection_finished(true, packet_id)
|
||
_eq(_hd.slots.used_count(), 1, "успех снова занимает слот")
|
||
_hd._remove_packet(packet_id)
|
||
|
||
|
||
func _test_full_hour_timescale() -> void:
|
||
_set_case("full_hour")
|
||
_game.hour = 2
|
||
_hd.start_hour(2)
|
||
_hour_events = 0
|
||
_hour_report = {}
|
||
_hd.hour_finished.connect(_on_hour_finished)
|
||
_hd.set_time_scale(30.0)
|
||
for i in range(120):
|
||
_hd.tick(0.1)
|
||
if not _hd.running:
|
||
break
|
||
_ok(not _hd.running, "час завершился")
|
||
_approx(_hd.time_left, 0.0, 0.001, "time_left = 0")
|
||
_eq(_hour_events, 1, "hour_finished эмитится один раз")
|
||
if _hour_events > 0:
|
||
_eq(int(_hour_report.get("hour", 0)), 2, "в отчёте час 2")
|
||
_ok(_hour_report.has("earned"), "в отчёте есть earned")
|
||
_ok(_hour_report.has("completed"), "в отчёте есть completed")
|
||
_ok(_hour_report.has("failed"), "в отчёте есть failed")
|
||
_ok(_hour_report.has("attacks"), "в отчёте есть attacks")
|
||
_ok(_hour_report.has("sla_delta"), "в отчёте есть sla_delta")
|
||
|
||
|
||
# --- служебное -------------------------------------------------------------
|
||
|
||
func _folder_id() -> String:
|
||
var section = _game.registry.sections.get("C")
|
||
if section == null or section.folder_ids.is_empty():
|
||
return ""
|
||
return str(section.folder_ids[0])
|
||
|
||
|
||
func _new_file(base: String, type: String, folder_id: String):
|
||
_name_seq += 1
|
||
var file = _game.registry.create_file("%s %d" % [base, _name_seq], type, folder_id)
|
||
return file
|
||
|
||
|
||
func _on_spawn_frozen(_reason: String) -> void:
|
||
_frozen_events += 1
|
||
|
||
|
||
func _on_hour_finished(report: Dictionary) -> void:
|
||
_hour_events += 1
|
||
_hour_report = report
|
||
|
||
|
||
# --- assert-хелперы --------------------------------------------------------
|
||
|
||
func _set_case(name: String) -> void:
|
||
_case = name
|
||
|
||
|
||
func _ok(condition: bool, message: String) -> bool:
|
||
_checks += 1
|
||
if condition:
|
||
return true
|
||
_fail(message)
|
||
return false
|
||
|
||
|
||
func _eq(actual, expected, message: String) -> bool:
|
||
_checks += 1
|
||
if actual == expected:
|
||
return true
|
||
_fail("%s — получено %s, ожидалось %s" % [message, str(actual), str(expected)])
|
||
return false
|
||
|
||
|
||
func _approx(actual: float, expected: float, eps: float, message: String) -> bool:
|
||
_checks += 1
|
||
if absf(actual - expected) <= eps:
|
||
return true
|
||
_fail("%s — получено %.6f, ожидалось %.6f" % [message, actual, expected])
|
||
return false
|
||
|
||
|
||
func _fail(message: String) -> void:
|
||
_fails += 1
|
||
print("FAIL [%s] %s" % [_case, message])
|
||
|
||
|
||
func _finish() -> void:
|
||
print("test_hour: проверок %d, провалов %d" % [_checks, _fails])
|
||
print("PASS test_hour" if _fails == 0 else "FAIL test_hour")
|
||
quit(0 if _fails == 0 else 1)
|