use super::*;
use crate::common::test_utils::helpers::assert_decimal_parse_error;
use crate::orders::{ExecutionSide, OrderStatusKind, TimeInForce, UnknownCondition};
#[test]
fn parse_required_none_errors_with_label() {
let err = parse_required::<OrderStatusKind>(None, "OrderStatus").unwrap_err();
match err {
Error::Parse(_, _, msg) => assert!(msg.contains("OrderStatus"), "expected label in message, got: {msg}"),
other => panic!("expected Error::Parse, got {other:?}"),
}
}
#[test]
fn parse_required_empty_errors_with_label() {
let err = parse_required::<OrderStatusKind>(Some(""), "OrderStatus").unwrap_err();
match err {
Error::Parse(_, _, msg) => assert!(msg.contains("OrderStatus"), "expected label in message, got: {msg}"),
other => panic!("expected Error::Parse, got {other:?}"),
}
}
#[test]
fn parse_required_valid_round_trips() {
let v: OrderStatusKind = parse_required(Some("Submitted"), "OrderStatus").unwrap();
assert_eq!(v, OrderStatusKind::Submitted);
}
#[test]
fn parse_required_defers_unknown_to_fromstr() {
assert!(matches!(
parse_required::<ExecutionSide>(Some("Garbage"), "side"),
Err(Error::Parse(_, _, _))
));
}
#[test]
fn parse_optional_none_is_ok_none() {
let v: Option<OrderStatusKind> = parse_optional(None).unwrap();
assert_eq!(v, None);
}
#[test]
fn parse_optional_empty_is_ok_none() {
let v: Option<OrderStatusKind> = parse_optional(Some("")).unwrap();
assert_eq!(v, None);
}
#[test]
fn parse_optional_valid_round_trips() {
let v: Option<OrderStatusKind> = parse_optional(Some("Filled")).unwrap();
assert_eq!(v, Some(OrderStatusKind::Filled));
}
#[test]
fn parse_optional_defers_unknown_to_fromstr() {
assert!(matches!(parse_optional::<ExecutionSide>(Some("Garbage")), Err(Error::Parse(_, _, _))));
}
fn unset_inputs() -> impl Iterator<Item = Option<&'static str>> {
[
None, Some(""), Some("inf"),
Some("-inf"),
Some("NaN"),
Some("1e309"), Some("1.7976931348623157E308"), Some("1.7976931348623157e308"), Some("1.7976931348623157E+308"), ]
.into_iter()
.chain(UNSET_DECIMAL_WIRE.map(Some))
}
#[test]
fn parse_optional_decimal_unset_inputs_are_none() {
for input in unset_inputs() {
assert_eq!(parse_optional_decimal(input).unwrap(), None, "expected None for {input:?}");
}
}
#[test]
fn parse_optional_decimal_preserves_fractional_values() {
for (input, expected) in [("0.5", 0.5), ("0.001", 0.001), ("-0.25", -0.25), ("1.5", 1.5)] {
assert_eq!(parse_optional_decimal(Some(input)).unwrap(), Some(expected), "input {input}");
}
}
#[test]
fn parse_optional_decimal_zero_is_a_value_not_unset() {
assert_eq!(parse_optional_decimal(Some("0")).unwrap(), Some(0.0));
assert_eq!(parse_optional_decimal(Some("100")).unwrap(), Some(100.0));
}
#[test]
fn parse_optional_decimal_sentinel_near_misses_survive() {
for (input, expected) in [
("2147483647.0", 2147483647.0),
("9223372036854775806", 9223372036854775806.0),
("21474836470", 21474836470.0),
("214748364", 214748364.0),
] {
assert_eq!(parse_optional_decimal(Some(input)).unwrap(), Some(expected), "input {input}");
}
}
#[test]
fn parse_optional_decimal_accepts_more_digits_than_f64_round_trips() {
assert_eq!(parse_optional_decimal(Some("0.1234567890123456789")).unwrap(), Some(0.12345678901234568));
assert_eq!(
parse_optional_decimal(Some("123456789012345678901234567890")).unwrap(),
Some(1.2345678901234568e29)
);
}
#[test]
fn parse_optional_decimal_malformed_errors_with_offending_value() {
for input in ["abc", " ", "1,000", "1.2.3", "--1", "0x10"] {
assert_decimal_parse_error(parse_optional_decimal(Some(input)), input);
}
}
#[test]
fn parse_decimal_or_zero_collapses_unset_to_zero() {
for input in unset_inputs() {
assert_eq!(parse_decimal_or_zero(input).unwrap(), 0.0, "expected 0.0 for {input:?}");
}
}
#[test]
fn parse_decimal_or_zero_passes_values_through() {
assert_eq!(parse_decimal_or_zero(Some("0.5")).unwrap(), 0.5);
}
#[test]
fn parse_decimal_or_zero_propagates_parse_error() {
assert!(matches!(parse_decimal_or_zero(Some("abc")), Err(Error::Parse(_, _, _))));
}
fn proto_leg(action: Option<&str>) -> proto::ComboLeg {
proto::ComboLeg {
con_id: Some(1),
ratio: Some(1),
action: action.map(str::to_string),
..Default::default()
}
}
#[test]
fn decode_combo_leg_rejects_missing_action() {
assert!(matches!(decode_combo_leg(&proto_leg(None)), Err(Error::Parse(_, _, _))));
}
#[test]
fn decode_combo_leg_rejects_empty_action() {
assert!(matches!(decode_combo_leg(&proto_leg(Some(""))), Err(Error::Parse(_, _, _))));
}
#[test]
fn decode_combo_leg_rejects_unknown_action() {
assert!(matches!(decode_combo_leg(&proto_leg(Some("SLONG"))), Err(Error::Parse(_, _, _))));
}
#[test]
fn decode_combo_leg_accepts_buy() {
let leg = decode_combo_leg(&proto_leg(Some("BUY"))).unwrap();
assert_eq!(leg.action, LegAction::Buy);
}
#[test]
fn decode_combo_leg_accepts_sell() {
let leg = decode_combo_leg(&proto_leg(Some("SELL"))).unwrap();
assert_eq!(leg.action, LegAction::Sell);
}
#[test]
fn decode_combo_leg_accepts_sshort() {
let leg = decode_combo_leg(&proto_leg(Some("SSHORT"))).unwrap();
assert_eq!(leg.action, LegAction::SellShort);
}
#[test]
fn decode_contract_propagates_bad_combo_leg() {
let proto_contract = proto::Contract {
combo_legs: vec![proto_leg(Some("NOTAVARIANT"))],
..stk_contract()
};
assert!(matches!(decode_contract(&proto_contract), Err(Error::Parse(_, _, _))));
}
#[test]
fn decode_contract_reads_missing_or_empty_sec_type_as_other_empty() {
for sec_type in [None, Some(String::new())] {
let proto_contract = proto::Contract {
sec_type,
..Default::default()
};
let contract = decode_contract(&proto_contract).unwrap();
assert_eq!(contract.security_type, crate::contracts::SecurityType::Other(String::new()));
}
}
#[test]
fn decode_contract_preserves_unknown_sec_type() {
let proto_contract = proto::Contract {
sec_type: Some("NOTASECTYPE".into()),
..Default::default()
};
let contract = decode_contract(&proto_contract).unwrap();
assert_eq!(contract.security_type, crate::contracts::SecurityType::Other("NOTASECTYPE".into()));
}
fn stk_contract() -> proto::Contract {
proto::Contract {
sec_type: Some("STK".into()),
..Default::default()
}
}
fn proto_order() -> proto::Order {
proto::Order {
action: Some("BUY".into()),
..Default::default()
}
}
#[test]
fn decode_order_maps_hedge_max_size() {
let proto_order = proto::Order {
hedge_max_size: Some(500),
..proto_order()
};
let order = decode_order(&proto_order).unwrap();
assert_eq!(order.hedge_max_size, Some(500));
}
#[test]
fn decode_order_hedge_max_size_absent_is_none() {
let order = decode_order(&proto_order()).unwrap();
assert!(order.hedge_max_size.is_none());
}
#[test]
fn decode_order_keeps_the_up_to_mid_sentinel() {
let proto_order = proto::Order {
compete_against_best_offset: Some(f64::INFINITY),
..proto_order()
};
let order = decode_order(&proto_order).unwrap();
assert_eq!(order.compete_against_best_offset, Some(f64::INFINITY));
}
#[test]
fn decode_order_maps_deactivate() {
let proto_order = proto::Order {
deactivate: Some(true),
..proto_order()
};
let order = decode_order(&proto_order).unwrap();
assert!(order.deactivate);
}
#[test]
fn decode_order_deactivate_absent_is_false() {
let order = decode_order(&proto_order()).unwrap();
assert!(!order.deactivate);
}
#[test]
fn decode_order_preserves_unknown_tif() {
let proto_order = proto::Order {
tif: Some("GTZ".to_string()),
..proto_order()
};
assert_eq!(decode_order(&proto_order).unwrap().tif, TimeInForce::Unknown("GTZ".to_string()));
}
#[test]
fn decode_order_maps_known_tif() {
let proto_order = proto::Order {
tif: Some("GTX".to_string()),
..proto_order()
};
assert_eq!(decode_order(&proto_order).unwrap().tif, TimeInForce::GoodTillCrossing);
}
#[test]
fn decode_order_tif_absent_or_empty_is_day() {
assert_eq!(decode_order(&proto_order()).unwrap().tif, TimeInForce::Day);
let empty_tif = proto::Order {
tif: Some(String::new()),
..proto_order()
};
assert_eq!(decode_order(&empty_tif).unwrap().tif, TimeInForce::Day);
}
#[test]
fn decode_order_rejects_malformed_total_quantity() {
let proto_order = proto::Order {
total_quantity: Some("abc".into()),
..proto_order()
};
assert_decimal_parse_error(decode_order(&proto_order), "abc");
}
#[test]
fn decode_order_preserves_fractional_total_quantity() {
let proto_order = proto::Order {
total_quantity: Some("0.5".into()),
..proto_order()
};
assert_eq!(decode_order(&proto_order).unwrap().total_quantity, 0.5);
}
#[test]
fn decode_order_rejects_missing_or_empty_action() {
for proto_order in [
proto::Order {
action: None,
..proto_order()
},
proto::Order {
action: Some(String::new()),
..proto_order()
},
] {
assert!(
matches!(decode_order(&proto_order), Err(Error::Parse(_, _, _))),
"expected Error::Parse for {proto_order:?}"
);
}
}
#[test]
fn decode_order_rejects_unknown_action() {
let proto_order = proto::Order {
action: Some("NOTASIDE".into()),
..proto_order()
};
assert!(matches!(decode_order(&proto_order), Err(Error::Parse(_, _, _))));
}
#[test]
fn decode_order_preserves_unknown_string_codes() {
let proto_order = proto::Order {
rule80_a: Some("Z".into()),
open_close: Some("X".into()),
..proto_order()
};
let order = decode_order(&proto_order).unwrap();
assert_eq!(order.rule_80_a, Some(Rule80A::Unknown("Z".into())));
assert_eq!(order.open_close, Some(OrderOpenClose::Unknown("X".into())));
}
#[test]
fn decode_order_empty_optional_strings_are_none() {
let proto_order = proto::Order {
rule80_a: Some(String::new()),
open_close: Some(String::new()),
..proto_order()
};
let order = decode_order(&proto_order).unwrap();
assert_eq!(order.rule_80_a, None);
assert_eq!(order.open_close, None);
}
#[test]
fn decode_order_preserves_unknown_integer_codes() {
let proto_order = proto::Order {
oca_type: Some(9),
trigger_method: Some(99),
volatility_type: Some(9),
reference_price_type: Some(9),
origin: Some(9),
short_sale_slot: Some(9),
..proto_order()
};
let order = decode_order(&proto_order).unwrap();
assert_eq!(order.oca_type, OcaType::Unknown(9));
assert_eq!(order.trigger_method, TriggerMethod::Unknown(99));
assert_eq!(order.volatility_type, Some(VolatilityType::Unknown(9)));
assert_eq!(order.reference_price_type, Some(ReferencePriceType::Unknown(9)));
assert_eq!(order.origin, OrderOrigin::Unknown(9));
assert_eq!(order.short_sale_slot, ShortSaleSlot::Unknown(9));
}
#[test]
fn decode_order_condition_preserves_unknown_trigger_method() {
let proto_order = proto::Order {
conditions: vec![proto::OrderCondition {
r#type: Some(1),
trigger_method: Some(99),
..Default::default()
}],
..proto_order()
};
let order = decode_order(&proto_order).unwrap();
match &order.conditions[..] {
[OrderCondition::Price(price)] => assert_eq!(price.trigger_method, TriggerMethod::Unknown(99)),
other => panic!("expected one price condition, got {other:?}"),
}
}
fn populated_condition(condition_type: i32) -> proto::OrderCondition {
proto::OrderCondition {
r#type: Some(condition_type),
is_conjunction_connection: Some(false),
is_more: Some(true),
con_id: Some(265598),
exchange: Some("SMART".into()),
symbol: Some("AAPL".into()),
sec_type: Some("STK".into()),
percent: Some(12),
change_percent: Some(2.5),
price: Some(150.0),
trigger_method: Some(2),
time: Some("20251230 14:30:00 US/Eastern".into()),
volume: Some(1000),
}
}
#[test]
fn decode_order_preserves_unknown_condition_type() {
let proto_order = proto::Order {
conditions: vec![
populated_condition(2),
proto::OrderCondition {
r#type: Some(99),
..Default::default()
},
],
..proto_order()
};
let order = decode_order(&proto_order).unwrap();
assert_eq!(
order.conditions,
vec![
OrderCondition::Unknown(UnknownCondition {
condition_type: 2,
is_conjunction: false,
is_more: Some(true),
contract_id: Some(265598),
exchange: Some("SMART".into()),
symbol: Some("AAPL".into()),
security_type: Some("STK".into()),
percent: Some(12),
change_percent: Some(2.5),
price: Some(150.0),
trigger_method: Some(2),
time: Some("20251230 14:30:00 US/Eastern".into()),
volume: Some(1000),
}),
OrderCondition::Unknown(UnknownCondition {
condition_type: 99,
is_conjunction: true,
..Default::default()
}),
]
);
}
#[test]
fn decode_order_condition_missing_type_is_parse_error() {
let proto_order = proto::Order {
conditions: vec![proto::OrderCondition::default()],
..proto_order()
};
match decode_order(&proto_order).unwrap_err() {
Error::Parse(_, _, msg) => assert!(msg.contains("OrderCondition type"), "got: {msg}"),
other => panic!("expected Error::Parse, got {other:?}"),
}
}
#[test]
fn decode_order_condition_maps_every_type_code() {
const MODELED: [i32; 6] = [1, 3, 4, 5, 6, 7];
for code in -8..=64 {
let condition = decode_order_condition(&proto::OrderCondition {
r#type: Some(code),
..Default::default()
})
.unwrap();
assert_eq!(condition.condition_type(), code, "code {code}");
assert_eq!(
matches!(condition, OrderCondition::Unknown(_)),
!MODELED.contains(&code),
"code {code} decoded as {condition:?}"
);
}
}
#[test]
fn decode_execution_rejects_malformed_shares() {
let proto_exec = proto::Execution {
side: Some("BOT".into()),
shares: Some("abc".into()),
..Default::default()
};
assert_decimal_parse_error(decode_execution(&proto_exec), "abc");
}
#[test]
fn decode_execution_rejects_malformed_cumulative_quantity() {
let proto_exec = proto::Execution {
side: Some("BOT".into()),
shares: Some("10".into()),
cum_qty: Some("abc".into()),
..Default::default()
};
assert_decimal_parse_error(decode_execution(&proto_exec), "abc");
}
#[test]
fn decode_contract_details_rejects_malformed_min_size() {
let details = proto::ContractDetails {
min_size: Some("abc".into()),
..Default::default()
};
assert_decimal_parse_error(decode_contract_details(&stk_contract(), &details), "abc");
}
#[test]
fn decode_contract_details_rejects_malformed_min_tick() {
let details = proto::ContractDetails {
min_tick: Some("abc".into()),
..Default::default()
};
assert_decimal_parse_error(decode_contract_details(&stk_contract(), &details), "abc");
}
#[test]
fn decode_order_state_sentinel_suggested_size_is_none() {
let state = proto::OrderState {
status: Some("Submitted".into()),
suggested_size: Some("2147483647".into()),
..Default::default()
};
assert_eq!(decode_order_state(&state).unwrap().suggested_size, None);
}
#[test]
fn decode_order_state_rejects_malformed_allocation_position() {
let state = proto::OrderState {
status: Some("Submitted".into()),
order_allocations: vec![proto::OrderAllocation {
account: Some("DU1234".into()),
position: Some("abc".into()),
..Default::default()
}],
..Default::default()
};
assert_decimal_parse_error(decode_order_state(&state), "abc");
}
#[test]
fn decode_contract_details_absent_size_rules_are_none() {
let details = decode_contract_details(&stk_contract(), &proto::ContractDetails::default()).unwrap();
assert_eq!(details.min_size, None);
assert_eq!(details.size_increment, None);
assert_eq!(details.suggested_size_increment, None);
}
#[test]
fn decode_contract_details_preserves_fractional_size_increment() {
let details = proto::ContractDetails {
size_increment: Some("0.0001".into()),
..Default::default()
};
let decoded = decode_contract_details(&stk_contract(), &details).unwrap();
assert_eq!(decoded.size_increment, Some(0.0001));
}