From 61ebcd004551d3a8f245623c88b27db208228556 Mon Sep 17 00:00:00 2001 From: JasonGrace2282 Date: Wed, 28 Aug 2024 15:53:21 -0400 Subject: [PATCH] add tests for Submissions app --- pyproject.toml | 5 + tin/apps/submissions/tests.py | 55 ----- tin/apps/submissions/tests/__init__.py | 0 tin/apps/submissions/tests/test_models.py | 26 +++ .../submissions/tests/test_submissions.py | 190 ++++++++++++++++++ tin/apps/submissions/views.py | 13 +- 6 files changed, 229 insertions(+), 60 deletions(-) delete mode 100644 tin/apps/submissions/tests.py create mode 100644 tin/apps/submissions/tests/__init__.py create mode 100644 tin/apps/submissions/tests/test_models.py create mode 100644 tin/apps/submissions/tests/test_submissions.py diff --git a/pyproject.toml b/pyproject.toml index c89edb08..5d3a22e7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,6 +26,11 @@ norecursedirs = ["media", "migrations", "sandboxing"] testpaths = "tin" addopts = "--doctest-modules tin --import-mode=importlib -n 8" doctest_optionflags = "NORMALIZE_WHITESPACE NUMBER" +filterwarnings = [ + "error", + 'ignore:.*Tin is using the dummy sandboxing module. This is insecure.:', + "ignore::DeprecationWarning:twisted.*:", +] [tool.coverage.run] branch = true diff --git a/tin/apps/submissions/tests.py b/tin/apps/submissions/tests.py deleted file mode 100644 index 26be9e29..00000000 --- a/tin/apps/submissions/tests.py +++ /dev/null @@ -1,55 +0,0 @@ -from __future__ import annotations - -from typing import TYPE_CHECKING - -import pytest -from django.urls import reverse - -from tin.tests import is_redirect, login - -if TYPE_CHECKING: - from django.test import Client - - from ..courses.models import Course - from .models import Submission - - -@login("student") -@pytest.mark.parametrize( - ("perm", "hidden", "archived"), - ( - # normal - ("-", False, False), - ("r", False, False), - ("w", False, False), - # archived - ("-", True, True), - ("r", False, True), - ("w", False, True), - ), -) -def test_see_submission_after_archived( - client: Client, course: Course, submission: Submission, perm: str, hidden: bool, archived: bool -): - course.permission = perm - course.archived = archived - course.save() - - response = client.get(reverse("submissions:show", args=[submission.id])) - assert (response.status_code == 404) is hidden - - -@login("student") -def test_student_requests_kill(client: Client, submission: Submission): - response = client.post(reverse("submissions:kill", args=[submission.id])) - submission.refresh_from_db() - assert is_redirect(response) - assert submission.kill_requested - - -@login("teacher") -def test_teacher_requests_kill(client: Client, submission: Submission): - response = client.post(reverse("submissions:kill", args=[submission.id])) - submission.refresh_from_db() - assert is_redirect(response) - assert submission.kill_requested diff --git a/tin/apps/submissions/tests/__init__.py b/tin/apps/submissions/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tin/apps/submissions/tests/test_models.py b/tin/apps/submissions/tests/test_models.py new file mode 100644 index 00000000..bae1f833 --- /dev/null +++ b/tin/apps/submissions/tests/test_models.py @@ -0,0 +1,26 @@ +from __future__ import annotations + +from pathlib import Path + +from ..models import Submission, upload_submission_file_path + + +def test_submission_save_file(settings, submission: Submission): + file_path = upload_submission_file_path(submission, "") + submission.save_file("something") + assert submission.file.name == file_path + + submission_path = submission.file_path + assert submission_path is not None + submission_path = Path(submission_path) + assert submission_path == Path(settings.MEDIA_ROOT) / file_path + assert submission_path.exists() + + +def test_make_submission_backup(submission: Submission): + submission.create_backup_copy("HI") + backup_file = submission.backup_file_path + assert backup_file is not None + backup_path = Path(backup_file) + assert backup_path.exists() + assert backup_path.read_text("utf-8") == "HI" diff --git a/tin/apps/submissions/tests/test_submissions.py b/tin/apps/submissions/tests/test_submissions.py new file mode 100644 index 00000000..673ab9c6 --- /dev/null +++ b/tin/apps/submissions/tests/test_submissions.py @@ -0,0 +1,190 @@ +from __future__ import annotations + +import json +import os +from typing import TYPE_CHECKING + +import psutil +import pytest +from django.urls import reverse +from django.utils import timezone + +from tin.tests import is_redirect, login + +if TYPE_CHECKING: + from django.contrib.auth.models import AbstractBaseUser + from django.test import Client + + from ...assignments.models import Assignment + from ...courses.models import Course + from ..models import Submission + + +@login("student") +@pytest.mark.parametrize( + ("perm", "hidden", "archived"), + ( + # normal + ("-", False, False), + ("r", False, False), + ("w", False, False), + # archived + ("-", True, True), + ("r", False, True), + ("w", False, True), + ), +) +def test_see_submission_after_archived( + client: Client, course: Course, submission: Submission, perm: str, hidden: bool, archived: bool +): + course.permission = perm + course.archived = archived + course.save() + + response = client.get(reverse("submissions:show", args=[submission.id])) + assert (response.status_code == 404) is hidden + + +@login("student") +def test_student_requests_kill(client: Client, submission: Submission): + response = client.post(reverse("submissions:kill", args=[submission.id])) + submission.refresh_from_db() + assert is_redirect(response) + assert submission.kill_requested + + +@login("teacher") +def test_teacher_requests_kill(client: Client, submission: Submission): + response = client.post(reverse("submissions:kill", args=[submission.id])) + submission.refresh_from_db() + assert is_redirect(response) + assert submission.kill_requested + + +@login("student") +def test_jsonapi_exists(client: Client, submission: Submission): + response = client.get(reverse("submissions:show_json", args=[submission.id])) + data = json.loads(response.content) + assert isinstance(data, dict) + + # a nonexistent submission + response = client.get(reverse("submissions:show_json", args=[1000000])) + data = json.loads(response.content) + assert data == {"error": "Submission not found"} + + +@login("student") +@pytest.mark.parametrize("language", ("P", "J")) +def test_download_submission( + client: Client, assignment: Assignment, student: AbstractBaseUser, language: str +): + extension = "py" if language == "P" else "java" + assignment.filename = f"main.{extension}" + assignment.save() + + submission = assignment.submissions.create(student=student) + # Yes this isn't valid Java ;) + code = "print('Hello World!')" + submission.save_file(code) + + response = client.get(reverse("submissions:download", args=[submission.id])) + + assert ( + response["Content-Disposition"] == f'attachment; filename="{student.username}.{extension}"' + ) + assert response.content.decode("utf-8") == submission.file_text_with_header + + +@login("teacher") +def test_comments(client: Client, teacher: AbstractBaseUser, submission: Submission): + submission.complete = True + submission.has_been_graded = True + submission.save() + + # create comment + response = client.post( + reverse("submissions:comment", args=[submission.id]), + {"comment": "HiABC", "point_override": "1.0"}, + ) + assert is_redirect(response) + comments = submission.comments.filter(author=teacher).all() + assert len(comments) == 1 + comment = comments[0] + assert comment.text == "HiABC" + + # edit the comment + response = client.post( + reverse("submissions:edit_comment", args=[submission.id, comment.id]), + {"text": "Hello", "point_override": "1.0"}, + ) + assert is_redirect(response) + comment.refresh_from_db() + assert comment.text == "Hello" + + # now delete it + response = client.post(reverse("submissions:delete_comment", args=[submission.id, comment.id])) + assert is_redirect(response) + assert not submission.comments.filter(author=teacher).exists() + + +@login("teacher") +def test_public_comment(client: Client, submission: Submission): + client.post(reverse("submissions:publish", args=[submission.id])) + assert submission.published_submission is not None + + client.post(reverse("submissions:unpublish", args=[submission.id])) + assert submission.published_submission is None + + +@login("admin") +@pytest.mark.skipif( + psutil.pid_exists(2**22 + 1), reason="PID exists, so cannot check if it does not exist" +) +def test_set_aborted_complete_invalid_pid(client: Client, submission: Submission): + submission.complete = False + # on linux x64, 2^22 is the max PID so 2^22+1 should always not exist + submission.grader_pid = 2**22 + 1 + submission.save() + + client.post(reverse("submissions:set_aborted_complete")) + submission.refresh_from_db() + assert submission.complete, "Should mark submission as complete if process has ended" + + +def test_set_aborted_complete_valid_pid(client: Client, submission: Submission): + submission.complete = False + submission.grader_pid = os.getpid() # this PID exists + submission.save() + + client.post(reverse("submissions:set_aborted_complete")) + assert not submission.complete, "Should not mark submission as complete while running" + + +@login("admin") +def test_set_past_timeout_complete_view( + client: Client, assignment: Assignment, submission: Submission +): + assignment.enable_grader_timeout = True + assignment.grader_timeout = 0 + assignment.save() + submission.complete = False + submission.grader_start_time = 0 + submission.save() + + client.post(reverse("submissions:set_past_timeout_complete")) + submission.refresh_from_db() + + assert submission.complete + + submission.complete = False + # the difference between the timestamp between now and when the timeout is called + # should be close to 0, much less than the 1e12 grader timeout set + submission.grader_start_time = timezone.localtime().timestamp() + submission.save() + assignment.grader_timeout = 1_000_000_000_000 + assignment.save() + + client.post(reverse("submissions:set_past_timeout_complete")) + submission.refresh_from_db() + + assert not submission.complete diff --git a/tin/apps/submissions/views.py b/tin/apps/submissions/views.py index 206f266d..eefd2d44 100644 --- a/tin/apps/submissions/views.py +++ b/tin/apps/submissions/views.py @@ -151,7 +151,11 @@ def comment_view(request, submission_id): raise http.Http404 comment = request.POST.get("comment", "") - point_override = request.POST.get("point_override", "") + point_override = request.POST.get("point_override") + + if point_override is None: + return http.HttpResponseBadRequest("Missing point_override") + comment = Comment( submission=submission, author=request.user, @@ -329,11 +333,10 @@ def set_aborted_complete_view(request): submissions = Submission.objects.filter(complete=False, grader_pid__isnull=False) for submission in submissions: - try: - psutil.Process(submission.grader_pid) - except psutil.NoSuchProcess: + if not psutil.pid_exists(submission.grader_pid): submission.complete = True - submission.save(update_fields=["complete"]) + submission.grader_pid = None + submission.save(update_fields=["complete", "grader_pid"]) return redirect("auth:index")