Rust
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Ownership and Borrowing
-
Section 4: Structuring Data
-
Section 5: Collections and Error Handling
-
Section 6: Traits and Generics
-
Section 7: Concurrency
-
Section 8: Building for the Web
-
Section 9: Memory and Performance
-
Section 10: More Standard Library and Ecosystem
-
Section 11: Advanced Rust
-
Section 12: Rust for Systems and WebAssembly
-
Section 13: Tooling and Best Practices
-
Section 14: Data Structures and Algorithms in Rust
-
Section 15: Practical Projects
-
Section 16: Interview Practice
-
Section 17: std::collections In Depth
-
Section 18: std::io and std::fs In Depth
-
Section 19: std::net
-
Section 20: std::option and std::result In Depth
-
Section 21: std::iter In Depth
-
Section 22: std::sync In Depth
-
Section 23: std::string and std::str
-
Section 24: Cargo and Crates.io Ecosystem
-
Section 25: Popular Crates Ecosystem
-
Section 26: Rust Memory Model Deep Dive
-
Section 27: More Practice Exercises
-
Section 28: More Interview Practice
-
Section 29: Async Rust Deep Dive
-
Section 30: Tokio Ecosystem In Depth
-
Section 31: Error Handling Ecosystem Deep Dive
-
Section 32: Serde In Depth
-
Section 33: Web Development with Rust Deep Dive
-
Section 34: Database Access Ecosystem
-
Section 35: Rust for Embedded Systems Deep Dive
-
Section 36: Rust Macros In Depth
-
Section 37: Advanced Trait System
-
Section 38: Unsafe Rust In Depth
-
Section 39: Rust CLI Development
-
Section 40: Testing Ecosystem Deep Dive
-
Section 41: WebAssembly Deep Dive
-
Section 42: Rust Design Patterns
-
Section 43: More Data Structures in Rust
-
Section 44: Final Practice Projects
-
Section 45: Rust Performance Optimization
-
Section 46: Rust Ecosystem Tooling
-
Section 47: More Interview and Review
208: Property-Based Testing with proptest
We've all been there. You write a function, you write three or four unit tests covering the "obvious" cases, the tests pass, and you merge the PR. Then, two weeks later, a user reports a crash because they entered an emoji or a string with a length that wasn't a multiple of your chunk size.
The problem is that humans are terrible at imagining the "weird" inputs. We test the happy path and maybe one or two edge cases we remember from past traumas. Property-based testing (PBT) flips the script. Instead of picking specific inputs, you define properties—invariants that should always hold true regardless of the input—and let a tool like proptest try its hardest to break your code by throwing thousands of randomized inputs at it.
The Slicing Panic and the UTF-8 Trap
Take a look at this function. It's supposed to take a string and split it into chunks of a specific size. It looks straightforward enough:
fn chunk_string(s: &str, size: usize) -> Vec<&str> {
let mut result = Vec::new();
let mut start = 0;
while start < s.len() {
let end = start + size;
result.push(&s[start..end]);
start = end;
}
result
}
If I test this with chunk_string("hello world", 3), it works great. If I test it with "rust" and size 2, it works. I might even feel confident enough to ship it. But there are two massive bombs waiting to go off here. First, if the string length isn't perfectly divisible by size, the final &s[start..end] will attempt to slice past the end of the string, causing a panic. Second, Rust strings are UTF-8. If start or end lands in the middle of a multi-byte character (like an emoji), the program will panic immediately.
Finding the Edge Case with proptest
Instead of trying to manually brainstorm every possible weird string, we can use proptest. First, add it to your dev-dependencies. Then, we define a property: "no matter the string and no matter the size, this function should never panic, and the combined length of the chunks should equal the original length."
use proptest::prelude::*;
proptest! {
#[test]
fn doesnt_crash(s in ".*", size in 1..100usize) {
chunk_string(&s, size);
}
}
The moment I run this, proptest doesn't just tell me it failed; it performs "shrinking." It finds a massive failing input and then automatically tries to find the smallest possible input that still causes the failure. It will likely hand me something like s = "🦀" and size = 1. Since a crab emoji is 4 bytes, and I'm trying to slice at index 1, the code panics. It's a brutal, efficient way to find bugs.
Safe Slicing and Character Boundaries
To fix this, we need to stop thinking in bytes and start thinking in chars. We also need to handle the end of the string gracefully using std::cmp::min or by iterating over the characters directly. Here is the professional way to handle this:
fn chunk_string(s: &str, size: usize) -> Vec<String> {
let mut result = Vec::new();
let chars: Vec<char> = s.chars().collect();
for chunk in chars.chunks(size) {
let chunk_str: String = chunk.iter().collect();
result.push(chunk_str);
}
result
}
I've changed the return type to Vec<String> because we are now constructing new strings from characters rather than slicing the original byte array. This completely eliminates the risk of splitting a UTF-8 character in half. Now, when we run the proptest again, it will throw thousands of random strings—null bytes, emojis, empty strings, giant blocks of text—and every single one will pass.
The real magic here isn't the fix itself, but the confidence that comes from knowing the function survived a "gauntlet" of random data. When you write a unit test, you're proving the code works for that specific input. When you write a property test, you're proving the code works for a class of inputs.
📋 Practical Task
Hardening a Custom Integer Range Splitter
You have been handed a function called split_range that is intended to take a start value, an end value, and a number of segments, returning a Vec<i32> representing the boundaries of those segments. The current implementation is buggy and panics under certain conditions (e.g., when start > end or when segments is 0).
Your Task:
- Implement a
proptestblock that attempts to crash thesplit_rangefunction. Use a range ofi32for the start/end and ausizefor the segments. - Once
proptestidentifies the failing inputs (likely division by zero or subtraction overflows), rewrite thesplit_rangefunction to be totally robust. - The function should return an
Option<Vec<i32>>: returnNoneif the inputs are logically impossible (like segments = 0), andSome(vec)otherwise. - Update your property test to ensure that for any
Someresult, the resulting vector always has exactlysegments + 1elements.
// The buggy starting point
fn split_range(start: i32, end: i32, segments: usize) -> Vec<i32> {
let mut boundaries = Vec::new();
let step = (end - start) / segments as i32;
for i in 0..=segments {
boundaries.push(start + (i as i32 * step));
}
boundaries
}There are no comments for now.