#![allow(warnings)]
use std::time::Instant as StdInstant;
use super::*;
use crate::{
Name, QueryHandle,
event::QueryEvent,
wire::{MessageReader, ResourceClass, ResourceType},
};
type TestQuery = Query<StdInstant, slab::Slab<CollectedAnswer>, slab::Slab<QueryUpdate>>;
fn make_a_response(name: &[u8], ip: [u8; 4]) -> std::vec::Vec<u8> {
let mut msg: std::vec::Vec<u8> = std::vec::Vec::new();
msg.extend_from_slice(&[
0x00, 0x00, 0x84, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00,
]);
msg.push(name.len() as u8);
msg.extend_from_slice(name);
msg.push(0x00); msg.extend_from_slice(&[0x00, 0x01]);
msg.extend_from_slice(&[0x00, 0x01]);
msg.extend_from_slice(&[0x00, 0x00, 0x00, 0x78]);
msg.extend_from_slice(&[0x00, 0x04]);
msg.extend_from_slice(&ip);
msg
}
fn make_query(qtype: ResourceType, qclass: ResourceClass) -> TestQuery {
let handle = QueryHandle::from_raw(0);
let qname = Name::try_from_str("printer.local.").unwrap();
TestQuery::try_new(handle, qname, qtype, qclass, 1, false, None)
}
fn inject_a_record(q: &mut TestQuery, msg: &[u8]) {
let reader = MessageReader::try_parse(msg).unwrap();
let record = reader.answers().next().unwrap().unwrap();
q.handle_event(QueryEvent::Answer(record));
}
#[test]
fn identical_answers_are_deduped() {
let mut q = make_query(ResourceType::Any, ResourceClass::Any);
let msg = make_a_response(b"printer", [192, 168, 1, 10]);
inject_a_record(&mut q, &msg);
inject_a_record(&mut q, &msg);
inject_a_record(&mut q, &msg);
assert_eq!(q.answers.len(), 1, "duplicate answers must be deduped");
}
#[test]
fn non_name_answer_stores_no_separate_identity_key() {
let mut q = make_query(ResourceType::Any, ResourceClass::Any);
let msg = make_a_response(b"printer", [192, 168, 1, 10]);
inject_a_record(&mut q, &msg);
let ans = q.collected_answers().next().unwrap();
assert!(
ans.rdata_key.is_none(),
"non-name rdata must not allocate a separate folded key buffer"
);
assert_eq!(ans.rdata_key(), ans.rdata_slice());
}
#[test]
fn different_rdata_not_deduped() {
let mut q = make_query(ResourceType::Any, ResourceClass::Any);
let msg1 = make_a_response(b"printer", [192, 168, 1, 10]);
let msg2 = make_a_response(b"printer", [192, 168, 1, 11]);
inject_a_record(&mut q, &msg1);
inject_a_record(&mut q, &msg2);
assert_eq!(q.answers.len(), 2, "different rdata must not be deduped");
}
#[test]
fn ptr_answer_with_compressed_target_is_decompressed() {
let mut q = make_query(ResourceType::Ptr, ResourceClass::In);
let mut msg: std::vec::Vec<u8> = std::vec::Vec::new();
msg.extend_from_slice(&[0, 0, 0x84, 0x00, 0, 0, 0, 1, 0, 0, 0, 0]);
msg.extend_from_slice(&[3, b's', b'v', b'c', 5, b'l', b'o', b'c', b'a', b'l', 0]);
msg.extend_from_slice(&12u16.to_be_bytes()); msg.extend_from_slice(&1u16.to_be_bytes()); msg.extend_from_slice(&120u32.to_be_bytes()); msg.extend_from_slice(&7u16.to_be_bytes()); msg.extend_from_slice(&[4, b'i', b'n', b's', b't', 0xC0, 0x0C]);
inject_a_record(&mut q, &msg);
assert_eq!(q.answers.len(), 1);
let ans = q.collected_answers().next().unwrap();
let expected: &[u8] = &[
4, b'i', b'n', b's', b't', 3, b's', b'v', b'c', 5, b'l', b'o', b'c', b'a', b'l', 0,
];
assert_eq!(
ans.rdata_slice(),
expected,
"PTR target must be stored as a decompressed self-contained wire name"
);
}
#[test]
fn ptr_answers_differing_only_in_case_are_deduped() {
let mut q = make_query(ResourceType::Ptr, ResourceClass::In);
let ptr_with_target = |first_label: &[u8]| -> std::vec::Vec<u8> {
let mut msg: std::vec::Vec<u8> = std::vec::Vec::new();
msg.extend_from_slice(&[0, 0, 0x84, 0x00, 0, 0, 0, 1, 0, 0, 0, 0]);
msg.extend_from_slice(&[3, b's', b'v', b'c', 5, b'l', b'o', b'c', b'a', b'l', 0]);
msg.extend_from_slice(&12u16.to_be_bytes()); msg.extend_from_slice(&1u16.to_be_bytes()); msg.extend_from_slice(&120u32.to_be_bytes()); let mut rdata: std::vec::Vec<u8> = std::vec::Vec::new();
rdata.push(first_label.len() as u8);
rdata.extend_from_slice(first_label);
rdata.extend_from_slice(&[3, b's', b'v', b'c', 5, b'l', b'o', b'c', b'a', b'l', 0]);
msg.extend_from_slice(&(rdata.len() as u16).to_be_bytes());
msg.extend_from_slice(&rdata);
msg
};
inject_a_record(&mut q, &ptr_with_target(b"INST"));
inject_a_record(&mut q, &ptr_with_target(b"inst"));
assert_eq!(
q.answers.len(),
1,
"case-only PTR variants must collapse to a single collected answer"
);
let ans = q.collected_answers().next().unwrap();
assert_eq!(
ans.rdata_slice(),
&[
4, b'I', b'N', b'S', b'T', 3, b's', b'v', b'c', 5, b'l', b'o', b'c', b'a', b'l', 0
][..],
"the first-seen display case must be preserved"
);
assert_eq!(
ans.rdata_key(),
&[
4, b'i', b'n', b's', b't', 3, b's', b'v', b'c', 5, b'l', b'o', b'c', b'a', b'l', 0
][..],
"the identity key must be case-folded"
);
}
#[test]
fn qtype_filter_drops_non_matching_records() {
let mut q = make_query(ResourceType::AAAA, ResourceClass::Any);
let msg = make_a_response(b"printer", [192, 168, 1, 10]);
inject_a_record(&mut q, &msg);
assert_eq!(
q.answers.len(),
0,
"A record should be dropped for AAAA query"
);
}
#[test]
fn qtype_any_accepts_all_rtypes() {
let mut q = make_query(ResourceType::Any, ResourceClass::Any);
let msg = make_a_response(b"printer", [192, 168, 1, 10]);
inject_a_record(&mut q, &msg);
assert_eq!(q.answers.len(), 1, "Any qtype should accept A records");
}
#[test]
fn qtype_exact_match_accepted() {
let mut q = make_query(ResourceType::A, ResourceClass::Any);
let msg = make_a_response(b"printer", [192, 168, 1, 10]);
inject_a_record(&mut q, &msg);
assert_eq!(q.answers.len(), 1, "exact rtype match should be accepted");
}
#[test]
fn answers_capped_at_max_answers() {
let mut q = make_query(ResourceType::A, ResourceClass::Any).with_max_answers(3);
for i in 0u8..5 {
let msg = make_a_response(b"printer", [10, 0, 0, i]);
inject_a_record(&mut q, &msg);
}
assert_eq!(
q.answers.len(),
3,
"answers must be capped at max_answers=3"
);
}
#[test]
fn max_answers_zero_disables_collection() {
let mut q = make_query(ResourceType::A, ResourceClass::Any).with_max_answers(0);
let msg = make_a_response(b"printer", [10, 0, 0, 1]);
inject_a_record(&mut q, &msg);
assert_eq!(q.answers.len(), 0, "max_answers=0 must disable collection");
}
#[test]
fn accepted_count_tracks_evictions_beyond_the_cap() {
let mut q = make_query(ResourceType::A, ResourceClass::Any).with_max_answers(1);
for i in 0u8..5 {
let msg = make_a_response(b"printer", [10, 0, 0, i]);
inject_a_record(&mut q, &msg);
}
assert_eq!(q.collected_answers().count(), 1, "snapshot capped at 1");
assert_eq!(q.accepted_count(), 5, "but all 5 were accepted");
assert_eq!(q.accepted_count() - q.collected_answers().count() as u64, 4);
}
#[test]
fn cap_evicts_oldest_on_overflow() {
let mut q = make_query(ResourceType::A, ResourceClass::Any).with_max_answers(2);
let msg_a = make_a_response(b"printer", [10, 0, 0, 0]); let msg_b = make_a_response(b"printer", [10, 0, 0, 1]); let msg_c = make_a_response(b"printer", [10, 0, 0, 2]); let msg_d = make_a_response(b"printer", [10, 0, 0, 3]); inject_a_record(&mut q, &msg_a);
inject_a_record(&mut q, &msg_b);
inject_a_record(&mut q, &msg_c);
inject_a_record(&mut q, &msg_d);
assert_eq!(q.answers.len(), 2, "pool must stay at cap after evictions");
let retained: std::vec::Vec<[u8; 4]> = q
.answers
.iter()
.filter_map(|(_, a)| {
let s = a.rdata_slice();
s.get(..4).and_then(|b| b.try_into().ok())
})
.collect();
assert!(
retained.contains(&[10, 0, 0, 2]),
"C (10.0.0.2) must be retained; got {:?}",
retained
);
assert!(
retained.contains(&[10, 0, 0, 3]),
"D (10.0.0.3) must be retained; got {:?}",
retained
);
assert!(
!retained.contains(&[10, 0, 0, 0]),
"A (10.0.0.0) must have been evicted; got {:?}",
retained
);
assert!(
!retained.contains(&[10, 0, 0, 1]),
"B (10.0.0.1) must have been evicted; got {:?}",
retained
);
}
#[derive(Debug)]
struct CappedPoolFull;
impl core::fmt::Display for CappedPoolFull {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str("capped pool full")
}
}
impl core::error::Error for CappedPoolFull {}
struct CappedPool<const N: usize> {
slots: std::vec::Vec<Option<CollectedAnswer>>,
}
impl<const N: usize> Pool<CollectedAnswer> for CappedPool<N> {
type Error = CappedPoolFull;
type Iter<'a> = std::boxed::Box<dyn Iterator<Item = (usize, &'a CollectedAnswer)> + 'a>;
type IterMut<'a> = std::boxed::Box<dyn Iterator<Item = (usize, &'a mut CollectedAnswer)> + 'a>;
fn new() -> Self {
Self {
slots: std::vec::Vec::new(),
}
}
fn with_capacity(_capacity: usize) -> Result<Self, Self::Error> {
Ok(Self::new())
}
fn vacant_key(&self) -> Result<usize, Self::Error> {
if let Some(i) = self.slots.iter().position(|s| s.is_none()) {
Ok(i)
} else if self.slots.len() < N {
Ok(self.slots.len())
} else {
Err(CappedPoolFull)
}
}
fn is_empty(&self) -> bool {
self.slots.iter().all(|s| s.is_none())
}
fn len(&self) -> usize {
self.slots.iter().filter(|s| s.is_some()).count()
}
fn get(&self, key: usize) -> Option<&CollectedAnswer> {
self.slots.get(key).and_then(|s| s.as_ref())
}
fn get_mut(&mut self, key: usize) -> Option<&mut CollectedAnswer> {
self.slots.get_mut(key).and_then(|s| s.as_mut())
}
fn insert(&mut self, value: CollectedAnswer) -> Result<usize, Self::Error> {
if let Some(i) = self.slots.iter().position(|s| s.is_none()) {
self.slots[i] = Some(value);
Ok(i)
} else if self.slots.len() < N {
self.slots.push(Some(value));
Ok(self.slots.len() - 1)
} else {
Err(CappedPoolFull)
}
}
fn try_remove(&mut self, key: usize) -> Option<CollectedAnswer> {
self.slots.get_mut(key).and_then(|s| s.take())
}
fn iter(&self) -> Self::Iter<'_> {
std::boxed::Box::new(
self
.slots
.iter()
.enumerate()
.filter_map(|(i, s)| s.as_ref().map(|v| (i, v))),
)
}
fn iter_mut(&mut self) -> Self::IterMut<'_> {
std::boxed::Box::new(
self
.slots
.iter_mut()
.enumerate()
.filter_map(|(i, s)| s.as_mut().map(|v| (i, v))),
)
}
}
#[test]
fn fixed_capacity_pool_below_max_answers_evicts_and_advances_seq() {
type CappedQuery = Query<StdInstant, CappedPool<2>, slab::Slab<QueryUpdate>>;
let mut q: CappedQuery = CappedQuery::try_new(
QueryHandle::from_raw(0),
Name::try_from_str("printer.local.").unwrap(),
ResourceType::A,
ResourceClass::Any,
1,
false,
None,
)
.with_max_answers(10);
for i in 0u8..5 {
let msg = make_a_response(b"printer", [10, 0, 0, i]);
let reader = MessageReader::try_parse(&msg).unwrap();
let record = reader.answers().next().unwrap().unwrap();
q.handle_event(QueryEvent::Answer(record));
}
assert_eq!(
q.answers.len(),
2,
"answers must be bounded by the pool's hard capacity, not max_answers"
);
assert_eq!(
q.accepted_count(),
5,
"next_seq must advance once per successful insert"
);
let retained: std::vec::Vec<u8> = q
.answers
.iter()
.filter_map(|(_, a)| a.rdata_slice().get(3).copied())
.collect();
assert!(
retained.contains(&3) && retained.contains(&4),
"drop-oldest must retain the newest answers; got {:?}",
retained
);
assert!(
!retained.contains(&0) && !retained.contains(&1),
"the oldest answers must have been evicted; got {:?}",
retained
);
}
#[test]
fn query_emits_unicast_response_bit_when_spec_set() {
use crate::wire::{MessageReader, QuestionRef};
let handle = QueryHandle::from_raw(0);
let qname = Name::try_from_str("host.local.").unwrap();
let now = StdInstant::now();
let mut q: TestQuery = TestQuery::try_new(
handle,
qname,
ResourceType::A,
ResourceClass::In,
42,
true, None,
);
let mut buf = [0u8; 512];
let result = q.poll_transmit(now, &mut buf).unwrap();
let n = result.expect("expected a transmit to be produced").size();
let reader = MessageReader::try_parse(&buf[..n]).expect("datagram must parse");
let question: QuestionRef<'_> = reader
.questions()
.next()
.expect("expected one question")
.expect("question must parse");
assert!(
question.unicast_response_requested(),
"QU bit must be set when unicast_response=true"
);
}
#[test]
fn query_omits_unicast_response_bit_by_default() {
use crate::wire::{MessageReader, QuestionRef};
let handle = QueryHandle::from_raw(0);
let qname = Name::try_from_str("host.local.").unwrap();
let now = StdInstant::now();
let mut q: TestQuery = TestQuery::try_new(
handle,
qname,
ResourceType::A,
ResourceClass::In,
43,
false, None,
);
let mut buf = [0u8; 512];
let result = q.poll_transmit(now, &mut buf).unwrap();
let n = result.expect("expected a transmit to be produced").size();
let reader = MessageReader::try_parse(&buf[..n]).expect("datagram must parse");
let question: QuestionRef<'_> = reader
.questions()
.next()
.expect("expected one question")
.expect("question must parse");
assert!(
!question.unicast_response_requested(),
"QU bit must NOT be set when unicast_response=false"
);
}
#[test]
fn query_times_out_at_absolute_deadline() {
use core::time::Duration;
let handle = QueryHandle::from_raw(0);
let qname = Name::try_from_str("host.local.").unwrap();
let now = StdInstant::now();
let deadline = now.checked_add(Duration::from_millis(100)).unwrap();
let mut q: TestQuery = TestQuery::try_new(
handle,
qname,
ResourceType::A,
ResourceClass::In,
44,
false,
Some(deadline),
);
let before = now.checked_add(Duration::from_millis(50)).unwrap();
q.handle_timeout(before).unwrap();
assert!(
q.poll().is_none() || !matches!(q.poll(), Some(QueryUpdate::Timeout)),
"query must not time out before the absolute deadline"
);
assert!(
!q.done,
"query must not be done before the absolute deadline"
);
let after = now.checked_add(Duration::from_millis(200)).unwrap();
q.handle_timeout(after).unwrap();
assert!(q.done, "query must be done after the absolute deadline");
assert!(
matches!(q.poll(), Some(QueryUpdate::Timeout)),
"query must emit QueryUpdate::Timeout at the absolute deadline"
);
}
#[test]
fn query_without_timeout_deadline_does_not_cancel_early() {
use core::time::Duration;
let handle = QueryHandle::from_raw(0);
let qname = Name::try_from_str("host.local.").unwrap();
let now = StdInstant::now();
let mut q: TestQuery = TestQuery::try_new(
handle,
qname,
ResourceType::A,
ResourceClass::In,
45,
false,
None, );
let soon = now.checked_add(Duration::from_secs(1)).unwrap();
q.handle_timeout(soon).unwrap();
assert!(
!q.done,
"query without a timeout_deadline must not auto-cancel at 1s"
);
}
#[test]
fn poll_timeout_reflects_absolute_timeout() {
use core::time::Duration;
let handle = QueryHandle::from_raw(0);
let qname = Name::try_from_str("host.local.").unwrap();
let now = StdInstant::now();
let timeout_dl = now.checked_add(Duration::from_millis(100)).unwrap();
let mut q: TestQuery = TestQuery::try_new(
handle,
qname,
ResourceType::A,
ResourceClass::In,
46,
false,
Some(timeout_dl),
);
let pt0 = q
.poll_timeout()
.expect("poll_timeout must be Some on construction");
assert!(
pt0 <= timeout_dl,
"initial poll_timeout must be <= timeout_deadline"
);
let mut buf = [0u8; 512];
let _ = q.poll_transmit(now, &mut buf).unwrap();
q.handle_timeout(now).unwrap();
assert!(!q.done, "query must not be done immediately");
let pt1 = q
.poll_timeout()
.expect("poll_timeout must be Some after first retry");
assert!(
pt1 <= timeout_dl,
"poll_timeout must return <= timeout_deadline (got instant past the timeout)"
);
let at_timeout = now.checked_add(Duration::from_millis(150)).unwrap();
q.handle_timeout(at_timeout).unwrap();
assert!(q.done, "query must be done after timeout_deadline");
assert!(
matches!(q.poll(), Some(crate::event::QueryUpdate::Timeout)),
"query must emit Timeout after absolute deadline fires"
);
assert!(
q.poll_timeout().is_none(),
"poll_timeout must be None after query is done"
);
}
#[test]
fn first_retry_is_one_second_and_no_same_tick_duplicate() {
use core::time::Duration;
let handle = QueryHandle::from_raw(0);
let qname = Name::try_from_str("host.local.").unwrap();
let now = StdInstant::now();
let mut q: TestQuery = TestQuery::try_new(
handle,
qname,
ResourceType::A,
ResourceClass::In,
48,
false,
None,
);
let mut buf = [0u8; 512];
assert!(
q.poll_transmit(now, &mut buf).unwrap().is_some(),
"initial query must be sent at now"
);
q.note_transmit_result(now, true);
let one_sec = now.checked_add(Duration::from_secs(1)).unwrap();
assert_eq!(
q.poll_timeout(),
Some(one_sec),
"first retry must be scheduled at now + 1s, not now"
);
q.handle_timeout(now).unwrap();
assert!(
q.poll_transmit(now, &mut buf).unwrap().is_none(),
"no duplicate query may be emitted at now"
);
assert_eq!(
q.poll_timeout(),
Some(one_sec),
"next wake must remain now + 1s after a no-op tick at now"
);
q.handle_timeout(one_sec).unwrap();
assert!(
q.poll_transmit(one_sec, &mut buf).unwrap().is_some(),
"first retry must be sent at now + 1s"
);
q.note_transmit_result(one_sec, true);
let three_sec = now.checked_add(Duration::from_secs(3)).unwrap();
assert_eq!(
q.poll_timeout(),
Some(three_sec),
"second retry must be scheduled at now + 3s (interval doubles to 2s)"
);
}
#[test]
fn failed_send_does_not_consume_retry_budget() {
use core::time::Duration;
let handle = QueryHandle::from_raw(0);
let qname = Name::try_from_str("host.local.").unwrap();
let now = StdInstant::now();
let mut q: TestQuery = TestQuery::try_new(
handle,
qname,
ResourceType::A,
ResourceClass::In,
49,
false,
None,
);
let mut buf = [0u8; 512];
let mut t = now;
for _ in 0..50 {
let sent = q.poll_transmit(t, &mut buf).unwrap().is_some();
assert!(sent, "query must keep attempting to send while undelivered");
q.note_transmit_result(t, false); assert!(!q.is_done(), "an undelivered query must NOT time out");
let due = q.poll_timeout().expect("a re-attempt must be scheduled");
t = due;
q.handle_timeout(t).unwrap();
}
assert!(
!q.is_done(),
"after 50 failed sends the query must still be live (budget untouched)"
);
assert!(q.poll_transmit(t, &mut buf).unwrap().is_some());
q.note_transmit_result(t, true);
let plus_1s = t.checked_add(Duration::from_secs(1)).unwrap();
assert_eq!(
q.poll_timeout(),
Some(plus_1s),
"after the first delivered send the retry is scheduled +1s out"
);
}
#[test]
fn query_retry_anchored_to_confirmation_time() {
use core::time::Duration;
let handle = QueryHandle::from_raw(0);
let qname = Name::try_from_str("host.local.").unwrap();
let t0 = StdInstant::now();
let mut q: TestQuery = TestQuery::try_new(
handle,
qname,
ResourceType::A,
ResourceClass::In,
50,
false,
None,
);
let mut buf = [0u8; 512];
assert!(q.poll_transmit(t0, &mut buf).unwrap().is_some());
let send_done = t0.checked_add(Duration::from_secs(5)).unwrap();
q.note_transmit_result(send_done, true);
assert_eq!(
q.poll_timeout(),
send_done.checked_add(Duration::from_secs(1)),
"retry backoff must be anchored to the confirmed-send time, not poll_transmit time"
);
}
#[test]
fn is_done_is_a_level_signal_independent_of_poll() {
use core::time::Duration;
let handle = QueryHandle::from_raw(0);
let qname = Name::try_from_str("host.local.").unwrap();
let now = StdInstant::now();
let timeout_dl = now.checked_add(Duration::from_millis(100)).unwrap();
let mut q: TestQuery = TestQuery::try_new(
handle,
qname,
ResourceType::A,
ResourceClass::In,
47,
false,
Some(timeout_dl),
);
assert!(!q.is_done(), "fresh query must not be done");
let at_timeout = now.checked_add(Duration::from_millis(150)).unwrap();
q.handle_timeout(at_timeout).unwrap();
assert!(q.is_done(), "is_done() must be true after timeout");
let update = q.poll();
assert!(
matches!(update, Some(crate::event::QueryUpdate::Timeout)),
"expected QueryUpdate::Timeout from poll(); got {update:?}"
);
assert!(
q.is_done(),
"is_done() must remain true after poll() drained the terminal update; \
this is the contract that lets cleanup work under a full pending_updates pool"
);
assert!(q.poll().is_none(), "no further updates after drain");
assert!(
q.is_done(),
"is_done() must remain true indefinitely after termination"
);
}
#[test]
fn txid_accessor_returns_transaction_id() {
let q = make_query(ResourceType::Any, ResourceClass::In);
assert_eq!(q.txid(), 1, "make_query builds the query with txid 1");
}
#[test]
fn truncated_event_collects_nothing() {
let mut q = make_query(ResourceType::Any, ResourceClass::Any);
q.handle_event(QueryEvent::Truncated);
assert_eq!(q.answers.len(), 0, "a Truncated event collects no answer");
}
#[test]
fn answer_in_non_matching_class_is_dropped() {
let mut q = make_query(ResourceType::A, ResourceClass::In);
let mut msg = make_a_response(b"printer", [192, 168, 1, 10]);
msg[23] = 0x00;
msg[24] = 0xFF; inject_a_record(&mut q, &msg);
assert_eq!(
q.answers.len(),
0,
"an answer whose class doesn't match the query (and isn't ANY) is dropped"
);
}
#[test]
fn answer_with_undecodable_rdata_is_dropped() {
let mut q = make_query(ResourceType::Ptr, ResourceClass::In);
let mut msg: std::vec::Vec<u8> = std::vec::Vec::new();
msg.extend_from_slice(&[0, 0, 0x84, 0x00, 0, 0, 0, 1, 0, 0, 0, 0]); msg.extend_from_slice(&[6, b'p', b'r', b'i', b'n', b't', b'r', 0]); msg.extend_from_slice(&12u16.to_be_bytes()); msg.extend_from_slice(&1u16.to_be_bytes()); msg.extend_from_slice(&120u32.to_be_bytes()); msg.extend_from_slice(&1u16.to_be_bytes()); msg.push(5); inject_a_record(&mut q, &msg);
assert_eq!(
q.answers.len(),
0,
"an answer with undecodable rdata is dropped"
);
}
#[test]
fn handle_timeout_is_noop_when_done() {
let mut q = make_query(ResourceType::Any, ResourceClass::Any);
q.retire();
assert!(q.handle_timeout(StdInstant::now()).is_ok());
}
#[test]
fn retire_is_idempotent() {
let mut q = make_query(ResourceType::Any, ResourceClass::Any);
q.retire();
q.retire(); let mut timeouts = 0;
while let Some(u) = q.poll() {
if matches!(u, QueryUpdate::Timeout) {
timeouts += 1;
}
}
assert_eq!(
timeouts, 1,
"retire must queue exactly one Timeout terminal"
);
}
#[test]
fn note_transmit_result_without_a_pending_send_is_noop() {
let mut q = make_query(ResourceType::Any, ResourceClass::Any);
q.note_transmit_result(StdInstant::now(), true);
assert_eq!(
q.retry_count, 0,
"a result with no in-flight send must not advance the retry budget"
);
}
#[test]
fn note_duplicate_question_is_noop_when_done() {
let mut q = make_query(ResourceType::Any, ResourceClass::Any);
q.retire();
q.note_duplicate_question(StdInstant::now());
assert!(q.is_done());
}
#[test]
fn retry_budget_exhaustion_retires_the_query() {
let mut q = make_query(ResourceType::Any, ResourceClass::Any);
let mut buf = std::vec![0u8; 512];
let mut now = StdInstant::now();
let mut retired = false;
for _ in 0..(MAX_RETRIES as usize + 5) {
now += std::time::Duration::from_secs(120); q.handle_timeout(now).unwrap();
if let Ok(Some(_)) = q.poll_transmit(now, &mut buf) {
q.note_transmit_result(now, true); }
if q.is_done() {
retired = true;
break;
}
}
assert!(
retired,
"a query must retire once its MAX_RETRIES budget is exhausted"
);
let mut saw_timeout = false;
while let Some(u) = q.poll() {
if matches!(u, QueryUpdate::Timeout) {
saw_timeout = true;
}
}
assert!(saw_timeout, "budget exhaustion queues a Timeout terminal");
}
#[test]
fn duplicate_question_exhaustion_produces_timeout_and_correct_stats() {
#[cfg(feature = "stats")]
use std::sync::Arc;
let handle = QueryHandle::from_raw(0);
let qname = Name::try_from_str("host.local.").unwrap();
let now = StdInstant::now();
let mut q: TestQuery = TestQuery::try_new(
handle,
qname,
ResourceType::A,
ResourceClass::In,
1,
false,
None,
);
#[cfg(feature = "stats")]
let stats: Arc<hick_trace::stats::Stats> = Arc::default();
#[cfg(feature = "stats")]
{
stats.queries_started(1);
stats.incr_queries_active(1);
q.set_stats(stats.clone());
}
let mut advanced_now = now;
for _ in 0..(MAX_RETRIES as usize + 2) {
if q.is_done() {
break;
}
advanced_now += std::time::Duration::from_secs(120);
q.note_duplicate_question(advanced_now);
}
assert!(
q.is_done(),
"duplicate-question exhaustion must set done=true"
);
let mut timeout_count = 0u32;
while let Some(u) = q.poll() {
if matches!(u, QueryUpdate::Timeout) {
timeout_count += 1;
}
}
assert_eq!(
timeout_count, 1,
"duplicate-question exhaustion must queue exactly one Timeout"
);
#[cfg(feature = "stats")]
{
let snap = stats.snapshot();
assert_eq!(
snap.queries_active, 0,
"queries_active must be 0 after duplicate-question termination"
);
assert_eq!(
snap.queries_timeout, 1,
"queries_timeout must be 1 after duplicate-question termination"
);
}
}