Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: handle multiple repos in search query #261

Merged
merged 1 commit into from
Apr 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 36 additions & 23 deletions issue_metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
Functions:
get_env_vars() -> EnvVars: Get the environment variables for use
in the script.
search_issues(search_query: str, github_connection: github3.GitHub)
search_issues(search_query: str, github_connection: github3.GitHub, owners_and_repositories: List[dict])
-> github3.structs.SearchIterator:
Searches for issues in a GitHub repository that match the given search query.
get_per_issue_metrics(issues: Union[List[dict], List[github3.issues.Issue]],
Expand Down Expand Up @@ -43,7 +43,9 @@


def search_issues(
search_query: str, github_connection: github3.GitHub, owner: str, repository: str
search_query: str,
github_connection: github3.GitHub,
owners_and_repositories: List[dict],
) -> List[github3.search.IssueSearchResult]: # type: ignore
"""
Searches for issues/prs/discussions in a GitHub repository that match
Expand All @@ -52,8 +54,8 @@ def search_issues(
Args:
search_query (str): The search query to use for finding issues/prs/discussions.
github_connection (github3.GitHub): A connection to the GitHub API.
owner (str): The owner of the repository to search in.
repository (str): The repository to search in.
owners_and_repositories (List[dict]): A list of dictionaries containing
the owner and repository names.

Returns:
List[github3.search.IssueSearchResult]: A list of issues that match the search query.
Expand All @@ -63,18 +65,22 @@ def search_issues(

# Print the issue titles
issues = []
repos_and_owners_string = ""
for item in owners_and_repositories:
repos_and_owners_string += f"{item['owner']}/{item['repository']} "

try:
for issue in issues_iterator:
print(issue.title) # type: ignore
issues.append(issue)
except github3.exceptions.ForbiddenError:
print(
f"You do not have permission to view this repository '{repository}'; Check your API Token."
f"You do not have permission to view a repository from: '{repos_and_owners_string}'; Check your API Token."
)
sys.exit(1)
except github3.exceptions.NotFoundError:
print(
f"The repository could not be found; Check the repository owner and name: '{owner}/{repository}"
f"The repository could not be found; Check the repository owner and names: '{repos_and_owners_string}"
)
sys.exit(1)
except github3.exceptions.ConnectionError:
Expand Down Expand Up @@ -212,28 +218,35 @@ def get_per_issue_metrics(
return issues_with_metrics, num_issues_open, num_issues_closed


def get_owner_and_repository(
def get_owners_and_repositories(
search_query: str,
) -> dict:
"""Get the owner and repository from the search query.
) -> List[dict]:
"""Get the owners and repositories from the search query.

Args:
search_query (str): The search query used to search for issues.

Returns:
dict: A dictionary of owner and repository.
List[dict]: A list of dictionaries of owners and repositories.

"""
search_query_split = search_query.split(" ")
result = {}
results_list = []
for item in search_query_split:
result = {}
if "repo:" in item and "/" in item:
result["owner"] = item.split(":")[1].split("/")[0]
result["repository"] = item.split(":")[1].split("/")[1]
if "org:" in item or "owner:" in item or "user:" in item:
result["owner"] = item.split(":")[1]
if "user:" in item:
result["owner"] = item.split(":")[1]
if "owner:" in item:
result["owner"] = item.split(":")[1]
if result:
results_list.append(result)

return result
return results_list


def main():
Expand Down Expand Up @@ -279,17 +292,17 @@ def main():
max_comments_eval = int(env_vars.max_comments_eval)
heavily_involved_cutoff = int(env_vars.heavily_involved_cutoff)

# Get the owner and repository from the search query
owner_and_repository = get_owner_and_repository(search_query)
owner = owner_and_repository.get("owner")
repository = owner_and_repository.get("repository")
# Get the owners and repositories from the search query
owners_and_repositories = get_owners_and_repositories(search_query)

if owner is None:
raise ValueError(
"The search query must include a repository owner and name \
(ie. repo:owner/repo), an organization (ie. org:organization), \
a user (ie. user:login) or an owner (ie. owner:user-or-organization)"
)
# Every search query must include a repository owner for each repository, organization, or user
for item in owners_and_repositories:
if item["owner"] is None:
raise ValueError(
"The search query must include a repository owner and name \
(ie. repo:owner/repo), an organization (ie. org:organization), \
a user (ie. user:login) or an owner (ie. owner:user-or-organization)"
)

# Determine if there are label to measure
labels = env_vars.labels_to_measure
Expand All @@ -307,7 +320,7 @@ def main():
write_to_markdown(None, None, None, None, None, None, None, None)
return
else:
issues = search_issues(search_query, github_connection, owner, repository)
issues = search_issues(search_query, github_connection, owners_and_repositories)
if len(issues) <= 0:
print("No issues found")
write_to_markdown(None, None, None, None, None, None, None, None)
Expand Down
173 changes: 41 additions & 132 deletions test_issue_metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,10 @@
from datetime import datetime, timedelta
from unittest.mock import MagicMock, patch

import issue_metrics
from issue_metrics import (
IssueWithMetrics,
get_env_vars,
get_owner_and_repository,
get_owners_and_repositories,
get_per_issue_metrics,
measure_time_to_close,
measure_time_to_first_response,
Expand Down Expand Up @@ -52,43 +51,62 @@ def test_search_issues(self):
mock_connection.search_issues.return_value = mock_issues

# Call search_issues and check that it returns the correct issues
issues = search_issues(
"is:open", mock_connection, "fakeowner", "fakerepository"
)
repo_with_owner = {"owner": "owner1", "repository": "repo1"}
owners_and_repositories = [repo_with_owner]
issues = search_issues("is:open", mock_connection, owners_and_repositories)
self.assertEqual(issues, mock_issues)


class TestGetOwnerAndRepository(unittest.TestCase):
"""Unit tests for the get_owner_and_repository function.
"""Unit tests for the get_owners_and_repositories function.

This class contains unit tests for the get_owner_and_repository function in the
This class contains unit tests for the get_owners_and_repositories function in the
issue_metrics module. The tests use the unittest module and the unittest.mock
module to mock the GitHub API and test the function in isolation.

Methods:
test_get_owner_with_owner_and_repo_in_query: Test get both owner and repo.
test_get_owner_and_repository_with_repo_in_query: Test get just owner.
test_get_owner_and_repository_without_either_in_query: Test get neither.

test_get_owners_with_owner_and_repo_in_query: Test get both owner and repo.
test_get_owners_and_repositories_with_repo_in_query: Test get just owner.
test_get_owners_and_repositories_without_either_in_query: Test get neither.
test_get_owners_and_repositories_with_multiple_entries: Test get multiple entries.
"""

def test_get_owner_with_owner_and_repo_in_query(self):
def test_get_owners_with_owner_and_repo_in_query(self):
"""Test get both owner and repo."""
result = get_owner_and_repository("repo:owner1/repo1")
self.assertEqual(result.get("owner"), "owner1")
self.assertEqual(result.get("repository"), "repo1")
result = get_owners_and_repositories("repo:owner1/repo1")
self.assertEqual(result[0].get("owner"), "owner1")
self.assertEqual(result[0].get("repository"), "repo1")

def test_get_owner_and_repository_with_repo_in_query(self):
def test_get_owner_and_repositories_with_repo_in_query(self):
"""Test get just owner."""
result = get_owner_and_repository("org:owner1")
self.assertEqual(result.get("owner"), "owner1")
self.assertIsNone(result.get("repository"))
result = get_owners_and_repositories("org:owner1")
self.assertEqual(result[0].get("owner"), "owner1")
self.assertIsNone(result[0].get("repository"))

def test_get_owner_and_repository_without_either_in_query(self):
def test_get_owners_and_repositories_without_either_in_query(self):
"""Test get neither."""
result = get_owner_and_repository("is:blah")
self.assertIsNone(result.get("owner"))
self.assertIsNone(result.get("repository"))
result = get_owners_and_repositories("is:blah")
self.assertEqual(result, [])

def test_get_owners_and_repositories_with_multiple_entries(self):
"""Test get multiple entries."""
result = get_owners_and_repositories("repo:owner1/repo1 org:owner2")
self.assertEqual(result[0].get("owner"), "owner1")
self.assertEqual(result[0].get("repository"), "repo1")
self.assertEqual(result[1].get("owner"), "owner2")
self.assertIsNone(result[1].get("repository"))

def test_get_owners_and_repositories_with_org(self):
"""Test get org as owner."""
result = get_owners_and_repositories("org:owner1")
self.assertEqual(result[0].get("owner"), "owner1")
self.assertIsNone(result[0].get("repository"))

def test_get_owners_and_repositories_with_user(self):
"""Test get user as owner."""
result = get_owners_and_repositories("user:owner1")
self.assertEqual(result[0].get("owner"), "owner1")
self.assertIsNone(result[0].get("repository"))


class TestGetEnvVars(unittest.TestCase):
Expand Down Expand Up @@ -120,115 +138,6 @@ def test_get_env_vars_missing_query(self):
get_env_vars(test=True)


class TestMain(unittest.TestCase):
"""Unit tests for the main function.

This class contains unit tests for the main function in the issue_metrics
module. The tests use the unittest module and the unittest.mock module to
mock the GitHub API and test the function in isolation.

Methods:
test_main: Test that main runs without errors.
test_main_no_issues_found: Test that main handles when no issues are found

"""

@patch("issue_metrics.auth_to_github")
@patch("issue_metrics.search_issues")
@patch("issue_metrics.measure_time_to_first_response")
@patch("issue_metrics.get_stats_time_to_first_response")
@patch.dict(
os.environ,
{
"SEARCH_QUERY": "is:open repo:user/repo",
"GH_TOKEN": "test_token",
},
)
def test_main(
self,
mock_get_stats_time_to_first_response,
mock_measure_time_to_first_response,
mock_search_issues,
mock_auth_to_github,
):
"""Test that main runs without errors."""
# Set up the mock GitHub connection object
mock_connection = MagicMock()
mock_auth_to_github.return_value = mock_connection

# Set up the mock search_issues function
mock_issues = MagicMock(
items=[
MagicMock(title="Issue 1"),
MagicMock(title="Issue 2"),
]
)

mock_search_issues.return_value = mock_issues

# Set up the mock measure_time_to_first_response function
mock_issues_with_ttfr = [
(
"Issue 1",
"https://github.com/user/repo/issues/1",
"alice",
timedelta(days=1, hours=2, minutes=30),
),
(
"Issue 2",
"https://github.com/user/repo/issues/2",
"bob",
timedelta(days=3, hours=4, minutes=30),
),
]
mock_measure_time_to_first_response.return_value = mock_issues_with_ttfr

# Set up the mock get_stats_time_to_first_response function
mock_stats_time_to_first_response = 15
mock_get_stats_time_to_first_response.return_value = (
mock_stats_time_to_first_response
)

# Call main and check that it runs without errors
issue_metrics.main()

# Remove the markdown file created by main
os.remove("issue_metrics.md")

@patch("issue_metrics.auth_to_github")
@patch("issue_metrics.search_issues")
@patch("issue_metrics.write_to_markdown")
@patch.dict(
os.environ,
{
"SEARCH_QUERY": "is:open repo:org/repo",
"GH_TOKEN": "test_token",
},
)
def test_main_no_issues_found(
self,
mock_write_to_markdown,
mock_search_issues,
mock_auth_to_github,
):
"""Test that main writes 'No issues found' to the
console and calls write_to_markdown with None."""

# Set up the mock GitHub connection object
mock_connection = MagicMock()
mock_auth_to_github.return_value = mock_connection

# Set up the mock search_issues function to return an empty list of issues
mock_issues = MagicMock(items=[])
mock_search_issues.return_value = mock_issues

# Call main and check that it writes 'No issues found'
issue_metrics.main()
mock_write_to_markdown.assert_called_once_with(
None, None, None, None, None, None, None, None
)


class TestGetPerIssueMetrics(unittest.TestCase):
"""Test suite for the get_per_issue_metrics function."""

Expand Down
Loading