use std::path::Path;
use std::process::Command;
use std::sync::Arc;
use rto_graph::{
FactSet, Node, NodeKind, ResolvedWorkspace, Store, Workspace, WorkspaceError, WorkspaceSet,
};
fn git_init(dir: &Path) {
let status = Command::new("git")
.args(["-c", "init.defaultBranch=main", "init", "-q"])
.current_dir(dir)
.status()
.expect("run git init");
assert!(status.success(), "git init failed in {}", dir.display());
}
fn repo_with_node(dir: &Path, sym: &str) {
std::fs::create_dir_all(dir).expect("mkdir repo");
git_init(dir);
let store_dir = dir.join(".git").join("roteiro");
std::fs::create_dir_all(&store_dir).expect("mkdir store");
let mut store = Store::open(&store_dir.join("graph.db")).expect("open store");
let facts = FactSet::new().with_node(Node::new(sym, NodeKind::Fn, sym));
store.apply_factset(&facts).expect("apply");
}
#[test]
fn workspace_routes_queries_to_the_right_project() {
let base = std::env::temp_dir().join(format!("rto-ws-{}", std::process::id()));
std::fs::remove_dir_all(&base).ok();
repo_with_node(&base.join("alpha"), "sym:rust:a.rs#only_in_alpha");
repo_with_node(&base.join("beta"), "sym:rust:b.rs#only_in_beta");
let ws = Workspace::from_repo_paths([base.join("alpha"), base.join("beta")])
.expect("build workspace");
assert_eq!(ws.names(), vec!["alpha".to_owned(), "beta".to_owned()]);
assert!(ws.is_multi());
let alpha_has = |key: &str| {
ws.with_store(Some("alpha"), |s| s.get_node(key).unwrap().is_some())
.unwrap()
};
let beta_has = |key: &str| {
ws.with_store(Some("beta"), |s| s.get_node(key).unwrap().is_some())
.unwrap()
};
assert!(alpha_has("sym:rust:a.rs#only_in_alpha"));
assert!(!alpha_has("sym:rust:b.rs#only_in_beta"));
assert!(beta_has("sym:rust:b.rs#only_in_beta"));
assert!(!beta_has("sym:rust:a.rs#only_in_alpha"));
let err = ws
.with_store(None, |s| s.node_count().unwrap())
.expect_err("bare call is ambiguous");
assert!(matches!(err, WorkspaceError::AmbiguousProject { .. }));
std::fs::remove_dir_all(&base).ok();
}
#[test]
fn reload_from_picks_up_added_and_dropped_repos() {
let base = std::env::temp_dir().join(format!("rto-ws-reload-{}", std::process::id()));
std::fs::remove_dir_all(&base).ok();
repo_with_node(&base.join("alpha"), "sym:rust:a.rs#in_alpha");
repo_with_node(&base.join("beta"), "sym:rust:b.rs#in_beta");
let ws = Workspace::from_repo_paths([base.join("alpha")]).expect("build");
assert_eq!(ws.names(), vec!["alpha".to_owned()]);
assert!(
ws.with_store(None, |s| s
.get_node("sym:rust:a.rs#in_alpha")
.unwrap()
.is_some())
.unwrap()
);
let names = ws
.reload_from([base.join("alpha"), base.join("beta")])
.expect("reload");
assert_eq!(names, vec!["alpha".to_owned(), "beta".to_owned()]);
assert!(ws.is_multi());
assert!(
ws.with_store(Some("beta"), |s| s
.get_node("sym:rust:b.rs#in_beta")
.unwrap()
.is_some())
.unwrap()
);
let names = ws.reload_from([base.join("beta")]).expect("reload");
assert_eq!(names, vec!["beta".to_owned()]);
assert!(matches!(
ws.resolve(Some("alpha")).unwrap_err(),
WorkspaceError::UnknownProject { .. }
));
assert_eq!(ws.resolve(None).unwrap(), "beta");
std::fs::remove_dir_all(&base).ok();
}
#[test]
fn reload_reroutes_when_a_name_maps_to_a_different_repo() {
let base = std::env::temp_dir().join(format!("rto-ws-reroute-{}", std::process::id()));
std::fs::remove_dir_all(&base).ok();
repo_with_node(&base.join("a").join("proj"), "sym:rust:x.rs#from_a");
repo_with_node(&base.join("b").join("proj"), "sym:rust:x.rs#from_b");
let ws = Workspace::from_repo_paths([base.join("a").join("proj")]).expect("build");
assert!(
ws.with_store(None, |s| s
.get_node("sym:rust:x.rs#from_a")
.unwrap()
.is_some())
.unwrap()
);
let names = ws
.reload_from([base.join("b").join("proj")])
.expect("reload");
assert_eq!(names, vec!["proj".to_owned()]);
assert!(
ws.with_store(None, |s| s
.get_node("sym:rust:x.rs#from_b")
.unwrap()
.is_some())
.unwrap(),
"should now read repo B's graph"
);
assert!(
!ws.with_store(None, |s| s
.get_node("sym:rust:x.rs#from_a")
.unwrap()
.is_some())
.unwrap(),
"must not still serve repo A's graph from a stale cache entry"
);
std::fs::remove_dir_all(&base).ok();
}
#[test]
fn on_open_hook_prepares_the_graph_on_first_access() {
let base = std::env::temp_dir().join(format!("rto-ws-onopen-{}", std::process::id()));
std::fs::remove_dir_all(&base).ok();
let dir = base.join("proj");
std::fs::create_dir_all(&dir).expect("mkdir");
git_init(&dir);
let db = dir.join(".git").join("roteiro").join("graph.db");
assert!(!db.exists(), "no graph before first access");
let ws = Workspace::from_repo_paths([&dir])
.expect("build")
.with_on_open(Arc::new(|db: &Path| {
std::fs::create_dir_all(db.parent().unwrap()).map_err(|e| e.to_string())?;
let mut store = Store::open(db).map_err(|e| e.to_string())?;
let facts = FactSet::new().with_node(Node::new(
"sym:rust:z.rs#prepared",
NodeKind::Fn,
"prepared",
));
store.apply_factset(&facts).map_err(|e| e.to_string())?;
Ok(())
}));
assert!(
ws.with_store(None, |s| s
.get_node("sym:rust:z.rs#prepared")
.unwrap()
.is_some())
.unwrap(),
"the on-open hook should have built the graph"
);
assert!(db.exists(), "graph.db exists after first access");
std::fs::remove_dir_all(&base).ok();
}
#[test]
fn a_registered_repo_without_a_graph_reports_no_graph() {
let base = std::env::temp_dir().join(format!("rto-ws-nograph-{}", std::process::id()));
std::fs::remove_dir_all(&base).ok();
let dir = base.join("fresh");
std::fs::create_dir_all(&dir).expect("mkdir");
git_init(&dir);
let ws = Workspace::from_repo_paths([&dir]).expect("build workspace");
let err = ws
.with_store(None, |s| s.node_count().unwrap())
.expect_err("no graph yet");
assert!(matches!(err, WorkspaceError::NoGraph { .. }), "{err:?}");
assert!(err.to_string().contains("roteiro sync"));
std::fs::remove_dir_all(&base).ok();
}
#[test]
fn workspace_set_from_resolved_partitions_linked_and_standalone() {
let base = std::env::temp_dir().join(format!("rto-wsset-{}", std::process::id()));
std::fs::remove_dir_all(&base).ok();
repo_with_node(&base.join("linked").join("api"), "sym:rust:a.rs#in_api");
repo_with_node(&base.join("linked").join("web"), "sym:rust:b.rs#in_web");
repo_with_node(&base.join("solo").join("tools"), "sym:rust:c.rs#in_tools");
let resolved = vec![
ResolvedWorkspace {
name: "prod".to_owned(),
roots: vec![base.join("linked").to_string_lossy().into_owned()],
repos: Vec::new(),
linked: true,
},
ResolvedWorkspace {
name: "tools".to_owned(),
roots: Vec::new(),
repos: vec![
base.join("solo")
.join("tools")
.to_string_lossy()
.into_owned(),
],
linked: false,
},
];
let set = WorkspaceSet::from_resolved(resolved).expect("build set");
assert_eq!(set.names(), vec!["prod".to_owned(), "tools".to_owned()]);
assert_eq!(set.linked("prod"), Some(true));
assert_eq!(set.linked("tools"), Some(false));
let prod = set.select(Some("prod")).expect("select prod");
assert_eq!(prod.names(), vec!["api".to_owned(), "web".to_owned()]);
assert!(prod.is_multi());
assert!(
prod.with_store(Some("api"), |s| s
.get_node("sym:rust:a.rs#in_api")
.unwrap()
.is_some())
.unwrap()
);
let tools = set.select(Some("tools")).expect("select tools");
assert!(!tools.is_multi());
assert_eq!(tools.names(), vec!["tools".to_owned()]);
assert!(
tools
.with_store(None, |s| s
.get_node("sym:rust:c.rs#in_tools")
.unwrap()
.is_some())
.unwrap()
);
let api_db = base
.join("linked")
.join("api")
.join(".git")
.join("roteiro")
.join("graph.db");
let tools_db = base
.join("solo")
.join("tools")
.join(".git")
.join("roteiro")
.join("graph.db");
assert_eq!(set.containing(&api_db), Some("prod"));
assert_eq!(set.containing(&tools_db), Some("tools"));
assert_eq!(set.containing(Path::new("/nope/graph.db")), None);
std::fs::remove_dir_all(&base).ok();
}
#[test]
fn workspace_set_from_resolved_skips_empty_groups() {
let base = std::env::temp_dir().join(format!("rto-wsset-empty-{}", std::process::id()));
std::fs::remove_dir_all(&base).ok();
std::fs::create_dir_all(base.join("barren")).expect("mkdir empty root");
repo_with_node(&base.join("real"), "sym:rust:a.rs#real");
let resolved = vec![
ResolvedWorkspace {
name: "ghost".to_owned(),
roots: vec![base.join("barren").to_string_lossy().into_owned()],
repos: Vec::new(),
linked: true,
},
ResolvedWorkspace {
name: "real".to_owned(),
roots: Vec::new(),
repos: vec![base.join("real").to_string_lossy().into_owned()],
linked: false,
},
];
let set = WorkspaceSet::from_resolved(resolved).expect("build set");
assert_eq!(set.names(), vec!["real".to_owned()]);
assert!(matches!(
set.select(Some("ghost")),
Err(WorkspaceError::UnknownWorkspace { .. })
));
assert!(set.select(None).is_ok());
std::fs::remove_dir_all(&base).ok();
}
#[test]
fn from_resolved_splits_a_standalone_group_into_single_repo_workspaces() {
let base = std::env::temp_dir().join(format!("rto-wsset-split-{}", std::process::id()));
std::fs::remove_dir_all(&base).ok();
repo_with_node(&base.join("one"), "sym:rust:a.rs#one");
repo_with_node(&base.join("two"), "sym:rust:b.rs#two");
let resolved = vec![ResolvedWorkspace {
name: "solo".to_owned(),
roots: Vec::new(),
repos: vec![
base.join("one").to_string_lossy().into_owned(),
base.join("two").to_string_lossy().into_owned(),
],
linked: false,
}];
let set = WorkspaceSet::from_resolved(resolved).expect("build set");
assert_eq!(set.names(), vec!["solo".to_owned(), "solo-2".to_owned()]);
assert_eq!(set.linked("solo"), Some(false));
assert_eq!(set.linked("solo-2"), Some(false));
assert!(
!set.select(Some("solo")).expect("solo").is_multi(),
"each standalone workspace holds exactly one repo"
);
assert!(!set.select(Some("solo-2")).expect("solo-2").is_multi());
std::fs::remove_dir_all(&base).ok();
}