Skip to content

Commit 3752ae2

Browse files
Copilotvchrombie
authored andcommitted
test: cover remaining contributor paths
Signed-off-by: Venu Vardhan Reddy Tekula <venuvrtekula@gmail.com>
1 parent d7977d1 commit 3752ae2

5 files changed

Lines changed: 214 additions & 17 deletions

File tree

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22

33
[![Python package](https://github.com/github/contributors/actions/workflows/python-ci.yml/badge.svg)](https://github.com/github/contributors/actions/workflows/python-ci.yml)
44
[![Docker Image CI](https://github.com/github/contributors/actions/workflows/docker-ci.yml/badge.svg)](https://github.com/github/contributors/actions/workflows/docker-ci.yml)
5-
[![CodeQL](https://github.com/github/contributors/actions/workflows/github-code-scanning/codeql/badge.svg)](https://github.com/github/contributors/actions/workflows/github-code-scanning/codeql)[![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/github/contributors/badge)](https://scorecard.dev/viewer/?uri=github.com/github/contributors)
5+
[![CodeQL](https://github.com/github/contributors/actions/workflows/github-code-scanning/codeql/badge.svg)](https://github.com/github/contributors/actions/workflows/github-code-scanning/codeql)
6+
[![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/github/contributors/badge)](https://scorecard.dev/viewer/?uri=github.com/github/contributors)
67

78
This is a GitHub Action that given an organization or specified repositories, produces information about the [contributors](https://chaoss.community/kb/metric-contributors/) over the specified time period.
89

contributor_stats.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,8 @@ def __repr__(self) -> str:
5959
f"contributor_stats(username={self.username}, "
6060
f"new_contributor={self.new_contributor}, "
6161
f"avatar_url={self.avatar_url}, "
62-
f"contribution_count={self.contribution_count}, commit_url={self.commit_url})"
62+
f"contribution_count={self.contribution_count}, "
63+
f"commit_url={self.commit_url}, "
6364
f"sponsor_info={self.sponsor_info})"
6465
)
6566

test_contributor_stats.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,18 @@ def test_init(self):
4545
"commit_url5",
4646
)
4747

48+
def test_repr(self):
49+
"""Test the __repr__ method includes key fields."""
50+
expected = (
51+
"contributor_stats(username=zkoppert, "
52+
"new_contributor=False, "
53+
"avatar_url=https://avatars.githubusercontent.com/u/29484535?v=4, "
54+
"contribution_count=1261, "
55+
"commit_url=commit_url5, "
56+
"sponsor_info=)"
57+
)
58+
self.assertEqual(repr(self.contributor), expected)
59+
4860
def test_merge_contributors(self):
4961
"""
5062
Test the merge_contributors function.
@@ -214,6 +226,28 @@ def test_fetch_sponsor_info(self, mock_post):
214226
timeout=60,
215227
)
216228

229+
@patch("requests.post")
230+
def test_fetch_sponsor_info_raises_on_error(self, mock_post):
231+
"""Test get_sponsor_information raises when the API response is invalid."""
232+
mock_response = MagicMock()
233+
mock_response.status_code = 500
234+
mock_response.json.return_value = {"errors": [{"message": "fail"}]}
235+
mock_post.return_value = mock_response
236+
237+
contributors = [
238+
ContributorStats(
239+
username="user1",
240+
new_contributor=False,
241+
avatar_url="https://avatars.githubusercontent.com/u/",
242+
contribution_count="100",
243+
commit_url="url1",
244+
sponsor_info="",
245+
),
246+
]
247+
248+
with self.assertRaises(ValueError):
249+
get_sponsor_information(contributors, token="token", ghe="")
250+
217251

218252
if __name__ == "__main__":
219253
unittest.main()

test_contributors.py

Lines changed: 161 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
"""This module contains the tests for the contributors.py module"""
22

3+
import runpy
34
import unittest
4-
from unittest.mock import MagicMock, patch
5+
from unittest.mock import MagicMock, call, patch
56

7+
import contributors as contributors_module
68
from contributor_stats import ContributorStats
7-
from contributors import get_all_contributors, get_contributors
89

910

1011
class TestContributors(unittest.TestCase):
@@ -25,7 +26,7 @@ def test_get_contributors(self, mock_contributor_stats):
2526
mock_repo.contributors.return_value = [mock_user]
2627
mock_repo.full_name = "owner/repo"
2728

28-
get_contributors(mock_repo, "2022-01-01", "2022-12-31", "")
29+
contributors_module.get_contributors(mock_repo, "2022-01-01", "2022-12-31", "")
2930

3031
mock_contributor_stats.assert_called_once_with(
3132
"user",
@@ -58,8 +59,8 @@ def test_get_all_contributors_with_organization(self, mock_get_contributors):
5859
]
5960
ghe = ""
6061

61-
result = get_all_contributors(
62-
"org", "", "2022-01-01", "2022-12-31", mock_github_connection, ghe
62+
result = contributors_module.get_all_contributors(
63+
"org", [], "2022-01-01", "2022-12-31", mock_github_connection, ghe
6364
)
6465

6566
self.assertEqual(
@@ -97,7 +98,7 @@ def test_get_all_contributors_with_repository(self, mock_get_contributors):
9798
]
9899
ghe = ""
99100

100-
result = get_all_contributors(
101+
result = contributors_module.get_all_contributors(
101102
"", ["owner/repo"], "2022-01-01", "2022-12-31", mock_github_connection, ghe
102103
)
103104

@@ -133,14 +134,22 @@ def test_get_contributors_skip_users_with_no_commits(self, mock_contributor_stat
133134
mock_user2.avatar_url = "https://avatars.githubusercontent.com/u/12345679?v=4"
134135
mock_user2.contributions_count = 102
135136

136-
mock_repo.contributors.return_value = [mock_user]
137+
mock_repo.contributors.return_value = [mock_user, mock_user2]
137138
mock_repo.full_name = "owner/repo"
138-
mock_repo.get_commits.side_effect = StopIteration
139+
mock_repo.commits.side_effect = [
140+
iter([object()]), # user has commits in range
141+
iter([]), # user2 has no commits in range and should be skipped
142+
]
139143
ghe = ""
140144

141-
get_contributors(mock_repo, "2022-01-01", "2022-12-31", ghe)
145+
contributors_module.get_contributors(mock_repo, "2022-01-01", "2022-12-31", ghe)
142146

143-
# Note that only user is returned and user2 is not returned here because there were no commits in the date range
147+
mock_repo.commits.assert_has_calls(
148+
[
149+
call(author="user", since="2022-01-01", until="2022-12-31"),
150+
call(author="user2", since="2022-01-01", until="2022-12-31"),
151+
]
152+
)
144153
mock_contributor_stats.assert_called_once_with(
145154
"user",
146155
False,
@@ -163,13 +172,13 @@ def test_get_contributors_skip_bot(self, mock_contributor_stats):
163172

164173
mock_repo.contributors.return_value = [mock_user]
165174
mock_repo.full_name = "owner/repo"
166-
mock_repo.get_commits.side_effect = StopIteration
167175
ghe = ""
168176

169-
get_contributors(mock_repo, "2022-01-01", "2022-12-31", ghe)
177+
contributors_module.get_contributors(mock_repo, "2022-01-01", "2022-12-31", ghe)
170178

171-
# Note that only user is returned and user2 is not returned here because there were no commits in the date range
172-
mock_contributor_stats.isEmpty()
179+
# Ensure that the bot user is skipped and ContributorStats is never instantiated
180+
mock_repo.commits.assert_not_called()
181+
mock_contributor_stats.assert_not_called()
173182

174183
@patch("contributors.contributor_stats.ContributorStats")
175184
def test_get_contributors_no_commit_end_date(self, mock_contributor_stats):
@@ -187,7 +196,7 @@ def test_get_contributors_no_commit_end_date(self, mock_contributor_stats):
187196
mock_repo.get_commits.side_effect = StopIteration
188197
ghe = ""
189198

190-
get_contributors(mock_repo, "2022-01-01", "", ghe)
199+
contributors_module.get_contributors(mock_repo, "2022-01-01", "", ghe)
191200

192201
# Note that only user is returned and user2 is not returned here because there were no commits in the date range
193202
mock_contributor_stats.assert_called_once_with(
@@ -199,6 +208,143 @@ def test_get_contributors_no_commit_end_date(self, mock_contributor_stats):
199208
"",
200209
)
201210

211+
def test_get_contributors_skips_when_no_commits_in_range(self):
212+
"""Test get_contributors skips users with no commits in the date range."""
213+
mock_repo = MagicMock()
214+
mock_user = MagicMock()
215+
mock_user.login = "user"
216+
mock_user.avatar_url = "https://avatars.githubusercontent.com/u/12345678?v=4"
217+
mock_user.contributions_count = 100
218+
mock_repo.contributors.return_value = [mock_user]
219+
mock_repo.full_name = "owner/repo"
220+
mock_repo.commits.return_value = iter([])
221+
222+
result = contributors_module.get_contributors(
223+
mock_repo, "2022-01-01", "2022-12-31", ""
224+
)
225+
226+
self.assertEqual(result, [])
227+
228+
def test_get_contributors_handles_exception(self):
229+
"""Test get_contributors returns None when an exception is raised."""
230+
231+
class BoomIterable: # pylint: disable=too-few-public-methods
232+
"""Iterable that raises an exception when iterated over."""
233+
234+
def __iter__(self):
235+
raise RuntimeError("boom")
236+
237+
mock_repo = MagicMock()
238+
mock_repo.full_name = "owner/repo"
239+
mock_repo.contributors.return_value = BoomIterable()
240+
241+
with patch("builtins.print") as mock_print:
242+
result = contributors_module.get_contributors(
243+
mock_repo, "2022-01-01", "2022-12-31", ""
244+
)
245+
246+
self.assertIsNone(result)
247+
mock_print.assert_any_call(
248+
"Error getting contributors for repository: owner/repo"
249+
)
250+
251+
def test_main_runs_under_main_guard(self):
252+
"""Test running contributors as a script executes main."""
253+
mock_env = MagicMock()
254+
mock_env.get_env_vars.return_value = (
255+
"org",
256+
[],
257+
123,
258+
456,
259+
b"key",
260+
False,
261+
"",
262+
"",
263+
"2022-01-01",
264+
"2022-12-31",
265+
"true",
266+
False,
267+
)
268+
269+
mock_auth = MagicMock()
270+
mock_github = MagicMock()
271+
mock_org = MagicMock()
272+
mock_org.repositories.return_value = []
273+
mock_github.organization.return_value = mock_org
274+
mock_auth.auth_to_github.return_value = mock_github
275+
mock_auth.get_github_app_installation_token.return_value = "token"
276+
277+
mock_markdown = MagicMock()
278+
mock_json_writer = MagicMock()
279+
280+
with patch.dict(
281+
"sys.modules",
282+
{
283+
"env": mock_env,
284+
"auth": mock_auth,
285+
"markdown": mock_markdown,
286+
"json_writer": mock_json_writer,
287+
},
288+
clear=False,
289+
):
290+
runpy.run_module("contributors", run_name="__main__")
291+
292+
mock_env.get_env_vars.assert_called_once()
293+
mock_auth.auth_to_github.assert_called_once()
294+
mock_auth.get_github_app_installation_token.assert_called_once_with(
295+
"", 123, b"key", 456
296+
)
297+
mock_markdown.write_to_markdown.assert_called_once()
298+
mock_json_writer.write_to_json.assert_called_once()
299+
300+
def test_main_sets_new_contributor_flag(self):
301+
"""Test main sets new_contributor when start/end dates are provided."""
302+
contributor = ContributorStats(
303+
"user1",
304+
False,
305+
"https://avatars.githubusercontent.com/u/1",
306+
10,
307+
"commit_url",
308+
"",
309+
)
310+
311+
with patch.object(
312+
contributors_module.env, "get_env_vars"
313+
) as mock_get_env_vars, patch.object(
314+
contributors_module.auth, "auth_to_github"
315+
) as mock_auth_to_github, patch.object(
316+
contributors_module, "get_all_contributors"
317+
) as mock_get_all_contributors, patch.object(
318+
contributors_module.contributor_stats,
319+
"is_new_contributor",
320+
return_value=True,
321+
) as mock_is_new, patch.object(
322+
contributors_module.markdown, "write_to_markdown"
323+
), patch.object(
324+
contributors_module.json_writer, "write_to_json"
325+
):
326+
mock_get_env_vars.return_value = (
327+
"org",
328+
[],
329+
None,
330+
None,
331+
b"",
332+
False,
333+
"token",
334+
"",
335+
"2022-01-01",
336+
"2022-12-31",
337+
False,
338+
False,
339+
)
340+
mock_auth_to_github.return_value = MagicMock()
341+
mock_get_all_contributors.side_effect = [[contributor], []]
342+
343+
contributors_module.main()
344+
345+
mock_is_new.assert_called_once_with("user1", [])
346+
self.assertTrue(contributor.new_contributor)
347+
202348

203349
if __name__ == "__main__":
204350
unittest.main()

test_env.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,21 @@ def test_get_env_vars_valid_date_range(self):
294294
self.assertEqual(start_date, "2024-01-01")
295295
self.assertEqual(end_date, "2025-01-01")
296296

297+
@patch.dict(os.environ, {"TEST_INT": "12.34"}, clear=True)
298+
def test_get_int_env_var_returns_none_for_invalid_int(self):
299+
"""Test that invalid integer env values return None."""
300+
self.assertIsNone(env.get_int_env_var("TEST_INT"))
301+
302+
def test_validate_date_range_invalid_date_format_raises(self):
303+
"""Test that invalid date formats raise a ValueError."""
304+
with self.assertRaises(ValueError) as cm:
305+
env.validate_date_range("2024/01/01", "2024-02-01")
306+
the_exception = cm.exception
307+
self.assertEqual(
308+
str(the_exception),
309+
"start_date and end_date must be in the format YYYY-MM-DD",
310+
)
311+
297312

298313
if __name__ == "__main__":
299314
unittest.main()

0 commit comments

Comments
 (0)