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:
parent
60ef9ed908
commit
0e7de9400c
@ -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` 字段
|
||||||
|
|||||||
4
Makefile
4
Makefile
@ -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
BIN
tests/unit/test_healthcheck
Executable file
Binary file not shown.
240
tests/unit/test_healthcheck.c
Normal file
240
tests/unit/test_healthcheck.c
Normal 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();
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user