The compressed essence of Rust Book ch. 1–13 · built for lookup while typing, not for reading
cargo new my_proj # create project (binary)
cargo new my_lib --lib # create library
cargo run # build + run
cargo run -- 95 83 # pass args to your program (after --)
cargo build # build only (debug)
cargo build --release # optimized build
cargo check # typecheck fast, no binary
cargo test # run #[test] functions
cargo add rand # add a dependency
Book: 1.3 Hello, Cargo!
let x = 5; // immutable
let mut y = 5; // mutable
y = 6; // ok, y is mut
const MAX: u32 = 100; // const: always typed, UPPER_CASE
let x = 5;
let x = x + 1; // shadowing: new variable, same name
Book: 3.1
i8 i16 i32 i64 i128 isize // signed ints (i32 = default)
u8 u16 u32 u64 u128 usize // unsigned ints (usize = index/len type)
f32 f64 // floats (f64 = default)
bool // true / false
char // 'a' — 4 bytes, Unicode scalar
let t: (i32, f64) = (500, 6.4); // tuple
let (a, b) = t; // destructure
let first = t.0; // index
let arr: [u16; 10] = [0; 10]; // array: fixed length, 10 zeros
let arr = [1, 2, 3]; // inferred [i32; 3]
let s = "hi"; // &str — borrowed, fixed
let s = String::from("hi"); // String — owned, growable
let n: u32 = "42".parse().unwrap(); // str -> number
let n = "42".parse::<u32>().unwrap(); // same, turbofish form
let s = 42.to_string(); // number -> String
let len = arr.len() as u32; // numeric cast
Book: 3.2
fn add(a: i32, b: i32) -> i32 {
a + b // no semicolon = this is the return value
}
fn shout(msg: &str) -> String {
msg.to_uppercase()
}
fn log(msg: &str) { // no -> means returns ()
println!("{msg}");
}
Statement vs expression: a line ending in ; is a statement (produces no value). Drop the ; and it is an expression whose value is returned. return x; works too, but is only idiomatic for early returns.
Book: 3.3
if n < 5 { ... } else if n < 10 { ... } else { ... }
let label = if n > 0 { "pos" } else { "neg" }; // arms must be same type
loop { break; } // infinite until break
let got = loop { break 7; }; // loop can return a value
while n < 10 { n += 1; }
for i in 0..5 { } // 0,1,2,3,4
for i in 0..=5 { } // 0..5 inclusive
for item in &vec { } // borrow each item
for (i, c) in word.char_indices() { }
Book: 3.5
let s1 = String::from("hi");
let s2 = s1; // MOVE — s1 is now invalid
// println!("{s1}"); // compile error: borrow of moved value
let s2 = s1.clone(); // deep copy — both usable, costs allocation
let n1 = 5;
let n2 = n1; // COPY — i32 is Copy, n1 still valid
Copy types (stack-only, fixed size): all integers, f32/f64, bool, char, and tuples containing only Copy types. Everything heap-owning (String, Vec) moves instead.
Book: 4.1
fn length(s: &String) -> usize { s.len() } // borrow, read-only
fn push_bang(s: &mut String) { s.push('!'); } // borrow, mutable
let mut s = String::from("hi");
length(&s); // pass an immutable reference
push_bang(&mut s); // pass a mutable reference
The borrowing rule: at any one time you may have either one mutable reference or any number of immutable references — never both. Enforced at compile time; this is what rules out data races without a GC.
Book: 4.2
let s = String::from("hello world");
let hello = &s[0..5]; // &str — pointer + length, owns nothing
let world = &s[6..11];
let whole = &s[..];
let arr = [1, 2, 3, 4, 5];
let part: &[i32] = &arr[1..3]; // [2, 3]
fn total(nums: &[u32]) -> u32 { ... } // takes Vec or array — prefer &[T]
Book: 4.3
struct Rectangle { width: u32, height: u32 } // named fields
struct Point(i32, i32); // tuple struct
struct Marker; // unit struct
#[derive(Debug)] // enables {:?} printing
struct User { name: String, active: bool }
let r = Rectangle { width: 30, height: 50 };
println!("{r:?}"); // needs #[derive(Debug)]
impl Rectangle {
fn new(width: u32, height: u32) -> Self { // associated fn (no self)
Self { width, height } // field init shorthand
}
fn area(&self) -> u32 { // method: borrows
self.width * self.height
}
fn scale(&mut self, by: u32) { // method: borrows mutably
self.width *= by;
}
fn consume(self) -> u32 { self.width } // method: takes ownership
}
Rectangle::new(3, 4).area(); // :: for associated fn, . for method
Book: 5.1–5.3
enum Shape {
Circle(f64), // variant with data
Rect { w: f64, h: f64 }, // variant with named fields
Empty, // variant with no data
}
enum Option<T> { Some(T), None } // in std — "maybe a value"
enum Result<T, E> { Ok(T), Err(E) } // in std — "value or error"
match shape {
Shape::Circle(r) => 3.14 * r * r,
Shape::Rect { w, h } => w * h,
Shape::Empty => 0.0,
} // must be exhaustive
match score {
90..=100 => "A", // range pattern
n if n > 50 => "pass", // match guard
_ => "F", // catch-all
}
if let Some(x) = maybe { ... } // one pattern, ignore rest
if let Some(x) = maybe { ... } else { ... }
while let Some(top) = stack.pop() { ... } // loop while pattern matches
Book: 6.1–6.3
// src/lib.rs or src/main.rs — the crate root
mod front_of_house; // loads src/front_of_house.rs (or .../mod.rs)
mod hosting { // inline module
pub fn add_to_waitlist() {} // pub = visible outside this module
fn seat() {} // private (default)
}
crate::front_of_house::hosting::add_to_waitlist(); // absolute path
front_of_house::hosting::add_to_waitlist(); // relative path
super::hosting::add_to_waitlist(); // parent module
use crate::front_of_house::hosting; // bring into scope
use std::collections::HashMap;
use std::io::{self, Read}; // multiple items
use std::fmt::Result as FmtResult; // rename
pub use crate::hosting; // re-export
Everything is private by default. A child module can see its parent's items; a parent needs pub to see into a child. pub on a struct does not make its fields public — each field needs its own pub.
Book: 7.1–7.5
my_proj/
├── Cargo.toml # ONE package
├── src/
│ ├── lib.rs # crate 1: the LIBRARY, named after the package
│ ├── thing.rs # a module inside crate 1
│ └── main.rs # crate 2: the BINARY — a separate crate
└── tests/
└── spec.rs # crate 3: integration tests — separate again
Cargo finds all three by filename. No [lib] or [[bin]] in Cargo.toml is needed.
// src/thing.rs — INSIDE the library crate
use crate::other::Thing; // crate:: = root of the crate I am in
// src/main.rs — a DIFFERENT crate
use my_proj::thing::Thing; // must use the package name
// tests/spec.rs — also a different crate
use my_proj::thing::Thing; // same as main.rs
use crate::.. in main.rs gives error[E0432]: unresolved import. When a path will not resolve, ask: which crate am I in right now?
Three wiring states, in the order you hit them:
In lib.rs | Result |
|---|---|
| nothing | E0433: cannot find `thing` — a file is not a module until declared |
mod thing; | E0603: module `thing` is private |
pub mod thing; | works |
Integration tests in tests/ can only reach pub items via the library crate — they cannot see inside main.rs at all. That is the reason to put logic in lib.rs and keep main.rs thin.
Book: 7.1 Packages and Crates · 11.3 Test Organization
#[derive(Debug, Clone, Copy, PartialEq)]
enum Size { Small, Large }
| Derive | Gives | Needed for |
|---|---|---|
Debug | {:?} | test failure output, logs |
PartialEq | == | assert_eq! on your type |
Clone | .clone() | explicit second copy |
Copy | assignment copies, not moves | small types, no heap data |
PartialOrd, Ord | <, .sort() | ordering; enum variant order = the ordering |
A type containing a String or Vec cannot be Copy. Forget a derive and the compiler names the exact trait and type.
Book: Appendix C — Derivable Traits
// Vec — owned, growable list
let mut v: Vec<u32> = Vec::new();
let v = vec![1, 2, 3];
v.push(4);
let third = &v[2]; // panics if out of range
let third = v.get(2); // returns Option<&u32> — safe
v.get(9).unwrap_or(&0); // default if missing
for n in &v { } // iterate borrowed
for n in &mut v { *n += 1; } // iterate mutably
v.len(); v.is_empty(); v.pop(); v.contains(&3);
// String — owned, growable, UTF-8
let mut s = String::new();
s.push_str("hi"); s.push('!');
let s = format!("{a}-{b}");
let joined = words.join(" ");
for w in text.split_whitespace() { }
for (i, c) in text.char_indices() { }
text.to_uppercase(); text.trim(); text.len(); // len = BYTES, not chars
// HashMap — key/value
use std::collections::HashMap;
let mut m = HashMap::new();
m.insert("a", 10);
m.get("a"); // Option<&i32>
let count = m.entry(key).or_insert(0); // insert-if-absent, returns &mut
*count += 1; // the counting idiom
for (k, v) in &m { }
Book: 8.1–8.3
// ONE trait, one method — everything else is a default method built on next()
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}
// THREE ways in — the &self / &mut self / self choice, one element at a time
v.iter() // Item = &T reading; collection survives
v.iter_mut() // Item = &mut T editing in place; collection survives
v.into_iter() // Item = T taking the elements; collection consumed
// ADAPTERS — lazy, return an iterator, compose freely
.map(|x| ..) .filter(|x| ..) .enumerate() .skip(n) .take(n) .rev() .zip(other)
// CONSUMERS — do the work, return something that is not an iterator
.collect() .find(|x| ..) .position(|x| ..) .any(|x| ..) .all(|x| ..)
.count() .sum() .max() .min() .fold(init, |acc, x| ..) .for_each(|x| ..)
// find gives the ITEM, position gives the INDEX — both Option, both take ok_or
list.iter().find(|t| t.id == id).ok_or(NotFound(id))? // -> &T
list.iter().position(|t| t.id == id).ok_or(NotFound(id))? // -> usize
list.retain(|t| t.keep); // Vec method, not Iterator: delete in place, one pass
// COLLECT builds any FromIterator type — you must say which
let v: Vec<String> = it.collect();
let s: String = it.collect(); // String: FromIterator<String>
let m: HashMap<K, V> = pairs.collect(); // from an iterator of (K, V)
let r: Result<Vec<T>, E> = it.collect(); // SHORT-CIRCUITS on the first Err
let o: Option<Vec<T>> = it.collect(); // .. and on the first None
text.lines() // trailing "\n" is a TERMINATOR — no empty last item, strips \r
text.split('\n') // trailing "\n" is a SEPARATOR — yields a final ""
Errors you will see:
warning: unused `Map` that must be used
= note: iterators are lazy and do nothing unless consumed // no consumer
error[E0283]: type annotations needed
| let x = it.collect(); ------- type must be known at this point
= note: multiple `impl`s satisfying `_: FromIterator<String>` found
error[E0502]: cannot borrow `self.v` as immutable because it is also borrowed as mutable
// an iter_mut() result in a variable holds the borrow until its LAST use
error[E0507]: cannot move out of `x.field` which is behind a shared reference
// a closure over &T cannot give away an owned field — Copy, clone, or &str
Cost: an adapter chain is a tower of small structs compiled to one pass — no temporary
vectors, no runtime penalty against the equivalent for loop.
When a loop still wins: folding many items into one accumulator you mutate. Iterators replace loops that search, transform, or collect.
Book: 13.2 Iterators · std: Iterator · FromIterator
use std::collections::HashMap;
// A key must be Eq + Hash: hash to pick the bucket, compare inside it.
#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)] // Copy for fieldless enums
enum Priority { Low, Medium, High }
let mut m: HashMap<Priority, usize> = HashMap::new();
*m.entry(key).or_insert(0) += 1; // THE counting idiom: one lookup, &mut V back
m.entry(key).or_default(); // same, using Default::default()
m.get(&key).copied().unwrap_or(0); // Option<&usize> -> usize, absent = 0
for (k, v) in &m { } // ARBITRARY order — impose your own before printing
HashMap::from([(Priority::High, 2)]); // literal, for tests
Eq is a marker: no methods, one extra promise over PartialEq — every value equals
itself. f64 breaks it (NAN != NAN), so f64 cannot be a key.
Want sorted keys instead of arbitrary order? BTreeMap, same API, K: Ord.
Book: 8.3 Hash maps · std: Eq · Hash
panic!("boom"); // unrecoverable — stops the program
// Recoverable: return Result and let the caller decide
fn read_name() -> Result<String, io::Error> {
let mut f = File::open("hello.txt")?; // ? = return Err early
let mut s = String::new();
f.read_to_string(&mut s)?;
Ok(s) // must wrap success in Ok
}
match File::open("x.txt") {
Ok(f) => f,
Err(e) => match e.kind() {
ErrorKind::NotFound => File::create("x.txt").unwrap(),
_ => panic!("{e:?}"),
},
};
r.unwrap(); // value, or panic
r.expect("message"); // value, or panic with your message
r.unwrap_or(0); // value, or fallback
r.unwrap_or_else(|e| 0); // value, or compute fallback
r.is_ok(); r.is_err();
fn main() -> Result<(), Box<dyn Error>> { ... } // main can return Result
Rule of thumb: panic! / unwrap in tests, prototypes, and truly impossible states. Result everywhere else — especially in library code and anything a backend service depends on.
Book: 9.1–9.3
enum Result<T, E> { Ok(T), Err(E) } // an enum: errors are VALUES
enum Option<T> { Some(T), None } // absence, with no reason attached
Pick the shortest rung that fits:
match r { Ok(v) => ..., Err(e) => ... } // full control, both arms required
if let Err(e) = r { ... } // only care about failure
r.unwrap_or(default) // fallback value
r.unwrap_or_else(|e| compute()) // fallback, computed
r.ok() // Result -> Option (drops the reason)
r.map(|v| v + 1) // change Ok, leave Err untouched
r.is_ok() / r.is_err() // just ask
r.unwrap() / r.expect("why") // value, or PANIC
r? // hand the error to my caller
What ? really does:
let v = thing()?;
// expands to roughly:
let v = match thing() {
Ok(value) => value,
Err(e) => return Err(From::from(e)), // early exit + TYPE CONVERSION
};
The From::from step is why ? can mix error types in one function:
fn load_port(path: &str) -> Result<u16, Box<dyn Error>> {
let text = fs::read_to_string(path)?; // io::Error -\
let port = text.trim().parse::<u16>()?; // ParseIntError -> Box<dyn Error>
Ok(port)
}
? requires the enclosing function to return Result or Option:
error[E0277]: the `?` operator can only be used in a function that returns `Result`
| cannot use the `?` operator in a function that returns `()`
help: consider adding return type
Fix the signature, not the ?. Even main can return Result — end it with Ok(()).
Dropping a Result is a warning, not silence:
warning: unused `Result` that must be used
= note: this `Result` may be an `Err` variant, which should be handled
help: use `let _ = ...` to ignore the resulting value
panic vs propagate: unwrap/expect in tests, prototypes, and states that truly cannot happen. Result for anything touching files, args, network, or users. In a service: propagate with ? through the inner layers, and decide what the user sees at one boundary (the request handler).
#[derive(Debug)] // Error requires Debug
enum SensorError {
Empty, // no data
NotANumber(ParseFloatError), // wrap the cause
OutOfRange(f64), // keep the bad value
}
impl fmt::Display for SensorError { // the human sentence
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
SensorError::Empty => write!(f, "no reading given"),
SensorError::NotANumber(_) => write!(f, "reading is not a number"),
SensorError::OutOfRange(v) => write!(f, "{v} is outside -90..60"),
}
}
}
impl Error for SensorError {} // std::error::Error — marker, no body needed
// …or, when a variant WRAPS another error, hand the cause to source() instead:
impl Error for SensorError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
SensorError::NotANumber(e) => Some(e), // the wrapped cause
_ => None, // nothing underneath
}
}
}
impl From<ParseFloatError> for SensorError { // makes bare `?` convert for you
fn from(e: ParseFloatError) -> SensorError { SensorError::NotANumber(e) }
}
Three obligations, in order: #[derive(Debug)], then Display, then the empty impl Error. Skip Display and the marker impl fails:
error[E0277]: `SensorError` doesn't implement `std::fmt::Display`
13 | impl Error for SensorError {}
| ^^^^^^^^^^^ unsatisfied trait bound
From is a trait with one method — fn from(value: T) -> Self, i.e. "how to build me out of a T". String::from("hi") is the same trait. Three equivalent spellings once the impl exists:
Err(SensorError::from(e)) // call it yourself
Err(e.into()) // same trait, from the value's side (Into comes free)
text.parse::<f64>()? // `?` calls From::from(e) for you
// what `?` expands to:
match thing() { Ok(v) => v, Err(e) => return Err(From::from(e)) }
Missing From impl, seen through ?:
error[E0277]: `?` couldn't convert the error to `SensorError`
28 | Ok(text.parse::<f64>()?)
| --------------^ the trait `From<ParseFloatError>` is not implemented for `SensorError`
// with an annotated `let`, the same missing impl is reported as:
error[E0271]: type mismatch resolving `<f64 as FromStr>::Err == SensorError`
29 | let value: f64 = text.parse()?;
| ^^^^^ expected `SensorError`, found `ParseFloatError`
ok_or("text")? in a function returning Result<_, String> already relies on this: std ships impl From<&str> for String.
Reporting it in a CLI — main -> Result prints with {:?} (Debug), so handle it yourself when a human reads the output:
if let Err(e) = run(&args, &mut store) {
eprintln!("error: {e}"); // stderr, Display
process::exit(1); // status a script can test
}
Wrapped cause: source() or Display, never both. std's own rule — "the underlying error should be either returned by the outer error's Error::source(), or rendered by the outer error's Display implementation, but not both." So write your one sentence in Display, drop the {e}, and expose the cause through source() for logs and --verbose.
let e = parse_reading("nope").unwrap_err();
e.to_string() // "reading is not a number" <- yours
e.source().map(|s| s.to_string()) // Some("invalid float literal") <- std's
Reviewer's checklist for an error type (API Guidelines C-GOOD-ERR): implements Error; is Send + Sync (needed to cross threads, so needed by every web framework); never (); Display message lowercase, no trailing punctuation, concise. Assert the bounds in a test with an empty generic fn:
fn assert_usable_as_error<E: Error + Send + Sync + 'static>() {}
assert_usable_as_error::<TaskError>(); // compile-time check, no body needed
Matching without naming every variant — matches! returns a bool:
assert!(matches!(err, TaskError::BadId(_))); // true if the shape matches
if matches!(status, Status::Todo | Status::InProgress) { .. }
Adding a variant is a compiler-enforced review:
error[E0004]: non-exhaustive patterns: `&TaskError::StoreFull` not covered
19 | match self {
| ^^^^ pattern `&TaskError::StoreFull` not covered
note: `TaskError` defined here … 14 | StoreFull, --------- not covered
That failure is the reason to prefer an enum over a String — and the reason not to write _ => .. when matching your own error type at the reporting edge.
Two more errors from the same migration:
error[E0432]: unresolved import `crate::error` // `pub mod error;` missing
error[E0369]: binary operation `==` cannot be applied to type `TaskError`
note: `TaskError` does not implement `PartialEq` // tests use assert_eq!
io::Error & FromStruse std::fs;
use std::io::{self, ErrorKind};
use std::path::{Path, PathBuf};
fs::read_to_string(path)? // io::Result<String> = Result<String, io::Error>
fs::write(path, text)? // io::Result<()> — creates or truncates, one call
text.lines() // iterator of &str, no trailing newlines
// a path from the environment, with a default
let path = PathBuf::from(
env::var("TASKS_FILE").unwrap_or_else(|_| "tasks.txt".to_string()));
One io failure is not another — e.kind() returns an ErrorKind. A missing file on first run is expected; everything else is not. Use a match guard:
let text = match fs::read_to_string(path) {
Ok(text) => text,
Err(e) if e.kind() == ErrorKind::NotFound => return Ok(Store::new()),
Err(e) => return Err(TaskError::Io(e)), // permissions, disk, …
};
FromStr is the trait behind .parse(). Implement it and .parse::<YourType>() starts working — type Err is an associated type: a slot the implementor fills once, not a parameter the caller passes.
pub trait FromStr: Sized {
type Err;
fn from_str(s: &str) -> Result<Self, Self::Err>;
}
impl FromStr for Task {
type Err = TaskError;
fn from_str(line: &str) -> Result<Task, TaskError> {
let bad = || TaskError::BadLine(line.to_string()); // built on failure
let mut parts = line.splitn(4, '|'); // splitn: last piece keeps its '|'
let id: u32 = parts.next().ok_or_else(bad)?.parse().map_err(|_| bad())?;
..
}
}
error[E0046]: not all trait items implemented, missing: `Err` // no `type Err`
error[E0277]: the trait bound `Task: FromStr` is not satisfied // no impl
? vs map_err: only one impl From<T> for YourError may exist per T (a second is error[E0119]: conflicting implementations). So ? handles the canonical meaning, and map_err names the variant for every other meaning of the same error type.
let id: u32 = text.parse()?; // -> BadId, via From
let id: u32 = text.parse().map_err(|_| bad())?; // -> BadLine, chosen here
Not everything derives. io::Error is not PartialEq, so a variant holding one breaks #[derive(PartialEq)] (E0369). Write it by hand; mem::discriminant covers the payload-free variants:
impl PartialEq for TaskError {
fn eq(&self, other: &Self) -> bool {
match (self, other) { // match on a TUPLE of both
(Io(a), Io(b)) => a.kind() == b.kind(),
(NotFound(a), NotFound(b)) => a == b,
_ => std::mem::discriminant(self) == std::mem::discriminant(other),
}
}
}
Book: 12.2, 12.5 · std: fs · ErrorKind · FromStr
trait Reading {
fn celsius(&self) -> f64; // REQUIRED — semicolon
fn label(&self) -> String { // DEFAULT — has a body, may be overridden
format!("{:.1}C", self.celsius())
}
}
impl Reading for Kettle { // impl TRAIT for TYPE
fn celsius(&self) -> f64 { (self.fahrenheit - 32.0) * 5.0 / 9.0 }
}
impl fmt::Display for Thermometer { // the trait behind `{}`
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{} [{:.1}C]", self.room, self.celsius) // INTO f, no `;`
}
}
Bounds — four spellings, one meaning ("any T that implements Reading"):
fn show<T: Reading>(r: &T) // angle brackets
fn show(r: &impl Reading) // shorthand
fn show<T>(r: &T) where T: Reading // where clause, for long lists
fn show(r: &dyn Reading) // trait OBJECT: type chosen at runtime
fn show<T: Reading + Clone>(r: &T) // two promises at once
Box<dyn Error> is the same dyn idea: "some heap value that implements Error". Use it when the failures are unrelated and an enum is not worth writing.
A generic function of your own — one definition, one specialised copy compiled per set of types actually used (monomorphisation), so the generality is free at runtime:
pub fn tally<T, K, F>(items: &[T], key: F) -> HashMap<K, usize>
where
K: Eq + Hash, // required by the HashMap being returned, not by the body
F: Fn(&T) -> K, // each closure has its OWN anonymous type
{
let mut counts = HashMap::new();
for item in items { *counts.entry(key(item)).or_insert(0) += 1; }
counts
}
tally(&tasks, |t| t.priority) // T = Task, K = Priority
tally(&["a", "bb"], |w| w.len()) // T = &str, K = usize
The clause is a contract both ways: callers must satisfy it, and the body may use only what it
promises. Fn = borrows its captures · FnMut = mutates them · FnOnce =
consumes them. Take Fn unless you need more.
Errors you will see:
error[E0046]: not all trait items implemented, missing: `celsius`
| ^^^^^^^^^^^^^^^^^^^^^^^ missing `celsius` in implementation
error[E0277]: `Kettle` doesn't implement `std::fmt::Display`
= note: in format strings you may be able to use `{:?}` instead
error[E0599]: no method named `celsius` found for reference `&&T` in the current scope
= help: items from traits can only be used if the trait is implemented and in scope
note: `Reading` defines an item `celsius`, perhaps you need to implement it
That last one is the missing-bound error: a bare <T> promises nothing, so no method exists on it. Add the bound.
Free consequences of one impl: Display grants ToString (so .to_string() works); From<A> for B grants A.into(): B and makes ? convert.
Orphan rule: impl Trait for Type is allowed only if the trait or the type is yours. Display for MyTask ✓ · MyTrait for Vec<T> ✓ · Display for Vec<String> ✗.
Trait in scope: to call a trait method you must have the trait imported (use std::io::Write; before .write_all()). println!("{}") is exempt — the macro names Display by full path.
Book: 10.2 Traits · 10.1 Generics · std: Display
println!("plain text");
println!("{}", value); // Display
println!("{value}"); // inline variable (preferred)
println!("{value:?}"); // Debug — needs #[derive(Debug)]
println!("{value:#?}"); // Debug, pretty-printed
eprintln!("to stderr");
Two independent traits. A type can have one, both, or neither.
{} {value} needs Display — text for END USERS
{:?} {value:?} needs Debug — text for PROGRAMMERS
{:#?} {value:#?} needs Debug — same, multi-line
let s = String::from("hi\tthere");
println!("{s}"); // hi there <- raw, for a user
println!("{s:?}"); // "hi\tthere" <- quoted + escaped, exact
#[derive(Debug)] // compiler writes Debug for you
struct Config { name: String }
impl fmt::Display for Point { // Display is NEVER derivable — write by hand
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "({}, {})", self.x, self.y)
}
}
Why no derive(Display): how a value should look to a human is your program's decision, not something the compiler can guess. Debug has one obvious form (type name + fields), so it can be generated.
Why Vec has Debug but no Display: there is no single correct way to show a list to a user (commas? bullets? brackets?), so std refuses to pick. You choose:
println!("{v:?}"); // ["a", "b"] — diagnostics
println!("{}", v.join(", ")); // a, b — you pick the format
Errors you will see:
error[E0277]: `Vec<String>` doesn't implement `std::fmt::Display`
= note: in format strings you may be able to use `{:?}` instead
error[E0277]: `Point` doesn't implement `Debug`
= note: add `#[derive(Debug)]` to `Point` or manually `impl Debug for Point`
Rule of thumb: use {:?} while developing and in logs. Write a Display impl only when a real person reads the output. Custom error types want both — Display for the caller's message, Debug for the log.
Book: 5.2 (derive Debug) · 10.2 Traits · std::fmt docs
#[cfg(test)] // compiled by `cargo test`, not by `cargo build`
mod tests {
use super::*; // the child module needs its parent's items
#[test] // no arguments, no return value
fn a_task_starts_as_todo() {
assert_eq!(Task::new(1, "x", Low).status, Status::Todo);
}
}
A test fails by panicking. Each test runs on its own thread; the harness marks it failed when that thread dies. So every assertion macro is a wrapper that panics, and unwrap/expect in a test body is a legitimate assertion.
assert!(cond) // prints the SOURCE TEXT of cond
assert!(cond, "id {} was wrong", id) // extra args go to format!
assert_eq!(a, b) // prints both values as `left` / `right`
assert_ne!(a, b) // same, passes when they differ
assert_eq! needs PartialEq (to compare) and Debug (to print) on the values — that is what the #[derive(Debug, PartialEq)] on your own types is for. Prefer it over assert!(a == b), which throws the values away. An assertion inside a loop needs a message naming the case.
Two ways to test a failure — one per way your code fails:
#[test]
#[should_panic(expected = "at most 30")] // substring of the panic message
fn refuses_a_high_target() { Thermostat::new(99); }
#[test]
fn a_reload_sees_what_was_saved() -> Result<(), TaskError> {
store.save(&path)?; // `?` for SETUP that must work
assert!(Store::load(&bad).is_err()); // is_err for the tested failure
Ok(())
}
Always give should_panic its expected, or any panic passes the test. #[should_panic] and a Result return cannot be combined; assert with is_err() or unwrap_err() instead. A Result test's error type only needs Debug.
Two homes, two levels of access:
| Unit test | Integration test | |
|---|---|---|
| Lives in | bottom of the source file | tests/<name>.rs |
| Compiled as | part of your crate | its own separate crate |
| Can reach | private items too | the public API only |
| Needs | #[cfg(test)] + use super::* | use mycrate::… |
| Run alone | cargo test --lib | cargo test --test name |
Nothing in src/main.rs is testable — a binary crate exposes nothing to use. Keep main thin, put the logic in the library, and take the output destination as a parameter so a test can read it:
pub fn run(args: &[String], store: &mut Store, out: &mut impl Write)
-> Result<(), TaskError>
{
writeln!(out, "added task {}", id)?; // never println! in library code
}
run(&args, &mut store, &mut io::stdout().lock())?; // in main
let mut out: Vec<u8> = Vec::new(); // in a test
run(&args, &mut store, &mut out)?;
assert_eq!(String::from_utf8(out)?, "added task 1\n");
Helpers shared by two integration files go in tests/common/mod.rs, never tests/common.rs — a plain file there is compiled as its own test crate and shows up as a stray running 0 tests section.
tests/
├── common/mod.rs mod common; then common::three_tasks()
├── cli.rs #![allow(dead_code)] in mod.rs: each crate uses some
└── mine.rs
Tests run in parallel and share nothing safely — no shared file, no current directory, no environment variable. Give every file-touching test its own path (process id + an AtomicU32 counter, inside env::temp_dir()).
cargo test # everything
cargo test warmer # every test whose FULL name contains it
cargo test --lib # unit tests only (--test cli = one file)
cargo test -- --show-output # also print stdout of tests that PASSED
cargo test -- --ignored # only the #[ignore] ones
cargo test -- --test-threads=1 # no parallelism — diagnose, do not fix
Flags before -- go to cargo, flags after it go to the test binary. The module path is part of a test's name (task::tests::…), so filtering on a module name runs that module.
running 4 tests
test tests::refuses_a_high_target - should panic ... ok
test tests::slow_one ... ignored, only when the range changes
test result: ok. 3 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out
// measured = nightly benchmarks · filtered out = excluded by your filter
Errors you will see:
error[E0433]: cannot find type `Thermostat` in this scope // no use super::*
error[E0603]: function `capped` is private // private fn, from tests/
error[E0616]: field `target` of struct `Thermostat` is private
help: a method `target` also exists, call it with parentheses
Judge a suite by the bugs it kills, not by the number of tests. Change one operator in the code by hand, run the suite, and put it back: if nothing went red, that behaviour is untested.
Book: 11.1 Writing tests · 11.2 Running them · 11.3 Organization