Java自动评分器¶
此文件记录了 prairielearn/grader-java Docker 映像中包含的默认 Java 自动分级器。有关如何设置外部评分器的一般信息,请访问 外部评分 文档。
概述¶
Java 自动评分器基于 JUnit 5。本质上,各个测试对应于运行测试代码的各个带注释的方法,例如(来源):
import static org.junit.jupiter.api.Assertions.assertEquals;
import example.util.Calculator;
import org.junit.jupiter.api.Test;
class MyFirstJUnitJupiterTests {
private final Calculator calculator = new Calculator();
@Test
void addition() {
assertEquals(2, calculator.add(1, 1));
}
}
自动评分器结合了多个类来允许进行这些测试:
- 学生提供的班级文件。这些是学生提交的文件(通常通过
pl-file-upload或pl-file-editor元素)包含要测试的代码。
- 测试类文件。这些由问题创建者在问题目录内的子目录
tests/junit中提供。可能有多个测试文件,每个文件有多种测试方法。
- 图书馆文件和讲师提供的课程。这些同样由教师提供,并且可以按问题或按课程设置,如下所述。
设置¶
info.json¶
问题应该首先设置启用外部评分,并在info.json设置中设置"gradingMethod": "External"。要使用本文档中详细介绍的特定 Java 自动评分器,在 "externalGradingOptions" 字典中,"image" 应设置为 "prairielearn/grader-java"。无需提供"entrypoint"。
完整的 info.json 文件应类似于:
{
"uuid": "...",
"title": "...",
"topic": "...",
"tags": ["..."],
"type": "v3",
"singleVariant": true,
"gradingMethod": "External",
"externalGradingOptions": {
"image": "prairielearn/grader-java"
}
}
!!! note “超时”
`externalGradingOptions` 字典中的 `timeout` 字段(如果设置)应该足够长,以允许自动评分器编译和运行所有测试。 30 秒的默认超时通常足以满足大多数问题,但教师可能希望根据问题的预期复杂性和测试数量调整此值。请注意,`javac` 可能需要几秒钟来编译学生代码和测试文件,因此小于 10 秒的超时可能会导致甚至在学生代码本身运行之前就发生超时。对于需要额外编译时间的更复杂的问题,或者包含大量测试或需要很长时间才能运行的测试的问题,鼓励使用更长的超时。
question.html¶
与其他自动评分器一样,使用此自动评分器的大多数问题都将包含 pl-file-editor 或 pl-file-upload 元素。该问题还应在 pl-submission-panel 中包含 pl-external-grader-results 以显示评分作业的状态。还建议在提交面板中放置 pl-file-preview 元素,以便学生可以看到他们以前提交的代码。下面给出了一个示例问题标记:
<pl-question-panel>
<pl-file-editor file-name="Example.java" ace-mode="ace/mode/java"></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/junit/*.java¶
在tests/junit目录中,可以为一个或多个类提供与JUnit测试相对应的方法。自动评分器将编译此目录中找到的每个类,然后在这些类上运行 JUnit。
作为包一部分的测试必须在等效的子目录中提供。例如,如果名为 AppTest 的测试位于包 com.example.myapp.test 中,则必须将其保存在文件 tests/junit/com/example/myapp/test/AppTest.java 中。
JUnit 发现的每个测试都将作为单独的结果提供给学生。测试结果的名称基于测试的显示名称。默认情况下,每个测试值一分。要更改此默认值,必须提供 @Tag("points=XX") 格式的 标签(用多个点替换 XX)。例如:
@Test
@DisplayName("Test addition of values 1 and 1")
@Tag("points=5")
void addition() {
assertEquals(2, calculator.add(1, 1));
}
指定点和显示名称的另一种方法是使用自定义 @AutograderInfo 注释,如下所示。建议对旧版 JUnit 4 测试使用此方法,因为它为标签提供有限的支持。
import org.prairielearn.autograder.AutograderInfo;
/* ... */
@Test
@AutograderInfo(points=1, name="1. Test with input 1")
public void testWithInputOne() {
assertEquals(1, Example.square(1));
}
要更改向用户显示测试结果的顺序,您可以使用@TestMethodOrder 注释。
为了生成学生可见的输出,可以选择使用 TestReporter 类型的参数 来声明测试。调用测试报告器的 publishEntry 方法 将导致所提供的条目被打印为测试的输出。例如:
@Test
@DisplayName("Test addition of values 1 and 1")
void addition(TestReporter reporter) {
// Produce a single line of output
reporter.publishEntry("Calculating the addition of 1 and 1, which should result in 2");
// Alternatively, multiple key-value entries may be provided instead
reporter.publishEntry("TEST", "Adding 1 plus 1");
reporter.publishEntry("EXPECTED", "2");
reporter.publishEntry("RESULT", calculator.add(1, 1).toString());
assertEquals(2, calculator.add(1, 1));
}
自动评分器将根据默认 Java 行为测试是否通过或失败给出问题分。请注意,Java 的内置断言 默认情况下处于禁用状态,因此依赖于 Java 的 assert 关键字的测试可能无法按预期工作。如果需要基于 assert 语句的测试失败,则必须将程序设置为使用 -ea 选项执行,如下所列。另一种方法是在 JUnit 本身中使用 assertTrue 方法,其优点是向学生显示的错误消息提供更大的灵活性。
动态、参数化和重复测试¶
JUnit 5 支持动态生成的测试。其中包括参数化测试、重复测试和测试工厂。
虽然自动评分器支持执行这些类型的测试,但在极少数情况下,它们可能会导致分配给每次提交的总分不一致。特别是,如果不同的提交生成不同的测试集,则分配给一名学生的测试总数(以及相应的最大分数)可能与其他学生不同,从而导致分数百分比不一致。此外,如果学生的代码导致自动评分器崩溃(例如,在内存不足错误或线程耗尽的情况下),某些测试可能无法按时注册以在评分过程中考虑。
为了避免这些问题,如果特定测试类包含任何动态测试(即使用 @ParameterizedTest、@RepeatedTest 或 @TestFactory 等注释的测试),则鼓励教师将最大预期分数定义为类注释,如下所示。仅使用常规方法测试的类(即使用 @Test 等注释的测试)不需要使用此类标签,因为可以在测试过程开始时从静态测试本身轻松检索总点数。
@Tag("maxpoints=16")
public class ExampleJUnit5Test {
@ParameterizedTest(name = "Test with input {0}")
@ValueSource(ints = {0, 1, 2, 4, 10, -1, -10, -100})
@Tag("points=2")
public void testWithInput(int i) {
// ...
}
}
更改编译选项¶
默认情况下,Java 编译器将向用户显示所有编译警告,但 serial 除外(可序列化类上缺少 serialVersionUID)。如果您想更改编译警告或其他编译设置,可以通过在 info.json 中设置 JDK_JAVAC_OPTIONS 环境变量来实现,如下所示:
{
"externalGradingOptions": {
"image": "prairielearn/grader-java",
"environment": {
"JDK_JAVAC_OPTIONS": "-Xlint:-static -Xmaxerrs 3",
"JDK_JAVA_OPTIONS": "-ea"
}
}
}
上面的示例禁用 static 警告(使用应用于对象表达式的静态字段)并将错误数量限制为 3。更全面的选项列表可以在 javac 文档页面 中找到。一些感兴趣的选项可能包括:
-Xlint:none或-nowarn禁用所有警告;-Xdoclint启用 javadoc 注释警告;--release 11使用Java 11语言版本进行编译。
同样,您可以使用 JDK_JAVA_OPTIONS 为 java 命令行设置特定选项。有效选项列表可以在 java 文档页面 中找到。一些感兴趣的选项可能包括:
-Dproperty=value设置可通过System.getProperty(name)检索的系统属性;- 这对于设置 JUnit 配置属性可能很有用,如 JUnit 文档 中所述。
-ea启用 Java 断言。
图书馆和讲师提供的课程¶
教师可以提供额外的库和类作为 Java 类路径的一部分。默认情况下,问题的子目录 tests/libs 中的所有 *.jar 文件以及 tests/libs 目录本身都将包含在类路径中。因此,需要包含在应用程序的编译和运行时中的类应该保存到 *.jar 文件中并保存在 tests/libs 目录中,或者编译到同一目录内的 *.class 文件中(如果使用包,则具有适当的子目录)。
有些问题可能包括多个问题中常见的库和基类。对于此类问题,可以使用与上述相同的约定将这些库和类保存在课程的 serverFilesCourse/java/libs 目录中。但是,如果使用此选项,问题的 info.json 文件应指示应将此目录添加到评分容器中,如下所示:
{
"externalGradingOptions": {
"image": "prairielearn/grader-java",
"serverFilesCourse": ["java/libs/"]
}
}
运行 JUnit 5 测试所需的库已作为自动评分器容器的一部分包含在内,无需再次包含。
技术细节¶
沙盒环境和文件访问¶
JUnit 测试以及学生代码以非 root 用户身份在沙盒环境中执行。该代码能够创建、修改或删除沙箱用户主目录 (/home/sbuser) 内的任何文件,但无法访问环境中的大多数其他目录。这样做的目的是阻止学生创建操纵自动评分器或评分结果的代码,因为这些代码只能由自动评分器脚本更新。
如果问题包含 tests/studentFiles 目录,则在测试开始之前,其内容将被复制到沙箱用户的主目录。这允许教师提供测试可能需要的文件,例如输入文件或配置文件。如果未提供此目录,则沙箱用户的主目录在测试开始时将为空。 JUnit 测试和装置以及学生代码也可以在测试期间根据需要创建、修改或删除此目录中的文件。
讲师提供的库文件 被复制到测试环境中,因为它们位于名为 /grade/classpath 的目录中。为了确保它们可以在 Java 库中使用,沙箱用户可以读取此目录及其内容。警告教师,学生不应查看的任何文件(例如,测试文件或某些库的源代码)不应包含在问题的 tests/libs 目录或课程的 serverFilesCourse/java/libs 目录中,因为精心设计的恶意学生提交内容可能会显示此类文件。
限制访问高级功能¶
自动评分器设置为禁用一些高级 Java 功能,学生可能会使用这些功能来尝试访问有关评分环境的私有信息,例如堆或堆栈。特别是,默认情况下禁用 Java 管理选项,这会阻止学生使用 jmap 或 jstack 等工具来获取有关自动评分器的内存或线程的信息。此外,Java 代码被阻止访问 /proc、/sys 或 /dev 等目录中的文件,这些文件可能包含有关系统或正在运行的进程的信息。
目前没有方法可以针对特定问题启用对这些目录的访问。如果您有需要使用这些功能的特定用例,请在 ZJUI-Learn 存储库 中创建一个问题,描述用例和所需的功能,以及您希望提供哪些防护措施来防止滥用这些功能。
受限方法(包括最近Java版本中的JNI)也默认被禁用,这会阻止学生运行可用于访问系统信息的本机代码。如果特定问题需要这些模块,可以设置 JDK_JAVA_OPTIONS 环境变量 以包含 --enable-native-access 选项,这将允许执行特定模块。然而,教师被警告说,这可能允许学生运行恶意代码,从而访问有关评分环境的私人信息,并且仅应在必要时并在适当的防护措施到位的情况下使用。另请注意,默认情况下,自动评分器映像不包含任何 C 库或编译器,因此可能需要将本机代码作为可由 Java 代码加载的共享库(例如,*.so 文件)提供。