Merge branch 'main' into P11c
This commit is contained in:
commit
a09008504d
7 files changed
+137
-33
No files matched your search
@@ -139,10 +139,11 @@ impl Home {
|
||||
/// asked for (a prompt ending ": " or "] "): its exit code and what
|
||||
/// the terminal showed.
|
||||
fn at_terminal(&self, words: &[String], lines: &[&str]) -> (i32, String) {
|
||||
let command = std::iter::once(env!("CARGO_BIN_EXE_wanderbooks").to_string())
|
||||
.chain(words.iter().cloned())
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
let command = shell_join(
|
||||
&std::iter::once(env!("CARGO_BIN_EXE_wanderbooks").to_string())
|
||||
.chain(words.iter().cloned())
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
let mut child = Command::new("script")
|
||||
.args(script_args(&command))
|
||||
.env_clear()
|
||||
@@ -985,3 +986,20 @@ fn script_args(command: &str) -> Vec<String> {
|
||||
};
|
||||
args.iter().map(|a| a.to_string()).collect()
|
||||
}
|
||||
|
||||
/// `words` as one shell command line, each word single-quoted (a macOS
|
||||
/// root has a space in it: `Library/Application Support`), except a
|
||||
/// word that is a shell operator a test means as one (`>`, `2>&1`, `|`).
|
||||
fn shell_join(words: &[String]) -> String {
|
||||
words
|
||||
.iter()
|
||||
.map(|w| {
|
||||
if matches!(w.as_str(), ">" | ">>" | "<" | "|" | "2>&1") {
|
||||
w.clone()
|
||||
} else {
|
||||
format!("'{}'", w.replace('\'', "'\\''"))
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ")
|
||||
}
|
||||
@@ -1944,7 +1944,7 @@ fn the_books_command_takes_its_flags_and_asks_before_adopting() {
|
||||
args.push(a.to_string());
|
||||
}
|
||||
let mut child = Command::new("script")
|
||||
.args(script_args(&args.join(" ")))
|
||||
.args(script_args(&shell_join(&args)))
|
||||
.env_clear()
|
||||
.env("PATH", "/usr/bin:/bin")
|
||||
.env("HOME", home.dir.path())
|
||||
@@ -2960,3 +2960,20 @@ fn script_args(command: &str) -> Vec<String> {
|
||||
};
|
||||
args.iter().map(|a| a.to_string()).collect()
|
||||
}
|
||||
|
||||
/// `words` as one shell command line, each word single-quoted (a macOS
|
||||
/// root has a space in it: `Library/Application Support`), except a
|
||||
/// word that is a shell operator a test means as one (`>`, `2>&1`, `|`).
|
||||
fn shell_join(words: &[String]) -> String {
|
||||
words
|
||||
.iter()
|
||||
.map(|w| {
|
||||
if matches!(w.as_str(), ">" | ">>" | "<" | "|" | "2>&1") {
|
||||
w.clone()
|
||||
} else {
|
||||
format!("'{}'", w.replace('\'', "'\\''"))
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ")
|
||||
}
|
||||
@@ -176,10 +176,11 @@ fn script_args(command: &str) -> Vec<String> {
|
||||
/// showed.
|
||||
fn at_terminal(args: &[String], lines: &[&str]) -> (i32, String) {
|
||||
use std::io::Write;
|
||||
let command = std::iter::once(env!("CARGO_BIN_EXE_wanderbooks").to_string())
|
||||
.chain(args.iter().cloned())
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
let command = shell_join(
|
||||
&std::iter::once(env!("CARGO_BIN_EXE_wanderbooks").to_string())
|
||||
.chain(args.iter().cloned())
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
let mut child = Command::new("script")
|
||||
.args(script_args(&command))
|
||||
.env_remove("XDG_DATA_HOME")
|
||||
@@ -323,3 +324,20 @@ fn the_passphrase_is_typed_by_a_person_and_changed_only_with_the_one_in_force()
|
||||
let kept = std::fs::read_to_string(auth.join("owner.json")).unwrap();
|
||||
assert!(!kept.contains("staple"), "the text itself was kept");
|
||||
}
|
||||
|
||||
/// `words` as one shell command line, each word single-quoted (a macOS
|
||||
/// root has a space in it: `Library/Application Support`), except a
|
||||
/// word that is a shell operator a test means as one (`>`, `2>&1`, `|`).
|
||||
fn shell_join(words: &[String]) -> String {
|
||||
words
|
||||
.iter()
|
||||
.map(|w| {
|
||||
if matches!(w.as_str(), ">" | ">>" | "<" | "|" | "2>&1") {
|
||||
w.clone()
|
||||
} else {
|
||||
format!("'{}'", w.replace('\'', "'\\''"))
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ")
|
||||
}
|
||||
@@ -24,6 +24,9 @@ fn platform() -> String {
|
||||
format!("{}-{}", std::env::consts::OS, std::env::consts::ARCH)
|
||||
}
|
||||
|
||||
/// A program's hash: the tests compare whole programs by it, never by
|
||||
/// their bytes, so a failure's message is a line, not the two binaries
|
||||
/// (GitHub dropped a step's whole log on one such message).
|
||||
fn sha256(bytes: &[u8]) -> String {
|
||||
ledger_store::journal::sha256_hex(bytes)
|
||||
}
|
||||
@@ -371,7 +374,7 @@ fn an_update_replaces_the_program_keeps_the_old_one_and_rollback_brings_it_back(
|
||||
release.publish("0.0.1", &old, |_| {});
|
||||
let older = home.run("update", Some(&release));
|
||||
assert!(!older.status.success(), "{}", text(&older));
|
||||
assert_eq!(home.program(), old);
|
||||
assert_eq!(sha256(&home.program()), sha256(&old));
|
||||
assert!(!home.updates().join("previous").exists());
|
||||
release.publish("9.0.0", &next, |_| {});
|
||||
|
||||
@@ -382,9 +385,16 @@ fn an_update_replaces_the_program_keeps_the_old_one_and_rollback_brings_it_back(
|
||||
let out = home.run("update", Some(&release));
|
||||
assert!(out.status.success(), "{}", text(&out));
|
||||
assert!(text(&out).contains("from 0.1.0 to 9.0.0"), "{}", text(&out));
|
||||
assert_eq!(home.program(), next, "the new program is in place");
|
||||
assert_eq!(
|
||||
sha256(&home.program()),
|
||||
sha256(&next),
|
||||
"the new program is in place"
|
||||
);
|
||||
let kept = home.updates().join("previous");
|
||||
assert_eq!(std::fs::read(kept.join("wanderbooks")).unwrap(), old);
|
||||
assert_eq!(
|
||||
sha256(&std::fs::read(kept.join("wanderbooks")).unwrap()),
|
||||
sha256(&old)
|
||||
);
|
||||
let meta: Value =
|
||||
serde_json::from_slice(&std::fs::read(kept.join("meta.json")).unwrap()).unwrap();
|
||||
assert_eq!(meta["version"], "0.1.0");
|
||||
@@ -397,7 +407,7 @@ fn an_update_replaces_the_program_keeps_the_old_one_and_rollback_brings_it_back(
|
||||
// Rollback: the old program is back, and nothing is kept to go back to.
|
||||
let out = home.run("rollback", None);
|
||||
assert!(out.status.success(), "{}", text(&out));
|
||||
assert_eq!(home.program(), old);
|
||||
assert_eq!(sha256(&home.program()), sha256(&old));
|
||||
assert!(!kept.exists());
|
||||
let nothing = home.run("rollback", None);
|
||||
assert!(!nothing.status.success());
|
||||
@@ -446,7 +456,7 @@ fn update_is_atomic() {
|
||||
assert!(out.status.success(), "{}", text(&out));
|
||||
assert!(reads > 0);
|
||||
assert!(bad.is_empty(), "a reader saw a part of a program: {bad:?}");
|
||||
assert_eq!(home.program(), next);
|
||||
assert_eq!(sha256(&home.program()), sha256(&next));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -459,7 +469,11 @@ fn failed_update_rolls_back() {
|
||||
let out = home.run("update", Some(&release));
|
||||
assert!(!out.status.success(), "{}", text(&out));
|
||||
assert!(text(&out).contains("didn't start"), "{}", text(&out));
|
||||
assert_eq!(home.program(), old, "the old program is back");
|
||||
assert_eq!(
|
||||
sha256(&home.program()),
|
||||
sha256(&old),
|
||||
"the old program is back"
|
||||
);
|
||||
assert_eq!(names(home.exe.parent().unwrap()), ["wanderbooks"]);
|
||||
// And it still works.
|
||||
home.runnable();
|
||||
@@ -500,7 +514,11 @@ fn unsigned_update_refused() {
|
||||
let out = home.run("update", Some(&release));
|
||||
assert!(!out.status.success(), "{name}: {}", text(&out));
|
||||
assert!(text(&out).contains("refused"), "{name}: {}", text(&out));
|
||||
assert_eq!(home.program(), old, "{name}: the program changed");
|
||||
assert_eq!(
|
||||
sha256(&home.program()),
|
||||
sha256(&old),
|
||||
"{name}: the program changed"
|
||||
);
|
||||
assert!(!home.updates().join("previous").exists(), "{name}: kept");
|
||||
assert!(!home.updates().join("9.0.0").exists(), "{name}: staged");
|
||||
assert_eq!(names(home.exe.parent().unwrap()), ["wanderbooks"], "{name}");
|
||||
@@ -550,7 +568,7 @@ fn package_managed_install_refuses_update() {
|
||||
text(&out)
|
||||
);
|
||||
}
|
||||
assert_eq!(home.program(), old);
|
||||
assert_eq!(sha256(&home.program()), sha256(&old));
|
||||
assert!(!home.updates().exists(), "nothing was made");
|
||||
}
|
||||
|
||||
@@ -578,7 +596,7 @@ fn rollback_refuses_after_a_book_consented_to_a_newer_format() {
|
||||
"{}",
|
||||
text(&out)
|
||||
);
|
||||
assert_eq!(home.program(), next, "the program stayed");
|
||||
assert_eq!(sha256(&home.program()), sha256(&next), "the program stayed");
|
||||
assert!(home.updates().join("previous/wanderbooks").is_file());
|
||||
// Declared what the kept version knows: it goes back.
|
||||
let mut meta: Value = serde_json::from_slice(&std::fs::read(&meta_path).unwrap()).unwrap();
|
||||
@@ -749,7 +767,7 @@ fn update_under_writes_in(home: Home) -> (Home, Agent, Vec<String>) {
|
||||
}
|
||||
let out = updating.join().unwrap();
|
||||
assert!(out.status.success(), "{}", text(&out));
|
||||
assert_eq!(home.program(), next);
|
||||
assert_eq!(sha256(&home.program()), sha256(&next));
|
||||
// The host and the server run the program now on disk.
|
||||
let host = wait_host(&home);
|
||||
assert_ne!(host, host_before);
|
||||
@@ -797,9 +815,9 @@ fn a_replaced_appimage_restarts_the_books() {
|
||||
let host_before = wait_host(&home);
|
||||
let out = home.run("update", Some(&release));
|
||||
assert!(out.status.success(), "{}", text(&out));
|
||||
assert_eq!(std::fs::read(&image).unwrap(), next);
|
||||
assert_eq!(sha256(&std::fs::read(&image).unwrap()), sha256(&next));
|
||||
// The mount's copy is untouched; only the image changed.
|
||||
assert_ne!(std::fs::read(&home.exe).unwrap(), next);
|
||||
assert_ne!(sha256(&std::fs::read(&home.exe).unwrap()), sha256(&next));
|
||||
assert!(
|
||||
wait_for(60, || {
|
||||
home.pids("host").len() == 1 && home.pids("host")[0] != host_before
|
||||
@@ -876,7 +894,11 @@ fn the_host_stages_a_daily_update_and_the_next_start_applies_it() {
|
||||
wait_for(30, || staged_version(&home).as_deref() == Some("9.0.0")),
|
||||
"nothing was staged"
|
||||
);
|
||||
assert_eq!(home.program(), old, "the program was swapped while open");
|
||||
assert_eq!(
|
||||
sha256(&home.program()),
|
||||
sha256(&old),
|
||||
"the program was swapped while open"
|
||||
);
|
||||
assert!(!home.updates().join("previous").exists());
|
||||
std::thread::sleep(Duration::from_millis(1200));
|
||||
assert_eq!(home.pids("host"), vec![host], "the host restarted");
|
||||
@@ -888,8 +910,14 @@ fn the_host_stages_a_daily_update_and_the_next_start_applies_it() {
|
||||
assert!(out.status.success(), "{}", text(&out));
|
||||
assert!(wait_for(20, || home.pids("host").is_empty()));
|
||||
let mut agent = Agent::start(&home, "acme");
|
||||
assert_eq!(home.program(), next, "the staged update wasn't applied");
|
||||
assert!(!home.updates().join("staged").exists());
|
||||
// The opener applies the staged copy before its host starts; the
|
||||
// session's start returns as soon as the books answer, so give the
|
||||
// apply its moment (the runner is slow).
|
||||
assert!(
|
||||
wait_for(10, || sha256(&home.program()) == sha256(&next)),
|
||||
"the staged update wasn't applied"
|
||||
);
|
||||
assert!(wait_for(10, || !home.updates().join("staged").exists()));
|
||||
assert!(
|
||||
home.updates()
|
||||
.join("previous")
|
||||
@@ -897,8 +925,8 @@ fn the_host_stages_a_daily_update_and_the_next_start_applies_it() {
|
||||
.is_file()
|
||||
);
|
||||
assert_eq!(
|
||||
std::fs::read(home.updates().join("previous/wanderbooks")).unwrap(),
|
||||
old
|
||||
sha256(&std::fs::read(home.updates().join("previous/wanderbooks")).unwrap()),
|
||||
sha256(&old)
|
||||
);
|
||||
let host = wait_host(&home);
|
||||
assert!(
|
||||
@@ -960,7 +988,7 @@ fn update_installs_the_copy_the_host_staged_without_downloading_again() {
|
||||
std::fs::remove_file(release.dir.path().join("wanderbooks-9.0.0")).unwrap();
|
||||
let out = home.run("update", None);
|
||||
assert!(out.status.success(), "{}", text(&out));
|
||||
assert_eq!(home.program(), next);
|
||||
assert_eq!(sha256(&home.program()), sha256(&next));
|
||||
assert!(!home.updates().join("staged").exists());
|
||||
}
|
||||
|
||||
@@ -990,6 +1018,21 @@ fn a_staged_update_that_was_tampered_with_is_dropped_not_applied() {
|
||||
meta["sha256"] = json!(sha256(&bytes));
|
||||
std::fs::write(staged.join("meta.json"), meta.to_string()).unwrap();
|
||||
let _agent = Agent::start(&home, "acme");
|
||||
assert_eq!(home.program(), old, "a tampered update was applied");
|
||||
assert!(!staged.exists(), "and kept");
|
||||
assert_eq!(
|
||||
sha256(&home.program()),
|
||||
sha256(&old),
|
||||
"a tampered update was applied"
|
||||
);
|
||||
// Dropped: the tampered bytes are gone. The new host may look at the
|
||||
// feed again and stage a fresh, true copy in their place; that is not
|
||||
// the tampered one kept.
|
||||
let tampered = sha256(&bytes);
|
||||
assert!(
|
||||
wait_for(10, || {
|
||||
std::fs::read(staged.join("wanderbooks"))
|
||||
.map(|b| sha256(&b) != tampered)
|
||||
.unwrap_or(true)
|
||||
}),
|
||||
"the tampered copy was kept"
|
||||
);
|
||||
}
|
||||
@@ -105,8 +105,10 @@ echo "fake-agent in $(basename "$(pwd)")"
|
||||
trap 'echo got-interrupt' INT
|
||||
trap 'echo "size $(stty size)"' WINCH
|
||||
echo ready
|
||||
# A whole second: macOS's /bin/bash is 3.2, whose `read -t` takes no
|
||||
# fraction (the portable job found it).
|
||||
while true; do
|
||||
if read -r -t 0.1 line; then echo "you-said: $line"; fi
|
||||
if read -r -t 1 line; then echo "you-said: $line"; fi
|
||||
done
|
||||
"#,
|
||||
)
|
||||
|
||||
@@ -386,7 +386,12 @@ fn the_viewer_finds_its_book_in_the_root() {
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
let dir = |id: &str| root.book_dir(&ledger_store::root::BookId::parse(id).unwrap());
|
||||
// As the system spells it: `book_in_root` answers a canonical path
|
||||
// (macOS writes /tmp as /private/tmp).
|
||||
let dir = |id: &str| {
|
||||
std::fs::canonicalize(root.book_dir(&ledger_store::root::BookId::parse(id).unwrap()))
|
||||
.unwrap()
|
||||
};
|
||||
assert_eq!(
|
||||
ledger_gui::book_in_root(Some(&r), Some("zeta")).unwrap(),
|
||||
dir("zeta")
|
||||
|
||||
@@ -719,8 +719,9 @@ mod tests {
|
||||
"{t}: no projection arm"
|
||||
);
|
||||
}
|
||||
// A type a later phase adds (plan/served.md 5.2, P8).
|
||||
let l = line("period.attested", json!({}));
|
||||
// A type no version knows yet (`period.attested` was the example
|
||||
// until P8 wrote it).
|
||||
let l = line("ledger.not_yet_written", json!({}));
|
||||
let mut ledger = Ledger::new(Currency::USD);
|
||||
assert!(is_newer(&replay_line_into_ledger(
|
||||
&mut ledger,
|
||||
|
||||
Reference in new issue
Block a user