use crate::range_set::RangeSet;
use std::collections::HashMap;
use std::fmt;
use std::str::FromStr;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum NodeSetError {
#[error("parse error: {msg} (in \"{part}\")")]
ParseError { part: String, msg: String },
#[error("range error: {0}")]
RangeError(#[from] crate::range_set::RangeSetError),
#[error("external error: {0}")]
ExternalError(String),
}
pub type Result<T> = std::result::Result<T, NodeSetError>;
#[derive(Debug, Clone)]
struct NodeSetBase {
patterns: HashMap<String, Option<RangeSet>>,
autostep: Option<u32>,
}
impl NodeSetBase {
fn new() -> Self {
Self {
patterns: HashMap::new(),
autostep: None,
}
}
fn with_autostep(autostep: Option<u32>) -> Self {
Self {
patterns: HashMap::new(),
autostep,
}
}
fn add(&mut self, pat: &str, rangeset: Option<RangeSet>) {
if let Some(existing) = self.patterns.get_mut(pat) {
if let (Some(existing_rs), Some(new_rs)) = (existing.as_mut(), &rangeset) {
existing_rs.update(new_rs);
}
} else {
self.patterns.insert(pat.to_string(), rangeset);
}
}
fn len(&self) -> usize {
self.patterns
.values()
.map(|rs| match rs {
Some(rs) => rs.len(),
None => 1, })
.sum()
}
fn is_empty(&self) -> bool {
self.patterns.is_empty()
}
fn clear(&mut self) {
self.patterns.clear();
}
fn update(&mut self, other: &NodeSetBase) {
for (pat, rangeset) in &other.patterns {
self.add(pat, rangeset.clone());
}
}
fn intersection_update(&mut self, other: &NodeSetBase) {
let mut result = HashMap::new();
for (pat, other_rs) in &other.patterns {
if let Some(self_rs) = self.patterns.get(pat) {
match (self_rs, other_rs) {
(Some(s), Some(o)) => {
let intersection = s.intersection(o);
if intersection.len() > 0 {
result.insert(pat.clone(), Some(intersection));
}
}
(None, None) => {
result.insert(pat.clone(), None);
}
_ => {} }
}
}
self.patterns = result;
}
fn difference_update(&mut self, other: &NodeSetBase) {
let mut purge = Vec::new();
for (pat, other_rs) in &other.patterns {
if let Some(self_rs) = self.patterns.get_mut(pat) {
match (self_rs, other_rs) {
(Some(s), Some(o)) => {
s.difference_update(o);
if s.len() == 0 {
purge.push(pat.clone());
}
}
(None, None) => {
purge.push(pat.clone());
}
_ => {} }
}
}
for pat in purge {
self.patterns.remove(&pat);
}
}
fn symmetric_difference_update(&mut self, other: &NodeSetBase) {
let mut purge = Vec::new();
for (pat, self_rs) in self.patterns.iter_mut() {
if let Some(other_rs) = other.patterns.get(pat) {
match (self_rs.as_mut(), other_rs) {
(Some(s), Some(o)) => {
s.symmetric_difference_update(o);
if s.len() == 0 {
purge.push(pat.clone());
}
}
(None, None) => {
purge.push(pat.clone());
}
_ => {}
}
}
}
for (pat, other_rs) in &other.patterns {
if !self.patterns.contains_key(pat) {
self.add(pat, other_rs.clone());
}
}
for pat in purge {
self.patterns.remove(&pat);
}
}
fn contains(&self, node: &str) -> bool {
let (pat, indices) = parse_node_indices(node);
if let Some(rs_opt) = self.patterns.get(&pat) {
return match (rs_opt, indices.as_slice()) {
(Some(rs), &[(_, _)]) => {
let digit_str = extract_last_digit_str(node);
rs.contains_str(&digit_str)
}
(None, &[]) => true,
_ => false,
};
}
if indices.len() > 1 {
let digit_strs = extract_all_digit_strs(node);
for n_sub in 1..indices.len() {
let mut candidate_pat = pat.clone();
for i in 0..n_sub {
candidate_pat = candidate_pat.replacen("%s", &digit_strs[i], 1);
}
if let Some(Some(rs)) = self.patterns.get(&candidate_pat) {
let last_digit = &digit_strs[digit_strs.len() - 1];
if rs.contains_str(last_digit) {
return true;
}
}
}
}
false
}
fn iter(&self) -> Vec<String> {
let mut nodes = Vec::new();
for (pat, rs_opt) in &self.patterns {
match rs_opt {
Some(rs) => {
for s in rs.striter() {
nodes.push(pat.replacen("%s", &s, 1));
}
}
None => {
nodes.push(pat.clone());
}
}
}
nodes.sort();
nodes
}
}
impl fmt::Display for NodeSetBase {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut parts: Vec<String> = Vec::new();
let mut sorted_pats: Vec<_> = self.patterns.iter().collect();
sorted_pats.sort_by(|a, b| a.0.cmp(b.0));
for (pat, rs_opt) in sorted_pats {
match rs_opt {
Some(rs) => {
let rs_str = format!("{}", rs);
if rs.len() == 1 {
parts.push(pat.replacen("%s", &rs_str, 1));
} else {
let bracketed = format!("[{}]", rs_str);
parts.push(pat.replacen("%s", &bracketed, 1));
}
}
None => {
parts.push(pat.clone());
}
}
}
write!(f, "{}", parts.join(","))
}
}
#[allow(dead_code)]
fn scan_node_single(node: &str) -> (String, Option<i64>) {
let mut pat = String::new();
let mut has_index = false;
let mut last_value: Option<i64> = None;
let chars: Vec<char> = node.chars().collect();
let len = chars.len();
let mut i = 0;
while i < len {
let prefix_start = i;
while i < len && !chars[i].is_ascii_digit() {
i += 1;
}
let prefix = &node[prefix_start..i];
let digit_start = i;
while i < len && chars[i].is_ascii_digit() {
i += 1;
}
let digits = &node[digit_start..i];
if digits.is_empty() {
pat.push_str(prefix);
} else {
pat.push_str(prefix);
pat.push_str("%s");
has_index = true;
last_value = Some(digits.parse().unwrap_or(0));
}
}
if has_index {
(pat, last_value)
} else {
(pat, None)
}
}
fn parse_node_indices(node: &str) -> (String, Vec<(i64, u32)>) {
let mut pat = String::new();
let mut indices = Vec::new();
let chars: Vec<char> = node.chars().collect();
let len = chars.len();
let mut i = 0;
while i < len {
let prefix_start = i;
while i < len && !chars[i].is_ascii_digit() {
i += 1;
}
pat.push_str(&node[prefix_start..i]);
let digit_start = i;
while i < len && chars[i].is_ascii_digit() {
i += 1;
}
let digits = &node[digit_start..i];
if !digits.is_empty() {
pat.push_str("%s");
let value: i64 = digits.parse().unwrap_or(0);
let pad = if value != 0 {
let stripped = digits.trim_start_matches('0');
if digits.len() > stripped.len() {
digits.len() as u32
} else {
0
}
} else if digits.len() > 1 {
digits.len() as u32
} else {
0
};
indices.push((value, pad));
}
}
(pat, indices)
}
fn parse_nodeset_string(input: &str, autostep: Option<u32>) -> Result<NodeSetBase> {
let input = input.trim();
if input.is_empty() {
return Ok(NodeSetBase::with_autostep(autostep));
}
let mut result = NodeSetBase::with_autostep(autostep);
let mut current_op = Op::Update;
let tokens = tokenize(input)?;
for token in tokens {
match token {
Token::Operator(op) => {
current_op = op;
}
Token::Pattern(s) => {
let parsed = parse_single_pattern(&s, autostep)?;
match current_op {
Op::Update => result.update(&parsed),
Op::Difference => result.difference_update(&parsed),
Op::Intersection => result.intersection_update(&parsed),
Op::SymmetricDifference => result.symmetric_difference_update(&parsed),
}
}
}
}
Ok(result)
}
#[derive(Debug, Clone, Copy)]
enum Op {
Update,
Difference,
Intersection,
SymmetricDifference,
}
#[derive(Debug)]
enum Token {
Operator(Op),
Pattern(String),
}
fn tokenize(input: &str) -> Result<Vec<Token>> {
let mut tokens = Vec::new();
let mut current = String::new();
let mut bracket_depth = 0;
let chars: Vec<char> = input.chars().collect();
let mut i = 0;
while i < chars.len() {
let ch = chars[i];
match ch {
'[' => {
bracket_depth += 1;
current.push(ch);
}
']' => {
if bracket_depth == 0 {
return Err(NodeSetError::ParseError {
part: input.to_string(),
msg: "illegal closing bracket".to_string(),
});
}
bracket_depth -= 1;
current.push(ch);
}
',' | '!' | '&' | '^' if bracket_depth == 0 => {
if !current.is_empty() {
tokens.push(Token::Pattern(current.trim().to_string()));
current = String::new();
}
let op = match ch {
',' => Op::Update,
'!' => Op::Difference,
'&' => Op::Intersection,
'^' => Op::SymmetricDifference,
_ => unreachable!(),
};
tokens.push(Token::Operator(op));
}
_ => {
current.push(ch);
}
}
i += 1;
}
if bracket_depth != 0 {
return Err(NodeSetError::ParseError {
part: input.to_string(),
msg: "missing bracket".to_string(),
});
}
if !current.is_empty() {
tokens.push(Token::Pattern(current.trim().to_string()));
}
let mut result = Vec::new();
let mut expect_pattern = true;
for token in tokens {
match &token {
Token::Pattern(_) => {
if expect_pattern {
if result.is_empty() {
result.push(Token::Operator(Op::Update));
}
result.push(token);
expect_pattern = false;
} else {
result.push(Token::Operator(Op::Update));
result.push(token);
expect_pattern = false;
}
}
Token::Operator(_) => {
result.push(token);
expect_pattern = true;
}
}
}
Ok(result)
}
fn parse_single_pattern(pat: &str, autostep: Option<u32>) -> Result<NodeSetBase> {
let mut base = NodeSetBase::with_autostep(autostep);
if pat.contains('[') {
parse_bracketed(pat, autostep, &mut base)?;
} else {
let (pattern, indices) = parse_node_indices(pat);
if indices.is_empty() {
base.add(&pattern, None);
} else {
let mut rs = RangeSet::new();
rs.set_autostep(autostep);
if indices.len() == 1 {
let (val, pad) = indices[0];
rs.add_int(val, pad);
base.add(&pattern, Some(rs));
} else {
let digit_strs = extract_all_digit_strs(pat);
let mut expanded_pat = pattern.clone();
for ds in &digit_strs[..digit_strs.len() - 1] {
expanded_pat = expanded_pat.replacen("%s", ds, 1);
}
let (val, pad) = indices[indices.len() - 1];
rs.add_int(val, pad);
base.add(&expanded_pat, Some(rs));
}
}
}
Ok(base)
}
fn parse_bracketed(pat: &str, autostep: Option<u32>, base: &mut NodeSetBase) -> Result<()> {
let mut remaining = pat;
let mut template = String::new();
let mut rangesets: Vec<RangeSet> = Vec::new();
while let Some(bracket_start) = remaining.find('[') {
let prefix = &remaining[..bracket_start];
let (clean_prefix, leading_digits) = split_trailing_digits(prefix);
template.push_str(clean_prefix);
template.push_str("%s");
let after_bracket = &remaining[bracket_start + 1..];
let bracket_end = after_bracket
.find(']')
.ok_or_else(|| NodeSetError::ParseError {
part: pat.to_string(),
msg: "missing bracket".to_string(),
})?;
let range_str = &after_bracket[..bracket_end];
remaining = &after_bracket[bracket_end + 1..];
let (trailing_digits, clean_remaining) = split_leading_digits(remaining);
let amended_range = amend_range(range_str, &leading_digits, &trailing_digits)?;
let rs = RangeSet::parse(&amended_range, autostep)?;
rangesets.push(rs);
remaining = clean_remaining;
}
if !remaining.is_empty() {
template.push_str(remaining);
}
if rangesets.len() == 1 {
base.add(&template, Some(rangesets.into_iter().next().unwrap()));
} else if rangesets.is_empty() {
base.add(&template, None);
} else {
expand_multi_dim(&template, &rangesets, base);
}
Ok(())
}
fn expand_multi_dim(template: &str, rangesets: &[RangeSet], base: &mut NodeSetBase) {
if rangesets.is_empty() {
return;
}
if rangesets.len() == 1 {
base.add(template, Some(rangesets[0].clone()));
return;
}
let last_rs = &rangesets[rangesets.len() - 1];
let prefixes = expand_dimensions(&rangesets[..rangesets.len() - 1]);
for prefix_vals in &prefixes {
let mut pat = template.to_string();
for val in prefix_vals {
pat = pat.replacen("%s", val, 1);
}
base.add(&pat, Some(last_rs.clone()));
}
}
fn expand_dimensions(rangesets: &[RangeSet]) -> Vec<Vec<String>> {
if rangesets.is_empty() {
return vec![vec![]];
}
let first_vals: Vec<String> = rangesets[0].striter().collect();
let rest = expand_dimensions(&rangesets[1..]);
let mut result = Vec::new();
for val in &first_vals {
for suffix in &rest {
let mut combined = vec![val.clone()];
combined.extend(suffix.iter().cloned());
result.push(combined);
}
}
result
}
fn extract_last_digit_str(node: &str) -> String {
let bytes = node.as_bytes();
let mut end = bytes.len();
while end > 0 && !bytes[end - 1].is_ascii_digit() {
end -= 1;
}
let mut start = end;
while start > 0 && bytes[start - 1].is_ascii_digit() {
start -= 1;
}
node[start..end].to_string()
}
fn extract_all_digit_strs(node: &str) -> Vec<String> {
let mut result = Vec::new();
let chars: Vec<char> = node.chars().collect();
let len = chars.len();
let mut i = 0;
while i < len {
while i < len && !chars[i].is_ascii_digit() {
i += 1;
}
let start = i;
while i < len && chars[i].is_ascii_digit() {
i += 1;
}
if i > start {
result.push(node[start..i].to_string());
}
}
result
}
fn split_trailing_digits(s: &str) -> (&str, String) {
let bytes = s.as_bytes();
let mut i = bytes.len();
while i > 0 && bytes[i - 1].is_ascii_digit() {
i -= 1;
}
(&s[..i], s[i..].to_string())
}
fn split_leading_digits(s: &str) -> (String, &str) {
let bytes = s.as_bytes();
let mut i = 0;
while i < bytes.len() && bytes[i].is_ascii_digit() {
i += 1;
}
(s[..i].to_string(), &s[i..])
}
fn amend_range(range: &str, leading: &str, trailing: &str) -> Result<String> {
let mut result = range.to_string();
if !leading.is_empty() {
let rs = RangeSet::parse(&result, None)?;
let mut parts = Vec::new();
for val_str in rs.striter() {
let combined = format!("{}{}", leading, val_str);
parts.push(combined);
}
result = parts.join(",");
}
if !trailing.is_empty() {
let rs = RangeSet::parse(&result, None)?;
let mut parts = Vec::new();
let trail_len = trailing.len();
let power = 10i64.pow(trail_len as u32);
let trail_val: i64 = trailing.parse().unwrap_or(0);
for val in rs.intiter() {
let combined = val * power + trail_val;
parts.push(format!("{}", combined));
}
result = parts.join(",");
}
Ok(result)
}
#[derive(Debug, Clone)]
pub struct NodeSet {
base: NodeSetBase,
}
impl NodeSet {
pub fn new() -> Self {
Self {
base: NodeSetBase::new(),
}
}
pub fn parse(pattern: &str) -> Result<Self> {
let base = parse_nodeset_string(pattern, None)?;
Ok(Self { base })
}
pub fn parse_with_autostep(pattern: &str, autostep: Option<u32>) -> Result<Self> {
let base = parse_nodeset_string(pattern, autostep)?;
Ok(Self { base })
}
pub fn len(&self) -> usize {
self.base.len()
}
pub fn is_empty(&self) -> bool {
self.base.is_empty()
}
pub fn contains(&self, node: &str) -> bool {
self.base.contains(node)
}
pub fn update(&mut self, other: &NodeSet) {
self.base.update(&other.base);
}
pub fn update_str(&mut self, pattern: &str) -> Result<()> {
let other = parse_nodeset_string(pattern, self.base.autostep)?;
self.base.update(&other);
Ok(())
}
pub fn union(&self, other: &NodeSet) -> NodeSet {
let mut copy = self.clone();
copy.update(other);
copy
}
pub fn intersection_update(&mut self, other: &NodeSet) {
self.base.intersection_update(&other.base);
}
pub fn intersection(&self, other: &NodeSet) -> NodeSet {
let mut copy = self.clone();
copy.intersection_update(other);
copy
}
pub fn difference_update(&mut self, other: &NodeSet) {
self.base.difference_update(&other.base);
}
pub fn difference(&self, other: &NodeSet) -> NodeSet {
let mut copy = self.clone();
copy.difference_update(other);
copy
}
pub fn symmetric_difference_update(&mut self, other: &NodeSet) {
self.base.symmetric_difference_update(&other.base);
}
pub fn symmetric_difference(&self, other: &NodeSet) -> NodeSet {
let mut copy = self.clone();
copy.symmetric_difference_update(other);
copy
}
pub fn clear(&mut self) {
self.base.clear();
}
pub fn iter(&self) -> impl Iterator<Item = String> + '_ {
self.base.iter().into_iter()
}
pub fn split(&self, nbr: usize) -> Vec<NodeSet> {
assert!(nbr > 0);
let all_nodes: Vec<String> = self.iter().collect();
let total = all_nodes.len();
let slice_size = total / nbr;
let leftover = total % nbr;
let mut result = Vec::new();
let mut begin = 0;
for i in 0..nbr.min(total) {
let length = slice_size + if i < leftover { 1 } else { 0 };
let slice = &all_nodes[begin..begin + length];
let pattern = slice.join(",");
if let Ok(ns) = NodeSet::parse(&pattern) {
result.push(ns);
}
begin += length;
}
result
}
pub fn is_subset(&self, other: &NodeSet) -> bool {
self.difference(other).is_empty()
}
pub fn is_superset(&self, other: &NodeSet) -> bool {
other.is_subset(self)
}
}
impl Default for NodeSet {
fn default() -> Self {
Self::new()
}
}
impl fmt::Display for NodeSet {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.base)
}
}
impl FromStr for NodeSet {
type Err = NodeSetError;
fn from_str(s: &str) -> Result<Self> {
NodeSet::parse(s)
}
}
pub fn expand(pat: &str) -> Result<Vec<String>> {
let ns = NodeSet::parse(pat)?;
Ok(ns.iter().collect())
}
pub fn fold(pat: &str) -> Result<String> {
let ns = NodeSet::parse(pat)?;
Ok(format!("{}", ns))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new_is_empty() {
let ns = NodeSet::new();
assert!(ns.is_empty());
assert_eq!(ns.len(), 0);
}
#[test]
fn test_parse_simple_node() {
let ns = NodeSet::parse("node1").unwrap();
assert_eq!(ns.len(), 1);
assert!(ns.contains("node1"));
assert!(!ns.contains("node2"));
}
#[test]
fn test_parse_range() {
let ns = NodeSet::parse("node[1-5]").unwrap();
assert_eq!(ns.len(), 5);
for i in 1..=5 {
assert!(ns.contains(&format!("node{}", i)));
}
assert!(!ns.contains("node0"));
assert!(!ns.contains("node6"));
}
#[test]
fn test_parse_multiple_ranges() {
let ns = NodeSet::parse("node[1-3,7-9]").unwrap();
assert_eq!(ns.len(), 6);
assert!(ns.contains("node1"));
assert!(ns.contains("node3"));
assert!(!ns.contains("node4"));
assert!(ns.contains("node7"));
assert!(ns.contains("node9"));
}
#[test]
fn test_parse_padded() {
let ns = NodeSet::parse("node[001-003]").unwrap();
assert_eq!(ns.len(), 3);
assert!(ns.contains("node001"));
assert!(ns.contains("node002"));
assert!(ns.contains("node003"));
let display = format!("{}", ns);
assert!(display.contains("001"));
}
#[test]
fn test_parse_comma_separated() {
let ns = NodeSet::parse("node1,node2,node3").unwrap();
assert_eq!(ns.len(), 3);
assert!(ns.contains("node1"));
assert!(ns.contains("node2"));
assert!(ns.contains("node3"));
}
#[test]
fn test_parse_difference_operator() {
let ns = NodeSet::parse("node[1-10]!node[5-7]").unwrap();
assert_eq!(ns.len(), 7);
assert!(ns.contains("node1"));
assert!(ns.contains("node4"));
assert!(!ns.contains("node5"));
assert!(!ns.contains("node6"));
assert!(!ns.contains("node7"));
assert!(ns.contains("node8"));
assert!(ns.contains("node10"));
}
#[test]
fn test_parse_intersection_operator() {
let ns = NodeSet::parse("node[1-10]&node[5-15]").unwrap();
assert_eq!(ns.len(), 6);
for i in 5..=10 {
assert!(ns.contains(&format!("node{}", i)));
}
assert!(!ns.contains("node1"));
assert!(!ns.contains("node11"));
}
#[test]
fn test_parse_xor_operator() {
let ns = NodeSet::parse("node[1-5]^node[3-7]").unwrap();
assert_eq!(ns.len(), 4);
assert!(ns.contains("node1"));
assert!(ns.contains("node2"));
assert!(!ns.contains("node3"));
assert!(!ns.contains("node5"));
assert!(ns.contains("node6"));
assert!(ns.contains("node7"));
}
#[test]
fn test_multi_pattern() {
let ns = NodeSet::parse("web[1-3],db[1-2]").unwrap();
assert_eq!(ns.len(), 5);
assert!(ns.contains("web1"));
assert!(ns.contains("web3"));
assert!(ns.contains("db1"));
assert!(ns.contains("db2"));
}
#[test]
fn test_display_fold() {
let ns = NodeSet::parse("node3,node1,node2").unwrap();
let display = format!("{}", ns);
assert_eq!(display, "node[1-3]");
}
#[test]
fn test_set_union() {
let a = NodeSet::parse("node[1-3]").unwrap();
let b = NodeSet::parse("node[5-7]").unwrap();
let c = a.union(&b);
assert_eq!(c.len(), 6);
}
#[test]
fn test_set_intersection() {
let a = NodeSet::parse("node[1-10]").unwrap();
let b = NodeSet::parse("node[5-15]").unwrap();
let c = a.intersection(&b);
assert_eq!(c.len(), 6);
}
#[test]
fn test_set_difference() {
let a = NodeSet::parse("node[1-10]").unwrap();
let b = NodeSet::parse("node[5-7]").unwrap();
let c = a.difference(&b);
assert_eq!(c.len(), 7);
}
#[test]
fn test_set_symmetric_difference() {
let a = NodeSet::parse("node[1-5]").unwrap();
let b = NodeSet::parse("node[3-7]").unwrap();
let c = a.symmetric_difference(&b);
assert_eq!(c.len(), 4);
}
#[test]
fn test_iter_sorted() {
let ns = NodeSet::parse("node[3,1,2]").unwrap();
let nodes: Vec<String> = ns.iter().collect();
assert_eq!(nodes, vec!["node1", "node2", "node3"]);
}
#[test]
fn test_non_indexed_node() {
let ns = NodeSet::parse("switch").unwrap();
assert_eq!(ns.len(), 1);
assert!(ns.contains("switch"));
}
#[test]
fn test_expand_function() {
let nodes = expand("node[1-3]").unwrap();
assert_eq!(nodes, vec!["node1", "node2", "node3"]);
}
#[test]
fn test_fold_function() {
let s = fold("node3,node1,node2").unwrap();
assert_eq!(s, "node[1-3]");
}
#[test]
fn test_from_str() {
let ns: NodeSet = "node[1-3]".parse().unwrap();
assert_eq!(ns.len(), 3);
}
#[test]
fn test_update_str() {
let mut ns = NodeSet::parse("node[1-3]").unwrap();
ns.update_str("node[5-7]").unwrap();
assert_eq!(ns.len(), 6);
}
#[test]
fn test_split() {
let ns = NodeSet::parse("node[1-6]").unwrap();
let parts = ns.split(3);
assert_eq!(parts.len(), 3);
assert_eq!(parts[0].len(), 2);
assert_eq!(parts[1].len(), 2);
assert_eq!(parts[2].len(), 2);
}
#[test]
fn test_subset_superset() {
let a = NodeSet::parse("node[1-5]").unwrap();
let b = NodeSet::parse("node[1-10]").unwrap();
assert!(a.is_subset(&b));
assert!(b.is_superset(&a));
assert!(!b.is_subset(&a));
}
#[test]
fn test_multi_dimensional() {
let ns = NodeSet::parse("rack[1-2]-node[1-3]").unwrap();
assert_eq!(ns.len(), 6);
assert!(ns.contains("rack1-node1"));
assert!(ns.contains("rack1-node3"));
assert!(ns.contains("rack2-node1"));
assert!(ns.contains("rack2-node3"));
assert!(!ns.contains("rack3-node1"));
}
#[test]
fn test_empty_parse() {
let ns = NodeSet::parse("").unwrap();
assert!(ns.is_empty());
}
#[test]
fn test_missing_bracket_error() {
let result = NodeSet::parse("node[1-3");
assert!(result.is_err());
}
#[test]
fn test_clear() {
let mut ns = NodeSet::parse("node[1-5]").unwrap();
assert!(!ns.is_empty());
ns.clear();
assert!(ns.is_empty());
}
#[test]
fn test_dotted_hostname_single() {
let ns = NodeSet::parse("server1.example.com").unwrap();
assert_eq!(ns.len(), 1);
let nodes: Vec<String> = ns.iter().collect();
assert_eq!(nodes, vec!["server1.example.com"]);
assert_eq!(format!("{ns}"), "server1.example.com");
}
#[test]
fn test_dotted_hostname_multiple() {
let ns = NodeSet::parse("server1.local,server2.local").unwrap();
assert_eq!(ns.len(), 2);
let nodes: Vec<String> = ns.iter().collect();
assert_eq!(nodes, vec!["server1.local", "server2.local"]);
}
#[test]
fn test_ip_address_single() {
let ns = NodeSet::parse("192.168.1.29").unwrap();
assert_eq!(ns.len(), 1);
let nodes: Vec<String> = ns.iter().collect();
assert_eq!(nodes, vec!["192.168.1.29"]);
assert_eq!(format!("{ns}"), "192.168.1.29");
}
#[test]
fn test_ip_address_multiple_same_subnet() {
let ns = NodeSet::parse("192.168.1.3,192.168.1.29").unwrap();
assert_eq!(ns.len(), 2);
let mut nodes: Vec<String> = ns.iter().collect();
nodes.sort();
assert_eq!(nodes, vec!["192.168.1.29", "192.168.1.3"]);
assert_eq!(format!("{ns}"), "192.168.1.[3,29]");
}
#[test]
fn test_ip_address_different_subnets() {
let ns = NodeSet::parse("10.0.0.1,192.168.1.1").unwrap();
assert_eq!(ns.len(), 2);
let mut nodes: Vec<String> = ns.iter().collect();
nodes.sort();
assert_eq!(nodes, vec!["10.0.0.1", "192.168.1.1"]);
}
#[test]
fn test_ip_address_contains() {
let ns = NodeSet::parse("192.168.1.3,192.168.1.29").unwrap();
assert!(ns.contains("192.168.1.3"));
assert!(ns.contains("192.168.1.29"));
assert!(!ns.contains("192.168.1.4"));
assert!(!ns.contains("10.0.0.1"));
}
#[test]
fn test_dotted_hostname_with_numbers() {
let ns = NodeSet::parse("node1.rack2.dc3").unwrap();
assert_eq!(ns.len(), 1);
let nodes: Vec<String> = ns.iter().collect();
assert_eq!(nodes, vec!["node1.rack2.dc3"]);
}
#[test]
fn test_mdns_hostname() {
let ns = NodeSet::parse("GN9CFLM92K-MBP.local").unwrap();
assert_eq!(ns.len(), 1);
let nodes: Vec<String> = ns.iter().collect();
assert_eq!(nodes, vec!["GN9CFLM92K-MBP.local"]);
}
#[test]
fn test_mdns_hostname_comma_list() {
let ns = NodeSet::parse("CK2Q9LN7PM-MBA.local,GJHC5VVN49-MBP.local,GN9CFLM92K-MBP.local")
.unwrap();
assert_eq!(ns.len(), 3);
let nodes: Vec<String> = ns.iter().collect();
assert!(nodes.contains(&"CK2Q9LN7PM-MBA.local".to_string()));
assert!(nodes.contains(&"GJHC5VVN49-MBP.local".to_string()));
assert!(nodes.contains(&"GN9CFLM92K-MBP.local".to_string()));
}
#[test]
fn test_thunderbolt_hostname() {
let ns = NodeSet::parse("CK2Q9LN7PM-MBA.tb").unwrap();
assert_eq!(ns.len(), 1);
let nodes: Vec<String> = ns.iter().collect();
assert_eq!(nodes, vec!["CK2Q9LN7PM-MBA.tb"]);
}
}