def setup_student_repos_and_user_accounts(usernames: List[str], platform_url: str): funcs.run_repobee( f"repos setup -a {const.TEMPLATE_REPOS_ARG} " f"--base-url {platform_url}", ) api = funcs.get_api(platform_url) api._add_users(usernames)
def test_assign_one_review(self, with_student_repos, tmpdir): assignment_name = assignment_names[1] expected_review_teams = [ plug.StudentTeam( members=[], name=plug.generate_review_team_name(student_team_name, assignment_name), ) for student_team_name in STUDENT_TEAM_NAMES ] command = " ".join([ *repobee_plug.cli.CoreCommand.reviews.assign.as_name_tuple(), *BASE_ARGS, "-a", assignment_name, *STUDENTS_ARG, "-n", "1", ]) group_assertion = expected_num_members_group_assertion( expected_num_members=1) run_repobee(command, workdir=tmpdir, plugins=[_repobee.ext.gitlab]) assert_on_groups(expected_review_teams, single_group_assertion=group_assertion) assert_num_issues(STUDENT_TEAMS, [assignment_name], 1) assert_issues_exist( STUDENT_TEAMS, [assignment_name], _repobee.command.peer.DEFAULT_REVIEW_ISSUE, expected_num_asignees=1, )
def test_end_double_blind_reviews_when_review_teams_are_missing( self, platform_url, with_student_repos): """Even if the review teams are missing, the anonymous repos should be deleted when running end. Such cases can occurr when there is failure in setup, see issue #825 for details. """ # arrange assignment_name = const.TEMPLATE_REPO_NAMES[0] api = funcs.get_api(platform_url) num_repos_before = len(list(api.get_repos())) key = 1234 funcs.run_repobee(f"reviews assign --num-reviews 1 " f"--base-url {platform_url} " f"--double-blind-key {key} " f"--assignments {assignment_name}") api._restore_platform_state() review_teams = [ team for team in api.get_teams() if "-" not in team.name ] for team in review_teams: api.delete_team(team) # act funcs.run_repobee(f"reviews end " f"--base-url {platform_url} " f"--double-blind-key {key} " f"--assignments {assignment_name}") # assert api._restore_platform_state() num_repos_after = len(list(api.get_repos())) assert num_repos_after == num_repos_before
def test_setup_with_default_branch_protection_does_not_carry_over( self, tmpdir): """Student repositories created when global default branch protection is enabled on the GitLab instance, should still not have default branch protection. """ # arrange gl = gitlab.Gitlab(url=BASE_URL, private_token=ADMIN_TOKEN, ssl_verify=False) gl.auth() settings = gl.settings.get() settings.default_branch_protection = ( _repobee.ext.gitlab.DefaultBranchProtection.FULL.value) settings.save() command = " ".join([ *repobee_plug.cli.CoreCommand.repos.setup.as_name_tuple(), *BASE_ARGS, *TEMPLATE_ORG_ARG, *MASTER_REPOS_ARG, *STUDENTS_ARG, ]) # act run_repobee(command, workdir=tmpdir, plugins=[_repobee.ext.gitlab]) # assert api = api_instance(ORG_NAME) loop_ran = False for repo in api.get_repos(): loop_ran = True assert not repo.implementation.protectedbranches.list() assert loop_ran, "assertion loop did not execute"
def test_opens_issue_if_update_rejected(self, tmpdir, with_student_repos): master_repo = assignment_names[0] conflict_repo = plug.generate_repo_name(STUDENT_TEAMS[0], master_repo) filename = "superfile.super" text = "some epic content\nfor this file!" # update the master repo update_repo(master_repo, filename, text) # conflicting update in the student repo update_repo(conflict_repo, "somefile.txt", "some other content") issue = plug.Issue(title="Oops, push was rejected!", body="") issue_file = pathlib.Path(str(tmpdir)) / "issue.md" issue_file.write_text(issue.title) command = " ".join([ *repobee_plug.cli.CoreCommand.repos.update.as_name_tuple(), *TEMPLATE_ORG_ARG, *BASE_ARGS, "-a", master_repo, *STUDENTS_ARG, "--issue", issue_file.name, ]) run_repobee(command, workdir=tmpdir, plugins=[_repobee.ext.gitlab]) assert_repos_contain(STUDENT_TEAMS[1:], [master_repo], filename, text) assert_issues_exist(STUDENT_TEAMS[0:1], [master_repo], issue)
def test_opens_issue_when_push_fails(self, platform_url, with_student_repos, tmp_path): """Test running update when a student repo has been modified such that the push is rejected. The specified issues should then be opened in that student's repo, but not in any of the others. """ # arrange title = "You done goofed" body = "You need to fix these things manually." issue_path = tmp_path / "issue.md" issue_path.write_text(f"{title}\n{body}", encoding="utf8") # modify a student repo repo_path = tmp_path / "repo" selected_repo = funcs.get_repos(platform_url)[0] repo = git.Repo.clone_from(selected_repo.path, to_path=repo_path) repo.git.commit("--amend", "-m", "Best commit") repo.git.push("--force") # act funcs.run_repobee(f"repos update -a {const.TEMPLATE_REPOS_ARG} " f"--base-url {platform_url} " f"--issue {issue_path}") # assert for platform_repo in funcs.get_repos(platform_url): if platform_repo.name == selected_repo.name: assert len(platform_repo.issues) == 1 issue = platform_repo.issues[0] assert issue.title == title assert issue.body == body else: assert not platform_repo.issues
def test_clone_does_not_alter_existing_dirs(self, with_student_repos, tmpdir): """Test that clone does not clobber existing directories.""" team_with_local_repos = STUDENT_TEAMS[0] teams_without_local_repos = STUDENT_TEAMS[1:] expected_dir_hashes = [] for template_repo_name in assignment_names: new_dir = plug.fileutils.generate_repo_path( str(tmpdir), team_with_local_repos.name, template_repo_name) new_dir.mkdir(parents=True) new_file = new_dir / "file" new_file.write_text(str(new_dir), encoding="utf-8") expected_dir_hashes.append((new_dir, hash_directory(new_dir))) repobee_testhelpers.funcs.initialize_repo(new_dir) command = " ".join([ *repobee_plug.cli.CoreCommand.repos.clone.as_name_tuple(), *BASE_ARGS, *MASTER_REPOS_ARG, *STUDENTS_ARG, ]) run_repobee(command, workdir=tmpdir, plugins=[_repobee.ext.gitlab]) assert_cloned_repos(teams_without_local_repos, assignment_names, tmpdir) for dirpath, expected_hash in expected_dir_hashes: dirhash = hash_directory(dirpath) assert dirhash == expected_hash, "hash mismatch for " + dirpath
def test_setup_multiple_template_repos(self, platform_dir, platform_url): funcs.run_repobee(f"repos setup -a {TEMPLATE_REPOS_ARG} " f"--base-url {platform_url}") assert_student_repos_match_templates(STUDENT_TEAMS, TEMPLATE_REPO_NAMES, funcs.get_repos(platform_url))
def test_setup_with_local_repos(self, platform_url, tmp_path): """Test running the setup command with the names of local repositories. That is to say, repos that are not in the template organization. """ # arrange template_repo_hashes = {} task_34 = tmp_path / "task-34" task_55 = tmp_path / "task-55" template_repo_hashes[task_34.name] = create_local_repo( task_34, [("somefile.txt", "This is task 34!")]) template_repo_hashes[task_55.name] = create_local_repo( task_55, [("hello.py", "print('hello, world!')")]) # act funcs.run_repobee( f"repos setup -a {task_55.name} {task_34.name} " f"--base-url {platform_url} " "--allow-local-templates", workdir=tmp_path, ) # assert repo_dict = { repo.name: repo.path for repo in funcs.get_repos(platform_url) } _assert_repos_match_templates( STUDENT_TEAMS, [task_34.name, task_55.name], template_repo_hashes, repo_dict, )
def test_create_repo_with_plugin(platform_url): team = const.STUDENT_TEAMS[0] repo_name = "super-repo" description = "This is the description" private = True class CreateSingle(plug.Plugin, plug.cli.Command): __settings__ = plug.cli.command_settings( category=plug.cli.CoreCommand.repos, action="create-single") team_name = plug.cli.option() repo_name = plug.cli.option() def command(self, api: plug.PlatformAPI): team = api.get_teams(team_names=[self.team_name]) api.create_repo( self.repo_name, description=description, private=private, team=team, ) funcs.run_repobee( f"repos create-single --bu {platform_url} " f"--team-name {team.name} --repo-name {repo_name}", plugins=[CreateSingle], ) existing_repos = funcs.get_repos(platform_url) matching_repo = next( (repo for repo in existing_repos if repo.name == repo_name), None) assert matching_repo.name == repo_name assert matching_repo.description == description assert matching_repo.private == private
def test_discover_repos_parser_does_not_discover_repos_if_flag_not_specified( platform_url, with_student_repos): """Test that the discovery mechanism is inactive if --discover-repos flag is not specified. """ args = [] student = "slarse" assignment = "epic-task" class Check(plug.Plugin, plug.cli.Command): __settings__ = plug.cli.command_settings(base_parsers=[ plug.cli.BaseParser.STUDENTS, plug.cli.BaseParser.REPO_DISCOVERY, ]) def command(self, api): nonlocal args args = self.args funcs.run_repobee( f"check --base-url {platform_url} " f"-s {student} " f"-a {assignment}", plugins=[Check], ) assert "repos" not in args
def test_discover_repos_parser_discovers_repos_if_flag_is_specified( platform_url, with_student_repos): """Test that the discovery mechanism kicks in if --discover-repos is specified. """ discovered_repos = [] class Check(plug.Plugin, plug.cli.Command): __settings__ = plug.cli.command_settings(base_parsers=[ plug.cli.BaseParser.STUDENTS, plug.cli.BaseParser.REPO_DISCOVERY, ]) def command(self, api): nonlocal discovered_repos discovered_repos = list(self.args.repos) funcs.run_repobee( f"check --base-url {platform_url} " f"--sf {const.STUDENTS_FILE} " f"--discover-repos", plugins=[Check], ) expected_num_repos = len(const.TEMPLATE_REPO_NAMES) * len( const.STUDENT_TEAMS) assert len(discovered_repos) == expected_num_repos
def test_closes_correct_issues(self, with_student_repos, platform_url): issue_to_close, open_issue = _open_predefined_issues(platform_url) funcs.run_repobee([ *f"issues close --base-url {platform_url} " f"--assignments {const.TEMPLATE_REPOS_ARG} ".split(), "--title-regex", issue_to_close.title, ]) iterations = 0 for repo in funcs.get_repos(platform_url, const.TARGET_ORG_NAME): iterations += 1 assert len(repo.issues) == 2 actual_open_issue, *_ = [ i for i in repo.issues if i.state == plug.IssueState.OPEN ] actual_closed_issue, *_ = [ i for i in repo.issues if i.state == plug.IssueState.CLOSED ] assert actual_open_issue.title == open_issue.title assert actual_closed_issue.title == issue_to_close.title assert iterations == len(const.STUDENT_TEAMS) * len( const.TEMPLATE_REPO_NAMES)
def test_open_issue_for_all_repos(self, with_student_repos, platform_url, issue): expected_repo_names = plug.generate_repo_names( const.STUDENT_TEAMS, const.TEMPLATE_REPO_NAMES) funcs.run_repobee( f"issues open --assignments {const.TEMPLATE_REPOS_ARG} " f"--base-url {platform_url} " f"--issue {issue.path} ") repos = funcs.get_repos(platform_url, const.TARGET_ORG_NAME) issues_dict = {repo.name: repo.issues for repo in repos} num_asserts = 0 for name in expected_repo_names: num_asserts += 1 issues = issues_dict[name] first_issue = issues[0] assert len(issues) == 1 assert first_issue.title == issue.title assert first_issue.body == issue.body assert first_issue.state == plug.IssueState.OPEN assert num_asserts == len(expected_repo_names)
def test_update_local_stashes_local_changes(self, platform_url, with_student_repos, tmp_path): """Test that updating local repositories with unstaged changes causes the changes to be stashed, and the update to proceed. """ new_file_name = "suspicious_file.txt" target_repo = funcs.get_repos(platform_url)[-1] self._clone_all_student_repos(platform_url, tmp_path) # update remote repo with funcs.update_repository(target_repo.url) as repo_path: (repo_path / new_file_name).write_text(new_file_name) # update local repo local_repo_path = list(tmp_path.rglob(target_repo.name))[0] next(file for file in local_repo_path.iterdir() if file.is_file()).write_text("this is an update!") # act funcs.run_repobee( f"repos clone -a {const.TEMPLATE_REPOS_ARG} " f"--update-local " f"--base-url {platform_url}", workdir=tmp_path, ) # assert assert local_repo_path.parent.parent == tmp_path local_new_file = local_repo_path / new_file_name assert local_new_file.is_file() assert local_new_file.read_text() == new_file_name assert git.Repo(local_repo_path).git.stash("list")
def test_does_not_push_to_existing_repos(self, platform_url, with_student_repos, capsys, tmp_path): """This command should not push to existing repos, that's for the ``update`` command to do. """ # arrange task = tmp_path / TEMPLATE_REPO_NAMES[0] create_local_repo(task, [("best/file/ever.txt", "content")]) # act # this push would fail if it was attempted, as the repo # content of the local template does not match that of # the remote template funcs.run_repobee( f"repos setup -a {TEMPLATE_REPOS_ARG} " f"--base-url {platform_url} " "--allow-local-templates", workdir=tmp_path, ) # nothing should have changed, and there should be no errors assert_student_repos_match_templates(STUDENT_TEAMS, TEMPLATE_REPO_NAMES, funcs.get_repos(platform_url)) assert "[ERROR]" not in capsys.readouterr().out
def test_does_not_update_local_by_default(self, platform_url, with_student_repos, tmp_path, capsys): """Test that cloning an update repository that exists locally does not cause it to be updated by default. """ # arrange new_file_name = "suspicious_file.txt" target_repo = funcs.get_repos(platform_url)[-1] self._clone_all_student_repos(platform_url, tmp_path) with funcs.update_repository(target_repo.url) as repo_path: (repo_path / new_file_name).write_text(new_file_name) # act funcs.run_repobee( f"repos clone -a {const.TEMPLATE_REPOS_ARG} " f"--base-url {platform_url}", workdir=tmp_path, ) # assert local_repo_path = list(tmp_path.rglob(target_repo.name))[0] local_new_file = local_repo_path / new_file_name assert not local_new_file.is_file() assert "--update-local" in capsys.readouterr().err
def test_use_local_template_with_strangely_named_default_branch( self, platform_url, tmp_path): """Test setting up student repos with a template repo that has a non-standard default branch name. The student repos should get the same default branch. """ strange_branch_name = "definitelynotmaster" task_99 = tmp_path / "task-99" create_local_repo( task_99, [("README.md", "Read me plz.")], default_branch=strange_branch_name, ) funcs.run_repobee( f"repos setup -a {task_99.name} " f"--base-url {platform_url} " "--allow-local-templates", workdir=tmp_path, ) repo = git.Repo(funcs.get_repos(platform_url)[0].path) assert len(repo.branches) == 1 assert repo.branches[0].name == strange_branch_name
def with_student_repos(platform_url): funcs.run_repobee(f"repos setup -a {TEMPLATE_REPOS_ARG} " f"--students-file {STUDENTS_FILE} " f"--base-url {platform_url} " f"--user {TEACHER} " f"--org-name {TARGET_ORG_NAME} " f"--template-org-name {TEMPLATE_ORG_NAME}")
def test_use_non_standard_repo_names(self, platform_url): """Test setting up repos with non-standard repo names using an implementation of the ``generate_repo_name`` hook. """ def generate_repo_name(team_name, assignment_name): return f"{assignment_name}-BONKERS-{team_name}" expected_repo_names = [ generate_repo_name(team, assignment_name) for team, assignment_name in itertools.product( const.STUDENT_TEAMS, const.TEMPLATE_REPO_NAMES) ] class StrangeNamingConvention(plug.Plugin): def generate_repo_name(self, team_name, assignment_name): return generate_repo_name(team_name, assignment_name) funcs.run_repobee( f"repos setup -a {const.TEMPLATE_REPOS_ARG} " f"--base-url {platform_url}", plugins=[StrangeNamingConvention], ) actual_repo_names = [ repo.name for repo in funcs.get_repos(platform_url) ] assert sorted(actual_repo_names) == sorted(expected_repo_names)
def test_clean_setup_in_subgroup(self, tmpdir): """It should be possible to use a subgroup as the target org.""" gl, template_group, target_group = gitlab_and_groups() subgroup_name = "bestgroup" subgroup_full_path = f"{target_group.path}/{subgroup_name}" gl.groups.create( dict( name=subgroup_name, path=subgroup_name, parent_id=target_group.id, )) base_args = [ arg if arg != ORG_NAME else subgroup_full_path for arg in BASE_ARGS ] command = " ".join([ *repobee_plug.cli.CoreCommand.repos.setup.as_name_tuple(), *base_args, *TEMPLATE_ORG_ARG, *MASTER_REPOS_ARG, *STUDENTS_ARG, ]) run_repobee(command, workdir=tmpdir, plugins=[_repobee.ext.gitlab]) assert_repos_exist(STUDENT_TEAMS, assignment_names, org_name=subgroup_full_path)
def test_post_setup_hook_called_on_correct_repos(self, platform_url): """Test that the repos are correctly marked as newly created or not.""" # arrange first_template = TEMPLATE_REPO_NAMES[0] funcs.run_repobee( f"repos setup -a {first_template} --base-url {platform_url}") executed = False class PostSetupRecorder(plug.Plugin): def post_setup(self, repo, api, newly_created): nonlocal executed executed = True if first_template in repo.name: assert (not newly_created ), f"expected {repo.name} to be newly created" else: assert ( newly_created), f"expected {repo.name} to be existing" # act/assert funcs.run_repobee( f"repos setup -a {TEMPLATE_REPOS_ARG} --base-url {platform_url}", plugins=[PostSetupRecorder], ) assert executed, "Test plugin was never executed"
def test_setup_with_wrong_case_on_student(self, tmpdir): """User names are case insensitive on GitLab, and so setup should work fine even if the case of some character in a student's username is "incorrect". See https://github.com/repobee/repobee/issues/900 """ student = STUDENT_TEAMS[0].members[0] student_wrong_case = student.upper() assert (student != student_wrong_case), "cases match, test is pointless :(" command = " ".join([ *repobee_plug.cli.CoreCommand.repos.setup.as_name_tuple(), *BASE_ARGS, *TEMPLATE_ORG_ARG, *MASTER_REPOS_ARG, "--students", student_wrong_case, ]) run_repobee(command, workdir=tmpdir, plugins=[_repobee.ext.gitlab]) assert_repos_exist([plug.StudentTeam(members=[student])], assignment_names)
def test_squashed_student_repos_contain_only_squash_commit( self, platform_url, tmp_path): """When using the squash plugin, student repos should initially only contain the squash commit. """ assignment_name = "repo-with-multiple-commits" repo_path = tmp_path / assignment_name self._setup_local_repo_with_multiple_commits(repo_path) squash_message = "This is a strange commit message" funcs.run_repobee( [ *plug.cli.CoreCommand.repos.setup.as_name_tuple(), "--assignments", assignment_name, "--allow-local-templates", "--base-url", platform_url, "--squash-message", squash_message, ], plugins=[_repobee.ext.squash], workdir=tmp_path, ) repos = funcs.get_repos(platform_url) assert repos for repo in repos: self._assert_single_commit_with_message(repo, squash_message)
def test_closes_only_matched_issues(self, open_issues, tmpdir): """Test that close-issues respects the regex.""" assert len(open_issues) == 2, "expected there to be only 2 open issues" close_issue = open_issues[0] open_issue = open_issues[1] command = " ".join([ *repobee_plug.cli.CoreCommand.issues.close.as_name_tuple(), *BASE_ARGS, *MASTER_REPOS_ARG, *STUDENTS_ARG, "-r", close_issue.title, ]) run_repobee(command, workdir=tmpdir, plugins=[_repobee.ext.gitlab]) assert_issues_exist( STUDENT_TEAM_NAMES, assignment_names, close_issue, expected_state="closed", ) assert_issues_exist( STUDENT_TEAM_NAMES, assignment_names, open_issue, expected_state="opened", )
def test_use_extended_students_syntax(self, platform_url, tmp_path): students_file = tmp_path / "students.yml" students_file.write_text( """ some-team: members: [simon] other-team: members: [eve, alice] """.strip(), encoding=sys.getdefaultencoding(), ) expected_repo_names = plug.generate_repo_names( team_names=["some-team", "other-team"], assignment_names=const.TEMPLATE_REPO_NAMES, ) funcs.run_repobee( f"{plug.cli.CoreCommand.repos.setup} --base-url {platform_url} " f"--students-file {students_file} " f"--assignments {const.TEMPLATE_REPOS_ARG}", plugins=[_repobee.ext.studentsyml], ) actual_repo_names = [ repo.name for repo in funcs.get_repos(platform_url) ] assert sorted(actual_repo_names) == sorted(expected_repo_names)
def test_assign_to_nonexisting_students(self, with_student_repos, tmpdir): """If you try to assign reviews where one or more of the allocated student repos don't exist, there should be an error. """ assignment_name = assignment_names[1] non_existing_group = "non-existing-group" student_team_names = STUDENT_TEAM_NAMES + [non_existing_group] command = " ".join([ *repobee_plug.cli.CoreCommand.reviews.assign.as_name_tuple(), *BASE_ARGS_NO_TB, "-a", assignment_name, "-s", *student_team_names, "-n", "1", ]) with pytest.raises(plug.NotFoundError) as exc_info: run_repobee(command, workdir=tmpdir, plugins=[_repobee.ext.gitlab]) non_existing_repo_name = plug.generate_repo_name( non_existing_group, assignment_name) assert f"Can't find repos: {non_existing_repo_name}" in str( exc_info.value) assert_num_issues(STUDENT_TEAMS, [assignment_name], 0)
def test_update_local(self, platform_url, with_student_repos, tmp_path): """Test cloning an updated repository that already exists locally, when there are no incompatible changes between the remote copy and the local copy and --update-local is specified. """ # arrange new_file_name = "suspicious_file.txt" target_repo = funcs.get_repos(platform_url)[-1] self._clone_all_student_repos(platform_url, tmp_path) with funcs.update_repository(target_repo.url) as repo_path: (repo_path / new_file_name).write_text(new_file_name) # act funcs.run_repobee( f"repos clone -a {const.TEMPLATE_REPOS_ARG} " f"--update-local " f"--base-url {platform_url}", workdir=tmp_path, ) # assert local_repo_path = list(tmp_path.rglob(target_repo.name))[0] assert local_repo_path.parent.parent == tmp_path local_new_file = local_repo_path / new_file_name assert local_new_file.is_file() assert local_new_file.read_text() == new_file_name
def test_auto_truncation_retains_final_lines(monkeypatch, tmp_path_factory): """The log file should be truncated by any command when it gets too large.""" # arrange log_dir = tmp_path_factory.mktemp("logs") logfile = log_dir / "repobee.log" max_size = 1024 * 10 logfile.write_bytes(b"a\n" * max_size * 10) last_lines = [b"these are", b"the last lines", b"of the log"] with open(logfile, mode="ab") as f: for line in last_lines: f.write(line) monkeypatch.setattr("_repobee.constants.LOG_DIR", log_dir) monkeypatch.setattr("_repobee.constants.MAX_LOGFILE_SIZE", max_size) # act with pytest.raises(SystemExit): funcs.run_repobee("-h") # assert log_contents = logfile.read_bytes() for line in last_lines: assert line in log_contents
def test_add_teachers_command_happy_path(platform_url): """The add-teachers command should add all existing repos to the teachers team, as well as the specified teachers. """ # arrange teachers = ["gork", "mork", "slanesh"] setup_student_repos_and_user_accounts(teachers, platform_url) # act funcs.run_repobee( f"teams add-teachers --teachers {' '.join(teachers)} " f"--base-url {platform_url}", plugins=[tamanager], ) # assert teachers_team = get_teachers_team(platform_url) num_expected_repos = len(const.STUDENT_TEAMS) * len( const.TEMPLATE_REPO_NAMES) assert len(teachers_team.repos) == num_expected_repos expected_repo_names = [r.name for r in funcs.get_repos(platform_url)] actual_repo_names = [r.name for r in teachers_team.repos] assert sorted(expected_repo_names) == sorted(actual_repo_names) assert sorted([m.username for m in teachers_team.members]) == sorted(teachers)