@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,304 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: cpp-testing
|
|
3
|
+
description: Use this skill when writing or improving C++ tests, choosing a test framework, designing host-side tests for embedded firmware, or running a test-gate / TDD workflow. Triggers on tasks involving GoogleTest, Catch2, doctest, mocking, coverage, or host-test binaries for firmware modules.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# C++ Testing Skill
|
|
7
|
+
|
|
8
|
+
## Overview
|
|
9
|
+
|
|
10
|
+
This skill provides guidance for writing effective C++ tests across the full testing pyramid: unit, integration, host-side, and on-device. It covers framework selection, test layering, host-test patterns for embedded firmware, TDD workflow, coverage targets, mocking strategies, and common anti-patterns.
|
|
11
|
+
|
|
12
|
+
Key use cases:
|
|
13
|
+
- Writing new tests for a C++ module
|
|
14
|
+
- Choosing between GoogleTest, Catch2, and doctest
|
|
15
|
+
- Setting up a host-side test binary that links firmware C++ code without ESP-IDF or FreeRTOS
|
|
16
|
+
- Running a test-gate to verify 80% coverage before merging
|
|
17
|
+
- Applying TDD to firmware policy/payload modules
|
|
18
|
+
|
|
19
|
+
## Test Framework Selection
|
|
20
|
+
|
|
21
|
+
| Framework | Best For | Strengths | Weaknesses |
|
|
22
|
+
|-----------|----------|-----------|------------|
|
|
23
|
+
| **GoogleTest** | Large projects, parametric tests, fixtures | Rich feature set, wide adoption, `TEST_P`, `TEST_F`, death tests | Verbose, heavy |
|
|
24
|
+
| **Catch2** | Quick to write, expressive | Single-header, BDD-style, very low boilerplate | Slower compile, less `gtest` ecosystem |
|
|
25
|
+
| **doctest** | Header-only, compile-time speed | Fast compile, lightweight, C++17 | Less ecosystem, newer |
|
|
26
|
+
|
|
27
|
+
**Recommendation:** Default to GoogleTest for embedded/firmware projects — its `TEST_P`/`TEST_F` and death tests are well-suited to policy modules. Use Catch2 when you want minimal friction for small utilities. Use doctest when compile speed is critical.
|
|
28
|
+
|
|
29
|
+
See `references/framework-compare.md` for code samples.
|
|
30
|
+
|
|
31
|
+
## Test Layering
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
┌─────────────────────────────────────┐
|
|
35
|
+
│ Host-Side Integration Tests │ ← Binary links firmware .cpp, tests policy/payload
|
|
36
|
+
├─────────────────────────────────────┤
|
|
37
|
+
│ On-Device Integration Tests │ ← Runs on target; tests HW / interrupts
|
|
38
|
+
├─────────────────────────────────────┤
|
|
39
|
+
│ Unit Tests (isolated) │ ← Pure C++, mocked deps, fast
|
|
40
|
+
└─────────────────────────────────────┘
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
- **Unit tests:** Test a single class/function in isolation. Link a `.cpp` directly into the test binary with mocked dependencies.
|
|
44
|
+
- **Host-side integration tests:** Compile a `test_*.cpp` binary that links one or more firmware modules from `main/` or `components/`. No ESP-IDF, no FreeRTOS. Use for policy, payload, and datacollector modules.
|
|
45
|
+
- **On-device tests:** Run on the actual hardware. Require `idf.py test` and device flashing.
|
|
46
|
+
|
|
47
|
+
See `references/host-test-for-embedded.md` for the CMake setup pattern.
|
|
48
|
+
|
|
49
|
+
## Host-Side Test Pattern for Embedded Firmware
|
|
50
|
+
|
|
51
|
+
The goal: build a standalone Linux binary that links firmware C++ code and runs assertions — no ESP-IDF, no FreeRTOS.
|
|
52
|
+
|
|
53
|
+
**CMake pattern (minimal):**
|
|
54
|
+
|
|
55
|
+
```cmake
|
|
56
|
+
# test/host/CMakeLists.txt
|
|
57
|
+
cmake_minimum_required(VERSION 3.16)
|
|
58
|
+
project(host_feature_flags_test)
|
|
59
|
+
|
|
60
|
+
add_executable(test_feature_flags
|
|
61
|
+
${CMAKE_CURRENT_SOURCE_DIR}/test_feature_flags.cpp
|
|
62
|
+
${FIRMWARE_ROOT}/main/policy/feature_flags.cpp
|
|
63
|
+
)
|
|
64
|
+
target_compile_features(test_feature_flags PRIVATE cxx_std_17)
|
|
65
|
+
target_link_libraries(test_feature_flags PRIVATE gtest gmock pthread)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
**Test source pattern:**
|
|
69
|
+
|
|
70
|
+
```cpp
|
|
71
|
+
// test/host/test_feature_flags.cpp
|
|
72
|
+
#include <gtest/gtest.h>
|
|
73
|
+
#include "feature_flags.h" // firmware header — no ESP-IDF, no FreeRTOS
|
|
74
|
+
|
|
75
|
+
// Mock any firmware-freeRTOS deps via abstract interface or std::function injection
|
|
76
|
+
namespace mock {
|
|
77
|
+
std::function<bool(const char*)> is_active_override;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
bool is_feature_active(const char* name) {
|
|
81
|
+
if (mock::is_active_override) return mock::is_active_override(name);
|
|
82
|
+
return feature_flags_impl(name); // call real impl
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
TEST(FeatureFlags, returns_true_for_known_feature) {
|
|
86
|
+
mock::is_active_override = [](const char* n) { return strcmp(n, "debug") == 0; };
|
|
87
|
+
EXPECT_TRUE(is_feature_active("debug"));
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
See `references/host-test-for-embedded.md` for full examples testing `feature_flags.cpp`, `resp_fallback.cpp`, and `datacollector.cpp`.
|
|
92
|
+
|
|
93
|
+
## TDD Workflow for C++
|
|
94
|
+
|
|
95
|
+
1. **Red** — Write a failing test first. Compile and run; confirm it fails for the right reason.
|
|
96
|
+
2. **Green** — Write the minimum production code to make the test pass. No optimization yet.
|
|
97
|
+
3. **Refactor** — Clean up production and test code. Re-run tests to confirm they still pass.
|
|
98
|
+
4. **Repeat** — Add the next test case.
|
|
99
|
+
|
|
100
|
+
**Tips for C++ TDD:**
|
|
101
|
+
- Keep tests compilable at all times (even if currently failing).
|
|
102
|
+
- Use `ASSERT_*` when continued execution after failure is meaningless.
|
|
103
|
+
- Write one logical assertion per test (multiple `EXPECT_*` is fine).
|
|
104
|
+
- Name tests `Subject_Under_Test_Behavior_Expected`. Example: `RespFallback_returns_original_when_fallback_unavailable`.
|
|
105
|
+
|
|
106
|
+
See `references/tdd-workflow.md` for a step-by-step walkthrough.
|
|
107
|
+
|
|
108
|
+
## Coverage
|
|
109
|
+
|
|
110
|
+
**80% line coverage** is the default gate. For a test-gate to pass:
|
|
111
|
+
- At least 80% of all `.cpp` files in `main/` and `components/` must be covered.
|
|
112
|
+
- **Exclude:** mock files (`*_mock.cpp`, `*_mock.h`), generated code, third-party sources.
|
|
113
|
+
- Coverage is measured with `gcov` / `lcov`. Run: `cmake --build build -- -j && lcov --capture --directory . --output coverage.info --exclude '*/mocks/*' --exclude '*/build/*'`
|
|
114
|
+
|
|
115
|
+
**What to cover:**
|
|
116
|
+
- All public class methods (including error paths)
|
|
117
|
+
- All `if/else` and `switch` branches
|
|
118
|
+
- Edge cases: empty input, null pointers, boundary values, error returns
|
|
119
|
+
- Negative tests: verify the code fails gracefully for bad inputs
|
|
120
|
+
|
|
121
|
+
See `references/coverage.md` for `gcov`/`lcov` setup and exclusion patterns.
|
|
122
|
+
|
|
123
|
+
## Mocking Strategies
|
|
124
|
+
|
|
125
|
+
### 1. Abstract Interface (Dependency Inversion)
|
|
126
|
+
|
|
127
|
+
Define a pure virtual interface and inject a test double:
|
|
128
|
+
|
|
129
|
+
```cpp
|
|
130
|
+
// interfaces.h
|
|
131
|
+
struct IFeatureStorage {
|
|
132
|
+
virtual bool read(const char* key, std::string& out) = 0;
|
|
133
|
+
virtual ~IFeatureStorage() = default;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
// production code uses IFeatureStorage*
|
|
137
|
+
class FeatureFlags {
|
|
138
|
+
public:
|
|
139
|
+
explicit FeatureFlags(IFeatureStorage* storage) : storage_(storage) {}
|
|
140
|
+
bool is_active(const char* name);
|
|
141
|
+
private:
|
|
142
|
+
IFeatureStorage* storage_;
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
// test double
|
|
146
|
+
struct FakeStorage : IFeatureStorage {
|
|
147
|
+
bool read(const char* key, std::string& out) override {
|
|
148
|
+
if (strcmp(key, "debug") == 0) { out = "true"; return true; }
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
TEST(FeatureFlags, uses_injected_storage) {
|
|
154
|
+
FakeStorage fake;
|
|
155
|
+
FeatureFlags ff(&fake);
|
|
156
|
+
EXPECT_TRUE(ff.is_active("debug"));
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### 2. Link-Time Seam
|
|
161
|
+
|
|
162
|
+
Compile the module under test with a fake implementation at link time. Replace the real `*.cpp` with a test stub. Useful when you cannot modify the source or inject dependencies:
|
|
163
|
+
|
|
164
|
+
```cmake
|
|
165
|
+
# Link test stub instead of production implementation
|
|
166
|
+
target_sources(test_feature_flags PRIVATE fake_feature_flags.cpp)
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### 3. `std::function` Injection
|
|
170
|
+
|
|
171
|
+
Pass behavior as `std::function` for runtime flexibility:
|
|
172
|
+
|
|
173
|
+
```cpp
|
|
174
|
+
class RespFallback {
|
|
175
|
+
public:
|
|
176
|
+
using response_check_t = std::function<bool(const Response&)>;
|
|
177
|
+
explicit RespFallback(response_check_t checker) : checker_(std::move(checker)) {}
|
|
178
|
+
// ...
|
|
179
|
+
};
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
This is the lightest option — no interface hierarchy needed.
|
|
183
|
+
|
|
184
|
+
See `references/mocking.md` for detailed patterns and tradeoffs.
|
|
185
|
+
|
|
186
|
+
## Fixtures and TEST_F / TEST_P
|
|
187
|
+
|
|
188
|
+
### TEST_F (Fixture-based)
|
|
189
|
+
|
|
190
|
+
Use `TEST_F` when multiple tests share setup/teardown logic:
|
|
191
|
+
|
|
192
|
+
```cpp
|
|
193
|
+
class DatacollectorTest : public ::testing::Test {
|
|
194
|
+
protected:
|
|
195
|
+
void SetUp() override { collector_ = std::make_unique<Datacollector>(); }
|
|
196
|
+
void TearDown() override { collector_.reset(); }
|
|
197
|
+
std::unique_ptr<Datacollector> collector_;
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
TEST_F(DatacollectorTest, appends_reading) {
|
|
201
|
+
collector_->append(42.0);
|
|
202
|
+
EXPECT_DOUBLE_EQ(collector_->latest(), 42.0);
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### TEST_P (Parametric)
|
|
207
|
+
|
|
208
|
+
Use `TEST_P` when the same test logic applies to multiple parameter sets:
|
|
209
|
+
|
|
210
|
+
```cpp
|
|
211
|
+
class RespFallbackParamTest : public ::testing::TestWithParam<const char*> {};
|
|
212
|
+
|
|
213
|
+
TEST_P(RespFallbackParamTest, handles_named_response) {
|
|
214
|
+
const char* name = GetParam();
|
|
215
|
+
RespFallback rf;
|
|
216
|
+
EXPECT_NO_THROW(rf.process(name));
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
INSTANTIATE_TEST_SUITE_P(NamedResponses, RespFallbackParamTest,
|
|
220
|
+
::testing::Values("keepalive", "chunked", "gzip"));
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
## Assertion Choice: EXPECT_* vs ASSERT_*
|
|
224
|
+
|
|
225
|
+
| Macro | Behavior on failure | Use when... |
|
|
226
|
+
|-------|---------------------|-------------|
|
|
227
|
+
| `EXPECT_*` | Continues test execution | Primary assertions; multiple related checks |
|
|
228
|
+
| `ASSERT_*` | Aborts immediately | Precondition failure; continuing is meaningless |
|
|
229
|
+
|
|
230
|
+
**Rule:** Default to `EXPECT_*`. Use `ASSERT_*` when failure would make subsequent assertions invalid (e.g., null pointer dereference, file open failure).
|
|
231
|
+
|
|
232
|
+
## Test Naming Convention
|
|
233
|
+
|
|
234
|
+
Follow the pattern: `Subject_Under_Test_Behavior_Expected`
|
|
235
|
+
|
|
236
|
+
```
|
|
237
|
+
RespFallback_returns_original_when_fallback_unavailable
|
|
238
|
+
FeatureFlags_is_active_returns_true_for_enabled_feature
|
|
239
|
+
Datacollector_latest_throws_when_collection_empty
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
Avoid: `test1`, `TestFeatureFlags`, `test_is_active` — they don't communicate intent.
|
|
243
|
+
|
|
244
|
+
## Common Anti-Patterns
|
|
245
|
+
|
|
246
|
+
1. **Testing private methods** — Test the public interface only. Private method changes break tests unnecessarily.
|
|
247
|
+
2. **Overspecifying** — Don't assert on exact ordering unless that's the contract. Prefer behavioral assertions.
|
|
248
|
+
3. **Missing negative tests** — Always test the error/edge path, not just the happy path.
|
|
249
|
+
4. **Global state leakage** — Each test must be independent. Reset shared state in `SetUp()`.
|
|
250
|
+
5. **Mocking too much** — If you're mocking everything, you're not testing the real code. Prefer integration tests for module interactions.
|
|
251
|
+
6. **Flaky tests** — Avoid timing, threading races, and filesystem dependencies in unit tests.
|
|
252
|
+
|
|
253
|
+
## Do / Don't
|
|
254
|
+
|
|
255
|
+
### Do
|
|
256
|
+
|
|
257
|
+
```cpp
|
|
258
|
+
// DO: Use descriptive test names
|
|
259
|
+
TEST(FeatureFlags, is_active_returns_false_for_unknown_feature) {
|
|
260
|
+
EXPECT_FALSE(flags.is_active("nonexistent"));
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// DO: Use ASSERT_* for preconditions
|
|
264
|
+
TEST(Datacollector, latest_throws_when_empty) {
|
|
265
|
+
ASSERT_FALSE(collector->has_readings()); // guard before calling latest()
|
|
266
|
+
EXPECT_THROW(collector->latest(), std::runtime_error);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// DO: Test through the public interface
|
|
270
|
+
TEST(RespFallback, returns_original_when_fallback_missing) {
|
|
271
|
+
RespFallback rf;
|
|
272
|
+
Response out = rf.process(Response{"original"});
|
|
273
|
+
EXPECT_EQ(out.payload, "original");
|
|
274
|
+
}
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
### Don't
|
|
278
|
+
|
|
279
|
+
```cpp
|
|
280
|
+
// DON'T: Test private methods — test the public API instead
|
|
281
|
+
TEST(FeatureFlags, DISABLED_test_internal_state) { /* ... */ }
|
|
282
|
+
|
|
283
|
+
// DON'T: Overspecify — don't assert on internal state
|
|
284
|
+
TEST(FeatureFlags, is_active_caches_result) { // fragile, tests implementation
|
|
285
|
+
flags.is_active("debug");
|
|
286
|
+
EXPECT_EQ(flags.cache_size(), 1); // too specific
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// DON'T: Forget negative tests
|
|
290
|
+
TEST(FeatureFlags, is_active_handles_nullptr) {
|
|
291
|
+
// Always test error paths
|
|
292
|
+
EXPECT_FALSE(flags.is_active(nullptr));
|
|
293
|
+
}
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
## References
|
|
297
|
+
|
|
298
|
+
Detailed guides on specific topics:
|
|
299
|
+
|
|
300
|
+
- **`references/framework-compare.md`** — GoogleTest vs Catch2 vs doctest with code samples
|
|
301
|
+
- **`references/host-test-for-embedded.md`** — CMake host-test setup, testing policy modules without ESP-IDF
|
|
302
|
+
- **`references/mocking.md`** — Abstract interfaces, link-time seams, `std::function` injection
|
|
303
|
+
- **`references/tdd-workflow.md`** — Red-green-refactor walkthrough in C++
|
|
304
|
+
- **`references/coverage.md`** — gcov/lcov setup, 80% gate, what to exclude
|
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
# Coverage for C++ Tests
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
Coverage measures which lines of production code are executed by your tests. The 80% line coverage gate means at least 80% of non-excluded lines must be covered before merging. This reference covers measurement tools, interpretation, exclusion patterns, and enforcement.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Tools
|
|
10
|
+
|
|
11
|
+
| Tool | Description | Notes |
|
|
12
|
+
|------|-------------|-------|
|
|
13
|
+
| **gcov** | GCC's coverage tool — comes with `gcc` | Works with any CMake project compiled with `-fprofile-arcs -ftest-coverage` |
|
|
14
|
+
| **lcov** | Front-end for gcov — generates HTML/JSON reports | `apt install lcov` |
|
|
15
|
+
| **gcovr** | Alternative — generates Cobertura XML for CI | `pip install gcovr` |
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Setup
|
|
20
|
+
|
|
21
|
+
### CMake Integration
|
|
22
|
+
|
|
23
|
+
```cmake
|
|
24
|
+
# test/host/CMakeLists.txt
|
|
25
|
+
option(COVERAGE "Enable coverage reporting" OFF)
|
|
26
|
+
|
|
27
|
+
if(COVERAGE)
|
|
28
|
+
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fprofile-arcs -ftest-coverage")
|
|
29
|
+
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fprofile-arcs -ftest-coverage")
|
|
30
|
+
endif()
|
|
31
|
+
|
|
32
|
+
# link gtest normally
|
|
33
|
+
target_link_libraries(test_policy PRIVATE gtest gmock pthread)
|
|
34
|
+
|
|
35
|
+
# on Linux, link gcov explicitly
|
|
36
|
+
if(COVERAGE)
|
|
37
|
+
target_link_libraries(test_policy PRIVATE gcov)
|
|
38
|
+
endif()
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Build and Capture
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
cd test/host && mkdir -p build && cd build
|
|
45
|
+
cmake .. -DCOVERAGE=ON -DCMAKE_BUILD_TYPE=Debug
|
|
46
|
+
cmake --build . -j$(nproc)
|
|
47
|
+
./test_policy # run tests — gcov writes .gcda/.gcno files
|
|
48
|
+
lcov --capture --directory . --output coverage.info \
|
|
49
|
+
--exclude '*/test_*' \
|
|
50
|
+
--exclude '*/stubs/*' \
|
|
51
|
+
--exclude '*/build/*' \
|
|
52
|
+
--exclude '*/googletest/*' \
|
|
53
|
+
--exclude '*/.cache/*'
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### View HTML Report
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
genhtml coverage.info --output-directory coverage_html
|
|
60
|
+
# Open coverage_html/index.html in browser
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Check Only Source Files (not test code)
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
# Filter to only main/ and components/ sources
|
|
67
|
+
lcov --extract coverage.info '*/main/*' --output filtered.info
|
|
68
|
+
lcov --list filtered.info
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## What 80% Coverage Means
|
|
74
|
+
|
|
75
|
+
| Metric | Description | Target |
|
|
76
|
+
|--------|-------------|--------|
|
|
77
|
+
| **Line coverage** | Percentage of source lines executed | ≥ 80% |
|
|
78
|
+
| **Branch coverage** | Percentage of branches (if/else, switch) taken | Not explicitly gated, but inspect for untested branches |
|
|
79
|
+
| **Function coverage** | Percentage of functions called | Should be 100% for public APIs |
|
|
80
|
+
|
|
81
|
+
**80% line coverage does NOT mean:**
|
|
82
|
+
- Every function is tested
|
|
83
|
+
- Every branch is exercised
|
|
84
|
+
- The code is bug-free
|
|
85
|
+
|
|
86
|
+
It means 80% of **executable lines** were executed at least once. You still need:
|
|
87
|
+
- Branch coverage for `if/else` and `switch`
|
|
88
|
+
- Negative tests for error paths
|
|
89
|
+
- Boundary value tests
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## Files to Exclude
|
|
94
|
+
|
|
95
|
+
Exclude these from coverage reports:
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
*/test_* — test source files (test_*.cpp)
|
|
99
|
+
*/stubs/* — test stubs (freertos_stubs.cpp, etc.)
|
|
100
|
+
*/build/* — CMake build directory
|
|
101
|
+
*/mocks/* — mock implementations
|
|
102
|
+
*/googletest/* — GoogleTest source
|
|
103
|
+
*/.cache/* — any cache directories
|
|
104
|
+
*/generated/* — auto-generated code
|
|
105
|
+
*/third_party/* — third-party dependencies
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### lcov Exclude Pattern
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
lcov --capture --directory . \
|
|
112
|
+
--output coverage.info \
|
|
113
|
+
--exclude '*/test_*' \
|
|
114
|
+
--exclude '*/stubs/*' \
|
|
115
|
+
--exclude '*/build/*' \
|
|
116
|
+
--exclude '*/googletest/*' \
|
|
117
|
+
--exclude '*/third_party/*'
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## CI Integration: Enforcing the 80% Gate
|
|
123
|
+
|
|
124
|
+
### GitHub Actions
|
|
125
|
+
|
|
126
|
+
```yaml
|
|
127
|
+
- name: Run host tests with coverage
|
|
128
|
+
run: |
|
|
129
|
+
cd test/host
|
|
130
|
+
mkdir -p build && cd build
|
|
131
|
+
cmake .. -DCOVERAGE=ON -DCMAKE_BUILD_TYPE=Debug
|
|
132
|
+
cmake --build . -j$(nproc)
|
|
133
|
+
./test_policy || { cat test_output.log; exit 1; }
|
|
134
|
+
|
|
135
|
+
- name: Capture coverage
|
|
136
|
+
run: |
|
|
137
|
+
lcov --capture --directory test/host/build \
|
|
138
|
+
--output coverage.info \
|
|
139
|
+
--exclude '*/test_*' --exclude '*/stubs/*' --exclude '*/build/*' \
|
|
140
|
+
--exclude '*/googletest/*'
|
|
141
|
+
lcov --extract coverage.info '*/main/*' --output src_coverage.info
|
|
142
|
+
lcov --list src_coverage.info
|
|
143
|
+
|
|
144
|
+
- name: Check 80% gate
|
|
145
|
+
run: |
|
|
146
|
+
total=$(lcov --list src_coverage.info | grep -E "lines\.\.\." | awk '{print $2}' | tr -d '%')
|
|
147
|
+
echo "Coverage: $total%"
|
|
148
|
+
if (( $(echo "$total < 80" | bc -l) )); then
|
|
149
|
+
echo "ERROR: Coverage $total% < 80% gate"
|
|
150
|
+
lcov --list src_coverage.info
|
|
151
|
+
exit 1
|
|
152
|
+
fi
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### GitLab CI
|
|
156
|
+
|
|
157
|
+
```yaml
|
|
158
|
+
coverage-test:
|
|
159
|
+
script:
|
|
160
|
+
- mkdir -p build && cd build
|
|
161
|
+
- cmake .. -DCOVERAGE=ON -DCMAKE_BUILD_TYPE=Debug
|
|
162
|
+
- cmake --build . -j$(nproc)
|
|
163
|
+
- ./test_policy
|
|
164
|
+
- lcov --capture --directory . --output coverage.info \
|
|
165
|
+
--exclude '*/test_*' --exclude '*/stubs/*' --exclude '*/build/*'
|
|
166
|
+
- lcov --extract coverage.info '*/main/*' --output src_coverage.info
|
|
167
|
+
- lcov --list src_coverage.info
|
|
168
|
+
coverage: /lines\.\.\.: (\d+\.\d+)%/
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## Interpreting Coverage Reports
|
|
174
|
+
|
|
175
|
+
### What "covered" means
|
|
176
|
+
|
|
177
|
+
A line is covered if **any test** executes it at least once.
|
|
178
|
+
|
|
179
|
+
```cpp
|
|
180
|
+
bool is_valid(int x) {
|
|
181
|
+
if (x > 0) // line 2 — covered if x=1 tested
|
|
182
|
+
return true; // line 3 — covered if x=1 tested
|
|
183
|
+
return false; // line 5 — covered if x=-1 tested
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### What "not covered" means
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
$ lcov --list filtered.info
|
|
191
|
+
Overall coverage rate:
|
|
192
|
+
lines......: 73.5% (217 / 295)
|
|
193
|
+
branches...: 58.3% (42 / 72)
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
- **lines 73.5%** — below 80%, gate fails
|
|
197
|
+
- **branches 58.3%** — some `if` branches untested; add negative tests
|
|
198
|
+
|
|
199
|
+
### Finding Uncovered Lines
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
genhtml coverage.info --output-directory html --show-details
|
|
203
|
+
# Open html/*/*.cpp.gcov.html — uncovered lines are highlighted red
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Or use `gcovr`:
|
|
207
|
+
|
|
208
|
+
```bash
|
|
209
|
+
gcovr --filter main/ --xml-pretty > coverage.xml
|
|
210
|
+
# coverage.xml can be imported into GitHub PR checks
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
## Coverage Gaps: How to Find and Fix
|
|
216
|
+
|
|
217
|
+
### 1. Uncovered Functions
|
|
218
|
+
|
|
219
|
+
```bash
|
|
220
|
+
lcov --list filtered.info | grep -E "function.*0\.0"
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
**Fix:** Write a test that calls the function.
|
|
224
|
+
|
|
225
|
+
### 2. Uncovered Branches
|
|
226
|
+
|
|
227
|
+
```bash
|
|
228
|
+
lcov --list filtered.info | grep -E "branch.*not executed"
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
**Fix:** Add a test for the untaken branch. Example: if `if (ptr == nullptr)` is untested, add:
|
|
232
|
+
|
|
233
|
+
```cpp
|
|
234
|
+
TEST(Datacollector, append_handles_nullptr) {
|
|
235
|
+
EXPECT_EQ(datacollector_append(nullptr, 42.0), -1);
|
|
236
|
+
}
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### 3. Uncovered Error Paths
|
|
240
|
+
|
|
241
|
+
```cpp
|
|
242
|
+
// production code
|
|
243
|
+
int datacollector_append(Datacollector* dc, double value) {
|
|
244
|
+
if (dc == nullptr) return -1; // ← often untested
|
|
245
|
+
if (dc->count >= MAX) return -1; // ← sometimes untested
|
|
246
|
+
dc->readings[dc->count++] = value;
|
|
247
|
+
return 0;
|
|
248
|
+
}
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
**Fix:** Add tests for both error conditions:
|
|
252
|
+
|
|
253
|
+
```cpp
|
|
254
|
+
TEST(Datacollector, append_returns_error_for_nullptr) {
|
|
255
|
+
EXPECT_EQ(datacollector_append(nullptr, 42.0), -1);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
TEST(Datacollector, append_returns_error_when_full) {
|
|
259
|
+
Datacollector dc;
|
|
260
|
+
for (int i = 0; i < MAX; ++i) datacollector_append(&dc, 0.0);
|
|
261
|
+
EXPECT_EQ(datacollector_append(&dc, 99.0), -1);
|
|
262
|
+
}
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
---
|
|
266
|
+
|
|
267
|
+
## Coverage Exclusions: When and How
|
|
268
|
+
|
|
269
|
+
### Exclude Generated Code
|
|
270
|
+
|
|
271
|
+
```cmake
|
|
272
|
+
# In CMakeLists.txt — mark generated sources
|
|
273
|
+
set_source_files_properties(
|
|
274
|
+
${CMAKE_CURRENT_SOURCE_DIR}/generated/serde_autogen.cpp
|
|
275
|
+
PROPERTIES HEADER_FILE_ONLY ON
|
|
276
|
+
)
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
Or via `lcov --remove`:
|
|
280
|
+
|
|
281
|
+
```bash
|
|
282
|
+
lcov --remove coverage.info '*/generated/*' --output cleaned.info
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
### Exclude Test Stubs
|
|
286
|
+
|
|
287
|
+
```bash
|
|
288
|
+
lcov --remove coverage.info '*/stubs/*' --output cleaned.info
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
### Exclude Platform-Specific Code
|
|
292
|
+
|
|
293
|
+
```cpp
|
|
294
|
+
// coverage: ignore start
|
|
295
|
+
#ifdef ESP_PLATFORM
|
|
296
|
+
// ESP-IDF only
|
|
297
|
+
#endif
|
|
298
|
+
// coverage: ignore end
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
Note: GCC supports `#pragma GCC coverage_options` but it's fragile. Prefer exclusion at the `lcov` level.
|
|
302
|
+
|
|
303
|
+
---
|
|
304
|
+
|
|
305
|
+
## Common Pitfalls
|
|
306
|
+
|
|
307
|
+
### 1. 100% Coverage ≠ Correct Code
|
|
308
|
+
|
|
309
|
+
```cpp
|
|
310
|
+
// Tests achieve 100% coverage but don't test correctness
|
|
311
|
+
TEST(Math, add_returns_something) {
|
|
312
|
+
EXPECT_NE(add(2, 2), 0); // passes for add(2,2)=99 — wrong!
|
|
313
|
+
}
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
**Fix:** Test for **exact expected values**, not just non-zero.
|
|
317
|
+
|
|
318
|
+
### 2. Over-Excluding Files
|
|
319
|
+
|
|
320
|
+
Excluding `*_test.cpp` is fine. Excluding entire modules because they're "hard to test" is a smell — those modules need redesign.
|
|
321
|
+
|
|
322
|
+
### 3. Coverage as a Goal
|
|
323
|
+
|
|
324
|
+
Writing tests to increase coverage % (rather than to verify behavior) leads to:
|
|
325
|
+
- Meaningless tests (e.g., `EXPECT_TRUE(true)`)
|
|
326
|
+
- Skipped edge cases
|
|
327
|
+
- False confidence
|
|
328
|
+
|
|
329
|
+
Coverage is a **minimum bar**, not a target. Aim for **behavioral correctness** first.
|
|
330
|
+
|
|
331
|
+
---
|
|
332
|
+
|
|
333
|
+
## Quick Reference: Coverage Commands
|
|
334
|
+
|
|
335
|
+
```bash
|
|
336
|
+
# Full pipeline
|
|
337
|
+
cd test/host && mkdir -p build && cd build
|
|
338
|
+
cmake .. -DCOVERAGE=ON -DCMAKE_BUILD_TYPE=Debug
|
|
339
|
+
cmake --build . -j$(nproc)
|
|
340
|
+
./test_policy || exit 1
|
|
341
|
+
|
|
342
|
+
# Capture (all files)
|
|
343
|
+
lcov --capture --directory . --output coverage.info \
|
|
344
|
+
--exclude '*/test_*' --exclude '*/stubs/*' --exclude '*/build/*'
|
|
345
|
+
|
|
346
|
+
# Filter to only main/
|
|
347
|
+
lcov --extract coverage.info '*/main/*' --output src_coverage.info
|
|
348
|
+
|
|
349
|
+
# List summary
|
|
350
|
+
lcov --list src_coverage.info
|
|
351
|
+
|
|
352
|
+
# HTML output
|
|
353
|
+
genhtml src_coverage.info --output-directory coverage_html
|
|
354
|
+
|
|
355
|
+
# Enforce 80% gate
|
|
356
|
+
total=$(lcov --list src_coverage.info | grep "lines" | awk '{print $2}' | tr -d '%')
|
|
357
|
+
python3 -c "exit(0 if float('$total') >= 80.0 else 1)" || \
|
|
358
|
+
{ echo "FAIL: coverage $total% < 80%"; lcov --list src_coverage.info; exit 1; }
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
---
|
|
362
|
+
|
|
363
|
+
## Summary
|
|
364
|
+
|
|
365
|
+
- **Measure:** Use `gcov` + `lcov` (or `gcovr`) to capture and report coverage
|
|
366
|
+
- **Exclude:** Test files, stubs, mocks, generated code, third-party deps
|
|
367
|
+
- **Gate:** 80% line coverage on `main/` and `components/` (non-ESP-IDF)
|
|
368
|
+
- **Inspect:** Check branch coverage for untested `if/else` paths
|
|
369
|
+
- **Fix:** Add tests for uncovered lines and branches, not just to raise %
|
|
370
|
+
- **CI:** Enforce the gate in CI before merging
|