Beispiel #1
0
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
        },
    )
Beispiel #2
0
    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)
Beispiel #3
0
    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,
        )
Beispiel #4
0
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
Beispiel #5
0
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)
Beispiel #7
0
 def command(self):
     return plug.Result(
         name=self.__plugin_name__,
         msg="Nice!",
         status=plug.Status.SUCCESS,
         data={"name": self.name, "age": self.age},
     )
Beispiel #8
0
 def command(self):
     return plug.Result(
         name="workdir",
         msg="workdir",
         status=plug.Status.SUCCESS,
         data={"cwd": os.getcwd()},
     )
Beispiel #9
0
    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,
        )
Beispiel #10
0
    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,
        )
Beispiel #11
0
    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
        },
    )
Beispiel #14
0
    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
Beispiel #18
0
    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",
        )
Beispiel #19
0
    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)
Beispiel #20
0
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()
Beispiel #22
0
    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)
Beispiel #23
0
    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,
        )
Beispiel #24
0
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()
    }
Beispiel #25
0
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
Beispiel #26
0
    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))