跳转至

C/C++ 自动评分器

此文件记录了 prairielearn/grader-c Docker 映像中包含的默认 C/C++ 自动分级器。有关如何设置外部评分器的一般信息,请查看 外部评分 文档。

设置

info.json

问题应该首先设置启用外部评分,并在info.json设置中设置"gradingMethod": "External"。要使用本文档中详细介绍的特定 C/C++ 自动评分器,在 "externalGradingOptions" 字典中,"image" 应设置为 "prairielearn/grader-c"。无需提供"entrypoint"

完整的 info.json 文件应类似于:

info.json
{
  "uuid": "...",
  "title": "...",
  "topic": "...",
  "tags": ["..."],
  "type": "v3",
  "singleVariant": true,
  "gradingMethod": "External",
  "externalGradingOptions": {
    "image": "prairielearn/grader-c",
    "timeout": 10
  }
}

question.html

使用此自动评分器的大多数问题将包含 pl-file-editorpl-file-upload 元素,但也可能使用其他元素(例如,用于简短表达式的 pl-string-input)。该问题还应在 pl-submission-panel 中包含 pl-external-grader-results 以显示评分作业的状态。还建议在提交面板中放置 pl-file-preview 元素,以便学生可以看到他们以前提交的代码。下面给出了一个示例问题标记:

<pl-question-panel>
  <pl-file-editor file-name="square.c" ace-mode="ace/mode/c_cpp"></pl-file-editor>
</pl-question-panel>

<pl-submission-panel>
  <pl-external-grader-results></pl-external-grader-results>
  <pl-file-preview></pl-file-preview>
</pl-submission-panel>

tests/test.py

test.py 文件将包含 C/C++ 评分者必须执行的基本测试。一个简单的 test.py C 代码将如下所示:

#! /usr/bin/python3

import cgrader

class QuestionGrader(cgrader.CGrader):

    def tests(self):
        # Include tests here

g = QuestionGrader()
g.start()

一个简单的 test.py 来对 C++ 代码进行评分将如下所示(唯一的区别是父类):

#! /usr/bin/python3

import cgrader

class QuestionGrader(cgrader.CPPGrader):

    def tests(self):
        # Include tests here

g = QuestionGrader()
g.start()

上面的 tests 方法将包含用户测试的基础,并且可以访问 C/C++ 评分器提供的一些常规函数。下面列出了一些可以调用的方法。

使用 pl-file-editorpl-file-upload 或通过工作区提交的任何文件都可供 C/C++ 评分者使用。要根据 server.py 定义的参数创建测试,或者访问其他元素(例如 pl-string-input 元素)提交的数据,您可以访问 self.data 字典。该字典包含与 server.py 中的 grade() 函数中找到的键类似的键,例如 self.data["params"]self.data["submitted_answers"]

可用的测试选项

编译学生和教师代码后,可以使用三种方法之一创建测试。方法的选择取决于学生期望编写的代码类型以及需要执行的验证类型。三个主要选项是:

  • 运行从学生代码编译的可执行文件,并检查其输出。如果学生需要编写可以从命令行执行的开放式完整程序,并且程序的主要目的是根据标准输入、命令行参数或外部文件生成一些预定的输出,则建议使用此选项。
  • 运行基于Check框架的单元测试套件。对于更复杂的情况,强烈建议使用此选项,在这些情况下,学生需要实现具有明确定义的行为的特定功能,并且必须检查多个测试用例和场景。如果您想检查内存泄漏和悬空指针,也建议使用此选项,这可以使用 AddressSanitizer 功能 来完成。
  • test.py 中执行您自己的检查。此选项可用于执行依赖于解析学生代码本身的检查(例如,查找递归或特定库的使用等功能)或副作用(例如,检查文件是否正确创建或修改)。您可以使用 self.run_command() 方法 执行学生代码并捕获其输出,然后创建您自己的测试结果。您还可以使用 clang 等库来解析学生代码并检查特定功能,或对代码执行静态分析。这可以与使用上述其他方法的学生代码执行相结合。

编译 C/C++ 程序

要编译C或C++文件,您可以使用方法self.compile_file()。假设学生编写了一个包含 main 函数的完整 C 文件,对此方法的典型调用将包括两个参数:要编译的 C 或 C++ 文件的名称(学生提交的文件)和要创建的可执行文件的名称。

self.compile_file("square.c", "square")      # Compile the file, but do not create a unit test result
self.test_compile_file("square.c", "square") # Compile the file and give one point to student if compilation is successful

默认情况下,这些方法使用 gccg++(分别用于 CGraderCPPGrader)来编译代码。要使用不同的编译器(例如,clang),请设置 compiler 参数:

self.compile_file("square.c", "square", compiler="clang")

默认情况下,如果编译失败,它将停止所有测试并将提交返回为不可评分。这意味着提交的内容不会计入学生的提交限制,并且学生不会获得成绩。然而,学生将看到编译的结果。如果您希望测试在失败时继续进行,您可以使用以下命令调用该函数:

self.compile_file("square.c", "square", ungradable_if_failed=False)

默认情况下,如果编译成功但给出警告,则带有警告的消息将列在主结果消息中,位于测试结果上方。如果您不希望在结果顶部列出警告,可以使用以下命令调用该函数:

self.compile_file("square.c", "square", add_warning_result_msg=False)

设置编译标志

您还可以使用 flags 参数包含 gcc(或 g++)接受的其他编译标志,可以使用单个标志字符串或字符串数​​组来调用它:

self.compile_file("square.c", "square", flags="-Wall -O3") # single string
self.compile_file("square.c", "square", flags=["-Wall", "-O3"]) # array

对于基于 pkg-config 的标志,请将 pkg_config_flags 与应查询的一个或多个库一起使用。

self.compile_file("square.c", "square", pkg_config_flags="check ncurses") # single string
self.compile_file("square.c", "square", pkg_config_flags=["check", "ncurses"]) # array

处理多个文件

也可以测试学生仅提交部分申请的项目。要使用讲师实现的某些函数(例如,包括 main 函数)来编译用户提交的 C/C++ 文件,您可以将 main.c(或 main.cpp)文件保存在 tests 文件夹中并运行:

self.compile_file("square.c", "square", add_c_file="/grade/tests/main.c")

上面的指令将把学生提供的 C/C++ 文件与教师提供的 C/C++ 文件编译成相同的可执行文件。

如果学生实现的任何函数也由教师的代码实现,包括 main(如果提供),则该函数将被忽略,并且教师提供的函数将优先。这是通过使用 gcc-Wl,--allow-multiple-definition 标志编译代码来实现的。但是,如果需要参考学生实现的某些功能,则可以使用选项 objcopy_args。如果提供,则使用指定的参数列表对学生的代码调用 objcopy 命令。此选项仅适用于学生文件,不适用于添加的 (instructor-provided) 文件。例如,以下指令编译与上述相同的文件,但将学生的 main 函数重命名为 student_main 并将静态函数 my_static_fn 设为全局,以便教师的 main 函数可以直接调用这两个函数。

self.compile_file(
  "square.c",
  "square",
  add_c_file="/grade/tests/main.c",
  objcopy_args=["--redefine-sym", "main=student_main", "--globalize-symbol", "my_static_fn"],
)

通过提供文件列表,还可以将多个学生文件和多个问题提供的文件编译为单个可执行文件:

self.compile_file(
    ["student_file1.c", "student_file2.c"],
    "executable",
    add_c_file=["/grade/tests/question_file1.c", "/grade/tests/question_file2.c"],
    flags=["-I/grade/tests", "-I/grade/student"],
)

如果编译涉及包含 (.h) 文件,则还建议使用标志 -I/grade/tests(对于问题提供的包含)和 -I/grade/student(对于学生提供的包含)。特定的 .h 文件不需要列为 compile_file 的参数。

如果未提供可执行文件名称,则文件将仅编译为等效的目标文件(扩展名为 .o)。要将这些文件链接到可执行文件,可以使用 link_object_files。该函数接收三个强制参数:学生目标文件、附加目标文件(如果没有,可以设置为 None)和可执行文件名称。这种分离允许在不同的 C 文件之间或编译和链接之间进行更微调的编译标志,以及使用生成的目标文件执行其他操作。例如,以下序列编译具有不同标志的学生文件和教师文件。

self.compile_file(["student_file1.c", "student_file2.c"],
                  flags=["-I/grade/tests", "-I/grade/student", "-Wall", "-g"])
self.compile_file([], # No student files in this invocation
                  add_c_file=["/grade/tests/question_file1.c",
                              "/grade/tests/question_file2.c"],
                  flags=["-I/grade/tests", "-I/grade/student"])
self.link_object_files(["student_file1.o", "student_file2.o"],
                       ["/grade/tests/question_file1.o", "/grade/tests/question_file2.o"],
                       "executable")

link_object_files 还接受 flagspkg_config_flagsadd_warning_result_msg=Falseungradable_if_failed=False 等参数,其功能与 self.compile_file()self.test_compile_file() 相同。

限制特定函数或全局变量的使用

对于不允许学生使用一组特定函数或全局变量(例如,不允许学生使用 system 库调用)的问题,可以拒绝一组特定符号。如果代码中引用了这些符号中的任何一个,则此选项将导致类似于编译错误的错误。仅根据此符号列表检查学生文件,因此它们仍然可以在教师代码中使用。

self.compile_file(
    ["student_file1.c", "student_file2.c"],
    "executable",
    add_c_file=["/grade/tests/question_file1.c", "/grade/tests/question_file2.c"],
    flags=["-I/grade/tests", "-I/grade/student", "-lrt"],
    reject_symbols=["system", "vfork", "clone", "clone3", "posix_spawn", "posix_spawnp"],
)

创建编译测试结果

要根据编译本身创建测试结果,请使用 self.test_compile_file() 而不是 self.compile_file()。此方法接受与 self.compile_file() 相同的参数,但默认情况下将创建一个名为“Compilation”的测试,值一分。

self.test_compile_file("square.c", "square") # Compile the file and give one point to student if compilation is successful

要更改名称和/或点,请设置 namepoints 参数,如下所示:

self.test_compile_file("square.c", "square", name="Compilation of the first file", points=3)

请注意,如果您想为编译创建测试结果,但不想为其分配任何点,则可以将 points 设置为零。这与 add_warning_result_msg=False 参数结合使用可能很有用,在这种情况下,这将导致测试输出中显示警告而不是主要结果消息。

self.test_compile_file("square.c", "square", points=0, add_warning_result_msg=False)

运行程序并检查其标准输出

self.test_run() 方法可用于运行可执行文件并检查其输出。这通常是由上面的编译器生成的程序,但它可以用于任何程序。该程序将作为非特权用户 运行。

唯一必需的参数是 command 参数,它对应于要执行的命令。如果不设置其他参数,则程序在一定的时间限制内完成即可通过测试。

self.test_run("./square")

验证执行程序的输出

但在大多数情况下,将执行该程序来检查程序的输出,这可以通过包含 exp_output 参数来完成。在最简单的情况下,可以为 exp_output 提供单个字符串,如果在程序输出中的某个位置找到该字符串,则测试将通过。

self.test_run("./square", exp_output="SUCCESS")

exp_output 参数还可用于通过传入字符串列表来检查多个输出模式。然后,测试将查找程序输出中的所有模式,测试结果将取决于 must_match_all_outputs 标志。该标志可以设置为:

  • must_match_all_outputs="any":如果在程序输出中找到任何模式,则测试通过并分配满分(这是默认设置)。为了向后兼容,值 False 也被接受。
  • must_match_all_outputs="all":必须在程序输出中找到所有模式才能通过测试。为了向后兼容,值 True 也被接受。
  • must_match_all_outputs="partial":分配给测试的分数基于在程序输出中找到的模式数量(例如,如果找到四个模式中的三个,则为测试分配 0.75 分)。
self.test_run("./square", exp_output=["SUCCESS", "CORRECT"]) # default, either SUCCESS or CORRECT are enough for full points
self.test_run("./square", exp_output=["TEST 1 PASSED", "TEST 2 PASSED"],
              must_match_all_outputs="partial") # Test passes with 0, 0.5 or 1, depending on if none, one or two patterns are found

有时测试必须确保在程序的输出中找不到某些字符串。这可以通过 reject_output 参数来实现,该参数也可以是数组或单个字符串。

self.test_run("diff -q output.txt expected.txt", reject_output=["differ"])

如果您想在测试消息或输出中突出显示预期和拒绝的输出,您可以使用 highlight_matches 参数。此选项将以绿色(对于预期输出)和红色(对于拒绝输出)突出显示程序输出中匹配的所有字符串。

self.test_run("./square",
              exp_output=["TEST 1 PASSED", "TEST 2 PASSED"],
              reject_output=["ERROR", "FAIL"],
              must_match_all_outputs="all",
              highlight_matches=True)

默认情况下,程序输出、预期输出和拒绝输出字符串中的任何类似空格的字符序列(空格、换行符、回车符、制表符)都将被视为单个空格进行比较。这意味着间距数量和类型的差异将被忽略。因此,例如,如果输出打印两个数字为 1 \n 2,而预期输出为 1 2,则测试将通过。但是,如果意图是空格必须与模式完全匹配,则可以将 ignore_consec_spaces 选项设置为 False

self.test_run("./pattern", exp_output="  1  2  3\n  4  5  6\n  7  8  9",
              ignore_consec_spaces=False)

默认情况下,忽略大小写差异。要使所有比较区分大小写,请将 ignore_case 参数设置为 False

self.test_run("./lowercase ABC", exp_output="abc", ignore_case=False)

对于 exp_outputreject_output 来说,可以通过提供来自 Python 的 re 模块 的已编译模式对象来使用正则表达式。注意:如果使用模式对象,参数 ignore_consec_spacesignore_case 不起作用。如果这些模式应忽略连续空格,则可以使用 \\s+ 等模式代替空格,同时可以使用 re.I 标志 来忽略大小写;

self.test_run("./valid_date", exp_output=re.compile('([12]\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01]))'))

为执行的程序提供输入

此方法还可以提供一个输入字符串,以传递到程序的标准输入,并使用 input 参数:

self.test_run("./square", input="3\n", exp_output="9")

要对命令使用命令行参数,参数可以包含在 command 本身中,也可以与 args 参数一起包含,该参数可以是设置为字符串的单个参数,也可以是列表中的多个参数:

self.test_run("./square 3 5", exp_output=["9", "25"],
              must_match_all_outputs="all")
self.test_run("./square", args="3", exp_output="9")
self.test_run("./square", args=["3", "5"], exp_output=["9", "25"],
              must_match_all_outputs="all")

限制执行时间和输出大小

为了避免学生提供的代码运行时间超过预期的问题(例如无限循环的情况),程序将在一秒后超时。在这种情况下,测试将被视为失败。可以使用 timeout 参数更改此设置,该参数应设置为秒数。

self.test_run("./slowprogram", exp_output="COMPLETED", timeout=10)

为了避免学生提供的代码生成太大而导致 ZJUI-Learn 无法处理的问题,默认情况下,任何输出超过 10 KB(更准确地说,10240 个字符)的程序都将失败并截断其输出。要更改此限制,请使用 size_limit 参数,该参数应设置为多个字符。

self.test_run("./verboseprogram", exp_output="COMPLETED", size_limit=102400)

设置自定义测试名称、消息和分数

测试将被创建并显示给用户,默认值为 1 分。包含 input 参数的测试的默认名称为:Test with input "<INPUT>"(其中 INPUT 是提供的输入)。对于使用 args 的测试,默认名称为 Test with arguments "<ARGS>"(其中 ARGS 是由空格分隔的参数集)。还将包含一条消息,其中包含预期输出和拒绝输出的摘要。要更改这些设置,请使用 max_pointsmsg 和/或 name 参数:

self.test_run("diff -q output.txt expected.txt", reject_output=["differ"],
              name="Comparing final output", max_points=3,
              msg="Output file should match expected file.")

运行 Check 框架测试套件

对于涉及更复杂场景的测试,特别是与单个函数调用和单元测试相关的测试,C 自动评分器允许与 Check 框架 的修改版本集成。该框架提供了运行多个测试套件和带有单独单元测试的测试用例的功能。它还能够通过在隔离进程中运行单元测试来捕获信号(例如,分段错误)。

要运行 Check 套件,请在 tests 目录中创建一个主 C 文件,其中包含测试和运行 Check 套件的 main 函数。该 C 文件可能如下所示:

main.c
#include <check.h>

extern int factorial(int x);

START_TEST(test_factorial) {
    ck_assert_int_eq(factorial(0), 1);
    ck_assert_int_eq(factorial(1), 1);
    ck_assert_int_eq(factorial(2), 2);
    ck_assert_int_eq(factorial(5), 120);
    ck_assert_int_eq(factorial(10), 3628800);
}
END_TEST

int main(void) {
    Suite *s = suite_create("factorial");

    TCase *tc_factorial = tcase_create("Check factorial return value");
    tcase_add_test(tc_factorial, test_factorial);
    suite_add_tcase(s, tc_factorial);

    SRunner *sr = srunner_create(s);
    srunner_run_all(sr, CK_NORMAL);
    srunner_free(sr);
    return 0;
}

有关如何创建测试套件、测试用例和单元测试的教程可以在官方 Check 文档 中找到。示例课程还包括可作为起点的基本问题

Note

请注意,使用 Check 框架的功能依赖于其测试日志记录功能。为了确保自动评分器正确捕获测试,您不应覆盖日志文件。

基于 Check 的套件的典型 test.py 文件将如下所示,假设 student_code.c 包含学生代码,/grade/tests/main.c 包含 Check 测试,如上例所示:

test.py
import cgrader

class DemoGrader(cgrader.CGrader):
    def tests(self):
        self.compile_file("student_code.c", "main", add_c_file="/grade/tests/main.c",
                          # The following must be included if compiling a Check test
                          pkg_config_flags="check")
        self.run_check_suite("./main")

g = DemoGrader()
g.start()

要编译学生代码和测试套件,请使用上述 self.compile_file()self.test_compile_file()。确保 pkg_config_flags 参数包含 check 库以便正确编译。

self.run_check_suite() 方法将调用包含 Check 测试套件的可执行文件,解析日志文件,并为 Check 套件执行的每个单元测试创​​建一个自动评分器测试。将为每个测试用例中的每个单元测试创​​建一个测试结果,因此您可以根据需要创建任意数量的测试用例和单元测试,以便向学生提供更多详细信息。测试的名称通常是 Check 测试用例的名称,后跟单元测试 ID,但这可以通过将以下参数设置为 TrueFalse 来更改:

  • use_suite_title:在测试名称中使用测试套件的标题(默认值:false);
  • use_case_name:测试名称中使用测试用例的名称(默认:true);
  • use_unit_test_id:在测试名称中使用单元测试的ID(默认值:true);
  • use_iteration:对于在循环中执行的测试,请在测试名称中包含迭代编号(默认值:false)。
self.run_check_suite("./main", use_suite_title=True, use_unit_test_id=False)

自动评分器中使用的 Check 版本已稍作修改,以包含针对恶意学生代码的额外保护措施。这些保护措施限制运行单元测试的进程对测试日志和其他资源的访问。为了确保这些保护措施按预期工作,您的测试应用程序:

  • 应保持 Check 的默认分叉状态处于启用状态,即不要设置“无分叉模式”。
  • 应该在单元测试本身或检查的装置中打开任何文件或类似文件的资源。不要在未经检查的装置或主应用程序中打开文件。
  • 任何学生应用程序不应依赖环境变量。如果需要环境变量,请在单元测试本身或检查的装置中手动设置它们。

如果您的应用程序明确需要保留上述任何受限制的环境,您可以在代码中禁用其中一些保护措施。 请注意,禁用这些保护措施会增加学生绕过您的单元测试和自动评分器的机会,因此仅在绝对必要时才执行此操作。您可以通过在测试代码的顶部设置以下预处理器指令(在 #include <check.h> 之前)来实现此目的:

// Use this directive to retain file descriptors opened by the test application or unchecked fixtures
#define PLCHECK_KEEP_FD

// Use this directive to run the unit test applications as root
#define PLCHECK_KEEP_UID

// Use this directive to retain environment variables
#define PLCHECK_KEEP_ENV

// Use this directive to have the unit test process remain a direct child of the test application
#define PLCHECK_NO_EXTRA_FORK

识别悬空指针、内存泄漏和类似问题

测试 C/C++ 代码时的一个主要问题是识别悬空指针和内存泄漏的情况。 AddressSanitizer 库可用于此自动评分器中的此目的。特别是,它能够检测:释放后使用和返回后使用;堆、栈和全局数组中的越界访问;和内存泄漏。

要将此库添加到代码中,请将以下选项添加到编译函数(compile_filetest_compile_filelink_object_files):

self.compile_file(..., enable_asan=True)

默认情况下,上面的选项将使用标志来编译代码,这些标志将导致应用程序在识别出无效内存访问时立即中止,或者在内存泄漏的情况下退出之前。如果您使用自动评分器工作流程来检查程序的标准输出,则此功能应该捕获大多数情况,尽管您可能希望包含一些捕获内存泄漏的拒绝字符串。例如:

self.test_run(..., reject_output=['AddressSanitizer'])

如果您使用基于检查的工作流程,请注意,虽然上述设置将导致测试在这些情况下失败,但它可能不会向学生提供有用的消息。在这种情况下,为了向学生提供更详细的反馈,强烈建议您在主函数的开头添加对 pl_setup_asan_hooks() 的调用,如下所示:

int main(int argc, char *argv[]) {

  pl_setup_asan_hooks();
  Suite *s = suite_create(...);
  // ...
}

如果您需要对这些内存访问问题发生的时间和地点进行更精细的控制,您可以使用 ASAN 接口来提供进一步的控制。例如,如果要识别各个指针的状态以检查它们是否已正确分配,可以使用 __asan_address_is_poisoned__asan_region_is_poisoned

  ck_assert_msg(__asan_address_is_poisoned(deleted_node), "Deleted node has not been freed");
  ck_assert_msg(!__asan_address_is_poisoned(other_node), "Node other than the first element has been freed");
  ck_assert_msg(!__asan_region_is_poisoned(new_node, sizeof(struct node)), "Node was not allocated with appropriate size");

还可以通过在调用 run_check_suite 时设置环境变量 ASAN_OPTIONS设置特定标志 来更改 AddressSanitizer 的行为。例如,要禁用内存泄漏检查,您可以使用:

self.run_check_suite("./main", env={"ASAN_OPTIONS": "detect_leaks=0"})

运行命令而不创建测试

也可以运行不直接链接到特定测试的命令。这可以通过 self.run_command() 方法来完成,该方法至少接收一个命令作为参数。此命令可以是带或不带参数的单个字符串,也可以是包含可执行文件作为第一个元素以及后面的参数的字符串数组。该方法返回一个包含程序生成的标准输出(和标准错误)的字符串。

files = self.run_command("ls -alR /grade")
files = self.run_command(["ls", "-alR", "/grade"])

默认情况下,该命令将以沙盒用户 身份运行。如果您需要作为容器的 root (运行测试脚本本身的同一用户)运行命令,您可以将参数 sandboxed 设置为 False (不建议用于学生提供的任何程序):

result = self.run_command("rm -rf testfile.txt", sandboxed=False)

要提供用作程序标准输入的字符串,请使用 input 参数:

result = self.run_command("./square", input="3\n")

为了确保程序不会永远运行,您可以设置 timeout 选项,该选项提供以秒为单位的超时。如果程序在此超时内未完成,该方法将返回标准超时消息。强烈建议学生提供的代码在超时设置下运行。

result = self.run_command("./square", timeout=1)

如果程序需要特定的环境变量,您可以设置 env 参数。此参数必须作为键值 dict 提供。

result = self.run_command("./square", env={"TEMP_FILE": "/tmp/my_temp.dat"})

手动添加测试结果

self.test_compile_file()self.test_run() 等方法将创建一个新的测试结果并呈现给用户。还可以使用 self.add_test_result() 方法基于单独的计算创建您自己的测试。此方法最简单的调用是仅使用测试名称,这将创建一个值得一分的通过测试,没有消息,没有输出,也没有描述。

self.add_test_result("Bonus point for submitting something!")

您还可以选择向测试添加描述、消息和输出:

self.add_test_result("Bonus point for submitting something!",
                     description="This is for all my students, thank you for submitting.",
                     msg="Nothing to be expected.",
                     output=submitted_answer)

要设置测试值得的点数和/或其最大点数,请使用 pointsmax_points 参数。 max_points 值必须是数字(整数或浮点数),如果未提供,则默认为 1points 参数可以是数字,在这种情况下,它基于最大点数;或者您可以将 points 设置为类似布尔的表达式,在这种情况下,如果表达式为 False,则 points 将设置为 0;如果表达式为 True,则 max_points 将设置为 max_points

self.add_test_result("I am lazy, everyone gets 70%",
                     points=70, max_points=100)

此方法还允许您向结果添加一张或多张图像。图像必须遵循外部分级 文档中描述的格式。

self.add_test_result("Generated image", points=matched_pixels,
                     max_points=total_pixels,
                     images=[{"label": "Your image", "url": dataURI},
                             {"label": "Expected image", "url": expectedURI})

沙箱执行

自动评分器设置为允许程序以 root 或非特权用户身份运行。此功能可确保学生无法操纵测试环境或手动修改容器中的成绩。

默认情况下,沙箱用户无权访问容器中 /grade 文件夹内的任何文件。如果期望在此环境中运行的程序能够访问特定文件,则必须显式向用户授予访问权限。这可以通过 change_mode() 方法来完成,该方法接收两个参数:文件名/路径和要设置的模式。

self.change_mode("/grade/student/myfile.txt", "744")

任何使用 test_compile_file() 编译的程序都将被授予可执行权限(模式 755),因此这些程序不需要被您的测试明确允许。

与类 Unix 系统中的典型情况一样,创建新文件需要在创建文件的目录中具有写权限。因此,如果程序需要在特定目录中创建新文件,则也必须向该目录授予写权限。您应该避免授予对系统目录(例如 /grade/grade/tests)的写入权限,否则学生可能能够绕过沙箱环境。相反,为学生文件创建特定的子目录,并仅授予这些目录的写入权限。您可能还想将当前工作目录切换到该目录,这可以使用 os.chdir() 方法来完成。

self.run_command("mkdir -p /tmp/sbdir", sandboxed=False)
self.change_mode("/tmp/sbdir", "777")
os.chdir("/tmp/sbdir")

请注意,更改工作目录也会影响学生程序的调用方式。这通常意味着学生程序的任何执行都必须将典型的 ./ 前缀替换为学生文件最初编译的目录(默认情况下为 /grade/student/)。例如,如果编译过程在上述语句之前执行,则必须使用以下命令来运行名为 myprog 的学生程序:

self.test_run("/grade/student/myprog", ...)

还建议使用 tempfile 库来创建学生代码可能使用的临时文件和目录。请注意,默认情况下,沙箱用户将无法访问使用此库创建的任何文件或目录,因此您必须显式更改其模式,如上所示。