Skip to content

Commit 58cf604

Browse files
aj3shAjesh Sen Thapa
and
Ajesh Sen Thapa
authored
test: add tests for github actions with coverage (#66)
Co-authored-by: Ajesh Sen Thapa <[email protected]>
1 parent dffb36b commit 58cf604

18 files changed

+848
-10
lines changed

Pipfile

+3-3
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ pytest-cov = "*"
1212

1313
[scripts]
1414
test = "pytest"
15-
coverage = "pytest --cov=src/ --no-cov-on-fail"
16-
coverage-html = "pytest --cov=src/ --cov-report=html --no-cov-on-fail"
17-
coverage-xml = "pytest --cov=src/ --cov-report=xml --no-cov-on-fail"
15+
coverage = "pytest --cov=src --cov=github_actions"
16+
coverage-html = "pytest --cov=src --cov=github_actions --cov-report=html"
17+
coverage-xml = "pytest --cov=src --cov=github_actions --cov-report=xml"
1818
install-hooks = "pre-commit install --hook-type pre-commit --hook-type commit-msg"

github_actions/__main__.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
"""Main entry point for the GitHub Actions workflow."""
22

3-
from action.run import run_action
3+
from action.run import run_action # pragma: no cover
44

5-
run_action()
5+
run_action() # pragma: no cover

github_actions/action/run.py

+3-4
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
import os
77
import subprocess
88
import sys
9-
from math import ceil
109
from typing import Iterable, List, Optional, Tuple, cast
1110

1211
from .event import GitHubEvent
@@ -33,6 +32,7 @@
3332
STATUS_FAILURE = "failure"
3433

3534
MAX_PR_COMMITS = 250
35+
PER_PAGE_COMMITS = 50
3636

3737

3838
def get_push_commit_messages(event: GitHubEvent) -> Iterable[str]:
@@ -75,16 +75,15 @@ def get_pr_commit_messages(event: GitHubEvent) -> Iterable[str]:
7575
)
7676

7777
# pagination
78-
per_page = 50
79-
total_page = ceil(total_commits / per_page)
78+
total_page = 1 + total_commits // PER_PAGE_COMMITS
8079

8180
commits: List[str] = []
8281
for page in range(1, total_page + 1):
8382
status, data = request_github_api(
8483
method="GET",
8584
url=f"/repos/{repo}/pulls/{pr_number}/commits",
8685
token=token,
87-
params={"per_page": per_page, "page": page},
86+
params={"per_page": PER_PAGE_COMMITS, "page": page},
8887
)
8988

9089
if status != 200:

pytest.ini

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
[pytest]
2-
pythonpath = src
2+
pythonpath = . src
33
python_files = test_*.py
44
addopts = -vvv

tests/fixtures/actions_env.py

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# type: ignore
2+
# pylint: disable=all
3+
import os
4+
5+
6+
def set_github_env_vars():
7+
# GitHub Action event env
8+
os.environ["GITHUB_EVENT_NAME"] = "push"
9+
os.environ["GITHUB_SHA"] = "commitlint_sha"
10+
os.environ["GITHUB_REF"] = "refs/heads/main"
11+
os.environ["GITHUB_WORKFLOW"] = "commitlint_ci"
12+
os.environ["GITHUB_ACTION"] = "action"
13+
os.environ["GITHUB_ACTOR"] = "actor"
14+
os.environ["GITHUB_REPOSITORY"] = "opensource-nepal/commitlint"
15+
os.environ["GITHUB_JOB"] = "job"
16+
os.environ["GITHUB_RUN_ATTEMPT"] = "9"
17+
os.environ["GITHUB_RUN_NUMBER"] = "8"
18+
os.environ["GITHUB_RUN_ID"] = "7"
19+
os.environ["GITHUB_EVENT_PATH"] = "/tmp/github_event.json"
20+
os.environ["GITHUB_STEP_SUMMARY"] = "/tmp/github_step_summary"
21+
os.environ["GITHUB_OUTPUT"] = "/tmp/github_output"
22+
23+
# GitHub Action input env
24+
os.environ["INPUT_TOKEN"] = "token"
25+
os.environ["INPUT_VERBOSE"] = "false"
26+
os.environ["INPUT_FAIL_ON_ERROR"] = "true"
+72
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# type: ignore
2+
# pylint: disable=all
3+
4+
import json
5+
import os
6+
from unittest.mock import mock_open, patch
7+
8+
import pytest
9+
10+
from github_actions.action.event import GitHubEvent
11+
from tests.fixtures.actions_env import set_github_env_vars
12+
13+
MOCK_PAYLOAD = {"key": "value"}
14+
15+
16+
@pytest.fixture(scope="module")
17+
def github_event():
18+
set_github_env_vars()
19+
with patch("builtins.open", mock_open(read_data=json.dumps(MOCK_PAYLOAD))):
20+
return GitHubEvent()
21+
22+
23+
def test__github_event__initialization(github_event):
24+
assert github_event.event_name == "push"
25+
assert github_event.sha == "commitlint_sha"
26+
assert github_event.ref == "refs/heads/main"
27+
assert github_event.workflow == "commitlint_ci"
28+
assert github_event.action == "action"
29+
assert github_event.actor == "actor"
30+
assert github_event.repository == "opensource-nepal/commitlint"
31+
assert github_event.job == "job"
32+
assert github_event.run_attempt == "9"
33+
assert github_event.run_number == "8"
34+
assert github_event.run_id == "7"
35+
assert github_event.event_path == "/tmp/github_event.json"
36+
assert github_event.payload == MOCK_PAYLOAD
37+
38+
39+
def test__github_event__to_dict(github_event):
40+
event_dict = github_event.to_dict()
41+
assert event_dict["event_name"] == "push"
42+
assert event_dict["sha"] == "commitlint_sha"
43+
assert event_dict["ref"] == "refs/heads/main"
44+
assert event_dict["workflow"] == "commitlint_ci"
45+
assert event_dict["action"] == "action"
46+
assert event_dict["actor"] == "actor"
47+
assert event_dict["repository"] == "opensource-nepal/commitlint"
48+
assert event_dict["job"] == "job"
49+
assert event_dict["run_attempt"] == "9"
50+
assert event_dict["run_number"] == "8"
51+
assert event_dict["run_id"] == "7"
52+
assert event_dict["event_path"] == "/tmp/github_event.json"
53+
assert event_dict["payload"] == MOCK_PAYLOAD
54+
55+
56+
def test__github_event__str(github_event):
57+
event_str = str(github_event)
58+
assert "push" in event_str
59+
assert "commitlint_sha" in event_str
60+
assert "refs/heads/main" in event_str
61+
assert "commitlint_ci" in event_str
62+
assert "action" in event_str
63+
assert "actor" in event_str
64+
assert "opensource-nepal/commitlint" in event_str
65+
assert "job" in event_str
66+
assert "9" in event_str
67+
68+
69+
def test__github_event__env_error():
70+
os.environ.pop("GITHUB_EVENT_NAME")
71+
with pytest.raises(EnvironmentError):
72+
GitHubEvent()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
# type: ignore
2+
# pylint: disable=all
3+
import os
4+
from unittest.mock import patch
5+
6+
import pytest
7+
8+
from github_actions.action.run import check_commit_messages
9+
from tests.fixtures.actions_env import set_github_env_vars
10+
11+
# Constants
12+
STATUS_SUCCESS = "success"
13+
STATUS_FAILURE = "failure"
14+
INPUT_FAIL_ON_ERROR = "fail_on_error"
15+
16+
17+
@pytest.fixture(scope="module", autouse=True)
18+
def setup_env():
19+
set_github_env_vars()
20+
21+
22+
@patch("github_actions.action.run.run_commitlint")
23+
@patch("github_actions.action.run.write_line_to_file")
24+
@patch("github_actions.action.run.write_output")
25+
@patch.dict(os.environ, {**os.environ, "GITHUB_STEP_SUMMARY": "summary_path"})
26+
def test__check_commit_messages__all_valid_messages(
27+
mock_write_output,
28+
mock_write_line_to_file,
29+
mock_run_commitlint,
30+
):
31+
commit_messages = ["feat: valid commit 1", "fix: valid commit 2"]
32+
mock_run_commitlint.return_value = (True, None)
33+
34+
check_commit_messages(commit_messages)
35+
36+
mock_run_commitlint.assert_any_call("feat: valid commit 1")
37+
mock_run_commitlint.assert_any_call("fix: valid commit 2")
38+
mock_write_line_to_file.assert_called_once_with(
39+
"summary_path", "commitlint: All commits passed!"
40+
)
41+
mock_write_output.assert_any_call("status", STATUS_SUCCESS)
42+
mock_write_output.assert_any_call("exit_code", 0)
43+
44+
45+
@patch("github_actions.action.run.run_commitlint")
46+
@patch("github_actions.action.run.write_line_to_file")
47+
@patch("github_actions.action.run.write_output")
48+
@patch.dict(os.environ, {**os.environ, "GITHUB_STEP_SUMMARY": "summary_path"})
49+
def test__check_commit_messages__partial_invalid_messages(
50+
mock_write_output,
51+
mock_write_line_to_file,
52+
mock_run_commitlint,
53+
):
54+
commit_messages = ["feat: valid commit", "invalid commit message"]
55+
mock_run_commitlint.side_effect = [
56+
(True, None),
57+
(False, "Error: invalid commit format"),
58+
]
59+
60+
with pytest.raises(SystemExit):
61+
check_commit_messages(commit_messages)
62+
63+
mock_run_commitlint.assert_any_call("feat: valid commit")
64+
mock_run_commitlint.assert_any_call("invalid commit message")
65+
mock_write_line_to_file.assert_called_once_with(
66+
"summary_path", "commitlint: 1 commit(s) failed!"
67+
)
68+
mock_write_output.assert_any_call("status", STATUS_FAILURE)
69+
mock_write_output.assert_any_call("exit_code", 1)
70+
71+
72+
@patch("github_actions.action.run.run_commitlint")
73+
@patch("github_actions.action.run.write_line_to_file")
74+
@patch("github_actions.action.run.write_output")
75+
@patch.dict(
76+
os.environ,
77+
{
78+
**os.environ,
79+
"GITHUB_STEP_SUMMARY": "summary_path",
80+
"INPUT_FAIL_ON_ERROR": "False",
81+
},
82+
)
83+
def test__check_commit_messages__fail_on_error_false(
84+
mock_write_output,
85+
mock_write_line_to_file,
86+
mock_run_commitlint,
87+
):
88+
commit_messages = ["invalid commit message"]
89+
mock_run_commitlint.return_value = (False, "Invalid commit format")
90+
91+
check_commit_messages(commit_messages)
92+
93+
mock_run_commitlint.assert_called_once_with("invalid commit message")
94+
mock_write_line_to_file.assert_called_once_with(
95+
"summary_path", "commitlint: 1 commit(s) failed!"
96+
)
97+
mock_write_output.assert_any_call("status", STATUS_FAILURE)
98+
mock_write_output.assert_any_call("exit_code", 1)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
# type: ignore
2+
# pylint: disable=all
3+
import json
4+
import os
5+
from unittest.mock import mock_open, patch
6+
7+
import pytest
8+
9+
from github_actions.action.event import GitHubEvent
10+
from github_actions.action.run import (
11+
MAX_PR_COMMITS,
12+
PER_PAGE_COMMITS,
13+
get_pr_commit_messages,
14+
)
15+
from tests.fixtures.actions_env import set_github_env_vars
16+
17+
18+
@pytest.fixture(scope="module", autouse=True)
19+
def setup_env():
20+
set_github_env_vars()
21+
22+
23+
@patch("github_actions.action.run.request_github_api")
24+
@patch.dict(os.environ, {**os.environ, "GITHUB_EVENT_NAME": "pull_request"})
25+
def test__get_pr_commit_messages__single_page(
26+
mock_request_github_api,
27+
):
28+
# mock github api request
29+
mock_request_github_api.return_value = (
30+
200,
31+
[{"commit": {"message": "feat: commit message"}}],
32+
)
33+
34+
payload = {"number": 10, "pull_request": {"commits": 2}}
35+
with patch("builtins.open", mock_open(read_data=json.dumps(payload))):
36+
event = GitHubEvent()
37+
result = get_pr_commit_messages(event)
38+
assert result == ["feat: commit message"]
39+
40+
mock_request_github_api.assert_called_once_with(
41+
method="GET",
42+
url="/repos/opensource-nepal/commitlint/pulls/10/commits",
43+
token="token",
44+
params={"per_page": PER_PAGE_COMMITS, "page": 1},
45+
)
46+
47+
48+
@patch("github_actions.action.run.request_github_api")
49+
@patch.dict(os.environ, {**os.environ, "GITHUB_EVENT_NAME": "pull_request"})
50+
def test__get_pr_commit_messages__multiple_page(
51+
mock_request_github_api,
52+
):
53+
# mock github api request
54+
mock_request_github_api.side_effect = [
55+
(
56+
200,
57+
[{"commit": {"message": "feat: commit message1"}}],
58+
),
59+
(
60+
200,
61+
[{"commit": {"message": "feat: commit message2"}}],
62+
),
63+
]
64+
65+
payload = {"number": 10, "pull_request": {"commits": 60}}
66+
with patch("builtins.open", mock_open(read_data=json.dumps(payload))):
67+
event = GitHubEvent()
68+
result = get_pr_commit_messages(event)
69+
assert result == ["feat: commit message1", "feat: commit message2"]
70+
71+
assert mock_request_github_api.call_count == 2
72+
mock_request_github_api.assert_any_call(
73+
method="GET",
74+
url="/repos/opensource-nepal/commitlint/pulls/10/commits",
75+
token="token",
76+
params={"per_page": PER_PAGE_COMMITS, "page": 1},
77+
)
78+
79+
mock_request_github_api.assert_any_call(
80+
method="GET",
81+
url="/repos/opensource-nepal/commitlint/pulls/10/commits",
82+
token="token",
83+
params={"per_page": PER_PAGE_COMMITS, "page": 2},
84+
)
85+
86+
87+
@patch("github_actions.action.run.request_github_api")
88+
@patch.dict(os.environ, {**os.environ, "GITHUB_EVENT_NAME": "pull_request"})
89+
def test__get_pr_commit_messages__api_failure(
90+
mock_request_github_api,
91+
):
92+
mock_request_github_api.return_value = (500, None)
93+
payload = {"number": 10, "pull_request": {"commits": 60}}
94+
with patch("builtins.open", mock_open(read_data=json.dumps(payload))):
95+
with pytest.raises(SystemExit):
96+
event = GitHubEvent()
97+
get_pr_commit_messages(event)
98+
99+
100+
@patch.dict(os.environ, {**os.environ, "GITHUB_EVENT_NAME": "pull_request"})
101+
def test__get_pr_commit_messages__exceed_max_commits():
102+
payload = {"number": 10, "pull_request": {"commits": MAX_PR_COMMITS + 1}}
103+
with patch("builtins.open", mock_open(read_data=json.dumps(payload))):
104+
with pytest.raises(SystemExit):
105+
event = GitHubEvent()
106+
get_pr_commit_messages(event)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# type: ignore
2+
# pylint: disable=all
3+
4+
import json
5+
from unittest.mock import mock_open, patch
6+
7+
import pytest
8+
9+
from github_actions.action.event import GitHubEvent
10+
from github_actions.action.run import get_push_commit_messages
11+
from tests.fixtures.actions_env import set_github_env_vars
12+
13+
14+
@pytest.fixture(scope="module", autouse=True)
15+
def setup_env():
16+
set_github_env_vars()
17+
18+
19+
def test__get_push_commit_messages__returns_push_commits():
20+
payload = {
21+
"commits": [
22+
{"message": "feat: valid message"},
23+
{"message": "fix(login): fix login message"},
24+
]
25+
}
26+
with patch("builtins.open", mock_open(read_data=json.dumps(payload))):
27+
commits = get_push_commit_messages(GitHubEvent())
28+
assert list(commits) == ["feat: valid message", "fix(login): fix login message"]

0 commit comments

Comments
 (0)