19–21. The Module System, Cargo, crates.io · Unit Tests, Integration Tests, doctests · Threads, Channels, Arc/Mutex
Once a Rust project grows you split code into modules and pull in external crates. This lesson covers the `mod` keyword and the file-split convention, `pub` visibility, `use` paths, declaring dependencies in Cargo.toml, and how to find and add packages from crates.io. Rust ships its test runner inside cargo. Add `#[test]` to a function and `cargo test` finds and runs it. This lesson covers unit tests (in a `mod tests` block), integration tests (under `tests/`), and doctests that automatically run code blocks in /// comments. Rust calls its model "fearless concurrency" because the borrow checker prevents data races at compile time even across threads. This lesson covers std::thread, mpsc channels for thread-to-thread messages, and the Arc<Mutex<T>> pattern when you actually need shared state.
What you'll learn
- 1Split code into modules with `mod`
- 2Control visibility with `pub`
- 3Shorten paths with `use`
- 4Add external crates in Cargo.toml's [dependencies]
- 5Use cargo add / cargo search / cargo doc for package workflow
- 6Write `#[test]` functions and run them with `cargo test`
- 7Follow the `mod tests` convention for unit tests
- 8Write integration tests under `tests/`
- 9Use `#[should_panic]` to verify a function panics
- 10Run code in /// comments as doctests
- 11Spawn threads with `std::thread::spawn` and join them
- 12Move values into a thread via a `move` closure
- 13Pass data between threads with mpsc channels
- 14Mutate shared state safely with `Arc<Mutex<T>>`
- 15Understand the role of the Send / Sync traits
19. The Module System, Cargo, crates.io
Once a Rust project grows you split code into modules and pull in external crates. This lesson covers the `mod` keyword and the file-split convention, `pub` visibility, `use` paths, declaring dependencies in Cargo.toml, and how to find and add packages from crates.io.
Overview
main.rs scaling past 100 lines turns into a mess. Rust's one-file-equals-one-module convention makes splitting natural without ceremony.
Core Concepts
1) crate vs. module
- **crate** — one Cargo.toml = one package = one build unit
- **module** — namespace unit inside a crate
- Binary crate root = `src/main.rs`, library crate root = `src/lib.rs`
2) mod and file layout
// src/main.rs
mod math; // looks for math.rs or math/mod.rs
fn main() {
println!("{}", math::add(2, 3));
}
// src/math.rs
pub fn add(a: i32, b: i32) -> i32 { a + b }3) pub — visibility
- Default = **private** (same-module only)
- `pub` — exported
- `pub(crate)` — exported within the crate
- `pub(super)` — exported to parent module
4) use — path shortening
use std::collections::HashMap;
use std::io::{self, Read}; // self = io itself, Read = trait
use crate::math::add as plus; // alias5) Cargo.toml dependencies
[dependencies]
serde = { version = "1", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
thiserror = "1"Hands-on Examples
Sample directory layout:
src/
├── main.rs
├── config.rs
└── api/
├── mod.rs (or src/api.rs)
├── user.rs
└── post.rs// src/main.rs
mod config;
mod api;
fn main() {
let cfg = config::load();
api::user::create(&cfg);
}
// src/api/mod.rs (or src/api.rs + src/api/ folder)
pub mod user;
pub mod post;
// src/api/user.rs
use crate::config::Config;
pub fn create(cfg: &Config) { /* ... */ }Add an external crate and use it:
cargo add serde --features derive
cargo add chrono
cargo search regex # search crates.iouse serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug)]
struct User { name: String, age: u32 }
fn main() {
let u = User { name: "Alice".into(), age: 30 };
let json = serde_json::to_string(&u).unwrap();
println!("{}", json);
}Common Mistakes
Q. mod vs. use — what's the difference?
A. **mod** brings a module into existence, **use** shortens a path to an existing module. Without `mod foo;` you can't `use crate::foo`.
Q. Without `pub`, can I use it from the same file?
A. Yes — within the same module (= same file or same `mod` block) you can access private items. **External modules need pub.**
Q. Should Cargo.lock be committed?
A. **Binary crates yes, library crates no.** Binaries want reproducible builds; libraries have no inherent lockfile because users have their own.
Recap
- crate = build unit, module = namespace unit
- mod declares, file/folder convention auto-maps
- pub / pub(crate) / pub(super) for visibility tiers
- use shortens paths, Cargo.toml manages deps, cargo add for quick install
Try It Yourself
- Split main.rs and math.rs, call math::add from main
- Add src/api/ with user.rs / post.rs and use them from main
- `cargo add chrono` and print the current time
20. Unit Tests, Integration Tests, doctests
Rust ships its test runner inside cargo. Add `#[test]` to a function and `cargo test` finds and runs it. This lesson covers unit tests (in a `mod tests` block), integration tests (under `tests/`), and doctests that automatically run code blocks in /// comments.
Overview
Having tests built into cargo is a big deal — no "which test framework should I use" decisions, and every new project can adopt testing from day one.
Core Concepts
1) Unit tests in the same file
pub fn add(a: i32, b: i32) -> i32 { a + b }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn add_works() {
assert_eq!(add(2, 3), 5);
}
#[test]
fn add_negative() {
assert_eq!(add(-1, 1), 0);
}
}- `#[cfg(test)]` — only compiled in test builds
- `use super::*` — imports private items from the parent module
- Only `#[test]`-marked fns get executed by cargo test
2) Integration tests in tests/
Test the public API only — same paths a downstream user would use.
// tests/api.rs
use my_crate::add;
#[test]
fn integration_add() {
assert_eq!(add(10, 20), 30);
}3) Common assertion macros
| Macro | Use |
|---|---|
| assert!(expr) | expr must be true |
| assert_eq!(a, b) | a == b |
| assert_ne!(a, b) | a != b |
| #[should_panic] | test must panic to pass |
| #[ignore] | skip by default (cargo test -- --ignored) |
4) Doctests — run code inside /// comments
/// Add two numbers.
///
/// # Examples
///
/// ```
/// assert_eq!(my_crate::add(2, 3), 5);
/// ```
pub fn add(a: i32, b: i32) -> i32 { a + b }Hands-on Examples
Result-returning test and should_panic:
pub fn divide(a: i32, b: i32) -> i32 {
if b == 0 { panic!("div by zero"); }
a / b
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ok_case() -> Result<(), String> {
if divide(10, 2) == 5 { Ok(()) } else { Err("expected 5".into()) }
}
#[test]
#[should_panic(expected = "div by zero")]
fn panic_case() { divide(10, 0); }
}Useful invocations:
cargo test # everything
cargo test add_ # name contains add_
cargo test -- --nocapture # show println! output
cargo test --doc # doctests only
cargo test -- --ignored # run ignored onesCommon Mistakes
Q. println! doesn't show up in cargo test
A. By design — passing tests hide output. Use `cargo test -- --nocapture` to see it.
Q. mod tests vs. tests/?
A. **mod tests** = same crate, has access to private items. **tests/** = external view, public API only. Use both as needed.
Q. Tests run too slow
A. cargo test parallelizes by default. Force serial with `cargo test -- --test-threads=1`. Or mark slow tests `#[ignore]` and run on demand.
Recap
- cargo test auto-discovers #[test] functions
- mod tests + #[cfg(test)] is the unit-test convention
- tests/ folder is for integration tests (public API)
- Code blocks in /// comments run as doctests
- Useful: should_panic / ignore / assert_eq
Try It Yourself
- Add 3+ unit tests in a `mod tests` block for an existing function
- Add one integration test under `tests/api.rs`
- Embed a doctest in /// comments and verify `cargo test --doc` passes
21. Threads, Channels, Arc/Mutex
Rust calls its model "fearless concurrency" because the borrow checker prevents data races at compile time even across threads. This lesson covers std::thread, mpsc channels for thread-to-thread messages, and the Arc<Mutex<T>> pattern when you actually need shared state.
Overview
Rust's concurrency promise is **"safe even when you make mistakes."** The same borrowing rule that prevented data races in single-threaded code scales out to threads, so the compiler catches missing locks before runtime. Debugging time goes way down.
Core Concepts
1) Spawning a thread
use std::thread;
use std::time::Duration;
fn main() {
let h = thread::spawn(|| {
for i in 1..=3 {
println!("child {}", i);
thread::sleep(Duration::from_millis(50));
}
});
for i in 1..=3 {
println!("main {}", i);
thread::sleep(Duration::from_millis(50));
}
h.join().unwrap();
}2) move closures to transfer values
If a closure borrows from outside, the compiler can't prove the thread won't outlive the data. `move` transfers ownership in.
let v = vec![1, 2, 3];
let h = thread::spawn(move || println!("{:?}", v));
// v has moved into the thread
h.join().unwrap();3) mpsc channels — message passing
Standard multi-producer, single-consumer channel. Multiple senders, one receiver.
4) Arc<Mutex<T>> for shared mutable state
- **Arc<T>** — atomic reference count, shareable across threads
- **Mutex<T>** — only one thread can write at a time (runtime lock)
- **Arc<Mutex<T>>** combo — multiple threads share + mutate safely
5) Send / Sync traits
| Trait | Meaning |
|---|---|
| Send | Safe to transfer ownership across threads |
| Sync | Safe to share references across threads |
Auto-derived for nearly all standard types. Rc and RefCell are **not** Send/Sync — passing one across threads is a compile error, and Arc/Mutex are the substitute.
Hands-on Examples
Send 1~10 from a worker, sum in main:
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel::<i32>();
thread::spawn(move || {
for i in 1..=10 { tx.send(i).unwrap(); }
});
let sum: i32 = rx.iter().sum();
println!("sum = {}", sum); // 55
}Increment a counter safely from many threads:
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let counter = Arc::new(Mutex::new(0));
let mut hs = vec![];
for _ in 0..10 {
let c = Arc::clone(&counter);
hs.push(thread::spawn(move || {
let mut n = c.lock().unwrap();
*n += 1;
}));
}
for h in hs { h.join().unwrap(); }
println!("counter = {}", *counter.lock().unwrap()); // 10
}Common Mistakes
Q. Can I pass Rc across threads?
A. No — Rc's reference count is non-atomic and races. Use Arc instead (atomic). The compiler enforces this via Send.
Q. When does a Mutex unlock?
A. When the **MutexGuard you got from `lock()` is dropped** — usually at end of scope. To release earlier use `drop(guard)` or narrow the scope with an explicit block.
Q. async/await as a replacement?
A. CPU work fits threads; IO-bound work fits async. The next lesson covers async I/O.
Recap
- Spawn with `thread::spawn` + move closure
- Pass data via mpsc channels
- Shared mutable state = Arc<Mutex<T>>
- Send/Sync traits encode thread safety at compile time
Try It Yourself
- Four threads sum 1..25, 26..50, 51..75, 76..100; aggregate via mpsc in main
- Make `Arc<Mutex<Vec<i32>>>` and have several threads push into it safely
- Try passing Rc into `thread::spawn` to read the exact compiler error
All lecture materials and example code are openly available on GitHub.
View on GitHub ↗Comments 0
No comments yet. Be the first.
Only members can comment.
Comments with ads, abuse, personal information or illegal content are hidden or removed. Comments are shown with your nickname. Privacy Policy