use std::collections::BTreeSet;
use super::{DecoderContext, StreamDecoder};
use crate::errors::Error;
use crate::market_data::historical::TickDecoder;
use crate::messages::{IncomingMessages, ResponseMessage};
const DISCRIMINANT_SCAN: std::ops::RangeInclusive<i32> = -2..=255;
fn all_message_types() -> Vec<IncomingMessages> {
let mut seen = BTreeSet::new();
DISCRIMINANT_SCAN
.map(IncomingMessages::from)
.filter(|kind| seen.insert(*kind as i32))
.collect()
}
fn probe(kind: IncomingMessages) -> ResponseMessage {
ResponseMessage::from(&format!("{}\0", kind as i32))
}
const DISPATCHER_INTERCEPTED: &[IncomingMessages] = &[IncomingMessages::Error, IncomingMessages::Shutdown];
fn check_decoder(decoder: &str, declared_ids: &[IncomingMessages], decode: impl Fn(&ResponseMessage) -> Result<(), Error>) -> Vec<String> {
let mut failures = Vec::new();
for kind in all_message_types() {
let declared = declared_ids.contains(&kind);
if declared && DISPATCHER_INTERCEPTED.contains(&kind) {
failures.push(format!(
"{decoder} declares {kind:?}, which the dispatcher intercepts before routing — \
it never arrives as a `RoutedItem::Response`, so `decode` cannot see it"
));
continue;
}
let handled = !matches!(decode(&probe(kind)), Err(Error::UnexpectedResponse(_)));
match (declared, handled) {
(true, false) => failures.push(format!(
"{decoder} declares {kind:?} in RESPONSE_MESSAGE_IDS but `decode` has no arm for it — \
the declaration is dead and the frame fails the subscription"
)),
(false, true) => failures.push(format!(
"{decoder} has a `decode` arm for {kind:?} but does not declare it in RESPONSE_MESSAGE_IDS — \
the driver drops the frame before `decode`, so the arm is unreachable and the data is lost silently"
)),
_ => {}
}
}
failures
}
#[derive(Default)]
struct Roster {
failures: Vec<String>,
stream: usize,
tick: usize,
}
fn check_stream<D: StreamDecoder<D>>(roster: &mut Roster) {
let context = DecoderContext::default();
roster.stream += 1;
roster
.failures
.extend(check_decoder(std::any::type_name::<D>(), D::RESPONSE_MESSAGE_IDS, |message| {
<D as StreamDecoder<D>>::decode(&context, message).map(|_| ())
}));
}
fn check_tick<T: TickDecoder<T>>(roster: &mut Roster) {
roster.tick += 1;
roster
.failures
.extend(check_decoder(std::any::type_name::<T>(), T::RESPONSE_MESSAGE_IDS, |message| {
<T as TickDecoder<T>>::decode(message).map(|_| ())
}));
}
fn check_all() -> Roster {
use crate::accounts::{AccountSummaryResult, AccountUpdate, AccountUpdateMulti, PnL, PnLSingle, PositionUpdate, PositionUpdateMulti};
use crate::contracts::{OptionChain, OptionComputation};
use crate::display_groups::DisplayGroupUpdate;
use crate::market_data::historical::{HistoricalBarUpdate, TickBidAsk, TickLast, TickMidpoint};
use crate::market_data::realtime::{Bar, BidAsk, MarketDepths, MidPoint, TickTypes, Trade};
use crate::news::{NewsArticle, NewsBulletin};
use crate::orders::{CancelOrder, Executions, ExerciseOptions, OrderUpdate, Orders, PlaceOrder};
use crate::scanner::ScannerData;
use crate::wsh::{WshEventData, WshMetadata};
let mut roster = Roster::default();
check_stream::<AccountSummaryResult>(&mut roster);
check_stream::<AccountUpdate>(&mut roster);
check_stream::<AccountUpdateMulti>(&mut roster);
check_stream::<PnL>(&mut roster);
check_stream::<PnLSingle>(&mut roster);
check_stream::<PositionUpdate>(&mut roster);
check_stream::<PositionUpdateMulti>(&mut roster);
check_stream::<OptionChain>(&mut roster);
check_stream::<OptionComputation>(&mut roster);
check_stream::<DisplayGroupUpdate>(&mut roster);
check_stream::<HistoricalBarUpdate>(&mut roster);
check_stream::<Bar>(&mut roster);
check_stream::<BidAsk>(&mut roster);
check_stream::<MarketDepths>(&mut roster);
check_stream::<MidPoint>(&mut roster);
check_stream::<TickTypes>(&mut roster);
check_stream::<Trade>(&mut roster);
check_stream::<NewsArticle>(&mut roster);
check_stream::<NewsBulletin>(&mut roster);
check_stream::<CancelOrder>(&mut roster);
check_stream::<Executions>(&mut roster);
check_stream::<ExerciseOptions>(&mut roster);
check_stream::<OrderUpdate>(&mut roster);
check_stream::<Orders>(&mut roster);
check_stream::<PlaceOrder>(&mut roster);
check_stream::<Vec<ScannerData>>(&mut roster);
check_stream::<WshEventData>(&mut roster);
check_stream::<WshMetadata>(&mut roster);
check_tick::<TickBidAsk>(&mut roster);
check_tick::<TickLast>(&mut roster);
check_tick::<TickMidpoint>(&mut roster);
roster
}
#[test]
fn test_response_message_ids_match_decode_arms() {
let roster = check_all();
assert!(
roster.failures.is_empty(),
"RESPONSE_MESSAGE_IDS and `decode` disagree:\n {}",
roster.failures.join("\n ")
);
}
#[test]
fn test_decoder_roster_is_complete() {
let roster = check_all();
let found = collect_impls(&[STREAM_HEADER, TICK_HEADER]);
assert_roster_covers(STREAM_HEADER, &found[0], roster.stream);
assert_roster_covers(TICK_HEADER, &found[1], roster.tick);
}
const STREAM_HEADER: &str = "impl StreamDecoder<";
const TICK_HEADER: &str = "impl TickDecoder<";
fn assert_roster_covers(header: &str, found: &[String], checked: usize) {
assert_eq!(
found.len(),
checked,
"src/ has {} `{header}` blocks but `check_all` checks {checked}. \
Add the missing decoder to `check_all`.\nFound:\n {}",
found.len(),
found.join("\n ")
);
}
fn collect_impls(headers: &[&str]) -> Vec<Vec<String>> {
let mut found = vec![Vec::new(); headers.len()];
crate::common::test_utils::source_scan::visit_production_sources(&mut |path, contents| {
for line in contents.lines() {
let line = line.trim_start();
for (bucket, header) in found.iter_mut().zip(headers) {
if line.starts_with(header) {
bucket.push(format!("{}: {line}", path.display()));
}
}
}
});
for bucket in &mut found {
bucket.sort();
}
found
}