@polderlabs/bizar 2.3.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/LICENSE +21 -0
- package/README.md +364 -0
- package/cli/audit.mjs +144 -0
- package/cli/banner.mjs +41 -0
- package/cli/bin.mjs +186 -0
- package/cli/copy.mjs +508 -0
- package/cli/export.mjs +87 -0
- package/cli/init.mjs +147 -0
- package/cli/install.mjs +390 -0
- package/cli/plan-templates.mjs +523 -0
- package/cli/plan.mjs +2087 -0
- package/cli/prompts.mjs +163 -0
- package/cli/update.mjs +273 -0
- package/cli/utils.mjs +153 -0
- package/config/AGENTS.md +282 -0
- package/config/agents/baldr.md +148 -0
- package/config/agents/forseti.md +112 -0
- package/config/agents/frigg.md +101 -0
- package/config/agents/heimdall.md +157 -0
- package/config/agents/hermod.md +144 -0
- package/config/agents/mimir.md +115 -0
- package/config/agents/odin.md +309 -0
- package/config/agents/quick.md +78 -0
- package/config/agents/semble-search.md +44 -0
- package/config/agents/thor.md +97 -0
- package/config/agents/tyr.md +96 -0
- package/config/agents/vidarr.md +100 -0
- package/config/agents/vor.md +140 -0
- package/config/commands/audit.md +1 -0
- package/config/commands/explain.md +1 -0
- package/config/commands/init.md +1 -0
- package/config/commands/learn.md +1 -0
- package/config/commands/pr-review.md +1 -0
- package/config/commands/tailscale-serve.md +96 -0
- package/config/hooks/README.md +29 -0
- package/config/hooks/post-tool-use.md +16 -0
- package/config/hooks/pre-tool-use.md +16 -0
- package/config/opencode.json +52 -0
- package/config/opencode.json.template +52 -0
- package/config/rules/general.md +8 -0
- package/config/rules/git.md +11 -0
- package/config/rules/javascript.md +10 -0
- package/config/rules/python.md +10 -0
- package/config/rules/testing.md +10 -0
- package/config/skills/bizar/README.md +9 -0
- package/config/skills/bizar/SKILL.md +187 -0
- package/config/skills/cpp-coding-standards/README.md +28 -0
- package/config/skills/cpp-coding-standards/SKILL.md +634 -0
- package/config/skills/cpp-coding-standards/agents/openai.yaml +4 -0
- package/config/skills/cpp-coding-standards/references/concurrency.md +320 -0
- package/config/skills/cpp-coding-standards/references/error-handling.md +229 -0
- package/config/skills/cpp-coding-standards/references/memory-safety.md +216 -0
- package/config/skills/cpp-coding-standards/references/modern-idioms.md +282 -0
- package/config/skills/cpp-coding-standards/references/review-checklist.md +96 -0
- package/config/skills/cpp-testing/README.md +28 -0
- package/config/skills/cpp-testing/SKILL.md +304 -0
- package/config/skills/cpp-testing/agents/openai.yaml +4 -0
- package/config/skills/cpp-testing/references/coverage.md +370 -0
- package/config/skills/cpp-testing/references/framework-compare.md +175 -0
- package/config/skills/cpp-testing/references/host-test-for-embedded.md +499 -0
- package/config/skills/cpp-testing/references/mocking.md +364 -0
- package/config/skills/cpp-testing/references/tdd-workflow.md +308 -0
- package/config/skills/embedded-esp-idf/README.md +41 -0
- package/config/skills/embedded-esp-idf/SKILL.md +439 -0
- package/config/skills/embedded-esp-idf/agents/openai.yaml +4 -0
- package/config/skills/embedded-esp-idf/references/freertos-patterns.md +214 -0
- package/config/skills/embedded-esp-idf/references/host-tests.md +164 -0
- package/config/skills/embedded-esp-idf/references/idf-py-commands.md +157 -0
- package/config/skills/embedded-esp-idf/references/kconfig.md +159 -0
- package/config/skills/embedded-esp-idf/references/logging-discipline.md +118 -0
- package/config/skills/embedded-esp-idf/references/memory-and-iram.md +137 -0
- package/config/skills/embedded-esp-idf/references/nvs.md +121 -0
- package/config/skills/embedded-esp-idf/references/packed-structs.md +192 -0
- package/config/skills/embedded-esp-idf/scripts/idf_env.sh +47 -0
- package/config/skills/embedded-esp-idf/scripts/size_check.sh +77 -0
- package/config/skills/self-improvement/SKILL.md +64 -0
- package/package.json +47 -0
- package/templates/plan/htmx.min.js +1 -0
- package/templates/plan/library/bug-investigation.mdx +79 -0
- package/templates/plan/library/decision-record.mdx +71 -0
- package/templates/plan/library/feature-design.mdx +92 -0
- package/templates/plan/meta.json.template +8 -0
- package/templates/plan/plan.canvas.template +1711 -0
- package/templates/plan/plan.html.template +937 -0
- package/templates/plan/plan.mdx.template +46 -0
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
# Mocking Strategies in C++
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
Mocking lets you test a unit in isolation by replacing its dependencies with controlled test doubles. In C++, the three primary strategies are:
|
|
6
|
+
|
|
7
|
+
1. **Abstract Interface** — dependency inversion with a pure virtual interface
|
|
8
|
+
2. **Link-Time Seam** — replace a compiled object at link time
|
|
9
|
+
3. **`std::function` Injection** — pass behavior as a callable at runtime
|
|
10
|
+
|
|
11
|
+
Each has a different trade-off between isolation, compile-time cost, and how much you need to modify existing code.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## 1. Abstract Interface (Dependency Inversion)
|
|
16
|
+
|
|
17
|
+
### Concept
|
|
18
|
+
|
|
19
|
+
Define a pure virtual interface. The class under test accepts a pointer/reference to the interface. In tests, pass a fake implementation.
|
|
20
|
+
|
|
21
|
+
### When to Use
|
|
22
|
+
|
|
23
|
+
- You control the design and can introduce interfaces
|
|
24
|
+
- You need fine-grained control over mock behavior (ON_CALL, EXPECT_CALL)
|
|
25
|
+
- You want to test multiple implementations of the same interface
|
|
26
|
+
|
|
27
|
+
### Code Example
|
|
28
|
+
|
|
29
|
+
```cpp
|
|
30
|
+
// === interfaces/sensor_reading.h ===
|
|
31
|
+
#pragma once
|
|
32
|
+
#include <cstdint>
|
|
33
|
+
#include <optional>
|
|
34
|
+
|
|
35
|
+
struct ISensor {
|
|
36
|
+
virtual ~ISensor() = default;
|
|
37
|
+
virtual std::optional<double> read() = 0;
|
|
38
|
+
virtual bool is_connected() = 0;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// === main/policy/temp_monitor.h ===
|
|
42
|
+
#pragma once
|
|
43
|
+
#include "sensor_reading.h"
|
|
44
|
+
|
|
45
|
+
class TempMonitor {
|
|
46
|
+
public:
|
|
47
|
+
explicit TempMonitor(ISensor* sensor) : sensor_(sensor) {}
|
|
48
|
+
bool has_valid_reading();
|
|
49
|
+
double last_reading();
|
|
50
|
+
private:
|
|
51
|
+
ISensor* sensor_;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// === main/policy/temp_monitor.cpp ===
|
|
55
|
+
#include "temp_monitor.h"
|
|
56
|
+
#include <algorithm>
|
|
57
|
+
|
|
58
|
+
bool TempMonitor::has_valid_reading() {
|
|
59
|
+
return sensor_->is_connected() && sensor_->read().has_value();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
double TempMonitor::last_reading() {
|
|
63
|
+
auto v = sensor_->read();
|
|
64
|
+
return v.value_or(0.0);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// === test/host/test_temp_monitor.cpp ===
|
|
68
|
+
#include <gtest/gtest.h>
|
|
69
|
+
#include "temp_monitor.h"
|
|
70
|
+
#include "sensor_reading.h"
|
|
71
|
+
#include <optional>
|
|
72
|
+
|
|
73
|
+
struct FakeSensor : ISensor {
|
|
74
|
+
std::optional<double> fake_value = std::nullopt;
|
|
75
|
+
bool connected = false;
|
|
76
|
+
|
|
77
|
+
std::optional<double> read() override { return fake_value; }
|
|
78
|
+
bool is_connected() override { return connected; }
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
TEST(TempMonitor, has_valid_reading_true_when_connected_and_has_value) {
|
|
82
|
+
FakeSensor fake;
|
|
83
|
+
fake.connected = true;
|
|
84
|
+
fake.fake_value = 23.5;
|
|
85
|
+
TempMonitor monitor(&fake);
|
|
86
|
+
EXPECT_TRUE(monitor.has_valid_reading());
|
|
87
|
+
EXPECT_DOUBLE_EQ(monitor.last_reading(), 23.5);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
TEST(TempMonitor, has_valid_reading_false_when_disconnected) {
|
|
91
|
+
FakeSensor fake;
|
|
92
|
+
fake.connected = false;
|
|
93
|
+
fake.fake_value = 23.5;
|
|
94
|
+
TempMonitor monitor(&fake);
|
|
95
|
+
EXPECT_FALSE(monitor.has_valid_reading());
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
TEST(TempMonitor, has_valid_reading_false_when_no_value) {
|
|
99
|
+
FakeSensor fake;
|
|
100
|
+
fake.connected = true;
|
|
101
|
+
fake.fake_value = std::nullopt;
|
|
102
|
+
TempMonitor monitor(&fake);
|
|
103
|
+
EXPECT_FALSE(monitor.has_valid_reading());
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Trade-offs
|
|
108
|
+
|
|
109
|
+
| Pros | Cons |
|
|
110
|
+
|------|------|
|
|
111
|
+
| Full control over mock behavior | Requires interface in production code |
|
|
112
|
+
| Works with `gmock` (`ON_CALL`, `EXPECT_CALL`) | Adds indirection to class design |
|
|
113
|
+
| Tests are fully isolated | Can be verbose for simple cases |
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## 2. Link-Time Seam
|
|
118
|
+
|
|
119
|
+
### Concept
|
|
120
|
+
|
|
121
|
+
Compile a different version of a file at link time to replace the production implementation. The test links its own `.cpp` that provides a minimal or fake implementation of the dependency.
|
|
122
|
+
|
|
123
|
+
### When to Use
|
|
124
|
+
|
|
125
|
+
- You cannot modify the existing production code to accept interfaces
|
|
126
|
+
- The dependency is a single global function or static method
|
|
127
|
+
- You want to test a module without any runtime injection mechanism
|
|
128
|
+
|
|
129
|
+
### Code Example
|
|
130
|
+
|
|
131
|
+
```cpp
|
|
132
|
+
// === main/policy/heartbeat.h ===
|
|
133
|
+
#pragma once
|
|
134
|
+
bool heartbeat_send(uint32_t interval_ms);
|
|
135
|
+
|
|
136
|
+
// === main/policy/heartbeat.cpp ===
|
|
137
|
+
#include "heartbeat.h"
|
|
138
|
+
#include "esp_timer.h" // FreeRTOS/ESP-IDF — not available on host
|
|
139
|
+
|
|
140
|
+
bool heartbeat_send(uint32_t interval_ms) {
|
|
141
|
+
esp_timer_start_periodic(..., interval_ms * 1000);
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// === test/host/CMakeLists.txt ===
|
|
146
|
+
# Replace real heartbeat.cpp with stub at link time
|
|
147
|
+
add_executable(test_heartbeat
|
|
148
|
+
test_heartbeat.cpp
|
|
149
|
+
${CMAKE_CURRENT_SOURCE_DIR}/../../main/policy/heartbeat.cpp # production
|
|
150
|
+
${CMAKE_CURRENT_SOURCE_DIR}/stubs/heartbeat_stub.cpp # our fake
|
|
151
|
+
)
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
```cpp
|
|
155
|
+
// === test/host/stubs/heartbeat_stub.cpp ===
|
|
156
|
+
#include "heartbeat.h"
|
|
157
|
+
|
|
158
|
+
bool heartbeat_send(uint32_t interval_ms) {
|
|
159
|
+
// No-op on host — just return true
|
|
160
|
+
(void)interval_ms;
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
```cpp
|
|
166
|
+
// === test/host/test_heartbeat.cpp ===
|
|
167
|
+
#include <gtest/gtest.h>
|
|
168
|
+
#include "heartbeat.h"
|
|
169
|
+
|
|
170
|
+
TEST(Heartbeat, send_returns_true) {
|
|
171
|
+
EXPECT_TRUE(heartbeat_send(1000));
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
TEST(Heartbeat, send_accepts_zero_interval) {
|
|
175
|
+
EXPECT_TRUE(heartbeat_send(0));
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Trade-offs
|
|
180
|
+
|
|
181
|
+
| Pros | Cons |
|
|
182
|
+
|------|------|
|
|
183
|
+
| No production code changes needed | Only works when you control what's linked |
|
|
184
|
+
| Works with any C/C++ function | Can mask integration issues if stub is too fake |
|
|
185
|
+
| Simple to implement | No runtime control — compile-time replacement only |
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## 3. `std::function` Injection
|
|
190
|
+
|
|
191
|
+
### Concept
|
|
192
|
+
|
|
193
|
+
Make behavior injectable via `std::function` member variables set at construction or via setters. No interface hierarchy needed.
|
|
194
|
+
|
|
195
|
+
### When to Use
|
|
196
|
+
|
|
197
|
+
- You want minimal boilerplate
|
|
198
|
+
- The dependency is a simple callable (function, functor, lambda)
|
|
199
|
+
- You don't need `gmock` expectations — simple behavior is enough
|
|
200
|
+
|
|
201
|
+
### Code Example
|
|
202
|
+
|
|
203
|
+
```cpp
|
|
204
|
+
// === main/policy/resp_fallback.h ===
|
|
205
|
+
#pragma once
|
|
206
|
+
#include <functional>
|
|
207
|
+
#include "resp_fallback_types.h" // Response, FallbackResult
|
|
208
|
+
|
|
209
|
+
class RespFallback {
|
|
210
|
+
public:
|
|
211
|
+
using response_check_t = std::function<bool(const Response&)>;
|
|
212
|
+
using fallback_fn_t = std::function<FallbackResult(const Response*, Response*)>;
|
|
213
|
+
|
|
214
|
+
explicit RespFallback(response_check_t checker = nullptr,
|
|
215
|
+
fallback_fn_t fallback_fn = nullptr)
|
|
216
|
+
: checker_(std::move(checker)), fallback_fn_(std::move(fallback_fn)) {}
|
|
217
|
+
|
|
218
|
+
FallbackResult process(const Response* req, Response* out);
|
|
219
|
+
|
|
220
|
+
private:
|
|
221
|
+
response_check_t checker_;
|
|
222
|
+
fallback_fn_t fallback_fn_;
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
// === test/host/test_resp_fallback.cpp ===
|
|
226
|
+
#include <gtest/gtest.h>
|
|
227
|
+
#include "resp_fallback.h"
|
|
228
|
+
|
|
229
|
+
TEST(RespFallback, uses_checker_when_provided) {
|
|
230
|
+
bool checker_called = false;
|
|
231
|
+
RespFallback rf(
|
|
232
|
+
[&checker_called](const Response&) {
|
|
233
|
+
checker_called = true;
|
|
234
|
+
return true;
|
|
235
|
+
},
|
|
236
|
+
nullptr
|
|
237
|
+
);
|
|
238
|
+
Response out;
|
|
239
|
+
Response in = { .payload = "test", .payload_len = 4, .status_code = 500 };
|
|
240
|
+
rf.process(&in, &out);
|
|
241
|
+
EXPECT_TRUE(checker_called);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
TEST(RespFallback, uses_fallback_fn_when_checker_fails) {
|
|
245
|
+
bool fallback_called = false;
|
|
246
|
+
RespFallback rf(
|
|
247
|
+
[](const Response&) { return false; }, // checker returns false
|
|
248
|
+
[&fallback_called](const Response*, Response* out) {
|
|
249
|
+
fallback_called = true;
|
|
250
|
+
out->payload = "fallback";
|
|
251
|
+
out->status_code = 200;
|
|
252
|
+
return FallbackResult::FALLBACK_OK;
|
|
253
|
+
}
|
|
254
|
+
);
|
|
255
|
+
Response out;
|
|
256
|
+
Response in = { .payload = "error", .payload_len = 5, .status_code = 500 };
|
|
257
|
+
FallbackResult r = rf.process(&in, &out);
|
|
258
|
+
EXPECT_TRUE(fallback_called);
|
|
259
|
+
EXPECT_EQ(r, FallbackResult::FALLBACK_OK);
|
|
260
|
+
}
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
### Trade-offs
|
|
264
|
+
|
|
265
|
+
| Pros | Cons |
|
|
266
|
+
|------|------|
|
|
267
|
+
| Zero boilerplate — no interface needed | Cannot use `EXPECT_CALL` / `ON_CALL` with gmock |
|
|
268
|
+
| Lambda-friendly | Lambdas can't be stored in `std::function` if they capture |
|
|
269
|
+
| Works with existing code (add a setter) | Tests must provide real behavior if production doesn't |
|
|
270
|
+
|
|
271
|
+
---
|
|
272
|
+
|
|
273
|
+
## 4. gmock (GoogleMock) — Struct-Level MATCHER
|
|
274
|
+
|
|
275
|
+
When using GoogleTest, `gmock` provides `MATCHER` and `ON_CALL` for fine-grained expectations on structs:
|
|
276
|
+
|
|
277
|
+
```cpp
|
|
278
|
+
#include <gmock/gmock.h>
|
|
279
|
+
#include <gtest/gtest.h>
|
|
280
|
+
|
|
281
|
+
struct Reading {
|
|
282
|
+
double temperature;
|
|
283
|
+
uint32_t timestamp;
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
MATCHER_P(TempNear, expected, "") {
|
|
287
|
+
return std::abs(arg.temperature - expected) < 0.5;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
TEST(Sensor, reading_near_expected) {
|
|
291
|
+
::testing::MockFunction<bool(const Reading&)> mock_check;
|
|
292
|
+
ON_CALL(mock_check, Call)
|
|
293
|
+
.WillByDefault(::testing::Return(true));
|
|
294
|
+
|
|
295
|
+
EXPECT_CALL(mock_check, Call(TempNear(25.0))).Times(1);
|
|
296
|
+
|
|
297
|
+
Reading r = { 25.1, 123456 };
|
|
298
|
+
mock_check.Call(r);
|
|
299
|
+
}
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
For simple structs, `MATCHER` is cleaner than writing a full fake class.
|
|
303
|
+
|
|
304
|
+
---
|
|
305
|
+
|
|
306
|
+
## Choosing a Strategy
|
|
307
|
+
|
|
308
|
+
| Situation | Recommended Strategy |
|
|
309
|
+
|-----------|---------------------|
|
|
310
|
+
| New code, you control design | Abstract Interface + `gmock` |
|
|
311
|
+
| Testing existing code with no interface | Link-Time Seam |
|
|
312
|
+
| Simple callable dependency, no gmock needed | `std::function` Injection |
|
|
313
|
+
| Struct-level assertions | `MATCHER` / `ON_CALL` |
|
|
314
|
+
| Need `EXPECT_CALL` with complex behavior | Abstract Interface + `gmock` |
|
|
315
|
+
| FreeRTOS/ESP-IDF call in production code | Link-Time Seam (stub at link) |
|
|
316
|
+
|
|
317
|
+
---
|
|
318
|
+
|
|
319
|
+
## Anti-Patterns
|
|
320
|
+
|
|
321
|
+
### Don't Over-Mock
|
|
322
|
+
|
|
323
|
+
```cpp
|
|
324
|
+
// BAD — mocking everything means you're not testing real code
|
|
325
|
+
TEST(System, processes_data) {
|
|
326
|
+
auto mock_storage = std::make_unique<MockStorage>();
|
|
327
|
+
auto mock_network = std::make_unique<MockNetwork>();
|
|
328
|
+
ON_CALL(*mock_storage, read).WillByDefault(...);
|
|
329
|
+
ON_CALL(*mock_network, send).WillByDefault(...);
|
|
330
|
+
System sys(std::move(mock_storage), std::move(mock_network));
|
|
331
|
+
// This tests nothing real
|
|
332
|
+
}
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
**Better:** Test `System` with real `Storage` and mocked `Network`, or vice versa. Only mock at boundaries.
|
|
336
|
+
|
|
337
|
+
### Don't Mock Value Types
|
|
338
|
+
|
|
339
|
+
```cpp
|
|
340
|
+
// BAD — no need to mock a simple struct
|
|
341
|
+
struct Config { int timeout_ms; bool enabled; };
|
|
342
|
+
|
|
343
|
+
// GOOD — construct directly, no mock needed
|
|
344
|
+
Config cfg{1000, true};
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
### Don't Mock Stateless Utilities
|
|
348
|
+
|
|
349
|
+
```cpp
|
|
350
|
+
// BAD — mocking a pure function like strlen is wasteful
|
|
351
|
+
// GOOD — just call strlen in tests directly
|
|
352
|
+
EXPECT_EQ(strlen("hello"), 5);
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
---
|
|
356
|
+
|
|
357
|
+
## Combining Strategies
|
|
358
|
+
|
|
359
|
+
You can combine strategies in the same project. For example:
|
|
360
|
+
- Use **Abstract Interface** for `ISensor`, `IStorage` (core domain interfaces)
|
|
361
|
+
- Use **Link-Time Seam** for FreeRTOS / ESP-IDF API calls
|
|
362
|
+
- Use **`std::function`** for simple policy callbacks
|
|
363
|
+
|
|
364
|
+
The key principle: **mock at boundaries, not deep inside the system**.
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
# TDD Workflow for C++
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
Test-Driven Development (TDD) follows a short red-green-refactor cycle:
|
|
6
|
+
|
|
7
|
+
1. **Red** — Write a failing test before touching production code.
|
|
8
|
+
2. **Green** — Write the minimum production code to make the test pass.
|
|
9
|
+
3. **Refactor** — Clean up both test and production code, keeping tests green.
|
|
10
|
+
|
|
11
|
+
The goal is **narrow, fast feedback**: every line of production code has a failing test that motivated it.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## The Cycle in Detail
|
|
16
|
+
|
|
17
|
+
### Step 1: Red — Write a Failing Test
|
|
18
|
+
|
|
19
|
+
Write the smallest possible test that describes the behavior you want:
|
|
20
|
+
|
|
21
|
+
```cpp
|
|
22
|
+
// test/host/test_rate_limiter.cpp
|
|
23
|
+
#include <gtest/gtest.h>
|
|
24
|
+
#include "rate_limiter.h"
|
|
25
|
+
|
|
26
|
+
TEST(RateLimiter, allows_request_under_limit) {
|
|
27
|
+
RateLimiter rl(10); // 10 requests per second
|
|
28
|
+
// First request should be allowed
|
|
29
|
+
EXPECT_TRUE(rl.allow());
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Compile and run. It **must fail** because `rate_limiter.h` / `rate_limiter.cpp` don't exist yet:
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
error: 'RateLimiter' file not found
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
This is the correct outcome — you've specified what you want before building it.
|
|
40
|
+
|
|
41
|
+
### Step 2: Green — Write Minimum Production Code
|
|
42
|
+
|
|
43
|
+
Create the stub to make it compile, then the implementation to make it pass:
|
|
44
|
+
|
|
45
|
+
```cpp
|
|
46
|
+
// main/policy/rate_limiter.h
|
|
47
|
+
#pragma once
|
|
48
|
+
class RateLimiter {
|
|
49
|
+
public:
|
|
50
|
+
explicit RateLimiter(int max_per_second);
|
|
51
|
+
bool allow();
|
|
52
|
+
};
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
```cpp
|
|
56
|
+
// main/policy/rate_limiter.cpp
|
|
57
|
+
#include "rate_limiter.h"
|
|
58
|
+
|
|
59
|
+
RateLimiter::RateLimiter(int max_per_second) {}
|
|
60
|
+
bool RateLimiter::allow() { return true; } // minimum: always allow
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Run tests — they should pass. You now have a compiling, passing test.
|
|
64
|
+
|
|
65
|
+
### Step 3: Refactor
|
|
66
|
+
|
|
67
|
+
Now add the second test:
|
|
68
|
+
|
|
69
|
+
```cpp
|
|
70
|
+
TEST(RateLimiter, blocks_request_over_limit) {
|
|
71
|
+
RateLimiter rl(2);
|
|
72
|
+
rl.allow(); // request 1
|
|
73
|
+
rl.allow(); // request 2
|
|
74
|
+
EXPECT_FALSE(rl.allow()); // request 3 — over limit
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Run tests — this new test **fails** (current impl always returns `true`). Go back to Step 2.
|
|
79
|
+
|
|
80
|
+
```cpp
|
|
81
|
+
// main/policy/rate_limiter.cpp — minimum change
|
|
82
|
+
#include "rate_limiter.h"
|
|
83
|
+
#include <atomic>
|
|
84
|
+
|
|
85
|
+
RateLimiter::RateLimiter(int max_per_second) : max_per_second_(max_per_second), count_(0) {}
|
|
86
|
+
|
|
87
|
+
bool RateLimiter::allow() {
|
|
88
|
+
int current = count_.load();
|
|
89
|
+
if (current >= max_per_second_) return false;
|
|
90
|
+
count_.store(current + 1);
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Run tests — all pass. Repeat the cycle.
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## Structuring Tests for TDD
|
|
100
|
+
|
|
101
|
+
### Arrange-Act-Assert (AAA)
|
|
102
|
+
|
|
103
|
+
```cpp
|
|
104
|
+
TEST(RateLimiter, blocks_over_limit) {
|
|
105
|
+
// Arrange
|
|
106
|
+
RateLimiter rl(1);
|
|
107
|
+
rl.allow(); // consume the one allowed request
|
|
108
|
+
|
|
109
|
+
// Act
|
|
110
|
+
bool allowed = rl.allow();
|
|
111
|
+
|
|
112
|
+
// Assert
|
|
113
|
+
EXPECT_FALSE(allowed);
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### One Logical Assertion Per Test
|
|
118
|
+
|
|
119
|
+
Prefer multiple `EXPECT_*` calls that are all checking related aspects of one behavior over one test per `EXPECT_*`. Example:
|
|
120
|
+
|
|
121
|
+
```cpp
|
|
122
|
+
// Good — one test, multiple related EXPECTs
|
|
123
|
+
TEST(RespFallback, error_returns_fallback_and_sets_status) {
|
|
124
|
+
Response in = { .payload = "error", .payload_len = 5, .status_code = 500 };
|
|
125
|
+
Response out;
|
|
126
|
+
FallbackResult r = resp_fallback_process(&in, &out);
|
|
127
|
+
EXPECT_EQ(r, FALLBACK_OK);
|
|
128
|
+
EXPECT_EQ(out.status_code, 200);
|
|
129
|
+
EXPECT_STREQ(out.payload, "default_response");
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Test Naming for TDD
|
|
134
|
+
|
|
135
|
+
Name tests to describe **behavior**, not implementation:
|
|
136
|
+
|
|
137
|
+
```
|
|
138
|
+
GOOD: RespFallback_returns_fallback_when_upstream_fails
|
|
139
|
+
GOOD: Datacollector_append_returns_minus_one_when_full
|
|
140
|
+
BAD: TestRespFallback (no behavior described)
|
|
141
|
+
BAD: Test1 (meaningless)
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## TDD for Embedded Firmware Modules
|
|
147
|
+
|
|
148
|
+
### Example: `feature_flags.cpp` from scratch
|
|
149
|
+
|
|
150
|
+
**Start:** You have an empty `feature_flags.cpp` with only a header stub.
|
|
151
|
+
|
|
152
|
+
**Test 1:** `is_active_returns_false_for_unknown_feature`
|
|
153
|
+
|
|
154
|
+
```cpp
|
|
155
|
+
TEST(FeatureFlags, is_active_returns_false_for_unknown_feature) {
|
|
156
|
+
EXPECT_FALSE(feature_flags_is_active("nonexistent"));
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Compile → link error (no implementation). Add stub:
|
|
161
|
+
|
|
162
|
+
```cpp
|
|
163
|
+
bool feature_flags_is_active(const char* name) { return false; }
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Test passes. (You chose the simplest implementation that makes the test pass.)
|
|
167
|
+
|
|
168
|
+
**Test 2:** `is_active_returns_true_for_known_feature`
|
|
169
|
+
|
|
170
|
+
```cpp
|
|
171
|
+
TEST(FeatureFlags, is_active_returns_true_for_known_feature) {
|
|
172
|
+
EXPECT_TRUE(feature_flags_is_active("debug"));
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Test fails (still returns `false`). Implement:
|
|
177
|
+
|
|
178
|
+
```cpp
|
|
179
|
+
bool feature_flags_is_active(const char* name) {
|
|
180
|
+
if (strcmp(name, "debug") == 0) return true;
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Test passes.
|
|
186
|
+
|
|
187
|
+
**Test 3:** `is_active_returns_false_for_nullptr`
|
|
188
|
+
|
|
189
|
+
```cpp
|
|
190
|
+
TEST(FeatureFlags, is_active_returns_false_for_nullptr) {
|
|
191
|
+
EXPECT_FALSE(feature_flags_is_active(nullptr));
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Add the null check. Tests pass.
|
|
196
|
+
|
|
197
|
+
**Refactor:** Replace chain of `if/strcmp` with a lookup table:
|
|
198
|
+
|
|
199
|
+
```cpp
|
|
200
|
+
static bool flags[] = { false, false, false }; // debug=0, trace=1, legacy=2
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
The tests **still pass** because they assert on behavior, not representation.
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
## Red-Green-Refactor in Practice
|
|
208
|
+
|
|
209
|
+
### Red Flags (stop and refactor if you see these)
|
|
210
|
+
|
|
211
|
+
- **Test takes too long to write** — break it into smaller pieces
|
|
212
|
+
- **Test requires many mocks** — the module under test may have too many dependencies (consider interface injection)
|
|
213
|
+
- **Production code can't be tested in isolation** — the module needs refactoring before tests can be written
|
|
214
|
+
- **Tests are brittle** — if renaming a private method breaks a test, you're testing the wrong thing
|
|
215
|
+
|
|
216
|
+
### Refactoring Rules
|
|
217
|
+
|
|
218
|
+
1. **Never change tests to make production code pass.** Change production code to make tests pass.
|
|
219
|
+
2. **Keep tests deterministic.** No random values, no timing dependencies.
|
|
220
|
+
3. **Test behavior, not implementation.** If you refactor internal representation and tests break, the tests were overspecified.
|
|
221
|
+
4. **Run the full test suite after every refactor** — green before, green after.
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
## What to Test First
|
|
226
|
+
|
|
227
|
+
### Priority 1: Happy Path (primary behavior)
|
|
228
|
+
|
|
229
|
+
```cpp
|
|
230
|
+
TEST(RateLimiter, allows_requests_under_limit) { ... }
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### Priority 2: Edge Cases (boundary values)
|
|
234
|
+
|
|
235
|
+
```cpp
|
|
236
|
+
TEST(RateLimiter, allows_zero_limit_as_always_blocked) { ... }
|
|
237
|
+
TEST(RateLimiter, handles_negative_limit) { ... } // invalid input
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### Priority 3: Error Paths
|
|
241
|
+
|
|
242
|
+
```cpp
|
|
243
|
+
TEST(Datacollector, append_returns_error_when_full) { ... }
|
|
244
|
+
TEST(RespFallback, returns_invalid_when_nullptr) { ... }
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### Priority 4: Negative Tests (the "what shouldn't happen")
|
|
248
|
+
|
|
249
|
+
```cpp
|
|
250
|
+
TEST(FeatureFlags, is_active_does_not_crash_on_nullptr) { ... }
|
|
251
|
+
TEST(RateLimiter, does_not_allow_over_limit_regardless_of_speed) { ... }
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
---
|
|
255
|
+
|
|
256
|
+
## Test Coverage in TDD
|
|
257
|
+
|
|
258
|
+
TDD naturally drives coverage high because:
|
|
259
|
+
- Every line of production code was written to satisfy a test
|
|
260
|
+
- You can't add code without a failing test first
|
|
261
|
+
|
|
262
|
+
However, TDD alone doesn't guarantee 80% coverage. Run `lcov` after the full suite to identify untested branches:
|
|
263
|
+
|
|
264
|
+
```bash
|
|
265
|
+
cmake --build build -j$(nproc)
|
|
266
|
+
./test_policy
|
|
267
|
+
lcov --capture --directory build --output coverage.info \
|
|
268
|
+
--exclude '*/test_*' --exclude '*/stubs/*' --exclude '*/build/*'
|
|
269
|
+
lcov --list coverage.info # inspect
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
Add missing tests for any uncovered lines before opening a PR.
|
|
273
|
+
|
|
274
|
+
---
|
|
275
|
+
|
|
276
|
+
## When TDD Is Not Worth It
|
|
277
|
+
|
|
278
|
+
- **Trivial accessors** — `getter()` / `setter()` with no logic
|
|
279
|
+
- **Generated code** — auto-generated `serde`, `protobuf` bindings
|
|
280
|
+
- **One-off scripts** — not part of the production codebase
|
|
281
|
+
- **Quick prototypes** — exploratory code that will be thrown away
|
|
282
|
+
|
|
283
|
+
For these, write tests **after** if the code becomes permanent.
|
|
284
|
+
|
|
285
|
+
---
|
|
286
|
+
|
|
287
|
+
## TDD and the 80% Coverage Gate
|
|
288
|
+
|
|
289
|
+
TDD gets you to ~60-70% coverage naturally. The last 10-20% requires disciplined addition of:
|
|
290
|
+
|
|
291
|
+
1. **Branch coverage** — every `if/else` and `switch` branch
|
|
292
|
+
2. **Error paths** — every function that can return an error code
|
|
293
|
+
3. **Negative tests** — every `if (ptr == nullptr)` path
|
|
294
|
+
|
|
295
|
+
Use `lcov --list coverage.info | grep -E "BRAN|BR" ` to find uncovered branches.
|
|
296
|
+
|
|
297
|
+
---
|
|
298
|
+
|
|
299
|
+
## Summary: TDD Checklist
|
|
300
|
+
|
|
301
|
+
Before each commit:
|
|
302
|
+
- [ ] Every new public method has at least one test
|
|
303
|
+
- [ ] Tests follow AAA structure
|
|
304
|
+
- [ ] Test names describe behavior (Subject_Behavior_Expected)
|
|
305
|
+
- [ ] Tests pass on first run (green)
|
|
306
|
+
- [ ] No test is skipped (`DISABLED_`) unless there's a tracked issue
|
|
307
|
+
- [ ] 80% line coverage gate passes (`lcov`)
|
|
308
|
+
- [ ] No new `// TODO` or `// FIXME` in test code
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Embedded ESP-IDF Skill
|
|
2
|
+
|
|
3
|
+
ESP-IDF v5.x C++ firmware patterns for opencode. Covers the `idf.py` workflow, FreeRTOS, IRAM/DRAM/PSRAM memory model, packed binary protocols with `static_assert`, Kconfig, drivers (I2C/SPI/GPIO/ADC/NVS/BLE/ESP-NOW), power management, and host-side tests that compile firmware headers without `idf.py`.
|
|
4
|
+
|
|
5
|
+
## What it provides
|
|
6
|
+
|
|
7
|
+
- **SKILL.md** — quick start + 7 deep-dive references + 2 helper scripts
|
|
8
|
+
- **references/idf-py-commands.md** — every `idf.py` subcommand + exit codes
|
|
9
|
+
- **references/freertos-patterns.md** — task lifecycle, ISR-to-task handoff, ringbuffer streams
|
|
10
|
+
- **references/memory-and-iram.md** — IRAM/DRAM/PSRAM model, `IRAM_ATTR`, `MALLOC_CAP_*`
|
|
11
|
+
- **references/kconfig.md** — `depends on` / `select` / `imply` / `range` / `choice`
|
|
12
|
+
- **references/packed-structs.md** — `__attribute__((packed))` + `static_assert(sizeof == N)`
|
|
13
|
+
- **references/logging-discipline.md** — `ESP_LOG*` + low-rate aggregate lines
|
|
14
|
+
- **references/host-tests.md** — host tests without `idf.py`
|
|
15
|
+
- **scripts/idf_env.sh** — sources the vendored ESP-IDF env, validates `idf.py` is reachable
|
|
16
|
+
- **scripts/size_check.sh** — runs the project size budget check (AMS7-aware) or falls back to `idf.py size`
|
|
17
|
+
|
|
18
|
+
## When it triggers
|
|
19
|
+
|
|
20
|
+
- Writing, reviewing, or debugging ESP-IDF C++ firmware
|
|
21
|
+
- Working with `idf.py build`/`flash`/`monitor`/`menuconfig`/`size`
|
|
22
|
+
- FreeRTOS task/queue/semaphore/mutex/ISR work
|
|
23
|
+
- IRAM/DRAM/PSRAM budgeting
|
|
24
|
+
- Packed binary protocols with `static_assert` on `sizeof`
|
|
25
|
+
- NVS, BLE/NimBLE, ESP-NOW
|
|
26
|
+
- Deep-sleep / light-sleep
|
|
27
|
+
- Adding Kconfig options
|
|
28
|
+
- Host-side C++ unit tests that compile firmware headers without `idf.py`
|
|
29
|
+
|
|
30
|
+
## AMS7-specific extensions
|
|
31
|
+
|
|
32
|
+
This skill was first authored for the AMS7 ambulatory-monitoring firmware at `/projects/ams7_esp32`. Rules tagged `(AMS7)` are project-specific — do not silently generalize them to other ESP-IDF projects. Universal ESP-IDF rules are the default.
|
|
33
|
+
|
|
34
|
+
## Manual install
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
cp -R SKILL.md references scripts ~/.opencode/skills/embedded-esp-idf/
|
|
38
|
+
chmod +x ~/.opencode/skills/embedded-esp-idf/scripts/*.sh
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
The BizarHarness installer can also install this automatically — select the **Embedded ESP-IDF** component.
|