use super::*;
use crate::actions::{close_enabled, unclaim_enabled};
use crate::app::Roots;
use crate::projects::join::JoinState;
use crate::projects::runner::BlRunner;
use crate::test_support::{FakeClock, OkProbe};
use std::collections::{HashMap, HashSet};
use std::fs;
use std::sync::{Arc, Mutex};
use tempfile::{TempDir, tempdir};
mod internal;
mod refresh;
mod ops;
mod convball;
#[derive(Clone)]
struct FakeBl {
live: Arc<Mutex<HashMap<PathBuf, String>>>,
closed: Arc<Mutex<HashMap<PathBuf, String>>>,
fail: Arc<Mutex<HashSet<PathBuf>>>,
}
impl BlRunner for FakeBl {
fn list(&self, project: &Path) -> std::io::Result<String> {
if self.fail.lock().unwrap().contains(project) {
return Err(std::io::Error::other("bl exited nonzero"));
}
Ok(self
.live
.lock()
.unwrap()
.get(project)
.cloned()
.unwrap_or_else(|| "[]".to_string()))
}
fn list_closed(&self, project: &Path) -> std::io::Result<String> {
Ok(self
.closed
.lock()
.unwrap()
.get(project)
.cloned()
.unwrap_or_else(|| "[]".to_string()))
}
fn show(&self, _p: &Path, _id: &str) -> std::io::Result<String> {
Ok("{}".to_string())
}
}
struct World {
_root: TempDir,
roots: Roots,
project: PathBuf,
ws_cobalt: PathBuf,
ws_spare: PathBuf,
live: Arc<Mutex<HashMap<PathBuf, String>>>,
closed: Arc<Mutex<HashMap<PathBuf, String>>>,
fail: Arc<Mutex<HashSet<PathBuf>>>,
}
const LIST: &str = r#"[{"id":"bl-work","title":"Work","claimant":"cobalt"},
{"id":"bl-boss","title":"Boss","claimant":"boss"}]"#;
fn world() -> World {
let root = tempdir().unwrap();
let roots = Roots {
yog_data: root.path().join("yog"),
lernie_data: root.path().join("lernie"),
yog_state: root.path().join("state"),
balls_clones: root.path().join("clones"),
home: root.path().join("home"),
};
fs::create_dir_all(roots.balls_clones.join("%2Fproj%2Fa")).unwrap();
fs::create_dir_all(&roots.yog_state).unwrap();
let project = PathBuf::from("/proj/a");
let ws = |name: &str| {
let p = roots.yog_data.join("workspaces").join(name);
fs::create_dir_all(p.join("repo.git")).unwrap();
p
};
let ws_cobalt = ws("cobalt");
let ws_spare = ws("spare");
let live = Arc::new(Mutex::new(HashMap::from([(
project.clone(),
LIST.to_string(),
)])));
let closed = Arc::new(Mutex::new(HashMap::new()));
let fail = Arc::new(Mutex::new(HashSet::new()));
World {
_root: root,
roots,
project,
ws_cobalt,
ws_spare,
live,
closed,
fail,
}
}
fn model(w: &World) -> (FakeClock, AppModel) {
build_model(w, None)
}
fn model_focused(w: &World, ws: &std::path::Path) -> (FakeClock, AppModel) {
build_model(w, Some(ws.to_path_buf()))
}
fn build_model(w: &World, focus: Option<PathBuf>) -> (FakeClock, AppModel) {
let clock = FakeClock::new();
let m = AppModel::new(
w.roots.clone(),
focus,
clock.arc(),
Box::new(FakeBl {
live: w.live.clone(),
closed: w.closed.clone(),
fail: w.fail.clone(),
}),
Box::new(OkProbe),
Some("me".to_string()),
);
(clock, m)
}
fn set_list(w: &World, json: &str) {
w.live
.lock()
.unwrap()
.insert(w.project.clone(), json.to_string());
}
fn set_closed(w: &World, json: &str) {
w.closed
.lock()
.unwrap()
.insert(w.project.clone(), json.to_string());
}
fn append_op(m: &AppModel) {
opslog::append(
m.state_root(),
&opslog::OpEntry {
ts: "TS".into(),
argv: vec!["bl".into(), "close".into(), "bl-work".into()],
cwd: "/proj/a".into(),
exit: 0,
stdout: String::new(),
stderr: String::new(),
},
)
.unwrap();
}
#[test]
fn construction_fetches_balls_and_builds_the_claimant_join() {
let w = world();
let (_c, m) = model(&w);
let bound = m.row_for(&w.ws_cobalt).unwrap();
assert_eq!(bound.state, JoinState::Bound, "claimant = workspace name");
assert_eq!(bound.ball_id, "bl-work");
let spare = m.row_for(&w.ws_spare).unwrap();
assert_eq!(spare.state, JoinState::UnassignedWorkspace);
let cobalt_balls = m.ws_balls(&w.ws_cobalt);
assert_eq!(cobalt_balls.len(), 1);
assert_eq!(cobalt_balls[0].id, "bl-work");
assert_eq!(cobalt_balls[0].badge, None);
assert!(m.ws_balls(&w.ws_spare).is_empty());
assert!(m.ws_balls(Path::new("/nope")).is_empty());
}
#[test]
fn focused_join_targets_the_focused_workspace_ball() {
let w = world();
let (_c, mut m) = model(&w);
m.focus_workspace(&w.ws_cobalt);
let row = m.focused_join().unwrap();
assert_eq!(row.ball_id, "bl-work");
assert!(close_enabled(row.state), "Bound ⇒ Close offered");
assert!(unclaim_enabled(row.state));
m.focus_workspace(&w.ws_spare);
let spare = m.focused_join().unwrap().state;
assert!(!close_enabled(spare), "unassigned workspace ⇒ no Close");
assert!(!unclaim_enabled(spare));
}
#[test]
fn identity_and_state_root_are_exposed() {
let w = world();
let (_c, m) = model(&w);
assert_eq!(
m.identity(),
"me",
"no recorded identity ⇒ the $USER fallback"
);
assert_eq!(m.state_root(), w.roots.yog_state.as_path());
}
#[test]
fn an_unlistable_clone_renders_orphaned() {
let w = world();
fs::create_dir_all(w.roots.balls_clones.join("%2Fproj%2Fgone")).unwrap();
let bad = PathBuf::from("/proj/gone");
w.fail.lock().unwrap().insert(bad.clone());
let (_c, m) = model(&w);
let orphan = m
.join_rows
.iter()
.find(|r| r.project == bad)
.expect("the unlistable clone has a row");
assert_eq!(orphan.state, JoinState::OrphanedProject);
}
pub(super) fn empty_model() -> (TempDir, AppModel) {
let root = tempdir().unwrap();
let roots = Roots {
yog_data: root.path().join("yog"),
lernie_data: root.path().join("lernie"),
yog_state: root.path().join("state"),
balls_clones: root.path().join("clones"),
home: root.path().join("home"),
};
let empty = FakeBl {
live: Arc::new(Mutex::new(HashMap::new())),
closed: Arc::new(Mutex::new(HashMap::new())),
fail: Arc::new(Mutex::new(HashSet::new())),
};
let m = AppModel::new(
roots,
None,
FakeClock::new().arc(),
Box::new(empty),
Box::new(OkProbe),
None,
);
(root, m)
}
#[test]
fn focused_join_is_none_without_a_focused_workspace() {
let (_root, m) = empty_model();
assert!(m.focused_join().is_none());
assert!(m.ops_rows().is_empty());
assert_eq!(m.identity(), "", "no recorded identity, no user ⇒ empty");
}