assignment-codeval 0.0.14__tar.gz → 0.0.16__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.14 → assignment_codeval-0.0.16}/PKG-INFO +1 -1
- {assignment_codeval-0.0.14 → assignment_codeval-0.0.16}/pyproject.toml +1 -1
- {assignment_codeval-0.0.14 → assignment_codeval-0.0.16}/src/assignment_codeval/cli.py +4 -1
- {assignment_codeval-0.0.14 → assignment_codeval-0.0.16}/src/assignment_codeval/github_connect.py +38 -5
- assignment_codeval-0.0.16/src/assignment_codeval/install_assignment.py +143 -0
- {assignment_codeval-0.0.14 → assignment_codeval-0.0.16}/src/assignment_codeval/submissions.py +151 -19
- {assignment_codeval-0.0.14 → assignment_codeval-0.0.16}/src/assignment_codeval.egg-info/PKG-INFO +1 -1
- {assignment_codeval-0.0.14 → assignment_codeval-0.0.16}/src/assignment_codeval.egg-info/SOURCES.txt +4 -1
- assignment_codeval-0.0.16/tests/test_evaluate_submissions.py +158 -0
- assignment_codeval-0.0.16/tests/test_install_assignment.py +183 -0
- {assignment_codeval-0.0.14 → assignment_codeval-0.0.16}/README.md +0 -0
- {assignment_codeval-0.0.14 → assignment_codeval-0.0.16}/setup.cfg +0 -0
- {assignment_codeval-0.0.14 → assignment_codeval-0.0.16}/src/assignment_codeval/__init__.py +0 -0
- {assignment_codeval-0.0.14 → assignment_codeval-0.0.16}/src/assignment_codeval/ai_benchmark.py +0 -0
- {assignment_codeval-0.0.14 → assignment_codeval-0.0.16}/src/assignment_codeval/canvas_utils.py +0 -0
- {assignment_codeval-0.0.14 → assignment_codeval-0.0.16}/src/assignment_codeval/commons.py +0 -0
- {assignment_codeval-0.0.14 → assignment_codeval-0.0.16}/src/assignment_codeval/convertMD2Html.py +0 -0
- {assignment_codeval-0.0.14 → assignment_codeval-0.0.16}/src/assignment_codeval/create_assignment.py +0 -0
- {assignment_codeval-0.0.14 → assignment_codeval-0.0.16}/src/assignment_codeval/evaluate.py +0 -0
- {assignment_codeval-0.0.14 → assignment_codeval-0.0.16}/src/assignment_codeval/file_utils.py +0 -0
- {assignment_codeval-0.0.14 → assignment_codeval-0.0.16}/src/assignment_codeval.egg-info/dependency_links.txt +0 -0
- {assignment_codeval-0.0.14 → assignment_codeval-0.0.16}/src/assignment_codeval.egg-info/entry_points.txt +0 -0
- {assignment_codeval-0.0.14 → assignment_codeval-0.0.16}/src/assignment_codeval.egg-info/requires.txt +0 -0
- {assignment_codeval-0.0.14 → assignment_codeval-0.0.16}/src/assignment_codeval.egg-info/top_level.txt +0 -0
- {assignment_codeval-0.0.14 → assignment_codeval-0.0.16}/tests/test_codeval.py +0 -0
- {assignment_codeval-0.0.14 → assignment_codeval-0.0.16}/tests/test_create_assignment.py +0 -0
|
@@ -4,7 +4,8 @@ from assignment_codeval.ai_benchmark import get_benchmark_command
|
|
|
4
4
|
from assignment_codeval.create_assignment import create_assignment
|
|
5
5
|
from assignment_codeval.evaluate import run_evaluation
|
|
6
6
|
from assignment_codeval.github_connect import github_setup_repo
|
|
7
|
-
from assignment_codeval.
|
|
7
|
+
from assignment_codeval.install_assignment import install_assignment
|
|
8
|
+
from assignment_codeval.submissions import download_submissions, upload_submission_comments, evaluate_submissions, list_codeval_assignments
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
@click.group()
|
|
@@ -17,6 +18,8 @@ cli.add_command(upload_submission_comments)
|
|
|
17
18
|
cli.add_command(github_setup_repo)
|
|
18
19
|
cli.add_command(evaluate_submissions)
|
|
19
20
|
cli.add_command(create_assignment)
|
|
21
|
+
cli.add_command(list_codeval_assignments)
|
|
22
|
+
cli.add_command(install_assignment)
|
|
20
23
|
cli.add_command(get_benchmark_command())
|
|
21
24
|
|
|
22
25
|
if __name__ == "__main__":
|
{assignment_codeval-0.0.14 → assignment_codeval-0.0.16}/src/assignment_codeval/github_connect.py
RENAMED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import os
|
|
1
|
+
import os
|
|
2
2
|
import re
|
|
3
3
|
import subprocess
|
|
4
4
|
from configparser import ConfigParser
|
|
@@ -13,8 +13,8 @@ HEX_DIGITS = "0123456789abcdefABCDEF"
|
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
@click.command()
|
|
16
|
-
@click.argument("course_name", metavar="COURSE")
|
|
17
|
-
@click.argument("assignment_name", metavar="ASSIGNMENT")
|
|
16
|
+
@click.argument("course_name", metavar="COURSE", required=False)
|
|
17
|
+
@click.argument("assignment_name", metavar="ASSIGNMENT", required=False)
|
|
18
18
|
@click.option("--all-repos", is_flag=True,
|
|
19
19
|
help="download all repositories, even if they don't have a valid commit hash")
|
|
20
20
|
@click.option("--target-dir", help="directory to download submissions to", default='./submissions', show_default=True)
|
|
@@ -28,14 +28,47 @@ def github_setup_repo(course_name, assignment_name, target_dir, github_field, al
|
|
|
28
28
|
|
|
29
29
|
COURSE can be a unique substring of the actual course name.
|
|
30
30
|
|
|
31
|
+
If COURSE and ASSIGNMENT are not specified, will process all course/assignment
|
|
32
|
+
subdirectories found in the submissions directory.
|
|
31
33
|
"""
|
|
32
34
|
canvas, user = connect_to_canvas()
|
|
33
35
|
parser = ConfigParser()
|
|
34
36
|
config_file = click.get_app_dir("codeval.ini")
|
|
35
37
|
parser.read(config_file)
|
|
36
38
|
parser.config_file = config_file
|
|
37
|
-
|
|
38
|
-
|
|
39
|
+
|
|
40
|
+
if course_name and assignment_name:
|
|
41
|
+
# Explicit course and assignment specified
|
|
42
|
+
course = get_course(canvas, course_name, True)
|
|
43
|
+
assignment = get_assignment(course, assignment_name)
|
|
44
|
+
_setup_repo_for_assignment(
|
|
45
|
+
canvas, parser, course, assignment, target_dir, github_field, all_repos, clone_delay
|
|
46
|
+
)
|
|
47
|
+
elif course_name or assignment_name:
|
|
48
|
+
raise click.UsageError("Both COURSE and ASSIGNMENT must be specified, or neither")
|
|
49
|
+
else:
|
|
50
|
+
# Scan submissions directory for course/assignment subdirectories
|
|
51
|
+
if not os.path.isdir(target_dir):
|
|
52
|
+
error(f"submissions directory {target_dir} does not exist")
|
|
53
|
+
return
|
|
54
|
+
for course_dir in sorted(os.listdir(target_dir)):
|
|
55
|
+
course_path = os.path.join(target_dir, course_dir)
|
|
56
|
+
if not os.path.isdir(course_path):
|
|
57
|
+
continue
|
|
58
|
+
for assignment_dir in sorted(os.listdir(course_path)):
|
|
59
|
+
assignment_path = os.path.join(course_path, assignment_dir)
|
|
60
|
+
if not os.path.isdir(assignment_path):
|
|
61
|
+
continue
|
|
62
|
+
info(f"processing {course_dir}/{assignment_dir}")
|
|
63
|
+
course = get_course(canvas, course_dir, True)
|
|
64
|
+
assignment = get_assignment(course, assignment_dir)
|
|
65
|
+
_setup_repo_for_assignment(
|
|
66
|
+
canvas, parser, course, assignment, target_dir, github_field, all_repos, clone_delay
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _setup_repo_for_assignment(canvas, parser, course, assignment, target_dir, github_field, all_repos, clone_delay):
|
|
71
|
+
"""Set up GitHub repos for a single assignment."""
|
|
39
72
|
submission_dir = os.path.join(target_dir, despace(course.name), despace(assignment.name))
|
|
40
73
|
os.makedirs(submission_dir, exist_ok=True)
|
|
41
74
|
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
#! /usr/bin/python3
|
|
2
|
+
"""Install a codeval assignment file and its referenced zip files to a destination."""
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
6
|
+
import shutil
|
|
7
|
+
import subprocess
|
|
8
|
+
|
|
9
|
+
import click
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def parse_z_tags(codeval_path: str) -> list[str]:
|
|
13
|
+
"""Parse a codeval file and return a list of zip files referenced by Z tags.
|
|
14
|
+
|
|
15
|
+
Arguments:
|
|
16
|
+
codeval_path: Path to the codeval file
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
List of zip file paths referenced by Z tags
|
|
20
|
+
"""
|
|
21
|
+
zip_files = []
|
|
22
|
+
with open(codeval_path, 'r') as f:
|
|
23
|
+
for line in f:
|
|
24
|
+
line = line.strip()
|
|
25
|
+
if line.startswith('Z '):
|
|
26
|
+
zip_file = line[2:].strip()
|
|
27
|
+
if zip_file:
|
|
28
|
+
zip_files.append(zip_file)
|
|
29
|
+
return zip_files
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def is_remote_destination(destination: str) -> bool:
|
|
33
|
+
"""Check if destination is a remote path (host:path format).
|
|
34
|
+
|
|
35
|
+
Arguments:
|
|
36
|
+
destination: The destination path
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
True if destination is remote (contains : but not a Windows drive letter)
|
|
40
|
+
"""
|
|
41
|
+
# Match host:path pattern but not Windows drive letters like C:\
|
|
42
|
+
if ':' not in destination:
|
|
43
|
+
return False
|
|
44
|
+
# Windows drive letter check (e.g., C:\path)
|
|
45
|
+
if re.match(r'^[A-Za-z]:\\', destination):
|
|
46
|
+
return False
|
|
47
|
+
return True
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def copy_file(source: str, destination: str, verbose: bool = False) -> None:
|
|
51
|
+
"""Copy a file to a local or remote destination.
|
|
52
|
+
|
|
53
|
+
Arguments:
|
|
54
|
+
source: Path to the source file
|
|
55
|
+
destination: Destination path (local or host:path for remote)
|
|
56
|
+
verbose: Print verbose output
|
|
57
|
+
|
|
58
|
+
Raises:
|
|
59
|
+
click.ClickException: If copy fails
|
|
60
|
+
"""
|
|
61
|
+
if is_remote_destination(destination):
|
|
62
|
+
# Use scp for remote destinations
|
|
63
|
+
cmd = ['scp', source, destination]
|
|
64
|
+
if verbose:
|
|
65
|
+
click.echo(f"Running: {' '.join(cmd)}")
|
|
66
|
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
67
|
+
if result.returncode != 0:
|
|
68
|
+
raise click.ClickException(f"scp failed: {result.stderr}")
|
|
69
|
+
else:
|
|
70
|
+
# Local copy
|
|
71
|
+
if verbose:
|
|
72
|
+
click.echo(f"Copying {source} to {destination}")
|
|
73
|
+
os.makedirs(destination, exist_ok=True)
|
|
74
|
+
shutil.copy2(source, destination)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@click.command()
|
|
78
|
+
@click.argument('codeval_file', type=click.Path(exists=True))
|
|
79
|
+
@click.argument('destination')
|
|
80
|
+
@click.option('--verbose', '-v', is_flag=True, help='Show detailed output')
|
|
81
|
+
def install_assignment(codeval_file: str, destination: str, verbose: bool):
|
|
82
|
+
"""Install a codeval file and its referenced zip files to a destination.
|
|
83
|
+
|
|
84
|
+
CODEVAL_FILE is the path to the codeval specification file.
|
|
85
|
+
|
|
86
|
+
DESTINATION is either a local path or a remote path in host:path format.
|
|
87
|
+
If remote, scp will be used to copy files.
|
|
88
|
+
|
|
89
|
+
\b
|
|
90
|
+
Examples:
|
|
91
|
+
# Local install
|
|
92
|
+
assignment-codeval install-assignment myassign.codeval /path/to/dest
|
|
93
|
+
|
|
94
|
+
# Remote install
|
|
95
|
+
assignment-codeval install-assignment myassign.codeval server:/home/user/assignments
|
|
96
|
+
"""
|
|
97
|
+
codeval_path = os.path.abspath(codeval_file)
|
|
98
|
+
codeval_dir = os.path.dirname(codeval_path)
|
|
99
|
+
codeval_name = os.path.basename(codeval_path)
|
|
100
|
+
|
|
101
|
+
# Parse Z tags to find referenced zip files
|
|
102
|
+
zip_files = parse_z_tags(codeval_path)
|
|
103
|
+
|
|
104
|
+
# Build list of files to copy
|
|
105
|
+
files_to_copy = [codeval_path]
|
|
106
|
+
|
|
107
|
+
for zip_file in zip_files:
|
|
108
|
+
# Zip files are relative to the codeval file's directory
|
|
109
|
+
zip_path = os.path.join(codeval_dir, zip_file)
|
|
110
|
+
if os.path.exists(zip_path):
|
|
111
|
+
files_to_copy.append(zip_path)
|
|
112
|
+
else:
|
|
113
|
+
click.echo(f"Warning: Referenced zip file not found: {zip_path}", err=True)
|
|
114
|
+
|
|
115
|
+
# Show summary
|
|
116
|
+
click.echo(f"Codeval file: {codeval_name}")
|
|
117
|
+
if zip_files:
|
|
118
|
+
click.echo(f"Referenced zip files: {', '.join(zip_files)}")
|
|
119
|
+
else:
|
|
120
|
+
click.echo("No zip files referenced")
|
|
121
|
+
click.echo(f"Destination: {destination}")
|
|
122
|
+
|
|
123
|
+
if is_remote_destination(destination):
|
|
124
|
+
click.echo("Using scp for remote copy")
|
|
125
|
+
|
|
126
|
+
# Copy files
|
|
127
|
+
for source_path in files_to_copy:
|
|
128
|
+
filename = os.path.basename(source_path)
|
|
129
|
+
if is_remote_destination(destination):
|
|
130
|
+
# For remote, append filename to destination
|
|
131
|
+
dest_path = f"{destination}/{filename}"
|
|
132
|
+
else:
|
|
133
|
+
dest_path = destination
|
|
134
|
+
|
|
135
|
+
try:
|
|
136
|
+
copy_file(source_path, dest_path, verbose)
|
|
137
|
+
click.echo(f"Copied: {filename}")
|
|
138
|
+
except click.ClickException as e:
|
|
139
|
+
raise e
|
|
140
|
+
except Exception as e:
|
|
141
|
+
raise click.ClickException(f"Failed to copy {filename}: {e}")
|
|
142
|
+
|
|
143
|
+
click.echo("Done")
|
{assignment_codeval-0.0.14 → assignment_codeval-0.0.16}/src/assignment_codeval/submissions.py
RENAMED
|
@@ -4,7 +4,7 @@ import shutil
|
|
|
4
4
|
import subprocess
|
|
5
5
|
import time
|
|
6
6
|
from configparser import ConfigParser
|
|
7
|
-
from datetime import datetime, timezone
|
|
7
|
+
from datetime import datetime, timedelta, timezone
|
|
8
8
|
from functools import cache
|
|
9
9
|
from tempfile import TemporaryDirectory
|
|
10
10
|
from zipfile import ZipFile
|
|
@@ -12,7 +12,7 @@ from zipfile import ZipFile
|
|
|
12
12
|
import click
|
|
13
13
|
import requests
|
|
14
14
|
|
|
15
|
-
from assignment_codeval.canvas_utils import connect_to_canvas, get_course, get_assignment
|
|
15
|
+
from assignment_codeval.canvas_utils import connect_to_canvas, get_course, get_courses, get_assignment
|
|
16
16
|
from assignment_codeval.commons import debug, error, info, warn, despace
|
|
17
17
|
|
|
18
18
|
|
|
@@ -90,9 +90,11 @@ def evaluate_submissions(codeval_dir, submissions_dir):
|
|
|
90
90
|
warn(f"no codeval file found for {assignment_name} in {codeval_file}")
|
|
91
91
|
continue
|
|
92
92
|
|
|
93
|
-
# get
|
|
93
|
+
# First pass: get CTO, CD tags, and collect Z files (don't extract yet)
|
|
94
94
|
compile_timeout = 20
|
|
95
95
|
assignment_working_dir = "."
|
|
96
|
+
has_cd_tag = False
|
|
97
|
+
zip_files = []
|
|
96
98
|
move_to_next_submission = False
|
|
97
99
|
with open(codeval_file, "r") as fd:
|
|
98
100
|
for line in fd:
|
|
@@ -103,6 +105,7 @@ def evaluate_submissions(codeval_dir, submissions_dir):
|
|
|
103
105
|
except Exception:
|
|
104
106
|
warn(f"could not parse compile timeout from {line}, using default {compile_timeout}")
|
|
105
107
|
if line.startswith("CD"):
|
|
108
|
+
has_cd_tag = True
|
|
106
109
|
assignment_working_dir = os.path.normpath(
|
|
107
110
|
os.path.join(assignment_working_dir, line.split()[1].strip()))
|
|
108
111
|
if not os.path.isdir(os.path.join(submission_dir, assignment_working_dir)):
|
|
@@ -110,16 +113,26 @@ def evaluate_submissions(codeval_dir, submissions_dir):
|
|
|
110
113
|
move_to_next_submission = True
|
|
111
114
|
break
|
|
112
115
|
if line.startswith("Z"):
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
116
|
+
zip_files.append(line.split(None, 1)[1])
|
|
117
|
+
|
|
118
|
+
# If no CD tag and this is a GitHub submission (has .git), use assignment name as working dir
|
|
119
|
+
if not has_cd_tag and os.path.exists(os.path.join(submission_dir, assignment_name, ".git")):
|
|
120
|
+
assignment_working_dir = assignment_name
|
|
121
|
+
if not os.path.isdir(os.path.join(submission_dir, assignment_working_dir)):
|
|
122
|
+
out = f"{assignment_working_dir} does not exist or is not a directory\n".encode('utf-8')
|
|
123
|
+
move_to_next_submission = True
|
|
124
|
+
|
|
125
|
+
# Now extract zip files to the correct working directory
|
|
126
|
+
if not move_to_next_submission:
|
|
127
|
+
for zf_name in zip_files:
|
|
128
|
+
with ZipFile(os.path.join(codeval_dir, zf_name)) as zf:
|
|
129
|
+
for f in zf.infolist():
|
|
130
|
+
dest_dir = os.path.join(submission_dir, assignment_working_dir)
|
|
131
|
+
zf.extract(f, dest_dir)
|
|
132
|
+
if not f.is_dir():
|
|
133
|
+
perms = f.external_attr >> 16
|
|
134
|
+
if perms:
|
|
135
|
+
os.chmod(os.path.join(dest_dir, f.filename), perms)
|
|
123
136
|
|
|
124
137
|
if not move_to_next_submission:
|
|
125
138
|
command = raw_command.replace("EVALUATE", "cd /submissions; assignment-codeval run-evaluation codeval.txt")
|
|
@@ -151,14 +164,17 @@ def evaluate_submissions(codeval_dir, submissions_dir):
|
|
|
151
164
|
info("finished executing docker")
|
|
152
165
|
|
|
153
166
|
info("writing results")
|
|
154
|
-
with open(f"{dirpath}/comments.txt", "
|
|
167
|
+
with open(f"{dirpath}/comments.txt", "ab") as fd:
|
|
155
168
|
fd.write(out)
|
|
156
169
|
info("continuing")
|
|
157
170
|
|
|
158
171
|
|
|
159
172
|
@click.command()
|
|
160
|
-
@click.argument("course_name", metavar="COURSE")
|
|
161
|
-
@click.argument("assignment_name", metavar="ASSIGNMENT")
|
|
173
|
+
@click.argument("course_name", metavar="COURSE", required=False)
|
|
174
|
+
@click.argument("assignment_name", metavar="ASSIGNMENT", required=False)
|
|
175
|
+
@click.option("--active", is_flag=True, help="download from all active assignments in all active courses")
|
|
176
|
+
@click.option("--until-window", default=24, show_default=True,
|
|
177
|
+
help="hours after the until date to still consider an assignment active")
|
|
162
178
|
@click.option("--target-dir", help="directory to download submissions to", default='./submissions', show_default=True)
|
|
163
179
|
@click.option("--include-commented", is_flag=True,
|
|
164
180
|
help="even download submissionsthat already have codeval comments since last submission")
|
|
@@ -168,17 +184,78 @@ def evaluate_submissions(codeval_dir, submissions_dir):
|
|
|
168
184
|
@click.option("--codeval-prefix", help="prefix for codeval comments", default="codeval: ", show_default=True)
|
|
169
185
|
@click.option("--include-empty", is_flag=True, help="include empty submissions")
|
|
170
186
|
@click.option("--for-name", help="only download submissions for this student name")
|
|
171
|
-
def download_submissions(course_name, assignment_name,
|
|
172
|
-
uncommented_for, for_name):
|
|
187
|
+
def download_submissions(course_name, assignment_name, active, until_window, target_dir, include_commented,
|
|
188
|
+
codeval_prefix, include_empty, uncommented_for, for_name):
|
|
173
189
|
"""
|
|
174
190
|
Download submissions for a given assignment in a course from Canvas.
|
|
175
191
|
|
|
176
|
-
|
|
192
|
+
The COURSE and ASSIGNMENT arguments can be partial names.
|
|
193
|
+
|
|
194
|
+
Use --active to download from all active assignments in all active courses
|
|
195
|
+
(COURSE and ASSIGNMENT are not required when --active is used).
|
|
177
196
|
"""
|
|
197
|
+
if not active and (not course_name or not assignment_name):
|
|
198
|
+
raise click.UsageError("COURSE and ASSIGNMENT are required unless --active is specified")
|
|
199
|
+
|
|
178
200
|
(canvas, user) = connect_to_canvas()
|
|
179
201
|
|
|
202
|
+
if active:
|
|
203
|
+
# Load codeval directory from config
|
|
204
|
+
parser = ConfigParser()
|
|
205
|
+
config_file = click.get_app_dir("codeval.ini")
|
|
206
|
+
parser.read(config_file)
|
|
207
|
+
if 'CODEVAL' not in parser or 'directory' not in parser['CODEVAL']:
|
|
208
|
+
raise click.UsageError(f"[CODEVAL] section with directory= is required in {config_file}")
|
|
209
|
+
codeval_dir = parser['CODEVAL']['directory']
|
|
210
|
+
|
|
211
|
+
courses = get_courses(canvas, course_name or "", is_active=True)
|
|
212
|
+
if not courses:
|
|
213
|
+
error("no active courses found")
|
|
214
|
+
return
|
|
215
|
+
|
|
216
|
+
now = datetime.now(timezone.utc)
|
|
217
|
+
for course in courses:
|
|
218
|
+
for assignment in course.get_assignments():
|
|
219
|
+
if assignment_name and assignment_name.lower() not in despace(assignment.name).lower():
|
|
220
|
+
continue
|
|
221
|
+
# Check if assignment is active: availability date passed and until date + window not passed
|
|
222
|
+
unlock_at = getattr(assignment, 'unlock_at_date', None)
|
|
223
|
+
lock_at = getattr(assignment, 'lock_at_date', None)
|
|
224
|
+
if unlock_at and unlock_at > now:
|
|
225
|
+
continue # not yet available
|
|
226
|
+
if lock_at:
|
|
227
|
+
window = timedelta(hours=until_window)
|
|
228
|
+
if lock_at + window < now:
|
|
229
|
+
continue # past the until window
|
|
230
|
+
|
|
231
|
+
# Filter by submission type: only text_entry (GitHub) or file upload assignments
|
|
232
|
+
submission_types = getattr(assignment, 'submission_types', [])
|
|
233
|
+
if "online_text_entry" not in submission_types and "online_upload" not in submission_types:
|
|
234
|
+
continue
|
|
235
|
+
|
|
236
|
+
# Only download if a corresponding codeval file exists
|
|
237
|
+
codeval_file = os.path.join(codeval_dir, f"{despace(assignment.name)}.codeval")
|
|
238
|
+
if not os.path.exists(codeval_file):
|
|
239
|
+
continue
|
|
240
|
+
|
|
241
|
+
info(f"downloading submissions for {course.name}: {assignment.name}")
|
|
242
|
+
_download_assignment_submissions(
|
|
243
|
+
course, assignment, target_dir, include_commented, codeval_prefix,
|
|
244
|
+
include_empty, uncommented_for, for_name
|
|
245
|
+
)
|
|
246
|
+
return
|
|
247
|
+
|
|
180
248
|
course = get_course(canvas, course_name)
|
|
181
249
|
assignment = get_assignment(course, assignment_name)
|
|
250
|
+
_download_assignment_submissions(
|
|
251
|
+
course, assignment, target_dir, include_commented, codeval_prefix,
|
|
252
|
+
include_empty, uncommented_for, for_name
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _download_assignment_submissions(course, assignment, target_dir, include_commented, codeval_prefix,
|
|
257
|
+
include_empty, uncommented_for, for_name):
|
|
258
|
+
"""Download submissions for a single assignment."""
|
|
182
259
|
submission_dir = os.path.join(target_dir, despace(course.name), despace(assignment.name))
|
|
183
260
|
os.makedirs(submission_dir, exist_ok=True)
|
|
184
261
|
|
|
@@ -221,6 +298,18 @@ attempt={submission.attempt}
|
|
|
221
298
|
late={submission.late}
|
|
222
299
|
date={submission.submitted_at}
|
|
223
300
|
last_comment={last_comment_date}""", file=fd)
|
|
301
|
+
|
|
302
|
+
# Save the last comment (any comment, not just codeval ones) to last-comment.txt
|
|
303
|
+
if submission.submission_comments:
|
|
304
|
+
all_comments = sorted(submission.submission_comments, key=lambda c: c['created_at'])
|
|
305
|
+
last_comment = all_comments[-1]
|
|
306
|
+
last_comment_path = os.path.join(student_submission_dir, "last-comment.txt")
|
|
307
|
+
with open(last_comment_path, "w") as fd:
|
|
308
|
+
print(f"date={last_comment['created_at']}", file=fd)
|
|
309
|
+
print(f"author={last_comment.get('author_name', 'unknown')}", file=fd)
|
|
310
|
+
print("", file=fd)
|
|
311
|
+
print(last_comment.get('comment', ''), file=fd)
|
|
312
|
+
|
|
224
313
|
body = submission.body
|
|
225
314
|
if body:
|
|
226
315
|
filepath = os.path.join(student_submission_dir, "content.txt")
|
|
@@ -246,3 +335,46 @@ def get_submissions_by_id(assignment):
|
|
|
246
335
|
student_id = str(submission.user_id)
|
|
247
336
|
submissions_by_id[student_id] = submission
|
|
248
337
|
return submissions_by_id
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
@click.command()
|
|
341
|
+
@click.argument("course_name", metavar="COURSE", required=False)
|
|
342
|
+
def list_codeval_assignments(course_name):
|
|
343
|
+
"""
|
|
344
|
+
List all assignments that have corresponding codeval files.
|
|
345
|
+
|
|
346
|
+
Optionally filter by COURSE (partial name match).
|
|
347
|
+
Reports errors for codeval files that don't match any assignment.
|
|
348
|
+
"""
|
|
349
|
+
# Load codeval directory from config
|
|
350
|
+
parser = ConfigParser()
|
|
351
|
+
config_file = click.get_app_dir("codeval.ini")
|
|
352
|
+
parser.read(config_file)
|
|
353
|
+
if 'CODEVAL' not in parser or 'directory' not in parser['CODEVAL']:
|
|
354
|
+
raise click.UsageError(f"[CODEVAL] section with directory= is required in {config_file}")
|
|
355
|
+
codeval_dir = parser['CODEVAL']['directory']
|
|
356
|
+
|
|
357
|
+
if not os.path.isdir(codeval_dir):
|
|
358
|
+
raise click.UsageError(f"CODEVAL directory does not exist: {codeval_dir}")
|
|
359
|
+
|
|
360
|
+
# Get all codeval files in the directory
|
|
361
|
+
codeval_files = {f[:-8] for f in os.listdir(codeval_dir) if f.endswith('.codeval')}
|
|
362
|
+
matched_codeval_files = set()
|
|
363
|
+
|
|
364
|
+
(canvas, user) = connect_to_canvas()
|
|
365
|
+
courses = get_courses(canvas, course_name or "", is_active=True)
|
|
366
|
+
if not courses:
|
|
367
|
+
error("no active courses found")
|
|
368
|
+
return
|
|
369
|
+
|
|
370
|
+
for course in courses:
|
|
371
|
+
for assignment in course.get_assignments():
|
|
372
|
+
assignment_key = despace(assignment.name)
|
|
373
|
+
if assignment_key in codeval_files:
|
|
374
|
+
matched_codeval_files.add(assignment_key)
|
|
375
|
+
info(f"{course.name}: {assignment.name}")
|
|
376
|
+
|
|
377
|
+
# Report codeval files that don't match any assignment
|
|
378
|
+
unmatched = codeval_files - matched_codeval_files
|
|
379
|
+
for codeval_name in sorted(unmatched):
|
|
380
|
+
error(f"codeval file has no matching assignment: {codeval_name}.codeval")
|
{assignment_codeval-0.0.14 → assignment_codeval-0.0.16}/src/assignment_codeval.egg-info/SOURCES.txt
RENAMED
|
@@ -10,6 +10,7 @@ src/assignment_codeval/create_assignment.py
|
|
|
10
10
|
src/assignment_codeval/evaluate.py
|
|
11
11
|
src/assignment_codeval/file_utils.py
|
|
12
12
|
src/assignment_codeval/github_connect.py
|
|
13
|
+
src/assignment_codeval/install_assignment.py
|
|
13
14
|
src/assignment_codeval/submissions.py
|
|
14
15
|
src/assignment_codeval.egg-info/PKG-INFO
|
|
15
16
|
src/assignment_codeval.egg-info/SOURCES.txt
|
|
@@ -18,4 +19,6 @@ src/assignment_codeval.egg-info/entry_points.txt
|
|
|
18
19
|
src/assignment_codeval.egg-info/requires.txt
|
|
19
20
|
src/assignment_codeval.egg-info/top_level.txt
|
|
20
21
|
tests/test_codeval.py
|
|
21
|
-
tests/test_create_assignment.py
|
|
22
|
+
tests/test_create_assignment.py
|
|
23
|
+
tests/test_evaluate_submissions.py
|
|
24
|
+
tests/test_install_assignment.py
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""Tests for evaluate_submissions command."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import tempfile
|
|
5
|
+
import zipfile
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TestGitHubSubmissionWorkingDir:
|
|
11
|
+
"""Tests for GitHub submission working directory detection."""
|
|
12
|
+
|
|
13
|
+
def test_zip_extracted_to_assignment_dir_for_github(self):
|
|
14
|
+
"""Verify zip files are extracted to assignment directory for GitHub submissions.
|
|
15
|
+
|
|
16
|
+
When a GitHub submission exists (has .git directory) and no CD tag is specified,
|
|
17
|
+
zip files should be extracted to the assignment_name subdirectory, not the root.
|
|
18
|
+
"""
|
|
19
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
20
|
+
# Setup directory structure:
|
|
21
|
+
# tmpdir/
|
|
22
|
+
# codeval/
|
|
23
|
+
# TestAssignment.codeval
|
|
24
|
+
# support.zip
|
|
25
|
+
# submissions/
|
|
26
|
+
# Course/
|
|
27
|
+
# TestAssignment/
|
|
28
|
+
# 12345/
|
|
29
|
+
# submission/
|
|
30
|
+
# TestAssignment/
|
|
31
|
+
# .git/
|
|
32
|
+
# main.py
|
|
33
|
+
|
|
34
|
+
codeval_dir = os.path.join(tmpdir, "codeval")
|
|
35
|
+
submissions_dir = os.path.join(tmpdir, "submissions")
|
|
36
|
+
os.makedirs(codeval_dir)
|
|
37
|
+
|
|
38
|
+
assignment_name = "TestAssignment"
|
|
39
|
+
student_dir = os.path.join(submissions_dir, "Course", assignment_name, "12345")
|
|
40
|
+
submission_dir = os.path.join(student_dir, "submission")
|
|
41
|
+
git_repo_dir = os.path.join(submission_dir, assignment_name)
|
|
42
|
+
os.makedirs(os.path.join(git_repo_dir, ".git"))
|
|
43
|
+
|
|
44
|
+
# Create a dummy file in the git repo
|
|
45
|
+
with open(os.path.join(git_repo_dir, "main.py"), "w") as f:
|
|
46
|
+
f.write("print('hello')\n")
|
|
47
|
+
|
|
48
|
+
# Create a zip file with a helper file
|
|
49
|
+
zip_path = os.path.join(codeval_dir, "support.zip")
|
|
50
|
+
with zipfile.ZipFile(zip_path, 'w') as zf:
|
|
51
|
+
zf.writestr("helper.txt", "helper content")
|
|
52
|
+
|
|
53
|
+
# Create codeval file with Z tag but NO CD tag
|
|
54
|
+
codeval_path = os.path.join(codeval_dir, f"{assignment_name}.codeval")
|
|
55
|
+
with open(codeval_path, "w") as f:
|
|
56
|
+
f.write("Z support.zip\n")
|
|
57
|
+
f.write("T echo test\n")
|
|
58
|
+
f.write("O test\n")
|
|
59
|
+
|
|
60
|
+
# Parse the codeval file and check where zip would be extracted
|
|
61
|
+
# This simulates what evaluate_submissions does
|
|
62
|
+
from assignment_codeval.submissions import evaluate_submissions
|
|
63
|
+
from zipfile import ZipFile
|
|
64
|
+
|
|
65
|
+
# Manually trace through the logic to verify the bug
|
|
66
|
+
assignment_working_dir = "."
|
|
67
|
+
has_cd_tag = False
|
|
68
|
+
zip_files = []
|
|
69
|
+
|
|
70
|
+
with open(codeval_path, "r") as fd:
|
|
71
|
+
for line in fd:
|
|
72
|
+
line = line.strip()
|
|
73
|
+
if line.startswith("CD"):
|
|
74
|
+
has_cd_tag = True
|
|
75
|
+
if line.startswith("Z"):
|
|
76
|
+
zip_files.append(line.split(None, 1)[1])
|
|
77
|
+
|
|
78
|
+
# Check for GitHub submission BEFORE extracting
|
|
79
|
+
is_github = os.path.exists(os.path.join(submission_dir, assignment_name, ".git"))
|
|
80
|
+
assert is_github, "Test setup: .git directory should exist"
|
|
81
|
+
|
|
82
|
+
if not has_cd_tag and is_github:
|
|
83
|
+
assignment_working_dir = assignment_name
|
|
84
|
+
|
|
85
|
+
# Now extract zip files to the correct location
|
|
86
|
+
for zf_name in zip_files:
|
|
87
|
+
with ZipFile(os.path.join(codeval_dir, zf_name)) as zf:
|
|
88
|
+
dest_dir = os.path.join(submission_dir, assignment_working_dir)
|
|
89
|
+
zf.extractall(dest_dir)
|
|
90
|
+
|
|
91
|
+
# Verify helper.txt was extracted to the assignment directory, not root
|
|
92
|
+
correct_path = os.path.join(submission_dir, assignment_name, "helper.txt")
|
|
93
|
+
wrong_path = os.path.join(submission_dir, "helper.txt")
|
|
94
|
+
|
|
95
|
+
assert os.path.exists(correct_path), \
|
|
96
|
+
f"helper.txt should be at {correct_path}"
|
|
97
|
+
assert not os.path.exists(wrong_path), \
|
|
98
|
+
f"helper.txt should NOT be at {wrong_path} (root of submission_dir)"
|
|
99
|
+
|
|
100
|
+
def test_zip_extracted_to_root_when_cd_tag_present(self):
|
|
101
|
+
"""Verify zip files respect CD tag even for GitHub submissions."""
|
|
102
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
103
|
+
codeval_dir = os.path.join(tmpdir, "codeval")
|
|
104
|
+
submissions_dir = os.path.join(tmpdir, "submissions")
|
|
105
|
+
os.makedirs(codeval_dir)
|
|
106
|
+
|
|
107
|
+
assignment_name = "TestAssignment"
|
|
108
|
+
student_dir = os.path.join(submissions_dir, "Course", assignment_name, "12345")
|
|
109
|
+
submission_dir = os.path.join(student_dir, "submission")
|
|
110
|
+
|
|
111
|
+
# Create custom directory specified by CD tag
|
|
112
|
+
custom_dir = os.path.join(submission_dir, "custom_dir")
|
|
113
|
+
os.makedirs(os.path.join(custom_dir, ".git"))
|
|
114
|
+
|
|
115
|
+
# Create codeval file WITH CD tag
|
|
116
|
+
codeval_path = os.path.join(codeval_dir, f"{assignment_name}.codeval")
|
|
117
|
+
with open(codeval_path, "w") as f:
|
|
118
|
+
f.write("CD custom_dir\n")
|
|
119
|
+
f.write("Z support.zip\n")
|
|
120
|
+
f.write("T echo test\n")
|
|
121
|
+
f.write("O test\n")
|
|
122
|
+
|
|
123
|
+
# Create zip file
|
|
124
|
+
zip_path = os.path.join(codeval_dir, "support.zip")
|
|
125
|
+
with zipfile.ZipFile(zip_path, 'w') as zf:
|
|
126
|
+
zf.writestr("helper.txt", "helper content")
|
|
127
|
+
|
|
128
|
+
# Trace through logic
|
|
129
|
+
from zipfile import ZipFile
|
|
130
|
+
|
|
131
|
+
assignment_working_dir = "."
|
|
132
|
+
has_cd_tag = False
|
|
133
|
+
zip_files = []
|
|
134
|
+
|
|
135
|
+
with open(codeval_path, "r") as fd:
|
|
136
|
+
for line in fd:
|
|
137
|
+
line = line.strip()
|
|
138
|
+
if line.startswith("CD"):
|
|
139
|
+
has_cd_tag = True
|
|
140
|
+
assignment_working_dir = os.path.normpath(
|
|
141
|
+
os.path.join(assignment_working_dir, line.split()[1].strip()))
|
|
142
|
+
if line.startswith("Z"):
|
|
143
|
+
zip_files.append(line.split(None, 1)[1])
|
|
144
|
+
|
|
145
|
+
# CD tag should take precedence
|
|
146
|
+
assert has_cd_tag
|
|
147
|
+
assert assignment_working_dir == "custom_dir"
|
|
148
|
+
|
|
149
|
+
# Extract zip files
|
|
150
|
+
for zf_name in zip_files:
|
|
151
|
+
with ZipFile(os.path.join(codeval_dir, zf_name)) as zf:
|
|
152
|
+
dest_dir = os.path.join(submission_dir, assignment_working_dir)
|
|
153
|
+
zf.extractall(dest_dir)
|
|
154
|
+
|
|
155
|
+
# Verify helper.txt was extracted to custom_dir
|
|
156
|
+
correct_path = os.path.join(submission_dir, "custom_dir", "helper.txt")
|
|
157
|
+
assert os.path.exists(correct_path), \
|
|
158
|
+
f"helper.txt should be at {correct_path}"
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""Tests for install_assignment command."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import tempfile
|
|
5
|
+
import zipfile
|
|
6
|
+
from unittest.mock import patch, MagicMock
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
from click.testing import CliRunner
|
|
10
|
+
|
|
11
|
+
from assignment_codeval.install_assignment import (
|
|
12
|
+
install_assignment,
|
|
13
|
+
is_remote_destination,
|
|
14
|
+
parse_z_tags,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class TestIsRemoteDestination:
|
|
19
|
+
"""Tests for is_remote_destination function."""
|
|
20
|
+
|
|
21
|
+
def test_local_path(self):
|
|
22
|
+
assert is_remote_destination('/home/user/path') is False
|
|
23
|
+
|
|
24
|
+
def test_relative_path(self):
|
|
25
|
+
assert is_remote_destination('relative/path') is False
|
|
26
|
+
|
|
27
|
+
def test_remote_path(self):
|
|
28
|
+
assert is_remote_destination('server:/home/user/path') is True
|
|
29
|
+
|
|
30
|
+
def test_remote_path_with_user(self):
|
|
31
|
+
assert is_remote_destination('user@server:/path') is True
|
|
32
|
+
|
|
33
|
+
def test_windows_drive_letter(self):
|
|
34
|
+
assert is_remote_destination('C:\\Users\\path') is False
|
|
35
|
+
|
|
36
|
+
def test_windows_drive_lowercase(self):
|
|
37
|
+
assert is_remote_destination('c:\\Users\\path') is False
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class TestParseZTags:
|
|
41
|
+
"""Tests for parse_z_tags function."""
|
|
42
|
+
|
|
43
|
+
def test_no_z_tags(self):
|
|
44
|
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.codeval', delete=False) as f:
|
|
45
|
+
f.write('C gcc -o test test.c\n')
|
|
46
|
+
f.write('T ./test\n')
|
|
47
|
+
f.name
|
|
48
|
+
try:
|
|
49
|
+
result = parse_z_tags(f.name)
|
|
50
|
+
assert result == []
|
|
51
|
+
finally:
|
|
52
|
+
os.unlink(f.name)
|
|
53
|
+
|
|
54
|
+
def test_single_z_tag(self):
|
|
55
|
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.codeval', delete=False) as f:
|
|
56
|
+
f.write('Z support.zip\n')
|
|
57
|
+
f.write('C gcc -o test test.c\n')
|
|
58
|
+
try:
|
|
59
|
+
result = parse_z_tags(f.name)
|
|
60
|
+
assert result == ['support.zip']
|
|
61
|
+
finally:
|
|
62
|
+
os.unlink(f.name)
|
|
63
|
+
|
|
64
|
+
def test_multiple_z_tags(self):
|
|
65
|
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.codeval', delete=False) as f:
|
|
66
|
+
f.write('Z first.zip\n')
|
|
67
|
+
f.write('Z second.zip\n')
|
|
68
|
+
f.write('C gcc -o test test.c\n')
|
|
69
|
+
try:
|
|
70
|
+
result = parse_z_tags(f.name)
|
|
71
|
+
assert result == ['first.zip', 'second.zip']
|
|
72
|
+
finally:
|
|
73
|
+
os.unlink(f.name)
|
|
74
|
+
|
|
75
|
+
def test_z_tag_with_whitespace(self):
|
|
76
|
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.codeval', delete=False) as f:
|
|
77
|
+
f.write('Z support.zip \n')
|
|
78
|
+
try:
|
|
79
|
+
result = parse_z_tags(f.name)
|
|
80
|
+
assert result == ['support.zip']
|
|
81
|
+
finally:
|
|
82
|
+
os.unlink(f.name)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class TestInstallAssignmentCommand:
|
|
86
|
+
"""Tests for the install_assignment CLI command."""
|
|
87
|
+
|
|
88
|
+
def test_local_install_codeval_only(self):
|
|
89
|
+
runner = CliRunner()
|
|
90
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
91
|
+
# Create a codeval file
|
|
92
|
+
codeval_path = os.path.join(tmpdir, 'test.codeval')
|
|
93
|
+
with open(codeval_path, 'w') as f:
|
|
94
|
+
f.write('C gcc -o test test.c\n')
|
|
95
|
+
f.write('T ./test\n')
|
|
96
|
+
|
|
97
|
+
# Create destination directory
|
|
98
|
+
dest_dir = os.path.join(tmpdir, 'dest')
|
|
99
|
+
os.makedirs(dest_dir)
|
|
100
|
+
|
|
101
|
+
result = runner.invoke(install_assignment, [codeval_path, dest_dir])
|
|
102
|
+
assert result.exit_code == 0
|
|
103
|
+
assert 'No zip files referenced' in result.output
|
|
104
|
+
assert 'Done' in result.output
|
|
105
|
+
assert os.path.exists(os.path.join(dest_dir, 'test.codeval'))
|
|
106
|
+
|
|
107
|
+
def test_local_install_with_zip(self):
|
|
108
|
+
runner = CliRunner()
|
|
109
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
110
|
+
# Create a zip file
|
|
111
|
+
zip_path = os.path.join(tmpdir, 'support.zip')
|
|
112
|
+
with zipfile.ZipFile(zip_path, 'w') as zf:
|
|
113
|
+
zf.writestr('helper.txt', 'test content')
|
|
114
|
+
|
|
115
|
+
# Create a codeval file referencing the zip
|
|
116
|
+
codeval_path = os.path.join(tmpdir, 'test.codeval')
|
|
117
|
+
with open(codeval_path, 'w') as f:
|
|
118
|
+
f.write('Z support.zip\n')
|
|
119
|
+
f.write('C gcc -o test test.c\n')
|
|
120
|
+
|
|
121
|
+
# Create destination directory
|
|
122
|
+
dest_dir = os.path.join(tmpdir, 'dest')
|
|
123
|
+
os.makedirs(dest_dir)
|
|
124
|
+
|
|
125
|
+
result = runner.invoke(install_assignment, [codeval_path, dest_dir])
|
|
126
|
+
assert result.exit_code == 0
|
|
127
|
+
assert 'support.zip' in result.output
|
|
128
|
+
assert 'Done' in result.output
|
|
129
|
+
assert os.path.exists(os.path.join(dest_dir, 'test.codeval'))
|
|
130
|
+
assert os.path.exists(os.path.join(dest_dir, 'support.zip'))
|
|
131
|
+
|
|
132
|
+
def test_missing_zip_warning(self):
|
|
133
|
+
runner = CliRunner()
|
|
134
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
135
|
+
# Create a codeval file referencing a non-existent zip
|
|
136
|
+
codeval_path = os.path.join(tmpdir, 'test.codeval')
|
|
137
|
+
with open(codeval_path, 'w') as f:
|
|
138
|
+
f.write('Z missing.zip\n')
|
|
139
|
+
f.write('C gcc -o test test.c\n')
|
|
140
|
+
|
|
141
|
+
# Create destination directory
|
|
142
|
+
dest_dir = os.path.join(tmpdir, 'dest')
|
|
143
|
+
os.makedirs(dest_dir)
|
|
144
|
+
|
|
145
|
+
result = runner.invoke(install_assignment, [codeval_path, dest_dir])
|
|
146
|
+
assert result.exit_code == 0
|
|
147
|
+
assert 'Warning: Referenced zip file not found' in result.output
|
|
148
|
+
|
|
149
|
+
def test_verbose_output(self):
|
|
150
|
+
runner = CliRunner()
|
|
151
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
152
|
+
codeval_path = os.path.join(tmpdir, 'test.codeval')
|
|
153
|
+
with open(codeval_path, 'w') as f:
|
|
154
|
+
f.write('C gcc -o test test.c\n')
|
|
155
|
+
|
|
156
|
+
dest_dir = os.path.join(tmpdir, 'dest')
|
|
157
|
+
os.makedirs(dest_dir)
|
|
158
|
+
|
|
159
|
+
result = runner.invoke(install_assignment, [codeval_path, dest_dir, '--verbose'])
|
|
160
|
+
assert result.exit_code == 0
|
|
161
|
+
assert 'Copying' in result.output
|
|
162
|
+
|
|
163
|
+
@patch('assignment_codeval.install_assignment.subprocess.run')
|
|
164
|
+
def test_remote_destination_detected(self, mock_run):
|
|
165
|
+
# Mock successful scp
|
|
166
|
+
mock_run.return_value = MagicMock(returncode=0)
|
|
167
|
+
|
|
168
|
+
runner = CliRunner()
|
|
169
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
170
|
+
codeval_path = os.path.join(tmpdir, 'test.codeval')
|
|
171
|
+
with open(codeval_path, 'w') as f:
|
|
172
|
+
f.write('C gcc -o test test.c\n')
|
|
173
|
+
|
|
174
|
+
result = runner.invoke(install_assignment, [codeval_path, 'server:/path'])
|
|
175
|
+
assert result.exit_code == 0
|
|
176
|
+
assert 'Using scp for remote copy' in result.output
|
|
177
|
+
# Verify scp was called
|
|
178
|
+
mock_run.assert_called()
|
|
179
|
+
|
|
180
|
+
def test_nonexistent_codeval_file(self):
|
|
181
|
+
runner = CliRunner()
|
|
182
|
+
result = runner.invoke(install_assignment, ['/nonexistent/file.codeval', '/dest'])
|
|
183
|
+
assert result.exit_code != 0
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{assignment_codeval-0.0.14 → assignment_codeval-0.0.16}/src/assignment_codeval/ai_benchmark.py
RENAMED
|
File without changes
|
{assignment_codeval-0.0.14 → assignment_codeval-0.0.16}/src/assignment_codeval/canvas_utils.py
RENAMED
|
File without changes
|
|
File without changes
|
{assignment_codeval-0.0.14 → assignment_codeval-0.0.16}/src/assignment_codeval/convertMD2Html.py
RENAMED
|
File without changes
|
{assignment_codeval-0.0.14 → assignment_codeval-0.0.16}/src/assignment_codeval/create_assignment.py
RENAMED
|
File without changes
|
|
File without changes
|
{assignment_codeval-0.0.14 → assignment_codeval-0.0.16}/src/assignment_codeval/file_utils.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{assignment_codeval-0.0.14 → assignment_codeval-0.0.16}/src/assignment_codeval.egg-info/requires.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|