Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
3 changes: 1 addition & 2 deletions issue_metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,8 +125,7 @@ def get_per_issue_metrics(
ready_for_review_at = get_time_to_ready_for_review(issue, pull_request)
if env_vars.draft_pr_tracking:
issue_with_metrics.time_in_draft = measure_time_in_draft(
issue=issue,
ready_for_review_at=ready_for_review_at,
issue=issue
)

if env_vars.hide_time_to_first_response is False:
Expand Down
61 changes: 51 additions & 10 deletions test_time_in_draft.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,38 +18,79 @@ def setUp(self):
Setup common test data and mocks.
"""
self.issue = MagicMock()
self.issue.issue.created_at = datetime(2021, 1, 1, tzinfo=pytz.utc)
self.issue.issue.state = "open"

def test_time_in_draft_with_ready_for_review(self):
"""
Test measure_time_in_draft when ready_for_review_at is provided.
Test measure_time_in_draft with one draft and review interval.
"""
ready_for_review_at = datetime(2021, 1, 3, tzinfo=pytz.utc)
result = measure_time_in_draft(self.issue, ready_for_review_at)
self.issue.events.return_value = [
MagicMock(event="converted_to_draft", created_at=datetime(2021, 1, 1, tzinfo=pytz.utc)),
MagicMock(event="ready_for_review", created_at=datetime(2021, 1, 3, tzinfo=pytz.utc)),
]
result = measure_time_in_draft(self.issue)
expected = timedelta(days=2)
self.assertEqual(result, expected, "The time in draft should be 2 days.")

def test_time_in_draft_without_ready_for_review(self):
"""
Test measure_time_in_draft when ready_for_review_at is not provided and issue is still open.
"""
self.issue.events.return_value = [
MagicMock(event="converted_to_draft", created_at=datetime(2021, 1, 1, tzinfo=pytz.utc)),
]
now = datetime(2021, 1, 4, tzinfo=pytz.utc)
with unittest.mock.patch("time_in_draft.datetime") as mock_datetime:
mock_datetime.now.return_value = now
result = measure_time_in_draft(self.issue, None)
result = measure_time_in_draft(self.issue)
expected = timedelta(days=3)
self.assertEqual(result, expected, "The time in draft should be 3 days.")

def test_time_in_draft_multiple_intervals(self):
"""
Test measure_time_in_draft with multiple draft intervals.
"""
self.issue.events.return_value = [
MagicMock(event="converted_to_draft", created_at=datetime(2021, 1, 1, tzinfo=pytz.utc)),
MagicMock(event="ready_for_review", created_at=datetime(2021, 1, 3, tzinfo=pytz.utc)),
MagicMock(event="converted_to_draft", created_at=datetime(2021, 1, 5, tzinfo=pytz.utc)),
MagicMock(event="ready_for_review", created_at=datetime(2021, 1, 7, tzinfo=pytz.utc)),
]
result = measure_time_in_draft(self.issue)
expected = timedelta(days=4)
self.assertEqual(result, expected, "The total time in draft should be 4 days.")

def test_time_in_draft_ongoing_draft(self):
"""
Test measure_time_in_draft with an ongoing draft interval.
"""
self.issue.events.return_value = [
MagicMock(event="converted_to_draft", created_at=datetime(2021, 1, 1, tzinfo=pytz.utc)),
]
with unittest.mock.patch("time_in_draft.datetime") as mock_datetime:
mock_datetime.now.return_value = datetime(2021, 1, 4, tzinfo=pytz.utc)
result = measure_time_in_draft(self.issue)
expected = timedelta(days=3)
self.assertEqual(result, expected, "The ongoing draft time should be 3 days.")

def test_time_in_draft_no_draft_events(self):
"""
Test measure_time_in_draft with no draft-related events.
"""
self.issue.events.return_value = []
result = measure_time_in_draft(self.issue)
self.assertIsNone(result, "The result should be None when there are no draft events.")

def test_time_in_draft_without_ready_for_review_and_closed(self):
"""
Test measure_time_in_draft when ready_for_review_at is not provided and issue is closed.
Test measure_time_in_draft for a closed issue with an ongoing draft and ready_for_review_at is not provided.
"""
self.issue.events.return_value = [
MagicMock(event="converted_to_draft", created_at=datetime(2021, 1, 1, tzinfo=pytz.utc)),
]
self.issue.issue.state = "closed"
result = measure_time_in_draft(self.issue, None)
self.assertIsNone(
result, "The result should be None when draft was never used."
)
result = measure_time_in_draft(self.issue)
self.assertIsNone(result, "The result should be None for a closed issue with an ongoing draft.")


class TestGetStatsTimeInDraft(unittest.TestCase):
Expand Down
31 changes: 20 additions & 11 deletions time_in_draft.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,23 +13,32 @@

def measure_time_in_draft(
issue: github3.issues.Issue,
ready_for_review_at: Union[datetime, None],
) -> Union[datetime, None]:
"""If a pull request has had time in the draft state, return the amount of time it was in draft.
) -> Union[timedelta, None]:
"""If a pull request has had time in the draft state, return the cumulative amount of time it was in draft.

args:
issue (github3.issues.Issue): A GitHub issue which has been pre-qualified as a pull request.
ready_for_review_at (datetime | None): The time the pull request was marked as
ready for review.

returns:
Union[datetime, None]: The time the pull request was in draft state.
Union[timedelta, None]: Total time the pull request has spent in draft state.
"""
if ready_for_review_at:
return ready_for_review_at - issue.issue.created_at
if issue.issue.state == "open":
return datetime.now(pytz.utc) - issue.issue.created_at
return None
events = issue.events()
draft_start = None
total_draft_time = timedelta(0)

for event in events:
if event.event == "converted_to_draft":
draft_start = event.created_at
elif event.event == "ready_for_review" and draft_start:
# Calculate draft time for this interval
total_draft_time += event.created_at - draft_start
draft_start = None

# If the PR is currently in draft state, calculate the time in draft up to now
if draft_start and issue.issue.state == "open":
total_draft_time += datetime.now(pytz.utc) - draft_start

return total_draft_time if total_draft_time > timedelta(0) else None


def get_stats_time_in_draft(
Expand Down