测试点和评测数据的保存
测试点信息的保存结构
为了方便我们理解上传测试数据的实现,我们先来看看数据的存储格式(此处仅记录各个测试点的配置以及捆绑测试的数据,测试点的输入和答案保存于文件而非数据库中;测试数据配置中与测试点无关的项没有在此处展示):
{
"test_case_config": [
{
"name": "test_case_1",
"score": 100,
"subcheck": null
}
],
"subcheck_config": [],
"use_subcheck": false,
}
{
"test_case_config": [
{
"name": "test_case_1",
"score": null,
"subcheck": 0
}
],
"subcheck_config": [
{
"score": 100
}
],
"use_subcheck": true,
}
每个测试点将有一个name
,该值没有什么硬性要求,只要不重复即可,但是为了显示看起来舒服建议还是遵循一定的规则命名,例如['test1', 'test2', 'test3', ...]
或['1', '2', '3']
。
score
和subcheck
参数均为integer
或null
,且每个时刻有且仅有一项为null
,分别对应捆绑测试是否开启。
subcheck
的值一定是从 0 开始,每次加 1 递增产生的,因为它要对应subcheck_config
这个数组中的数据。
subcheck_config
是一个数组,当不启用捆绑测试时长度为 0 。若开启捆绑测试,则subcheck_config
的长度为test_case_config
中subcheck
的最大值 +1 。
编辑界面的使用
在前端访问/problem/{id}/edit/
,选择“数据”标签页即可编辑评测数据。
在此处,有一个表格展示了每个测试点的相关信息(下称“表一”),若开启了捆绑测试,则还会有一个表格展示每组捆绑测试的相关信息(下称“表二”)。
表一中,点击“测试点名称”即可对其进行编辑,其它几项功能很明显了。需要注意的是删除操作在仅剩一个测试点时将被禁用。
当启用捆绑测试时,需保证各捆绑测试组分数加和为 100 ;关闭捆绑测试时,则须保证各测试点分数加和为 100 。
INFO
该值仅在前端校验,因为实际上讲分数加和不为 100 也可以正常返回评测状态,只是说分数不为 100 却 AC 的话看起来多少是有些奇怪了。
新增测试点需通过选择 ZIP 文件实现。选择的 ZIP 中应包含 N 组测试数据,每一组由name[i].in
和name[i].out | name[i].ans
两个文件组成。具体的,一个示例如下:
|-test_case.zip
|-1.in
|-1.out
|-2.in
|-2.ans
在本页面的所有更改都需要点击“保存”按钮才会上传,因此当你发现自己操作错误时,点击“重置”是一个明智的选择。当然为了防止我写出 bug,或许直接刷新界面更保险哈哈哈
测试点在服务器的保存
评测配置数据(上方所示的 Json)
评测配置数据存放于数据库中,每个 Problem 对象均有一个通过 OneToOneField 连接的 TestCase 对象。
测试点原始数据
测试点原始数据通过 ZIP 格式上传到后端,后端将它解压后的文件保存在后端设置的TEST_DATA_ROOT / {test_case_id}
之中,按name.in
和name.ans
的格式,其中答案文件已经做好了行末空格和末尾回车的清洗工作。
服务器还会为每个答案文件计算其内容的 MD5 值,并命名为name.md5
保存在同一目录下。
每次评测输入数据的获取、答案的比对和用户输出的保存
评测端运行用户提交的程序得到输出后也是对它清洗、计算 MD5 值之后将其与答案文件 MD5 进行比对。这样做意在防止长字符串比对引发的性能问题。
评测时,评测端会将TEST_DATA_ROOT
中的输入文件复制到工作目录BASE_DIR / {submission_id}
下(工作目录会在当前评测任务完成后自动删除),供用户提交的程序读取(这里的BASE_DIR
是在评测端设置的,不同于后端)
用户程序的输出将由评测端通过 WebSocket 传递给 Celery Worker,由它保存在SUBMISSION_ROOT / {submission_id}
中。特别说明的是,若当前测试点的评测状态为 AC,则输出文件的返回值为空以节省磁盘空间。前端应对此种情况进行特判,用户对 AC 的测试点查看输出时,将直接返回答案文件。
更改测试数据后对先前存在的提交的影响
对于每次提交产生的独立数据为数据库中存储的detail
数组以及SUBMISSION_ROOT / {submission_id}
中的输出文件。
后者的相关信息已经在上一部分介绍过了。对于前者,其结构大体如下:
[
{
"case_name": "test_case_1",
"status": 0,
"statistics": {
"time": 15,
"memory": 14073856,
"exit_code": 0
},
"subcheck": null
}
]
由此我们可以轻松看出,我们可以从前者中获取到本次提交每一个测试点的运行状态,以及这些测试点的捆绑测试关系。与此同时,也始终可以从后者中获取到每个测试点的输出文件(除非你嫌这些数据太占地方写了脚本定期删除)。
然而,每次提交并没有相互独立的输入文件和答案文件存在,同时输入文件和答案文件被更改时也不会保留历史备份。
因此,如果评测后题目的测试数据发生了变化,本次提交的各个测试点信息以及各个测试点的输出仍可获取(状态为 AC 的输出不行,原因上面说了)。但是如果出现了测试数据更新的内容中包含测试点的删除,那么这次提交各个测试点的输入文件和答案文件将无法获取;如果出现删除测试点后又新增了同名测试点,则可能会获取到和评测时不同的输入和答案文件。
上述的最后一种情况是我们所不想看到的,为了防止这一现象出现,强烈建议当一个题目出现提交后却仍需更改评测数据时,不要使用曾经使用过的测试点名称,例如可以测试点编号顺延或采用{name}_fix
的命名方法,这样子可以保证先前提交的详情页获取到的输入和答案文件不会乱掉。