use super::helpers::yield_alias;
use std::collections::{HashMap, VecDeque};
use petgraph::graph::NodeIndex;
use petgraph::Direction;
use super::super::ast::YieldItem;
use super::super::result::ResultRow;
use crate::datatypes::values::Value;
use crate::graph::dir_graph::DirGraph;
use crate::graph::schema::InternedKey;
use crate::graph::storage::GraphRead;
const PROC: &str = "affected_tests";
pub(super) fn execute_affected_tests(
graph: &DirGraph,
params: &HashMap<String, Value>,
yield_items: &[YieldItem],
) -> Result<Vec<ResultRow>, String> {
let files = require_files_param(params)?;
if files.is_empty() {
return Ok(Vec::new());
}
let max_depth = params
.get("max_depth")
.and_then(|v| match v {
Value::Int64(n) => Some((*n).max(0) as usize),
Value::Float64(n) => Some((*n).max(0.0) as usize),
_ => None,
})
.unwrap_or(10);
let test_file_var = yield_alias(yield_items, "test_file");
let depth_var = yield_alias(yield_items, "depth");
if test_file_var.is_none() && depth_var.is_none() {
return Err(format!(
"CALL {PROC}: must YIELD at least one of 'test_file', 'depth'."
));
}
let file_idx = match graph.type_indices.get("File") {
Some(idx) => idx,
None => return Ok(Vec::new()), };
let mut path_to_idx: HashMap<String, NodeIndex> = HashMap::new();
for nidx in file_idx.iter() {
if let Some(node) = graph.graph.node_weight(nidx) {
if let Value::String(s) = node.id().as_ref() {
path_to_idx.insert(s.clone(), nidx);
}
}
}
let imports_key = InternedKey::from_str("IMPORTS");
let mut depth_of: HashMap<NodeIndex, usize> = HashMap::new();
let mut queue: VecDeque<(NodeIndex, usize)> = VecDeque::new();
for seed in &files {
if let Some(&nidx) = path_to_idx.get(seed) {
if depth_of.insert(nidx, 0).is_none() {
queue.push_back((nidx, 0));
}
}
}
while let Some((nidx, d)) = queue.pop_front() {
if d >= max_depth {
continue;
}
for er in graph.graph.edges_directed(nidx, Direction::Incoming) {
if er.weight().connection_type != imports_key {
continue;
}
let importer = er.source();
if depth_of.contains_key(&importer) {
continue;
}
depth_of.insert(importer, d + 1);
queue.push_back((importer, d + 1));
}
}
let mut rows: Vec<(String, ResultRow)> = Vec::new();
for (nidx, depth) in &depth_of {
if *depth == 0 {
continue;
}
let node = match graph.graph.node_weight(*nidx) {
Some(n) => n,
None => continue,
};
let is_test = matches!(
node.get_property("is_test").as_deref(),
Some(Value::Boolean(true))
);
if !is_test {
continue;
}
let path = match node.id().as_ref() {
Value::String(s) => s.clone(),
_ => continue,
};
let mut row = ResultRow::new();
if let Some(name) = &test_file_var {
row.projected
.insert(name.clone(), Value::String(path.clone()));
}
if let Some(name) = &depth_var {
row.projected
.insert(name.clone(), Value::Int64(*depth as i64));
}
rows.push((path, row));
}
rows.sort_by(|a, b| a.0.cmp(&b.0));
Ok(rows.into_iter().map(|(_, row)| row).collect())
}
fn require_files_param(params: &HashMap<String, Value>) -> Result<Vec<String>, String> {
match params.get("files") {
Some(Value::List(items)) => Ok(items
.iter()
.filter_map(|v| match v {
Value::String(s) => Some(s.clone()),
_ => None,
})
.collect()),
Some(Value::String(s)) => {
if s.starts_with('[') {
let items = super::helpers::parse_list_value(&Value::String(s.clone()));
Ok(items
.into_iter()
.filter_map(|v| match v {
Value::String(s) => Some(s),
_ => None,
})
.collect())
} else {
Ok(vec![s.clone()])
}
}
Some(other) => Err(format!(
"CALL {PROC}: parameter 'files' must be a list of file paths (got {other:?})."
)),
None => Err(format!(
"CALL {PROC}: missing required parameter 'files'. \
Use map syntax — e.g. CALL {PROC}({{files: ['src/foo.py'], max_depth: 5}})."
)),
}
}