chalk-grader 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- chalk_grader/__init__.py +3 -0
- chalk_grader/disqualification.py +70 -0
- chalk_grader/graded.py +61 -0
- chalk_grader/models.py +410 -0
- chalk_grader/profile_grader.py +583 -0
- chalk_grader/profiles.py +266 -0
- chalk_grader/profiles.toml +174 -0
- chalk_grader/repo.py +271 -0
- chalk_grader/result_extractor.py +357 -0
- chalk_grader/runners/__init__.py +0 -0
- chalk_grader/runners/chalk_test.h +201 -0
- chalk_grader/runners/net.py +200 -0
- chalk_grader/runners/prolog_plunit.pl +196 -0
- chalk_grader/runners/prolog_plunit.py +176 -0
- chalk_grader/runners/pytest_guard.py +175 -0
- chalk_grader/runners/stdio.py +168 -0
- chalk_grader/sandbox.py +176 -0
- chalk_grader/score.py +313 -0
- chalk_grader/scoring.py +121 -0
- chalk_grader/script_grader.py +212 -0
- chalk_grader/script_runner.py +405 -0
- chalk_grader/services.py +210 -0
- chalk_grader/suspicious.py +92 -0
- chalk_grader/task.py +29 -0
- chalk_grader-0.2.0.dist-info/METADATA +53 -0
- chalk_grader-0.2.0.dist-info/RECORD +28 -0
- chalk_grader-0.2.0.dist-info/WHEEL +4 -0
- chalk_grader-0.2.0.dist-info/licenses/LICENSE +21 -0
chalk_grader/__init__.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Reasons a grader gives no score to an answer.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from enum import Enum
|
|
8
|
+
from typing import Literal
|
|
9
|
+
|
|
10
|
+
from rich.console import RenderableType
|
|
11
|
+
from rich.text import Text
|
|
12
|
+
|
|
13
|
+
__all__ = ["Disqualified", "Timeout", "InvalidInput", "Error", "BuildError"]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Disqualified(Enum):
|
|
17
|
+
"""
|
|
18
|
+
An enumeration of reasons why a response might be disqualified.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
TIMEOUT = "The response took too long to execute."
|
|
22
|
+
INVALID_INPUT = "The response contains text with an invalid syntax."
|
|
23
|
+
ERROR = "Internal error during grading."
|
|
24
|
+
BUILD_ERROR = "The response does not compile."
|
|
25
|
+
|
|
26
|
+
def message(self) -> str:
|
|
27
|
+
return self.value
|
|
28
|
+
|
|
29
|
+
def render(self) -> str:
|
|
30
|
+
return self.name.lower().replace("_", "-")
|
|
31
|
+
|
|
32
|
+
def __str__(self) -> str:
|
|
33
|
+
return self.render()
|
|
34
|
+
|
|
35
|
+
def __repr__(self) -> str:
|
|
36
|
+
return f"{self.name.lower().title()}()"
|
|
37
|
+
|
|
38
|
+
def __bool__(self) -> Literal[False]:
|
|
39
|
+
return False
|
|
40
|
+
|
|
41
|
+
def __rich__(self) -> RenderableType:
|
|
42
|
+
return Text(self.render(), style="red")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def Timeout() -> Disqualified:
|
|
46
|
+
"""
|
|
47
|
+
Indicates that a response was disqualified due to a timeout during grading.
|
|
48
|
+
"""
|
|
49
|
+
return Disqualified.TIMEOUT
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def InvalidInput() -> Disqualified:
|
|
53
|
+
"""
|
|
54
|
+
Indicates that a response was disqualified due to invalid syntax or format.
|
|
55
|
+
"""
|
|
56
|
+
return Disqualified.INVALID_INPUT
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def Error() -> Disqualified:
|
|
60
|
+
"""
|
|
61
|
+
Indicates that a response was disqualified due to an internal error during grading.
|
|
62
|
+
"""
|
|
63
|
+
return Disqualified.ERROR
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def BuildError() -> Disqualified:
|
|
67
|
+
"""
|
|
68
|
+
Indicates that the response does not compile, so its tests could not run.
|
|
69
|
+
"""
|
|
70
|
+
return Disqualified.BUILD_ERROR
|
chalk_grader/graded.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""
|
|
2
|
+
The result of grading one answer.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
from typing import Any, NamedTuple
|
|
9
|
+
|
|
10
|
+
from rich.columns import Columns
|
|
11
|
+
from rich.console import RenderableType
|
|
12
|
+
from rich.text import Text
|
|
13
|
+
|
|
14
|
+
from .score import Score, is_score, rich_render_percent
|
|
15
|
+
|
|
16
|
+
__all__ = ["DEFAULT_TIMEOUT", "CaseResult", "Graded"]
|
|
17
|
+
|
|
18
|
+
DEFAULT_TIMEOUT = 30.0
|
|
19
|
+
try:
|
|
20
|
+
DEFAULT_TIMEOUT = float(os.environ.get("CHALK_AUTOGRADE_TIMEOUT", "30"))
|
|
21
|
+
except ValueError:
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class CaseResult(NamedTuple):
|
|
26
|
+
"""
|
|
27
|
+
The outcome of one expected test of a run.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
#: (classname, name) of the test in the JUnit report, or the case id of a
|
|
31
|
+
#: stdio report.
|
|
32
|
+
key: tuple[str, str]
|
|
33
|
+
#: The name shown in the feedback.
|
|
34
|
+
name: str
|
|
35
|
+
passed: bool
|
|
36
|
+
#: Why the test failed: its message, or "(not run)".
|
|
37
|
+
message: str | None = None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class Graded(NamedTuple):
|
|
41
|
+
#: A score or a disqualification (chalk_grader.disqualification, or a
|
|
42
|
+
#: richer type of the caller with the same interface).
|
|
43
|
+
score: Score | Any
|
|
44
|
+
feedback: str | None = None
|
|
45
|
+
#: Notes for the teacher, such as signs of tampering. Never shown to
|
|
46
|
+
#: the student.
|
|
47
|
+
notes: str | None = None
|
|
48
|
+
#: One entry per expected test, in the order of the reference run, when
|
|
49
|
+
#: the result came from a test report. None otherwise (a script, a build
|
|
50
|
+
#: error, a timeout).
|
|
51
|
+
cases: tuple[CaseResult, ...] | None = None
|
|
52
|
+
|
|
53
|
+
def __rich__(self) -> RenderableType:
|
|
54
|
+
parts: list[RenderableType] = []
|
|
55
|
+
if is_score(self.score):
|
|
56
|
+
parts.append(rich_render_percent(self.score))
|
|
57
|
+
else:
|
|
58
|
+
parts.append(self.score.__rich__())
|
|
59
|
+
if self.feedback:
|
|
60
|
+
parts.append(Text(f"({self.feedback})", style="dim"))
|
|
61
|
+
return Columns(parts) if len(parts) > 1 else parts[0]
|
chalk_grader/models.py
ADDED
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Configuration of an autograder: the `autograder` block of a question, the
|
|
3
|
+
profiles and their steps.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Annotated, Literal
|
|
10
|
+
|
|
11
|
+
from pydantic import BaseModel, ByteSize, ConfigDict, Field, model_validator
|
|
12
|
+
|
|
13
|
+
__all__ = ["Limits", "Stage", "ProfileStep", "PrepareStep", "Service", "Profile", "Autograder"]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Limits(BaseModel):
|
|
17
|
+
"""
|
|
18
|
+
Resource limits of an autograder run. Sizes accept units, as in "512MB"
|
|
19
|
+
or "1GiB".
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
memory: Annotated[
|
|
23
|
+
ByteSize | None,
|
|
24
|
+
Field(
|
|
25
|
+
description=(
|
|
26
|
+
"Address space limit of the script and each process it starts (RLIMIT_AS). "
|
|
27
|
+
"Runtimes that reserve large virtual areas, such as Node and the JVM, need "
|
|
28
|
+
"a generous value."
|
|
29
|
+
),
|
|
30
|
+
),
|
|
31
|
+
] = None
|
|
32
|
+
file_size: Annotated[
|
|
33
|
+
ByteSize | None,
|
|
34
|
+
Field(description="Largest file the script or its processes can write (RLIMIT_FSIZE)."),
|
|
35
|
+
] = None
|
|
36
|
+
answer_size: Annotated[
|
|
37
|
+
ByteSize,
|
|
38
|
+
Field(description="Largest text answer that is graded. Larger answers are rejected."),
|
|
39
|
+
] = ByteSize(1024 * 1024)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class Stage(BaseModel):
|
|
43
|
+
"""
|
|
44
|
+
Options of one step of a profile, overriding the autograder's.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
timeout: Annotated[
|
|
48
|
+
float | None,
|
|
49
|
+
Field(description="Timeout of the step, in seconds."),
|
|
50
|
+
] = None
|
|
51
|
+
limits: Annotated[
|
|
52
|
+
Limits | None,
|
|
53
|
+
Field(description="Resource limits of the step."),
|
|
54
|
+
] = None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class ProfileStep(Stage):
|
|
58
|
+
"""
|
|
59
|
+
A step of a profile that runs in the sandbox, once per student.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
model_config = ConfigDict(extra="forbid")
|
|
63
|
+
|
|
64
|
+
command: Annotated[
|
|
65
|
+
str,
|
|
66
|
+
Field(description="Shell command of the step, run in the working directory."),
|
|
67
|
+
]
|
|
68
|
+
seed: Annotated[
|
|
69
|
+
dict[str, str],
|
|
70
|
+
Field(
|
|
71
|
+
description=(
|
|
72
|
+
"Paths of the working directory filled with a copy of a path, usually in "
|
|
73
|
+
"the cache, before the step. The student's files are touched after the copy, "
|
|
74
|
+
"so build tools that compare modification times rebuild them."
|
|
75
|
+
),
|
|
76
|
+
),
|
|
77
|
+
] = {}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class PrepareStep(BaseModel):
|
|
81
|
+
"""
|
|
82
|
+
The trusted step of a profile, run once per cache with the network on.
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
model_config = ConfigDict(extra="forbid")
|
|
86
|
+
|
|
87
|
+
commands: Annotated[
|
|
88
|
+
list[str],
|
|
89
|
+
Field(description="Shell commands run in order, outside the sandbox."),
|
|
90
|
+
] = []
|
|
91
|
+
timeout: Annotated[
|
|
92
|
+
float | None,
|
|
93
|
+
Field(description="Timeout of each command, in seconds."),
|
|
94
|
+
] = None
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class Service(BaseModel):
|
|
98
|
+
"""
|
|
99
|
+
A program the student's code talks to through the network, such as an
|
|
100
|
+
HTTP server, started before the test step of each run in a sandbox of
|
|
101
|
+
its own.
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
model_config = ConfigDict(extra="forbid")
|
|
105
|
+
|
|
106
|
+
command: Annotated[
|
|
107
|
+
str,
|
|
108
|
+
Field(
|
|
109
|
+
description=(
|
|
110
|
+
"Shell command that starts the service, run in a copy of the template. It "
|
|
111
|
+
"listens on 127.0.0.1 at `port` and may write a JUnit report to "
|
|
112
|
+
"$CHALK_RESULT when it gets SIGTERM; its tests count in the grade."
|
|
113
|
+
),
|
|
114
|
+
),
|
|
115
|
+
]
|
|
116
|
+
port: Annotated[
|
|
117
|
+
int,
|
|
118
|
+
Field(ge=1, le=65535, description="TCP port of the service, the same for the student."),
|
|
119
|
+
]
|
|
120
|
+
env: Annotated[
|
|
121
|
+
dict[str, str],
|
|
122
|
+
Field(description="Environment variables of the service."),
|
|
123
|
+
] = {}
|
|
124
|
+
start_timeout: Annotated[
|
|
125
|
+
float,
|
|
126
|
+
Field(gt=0, description="Seconds the service has to open the port."),
|
|
127
|
+
] = 10
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class Profile(BaseModel):
|
|
131
|
+
"""
|
|
132
|
+
Grading recipe for one ecosystem, written as data: the commands of each
|
|
133
|
+
step, the environment, the files the template controls and how the
|
|
134
|
+
report is read. See docs/user-manual/autograding-scripts.md.
|
|
135
|
+
"""
|
|
136
|
+
|
|
137
|
+
model_config = ConfigDict(extra="forbid")
|
|
138
|
+
|
|
139
|
+
description: str = ""
|
|
140
|
+
extends: Annotated[
|
|
141
|
+
str | None,
|
|
142
|
+
Field(description="Profile whose keys this one inherits. Tables are merged."),
|
|
143
|
+
] = None
|
|
144
|
+
key: Annotated[
|
|
145
|
+
list[str],
|
|
146
|
+
Field(
|
|
147
|
+
description=(
|
|
148
|
+
"Template files the profile needs, such as lock files. Their contents, the "
|
|
149
|
+
"probes and the prepare options select the cache directory."
|
|
150
|
+
),
|
|
151
|
+
),
|
|
152
|
+
] = []
|
|
153
|
+
trusted: Annotated[
|
|
154
|
+
list[str],
|
|
155
|
+
Field(
|
|
156
|
+
description=(
|
|
157
|
+
"Files and directories of the template that replace the student's copies, "
|
|
158
|
+
"or are removed from the student's project when the template lacks them."
|
|
159
|
+
),
|
|
160
|
+
),
|
|
161
|
+
] = []
|
|
162
|
+
tests: Annotated[
|
|
163
|
+
list[str],
|
|
164
|
+
Field(description="Default test directories or files of the template."),
|
|
165
|
+
] = ["tests"]
|
|
166
|
+
probe: Annotated[
|
|
167
|
+
dict[str, str],
|
|
168
|
+
Field(
|
|
169
|
+
description=(
|
|
170
|
+
"Variables set from the output of shell commands run outside the sandbox, "
|
|
171
|
+
"such as the path of a toolchain."
|
|
172
|
+
),
|
|
173
|
+
),
|
|
174
|
+
] = {}
|
|
175
|
+
env: Annotated[
|
|
176
|
+
dict[str, str],
|
|
177
|
+
Field(description="Environment variables of every step."),
|
|
178
|
+
] = {}
|
|
179
|
+
unset: Annotated[
|
|
180
|
+
list[str],
|
|
181
|
+
Field(description="Environment variables removed in every step."),
|
|
182
|
+
] = []
|
|
183
|
+
readable: Annotated[
|
|
184
|
+
list[str],
|
|
185
|
+
Field(description="Paths the sandbox mounts read only, such as the cache."),
|
|
186
|
+
] = ["{cache}"]
|
|
187
|
+
hide: Annotated[
|
|
188
|
+
list[str],
|
|
189
|
+
Field(
|
|
190
|
+
description=(
|
|
191
|
+
"Glob patterns of files removed from the working directory before the "
|
|
192
|
+
"build and test steps, such as the expected outputs of the stdio profile."
|
|
193
|
+
),
|
|
194
|
+
),
|
|
195
|
+
] = []
|
|
196
|
+
vars: Annotated[
|
|
197
|
+
dict[str, str],
|
|
198
|
+
Field(description="Variables for the other keys. Questions override them with `vars`."),
|
|
199
|
+
] = {}
|
|
200
|
+
files: Annotated[
|
|
201
|
+
dict[str, str],
|
|
202
|
+
Field(description="Files written to the cache directory: name -> content."),
|
|
203
|
+
] = {}
|
|
204
|
+
prepare: PrepareStep = Field(default_factory=PrepareStep)
|
|
205
|
+
suspicious: Annotated[
|
|
206
|
+
dict[str, dict[str, str]],
|
|
207
|
+
Field(
|
|
208
|
+
description=(
|
|
209
|
+
"Signs of tampering searched in the student's files: glob -> {reason: regex}. "
|
|
210
|
+
"Matches become notes for the teacher; the score does not change."
|
|
211
|
+
),
|
|
212
|
+
),
|
|
213
|
+
] = {}
|
|
214
|
+
services: Annotated[
|
|
215
|
+
dict[str, Service],
|
|
216
|
+
Field(
|
|
217
|
+
description=(
|
|
218
|
+
"Services started for the test step, each in its own sandbox. The student's "
|
|
219
|
+
"code reaches them at 127.0.0.1 on their ports, and cannot see their files or "
|
|
220
|
+
"processes."
|
|
221
|
+
),
|
|
222
|
+
),
|
|
223
|
+
] = {}
|
|
224
|
+
build: ProfileStep | None = None
|
|
225
|
+
test: ProfileStep
|
|
226
|
+
report: Annotated[
|
|
227
|
+
Literal["junit", "stdio"],
|
|
228
|
+
Field(
|
|
229
|
+
description=(
|
|
230
|
+
"Format of the report the test step writes to $CHALK_RESULT: 'junit', or "
|
|
231
|
+
"'stdio' for the outputs of chalk's stdio runner, compared by chalk with the "
|
|
232
|
+
"template's .out files according to the `compare` variable."
|
|
233
|
+
),
|
|
234
|
+
),
|
|
235
|
+
] = "junit"
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
class Autograder(BaseModel):
|
|
239
|
+
answer: Annotated[
|
|
240
|
+
str,
|
|
241
|
+
Field(
|
|
242
|
+
description=(
|
|
243
|
+
"The path in the local grading repository in which the student's answer "
|
|
244
|
+
"is saved to before the grading process begins."
|
|
245
|
+
),
|
|
246
|
+
),
|
|
247
|
+
] = "answer.txt"
|
|
248
|
+
timeout: Annotated[
|
|
249
|
+
float | None,
|
|
250
|
+
Field(
|
|
251
|
+
description=(
|
|
252
|
+
"Default timeout (in seconds) for the autograder to run at each response. This is "
|
|
253
|
+
"applicable to potentially CPU intensive grading tasks such as running test cases "
|
|
254
|
+
"or executing student code."
|
|
255
|
+
),
|
|
256
|
+
),
|
|
257
|
+
] = None
|
|
258
|
+
syntax: Annotated[
|
|
259
|
+
bool | str | None,
|
|
260
|
+
Field(
|
|
261
|
+
description=(
|
|
262
|
+
"If true, the autograder will automatically apply syntax validators based on the "
|
|
263
|
+
"file extension of the answer or the given extension."
|
|
264
|
+
),
|
|
265
|
+
),
|
|
266
|
+
] = None
|
|
267
|
+
script: Annotated[
|
|
268
|
+
Path | None,
|
|
269
|
+
Field(
|
|
270
|
+
description=(
|
|
271
|
+
"The path to a script that should be executed as part of the autograding process. "
|
|
272
|
+
"The path is given relative to the question repository. Absolute paths are relative to the root of the chalk project."
|
|
273
|
+
),
|
|
274
|
+
),
|
|
275
|
+
] = None
|
|
276
|
+
template: Annotated[
|
|
277
|
+
Path | None,
|
|
278
|
+
Field(
|
|
279
|
+
description=(
|
|
280
|
+
"The path to a template file that should be used to generate the autograder script. "
|
|
281
|
+
"The path is given relative to the question repository. Absolute paths are relative to the root of the chalk project."
|
|
282
|
+
),
|
|
283
|
+
),
|
|
284
|
+
] = None
|
|
285
|
+
pass_env: Annotated[
|
|
286
|
+
list[str],
|
|
287
|
+
Field(
|
|
288
|
+
description=(
|
|
289
|
+
"Environment variables passed from chalk to the autograder script, besides "
|
|
290
|
+
"PATH, HOME, locale and a few others. Everything else is dropped."
|
|
291
|
+
),
|
|
292
|
+
),
|
|
293
|
+
] = []
|
|
294
|
+
profile: Annotated[
|
|
295
|
+
str | None,
|
|
296
|
+
Field(
|
|
297
|
+
description=(
|
|
298
|
+
"How the answer is graded. 'script' runs the autograder script; 'regex' "
|
|
299
|
+
"tests a regex answer against the examples in `match` and `reject`; "
|
|
300
|
+
"'python-pytest' runs the template tests with pytest; 'rust-cargo' builds "
|
|
301
|
+
"the crate and runs the template tests with cargo nextest. Other names refer "
|
|
302
|
+
"to the [profiles] tables of chalk.toml. By default, 'script' when the "
|
|
303
|
+
"question has a script."
|
|
304
|
+
),
|
|
305
|
+
),
|
|
306
|
+
] = None
|
|
307
|
+
sandbox: Annotated[
|
|
308
|
+
Literal["none", "bwrap", "podman"] | None,
|
|
309
|
+
Field(
|
|
310
|
+
description=(
|
|
311
|
+
"Isolation of the autograder run. Scripts run in 'bwrap' by default; "
|
|
312
|
+
"'none' is for scripts that only read the answer. 'podman' is not "
|
|
313
|
+
"implemented yet."
|
|
314
|
+
),
|
|
315
|
+
),
|
|
316
|
+
] = None
|
|
317
|
+
network: Annotated[
|
|
318
|
+
Literal["none", "host"],
|
|
319
|
+
Field(description="Network inside the sandbox: 'none' or the host network."),
|
|
320
|
+
] = "none"
|
|
321
|
+
mounts: Annotated[
|
|
322
|
+
list[Path],
|
|
323
|
+
Field(
|
|
324
|
+
description=(
|
|
325
|
+
"Paths the sandbox mounts read only, for files under hidden directories "
|
|
326
|
+
"such as the home directory. Relative paths start at the question directory."
|
|
327
|
+
),
|
|
328
|
+
),
|
|
329
|
+
] = []
|
|
330
|
+
limits: Limits = Field(default_factory=Limits)
|
|
331
|
+
match: Annotated[
|
|
332
|
+
list[str],
|
|
333
|
+
Field(description="regex profile: examples the answer must match."),
|
|
334
|
+
] = []
|
|
335
|
+
reject: Annotated[
|
|
336
|
+
list[str],
|
|
337
|
+
Field(description="regex profile: examples the answer must not match."),
|
|
338
|
+
] = []
|
|
339
|
+
regex_mode: Annotated[
|
|
340
|
+
Literal["full", "search"],
|
|
341
|
+
Field(
|
|
342
|
+
description=(
|
|
343
|
+
"regex profile: 'full' requires the regex to match the whole example, "
|
|
344
|
+
"'search' anywhere in it."
|
|
345
|
+
),
|
|
346
|
+
),
|
|
347
|
+
] = "full"
|
|
348
|
+
tests: Annotated[
|
|
349
|
+
list[str] | None,
|
|
350
|
+
Field(
|
|
351
|
+
description=(
|
|
352
|
+
"Profiles that run tests: test directories or files of the template, by "
|
|
353
|
+
"default the profile's. They replace the student's copies and are read only "
|
|
354
|
+
"during the run."
|
|
355
|
+
),
|
|
356
|
+
),
|
|
357
|
+
] = None
|
|
358
|
+
vars: Annotated[
|
|
359
|
+
dict[str, str],
|
|
360
|
+
Field(description="Profiles: values of the profile variables, such as `run` in stdio."),
|
|
361
|
+
] = {}
|
|
362
|
+
build: Annotated[
|
|
363
|
+
Stage,
|
|
364
|
+
Field(description="Profiles with a build step: timeout and limits of the build."),
|
|
365
|
+
] = Field(default_factory=Stage)
|
|
366
|
+
on_build_error: Annotated[
|
|
367
|
+
Literal["build-error", "zero", "manual"],
|
|
368
|
+
Field(
|
|
369
|
+
description=(
|
|
370
|
+
"Profiles with a build step: an answer that does not compile gets the 'build-error' "
|
|
371
|
+
"disqualification, a zero, or is left for manual grading. The compiler "
|
|
372
|
+
"output is the feedback."
|
|
373
|
+
),
|
|
374
|
+
),
|
|
375
|
+
] = "build-error"
|
|
376
|
+
command: Annotated[
|
|
377
|
+
str | None,
|
|
378
|
+
Field(
|
|
379
|
+
description=(
|
|
380
|
+
"The command to execute as part of the autograding process. This can be used "
|
|
381
|
+
"as an alternative to specifying an external script."
|
|
382
|
+
),
|
|
383
|
+
),
|
|
384
|
+
] = None
|
|
385
|
+
scoring: Annotated[
|
|
386
|
+
Literal["proportional", "all-or-nothing", "script"],
|
|
387
|
+
Field(
|
|
388
|
+
description=(
|
|
389
|
+
"Profiles with per-test results: 'proportional' scores passed / expected "
|
|
390
|
+
"tests, 'all-or-nothing' gives 1 when every expected test passed and 0 "
|
|
391
|
+
"otherwise, 'script' runs `scoring_script`."
|
|
392
|
+
),
|
|
393
|
+
),
|
|
394
|
+
] = "proportional"
|
|
395
|
+
scoring_script: Annotated[
|
|
396
|
+
Path | None,
|
|
397
|
+
Field(
|
|
398
|
+
description=(
|
|
399
|
+
"scoring 'script': teacher script, relative to the folder of the configuration. "
|
|
400
|
+
"It reads the test results as JSON from stdin and prints the score, a decimal "
|
|
401
|
+
"or n/d in [0, 1]. It runs outside the sandbox."
|
|
402
|
+
),
|
|
403
|
+
),
|
|
404
|
+
] = None
|
|
405
|
+
|
|
406
|
+
@model_validator(mode="after")
|
|
407
|
+
def _check_scoring(self) -> Autograder:
|
|
408
|
+
if (self.scoring == "script") != (self.scoring_script is not None):
|
|
409
|
+
raise ValueError("`scoring_script` goes together with `scoring: script`")
|
|
410
|
+
return self
|