assignment-codeval 0.0.23__tar.gz → 0.0.25__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.23 → assignment_codeval-0.0.25}/PKG-INFO +2 -1
- {assignment_codeval-0.0.23 → assignment_codeval-0.0.25}/README.md +1 -0
- {assignment_codeval-0.0.23 → assignment_codeval-0.0.25}/pyproject.toml +4 -1
- {assignment_codeval-0.0.23 → assignment_codeval-0.0.25}/src/assignment_codeval/canvas_utils.py +75 -0
- {assignment_codeval-0.0.23 → assignment_codeval-0.0.25}/src/assignment_codeval/check_grading.py +7 -79
- {assignment_codeval-0.0.23 → assignment_codeval-0.0.25}/src/assignment_codeval/cli.py +2 -0
- {assignment_codeval-0.0.23 → assignment_codeval-0.0.25}/src/assignment_codeval/evaluate.py +226 -39
- assignment_codeval-0.0.25/src/assignment_codeval/export_tests.py +187 -0
- {assignment_codeval-0.0.23 → assignment_codeval-0.0.25}/src/assignment_codeval/recent_comments.py +55 -36
- {assignment_codeval-0.0.23 → assignment_codeval-0.0.25}/src/assignment_codeval/submissions.py +156 -22
- assignment_codeval-0.0.25/src/assignment_codeval/test_template.html +111 -0
- {assignment_codeval-0.0.23 → assignment_codeval-0.0.25}/src/assignment_codeval.egg-info/PKG-INFO +2 -1
- {assignment_codeval-0.0.23 → assignment_codeval-0.0.25}/src/assignment_codeval.egg-info/SOURCES.txt +4 -0
- assignment_codeval-0.0.25/tests/test_export_tests.py +172 -0
- assignment_codeval-0.0.25/tests/test_function_detection.py +195 -0
- {assignment_codeval-0.0.23 → assignment_codeval-0.0.25}/setup.cfg +0 -0
- {assignment_codeval-0.0.23 → assignment_codeval-0.0.25}/src/assignment_codeval/__init__.py +0 -0
- {assignment_codeval-0.0.23 → assignment_codeval-0.0.25}/src/assignment_codeval/ai_benchmark.py +0 -0
- {assignment_codeval-0.0.23 → assignment_codeval-0.0.25}/src/assignment_codeval/commons.py +0 -0
- {assignment_codeval-0.0.23 → assignment_codeval-0.0.25}/src/assignment_codeval/convertMD2Html.py +0 -0
- {assignment_codeval-0.0.23 → assignment_codeval-0.0.25}/src/assignment_codeval/create_assignment.py +0 -0
- {assignment_codeval-0.0.23 → assignment_codeval-0.0.25}/src/assignment_codeval/file_utils.py +0 -0
- {assignment_codeval-0.0.23 → assignment_codeval-0.0.25}/src/assignment_codeval/github_connect.py +0 -0
- {assignment_codeval-0.0.23 → assignment_codeval-0.0.25}/src/assignment_codeval/install_assignment.py +0 -0
- {assignment_codeval-0.0.23 → assignment_codeval-0.0.25}/src/assignment_codeval.egg-info/dependency_links.txt +0 -0
- {assignment_codeval-0.0.23 → assignment_codeval-0.0.25}/src/assignment_codeval.egg-info/entry_points.txt +0 -0
- {assignment_codeval-0.0.23 → assignment_codeval-0.0.25}/src/assignment_codeval.egg-info/requires.txt +0 -0
- {assignment_codeval-0.0.23 → assignment_codeval-0.0.25}/src/assignment_codeval.egg-info/top_level.txt +0 -0
- {assignment_codeval-0.0.23 → assignment_codeval-0.0.25}/tests/test_check_grading.py +0 -0
- {assignment_codeval-0.0.23 → assignment_codeval-0.0.25}/tests/test_codeval.py +0 -0
- {assignment_codeval-0.0.23 → assignment_codeval-0.0.25}/tests/test_create_assignment.py +0 -0
- {assignment_codeval-0.0.23 → assignment_codeval-0.0.25}/tests/test_evaluate_submissions.py +0 -0
- {assignment_codeval-0.0.23 → assignment_codeval-0.0.25}/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.25
|
|
4
4
|
Summary: CodEval for evaluating programming assignments
|
|
5
5
|
Requires-Python: >=3.12
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -69,6 +69,7 @@ Tags used in a spec file (\<course name>.codeval)
|
|
|
69
69
|
| CF | Check Function | Will be followed by a function name and a list of files to check to ensure that the function is used by one of those files. |
|
|
70
70
|
| CC | Check Container | Will be followed by a function name and a list of files to check to ensure that a container is used by one of those files. Primarily supports C++ containers such as std::vector |
|
|
71
71
|
| CO | Check Object | Will be followed by a function name and a list of files to check to ensure that an object is used by one of those files. Primarily support C++ stream operations |
|
|
72
|
+
| PRINT | Print Label | Prints a label/message to stdout. Cleaner alternative to `CMD echo "..."` for section labels. |
|
|
72
73
|
| CMD/TCMD | Run Command | Will be followed by a command to run. The TCMD will cause the evaluation to fail if the command exits with an error. |
|
|
73
74
|
| CMP | Compare | Will be followed by two files to compare. |
|
|
74
75
|
| T/HT | Test Case | Will be followed by the command to run to test the submission. |
|
|
@@ -49,6 +49,7 @@ Tags used in a spec file (\<course name>.codeval)
|
|
|
49
49
|
| CF | Check Function | Will be followed by a function name and a list of files to check to ensure that the function is used by one of those files. |
|
|
50
50
|
| CC | Check Container | Will be followed by a function name and a list of files to check to ensure that a container is used by one of those files. Primarily supports C++ containers such as std::vector |
|
|
51
51
|
| CO | Check Object | Will be followed by a function name and a list of files to check to ensure that an object is used by one of those files. Primarily support C++ stream operations |
|
|
52
|
+
| PRINT | Print Label | Prints a label/message to stdout. Cleaner alternative to `CMD echo "..."` for section labels. |
|
|
52
53
|
| CMD/TCMD | Run Command | Will be followed by a command to run. The TCMD will cause the evaluation to fail if the command exits with an error. |
|
|
53
54
|
| CMP | Compare | Will be followed by two files to compare. |
|
|
54
55
|
| T/HT | Test Case | Will be followed by the command to run to test the submission. |
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "assignment-codeval"
|
|
7
|
-
version = "0.0.
|
|
7
|
+
version = "0.0.25"
|
|
8
8
|
description = "CodEval for evaluating programming assignments"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.12"
|
|
@@ -27,3 +27,6 @@ test = ["pytest>=7.0"]
|
|
|
27
27
|
|
|
28
28
|
[project.scripts]
|
|
29
29
|
assignment-codeval = "assignment_codeval.cli:cli"
|
|
30
|
+
|
|
31
|
+
[tool.setuptools.package-data]
|
|
32
|
+
assignment_codeval = ["*.html"]
|
{assignment_codeval-0.0.23 → assignment_codeval-0.0.25}/src/assignment_codeval/canvas_utils.py
RENAMED
|
@@ -5,6 +5,7 @@ from functools import cache
|
|
|
5
5
|
from typing import NamedTuple
|
|
6
6
|
|
|
7
7
|
import click
|
|
8
|
+
import requests
|
|
8
9
|
from canvasapi import Canvas
|
|
9
10
|
from canvasapi.current_user import CurrentUser
|
|
10
11
|
|
|
@@ -107,3 +108,77 @@ def get_assignment(course, assignment_name):
|
|
|
107
108
|
sys.exit(2)
|
|
108
109
|
assignment = assignments[0]
|
|
109
110
|
return assignment
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
SUBMISSIONS_QUERY = """
|
|
114
|
+
query SubmissionsQuery($assignmentId: ID!, $cursor: String) {
|
|
115
|
+
assignment(id: $assignmentId) {
|
|
116
|
+
submissionsConnection(after: $cursor) {
|
|
117
|
+
pageInfo {
|
|
118
|
+
hasNextPage
|
|
119
|
+
endCursor
|
|
120
|
+
}
|
|
121
|
+
nodes {
|
|
122
|
+
_id
|
|
123
|
+
submittedAt
|
|
124
|
+
user {
|
|
125
|
+
name
|
|
126
|
+
}
|
|
127
|
+
commentsConnection(filter: {allComments: true}, sortOrder: desc) {
|
|
128
|
+
nodes {
|
|
129
|
+
comment
|
|
130
|
+
createdAt
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
"""
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def get_canvas_credentials():
|
|
141
|
+
"""Read raw url and token from codeval.ini config file."""
|
|
142
|
+
parser = ConfigParser()
|
|
143
|
+
config_file = click.get_app_dir("codeval.ini")
|
|
144
|
+
parser.read(config_file)
|
|
145
|
+
if 'SERVER' not in parser:
|
|
146
|
+
error(f"did not find [SERVER] section in {config_file}.")
|
|
147
|
+
sys.exit(1)
|
|
148
|
+
for key in ['url', 'token']:
|
|
149
|
+
if key not in parser['SERVER']:
|
|
150
|
+
error(f"did not find {key} in [SERVER] in {config_file}.")
|
|
151
|
+
sys.exit(1)
|
|
152
|
+
return parser['SERVER']['url'], parser['SERVER']['token']
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def graphql_request(base_url, token, query, variables):
|
|
156
|
+
"""POST a GraphQL query to Canvas and return the data."""
|
|
157
|
+
url = f"{base_url}/api/graphql"
|
|
158
|
+
headers = {"Authorization": f"Bearer {token}"}
|
|
159
|
+
payload = {"query": query, "variables": variables}
|
|
160
|
+
resp = requests.post(url, json=payload, headers=headers)
|
|
161
|
+
resp.raise_for_status()
|
|
162
|
+
result = resp.json()
|
|
163
|
+
if "errors" in result:
|
|
164
|
+
error(f"GraphQL errors: {result['errors']}")
|
|
165
|
+
sys.exit(1)
|
|
166
|
+
return result["data"]
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def fetch_all_submissions(base_url, token, assignment_id):
|
|
170
|
+
"""Fetch all submissions for an assignment using relay pagination."""
|
|
171
|
+
all_nodes = []
|
|
172
|
+
cursor = None
|
|
173
|
+
while True:
|
|
174
|
+
variables = {"assignmentId": str(assignment_id)}
|
|
175
|
+
if cursor:
|
|
176
|
+
variables["cursor"] = cursor
|
|
177
|
+
data = graphql_request(base_url, token, SUBMISSIONS_QUERY, variables)
|
|
178
|
+
conn = data["assignment"]["submissionsConnection"]
|
|
179
|
+
all_nodes.extend(conn["nodes"])
|
|
180
|
+
if conn["pageInfo"]["hasNextPage"]:
|
|
181
|
+
cursor = conn["pageInfo"]["endCursor"]
|
|
182
|
+
else:
|
|
183
|
+
break
|
|
184
|
+
return all_nodes
|
{assignment_codeval-0.0.23 → assignment_codeval-0.0.25}/src/assignment_codeval/check_grading.py
RENAMED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
"""Check which submissions are missing a codeval comment newer than the submission."""
|
|
2
2
|
|
|
3
3
|
import sys
|
|
4
|
-
from configparser import ConfigParser
|
|
5
4
|
from datetime import datetime, timedelta, timezone
|
|
6
5
|
from math import floor
|
|
7
6
|
|
|
8
7
|
import click
|
|
9
|
-
import requests
|
|
10
8
|
|
|
11
|
-
from assignment_codeval.canvas_utils import
|
|
9
|
+
from assignment_codeval.canvas_utils import (
|
|
10
|
+
connect_to_canvas, get_course,
|
|
11
|
+
get_canvas_credentials, graphql_request, fetch_all_submissions,
|
|
12
|
+
)
|
|
12
13
|
from assignment_codeval.commons import debug, error
|
|
13
14
|
|
|
14
15
|
ASSIGNMENTS_QUERY = """
|
|
@@ -29,79 +30,6 @@ query AssignmentsQuery($courseId: ID!) {
|
|
|
29
30
|
}
|
|
30
31
|
"""
|
|
31
32
|
|
|
32
|
-
SUBMISSIONS_QUERY = """
|
|
33
|
-
query SubmissionsQuery($assignmentId: ID!, $cursor: String) {
|
|
34
|
-
assignment(id: $assignmentId) {
|
|
35
|
-
submissionsConnection(after: $cursor) {
|
|
36
|
-
pageInfo {
|
|
37
|
-
hasNextPage
|
|
38
|
-
endCursor
|
|
39
|
-
}
|
|
40
|
-
nodes {
|
|
41
|
-
_id
|
|
42
|
-
submittedAt
|
|
43
|
-
user {
|
|
44
|
-
name
|
|
45
|
-
}
|
|
46
|
-
commentsConnection(filter: {allComments: true}) {
|
|
47
|
-
nodes {
|
|
48
|
-
comment
|
|
49
|
-
createdAt
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
"""
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
def _get_canvas_credentials():
|
|
60
|
-
"""Read raw url and token from codeval.ini config file."""
|
|
61
|
-
parser = ConfigParser()
|
|
62
|
-
config_file = click.get_app_dir("codeval.ini")
|
|
63
|
-
parser.read(config_file)
|
|
64
|
-
if 'SERVER' not in parser:
|
|
65
|
-
error(f"did not find [SERVER] section in {config_file}.")
|
|
66
|
-
sys.exit(1)
|
|
67
|
-
for key in ['url', 'token']:
|
|
68
|
-
if key not in parser['SERVER']:
|
|
69
|
-
error(f"did not find {key} in [SERVER] in {config_file}.")
|
|
70
|
-
sys.exit(1)
|
|
71
|
-
return parser['SERVER']['url'], parser['SERVER']['token']
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
def _graphql_request(base_url, token, query, variables):
|
|
75
|
-
"""POST a GraphQL query to Canvas and return the data."""
|
|
76
|
-
url = f"{base_url}/api/graphql"
|
|
77
|
-
headers = {"Authorization": f"Bearer {token}"}
|
|
78
|
-
payload = {"query": query, "variables": variables}
|
|
79
|
-
resp = requests.post(url, json=payload, headers=headers)
|
|
80
|
-
resp.raise_for_status()
|
|
81
|
-
result = resp.json()
|
|
82
|
-
if "errors" in result:
|
|
83
|
-
error(f"GraphQL errors: {result['errors']}")
|
|
84
|
-
sys.exit(1)
|
|
85
|
-
return result["data"]
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
def _fetch_all_submissions(base_url, token, assignment_id):
|
|
89
|
-
"""Fetch all submissions for an assignment using relay pagination."""
|
|
90
|
-
all_nodes = []
|
|
91
|
-
cursor = None
|
|
92
|
-
while True:
|
|
93
|
-
variables = {"assignmentId": str(assignment_id)}
|
|
94
|
-
if cursor:
|
|
95
|
-
variables["cursor"] = cursor
|
|
96
|
-
data = _graphql_request(base_url, token, SUBMISSIONS_QUERY, variables)
|
|
97
|
-
conn = data["assignment"]["submissionsConnection"]
|
|
98
|
-
all_nodes.extend(conn["nodes"])
|
|
99
|
-
if conn["pageInfo"]["hasNextPage"]:
|
|
100
|
-
cursor = conn["pageInfo"]["endCursor"]
|
|
101
|
-
else:
|
|
102
|
-
break
|
|
103
|
-
return all_nodes
|
|
104
|
-
|
|
105
33
|
|
|
106
34
|
def _format_elapsed(submitted_at_str):
|
|
107
35
|
"""Format elapsed time since submission as a human-readable string."""
|
|
@@ -160,10 +88,10 @@ def check_grading(course_name, assignment_group, codeval_prefix, verbose, warn,
|
|
|
160
88
|
"""
|
|
161
89
|
(canvas, user) = connect_to_canvas()
|
|
162
90
|
course = get_course(canvas, course_name)
|
|
163
|
-
base_url, token =
|
|
91
|
+
base_url, token = get_canvas_credentials()
|
|
164
92
|
|
|
165
93
|
debug(f"fetching assignment groups for course {course.name} (id={course.id})")
|
|
166
|
-
data =
|
|
94
|
+
data = graphql_request(base_url, token, ASSIGNMENTS_QUERY, {"courseId": str(course.id)})
|
|
167
95
|
|
|
168
96
|
groups = data["course"]["assignmentGroupsConnection"]["nodes"]
|
|
169
97
|
target_group = None
|
|
@@ -192,7 +120,7 @@ def check_grading(course_name, assignment_group, codeval_prefix, verbose, warn,
|
|
|
192
120
|
assignment_name = assignment["name"]
|
|
193
121
|
debug(f"checking assignment: {assignment_name}")
|
|
194
122
|
|
|
195
|
-
submissions =
|
|
123
|
+
submissions = fetch_all_submissions(base_url, token, assignment_id)
|
|
196
124
|
|
|
197
125
|
missing = []
|
|
198
126
|
warned = []
|
|
@@ -5,6 +5,7 @@ from assignment_codeval.check_grading import check_grading
|
|
|
5
5
|
from assignment_codeval.commons import set_config
|
|
6
6
|
from assignment_codeval.create_assignment import create_assignment
|
|
7
7
|
from assignment_codeval.evaluate import run_evaluation
|
|
8
|
+
from assignment_codeval.export_tests import export_tests
|
|
8
9
|
from assignment_codeval.github_connect import github_setup_repo
|
|
9
10
|
from assignment_codeval.install_assignment import install_assignment
|
|
10
11
|
from assignment_codeval.recent_comments import recent_comments
|
|
@@ -26,6 +27,7 @@ cli.add_command(list_codeval_assignments)
|
|
|
26
27
|
cli.add_command(install_assignment)
|
|
27
28
|
cli.add_command(recent_comments)
|
|
28
29
|
cli.add_command(check_grading)
|
|
30
|
+
cli.add_command(export_tests)
|
|
29
31
|
cli.add_command(get_benchmark_command())
|
|
30
32
|
|
|
31
33
|
if __name__ == "__main__":
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#! /usr/bin/python3
|
|
2
2
|
|
|
3
|
+
import ast
|
|
3
4
|
import os
|
|
4
5
|
import re
|
|
5
6
|
import subprocess
|
|
@@ -80,52 +81,178 @@ def compile_code(compile_command):
|
|
|
80
81
|
sys.exit(1)
|
|
81
82
|
|
|
82
83
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
84
|
+
###########################################################
|
|
85
|
+
# Function detection helpers
|
|
86
|
+
###########################################################
|
|
86
87
|
|
|
87
|
-
Arguments:
|
|
88
|
-
function_name: the function name to check files for usage of
|
|
89
|
-
*files: the files to check for the function name
|
|
90
88
|
|
|
91
|
-
|
|
92
|
-
|
|
89
|
+
def _detect_language(files):
|
|
90
|
+
"""Return 'c_cpp', 'java', 'python', or 'unknown' based on file extensions."""
|
|
91
|
+
for f in files:
|
|
92
|
+
ext = os.path.splitext(f)[1].lower()
|
|
93
|
+
if ext in ('.c', '.cpp', '.cc', '.cxx', '.h', '.hpp'):
|
|
94
|
+
return 'c_cpp'
|
|
95
|
+
if ext == '.java':
|
|
96
|
+
return 'java'
|
|
97
|
+
if ext == '.py':
|
|
98
|
+
return 'python'
|
|
99
|
+
return 'unknown'
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _find_compiled_artifact(source_file):
|
|
103
|
+
"""Return the compiled artifact for a source file, or None if not found.
|
|
104
|
+
|
|
105
|
+
Checks (in order): same base name with .o, .exe, and no extension.
|
|
93
106
|
"""
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
107
|
+
base = os.path.splitext(source_file)[0]
|
|
108
|
+
for candidate in (base + '.o', base + '.exe', base):
|
|
109
|
+
if os.path.exists(candidate):
|
|
110
|
+
return candidate
|
|
111
|
+
return None
|
|
98
112
|
|
|
99
|
-
# Match:
|
|
100
|
-
# [1] start of line OR non-identifier character
|
|
101
|
-
# [2] function name
|
|
102
|
-
# [3] optional whitespace
|
|
103
|
-
# [4] opening parenthesis (function call)
|
|
104
|
-
regex = rf'(^|[^[:alnum:]_]){function_name}[[:space:]]*\('
|
|
105
113
|
|
|
106
|
-
|
|
114
|
+
def _function_used_in_c_cpp(function_name, files):
|
|
115
|
+
"""Use objdump on compiled artifacts to detect C/C++ function usage.
|
|
116
|
+
|
|
117
|
+
Returns True if found, False if artifact exists but function absent,
|
|
118
|
+
None if no compiled artifact was found (caller should fall back).
|
|
119
|
+
"""
|
|
120
|
+
found_artifact = False
|
|
121
|
+
for source_file in files:
|
|
122
|
+
artifact = _find_compiled_artifact(source_file)
|
|
123
|
+
if artifact is None:
|
|
124
|
+
continue
|
|
125
|
+
found_artifact = True
|
|
126
|
+
# objdump -t dumps the symbol table; pipe through c++filt to demangle C++ names.
|
|
127
|
+
# Mangled C++ names embed the original identifier, so grep -w finds exact matches.
|
|
128
|
+
cmd = f"objdump -t '{artifact}' | c++filt | grep -w '{function_name}'"
|
|
129
|
+
result = subprocess.run(
|
|
130
|
+
["bash", "-c", cmd],
|
|
131
|
+
stdout=subprocess.PIPE,
|
|
132
|
+
stderr=subprocess.PIPE,
|
|
133
|
+
)
|
|
134
|
+
if result.returncode == 0:
|
|
135
|
+
return True
|
|
136
|
+
if not found_artifact:
|
|
137
|
+
return None
|
|
138
|
+
return False
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _function_used_in_java(function_name, files):
|
|
142
|
+
"""Use javap on compiled .class files to detect Java method usage.
|
|
143
|
+
|
|
144
|
+
Returns True if found, False if class file exists but method absent,
|
|
145
|
+
None if no .class file was found (caller should fall back).
|
|
146
|
+
"""
|
|
147
|
+
found_class = False
|
|
148
|
+
for source_file in files:
|
|
149
|
+
base = os.path.splitext(source_file)[0]
|
|
150
|
+
class_file = base + '.class'
|
|
151
|
+
if not os.path.exists(class_file):
|
|
152
|
+
continue
|
|
153
|
+
found_class = True
|
|
154
|
+
# javap -c disassembles bytecode; method invocations reference the method name.
|
|
155
|
+
result = subprocess.run(
|
|
156
|
+
["javap", "-c", class_file],
|
|
157
|
+
stdout=subprocess.PIPE,
|
|
158
|
+
stderr=subprocess.PIPE,
|
|
159
|
+
)
|
|
160
|
+
if function_name.encode() in result.stdout:
|
|
161
|
+
return True
|
|
162
|
+
if not found_class:
|
|
163
|
+
return None
|
|
164
|
+
return False
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _function_used_in_python(function_name, files):
|
|
168
|
+
"""Use the ast module to detect function calls in Python source files.
|
|
169
|
+
|
|
170
|
+
Returns True if a matching call is found, False otherwise.
|
|
171
|
+
Handles both direct calls (func()) and attribute calls (obj.func()).
|
|
172
|
+
"""
|
|
173
|
+
for source_file in files:
|
|
174
|
+
try:
|
|
175
|
+
with open(source_file, 'r') as f:
|
|
176
|
+
source = f.read()
|
|
177
|
+
tree = ast.parse(source)
|
|
178
|
+
for node in ast.walk(tree):
|
|
179
|
+
if isinstance(node, ast.Call):
|
|
180
|
+
func = node.func
|
|
181
|
+
if isinstance(func, ast.Name) and func.id == function_name:
|
|
182
|
+
return True
|
|
183
|
+
if isinstance(func, ast.Attribute) and func.attr == function_name:
|
|
184
|
+
return True
|
|
185
|
+
except (SyntaxError, OSError):
|
|
186
|
+
continue
|
|
187
|
+
return False
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _function_used_regex(function_name, files):
|
|
191
|
+
"""Legacy regex-based fallback used when no compiled artifact is available."""
|
|
192
|
+
regex = rf'(^|[^[:alnum:]_]){function_name}[[:space:]]*\('
|
|
107
193
|
cmd = (
|
|
108
194
|
"sed -E "
|
|
109
195
|
"'"
|
|
110
|
-
"s://.*$::g; "
|
|
111
|
-
"s:#.*$::g; "
|
|
196
|
+
"s://.*$::g; "
|
|
197
|
+
"s:#.*$::g; "
|
|
112
198
|
":a; /\\/\\*/{N; s:/\\*.*?\\*/::g; ba}"
|
|
113
199
|
"' "
|
|
114
200
|
+ " ".join(files)
|
|
115
201
|
+ f" | grep -E '{regex}'"
|
|
116
202
|
)
|
|
117
|
-
|
|
118
|
-
function_popen = subprocess.Popen(
|
|
203
|
+
result = subprocess.run(
|
|
119
204
|
["bash", "-c", cmd],
|
|
120
205
|
stdout=subprocess.PIPE,
|
|
121
206
|
stderr=subprocess.PIPE,
|
|
122
207
|
)
|
|
208
|
+
return result.returncode == 0
|
|
123
209
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
210
|
+
|
|
211
|
+
def _is_function_used(function_name, files):
|
|
212
|
+
"""Detect whether function_name is used in the given files.
|
|
213
|
+
|
|
214
|
+
Dispatches to the appropriate tool based on file language:
|
|
215
|
+
- C/C++: objdump on compiled artifact (falls back to regex if none found)
|
|
216
|
+
- Java: javap on .class file (falls back to regex if none found)
|
|
217
|
+
- Python: ast module on source
|
|
218
|
+
- Other: regex on source
|
|
219
|
+
"""
|
|
220
|
+
lang = _detect_language(files)
|
|
221
|
+
if lang == 'python':
|
|
222
|
+
return _function_used_in_python(function_name, files)
|
|
223
|
+
if lang == 'java':
|
|
224
|
+
result = _function_used_in_java(function_name, files)
|
|
225
|
+
if result is None:
|
|
226
|
+
return _function_used_regex(function_name, files)
|
|
227
|
+
return result
|
|
228
|
+
if lang == 'c_cpp':
|
|
229
|
+
result = _function_used_in_c_cpp(function_name, files)
|
|
230
|
+
if result is None:
|
|
231
|
+
return _function_used_regex(function_name, files)
|
|
232
|
+
return result
|
|
233
|
+
return _function_used_regex(function_name, files)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def check_function(args):
|
|
237
|
+
"""Will be followed by a function name and a list of files to check to ensure that the function
|
|
238
|
+
is used by one of those files.
|
|
239
|
+
|
|
240
|
+
Arguments:
|
|
241
|
+
function_name: the function name to check files for usage of
|
|
242
|
+
*files: the files to check for the function name
|
|
243
|
+
|
|
244
|
+
Returns:
|
|
245
|
+
None
|
|
246
|
+
"""
|
|
247
|
+
check_test()
|
|
248
|
+
args = args.split()
|
|
249
|
+
function_name = args[0]
|
|
250
|
+
files = args[1:]
|
|
251
|
+
|
|
252
|
+
if _is_function_used(function_name, files):
|
|
128
253
|
print(f"Used {function_name} PASSED")
|
|
254
|
+
else:
|
|
255
|
+
print(f"Not using {function_name} FAILED")
|
|
129
256
|
|
|
130
257
|
def check_object(args):
|
|
131
258
|
"""Will be followed by an object and a list of files to ensure that the function is
|
|
@@ -228,7 +355,7 @@ def check_not_function(args):
|
|
|
228
355
|
is not used by any of those files.
|
|
229
356
|
|
|
230
357
|
Arguments:
|
|
231
|
-
function_name: the
|
|
358
|
+
function_name: the function name to check files for usage of
|
|
232
359
|
*files: the files to check for the function name
|
|
233
360
|
|
|
234
361
|
Returns:
|
|
@@ -239,18 +366,23 @@ def check_not_function(args):
|
|
|
239
366
|
function_name = args[0]
|
|
240
367
|
files = args[1:]
|
|
241
368
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
["grep", f"[^[:alpha:]]{function_name}[[:space:]]*("] + files,
|
|
245
|
-
stdout=subprocess.PIPE,
|
|
246
|
-
stderr=subprocess.PIPE,
|
|
247
|
-
)
|
|
248
|
-
|
|
249
|
-
function_popen.communicate()
|
|
250
|
-
if function_popen.returncode:
|
|
251
|
-
print(f"used{function_name} PASSED")
|
|
369
|
+
if _is_function_used(function_name, files):
|
|
370
|
+
print(f"Used {function_name} FAILED")
|
|
252
371
|
else:
|
|
253
|
-
print(f"
|
|
372
|
+
print(f"Not using {function_name} PASSED")
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def print_label(text):
|
|
376
|
+
"""Print a label/message to stdout.
|
|
377
|
+
|
|
378
|
+
Arguments:
|
|
379
|
+
text: the text to print
|
|
380
|
+
|
|
381
|
+
Returns:
|
|
382
|
+
None
|
|
383
|
+
"""
|
|
384
|
+
check_test()
|
|
385
|
+
print(text)
|
|
254
386
|
|
|
255
387
|
|
|
256
388
|
def run_command(command):
|
|
@@ -578,6 +710,7 @@ tag_func_map = {
|
|
|
578
710
|
"CO" : check_object,
|
|
579
711
|
"CC": check_container,
|
|
580
712
|
"NCF": check_not_function,
|
|
713
|
+
"PRINT": print_label,
|
|
581
714
|
"CMD": run_command,
|
|
582
715
|
"TCMD": run_command_noerror,
|
|
583
716
|
"CMP": compare,
|
|
@@ -774,6 +907,27 @@ def parse_diff(diff_lines: list[str], testing_dir: str):
|
|
|
774
907
|
outfile.write(line)
|
|
775
908
|
|
|
776
909
|
|
|
910
|
+
def _find_touched_files(pre_run_timestamp):
|
|
911
|
+
"""Find files touched (modified or accessed) since pre_run_timestamp.
|
|
912
|
+
|
|
913
|
+
Recursively scans the current working directory, ignoring TESTING_DIR.
|
|
914
|
+
"""
|
|
915
|
+
touched = []
|
|
916
|
+
for dirpath, dirnames, filenames in os.walk("."):
|
|
917
|
+
# Prune TESTING_DIR from traversal
|
|
918
|
+
if TESTING_DIR in dirnames:
|
|
919
|
+
dirnames.remove(TESTING_DIR)
|
|
920
|
+
for filename in filenames:
|
|
921
|
+
filepath = os.path.join(dirpath, filename)
|
|
922
|
+
try:
|
|
923
|
+
st = os.stat(filepath)
|
|
924
|
+
if st.st_mtime > pre_run_timestamp or st.st_atime > pre_run_timestamp:
|
|
925
|
+
touched.append(filepath)
|
|
926
|
+
except OSError:
|
|
927
|
+
continue
|
|
928
|
+
return touched
|
|
929
|
+
|
|
930
|
+
|
|
777
931
|
def check_test():
|
|
778
932
|
global test_args
|
|
779
933
|
if test_args == "":
|
|
@@ -782,6 +936,9 @@ def check_test():
|
|
|
782
936
|
print(f"Test case {test_case_count} of {test_case_total}")
|
|
783
937
|
passed = True
|
|
784
938
|
|
|
939
|
+
# Record timestamp before running student code for file tracking
|
|
940
|
+
pre_run_timestamp = time.time()
|
|
941
|
+
|
|
785
942
|
with open(get_testing_path("fileinput"), "rb") as fileinput, open(
|
|
786
943
|
get_testing_path("youroutput"), "w"
|
|
787
944
|
) as youroutput, open(get_testing_path("yourerror"), "w") as yourerror:
|
|
@@ -797,6 +954,9 @@ def check_test():
|
|
|
797
954
|
print(f"Took more than {timeout_val} seconds to run. FAIL")
|
|
798
955
|
passed = False
|
|
799
956
|
|
|
957
|
+
# Find files touched by student's program
|
|
958
|
+
touched_files = _find_touched_files(pre_run_timestamp)
|
|
959
|
+
|
|
800
960
|
# Difflog handling
|
|
801
961
|
with open(get_testing_path("difflog"), "w") as outfile:
|
|
802
962
|
diff_popen = subprocess.Popen(
|
|
@@ -872,6 +1032,33 @@ def check_test():
|
|
|
872
1032
|
# Print entire file
|
|
873
1033
|
print("".join(file_lines))
|
|
874
1034
|
|
|
1035
|
+
# Show diffs for files touched by student's program
|
|
1036
|
+
if touched_files and cmps:
|
|
1037
|
+
expected_map = {}
|
|
1038
|
+
for file_pair in cmps:
|
|
1039
|
+
if len(file_pair) >= 2:
|
|
1040
|
+
expected_map[os.path.normpath(file_pair[0])] = file_pair[1]
|
|
1041
|
+
|
|
1042
|
+
for filepath in touched_files:
|
|
1043
|
+
normalized = os.path.normpath(filepath)
|
|
1044
|
+
if normalized in expected_map:
|
|
1045
|
+
expected_file = expected_map[normalized]
|
|
1046
|
+
if os.path.exists(expected_file):
|
|
1047
|
+
diff_proc = subprocess.Popen(
|
|
1048
|
+
["diff", "-U1", "-a", filepath, expected_file],
|
|
1049
|
+
stdout=subprocess.PIPE,
|
|
1050
|
+
stderr=subprocess.PIPE,
|
|
1051
|
+
text=True,
|
|
1052
|
+
)
|
|
1053
|
+
diff_output, _ = diff_proc.communicate()
|
|
1054
|
+
if diff_output:
|
|
1055
|
+
print(f" File output diff ({os.path.basename(filepath)}):")
|
|
1056
|
+
lines = diff_output.splitlines()
|
|
1057
|
+
for line in lines[:22]:
|
|
1058
|
+
print(f" {line}")
|
|
1059
|
+
if len(lines) > 22:
|
|
1060
|
+
print(f" ... ({len(lines) - 22} more lines)")
|
|
1061
|
+
|
|
875
1062
|
cleanup()
|
|
876
1063
|
|
|
877
1064
|
# Exit program after failed test case
|