Skip to content

Commit 228f327

Browse files
author
Nils Bars
committed
Document scored exercises and update submission_tests examples
Add a "Scored Exercises" section covering TestResult, scoring policies, check vs. submit behavior adaptation, and multi-task scored exercises. Update existing examples to use current decorator names (environment_test, submission_test) and task_name parameter instead of deprecated aliases.
1 parent 3b3be64 commit 228f327

1 file changed

Lines changed: 103 additions & 24 deletions

File tree

EXERCISES.md

Lines changed: 103 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -67,45 +67,32 @@ To ease your work and aid the students during solving the exercises, REF allows
6767
> [!NOTE]
6868
> Automated solution checking requires to set `submission-test: True` in `settings.yml`
6969
70-
The automated tests to run are described as a Python file called `submission_tests`. An exemplary file looks like this:
70+
The automated tests to run are described as a Python file called `submission_tests`. The simplest form returns a boolean indicating pass/fail:
7171

7272
```Python
7373
#!/usr/bin/env python3
7474

75-
# custom imports for this task
7675
from pathlib import Path
77-
from typing import List, Optional
78-
79-
import subprocess
80-
81-
82-
# REQUIRED IMPORTS
8376
import ref_utils as rf
84-
rf.ref_util_install_global_exception_hook()
85-
from ref_utils import print_ok, print_warn, print_err, assert_is_file, assert_is_exec, add_environment_test, add_submission_test, drop_privileges
86-
87-
8877

78+
rf.ref_util_install_global_exception_hook()
79+
from ref_utils import print_ok, print_err, assert_is_file, assert_is_exec, environment_test, submission_test
8980

9081
################################################################
9182

9283
TARGET_BIN = Path("/home/user/shellcode")
9384

94-
@add_environment_test() # type: ignore
85+
@environment_test() # type: ignore
9586
def test_environment() -> bool:
96-
"""
97-
Test whether all files that should be submitted are in place.
98-
"""
87+
"""Check whether all required files are in place."""
9988
tests_passed = True
10089
tests_passed &= assert_is_exec(TARGET_BIN)
10190
return tests_passed
10291

10392

104-
@add_submission_test() # type: ignore
93+
@submission_test() # type: ignore
10594
def test_submission() -> bool:
106-
"""
107-
Test if the submitted code successfully solves the exercise.
108-
"""
95+
"""Check if the submitted code successfully solves the exercise."""
10996
ret, out = rf.run_with_payload(['make', '-B'])
11097
if ret != 0:
11198
print_err(f'[!] Failed to build! {out}')
@@ -116,17 +103,109 @@ def test_submission() -> bool:
116103
return True
117104

118105
rf.run_tests()
106+
```
107+
108+
The Python file imports `ref_utils`, which provides two types of tests and various convenience functions.
109+
110+
Functions are converted into either an `environment test` or a `submission test` by using the respective decorator, which registers them. When testing a submission, first all environment tests are run. If one fails, testing is aborted (and the student informed about the failure). In the example above, the environment test checks whether an executable called `shellcode` exists. Once all environment tests pass, the submission test is executed. This two-stage design lets you first verify prerequisites (e.g., that specific binaries have been compiled) before checking whether their behavior matches the expected one.
111+
112+
When needed, both decorators accept an optional `task_name` argument (e.g., `@submission_test(task_name="part_one")`) by which specific tests can be grouped into independent tasks. A failure in task `"part_one"` will not abort the running of `"part_two"`. Each task can have multiple `@environment_test` functions but only one `@submission_test`.
119113

114+
Finally, `submission_tests` needs to call `rf.run_tests()` to execute all registered tests. To avoid leaking critical information (when hitting unexpected conditions in the submission tests themselves), `ref_utils` suppresses error output using `rf.ref_util_install_global_exception_hook()`. The `ref_utils` module provides various convenience functions, such as colored printing (`print_err`, `print_warn`, `print_ok`) or executing binaries with a specific payload (`rf.run_with_payload(..)`).
115+
116+
### Scored Exercises
117+
118+
The example above uses a boolean return value — the submission either passes or fails. For exercises that need a numeric score (e.g., code coverage percentage, number of tests passed, performance benchmarks), the `@submission_test` function can return a `TestResult` instead of a `bool`.
119+
120+
A `TestResult` carries two fields:
121+
122+
- `success` (`bool`) — whether the submission is considered successful.
123+
- `score` (`float | None`) — the numeric score achieved. This value is recorded per task and displayed on the scoreboard.
124+
125+
Here is a minimal scored example:
126+
127+
```Python
128+
#!/usr/bin/env python3
129+
130+
from pathlib import Path
131+
import ref_utils as rf
132+
133+
rf.ref_util_install_global_exception_hook()
134+
from ref_utils import (
135+
print_ok,
136+
print_err,
137+
assert_is_file,
138+
environment_test,
139+
submission_test,
140+
TestResult,
141+
)
142+
143+
################################################################
144+
145+
SO_PATH = Path("/home/user/libgenerator.so")
146+
147+
@environment_test() # type: ignore
148+
def test_environment() -> bool:
149+
return assert_is_file(SO_PATH) # type: ignore
150+
151+
152+
@submission_test() # type: ignore
153+
def test_submission() -> TestResult:
154+
coverage = run_coverage_measurement() # your scoring logic here
155+
print_ok(f"[+] You got {coverage:.02f}% coverage")
156+
return TestResult(success=True, score=coverage)
157+
158+
rf.run_tests()
120159
```
121160

122-
There's a lot going on, so let's dissect this step-by-step. The Python file needs to import ref_utils, which provides two types of tests and various convenience functions.
161+
The key differences from a pass/fail test:
123162

124-
Functions are converted into either an `environment test` or a `submission test` by using the respective decorator, which registers them. Conceptually, these two types are similar. When testing a submission, first all environment tests are run. If one fails, testing is aborted (and the user informed about the failure). In our example code above, the environment test merely checks whether the student created an executable called `shellcode`. Once all environment tests pass, the submission test(s) will be executed. This two-stage design enables to first test whether all prerequisites are in-place (for example, specific binaries have been compiled) via the environment tests before then checking whether their behavior matches the expected one via the submission tests. A failure in any test will abort the execution of subsequent ones.
163+
1. Import `TestResult` from `ref_utils`.
164+
2. Annotate the `@submission_test` function to return `TestResult` instead of `bool`.
165+
3. Return `TestResult(success=..., score=...)` where `score` is the raw numeric value.
125166

126-
When needed, both decorators accept an optional `group: str` argument (e.g., `@add_submission_test(group="task_part_one")`) by which specific tests can be grouped. Grouping allows to run multiple, independent test groups; in particular, a failure in test group "task_part_one" will not abort the running of "task_part_two".
167+
The raw score is stored in the database and shown on the scoreboard. Admins can optionally configure per-task **scoring policies** in the web interface to transform raw scores into final points. The available scoring modes are:
127168

169+
| Mode | Description | Parameters |
170+
|------|-------------|------------|
171+
| `none` (default) | Pass raw score through unchanged ||
172+
| `linear` | Linearly map a raw score range to points: `(raw - min_raw) / (max_raw - min_raw) * max_points`, clamped to `[0, max_points]` | `min_raw`, `max_raw`, `max_points` |
173+
| `threshold` | Award fixed points if raw score meets a threshold, otherwise 0 | `threshold`, `points` |
174+
| `tiered` | Multiple threshold tiers; the highest matching tier's points are awarded | `tiers` (list of `{above, points}`) |
175+
| `discard` | Omit the task from scoring entirely (contributes 0, hidden from breakdown) ||
176+
177+
#### Adapting behavior based on check vs. submit
178+
179+
Students can run `task check` (quick feedback loop) or `task submit` (final graded submission). Use `rf.test_result_will_be_submitted()` to detect which mode the test is running in and adjust accordingly (e.g., run a shorter measurement during check, full measurement during submit):
180+
181+
```Python
182+
@submission_test() # type: ignore
183+
def test_submission() -> TestResult:
184+
if rf.test_result_will_be_submitted():
185+
duration = 1800 # 30 minutes for final submission
186+
else:
187+
duration = 10 # quick check
188+
score = run_measurement(duration)
189+
return TestResult(success=True, score=score)
190+
```
191+
192+
#### Multi-task scored exercises
193+
194+
Exercises with multiple independently scored parts combine `task_name` with `TestResult`:
195+
196+
```Python
197+
@submission_test(task_name="correctness") # type: ignore
198+
def test_correctness() -> TestResult:
199+
passed = run_correctness_checks()
200+
return TestResult(success=passed > 0, score=passed)
201+
202+
@submission_test(task_name="performance") # type: ignore
203+
def test_performance() -> TestResult:
204+
throughput = measure_throughput()
205+
return TestResult(success=True, score=throughput)
206+
```
128207

129-
Finally, `submission_tests` needs to call `rf.run_tests()` to execute all registered tests. To avoid leaking critical information (when hitting unexpected conditions in the submission tests themselves), `ref_utils` suppresses error output using `rf.ref_util_install_global_exception_hook()`. The `ref_utils` module provides various convenience functions, such as colored printing (print_err, print_warn, print_ok) or executing binaries with a specific payload (rf.run_with_payload(..)).
208+
Each task produces its own `TestResult` and can have its own scoring policy configured in the admin interface. Tasks are independent — a failure in one does not affect the others.
130209

131210

132211

0 commit comments

Comments
 (0)