use std::sync::Arc;
use std::thread;
use std::time::Duration;
use ek_data::constant::{Direction, Exchange, Offset, OrderType};
use ek_station::wire::PlugRow;
use ek_ui::state::{BottomRightTab, OrderForm, TopLeftTab};
use ek_ui::{BusSnap, LogRing, UiShared};
fn row(name: &str) -> PlugRow {
PlugRow {
name: name.to_string(),
version: "0.1.0".to_string(),
state: "Running".to_string(),
deps: vec![],
spilled: 0,
panics: 0,
}
}
#[test]
fn r0_ring_cap_and_spill_count() {
let mut ring = LogRing::new(3);
for i in 0..5 {
ring.push(format!("L{i}"));
}
assert_eq!(ring.len(), 3);
assert_eq!(ring.spilled(), 2);
let snap = ring.snapshot();
assert_eq!(snap, vec!["L2", "L3", "L4"]);
}
#[test]
fn r1_ring_clear_keeps_spilled_fact() {
let mut ring = LogRing::new(2);
for i in 0..4 {
ring.push(format!("L{i}"));
}
assert_eq!(ring.spilled(), 2);
ring.clear();
assert!(ring.is_empty());
assert_eq!(ring.spilled(), 2, "溢出事实不抹");
}
#[test]
fn r2_shared_default_is_clean() {
let s = UiShared::new();
assert_eq!(s.rev(), 0);
assert!(!s.is_stop());
assert!(s.snapshot().rows.is_empty());
let (lines, spilled) = s.logs_snapshot();
assert!(lines.is_empty());
assert_eq!(spilled, 0);
}
#[test]
fn r3_shared_trace_and_revision() {
let s = UiShared::new();
s.log_line("a");
s.log_line("b");
let r1 = s.rev();
assert!(r1 >= 2);
s.clear_logs();
assert!(s.rev() > r1);
let (lines, _) = s.logs_snapshot();
assert!(lines.is_empty());
}
#[test]
fn r4_snapshot_roundtrip_shared_arc() {
let s = Arc::new(UiShared::new());
let mut snap = BusSnap::default();
snap.rows.push(row("dm"));
snap.services
.push(("dm".to_string(), vec!["dm.reload".to_string()]));
s.set_snapshot(snap);
let got = s.snapshot();
assert_eq!(got.rows.len(), 1);
assert_eq!(got.rows[0].name, "dm");
assert_eq!(got.services[0].1, vec!["dm.reload".to_string()]);
}
#[test]
fn r5_stop_flag_set_and_read() {
let s = Arc::new(UiShared::new());
let s2 = Arc::clone(&s);
let h = thread::spawn(move || {
thread::sleep(Duration::from_millis(50));
s2.request_stop();
});
let mut saw = false;
for _ in 0..50 {
if s.is_stop() {
saw = true;
break;
}
thread::sleep(Duration::from_millis(20));
}
h.join().unwrap();
assert!(saw, "stop 位跨线程可见");
}
fn tick(ex: Exchange, symbol: &str, last: f64) -> ek_data::tick::TickData {
ek_data::tick::TickData {
symbol: symbol.to_string(),
exchange: ex,
last_price: last,
..Default::default()
}
}
#[test]
fn r6_tick_store_en_symbol_key_and_groups() {
let s = UiShared::new();
s.update_tick(&tick(Exchange::SHFE, "cu2709", 107660.0));
s.update_tick(&tick(Exchange::DCE, "a2709", 3500.0));
s.update_tick(&tick(Exchange::SHFE, "au2706", 786.5));
s.update_tick(&tick(Exchange::SHFE, "cu2709", 107670.0));
assert_eq!(s.tick_count(), 3);
let rows = s.tick_rows();
assert_eq!(rows[0].symbol, "a2709");
assert_eq!(rows[1].symbol, "au2706");
assert_eq!(rows[2].symbol, "cu2709");
assert_eq!(rows[2].last_price, 107670.0, "覆盖生效");
let groups = s.tick_groups();
assert_eq!(groups.len(), 2);
assert_eq!(groups[0].0, "DCE");
assert_eq!(groups[1].0, "SHFE");
assert_eq!(groups[1].1.len(), 2);
s.update_tick(&tick(Exchange::DCE, "cu2709", 1.0));
assert_eq!(s.tick_count(), 4);
}
#[test]
fn r7_order_form_to_request_bits_and_errors() {
let f = OrderForm {
exchange: "SHFE".into(),
symbol: "cu2709".into(),
price: "107660".into(),
volume: "2".into(),
..Default::default()
};
let req = f.to_request().unwrap();
assert_eq!(req.symbol, "cu2709");
assert_eq!(req.exchange, Exchange::SHFE);
assert_eq!(req.direction, Direction::LONG);
assert_eq!(req.offset, Offset::OPEN);
assert_eq!(req.order_type, OrderType::LIMIT);
assert_eq!(req.price, 107660.0);
assert_eq!(req.volume, 2.0);
assert!(
OrderForm {
symbol: "".into(),
..f.clone()
}
.to_request()
.unwrap_err()
.contains("代码为空")
);
assert!(
OrderForm {
symbol: "SHFE.cu2709".into(),
..f.clone()
}
.to_request()
.unwrap_err()
.contains("前缀")
);
assert!(
OrderForm {
exchange: "XXX".into(),
..f.clone()
}
.to_request()
.unwrap_err()
.contains("Exchange")
);
assert!(
OrderForm {
price: "abc".into(),
..f.clone()
}
.to_request()
.unwrap_err()
.contains("价格")
);
assert!(
OrderForm {
price: "-1".into(),
..f.clone()
}
.to_request()
.unwrap_err()
.contains("为正")
);
assert!(
OrderForm {
volume: "0".into(),
..f.clone()
}
.to_request()
.unwrap_err()
.contains("为正")
);
let mk = OrderForm {
order_type: OrderType::MARKET,
price: "0".into(),
..f.clone()
};
assert_eq!(mk.to_request().unwrap().price, 0.0);
}
#[test]
fn r8_group_tabs_defaults_and_switch() {
let s = UiShared::new();
assert_eq!(s.topleft_tab(), TopLeftTab::Orders);
assert_eq!(s.bottomright_tab(), BottomRightTab::Account);
s.set_topleft_tab(TopLeftTab::Active);
s.set_bottomright_tab(BottomRightTab::Logs);
assert_eq!(s.topleft_tab(), TopLeftTab::Active);
assert_eq!(s.bottomright_tab(), BottomRightTab::Logs);
assert_eq!(TopLeftTab::Active.label(), "未成交");
assert_eq!(BottomRightTab::Logs.label(), "日志");
}
fn contract(ex: Exchange, symbol: &str, name: &str) -> ek_data::contract::ContractData {
ek_data::contract::ContractData {
gateway_name: "ctp".to_string(),
symbol: symbol.to_string(),
exchange: ex,
name: name.to_string(),
asset: ek_data::constant::Asset::default(),
instrument_type: None,
size: 10.0,
price_tick: 1.0,
product_id: None,
max_limit_order_volume: None,
min_limit_order_volume: None,
max_market_order_volume: None,
min_market_order_volume: None,
delivery_year: None,
delivery_month: None,
open_date: None,
pre_close: None,
up_stop_price: None,
down_stop_price: None,
float_volume: None,
total_volume: None,
instrument_status: None,
is_trading: None,
}
}
#[test]
fn r9_contract_store_query_and_selection() {
let s = UiShared::new();
assert_eq!(s.contract_count(), 0);
s.update_contract(&contract(Exchange::SHFE, "rb2605", "螺纹钢2605"));
s.update_contract(&contract(Exchange::DCE, "a2709", "黄玉米2709"));
s.update_contract(&contract(Exchange::SHFE, "au2706", "黄金2706"));
assert_eq!(s.contract_count(), 3);
s.update_contract(&contract(Exchange::SHFE, "rb2605", "螺纹钢2605"));
assert_eq!(s.contract_count(), 3);
assert_eq!(s.contracts("").len(), 3);
assert_eq!(s.contracts("rb").len(), 1);
assert_eq!(s.contracts("DCE").len(), 1);
assert_eq!(s.contracts("玉米").len(), 1);
assert_eq!(s.contracts("不存在").len(), 0);
s.toggle_contract_selection("SHFE.rb2605");
s.toggle_contract_selection("DCE.a2709");
assert_eq!(
s.selected_contracts(),
vec!["SHFE.rb2605".to_string(), "DCE.a2709".to_string()]
);
s.toggle_contract_selection("SHFE.rb2605");
assert_eq!(s.selected_contracts(), vec!["DCE.a2709".to_string()]);
}
#[test]
fn r10_window_flags_and_kline_multiopen() {
let s = UiShared::new();
assert!(!s.ops_window_open());
assert!(!s.ctp_window_open());
assert!(!s.contracts_window_open());
assert!(s.kline_symbols().is_empty());
s.set_ops_window_open(true);
s.set_ctp_window_open(true);
s.set_contracts_window_open(true);
assert!(s.ops_window_open() && s.ctp_window_open() && s.contracts_window_open());
s.open_kline("rb2605");
s.open_kline("au2706");
s.open_kline("rb2605");
assert_eq!(
s.kline_symbols(),
vec!["rb2605".to_string(), "au2706".to_string()]
);
s.close_kline("rb2605");
assert_eq!(s.kline_symbols(), vec!["au2706".to_string()]);
assert!(
s.logs_snapshot()
.0
.iter()
.any(|l| l.contains("打开 K线窗 rb2605")),
"kline open log missing"
);
}
#[test]
fn r8_import_tick_columns() {
let s = UiShared::new();
let tick = ek_data::tick::TickData {
gateway_name: "ctp".to_string(),
symbol: "sn2709".to_string(),
exchange: Exchange::SHFE,
datetime: 1,
last_price: 380.0,
bid_price_1: 379.0,
bid_volume_1: 3.0,
ask_price_1: 381.0,
ask_volume_1: 4.0,
..ek_data::tick::TickData::default()
};
let base = OrderForm {
symbol: "cu2709".to_string(),
..OrderForm::default()
};
s.set_order_form(base.clone());
ek_ui::render::import_tick(&s, &tick, "SHFE", 8);
let f = s.order_form();
assert_eq!(f.symbol, "sn2709");
assert_eq!(f.exchange, "SHFE");
assert_eq!(f.direction, Direction::SHORT);
assert_eq!(f.price, "379.0");
ek_ui::render::import_tick(&s, &tick, "SHFE", 9);
let f = s.order_form();
assert_eq!(f.direction, Direction::SHORT);
assert_eq!(f.price, "379.0");
ek_ui::render::import_tick(&s, &tick, "SHFE", 10);
let f = s.order_form();
assert_eq!(f.direction, Direction::LONG);
assert_eq!(f.price, "381.0");
ek_ui::render::import_tick(&s, &tick, "SHFE", 11);
assert_eq!(s.order_form().direction, Direction::LONG);
s.set_order_form(OrderForm {
price: "999".to_string(),
direction: Direction::SHORT,
..base.clone()
});
ek_ui::render::import_tick(&s, &tick, "SHFE", 2);
let f = s.order_form();
assert_eq!(f.symbol, "sn2709");
assert_eq!(f.exchange, "SHFE");
assert_eq!(f.direction, Direction::SHORT);
assert_eq!(f.price, "999");
}
#[test]
fn r9_theme_cycle_and_default() {
use ek_ui::state::Theme;
let s = UiShared::new();
assert_eq!(s.theme(), Theme::FinanceDark);
assert_eq!(Theme::default(), Theme::FinanceDark);
assert_eq!(s.cycle_theme(), Theme::EguiDark);
assert_eq!(s.cycle_theme(), Theme::EguiLight);
assert_eq!(s.cycle_theme(), Theme::QtClassic);
assert_eq!(s.cycle_theme(), Theme::FinanceDark);
s.set_theme(Theme::QtClassic);
assert_eq!(s.theme(), Theme::QtClassic);
assert_eq!(Theme::FinanceDark.label(), "金融深色");
assert_eq!(Theme::QtClassic.label(), "Qt灰白");
}