assignment-codeval 0.0.24__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.24 → assignment_codeval-0.0.26}/PKG-INFO +2 -1
- {assignment_codeval-0.0.24 → assignment_codeval-0.0.26}/README.md +1 -0
- {assignment_codeval-0.0.24 → assignment_codeval-0.0.26}/pyproject.toml +4 -1
- {assignment_codeval-0.0.24 → assignment_codeval-0.0.26}/src/assignment_codeval/cli.py +2 -0
- {assignment_codeval-0.0.24 → assignment_codeval-0.0.26}/src/assignment_codeval/evaluate.py +317 -51
- assignment_codeval-0.0.26/src/assignment_codeval/export_tests.py +187 -0
- {assignment_codeval-0.0.24 → assignment_codeval-0.0.26}/src/assignment_codeval/submissions.py +192 -20
- assignment_codeval-0.0.26/src/assignment_codeval/test_template.html +111 -0
- {assignment_codeval-0.0.24 → assignment_codeval-0.0.26}/src/assignment_codeval.egg-info/PKG-INFO +2 -1
- {assignment_codeval-0.0.24 → assignment_codeval-0.0.26}/src/assignment_codeval.egg-info/SOURCES.txt +4 -0
- assignment_codeval-0.0.26/tests/test_codeval.py +77 -0
- {assignment_codeval-0.0.24 → assignment_codeval-0.0.26}/tests/test_evaluate_submissions.py +154 -0
- assignment_codeval-0.0.26/tests/test_export_tests.py +172 -0
- assignment_codeval-0.0.26/tests/test_function_detection.py +195 -0
- assignment_codeval-0.0.24/tests/test_codeval.py +0 -35
- {assignment_codeval-0.0.24 → assignment_codeval-0.0.26}/setup.cfg +0 -0
- {assignment_codeval-0.0.24 → assignment_codeval-0.0.26}/src/assignment_codeval/__init__.py +0 -0
- {assignment_codeval-0.0.24 → assignment_codeval-0.0.26}/src/assignment_codeval/ai_benchmark.py +0 -0
- {assignment_codeval-0.0.24 → assignment_codeval-0.0.26}/src/assignment_codeval/canvas_utils.py +0 -0
- {assignment_codeval-0.0.24 → assignment_codeval-0.0.26}/src/assignment_codeval/check_grading.py +0 -0
- {assignment_codeval-0.0.24 → assignment_codeval-0.0.26}/src/assignment_codeval/commons.py +0 -0
- {assignment_codeval-0.0.24 → assignment_codeval-0.0.26}/src/assignment_codeval/convertMD2Html.py +0 -0
- {assignment_codeval-0.0.24 → assignment_codeval-0.0.26}/src/assignment_codeval/create_assignment.py +0 -0
- {assignment_codeval-0.0.24 → assignment_codeval-0.0.26}/src/assignment_codeval/file_utils.py +0 -0
- {assignment_codeval-0.0.24 → assignment_codeval-0.0.26}/src/assignment_codeval/github_connect.py +0 -0
- {assignment_codeval-0.0.24 → assignment_codeval-0.0.26}/src/assignment_codeval/install_assignment.py +0 -0
- {assignment_codeval-0.0.24 → assignment_codeval-0.0.26}/src/assignment_codeval/recent_comments.py +0 -0
- {assignment_codeval-0.0.24 → assignment_codeval-0.0.26}/src/assignment_codeval.egg-info/dependency_links.txt +0 -0
- {assignment_codeval-0.0.24 → assignment_codeval-0.0.26}/src/assignment_codeval.egg-info/entry_points.txt +0 -0
- {assignment_codeval-0.0.24 → assignment_codeval-0.0.26}/src/assignment_codeval.egg-info/requires.txt +0 -0
- {assignment_codeval-0.0.24 → assignment_codeval-0.0.26}/src/assignment_codeval.egg-info/top_level.txt +0 -0
- {assignment_codeval-0.0.24 → assignment_codeval-0.0.26}/tests/test_check_grading.py +0 -0
- {assignment_codeval-0.0.24 → assignment_codeval-0.0.26}/tests/test_create_assignment.py +0 -0
- {assignment_codeval-0.0.24 → assignment_codeval-0.0.26}/tests/test_install_assignment.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: assignment-codeval
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.26
|
|
4
4
|
Summary: CodEval for evaluating programming assignments
|
|
5
5
|
Requires-Python: >=3.12
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -69,6 +69,7 @@ Tags used in a spec file (\<course name>.codeval)
|
|
|
69
69
|
| CF | Check Function | Will be followed by a function name and a list of files to check to ensure that the function is used by one of those files. |
|
|
70
70
|
| CC | Check Container | Will be followed by a function name and a list of files to check to ensure that a container is used by one of those files. Primarily supports C++ containers such as std::vector |
|
|
71
71
|
| CO | Check Object | Will be followed by a function name and a list of files to check to ensure that an object is used by one of those files. Primarily support C++ stream operations |
|
|
72
|
+
| PRINT | Print Label | Prints a label/message to stdout. Cleaner alternative to `CMD echo "..."` for section labels. |
|
|
72
73
|
| CMD/TCMD | Run Command | Will be followed by a command to run. The TCMD will cause the evaluation to fail if the command exits with an error. |
|
|
73
74
|
| CMP | Compare | Will be followed by two files to compare. |
|
|
74
75
|
| T/HT | Test Case | Will be followed by the command to run to test the submission. |
|
|
@@ -49,6 +49,7 @@ Tags used in a spec file (\<course name>.codeval)
|
|
|
49
49
|
| CF | Check Function | Will be followed by a function name and a list of files to check to ensure that the function is used by one of those files. |
|
|
50
50
|
| CC | Check Container | Will be followed by a function name and a list of files to check to ensure that a container is used by one of those files. Primarily supports C++ containers such as std::vector |
|
|
51
51
|
| CO | Check Object | Will be followed by a function name and a list of files to check to ensure that an object is used by one of those files. Primarily support C++ stream operations |
|
|
52
|
+
| PRINT | Print Label | Prints a label/message to stdout. Cleaner alternative to `CMD echo "..."` for section labels. |
|
|
52
53
|
| CMD/TCMD | Run Command | Will be followed by a command to run. The TCMD will cause the evaluation to fail if the command exits with an error. |
|
|
53
54
|
| CMP | Compare | Will be followed by two files to compare. |
|
|
54
55
|
| T/HT | Test Case | Will be followed by the command to run to test the submission. |
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "assignment-codeval"
|
|
7
|
-
version = "0.0.
|
|
7
|
+
version = "0.0.26"
|
|
8
8
|
description = "CodEval for evaluating programming assignments"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.12"
|
|
@@ -27,3 +27,6 @@ test = ["pytest>=7.0"]
|
|
|
27
27
|
|
|
28
28
|
[project.scripts]
|
|
29
29
|
assignment-codeval = "assignment_codeval.cli:cli"
|
|
30
|
+
|
|
31
|
+
[tool.setuptools.package-data]
|
|
32
|
+
assignment_codeval = ["*.html"]
|
|
@@ -5,6 +5,7 @@ from assignment_codeval.check_grading import check_grading
|
|
|
5
5
|
from assignment_codeval.commons import set_config
|
|
6
6
|
from assignment_codeval.create_assignment import create_assignment
|
|
7
7
|
from assignment_codeval.evaluate import run_evaluation
|
|
8
|
+
from assignment_codeval.export_tests import export_tests
|
|
8
9
|
from assignment_codeval.github_connect import github_setup_repo
|
|
9
10
|
from assignment_codeval.install_assignment import install_assignment
|
|
10
11
|
from assignment_codeval.recent_comments import recent_comments
|
|
@@ -26,6 +27,7 @@ cli.add_command(list_codeval_assignments)
|
|
|
26
27
|
cli.add_command(install_assignment)
|
|
27
28
|
cli.add_command(recent_comments)
|
|
28
29
|
cli.add_command(check_grading)
|
|
30
|
+
cli.add_command(export_tests)
|
|
29
31
|
cli.add_command(get_benchmark_command())
|
|
30
32
|
|
|
31
33
|
if __name__ == "__main__":
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#! /usr/bin/python3
|
|
2
2
|
|
|
3
|
+
import ast
|
|
3
4
|
import os
|
|
4
5
|
import re
|
|
5
6
|
import subprocess
|
|
@@ -27,6 +28,7 @@ def get_testing_path(filename: str) -> str:
|
|
|
27
28
|
test_args = ""
|
|
28
29
|
cmps = []
|
|
29
30
|
timeout_val = 10
|
|
31
|
+
output_length_limit = 4096
|
|
30
32
|
expected_exit_code = -1
|
|
31
33
|
test_case_count = 0
|
|
32
34
|
test_case_hint = ""
|
|
@@ -80,52 +82,178 @@ def compile_code(compile_command):
|
|
|
80
82
|
sys.exit(1)
|
|
81
83
|
|
|
82
84
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
85
|
+
###########################################################
|
|
86
|
+
# Function detection helpers
|
|
87
|
+
###########################################################
|
|
86
88
|
|
|
87
|
-
Arguments:
|
|
88
|
-
function_name: the function name to check files for usage of
|
|
89
|
-
*files: the files to check for the function name
|
|
90
89
|
|
|
91
|
-
|
|
92
|
-
|
|
90
|
+
def _detect_language(files):
|
|
91
|
+
"""Return 'c_cpp', 'java', 'python', or 'unknown' based on file extensions."""
|
|
92
|
+
for f in files:
|
|
93
|
+
ext = os.path.splitext(f)[1].lower()
|
|
94
|
+
if ext in ('.c', '.cpp', '.cc', '.cxx', '.h', '.hpp'):
|
|
95
|
+
return 'c_cpp'
|
|
96
|
+
if ext == '.java':
|
|
97
|
+
return 'java'
|
|
98
|
+
if ext == '.py':
|
|
99
|
+
return 'python'
|
|
100
|
+
return 'unknown'
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _find_compiled_artifact(source_file):
|
|
104
|
+
"""Return the compiled artifact for a source file, or None if not found.
|
|
105
|
+
|
|
106
|
+
Checks (in order): same base name with .o, .exe, and no extension.
|
|
93
107
|
"""
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
108
|
+
base = os.path.splitext(source_file)[0]
|
|
109
|
+
for candidate in (base + '.o', base + '.exe', base):
|
|
110
|
+
if os.path.exists(candidate):
|
|
111
|
+
return candidate
|
|
112
|
+
return None
|
|
98
113
|
|
|
99
|
-
# Match:
|
|
100
|
-
# [1] start of line OR non-identifier character
|
|
101
|
-
# [2] function name
|
|
102
|
-
# [3] optional whitespace
|
|
103
|
-
# [4] opening parenthesis (function call)
|
|
104
|
-
regex = rf'(^|[^[:alnum:]_]){function_name}[[:space:]]*\('
|
|
105
114
|
|
|
106
|
-
|
|
115
|
+
def _function_used_in_c_cpp(function_name, files):
|
|
116
|
+
"""Use objdump on compiled artifacts to detect C/C++ function usage.
|
|
117
|
+
|
|
118
|
+
Returns True if found, False if artifact exists but function absent,
|
|
119
|
+
None if no compiled artifact was found (caller should fall back).
|
|
120
|
+
"""
|
|
121
|
+
found_artifact = False
|
|
122
|
+
for source_file in files:
|
|
123
|
+
artifact = _find_compiled_artifact(source_file)
|
|
124
|
+
if artifact is None:
|
|
125
|
+
continue
|
|
126
|
+
found_artifact = True
|
|
127
|
+
# objdump -t dumps the symbol table; pipe through c++filt to demangle C++ names.
|
|
128
|
+
# Mangled C++ names embed the original identifier, so grep -w finds exact matches.
|
|
129
|
+
cmd = f"objdump -t '{artifact}' | c++filt | grep -w '{function_name}'"
|
|
130
|
+
result = subprocess.run(
|
|
131
|
+
["bash", "-c", cmd],
|
|
132
|
+
stdout=subprocess.PIPE,
|
|
133
|
+
stderr=subprocess.PIPE,
|
|
134
|
+
)
|
|
135
|
+
if result.returncode == 0:
|
|
136
|
+
return True
|
|
137
|
+
if not found_artifact:
|
|
138
|
+
return None
|
|
139
|
+
return False
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _function_used_in_java(function_name, files):
|
|
143
|
+
"""Use javap on compiled .class files to detect Java method usage.
|
|
144
|
+
|
|
145
|
+
Returns True if found, False if class file exists but method absent,
|
|
146
|
+
None if no .class file was found (caller should fall back).
|
|
147
|
+
"""
|
|
148
|
+
found_class = False
|
|
149
|
+
for source_file in files:
|
|
150
|
+
base = os.path.splitext(source_file)[0]
|
|
151
|
+
class_file = base + '.class'
|
|
152
|
+
if not os.path.exists(class_file):
|
|
153
|
+
continue
|
|
154
|
+
found_class = True
|
|
155
|
+
# javap -c disassembles bytecode; method invocations reference the method name.
|
|
156
|
+
result = subprocess.run(
|
|
157
|
+
["javap", "-c", class_file],
|
|
158
|
+
stdout=subprocess.PIPE,
|
|
159
|
+
stderr=subprocess.PIPE,
|
|
160
|
+
)
|
|
161
|
+
if function_name.encode() in result.stdout:
|
|
162
|
+
return True
|
|
163
|
+
if not found_class:
|
|
164
|
+
return None
|
|
165
|
+
return False
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _function_used_in_python(function_name, files):
|
|
169
|
+
"""Use the ast module to detect function calls in Python source files.
|
|
170
|
+
|
|
171
|
+
Returns True if a matching call is found, False otherwise.
|
|
172
|
+
Handles both direct calls (func()) and attribute calls (obj.func()).
|
|
173
|
+
"""
|
|
174
|
+
for source_file in files:
|
|
175
|
+
try:
|
|
176
|
+
with open(source_file, 'r') as f:
|
|
177
|
+
source = f.read()
|
|
178
|
+
tree = ast.parse(source)
|
|
179
|
+
for node in ast.walk(tree):
|
|
180
|
+
if isinstance(node, ast.Call):
|
|
181
|
+
func = node.func
|
|
182
|
+
if isinstance(func, ast.Name) and func.id == function_name:
|
|
183
|
+
return True
|
|
184
|
+
if isinstance(func, ast.Attribute) and func.attr == function_name:
|
|
185
|
+
return True
|
|
186
|
+
except (SyntaxError, OSError):
|
|
187
|
+
continue
|
|
188
|
+
return False
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _function_used_regex(function_name, files):
|
|
192
|
+
"""Legacy regex-based fallback used when no compiled artifact is available."""
|
|
193
|
+
regex = rf'(^|[^[:alnum:]_]){function_name}[[:space:]]*\('
|
|
107
194
|
cmd = (
|
|
108
195
|
"sed -E "
|
|
109
196
|
"'"
|
|
110
|
-
"s://.*$::g; "
|
|
111
|
-
"s:#.*$::g; "
|
|
197
|
+
"s://.*$::g; "
|
|
198
|
+
"s:#.*$::g; "
|
|
112
199
|
":a; /\\/\\*/{N; s:/\\*.*?\\*/::g; ba}"
|
|
113
200
|
"' "
|
|
114
201
|
+ " ".join(files)
|
|
115
202
|
+ f" | grep -E '{regex}'"
|
|
116
203
|
)
|
|
117
|
-
|
|
118
|
-
function_popen = subprocess.Popen(
|
|
204
|
+
result = subprocess.run(
|
|
119
205
|
["bash", "-c", cmd],
|
|
120
206
|
stdout=subprocess.PIPE,
|
|
121
207
|
stderr=subprocess.PIPE,
|
|
122
208
|
)
|
|
209
|
+
return result.returncode == 0
|
|
123
210
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
211
|
+
|
|
212
|
+
def _is_function_used(function_name, files):
|
|
213
|
+
"""Detect whether function_name is used in the given files.
|
|
214
|
+
|
|
215
|
+
Dispatches to the appropriate tool based on file language:
|
|
216
|
+
- C/C++: objdump on compiled artifact (falls back to regex if none found)
|
|
217
|
+
- Java: javap on .class file (falls back to regex if none found)
|
|
218
|
+
- Python: ast module on source
|
|
219
|
+
- Other: regex on source
|
|
220
|
+
"""
|
|
221
|
+
lang = _detect_language(files)
|
|
222
|
+
if lang == 'python':
|
|
223
|
+
return _function_used_in_python(function_name, files)
|
|
224
|
+
if lang == 'java':
|
|
225
|
+
result = _function_used_in_java(function_name, files)
|
|
226
|
+
if result is None:
|
|
227
|
+
return _function_used_regex(function_name, files)
|
|
228
|
+
return result
|
|
229
|
+
if lang == 'c_cpp':
|
|
230
|
+
result = _function_used_in_c_cpp(function_name, files)
|
|
231
|
+
if result is None:
|
|
232
|
+
return _function_used_regex(function_name, files)
|
|
233
|
+
return result
|
|
234
|
+
return _function_used_regex(function_name, files)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def check_function(args):
|
|
238
|
+
"""Will be followed by a function name and a list of files to check to ensure that the function
|
|
239
|
+
is used by one of those files.
|
|
240
|
+
|
|
241
|
+
Arguments:
|
|
242
|
+
function_name: the function name to check files for usage of
|
|
243
|
+
*files: the files to check for the function name
|
|
244
|
+
|
|
245
|
+
Returns:
|
|
246
|
+
None
|
|
247
|
+
"""
|
|
248
|
+
check_test()
|
|
249
|
+
args = args.split()
|
|
250
|
+
function_name = args[0]
|
|
251
|
+
files = args[1:]
|
|
252
|
+
|
|
253
|
+
if _is_function_used(function_name, files):
|
|
128
254
|
print(f"Used {function_name} PASSED")
|
|
255
|
+
else:
|
|
256
|
+
print(f"Not using {function_name} FAILED")
|
|
129
257
|
|
|
130
258
|
def check_object(args):
|
|
131
259
|
"""Will be followed by an object and a list of files to ensure that the function is
|
|
@@ -228,7 +356,7 @@ def check_not_function(args):
|
|
|
228
356
|
is not used by any of those files.
|
|
229
357
|
|
|
230
358
|
Arguments:
|
|
231
|
-
function_name: the
|
|
359
|
+
function_name: the function name to check files for usage of
|
|
232
360
|
*files: the files to check for the function name
|
|
233
361
|
|
|
234
362
|
Returns:
|
|
@@ -239,18 +367,23 @@ def check_not_function(args):
|
|
|
239
367
|
function_name = args[0]
|
|
240
368
|
files = args[1:]
|
|
241
369
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
["grep", f"[^[:alpha:]]{function_name}[[:space:]]*("] + files,
|
|
245
|
-
stdout=subprocess.PIPE,
|
|
246
|
-
stderr=subprocess.PIPE,
|
|
247
|
-
)
|
|
248
|
-
|
|
249
|
-
function_popen.communicate()
|
|
250
|
-
if function_popen.returncode:
|
|
251
|
-
print(f"used{function_name} PASSED")
|
|
370
|
+
if _is_function_used(function_name, files):
|
|
371
|
+
print(f"Used {function_name} FAILED")
|
|
252
372
|
else:
|
|
253
|
-
print(f"
|
|
373
|
+
print(f"Not using {function_name} PASSED")
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def print_label(text):
|
|
377
|
+
"""Print a label/message to stdout.
|
|
378
|
+
|
|
379
|
+
Arguments:
|
|
380
|
+
text: the text to print
|
|
381
|
+
|
|
382
|
+
Returns:
|
|
383
|
+
None
|
|
384
|
+
"""
|
|
385
|
+
check_test()
|
|
386
|
+
print(text)
|
|
254
387
|
|
|
255
388
|
|
|
256
389
|
def run_command(command):
|
|
@@ -511,6 +644,19 @@ def timeout(timeout_sec):
|
|
|
511
644
|
timeout_val = float(timeout_sec)
|
|
512
645
|
|
|
513
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
|
+
|
|
514
660
|
def exit_code(test_case_exit_code):
|
|
515
661
|
"""Specifies the expected exit code for a test case. Defaults to zero.
|
|
516
662
|
|
|
@@ -578,6 +724,7 @@ tag_func_map = {
|
|
|
578
724
|
"CO" : check_object,
|
|
579
725
|
"CC": check_container,
|
|
580
726
|
"NCF": check_not_function,
|
|
727
|
+
"PRINT": print_label,
|
|
581
728
|
"CMD": run_command,
|
|
582
729
|
"TCMD": run_command_noerror,
|
|
583
730
|
"CMP": compare,
|
|
@@ -593,6 +740,7 @@ tag_func_map = {
|
|
|
593
740
|
"EB": check_error_bare,
|
|
594
741
|
"HINT": hint,
|
|
595
742
|
"TO": timeout,
|
|
743
|
+
"OLEN": output_length,
|
|
596
744
|
"X": exit_code,
|
|
597
745
|
"SS": start_server,
|
|
598
746
|
}
|
|
@@ -774,6 +922,93 @@ def parse_diff(diff_lines: list[str], testing_dir: str):
|
|
|
774
922
|
outfile.write(line)
|
|
775
923
|
|
|
776
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
|
+
|
|
991
|
+
def _find_touched_files(pre_run_timestamp):
|
|
992
|
+
"""Find files touched (modified or accessed) since pre_run_timestamp.
|
|
993
|
+
|
|
994
|
+
Recursively scans the current working directory, ignoring TESTING_DIR.
|
|
995
|
+
"""
|
|
996
|
+
touched = []
|
|
997
|
+
for dirpath, dirnames, filenames in os.walk("."):
|
|
998
|
+
# Prune TESTING_DIR from traversal
|
|
999
|
+
if TESTING_DIR in dirnames:
|
|
1000
|
+
dirnames.remove(TESTING_DIR)
|
|
1001
|
+
for filename in filenames:
|
|
1002
|
+
filepath = os.path.join(dirpath, filename)
|
|
1003
|
+
try:
|
|
1004
|
+
st = os.stat(filepath)
|
|
1005
|
+
if st.st_mtime > pre_run_timestamp or st.st_atime > pre_run_timestamp:
|
|
1006
|
+
touched.append(filepath)
|
|
1007
|
+
except OSError:
|
|
1008
|
+
continue
|
|
1009
|
+
return touched
|
|
1010
|
+
|
|
1011
|
+
|
|
777
1012
|
def check_test():
|
|
778
1013
|
global test_args
|
|
779
1014
|
if test_args == "":
|
|
@@ -782,6 +1017,9 @@ def check_test():
|
|
|
782
1017
|
print(f"Test case {test_case_count} of {test_case_total}")
|
|
783
1018
|
passed = True
|
|
784
1019
|
|
|
1020
|
+
# Record timestamp before running student code for file tracking
|
|
1021
|
+
pre_run_timestamp = time.time()
|
|
1022
|
+
|
|
785
1023
|
with open(get_testing_path("fileinput"), "rb") as fileinput, open(
|
|
786
1024
|
get_testing_path("youroutput"), "w"
|
|
787
1025
|
) as youroutput, open(get_testing_path("yourerror"), "w") as yourerror:
|
|
@@ -797,27 +1035,28 @@ def check_test():
|
|
|
797
1035
|
print(f"Took more than {timeout_val} seconds to run. FAIL")
|
|
798
1036
|
passed = False
|
|
799
1037
|
|
|
1038
|
+
# Find files touched by student's program
|
|
1039
|
+
touched_files = _find_touched_files(pre_run_timestamp)
|
|
1040
|
+
|
|
800
1041
|
# Difflog handling
|
|
801
1042
|
with open(get_testing_path("difflog"), "w") as outfile:
|
|
802
1043
|
diff_popen = subprocess.Popen(
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
stdout=
|
|
806
|
-
stderr=outfile,
|
|
807
|
-
text=True,
|
|
1044
|
+
["diff", "-U1", "-a",
|
|
1045
|
+
f"./{TESTING_DIR}/youroutput", f"./{TESTING_DIR}/expectedoutput"],
|
|
1046
|
+
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
|
808
1047
|
)
|
|
809
|
-
diff_popen.communicate()
|
|
1048
|
+
raw_output, _ = diff_popen.communicate()
|
|
1049
|
+
outfile.write(_render_diff_output(raw_output[:output_length_limit]))
|
|
810
1050
|
|
|
811
1051
|
# Append to difflog second time around
|
|
812
1052
|
with open(get_testing_path("difflog"), "a") as outfile:
|
|
813
1053
|
diff_popen = subprocess.Popen(
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
stdout=
|
|
817
|
-
stderr=outfile,
|
|
818
|
-
text=True,
|
|
1054
|
+
["diff", "-U1", "-a",
|
|
1055
|
+
f"./{TESTING_DIR}/yourerror", f"./{TESTING_DIR}/expectederror"],
|
|
1056
|
+
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
|
819
1057
|
)
|
|
820
|
-
diff_popen.communicate()
|
|
1058
|
+
raw_output, _ = diff_popen.communicate()
|
|
1059
|
+
outfile.write(_render_diff_output(raw_output[:output_length_limit]))
|
|
821
1060
|
|
|
822
1061
|
# Now read all the lines to accumulate both diffs
|
|
823
1062
|
with open(get_testing_path("difflog"), "r") as infile:
|
|
@@ -872,6 +1111,33 @@ def check_test():
|
|
|
872
1111
|
# Print entire file
|
|
873
1112
|
print("".join(file_lines))
|
|
874
1113
|
|
|
1114
|
+
# Show diffs for files touched by student's program
|
|
1115
|
+
if touched_files and cmps:
|
|
1116
|
+
expected_map = {}
|
|
1117
|
+
for file_pair in cmps:
|
|
1118
|
+
if len(file_pair) >= 2:
|
|
1119
|
+
expected_map[os.path.normpath(file_pair[0])] = file_pair[1]
|
|
1120
|
+
|
|
1121
|
+
for filepath in touched_files:
|
|
1122
|
+
normalized = os.path.normpath(filepath)
|
|
1123
|
+
if normalized in expected_map:
|
|
1124
|
+
expected_file = expected_map[normalized]
|
|
1125
|
+
if os.path.exists(expected_file):
|
|
1126
|
+
diff_proc = subprocess.Popen(
|
|
1127
|
+
["diff", "-U1", "-a", filepath, expected_file],
|
|
1128
|
+
stdout=subprocess.PIPE,
|
|
1129
|
+
stderr=subprocess.PIPE,
|
|
1130
|
+
text=True,
|
|
1131
|
+
)
|
|
1132
|
+
diff_output, _ = diff_proc.communicate()
|
|
1133
|
+
if diff_output:
|
|
1134
|
+
print(f" File output diff ({os.path.basename(filepath)}):")
|
|
1135
|
+
lines = diff_output.splitlines()
|
|
1136
|
+
for line in lines[:22]:
|
|
1137
|
+
print(f" {line}")
|
|
1138
|
+
if len(lines) > 22:
|
|
1139
|
+
print(f" ... ({len(lines) - 22} more lines)")
|
|
1140
|
+
|
|
875
1141
|
cleanup()
|
|
876
1142
|
|
|
877
1143
|
# Exit program after failed test case
|