Analysis of testability across the AqualinkD codebase, framework recommendation, and a concrete plan for the first test PR.
AqualinkD has 39 source files (excluding the vendored mongoose.c), 0 test files, and no make test target. Every change — bug fix, new feature, protocol tweak — ships without automated verification.
This isn't unusual for embedded-ish C projects, but AqualinkD has grown to the point where manual testing can't cover the surface area. The codebase handles external input (JSON from web clients, raw bytes from an RS-485 bus), does manual buffer management, and has protocol handlers that vary across hardware revisions. These are exactly the areas where automated tests prevent regressions.
source/
├── 39 .c files (~35,000 lines, excluding mongoose.c)
├── 38 .h files
├── 0 test files
└── Makefile (no test target)
Every source file falls into one of four tiers based on how much work is needed to test it.
These functions take inputs, return outputs, and touch nothing else. Tests can call them directly.
| File | Lines | Key functions | Why it's testable |
|---|---|---|---|
rs_msg_utils.c |
577 | rsm_strncpy, rsm_strcmp, rsm_atoi, rsm_atof, rsm_strnstr, rsm_strncasestr, rsm_HHMM2min, rsm_get_revision, rsm_get_boardcpu |
All pure string/message utilities. No includes of aqualink.h, no globals. |
timespec_subtract.c |
46 | timespec_subtract |
Single pure function. Computes time difference with carry handling. |
debug_timer.c |
75 | Timer start/stop/report functions | Pure timing utilities (when clock_gettime is available). |
utils.c (subset) |
~200 | cleanwhitespace, trimwhitespace, stripwhitespace, chopwhitespace, cleanint, text2bool, request2bool, bool2text, degFtoC, degCtoF, count_characters, stristr, prittyString, getTemperatureUOM, isUomTemperature, timespec2float, text2elevel, elevel2text |
These are self-contained helpers. The logging functions in utils.c touch globals and I/O, but the utility functions listed here are pure. |
color_lights.c (subset) |
~200 | isShowMode, light color lookup functions, color name→value mapping |
Many functions are pure lookups and string comparisons. Some functions reference _custom_shows (file-level static), but the core light mode identification is testable. |
Estimated test count: ~50 tests covering the foundation functions used everywhere.
These functions are pure in behavior — they read/write struct fields and return results — but need populated structs as input.
| File | Lines | Key testable functions | Setup needed |
|---|---|---|---|
json_messages.c |
1,577 | parseJSONrequest, build_aqualink_status_JSON, build_device_JSON, build_logmsg_JSON, build_mqtt_status_JSON, LED2text |
struct JSONkvptr for parsing; struct aqualinkdata for builders |
aq_panel.c |
2,021 | Panel type detection, button initialization, setPanel* functions |
struct aqualinkdata + struct aqconfig |
aq_programmer.c |
1,186 | Validation helpers, programmer type identification | struct aqualinkdata |
devices_jandy.c |
1,526 | processPacketToJandyDevice, processPacketFromSWG, isSWGDeviceErrorState, setSWGdeviceStatus, processPacketFromJandyPump, getPumpStatus |
struct aqualinkdata + raw packet byte arrays |
devices_pentair.c |
251 | processPacketFromPentairPump, Pentair status parsing |
struct aqualinkdata + Pentair packet bytes |
packetLogger.c |
224 | beautifyPacket, packet-to-string formatting |
Raw packet byte arrays |
Estimated test count: ~80 tests covering JSON parsing, device packet handling, and panel configuration.
These files mix pure logic with I/O. Testing requires either extracting the pure parts or mocking the I/O layer.
| File | Lines | Coupled to | Extractable pure logic |
|---|---|---|---|
config.c |
2,540 | File I/O (fopen/fclose) |
setConfigValue() — the core parameter parser that converts string values to typed config. ~800 lines of pure parsing logic trapped behind a file reader. |
aq_serial.c |
1,345 | Serial port (open/read/write/ioctl) |
Already extracted: generate_checksum, check_jandy_checksum, check_pentair_checksum, generate_pentair_checksum, getProtocolType, getJandyDeviceType. These are pure and critical. |
net_services.c |
2,393 | Mongoose HTTP/WS, pthreads | action_URI() parsing logic could be extracted. MQTT message formatting is interleaved with send calls. |
sensors.c |
240 | File I/O, pthreads | Sensor value regex parsing could be extracted from read_sensor(). |
aq_timer.c |
228 | time(), pthreads |
Timer remaining calculation is pure if you inject current time. |
aq_systemutils.c |
326 | mount(), fopen, subprocess exec |
All I/O coupled. No pure logic to extract without refactoring. |
Note: The checksum functions in aq_serial.c are already cleanly separated — they're Tier 1 in practice. We list them here because the file overall is I/O-coupled.
These files implement the Jandy panel communication protocols. Per project policy, we will not modify this source code. But writing tests against their public interfaces with known packet data is high-value — tests act as a regression safety net for the maintainer's future protocol changes.
| File | Lines | Public interface | Test data source |
|---|---|---|---|
allbutton.c |
822 | process_allbutton_packet(packet, length, aqdata) |
LED status packets, command ACKs |
onetouch.c |
1,098 | process_onetouch_packet(packet, length, aqdata) |
Menu display packets, highlight packets |
iaqtouch.c |
1,501 | process_iaqtouch_packet(packet, length, aqdata) |
Page navigation, button state, status packets |
iaqualink.c |
838 | process_iaqualink_packet(packet, length, aqdata), process_iAqualinkStatusPacket(...) |
Main status, aux status, command packets |
pda.c |
1,097 | process_pda_packet(packet, length) |
Menu display, highlight, clear/shift packets |
These are future work (Phase 2+). Testing them requires capturing real packet sequences from each panel type — something the maintainer has direct access to.
Ranked by risk (external input handling, buffer safety, correctness):
This is the highest-risk function in the codebase. It receives raw input from web clients, destructively modifies the buffer, and has no bounds checking:
// json_messages.c:1029 — the entire parser
bool parseJSONrequest(char *buffer, struct JSONkvptr *request)
{
int i=0;
int found=0;
bool reading = false;
// Initialize 4 key-value slots
request->kv[0].key = NULL; request->kv[0].value = NULL;
request->kv[1].key = NULL; request->kv[1].value = NULL;
request->kv[2].key = NULL; request->kv[2].value = NULL;
request->kv[3].key = NULL; request->kv[3].value = NULL;
int length = strlen(buffer);
while ( i < length ) {
switch (buffer[i]) {
case '{': case '"': case '}': case ':': case ',': case ' ':
if (reading == true && buffer[i] != ' ' && buffer[i] != ',' && buffer[i] != ':'){
reading = false;
buffer[i] = '\0'; // destructive modification
found++;
}
break;
default:
if (reading == false) {
reading = true;
if ( found%2 == 0 )
request->kv[found / 2].key = &buffer[i];
else
request->kv[(found-1) / 2].value = &buffer[i];
}
break;
}
if (found >= 8) break;
i++;
}
return true; // always returns true — no error detection
}What tests catch:
- Normal case:
{"uri":"/api/setpoint","value":"85"}→ correct key/value extraction - Empty input, empty object
{} - More than 4 key-value pairs (silent truncation at
found >= 8) - Values with spaces (the parser ignores spaces as delimiters while reading)
- Malformed input: missing quotes, missing braces, nested objects
These are the most testable high-value targets. A wrong checksum means dropped or corrupted commands to pool equipment:
// aq_serial.c:323 — Jandy checksum: sum of bytes 0..n-3, masked to 8 bits
int generate_checksum(unsigned char* packet, int length)
{
int i, sum, n;
n = length - 3;
sum = 0;
for (i = 0; i < n; i++)
sum += (int) packet[i];
return(sum & 0x0ff);
}
// aq_serial.c:334 — includes workaround for known Jandy protocol bug
bool check_jandy_checksum(unsigned char* packet, int length)
{
if (generate_checksum(packet, length) == packet[length-3])
return true;
// Bug workaround: OneTouch long msg, line 3, always 0x0a checksum
if (packet[3] == 0x04 && packet[4] == 0x03 && packet[length-3] == 0x0a) {
return true; // known false-negative in Jandy protocol
}
return false;
}
// aq_serial.c:357 — Pentair: 16-bit checksum starting at byte 3
bool check_pentair_checksum(unsigned char* packet, int length)
{
int i, sum, n;
n = packet[8] + 9;
sum = 0;
for (i = 3; i < n; i++)
sum += (int) packet[i];
if (sum == (packet[length-2] * 256 + packet[length-1]))
return true;
if (sum == (packet[n] * 256 + packet[n+1])) // length mismatch recovery
return true;
return false;
}What tests catch:
- Known-good packets from real hardware (golden test vectors)
- Off-by-one in checksum range
- The Jandy protocol bug workaround (ensure it fires for the right packet pattern and doesn't fire for others)
- Pentair length field vs actual length mismatch handling
setConfigValue() in config.c is the central config parser. It converts raw string values from the config file into typed values (int, bool, hex, float, bitmask) and stores them into struct aqconfig:
// config.c — type conversion switch (simplified)
switch (_cfgParams[i].value_type) {
case CFG_STRING:
*(char **)_cfgParams[i].value_ptr = cleanalloc(value);
break;
case CFG_INT:
*(int *)_cfgParams[i].value_ptr = strtoul(cleanwhitespace(value), NULL, 10);
break;
case CFG_BOOL:
*(bool *)_cfgParams[i].value_ptr = text2bool(value);
break;
case CFG_HEX:
*(unsigned char *)_cfgParams[i].value_ptr = strtoul(cleanwhitespace(value), NULL, 16);
break;
case CFG_FLOAT:
*(float *)_cfgParams[i].value_ptr = atof(tmpval);
break;
case CFG_BITMASK:
if (text2bool(value))
*(uint16_t *)_cfgParams[i].value_ptr |= _cfgParams[i].mask;
else
*(uint16_t *)_cfgParams[i].value_ptr &= ~_cfgParams[i].mask;
break;
}What tests catch:
strtouloverflow (e.g.,"999999"for aunsigned charhex field)- Whitespace handling:
" 0x0a "vs"0x0a"vs"0x0A" - Boolean edge cases:
"yes","YES","on","ON","true"(note:text2boolonly recognizes YES/ON) - Empty or blank values triggering default fallback
These are called from almost every file. A bug here ripples through the entire system:
// utils.c — used in config parsing, message handling, everywhere
char *cleanwhitespace(char *str) {
char *end;
if (str == NULL) return str;
while(isspace(*str)) str++;
if(*str == 0) return str;
end = str + strlen(str) - 1;
while(end > str && isspace(*end)) end--;
if (end != (str + strlen(str) - 1) )
*(end+1) = 0;
return str;
}
// rs_msg_utils.c — RS-485 message string handling
int rsm_strncpy(char *dest, const unsigned char *src, int dest_len, int src_len);
int rsm_strcmp(const char *s1, const char *s2);
char *rsm_strnstr(const char *haystack, const char *needle, size_t slen);
int rsm_atoi(const char* str);
float rsm_atof(const char* str);
int rsm_HHMM2min(char *message);What tests catch:
- NULL input handling
- All-whitespace strings
- Empty strings
- Strings with only leading or only trailing whitespace
rsm_atoi/rsm_atofwith non-numeric inputrsm_HHMM2minwith malformed time strings
The build_*_JSON functions use sprintf chains to construct JSON. Tests verify the output is valid JSON and contains expected field values:
What tests catch:
- Buffer overflow (output exceeds
JSON_BUFFER_SIZE) - Invalid JSON from unescaped special characters in
aqdata->last_message - Missing or extra commas/braces
- Correct field values for known struct inputs
Unity is a test framework designed for C, especially embedded C. It's three files (MIT license):
| File | Lines | Purpose |
|---|---|---|
unity.c |
~1,300 | Test runner and assertion implementations |
unity.h |
~600 | Public API (assertions, test macros) |
unity_internals.h |
~800 | Internal implementation details |
| Factor | Unity | cmocka | Check | CUnit |
|---|---|---|---|---|
| Files to vendor | 3 | 6+ | 15+ | 20+ |
| Dependencies | None | None | fork()/setjmp |
None |
| License | MIT | Apache 2.0 | LGPL | LGPL |
| Designed for embedded C | Yes | Yes | No | No |
| Assertion readability | TEST_ASSERT_EQUAL(85, temp) |
assert_int_equal(85, temp) |
ck_assert_int_eq(85, temp) |
CU_ASSERT_EQUAL(85, temp) |
| Setup/teardown | Built-in | Built-in | Built-in | Built-in |
| Binary size impact | ~15 KB | ~30 KB | ~100 KB | ~50 KB |
| Build integration | Add 1 .c file | pkg-config or vendor | pkg-config | pkg-config |
| Cross-compile friendly | Yes (no OS deps) | Yes | No (fork) |
Mostly |
- cmocka: Good framework, but Unity is simpler for our needs — we don't need mocking for Tier 1/2 tests.
- Check: Uses
fork()for test isolation, which adds complexity and makes debugging harder. LGPL license. - CUnit: Heavier, less commonly used in modern embedded C projects. LGPL license.
- Google Test: C++, not C. Would require wrapping all test files in
extern "C".
// test/test_rs_msg_utils.c
#include "unity.h"
#include "rs_msg_utils.h"
void setUp(void) {}
void tearDown(void) {}
void test_rsm_atoi_normal(void) {
TEST_ASSERT_EQUAL_INT(42, rsm_atoi("42"));
TEST_ASSERT_EQUAL_INT(0, rsm_atoi("0"));
TEST_ASSERT_EQUAL_INT(-5, rsm_atoi("-5"));
}
void test_rsm_atoi_with_whitespace(void) {
TEST_ASSERT_EQUAL_INT(42, rsm_atoi(" 42 "));
}
void test_rsm_strnstr_found(void) {
TEST_ASSERT_NOT_NULL(rsm_strnstr("REV T.0.1", "REV", 9));
}
void test_rsm_strnstr_not_found(void) {
TEST_ASSERT_NULL(rsm_strnstr("hello world", "REV", 11));
}
void test_rsm_HHMM2min_normal(void) {
TEST_ASSERT_EQUAL_INT(150, rsm_HHMM2min("2:30"));
TEST_ASSERT_EQUAL_INT(60, rsm_HHMM2min("1:00"));
}
int main(void) {
UNITY_BEGIN();
RUN_TEST(test_rsm_atoi_normal);
RUN_TEST(test_rsm_atoi_with_whitespace);
RUN_TEST(test_rsm_strnstr_found);
RUN_TEST(test_rsm_strnstr_not_found);
RUN_TEST(test_rsm_HHMM2min_normal);
return UNITY_END();
}// test/test_checksum.c
#include "unity.h"
#include "aq_serial.h"
void setUp(void) {}
void tearDown(void) {}
void test_jandy_checksum_known_good_packet(void) {
// Real packet: probe to device 0x0a
// DLE STX DEST CMD ... CHECKSUM DLE ETX
unsigned char packet[] = {0x10, 0x02, 0x0a, 0x00, 0x1c, 0x10, 0x03};
TEST_ASSERT_TRUE(check_jandy_checksum(packet, 7));
}
void test_jandy_checksum_bad_packet(void) {
unsigned char packet[] = {0x10, 0x02, 0x0a, 0x00, 0xFF, 0x10, 0x03};
TEST_ASSERT_FALSE(check_jandy_checksum(packet, 7));
}
void test_jandy_checksum_protocol_bug_workaround(void) {
// OneTouch long msg, line 3, checksum 0x0a — known Jandy bug
unsigned char packet[] = {
0x10, 0x02, 0x43, 0x04, 0x03, 0x20, 0x20, 0x20,
0x20, 0x35, 0x3a, 0x30, 0x35, 0x20, 0x50, 0x4d,
0x20, 0x20, 0x20, 0x20, 0x20, 0x0a, 0x10, 0x03
};
// Checksum doesn't match, but the workaround should return true
TEST_ASSERT_TRUE(check_jandy_checksum(packet, 24));
}
void test_generate_checksum_simple(void) {
// DLE(0x10) + STX(0x02) + DEST(0x0a) + CMD(0x00) = sum of first n-3 bytes
unsigned char packet[] = {0x10, 0x02, 0x0a, 0x00, 0x00, 0x10, 0x03};
int checksum = generate_checksum(packet, 7);
// Sum of bytes 0..3 = 0x10 + 0x02 + 0x0a + 0x00 = 0x1c
TEST_ASSERT_EQUAL_HEX8(0x1c, checksum);
}
void test_pentair_checksum_known_good(void) {
// Pentair packet: FF 00 FF A5 [version] [dest] [src] [cmd] [datalen] [data...] [chk_hi] [chk_lo]
unsigned char packet[] = {
0xFF, 0x00, 0xFF, 0xA5, 0x00, 0x60, 0x10, 0x07,
0x0F, // datalen = 15
0x0A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x01, 0x95 // checksum high, low
};
TEST_ASSERT_TRUE(check_pentair_checksum(packet, 26));
}
int main(void) {
UNITY_BEGIN();
RUN_TEST(test_jandy_checksum_known_good_packet);
RUN_TEST(test_jandy_checksum_bad_packet);
RUN_TEST(test_jandy_checksum_protocol_bug_workaround);
RUN_TEST(test_generate_checksum_simple);
RUN_TEST(test_pentair_checksum_known_good);
return UNITY_END();
}// test/test_json_parse.c
#include "unity.h"
#include "json_messages.h"
#include <string.h>
void setUp(void) {}
void tearDown(void) {}
void test_parseJSON_normal_request(void) {
char buffer[] = "{\"uri\":\"/api/setpoint\",\"value\":\"85\"}";
struct JSONkvptr request;
parseJSONrequest(buffer, &request);
TEST_ASSERT_EQUAL_STRING("uri", request.kv[0].key);
TEST_ASSERT_EQUAL_STRING("/api/setpoint", request.kv[0].value);
TEST_ASSERT_EQUAL_STRING("value", request.kv[1].key);
TEST_ASSERT_EQUAL_STRING("85", request.kv[1].value);
}
void test_parseJSON_empty_object(void) {
char buffer[] = "{}";
struct JSONkvptr request;
parseJSONrequest(buffer, &request);
TEST_ASSERT_NULL(request.kv[0].key);
}
void test_parseJSON_four_pairs_max(void) {
char buffer[] = "{\"a\":\"1\",\"b\":\"2\",\"c\":\"3\",\"d\":\"4\",\"e\":\"5\"}";
struct JSONkvptr request;
parseJSONrequest(buffer, &request);
// Only 4 pairs fit — 5th should be silently dropped
TEST_ASSERT_EQUAL_STRING("a", request.kv[0].key);
TEST_ASSERT_EQUAL_STRING("d", request.kv[3].key);
}
int main(void) {
UNITY_BEGIN();
RUN_TEST(test_parseJSON_normal_request);
RUN_TEST(test_parseJSON_empty_object);
RUN_TEST(test_parseJSON_four_pairs_max);
return UNITY_END();
}// test/test_utils.c
#include "unity.h"
#include "utils.h"
#include <string.h>
void setUp(void) {}
void tearDown(void) {}
void test_degFtoC(void) {
TEST_ASSERT_FLOAT_WITHIN(0.01, 0.0, degFtoC(32.0));
TEST_ASSERT_FLOAT_WITHIN(0.01, 100.0, degFtoC(212.0));
TEST_ASSERT_FLOAT_WITHIN(0.01, 37.0, degFtoC(98.6));
}
void test_degCtoF(void) {
TEST_ASSERT_FLOAT_WITHIN(0.01, 32.0, degCtoF(0.0));
TEST_ASSERT_FLOAT_WITHIN(0.01, 212.0, degCtoF(100.0));
}
void test_text2bool(void) {
char yes[] = "YES";
char on[] = "ON";
char no[] = "NO";
char off[] = "anything_else";
TEST_ASSERT_TRUE(text2bool(yes));
TEST_ASSERT_TRUE(text2bool(on));
TEST_ASSERT_FALSE(text2bool(no));
TEST_ASSERT_FALSE(text2bool(off));
}
void test_cleanwhitespace_leading_trailing(void) {
char str[] = " hello ";
char *result = cleanwhitespace(str);
TEST_ASSERT_EQUAL_STRING("hello", result);
}
void test_cleanwhitespace_null(void) {
TEST_ASSERT_NULL(cleanwhitespace(NULL));
}
void test_cleanwhitespace_all_spaces(void) {
char str[] = " ";
char *result = cleanwhitespace(str);
TEST_ASSERT_EQUAL_STRING("", result);
}
void test_count_characters(void) {
TEST_ASSERT_EQUAL_INT(3, count_characters("a,b,c,d", ','));
TEST_ASSERT_EQUAL_INT(0, count_characters("hello", ','));
}
void test_getTemperatureUOM(void) {
// °F = bytes 194, 176, 'F'
char fahrenheit[] = {194, 176, 'F', '\0'};
TEST_ASSERT_EQUAL(FAHRENHEIT, getTemperatureUOM(fahrenheit));
char celsius[] = {194, 176, 'C', '\0'};
TEST_ASSERT_EQUAL(CELSIUS, getTemperatureUOM(celsius));
TEST_ASSERT_EQUAL(UNKNOWN, getTemperatureUOM(NULL));
TEST_ASSERT_EQUAL(UNKNOWN, getTemperatureUOM("xyz"));
}
int main(void) {
UNITY_BEGIN();
RUN_TEST(test_degFtoC);
RUN_TEST(test_degCtoF);
RUN_TEST(test_text2bool);
RUN_TEST(test_cleanwhitespace_leading_trailing);
RUN_TEST(test_cleanwhitespace_null);
RUN_TEST(test_cleanwhitespace_all_spaces);
RUN_TEST(test_count_characters);
RUN_TEST(test_getTemperatureUOM);
return UNITY_END();
}The first PR is infrastructure + Tier 1 tests only. No source modifications. No refactoring.
test/
├── unity/
│ ├── unity.c # vendored (MIT)
│ ├── unity.h # vendored (MIT)
│ └── unity_internals.h # vendored (MIT)
├── test_rs_msg_utils.c # ~20 tests: string matching, copying, atoi, revision parsing
├── test_checksum.c # ~15 tests: Jandy/Pentair checksum generate + verify
├── test_utils.c # ~25 tests: whitespace, bool, temp conversion, string helpers
├── test_json_parse.c # ~10 tests: parseJSONrequest edge cases
└── test_timespec.c # ~5 tests: timespec_subtract
# Test configuration
TEST_DIR := ./test
TEST_BUILD_DIR := $(OBJ_DIR)/test
UNITY_DIR := $(TEST_DIR)/unity
UNITY_SRC := $(UNITY_DIR)/unity.c
# Test sources (each builds into its own test binary)
TEST_SRCS := $(wildcard $(TEST_DIR)/test_*.c)
TEST_BINS := $(patsubst $(TEST_DIR)/%.c,$(TEST_BUILD_DIR)/%,$(TEST_SRCS))
# Test compile flags (debug, no optimization)
TEST_CFLAGS = -Wall -O0 -g $(AQ_FLAGS) $(MGFLAGS)
# Build and run all tests
test: $(TEST_BINS)
@echo "Running tests..."
@for t in $(TEST_BINS); do echo "--- $$t ---"; $$t || exit 1; done
@echo "All tests passed."
# Compile each test binary: test source + unity + required source files
$(TEST_BUILD_DIR)/test_rs_msg_utils: $(TEST_DIR)/test_rs_msg_utils.c $(UNITY_SRC) $(SRC_DIR)/rs_msg_utils.c $(SRC_DIR)/utils.c | $(TEST_BUILD_DIR)
$(CC) $(TEST_CFLAGS) $(INCLUDES) -I$(UNITY_DIR) -o $@ $^ $(LIBS)
$(TEST_BUILD_DIR)/test_checksum: $(TEST_DIR)/test_checksum.c $(UNITY_SRC) $(SRC_DIR)/aq_serial.c $(SRC_DIR)/utils.c $(SRC_DIR)/packetLogger.c $(SRC_DIR)/rs_msg_utils.c $(SRC_DIR)/timespec_subtract.c | $(TEST_BUILD_DIR)
$(CC) $(TEST_CFLAGS) $(INCLUDES) -I$(UNITY_DIR) -o $@ $^ $(LIBS)
$(TEST_BUILD_DIR)/test_utils: $(TEST_DIR)/test_utils.c $(UNITY_SRC) $(SRC_DIR)/utils.c | $(TEST_BUILD_DIR)
$(CC) $(TEST_CFLAGS) $(INCLUDES) -I$(UNITY_DIR) -o $@ $^ $(LIBS)
$(TEST_BUILD_DIR)/test_json_parse: $(TEST_DIR)/test_json_parse.c $(UNITY_SRC) $(SRC_DIR)/json_messages.c $(SRC_DIR)/utils.c $(SRC_DIR)/rs_msg_utils.c | $(TEST_BUILD_DIR)
$(CC) $(TEST_CFLAGS) $(INCLUDES) -I$(UNITY_DIR) -o $@ $^ $(LIBS)
$(TEST_BUILD_DIR)/test_timespec: $(TEST_DIR)/test_timespec.c $(UNITY_SRC) $(SRC_DIR)/timespec_subtract.c | $(TEST_BUILD_DIR)
$(CC) $(TEST_CFLAGS) $(INCLUDES) -I$(UNITY_DIR) -o $@ $^ $(LIBS)
$(TEST_BUILD_DIR):
$(MKDIR) $(call FixPath,$@)- No changes to any existing source files
- No new dependencies beyond Unity's 3 files (MIT license)
- No build system changes that affect the production binary
- No protocol handler tests (those need real packet data from the maintainer)
$ make test
Running tests...
--- ./build/test/test_rs_msg_utils ---
test/test_rs_msg_utils.c:10:test_rsm_atoi_normal:PASS
test/test_rs_msg_utils.c:16:test_rsm_atoi_with_whitespace:PASS
test/test_rs_msg_utils.c:20:test_rsm_strnstr_found:PASS
...
20 Tests 0 Failures 0 Ignored
--- ./build/test/test_checksum ---
test/test_checksum.c:8:test_jandy_checksum_known_good_packet:PASS
test/test_checksum.c:14:test_jandy_checksum_bad_packet:PASS
...
15 Tests 0 Failures 0 Ignored
--- ./build/test/test_utils ---
...
25 Tests 0 Failures 0 Ignored
--- ./build/test/test_json_parse ---
...
10 Tests 0 Failures 0 Ignored
--- ./build/test/test_timespec ---
...
5 Tests 0 Failures 0 Ignored
All tests passed.
| Phase | Scope | Source changes? | Tests added |
|---|---|---|---|
| 1 (this PR) | Infrastructure + Tier 1 | None | ~75 tests |
| 2 | Tier 2: JSON builders, device packet parsing | None (tests only) | ~50 tests |
| 3 | Tier 2: aq_panel.c configuration, aq_programmer.c validation |
None (tests only) | ~30 tests |
| 4 | Tier 3: Extract setConfigValue tests (config parsing) |
Minimal (may need to expose internal helpers) | ~20 tests |
| 5 | Tier 4: Protocol handler regression tests (with real packet captures) | None | ~40 tests |
Phase 1 is self-contained and immediately valuable. Each subsequent phase is independent and can be prioritized based on where bugs actually occur.
| File | Lines | Tier | Testable surface |
|---|---|---|---|
rs_msg_utils.c |
577 | 1 | 16 pure string functions |
timespec_subtract.c |
46 | 1 | 1 pure function |
debug_timer.c |
75 | 1 | Timer utilities |
utils.c |
1,028 | 1/3 | ~18 pure helpers + logging (I/O) |
color_lights.c |
558 | 1/2 | Light mode lookups (some statics) |
json_messages.c |
1,577 | 2 | Parser + 12 JSON builders |
aq_panel.c |
2,021 | 2 | Panel config and button setup |
aq_programmer.c |
1,186 | 2/3 | Validation helpers + thread-based programmer |
devices_jandy.c |
1,526 | 2 | Packet processors, SWG/pump status |
devices_pentair.c |
251 | 2 | Pentair pump packet parsing |
packetLogger.c |
224 | 2 | beautifyPacket formatting |
mqtt_discovery.c |
766 | 2/3 | MQTT discovery message building |
config.c |
2,540 | 3 | setConfigValue parser (extractable) |
aq_serial.c |
1,345 | 1/3 | Checksums (pure) + serial I/O |
net_services.c |
2,393 | 3 | URI parsing (extractable) + network I/O |
sensors.c |
240 | 3 | Regex parsing + file I/O |
aq_timer.c |
228 | 3 | Timer calc + threading |
aq_systemutils.c |
326 | 3 | All I/O coupled |
web_config.c |
183 | 3 | JSON config save/load |
aq_scheduler.c |
412 | 3 | Scheduler logic + time |
auto_configure.c |
291 | 2/3 | Auto-detection logic |
serialadapter.c |
1,415 | 3 | RS serial adapter I/O |
simulator.c |
138 | 2/3 | Simulator packet building |
net_interface.c |
191 | 3 | Network interface detection |
allbutton.c |
822 | 4 | Protocol handler (read-only) |
allbutton_aq_programmer.c |
1,616 | 4 | Protocol programmer (read-only) |
onetouch.c |
1,098 | 4 | Protocol handler (read-only) |
onetouch_aq_programmer.c |
1,086 | 4 | Protocol programmer (read-only) |
iaqtouch.c |
1,501 | 4 | Protocol handler (read-only) |
iaqtouch_aq_programmer.c |
1,761 | 4 | Protocol programmer (read-only) |
iaqualink.c |
838 | 4 | Protocol handler (read-only) |
pda.c |
1,097 | 4 | Protocol handler (read-only) |
pda_aq_programmer.c |
2,092 | 4 | Protocol programmer (read-only) |
pda_menu.c |
392 | 4 | PDA menu state (read-only) |
aqualinkd.c |
1,427 | 3 | Main loop (not testable) |
dummy_device.c |
253 | — | Test utility (already exists) |
dummy_reader.c |
187 | — | Test utility (already exists) |
rs485mon.c |
1,214 | 3 | RS-485 monitor daemon |