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