I am a doctoral researcher in scientific machine learning at Chair of scientific computing (Chair 05 at TUM School of CIT).
"Testing leads to failure, and failure leads to understanding." — Burt Rutan
.
├── Cargo.lock
├── Cargo.toml
├── src/
│ ├── lib.rs
│ ├── main.rs
│ ├── module_1/
│ │ ├── mod.rs
│ │ └── [...]
│ └── [...]
└── [...]
src/module_1/mod.rs)
src/module_1/tests.rs)
#[test]
fn test_name() {
// Setup: Prepare variables or context
[...]
// Assertion: Ensure a condition is true
assert!(...);
// Assertion: Check equality of values
assert_eq!(...);
// Assertion: Check inequality of values
assert_ne!(...);
}
$ cargo test on the consolesrc/add/mod.rspub fn add(a: i32, b: i32) -> i32 {
a + b
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add() {
let result = add(2, 3);
assert_eq!(result, 5);
}
}
src/add/mod.rspub fn add(a: i32, b: i32) -> i32 {
a + b
}
#[cfg(test)]
mod tests;
src/add/tests.rsuse super::*;
#[test]
fn test_add() {
let result = add(2, 3);
assert_eq!(result, 5);
}
$ cargo test add
[...]
running 1 test
test add::tests::test_add ... ok
[...]
$ cargo test add::test_add$ cargo test some_other_module
[...]
test some_other_module::tests::test_some_function ... FAILED
failures:
---- some_other_module::tests::test_some_function stdout ----
thread 'some_other_module::tests::test_some_function' panicked:
assertion `left == right` failed
left: 0
right: 1
[...]
assertion `left == right` failed may not give us enough informationsrc/some_other_module/tests.rsuse super::*;
#[test]
fn test_some_function() {
let result = some_function(); // 0
assert_eq!(
some_function(), 1,
"{} must equal 1", result
);
}
assertion `left == right` failed: 0 must equal 1
assert!, assert_ne!should_panic#[test]
#[should_panic]
fn test_divide_by_zero() {
div(1, 0);
}
#[test]
#[should_panic(expected = "Cannot divide by zero")]
fn test_divide_by_zero() {
div(1, 0);
}
Result type?fn add_strings(a: &str, b: &str) -> Result<i32, String> {
let num_a: i32 = a.parse()
.map_err(|_| "Failed to parse left operand")?;
let num_b: i32 = b.parse()
.map_err(|_| "Failed to parse right operand")?;
Ok(num_a + num_b)
}
Result type in tests#[test]
fn test_add_strings() -> Result<(), String> {
let result = add_strings("2", "3")?;
assert_eq!(result, 5, "Two plus three must equal five");
Ok(())
}
$ cargo test -- --test-threads=1
--test-threads specifies the number of threads you want to use to the test binary
--test-threads=1 means running the tests consecutively#[test]
#[ignore]
fn expensive_test() {
// code that takes an hour to run
}
$ cargo test -- --ignored to only run the ignored tests$ cargo test -- --include-ignored to run all tests/// Adds two numbers.
///
/// # Examples
///
/// ```
/// use my_crate::add;
///
/// assert_eq!(add(2,2), 4);
/// ```
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
$ cargo test, Rust ecosystem encourages testing through documentation!
.
├── Cargo.lock
├── Cargo.toml
├── src/
│ └── [...]
└── tests/
└── integration_test.rs
tests/ directory is a separate cratetests/ with #[cfg(test)]$ cargo test runs first unit tests, then integration tests, and then the doc testssrc/main.rsuse add_cli::{add, parse};
use std::env;
fn main() -> Result<(), String> {
let args: Vec<String> = env::args().collect();
let left: i32 = parse(&args[1])?;
let right: i32 = parse(&args[2])?;
print!("Sum: {}\n", add(left, right));
Ok(())
}
$ cargo run -- 1 2 outputs Sum: 3 with a newline
parse and addtests/test_parse_and_add.rsuse add_cli::{add, parse};
#[test]
fn test_successful_parse_and_add() -> Result<(), String> {
let num_str_left = "1";
let num_str_right = "2";
let left: i32 = parse(num_str_left)?;
let right: i32 = parse(num_str_right)?;
let sum = add(left, right);
assert_eq!(sum, 3);
Ok(())
}
tests/test_add_cli.rsuse std::process::Command;
#[test]
fn test_valid_input() {
let output = Command::new(env!("CARGO_BIN_EXE_add_cli"))
.arg("1").arg("2")
.output()
.expect("Failed to execute command");
assert!(output.status.success());
let stdout = String::from_utf8(output.stdout)
.expect("Failed to parse stdout");
assert_eq!(stdout, "Sum: 3\n");
}
nextest: Faster and prettier test runner (doctests are not supported yet).
cargo-nextest
tarpaulin: Code coverage tool.
22.58% coverage, 7/31 lines covered
proptest: Property-based testing.
src/main.rs// The main executable will consist of many methods
// We can usually make many decision-mistakes here
// Example: Training a model to approximate a complex function.
// 1) Generate a dataset
// 2) Configure a model
// 3) Fit the model
// 4) Inspect the error and visualize the solution
fn main() {}
todo!(message)todo!src/main.rsfn main() {
let input = 5;
let result = square(input);
println!("The square of 5 is {}", result);
}
fn square(x: i32) -> i32 {
todo!("Implement square function");
}
$ cargo run gives
debug_assert!, debug_assert_eq!fn square(x: i32) -> i32 {
debug_assert!(x.abs() < 1000,
"Input is too large for this example");
todo!("Implement square function");
}
todo! to make your code compile for your skeletondebug_assert! is like assert but only runs in debug modeassert! can be used too if you must abort in an unrecoverable statedebug_print crate to have print macros that are not compiled in release builds#[test]
fn test_square() {
assert_eq!(square(2), 4);
assert_eq!(square(-3), 9);
assert_eq!(square(0), 0);
}
fn square(x: i32) -> i32 {
debug_assert!(x.abs() < 1000,
"Input is too large for this example");
x * x
}
let status = if logged_in {
"Active"
} else {
"Inactive"
};
let state = loop {
// game loop
if game_over {
break 1
}
};
let grade = match score {
90..=100 => "A",
80..=89 => "B",
70..=79 => "C",
_ => "D",
};
mod to organize your code into modulessrc/lib.rspub mod math {
pub fn square(x: i32) -> i32 {...}
}
src/math.rssrc/math/mod.rsCargo.lock//! for module docssrc/lib.rs// [Usually some License information]
//! This is the root module documentation for the crate.
//! It describes the overall purpose and functionality.
// Module definition, this is a normal comment
pub mod math;
/// for function docssrc/math.rs//! This is the documentation for the `math` module.
/// Docs for the square function
/// Calculates the square of a number.
pub fn square(x: i32) -> i32 {
x * x
}
match result {
Ok(value) => process(value),
Err(err) => match err {
Error::NotFound => handle_not_found(),
Error::PermissionDenied => handle_permission(),
},
}
match result {
Ok(value) => process(value),
Err(Error::NotFound) => handle_not_found(),
Err(Error::PermissionDenied) => handle_permission(),
}
let msg = match a {
Some(x) => match b {
Some(y) => format!("Both: {}, {}", x, y),
None => format!("Only a: {}", x),
},
None => match b {
Some(y) => format!("Only b: {}", y),
None => String::from("Neither"),
}
};
let msg = match (a, b) {
(Some(x), Some(y)) => format!("Both: {}, {}", x, y),
(Some(x), None) => format!("Only a: {}", x),
(None, Some(y)) => format!("Only b: {}", y),
(None, None) => String::from("Neither"),
};
rustfmt$ cargo fmtfn main() { let x=5;let y=10;println!("Sum: {}",x+y); }
$ cargo fmtfn main() {
let x = 5;
let y = 10;
println!("Sum: {}", x + y);
}
rustfmt.tomlrustc gives you warnings when you run $ cargo build to build your codewarning: variable `someNumber` should have a snake case name
--> src/main.rs:23:9
|
23 | let someNumber = 12;
| ^^^^^^^^^^ help: convert the identifier to
snake case: `some_number`
clippyclippylet vec = vec![1, 2, 3];
if vec.len() > 0 {
println!("Vector is not empty");
}
$ cargo clippywarning: length comparison to zero
--> src/main.rs:27:8
|
27 | if vec.len() > 0 {
| ^^^^^^^^^^^^^ help: using `!is_empty` is clearer
and more explicit:
`!vec.is_empty()`
Cargo.toml$ cargo build --release$ cargo check instead of $ cargo build to quickly ensure your code still compiles during development--release flag during development for incremental builds: Debug builds are usually faster than release builds[profile.release]
codegen-units = 1
[profile.release]
lto = true # or "thin". This is default for release
# lto = "fat" # Uncomment for more aggresive LTO
$ RUSTFLAGS="-C target-cpu=native" cargo build --release
$ rustc --print cfg and $ rustc --print cfg -C target-cpu=nativejemalloc, mimalloc), profile-guided optimization
[profile.release]
opt-level = "z"
# Uncomment for vectorization of loops
# Targets minimal binary size little less aggressively
# opt-level = "s"
[profile.release]
panic = "abort"
lld or mold which are often faster
$ RUSTFLAGS="-C link-arg=-fuse-ld=lld" cargo build --release
[profile.dev]
opt-level = 1 # Enable minimal optimization for your code
[profile.dev.package."*"]
opt-level = 3 # Enable optimization for dependencies
[profile.release]
lto = "off"
cranelift)
std::time::Instant
criterion
std::time::Instantlet start = std::time::Instant::now();
expensive_function();
let secs = start.elapsed().as_secs_f64();
criterion when necessaryenum E {
A, // holds 0 bytes
B(i32), // holds 4 bytes
C(u64, u8, u64, u8), // holds 18 bytes
D(Vec<u32>) // holds 24 bytes
}
enum E {
A, // holds 0 bytes
B(i32), // holds 4 bytes
C(Box<(u64, u8, u64, u8)>), // holds 8 bytes
D(Box<Vec<u32>>) // holds 8 bytes
}
Vec that is unlikely to be changed in the futurelet v: Vec<u32> = vec![1, 2, 3];
let boxed_slice: Box<[u32]> = Box::new([1, 2, 3]);
let v: Vec<u32> = boxed_slice.into_vec(); // back to Vec
Vec type without cloning or reallocation
#[should_panic], assertions and debug assertions to catch errors early.rustfmt and clippy