This commit is contained in:
1 parent
cd7ddd3cf6
commit
89621bc7c3
15 files changed
+289
-74
No files matched your search
+65
-2
@@ -5,10 +5,12 @@ from pathlib import Path
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
import pyarrow.parquet as pq
|
||||
|
||||
from solar_relay.cli import next_poll_at
|
||||
from solar_relay.config import retention_cutoff
|
||||
from solar_relay.storage import Store, StoreError
|
||||
from tests.test_status import observed
|
||||
|
||||
@@ -34,8 +36,8 @@ class ParquetTests(unittest.TestCase):
|
||||
self.assertEqual(json.loads(row["payload"]), value)
|
||||
self.assertEqual(self.store.status()["format"], "parquet")
|
||||
|
||||
def test_retention_uses_ack_time_and_never_deletes_pending(self):
|
||||
items = [observed(self.now - timedelta(days=90, minutes=index)) for index in range(4)]
|
||||
def test_retention_uses_ack_calendar_days_and_never_deletes_pending(self):
|
||||
items = [observed(self.now - timedelta(days=90 + index)) for index in range(4)]
|
||||
for item in items:
|
||||
self.store.add(item)
|
||||
self.store.acknowledge([items[0]["id"]], now=self.now - timedelta(days=31))
|
||||
@@ -47,6 +49,67 @@ class ParquetTests(unittest.TestCase):
|
||||
self.assertEqual(self.store.prune(now=self.now + timedelta(days=31)), 2)
|
||||
self.assertEqual(list(self.store.records()), [items[3]])
|
||||
|
||||
def test_retention_boundary_is_local_midnight_regardless_of_execution_time(self):
|
||||
zone = ZoneInfo("Europe/Zurich")
|
||||
for hour in (0, 12, 23):
|
||||
now = datetime(2026, 11, 2, hour, 30, tzinfo=zone)
|
||||
self.assertEqual(retention_cutoff(now, 30, zone.key), datetime(2026, 10, 2, 22, tzinfo=timezone.utc))
|
||||
with self.assertRaisesRegex(ValueError, "timezone"):
|
||||
retention_cutoff(datetime(2026, 11, 2), 30, zone.key)
|
||||
|
||||
def test_prune_uses_the_configured_timezone_at_the_date_boundary(self):
|
||||
store = Store(self.path, timezone_name="America/New_York")
|
||||
values = [observed(self.now - timedelta(days=60 + offset)) for offset in range(2)]
|
||||
for value in values:
|
||||
store.add(value)
|
||||
store.acknowledge([values[0]["id"]], now=datetime(2026, 10, 2, 3, 59, tzinfo=timezone.utc))
|
||||
store.acknowledge([values[1]["id"]], now=datetime(2026, 10, 2, 4, tzinfo=timezone.utc))
|
||||
self.assertEqual(store.prune(now=datetime(2026, 11, 2, 1, 30, tzinfo=timezone.utc)), 1)
|
||||
self.assertEqual(list(store.records()), [values[1]])
|
||||
|
||||
def test_prune_keeps_the_entire_cutoff_acknowledgement_day(self):
|
||||
zone = ZoneInfo("Europe/Zurich")
|
||||
start = datetime(2026, 10, 3, tzinfo=zone)
|
||||
values = [observed(start - timedelta(days=10) + timedelta(minutes=offset)) for offset in (0, 5)]
|
||||
for value, ack in zip(values, (start, start + timedelta(hours=23, minutes=55))):
|
||||
self.store.add(value)
|
||||
self.store.acknowledge([value["id"]], now=ack)
|
||||
self.assertEqual(self.store.prune(now=datetime(2026, 11, 2, 23, 59, tzinfo=zone)), 0)
|
||||
self.assertEqual(self.store.prune(now=datetime(2026, 11, 3, tzinfo=zone)), 2)
|
||||
|
||||
def test_prune_removes_whole_local_days_across_utc_partitions(self):
|
||||
zone = ZoneInfo("Europe/Zurich")
|
||||
first = datetime(2026, 10, 3, tzinfo=zone)
|
||||
expired = [observed(first - timedelta(hours=23, minutes=55)), observed(first - timedelta(minutes=5))]
|
||||
retained = [observed(first), observed(first + timedelta(hours=23, minutes=55))]
|
||||
for value in expired + retained:
|
||||
self.store.add(value)
|
||||
self.store.acknowledge([value["id"] for value in expired], now=first - timedelta(minutes=1))
|
||||
self.store.acknowledge([value["id"] for value in retained], now=first + timedelta(days=1))
|
||||
self.assertEqual(self.store.prune(now=datetime(2026, 11, 2, 22, tzinfo=zone)), 2)
|
||||
self.assertEqual(list(self.store.records()), retained)
|
||||
|
||||
def test_one_pending_record_protects_its_entire_local_day_across_partitions(self):
|
||||
zone = ZoneInfo("Europe/Zurich")
|
||||
day = datetime(2026, 10, 2, tzinfo=zone)
|
||||
values = [observed(day + timedelta(minutes=5)), observed(day + timedelta(hours=23, minutes=55))]
|
||||
for value in values:
|
||||
self.store.add(value)
|
||||
self.store.acknowledge([values[0]["id"]], now=day + timedelta(hours=23, minutes=59))
|
||||
self.assertEqual(self.store.prune(now=datetime(2026, 11, 2, 22, tzinfo=zone)), 0)
|
||||
self.assertEqual(list(self.store.records()), values)
|
||||
|
||||
def test_daylight_saving_retention_boundaries_preserve_complete_local_days(self):
|
||||
zone = ZoneInfo("Europe/Zurich")
|
||||
for day, expected_hours in [(datetime(2026, 3, 29, tzinfo=zone), 23),
|
||||
(datetime(2026, 10, 25, tzinfo=zone), 25)]:
|
||||
with self.subTest(day=day):
|
||||
now = day + timedelta(days=30, hours=12)
|
||||
cutoff = retention_cutoff(now, 30, zone.key)
|
||||
self.assertEqual(cutoff, day.astimezone(timezone.utc))
|
||||
next_day = (day + timedelta(days=1)).astimezone(timezone.utc)
|
||||
self.assertEqual((next_day - cutoff).total_seconds(), expected_hours * 3600)
|
||||
|
||||
def test_replay_cancels_expiry_until_the_next_acknowledgement(self):
|
||||
value = observed(self.now)
|
||||
self.store.add(value)
|
||||
|
||||
Reference in new issue
Block a user