Skip to content

Commit 750533a

Browse files
committed
feat: add handling of Y2K27 rollover
Fixes #11
1 parent 799d61a commit 750533a

File tree

2 files changed

+63
-6
lines changed

2 files changed

+63
-6
lines changed

frugy/areas.py

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from frugy.fru_registry import FruRecordType, rec_register
1515
from datetime import datetime, timedelta
1616
import logging
17-
17+
import frugy
1818

1919
def ipmi_area(cls):
2020
rec_register(cls, FruRecordType.ipmi_area)
@@ -77,23 +77,61 @@ class BoardInfo(FruAreaSized):
7777
('custom_info_fields', CustomStringArray),
7878
]
7979

80+
# With a Y2K27 rollover baked into the specification, we have to deal with
81+
# - 24bit overflow when converting yaml to bin
82+
# - uncertainty of original 25th bit when converting bin to yaml
83+
# see also: https://github.com/MicroTCA-Tech-Lab/frugy/issues/11
8084
_time_ref = datetime(1996, 1, 1)
85+
_time_rollover_limit = 2**24
86+
87+
def _timestamp_to_minutes(self, timestamp):
88+
td = timestamp - self._time_ref
89+
return td.seconds // 60 + td.days * (60*24)
90+
91+
def _timestamp_from_minutes(self, minutes):
92+
return BoardInfo._time_ref + timedelta(minutes=minutes)
93+
94+
def _handle_y2k27_rollover_yaml2bin(self, minutes):
95+
# Truncate the timestamp to 24 bits
96+
# If the remainder is nonzero, log a warning.
97+
minutes_truncated = minutes % BoardInfo._time_rollover_limit
98+
if minutes != minutes_truncated:
99+
min_str = self._timestamp_from_minutes(minutes).isoformat()
100+
min_trunc_str = self._timestamp_from_minutes(minutes_truncated).isoformat()
101+
logging.warning(f'Timestamp {min_str} truncated; may be interpreted by FRU parsers as {min_trunc_str}')
102+
return minutes_truncated
103+
104+
def _handle_y2k27_rollover_bin2yaml(self, minutes, now):
105+
# Apply heuristic to possibly truncated timestamp:
106+
# If it is "too old" relative to the system time, add rollover timedelta and log a warning.
107+
# We draw the line at 31 years into the past (not the full 2**24 minutes).
108+
# This will allow a margin of a couple months in case the system time is incorrect
109+
# or the board is dated a bit into the future.
110+
if now - self._timestamp_from_minutes(minutes) > timedelta(days=31*365):
111+
minutes_ext = minutes + self._time_rollover_limit
112+
min_str = self._timestamp_from_minutes(minutes).isoformat()
113+
min_ext_str = self._timestamp_from_minutes(minutes_ext).isoformat()
114+
warn_str = f'BoardInfo.mfg_date_time: possible Y2K27 rollover detected; timestamp bumped to {min_ext_str} - original timestamp was {min_str}'
115+
logging.warning(warn_str)
116+
frugy.fru.import_log(warn_str)
117+
return minutes_ext
118+
return minutes
81119

82120
def _set_mfg_date_time(self, timestamp):
83121
if timestamp is not None:
84122
if type(timestamp) == str:
85123
timestamp = datetime.fromisoformat(timestamp)
86-
td = timestamp - BoardInfo._time_ref
87-
minutes = td.seconds // 60 + td.days * (60*24)
124+
minutes = self._timestamp_to_minutes(timestamp)
125+
minutes = self._handle_y2k27_rollover_yaml2bin(minutes)
88126
self._set('mfg_date_time', minutes)
89127
else:
90128
self._set('mfg_date_time', 0)
91129

92130
def _get_mfg_date_time(self):
93131
minutes = self._get('mfg_date_time')
94132
if minutes != 0:
95-
timestamp = BoardInfo._time_ref + timedelta(minutes=minutes)
96-
return timestamp
133+
minutes = self._handle_y2k27_rollover_bin2yaml(minutes, datetime.now())
134+
return self._timestamp_from_minutes(minutes)
97135
else:
98136
return None
99137

tests/test_areas.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"""
66

77
import unittest
8-
from datetime import datetime
8+
from datetime import date, datetime
99
from frugy.areas import CommonHeader, ChassisInfo, BoardInfo, ProductInfo
1010
from frugy.types import FruAreaBase, FixedField
1111

@@ -90,5 +90,24 @@ def test_bitfield(self):
9090
self.assertEqual(v, b'remainder')
9191
self.assertEqual(e.to_dict(), {'first2': 2, 'second2': 1, 'then4': 5, 'lastone': 7})
9292

93+
94+
def test_y2k_rollover(self):
95+
bi = BoardInfo()
96+
dt_2000 = bi._timestamp_to_minutes(datetime(2000, 2, 3))
97+
dt_2031 = bi._timestamp_to_minutes(datetime(2031, 12, 27, 20, 16, 0))
98+
99+
# Test truncation (yaml 2 bin)
100+
self.assertEqual(bi._handle_y2k27_rollover_yaml2bin(dt_2000), dt_2000)
101+
self.assertEqual(bi._handle_y2k27_rollover_yaml2bin(dt_2031), dt_2000)
102+
103+
# Test extension (bin 2 yaml)
104+
now_2025 = datetime(2025, 1, 1)
105+
now_2031 = datetime(2031, 5, 1)
106+
now_2032 = datetime(2032, 1, 1)
107+
108+
self.assertEqual(bi._handle_y2k27_rollover_bin2yaml(dt_2000, now_2025), dt_2000)
109+
self.assertEqual(bi._handle_y2k27_rollover_bin2yaml(dt_2000, now_2031), dt_2031)
110+
self.assertEqual(bi._handle_y2k27_rollover_bin2yaml(dt_2000, now_2032), dt_2031)
111+
93112
if __name__ == '__main__':
94113
unittest.main()

0 commit comments

Comments
 (0)