@coralai/sps-cli 0.42.0 → 0.44.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/README.md +59 -4
- package/dist/commands/consoleCommand.d.ts +2 -0
- package/dist/commands/consoleCommand.d.ts.map +1 -0
- package/dist/commands/consoleCommand.js +129 -0
- package/dist/commands/consoleCommand.js.map +1 -0
- package/dist/commands/projectInit.d.ts.map +1 -1
- package/dist/commands/projectInit.js +40 -53
- package/dist/commands/projectInit.js.map +1 -1
- package/dist/commands/setup.d.ts.map +1 -1
- package/dist/commands/setup.js +14 -2
- package/dist/commands/setup.js.map +1 -1
- package/dist/commands/skillCommand.d.ts +2 -0
- package/dist/commands/skillCommand.d.ts.map +1 -0
- package/dist/commands/skillCommand.js +235 -0
- package/dist/commands/skillCommand.js.map +1 -0
- package/dist/console-assets/assets/index-Bhd2f9AP.js +125 -0
- package/dist/console-assets/assets/index-bsAN2a12.css +1 -0
- package/dist/console-assets/index.html +16 -0
- package/dist/console-server/index.d.ts +29 -0
- package/dist/console-server/index.d.ts.map +1 -0
- package/dist/console-server/index.js +145 -0
- package/dist/console-server/index.js.map +1 -0
- package/dist/console-server/lib/lockFile.d.ts +17 -0
- package/dist/console-server/lib/lockFile.d.ts.map +1 -0
- package/dist/console-server/lib/lockFile.js +61 -0
- package/dist/console-server/lib/lockFile.js.map +1 -0
- package/dist/console-server/lib/portPicker.d.ts +3 -0
- package/dist/console-server/lib/portPicker.d.ts.map +1 -0
- package/dist/console-server/lib/portPicker.js +25 -0
- package/dist/console-server/lib/portPicker.js.map +1 -0
- package/dist/console-server/routes/projects.d.ts +11 -0
- package/dist/console-server/routes/projects.d.ts.map +1 -0
- package/dist/console-server/routes/projects.js +149 -0
- package/dist/console-server/routes/projects.js.map +1 -0
- package/dist/console-server/routes/system.d.ts +7 -0
- package/dist/console-server/routes/system.d.ts.map +1 -0
- package/dist/console-server/routes/system.js +19 -0
- package/dist/console-server/routes/system.js.map +1 -0
- package/dist/console-server/sse/eventBus.d.ts +25 -0
- package/dist/console-server/sse/eventBus.d.ts.map +1 -0
- package/dist/console-server/sse/eventBus.js +32 -0
- package/dist/console-server/sse/eventBus.js.map +1 -0
- package/dist/console-server/watchers/cardWatcher.d.ts +9 -0
- package/dist/console-server/watchers/cardWatcher.d.ts.map +1 -0
- package/dist/console-server/watchers/cardWatcher.js +42 -0
- package/dist/console-server/watchers/cardWatcher.js.map +1 -0
- package/dist/core/skillStore.d.ts +46 -0
- package/dist/core/skillStore.d.ts.map +1 -0
- package/dist/core/skillStore.js +210 -0
- package/dist/core/skillStore.js.map +1 -0
- package/dist/core/skillStore.test.d.ts +2 -0
- package/dist/core/skillStore.test.d.ts.map +1 -0
- package/dist/core/skillStore.test.js +203 -0
- package/dist/core/skillStore.test.js.map +1 -0
- package/dist/main.js +27 -17
- package/dist/main.js.map +1 -1
- package/package.json +8 -2
- package/skills/architecture-decision-records/SKILL.md +207 -0
- package/skills/backend/SKILL.md +62 -0
- package/skills/backend/references/api-design.md +168 -0
- package/skills/backend/references/caching.md +181 -0
- package/skills/backend/references/data-access.md +173 -0
- package/skills/backend/references/layering.md +181 -0
- package/skills/backend/references/observability.md +190 -0
- package/skills/backend/references/resilience.md +201 -0
- package/skills/backend/references/security.md +186 -0
- package/skills/backend-architect/SKILL.md +119 -0
- package/skills/code-reviewer/SKILL.md +143 -0
- package/skills/coding-standards/SKILL.md +60 -0
- package/skills/coding-standards/references/clean-code.md +258 -0
- package/skills/coding-standards/references/code-review.md +192 -0
- package/skills/coding-standards/references/commits-and-prs.md +226 -0
- package/skills/coding-standards/references/error-strategy.md +193 -0
- package/skills/coding-standards/references/naming.md +185 -0
- package/skills/coding-standards/references/tdd.md +171 -0
- package/skills/database/SKILL.md +53 -0
- package/skills/database/references/indexing.md +190 -0
- package/skills/database/references/migrations.md +199 -0
- package/skills/database/references/nosql.md +185 -0
- package/skills/database/references/queries.md +295 -0
- package/skills/database/references/scaling.md +203 -0
- package/skills/database/references/schema.md +191 -0
- package/skills/database-optimizer/SKILL.md +168 -0
- package/skills/debugging-workflow/SKILL.md +244 -0
- package/skills/devops/SKILL.md +55 -0
- package/skills/devops/references/ci-cd.md +204 -0
- package/skills/devops/references/containers.md +272 -0
- package/skills/devops/references/deploy.md +201 -0
- package/skills/devops/references/iac.md +252 -0
- package/skills/devops/references/observability.md +228 -0
- package/skills/devops/references/secrets.md +178 -0
- package/skills/devops-automator/SKILL.md +164 -0
- package/skills/frontend/SKILL.md +52 -0
- package/skills/frontend/references/accessibility.md +222 -0
- package/skills/frontend/references/components.md +206 -0
- package/skills/frontend/references/performance.md +219 -0
- package/skills/frontend/references/routing.md +209 -0
- package/skills/frontend/references/state.md +190 -0
- package/skills/frontend/references/testing.md +216 -0
- package/skills/frontend-developer/SKILL.md +115 -0
- package/skills/git-workflow/SKILL.md +355 -0
- package/skills/golang/SKILL.md +49 -0
- package/skills/golang/references/concurrency.md +284 -0
- package/skills/golang/references/errors.md +241 -0
- package/skills/golang/references/idioms.md +285 -0
- package/skills/golang/references/testing.md +238 -0
- package/skills/java/SKILL.md +50 -0
- package/skills/java/references/concurrency.md +194 -0
- package/skills/java/references/idioms.md +283 -0
- package/skills/java/references/testing.md +228 -0
- package/skills/kotlin/SKILL.md +47 -0
- package/skills/kotlin/references/coroutines.md +240 -0
- package/skills/kotlin/references/idioms.md +268 -0
- package/skills/kotlin/references/testing.md +219 -0
- package/skills/mobile/SKILL.md +50 -0
- package/skills/mobile/references/architecture.md +204 -0
- package/skills/mobile/references/navigation.md +158 -0
- package/skills/mobile/references/performance.md +152 -0
- package/skills/mobile/references/platform.md +166 -0
- package/skills/mobile/references/state-and-data.md +174 -0
- package/skills/python/SKILL.md +51 -0
- package/skills/python/THIRD_PARTY.md +14 -0
- package/skills/python/references/async.md +218 -0
- package/skills/python/references/error-handling.md +254 -0
- package/skills/python/references/idioms.md +279 -0
- package/skills/python/references/packaging.md +233 -0
- package/skills/python/references/testing.md +269 -0
- package/skills/python/references/typing.md +292 -0
- package/skills/qa-tester/SKILL.md +186 -0
- package/skills/rust/SKILL.md +50 -0
- package/skills/rust/references/async.md +224 -0
- package/skills/rust/references/errors.md +240 -0
- package/skills/rust/references/ownership.md +263 -0
- package/skills/rust/references/testing.md +274 -0
- package/skills/rust/references/traits.md +250 -0
- package/skills/security-engineer/SKILL.md +157 -0
- package/skills/swift/SKILL.md +48 -0
- package/skills/swift/references/concurrency.md +280 -0
- package/skills/swift/references/idioms.md +334 -0
- package/skills/swift/references/testing.md +229 -0
- package/skills/typescript/SKILL.md +51 -0
- package/skills/typescript/references/async.md +241 -0
- package/skills/typescript/references/errors.md +208 -0
- package/skills/typescript/references/idioms.md +246 -0
- package/skills/typescript/references/testing.md +225 -0
- package/skills/typescript/references/tooling.md +208 -0
- package/skills/typescript/references/types.md +259 -0
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
# Rust — Ownership
|
|
2
|
+
|
|
3
|
+
Borrowing, lifetimes, move vs. copy, smart pointers. The part the language is organized around.
|
|
4
|
+
|
|
5
|
+
## The rules
|
|
6
|
+
|
|
7
|
+
1. Each value has one owner.
|
|
8
|
+
2. When the owner goes out of scope, the value is dropped.
|
|
9
|
+
3. You can borrow immutably (any number of `&T`) OR mutably (exactly one `&mut T`), never both at once.
|
|
10
|
+
|
|
11
|
+
Everything else is consequences of these.
|
|
12
|
+
|
|
13
|
+
## Move vs. copy
|
|
14
|
+
|
|
15
|
+
```rust
|
|
16
|
+
let s = String::from("hi");
|
|
17
|
+
let t = s; // move: s is no longer valid
|
|
18
|
+
// println!("{s}") // ❌ error
|
|
19
|
+
|
|
20
|
+
let n = 5;
|
|
21
|
+
let m = n; // copy: n is still valid (i32 is Copy)
|
|
22
|
+
println!("{n} {m}"); // ✅
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
`Copy` types (primitives, `&T`, arrays of `Copy`) are copied implicitly. Everything else is moved. Add `#[derive(Clone, Copy)]` to your own small value types.
|
|
26
|
+
|
|
27
|
+
## Borrowing
|
|
28
|
+
|
|
29
|
+
Prefer borrows over ownership in parameters. The caller chooses.
|
|
30
|
+
|
|
31
|
+
```rust
|
|
32
|
+
fn length(s: &str) -> usize { s.len() }
|
|
33
|
+
|
|
34
|
+
let owned = String::from("hi");
|
|
35
|
+
length(&owned); // caller keeps the String
|
|
36
|
+
length("hello"); // works with a &'static str too
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Rule: **accept `&str` / `&[T]` / `&T`, not `String` / `Vec<T>` / `T`**, unless you need ownership (e.g., storing it).
|
|
40
|
+
|
|
41
|
+
## The borrow checker — common errors and fixes
|
|
42
|
+
|
|
43
|
+
### "cannot borrow X as mutable because it is also borrowed as immutable"
|
|
44
|
+
|
|
45
|
+
```rust
|
|
46
|
+
// ❌
|
|
47
|
+
let mut v = vec![1, 2, 3];
|
|
48
|
+
let first = &v[0]; // immutable borrow
|
|
49
|
+
v.push(4); // mutable borrow while &first alive
|
|
50
|
+
println!("{first}"); // error
|
|
51
|
+
|
|
52
|
+
// ✅ shorten the immutable borrow
|
|
53
|
+
let first = v[0]; // copy out (if Copy)
|
|
54
|
+
v.push(4);
|
|
55
|
+
println!("{first}");
|
|
56
|
+
|
|
57
|
+
// ✅ scope it
|
|
58
|
+
{
|
|
59
|
+
let first = &v[0];
|
|
60
|
+
println!("{first}");
|
|
61
|
+
} // borrow ends here
|
|
62
|
+
v.push(4);
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### "use of moved value"
|
|
66
|
+
|
|
67
|
+
```rust
|
|
68
|
+
// ❌
|
|
69
|
+
let s = String::from("hi");
|
|
70
|
+
takes(s);
|
|
71
|
+
println!("{s}"); // already moved
|
|
72
|
+
|
|
73
|
+
// ✅ pass by reference
|
|
74
|
+
takes(&s);
|
|
75
|
+
|
|
76
|
+
// ✅ clone if you really need two owners
|
|
77
|
+
takes(s.clone());
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Lifetimes — usually inferred
|
|
81
|
+
|
|
82
|
+
Rust figures out most lifetimes. You only annotate when the compiler can't:
|
|
83
|
+
|
|
84
|
+
```rust
|
|
85
|
+
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
|
|
86
|
+
if x.len() > y.len() { x } else { y }
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
The `'a` says "the returned reference is valid for the shorter of x's and y's lifetimes".
|
|
91
|
+
|
|
92
|
+
In structs that hold references, you must annotate:
|
|
93
|
+
|
|
94
|
+
```rust
|
|
95
|
+
struct Parser<'src> {
|
|
96
|
+
input: &'src str,
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Rule of thumb: if you're fighting lifetimes, you might want owned data instead of references. Sometimes a `String` is fine.
|
|
101
|
+
|
|
102
|
+
## `'static` lifetime
|
|
103
|
+
|
|
104
|
+
`'static` means "lives for the whole program". Comes up with:
|
|
105
|
+
- String literals (`&'static str`)
|
|
106
|
+
- Bounded trait objects in async / threading (`Send + Sync + 'static`)
|
|
107
|
+
- Global `Mutex<T>` via `OnceLock` / `LazyLock`
|
|
108
|
+
|
|
109
|
+
A function bound `T: 'static` does NOT mean "must be owned forever" — it means "contains no non-static references". An owned `String` is `'static` because it borrows nothing.
|
|
110
|
+
|
|
111
|
+
## Smart pointers
|
|
112
|
+
|
|
113
|
+
| Type | Use |
|
|
114
|
+
|---|---|
|
|
115
|
+
| `Box<T>` | Heap allocate; needed for recursive types, trait objects |
|
|
116
|
+
| `Rc<T>` | Shared ownership, single-threaded; refcount |
|
|
117
|
+
| `Arc<T>` | Shared ownership, thread-safe |
|
|
118
|
+
| `RefCell<T>` | Interior mutability, single-threaded, checked at runtime |
|
|
119
|
+
| `Mutex<T>` / `RwLock<T>` | Interior mutability, thread-safe |
|
|
120
|
+
| `Cow<'a, T>` | Borrow OR own; copy-on-write |
|
|
121
|
+
|
|
122
|
+
Rule: **start with `&T` / `&mut T`. Reach for smart pointers only when simple borrowing can't model what you need.**
|
|
123
|
+
|
|
124
|
+
## Interior mutability
|
|
125
|
+
|
|
126
|
+
When a value is shared (`&T`) but a method needs to mutate it internally (caches, ref counts), use `RefCell` (single-threaded) or `Mutex` (threaded).
|
|
127
|
+
|
|
128
|
+
```rust
|
|
129
|
+
use std::cell::RefCell;
|
|
130
|
+
|
|
131
|
+
struct Cache {
|
|
132
|
+
map: RefCell<HashMap<String, String>>,
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
impl Cache {
|
|
136
|
+
fn get(&self, k: &str) -> Option<String> {
|
|
137
|
+
self.map.borrow().get(k).cloned()
|
|
138
|
+
}
|
|
139
|
+
fn put(&self, k: String, v: String) {
|
|
140
|
+
self.map.borrow_mut().insert(k, v);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
`.borrow_mut()` while another `.borrow()` is live panics at runtime. It trades compile-time for runtime checks — a last resort.
|
|
146
|
+
|
|
147
|
+
## `Rc<RefCell<T>>` — the single-threaded shared-mutable
|
|
148
|
+
|
|
149
|
+
Legal, common in tree / graph data structures, single-threaded only.
|
|
150
|
+
|
|
151
|
+
```rust
|
|
152
|
+
use std::{cell::RefCell, rc::Rc};
|
|
153
|
+
|
|
154
|
+
type Node = Rc<RefCell<NodeData>>;
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
If two threads may touch it → `Arc<Mutex<T>>`. If you find yourself writing `Rc<RefCell<...>>` often, reconsider — sometimes ownership flows would be cleaner with a different data layout (`Vec<Node>` + indexes).
|
|
158
|
+
|
|
159
|
+
## `Cow` — borrow or own, at runtime
|
|
160
|
+
|
|
161
|
+
```rust
|
|
162
|
+
use std::borrow::Cow;
|
|
163
|
+
|
|
164
|
+
fn normalize(s: &str) -> Cow<'_, str> {
|
|
165
|
+
if s.chars().any(|c| c.is_uppercase()) {
|
|
166
|
+
Cow::Owned(s.to_lowercase())
|
|
167
|
+
} else {
|
|
168
|
+
Cow::Borrowed(s)
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Great for "usually don't need to clone, sometimes do" paths. Callers get a `&str` from `.as_ref()`.
|
|
174
|
+
|
|
175
|
+
## `Drop` — RAII
|
|
176
|
+
|
|
177
|
+
Values implement `Drop` to release resources (file handles, mutex guards, DB connections).
|
|
178
|
+
|
|
179
|
+
```rust
|
|
180
|
+
impl Drop for TempFile {
|
|
181
|
+
fn drop(&mut self) {
|
|
182
|
+
std::fs::remove_file(&self.path).ok();
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Runs when the value goes out of scope. Don't `mem::forget` something with a `Drop` unless you really know.
|
|
188
|
+
|
|
189
|
+
Guards (`MutexGuard`, `RefMut`) are `Drop`-based — they release the lock when dropped. Keep them scoped tightly:
|
|
190
|
+
|
|
191
|
+
```rust
|
|
192
|
+
{
|
|
193
|
+
let mut g = mu.lock().unwrap();
|
|
194
|
+
g.push(x);
|
|
195
|
+
} // lock released HERE
|
|
196
|
+
call_may_block(); // no lock held
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## `unsafe` — the escape hatch
|
|
200
|
+
|
|
201
|
+
Only where the borrow checker can't prove safety (FFI, raw pointers, some `Send`/`Sync` work). Every `unsafe` block needs a `// SAFETY:` comment stating the invariants you've manually verified.
|
|
202
|
+
|
|
203
|
+
```rust
|
|
204
|
+
// SAFETY: `ptr` is not null (checked above) and `len` is within the allocation.
|
|
205
|
+
let slice = unsafe { std::slice::from_raw_parts(ptr, len) };
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
If you can do it safely, do it safely. `unsafe` is not a performance knob.
|
|
209
|
+
|
|
210
|
+
## Common patterns
|
|
211
|
+
|
|
212
|
+
### Newtype for domain meaning
|
|
213
|
+
|
|
214
|
+
```rust
|
|
215
|
+
pub struct UserId(pub String);
|
|
216
|
+
pub struct Email(pub String);
|
|
217
|
+
|
|
218
|
+
fn find_user(id: UserId) -> Option<User> { ... }
|
|
219
|
+
|
|
220
|
+
// fn call takes UserId, not String → hard to mix with other string IDs
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### Builder
|
|
224
|
+
|
|
225
|
+
```rust
|
|
226
|
+
let req = Request::builder()
|
|
227
|
+
.url("https://x.com")
|
|
228
|
+
.timeout(Duration::from_secs(5))
|
|
229
|
+
.build()?;
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
For types with many optional fields or construction invariants.
|
|
233
|
+
|
|
234
|
+
### Typestate — encode state in the type
|
|
235
|
+
|
|
236
|
+
```rust
|
|
237
|
+
struct Draft;
|
|
238
|
+
struct Published;
|
|
239
|
+
|
|
240
|
+
struct Post<State> { body: String, _s: PhantomData<State> }
|
|
241
|
+
|
|
242
|
+
impl Post<Draft> {
|
|
243
|
+
fn publish(self) -> Post<Published> { ... }
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
impl Post<Published> {
|
|
247
|
+
fn url(&self) -> String { ... }
|
|
248
|
+
// no publish() here — you can't double-publish
|
|
249
|
+
}
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
## Anti-patterns
|
|
253
|
+
|
|
254
|
+
| Anti-pattern | Fix |
|
|
255
|
+
|---|---|
|
|
256
|
+
| `.clone()` reflex to silence the borrow checker | Understand what ownership you need; clones should be deliberate |
|
|
257
|
+
| `Rc<RefCell<T>>` everywhere | Revisit data layout; many problems want a `Vec<T>` with indexes |
|
|
258
|
+
| Accepting `String` / `Vec<T>` when `&str` / `&[T]` works | Over-constrains callers |
|
|
259
|
+
| Returning references from local functions ("dangling reference") | Return owned data, or rework the lifetime story |
|
|
260
|
+
| `mut` everywhere | Minimize mutation; Rust rewards immutable-by-default |
|
|
261
|
+
| Implementing `Clone` by default | Only for types callers will reasonably clone |
|
|
262
|
+
| Allocating in a hot loop (`format!`, `vec![...]`) | Preallocate; `write!` into a reused buffer |
|
|
263
|
+
| Leaking a `MutexGuard` into an async `.await` | Guard is not `Send`; use `tokio::sync::Mutex` or drop the guard before awaiting |
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
# Rust — Testing
|
|
2
|
+
|
|
3
|
+
`#[test]`, integration tests, property testing, snapshots. For general TDD, see `coding-standards/references/tdd.md`.
|
|
4
|
+
|
|
5
|
+
## Unit tests — alongside the code
|
|
6
|
+
|
|
7
|
+
```rust
|
|
8
|
+
// src/math.rs
|
|
9
|
+
pub fn add(a: i32, b: i32) -> i32 { a + b }
|
|
10
|
+
|
|
11
|
+
#[cfg(test)]
|
|
12
|
+
mod tests {
|
|
13
|
+
use super::*;
|
|
14
|
+
|
|
15
|
+
#[test]
|
|
16
|
+
fn adds_positives() {
|
|
17
|
+
assert_eq!(add(2, 3), 5);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
#[test]
|
|
21
|
+
#[should_panic(expected = "overflow")]
|
|
22
|
+
fn panics_on_overflow() {
|
|
23
|
+
add(i32::MAX, 1);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Run with `cargo test`. The `#[cfg(test)]` block is compiled only for tests — the test code doesn't ship in release builds.
|
|
29
|
+
|
|
30
|
+
## Integration tests — `tests/` directory
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
mycrate/
|
|
34
|
+
├── src/
|
|
35
|
+
│ └── lib.rs
|
|
36
|
+
└── tests/
|
|
37
|
+
└── api.rs # one file = one crate, black-box tests
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
```rust
|
|
41
|
+
// tests/api.rs
|
|
42
|
+
use mycrate::load;
|
|
43
|
+
|
|
44
|
+
#[test]
|
|
45
|
+
fn loads_from_file() {
|
|
46
|
+
let cfg = load("tests/fixtures/sample.yaml").unwrap();
|
|
47
|
+
assert_eq!(cfg.name, "sample");
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Integration tests use only the public API. If you need internal access, that's a unit test.
|
|
52
|
+
|
|
53
|
+
## Assertions
|
|
54
|
+
|
|
55
|
+
```rust
|
|
56
|
+
assert!(cond);
|
|
57
|
+
assert_eq!(a, b);
|
|
58
|
+
assert_ne!(a, b);
|
|
59
|
+
|
|
60
|
+
// With custom message
|
|
61
|
+
assert_eq!(got, want, "parsed value mismatch for input {input:?}");
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Failures show a nice diff. Stick with `assert_eq!` over manual `if got != want { panic! }`.
|
|
65
|
+
|
|
66
|
+
## Parameterized tests — table style
|
|
67
|
+
|
|
68
|
+
```rust
|
|
69
|
+
#[test]
|
|
70
|
+
fn add_cases() {
|
|
71
|
+
for (a, b, want) in [(1, 2, 3), (0, 0, 0), (-1, 1, 0)] {
|
|
72
|
+
assert_eq!(add(a, b), want, "add({a}, {b})");
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Or use `rstest` for per-case test names:
|
|
78
|
+
|
|
79
|
+
```rust
|
|
80
|
+
use rstest::rstest;
|
|
81
|
+
|
|
82
|
+
#[rstest]
|
|
83
|
+
#[case(1, 2, 3)]
|
|
84
|
+
#[case(0, 0, 0)]
|
|
85
|
+
#[case(-1, 1, 0)]
|
|
86
|
+
fn add(#[case] a: i32, #[case] b: i32, #[case] want: i32) {
|
|
87
|
+
assert_eq!(crate::add(a, b), want);
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
`cargo test add::case_1` runs one case.
|
|
92
|
+
|
|
93
|
+
## Async tests
|
|
94
|
+
|
|
95
|
+
With tokio:
|
|
96
|
+
|
|
97
|
+
```rust
|
|
98
|
+
#[tokio::test]
|
|
99
|
+
async fn fetches() {
|
|
100
|
+
let body = fetch("http://...").await.unwrap();
|
|
101
|
+
assert!(body.contains("hi"));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
|
|
105
|
+
async fn multi_thread() { ... }
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Other runtimes have their own macros (`#[async_std::test]`).
|
|
109
|
+
|
|
110
|
+
## Property tests — `proptest` / `quickcheck`
|
|
111
|
+
|
|
112
|
+
Generates random inputs; shrinks failing cases to a minimal counter-example.
|
|
113
|
+
|
|
114
|
+
```rust
|
|
115
|
+
use proptest::prelude::*;
|
|
116
|
+
|
|
117
|
+
proptest! {
|
|
118
|
+
#[test]
|
|
119
|
+
fn sort_is_idempotent(v in any::<Vec<i32>>()) {
|
|
120
|
+
let mut s = v.clone();
|
|
121
|
+
s.sort();
|
|
122
|
+
let mut s2 = s.clone();
|
|
123
|
+
s2.sort();
|
|
124
|
+
prop_assert_eq!(s, s2);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
#[test]
|
|
128
|
+
fn roundtrip(s in "\\PC*") { // arbitrary printable UTF-8
|
|
129
|
+
let encoded = encode(&s);
|
|
130
|
+
prop_assert_eq!(decode(&encoded), s);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Use for: parsers, serializers, invariant checks, anything with a large input space.
|
|
136
|
+
|
|
137
|
+
## Snapshot tests — `insta`
|
|
138
|
+
|
|
139
|
+
```rust
|
|
140
|
+
#[test]
|
|
141
|
+
fn render() {
|
|
142
|
+
insta::assert_snapshot!(render(&input));
|
|
143
|
+
insta::assert_yaml_snapshot!(serialize(&input));
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Run `cargo insta review` to approve / reject. Commit the snapshots under `src/snapshots/`. Review every diff deliberately.
|
|
148
|
+
|
|
149
|
+
Great for CLI output, parser ASTs, generated config. Weak for flaky fields (timestamps, UUIDs) — redact or inject deterministic values.
|
|
150
|
+
|
|
151
|
+
## Test doubles
|
|
152
|
+
|
|
153
|
+
Fakes > mocks in Rust too.
|
|
154
|
+
|
|
155
|
+
```rust
|
|
156
|
+
// Production
|
|
157
|
+
pub trait UserRepo {
|
|
158
|
+
fn find(&self, id: &str) -> Option<User>;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// In tests
|
|
162
|
+
struct FakeRepo { users: HashMap<String, User> }
|
|
163
|
+
|
|
164
|
+
impl UserRepo for FakeRepo {
|
|
165
|
+
fn find(&self, id: &str) -> Option<User> {
|
|
166
|
+
self.users.get(id).cloned()
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
For mocks, `mockall`:
|
|
172
|
+
|
|
173
|
+
```rust
|
|
174
|
+
use mockall::automock;
|
|
175
|
+
|
|
176
|
+
#[automock]
|
|
177
|
+
trait UserRepo {
|
|
178
|
+
fn find(&self, id: &str) -> Option<User>;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
let mut mock = MockUserRepo::new();
|
|
182
|
+
mock.expect_find()
|
|
183
|
+
.with(eq("u1"))
|
|
184
|
+
.returning(|_| Some(User::sample()));
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Reach for `mockall` when the trait has complex call expectations. Otherwise, fakes are clearer.
|
|
188
|
+
|
|
189
|
+
## Test filtering
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
cargo test # all tests
|
|
193
|
+
cargo test math # name contains 'math'
|
|
194
|
+
cargo test --lib # unit tests only
|
|
195
|
+
cargo test --test api # one integration file
|
|
196
|
+
cargo test -- --nocapture # show println! output
|
|
197
|
+
cargo test -- --test-threads=1 # serial execution (rare)
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Add `--quiet` for cleaner CI output.
|
|
201
|
+
|
|
202
|
+
## Golden files — `testdata/` + `env var`
|
|
203
|
+
|
|
204
|
+
```rust
|
|
205
|
+
const UPDATE: bool = option_env!("UPDATE_GOLDEN").is_some();
|
|
206
|
+
|
|
207
|
+
#[test]
|
|
208
|
+
fn render_matches_golden() {
|
|
209
|
+
let got = render(sample());
|
|
210
|
+
let path = "testdata/rendered.txt";
|
|
211
|
+
if UPDATE {
|
|
212
|
+
std::fs::write(path, &got).unwrap();
|
|
213
|
+
}
|
|
214
|
+
let want = std::fs::read_to_string(path).unwrap();
|
|
215
|
+
assert_eq!(got, want);
|
|
216
|
+
}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Run `UPDATE_GOLDEN=1 cargo test` to regenerate. `insta` does this better in most cases.
|
|
220
|
+
|
|
221
|
+
## Doc tests — executable examples in docs
|
|
222
|
+
|
|
223
|
+
```rust
|
|
224
|
+
/// Adds two numbers.
|
|
225
|
+
///
|
|
226
|
+
/// ```
|
|
227
|
+
/// use mycrate::add;
|
|
228
|
+
/// assert_eq!(add(2, 3), 5);
|
|
229
|
+
/// ```
|
|
230
|
+
pub fn add(a: i32, b: i32) -> i32 { a + b }
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
Run with `cargo test --doc`. Keeps public examples honest — if you change the API, the doc test breaks.
|
|
234
|
+
|
|
235
|
+
## Benchmarks
|
|
236
|
+
|
|
237
|
+
Stable Rust: `cargo bench` with `criterion`.
|
|
238
|
+
|
|
239
|
+
```rust
|
|
240
|
+
// benches/my_bench.rs
|
|
241
|
+
use criterion::{criterion_group, criterion_main, Criterion};
|
|
242
|
+
|
|
243
|
+
fn bench_parse(c: &mut Criterion) {
|
|
244
|
+
let input = "...";
|
|
245
|
+
c.bench_function("parse", |b| b.iter(|| parse(input)));
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
criterion_group!(benches, bench_parse);
|
|
249
|
+
criterion_main!(benches);
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Criterion runs statistical comparisons against previous runs, flags regressions.
|
|
253
|
+
|
|
254
|
+
## `cargo test` + `cargo llvm-cov` for coverage
|
|
255
|
+
|
|
256
|
+
```
|
|
257
|
+
cargo install cargo-llvm-cov
|
|
258
|
+
cargo llvm-cov --html
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
See `coding-standards/references/tdd.md` for target numbers.
|
|
262
|
+
|
|
263
|
+
## Anti-patterns
|
|
264
|
+
|
|
265
|
+
| Anti-pattern | Fix |
|
|
266
|
+
|---|---|
|
|
267
|
+
| `unwrap()` in non-test code copied into tests | Use `?` in `fn test() -> Result<(), Err>` if you want |
|
|
268
|
+
| `std::thread::sleep` in async tests | `tokio::time::sleep` (or pause time for determinism) |
|
|
269
|
+
| Tests that depend on execution order | Each test is independent; don't share globals |
|
|
270
|
+
| Hitting real external services in unit tests | Use fakes / `wiremock` / `httpmock` |
|
|
271
|
+
| Comparing `Debug` strings for equality | Use `assert_eq!` on typed values |
|
|
272
|
+
| Snapshot of time-dependent output without redaction | Inject a clock |
|
|
273
|
+
| Ignoring `#[should_panic]` without `expected = "..."` | Any panic passes; you want the specific one |
|
|
274
|
+
| Test file names that don't match the module under test | `foo.rs` → `foo/tests.rs` or `tests/foo.rs` |
|