mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
329 lines
17 KiB
Python
329 lines
17 KiB
Python
|
from datetime import datetime, date, timedelta
|
||
|
import unittest
|
||
|
import moment
|
||
|
import moment_parse
|
||
|
|
||
|
# Helpful strftime() format that imcludes all parts of the date including the time zone.
|
||
|
fmt = "%Y-%m-%d %H:%M:%S %Z"
|
||
|
|
||
|
class TestMoment(unittest.TestCase):
|
||
|
|
||
|
new_york = [
|
||
|
# - 1918 -
|
||
|
[datetime(1918, 3, 31, 6, 59, 59), -1633280401000, "EST", 300, 1, 59],
|
||
|
[datetime(1918, 3, 31, 7, 0, 0), -1633280400000, "EDT", 240, 3, 0],
|
||
|
[datetime(1918, 10, 27, 5, 59, 59), -1615140001000, "EDT", 240, 1, 59],
|
||
|
[datetime(1918, 10, 27, 6, 0, 0), -1615140000000, "EST", 300, 1, 0],
|
||
|
# - 1979 -
|
||
|
[datetime(1979, 4, 29, 6, 59, 59), 294217199000, "EST", 300, 1, 59],
|
||
|
[datetime(1979, 4, 29, 7, 0, 0), 294217200000, "EDT", 240, 3, 0],
|
||
|
[datetime(1979, 10, 28, 5, 59, 59), 309938399000, "EDT", 240, 1, 59],
|
||
|
[datetime(1979, 10, 28, 6, 0, 0), 309938400000, "EST", 300, 1, 0],
|
||
|
# - 2037 -
|
||
|
[datetime(2037, 3, 8, 6, 59, 59), 2120108399000, "EST", 300, 1, 59],
|
||
|
[datetime(2037, 03, 8, 7, 0, 0), 2120108400000, "EDT", 240, 3, 0],
|
||
|
[datetime(2037, 11, 1, 5, 59, 59), 2140667999000, "EDT", 240, 1, 59]
|
||
|
]
|
||
|
new_york_errors = [
|
||
|
["America/New_York", "2037-3-8 6:59:59", TypeError],
|
||
|
["America/New_York", [2037, 3, 8, 6, 59, 59], TypeError],
|
||
|
["America/new_york", datetime(1979, 4, 29, 6, 59, 59), KeyError]
|
||
|
]
|
||
|
|
||
|
los_angeles = [
|
||
|
# - 1918 -
|
||
|
# Spanning non-existent hour
|
||
|
[datetime(1918, 3, 31, 1, 59, 59, 0), -1633269601000, "PST", 480, 1, 59],
|
||
|
[datetime(1918, 3, 31, 2, 0, 0, 0), -1633273200000, "PST", 480, 1, 0],
|
||
|
[datetime(1918, 3, 31, 2, 59, 59, 0), -1633269601000, "PST", 480, 1, 59],
|
||
|
[datetime(1918, 3, 31, 3, 0, 0, 0), -1633269600000, "PDT", 420, 3, 0],
|
||
|
# Spanning doubly-existent hour
|
||
|
[datetime(1918, 10, 27, 0, 59, 59, 0), -1615132801000, "PDT", 420, 0, 59],
|
||
|
[datetime(1918, 10, 27, 1, 0, 0, 0), -1615132800000, "PDT", 420, 1, 0],
|
||
|
[datetime(1918, 10, 27, 1, 59, 59, 0), -1615129201000, "PDT", 420, 1, 59],
|
||
|
[datetime(1918, 10, 27, 2, 0, 0, 0), -1615125600000, "PST", 480, 2, 0],
|
||
|
# - 2008 -
|
||
|
# Spanning non-existent hour
|
||
|
[datetime(2008, 3, 9, 1, 59, 59, 0), 1205056799000, "PST", 480, 1, 59],
|
||
|
[datetime(2008, 3, 9, 2, 0, 0, 0), 1205053200000, "PST", 480, 1, 0],
|
||
|
[datetime(2008, 3, 9, 2, 59, 59, 0), 1205056799000, "PST", 480, 1, 59],
|
||
|
[datetime(2008, 3, 9, 3, 0, 0, 0), 1205056800000, "PDT", 420, 3, 0],
|
||
|
# Spanning doubly-existent hour
|
||
|
[datetime(2008, 11, 2, 0, 59, 59, 0), 1225612799000, "PDT", 420, 0, 59],
|
||
|
[datetime(2008, 11, 2, 1, 0, 0, 0), 1225612800000, "PDT", 420, 1, 0],
|
||
|
[datetime(2008, 11, 2, 1, 59, 59, 0), 1225616399000, "PDT", 420, 1, 59],
|
||
|
[datetime(2008, 11, 2, 2, 0, 0, 0), 1225620000000, "PST", 480, 2, 0],
|
||
|
# - 2037 -
|
||
|
[datetime(2037, 3, 8, 1, 59, 59, 0), 2120119199000, "PST", 480, 1, 59],
|
||
|
[datetime(2037, 3, 8, 2, 0, 0, 0), 2120115600000, "PST", 480, 1, 0],
|
||
|
[datetime(2037, 11, 1, 0, 59, 59, 0), 2140675199000, "PDT", 420, 0, 59],
|
||
|
[datetime(2037, 11, 1, 1, 0, 0, 0), 2140675200000, "PDT", 420, 1, 0],
|
||
|
]
|
||
|
|
||
|
parse_samples = [
|
||
|
# Basic set
|
||
|
['MM-DD-YYYY', '12-02-1999', 944092800.000000],
|
||
|
['DD-MM-YYYY', '12-02-1999', 918777600.000000],
|
||
|
['DD/MM/YYYY', '12/02/1999', 918777600.000000],
|
||
|
['DD_MM_YYYY', '12_02_1999', 918777600.000000],
|
||
|
['DD:MM:YYYY', '12:02:1999', 918777600.000000],
|
||
|
['D-M-YY', '2-2-99', 917913600.000000],
|
||
|
['YY', '99', 922060800.000000],
|
||
|
['DD-MM-YYYY h:m:s', '12-02-1999 2:45:10', 918787510.000000],
|
||
|
['DD-MM-YYYY h:m:s a', '12-02-1999 2:45:10 am', 918787510.000000],
|
||
|
['DD-MM-YYYY h:m:s a', '12-02-1999 2:45:10 pm', 918830710.000000],
|
||
|
['h:mm a', '12:00 pm', 1458648000.000000],
|
||
|
['h:mm a', '12:30 pm', 1458649800.000000],
|
||
|
['h:mm a', '12:00 am', 1458604800.000000],
|
||
|
['h:mm a', '12:30 am', 1458606600.000000],
|
||
|
['HH:mm', '12:00', 1458648000.000000],
|
||
|
['YYYY-MM-DDTHH:mm:ss', '2011-11-11T11:11:11', 1321009871.000000],
|
||
|
['ddd MMM DD HH:mm:ss YYYY', 'Tue Apr 07 22:52:51 2009', 1239144771.000000],
|
||
|
['ddd MMMM DD HH:mm:ss YYYY', 'Tue April 07 22:52:51 2009', 1239144771.000000],
|
||
|
['HH:mm:ss', '12:00:00', 1458648000.000000],
|
||
|
['HH:mm:ss', '12:30:00', 1458649800.000000],
|
||
|
['HH:mm:ss', '00:00:00', 1458604800.000000],
|
||
|
['HH:mm:ss S', '00:30:00 1', 1458606600.100000],
|
||
|
['HH:mm:ss SS', '00:30:00 12', 1458606600.120000],
|
||
|
['HH:mm:ss SSS', '00:30:00 123', 1458606600.123000],
|
||
|
['HH:mm:ss S', '00:30:00 7', 1458606600.700000],
|
||
|
['HH:mm:ss SS', '00:30:00 78', 1458606600.780000],
|
||
|
['HH:mm:ss SSS', '00:30:00 789', 1458606600.789000],
|
||
|
|
||
|
# Dropped m
|
||
|
['MM/DD/YYYY h:m:s a', '05/1/2012 12:25:00 p', 1335875100.000000],
|
||
|
['MM/DD/YYYY h:m:s a', '05/1/2012 12:25:00 a', 1335831900.000000],
|
||
|
|
||
|
# 2 digit year with YYYY
|
||
|
['D/M/YYYY', '9/2/99', 918518400.000000],
|
||
|
['D/M/YYYY', '9/2/1999', 918518400.000000],
|
||
|
['D/M/YYYY', '9/2/66', -122860800.000000],
|
||
|
['D/M/YYYY', '9/2/65', 3001363200.000000],
|
||
|
|
||
|
# No separators
|
||
|
['MMDDYYYY', '12021999', 944092800.000000],
|
||
|
['DDMMYYYY', '12021999', 918777600.000000],
|
||
|
['YYYYMMDD', '19991202', 944092800.000000],
|
||
|
['DDMMMYYYY', '10Sep2001', 1000080000.000000],
|
||
|
|
||
|
# Error forgiveness
|
||
|
['MM/DD/YYYY', '12-02-1999', 944092800.000000],
|
||
|
['DD/MM/YYYY', '12/02 /1999', 918777600.000000],
|
||
|
['DD:MM:YYYY', '12:02 :1999', 918777600.000000],
|
||
|
['D-M-YY', '2 2 99', 917913600.000000],
|
||
|
['DD-MM-YYYY h:m:s', '12-02-1999 2:45:10.00', 918787510.000000],
|
||
|
['h:mm a', '12:00pm', 1458648000.000000],
|
||
|
['HH:mm', '1200', 1458648000.000000],
|
||
|
['dddd MMMM DD HH:mm:ss YYYY', 'Tue Apr 7 22:52:51 2009', 1239144771.000000],
|
||
|
['ddd MMM DD HH:mm:ss YYYY', 'Tuesday April 7 22:52:51 2009', 1239144771.000000],
|
||
|
['ddd MMM Do HH:mm:ss YYYY', 'Tuesday April 7th 22:52:51 2009', 1239144771.000000]
|
||
|
]
|
||
|
|
||
|
parse_timezone_samples = [
|
||
|
# Timezone corner cases
|
||
|
['MM-DD-YYYY h:ma', '3-13-2016 1:59am', 'America/New_York', 1457852340], # EST
|
||
|
['MM-DD-YYYY h:ma', '3-13-2016 2:00am', 'America/New_York', 1457848800], # Invalid, -1hr
|
||
|
['MM-DD-YYYY h:ma', '3-13-2016 2:59am', 'America/New_York', 1457852340], # Invalid, -1hr
|
||
|
['MM-DD-YYYY h:ma', '3-13-2016 3:00am', 'America/New_York', 1457852400], # EDT
|
||
|
['MM-DD-YYYY h:ma', '3-13-2016 1:59am', 'America/Los_Angeles', 1457863140], # PST
|
||
|
['MM-DD-YYYY h:ma', '3-13-2016 2:00am', 'America/Los_Angeles', 1457859600], # Invalid, -1hr
|
||
|
['MM-DD-YYYY h:ma', '3-13-2016 2:59am', 'America/Los_Angeles', 1457863140], # Invalid, -1hr
|
||
|
['MM-DD-YYYY h:ma', '3-13-2016 3:00am', 'America/Los_Angeles', 1457863200] # PDT
|
||
|
]
|
||
|
|
||
|
|
||
|
def assertMatches(self, data_entry, moment_obj):
|
||
|
date, timestamp, abbr, offset, hour, minute = data_entry
|
||
|
dt = moment_obj.datetime()
|
||
|
self.assertEqual(moment_obj.timestamp, timestamp)
|
||
|
self.assertEqual(moment_obj.zoneAbbr(), abbr)
|
||
|
self.assertEqual(moment_obj.zoneOffset(), timedelta(minutes=-offset))
|
||
|
self.assertEqual(dt.hour, hour)
|
||
|
self.assertEqual(dt.minute, minute)
|
||
|
|
||
|
# For each UTC date, convert to New York time and compare with expected values
|
||
|
def test_standard_entry(self):
|
||
|
name = "America/New_York"
|
||
|
data = self.new_york
|
||
|
for entry in data:
|
||
|
date = entry[0]
|
||
|
timestamp = entry[1]
|
||
|
m = moment.tz(date).tz(name)
|
||
|
mts = moment.tz(timestamp, name)
|
||
|
self.assertMatches(entry, m)
|
||
|
self.assertMatches(entry, mts)
|
||
|
error_data = self.new_york_errors
|
||
|
for entry in error_data:
|
||
|
name = entry[0]
|
||
|
date = entry[1]
|
||
|
error = entry[2]
|
||
|
self.assertRaises(error, moment.tz, date, name)
|
||
|
|
||
|
# For each Los Angeles date, check that the returned date matches expected values
|
||
|
def test_zone_entry(self):
|
||
|
name = "America/Los_Angeles"
|
||
|
data = self.los_angeles
|
||
|
for entry in data:
|
||
|
date = entry[0]
|
||
|
timestamp = entry[1]
|
||
|
m = moment.tz(date, name)
|
||
|
self.assertMatches(entry, m)
|
||
|
|
||
|
|
||
|
def test_zone(self):
|
||
|
name = "America/New_York"
|
||
|
tzinfo = moment.tzinfo(name)
|
||
|
data = self.new_york
|
||
|
for entry in data:
|
||
|
date = entry[0]
|
||
|
ts = entry[1]
|
||
|
abbr = entry[2]
|
||
|
offset = entry[3]
|
||
|
dt = moment.tz(ts, name).datetime()
|
||
|
self.assertEqual(dt.tzname(), abbr)
|
||
|
self.assertEqual(dt.utcoffset(), timedelta(minutes=-offset))
|
||
|
|
||
|
def test_parse(self):
|
||
|
for s in self.parse_samples:
|
||
|
self.assertEqual(moment_parse.parse(s[1], s[0], 'UTC', date(2016, 3, 22)), s[2])
|
||
|
for s in self.parse_timezone_samples:
|
||
|
self.assertEqual(moment_parse.parse(s[1], s[0], s[2], date(2016, 3, 22)), s[3])
|
||
|
|
||
|
def test_ts_to_dt(self):
|
||
|
# Verify that ts_to_dt works as expected.
|
||
|
value_sec = 1426291200 # 2015-03-14 00:00:00 in UTC
|
||
|
|
||
|
value_dt_utc = moment.ts_to_dt(value_sec, moment.get_zone('UTC'))
|
||
|
value_dt_aware = moment.ts_to_dt(value_sec, moment.get_zone('America/New_York'))
|
||
|
self.assertEqual(value_dt_utc.strftime("%Y-%m-%d %H:%M:%S %Z"), '2015-03-14 00:00:00 UTC')
|
||
|
self.assertEqual(value_dt_aware.strftime("%Y-%m-%d %H:%M:%S %Z"), '2015-03-13 20:00:00 EDT')
|
||
|
|
||
|
def test_dst_switches(self):
|
||
|
# Verify that conversions around DST switches happen correctly. (This is tested in other tests
|
||
|
# as well, but this test case is more focused and easier to debug.)
|
||
|
dst_before = -1633280401
|
||
|
dst_begin = -1633280400
|
||
|
dst_end = -1615140001
|
||
|
dst_after = -1615140000
|
||
|
|
||
|
# Should have no surprises in converting to UTC, since there are not DST dfferences.
|
||
|
def ts_to_dt_utc(dt):
|
||
|
return moment.ts_to_dt(dt, moment.get_zone('UTC'))
|
||
|
self.assertEqual(ts_to_dt_utc(dst_before).strftime(fmt), "1918-03-31 06:59:59 UTC")
|
||
|
self.assertEqual(ts_to_dt_utc(dst_begin ).strftime(fmt), "1918-03-31 07:00:00 UTC")
|
||
|
self.assertEqual(ts_to_dt_utc(dst_end ).strftime(fmt), "1918-10-27 05:59:59 UTC")
|
||
|
self.assertEqual(ts_to_dt_utc(dst_after ).strftime(fmt), "1918-10-27 06:00:00 UTC")
|
||
|
|
||
|
# Converting to America/New_York should produce correct jumps.
|
||
|
def ts_to_dt_nyc(dt):
|
||
|
return moment.ts_to_dt(dt, moment.get_zone('America/New_York'))
|
||
|
self.assertEqual(ts_to_dt_nyc(dst_before).strftime(fmt), "1918-03-31 01:59:59 EST")
|
||
|
self.assertEqual(ts_to_dt_nyc(dst_begin ).strftime(fmt), "1918-03-31 03:00:00 EDT")
|
||
|
self.assertEqual(ts_to_dt_nyc(dst_end ).strftime(fmt), "1918-10-27 01:59:59 EDT")
|
||
|
self.assertEqual(ts_to_dt_nyc(dst_after ).strftime(fmt), "1918-10-27 01:00:00 EST")
|
||
|
self.assertEqual(ts_to_dt_nyc(dst_after + 3599).strftime(fmt), "1918-10-27 01:59:59 EST")
|
||
|
|
||
|
|
||
|
def test_tzinfo(self):
|
||
|
# Verify that tzinfo works correctly.
|
||
|
ts1 = 294217199000 # In EST
|
||
|
ts2 = 294217200000 # In EDT (spring forward, we skip ahead by 1 hour)
|
||
|
utc_dt1 = datetime(1979, 4, 29, 6, 59, 59)
|
||
|
utc_dt2 = datetime(1979, 4, 29, 7, 0, 0)
|
||
|
self.assertEqual(moment.tz(ts1).datetime().strftime(fmt), '1979-04-29 06:59:59 UTC')
|
||
|
self.assertEqual(moment.tz(ts2).datetime().strftime(fmt), '1979-04-29 07:00:00 UTC')
|
||
|
|
||
|
# Verify that we get correct time zone variation depending on DST status.
|
||
|
nyc_dt1 = moment.tz(ts1, 'America/New_York').datetime()
|
||
|
nyc_dt2 = moment.tz(ts2, 'America/New_York').datetime()
|
||
|
self.assertEqual(nyc_dt1.strftime(fmt), '1979-04-29 01:59:59 EST')
|
||
|
self.assertEqual(nyc_dt2.strftime(fmt), '1979-04-29 03:00:00 EDT')
|
||
|
|
||
|
# Make sure we can get timestamps back from these datatimes.
|
||
|
self.assertEqual(moment.dt_to_ts(nyc_dt1)*1000, ts1)
|
||
|
self.assertEqual(moment.dt_to_ts(nyc_dt2)*1000, ts2)
|
||
|
|
||
|
# Verify that the datetime objects we get produce correct time zones in terms of DST when we
|
||
|
# manipulate them. NOTE: it is a bit unexpected that we add 1hr + 1sec rather than just 1sec,
|
||
|
# but it seems like that is how Python datetime works. Note that timezone does get switched
|
||
|
# correctly between EDT and EST.
|
||
|
self.assertEqual(nyc_dt1 + timedelta(seconds=3601), nyc_dt2)
|
||
|
self.assertEqual(nyc_dt2 - timedelta(seconds=3601), nyc_dt1)
|
||
|
self.assertEqual((nyc_dt1 + timedelta(seconds=3601)).strftime(fmt), '1979-04-29 03:00:00 EDT')
|
||
|
self.assertEqual((nyc_dt2 - timedelta(seconds=3601)).strftime(fmt), '1979-04-29 01:59:59 EST')
|
||
|
|
||
|
|
||
|
def test_dt_to_ds(self):
|
||
|
# Verify that dt_to_ts works for both naive and aware datetime objects.
|
||
|
value_dt = datetime(2015, 03, 14, 0, 0) # In UTC
|
||
|
value_sec = 1426291200
|
||
|
tzla = moment.get_zone('America/Los_Angeles')
|
||
|
def format_utc(ts):
|
||
|
return moment.ts_to_dt(ts, moment.get_zone('UTC')).strftime(fmt)
|
||
|
|
||
|
# Check that a naive datetime is interpreted in UTC.
|
||
|
self.assertEqual(value_dt.strftime("%Y-%m-%d %H:%M:%S %Z"), '2015-03-14 00:00:00 ')
|
||
|
self.assertEqual(moment.dt_to_ts(value_dt), value_sec) # Interpreted in UTC
|
||
|
|
||
|
# Get an explicit UTC version and make sure that also works.
|
||
|
value_dt_utc = value_dt.replace(tzinfo=moment.TZ_UTC)
|
||
|
self.assertEqual(value_dt_utc.strftime(fmt), '2015-03-14 00:00:00 UTC')
|
||
|
self.assertEqual(moment.dt_to_ts(value_dt_utc), value_sec)
|
||
|
|
||
|
# Get an aware datetime, and make sure that works too.
|
||
|
value_dt_aware = moment.ts_to_dt(value_sec, moment.get_zone('America/New_York'))
|
||
|
self.assertEqual(value_dt_aware.strftime(fmt), '2015-03-13 20:00:00 EDT')
|
||
|
self.assertEqual(moment.dt_to_ts(value_dt_aware), value_sec)
|
||
|
|
||
|
# Check that dt_to_ts pays attention to the timezone.
|
||
|
# If we interpret midnight in LA time, it's a later timestamp.
|
||
|
self.assertEqual(format_utc(moment.dt_to_ts(value_dt, tzla)), '2015-03-14 07:00:00 UTC')
|
||
|
# The second argument is ignored if the datetime is aware.
|
||
|
self.assertEqual(format_utc(moment.dt_to_ts(value_dt_utc, tzla)), '2015-03-14 00:00:00 UTC')
|
||
|
self.assertEqual(format_utc(moment.dt_to_ts(value_dt_aware, tzla)), '2015-03-14 00:00:00 UTC')
|
||
|
|
||
|
# If we modify an aware datetime, we may get a new timezone abbreviation.
|
||
|
value_dt_aware -= timedelta(days=28)
|
||
|
self.assertEqual(value_dt_aware.strftime(fmt), '2015-02-13 20:00:00 EST')
|
||
|
|
||
|
def test_date_to_ts(self):
|
||
|
d = date(2015, 03, 14)
|
||
|
tzla = moment.get_zone('America/Los_Angeles')
|
||
|
def format_utc(ts):
|
||
|
return moment.ts_to_dt(ts, moment.get_zone('UTC')).strftime(fmt)
|
||
|
|
||
|
self.assertEqual(format_utc(moment.date_to_ts(d)), '2015-03-14 00:00:00 UTC')
|
||
|
self.assertEqual(format_utc(moment.date_to_ts(d, tzla)), '2015-03-14 07:00:00 UTC')
|
||
|
self.assertEqual(moment.ts_to_dt(moment.date_to_ts(d, tzla), tzla).strftime(fmt),
|
||
|
'2015-03-14 00:00:00 PDT')
|
||
|
|
||
|
|
||
|
def test_parse_iso(self):
|
||
|
tzny = moment.get_zone('America/New_York')
|
||
|
iso = moment.parse_iso
|
||
|
self.assertEqual(iso('2011-11-11T11:11:11'), 1321009871.000000)
|
||
|
self.assertEqual(iso('2019-01-22T00:47:39.219071-05:00'), 1548136059.219071)
|
||
|
self.assertEqual(iso('2019-01-22T00:47:39.219071-0500'), 1548136059.219071)
|
||
|
self.assertEqual(iso('2019-01-22T00:47:39.219071', timezone=tzny), 1548136059.219071)
|
||
|
self.assertEqual(iso('2019-01-22T00:47:39.219071'), 1548118059.219071)
|
||
|
self.assertEqual(iso('2019-01-22T00:47:39.219071Z'), 1548118059.219071)
|
||
|
self.assertEqual(iso('2019-01-22T00:47:39.219071Z', timezone=tzny), 1548118059.219071)
|
||
|
self.assertEqual(iso('2019-01-22T00:47:39.219'), 1548118059.219)
|
||
|
self.assertEqual(iso('2019-01-22T00:47:39'), 1548118059)
|
||
|
self.assertEqual(iso('2019-01-22 00:47:39.219071'), 1548118059.219071)
|
||
|
self.assertEqual(iso('2019-01-22 00:47:39'), 1548118059)
|
||
|
self.assertEqual(iso('2019-01-22'), 1548115200)
|
||
|
|
||
|
def test_parse_iso_date(self):
|
||
|
tzny = moment.get_zone('America/New_York')
|
||
|
iso = moment.parse_iso_date
|
||
|
# Note that time components and time zone do NOT affect the returned timestamp.
|
||
|
self.assertEqual(iso('2019-01-22'), 1548115200)
|
||
|
self.assertEqual(iso('2019-01-22T00:47:39.219071'), 1548115200)
|
||
|
self.assertEqual(iso('2019-01-22 00:47:39Z'), 1548115200)
|
||
|
self.assertEqual(iso('2019-01-22T00:47:39.219071-05:00'), 1548115200)
|
||
|
self.assertEqual(iso('2019-01-22T00:47:39.219071+05:00'), 1548115200)
|
||
|
|
||
|
if __name__ == "__main__":
|
||
|
unittest.main()
|