agentic-team-templates 0.10.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/index.js +4 -0
- package/src/index.test.js +1 -0
- package/templates/rust-expert/.cursorrules/concurrency.md +250 -0
- package/templates/rust-expert/.cursorrules/ecosystem-and-tooling.md +299 -0
- package/templates/rust-expert/.cursorrules/error-handling.md +190 -0
- package/templates/rust-expert/.cursorrules/overview.md +142 -0
- package/templates/rust-expert/.cursorrules/ownership-and-borrowing.md +204 -0
- package/templates/rust-expert/.cursorrules/performance-and-unsafe.md +256 -0
- package/templates/rust-expert/.cursorrules/testing.md +300 -0
- package/templates/rust-expert/.cursorrules/traits-and-generics.md +236 -0
- package/templates/rust-expert/CLAUDE.md +283 -0
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
# Rust Performance and Unsafe
|
|
2
|
+
|
|
3
|
+
Rust's zero-cost abstractions mean you rarely need to choose between ergonomics and performance. When you do need to go lower, `unsafe` is the mechanism — and it comes with strict obligations.
|
|
4
|
+
|
|
5
|
+
## Performance
|
|
6
|
+
|
|
7
|
+
### Measure First
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
# Profile before optimizing
|
|
11
|
+
cargo bench # Run benchmarks
|
|
12
|
+
cargo flamegraph # Generate flamegraph (needs cargo-flamegraph)
|
|
13
|
+
cargo instruments -t "Time Profiler" # macOS Instruments
|
|
14
|
+
|
|
15
|
+
# Check binary size
|
|
16
|
+
cargo bloat --release --crates # Which crates contribute to binary size
|
|
17
|
+
cargo bloat --release --filter "my_" # Your functions by size
|
|
18
|
+
|
|
19
|
+
# Check compile times
|
|
20
|
+
cargo build --timings # HTML report of compile times per crate
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### Allocation Patterns
|
|
24
|
+
|
|
25
|
+
```rust
|
|
26
|
+
// Preallocate collections when size is known
|
|
27
|
+
let mut results = Vec::with_capacity(items.len());
|
|
28
|
+
for item in items {
|
|
29
|
+
results.push(process(item));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Use iterators — they often avoid allocation entirely
|
|
33
|
+
let sum: i64 = items.iter()
|
|
34
|
+
.filter(|x| x.is_valid())
|
|
35
|
+
.map(|x| x.value())
|
|
36
|
+
.sum();
|
|
37
|
+
|
|
38
|
+
// Avoid unnecessary clones
|
|
39
|
+
fn process(data: &str) -> Result<Output> { ... } // Borrow when possible
|
|
40
|
+
fn consume(data: String) -> Result<Output> { ... } // Take ownership when needed
|
|
41
|
+
|
|
42
|
+
// Cow for conditional ownership
|
|
43
|
+
use std::borrow::Cow;
|
|
44
|
+
fn normalize(input: &str) -> Cow<'_, str> {
|
|
45
|
+
if input.contains('\t') {
|
|
46
|
+
Cow::Owned(input.replace('\t', " "))
|
|
47
|
+
} else {
|
|
48
|
+
Cow::Borrowed(input) // Zero allocation when no tabs
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// SmallVec for small, stack-allocated collections
|
|
53
|
+
use smallvec::SmallVec;
|
|
54
|
+
let mut tags: SmallVec<[Tag; 4]> = SmallVec::new();
|
|
55
|
+
// Up to 4 tags on the stack, spills to heap only if exceeded
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### String Performance
|
|
59
|
+
|
|
60
|
+
```rust
|
|
61
|
+
// String concatenation: use a builder pattern
|
|
62
|
+
use std::fmt::Write;
|
|
63
|
+
let mut output = String::with_capacity(estimated_size);
|
|
64
|
+
for item in items {
|
|
65
|
+
write!(output, "{}: {}\n", item.key, item.value).unwrap();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// For byte-level work, operate on &[u8] instead of &str
|
|
69
|
+
// Converting to str requires UTF-8 validation — skip it when you can
|
|
70
|
+
|
|
71
|
+
// Interning for repeated strings
|
|
72
|
+
// Use string interning crates (lasso, string_cache) for heavy deduplication
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Iterator Optimization
|
|
76
|
+
|
|
77
|
+
```rust
|
|
78
|
+
// Iterators compile to the same code as manual loops — use them freely
|
|
79
|
+
|
|
80
|
+
// Collect into specific types
|
|
81
|
+
let map: HashMap<_, _> = pairs.into_iter().collect();
|
|
82
|
+
let set: HashSet<_> = items.into_iter().collect();
|
|
83
|
+
|
|
84
|
+
// Avoid collect() when you don't need a collection
|
|
85
|
+
// Bad: allocates a Vec just to iterate it
|
|
86
|
+
let filtered: Vec<_> = items.iter().filter(|x| x.active).collect();
|
|
87
|
+
for item in &filtered { ... }
|
|
88
|
+
|
|
89
|
+
// Good: iterate directly
|
|
90
|
+
for item in items.iter().filter(|x| x.active) { ... }
|
|
91
|
+
|
|
92
|
+
// chunks/windows for batch processing
|
|
93
|
+
for chunk in data.chunks(1024) {
|
|
94
|
+
process_batch(chunk)?;
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Data Layout
|
|
99
|
+
|
|
100
|
+
```rust
|
|
101
|
+
// Field ordering affects struct size due to padding
|
|
102
|
+
// Bad: 24 bytes (with padding)
|
|
103
|
+
struct Padded {
|
|
104
|
+
a: u8, // 1 byte + 7 padding
|
|
105
|
+
b: u64, // 8 bytes
|
|
106
|
+
c: u8, // 1 byte + 7 padding
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Good: 16 bytes (reordered to minimize padding)
|
|
110
|
+
struct Compact {
|
|
111
|
+
b: u64, // 8 bytes
|
|
112
|
+
a: u8, // 1 byte
|
|
113
|
+
c: u8, // 1 byte + 6 padding
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// repr(C) for FFI — disables Rust's field reordering
|
|
117
|
+
#[repr(C)]
|
|
118
|
+
struct FfiStruct { ... }
|
|
119
|
+
|
|
120
|
+
// The compiler may reorder fields automatically in default repr(Rust),
|
|
121
|
+
// but being explicit about layout helps readability and FFI correctness
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Unsafe
|
|
125
|
+
|
|
126
|
+
### The Contract
|
|
127
|
+
|
|
128
|
+
Every `unsafe` block is a proof obligation. You are telling the compiler: "I have verified that the safety invariants hold here, and I accept responsibility."
|
|
129
|
+
|
|
130
|
+
```rust
|
|
131
|
+
// ALWAYS document the safety invariant
|
|
132
|
+
// SAFETY: We've verified that `index` is within bounds via the
|
|
133
|
+
// length check on line 42, and the slice is valid for the lifetime
|
|
134
|
+
// of this function.
|
|
135
|
+
unsafe {
|
|
136
|
+
*ptr.add(index) = value;
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Valid Uses of Unsafe
|
|
141
|
+
|
|
142
|
+
```rust
|
|
143
|
+
// 1. Calling unsafe functions (FFI)
|
|
144
|
+
extern "C" {
|
|
145
|
+
fn external_function(ptr: *const u8, len: usize) -> i32;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
pub fn safe_wrapper(data: &[u8]) -> Result<i32> {
|
|
149
|
+
// SAFETY: data.as_ptr() is valid for data.len() bytes,
|
|
150
|
+
// and the external function only reads from the pointer.
|
|
151
|
+
let result = unsafe { external_function(data.as_ptr(), data.len()) };
|
|
152
|
+
if result < 0 {
|
|
153
|
+
Err(Error::ExternalFailure(result))
|
|
154
|
+
} else {
|
|
155
|
+
Ok(result)
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// 2. Implementing unsafe traits
|
|
160
|
+
// SAFETY: MyType is Send because its internal raw pointer
|
|
161
|
+
// is only accessed from the thread that owns MyType.
|
|
162
|
+
// The pointer is never shared or aliased.
|
|
163
|
+
unsafe impl Send for MyType {}
|
|
164
|
+
|
|
165
|
+
// 3. Accessing mutable statics
|
|
166
|
+
static mut COUNTER: u64 = 0;
|
|
167
|
+
// SAFETY: This is only called from a single thread during initialization.
|
|
168
|
+
unsafe { COUNTER += 1; }
|
|
169
|
+
// Prefer AtomicU64 or OnceLock instead — this is almost always avoidable.
|
|
170
|
+
|
|
171
|
+
// 4. Unchecked operations for performance (after profiling proves it matters)
|
|
172
|
+
// SAFETY: We've validated that all bytes in `data` are valid UTF-8
|
|
173
|
+
// in the validation pass on line 30.
|
|
174
|
+
let s = unsafe { std::str::from_utf8_unchecked(data) };
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### Minimizing Unsafe Surface
|
|
178
|
+
|
|
179
|
+
```rust
|
|
180
|
+
// Wrap unsafe in safe abstractions with narrow interfaces
|
|
181
|
+
pub struct AlignedBuffer {
|
|
182
|
+
ptr: *mut u8,
|
|
183
|
+
len: usize,
|
|
184
|
+
cap: usize,
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
impl AlignedBuffer {
|
|
188
|
+
pub fn new(capacity: usize, alignment: usize) -> Self {
|
|
189
|
+
let layout = Layout::from_size_align(capacity, alignment).unwrap();
|
|
190
|
+
// SAFETY: layout is valid (non-zero size, power-of-two alignment)
|
|
191
|
+
let ptr = unsafe { alloc::alloc(layout) };
|
|
192
|
+
if ptr.is_null() {
|
|
193
|
+
alloc::handle_alloc_error(layout);
|
|
194
|
+
}
|
|
195
|
+
Self { ptr, len: 0, cap: capacity }
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Public API is fully safe — unsafe is encapsulated
|
|
199
|
+
pub fn push(&mut self, byte: u8) {
|
|
200
|
+
assert!(self.len < self.cap, "buffer full");
|
|
201
|
+
// SAFETY: We've verified len < cap, so ptr.add(len) is within allocation
|
|
202
|
+
unsafe { self.ptr.add(self.len).write(byte); }
|
|
203
|
+
self.len += 1;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
pub fn as_slice(&self) -> &[u8] {
|
|
207
|
+
// SAFETY: ptr is valid for len bytes, all initialized by push()
|
|
208
|
+
unsafe { std::slice::from_raw_parts(self.ptr, self.len) }
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
impl Drop for AlignedBuffer {
|
|
213
|
+
fn drop(&mut self) {
|
|
214
|
+
let layout = Layout::from_size_align(self.cap, /* alignment */).unwrap();
|
|
215
|
+
// SAFETY: ptr was allocated with this layout in new()
|
|
216
|
+
unsafe { alloc::dealloc(self.ptr, layout); }
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### Miri
|
|
222
|
+
|
|
223
|
+
```bash
|
|
224
|
+
# Miri detects undefined behavior in unsafe code
|
|
225
|
+
cargo +nightly miri test
|
|
226
|
+
|
|
227
|
+
# Miri catches:
|
|
228
|
+
# - Out-of-bounds memory access
|
|
229
|
+
# - Use-after-free
|
|
230
|
+
# - Invalid use of uninitialized data
|
|
231
|
+
# - Violations of aliasing rules (Stacked Borrows)
|
|
232
|
+
# - Data races
|
|
233
|
+
|
|
234
|
+
# Run Miri in CI for any crate with unsafe code
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
## Anti-Patterns
|
|
238
|
+
|
|
239
|
+
```rust
|
|
240
|
+
// Never: unsafe to "shut up the borrow checker"
|
|
241
|
+
// If the borrow checker rejects it, there's a reason. Redesign.
|
|
242
|
+
|
|
243
|
+
// Never: unsafe without a SAFETY comment
|
|
244
|
+
unsafe { ptr::write(dst, src) } // WHY is this safe? Document it.
|
|
245
|
+
|
|
246
|
+
// Never: transmute as a first resort
|
|
247
|
+
let x: u32 = unsafe { std::mem::transmute(my_float) };
|
|
248
|
+
// Use to_bits() / from_bits() instead — safe, clear, correct
|
|
249
|
+
|
|
250
|
+
// Never: Assuming layout without repr(C)
|
|
251
|
+
// Rust's default repr can reorder fields — don't assume memory layout
|
|
252
|
+
|
|
253
|
+
// Never: Dereferencing raw pointers without proving validity
|
|
254
|
+
// A raw pointer might be null, dangling, or misaligned.
|
|
255
|
+
// Prove all three are impossible before dereferencing.
|
|
256
|
+
```
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
# Rust Testing
|
|
2
|
+
|
|
3
|
+
Rust has testing built into the language and toolchain. `cargo test` runs unit tests, integration tests, and doc tests in one command.
|
|
4
|
+
|
|
5
|
+
## Unit Tests
|
|
6
|
+
|
|
7
|
+
### Module-Level Tests
|
|
8
|
+
|
|
9
|
+
```rust
|
|
10
|
+
// Tests live in the same file as the code they test
|
|
11
|
+
pub fn celsius_to_fahrenheit(c: f64) -> f64 {
|
|
12
|
+
c * 9.0 / 5.0 + 32.0
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
#[cfg(test)]
|
|
16
|
+
mod tests {
|
|
17
|
+
use super::*;
|
|
18
|
+
|
|
19
|
+
#[test]
|
|
20
|
+
fn freezing_point() {
|
|
21
|
+
assert!((celsius_to_fahrenheit(0.0) - 32.0).abs() < f64::EPSILON);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
#[test]
|
|
25
|
+
fn boiling_point() {
|
|
26
|
+
assert!((celsius_to_fahrenheit(100.0) - 212.0).abs() < f64::EPSILON);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
#[test]
|
|
30
|
+
fn negative_temperature() {
|
|
31
|
+
assert!((celsius_to_fahrenheit(-40.0) - -40.0).abs() < f64::EPSILON);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Testing Error Cases
|
|
37
|
+
|
|
38
|
+
```rust
|
|
39
|
+
#[test]
|
|
40
|
+
fn parse_invalid_input_returns_error() {
|
|
41
|
+
let result = parse_config("not valid toml");
|
|
42
|
+
assert!(result.is_err());
|
|
43
|
+
|
|
44
|
+
let err = result.unwrap_err();
|
|
45
|
+
assert!(err.to_string().contains("expected"));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
#[test]
|
|
49
|
+
fn empty_name_fails_validation() {
|
|
50
|
+
let input = CreateUserInput { name: "".into(), email: "a@b.com".into() };
|
|
51
|
+
let result = validate_user(&input);
|
|
52
|
+
|
|
53
|
+
assert!(matches!(result, Err(ValidationError::EmptyField { field }) if field == "name"));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Testing that something panics
|
|
57
|
+
#[test]
|
|
58
|
+
#[should_panic(expected = "index out of bounds")]
|
|
59
|
+
fn panics_on_out_of_bounds() {
|
|
60
|
+
let v = vec![1, 2, 3];
|
|
61
|
+
let _ = v[10];
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Test Organization
|
|
66
|
+
|
|
67
|
+
```rust
|
|
68
|
+
// #[cfg(test)] ensures test code is never compiled into release builds
|
|
69
|
+
#[cfg(test)]
|
|
70
|
+
mod tests {
|
|
71
|
+
use super::*;
|
|
72
|
+
|
|
73
|
+
// Shared test fixtures
|
|
74
|
+
fn sample_user() -> User {
|
|
75
|
+
User {
|
|
76
|
+
id: UserId::new("test-123"),
|
|
77
|
+
name: "Alice".into(),
|
|
78
|
+
email: "alice@example.com".into(),
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Group related tests with nested modules
|
|
83
|
+
mod validation {
|
|
84
|
+
use super::*;
|
|
85
|
+
|
|
86
|
+
#[test]
|
|
87
|
+
fn rejects_empty_name() { ... }
|
|
88
|
+
|
|
89
|
+
#[test]
|
|
90
|
+
fn rejects_invalid_email() { ... }
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
mod serialization {
|
|
94
|
+
use super::*;
|
|
95
|
+
|
|
96
|
+
#[test]
|
|
97
|
+
fn round_trips_through_json() { ... }
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Integration Tests
|
|
103
|
+
|
|
104
|
+
```rust
|
|
105
|
+
// tests/api_test.rs — separate compilation unit, tests public API only
|
|
106
|
+
use my_crate::{Config, Server};
|
|
107
|
+
|
|
108
|
+
#[test]
|
|
109
|
+
fn server_responds_to_health_check() {
|
|
110
|
+
let config = Config::default();
|
|
111
|
+
let server = Server::new(config);
|
|
112
|
+
|
|
113
|
+
let response = server.handle_request("/healthz");
|
|
114
|
+
|
|
115
|
+
assert_eq!(response.status(), 200);
|
|
116
|
+
assert_eq!(response.body(), r#"{"status":"ok"}"#);
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Async Tests
|
|
121
|
+
|
|
122
|
+
```rust
|
|
123
|
+
#[tokio::test]
|
|
124
|
+
async fn fetches_user_by_id() {
|
|
125
|
+
let db = setup_test_db().await;
|
|
126
|
+
let repo = UserRepo::new(db);
|
|
127
|
+
|
|
128
|
+
let user = repo.find_by_id("123").await.unwrap();
|
|
129
|
+
|
|
130
|
+
assert_eq!(user.name, "Alice");
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// With custom runtime config
|
|
134
|
+
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
135
|
+
async fn concurrent_operations() {
|
|
136
|
+
// ...
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Property-Based Testing
|
|
141
|
+
|
|
142
|
+
```rust
|
|
143
|
+
use proptest::prelude::*;
|
|
144
|
+
|
|
145
|
+
proptest! {
|
|
146
|
+
#[test]
|
|
147
|
+
fn parse_never_panics(s in "\\PC*") {
|
|
148
|
+
// Whatever string we throw at parse, it must not panic
|
|
149
|
+
let _ = parse_input(&s);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
#[test]
|
|
153
|
+
fn serialization_round_trips(
|
|
154
|
+
name in "[a-zA-Z]{1,50}",
|
|
155
|
+
age in 0u32..150,
|
|
156
|
+
) {
|
|
157
|
+
let user = User { name, age };
|
|
158
|
+
let json = serde_json::to_string(&user).unwrap();
|
|
159
|
+
let decoded: User = serde_json::from_str(&json).unwrap();
|
|
160
|
+
assert_eq!(user, decoded);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## Doc Tests
|
|
166
|
+
|
|
167
|
+
```rust
|
|
168
|
+
/// Adds two numbers together.
|
|
169
|
+
///
|
|
170
|
+
/// # Examples
|
|
171
|
+
///
|
|
172
|
+
/// ```
|
|
173
|
+
/// use my_crate::add;
|
|
174
|
+
///
|
|
175
|
+
/// assert_eq!(add(2, 3), 5);
|
|
176
|
+
/// assert_eq!(add(-1, 1), 0);
|
|
177
|
+
/// ```
|
|
178
|
+
///
|
|
179
|
+
/// # Panics
|
|
180
|
+
///
|
|
181
|
+
/// Panics if the result overflows.
|
|
182
|
+
///
|
|
183
|
+
/// ```should_panic
|
|
184
|
+
/// use my_crate::add;
|
|
185
|
+
///
|
|
186
|
+
/// add(i64::MAX, 1); // Overflow!
|
|
187
|
+
/// ```
|
|
188
|
+
pub fn add(a: i64, b: i64) -> i64 {
|
|
189
|
+
a.checked_add(b).expect("overflow")
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Doc tests are compiled and run — they're both documentation AND tests.
|
|
193
|
+
// They verify your examples actually work.
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## Mocking and Test Doubles
|
|
197
|
+
|
|
198
|
+
```rust
|
|
199
|
+
// Prefer trait-based dependency injection over mocking frameworks
|
|
200
|
+
|
|
201
|
+
// Define trait at the consumer
|
|
202
|
+
trait Clock {
|
|
203
|
+
fn now(&self) -> DateTime<Utc>;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Production implementation
|
|
207
|
+
struct SystemClock;
|
|
208
|
+
impl Clock for SystemClock {
|
|
209
|
+
fn now(&self) -> DateTime<Utc> { Utc::now() }
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Test implementation — deterministic
|
|
213
|
+
struct FakeClock(DateTime<Utc>);
|
|
214
|
+
impl Clock for FakeClock {
|
|
215
|
+
fn now(&self) -> DateTime<Utc> { self.0 }
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
#[test]
|
|
219
|
+
fn token_expires_after_one_hour() {
|
|
220
|
+
let fixed_time = Utc.with_ymd_and_hms(2025, 1, 1, 12, 0, 0).unwrap();
|
|
221
|
+
let clock = FakeClock(fixed_time);
|
|
222
|
+
let token = generate_token(&clock);
|
|
223
|
+
|
|
224
|
+
let check_time = fixed_time + Duration::hours(2);
|
|
225
|
+
assert!(token.is_expired_at(check_time));
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// When you do need a mocking library: mockall
|
|
229
|
+
#[automock]
|
|
230
|
+
trait Database {
|
|
231
|
+
fn get(&self, key: &str) -> Option<String>;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
#[test]
|
|
235
|
+
fn returns_cached_value() {
|
|
236
|
+
let mut mock = MockDatabase::new();
|
|
237
|
+
mock.expect_get()
|
|
238
|
+
.with(eq("user:123"))
|
|
239
|
+
.returning(|_| Some("Alice".into()));
|
|
240
|
+
|
|
241
|
+
let service = Service::new(mock);
|
|
242
|
+
assert_eq!(service.get_user("123"), Some("Alice".into()));
|
|
243
|
+
}
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
## Benchmarks
|
|
247
|
+
|
|
248
|
+
```rust
|
|
249
|
+
// Using criterion (de facto standard)
|
|
250
|
+
// benches/my_benchmark.rs
|
|
251
|
+
use criterion::{criterion_group, criterion_main, Criterion, black_box};
|
|
252
|
+
|
|
253
|
+
fn bench_parse(c: &mut Criterion) {
|
|
254
|
+
let input = include_str!("../testdata/large_input.txt");
|
|
255
|
+
|
|
256
|
+
c.bench_function("parse_large_input", |b| {
|
|
257
|
+
b.iter(|| parse(black_box(input)))
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
fn bench_comparison(c: &mut Criterion) {
|
|
262
|
+
let mut group = c.benchmark_group("serialization");
|
|
263
|
+
let data = generate_test_data();
|
|
264
|
+
|
|
265
|
+
group.bench_function("json", |b| {
|
|
266
|
+
b.iter(|| serde_json::to_vec(black_box(&data)))
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
group.bench_function("bincode", |b| {
|
|
270
|
+
b.iter(|| bincode::serialize(black_box(&data)))
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
group.finish();
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
criterion_group!(benches, bench_parse, bench_comparison);
|
|
277
|
+
criterion_main!(benches);
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
## Test Anti-Patterns
|
|
281
|
+
|
|
282
|
+
```rust
|
|
283
|
+
// Never: Tests that depend on execution order
|
|
284
|
+
// Each test runs in its own thread — no shared mutable state
|
|
285
|
+
|
|
286
|
+
// Never: Ignoring tests instead of fixing them
|
|
287
|
+
#[ignore] // Why? For how long? File an issue.
|
|
288
|
+
|
|
289
|
+
// Never: Testing private internals through hacks
|
|
290
|
+
// If you can't test it through the public API, the design needs work
|
|
291
|
+
|
|
292
|
+
// Never: Assertions without messages in complex tests
|
|
293
|
+
assert!(result.is_ok()); // What was the input? What was the error?
|
|
294
|
+
// Better:
|
|
295
|
+
assert!(result.is_ok(), "expected Ok for input {input:?}, got {result:?}");
|
|
296
|
+
|
|
297
|
+
// Never: Flaky tests
|
|
298
|
+
// Flaky = broken. Fix the race condition or the timing dependency.
|
|
299
|
+
// Use deterministic clocks, seeded RNGs, and proper synchronization.
|
|
300
|
+
```
|