extends "res://tests/test_case.gd" # Task 5.10. The log's whole value is that a recorded match can be replayed # faithfully enough to reproduce a reported snap, so what matters is that the # bytes come back BYTE-IDENTICAL and correctly framed — not merely that # something was written. const ReplayLogScript = preload("res://scripts/replay_log.gd") func _temp_path(suffix: String) -> String: return "user://test_replay_%s_%d.ccrp" % [suffix, Time.get_ticks_usec()] func test_records_round_trip_byte_for_byte() -> void: var path := _temp_path("roundtrip") var log_writer = ReplayLogScript.new() assert_eq(log_writer.open_for_write(path), OK, "opens for write") var input_payload := PackedByteArray([0x01, 0xFF, 0x00, 0x7F, 0x80]) var snapshot_payload := PackedByteArray([0xDE, 0xAD, 0xBE, 0xEF]) log_writer.record_input(120, 4242, input_payload) log_writer.record_snapshot(121, snapshot_payload) log_writer.close() var read := ReplayLogScript.read_all(path) assert_eq(read.get("version", -1), ReplayLogScript.FORMAT_VERSION, "version round-trips") assert_eq(read.get("tick_hz", -1), SimConstants.TICK_HZ, "tick rate is recorded so a reader need not guess") var records: Array = read.get("records", []) assert_eq(records.size(), 2, "both records read back") assert_eq(records[0]["kind"], ReplayLogScript.RecordKind.INPUT, "first is an input") assert_eq(records[0]["tick"], 120, "input tick") assert_eq(records[0]["peer_id"], 4242, "input peer") assert_eq(records[0]["payload"], input_payload, "input payload is byte-identical") assert_eq(records[1]["kind"], ReplayLogScript.RecordKind.SNAPSHOT, "second is a snapshot") assert_eq(records[1]["tick"], 121, "snapshot tick") assert_eq(records[1]["payload"], snapshot_payload, "snapshot payload is byte-identical") DirAccess.remove_absolute(ProjectSettings.globalize_path(path)) func test_a_real_packet_survives_the_round_trip() -> void: # The payloads above are hand-made. Use a genuine NetCodec input packet so # a framing bug that only shows up at real packet sizes cannot hide. var path := _temp_path("realpacket") var action := ShipAction.new() action.thrust = Vector3(0.5, -0.25, 1.0) action.turbo = true var packet := NetCodec.pack_input(77, 55, 1234, [action, action, action]) var log_writer = ReplayLogScript.new() log_writer.open_for_write(path) log_writer.record_input(500, 7, packet) log_writer.close() var records: Array = ReplayLogScript.read_all(path).get("records", []) assert_eq(records.size(), 1, "one record") assert_eq(records[0]["payload"], packet, "a real input packet round-trips unchanged") # And it must still decode as the packet it was. var decoded := NetCodec.unpack_input(records[0]["payload"]) assert_eq(decoded["seq"], 77, "the replayed packet still decodes to its own sequence") DirAccess.remove_absolute(ProjectSettings.globalize_path(path)) func test_empty_log_reads_back_as_no_records() -> void: var path := _temp_path("empty") var log_writer = ReplayLogScript.new() log_writer.open_for_write(path) log_writer.close() var read := ReplayLogScript.read_all(path) assert_eq(read.get("records", [-1]).size(), 0, "a header-only log has no records") assert_eq(read.get("tick_hz", -1), SimConstants.TICK_HZ, "but still reports its header") DirAccess.remove_absolute(ProjectSettings.globalize_path(path)) func test_a_non_replay_file_is_rejected_rather_than_misread() -> void: # FileAccess zero-fills past EOF exactly as StreamPeerBuffer does, so # without a magic check an arbitrary file decodes as an endless run of # zero-length records instead of failing. var path := _temp_path("garbage") var f := FileAccess.open(path, FileAccess.WRITE) f.store_string("this is definitely not a replay log") f.close() assert_true(ReplayLogScript.read_all(path).is_empty(), "a foreign file is refused") DirAccess.remove_absolute(ProjectSettings.globalize_path(path)) func test_a_truncated_log_yields_its_intact_records() -> void: # A server killed mid-write is the NORMAL way one of these ends, so a # partial final record must not discard the whole session. var path := _temp_path("truncated") var log_writer = ReplayLogScript.new() log_writer.open_for_write(path) log_writer.record_input(1, 1, PackedByteArray([1, 2, 3, 4])) log_writer.record_input(2, 1, PackedByteArray([5, 6, 7, 8])) log_writer.close() var whole := FileAccess.get_file_as_bytes(path) var cut := whole.slice(0, whole.size() - 3) # lop off part of the last payload var f := FileAccess.open(path, FileAccess.WRITE) f.store_buffer(cut) f.close() var records: Array = ReplayLogScript.read_all(path).get("records", []) assert_eq(records.size(), 1, "the intact record survives a truncated tail") assert_eq(records[0]["payload"], PackedByteArray([1, 2, 3, 4]), "and is still correct") DirAccess.remove_absolute(ProjectSettings.globalize_path(path)) func test_rejected_packets_are_recorded_and_distinguishable_by_reason() -> void: # The log exists to answer "my input did nothing", and the packets that # explain that are exactly the ones the server threw away. Recording them # is only useful if the reason survives too, so a reader can tell a # malformed packet from one the rate limiter dropped. var path := _temp_path("rejects") var log_writer = ReplayLogScript.new() log_writer.open_for_write(path) var malformed := PackedByteArray([0x01, 0x02]) var flooded := PackedByteArray([0x09, 0x08, 0x07]) var far_future := PackedByteArray([0x11, 0x22, 0x33, 0x44]) log_writer.record_rejected_input(ReplayLogScript.RecordKind.REJECTED_MALFORMED, 10, 5, malformed) log_writer.record_rejected_input(ReplayLogScript.RecordKind.REJECTED_RATE_LIMIT, 11, 5, flooded) log_writer.record_rejected_input(ReplayLogScript.RecordKind.REJECTED_SEQ_GUARD, 12, 6, far_future) log_writer.record_input(13, 5, PackedByteArray([0xAA])) log_writer.close() var records: Array = ReplayLogScript.read_all(path).get("records", []) assert_eq(records.size(), 4, "rejects and accepted input share one ordered stream") assert_eq(records[0]["kind"], ReplayLogScript.RecordKind.REJECTED_MALFORMED, "malformed reason survives") assert_eq(records[0]["payload"], malformed, "and so do the bytes that caused it") assert_eq(records[1]["kind"], ReplayLogScript.RecordKind.REJECTED_RATE_LIMIT, "rate-limit reason survives") assert_eq(records[1]["payload"], flooded, "with its own payload") assert_eq(records[2]["kind"], ReplayLogScript.RecordKind.REJECTED_SEQ_GUARD, "seq-guard reason survives") assert_eq(records[2]["peer_id"], 6, "attributed to the peer that sent it") assert_eq(records[3]["kind"], ReplayLogScript.RecordKind.INPUT, "an accepted packet is still its own kind") DirAccess.remove_absolute(ProjectSettings.globalize_path(path)) func test_a_failed_write_stops_the_log_instead_of_corrupting_it() -> void: # A half-written record desyncs the framing of everything after it, turning # "the disk filled up" into "the file is garbage". Forcing a real ENOSPC is # out of scope for a unit test, so the failure is injected directly — this # covers the guard and the accounting, not the detection, which is a # get_error() check on the real FileAccess. var path := _temp_path("writefail") var log_writer = ReplayLogScript.new() log_writer.open_for_write(path) log_writer.record_input(1, 1, PackedByteArray([1, 2, 3, 4])) log_writer.write_failed = true log_writer.record_input(2, 1, PackedByteArray([5, 6, 7, 8])) log_writer.record_snapshot(3, PackedByteArray([9])) assert_eq(log_writer.records_written, 1, "nothing is written after a failure") log_writer.close() var records: Array = ReplayLogScript.read_all(path).get("records", []) assert_eq(records.size(), 1, "what was written before the failure is still readable") assert_eq(records[0]["payload"], PackedByteArray([1, 2, 3, 4]), "and intact") DirAccess.remove_absolute(ProjectSettings.globalize_path(path)) func test_an_oversized_payload_is_dropped_without_breaking_later_records() -> void: # `length` is a u16. Truncating an over-64KB payload to fit would leave the # reader parsing the payload's own tail as the next record header. var path := _temp_path("oversized") var log_writer = ReplayLogScript.new() log_writer.open_for_write(path) var oversized := PackedByteArray() oversized.resize(0x10000) log_writer.record_input(1, 1, oversized) assert_eq(log_writer.records_dropped, 1, "the oversized record is counted as dropped") log_writer.record_input(2, 1, PackedByteArray([7, 7])) log_writer.close() var records: Array = ReplayLogScript.read_all(path).get("records", []) assert_eq(records.size(), 1, "only the well-sized record is present") assert_eq(records[0]["tick"], 2, "and it is the one that came after the drop, correctly framed") DirAccess.remove_absolute(ProjectSettings.globalize_path(path)) func test_writing_to_an_unopened_log_is_a_no_op() -> void: # --replay-log is optional, so every record_* call happens behind a null # check in production — but the class must not corrupt or crash if that # check is ever missed. var log_writer = ReplayLogScript.new() assert_true(not log_writer.is_open(), "starts closed") log_writer.record_input(1, 1, PackedByteArray([1])) log_writer.record_snapshot(1, PackedByteArray([1])) assert_eq(log_writer.records_written, 0, "nothing was recorded") log_writer.close()