From 10b069921e74f0d0411fb105349471e0f9a79f29 Mon Sep 17 00:00:00 2001
From: David Douard <david.douard@sdfa3.org>
Date: Thu, 12 Mar 2020 16:01:55 +0100
Subject: [PATCH] model: improve a bit the TimestampWithTimezone model

- add a validator for negative_utc (can be True iff offset is 0),
- update the timestamps_with_timezone hypothesis strategy,
- add low-level tests for it.
---
 swh/model/hypothesis_strategies.py | 20 +++++--
 swh/model/model.py                 |  5 ++
 swh/model/tests/test_model.py      | 92 +++++++++++++++++++++++++++++-
 3 files changed, 111 insertions(+), 6 deletions(-)

diff --git a/swh/model/hypothesis_strategies.py b/swh/model/hypothesis_strategies.py
index 19c3fd31..542b457e 100644
--- a/swh/model/hypothesis_strategies.py
+++ b/swh/model/hypothesis_strategies.py
@@ -6,8 +6,9 @@
 import attr
 import datetime
 
+from hypothesis import assume
 from hypothesis.strategies import (
-    binary, builds, characters, composite, dictionaries,
+    binary, booleans, builds, characters, composite, dictionaries,
     from_regex, integers, just, lists, none, one_of,
     sampled_from, sets, text, tuples,
 )
@@ -65,11 +66,20 @@ def timestamps():
         microseconds=integers(0, 1000000))
 
 
-def timestamps_with_timezone():
-    return builds(
-        TimestampWithTimezone,
+@composite
+def timestamps_with_timezone(
+        draw,
         timestamp=timestamps(),
-        offset=integers(min_value=-14*60, max_value=14*60))
+        offset=integers(min_value=-14*60, max_value=14*60),
+        negative_utc=booleans()):
+    timestamp = draw(timestamp)
+    offset = draw(offset)
+    negative_utc = draw(negative_utc)
+    assume(not (negative_utc and offset))
+    return TimestampWithTimezone(
+        timestamp=timestamp,
+        offset=offset,
+        negative_utc=negative_utc)
 
 
 def origins():
diff --git a/swh/model/model.py b/swh/model/model.py
index eb2ec15a..6fdfe329 100644
--- a/swh/model/model.py
+++ b/swh/model/model.py
@@ -180,6 +180,11 @@ class TimestampWithTimezone(BaseModel):
             # you'll find in the wild...
             raise ValueError('offset too large: %d minutes' % value)
 
+    @negative_utc.validator
+    def check_negative_utc(self, attribute, value):
+        if self.offset and value:
+            raise ValueError("negative_utc can only be True is offset=0")
+
     @classmethod
     def from_dict(cls, obj: Union[Dict, datetime.datetime, int]):
         """Builds a TimestampWithTimezone from any of the formats
diff --git a/swh/model/tests/test_model.py b/swh/model/tests/test_model.py
index 35d5695d..e167d5a7 100644
--- a/swh/model/tests/test_model.py
+++ b/swh/model/tests/test_model.py
@@ -18,9 +18,11 @@ from swh.model.model import (
     MissingData, Person
 )
 from swh.model.hashutil import hash_to_bytes, MultiHash
+
 from swh.model.hypothesis_strategies import (
-    objects, origins, origin_visits, origin_visit_updates
+    objects, origins, origin_visits, origin_visit_updates, timestamps
 )
+
 from swh.model.identifiers import (
     directory_identifier, revision_identifier, release_identifier,
     snapshot_identifier
@@ -72,6 +74,13 @@ def test_todict_origin_visit_updates(origin_visit_update):
     assert origin_visit_update == type(origin_visit_update).from_dict(obj)
 
 
+# Timestamp
+
+@given(timestamps())
+def test_timestamps_strategy(timestamp):
+    attr.validate(timestamp)
+
+
 def test_timestamp_seconds():
     attr.validate(Timestamp(seconds=0, microseconds=0))
     with pytest.raises(AttributeTypeError):
@@ -99,6 +108,87 @@ def test_timestamp_microseconds():
         Timestamp(seconds=0, microseconds=-1)
 
 
+def test_timestamp_from_dict():
+    assert Timestamp.from_dict({'seconds': 10, 'microseconds': 5})
+
+    with pytest.raises(AttributeTypeError):
+        Timestamp.from_dict({'seconds': '10', 'microseconds': 5})
+
+    with pytest.raises(AttributeTypeError):
+        Timestamp.from_dict({'seconds': 10, 'microseconds': '5'})
+    with pytest.raises(ValueError):
+        Timestamp.from_dict({'seconds': 0, 'microseconds': -1})
+
+    Timestamp.from_dict({'seconds': 0, 'microseconds': 10**6 - 1})
+    with pytest.raises(ValueError):
+        Timestamp.from_dict({'seconds': 0, 'microseconds': 10**6})
+
+
+# TimestampWithTimezone
+
+def test_timestampwithtimezone():
+    ts = Timestamp(seconds=0, microseconds=0)
+    tstz = TimestampWithTimezone(
+        timestamp=ts,
+        offset=0,
+        negative_utc=False)
+    attr.validate(tstz)
+    assert tstz.negative_utc is False
+
+    attr.validate(TimestampWithTimezone(
+        timestamp=ts,
+        offset=10,
+        negative_utc=False))
+
+    attr.validate(TimestampWithTimezone(
+        timestamp=ts,
+        offset=-10,
+        negative_utc=False))
+
+    tstz = TimestampWithTimezone(
+        timestamp=ts,
+        offset=0,
+        negative_utc=True)
+    attr.validate(tstz)
+    assert tstz.negative_utc is True
+
+    with pytest.raises(AttributeTypeError):
+        TimestampWithTimezone(
+            timestamp=datetime.datetime.now(),
+            offset=0,
+            negative_utc=False)
+
+    with pytest.raises(AttributeTypeError):
+        TimestampWithTimezone(
+            timestamp=ts,
+            offset='0',
+            negative_utc=False)
+
+    with pytest.raises(AttributeTypeError):
+        TimestampWithTimezone(
+            timestamp=ts,
+            offset=1.0,
+            negative_utc=False)
+
+    with pytest.raises(AttributeTypeError):
+        TimestampWithTimezone(
+            timestamp=ts,
+            offset=1,
+            negative_utc=0)
+
+    with pytest.raises(ValueError):
+        TimestampWithTimezone(
+            timestamp=ts,
+            offset=1,
+            negative_utc=True)
+
+    with pytest.raises(ValueError):
+        TimestampWithTimezone(
+            timestamp=ts,
+            offset=-1,
+            negative_utc=True)
+
+
 def test_timestampwithtimezone_from_datetime():
     tz = datetime.timezone(datetime.timedelta(minutes=+60))
     date = datetime.datetime(
-- 
GitLab