use std::collections::HashSet;
use crate::error::codes::ErrorCode;
use crate::error::diagnostic::{AgmError, ErrorLocation, Severity};
use crate::model::fields::NodeStatus;
use crate::model::file::AgmFile;
use crate::model::node::Node;
fn is_active(node: &Node) -> bool {
matches!(&node.status, None | Some(NodeStatus::Active))
}
#[must_use]
pub fn validate_compatibility(file: &AgmFile, file_name: &str) -> Vec<AgmError> {
let mut errors = Vec::new();
let node_map: std::collections::HashMap<&str, &Node> =
file.nodes.iter().map(|n| (n.id.as_str(), n)).collect();
let mut reported: HashSet<(String, String)> = HashSet::new();
for node in &file.nodes {
if !is_active(node) {
continue;
}
let conflicts = match &node.conflicts {
Some(c) => c,
None => continue,
};
for conflict_id in conflicts {
if conflict_id == &node.id {
continue;
}
let other = match node_map.get(conflict_id.as_str()) {
Some(n) => n,
None => continue, };
if !is_active(other) {
continue;
}
let pair = if node.id < *conflict_id {
(node.id.clone(), conflict_id.clone())
} else {
(conflict_id.clone(), node.id.clone())
};
if reported.insert(pair) {
errors.push(AgmError::with_severity(
ErrorCode::V013,
Severity::Warning,
format!(
"Conflicting active nodes co-loaded: `{}` and `{conflict_id}`",
node.id
),
ErrorLocation::full(file_name, node.span.start_line, &node.id),
));
}
}
}
errors
}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use super::*;
use crate::model::fields::{NodeStatus, NodeType, Span};
use crate::model::file::{AgmFile, Header};
use crate::model::node::Node;
fn minimal_header() -> Header {
Header {
agm: "1.0".to_owned(),
package: "test.pkg".to_owned(),
version: "0.1.0".to_owned(),
title: None,
owner: None,
imports: None,
default_load: None,
description: None,
tags: None,
status: None,
load_profiles: None,
target_runtime: None,
}
}
fn make_node(id: &str) -> Node {
Node {
id: id.to_owned(),
node_type: NodeType::Facts,
summary: "a node".to_owned(),
priority: None,
stability: None,
confidence: None,
status: None,
depends: None,
related_to: None,
replaces: None,
conflicts: None,
see_also: None,
items: None,
steps: None,
fields: None,
input: None,
output: None,
detail: None,
rationale: None,
tradeoffs: None,
resolution: None,
examples: None,
notes: None,
code: None,
code_blocks: None,
verify: None,
agent_context: None,
target: None,
execution_status: None,
executed_by: None,
executed_at: None,
execution_log: None,
retry_count: None,
parallel_groups: None,
memory: None,
scope: None,
applies_when: None,
valid_from: None,
valid_until: None,
tags: None,
aliases: None,
keywords: None,
extra_fields: BTreeMap::new(),
span: Span::new(5, 7),
}
}
#[test]
fn test_validate_compatibility_no_conflicts_returns_empty() {
let node_a = make_node("auth.a");
let node_b = make_node("auth.b");
let file = AgmFile {
header: minimal_header(),
nodes: vec![node_a, node_b],
};
let errors = validate_compatibility(&file, "test.agm");
assert!(errors.is_empty());
}
#[test]
fn test_validate_compatibility_conflicting_active_returns_v013() {
let mut node_a = make_node("auth.a");
let node_b = make_node("auth.b");
node_a.conflicts = Some(vec!["auth.b".to_owned()]);
let file = AgmFile {
header: minimal_header(),
nodes: vec![node_a, node_b],
};
let errors = validate_compatibility(&file, "test.agm");
assert!(errors.iter().any(|e| e.code == ErrorCode::V013));
}
#[test]
fn test_validate_compatibility_conflicting_one_deprecated_returns_empty() {
let mut node_a = make_node("auth.a");
let mut node_b = make_node("auth.b");
node_a.conflicts = Some(vec!["auth.b".to_owned()]);
node_b.status = Some(NodeStatus::Deprecated); let file = AgmFile {
header: minimal_header(),
nodes: vec![node_a, node_b],
};
let errors = validate_compatibility(&file, "test.agm");
assert!(!errors.iter().any(|e| e.code == ErrorCode::V013));
}
#[test]
fn test_validate_compatibility_bidirectional_reports_once() {
let mut node_a = make_node("auth.a");
let mut node_b = make_node("auth.b");
node_a.conflicts = Some(vec!["auth.b".to_owned()]);
node_b.conflicts = Some(vec!["auth.a".to_owned()]);
let file = AgmFile {
header: minimal_header(),
nodes: vec![node_a, node_b],
};
let errors = validate_compatibility(&file, "test.agm");
let v013_count = errors.iter().filter(|e| e.code == ErrorCode::V013).count();
assert_eq!(
v013_count, 1,
"Bidirectional conflict should be reported only once"
);
}
#[test]
fn test_validate_compatibility_self_conflict_skipped() {
let mut node = make_node("auth.a");
node.conflicts = Some(vec!["auth.a".to_owned()]);
let file = AgmFile {
header: minimal_header(),
nodes: vec![node],
};
let errors = validate_compatibility(&file, "test.agm");
assert!(!errors.iter().any(|e| e.code == ErrorCode::V013));
}
#[test]
fn test_validate_compatibility_conflict_with_nonexistent_skipped() {
let mut node = make_node("auth.a");
node.conflicts = Some(vec!["nonexistent.node".to_owned()]);
let file = AgmFile {
header: minimal_header(),
nodes: vec![node],
};
let errors = validate_compatibility(&file, "test.agm");
assert!(!errors.iter().any(|e| e.code == ErrorCode::V013));
}
}