assignment-codeval 0.0.15__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.15 → assignment_codeval-0.0.16}/PKG-INFO +1 -1
- {assignment_codeval-0.0.15 → assignment_codeval-0.0.16}/pyproject.toml +1 -1
- {assignment_codeval-0.0.15 → assignment_codeval-0.0.16}/src/assignment_codeval/cli.py +2 -0
- assignment_codeval-0.0.16/src/assignment_codeval/install_assignment.py +143 -0
- {assignment_codeval-0.0.15 → assignment_codeval-0.0.16}/src/assignment_codeval/submissions.py +24 -11
- {assignment_codeval-0.0.15 → assignment_codeval-0.0.16}/src/assignment_codeval.egg-info/PKG-INFO +1 -1
- {assignment_codeval-0.0.15 → 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.15 → assignment_codeval-0.0.16}/README.md +0 -0
- {assignment_codeval-0.0.15 → assignment_codeval-0.0.16}/setup.cfg +0 -0
- {assignment_codeval-0.0.15 → assignment_codeval-0.0.16}/src/assignment_codeval/__init__.py +0 -0
- {assignment_codeval-0.0.15 → assignment_codeval-0.0.16}/src/assignment_codeval/ai_benchmark.py +0 -0
- {assignment_codeval-0.0.15 → assignment_codeval-0.0.16}/src/assignment_codeval/canvas_utils.py +0 -0
- {assignment_codeval-0.0.15 → assignment_codeval-0.0.16}/src/assignment_codeval/commons.py +0 -0
- {assignment_codeval-0.0.15 → assignment_codeval-0.0.16}/src/assignment_codeval/convertMD2Html.py +0 -0
- {assignment_codeval-0.0.15 → assignment_codeval-0.0.16}/src/assignment_codeval/create_assignment.py +0 -0
- {assignment_codeval-0.0.15 → assignment_codeval-0.0.16}/src/assignment_codeval/evaluate.py +0 -0
- {assignment_codeval-0.0.15 → assignment_codeval-0.0.16}/src/assignment_codeval/file_utils.py +0 -0
- {assignment_codeval-0.0.15 → assignment_codeval-0.0.16}/src/assignment_codeval/github_connect.py +0 -0
- {assignment_codeval-0.0.15 → assignment_codeval-0.0.16}/src/assignment_codeval.egg-info/dependency_links.txt +0 -0
- {assignment_codeval-0.0.15 → assignment_codeval-0.0.16}/src/assignment_codeval.egg-info/entry_points.txt +0 -0
- {assignment_codeval-0.0.15 → assignment_codeval-0.0.16}/src/assignment_codeval.egg-info/requires.txt +0 -0
- {assignment_codeval-0.0.15 → assignment_codeval-0.0.16}/src/assignment_codeval.egg-info/top_level.txt +0 -0
- {assignment_codeval-0.0.15 → assignment_codeval-0.0.16}/tests/test_codeval.py +0 -0
- {assignment_codeval-0.0.15 → assignment_codeval-0.0.16}/tests/test_create_assignment.py +0 -0
|
@@ -4,6 +4,7 @@ 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.install_assignment import install_assignment
|
|
7
8
|
from assignment_codeval.submissions import download_submissions, upload_submission_comments, evaluate_submissions, list_codeval_assignments
|
|
8
9
|
|
|
9
10
|
|
|
@@ -18,6 +19,7 @@ cli.add_command(github_setup_repo)
|
|
|
18
19
|
cli.add_command(evaluate_submissions)
|
|
19
20
|
cli.add_command(create_assignment)
|
|
20
21
|
cli.add_command(list_codeval_assignments)
|
|
22
|
+
cli.add_command(install_assignment)
|
|
21
23
|
cli.add_command(get_benchmark_command())
|
|
22
24
|
|
|
23
25
|
if __name__ == "__main__":
|
|
@@ -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.15 → assignment_codeval-0.0.16}/src/assignment_codeval/submissions.py
RENAMED
|
@@ -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")
|
{assignment_codeval-0.0.15 → 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.15 → assignment_codeval-0.0.16}/src/assignment_codeval/ai_benchmark.py
RENAMED
|
File without changes
|
{assignment_codeval-0.0.15 → assignment_codeval-0.0.16}/src/assignment_codeval/canvas_utils.py
RENAMED
|
File without changes
|
|
File without changes
|
{assignment_codeval-0.0.15 → assignment_codeval-0.0.16}/src/assignment_codeval/convertMD2Html.py
RENAMED
|
File without changes
|
{assignment_codeval-0.0.15 → assignment_codeval-0.0.16}/src/assignment_codeval/create_assignment.py
RENAMED
|
File without changes
|
|
File without changes
|
{assignment_codeval-0.0.15 → assignment_codeval-0.0.16}/src/assignment_codeval/file_utils.py
RENAMED
|
File without changes
|
{assignment_codeval-0.0.15 → assignment_codeval-0.0.16}/src/assignment_codeval/github_connect.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{assignment_codeval-0.0.15 → 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
|