diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 752d61f..07e353e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -56,6 +56,11 @@ jobs: components: rustfmt, clippy targets: wasm32-wasip2, x86_64-pc-windows-gnu - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 + # Ubuntu's AppArmor lets an unprivileged process create a user namespace but not map + # its uid in it, so no confined test could run. A laptop installs the amem profile in + # packaging/apparmor; the tests' own probe is unshare, not amem, so this throwaway VM + # turns the restriction off instead + - run: sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 - run: make check # the embedded PostgreSQL for the ledger tests lists its releases through GitHub's # API; unauthenticated, a runner's address is soon refused @@ -164,6 +169,8 @@ jobs: - uses: taiki-e/install-action@83ac0ad63c0167e6f06796fab0fce28db1bf3db0 # v2.87.22 with: tool: cargo-llvm-cov + # the confined tests run here too, as in the check job + - run: sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 - run: make coverage # the embedded PostgreSQL for the ledger tests lists its releases through GitHub's # API; unauthenticated, a runner's address is soon refused diff --git a/crates/memory/tests/launch.rs b/crates/memory/tests/launch.rs index 6188226..f03c519 100644 --- a/crates/memory/tests/launch.rs +++ b/crates/memory/tests/launch.rs @@ -205,16 +205,21 @@ fn launch_refuses_without_the_credential_its_mode_needs() { } /// Whether this kernel lets an unprivileged process create user and network namespaces. -/// Ubuntu's AppArmor restriction and some CI kernels say no; the confined test is then -/// reported as skipped rather than failed. +/// Ubuntu's AppArmor restriction says no by default; a confined test is then reported as +/// skipped rather than failed, except on CI, which is set up to run it and fails instead. #[cfg(target_os = "linux")] fn namespaces_available() -> bool { - Command::new("unshare") + let available = Command::new("unshare") .args(["--user", "--map-root-user", "--net", "--mount", "true"]) .stdout(Stdio::null()) .stderr(Stdio::null()) .status() - .is_ok_and(|s| s.success()) + .is_ok_and(|s| s.success()); + assert!( + available || std::env::var_os("CI").is_none(), + "unprivileged namespaces are not available, and on CI the confined tests must run" + ); + available } #[cfg(target_os = "linux")] @@ -1037,7 +1042,8 @@ fn a_kept_tree_is_the_commit_and_repeated_runs_are_not_a_subscription_s() { /// The projection as a live mount: a shell `cat` inside the sandbox, which no hook /// sees, is a `get` the memory serves and logs. Where this host has no FUSE the launch -/// says the projection is a render, and the test says it skipped the rest. +/// says the projection is a render, and the test says it skipped the rest; on CI, which +/// has FUSE, that is a failure. #[cfg(target_os = "linux")] #[test] fn a_file_read_inside_the_sandbox_is_a_get_the_memory_serves() { @@ -1079,6 +1085,10 @@ fn a_file_read_inside_the_sandbox_is_a_get_the_memory_serves() { let reads = dm_server::reads::all(&root).unwrap(); let gets: Vec<_> = reads.iter().filter(|r| r.tool == "get").collect(); if stderr.contains("is a render:") { + assert!( + std::env::var_os("CI").is_none(), + "on CI the projection must be a live mount: {stderr}" + ); eprintln!("skipping the rest: {stderr}"); assert!(gets.is_empty(), "no hook, no mount, no get: {gets:?}"); return; diff --git a/crates/mount/tests/live.rs b/crates/mount/tests/live.rs index 5a5c889..c354bb7 100644 --- a/crates/mount/tests/live.rs +++ b/crates/mount/tests/live.rs @@ -1,5 +1,5 @@ //! The mount, live: a memory's render served over FUSE, read with ordinary file calls. -//! Skipped where the host has no FUSE, saying so. +//! Skipped where the host has no FUSE, saying so; on CI that is a failure. // An integration test is test code; a panic here is the failure being reported. #![allow(clippy::unwrap_used)] @@ -30,6 +30,20 @@ async fn rendered(root: &Path) -> (Arc, String, PathBuf) { (m, w.id, render) } +/// Whether this host can serve a FUSE mount. Without it a test says it skipped, except on +/// CI, which is set up to run it and fails instead. +fn fuse_available() -> bool { + let Err(why) = dm_mount::availability() else { + return true; + }; + assert!( + std::env::var_os("CI").is_none(), + "{why}, and on CI the mount tests must run" + ); + eprintln!("skipping: {why}"); + false +} + fn names(dir: &Path) -> Vec { let mut v: Vec = std::fs::read_dir(dir) .unwrap() @@ -42,8 +56,7 @@ fn names(dir: &Path) -> Vec { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn an_open_in_the_mount_is_a_get_the_memory_serves_logs_and_tapes() { - if let Err(why) = dm_mount::availability() { - eprintln!("skipping: {why}"); + if !fuse_available() { return; } let dir = tempfile::tempdir().unwrap(); @@ -124,8 +137,7 @@ async fn an_open_in_the_mount_is_a_get_the_memory_serves_logs_and_tapes() { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn the_record_s_word_decides_an_open_not_the_render_s() { - if let Err(why) = dm_mount::availability() { - eprintln!("skipping: {why}"); + if !fuse_available() { return; } let dir = tempfile::tempdir().unwrap();