- 添加缺失的 parser 辅助函数(parser_expect_string, parser_skip_value, parser_number_str) - 修复 Makefile:添加 test_cache 构建规则,补充 cache.c 依赖 - 修复 test_cache.c:使用 cache_stats() API 替代直接访问内部字段 - 修复 test_config.c:更新 config_merge() 调用参数以匹配新签名 - 修复 CI 工作流:使用 make build-all 构建依赖
This commit is contained in:
parent
8c1c103a1b
commit
eb38c9d942
14
Makefile
14
Makefile
@ -20,7 +20,7 @@ PREFIX ?= /usr/local
|
||||
BINDIR = $(PREFIX)/bin
|
||||
|
||||
# 源文件
|
||||
SRCS = main.c server.c http.c static.c log.c config.c multipart.c tls.c http2.c access_log.c websocket.c platform.c middleware.c plugin.c proxy.c proxy_tls.c healthcheck.c middleware_ext.c load_balance.c grpc.c http3.c sse.c fastcgi.c fcgi_handler.c
|
||||
SRCS = main.c server.c http.c static.c log.c config.c multipart.c tls.c http2.c access_log.c websocket.c platform.c middleware.c plugin.c proxy.c proxy_tls.c healthcheck.c middleware_ext.c load_balance.c grpc.c http3.c sse.c fastcgi.c fcgi_handler.c cache.c
|
||||
OBJS = $(SRCS:.c=.o)
|
||||
TARGET = cocoon
|
||||
|
||||
@ -90,8 +90,8 @@ unit-test: $(UNIT_TEST_BINS)
|
||||
fi
|
||||
|
||||
# 单元测试编译规则
|
||||
$(UNIT_TEST_DIR)/test_server: $(UNIT_TEST_DIR)/test_server.c server.c http.c static.c log.c config.c multipart.c tls.c http2.c access_log.c websocket.c platform.c middleware.c plugin.c proxy.c proxy_tls.c healthcheck.c sse.c fastcgi.c fcgi_handler.c $(UNITY_SRC)
|
||||
$(CC) $(CFLAGS) -I. -I$(UNIT_TEST_DIR)/../unity -o $@ $(UNIT_TEST_DIR)/test_server.c server.c http.c static.c log.c config.c multipart.c tls.c http2.c access_log.c websocket.c platform.c middleware.c plugin.c proxy.c proxy_tls.c healthcheck.c sse.c fastcgi.c fcgi_handler.c $(UNITY_SRC) $(LDFLAGS)
|
||||
$(UNIT_TEST_DIR)/test_server: $(UNIT_TEST_DIR)/test_server.c server.c http.c static.c log.c config.c multipart.c tls.c http2.c access_log.c websocket.c platform.c middleware.c plugin.c proxy.c proxy_tls.c healthcheck.c sse.c fastcgi.c fcgi_handler.c cache.c $(UNITY_SRC)
|
||||
$(CC) $(CFLAGS) -I. -I$(UNIT_TEST_DIR)/../unity -o $@ $(UNIT_TEST_DIR)/test_server.c server.c http.c static.c log.c config.c multipart.c tls.c http2.c access_log.c websocket.c platform.c middleware.c plugin.c proxy.c proxy_tls.c healthcheck.c sse.c fastcgi.c fcgi_handler.c cache.c $(UNITY_SRC) $(LDFLAGS)
|
||||
|
||||
$(UNIT_TEST_DIR)/test_multipart: $(UNIT_TEST_DIR)/test_multipart.c multipart.c $(UNITY_SRC)
|
||||
$(CC) $(CFLAGS) -I. -I$(UNIT_TEST_DIR)/../unity -o $@ $(UNIT_TEST_DIR)/test_multipart.c multipart.c $(UNITY_SRC) -lm
|
||||
@ -99,8 +99,8 @@ $(UNIT_TEST_DIR)/test_multipart: $(UNIT_TEST_DIR)/test_multipart.c multipart.c $
|
||||
$(UNIT_TEST_DIR)/test_http: $(UNIT_TEST_DIR)/test_http.c http.c log.c $(UNITY_SRC)
|
||||
$(CC) $(CFLAGS) -I. -I$(UNIT_TEST_DIR)/../unity -o $@ $(UNIT_TEST_DIR)/test_http.c http.c log.c $(UNITY_SRC) -lm
|
||||
|
||||
$(UNIT_TEST_DIR)/test_static: $(UNIT_TEST_DIR)/test_static.c http.c log.c tls.c access_log.c platform.c $(UNITY_SRC)
|
||||
$(CC) $(CFLAGS) -I. -I$(UNIT_TEST_DIR)/../unity -o $@ $(UNIT_TEST_DIR)/test_static.c http.c log.c tls.c access_log.c platform.c $(UNITY_SRC) $(LDFLAGS)
|
||||
$(UNIT_TEST_DIR)/test_static: $(UNIT_TEST_DIR)/test_static.c http.c log.c tls.c access_log.c platform.c cache.c $(UNITY_SRC)
|
||||
$(CC) $(CFLAGS) -I. -I$(UNIT_TEST_DIR)/../unity -o $@ $(UNIT_TEST_DIR)/test_static.c http.c log.c tls.c access_log.c platform.c cache.c $(UNITY_SRC) $(LDFLAGS)
|
||||
|
||||
$(UNIT_TEST_DIR)/test_websocket: $(UNIT_TEST_DIR)/test_websocket.c websocket.c log.c platform.c $(UNITY_SRC)
|
||||
$(CC) $(CFLAGS) -I. -I$(UNIT_TEST_DIR)/../unity -o $@ $(UNIT_TEST_DIR)/test_websocket.c websocket.c log.c platform.c $(UNITY_SRC) $(LDFLAGS)
|
||||
@ -142,6 +142,10 @@ $(UNIT_TEST_DIR)/test_sse: $(UNIT_TEST_DIR)/test_sse.c sse.c log.c $(UNITY_SRC)
|
||||
$(UNIT_TEST_DIR)/test_fastcgi: $(UNIT_TEST_DIR)/test_fastcgi.c fastcgi.c log.c $(UNITY_SRC)
|
||||
$(CC) $(CFLAGS) -I. -I$(UNIT_TEST_DIR)/../unity -o $@ $(UNIT_TEST_DIR)/test_fastcgi.c fastcgi.c log.c $(UNITY_SRC) $(LDFLAGS)
|
||||
|
||||
# 缓存测试
|
||||
$(UNIT_TEST_DIR)/test_cache: $(UNIT_TEST_DIR)/test_cache.c cache.c log.c $(UNITY_SRC)
|
||||
$(CC) $(CFLAGS) -I. -I$(UNIT_TEST_DIR)/../unity -o $@ $(UNIT_TEST_DIR)/test_cache.c cache.c log.c $(UNITY_SRC) $(LDFLAGS)
|
||||
|
||||
# 安装
|
||||
install: $(TARGET)
|
||||
install -d $(BINDIR)
|
||||
|
||||
400
cache.c
Normal file
400
cache.c
Normal file
@ -0,0 +1,400 @@
|
||||
/**
|
||||
* cache.c - 内存响应缓存实现
|
||||
*
|
||||
* LRU + TTL 缓存,哈希表 + 双向链表。
|
||||
* 线程安全:所有操作受 pthread_mutex 保护。
|
||||
*
|
||||
* @author xfy
|
||||
*/
|
||||
|
||||
#include "cache.h"
|
||||
#include "log.h"
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <pthread.h>
|
||||
|
||||
#define CACHE_DEFAULT_MAX_SIZE (64 * 1024 * 1024) /**< 默认 64MB */
|
||||
#define CACHE_DEFAULT_TTL_SECONDS 60 /**< 默认 60 秒 */
|
||||
#define CACHE_DEFAULT_MAX_ENTRY_SIZE (1024 * 1024) /**< 默认 1MB */
|
||||
#define CACHE_DEFAULT_BUCKET_COUNT 512 /**< 哈希桶数 */
|
||||
|
||||
/**
|
||||
* cache_node_t - 内部链表节点
|
||||
*
|
||||
* 双向链表实现 LRU:头节点为最近使用,尾节点为最久未使用。
|
||||
*/
|
||||
typedef struct cache_node {
|
||||
char *key; /**< 键(独立拷贝) */
|
||||
char *header; /**< 响应头 */
|
||||
size_t header_len; /**< 响应头长度 */
|
||||
char *body; /**< 响应体 */
|
||||
size_t body_len; /**< 响应体长度 */
|
||||
time_t mtime; /**< 文件修改时间 */
|
||||
time_t expires_at; /**< TTL 过期时间 */
|
||||
struct cache_node *next; /**< LRU 链表:前驱(更近) */
|
||||
struct cache_node *prev; /**< LRU 链表:后继(更久) */
|
||||
struct cache_node *hash_next; /**< 哈希链表 */
|
||||
} cache_node_t;
|
||||
|
||||
struct cocoon_cache {
|
||||
cache_node_t *lru_head; /**< LRU 头(最近使用) */
|
||||
cache_node_t *lru_tail; /**< LRU 尾(最久未使用) */
|
||||
cache_node_t **buckets; /**< 哈希桶数组 */
|
||||
size_t bucket_count; /**< 桶数量 */
|
||||
size_t max_size; /**< 最大总容量 */
|
||||
size_t max_entry_size; /**< 单条最大大小 */
|
||||
uint32_t ttl_seconds; /**< TTL 秒数 */
|
||||
size_t total_size; /**< 当前总大小 */
|
||||
size_t hits; /**< 命中次数 */
|
||||
size_t misses; /**< 未命中次数 */
|
||||
size_t evictions; /**< 淘汰次数 */
|
||||
size_t expirations; /**< 过期次数 */
|
||||
pthread_mutex_t lock; /**< 线程锁 */
|
||||
};
|
||||
|
||||
/* === 内部辅助函数 === */
|
||||
|
||||
/**
|
||||
* hash_fn - djb2 哈希
|
||||
*/
|
||||
static size_t hash_fn(const char *str) {
|
||||
size_t hash = 5381;
|
||||
int c;
|
||||
while ((c = (unsigned char)*str++)) {
|
||||
hash = ((hash << 5) + hash) + c;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
/**
|
||||
* lru_move_to_head - 将节点移到 LRU 链表头部(最近使用)
|
||||
*/
|
||||
static void lru_move_to_head(cocoon_cache_t *cache, cache_node_t *node) {
|
||||
if (cache->lru_head == node) return;
|
||||
|
||||
/* 从原位置移除 */
|
||||
if (node->prev) node->prev->next = node->next;
|
||||
if (node->next) node->next->prev = node->prev;
|
||||
if (cache->lru_tail == node) cache->lru_tail = node->prev;
|
||||
|
||||
/* 插入头部 */
|
||||
node->next = cache->lru_head;
|
||||
node->prev = NULL;
|
||||
if (cache->lru_head) cache->lru_head->prev = node;
|
||||
cache->lru_head = node;
|
||||
if (!cache->lru_tail) cache->lru_tail = node;
|
||||
}
|
||||
|
||||
/**
|
||||
* lru_remove - 从 LRU 链表中移除节点
|
||||
*/
|
||||
static void lru_remove(cocoon_cache_t *cache, cache_node_t *node) {
|
||||
if (node->prev) node->prev->next = node->next;
|
||||
else cache->lru_head = node->next;
|
||||
|
||||
if (node->next) node->next->prev = node->prev;
|
||||
else cache->lru_tail = node->prev;
|
||||
|
||||
node->prev = NULL;
|
||||
node->next = NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* hash_remove - 从哈希表中移除节点
|
||||
*/
|
||||
static void hash_remove(cocoon_cache_t *cache, const char *key) {
|
||||
size_t idx = hash_fn(key) % cache->bucket_count;
|
||||
cache_node_t **pp = &cache->buckets[idx];
|
||||
while (*pp) {
|
||||
if (strcmp((*pp)->key, key) == 0) {
|
||||
*pp = (*pp)->hash_next;
|
||||
return;
|
||||
}
|
||||
pp = &(*pp)->hash_next;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* hash_insert - 插入到哈希表
|
||||
*/
|
||||
static void hash_insert(cocoon_cache_t *cache, cache_node_t *node) {
|
||||
size_t idx = hash_fn(node->key) % cache->bucket_count;
|
||||
node->hash_next = cache->buckets[idx];
|
||||
cache->buckets[idx] = node;
|
||||
}
|
||||
|
||||
/**
|
||||
* hash_find - 在哈希表中查找节点
|
||||
*/
|
||||
static cache_node_t *hash_find(cocoon_cache_t *cache, const char *key) {
|
||||
size_t idx = hash_fn(key) % cache->bucket_count;
|
||||
cache_node_t *node = cache->buckets[idx];
|
||||
while (node) {
|
||||
if (strcmp(node->key, key) == 0) {
|
||||
return node;
|
||||
}
|
||||
node = node->hash_next;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* node_free - 释放单个节点
|
||||
*/
|
||||
static void node_free(cache_node_t *node) {
|
||||
if (!node) return;
|
||||
free(node->key);
|
||||
free(node->header);
|
||||
free(node->body);
|
||||
free(node);
|
||||
}
|
||||
|
||||
/**
|
||||
* evict_one - 淘汰最久未使用的条目
|
||||
*/
|
||||
static void evict_one(cocoon_cache_t *cache) {
|
||||
cache_node_t *victim = cache->lru_tail;
|
||||
if (!victim) return;
|
||||
|
||||
lru_remove(cache, victim);
|
||||
hash_remove(cache, victim->key);
|
||||
|
||||
size_t entry_size = victim->header_len + victim->body_len;
|
||||
cache->total_size -= entry_size;
|
||||
cache->evictions++;
|
||||
|
||||
log_debug("[Cache] LRU 淘汰: %s (大小 %zu 字节)", victim->key, entry_size);
|
||||
node_free(victim);
|
||||
}
|
||||
|
||||
/**
|
||||
* make_room - 为新条目腾出空间
|
||||
*/
|
||||
static void make_room(cocoon_cache_t *cache, size_t needed) {
|
||||
while (cache->total_size + needed > cache->max_size && cache->lru_tail) {
|
||||
evict_one(cache);
|
||||
}
|
||||
}
|
||||
|
||||
/* === 公共 API === */
|
||||
|
||||
cocoon_cache_t *cache_create(size_t max_size, uint32_t ttl_seconds, size_t max_entry_size) {
|
||||
cocoon_cache_t *cache = (cocoon_cache_t *)calloc(1, sizeof(cocoon_cache_t));
|
||||
if (!cache) {
|
||||
log_error("[Cache] 分配缓存结构失败");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
cache->max_size = max_size > 0 ? max_size : CACHE_DEFAULT_MAX_SIZE;
|
||||
cache->ttl_seconds = ttl_seconds > 0 ? ttl_seconds : CACHE_DEFAULT_TTL_SECONDS;
|
||||
cache->max_entry_size = max_entry_size > 0 ? max_entry_size : CACHE_DEFAULT_MAX_ENTRY_SIZE;
|
||||
cache->bucket_count = CACHE_DEFAULT_BUCKET_COUNT;
|
||||
cache->buckets = (cache_node_t **)calloc(cache->bucket_count, sizeof(cache_node_t *));
|
||||
if (!cache->buckets) {
|
||||
log_error("[Cache] 分配哈希桶失败");
|
||||
free(cache);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (pthread_mutex_init(&cache->lock, NULL) != 0) {
|
||||
log_error("[Cache] 初始化互斥锁失败");
|
||||
free(cache->buckets);
|
||||
free(cache);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
log_info("[Cache] 已创建: 最大容量 %zu 字节, TTL %u 秒, 单条上限 %zu 字节",
|
||||
cache->max_size, cache->ttl_seconds, cache->max_entry_size);
|
||||
return cache;
|
||||
}
|
||||
|
||||
void cache_destroy(cocoon_cache_t *cache) {
|
||||
if (!cache) return;
|
||||
|
||||
pthread_mutex_lock(&cache->lock);
|
||||
|
||||
cache_node_t *node = cache->lru_head;
|
||||
while (node) {
|
||||
cache_node_t *next = node->next;
|
||||
node_free(node);
|
||||
node = next;
|
||||
}
|
||||
|
||||
free(cache->buckets);
|
||||
pthread_mutex_unlock(&cache->lock);
|
||||
pthread_mutex_destroy(&cache->lock);
|
||||
free(cache);
|
||||
|
||||
log_info("[Cache] 已销毁");
|
||||
}
|
||||
|
||||
const cocoon_cache_entry_t *cache_get(cocoon_cache_t *cache, const char *key, time_t mtime) {
|
||||
if (!cache || !key) return NULL;
|
||||
|
||||
pthread_mutex_lock(&cache->lock);
|
||||
|
||||
cache_node_t *node = hash_find(cache, key);
|
||||
if (!node) {
|
||||
cache->misses++;
|
||||
pthread_mutex_unlock(&cache->lock);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* 检查 TTL */
|
||||
time_t now = time(NULL);
|
||||
if (now > node->expires_at) {
|
||||
cache->expirations++;
|
||||
cache->misses++;
|
||||
lru_remove(cache, node);
|
||||
hash_remove(cache, key);
|
||||
cache->total_size -= (node->header_len + node->body_len);
|
||||
node_free(node);
|
||||
pthread_mutex_unlock(&cache->lock);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* 检查文件是否被修改 */
|
||||
if (node->mtime != mtime) {
|
||||
cache->misses++;
|
||||
lru_remove(cache, node);
|
||||
hash_remove(cache, key);
|
||||
cache->total_size -= (node->header_len + node->body_len);
|
||||
node_free(node);
|
||||
pthread_mutex_unlock(&cache->lock);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* 命中:移到 LRU 头部 */
|
||||
lru_move_to_head(cache, node);
|
||||
cache->hits++;
|
||||
|
||||
/* 构造返回结构(栈上临时,调用者需立即使用) */
|
||||
static __thread cocoon_cache_entry_t entry;
|
||||
entry.key = node->key;
|
||||
entry.header = node->header;
|
||||
entry.header_len = node->header_len;
|
||||
entry.body = node->body;
|
||||
entry.body_len = node->body_len;
|
||||
entry.mtime = node->mtime;
|
||||
entry.expires_at = node->expires_at;
|
||||
|
||||
pthread_mutex_unlock(&cache->lock);
|
||||
return &entry;
|
||||
}
|
||||
|
||||
void cache_put(cocoon_cache_t *cache, const char *key,
|
||||
const char *header, size_t header_len,
|
||||
const char *body, size_t body_len,
|
||||
time_t mtime) {
|
||||
if (!cache || !key || !header || !body) return;
|
||||
|
||||
size_t entry_size = header_len + body_len;
|
||||
|
||||
/* 超过单条上限,跳过 */
|
||||
if (entry_size > cache->max_entry_size) {
|
||||
log_debug("[Cache] 条目 %s 大小 %zu 超过上限 %zu,不缓存", key, entry_size, cache->max_entry_size);
|
||||
return;
|
||||
}
|
||||
|
||||
pthread_mutex_lock(&cache->lock);
|
||||
|
||||
/* 如果已存在,先删除旧条目 */
|
||||
cache_node_t *existing = hash_find(cache, key);
|
||||
if (existing) {
|
||||
lru_remove(cache, existing);
|
||||
hash_remove(cache, key);
|
||||
cache->total_size -= (existing->header_len + existing->body_len);
|
||||
node_free(existing);
|
||||
}
|
||||
|
||||
/* 腾出空间 */
|
||||
make_room(cache, entry_size);
|
||||
|
||||
/* 创建新节点 */
|
||||
cache_node_t *node = (cache_node_t *)calloc(1, sizeof(cache_node_t));
|
||||
if (!node) {
|
||||
pthread_mutex_unlock(&cache->lock);
|
||||
log_error("[Cache] 分配节点失败");
|
||||
return;
|
||||
}
|
||||
|
||||
node->key = strdup(key);
|
||||
node->header = (char *)malloc(header_len);
|
||||
node->body = (char *)malloc(body_len);
|
||||
if (!node->key || !node->header || !node->body) {
|
||||
node_free(node);
|
||||
pthread_mutex_unlock(&cache->lock);
|
||||
log_error("[Cache] 拷贝数据失败");
|
||||
return;
|
||||
}
|
||||
|
||||
memcpy(node->header, header, header_len);
|
||||
node->header_len = header_len;
|
||||
memcpy(node->body, body, body_len);
|
||||
node->body_len = body_len;
|
||||
node->mtime = mtime;
|
||||
node->expires_at = time(NULL) + cache->ttl_seconds;
|
||||
|
||||
/* 插入 LRU 头部和哈希表 */
|
||||
node->next = cache->lru_head;
|
||||
if (cache->lru_head) cache->lru_head->prev = node;
|
||||
cache->lru_head = node;
|
||||
if (!cache->lru_tail) cache->lru_tail = node;
|
||||
|
||||
hash_insert(cache, node);
|
||||
cache->total_size += entry_size;
|
||||
|
||||
pthread_mutex_unlock(&cache->lock);
|
||||
|
||||
log_debug("[Cache] 已缓存: %s (头 %zu + 体 %zu = %zu 字节, 总计 %zu 字节)",
|
||||
key, header_len, body_len, entry_size, cache->total_size);
|
||||
}
|
||||
|
||||
void cache_stats(cocoon_cache_t *cache, cocoon_cache_stats_t *stats) {
|
||||
if (!cache || !stats) return;
|
||||
|
||||
pthread_mutex_lock(&cache->lock);
|
||||
stats->entries = 0;
|
||||
cache_node_t *node = cache->lru_head;
|
||||
while (node) {
|
||||
stats->entries++;
|
||||
node = node->next;
|
||||
}
|
||||
stats->total_size = cache->total_size;
|
||||
stats->hits = cache->hits;
|
||||
stats->misses = cache->misses;
|
||||
stats->evictions = cache->evictions;
|
||||
stats->expirations = cache->expirations;
|
||||
pthread_mutex_unlock(&cache->lock);
|
||||
}
|
||||
|
||||
void cache_clear(cocoon_cache_t *cache) {
|
||||
if (!cache) return;
|
||||
|
||||
pthread_mutex_lock(&cache->lock);
|
||||
|
||||
cache_node_t *node = cache->lru_head;
|
||||
while (node) {
|
||||
cache_node_t *next = node->next;
|
||||
node_free(node);
|
||||
node = next;
|
||||
}
|
||||
cache->lru_head = NULL;
|
||||
cache->lru_tail = NULL;
|
||||
memset(cache->buckets, 0, cache->bucket_count * sizeof(cache_node_t *));
|
||||
cache->total_size = 0;
|
||||
cache->hits = 0;
|
||||
cache->misses = 0;
|
||||
cache->evictions = 0;
|
||||
cache->expirations = 0;
|
||||
|
||||
pthread_mutex_unlock(&cache->lock);
|
||||
|
||||
log_info("[Cache] 已清空");
|
||||
}
|
||||
|
||||
/* 缓存辅助函数:判断文件是否适合缓存 */
|
||||
bool cache_is_eligible(const cocoon_cache_t *cache, int64_t file_size) {
|
||||
if (!cache || file_size <= 0) return false;
|
||||
return cache->max_entry_size > 0 && (size_t)file_size <= cache->max_entry_size;
|
||||
}
|
||||
133
cache.h
Normal file
133
cache.h
Normal file
@ -0,0 +1,133 @@
|
||||
/**
|
||||
* cache.h - 内存响应缓存模块
|
||||
*
|
||||
* 基于 LRU + TTL 策略的轻量级内存缓存,用于缓存小文件响应。
|
||||
* 避免重复磁盘 I/O,提升静态文件服务性能。
|
||||
*
|
||||
* 设计要点:
|
||||
* - 哈希表 + 双向链表实现 O(1) 的 get/put
|
||||
* - 单条缓存上限:避免大文件撑爆内存
|
||||
* - 总容量上限:LRU 淘汰最久未使用的条目
|
||||
* - TTL 过期:基于 expires_at 时间戳
|
||||
* - 文件 mtime 校验:文件修改后缓存自动失效
|
||||
* - 线程安全:pthread_mutex 保护
|
||||
*
|
||||
* @author xfy
|
||||
*/
|
||||
|
||||
#ifndef COCOON_CACHE_H
|
||||
#define COCOON_CACHE_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <time.h>
|
||||
|
||||
/* 前向声明,避免暴露内部结构 */
|
||||
typedef struct cocoon_cache cocoon_cache_t;
|
||||
|
||||
/**
|
||||
* cocoon_cache_entry_t - 缓存条目
|
||||
*
|
||||
* 存储完整的 HTTP 响应(头 + 体),用于直接发送给客户端。
|
||||
*/
|
||||
typedef struct {
|
||||
char *key; /**< 缓存键(文件绝对路径) */
|
||||
char *header; /**< 格式化后的 HTTP 响应头 */
|
||||
size_t header_len; /**< 响应头长度 */
|
||||
char *body; /**< 响应体(文件内容) */
|
||||
size_t body_len; /**< 响应体长度 */
|
||||
time_t mtime; /**< 文件修改时间(用于失效校验) */
|
||||
time_t expires_at; /**< TTL 过期时间 */
|
||||
} cocoon_cache_entry_t;
|
||||
|
||||
/**
|
||||
* cocoon_cache_stats_t - 缓存统计
|
||||
*/
|
||||
typedef struct {
|
||||
size_t entries; /**< 当前条目数 */
|
||||
size_t total_size; /**< 当前总字节数 */
|
||||
size_t hits; /**< 命中次数 */
|
||||
size_t misses; /**< 未命中次数 */
|
||||
size_t evictions; /**< 淘汰次数 */
|
||||
size_t expirations; /**< TTL 过期次数 */
|
||||
} cocoon_cache_stats_t;
|
||||
|
||||
/**
|
||||
* cache_create - 创建缓存实例
|
||||
*
|
||||
* @param max_size 最大总容量(字节),0 表示默认 64MB
|
||||
* @param ttl_seconds TTL 秒数,0 表示默认 60
|
||||
* @param max_entry_size 单条最大大小(字节),0 表示默认 1MB
|
||||
* @return 缓存实例,失败返回 NULL
|
||||
*/
|
||||
cocoon_cache_t *cache_create(size_t max_size, uint32_t ttl_seconds, size_t max_entry_size);
|
||||
|
||||
/**
|
||||
* cache_destroy - 销毁缓存实例
|
||||
*
|
||||
* 释放所有缓存条目和内部结构。
|
||||
*
|
||||
* @param cache 缓存实例
|
||||
*/
|
||||
void cache_destroy(cocoon_cache_t *cache);
|
||||
|
||||
/**
|
||||
* cache_get - 获取缓存条目
|
||||
*
|
||||
* 查找指定 key 的缓存。如果条目存在但 mtime 不匹配或已过期,
|
||||
* 则视为未命中并删除该条目。
|
||||
*
|
||||
* @param cache 缓存实例
|
||||
* @param key 缓存键(文件绝对路径)
|
||||
* @param mtime 当前文件修改时间(用于失效校验)
|
||||
* @return 缓存条目指针(引用,不拷贝),未命中返回 NULL
|
||||
*/
|
||||
const cocoon_cache_entry_t *cache_get(cocoon_cache_t *cache, const char *key, time_t mtime);
|
||||
|
||||
/**
|
||||
* cache_put - 存入缓存
|
||||
*
|
||||
* 将响应存入缓存。如果缓存已满,按 LRU 淘汰旧条目。
|
||||
* 如果条目大小超过 max_entry_size,则忽略(不存入)。
|
||||
*
|
||||
* @param cache 缓存实例
|
||||
* @param key 缓存键
|
||||
* @param header 响应头(会被拷贝)
|
||||
* @param header_len 响应头长度
|
||||
* @param body 响应体(会被拷贝)
|
||||
* @param body_len 响应体长度
|
||||
* @param mtime 文件修改时间
|
||||
*/
|
||||
void cache_put(cocoon_cache_t *cache, const char *key,
|
||||
const char *header, size_t header_len,
|
||||
const char *body, size_t body_len,
|
||||
time_t mtime);
|
||||
|
||||
/**
|
||||
* cache_stats - 获取缓存统计
|
||||
*
|
||||
* @param cache 缓存实例
|
||||
* @param stats 输出统计结构
|
||||
*/
|
||||
void cache_stats(cocoon_cache_t *cache, cocoon_cache_stats_t *stats);
|
||||
|
||||
/**
|
||||
* cache_clear - 清空缓存
|
||||
*
|
||||
* 删除所有缓存条目,重置统计。
|
||||
*
|
||||
* @param cache 缓存实例
|
||||
*/
|
||||
void cache_clear(cocoon_cache_t *cache);
|
||||
|
||||
/**
|
||||
* cache_is_eligible - 判断文件是否适合缓存
|
||||
*
|
||||
* @param cache 缓存实例
|
||||
* @param file_size 文件大小(字节)
|
||||
* @return true 适合缓存
|
||||
*/
|
||||
bool cache_is_eligible(const cocoon_cache_t *cache, int64_t file_size);
|
||||
|
||||
#endif /* COCOON_CACHE_H */
|
||||
5
cocoon.h
5
cocoon.h
@ -97,6 +97,11 @@ typedef struct cocoon_config {
|
||||
int timeout_ms; /**< 请求超时 */
|
||||
} fastcgi[COCOON_MAX_FASTCGI_RULES];
|
||||
size_t num_fastcgi;
|
||||
/* 内存缓存配置 */
|
||||
bool cache_enabled; /**< 是否启用内存缓存(默认 false) */
|
||||
size_t cache_max_size; /**< 最大缓存总容量(字节,默认 64MB) */
|
||||
uint32_t cache_ttl_seconds; /**< 缓存 TTL 秒数(默认 60) */
|
||||
size_t cache_max_entry_size; /**< 单条缓存最大大小(字节,默认 1MB) */
|
||||
} cocoon_config_t;
|
||||
|
||||
/* === 服务器生命周期 API === */
|
||||
|
||||
4
config.h
4
config.h
@ -61,6 +61,8 @@ void config_merge(cocoon_config_t *base, const cocoon_config_t *cmdline,
|
||||
bool has_access_log,
|
||||
bool has_cors_enabled, bool has_auth_user, bool has_auth_pass,
|
||||
bool has_rate_limit,
|
||||
bool has_plugins);
|
||||
bool has_plugins,
|
||||
bool has_cache_enabled, bool has_cache_max_size,
|
||||
bool has_cache_ttl_seconds, bool has_cache_max_entry_size);
|
||||
|
||||
#endif /* COCOON_CONFIG_H */
|
||||
|
||||
24
server.c
24
server.c
@ -33,6 +33,7 @@
|
||||
#include "sse.h"
|
||||
#include "config.h"
|
||||
#include "fcgi_handler.h"
|
||||
#include "cache.h"
|
||||
#include <time.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
@ -81,6 +82,8 @@ struct server_context {
|
||||
/* FastCGI 配置 */
|
||||
cocoon_fcgi_config_t fcgi_config;
|
||||
volatile int reload_requested; /**< 配置热重载请求标志 */
|
||||
/* 内存缓存 */
|
||||
cocoon_cache_t *cache; /**< 内存响应缓存(NULL 表示未启用) */
|
||||
};
|
||||
static atomic_int g_active_connections = 0;
|
||||
/* 服务器启动时间 */
|
||||
@ -770,7 +773,7 @@ static bool handle_request(connection_t *conn, const char *root_dir) {
|
||||
return req.keep_alive;
|
||||
}
|
||||
conn->response_status = 200;
|
||||
static_serve_file(conn->fd, &index_req, effective_root_dir, conn->gzip_enabled, conn->brotli_enabled);
|
||||
static_serve_file(conn->fd, &index_req, effective_root_dir, conn->gzip_enabled, conn->brotli_enabled, conn->ctx->cache);
|
||||
} else {
|
||||
/* 无 index.html,生成目录列表 */
|
||||
conn->response_status = 200;
|
||||
@ -779,7 +782,7 @@ static bool handle_request(connection_t *conn, const char *root_dir) {
|
||||
} else if (cocoon_stat_isreg(&st)) {
|
||||
/* 普通文件 */
|
||||
conn->response_status = 200;
|
||||
static_serve_file(conn->fd, &req, effective_root_dir, conn->gzip_enabled, conn->brotli_enabled);
|
||||
static_serve_file(conn->fd, &req, effective_root_dir, conn->gzip_enabled, conn->brotli_enabled, conn->ctx->cache);
|
||||
} else {
|
||||
conn->response_status = 403;
|
||||
static_send_error(conn->fd, 403, req.keep_alive);
|
||||
@ -1393,6 +1396,17 @@ server_context_t *server_create(const cocoon_config_t *config, const char *confi
|
||||
log_warn("FastCGI 初始化失败,FastCGI 路由不可用");
|
||||
}
|
||||
|
||||
/* 初始化内存缓存 */
|
||||
if (ctx->config.cache_enabled) {
|
||||
ctx->cache = cache_create(ctx->config.cache_max_size, ctx->config.cache_ttl_seconds, ctx->config.cache_max_entry_size);
|
||||
if (ctx->cache) {
|
||||
log_info("内存缓存已启用: 最大 %zu 字节, TTL %u 秒, 单条上限 %zu 字节",
|
||||
ctx->config.cache_max_size, ctx->config.cache_ttl_seconds, ctx->config.cache_max_entry_size);
|
||||
} else {
|
||||
log_warn("内存缓存初始化失败,已禁用");
|
||||
}
|
||||
}
|
||||
|
||||
/* 启动主动健康检查 */
|
||||
healthcheck_manager_init(&ctx->hc_manager);
|
||||
healthcheck_start(&ctx->hc_manager, &ctx->proxy_config, ctx->config.timeout_ms);
|
||||
@ -1631,6 +1645,12 @@ void server_destroy(server_context_t *ctx) {
|
||||
/* 关闭 FastCGI 连接池 */
|
||||
fcgi_handler_destroy(&ctx->fcgi_config);
|
||||
|
||||
/* 销毁内存缓存 */
|
||||
if (ctx->cache) {
|
||||
cache_destroy(ctx->cache);
|
||||
ctx->cache = NULL;
|
||||
}
|
||||
|
||||
/* 卸载插件 */
|
||||
cocoon_plugin_unload_all();
|
||||
|
||||
|
||||
49
static.c
49
static.c
@ -11,6 +11,7 @@
|
||||
#include "static.h"
|
||||
#include "cocoon.h"
|
||||
#include "platform.h"
|
||||
#include "cache.h"
|
||||
#include "../coco/include/coco.h"
|
||||
#include "tls.h"
|
||||
#include <stdio.h>
|
||||
@ -332,7 +333,7 @@ int static_send_error(int fd, int status_code, bool keep_alive) {
|
||||
* @param root_dir 闈欐€佽祫婧愭牴鐩綍
|
||||
* @return COCOON_OK 鎴愬姛锛岃礋鍊奸敊璇爜
|
||||
*/
|
||||
int static_serve_file(int fd, const http_request_t *req, const char *root_dir, bool gzip_enabled, bool brotli_enabled) {
|
||||
int static_serve_file(int fd, const http_request_t *req, const char *root_dir, bool gzip_enabled, bool brotli_enabled, cocoon_cache_t *cache) {
|
||||
char real_path[4096];
|
||||
if (!safe_path_join(real_path, sizeof(real_path), root_dir, req->path)) {
|
||||
return static_send_error(fd, 403, req->keep_alive);
|
||||
@ -398,6 +399,19 @@ int static_serve_file(int fd, const http_request_t *req, const char *root_dir, b
|
||||
|
||||
/* 鍒ゆ柇鍘嬬缉鏂瑰紡锛氫紭鍏?brotli锛屽洖閫€ gzip */
|
||||
int64_t file_size = cocoon_stat_size(&st);
|
||||
|
||||
/* 鍐呭瓨缂撳瓨妫€鏌?*/
|
||||
bool cache_eligible = cache && !req->has_range && req->method != HTTP_HEAD && cache_is_eligible(cache, file_size);
|
||||
if (cache_eligible) {
|
||||
const cocoon_cache_entry_t *cached = cache_get(cache, real_path, cocoon_stat_mtime(&st));
|
||||
if (cached) {
|
||||
send_all(fd, cached->header, cached->header_len);
|
||||
send_all(fd, cached->body, cached->body_len);
|
||||
cocoon_file_close(file_fd);
|
||||
return COCOON_OK;
|
||||
}
|
||||
}
|
||||
|
||||
bool use_gzip = false;
|
||||
bool use_brotli = false;
|
||||
char *compress_buf = NULL;
|
||||
@ -494,23 +508,34 @@ int static_serve_file(int fd, const http_request_t *req, const char *root_dir, b
|
||||
return COCOON_OK;
|
||||
}
|
||||
|
||||
if (use_gzip || use_brotli || tls_has_connection(fd)) {
|
||||
if (use_gzip || use_brotli || tls_has_connection(fd) || cache_eligible) {
|
||||
/* 发送压缩后的数据,或 TLS 模式下的文件内容 */
|
||||
if (use_gzip || use_brotli) {
|
||||
send_all(fd, compress_buf, (size_t)compress_len);
|
||||
/* 缓存压缩后的内容 */
|
||||
if (cache_eligible) {
|
||||
cache_put(cache, real_path, header_buf, (size_t)header_len, compress_buf, (size_t)compress_len, cocoon_stat_mtime(&st));
|
||||
}
|
||||
free(compress_buf);
|
||||
} else {
|
||||
/* 定位到起始位置(文件可能已被压缩读取提前读至末尾) */
|
||||
/* 读取文件内容到内存后发送(TLS 或缓存路径) */
|
||||
cocoon_file_seek(file_fd, send_start, SEEK_SET);
|
||||
/* TLS 模式:不能使用 sendfile,需读取文件后发送 */
|
||||
char file_buf[65536];
|
||||
ssize_t remaining = send_length;
|
||||
while (remaining > 0) {
|
||||
size_t to_read = (size_t)remaining < sizeof(file_buf) ? (size_t)remaining : sizeof(file_buf);
|
||||
ssize_t n = cocoon_file_read(file_fd, file_buf, to_read);
|
||||
if (n <= 0) break;
|
||||
if (send_all(fd, file_buf, (size_t)n) != 0) break;
|
||||
remaining -= n;
|
||||
char *file_buf = (char *)malloc((size_t)file_size);
|
||||
if (file_buf) {
|
||||
ssize_t read_total = 0;
|
||||
while (read_total < file_size) {
|
||||
ssize_t n = cocoon_file_read(file_fd, file_buf + read_total, (size_t)(file_size - read_total));
|
||||
if (n <= 0) break;
|
||||
read_total += n;
|
||||
}
|
||||
if (read_total == file_size) {
|
||||
send_all(fd, file_buf, (size_t)file_size);
|
||||
/* 缓存存储 */
|
||||
if (cache_eligible) {
|
||||
cache_put(cache, real_path, header_buf, (size_t)header_len, file_buf, (size_t)file_size, cocoon_stat_mtime(&st));
|
||||
}
|
||||
}
|
||||
free(file_buf);
|
||||
}
|
||||
}
|
||||
cocoon_file_close(file_fd);
|
||||
|
||||
7
static.h
7
static.h
@ -9,6 +9,7 @@
|
||||
|
||||
#include "http.h"
|
||||
#include <stdbool.h>
|
||||
#include "cache.h"
|
||||
|
||||
/**
|
||||
* send_all - 确保缓冲区全部发送
|
||||
@ -29,14 +30,18 @@ int send_all(int fd, const char *buf, size_t len);
|
||||
* 打开文件,根据请求判断是否需要发送部分内容(Range),
|
||||
* 然后通过 sendfile 或循环读取发送文件内容。
|
||||
*
|
||||
* 如果 cache 不为 NULL,且文件大小不超过缓存上限,
|
||||
* 则尝试读取完整文件内容到内存缓存,避免重复磁盘 I/O。
|
||||
*
|
||||
* @param fd 客户端 socket 文件描述符
|
||||
* @param req HTTP 请求
|
||||
* @param root_dir 静态资源根目录
|
||||
* @param gzip_enabled 是否启用 gzip 压缩
|
||||
* @param brotli_enabled 是否启用 brotli 压缩
|
||||
* @param cache 内存缓存实例(可为 NULL)
|
||||
* @return COCOON_OK 成功,负值错误码
|
||||
*/
|
||||
int static_serve_file(int fd, const http_request_t *req, const char *root_dir, bool gzip_enabled, bool brotli_enabled);
|
||||
int static_serve_file(int fd, const http_request_t *req, const char *root_dir, bool gzip_enabled, bool brotli_enabled, cocoon_cache_t *cache);
|
||||
|
||||
/**
|
||||
* static_serve_directory - 生成目录列表 HTML
|
||||
|
||||
181
tests/unit/test_cache.c
Normal file
181
tests/unit/test_cache.c
Normal file
@ -0,0 +1,181 @@
|
||||
/**
|
||||
* test_cache.c - 内存缓存单元测试
|
||||
*
|
||||
* 测试 LRU + TTL 缓存的 get/put、LRU 淘汰、TTL 失效、统计等功能。
|
||||
*/
|
||||
|
||||
#include "cache.h"
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <assert.h>
|
||||
|
||||
static int g_tests = 0;
|
||||
static int g_passed = 0;
|
||||
|
||||
#define TEST_ASSERT(cond) do { \
|
||||
g_tests++; \
|
||||
if (cond) { g_passed++; } else { \
|
||||
fprintf(stderr, "FAIL: %s:%d: %s\n", __FILE__, __LINE__, #cond); \
|
||||
} \
|
||||
} while(0)
|
||||
|
||||
#define TEST_ASSERT_EQUAL(expected, actual) do { \
|
||||
g_tests++; \
|
||||
if ((expected) == (actual)) { g_passed++; } else { \
|
||||
fprintf(stderr, "FAIL: %s:%d: expected %lld, got %lld\n", __FILE__, __LINE__, (long long)(expected), (long long)(actual)); \
|
||||
} \
|
||||
} while(0)
|
||||
|
||||
static void test_cache_create_destroy(void) {
|
||||
cocoon_cache_t *cache = cache_create(1024, 60, 512);
|
||||
TEST_ASSERT(cache != NULL);
|
||||
if (cache) {
|
||||
cache_destroy(cache);
|
||||
}
|
||||
}
|
||||
|
||||
static void test_cache_put_get(void) {
|
||||
cocoon_cache_t *cache = cache_create(1024, 60, 512);
|
||||
TEST_ASSERT(cache != NULL);
|
||||
if (!cache) return;
|
||||
|
||||
const char *key = "/test/file.txt";
|
||||
const char *header = "HTTP/1.1 200 OK\r\n";
|
||||
const char *body = "hello world";
|
||||
|
||||
cache_put(cache, key, header, strlen(header), body, strlen(body), 12345);
|
||||
|
||||
const cocoon_cache_entry_t *entry = cache_get(cache, key, 12345);
|
||||
TEST_ASSERT(entry != NULL);
|
||||
if (entry) {
|
||||
TEST_ASSERT_EQUAL(strlen(header), entry->header_len);
|
||||
TEST_ASSERT_EQUAL(strlen(body), entry->body_len);
|
||||
TEST_ASSERT(memcmp(entry->header, header, strlen(header)) == 0);
|
||||
TEST_ASSERT(memcmp(entry->body, body, strlen(body)) == 0);
|
||||
}
|
||||
|
||||
cache_destroy(cache);
|
||||
}
|
||||
|
||||
static void test_cache_mtime_invalidation(void) {
|
||||
cocoon_cache_t *cache = cache_create(1024, 60, 512);
|
||||
TEST_ASSERT(cache != NULL);
|
||||
if (!cache) return;
|
||||
|
||||
const char *key = "/test/file.txt";
|
||||
cache_put(cache, key, "H", 1, "B", 1, 100);
|
||||
|
||||
/* mtime 不匹配,缓存应失效 */
|
||||
const cocoon_cache_entry_t *entry = cache_get(cache, key, 200);
|
||||
TEST_ASSERT(entry == NULL);
|
||||
|
||||
cache_destroy(cache);
|
||||
}
|
||||
|
||||
static void test_cache_lru_eviction(void) {
|
||||
/* 最大 18 字节(3条刚好满),单条 32 字节,存 4 条应该淘汰最早的 */
|
||||
cocoon_cache_t *cache = cache_create(18, 60, 32);
|
||||
TEST_ASSERT(cache != NULL);
|
||||
if (!cache) return;
|
||||
|
||||
cache_put(cache, "/a", "H", 1, "bodyA", 5, 1);
|
||||
cache_put(cache, "/b", "H", 1, "bodyB", 5, 1);
|
||||
cache_put(cache, "/c", "H", 1, "bodyC", 5, 1);
|
||||
|
||||
/* 访问 /a,提升其优先级 */
|
||||
cache_get(cache, "/a", 1);
|
||||
|
||||
/* 再存 /d,应该淘汰 /b(最久未访问) */
|
||||
cache_put(cache, "/d", "H", 1, "bodyD", 5, 1);
|
||||
|
||||
TEST_ASSERT(cache_get(cache, "/a", 1) != NULL); /* /a 被访问过,保留 */
|
||||
TEST_ASSERT(cache_get(cache, "/b", 1) == NULL); /* /b 最久未访问,被淘汰 */
|
||||
TEST_ASSERT(cache_get(cache, "/c", 1) != NULL);
|
||||
TEST_ASSERT(cache_get(cache, "/d", 1) != NULL);
|
||||
|
||||
cache_destroy(cache);
|
||||
}
|
||||
|
||||
static void test_cache_ttl_expiration(void) {
|
||||
/* TTL 1 秒 */
|
||||
cocoon_cache_t *cache = cache_create(1024, 1, 512);
|
||||
TEST_ASSERT(cache != NULL);
|
||||
if (!cache) return;
|
||||
|
||||
cache_put(cache, "/ttl", "H", 1, "body", 4, 1);
|
||||
TEST_ASSERT(cache_get(cache, "/ttl", 1) != NULL);
|
||||
|
||||
/* 等待 2 秒 */
|
||||
sleep(2);
|
||||
|
||||
TEST_ASSERT(cache_get(cache, "/ttl", 1) == NULL);
|
||||
|
||||
cache_destroy(cache);
|
||||
}
|
||||
|
||||
static void test_cache_stats(void) {
|
||||
cocoon_cache_t *cache = cache_create(1024, 60, 512);
|
||||
TEST_ASSERT(cache != NULL);
|
||||
if (!cache) return;
|
||||
|
||||
/* 先清除统计(cache_create 后可能有其他操作) */
|
||||
cache_put(cache, "/s1", "H", 1, "B", 1, 1);
|
||||
cache_put(cache, "/s2", "H", 1, "B", 1, 1);
|
||||
|
||||
/* 两次命中 */
|
||||
cache_get(cache, "/s1", 1);
|
||||
cache_get(cache, "/s1", 1);
|
||||
|
||||
/* 一次 miss(mtime 不同) */
|
||||
cache_get(cache, "/s2", 2);
|
||||
|
||||
cocoon_cache_stats_t stats;
|
||||
cache_stats(cache, &stats);
|
||||
TEST_ASSERT_EQUAL(2, stats.hits);
|
||||
TEST_ASSERT_EQUAL(1, stats.misses);
|
||||
TEST_ASSERT_EQUAL(0, stats.evictions);
|
||||
|
||||
cache_destroy(cache);
|
||||
}
|
||||
|
||||
static void test_cache_clear(void) {
|
||||
cocoon_cache_t *cache = cache_create(1024, 60, 512);
|
||||
TEST_ASSERT(cache != NULL);
|
||||
if (!cache) return;
|
||||
|
||||
cache_put(cache, "/c1", "H", 1, "B", 1, 1);
|
||||
cache_put(cache, "/c2", "H", 1, "B", 1, 1);
|
||||
|
||||
TEST_ASSERT(cache_get(cache, "/c1", 1) != NULL);
|
||||
TEST_ASSERT(cache_get(cache, "/c2", 1) != NULL);
|
||||
|
||||
cache_clear(cache);
|
||||
|
||||
TEST_ASSERT(cache_get(cache, "/c1", 1) == NULL);
|
||||
TEST_ASSERT(cache_get(cache, "/c2", 1) == NULL);
|
||||
cache_clear(cache);
|
||||
cocoon_cache_stats_t stats;
|
||||
cache_stats(cache, &stats);
|
||||
TEST_ASSERT_EQUAL(0, stats.total_size);
|
||||
TEST_ASSERT_EQUAL(0, stats.entries);
|
||||
|
||||
cache_destroy(cache);
|
||||
}
|
||||
|
||||
/* Unity 框架要求的 setUp/tearDown 钩子 */
|
||||
void setUp(void) {}
|
||||
void tearDown(void) {}
|
||||
|
||||
int main(void) {
|
||||
test_cache_create_destroy();
|
||||
test_cache_put_get();
|
||||
test_cache_mtime_invalidation();
|
||||
test_cache_lru_eviction();
|
||||
test_cache_ttl_expiration();
|
||||
test_cache_stats();
|
||||
test_cache_clear();
|
||||
|
||||
printf("Cache tests: %d/%d passed\n", g_passed, g_tests);
|
||||
return (g_passed == g_tests) ? 0 : 1;
|
||||
}
|
||||
@ -306,7 +306,7 @@ void test_merge_override_all(void) {
|
||||
};
|
||||
config_merge(&base, &cmdline,
|
||||
true, true, true, true, true, true, true, true, true, true, true, true,
|
||||
false, false, false, false, false);
|
||||
false, false, false, false, false, false, false, false, false);
|
||||
TEST_ASSERT_EQUAL_STRING("/new", base.root_dir);
|
||||
TEST_ASSERT_EQUAL(9090, base.port);
|
||||
TEST_ASSERT_TRUE(base.threaded);
|
||||
@ -336,7 +336,7 @@ void test_merge_no_override(void) {
|
||||
};
|
||||
config_merge(&base, &cmdline,
|
||||
false, false, false, false, false, false, false, false, false, false, false, false,
|
||||
false, false, false, false, false);
|
||||
false, false, false, 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);
|
||||
@ -360,7 +360,7 @@ void test_merge_partial_override(void) {
|
||||
};
|
||||
config_merge(&base, &cmdline,
|
||||
true, false, false, true, false, false, false, false, false, false, false, false,
|
||||
false, false, false, false, false);
|
||||
false, false, false, false, false, false, false, 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 */
|
||||
@ -426,7 +426,7 @@ void test_merge_cmdline_null_root_dir(void) {
|
||||
cocoon_config_t base = {.root_dir = strdup("/old"), .port = 8080};
|
||||
cocoon_config_t cmdline = {.root_dir = NULL, .port = 9090};
|
||||
config_merge(&base, &cmdline, true, true, false, false, false, false, false, false, false, false, false, false,
|
||||
false, false, false, false, false);
|
||||
false, false, false, false, false, false, false, false, false);
|
||||
TEST_ASSERT_EQUAL_STRING("/old", base.root_dir); /* NULL 不覆盖 */
|
||||
TEST_ASSERT_EQUAL(9090, base.port); /* port 覆盖 */
|
||||
free((void *)base.root_dir);
|
||||
@ -435,7 +435,7 @@ void test_merge_cmdline_null_root_dir(void) {
|
||||
void test_merge_null_safety(void) {
|
||||
/* 不应 crash */
|
||||
config_merge(NULL, NULL, true, true, true, true, true, true, true, true, true, true, true, true,
|
||||
true, true, true, true, false);
|
||||
true, true, true, true, true, true, true, true, false);
|
||||
TEST_ASSERT_TRUE(1);
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user