use std::collections::HashSet;
use graph_process_manager_core::process::config::AbstractProcessConfiguration;
use graph_process_manager_core::process::event::ExplorationEvent;
use graph_process_manager_core::process::filter::{
AbstractNodePostFilter, AbstractNodePreFilter, AbstractStepFilter, GenericFiltersManager,
};
use graph_process_manager_core::process::manager::GenericProcessManager;
use graph_process_manager_core::process::persistent_state::AbstractProcessMutablePersistentState;
use graph_process_manager_core::queue::priorities::{AbstractPriorities, GenericProcessPriorities};
use graph_process_manager_core::queue::strategy::QueueSearchStrategy;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct Node(u8);
#[derive(Debug, Clone, PartialEq, Eq)]
struct Step(u8);
struct GraphConf;
impl AbstractProcessConfiguration for GraphConf {
type ContextAndParameterization = ();
type DomainSpecificNode = Node;
type DomainSpecificStep = Step;
type Priorities = FlatPriorities;
type MutablePersistentState = TrackingState;
type FiltrationResult = ();
fn process_new_step(
_ctx: &(),
_state: &mut TrackingState,
_parent: &Node,
step: &Step,
) -> Node {
Node(step.0)
}
fn collect_next_steps(_ctx: &(), _state: &TrackingState, parent: &Node) -> Vec<Step> {
match parent.0 {
0 => vec![Step(1), Step(2)], 1 => vec![Step(3), Step(4)], 2 => vec![Step(4), Step(5)], 4 => vec![Step(6)], _ => vec![], }
}
}
struct FlatPriorities;
impl AbstractPriorities<Step> for FlatPriorities {
fn get_priority_of_step(&self, _step: &Step) -> i32 {
0
}
}
#[derive(Default)]
struct TrackingState {
visited_sequence: Vec<u8>,
terminate_on: Option<u8>,
}
impl AbstractProcessMutablePersistentState<GraphConf> for TrackingState {
fn get_initial_state(_ctx: &(), _initial_node: &Node) -> Self {
TrackingState::default()
}
fn update_on_node_reached(&mut self, _ctx: &(), node: &Node) {
self.visited_sequence.push(node.0);
}
fn update_on_next_steps_collected_reached(&mut self, _ctx: &(), _node: &Node, _steps: &[Step]) {}
fn update_on_filtered(&mut self, _ctx: &(), _node: &Node, _result: &()) {}
fn warrants_termination_of_the_process(&self, _ctx: &()) -> bool {
self.terminate_on
.map_or(false, |t| self.visited_sequence.contains(&t))
}
}
fn make_manager(
strategy: QueueSearchStrategy,
memoized: bool,
pre: Vec<Box<dyn AbstractNodePreFilter<GraphConf>>>,
post: Vec<Box<dyn AbstractNodePostFilter<GraphConf>>>,
step: Vec<Box<dyn AbstractStepFilter<GraphConf>>>,
) -> GenericProcessManager<GraphConf> {
GenericProcessManager::new(
(),
strategy,
GenericProcessPriorities::new(FlatPriorities, false),
GenericFiltersManager::new(pre, post, step),
memoized,
Node(0),
)
}
fn new_node_sequence(manager: GenericProcessManager<GraphConf>) -> Vec<u8> {
manager
.filter_map(|ev| match ev {
ExplorationEvent::NewNode { node, .. } => Some(node.0),
_ => None,
})
.collect()
}
fn collect_events(manager: GenericProcessManager<GraphConf>) -> Vec<ExplorationEvent<GraphConf>> {
manager.collect()
}
#[test]
fn bfs_node_discovery_order_no_memo() {
let seq = new_node_sequence(make_manager(QueueSearchStrategy::BFS, false, vec![], vec![], vec![]));
assert_eq!(seq, vec![0, 2, 1, 5, 4, 4, 3, 6, 6]);
}
#[test]
fn dfs_node_discovery_order_no_memo() {
let seq = new_node_sequence(make_manager(QueueSearchStrategy::DFS, false, vec![], vec![], vec![]));
assert_eq!(seq, vec![0, 2, 5, 4, 6, 1, 4, 6, 3]);
}
#[test]
fn hcs_node_discovery_order_no_memo() {
let seq = new_node_sequence(make_manager(QueueSearchStrategy::HCS, false, vec![], vec![], vec![]));
assert_eq!(seq, vec![0, 2, 5, 1, 4, 6, 4, 6, 3]);
}
#[test]
fn all_strategies_differ() {
let bfs = new_node_sequence(make_manager(QueueSearchStrategy::BFS, false, vec![], vec![], vec![]));
let dfs = new_node_sequence(make_manager(QueueSearchStrategy::DFS, false, vec![], vec![], vec![]));
let hcs = new_node_sequence(make_manager(QueueSearchStrategy::HCS, false, vec![], vec![], vec![]));
assert_ne!(bfs, dfs);
assert_ne!(bfs, hcs);
assert_ne!(dfs, hcs);
}
#[test]
fn memo_reaches_same_set_of_nodes_as_no_memo() {
let with_memo = new_node_sequence(make_manager(QueueSearchStrategy::BFS, true, vec![], vec![], vec![]));
let no_memo = new_node_sequence(make_manager(QueueSearchStrategy::BFS, false, vec![], vec![], vec![]));
let set_memo: HashSet<u8> = with_memo.into_iter().collect();
let set_no_memo: HashSet<u8> = no_memo.into_iter().collect();
assert_eq!(set_memo, set_no_memo);
assert_eq!(set_memo, HashSet::from([0, 1, 2, 3, 4, 5, 6]));
}
#[test]
fn memo_visits_each_node_exactly_once() {
for strategy in [QueueSearchStrategy::BFS, QueueSearchStrategy::DFS, QueueSearchStrategy::HCS] {
let seq = new_node_sequence(make_manager(strategy, true, vec![], vec![], vec![]));
let unique: HashSet<u8> = seq.iter().copied().collect();
assert_eq!(seq.len(), unique.len(), "duplicate nodes with {:?}", strategy);
assert_eq!(unique, HashSet::from([0, 1, 2, 3, 4, 5, 6]));
}
}
#[test]
fn no_memo_visits_shared_nodes_multiple_times() {
let seq = new_node_sequence(make_manager(QueueSearchStrategy::BFS, false, vec![], vec![], vec![]));
assert_eq!(seq.iter().filter(|&&v| v == 4).count(), 2, "E should appear twice");
assert_eq!(seq.iter().filter(|&&v| v == 6).count(), 2, "G should appear twice");
}
#[test]
fn memo_step_targets_already_known_node() {
let mut id_of_e: Option<u32> = None;
let mut second_e_target: Option<u32> = None;
let mut e_count = 0u32;
let manager = make_manager(QueueSearchStrategy::BFS, true, vec![], vec![], vec![]);
for ev in manager {
match ev {
ExplorationEvent::NewNode { id, node } if node.0 == 4 => {
e_count += 1;
if e_count == 1 { id_of_e = Some(id); }
}
ExplorationEvent::NewStep { step, target_node_id, .. } if step.0 == 4 => {
if e_count >= 1 && id_of_e.is_some() && second_e_target.is_none() {
second_e_target = Some(target_node_id);
}
}
_ => {}
}
}
assert_eq!(e_count, 1, "E should be discovered (NewNode) exactly once with memo");
assert_eq!(
second_e_target,
id_of_e,
"second step to E should target the memoized id"
);
}
struct PruneNodeB;
impl AbstractNodePreFilter<GraphConf> for PruneNodeB {
fn apply_pre_filter(&self, _ctx: &(), _state: &TrackingState, node: &Node) -> Option<()> {
if node.0 == 1 { Some(()) } else { None }
}
}
#[test]
fn pre_filter_prunes_subtree() {
let manager = make_manager(
QueueSearchStrategy::BFS,
false,
vec![Box::new(PruneNodeB)],
vec![],
vec![],
);
let events: Vec<_> = collect_events(manager);
let nodes: Vec<u8> = events.iter().filter_map(|e| match e {
ExplorationEvent::NewNode { node, .. } => Some(node.0),
_ => None,
}).collect();
assert!(nodes.contains(&1), "B should still appear as a NewNode (filter fires after)");
assert!(!nodes.contains(&3), "D should not be visited (child of pruned B)");
let filtered_count = events.iter().filter(|e| matches!(e, ExplorationEvent::Filtered { .. })).count();
assert_eq!(filtered_count, 1, "exactly one Filtered event for B");
assert!(nodes.contains(&4), "E reachable via C");
assert!(nodes.contains(&6), "G reachable via C→E");
}
struct PruneIfChildIsG;
impl AbstractNodePostFilter<GraphConf> for PruneIfChildIsG {
fn apply_post_filter(
&self,
_ctx: &(),
_state: &TrackingState,
_node: &Node,
steps: &[Step],
) -> Option<()> {
if steps.iter().any(|s| s.0 == 6) { Some(()) } else { None }
}
}
#[test]
fn post_filter_prunes_after_step_collection() {
let manager = make_manager(
QueueSearchStrategy::BFS,
false,
vec![],
vec![Box::new(PruneIfChildIsG)],
vec![],
);
let events: Vec<_> = collect_events(manager);
let nodes: Vec<u8> = events.iter().filter_map(|e| match e {
ExplorationEvent::NewNode { node, .. } => Some(node.0),
_ => None,
}).collect();
assert!(nodes.contains(&4), "E is reached and emits NewNode");
assert!(!nodes.contains(&6), "G is never reached");
let filtered_count = events.iter().filter(|e| matches!(e, ExplorationEvent::Filtered { .. })).count();
assert_eq!(filtered_count, 2, "both copies of E should be post-filtered");
}
struct PruneStepToE;
impl AbstractStepFilter<GraphConf> for PruneStepToE {
fn apply_step_filter(
&self,
_ctx: &(),
_state: &TrackingState,
_parent: &Node,
step: &Step,
) -> Option<()> {
if step.0 == 4 { Some(()) } else { None }
}
}
#[test]
fn step_filter_prevents_transition() {
let manager = make_manager(
QueueSearchStrategy::BFS,
false,
vec![],
vec![],
vec![Box::new(PruneStepToE)],
);
let events: Vec<_> = collect_events(manager);
let nodes: Vec<u8> = events.iter().filter_map(|e| match e {
ExplorationEvent::NewNode { node, .. } => Some(node.0),
_ => None,
}).collect();
assert!(!nodes.contains(&4), "E should not be reached");
assert!(!nodes.contains(&6), "G should not be reached");
assert_eq!(
nodes.iter().copied().collect::<HashSet<u8>>(),
HashSet::from([0, 1, 2, 3, 5])
);
let filtered_count = events.iter().filter(|e| matches!(e, ExplorationEvent::Filtered { .. })).count();
assert_eq!(filtered_count, 2);
}
#[test]
fn early_termination_on_target_node() {
let mut manager = make_manager(QueueSearchStrategy::DFS, false, vec![], vec![], vec![]);
manager.global_state.terminate_on = Some(6);
let nodes: Vec<u8> = manager
.filter_map(|ev| match ev {
ExplorationEvent::NewNode { node, .. } => Some(node.0),
_ => None,
})
.collect();
assert!(nodes.contains(&6), "G should be reached before termination");
assert!(!nodes.contains(&3), "D should not be reached after early termination");
assert!(!nodes.contains(&1), "B should not be reached after early termination");
assert_eq!(*nodes.last().unwrap(), 6);
}
#[test]
fn new_node_always_precedes_its_new_step() {
let events = collect_events(make_manager(QueueSearchStrategy::BFS, false, vec![], vec![], vec![]));
let mut known_ids: HashSet<u32> = HashSet::new();
for ev in &events {
match ev {
ExplorationEvent::NewNode { id, .. } => { known_ids.insert(*id); }
ExplorationEvent::NewStep { target_node_id, .. } => {
assert!(
known_ids.contains(target_node_id),
"NewStep target {} seen before its NewNode",
target_node_id
);
}
_ => {}
}
}
}
#[test]
fn all_children_processed_fires_after_last_child_step() {
let events = collect_events(make_manager(QueueSearchStrategy::BFS, false, vec![], vec![], vec![]));
let mut last_step_pos: std::collections::HashMap<u32, usize> = std::collections::HashMap::new();
let mut all_children_pos: std::collections::HashMap<u32, usize> = std::collections::HashMap::new();
for (i, ev) in events.iter().enumerate() {
match ev {
ExplorationEvent::NewStep { origin_node_id, .. } |
ExplorationEvent::Filtered { parent_node_id: origin_node_id, .. } => {
last_step_pos.insert(*origin_node_id, i);
}
ExplorationEvent::AllChildrenProcessed { parent_node_id } => {
all_children_pos.insert(*parent_node_id, i);
}
_ => {}
}
}
for (parent_id, acp_pos) in &all_children_pos {
if let Some(last_pos) = last_step_pos.get(parent_id) {
assert!(
acp_pos > last_pos,
"AllChildrenProcessed({}) at {} must come after last step at {}",
parent_id, acp_pos, last_pos
);
}
}
}
#[test]
fn terminal_nodes_emit_node_without_children() {
let events = collect_events(make_manager(QueueSearchStrategy::BFS, true, vec![], vec![], vec![]));
let terminal_node_values: HashSet<u8> = events.iter().filter_map(|e| match e {
ExplorationEvent::NodeWithoutChildren { node_id } => {
events.iter().find_map(|e2| match e2 {
ExplorationEvent::NewNode { id, node } if id == node_id => Some(node.0),
_ => None,
})
}
_ => None,
}).collect();
assert_eq!(terminal_node_values, HashSet::from([3, 5, 6]), "D, F, G are the terminal nodes");
}
#[test]
fn total_event_count_matches_expected_with_memo() {
let events = collect_events(make_manager(QueueSearchStrategy::BFS, true, vec![], vec![], vec![]));
let new_nodes = events.iter().filter(|e| matches!(e, ExplorationEvent::NewNode { .. })).count();
let new_steps = events.iter().filter(|e| matches!(e, ExplorationEvent::NewStep { .. })).count();
let no_child = events.iter().filter(|e| matches!(e, ExplorationEvent::NodeWithoutChildren { .. })).count();
let all_done = events.iter().filter(|e| matches!(e, ExplorationEvent::AllChildrenProcessed { .. })).count();
let filtered = events.iter().filter(|e| matches!(e, ExplorationEvent::Filtered { .. })).count();
assert_eq!(new_nodes, 7);
assert_eq!(new_steps, 7);
assert_eq!(no_child, 3);
assert_eq!(all_done, 4);
assert_eq!(filtered, 0);
}