feat: add E2E test suite with nightly CI, fix dashboard Dockerfile #52

Merged
sharang merged 3 commits from feat/e2e-tests into main 2026-03-30 10:04:07 +00:00
Showing only changes of commit 08a1ee2f00 - Show all commits

View File

@@ -15,6 +15,30 @@ use crate::parsers::registry::ParserRegistry;
use super::community::detect_communities;
use super::impact::ImpactAnalyzer;
/// Walk up the qualified-name hierarchy to find the closest ancestor
/// that exists in the node map.
///
/// For `"src/main.rs::config::load"` this tries:
/// 1. `"src/main.rs::config"` (trim last `::` segment)
/// 2. `"src/main.rs"` (trim again)
///
/// Returns the first match found, or `None` if the node is a root.
Review

[high] Off-by-one error in find_parent_qname when handling qualified names without '::'

The find_parent_qname function incorrectly handles qualified names that do not contain any '::' separator. When current.rfind("::") returns None, it should immediately return None because there's no parent. However, the current implementation enters an infinite loop because current is never updated to an empty string, and current.rfind("::") will always return None on subsequent iterations.

Suggested fix: Fix the loop condition to properly handle the case where there are no '::' separators. The function should return None immediately when rfind("::") returns None.

*Scanner: code-review/logic | *

**[high] Off-by-one error in `find_parent_qname` when handling qualified names without '::'** The `find_parent_qname` function incorrectly handles qualified names that do not contain any '::' separator. When `current.rfind("::")` returns `None`, it should immediately return `None` because there's no parent. However, the current implementation enters an infinite loop because `current` is never updated to an empty string, and `current.rfind("::")` will always return `None` on subsequent iterations. Suggested fix: Fix the loop condition to properly handle the case where there are no '::' separators. The function should return `None` immediately when `rfind("::")` returns `None`. *Scanner: code-review/logic | * <!-- compliance-fp:4f0c8fed46fedb2cd694d79018ff461ca30882bddb2a67889b803bf9400395b4 -->
fn find_parent_qname(qname: &str, node_map: &HashMap<String, NodeIndex>) -> Option<String> {
let mut current = qname.to_string();
Review

[high] Off-by-one error in find_parent_qname when handling empty segments

The find_parent_qname function can produce incorrect results when dealing with qualified names that end with '::'. For example, given a node map containing "src/main.rs::config::" (with trailing '::'), calling find_parent_qname("src/main.rs::config::", &node_map) will incorrectly return Some("src/main.rs::") instead of None. This happens because rfind("::") finds the last '::' but truncating it leaves an empty string which may match a node if one exists with an empty string key.

Suggested fix: Add a check after current.truncate(pos) to ensure that current is not empty before checking node_map.contains_key(&current). If current becomes empty, immediately return None since there's no valid parent.

*Scanner: code-review/logic | *

**[high] Off-by-one error in `find_parent_qname` when handling empty segments** The `find_parent_qname` function can produce incorrect results when dealing with qualified names that end with '::'. For example, given a node map containing "src/main.rs::config::" (with trailing '::'), calling `find_parent_qname("src/main.rs::config::", &node_map)` will incorrectly return `Some("src/main.rs::")` instead of `None`. This happens because `rfind("::")` finds the last '::' but truncating it leaves an empty string which may match a node if one exists with an empty string key. Suggested fix: Add a check after `current.truncate(pos)` to ensure that `current` is not empty before checking `node_map.contains_key(&current)`. If `current` becomes empty, immediately return `None` since there's no valid parent. *Scanner: code-review/logic | * <!-- compliance-fp:2eb26751c2b51b0992ca797d2171add9abeef5e3b7ec00641b9438058c779075 -->
loop {
// Try stripping the last "::" segment
if let Some(pos) = current.rfind("::") {
current.truncate(pos);
if node_map.contains_key(&current) {
return Some(current);
}
continue;
}
// No more "::" — this is a top-level node (file), no parent
return None;
}
}
/// The main graph engine that builds and manages code knowledge graphs
pub struct GraphEngine {
parser_registry: ParserRegistry,
@@ -89,7 +113,12 @@ impl GraphEngine {
Ok((code_graph, build_run))
}
/// Build petgraph from parsed output, resolving edges to node indices
/// Build petgraph from parsed output, resolving edges to node indices.
///
/// After resolving the explicit edges from parsers, we synthesise
/// `Contains` edges so that every node is reachable from its parent
/// file or module. This eliminates disconnected "islands" that
/// otherwise appear when files share no direct call/import edges.
fn build_petgraph(&self, parse_output: ParseOutput) -> Result<CodeGraph, CoreError> {
let mut graph = DiGraph::new();
let mut node_map: HashMap<String, NodeIndex> = HashMap::new();
@@ -102,15 +131,13 @@ impl GraphEngine {
node_map.insert(node.qualified_name.clone(), idx);
}
// Resolve and add edges — rewrite target to the resolved qualified name
// so the persisted edge references match node qualified_names.
// Resolve and add explicit edges from parsers
let mut resolved_edges = Vec::new();
for mut edge in parse_output.edges {
let source_idx = node_map.get(&edge.source);
let resolved = self.resolve_edge_target(&edge.target, &node_map);
if let (Some(&src), Some(tgt)) = (source_idx, resolved) {
// Update target to the resolved qualified name
let resolved_name = node_map
.iter()
.find(|(_, &idx)| idx == tgt)
@@ -121,7 +148,48 @@ impl GraphEngine {
graph.add_edge(src, tgt, edge.kind.clone());
Review

[medium] Deeply nested control flow in Contains edge synthesis

The Contains edge synthesis logic in build_petgraph has deeply nested control flow with multiple nested conditions and loops. The code walks up the qualified name hierarchy while checking for existing edges, which creates a complex flow that's hard to reason about and maintain.

Suggested fix: Flatten the nested structure by extracting the parent lookup logic into a separate function and using early returns instead of deep nesting. This would simplify the main control flow and make it easier to verify correctness.

*Scanner: code-review/complexity | *

**[medium] Deeply nested control flow in Contains edge synthesis** The Contains edge synthesis logic in `build_petgraph` has deeply nested control flow with multiple nested conditions and loops. The code walks up the qualified name hierarchy while checking for existing edges, which creates a complex flow that's hard to reason about and maintain. Suggested fix: Flatten the nested structure by extracting the parent lookup logic into a separate function and using early returns instead of deep nesting. This would simplify the main control flow and make it easier to verify correctness. *Scanner: code-review/complexity | * <!-- compliance-fp:ebb388b8c9a39790be19560562289778152605b8e0efa7c4437eb42fe14eb898 -->
Review

[medium] Deeply nested control flow in Contains edge synthesis

The Contains edge synthesis logic in build_petgraph has deeply nested control flow with multiple nested conditions and loops. The code walks up the qualified name hierarchy while checking for existing edges, which creates a complex flow that's hard to reason about and could introduce bugs during maintenance.

Suggested fix: Flatten the nested structure by extracting the parent lookup logic into a separate function and using early returns to avoid deep nesting. Consider using a more functional approach with iterators to simplify the edge creation logic.

*Scanner: code-review/complexity | *

**[medium] Deeply nested control flow in Contains edge synthesis** The Contains edge synthesis logic in `build_petgraph` has deeply nested control flow with multiple nested conditions and loops. The code walks up the qualified name hierarchy while checking for existing edges, which creates a complex flow that's hard to reason about and could introduce bugs during maintenance. Suggested fix: Flatten the nested structure by extracting the parent lookup logic into a separate function and using early returns to avoid deep nesting. Consider using a more functional approach with iterators to simplify the edge creation logic. *Scanner: code-review/complexity | * <!-- compliance-fp:ebb388b8c9a39790be19560562289778152605b8e0efa7c4437eb42fe14eb898 -->
resolved_edges.push(edge);
}
// Skip unresolved edges (cross-file, external deps) — conservative approach
}
Review

[medium] Potential panic in Contains edge synthesis due to unwrapping

In the build_petgraph function, there are several instances of .unwrap_or("")) and .cloned().unwrap_or_default() which can lead to panics or incorrect behavior if the nodes collection is empty or if expected fields are missing. Specifically, when accessing nodes.first() and then calling .as_str() on potentially missing fields, this could cause runtime issues if the parsing output structure isn't consistent.

Suggested fix: Use proper error propagation instead of unwrapping. Replace unwrap_or("") with explicit error handling or default values that don't risk panicking. Also consider adding assertions or early returns to validate that required fields exist before proceeding.

*Scanner: code-review/convention | *

**[medium] Potential panic in Contains edge synthesis due to unwrapping** In the `build_petgraph` function, there are several instances of `.unwrap_or(""))` and `.cloned().unwrap_or_default()` which can lead to panics or incorrect behavior if the nodes collection is empty or if expected fields are missing. Specifically, when accessing `nodes.first()` and then calling `.as_str()` on potentially missing fields, this could cause runtime issues if the parsing output structure isn't consistent. Suggested fix: Use proper error propagation instead of unwrapping. Replace `unwrap_or("")` with explicit error handling or default values that don't risk panicking. Also consider adding assertions or early returns to validate that required fields exist before proceeding. *Scanner: code-review/convention | * <!-- compliance-fp:8fef57ee461770d6e5a6f05027d6682a43b24a46e96ec186f1217b0521ac7b2c -->
// Synthesise Contains edges: connect each node to its closest
// parent in the qualified-name hierarchy.
//
// For "src/main.rs::config::load", the parent chain is:
// "src/main.rs::config" → "src/main.rs"
//
// We walk up the qualified name (splitting on "::") and link to
// the first ancestor that exists in the node map.
let repo_id = nodes.first().map(|n| n.repo_id.as_str()).unwrap_or("");
let build_id = nodes
.first()
.map(|n| n.graph_build_id.as_str())
.unwrap_or("");
Review

[medium] Potential panic in Contains edge synthesis

In the build_petgraph function, when synthesizing Contains edges, the code directly indexes into node_map with node_map[qname] and node_map[&parent_qname] without checking if keys exist. If find_parent_qname returns a parent that doesn't exist in the node_map, this will panic during runtime.

Suggested fix: Use .get() instead of indexing with [] to safely access values from node_map, or ensure that find_parent_qname always returns valid keys that exist in the node_map.

*Scanner: code-review/convention | *

**[medium] Potential panic in Contains edge synthesis** In the `build_petgraph` function, when synthesizing Contains edges, the code directly indexes into `node_map` with `node_map[qname]` and `node_map[&parent_qname]` without checking if keys exist. If `find_parent_qname` returns a parent that doesn't exist in the node_map, this will panic during runtime. Suggested fix: Use `.get()` instead of indexing with `[]` to safely access values from node_map, or ensure that `find_parent_qname` always returns valid keys that exist in the node_map. *Scanner: code-review/convention | * <!-- compliance-fp:8dd7e0d4858858437bc1d8ee1aab9a0dc4707d18346582b2b885e696a5588737 -->
let qualified_names: Vec<String> = nodes.iter().map(|n| n.qualified_name.clone()).collect();
let file_paths: HashMap<String, String> = nodes
.iter()
.map(|n| (n.qualified_name.clone(), n.file_path.clone()))
.collect();
for qname in &qualified_names {
if let Some(parent_qname) = find_parent_qname(qname, &node_map) {
let child_idx = node_map[qname];
let parent_idx = node_map[&parent_qname];
Review

[medium] Incorrect edge duplication prevention in Contains edge synthesis

In the Contains edge synthesis logic, the check !graph.contains_edge(parent_idx, child_idx) only prevents adding duplicate edges based on the indices. However, it doesn't prevent adding multiple edges with different metadata (like file paths) between the same pair of nodes. This could lead to inconsistent graph state where the same logical relationship appears multiple times with different metadata.

Suggested fix: Consider using a more robust deduplication mechanism that also compares edge metadata, or ensure that all synthesized edges have consistent metadata across the graph.

*Scanner: code-review/logic | *

**[medium] Incorrect edge duplication prevention in Contains edge synthesis** In the Contains edge synthesis logic, the check `!graph.contains_edge(parent_idx, child_idx)` only prevents adding duplicate edges based on the indices. However, it doesn't prevent adding multiple edges with different metadata (like file paths) between the same pair of nodes. This could lead to inconsistent graph state where the same logical relationship appears multiple times with different metadata. Suggested fix: Consider using a more robust deduplication mechanism that also compares edge metadata, or ensure that all synthesized edges have consistent metadata across the graph. *Scanner: code-review/logic | * <!-- compliance-fp:0a07b5c5e60542e191d085eb18dc095e44d9bb6e67d6ba8198ed436e707b610d -->
Review

[low] Unnecessary unwrap_or_default in Contains edge creation

In the Contains edge synthesis logic, file_paths.get(qname).cloned().unwrap_or_default() can silently provide empty strings when file paths aren't found. This might mask missing data or incorrect assumptions about node structure, especially since the code already checks for existence of nodes via node_map.

Suggested fix: Consider adding a debug assertion or logging when a file path is missing, rather than silently defaulting to an empty string.

*Scanner: code-review/convention | *

**[low] Unnecessary unwrap_or_default in Contains edge creation** In the Contains edge synthesis logic, `file_paths.get(qname).cloned().unwrap_or_default()` can silently provide empty strings when file paths aren't found. This might mask missing data or incorrect assumptions about node structure, especially since the code already checks for existence of nodes via `node_map`. Suggested fix: Consider adding a debug assertion or logging when a file path is missing, rather than silently defaulting to an empty string. *Scanner: code-review/convention | * <!-- compliance-fp:7b8b7aeab65def86d8a4928c52add29e13f3dc20b8300ef4ba84c3239eabda40 -->
// Avoid duplicate edges
if !graph.contains_edge(parent_idx, child_idx) {
graph.add_edge(parent_idx, child_idx, CodeEdgeKind::Contains);
resolved_edges.push(CodeEdge {
id: None,
repo_id: repo_id.to_string(),
graph_build_id: build_id.to_string(),
source: parent_qname,
target: qname.clone(),
kind: CodeEdgeKind::Contains,
file_path: file_paths.get(qname).cloned().unwrap_or_default(),
line_number: None,
});
}
}
}
Ok(CodeGraph {
@@ -132,33 +200,62 @@ impl GraphEngine {
})
Review

[medium] Complex boolean expression in edge resolution

The resolve_edge_target function contains a complex boolean expression that checks multiple conditions for edge resolution. The logic involves multiple string pattern matching operations with various prefixes and suffixes, making it difficult to follow and prone to subtle bugs when modifying the resolution strategies.

Suggested fix: Break down the complex conditionals into separate helper functions with clear names that describe their specific resolution strategy. This would make the intent clearer and reduce the chance of logical errors when adding new resolution types.

*Scanner: code-review/complexity | *

**[medium] Complex boolean expression in edge resolution** The `resolve_edge_target` function contains a complex boolean expression that checks multiple conditions for edge resolution. The logic involves multiple string pattern matching operations with various prefixes and suffixes, making it difficult to follow and prone to subtle bugs when modifying the resolution strategies. Suggested fix: Break down the complex conditionals into separate helper functions with clear names that describe their specific resolution strategy. This would make the intent clearer and reduce the chance of logical errors when adding new resolution types. *Scanner: code-review/complexity | * <!-- compliance-fp:1df9276a3941f76ed4bfb6e0944d204c3e52011243f491b1b22c00b4e0e75c90 -->
Review

[medium] Complex boolean expression in edge resolution

The resolve_edge_target function contains a complex boolean expression that checks multiple conditions for edge resolution. The logic involves multiple string pattern matching operations with various prefixes and suffixes, making it difficult to follow and prone to subtle bugs when modifying the resolution strategies.

Suggested fix: Break down the complex conditionals into separate helper functions or early returns to improve readability and reduce the chance of logical errors when adding new resolution strategies.

*Scanner: code-review/complexity | *

**[medium] Complex boolean expression in edge resolution** The `resolve_edge_target` function contains a complex boolean expression that checks multiple conditions for edge resolution. The logic involves multiple string pattern matching operations with various prefixes and suffixes, making it difficult to follow and prone to subtle bugs when modifying the resolution strategies. Suggested fix: Break down the complex conditionals into separate helper functions or early returns to improve readability and reduce the chance of logical errors when adding new resolution strategies. *Scanner: code-review/complexity | * <!-- compliance-fp:1df9276a3941f76ed4bfb6e0944d204c3e52011243f491b1b22c00b4e0e75c90 -->
}
/// Try to resolve an edge target to a known node
/// Try to resolve an edge target to a known node.
///
/// Resolution strategies (in order):
/// 1. Direct qualified-name match
/// 2. Suffix match: "foo" matches "src/main.rs::mod::foo"
/// 3. Module-path match: "config::load" matches "src/config.rs::load"
/// 4. Self-method: "self.method" matches "::method"
fn resolve_edge_target(
&self,
target: &str,
node_map: &HashMap<String, NodeIndex>,
) -> Option<NodeIndex> {
// Direct match
// 1. Direct match
if let Some(idx) = node_map.get(target) {
return Some(*idx);
}
// Try matching just the function/type name (intra-file resolution)
// 2. Suffix match: "foo" → "path/file.rs::foo"
Review

[medium] Inconsistent error handling in edge resolution

The resolve_edge_target function uses multiple resolution strategies but doesn't consistently handle cases where resolution fails. Specifically, the module-path matching strategy has a fallback that uses unwrap_or(target) which could silently ignore resolution failures. Additionally, the function returns None for failed resolutions but the logic around stripped and segments processing could lead to missed matches without clear indication.

Suggested fix: Replace unwrap_or(target) with explicit error handling or logging to ensure resolution failures are properly tracked. Consider using a more robust approach for handling the module-path matching that doesn't silently fall back to the original target.

*Scanner: code-review/convention | *

**[medium] Inconsistent error handling in edge resolution** The `resolve_edge_target` function uses multiple resolution strategies but doesn't consistently handle cases where resolution fails. Specifically, the module-path matching strategy has a fallback that uses `unwrap_or(target)` which could silently ignore resolution failures. Additionally, the function returns `None` for failed resolutions but the logic around `stripped` and `segments` processing could lead to missed matches without clear indication. Suggested fix: Replace `unwrap_or(target)` with explicit error handling or logging to ensure resolution failures are properly tracked. Consider using a more robust approach for handling the module-path matching that doesn't silently fall back to the original target. *Scanner: code-review/convention | * <!-- compliance-fp:db291b9f59cd056cf731606233f9adf47c61a007c50358bf7c59a62b24d6ab03 -->
let suffix_pattern = format!("::{target}");
let dot_pattern = format!(".{target}");
for (qualified, idx) in node_map {
// Match "foo" to "path/file.rs::foo" or "path/file.rs::Type::foo"
if qualified.ends_with(&format!("::{target}"))
|| qualified.ends_with(&format!(".{target}"))
{
if qualified.ends_with(&suffix_pattern) || qualified.ends_with(&dot_pattern) {
return Some(*idx);
Review

[medium] Inconsistent error handling in edge resolution

The resolve_edge_target function uses multiple resolution strategies but doesn't consistently handle cases where resolution fails. Specifically, the module-path matching strategy has a fallback that uses unwrap_or(target) which could lead to silent failures or incorrect behavior when the target string contains '::' but doesn't match any pattern.

Suggested fix: Replace unwrap_or(target) with explicit error handling or logging to make it clear when resolution fails. Consider returning an error or using a more robust fallback mechanism.

*Scanner: code-review/convention | *

**[medium] Inconsistent error handling in edge resolution** The `resolve_edge_target` function uses multiple resolution strategies but doesn't consistently handle cases where resolution fails. Specifically, the module-path matching strategy has a fallback that uses `unwrap_or(target)` which could lead to silent failures or incorrect behavior when the target string contains '::' but doesn't match any pattern. Suggested fix: Replace `unwrap_or(target)` with explicit error handling or logging to make it clear when resolution fails. Consider returning an error or using a more robust fallback mechanism. *Scanner: code-review/convention | * <!-- compliance-fp:fe0ea8db4d5285f77a7d0255e0c2c805e208c7328ec273c297250ab58f56868c -->
}
}
// Try matching method calls like "self.method" -> look for "::method"
// 3. Module-path match: "config::load" → try matching the last N
// segments of the target against node qualified names.
// This handles cross-file calls like `crate::config::load` or
// `super::handlers::process` where the prefix differs.
if target.contains("::") {
// Strip common Rust path prefixes
let stripped = target
.strip_prefix("crate::")
.or_else(|| target.strip_prefix("super::"))
.or_else(|| target.strip_prefix("self::"))
.unwrap_or(target);
let segments: Vec<&str> = stripped.split("::").collect();
// Try matching progressively shorter suffixes
for start in 0..segments.len() {
let suffix = segments[start..].join("::");
let pattern = format!("::{suffix}");
for (qualified, idx) in node_map {
if qualified.ends_with(&pattern) {
return Some(*idx);
}
}
}
}
// 4. Self-method: "self.method" → "::method"
if let Some(method_name) = target.strip_prefix("self.") {
let pattern = format!("::{method_name}");
for (qualified, idx) in node_map {
if qualified.ends_with(&format!("::{method_name}"))
|| qualified.ends_with(&format!(".{method_name}"))
{
if qualified.ends_with(&pattern) {
return Some(*idx);
}
}
@@ -353,4 +450,83 @@ mod tests {
assert!(code_graph.node_map.contains_key("a::c"));
assert!(code_graph.node_map.contains_key("a::d"));
}
#[test]
fn test_contains_edges_synthesised() {
let engine = GraphEngine::new(1000);
let mut output = ParseOutput::default();
// File → Module → Function hierarchy
output.nodes.push(make_node("src/main.rs"));
output.nodes.push(make_node("src/main.rs::config"));
output.nodes.push(make_node("src/main.rs::config::load"));
let code_graph = engine.build_petgraph(output).unwrap();
Review

[low] Test assertion relies on specific ordering of edges

The test test_contains_edges_synthesised checks for the presence of Contains edges by collecting all edges and asserting their count, but it doesn't verify that the specific parent-child relationships are correctly formed. The assertion only checks that the sources contain the expected values, but doesn't ensure that the edges actually connect the correct nodes in the graph structure.

Suggested fix: Enhance the test to also verify that the actual graph connections match the expected parent-child relationships by checking both source and target of the edges, not just the sources.

*Scanner: code-review/convention | *

**[low] Test assertion relies on specific ordering of edges** The test `test_contains_edges_synthesised` checks for the presence of Contains edges by collecting all edges and asserting their count, but it doesn't verify that the specific parent-child relationships are correctly formed. The assertion only checks that the sources contain the expected values, but doesn't ensure that the edges actually connect the correct nodes in the graph structure. Suggested fix: Enhance the test to also verify that the actual graph connections match the expected parent-child relationships by checking both source and target of the edges, not just the sources. *Scanner: code-review/convention | * <!-- compliance-fp:dc29a0cbe654ab4e6cda772de23ffc1bc46b39fd440b495dd484d974570e2030 -->
// Should have 2 Contains edges:
// src/main.rs → src/main.rs::config
// src/main.rs::config → src/main.rs::config::load
let contains_edges: Vec<_> = code_graph
.edges
.iter()
.filter(|e| matches!(e.kind, CodeEdgeKind::Contains))
.collect();
assert_eq!(contains_edges.len(), 2, "expected 2 Contains edges");
let sources: Vec<&str> = contains_edges.iter().map(|e| e.source.as_str()).collect();
assert!(sources.contains(&"src/main.rs"));
assert!(sources.contains(&"src/main.rs::config"));
}
#[test]
fn test_contains_edges_no_duplicates_with_existing_edges() {
let engine = GraphEngine::new(1000);
let mut output = ParseOutput::default();
output.nodes.push(make_node("src/main.rs"));
output.nodes.push(make_node("src/main.rs::foo"));
// Explicit Calls edge (foo calls itself? just for testing)
output.edges.push(CodeEdge {
id: None,
repo_id: "test".to_string(),
graph_build_id: "build1".to_string(),
source: "src/main.rs::foo".to_string(),
target: "src/main.rs::foo".to_string(),
kind: CodeEdgeKind::Calls,
file_path: "src/main.rs".to_string(),
line_number: Some(1),
});
let code_graph = engine.build_petgraph(output).unwrap();
// 1 Calls + 1 Contains = 2 edges total
assert_eq!(code_graph.edges.len(), 2);
}
#[test]
fn test_cross_file_resolution_with_module_path() {
let engine = GraphEngine::new(1000);
let node_map = build_test_node_map(&["src/config.rs::load_config", "src/main.rs::main"]);
// "crate::config::load_config" should resolve to "src/config.rs::load_config"
let result = engine.resolve_edge_target("crate::config::load_config", &node_map);
assert!(result.is_some(), "cross-file crate:: path should resolve");
}
#[test]
fn test_find_parent_qname() {
let node_map = build_test_node_map(&[
"src/main.rs",
"src/main.rs::config",
"src/main.rs::config::load",
]);
assert_eq!(
find_parent_qname("src/main.rs::config::load", &node_map),
Some("src/main.rs::config".to_string())
);
assert_eq!(
find_parent_qname("src/main.rs::config", &node_map),
Some("src/main.rs".to_string())
);
assert_eq!(find_parent_qname("src/main.rs", &node_map), None);
}
}