From e8b1be00cfd6ff006806e16bdc9aa2eff3a81ae4 Mon Sep 17 00:00:00 2001 From: Georgy Moiseev Date: Thu, 8 Sep 2022 16:45:33 +0300 Subject: [PATCH] msgpack: support datetime interval arithmetic Support datetime and interval arithmetic with the same rules as in Tarantool [1]. Valid operations: - `tarantool.Datetime` + `tarantool.Interval` = `tarantool.Datetime` - `tarantool.Datetime` - `tarantool.Datetime` = `tarantool.Interval` - `tarantool.Interval` + `tarantool.Interval` = `tarantool.Interval` - `tarantool.Interval` - `tarantool.Interval` = `tarantool.Interval` Since `tarantool.Interval` could contain `month` and `year` fields and such operations could be ambiguous, you can use `adjust` field to tune the logic. - `tarantool.IntervalAdjust.NONE` -- only truncation toward the end of month performed (default mode). ``` >>> dt = tarantool.Datetime(year=2022, month=3, day=31) datetime: Timestamp('2022-03-31 00:00:00'), tz: "" >>> di = tarantool.Interval(month=1, adjust=tarantool.IntervalAdjust.NONE) >>> dt + di datetime: Timestamp('2022-04-30 00:00:00'), tz: "" ``` - `tarantool.IntervalAdjust.EXCESS` -- overflow mode, without any snap or truncation to the end of month, straight addition of days in month, stopping over month boundaries if there is less number of days. ``` >>> dt = tarantool.Datetime(year=2022, month=1, day=31) datetime: Timestamp('2022-01-31 00:00:00'), tz: "" >>> di = tarantool.Interval(month=1, adjust=tarantool.IntervalAdjust.EXCESS) >>> dt + di datetime: Timestamp('2022-03-02 00:00:00'), tz: "" ``` - `tarantool.IntervalAdjust.LAST` -- mode when day snaps to the end of month, if happens. ``` >>> dt = tarantool.Datetime(year=2022, month=2, day=28) datetime: Timestamp('2022-02-28 00:00:00'), tz: "" >>> di = tarantool.Interval(month=1, adjust=tarantool.IntervalAdjust.LAST) >>> dt + di datetime: Timestamp('2022-03-31 00:00:00'), tz: "" ``` Tarantool does not yet correctly support subtraction of datetime objects with different timezones [2] and addition of intervals to datetimes with non-fixed offset timezones [3]. tarantool-python implementation support them, but it could be reworked later if core team change another solution. 1. https://github.com/tarantool/tarantool/wiki/Datetime-Internals#interval-arithmetic 2. https://github.com/tarantool/tarantool/issues/7698 3. https://github.com/tarantool/tarantool/issues/7700 Closes #229 --- CHANGELOG.md | 46 ++++ tarantool/msgpack_ext/types/datetime.py | 75 ++++++ tarantool/msgpack_ext/types/interval.py | 64 +++++ test/suites/__init__.py | 4 +- test/suites/test_datetime_arithmetic.py | 317 ++++++++++++++++++++++++ 5 files changed, 505 insertions(+), 1 deletion(-) create mode 100644 test/suites/test_datetime_arithmetic.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 200b78ea..f0f83b9c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -85,6 +85,52 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 Its attributes (same as in init API) are exposed, so you can use them if needed. +- Datetime interval arithmetic support (#229). + + Valid operations: + - `tarantool.Datetime` + `tarantool.Interval` = `tarantool.Datetime` + - `tarantool.Datetime` - `tarantool.Datetime` = `tarantool.Interval` + - `tarantool.Interval` + `tarantool.Interval` = `tarantool.Interval` + - `tarantool.Interval` - `tarantool.Interval` = `tarantool.Interval` + + Since `tarantool.Interval` could contain `month` and `year` fields + and such operations could be ambiguous, you can use `adjust` field + to tune the logic. + + - `tarantool.IntervalAdjust.NONE` -- only truncation toward the end of + month performed (default mode). + + ```python + >>> dt = tarantool.Datetime(year=2022, month=3, day=31) + datetime: Timestamp('2022-03-31 00:00:00'), tz: "" + >>> di = tarantool.Interval(month=1, adjust=tarantool.IntervalAdjust.NONE) + >>> dt + di + datetime: Timestamp('2022-04-30 00:00:00'), tz: "" + ``` + + - `tarantool.IntervalAdjust.EXCESS` -- overflow mode, without any snap + or truncation to the end of month, straight addition of days in month, + stopping over month boundaries if there is less number of days. + + ```python + >>> dt = tarantool.Datetime(year=2022, month=1, day=31) + datetime: Timestamp('2022-01-31 00:00:00'), tz: "" + >>> di = tarantool.Interval(month=1, adjust=tarantool.IntervalAdjust.EXCESS) + >>> dt + di + datetime: Timestamp('2022-03-02 00:00:00'), tz: "" + ``` + + - `tarantool.IntervalAdjust.LAST` -- mode when day snaps to the end of month, + if happens. + + ```python + >>> dt = tarantool.Datetime(year=2022, month=2, day=28) + datetime: Timestamp('2022-02-28 00:00:00'), tz: "" + >>> di = tarantool.Interval(month=1, adjust=tarantool.IntervalAdjust.LAST) + >>> dt + di + datetime: Timestamp('2022-03-31 00:00:00'), tz: "" + ``` + ### Changed - Bump msgpack requirement to 1.0.4 (PR #223). The only reason of this bump is various vulnerability fixes, diff --git a/tarantool/msgpack_ext/types/datetime.py b/tarantool/msgpack_ext/types/datetime.py index 80541ebe..75addc8f 100644 --- a/tarantool/msgpack_ext/types/datetime.py +++ b/tarantool/msgpack_ext/types/datetime.py @@ -6,6 +6,8 @@ import tarantool.msgpack_ext.types.timezones as tt_timezones from tarantool.error import MsgpackError +from tarantool.msgpack_ext.types.interval import Interval, Adjust + # https://www.tarantool.io/en/doc/latest/dev_guide/internals/msgpack_extensions/#the-datetime-type # # The datetime MessagePack representation looks like this: @@ -47,6 +49,7 @@ NSEC_IN_SEC = 1000000000 NSEC_IN_MKSEC = 1000 SEC_IN_MIN = 60 +MONTH_IN_YEAR = 12 def get_bytes_as_int(data, cursor, size): part = data[cursor:cursor + size] @@ -168,6 +171,78 @@ def __init__(self, data=None, *, timestamp=None, year=None, month=None, self._datetime = datetime self._tz = '' + def __add__(self, other): + if not isinstance(other, Interval): + raise TypeError(f"unsupported operand type(s) for +: '{type(self)}' and '{type(other)}'") + + self_dt = self._datetime + + # https://github.com/tarantool/tarantool/wiki/Datetime-Internals#date-adjustions-and-leap-years + months = other.year * MONTH_IN_YEAR + other.month + + res = self_dt + pandas.DateOffset(months = months) + + # pandas.DateOffset works exactly like Adjust.NONE + if other.adjust == Adjust.EXCESS: + if self_dt.day > res.day: + res = res + pandas.DateOffset(days = self_dt.day - res.day) + elif other.adjust == Adjust.LAST: + if self_dt.is_month_end: + # day replaces days + res = res.replace(day = res.days_in_month) + + res = res + pandas.Timedelta(weeks = other.week, + days = other.day, + hours = other.hour, + minutes = other.minute, + seconds = other.sec, + microseconds = other.nsec // NSEC_IN_MKSEC, + nanoseconds = other.nsec % NSEC_IN_MKSEC) + + if res.tzinfo is not None: + tzoffset = compute_offset(res) + else: + tzoffset = 0 + return Datetime(year=res.year, month=res.month, day=res.day, + hour=res.hour, minute=res.minute, sec=res.second, + nsec=res.nanosecond + res.microsecond * NSEC_IN_MKSEC, + tzoffset=tzoffset, tz=self.tz) + + def __sub__(self, other): + if not isinstance(other, Datetime): + raise TypeError(f"unsupported operand type(s) for -: '{type(self)}' and '{type(other)}'") + + self_dt = self._datetime + other_dt = other._datetime + + # Tarantool datetime subtraction ignores timezone info, but it is a bug: + # + # Tarantool 2.10.1-0-g482d91c66 + # + # tarantool> datetime.new{tz='MSK'} - datetime.new{tz='UTC'} + # --- + # - +0 seconds + # ... + # + # Refer to https://github.com/tarantool/tarantool/issues/7698 + # for possible updates. + + if self_dt.tzinfo != other_dt.tzinfo: + other_dt = other_dt.tz_convert(self_dt.tzinfo) + + self_nsec = self_dt.microsecond * NSEC_IN_MKSEC + self_dt.nanosecond + other_nsec = other_dt.microsecond * NSEC_IN_MKSEC + other_dt.nanosecond + + return Interval( + year = self_dt.year - other_dt.year, + month = self_dt.month - other_dt.month, + day = self_dt.day - other_dt.day, + hour = self_dt.hour - other_dt.hour, + minute = self_dt.minute - other_dt.minute, + sec = self_dt.second - other_dt.second, + nsec = self_nsec - other_nsec, + ) + def __eq__(self, other): if isinstance(other, Datetime): return self._datetime == other._datetime diff --git a/tarantool/msgpack_ext/types/interval.py b/tarantool/msgpack_ext/types/interval.py index 61cbdc27..e910062d 100644 --- a/tarantool/msgpack_ext/types/interval.py +++ b/tarantool/msgpack_ext/types/interval.py @@ -102,6 +102,70 @@ def __init__(self, data=None, *, year=0, month=0, week=0, self.nsec = nsec self.adjust = adjust + def __add__(self, other): + if not isinstance(other, Interval): + raise TypeError(f"unsupported operand type(s) for +: '{type(self)}' and '{type(other)}'") + + # Tarantool saves adjust of the first argument + # + # Tarantool 2.10.1-0-g482d91c66 + # + # tarantool> dt1 = datetime.interval.new{year = 2, adjust='last'} + # --- + # ... + # + # tarantool> dt2 = datetime.interval.new{year = 1, adjust='excess'} + # --- + # ... + # + # tarantool> (dt1 + dt2).adjust + # --- + # - 'cdata: 2' + # ... + + return Interval( + year = self.year + other.year, + month = self.month + other.month, + day = self.day + other.day, + hour = self.hour + other.hour, + minute = self.minute + other.minute, + sec = self.sec + other.sec, + nsec = self.nsec + other.nsec, + adjust = self.adjust, + ) + + def __sub__(self, other): + if not isinstance(other, Interval): + raise TypeError(f"unsupported operand type(s) for -: '{type(self)}' and '{type(other)}'") + + # Tarantool saves adjust of the first argument + # + # Tarantool 2.10.1-0-g482d91c66 + # + # tarantool> dt1 = datetime.interval.new{year = 2, adjust='last'} + # --- + # ... + # + # tarantool> dt2 = datetime.interval.new{year = 1, adjust='excess'} + # --- + # ... + # + # tarantool> (dt1 - dt2).adjust + # --- + # - 'cdata: 2' + # ... + + return Interval( + year = self.year - other.year, + month = self.month - other.month, + day = self.day - other.day, + hour = self.hour - other.hour, + minute = self.minute - other.minute, + sec = self.sec - other.sec, + nsec = self.nsec - other.nsec, + adjust = self.adjust, + ) + def __eq__(self, other): if not isinstance(other, Interval): return False diff --git a/test/suites/__init__.py b/test/suites/__init__.py index 7096cad9..903826fa 100644 --- a/test/suites/__init__.py +++ b/test/suites/__init__.py @@ -19,6 +19,7 @@ from .test_uuid import TestSuite_UUID from .test_datetime import TestSuite_Datetime from .test_interval import TestSuite_Interval +from .test_datetime_arithmetic import TestSuite_DatetimeArithmetic test_cases = (TestSuite_Schema_UnicodeConnection, TestSuite_Schema_BinaryConnection, @@ -26,7 +27,8 @@ TestSuite_Mesh, TestSuite_Execute, TestSuite_DBAPI, TestSuite_Encoding, TestSuite_Pool, TestSuite_Ssl, TestSuite_Decimal, TestSuite_UUID, TestSuite_Datetime, - TestSuite_Interval) + TestSuite_Interval, TestSuite_DatetimeArithmetic) + def load_tests(loader, tests, pattern): suite = unittest.TestSuite() diff --git a/test/suites/test_datetime_arithmetic.py b/test/suites/test_datetime_arithmetic.py new file mode 100644 index 00000000..0e13dea4 --- /dev/null +++ b/test/suites/test_datetime_arithmetic.py @@ -0,0 +1,317 @@ +# -*- coding: utf-8 -*- + +from __future__ import print_function + +import sys +import unittest +import msgpack +import warnings +import tarantool +import pandas +import pytz + +from tarantool.msgpack_ext.packer import default as packer_default +from tarantool.msgpack_ext.unpacker import ext_hook as unpacker_ext_hook + +from .lib.tarantool_server import TarantoolServer +from .lib.skip import skip_or_run_datetime_test +from tarantool.error import MsgpackError + +class TestSuite_DatetimeArithmetic(unittest.TestCase): + @classmethod + def setUpClass(self): + print(' DATETIME TYPE ARITHMETIC '.center(70, '='), file=sys.stderr) + print('-' * 70, file=sys.stderr) + self.srv = TarantoolServer() + self.srv.script = 'test/suites/box.lua' + self.srv.start() + + self.adm = self.srv.admin + self.adm(r""" + _, datetime = pcall(require, 'datetime') + + box.schema.space.create('test') + box.space['test']:create_index('primary', { + type = 'tree', + parts = {1, 'string'}, + unique = true}) + + box.schema.user.create('test', {password = 'test', if_not_exists = true}) + box.schema.user.grant('test', 'read,write,execute', 'universe') + + local function add(arg1, arg2) + return arg1 + arg2 + end + rawset(_G, 'add', add) + + local function sub(arg1, arg2) + return arg1 - arg2 + end + rawset(_G, 'sub', sub) + """) + + self.con = tarantool.Connection(self.srv.host, self.srv.args['primary'], + user='test', password='test') + + def setUp(self): + # prevent a remote tarantool from clean our session + if self.srv.is_started(): + self.srv.touch_lock() + + self.adm("box.space['test']:truncate()") + + + interval_cases = { + 'year': { + 'arg_1': tarantool.Interval(year=2), + 'arg_2': tarantool.Interval(year=1), + 'res_add': tarantool.Interval(year=3), + 'res_sub': tarantool.Interval(year=1), + }, + 'date': { + 'arg_1': tarantool.Interval(year=1, month=2, day=3), + 'arg_2': tarantool.Interval(year=3, month=2, day=1), + 'res_add': tarantool.Interval(year=4, month=4, day=4), + 'res_sub': tarantool.Interval(year=-2, month=0, day=2), + }, + 'time': { + 'arg_1': tarantool.Interval(hour=10, minute=20, sec=30), + 'arg_2': tarantool.Interval(hour=2, minute=15, sec=50), + 'res_add': tarantool.Interval(hour=12, minute=35, sec=80), + 'res_sub': tarantool.Interval(hour=8, minute=5, sec=-20), + }, + 'datetime': { + 'arg_1': tarantool.Interval(year=1, month=2, day=3, hour=1, minute=2, sec=3000), + 'arg_2': tarantool.Interval(year=2, month=1, day=31, hour=-3, minute=0, sec=-2000), + 'res_add': tarantool.Interval(year=3, month=3, day=34, hour=-2, minute=2, sec=1000), + 'res_sub': tarantool.Interval(year=-1, month=1, day=-28, hour=4, minute=2, sec=5000), + }, + 'datetime_with_nsec': { + 'arg_1': tarantool.Interval(year=1, month=2, day=3, hour=1, minute=2, + sec=3000, nsec=10000000), + 'arg_2': tarantool.Interval(year=2, month=1, day=31, hour=-3, minute=0, + sec=1000, nsec=9876543), + 'res_add': tarantool.Interval(year=3, month=3, day=34, hour=-2, minute=2, + sec=4000, nsec=19876543), + 'res_sub': tarantool.Interval(year=-1, month=1, day=-28, hour=4, minute=2, + sec=2000, nsec=123457), + }, + 'heterogenous': { + 'arg_1': tarantool.Interval(year=1, month=2, day=3), + 'arg_2': tarantool.Interval(sec=3000, nsec=9876543), + 'res_add': tarantool.Interval(year=1, month=2, day=3, + sec=3000, nsec=9876543), + 'res_sub': tarantool.Interval(year=1, month=2, day=3, + sec=-3000, nsec=-9876543), + }, + 'same_adjust': { + 'arg_1': tarantool.Interval(year=2, adjust=tarantool.IntervalAdjust.LAST), + 'arg_2': tarantool.Interval(year=1, adjust=tarantool.IntervalAdjust.LAST), + 'res_add': tarantool.Interval(year=3, adjust=tarantool.IntervalAdjust.LAST), + 'res_sub': tarantool.Interval(year=1, adjust=tarantool.IntervalAdjust.LAST), + }, + 'different_adjust': { + 'arg_1': tarantool.Interval(year=2, adjust=tarantool.IntervalAdjust.LAST), + 'arg_2': tarantool.Interval(year=1, adjust=tarantool.IntervalAdjust.EXCESS), + 'res_add': tarantool.Interval(year=3, adjust=tarantool.IntervalAdjust.LAST), + 'res_sub': tarantool.Interval(year=1, adjust=tarantool.IntervalAdjust.LAST), + }, + } + + def test_python_interval_add(self): + for name in self.interval_cases.keys(): + with self.subTest(msg=name): + case = self.interval_cases[name] + + self.assertEqual(case['arg_1'] + case['arg_2'], case['res_add']) + + def test_python_interval_sub(self): + for name in self.interval_cases.keys(): + with self.subTest(msg=name): + case = self.interval_cases[name] + + self.assertEqual(case['arg_1'] - case['arg_2'], case['res_sub']) + + @skip_or_run_datetime_test + def test_tarantool_interval_add(self): + for name in self.interval_cases.keys(): + with self.subTest(msg=name): + case = self.interval_cases[name] + + self.assertSequenceEqual(self.con.call('add', case['arg_1'], case['arg_2']), + [case['res_add']]) + + @skip_or_run_datetime_test + def test_tarantool_interval_sub(self): + for name in self.interval_cases.keys(): + with self.subTest(msg=name): + case = self.interval_cases[name] + + self.assertSequenceEqual(self.con.call('sub', case['arg_1'], case['arg_2']), + [case['res_sub']]) + + + datetime_sub_cases = { + 'date': { + 'arg_1': tarantool.Datetime(year=2008, month=2, day=3), + 'arg_2': tarantool.Datetime(year=2010, month=2, day=1), + 'res': tarantool.Interval(year=-2, month=0, day=2), + }, + 'datetime': { + 'arg_1': tarantool.Datetime(year=2001, month=2, day=3, hour=1, minute=2, sec=30), + 'arg_2': tarantool.Datetime(year=2002, month=1, day=31, hour=3, minute=0, sec=20), + 'res': tarantool.Interval(year=-1, month=1, day=-28, hour=-2, minute=2, sec=10), + }, + 'datetime_with_nsec': { + 'arg_1': tarantool.Datetime(year=2001, month=2, day=3, hour=1, minute=2, + sec=30, nsec=10000000), + 'arg_2': tarantool.Datetime(year=2002, month=1, day=31, hour=3, minute=0, + sec=10, nsec=9876543), + 'res': tarantool.Interval(year=-1, month=1, day=-28, hour=-2, minute=2, + sec=20, nsec=123457), + }, + 'heterogenous': { + 'arg_1': tarantool.Datetime(year=2001, month=2, day=3, hour=1, minute=2), + 'arg_2': tarantool.Datetime(year=2001, month=2, day=3, sec=30, + nsec=9876543), + 'res': tarantool.Interval(hour=1, minute=2, sec=-30, nsec=-9876543), + }, + } + + def test_python_datetime_sub(self): + for name in self.datetime_sub_cases.keys(): + with self.subTest(msg=name): + case = self.datetime_sub_cases[name] + + self.assertEqual(case['arg_1'] - case['arg_2'], case['res']) + + @skip_or_run_datetime_test + def test_tarantool_datetime_sub(self): + for name in self.datetime_sub_cases.keys(): + with self.subTest(msg=name): + case = self.datetime_sub_cases[name] + + self.assertSequenceEqual(self.con.call('sub', case['arg_1'], case['arg_2']), + [case['res']]) + + + datetime_sub_different_timezones_case = { + 'arg_1': tarantool.Datetime(year=2001, month=2, day=3, tz='UTC'), + 'arg_2': tarantool.Datetime(year=2001, month=2, day=3, tz='MSK'), + 'res': tarantool.Interval(day=1, hour=-21), + } + + def test_python_datetime_sub_different_timezones(self): + case = self.datetime_sub_different_timezones_case + + self.assertEqual(case['arg_1'] - case['arg_2'], case['res']) + + @skip_or_run_datetime_test + @unittest.expectedFailure # See https://github.com/tarantool/tarantool/issues/7698 + def test_tarantool_datetime_sub(self): + case = self.datetime_sub_different_timezones_case + + self.assertSequenceEqual(self.con.call('sub', case['arg_1'], case['arg_2']), + [case['res']]) + + + datetime_add_cases = { + 'year': { + 'arg_1': tarantool.Datetime(year=2008, month=2, day=3), + 'arg_2': tarantool.Interval(year=1), + 'res': tarantool.Datetime(year=2009, month=2, day=3), + }, + 'date': { + 'arg_1': tarantool.Datetime(year=2008, month=2, day=3), + 'arg_2': tarantool.Interval(year=1, month=2, day=3), + 'res': tarantool.Datetime(year=2009, month=4, day=6), + }, + 'date_days_overflow': { + 'arg_1': tarantool.Datetime(year=2008, month=2, day=3), + 'arg_2': tarantool.Interval(year=1, month=2, day=30), + 'res': tarantool.Datetime(year=2009, month=5, day=3), + }, + 'time': { + 'arg_1': tarantool.Datetime(year=2008, month=2, day=3), + 'arg_2': tarantool.Interval(hour=1, minute=2, sec=3), + 'res': tarantool.Datetime(year=2008, month=2, day=3, hour=1, minute=2, sec=3), + }, + 'time_secs_overflow': { + 'arg_1': tarantool.Datetime(year=2008, month=2, day=3), + 'arg_2': tarantool.Interval(hour=1, minute=2, sec=13003), + 'res': tarantool.Datetime(year=2008, month=2, day=3, hour=4, minute=38, sec=43), + }, + 'nsecs': { + 'arg_1': tarantool.Datetime(year=2008, month=2, day=3, hour=3, minute=36, sec=43), + 'arg_2': tarantool.Interval(nsec=10000023), + 'res': tarantool.Datetime(year=2008, month=2, day=3, hour=3, minute=36, sec=43, + nsec=10000023), + }, + 'zero': { + 'arg_1': tarantool.Datetime(year=2008, month=2, day=3, hour=3, minute=36, sec=43), + 'arg_2': tarantool.Interval(), + 'res': tarantool.Datetime(year=2008, month=2, day=3, hour=3, minute=36, sec=43), + }, + 'month_non_last_day_none_adjust': { + 'arg_1': tarantool.Datetime(year=2009, month=1, day=30), + 'arg_2': tarantool.Interval(month=13, adjust=tarantool.IntervalAdjust.NONE), + 'res': tarantool.Datetime(year=2010, month=2, day=28), + }, + 'month_non_last_day_excess_adjust': { + 'arg_1': tarantool.Datetime(year=2009, month=1, day=30), + 'arg_2': tarantool.Interval(month=13, adjust=tarantool.IntervalAdjust.EXCESS), + 'res': tarantool.Datetime(year=2010, month=3, day=2), + }, + 'month_non_last_day_last_adjust': { + 'arg_1': tarantool.Datetime(year=2009, month=3, day=30), + 'arg_2': tarantool.Interval(month=2, adjust=tarantool.IntervalAdjust.LAST), + 'res': tarantool.Datetime(year=2009, month=5, day=30), + }, + 'month_overflow_last_day_last_adjust': { + 'arg_1': tarantool.Datetime(year=2009, month=2, day=28), + 'arg_2': tarantool.Interval(month=1, adjust=tarantool.IntervalAdjust.LAST), + 'res': tarantool.Datetime(year=2009, month=3, day=31), + }, + } + + def test_python_datetime_add(self): + for name in self.datetime_add_cases.keys(): + with self.subTest(msg=name): + case = self.datetime_add_cases[name] + + self.assertEqual(case['arg_1'] + case['arg_2'], case['res']) + + @skip_or_run_datetime_test + def test_tarantool_datetime_add(self): + for name in self.datetime_add_cases.keys(): + with self.subTest(msg=name): + case = self.datetime_add_cases[name] + + self.assertSequenceEqual(self.con.call('add', case['arg_1'], case['arg_2']), + [case['res']]) + + datetime_add_winter_time_switch_case = { + 'arg_1': tarantool.Datetime(year=2008, month=1, day=1, tz='Europe/Moscow'), + 'arg_2': tarantool.Interval(month=6), + 'res': tarantool.Datetime(year=2008, month=7, day=1, tz='Europe/Moscow'), + } + + def test_python_datetime_add_winter_time_switch(self): + case = self.datetime_add_winter_time_switch_case + + self.assertEqual(case['arg_1'] + case['arg_2'], case['res']) + + @skip_or_run_datetime_test + @unittest.expectedFailure # See https://github.com/tarantool/tarantool/issues/7700 + def test_tarantool_datetime_add_winter_time_switch(self): + case = self.datetime_add_winter_time_switch_case + + self.assertSequenceEqual(self.con.call('add', case['arg_1'], case['arg_2']), + [case['res']]) + + + @classmethod + def tearDownClass(self): + self.con.close() + self.srv.stop() + self.srv.clean()