Skip to content

Commit 07295e5

Browse files
authored
Merge pull request #18 from tinyhumansai/tests-to-files
docs: require unit tests in *_tests.rs files
2 parents 534112b + 935362d commit 07295e5

23 files changed

Lines changed: 78 additions & 50 deletions

File tree

‎AGENTS.md‎

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,15 +48,17 @@ placeholder.
4848
Each feature area is a directory module under a crate's `src/`. A module root
4949
explains the module, wires its pieces together, and exposes the smallest useful
5050
API. Move substantial type definitions into `types.rs` and put module-local
51-
unit tests in a dedicated `test.rs`, wired from the bottom of the module root
51+
unit tests in a sibling `<module>_tests.rs`, wired from the bottom of the module root
5252
with:
5353

5454
```rust
5555
#[cfg(test)]
56-
mod test;
56+
#[path = "mod_tests.rs"]
57+
mod tests;
5758
```
5859

59-
Do not accumulate inline `mod tests` blocks in implementation files, and do not
60+
Do not write inline `mod tests` blocks in implementation files, do not name a test
61+
file `test.rs`, `tests.rs` or `<module>_test.rs`, and do not
6062
let a general-purpose `utils.rs` or `helpers.rs` grow — those are a symptom of a
6163
missing module. Prefer many small modules that each do one thing well over few
6264
broad ones.
@@ -186,7 +188,7 @@ new module capability requires more.
186188

187189
## Testing
188190

189-
- Module-local unit tests live in `crates/<crate>/src/<feature>/test.rs` and may
191+
- Module-local unit tests live in `crates/<crate>/src/<feature>/mod_tests.rs` and may
190192
touch private items.
191193
- `unwrap_used`, `expect_used`, and `panic` are denied in **test** targets too.
192194
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.
218220

219221
- Every public item gets a rustdoc comment. `missing_docs` is a warning that CI
220222
treats as an error.
221-
- Start every `mod.rs` and `test.rs` with a concise module-level `//!`
223+
- Start every `mod.rs` and `*_tests.rs` with a concise module-level `//!`
222224
description.
223225
- `src/lib.rs` carries the crate-level overview: what the crate does, the
224226
primary entry points, and a short runnable example.
@@ -307,3 +309,28 @@ For automated contributors specifically:
307309
credentials, and never paste them into a pull request or issue.
308310
7. **Ask only when blocked.** Make routine judgment calls yourself; escalate
309311
only irreversible decisions or genuine forks with no clear default.
312+
313+
## Tests live in `*_tests.rs` files
314+
315+
- Unit tests are never inline. Do not write a `#[cfg(test)] mod tests { ... }`
316+
block in a source file. Put the tests in a sibling `<module>_tests.rs`
317+
(`mod_tests.rs` beside a `mod.rs`, `lib_tests.rs` beside `lib.rs`) and declare
318+
it at the bottom of the module:
319+
320+
```rust
321+
#[cfg(test)]
322+
#[path = "foo_tests.rs"]
323+
mod tests;
324+
```
325+
326+
- The test file starts with `use super::*;` and carries no `#[cfg(test)]` of its
327+
own. It is still a child module, so it reaches private items exactly as an
328+
inline module did.
329+
- Name test files `<module>_tests.rs`; a second group for the same module is
330+
`<module>_<topic>_tests.rs`. Never `test.rs`, `tests.rs` or `<module>_test.rs`.
331+
- Integration tests stay in the crate's `tests/` directory.
332+
- OpenHuman's `scripts/externalize-inline-tests.mjs <repo-root> --write` moves
333+
inline test modules out mechanically; without `--write` it only reports.
334+
- Existing `test.rs` and `<module>_test.rs` files predate this rule. Rename each
335+
to `<module>_tests.rs` (keep its `mod` name, add the `#[path]` attribute) the
336+
next time you touch it.

‎crates/tinybox-cli/src/command/test.rs‎

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ async fn a_box_can_be_created_used_and_removed() -> Result<()> {
8989
let removed = invoke(dir.path(), &["rm", "box-0"]).await;
9090
assert_eq!(removed.code, 0);
9191

92-
assert!(invoke(dir.path(), &["ls"]).await.out.is_empty());
92+
assert_eq!(invoke(dir.path(), &["ls"]).await.out.len(), 0);
9393
Ok(())
9494
}
9595

@@ -197,7 +197,7 @@ async fn run_creates_uses_and_destroys_a_box_in_one_step() -> Result<()> {
197197
assert_eq!(executed.code, 0);
198198
assert_eq!(executed.out.trim(), "once");
199199
// Nothing is left behind.
200-
assert!(invoke(dir.path(), &["ls"]).await.out.is_empty());
200+
assert_eq!(invoke(dir.path(), &["ls"]).await.out.len(), 0);
201201
Ok(())
202202
}
203203

@@ -209,7 +209,7 @@ async fn run_leaves_nothing_behind_when_the_command_fails() -> Result<()> {
209209

210210
assert_eq!(executed.code, 3);
211211
// A failing command must not leak a box; the cleanup is unconditional.
212-
assert!(invoke(dir.path(), &["ls"]).await.out.is_empty());
212+
assert_eq!(invoke(dir.path(), &["ls"]).await.out.len(), 0);
213213
Ok(())
214214
}
215215

@@ -225,7 +225,7 @@ async fn run_reports_a_command_that_cannot_start_and_still_cleans_up() -> Result
225225

226226
assert_eq!(executed.code, EXIT_TINYBOX_ERROR);
227227
assert!(executed.err.contains("error:"));
228-
assert!(invoke(dir.path(), &["ls"]).await.out.is_empty());
228+
assert_eq!(invoke(dir.path(), &["ls"]).await.out.len(), 0);
229229
Ok(())
230230
}
231231

@@ -313,7 +313,7 @@ async fn a_usage_error_reports_clap_s_exit_code() -> Result<()> {
313313
let outcome = invoke(dir.path(), &["not-a-command"]).await;
314314

315315
assert_eq!(outcome.code, 2);
316-
assert!(!outcome.err.is_empty());
316+
assert_ne!(outcome.err.len(), 0);
317317
Ok(())
318318
}
319319

@@ -343,8 +343,8 @@ async fn requested_help_goes_to_stdout_and_usage_errors_to_stderr() -> Result<()
343343
// A mistake is a diagnostic, and stays on stderr.
344344
let misuse = invoke(dir.path(), &["not-a-command"]).await;
345345
assert_eq!(misuse.code, 2);
346-
assert!(misuse.out.is_empty());
347-
assert!(!misuse.err.is_empty());
346+
assert_eq!(misuse.out.len(), 0);
347+
assert_ne!(misuse.err.len(), 0);
348348
Ok(())
349349
}
350350

@@ -848,11 +848,12 @@ async fn a_one_shot_docker_run_leaves_nothing_behind() -> Result<()> {
848848

849849
assert_eq!(executed.code, 0);
850850
assert_eq!(executed.out.trim(), "once");
851-
assert!(
851+
assert_eq!(
852852
invoke_scripted(dir.path(), host.clone(), &["ls"])
853853
.await
854854
.out
855-
.is_empty()
855+
.len(),
856+
0
856857
);
857858
Ok(())
858859
}
@@ -932,7 +933,7 @@ async fn an_ssh_destination_that_would_be_read_as_an_option_is_refused() -> Resu
932933

933934
assert_eq!(outcome.code, EXIT_TINYBOX_ERROR);
934935
assert!(outcome.err.contains("ssh destination"));
935-
assert!(host.commands().is_empty());
936+
assert_eq!(host.commands().len(), 0);
936937
Ok(())
937938
}
938939

@@ -1151,7 +1152,7 @@ async fn templates_can_be_listed_and_forgotten() -> Result<()> {
11511152
assert!(listed.out.contains("sha-9f2c0e1b7a4d"));
11521153

11531154
assert_eq!(invoke(dir.path(), &["template", "rm", "ci"]).await.code, 0);
1154-
assert!(invoke(dir.path(), &["template", "ls"]).await.out.is_empty());
1155+
assert_eq!(invoke(dir.path(), &["template", "ls"]).await.out.len(), 0);
11551156
Ok(())
11561157
}
11571158

‎crates/tinybox-cli/src/store/test.rs‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ fn a_missing_file_reads_as_an_empty_store() -> Result<()> {
3838

3939
// No initialization step: a fresh install must just work.
4040
assert!(!store.path().exists());
41-
assert!(store.list()?.is_empty());
41+
assert_eq!(store.list()?.len(), 0);
4242
assert_eq!(store.allocate_id()?.as_str(), "box-0");
4343
Ok(())
4444
}
@@ -117,7 +117,7 @@ fn state_changes_and_removals_persist() -> Result<()> {
117117
);
118118

119119
store.remove(&recorded.id)?;
120-
assert!(FileStore::new(store.path()).list()?.is_empty());
120+
assert_eq!(FileStore::new(store.path()).list()?.len(), 0);
121121
Ok(())
122122
}
123123

‎crates/tinybox-cli/src/templates/test.rs‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ fn a_missing_file_reads_as_an_empty_index() -> Result<()> {
3030
let (templates, _dir) = templates()?;
3131

3232
assert!(!templates.path().exists());
33-
assert!(templates.list()?.is_empty());
33+
assert_eq!(templates.list()?.len(), 0);
3434
Ok(())
3535
}
3636

@@ -73,7 +73,7 @@ fn removals_persist() -> Result<()> {
7373

7474
templates.remove(&name("ci")?)?;
7575

76-
assert!(FileTemplates::new(templates.path()).list()?.is_empty());
76+
assert_eq!(FileTemplates::new(templates.path()).list()?.len(), 0);
7777
Ok(())
7878
}
7979

‎crates/tinybox-cli/tests/binary.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ fn a_box_is_created_used_and_destroyed_through_the_binary() -> io::Result<()> {
5656

5757
let removed = tinybox(state.path(), &["rm", "box-0"])?;
5858
assert!(removed.status.success());
59-
assert!(stdout(&tinybox(state.path(), &["ls"])?).is_empty());
59+
assert_eq!(stdout(&tinybox(state.path(), &["ls"])?).len(), 0);
6060
Ok(())
6161
}
6262

‎crates/tinybox-core/src/capability/test.rs‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ fn passthrough_admits_it_isolates_nothing() {
4141
fn each_builder_method_adds_exactly_one_capability() {
4242
let base = SandboxCapabilities::new(IsolationLevel::Kernel, SnapshotSupport::None);
4343

44-
assert!(base.declared().is_empty());
44+
assert_eq!(base.declared().len(), 0);
4545
assert_eq!(base.with_fork().declared(), [Capability::Fork]);
4646
assert_eq!(
4747
base.with_pause_resume().declared(),
@@ -104,7 +104,7 @@ fn a_declared_set_lists_snapshot_and_feature_capabilities_together() {
104104
]
105105
);
106106
assert_eq!(MICROVM.declared(), Capability::ALL);
107-
assert!(SandboxCapabilities::PASSTHROUGH.declared().is_empty());
107+
assert_eq!(SandboxCapabilities::PASSTHROUGH.declared().len(), 0);
108108
}
109109

110110
#[test]

‎crates/tinybox-core/src/shell/scan_test.rs‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,8 @@ fn single_ampersand_stays_inside_its_segment() {
5353

5454
#[test]
5555
fn empty_and_blank_segments_are_dropped() {
56-
assert!(segs("").is_empty());
57-
assert!(segs(" ; ;\n").is_empty());
56+
assert_eq!(segs("").len(), 0);
57+
assert_eq!(segs(" ; ;\n").len(), 0);
5858
assert_eq!(segs(";;a;;"), ["a"]);
5959
}
6060

‎crates/tinybox-core/src/store/test.rs‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ fn removing_a_box_forgets_it() -> Result<()> {
7979

8080
store.remove(&recorded.id)?;
8181

82-
assert!(store.list()?.is_empty());
82+
assert_eq!(store.list()?.len(), 0);
8383
assert!(store.get(&recorded.id).is_err());
8484
Ok(())
8585
}
@@ -140,7 +140,7 @@ fn allocation_steps_past_identifiers_that_are_not_generated_names() -> Result<()
140140

141141
#[test]
142142
fn a_default_store_is_empty() -> Result<()> {
143-
assert!(MemoryStore::default().list()?.is_empty());
143+
assert_eq!(MemoryStore::default().list()?.len(), 0);
144144
Ok(())
145145
}
146146

@@ -304,7 +304,7 @@ fn a_failure_that_is_not_a_collision_is_not_retried() -> Result<()> {
304304
// The rest of the stub behaves as the trait requires, so a future change
305305
// that starts calling it does not silently get nonsense.
306306
let id = BoxId::new("box-0")?;
307-
assert!(ReadOnly.list()?.is_empty());
307+
assert_eq!(ReadOnly.list()?.len(), 0);
308308
assert!(ReadOnly.get(&id).is_err());
309309
assert!(ReadOnly.set_state(&id, BoxState::Stopped).is_ok());
310310
assert!(ReadOnly.remove(&id).is_ok());

‎crates/tinybox-core/src/template/test.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ fn a_template_name_follows_the_usual_identifier_rule() {
102102

103103
#[test]
104104
fn an_empty_index_lists_nothing() -> Result<()> {
105-
assert!(MemoryTemplates::default().list()?.is_empty());
105+
assert_eq!(MemoryTemplates::default().list()?.len(), 0);
106106
Ok(())
107107
}
108108

‎crates/tinybox-docker/src/oneshot/test.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ async fn run_times_out_with_the_pinned_message() {
200200
.unwrap();
201201
assert!(got.timed_out);
202202
assert_eq!(got.exit_code, -1);
203-
assert!(got.stdout.is_empty());
203+
assert_eq!(got.stdout.len(), 0);
204204
assert_eq!(got.stderr, "Command timed out after 0s and was killed");
205205
}
206206

0 commit comments

Comments
 (0)