use std::io::Write;
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};
use std::process::{Command, Output, Stdio};
use tempfile::TempDir;
use crate::common;
use common::dolt_installed;
use common::templates;
const RUNNING: &str = env!("CARGO_PKG_VERSION");
fn install_binary(target: &Path) {
let source = env!("CARGO_BIN_EXE_tsk");
if std::fs::hard_link(source, target).is_err() {
std::fs::copy(source, target).unwrap();
}
}
fn spawn_retry(command: &mut Command) -> std::process::Child {
for _ in 0..200 {
match command.spawn() {
Err(e) if e.kind() == std::io::ErrorKind::ExecutableFileBusy => {
std::thread::sleep(std::time::Duration::from_millis(25));
}
result => return result.unwrap(),
}
}
command.spawn().unwrap()
}
struct Fixture {
root: TempDir,
}
impl Drop for Fixture {
fn drop(&mut self) {
common::stop_servers(self.root.path(), false);
}
}
impl Fixture {
fn new() -> Fixture {
let fx = Fixture {
root: templates::instance(&templates::FETCHED),
};
let root = fx.root.path();
std::fs::create_dir_all(root.join("bin")).unwrap();
install_binary(&fx.installed());
let path = fx.tsk(&["ledger", "path"], "", &[]);
assert!(path.status.success(), "{}", err(&path));
common::stop_servers(root, true);
let sql = Command::new("dolt")
.args([
"sql",
"-q",
"replace into manifest (k, v) values ('version', '10')",
])
.current_dir(String::from_utf8_lossy(&path.stdout).trim_end())
.env("HOME", fx.home())
.env("DOLT_DISABLE_EVENT_FLUSH", "1")
.output()
.unwrap();
assert!(sql.status.success(), "{}", err(&sql));
fx
}
fn home(&self) -> PathBuf {
self.root.path().join("home")
}
fn origin(&self) -> PathBuf {
self.root.path().join("origin.git")
}
fn work(&self) -> PathBuf {
self.root.path().join("work")
}
fn installed(&self) -> PathBuf {
self.root.path().join("bin").join("tsk")
}
fn marker(&self) -> PathBuf {
self.root.path().join("update-ran")
}
fn update_script(&self, version: &str) -> PathBuf {
let script = self.root.path().join("update.sh");
let body = format!(
"#!/bin/sh\ntouch \"{marker}\"\ncat > \"$1.new\" <<'EOF'\n#!/bin/sh\n\
if [ \"$1\" = \"--version\" ]; then echo \"tsk {version}\"; exit 0; fi\n\
echo \"rerun: $* guard=$TSK_AUTO_UPDATED stdin=$(cat)\"\nexit 7\nEOF\n\
chmod 755 \"$1.new\"\nmv \"$1.new\" \"$1\"\n",
marker = self.marker().display(),
version = version
);
std::fs::write(&script, body).unwrap();
std::fs::set_permissions(&script, std::fs::Permissions::from_mode(0o755)).unwrap();
script
}
fn plain_script(&self, exit: i32) -> PathBuf {
let script = self.root.path().join("plain.sh");
let body = format!(
"#!/bin/sh\ntouch \"{}\"\nexit {}\n",
self.marker().display(),
exit
);
std::fs::write(&script, body).unwrap();
std::fs::set_permissions(&script, std::fs::Permissions::from_mode(0o755)).unwrap();
script
}
fn tsk(&self, args: &[&str], stdin: &str, env: &[(&str, &Path)]) -> Output {
self.tsk_with(args, stdin, env, &[])
}
fn tsk_with(
&self,
args: &[&str],
stdin: &str,
paths: &[(&str, &Path)],
vars: &[(&str, &str)],
) -> Output {
let mut command = Command::new(self.installed());
command
.args(args)
.current_dir(self.work())
.env("GIT_CONFIG_GLOBAL", "/dev/null")
.env("GIT_CONFIG_NOSYSTEM", "1")
.env("GIT_AUTHOR_NAME", "Test")
.env("GIT_AUTHOR_EMAIL", "test@example.com")
.env("GIT_COMMITTER_NAME", "Test")
.env("GIT_COMMITTER_EMAIL", "test@example.com")
.env("HOME", self.home())
.env("XDG_STATE_HOME", self.root.path().join("state"))
.env("XDG_CONFIG_HOME", self.home().join("config"))
.env("TSK_HOME", self.home().join(".tsk"))
.env("TSK_LEDGER_REFS", "branches")
.env("TSK_UPDATE_CHECK", "never")
.env("GIT_CEILING_DIRECTORIES", self.root.path())
.env_remove("TSK_NO_UPDATE_CHECK")
.env_remove("TSK_AUTO_UPDATE")
.env_remove("TSK_AUTO_UPDATED")
.env_remove("TSK_SELF_UPDATE_CMD")
.env_remove("TSK_TEST_MODE")
.env_remove("CLAUDE_CODE_REMOTE")
.env_remove("CLAUDE_CODE_REMOTE_SESSION_ID")
.env_remove("CLAUDE_ENV_FILE")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
for (k, v) in paths {
command.env(k, v);
}
for (k, v) in vars {
command.env(k, v);
}
let mut child = spawn_retry(&mut command);
child
.stdin
.take()
.unwrap()
.write_all(stdin.as_bytes())
.unwrap();
child.wait_with_output().unwrap()
}
fn with_update(
&self,
args: &[&str],
stdin: &str,
script: &Path,
vars: &[(&str, &str)],
) -> Output {
let mut all = vec![("TSK_TEST_MODE", "1")];
all.extend_from_slice(vars);
self.tsk_with(args, stdin, &[("TSK_SELF_UPDATE_CMD", script)], &all)
}
}
fn out(o: &Output) -> String {
String::from_utf8_lossy(&o.stdout).into_owned()
}
fn err(o: &Output) -> String {
String::from_utf8_lossy(&o.stderr).into_owned()
}
fn warning() -> String {
format!(
"warning: this ledger is at layout 10 and this tsk ({}) supports up to 9. Updating tsk now.",
RUNNING
)
}
macro_rules! need_dolt {
() => {
if !dolt_installed() {
eprintln!("note: dolt is not installed; skipping");
return;
}
};
}
#[test]
fn a_newer_ledger_warns_updates_and_runs_the_command_again() {
need_dolt!();
let fx = Fixture::new();
let script = fx.update_script("99.0.0");
let o = fx.with_update(&["mission", "list", "--porcelain"], "piped\n", &script, &[]);
assert!(err(&o).contains(&warning()), "{}", err(&o));
assert!(fx.marker().exists());
assert_eq!(
out(&o),
"rerun: mission list --porcelain guard=1 stdin=piped\n"
);
assert_eq!(o.status.code(), Some(7), "{}", err(&o));
assert!(
!err(&o).contains("unsupported ledger version"),
"{}",
err(&o)
);
}
#[test]
fn a_tsk_that_still_does_not_support_the_layout_stops_with_the_guard_line() {
need_dolt!();
let fx = Fixture::new();
let script = fx.update_script("99.0.0");
let o = fx.with_update(
&["mission", "list", "--porcelain"],
"",
&script,
&[("TSK_AUTO_UPDATED", "1")],
);
assert_eq!(o.status.code(), Some(1));
let e = err(&o);
assert!(e.contains("error: unsupported ledger version 10"), "{}", e);
assert!(
e.trim_end().ends_with(
"tsk was updated but still does not support layout 10; update it by hand: tsk self-update"
),
"{}",
e
);
assert!(!e.contains("Updating tsk now"), "{}", e);
assert!(!fx.marker().exists());
}
#[test]
fn the_hook_commands_never_update() {
need_dolt!();
let fx = Fixture::new();
let script = fx.update_script("99.0.0");
for args in [&["thread", "session-start"][..], &["thread", "guard"][..]] {
let o = fx.with_update(args, "{}", &script, &[]);
assert!(
!err(&o).contains("Updating tsk now"),
"{:?}: {}",
args,
err(&o)
);
assert!(!out(&o).contains("rerun:"), "{:?}: {}", args, out(&o));
assert!(!fx.marker().exists(), "{:?}", args);
}
}
#[test]
fn the_opt_outs_keep_the_error_and_run_no_update() {
need_dolt!();
let fx = Fixture::new();
let script = fx.update_script("99.0.0");
for var in [("TSK_AUTO_UPDATE", "never"), ("TSK_NO_UPDATE_CHECK", "1")] {
let o = fx.with_update(&["mission", "list", "--porcelain"], "", &script, &[var]);
assert_eq!(o.status.code(), Some(1), "{:?}", var);
assert!(
err(&o).contains("error: unsupported ledger version 10"),
"{:?}: {}",
var,
err(&o)
);
assert!(!err(&o).contains("Updating tsk now"), "{}", err(&o));
assert!(!err(&o).contains("update it by hand"), "{}", err(&o));
assert!(!fx.marker().exists(), "{:?}", var);
}
}
#[test]
fn the_update_script_is_ignored_without_test_mode() {
need_dolt!();
let fx = Fixture::new();
let script = fx.update_script("99.0.0");
let o = fx.tsk_with(
&["mission", "list", "--porcelain"],
"",
&[("TSK_SELF_UPDATE_CMD", &script)],
&[("TSK_RELEASE_REPO_URL", fx.origin().to_str().unwrap())],
);
assert_eq!(o.status.code(), Some(1));
assert!(!fx.marker().exists());
assert!(err(&o).contains("has no release tag"), "{}", err(&o));
assert!(
err(&o).contains("unsupported ledger version 10"),
"{}",
err(&o)
);
}
#[test]
fn a_failed_update_prints_its_error_then_the_original_error() {
need_dolt!();
let fx = Fixture::new();
let script = fx.plain_script(3);
let o = fx.with_update(&["mission", "list", "--porcelain"], "", &script, &[]);
assert_eq!(o.status.code(), Some(1));
let e = err(&o);
let at = |needle: &str| {
e.find(needle)
.unwrap_or_else(|| panic!("{}: {}", needle, e))
};
assert!(at(&warning()) < at("the update script failed"));
assert!(at("the update script failed") < at("error: unsupported ledger version 10"));
assert!(fx.marker().exists());
}
#[test]
fn an_update_that_does_not_raise_the_version_is_an_error() {
need_dolt!();
let fx = Fixture::new();
let script = fx.plain_script(0);
let o = fx.with_update(&["mission", "list", "--porcelain"], "", &script, &[]);
assert_eq!(o.status.code(), Some(1));
let e = err(&o);
assert!(e.contains("not newer than"), "{}", e);
assert!(e.contains("error: unsupported ledger version 10"), "{}", e);
}
#[test]
fn an_error_that_is_not_the_unsupported_layout_never_updates() {
need_dolt!();
let fx = Fixture::new();
let script = fx.update_script("99.0.0");
let o = fx.with_update(
&["mission", "list", "--porcelain"],
"",
&script,
&[("TSK_LEDGER_BACKEND", "git")],
);
assert_eq!(o.status.code(), Some(1), "{}", err(&o));
assert!(!err(&o).contains("Updating tsk now"), "{}", err(&o));
assert!(!fx.marker().exists());
}