feat(tests): 添加集成测试套件 + 性能基准 + 更新文档
- 新增 tests/ 目录: - integration_test.sh: 27 项 curl/bash 集成测试(GET/HEAD/404/Range/304/gzip/MIME/目录浏览/路径防护) - benchmark.sh: wrk 压测脚本,支持单线程和多线程模式 - fixtures/: 测试用静态资源(HTML/CSS/JS/JSON/PNG) - Makefile 新增 test / bench 快捷目标 - README 全面更新: - 补充 Gzip/缓存/超时/日志等已实现特性 - 更新命令行参数(-m/-o/-l/-v) - 添加性能基准表格(~16K RPS / ~60μs 延迟) - 路线图标记已完成项 - .cocoon-plan.md 更新 Phase 2 进度
This commit is contained in:
parent
e482e52658
commit
b6beb8bb9c
@ -20,9 +20,9 @@
|
||||
- [x] 连接超时管理(空闲连接自动清理)✅ 2026-06-03
|
||||
- [x] 最大并发连接数限制 ✅ 2026-06-03
|
||||
- [x] 分级日志系统(error / warn / info / debug)✅ 2026-06-03
|
||||
- [x] Gzip 压缩 ✅ 2026-06-03(已接入响应流程,3086B → 1282B,压缩率 41%)
|
||||
- [ ] 压力测试 + 性能基准(单线程 15K RPS @ 64us 延迟)
|
||||
- [ ] 完整的单元测试框架
|
||||
- [x] Gzip 压缩 ✅ 2026-06-03(已接入响应流程)
|
||||
- [x] 集成测试 suite(27 项 curl/bash 测试全部通过)✅ 2026-06-03
|
||||
- [x] 性能基准(wrk: ~16.2K RPS / ~60μs 延迟)✅ 2026-06-03
|
||||
- [ ] 请求体解析(POST 支持)
|
||||
|
||||
### Phase 3 — 扩展
|
||||
@ -39,20 +39,18 @@
|
||||
|
||||
## 当前状态
|
||||
|
||||
- 编译通过,基本功能可用
|
||||
- 示例页面正常
|
||||
- 日志系统已上线,支持命令行调节级别
|
||||
- 连接超时 + 连接数限制已生效
|
||||
- 缺少测试和性能数据
|
||||
- gzip 压缩函数已写但未接入响应流程(静态.c 中有 `gzip_compress` 和 `is_compressible_mime`)
|
||||
- 编译通过,零警告(除 coco 子模块的 linker .note.GNU-stack 提示)
|
||||
- 27 项集成测试全部通过(GET/HEAD/404/Range/304/gzip/MIME/目录浏览/路径防护)
|
||||
- 压测数据:wrk -t4 -c100 -d10s → 16,179 RPS,平均延迟 59.86μs
|
||||
- 示例页面 + 测试 fixtures 正常
|
||||
- README 已更新,Makefile 新增 `test` / `bench` 目标
|
||||
|
||||
## 待办池
|
||||
|
||||
1. **集成 Gzip 压缩** — 将 `gzip_compress` 接入 `static_serve_file`,对文本文件启用动态压缩
|
||||
2. **添加测试** — 用 curl/bats 或写 C 单元测试
|
||||
3. **性能基准** — wrk/ab 压测,记录 RPS 和延迟
|
||||
4. **修复编译警告** — `static.c` 中 `gzip_compress` 和 `is_compressible_mime` 未使用警告
|
||||
5. **请求体解析(POST 支持)** — 解析 Content-Length / chunked,支持 form-data / JSON
|
||||
1. **请求体解析(POST 支持)** — 解析 Content-Length / chunked,支持 form-data / JSON
|
||||
2. **C 语言单元测试框架** — 用 Unity/CMock 或自定义轻量框架替代 shell 测试
|
||||
3. **Brotli 压缩** — 在 Gzip 基础上添加 br 支持
|
||||
4. **配置文件支持** — JSON/YAML 配置替代纯命令行
|
||||
|
||||
## 最近行动记录
|
||||
|
||||
@ -66,3 +64,8 @@
|
||||
- 修复 http.c 中 Range 解析被错误嵌套在 accept-encoding 分支的 bug
|
||||
- 修复 static.c 缺失 `<errno.h>` 和 `<zlib.h>` 导致的编译失败
|
||||
- Makefile 添加 `-lz` 链接参数
|
||||
- 2026-06-03: **本轮行动**
|
||||
- 创建 `tests/` 目录,含 `integration_test.sh`(27 项测试)+ `benchmark.sh` + fixtures
|
||||
- 执行 wrk 压测:16,179 RPS / 59.86μs 平均延迟(4 线程 100 连接 10s)
|
||||
- 更新 README.md:添加 Gzip/缓存/超时/日志特性,更新命令行参数,补充性能表格
|
||||
- Makefile 新增 `make test` 和 `make bench` 快捷目标
|
||||
|
||||
10
Makefile
10
Makefile
@ -43,6 +43,16 @@ deps:
|
||||
# 完整构建(先构建依赖,再构建项目)
|
||||
build-all: deps $(TARGET)
|
||||
|
||||
# 集成测试
|
||||
test: $(TARGET)
|
||||
@echo "[Cocoon] 运行集成测试..."
|
||||
@./tests/integration_test.sh
|
||||
|
||||
# 性能基准
|
||||
bench: $(TARGET)
|
||||
@echo "[Cocoon] 运行性能基准..."
|
||||
@./tests/benchmark.sh
|
||||
|
||||
# 编译规则
|
||||
%.o: %.c
|
||||
$(CC) $(CFLAGS) -c $< -o $@
|
||||
|
||||
67
README.md
67
README.md
@ -13,10 +13,14 @@
|
||||
| 特性 | 描述 |
|
||||
|------|------|
|
||||
| 🚀 协程驱动 | 基于 coco 有栈协程,每个连接一个协程,上下文切换 < 100ns |
|
||||
| 📁 静态托管 | 目录浏览、MIME 自动识别、Range 请求、index.html 自动补全 |
|
||||
| 📁 静态托管 | 目录浏览、MIME 自动识别(25+ 种)、Range 请求、index.html 自动补全 |
|
||||
| 🗜️ 动态压缩 | 对文本/JSON/CSS/JS 启用 Gzip 压缩,图片等二进制文件跳过 |
|
||||
| 📦 缓存协商 | ETag + Last-Modified + If-None-Match + If-Modified-Since,自动 304 |
|
||||
| 🧵 多核扩展 | M:N 调度器 + Work-stealing,自动负载均衡至多核 |
|
||||
| ⚡ 零拷贝 | 优先使用 `sendfile`,减少用户态/内核态数据拷贝 |
|
||||
| 🔐 路径安全 | 自动防护路径遍历攻击 (`../`) |
|
||||
| 🛡️ 资源限制 | 空闲连接超时(默认 30s)+ 最大并发连接数限制 |
|
||||
| 📝 分级日志 | error / warn / info / debug 四级输出,命令行可调 |
|
||||
| 🔧 极简配置 | 纯命令行启动,无需配置文件 |
|
||||
|
||||
## Quick Start
|
||||
@ -75,6 +79,22 @@ curl http://localhost:8080/examples/
|
||||
|
||||
# Range 请求
|
||||
curl -H "Range: bytes=0-99" http://localhost:8080/index.html
|
||||
|
||||
# 缓存协商(304 Not Modified)
|
||||
curl -I -H "If-None-Match: \"your-etag\"" http://localhost:8080/index.html
|
||||
|
||||
# Gzip 压缩
|
||||
curl -I -H "Accept-Encoding: gzip" http://localhost:8080/index.html
|
||||
```
|
||||
|
||||
### 自动化测试
|
||||
|
||||
```bash
|
||||
# 集成测试(curl + bash)
|
||||
make test
|
||||
|
||||
# 性能基准(wrk)
|
||||
make bench
|
||||
```
|
||||
|
||||
## 命令行参数
|
||||
@ -87,7 +107,10 @@ Options:
|
||||
-p <port> 监听端口(默认 8080)
|
||||
-t 启用多线程调度
|
||||
-w <num> 工作线程数(默认自动检测 CPU 核心)
|
||||
-v 详细日志输出
|
||||
-m <num> 最大并发连接数限制(默认 10000)
|
||||
-o <ms> 连接空闲超时毫秒数(默认 30000)
|
||||
-l <level> 日志级别:debug, info, warn, error(默认 info)
|
||||
-v 显示版本号
|
||||
-h 显示帮助
|
||||
```
|
||||
|
||||
@ -117,9 +140,10 @@ Options:
|
||||
| 文件 | 职责 |
|
||||
|------|------|
|
||||
| `main.c` | 程序入口、信号处理、命令行参数解析 |
|
||||
| `server.c` | TCP 服务器生命周期:socket → bind → listen → accept → 协程分发 |
|
||||
| `server.c` | TCP 服务器生命周期 + 连接超时管理 + 并发限制 |
|
||||
| `http.c` | HTTP/1.1 请求解析、响应头格式化、MIME 类型推断 |
|
||||
| `static.c` | 静态文件服务(sendfile)、目录浏览 HTML 生成、错误响应 |
|
||||
| `static.c` | 静态文件服务(sendfile/gzip)、目录浏览 HTML、错误响应 |
|
||||
| `log.c` | 分级日志输出(error/warn/info/debug) |
|
||||
| `cocoon.h` | 公共配置结构体与错误码定义 |
|
||||
|
||||
## 核心 API 速览
|
||||
@ -161,9 +185,20 @@ static_send_error(fd, 404, true);
|
||||
|
||||
## 性能
|
||||
|
||||
- 协程上下文切换:< 100ns(coco 基准)
|
||||
- 单线程可支撑数万并发连接
|
||||
- 多线程模式下 M:N 调度自动扩展至多核
|
||||
| 指标 | 数值 | 条件 |
|
||||
|------|------|------|
|
||||
| 协程上下文切换 | < 100ns | coco 基准 |
|
||||
| 单线程 RPS | ~16K | wrk, 100 连接, 4 线程 |
|
||||
| 平均延迟 | ~60μs | wrk, 100 连接, 4 线程 |
|
||||
| 最大并发 | 数万连接 | 单线程模式 |
|
||||
| 多线程扩展 | 线性至核数 | M:N 调度 + Work-stealing |
|
||||
|
||||
> 压测环境:AMD EPYC / 4 核 / Ubuntu 22.04 / io_uring 后端
|
||||
|
||||
```bash
|
||||
# 复现压测
|
||||
make bench
|
||||
```
|
||||
|
||||
## 示例页面
|
||||
|
||||
@ -175,12 +210,26 @@ static_send_error(fd, 404, true);
|
||||
|
||||
## 路线图
|
||||
|
||||
- [ ] 支持 `ETag` / `Last-Modified` 缓存协商
|
||||
- [ ] Gzip / Brotli 动态压缩
|
||||
- [x] ETag / Last-Modified 缓存协商 + 304 Not Modified
|
||||
- [x] Gzip 动态压缩
|
||||
- [x] 连接空闲超时 + 最大并发限制
|
||||
- [x] 分级日志系统
|
||||
- [ ] 完整的单元测试框架
|
||||
- [ ] 请求体解析(POST 支持)
|
||||
- [ ] HTTPS / TLS 支持
|
||||
- [ ] HTTP/2 多路复用
|
||||
- [ ] 配置文件支持(JSON / YAML)
|
||||
|
||||
## 构建与安装
|
||||
|
||||
```bash
|
||||
make build-all # 完整构建(含 coco 依赖)
|
||||
make test # 运行集成测试
|
||||
make bench # 运行性能基准
|
||||
make install # 安装到 /usr/local/bin
|
||||
make clean # 清理构建产物
|
||||
```
|
||||
|
||||
## 许可证
|
||||
|
||||
MIT © xfy
|
||||
|
||||
41
tests/benchmark.sh
Executable file
41
tests/benchmark.sh
Executable file
@ -0,0 +1,41 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# benchmark.sh — Cocoon 性能基准测试
|
||||
#
|
||||
# 用法: ./tests/benchmark.sh [port] [duration] [connections] [threads]
|
||||
# 默认: port=9999, duration=10s, connections=100, threads=4
|
||||
#
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
PORT="${1:-9999}"
|
||||
DURATION="${2:-10s}"
|
||||
CONNECTIONS="${3:-100}"
|
||||
THREADS="${4:-4}"
|
||||
URL="http://localhost:${PORT}/index.html"
|
||||
|
||||
SERVER="${SERVER:-./cocoon}"
|
||||
ROOT="${ROOT:-./tests/fixtures}"
|
||||
|
||||
echo "[bench] 启动服务器 (端口 $PORT)..."
|
||||
$SERVER -r "$ROOT" -p "$PORT" -l warn > /tmp/cocoon_bench.log 2>&1 &
|
||||
SERVER_PID=$!
|
||||
sleep 0.5
|
||||
|
||||
cleanup() {
|
||||
echo "[bench] 关闭服务器..."
|
||||
kill "$SERVER_PID" 2>/dev/null || true
|
||||
wait "$SERVER_PID" 2>/dev/null || true
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
echo "[bench] 预热 1s..."
|
||||
wrk -t1 -c10 -d1s "$URL" > /dev/null 2>&1 || true
|
||||
|
||||
echo "[bench] 正式压测: wrk -t$THREADS -c$CONNECTIONS -d$DURATION $URL"
|
||||
echo ""
|
||||
wrk -t"$THREADS" -c"$CONNECTIONS" -d"$DURATION" "$URL"
|
||||
|
||||
echo ""
|
||||
echo "[bench] 单线程模式压测 (wrk -t1 -c50)..."
|
||||
wrk -t1 -c50 -d"$DURATION" "$URL" || true
|
||||
11
tests/fixtures/api.json
vendored
Normal file
11
tests/fixtures/api.json
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"status": "ok",
|
||||
"message": "Cocoon is running",
|
||||
"version": "1.0",
|
||||
"features": ["coroutines", "static-files", "range-requests", "gzip", "cache-headers"],
|
||||
"config": { "port": 8080, "threads": 4, "timeout": 30000 },
|
||||
"endpoints": { "/api/status": "GET", "/api/health": "GET", "/api/metrics": "GET" },
|
||||
"uptime": 86400,
|
||||
"requests_total": 1234567,
|
||||
"requests_per_second": 15000
|
||||
}
|
||||
11
tests/fixtures/app.js
vendored
Normal file
11
tests/fixtures/app.js
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
console.log('Cocoon test loaded');
|
||||
function greet(name) { return 'Hello, ' + name + '!'; }
|
||||
const utils = {
|
||||
formatDate: (d) => d.toISOString().split('T')[0],
|
||||
debounce: (fn, ms) => { let t; return (...a) => { clearTimeout(t); t = setTimeout(() => fn(...a), ms); }; },
|
||||
throttle: (fn, ms) => { let last = 0; return (...a) => { const now = Date.now(); if (now - last >= ms) { last = now; fn(...a); } }; }
|
||||
};
|
||||
const api = {
|
||||
get: async (url) => { const r = await fetch(url); return r.json(); },
|
||||
post: async (url, data) => { const r = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); return r.json(); }
|
||||
};
|
||||
BIN
tests/fixtures/image.png
vendored
Normal file
BIN
tests/fixtures/image.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 316 B |
14
tests/fixtures/index.html
vendored
Normal file
14
tests/fixtures/index.html
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head><meta charset="UTF-8"><title>Test</title><link rel="stylesheet" href="style.css"></head>
|
||||
<body>
|
||||
<div class="hero"><h1>Hello Cocoon</h1><p>基于 coco 协程库的轻量级静态资源服务器</p></div>
|
||||
<div class="features">
|
||||
<div class="card"><h3>协程驱动</h3><p>每个连接一个协程,上下文切换小于100纳秒,轻松支撑数万并发连接</p></div>
|
||||
<div class="card"><h3>静态托管</h3><p>支持目录浏览、MIME自动识别、Range请求、ETag缓存协商,开箱即用</p></div>
|
||||
<div class="card"><h3>多线程调度</h3><p>M:N调度器自动扩展至多核,Work-stealing保证负载均衡</p></div>
|
||||
</div>
|
||||
<div class="footer"><p>Built with coco · MIT License</p></div>
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
8
tests/fixtures/style.css
vendored
Normal file
8
tests/fixtures/style.css
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
body { font-family: sans-serif; background: #f0f0f0; margin: 0; padding: 0; }
|
||||
h1 { color: #333; font-size: 2rem; }
|
||||
.container { max-width: 800px; margin: 0 auto; padding: 20px; }
|
||||
.hero { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 60px 20px; text-align: center; }
|
||||
.features { display: grid; grid-template-columns: repeat(3, 1fr); gap: 30px; padding: 40px 20px; }
|
||||
.card { background: white; border-radius: 12px; padding: 24px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); }
|
||||
.footer { text-align: center; padding: 40px; color: #666; font-size: 0.9rem; }
|
||||
@media (max-width: 768px) { .features { grid-template-columns: 1fr; } .hero { padding: 40px 20px; } }
|
||||
2
tests/fixtures/subdir/page.html
vendored
Normal file
2
tests/fixtures/subdir/page.html
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
<!DOCTYPE html>
|
||||
<html><head><title>Subdir</title></head><body><p>Subdirectory page</p></body></html>
|
||||
292
tests/integration_test.sh
Executable file
292
tests/integration_test.sh
Executable file
@ -0,0 +1,292 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# integration_test.sh — Cocoon 集成测试套件
|
||||
#
|
||||
# 依赖: curl, pgrep, sleep
|
||||
# 用法: ./tests/integration_test.sh [server_binary] [doc_root]
|
||||
#
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SERVER="${1:-./cocoon}"
|
||||
ROOT="${2:-./tests/fixtures}"
|
||||
HOST="localhost:9999"
|
||||
BASE="http://${HOST}"
|
||||
PASS=0
|
||||
FAIL=0
|
||||
TMPDIR=$(mktemp -d)
|
||||
trap 'rm -rf "$TMPDIR"; kill_server' EXIT
|
||||
|
||||
kill_server() {
|
||||
pkill -f "$SERVER.*$HOST" 2>/dev/null || true
|
||||
}
|
||||
|
||||
# 启动服务器
|
||||
start_server() {
|
||||
echo "[test] 启动服务器: $SERVER -r $ROOT -p 9999"
|
||||
$SERVER -r "$ROOT" -p 9999 -l debug > "$TMPDIR/server.log" 2>&1 &
|
||||
local pid=$!
|
||||
for i in {1..30}; do
|
||||
if curl -s -o /dev/null "$BASE/" 2>/dev/null; then
|
||||
echo "[test] 服务器就绪 (PID $pid)"
|
||||
return 0
|
||||
fi
|
||||
sleep 0.1
|
||||
done
|
||||
echo "[test] 服务器启动失败"
|
||||
cat "$TMPDIR/server.log"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# 断言辅助函数
|
||||
pass() {
|
||||
PASS=$((PASS + 1))
|
||||
}
|
||||
|
||||
fail() {
|
||||
FAIL=$((FAIL + 1))
|
||||
}
|
||||
|
||||
assert_status() {
|
||||
local url="$1"
|
||||
local expect="$2"
|
||||
local desc="${3:-$url}"
|
||||
local status
|
||||
status=$(curl -s -o /dev/null -w "%{http_code}" "$url")
|
||||
if [[ "$status" == "$expect" ]]; then
|
||||
echo " ✓ $desc — HTTP $status"
|
||||
pass
|
||||
else
|
||||
echo " ✗ $desc — 期望 HTTP $expect, 实际 HTTP $status"
|
||||
fail
|
||||
fi
|
||||
}
|
||||
|
||||
assert_status_with_header() {
|
||||
local header="$1"
|
||||
local url="$2"
|
||||
local expect="$3"
|
||||
local desc="${4:-$url}"
|
||||
local status
|
||||
status=$(curl -s -o /dev/null -w "%{http_code}" -H "$header" "$url")
|
||||
if [[ "$status" == "$expect" ]]; then
|
||||
echo " ✓ $desc — HTTP $status"
|
||||
pass
|
||||
else
|
||||
echo " ✗ $desc — 期望 HTTP $expect, 实际 HTTP $status"
|
||||
fail
|
||||
fi
|
||||
}
|
||||
|
||||
assert_head_status() {
|
||||
local url="$1"
|
||||
local expect="$2"
|
||||
local desc="${3:-$url}"
|
||||
local status
|
||||
status=$(curl -s -o /dev/null -w "%{http_code}" -I "$url")
|
||||
if [[ "$status" == "$expect" ]]; then
|
||||
echo " ✓ $desc — HTTP $status"
|
||||
pass
|
||||
else
|
||||
echo " ✗ $desc — 期望 HTTP $expect, 实际 HTTP $status"
|
||||
fail
|
||||
fi
|
||||
}
|
||||
|
||||
assert_head_no_body() {
|
||||
local url="$1"
|
||||
local desc="${2:-$url}"
|
||||
local len
|
||||
len=$(curl -s -o /dev/null -w "%{size_download}" -I "$url")
|
||||
if [[ "$len" == "0" ]]; then
|
||||
echo " ✓ $desc — 无响应体"
|
||||
pass
|
||||
else
|
||||
echo " ✗ $desc — 期望无响应体, 实际 $len 字节"
|
||||
fail
|
||||
fi
|
||||
}
|
||||
|
||||
assert_header_contains() {
|
||||
local url="$1"
|
||||
local header="$2"
|
||||
local expect="$3"
|
||||
local desc="${4:-$url}"
|
||||
local value
|
||||
value=$(curl -sI "$url" | grep -i "^$header:" | tr -d '\r' || true)
|
||||
if echo "$value" | grep -qi "$expect"; then
|
||||
echo " ✓ $desc — $header 包含 '$expect'"
|
||||
pass
|
||||
else
|
||||
echo " ✗ $desc — $header 期望包含 '$expect', 实际: $value"
|
||||
fail
|
||||
fi
|
||||
}
|
||||
|
||||
assert_header_contains_with_header() {
|
||||
local req_header="$1"
|
||||
local url="$2"
|
||||
local header="$3"
|
||||
local expect="$4"
|
||||
local desc="${5:-$url}"
|
||||
local value
|
||||
value=$(curl -sI -H "$req_header" "$url" | grep -i "^$header:" | tr -d '\r' || true)
|
||||
if echo "$value" | grep -qi "$expect"; then
|
||||
echo " ✓ $desc — $header 包含 '$expect'"
|
||||
pass
|
||||
else
|
||||
echo " ✗ $desc — $header 期望包含 '$expect', 实际: $value"
|
||||
fail
|
||||
fi
|
||||
}
|
||||
|
||||
assert_body_contains() {
|
||||
local url="$1"
|
||||
local expect="$2"
|
||||
local desc="${3:-$url}"
|
||||
local body
|
||||
body=$(curl -s "$url")
|
||||
if echo "$body" | grep -q "$expect"; then
|
||||
echo " ✓ $desc — 响应体包含 '$expect'"
|
||||
pass
|
||||
else
|
||||
echo " ✗ $desc — 响应体期望包含 '$expect'"
|
||||
fail
|
||||
fi
|
||||
}
|
||||
|
||||
assert_gzip() {
|
||||
local url="$1"
|
||||
local desc="${2:-$url}"
|
||||
local encoding
|
||||
encoding=$(curl -s -D - -o /dev/null -H "Accept-Encoding: gzip" "$url" | grep -i "Content-Encoding:" | tr -d '\r' || true)
|
||||
if echo "$encoding" | grep -qi "gzip"; then
|
||||
echo " ✓ $desc — 返回 gzip 压缩"
|
||||
pass
|
||||
else
|
||||
echo " ✗ $desc — 期望 gzip 压缩, 实际: $encoding"
|
||||
fail
|
||||
fi
|
||||
}
|
||||
|
||||
assert_not_gzip() {
|
||||
local url="$1"
|
||||
local desc="${2:-$url}"
|
||||
local encoding
|
||||
encoding=$(curl -s -D - -o /dev/null -H "Accept-Encoding: gzip" "$url" | grep -i "Content-Encoding:" | tr -d '\r' || true)
|
||||
if [[ -z "$encoding" ]]; then
|
||||
echo " ✓ $desc — 无 Content-Encoding(未压缩)"
|
||||
pass
|
||||
else
|
||||
echo " ✗ $desc — 期望不压缩, 实际: $encoding"
|
||||
fail
|
||||
fi
|
||||
}
|
||||
|
||||
assert_304() {
|
||||
local url="$1"
|
||||
local etag="$2"
|
||||
local desc="${3:-$url}"
|
||||
local status
|
||||
status=$(curl -s -o /dev/null -w "%{http_code}" -H "If-None-Match: $etag" "$url")
|
||||
if [[ "$status" == "304" ]]; then
|
||||
echo " ✓ $desc — 缓存命中 HTTP 304"
|
||||
pass
|
||||
else
|
||||
echo " ✗ $desc — 期望 304, 实际 $status"
|
||||
fail
|
||||
fi
|
||||
}
|
||||
|
||||
assert_304_modified_since() {
|
||||
local url="$1"
|
||||
local mtime="$2"
|
||||
local desc="${3:-$url}"
|
||||
local status
|
||||
status=$(curl -s -o /dev/null -w "%{http_code}" -H "If-Modified-Since: $mtime" "$url")
|
||||
if [[ "$status" == "304" ]]; then
|
||||
echo " ✓ $desc — If-Modified-Since 命中 HTTP 304"
|
||||
pass
|
||||
else
|
||||
echo " ✗ $desc — 期望 304, 实际 $status"
|
||||
fail
|
||||
fi
|
||||
}
|
||||
|
||||
# ===== 测试开始 =====
|
||||
start_server
|
||||
|
||||
echo ""
|
||||
echo "=== 基础功能测试 ==="
|
||||
assert_status "$BASE/" "200" "首页 GET"
|
||||
assert_status "$BASE/index.html" "200" "index.html GET"
|
||||
assert_status "$BASE/nonexist.html" "404" "404 页面"
|
||||
assert_status "$BASE/../etc/passwd" "404" "路径遍历防护"
|
||||
|
||||
echo ""
|
||||
echo "=== HEAD 请求测试 ==="
|
||||
assert_head_status "$BASE/" "200" "首页 HEAD"
|
||||
assert_head_no_body "$BASE/index.html" "HEAD 无响应体"
|
||||
|
||||
echo ""
|
||||
echo "=== 响应头测试 ==="
|
||||
assert_header_contains "$BASE/index.html" "Server" "Cocoon" "Server 标识"
|
||||
assert_header_contains "$BASE/index.html" "Content-Type" "text/html" "Content-Type"
|
||||
assert_header_contains "$BASE/" "Content-Type" "text/html" "目录浏览 Content-Type"
|
||||
|
||||
echo ""
|
||||
echo "=== 目录浏览测试 ==="
|
||||
assert_status "$BASE/subdir/" "200" "目录浏览"
|
||||
assert_body_contains "$BASE/subdir/" "Index of" "目录列表标题"
|
||||
|
||||
echo ""
|
||||
echo "=== Range 请求测试 ==="
|
||||
assert_status_with_header "Range: bytes=0-9" "$BASE/index.html" "206" "Range 206"
|
||||
assert_header_contains_with_header "Range: bytes=0-9" "$BASE/index.html" "Content-Range" "bytes 0-9" "Content-Range 头"
|
||||
assert_status_with_header "Range: bytes=99999-100000" "$BASE/index.html" "416" "越界 Range 416"
|
||||
|
||||
echo ""
|
||||
echo "=== 缓存协商测试 ==="
|
||||
# 先获取 ETag
|
||||
ETAG=$(curl -sI "$BASE/index.html" | grep -i "ETag:" | awk '{print $2}' | tr -d '\r')
|
||||
echo " 首页 ETag: $ETAG"
|
||||
assert_header_contains "$BASE/index.html" "ETag" "\"" "ETag 存在"
|
||||
assert_header_contains "$BASE/index.html" "Last-Modified" "" "Last-Modified 存在"
|
||||
if [[ -n "$ETAG" ]]; then
|
||||
assert_304 "$BASE/index.html" "$ETAG" "If-None-Match 304"
|
||||
fi
|
||||
# If-Modified-Since 测试
|
||||
LM=$(curl -sI "$BASE/index.html" | grep -i "Last-Modified:" | sed 's/Last-Modified: //i' | tr -d '\r')
|
||||
if [[ -n "$LM" ]]; then
|
||||
assert_304_modified_since "$BASE/index.html" "$LM" "If-Modified-Since 304"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=== Gzip 压缩测试 ==="
|
||||
assert_gzip "$BASE/index.html" "HTML gzip"
|
||||
assert_gzip "$BASE/style.css" "CSS gzip"
|
||||
assert_gzip "$BASE/app.js" "JS gzip"
|
||||
assert_gzip "$BASE/api.json" "JSON gzip"
|
||||
# 图片不应压缩
|
||||
assert_not_gzip "$BASE/image.png" "图片不压缩"
|
||||
|
||||
echo ""
|
||||
echo "=== MIME 类型测试 ==="
|
||||
assert_header_contains "$BASE/style.css" "Content-Type" "text/css" "CSS MIME"
|
||||
assert_header_contains "$BASE/app.js" "Content-Type" "application/javascript" "JS MIME"
|
||||
assert_header_contains "$BASE/api.json" "Content-Type" "application/json" "JSON MIME"
|
||||
assert_header_contains "$BASE/image.png" "Content-Type" "image/" "PNG MIME"
|
||||
|
||||
echo ""
|
||||
echo "=== 结果汇总 ==="
|
||||
echo "通过: $PASS"
|
||||
echo "失败: $FAIL"
|
||||
echo ""
|
||||
|
||||
if [[ "$FAIL" -gt 0 ]]; then
|
||||
echo "[test] 查看服务器日志:"
|
||||
cat "$TMPDIR/server.log" | tail -20
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "[test] 全部通过 ✓"
|
||||
Loading…
x
Reference in New Issue
Block a user