use std::collections::{HashMap, HashSet};
use arcstr::ArcStr;
use crate::CacheStrategy;
use crate::topic_match::TopicPath;
use crate::topic_matcher::TopicMatcherNode;
use crate::topic_pattern_path::TopicPatternPath;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct SubscriptionId {
pub id: usize,
}
fn make_subscription_id(id: usize) -> SubscriptionId {
SubscriptionId { id }
}
fn new_from_string(pattern: &str) -> Result<TopicPatternPath, String> {
TopicPatternPath::new_from_string(pattern, CacheStrategy::NoCache)
.map_err(|e| e.to_string())
}
fn test_subscriptions(
subscriptions: &[(&str, usize)],
expected_matches: &[(&str, Vec<usize>)],
) {
let mut root = TopicMatcherNode::<HashSet<SubscriptionId>>::new();
for (pattern_str, sub_id) in subscriptions {
let pattern = new_from_string(pattern_str).unwrap();
let sub_id = make_subscription_id(*sub_id);
root.get_or_create_subscription_table(&pattern)
.insert(sub_id);
}
for (path, expected_sub_ids) in expected_matches {
let expected: HashSet<SubscriptionId> = expected_sub_ids
.iter()
.map(|id| make_subscription_id(*id))
.collect();
let topic = TopicPath::new(ArcStr::from(*path));
let matches = root.find_by_path(&topic);
let actual: HashSet<SubscriptionId> =
matches.iter().flat_map(|set| set.iter().cloned()).collect();
assert_eq!(
actual, expected,
"Path '{path}' matched subscriptions {actual:?}, expected \
{expected:?}"
);
}
}
#[test]
fn test_exact_matches() {
let subscriptions = [
("sensors/temperature", 1),
("sensors/humidity", 2),
("devices/light/status", 3),
];
let expected_matches = [
("sensors/temperature", vec![1]),
("sensors/humidity", vec![2]),
("devices/light/status", vec![3]),
("sensors/pressure", vec![]), ];
test_subscriptions(&subscriptions, &expected_matches);
}
#[test]
fn test_plus_wildcards() {
let subscriptions = [
("sensors/+/reading", 1),
("devices/+/+/state", 2),
("home/+", 3),
];
let expected_matches = [
("sensors/temperature/reading", vec![1]),
("sensors/humidity/reading", vec![1]),
("sensors/temperature/value", vec![]), ("devices/light/kitchen/state", vec![2]),
("devices/light/state", vec![]), ("home/kitchen", vec![3]),
("home/livingroom", vec![3]),
("home/kitchen/temperature", vec![]), ];
test_subscriptions(&subscriptions, &expected_matches);
}
#[test]
fn test_hash_wildcards() {
let subscriptions = [
("sensors/#", 1),
("home/livingroom/#", 2),
("#", 3), ];
let expected_matches = [
("sensors", vec![1, 3]),
("sensors/temperature", vec![1, 3]),
("sensors/kitchen/temperature", vec![1, 3]),
("home/livingroom", vec![2, 3]),
("home/livingroom/light", vec![2, 3]),
("home/kitchen", vec![3]), ];
test_subscriptions(&subscriptions, &expected_matches);
}
#[test]
fn test_complex_subscriptions() {
let subscriptions = [
("home/kitchen/temperature", 1), ("home/+/temperature", 2), ("home/kitchen/+", 3), ("home/#", 4), ("+/kitchen/#", 5), ];
let expected_matches = [
("home/kitchen/temperature", vec![1, 2, 3, 4, 5]), ("home/livingroom/temperature", vec![2, 4]), ("home/kitchen/humidity", vec![3, 4, 5]), ("home/kitchen/temperature/celsius", vec![4, 5]), ("office/kitchen/temperature", vec![5]), ("home", vec![4]), ];
test_subscriptions(&subscriptions, &expected_matches);
}
#[test]
fn test_edge_cases() {
let subscriptions = [
("#", 2), ("+", 3), ("+/+", 4), ("+/#", 5), ];
let expected_matches = [
("segment", vec![2, 3, 5]), ("segment1/segment2", vec![2, 4, 5]), ("segment1/segment2/segment3", vec![2, 5]), ];
test_subscriptions(&subscriptions, &expected_matches);
}
#[test]
fn test_multiple_subscribers_to_same_pattern() {
let mut root = TopicMatcherNode::<HashSet<SubscriptionId>>::new();
let pattern = new_from_string("sensors/temperature").unwrap();
let sub_id1 = make_subscription_id(1);
let sub_id2 = make_subscription_id(2);
let subscribers = root.get_or_create_subscription_table(&pattern);
subscribers.insert(sub_id1.clone());
subscribers.insert(sub_id2.clone());
let topic = TopicPath::new(ArcStr::from("sensors/temperature"));
let matches = root.find_by_path(&topic);
assert_eq!(matches.len(), 1);
let matched_subs: HashSet<SubscriptionId> =
matches[0].iter().cloned().collect();
let expected_subs: HashSet<SubscriptionId> =
[sub_id1, sub_id2].into_iter().collect();
assert_eq!(matched_subs, expected_subs);
}
#[test]
fn test_same_subscriber_multiple_patterns() {
let mut root = TopicMatcherNode::<HashSet<SubscriptionId>>::new();
let sub_id = make_subscription_id(1);
let pattern1 = new_from_string("devices/living-room/temperature").unwrap();
root.get_or_create_subscription_table(&pattern1)
.insert(sub_id.clone());
let pattern2 = new_from_string("devices/+/humidity").unwrap();
root.get_or_create_subscription_table(&pattern2)
.insert(sub_id.clone());
let pattern3 = new_from_string("sensors/#").unwrap();
root.get_or_create_subscription_table(&pattern3)
.insert(sub_id.clone());
{
let topic =
TopicPath::new(ArcStr::from("devices/living-room/temperature"));
let temp_matches = root.find_by_path(&topic);
let matched_subs = collect_sub_ids(&temp_matches);
assert_eq!(matched_subs.len(), 1);
assert!(matched_subs.contains(&sub_id));
}
{
let topic =
TopicPath::new(ArcStr::from("devices/living-room/humidity"));
let humidity_matches = root.find_by_path(&topic);
let matched_subs = collect_sub_ids(&humidity_matches);
assert_eq!(matched_subs.len(), 1);
assert!(matched_subs.contains(&sub_id));
}
{
let topic =
TopicPath::new(ArcStr::from("sensors/living-room/temperature"));
let sensors_matches = root.find_by_path(&topic);
let matched_subs = collect_sub_ids(&sensors_matches);
assert_eq!(matched_subs.len(), 1);
assert!(matched_subs.contains(&sub_id));
}
}
fn collect_sub_ids(
matches: &[&HashSet<SubscriptionId>],
) -> HashSet<SubscriptionId> {
matches.iter().flat_map(|set| set.iter().cloned()).collect()
}
fn test_active_subscriptions(
subscriptions: &[(&str, usize)],
expected_active: &[(String, usize)],
) {
let mut root = TopicMatcherNode::<HashSet<SubscriptionId>>::new();
for (pattern_str, sub_id) in subscriptions {
let pattern = new_from_string(pattern_str).unwrap();
let sub_id = make_subscription_id(*sub_id);
root.get_or_create_subscription_table(&pattern)
.insert(sub_id);
}
let active = root
.collect_active_subscriptions()
.into_iter()
.map(|(path, d)| (path.to_string(), d.len()));
let mut expected_map: HashMap<String, usize> = HashMap::new();
for (pattern, count) in expected_active {
expected_map.insert(pattern.into(), *count);
}
let actual_map: HashMap<_, _> = active.into_iter().collect();
assert_eq!(
actual_map, expected_map,
"Active subscriptions {actual_map:?}, expected {expected_map:?}"
);
}
#[test]
fn test_simple_active_subscriptions() {
let subscriptions = [
("sensors/temperature", 1),
("sensors/humidity", 2),
("devices/light/status", 3),
];
let expected_active = [
("sensors/temperature".to_string(), 1),
("sensors/humidity".to_string(), 1),
("devices/light/status".to_string(), 1),
];
test_active_subscriptions(&subscriptions, &expected_active);
}
#[test]
fn test_wildcards_active_subscriptions() {
let subscriptions = [
("sensors/+/reading", 1),
("devices/+/+/state", 2),
("home/#", 3),
("#", 4),
("/", 5),
];
let expected_active = [
("sensors/+/reading".to_string(), 1),
("devices/+/+/state".to_string(), 1),
("home/#".to_string(), 1),
("#".to_string(), 1),
("/".to_string(), 1),
];
test_active_subscriptions(&subscriptions, &expected_active);
}
#[test]
fn test_multiple_subs_active_subscriptions() {
let mut root = TopicMatcherNode::<HashSet<SubscriptionId>>::new();
let pattern1 = new_from_string("sensors/temperature").unwrap();
let pattern2 = new_from_string("home/#").unwrap();
let subscribers1 = root.get_or_create_subscription_table(&pattern1);
subscribers1.insert(make_subscription_id(1));
subscribers1.insert(make_subscription_id(2));
subscribers1.insert(make_subscription_id(3));
let subscribers2 = root.get_or_create_subscription_table(&pattern2);
subscribers2.insert(make_subscription_id(4));
subscribers2.insert(make_subscription_id(5));
let expected: HashMap<String, usize> = [
("sensors/temperature".to_string(), 3),
("home/#".to_string(), 2),
]
.into_iter()
.collect();
let active = root.collect_active_subscriptions();
let actual: HashMap<String, usize> = active
.into_iter()
.map(|(s, d)| (s.to_string(), d.len()))
.collect();
assert_eq!(
actual, expected,
"Active subscriptions {actual:?}, expected {expected:?}"
);
}
fn test_update_node(
initial_subs: &[(&str, usize)],
operations: &[(&str, usize, bool)],
expected_active: &[(String, usize)],
) {
let mut root = TopicMatcherNode::<HashSet<SubscriptionId>>::new();
for (pattern_str, sub_id) in initial_subs {
let pattern = new_from_string(pattern_str).unwrap();
let sub_id = make_subscription_id(*sub_id);
root.get_or_create_subscription_table(&pattern)
.insert(sub_id);
}
for (pattern_str, sub_id, should_remove) in operations {
let pattern = new_from_string(pattern_str).unwrap();
let sub_id = make_subscription_id(*sub_id);
if *should_remove {
root.update_node(pattern.slice(), |subscriptions| {
subscriptions.remove(&sub_id);
})
.unwrap();
} else {
let subscriptions = root.get_or_create_subscription_table(&pattern);
subscriptions.insert(sub_id);
}
}
let active = root.collect_active_subscriptions();
let mut expected_map: HashMap<String, usize> = HashMap::new();
for (pattern, count) in expected_active {
expected_map.insert(pattern.clone(), *count);
}
let actual_map: HashMap<String, usize> = active
.into_iter()
.map(|(s, d)| (s.to_string(), d.len()))
.collect();
assert_eq!(
actual_map, expected_map,
"After update operations, active subscriptions are {actual_map:?}, \
expected {expected_map:?}"
);
}
#[test]
fn test_simple_unsubscribe() {
let initial_subs = [
("sensors/temperature", 1),
("sensors/humidity", 2),
("devices/light/status", 3),
];
let operations = [
("sensors/temperature", 1, true), ];
let expected_active = [
("sensors/humidity".to_string(), 1),
("devices/light/status".to_string(), 1),
];
test_update_node(&initial_subs, &operations, &expected_active);
}
#[test]
fn test_wildcard_unsubscribe() {
let initial_subs = [
("sensors/+/reading", 1),
("devices/+/+/state", 2),
("home/#", 3),
];
let operations = [
("home/#", 3, true), ("devices/+/+/state", 2, true), ];
let expected_active = [("sensors/+/reading".to_string(), 1)];
test_update_node(&initial_subs, &operations, &expected_active);
}
#[test]
fn test_multiple_subscribers_unsubscribe() {
let initial_subs = [
("sensors/temperature", 1),
("sensors/temperature", 2),
("sensors/temperature", 3),
("home/#", 4),
("home/#", 5),
];
let operations = [
("sensors/temperature", 2, true), ("home/#", 4, true), ];
let expected_active = [
("sensors/temperature".to_string(), 2), ("home/#".to_string(), 1), ];
test_update_node(&initial_subs, &operations, &expected_active);
}
#[test]
fn test_mixed_operations() {
let initial_subs = [("sensors/temperature", 1), ("home/+/light", 2)];
let operations = [
("sensors/temperature", 1, true), ("sensors/humidity", 3, false), ("home/+/light", 4, false), ("devices/#", 5, false), ];
let expected_active = [
("sensors/humidity".to_string(), 1),
("home/+/light".to_string(), 2), ("devices/#".to_string(), 1),
];
test_update_node(&initial_subs, &operations, &expected_active);
}
#[test]
fn test_unsubscribe_all() {
let initial_subs = [
("sensors/temperature", 1),
("sensors/humidity", 2),
("home/#", 3),
];
let operations = [
("sensors/temperature", 1, true),
("sensors/humidity", 2, true),
("home/#", 3, true),
];
let expected_active: [(String, usize); 0] = [];
test_update_node(&initial_subs, &operations, &expected_active);
let mut root = TopicMatcherNode::<HashSet<SubscriptionId>>::new();
for (pattern_str, sub_id) in initial_subs {
let pattern = new_from_string(pattern_str).unwrap();
let sub_id = make_subscription_id(sub_id);
root.get_or_create_subscription_table(&pattern)
.insert(sub_id);
}
for (pattern_str, sub_id, _) in operations {
let pattern = new_from_string(pattern_str).unwrap();
let sub_id = make_subscription_id(sub_id);
root.update_node(pattern.slice(), |subscriptions| {
subscriptions.remove(&sub_id);
})
.unwrap();
}
assert!(
root.is_empty(),
"Root node should be empty after unsubscribing all:{root:#?}"
);
}