Skip to content

Commit 5d44d18

Browse files
add tests for Submissions app
1 parent 401ca0a commit 5d44d18

File tree

6 files changed

+229
-60
lines changed

6 files changed

+229
-60
lines changed

pyproject.toml

+5
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ norecursedirs = ["media", "migrations", "sandboxing"]
2626
testpaths = "tin"
2727
addopts = "--doctest-modules tin --import-mode=importlib -n 8"
2828
doctest_optionflags = "NORMALIZE_WHITESPACE NUMBER"
29+
filterwarnings = [
30+
"error",
31+
'ignore:.*Tin is using the dummy sandboxing module. This is insecure.:',
32+
"ignore::DeprecationWarning:twisted.*:",
33+
]
2934

3035
[tool.coverage.run]
3136
branch = true

tin/apps/submissions/tests.py

-55
This file was deleted.

tin/apps/submissions/tests/__init__.py

Whitespace-only changes.
+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
from __future__ import annotations
2+
3+
from pathlib import Path
4+
5+
from ..models import Submission, upload_submission_file_path
6+
7+
8+
def test_submission_save_file(settings, submission: Submission):
9+
file_path = upload_submission_file_path(submission, "")
10+
submission.save_file("something")
11+
assert submission.file.name == file_path
12+
13+
submission_path = submission.file_path
14+
assert submission_path is not None
15+
submission_path = Path(submission_path)
16+
assert submission_path == Path(settings.MEDIA_ROOT) / file_path
17+
assert submission_path.exists()
18+
19+
20+
def test_make_submission_backup(submission: Submission):
21+
submission.create_backup_copy("HI")
22+
backup_file = submission.backup_file_path
23+
assert backup_file is not None
24+
backup_path = Path(backup_file)
25+
assert backup_path.exists()
26+
assert backup_path.read_text("utf-8") == "HI"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
from __future__ import annotations
2+
3+
import json
4+
import os
5+
from typing import TYPE_CHECKING
6+
7+
import psutil
8+
import pytest
9+
from django.urls import reverse
10+
from django.utils import timezone
11+
12+
from tin.tests import is_redirect, login
13+
14+
if TYPE_CHECKING:
15+
from django.contrib.auth.models import AbstractBaseUser
16+
from django.test import Client
17+
18+
from ...assignments.models import Assignment
19+
from ...courses.models import Course
20+
from ..models import Submission
21+
22+
23+
@login("student")
24+
@pytest.mark.parametrize(
25+
("perm", "hidden", "archived"),
26+
(
27+
# normal
28+
("-", False, False),
29+
("r", False, False),
30+
("w", False, False),
31+
# archived
32+
("-", True, True),
33+
("r", False, True),
34+
("w", False, True),
35+
),
36+
)
37+
def test_see_submission_after_archived(
38+
client: Client, course: Course, submission: Submission, perm: str, hidden: bool, archived: bool
39+
):
40+
course.permission = perm
41+
course.archived = archived
42+
course.save()
43+
44+
response = client.get(reverse("submissions:show", args=[submission.id]))
45+
assert (response.status_code == 404) is hidden
46+
47+
48+
@login("student")
49+
def test_student_requests_kill(client: Client, submission: Submission):
50+
response = client.post(reverse("submissions:kill", args=[submission.id]))
51+
submission.refresh_from_db()
52+
assert is_redirect(response)
53+
assert submission.kill_requested
54+
55+
56+
@login("teacher")
57+
def test_teacher_requests_kill(client: Client, submission: Submission):
58+
response = client.post(reverse("submissions:kill", args=[submission.id]))
59+
submission.refresh_from_db()
60+
assert is_redirect(response)
61+
assert submission.kill_requested
62+
63+
64+
@login("student")
65+
def test_jsonapi_exists(client: Client, submission: Submission):
66+
response = client.get(reverse("submissions:show_json", args=[submission.id]))
67+
data = json.loads(response.content)
68+
assert isinstance(data, dict)
69+
70+
# a nonexistent submission
71+
response = client.get(reverse("submissions:show_json", args=[1000000]))
72+
data = json.loads(response.content)
73+
assert data == {"error": "Submission not found"}
74+
75+
76+
@login("student")
77+
@pytest.mark.parametrize("language", ("P", "J"))
78+
def test_download_submission(
79+
client: Client, assignment: Assignment, student: AbstractBaseUser, language: str
80+
):
81+
extension = "py" if language == "P" else "java"
82+
assignment.filename = f"main.{extension}"
83+
assignment.save()
84+
85+
submission = assignment.submissions.create(student=student)
86+
# Yes this isn't valid Java ;)
87+
code = "print('Hello World!')"
88+
submission.save_file(code)
89+
90+
response = client.get(reverse("submissions:download", args=[submission.id]))
91+
92+
assert (
93+
response["Content-Disposition"] == f'attachment; filename="{student.username}.{extension}"'
94+
)
95+
assert response.content.decode("utf-8") == submission.file_text_with_header
96+
97+
98+
@login("teacher")
99+
def test_comments(client: Client, teacher: AbstractBaseUser, submission: Submission):
100+
submission.complete = True
101+
submission.has_been_graded = True
102+
submission.save()
103+
104+
# create comment
105+
response = client.post(
106+
reverse("submissions:comment", args=[submission.id]),
107+
{"comment": "HiABC", "point_override": "1.0"},
108+
)
109+
assert is_redirect(response)
110+
comments = submission.comments.filter(author=teacher).all()
111+
assert len(comments) == 1
112+
comment = comments[0]
113+
assert comment.text == "HiABC"
114+
115+
# edit the comment
116+
response = client.post(
117+
reverse("submissions:edit_comment", args=[submission.id, comment.id]),
118+
{"text": "Hello", "point_override": "1.0"},
119+
)
120+
assert is_redirect(response)
121+
comment.refresh_from_db()
122+
assert comment.text == "Hello"
123+
124+
# now delete it
125+
response = client.post(reverse("submissions:delete_comment", args=[submission.id, comment.id]))
126+
assert is_redirect(response)
127+
assert not submission.comments.filter(author=teacher).exists()
128+
129+
130+
@login("teacher")
131+
def test_public_comment(client: Client, submission: Submission):
132+
client.post(reverse("submissions:publish", args=[submission.id]))
133+
assert submission.published_submission is not None
134+
135+
client.post(reverse("submissions:unpublish", args=[submission.id]))
136+
assert submission.published_submission is None
137+
138+
139+
@login("admin")
140+
@pytest.mark.skipif(
141+
psutil.pid_exists(2**22 + 1), reason="PID exists, so cannot check if it does not exist"
142+
)
143+
def test_set_aborted_complete_invalid_pid(client: Client, submission: Submission):
144+
submission.complete = False
145+
# on linux x64, 2^22 is the max PID so 2^22+1 should always not exist
146+
submission.grader_pid = 2**22 + 1
147+
submission.save()
148+
149+
client.post(reverse("submissions:set_aborted_complete"))
150+
submission.refresh_from_db()
151+
assert submission.complete, "Should mark submission as complete if process has ended"
152+
153+
154+
def test_set_aborted_complete_valid_pid(client: Client, submission: Submission):
155+
submission.complete = False
156+
submission.grader_pid = os.getpid() # this PID exists
157+
submission.save()
158+
159+
client.post(reverse("submissions:set_aborted_complete"))
160+
assert not submission.complete, "Should not mark submission as complete while running"
161+
162+
163+
@login("admin")
164+
def test_set_past_timeout_complete_view(
165+
client: Client, assignment: Assignment, submission: Submission
166+
):
167+
assignment.enable_grader_timeout = True
168+
assignment.grader_timeout = 0
169+
assignment.save()
170+
submission.complete = False
171+
submission.grader_start_time = 0
172+
submission.save()
173+
174+
client.post(reverse("submissions:set_past_timeout_complete"))
175+
submission.refresh_from_db()
176+
177+
assert submission.complete
178+
179+
submission.complete = False
180+
# the difference between the timestamp between now and when the timeout is called
181+
# should be close to 0, much less than the 1e12 grader timeout set
182+
submission.grader_start_time = timezone.localtime().timestamp()
183+
submission.save()
184+
assignment.grader_timeout = 1_000_000_000_000
185+
assignment.save()
186+
187+
client.post(reverse("submissions:set_past_timeout_complete"))
188+
submission.refresh_from_db()
189+
190+
assert not submission.complete

tin/apps/submissions/views.py

+8-5
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,11 @@ def comment_view(request, submission_id):
151151
raise http.Http404
152152

153153
comment = request.POST.get("comment", "")
154-
point_override = request.POST.get("point_override", "")
154+
point_override = request.POST.get("point_override")
155+
156+
if point_override is None:
157+
return http.HttpResponseBadRequest("Missing point_override")
158+
155159
comment = Comment(
156160
submission=submission,
157161
author=request.user,
@@ -329,11 +333,10 @@ def set_aborted_complete_view(request):
329333
submissions = Submission.objects.filter(complete=False, grader_pid__isnull=False)
330334

331335
for submission in submissions:
332-
try:
333-
psutil.Process(submission.grader_pid)
334-
except psutil.NoSuchProcess:
336+
if not psutil.pid_exists(submission.grader_pid):
335337
submission.complete = True
336-
submission.save(update_fields=["complete"])
338+
submission.grader_pid = None
339+
submission.save(update_fields=["complete", "grader_pid"])
337340

338341
return redirect("auth:index")
339342

0 commit comments

Comments
 (0)