assignment-codeval 0.0.25__tar.gz → 0.0.26__tar.gz
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.
- {assignment_codeval-0.0.25 → assignment_codeval-0.0.26}/PKG-INFO +1 -1
- {assignment_codeval-0.0.25 → assignment_codeval-0.0.26}/pyproject.toml +1 -1
- {assignment_codeval-0.0.25 → assignment_codeval-0.0.26}/src/assignment_codeval/evaluate.py +91 -12
- {assignment_codeval-0.0.25 → assignment_codeval-0.0.26}/src/assignment_codeval/submissions.py +41 -3
- {assignment_codeval-0.0.25 → assignment_codeval-0.0.26}/src/assignment_codeval.egg-info/PKG-INFO +1 -1
- assignment_codeval-0.0.26/tests/test_codeval.py +77 -0
- {assignment_codeval-0.0.25 → assignment_codeval-0.0.26}/tests/test_evaluate_submissions.py +154 -0
- assignment_codeval-0.0.25/tests/test_codeval.py +0 -35
- {assignment_codeval-0.0.25 → assignment_codeval-0.0.26}/README.md +0 -0
- {assignment_codeval-0.0.25 → assignment_codeval-0.0.26}/setup.cfg +0 -0
- {assignment_codeval-0.0.25 → assignment_codeval-0.0.26}/src/assignment_codeval/__init__.py +0 -0
- {assignment_codeval-0.0.25 → assignment_codeval-0.0.26}/src/assignment_codeval/ai_benchmark.py +0 -0
- {assignment_codeval-0.0.25 → assignment_codeval-0.0.26}/src/assignment_codeval/canvas_utils.py +0 -0
- {assignment_codeval-0.0.25 → assignment_codeval-0.0.26}/src/assignment_codeval/check_grading.py +0 -0
- {assignment_codeval-0.0.25 → assignment_codeval-0.0.26}/src/assignment_codeval/cli.py +0 -0
- {assignment_codeval-0.0.25 → assignment_codeval-0.0.26}/src/assignment_codeval/commons.py +0 -0
- {assignment_codeval-0.0.25 → assignment_codeval-0.0.26}/src/assignment_codeval/convertMD2Html.py +0 -0
- {assignment_codeval-0.0.25 → assignment_codeval-0.0.26}/src/assignment_codeval/create_assignment.py +0 -0
- {assignment_codeval-0.0.25 → assignment_codeval-0.0.26}/src/assignment_codeval/export_tests.py +0 -0
- {assignment_codeval-0.0.25 → assignment_codeval-0.0.26}/src/assignment_codeval/file_utils.py +0 -0
- {assignment_codeval-0.0.25 → assignment_codeval-0.0.26}/src/assignment_codeval/github_connect.py +0 -0
- {assignment_codeval-0.0.25 → assignment_codeval-0.0.26}/src/assignment_codeval/install_assignment.py +0 -0
- {assignment_codeval-0.0.25 → assignment_codeval-0.0.26}/src/assignment_codeval/recent_comments.py +0 -0
- {assignment_codeval-0.0.25 → assignment_codeval-0.0.26}/src/assignment_codeval/test_template.html +0 -0
- {assignment_codeval-0.0.25 → assignment_codeval-0.0.26}/src/assignment_codeval.egg-info/SOURCES.txt +0 -0
- {assignment_codeval-0.0.25 → assignment_codeval-0.0.26}/src/assignment_codeval.egg-info/dependency_links.txt +0 -0
- {assignment_codeval-0.0.25 → assignment_codeval-0.0.26}/src/assignment_codeval.egg-info/entry_points.txt +0 -0
- {assignment_codeval-0.0.25 → assignment_codeval-0.0.26}/src/assignment_codeval.egg-info/requires.txt +0 -0
- {assignment_codeval-0.0.25 → assignment_codeval-0.0.26}/src/assignment_codeval.egg-info/top_level.txt +0 -0
- {assignment_codeval-0.0.25 → assignment_codeval-0.0.26}/tests/test_check_grading.py +0 -0
- {assignment_codeval-0.0.25 → assignment_codeval-0.0.26}/tests/test_create_assignment.py +0 -0
- {assignment_codeval-0.0.25 → assignment_codeval-0.0.26}/tests/test_export_tests.py +0 -0
- {assignment_codeval-0.0.25 → assignment_codeval-0.0.26}/tests/test_function_detection.py +0 -0
- {assignment_codeval-0.0.25 → assignment_codeval-0.0.26}/tests/test_install_assignment.py +0 -0
|
@@ -28,6 +28,7 @@ def get_testing_path(filename: str) -> str:
|
|
|
28
28
|
test_args = ""
|
|
29
29
|
cmps = []
|
|
30
30
|
timeout_val = 10
|
|
31
|
+
output_length_limit = 4096
|
|
31
32
|
expected_exit_code = -1
|
|
32
33
|
test_case_count = 0
|
|
33
34
|
test_case_hint = ""
|
|
@@ -643,6 +644,19 @@ def timeout(timeout_sec):
|
|
|
643
644
|
timeout_val = float(timeout_sec)
|
|
644
645
|
|
|
645
646
|
|
|
647
|
+
def output_length(length):
|
|
648
|
+
"""Specifies the maximum number of bytes of diff output to render. Defaults to 4096.
|
|
649
|
+
|
|
650
|
+
Arguments:
|
|
651
|
+
length: maximum output length in bytes
|
|
652
|
+
|
|
653
|
+
Returns:
|
|
654
|
+
None
|
|
655
|
+
"""
|
|
656
|
+
global output_length_limit
|
|
657
|
+
output_length_limit = int(length)
|
|
658
|
+
|
|
659
|
+
|
|
646
660
|
def exit_code(test_case_exit_code):
|
|
647
661
|
"""Specifies the expected exit code for a test case. Defaults to zero.
|
|
648
662
|
|
|
@@ -726,6 +740,7 @@ tag_func_map = {
|
|
|
726
740
|
"EB": check_error_bare,
|
|
727
741
|
"HINT": hint,
|
|
728
742
|
"TO": timeout,
|
|
743
|
+
"OLEN": output_length,
|
|
729
744
|
"X": exit_code,
|
|
730
745
|
"SS": start_server,
|
|
731
746
|
}
|
|
@@ -907,6 +922,72 @@ def parse_diff(diff_lines: list[str], testing_dir: str):
|
|
|
907
922
|
outfile.write(line)
|
|
908
923
|
|
|
909
924
|
|
|
925
|
+
def _render_diff_output(raw_bytes: bytes) -> str:
|
|
926
|
+
"""Render raw bytes for diff output, making unprintable characters visible.
|
|
927
|
+
|
|
928
|
+
Replaces the shell pipeline `cat -te | head -22` with pure Python.
|
|
929
|
+
"""
|
|
930
|
+
result = []
|
|
931
|
+
i = 0
|
|
932
|
+
n = len(raw_bytes)
|
|
933
|
+
|
|
934
|
+
while i < n:
|
|
935
|
+
b = raw_bytes[i]
|
|
936
|
+
|
|
937
|
+
if b == 0x0A: # newline
|
|
938
|
+
result.append("$\n")
|
|
939
|
+
i += 1
|
|
940
|
+
elif b == 0x20 or (0x21 <= b <= 0x7E): # space or printable ASCII
|
|
941
|
+
result.append(chr(b))
|
|
942
|
+
i += 1
|
|
943
|
+
elif b <= 0x1F: # control chars (except newline handled above)
|
|
944
|
+
result.append("^" + chr(b + 0x40))
|
|
945
|
+
i += 1
|
|
946
|
+
elif b == 0x7F: # DEL
|
|
947
|
+
result.append("^?")
|
|
948
|
+
i += 1
|
|
949
|
+
elif b >= 0x80:
|
|
950
|
+
# Try to decode as UTF-8 multi-byte sequence
|
|
951
|
+
seq_len = 0
|
|
952
|
+
if (b & 0xE0) == 0xC0:
|
|
953
|
+
seq_len = 2
|
|
954
|
+
elif (b & 0xF0) == 0xE0:
|
|
955
|
+
seq_len = 3
|
|
956
|
+
elif (b & 0xF8) == 0xF0:
|
|
957
|
+
seq_len = 4
|
|
958
|
+
|
|
959
|
+
decoded_char = None
|
|
960
|
+
if seq_len >= 2 and i + seq_len <= n:
|
|
961
|
+
try:
|
|
962
|
+
decoded_char = raw_bytes[i:i + seq_len].decode("utf-8")
|
|
963
|
+
except UnicodeDecodeError:
|
|
964
|
+
pass
|
|
965
|
+
|
|
966
|
+
if decoded_char and len(decoded_char) == 1 and decoded_char.isprintable():
|
|
967
|
+
result.append(decoded_char)
|
|
968
|
+
i += seq_len
|
|
969
|
+
elif decoded_char and len(decoded_char) == 1:
|
|
970
|
+
# Valid UTF-8 but non-printable: render each byte as \xCC
|
|
971
|
+
for j in range(seq_len):
|
|
972
|
+
result.append(f"\\x{raw_bytes[i + j]:02X}")
|
|
973
|
+
i += seq_len
|
|
974
|
+
else:
|
|
975
|
+
# Invalid UTF-8 byte
|
|
976
|
+
result.append(f"\\x{b:02X}")
|
|
977
|
+
i += 1
|
|
978
|
+
else:
|
|
979
|
+
result.append(chr(b))
|
|
980
|
+
i += 1
|
|
981
|
+
|
|
982
|
+
output = "".join(result)
|
|
983
|
+
|
|
984
|
+
# If output doesn't end with \n, append $ at the very end
|
|
985
|
+
if output and not output.endswith("\n"):
|
|
986
|
+
output += "$"
|
|
987
|
+
|
|
988
|
+
return output
|
|
989
|
+
|
|
990
|
+
|
|
910
991
|
def _find_touched_files(pre_run_timestamp):
|
|
911
992
|
"""Find files touched (modified or accessed) since pre_run_timestamp.
|
|
912
993
|
|
|
@@ -960,24 +1041,22 @@ def check_test():
|
|
|
960
1041
|
# Difflog handling
|
|
961
1042
|
with open(get_testing_path("difflog"), "w") as outfile:
|
|
962
1043
|
diff_popen = subprocess.Popen(
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
stdout=
|
|
966
|
-
stderr=outfile,
|
|
967
|
-
text=True,
|
|
1044
|
+
["diff", "-U1", "-a",
|
|
1045
|
+
f"./{TESTING_DIR}/youroutput", f"./{TESTING_DIR}/expectedoutput"],
|
|
1046
|
+
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
|
968
1047
|
)
|
|
969
|
-
diff_popen.communicate()
|
|
1048
|
+
raw_output, _ = diff_popen.communicate()
|
|
1049
|
+
outfile.write(_render_diff_output(raw_output[:output_length_limit]))
|
|
970
1050
|
|
|
971
1051
|
# Append to difflog second time around
|
|
972
1052
|
with open(get_testing_path("difflog"), "a") as outfile:
|
|
973
1053
|
diff_popen = subprocess.Popen(
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
stdout=
|
|
977
|
-
stderr=outfile,
|
|
978
|
-
text=True,
|
|
1054
|
+
["diff", "-U1", "-a",
|
|
1055
|
+
f"./{TESTING_DIR}/yourerror", f"./{TESTING_DIR}/expectederror"],
|
|
1056
|
+
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
|
979
1057
|
)
|
|
980
|
-
diff_popen.communicate()
|
|
1058
|
+
raw_output, _ = diff_popen.communicate()
|
|
1059
|
+
outfile.write(_render_diff_output(raw_output[:output_length_limit]))
|
|
981
1060
|
|
|
982
1061
|
# Now read all the lines to accumulate both diffs
|
|
983
1062
|
with open(get_testing_path("difflog"), "r") as infile:
|
{assignment_codeval-0.0.25 → assignment_codeval-0.0.26}/src/assignment_codeval/submissions.py
RENAMED
|
@@ -180,6 +180,37 @@ def delete_submission_comment(canvas, course_id, assignment_id, user_id, comment
|
|
|
180
180
|
resp.raise_for_status()
|
|
181
181
|
|
|
182
182
|
|
|
183
|
+
def _parse_substitutions_file(filepath):
|
|
184
|
+
"""Parse a substitutions file where each line has the form /pattern/replacement/.
|
|
185
|
+
|
|
186
|
+
The first character of each line defines the delimiter. The delimiter must appear
|
|
187
|
+
exactly once in-between (separating pattern from replacement) and once at the end.
|
|
188
|
+
Returns a list of (pattern, replacement) tuples.
|
|
189
|
+
Raises click.ClickException on parse errors.
|
|
190
|
+
"""
|
|
191
|
+
substitutions = []
|
|
192
|
+
with open(filepath, "r") as fd:
|
|
193
|
+
for lineno, line in enumerate(fd, start=1):
|
|
194
|
+
line = line.rstrip("\n")
|
|
195
|
+
if not line:
|
|
196
|
+
continue
|
|
197
|
+
delim = line[0]
|
|
198
|
+
parts = line[1:].split(delim)
|
|
199
|
+
if len(parts) != 3 or parts[2] != "":
|
|
200
|
+
raise click.ClickException(
|
|
201
|
+
f"{filepath}:{lineno}: invalid substitution line: {line}"
|
|
202
|
+
)
|
|
203
|
+
substitutions.append((parts[0], parts[1]))
|
|
204
|
+
return substitutions
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _apply_substitutions(comment, substitutions):
|
|
208
|
+
"""Apply a list of (pattern, replacement) substitutions to comment text."""
|
|
209
|
+
for pattern, replacement in substitutions:
|
|
210
|
+
comment = comment.replace(pattern, replacement)
|
|
211
|
+
return comment
|
|
212
|
+
|
|
213
|
+
|
|
183
214
|
@click.command()
|
|
184
215
|
@click.argument("submissions_dir", metavar="SUBMISSIONS_DIR")
|
|
185
216
|
@click.option("--codeval-prefix", help="prefix for codeval comments", default="codeval: ", show_default=True)
|
|
@@ -214,8 +245,12 @@ def upload_submission_comments(submissions_dir, codeval_prefix, delete):
|
|
|
214
245
|
write_html_file(dirpath)
|
|
215
246
|
file_id = upload_file_for_comment(canvas, course.id, assignment.id, student_id, f"{dirpath}/results.html")
|
|
216
247
|
with open(f"{dirpath}/comments.txt", "r") as fd:
|
|
217
|
-
comment = fd.read()
|
|
218
|
-
comment = comment.replace("\0", "\\0").strip()
|
|
248
|
+
comment = fd.read(4096)
|
|
249
|
+
comment = comment.replace("\0", "\\0").strip().replace("<", "<")
|
|
250
|
+
subs_file = f"{dirpath}/SUBSTITUTIONS.txt"
|
|
251
|
+
if os.path.isfile(subs_file):
|
|
252
|
+
substitutions = _parse_substitutions_file(subs_file)
|
|
253
|
+
comment = _apply_substitutions(comment, substitutions)
|
|
219
254
|
if delete:
|
|
220
255
|
all_comments = sorted(submission.submission_comments, key=lambda c: c['created_at'])
|
|
221
256
|
if all_comments:
|
|
@@ -226,7 +261,7 @@ def upload_submission_comments(submissions_dir, codeval_prefix, delete):
|
|
|
226
261
|
f'{canvas_url}/api/v1/courses/{course.id}/assignments/{assignment.id}/submissions/{student_id}',
|
|
227
262
|
headers={'Authorization': f'Bearer {canvas_token}'},
|
|
228
263
|
data={
|
|
229
|
-
'comment[text_comment]': f'{codeval_prefix.rstrip()}\n{comment}',
|
|
264
|
+
'comment[text_comment]': f'{codeval_prefix.rstrip()}\n<pre>{comment}</pre>',
|
|
230
265
|
'comment[file_ids][]': file_id,
|
|
231
266
|
}
|
|
232
267
|
).raise_for_status()
|
|
@@ -312,6 +347,9 @@ def evaluate_submissions(codeval_dir, submissions_dir):
|
|
|
312
347
|
if not move_to_next_submission:
|
|
313
348
|
for zf_name in zip_files:
|
|
314
349
|
with ZipFile(os.path.join(codeval_dir, zf_name)) as zf:
|
|
350
|
+
if "SUBSTITUTIONS.txt" in zf.namelist():
|
|
351
|
+
with open(os.path.join(dirpath, "SUBSTITUTIONS.txt"), "wb") as out_f:
|
|
352
|
+
out_f.write(zf.read("SUBSTITUTIONS.txt"))
|
|
315
353
|
for f in zf.infolist():
|
|
316
354
|
dest_dir = os.path.join(submission_dir, assignment_working_dir)
|
|
317
355
|
zf.extract(f, dest_dir)
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Pytest tests for CodEval test files."""
|
|
2
|
+
import glob
|
|
3
|
+
import os
|
|
4
|
+
import subprocess
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from assignment_codeval.evaluate import _render_diff_output
|
|
8
|
+
|
|
9
|
+
# Get all .codeval test files
|
|
10
|
+
TEST_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
11
|
+
CODEVAL_FILES = sorted(glob.glob(os.path.join(TEST_DIR, "*.codeval")))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_test_ids():
|
|
15
|
+
"""Generate test IDs from filenames."""
|
|
16
|
+
return [os.path.basename(f).replace(".codeval", "") for f in CODEVAL_FILES]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@pytest.mark.parametrize("codeval_file", CODEVAL_FILES, ids=get_test_ids())
|
|
20
|
+
def test_codeval(codeval_file):
|
|
21
|
+
"""Run a .codeval test file and verify it passes."""
|
|
22
|
+
test_name = os.path.basename(codeval_file)
|
|
23
|
+
|
|
24
|
+
result = subprocess.run(
|
|
25
|
+
["assignment-codeval", "run-evaluation", test_name],
|
|
26
|
+
cwd=TEST_DIR,
|
|
27
|
+
capture_output=True,
|
|
28
|
+
text=True,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
# Print output for debugging
|
|
32
|
+
if result.stdout:
|
|
33
|
+
print(result.stdout)
|
|
34
|
+
if result.stderr:
|
|
35
|
+
print(result.stderr)
|
|
36
|
+
|
|
37
|
+
assert result.returncode == 0, f"Test {test_name} failed with exit code {result.returncode}"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class TestRenderDiffOutput:
|
|
41
|
+
def test_plain_ascii(self):
|
|
42
|
+
assert _render_diff_output(b"hello\n") == "hello$\n"
|
|
43
|
+
|
|
44
|
+
def test_multiple_lines(self):
|
|
45
|
+
assert _render_diff_output(b"a\nb\n") == "a$\nb$\n"
|
|
46
|
+
|
|
47
|
+
def test_no_trailing_newline(self):
|
|
48
|
+
assert _render_diff_output(b"hello") == "hello$"
|
|
49
|
+
|
|
50
|
+
def test_tab_rendered_as_caret_i(self):
|
|
51
|
+
assert _render_diff_output(b"a\tb\n") == "a^Ib$\n"
|
|
52
|
+
|
|
53
|
+
def test_control_chars(self):
|
|
54
|
+
assert _render_diff_output(b"\x01\x02\x03\n") == "^A^B^C$\n"
|
|
55
|
+
|
|
56
|
+
def test_null_byte(self):
|
|
57
|
+
assert _render_diff_output(b"\x00\n") == "^@$\n"
|
|
58
|
+
|
|
59
|
+
def test_del_byte(self):
|
|
60
|
+
assert _render_diff_output(b"\x7f\n") == "^?$\n"
|
|
61
|
+
|
|
62
|
+
def test_invalid_utf8_bytes(self):
|
|
63
|
+
assert _render_diff_output(b"\x80\xff\n") == "\\x80\\xFF$\n"
|
|
64
|
+
|
|
65
|
+
def test_valid_utf8_printable(self):
|
|
66
|
+
# é is U+00E9, encoded as 0xC3 0xA9
|
|
67
|
+
assert _render_diff_output("é\n".encode("utf-8")) == "é$\n"
|
|
68
|
+
|
|
69
|
+
def test_mixed_content(self):
|
|
70
|
+
raw = b"ok\t\x01\x80end\n"
|
|
71
|
+
assert _render_diff_output(raw) == "ok^I^A\\x80end$\n"
|
|
72
|
+
|
|
73
|
+
def test_empty_input(self):
|
|
74
|
+
assert _render_diff_output(b"") == ""
|
|
75
|
+
|
|
76
|
+
def test_spaces_preserved(self):
|
|
77
|
+
assert _render_diff_output(b"a b\n") == "a b$\n"
|
|
@@ -4,8 +4,11 @@ import os
|
|
|
4
4
|
import tempfile
|
|
5
5
|
import zipfile
|
|
6
6
|
|
|
7
|
+
import click
|
|
7
8
|
import pytest
|
|
8
9
|
|
|
10
|
+
from assignment_codeval.submissions import _parse_substitutions_file, _apply_substitutions
|
|
11
|
+
|
|
9
12
|
|
|
10
13
|
class TestGitHubSubmissionWorkingDir:
|
|
11
14
|
"""Tests for GitHub submission working directory detection."""
|
|
@@ -156,3 +159,154 @@ class TestGitHubSubmissionWorkingDir:
|
|
|
156
159
|
correct_path = os.path.join(submission_dir, "custom_dir", "helper.txt")
|
|
157
160
|
assert os.path.exists(correct_path), \
|
|
158
161
|
f"helper.txt should be at {correct_path}"
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class TestSubstitutionsFromZip:
|
|
165
|
+
"""Tests for copying SUBSTITUTIONS.txt from zip to comments.txt directory."""
|
|
166
|
+
|
|
167
|
+
def _setup_submission(self, tmpdir, assignment_name, zip_contents):
|
|
168
|
+
"""Helper to create a submission directory with a zip file."""
|
|
169
|
+
codeval_dir = os.path.join(tmpdir, "codeval")
|
|
170
|
+
submissions_dir = os.path.join(tmpdir, "submissions")
|
|
171
|
+
os.makedirs(codeval_dir)
|
|
172
|
+
|
|
173
|
+
student_dir = os.path.join(submissions_dir, "Course", assignment_name, "12345")
|
|
174
|
+
submission_dir = os.path.join(student_dir, "submission")
|
|
175
|
+
os.makedirs(submission_dir)
|
|
176
|
+
|
|
177
|
+
# Create zip
|
|
178
|
+
zip_path = os.path.join(codeval_dir, "support.zip")
|
|
179
|
+
with zipfile.ZipFile(zip_path, 'w') as zf:
|
|
180
|
+
for name, content in zip_contents.items():
|
|
181
|
+
zf.writestr(name, content)
|
|
182
|
+
|
|
183
|
+
# Create codeval file
|
|
184
|
+
codeval_path = os.path.join(codeval_dir, f"{assignment_name}.codeval")
|
|
185
|
+
with open(codeval_path, "w") as f:
|
|
186
|
+
f.write("Z support.zip\n")
|
|
187
|
+
f.write("T echo test\n")
|
|
188
|
+
f.write("O test\n")
|
|
189
|
+
|
|
190
|
+
return codeval_dir, student_dir, submission_dir
|
|
191
|
+
|
|
192
|
+
def test_substitutions_copied_to_dirpath(self):
|
|
193
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
194
|
+
codeval_dir, student_dir, submission_dir = self._setup_submission(
|
|
195
|
+
tmpdir, "TestAssignment",
|
|
196
|
+
{"SUBSTITUTIONS.txt": "/foo/bar/\n", "helper.txt": "data"}
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
# Simulate the zip extraction logic from evaluate_submissions
|
|
200
|
+
from zipfile import ZipFile
|
|
201
|
+
codeval_file = os.path.join(codeval_dir, "TestAssignment.codeval")
|
|
202
|
+
zip_files = ["support.zip"]
|
|
203
|
+
|
|
204
|
+
for zf_name in zip_files:
|
|
205
|
+
with ZipFile(os.path.join(codeval_dir, zf_name)) as zf:
|
|
206
|
+
if "SUBSTITUTIONS.txt" in zf.namelist():
|
|
207
|
+
with open(os.path.join(student_dir, "SUBSTITUTIONS.txt"), "wb") as out_f:
|
|
208
|
+
out_f.write(zf.read("SUBSTITUTIONS.txt"))
|
|
209
|
+
|
|
210
|
+
subs_path = os.path.join(student_dir, "SUBSTITUTIONS.txt")
|
|
211
|
+
assert os.path.exists(subs_path)
|
|
212
|
+
with open(subs_path) as f:
|
|
213
|
+
assert f.read() == "/foo/bar/\n"
|
|
214
|
+
|
|
215
|
+
def test_no_substitutions_in_zip(self):
|
|
216
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
217
|
+
codeval_dir, student_dir, submission_dir = self._setup_submission(
|
|
218
|
+
tmpdir, "TestAssignment",
|
|
219
|
+
{"helper.txt": "data"}
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
from zipfile import ZipFile
|
|
223
|
+
zip_files = ["support.zip"]
|
|
224
|
+
|
|
225
|
+
for zf_name in zip_files:
|
|
226
|
+
with ZipFile(os.path.join(codeval_dir, zf_name)) as zf:
|
|
227
|
+
if "SUBSTITUTIONS.txt" in zf.namelist():
|
|
228
|
+
with open(os.path.join(student_dir, "SUBSTITUTIONS.txt"), "wb") as out_f:
|
|
229
|
+
out_f.write(zf.read("SUBSTITUTIONS.txt"))
|
|
230
|
+
|
|
231
|
+
assert not os.path.exists(os.path.join(student_dir, "SUBSTITUTIONS.txt"))
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
class TestParseSubstitutionsFile:
|
|
235
|
+
def test_basic_substitution(self, tmp_path):
|
|
236
|
+
f = tmp_path / "SUBSTITUTIONS.txt"
|
|
237
|
+
f.write_text("/foo/bar/\n")
|
|
238
|
+
result = _parse_substitutions_file(str(f))
|
|
239
|
+
assert result == [("foo", "bar")]
|
|
240
|
+
|
|
241
|
+
def test_custom_delimiter(self, tmp_path):
|
|
242
|
+
f = tmp_path / "SUBSTITUTIONS.txt"
|
|
243
|
+
f.write_text("|foo|bar|\n")
|
|
244
|
+
result = _parse_substitutions_file(str(f))
|
|
245
|
+
assert result == [("foo", "bar")]
|
|
246
|
+
|
|
247
|
+
def test_multiple_lines(self, tmp_path):
|
|
248
|
+
f = tmp_path / "SUBSTITUTIONS.txt"
|
|
249
|
+
f.write_text("/foo/bar/\n#baz#qux#\n")
|
|
250
|
+
result = _parse_substitutions_file(str(f))
|
|
251
|
+
assert result == [("foo", "bar"), ("baz", "qux")]
|
|
252
|
+
|
|
253
|
+
def test_empty_replacement(self, tmp_path):
|
|
254
|
+
f = tmp_path / "SUBSTITUTIONS.txt"
|
|
255
|
+
f.write_text("/foo//\n")
|
|
256
|
+
result = _parse_substitutions_file(str(f))
|
|
257
|
+
assert result == [("foo", "")]
|
|
258
|
+
|
|
259
|
+
def test_blank_lines_skipped(self, tmp_path):
|
|
260
|
+
f = tmp_path / "SUBSTITUTIONS.txt"
|
|
261
|
+
f.write_text("\n/foo/bar/\n\n")
|
|
262
|
+
result = _parse_substitutions_file(str(f))
|
|
263
|
+
assert result == [("foo", "bar")]
|
|
264
|
+
|
|
265
|
+
def test_invalid_missing_trailing_delimiter(self, tmp_path):
|
|
266
|
+
f = tmp_path / "SUBSTITUTIONS.txt"
|
|
267
|
+
f.write_text("/foo/bar\n")
|
|
268
|
+
with pytest.raises(click.ClickException, match=":1:"):
|
|
269
|
+
_parse_substitutions_file(str(f))
|
|
270
|
+
|
|
271
|
+
def test_invalid_too_many_delimiters(self, tmp_path):
|
|
272
|
+
f = tmp_path / "SUBSTITUTIONS.txt"
|
|
273
|
+
f.write_text("/foo/bar/baz/\n")
|
|
274
|
+
with pytest.raises(click.ClickException, match=":1:"):
|
|
275
|
+
_parse_substitutions_file(str(f))
|
|
276
|
+
|
|
277
|
+
def test_invalid_no_delimiter_in_between(self, tmp_path):
|
|
278
|
+
f = tmp_path / "SUBSTITUTIONS.txt"
|
|
279
|
+
f.write_text("/foobar/\n")
|
|
280
|
+
with pytest.raises(click.ClickException, match=":1:"):
|
|
281
|
+
_parse_substitutions_file(str(f))
|
|
282
|
+
|
|
283
|
+
def test_error_reports_correct_line_number(self, tmp_path):
|
|
284
|
+
f = tmp_path / "SUBSTITUTIONS.txt"
|
|
285
|
+
f.write_text("/foo/bar/\nbad line\n")
|
|
286
|
+
with pytest.raises(click.ClickException, match=":2:"):
|
|
287
|
+
_parse_substitutions_file(str(f))
|
|
288
|
+
|
|
289
|
+
def test_delimiter_in_pattern_via_different_delimiter(self, tmp_path):
|
|
290
|
+
f = tmp_path / "SUBSTITUTIONS.txt"
|
|
291
|
+
f.write_text("|a/b|c/d|\n")
|
|
292
|
+
result = _parse_substitutions_file(str(f))
|
|
293
|
+
assert result == [("a/b", "c/d")]
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
class TestApplySubstitutions:
|
|
297
|
+
def test_single_substitution(self):
|
|
298
|
+
assert _apply_substitutions("hello foo world", [("foo", "bar")]) == "hello bar world"
|
|
299
|
+
|
|
300
|
+
def test_multiple_substitutions(self):
|
|
301
|
+
result = _apply_substitutions("aaa bbb", [("aaa", "xxx"), ("bbb", "yyy")])
|
|
302
|
+
assert result == "xxx yyy"
|
|
303
|
+
|
|
304
|
+
def test_literal_pattern(self):
|
|
305
|
+
result = _apply_substitutions("a^[[1mb", [("^[[1m", "<b>B</b>")])
|
|
306
|
+
assert result == "a<b>B</b>b"
|
|
307
|
+
|
|
308
|
+
def test_empty_substitutions(self):
|
|
309
|
+
assert _apply_substitutions("hello", []) == "hello"
|
|
310
|
+
|
|
311
|
+
def test_deletion(self):
|
|
312
|
+
assert _apply_substitutions("hello world", [("world", "")]) == "hello "
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
"""Pytest tests for CodEval test files."""
|
|
2
|
-
import glob
|
|
3
|
-
import os
|
|
4
|
-
import subprocess
|
|
5
|
-
import pytest
|
|
6
|
-
|
|
7
|
-
# Get all .codeval test files
|
|
8
|
-
TEST_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
9
|
-
CODEVAL_FILES = sorted(glob.glob(os.path.join(TEST_DIR, "*.codeval")))
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def get_test_ids():
|
|
13
|
-
"""Generate test IDs from filenames."""
|
|
14
|
-
return [os.path.basename(f).replace(".codeval", "") for f in CODEVAL_FILES]
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
@pytest.mark.parametrize("codeval_file", CODEVAL_FILES, ids=get_test_ids())
|
|
18
|
-
def test_codeval(codeval_file):
|
|
19
|
-
"""Run a .codeval test file and verify it passes."""
|
|
20
|
-
test_name = os.path.basename(codeval_file)
|
|
21
|
-
|
|
22
|
-
result = subprocess.run(
|
|
23
|
-
["assignment-codeval", "run-evaluation", test_name],
|
|
24
|
-
cwd=TEST_DIR,
|
|
25
|
-
capture_output=True,
|
|
26
|
-
text=True,
|
|
27
|
-
)
|
|
28
|
-
|
|
29
|
-
# Print output for debugging
|
|
30
|
-
if result.stdout:
|
|
31
|
-
print(result.stdout)
|
|
32
|
-
if result.stderr:
|
|
33
|
-
print(result.stderr)
|
|
34
|
-
|
|
35
|
-
assert result.returncode == 0, f"Test {test_name} failed with exit code {result.returncode}"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{assignment_codeval-0.0.25 → assignment_codeval-0.0.26}/src/assignment_codeval/ai_benchmark.py
RENAMED
|
File without changes
|
{assignment_codeval-0.0.25 → assignment_codeval-0.0.26}/src/assignment_codeval/canvas_utils.py
RENAMED
|
File without changes
|
{assignment_codeval-0.0.25 → assignment_codeval-0.0.26}/src/assignment_codeval/check_grading.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{assignment_codeval-0.0.25 → assignment_codeval-0.0.26}/src/assignment_codeval/convertMD2Html.py
RENAMED
|
File without changes
|
{assignment_codeval-0.0.25 → assignment_codeval-0.0.26}/src/assignment_codeval/create_assignment.py
RENAMED
|
File without changes
|
{assignment_codeval-0.0.25 → assignment_codeval-0.0.26}/src/assignment_codeval/export_tests.py
RENAMED
|
File without changes
|
{assignment_codeval-0.0.25 → assignment_codeval-0.0.26}/src/assignment_codeval/file_utils.py
RENAMED
|
File without changes
|
{assignment_codeval-0.0.25 → assignment_codeval-0.0.26}/src/assignment_codeval/github_connect.py
RENAMED
|
File without changes
|
{assignment_codeval-0.0.25 → assignment_codeval-0.0.26}/src/assignment_codeval/install_assignment.py
RENAMED
|
File without changes
|
{assignment_codeval-0.0.25 → assignment_codeval-0.0.26}/src/assignment_codeval/recent_comments.py
RENAMED
|
File without changes
|
{assignment_codeval-0.0.25 → assignment_codeval-0.0.26}/src/assignment_codeval/test_template.html
RENAMED
|
File without changes
|
{assignment_codeval-0.0.25 → assignment_codeval-0.0.26}/src/assignment_codeval.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{assignment_codeval-0.0.25 → assignment_codeval-0.0.26}/src/assignment_codeval.egg-info/requires.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|