use super::{OutlineOpts, filter_signatures, run};
use crate::core::signatures::Signature;
fn sig(kind: &'static str, name: &str, exported: bool) -> Signature {
Signature {
kind,
name: name.to_string(),
params: String::new(),
return_type: String::new(),
is_async: false,
is_exported: exported,
indent: 0,
..Signature::no_span()
}
}
fn sample() -> Vec<Signature> {
vec![
sig("fn", "main", false),
sig("struct", "Config", true),
sig("fn", "load_config", true),
]
}
fn write(dir: &std::path::Path, rel: &str, body: &str) {
let p = dir.join(rel);
if let Some(parent) = p.parent() {
std::fs::create_dir_all(parent).unwrap();
}
std::fs::write(p, body).unwrap();
}
#[test]
fn filter_kind_matches_case_insensitively() {
let sigs = sample();
let opts = OutlineOpts {
kind: Some("FN"),
..Default::default()
};
assert_eq!(filter_signatures(&sigs, &opts).len(), 2);
}
#[test]
fn filter_all_and_none_return_everything() {
let sigs = sample();
let all = OutlineOpts {
kind: Some("all"),
..Default::default()
};
assert_eq!(filter_signatures(&sigs, &all).len(), 3);
assert_eq!(filter_signatures(&sigs, &OutlineOpts::default()).len(), 3);
}
#[test]
fn filter_name_match_is_substring_case_insensitive() {
let sigs = sample();
let opts = OutlineOpts {
name_match: Some("CONFIG"),
..Default::default()
};
let names: Vec<&str> = filter_signatures(&sigs, &opts)
.iter()
.map(|s| s.name.as_str())
.collect();
assert_eq!(names, vec!["Config", "load_config"]);
}
#[test]
fn filter_kind_and_match_compose() {
let sigs = sample();
let opts = OutlineOpts {
kind: Some("fn"),
name_match: Some("config"),
..Default::default()
};
let got = filter_signatures(&sigs, &opts);
assert_eq!(got.len(), 1);
assert_eq!(got[0].name, "load_config");
}
#[test]
fn file_outline_includes_line_spans() {
let tmp = tempfile::tempdir().unwrap();
write(tmp.path(), "a.rs", "pub fn alpha() {}\npub fn beta() {}\n");
let path = tmp.path().join("a.rs");
let (out, original) = run(path.to_str().unwrap(), &OutlineOpts::default());
assert!(out.contains("alpha"), "{out}");
assert!(out.contains("@L1"), "{out}");
assert!(original > 0);
}
#[test]
fn file_outline_emits_handle_hint() {
let tmp = tempfile::tempdir().unwrap();
write(tmp.path(), "a.rs", "pub fn alpha() {}\n");
let path = tmp.path().join("a.rs");
let (out, _) = run(path.to_str().unwrap(), &OutlineOpts::default());
assert!(out.contains("handle=\"path#name@Lstart\""), "{out}");
}
#[test]
fn rust_impl_has_impl_kind_not_class() {
let tmp = tempfile::tempdir().unwrap();
write(
tmp.path(),
"a.rs",
"pub struct Foo;\nimpl Clone for Foo {\n fn clone(&self) -> Self { Foo }\n}\n",
);
let path = tmp.path().join("a.rs");
let (out, _) = run(
path.to_str().unwrap(),
&OutlineOpts {
as_json: true,
..Default::default()
},
);
let v: serde_json::Value = serde_json::from_str(&out).unwrap();
let syms = v["symbols"].as_array().unwrap();
let impl_sym = syms
.iter()
.find(|s| s["kind"] == "impl")
.expect("impl rendered with its own kind");
assert!(
impl_sym["name"].as_str().unwrap().contains("Clone for Foo"),
"{out}"
);
assert!(
!syms.iter().any(|s| s["kind"] == "class"),
"impl must never be labelled class: {out}"
);
}
#[test]
fn no_match_message_is_informative() {
let tmp = tempfile::tempdir().unwrap();
write(tmp.path(), "a.rs", "pub fn alpha() {}\n");
let path = tmp.path().join("a.rs");
let opts = OutlineOpts {
name_match: Some("zzz"),
..Default::default()
};
let (out, original) = run(path.to_str().unwrap(), &opts);
assert!(out.contains("No symbols matching 'zzz'"), "{out}");
assert_eq!(original, 0);
}
#[test]
#[cfg(unix)]
fn symlink_in_tree_is_followed() {
let tmp = tempfile::tempdir().unwrap();
let target = tmp.path().join("real.rs");
std::fs::write(&target, "pub fn alpha() {}\n").unwrap();
let link = tmp.path().join("link.rs");
std::os::unix::fs::symlink(&target, &link).unwrap();
let (out, _) = run(link.to_str().unwrap(), &OutlineOpts::default());
assert!(out.contains("alpha"), "{out}");
}
#[test]
fn directory_outline_is_sorted_and_grouped() {
let tmp = tempfile::tempdir().unwrap();
write(tmp.path(), "z_last.rs", "pub fn zeta() {}\n");
write(tmp.path(), "a_first.rs", "pub fn alpha() {}\n");
let (out, _) = run(tmp.path().to_str().unwrap(), &OutlineOpts::default());
let a_pos = out.find("a_first.rs").expect("a_first listed");
let z_pos = out.find("z_last.rs").expect("z_last listed");
assert!(a_pos < z_pos, "files must be sorted by path:\n{out}");
assert!(out.contains("alpha") && out.contains("zeta"), "{out}");
}
#[test]
fn directory_outline_is_deterministic() {
let tmp = tempfile::tempdir().unwrap();
write(tmp.path(), "a.rs", "pub fn a() {}\n");
write(tmp.path(), "b.ts", "export function b(): void {}\n");
let d = tmp.path().to_str().unwrap();
let (o1, _) = run(d, &OutlineOpts::default());
let (o2, _) = run(d, &OutlineOpts::default());
assert_eq!(o1, o2, "directory outline must be byte-stable");
}
#[test]
fn directory_outline_skips_vendor_dirs() {
let tmp = tempfile::tempdir().unwrap();
write(tmp.path(), "src/app.rs", "pub fn app() {}\n");
write(
tmp.path(),
"node_modules/lib/index.js",
"export function vendor() {}\n",
);
let (out, _) = run(tmp.path().to_str().unwrap(), &OutlineOpts::default());
assert!(out.contains("app"), "{out}");
assert!(
!out.contains("vendor"),
"vendor dir must be skipped:\n{out}"
);
}
#[test]
fn match_filter_narrows_directory_outline() {
let tmp = tempfile::tempdir().unwrap();
write(
tmp.path(),
"a.rs",
"pub fn handle_request() {}\npub fn helper() {}\n",
);
let opts = OutlineOpts {
name_match: Some("request"),
..Default::default()
};
let (out, _) = run(tmp.path().to_str().unwrap(), &opts);
assert!(out.contains("handle_request"), "{out}");
assert!(!out.contains("helper"), "{out}");
}
#[test]
fn file_json_is_valid_and_labels_backend() {
let tmp = tempfile::tempdir().unwrap();
write(
tmp.path(),
"a.rs",
"pub fn alpha(x: i32) -> bool { x > 0 }\n",
);
let path = tmp.path().join("a.rs");
let (out, _) = run(
path.to_str().unwrap(),
&OutlineOpts {
as_json: true,
..Default::default()
},
);
let v: serde_json::Value = serde_json::from_str(&out).expect("valid json");
assert_eq!(v["language"], "rust");
assert_eq!(v["backend"], "tree-sitter");
let syms = v["symbols"].as_array().unwrap();
let alpha = syms.iter().find(|s| s["name"] == "alpha").expect("alpha");
assert_eq!(alpha["kind"], "fn");
assert_eq!(alpha["exported"], true);
}
#[test]
fn dir_json_is_deterministic_and_sorted() {
let tmp = tempfile::tempdir().unwrap();
write(tmp.path(), "z.rs", "pub fn z() {}\n");
write(tmp.path(), "a.rs", "pub fn a() {}\n");
let d = tmp.path().to_str().unwrap();
let opts = OutlineOpts {
as_json: true,
..Default::default()
};
let (o1, _) = run(d, &opts);
let (o2, _) = run(d, &opts);
assert_eq!(o1, o2, "json must be byte-stable");
let v: serde_json::Value = serde_json::from_str(&o1).unwrap();
assert_eq!(v["version"], 1);
let files = v["files"].as_array().unwrap();
assert_eq!(files[0]["path"], "a.rs");
assert_eq!(files[1]["path"], "z.rs");
}