Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 32 additions & 5 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<module>_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 `<module>_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.
Expand Down Expand Up @@ -186,7 +188,7 @@ new module capability requires more.

## Testing

- Module-local unit tests live in `crates/<crate>/src/<feature>/test.rs` and may
- Module-local unit tests live in `crates/<crate>/src/<feature>/mod_tests.rs` and may
touch private items.
Comment on lines +191 to 192

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Cover file modules in the documented test path

This path states that all module-local tests live under <feature>/mod_tests.rs, which only describes directory modules. The repository also has file modules such as jail.rs with sibling jail_tests.rs, and the new detailed section explicitly prescribes that layout, so contributors working on file modules receive conflicting instructions; describe both mod.rs and foo.rs layouts here.

AGENTS.md reference: AGENTS.md:L315-L317

Useful? React with 👍 / 👎.

- `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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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 `<module>_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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reconcile the required first line in test files

This requires every test file to begin with use super::*;, while the Documentation section requires every *_tests.rs file to begin with a module-level //! comment. Both cannot be first, and placing the inner module documentation after the import is invalid Rust, so contributors cannot satisfy both repository rules. Specify that the //! documentation comes first and use super::*; follows it.

AGENTS.md reference: AGENTS.md:L221-L224

Useful? React with 👍 / 👎.

own. It is still a child module, so it reaches private items exactly as an
inline module did.
- Name test files `<module>_tests.rs`; a second group for the same module is
`<module>_<topic>_tests.rs`. Never `test.rs`, `tests.rs` or `<module>_test.rs`.
- Integration tests stay in the crate's `tests/` directory.
- OpenHuman's `scripts/externalize-inline-tests.mjs <repo-root> --write` moves
inline test modules out mechanically; without `--write` it only reports.
- Existing `test.rs` and `<module>_test.rs` files predate this rule. Rename each
to `<module>_tests.rs` (keep its `mod` name, add the `#[path]` attribute) the
next time you touch it.
Comment on lines +334 to +336

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Rename the legacy test files changed here

This migration rule says an existing test.rs must be renamed the next time it is touched, but this same commit modifies 14 such source test files—including crates/tinybox-cli/src/command/test.rs—without renaming them or updating their module declarations. Rename the touched files now, or drop the unrelated assertion edits so the newly documented convention is not violated immediately.

AGENTS.md reference: AGENTS.md:L334-L336

Useful? React with 👍 / 👎.

23 changes: 12 additions & 11 deletions crates/tinybox-cli/src/command/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

priority high tests confident

Revert to .is\_empty() to avoid clippy len\_zero warning

Replacing .is_empty() with .len() == 0 triggers clippy's len_zero lint (part of clippy::all, which is set to warn in the workspace). Since CI runs clippy with -D warnings, this change would break the build. The same pattern is applied to many other files; all should be reverted to use .is_empty().

Suggested change
assert_eq!(invoke(dir.path(), &["ls"]).await.out.len(), 0);
assert!(invoke(dir.path(), &["ls"]).await.out.is_empty());

[RULE] clippy-lint-regression ·

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

priority medium tests confident

Rename test.rs files when touching them per the newly added rule

This PR adds a rule to AGENTS.md stating: "Existing test.rs and <module>_test.rs files predate this rule. Rename each to <module>_tests.rs the next time you touch it." This file (crates/tinybox-cli/src/command/test.rs) is being touched but not renamed, directly violating the rule the author is introducing. The same applies to every other test.rs file modified in this diff: store/test.rs, templates/test.rs, capability/test.rs, shell/scan_test.rs, store/test.rs, template/test.rs, oneshot/test.rs, sandbox/test.rs, local/test.rs, sandbox/test.rs, guest/test.rs, exclude/test.rs, transfer/test.rs, and jail variants. Each should be renamed to <module>_tests.rs and have its mod declaration updated with #[path] accordingly.

[RULE] test-file-naming ·

Ok(())
}

Expand Down Expand Up @@ -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(())
}

Expand All @@ -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(())
}

Expand All @@ -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(())
}

Expand Down Expand Up @@ -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(())
}

Expand Down Expand Up @@ -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(())
}

Expand Down Expand Up @@ -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(())
}
Expand Down Expand Up @@ -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(())
}

Expand Down Expand Up @@ -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(())
}

Expand Down
4 changes: 2 additions & 2 deletions crates/tinybox-cli/src/store/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(())
}
Expand Down Expand Up @@ -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(())
}

Expand Down
4 changes: 2 additions & 2 deletions crates/tinybox-cli/src/templates/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(())
}

Expand Down Expand Up @@ -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(())
}

Expand Down
2 changes: 1 addition & 1 deletion crates/tinybox-cli/tests/binary.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(())
}

Expand Down
4 changes: 2 additions & 2 deletions crates/tinybox-core/src/capability/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down Expand Up @@ -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]
Expand Down
4 changes: 2 additions & 2 deletions crates/tinybox-core/src/shell/scan_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"]);
}

Expand Down
6 changes: 3 additions & 3 deletions crates/tinybox-core/src/store/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(())
}
Expand Down Expand Up @@ -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(())
}

Expand Down Expand Up @@ -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());
Expand Down
2 changes: 1 addition & 1 deletion crates/tinybox-core/src/template/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(())
}

Expand Down
2 changes: 1 addition & 1 deletion crates/tinybox-docker/src/oneshot/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}

Expand Down
8 changes: 4 additions & 4 deletions crates/tinybox-docker/src/sandbox/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(())
}

Expand Down Expand Up @@ -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(())
}

Expand Down Expand Up @@ -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(())
}

Expand All @@ -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(())
}

Expand Down
6 changes: 3 additions & 3 deletions crates/tinybox-host/src/local/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(())
}

Expand Down Expand Up @@ -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(())
}

Expand All @@ -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(())
}

Expand Down
2 changes: 1 addition & 1 deletion crates/tinybox-jail/src/detect_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
2 changes: 1 addition & 1 deletion crates/tinybox-jail/src/jail_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
2 changes: 1 addition & 1 deletion crates/tinybox-jail/src/mod_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
2 changes: 1 addition & 1 deletion crates/tinybox-linux/src/sandbox/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
2 changes: 1 addition & 1 deletion crates/tinybox-microvm/src/sandbox/guest/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
4 changes: 2 additions & 2 deletions crates/tinybox-microvm/src/sandbox/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(())
}

Expand Down Expand Up @@ -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(())
}

Expand Down
Loading
Loading