#![cfg_attr(coverage_nightly, coverage(off))]
use super::types::ExecutionSnapshot;
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct VariableChange {
pub old_value: serde_json::Value,
pub new_value: serde_json::Value,
pub type_changed: bool,
}
#[derive(Debug, Clone)]
pub struct DiffStatistics {
pub changed_count: usize,
pub added_count: usize,
pub removed_count: usize,
pub unchanged_count: usize,
pub total_variables_before: usize,
pub total_variables_after: usize,
}
#[derive(Debug, Clone)]
pub struct VariableDiff {
pub changed: HashMap<String, VariableChange>,
pub added: HashMap<String, serde_json::Value>,
pub removed: HashMap<String, serde_json::Value>,
pub unchanged: Vec<String>,
}
impl VariableDiff {
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "score_range")]
pub fn compute(before: &ExecutionSnapshot, after: &ExecutionSnapshot) -> Self {
let mut changed = HashMap::new();
let mut added = HashMap::new();
let mut removed = HashMap::new();
let mut unchanged = Vec::new();
for (name, old_value) in &before.variables {
if let Some(new_value) = after.variables.get(name) {
if old_value != new_value {
let type_changed =
Self::value_type_name(old_value) != Self::value_type_name(new_value);
changed.insert(
name.clone(),
VariableChange {
old_value: old_value.clone(),
new_value: new_value.clone(),
type_changed,
},
);
} else {
unchanged.push(name.clone());
}
} else {
removed.insert(name.clone(), old_value.clone());
}
}
for (name, new_value) in &after.variables {
if !before.variables.contains_key(name) {
added.insert(name.clone(), new_value.clone());
}
}
Self {
changed,
added,
removed,
unchanged,
}
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub fn render_colored(&self) -> String {
let mut output = String::new();
output.push_str("=== Variable Diff ===\n\n");
if !self.changed.is_empty() {
output.push_str("\x1b[33mChanged:\x1b[0m\n");
for (name, change) in &self.changed {
if change.type_changed {
output.push_str(&format!(
" \x1b[33m{}\x1b[0m: {} -> {} \x1b[35m(type changed)\x1b[0m\n",
name, change.old_value, change.new_value
));
} else {
output.push_str(&format!(
" \x1b[33m{}\x1b[0m: {} -> {}\n",
name, change.old_value, change.new_value
));
}
}
output.push('\n');
}
if !self.added.is_empty() {
output.push_str("\x1b[32mAdded:\x1b[0m\n");
for (name, value) in &self.added {
output.push_str(&format!(" \x1b[32m+{}\x1b[0m: {}\n", name, value));
}
output.push('\n');
}
if !self.removed.is_empty() {
output.push_str("\x1b[31mRemoved:\x1b[0m\n");
for (name, value) in &self.removed {
output.push_str(&format!(" \x1b[31m-{}\x1b[0m: {}\n", name, value));
}
output.push('\n');
}
if !self.unchanged.is_empty() {
output.push_str("\x1b[90mUnchanged:\x1b[0m ");
output.push_str(&self.unchanged.join(", "));
output.push('\n');
}
output
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub fn render_side_by_side(&self) -> String {
let mut output = String::new();
output.push_str("=== Side-by-Side Comparison ===\n\n");
output.push_str(&format!(
"{:<30} | {:<30}\n",
"Snapshot #0 (Before)", "Snapshot #1 (After)"
));
output.push_str(&"-".repeat(63));
output.push('\n');
for (name, change) in &self.changed {
let old_display = format!("{}: {}", name, change.old_value);
let new_display = format!("{}: {}", name, change.new_value);
output.push_str(&format!("{:<30} | {:<30}\n", old_display, new_display));
}
for (name, value) in &self.removed {
let old_display = format!("{}: {}", name, value);
output.push_str(&format!("{:<30} | {:<30}\n", old_display, "(removed)"));
}
for (name, value) in &self.added {
let new_display = format!("{}: {}", name, value);
output.push_str(&format!("{:<30} | {:<30}\n", "(new)", new_display));
}
output
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub fn get_statistics(&self) -> DiffStatistics {
let total_before = self.changed.len() + self.removed.len() + self.unchanged.len();
let total_after = self.changed.len() + self.added.len() + self.unchanged.len();
DiffStatistics {
changed_count: self.changed.len(),
added_count: self.added.len(),
removed_count: self.removed.len(),
unchanged_count: self.unchanged.len(),
total_variables_before: total_before,
total_variables_after: total_after,
}
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub fn to_json(&self) -> String {
let mut json = serde_json::Map::new();
let changed_obj: serde_json::Map<String, serde_json::Value> = self
.changed
.iter()
.map(|(name, change)| {
let mut change_obj = serde_json::Map::new();
change_obj.insert("old".to_string(), change.old_value.clone());
change_obj.insert("new".to_string(), change.new_value.clone());
change_obj.insert(
"type_changed".to_string(),
serde_json::Value::Bool(change.type_changed),
);
(name.clone(), serde_json::Value::Object(change_obj))
})
.collect();
json.insert(
"changed".to_string(),
serde_json::Value::Object(changed_obj),
);
let added_obj: serde_json::Map<String, serde_json::Value> = self
.added
.iter()
.map(|(name, value)| (name.clone(), value.clone()))
.collect();
json.insert("added".to_string(), serde_json::Value::Object(added_obj));
let removed_obj: serde_json::Map<String, serde_json::Value> = self
.removed
.iter()
.map(|(name, value)| (name.clone(), value.clone()))
.collect();
json.insert(
"removed".to_string(),
serde_json::Value::Object(removed_obj),
);
let unchanged_arr: Vec<serde_json::Value> = self
.unchanged
.iter()
.map(|name| serde_json::Value::String(name.clone()))
.collect();
json.insert(
"unchanged".to_string(),
serde_json::Value::Array(unchanged_arr),
);
let stats = self.get_statistics();
let mut stats_obj = serde_json::Map::new();
stats_obj.insert(
"changed".to_string(),
serde_json::Value::Number(stats.changed_count.into()),
);
stats_obj.insert(
"added".to_string(),
serde_json::Value::Number(stats.added_count.into()),
);
stats_obj.insert(
"removed".to_string(),
serde_json::Value::Number(stats.removed_count.into()),
);
stats_obj.insert(
"unchanged".to_string(),
serde_json::Value::Number(stats.unchanged_count.into()),
);
stats_obj.insert(
"total_before".to_string(),
serde_json::Value::Number(stats.total_variables_before.into()),
);
stats_obj.insert(
"total_after".to_string(),
serde_json::Value::Number(stats.total_variables_after.into()),
);
json.insert(
"statistics".to_string(),
serde_json::Value::Object(stats_obj),
);
serde_json::to_string_pretty(&json).expect("internal error")
}
fn value_type_name(value: &serde_json::Value) -> &'static str {
match value {
serde_json::Value::Null => "null",
serde_json::Value::Bool(_) => "boolean",
serde_json::Value::Number(_) => "number",
serde_json::Value::String(_) => "string",
serde_json::Value::Array(_) => "array",
serde_json::Value::Object(_) => "object",
}
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod tests {
use super::*;
use crate::services::dap::types::{SourceLocation, StackFrame};
fn create_test_snapshot(sequence: usize) -> ExecutionSnapshot {
ExecutionSnapshot {
timestamp: 1000000 + (sequence as u64 * 1000),
sequence,
variables: std::collections::HashMap::new(),
call_stack: vec![StackFrame {
id: 1,
name: "main".to_string(),
source: None,
line: 10,
column: 0,
}],
location: SourceLocation {
file: "test.rs".to_string(),
line: 10,
column: Some(0),
},
delta: None,
}
}
#[test]
fn test_basic_diff_computation() {
let mut before = create_test_snapshot(0);
before
.variables
.insert("x".to_string(), serde_json::json!(10));
let mut after = create_test_snapshot(1);
after
.variables
.insert("x".to_string(), serde_json::json!(15));
let diff = VariableDiff::compute(&before, &after);
assert_eq!(diff.changed.len(), 1);
assert!(diff.changed.contains_key("x"));
}
#[test]
fn test_type_detection() {
assert_eq!(
VariableDiff::value_type_name(&serde_json::json!(42)),
"number"
);
assert_eq!(
VariableDiff::value_type_name(&serde_json::json!("hello")),
"string"
);
assert_eq!(
VariableDiff::value_type_name(&serde_json::json!(true)),
"boolean"
);
assert_eq!(
VariableDiff::value_type_name(&serde_json::json!([1, 2, 3])),
"array"
);
assert_eq!(
VariableDiff::value_type_name(&serde_json::json!({"key": "value"})),
"object"
);
}
}