Skip to content

Commit 35baadd

Browse files
Refactor exercise unit test by using helper functions to mock repos (#284)
1 parent c2b8a79 commit 35baadd

File tree

4 files changed

+136
-157
lines changed

4 files changed

+136
-157
lines changed

clone_repo/test_verify.py

Lines changed: 32 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
1-
import json
2-
from pathlib import Path
31
from unittest.mock import patch
42

53
import pytest
64
from exercise_utils.test import GitAutograderTestLoader, assert_output
7-
from git.repo import Repo
8-
from git_autograder import GitAutograderExercise, GitAutograderWrongAnswerException
5+
from git_autograder import GitAutograderWrongAnswerException
96
from git_autograder.status import GitAutograderStatus
107

118
from .verify import (
@@ -28,49 +25,15 @@
2825
# we directly mock function calls to verify that all branches are covered for us.
2926

3027

31-
# TODO: The current tooling isn't mature enough to handle mock GitAutograderExercise in
32-
# cases like these. We would ideally need some abstraction rather than creating our own.
33-
34-
35-
@pytest.fixture
36-
def exercise(tmp_path: Path) -> GitAutograderExercise:
37-
repo_dir = tmp_path / "ignore-me"
38-
repo_dir.mkdir()
39-
40-
Repo.init(repo_dir)
41-
with open(tmp_path / ".gitmastery-exercise.json", "a") as config_file:
42-
config_file.write(
43-
json.dumps(
44-
{
45-
"exercise_name": "clone-repo",
46-
"tags": [],
47-
"requires_git": True,
48-
"requires_github": True,
49-
"base_files": {},
50-
"exercise_repo": {
51-
"repo_type": "ignore",
52-
"repo_name": "ignore-me",
53-
"init": True,
54-
"create_fork": None,
55-
"repo_title": None,
56-
},
57-
"downloaded_at": None,
58-
}
59-
)
60-
)
61-
62-
exercise = GitAutograderExercise(exercise_path=tmp_path)
63-
return exercise
64-
65-
66-
def test_pass(exercise: GitAutograderExercise):
28+
def test_pass():
6729
fake_origin = type(
6830
"FakeRemote", (), {"url": "https://github.com/dummy/gm-shapes.git"}
6931
)()
7032
fake_upstream = type(
7133
"FakeRemote", (), {"url": "https://github.com/git-mastery/gm-shapes.git"}
7234
)()
7335
with (
36+
loader.start_mock_exercise() as exercise,
7437
patch("clone_repo.verify.get_username", return_value="dummy"),
7538
patch("clone_repo.verify.has_fork", return_value=True),
7639
patch("clone_repo.verify.is_parent_git_mastery", return_value=True),
@@ -90,56 +53,62 @@ def test_pass(exercise: GitAutograderExercise):
9053
assert_output(output, GitAutograderStatus.SUCCESSFUL)
9154

9255

93-
def test_improper_gh_setup(exercise: GitAutograderExercise):
56+
def test_improper_gh_setup():
9457
with (
58+
loader.start_mock_exercise() as exercise,
9559
patch("clone_repo.verify.get_username", return_value=None),
9660
patch("clone_repo.verify.has_fork", return_value=True),
9761
patch("clone_repo.verify.is_parent_git_mastery", return_value=True),
9862
pytest.raises(GitAutograderWrongAnswerException) as exception,
9963
):
10064
verify(exercise)
10165

102-
assert exception.value.message == [IMPROPER_GH_CLI_SETUP]
66+
assert exception.value.message == [IMPROPER_GH_CLI_SETUP]
10367

10468

105-
def test_no_fork(exercise: GitAutograderExercise):
69+
def test_no_fork():
10670
with (
71+
loader.start_mock_exercise() as exercise,
10772
patch("clone_repo.verify.get_username", return_value="dummy"),
10873
patch("clone_repo.verify.has_fork", return_value=False),
10974
patch("clone_repo.verify.is_parent_git_mastery", return_value=True),
11075
pytest.raises(GitAutograderWrongAnswerException) as exception,
11176
):
11277
verify(exercise)
11378

114-
assert exception.value.message == [NO_FORK_FOUND]
79+
assert exception.value.message == [NO_FORK_FOUND]
11580

11681

117-
def test_not_right_parent(exercise: GitAutograderExercise):
82+
def test_not_right_parent():
11883
with (
84+
loader.start_mock_exercise() as exercise,
11985
patch("clone_repo.verify.get_username", return_value="dummy"),
12086
patch("clone_repo.verify.has_fork", return_value=True),
12187
patch("clone_repo.verify.is_parent_git_mastery", return_value=False),
12288
pytest.raises(GitAutograderWrongAnswerException) as exception,
12389
):
12490
verify(exercise)
12591

126-
assert exception.value.message == [NOT_GIT_MASTERY_FORK]
92+
assert exception.value.message == [NOT_GIT_MASTERY_FORK]
12793

12894

129-
def test_missing_shapes_folder(exercise: GitAutograderExercise):
95+
def test_missing_shapes_folder():
13096
with (
97+
loader.start_mock_exercise() as exercise,
13198
patch("clone_repo.verify.get_username", return_value="dummy"),
13299
patch("clone_repo.verify.has_fork", return_value=True),
133100
patch("clone_repo.verify.is_parent_git_mastery", return_value=True),
134101
patch("clone_repo.verify.has_shapes_folder", return_value=False),
135102
pytest.raises(GitAutograderWrongAnswerException) as exception,
136103
):
137104
verify(exercise)
138-
assert exception.value.message == [CLONE_MISSING]
139105

106+
assert exception.value.message == [CLONE_MISSING]
140107

141-
def test_missing_origin_remote(exercise: GitAutograderExercise):
108+
109+
def test_missing_origin_remote():
142110
with (
111+
loader.start_mock_exercise() as exercise,
143112
patch("clone_repo.verify.get_username", return_value="dummy"),
144113
patch("clone_repo.verify.has_fork", return_value=True),
145114
patch("clone_repo.verify.is_parent_git_mastery", return_value=True),
@@ -151,12 +120,14 @@ def test_missing_origin_remote(exercise: GitAutograderExercise):
151120
pytest.raises(GitAutograderWrongAnswerException) as exception,
152121
):
153122
verify(exercise)
154-
assert exception.value.message == [ORIGIN_MISSING]
123+
124+
assert exception.value.message == [ORIGIN_MISSING]
155125

156126

157-
def test_wrong_origin_remote_url(exercise: GitAutograderExercise):
127+
def test_wrong_origin_remote_url():
158128
fake_origin = type("FakeRemote", (), {"url": "https://github.com/wrong/repo.git"})()
159129
with (
130+
loader.start_mock_exercise() as exercise,
160131
patch("clone_repo.verify.get_username", return_value="dummy"),
161132
patch("clone_repo.verify.has_fork", return_value=True),
162133
patch("clone_repo.verify.is_parent_git_mastery", return_value=True),
@@ -168,14 +139,16 @@ def test_wrong_origin_remote_url(exercise: GitAutograderExercise):
168139
pytest.raises(GitAutograderWrongAnswerException) as exception,
169140
):
170141
verify(exercise)
171-
assert exception.value.message == [ORIGIN_WRONG]
142+
143+
assert exception.value.message == [ORIGIN_WRONG]
172144

173145

174-
def test_missing_upstream_remote(exercise: GitAutograderExercise):
146+
def test_missing_upstream_remote():
175147
fake_origin = type(
176148
"FakeRemote", (), {"url": "https://github.com/dummy/gm-shapes.git"}
177149
)()
178150
with (
151+
loader.start_mock_exercise() as exercise,
179152
patch("clone_repo.verify.get_username", return_value="dummy"),
180153
patch("clone_repo.verify.has_fork", return_value=True),
181154
patch("clone_repo.verify.is_parent_git_mastery", return_value=True),
@@ -187,17 +160,19 @@ def test_missing_upstream_remote(exercise: GitAutograderExercise):
187160
pytest.raises(GitAutograderWrongAnswerException) as exception,
188161
):
189162
verify(exercise)
190-
assert exception.value.message == [UPSTREAM_MISSING]
191163

164+
assert exception.value.message == [UPSTREAM_MISSING]
192165

193-
def test_wrong_upstream_remote_url(exercise: GitAutograderExercise):
166+
167+
def test_wrong_upstream_remote_url():
194168
fake_origin = type(
195169
"FakeRemote", (), {"url": "https://github.com/dummy/gm-shapes.git"}
196170
)()
197171
fake_upstream = type(
198172
"FakeRemote", (), {"url": "https://github.com/wrong/repo.git"}
199173
)()
200174
with (
175+
loader.start_mock_exercise() as exercise,
201176
patch("clone_repo.verify.get_username", return_value="dummy"),
202177
patch("clone_repo.verify.has_fork", return_value=True),
203178
patch("clone_repo.verify.is_parent_git_mastery", return_value=True),
@@ -209,4 +184,5 @@ def test_wrong_upstream_remote_url(exercise: GitAutograderExercise):
209184
pytest.raises(GitAutograderWrongAnswerException) as exception,
210185
):
211186
verify(exercise)
212-
assert exception.value.message == [UPSTREAM_WRONG]
187+
188+
assert exception.value.message == [UPSTREAM_WRONG]

exercise_utils/test.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import json
12
import os
23
import tempfile
34
from contextlib import contextmanager
@@ -275,6 +276,71 @@ def start(
275276
with test as (ctx, rs, rs_remote):
276277
yield ctx, rs
277278

279+
@contextmanager
280+
def start_mock_exercise(
281+
self,
282+
*,
283+
tags: Optional[List[str]] = None,
284+
requires_git: bool = True,
285+
requires_github: bool = True,
286+
base_files: Optional[Dict[str, str]] = None,
287+
repo_type: str = "local",
288+
repo_name: str = "ignore-me",
289+
init: bool = True,
290+
create_fork: Optional[bool] = None,
291+
repo_title: Optional[str] = None,
292+
has_pr_context: bool = False,
293+
pr_number: Optional[int] = None,
294+
pr_repo_full_name: Optional[str] = None,
295+
downloaded_at: Optional[str] = None,
296+
) -> Iterator[GitAutograderExercise]:
297+
with tempfile.TemporaryDirectory() as temp_dir:
298+
exercise_path = Path(temp_dir)
299+
repo_dir = exercise_path / repo_name
300+
repo_dir.mkdir(parents=True, exist_ok=True)
301+
302+
if repo_type == "local":
303+
repo_dir.mkdir(parents=True, exist_ok=True)
304+
if init:
305+
Repo.init(repo_dir)
306+
307+
exercise_repo: Dict[str, Any] = {
308+
"repo_type": repo_type,
309+
"repo_name": repo_name,
310+
"init": init,
311+
"create_fork": create_fork,
312+
"repo_title": repo_title,
313+
}
314+
config: Dict[str, Any] = {
315+
"exercise_name": self.exercise_name,
316+
"tags": tags or [],
317+
"requires_git": requires_git,
318+
"requires_github": requires_github,
319+
"base_files": base_files or {},
320+
"exercise_repo": exercise_repo,
321+
"downloaded_at": downloaded_at,
322+
}
323+
324+
if has_pr_context:
325+
# If the user does not provide PR context, dummy values will be used.
326+
if pr_number is None:
327+
pr_number = 1
328+
if pr_repo_full_name is None:
329+
pr_repo_full_name = "dummy/repo"
330+
exercise_repo["pr_number"] = pr_number
331+
exercise_repo["pr_repo_full_name"] = pr_repo_full_name
332+
with open(exercise_path / ".gitmastery-exercise.json", "w") as f:
333+
json.dump(config, f)
334+
335+
if has_pr_context:
336+
with mock.patch(
337+
"git_autograder.pr.fetch_pull_request_data",
338+
return_value={},
339+
):
340+
yield GitAutograderExercise(exercise_path=exercise_path)
341+
else:
342+
yield GitAutograderExercise(exercise_path=exercise_path)
343+
278344

279345
def assert_output(
280346
output: GitAutograderOutput,

fork_repo/test_verify.py

Lines changed: 9 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,8 @@
1-
import json
2-
from pathlib import Path
31
from unittest.mock import patch
42

53
import pytest
64
from exercise_utils.test import GitAutograderTestLoader, assert_output
7-
from git.repo import Repo
8-
from git_autograder import (
9-
GitAutograderExercise,
10-
GitAutograderWrongAnswerException,
11-
)
5+
from git_autograder import GitAutograderWrongAnswerException
126
from git_autograder.status import GitAutograderStatus
137

148
from .verify import IMPROPER_GH_CLI_SETUP, NO_FORK_FOUND, NOT_GIT_MASTERY_FORK, verify
@@ -21,43 +15,9 @@
2115
# we directly mock function calls to verify that all branches are covered for us.
2216

2317

24-
# TODO: The current tooling isn't mature enough to handle mock GitAutograderExercise in
25-
# cases like these. We would ideally need some abstraction rather than creating our own.
26-
27-
28-
@pytest.fixture
29-
def exercise(tmp_path: Path) -> GitAutograderExercise:
30-
repo_dir = tmp_path / "ignore-me"
31-
repo_dir.mkdir()
32-
33-
Repo.init(repo_dir)
34-
with open(tmp_path / ".gitmastery-exercise.json", "a") as config_file:
35-
config_file.write(
36-
json.dumps(
37-
{
38-
"exercise_name": "remote-control",
39-
"tags": [],
40-
"requires_git": True,
41-
"requires_github": True,
42-
"base_files": {},
43-
"exercise_repo": {
44-
"repo_type": "local",
45-
"repo_name": "ignore-me",
46-
"init": True,
47-
"create_fork": None,
48-
"repo_title": None,
49-
},
50-
"downloaded_at": None,
51-
}
52-
)
53-
)
54-
55-
exercise = GitAutograderExercise(exercise_path=tmp_path)
56-
return exercise
57-
58-
59-
def test_pass(exercise: GitAutograderExercise):
18+
def test_pass():
6019
with (
20+
loader.start_mock_exercise() as exercise,
6121
patch("fork_repo.verify.get_username", return_value="dummy"),
6222
patch("fork_repo.verify.has_fork", return_value=True),
6323
patch("fork_repo.verify.is_parent_git_mastery", return_value=True),
@@ -66,8 +26,9 @@ def test_pass(exercise: GitAutograderExercise):
6626
assert_output(output, GitAutograderStatus.SUCCESSFUL)
6727

6828

69-
def test_improper_gh_setup(exercise: GitAutograderExercise):
29+
def test_improper_gh_setup():
7030
with (
31+
loader.start_mock_exercise() as exercise,
7132
patch("fork_repo.verify.get_username", return_value=None),
7233
patch("fork_repo.verify.has_fork", return_value=True),
7334
patch("fork_repo.verify.is_parent_git_mastery", return_value=True),
@@ -78,8 +39,9 @@ def test_improper_gh_setup(exercise: GitAutograderExercise):
7839
assert exception.value.message == [IMPROPER_GH_CLI_SETUP]
7940

8041

81-
def test_no_fork(exercise: GitAutograderExercise):
42+
def test_no_fork():
8243
with (
44+
loader.start_mock_exercise() as exercise,
8345
patch("fork_repo.verify.get_username", return_value="dummy"),
8446
patch("fork_repo.verify.has_fork", return_value=False),
8547
patch("fork_repo.verify.is_parent_git_mastery", return_value=True),
@@ -90,8 +52,9 @@ def test_no_fork(exercise: GitAutograderExercise):
9052
assert exception.value.message == [NO_FORK_FOUND]
9153

9254

93-
def test_not_right_parent(exercise: GitAutograderExercise):
55+
def test_not_right_parent():
9456
with (
57+
loader.start_mock_exercise() as exercise,
9558
patch("fork_repo.verify.get_username", return_value="dummy"),
9659
patch("fork_repo.verify.has_fork", return_value=True),
9760
patch("fork_repo.verify.is_parent_git_mastery", return_value=False),

0 commit comments

Comments
 (0)