diff --git a/setup.py b/setup.py index 41aed1bb6..043ac0d00 100644 --- a/setup.py +++ b/setup.py @@ -70,6 +70,7 @@ def read(path): 'zope.testing>=4,<5', 'zope.testrunner>=5,<6', 'zc.customdoctests>=1.0.1,<2', + 'time-machine>=2,<3', 'createcoverage>=1,<2', 'stopit>=1.1.2,<2', 'flake8>=4,<5'], diff --git a/src/crate/client/sqlalchemy/doctests/itests.txt b/src/crate/client/sqlalchemy/doctests/itests.txt index 6f285610a..f9e2d09e0 100644 --- a/src/crate/client/sqlalchemy/doctests/itests.txt +++ b/src/crate/client/sqlalchemy/doctests/itests.txt @@ -105,7 +105,7 @@ aren't set when the row is inserted as there is no default method:: The datetime and date can be set using a update statement:: - >>> location.nullable_date = datetime.today() + >>> location.nullable_date = datetime.utcnow().date() >>> location.nullable_datetime = datetime.utcnow() >>> session.flush() diff --git a/src/crate/client/tests.py b/src/crate/client/tests.py index fe0a8300d..e0abafd20 100644 --- a/src/crate/client/tests.py +++ b/src/crate/client/tests.py @@ -27,7 +27,7 @@ import unittest import doctest from pprint import pprint -from datetime import datetime, date +from datetime import datetime from http.server import HTTPServer, BaseHTTPRequestHandler import ssl import time @@ -213,7 +213,7 @@ class Location(Base): __tablename__ = 'locations' name = sa.Column(sa.String, primary_key=True) kind = sa.Column(sa.String) - date = sa.Column(sa.Date, default=date.today) + date = sa.Column(sa.Date, default=lambda: datetime.utcnow().date()) datetime_tz = sa.Column(sa.DateTime, default=datetime.utcnow) datetime_notz = sa.Column(sa.DateTime, default=datetime.utcnow) nullable_datetime = sa.Column(sa.DateTime) diff --git a/src/crate/testing/test_datetime.py b/src/crate/testing/test_datetime.py new file mode 100644 index 000000000..db08c9cbc --- /dev/null +++ b/src/crate/testing/test_datetime.py @@ -0,0 +1,90 @@ +# -*- coding: utf-8; -*- +# +# Licensed to CRATE Technology GmbH ("Crate") under one or more contributor +# license agreements. See the NOTICE file distributed with this work for +# additional information regarding copyright ownership. Crate licenses +# this file to you under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. You may +# obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +# However, if you have executed another commercial license agreement +# with Crate these terms will supersede the license and you may use the +# software solely pursuant to the terms of the relevant commercial agreement. + +import os +import sys +import unittest +from datetime import datetime, date +from unittest import TestCase, mock + +import time_machine + + +class DateTodayTest(TestCase): + """ + `date.today()` returns the current local date, as advertised in the + documentation [1]. Thus, it depends on the system time zone. + + The following test cases demonstrate that the test suite previously + failed around midnight, where the UTC vs. non-UTC days overlapped, + and when running on machines with non-UTC time zone. + + On the other hand, `datetime.utcnow().date()` works equally well in all + situations, so we want to use that within the SQLAlchemy test cases. + + Funny enough, the problem is not observable on Linux? + + [1] https://docs.python.org/3/library/datetime.html#datetime.date.today + """ + + @mock.patch.dict(os.environ, {"TZ": "UTC"}) + @time_machine.travel("2022-07-22T00:42:00+0200") + def test_date_today_depends_on_system_timezone_success_on_utc(self): + today_local = date.today() + today_utc = datetime.utcnow().date() + self.assertEqual(today_local, today_utc) + self.assertEqual(today_local, date(2022, 7, 21)) + self.assertEqual(today_utc, date(2022, 7, 21)) + + @unittest.skipIf(sys.platform == "linux", "Problem not observable on Linux") + @mock.patch.dict(os.environ, {"TZ": "Europe/Prague"}) + @time_machine.travel("2022-07-22T00:42:00+0200") + def test_date_today_depends_on_system_timezone_failure_on_non_utc(self): + today_local = date.today() + today_utc = datetime.utcnow().date() + self.assertNotEqual(today_local, today_utc) + self.assertEqual(today_local, date(2022, 7, 22)) + self.assertEqual(today_utc, date(2022, 7, 21)) + + @mock.patch.dict(os.environ, {"TZ": "UTC"}) + @time_machine.travel("2022-07-22T00:42:00+0200") + def test_date_today_utc(self): + today_local = date.today() + self.assertEqual(today_local, date(2022, 7, 21)) + + @unittest.skipIf(sys.platform == "linux", "Problem not observable on Linux") + @mock.patch.dict(os.environ, {"TZ": "Europe/Prague"}) + @time_machine.travel("2022-07-22T00:42:00+0200") + def test_date_today_non_utc(self): + today_local = date.today() + self.assertEqual(today_local, date(2022, 7, 22)) + + @mock.patch.dict(os.environ, {"TZ": "UTC"}) + @time_machine.travel("2022-07-22T00:42:00+0200") + def test_utcnow_date_utc(self): + today_utc = datetime.utcnow().date() + self.assertEqual(today_utc, date(2022, 7, 21)) + + @mock.patch.dict(os.environ, {"TZ": "Europe/Prague"}) + @time_machine.travel("2022-07-22T00:42:00+0200") + def test_utcnow_date_non_utc(self): + today_utc = datetime.utcnow().date() + self.assertEqual(today_utc, date(2022, 7, 21)) diff --git a/src/crate/testing/tests.py b/src/crate/testing/tests.py index 9c9c30179..967ba7773 100644 --- a/src/crate/testing/tests.py +++ b/src/crate/testing/tests.py @@ -24,6 +24,8 @@ import unittest import doctest import tempfile + +from .test_datetime import DateTodayTest from .test_layer import LayerUtilsTest @@ -64,4 +66,5 @@ def test_suite(): ) suite.addTest(s) suite.addTest(unittest.makeSuite(LayerUtilsTest)) + suite.addTest(unittest.makeSuite(DateTodayTest)) return suite