assignment-codeval 0.0.25__py3-none-any.whl → 0.0.26__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.
- assignment_codeval/evaluate.py +91 -12
- assignment_codeval/submissions.py +41 -3
- {assignment_codeval-0.0.25.dist-info → assignment_codeval-0.0.26.dist-info}/METADATA +1 -1
- {assignment_codeval-0.0.25.dist-info → assignment_codeval-0.0.26.dist-info}/RECORD +7 -7
- {assignment_codeval-0.0.25.dist-info → assignment_codeval-0.0.26.dist-info}/WHEEL +0 -0
- {assignment_codeval-0.0.25.dist-info → assignment_codeval-0.0.26.dist-info}/entry_points.txt +0 -0
- {assignment_codeval-0.0.25.dist-info → assignment_codeval-0.0.26.dist-info}/top_level.txt +0 -0
assignment_codeval/evaluate.py
CHANGED
|
@@ -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:
|
|
@@ -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)
|
|
@@ -6,16 +6,16 @@ assignment_codeval/cli.py,sha256=Vl27zNI7kI0QEZoXTIE2aTrraEJWj9vgJiyVWou3vgU,135
|
|
|
6
6
|
assignment_codeval/commons.py,sha256=rr09qHacHoLmmU8S9sBss4_utb0DH8-IyL9Dsj0ycE0,1412
|
|
7
7
|
assignment_codeval/convertMD2Html.py,sha256=4yu41U4Up1oGI5vDZGelmuogwFmEZrRS55M4ZHbRfaE,6243
|
|
8
8
|
assignment_codeval/create_assignment.py,sha256=oZ-0LadVb6R-rXVg0Lz5itziaalIvFoxd71cIxgMfow,10815
|
|
9
|
-
assignment_codeval/evaluate.py,sha256=
|
|
9
|
+
assignment_codeval/evaluate.py,sha256=er21lA-eohP1BfHPYPWRw9m0a_IOeljMNtcidOYacJY,36253
|
|
10
10
|
assignment_codeval/export_tests.py,sha256=IDAlGAp3zk-K5AFwLr85v4w9_WcYr6HXVF0joi4eYTg,5467
|
|
11
11
|
assignment_codeval/file_utils.py,sha256=RDgAgmyIOLKvRlxbAr-zILz2A24zSr-r3--p-dxl4G8,2132
|
|
12
12
|
assignment_codeval/github_connect.py,sha256=AP7mZPhzKKK8fxS-3JMfG6AQ-1wUutfsq-Bt-KG9WUs,4270
|
|
13
13
|
assignment_codeval/install_assignment.py,sha256=HsXzTex4G-U3t4tb-HyQ2AobBDBPcKeR6oAUDFqBP-A,4599
|
|
14
14
|
assignment_codeval/recent_comments.py,sha256=e8UQL2_9dsPlf-NYFrGSaM6h6XYnTf_MW2jWhtEsb38,9097
|
|
15
|
-
assignment_codeval/submissions.py,sha256=
|
|
15
|
+
assignment_codeval/submissions.py,sha256=YbwKynZwWsmcyytwRi3Kuxq3WN-V-vvshO9JrNJoxAs,28333
|
|
16
16
|
assignment_codeval/test_template.html,sha256=lgWsin5sYE_Pwjp3xqa7_UT-yZ4dWrVqAvNlSBBiNMI,2584
|
|
17
|
-
assignment_codeval-0.0.
|
|
18
|
-
assignment_codeval-0.0.
|
|
19
|
-
assignment_codeval-0.0.
|
|
20
|
-
assignment_codeval-0.0.
|
|
21
|
-
assignment_codeval-0.0.
|
|
17
|
+
assignment_codeval-0.0.26.dist-info/METADATA,sha256=CvKtHeb1kR3x4Sz0ojGIKui1v_Q1ejiColTIShfMOfg,15589
|
|
18
|
+
assignment_codeval-0.0.26.dist-info/WHEEL,sha256=YCfwYGOYMi5Jhw2fU4yNgwErybb2IX5PEwBKV4ZbdBo,91
|
|
19
|
+
assignment_codeval-0.0.26.dist-info/entry_points.txt,sha256=Pk0eAhsXBpCsgkv6HYLSHTRf5x_zUf-Lg8KK9KsK2B8,66
|
|
20
|
+
assignment_codeval-0.0.26.dist-info/top_level.txt,sha256=a_P8W2IWjAELxKlTV2t4rLwWVoTxElxUMvWgWjR_1n4,19
|
|
21
|
+
assignment_codeval-0.0.26.dist-info/RECORD,,
|
|
File without changes
|
{assignment_codeval-0.0.25.dist-info → assignment_codeval-0.0.26.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|