test(healthcheck): 新增主动健康检查单元测试(17项)

- 覆盖 healthcheck_parse_status 状态码解析(正常/异常/边界)
- 覆盖 healthcheck_build_request 请求构建(默认路径/自定义路径/空路径)
- 覆盖 healthcheck_update_state 状态机(阈值恢复/阈值失败/计数器重置/时间戳)
- 覆盖 healthcheck_probe_once NULL 防护
- 使用 #include healthcheck.c 白盒测试技巧访问静态函数
- Makefile: 新增 test_healthcheck 编译规则
- .cocoon-plan.md: 标记主动健康检查完成,更新单元测试总数 159

编译零警告,159 个单元测试 + 106 项集成测试全部通过。
This commit is contained in:
xfy911 2026-06-09 18:16:35 +08:00
parent 60ef9ed908
commit 0e7de9400c
4 changed files with 259 additions and 3 deletions

View File

@ -58,9 +58,9 @@
> **注意**:用户已指示恢复 cocoon 开发,继续推进新功能。不暂停。 > **注意**:用户已指示恢复 cocoon 开发,继续推进新功能。不暂停。
- 所有 Phase 1-4 核心功能已完成(反向代理负载均衡 + Prometheus 指标 + 虚拟主机已加入) - 所有 Phase 1-4 核心功能已完成(反向代理负载均衡 + Prometheus 指标 + 虚拟主机 + 主动健康检查已加入)
- 编译通过,零警告 - 编译通过,零警告
- **142 个单元测试全部通过** + **106 项集成测试全部通过**(新增 3 项虚拟主机测试) - **159 个单元测试全部通过**(新增 17 项 healthcheck 单元测试) + **106 项集成测试全部通过**(新增 3 项虚拟主机测试)
- 当前仅做编译和测试验证,不添加新代码 - 当前仅做编译和测试验证,不添加新代码
## 待办池(活跃) ## 待办池(活跃)
@ -69,10 +69,22 @@
- 后端发送 Connection: close 时正确关闭连接而非归还 - 后端发送 Connection: close 时正确关闭连接而非归还
3. **HTTP/2 代理流式转发** — 当前为非流式(先收集完整响应),未来改为流式 3. **HTTP/2 代理流式转发** — 当前为非流式(先收集完整响应),未来改为流式
4. ~~虚拟主机 / 多站点~~ ✅ 2026-06-09 — 基于 Host 头匹配HTTP/1.1 + HTTP/2 均支持 4. ~~虚拟主机 / 多站点~~ ✅ 2026-06-09 — 基于 Host 头匹配HTTP/1.1 + HTTP/2 均支持
5. 主动健康检查 — 后台线程定期探测后端(当前为被动式) 5. ~~主动健康检查~~ ✅ 2026-06-09 — 后台线程定期探测后端4 项集成测试 + 17 项单元测试覆盖
## 最近行动记录 ## 最近行动记录
- 2026-06-09 18:00: **主动健康检查单元测试**
- 新增 `tests/unit/test_healthcheck.c`17 项单元测试
- `healthcheck_parse_status`:覆盖 200/404/500/100/302/503 正常解析、长度不足、非 HTTP 协议、非法状态码
- `healthcheck_build_request`:覆盖默认路径 `/health`、自定义路径 `/ready`、空路径回退
- `healthcheck_update_state`:覆盖健康阈值恢复(连续 2 次成功)、不健康阈值标记(连续 3 次失败)、计数器重置、单次失败/成功不触发状态变更、`last_check` 时间戳更新
- `healthcheck_probe_once`NULL 后端返回 false
- 使用 `#include "healthcheck.c"` 白盒测试技巧访问静态函数
- `Makefile`:新增 `test_healthcheck` 编译规则(链接 proxy.c、http.c、platform.c 等依赖)
- 编译零警告159 个单元测试 + 106 项集成测试全部通过
- `.cocoon-plan.md`:标记主动健康检查完成
- 提交已推送到 `main`
- 2026-06-09 15:00: **虚拟主机 / 多站点支持** - 2026-06-09 15:00: **虚拟主机 / 多站点支持**
- `cocoon.h`: 新增 `cocoon_vhost_t` 结构体(`server_name[256]` + `root_dir[512]``cocoon_config_t` 新增 `vhosts[COCOON_MAX_VHOSTS]``num_vhosts` - `cocoon.h`: 新增 `cocoon_vhost_t` 结构体(`server_name[256]` + `root_dir[512]``cocoon_config_t` 新增 `vhosts[COCOON_MAX_VHOSTS]``num_vhosts`
- `config.c`: JSON 解析器新增 `vhosts` 数组解析,每个对象支持 `server_name``root_dir` 字段 - `config.c`: JSON 解析器新增 `vhosts` 数组解析,每个对象支持 `server_name``root_dir` 字段

View File

@ -108,6 +108,10 @@ $(UNIT_TEST_DIR)/test_websocket: $(UNIT_TEST_DIR)/test_websocket.c websocket.c l
$(UNIT_TEST_DIR)/test_log: $(UNIT_TEST_DIR)/test_log.c log.c $(UNITY_SRC) $(UNIT_TEST_DIR)/test_log: $(UNIT_TEST_DIR)/test_log.c log.c $(UNITY_SRC)
$(CC) $(CFLAGS) -I. -I$(UNIT_TEST_DIR)/../unity -o $@ $(UNIT_TEST_DIR)/test_log.c log.c $(UNITY_SRC) -lm $(CC) $(CFLAGS) -I. -I$(UNIT_TEST_DIR)/../unity -o $@ $(UNIT_TEST_DIR)/test_log.c log.c $(UNITY_SRC) -lm
# 单元测试编译规则
$(UNIT_TEST_DIR)/test_healthcheck: $(UNIT_TEST_DIR)/test_healthcheck.c proxy.c proxy_tls.c http.c log.c platform.c $(UNITY_SRC)
$(CC) $(CFLAGS) -I. -I$(UNIT_TEST_DIR)/../unity -o $@ $(UNIT_TEST_DIR)/test_healthcheck.c proxy.c proxy_tls.c http.c log.c platform.c $(UNITY_SRC) $(LDFLAGS)
$(UNIT_TEST_DIR)/test_config: $(UNIT_TEST_DIR)/test_config.c config.c log.c access_log.c http.c $(UNITY_SRC) $(UNIT_TEST_DIR)/test_config: $(UNIT_TEST_DIR)/test_config.c config.c log.c access_log.c http.c $(UNITY_SRC)
$(CC) $(CFLAGS) -I. -I$(UNIT_TEST_DIR)/../unity -o $@ $(UNIT_TEST_DIR)/test_config.c config.c log.c access_log.c http.c $(UNITY_SRC) -lm $(CC) $(CFLAGS) -I. -I$(UNIT_TEST_DIR)/../unity -o $@ $(UNIT_TEST_DIR)/test_config.c config.c log.c access_log.c http.c $(UNITY_SRC) -lm

BIN
tests/unit/test_healthcheck Executable file

Binary file not shown.

View File

@ -0,0 +1,240 @@
#include "unity.h"
#include "healthcheck.h"
#include "proxy.h"
#include <string.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
/* 白盒测试:包含 healthcheck.c 以访问静态函数 */
#define UNIT_TEST
#include "healthcheck.c"
#undef UNIT_TEST
/* Unity 测试框架要求的 setUp/tearDown */
void setUp(void) { }
void tearDown(void) { }
/* ===== healthcheck_parse_status ===== */
void test_parse_status_200(void) {
const char *resp = "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n";
int status = healthcheck_parse_status(resp, strlen(resp));
TEST_ASSERT_EQUAL(200, status);
}
void test_parse_status_404(void) {
const char *resp = "HTTP/1.1 404 Not Found\r\n\r\n";
int status = healthcheck_parse_status(resp, strlen(resp));
TEST_ASSERT_EQUAL(404, status);
}
void test_parse_status_500(void) {
const char *resp = "HTTP/1.0 500 Internal Server Error\r\n\r\n";
int status = healthcheck_parse_status(resp, strlen(resp));
TEST_ASSERT_EQUAL(500, status);
}
void test_parse_status_too_short(void) {
TEST_ASSERT_EQUAL(-1, healthcheck_parse_status("HTTP/1.1", 8));
TEST_ASSERT_EQUAL(-1, healthcheck_parse_status("", 0));
TEST_ASSERT_EQUAL(-1, healthcheck_parse_status("HTTP", 4));
}
void test_parse_status_not_http(void) {
const char *resp = "SSH-2.0-OpenSSH_8.0\r\n";
TEST_ASSERT_EQUAL(-1, healthcheck_parse_status(resp, strlen(resp)));
}
void test_parse_status_malformed_status(void) {
const char *resp = "HTTP/1.1 abc OK\r\n\r\n";
TEST_ASSERT_EQUAL(-1, healthcheck_parse_status(resp, strlen(resp)));
}
void test_parse_status_edge_cases(void) {
/* 100 Continue */
const char *resp = "HTTP/1.1 100 Continue\r\n\r\n";
TEST_ASSERT_EQUAL(100, healthcheck_parse_status(resp, strlen(resp)));
/* 302 Found */
const char *resp2 = "HTTP/1.1 302 Found\r\n\r\n";
TEST_ASSERT_EQUAL(302, healthcheck_parse_status(resp2, strlen(resp2)));
/* 503 Service Unavailable */
const char *resp3 = "HTTP/1.1 503 Service Unavailable\r\n\r\n";
TEST_ASSERT_EQUAL(503, healthcheck_parse_status(resp3, strlen(resp3)));
}
/* ===== healthcheck_build_request ===== */
void test_build_request_default_path(void) {
cocoon_proxy_backend_t backend = {0};
strncpy(backend.target_host, "localhost", sizeof(backend.target_host) - 1);
backend.target_port = 3000;
char buf[512];
healthcheck_build_request(&backend, NULL, buf, sizeof(buf));
TEST_ASSERT_TRUE(strstr(buf, "GET /health HTTP/1.1") != NULL);
TEST_ASSERT_TRUE(strstr(buf, "Host: localhost:3000") != NULL);
TEST_ASSERT_TRUE(strstr(buf, "Connection: close") != NULL);
TEST_ASSERT_TRUE(strstr(buf, "Cocoon-Healthcheck/1.0") != NULL);
}
void test_build_request_custom_path(void) {
cocoon_proxy_backend_t backend = {0};
strncpy(backend.target_host, "api.example.com", sizeof(backend.target_host) - 1);
backend.target_port = 8080;
char buf[512];
healthcheck_build_request(&backend, "/ready", buf, sizeof(buf));
TEST_ASSERT_TRUE(strstr(buf, "GET /ready HTTP/1.1") != NULL);
TEST_ASSERT_TRUE(strstr(buf, "Host: api.example.com:8080") != NULL);
}
void test_build_request_empty_path_uses_default(void) {
cocoon_proxy_backend_t backend = {0};
strncpy(backend.target_host, "127.0.0.1", sizeof(backend.target_host) - 1);
backend.target_port = 5000;
char buf[512];
healthcheck_build_request(&backend, "", buf, sizeof(buf));
TEST_ASSERT_TRUE(strstr(buf, "GET /health HTTP/1.1") != NULL);
}
/* ===== healthcheck_update_state ===== */
void test_update_state_healthy_threshold(void) {
cocoon_proxy_backend_t backend = {0};
backend.healthy = false;
/* 第1次成功 — 仍不健康 */
healthcheck_update_state(&backend, true);
TEST_ASSERT_FALSE(backend.healthy);
TEST_ASSERT_EQUAL(1, backend.success_count);
/* 第2次成功 — 达到阈值,恢复健康 */
healthcheck_update_state(&backend, true);
TEST_ASSERT_TRUE(backend.healthy);
TEST_ASSERT_EQUAL(2, backend.success_count);
TEST_ASSERT_EQUAL(0, backend.fail_count);
}
void test_update_state_unhealthy_threshold(void) {
cocoon_proxy_backend_t backend = {0};
backend.healthy = true;
/* 连续3次失败 — 达到阈值,标记不健康 */
healthcheck_update_state(&backend, false);
TEST_ASSERT_TRUE(backend.healthy); /* 第1次仍健康 */
TEST_ASSERT_EQUAL(1, backend.fail_count);
healthcheck_update_state(&backend, false);
TEST_ASSERT_TRUE(backend.healthy); /* 第2次仍健康 */
TEST_ASSERT_EQUAL(2, backend.fail_count);
healthcheck_update_state(&backend, false);
TEST_ASSERT_FALSE(backend.healthy); /* 第3次标记不健康 */
TEST_ASSERT_EQUAL(3, backend.fail_count);
TEST_ASSERT_EQUAL(0, backend.success_count);
}
void test_update_state_reset_counts(void) {
cocoon_proxy_backend_t backend = {0};
backend.healthy = true;
/* 先失败1次 */
healthcheck_update_state(&backend, false);
TEST_ASSERT_EQUAL(1, backend.fail_count);
/* 然后成功1次 — fail_count 重置 */
healthcheck_update_state(&backend, true);
TEST_ASSERT_TRUE(backend.healthy);
TEST_ASSERT_EQUAL(0, backend.fail_count);
TEST_ASSERT_EQUAL(1, backend.success_count);
/* 再失败1次 — success_count 重置 */
healthcheck_update_state(&backend, false);
TEST_ASSERT_TRUE(backend.healthy); /* 只失败1次仍健康 */
TEST_ASSERT_EQUAL(1, backend.fail_count);
TEST_ASSERT_EQUAL(0, backend.success_count);
}
void test_update_state_stays_healthy_after_single_failure(void) {
cocoon_proxy_backend_t backend = {0};
backend.healthy = true;
/* 只失败1次不应标记不健康 */
healthcheck_update_state(&backend, false);
TEST_ASSERT_TRUE(backend.healthy);
TEST_ASSERT_EQUAL(1, backend.fail_count);
/* 再次成功 */
healthcheck_update_state(&backend, true);
TEST_ASSERT_TRUE(backend.healthy);
}
void test_update_state_stays_unhealthy_after_single_success(void) {
cocoon_proxy_backend_t backend = {0};
backend.healthy = false;
/* 只成功1次不应标记健康 */
healthcheck_update_state(&backend, true);
TEST_ASSERT_FALSE(backend.healthy);
TEST_ASSERT_EQUAL(1, backend.success_count);
}
void test_update_state_last_check_updated(void) {
cocoon_proxy_backend_t backend = {0};
time_t before = time(NULL);
healthcheck_update_state(&backend, true);
time_t after = time(NULL);
TEST_ASSERT_TRUE(backend.last_check >= before);
TEST_ASSERT_TRUE(backend.last_check <= after);
}
/* ===== healthcheck_probe_once ===== */
void test_probe_once_null_backend(void) {
TEST_ASSERT_FALSE(healthcheck_probe_once(NULL, 1000));
}
/* ===== 主函数 ===== */
int main(void) {
UNITY_BEGIN();
/* parse_status */
RUN_TEST(test_parse_status_200);
RUN_TEST(test_parse_status_404);
RUN_TEST(test_parse_status_500);
RUN_TEST(test_parse_status_too_short);
RUN_TEST(test_parse_status_not_http);
RUN_TEST(test_parse_status_malformed_status);
RUN_TEST(test_parse_status_edge_cases);
/* build_request */
RUN_TEST(test_build_request_default_path);
RUN_TEST(test_build_request_custom_path);
RUN_TEST(test_build_request_empty_path_uses_default);
/* update_state */
RUN_TEST(test_update_state_healthy_threshold);
RUN_TEST(test_update_state_unhealthy_threshold);
RUN_TEST(test_update_state_reset_counts);
RUN_TEST(test_update_state_stays_healthy_after_single_failure);
RUN_TEST(test_update_state_stays_unhealthy_after_single_success);
RUN_TEST(test_update_state_last_check_updated);
/* probe_once */
RUN_TEST(test_probe_once_null_backend);
return UNITY_END();
}