tests: 24 protocol tests check what their names claim
Thirteen now assert what they claimed: - every bare trade status, and the 444-byte extended snapshot for empty, full and mixed slots; - the status-1 and status-3 battlefield tails; - every quest-share verdict byte through encode, parse and decode; - the greeting pick itself, and the whole compressed-move batch. Eleven are renamed to what they check, where the claim had nothing to check against: no capture bytes, no production opcode list, no server encoder. No strengthened assertion failed. Probe and assert messages drop their record numbers. The aura probe's refusal message no longer claims the level-6 probe accounts need a grant for a level-4 command. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -39,7 +39,7 @@ fn probe_char_name(user: &str) -> Option<String> {
|
||||
}
|
||||
|
||||
fn login(host: &str, user: &str, pass: &str) -> Result<(WorldSession, String)> {
|
||||
let name = probe_char_name(user).context("account must be a probeN (decision 0530)")?;
|
||||
let name = probe_char_name(user).context("account must be a probeN")?;
|
||||
let logon = benilla_protocol::logon(host, user, pass)?;
|
||||
let addr = logon
|
||||
.realms
|
||||
|
||||
@@ -115,10 +115,8 @@ impl Probe for Aura {
|
||||
.context(
|
||||
"--aura: spell 1126 never appeared in UNIT_FIELD_AURA. Either the descriptor field \
|
||||
index is wrong, or the GM `.aura` command was refused — it needs gmlevel >= 4 \
|
||||
(VERIFIED vmangos `Chat/Chat.cpp:1229`: SEC_BASIC_ADMIN, which is 4 in \
|
||||
`shared/Common.h:142`), and the probe accounts are gmlevel 6, so this \
|
||||
probe cannot run as `probeN` without a temporary grant (docs/METHOD.md, decision 0450's \
|
||||
precedent for --worldstate).",
|
||||
(vmangos `Chat/Chat.cpp:1229`: SEC_BASIC_ADMIN, which is 4 in \
|
||||
`shared/Common.h:142`).",
|
||||
)?;
|
||||
let debuff = auras
|
||||
.iter()
|
||||
@@ -192,7 +190,7 @@ impl Probe for Aura {
|
||||
ensure!(
|
||||
d < v,
|
||||
"SMSG_UPDATE_AURA_DURATION arrived AFTER the descriptor delta (seq {d} vs {v}) — \
|
||||
decision 0255's slot-keyed buffering is built on the opposite order"
|
||||
the app's slot-keyed buffering is built on the opposite order"
|
||||
);
|
||||
println!("✅ duration packet precedes the descriptor delta (event {d} before {v})");
|
||||
|
||||
@@ -203,7 +201,7 @@ impl Probe for Aura {
|
||||
{
|
||||
bail!(
|
||||
"--aura: slot {slot} reported a duration without being (re)applied — durations are \
|
||||
not the apply/refresh edges decision 0255's client-side countdown assumes"
|
||||
not the apply/refresh edges the client-side countdown assumes"
|
||||
);
|
||||
}
|
||||
println!(
|
||||
|
||||
@@ -162,8 +162,8 @@ impl Probe for MountTele {
|
||||
ensure!(
|
||||
(create_run - mounted).abs() < 0.01,
|
||||
"--mount-tele: the destination's create block should still carry the MOUNTED run \
|
||||
speed ({mounted}), got {create_run} — the strip would then precede the create and \
|
||||
B213's ordering would not be this one"
|
||||
speed ({mounted}), got {create_run} — the strip would then precede the create, \
|
||||
the opposite of the order this probe expects"
|
||||
);
|
||||
ensure!(
|
||||
(strip_run - BASE_RUN).abs() < 0.01,
|
||||
@@ -174,8 +174,7 @@ impl Probe for MountTele {
|
||||
"\n--mount-tele PASS: arrived on map {map}; create block carried the mount's \
|
||||
{create_run} yd/s, the dismount's SMSG_FORCE_RUN_SPEED_CHANGE followed {} µs later \
|
||||
at {strip_run} yd/s.\n Both packets are written by one HandleMoveWorldportAckOpcode \
|
||||
call, so a client that drains its socket once a frame sees them in ONE drain — which \
|
||||
is what B213 (decision 1478) turns on.",
|
||||
call, so a client that drains its socket once a frame sees them in ONE drain.",
|
||||
gap.as_micros()
|
||||
);
|
||||
Ok(())
|
||||
|
||||
@@ -158,7 +158,7 @@ impl Probe for QuestItem {
|
||||
)
|
||||
.context(
|
||||
"--quest-item: the re-query got no DETAILS — this server DOES gate the query by \
|
||||
quest status, so the panel would not re-open (decision 0669 expects vmangos' \
|
||||
quest status, so the panel would not re-open (the probe expects vmangos's \
|
||||
ungated handler)",
|
||||
)?;
|
||||
println!("✅ the panel legitimately re-opens: DETAILS {reopened}");
|
||||
|
||||
@@ -248,17 +248,17 @@ impl Probe for QuestLog {
|
||||
bail!(
|
||||
"--questlog: slot {slot}'s PLAYER_QUEST_LOG id field never cleared to 0 within \
|
||||
8s after CMSG_QUESTLOG_REMOVE_QUEST (no ack SMSG exists on this wire — the field \
|
||||
clear IS the confirmation, decision 0109)"
|
||||
clear IS the confirmation)"
|
||||
);
|
||||
}
|
||||
println!(
|
||||
"✅ abandon: slot {slot}'s PLAYER_QUEST_LOG id field cleared to 0 (no ack SMSG on \
|
||||
this wire — the field update is the confirmation, decision 0109)."
|
||||
this wire — the field update is the confirmation)."
|
||||
);
|
||||
|
||||
println!(
|
||||
"\n✅ --questlog PASS: template parsed, slot {slot} tracked → COMPLETE → abandoned \
|
||||
(id field cleared) — the quest-log wire verified end to end (decision 0109)."
|
||||
(id field cleared) — the quest-log wire verified end to end."
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -98,8 +98,9 @@ pub fn addon_tail(addons: &[SecureAddon]) -> Vec<u8> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// The block has retail's 342-byte length and parses as twelve sorted, stock-signed records.
|
||||
#[test]
|
||||
fn stock_block_matches_the_retail_capture() {
|
||||
fn stock_block_has_the_retail_size_and_twelve_sorted_stock_records() {
|
||||
let plain = addon_block(&STOCK_SECURE_ADDONS);
|
||||
assert_eq!(plain.len(), 342, "retail sent 342 uncompressed bytes");
|
||||
|
||||
|
||||
@@ -249,6 +249,7 @@ pub fn battlefield_port(map_id: u32, accept: bool) -> Vec<u8> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Status 2, 3 and 1 each read their own tail and no further; a zero map ends the packet.
|
||||
#[test]
|
||||
fn status_reads_the_conditional_tails() {
|
||||
let mut body = vec![
|
||||
@@ -260,6 +261,34 @@ mod tests {
|
||||
(1, 30, 5, 7, 2)
|
||||
);
|
||||
assert_eq!(s.time_ms, Some(100));
|
||||
assert_eq!((s.in_progress, s.queued), (None, None));
|
||||
|
||||
// The same slot, map, bracket and instance under status 3, then status 1.
|
||||
let header = body[..13].to_vec();
|
||||
let mut b = header.clone();
|
||||
for v in [3u32, 120_000, 45_000] {
|
||||
b.extend_from_slice(&v.to_le_bytes());
|
||||
}
|
||||
b.push(0xEE); // a byte past the tail
|
||||
let mut r = b.as_slice();
|
||||
let s = read_battlefield_status(&mut r).unwrap();
|
||||
assert_eq!(s.status, 3);
|
||||
assert_eq!(s.in_progress, Some((120_000, 45_000)));
|
||||
assert_eq!((s.time_ms, s.queued), (None, None));
|
||||
assert_eq!(r, [0xEE], "the status-3 tail is two u32s");
|
||||
|
||||
let mut b = header;
|
||||
for v in [1u32, 30_000, 5_000] {
|
||||
b.extend_from_slice(&v.to_le_bytes());
|
||||
}
|
||||
b.push(0xEE);
|
||||
let mut r = b.as_slice();
|
||||
let s = read_battlefield_status(&mut r).unwrap();
|
||||
assert_eq!(s.status, 1);
|
||||
assert_eq!(s.queued, Some((30_000, 5_000)));
|
||||
assert_eq!((s.time_ms, s.in_progress), (None, None));
|
||||
assert_eq!(r, [0xEE], "the status-1 tail is two u32s");
|
||||
|
||||
body = vec![0u8, 0, 0, 0, 0, 0, 0, 0];
|
||||
let s = read_battlefield_status(&mut body.as_slice()).unwrap();
|
||||
assert_eq!(
|
||||
|
||||
@@ -186,6 +186,7 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// A spirit healer's offer as `Spell::SendResurrectRequest` builds it: named, with sickness.
|
||||
#[test]
|
||||
fn resurrect_request_npc_caster_golden() {
|
||||
let name = b"Spirit Healer";
|
||||
@@ -195,12 +196,19 @@ mod tests {
|
||||
body.extend_from_slice(name);
|
||||
body.push(0);
|
||||
body.push(1); // sickness = true
|
||||
body.push(1);
|
||||
body.push(1); // hasResTimer = true
|
||||
let mut r = body.as_slice();
|
||||
let req = read_resurrect_request(&mut r).unwrap();
|
||||
assert!(r.is_empty());
|
||||
assert_eq!(req.name, "Spirit Healer");
|
||||
assert!(req.sickness);
|
||||
assert_eq!(
|
||||
req,
|
||||
ResurrectRequestBody {
|
||||
caster: 0xF130_0FBE_0000_2AB3,
|
||||
name: "Spirit Healer".into(),
|
||||
sickness: true,
|
||||
has_timer: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/// `SMSG_SPIRIT_HEALER_CONFIRM` is one full guid (`SpellEffects.cpp:825-829`).
|
||||
@@ -231,6 +239,10 @@ mod tests {
|
||||
let mut expect = 0x2Au64.to_le_bytes().to_vec();
|
||||
expect.push(0);
|
||||
assert_eq!(resurrect_response(0x2A, false), expect);
|
||||
assert_eq!(
|
||||
area_spirit_healer(0xF130_0033_3C00_0010),
|
||||
0xF130_0033_3C00_0010u64.to_le_bytes().to_vec()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -657,6 +657,8 @@ mod tests {
|
||||
fn wrap_item_body_paper_first() {
|
||||
// 467: giftbag, giftslot, itembag, itemslot (Item.cpp:121-127).
|
||||
assert_eq!(wrap_item(255, 23, 255, 24), vec![255, 23, 255, 24]);
|
||||
// Paper in the first bag's slot 3, the target in backpack slot 24: four distinct bytes.
|
||||
assert_eq!(wrap_item(19, 3, 255, 24), vec![19, 3, 255, 24]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -122,6 +122,12 @@ mod tests {
|
||||
);
|
||||
assert!(start.paused);
|
||||
assert_eq!(start.spell_id, 5697);
|
||||
|
||||
// The draining rate, -1 on the wire, reads back negative.
|
||||
body[12..16].copy_from_slice(&(-1i32).to_le_bytes());
|
||||
let start = read_start_mirror_timer(&mut &body[..]).unwrap();
|
||||
assert_eq!(start.scale, -1, "draining");
|
||||
assert_eq!(start.spell_id, 5697);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -60,8 +60,9 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// A last page's zero `nextPageId` reads as 0, the value that ends the chain.
|
||||
#[test]
|
||||
fn last_page_terminates_the_chain() {
|
||||
fn the_last_page_reads_a_zero_next_page_id() {
|
||||
let mut body = 9u32.to_le_bytes().to_vec();
|
||||
body.extend_from_slice(b"The end.\0");
|
||||
body.extend_from_slice(&0u32.to_le_bytes());
|
||||
|
||||
@@ -1636,8 +1636,9 @@ mod tests {
|
||||
assert_eq!(tail, 0);
|
||||
}
|
||||
|
||||
/// A count past the body ends in `UnexpectedEof` at the first missing row, in both opcodes.
|
||||
#[test]
|
||||
fn a_lying_faction_count_is_a_short_read_not_an_allocation() {
|
||||
fn a_lying_faction_count_is_a_short_read() {
|
||||
let mut body = 0xFFFF_FFFFu32.to_le_bytes().to_vec();
|
||||
body.push(0x01); // flags
|
||||
body.extend_from_slice(&3000i32.to_le_bytes()); // standing
|
||||
|
||||
@@ -517,9 +517,9 @@ mod tests {
|
||||
assert_eq!(d.reward_spell, 42);
|
||||
}
|
||||
|
||||
/// The pairs are read and discarded, so the check is that the fields after the run align.
|
||||
#[test]
|
||||
fn offer_reward_emotes_are_delay_first() {
|
||||
// Both emote values are discarded; this checks that the fields after them align.
|
||||
fn offer_reward_skips_its_counted_emote_pairs() {
|
||||
let mut b = Vec::new();
|
||||
push_u64(&mut b, 0x42);
|
||||
push_u32(&mut b, 100);
|
||||
|
||||
@@ -475,8 +475,9 @@ mod tests {
|
||||
assert_eq!(who(&request), want);
|
||||
}
|
||||
|
||||
/// A bare `/who` as bytes: levels 0 to 100, both names empty, every race and class, no lists.
|
||||
#[test]
|
||||
fn the_default_who_query_is_the_clients_unset_shape() {
|
||||
fn the_default_who_query_encodes_levels_0_to_100_with_every_filter_open() {
|
||||
let body = who(&WhoRequest::default());
|
||||
assert_eq!(&body[0..4], &0u32.to_le_bytes(), "levelMin");
|
||||
assert_eq!(&body[4..8], &100u32.to_le_bytes(), "levelMax — not 255");
|
||||
|
||||
@@ -293,12 +293,42 @@ mod tests {
|
||||
|
||||
// ── SMSG_TRADE_STATUS parse goldens ──
|
||||
|
||||
/// Every code without a tail reads as its variant and leaves the byte after the code unread.
|
||||
#[test]
|
||||
fn trade_status_bare_code_has_no_tail() {
|
||||
let buf = 2u32.to_le_bytes(); // OPEN_WINDOW
|
||||
let mut r = &buf[..];
|
||||
assert_eq!(read_trade_status(&mut r).unwrap(), TradeStatus::OpenWindow);
|
||||
assert!(r.is_empty(), "no tail should be consumed for a bare status");
|
||||
for (code, want) in [
|
||||
(0, TradeStatus::Busy),
|
||||
(2, TradeStatus::OpenWindow),
|
||||
(3, TradeStatus::Canceled),
|
||||
(4, TradeStatus::Accept),
|
||||
(5, TradeStatus::Busy2),
|
||||
(6, TradeStatus::NoTarget),
|
||||
(7, TradeStatus::BackToTrade),
|
||||
(8, TradeStatus::Complete),
|
||||
(9, TradeStatus::Rejected),
|
||||
(10, TradeStatus::TargetTooFar),
|
||||
(11, TradeStatus::WrongFaction),
|
||||
(13, TradeStatus::Unknown13),
|
||||
(14, TradeStatus::IgnoreYou),
|
||||
(15, TradeStatus::YouStunned),
|
||||
(16, TradeStatus::TargetStunned),
|
||||
(17, TradeStatus::YouDead),
|
||||
(18, TradeStatus::TargetDead),
|
||||
(19, TradeStatus::YouLogout),
|
||||
(20, TradeStatus::TargetLogout),
|
||||
(21, TradeStatus::TrialAccount),
|
||||
] {
|
||||
let mut buf = u32::to_le_bytes(code).to_vec();
|
||||
buf.push(0xEE); // the next byte, which a bare status leaves unread
|
||||
let mut r = &buf[..];
|
||||
assert_eq!(read_trade_status(&mut r).unwrap(), want, "code {code}");
|
||||
assert_eq!(
|
||||
r,
|
||||
[0xEE],
|
||||
"no tail should be consumed for bare status {code}"
|
||||
);
|
||||
assert_eq!(want.code(), code);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -394,12 +424,30 @@ mod tests {
|
||||
b
|
||||
}
|
||||
|
||||
/// The parser reads exactly 444 bytes, whether the slots are empty, full or mixed.
|
||||
#[test]
|
||||
fn extended_snapshot_is_always_444_bytes() {
|
||||
// A 17-byte header (u8 and four u32), then 7 records of u8 index and a 60-byte block.
|
||||
let wire = extended_wire(0, 0, 0, [None; TRADE_SLOT_COUNT]);
|
||||
assert_eq!(wire.len(), 17 + TRADE_SLOT_COUNT * 61);
|
||||
assert_eq!(wire.len(), 444);
|
||||
let mut mixed = [None; TRADE_SLOT_COUNT];
|
||||
mixed[0] = Some(0xABCD);
|
||||
mixed[TRADE_SLOT_NONTRADED] = Some(0x1111);
|
||||
for entries in [
|
||||
[None; TRADE_SLOT_COUNT],
|
||||
[Some(0xABCD); TRADE_SLOT_COUNT],
|
||||
mixed,
|
||||
] {
|
||||
let mut wire = extended_wire(0, 0, 0, entries);
|
||||
assert_eq!(wire.len(), 17 + TRADE_SLOT_COUNT * 61);
|
||||
assert_eq!(wire.len(), 444);
|
||||
assert!(
|
||||
read_trade_status_extended(&mut &wire[..443]).is_err(),
|
||||
"443 bytes is a short read"
|
||||
);
|
||||
wire.push(0xEE); // a byte past the snapshot
|
||||
let mut r = &wire[..];
|
||||
read_trade_status_extended(&mut r).unwrap();
|
||||
assert_eq!(r, [0xEE], "the parser stops at byte 444");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -173,7 +173,7 @@ mod tests {
|
||||
/// Every `with_capacity` under `messages/` whose argument is a bare variable goes through
|
||||
/// [`capacity_hint`], carries a `.min(`, or is listed in [`EXEMPT`].
|
||||
#[test]
|
||||
fn wire_count_capacity_hints_are_capped() {
|
||||
fn bare_variable_capacity_hints_under_messages_are_capped() {
|
||||
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src/messages");
|
||||
let mut offenders = Vec::new();
|
||||
for file in rust_files(&root) {
|
||||
@@ -195,7 +195,7 @@ mod tests {
|
||||
process instead of skipping the packet. Bound each with \
|
||||
`crate::wire::capacity_hint(count, CAP)` (CAP = the protocol's own bound, cited at \
|
||||
the site, or a generous sane one), or add it to `EXEMPT` in `wire.rs` with the reason \
|
||||
it is not a wire count (decision 2265 §B1):\n {}",
|
||||
it is not a wire count:\n {}",
|
||||
offenders.join("\n ")
|
||||
);
|
||||
}
|
||||
|
||||
@@ -36,12 +36,13 @@ pub(super) fn movement_info(pos: [f32; 3], orientation: f32, flags: u32) -> Move
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Never 0, and a stamp taken right after is not smaller; it wraps at `u32::MAX` ms.
|
||||
#[test]
|
||||
fn client_uptime_ms_is_nonzero_and_monotonic() {
|
||||
fn client_uptime_ms_is_nonzero_and_back_to_back_calls_do_not_decrease() {
|
||||
let a = client_uptime_ms();
|
||||
let b = client_uptime_ms();
|
||||
assert!(a >= 1, "stamp is non-zero: {a}");
|
||||
assert!(b >= a, "stamp is monotonic non-decreasing: {a} -> {b}");
|
||||
assert!(b >= a, "the next stamp is not smaller: {a} -> {b}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -529,6 +529,13 @@ fn name_and_creature_query_roundtrip() {
|
||||
"CMSG_CREATURE_QUERY body"
|
||||
);
|
||||
|
||||
// CMSG_NAME_QUERY: the full guid (vmangos `QueryPlayerName`), built by `full_guid`.
|
||||
assert_eq!(
|
||||
messages::full_guid(7),
|
||||
hx("0700000000000000"),
|
||||
"CMSG_NAME_QUERY body"
|
||||
);
|
||||
|
||||
// SMSG_NAME_QUERY_RESPONSE (vmangos `NameQueryResponse::AppendBodyTo`): guid, name, an empty
|
||||
// realm cstring, then race, gender and class as `u32`.
|
||||
let body = hx("070000000000000042656e696c6c610000010000000000000001000000");
|
||||
|
||||
@@ -249,6 +249,16 @@ fn npc_text_update_greeting_extraction() {
|
||||
SessionEvent::NpcGreeting { text_id, blocks } => {
|
||||
assert_eq!(text_id, 321);
|
||||
assert_eq!(blocks.len(), messages::NPC_TEXT_BLOCKS);
|
||||
|
||||
// A near-zero threshold draws the first block with text in the gender's column, the
|
||||
// full sum the last; the male column skips block 6, the female column block 0.
|
||||
const NEAR: f32 = 1.999_999_9;
|
||||
const FAR: f32 = 1.0;
|
||||
let pick = |gender, roll| messages::select_greeting(&blocks, gender, roll);
|
||||
assert_eq!(pick(0, NEAR), Some("Low probability greeting"));
|
||||
assert_eq!(pick(0, FAR), Some("Welcome, $N!"));
|
||||
assert_eq!(pick(1, NEAR), Some("Welcome, traveler!"));
|
||||
assert_eq!(pick(1, FAR), Some("Female-only greeting"));
|
||||
}
|
||||
other => panic!("npc greeting event, got {other:?}"),
|
||||
}
|
||||
|
||||
@@ -337,6 +337,12 @@ fn group_list_raid_with_assistant_flag() {
|
||||
fn group_list_empty_you_left_shape_is_14_bytes() {
|
||||
let body = vec![0u8; 14];
|
||||
assert_eq!(body.len(), 14);
|
||||
let (_, tail) = messages::parse_server_with_tail(opcode::SMSG_GROUP_LIST, &body).unwrap();
|
||||
assert_eq!(tail, 0, "the parser reads all 14 bytes");
|
||||
assert!(
|
||||
messages::parse_server(opcode::SMSG_GROUP_LIST, &body[..13]).is_err(),
|
||||
"13 bytes is a short read"
|
||||
);
|
||||
|
||||
let p = messages::parse_server(opcode::SMSG_GROUP_LIST, &body).unwrap();
|
||||
match &p {
|
||||
|
||||
@@ -524,14 +524,21 @@ fn compressed_moves_unwraps_to_the_same_events_as_loose_relays() {
|
||||
),
|
||||
}
|
||||
|
||||
let loose = decode(
|
||||
messages::parse_server(
|
||||
// The batch's two records, relayed loose under their own opcodes.
|
||||
let loose: Vec<SessionEvent> = [
|
||||
(
|
||||
messages::opcode::MSG_MOVE_START_FORWARD,
|
||||
&hx("01aa0100000004030201cdd70bc6357e04c3f90fa7420000a03f00000000"),
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
assert_eq!(format!("{:?}", events[0]), format!("{:?}", loose[0]));
|
||||
"01aa0100000004030201cdd70bc6357e04c3f90fa7420000a03f00000000",
|
||||
),
|
||||
(
|
||||
messages::opcode::MSG_MOVE_SET_FACING,
|
||||
"01aa0000000004030201cdd70bc6357e04c3f90fa7420000c03f00000000",
|
||||
),
|
||||
]
|
||||
.into_iter()
|
||||
.flat_map(|(opcode, body)| decode(messages::parse_server(opcode, &hx(body)).unwrap()))
|
||||
.collect();
|
||||
assert_eq!(format!("{events:?}"), format!("{loose:?}"));
|
||||
}
|
||||
|
||||
/// A misframed batch is a parse error, never a silent drop of the moves it carries.
|
||||
|
||||
@@ -282,7 +282,7 @@ fn the_two_results_packets_share_an_enum_but_not_a_layout() {
|
||||
/// `MSG_PETITION_DECLINE` sends the charter's item guid but receives the decliner's player guid,
|
||||
/// both eight bytes; `MSG_PETITION_RENAME` is `u64` item and cstring name both ways.
|
||||
#[test]
|
||||
fn msg_petition_opcodes_read_a_different_body_than_they_write() {
|
||||
fn msg_petition_decline_differs_by_direction_and_rename_does_not() {
|
||||
assert_eq!(
|
||||
messages::petition_decline(0x0001_0203),
|
||||
hx("0302010000000000"),
|
||||
|
||||
@@ -55,19 +55,31 @@ fn push_result_decodes_member_and_verdict() {
|
||||
}
|
||||
}
|
||||
|
||||
/// An unmapped verdict byte reaches the app as data rather than failing the parse.
|
||||
/// Every verdict byte survives encode, parse and decode; an unmapped one is data, not an error.
|
||||
#[test]
|
||||
fn every_verdict_byte_round_trips() {
|
||||
for raw in [0u8, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0xFF] {
|
||||
for raw in 0..=u8::MAX {
|
||||
let mut body = 1u64.to_le_bytes().to_vec();
|
||||
body.push(raw);
|
||||
match messages::parse_server(opcode::MSG_QUEST_PUSH_RESULT, &body).unwrap() {
|
||||
assert_eq!(
|
||||
messages::quest_push_result(1, QuestShareMsg(raw)),
|
||||
body,
|
||||
"verdict {raw} encodes"
|
||||
);
|
||||
let p = messages::parse_server(opcode::MSG_QUEST_PUSH_RESULT, &body).unwrap();
|
||||
match &p {
|
||||
ServerPacket::QuestPushResult(r) => {
|
||||
assert_eq!(r.msg, QuestShareMsg(raw), "verdict {raw}");
|
||||
assert_eq!(r.member, 1);
|
||||
}
|
||||
other => panic!("verdict {raw} parsed as {}", other.name()),
|
||||
}
|
||||
match decode(p).as_slice() {
|
||||
[SessionEvent::QuestPushResult { member: 1, msg }] => {
|
||||
assert_eq!(*msg, QuestShareMsg(raw), "verdict {raw} decodes");
|
||||
}
|
||||
other => panic!("verdict {raw} decoded to {other:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ fn simple_server_bodies_parse() {
|
||||
|
||||
/// The response is one u32 of unix-epoch seconds (vmangos `Handlers/QueryHandler.cpp:418-423`).
|
||||
#[test]
|
||||
fn query_time_round_trips_the_server_wall_clock() {
|
||||
fn query_time_asks_empty_and_decodes_the_server_wall_clock() {
|
||||
assert!(
|
||||
messages::query_time().is_empty(),
|
||||
"CMSG_QUERY_TIME is a NullClientPacket"
|
||||
@@ -77,7 +77,8 @@ fn query_time_round_trips_the_server_wall_clock() {
|
||||
);
|
||||
}
|
||||
|
||||
/// `SMSG_TRIGGER_CINEMATIC` is one u32 `CinematicSequences.dbc` id; `SMSG_CHAR_DELETE` one byte.
|
||||
/// `SMSG_TRIGGER_CINEMATIC` is one u32 `CinematicSequences.dbc` id; `SMSG_CHAR_DELETE` one byte,
|
||||
/// which the handshake consumes, so it decodes to no event.
|
||||
#[test]
|
||||
fn cinematic_and_char_delete_parse_and_decode() {
|
||||
// 41 = the dwarf intro.
|
||||
@@ -95,6 +96,7 @@ fn cinematic_and_char_delete_parse_and_decode() {
|
||||
// 0x39 = CHAR_DELETE_SUCCESS.
|
||||
let p = messages::parse_server(messages::opcode::SMSG_CHAR_DELETE, &[0x39]).unwrap();
|
||||
assert!(matches!(p, ServerPacket::CharDelete { result: 0x39 }));
|
||||
assert!(decode(p).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -131,7 +131,7 @@ fn spell_and_action_wire() {
|
||||
assert_eq!(
|
||||
messages::set_actionbar_toggles(0x0f),
|
||||
hx("0f"),
|
||||
"all four — the largest value the reference binding can ever accumulate (§2)"
|
||||
"all four — the largest value the reference binding can ever accumulate"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
@@ -1103,9 +1103,9 @@ fn spell_chain_targets_wire() {
|
||||
);
|
||||
}
|
||||
|
||||
/// Eight more combat-log bodies, each checked against the vmangos writer cited at it.
|
||||
/// Eight combat-log opcodes, each parsed against the vmangos writer cited at it.
|
||||
#[test]
|
||||
fn combat_log_completeness_wire() {
|
||||
fn kill_outcome_dispel_enchantment_and_execute_logs_parse() {
|
||||
use benilla_protocol::messages::{
|
||||
DispelFailed, EnchantmentLog, ExecuteLog, PartyKillLog, SpellDispelLog, SpellInstaKillLog,
|
||||
SpellLogExecute, SpellOutcomeLog,
|
||||
|
||||
@@ -149,9 +149,9 @@ fn an_empty_list_still_carries_the_purchased_slot_count() {
|
||||
assert!(pets.is_empty());
|
||||
}
|
||||
|
||||
/// `SUCCESS_UNSTABLE` answers both an unstable and a swap, so the reply never names the verb.
|
||||
/// Each result byte parses as itself, and the codes are vmangos's `StableResultCode` values.
|
||||
#[test]
|
||||
fn stable_result_codes_round_trip() {
|
||||
fn stable_result_codes_parse_as_their_vmangos_bytes() {
|
||||
for code in [
|
||||
stable_result::ERR_MONEY,
|
||||
stable_result::ERR_STABLE,
|
||||
|
||||
Reference in New Issue
Block a user