@mrciphersmith/keryx 0.3.2 → 0.3.3
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/dist/cli.js +4634 -2445
- package/dist/core.js +66 -10
- package/package.json +1 -1
- package/src/gdskills/bundled/install-manifest.json +349 -2
- package/src/gdskills/bundled/rules/core/model-selection.mdc +18 -0
- package/src/gdskills/bundled/skills/orchestration/job-orchestrator/SKILL.md +1 -1
- package/src/gdskills/bundled/skills/planning/brainstorm/SKILL.md +1 -1
- package/src/gdskills/bundled/skills/planning/interviewer/SKILL.md +1 -1
- package/src/gdskills/bundled/skills/quality/deploy/SKILL.md +1 -1
- package/src/gdskills/bundled/skills/review/review-jev-contract/SKILL.md +193 -0
- package/src/gdskills/bundled/skills/review/review-orchestrator/SKILL.detail.md +81 -21
- package/src/gdskills/bundled/skills/review/review-orchestrator/SKILL.md +4 -4
- package/src/gdskills/bundled/stacks/c-cpp/agent-refs.json +4 -0
- package/src/gdskills/bundled/stacks/c-cpp/governance/eval.json +1777 -0
- package/src/gdskills/bundled/stacks/c-cpp/governance/scout.json +31 -0
- package/src/gdskills/bundled/stacks/c-cpp/pack.json +42 -0
- package/src/gdskills/bundled/stacks/c-cpp/rules/coding-style.mdc +80 -0
- package/src/gdskills/bundled/stacks/c-cpp/rules/patterns.mdc +87 -0
- package/src/gdskills/bundled/stacks/c-cpp/rules/security.mdc +90 -0
- package/src/gdskills/bundled/stacks/c-cpp/rules/testing.mdc +83 -0
- package/src/gdskills/bundled/stacks/c-cpp/skills/c-cpp-build-fix/SKILL.md +153 -0
- package/src/gdskills/bundled/stacks/c-cpp/skills/c-cpp-build-fix/evals.json +74 -0
- package/src/gdskills/bundled/stacks/c-cpp/skills/c-cpp-code-review/SKILL.md +132 -0
- package/src/gdskills/bundled/stacks/c-cpp/skills/c-cpp-code-review/evals.json +73 -0
- package/src/gdskills/bundled/stacks/c-cpp/skills/c-cpp-implementation/SKILL.md +151 -0
- package/src/gdskills/bundled/stacks/c-cpp/skills/c-cpp-implementation/evals.json +74 -0
- package/src/gdskills/bundled/stacks/c-cpp/skills/c-cpp-testing/SKILL.md +152 -0
- package/src/gdskills/bundled/stacks/c-cpp/skills/c-cpp-testing/evals.json +74 -0
- package/src/gdskills/bundled/stacks/ci-github-gitlab/agent-refs.json +4 -0
- package/src/gdskills/bundled/stacks/ci-github-gitlab/governance/eval.json +1295 -0
- package/src/gdskills/bundled/stacks/ci-github-gitlab/governance/scout.json +26 -0
- package/src/gdskills/bundled/stacks/ci-github-gitlab/pack.json +41 -0
- package/src/gdskills/bundled/stacks/ci-github-gitlab/rules/patterns.mdc +77 -0
- package/src/gdskills/bundled/stacks/ci-github-gitlab/rules/security.mdc +144 -0
- package/src/gdskills/bundled/stacks/ci-github-gitlab/skills/ci-pipeline-build-fix/SKILL.md +121 -0
- package/src/gdskills/bundled/stacks/ci-github-gitlab/skills/ci-pipeline-build-fix/evals.json +73 -0
- package/src/gdskills/bundled/stacks/ci-github-gitlab/skills/ci-pipeline-code-review/SKILL.md +139 -0
- package/src/gdskills/bundled/stacks/ci-github-gitlab/skills/ci-pipeline-code-review/evals.json +73 -0
- package/src/gdskills/bundled/stacks/ci-github-gitlab/skills/ci-pipeline-implementation/SKILL.md +147 -0
- package/src/gdskills/bundled/stacks/ci-github-gitlab/skills/ci-pipeline-implementation/evals.json +74 -0
- package/src/gdskills/bundled/stacks/docker-k8s-terraform/agent-refs.json +4 -0
- package/src/gdskills/bundled/stacks/docker-k8s-terraform/governance/eval.json +865 -0
- package/src/gdskills/bundled/stacks/docker-k8s-terraform/governance/scout.json +16 -0
- package/src/gdskills/bundled/stacks/docker-k8s-terraform/pack.json +46 -0
- package/src/gdskills/bundled/stacks/docker-k8s-terraform/rules/coding-style.mdc +74 -0
- package/src/gdskills/bundled/stacks/docker-k8s-terraform/rules/patterns.mdc +81 -0
- package/src/gdskills/bundled/stacks/docker-k8s-terraform/rules/security.mdc +146 -0
- package/src/gdskills/bundled/stacks/docker-k8s-terraform/rules/testing.mdc +61 -0
- package/src/gdskills/bundled/stacks/docker-k8s-terraform/skills/docker-k8s-terraform-build-fix/SKILL.md +151 -0
- package/src/gdskills/bundled/stacks/docker-k8s-terraform/skills/docker-k8s-terraform-build-fix/evals.json +74 -0
- package/src/gdskills/bundled/stacks/docker-k8s-terraform/skills/docker-k8s-terraform-review/SKILL.md +135 -0
- package/src/gdskills/bundled/stacks/docker-k8s-terraform/skills/docker-k8s-terraform-review/evals.json +76 -0
- package/src/gdskills/bundled/stacks/php-laravel/agent-refs.json +4 -0
- package/src/gdskills/bundled/stacks/php-laravel/governance/eval.json +1829 -0
- package/src/gdskills/bundled/stacks/php-laravel/governance/scout.json +33 -0
- package/src/gdskills/bundled/stacks/php-laravel/pack.json +41 -0
- package/src/gdskills/bundled/stacks/php-laravel/rules/coding-style.mdc +82 -0
- package/src/gdskills/bundled/stacks/php-laravel/rules/patterns.mdc +80 -0
- package/src/gdskills/bundled/stacks/php-laravel/rules/security.mdc +80 -0
- package/src/gdskills/bundled/stacks/php-laravel/rules/testing.mdc +82 -0
- package/src/gdskills/bundled/stacks/php-laravel/skills/php-laravel-build-fix/SKILL.md +143 -0
- package/src/gdskills/bundled/stacks/php-laravel/skills/php-laravel-build-fix/evals.json +74 -0
- package/src/gdskills/bundled/stacks/php-laravel/skills/php-laravel-code-review/SKILL.md +126 -0
- package/src/gdskills/bundled/stacks/php-laravel/skills/php-laravel-code-review/evals.json +76 -0
- package/src/gdskills/bundled/stacks/php-laravel/skills/php-laravel-implementation/SKILL.md +140 -0
- package/src/gdskills/bundled/stacks/php-laravel/skills/php-laravel-implementation/evals.json +75 -0
- package/src/gdskills/bundled/stacks/php-laravel/skills/php-laravel-testing/SKILL.md +124 -0
- package/src/gdskills/bundled/stacks/php-laravel/skills/php-laravel-testing/evals.json +74 -0
- package/src/gdskills/bundled/stacks/ruby-rails/agent-refs.json +4 -0
- package/src/gdskills/bundled/stacks/ruby-rails/governance/eval.json +1673 -0
- package/src/gdskills/bundled/stacks/ruby-rails/governance/scout.json +33 -0
- package/src/gdskills/bundled/stacks/ruby-rails/pack.json +42 -0
- package/src/gdskills/bundled/stacks/ruby-rails/rules/coding-style.mdc +69 -0
- package/src/gdskills/bundled/stacks/ruby-rails/rules/patterns.mdc +93 -0
- package/src/gdskills/bundled/stacks/ruby-rails/rules/security.mdc +90 -0
- package/src/gdskills/bundled/stacks/ruby-rails/rules/testing.mdc +89 -0
- package/src/gdskills/bundled/stacks/ruby-rails/skills/ruby-rails-build-fix/SKILL.md +143 -0
- package/src/gdskills/bundled/stacks/ruby-rails/skills/ruby-rails-build-fix/evals.json +73 -0
- package/src/gdskills/bundled/stacks/ruby-rails/skills/ruby-rails-code-review/SKILL.md +134 -0
- package/src/gdskills/bundled/stacks/ruby-rails/skills/ruby-rails-code-review/evals.json +71 -0
- package/src/gdskills/bundled/stacks/ruby-rails/skills/ruby-rails-implementation/SKILL.md +141 -0
- package/src/gdskills/bundled/stacks/ruby-rails/skills/ruby-rails-implementation/evals.json +72 -0
- package/src/gdskills/bundled/stacks/ruby-rails/skills/ruby-rails-testing/SKILL.md +125 -0
- package/src/gdskills/bundled/stacks/ruby-rails/skills/ruby-rails-testing/evals.json +72 -0
- package/src/gdskills/bundled/stacks/sql-db/agent-refs.json +4 -0
- package/src/gdskills/bundled/stacks/sql-db/governance/eval.json +1829 -0
- package/src/gdskills/bundled/stacks/sql-db/governance/scout.json +30 -0
- package/src/gdskills/bundled/stacks/sql-db/pack.json +40 -0
- package/src/gdskills/bundled/stacks/sql-db/rules/coding-style.mdc +69 -0
- package/src/gdskills/bundled/stacks/sql-db/rules/patterns.mdc +134 -0
- package/src/gdskills/bundled/stacks/sql-db/rules/security.mdc +74 -0
- package/src/gdskills/bundled/stacks/sql-db/rules/testing.mdc +83 -0
- package/src/gdskills/bundled/stacks/sql-db/skills/sql-db-build-fix/SKILL.md +147 -0
- package/src/gdskills/bundled/stacks/sql-db/skills/sql-db-build-fix/evals.json +72 -0
- package/src/gdskills/bundled/stacks/sql-db/skills/sql-db-code-review/SKILL.md +132 -0
- package/src/gdskills/bundled/stacks/sql-db/skills/sql-db-code-review/evals.json +73 -0
- package/src/gdskills/bundled/stacks/sql-db/skills/sql-db-implementation/SKILL.md +153 -0
- package/src/gdskills/bundled/stacks/sql-db/skills/sql-db-implementation/evals.json +77 -0
- package/src/gdskills/bundled/stacks/sql-db/skills/sql-db-testing/SKILL.md +129 -0
- package/src/gdskills/bundled/stacks/sql-db/skills/sql-db-testing/evals.json +73 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: c-cpp-code-review
|
|
3
|
+
description: "Use when reviewing a C or C++ change for memory-safety and undefined-behavior risks -- use-after-free, double-free, buffer overflows, dangling references, iterator invalidation, unchecked allocations, signed overflow, and unsynchronized shared state. Read-only, no edits."
|
|
4
|
+
triggers:
|
|
5
|
+
- "review this C++ diff for memory safety"
|
|
6
|
+
- "check this C change for a use-after-free"
|
|
7
|
+
- "review this C++ PR for buffer overflows"
|
|
8
|
+
- "any dangling references in this C++ change"
|
|
9
|
+
- "check this diff for iterator invalidation"
|
|
10
|
+
- "review this C code for unchecked malloc"
|
|
11
|
+
metadata:
|
|
12
|
+
origin: authored
|
|
13
|
+
category: review
|
|
14
|
+
version: "1.0.0"
|
|
15
|
+
compatible_harnesses: "claude,codex,cursor,zed,opencode"
|
|
16
|
+
license: "MIT"
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
# C / C++ code review
|
|
20
|
+
|
|
21
|
+
Read-only review of a C/C++ change for memory-safety, undefined-behavior,
|
|
22
|
+
and concurrency risks specific to C/C++: use-after-free, double-free,
|
|
23
|
+
buffer overflows, dangling references, iterator invalidation, unchecked
|
|
24
|
+
allocations, signed overflow, and unsynchronized shared state. This skill
|
|
25
|
+
never edits code — it reports findings. `rules/coding-style.mdc`,
|
|
26
|
+
`rules/patterns.mdc`, and `rules/security.mdc` are the rule set findings
|
|
27
|
+
are checked against.
|
|
28
|
+
|
|
29
|
+
## Workflow
|
|
30
|
+
|
|
31
|
+
### Step 1: Scope the review
|
|
32
|
+
|
|
33
|
+
1. Identify the changed files (`git diff` against the review base) —
|
|
34
|
+
review only `.c`/`.h`/`.cpp`/`.cc`/`.cxx`/`.hpp`/`.hxx` files in the
|
|
35
|
+
diff, not the whole repository.
|
|
36
|
+
2. Read enough of the surrounding, unchanged code to know whether a
|
|
37
|
+
flagged pattern is new in this diff or pre-existing; note pre-existing
|
|
38
|
+
issues separately from ones the diff introduces.
|
|
39
|
+
|
|
40
|
+
### Step 2: Check each changed function against the focus list
|
|
41
|
+
|
|
42
|
+
**Ownership and lifetime**
|
|
43
|
+
- A pointer or reference returned, stored, or captured that points into a
|
|
44
|
+
local (stack) variable, a temporary, or a since-destroyed object — flag
|
|
45
|
+
it as a use-after-free/dangling-reference risk.
|
|
46
|
+
- A raw `new`/`malloc` with no traceable single owner (an RAII type, or a
|
|
47
|
+
`free`/`delete` on every exit path) — flag it; also flag a `free`/
|
|
48
|
+
`delete` that could run twice on the same pointer on different paths
|
|
49
|
+
(double-free).
|
|
50
|
+
- A `malloc`/`calloc`/`realloc`/`new` result dereferenced without a
|
|
51
|
+
preceding null/exception check.
|
|
52
|
+
|
|
53
|
+
**Containers and iterators**
|
|
54
|
+
- An iterator, pointer, or reference into a `std::vector`/similar
|
|
55
|
+
container held across an operation that can reallocate or shift storage
|
|
56
|
+
(`push_back` past capacity, `insert`, `erase`) — flag the invalidation
|
|
57
|
+
risk.
|
|
58
|
+
|
|
59
|
+
**Bounds and undefined behavior**
|
|
60
|
+
- An array/buffer index or pointer-arithmetic expression built from
|
|
61
|
+
untrusted input with no visible bounds check before the read/write.
|
|
62
|
+
- A signed integer addition/multiplication whose operands could plausibly
|
|
63
|
+
overflow (a size/length/offset computation from untrusted input
|
|
64
|
+
especially) — signed overflow is undefined behavior, not just "wrong
|
|
65
|
+
answer on overflow."
|
|
66
|
+
- A pointer cast between incompatible types followed by a dereference (a
|
|
67
|
+
strict-aliasing violation) instead of `memcpy`/`std::bit_cast`.
|
|
68
|
+
- `strcpy`/`strcat`/`sprintf`/`gets` on data whose length is not
|
|
69
|
+
statically known to fit the destination.
|
|
70
|
+
|
|
71
|
+
**Concurrency**
|
|
72
|
+
- A variable read from one thread while written from another with no
|
|
73
|
+
mutex/`std::atomic`/`_Atomic` guarding it — flag unsynchronized shared
|
|
74
|
+
access.
|
|
75
|
+
- A manual `lock()`/`unlock()` pair instead of `std::lock_guard`/
|
|
76
|
+
`std::scoped_lock`, which skips the unlock on an early return or
|
|
77
|
+
exception thrown between them.
|
|
78
|
+
|
|
79
|
+
**Interfaces**
|
|
80
|
+
- A polymorphic C++ base class (deleted through a base pointer/reference
|
|
81
|
+
somewhere in the diff or its callers) with a non-virtual destructor —
|
|
82
|
+
flag as undefined behavior on delete.
|
|
83
|
+
- A memory-safety or UB bug-fix diff whose test/verification step never
|
|
84
|
+
mentions running under ASan/UBSan/TSan.
|
|
85
|
+
|
|
86
|
+
### Step 3: Report
|
|
87
|
+
|
|
88
|
+
For each finding: file:line, the pattern, why it matters (UAF, race,
|
|
89
|
+
overflow, leak), and the fix direction — but do not apply it.
|
|
90
|
+
|
|
91
|
+
```
|
|
92
|
+
src/parser/token_stream.cpp:88 — std::string_view into a temporary
|
|
93
|
+
std::string returned from trim() outlives the temporary once trim()'s
|
|
94
|
+
return value goes out of scope. Risk: use-after-free on first access.
|
|
95
|
+
Fix direction: return std::string by value, or take the buffer by
|
|
96
|
+
reference and return a span into the caller-owned storage.
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Rules
|
|
100
|
+
|
|
101
|
+
- NEVER edit code — findings and fix direction only.
|
|
102
|
+
- Flag use-after-free, double-free, buffer overflows, dangling
|
|
103
|
+
references, iterator invalidation, unchecked allocations, signed
|
|
104
|
+
overflow, strict-aliasing violations, and unsynchronized shared state;
|
|
105
|
+
do not report generic style nits already covered by
|
|
106
|
+
`clang-format`/`clang-tidy` (those are noise here).
|
|
107
|
+
- Distinguish a finding the diff introduces from a pre-existing one in
|
|
108
|
+
code the diff merely touches.
|
|
109
|
+
- When a suspected race or UB is not certain from reading alone, say "run
|
|
110
|
+
under ThreadSanitizer/UBSan to confirm" rather than asserting it exists
|
|
111
|
+
without evidence.
|
|
112
|
+
|
|
113
|
+
## Red Flags
|
|
114
|
+
|
|
115
|
+
| Rationalization | Why it is wrong |
|
|
116
|
+
|---|---|
|
|
117
|
+
| "The temporary's lifetime probably extends long enough in practice" | "Probably" is not a lifetime guarantee; a dangling reference into a destroyed temporary is undefined behavior the moment it is read, whether or not it happens to work today |
|
|
118
|
+
| "It's just a config struct, it's only written once at startup" | If it can be written concurrently with any read (even from an init thread), it needs a guard — "only once" is a claim to verify, not assume |
|
|
119
|
+
| "I'll just fix the missing NULL check myself since it's a one-line change" | This skill is read-only; report the finding and its fix direction, do not edit the file |
|
|
120
|
+
| "The overflow can't realistically happen with real-world inputs" | Untrusted input is exactly the case an attacker controls; a signed-overflow bounds check exists for the input that isn't realistic in normal use |
|
|
121
|
+
|
|
122
|
+
## Verification
|
|
123
|
+
|
|
124
|
+
Do not report the review done until all of the following hold:
|
|
125
|
+
|
|
126
|
+
- Every changed C/C++ file in the diff was read, not just files named in
|
|
127
|
+
the PR description.
|
|
128
|
+
- Every finding names a concrete file:line, the specific risk category
|
|
129
|
+
from Step 2, and a fix direction.
|
|
130
|
+
- No source file was modified by this review.
|
|
131
|
+
- Findings distinguish diff-introduced issues from pre-existing ones in
|
|
132
|
+
touched files.
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
{
|
|
2
|
+
"triggers": {
|
|
3
|
+
"positive": [
|
|
4
|
+
"I refactored Buffer::resize to use raw pointers instead of std::vector -- can you look for memory bugs before I open the PR",
|
|
5
|
+
"This PR frees the connection object on the error path -- does anything later in the function still touch it",
|
|
6
|
+
"I'm returning a reference to a local Session inside this function -- could that reference outlive the object it points to",
|
|
7
|
+
"Review this change for unsynchronized access to the shared cache",
|
|
8
|
+
"Look over this diff for iterator invalidation risk",
|
|
9
|
+
"Check whether this new parser code has any bounds issues",
|
|
10
|
+
"Review this pull request for double-free risk"
|
|
11
|
+
],
|
|
12
|
+
"negative": [
|
|
13
|
+
"Implement this feature in modern C++, don't just review it",
|
|
14
|
+
"Fix this ctest failure in the parser module",
|
|
15
|
+
"Review this Go diff for goroutine leaks and data races",
|
|
16
|
+
"Review this Rust diff for unsafe block soundness",
|
|
17
|
+
"Write GoogleTest cases for this class",
|
|
18
|
+
"Fix this CMake linker error"
|
|
19
|
+
]
|
|
20
|
+
},
|
|
21
|
+
"scenarios": [
|
|
22
|
+
{
|
|
23
|
+
"id": "review-iterator-invalidation",
|
|
24
|
+
"prompt": "Please review this diff:\n\n```cpp\nvoid remove_expired(std::vector<Session>& sessions) {\n for (auto it = sessions.begin(); it != sessions.end(); ++it) {\n if (it->expired()) {\n sessions.erase(it);\n }\n }\n}\n```",
|
|
25
|
+
"strictness": "high",
|
|
26
|
+
"expected_behavior": [
|
|
27
|
+
{
|
|
28
|
+
"grader": "judge",
|
|
29
|
+
"rubric": "A correct review identifies that sessions.erase(it) invalidates `it` (and every iterator/pointer/reference from the erase point onward), so the loop's own `++it` afterward is undefined behavior, and gives a concrete fix direction -- assigning erase's return value back to `it` and skipping the increment on that branch, or using std::erase_if / the erase-remove idiom -- without applying the fix itself.",
|
|
30
|
+
"pass_criteria": [
|
|
31
|
+
"States that sessions.erase(it) invalidates the iterator `it`, and that continuing to use it (including the loop's `++it`) afterward is undefined behavior/a bug, not just a style concern.",
|
|
32
|
+
"Names a concrete fix direction: assign erase's return value back to `it` (and skip the redundant `++it` on that branch), or use std::erase_if / the erase-remove idiom.",
|
|
33
|
+
"Gives the finding with a specific location (the erase call / the loop) rather than a generic 'this loop looks risky' comment."
|
|
34
|
+
],
|
|
35
|
+
"fail_criteria": [
|
|
36
|
+
"Says the loop is correct as written, or reviews only style/formatting without flagging the iterator invalidation."
|
|
37
|
+
]
|
|
38
|
+
}
|
|
39
|
+
],
|
|
40
|
+
"calibration": {
|
|
41
|
+
"known_right": "This has an iterator-invalidation bug: `sessions.erase(it)` invalidates `it` itself (std::vector::erase invalidates the erased element's iterator and everything after it), so the loop's own `++it` on the next iteration is undefined behavior -- it's advancing an iterator that's already been invalidated. The fix is to capture erase's return value, which is the iterator to the next valid element, and only increment in the branch that didn't erase: `if (it->expired()) { it = sessions.erase(it); } else { ++it; }`. Simpler still, since this is just filtering a vector, `std::erase_if(sessions, [](const Session& s) { return s.expired(); });` (C++20) replaces the whole loop and can't get the iterator handling wrong. Flag this as a diff-introduced correctness bug, not a style nit -- it's undefined behavior, not just inefficient.",
|
|
42
|
+
"known_wrong": "This looks fine -- the loop erases each expired session as it finds one and keeps going. std::vector::erase is a normal member function call, so once it returns the loop just continues from where `it` was pointing, same as with any other container operation in the middle of a loop.",
|
|
43
|
+
"vague": "The loop might have an issue with how it's iterating while also modifying the vector, worth taking another look at that.",
|
|
44
|
+
"subtle_wrong": "Good use of `expired()` to filter here. One nit: `sessions.erase(it)` is O(n) per call since it has to shift every following element down, so for a large sessions vector this loop is O(n^2) overall. Consider collecting the expired indices first and erasing them in a single pass afterward for better performance, but functionally the loop is correct since erase() returns a valid iterator afterward regardless of what the loop does with it."
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"id": "review-unchecked-malloc-and-free",
|
|
49
|
+
"prompt": "Please review this diff:\n\n```c\nstruct Buffer *buffer_create(size_t size) {\n struct Buffer *buf = malloc(sizeof(struct Buffer));\n buf->data = malloc(size);\n buf->size = size;\n return buf;\n}\n```",
|
|
50
|
+
"strictness": "high",
|
|
51
|
+
"expected_behavior": [
|
|
52
|
+
{
|
|
53
|
+
"grader": "judge",
|
|
54
|
+
"rubric": "A correct review flags that neither malloc call's return value is checked for NULL before being dereferenced or stored (buf->data is written on a possibly-NULL buf, and buf->data itself is never checked), and that a failure of the second malloc leaks the first allocation with no free -- and states a fix direction (check both returns, free buf on the second failure) without applying it.",
|
|
55
|
+
"pass_criteria": [
|
|
56
|
+
"Flags that `buf` (the first malloc's result) is dereferenced (buf->data, buf->size) with no NULL check first.",
|
|
57
|
+
"Flags that `buf->data` (the second malloc's result) is also never checked for NULL before being stored/returned.",
|
|
58
|
+
"States that if the second malloc fails, the first allocation (`buf`) is never freed before the function could reasonably be considered to have failed -- a leak on that error path -- and gives a concrete fix direction (check both allocations, free buf and return NULL/an error on the second one failing)."
|
|
59
|
+
],
|
|
60
|
+
"fail_criteria": [
|
|
61
|
+
"Reviews the diff without flagging at least the missing NULL checks on both malloc results."
|
|
62
|
+
]
|
|
63
|
+
}
|
|
64
|
+
],
|
|
65
|
+
"calibration": {
|
|
66
|
+
"known_right": "Two problems here. First, neither malloc result is checked for NULL: `buf` is dereferenced immediately (`buf->data = ...`, `buf->size = ...`) with no check that the first malloc actually succeeded, and `buf->data` itself is stored and returned without ever being checked either -- a caller has no way to tell a real allocation from a failed one. Second, if the second malloc (for `buf->data`) fails, `buf` itself is already allocated and is never freed on that path -- a leak. Fix direction: check `buf` for NULL and return NULL immediately if it is; check `buf->data` for NULL and, if it failed, `free(buf)` before returning NULL so the first allocation doesn't leak on the error path.",
|
|
67
|
+
"known_wrong": "This looks like a standard constructor-style function -- allocate the struct, allocate its buffer, fill in the fields, return the pointer. malloc failures are rare enough in practice that most codebases don't bother checking every single call, and the two-allocation pattern here is pretty typical for a struct that owns a separately-sized buffer.",
|
|
68
|
+
"vague": "Might want to double check the error handling around the allocations in this function before merging.",
|
|
69
|
+
"subtle_wrong": "One issue: if `malloc(size)` for `buf->data` fails, the function still returns `buf` with `buf->data` set to NULL and `buf->size` set to the requested size, which means a caller could later try to write into `buf->data` at up to `buf->size` bytes and crash on a NULL write. The fix is to have callers check `buf->data != NULL` themselves before using it, rather than checking anything inside buffer_create -- that way the allocation and the usage-time check stay in the caller's control, and buf itself never needs a NULL check since malloc(sizeof(struct Buffer)) essentially never fails for a small fixed-size struct like this."
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
]
|
|
73
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: c-cpp-implementation
|
|
3
|
+
description: "Use when implementing or extending a feature in C or modern C++ (17/20/23) -- ownership and RAII, smart pointer choice, move semantics, std::span, manual malloc/free lifetime in C, and choosing safe standard-library APIs over unsafe ones. Not for Rust, Go, or another systems language (use that language's own implementation skill)."
|
|
4
|
+
triggers:
|
|
5
|
+
- "implement this feature in modern C++"
|
|
6
|
+
- "add a function to this C module"
|
|
7
|
+
- "which smart pointer should own this object"
|
|
8
|
+
- "convert this raw new/delete to RAII"
|
|
9
|
+
- "add a std::span-based API for this buffer"
|
|
10
|
+
- "write the malloc/free lifetime for this C struct"
|
|
11
|
+
metadata:
|
|
12
|
+
origin: authored
|
|
13
|
+
category: implement
|
|
14
|
+
version: "1.0.0"
|
|
15
|
+
compatible_harnesses: "claude,codex,cursor,zed,opencode"
|
|
16
|
+
license: "MIT"
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
# C / C++ implementation (C++17/20/23, C99/C11+)
|
|
20
|
+
|
|
21
|
+
Implement or extend a feature in a C or C++ codebase: ownership design,
|
|
22
|
+
resource lifetime, memory safety, and modern idiom for whichever of the
|
|
23
|
+
two languages the file under change actually is. Scoped to C/C++
|
|
24
|
+
specifically — `rules/coding-style.mdc`, `rules/patterns.mdc`, and
|
|
25
|
+
`rules/security.mdc` carry the full stack-specific rule set this skill's
|
|
26
|
+
checklist is built from; read them before writing code, not just this
|
|
27
|
+
summary.
|
|
28
|
+
|
|
29
|
+
## Workflow
|
|
30
|
+
|
|
31
|
+
### Step 1: Discover the project's own conventions
|
|
32
|
+
|
|
33
|
+
1. Identify the build system (`CMakeLists.txt`, `Makefile`, `meson.build`)
|
|
34
|
+
and the C/C++ standard it targets (`CMAKE_CXX_STANDARD`, `-std=`
|
|
35
|
+
flags) — do not use a language feature (`std::span`, `constexpr` on a
|
|
36
|
+
function that needs C++20, a C11-only `_Generic`) the project's own
|
|
37
|
+
declared standard predates.
|
|
38
|
+
2. Determine whether the file under change is C or C++ from its
|
|
39
|
+
extension (`.c`/`.h` vs `.cpp`/`.cc`/`.cxx`/`.hpp`/`.hxx`) and match
|
|
40
|
+
that language's idiom — do not introduce C++-only constructs
|
|
41
|
+
(`std::unique_ptr`, exceptions, namespaces) into a `.c` file.
|
|
42
|
+
3. Read 1-2 neighboring files for: existing ownership style (smart
|
|
43
|
+
pointers vs. manual `malloc`/`free`), whether the project has a
|
|
44
|
+
`.clang-format`/`.clang-tidy` config, existing error-handling
|
|
45
|
+
convention (exceptions, error codes, `std::expected`), and whether a
|
|
46
|
+
testing framework (GoogleTest, Catch2) is already wired into the
|
|
47
|
+
build.
|
|
48
|
+
|
|
49
|
+
### Step 2: Design ownership before writing
|
|
50
|
+
|
|
51
|
+
- For every new heap allocation, decide who owns it and for how long
|
|
52
|
+
*before* writing the allocation: a `std::unique_ptr` (single owner,
|
|
53
|
+
the default), a `std::shared_ptr` (only when ownership is genuinely
|
|
54
|
+
shared across objects with independent lifetimes), a stack/automatic
|
|
55
|
+
object (when it never needs to outlive its scope), or — in C — a
|
|
56
|
+
`malloc`/`free` pair with an explicit, documented ownership contract in
|
|
57
|
+
the header comment.
|
|
58
|
+
- Trace every pointer/reference this change returns or stores: does it
|
|
59
|
+
outlive the object it points into? A pointer or reference into a local
|
|
60
|
+
variable, a temporary, or a container element that might reallocate is
|
|
61
|
+
a use-after-free or dangling-reference bug the moment it escapes.
|
|
62
|
+
- For concurrent access, decide up front which mutex (or `std::atomic`)
|
|
63
|
+
guards which shared variable — do not add a shared variable first and
|
|
64
|
+
retrofit synchronization after a race shows up.
|
|
65
|
+
|
|
66
|
+
### Step 3: Implement
|
|
67
|
+
|
|
68
|
+
1. C++: prefer RAII types (smart pointers, `std::lock_guard`,
|
|
69
|
+
`std::vector`/`std::string`) over manual acquire/release; use
|
|
70
|
+
`std::make_unique`/`std::make_shared`, not a bare `new` handed to a
|
|
71
|
+
smart pointer constructor.
|
|
72
|
+
2. C: check every `malloc`/`calloc`/`realloc` return for `NULL`; release
|
|
73
|
+
every acquired resource on every exit path (a `goto cleanup;` pattern
|
|
74
|
+
for functions with more than one early return keeps this from being
|
|
75
|
+
duplicated and missed).
|
|
76
|
+
3. Use `std::span` (C++20+) for a non-owning view over a buffer instead
|
|
77
|
+
of a raw `(pointer, length)` pair when the project's standard allows
|
|
78
|
+
it; validate every index/length derived from untrusted input before
|
|
79
|
+
it reaches a read or write.
|
|
80
|
+
4. Move rather than copy when transferring ownership (`std::move` into a
|
|
81
|
+
by-value parameter, or an rvalue-ref overload) for anything
|
|
82
|
+
non-trivially-copyable.
|
|
83
|
+
5. Use a named cast (`static_cast`/`const_cast`/`reinterpret_cast`) in
|
|
84
|
+
C++, never a C-style cast — it states which conversion is actually
|
|
85
|
+
intended.
|
|
86
|
+
6. Format with the project's configured `clang-format` as you go, not as
|
|
87
|
+
an afterthought.
|
|
88
|
+
|
|
89
|
+
### Step 4: Verify
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
cmake --build build # or the project's own configured build command
|
|
93
|
+
ctest --test-dir build --output-on-failure
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
For anything touching manual memory management, buffers, pointer
|
|
97
|
+
arithmetic, or concurrency, also build and run under sanitizers before
|
|
98
|
+
reporting done:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
cmake -S . -B build-asan -DCMAKE_BUILD_TYPE=Debug \
|
|
102
|
+
-DCMAKE_CXX_FLAGS='-fsanitize=address,undefined -fno-omit-frame-pointer -g'
|
|
103
|
+
cmake --build build-asan && ctest --test-dir build-asan --output-on-failure
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
A build/test/sanitizer failure at this step is a signal to fix the
|
|
107
|
+
implementation, not to reach for `c-cpp-build-fix`'s scope unless the
|
|
108
|
+
failure is a build/toolchain/link problem unrelated to the feature logic.
|
|
109
|
+
|
|
110
|
+
### Step 5: Report
|
|
111
|
+
|
|
112
|
+
```
|
|
113
|
+
Implemented: src/parser/token_stream.cpp, include/parser/token_stream.hpp
|
|
114
|
+
- std::unique_ptr-owned TokenStream, std::span<const char> input view
|
|
115
|
+
- build + ctest + ASan/UBSan build all pass
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Rules
|
|
119
|
+
|
|
120
|
+
- Never write a raw `new`/`malloc` whose matching `delete`/`free` is not
|
|
121
|
+
either in the same RAII destructor or on every exit path of the
|
|
122
|
+
function that allocated it.
|
|
123
|
+
- Never return, store, or capture a pointer/reference to a local
|
|
124
|
+
(stack-lifetime) variable, or to a temporary's member.
|
|
125
|
+
- Never dereference a `malloc`/`new` result without first checking it for
|
|
126
|
+
`NULL`/an exception path.
|
|
127
|
+
- Match the project's declared C/C++ standard — do not use a language or
|
|
128
|
+
library feature newer than what the build configuration targets.
|
|
129
|
+
|
|
130
|
+
## Red Flags
|
|
131
|
+
|
|
132
|
+
| Rationalization | Why it is wrong |
|
|
133
|
+
|---|---|
|
|
134
|
+
| "I'll use `shared_ptr` here so I don't have to think about who owns it" | `shared_ptr` is not a substitute for an ownership decision — reaching for it by default hides a real single-owner relationship behind atomic refcounting and can create reference cycles a `unique_ptr` design would have made obvious |
|
|
135
|
+
| "This buffer's length is always correct in practice, I won't re-check it" | "In practice" is not a proof; an unchecked length from untrusted input is exactly how a buffer overflow gets in |
|
|
136
|
+
| "I'll return a pointer to this local struct, the caller will use it right away" | "Right away" still means after the function returns, by which point the stack frame is gone; the pointer is already dangling at the call site |
|
|
137
|
+
| "malloc can't really fail here, I'll skip the NULL check" | A failed allocation dereferenced as non-NULL is a guaranteed crash or corruption the moment it does happen; the check costs one branch |
|
|
138
|
+
|
|
139
|
+
## Verification
|
|
140
|
+
|
|
141
|
+
Do not report the work done until all of the following hold:
|
|
142
|
+
|
|
143
|
+
- The project's configured build and test commands exit 0.
|
|
144
|
+
- For any change touching manual memory management, buffers, or
|
|
145
|
+
concurrency: an ASan+UBSan (and, for threaded code, TSan) build of the
|
|
146
|
+
touched tests also passes clean.
|
|
147
|
+
- Every new heap allocation has a traceable single owner (an RAII type,
|
|
148
|
+
or a documented `malloc`/`free` contract) — no bare `new`/`malloc` with
|
|
149
|
+
no visible release path.
|
|
150
|
+
- No pointer or reference returned or stored by the change outlives the
|
|
151
|
+
object it refers to.
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
{
|
|
2
|
+
"triggers": {
|
|
3
|
+
"positive": [
|
|
4
|
+
"Implement a new Logger class that owns a single output file handle for its lifetime",
|
|
5
|
+
"Add a function that hands back a parsed config object to the caller",
|
|
6
|
+
"Write the ownership logic for this heap-allocated audio buffer",
|
|
7
|
+
"Implement a factory function that constructs and returns this object",
|
|
8
|
+
"Add a getter that returns a view into this struct's internal array",
|
|
9
|
+
"I need to add a resource-owning class to this module, how should it manage its handle",
|
|
10
|
+
"Add a new TokenStream that wraps a buffer passed in at construction"
|
|
11
|
+
],
|
|
12
|
+
"negative": [
|
|
13
|
+
"Implement this feature in Rust using ownership and borrowing",
|
|
14
|
+
"Implement this feature in Go using a worker pool and errgroup",
|
|
15
|
+
"Write a unit test for this C++ class",
|
|
16
|
+
"Review this C++ diff for memory safety issues",
|
|
17
|
+
"Fix this failing ctest run for the parser module",
|
|
18
|
+
"Add table-driven tests for this Python function"
|
|
19
|
+
]
|
|
20
|
+
},
|
|
21
|
+
"scenarios": [
|
|
22
|
+
{
|
|
23
|
+
"id": "ownership-unique-vs-shared",
|
|
24
|
+
"prompt": "I'm adding a new Logger class that owns a single output file handle for its lifetime, and only one part of the program ever creates or holds it. What smart pointer should manage the file handle, and how should the class be structured?",
|
|
25
|
+
"strictness": "high",
|
|
26
|
+
"expected_behavior": [
|
|
27
|
+
{
|
|
28
|
+
"grader": "judge",
|
|
29
|
+
"rubric": "A correct answer recommends single, exclusive ownership for the file handle -- either std::unique_ptr<std::ofstream>, or holding a plain std::ofstream member directly by value (std::ofstream is itself move-only RAII, so no pointer indirection is required at all for a single owner) -- explains concretely why std::shared_ptr is the wrong choice here (unnecessary atomic refcount overhead and it obscures the fact that there is exactly one owner), and, if it chooses unique_ptr, shows constructing it with std::make_unique rather than a bare new.",
|
|
30
|
+
"pass_criteria": [
|
|
31
|
+
"Recommends either std::unique_ptr specifically or a plain by-value std::ofstream member (not just 'a smart pointer', and not std::shared_ptr) for the file handle.",
|
|
32
|
+
"Gives a concrete reason std::shared_ptr is inappropriate for this single-owner case -- refcount overhead, or that it hides/misrepresents a single-owner relationship -- not just 'keep it simple'.",
|
|
33
|
+
"If it recommends std::unique_ptr, shows concrete construction, e.g. std::make_unique, rather than a bare new handed to the pointer; a by-value std::ofstream member satisfies this criterion by needing no pointer construction at all."
|
|
34
|
+
],
|
|
35
|
+
"fail_criteria": [
|
|
36
|
+
"Recommends std::shared_ptr as the way to manage the file handle for this single-owner case instead of std::unique_ptr. Mentioning std::shared_ptr only to explain why it is the wrong choice here is not a failure."
|
|
37
|
+
]
|
|
38
|
+
}
|
|
39
|
+
],
|
|
40
|
+
"calibration": {
|
|
41
|
+
"known_right": "Since exactly one part of the program creates and holds the Logger's file handle, the simplest correct option is to hold std::ofstream directly as a plain by-value member -- std::ofstream is already move-only RAII, so it needs no pointer wrapper at all: `std::ofstream file_;`, opened in the constructor's initializer list or body. If the handle weren't already an RAII type, or you specifically needed heap allocation (e.g. a pimpl or a polymorphic handle), std::unique_ptr<std::ofstream> constructed with std::make_unique<std::ofstream>(path) is the equivalent single-owner choice. Either way, don't reach for std::shared_ptr here -- there's only one owner, so shared_ptr's atomic reference counting is pure overhead, and using it anyway would misrepresent the ownership model to anyone reading the class later, making them think the handle might be shared when it never is. Make Logger non-copyable (delete the copy constructor/assignment, or let it default from the ofstream/unique_ptr member already being move-only) so the compiler enforces the single-owner invariant instead of relying on convention. The destructor needs no explicit code -- the member's own destructor closes the file when the Logger is destroyed.",
|
|
42
|
+
"known_wrong": "Just use std::shared_ptr<std::ofstream> for the member -- it's the safer default for any owned resource since you don't have to think hard about whether ownership might need to become shared later, and the reference counting overhead is negligible in practice. Construct it with std::make_shared<std::ofstream>(path) and you're done; if another part of the code ever needs access to the same handle down the road, shared_ptr already supports that with no further changes.",
|
|
43
|
+
"vague": "Use a smart pointer to manage the file handle so it cleans up automatically instead of managing it by hand.",
|
|
44
|
+
"subtle_wrong": "std::unique_ptr would work, but since Logger might eventually get passed into a callback stored in a std::function elsewhere in the codebase, go with std::shared_ptr<std::ofstream> from the start -- unique_ptr can be awkward to move into callback storage, and shared_ptr sidesteps that friction entirely while still being reference-counted-safe if the Logger is ever captured by more than one closure."
|
|
45
|
+
},
|
|
46
|
+
"anti_patterns": ["std::shared_ptr"]
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"id": "dangling-view-into-local",
|
|
50
|
+
"prompt": "I want to add a helper that trims leading/trailing whitespace from a string for the caller to use right away. Here's my sketch:\n\n```cpp\nstd::string_view trim(const std::string& input) {\n std::string result = strip_ws(input);\n return result;\n}\n```\n\nDoes this look right?",
|
|
51
|
+
"strictness": "high",
|
|
52
|
+
"expected_behavior": [
|
|
53
|
+
{
|
|
54
|
+
"grader": "judge",
|
|
55
|
+
"rubric": "A correct answer identifies that `result` is a local std::string destroyed when trim() returns, so the returned std::string_view refers to freed/destroyed storage -- a dangling view whose use is undefined behavior, not merely risky -- and proposes a concrete fix such as returning std::string by value instead of a view, or having the caller supply the backing storage so a view into it stays valid.",
|
|
56
|
+
"pass_criteria": [
|
|
57
|
+
"States that `result` is a local variable destroyed at the end of trim(), so the returned string_view is left referring to destroyed/deallocated storage.",
|
|
58
|
+
"States or implies this is undefined behavior (or otherwise unsafe to use) on the very first access by the caller, not just 'risky' or 'not ideal'.",
|
|
59
|
+
"Proposes a concrete fix: return std::string by value (relying on move/RVO), or restructure so the view points into storage the caller owns and that outlives the call."
|
|
60
|
+
],
|
|
61
|
+
"fail_criteria": [
|
|
62
|
+
"Says the code is fine as long as the caller uses the returned string_view immediately after the call, or otherwise treats prompt use as making the dangling view safe."
|
|
63
|
+
]
|
|
64
|
+
}
|
|
65
|
+
],
|
|
66
|
+
"calibration": {
|
|
67
|
+
"known_right": "This has a dangling-reference bug: `result` is a std::string local to trim(), and it's destroyed the moment the function returns. The std::string_view you're handing back still points at wherever result's buffer was, which is now freed -- reading through it on the caller's side is undefined behavior, not just fragile, and it doesn't matter that the caller intends to use it 'right away'; the string's destructor has already run by the time the caller gets the return value. The fix is to return std::string by value instead of a view -- `std::string trim(const std::string& input) { return strip_ws(input); }` -- and let move semantics/RVO make that cheap. If you specifically need a non-owning view into existing storage (to avoid a copy), the caller has to own the buffer and pass it in so the view you return points into memory that outlives the call, rather than into something trim() allocated and destroyed itself.",
|
|
68
|
+
"known_wrong": "That looks fine -- as long as the caller assigns the returned string_view to a variable and uses it right away, before anything else runs, the underlying characters are still going to be sitting in memory. string_view is a lightweight, non-owning view specifically so you avoid an extra string copy on the return path, so this is actually the more efficient version of the function compared to returning a std::string by value.",
|
|
69
|
+
"vague": "Something about the lifetime of the string here seems off, you might want to double check how long the data returned actually lives.",
|
|
70
|
+
"subtle_wrong": "The one thing to watch here is that `result` is constructed by value inside the function, so returning a view of it does incur a hidden copy of the underlying buffer before the view is handed back -- that's a performance concern more than a correctness one. If you want to avoid that copy, mark `result` as `static thread_local` inside trim() so its storage persists across calls instead of being destroyed on return; that keeps the string_view you return valid indefinitely without needing to change the function's return type."
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
]
|
|
74
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: c-cpp-testing
|
|
3
|
+
description: "Use when a C or C++ test suite needs writing, extending, or fixing -- GoogleTest TEST/TEST_F/TEST_P, death tests, fixture setup/teardown, and verifying a memory-safety or concurrency fix under AddressSanitizer/UndefinedBehaviorSanitizer/ThreadSanitizer. Not for another systems language's own test tooling, such as Rust's cargo test (use that language's own testing skill)."
|
|
4
|
+
triggers:
|
|
5
|
+
- "write GoogleTest cases for this C++ class"
|
|
6
|
+
- "add a TEST_F fixture for this component"
|
|
7
|
+
- "fix this failing ctest"
|
|
8
|
+
- "add a death test for this assertion"
|
|
9
|
+
- "verify this fix under AddressSanitizer"
|
|
10
|
+
- "add a parameterized test with TEST_P"
|
|
11
|
+
metadata:
|
|
12
|
+
origin: authored
|
|
13
|
+
category: test
|
|
14
|
+
version: "1.0.0"
|
|
15
|
+
compatible_harnesses: "claude,codex,cursor,zed,opencode"
|
|
16
|
+
license: "MIT"
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
# C / C++ testing (GoogleTest, sanitizers)
|
|
20
|
+
|
|
21
|
+
Write, extend, or fix a C/C++ test suite: GoogleTest cases and fixtures,
|
|
22
|
+
death tests, parameterized tests, and sanitizer-backed verification for
|
|
23
|
+
memory-safety and concurrency fixes. `rules/testing.mdc` carries the full
|
|
24
|
+
rule set this skill's checklist is built from — read it, not just this
|
|
25
|
+
summary, before writing tests.
|
|
26
|
+
|
|
27
|
+
## Workflow
|
|
28
|
+
|
|
29
|
+
### Step 1: Discover the project's test conventions
|
|
30
|
+
|
|
31
|
+
1. Find the test tree (`tests/`, `test/`, or `*_test.cpp` beside the
|
|
32
|
+
source) and the test framework already wired into the build
|
|
33
|
+
(GoogleTest is assumed unless the project's `CMakeLists.txt`/build
|
|
34
|
+
files show Catch2 or another framework — match whichever is there).
|
|
35
|
+
2. Read 1-2 neighboring test files for: fixture naming, assertion style
|
|
36
|
+
(`ASSERT_*` vs `EXPECT_*` usage pattern), whether mocks
|
|
37
|
+
(`gmock`) are already in use, and whether sanitizer builds are already
|
|
38
|
+
configured as a separate CMake preset/target.
|
|
39
|
+
3. Check whether the project's CI or build scripts already run an ASan/
|
|
40
|
+
UBSan/TSan build — reuse that configuration rather than inventing a
|
|
41
|
+
new one.
|
|
42
|
+
|
|
43
|
+
### Step 2: Plan test cases
|
|
44
|
+
|
|
45
|
+
**Functions:** happy path, edge cases (empty/zero/null inputs, boundary
|
|
46
|
+
values), error cases (an exception thrown, an error code returned, or —
|
|
47
|
+
for a hard invariant — a process-terminating `CHECK`/`assert`).
|
|
48
|
+
|
|
49
|
+
**Fixtures:** `TEST_F` with a fixture class deriving from
|
|
50
|
+
`::testing::Test`, shared setup in `SetUp()`/teardown in `TearDown()`,
|
|
51
|
+
not the constructor/destructor, when setup can fail in a way the test
|
|
52
|
+
should assert on.
|
|
53
|
+
|
|
54
|
+
**Parameterized:** `TEST_P` + `INSTANTIATE_TEST_SUITE_P` for the same
|
|
55
|
+
case body run across a range of inputs, instead of copy-pasted `TEST`s
|
|
56
|
+
with different literals.
|
|
57
|
+
|
|
58
|
+
**Memory-safety/UB fixes:** a regression test that reproduces the bug
|
|
59
|
+
(use-after-free, buffer overflow, signed overflow, data race) under the
|
|
60
|
+
relevant sanitizer before the fix, and passes clean under that same
|
|
61
|
+
sanitizer after it.
|
|
62
|
+
|
|
63
|
+
**Concurrent code:** a test that starts threads under test joins them
|
|
64
|
+
(`std::thread::join`, a future, a condition variable) before asserting —
|
|
65
|
+
never `sleep`-synchronized — and the whole suite runs at least once under
|
|
66
|
+
ThreadSanitizer.
|
|
67
|
+
|
|
68
|
+
### Step 3: Write
|
|
69
|
+
|
|
70
|
+
1. Create/extend the test file at the project's own convention path.
|
|
71
|
+
2. Name suites/cases for the behavior under test (`ParsesEmptyInput`,
|
|
72
|
+
not `Test1`).
|
|
73
|
+
3. Use `ASSERT_*` when a failure means the rest of the test cannot
|
|
74
|
+
meaningfully continue (a null result every later line dereferences);
|
|
75
|
+
`EXPECT_*` otherwise, so later checks still report.
|
|
76
|
+
4. For a death test, set the death test style explicitly
|
|
77
|
+
(`GTEST_FLAG_SET(death_test_style, "threadsafe")`) when the binary is
|
|
78
|
+
multi-threaded — the default `fast` style can misbehave forking a
|
|
79
|
+
process with more than one live thread.
|
|
80
|
+
5. Release any resource a fixture owns (temp files, mock servers,
|
|
81
|
+
threads) in `TearDown()`, not just at the end of a passing test body.
|
|
82
|
+
|
|
83
|
+
### Step 4: Run and fix
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
ctest --test-dir build --output-on-failure
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Fix failing tests (max 3 iterations) — fix the test, not the source under
|
|
90
|
+
test, unless the test itself has correctly caught a real bug (say so in
|
|
91
|
+
the report rather than silently changing production code).
|
|
92
|
+
|
|
93
|
+
### Step 5: Sanitizer verification (when relevant)
|
|
94
|
+
|
|
95
|
+
For any test covering manual memory management, buffers, pointer
|
|
96
|
+
arithmetic, or shared concurrent state, also run:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
cmake -S . -B build-asan -DCMAKE_BUILD_TYPE=Debug \
|
|
100
|
+
-DCMAKE_CXX_FLAGS='-fsanitize=address,undefined -fno-omit-frame-pointer -g'
|
|
101
|
+
cmake --build build-asan && ctest --test-dir build-asan --output-on-failure
|
|
102
|
+
|
|
103
|
+
cmake -S . -B build-tsan -DCMAKE_BUILD_TYPE=Debug \
|
|
104
|
+
-DCMAKE_CXX_FLAGS='-fsanitize=thread -fno-omit-frame-pointer -g'
|
|
105
|
+
cmake --build build-tsan && ctest --test-dir build-tsan --output-on-failure
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
ASan/UBSan and TSan are separate runtimes and must not be linked into the
|
|
109
|
+
same binary — build them as distinct configurations.
|
|
110
|
+
|
|
111
|
+
### Step 6: Report
|
|
112
|
+
|
|
113
|
+
```
|
|
114
|
+
Generated: tests/token_stream_test.cpp
|
|
115
|
+
- 9 cases (3 TEST_F, 2 TEST_P instances), ctest all passing
|
|
116
|
+
- Regression case for the reported use-after-free passes clean under ASan
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Rules
|
|
120
|
+
|
|
121
|
+
- ALWAYS match the project's existing framework, fixture, and assertion
|
|
122
|
+
conventions found in Step 1, not a different project's style.
|
|
123
|
+
- NEVER modify source code under test — only test files (and fixtures/
|
|
124
|
+
mocks/testdata).
|
|
125
|
+
- NEVER synchronize a concurrent test with `sleep`/`usleep`/
|
|
126
|
+
`std::this_thread::sleep_for`; join the thread or wait on a
|
|
127
|
+
condition variable/future instead.
|
|
128
|
+
- Run the relevant sanitizer(s) before reporting a memory-safety,
|
|
129
|
+
UB, or concurrency fix verified.
|
|
130
|
+
|
|
131
|
+
## Red Flags
|
|
132
|
+
|
|
133
|
+
| Rationalization | Why it is wrong |
|
|
134
|
+
|---|---|
|
|
135
|
+
| "I'll add a short `sleep_for(100ms)` so the worker thread finishes" | Non-deterministic under load; join the thread or wait on a condition variable so the test cannot flake |
|
|
136
|
+
| "The fix looks right by inspection, I don't need to run ASan again" | Memory-safety and UB bugs are exactly the class of defect that "looks right" while still being wrong; sanitizer re-verification is the actual check |
|
|
137
|
+
| "This test keeps failing; I'll change EXPECT_EQ to EXPECT_NE so it's easier to satisfy" | Weakening the assertion covers nothing about *what* the correct behavior is; find out why the actual value differs from what's expected |
|
|
138
|
+
| "It's just a background thread, TSan is overkill for one test" | A single unguarded shared access is enough to be a data race; TSan is the tool built to catch exactly that at low cost |
|
|
139
|
+
|
|
140
|
+
## Verification
|
|
141
|
+
|
|
142
|
+
Do not report the work done until all of the following hold:
|
|
143
|
+
|
|
144
|
+
- The test file sits at the project's own convention path, matching the
|
|
145
|
+
fixture/assertion style read in Step 1.
|
|
146
|
+
- `ctest --test-dir build --output-on-failure` exits 0 with every
|
|
147
|
+
generated test passing.
|
|
148
|
+
- For a memory-safety, UB, or concurrency fix: the regression test also
|
|
149
|
+
passes under the relevant sanitizer build (ASan+UBSan, or TSan for a
|
|
150
|
+
race).
|
|
151
|
+
- Every thread started by a test is joined before its assertions run; no
|
|
152
|
+
test synchronizes with `sleep`.
|