mod diff;
mod index;
mod query;
mod status;
pub use diff::run_diff;
pub use index::run_init;
pub(crate) use index::{discover_source_files, language_for_path, should_skip_dir};
pub(crate) use query::run_impact;
pub use query::{
run_callees, run_callers, run_explore, run_howto, run_node, run_search, run_tests_for,
};
pub(crate) use status::indexed_sources_are_fresh;
pub use status::{run_doctor, run_files, run_status, run_unresolved};
#[cfg(test)]
mod tests {
use super::*;
use crate::db;
use rusqlite::Connection;
use std::fs;
use std::path::PathBuf;
fn create_temp_dir() -> PathBuf {
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
static COUNTER: AtomicUsize = AtomicUsize::new(0);
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos();
let count = COUNTER.fetch_add(1, Ordering::SeqCst);
let path = std::env::temp_dir().join(format!("ochna_test_{}_{}", now, count));
fs::create_dir_all(&path).unwrap();
path
}
#[test]
fn test_commands_workflow() {
let temp_workspace = create_temp_dir();
let src_dir = temp_workspace.join("src");
fs::create_dir_all(&src_dir).unwrap();
let rust_file = src_dir.join("main.rs");
let rust_code = r#"
/// A main entry point.
fn main() {
helper();
}
fn helper() {
println!("hello");
}
"#;
fs::write(&rust_file, rust_code).unwrap();
let go_file = temp_workspace.join("main.go");
let go_code = r#"
package main
import "fmt"
// GoHelper function
func GoHelper() {
fmt.Println("go helper")
}
"#;
fs::write(&go_file, go_code).unwrap();
let c_file = temp_workspace.join("main.c");
let c_code = r#"
int c_helper(void) {
return 1;
}
"#;
fs::write(&c_file, c_code).unwrap();
let cpp_file = temp_workspace.join("main.cpp");
let cpp_code = r#"
int cpp_helper() {
return 2;
}
"#;
fs::write(&cpp_file, cpp_code).unwrap();
let zig_file = temp_workspace.join("main.zig");
let zig_code = r#"
fn zigHelper() i32 {
return 3;
}
"#;
fs::write(&zig_file, zig_code).unwrap();
let ignored_dir = temp_workspace.join(".git");
fs::create_dir_all(&ignored_dir).unwrap();
fs::write(ignored_dir.join("config"), "dummy content").unwrap();
let target_dir = temp_workspace.join("target");
fs::create_dir_all(&target_dir).unwrap();
fs::write(target_dir.join("binary.rs"), "dummy rust in target").unwrap();
run_init(&temp_workspace, false).unwrap();
let db_path = temp_workspace.join(".ochna").join("ochna.db");
assert!(db_path.exists());
let agent_pointer = temp_workspace.join(".ochna").join("AGENT.md");
assert!(agent_pointer.exists());
let pointer_text = fs::read_to_string(&agent_pointer).unwrap();
assert!(pointer_text.contains("generated by ochna init"));
assert!(pointer_text.contains("ochna howto"));
let conn = Connection::open(&db_path).unwrap();
let files_count: i64 = conn
.query_row("SELECT COUNT(*) FROM files", [], |row| row.get(0))
.unwrap();
let nodes_count: i64 = conn
.query_row("SELECT COUNT(*) FROM nodes", [], |row| row.get(0))
.unwrap();
assert_eq!(files_count, 5);
assert_eq!(nodes_count, 6);
run_status(&temp_workspace, false).unwrap();
run_status(&temp_workspace, true).unwrap();
run_howto(false).unwrap();
run_howto(true).unwrap();
run_files(&temp_workspace, false).unwrap();
run_files(&temp_workspace, true).unwrap();
run_search(&temp_workspace, "helper", false, false, 30).unwrap();
run_search(&temp_workspace, "helper", true, false, 30).unwrap();
run_callers(&temp_workspace, "helper", false, false, None, false, None).unwrap();
run_callers(&temp_workspace, "helper", true, false, None, false, None).unwrap();
run_callees(&temp_workspace, "helper", false, false, None, false, None).unwrap();
run_callees(&temp_workspace, "helper", true, false, None, false, None).unwrap();
run_callers(
&temp_workspace,
"helper",
false,
false,
None,
false,
Some("src"),
)
.unwrap();
run_callees(
&temp_workspace,
"helper",
false,
false,
None,
false,
Some("src"),
)
.unwrap();
run_node(
&temp_workspace,
Some("src/main.rs".to_string()),
Some(1),
Some(10),
false,
None,
false,
None,
false,
false,
false,
)
.unwrap();
run_node(
&temp_workspace,
Some("src/main.rs".to_string()),
None,
None,
true,
None,
false,
None,
false,
false,
false,
)
.unwrap();
run_node(
&temp_workspace,
None,
None,
None,
false,
Some("helper".to_string()),
true,
None,
false,
false,
false,
)
.unwrap();
run_node(
&temp_workspace,
None,
None,
None,
false,
Some("helper".to_string()),
true,
None,
true,
false,
false,
)
.unwrap();
run_node(
&temp_workspace,
None,
None,
None,
false,
Some("helper".to_string()),
true,
Some(6),
false,
false,
false,
)
.unwrap();
run_explore(&temp_workspace, "helper", false, false, false).unwrap();
run_explore(&temp_workspace, "helper", true, false, false).unwrap();
let rust_code_modified = r#"
/// Modified main entry point.
fn main() {
// calls deleted helper
}
"#;
fs::write(&rust_file, rust_code_modified).unwrap();
run_init(&temp_workspace, false).unwrap();
let nodes_count_after: i64 = conn
.query_row("SELECT COUNT(*) FROM nodes", [], |row| row.get(0))
.unwrap();
assert_eq!(nodes_count_after, 5);
fs::remove_dir_all(&temp_workspace).unwrap();
}
#[test]
fn test_status_json_fails_when_git_baseline_is_stale() {
let temp_workspace = create_temp_dir();
fs::write(temp_workspace.join(".gitignore"), ".ochna\n").unwrap();
fs::write(temp_workspace.join("main.rs"), "fn first() {}\n").unwrap();
std::process::Command::new("git")
.args(["init"])
.current_dir(&temp_workspace)
.output()
.unwrap();
std::process::Command::new("git")
.args(["config", "user.email", "ochna@example.invalid"])
.current_dir(&temp_workspace)
.output()
.unwrap();
std::process::Command::new("git")
.args(["config", "user.name", "Ochna Test"])
.current_dir(&temp_workspace)
.output()
.unwrap();
std::process::Command::new("git")
.args(["add", ".gitignore", "main.rs"])
.current_dir(&temp_workspace)
.output()
.unwrap();
std::process::Command::new("git")
.args(["commit", "-m", "test baseline"])
.current_dir(&temp_workspace)
.output()
.unwrap();
run_init(&temp_workspace, false).unwrap();
run_status(&temp_workspace, true).unwrap();
fs::write(temp_workspace.join("main.rs"), "fn second() {}\n").unwrap();
let err = run_status(&temp_workspace, true).unwrap_err();
assert!(err.to_string().contains("ochna sync"));
fs::remove_dir_all(&temp_workspace).unwrap();
}
#[test]
fn test_status_json_fresh_immediately_after_init_on_dirty_worktree() {
let temp_workspace = create_temp_dir();
fs::write(temp_workspace.join(".gitignore"), ".ochna\n").unwrap();
fs::write(temp_workspace.join("main.rs"), "fn first() {}\n").unwrap();
std::process::Command::new("git")
.args(["init"])
.current_dir(&temp_workspace)
.output()
.unwrap();
std::process::Command::new("git")
.args(["config", "user.email", "ochna@example.invalid"])
.current_dir(&temp_workspace)
.output()
.unwrap();
std::process::Command::new("git")
.args(["config", "user.name", "Ochna Test"])
.current_dir(&temp_workspace)
.output()
.unwrap();
std::process::Command::new("git")
.args(["add", ".gitignore", "main.rs"])
.current_dir(&temp_workspace)
.output()
.unwrap();
std::process::Command::new("git")
.args(["commit", "-m", "test baseline"])
.current_dir(&temp_workspace)
.output()
.unwrap();
fs::write(temp_workspace.join("main.rs"), "fn second() {}\n").unwrap();
run_init(&temp_workspace, false).unwrap();
run_status(&temp_workspace, true).unwrap();
fs::remove_dir_all(&temp_workspace).unwrap();
}
#[test]
fn test_cross_file_edges_and_unresolved() {
let temp_workspace = create_temp_dir();
let src_dir = temp_workspace.join("src");
fs::create_dir_all(&src_dir).unwrap();
fs::write(
src_dir.join("a.rs"),
"fn caller() {\n target();\n missing();\n}\n",
)
.unwrap();
fs::write(src_dir.join("b.rs"), "fn target() {}\n").unwrap();
run_init(&temp_workspace, false).unwrap();
let db_path = temp_workspace.join(".ochna").join("ochna.db");
let conn = Connection::open(&db_path).unwrap();
let cross_file: i64 = conn
.query_row(
"SELECT COUNT(*) FROM edges e \
JOIN nodes s ON e.source_nid = s.nid \
JOIN nodes t ON e.target_nid = t.nid \
WHERE s.file_path <> t.file_path",
[],
|row| row.get(0),
)
.unwrap();
assert_eq!(cross_file, 1, "expected one cross-file call edge");
let unresolved: i64 = conn
.query_row(
"SELECT COUNT(*) FROM unresolved_refs WHERE specifier = 'missing'",
[],
|row| row.get(0),
)
.unwrap();
assert_eq!(unresolved, 1, "expected one unresolved reference");
fs::remove_dir_all(&temp_workspace).unwrap();
}
#[test]
fn test_scope_classification_tags_tests_and_skips_libraries() {
let temp_workspace = create_temp_dir();
fs::create_dir_all(temp_workspace.join("src/test/java")).unwrap();
fs::create_dir_all(temp_workspace.join("tests")).unwrap();
fs::create_dir_all(temp_workspace.join("vendor")).unwrap();
fs::create_dir_all(temp_workspace.join("target")).unwrap();
fs::create_dir_all(temp_workspace.join("clones")).unwrap();
fs::write(temp_workspace.join("src/main.rs"), "fn app_main() {}\n").unwrap();
fs::write(
temp_workspace.join("tests/parser.rs"),
"fn parser_test() {}\n",
)
.unwrap();
fs::write(
temp_workspace.join("client_test.go"),
"package main\nfunc TestClient() {}\n",
)
.unwrap();
fs::write(
temp_workspace.join("src/test/java/AppTest.java"),
"public class AppTest { public void runs() {} }\n",
)
.unwrap();
fs::write(temp_workspace.join("vendor/lib.rs"), "fn vendored() {}\n").unwrap();
fs::write(
temp_workspace.join("target/generated.rs"),
"fn generated() {}\n",
)
.unwrap();
fs::write(temp_workspace.join("clones/nested.rs"), "fn nested() {}\n").unwrap();
run_init(&temp_workspace, false).unwrap();
let db_path = temp_workspace.join(".ochna").join("ochna.db");
let conn = Connection::open(&db_path).unwrap();
let files_count: i64 = conn
.query_row("SELECT COUNT(*) FROM files", [], |row| row.get(0))
.unwrap();
assert_eq!(files_count, 4, "library directories should be skipped");
let test_files: i64 = conn
.query_row("SELECT COUNT(*) FROM files WHERE is_test = 1", [], |row| {
row.get(0)
})
.unwrap();
assert_eq!(test_files, 3);
let test_nodes: i64 = conn
.query_row("SELECT COUNT(*) FROM nodes WHERE is_test = 1", [], |row| {
row.get(0)
})
.unwrap();
assert!(test_nodes >= 3);
run_init(&temp_workspace, true).unwrap();
let library_files: i64 = conn
.query_row(
"SELECT COUNT(*) FROM files WHERE file_path IN ('vendor/lib.rs', 'target/generated.rs', 'clones/nested.rs')",
[],
|row| row.get(0),
)
.unwrap();
assert_eq!(
library_files, 3,
"--include-library should index library dirs"
);
fs::remove_dir_all(&temp_workspace).unwrap();
}
#[test]
fn test_fresh_init_rebuilds_fts_index() {
let temp_workspace = create_temp_dir();
let src_dir = temp_workspace.join("src");
fs::create_dir_all(&src_dir).unwrap();
fs::write(
src_dir.join("main.rs"),
"/// Performs a searchable calibration.\nfn calibrate() {}\n",
)
.unwrap();
run_init(&temp_workspace, false).unwrap();
let db_path = temp_workspace.join(".ochna").join("ochna.db");
let conn = Connection::open(&db_path).unwrap();
let fts_results = db::search_nodes_fts(&conn, "calibration").unwrap();
assert_eq!(fts_results.len(), 1);
assert_eq!(fts_results[0].name, "calibrate");
fs::remove_dir_all(&temp_workspace).unwrap();
}
#[test]
fn test_incremental_sync_keeps_fts_triggers_after_fresh_rebuild() {
let temp_workspace = create_temp_dir();
let src_dir = temp_workspace.join("src");
fs::create_dir_all(&src_dir).unwrap();
let rust_file = src_dir.join("main.rs");
fs::write(
&rust_file,
"/// Mentions the original marker.\nfn searchable() {}\n",
)
.unwrap();
run_init(&temp_workspace, false).unwrap();
let db_path = temp_workspace.join(".ochna").join("ochna.db");
let conn = Connection::open(&db_path).unwrap();
assert_eq!(db::search_nodes_fts(&conn, "original").unwrap().len(), 1);
fs::write(
&rust_file,
"/// Mentions the replacement marker.\nfn searchable() {}\n",
)
.unwrap();
run_init(&temp_workspace, false).unwrap();
assert_eq!(db::search_nodes_fts(&conn, "replacement").unwrap().len(), 1);
assert!(
db::search_nodes_fts(&conn, "original").unwrap().is_empty(),
"updated file should remove stale FTS content"
);
fs::remove_file(&rust_file).unwrap();
run_init(&temp_workspace, false).unwrap();
assert!(
db::search_nodes_fts(&conn, "replacement")
.unwrap()
.is_empty(),
"deleted file should remove FTS content"
);
fs::remove_dir_all(&temp_workspace).unwrap();
}
#[test]
fn test_incremental_sync_re_resolves_unmodified_incoming_callers() {
let temp_workspace = create_temp_dir();
let src_dir = temp_workspace.join("src");
fs::create_dir_all(&src_dir).unwrap();
let caller_file = src_dir.join("a.rs");
let old_target_file = src_dir.join("b.rs");
let new_target_file = src_dir.join("c.rs");
fs::write(
&caller_file,
"fn caller() {\n target();\n local_keep();\n}\nfn local_keep() {}\n",
)
.unwrap();
fs::write(
src_dir.join("incoming.rs"),
"fn incoming() {\n caller();\n}\n",
)
.unwrap();
fs::write(&old_target_file, "fn target() {}\n").unwrap();
run_init(&temp_workspace, false).unwrap();
fs::write(&old_target_file, "fn other() {}\n").unwrap();
fs::write(&new_target_file, "fn target() {}\n").unwrap();
run_init(&temp_workspace, false).unwrap();
let db_path = temp_workspace.join(".ochna").join("ochna.db");
let conn = Connection::open(&db_path).unwrap();
let moved_edge: i64 = conn
.query_row(
"SELECT COUNT(*) FROM edges
WHERE source_nid = (SELECT nid FROM nodes WHERE id = 'src/a.rs::caller')
AND target_nid = (SELECT nid FROM nodes WHERE id = 'src/c.rs::target')",
[],
|row| row.get(0),
)
.unwrap();
assert_eq!(
moved_edge, 1,
"unmodified caller should point at new target"
);
let stale_edge: i64 = conn
.query_row(
"SELECT COUNT(*) FROM edges
WHERE source_nid = (SELECT nid FROM nodes WHERE id = 'src/a.rs::caller')
AND target_nid = (SELECT nid FROM nodes WHERE id = 'src/b.rs::target')",
[],
|row| row.get(0),
)
.unwrap();
assert_eq!(stale_edge, 0, "stale target edge should be removed");
let preserved_edge: i64 = conn
.query_row(
"SELECT COUNT(*) FROM edges
WHERE source_nid = (SELECT nid FROM nodes WHERE id = 'src/a.rs::caller')
AND target_nid = (SELECT nid FROM nodes WHERE id = 'src/a.rs::local_keep')",
[],
|row| row.get(0),
)
.unwrap();
assert_eq!(
preserved_edge, 1,
"other edges from the source are reinserted"
);
let preserved_incoming_edge: i64 = conn
.query_row(
"SELECT COUNT(*) FROM edges
WHERE source_nid = (SELECT nid FROM nodes WHERE id = 'src/incoming.rs::incoming')
AND target_nid = (SELECT nid FROM nodes WHERE id = 'src/a.rs::caller')",
[],
|row| row.get(0),
)
.unwrap();
assert_eq!(
preserved_incoming_edge, 1,
"replaying a caller must not delete unrelated incoming edges"
);
fs::remove_dir_all(&temp_workspace).unwrap();
}
#[test]
fn test_incremental_sync_re_resolves_matching_unresolved_refs() {
let temp_workspace = create_temp_dir();
let src_dir = temp_workspace.join("src");
fs::create_dir_all(&src_dir).unwrap();
fs::write(src_dir.join("a.rs"), "fn caller() {\n missing();\n}\n").unwrap();
run_init(&temp_workspace, false).unwrap();
let db_path = temp_workspace.join(".ochna").join("ochna.db");
let conn = Connection::open(&db_path).unwrap();
let unresolved_before: i64 = conn
.query_row(
"SELECT COUNT(*) FROM unresolved_refs WHERE specifier = 'missing'",
[],
|row| row.get(0),
)
.unwrap();
assert_eq!(unresolved_before, 1);
fs::write(src_dir.join("b.rs"), "fn missing() {}\n").unwrap();
run_init(&temp_workspace, false).unwrap();
let resolved_edge: i64 = conn
.query_row(
"SELECT COUNT(*) FROM edges
WHERE source_nid = (SELECT nid FROM nodes WHERE id = 'src/a.rs::caller')
AND target_nid = (SELECT nid FROM nodes WHERE id = 'src/b.rs::missing')",
[],
|row| row.get(0),
)
.unwrap();
assert_eq!(resolved_edge, 1);
let unresolved_after: i64 = conn
.query_row(
"SELECT COUNT(*) FROM unresolved_refs WHERE specifier = 'missing'",
[],
|row| row.get(0),
)
.unwrap();
assert_eq!(unresolved_after, 0);
fs::remove_dir_all(&temp_workspace).unwrap();
}
#[test]
fn test_call_resolution_baseline_fixtures() {
let temp_workspace = create_temp_dir();
let src_dir = temp_workspace.join("src");
fs::create_dir_all(&src_dir).unwrap();
let go_code = r#"
package main
type Cacher struct {}
func (c *Cacher) GetList() {}
func (c *Cacher) Add() {}
func (c *Cacher) Run() {}
type Queue struct {}
func (q *Queue) GetList() {}
func (q *Queue) Add() {}
func (q *Queue) Run() {}
func Run() {}
func work() {
c := &Cacher{}
c.GetList()
c.Add()
c.Run()
q := &Queue{}
q.GetList()
q.Add()
q.Run()
Run()
}
"#;
fs::write(src_dir.join("main.go"), go_code).unwrap();
let java_code_app = r#"
package demo;
import demo.StaticHelper;
class Promise {
public void release() {}
public void tryFailure() {}
public void run() {}
}
class TrafficHandler {
public void release() {}
public void tryFailure() {}
public void run() {}
}
public class App {
public static void main(String[] args) {
Promise promise = new Promise();
promise.release();
promise.tryFailure();
promise.run();
TrafficHandler handler = new TrafficHandler();
handler.release();
handler.tryFailure();
handler.run();
StaticHelper.run();
}
}
"#;
fs::write(src_dir.join("App.java"), java_code_app).unwrap();
let java_code_helper = r#"
package demo;
public class StaticHelper {
public static void run() {}
}
"#;
fs::write(src_dir.join("StaticHelper.java"), java_code_helper).unwrap();
let c_code = r#"
void helper(void) {}
#define MY_MACRO(x) x
int main(void) {
helper();
MY_MACRO(1);
void (*ptr)(void) = helper;
ptr();
return 0;
}
"#;
fs::write(src_dir.join("main.c"), c_code).unwrap();
run_init(&temp_workspace, false).unwrap();
let db_path = temp_workspace.join(".ochna").join("ochna.db");
let conn = Connection::open(&db_path).unwrap();
let mut stmt = conn
.prepare(
"SELECT (SELECT id FROM nodes WHERE nid = source_nid) as src, \
(SELECT id FROM nodes WHERE nid = target_nid) as tgt \
FROM edges ORDER BY src, tgt",
)
.unwrap();
let rows = stmt
.query_map([], |row| {
Ok((
row.get::<_, Option<String>>(0)?.unwrap_or_default(),
row.get::<_, Option<String>>(1)?.unwrap_or_default(),
))
})
.unwrap();
let mut edges_resolved = Vec::new();
for r in rows {
let (src, tgt) = r.unwrap();
edges_resolved.push(format!("{} -> {}", src, tgt));
}
let mut stmt = conn
.prepare("SELECT (SELECT id FROM nodes WHERE nid = source_nid), specifier FROM unresolved_refs ORDER BY specifier")
.unwrap();
let unresolved_rows = stmt
.query_map([], |row| {
Ok((
row.get::<_, Option<String>>(0)?.unwrap_or_default(),
row.get::<_, String>(1)?,
))
})
.unwrap();
let mut unresolved = Vec::new();
for r in unresolved_rows {
let (src, specifier) = r.unwrap();
unresolved.push(format!("{} -> (unresolved) {}", src, specifier));
}
let mut stmt_kinds = conn
.prepare(
"SELECT (SELECT id FROM nodes WHERE nid = source_nid) as src, \
(SELECT id FROM nodes WHERE nid = target_nid) as tgt, \
resolution_kind \
FROM edges ORDER BY src, tgt",
)
.unwrap();
let rows_kinds = stmt_kinds
.query_map([], |row| {
Ok((
row.get::<_, Option<String>>(0)?.unwrap_or_default(),
row.get::<_, Option<String>>(1)?.unwrap_or_default(),
row.get::<_, i64>(2)?,
))
})
.unwrap();
let mut edges_kinds_resolved = Vec::new();
for r in rows_kinds {
let (src, tgt, kind) = r.unwrap();
edges_kinds_resolved.push(format!("{} -> {} (kind={})", src, tgt, kind));
}
assert!(edges_kinds_resolved
.contains(&"src/main.go::work -> src/main.go::Cacher::GetList (kind=1)".to_string()));
assert!(edges_kinds_resolved
.contains(&"src/main.go::work -> src/main.go::Cacher::Add (kind=1)".to_string()));
assert!(edges_kinds_resolved
.contains(&"src/main.go::work -> src/main.go::Cacher::Run (kind=1)".to_string()));
assert!(edges_kinds_resolved
.contains(&"src/main.go::work -> src/main.go::Queue::GetList (kind=1)".to_string()));
assert!(edges_kinds_resolved
.contains(&"src/main.go::work -> src/main.go::Queue::Add (kind=1)".to_string()));
assert!(edges_kinds_resolved
.contains(&"src/main.go::work -> src/main.go::Queue::Run (kind=1)".to_string()));
assert!(edges_kinds_resolved
.contains(&"src/main.go::work -> src/main.go::Run (kind=1)".to_string()));
assert!(edges_kinds_resolved.contains(
&"src/App.java::demo::App::main -> src/App.java::demo::Promise::release (kind=4)"
.to_string()
));
assert!(edges_kinds_resolved.contains(
&"src/App.java::demo::App::main -> src/App.java::demo::Promise::tryFailure (kind=4)"
.to_string()
));
assert!(edges_kinds_resolved.contains(
&"src/App.java::demo::App::main -> src/App.java::demo::Promise::run (kind=4)"
.to_string()
));
assert!(edges_kinds_resolved.contains(&"src/App.java::demo::App::main -> src/App.java::demo::TrafficHandler::release (kind=4)".to_string()));
assert!(edges_kinds_resolved.contains(&"src/App.java::demo::App::main -> src/App.java::demo::TrafficHandler::tryFailure (kind=4)".to_string()));
assert!(edges_kinds_resolved.contains(
&"src/App.java::demo::App::main -> src/App.java::demo::TrafficHandler::run (kind=4)"
.to_string()
));
assert!(edges_kinds_resolved.contains(&"src/App.java::demo::App::main -> src/StaticHelper.java::demo::StaticHelper::run (kind=5)".to_string()));
assert!(unresolved.contains(&"src/main.c::main -> (unresolved) MY_MACRO".to_string()));
assert!(unresolved.contains(&"src/main.c::main -> (unresolved) ptr".to_string()));
fs::remove_dir_all(&temp_workspace).unwrap();
}
#[test]
fn test_raw_call_metadata_capture() {
let temp_workspace = create_temp_dir();
let src_dir = temp_workspace.join("src");
fs::create_dir_all(&src_dir).unwrap();
let go_code = r#"
package main
import (
"fmt"
storage "k8s.io/apiserver/pkg/storage"
)
func work() {
storage.ValidateListOptions()
}
"#;
fs::write(src_dir.join("main.go"), go_code).unwrap();
let java_code = r#"
package demo;
import io.netty.channel.ChannelPromise;
public class App {
public void method(ChannelPromise promise) {
promise.tryFailure();
}
}
"#;
fs::write(src_dir.join("App.java"), java_code).unwrap();
run_init(&temp_workspace, false).unwrap();
let db_path = temp_workspace.join(".ochna").join("ochna.db");
let conn = Connection::open(&db_path).unwrap();
let mut stmt = conn
.prepare(
"SELECT callee_name, call_kind, receiver_expr, receiver_type, package_or_namespace, import_hint \
FROM raw_calls ORDER BY callee_name",
)
.unwrap();
let rows = stmt
.query_map([], |row| {
Ok((
row.get::<_, String>(0)?,
row.get::<_, Option<String>>(1)?,
row.get::<_, Option<String>>(2)?,
row.get::<_, Option<String>>(3)?,
row.get::<_, Option<String>>(4)?,
row.get::<_, Option<String>>(5)?,
))
})
.unwrap();
let mut raw_calls_verified = Vec::new();
for r in rows {
let (name, kind, rx, rx_t, ns, imp) = r.unwrap();
raw_calls_verified.push(format!(
"{} | {:?} | {:?} | {:?} | {:?} | {:?}",
name, kind, rx, rx_t, ns, imp
));
}
println!("Raw calls metadata:\n{}", raw_calls_verified.join("\n"));
let go_call = raw_calls_verified
.iter()
.find(|c| c.contains("ValidateListOptions"))
.unwrap();
assert!(go_call.contains(&r#"Some("method")"#.to_string()));
assert!(go_call.contains(&r#"Some("storage")"#.to_string()));
assert!(go_call.contains(&r#"Some("main")"#.to_string()));
assert!(go_call.contains(&r#"Some("k8s.io/apiserver/pkg/storage")"#.to_string()));
let java_call = raw_calls_verified
.iter()
.find(|c| c.contains("tryFailure"))
.unwrap();
assert!(java_call.contains(&r#"Some("method")"#.to_string()));
assert!(java_call.contains(&r#"Some("promise")"#.to_string()));
assert!(java_call.contains(&r#"Some("ChannelPromise")"#.to_string()));
assert!(java_call.contains(&r#"Some("demo")"#.to_string()));
assert!(java_call.contains(&r#"Some("io.netty.channel.ChannelPromise")"#.to_string()));
fs::remove_dir_all(&temp_workspace).unwrap();
}
#[test]
fn framework_relationships_survive_incremental_reindex() {
let workspace = create_temp_dir();
let source = workspace.join("App.java");
let base = r#"
@RestController class Controller {
private final Dependency dependency;
Controller(Dependency dependency) { this.dependency = dependency; }
@GetMapping("/items") String items() { return "ok"; }
}
interface Dependency {}
"#;
fs::write(&source, base).unwrap();
run_init(&workspace, false).unwrap();
let snapshot = |workspace: &PathBuf| -> (Vec<(String, String, String, i64)>, i64) {
let conn = Connection::open(workspace.join(".ochna/ochna.db")).unwrap();
let mut statement = conn
.prepare(
"SELECT source.id, target.id, edges.kind, edges.resolution_kind
FROM edges JOIN nodes source ON source.nid = edges.source_nid
JOIN nodes target ON target.nid = edges.target_nid
ORDER BY source.id, target.id, edges.kind",
)
.unwrap();
let edges = statement
.query_map([], |row| {
Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?))
})
.unwrap()
.map(Result::unwrap)
.collect();
let raw_count = conn
.query_row("SELECT COUNT(*) FROM raw_calls", [], |row| row.get(0))
.unwrap();
(edges, raw_count)
};
let (first, first_raw_count) = snapshot(&workspace);
assert!(first
.iter()
.any(|(_, _, kind, resolution)| kind == "route_handler" && *resolution == 6));
assert!(first.iter().any(|(source, target, kind, resolution)| source
.ends_with("Dependency")
&& target.ends_with("Controller")
&& kind == "injected_into"
&& *resolution == 7));
fs::write(
&source,
format!("{base}\n@ConfigurationProperties(\"billing\") class Billing {{}}\n"),
)
.unwrap();
run_init(&workspace, false).unwrap();
let (second, second_raw_count) = snapshot(&workspace);
assert!(second.iter().any(|(_, _, kind, _)| kind == "route_handler"));
assert!(second.iter().any(|(_, _, kind, _)| kind == "injected_into"));
assert!(second
.iter()
.any(|(_, _, kind, _)| kind == "configuration_binds"));
assert!(second_raw_count > first_raw_count);
run_init(&workspace, false).unwrap();
assert_eq!(snapshot(&workspace), (second, second_raw_count));
fs::write(&source, "@RestController class Controller { @GetMapping(\"/items\") String items() { return \"ok\"; } }\n").unwrap();
run_init(&workspace, false).unwrap();
let (removed, _) = snapshot(&workspace);
assert!(removed
.iter()
.all(|(_, _, kind, _)| kind != "injected_into" && kind != "configuration_binds"));
fs::remove_dir_all(workspace).unwrap();
}
}