facts - fix incorrect time for some date_time_facts (#70665)
The iso8601_micro and iso8601 facts incorrectly called now.utcnow(), resulting in a new timestamp at the time it was called, not a conversion of the previously stored timestamp. Correct this by capturing the UTC timestamp once then calculating the local time using the UTC offset of the current system. * Use time.time() for getting the current time * Convert from that stored epoch timestamp to local and UTC times * Used existing timestamp for epoch time * Add unit tests that validate the formate of the return value rather than an exact value since mocking time and timezone is non-trivial
This commit is contained in:
parent
2e0097ada3
commit
c4f442ed5a
4 changed files with 114 additions and 66 deletions
2
changelogs/fragments/date-time-facts-fix-utctime.yml
Normal file
2
changelogs/fragments/date-time-facts-fix-utctime.yml
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
bugfixes:
|
||||||
|
- facts - fix incorrect UTC timestamp in ``iso8601_micro`` and ``iso8601``
|
|
@ -32,7 +32,11 @@ class DateTimeFactCollector(BaseFactCollector):
|
||||||
facts_dict = {}
|
facts_dict = {}
|
||||||
date_time_facts = {}
|
date_time_facts = {}
|
||||||
|
|
||||||
now = datetime.datetime.now()
|
# Store the timestamp once, then get local and UTC versions from that
|
||||||
|
epoch_ts = time.time()
|
||||||
|
now = datetime.datetime.fromtimestamp(epoch_ts)
|
||||||
|
utcnow = datetime.datetime.utcfromtimestamp(epoch_ts)
|
||||||
|
|
||||||
date_time_facts['year'] = now.strftime('%Y')
|
date_time_facts['year'] = now.strftime('%Y')
|
||||||
date_time_facts['month'] = now.strftime('%m')
|
date_time_facts['month'] = now.strftime('%m')
|
||||||
date_time_facts['weekday'] = now.strftime('%A')
|
date_time_facts['weekday'] = now.strftime('%A')
|
||||||
|
@ -44,12 +48,11 @@ class DateTimeFactCollector(BaseFactCollector):
|
||||||
date_time_facts['second'] = now.strftime('%S')
|
date_time_facts['second'] = now.strftime('%S')
|
||||||
date_time_facts['epoch'] = now.strftime('%s')
|
date_time_facts['epoch'] = now.strftime('%s')
|
||||||
if date_time_facts['epoch'] == '' or date_time_facts['epoch'][0] == '%':
|
if date_time_facts['epoch'] == '' or date_time_facts['epoch'][0] == '%':
|
||||||
# NOTE: in this case, the epoch wont match the rest of the date_time facts? ie, it's a few milliseconds later..? -akl
|
date_time_facts['epoch'] = str(int(epoch_ts))
|
||||||
date_time_facts['epoch'] = str(int(time.time()))
|
|
||||||
date_time_facts['date'] = now.strftime('%Y-%m-%d')
|
date_time_facts['date'] = now.strftime('%Y-%m-%d')
|
||||||
date_time_facts['time'] = now.strftime('%H:%M:%S')
|
date_time_facts['time'] = now.strftime('%H:%M:%S')
|
||||||
date_time_facts['iso8601_micro'] = now.utcnow().strftime("%Y-%m-%dT%H:%M:%S.%fZ")
|
date_time_facts['iso8601_micro'] = utcnow.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
|
||||||
date_time_facts['iso8601'] = now.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ")
|
date_time_facts['iso8601'] = utcnow.strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||||
date_time_facts['iso8601_basic'] = now.strftime("%Y%m%dT%H%M%S%f")
|
date_time_facts['iso8601_basic'] = now.strftime("%Y%m%dT%H%M%S%f")
|
||||||
date_time_facts['iso8601_basic_short'] = now.strftime("%Y%m%dT%H%M%S")
|
date_time_facts['iso8601_basic_short'] = now.strftime("%Y%m%dT%H%M%S")
|
||||||
date_time_facts['tz'] = time.strftime("%Z")
|
date_time_facts['tz'] = time.strftime("%Z")
|
||||||
|
|
104
test/units/module_utils/facts/test_date_time.py
Normal file
104
test/units/module_utils/facts/test_date_time.py
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright (c) 2020 Ansible Project
|
||||||
|
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
|
||||||
|
from __future__ import absolute_import, division, print_function
|
||||||
|
__metaclass__ = type
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import datetime
|
||||||
|
import string
|
||||||
|
import time
|
||||||
|
|
||||||
|
from ansible.module_utils.facts.system import date_time
|
||||||
|
|
||||||
|
EPOCH_TS = 1594449296.123456
|
||||||
|
DT = datetime.datetime(2020, 7, 11, 12, 34, 56, 124356)
|
||||||
|
DT_UTC = datetime.datetime(2020, 7, 11, 2, 34, 56, 124356)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def fake_now(monkeypatch):
|
||||||
|
"""
|
||||||
|
Patch `datetime.datetime.fromtimestamp()`, `datetime.datetime.utcfromtimestamp()`,
|
||||||
|
and `time.time()` to return deterministic values.
|
||||||
|
"""
|
||||||
|
|
||||||
|
class FakeNow:
|
||||||
|
@classmethod
|
||||||
|
def fromtimestamp(cls, timestamp):
|
||||||
|
return DT
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def utcfromtimestamp(cls, timestamp):
|
||||||
|
return DT_UTC
|
||||||
|
|
||||||
|
def _time():
|
||||||
|
return EPOCH_TS
|
||||||
|
|
||||||
|
monkeypatch.setattr(date_time.datetime, 'datetime', FakeNow)
|
||||||
|
monkeypatch.setattr(time, 'time', _time)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def fake_date_facts(fake_now):
|
||||||
|
"""Return a predictable instance of collected date_time facts."""
|
||||||
|
|
||||||
|
collector = date_time.DateTimeFactCollector()
|
||||||
|
data = collector.collect()
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
('fact_name', 'fact_value'),
|
||||||
|
(
|
||||||
|
('year', '2020'),
|
||||||
|
('month', '07'),
|
||||||
|
('weekday', 'Saturday'),
|
||||||
|
('weekday_number', '6'),
|
||||||
|
('weeknumber', '27'),
|
||||||
|
('day', '11'),
|
||||||
|
('hour', '12'),
|
||||||
|
('minute', '34'),
|
||||||
|
('second', '56'),
|
||||||
|
('date', '2020-07-11'),
|
||||||
|
('time', '12:34:56'),
|
||||||
|
('iso8601_basic', '20200711T123456124356'),
|
||||||
|
('iso8601_basic_short', '20200711T123456'),
|
||||||
|
('iso8601_micro', '2020-07-11T02:34:56.124356Z'),
|
||||||
|
('iso8601', '2020-07-11T02:34:56Z'),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
def test_date_time_facts(fake_date_facts, fact_name, fact_value):
|
||||||
|
assert fake_date_facts['date_time'][fact_name] == fact_value
|
||||||
|
|
||||||
|
|
||||||
|
def test_date_time_epoch(fake_date_facts):
|
||||||
|
"""Test that format of returned epoch value is correct"""
|
||||||
|
|
||||||
|
assert fake_date_facts['date_time']['epoch'].isdigit()
|
||||||
|
assert len(fake_date_facts['date_time']['epoch']) == 10 # This length will not change any time soon
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('fact_name', ('tz', 'tz_dst'))
|
||||||
|
def test_date_time_tz(fake_date_facts, fact_name):
|
||||||
|
"""
|
||||||
|
Test the the returned value for timezone consists of only uppercase
|
||||||
|
letters and is the expected length.
|
||||||
|
"""
|
||||||
|
|
||||||
|
assert fake_date_facts['date_time'][fact_name].isupper()
|
||||||
|
assert 2 <= len(fake_date_facts['date_time'][fact_name]) <= 5
|
||||||
|
assert not set(fake_date_facts['date_time'][fact_name]).difference(set(string.ascii_uppercase))
|
||||||
|
|
||||||
|
|
||||||
|
def test_date_time_tz_offset(fake_date_facts):
|
||||||
|
"""
|
||||||
|
Test that the timezone offset begins with a `+` or `-` and ends with a
|
||||||
|
series of integers.
|
||||||
|
"""
|
||||||
|
|
||||||
|
assert fake_date_facts['date_time']['tz_offset'][0] in ['-', '+']
|
||||||
|
assert fake_date_facts['date_time']['tz_offset'][1:].isdigit()
|
||||||
|
assert len(fake_date_facts['date_time']['tz_offset']) == 5
|
|
@ -1,61 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Copyright (c) 2020 Ansible Project
|
|
||||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
||||||
|
|
||||||
from __future__ import absolute_import, division, print_function
|
|
||||||
__metaclass__ = type
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
import datetime
|
|
||||||
|
|
||||||
from ansible.module_utils.facts.system.date_time import DateTimeFactCollector
|
|
||||||
|
|
||||||
TIMESTAMP = datetime.datetime(2020, 7, 11, 12, 34, 56, 124356)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def fake_now(monkeypatch):
|
|
||||||
"""Patch `datetime.datetime.now()` to return a deterministic value."""
|
|
||||||
class FakeNow:
|
|
||||||
@classmethod
|
|
||||||
def now(cls):
|
|
||||||
return TIMESTAMP
|
|
||||||
|
|
||||||
monkeypatch.setattr(datetime, 'datetime', FakeNow)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def date_facts(monkeypatch, fake_now):
|
|
||||||
"""Return a predictable instance of collected date_time facts."""
|
|
||||||
monkeypatch.setenv('TZ', 'Australia/Melbourne')
|
|
||||||
|
|
||||||
collector = DateTimeFactCollector()
|
|
||||||
data = collector.collect()
|
|
||||||
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
('fact_name', 'fact_value'),
|
|
||||||
(
|
|
||||||
('year', '2020'),
|
|
||||||
('month', '07'),
|
|
||||||
('weekday', 'Saturday'),
|
|
||||||
('weekday_number', '6'),
|
|
||||||
('weeknumber', '27'),
|
|
||||||
('day', '11'),
|
|
||||||
('hour', '12'),
|
|
||||||
('minute', '34'),
|
|
||||||
('second', '56'),
|
|
||||||
('epoch', '1594434896'),
|
|
||||||
('date', '2020-07-11'),
|
|
||||||
('time', '12:34:56'),
|
|
||||||
('iso8601_basic', '20200711T123456124356'),
|
|
||||||
('iso8601_basic_short', '20200711T123456'),
|
|
||||||
('tz', 'AEST'),
|
|
||||||
('tz_dst', 'AEDT'),
|
|
||||||
('tz_offset', '+1000')
|
|
||||||
),
|
|
||||||
)
|
|
||||||
def test_date_time_facts(date_facts, fact_name, fact_value):
|
|
||||||
assert date_facts['date_time'][fact_name] == fact_value
|
|
Loading…
Reference in a new issue