pub mod pty;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
pub fn programs() -> Vec<String> {
let text = std::fs::read_to_string(concat!(env!("CARGO_MANIFEST_DIR"), "/tests/tui.toml")).expect("tui.toml");
let manifest: toml::Value = toml::from_str(&text).expect("tui.toml parses");
let mut names: Vec<String> = ["programs", "layout_commands"]
.iter()
.flat_map(|key| manifest[key].as_array().expect("a list").clone())
.map(|p| p.as_str().expect("a program name").to_string())
.collect();
names.sort();
names.dedup();
names
}
fn default_body(name: &str) -> &'static str {
match name {
"git" => "exec /usr/bin/git \"$@\"",
"rm" => "exec /bin/rm \"$@\"",
"jj" => "exit 1",
"zellij" => {
"case \"$1\" in\n\
--version) echo 'zellij 0.43.1' ;;\n\
list-sessions) echo 'No active zellij sessions found.' >&2; exit 1 ;;\n\
esac"
}
"branchdiff" => "[ \"$1\" = --version ] && echo 'branchdiff 0.60.0'; exit 0",
"claude" => "[ \"$1\" = --version ] && echo '2.1.0 (Claude Code)'; exit 0",
"mise" => "[ \"$1\" = env ] && echo '{}'; exit 0",
"id" => "echo 501",
"pgrep" | "pkill" => "exit 1",
_ => "exit 0",
}
}
pub struct Stubs {
dir: tempfile::TempDir,
}
impl Stubs {
fn new() -> Self {
let stubs = Self { dir: tempfile::tempdir().expect("a stub dir") };
for name in programs() {
stubs.set(&name, default_body(&name));
}
stubs
}
pub fn set(&self, name: &str, body: &str) {
use std::os::unix::fs::PermissionsExt;
let log = self.dir.path().join(format!("{name}.calls"));
let script = format!(
"#!/bin/sh\nt=\"{log}.$$\"\nprintf '%s\\037' \"$(pwd -P)\" > \"$t\"\n\
for a in \"$@\"; do printf '%s\\037' \"$a\"; done >> \"$t\"\n\
printf '\\n' >> \"$t\"\ncat \"$t\" >> \"{log}\"\n/bin/rm -f \"$t\"\n{body}\n",
log = log.display(),
);
let path = self.dir.path().join(name);
let status = Command::new("/bin/sh")
.args(["-c", "printf %s \"$WORKON_STUB\" > \"$1\"", "workon stub writer"])
.arg(&path)
.env("WORKON_STUB", script)
.status()
.expect("a stub writer");
assert!(status.success(), "writing the {name} stub: {status}");
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o755)).expect("executable");
}
pub fn path(&self) -> String {
format!("{}:/usr/bin:/bin", self.dir.path().display())
}
pub fn calls(&self, name: &str) -> Vec<Call> {
std::fs::read_to_string(self.dir.path().join(format!("{name}.calls")))
.unwrap_or_default()
.lines()
.map(|line| {
let mut fields: Vec<String> = line.split('\u{1f}').map(String::from).collect();
fields.pop(); let cwd = PathBuf::from(fields.remove(0));
Call { cwd, args: fields }
})
.collect()
}
pub fn call(&self, name: &str, first: &str) -> Call {
let calls = self.calls(name);
let matching: Vec<&Call> = calls.iter().filter(|c| c.args.first().is_some_and(|a| a == first)).collect();
assert_eq!(matching.len(), 1, "one `{name} {first}` expected; calls: {calls:?}");
matching[0].clone()
}
}
#[derive(Clone, Debug)]
pub struct Call {
pub cwd: PathBuf,
pub args: Vec<String>,
}
pub struct World {
root: tempfile::TempDir,
sockets: tempfile::TempDir,
pub stubs: Stubs,
}
impl World {
pub fn new() -> Self {
let root = tempfile::tempdir().expect("a world");
std::fs::create_dir_all(root.path().join("home")).expect("a home");
let zellij_cfg = root.path().join("home/.config/zellij");
std::fs::create_dir_all(&zellij_cfg).expect("a zellij config dir");
std::fs::write(zellij_cfg.join("config.kdl"), "default_shell \"/bin/sh\"\n").expect("a zellij config");
let sockets = tempfile::Builder::new().prefix("wz").tempdir_in("/tmp").expect("short sockets");
for name in ["zellij", "tmux", "zmx"] {
std::fs::create_dir_all(sockets.path().join(name)).expect("a socket dir");
}
std::fs::write(
root.path().join("home/.gitconfig"),
"[user]\n\tname = Test\n\temail = test@example.com\n[init]\n\tdefaultBranch = main\n\
[commit]\n\tgpgsign = false\n",
)
.expect("a gitconfig");
Self { root, sockets, stubs: Stubs::new() }
}
pub fn socket_dir(&self, name: &str) -> PathBuf {
self.sockets.path().join(name)
}
pub fn path(&self) -> PathBuf {
self.root.path().canonicalize().expect("canonical")
}
pub fn home(&self) -> PathBuf {
self.path().join("home")
}
pub fn worktrees(&self) -> PathBuf {
self.home().join(".worktrees")
}
pub fn env(&self) -> Vec<(String, String)> {
vec![
("TERM".into(), "xterm-256color".into()),
("HOME".into(), self.home().display().to_string()),
("XDG_CONFIG_HOME".into(), self.home().join(".config").display().to_string()),
("ZELLIJ_SOCKET_DIR".into(), self.socket_dir("zellij").display().to_string()),
("TMUX_TMPDIR".into(), self.socket_dir("tmux").display().to_string()),
("ZMX_DIR".into(), self.socket_dir("zmx").display().to_string()),
("GIT_CONFIG_NOSYSTEM".into(), "1".into()),
("PATH".into(), self.stubs.path()),
]
}
pub fn workon(&self, cwd: &Path) -> assert_cmd::Command {
let mut cmd = assert_cmd::Command::new(env!("CARGO_BIN_EXE_workon"));
cmd.env_clear();
cmd.envs(self.env());
cmd.current_dir(cwd);
cmd
}
pub fn tui(&self, cwd: &Path, args: &[&str]) -> pty::Tui {
pty::Tui::spawn(self, args, cwd)
}
pub fn tui_with(&self, cwd: &Path, args: &[&str], extra: &[(&str, &str)]) -> pty::Tui {
pty::Tui::spawn_with(self, args, cwd, extra)
}
pub fn git(&self, dir: &Path, args: &[&str]) -> String {
let out = Command::new("/usr/bin/git")
.args(args)
.current_dir(dir)
.env("HOME", self.home())
.env("GIT_CONFIG_NOSYSTEM", "1")
.stdin(Stdio::null())
.output()
.expect("git runs");
assert!(out.status.success(), "git {args:?}: {}", String::from_utf8_lossy(&out.stderr));
String::from_utf8_lossy(&out.stdout).into_owned()
}
pub fn project(&self, files: &[(&str, &str)]) -> PathBuf {
let proj = self.path().join("proj");
std::fs::create_dir_all(&proj).expect("a project");
self.git(&proj, &["init", "-q", "-b", "main"]);
for (rel, content) in files {
let path = proj.join(rel);
std::fs::create_dir_all(path.parent().expect("a parent")).expect("dirs");
std::fs::write(path, content).expect("a file");
}
self.git(&proj, &["add", "-A"]);
self.git(&proj, &["commit", "-q", "--allow-empty", "-m", "init"]);
let origin = self.path().join("origin.git");
self.git(&self.path(), &["init", "-q", "--bare", origin.to_str().expect("utf-8")]);
self.git(&proj, &["remote", "add", "origin", origin.to_str().expect("utf-8")]);
self.git(&proj, &["push", "-q", "origin", "main"]);
proj
}
}