Skip to content
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
2 changes: 1 addition & 1 deletion issue_metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ def get_per_issue_metrics(
)
if env_vars.draft_pr_tracking:
issue_with_metrics.time_in_draft = measure_time_in_draft(
issue=issue
issue=issue, pull_request=pull_request
)
except TypeError as e:
print(
Expand Down
52 changes: 52 additions & 0 deletions test_time_in_draft.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,58 @@ def test_time_in_draft_without_ready_for_review_and_closed(self):
"The result should be None for a closed issue with an ongoing draft.",
)

def test_time_in_draft_initially_created_as_draft(self):
"""
Test measure_time_in_draft with a PR initially created as draft.
"""
# Set up issue created_at time
self.issue.issue.created_at = "2021-01-01T00:00:00Z"

# Mock events with only ready_for_review (no converted_to_draft)
self.issue.issue.events.return_value = [
MagicMock(
event="ready_for_review",
created_at=datetime(2021, 1, 3, tzinfo=pytz.utc),
),
]

# Mock pull request object
mock_pull_request = MagicMock()

result = measure_time_in_draft(self.issue, mock_pull_request)
expected = timedelta(days=2)
self.assertEqual(
result,
expected,
"The time in draft should be 2 days for initially draft PR.",
)

def test_time_in_draft_initially_created_as_draft_still_open(self):
"""
Test measure_time_in_draft with a PR initially created as draft and still in draft.
"""
# Set up issue created_at time
self.issue.issue.created_at = "2021-01-01T00:00:00Z"

# Mock events with no ready_for_review events (still draft)
self.issue.issue.events.return_value = []

# Mock pull request object indicating it's currently draft
mock_pull_request = MagicMock()
mock_pull_request.draft = True

with unittest.mock.patch("time_in_draft.datetime") as mock_datetime:
# Keep the real datetime class but only mock the now() method
mock_datetime.fromisoformat = datetime.fromisoformat
Comment on lines +172 to +173
Copy link

Copilot AI Sep 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This approach of selectively mocking datetime methods is fragile and could break if the implementation changes. Consider using freezegun or a similar library for more robust datetime mocking in tests.

Copilot uses AI. Check for mistakes.

mock_datetime.now.return_value = datetime(2021, 1, 4, tzinfo=pytz.utc)
result = measure_time_in_draft(self.issue, mock_pull_request)
expected = timedelta(days=3)
self.assertEqual(
result,
expected,
"The time in draft should be 3 days for initially draft PR still in draft.",
)

def test_time_in_draft_with_attribute_error_scenario(self):
"""
Test measure_time_in_draft to ensure it doesn't raise AttributeError when called
Expand Down
50 changes: 50 additions & 0 deletions time_in_draft.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,13 @@

def measure_time_in_draft(
issue: github3.issues.Issue,
pull_request: Union[github3.pulls.PullRequest, None] = None,
) -> 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.
pull_request (github3.pulls.PullRequest, optional): The pull request object.

returns:
Union[timedelta, None]: Total time the pull request has spent in draft state.
Expand All @@ -26,6 +28,54 @@ def measure_time_in_draft(
draft_start = None
total_draft_time = timedelta(0)

# Check if PR was initially created as draft
pr_created_at = None

try:
if pull_request is None:
pull_request = issue.issue.pull_request()

pr_created_at = datetime.fromisoformat(
issue.issue.created_at.replace("Z", "+00:00")
)

# Look for ready_for_review events to determine if PR was initially draft
ready_for_review_events = []
converted_to_draft_events = []
for event in events:
if event.event == "ready_for_review":
ready_for_review_events.append(event)
elif event.event == "converted_to_draft":
converted_to_draft_events.append(event)

# If there are ready_for_review events, check if PR was initially draft
if ready_for_review_events:
first_ready_event = min(ready_for_review_events, key=lambda x: x.created_at)
prior_draft_events = [
e
for e in converted_to_draft_events
if e.created_at < first_ready_event.created_at
]

if not prior_draft_events:
# PR was initially created as draft, calculate time from creation to first ready_for_review
total_draft_time += first_ready_event.created_at - pr_created_at

# If there are no ready_for_review events but the PR is currently draft, it might be initially draft and still open
elif not ready_for_review_events and not converted_to_draft_events:
# Check if PR is currently draft and open
if (
hasattr(pull_request, "draft")
and pull_request.draft
and issue.issue.state == "open"
):
# PR was initially created as draft and is still draft
draft_start = pr_created_at

except (AttributeError, ValueError, TypeError):
# If we can't get PR info, fall back to original logic
pass

for event in events:
if event.event == "converted_to_draft":
draft_start = event.created_at
Expand Down
Loading