Performant Software Systems with Rust — Lecture 14
Best Practices and Idiomatic Rust
Using clippy for static checking
Writing tests and running them using cargo test
Cargo, crates, and modules: building a larger project
Design patterns in Rust
Idiomatic Rust
Using clippy for Static Checking
Just run cargo clippy
If you wish to make sure it runs on everything in the current project, run cargo clippy --all-targets --all-features
To automatically fix some of the clippy issues, run cargo clippy --fix
Live Demo
Writing Tests
We use a Rust attribute #[cfg(test)] to tell the Rust compiler that a piece of code should only be compiled when the test config is active
#[cfg(...)] is one of the built-in attributes in RustLearn more: The Anatomy of a Test Function
Writing Tests: Example
Learn more: The Anatomy of a Test Function
Checking Results with the assert! Macro
Learn more: Checking Results with the assert! Macro
Checking Results with the assert! Macro
#[test]
fn larger_can_hold_smaller() {
let larger = Rectangle {
width: 8,
height: 7,
};
let smaller = Rectangle {
width: 5,
height: 1,
};
assert!(larger.can_hold(&smaller));
}
#[test]
fn smaller_cannot_hold_larger() {
let larger = Rectangle {
width: 8,
height: 7,
};
let smaller = Rectangle {
width: 5,
height: 1,
};
assert!(!smaller.can_hold(&larger));
}Learn more: Checking Results with the assert! Macro
Testing Equality with the assert_eq! and assert_ne! Macros
assert_eq! is equivalent to assert! with the == operatorassert_ne! is most useful when we know what a value definitely shouldn’t beassert! — they also print the values on the left and right if the assertion failedAdding Custom Failure Messages
Learn more: Adding Custom Failure Messages
Checking for Panics with should_panic
pub struct Guess {
value: i32,
}
impl Guess {
pub fn new(value: i32) -> Guess {
if value < 1 {
panic!(
"Guess value must be greater than or equal to 1."
);
} else if value > 100 {
panic!(
"Guess value must be less than or equal to 100."
);
}
Guess { value }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[should_panic(expected = "less than or equal to 100")]
fn greater_than_100() {
Guess::new(200);
}
}Learn more: Checking for Panics with should_panic
Using Result<T, E> in Tests
This allows the use of the ? operator in the body of tests
You can’t use the #[should_panic] annotation on tests that use Result<T, E>
Use assert!(value.is_err()) to assert that an operation returns an Err variant
Learn more: Using Result<T, E> in Tests
Using Result<T, E> in Tests
Learn more: Using Result<T, E> in Tests
cargo test command-line options
cargo test --help displays the options you can use with cargo test
cargo test -- --help displays the options you can use after the separator that go to the resultant test binary
Learn more: Controlling How Tests Are Run
cargo test command-line options
cargo test -- --test-threads=1 runs tests consecutively rather than in parallel
cargo test -- --show-output shows what’s printed to standard output
cargo test add_two_and_two runs only the named test add_two_and_two
cargo test add runs all tests with add in the name
Learn more: Controlling How Tests Are Run
Ignoring Tests
cargo test -- --ignored to run only the ignored tests
cargo test -- --include-ignored to run all tests
Unit Tests vs. Integration Tests
To add unit tests, create a module named tests in each file to contain the test functions, annotated with cfg(test)
Integration tests are external to your library, and are in the test directory
Learn more: Test Organization
Managing Large Projects
Our assignments have always been in one module and one file, main.rs
As a project grows, you should organize code by splitting it into multiple modules and then multiple files
Features in Rust that Helps Manage Large Projects
Crates and Packages
Defining Modules to Control Scope and Privacy
Grouping related code in modules
Private (default) vs. public visibility
Use the use keyword and a relative (with super::) or absolute path to create shortcuts
Separate modules into different files
Live Demo
Learn more: Defining Modules to Control Scope and Privacy
Design Patterns
Design patterns are “general reusable solutions to a commonly occurring problem within a given context in software design”
They are very language-specific and sometimes controversial
Learn more: Design Patterns
Design Patterns
If overused, design patterns can add unnecessary complexity to programs
Features in Rust allow us to throw out many conventional design patterns, which were invented in the prime times of object orientation
Learn more: Design Patterns
Prefer Small Crates
Prefer small crates and do not over-engineer the design
The url crate only provides tools for working with URLs
The num_cpus crate only provides a function to query the number of CPUs on a machine
Learn more: Prefer small crates
The Singleton Design Pattern
The singleton pattern restricts the instantiation of a type to a singular instance
The best practice is to avoid using this pattern completely
&Config) directly through your APIAppContext typeImplementing the Singleton Pattern
Example: Implementing Config as a Singleton
use std::sync::OnceLock;
#[derive(Debug)]
pub struct Config {
pub db_url: String,
pub num_workers: usize,
}
static CONFIG: OnceLock<Config> = OnceLock::new();
pub fn init_config(db_url: String, num_workers: usize) {
CONFIG
.set(Config { db_url, num_workers })
.expect("Config is already initialized");
}
pub fn config() -> &'static Config {
CONFIG.get().expect("Config is not initialized")
}Using the Singleton Pattern
Idiomatic Rust
Make Illegal States Unrepresentable
Make Illegal States Unrepresentable
Example: Managing a list of users
Learn more: Make Illegal States Unrepresentable
Make Illegal States Unrepresentable
But what happens if we create a user with an empty username?
Not what we want — the type system is our friend!
Learn more: Make Illegal States Unrepresentable
Define a Type that Represents a Username
struct Username(String);
impl Username {
// 'static: reference lives for the remaining lifetime of
// the running program; a string literal here is a `&'static str`
fn new(username: String) -> Result<Self, &'static str> {
if username.is_empty() {
return Err("Username cannot be empty");
}
Ok(Self(username))
}
}Learn more: Make Illegal States Unrepresentable
Define a Type that Represents a Username
Learn more: Make Illegal States Unrepresentable
What About the Birthdate?
A new user that is 1000 years old is perhaps not what we want either!
struct Birthdate(chrono::NaiveDate);
impl Birthdate {
fn new(birthdate: chrono::NaiveDate) -> Result<Self, &'static str> {
let today = chrono::Utc::today().naive_utc();
if birthdate > today {
return Err("Birthdate cannot be in the future")
}
let age = today.year() - birthdate.year();
if age < 12 {
return Err("Not old enough to register")
}
if age >= 122 {
return Err("The longest living person was 122 years old")
}
Ok(Self(birthdate))
}
}Learn more: Make Illegal States Unrepresentable
It’s Now Time to Write Some Tests!
[cfg(test)]
mod tests {
use super::*;
use chrono::Duration;
#[test]
fn test_birthdate() {
let today = chrono::Utc::today().naive_utc();
// Birthdate cannot be in the future
assert!(Birthdate::new(today + Duration::days(1)).is_err());
// Excuse me, how old are you?
assert!(Birthdate::new(today - Duration::days(365 * 122)).is_err());
// Not old enough
assert!(Birthdate::new(today - Duration::days(365 * 11)).is_err());
// Ok
assert!(Birthdate::new(today - Duration::days(365 * 15)).is_ok());
}
}Using Enums to Represent State
Using Enums to Represent State
Do not use bool to represent state!
What does active = false mean anyway?
Learn more: Using Enums to Represent State
Using Enums to Represent State
Can we use an unsigned integer to represent state?
It’s still not ideal!
Learn more: Using Enums to Represent State
Using Enums to Represent State
Enums are a great way to model state!
#[derive(Debug)]
pub enum UserStatus {
/// The user is active and has full access
/// to their account and any associated features.
Active,
/// The user's account is inactive.
/// This state can be reverted to active by
/// the user or an administrator.
Inactive,
/// The user's account has been temporarily suspended,
/// possibly due to suspicious activity or policy violations.
/// During this state, the user cannot access their account,
/// and an administrator's intervention might
/// be required to restore the account.
Suspended,
/// The user's account has been permanently
/// deleted and cannot be restored.
/// All associated data with the account might be
/// removed, and the user would need to create a new account
/// to use the service again.
Deleted,
}
struct User {
// ...
status: UserStatus,
}Learn more: Using Enums to Represent State
Aim for Immutability
Aim for Immutability
Variables in Rust are immutable by default
The mut keyword should be used sparingly, preferably only in tight scopes
Move instead of mut
Don’t be afraid of copying data!
Learn more: Aim for Immutability in Rust
Aim for Immutability
ub struct Mailbox {
/// The emails in the mailbox
// Obviously, don't represent emails as strings in real code!
// Use higher-level abstractions instead.
emails: Vec<String>,
/// The total number of words in all emails
total_word_count: usize,
}
impl Mailbox {
pub fn new() -> Self {
Mailbox {
emails: Vec::new(),
total_word_count: 0,
}
}
pub fn add_email(&mut self, email: &str) {
self.emails.push(email.to_string());
// Misguided optimization: Track the total word count
let word_count: usize = email.split_whitespace().count();
self.total_word_count += word_count;
}
pub fn get_word_count(&self) -> usize {
self.total_word_count
}
}Learn more: Aim for Immutability in Rust
Aim for Immutability
pub struct Mailbox {
emails: Vec<String>,
}
impl Mailbox {
pub fn new() -> Self {
Mailbox {
emails: Vec::new(),
}
}
pub fn add_email(&mut self, email: &str) {
self.emails.push(email.to_string());
}
pub fn get_word_count(&self) -> usize {
self.emails
.iter()
// In real code, `email` might have a `body` field with a
// `word_count()` method instead
.map(|email| email.split_whitespace().count())
.sum()
}
}Learn more: Aim for Immutability in Rust
That’s It for the Course!
Unique in Canada
In U.S. universities, only offered at Maryland, Stanford, UPenn, and Bennington College
I hope you learned a lot from this course, and good luck with your project!
The Rust Programming Language, Chapter 7 and 11