C++ 在线判题平台(OJ)测试报告
项目:CPP Online Judge
测试类型:功能测试 + 接口测试 + UI 自动化测试 + 并发正确性测试 + 性能测试
测试环境:阿里云 ECS(2 核 / 1.8 GB,Alibaba Cloud Linux 3)
部署地址:
http://139.196.124.168:8088报告日期:2026-09-14
目录
- 一、项目简介
- 二、开发技术
- 三、测试环境与部署
- [四、测试用例设计(Xmind 思维导图)](#四、测试用例设计(Xmind 思维导图))
- 五、自动化测试代码
- 六、功能测试结果
- [七、Postman 接口测试](#七、Postman 接口测试)
- 八、性能测试报告(JMeter)
- 九、并发正确性测试(重大发现)
- 十、缺陷清单
- 十一、测试结论与改进建议
- 十二、测试产物清单
一、项目简介
本项目实现了一个完整 OJ 平台所应具备的核心功能,并按 B 端 / C 端特性做了功能切分:
- 普通用户(学生) :注册登录、浏览题目列表(分页 / 难度筛选 / 关键词搜索)、查看题目详情、
在 Monaco 在线编辑器中编写 C++ 代码、提交判题、查看个人提交记录与提交详情。 - 管理员(教师):题目的新增 / 编辑 / 删除、上传测试用例文件、查看全部提交记录。
- 游客:仅可浏览题目列表与题目详情,无法提交代码。
二、开发技术
| 层次 | 选型 |
|---|---|
| 后端 Web 框架 | cpp-httplib(header-only,C++17) |
| 前端 | 原生 HTML + CSS + JS + Monaco Editor + marked.js + KaTeX |
| 数据库 | MySQL 8.0(本项目实测部署使用 MySQL 5.7.40) |
| 消息队列 / 缓存 | Redis List(oj:judge:queue) |
| 判题沙箱 | Docker 容器(gcc:13 镜像) |
| 鉴权 | JWT(HS256,jwt-cpp) |
| 密码存储 | bcrypt(cost=12,通过 crypt() 实现) |
| 构建 | CMake 3.26 + GCC 10.2 |
三、测试环境与部署
3.1 环境信息
| 项目 | 配置 |
|---|---|
| 服务器 | 阿里云 ECS,2 核 CPU / 1869 MB 内存 / 40 GB 磁盘 |
| 操作系统 | Alibaba Cloud Linux 3(Anolis,内核对应 EL8) |
| 编译器 | g++ 10.2.1、CMake 3.26.5 |
| 数据库 | MySQL 5.7.40(为被测应用单独拉起 3307 实例,与服务器上原有的 3306 生产库隔离) |
| 缓存/队列 | Redis 6.2.22(6379) |
| 判题沙箱 | Docker 26.1.3 + gcc:13 镜像(1.39 GB) |
| 服务端口 | 后端 8088(与服务器上原有的 8080 服务互不影响) |
| 进程托管 | systemd(cpp-oj-backend / cpp-oj-judge / cpp-oj-mysql) |
3.2 测试数据准备
通过管理端 API 向数据库写入测试数据(脚本 data/seed_test_data.py,25 项校验全部通过):
| 类型 | 内容 |
|---|---|
| 内置题目 | A + B Problem、Hello World、斐波那契数列 |
| 新增测试题 | 数组求和、字符串反转、最大子段和、多行输出与末尾空行容差、大整数相加、判题-TLE 验证题、判题-MLE 验证题、无测试用例题目-SE 验证 |
| 测试账号 | 管理员 admin/admin123;普通用户 stu_test、stu_demo |
| 题库合计 | 12 道题,其中 11 道配好输入/输出测试用例,1 道故意不配用例用于验证 SE |
四、测试用例设计(Xmind 思维导图)
测试用例按 认证 / 题目 / 判题 / 提交记录 / 管理端 / 安全 六大模块 + 边界值 + 异常值设计,
共 119 条(接口 85 + UI 34)

测试执行流程如下:

4.1 用例设计要点
| 设计维度 | 具体做法 | 示例 |
|---|---|---|
| 正常路径 | 覆盖每个接口的成功分支 | 正确代码提交判 AC |
| 边界值 | 取上边界、下边界、边界±1 | 用户名 2/3/20/21 字节;密码 5/6 字节;代码 65536/65537 字节 |
| 异常输入 | 空值、非法格式、类型错误 | 空 JSON、非法难度、非数字 ID |
| 状态机 | 异步判题的全部 7 种状态 | AC / WA / TLE / MLE / RE / CE / SE |
| 输出比对规则 | 验证题面约定的容差 | 行尾空格、末尾空行、多行输出、大整数 |
| 权限矩阵 | 游客 / 普通用户 / 管理员三视角 | 同一管理接口分别验 401 / 403 / 200 |
| 越权访问 | 跨用户读取资源 | 普通用户读他人提交 → 403 |
| 安全 | 注入与鉴权绕过 | SQL 注入、注释符注入、路径注入、JWT 前缀 |
| 并发 | 顺序基线 vs 并发对照 | 见第九章(发现 P0 缺陷) |
五、自动化测试代码
5.1 技术栈
- 接口自动化 :Python 3.10 标准库(
urllib),无第三方依赖,便于在服务器直接运行 - UI 自动化:Python + Selenium 4.49 + Chrome headless
- 接口集合:Postman Collection v2.1(62 个请求),用 newman 执行
- 性能测试:Apache JMeter 5.6.3
- 测试数据:Python 脚本调用管理端 API 写入
5.2 接口自动化测试(核心代码片段)
判题是异步的,因此封装了「提交 → 轮询 → 断言终态」的通用方法:
python
def submit(sub_token, problem_id, code, language="cpp"):
return call("POST", "/api/submissions", token=sub_token,
body={"problem_id": problem_id, "code": code,
"language": language})
def wait_result(token, sid, timeout=120):
"""轮询直到状态不再是 pending,返回 (状态, 详情)"""
st, t0 = "", time.time()
while time.time() - t0 < timeout:
r = call("GET", "/api/submissions/%s" % sid, token=token)
d = data_of(r) or {}
st = d.get("status", "")
if st and st != "pending":
return st, d
time.sleep(1)
return st, (data_of(r) or {})
判题状态用例矩阵(每条都真实提交代码并等待结果):
python
judge_cases = [
("TC-SUB-001", "提交正确代码判 AC", AB_ID,
'#include <iostream>\nint main(){long long a,b;std::cin>>a>>b;'
'std::cout<<a+b<<std::endl;return 0;}', "ac"),
("TC-SUB-002", "提交错误代码判 WA", AB_ID, "... cout<<a-b; ...", "wa"),
("TC-SUB-003", "死循环代码判 TLE", TLE_ID, "int main(){while(1){}}", "tle"),
("TC-SUB-004", "空指针解引用判 RE", AB_ID, "int main(){int* p=nullptr;*p=42;}", "re"),
("TC-SUB-005", "内存超限代码判 MLE", MLE_ID, "std::vector<long long> v; while(1) v.push_back(1);", "mle"),
("TC-SUB-006", "编译错误代码判 CE", AB_ID, "int main(){ this is not valid c++; }", "ce"),
]
5.3 UI 自动化测试(关键踩坑)
该前端有一个很关键的行为 :只要 localStorage 里存在 oj_token,
访问 /pages/login.html 会被立即重定向到题目列表页。
因此自动化的登录动作必须先清空登录态,否则永远等不到登录表单:
python
def do_login(d, user, pwd):
"""注意:该前端在存在 oj_token 时会立刻把 login.html 重定向到 problems.html,
因此必须先清空登录态再进入登录页,否则永远等不到表单。"""
d.get(BASE + "/pages/login.html")
d.execute_script("localStorage.clear();")
d.get(BASE + "/pages/login.html")
WebDriverWait(d, 15).until(EC.presence_of_element_located((By.ID, "username")))
d.find_element(By.ID, "username").send_keys(user)
d.find_element(By.ID, "password").send_keys(pwd)
d.find_element(By.ID, "submitBtn").click()
5.4 并发正确性测试(对照法)
为了区分「性能不达标」与「并发下结果错误」,设计了顺序基线 vs 并发的对照实验:
python
def sequential_login(n, user, pwd): # 顺序基线
...按顺序发 n 次登录,统计成功/失败
def concurrent_login(threads, per_thread, user, pwd): # 并发对照
ts = [threading.Thread(target=worker) for _ in range(threads)]
...
六、功能测试结果
6.1 接口自动化测试
执行结果:85 条用例,通过 77 条,失败 8 条,通过率 90.6%
| 模块 | 通过 / 总数 | 通过率 |
|---|---|---|
| 认证(注册/登录/鉴权) | 19 / 20 | 95.0% |
| 题目(列表/详情/筛选/搜索) | 12 / 14 | 85.7% |
| 提交与判题(7 种状态 + 参数校验) | 18 / 20 | 90.0% |
| 提交记录与可见性 | 10 / 11 | 90.9% |
| 管理端与权限 | 11 / 12 | 91.7% |
| 权限控制 | 3 / 3 | 100% |
| 安全与边界 | 4 / 5 | 80.0% |
| 合计 | 77 / 85 | 90.6% |
6.2 判题核心链路验证(全部通过)
这是本项目最核心的功能,7 种判题状态全部真实验证通过:
| # | 用例 | 提交内容 | 期望 | 实际 | 耗时 | 内存 |
|---|---|---|---|---|---|---|
| 1 | AC 答案正确 | 正确的 a+b | ac | ac ✅ | 326 ms | 0 KB |
| 2 | WA 答案错误 | 输出 a-b | wa | wa ✅ | 295 ms | 0 KB |
| 3 | TLE 超时 | while(1){} |
tle | tle ✅ | 1000 ms | 0 KB |
| 4 | RE 运行错误 | 空指针解引用 | re | re ✅ | 0 ms | 0 KB |
| 5 | MLE 内存超限 | 无限 push_back | mle | mle ✅ | 0 ms | 65536 KB |
| 6 | CE 编译错误 | 非法 C++ 语法 | ce | ce ✅ | 0 ms | 0 KB |
| 7 | SE 系统错误 | 向无测试用例题目提交 | se | se ✅ | --- | --- |
6.3 UI 自动化测试
执行结果:34 条用例,通过 33 条,失败 1 条,通过率 97.1% ,同步产出 19 张页面截图。
| 用例编号 | 用例名称 | 结果 |
|---|---|---|
| TC-UI-001~004 | 首页渲染、游客导航、主视觉按钮、统计卡片 | ✅ |
| TC-UI-005~009 | 注册页加载、用户名/密码/一致性三项前端校验、注册成功 | ✅ |
| TC-UI-010~013 | 登录页加载、错误密码提示、普通用户登录、管理员登录跳转后台 | ✅ |
| TC-UI-014~016 | 管理员菜单含「管理后台」、普通用户菜单不含、普通用户直访后台被拦截 | ✅ |
| TC-UI-017 | 登录用户可访问提交记录页 | ✅ |
| TC-UI-018~020 | 题目列表渲染、关键词搜索、难度筛选 | ✅ |
| TC-UI-021~022 | 题目详情渲染、Monaco 编辑器加载成功 | ✅ |
| TC-UI-023~026 | 判题中中间态、AC 显示「通过」、WA 显示「答案错误」、CE 显示「编译错误」 | ✅ |
| TC-UI-027~028 | 提交记录列表、提交详情弹窗 | ✅ |
| TC-UI-029~031 | 管理后台列表、用例按钮状态标识、用例上传弹窗 | ✅ |
| TC-UI-032 | 点击「+ 新增题目」弹窗应出现 | ❌ 缺陷 D-07 |
| TC-UI-033~034 | 编辑题目弹窗回填、退出登录清除登录态 | ✅ |
前端权限差异测试的一个亮点:管理员下拉菜单里多出「管理后台」入口,普通用户没有 ,
且普通用户直接访问 /pages/admin.html 会被前端拦截跳回首页 ------ 界面层权限隔离生效。
6.4 测试截图
以下截图均由 Selenium 在 headless Chrome(1440×900)中真实操作页面后自动截取。
登录页与前端校验


题目列表(支持搜索 / 难度筛选 / 分页)

题目详情 ------ 左侧 Markdown + LaTeX 题面,右侧 Monaco 在线编辑器

提交代码后的判题结果(异步轮询 → 状态标签 + 耗时 + 内存)


编译错误(含 g++ 原始报错信息)

提交记录与提交详情弹窗


管理后台 ------ 题目列表与测试用例上传


缺陷复现 ------ 点击「新增题目」弹窗不出现(D-07)

七、Postman 接口测试
7.1 集合结构
交付一套可直接导入 Postman 9.x 的集合(Collection v2.1)+ 环境文件:
| 文件夹 | 请求数 | 覆盖内容 |
|---|---|---|
| 01 认证模块 | 11 | 注册正常/重复用户名/用户名过短/密码过短、登录成功/密码错误/字段缺失、获取当前用户、未带 token |
| 02 题目模块 | 10 | 列表分页、难度筛选、非法难度、关键词搜索、题目详情、不存在 ID、非数字 ID |
| 03 提交与判题 | 20 | AC/WA/TLE/RE/CE 各一对「提交 + 轮询」、空代码、超长代码、题目不存在、未登录 |
| 04 提交记录 | 10 | 本人列表、状态筛选、非法状态、本人详情、越权查看他人提交 |
| 05 管理端 | 11 | 管理员列表、普通用户越权、创建/更新/删除题目、上传测试用例、删除不存在题目 |
| 合计 | 62 |
集合中内建了 412 条断言,并对每个请求都校验响应时间小于 3000 ms。
八、性能测试报告(JMeter)
8.1 测试计划设计
用 Apache JMeter 5.6.3 编写测试计划(jmeter/在线OJ平台性能测试.jmx),共 5 个线程组 / 18 个采样器 / 113 条断言:
| 线程组 | 默认负载 | 说明 |
|---|---|---|
| setUp 准备组 | 1 线程 × 1 次 | 管理员登录取 token,供后续线程组使用 |
| 线程组 1:在线用户登录 | 20 线程 / ramp-up 10s / 循环 10 | POST /api/auth/login |
| 线程组 2:题目列表与详情浏览 | 20 线程 / ramp-up 10s / 循环 10 | 含 100~500ms 随机思考时间 |
| 线程组 3:提交代码并轮询判题 | 5 线程 / ramp-up 5s / 循环 3 | 提交后用 While Controller 每 1 秒轮询判题结果 |
所有并发参数均可通过 -J 覆盖(-Jthreads、-Jloops、-Jhost、-Jport 等)。
考虑到被测服务器只有 2 核 1.8 GB 且同时运行着另一个生产服务,计划中明确建议并发不超过 50。
8.2 执行结果
summary = 746 in 00:00:42 = 17.8/s Avg: 1043 Min: 63 Max: 7657 Err: 199 (26.68%)
746 个样本,总耗时 42 秒,平均响应 1043 ms,最大 7657 ms,错误率 26.68%。
按采样器细分后,问题的分布非常集中:
| 采样器 | 样本数 | 错误率 | 平均 | P90 | P95 | 最大 | 响应码 |
|---|---|---|---|---|---|---|---|
POST /api/auth/login(用户登录) |
200 | 99.5% | 3112 ms | 5361 | 5759 | 7657 | 200×1, 401×199 |
GET /api/problems(题目列表) |
200 | 0.0% | 331 ms | 462 | 553 | 2754 | 200×200 |
GET /api/problems/1(题目详情) |
200 | 0.0% | 250 ms | 373 | 419 | 604 | 200×200 |
POST /api/submissions(提交代码) |
15 | 0.0% | 451 ms | 674 | 756 | 756 | 200×15 |
GET /api/submissions/{id}(轮询判题) |
约 130 | 0.0% | 约 250 ms | --- | --- | 619 | 200×全部 |
全体请求分位:p50 = 305 ms、p90 = 3596 ms、p95 = 4254 ms、p99 = 6088 ms、max = 7657 ms。
8.3 关键结论
- 题目浏览类接口表现良好 :列表平均 331 ms / P95 553 ms,详情平均 250 ms / P95 419 ms,
20 并发下零错误。 - 提交与轮询链路零错误 :提交平均 451 ms,轮询平均约 250 ms,全部 200,
说明异步判题队列在并发提交下工作正常。 - 登录接口 99.5% 失败,且不是超时而是 401 :199 次登录返回 401 Unauthorized。
这不是性能问题,而是正确性问题 ------ 详见第九章。 - 除去登录接口,其余接口的总体错误率为 0% ;也就是说 26.68% 的整体错误率完全由登录接口贡献。
- 登录接口平均耗时 3112 ms 明显高于其它接口(300 ms 级别),是因为它是唯一走
bcrypt(cost=12) 密码校验 的接口,单次校验即消耗数百毫秒 CPU;
在 2 核机器上 20 并发时 CPU 被迅速打满,这也是它成为瓶颈的直接原因。
九、并发正确性测试(重大发现)
JMeter 报告里 198 次 401 是一个强烈信号。为了区分「性能不足」与「并发下结果错误」,
专门设计了顺序基线 vs 并发对照实验。
9.1 测试结果
| 场景 | 并发 | 成功 / 总数 | 失败率 | 响应码分布 |
|---|---|---|---|---|
| 顺序登录 30 次(admin) | 1 | 30 / 30 | 0% | 200×30 |
| 顺序登录 30 次(自建账号) | 1 | 30 / 30 | 0% | 200×30 |
| 并发登录(admin) | 5 线程 × 6 | 0 / 30 | 100% | 401×30 |
| 并发登录(admin) | 10 线程 × 5 | 0 / 50 | 100% | 401×50 |
| 并发登录(admin) | 20 线程 × 5 | 0 / 100 | 100% | 401×100 |
| 并发登录(自建账号) | 10 线程 × 5 | 0 / 50 | 100% | 401×50 |
| 并发注册 | 10 线程 × 4 | 8 / 40 | 80% | 200×8, 400×32 |
顺序执行 100% 成功,并发(≥5 线程)100% 失败。 这排除了「凭据错误」「账号问题」,
明确指向并发数据竞争。
9.2 根因定位
查看后端日志,捕获得到了直接证据:
[2026-09-14 21:01:42.615] [ERROR] [user_service.cpp:25 registerUser]
bcrypt hash failed: bcrypt hash failed: crypt() returned null
[2026-09-14 21:01:42.782] [ERROR] [user_service.cpp:25 registerUser]
bcrypt hash failed: bcrypt hash failed: crypt() returned null
... (共出现 32 次)
定位到问题代码:
cpp
// backend/src/utils/password.cpp
std::string bcryptHash(const std::string& password) {
std::string salt = generateBcryptSalt(12);
char* result = crypt(password.c_str(), salt.c_str()); // ← 非线程安全
if (!result) throw std::runtime_error("bcrypt hash failed: crypt() returned null");
return std::string(result);
}
bool bcryptVerify(const std::string& password, const std::string& hash) {
char* result = crypt(password.c_str(), hash.c_str()); // ← 非线程安全
if (!result) return false;
return std::strcmp(result, hash.c_str()) == 0;
}
crypt() 是线程不安全的。 服务器上 man 3 crypt 的原文(glibc 2.32 / libxcrypt 4.1.1):
Interface Attribute Value
────────────────────────────────────────────────────────
crypt Thread safety MT-Unsafe race:crypt
crypt_r, crypt_rn, Thread safety MT-Safe
crypt_ra
crypt() 内部使用静态缓冲区保存结果,并发调用时会互相踩踏,
表现为返回 NULL 或返回被覆盖的哈希串。
而 cpp-httplib 默认使用线程池并发处理请求,多个登录/注册请求会同时进入 crypt():
- 注册 :
crypt()返回 NULL → 抛异常 →user_service.cpp:25记错误日志 → 返回 400internal error - 登录 :
crypt()结果被污染 →strcmp(result, hash)不相等 → 判定密码错误 → 返回 401
9.3 影响评估
| 维度 | 评估 |
|---|---|
| 严重程度 | P0 / 严重 ------ 核心功能在并发下完全不可用 |
| 影响范围 | 所有涉及密码校验的接口:/api/auth/login、/api/auth/register |
| 触发条件 | 仅需 5 个并发请求(无需高负载),任何真实多人使用场景必然触发 |
| 业务影响 | 班级/教学场景下多个学生同时登录时几乎全部失败;且有概率把错误哈希写入数据库,导致账号永久无法登录 |
| 隐蔽性 | 单用户手工测试完全无法发现(顺序 100% 成功),必须并发才暴露 |
9.4 修复建议
cpp
// 方案一(推荐):改用可重入版本 crypt_r
#include <crypt.h>
std::string bcryptHash(const std::string& password) {
std::string salt = generateBcryptSalt(12);
struct crypt_data data;
std::memset(&data, 0, sizeof(data));
char* result = crypt_r(password.c_str(), salt.c_str(), &data); // MT-Safe
if (!result) throw std::runtime_error("bcrypt hash failed");
return std::string(result);
}
bool bcryptVerify(const std::string& password, const std::string& hash) {
struct crypt_data data;
std::memset(&data, 0, sizeof(data));
char* result = crypt_r(password.c_str(), hash.c_str(), &data); // MT-Safe,且线程栈上独立
if (!result) return false;
return std::string(result) == hash;
}
注意:
crypt_r的crypt_data必须每个线程/每次调用独立 (用栈变量即可),不能共享同一个
crypt_data。
方案二:用互斥锁把 crypt() 调用串行化(实现简单,但会限制登录并发度)。
方案三:替换为原生 bcrypt 实现(如 libbcrypt),彻底规避 crypt() 的线程安全问题。
十、缺陷清单
本次测试共发现 12 个已确认缺陷 ,其中 P0 严重 1 个、P1 高 3 个、P2 中 3 个、P3 轻微 5 个。
(另有 1 条曾疑似缺陷的观测项 D-06,经公网复测确认非缺陷,已在清单中标注排除。)
P0 ------ 严重
D-01 并发下 crypt() 非线程安全,导致并发登录/注册全部失败
| 项 | 内容 |
|---|---|
| 用例 | TC-CONC-001、JMeter 线程组 1 |
| 现象 | 顺序登录 100% 成功;并发 ≥5 线程时登录 100% 返回 401,并发注册 80% 返回 400 |
| 证据 | 后端日志 32 次 bcrypt hash failed: crypt() returned null;man 3 crypt 标注 MT-Unsafe race:crypt |
| 位置 | backend/src/utils/password.cpp:33、:41 |
| 根因 | crypt() 使用静态缓冲区,多线程并发调用互相踩踏,返回 NULL 或被污染的哈希 |
| 影响 | 核心功能在并发下完全不可用;可能把错误哈希写入库导致账号永久无法登录 |
| 建议 | 改用 crypt_r()(每线程独立 crypt_data),或用互斥锁串行化 |
P1 ------ 高
D-02 更新题目会清空测试用例,导致该题后续提交全部判 SE
| 项 | 内容 |
|---|---|
| 用例 | TC-ADMIN-010 |
| 复现 | 创建题目 → 上传测试用例 → PUT 修改标题 → 查询该题,testcase_in_key 变为空 |
| 位置 | backend/src/handlers/admin_handler.cpp 的 parseProblemFromJson 不解析用例 key;problem_service.cpp 的 update 未保留原 key |
| 影响 | 管理员编辑一次题目后,该题所有提交都会得到「系统错误」 |
| 建议 | update 时保留未传字段的既有值(改为真正的部分更新) |
D-03 非数字/混合题目 ID 处理错误,且存在路径注入前缀解析
| 项 | 内容 |
|---|---|
| 用例 | TC-PROB-011、TC-PROB-013、TC-SEC-004 |
| 现象 | ① GET /api/problems/abc → 500 且响应体为空 (破坏统一 JSON 信封) ② GET /api/problems/1abc → 200 且返回题目 1 ③ GET /api/problems/1%20OR%201=1 → 200 且返回题目 1 |
| 位置 | backend/src/handlers/problem_handler.cpp:52 直接 std::stoll,无 try/catch,服务端也未注册全局异常处理器 |
| 根因 | std::stoll 的前缀宽容解析 特性("1abc" 解析为 1),且异常未被捕获 |
| 影响 | 参数校验形同虚设,客户端错误请求会拿到合法数据;异常路径返回 500 空体不利于排障 |
| 建议 | 用严格数字校验(如正则 ^\d+$)+ try/catch,非法参数统一返回 400 |
P2 ------ 中
D-04 非法 status 筛选返回 500 而非 400
- 用例
TC-LIST-003:GET /api/submissions?status=bogus→ 500 +invalid status - 位置:
backend/src/services/submission_service.cpp产出invalid status,
但submission_handler.cpp把所有未识别错误一律映射为 500 - 影响:客户端参数错误被误报为服务端故障;建议 handler 增加错误前缀到状态码的映射
D-05 恰好 65536 字节代码返回 500(长度上限与数据库列容量不一致)
- 用例
TC-SUB-015:提交 65536 字节代码 → 500 +failed to create submission - 分析:服务层长度校验为
code.size() > 65536才拒绝(即 65536 视为合法),
但submissions.code列类型是TEXT(上限 65535 字节),写入时溢出报错 - 影响:边界值上的错误不是清晰的参数错误,而是 500
- 建议:把长度上限调整为 65535,或把列类型改为
MEDIUMTEXT
D-06 判题耗时统计包含容器启动开销(已排除,非缺陷)
⚠️ 本条在真实环境下未能复现 ,特此更正说明:
早期经 SSH 隧道测试时观察到「题目时限 1000 ms,AC 用例耗时 1745~4641 ms」,
一度判定为「AC 但耗时超时限」。但 8088 端口放行后改为公网直连 复测,
AC 用例耗时稳定为 326 ms 、WA 为 295 ms ,均远低于题目时限,
用例
TC-SUB-019通过 。结论:早期观测值是压测链路(自建 SSH 隧道)自身引入的时延 造成的假象,
不是被测系统的缺陷,故从缺陷清单中移除。
保留一条设计观察 (不构成缺陷):
backend/judge/sandbox.cpp用整条docker run的墙钟时间作为
time_used_ms,其中包含容器启动开销,因此该数值大于程序真实运行时间 ;当题目时限设置得很小(如 100 ms 级)或宿主机负载很高时,存在统计偏大的风险。
建议后续改为采集容器内进程的 CPU 时间(
/usr/bin/time、getrusage),或引入 warm container 池消除启动开销。
D-07 管理后台「新增题目」按钮失效(弹窗永不出现)
-
用例
TC-UI-032;截图18_管理后台_新增题目失败.png -
位置:
frontend/js/admin.js:244javascriptdocument.getElementById('formTestCases').value = ''; // 全前端仅此一处,元素不存在该语句位于
resetForm()中,在设置弹窗标题与调用openModal()之前 抛出异常,
导致弹窗永远打不开 -
验证:全前端(html+js)搜索
formTestCases仅命中这一行,admin.html中不存在该 id -
影响:管理端无法通过界面新增题目,只能靠编辑已有题目或直接调 API
-
建议:删除该行,或补齐对应表单元素
D-08 密码允许为纯空白字符
- 用例
TC-AUTH-020:提交password = " "(6 个空格)→ 注册成功 - 位置:
backend/src/utils/user_service.cpp:13仅校验长度password.length() < 6,
未校验是否全为空白;前端register.html同样未做 trim - 影响:可注册「空格密码」账号,且登录时用户难以察觉
- 建议:增加 trim 后非空校验
P3 ------ 轻微
| 编号 | 缺陷 | 用例 | 说明 |
|---|---|---|---|
| D-09 | 空代码的错误文案误导 | TC-SUB-010 |
提交空 code 返回 problem_id and code required,应提示 code cannot be empty(与缺 problem_id 共用文案) |
| D-10 | 成功响应 data 为 null 而非 {} |
Postman 4 条断言失败 | response.h 中 const nlohmann::json& data = {} 默认构造出的是 null 而非空对象,应写 nlohmann::json::object() |
| D-11 | 上传测试用例不选文件仍返回 200 | TC-ADMIN-011 |
input_file/output_file 均可缺省,管理员易误以为配置成功;建议至少要求一个文件 |
| D-12 | 更新题目缺少难度枚举校验 | TC-ADMIN-009 |
创建题目会校验 difficulty ∈ {easy,medium,hard},更新路径却没有,非法值会进入数据库 |
| D-13 | language=c 被接受但始终按 C++17 编译 |
TC-SUB-017 |
提交语言选 c 可正常受理且界面显示「语言: c」,但判题机固定执行 g++ -std=c++17 |
缺陷分布
| 严重程度 | 数量 | 编号 |
|---|---|---|
| P0 严重 | 1 | D-01 |
| P1 高 | 3 | D-02、D-03(含 3 条用例)、D-07 |
| P2 中 | 3 | D-04、D-05、D-08 |
| P3 轻微 | 5 | D-09 ~ D-13 |
| 合计(已确认) | 12 | |
| 已排除 | 1 | D-06(经公网复测确认非缺陷,见上) |
说明:另有 2 条曾被记为失败的用例,经复核属测试脚本自身缺陷 而非产品缺陷,已修正:
① UI 用例读取提交详情弹窗时未等待异步内容加载;
② 在跑接口测试的同时并行运行了另一个脚本,导致
crypt()竞态被触发。上述用例在干净环境下均已通过(最终 UI 通过率 33/34,仅剩 D-07 一条真实失败)。
十一、测试结论与改进建议
11.1 结论
- 核心判题功能完全可用 :AC / WA / TLE / MLE / RE / CE / SE 七种状态全部通过真实验证,
输出比对的行尾空白与末尾空行容差符合预期,Docker 沙箱的资源限制(内存、超时)也真实生效。 - Web 层基本功能健全 :119 条用例中 110 条通过(接口 77/85、UI 33/34),
题目浏览类接口在 20 并发下零错误,
权限控制(401/403)与前端三视角差异(游客/用户/管理员)均正确。 - 存在一个 P0 级并发缺陷 :
crypt()非线程安全导致并发登录/注册几乎全部失败。
该缺陷在顺序测试下完全不可见,只有并发测试才能暴露 ------ 这也是本次测试最有价值的产出。 - 可用性方面的明显短板:管理后台「新增题目」入口因前端 JS 异常完全不可用。
11.2 改进建议(按优先级)
| 优先级 | 建议 |
|---|---|
| P0 | 用 crypt_r() 替换 crypt();并在 CI 中加入并发登录用例作为回归门禁 |
| P1 | 修复 admin.js:244 的 #formTestCases 空引用;为前端的写入接口补充单元/UI 冒烟用例 |
| P1 | 题目更新改为真正的部分更新,保留未传字段;并为「更新后用例仍可判题」加回归用例 |
| P1 | 统一 ID 参数的严格数字校验,注册全局异常处理器,保证任何情况都返回 JSON 信封 |
| P2 | 统一长度上限与数据库列容量的定义(65536 vs TEXT 65535) |
| P2 | 判题耗时改为采集容器内进程 CPU 时间,或引入容器池消除启动开销 |
| P2 | handler 层建立「错误码 ↔ HTTP 状态码」映射表,避免参数错误一律 500 |
| P3 | 修正 nlohmann::json 默认构造为 null 的问题;补齐用例上传的文件校验 |
| 工程 | 补充提交频率限制、JWT 密钥强制从环境变量注入(当前默认值为硬编码)、启用 HTTPS |
复现方式
bash
# 1. 初始化测试数据
set OJ_BASE_URL=http://139.196.124.168:8088
python data/seed_test_data.py
# 2. 接口自动化测试
python automation/api_tests.py
# 3. UI 自动化测试(自动产出截图)
python automation/ui_tests.py
# 4. 并发正确性测试
python automation/concurrency_tests.py
# 5. Postman 集合(newman)
newman run postman/在线OJ平台.postman_collection.json \
-e postman/在线OJ环境.postman_environment.json \
--reporters cli,htmlextra
# 6. JMeter 性能测试
jmeter -n -t jmeter/在线OJ平台性能测试.jmx \
-l results/jmeter_result.jtl -e -o results/jmeter_report
附录:测试数据统计
| 统计项 | 数值 |
|---|---|
| 测试用例总数 | 119 条(接口 85 + UI 34) |
| 通过 | 110 条(接口 77 + UI 33) |
| 失败 | 9 条(全部对应真实缺陷) |
| 总通过率 | 92.4% |
| 发现缺陷 | 12 个已确认(P0×1、P1×3、P2×3、P3×5)+ 1 个已排除 |
| Postman 断言 | 412 条(失败 4 条) |
| JMeter 样本 | 746 个 |
| 页面截图 | 19 张 |
| Xmind 导图 | 2 份 |