mod common;
use common::sqry_bin;
use serde_json::Value;
use std::ffi::OsStr;
use std::fs;
use std::path::Path;
use std::process::{Command, Output};
use tempfile::TempDir;
fn write_fixture(dir: &Path) {
fs::create_dir_all(dir.join("src")).expect("create src");
fs::write(
dir.join("Cargo.toml"),
r#"[package]
name = "macro-search-fixture"
version = "0.0.1"
edition = "2024"
[features]
alpha = []
beta = []
[lib]
path = "src/lib.rs"
"#,
)
.expect("write Cargo.toml");
fs::write(
dir.join("src/lib.rs"),
r#"pub fn always_present() -> i32 {
42
}
#[cfg(feature = "alpha")]
pub fn alpha_only() -> &'static str {
"alpha"
}
#[cfg(feature = "beta")]
pub fn beta_only() -> &'static str {
"beta"
}
"#,
)
.expect("write src/lib.rs");
}
fn run<I, S>(project: &Path, args: I) -> Output
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
Command::new(sqry_bin())
.args(args)
.current_dir(project)
.env("NO_COLOR", "1")
.env("SQRY_NO_HISTORY", "1")
.env("SQRY_REDACTION_PRESET", "none")
.env("HOME", project.join(".home"))
.env("XDG_CONFIG_HOME", project.join(".xdg/config"))
.env("XDG_CACHE_HOME", project.join(".xdg/cache"))
.env("XDG_DATA_HOME", project.join(".xdg/data"))
.env("XDG_RUNTIME_DIR", project.join(".xdg/runtime"))
.env(
"SQRY_DAEMON_SOCKET",
project.join(".xdg/runtime/sqryd.sock"),
)
.output()
.expect("run sqry")
}
fn assert_success(name: &str, out: &Output) {
assert!(
out.status.success(),
"{name} failed (exit={:?})\nstdout:\n{}\nstderr:\n{}",
out.status.code(),
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr),
);
}
fn parse_json(out: &Output) -> Value {
serde_json::from_slice(&out.stdout).unwrap_or_else(|err| {
panic!(
"expected JSON\nerror: {err}\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr),
)
})
}
fn results(value: &Value) -> &[Value] {
value
.get("results")
.and_then(Value::as_array)
.map(Vec::as_slice)
.unwrap_or(&[])
}
fn setup() -> TempDir {
let temp = TempDir::new().expect("create tempdir");
let project = temp.path();
fs::create_dir_all(project.join(".home")).unwrap();
fs::create_dir_all(project.join(".xdg/config")).unwrap();
fs::create_dir_all(project.join(".xdg/cache")).unwrap();
fs::create_dir_all(project.join(".xdg/data")).unwrap();
fs::create_dir_all(project.join(".xdg/runtime")).unwrap();
write_fixture(project);
let index = run(project, ["index", "."]);
assert_success("index", &index);
temp
}
#[test]
fn search_finds_always_present_baseline() {
let temp = setup();
let out = run(temp.path(), ["--json", "search", "always_present", "."]);
assert_success("search baseline", &out);
let value = parse_json(&out);
let hits = results(&value);
assert!(
hits.iter()
.any(|r| r.get("name").and_then(Value::as_str) == Some("always_present")),
"expected always_present in results: {value}"
);
}
#[test]
fn cfg_filter_drops_non_matching_results() {
let temp = setup();
let out = run(
temp.path(),
[
"--json",
"search",
"always_present",
".",
"--cfg-filter",
"feature = \"this-cfg-does-not-exist\"",
],
);
assert_success("search --cfg-filter bogus", &out);
let value = parse_json(&out);
let hits = results(&value);
let always_present_hits: usize = hits
.iter()
.filter(|r| r.get("name").and_then(Value::as_str) == Some("always_present"))
.count();
assert_eq!(
always_present_hits, 0,
"always_present must be dropped under --cfg-filter when its cfg metadata does not match: {value}"
);
}
#[test]
fn macro_boundaries_emits_group_key_on_every_result() {
let temp = setup();
let out = run(
temp.path(),
[
"--json",
"search",
"always_present",
".",
"--macro-boundaries",
],
);
assert_success("search --macro-boundaries", &out);
let value = parse_json(&out);
let hits = results(&value);
assert!(!hits.is_empty(), "expected non-empty hits: {value}");
for hit in hits {
let metadata = hit.get("metadata").and_then(Value::as_object);
assert!(
metadata.is_some_and(|m| m.contains_key("macro_boundary_group")),
"every macro-boundary result must carry macro_boundary_group: {hit}"
);
}
}