use std::collections::BTreeMap;
mod common;
#[test]
fn rust_header_survives_index_and_surface() {
let (repo, dir, attrs) = index_and_attrs();
let expect = "pub async fn process<T: Event>(\n &self,\n event: T,\n ) -> Result<Incident, Box<dyn Error>>\n where\n T: Send + Sync,";
assert_eq!(header_of(&attrs, "rust_surface.rs", "Handler.process"), expect);
assert_eq!(header_of(&attrs, "rust_surface.rs", "Incident"), "pub struct Incident");
assert_eq!(header_of(&attrs, "rust_surface.rs", "Severity"), "pub enum Severity");
assert_eq!(header_of(&attrs, "rust_surface.rs", "Event"), "pub trait Event: Send + Sync");
let out = common::run_ok(&dir, &["surface"]);
assert!(
out.contains(&rendered(expect)),
"surface must render the full multi-line rust header:\n{out}"
);
let _ = repo;
}
#[test]
fn python_header_survives_index_and_surface() {
let (repo, dir, attrs) = index_and_attrs();
let expect = "async def fetch_all(\n endpoint: str,\n limit: int = 20,\n retries: Optional[int] = None,\n) -> List[dict]";
assert_eq!(header_of(&attrs, "python_surface.py", "fetch_all"), expect);
assert_eq!(header_of(&attrs, "python_surface.py", "QueryBuilder"), "class QueryBuilder");
assert_eq!(
header_of(&attrs, "python_surface.py", "QueryBuilder.build"),
"def build(\n self,\n fields: List[str],\n where: Optional[str] = None,\n order_by: str = \"id\",\n ) -> \"QueryBuilder\""
);
let out = common::run_ok(&dir, &["surface"]);
assert!(
out.contains(&rendered(expect)),
"surface must render the full multi-line python header:\n{out}"
);
let _ = repo;
}
#[test]
fn typescript_header_survives_index_and_surface() {
let (repo, dir, attrs) = index_and_attrs();
assert_eq!(header_of(&attrs, "ts_surface.ts", "Repo"), "interface Repo<T>");
assert_eq!(
header_of(&attrs, "ts_surface.ts", "IncidentRepo"),
"class IncidentRepo implements Repo<Incident>"
);
let expect = "async findByOwner(\n owner: string,\n opts?: { limit?: number },\n ): Promise<Incident[]>";
assert_eq!(header_of(&attrs, "ts_surface.ts", "IncidentRepo.findByOwner"), expect);
let out = common::run_ok(&dir, &["surface"]);
assert!(
out.contains(&rendered(expect)),
"surface must render the full multi-line typescript header:\n{out}"
);
let _ = repo;
}
#[test]
fn go_header_survives_index_and_surface() {
let (repo, dir, attrs) = index_and_attrs();
assert_eq!(header_of(&attrs, "go_surface.go", "Incident"), "type Incident struct");
assert_eq!(header_of(&attrs, "go_surface.go", "Reporter"), "type Reporter struct");
assert_eq!(header_of(&attrs, "go_surface.go", "Notifier"), "type Notifier interface");
let expect = "func (r *Reporter) Summarize(\n\tincidents []*Incident,\n\tlimit int,\n) ([]string, error)";
assert_eq!(header_of(&attrs, "go_surface.go", "Reporter.Summarize"), expect);
assert_eq!(
header_of(&attrs, "go_surface.go", "Reporter.Merge"),
"func (r *Reporter) Merge(values ...string) string"
);
let out = common::run_ok(&dir, &["surface"]);
assert!(
out.contains(&rendered(expect)),
"surface must render the full multi-line go header:\n{out}"
);
let _ = repo;
}
#[test]
fn java_header_survives_index_and_surface() {
let (repo, dir, attrs) = index_and_attrs();
assert_eq!(header_of(&attrs, "java_surface.java", "Repository"), "public interface Repository<T>");
assert_eq!(
header_of(&attrs, "java_surface.java", "IncidentService"),
"public class IncidentService<T> implements Repository<T>"
);
let expect = "public List<T> findIncidents(\n String owner,\n int limit\n ) throws IOException";
assert_eq!(header_of(&attrs, "java_surface.java", "IncidentService.findIncidents"), expect);
let out = common::run_ok(&dir, &["surface"]);
assert!(
out.contains(&rendered(expect)),
"surface must render the full multi-line java header:\n{out}"
);
let _ = repo;
}
#[test]
fn overload_symbols_are_separate_entries_with_distinct_indexes() {
let (_repo, dir, _attrs) = index_and_attrs();
let ir = common::run_ok(&dir, &["export", "system-ir.json"]);
let v: serde_json::Value = serde_json::from_str(&ir).expect("system-ir.json parses");
let mut foos: Vec<serde_json::Value> = Vec::new();
for e in v["entities"].as_array().expect("entities array") {
if e["kind"].as_str() == Some("symbol")
&& e["name"].as_str() == Some("Calc.foo")
{
foos.push(e.clone());
}
}
assert_eq!(foos.len(), 2, "two separate Calc.foo entities: {foos:?}");
assert_ne!(
foos[0]["id"].as_str(),
foos[1]["id"].as_str(),
"overload entity ids are distinct"
);
let mut by_idx: BTreeMap<u64, &serde_json::Value> = BTreeMap::new();
for e in &foos {
let idx = e["attributes"]["overload_index"]
.as_u64()
.expect("overload_index attr");
by_idx.insert(idx, e);
}
assert_eq!(by_idx.keys().copied().collect::<Vec<_>>(), vec![0, 1]);
assert_eq!(
by_idx[&0]["attributes"]["decl_header"].as_str(),
Some("public int foo(int a)")
);
assert_eq!(
by_idx[&1]["attributes"]["decl_header"].as_str(),
Some("public int foo(int a, int b)")
);
let out = common::run_ok(&dir, &["surface"]);
assert!(
out.contains(&rendered("public int foo(int a)")),
"surface renders overload 0 header:\n{out}"
);
assert!(
out.contains(&rendered("public int foo(int a, int b)")),
"surface renders overload 1 header:\n{out}"
);
}
fn index_and_attrs(
) -> (tempfile::TempDir, std::path::PathBuf, BTreeMap<(String, String), serde_json::Value>) {
let repo = common::copy_fixture("surface-signatures");
let dir = common::workdir(repo.path());
common::run_ok(&dir, &["index", "--quiet"]);
let ir = common::run_ok(&dir, &["export", "system-ir.json"]);
let v: serde_json::Value = serde_json::from_str(&ir).expect("system-ir.json parses");
let mut attrs = BTreeMap::new();
for e in v["entities"].as_array().expect("entities array") {
if e["kind"].as_str() != Some("symbol") {
continue;
}
let file = e["attributes"]["file"].as_str().unwrap_or("").to_string();
let name = e["name"].as_str().unwrap_or("").to_string();
attrs.insert((file, name), e["attributes"].clone());
}
(repo, dir, attrs)
}
fn header_of(
attrs: &BTreeMap<(String, String), serde_json::Value>,
file: &str,
name: &str,
) -> String {
attrs
.get(&(file.to_string(), name.to_string()))
.and_then(|a| a.get("decl_header"))
.and_then(|v| v.as_str())
.unwrap_or_else(|| {
panic!(
"no decl_header attr for {file} {name}: {:?}",
attrs.get(&(file.to_string(), name.to_string()))
)
})
.to_string()
}
fn rendered(header: &str) -> String {
header
.split('\n')
.map(|l| format!(" {l}"))
.collect::<Vec<_>>()
.join("\n")
}