/ tests / test_time.py
test_time.py
  1  # SPDX-FileCopyrightText: 2001-2021 Python Software Foundation.All rights reserved.
  2  # SPDX-FileCopyrightText: 2000 BeOpen.com. All rights reserved.
  3  # SPDX-FileCopyrightText: 1995-2001 Corporation for National Research Initiatives.
  4  #                         All rights reserved.
  5  # SPDX-FileCopyrightText: 1995-2001 Corporation for National Research Initiatives.
  6  #                         All rights reserved.
  7  # SPDX-FileCopyrightText: 1991-1995 Stichting Mathematisch Centrum. All rights reserved.
  8  # SPDX-FileCopyrightText: 2021 Brent Rubell for Adafruit Industries
  9  # SPDX-License-Identifier: Python-2.0
 10  # Implements a subset of https://github.com/python/cpython/blob/master/Lib/test/datetimetester.py
 11  # NOTE: This test is based off CPython and therefore linting is disabled within this file.
 12  # pylint:disable=invalid-name, no-member, cell-var-from-loop, unused-argument, no-self-use, too-few-public-methods, consider-using-enumerate, undefined-variable, wrong-import-order, wrong-import-position
 13  # CircuitPython subset implementation
 14  import sys
 15  
 16  sys.path.append("..")
 17  from adafruit_datetime import time as cpy_time
 18  
 19  # CPython standard implementation
 20  from datetime import time as cpython_time
 21  import unittest
 22  
 23  
 24  # An arbitrary collection of objects of non-datetime types, for testing
 25  # mixed-type comparisons.
 26  OTHERSTUFF = (10, 34.5, "abc", {}, [], ())
 27  
 28  #############################################################################
 29  # Base class for testing a particular aspect of timedelta, time, date and
 30  # datetime comparisons.
 31  
 32  
 33  class HarmlessMixedComparison:
 34      # Test that __eq__ and __ne__ don't complain for mixed-type comparisons.
 35  
 36      # Subclasses must define 'theclass', and theclass(1, 1, 1) must be a
 37      # legit constructor.
 38  
 39      def test_harmless_mixed_comparison(self):
 40          me = self.theclass(1, 1, 1)
 41  
 42          self.assertFalse(me == ())
 43          self.assertTrue(me != ())
 44          self.assertFalse(() == me)
 45          self.assertTrue(() != me)
 46  
 47          self.assertIn(me, [1, 20, [], me])
 48          self.assertIn([], [me, 1, 20, []])
 49  
 50      def test_harmful_mixed_comparison(self):
 51          me = self.theclass(1, 1, 1)
 52  
 53          self.assertRaises(TypeError, lambda: me < ())
 54          self.assertRaises(TypeError, lambda: me <= ())
 55          self.assertRaises(TypeError, lambda: me > ())
 56          self.assertRaises(TypeError, lambda: me >= ())
 57  
 58          self.assertRaises(TypeError, lambda: () < me)
 59          self.assertRaises(TypeError, lambda: () <= me)
 60          self.assertRaises(TypeError, lambda: () > me)
 61          self.assertRaises(TypeError, lambda: () >= me)
 62  
 63  
 64  class TestTime(HarmlessMixedComparison, unittest.TestCase):
 65  
 66      theclass = cpy_time
 67      theclass_cpython = cpython_time
 68  
 69      def test_basic_attributes(self):
 70          t = self.theclass(12, 0)
 71          t2 = self.theclass_cpython(12, 0)
 72          # Check adafruit_datetime module
 73          self.assertEqual(t.hour, 12)
 74          self.assertEqual(t.minute, 0)
 75          self.assertEqual(t.second, 0)
 76          self.assertEqual(t.microsecond, 0)
 77          # Validate against CPython datetime module
 78          self.assertEqual(t.hour, t2.hour)
 79          self.assertEqual(t.minute, t2.minute)
 80          self.assertEqual(t.second, t2.second)
 81          self.assertEqual(t.microsecond, t2.microsecond)
 82  
 83      def test_basic_attributes_nonzero(self):
 84          # Make sure all attributes are non-zero so bugs in
 85          # bit-shifting access show up.
 86          t = self.theclass(12, 59, 59, 8000)
 87          t2 = self.theclass_cpython(12, 59, 59, 8000)
 88          # Check adafruit_datetime module
 89          self.assertEqual(t.hour, 12)
 90          self.assertEqual(t.minute, 59)
 91          self.assertEqual(t.second, 59)
 92          self.assertEqual(t.microsecond, 8000)
 93          # Validate against CPython datetime module
 94          self.assertEqual(t.hour, t2.hour)
 95          self.assertEqual(t.minute, t2.minute)
 96          self.assertEqual(t.second, t2.second)
 97          self.assertEqual(t.microsecond, t2.microsecond)
 98  
 99      def test_comparing(self):
100          args = [1, 2, 3, 4]
101          t1 = self.theclass(*args)
102          t2 = self.theclass(*args)
103          self.assertEqual(t1, t2)
104          self.assertTrue(t1 <= t2)
105          self.assertTrue(t1 >= t2)
106          self.assertTrue(not t1 != t2)
107          self.assertTrue(not t1 < t2)
108          self.assertTrue(not t1 > t2)
109  
110          for i in range(len(args)):
111              newargs = args[:]
112              newargs[i] = args[i] + 1
113              t2 = self.theclass(*newargs)  # this is larger than t1
114              self.assertTrue(t1 < t2)
115              self.assertTrue(t2 > t1)
116              self.assertTrue(t1 <= t2)
117              self.assertTrue(t2 >= t1)
118              self.assertTrue(t1 != t2)
119              self.assertTrue(t2 != t1)
120              self.assertTrue(not t1 == t2)
121              self.assertTrue(not t2 == t1)
122              self.assertTrue(not t1 > t2)
123              self.assertTrue(not t2 < t1)
124              self.assertTrue(not t1 >= t2)
125              self.assertTrue(not t2 <= t1)
126  
127          for badarg in OTHERSTUFF:
128              self.assertEqual(t1 == badarg, False)
129              self.assertEqual(t1 != badarg, True)
130              self.assertEqual(badarg == t1, False)
131              self.assertEqual(badarg != t1, True)
132  
133              self.assertRaises(TypeError, lambda: t1 <= badarg)
134              self.assertRaises(TypeError, lambda: t1 < badarg)
135              self.assertRaises(TypeError, lambda: t1 > badarg)
136              self.assertRaises(TypeError, lambda: t1 >= badarg)
137              self.assertRaises(TypeError, lambda: badarg <= t1)
138              self.assertRaises(TypeError, lambda: badarg < t1)
139              self.assertRaises(TypeError, lambda: badarg > t1)
140              self.assertRaises(TypeError, lambda: badarg >= t1)
141  
142      def test_bad_constructor_arguments(self):
143          # bad hours
144          self.theclass(0, 0)  # no exception
145          self.theclass(23, 0)  # no exception
146          self.assertRaises(ValueError, self.theclass, -1, 0)
147          self.assertRaises(ValueError, self.theclass, 24, 0)
148          # bad minutes
149          self.theclass(23, 0)  # no exception
150          self.theclass(23, 59)  # no exception
151          self.assertRaises(ValueError, self.theclass, 23, -1)
152          self.assertRaises(ValueError, self.theclass, 23, 60)
153          # bad seconds
154          self.theclass(23, 59, 0)  # no exception
155          self.theclass(23, 59, 59)  # no exception
156          self.assertRaises(ValueError, self.theclass, 23, 59, -1)
157          self.assertRaises(ValueError, self.theclass, 23, 59, 60)
158          # bad microseconds
159          self.theclass(23, 59, 59, 0)  # no exception
160          self.theclass(23, 59, 59, 999999)  # no exception
161          self.assertRaises(ValueError, self.theclass, 23, 59, 59, -1)
162          self.assertRaises(ValueError, self.theclass, 23, 59, 59, 1000000)
163  
164      def test_hash_equality(self):
165          d = self.theclass(23, 30, 17)
166          e = self.theclass(23, 30, 17)
167          self.assertEqual(d, e)
168          self.assertEqual(hash(d), hash(e))
169  
170          dic = {d: 1}
171          dic[e] = 2
172          self.assertEqual(len(dic), 1)
173          self.assertEqual(dic[d], 2)
174          self.assertEqual(dic[e], 2)
175  
176          d = self.theclass(0, 5, 17)
177          e = self.theclass(0, 5, 17)
178          self.assertEqual(d, e)
179          self.assertEqual(hash(d), hash(e))
180  
181          dic = {d: 1}
182          dic[e] = 2
183          self.assertEqual(len(dic), 1)
184          self.assertEqual(dic[d], 2)
185          self.assertEqual(dic[e], 2)
186  
187      def test_isoformat(self):
188          t = self.theclass(4, 5, 1, 123)
189          self.assertEqual(t.isoformat(), "04:05:01.000123")
190          self.assertEqual(t.isoformat(), str(t))
191  
192          t = self.theclass()
193          self.assertEqual(t.isoformat(), "00:00:00")
194          self.assertEqual(t.isoformat(), str(t))
195  
196          t = self.theclass(microsecond=1)
197          self.assertEqual(t.isoformat(), "00:00:00.000001")
198          self.assertEqual(t.isoformat(), str(t))
199  
200          t = self.theclass(microsecond=10)
201          self.assertEqual(t.isoformat(), "00:00:00.000010")
202          self.assertEqual(t.isoformat(), str(t))
203  
204          t = self.theclass(microsecond=100)
205          self.assertEqual(t.isoformat(), "00:00:00.000100")
206          self.assertEqual(t.isoformat(), str(t))
207  
208          t = self.theclass(microsecond=1000)
209          self.assertEqual(t.isoformat(), "00:00:00.001000")
210          self.assertEqual(t.isoformat(), str(t))
211  
212          t = self.theclass(microsecond=10000)
213          self.assertEqual(t.isoformat(), "00:00:00.010000")
214          self.assertEqual(t.isoformat(), str(t))
215  
216          t = self.theclass(microsecond=100000)
217          self.assertEqual(t.isoformat(), "00:00:00.100000")
218          self.assertEqual(t.isoformat(), str(t))
219  
220      def test_1653736(self):
221          # verify it doesn't accept extra keyword arguments
222          t = self.theclass(second=1)
223          self.assertRaises(TypeError, t.isoformat, foo=3)
224  
225      @unittest.skip("strftime not implemented for CircuitPython time objects")
226      def test_strftime(self):
227          t = self.theclass(1, 2, 3, 4)
228          self.assertEqual(t.strftime("%H %M %S %f"), "01 02 03 000004")
229          # A naive object replaces %z and %Z with empty strings.
230          self.assertEqual(t.strftime("'%z' '%Z'"), "'' ''")
231          # bpo-34482: Check that surrogates don't cause a crash.
232          try:
233              t.strftime("%H\ud800%M")
234          except UnicodeEncodeError:
235              pass
236  
237      @unittest.skip("strftime not implemented for CircuitPython time objects")
238      def test_format(self):
239          t = self.theclass(1, 2, 3, 4)
240          self.assertEqual(t.__format__(""), str(t))
241  
242          with self.assertRaisesRegex(TypeError, "must be str, not int"):
243              t.__format__(123)
244  
245          # check that a derived class's __str__() gets called
246          class A(self.theclass):
247              def __str__(self):
248                  return "A"
249  
250          a = A(1, 2, 3, 4)
251          self.assertEqual(a.__format__(""), "A")
252  
253          # check that a derived class's strftime gets called
254          class B(self.theclass):
255              def strftime(self, format_spec):
256                  return "B"
257  
258          b = B(1, 2, 3, 4)
259          self.assertEqual(b.__format__(""), str(t))
260  
261          for fmt in [
262              "%H %M %S",
263          ]:
264              self.assertEqual(t.__format__(fmt), t.strftime(fmt))
265              self.assertEqual(a.__format__(fmt), t.strftime(fmt))
266              self.assertEqual(b.__format__(fmt), "B")
267  
268      def test_str(self):
269          self.assertEqual(str(self.theclass(1, 2, 3, 4)), "01:02:03.000004")
270          self.assertEqual(str(self.theclass(10, 2, 3, 4000)), "10:02:03.004000")
271          self.assertEqual(str(self.theclass(0, 2, 3, 400000)), "00:02:03.400000")
272          self.assertEqual(str(self.theclass(12, 2, 3, 0)), "12:02:03")
273          self.assertEqual(str(self.theclass(23, 15, 0, 0)), "23:15:00")
274  
275      def test_repr(self):
276          name = "datetime." + self.theclass.__name__
277          self.assertEqual(repr(self.theclass(1, 2, 3, 4)), "%s(1, 2, 3, 4)" % name)
278          self.assertEqual(
279              repr(self.theclass(10, 2, 3, 4000)), "%s(10, 2, 3, 4000)" % name
280          )
281          self.assertEqual(
282              repr(self.theclass(0, 2, 3, 400000)), "%s(0, 2, 3, 400000)" % name
283          )
284          self.assertEqual(repr(self.theclass(12, 2, 3, 0)), "%s(12, 2, 3)" % name)
285          self.assertEqual(repr(self.theclass(23, 15, 0, 0)), "%s(23, 15)" % name)
286  
287      @unittest.skip("Skip for CircuitPython  - not implemented")
288      def test_resolution_info(self):
289          self.assertIsInstance(self.theclass.min, self.theclass)
290          self.assertIsInstance(self.theclass.max, self.theclass)
291          self.assertIsInstance(self.theclass.resolution, timedelta)
292          self.assertTrue(self.theclass.max > self.theclass.min)
293  
294      @unittest.skip("Skip for CircuitPython  - not implemented")
295      def test_pickling(self):
296          args = 20, 59, 16, 64 ** 2
297          orig = self.theclass(*args)
298          for pickler, unpickler, proto in pickle_choices:
299              green = pickler.dumps(orig, proto)
300              derived = unpickler.loads(green)
301              self.assertEqual(orig, derived)
302  
303      @unittest.skip("Skip for CircuitPython  - not implemented")
304      def test_pickling_subclass_time(self):
305          args = 20, 59, 16, 64 ** 2
306          orig = SubclassTime(*args)
307          for pickler, unpickler, proto in pickle_choices:
308              green = pickler.dumps(orig, proto)
309              derived = unpickler.loads(green)
310              self.assertEqual(orig, derived)
311  
312      def test_bool(self):
313          # time is always True.
314          cls = self.theclass
315          self.assertTrue(cls(1))
316          self.assertTrue(cls(0, 1))
317          self.assertTrue(cls(0, 0, 1))
318          self.assertTrue(cls(0, 0, 0, 1))
319          self.assertTrue(cls(0))
320          self.assertTrue(cls())
321  
322      @unittest.skip("Skip for CircuitPython  - replace() not implemented")
323      def test_replace(self):
324          cls = self.theclass
325          args = [1, 2, 3, 4]
326          base = cls(*args)
327          self.assertEqual(base, base.replace())
328  
329          i = 0
330          for name, newval in (
331              ("hour", 5),
332              ("minute", 6),
333              ("second", 7),
334              ("microsecond", 8),
335          ):
336              newargs = args[:]
337              newargs[i] = newval
338              expected = cls(*newargs)
339              got = base.replace(**{name: newval})
340              self.assertEqual(expected, got)
341              i += 1
342  
343          # Out of bounds.
344          base = cls(1)
345          self.assertRaises(ValueError, base.replace, hour=24)
346          self.assertRaises(ValueError, base.replace, minute=-1)
347          self.assertRaises(ValueError, base.replace, second=100)
348          self.assertRaises(ValueError, base.replace, microsecond=1000000)
349  
350      @unittest.skip("Skip for CircuitPython  - replace() not implemented")
351      def test_subclass_replace(self):
352          class TimeSubclass(self.theclass):
353              pass
354  
355          ctime = TimeSubclass(12, 30)
356          self.assertIs(type(ctime.replace(hour=10)), TimeSubclass)
357  
358      def test_subclass_time(self):
359          class C(self.theclass):
360              theAnswer = 42
361  
362              def __new__(cls, *args, **kws):
363                  temp = kws.copy()
364                  extra = temp.pop("extra")
365                  result = self.theclass.__new__(cls, *args, **temp)
366                  result.extra = extra
367                  return result
368  
369              def newmeth(self, start):
370                  return start + self.hour + self.second
371  
372          args = 4, 5, 6
373  
374          dt1 = self.theclass(*args)
375          dt2 = C(*args, **{"extra": 7})
376  
377          self.assertEqual(dt2.__class__, C)
378          self.assertEqual(dt2.theAnswer, 42)
379          self.assertEqual(dt2.extra, 7)
380          self.assertEqual(dt1.isoformat(), dt2.isoformat())
381          self.assertEqual(dt2.newmeth(-7), dt1.hour + dt1.second - 7)
382  
383      def test_backdoor_resistance(self):
384          # see TestDate.test_backdoor_resistance().
385          base = "2:59.0"
386          for hour_byte in " ", "9", chr(24), "\xff":
387              self.assertRaises(TypeError, self.theclass, hour_byte + base[1:])
388          # Good bytes, but bad tzinfo:
389          with self.assertRaises(TypeError):
390              self.theclass(bytes([1] * len(base)), "EST")