use std::collections::HashSet;
use super::filter::PipelineFilter;
pub(super) fn extract_selected_clusters(filters: &[PipelineFilter]) -> HashSet<String> {
let mut out = HashSet::new();
for pf in filters {
out.extend(pf.filter.selected_clusters());
for branch in &pf.branches {
out.extend(extract_selected_clusters(&branch.filters));
}
}
out
}
pub(super) fn extract_lb_clusters(filters: &[PipelineFilter]) -> HashSet<String> {
let mut out = HashSet::new();
for pf in filters {
out.extend(pf.filter.load_balancer_clusters());
for branch in &pf.branches {
out.extend(extract_lb_clusters(&branch.filters));
}
}
out
}
pub(super) fn level_selected_clusters(filters: &[PipelineFilter]) -> HashSet<String> {
filters.iter().flat_map(|pf| pf.filter.selected_clusters()).collect()
}
pub(super) fn reachable_lb_clusters(filters: &[PipelineFilter]) -> HashSet<String> {
let mut out = HashSet::new();
for pf in filters {
out.extend(pf.filter.load_balancer_clusters());
if pf.conditions.is_empty() {
for branch in &pf.branches {
if branch.condition.is_none() {
out.extend(reachable_lb_clusters(&branch.filters));
}
}
}
}
out
}
#[cfg(test)]
#[expect(clippy::allow_attributes, reason = "blanket test suppressions")]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::indexing_slicing,
clippy::panic,
reason = "tests"
)]
mod tests {
use super::*;
use crate::pipeline::test_filters::{lb_filter, noop_filter, selector_filter};
#[test]
fn extracts_selected_clusters() {
let filters = vec![selector_filter("router", &["web", "api"])];
let clusters = extract_selected_clusters(&filters);
assert_eq!(clusters.len(), 2, "should extract two clusters");
assert!(clusters.contains("web"), "should contain 'web'");
assert!(clusters.contains("api"), "should contain 'api'");
}
#[test]
fn extracts_lb_clusters() {
let filters = vec![lb_filter(&["web", "api"])];
let clusters = extract_lb_clusters(&filters);
assert_eq!(clusters.len(), 2, "should extract two clusters");
assert!(clusters.contains("web"), "should contain 'web'");
assert!(clusters.contains("api"), "should contain 'api'");
}
#[test]
fn skips_non_cluster_selecting_entries() {
let filters = vec![noop_filter("ip_acl")];
let clusters = extract_selected_clusters(&filters);
assert!(
clusters.is_empty(),
"non-cluster-selecting entries should yield no clusters"
);
}
#[test]
fn merges_selected_clusters_from_multiple_filters() {
let filters = vec![
selector_filter("router", &["web"]),
selector_filter("custom_selector", &["weather-backend"]),
];
let clusters = extract_selected_clusters(&filters);
assert_eq!(clusters.len(), 2, "should merge selected clusters");
assert!(clusters.contains("web"), "should contain router cluster");
assert!(
clusters.contains("weather-backend"),
"should contain custom selector cluster"
);
}
#[test]
fn skips_non_load_balancer_entries() {
let filters = vec![selector_filter("router", &["web"])];
let clusters = extract_lb_clusters(&filters);
assert!(clusters.is_empty(), "non-LB entries should yield no clusters");
}
#[test]
fn deduplicates_selected_clusters() {
let filters = vec![
selector_filter("router", &["web"]),
selector_filter("custom_selector", &["web"]),
];
let clusters = extract_selected_clusters(&filters);
assert_eq!(clusters.len(), 1, "duplicate cluster names should be deduplicated");
assert!(clusters.contains("web"), "should contain 'web'");
}
#[test]
fn recurses_into_branch_subchains() {
use std::sync::Arc;
use crate::pipeline::branch::{RejoinTarget, ResolvedBranch};
let mut host = noop_filter("headers");
host.branches = vec![ResolvedBranch {
condition: None,
filters: vec![selector_filter("router", &["branch-cluster"])],
max_iterations: None,
name: Arc::from("br"),
rejoin: RejoinTarget::Terminal,
}];
let selected = extract_selected_clusters(&[host]);
assert!(
selected.contains("branch-cluster"),
"a cluster selected inside a branch sub-chain must be collected"
);
}
#[test]
fn empty_entries_yields_empty() {
let filters = vec![];
assert!(
extract_selected_clusters(&filters).is_empty(),
"empty input should yield empty set"
);
assert!(
extract_lb_clusters(&filters).is_empty(),
"empty input should yield empty set"
);
}
use std::sync::Arc;
use praxis_core::config::{Condition, ConditionMatch};
use crate::pipeline::branch::{RejoinTarget, ResolvedBranch, ResolvedBranchCondition};
fn host_with(condition: Option<ResolvedBranchCondition>, branch_filters: Vec<PipelineFilter>) -> PipelineFilter {
let mut host = noop_filter("headers");
host.branches = vec![ResolvedBranch {
condition,
filters: branch_filters,
max_iterations: None,
name: Arc::from("br"),
rejoin: RejoinTarget::Next,
}];
host
}
fn cond() -> ResolvedBranchCondition {
ResolvedBranchCondition {
filter_name: Arc::from("classifier"),
key: Arc::from("kind"),
value: Arc::from("premium"),
}
}
#[test]
fn reachable_includes_this_levels_load_balancers() {
let filters = vec![lb_filter(&["web", "api"])];
let clusters = reachable_lb_clusters(&filters);
assert!(clusters.contains("web") && clusters.contains("api"));
}
#[test]
fn reachable_folds_unconditional_branch_lb() {
let filters = vec![host_with(None, vec![lb_filter(&["x"])])];
assert!(
reachable_lb_clusters(&filters).contains("x"),
"unconditional branch LB must be reachable"
);
}
#[test]
fn reachable_excludes_conditional_branch_lb() {
let filters = vec![host_with(Some(cond()), vec![lb_filter(&["x"])])];
assert!(
!reachable_lb_clusters(&filters).contains("x"),
"conditional branch LB must not be reachable"
);
}
#[test]
fn reachable_excludes_branch_lb_on_conditional_host() {
let mut host = noop_filter("headers");
host.conditions = vec![Condition::When(ConditionMatch {
path: None,
path_prefix: Some("/x".to_owned()),
methods: None,
headers: None,
})];
host.branches = vec![ResolvedBranch {
condition: None,
filters: vec![lb_filter(&["x"])],
max_iterations: None,
name: Arc::from("br"),
rejoin: RejoinTarget::Next,
}];
assert!(
!reachable_lb_clusters(&[host]).contains("x"),
"a branch on a conditional host must not be reachable"
);
}
#[test]
fn reachable_folds_nested_unconditional_branches() {
let inner = host_with(None, vec![lb_filter(&["deep"])]);
let outer = host_with(None, vec![inner]);
assert!(
reachable_lb_clusters(&[outer]).contains("deep"),
"nested unconditional branch LBs must fold up"
);
}
#[test]
fn reachable_stops_folding_at_conditional_nesting() {
let inner = host_with(Some(cond()), vec![lb_filter(&["deep"])]);
let outer = host_with(None, vec![inner]);
assert!(
!reachable_lb_clusters(&[outer]).contains("deep"),
"a conditional nested branch stops the reachability fold"
);
}
}