Skip to content

Commit 69e4851

Browse files
Implement T9L1/create-pr-from-main (#276)
# Exercise Review ## Exercise Discussion Fixes #274 ## Checklist - [ ] If you require a new remote repository on the `Git-Mastery` organization, have you [created a request](https://github.com/git-mastery/exercises/issues/new?template=request_exercise_repository.yaml) for it? - [x] Have you written unit tests using [`repo-smith`](https://github.com/git-mastery/repo-smith) to validate the exercise grading scheme? - [x] Have you tested your changes using the instructions posted? - [x] Have you verified that this exercise does not already exist or is not currently in review? - [ ] Did you introduce a new grading mechanism that should belong to [`git-autograder`](https://github.com/git-mastery/git-autograder)? - [ ] Did you introduce a new dependency that should belong to [`app`](https://github.com/git-mastery/app)?
1 parent b0a66f7 commit 69e4851

6 files changed

Lines changed: 193 additions & 0 deletions

File tree

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"exercise_name": "create-pr-from-main",
3+
"tags": [],
4+
"requires_git": true,
5+
"requires_github": true,
6+
"base_files": {},
7+
"exercise_repo": {
8+
"repo_type": "remote",
9+
"repo_name": "languages",
10+
"repo_title": "gm-languages",
11+
"create_fork": true,
12+
"init": null
13+
}
14+
}

create_pr_from_main/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
See https://git-mastery.org/lessons/prsCreate/exercise-create-pr-from-main.html

create_pr_from_main/__init__.py

Whitespace-only changes.

create_pr_from_main/download.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from pathlib import Path
2+
3+
from exercise_utils.exercise_config import add_pr_config
4+
from exercise_utils.gitmastery import create_start_tag
5+
6+
def setup(verbose: bool = False):
7+
create_start_tag(verbose)
8+
add_pr_config(pr_repo_full_name="git-mastery/gm-languages", config_path=Path("../"))

create_pr_from_main/test_verify.py

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
from contextlib import contextmanager
2+
from unittest.mock import PropertyMock, patch
3+
4+
import pytest
5+
from exercise_utils.test import GitAutograderTestLoader, assert_output
6+
from git_autograder import (
7+
GitAutograderStatus,
8+
GitAutograderWrongAnswerException,
9+
)
10+
from git_autograder.pr import GitAutograderPr
11+
12+
from .verify import (
13+
EXPECTED_CONTENT_STEP_3,
14+
JAVA_FILE_MISSING,
15+
JAVA_INVALID_CONTENT,
16+
PR_MISSING,
17+
WRONG_HEAD_BRANCH,
18+
verify,
19+
)
20+
21+
REPOSITORY_NAME = "create-pr-from-main"
22+
23+
loader = GitAutograderTestLoader(REPOSITORY_NAME, verify)
24+
25+
# NOTE: This exercise is a special case where we do not require repo-smith. Instead,
26+
# we directly mock function calls to verify that all branches are covered for us.
27+
28+
29+
class FakeCommit:
30+
def __init__(self, java_content: str | None) -> None:
31+
self._java_content = java_content
32+
33+
@contextmanager
34+
def file(self, file_path: str):
35+
yield self._java_content
36+
37+
38+
def _run_verify(
39+
pr_numbers: list[int] | None = None,
40+
head_branch: str = "",
41+
java_content: str | None = None,
42+
):
43+
if pr_numbers is None:
44+
pr_numbers = []
45+
fake_commit = FakeCommit(java_content)
46+
with (
47+
loader.start_mock_exercise(
48+
has_pr_context=True, pr_number=1, pr_repo_full_name="dummy/repo"
49+
) as exercise,
50+
patch(
51+
"create_pr_from_main.verify.get_pr_numbers_by_author",
52+
return_value=pr_numbers,
53+
),
54+
patch("create_pr_from_main.verify.add_pr_config"),
55+
patch.object(exercise, "fetch_pr", return_value=None),
56+
patch.object(
57+
GitAutograderPr,
58+
"head_branch",
59+
new_callable=PropertyMock,
60+
return_value=head_branch,
61+
),
62+
patch.object(
63+
GitAutograderPr,
64+
"last_user_commit",
65+
new_callable=PropertyMock,
66+
return_value=fake_commit,
67+
),
68+
):
69+
return verify(exercise)
70+
71+
72+
def test_success():
73+
output = _run_verify(
74+
pr_numbers=[123],
75+
head_branch="main",
76+
java_content="\n".join(EXPECTED_CONTENT_STEP_3),
77+
)
78+
79+
assert_output(output, GitAutograderStatus.SUCCESSFUL)
80+
81+
82+
def test_pr_missing():
83+
with pytest.raises(GitAutograderWrongAnswerException) as exception:
84+
_run_verify()
85+
86+
assert exception.value.message == [PR_MISSING]
87+
88+
89+
def test_wrong_head_branch():
90+
with pytest.raises(GitAutograderWrongAnswerException) as exception:
91+
_run_verify(pr_numbers=[1], head_branch="feature/pr-branch")
92+
93+
assert exception.value.message == [WRONG_HEAD_BRANCH]
94+
95+
96+
def test_java_file_missing():
97+
with pytest.raises(GitAutograderWrongAnswerException) as exception:
98+
_run_verify(
99+
pr_numbers=[1],
100+
head_branch="main",
101+
java_content=None,
102+
)
103+
104+
assert exception.value.message == [JAVA_FILE_MISSING]
105+
106+
107+
def test_java_content_invalid():
108+
with pytest.raises(GitAutograderWrongAnswerException) as exception:
109+
_run_verify(
110+
pr_numbers=[1],
111+
head_branch="main",
112+
java_content="wrong content\n",
113+
)
114+
115+
assert exception.value.message == [JAVA_INVALID_CONTENT]

create_pr_from_main/verify.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
from pathlib import Path
2+
3+
from git_autograder import (
4+
GitAutograderOutput,
5+
GitAutograderExercise,
6+
GitAutograderStatus,
7+
)
8+
9+
from exercise_utils.exercise_config import add_pr_config
10+
from exercise_utils.github_cli import get_github_username, get_pr_numbers_by_author
11+
12+
13+
JAVA_FILE_MISSING = "Java.txt file is missing in the latest commit on main branch."
14+
JAVA_INVALID_CONTENT = "The content in Java.txt in main branch is not correct."
15+
MULTIPLE_PRS = "Multiple PRs found. The latest pr will be used in grading."
16+
PR_MISSING = "No PR is found."
17+
WRONG_HEAD_BRANCH = "The PR's head branch is not 'main'."
18+
19+
20+
EXPECTED_CONTENT_STEP_3 = ["1995, by James Gosling"]
21+
22+
23+
def verify(exercise: GitAutograderExercise) -> GitAutograderOutput:
24+
username = get_github_username(False)
25+
target_repo = f"git-mastery/{exercise.config.exercise_repo.repo_title}"
26+
comments = []
27+
28+
pr_numbers = get_pr_numbers_by_author(username, target_repo, False)
29+
if not pr_numbers:
30+
raise exercise.wrong_answer([PR_MISSING])
31+
if len(pr_numbers) > 1:
32+
comments.append(MULTIPLE_PRS)
33+
pr_number = pr_numbers[-1]
34+
35+
add_pr_config(pr_number=pr_number, config_path=Path("./"))
36+
exercise.fetch_pr()
37+
38+
if exercise.repo.prs.pr.head_branch != "main":
39+
comments.append(WRONG_HEAD_BRANCH)
40+
raise exercise.wrong_answer(comments)
41+
42+
latest_user_commit = exercise.repo.prs.pr.last_user_commit
43+
with latest_user_commit.file("Java.txt") as content:
44+
if content is None:
45+
comments.append(JAVA_FILE_MISSING)
46+
raise exercise.wrong_answer(comments)
47+
extracted_content = [
48+
line.strip() for line in content.splitlines() if line.strip() != ""
49+
]
50+
if extracted_content != EXPECTED_CONTENT_STEP_3:
51+
comments.append(JAVA_INVALID_CONTENT)
52+
raise exercise.wrong_answer(comments)
53+
54+
comments.append("Good job creating the PR and pushing commits!")
55+
return exercise.to_output(comments, GitAutograderStatus.SUCCESSFUL)

0 commit comments

Comments
 (0)