diff --git a/AGENTS.md b/AGENTS.md index 0408346..48b0ad1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -48,15 +48,17 @@ placeholder. Each feature area is a directory module under a crate's `src/`. A module root explains the module, wires its pieces together, and exposes the smallest useful API. Move substantial type definitions into `types.rs` and put module-local -unit tests in a dedicated `test.rs`, wired from the bottom of the module root +unit tests in a sibling `_tests.rs`, wired from the bottom of the module root with: ```rust #[cfg(test)] -mod test; +#[path = "mod_tests.rs"] +mod tests; ``` -Do not accumulate inline `mod tests` blocks in implementation files, and do not +Do not write inline `mod tests` blocks in implementation files, do not name a test +file `test.rs`, `tests.rs` or `_test.rs`, and do not let a general-purpose `utils.rs` or `helpers.rs` grow — those are a symptom of a missing module. Prefer many small modules that each do one thing well over few broad ones. @@ -186,7 +188,7 @@ new module capability requires more. ## Testing -- Module-local unit tests live in `crates//src//test.rs` and may +- Module-local unit tests live in `crates//src//mod_tests.rs` and may touch private items. - `unwrap_used`, `expect_used`, and `panic` are denied in **test** targets too. Return `Result` from a test and use `?` for paths that should succeed; assert @@ -218,7 +220,7 @@ Write documentation for the reader who has never seen the code. - Every public item gets a rustdoc comment. `missing_docs` is a warning that CI treats as an error. -- Start every `mod.rs` and `test.rs` with a concise module-level `//!` +- Start every `mod.rs` and `*_tests.rs` with a concise module-level `//!` description. - `src/lib.rs` carries the crate-level overview: what the crate does, the primary entry points, and a short runnable example. @@ -307,3 +309,28 @@ For automated contributors specifically: credentials, and never paste them into a pull request or issue. 7. **Ask only when blocked.** Make routine judgment calls yourself; escalate only irreversible decisions or genuine forks with no clear default. + +## Tests live in `*_tests.rs` files + +- Unit tests are never inline. Do not write a `#[cfg(test)] mod tests { ... }` + block in a source file. Put the tests in a sibling `_tests.rs` + (`mod_tests.rs` beside a `mod.rs`, `lib_tests.rs` beside `lib.rs`) and declare + it at the bottom of the module: + + ```rust + #[cfg(test)] + #[path = "foo_tests.rs"] + mod tests; + ``` + +- The test file starts with `use super::*;` and carries no `#[cfg(test)]` of its + own. It is still a child module, so it reaches private items exactly as an + inline module did. +- Name test files `_tests.rs`; a second group for the same module is + `__tests.rs`. Never `test.rs`, `tests.rs` or `_test.rs`. +- Integration tests stay in the crate's `tests/` directory. +- OpenHuman's `scripts/externalize-inline-tests.mjs --write` moves + inline test modules out mechanically; without `--write` it only reports. +- Existing `test.rs` and `_test.rs` files predate this rule. Rename each + to `_tests.rs` (keep its `mod` name, add the `#[path]` attribute) the + next time you touch it. diff --git a/crates/tinybox-cli/src/command/test.rs b/crates/tinybox-cli/src/command/test.rs index 409c249..4364562 100644 --- a/crates/tinybox-cli/src/command/test.rs +++ b/crates/tinybox-cli/src/command/test.rs @@ -89,7 +89,7 @@ async fn a_box_can_be_created_used_and_removed() -> Result<()> { let removed = invoke(dir.path(), &["rm", "box-0"]).await; assert_eq!(removed.code, 0); - assert!(invoke(dir.path(), &["ls"]).await.out.is_empty()); + assert_eq!(invoke(dir.path(), &["ls"]).await.out.len(), 0); Ok(()) } @@ -197,7 +197,7 @@ async fn run_creates_uses_and_destroys_a_box_in_one_step() -> Result<()> { assert_eq!(executed.code, 0); assert_eq!(executed.out.trim(), "once"); // Nothing is left behind. - assert!(invoke(dir.path(), &["ls"]).await.out.is_empty()); + assert_eq!(invoke(dir.path(), &["ls"]).await.out.len(), 0); Ok(()) } @@ -209,7 +209,7 @@ async fn run_leaves_nothing_behind_when_the_command_fails() -> Result<()> { assert_eq!(executed.code, 3); // A failing command must not leak a box; the cleanup is unconditional. - assert!(invoke(dir.path(), &["ls"]).await.out.is_empty()); + assert_eq!(invoke(dir.path(), &["ls"]).await.out.len(), 0); Ok(()) } @@ -225,7 +225,7 @@ async fn run_reports_a_command_that_cannot_start_and_still_cleans_up() -> Result assert_eq!(executed.code, EXIT_TINYBOX_ERROR); assert!(executed.err.contains("error:")); - assert!(invoke(dir.path(), &["ls"]).await.out.is_empty()); + assert_eq!(invoke(dir.path(), &["ls"]).await.out.len(), 0); Ok(()) } @@ -313,7 +313,7 @@ async fn a_usage_error_reports_clap_s_exit_code() -> Result<()> { let outcome = invoke(dir.path(), &["not-a-command"]).await; assert_eq!(outcome.code, 2); - assert!(!outcome.err.is_empty()); + assert_ne!(outcome.err.len(), 0); Ok(()) } @@ -343,8 +343,8 @@ async fn requested_help_goes_to_stdout_and_usage_errors_to_stderr() -> Result<() // A mistake is a diagnostic, and stays on stderr. let misuse = invoke(dir.path(), &["not-a-command"]).await; assert_eq!(misuse.code, 2); - assert!(misuse.out.is_empty()); - assert!(!misuse.err.is_empty()); + assert_eq!(misuse.out.len(), 0); + assert_ne!(misuse.err.len(), 0); Ok(()) } @@ -848,11 +848,12 @@ async fn a_one_shot_docker_run_leaves_nothing_behind() -> Result<()> { assert_eq!(executed.code, 0); assert_eq!(executed.out.trim(), "once"); - assert!( + assert_eq!( invoke_scripted(dir.path(), host.clone(), &["ls"]) .await .out - .is_empty() + .len(), + 0 ); Ok(()) } @@ -932,7 +933,7 @@ async fn an_ssh_destination_that_would_be_read_as_an_option_is_refused() -> Resu assert_eq!(outcome.code, EXIT_TINYBOX_ERROR); assert!(outcome.err.contains("ssh destination")); - assert!(host.commands().is_empty()); + assert_eq!(host.commands().len(), 0); Ok(()) } @@ -1151,7 +1152,7 @@ async fn templates_can_be_listed_and_forgotten() -> Result<()> { assert!(listed.out.contains("sha-9f2c0e1b7a4d")); assert_eq!(invoke(dir.path(), &["template", "rm", "ci"]).await.code, 0); - assert!(invoke(dir.path(), &["template", "ls"]).await.out.is_empty()); + assert_eq!(invoke(dir.path(), &["template", "ls"]).await.out.len(), 0); Ok(()) } diff --git a/crates/tinybox-cli/src/store/test.rs b/crates/tinybox-cli/src/store/test.rs index e06df17..472adb0 100644 --- a/crates/tinybox-cli/src/store/test.rs +++ b/crates/tinybox-cli/src/store/test.rs @@ -38,7 +38,7 @@ fn a_missing_file_reads_as_an_empty_store() -> Result<()> { // No initialization step: a fresh install must just work. assert!(!store.path().exists()); - assert!(store.list()?.is_empty()); + assert_eq!(store.list()?.len(), 0); assert_eq!(store.allocate_id()?.as_str(), "box-0"); Ok(()) } @@ -117,7 +117,7 @@ fn state_changes_and_removals_persist() -> Result<()> { ); store.remove(&recorded.id)?; - assert!(FileStore::new(store.path()).list()?.is_empty()); + assert_eq!(FileStore::new(store.path()).list()?.len(), 0); Ok(()) } diff --git a/crates/tinybox-cli/src/templates/test.rs b/crates/tinybox-cli/src/templates/test.rs index 523c92a..72c70d7 100644 --- a/crates/tinybox-cli/src/templates/test.rs +++ b/crates/tinybox-cli/src/templates/test.rs @@ -30,7 +30,7 @@ fn a_missing_file_reads_as_an_empty_index() -> Result<()> { let (templates, _dir) = templates()?; assert!(!templates.path().exists()); - assert!(templates.list()?.is_empty()); + assert_eq!(templates.list()?.len(), 0); Ok(()) } @@ -73,7 +73,7 @@ fn removals_persist() -> Result<()> { templates.remove(&name("ci")?)?; - assert!(FileTemplates::new(templates.path()).list()?.is_empty()); + assert_eq!(FileTemplates::new(templates.path()).list()?.len(), 0); Ok(()) } diff --git a/crates/tinybox-cli/tests/binary.rs b/crates/tinybox-cli/tests/binary.rs index 7c177d5..0c761fa 100644 --- a/crates/tinybox-cli/tests/binary.rs +++ b/crates/tinybox-cli/tests/binary.rs @@ -56,7 +56,7 @@ fn a_box_is_created_used_and_destroyed_through_the_binary() -> io::Result<()> { let removed = tinybox(state.path(), &["rm", "box-0"])?; assert!(removed.status.success()); - assert!(stdout(&tinybox(state.path(), &["ls"])?).is_empty()); + assert_eq!(stdout(&tinybox(state.path(), &["ls"])?).len(), 0); Ok(()) } diff --git a/crates/tinybox-core/src/capability/test.rs b/crates/tinybox-core/src/capability/test.rs index 9bdc4d6..aa7eeda 100644 --- a/crates/tinybox-core/src/capability/test.rs +++ b/crates/tinybox-core/src/capability/test.rs @@ -41,7 +41,7 @@ fn passthrough_admits_it_isolates_nothing() { fn each_builder_method_adds_exactly_one_capability() { let base = SandboxCapabilities::new(IsolationLevel::Kernel, SnapshotSupport::None); - assert!(base.declared().is_empty()); + assert_eq!(base.declared().len(), 0); assert_eq!(base.with_fork().declared(), [Capability::Fork]); assert_eq!( base.with_pause_resume().declared(), @@ -104,7 +104,7 @@ fn a_declared_set_lists_snapshot_and_feature_capabilities_together() { ] ); assert_eq!(MICROVM.declared(), Capability::ALL); - assert!(SandboxCapabilities::PASSTHROUGH.declared().is_empty()); + assert_eq!(SandboxCapabilities::PASSTHROUGH.declared().len(), 0); } #[test] diff --git a/crates/tinybox-core/src/shell/scan_test.rs b/crates/tinybox-core/src/shell/scan_test.rs index 4e2089c..b5d7c02 100644 --- a/crates/tinybox-core/src/shell/scan_test.rs +++ b/crates/tinybox-core/src/shell/scan_test.rs @@ -53,8 +53,8 @@ fn single_ampersand_stays_inside_its_segment() { #[test] fn empty_and_blank_segments_are_dropped() { - assert!(segs("").is_empty()); - assert!(segs(" ; ;\n").is_empty()); + assert_eq!(segs("").len(), 0); + assert_eq!(segs(" ; ;\n").len(), 0); assert_eq!(segs(";;a;;"), ["a"]); } diff --git a/crates/tinybox-core/src/store/test.rs b/crates/tinybox-core/src/store/test.rs index 903196f..ec684d7 100644 --- a/crates/tinybox-core/src/store/test.rs +++ b/crates/tinybox-core/src/store/test.rs @@ -79,7 +79,7 @@ fn removing_a_box_forgets_it() -> Result<()> { store.remove(&recorded.id)?; - assert!(store.list()?.is_empty()); + assert_eq!(store.list()?.len(), 0); assert!(store.get(&recorded.id).is_err()); Ok(()) } @@ -140,7 +140,7 @@ fn allocation_steps_past_identifiers_that_are_not_generated_names() -> Result<() #[test] fn a_default_store_is_empty() -> Result<()> { - assert!(MemoryStore::default().list()?.is_empty()); + assert_eq!(MemoryStore::default().list()?.len(), 0); Ok(()) } @@ -304,7 +304,7 @@ fn a_failure_that_is_not_a_collision_is_not_retried() -> Result<()> { // The rest of the stub behaves as the trait requires, so a future change // that starts calling it does not silently get nonsense. let id = BoxId::new("box-0")?; - assert!(ReadOnly.list()?.is_empty()); + assert_eq!(ReadOnly.list()?.len(), 0); assert!(ReadOnly.get(&id).is_err()); assert!(ReadOnly.set_state(&id, BoxState::Stopped).is_ok()); assert!(ReadOnly.remove(&id).is_ok()); diff --git a/crates/tinybox-core/src/template/test.rs b/crates/tinybox-core/src/template/test.rs index 5e6b2cb..9343c87 100644 --- a/crates/tinybox-core/src/template/test.rs +++ b/crates/tinybox-core/src/template/test.rs @@ -102,7 +102,7 @@ fn a_template_name_follows_the_usual_identifier_rule() { #[test] fn an_empty_index_lists_nothing() -> Result<()> { - assert!(MemoryTemplates::default().list()?.is_empty()); + assert_eq!(MemoryTemplates::default().list()?.len(), 0); Ok(()) } diff --git a/crates/tinybox-docker/src/oneshot/test.rs b/crates/tinybox-docker/src/oneshot/test.rs index 8765b12..533fae0 100644 --- a/crates/tinybox-docker/src/oneshot/test.rs +++ b/crates/tinybox-docker/src/oneshot/test.rs @@ -200,7 +200,7 @@ async fn run_times_out_with_the_pinned_message() { .unwrap(); assert!(got.timed_out); assert_eq!(got.exit_code, -1); - assert!(got.stdout.is_empty()); + assert_eq!(got.stdout.len(), 0); assert_eq!(got.stderr, "Command timed out after 0s and was killed"); } diff --git a/crates/tinybox-docker/src/sandbox/test.rs b/crates/tinybox-docker/src/sandbox/test.rs index 85605ed..b3807b1 100644 --- a/crates/tinybox-docker/src/sandbox/test.rs +++ b/crates/tinybox-docker/src/sandbox/test.rs @@ -336,7 +336,7 @@ async fn a_docker_failure_carries_dockers_own_diagnostic() -> Result<()> { }) ); // A container that was never created must leave no record. - assert!(store.list()?.is_empty()); + assert_eq!(store.list()?.len(), 0); Ok(()) } @@ -526,7 +526,7 @@ async fn destroying_a_box_removes_the_container_before_the_record() -> Result<() assert_eq!(argv[0..2], ["docker", "rm"]); assert!(argv.contains(&"--force".to_owned())); assert!(argv.contains(&"--volumes".to_owned())); - assert!(store.list()?.is_empty()); + assert_eq!(store.list()?.len(), 0); Ok(()) } @@ -554,7 +554,7 @@ async fn an_unknown_box_is_reported_without_touching_docker() -> Result<()> { assert_eq!(sandbox.inspect(&missing).await.err(), expected); assert_eq!(sandbox.destroy(&missing).await.err(), expected); assert_eq!(sandbox.snapshot(&missing).await.err(), expected); - assert!(host.commands().is_empty()); + assert_eq!(host.commands().len(), 0); Ok(()) } @@ -572,7 +572,7 @@ async fn an_invalid_spec_never_reaches_docker() -> Result<()> { limit: "memory_bytes" }) ); - assert!(host.commands().is_empty()); + assert_eq!(host.commands().len(), 0); Ok(()) } diff --git a/crates/tinybox-host/src/local/test.rs b/crates/tinybox-host/src/local/test.rs index b34578e..55f17dc 100644 --- a/crates/tinybox-host/src/local/test.rs +++ b/crates/tinybox-host/src/local/test.rs @@ -25,7 +25,7 @@ async fn it_runs_a_command_and_captures_stdout() -> Result<()> { assert!(output.succeeded()); assert_eq!(output.exit_code, 0); assert_eq!(output.stdout_lossy().trim(), "hello"); - assert!(output.stderr.is_empty()); + assert_eq!(output.stderr.len(), 0); Ok(()) } @@ -137,7 +137,7 @@ async fn the_child_inherits_the_parent_environment() -> Result<()> { .run(&ExecRequest::new(["sh", "-c", "printf %s \"$PATH\""])) .await?; - assert!(!output.stdout.is_empty()); + assert_ne!(output.stdout.len(), 0); Ok(()) } @@ -160,7 +160,7 @@ async fn a_child_reading_stdin_does_not_hang() -> Result<()> { let output = LocalHost::new().run(&ExecRequest::new(["cat"])).await?; assert!(output.succeeded()); - assert!(output.stdout.is_empty()); + assert_eq!(output.stdout.len(), 0); Ok(()) } diff --git a/crates/tinybox-jail/src/detect_tests.rs b/crates/tinybox-jail/src/detect_tests.rs index b76e9da..d817cc2 100644 --- a/crates/tinybox-jail/src/detect_tests.rs +++ b/crates/tinybox-jail/src/detect_tests.rs @@ -16,5 +16,5 @@ fn unavailable_backend_rejects_spawning() { #[test] fn backend_detection_returns_a_backend() { - assert!(!pick_backend().name().is_empty()); + assert_ne!(pick_backend().name().len(), 0); } diff --git a/crates/tinybox-jail/src/jail_tests.rs b/crates/tinybox-jail/src/jail_tests.rs index c949b19..f84537a 100644 --- a/crates/tinybox-jail/src/jail_tests.rs +++ b/crates/tinybox-jail/src/jail_tests.rs @@ -6,7 +6,7 @@ fn defaults_are_permissive() { assert!(j.allow_net); assert!(j.allow_subprocess); assert_eq!(j.label, "x"); - assert!(j.read_only.is_empty()); + assert_eq!(j.read_only.len(), 0); } #[test] diff --git a/crates/tinybox-jail/src/mod_tests.rs b/crates/tinybox-jail/src/mod_tests.rs index cb3eb22..41e89b1 100644 --- a/crates/tinybox-jail/src/mod_tests.rs +++ b/crates/tinybox-jail/src/mod_tests.rs @@ -37,7 +37,7 @@ fn missing_root_errors() { #[test] fn default_backend_returns_something() { let b = default_backend(); - assert!(!b.name().is_empty()); + assert_ne!(b.name().len(), 0); } #[test] diff --git a/crates/tinybox-linux/src/sandbox/test.rs b/crates/tinybox-linux/src/sandbox/test.rs index e2aaee6..9c0b022 100644 --- a/crates/tinybox-linux/src/sandbox/test.rs +++ b/crates/tinybox-linux/src/sandbox/test.rs @@ -94,7 +94,7 @@ fn it_declares_kernel_isolation_and_nothing_it_cannot_do() { assert!(!caps.supports(Capability::PauseResume)); // Limits need a systemd user session, so they are not claimed by default. assert!(!caps.supports(Capability::ResourceLimits)); - assert!(caps.declared().is_empty()); + assert_eq!(caps.declared().len(), 0); } #[test] diff --git a/crates/tinybox-microvm/src/sandbox/guest/test.rs b/crates/tinybox-microvm/src/sandbox/guest/test.rs index 593628e..e6a0c77 100644 --- a/crates/tinybox-microvm/src/sandbox/guest/test.rs +++ b/crates/tinybox-microvm/src/sandbox/guest/test.rs @@ -93,7 +93,7 @@ fn a_command_crosses_as_one_base64_word() { .split_whitespace() .find_map(|part| part.strip_prefix("tinybox_cmd=")) .unwrap_or_default(); - assert!(!value.is_empty()); + assert_ne!(value.len(), 0); assert!( value .chars() diff --git a/crates/tinybox-microvm/src/sandbox/test.rs b/crates/tinybox-microvm/src/sandbox/test.rs index 34a4618..51d3ea2 100644 --- a/crates/tinybox-microvm/src/sandbox/test.rs +++ b/crates/tinybox-microvm/src/sandbox/test.rs @@ -305,7 +305,7 @@ async fn sources_it_cannot_build_a_guest_from_are_refused_at_creation() -> Resul }) ); } - assert!(host.commands().is_empty()); + assert_eq!(host.commands().len(), 0); Ok(()) } @@ -384,7 +384,7 @@ async fn an_unknown_box_is_reported_without_booting_anything() -> Result<()> { .err(), expected ); - assert!(host.commands().is_empty()); + assert_eq!(host.commands().len(), 0); Ok(()) } diff --git a/crates/tinybox-microvm/tests/live_microvm.rs b/crates/tinybox-microvm/tests/live_microvm.rs index ad9de11..3c9e768 100644 --- a/crates/tinybox-microvm/tests/live_microvm.rs +++ b/crates/tinybox-microvm/tests/live_microvm.rs @@ -119,7 +119,7 @@ async fn live_the_guest_has_its_own_kernel() -> Result<()> { // The whole point of this backend: not a namespace of the host kernel, a // different kernel entirely. A container cannot produce this result. assert_ne!(guest, host, "the guest is running the host's kernel"); - assert!(!guest.is_empty()); + assert_ne!(guest.len(), 0); Ok(()) } diff --git a/crates/tinybox-ssh/tests/live_ssh.rs b/crates/tinybox-ssh/tests/live_ssh.rs index e06f0e6..beece13 100644 --- a/crates/tinybox-ssh/tests/live_ssh.rs +++ b/crates/tinybox-ssh/tests/live_ssh.rs @@ -286,7 +286,7 @@ async fn live_a_missing_working_directory_fails_rather_than_running_elsewhere() // `cd ... &&` rather than `;`: running a build in the wrong directory would // be worse than failing. assert!(!output.succeeded()); - assert!(output.stdout_lossy().trim().is_empty()); + assert_eq!(output.stdout_lossy().trim().len(), 0); Ok(()) } @@ -398,7 +398,7 @@ async fn live_an_unreachable_machine_fails_instead_of_hanging() -> Result<()> { let output = remote.run(&ExecRequest::new(["true"])).await?; assert!(!output.succeeded()); - assert!(!output.stderr.is_empty()); + assert_ne!(output.stderr.len(), 0); Ok(()) } diff --git a/crates/tinybox-sync/src/exclude/test.rs b/crates/tinybox-sync/src/exclude/test.rs index 8fcb3d7..679606d 100644 --- a/crates/tinybox-sync/src/exclude/test.rs +++ b/crates/tinybox-sync/src/exclude/test.rs @@ -42,7 +42,7 @@ fn a_workspace_with_no_ignore_files_excludes_nothing() -> Result<()> { let exclusions = Exclusions::read(dir.path())?; assert!(exclusions.is_empty()); - assert!(exclusions.sources().is_empty()); + assert_eq!(exclusions.sources().len(), 0); assert!(!excludes_file(&exclusions, "anything.txt")); Ok(()) } diff --git a/crates/tinybox-sync/src/transfer/test.rs b/crates/tinybox-sync/src/transfer/test.rs index a79cb82..0683e19 100644 --- a/crates/tinybox-sync/src/transfer/test.rs +++ b/crates/tinybox-sync/src/transfer/test.rs @@ -364,7 +364,7 @@ fn packing_the_same_tree_twice_produces_identical_bytes() -> Result<()> { // Deterministic headers: no timestamps, no uid, no gid. Without this an // archive would differ on every run and be useless to cache or compare. assert_eq!(once, twice); - assert!(!once.is_empty()); + assert_ne!(once.len(), 0); Ok(()) } diff --git a/vendor/tinybus b/vendor/tinybus index ceeef87..dc8c92f 160000 --- a/vendor/tinybus +++ b/vendor/tinybus @@ -1 +1 @@ -Subproject commit ceeef8752ef20c3f71ec224b14341351c77e4737 +Subproject commit dc8c92f58b5dcb8b8af4a5b3a81aafe3333fa1e4