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.
Files changed (34) hide show
  1. {assignment_codeval-0.0.29 → assignment_codeval-0.0.30}/PKG-INFO +4 -2
  2. {assignment_codeval-0.0.29 → assignment_codeval-0.0.30}/README.md +3 -1
  3. {assignment_codeval-0.0.29 → assignment_codeval-0.0.30}/pyproject.toml +1 -1
  4. {assignment_codeval-0.0.29 → assignment_codeval-0.0.30}/src/assignment_codeval/canvas_utils.py +1 -1
  5. {assignment_codeval-0.0.29 → assignment_codeval-0.0.30}/src/assignment_codeval/evaluate.py +178 -25
  6. {assignment_codeval-0.0.29 → assignment_codeval-0.0.30}/src/assignment_codeval/submissions.py +12 -3
  7. {assignment_codeval-0.0.29 → assignment_codeval-0.0.30}/src/assignment_codeval.egg-info/PKG-INFO +4 -2
  8. {assignment_codeval-0.0.29 → assignment_codeval-0.0.30}/tests/test_codeval.py +30 -0
  9. assignment_codeval-0.0.30/tests/test_function_detection.py +411 -0
  10. assignment_codeval-0.0.29/tests/test_function_detection.py +0 -195
  11. {assignment_codeval-0.0.29 → assignment_codeval-0.0.30}/setup.cfg +0 -0
  12. {assignment_codeval-0.0.29 → assignment_codeval-0.0.30}/src/assignment_codeval/__init__.py +0 -0
  13. {assignment_codeval-0.0.29 → assignment_codeval-0.0.30}/src/assignment_codeval/ai_benchmark.py +0 -0
  14. {assignment_codeval-0.0.29 → assignment_codeval-0.0.30}/src/assignment_codeval/check_grading.py +0 -0
  15. {assignment_codeval-0.0.29 → assignment_codeval-0.0.30}/src/assignment_codeval/cli.py +0 -0
  16. {assignment_codeval-0.0.29 → assignment_codeval-0.0.30}/src/assignment_codeval/commons.py +0 -0
  17. {assignment_codeval-0.0.29 → assignment_codeval-0.0.30}/src/assignment_codeval/convertMD2Html.py +0 -0
  18. {assignment_codeval-0.0.29 → assignment_codeval-0.0.30}/src/assignment_codeval/create_assignment.py +0 -0
  19. {assignment_codeval-0.0.29 → assignment_codeval-0.0.30}/src/assignment_codeval/export_tests.py +0 -0
  20. {assignment_codeval-0.0.29 → assignment_codeval-0.0.30}/src/assignment_codeval/file_utils.py +0 -0
  21. {assignment_codeval-0.0.29 → assignment_codeval-0.0.30}/src/assignment_codeval/github_connect.py +0 -0
  22. {assignment_codeval-0.0.29 → assignment_codeval-0.0.30}/src/assignment_codeval/install_assignment.py +0 -0
  23. {assignment_codeval-0.0.29 → assignment_codeval-0.0.30}/src/assignment_codeval/recent_comments.py +0 -0
  24. {assignment_codeval-0.0.29 → assignment_codeval-0.0.30}/src/assignment_codeval/test_template.html +0 -0
  25. {assignment_codeval-0.0.29 → assignment_codeval-0.0.30}/src/assignment_codeval.egg-info/SOURCES.txt +0 -0
  26. {assignment_codeval-0.0.29 → assignment_codeval-0.0.30}/src/assignment_codeval.egg-info/dependency_links.txt +0 -0
  27. {assignment_codeval-0.0.29 → assignment_codeval-0.0.30}/src/assignment_codeval.egg-info/entry_points.txt +0 -0
  28. {assignment_codeval-0.0.29 → assignment_codeval-0.0.30}/src/assignment_codeval.egg-info/requires.txt +0 -0
  29. {assignment_codeval-0.0.29 → assignment_codeval-0.0.30}/src/assignment_codeval.egg-info/top_level.txt +0 -0
  30. {assignment_codeval-0.0.29 → assignment_codeval-0.0.30}/tests/test_check_grading.py +0 -0
  31. {assignment_codeval-0.0.29 → assignment_codeval-0.0.30}/tests/test_create_assignment.py +0 -0
  32. {assignment_codeval-0.0.29 → assignment_codeval-0.0.30}/tests/test_evaluate_submissions.py +0 -0
  33. {assignment_codeval-0.0.29 → assignment_codeval-0.0.30}/tests/test_export_tests.py +0 -0
  34. {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.29
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 | 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. |
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 | 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. |
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
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "assignment-codeval"
7
- version = "0.0.29"
7
+ version = "0.0.30"
8
8
  description = "CodEval for evaluating programming assignments"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -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
- - Java: javap on .class file (falls back to regex if none found)
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
- return _function_used_regex(function_name, files)
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 a list of files to check to ensure that the function
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
- args = args.split()
250
- function_name = args[0]
251
- files = args[1:]
252
-
253
- if _is_function_used(function_name, files):
254
- print(f"Used {function_name} PASSED")
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
- print(f"Not using {function_name} FAILED")
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 a list of files to check to ensure that the function
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
- args = args.split()
367
- function_name = args[0]
368
- files = args[1:]
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 _is_function_used(function_name, files):
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
 
@@ -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
- def list_codeval_assignments(course_name):
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
- info(f"{course.name}: {assignment.name}")
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
- info(f"{course.name}: {assignment.name}")
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: assignment-codeval
3
- Version: 0.0.29
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 | 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. |
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)