1124 lines
33 KiB
Rust
1124 lines
33 KiB
Rust
use super::*;
|
|
use std::{
|
|
thread,
|
|
time::{Duration, Instant},
|
|
};
|
|
use tempfile::tempdir;
|
|
|
|
#[test]
|
|
#[ignore = "requires clangd on PATH; run with cargo test -p cex-core -- --ignored"]
|
|
fn clangd_cpp23_cross_file_navigation_and_cache_location() {
|
|
let temp = tempdir().unwrap();
|
|
let root = temp.path().join("project with spaces");
|
|
let build = root.join("build");
|
|
fs::create_dir_all(build.join("meson-info")).unwrap();
|
|
fs::write(build.join("meson-info/intro-projectinfo.json"), "{}").unwrap();
|
|
let header =
|
|
"template <typename T>\nconcept Addable = requires(T a) { a + a; };\nint add(int value);\n";
|
|
let lib = "#include \"lib.hpp\"\nint add(int value) { return value + 1; }\n";
|
|
let main = "#include \"lib.hpp\"\nstatic_assert(Addable<int>);\nconstexpr int cpp23() { if consteval { return 1; } else { return 2; } }\nint main() { return add(41); }\n";
|
|
fs::write(root.join("lib.hpp"), header).unwrap();
|
|
fs::write(root.join("lib.cpp"), lib).unwrap();
|
|
fs::write(root.join("main.cpp"), main).unwrap();
|
|
let database = json!(["lib.cpp", "main.cpp"].map(|name| json!({"directory": root, "file": root.join(name), "arguments": ["clang++", "-std=c++23", "-c", root.join(name)]})));
|
|
let original_database = serde_json::to_string(&database).unwrap();
|
|
fs::write(build.join("compile_commands.json"), &original_database).unwrap();
|
|
let workspace = crate::open(&root, Some(&build), false).unwrap();
|
|
let server = Clangd::start(workspace.clone()).unwrap();
|
|
let pos = Position {
|
|
line: 2,
|
|
character: 5,
|
|
};
|
|
let hover = server.query("lib.hpp", pos, Query::Hover).unwrap();
|
|
assert!(hover.hover.contains("add"), "{}", hover.hover);
|
|
let deadline = Instant::now() + Duration::from_secs(15);
|
|
loop {
|
|
let refs = server.query("lib.hpp", pos, Query::References).unwrap();
|
|
if refs.locations.iter().any(|l| l.path == "main.cpp") {
|
|
break;
|
|
}
|
|
assert!(
|
|
Instant::now() < deadline,
|
|
"No main.cpp reference: {:?}",
|
|
server.status()
|
|
);
|
|
thread::sleep(Duration::from_millis(100));
|
|
}
|
|
let definition = server
|
|
.query(
|
|
"main.cpp",
|
|
Position {
|
|
line: 3,
|
|
character: 21,
|
|
},
|
|
Query::Definition,
|
|
)
|
|
.unwrap();
|
|
assert!(
|
|
definition.locations.iter().any(|l| l.path == "lib.cpp"),
|
|
"{definition:?}"
|
|
);
|
|
let callers = server.query("lib.hpp", pos, Query::Callers).unwrap();
|
|
assert!(
|
|
callers
|
|
.locations
|
|
.iter()
|
|
.any(|l| l.path == "main.cpp" && l.label.contains("main")),
|
|
"{callers:?}"
|
|
);
|
|
let concept = server
|
|
.query(
|
|
"lib.hpp",
|
|
Position {
|
|
line: 1,
|
|
character: 10,
|
|
},
|
|
Query::References,
|
|
)
|
|
.unwrap();
|
|
assert!(
|
|
concept.locations.iter().any(|l| l.path == "main.cpp"),
|
|
"{concept:?}"
|
|
);
|
|
assert!(server.read_target("/etc/passwd").is_err());
|
|
let context = crate::explanation::prepare(
|
|
&workspace,
|
|
server.as_ref(),
|
|
"lib.hpp",
|
|
pos,
|
|
None,
|
|
"Explain callers",
|
|
&tokio_util::sync::CancellationToken::new(),
|
|
)
|
|
.unwrap();
|
|
assert!(
|
|
context
|
|
.bundle
|
|
.sources
|
|
.iter()
|
|
.any(|s| s.path == "main.cpp" && s.enclosing == "main" && s.code.contains("add(41)")),
|
|
"{context:?}"
|
|
);
|
|
assert!(
|
|
context
|
|
.bundle
|
|
.sources
|
|
.iter()
|
|
.any(|s| s.path == "lib.cpp" && s.code.contains("value + 1")),
|
|
"{context:?}"
|
|
);
|
|
let dependencies = server
|
|
.query(
|
|
"main.cpp",
|
|
Position {
|
|
line: 3,
|
|
character: 5,
|
|
},
|
|
Query::Callees,
|
|
)
|
|
.unwrap();
|
|
assert!(
|
|
dependencies
|
|
.locations
|
|
.iter()
|
|
.any(|l| l.label.contains("add")),
|
|
"{dependencies:?}"
|
|
);
|
|
let selected_function = crate::explanation::prepare(
|
|
&workspace,
|
|
server.as_ref(),
|
|
"main.cpp",
|
|
Position {
|
|
line: 3,
|
|
character: 0,
|
|
},
|
|
Some(Range {
|
|
start: Position {
|
|
line: 3,
|
|
character: 0,
|
|
},
|
|
end: Position {
|
|
line: 3,
|
|
character: 28,
|
|
},
|
|
}),
|
|
"",
|
|
&tokio_util::sync::CancellationToken::new(),
|
|
)
|
|
.unwrap();
|
|
assert!(
|
|
!selected_function.available_callees.is_empty(),
|
|
"{selected_function:?}"
|
|
);
|
|
let expanded = crate::explanation::expand(
|
|
&selected_function,
|
|
server.as_ref(),
|
|
&selected_function
|
|
.available_callees
|
|
.iter()
|
|
.map(|c| c.id.clone())
|
|
.collect::<Vec<_>>(),
|
|
2,
|
|
"",
|
|
&tokio_util::sync::CancellationToken::new(),
|
|
)
|
|
.unwrap();
|
|
assert!(
|
|
expanded
|
|
.bundle
|
|
.sources
|
|
.iter()
|
|
.any(|s| s.path == "lib.cpp" && s.code.contains("value + 1")),
|
|
"{expanded:?}"
|
|
);
|
|
server.stop();
|
|
assert_eq!(server.status().phase, "stopped");
|
|
assert_eq!(
|
|
fs::read_to_string(build.join("compile_commands.json")).unwrap(),
|
|
original_database
|
|
);
|
|
assert_eq!(fs::read_to_string(root.join("main.cpp")).unwrap(), main);
|
|
assert!(!build.join(".cache").exists());
|
|
assert!(!root.join(".cache").exists());
|
|
assert!(root.join(".cex/clangd/compile_commands.json").is_file());
|
|
assert!(root.join(".cex/clangd/.cache/clangd/index").is_dir());
|
|
}
|
|
|
|
#[test]
|
|
#[ignore = "requires clangd on PATH; exercises a generated 120k-line project"]
|
|
fn medium_project_indexes_unopened_translation_units() {
|
|
let temp = tempdir().unwrap();
|
|
let root = temp.path();
|
|
let build = root.join("build");
|
|
fs::create_dir_all(build.join("meson-info")).unwrap();
|
|
fs::write(build.join("meson-info/intro-projectinfo.json"), "{}").unwrap();
|
|
fs::write(root.join("api.hpp"), "int target(int);\n").unwrap();
|
|
let mut commands = Vec::new();
|
|
for i in 0..120 {
|
|
let name = format!("unit{i}.cpp");
|
|
let mut code = format!("#include \"api.hpp\"\nint caller{i}() {{ return target({i}); }}\n");
|
|
for line in 2..1000 {
|
|
code.push_str(&format!("constexpr int value{i}_{line} = {line};\n"));
|
|
}
|
|
fs::write(root.join(&name), code).unwrap();
|
|
commands.push(json!({"directory": root, "file": root.join(&name), "arguments": ["clang++", "-std=c++23", "-c", name]}));
|
|
}
|
|
fs::write(
|
|
build.join("compile_commands.json"),
|
|
serde_json::to_vec(&commands).unwrap(),
|
|
)
|
|
.unwrap();
|
|
let mut workspace = crate::open(root, Some(&build), false).unwrap();
|
|
// Search filtering must not remove translation units from clangd's index.
|
|
workspace
|
|
.save_options(vec!["unit*.cpp".into()], "clangd".into())
|
|
.unwrap();
|
|
let started = Instant::now();
|
|
let server = Clangd::start(workspace).unwrap();
|
|
let position = Position {
|
|
line: 0,
|
|
character: 5,
|
|
};
|
|
loop {
|
|
let references = server
|
|
.query("api.hpp", position, Query::References)
|
|
.unwrap();
|
|
if references
|
|
.locations
|
|
.iter()
|
|
.filter(|l| l.path.ends_with(".cpp"))
|
|
.count()
|
|
== 120
|
|
{
|
|
break;
|
|
}
|
|
assert!(
|
|
started.elapsed() < Duration::from_secs(45),
|
|
"Index incomplete: {} references",
|
|
references.locations.len()
|
|
);
|
|
thread::sleep(Duration::from_millis(200));
|
|
}
|
|
let callers = server.query("api.hpp", position, Query::Callers).unwrap();
|
|
assert_eq!(callers.locations.len(), 120);
|
|
eprintln!(
|
|
"120,000 C++ lines, 120 translation units: all callers indexed in {:?}",
|
|
started.elapsed()
|
|
);
|
|
server.stop();
|
|
}
|
|
|
|
#[test]
|
|
#[ignore = "requires clangd"]
|
|
fn composed_concept_dependencies() {
|
|
let temp = tempdir().unwrap();
|
|
let root = temp.path();
|
|
let build = root.join("build");
|
|
fs::create_dir_all(build.join("meson-info")).unwrap();
|
|
fs::write(build.join("meson-info/intro-projectinfo.json"), "{}").unwrap();
|
|
for (name, content) in [
|
|
(
|
|
"foundation.hpp",
|
|
include_str!("../../../../examples/concepts/foundation.hpp"),
|
|
),
|
|
(
|
|
"composed.hpp",
|
|
include_str!("../../../../examples/concepts/composed.hpp"),
|
|
),
|
|
(
|
|
"main.cpp",
|
|
include_str!("../../../../examples/concepts/main.cpp"),
|
|
),
|
|
(
|
|
"policies.hpp",
|
|
include_str!("../../../../examples/concepts/policies.hpp"),
|
|
),
|
|
] {
|
|
fs::write(root.join(name), content).unwrap();
|
|
}
|
|
fs::write(build.join("compile_commands.json"), serde_json::to_vec(&json!([{"directory":root,"file":root.join("main.cpp"),"arguments":["clang++","-std=c++23","-c",root.join("main.cpp")]}])).unwrap()).unwrap();
|
|
let workspace = crate::open(root, Some(&build), false).unwrap();
|
|
let server = Clangd::start(workspace.clone()).unwrap();
|
|
let policies = fs::read_to_string(root.join("policies.hpp")).unwrap();
|
|
for (concept, function, unrelated) in [
|
|
(
|
|
"IsReactionChainPolicy",
|
|
"registerReactionChainPolicyDefs",
|
|
"registerNetworkPolicyDefs",
|
|
),
|
|
(
|
|
"IsNetworkPolicy",
|
|
"registerNetworkPolicyDefs",
|
|
"registerReactionChainPolicyDefs",
|
|
),
|
|
] {
|
|
let line = policies
|
|
.lines()
|
|
.position(|l| l.contains(&format!("concept {concept} =")))
|
|
.unwrap();
|
|
let position = Position {
|
|
line: line as u32,
|
|
character: policies.lines().nth(line).unwrap().find(concept).unwrap() as u32,
|
|
};
|
|
let deadline = Instant::now() + Duration::from_secs(10);
|
|
let graph = loop {
|
|
let graph = server
|
|
.query("policies.hpp", position, Query::ConceptGraph)
|
|
.unwrap()
|
|
.graph
|
|
.unwrap();
|
|
if graph.nodes.iter().any(|n| n.label == function) {
|
|
break graph;
|
|
}
|
|
assert!(Instant::now() < deadline, "Missing {function}: {graph:?}");
|
|
thread::sleep(Duration::from_millis(100));
|
|
};
|
|
let functions: Vec<_> = graph.nodes.iter().filter(|n| n.label == function).collect();
|
|
assert_eq!(
|
|
functions.len(),
|
|
1,
|
|
"Two constrained parameters must share one function node"
|
|
);
|
|
assert!(
|
|
graph
|
|
.edges
|
|
.iter()
|
|
.any(|e| e.to == functions[0].id && e.label == "constrains"),
|
|
"{graph:?}"
|
|
);
|
|
assert!(
|
|
!graph.nodes.iter().any(|n| n.label == unrelated),
|
|
"{graph:?}"
|
|
);
|
|
if concept == "IsNetworkPolicy" {
|
|
let line = policies
|
|
.lines()
|
|
.position(|l| l.starts_with("static_assert"))
|
|
.unwrap();
|
|
let usage = graph
|
|
.nodes
|
|
.iter()
|
|
.find(|n| n.label == format!("policies.hpp:{}", line + 1))
|
|
.unwrap();
|
|
assert!(
|
|
graph
|
|
.edges
|
|
.iter()
|
|
.any(|e| e.to == usage.id && e.label == "used by")
|
|
);
|
|
}
|
|
}
|
|
let token = tokio_util::sync::CancellationToken::new();
|
|
let position = |source: &str, name: &str| {
|
|
let line = source
|
|
.lines()
|
|
.position(|line| line.contains(&format!("concept {name} =")))
|
|
.unwrap();
|
|
Position {
|
|
line: line as u32,
|
|
character: source.lines().nth(line).unwrap().find(name).unwrap() as u32,
|
|
}
|
|
};
|
|
let code = fs::read_to_string(root.join("composed.hpp")).unwrap();
|
|
let direct = server
|
|
.query(
|
|
"composed.hpp",
|
|
position(&code, "Flexible"),
|
|
Query::Constraints,
|
|
)
|
|
.unwrap();
|
|
assert!(direct.is_concept);
|
|
let labels: Vec<_> = direct.locations.iter().map(|l| l.label.as_str()).collect();
|
|
assert!(
|
|
labels.contains(&"demo::Arithmetic")
|
|
&& labels.contains(&"demo::Sized")
|
|
&& labels.contains(&"std::copyable"),
|
|
"{labels:?}"
|
|
);
|
|
let sized = server
|
|
.query("composed.hpp", position(&code, "Sized"), Query::Constraints)
|
|
.unwrap();
|
|
assert!(
|
|
sized.locations.iter().any(|l| l.label == "std::integral"),
|
|
"{sized:?}"
|
|
);
|
|
assert!(
|
|
sized.locations.iter().any(|l| l.label == "std::same_as"),
|
|
"{sized:?}"
|
|
);
|
|
let prepared = crate::explanation::prepare(
|
|
&workspace,
|
|
server.as_ref(),
|
|
"composed.hpp",
|
|
position(&code, "Flexible"),
|
|
None,
|
|
"Explain all alternatives",
|
|
&token,
|
|
)
|
|
.unwrap();
|
|
assert!(prepared.concept_dependencies && !prepared.include_standard);
|
|
assert!(
|
|
prepared
|
|
.available_callees
|
|
.iter()
|
|
.any(|c| c.is_standard && c.location.label == "std::copyable")
|
|
);
|
|
let ids: Vec<_> = prepared
|
|
.available_callees
|
|
.iter()
|
|
.map(|c| c.id.clone())
|
|
.collect();
|
|
let one = crate::explanation::expand(&prepared, server.as_ref(), &ids, 1, "", &token).unwrap();
|
|
assert!(
|
|
one.bundle
|
|
.sources
|
|
.iter()
|
|
.any(|s| s.enclosing == "Arithmetic")
|
|
);
|
|
assert!(!one.bundle.sources.iter().any(|s| s.enclosing == "Addable"));
|
|
let two = crate::explanation::expand(&prepared, server.as_ref(), &ids, 2, "", &token).unwrap();
|
|
assert!(
|
|
two.bundle.sources.iter().any(|s| s.enclosing == "Addable"),
|
|
"{two:?}"
|
|
);
|
|
assert!(
|
|
two.bundle.sources.iter().all(|s| !s.path.starts_with('/')),
|
|
"Standard definitions leaked into default expansion"
|
|
);
|
|
assert!(
|
|
two.bundle
|
|
.omissions
|
|
.iter()
|
|
.any(|s| s.contains("std namespace"))
|
|
);
|
|
let with_std = crate::explanation::expand_with_standard(
|
|
&prepared,
|
|
server.as_ref(),
|
|
&ids,
|
|
1,
|
|
true,
|
|
"",
|
|
&token,
|
|
)
|
|
.unwrap();
|
|
assert!(
|
|
with_std
|
|
.bundle
|
|
.sources
|
|
.iter()
|
|
.any(|s| s.enclosing == "copyable"),
|
|
"{with_std:?}"
|
|
);
|
|
let outer = crate::explanation::prepare(
|
|
&workspace,
|
|
server.as_ref(),
|
|
"composed.hpp",
|
|
position(&code, "Outer"),
|
|
None,
|
|
"",
|
|
&token,
|
|
)
|
|
.unwrap();
|
|
let outer_ids: Vec<_> = outer
|
|
.available_callees
|
|
.iter()
|
|
.map(|c| c.id.clone())
|
|
.collect();
|
|
let expanded =
|
|
crate::explanation::expand(&outer, server.as_ref(), &outer_ids, 2, "", &token).unwrap();
|
|
assert!(
|
|
expanded
|
|
.bundle
|
|
.sources
|
|
.iter()
|
|
.any(|s| s.enclosing == "Flexible")
|
|
);
|
|
assert!(
|
|
expanded
|
|
.bundle
|
|
.sources
|
|
.iter()
|
|
.any(|s| s.enclosing == "Sized")
|
|
);
|
|
assert_eq!(
|
|
expanded
|
|
.bundle
|
|
.sources
|
|
.iter()
|
|
.filter(|s| s.enclosing == "Addable")
|
|
.count(),
|
|
1
|
|
);
|
|
assert!(
|
|
crate::explanation::expand(&outer, server.as_ref(), &outer_ids, 3, "", &token).is_err()
|
|
);
|
|
let deadline = Instant::now() + Duration::from_secs(10);
|
|
let graph = loop {
|
|
let graph = server
|
|
.query(
|
|
"composed.hpp",
|
|
position(&code, "Flexible"),
|
|
Query::ConceptGraph,
|
|
)
|
|
.unwrap()
|
|
.graph
|
|
.unwrap();
|
|
if graph
|
|
.nodes
|
|
.iter()
|
|
.any(|n| n.label.contains("constrained_identity"))
|
|
{
|
|
break graph;
|
|
}
|
|
assert!(
|
|
Instant::now() < deadline,
|
|
"Missing constrained declaration: {graph:?}"
|
|
);
|
|
thread::sleep(Duration::from_millis(100));
|
|
};
|
|
assert!(
|
|
graph
|
|
.nodes
|
|
.iter()
|
|
.any(|n| n.kind == "operator" && n.label == "||")
|
|
);
|
|
assert!(
|
|
graph
|
|
.nodes
|
|
.iter()
|
|
.any(|n| n.kind == "operator" && n.label == "&&")
|
|
);
|
|
assert!(
|
|
graph
|
|
.nodes
|
|
.iter()
|
|
.any(|n| n.kind == "requires" && n.detail.contains("copyable"))
|
|
);
|
|
assert!(
|
|
graph
|
|
.nodes
|
|
.iter()
|
|
.any(|n| n.kind == "concept" && n.label.contains("Arithmetic"))
|
|
);
|
|
assert!(
|
|
graph
|
|
.nodes
|
|
.iter()
|
|
.any(|n| n.kind == "concept" && n.label.contains("Sized"))
|
|
);
|
|
let constrained = graph
|
|
.nodes
|
|
.iter()
|
|
.find(|n| n.label.contains("constrained_identity"))
|
|
.unwrap();
|
|
assert!(
|
|
graph
|
|
.edges
|
|
.iter()
|
|
.any(|e| e.to == constrained.id && e.label == "constrains"),
|
|
"{graph:?}"
|
|
);
|
|
assert_eq!(
|
|
graph
|
|
.nodes
|
|
.iter()
|
|
.filter(|n| n.label.contains("constrained_identity"))
|
|
.count(),
|
|
1
|
|
);
|
|
assert!(graph.nodes.iter().any(|n| n.label == "main.cpp:3"));
|
|
let type_use = graph.nodes.iter().find(|n| n.label == "type_use").unwrap();
|
|
assert!(
|
|
graph
|
|
.edges
|
|
.iter()
|
|
.any(|e| e.to == type_use.id && e.label == "used by")
|
|
);
|
|
let ordinary = graph
|
|
.nodes
|
|
.iter()
|
|
.find(|n| n.label == "ordinary_use")
|
|
.unwrap();
|
|
assert!(
|
|
graph
|
|
.edges
|
|
.iter()
|
|
.any(|e| e.to == ordinary.id && e.label == "used by"),
|
|
"{graph:?}"
|
|
);
|
|
let outer = graph.nodes.iter().find(|n| n.label == "Outer").unwrap();
|
|
assert!(
|
|
graph
|
|
.edges
|
|
.iter()
|
|
.any(|e| e.to == outer.id && e.label == "contributes to")
|
|
);
|
|
assert!(
|
|
graph
|
|
.edges
|
|
.iter()
|
|
.all(|e| graph.nodes.iter().any(|n| n.id == e.from)
|
|
&& graph.nodes.iter().any(|n| n.id == e.to))
|
|
);
|
|
let recursive_graph = server
|
|
.graph(
|
|
"composed.hpp",
|
|
position(&code, "Flexible"),
|
|
&GraphOptions {
|
|
depth: 2,
|
|
..GraphOptions::default()
|
|
},
|
|
)
|
|
.unwrap();
|
|
assert!(
|
|
recursive_graph
|
|
.nodes
|
|
.iter()
|
|
.any(|n| n.label == "demo::Addable"),
|
|
"{recursive_graph:?}"
|
|
);
|
|
assert!(
|
|
!recursive_graph
|
|
.nodes
|
|
.iter()
|
|
.any(|n| n.label.starts_with("std::")),
|
|
"{recursive_graph:?}"
|
|
);
|
|
let library_graph = server
|
|
.graph(
|
|
"composed.hpp",
|
|
position(&code, "Flexible"),
|
|
&GraphOptions {
|
|
depth: 1,
|
|
excluded_namespaces: vec![],
|
|
},
|
|
)
|
|
.unwrap();
|
|
assert!(
|
|
library_graph
|
|
.nodes
|
|
.iter()
|
|
.any(|n| n.label == "std::copyable"),
|
|
"{library_graph:?}"
|
|
);
|
|
let ordinary_position = Position {
|
|
line: fs::read_to_string(root.join("main.cpp"))
|
|
.unwrap()
|
|
.lines()
|
|
.position(|l| l.contains("bool ordinary_use"))
|
|
.unwrap() as u32,
|
|
character: 6,
|
|
};
|
|
assert!(
|
|
server
|
|
.query("main.cpp", ordinary_position, Query::ConceptGraph)
|
|
.is_err()
|
|
);
|
|
server.stop();
|
|
}
|
|
|
|
#[test]
|
|
#[ignore = "requires cmake, ninja, a C++23 compiler, and clangd"]
|
|
fn cmake_cpp23_navigation_with_generated_headers() {
|
|
let temp = tempdir().unwrap();
|
|
let root = temp.path().join("source with spaces");
|
|
let build = temp.path().join("external build");
|
|
fs::create_dir_all(root.join("src")).unwrap();
|
|
for (name, content) in [
|
|
(
|
|
"CMakeLists.txt",
|
|
include_str!("../../../../examples/cmake/CMakeLists.txt"),
|
|
),
|
|
(
|
|
"config.hpp.in",
|
|
include_str!("../../../../examples/cmake/config.hpp.in"),
|
|
),
|
|
(
|
|
"main.cpp",
|
|
include_str!("../../../../examples/cmake/main.cpp"),
|
|
),
|
|
(
|
|
"src/numbers.hpp",
|
|
include_str!("../../../../examples/cmake/src/numbers.hpp"),
|
|
),
|
|
(
|
|
"src/numbers.cpp",
|
|
include_str!("../../../../examples/cmake/src/numbers.cpp"),
|
|
),
|
|
] {
|
|
fs::write(root.join(name), content).unwrap();
|
|
}
|
|
let configure = std::process::Command::new("cmake")
|
|
.arg("-S")
|
|
.arg(&root)
|
|
.arg("-B")
|
|
.arg(&build)
|
|
.args(["-G", "Ninja", "-DCMAKE_EXPORT_COMPILE_COMMANDS=ON"])
|
|
.output()
|
|
.unwrap();
|
|
assert!(
|
|
configure.status.success(),
|
|
"{}\n{}",
|
|
String::from_utf8_lossy(&configure.stdout),
|
|
String::from_utf8_lossy(&configure.stderr)
|
|
);
|
|
let cache = fs::read(build.join("CMakeCache.txt")).unwrap();
|
|
let database = fs::read(build.join("compile_commands.json")).unwrap();
|
|
let workspace = crate::open(&root, Some(&build), false).unwrap();
|
|
let server = Clangd::start(workspace).unwrap();
|
|
let definition = server
|
|
.query(
|
|
"main.cpp",
|
|
Position {
|
|
line: 6,
|
|
character: 22,
|
|
},
|
|
Query::Definition,
|
|
)
|
|
.unwrap();
|
|
assert!(
|
|
definition
|
|
.locations
|
|
.iter()
|
|
.any(|l| l.path == "src/numbers.cpp"),
|
|
"{definition:?}"
|
|
);
|
|
let generated = server
|
|
.query(
|
|
"src/numbers.cpp",
|
|
Position {
|
|
line: 2,
|
|
character: 46,
|
|
},
|
|
Query::Definition,
|
|
)
|
|
.unwrap();
|
|
let config = generated
|
|
.locations
|
|
.iter()
|
|
.find(|l| l.path.ends_with("generated/config.hpp"))
|
|
.expect("Resolve generated header using CMake's include flags");
|
|
assert!(
|
|
server
|
|
.read_target(&config.path)
|
|
.unwrap()
|
|
.content
|
|
.contains("#define DEMO_INCREMENT 1")
|
|
);
|
|
let deadline = Instant::now() + Duration::from_secs(15);
|
|
loop {
|
|
let callers = server
|
|
.query(
|
|
"src/numbers.hpp",
|
|
Position {
|
|
line: 3,
|
|
character: 5,
|
|
},
|
|
Query::Callers,
|
|
)
|
|
.unwrap();
|
|
if callers.locations.iter().any(|l| l.path == "main.cpp") {
|
|
break;
|
|
}
|
|
assert!(Instant::now() < deadline, "{callers:?}");
|
|
thread::sleep(Duration::from_millis(100));
|
|
}
|
|
server.stop();
|
|
assert!(root.join(".cex/clangd/compile_commands.json").is_file());
|
|
assert_eq!(
|
|
fs::read(build.join("compile_commands.json")).unwrap(),
|
|
database
|
|
);
|
|
assert_eq!(fs::read(build.join("CMakeCache.txt")).unwrap(), cache);
|
|
assert!(!build.join(".cache").exists());
|
|
}
|
|
|
|
#[test]
|
|
#[ignore = "requires clangd and a C++23 compiler"]
|
|
fn function_graph_depth_namespaces_and_cycles() {
|
|
let temp = tempdir().unwrap();
|
|
let root = temp.path();
|
|
let build = root.join("build");
|
|
fs::create_dir_all(build.join("meson-info")).unwrap();
|
|
fs::write(build.join("meson-info/intro-projectinfo.json"), "{}").unwrap();
|
|
let code = "#include <utility>\nnamespace alpha {\nint leaf(int n) { return n+1; }\nint middle(int n) { return leaf(n); }\n}\nnamespace a = alpha;\nint recursive(int n) { return n ? recursive(n-1) : 0; }\nint target(int n) { return a::middle(n) + std::move(n) + recursive(n); }\nint caller(int n) { return target(n) + target(n); }\nint outer(int n) { return caller(n); }\n";
|
|
fs::write(root.join("main.cpp"), code).unwrap();
|
|
fs::write(build.join("compile_commands.json"), serde_json::to_vec(&json!([{"directory":root,"file":root.join("main.cpp"),"arguments":["clang++","-std=c++23","-c",root.join("main.cpp")]}])).unwrap()).unwrap();
|
|
let workspace = crate::open(root, Some(&build), false).unwrap();
|
|
let server = Clangd::start(workspace).unwrap();
|
|
let pos = Position {
|
|
line: 7,
|
|
character: 5,
|
|
};
|
|
let deadline = Instant::now() + Duration::from_secs(10);
|
|
let direct = loop {
|
|
let graph = server
|
|
.graph("main.cpp", pos, &GraphOptions::default())
|
|
.unwrap();
|
|
if graph.nodes.iter().any(|n| n.label == "caller") {
|
|
break graph;
|
|
}
|
|
assert!(Instant::now() < deadline, "{graph:?}");
|
|
thread::sleep(Duration::from_millis(100));
|
|
};
|
|
assert!(
|
|
direct.nodes.iter().any(|n| n.label == "alpha::middle"),
|
|
"{direct:?}"
|
|
);
|
|
assert!(
|
|
!direct.nodes.iter().any(|n| n.label == "alpha::leaf"
|
|
|| n.label == "outer"
|
|
|| n.label.starts_with("std::")),
|
|
"{direct:?}"
|
|
);
|
|
let middle = direct
|
|
.nodes
|
|
.iter()
|
|
.find(|n| n.label == "alpha::middle")
|
|
.unwrap();
|
|
let caller = direct.nodes.iter().find(|n| n.label == "caller").unwrap();
|
|
assert!(
|
|
direct
|
|
.edges
|
|
.iter()
|
|
.any(|e| e.from == middle.id && e.to == "selected")
|
|
);
|
|
assert!(
|
|
direct
|
|
.edges
|
|
.iter()
|
|
.any(|e| e.from == "selected" && e.to == caller.id)
|
|
);
|
|
assert_eq!(
|
|
direct.nodes.iter().filter(|n| n.label == "caller").count(),
|
|
1
|
|
);
|
|
let expanded = server
|
|
.graph(
|
|
"main.cpp",
|
|
pos,
|
|
&GraphOptions {
|
|
depth: 2,
|
|
..GraphOptions::default()
|
|
},
|
|
)
|
|
.unwrap();
|
|
assert!(
|
|
expanded.nodes.iter().any(|n| n.label == "alpha::leaf"),
|
|
"{expanded:?}"
|
|
);
|
|
assert!(
|
|
expanded.nodes.iter().any(|n| n.label == "outer"),
|
|
"{expanded:?}"
|
|
);
|
|
assert!(
|
|
expanded.edges.iter().any(|e| e.from == e.to),
|
|
"Recursive calls should be preserved: {expanded:?}"
|
|
);
|
|
let included = server
|
|
.graph(
|
|
"main.cpp",
|
|
pos,
|
|
&GraphOptions {
|
|
depth: 1,
|
|
excluded_namespaces: vec![],
|
|
},
|
|
)
|
|
.unwrap();
|
|
assert!(
|
|
included
|
|
.nodes
|
|
.iter()
|
|
.any(|n| n.label.starts_with("std::move")),
|
|
"{included:?}"
|
|
);
|
|
let excluded = server
|
|
.graph(
|
|
"main.cpp",
|
|
pos,
|
|
&GraphOptions {
|
|
depth: 2,
|
|
excluded_namespaces: vec!["std".into(), "alpha".into()],
|
|
},
|
|
)
|
|
.unwrap();
|
|
assert!(
|
|
!excluded
|
|
.nodes
|
|
.iter()
|
|
.any(|n| n.label.starts_with("alpha::")),
|
|
"{excluded:?}"
|
|
);
|
|
assert!(
|
|
server
|
|
.graph(
|
|
"main.cpp",
|
|
pos,
|
|
&GraphOptions {
|
|
depth: 5,
|
|
..GraphOptions::default()
|
|
}
|
|
)
|
|
.is_err()
|
|
);
|
|
server.stop();
|
|
}
|
|
|
|
#[test]
|
|
#[ignore = "requires clangd"]
|
|
fn type_graph_constraints_and_members() {
|
|
let temp = tempdir().unwrap();
|
|
let root = temp.path();
|
|
let build = root.join("build");
|
|
fs::create_dir_all(build.join("meson-info")).unwrap();
|
|
fs::write(build.join("meson-info/intro-projectinfo.json"), "{}").unwrap();
|
|
fs::write(
|
|
root.join("types.hpp"),
|
|
include_str!("../../../../examples/types/types.hpp"),
|
|
)
|
|
.unwrap();
|
|
fs::write(
|
|
root.join("main.cpp"),
|
|
include_str!("../../../../examples/types/main.cpp"),
|
|
)
|
|
.unwrap();
|
|
fs::write(build.join("compile_commands.json"),serde_json::to_vec(&json!([{"directory":root,"file":root.join("main.cpp"),"arguments":["clang++","-std=c++23","-c",root.join("main.cpp")]}])).unwrap()).unwrap();
|
|
let server = Clangd::start(crate::open(root, Some(&build), false).unwrap()).unwrap();
|
|
let source = include_str!("../../../../examples/types/types.hpp");
|
|
let position = |needle: &str| {
|
|
let (line, text) = source
|
|
.lines()
|
|
.enumerate()
|
|
.find(|(_, l)| l.contains(needle))
|
|
.unwrap();
|
|
Position {
|
|
line: line as u32,
|
|
character: (text.find(needle).unwrap() + 1) as u32,
|
|
}
|
|
};
|
|
let pos = position("Box :");
|
|
let graph = server
|
|
.graph(
|
|
"types.hpp",
|
|
pos,
|
|
&GraphOptions {
|
|
depth: 1,
|
|
excluded_namespaces: vec![],
|
|
},
|
|
)
|
|
.unwrap();
|
|
for label in [
|
|
"demo::Base",
|
|
"demo::Helper",
|
|
"demo::Box::value",
|
|
"demo::Box::helper",
|
|
"demo::Box::set",
|
|
"demo::Box::run",
|
|
"demo::Addable",
|
|
"demo::Sized",
|
|
"std::copyable",
|
|
"std::constructible_from",
|
|
"std::integral",
|
|
"std::convertible_to",
|
|
] {
|
|
assert!(
|
|
graph.nodes.iter().any(|n| n.label == label),
|
|
"Missing {label}: {graph:?}"
|
|
);
|
|
}
|
|
assert!(
|
|
graph
|
|
.nodes
|
|
.iter()
|
|
.any(|n| n.kind == "requires" && n.detail.contains("u.size()"))
|
|
);
|
|
assert!(
|
|
graph
|
|
.nodes
|
|
.iter()
|
|
.any(|n| n.kind == "parameter" && n.label == "argument x")
|
|
);
|
|
assert!(
|
|
graph
|
|
.nodes
|
|
.iter()
|
|
.any(|n| n.kind == "template-parameter" && n.label == "template T")
|
|
);
|
|
let default_graph = server
|
|
.graph("types.hpp", pos, &GraphOptions::default())
|
|
.unwrap();
|
|
assert!(
|
|
!default_graph
|
|
.nodes
|
|
.iter()
|
|
.any(|n| n.label.starts_with("std::"))
|
|
);
|
|
let node = |label: &str| graph.nodes.iter().find(|n| n.label == label).unwrap();
|
|
let edge = |from: &str, to: &str, label: &str| {
|
|
graph
|
|
.edges
|
|
.iter()
|
|
.any(|e| e.from == from && e.to == to && e.label == label)
|
|
};
|
|
assert!(edge(&node("demo::Base").id, "selected", "base of"));
|
|
assert!(edge(
|
|
&node("demo::Helper").id,
|
|
&node("demo::Box::helper").id,
|
|
"type of"
|
|
));
|
|
assert!(edge(
|
|
&node("template T").id,
|
|
&node("demo::Box::value").id,
|
|
"type of"
|
|
));
|
|
assert!(edge(
|
|
&node("std::integral").id,
|
|
&node("argument n").id,
|
|
"constrains"
|
|
));
|
|
let u = graph
|
|
.nodes
|
|
.iter()
|
|
.find(|n| {
|
|
n.label == "template U" && edge(&n.id, &node("demo::Box::set").id, "template parameter")
|
|
})
|
|
.unwrap();
|
|
let argument = graph
|
|
.nodes
|
|
.iter()
|
|
.find(|n| n.label == "argument u" && edge(&n.id, &node("demo::Box::set").id, "argument"))
|
|
.unwrap();
|
|
assert!(edge(&node("demo::Sized").id, &u.id, "constrains"));
|
|
assert!(edge(&u.id, &argument.id, "type of"));
|
|
let calls = server
|
|
.graph(
|
|
"types.hpp",
|
|
pos,
|
|
&GraphOptions {
|
|
depth: 2,
|
|
..Default::default()
|
|
},
|
|
)
|
|
.unwrap();
|
|
let work = calls
|
|
.nodes
|
|
.iter()
|
|
.find(|n| n.label == "demo::Helper::work")
|
|
.unwrap();
|
|
let set = calls
|
|
.nodes
|
|
.iter()
|
|
.find(|n| n.label == "demo::Box::set")
|
|
.unwrap();
|
|
assert!(
|
|
calls
|
|
.edges
|
|
.iter()
|
|
.any(|e| e.from == work.id && e.to == set.id && e.label == "called by")
|
|
);
|
|
assert!(
|
|
graph
|
|
.nodes
|
|
.iter()
|
|
.any(|n| n.kind == "operator" && n.label == "||")
|
|
);
|
|
assert!(
|
|
graph
|
|
.nodes
|
|
.iter()
|
|
.any(|n| n.kind == "type" && n.label.starts_with("std::vector")),
|
|
"{graph:?}"
|
|
);
|
|
let method = server
|
|
.graph(
|
|
"types.hpp",
|
|
position("set("),
|
|
&GraphOptions {
|
|
depth: 1,
|
|
excluded_namespaces: vec![],
|
|
},
|
|
)
|
|
.unwrap();
|
|
assert!(method.title.ends_with("::set"), "{method:?}");
|
|
assert!(
|
|
method.nodes.iter().any(|n| n.label == "demo::Sized"),
|
|
"{method:?}"
|
|
);
|
|
let constructor = server
|
|
.graph(
|
|
"types.hpp",
|
|
position("Box(Addable"),
|
|
&GraphOptions {
|
|
depth: 1,
|
|
excluded_namespaces: vec![],
|
|
},
|
|
)
|
|
.unwrap();
|
|
assert!(
|
|
constructor
|
|
.nodes
|
|
.iter()
|
|
.any(|n| n.label == "std::constructible_from"),
|
|
"{constructor:?}"
|
|
);
|
|
let alias = server
|
|
.graph("types.hpp", position("IntBox ="), &GraphOptions::default())
|
|
.unwrap();
|
|
assert!(
|
|
alias.title == "demo::Box"
|
|
|| alias
|
|
.nodes
|
|
.iter()
|
|
.any(|n| n.kind == "type" && n.label == "demo::Box"),
|
|
"{alias:?}"
|
|
);
|
|
let enumeration = server
|
|
.graph("types.hpp", position("State {"), &GraphOptions::default())
|
|
.unwrap();
|
|
assert!(
|
|
enumeration.nodes.iter().any(|n| n.label == "idle"),
|
|
"{enumeration:?}"
|
|
);
|
|
server.stop();
|
|
}
|