Rust Tests, Documentation and Cargo¶
Rust tests should check a stated contract, not merely repeat what the implementation happens to do. This lesson turns the fixture parser into a documented library with meaningful errors, then tests its internals, public API and documentation examples separately.
Prerequisites and outcome¶
Complete iterators and lazy adapters. You will build a library and binary in one package, write three kinds of tests, and distinguish a compile check from running behavioural assertions. This is the Part II checkpoint; it uses fixture strings and no external crates.
Define the contract before writing Rust tests¶
The parser trims surrounding whitespace, treats empty input as missing, preserves zero, and accepts signed integer millidegrees in the inclusive fixture range -40000 through 125000. That range is an application rule for this exercise, not a claim about safe operating temperatures or a particular Pi sensor specification.
Invalid integer text retains its input and underlying ParseIntError. A valid integer outside the fixture range produces a separate OutOfRange error. The report accounts for every input and runs its predicate only for accepted measured values. Unit tests establish boundaries; integration and doc tests establish the public calling contract.
Create the complete five-file project¶
Replace or create the following files. Keep the package name pi_report so the external examples import the correct crate.
Cargo.toml¶
src/parsing.rs — domain errors and private implementation¶
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 | |
src/lib.rs — documented public API and generic reporting¶
src/main.rs — a consumer of the public crate¶
tests/public_api.rs — an external caller's assertions¶
Run each verification layer¶
The default test command runs 9 library unit tests, 2 integration tests and 3 documentation tests, including the compile-fail example. The binary has no tests of its own. Separate target commands help locate a failure; they are not additional unique tests. The release command checks the same contracts under that build profile.
Expected binary output:
Unit, integration and documentation tests answer different questions¶
Tests under cfg(test) in library source can inspect the surrounding crate's private implementation. They exercise the parser's boundary rules and the report's accounting. Integration tests under tests/ are separate crates and import the public re-exports, like a downstream consumer. See the Book's test organisation.
The triple-slash comments document items; //! documents the enclosing crate or module. Rustdoc compiles ordinary Rust examples and runs them as tests. compile_fail expects rejection, but does not by itself prove the intended diagnostic: a typo could also fail compilation. We therefore also verify the private-module access directly. no_run compiles without running, while ignore skips a test; neither is a substitute for a passing behavioural example. See documentation tests.
Assertions state specific expectations. assert_eq reports actual and expected values, and requires suitable comparison and debug implementations. expect in a test fixture makes an unexpected error fail the test; this is not a recommendation to panic on every production input. A should_panic test should target an actual panic contract, not hide an ordinary Result error. The Book explains test assertions.
Cargo checks are tools, not language guarantees¶
cargo check type-checks selected targets but does not execute their assertions or fully replace a native build. --all-targets includes test targets for checking, but does not run rustdoc examples. cargo test builds and runs the selected tests; --no-run builds tests without executing them. These distinctions are documented in cargo check and cargo test.
Tests can run in parallel. Keep fixtures independent of shared files, process-wide environment and external hardware. Test filters can leave other tests unexecuted; -- --test-threads=1 controls the test harness, not Cargo compilation. Use -- --nocapture when output is useful for debugging, and never treat printed logs alone as assertions.
Cargo.toml expresses package requirements, including the edition and dependency declarations; Cargo.lock records resolved dependencies. Preserve the lockfile for this executable project and use --locked to detect unexpected resolution changes once it exists. --offline avoids network access but cannot provide dependencies absent from the local cache. Our project has no external dependencies. See manifest and lockfile roles.
Formatting and documentation generation are separate tools. Clippy can suggest additional checks if installed, but a clean lint report is not a proof of all program properties. Do not install extra components merely to run this example. A finite test suite cannot establish exhaustive correctness, memory-model claims or performance.
The public error and borrowing policy¶
ParseError implements Debug, Display and Error. InvalidInteger retains the low-level source separately from its contextual display text; callers can inspect the source rather than parse a human-readable message. OutOfRange has no underlying error. The Error trait describes that separation.
analyze accepts any suitable IntoIterator whose Item can cheaply expose text through AsRef
Deliberately failing: an assertion with the wrong contract¶
In a separate copy of the project, add tests/broken_contract.rs:
cargo check --all-targets succeeds, but cargo test --test broken_contract fails: zero produces Some(0), not None. Repair the assertion to expect Some(0); do not change the parser to agree with an incorrect test. Keep the deliberately failing test out of the normal project.
Part II checkpoint: defend and extend the public contract¶
- Explain why the parser's implementation module remains private while its API is reachable from the binary, external test and doc example. Replace main's import with
pi_report::parsing::parse_reading: expect E0603. - Run the three test categories individually and identify their boundaries. Explain why cargo check alone cannot detect the deliberately wrong zero assertion.
- Raise the predicate threshold from 60000 to 60001 in main: measured/missing/invalid counts stay 3/1/2 while matching becomes 0. Use a stateful FnMut predicate to record valid inputs; existing tests prove missing and invalid inputs never call it.
- Change the parser's range check to reject 125000 instead of 125001: the upper-boundary unit test must fail. Change missing input into Some(0): both the missing-input tests and the documented example must fail.
- Pass a Vec
rather than an array of borrowed strings to analyze: the same fixture report should be produced. Explain why the result needs no lifetime tied to that container. - Repair broken_contract's assertion and rerun its target. Introduce a doc-example typo only in a scratch copy and verify that cargo test --doc fails rather than assuming generated HTML validates examples.
Do not update correct tests just to make an accidental policy change green. Decide whether a changed contract is intended, update its documentation and boundaries consistently, then verify the public caller experience again.
Verification and next step¶
On October 10, 2026, all five files were verified together on a Raspberry Pi 4B with 64-bit user space, kernel 6.18.50+rpt-rpi-v8, Rust and Cargo 1.99.0, and edition 2024. All-target checking, 9 unit tests, 2 integration tests, 3 doc tests, separate target selection, release tests, formatting, documentation generation and debug/release output comparisons passed. Locked testing, build-only testing and name-filtered harness options were also checked. The assertion repair, changed threshold and owned-input report passed; private-module access produced E0603. Wrong zero assertions, range/missing policy changes and a doc-example typo failed their intended test targets. No live sensor or performance result is claimed.
Next: Box, Deref and Drop, separating owned storage, borrowed access and cleanup.