use crate::ie::IeType;
use crate::message::MsgType;
use std::fmt;
#[derive(Debug, Clone)]
pub struct MessageDiff {
pub left_type: MsgType,
pub right_type: MsgType,
pub differences: Vec<Difference>,
}
impl MessageDiff {
pub fn new(left_type: MsgType, right_type: MsgType) -> Self {
Self {
left_type,
right_type,
differences: Vec::new(),
}
}
pub fn from_result(result: &super::result::ComparisonResult) -> Self {
Self::from_parts(
result.left_type,
result.right_type,
result.header_match,
&result.ie_mismatches,
&result.left_only_ies,
&result.right_only_ies,
)
}
pub(super) fn from_parts(
left_type: MsgType,
right_type: MsgType,
header_match: super::result::HeaderMatch,
ie_mismatches: &[super::result::IeMismatch],
left_only_ies: &[crate::ie::IeType],
right_only_ies: &[crate::ie::IeType],
) -> Self {
let mut diff = Self::new(left_type, right_type);
if !header_match.is_complete_match() {
if !header_match.message_type_match {
diff.differences.push(Difference::HeaderField {
field: HeaderField::MessageType,
left_value: format!("{:?}", left_type),
right_value: format!("{:?}", right_type),
});
}
if header_match.sequence_match == Some(false) {
diff.differences.push(Difference::HeaderField {
field: HeaderField::Sequence,
left_value: "differs".to_string(),
right_value: "differs".to_string(),
});
}
if header_match.seid_match == Some(false) {
diff.differences.push(Difference::HeaderField {
field: HeaderField::Seid,
left_value: "differs".to_string(),
right_value: "differs".to_string(),
});
}
if header_match.priority_match == Some(false) {
diff.differences.push(Difference::HeaderField {
field: HeaderField::Priority,
left_value: "differs".to_string(),
right_value: "differs".to_string(),
});
}
}
for mismatch in ie_mismatches {
match &mismatch.reason {
super::result::MismatchReason::ValueMismatch => {
let left_hex = mismatch
.left_payload
.as_ref()
.map(|p| format_hex(p))
.unwrap_or_else(|| "N/A".to_string());
let right_hex = mismatch
.right_payload
.as_ref()
.map(|p| format_hex(p))
.unwrap_or_else(|| "N/A".to_string());
diff.differences.push(Difference::IeValue {
ie_type: mismatch.ie_type,
context: vec![],
left_hex,
right_hex,
});
}
super::result::MismatchReason::CountMismatch {
left_count,
right_count,
} => {
diff.differences.push(Difference::IeCount {
ie_type: mismatch.ie_type,
left_count: *left_count,
right_count: *right_count,
});
}
super::result::MismatchReason::GroupedIeMismatch { .. } => {
diff.differences.push(Difference::GroupedIeStructure {
ie_type: mismatch.ie_type,
details: mismatch.reason.to_string(),
});
}
super::result::MismatchReason::SemanticMismatch { details } => {
diff.differences.push(Difference::IeValue {
ie_type: mismatch.ie_type,
context: vec![],
left_hex: format!("semantic: {}", details),
right_hex: "differs".to_string(),
});
}
_ => {}
}
}
for ie_type in left_only_ies {
diff.differences.push(Difference::LeftOnly {
ie_type: *ie_type,
context: vec![],
});
}
for ie_type in right_only_ies {
diff.differences.push(Difference::RightOnly {
ie_type: *ie_type,
context: vec![],
});
}
diff
}
pub fn is_empty(&self) -> bool {
self.differences.is_empty()
}
pub fn len(&self) -> usize {
self.differences.len()
}
pub fn to_yaml(&self) -> String {
let mut output = String::new();
output.push_str(&format!("left: {:?}\n", self.left_type));
output.push_str(&format!("right: {:?}\n", self.right_type));
output.push_str(&format!("differences: {}\n", self.len()));
if self.is_empty() {
output.push_str(" # No differences found\n");
} else {
for (i, diff) in self.differences.iter().enumerate() {
output.push_str(&format!(" {}:\n", i + 1));
output.push_str(&diff.to_yaml_entry());
}
}
output
}
pub fn summary(&self) -> String {
if self.is_empty() {
"No differences".to_string()
} else {
format!("{} difference(s) found", self.len())
}
}
}
impl fmt::Display for MessageDiff {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.to_yaml())
}
}
#[derive(Debug, Clone)]
pub enum Difference {
HeaderField {
field: HeaderField,
left_value: String,
right_value: String,
},
IeValue {
ie_type: IeType,
context: Vec<IeType>, left_hex: String,
right_hex: String,
},
IeCount {
ie_type: IeType,
left_count: usize,
right_count: usize,
},
LeftOnly {
ie_type: IeType,
context: Vec<IeType>,
},
RightOnly {
ie_type: IeType,
context: Vec<IeType>,
},
GroupedIeStructure { ie_type: IeType, details: String },
}
impl Difference {
fn to_yaml_entry(&self) -> String {
let mut output = String::new();
match self {
Difference::HeaderField {
field,
left_value,
right_value,
} => {
output.push_str(" type: header_field\n");
output.push_str(&format!(" field: {:?}\n", field));
output.push_str(&format!(" left: '{}'\n", left_value));
output.push_str(&format!(" right: '{}'\n", right_value));
}
Difference::IeValue {
ie_type,
context,
left_hex,
right_hex,
} => {
output.push_str(" type: ie_value\n");
let path = if context.is_empty() {
format!("{:?}", ie_type)
} else {
format!(
"{} > {:?}",
context
.iter()
.map(|t| format!("{:?}", t))
.collect::<Vec<_>>()
.join(" > "),
ie_type
)
};
output.push_str(&format!(" ie: {}\n", path));
output.push_str(&format!(" left: {}\n", left_hex));
output.push_str(&format!(" right: {}\n", right_hex));
}
Difference::IeCount {
ie_type,
left_count,
right_count,
} => {
output.push_str(" type: ie_count\n");
output.push_str(&format!(" ie: {:?}\n", ie_type));
output.push_str(&format!(" left_count: {}\n", left_count));
output.push_str(&format!(" right_count: {}\n", right_count));
}
Difference::LeftOnly { ie_type, context } => {
output.push_str(" type: left_only\n");
let path = if context.is_empty() {
format!("{:?}", ie_type)
} else {
format!(
"{} > {:?}",
context
.iter()
.map(|t| format!("{:?}", t))
.collect::<Vec<_>>()
.join(" > "),
ie_type
)
};
output.push_str(&format!(" ie: {}\n", path));
}
Difference::RightOnly { ie_type, context } => {
output.push_str(" type: right_only\n");
let path = if context.is_empty() {
format!("{:?}", ie_type)
} else {
format!(
"{} > {:?}",
context
.iter()
.map(|t| format!("{:?}", t))
.collect::<Vec<_>>()
.join(" > "),
ie_type
)
};
output.push_str(&format!(" ie: {}\n", path));
}
Difference::GroupedIeStructure { ie_type, details } => {
output.push_str(" type: grouped_ie_structure\n");
output.push_str(&format!(" ie: {:?}\n", ie_type));
output.push_str(&format!(" details: {}\n", details));
}
}
output
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HeaderField {
MessageType,
Sequence,
Seid,
Priority,
}
fn format_hex(bytes: &[u8]) -> String {
const MAX_BYTES: usize = 16;
if bytes.len() <= MAX_BYTES {
bytes
.iter()
.map(|b| format!("{:02x}", b))
.collect::<Vec<_>>()
.join(" ")
} else {
let shown = &bytes[..MAX_BYTES];
let hex = shown
.iter()
.map(|b| format!("{:02x}", b))
.collect::<Vec<_>>()
.join(" ");
format!("{} ... ({} bytes total)", hex, bytes.len())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_message_diff_new() {
let diff = MessageDiff::new(MsgType::HeartbeatRequest, MsgType::HeartbeatResponse);
assert_eq!(diff.left_type, MsgType::HeartbeatRequest);
assert_eq!(diff.right_type, MsgType::HeartbeatResponse);
assert!(diff.is_empty());
assert_eq!(diff.len(), 0);
}
#[test]
fn test_message_diff_summary() {
let mut diff = MessageDiff::new(MsgType::HeartbeatRequest, MsgType::HeartbeatRequest);
assert_eq!(diff.summary(), "No differences");
diff.differences.push(Difference::HeaderField {
field: HeaderField::Sequence,
left_value: "100".to_string(),
right_value: "200".to_string(),
});
assert_eq!(diff.summary(), "1 difference(s) found");
}
#[test]
fn test_format_hex_short() {
let bytes = vec![0x01, 0x02, 0x03, 0x04];
assert_eq!(format_hex(&bytes), "01 02 03 04");
}
#[test]
fn test_format_hex_long() {
let bytes = vec![0u8; 20];
let result = format_hex(&bytes);
assert!(result.contains("..."));
assert!(result.contains("20 bytes total"));
}
#[test]
fn test_difference_yaml_header_field() {
let diff = Difference::HeaderField {
field: HeaderField::Sequence,
left_value: "100".to_string(),
right_value: "200".to_string(),
};
let yaml = diff.to_yaml_entry();
assert!(yaml.contains("type: header_field"));
assert!(yaml.contains("field: Sequence"));
assert!(yaml.contains("left: '100'"));
assert!(yaml.contains("right: '200'"));
}
#[test]
fn test_difference_yaml_ie_value() {
let diff = Difference::IeValue {
ie_type: IeType::Cause,
context: vec![],
left_hex: "01".to_string(),
right_hex: "02".to_string(),
};
let yaml = diff.to_yaml_entry();
assert!(yaml.contains("type: ie_value"));
assert!(yaml.contains("ie: Cause"));
assert!(yaml.contains("left: 01"));
assert!(yaml.contains("right: 02"));
}
#[test]
fn test_difference_yaml_ie_count() {
let diff = Difference::IeCount {
ie_type: IeType::CreatePdr,
left_count: 2,
right_count: 3,
};
let yaml = diff.to_yaml_entry();
assert!(yaml.contains("type: ie_count"));
assert!(yaml.contains("ie: CreatePdr"));
assert!(yaml.contains("left_count: 2"));
assert!(yaml.contains("right_count: 3"));
}
#[test]
fn test_message_diff_display() {
let mut diff = MessageDiff::new(MsgType::HeartbeatRequest, MsgType::HeartbeatRequest);
diff.differences.push(Difference::HeaderField {
field: HeaderField::Sequence,
left_value: "100".to_string(),
right_value: "200".to_string(),
});
let display = format!("{}", diff);
assert!(display.contains("HeartbeatRequest"));
assert!(display.contains("differences: 1"));
}
#[test]
fn test_message_diff_to_yaml() {
let mut diff = MessageDiff::new(MsgType::HeartbeatRequest, MsgType::HeartbeatRequest);
diff.differences.push(Difference::LeftOnly {
ie_type: IeType::NodeId,
context: vec![],
});
let yaml = diff.to_yaml();
assert!(yaml.contains("left: HeartbeatRequest"));
assert!(yaml.contains("right: HeartbeatRequest"));
assert!(yaml.contains("differences: 1"));
assert!(yaml.contains("type: left_only"));
}
}