@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,175 @@
|
|
|
1
|
+
# Framework Compare: GoogleTest vs Catch2 vs doctest
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
All three frameworks are C++17-compatible, header-only (or single-header), and support fixtures, parametrics, and mocking. The choice is primarily ergonomic and project-specific.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## GoogleTest
|
|
10
|
+
|
|
11
|
+
**Installation:** `apt install libgtest-dev` or `FetchContent` CMake include.
|
|
12
|
+
|
|
13
|
+
**Strengths:**
|
|
14
|
+
- `TEST_F` (fixture), `TEST_P` (parametric), death tests, `ScopedFakeTimer`
|
|
15
|
+
- Widest ecosystem: `gmock` for mocking, `gtest-param-test` for parameterized
|
|
16
|
+
- XML/JSON/HTML output for CI integration
|
|
17
|
+
- Well-understood by embedded/firmware teams
|
|
18
|
+
|
|
19
|
+
**Sample:**
|
|
20
|
+
|
|
21
|
+
```cpp
|
|
22
|
+
#include <gtest/gtest.h>
|
|
23
|
+
|
|
24
|
+
class StackTest : public ::testing::Test {
|
|
25
|
+
protected:
|
|
26
|
+
void SetUp() override { stack_.clear(); }
|
|
27
|
+
std::vector<int> stack_;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
TEST_F(StackTest, push_increases_size) {
|
|
31
|
+
stack_.push_back(1);
|
|
32
|
+
EXPECT_EQ(stack_.size(), 1u);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
TEST_F(StackTest, pop_returns_last_element) {
|
|
36
|
+
stack_.push_back(1);
|
|
37
|
+
stack_.push_back(2);
|
|
38
|
+
EXPECT_EQ(stack_.back(), 2);
|
|
39
|
+
stack_.pop_back();
|
|
40
|
+
EXPECT_EQ(stack_.size(), 1u);
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
**Parametric example:**
|
|
45
|
+
|
|
46
|
+
```cpp
|
|
47
|
+
class StringEncodeTest : public ::testing::TestWithParam<const char*> {};
|
|
48
|
+
|
|
49
|
+
TEST_P(StringEncodeTest, encodes_known_formats) {
|
|
50
|
+
const char* fmt = GetParam();
|
|
51
|
+
EXPECT_NO_THROW(encode(fmt));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
INSTANTIATE_TEST_SUITE_P(KnownFormats, StringEncodeTest,
|
|
55
|
+
::testing::Values("utf-8", "iso-8859-1", "windows-1252"));
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
**Best for:** Large codebases, embedded firmware, projects needing `gmock`, parametric tests, or death tests.
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Catch2
|
|
63
|
+
|
|
64
|
+
**Installation:** Single header: `curl -L https://github.com/catchorg/Catch2/releases/download/v3.5.4/catch2_with_main.hpp -o include/catch2_with_main.hpp`
|
|
65
|
+
|
|
66
|
+
**Strengths:**
|
|
67
|
+
- BDD-style `SCENARIO` / `GIVEN` / `WHEN` / `THEN` DSL available
|
|
68
|
+
- Minimal boilerplate — no `TEST_F` needed for simple cases
|
|
69
|
+
- Rich assertion syntax: `REQUIRE`, `CHECK`, `REQUIRE_THAT`, `CHECK_THAT`
|
|
70
|
+
|
|
71
|
+
**Sample:**
|
|
72
|
+
|
|
73
|
+
```cpp
|
|
74
|
+
#define CATCH_CONFIG_MAIN
|
|
75
|
+
#include <catch2/catch_test_macros.hpp>
|
|
76
|
+
|
|
77
|
+
unsigned int factorial(unsigned int n) {
|
|
78
|
+
return n <= 1 ? 1 : n * factorial(n - 1);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
TEST_CASE("factorial computes correctly", "[factorial]") {
|
|
82
|
+
CHECK(factorial(0) == 1);
|
|
83
|
+
CHECK(factorial(1) == 1);
|
|
84
|
+
CHECK(factorial(4) == 24);
|
|
85
|
+
CHECK(factorial(5) == 120);
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
**BDD-style:**
|
|
90
|
+
|
|
91
|
+
```cpp
|
|
92
|
+
SCENARIO("stack behavior") {
|
|
93
|
+
GIVEN("an empty stack") {
|
|
94
|
+
std::vector<int> stack;
|
|
95
|
+
WHEN("an element is pushed") {
|
|
96
|
+
stack.push_back(42);
|
|
97
|
+
THEN("size increases") { CHECK(stack.size() == 1); }
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
**Best for:** Quick-to-write tests, BDD-style readability, smaller utilities.
|
|
104
|
+
|
|
105
|
+
**Weakness:** Slower compile times than doctest; less ecosystem (no built-in mocking — pair with `trompeloeil` or manual fakes).
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## doctest
|
|
110
|
+
|
|
111
|
+
**Installation:** Single header: `curl -L https://github.com/doctest/doctest/releases/download/v2.4.11/doctest.h -o include/doctest.h`
|
|
112
|
+
|
|
113
|
+
**Strengths:**
|
|
114
|
+
- Fastest compile time of the three
|
|
115
|
+
- Lightweight — minimal impact on build times
|
|
116
|
+
- `CHECK`/`REQUIRE` similar to Catch2
|
|
117
|
+
- Sub-test support via `SUBTEST`
|
|
118
|
+
|
|
119
|
+
**Sample:**
|
|
120
|
+
|
|
121
|
+
```cpp
|
|
122
|
+
#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
|
|
123
|
+
#include <doctest/doctest.h>
|
|
124
|
+
|
|
125
|
+
int add(int a, int b) { return a + b; }
|
|
126
|
+
|
|
127
|
+
TEST_CASE("addition works") {
|
|
128
|
+
CHECK(add(2, 3) == 5);
|
|
129
|
+
CHECK(add(-1, 1) == 0);
|
|
130
|
+
REQUIRE(add(0, 0) == 0); // REQUIRE aborts on failure
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
**Best for:** Compile-time-sensitive projects, small-to-medium codebases, projects that already use many headers.
|
|
135
|
+
|
|
136
|
+
**Weakness:** Smaller ecosystem; mocking requires manual fakes or `trompeloeil`.
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## Side-by-Side Comparison
|
|
141
|
+
|
|
142
|
+
| Feature | GoogleTest | Catch2 | doctest |
|
|
143
|
+
|---------|-----------|--------|---------|
|
|
144
|
+
| Header-only | No (needs gtest lib) | Yes | Yes |
|
|
145
|
+
| Compile speed | Medium | Slow | Fast |
|
|
146
|
+
| `TEST_F` (fixtures) | Yes | Via `SECTION` | Via `SUBCASE` |
|
|
147
|
+
| `TEST_P` (parametric) | Yes | No | No |
|
|
148
|
+
| Death tests | Yes | No | No |
|
|
149
|
+
| Built-in mocking | gmock | No | No |
|
|
150
|
+
| BDD DSL | No | Yes | No |
|
|
151
|
+
| CI XML output | Yes | Yes | Yes |
|
|
152
|
+
| C++17 required | Yes | Yes | Yes |
|
|
153
|
+
| Ecosystem size | Large | Medium | Small |
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## Recommendation for Embedded/Firmware Host Tests
|
|
158
|
+
|
|
159
|
+
Default to **GoogleTest** + **gmock**:
|
|
160
|
+
- `TEST_F` / `TEST_P` are well-suited to firmware policy modules
|
|
161
|
+
- `gmock` provides struct-level mocking via `MATCHER` / `ON_CALL`
|
|
162
|
+
- `EXPECT_DEATH` / `EXPECT_DEATH_IF_SUPPORTED` useful for error-injection tests
|
|
163
|
+
- Widely understood — existing firmware teams will recognize it
|
|
164
|
+
|
|
165
|
+
Use **Catch2** when you want BDD-style readability for payload parsing or protocol tests.
|
|
166
|
+
|
|
167
|
+
Use **doctest** when build time is a bottleneck and you only need simple `CHECK` assertions.
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## Mixing Frameworks
|
|
172
|
+
|
|
173
|
+
It is possible to link multiple frameworks into the same binary, but avoid this — it causes confusion and doubled setup cost. Pick one framework per binary.
|
|
174
|
+
|
|
175
|
+
If you need both BDD-style and parametric tests, use GoogleTest's `TEST_F` with `DESCRIPTION` or use Catch2's `SECTION` mechanism instead of `TEST_P`.
|
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
# Host-Side Tests for Embedded Firmware
|
|
2
|
+
|
|
3
|
+
## Goal
|
|
4
|
+
|
|
5
|
+
Build a standalone Linux binary that links and executes C++ firmware code from `main/` or `components/` — with no ESP-IDF, no FreeRTOS, and no `idf.py`. The binary runs on the host (your laptop or CI runner) and exercises the real production code, just with mocked hardware/OS boundaries.
|
|
6
|
+
|
|
7
|
+
This pattern is ideal for:
|
|
8
|
+
- **Policy modules:** `feature_flags.cpp`, `resp_fallback.cpp`, `rate_limiter.cpp`
|
|
9
|
+
- **Payload parsers:** `mqtt_payload.cpp`, `json_parser.cpp`
|
|
10
|
+
- **Data collectors:** `datacollector.cpp`, `telemetry.cpp`
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Why Host-Side Tests?
|
|
15
|
+
|
|
16
|
+
| On-Device (`idf.py test`) | Host-Side Binary |
|
|
17
|
+
|---------------------------|------------------|
|
|
18
|
+
| Runs on actual hardware | Runs on Linux/CI |
|
|
19
|
+
| Needs device flashing | No hardware required |
|
|
20
|
+
| Slow iteration | Fast iteration |
|
|
21
|
+
| FreeRTOS available | No FreeRTOS |
|
|
22
|
+
| ESP-IDF available | No ESP-IDF |
|
|
23
|
+
| HW timing real | Deterministic |
|
|
24
|
+
|
|
25
|
+
Host-side tests catch >80% of logic bugs before ever flashing. Use on-device tests only for hardware-specific behavior (interrupts, DMA, sensor drivers).
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Project Structure
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
projects/ams7_esp32/
|
|
33
|
+
├── main/
|
|
34
|
+
│ ├── policy/
|
|
35
|
+
│ │ ├── feature_flags.cpp
|
|
36
|
+
│ │ ├── feature_flags.h
|
|
37
|
+
│ │ ├── resp_fallback.cpp
|
|
38
|
+
│ │ └── resp_fallback.h
|
|
39
|
+
│ ├── payload/
|
|
40
|
+
│ │ ├── datacollector.cpp
|
|
41
|
+
│ │ └── datacollector.h
|
|
42
|
+
│ └── utils/
|
|
43
|
+
│ └── hex_utils.cpp
|
|
44
|
+
├── test/ ← your host test directory
|
|
45
|
+
│ └── host/
|
|
46
|
+
│ ├── CMakeLists.txt
|
|
47
|
+
│ ├── test_feature_flags.cpp
|
|
48
|
+
│ ├── test_resp_fallback.cpp
|
|
49
|
+
│ └── test_datacollector.cpp
|
|
50
|
+
└── components/ ← if you have ESP-IDF components
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## CMakeLists.txt Pattern
|
|
56
|
+
|
|
57
|
+
```cmake
|
|
58
|
+
# test/host/CMakeLists.txt
|
|
59
|
+
cmake_minimum_required(VERSION 3.16)
|
|
60
|
+
project(host_policy_tests)
|
|
61
|
+
|
|
62
|
+
set(CMAKE_CXX_STANDARD 17)
|
|
63
|
+
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
|
64
|
+
|
|
65
|
+
# --- GoogleTest ---
|
|
66
|
+
include(FetchContent)
|
|
67
|
+
FetchContent_Declare(
|
|
68
|
+
googletest
|
|
69
|
+
GIT_REPOSITORY https://github.com/google/googletest.git
|
|
70
|
+
GIT_TAG v1.14.0
|
|
71
|
+
)
|
|
72
|
+
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
|
|
73
|
+
FetchContent_MakeAvailable(googletest)
|
|
74
|
+
|
|
75
|
+
# --- Test executable ---
|
|
76
|
+
add_executable(test_policy
|
|
77
|
+
test_feature_flags.cpp
|
|
78
|
+
test_resp_fallback.cpp
|
|
79
|
+
test_datacollector.cpp
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
# --- Link firmware modules under test ---
|
|
83
|
+
# Point FIRMWARE_ROOT at the project root or adjust as needed
|
|
84
|
+
target_include_directories(test_policy PRIVATE
|
|
85
|
+
${CMAKE_CURRENT_SOURCE_DIR}/../../main
|
|
86
|
+
${CMAKE_CURRENT_SOURCE_DIR}/../../main/policy
|
|
87
|
+
${CMAKE_CURRENT_SOURCE_DIR}/../../main/payload
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# Link the real implementation files directly (no ESP-IDF)
|
|
91
|
+
target_sources(test_policy PRIVATE
|
|
92
|
+
${CMAKE_CURRENT_SOURCE_DIR}/../../main/policy/feature_flags.cpp
|
|
93
|
+
${CMAKE_CURRENT_SOURCE_DIR}/../../main/policy/resp_fallback.cpp
|
|
94
|
+
${CMAKE_CURRENT_SOURCE_DIR}/../../main/payload/datacollector.cpp
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
target_link_libraries(test_policy PRIVATE
|
|
98
|
+
gtest
|
|
99
|
+
gmock
|
|
100
|
+
pthread
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
# --- Optional: gcov coverage ---
|
|
104
|
+
if(COVERAGE)
|
|
105
|
+
target_compile_options(test_policy PRIVATE -fprofile-arcs -ftest-coverage)
|
|
106
|
+
target_link_libraries(test_policy PRIVATE gcov)
|
|
107
|
+
endif()
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
**Build and run:**
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
cd test/host
|
|
114
|
+
mkdir build && cd build
|
|
115
|
+
cmake .. -DCOVERAGE=ON
|
|
116
|
+
cmake --build . -j$(nproc)
|
|
117
|
+
./test_policy
|
|
118
|
+
# or with coverage:
|
|
119
|
+
cmake --build . -j$(nproc) && lcov --capture --directory . --output coverage.info --exclude '*/test_*' --exclude '*/build/*'
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## Pattern 1: Testing `feature_flags.cpp`
|
|
125
|
+
|
|
126
|
+
### Firmware header (minimal)
|
|
127
|
+
|
|
128
|
+
```cpp
|
|
129
|
+
// main/policy/feature_flags.h
|
|
130
|
+
#pragma once
|
|
131
|
+
#include <stdbool.h>
|
|
132
|
+
#include <stddef.h>
|
|
133
|
+
|
|
134
|
+
#ifdef __cplusplus
|
|
135
|
+
extern "C" {
|
|
136
|
+
#endif
|
|
137
|
+
|
|
138
|
+
bool feature_flags_is_active(const char* name);
|
|
139
|
+
const char* feature_flags_get_version(void);
|
|
140
|
+
|
|
141
|
+
#ifdef __cplusplus
|
|
142
|
+
}
|
|
143
|
+
#endif
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Firmware implementation (real code)
|
|
147
|
+
|
|
148
|
+
```cpp
|
|
149
|
+
// main/policy/feature_flags.cpp
|
|
150
|
+
#include "feature_flags.h"
|
|
151
|
+
#include <string.h>
|
|
152
|
+
|
|
153
|
+
static bool debug_flag = false;
|
|
154
|
+
static bool trace_flag = false;
|
|
155
|
+
static bool legacy_mode = false;
|
|
156
|
+
|
|
157
|
+
bool feature_flags_is_active(const char* name) {
|
|
158
|
+
if (name == nullptr) return false;
|
|
159
|
+
if (strcmp(name, "debug") == 0) return debug_flag;
|
|
160
|
+
if (strcmp(name, "trace") == 0) return trace_flag;
|
|
161
|
+
if (strcmp(name, "legacy") == 0) return legacy_mode;
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const char* feature_flags_get_version(void) {
|
|
166
|
+
return "1.2.0";
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Host test
|
|
171
|
+
|
|
172
|
+
```cpp
|
|
173
|
+
// test/host/test_feature_flags.cpp
|
|
174
|
+
#include <gtest/gtest.h>
|
|
175
|
+
#include "feature_flags.h"
|
|
176
|
+
#include <string.h>
|
|
177
|
+
|
|
178
|
+
TEST(FeatureFlags, is_active_returns_false_for_nullptr) {
|
|
179
|
+
EXPECT_FALSE(feature_flags_is_active(nullptr));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
TEST(FeatureFlags, is_active_returns_false_for_unknown_feature) {
|
|
183
|
+
EXPECT_FALSE(feature_flags_is_active("nonexistent"));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
TEST(FeatureFlags, is_active_returns_true_for_known_enabled_feature) {
|
|
187
|
+
// In real firmware these might be set at boot; for host test
|
|
188
|
+
// we test the implementation directly — no mocking needed here
|
|
189
|
+
// because the flags are compile-time false in this build.
|
|
190
|
+
EXPECT_FALSE(feature_flags_is_active("debug")); // currently disabled
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
TEST(FeatureFlags, get_version_returns_semver_string) {
|
|
194
|
+
const char* ver = feature_flags_get_version();
|
|
195
|
+
ASSERT_NE(nullptr, ver);
|
|
196
|
+
EXPECT_STREQ("1.2.0", ver);
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
## Pattern 2: Testing `resp_fallback.cpp` with Mocked Response
|
|
203
|
+
|
|
204
|
+
### Firmware header
|
|
205
|
+
|
|
206
|
+
```cpp
|
|
207
|
+
// main/policy/resp_fallback.h
|
|
208
|
+
#pragma once
|
|
209
|
+
#include <stddef.h>
|
|
210
|
+
|
|
211
|
+
typedef struct {
|
|
212
|
+
const char* payload;
|
|
213
|
+
size_t payload_len;
|
|
214
|
+
int status_code;
|
|
215
|
+
} Response;
|
|
216
|
+
|
|
217
|
+
typedef enum {
|
|
218
|
+
FALLBACK_OK,
|
|
219
|
+
FALLBACK_MISSING,
|
|
220
|
+
FALLBACK_INVALID
|
|
221
|
+
} FallbackResult;
|
|
222
|
+
|
|
223
|
+
FallbackResult resp_fallback_process(const Response* req, Response* out);
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### Firmware implementation
|
|
227
|
+
|
|
228
|
+
```cpp
|
|
229
|
+
// main/policy/resp_fallback.cpp
|
|
230
|
+
#include "resp_fallback.h"
|
|
231
|
+
#include <string.h>
|
|
232
|
+
|
|
233
|
+
static const char* s_fallback_payload = "default_response";
|
|
234
|
+
static bool s_fallback_available = true;
|
|
235
|
+
|
|
236
|
+
void resp_fallback_set_fallback_available(bool available) {
|
|
237
|
+
s_fallback_available = available;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
FallbackResult resp_fallback_process(const Response* req, Response* out) {
|
|
241
|
+
if (req == nullptr || out == nullptr) return FALLBACK_INVALID;
|
|
242
|
+
if (req->status_code >= 200 && req->status_code < 300) {
|
|
243
|
+
// Pass-through for success codes
|
|
244
|
+
out->payload = req->payload;
|
|
245
|
+
out->payload_len = req->payload_len;
|
|
246
|
+
out->status_code = req->status_code;
|
|
247
|
+
return FALLBACK_OK;
|
|
248
|
+
}
|
|
249
|
+
// Error code — try fallback
|
|
250
|
+
if (!s_fallback_available) return FALLBACK_MISSING;
|
|
251
|
+
out->payload = s_fallback_payload;
|
|
252
|
+
out->payload_len = strlen(s_fallback_payload);
|
|
253
|
+
out->status_code = 200;
|
|
254
|
+
return FALLBACK_OK;
|
|
255
|
+
}
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### Host test with `std::function` injection
|
|
259
|
+
|
|
260
|
+
```cpp
|
|
261
|
+
// test/host/test_resp_fallback.cpp
|
|
262
|
+
#include <gtest/gtest.h>
|
|
263
|
+
#include "resp_fallback.h"
|
|
264
|
+
#include <cstring>
|
|
265
|
+
|
|
266
|
+
class RespFallbackTest : public ::testing::Test {
|
|
267
|
+
protected:
|
|
268
|
+
void SetUp() override {
|
|
269
|
+
resp_fallback_set_fallback_available(true);
|
|
270
|
+
}
|
|
271
|
+
void TearDown() override {
|
|
272
|
+
resp_fallback_set_fallback_available(true);
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
TEST_F(RespFallbackTest, success_codes_pass_through) {
|
|
277
|
+
Response in = { .payload = "ok", .payload_len = 2, .status_code = 200 };
|
|
278
|
+
Response out;
|
|
279
|
+
FallbackResult r = resp_fallback_process(&in, &out);
|
|
280
|
+
EXPECT_EQ(r, FALLBACK_OK);
|
|
281
|
+
EXPECT_EQ(out.status_code, 200);
|
|
282
|
+
EXPECT_STREQ(out.payload, "ok");
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
TEST_F(RespFallbackTest, error_code_returns_fallback) {
|
|
286
|
+
Response in = { .payload = "error", .payload_len = 5, .status_code = 500 };
|
|
287
|
+
Response out;
|
|
288
|
+
FallbackResult r = resp_fallback_process(&in, &out);
|
|
289
|
+
EXPECT_EQ(r, FALLBACK_OK);
|
|
290
|
+
EXPECT_EQ(out.status_code, 200);
|
|
291
|
+
EXPECT_STREQ(out.payload, "default_response");
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
TEST_F(RespFallbackTest, error_code_when_fallback_unavailable_returns_missing) {
|
|
295
|
+
resp_fallback_set_fallback_available(false);
|
|
296
|
+
Response in = { .payload = "error", .payload_len = 5, .status_code = 500 };
|
|
297
|
+
Response out;
|
|
298
|
+
FallbackResult r = resp_fallback_process(&in, &out);
|
|
299
|
+
EXPECT_EQ(r, FALLBACK_MISSING);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
TEST_F(RespFallbackTest, null_request_returns_invalid) {
|
|
303
|
+
Response out;
|
|
304
|
+
EXPECT_EQ(resp_fallback_process(nullptr, &out), FALLBACK_INVALID);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
TEST_F(RespFallbackTest, null_output_returns_invalid) {
|
|
308
|
+
Response in = { .payload = "ok", .payload_len = 2, .status_code = 200 };
|
|
309
|
+
EXPECT_EQ(resp_fallback_process(&in, nullptr), FALLBACK_INVALID);
|
|
310
|
+
}
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
---
|
|
314
|
+
|
|
315
|
+
## Pattern 3: Testing `datacollector.cpp`
|
|
316
|
+
|
|
317
|
+
### Firmware header
|
|
318
|
+
|
|
319
|
+
```cpp
|
|
320
|
+
// main/payload/datacollector.h
|
|
321
|
+
#pragma once
|
|
322
|
+
#include <stddef.h>
|
|
323
|
+
#include <stdint.h>
|
|
324
|
+
|
|
325
|
+
#define DATACOLLECTOR_MAX_READINGS 64
|
|
326
|
+
|
|
327
|
+
typedef struct {
|
|
328
|
+
double readings[DATACOLLECTOR_MAX_READINGS];
|
|
329
|
+
size_t count;
|
|
330
|
+
} Datacollector;
|
|
331
|
+
|
|
332
|
+
void datacollector_init(Datacollector* dc);
|
|
333
|
+
int datacollector_append(Datacollector* dc, double value);
|
|
334
|
+
double datacollector_latest(const Datacollector* dc);
|
|
335
|
+
size_t datacollector_count(const Datacollector* dc);
|
|
336
|
+
void datacollector_clear(Datacollector* dc);
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
### Firmware implementation
|
|
340
|
+
|
|
341
|
+
```cpp
|
|
342
|
+
// main/payload/datacollector.cpp
|
|
343
|
+
#include "datacollector.h"
|
|
344
|
+
#include <string.h>
|
|
345
|
+
|
|
346
|
+
void datacollector_init(Datacollector* dc) {
|
|
347
|
+
memset(dc, 0, sizeof(*dc));
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
int datacollector_append(Datacollector* dc, double value) {
|
|
351
|
+
if (dc->count >= DATACOLLECTOR_MAX_READINGS) return -1;
|
|
352
|
+
dc->readings[dc->count++] = value;
|
|
353
|
+
return 0;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
double datacollector_latest(const Datacollector* dc) {
|
|
357
|
+
if (dc->count == 0) return 0.0; // Note: consider throwing instead
|
|
358
|
+
return dc->readings[dc->count - 1];
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
size_t datacollector_count(const Datacollector* dc) {
|
|
362
|
+
return dc->count;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
void datacollector_clear(Datacollector* dc) {
|
|
366
|
+
memset(dc, 0, sizeof(*dc));
|
|
367
|
+
}
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
### Host test
|
|
371
|
+
|
|
372
|
+
```cpp
|
|
373
|
+
// test/host/test_datacollector.cpp
|
|
374
|
+
#include <gtest/gtest.h>
|
|
375
|
+
#include "datacollector.h"
|
|
376
|
+
#include <cmath>
|
|
377
|
+
|
|
378
|
+
class DatacollectorTest : public ::testing::Test {
|
|
379
|
+
protected:
|
|
380
|
+
void SetUp() override { datacollector_init(&dc_); }
|
|
381
|
+
Datacollector dc_;
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
TEST_F(DatacollectorTest, init_clears_readings) {
|
|
385
|
+
EXPECT_EQ(datacollector_count(&dc_), 0u);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
TEST_F(DatacollectorTest, append_increments_count) {
|
|
389
|
+
EXPECT_EQ(datacollector_append(&dc_, 1.0), 0);
|
|
390
|
+
EXPECT_EQ(datacollector_count(&dc_), 1u);
|
|
391
|
+
EXPECT_EQ(datacollector_append(&dc_, 2.0), 0);
|
|
392
|
+
EXPECT_EQ(datacollector_count(&dc_), 2u);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
TEST_F(DatacollectorTest, latest_returns_last_value) {
|
|
396
|
+
datacollector_append(&dc_, 1.0);
|
|
397
|
+
datacollector_append(&dc_, 2.0);
|
|
398
|
+
datacollector_append(&dc_, 3.0);
|
|
399
|
+
EXPECT_DOUBLE_EQ(datacollector_latest(&dc_), 3.0);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
TEST_F(DatacollectorTest, latest_returns_zero_when_empty) {
|
|
403
|
+
// Current impl returns 0 — this documents current behavior
|
|
404
|
+
EXPECT_DOUBLE_EQ(datacollector_latest(&dc_), 0.0);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
TEST_F(DatacollectorTest, append_returns_minus_one_when_full) {
|
|
408
|
+
for (int i = 0; i < DATACOLLECTOR_MAX_READINGS; ++i) {
|
|
409
|
+
EXPECT_EQ(datacollector_append(&dc_, static_cast<double>(i)), 0);
|
|
410
|
+
}
|
|
411
|
+
EXPECT_EQ(datacollector_append(&dc_, 999.0), -1); // full
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
TEST_F(DatacollectorTest, clear_resets_count) {
|
|
415
|
+
datacollector_append(&dc_, 1.0);
|
|
416
|
+
datacollector_append(&dc_, 2.0);
|
|
417
|
+
datacollector_clear(&dc_);
|
|
418
|
+
EXPECT_EQ(datacollector_count(&dc_), 0u);
|
|
419
|
+
}
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
---
|
|
423
|
+
|
|
424
|
+
## Handling FreeRTOS Dependencies
|
|
425
|
+
|
|
426
|
+
If a firmware module calls FreeRTOS APIs (e.g., `vTaskDelay`, `xQueueSend`), create a **stub library** that provides no-op or fake implementations:
|
|
427
|
+
|
|
428
|
+
```cpp
|
|
429
|
+
// test/host/stubs/freertos_stubs.cpp
|
|
430
|
+
#include "freertos/FreeRTOS.h"
|
|
431
|
+
#include "freertos/task.h"
|
|
432
|
+
|
|
433
|
+
extern "C" {
|
|
434
|
+
|
|
435
|
+
void vTaskDelay(const TickType_t xTicksToDelay) {
|
|
436
|
+
(void)xTicksToDelay; // no-op on host
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
BaseType_t xQueueSend(QueueHandle_t, const void*, TickType_t) {
|
|
440
|
+
return pdTRUE; // fake success
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
} // extern "C"
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
Then in `CMakeLists.txt`:
|
|
447
|
+
|
|
448
|
+
```cmake
|
|
449
|
+
target_sources(test_policy PRIVATE
|
|
450
|
+
${CMAKE_CURRENT_SOURCE_DIR}/../../main/policy/feature_flags.cpp
|
|
451
|
+
${CMAKE_CURRENT_SOURCE_DIR}/stubs/freertos_stubs.cpp # link stubs
|
|
452
|
+
)
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
Keep stubs minimal. If a module requires extensive FreeRTOS mocking, consider testing it as an integration test on-device instead.
|
|
456
|
+
|
|
457
|
+
---
|
|
458
|
+
|
|
459
|
+
## CI Integration
|
|
460
|
+
|
|
461
|
+
```yaml
|
|
462
|
+
# .github/workflows/host-tests.yml (GitHub Actions example)
|
|
463
|
+
name: Host-Side Tests
|
|
464
|
+
|
|
465
|
+
on: [push, pull_request]
|
|
466
|
+
|
|
467
|
+
jobs:
|
|
468
|
+
host-tests:
|
|
469
|
+
runs-on: ubuntu-latest
|
|
470
|
+
steps:
|
|
471
|
+
- uses: actions/checkout@v4
|
|
472
|
+
- name: Install deps
|
|
473
|
+
run: sudo apt-get update && sudo apt-get install -y cmake g++ lcov
|
|
474
|
+
- name: Build and test
|
|
475
|
+
run: |
|
|
476
|
+
cd test/host
|
|
477
|
+
mkdir -p build && cd build
|
|
478
|
+
cmake .. -DCOVERAGE=ON
|
|
479
|
+
cmake --build . -j$(nproc)
|
|
480
|
+
./test_policy || exit 1
|
|
481
|
+
lcov --capture --directory . --output coverage.info \
|
|
482
|
+
--exclude '*/test_*' --exclude '*/build/*' --exclude '*/stubs/*'
|
|
483
|
+
lcov --filter coverage.info --output filtered.info \
|
|
484
|
+
--exclude '*/test_*' --exclude '*/stubs/*'
|
|
485
|
+
lcov --list filtered.info
|
|
486
|
+
# 80% gate:
|
|
487
|
+
lcov --filter filtered.info --output-filtered.info \
|
|
488
|
+
| grep -q "lines\.\.\.\." || true
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
---
|
|
492
|
+
|
|
493
|
+
## Key Principles
|
|
494
|
+
|
|
495
|
+
1. **Link real `.cpp` files** — not headers only. You want to test the compiled implementation.
|
|
496
|
+
2. **No ESP-IDF includes** in test sources — if a header pulls in `esp_log.h`, create a local stub or use `-D` flags to mock it out.
|
|
497
|
+
3. **Use `extern "C"`** when linking firmware `.cpp` files that have C linkage.
|
|
498
|
+
4. **Test behavior, not structure** — assert on return values, state changes, and side effects visible through the public API.
|
|
499
|
+
5. **Deterministic only** — host tests must be free of timing, threading races, and hardware dependencies.
|