Rust Tooling Ecosystem: Cargo, Rustfmt, Clippy, and CI/CD
Master Rust's development tools: advanced Cargo features, Rustfmt, Clippy linter, documentation tools, testing frameworks, and CI/CD pipelines.
Rust Tooling Ecosystem: Cargo, Rustfmt, Clippy, and CI/CD
Rust's tooling ecosystem makes development efficient and enjoyable.
Cargo Advanced Features
Workspaces
Workspaces allow managing multiple related packages together.
TOML# Cargo.toml (workspace root) [workspace] members = [ "crates/parser", "crates/compiler", "crates/runtime", ] [workspace.package] version = "1.0.0" edition = "2021" [workspace.dependencies] serde = { version = "1.0", features = ["derive"] }
Features
Features enable conditional compilation.
TOML# Cargo.toml [features] default = ["std", "async"] std = [] async = ["tokio"] no-std = [] [dependencies] tokio = { version = "1.0", optional = true }
RUST// In code #[cfg(feature = "async")] use tokio::runtime::Runtime; #[cfg(not(feature = "no-std"))] use std::collections::HashMap;
Build Scripts
Build scripts run before compilation.
RUST// build.rs fn main() { // Generate code println!("cargo:rerun-if-changed=src/schema.proto"); // Set environment variables println!("cargo:rustc-env=BUILD_VERSION={}", env!("CARGO_PKG_VERSION")); // Link libraries println!("cargo:rustc-link-lib=static=mylib"); }
Cargo Profiles
TOML[profile.dev] opt-level = 0 # No optimization debug = true # Include debug info overflow-checks = true # Runtime overflow checks [profile.release] opt-level = 3 # Maximum optimization lto = true # Link-time optimization codegen-units = 1 # Better optimization strip = true # Strip symbols [profile.test] opt-level = 1 # Some optimization for tests debug = true
Cargo Commands
BASH# Build cargo build # Debug build cargo build --release # Release build cargo build --features async # With features # Testing cargo test # Run all tests cargo test --lib # Only library tests cargo test -- --nocapture # Show println! output cargo test test_name # Run specific test # Documentation cargo doc # Generate docs cargo doc --open # Open in browser cargo doc --no-deps # Only current crate # Dependencies cargo update # Update dependencies cargo tree # Show dependency tree cargo tree --duplicates # Find duplicate deps cargo audit # Security audit # Code Quality cargo fmt # Format code cargo clippy # Run linter cargo clippy -- -W clippy::all # All lints # Benchmarks cargo bench # Run benchmarks # Clean cargo clean # Remove build artifacts
Rustfmt
Rustfmt automatically formats Rust code.
Configuration
TOML# rustfmt.toml edition = "2021" max_width = 100 tab_spaces = 4 newline_style = "Unix" use_small_heuristics = "Default" # Formatting options fn_args_layout = "Tall" brace_style = "SameLineWhere" control_brace_style = "AlwaysSameLine"
Usage
BASH# Format all files cargo fmt # Format specific file cargo fmt -- src/main.rs # Check formatting (CI) cargo fmt -- --check # Format with specific config cargo fmt --config max_width=120
Editor Integration
Most editors auto-format on save:
- VS Code: rust-analyzer extension
- Vim/Neovim: rustfmt on save
- IntelliJ: Rust plugin
Clippy
Clippy is Rust's linter that catches common mistakes.
Installation
BASHrustup component add clippy
Usage
BASH# Run clippy cargo clippy # All lints (including pedantic) cargo clippy -- -W clippy::all -W clippy::pedantic # Allow specific lint cargo clippy -- -A clippy::too_many_arguments # Deny specific lint cargo clippy -- -D clippy::unwrap_used # Fix automatically cargo clippy --fix
Common Lints
RUST// Clippy warns about: // 1. Unnecessary clones let s = String::from("hello"); let s2 = s.clone(); // Warning: unnecessary clone // 2. Unused variables let _unused = 42; // Use underscore prefix // 3. Unnecessary unwrap let value = option.unwrap(); // Warning: use expect or handle error // 4. Single char patterns if s == "a" { } // Should use 'a' // 5. Manual range checks if x >= 0 && x < 10 { } // Use (0..10).contains(&x) // 6. Redundant closures vec.iter().map(|x| x + 1).collect(); // Use .map(|&x| x + 1)
Configuration
TOML# .clippy.toml or clippy.toml # Allow specific lints # Or use attributes in code # #[allow(clippy::too_many_arguments)]
In Code
RUST#[allow(clippy::too_many_arguments)] fn complex_function(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) { // ... } #[warn(clippy::all)] mod strict_module { // All clippy lints enabled here }
Documentation Tools
Writing Documentation
RUST/// This function adds two numbers. /// /// # Examples /// /// ``` /// use my_crate::add; /// /// assert_eq!(add(2, 3), 5); /// ``` /// /// # Panics /// /// This function will panic if the result overflows. /// /// # Errors /// /// Returns an error if... pub fn add(a: u32, b: u32) -> u32 { a + b } /// Module-level documentation /// /// This module provides utilities for... mod utils { // ... }
Doc Tests
RUST/// Calculates factorial /// /// # Examples /// /// ``` /// # use my_crate::factorial; /// assert_eq!(factorial(5), 120); /// ``` /// /// ```should_panic /// # use my_crate::factorial; /// factorial(1000); // Panics on overflow /// ``` pub fn factorial(n: u32) -> u32 { if n <= 1 { 1 } else { n * factorial(n - 1) } }
Generating Documentation
BASH# Generate docs cargo doc # Open in browser cargo doc --open # Private items too cargo doc --document-private-items # All dependencies cargo doc --all-features # Serve docs locally cargo doc --no-deps && python3 -m http.server --directory target/doc
Documentation Attributes
RUST#[doc(hidden)] pub fn internal_function() { // Hidden from public docs } #[doc(alias = "add")] #[doc(alias = "sum")] pub fn combine(a: u32, b: u32) -> u32 { a + b }
Testing Frameworks
Built-in Testing
RUST#[cfg(test)] mod tests { use super::*; #[test] fn test_basic() { assert_eq!(2 + 2, 4); } #[test] #[should_panic] fn test_panic() { panic!("This should panic"); } #[test] #[ignore] fn test_expensive() { // Only run with cargo test -- --ignored } }
Test Organization
RUST// tests/integration_test.rs use my_crate; #[test] fn integration_test() { // Integration test } // tests/common/mod.rs pub fn setup() { // Test setup } // benches/my_bench.rs #![feature(test)] extern crate test; use test::Bencher; #[bench] fn bench_function(b: &mut Bencher) { b.iter(|| { // Code to benchmark }); }
External Testing Crates
Criterion (Benchmarking)
TOML[dev-dependencies] criterion = { version = "0.5", features = ["html_reports"] } [[bench]] name = "my_bench" harness = false
RUST// benches/my_bench.rs use criterion::{black_box, criterion_group, criterion_main, Criterion}; fn benchmark_function(c: &mut Criterion) { c.bench_function("fib 20", |b| b.iter(|| fibonacci(black_box(20)))); } criterion_group!(benches, benchmark_function); criterion_main!(benches);
Proptest (Property-Based Testing)
TOML[dev-dependencies] proptest = "1.0"
RUSTuse proptest::prelude::*; proptest! { #[test] fn test_addition(a in 0u32..1000, b in 0u32..1000) { let result = a + b; prop_assert!(result >= a); prop_assert!(result >= b); } }
Mockall (Mocking)
TOML[dev-dependencies] mockall = "0.11"
RUSTuse mockall::automock; #[automock] trait Database { fn get_user(&self, id: u64) -> Option<String>; } #[test] fn test_with_mock() { let mut mock_db = MockDatabase::new(); mock_db.expect_get_user() .with(predicate::eq(1)) .returning(|_| Some("Alice".to_string())); assert_eq!(mock_db.get_user(1), Some("Alice".to_string())); }
CI/CD for Rust
GitHub Actions
YAML# .github/workflows/rust.yml name: Rust on: push: branches: [ main ] pull_request: branches: [ main ] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Install Rust uses: actions-rs/toolchain@v1 with: toolchain: stable components: rustfmt, clippy override: true - name: Cache dependencies uses: actions/cache@v3 with: path: | ~/.cargo/bin/ ~/.cargo/registry/index/ ~/.cargo/registry/cache/ ~/.cargo/git/db/ target/ key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} - name: Format check run: cargo fmt -- --check - name: Clippy run: cargo clippy -- -D warnings - name: Build run: cargo build --verbose - name: Test run: cargo test --verbose - name: Build docs run: cargo doc --no-deps coverage: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Install Rust uses: actions-rs/toolchain@v1 with: toolchain: stable - name: Run tests with coverage uses: actions-rs/cargo@v1 with: command: test args: --all-features - name: Upload coverage uses: codecov/codecov-action@v3
GitLab CI
YAML# .gitlab-ci.yml image: rust:latest stages: - test - build - deploy variables: CARGO_HOME: $CI_PROJECT_DIR/.cargo cache: paths: - .cargo/ - target/ test: stage: test script: - rustup component add rustfmt clippy - cargo fmt -- --check - cargo clippy -- -D warnings - cargo test --all-features build: stage: build script: - cargo build --release artifacts: paths: - target/release/ deploy: stage: deploy script: - echo "Deploy to production" only: - main
Pre-commit Hooks
BASH# .git/hooks/pre-commit #!/bin/bash # Format check cargo fmt -- --check || { echo "Code is not formatted. Run 'cargo fmt'" exit 1 } # Clippy cargo clippy -- -D warnings || { echo "Clippy found issues" exit 1 } # Tests cargo test || { echo "Tests failed" exit 1 }
Best Practices
- Always format:
cargo fmtbefore committing - Run Clippy: Fix warnings before merging
- Write docs: Document public APIs
- Test coverage: Aim for high test coverage
- CI/CD: Automate checks in CI
- Dependency updates: Regularly update with
cargo update - Security: Run
cargo auditfor vulnerabilities - Performance: Use
cargo benchfor benchmarks
Useful Tools
- cargo-watch: Auto-run commands on file changes
- cargo-expand: Expand macros
- cargo-udeps: Find unused dependencies
- cargo-tree: Visualize dependency tree
- cargo-audit: Security audit
- cargo-outdated: Check for outdated dependencies
- cargo-deny: Lint dependencies
- cargo-machete: Remove unused dependencies
Code Examples
Cargo Workspace
Setting up a Cargo workspace
// Workspace structure:
// my-project/
// Cargo.toml (workspace root)
// crates/
// parser/
// Cargo.toml
// src/lib.rs
// compiler/
// Cargo.toml
// src/lib.rs
// Cargo.toml (workspace root)
/*
[workspace]
members = ["crates/parser", "crates/compiler"]
[workspace.package]
version = "1.0.0"
edition = "2021"
[workspace.dependencies]
serde = { version = "1.0", features = ["derive"] }
*/
// crates/parser/Cargo.toml
/*
[package]
name = "parser"
version.workspace = true
edition.workspace = true
[dependencies]
serde.workspace = true
*/
// Example: Using workspace dependencies
fn main() {
println!("Workspace allows sharing dependencies and version management");
println!("All crates in workspace share same dependency versions");
println!("Build all: cargo build --workspace");
println!("Test all: cargo test --workspace");
}Explanation:
Cargo workspaces allow managing multiple related crates together. They share dependency versions and can be built/tested together. This is essential for large projects with multiple components.
Clippy Lints
Common Clippy warnings and fixes
// Clippy will warn about these:
// 1. Unnecessary clone
fn bad_clone() {
let s = String::from("hello");
let s2 = s.clone(); // Clippy: unnecessary clone
println!("{}", s2);
}
// Fixed:
fn good_clone() {
let s = String::from("hello");
let s2 = &s; // Just borrow
println!("{}", s2);
}
// 2. Unnecessary unwrap
fn bad_unwrap() {
let opt = Some(42);
let value = opt.unwrap(); // Clippy: use expect or handle error
println!("{}", value);
}
// Fixed:
fn good_unwrap() {
let opt = Some(42);
match opt {
Some(value) => println!("{}", value),
None => println!("No value"),
}
}
// 3. Single char string
fn bad_char() {
let s = "hello";
if s == "a" { } // Clippy: use 'a' instead
}
// Fixed:
fn good_char() {
let s = "hello";
if s == 'a' { } // Use char literal
}
// 4. Manual range check
fn bad_range(x: u32) -> bool {
x >= 0 && x < 10 // Clippy: use range.contains
}
// Fixed:
fn good_range(x: u32) -> bool {
(0..10).contains(&x)
}
fn main() {
println!("Run 'cargo clippy' to see these warnings");
println!("Clippy helps write idiomatic Rust code!");
}Explanation:
Clippy catches common mistakes and suggests improvements. It helps write idiomatic Rust code. Many warnings can be auto-fixed with 'cargo clippy --fix'.
Documentation Tests
Writing and running doc tests
/// Adds two numbers together.
///
/// # Examples
///
/// ```
/// use my_crate::add;
/// assert_eq!(add(2, 3), 5);
/// ```
///
/// # Panics
///
/// This function will panic on overflow:
///
/// ```should_panic
/// # use my_crate::add;
/// add(u32::MAX, 1);
/// ```
pub fn add(a: u32, b: u32) -> u32 {
a.checked_add(b).expect("Addition overflow")
}
/// Calculates factorial.
///
/// # Examples
///
/// ```
/// # use my_crate::factorial;
/// assert_eq!(factorial(5), 120);
/// assert_eq!(factorial(0), 1);
/// ```
pub fn factorial(n: u32) -> u32 {
match n {
0 | 1 => 1,
_ => n * factorial(n - 1),
}
}
/// Processes a vector of numbers.
///
/// # Examples
///
/// ```
/// # use my_crate::process;
/// let numbers = vec![1, 2, 3];
/// let result = process(&numbers);
/// assert_eq!(result, 6);
/// ```
pub fn process(numbers: &[u32]) -> u32 {
numbers.iter().sum()
}
fn main() {
println!("Doc tests run with: cargo test --doc");
println!("They ensure examples in documentation are correct!");
}Explanation:
Doc tests are code examples in documentation that are automatically tested. They ensure documentation stays up-to-date and examples work correctly. Run with 'cargo test --doc'.
Exercises
Fix Clippy Warnings
Fix Clippy warnings in this code!
Starter Code:
fn process_data(data: Vec<String>) -> Vec<String> {
let mut result = Vec::new();
for item in data {
let cloned = item.clone();
result.push(cloned);
}
result
}
fn main() {
let data = vec![String::from("hello"), String::from("world")];
let result = process_data(data);
println!("{:?}", result);
}