From 1353be0c106d620991850d9f2f86dc5319f59382 Mon Sep 17 00:00:00 2001 From: Stephen Waits Date: Thu, 29 Oct 2026 18:27:54 -0600 Subject: [PATCH 1/3] ci: one workflow per portable OS, so each job's log is readable when it ends Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01HS8WZziDnyxunC23MUxrLp --- .github/workflows/portable-macos.yml | 59 +++++++++++++++++++ .../{portable.yml => portable-windows.yml} | 14 ++--- 2 files changed, 65 insertions(+), 8 deletions(-) create mode 100644 .github/workflows/portable-macos.yml rename .github/workflows/{portable.yml => portable-windows.yml} (84%) diff --git a/.github/workflows/portable-macos.yml b/.github/workflows/portable-macos.yml new file mode 100644 index 0000000..af6425a --- /dev/null +++ b/.github/workflows/portable-macos.yml @@ -0,0 +1,59 @@ +# The portable builds (plan P11a step 0, D-56): the workspace builds and +# passes its tests on macOS and Windows, not only on Linux. A workflow of +# its own per OS, not a job beside `check` or beside the other OS: GitHub +# serves a job's log only when its whole run has completed, and `check` +# takes over an hour (the Windows suite most of one), so a failure's reason +# was unreadable for that long while it sat in a shared run. +name: portable-macos +on: + push: + pull_request: + workflow_dispatch: +concurrency: + group: portable-macos-${{ github.ref }} + cancel-in-progress: true +jobs: + # The destination is one installation for macOS, Windows and Linux (D-56, + # P11a step 0): the workspace must build and pass its tests on each, not + # only here. `just check` (fmt, clippy, rustdoc, proofs' friends) stays + # the Linux job's; these run the build and the tests. The job tells + # which OS a failure is on; neither is allowed to hide the other. + portable: + name: portable (${{ matrix.os }}) + strategy: + fail-fast: false + matrix: + include: + - os: macos-latest + experimental: false + runs-on: ${{ matrix.os }} + continue-on-error: ${{ matrix.experimental }} + # Same disk discipline as above: no incremental caches, line tables only. + env: + CARGO_INCREMENTAL: "0" + CARGO_PROFILE_DEV_DEBUG: line-tables-only + CARGO_PROFILE_TEST_DEBUG: line-tables-only + # macOS: a Unix socket path holds 104 bytes, and the runner's default + # temp folder (/var/folders/...) plus a test home plus + # `Library/Application Support/wanderbooks/run/` is longer than that; + # the server then refuses the root ("too long for its sockets"). The + # tests' temp homes go under /tmp there. Windows ignores TMPDIR. + TMPDIR: /tmp + steps: + - uses: actions/checkout@v4 + - name: Rust (the version mise.toml pins) + shell: bash + run: rustup toolchain install 1.99 --profile minimal && rustup default 1.99 + - name: Room for the build + shell: bash + run: df -h . | tail -1 + - name: The server-only build (no viewer) + run: cargo build -p ledger-cli --no-default-features + - name: Everything compiles + run: cargo build --workspace --all-targets + - name: Tests + run: cargo test --workspace + - name: What the build used + if: always() + shell: bash + run: df -h . | tail -1 diff --git a/.github/workflows/portable.yml b/.github/workflows/portable-windows.yml similarity index 84% rename from .github/workflows/portable.yml rename to .github/workflows/portable-windows.yml index 774cf18..6000bd1 100644 --- a/.github/workflows/portable.yml +++ b/.github/workflows/portable-windows.yml @@ -1,16 +1,16 @@ # The portable builds (plan P11a step 0, D-56): the workspace builds and # passes its tests on macOS and Windows, not only on Linux. A workflow of -# its own, not a job beside `check`: GitHub serves a job's log only when -# its whole run has completed, and `check` takes over an hour, so a macOS -# failure's reason was unreadable for that long while it sat in the same -# run. Here each OS's run completes on its own. -name: portable +# its own per OS, not a job beside `check` or beside the other OS: GitHub +# serves a job's log only when its whole run has completed, and `check` +# takes over an hour (the Windows suite most of one), so a failure's reason +# was unreadable for that long while it sat in a shared run. +name: portable-windows on: push: pull_request: workflow_dispatch: concurrency: - group: portable-${{ github.ref }} + group: portable-windows-${{ github.ref }} cancel-in-progress: true jobs: # The destination is one installation for macOS, Windows and Linux (D-56, @@ -24,8 +24,6 @@ jobs: fail-fast: false matrix: include: - - os: macos-latest - experimental: false - os: windows-latest experimental: true runs-on: ${{ matrix.os }} From 932c05177f1d4a1f8a83f410d2c257ec9bcda744 Mon Sep 17 00:00:00 2001 From: Stephen Waits Date: Thu, 29 Oct 2026 18:36:34 -0600 Subject: [PATCH 2/3] test(cli): the terminal tests speak BSD script on macOS; a noted root is compared as the system spells it The portable macOS job, now past the server start: `script -qefc` is util-linux's spelling; macOS's BSD `script` has no -f and no -c, so the three pseudo-terminal helpers choose the spelling for the OS. And the root noted "for next time" is the canonical path (macOS writes /tmp as /private/tmp); the test compares it canonically. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01HS8WZziDnyxunC23MUxrLp --- crates/ledger-cli/tests/format_upgrade.rs | 16 ++++++++++++- crates/ledger-cli/tests/open_while_held.rs | 16 ++++++++++++- crates/ledger-cli/tests/setup.rs | 28 ++++++++++++++++++---- 3 files changed, 53 insertions(+), 7 deletions(-) diff --git a/crates/ledger-cli/tests/format_upgrade.rs b/crates/ledger-cli/tests/format_upgrade.rs index 0c9f2b8..ec4d54c 100644 --- a/crates/ledger-cli/tests/format_upgrade.rs +++ b/crates/ledger-cli/tests/format_upgrade.rs @@ -144,7 +144,7 @@ impl Home { .collect::>() .join(" "); let mut child = Command::new("script") - .args(["-qefc", &command, "/dev/null"]) + .args(script_args(&command)) .env_clear() .env("PATH", "/usr/bin:/bin") .env("HOME", self.dir.path()) @@ -973,3 +973,17 @@ fn books_p2_served_cut_over_whole() { "and never after" ); } + +/// `script`'s arguments to run `command` on a pseudo-terminal, quietly, +/// with the command's exit code as its own: util-linux's spelling on +/// Linux, BSD's on macOS (no `-f`, no `-c`; the command after the file). +fn script_args(command: &str) -> Vec { + if cfg!(target_os = "macos") { + ["-qeF", "/dev/null", "/bin/sh", "-c", command] + } else { + ["-qefc", command, "/dev/null"] + } + .into_iter() + .map(String::from) + .collect() +} diff --git a/crates/ledger-cli/tests/open_while_held.rs b/crates/ledger-cli/tests/open_while_held.rs index fd6b33e..9f79a2f 100644 --- a/crates/ledger-cli/tests/open_while_held.rs +++ b/crates/ledger-cli/tests/open_while_held.rs @@ -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(["-qefc", &args.join(" "), "/dev/null"]) + .args(script_args(&args.join(" "))) .env_clear() .env("PATH", "/usr/bin:/bin") .env("HOME", home.dir.path()) @@ -2948,3 +2948,17 @@ fn token_check_says_where_the_token_file_stands_and_an_unreadable_one_is_said() let (_, listed, _) = token(&["list"]); assert!(listed.contains("No tokens yet"), "{listed}"); } + +/// `script`'s arguments to run `command` on a pseudo-terminal, quietly, +/// with the command's exit code as its own: util-linux's spelling on +/// Linux, BSD's on macOS (no `-f`, no `-c`; the command after the file). +fn script_args(command: &str) -> Vec { + if cfg!(target_os = "macos") { + ["-qeF", "/dev/null", "/bin/sh", "-c", command] + } else { + ["-qefc", command, "/dev/null"] + } + .into_iter() + .map(String::from) + .collect() +} diff --git a/crates/ledger-cli/tests/setup.rs b/crates/ledger-cli/tests/setup.rs index 71806b2..0a76c19 100644 --- a/crates/ledger-cli/tests/setup.rs +++ b/crates/ledger-cli/tests/setup.rs @@ -95,7 +95,11 @@ fn a_root_elsewhere_is_noted_for_next_time() { ledger_store::root::platform_data_dir(home.path()).join("wanderbooks.location"), ) .unwrap(); - assert_eq!(noted.trim_end(), elsewhere.display().to_string()); + // As the system spells it (macOS writes /tmp as /private/tmp). + assert_eq!( + std::path::PathBuf::from(noted.trim_end()), + std::fs::canonicalize(&elsewhere).unwrap() + ); } #[test] @@ -155,9 +159,23 @@ fn another_home_ignores_this_accounts_data_home() { ); } -/// Run `args` at a terminal (a pseudo-terminal, through util-linux -/// `script`), typing `lines` one at a time: its exit code and what the -/// terminal showed. +/// `script`'s arguments to run `command` on a pseudo-terminal, quietly, +/// with the command's exit code as its own: util-linux's spelling on +/// Linux, BSD's on macOS (no `-f`, no `-c`; the command after the file). +fn script_args(command: &str) -> Vec { + if cfg!(target_os = "macos") { + ["-qeF", "/dev/null", "/bin/sh", "-c", command] + } else { + ["-qefc", command, "/dev/null"] + } + .into_iter() + .map(String::from) + .collect() +} + +/// Run `args` at a terminal (a pseudo-terminal, through `script`), +/// typing `lines` one at a time: its exit code and what the terminal +/// 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()) @@ -165,7 +183,7 @@ fn at_terminal(args: &[String], lines: &[&str]) -> (i32, String) { .collect::>() .join(" "); let mut child = Command::new("script") - .args(["-qefc", &command, "/dev/null"]) + .args(script_args(&command)) .env_remove("XDG_DATA_HOME") .stdin(std::process::Stdio::piped()) .stdout(std::process::Stdio::piped()) From a8ce2e7d6cd6f8c2db29d48d933f0cd07d0d43c7 Mon Sep 17 00:00:00 2001 From: Stephen Waits Date: Thu, 29 Oct 2026 18:37:38 -0600 Subject: [PATCH 3/3] test(cli): the script helper's two spellings share one type 932c0517 pushed a helper whose branches were arrays of different lengths; the three test binaries did not compile. A slice now. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01HS8WZziDnyxunC23MUxrLp --- crates/ledger-cli/tests/format_upgrade.rs | 12 +++++------- crates/ledger-cli/tests/open_while_held.rs | 12 +++++------- crates/ledger-cli/tests/setup.rs | 12 +++++------- 3 files changed, 15 insertions(+), 21 deletions(-) diff --git a/crates/ledger-cli/tests/format_upgrade.rs b/crates/ledger-cli/tests/format_upgrade.rs index ec4d54c..b5a9dba 100644 --- a/crates/ledger-cli/tests/format_upgrade.rs +++ b/crates/ledger-cli/tests/format_upgrade.rs @@ -978,12 +978,10 @@ fn books_p2_served_cut_over_whole() { /// with the command's exit code as its own: util-linux's spelling on /// Linux, BSD's on macOS (no `-f`, no `-c`; the command after the file). fn script_args(command: &str) -> Vec { - if cfg!(target_os = "macos") { - ["-qeF", "/dev/null", "/bin/sh", "-c", command] + let args: &[&str] = if cfg!(target_os = "macos") { + &["-qeF", "/dev/null", "/bin/sh", "-c", command] } else { - ["-qefc", command, "/dev/null"] - } - .into_iter() - .map(String::from) - .collect() + &["-qefc", command, "/dev/null"] + }; + args.iter().map(|a| a.to_string()).collect() } diff --git a/crates/ledger-cli/tests/open_while_held.rs b/crates/ledger-cli/tests/open_while_held.rs index 9f79a2f..d1275c8 100644 --- a/crates/ledger-cli/tests/open_while_held.rs +++ b/crates/ledger-cli/tests/open_while_held.rs @@ -2953,12 +2953,10 @@ fn token_check_says_where_the_token_file_stands_and_an_unreadable_one_is_said() /// with the command's exit code as its own: util-linux's spelling on /// Linux, BSD's on macOS (no `-f`, no `-c`; the command after the file). fn script_args(command: &str) -> Vec { - if cfg!(target_os = "macos") { - ["-qeF", "/dev/null", "/bin/sh", "-c", command] + let args: &[&str] = if cfg!(target_os = "macos") { + &["-qeF", "/dev/null", "/bin/sh", "-c", command] } else { - ["-qefc", command, "/dev/null"] - } - .into_iter() - .map(String::from) - .collect() + &["-qefc", command, "/dev/null"] + }; + args.iter().map(|a| a.to_string()).collect() } diff --git a/crates/ledger-cli/tests/setup.rs b/crates/ledger-cli/tests/setup.rs index 0a76c19..8e2dddb 100644 --- a/crates/ledger-cli/tests/setup.rs +++ b/crates/ledger-cli/tests/setup.rs @@ -163,14 +163,12 @@ fn another_home_ignores_this_accounts_data_home() { /// with the command's exit code as its own: util-linux's spelling on /// Linux, BSD's on macOS (no `-f`, no `-c`; the command after the file). fn script_args(command: &str) -> Vec { - if cfg!(target_os = "macos") { - ["-qeF", "/dev/null", "/bin/sh", "-c", command] + let args: &[&str] = if cfg!(target_os = "macos") { + &["-qeF", "/dev/null", "/bin/sh", "-c", command] } else { - ["-qefc", command, "/dev/null"] - } - .into_iter() - .map(String::from) - .collect() + &["-qefc", command, "/dev/null"] + }; + args.iter().map(|a| a.to_string()).collect() } /// Run `args` at a terminal (a pseudo-terminal, through `script`),