Skip to content

Commit 48cbff1

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

5 files changed

Lines changed: 224 additions & 23 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: 39 additions & 5 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.
@@ -113,15 +125,15 @@ def test_is_new_contributor_true(self):
113125
username="user1",
114126
new_contributor=False,
115127
avatar_url="https://avatars.githubusercontent.com/u/",
116-
contribution_count="100",
128+
contribution_count=100,
117129
commit_url="url1",
118130
sponsor_info="",
119131
),
120132
ContributorStats(
121133
username="user2",
122134
new_contributor=False,
123135
avatar_url="https://avatars.githubusercontent.com/u/",
124-
contribution_count="200",
136+
contribution_count=200,
125137
commit_url="url2",
126138
sponsor_info="",
127139
),
@@ -141,15 +153,15 @@ def test_is_new_contributor_false(self):
141153
username="user1",
142154
new_contributor=False,
143155
avatar_url="https://avatars.githubusercontent.com/u/",
144-
contribution_count="100",
156+
contribution_count=100,
145157
commit_url="url1",
146158
sponsor_info="",
147159
),
148160
ContributorStats(
149161
username="user2",
150162
new_contributor=False,
151163
avatar_url="https://avatars.githubusercontent.com/u/",
152-
contribution_count="200",
164+
contribution_count=200,
153165
commit_url="url2",
154166
sponsor_info="",
155167
),
@@ -179,7 +191,7 @@ def test_fetch_sponsor_info(self, mock_post):
179191
username=user,
180192
new_contributor=False,
181193
avatar_url="https://avatars.githubusercontent.com/u/",
182-
contribution_count="100",
194+
contribution_count=100,
183195
commit_url="url1",
184196
sponsor_info="",
185197
),
@@ -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: 166 additions & 16 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):
@@ -24,9 +25,13 @@ def test_get_contributors(self, mock_contributor_stats):
2425
mock_user.contributions_count = 100
2526
mock_repo.contributors.return_value = [mock_user]
2627
mock_repo.full_name = "owner/repo"
28+
mock_repo.commits.return_value = iter([object()])
2729

28-
get_contributors(mock_repo, "2022-01-01", "2022-12-31", "")
30+
contributors_module.get_contributors(mock_repo, "2022-01-01", "2022-12-31", "")
2931

32+
mock_repo.commits.assert_called_once_with(
33+
author="user", since="2022-01-01", until="2022-12-31"
34+
)
3035
mock_contributor_stats.assert_called_once_with(
3136
"user",
3237
False,
@@ -58,8 +63,8 @@ def test_get_all_contributors_with_organization(self, mock_get_contributors):
5863
]
5964
ghe = ""
6065

61-
result = get_all_contributors(
62-
"org", "", "2022-01-01", "2022-12-31", mock_github_connection, ghe
66+
result = contributors_module.get_all_contributors(
67+
"org", [], "2022-01-01", "2022-12-31", mock_github_connection, ghe
6368
)
6469

6570
self.assertEqual(
@@ -97,7 +102,7 @@ def test_get_all_contributors_with_repository(self, mock_get_contributors):
97102
]
98103
ghe = ""
99104

100-
result = get_all_contributors(
105+
result = contributors_module.get_all_contributors(
101106
"", ["owner/repo"], "2022-01-01", "2022-12-31", mock_github_connection, ghe
102107
)
103108

@@ -133,14 +138,22 @@ def test_get_contributors_skip_users_with_no_commits(self, mock_contributor_stat
133138
mock_user2.avatar_url = "https://avatars.githubusercontent.com/u/12345679?v=4"
134139
mock_user2.contributions_count = 102
135140

136-
mock_repo.contributors.return_value = [mock_user]
141+
mock_repo.contributors.return_value = [mock_user, mock_user2]
137142
mock_repo.full_name = "owner/repo"
138-
mock_repo.get_commits.side_effect = StopIteration
143+
mock_repo.commits.side_effect = [
144+
iter([object()]), # user has commits in range
145+
iter([]), # user2 has no commits in range and should be skipped
146+
]
139147
ghe = ""
140148

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

143-
# Note that only user is returned and user2 is not returned here because there were no commits in the date range
151+
mock_repo.commits.assert_has_calls(
152+
[
153+
call(author="user", since="2022-01-01", until="2022-12-31"),
154+
call(author="user2", since="2022-01-01", until="2022-12-31"),
155+
]
156+
)
144157
mock_contributor_stats.assert_called_once_with(
145158
"user",
146159
False,
@@ -163,13 +176,13 @@ def test_get_contributors_skip_bot(self, mock_contributor_stats):
163176

164177
mock_repo.contributors.return_value = [mock_user]
165178
mock_repo.full_name = "owner/repo"
166-
mock_repo.get_commits.side_effect = StopIteration
167179
ghe = ""
168180

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

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()
183+
# Ensure that the bot user is skipped and ContributorStats is never instantiated
184+
mock_repo.commits.assert_not_called()
185+
mock_contributor_stats.assert_not_called()
173186

174187
@patch("contributors.contributor_stats.ContributorStats")
175188
def test_get_contributors_no_commit_end_date(self, mock_contributor_stats):
@@ -184,12 +197,12 @@ def test_get_contributors_no_commit_end_date(self, mock_contributor_stats):
184197

185198
mock_repo.contributors.return_value = [mock_user]
186199
mock_repo.full_name = "owner/repo"
187-
mock_repo.get_commits.side_effect = StopIteration
188200
ghe = ""
189201

190-
get_contributors(mock_repo, "2022-01-01", "", ghe)
202+
contributors_module.get_contributors(mock_repo, "2022-01-01", "", ghe)
191203

192204
# Note that only user is returned and user2 is not returned here because there were no commits in the date range
205+
mock_repo.commits.assert_not_called()
193206
mock_contributor_stats.assert_called_once_with(
194207
"user",
195208
False,
@@ -199,6 +212,143 @@ def test_get_contributors_no_commit_end_date(self, mock_contributor_stats):
199212
"",
200213
)
201214

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

203353
if __name__ == "__main__":
204354
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)