From 829cea1a80412b3c7f96b16ebf897601ded3ea77 Mon Sep 17 00:00:00 2001 From: Klaas van Schelven Date: Wed, 10 Sep 2025 09:28:00 +0200 Subject: [PATCH] =?UTF-8?q?detection=20of=20a=20new=20release=20through=20?= =?UTF-8?q?an=20event=20=E2=87=8F=20triggering=20of=20a=20TurningPoint?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This more exactly expresses semantics by itself, and is also in preparation of creating releases through the API (which have no triggering event) See #146 --- ingest/views.py | 2 +- issues/tests.py | 24 ++++++++++++++---------- releases/models.py | 12 +++++------- 3 files changed, 20 insertions(+), 18 deletions(-) diff --git a/ingest/views.py b/ingest/views.py index 332999c..3d554e6 100644 --- a/ingest/views.py +++ b/ingest/views.py @@ -369,7 +369,7 @@ class BaseIngestAPIView(View): # multiple events with the same event_id "don't happen" (i.e. are the result of badly misbehaving clients) raise ValidationError("Event already exists", code="event_already_exists") - release = create_release_if_needed(project, event.release, event, issue) + release = create_release_if_needed(project, event.release, event.ingested_at, issue) if issue_created: TurningPoint.objects.create( diff --git a/issues/tests.py b/issues/tests.py index 0e04e7b..52c905f 100644 --- a/issues/tests.py +++ b/issues/tests.py @@ -193,7 +193,8 @@ class RegressionIssueTestCase(DjangoTestCase): def test_issue_is_regression_no_releases(self): project = Project.objects.create() - create_release_if_needed(fresh(project), "", create_event(project)) + timestamp = datetime(2020, 1, 1, tzinfo=timezone.utc) + create_release_if_needed(fresh(project), "", timestamp) # new issue is not a regression issue = Issue.objects.create(project=project, **denormalized_issue_fields()) @@ -212,7 +213,8 @@ class RegressionIssueTestCase(DjangoTestCase): def test_issue_had_no_releases_but_now_does(self): project = Project.objects.create() - create_release_if_needed(fresh(project), "", create_event(project)) + timestamp = datetime(2020, 1, 1, tzinfo=timezone.utc) + create_release_if_needed(fresh(project), "", timestamp) # new issue is not a regression issue = Issue.objects.create(project=project, **denormalized_issue_fields()) @@ -223,15 +225,16 @@ class RegressionIssueTestCase(DjangoTestCase): issue.save() # a new release happens - create_release_if_needed(fresh(project), "1.0.0", create_event(project)) + create_release_if_needed(fresh(project), "1.0.0", timestamp) self.assertTrue(issue_is_regression(fresh(issue), "1.0.0")) def test_issue_is_regression_with_releases_resolve_by_latest(self): project = Project.objects.create() + timestamp = datetime(2020, 1, 1, tzinfo=timezone.utc) - create_release_if_needed(fresh(project), "1.0.0", create_event(project)) - create_release_if_needed(fresh(project), "2.0.0", create_event(project)) + create_release_if_needed(fresh(project), "1.0.0", timestamp) + create_release_if_needed(fresh(project), "2.0.0", timestamp) # new issue is not a regression issue = Issue.objects.create(project=project, **denormalized_issue_fields()) @@ -244,7 +247,7 @@ class RegressionIssueTestCase(DjangoTestCase): self.assertTrue(issue_is_regression(fresh(issue), "2.0.0")) # a new release happens, and the issue is seen there: also a regression - create_release_if_needed(fresh(project), "3.0.0", create_event(project)) + create_release_if_needed(fresh(project), "3.0.0", timestamp) self.assertTrue(issue_is_regression(fresh(issue), "3.0.0")) # reopen the issue (as is done when a real regression is seen; or as would be done manually); nothing is a @@ -256,9 +259,10 @@ class RegressionIssueTestCase(DjangoTestCase): def test_issue_is_regression_with_releases_resolve_by_next(self): project = Project.objects.create() + timestamp = datetime(2020, 1, 1, tzinfo=timezone.utc) - create_release_if_needed(fresh(project), "1.0.0", create_event(project)) - create_release_if_needed(fresh(project), "2.0.0", create_event(project)) + create_release_if_needed(fresh(project), "1.0.0", timestamp) + create_release_if_needed(fresh(project), "2.0.0", timestamp) # new issue is not a regression issue = Issue.objects.create(project=project, **denormalized_issue_fields()) @@ -271,11 +275,11 @@ class RegressionIssueTestCase(DjangoTestCase): self.assertFalse(issue_is_regression(fresh(issue), "2.0.0")) # a new release appears (as part of a new event); this is a regression - create_release_if_needed(fresh(project), "3.0.0", create_event(project)) + create_release_if_needed(fresh(project), "3.0.0", timestamp) self.assertTrue(issue_is_regression(fresh(issue), "3.0.0")) # first-seen at any later release: regression - create_release_if_needed(fresh(project), "4.0.0", create_event(project)) + create_release_if_needed(fresh(project), "4.0.0", timestamp) self.assertTrue(issue_is_regression(fresh(issue), "4.0.0")) diff --git a/releases/models.py b/releases/models.py index 5d57288..4510571 100644 --- a/releases/models.py +++ b/releases/models.py @@ -100,7 +100,7 @@ class Release(models.Model): return self.version[:12] -def create_release_if_needed(project, version, event, issue=None): +def create_release_if_needed(project, version, timestamp, issue=None): if version is None: # because `create_release_if_needed` is called with Issue.release (non-nullable), the below "won't happen" raise ValueError('The None-like version must be the empty string') @@ -119,16 +119,14 @@ def create_release_if_needed(project, version, event, issue=None): if release == project.get_latest_release(): resolved_by_next_qs = Issue.objects.filter(project=project, is_resolved_by_next_release=True) - # NOTE: once we introduce an explicit way of creating releases (not event-based) we can not rely on a - # triggering event anymore for our timestamp. - TurningPoint.objects.bulk_create([TurningPoint( project=project, - issue=issue, kind=TurningPointKind.NEXT_MATERIALIZED, triggering_event=event, - metadata=json.dumps({"actual_release": release.version}), timestamp=event.ingested_at) + issue=issue, kind=TurningPointKind.NEXT_MATERIALIZED, + # the detection of a new release through an event does not imply a triggering of a TurningPoint: + triggering_event=None, + metadata=json.dumps({"actual_release": release.version}), timestamp=timestamp) for issue in resolved_by_next_qs ]) - event.never_evict = True # .save() will be called by the caller of this function resolved_by_next_qs.update( fixed_at=Concat("fixed_at", Value(release.version + "\n")),