Skip to content

Commit acc944a

Browse files
VikramGoyal23jovnc
andauthored
[tags-push] Implement exercise T4L2/tags-push (#95)
# Exercise Review ## Exercise Discussion #63 ## 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.md) 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 the download script using `test-download.sh`? - [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)? --------- Co-authored-by: jovnc <[email protected]>
1 parent 998c55b commit acc944a

File tree

8 files changed

+243
-0
lines changed

8 files changed

+243
-0
lines changed
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"exercise_name": "tags-push",
3+
"tags": [
4+
"git-tag"
5+
],
6+
"requires_git": true,
7+
"requires_github": true,
8+
"base_files": {},
9+
"exercise_repo": {
10+
"repo_type": "remote",
11+
"repo_name": "duty-roster",
12+
"repo_title": "gm-duty-roster",
13+
"create_fork": true,
14+
"init": null
15+
}
16+
}

tags_push/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
See https://git-mastery.github.io/lessons/tag/exercise-tags-push.html

tags_push/__init__.py

Whitespace-only changes.

tags_push/download.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from exercise_utils.cli import run_command
2+
from exercise_utils.git import tag, tag_with_options
3+
4+
5+
def setup(verbose: bool = False):
6+
run_command(["git", "remote", "rename", "origin", "production"], verbose)
7+
tag("beta", verbose)
8+
run_command(["git", "push", "production", "--tags"], verbose)
9+
run_command(["git", "tag", "-d", "beta"], verbose)
10+
11+
tag_with_options("v1.0", ["HEAD~4"], verbose)
12+
tag_with_options("v2.0", ["-a", "HEAD~1", "-m", "First stable roster"], verbose)

tags_push/tests/__init__.py

Whitespace-only changes.

tags_push/tests/specs/base.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
initialization:
2+
steps:
3+
- type: commit
4+
empty: true
5+
message: Empty commit
6+
id: start

tags_push/tests/test_verify.py

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import json
2+
from pathlib import Path
3+
from unittest.mock import patch
4+
5+
import pytest
6+
from git.repo import Repo
7+
8+
from git_autograder import (
9+
GitAutograderExercise,
10+
GitAutograderStatus,
11+
GitAutograderTestLoader,
12+
GitAutograderWrongAnswerException,
13+
assert_output,
14+
)
15+
16+
from ..verify import (
17+
IMPROPER_GH_CLI_SETUP,
18+
TAG_1_NAME,
19+
TAG_2_NAME,
20+
TAG_DELETE_NAME,
21+
TAG_1_MISSING,
22+
TAG_2_MISSING,
23+
TAG_DELETE_NOT_REMOVED,
24+
verify,
25+
)
26+
27+
REPOSITORY_NAME = "tags-push"
28+
29+
loader = GitAutograderTestLoader(__file__, REPOSITORY_NAME, verify)
30+
31+
32+
# NOTE: This exercise is a special case where we do not require repo-smith. Instead,
33+
# we directly mock function calls to verify that all branches are covered for us.
34+
35+
36+
# TODO: The current tooling isn't mature enough to handle mock GitAutograderExercise in
37+
# cases like these. We would ideally need some abstraction rather than creating our own.
38+
39+
40+
@pytest.fixture
41+
def exercise(tmp_path: Path) -> GitAutograderExercise:
42+
repo_dir = tmp_path / "ignore-me"
43+
repo_dir.mkdir()
44+
45+
Repo.init(repo_dir)
46+
with open(tmp_path / ".gitmastery-exercise.json", "a") as config_file:
47+
config_file.write(
48+
json.dumps(
49+
{
50+
"exercise_name": "tags-push",
51+
"tags": [],
52+
"requires_git": True,
53+
"requires_github": True,
54+
"base_files": {},
55+
"exercise_repo": {
56+
"repo_type": "local",
57+
"repo_name": "ignore-me",
58+
"init": True,
59+
"create_fork": None,
60+
"repo_title": None,
61+
},
62+
"downloaded_at": None,
63+
}
64+
)
65+
)
66+
67+
exercise = GitAutograderExercise(exercise_path=tmp_path)
68+
return exercise
69+
70+
71+
def test_pass(exercise: GitAutograderExercise):
72+
with (
73+
patch("tags_push.verify.get_username", return_value="dummy"),
74+
patch(
75+
"tags_push.verify.get_remote_tags", return_value=[TAG_1_NAME, TAG_2_NAME]
76+
),
77+
):
78+
output = verify(exercise)
79+
assert_output(output, GitAutograderStatus.SUCCESSFUL)
80+
81+
82+
def test_improper_gh_setup(exercise: GitAutograderExercise):
83+
with (
84+
patch("tags_push.verify.get_username", return_value=None),
85+
patch(
86+
"tags_push.verify.get_remote_tags", return_value=[TAG_1_NAME, TAG_2_NAME]
87+
),
88+
pytest.raises(GitAutograderWrongAnswerException, match=IMPROPER_GH_CLI_SETUP),
89+
):
90+
verify(exercise)
91+
92+
93+
def test_beta_present(exercise: GitAutograderExercise):
94+
with (
95+
patch("tags_push.verify.get_username", return_value="dummy"),
96+
patch(
97+
"tags_push.verify.get_remote_tags",
98+
return_value=[TAG_1_NAME, TAG_2_NAME, TAG_DELETE_NAME],
99+
),
100+
pytest.raises(GitAutograderWrongAnswerException, match=TAG_DELETE_NOT_REMOVED),
101+
):
102+
verify(exercise)
103+
104+
105+
def test_tag_1_absent(exercise: GitAutograderExercise):
106+
with (
107+
patch("tags_push.verify.get_username", return_value="dummy"),
108+
patch("tags_push.verify.get_remote_tags", return_value=[TAG_2_NAME]),
109+
pytest.raises(GitAutograderWrongAnswerException, match=TAG_1_MISSING),
110+
):
111+
verify(exercise)
112+
113+
114+
def test_tag_2_absent(exercise: GitAutograderExercise):
115+
with (
116+
patch("tags_push.verify.get_username", return_value="dummy"),
117+
patch("tags_push.verify.get_remote_tags", return_value=[TAG_1_NAME]),
118+
pytest.raises(GitAutograderWrongAnswerException, match=TAG_2_MISSING),
119+
):
120+
verify(exercise)
121+
122+
123+
def test_all_wrong(exercise: GitAutograderExercise):
124+
with (
125+
patch("tags_push.verify.get_username", return_value="dummy"),
126+
patch("tags_push.verify.get_remote_tags", return_value=[TAG_DELETE_NAME]),
127+
pytest.raises(GitAutograderWrongAnswerException) as exception,
128+
):
129+
verify(exercise)
130+
131+
assert exception.value.message == [
132+
TAG_1_MISSING,
133+
TAG_2_MISSING,
134+
TAG_DELETE_NOT_REMOVED,
135+
]

tags_push/verify.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import os
2+
import subprocess
3+
from typing import List, Optional
4+
5+
from git_autograder import (
6+
GitAutograderOutput,
7+
GitAutograderExercise,
8+
GitAutograderStatus,
9+
)
10+
11+
IMPROPER_GH_CLI_SETUP = "Your Github CLI is not setup correctly"
12+
13+
TAG_1_NAME = "v1.0"
14+
TAG_2_NAME = "v2.0"
15+
TAG_DELETE_NAME = "beta"
16+
17+
TAG_1_MISSING = f"Tag {TAG_1_NAME} is missing, did you push it to the remote?"
18+
TAG_2_MISSING = f"Tag {TAG_2_NAME} is missing, did you push it to the remote?"
19+
TAG_DELETE_NOT_REMOVED = f"Tag {TAG_DELETE_NAME} is still on the remote!"
20+
21+
22+
def run_command(command: List[str]) -> Optional[str]:
23+
try:
24+
result = subprocess.run(
25+
command,
26+
capture_output=True,
27+
text=True,
28+
check=True,
29+
env=dict(os.environ, **{"GH_PAGER": "cat"}),
30+
)
31+
return result.stdout.strip()
32+
except subprocess.CalledProcessError:
33+
return None
34+
35+
36+
def get_username() -> Optional[str]:
37+
return run_command(["gh", "api", "user", "-q", ".login"])
38+
39+
40+
def get_remote_tags(username: str) -> List[str]:
41+
raw_tags = run_command(["git", "ls-remote", "--tags"])
42+
if raw_tags is None:
43+
return []
44+
return [line.split("/")[2] for line in raw_tags.strip().splitlines()]
45+
46+
47+
def verify(exercise: GitAutograderExercise) -> GitAutograderOutput:
48+
username = get_username()
49+
if username is None:
50+
raise exercise.wrong_answer([IMPROPER_GH_CLI_SETUP])
51+
52+
tag_names = get_remote_tags(username)
53+
54+
comments = []
55+
56+
if TAG_1_NAME not in tag_names:
57+
comments.append(TAG_1_MISSING)
58+
59+
if TAG_2_NAME not in tag_names:
60+
comments.append(TAG_2_MISSING)
61+
62+
if TAG_DELETE_NAME in tag_names:
63+
comments.append(TAG_DELETE_NOT_REMOVED)
64+
65+
if comments:
66+
raise exercise.wrong_answer(comments)
67+
68+
return exercise.to_output(
69+
[
70+
"Wonderful! You have successfully synced the local tags with the remote tags!"
71+
],
72+
GitAutograderStatus.SUCCESSFUL,
73+
)

0 commit comments

Comments
 (0)