use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
fn scratch(label: &str) -> (PathBuf, PathBuf) {
let base = std::env::temp_dir();
let dir = base.join(format!("rq-e2e-{}-{label}", std::process::id()));
let db = base.join(format!("rq-e2e-{}-{label}.db", std::process::id()));
let _ = fs::remove_dir_all(&dir);
for suffix in ["", "-wal", "-shm"] {
let _ = fs::remove_file(format!("{}{suffix}", db.display()));
}
fs::create_dir_all(&dir).unwrap();
(dir, db)
}
fn rq(db: &Path, cwd: &Path, args: &[&str]) -> (bool, String) {
let out = Command::new(env!("CARGO_BIN_EXE_rq"))
.args(args)
.current_dir(cwd)
.env("RQ_DB", db)
.output()
.expect("run rq");
(
out.status.success(),
String::from_utf8_lossy(&out.stdout).into_owned(),
)
}
fn first_line(s: &str) -> &str {
s.lines().next().unwrap_or("")
}
fn warmed(db: &Path, cwd: &Path, query: &str) -> bool {
let run = Command::new(env!("CARGO_BIN_EXE_rq"))
.args(["-v", query, "--no-record"])
.current_dir(cwd)
.env("RQ_DB", db)
.output()
.expect("run rq");
String::from_utf8_lossy(&run.stderr).contains("background warm")
}
fn git_init(dir: &Path) {
let _ = Command::new("git")
.arg("init")
.arg("-q")
.current_dir(dir)
.output();
}
fn git_init_commit(dir: &Path) {
git_init(dir);
let git = |args: &[&str]| {
let _ = Command::new("git").args(args).current_dir(dir).output();
};
git(&["add", "-A"]);
git(&[
"-c",
"user.email=t@e.st",
"-c",
"user.name=test",
"commit",
"-qm",
"init",
]);
}
#[test]
fn index_search_and_learn_through_the_cli() {
let (dir, db) = scratch("learn");
fs::write(dir.join("alpha.rb"), "class HandlerA\nend\n").unwrap();
fs::write(dir.join("beta.rb"), "class HandlerB\nend\n").unwrap();
let (ok, out) = rq(&db, &dir, &["--index"]);
assert!(ok, "index failed: {out}");
assert!(out.contains("symbol"), "index output: {out}");
let (ok, out) = rq(&db, &dir, &["handler"]);
assert!(ok, "search failed: {out}");
assert!(
first_line(&out).contains("HandlerA"),
"search output: {out}"
);
let (ok, _) = rq(
&db,
&dir,
&["--record", "--file", "beta.rb", "--line", "1", "handler"],
);
assert!(ok, "record failed");
let (ok, out) = rq(&db, &dir, &["handler"]);
assert!(ok, "second search failed: {out}");
assert!(
first_line(&out).contains("HandlerB"),
"after learning, expected HandlerB first: {out}"
);
let (ok, out) = rq(&db, &dir, &["--status"]);
assert!(ok, "status failed: {out}");
assert!(out.contains("local:"), "status output: {out}");
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn a_strong_match_suppresses_the_scattered_tail() {
let (dir, db) = scratch("gate");
fs::write(
dir.join("employees_controller.rb"),
"class EmployeesController\nend\n",
)
.unwrap();
fs::write(
dir.join("employee_status_controller.rb"),
"class EmployeeStatusController\nend\n",
)
.unwrap();
rq(&db, &dir, &["--index"]);
let (_, out) = rq(
&db,
&dir,
&["employeescontroller", "--no-record", "--ndjson"],
);
assert!(out.contains("EmployeesController"), "exact kept: {out}");
assert!(
!out.contains("EmployeeStatusController"),
"scattered fuzzy dropped: {out}"
);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn a_wildcard_bridges_an_explicit_gap() {
let (dir, db) = scratch("wildcard");
fs::write(
dir.join("widget_alpha_bravo_controller.rb"),
"class WidgetAlphaBravoController\nend\n",
)
.unwrap();
fs::write(dir.join("gadget_service.rb"), "class GadgetService\nend\n").unwrap();
rq(&db, &dir, &["--index"]);
let (ok, out) = rq(&db, &dir, &["widget*controller", "--no-record", "--ndjson"]);
assert!(ok, "wildcard search should match: {out}");
assert!(
out.contains("WidgetAlphaBravoController"),
"star bridges the gap: {out}"
);
assert!(!out.contains("GadgetService"), "non-match excluded: {out}");
let (matched, _) = rq(&db, &dir, &["widgetcontroller", "--no-record"]);
assert!(!matched, "plain fuzzy won't skip whole words");
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn cold_index_builds_a_working_fuzzy_index() {
let (dir, db) = scratch("coldfts");
fs::write(dir.join("a.rb"), "class AlphaWidgetController\nend\n").unwrap();
let (ok, out) = rq(&db, &dir, &["--index"]);
assert!(ok, "index failed: {out}");
let (ok, out) = rq(&db, &dir, &["widget", "--no-record"]);
assert!(ok, "fuzzy recall should find it: {out}");
assert!(out.contains("AlphaWidgetController"), "fts recall: {out}");
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn a_compact_namespaced_class_is_found_by_its_leaf_name() {
let (dir, db) = scratch("namespaced");
fs::write(
dir.join("a.rb"),
"class My::Module::EmployeesController\n def index; end\nend\n",
)
.unwrap();
fs::write(
dir.join("b.rb"),
"class EmployeesController\n def show; end\nend\n",
)
.unwrap();
rq(&db, &dir, &["--index"]);
let (ok, out) = rq(
&db,
&dir,
&["employeescontroller", "--no-record", "--ndjson"],
);
assert!(ok, "search failed: {out}");
assert!(out.contains("a.rb"), "namespaced class kept: {out}");
assert!(out.contains("b.rb"), "top-level class kept: {out}");
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn a_clean_complete_repo_does_not_re_warm_on_search() {
let (dir, db) = scratch("nowarm");
fs::write(dir.join("a.rb"), "class Widget\nend\n").unwrap();
git_init_commit(&dir);
rq(&db, &dir, &["--index"]);
assert!(
!warmed(&db, &dir, "widget"),
"clean complete repo should not re-warm"
);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn a_tracked_edit_warms_but_a_new_untracked_file_does_not() {
let (dir, db) = scratch("dirty-check");
fs::write(dir.join("a.rb"), "class Widget\nend\n").unwrap();
git_init_commit(&dir);
rq(&db, &dir, &["--index"]);
fs::write(dir.join("a.rb"), "class Widget\n def go; end\nend\n").unwrap();
assert!(warmed(&db, &dir, "widget"), "tracked edit triggers a warm");
fs::write(dir.join("a.rb"), "class Widget\nend\n").unwrap();
fs::write(dir.join("b.rb"), "class Gadget\nend\n").unwrap();
assert!(
!warmed(&db, &dir, "widget"),
"a new untracked file does not trigger a warm (accepted tradeoff)"
);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn indexing_a_subdir_scopes_to_it_but_keeps_root_relative_paths() {
let (dir, db) = scratch("index-subdir");
fs::create_dir_all(dir.join("sub")).unwrap();
fs::create_dir_all(dir.join("other")).unwrap();
fs::write(dir.join("sub/a.rb"), "class InScope\nend\n").unwrap();
fs::write(dir.join("other/b.rb"), "class OutOfScope\nend\n").unwrap();
git_init_commit(&dir);
let (ok, out) = rq(&db, &dir, &["--index", "sub"]);
assert!(ok, "scoped index failed: {out}");
assert!(out.contains("partial"), "subdir index is partial: {out}");
assert!(
out.contains("1 files"),
"indexed exactly the one in-scope file: {out}"
);
let (ok, out) = rq(&db, &dir, &["inscope", "--no-record", "--ndjson"]);
assert!(ok, "search failed: {out}");
assert!(out.contains("InScope"), "in-scope class indexed: {out}");
assert!(
out.contains("\"file\":\"sub/a.rb\""),
"path is root-relative, not subdir-relative: {out}"
);
let (found, _) = rq(&db, &dir, &["outofscope", "--no-record"]);
assert!(!found, "out-of-scope subtree not indexed");
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn empty_status_points_at_the_real_index_flag() {
let (dir, db) = scratch("empty-status");
let (ok, out) = rq(&db, &dir, &["--status"]);
assert!(ok, "status on an empty db should succeed: {out}");
assert!(out.contains("rq --index"), "hint names the flag: {out}");
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn open_launches_and_records_the_pick() {
let (dir, db) = scratch("open");
fs::write(dir.join("user.rb"), "class User\nend\n").unwrap();
rq(&db, &dir, &["--index"]);
let run = Command::new(env!("CARGO_BIN_EXE_rq"))
.args(["--open", "user"])
.current_dir(&dir)
.env("RQ_DB", &db)
.env("RQ_OPEN", "true")
.output()
.expect("run rq");
assert!(run.status.success(), "open should exit 0 via the launcher");
let run = Command::new(env!("CARGO_BIN_EXE_rq"))
.args(["--open", "user", "--no-record"])
.current_dir(&dir)
.env("RQ_DB", &db)
.env_remove("EDITOR")
.env_remove("VISUAL")
.env("PATH", "/nonexistent") .output()
.expect("run rq");
let printed = String::from_utf8_lossy(&run.stdout);
assert!(
printed.trim().ends_with("user.rb:1"),
"prints resolved path:line: {printed}"
);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn drop_honors_json_output() {
let (dir, db) = scratch("drop-json");
fs::write(dir.join("a.rb"), "class Widget\nend\n").unwrap();
rq(&db, &dir, &["--index"]);
let (ok, out) = rq(&db, &dir, &["--drop", "--json"]);
assert!(ok, "drop --json failed: {out}");
assert!(out.trim_start().starts_with('{'), "json object: {out}");
assert!(out.contains("\"dropped\": true"), "reports dropped: {out}");
let (ok, out) = rq(&db, &dir, &["--drop", "--ndjson"]);
assert!(ok, "second drop should not error: {out}");
let line = out.lines().next().unwrap_or("");
assert!(
line.starts_with('{') && line.contains("\"dropped\":false"),
"ndjson dropped:false: {out}"
);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn drop_removes_a_repos_index() {
let (dir, db) = scratch("drop");
fs::write(dir.join("a.rb"), "class Widget\nend\n").unwrap();
rq(&db, &dir, &["--index"]);
let (_, before) = rq(&db, &dir, &["--status"]);
assert!(before.contains("symbol"), "indexed before drop: {before}");
let (ok, out) = rq(&db, &dir, &["--drop"]);
assert!(ok, "drop failed: {out}");
assert!(out.contains("dropped"), "drop confirms: {out}");
let (ok2, after) = rq(&db, &dir, &["--status"]);
assert!(ok2, "status after drop: {after}");
assert!(
!after.contains("symbol"),
"coverage gone after drop: {after}"
);
let (ok3, again) = rq(&db, &dir, &["--drop"]);
assert!(ok3, "second drop should not error: {again}");
assert!(again.contains("not indexed"), "idempotent message: {again}");
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn record_is_a_searchable_word_not_a_subcommand() {
let (dir, db) = scratch("disambig");
fs::write(dir.join("a.rb"), "class Widget\nend\n").unwrap();
rq(&db, &dir, &["--index"]);
let (ok, out) = rq(&db, &dir, &["record"]);
assert!(!ok, "no-match search should exit non-zero");
assert!(out.is_empty(), "expected no result lines, got: {out}");
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn status_and_index_honor_json_output() {
let (dir, db) = scratch("ops-json");
fs::write(dir.join("a.rb"), "class Widget\nend\n").unwrap();
let (ok, out) = rq(&db, &dir, &["--index", "--json"]);
assert!(ok, "index --json failed: {out}");
assert!(
out.trim_start().starts_with('{'),
"index json object: {out}"
);
assert!(out.contains("\"files_added\""), "index run counts: {out}");
assert!(out.contains("\"repo\""), "index repo field: {out}");
let (ok, out) = rq(&db, &dir, &["--status", "--json"]);
assert!(ok, "status --json failed: {out}");
assert!(
out.trim_start().starts_with('['),
"status json array: {out}"
);
assert!(
out.contains("\"status\": \"complete\""),
"status field: {out}"
);
let (ok, out) = rq(&db, &dir, &["--status", "--ndjson"]);
assert!(ok, "status -J failed: {out}");
let line = out.lines().next().unwrap_or("");
assert!(
line.starts_with('{') && line.ends_with('}') && line.contains("\"repo\""),
"ndjson object per line: {out}"
);
rq(&db, &dir, &["--drop"]);
let (ok, out) = rq(&db, &dir, &["--status", "--json"]);
assert!(ok && out.trim() == "[]", "empty status json is []: {out}");
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn json_and_ndjson_output() {
let (dir, db) = scratch("json");
fs::write(dir.join("alpha.rb"), "class HandlerA\nend\n").unwrap();
rq(&db, &dir, &["--index"]);
let (ok, out) = rq(&db, &dir, &["handler", "--json"]);
assert!(ok, "json search failed: {out}");
assert!(
out.trim_start().starts_with('['),
"expected a JSON array: {out}"
);
assert!(out.contains("\"name\": \"HandlerA\""), "name field: {out}");
assert!(out.contains("\"file\": \"alpha.rb\""), "file field: {out}");
assert!(out.contains("\"repo\":"), "repo field: {out}");
assert!(
out.contains("\"signature\": \"class HandlerA\""),
"signature: {out}"
);
let (ok, out) = rq(&db, &dir, &["handler", "--ndjson"]);
assert!(ok, "ndjson search failed: {out}");
let first = out.lines().next().unwrap_or("");
assert!(
first.starts_with('{') && first.ends_with('}'),
"object per line: {out}"
);
assert!(
first.contains("\"name\":\"HandlerA\""),
"compact name: {out}"
);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn path_filter_restricts_results() {
let (dir, db) = scratch("path");
fs::create_dir_all(dir.join("app/services")).unwrap();
fs::create_dir_all(dir.join("app/models")).unwrap();
fs::write(
dir.join("app/services/widget.rb"),
"class WidgetService\nend\n",
)
.unwrap();
fs::write(dir.join("app/models/widget.rb"), "class Widget\nend\n").unwrap();
rq(&db, &dir, &["--index"]);
let (_, out) = rq(&db, &dir, &["widget", "--ndjson"]);
assert!(
out.contains("app/models/widget.rb"),
"expected models hit: {out}"
);
assert!(
out.contains("app/services/widget.rb"),
"expected services hit: {out}"
);
let (ok, out) = rq(&db, &dir, &["widget", "--path", "app/services", "--ndjson"]);
assert!(ok, "path search failed: {out}");
assert!(
out.contains("app/services/widget.rb"),
"services hit kept: {out}"
);
assert!(
!out.contains("app/models/widget.rb"),
"models hit filtered out: {out}"
);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn path_filter_accepts_absolute_and_relative_paths() {
let (dir, db) = scratch("path-abs");
fs::create_dir_all(dir.join("app/services")).unwrap();
fs::create_dir_all(dir.join("app/models")).unwrap();
fs::write(
dir.join("app/services/widget.rb"),
"class WidgetService\nend\n",
)
.unwrap();
fs::write(dir.join("app/models/widget.rb"), "class Widget\nend\n").unwrap();
rq(&db, &dir, &["--index"]);
let abs = dir.join("app/services");
let abs = abs.to_str().unwrap();
for spec in [abs, "./app/services", "app/services"] {
let (ok, out) = rq(&db, &dir, &["widget", "--path", spec, "--ndjson"]);
assert!(ok, "path search failed for {spec:?}: {out}");
assert!(
out.contains("app/services/widget.rb"),
"services kept for {spec:?}: {out}"
);
assert!(
!out.contains("app/models/widget.rb"),
"models filtered for {spec:?}: {out}"
);
}
let (_, out) = rq(
&db,
&dir,
&["widget", "--path", "/nonexistent/elsewhere", "--ndjson"],
);
assert!(out.trim().is_empty(), "outside path yields no hits: {out}");
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn limit_caps_the_number_of_results() {
let (dir, db) = scratch("limit");
fs::write(dir.join("a.rb"), "class HandlerA\nend\n").unwrap();
fs::write(dir.join("b.rb"), "class HandlerB\nend\n").unwrap();
rq(&db, &dir, &["--index"]);
let (ok, out) = rq(&db, &dir, &["handler", "--limit", "1", "--ndjson"]);
assert!(ok, "limited search failed: {out}");
assert_eq!(out.lines().count(), 1, "expected exactly one result: {out}");
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn index_a_subset_of_a_repo() {
let (dir, db) = scratch("subset");
fs::create_dir_all(dir.join("app/services")).unwrap();
fs::create_dir_all(dir.join("app/models")).unwrap();
fs::write(dir.join("app/services/charge.rb"), "class Charge\nend\n").unwrap();
fs::write(dir.join("app/models/account.rb"), "class Account\nend\n").unwrap();
git_init(&dir);
let (ok, out) = rq(&db, &dir, &["--index", "--path", "app/services"]);
assert!(ok, "subset index failed: {out}");
assert!(out.contains("(partial)"), "expected partial marker: {out}");
let (ok, out) = rq(&db, &dir, &["charge", "--ndjson"]);
assert!(ok, "charge search failed: {out}");
assert!(
out.contains("\"file\":\"app/services/charge.rb\""),
"subset hit: {out}"
);
let (ok, _) = rq(&db, &dir, &["account"]);
assert!(!ok, "account is outside the indexed subset, should miss");
let (_, status) = rq(&db, &dir, &["--status"]);
assert!(
status.contains("partial"),
"coverage stays partial: {status}"
);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn positional_paths_filter_like_rg() {
let (dir, db) = scratch("pospath");
fs::create_dir_all(dir.join("app/services")).unwrap();
fs::create_dir_all(dir.join("app/models")).unwrap();
fs::write(
dir.join("app/services/widget.rb"),
"class WidgetService\nend\n",
)
.unwrap();
fs::write(dir.join("app/models/widget.rb"), "class Widget\nend\n").unwrap();
rq(&db, &dir, &["--index"]);
let (ok, out) = rq(&db, &dir, &["widget", "app/services", "--ndjson"]);
assert!(ok, "positional-path search failed: {out}");
assert!(
out.contains("app/services/widget.rb"),
"services hit kept: {out}"
);
assert!(
!out.contains("app/models/widget.rb"),
"models hit filtered out: {out}"
);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn no_record_still_returns_results() {
let (dir, db) = scratch("norec");
fs::write(dir.join("a.rb"), "class Widget\nend\n").unwrap();
rq(&db, &dir, &["--index"]);
let (ok, out) = rq(&db, &dir, &["widget", "--no-record", "--ndjson"]);
assert!(ok, "no-record search failed: {out}");
assert!(out.contains("\"name\":\"Widget\""), "result present: {out}");
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn kind_filter_scopes_by_symbol_kind() {
let (dir, db) = scratch("kind");
fs::write(dir.join("a.rb"), "class Charge\n def charge\n end\nend\n").unwrap();
rq(&db, &dir, &["--index"]);
let (_, out) = rq(&db, &dir, &["charge", "--ndjson"]);
assert!(out.contains("\"kind\":\"class\""), "class present: {out}");
assert!(out.contains("\"kind\":\"method\""), "method present: {out}");
let (ok, out) = rq(&db, &dir, &["charge", "-k", "m", "--ndjson"]);
assert!(ok, "kind search failed: {out}");
assert!(out.contains("\"kind\":\"method\""), "method kept: {out}");
assert!(
!out.contains("\"kind\":\"class\""),
"class filtered out: {out}"
);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn first_query_warms_the_index_without_an_explicit_reindex() {
let (dir, db) = scratch("warm");
git_init(&dir);
fs::write(dir.join("widget.rb"), "class Widget\nend\n").unwrap();
let (ok, out) = rq(&db, &dir, &["widget", "--ndjson"]);
assert!(ok, "cold search failed: {out}");
assert!(out.contains("\"name\":\"Widget\""), "result present: {out}");
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn explicitly_indexes_and_recognizes_a_non_git_directory() {
let (dir, db) = scratch("nongit");
fs::write(dir.join("widget.rb"), "class Widget\nend\n").unwrap();
let (ok, out) = rq(&db, &dir, &["--index"]);
assert!(ok, "index of a non-git dir failed: {out}");
let (ok, out) = rq(&db, &dir, &["widget", "--ndjson"]);
assert!(ok, "search failed: {out}");
assert!(out.contains("\"name\":\"Widget\""), "result present: {out}");
assert!(
out.contains("current_repo"),
"current-repo boost applied: {out}"
);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn lang_filter_scopes_by_language() {
let (dir, db) = scratch("lang");
fs::write(dir.join("widget.rb"), "class Widget\nend\n").unwrap();
fs::write(dir.join("widget.rs"), "pub struct Widget {}\n").unwrap();
rq(&db, &dir, &["--index"]);
let (_, out) = rq(&db, &dir, &["widget", "--ndjson"]);
assert!(out.contains("\"language\":\"ruby\""), "ruby present: {out}");
assert!(out.contains("\"language\":\"rust\""), "rust present: {out}");
let (ok, out) = rq(&db, &dir, &["widget", "-x", "rs", "--ndjson"]);
assert!(ok, "lang search failed: {out}");
assert!(out.contains("\"language\":\"rust\""), "rust kept: {out}");
assert!(
!out.contains("\"language\":\"ruby\""),
"ruby filtered: {out}"
);
let (_, out) = rq(&db, &dir, &["widget", "-x", "r", "--ndjson"]);
assert!(
out.contains("\"language\":\"ruby\"") && out.contains("\"language\":\"rust\""),
"both kept for -x r: {out}"
);
let (_, out) = rq(&db, &dir, &["widget", "-x", "ruby", "--ndjson"]);
assert!(out.contains("\"language\":\"ruby\""), "ruby kept: {out}");
assert!(
!out.contains("\"language\":\"rust\""),
"rust filtered: {out}"
);
let (ok, out) = rq(&db, &dir, &["widget", "-x", "py", "--ndjson"]);
assert!(!ok, "no python here, should exit non-zero");
assert!(out.is_empty(), "expected no results: {out}");
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn searching_from_a_subdirectory_uses_the_repo_root() {
let (dir, db) = scratch("subdir");
git_init(&dir);
let sub = dir.join("nested");
fs::create_dir_all(&sub).unwrap();
fs::write(dir.join("top.rb"), "class TopWidget\nend\n").unwrap();
fs::write(sub.join("deep.rb"), "class DeepWidget\nend\n").unwrap();
let (ok, _) = rq(&db, &dir, &["--index"]);
assert!(ok);
let (_, out) = rq(&db, &dir, &["DeepWidget", "--no-record"]);
assert!(out.contains("nested/deep.rb"), "root-relative path: {out}");
let (_, out) = rq(&db, &sub, &["TopWidget", "--no-record"]);
assert!(
out.contains("TopWidget"),
"top symbol found from subdir: {out}"
);
let (_, status) = rq(&db, &dir, &["--status"]);
assert_eq!(
status.lines().count(),
1,
"one repo, not re-keyed: {status}"
);
assert!(status.contains("2 files"), "both files retained: {status}");
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn warming_a_committed_repo_indexes_tracked_source() {
let (dir, db) = scratch("gitwarm");
fs::create_dir_all(dir.join("lib")).unwrap();
fs::write(dir.join("lib/widget.rb"), "class Widget\nend\n").unwrap();
fs::write(dir.join("README.md"), "# docs, not source\n").unwrap();
git_init_commit(&dir);
let (ok, out) = rq(&db, &dir, &["Widget", "--no-record"]);
assert!(ok, "warmed search failed: {out}");
assert!(
out.contains("lib/widget.rb"),
"tracked source warmed: {out}"
);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn symbols_outlines_a_file_in_line_order() {
let (dir, db) = scratch("symbols");
fs::write(
dir.join("widget.rb"),
"class Widget\n def build\n end\n def render\n end\nend\n",
)
.unwrap();
fs::write(dir.join("other.rb"), "class Other\nend\n").unwrap();
git_init_commit(&dir);
let (ok, out) = rq(&db, &dir, &["--symbols", "widget.rb", "--ndjson"]);
assert!(ok, "symbols failed: {out}");
let lines: Vec<&str> = out.lines().collect();
assert_eq!(lines.len(), 3, "class + two methods: {out}");
assert!(
first_line(&out).contains("\"name\":\"Widget\""),
"class first: {out}"
);
assert!(out.contains("\"name\":\"build\""), "build present: {out}");
assert!(
out.contains("\"parent\":\"Widget\""),
"method nests under class: {out}"
);
assert!(
out.contains("\"signature\":\"def build\""),
"signature read: {out}"
);
assert!(!out.contains("Other"), "other file excluded: {out}");
let (ok, out) = rq(
&db,
&dir,
&["--symbols", "widget.rb", "-k", "method", "--ndjson"],
);
assert!(ok, "filtered symbols failed: {out}");
assert_eq!(out.lines().count(), 2, "two methods only: {out}");
assert!(
!out.contains("\"name\":\"Widget\""),
"class filtered out: {out}"
);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn bare_invocation_prints_help() {
let (dir, db) = scratch("help");
let (ok, out) = rq(&db, &dir, &[]);
assert!(ok, "bare rq should exit 0");
assert!(
out.contains("rq finds where a symbol is defined"),
"help banner: {out}"
);
assert!(out.contains("Usage:"), "usage in help: {out}");
let _ = fs::remove_dir_all(&dir);
}