use nusy_codegraph::{CodeNodeKind, PythonModuleResolver, PythonParser, ingest_python_directory};
use std::path::Path;
#[test]
fn test_python_parser_on_realistic_file() {
let source = r#"
"""Cognitive signal fusion module."""
from __future__ import annotations
import logging
from typing import Any, Dict, List, Optional
logger = logging.getLogger(__name__)
class SignalFusionBrain:
"""Fuses cognitive signals from multiple assessors."""
def __init__(self, config: Dict[str, Any]) -> None:
"""Initialize the fusion brain."""
self.config = config
self._cache: Dict[str, float] = {}
@property
def domain(self) -> str:
"""Return the cognitive domain."""
return self.config.get("domain", "general")
@classmethod
def from_config(cls, path: str) -> "SignalFusionBrain":
"""Load from config file."""
return cls({})
async def fuse(self, signals: List[Any]) -> Dict[str, float]:
"""Async signal fusion pipeline."""
result: Dict[str, float] = {}
for signal in signals:
if signal.weight > 0.0 and signal.valid:
result[signal.name] = signal.score
elif signal.fallback is not None:
result[signal.name] = signal.fallback
return result
def _compute(self, x: float, y: float) -> float:
"""Private computation helper."""
return x * y + 1.0
def standalone_helper(x: int) -> int:
"""Top-level utility function."""
return x + 1
async def async_pipeline(items: List[Any]) -> List[Any]:
"""Top-level async function."""
return [item for item in items if item is not None]
"#;
let mut parser = PythonParser::new().expect("parser init");
let path = std::path::PathBuf::from("brain/perception/signal_fusion.py");
let result = parser.parse_file(&path, source).expect("parse");
let class_nodes: Vec<_> = result
.nodes
.iter()
.filter(|n| n.kind == CodeNodeKind::PythonClass)
.collect();
assert_eq!(class_nodes.len(), 1, "one class expected");
assert_eq!(class_nodes[0].name, "SignalFusionBrain");
let methods: Vec<_> = result
.nodes
.iter()
.filter(|n| n.kind == CodeNodeKind::PythonMethod)
.collect();
assert!(
methods.len() >= 2,
"expected >= 2 PythonMethod nodes, got {}",
methods.len()
);
let props: Vec<_> = result
.nodes
.iter()
.filter(|n| n.kind == CodeNodeKind::PythonProperty)
.collect();
assert_eq!(props.len(), 1, "expected 1 PythonProperty");
assert_eq!(props[0].name, "domain");
let async_nodes: Vec<_> = result
.nodes
.iter()
.filter(|n| n.kind == CodeNodeKind::PythonAsync)
.collect();
assert!(
async_nodes.len() >= 2,
"expected >= 2 PythonAsync nodes, got {}",
async_nodes.len()
);
let funcs: Vec<_> = result
.nodes
.iter()
.filter(|n| n.kind == CodeNodeKind::PythonFunction)
.collect();
assert!(
funcs.iter().any(|f| f.name == "standalone_helper"),
"should have standalone_helper as PythonFunction"
);
for node in &result.nodes {
assert!(node.start_line.is_some(), "{} missing start_line", node.id);
assert!(node.file_path.is_some(), "{} missing file_path", node.id);
assert!(
node.byte_offset.is_some(),
"{} missing byte_offset",
node.id
);
}
for node in &result.nodes {
assert!(
!matches!(
node.kind,
CodeNodeKind::Function
| CodeNodeKind::Method
| CodeNodeKind::Class
| CodeNodeKind::Module
),
"node {} has generic kind {:?}",
node.id,
node.kind
);
}
}
#[test]
fn test_resolver_on_temp_package() {
let dir = tempfile::tempdir().expect("tempdir");
let root = dir.path();
let files = [
("brain/__init__.py", ""),
("brain/perception/__init__.py", ""),
(
"brain/perception/signal_fusion.py",
"from .utils import helper\nfrom brain.utils import top_helper",
),
("brain/perception/utils.py", "def helper(): pass"),
("brain/utils.py", "def top_helper(): pass"),
];
for (rel, content) in &files {
let path = root.join(rel);
std::fs::create_dir_all(path.parent().unwrap()).expect("mkdir");
std::fs::write(&path, content).expect("write");
}
let resolver = PythonModuleResolver::from_root(root).expect("build resolver");
assert!(resolver.knows_module("brain.perception.signal_fusion"));
assert!(resolver.knows_module("brain.perception.utils"));
assert!(resolver.knows_module("brain.utils"));
let resolved = resolver
.resolve_import("brain.perception.utils", None)
.expect("should resolve brain.perception.utils");
assert!(
resolved.ends_with("brain/perception/utils.py"),
"resolved to wrong path: {resolved:?}"
);
let from_file = Path::new("brain/perception/signal_fusion.py");
let resolved_rel = resolver.resolve_import(".utils", Some(from_file));
assert!(
resolved_rel.is_some(),
"should resolve relative .utils import"
);
}
#[test]
fn test_brain_v13_full_ingest() {
let manifest_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let workspace_root = manifest_dir
.parent()
.and_then(|p| p.parent())
.expect("workspace root");
let brain_v13 = workspace_root.join("_archive/brain-v13");
if !brain_v13.is_dir() {
eprintln!("SKIP: _archive/brain-v13 not present");
return;
}
let result =
ingest_python_directory(&brain_v13).expect("ingest_python_directory should succeed");
let total_files = result.parse_results.len() + result.errors.len();
let error_count = result.errors.len();
let error_pct = if total_files > 0 {
(error_count as f64 / total_files as f64) * 100.0
} else {
0.0
};
eprintln!(
"brain-v13 ingest: {} nodes, {} edges, {} files parsed, {} errors ({:.1}%)",
result.nodes.len(),
result.edges.len(),
result.parse_results.len(),
error_count,
error_pct,
);
assert!(
result.nodes.len() >= 1_000,
"Expected >= 1,000 Python nodes, got {}",
result.nodes.len()
);
assert!(
error_pct < 5.0,
"Parse failure rate {:.1}% exceeds 5% limit ({} errors out of {} files)",
error_pct,
error_count,
total_files,
);
let python_class_count = result
.nodes
.iter()
.filter(|n| n.kind == CodeNodeKind::PythonClass)
.count();
let python_func_count = result
.nodes
.iter()
.filter(|n| n.kind == CodeNodeKind::PythonFunction)
.count();
let python_method_count = result
.nodes
.iter()
.filter(|n| n.kind == CodeNodeKind::PythonMethod)
.count();
eprintln!(
" PythonClass: {}, PythonFunction: {}, PythonMethod: {}",
python_class_count, python_func_count, python_method_count
);
assert!(python_class_count > 0, "should have PythonClass nodes");
assert!(
python_func_count + python_method_count > 0,
"should have PythonFunction or PythonMethod nodes"
);
let nodes_with_position = result
.nodes
.iter()
.filter(|n| n.start_line.is_some())
.count();
let position_coverage = nodes_with_position as f64 / result.nodes.len() as f64;
eprintln!(
" Position coverage: {:.1}% ({}/{} nodes)",
position_coverage * 100.0,
nodes_with_position,
result.nodes.len()
);
assert!(
position_coverage >= 0.8,
"Expected >= 80% nodes to have position metadata, got {:.1}%",
position_coverage * 100.0,
);
eprintln!("{}", result.summary());
}