assignment-codeval 0.0.29__tar.gz → 0.0.30__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.29 → assignment_codeval-0.0.30}/PKG-INFO +4 -2
- {assignment_codeval-0.0.29 → assignment_codeval-0.0.30}/README.md +3 -1
- {assignment_codeval-0.0.29 → assignment_codeval-0.0.30}/pyproject.toml +1 -1
- {assignment_codeval-0.0.29 → assignment_codeval-0.0.30}/src/assignment_codeval/canvas_utils.py +1 -1
- {assignment_codeval-0.0.29 → assignment_codeval-0.0.30}/src/assignment_codeval/evaluate.py +178 -25
- {assignment_codeval-0.0.29 → assignment_codeval-0.0.30}/src/assignment_codeval/submissions.py +12 -3
- {assignment_codeval-0.0.29 → assignment_codeval-0.0.30}/src/assignment_codeval.egg-info/PKG-INFO +4 -2
- {assignment_codeval-0.0.29 → assignment_codeval-0.0.30}/tests/test_codeval.py +30 -0
- assignment_codeval-0.0.30/tests/test_function_detection.py +411 -0
- assignment_codeval-0.0.29/tests/test_function_detection.py +0 -195
- {assignment_codeval-0.0.29 → assignment_codeval-0.0.30}/setup.cfg +0 -0
- {assignment_codeval-0.0.29 → assignment_codeval-0.0.30}/src/assignment_codeval/__init__.py +0 -0
- {assignment_codeval-0.0.29 → assignment_codeval-0.0.30}/src/assignment_codeval/ai_benchmark.py +0 -0
- {assignment_codeval-0.0.29 → assignment_codeval-0.0.30}/src/assignment_codeval/check_grading.py +0 -0
- {assignment_codeval-0.0.29 → assignment_codeval-0.0.30}/src/assignment_codeval/cli.py +0 -0
- {assignment_codeval-0.0.29 → assignment_codeval-0.0.30}/src/assignment_codeval/commons.py +0 -0
- {assignment_codeval-0.0.29 → assignment_codeval-0.0.30}/src/assignment_codeval/convertMD2Html.py +0 -0
- {assignment_codeval-0.0.29 → assignment_codeval-0.0.30}/src/assignment_codeval/create_assignment.py +0 -0
- {assignment_codeval-0.0.29 → assignment_codeval-0.0.30}/src/assignment_codeval/export_tests.py +0 -0
- {assignment_codeval-0.0.29 → assignment_codeval-0.0.30}/src/assignment_codeval/file_utils.py +0 -0
- {assignment_codeval-0.0.29 → assignment_codeval-0.0.30}/src/assignment_codeval/github_connect.py +0 -0
- {assignment_codeval-0.0.29 → assignment_codeval-0.0.30}/src/assignment_codeval/install_assignment.py +0 -0
- {assignment_codeval-0.0.29 → assignment_codeval-0.0.30}/src/assignment_codeval/recent_comments.py +0 -0
- {assignment_codeval-0.0.29 → assignment_codeval-0.0.30}/src/assignment_codeval/test_template.html +0 -0
- {assignment_codeval-0.0.29 → assignment_codeval-0.0.30}/src/assignment_codeval.egg-info/SOURCES.txt +0 -0
- {assignment_codeval-0.0.29 → assignment_codeval-0.0.30}/src/assignment_codeval.egg-info/dependency_links.txt +0 -0
- {assignment_codeval-0.0.29 → assignment_codeval-0.0.30}/src/assignment_codeval.egg-info/entry_points.txt +0 -0
- {assignment_codeval-0.0.29 → assignment_codeval-0.0.30}/src/assignment_codeval.egg-info/requires.txt +0 -0
- {assignment_codeval-0.0.29 → assignment_codeval-0.0.30}/src/assignment_codeval.egg-info/top_level.txt +0 -0
- {assignment_codeval-0.0.29 → assignment_codeval-0.0.30}/tests/test_check_grading.py +0 -0
- {assignment_codeval-0.0.29 → assignment_codeval-0.0.30}/tests/test_create_assignment.py +0 -0
- {assignment_codeval-0.0.29 → assignment_codeval-0.0.30}/tests/test_evaluate_submissions.py +0 -0
- {assignment_codeval-0.0.29 → assignment_codeval-0.0.30}/tests/test_export_tests.py +0 -0
- {assignment_codeval-0.0.29 → assignment_codeval-0.0.30}/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.30
|
|
4
4
|
Summary: CodEval for evaluating programming assignments
|
|
5
5
|
Requires-Python: >=3.12
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -69,7 +69,8 @@ Tags used in a spec file (\<course name>.codeval)
|
|
|
69
69
|
| CTO | Compile Timeout | Timeout in seconds for the compile command to run |
|
|
70
70
|
| RUN | Run Script | Specifies the script to use to evaluate the specification file. Defaults to evaluate.sh. |
|
|
71
71
|
| Z | Download Zip | Will be followed by zip files to download from Canvas to use when running the test cases. |
|
|
72
|
-
| CF | Check Function |
|
|
72
|
+
| CF | Check Function | `CF <function_name> [filename]` — checks that the function is used. The filename is **optional** and omitting it (e.g. `CF strtol`) is the preferred usage; source files are inferred from the most recent `C` tag and the check uses compiled-artifact inspection only (objdump/javap/ast). Providing a filename (e.g. `CF strtol mycalc.c`) is supported for backwards compatibility and additionally enables a regex fallback when no compiled artifact is found. |
|
|
73
|
+
| NCF | Check Not Function | `NCF <function_name> [filename]` — checks that the function is **not** used. The filename is **optional** (same behaviour as CF). |
|
|
73
74
|
| 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 |
|
|
74
75
|
| 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 |
|
|
75
76
|
| PRINT | Print Label | Prints a label/message to stdout. Cleaner alternative to `CMD echo "..."` for section labels. |
|
|
@@ -82,6 +83,7 @@ Tags used in a spec file (\<course name>.codeval)
|
|
|
82
83
|
| TO | Timeout | Specifies the time limit in seconds for a test case to run. Defaults to 20 seconds. |
|
|
83
84
|
| X | Exit Code | Specifies the expected exit code for a test case. Defaults to zero. |
|
|
84
85
|
| SS | Start Server | Command containing timeout (wait until server starts), kill timeout (wait to kill the server), and the command to start a server |
|
|
86
|
+
| TEMP | Temp File | Registers a file to be deleted before the next T, HT, or TCMD test runs (clean state) and again after it completes (cleanup). Only applies to the immediately following test — use a new TEMP tag for each test that needs it. |
|
|
85
87
|
|
|
86
88
|
Refer to a sample spec file [here](samples/assignment-name.codeval)
|
|
87
89
|
|
|
@@ -49,7 +49,8 @@ Tags used in a spec file (\<course name>.codeval)
|
|
|
49
49
|
| CTO | Compile Timeout | Timeout in seconds for the compile command to run |
|
|
50
50
|
| RUN | Run Script | Specifies the script to use to evaluate the specification file. Defaults to evaluate.sh. |
|
|
51
51
|
| Z | Download Zip | Will be followed by zip files to download from Canvas to use when running the test cases. |
|
|
52
|
-
| CF | Check Function |
|
|
52
|
+
| CF | Check Function | `CF <function_name> [filename]` — checks that the function is used. The filename is **optional** and omitting it (e.g. `CF strtol`) is the preferred usage; source files are inferred from the most recent `C` tag and the check uses compiled-artifact inspection only (objdump/javap/ast). Providing a filename (e.g. `CF strtol mycalc.c`) is supported for backwards compatibility and additionally enables a regex fallback when no compiled artifact is found. |
|
|
53
|
+
| NCF | Check Not Function | `NCF <function_name> [filename]` — checks that the function is **not** used. The filename is **optional** (same behaviour as CF). |
|
|
53
54
|
| 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 |
|
|
54
55
|
| 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 |
|
|
55
56
|
| PRINT | Print Label | Prints a label/message to stdout. Cleaner alternative to `CMD echo "..."` for section labels. |
|
|
@@ -62,6 +63,7 @@ Tags used in a spec file (\<course name>.codeval)
|
|
|
62
63
|
| TO | Timeout | Specifies the time limit in seconds for a test case to run. Defaults to 20 seconds. |
|
|
63
64
|
| X | Exit Code | Specifies the expected exit code for a test case. Defaults to zero. |
|
|
64
65
|
| SS | Start Server | Command containing timeout (wait until server starts), kill timeout (wait to kill the server), and the command to start a server |
|
|
66
|
+
| TEMP | Temp File | Registers a file to be deleted before the next T, HT, or TCMD test runs (clean state) and again after it completes (cleanup). Only applies to the immediately following test — use a new TEMP tag for each test that needs it. |
|
|
65
67
|
|
|
66
68
|
Refer to a sample spec file [here](samples/assignment-name.codeval)
|
|
67
69
|
|
{assignment_codeval-0.0.29 → assignment_codeval-0.0.30}/src/assignment_codeval/canvas_utils.py
RENAMED
|
@@ -98,7 +98,7 @@ def get_assignment(course, assignment_name):
|
|
|
98
98
|
error(fr" {a.name}")
|
|
99
99
|
sys.exit(2)
|
|
100
100
|
elif len(assignments) > 1:
|
|
101
|
-
strict_name_assignments = [a for a in assignments if a.name == assignment_name]
|
|
101
|
+
strict_name_assignments = [a for a in assignments if despace(a.name) == assignment_name]
|
|
102
102
|
if len(strict_name_assignments) == 1:
|
|
103
103
|
assignments = strict_name_assignments
|
|
104
104
|
else:
|
|
@@ -38,11 +38,23 @@ num_failed = 0
|
|
|
38
38
|
is_hidden_testcase = False
|
|
39
39
|
is_verbose = False
|
|
40
40
|
compilelog = []
|
|
41
|
+
last_compile_command = ""
|
|
42
|
+
temp_files = []
|
|
43
|
+
_active_temp_files = []
|
|
41
44
|
|
|
42
45
|
###########################################################
|
|
43
46
|
# Specification Tags to Function Mapping
|
|
44
47
|
###########################################################
|
|
45
48
|
|
|
49
|
+
# Shell commands that likely indicate a missing CMD prefix when found untagged
|
|
50
|
+
_BARE_SHELL_COMMANDS = {
|
|
51
|
+
'echo', 'rm', 'cp', 'mv', 'mkdir', 'rmdir', 'cat', 'ls', 'grep',
|
|
52
|
+
'sed', 'awk', 'find', 'chmod', 'chown', 'touch', 'ln', 'diff',
|
|
53
|
+
'sort', 'head', 'tail', 'cut', 'tr', 'wc', 'bash', 'sh', 'python',
|
|
54
|
+
'python3', 'make', 'export', 'source', 'kill', 'pkill', 'sleep',
|
|
55
|
+
'printf', 'read', 'unzip', 'tar', 'curl', 'wget',
|
|
56
|
+
}
|
|
57
|
+
|
|
46
58
|
|
|
47
59
|
def compile_code(compile_command):
|
|
48
60
|
"""Specifies the command to compile the submission code
|
|
@@ -53,6 +65,9 @@ def compile_code(compile_command):
|
|
|
53
65
|
Returns:
|
|
54
66
|
None
|
|
55
67
|
"""
|
|
68
|
+
global last_compile_command
|
|
69
|
+
last_compile_command = compile_command
|
|
70
|
+
|
|
56
71
|
if test_case_count != 0:
|
|
57
72
|
check_test()
|
|
58
73
|
|
|
@@ -112,6 +127,44 @@ def _find_compiled_artifact(source_file):
|
|
|
112
127
|
return None
|
|
113
128
|
|
|
114
129
|
|
|
130
|
+
def _extract_executable_from_compile(compile_command):
|
|
131
|
+
"""Extract the output executable name from a compile command (looks for -o <name>).
|
|
132
|
+
|
|
133
|
+
Returns the executable name, or None if not found.
|
|
134
|
+
"""
|
|
135
|
+
parts = compile_command.split()
|
|
136
|
+
for i, part in enumerate(parts):
|
|
137
|
+
if part == '-o' and i + 1 < len(parts):
|
|
138
|
+
return parts[i + 1]
|
|
139
|
+
return None
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _extract_source_files_from_compile(compile_command):
|
|
143
|
+
"""Extract source file arguments from a compile command.
|
|
144
|
+
|
|
145
|
+
Recognises common source file extensions (.c, .cpp, .cc, .cxx, .java, .py)
|
|
146
|
+
and skips option arguments that consume the following token as a value
|
|
147
|
+
(e.g. -o, -I, -L, -l, -include, -isystem, -MF, -MT, -MQ).
|
|
148
|
+
|
|
149
|
+
Returns a list of source file paths found in the command.
|
|
150
|
+
"""
|
|
151
|
+
source_exts = ('.c', '.cpp', '.cc', '.cxx', '.java', '.py')
|
|
152
|
+
value_flags = {'-o', '-I', '-L', '-l', '-include', '-isystem', '-MF', '-MT', '-MQ'}
|
|
153
|
+
parts = compile_command.split()
|
|
154
|
+
files = []
|
|
155
|
+
skip_next = False
|
|
156
|
+
for part in parts:
|
|
157
|
+
if skip_next:
|
|
158
|
+
skip_next = False
|
|
159
|
+
continue
|
|
160
|
+
if part in value_flags:
|
|
161
|
+
skip_next = True
|
|
162
|
+
continue
|
|
163
|
+
if not part.startswith('-') and any(part.endswith(ext) for ext in source_exts):
|
|
164
|
+
files.append(part)
|
|
165
|
+
return files
|
|
166
|
+
|
|
167
|
+
|
|
115
168
|
def _function_used_in_c_cpp(function_name, files):
|
|
116
169
|
"""Use objdump on compiled artifacts to detect C/C++ function usage.
|
|
117
170
|
|
|
@@ -209,14 +262,20 @@ def _function_used_regex(function_name, files):
|
|
|
209
262
|
return result.returncode == 0
|
|
210
263
|
|
|
211
264
|
|
|
212
|
-
def _is_function_used(function_name, files):
|
|
265
|
+
def _is_function_used(function_name, files, allow_regex_fallback=True):
|
|
213
266
|
"""Detect whether function_name is used in the given files.
|
|
214
267
|
|
|
215
268
|
Dispatches to the appropriate tool based on file language:
|
|
216
|
-
- C/C++: objdump on compiled artifact (falls back to regex if none found
|
|
217
|
-
|
|
269
|
+
- C/C++: objdump on compiled artifact (falls back to regex if none found
|
|
270
|
+
and allow_regex_fallback is True)
|
|
271
|
+
- Java: javap on .class file (falls back to regex if none found
|
|
272
|
+
and allow_regex_fallback is True)
|
|
218
273
|
- Python: ast module on source
|
|
219
|
-
- Other: regex on source
|
|
274
|
+
- Other: regex on source (only when allow_regex_fallback is True)
|
|
275
|
+
|
|
276
|
+
When allow_regex_fallback is False (used when no filename was given in the
|
|
277
|
+
CF tag), the function relies solely on compiled-artifact inspection and
|
|
278
|
+
returns False rather than falling back to regex.
|
|
220
279
|
"""
|
|
221
280
|
lang = _detect_language(files)
|
|
222
281
|
if lang == 'python':
|
|
@@ -224,36 +283,56 @@ def _is_function_used(function_name, files):
|
|
|
224
283
|
if lang == 'java':
|
|
225
284
|
result = _function_used_in_java(function_name, files)
|
|
226
285
|
if result is None:
|
|
227
|
-
return _function_used_regex(function_name, files)
|
|
286
|
+
return _function_used_regex(function_name, files) if allow_regex_fallback else False
|
|
228
287
|
return result
|
|
229
288
|
if lang == 'c_cpp':
|
|
230
289
|
result = _function_used_in_c_cpp(function_name, files)
|
|
231
290
|
if result is None:
|
|
232
|
-
return _function_used_regex(function_name, files)
|
|
291
|
+
return _function_used_regex(function_name, files) if allow_regex_fallback else False
|
|
233
292
|
return result
|
|
234
|
-
|
|
293
|
+
if allow_regex_fallback:
|
|
294
|
+
return _function_used_regex(function_name, files)
|
|
295
|
+
return False
|
|
235
296
|
|
|
236
297
|
|
|
237
298
|
def check_function(args):
|
|
238
|
-
"""Will be followed by a function name and
|
|
239
|
-
is used by one of those files.
|
|
299
|
+
"""Will be followed by a function name and an optional list of files to check to ensure that
|
|
300
|
+
the function is used by one of those files.
|
|
301
|
+
|
|
302
|
+
When no filename is provided (preferred usage), the source files are derived from the most
|
|
303
|
+
recent C (compile) tag. The check then relies solely on compiled-artifact inspection
|
|
304
|
+
(objdump for C/C++, javap for Java, ast for Python) with no regex fallback.
|
|
305
|
+
|
|
306
|
+
When a filename is provided (legacy usage), the same compiled-artifact inspection is used
|
|
307
|
+
first, but a regex scan of the source file is available as a fallback when no compiled
|
|
308
|
+
artifact can be found.
|
|
240
309
|
|
|
241
310
|
Arguments:
|
|
242
311
|
function_name: the function name to check files for usage of
|
|
243
|
-
*files: the files to check for the function name
|
|
312
|
+
*files: (optional) the files to check for the function name. If omitted, source
|
|
313
|
+
files are inferred from the most recent C tag.
|
|
244
314
|
|
|
245
315
|
Returns:
|
|
246
316
|
None
|
|
247
317
|
"""
|
|
248
318
|
check_test()
|
|
249
|
-
|
|
250
|
-
function_name =
|
|
251
|
-
files =
|
|
252
|
-
|
|
253
|
-
if
|
|
254
|
-
|
|
319
|
+
args_list = args.split()
|
|
320
|
+
function_name = args_list[0]
|
|
321
|
+
files = args_list[1:]
|
|
322
|
+
|
|
323
|
+
if not files:
|
|
324
|
+
# No filename provided — derive source files from the compile command and
|
|
325
|
+
# disable the regex fallback (compiled-artifact check only).
|
|
326
|
+
files = _extract_source_files_from_compile(last_compile_command)
|
|
327
|
+
if _is_function_used(function_name, files, allow_regex_fallback=False):
|
|
328
|
+
print(f"Used {function_name} PASSED")
|
|
329
|
+
else:
|
|
330
|
+
print(f"Not using {function_name} FAILED")
|
|
255
331
|
else:
|
|
256
|
-
|
|
332
|
+
if _is_function_used(function_name, files):
|
|
333
|
+
print(f"Used {function_name} PASSED")
|
|
334
|
+
else:
|
|
335
|
+
print(f"Not using {function_name} FAILED")
|
|
257
336
|
|
|
258
337
|
def check_object(args):
|
|
259
338
|
"""Will be followed by an object and a list of files to ensure that the function is
|
|
@@ -352,22 +431,38 @@ def check_container(args):
|
|
|
352
431
|
|
|
353
432
|
|
|
354
433
|
def check_not_function(args):
|
|
355
|
-
"""Will be followed by a function name and
|
|
356
|
-
is not used by any of those files.
|
|
434
|
+
"""Will be followed by a function name and an optional list of files to check to ensure that
|
|
435
|
+
the function is not used by any of those files.
|
|
436
|
+
|
|
437
|
+
When no filename is provided (preferred usage), the source files are derived from the most
|
|
438
|
+
recent C (compile) tag. The check relies solely on compiled-artifact inspection with no
|
|
439
|
+
regex fallback.
|
|
440
|
+
|
|
441
|
+
When a filename is provided (legacy usage), compiled-artifact inspection is used first with
|
|
442
|
+
a regex fallback when no compiled artifact is found.
|
|
357
443
|
|
|
358
444
|
Arguments:
|
|
359
445
|
function_name: the function name to check files for usage of
|
|
360
|
-
*files: the files to check for the function name
|
|
446
|
+
*files: (optional) the files to check for the function name. If omitted, source
|
|
447
|
+
files are inferred from the most recent C tag.
|
|
361
448
|
|
|
362
449
|
Returns:
|
|
363
450
|
None
|
|
364
451
|
"""
|
|
365
452
|
check_test()
|
|
366
|
-
|
|
367
|
-
function_name =
|
|
368
|
-
files =
|
|
453
|
+
args_list = args.split()
|
|
454
|
+
function_name = args_list[0]
|
|
455
|
+
files = args_list[1:]
|
|
456
|
+
|
|
457
|
+
if not files:
|
|
458
|
+
# No filename provided — derive source files from the compile command and
|
|
459
|
+
# disable the regex fallback (compiled-artifact check only).
|
|
460
|
+
files = _extract_source_files_from_compile(last_compile_command)
|
|
461
|
+
used = _is_function_used(function_name, files, allow_regex_fallback=False)
|
|
462
|
+
else:
|
|
463
|
+
used = _is_function_used(function_name, files)
|
|
369
464
|
|
|
370
|
-
if
|
|
465
|
+
if used:
|
|
371
466
|
print(f"Used {function_name} FAILED")
|
|
372
467
|
else:
|
|
373
468
|
print(f"Not using {function_name} PASSED")
|
|
@@ -412,6 +507,7 @@ def run_command_noerror(command):
|
|
|
412
507
|
None
|
|
413
508
|
"""
|
|
414
509
|
check_test()
|
|
510
|
+
_pre_test_temp_cleanup()
|
|
415
511
|
|
|
416
512
|
# Run as test case
|
|
417
513
|
global test_case_count
|
|
@@ -460,6 +556,7 @@ def test_case(test_case_command):
|
|
|
460
556
|
None
|
|
461
557
|
"""
|
|
462
558
|
check_test()
|
|
559
|
+
_pre_test_temp_cleanup()
|
|
463
560
|
|
|
464
561
|
# Clear hint
|
|
465
562
|
global test_case_hint
|
|
@@ -488,6 +585,7 @@ def test_case_hidden(test_case_command):
|
|
|
488
585
|
None
|
|
489
586
|
"""
|
|
490
587
|
check_test()
|
|
588
|
+
_pre_test_temp_cleanup()
|
|
491
589
|
|
|
492
590
|
# Clear hint
|
|
493
591
|
global test_case_hint
|
|
@@ -585,9 +683,10 @@ def check_output_file(output_file):
|
|
|
585
683
|
Returns:
|
|
586
684
|
None
|
|
587
685
|
"""
|
|
686
|
+
output_length(os.path.getsize(output_file))
|
|
588
687
|
with open(output_file, "r") as infile:
|
|
589
688
|
output_lines = infile.readlines()
|
|
590
|
-
|
|
689
|
+
|
|
591
690
|
with open(get_testing_path("expectedoutput"), "a") as outfile:
|
|
592
691
|
outfile.writelines(output_lines)
|
|
593
692
|
|
|
@@ -631,6 +730,48 @@ def hint(hints):
|
|
|
631
730
|
test_case_hint = hints
|
|
632
731
|
|
|
633
732
|
|
|
733
|
+
def register_temp_file(filename):
|
|
734
|
+
"""Registers a file to be deleted before and after the next T, HT, or TCMD test.
|
|
735
|
+
|
|
736
|
+
Arguments:
|
|
737
|
+
filename: the file to delete before and after the next test
|
|
738
|
+
|
|
739
|
+
Returns:
|
|
740
|
+
None
|
|
741
|
+
"""
|
|
742
|
+
global temp_files
|
|
743
|
+
temp_files.append(filename.strip())
|
|
744
|
+
|
|
745
|
+
|
|
746
|
+
def _cleanup_temp_files():
|
|
747
|
+
"""Delete all registered temp files and reset the list."""
|
|
748
|
+
global temp_files
|
|
749
|
+
for f in temp_files:
|
|
750
|
+
try:
|
|
751
|
+
os.remove(f)
|
|
752
|
+
except FileNotFoundError:
|
|
753
|
+
pass
|
|
754
|
+
temp_files = []
|
|
755
|
+
|
|
756
|
+
|
|
757
|
+
def _pre_test_temp_cleanup():
|
|
758
|
+
"""Before a test: delete registered temp files and save the list for post-test cleanup."""
|
|
759
|
+
global temp_files, _active_temp_files
|
|
760
|
+
_active_temp_files = list(temp_files)
|
|
761
|
+
_cleanup_temp_files()
|
|
762
|
+
|
|
763
|
+
|
|
764
|
+
def _post_test_temp_cleanup():
|
|
765
|
+
"""After a test: delete the files that were registered when the test started."""
|
|
766
|
+
global _active_temp_files
|
|
767
|
+
for f in _active_temp_files:
|
|
768
|
+
try:
|
|
769
|
+
os.remove(f)
|
|
770
|
+
except FileNotFoundError:
|
|
771
|
+
pass
|
|
772
|
+
_active_temp_files = []
|
|
773
|
+
|
|
774
|
+
|
|
634
775
|
def timeout(timeout_sec):
|
|
635
776
|
"""Specifies the time limit in seconds for a test case to run. Defaults to 20 seconds.
|
|
636
777
|
|
|
@@ -743,6 +884,7 @@ tag_func_map = {
|
|
|
743
884
|
"OLEN": output_length,
|
|
744
885
|
"X": exit_code,
|
|
745
886
|
"SS": start_server,
|
|
887
|
+
"TEMP": register_temp_file,
|
|
746
888
|
}
|
|
747
889
|
|
|
748
890
|
|
|
@@ -840,6 +982,13 @@ def parse_tags(tags: list[str]):
|
|
|
840
982
|
print(f" {line_num}: {tag_line.rstrip()}")
|
|
841
983
|
print(f" Valid tags are: {', '.join(sorted(valid_tags))}")
|
|
842
984
|
sys.exit(1)
|
|
985
|
+
else:
|
|
986
|
+
# Check if this looks like a bare shell command missing a CMD prefix
|
|
987
|
+
first_word = tag_line.split()[0] if tag_line.split() else ""
|
|
988
|
+
if first_word.lower() in _BARE_SHELL_COMMANDS:
|
|
989
|
+
print(f"Warning on line {line_num}: bare shell command without CMD prefix, shell commands will be ignored")
|
|
990
|
+
print(f" {line_num}: {tag_line.rstrip()}")
|
|
991
|
+
print(f" Did you mean: CMD {tag_line.rstrip()}")
|
|
843
992
|
continue
|
|
844
993
|
|
|
845
994
|
tag = tag_match.group(1)
|
|
@@ -1138,10 +1287,14 @@ def check_test():
|
|
|
1138
1287
|
if len(lines) > 22:
|
|
1139
1288
|
print(f" ... ({len(lines) - 22} more lines)")
|
|
1140
1289
|
|
|
1290
|
+
_post_test_temp_cleanup()
|
|
1291
|
+
cleanup()
|
|
1292
|
+
|
|
1141
1293
|
# Exit program after failed test case
|
|
1142
1294
|
sys.exit(2)
|
|
1143
1295
|
|
|
1144
1296
|
# reinitialize test variables and files here
|
|
1297
|
+
_post_test_temp_cleanup()
|
|
1145
1298
|
setup()
|
|
1146
1299
|
|
|
1147
1300
|
|
{assignment_codeval-0.0.29 → assignment_codeval-0.0.30}/src/assignment_codeval/submissions.py
RENAMED
|
@@ -605,7 +605,8 @@ def get_submissions_by_id(assignment):
|
|
|
605
605
|
|
|
606
606
|
@click.command()
|
|
607
607
|
@click.argument("course_name", metavar="COURSE", required=False)
|
|
608
|
-
|
|
608
|
+
@click.option("--path", is_flag=True, help="Show the path to the codeval file")
|
|
609
|
+
def list_codeval_assignments(course_name, path):
|
|
609
610
|
"""
|
|
610
611
|
List all assignments that have corresponding codeval files.
|
|
611
612
|
|
|
@@ -646,10 +647,18 @@ def list_codeval_assignments(course_name):
|
|
|
646
647
|
assignment_key = despace(assignment.name).lower()
|
|
647
648
|
if assignment_key in codeval_files:
|
|
648
649
|
matched_codeval_keys.add(assignment_key)
|
|
649
|
-
|
|
650
|
+
codeval_path = os.path.join(codeval_dir, f"{codeval_files[assignment_key]}.codeval")
|
|
651
|
+
if path:
|
|
652
|
+
info(f"{course.name}: {assignment.name} [{codeval_path}]")
|
|
653
|
+
else:
|
|
654
|
+
info(f"{course.name}: {assignment.name}")
|
|
650
655
|
elif assignment_key in codeval_titles:
|
|
651
656
|
matched_codeval_keys.add(codeval_titles[assignment_key].lower())
|
|
652
|
-
|
|
657
|
+
codeval_path = os.path.join(codeval_dir, f"{codeval_titles[assignment_key]}.codeval")
|
|
658
|
+
if path:
|
|
659
|
+
info(f"{course.name}: {assignment.name} [{codeval_path}]")
|
|
660
|
+
else:
|
|
661
|
+
info(f"{course.name}: {assignment.name}")
|
|
653
662
|
|
|
654
663
|
# Report codeval files that don't match any assignment
|
|
655
664
|
unmatched_keys = set(codeval_files.keys()) - matched_codeval_keys
|
{assignment_codeval-0.0.29 → assignment_codeval-0.0.30}/src/assignment_codeval.egg-info/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: assignment-codeval
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.30
|
|
4
4
|
Summary: CodEval for evaluating programming assignments
|
|
5
5
|
Requires-Python: >=3.12
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -69,7 +69,8 @@ Tags used in a spec file (\<course name>.codeval)
|
|
|
69
69
|
| CTO | Compile Timeout | Timeout in seconds for the compile command to run |
|
|
70
70
|
| RUN | Run Script | Specifies the script to use to evaluate the specification file. Defaults to evaluate.sh. |
|
|
71
71
|
| Z | Download Zip | Will be followed by zip files to download from Canvas to use when running the test cases. |
|
|
72
|
-
| CF | Check Function |
|
|
72
|
+
| CF | Check Function | `CF <function_name> [filename]` — checks that the function is used. The filename is **optional** and omitting it (e.g. `CF strtol`) is the preferred usage; source files are inferred from the most recent `C` tag and the check uses compiled-artifact inspection only (objdump/javap/ast). Providing a filename (e.g. `CF strtol mycalc.c`) is supported for backwards compatibility and additionally enables a regex fallback when no compiled artifact is found. |
|
|
73
|
+
| NCF | Check Not Function | `NCF <function_name> [filename]` — checks that the function is **not** used. The filename is **optional** (same behaviour as CF). |
|
|
73
74
|
| 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 |
|
|
74
75
|
| 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 |
|
|
75
76
|
| PRINT | Print Label | Prints a label/message to stdout. Cleaner alternative to `CMD echo "..."` for section labels. |
|
|
@@ -82,6 +83,7 @@ Tags used in a spec file (\<course name>.codeval)
|
|
|
82
83
|
| TO | Timeout | Specifies the time limit in seconds for a test case to run. Defaults to 20 seconds. |
|
|
83
84
|
| X | Exit Code | Specifies the expected exit code for a test case. Defaults to zero. |
|
|
84
85
|
| SS | Start Server | Command containing timeout (wait until server starts), kill timeout (wait to kill the server), and the command to start a server |
|
|
86
|
+
| TEMP | Temp File | Registers a file to be deleted before the next T, HT, or TCMD test runs (clean state) and again after it completes (cleanup). Only applies to the immediately following test — use a new TEMP tag for each test that needs it. |
|
|
85
87
|
|
|
86
88
|
Refer to a sample spec file [here](samples/assignment-name.codeval)
|
|
87
89
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Pytest tests for CodEval test files."""
|
|
2
2
|
import glob
|
|
3
3
|
import os
|
|
4
|
+
import shutil
|
|
4
5
|
import subprocess
|
|
5
6
|
import pytest
|
|
6
7
|
|
|
@@ -10,6 +11,31 @@ from assignment_codeval.evaluate import _render_diff_output
|
|
|
10
11
|
TEST_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
11
12
|
CODEVAL_FILES = sorted(glob.glob(os.path.join(TEST_DIR, "*.codeval")))
|
|
12
13
|
|
|
14
|
+
def _tool_works(*cmd):
|
|
15
|
+
"""Return True if cmd exits with code 0 (handles macOS stub wrappers)."""
|
|
16
|
+
try:
|
|
17
|
+
return subprocess.run(list(cmd), capture_output=True, timeout=10).returncode == 0
|
|
18
|
+
except (FileNotFoundError, subprocess.TimeoutExpired):
|
|
19
|
+
return False
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# Tools that codeval files may declare as requirements via "# REQUIRES: <tool>"
|
|
23
|
+
_TOOL_AVAILABLE = {
|
|
24
|
+
"java": _tool_works("javac", "--version") and _tool_works("java", "-version"),
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _parse_requires(codeval_file):
|
|
29
|
+
"""Return the set of tools declared with '# REQUIRES: <tool>' in a codeval file."""
|
|
30
|
+
required = set()
|
|
31
|
+
with open(codeval_file) as f:
|
|
32
|
+
for line in f:
|
|
33
|
+
line = line.strip()
|
|
34
|
+
if line.startswith("# REQUIRES:"):
|
|
35
|
+
tool = line.split(":", 1)[1].strip().lower()
|
|
36
|
+
required.add(tool)
|
|
37
|
+
return required
|
|
38
|
+
|
|
13
39
|
|
|
14
40
|
def get_test_ids():
|
|
15
41
|
"""Generate test IDs from filenames."""
|
|
@@ -21,6 +47,10 @@ def test_codeval(codeval_file):
|
|
|
21
47
|
"""Run a .codeval test file and verify it passes."""
|
|
22
48
|
test_name = os.path.basename(codeval_file)
|
|
23
49
|
|
|
50
|
+
for tool in _parse_requires(codeval_file):
|
|
51
|
+
if not _TOOL_AVAILABLE.get(tool, shutil.which(tool) is not None):
|
|
52
|
+
pytest.skip(f"{tool} not available")
|
|
53
|
+
|
|
24
54
|
result = subprocess.run(
|
|
25
55
|
["assignment-codeval", "run-evaluation", test_name],
|
|
26
56
|
cwd=TEST_DIR,
|
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
"""Unit tests for CF/NCF function detection helpers.
|
|
2
|
+
|
|
3
|
+
Tests the three detection paths introduced in evaluate.py:
|
|
4
|
+
- C/C++: objdump -t on compiled artifact
|
|
5
|
+
- Java: javap -c on .class file
|
|
6
|
+
- Python: ast module on source
|
|
7
|
+
|
|
8
|
+
C++ and Java tests are skipped automatically when the required tools
|
|
9
|
+
are not available (e.g. on macOS without a full JDK install).
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
import shutil
|
|
14
|
+
import subprocess
|
|
15
|
+
import tempfile
|
|
16
|
+
|
|
17
|
+
import pytest
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# ---------------------------------------------------------------------------
|
|
21
|
+
# Tool availability guards
|
|
22
|
+
# ---------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
def _tool_works(*cmd):
|
|
25
|
+
"""Return True if cmd exits with code 0."""
|
|
26
|
+
try:
|
|
27
|
+
result = subprocess.run(list(cmd), capture_output=True, timeout=10)
|
|
28
|
+
return result.returncode == 0
|
|
29
|
+
except (FileNotFoundError, subprocess.TimeoutExpired):
|
|
30
|
+
return False
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
HAS_GPP_OBJDUMP = (
|
|
34
|
+
shutil.which("g++") is not None
|
|
35
|
+
and shutil.which("objdump") is not None
|
|
36
|
+
and _tool_works("g++", "--version")
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
HAS_JAVA = _tool_works("javac", "--version") and _tool_works("javap", "-version")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# ---------------------------------------------------------------------------
|
|
43
|
+
# C++ — objdump path
|
|
44
|
+
# ---------------------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
CPP_SOURCE = """\
|
|
47
|
+
#include <iostream>
|
|
48
|
+
void greet() { std::cout << "Hello World" << std::endl; }
|
|
49
|
+
int main() { greet(); return 0; }
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@pytest.mark.skipif(not HAS_GPP_OBJDUMP, reason="g++ or objdump not available")
|
|
54
|
+
class TestCppFunctionDetection:
|
|
55
|
+
"""_function_used_in_c_cpp uses objdump -t on the compiled artifact."""
|
|
56
|
+
|
|
57
|
+
def setup_method(self):
|
|
58
|
+
self.tmpdir = tempfile.mkdtemp()
|
|
59
|
+
self.src = os.path.join(self.tmpdir, "sample.cpp")
|
|
60
|
+
self.exe = os.path.join(self.tmpdir, "sample")
|
|
61
|
+
with open(self.src, "w") as f:
|
|
62
|
+
f.write(CPP_SOURCE)
|
|
63
|
+
subprocess.run(
|
|
64
|
+
["g++", "-o", self.exe, self.src],
|
|
65
|
+
check=True,
|
|
66
|
+
capture_output=True,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
def teardown_method(self):
|
|
70
|
+
shutil.rmtree(self.tmpdir, ignore_errors=True)
|
|
71
|
+
|
|
72
|
+
def test_detects_used_function(self):
|
|
73
|
+
from assignment_codeval.evaluate import _function_used_in_c_cpp
|
|
74
|
+
assert _function_used_in_c_cpp("greet", [self.src]) is True
|
|
75
|
+
|
|
76
|
+
def test_returns_false_for_unused_function(self):
|
|
77
|
+
from assignment_codeval.evaluate import _function_used_in_c_cpp
|
|
78
|
+
assert _function_used_in_c_cpp("forbidden_func", [self.src]) is False
|
|
79
|
+
|
|
80
|
+
def test_returns_none_when_no_artifact(self):
|
|
81
|
+
"""No compiled artifact → caller should fall back to regex."""
|
|
82
|
+
from assignment_codeval.evaluate import _function_used_in_c_cpp
|
|
83
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
84
|
+
src = os.path.join(tmpdir, "uncompiled.cpp")
|
|
85
|
+
with open(src, "w") as f:
|
|
86
|
+
f.write("int main() { return 0; }\n")
|
|
87
|
+
assert _function_used_in_c_cpp("greet", [src]) is None
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# ---------------------------------------------------------------------------
|
|
91
|
+
# Java — javap path
|
|
92
|
+
# ---------------------------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
JAVA_SOURCE = """\
|
|
95
|
+
public class SampleCfDetect {
|
|
96
|
+
public static void greet() { System.out.println("Hello World"); }
|
|
97
|
+
public static void main(String[] args) { greet(); }
|
|
98
|
+
}
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@pytest.mark.skipif(not HAS_JAVA, reason="javac or javap not available")
|
|
103
|
+
class TestJavaFunctionDetection:
|
|
104
|
+
"""_function_used_in_java uses javap -c on the compiled .class file."""
|
|
105
|
+
|
|
106
|
+
def setup_method(self):
|
|
107
|
+
self.tmpdir = tempfile.mkdtemp()
|
|
108
|
+
self.src = os.path.join(self.tmpdir, "SampleCfDetect.java")
|
|
109
|
+
with open(self.src, "w") as f:
|
|
110
|
+
f.write(JAVA_SOURCE)
|
|
111
|
+
subprocess.run(
|
|
112
|
+
["javac", self.src],
|
|
113
|
+
check=True,
|
|
114
|
+
capture_output=True,
|
|
115
|
+
cwd=self.tmpdir,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
def teardown_method(self):
|
|
119
|
+
shutil.rmtree(self.tmpdir, ignore_errors=True)
|
|
120
|
+
|
|
121
|
+
def test_detects_used_method(self):
|
|
122
|
+
from assignment_codeval.evaluate import _function_used_in_java
|
|
123
|
+
assert _function_used_in_java("greet", [self.src]) is True
|
|
124
|
+
|
|
125
|
+
def test_returns_false_for_unused_method(self):
|
|
126
|
+
from assignment_codeval.evaluate import _function_used_in_java
|
|
127
|
+
assert _function_used_in_java("forbidden_func", [self.src]) is False
|
|
128
|
+
|
|
129
|
+
def test_returns_none_when_no_class_file(self):
|
|
130
|
+
"""No .class file → caller should fall back to regex."""
|
|
131
|
+
from assignment_codeval.evaluate import _function_used_in_java
|
|
132
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
133
|
+
src = os.path.join(tmpdir, "Uncompiled.java")
|
|
134
|
+
with open(src, "w") as f:
|
|
135
|
+
f.write(
|
|
136
|
+
"public class Uncompiled { public static void main(String[] a) {} }\n"
|
|
137
|
+
)
|
|
138
|
+
assert _function_used_in_java("greet", [src]) is None
|
|
139
|
+
|
|
140
|
+
def test_cf_no_filename_detects_used_method(self, capsys):
|
|
141
|
+
import assignment_codeval.evaluate as ev
|
|
142
|
+
old_cmd = ev.last_compile_command
|
|
143
|
+
old_cwd = os.getcwd()
|
|
144
|
+
try:
|
|
145
|
+
os.chdir(self.tmpdir)
|
|
146
|
+
ev.last_compile_command = f"javac SampleCfDetect.java"
|
|
147
|
+
ev.test_case_count = 0
|
|
148
|
+
ev.check_function("greet")
|
|
149
|
+
finally:
|
|
150
|
+
ev.last_compile_command = old_cmd
|
|
151
|
+
os.chdir(old_cwd)
|
|
152
|
+
captured = capsys.readouterr()
|
|
153
|
+
assert "PASSED" in captured.out
|
|
154
|
+
|
|
155
|
+
def test_cf_no_filename_fails_for_unused_method(self, capsys):
|
|
156
|
+
import assignment_codeval.evaluate as ev
|
|
157
|
+
old_cmd = ev.last_compile_command
|
|
158
|
+
old_cwd = os.getcwd()
|
|
159
|
+
try:
|
|
160
|
+
os.chdir(self.tmpdir)
|
|
161
|
+
ev.last_compile_command = f"javac SampleCfDetect.java"
|
|
162
|
+
ev.test_case_count = 0
|
|
163
|
+
ev.check_function("forbidden_func")
|
|
164
|
+
finally:
|
|
165
|
+
ev.last_compile_command = old_cmd
|
|
166
|
+
os.chdir(old_cwd)
|
|
167
|
+
captured = capsys.readouterr()
|
|
168
|
+
assert "FAILED" in captured.out
|
|
169
|
+
|
|
170
|
+
def test_ncf_no_filename_passes_for_absent_method(self, capsys):
|
|
171
|
+
import assignment_codeval.evaluate as ev
|
|
172
|
+
old_cmd = ev.last_compile_command
|
|
173
|
+
old_cwd = os.getcwd()
|
|
174
|
+
try:
|
|
175
|
+
os.chdir(self.tmpdir)
|
|
176
|
+
ev.last_compile_command = f"javac SampleCfDetect.java"
|
|
177
|
+
ev.test_case_count = 0
|
|
178
|
+
ev.check_not_function("forbidden_func")
|
|
179
|
+
finally:
|
|
180
|
+
ev.last_compile_command = old_cmd
|
|
181
|
+
os.chdir(old_cwd)
|
|
182
|
+
captured = capsys.readouterr()
|
|
183
|
+
assert "PASSED" in captured.out
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
# ---------------------------------------------------------------------------
|
|
187
|
+
# Python — ast path
|
|
188
|
+
# ---------------------------------------------------------------------------
|
|
189
|
+
|
|
190
|
+
class TestPythonFunctionDetection:
|
|
191
|
+
"""_function_used_in_python uses the ast module; no external tools needed."""
|
|
192
|
+
|
|
193
|
+
def _write_source(self, code):
|
|
194
|
+
f = tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False)
|
|
195
|
+
f.write(code)
|
|
196
|
+
f.close()
|
|
197
|
+
return f.name
|
|
198
|
+
|
|
199
|
+
def test_detects_direct_call(self):
|
|
200
|
+
from assignment_codeval.evaluate import _function_used_in_python
|
|
201
|
+
fname = self._write_source("print('hello')\nlen([1, 2])\n")
|
|
202
|
+
try:
|
|
203
|
+
assert _function_used_in_python("print", [fname]) is True
|
|
204
|
+
assert _function_used_in_python("len", [fname]) is True
|
|
205
|
+
finally:
|
|
206
|
+
os.unlink(fname)
|
|
207
|
+
|
|
208
|
+
def test_detects_attribute_call(self):
|
|
209
|
+
from assignment_codeval.evaluate import _function_used_in_python
|
|
210
|
+
fname = self._write_source("items = []\nitems.append(1)\n")
|
|
211
|
+
try:
|
|
212
|
+
assert _function_used_in_python("append", [fname]) is True
|
|
213
|
+
finally:
|
|
214
|
+
os.unlink(fname)
|
|
215
|
+
|
|
216
|
+
def test_returns_false_for_unused_function(self):
|
|
217
|
+
from assignment_codeval.evaluate import _function_used_in_python
|
|
218
|
+
fname = self._write_source("print('hello')\n")
|
|
219
|
+
try:
|
|
220
|
+
assert _function_used_in_python("eval", [fname]) is False
|
|
221
|
+
finally:
|
|
222
|
+
os.unlink(fname)
|
|
223
|
+
|
|
224
|
+
def test_ignores_function_name_in_string(self):
|
|
225
|
+
"""A function name that only appears inside a string is not a call."""
|
|
226
|
+
from assignment_codeval.evaluate import _function_used_in_python
|
|
227
|
+
fname = self._write_source('x = "eval is dangerous"\n')
|
|
228
|
+
try:
|
|
229
|
+
assert _function_used_in_python("eval", [fname]) is False
|
|
230
|
+
finally:
|
|
231
|
+
os.unlink(fname)
|
|
232
|
+
|
|
233
|
+
def test_ignores_function_name_in_comment(self):
|
|
234
|
+
"""A function name that only appears in a comment is not a call."""
|
|
235
|
+
from assignment_codeval.evaluate import _function_used_in_python
|
|
236
|
+
fname = self._write_source("# eval would be bad here\nx = 1\n")
|
|
237
|
+
try:
|
|
238
|
+
assert _function_used_in_python("eval", [fname]) is False
|
|
239
|
+
finally:
|
|
240
|
+
os.unlink(fname)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
# ---------------------------------------------------------------------------
|
|
244
|
+
# Compile-command helpers
|
|
245
|
+
# ---------------------------------------------------------------------------
|
|
246
|
+
|
|
247
|
+
class TestExtractExecutableFromCompile:
|
|
248
|
+
"""_extract_executable_from_compile returns the -o argument."""
|
|
249
|
+
|
|
250
|
+
def test_simple_c_compile(self):
|
|
251
|
+
from assignment_codeval.evaluate import _extract_executable_from_compile
|
|
252
|
+
assert _extract_executable_from_compile("cc -o mycalc --std=gnu11 mycalc.c") == "mycalc"
|
|
253
|
+
|
|
254
|
+
def test_cpp_compile(self):
|
|
255
|
+
from assignment_codeval.evaluate import _extract_executable_from_compile
|
|
256
|
+
assert _extract_executable_from_compile("g++ -o sample_cf_functions sample_cf_functions.cpp") == "sample_cf_functions"
|
|
257
|
+
|
|
258
|
+
def test_no_output_flag(self):
|
|
259
|
+
from assignment_codeval.evaluate import _extract_executable_from_compile
|
|
260
|
+
assert _extract_executable_from_compile("cc mycalc.c") is None
|
|
261
|
+
|
|
262
|
+
def test_empty_command(self):
|
|
263
|
+
from assignment_codeval.evaluate import _extract_executable_from_compile
|
|
264
|
+
assert _extract_executable_from_compile("") is None
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
class TestExtractSourceFilesFromCompile:
|
|
268
|
+
"""_extract_source_files_from_compile returns source file arguments."""
|
|
269
|
+
|
|
270
|
+
def test_single_c_file(self):
|
|
271
|
+
from assignment_codeval.evaluate import _extract_source_files_from_compile
|
|
272
|
+
assert _extract_source_files_from_compile("cc -o mycalc --std=gnu11 mycalc.c") == ["mycalc.c"]
|
|
273
|
+
|
|
274
|
+
def test_cpp_file(self):
|
|
275
|
+
from assignment_codeval.evaluate import _extract_source_files_from_compile
|
|
276
|
+
assert _extract_source_files_from_compile("g++ -o out sample.cpp") == ["sample.cpp"]
|
|
277
|
+
|
|
278
|
+
def test_multiple_source_files(self):
|
|
279
|
+
from assignment_codeval.evaluate import _extract_source_files_from_compile
|
|
280
|
+
result = _extract_source_files_from_compile("cc -o prog main.c util.c helper.c")
|
|
281
|
+
assert result == ["main.c", "util.c", "helper.c"]
|
|
282
|
+
|
|
283
|
+
def test_python_file(self):
|
|
284
|
+
from assignment_codeval.evaluate import _extract_source_files_from_compile
|
|
285
|
+
assert _extract_source_files_from_compile("python3 myscript.py") == ["myscript.py"]
|
|
286
|
+
|
|
287
|
+
def test_java_file(self):
|
|
288
|
+
from assignment_codeval.evaluate import _extract_source_files_from_compile
|
|
289
|
+
assert _extract_source_files_from_compile("javac MyClass.java") == ["MyClass.java"]
|
|
290
|
+
|
|
291
|
+
def test_skips_output_flag_value(self):
|
|
292
|
+
"""The value after -o should not be treated as a source file."""
|
|
293
|
+
from assignment_codeval.evaluate import _extract_source_files_from_compile
|
|
294
|
+
result = _extract_source_files_from_compile("cc -o mycalc.exe mycalc.c")
|
|
295
|
+
assert "mycalc.exe" not in result
|
|
296
|
+
assert "mycalc.c" in result
|
|
297
|
+
|
|
298
|
+
def test_empty_command(self):
|
|
299
|
+
from assignment_codeval.evaluate import _extract_source_files_from_compile
|
|
300
|
+
assert _extract_source_files_from_compile("") == []
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
# ---------------------------------------------------------------------------
|
|
304
|
+
# CF / NCF no-filename path (check_function / check_not_function)
|
|
305
|
+
# ---------------------------------------------------------------------------
|
|
306
|
+
|
|
307
|
+
class TestCheckFunctionNoFilename:
|
|
308
|
+
"""check_function and check_not_function infer source files from last_compile_command."""
|
|
309
|
+
|
|
310
|
+
@pytest.mark.skipif(not HAS_GPP_OBJDUMP, reason="g++ or objdump not available")
|
|
311
|
+
def test_cf_no_filename_detects_used_function(self, capsys):
|
|
312
|
+
import assignment_codeval.evaluate as ev
|
|
313
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
314
|
+
src = os.path.join(tmpdir, "sample.cpp")
|
|
315
|
+
exe = os.path.join(tmpdir, "sample")
|
|
316
|
+
with open(src, "w") as f:
|
|
317
|
+
f.write(CPP_SOURCE)
|
|
318
|
+
subprocess.run(["g++", "-o", exe, src], check=True, capture_output=True)
|
|
319
|
+
old_cmd = ev.last_compile_command
|
|
320
|
+
old_cwd = os.getcwd()
|
|
321
|
+
try:
|
|
322
|
+
os.chdir(tmpdir)
|
|
323
|
+
ev.last_compile_command = f"g++ -o sample sample.cpp"
|
|
324
|
+
ev.test_case_count = 0
|
|
325
|
+
ev.check_function("greet")
|
|
326
|
+
finally:
|
|
327
|
+
ev.last_compile_command = old_cmd
|
|
328
|
+
os.chdir(old_cwd)
|
|
329
|
+
captured = capsys.readouterr()
|
|
330
|
+
assert "PASSED" in captured.out
|
|
331
|
+
|
|
332
|
+
@pytest.mark.skipif(not HAS_GPP_OBJDUMP, reason="g++ or objdump not available")
|
|
333
|
+
def test_cf_no_filename_fails_for_unused_function(self, capsys):
|
|
334
|
+
import assignment_codeval.evaluate as ev
|
|
335
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
336
|
+
src = os.path.join(tmpdir, "sample.cpp")
|
|
337
|
+
exe = os.path.join(tmpdir, "sample")
|
|
338
|
+
with open(src, "w") as f:
|
|
339
|
+
f.write(CPP_SOURCE)
|
|
340
|
+
subprocess.run(["g++", "-o", exe, src], check=True, capture_output=True)
|
|
341
|
+
old_cmd = ev.last_compile_command
|
|
342
|
+
old_cwd = os.getcwd()
|
|
343
|
+
try:
|
|
344
|
+
os.chdir(tmpdir)
|
|
345
|
+
ev.last_compile_command = f"g++ -o sample sample.cpp"
|
|
346
|
+
ev.test_case_count = 0
|
|
347
|
+
ev.check_function("forbidden_func")
|
|
348
|
+
finally:
|
|
349
|
+
ev.last_compile_command = old_cmd
|
|
350
|
+
os.chdir(old_cwd)
|
|
351
|
+
captured = capsys.readouterr()
|
|
352
|
+
assert "FAILED" in captured.out
|
|
353
|
+
|
|
354
|
+
def test_cf_no_filename_python_detects_used_function(self, capsys):
|
|
355
|
+
"""Python uses ast — no compiled artifact needed; no-filename path works."""
|
|
356
|
+
import assignment_codeval.evaluate as ev
|
|
357
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
358
|
+
src = os.path.join(tmpdir, "myscript.py")
|
|
359
|
+
with open(src, "w") as f:
|
|
360
|
+
f.write("print('hello')\nlen([1, 2])\n")
|
|
361
|
+
old_cmd = ev.last_compile_command
|
|
362
|
+
old_cwd = os.getcwd()
|
|
363
|
+
try:
|
|
364
|
+
os.chdir(tmpdir)
|
|
365
|
+
ev.last_compile_command = "python3 -m py_compile myscript.py"
|
|
366
|
+
ev.test_case_count = 0
|
|
367
|
+
ev.check_function("print")
|
|
368
|
+
finally:
|
|
369
|
+
ev.last_compile_command = old_cmd
|
|
370
|
+
os.chdir(old_cwd)
|
|
371
|
+
captured = capsys.readouterr()
|
|
372
|
+
assert "PASSED" in captured.out
|
|
373
|
+
|
|
374
|
+
def test_cf_no_filename_python_fails_for_unused_function(self, capsys):
|
|
375
|
+
import assignment_codeval.evaluate as ev
|
|
376
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
377
|
+
src = os.path.join(tmpdir, "myscript.py")
|
|
378
|
+
with open(src, "w") as f:
|
|
379
|
+
f.write("print('hello')\n")
|
|
380
|
+
old_cmd = ev.last_compile_command
|
|
381
|
+
old_cwd = os.getcwd()
|
|
382
|
+
try:
|
|
383
|
+
os.chdir(tmpdir)
|
|
384
|
+
ev.last_compile_command = "python3 -m py_compile myscript.py"
|
|
385
|
+
ev.test_case_count = 0
|
|
386
|
+
ev.check_function("eval")
|
|
387
|
+
finally:
|
|
388
|
+
ev.last_compile_command = old_cmd
|
|
389
|
+
os.chdir(old_cwd)
|
|
390
|
+
captured = capsys.readouterr()
|
|
391
|
+
assert "FAILED" in captured.out
|
|
392
|
+
|
|
393
|
+
def test_ncf_no_filename_python(self, capsys):
|
|
394
|
+
"""NCF without filename works for Python via the ast path."""
|
|
395
|
+
import assignment_codeval.evaluate as ev
|
|
396
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
397
|
+
src = os.path.join(tmpdir, "myscript.py")
|
|
398
|
+
with open(src, "w") as f:
|
|
399
|
+
f.write("print('hello')\n")
|
|
400
|
+
old_cmd = ev.last_compile_command
|
|
401
|
+
old_cwd = os.getcwd()
|
|
402
|
+
try:
|
|
403
|
+
os.chdir(tmpdir)
|
|
404
|
+
ev.last_compile_command = "python3 -m py_compile myscript.py"
|
|
405
|
+
ev.test_case_count = 0
|
|
406
|
+
ev.check_not_function("eval")
|
|
407
|
+
finally:
|
|
408
|
+
ev.last_compile_command = old_cmd
|
|
409
|
+
os.chdir(old_cwd)
|
|
410
|
+
captured = capsys.readouterr()
|
|
411
|
+
assert "PASSED" in captured.out
|
|
@@ -1,195 +0,0 @@
|
|
|
1
|
-
"""Unit tests for CF/NCF function detection helpers.
|
|
2
|
-
|
|
3
|
-
Tests the three detection paths introduced in evaluate.py:
|
|
4
|
-
- C/C++: objdump -t on compiled artifact
|
|
5
|
-
- Java: javap -c on .class file
|
|
6
|
-
- Python: ast module on source
|
|
7
|
-
|
|
8
|
-
C++ and Java tests are skipped automatically when the required tools
|
|
9
|
-
are not available (e.g. on macOS without a full JDK install).
|
|
10
|
-
"""
|
|
11
|
-
|
|
12
|
-
import os
|
|
13
|
-
import shutil
|
|
14
|
-
import subprocess
|
|
15
|
-
import tempfile
|
|
16
|
-
|
|
17
|
-
import pytest
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
# ---------------------------------------------------------------------------
|
|
21
|
-
# Tool availability guards
|
|
22
|
-
# ---------------------------------------------------------------------------
|
|
23
|
-
|
|
24
|
-
def _tool_works(*cmd):
|
|
25
|
-
"""Return True if cmd exits with code 0."""
|
|
26
|
-
try:
|
|
27
|
-
result = subprocess.run(list(cmd), capture_output=True, timeout=10)
|
|
28
|
-
return result.returncode == 0
|
|
29
|
-
except (FileNotFoundError, subprocess.TimeoutExpired):
|
|
30
|
-
return False
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
HAS_GPP_OBJDUMP = (
|
|
34
|
-
shutil.which("g++") is not None
|
|
35
|
-
and shutil.which("objdump") is not None
|
|
36
|
-
and _tool_works("g++", "--version")
|
|
37
|
-
)
|
|
38
|
-
|
|
39
|
-
HAS_JAVA = _tool_works("javac", "--version") and _tool_works("javap", "-version")
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
# ---------------------------------------------------------------------------
|
|
43
|
-
# C++ — objdump path
|
|
44
|
-
# ---------------------------------------------------------------------------
|
|
45
|
-
|
|
46
|
-
CPP_SOURCE = """\
|
|
47
|
-
#include <iostream>
|
|
48
|
-
void greet() { std::cout << "Hello World" << std::endl; }
|
|
49
|
-
int main() { greet(); return 0; }
|
|
50
|
-
"""
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
@pytest.mark.skipif(not HAS_GPP_OBJDUMP, reason="g++ or objdump not available")
|
|
54
|
-
class TestCppFunctionDetection:
|
|
55
|
-
"""_function_used_in_c_cpp uses objdump -t on the compiled artifact."""
|
|
56
|
-
|
|
57
|
-
def setup_method(self):
|
|
58
|
-
self.tmpdir = tempfile.mkdtemp()
|
|
59
|
-
self.src = os.path.join(self.tmpdir, "sample.cpp")
|
|
60
|
-
self.exe = os.path.join(self.tmpdir, "sample")
|
|
61
|
-
with open(self.src, "w") as f:
|
|
62
|
-
f.write(CPP_SOURCE)
|
|
63
|
-
subprocess.run(
|
|
64
|
-
["g++", "-o", self.exe, self.src],
|
|
65
|
-
check=True,
|
|
66
|
-
capture_output=True,
|
|
67
|
-
)
|
|
68
|
-
|
|
69
|
-
def teardown_method(self):
|
|
70
|
-
shutil.rmtree(self.tmpdir, ignore_errors=True)
|
|
71
|
-
|
|
72
|
-
def test_detects_used_function(self):
|
|
73
|
-
from assignment_codeval.evaluate import _function_used_in_c_cpp
|
|
74
|
-
assert _function_used_in_c_cpp("greet", [self.src]) is True
|
|
75
|
-
|
|
76
|
-
def test_returns_false_for_unused_function(self):
|
|
77
|
-
from assignment_codeval.evaluate import _function_used_in_c_cpp
|
|
78
|
-
assert _function_used_in_c_cpp("forbidden_func", [self.src]) is False
|
|
79
|
-
|
|
80
|
-
def test_returns_none_when_no_artifact(self):
|
|
81
|
-
"""No compiled artifact → caller should fall back to regex."""
|
|
82
|
-
from assignment_codeval.evaluate import _function_used_in_c_cpp
|
|
83
|
-
with tempfile.TemporaryDirectory() as tmpdir:
|
|
84
|
-
src = os.path.join(tmpdir, "uncompiled.cpp")
|
|
85
|
-
with open(src, "w") as f:
|
|
86
|
-
f.write("int main() { return 0; }\n")
|
|
87
|
-
assert _function_used_in_c_cpp("greet", [src]) is None
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
# ---------------------------------------------------------------------------
|
|
91
|
-
# Java — javap path
|
|
92
|
-
# ---------------------------------------------------------------------------
|
|
93
|
-
|
|
94
|
-
JAVA_SOURCE = """\
|
|
95
|
-
public class SampleCfDetect {
|
|
96
|
-
public static void greet() { System.out.println("Hello World"); }
|
|
97
|
-
public static void main(String[] args) { greet(); }
|
|
98
|
-
}
|
|
99
|
-
"""
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
@pytest.mark.skipif(not HAS_JAVA, reason="javac or javap not available")
|
|
103
|
-
class TestJavaFunctionDetection:
|
|
104
|
-
"""_function_used_in_java uses javap -c on the compiled .class file."""
|
|
105
|
-
|
|
106
|
-
def setup_method(self):
|
|
107
|
-
self.tmpdir = tempfile.mkdtemp()
|
|
108
|
-
self.src = os.path.join(self.tmpdir, "SampleCfDetect.java")
|
|
109
|
-
with open(self.src, "w") as f:
|
|
110
|
-
f.write(JAVA_SOURCE)
|
|
111
|
-
subprocess.run(
|
|
112
|
-
["javac", self.src],
|
|
113
|
-
check=True,
|
|
114
|
-
capture_output=True,
|
|
115
|
-
cwd=self.tmpdir,
|
|
116
|
-
)
|
|
117
|
-
|
|
118
|
-
def teardown_method(self):
|
|
119
|
-
shutil.rmtree(self.tmpdir, ignore_errors=True)
|
|
120
|
-
|
|
121
|
-
def test_detects_used_method(self):
|
|
122
|
-
from assignment_codeval.evaluate import _function_used_in_java
|
|
123
|
-
assert _function_used_in_java("greet", [self.src]) is True
|
|
124
|
-
|
|
125
|
-
def test_returns_false_for_unused_method(self):
|
|
126
|
-
from assignment_codeval.evaluate import _function_used_in_java
|
|
127
|
-
assert _function_used_in_java("forbidden_func", [self.src]) is False
|
|
128
|
-
|
|
129
|
-
def test_returns_none_when_no_class_file(self):
|
|
130
|
-
"""No .class file → caller should fall back to regex."""
|
|
131
|
-
from assignment_codeval.evaluate import _function_used_in_java
|
|
132
|
-
with tempfile.TemporaryDirectory() as tmpdir:
|
|
133
|
-
src = os.path.join(tmpdir, "Uncompiled.java")
|
|
134
|
-
with open(src, "w") as f:
|
|
135
|
-
f.write(
|
|
136
|
-
"public class Uncompiled { public static void main(String[] a) {} }\n"
|
|
137
|
-
)
|
|
138
|
-
assert _function_used_in_java("greet", [src]) is None
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
# ---------------------------------------------------------------------------
|
|
142
|
-
# Python — ast path
|
|
143
|
-
# ---------------------------------------------------------------------------
|
|
144
|
-
|
|
145
|
-
class TestPythonFunctionDetection:
|
|
146
|
-
"""_function_used_in_python uses the ast module; no external tools needed."""
|
|
147
|
-
|
|
148
|
-
def _write_source(self, code):
|
|
149
|
-
f = tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False)
|
|
150
|
-
f.write(code)
|
|
151
|
-
f.close()
|
|
152
|
-
return f.name
|
|
153
|
-
|
|
154
|
-
def test_detects_direct_call(self):
|
|
155
|
-
from assignment_codeval.evaluate import _function_used_in_python
|
|
156
|
-
fname = self._write_source("print('hello')\nlen([1, 2])\n")
|
|
157
|
-
try:
|
|
158
|
-
assert _function_used_in_python("print", [fname]) is True
|
|
159
|
-
assert _function_used_in_python("len", [fname]) is True
|
|
160
|
-
finally:
|
|
161
|
-
os.unlink(fname)
|
|
162
|
-
|
|
163
|
-
def test_detects_attribute_call(self):
|
|
164
|
-
from assignment_codeval.evaluate import _function_used_in_python
|
|
165
|
-
fname = self._write_source("items = []\nitems.append(1)\n")
|
|
166
|
-
try:
|
|
167
|
-
assert _function_used_in_python("append", [fname]) is True
|
|
168
|
-
finally:
|
|
169
|
-
os.unlink(fname)
|
|
170
|
-
|
|
171
|
-
def test_returns_false_for_unused_function(self):
|
|
172
|
-
from assignment_codeval.evaluate import _function_used_in_python
|
|
173
|
-
fname = self._write_source("print('hello')\n")
|
|
174
|
-
try:
|
|
175
|
-
assert _function_used_in_python("eval", [fname]) is False
|
|
176
|
-
finally:
|
|
177
|
-
os.unlink(fname)
|
|
178
|
-
|
|
179
|
-
def test_ignores_function_name_in_string(self):
|
|
180
|
-
"""A function name that only appears inside a string is not a call."""
|
|
181
|
-
from assignment_codeval.evaluate import _function_used_in_python
|
|
182
|
-
fname = self._write_source('x = "eval is dangerous"\n')
|
|
183
|
-
try:
|
|
184
|
-
assert _function_used_in_python("eval", [fname]) is False
|
|
185
|
-
finally:
|
|
186
|
-
os.unlink(fname)
|
|
187
|
-
|
|
188
|
-
def test_ignores_function_name_in_comment(self):
|
|
189
|
-
"""A function name that only appears in a comment is not a call."""
|
|
190
|
-
from assignment_codeval.evaluate import _function_used_in_python
|
|
191
|
-
fname = self._write_source("# eval would be bad here\nx = 1\n")
|
|
192
|
-
try:
|
|
193
|
-
assert _function_used_in_python("eval", [fname]) is False
|
|
194
|
-
finally:
|
|
195
|
-
os.unlink(fname)
|
|
File without changes
|
|
File without changes
|
{assignment_codeval-0.0.29 → assignment_codeval-0.0.30}/src/assignment_codeval/ai_benchmark.py
RENAMED
|
File without changes
|
{assignment_codeval-0.0.29 → assignment_codeval-0.0.30}/src/assignment_codeval/check_grading.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{assignment_codeval-0.0.29 → assignment_codeval-0.0.30}/src/assignment_codeval/convertMD2Html.py
RENAMED
|
File without changes
|
{assignment_codeval-0.0.29 → assignment_codeval-0.0.30}/src/assignment_codeval/create_assignment.py
RENAMED
|
File without changes
|
{assignment_codeval-0.0.29 → assignment_codeval-0.0.30}/src/assignment_codeval/export_tests.py
RENAMED
|
File without changes
|
{assignment_codeval-0.0.29 → assignment_codeval-0.0.30}/src/assignment_codeval/file_utils.py
RENAMED
|
File without changes
|
{assignment_codeval-0.0.29 → assignment_codeval-0.0.30}/src/assignment_codeval/github_connect.py
RENAMED
|
File without changes
|
{assignment_codeval-0.0.29 → assignment_codeval-0.0.30}/src/assignment_codeval/install_assignment.py
RENAMED
|
File without changes
|
{assignment_codeval-0.0.29 → assignment_codeval-0.0.30}/src/assignment_codeval/recent_comments.py
RENAMED
|
File without changes
|
{assignment_codeval-0.0.29 → assignment_codeval-0.0.30}/src/assignment_codeval/test_template.html
RENAMED
|
File without changes
|
{assignment_codeval-0.0.29 → assignment_codeval-0.0.30}/src/assignment_codeval.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{assignment_codeval-0.0.29 → assignment_codeval-0.0.30}/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
|