use std::collections::BTreeSet;
use rstest::rstest;
const DATA_ACTOR_SOURCE: &str = include_str!("../../common/src/actor/data_actor.rs");
const STRATEGY_SOURCE: &str = include_str!("../../trading/src/strategy/mod.rs");
const HOST_SOURCE: &str = include_str!("../src/host.rs");
const PLUGIN_ACTOR_SOURCE: &str = include_str!("../src/surfaces/actor.rs");
const PLUGIN_STRATEGY_SOURCE: &str = include_str!("../src/surfaces/strategy.rs");
const PLUGIN_ACTOR_DEFERRED_CALLBACKS: &[&str] = &[
"on_save",
"on_load",
"on_block",
"on_pool",
"on_pool_swap",
"on_pool_liquidity_update",
"on_pool_fee_collect",
"on_pool_flash",
"on_historical_data",
];
const PLUGIN_STRATEGY_DEFERRED_CALLBACKS: &[&str] = &[
"on_save",
"on_load",
"on_block",
"on_pool",
"on_pool_swap",
"on_pool_liquidity_update",
"on_pool_fee_collect",
"on_pool_flash",
"on_historical_data",
];
const PLUGIN_STRATEGY_DEFERRED_EXECUTION_METHODS: &[&str] = &[
"mark_order_pending_update",
"mark_order_pending_cancel",
"generate_order_pending_update",
"generate_order_pending_cancel",
];
const PLUGIN_STRATEGY_HOST_OWNED_METHODS: &[&str] = &[
"core",
"core_mut",
"external_order_claims",
"handle_order_event",
"handle_position_event",
"post_market_exit",
"is_exiting",
"market_exit",
"check_market_exit",
"finalize_market_exit",
"cancel_market_exit",
"stop",
"deny_order",
"deny_order_list",
"set_gtd_expiry",
"cancel_gtd_expiry",
"has_gtd_expiry_timer",
"expire_gtd_order",
"reactivate_gtd_timers",
];
#[rstest]
fn plugin_actor_surface_accounts_for_every_data_actor_callback() {
let data_actor_callbacks = data_actor_callback_names();
let plugin_actor_callbacks = plugin_actor_callback_names();
let deferred_callbacks = PLUGIN_ACTOR_DEFERRED_CALLBACKS
.iter()
.map(|name| (*name).to_string())
.collect::<BTreeSet<_>>();
let accounted_callbacks = plugin_actor_callbacks
.union(&deferred_callbacks)
.cloned()
.collect::<BTreeSet<_>>();
let missing_callbacks = data_actor_callbacks
.difference(&accounted_callbacks)
.cloned()
.collect::<Vec<_>>();
assert!(
missing_callbacks.is_empty(),
"DataActor callbacks must be added to PluginActor or explicitly deferred: {missing_callbacks:?}",
);
let stale_deferred_callbacks = deferred_callbacks
.difference(&data_actor_callbacks)
.cloned()
.collect::<Vec<_>>();
assert!(
stale_deferred_callbacks.is_empty(),
"Deferred plug-in actor callbacks are no longer DataActor callbacks: {stale_deferred_callbacks:?}",
);
let implemented_deferred_callbacks = deferred_callbacks
.intersection(&plugin_actor_callbacks)
.cloned()
.collect::<Vec<_>>();
assert!(
implemented_deferred_callbacks.is_empty(),
"PluginActor now supports callbacks still listed as deferred: {implemented_deferred_callbacks:?}",
);
}
#[rstest]
fn plugin_actor_trait_callbacks_match_vtable_slots() {
let plugin_actor_callbacks = plugin_actor_callback_names();
let vtable_callbacks = actor_vtable_callback_names();
let missing_vtable_slots = plugin_actor_callbacks
.difference(&vtable_callbacks)
.cloned()
.collect::<Vec<_>>();
assert!(
missing_vtable_slots.is_empty(),
"PluginActor callbacks must have ActorVTable slots: {missing_vtable_slots:?}",
);
let missing_trait_callbacks = vtable_callbacks
.difference(&plugin_actor_callbacks)
.cloned()
.collect::<Vec<_>>();
assert!(
missing_trait_callbacks.is_empty(),
"ActorVTable callback slots must have PluginActor methods: {missing_trait_callbacks:?}",
);
}
#[rstest]
fn plugin_strategy_surface_accounts_for_every_upstream_callback() {
let mut upstream_callbacks = data_actor_callback_names();
upstream_callbacks.extend(strategy_callback_names());
let plugin_strategy_callbacks = plugin_strategy_callback_names();
let deferred_callbacks = PLUGIN_STRATEGY_DEFERRED_CALLBACKS
.iter()
.map(|name| (*name).to_string())
.collect::<BTreeSet<_>>();
let accounted_callbacks = plugin_strategy_callbacks
.union(&deferred_callbacks)
.cloned()
.collect::<BTreeSet<_>>();
let missing_callbacks = upstream_callbacks
.difference(&accounted_callbacks)
.cloned()
.collect::<Vec<_>>();
assert!(
missing_callbacks.is_empty(),
"DataActor or Strategy callbacks must be added to PluginStrategy or explicitly deferred: {missing_callbacks:?}",
);
let stale_deferred_callbacks = deferred_callbacks
.difference(&upstream_callbacks)
.cloned()
.collect::<Vec<_>>();
assert!(
stale_deferred_callbacks.is_empty(),
"Deferred plug-in strategy callbacks are no longer DataActor or Strategy callbacks: {stale_deferred_callbacks:?}",
);
let implemented_deferred_callbacks = deferred_callbacks
.intersection(&plugin_strategy_callbacks)
.cloned()
.collect::<Vec<_>>();
assert!(
implemented_deferred_callbacks.is_empty(),
"PluginStrategy now supports callbacks still listed as deferred: {implemented_deferred_callbacks:?}",
);
}
#[rstest]
fn plugin_strategy_trait_callbacks_match_vtable_slots() {
let plugin_strategy_callbacks = plugin_strategy_callback_names();
let vtable_callbacks = strategy_vtable_callback_names();
let missing_vtable_slots = plugin_strategy_callbacks
.difference(&vtable_callbacks)
.cloned()
.collect::<Vec<_>>();
assert!(
missing_vtable_slots.is_empty(),
"PluginStrategy callbacks must have StrategyVTable slots: {missing_vtable_slots:?}",
);
let missing_trait_callbacks = vtable_callbacks
.difference(&plugin_strategy_callbacks)
.cloned()
.collect::<Vec<_>>();
assert!(
missing_trait_callbacks.is_empty(),
"StrategyVTable callback slots must have PluginStrategy methods: {missing_trait_callbacks:?}",
);
}
#[rstest]
fn plugin_strategy_execution_surface_accounts_for_every_strategy_method() {
let strategy_execution_methods = strategy_execution_method_names();
let host_vtable_methods = host_vtable_field_names();
let deferred_methods = PLUGIN_STRATEGY_DEFERRED_EXECUTION_METHODS
.iter()
.map(|name| (*name).to_string())
.collect::<BTreeSet<_>>();
let accounted_methods = host_vtable_methods
.union(&deferred_methods)
.cloned()
.collect::<BTreeSet<_>>();
let missing_methods = strategy_execution_methods
.difference(&accounted_methods)
.cloned()
.collect::<Vec<_>>();
assert!(
missing_methods.is_empty(),
"Strategy execution methods must be added to HostVTable or explicitly deferred: {missing_methods:?}",
);
let stale_deferred_methods = deferred_methods
.difference(&strategy_execution_methods)
.cloned()
.collect::<Vec<_>>();
assert!(
stale_deferred_methods.is_empty(),
"Deferred plug-in strategy execution methods are no longer Strategy methods: {stale_deferred_methods:?}",
);
let implemented_deferred_methods = deferred_methods
.intersection(&host_vtable_methods)
.cloned()
.collect::<Vec<_>>();
assert!(
implemented_deferred_methods.is_empty(),
"HostVTable now supports strategy execution methods still listed as deferred: {implemented_deferred_methods:?}",
);
}
#[rstest]
fn plugin_strategy_surface_classifies_every_strategy_method() {
let strategy_methods = strategy_method_names();
let host_vtable_methods = host_vtable_field_names();
let plugin_strategy_callbacks = plugin_strategy_callback_names();
let deferred_callbacks = PLUGIN_STRATEGY_DEFERRED_CALLBACKS
.iter()
.map(|name| (*name).to_string())
.collect::<BTreeSet<_>>();
let deferred_execution_methods = PLUGIN_STRATEGY_DEFERRED_EXECUTION_METHODS
.iter()
.map(|name| (*name).to_string())
.collect::<BTreeSet<_>>();
let host_owned_methods = PLUGIN_STRATEGY_HOST_OWNED_METHODS
.iter()
.map(|name| (*name).to_string())
.collect::<BTreeSet<_>>();
let mut classified_methods = host_vtable_methods
.union(&plugin_strategy_callbacks)
.cloned()
.collect::<BTreeSet<_>>();
classified_methods.extend(deferred_callbacks);
classified_methods.extend(deferred_execution_methods);
classified_methods.extend(host_owned_methods.iter().cloned());
let unclassified_methods = strategy_methods
.difference(&classified_methods)
.cloned()
.collect::<Vec<_>>();
assert!(
unclassified_methods.is_empty(),
"Strategy methods must be backed by the plug-in surface or explicitly classified: {unclassified_methods:?}",
);
let stale_host_owned_methods = host_owned_methods
.difference(&strategy_methods)
.cloned()
.collect::<Vec<_>>();
assert!(
stale_host_owned_methods.is_empty(),
"Host-owned plug-in strategy methods are no longer Strategy methods: {stale_host_owned_methods:?}",
);
}
fn data_actor_callback_names() -> BTreeSet<String> {
callback_names_between(
DATA_ACTOR_SOURCE,
"pub trait DataActor:",
" /// Handles a received time event.",
)
}
fn strategy_callback_names() -> BTreeSet<String> {
callback_names_between(
STRATEGY_SOURCE,
"pub trait Strategy: DataActor",
"#[cfg(test)]",
)
}
fn strategy_method_names() -> BTreeSet<String> {
method_names_between(
STRATEGY_SOURCE,
"pub trait Strategy: DataActor",
"\nfn publish_order_initialized",
)
}
fn strategy_execution_method_names() -> BTreeSet<String> {
method_names_between(
STRATEGY_SOURCE,
" /// Submits an order.",
" /// Handles an order event",
)
}
fn plugin_actor_callback_names() -> BTreeSet<String> {
callback_names_between(
PLUGIN_ACTOR_SOURCE,
"pub trait PluginActor:",
"/// Returns a `*const ActorVTable`",
)
}
fn plugin_strategy_callback_names() -> BTreeSet<String> {
callback_names_between(
PLUGIN_STRATEGY_SOURCE,
"pub trait PluginStrategy:",
"/// Returns a `*const StrategyVTable`",
)
}
fn actor_vtable_callback_names() -> BTreeSet<String> {
vtable_callback_names(PLUGIN_ACTOR_SOURCE)
}
fn strategy_vtable_callback_names() -> BTreeSet<String> {
vtable_callback_names(PLUGIN_STRATEGY_SOURCE)
}
fn host_vtable_field_names() -> BTreeSet<String> {
field_names_between(HOST_SOURCE, "pub struct HostVTable {", "impl HostVTable {")
}
fn vtable_callback_names(source: &str) -> BTreeSet<String> {
source
.lines()
.filter_map(|line| {
let line = line.trim_start().strip_prefix("pub on_")?;
let (name, _) = line.split_once(':')?;
Some(format!("on_{name}"))
})
.collect()
}
fn method_names_between(source: &str, start_marker: &str, stop_marker: &str) -> BTreeSet<String> {
let section = source_between(source, start_marker, stop_marker);
section.lines().filter_map(method_name).collect()
}
fn callback_names_between(source: &str, start_marker: &str, stop_marker: &str) -> BTreeSet<String> {
let section = source_between(source, start_marker, stop_marker);
section.lines().filter_map(callback_name).collect()
}
fn field_names_between(source: &str, start_marker: &str, stop_marker: &str) -> BTreeSet<String> {
let section = source_between(source, start_marker, stop_marker);
section
.lines()
.filter_map(|line| {
let line = line.trim_start().strip_prefix("pub ")?;
let (name, _) = line.split_once(':')?;
Some(name.to_string())
})
.collect()
}
fn source_between<'a>(source: &'a str, start_marker: &str, stop_marker: &str) -> &'a str {
let (_, after_start) = source
.split_once(start_marker)
.expect("surface start marker must exist");
let (section, _) = after_start
.split_once(stop_marker)
.expect("surface stop marker must exist");
section
}
fn method_name(line: &str) -> Option<String> {
let line = line.trim_start().strip_prefix("fn ")?;
let (name, _) = line.split_once('(')?;
Some(name.to_string())
}
fn callback_name(line: &str) -> Option<String> {
method_name(line).filter(|name| name.starts_with("on_"))
}