def post_clone(repo: plug.StudentRepo, api: plug.PlatformAPI) -> plug.Result: """Run pylint on all Python files in a repo. Args: path: Path to the repo. api: A platform API class instance. Returns: a plug.Result specifying the outcome. """ lint_results = _pylint(repo.path) if not lint_results: msg = "no .py files found" return plug.Result(SECTION, plug.Status.WARNING, msg) msg = "\n".join([ f"{res.path} -- {'ERROR' if res.errored else 'OK'}" for res in lint_results ], ) has_errors = any(map(lambda res: res.errored != 0, lint_results)) return plug.Result( name=SECTION, msg=msg, status=plug.Status.SUCCESS if not has_errors else plug.Status.ERROR, data={ str(pylint_res.path): pylint_res.output for pylint_res in lint_results }, )
def post_clone( self, repo: plug.StudentRepo, api: plug.PlatformAPI ) -> plug.Result: """Run ``javac`` on all .java files in the repo. Args: repo: A student repo. api: A platform API class instance. Returns: a Result specifying the outcome. """ ignore = list(self.javac_ignore or []) java_files = [ str(file) for file in util.find_files_by_extension(repo.path, ".java") if file.name not in ignore ] if not java_files: msg = "no .java files found" status = plug.Status.WARNING return plug.Result(PLUGIN_NAME, status, msg) status, msg = self._javac(java_files) return plug.Result(PLUGIN_NAME, status, msg)
def command(self) -> Optional[plug.Result]: """A callback function that runs the sanitization protocol on a given file. Returns: Result if the syntax is invalid, otherwise nothing. """ infile_encoding = _fileutils.guess_encoding(self.infile) infile_content = self.infile.read_text( encoding=infile_encoding).split("\n") errors = _syntax.check_syntax(infile_content) if errors: file_errors = [_format.FileWithErrors(self.infile.name, errors)] msg = _format.format_error_string(file_errors) return plug.Result( name="sanitize-file", msg=msg, status=plug.Status.ERROR, ) result = _sanitize.sanitize_text(infile_content, strip=self.strip) if result: self.outfile.write_text(result, encoding=infile_encoding) return plug.Result( name="sanitize-file", msg="Successfully sanitized file", status=plug.Status.SUCCESS, )
def pairwise_compile( test_classes: List[pathlib.Path], java_files: List[pathlib.Path], classpath: str, ) -> Tuple[List[plug.Result], List[plug.Result]]: """Compile test classes with their associated production classes. For each test class: 1. Find the associated production class among the ``java_files`` 2. Compile the test class together with all of the .java files in the associated production class' directory. Args: test_classes: A list of paths to test classes. java_files: A list of paths to java files from the student repo. classpath: A base classpath to use. Returns: A tuple of lists of Results on the form ``(succeeded, failed)`` """ failed = [] succeeded = [] # only use concrete test classes concrete_test_classes = filter(lambda t: not is_abstract_class(t), test_classes) for test_class in concrete_test_classes: status, msg, prod_class_path = _pairwise_compile( test_class, classpath, java_files) if status != Status.SUCCESS: failed.append(plug.Result(SECTION, status, msg)) else: succeeded.append((test_class, prod_class_path)) return succeeded, failed
def hook_result_mapping(): """A hook result mapping for use as a mocked return value to commands that return hook results (e.g. command.clone_repos). """ hook_results = collections.defaultdict(list) for repo_name, hook_name, status, msg, data in [ ( "slarse-task-1", "junit4", plug.Status.SUCCESS, "All tests passed", {"extra": "data", "arbitrary": {"nesting": "here"}}, ), ( "slarse-task-1", "javac", plug.Status.ERROR, "Some stuff failed", None, ), ( "glassey-task-2", "pylint", plug.Status.WARNING, "-10/10 code quality", None, ), ]: hook_results[repo_name].append( plug.Result(name=hook_name, status=status, msg=msg, data=data) ) return dict(hook_results)
def test_raises_if_state_is_not_all(self, tmp_grades_file, mocked_hook_results): """Test that a warning is issued if the plugin is run on ``issues list`` results where the state is not ``all`` (i.e. ``repobee issues list`` was not run with the ``--all`` flag). This is important as closed issues should still be taken into account. """ args = argparse.Namespace( students=list(TEAMS), hook_results_file="", # don't care, read_results_file is mocked grades_file=tmp_grades_file, assignments="week-1 week-2 week-4 week-6".split(), edit_msg_file=str(tmp_grades_file.parent / "editmsg.txt"), teachers=list(TEACHERS), grade_specs=[PASS_GRADESPEC_FORMAT], allow_other_states=False, ) mocked_hook_results["list-issues"] = [ plug.Result( name="list-issues", status=plug.Status.SUCCESS, msg=None, # change the state to OPEN, which will cause any closed # grading issues to be missed data={"state": plug.IssueState.OPEN.value}, ) ] with pytest.raises(_exception.FileError) as exc_info: csvgrades.callback(args=args) assert "`repobee issues list` was not run with the --all flag" in str( exc_info.value)
def command(self): return plug.Result( name=self.__plugin_name__, msg="Nice!", status=plug.Status.SUCCESS, data={"name": self.name, "age": self.age}, )
def command(self): return plug.Result( name="workdir", msg="workdir", status=plug.Status.SUCCESS, data={"cwd": os.getcwd()}, )
def command(self) -> Optional[plug.Result]: repo_root = self.repo_root.absolute() input_error = self._validate_input(repo_root) if input_error: return input_error if self.no_commit: LOGGER.info("Executing dry run") file_relpaths = _sanitize_repo.discover_dirty_files(repo_root) errors = _sanitize_repo.sanitize_files(repo_root, file_relpaths) else: LOGGER.info(f"Sanitizing repo and updating {self.target_branch}") effective_target_branch = self._resolve_effective_target_branch( repo_root) try: errors = _sanitize_repo.sanitize_to_target_branch( repo_root, effective_target_branch, self.commit_message) except _gitutils.EmptyCommitError: return plug.Result( name="sanitize-repo", msg="No diff between target branch and sanitized output. " f"No changes will be made to branch: {self.target_branch}", status=plug.Status.WARNING, ) if errors: return plug.Result( name="sanitize-repo", msg=_format.format_error_string(errors), status=plug.Status.ERROR, ) result_message = "Successfully sanitized repo" + ( f" to pull request branch\n\nrun 'git switch " f"{effective_target_branch}' to checkout the branch" if self.create_pr_branch else "") return plug.Result( name="sanitize-repo", msg=result_message, status=plug.Status.SUCCESS, )
def command(self) -> Optional[plug.Result]: repo_root = self.repo_root.absolute() message = _sanitize_repo.check_repo_state(repo_root) if message and not self.force: return plug.Result( name="sanitize-repo", msg=message, status=plug.Status.ERROR, ) if self.no_commit: LOGGER.info("Executing dry run") file_relpaths = _sanitize_repo.discover_dirty_files(repo_root) errors = _sanitize_repo.sanitize_files(repo_root, file_relpaths) else: LOGGER.info(f"Sanitizing repo and updating {self.target_branch}") try: errors = _sanitize_repo.sanitize_to_target_branch( repo_root, self.target_branch, self.commit_message, ) except _sanitize_repo.EmptyCommitError: return plug.Result( name="sanitize-repo", msg="No diff between target branch and sanitized output. " f"No changes will be made to branch: {self.target_branch}", status=plug.Status.WARNING, ) if errors: return plug.Result( name="sanitize-repo", msg=_format.format_error_string(errors), status=plug.Status.ERROR, ) return plug.Result( name="sanitize-repo", msg="Successfully sanitized repo", status=plug.Status.SUCCESS, )
def _find_test_classes(self, assignment_name) -> List[pathlib.Path]: """Find all test classes (files ending in ``Test.java``) in directory at <reference_tests_dir>/<assignment_name>. Args: assignment_name: Name of an assignment. Returns: a list of test classes from the corresponding reference test directory. """ test_dir = ( pathlib.Path(self.junit4_reference_tests_dir) / assignment_name ) if not test_dir.is_dir(): res = plug.Result( SECTION, plug.Status.ERROR, "no reference test directory for {} in {}".format( assignment_name, self.junit4_reference_tests_dir ), ) raise _exception.ActError(res) test_classes = [ file for file in test_dir.rglob("*.java") if file.name.endswith("Test.java") and file.name not in (self.junit4_ignore_tests or []) ] if not test_classes: res = plug.Result( SECTION, plug.Status.WARNING, "no files ending in `Test.java` found in {!s}".format( test_dir ), ) raise _exception.ActError(res) return test_classes
def create_duplicated_pass_hookresult(author): first_pass = create_pass_hookresult(author, number=3) second_pass = create_pass_hookresult(author, number=4) return plug.Result( name="list-issues", status=plug.Status.SUCCESS, msg=None, data={ **first_pass.data, **second_pass.data }, )
def create_komp_and_pass_hookresult(author): other = create_komp_hookresult(author) pass_ = create_pass_hookresult(author) return plug.Result( name="list-issues", status=plug.Status.SUCCESS, msg=None, data={ **other.data, **pass_.data }, )
def _validate_input(self, repo_root) -> plug.Result: message = _sanitize_repo.check_repo_state(repo_root) if message and not self.force: return plug.Result(name="sanitize-repo", msg=message, status=plug.Status.ERROR) if self.create_pr_branch: if not self.target_branch: return plug.Result( name="sanitize-repo", msg="Can not create a pull request without a target " "branch, please specify --target-branch", status=plug.Status.ERROR, ) elif not _gitutils.branch_exists(repo_root, self.target_branch): return plug.Result( name="sanitize-repo", msg=f"Can not create a pull request branch from " f"non-existing target branch {self.target_branch}", status=plug.Status.ERROR, )
def create_komp_hookresult(author): komp_issue = plug.Issue( title="Komplettering", body="This is komplettering", number=1, created_at=datetime(2009, 12, 31), author=author, ) return plug.Result( name="list-issues", status=plug.Status.SUCCESS, msg=None, data={komp_issue.number: komp_issue.to_dict()}, )
def create_pass_hookresult(author, number=3): pass_issue = plug.Issue( title="Pass", body="This is a pass", number=number, created_at=datetime(1992, 9, 19), author=author, ) return plug.Result( name="list-issues", status=plug.Status.SUCCESS, msg=None, data={pass_issue.number: pass_issue.to_dict()}, )
def mocked_hook_results(mocker): """Hook results with passes for glassey-glennol in week-1 and week-2, and for slarse in week-4 and week-6. """ slarse, glassey_glennol = TEAMS gen_name = _marker.generate_repo_name hook_results = { gen_name(str(team), repo_name): [result] for team, repo_name, result in [ (slarse, "week-1", create_komp_hookresult(SLARSE_TA)), (slarse, "week-2", create_komp_hookresult(SLARSE_TA)), (slarse, "week-4", create_duplicated_pass_hookresult(SLARSE_TA)), (slarse, "week-6", create_komp_and_pass_hookresult(SLARSE_TA)), ( glassey_glennol, "week-1", create_pass_hookresult(GLASSEY_GLENNOL_TA), ), ( glassey_glennol, "week-2", create_komp_and_pass_hookresult(GLASSEY_GLENNOL_TA), ), ( glassey_glennol, "week-4", create_komp_hookresult(GLASSEY_GLENNOL_TA), ), ( glassey_glennol, "week-6", create_komp_hookresult(GLASSEY_GLENNOL_TA), ), ] } hook_results["list-issues"] = [ plug.Result( name="list-issues", status=plug.Status.SUCCESS, msg=None, data={"state": plug.IssueState.ALL.value}, ) ] mocker.patch( "repobee_csvgrades._file.read_results_file", return_value=hook_results, autospec=True, ) return hook_results
def post_setup(self, repo: plug.StudentRepo, api: plug.PlatformAPI): """Add a created student repo to the teachers team.""" platform_repo = next(iter(api.get_repos([repo.url]))) teachers_team = _get_or_create_team(TEACHERS_TEAM_NAME, api) api.assign_repo( team=teachers_team, repo=platform_repo, permission=plug.TeamPermission.PULL, ) return plug.Result( name="tamanager", status=plug.Status.SUCCESS, msg=f"Added to the {TEACHERS_TEAM_NAME} team", )
def _extract_assignment_name(self, repo_name: str) -> str: matches = list(filter(repo_name.endswith, self.args.assignments)) if len(matches) == 1: return matches[0] else: msg = ( "no assignment name matching the student repo" if not matches else "multiple matching master repo names: {}".format( ", ".join(matches) ) ) res = plug.Result(SECTION, plug.Status.ERROR, msg) raise _exception.ActError(res)
def _generate_test_dirs( assignment_names: List[str], branch: str, template_org_name: str, reference_tests_dir: pathlib.Path, api: plug.PlatformAPI, ) -> plug.Result: """Generate test directories for the provided assignments, assuming that they are not already present in the reference tests directory. """ with tempfile.TemporaryDirectory() as tmpdir: workdir = pathlib.Path(tmpdir) assignment_test_classes = {} for assignment_name in assignment_names: try: extracted_test_classes = _generate_assignment_tests_dir( assignment_name, branch, template_org_name, workdir, api) except _CloneError as exc: return plug.Result( name=str(JUNIT4_COMMAND_CATEGORY.generate_rtd), msg=f"Failed to clone template for " f"'{exc.dir_name}' on branch '{exc.branch}'. " "Ensure that the repo and branch exist.", status=plug.Status.ERROR, ) assignment_test_classes[assignment_name] = extracted_test_classes for test_dir in workdir.iterdir(): shutil.copytree(src=test_dir, dst=reference_tests_dir / test_dir.name) return plug.Result( name=str(JUNIT4_COMMAND_CATEGORY.generate_rtd), msg=_format_success_message(assignment_test_classes), status=plug.Status.SUCCESS, )
def test_does_not_overwrite_lower_priority_grades(self, tmp_grades_file): """Test that e.g. a grade with priority 3 does not overwrite a grade with priority 1 that is already in the grades file. """ shutil.copy(str(EXPECTED_GRADES_MULTI_SPEC_FILE), tmp_grades_file) slarse, *_ = TEAMS hook_result_mapping = { _marker.generate_repo_name(str(slarse), "week-4"): [create_komp_hookresult(SLARSE_TA)], "list-issues": [ plug.Result( name="list-issues", status=plug.Status.SUCCESS, msg=None, data={"state": plug.IssueState.ALL.value}, ) ], } grades_file_contents = tmp_grades_file.read_text("utf8") edit_msg_file = tmp_grades_file.parent / "editmsg.txt" args = argparse.Namespace( students=[slarse], hook_results_file="", # don't care, read_results_file is mocked grades_file=tmp_grades_file, assignments=["week-4"], edit_msg_file=str(edit_msg_file), teachers=list(TEACHERS), grade_specs=[PASS_GRADESPEC_FORMAT, KOMP_GRADESPEC_FORMAT], allow_other_states=False, ) with mock.patch( "repobee_csvgrades._file.read_results_file", autospec=True, return_value=hook_result_mapping, ): csvgrades.callback(args=args) assert tmp_grades_file.read_text("utf8") == grades_file_contents assert not edit_msg_file.exists()
def command(self, api: plug.PlatformAPI) -> Optional[plug.Result]: teachers_team = _get_or_create_team(TEACHERS_TEAM_NAME, api) existing_members = teachers_team.members new_members = list(set(self.teachers) - set(existing_members)) api.assign_members(teachers_team, new_members, permission=plug.TeamPermission.PULL) for repo in plug.cli.io.progress_bar( api.get_repos(), desc="Granting read access to repos"): api.assign_repo( repo=repo, team=teachers_team, permission=plug.TeamPermission.PULL, ) msg = (f"Added {', '.join(new_members)} to the '{TEACHERS_TEAM_NAME}' " "team") return plug.Result(name="add-teachers", status=plug.Status.SUCCESS, msg=msg)
def command(self, api: plug.PlatformAPI): existing_test_dirs = _get_existing_assignment_test_dirs( self.junit4_reference_tests_dir, self.args.assignments) if existing_test_dirs: return plug.Result( name=str(JUNIT4_COMMAND_CATEGORY.generate_rtd), msg=_format_failure_message(existing_test_dirs), status=plug.Status.ERROR, ) assignment_names_progress = plug.cli.io.progress_bar( self.args.assignments, desc="Processing template repos", unit="repo", ) return _generate_test_dirs( assignment_names_progress, branch=self.branch, template_org_name=self.args.template_org_name, reference_tests_dir=self.junit4_reference_tests_dir, api=api, )
def hook_result_mapping(): hook_results = collections.defaultdict(list) for repo_name, hook_name, status, msg, data in [ ( "slarse-task-1", "junit4", plug.Status.SUCCESS, "All tests passed", { "extra": "data", "arbitrary": { "nesting": "here" } }, ), ( "slarse-task-1", "javac", plug.Status.ERROR, "Some stuff failed", None, ), ( "glassey-task-2", "pylint", plug.Status.WARNING, "-10/10 code quality", None, ), ]: hook_results[repo_name].append( plug.Result(name=hook_name, status=status, msg=msg, data=data)) return { repo_name: sorted(results) for reponame, results in hook_results.items() }
def list_issues( repos: Iterable[plug.StudentRepo], api: plug.PlatformAPI, state: plug.IssueState = plug.IssueState.OPEN, title_regex: str = "", show_body: bool = False, author: Optional[str] = None, double_blind_key: Optional[str] = None, ) -> Mapping[str, List[plug.Result]]: """List all issues in the specified repos. Args: repos: The repos from which to fetch issues. api: An implementation of :py:class:`repobee_plug.PlatformAPI` used to interface with the platform (e.g. GitHub or GitLab) instance. state: state of the repo (open or closed). Defaults to open. title_regex: If specified, only issues with titles matching the regex are displayed. Defaults to the empty string (which matches everything). show_body: If True, the body of the issue is displayed along with the default info. author: Only show issues by this author. double_blind_key: If provided, use to deanonymize anonymous repos. """ # TODO optimize by not getting all repos at once repos = list(repos) repo_names = [repo.name for repo in repos] max_repo_name_length = max(map(len, repo_names)) issues_per_repo = _get_issue_generator( repos, title_regex=title_regex, author=author, state=state, double_blind_key=double_blind_key, api=api, ) # _log_repo_issues exhausts the issues_per_repo iterator and # returns a list with the same information. It's important to # have issues_per_repo as an iterator as it greatly speeds # up visual feedback to the user when fetching many issues pers_issues_per_repo = _log_repo_issues(issues_per_repo, show_body, max_repo_name_length + 6) # for writing to JSON hook_result_mapping = { repo.name: [ plug.Result( name="list-issues", status=plug.Status.SUCCESS, msg=f"Fetched {len(issues)} issues from {repo.name}", data={issue.number: issue.to_dict() for issue in issues}, ) ] for repo, issues in pers_issues_per_repo } # meta hook result hook_result_mapping["list-issues"] = [ plug.Result( name="meta", status=plug.Status.SUCCESS, msg="Meta info about the list-issues hook results", data={"state": state.value}, ) ] # new experimental format for repo data used by `issues list` with # --hook-results-file repos_data = {repo.url: dataclasses.asdict(repo) for repo in repos} for repo, issues in pers_issues_per_repo: repos_data[repo.url]["issues"] = { issue.number: issue.to_dict() for issue in issues } hook_result_mapping["repos"] = [ plug.Result("repos", plug.Status.SUCCESS, "repo_data", data=repos_data) ] return hook_result_mapping
def post_clone( self, repo: plug.StudentRepo, api: plug.PlatformAPI ) -> plug.Result: """Look for production classes in the student repo corresponding to test classes in the reference tests directory. Assumes that all test classes end in ``Test.java`` and that there is a directory with the same name as the master repo in the reference tests directory. Args: repo: A student repo. api: An instance of the platform API. Returns: a plug.Result specifying the outcome. """ self._check_jars_exist() if not pathlib.Path(self.junit4_reference_tests_dir).is_dir(): raise plug.PlugError( "{} is not a directory".format(self.junit4_reference_tests_dir) ) assert self.args.assignments assert self.junit4_reference_tests_dir try: if not repo.path.exists(): return plug.Result( SECTION, plug.Status.ERROR, "student repo {!s} does not exist".format(repo.path), ) compile_succeeded, compile_failed = self._compile_all(repo) test_results = self._run_tests(compile_succeeded) has_failures = compile_failed or any( map(lambda r: not r.success, test_results) ) msg = _output.format_results( test_results, compile_failed, self.junit4_verbose, self.junit4_very_verbose, ) status = ( plug.Status.ERROR if compile_failed else ( plug.Status.WARNING if has_failures else plug.Status.SUCCESS ) ) return plug.Result(SECTION, status, msg) except _exception.ActError as exc: return exc.hook_result except Exception as exc: plug.log.exception("critical") return plug.Result(SECTION, plug.Status.ERROR, str(exc))