diff --git a/issue_metrics.py b/issue_metrics.py index fd99f09..a7dd504 100644 --- a/issue_metrics.py +++ b/issue_metrics.py @@ -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( diff --git a/test_time_in_draft.py b/test_time_in_draft.py index 8a442cc..8a4b950 100644 --- a/test_time_in_draft.py +++ b/test_time_in_draft.py @@ -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 + 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 diff --git a/time_in_draft.py b/time_in_draft.py index 98f01e7..a663cc5 100644 --- a/time_in_draft.py +++ b/time_in_draft.py @@ -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. @@ -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