feat(tests): 添加 config 和 log 模块单元测试 + http.c 缓冲区溢出防护

- 新增 test_log.c: 8 个测试覆盖日志级别 getter/setter、前缀设置、持久化
- 新增 test_config.c: 11 个测试覆盖 JSON 配置加载、注释支持、错误处理、merge 覆盖逻辑
- http.c: 修复 response header 格式化中的 snprintf 边界条件,防止缓冲区溢出
- Makefile: 添加 test_log、test_config 编译规则,更新 .PHONY 列表
This commit is contained in:
xfy911 2026-06-04 00:02:18 +08:00
parent d5b8782a1b
commit 3c521823b1
10 changed files with 322 additions and 16 deletions

View File

@ -84,6 +84,12 @@ $(UNIT_TEST_DIR)/test_http: $(UNIT_TEST_DIR)/test_http.c http.c log.c $(UNITY_SR
$(UNIT_TEST_DIR)/test_static: $(UNIT_TEST_DIR)/test_static.c http.c log.c $(UNITY_SRC) $(UNIT_TEST_DIR)/test_static: $(UNIT_TEST_DIR)/test_static.c http.c log.c $(UNITY_SRC)
$(CC) $(CFLAGS) -D_GNU_SOURCE -I. -I$(UNIT_TEST_DIR)/../unity -o $@ $(UNIT_TEST_DIR)/test_static.c http.c log.c $(UNITY_SRC) -lm -lz $(CC) $(CFLAGS) -D_GNU_SOURCE -I. -I$(UNIT_TEST_DIR)/../unity -o $@ $(UNIT_TEST_DIR)/test_static.c http.c log.c $(UNITY_SRC) -lm -lz
$(UNIT_TEST_DIR)/test_log: $(UNIT_TEST_DIR)/test_log.c log.c $(UNITY_SRC)
$(CC) $(CFLAGS) -D_GNU_SOURCE -I. -I$(UNIT_TEST_DIR)/../unity -o $@ $(UNIT_TEST_DIR)/test_log.c log.c $(UNITY_SRC) -lm
$(UNIT_TEST_DIR)/test_config: $(UNIT_TEST_DIR)/test_config.c config.c log.c $(UNITY_SRC)
$(CC) $(CFLAGS) -D_GNU_SOURCE -I. -I$(UNIT_TEST_DIR)/../unity -o $@ $(UNIT_TEST_DIR)/test_config.c config.c log.c $(UNITY_SRC) -lm
# 编译规则 # 编译规则
%.o: %.c %.o: %.c
$(CC) $(CFLAGS) -c $< -o $@ $(CC) $(CFLAGS) -c $< -o $@
@ -105,4 +111,4 @@ clean:
# 重新构建 # 重新构建
rebuild: clean all rebuild: clean all
.PHONY: all clean install uninstall rebuild deps build-all test bench unit-test .PHONY: all clean install uninstall rebuild deps build-all test bench unit-test test-all

36
http.c
View File

@ -245,7 +245,7 @@ int http_format_response_header(char *buf, size_t buf_size, const http_response_
/* 状态行 */ /* 状态行 */
if (resp->has_range) { if (resp->has_range) {
n += snprintf(buf + n, buf_size - n, n += snprintf(buf + n, buf_size - (size_t)n > buf_size ? 0 : buf_size - (size_t)n,
"HTTP/1.1 %d %s\r\n" "HTTP/1.1 %d %s\r\n"
"Content-Type: %s\r\n" "Content-Type: %s\r\n"
"Content-Length: %ld\r\n" "Content-Length: %ld\r\n"
@ -256,7 +256,7 @@ int http_format_response_header(char *buf, size_t buf_size, const http_response_
(long)resp->content_length, (long)resp->content_length,
(long)resp->range_start, (long)resp->range_end, (long)resp->total_length); (long)resp->range_start, (long)resp->range_end, (long)resp->total_length);
} else { } else {
n += snprintf(buf + n, buf_size - n, n += snprintf(buf + n, buf_size - (size_t)n > buf_size ? 0 : buf_size - (size_t)n,
"HTTP/1.1 %d %s\r\n" "HTTP/1.1 %d %s\r\n"
"Content-Type: %s\r\n" "Content-Type: %s\r\n"
"Content-Length: %ld\r\n" "Content-Length: %ld\r\n"
@ -268,21 +268,37 @@ int http_format_response_header(char *buf, size_t buf_size, const http_response_
/* 缓存头 */ /* 缓存头 */
if (resp->etag && resp->etag[0]) { if (resp->etag && resp->etag[0]) {
n += snprintf(buf + n, buf_size - n, "ETag: %s\r\n", resp->etag); if ((size_t)n < buf_size) {
n += snprintf(buf + n, buf_size - (size_t)n, "ETag: %s\r\n", resp->etag);
} else {
n += (int)strlen(resp->etag) + 8;
}
} }
if (resp->last_modified && resp->last_modified[0]) { if (resp->last_modified && resp->last_modified[0]) {
n += snprintf(buf + n, buf_size - n, "Last-Modified: %s\r\n", resp->last_modified); if ((size_t)n < buf_size) {
n += snprintf(buf + n, buf_size - (size_t)n, "Last-Modified: %s\r\n", resp->last_modified);
} else {
n += (int)strlen(resp->last_modified) + 16;
}
} }
if (resp->content_encoding && resp->content_encoding[0]) { if (resp->content_encoding && resp->content_encoding[0]) {
n += snprintf(buf + n, buf_size - n, "Content-Encoding: %s\r\n", resp->content_encoding); if ((size_t)n < buf_size) {
n += snprintf(buf + n, buf_size - (size_t)n, "Content-Encoding: %s\r\n", resp->content_encoding);
} else {
n += (int)strlen(resp->content_encoding) + 20;
}
} }
/* 连接头 + 空行 */ /* 连接头 + 空行 */
n += snprintf(buf + n, buf_size - n, if ((size_t)n < buf_size) {
"Connection: %s\r\n" n += snprintf(buf + n, buf_size - (size_t)n,
"Server: Cocoon/1.0\r\n" "Connection: %s\r\n"
"\r\n", "Server: Cocoon/1.0\r\n"
resp->keep_alive ? "keep-alive" : "close"); "\r\n",
resp->keep_alive ? "keep-alive" : "close");
} else {
n += 35; /* approximate */
}
if ((size_t)n >= buf_size) return -1; if ((size_t)n >= buf_size) return -1;
return n; return n;

BIN
tests/unit/test_config Executable file

Binary file not shown.

206
tests/unit/test_config.c Normal file
View File

@ -0,0 +1,206 @@
#include "unity.h"
#include "config.h"
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
/* 辅助:写临时配置文件,返回路径 */
static const char *write_temp_config(const char *content) {
static char path[256];
static int counter = 0;
snprintf(path, sizeof(path), "/tmp/cocoon_test_config_%d_%d.json",
(int)getpid(), counter++);
FILE *fp = fopen(path, "w");
if (fp) {
fwrite(content, 1, strlen(content), fp);
fclose(fp);
}
return path;
}
static void cleanup(const char *path) {
remove(path);
}
/* ===== config_load_from_file ===== */
void test_load_valid_config(void) {
const char *p = write_temp_config(
"{\n"
" \"root_dir\": \"/var/www\",\n"
" \"port\": 8080,\n"
" \"threaded\": true,\n"
" \"num_workers\": 8,\n"
" \"max_connections\": 5000,\n"
" \"timeout_ms\": 60000,\n"
" \"log_level\": \"debug\"\n"
"}\n"
);
cocoon_config_t cfg = {0};
TEST_ASSERT_TRUE(config_load_from_file(p, &cfg));
TEST_ASSERT_EQUAL_STRING("/var/www", cfg.root_dir);
TEST_ASSERT_EQUAL(8080, cfg.port);
TEST_ASSERT_TRUE(cfg.threaded);
TEST_ASSERT_EQUAL(8, cfg.num_workers);
TEST_ASSERT_EQUAL(5000, cfg.max_connections);
TEST_ASSERT_EQUAL(60000, cfg.timeout_ms);
TEST_ASSERT_EQUAL(LOG_LEVEL_DEBUG, cfg.log_level);
free((void *)cfg.root_dir);
cleanup(p);
}
void test_load_valid_minimal(void) {
const char *p = write_temp_config("{\"port\": 3000}");
cocoon_config_t cfg = {0};
TEST_ASSERT_TRUE(config_load_from_file(p, &cfg));
TEST_ASSERT_EQUAL(3000, cfg.port);
free((void *)cfg.root_dir);
cleanup(p);
}
void test_load_valid_with_comments(void) {
const char *p = write_temp_config(
"{\n"
" // this is a comment\n"
" \"port\": 9090\n"
"}\n"
);
cocoon_config_t cfg = {0};
TEST_ASSERT_TRUE(config_load_from_file(p, &cfg));
TEST_ASSERT_EQUAL(9090, cfg.port);
free((void *)cfg.root_dir);
cleanup(p);
}
void test_load_invalid_json(void) {
const char *p = write_temp_config("not json");
cocoon_config_t cfg = {0};
TEST_ASSERT_FALSE(config_load_from_file(p, &cfg));
cleanup(p);
}
void test_load_missing_file(void) {
cocoon_config_t cfg = {0};
TEST_ASSERT_FALSE(config_load_from_file(
"/tmp/cocoon_nonexist_12345.json", &cfg));
}
void test_load_empty_file(void) {
const char *p = write_temp_config("");
cocoon_config_t cfg = {0};
TEST_ASSERT_FALSE(config_load_from_file(p, &cfg));
cleanup(p);
}
void test_load_null_args(void) {
TEST_ASSERT_FALSE(config_load_from_file(NULL, NULL));
TEST_ASSERT_FALSE(config_load_from_file("/tmp/x", NULL));
}
/* ===== config_merge ===== */
void test_merge_override_all(void) {
cocoon_config_t base = {
.root_dir = strdup("/old"),
.port = 8080,
.threaded = false,
.num_workers = 2,
.max_connections = 100,
.timeout_ms = 30000,
.log_level = LOG_LEVEL_INFO
};
cocoon_config_t cmdline = {
.root_dir = "/new",
.port = 9090,
.threaded = true,
.num_workers = 4,
.max_connections = 200,
.timeout_ms = 60000,
.log_level = LOG_LEVEL_DEBUG
};
config_merge(&base, &cmdline,
true, true, true, true, true, true);
TEST_ASSERT_EQUAL_STRING("/new", base.root_dir);
TEST_ASSERT_EQUAL(9090, base.port);
TEST_ASSERT_TRUE(base.threaded);
TEST_ASSERT_EQUAL(4, base.num_workers);
TEST_ASSERT_EQUAL(200, base.max_connections);
TEST_ASSERT_EQUAL(60000, base.timeout_ms);
TEST_ASSERT_EQUAL(LOG_LEVEL_DEBUG, base.log_level);
free((void *)base.root_dir);
}
void test_merge_no_override(void) {
cocoon_config_t base = {
.root_dir = strdup("/old"),
.port = 8080,
.threaded = false,
.num_workers = 2,
.log_level = LOG_LEVEL_INFO
};
cocoon_config_t cmdline = {
.root_dir = "/new",
.port = 9090,
.num_workers = 4,
.log_level = LOG_LEVEL_DEBUG
};
config_merge(&base, &cmdline,
false, false, false, false, false, false);
TEST_ASSERT_EQUAL_STRING("/old", base.root_dir);
TEST_ASSERT_EQUAL(8080, base.port);
TEST_ASSERT_FALSE(base.threaded);
TEST_ASSERT_EQUAL(2, base.num_workers);
TEST_ASSERT_EQUAL(LOG_LEVEL_INFO, base.log_level);
free((void *)base.root_dir);
}
void test_merge_partial_override(void) {
cocoon_config_t base = {
.root_dir = strdup("/old"),
.port = 8080,
.num_workers = 2,
.log_level = LOG_LEVEL_INFO
};
cocoon_config_t cmdline = {
.root_dir = "/new",
.port = 9090,
.num_workers = 4,
.log_level = LOG_LEVEL_DEBUG
};
config_merge(&base, &cmdline,
true, false, false, true, false, false);
TEST_ASSERT_EQUAL_STRING("/new", base.root_dir); /* overridden */
TEST_ASSERT_EQUAL(8080, base.port); /* not overridden */
TEST_ASSERT_EQUAL(2, base.num_workers); /* not overridden */
TEST_ASSERT_EQUAL(LOG_LEVEL_INFO, base.log_level); /* not overridden */
free((void *)base.root_dir);
}
void test_merge_null_safety(void) {
/* 不应 crash */
config_merge(NULL, NULL, true, true, true, true, true, true);
TEST_ASSERT_TRUE(1);
}
void setUp(void) {}
void tearDown(void) {}
int main(void) {
UNITY_BEGIN();
RUN_TEST(test_load_valid_config);
RUN_TEST(test_load_valid_minimal);
RUN_TEST(test_load_valid_with_comments);
RUN_TEST(test_load_invalid_json);
RUN_TEST(test_load_missing_file);
RUN_TEST(test_load_empty_file);
RUN_TEST(test_load_null_args);
RUN_TEST(test_merge_override_all);
RUN_TEST(test_merge_no_override);
RUN_TEST(test_merge_partial_override);
RUN_TEST(test_merge_null_safety);
return UNITY_END();
}

Binary file not shown.

View File

@ -179,11 +179,8 @@ void test_parse_keep_alive_override(void) {
http_request_t parsed = {0}; http_request_t parsed = {0};
http_parse_request(req, strlen(req), &parsed); http_parse_request(req, strlen(req), &parsed);
/* 解析器先设置 HTTP/1.1 默认 keep-alive=true然后解析头部时如果 Connection: close 则设为 false */ /* HTTP/1.1 默认 keep-alive=true但 Connection: close 应覆盖为 false */
/* 但当前实现是 Connection: keep-alive 才设为 true否则保持 false */ TEST_ASSERT_FALSE(parsed.keep_alive);
/* 实际上 HTTP/1.1 默认 keep-alive这里应该是 true 因为代码逻辑是先设置默认值,再解析头部 */
/* 修改测试以匹配实际行为:代码在解析头部后没有强制覆盖默认值 */
TEST_ASSERT_TRUE(parsed.keep_alive); /* HTTP/1.1 默认保持 */
http_request_free(&parsed); http_request_free(&parsed);
} }

Binary file not shown.

BIN
tests/unit/test_log Executable file

Binary file not shown.

81
tests/unit/test_log.c Normal file
View File

@ -0,0 +1,81 @@
#include "unity.h"
#include "log.h"
/* ===== log_get_level / log_set_level ===== */
void test_default_level_is_info(void) {
/* 静态全局变量默认初始化为 LOG_LEVEL_INFO */
TEST_ASSERT_EQUAL(LOG_LEVEL_INFO, log_get_level());
}
void test_set_level_error(void) {
log_set_level(LOG_LEVEL_ERROR);
TEST_ASSERT_EQUAL(LOG_LEVEL_ERROR, log_get_level());
}
void test_set_level_warn(void) {
log_set_level(LOG_LEVEL_WARN);
TEST_ASSERT_EQUAL(LOG_LEVEL_WARN, log_get_level());
}
void test_set_level_debug(void) {
log_set_level(LOG_LEVEL_DEBUG);
TEST_ASSERT_EQUAL(LOG_LEVEL_DEBUG, log_get_level());
}
void test_set_level_info(void) {
log_set_level(LOG_LEVEL_INFO);
TEST_ASSERT_EQUAL(LOG_LEVEL_INFO, log_get_level());
}
/* ===== log_set_prefix ===== */
void test_set_prefix(void) {
log_set_prefix("[Test]");
/* 无返回值,仅确认不 crash */
TEST_ASSERT_EQUAL(LOG_LEVEL_INFO, log_get_level());
}
void test_set_prefix_null(void) {
log_set_prefix(NULL);
/* NULL 表示无前缀,不 crash */
TEST_ASSERT_EQUAL(LOG_LEVEL_INFO, log_get_level());
}
/* ===== 边界:多次设置 ===== */
void test_level_persistence(void) {
log_set_level(LOG_LEVEL_DEBUG);
TEST_ASSERT_EQUAL(LOG_LEVEL_DEBUG, log_get_level());
log_set_level(LOG_LEVEL_ERROR);
TEST_ASSERT_EQUAL(LOG_LEVEL_ERROR, log_get_level());
log_set_level(LOG_LEVEL_INFO);
TEST_ASSERT_EQUAL(LOG_LEVEL_INFO, log_get_level());
}
void setUp(void) {
/* 每个测试前重置为默认 */
log_set_level(LOG_LEVEL_INFO);
log_set_prefix("[Cocoon]");
}
void tearDown(void) {
/* 每个测试后重置 */
log_set_level(LOG_LEVEL_INFO);
log_set_prefix("[Cocoon]");
}
int main(void) {
UNITY_BEGIN();
RUN_TEST(test_default_level_is_info);
RUN_TEST(test_set_level_error);
RUN_TEST(test_set_level_warn);
RUN_TEST(test_set_level_debug);
RUN_TEST(test_set_level_info);
RUN_TEST(test_set_prefix);
RUN_TEST(test_set_prefix_null);
RUN_TEST(test_level_persistence);
return UNITY_END();
}

Binary file not shown.