[stable-2.9] facts - fix incorrect time for some date_time_facts (#70665) (#70997)

* [stable-2.9] 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
(cherry picked from commit c4f442ed5a)

Co-authored-by: Sam Doran <sdoran@redhat.com>

* Remove tests for tz_dst since that fact only exists in newer versions
pull/70851/head
Sam Doran 4 years ago committed by GitHub
parent 87364a3286
commit 59a409193d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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")

@ -0,0 +1,103 @@
# -*- 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
def test_date_time_tz(fake_date_facts):
"""
Test that the returned value for timezone consists of only uppercase
letters and is the expected length.
"""
assert fake_date_facts['date_time']['tz'].isupper()
assert 2 <= len(fake_date_facts['date_time']['tz']) <= 5
assert not set(fake_date_facts['date_time']['tz']).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
Loading…
Cancel
Save