diff --git a/conftest.py b/conftest.py index 8178b2ec8e0ecad089374869bce1c1da3f0e046e..1e1e93df63400dbc058b20c037ea28e411dee74c 100644 --- a/conftest.py +++ b/conftest.py @@ -3,11 +3,11 @@ import os.path import pytest # type: ignore import random import string -from typing import Iterator +from typing import Callable, Iterator, List -from travo.gitlab import GitLab, GitLabTest, Group, Project, User +from travo.gitlab import GitLab, GitLabTest, Group, Project, User, Resource from travo.course import Course -from travo.assignment import Assignment +from travo.assignment import Assignment, Submission @pytest.fixture @@ -33,7 +33,7 @@ def test_run_id() -> str: """ A (statistically) unique id for this test run """ - now = datetime.datetime.now().isoformat().replace(":", "-") + now = datetime.datetime.now().isoformat().replace(":", "_").replace("-", "_") return ( now + "-" + "".join(random.choices(string.ascii_uppercase + string.digits, k=5)) ) @@ -117,7 +117,9 @@ def standalone_assignment( test_run_id: str, ) -> Iterator[Assignment]: repo = gitlab.ensure_project( - f"TestGroup/TestAssignment-{test_run_id}", f"Test assignment - {test_run_id}" + path=f"TestGroup/TestAssignment-{test_run_id}", + name=f"Test assignment {test_run_id}", + visibility="public", ) repo.ensure_file("README.md", branch="master") yield Assignment( @@ -126,7 +128,6 @@ def standalone_assignment( repo_path=repo.path_with_namespace, name=repo.path, instructors_path="TestGroup", - username=user_name, ) gitlab.remove_project(repo.path_with_namespace, force=True) @@ -136,38 +137,76 @@ def standalone_assignment_dir(tmp_path: str, test_run_id: str) -> str: return os.path.join(tmp_path, f"Assignment-{test_run_id}") +@pytest.fixture +def to_be_teared_down() -> Iterator[Callable[[Resource], None]]: + """ + A factory fixture for planning the removal of resources upon tear down + + Currently projects and groups are supported. + + Example: + + >>> forge = get_fixture("gitlab") + >>> to_be_teared_down = get_fixture("to_be_teared_down") + >>> group = forge.ensure_group("MyTemporaryGroup") + >>> to_be_teared_down(group) + + Reference: https://docs.pytest.org/en/6.2.x/fixture.html#factories-as-fixtures + """ + resources: List[Resource] = [] + + def _to_be_teared_down(resource: Resource) -> None: + resources.append(resource) + + yield _to_be_teared_down + + # Tear down resources + + for resource in reversed(resources): + forge = resource.gitlab + assert isinstance(forge, GitLabTest) + forge.login("root", "dr0w554p!&ew=]gdS") + if isinstance(resource, Group): + forge.remove_group(resource.id) + else: + assert isinstance(resource, Project) + forge.remove_project(resource.id) + forge.logout() + + @pytest.fixture def standalone_assignment_submission( standalone_assignment: Assignment, -) -> Iterator[Project]: - project = standalone_assignment.ensure_submission_repo() - yield project - standalone_assignment.forge.remove_project(project.path_with_namespace, force=True) +) -> Iterator[Submission]: + standalone_assignment.ensure_submission_repo() + yield standalone_assignment.submission() + standalone_assignment.forge.remove_project(standalone_assignment.submission_path()) @pytest.fixture -def course() -> Course: +def course(gitlab: GitLabTest) -> Course: return Course( - name="Test course", - forge=GitLabTest(), + forge=gitlab, path="TestCourse", - student_dir="~/TestCourse", + name="Test course", assignments_group_path="TestCourse/2020-2021", + student_dir="~/TestCourse", group_submissions=False, ) # course.log.setLevel("DEBUG") @pytest.fixture -def rich_course() -> Course: +def rich_course(gitlab: GitLabTest, test_run_id: str) -> Course: + # The course path and name could be randomized to enable parallel tests return Course( - name="Test course", - forge=GitLabTest(), - path="TestCourse", + forge=gitlab, + path=f"TestRichCourse-{test_run_id}", + name=f"Test rich course {test_run_id}", session_path="2020-2021", assignments=["Assignment1", "Assignment2"], student_groups=["Group1", "Group2"], - student_dir="~/TestCourse", + student_dir="~/RichCourse", group_submissions=False, ) diff --git a/travo/assignment.py b/travo/assignment.py index f5e56e4b1a152bd5e83e0c1c71967b71502cd497..f546b5afc3fda1d39ec3c52f99196eea9331c026 100755 --- a/travo/assignment.py +++ b/travo/assignment.py @@ -128,7 +128,7 @@ class Assignment: >>> assignment = getfixture("standalone_assignment") >>> assignment.submission_name() - 'Test assignment - 2...' + 'Test assignment 2...' """ return self.repo().name @@ -165,8 +165,8 @@ class Assignment: >>> assignment = getfixture("standalone_assignment") >>> assignment.get_submission_username(assignment.repo()) - >>> project = getfixture("standalone_assignment_submission") - >>> assignment.get_submission_username(project) + >>> submission = getfixture("standalone_assignment_submission") + >>> assignment.get_submission_username(submission.repo) 'student1' """ if project.path_with_namespace == self.repo_path: @@ -217,7 +217,7 @@ class Assignment: >>> my_repo.path 'TestAssignment-20...' >>> my_repo.name - 'Test assignment - 20...' + 'Test assignment 20...' >>> assert my_repo.forked_from_project.id == repo.id >>> my_repo = assignment.ensure_submission_repo() diff --git a/travo/course.py b/travo/course.py index 1c59bdbe089ede106b95388c134c7813e9ea0869..81c95762622427049601636adff236e246296924 100755 --- a/travo/course.py +++ b/travo/course.py @@ -80,14 +80,15 @@ class CourseAssignment(Assignment): >>> assignment = course.assignment("SubCourse/Assignment1") >>> assignment.submission_path_components() - ('student1', 'TestCourse', '2020-2021', 'SubCourse', 'Assignment1') + ('student1', 'TestRichCourse-...', '2020-2021', 'SubCourse', 'Assignment1') + >>> assignment.submission_path_components(username="john.doo") - ('john.doo', 'TestCourse', '2020-2021', 'SubCourse', 'Assignment1') + ('john.doo', 'TestRichCourse-...', '2020-2021', 'SubCourse', 'Assignment1') >>> course.group_submissions = True >>> assignment.submission_path_components(username="john.doo") - ('john-doo-travo', 'TestCourse', '2020-2021', 'SubCourse', 'Assignment1') + ('john-doo-travo', 'TestRichCourse-...', '2020-2021', 'SubCourse', 'Assignment1') >>> course.path='TestModule/TestCourse' >>> assignment.submission_path_components() @@ -114,19 +115,20 @@ class CourseAssignment(Assignment): >>> course = getfixture("rich_course") >>> course.assignment("SubCourse/Assignment1").submission_path() - 'student1/TestCourse-2020-2021-SubCourse-Assignment1' + 'student1/TestRichCourse-...-2020-2021-SubCourse-Assignment1' >>> course.assignment("SubCourse/Assignment1", ... student_group="Group1").submission_path() - 'student1/TestCourse-2020-2021-SubCourse-Assignment1' + 'student1/TestRichCourse-...-2020-2021-SubCourse-Assignment1' + More examples with grouped submissions: >>> course.group_submissions = True >>> assignment = course.assignment("SubCourse/Assignment1") >>> assignment.submission_path() - 'student1-travo/TestCourse/2020-2021/SubCourse/Assignment1' + 'student1-travo/TestRichCourse-.../2020-2021/SubCourse/Assignment1' >>> assignment.submission_path(username="john.doo") - 'john-doo-travo/TestCourse/2020-2021/SubCourse/Assignment1' + 'john-doo-travo/TestRichCourse-.../2020-2021/SubCourse/Assignment1' """ components = self.submission_path_components(username) if self.course.group_submissions: @@ -148,14 +150,15 @@ class CourseAssignment(Assignment): >>> assignment.submission_name_components() ('Étudiant de test pour travo', - 'Test course', '2020-2021', 'SubCourse', 'Assignment1') + 'Test rich course ...', '2020-2021', 'SubCourse', 'Assignment1') + >>> from travo.i18n import _ >>> import i18n >>> i18n.set('locale', 'fr') >>> course.group_submissions = True >>> assignment.submission_name_components() (...Étudiant de test pour travo..., - 'Test course', '2020-2021', 'SubCourse', 'Assignment1') + 'Test rich course ...', '2020-2021', 'SubCourse', 'Assignment1') Test: @@ -197,7 +200,7 @@ class CourseAssignment(Assignment): >>> course = getfixture("rich_course") >>> course.forge.login() >>> course.assignment("SubCourse/Assignment1").submission_name() - 'Test course - 2020-2021 - SubCourse - Assignment1' + 'Test rich course ... - 2020-2021 - SubCourse - Assignment1' """ components = self.submission_name_components() if self.course.group_submissions: @@ -205,7 +208,9 @@ class CourseAssignment(Assignment): else: return " - ".join(components[1:]) - def ensure_submission_repo(self, leader_name: Optional[str] = None) -> Project: + def ensure_submission_repo( + self, leader_name: Optional[str] = None, initialized: bool = False + ) -> Project: """ Return the submission repository for this assignment @@ -239,7 +244,7 @@ class CourseAssignment(Assignment): unknown >>> course.assignment("SubCourse/Assignment1", ... student_group="Group1").submissions_forked_from_path() - 'TestCourse...2020-2021/SubCourse/Group1/Assignment1' + 'TestRichCourse-.../2020-2021/SubCourse/Group1/Assignment1' """ if self.leader_name is not None: return super().submissions_forked_from_path() diff --git a/travo/gitlab.py b/travo/gitlab.py index 18f110b3e004ff6a5432b3578cf32358f5faced6..2643e143be81db602728aa9182f18f26524660eb 100644 --- a/travo/gitlab.py +++ b/travo/gitlab.py @@ -696,7 +696,7 @@ class GitLab: **attributes, ) - def remove_group(self, id_or_path: str, force: bool = False) -> None: + def remove_group(self, id_or_path: Union[str, int], force: bool = False) -> None: """ Remove group (DANGEROUS!) diff --git a/travo/tests/test_assignement.py b/travo/tests/test_assignement.py index ddb3e01bcb84960acf18a5acab4c7a3e1b24fb2d..e50585190b3e78e2c44223d41309c4e8e2051571 100644 --- a/travo/tests/test_assignement.py +++ b/travo/tests/test_assignement.py @@ -1,20 +1,23 @@ import os -from travo.gitlab import Project -from travo.assignment import Assignment +from travo.assignment import Assignment, Submission +from travo.utils import working_directory -def test_collect_assignment( - standalone_assignment: Assignment, standalone_assignment_submission: Project +def test_collect( + standalone_assignment: Assignment, + standalone_assignment_submission: Submission, + tmp_path: str, ) -> None: assignment = standalone_assignment - student = standalone_assignment_submission.owner + student = standalone_assignment_submission.student - assignment.collect() - assert os.path.isdir(f"{student.username}") + with working_directory(tmp_path): + assignment.collect() + assert os.path.isdir(os.path.join(tmp_path, student)) assignment.collect(template="foo/bar-{path}-{username}") - assert os.path.isdir(f"foo/bar-{assignment.name}-{student.username}") + assert os.path.isdir(f"foo/bar-{assignment.name}-{student}") def test_fetch_from_empty_submission_repo( diff --git a/travo/tests/test_course.py b/travo/tests/test_course.py index 0eea5b7a00d839b143fab9250e551117bb427d25..901c9bfc451a21ad32e45fbab638415cbe092d3b 100644 --- a/travo/tests/test_course.py +++ b/travo/tests/test_course.py @@ -80,12 +80,24 @@ def test_course_share_with(course): course.share_with(username="travo-test-etu", assignment_name="Assignment1") -def test_course_collect(rich_course, user_name, tmp_path): +def test_course_collect(gitlab, rich_course, to_be_teared_down, user_name, tmp_path): + gitlab.login("instructor1", "aqwzsx(t3") + rich_course.forge.ensure_group( + path=rich_course.path, + name=rich_course.name, + visibility="public", + ) + session_path = rich_course.path + "/" + rich_course.session_path + rich_course.forge.ensure_group( + path=session_path, + name=rich_course.session_name, + visibility="public", + ) assignments = rich_course.assignments student_groups = rich_course.student_groups for group in student_groups: - group_path = rich_course.assignments_group_path + "/" + group + group_path = session_path + "/" + group group_name = group rich_course.forge.ensure_group( path=group_path, name=group_name, visibility="public" @@ -93,13 +105,15 @@ def test_course_collect(rich_course, user_name, tmp_path): for assignment in assignments: path = rich_course.assignments_group_path + "/" + assignment project = rich_course.forge.ensure_project( - path, assignment, visibility="public" + path=path, name=assignment, visibility="public" ) project.ensure_file("README.md", branch="master") path = group_path + "/" + assignment project.ensure_fork(path, assignment, visibility="public") + gitlab.logout() os.chdir(tmp_path) + gitlab.login("student1", "aqwzsx(t1") rich_course.fetch("Assignment1") rich_course.submit("Assignment1", student_group="Group1") @@ -126,11 +140,6 @@ def test_course_collect(rich_course, user_name, tmp_path): ) assert os.path.isdir(f"submitted/{user_name}") - # Clean after test - # Commented out for now for the next test should pass - # This is bad as tests should be independent! - # rich_course.forge.remove_group(rich_course.assignments_group_path, force=True) - def test_course_generate_and_release(rich_course): rich_course.group_submissions = True diff --git a/travo/utils.py b/travo/utils.py index 84b46d690cee50c9bc7e01f58bde7a10f5fb68fb..3ae6e07ef6916947a253f4ce8c75ea964d280604 100755 --- a/travo/utils.py +++ b/travo/utils.py @@ -1,9 +1,12 @@ import subprocess import urllib import urllib.parse -from typing import cast, Any, Sequence, Optional +from typing import cast, Any, Iterator, Sequence, Optional import logging import colorlog # type: ignore +import os +from contextlib import contextmanager + _logger: Optional[logging.Logger] = None @@ -103,3 +106,31 @@ def git_get_origin(cwd: str = ".") -> str: lines = result.stdout.decode().splitlines() assert lines return cast(str, lines[0]) + + +@contextmanager +def working_directory(path: str) -> Iterator: + """ + A context manager which changes the working directory to the given + path, and then changes it back to its previous value on exit. + + Example: + + >>> tmp_path = getfixture("tmp_path") + >>> dirname = "this-is-a-long-directory-name" + + This creates a directory in `tmp_path`, instead of in the current + working directory: + + >>> with working_directory(tmp_path): + ... os.mkdir(dirname) + + >>> assert os.path.exists(os.path.join(tmp_path, dirname)) + >>> assert not os.path.exists(dirname) + """ + prev_cwd = os.getcwd() + os.chdir(path) + try: + yield + finally: + os.chdir(prev_cwd)