use super::*;
#[cfg(unix)]
fn executable(path: &Path, body: &[u8]) {
use std::os::unix::fs::PermissionsExt;
std::fs::write(path, body).expect("an executable is written");
std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o755)).expect("it is made executable");
}
#[cfg(unix)]
fn reporting(path: &Path, name: &str, version: &str) {
executable(path, format!("#!/bin/sh\necho '{name} {version}'\n").as_bytes());
}
#[cfg(unix)]
fn unstartable(path: &Path) {
executable(path, &[0x00, 0x01, 0x02, 0x03, 0xde, 0xad, 0xbe, 0xef]);
}
#[cfg(unix)]
fn installed(root: &Path) -> PathBuf {
let dir = root.join("bin");
std::fs::create_dir_all(&dir).expect("the install directory");
reporting(&dir.join("wsb"), "wsb", "1.0.0");
reporting(&dir.join("wsb-daemon"), "wsb-daemon", "1.0.0");
dir
}
fn built_in(root: &Path) -> Built {
let dir = root.join("built");
std::fs::create_dir_all(&dir).expect("the build directory");
Built {
cli: dir.join("wsb"),
daemon: dir.join("wsb-daemon"),
}
}
fn bytes(path: &Path) -> Vec<u8> {
std::fs::read(path).expect("the file reads")
}
fn names(dir: &Path) -> Vec<String> {
let mut names: Vec<String> = std::fs::read_dir(dir)
.expect("the directory reads")
.map(|entry| entry.expect("an entry").file_name().to_string_lossy().into_owned())
.collect();
names.sort();
names
}
#[test]
fn test20063_a_missing_half_is_refused_naming_the_install() {
let dir = tempfile::tempdir().expect("a directory");
let refused = installed_pair(dir.path()).expect_err("an empty directory holds no installation");
assert!(refused.contains("(wsb, wsb-daemon missing)"), "{refused}");
assert!(refused.contains("qrust install wsb wsb-daemon"), "the refusal does not say how to install: {refused}");
std::fs::write(dir.path().join(Half::Cli.file_name()), b"").expect("one half");
let refused = installed_pair(dir.path()).expect_err("half a pair is not an installation");
assert!(refused.contains("(wsb-daemon missing)"), "{refused}");
std::fs::write(dir.path().join(Half::Daemon.file_name()), b"").expect("the other half");
installed_pair(dir.path()).expect("both halves are an installation");
}
#[cfg(unix)]
#[test]
fn test20064_an_unwritable_install_directory_is_refused_before_any_build() {
use std::os::unix::fs::PermissionsExt;
if unsafe { libc::geteuid() } == 0 {
crate::errln!("TEST20064 proves nothing as root, whose writes no permission stops; run it as a user");
return;
}
let root = tempfile::tempdir().expect("a directory");
let install_dir = installed(root.path());
std::fs::set_permissions(&install_dir, std::fs::Permissions::from_mode(0o555)).expect("read-only");
let checkout = root.path().join("checkout");
let refused = run_in(&install_dir, &checkout, &root.path().join("no-remote").display().to_string(), "main", false);
std::fs::set_permissions(&install_dir, std::fs::Permissions::from_mode(0o755)).expect("writable again");
let refused = refused.expect_err("an unwritable install directory is refused");
assert!(refused.contains("cannot be written to"), "refused for another reason: {refused}");
assert!(!checkout.exists(), "the checkout was started before the refusal");
}
#[cfg(unix)]
#[test]
fn test20065_a_built_binary_that_will_not_start_is_not_installed() {
let cases: [(&str, fn(&Built)); 3] = [
("the CLI cannot start", |built| {
unstartable(&built.cli);
reporting(&built.daemon, "wsb-daemon", "2.0.0");
}),
("the daemon cannot start", |built| {
reporting(&built.cli, "wsb", "2.0.0");
unstartable(&built.daemon);
}),
("the halves are two builds", |built| {
reporting(&built.cli, "wsb", "2.0.0");
reporting(&built.daemon, "wsb-daemon", "2.0.1");
}),
];
for (case, build) in cases {
let root = tempfile::tempdir().expect("a directory");
let install_dir = installed(root.path());
let before = (bytes(&install_dir.join("wsb")), bytes(&install_dir.join("wsb-daemon")));
let built = built_in(root.path());
build(&built);
let refused = replace(&built, &install_dir, false).expect_err(case);
assert!(refused.contains("Nothing was replaced"), "{case}: {refused}");
assert_eq!(
(bytes(&install_dir.join("wsb")), bytes(&install_dir.join("wsb-daemon"))),
before,
"{case}: the installed pair changed"
);
assert_eq!(names(&install_dir), ["wsb", "wsb-daemon"], "{case}: something was left in the install directory");
}
}
#[cfg(unix)]
#[test]
fn test20066_both_halves_are_replaced_when_both_start() {
let root = tempfile::tempdir().expect("a directory");
let install_dir = installed(root.path());
let built = built_in(root.path());
reporting(&built.cli, "wsb", "2.0.0");
reporting(&built.daemon, "wsb-daemon", "2.0.0");
let (version, replaced) = replace(&built, &install_dir, false).expect("a pair that starts is installed");
assert_eq!(version, "2.0.0");
assert_eq!(replaced, [install_dir.join("wsb"), install_dir.join("wsb-daemon")]);
assert_eq!(bytes(&install_dir.join("wsb")), bytes(&built.cli), "the CLI was not replaced");
assert_eq!(bytes(&install_dir.join("wsb-daemon")), bytes(&built.daemon), "the daemon was not replaced");
assert_eq!(
started_version(&install_dir.join("wsb"), Half::Cli).expect("the installed CLI starts"),
"2.0.0",
"what was installed does not start as the build did"
);
assert_eq!(names(&install_dir), ["wsb", "wsb-daemon"], "a staged binary was left behind");
}
#[cfg(unix)]
#[test]
fn test20067_a_dry_run_replaces_nothing() {
let root = tempfile::tempdir().expect("a directory");
let install_dir = installed(root.path());
let before = (bytes(&install_dir.join("wsb")), bytes(&install_dir.join("wsb-daemon")));
let built = built_in(root.path());
reporting(&built.cli, "wsb", "2.0.0");
reporting(&built.daemon, "wsb-daemon", "2.0.0");
let (version, would) = replace(&built, &install_dir, true).expect("a dry run of a pair that starts");
assert_eq!(version, "2.0.0");
assert_eq!(would, [install_dir.join("wsb"), install_dir.join("wsb-daemon")]);
assert_eq!(
(bytes(&install_dir.join("wsb")), bytes(&install_dir.join("wsb-daemon"))),
before,
"a dry run replaced the installed pair"
);
assert_eq!(names(&install_dir), ["wsb", "wsb-daemon"]);
unstartable(&built.daemon);
replace(&built, &install_dir, true).expect_err("a dry run passed a daemon that cannot start");
}
#[test]
fn test20068_the_install_root_follows_qrust_home() {
let home = PathBuf::from("/home/someone");
let qrust_home = PathBuf::from("/opt/qrust");
assert_eq!(
install_root_bin(Some(qrust_home.clone()), Some(home.clone())).expect("QRUST_HOME"),
qrust_home.join("bin")
);
assert_eq!(
install_root_bin(Some(PathBuf::new()), Some(home.clone())).expect("an empty QRUST_HOME"),
home.join(".qrust").join("bin"),
"an empty QRUST_HOME is unset"
);
assert_eq!(install_root_bin(None, Some(home.clone())).expect("home"), home.join(".qrust").join("bin"));
install_root_bin(None, None).expect_err("with no home there is no install root to name");
}
fn git_in(dir: &Path, args: &[&str]) -> String {
let output = crate::git_backend::git_command()
.current_dir(dir)
.args(["-c", "user.name=t", "-c", "user.email=t@t", "-c", "init.defaultBranch=main"])
.args(args)
.output()
.expect("git runs");
assert!(output.status.success(), "git {args:?}: {}", String::from_utf8_lossy(&output.stderr));
String::from_utf8_lossy(&output.stdout).trim().to_string()
}
fn remote(root: &Path) -> (PathBuf, PathBuf) {
let bare = root.join("remote.git");
let work = root.join("work");
std::fs::create_dir_all(&bare).expect("the remote");
std::fs::create_dir_all(&work).expect("the work clone");
git_in(&bare, &["init", "--quiet", "--bare"]);
git_in(&work, &["init", "--quiet"]);
std::fs::write(work.join("source.rs"), "fn one() {}\n").expect("a file");
git_in(&work, &["add", "-A"]);
git_in(&work, &["commit", "--quiet", "-m", "one"]);
git_in(&work, &["push", "--quiet", &bare.display().to_string(), "main"]);
(bare, work)
}
#[test]
fn test20069_the_managed_checkout_is_reset_to_the_ref() {
let root = tempfile::tempdir().expect("a directory");
let (bare, work) = remote(root.path());
let repository = bare.display().to_string();
let checkout = root.path().join("machine").join("upgrade").join("wsb");
let first = fetch(&checkout, &repository, "main").expect("a first fetch clones");
assert_eq!(first, git_in(&work, &["rev-parse", "HEAD"]));
assert_eq!(std::fs::read_to_string(checkout.join("source.rs")).expect("checked out"), "fn one() {}\n");
std::fs::write(checkout.join("source.rs"), "fn edited() {}\n").expect("an edit");
git_in(&checkout, &["commit", "--quiet", "-am", "local"]);
std::fs::write(checkout.join("source.rs"), "fn edited_again() {}\n").expect("an edit");
std::fs::write(checkout.join("stray.rs"), "fn stray() {}\n").expect("an untracked file");
std::fs::write(work.join("source.rs"), "fn two() {}\n").expect("the remote moves");
git_in(&work, &["commit", "--quiet", "-am", "two"]);
git_in(&work, &["push", "--quiet", &repository, "main"]);
git_in(&work, &["tag", "v1", "HEAD~1"]);
git_in(&work, &["push", "--quiet", &repository, "v1"]);
let second = fetch(&checkout, &repository, "main").expect("a second fetch");
assert_eq!(second, git_in(&work, &["rev-parse", "HEAD"]), "the checkout is not at the remote's main");
assert_eq!(git_in(&checkout, &["rev-parse", "HEAD"]), second);
assert_eq!(std::fs::read_to_string(checkout.join("source.rs")).expect("reset"), "fn two() {}\n");
assert!(!checkout.join("stray.rs").exists(), "an untracked file survived");
assert_eq!(git_in(&checkout, &["status", "--porcelain"]), "", "the checkout is not clean");
assert_eq!(fetch(&checkout, &repository, "v1").expect("a tag"), first);
let refused = fetch(&checkout, &repository, "no-such-ref").expect_err("an unknown ref");
assert!(refused.contains("no branch or tag named 'no-such-ref'"), "{refused}");
}
#[cfg(unix)]
#[test]
fn test20070_a_half_finished_swap_says_the_pair_is_mismatched() {
let root = tempfile::tempdir().expect("a directory");
let install_dir = installed(root.path());
std::fs::remove_file(install_dir.join("wsb-daemon")).expect("the daemon goes");
std::fs::create_dir_all(install_dir.join("wsb-daemon").join("held")).expect("a directory in its place");
let built = built_in(root.path());
reporting(&built.cli, "wsb", "2.0.0");
reporting(&built.daemon, "wsb-daemon", "2.0.0");
let refused = replace(&built, &install_dir, false).expect_err("the daemon cannot be put in place");
assert!(refused.contains("ALREADY replaced"), "{refused}");
assert!(refused.contains(&install_dir.join("wsb").display().to_string()), "it does not name the half replaced: {refused}");
assert!(refused.contains("mismatched"), "{refused}");
assert_eq!(bytes(&install_dir.join("wsb")), bytes(&built.cli), "the first half was not the one replaced");
assert_eq!(names(&install_dir), ["wsb", "wsb-daemon"], "a staged binary was left behind");
}
#[test]
fn test20071_the_service_keeps_the_ports_it_was_installed_with() {
let spawn = crate::daemon::Spawn {
listen: crate::daemon::LOOPBACK,
http_port: crate::daemon::Port::Exact(3001),
parent_pid: None,
debug: false,
peer_port: 7420,
};
let arguments = crate::daemon::daemon_arguments(Path::new("/home/op/ws"), &spawn).expect("exact ports");
assert_eq!(recorded(&arguments, "--port").expect("the port"), "3001");
assert_eq!(recorded(&arguments, "--peer-port").expect("the peer port"), "7420");
let without: Vec<String> = arguments.iter().filter(|a| *a != "--peer-port").cloned().collect();
recorded(&without, "--peer-port").expect_err("a record without the flag is refused, not guessed");
}
#[cfg(unix)]
fn recording_wsb(dir: &Path, log: &Path, fail_on: &str) {
std::fs::create_dir_all(dir).expect("the install directory");
executable(
&dir.join("wsb"),
format!(
"#!/bin/sh\necho \"$(pwd -P)|$*\" >> '{}'\n[ \"$1\" = '{fail_on}' ] && exit 1\nexit 0\n",
log.display()
)
.as_bytes(),
);
}
#[cfg(unix)]
#[test]
fn test20133_the_machine_is_migrated_between_the_old_daemon_and_the_new() {
let temp = tempfile::tempdir().expect("a scratch directory");
let root = crate::platform::canonicalize(temp.path()).expect("resolves");
let (machine, here, service_root) = (root.join("machine"), root.join("here"), root.join("served"));
for dir in [&machine, &here, &service_root] {
std::fs::create_dir_all(dir).expect("a folder");
}
let _env = crate::test_environment::Environment::set(&[(
crate::daemon::MACHINE_DIR_ENV,
&machine.to_string_lossy(),
)]);
let log = root.join("calls.log");
let calls = || std::fs::read_to_string(&log).unwrap_or_default();
recording_wsb(&root.join("bin"), &log, "never");
assert_eq!(bring_the_machine_over(&root.join("bin"), &here), Ok(MachineStep::Stopped));
let here_shown = here.display();
assert_eq!(calls(), format!("{here_shown}|daemon stop\n{here_shown}|migrate\n"));
std::fs::remove_file(&log).expect("the log is cleared");
let record = crate::daemon::service::Record {
executable: root.join("bin/wsb-daemon"),
arguments: ["--worktree", &service_root.to_string_lossy(), "--port", "3123", "--peer-port", "7499"]
.map(String::from)
.to_vec(),
worktree_root: service_root.clone(),
log: machine.join("daemon.log"),
path: "/usr/bin".to_string(),
machine_dir: machine.clone(),
prj: None,
autostart: false,
};
std::fs::write(
crate::daemon::service::record_path(&machine),
serde_json::to_string_pretty(&record).expect("a record"),
)
.expect("the service record");
assert_eq!(
bring_the_machine_over(&root.join("bin"), &here),
Ok(MachineStep::Restarted { worktree: service_root.clone() })
);
let served = service_root.display();
assert_eq!(
calls(),
format!(
"{here_shown}|daemon stop\n{here_shown}|migrate\n\
{served}|daemon install --port 3123 --peer-port 7499\n{served}|daemon restart\n"
)
);
std::fs::remove_file(&log).expect("the log is cleared");
recording_wsb(&root.join("bin"), &log, "migrate");
let refused = bring_the_machine_over(&root.join("bin"), &here).expect_err("a migration that fails");
assert!(refused.contains("`wsb migrate` failed"), "{refused}");
assert_eq!(calls(), format!("{here_shown}|daemon stop\n{here_shown}|migrate\n"), "steps ran after a failure");
}