#!/usr/bin/env python3
"""Tests for the Timestamp class."""
#
# (C) Pywikibot team, 2014-2023
#
# Distributed under the terms of the MIT license.
#
import calendar
import re
import unittest
from contextlib import suppress
from datetime import datetime, timedelta
from pywikibot.time import Timestamp, parse_duration, str2timedelta
from tests.aspects import TestCase
[docs]
class TestTimestamp(TestCase):
"""Test Timestamp class comparisons."""
net = False
test_results = {
'MW': [
['20090213233130', '1234567890.000000'],
],
'ISO8601': [
['2009-02-13T23:31:30Z', '1234567890.000000'],
['2009-02-13T23:31:30', '1234567890.000000'],
['2009-02-13T23:31:30.123Z', '1234567890.123000'],
['2009-02-13T23:31:30.123', '1234567890.123000'],
['2009-02-13T23:31:30.123456Z', '1234567890.123456'],
['2009-02-13T23:31:30.123456', '1234567890.123456'],
['2009-02-13T23:31:30,123456Z', '1234567890.123456'],
['2009-02-13T23:31:30,123456', '1234567890.123456'],
['2009-02-14T00:31:30+0100', '1234567890.000000'],
['2009-02-13T22:31:30-0100', '1234567890.000000'],
['2009-02-14T00:31:30+01:00', '1234567890.000000'],
['2009-02-13T22:31:30-01:00', '1234567890.000000'],
['2009-02-13T23:41:30+00:10', '1234567890.000000'],
['2009-02-13T23:21:30-00:10', '1234567890.000000'],
['2009-02-14T00:31:30.123456+01', '1234567890.123456'],
['2009-02-13T22:31:30.123456-01', '1234567890.123456'],
['2009-02-14 00:31:30.123456+01', '1234567890.123456'],
['2009-02-13 22:31:30.123456-01', '1234567890.123456'],
],
'POSIX': [
['1234567890', '1234567890.000000'],
['-1234567890', '-1234567890.000000'],
['1234567890.123', '1234567890.123000'],
['-1234567890.123', '-1234567890.123000'],
['1234567890.123456', '1234567890.123456'],
['-1234567890.123456', '-1234567890.123456'],
['1234567890.000001', '1234567890.000001'],
['-1234567890.000001', '-1234567890.000001'],
],
'INVALID': [
['200902132331309999', None],
['2009-99-99 22:31:30.123456-01', None],
['1234567890.1234569999', None],
],
}
[docs]
def test_set_from_timestamp(self):
"""Test creating instance from Timestamp string."""
t1 = Timestamp.utcnow()
t2 = Timestamp.set_timestamp(t1)
self.assertEqual(t1, t2)
self.assertIsInstance(t2, Timestamp)
[docs]
def test_set_from_datetime(self):
"""Test creating instance from datetime.datetime string."""
t1 = datetime.utcnow()
t2 = Timestamp.set_timestamp(t1)
self.assertEqual(t1, t2)
self.assertIsInstance(t2, datetime)
@staticmethod
def _compute_posix(timestr):
"""Compute POSIX timestamp with independent method."""
sec, usec = map(int, timestr.split('.'))
if sec < 0 < usec:
sec -= 1
usec = 1000000 - usec
return datetime(1970, 1, 1) + timedelta(seconds=sec, microseconds=usec)
def _test_set_from_string_fmt(self, fmt):
"""Test creating instance from <FMT> string."""
for timestr, posix in self.test_results[fmt]:
with self.subTest(timestr):
ts = Timestamp.set_timestamp(timestr)
self.assertEqual(ts, self._compute_posix(posix))
self.assertEqual(ts.posix_timestamp_format(), posix)
[docs]
def test_set_from_string_mw(self):
"""Test creating instance from MW string."""
self._test_set_from_string_fmt('MW')
[docs]
def test_set_from_string_iso8601(self):
"""Test creating instance from ISO8601 string."""
self._test_set_from_string_fmt('ISO8601')
[docs]
def test_set_from_string_posix(self):
"""Test creating instance from POSIX string."""
self._test_set_from_string_fmt('POSIX')
[docs]
def test_set_from_string_invalid(self):
"""Test failure creating instance from invalid string."""
for timestr, _posix in self.test_results['INVALID']:
regex = "time data \'[^\']*?\' does not match"
with self.subTest(timestr), \
self.assertRaisesRegex(ValueError, regex):
Timestamp.set_timestamp(timestr)
[docs]
def test_instantiate_from_instance(self):
"""Test passing instance to factory methods works."""
t1 = Timestamp.utcnow()
self.assertIsNot(t1, Timestamp.fromISOformat(t1))
self.assertEqual(t1, Timestamp.fromISOformat(t1))
self.assertIsInstance(Timestamp.fromISOformat(t1), Timestamp)
self.assertIsNot(t1, Timestamp.fromtimestampformat(t1))
self.assertEqual(t1, Timestamp.fromtimestampformat(t1))
self.assertIsInstance(Timestamp.fromtimestampformat(t1), Timestamp)
[docs]
def test_add_timedelta(self):
"""Test addin a timedelta to a Timestamp."""
t1 = Timestamp.utcnow()
t2 = t1 + timedelta(days=1)
if t1.month != t2.month:
self.assertEqual(1, t2.day)
else:
self.assertEqual(t1.day + 1, t2.day)
self.assertIsInstance(t2, Timestamp)
[docs]
def test_add_timedate(self):
"""Test unsupported additions raise NotImplemented."""
t1 = datetime.utcnow()
t2 = t1 + timedelta(days=1)
t3 = t1.__add__(t2)
self.assertIs(t3, NotImplemented)
# Now check that the pywikibot sub-class behaves the same way
t1 = Timestamp.utcnow()
t2 = t1 + timedelta(days=1)
t3 = t1.__add__(t2)
self.assertIs(t3, NotImplemented)
[docs]
def test_sub_timedelta(self):
"""Test subtracting a timedelta from a Timestamp."""
t1 = Timestamp.utcnow()
t2 = t1 - timedelta(days=1)
if t1.month != t2.month:
self.assertEqual(calendar.monthrange(t2.year, t2.month)[1], t2.day)
else:
self.assertEqual(t1.day - 1, t2.day)
self.assertIsInstance(t2, Timestamp)
[docs]
def test_sub_timedate(self):
"""Test subtracting two timestamps."""
t1 = Timestamp.utcnow()
t2 = t1 - timedelta(days=1)
td = t1 - t2
self.assertIsInstance(td, timedelta)
self.assertEqual(t2 + td, t1)
[docs]
class TestTimeFunctions(TestCase):
"""Test functions in time module."""
net = False
[docs]
def test_str2timedelta(self):
"""Test for parsing the shorthand notation of durations."""
date = datetime(2017, 1, 1) # non leap year
self.assertEqual(str2timedelta('0d'), timedelta(0))
self.assertEqual(str2timedelta('4000s'), timedelta(seconds=4000))
self.assertEqual(str2timedelta('4000h'), timedelta(hours=4000))
self.assertEqual(str2timedelta('7d'), str2timedelta('1w'))
self.assertEqual(str2timedelta('3y'), timedelta(1096))
self.assertEqual(str2timedelta('3y', date), timedelta(1095))
with self.assertRaises(ValueError):
str2timedelta('4000@')
with self.assertRaises(ValueError):
str2timedelta('$1')
[docs]
def test_parse_duration(self):
"""Test for extracting key and duration from shorthand notation."""
self.assertEqual(parse_duration('400s'), ('s', 400))
self.assertEqual(parse_duration('7d'), ('d', 7))
self.assertEqual(parse_duration('3y'), ('y', 3))
for invalid_value in ('', '3000', '4000@'):
with self.subTest(value=invalid_value), \
self.assertRaises(ValueError):
parse_duration(invalid_value)
if __name__ == '__main__': # pragma: no cover
with suppress(SystemExit):
unittest.main()