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:
parent
d5b8782a1b
commit
3c521823b1
8
Makefile
8
Makefile
@ -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)
|
||||
$(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
|
||||
$(CC) $(CFLAGS) -c $< -o $@
|
||||
@ -105,4 +111,4 @@ clean:
|
||||
# 重新构建
|
||||
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
|
||||
|
||||
28
http.c
28
http.c
@ -245,7 +245,7 @@ int http_format_response_header(char *buf, size_t buf_size, const http_response_
|
||||
|
||||
/* 状态行 */
|
||||
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"
|
||||
"Content-Type: %s\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->range_start, (long)resp->range_end, (long)resp->total_length);
|
||||
} 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"
|
||||
"Content-Type: %s\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]) {
|
||||
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]) {
|
||||
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]) {
|
||||
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) {
|
||||
n += snprintf(buf + n, buf_size - (size_t)n,
|
||||
"Connection: %s\r\n"
|
||||
"Server: Cocoon/1.0\r\n"
|
||||
"\r\n",
|
||||
resp->keep_alive ? "keep-alive" : "close");
|
||||
} else {
|
||||
n += 35; /* approximate */
|
||||
}
|
||||
|
||||
if ((size_t)n >= buf_size) return -1;
|
||||
return n;
|
||||
|
||||
BIN
tests/unit/test_config
Executable file
BIN
tests/unit/test_config
Executable file
Binary file not shown.
206
tests/unit/test_config.c
Normal file
206
tests/unit/test_config.c
Normal 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.
@ -179,11 +179,8 @@ void test_parse_keep_alive_override(void) {
|
||||
http_request_t parsed = {0};
|
||||
http_parse_request(req, strlen(req), &parsed);
|
||||
|
||||
/* 解析器先设置 HTTP/1.1 默认 keep-alive=true,然后解析头部时,如果 Connection: close 则设为 false */
|
||||
/* 但当前实现是 Connection: keep-alive 才设为 true,否则保持 false */
|
||||
/* 实际上 HTTP/1.1 默认 keep-alive,这里应该是 true 因为代码逻辑是先设置默认值,再解析头部 */
|
||||
/* 修改测试以匹配实际行为:代码在解析头部后没有强制覆盖默认值 */
|
||||
TEST_ASSERT_TRUE(parsed.keep_alive); /* HTTP/1.1 默认保持 */
|
||||
/* HTTP/1.1 默认 keep-alive=true,但 Connection: close 应覆盖为 false */
|
||||
TEST_ASSERT_FALSE(parsed.keep_alive);
|
||||
http_request_free(&parsed);
|
||||
}
|
||||
|
||||
|
||||
Binary file not shown.
BIN
tests/unit/test_log
Executable file
BIN
tests/unit/test_log
Executable file
Binary file not shown.
81
tests/unit/test_log.c
Normal file
81
tests/unit/test_log.c
Normal 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.
Loading…
x
Reference in New Issue
Block a user