Rust 4 - Modules, crates, testing, documentation
Petr Janik
Posted on August 26, 2020
Rust 4 - Modules, crates, testing, documentation
Modules
We need to explicitly build the module tree in Rust - there’s no implicit mapping between file system tree to module tree.
Consider we want to create the following module structure (the leaves of this tree are functions):
crate
└── front_of_house
├── hosting
│ ├── add_to_waitlist
│ └── seat_at_table
└── serving
├── take_order
├── serve_order
└── take_payment
To create this module structure we can add the following to the main.rs
file.
// main.rs
mod front_of_house {
mod hosting {
fn add_to_waitlist() {}
fn seat_at_table() {}
}
mod serving {
fn take_order() {}
fn serve_order() {}
fn take_payment() {}
}
}
fn main() {}
Or we create the following files structure:
src
├── front_of_house
│ ├── mod.rs
│ ├── hosting.rs
│ └── serving.rs
└── main.rs
The hosting.rs
and serving.rs
files are modules and contain the functions mentioned above.
mod.rs
file must declare the submodules, such as:
// mod.rs
mod hosting;
mod serving;
In main.rs
, we must also declare the front_of_house
submodule.
// main.rs
mod front_of_house;
fn main() {}
To be able to call the front_of_house::serving::take_order();
function from main.rs
, the function has to be public. Also the modules leading to that function have to be public.
Make the function public.
// serving.rs
pub fn take_order(){}
fn serve_order(){}
fn take_payment(){}
Make the serving
module in mod.rs
public.
// mod.rs
mod hosting;
pub mod serving;
Now we can call it from main()
.
mod front_of_house;
fn main() {
// Absolute path
crate::front_of_house::serving::take_order();
// Relative path
front_of_house::serving::take_order();
}
For more thorough explanation of modules, I suggest reading the following posts:
Clear explanation of Rust's module system
Public structures
mod back_of_house {
pub struct Breakfast {
pub toast: String,
seasonal_fruit: String,
}
impl Breakfast {
pub fn summer(toast: &str) -> Breakfast {
Breakfast {
toast: String::from(toast),
seasonal_fruit: String::from("peaches"),
}
}
}
}
pub fn eat_at_restaurant() {
// Order a breakfast in the summer with Rye toast
let mut meal = back_of_house::Breakfast::summer("Rye");
// Change our mind about what bread we'd like
meal.toast = String::from("Wheat");
println!("I'd like {} toast please", meal.toast);
// The next line won't compile if we uncomment it; we're not allowed
// to see or modify the seasonal fruit that comes with the meal
// meal.seasonal_fruit = String::from("blueberries");
}
Public enums
mod back_of_house {
pub enum Appetizer {
Soup,
Salad,
}
}
pub fn eat_at_restaurant() {
let order1 = back_of_house::Appetizer::Soup;
let order2 = back_of_house::Appetizer::Salad;
}
use with relative and absolute path
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
pub fn eat_at_restaurant_rel() {
// relative
use self::front_of_house::hosting;
hosting::add_to_waitlist();
}
pub fn eat_at_restaurant_abs() {
// absolute
use crate::front_of_house::hosting;
hosting::add_to_waitlist();
}
pub fn eat_at_restaurant_full() {
// full path
use crate::front_of_house::hosting::add_to_waitlist;
add_to_waitlist();
}
use with multiple modules
use std::io::{self, Write};
// Write does not need to be prefixed but you’d still need to do io::BufReader and so on.
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
pub fn seat_at_table() {}
}
}
fn eat_at_restaurant() {
use front_of_house::hosting::{self, add_to_waitlist};
hosting::seat_at_table();
hosting::add_to_waitlist();
add_to_waitlist();
}
Glob operator - avoid
use std::collections::*;
super and self
fn function() {
println!("called `function()`");
}
mod cool {
pub fn function() {
println!("called `cool::function()`");
}
}
mod my {
fn function() {
println!("called `my::function()`");
}
mod cool {
pub fn function() {
println!("called `my::cool::function()`");
}
}
pub fn indirect_call() {
// Let's access all the functions named `function` from this scope!
print!("called `my::indirect_call()`, that\n");
// The `self` keyword refers to the current module scope - in this case `my`.
// Calling `self::function()` and calling `function()` directly both give
// the same result, because they refer to the same function.
self::function();
function();
// We can also use `self` to access another module inside `my`:
self::cool::function();
// The `super` keyword refers to the parent scope (outside the `my` module).
super::function();
// This will bind to the `cool::function` in the *crate* scope.
// In this case the crate scope is the outermost scope.
{
use crate::cool::function as root_function;
root_function();
}
}
}
fn main() {
my::indirect_call();
// prints:
// called `my::indirect_call()`, that
// called `my::function()`
// called `my::function()`
// called `my::cool::function()`
// called `function()`
// called `cool::function()`
}
Dependencies
On crates.io
// Cargo.toml
// ...
[dependencies]
time = "0.2.16"
// main.rs
use time;
fn main() {
println!("2020 has {} days", time::days_in_year(2020));
}
Local
projects
├── hello_utils
│ └── src
│ └── lib.rs
└── my_project
└── src
└── main.rs
// projects/my_project/Cargo.toml
// ...
[dependencies]
hello_utils = { path = "../hello_utils" }
// lib.rs
pub fn hello(){
println!("Hello!");
}
// main.rs
use hello_utils;
fn main() {
hello_utils::hello();
}
Workspace
Create add
folder.
Inside add
folder, run cargo new adder
and cargo new add-one --lib
.
This creates the following structure:
add
├── Cargo.lock
├── Cargo.toml
├── add-one
│ ├── Cargo.toml
│ └── src
│ └── lib.rs
├── adder
│ ├── Cargo.toml
│ └── src
│ └── main.rs
└── target
// add/Cargo.toml (note that [package] section is missing)
[workspace]
members = [
"adder",
"add-one",
]
// add/add-one/src/lib.rs
pub fn add_one(x: i32) -> i32 {
x + 1
}
// add/adder/src/main.rs
use add_one;
fn main() {
let num = 10;
println!(
"Hello, world! {} plus one is {}!",
num,
add_one::add_one(num)
);
}
// add/adder/Cargo.toml
// ...
[dependencies]
add-one = { path = "../add-one" }
Run with cargo run -p adder
.
Output: Hello, world! 10 plus one is 11!.
Testing
We can write tests either in the same file or separately.
Unit tests
Put unit tests in the
src
directory in each file with the code that they’re testing. The convention is to create a module namedtests
in each file to contain the test functions and to annotate the module with#[cfg(test)]
.
// src/lib.rs
#[cfg(test)]
mod tests {
#[test]
fn it_works() {
assert_eq!(4, 2 + 2);
}
}
Integration tests
Integration test use your library in the same way any other code would, which means they can only call functions that are part of your library’s public API. To create integration tests, you need a
tests
directory next to yoursrc
directory.
// tests/integration_test.rs
use adder;
#[test]
fn it_adds_two() {
assert_eq!(4, adder::add_two(2));
}
Run tests with:
# runs all tests
cargo test
# runs only library tests
cargo test --lib
# runs only documentation tests
cargo test --doc
Test fails when the program panics.
#[test]
fn test_that_fails() {
panic!("Make this test fail");
}
Assertions
// when you want to ensure that some condition in a test evaluates to true
assert!(4 == 2 + 2);
// compare two arguments for equality
assert_eq!(4, 2 + 2);
// compare two arguments for inequality
assert_ne!(4, 2 + 1);
Custom messages
pub fn greeting(name: &str) -> String {
String::from("Hello!")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn greeting_contains_name() {
let result = greeting("Carol");
assert!(
result.contains("Carol"),
"Greeting did not contain name, value was `{}`",
result
);
}
}
Testing panic
pub struct Guess {
value: i32,
}
impl Guess {
pub fn new(value: i32) -> Guess {
if value < 1 || value > 100 {
panic!("Guess value must be between 1 and 100, got {}.", value);
}
Guess { value }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[should_panic]
fn greater_than_100() {
Guess::new(200);
}
}
Test can return Result
#[cfg(test)]
mod tests {
#[test]
fn it_works() -> Result<(), String> {
if 2 + 2 == 4 {
Ok(())
} else {
Err(String::from("two plus two does not equal four"))
}
}
}
Documentation
Uses CommonMark.
Tests in documentation
Sections
Generate documentation with:
# '--open' opens generated documentation in the browser
cargo doc --open
Exercises
Password converter - part 2
Task
Refactor program from the previous week into a library.
Inside converters
module, create a Converter
trait.
Implement this trait for KasperskyPasswordManager
inside converters::kaspersky
module.
Write documentation, doc tests and unit tests for the KasperskyPasswordManager
.
Make KasperskyPasswordManager
generic.
Solution
Adventure game - part 2
Task
Add fight scenes to the adventure game from the previous week.
There will be an enemy. The enemy has an attack damage range and health.
The player has an attack damage range and health (for each scene separate).
The player chooses an action:
- attack - a random value in attack damage range is dealt to the enemy. If the enemy dies, the player proceeds to the next scene. Otherwise the enemy attacks back. If the player survives, the options are repeated with updated health. If the player dies, the game is over.
- run away - go to another scene
Scenes will share code using trait.
Add documentation and tests.
Solution
Check out my learning Rust repo on GitHub!
Posted on August 26, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.