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.

Intermediate⏱️ 50 min📚 Prerequisites: 2

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

BASH
rustup 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"
RUST
use 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"
RUST
use 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

  1. Always format: cargo fmt before committing
  2. Run Clippy: Fix warnings before merging
  3. Write docs: Document public APIs
  4. Test coverage: Aim for high test coverage
  5. CI/CD: Automate checks in CI
  6. Dependency updates: Regularly update with cargo update
  7. Security: Run cargo audit for vulnerabilities
  8. Performance: Use cargo bench for 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

RUST
// 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

RUST
// 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

RUST
/// 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!

Easy

Starter Code:

RUST
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);
}