diff --git a/swh/model/hypothesis_strategies.py b/swh/model/hypothesis_strategies.py index a647aa8d13bd73ef2e8e296443f27877601e8375..2dc601b1b26116f2f9879f4eea95afa511aebfd7 100644 --- a/swh/model/hypothesis_strategies.py +++ b/swh/model/hypothesis_strategies.py @@ -239,6 +239,7 @@ def present_contents_d(): return builds( dict, data=binary(max_size=4096), + ctime=optional(datetimes()), status=one_of(just('visible'), just('hidden')), ) @@ -258,6 +259,7 @@ def skipped_contents_d(draw): result[k] = None result['reason'] = draw(pgsql_text()) result['status'] = 'absent' + result['ctime'] = draw(optional(datetimes())) return result diff --git a/swh/model/model.py b/swh/model/model.py index 5cc40afe150d12ae17ea15213cae2e88fd6ca7b6..95fe19900c1fa3435358995fb5f62775b9e0e6b4 100644 --- a/swh/model/model.py +++ b/swh/model/model.py @@ -554,12 +554,6 @@ class BaseContent(BaseModel): return d - def to_dict(self): - content = super().to_dict() - if content['ctime'] is None: - del content['ctime'] - return content - @classmethod def from_dict(cls, d, use_subclass=True): if use_subclass: @@ -628,7 +622,7 @@ class Content(BaseContent): return content @classmethod - def from_data(cls, data, status='visible') -> 'Content': + def from_data(cls, data, status='visible', ctime=None) -> 'Content': """Generate a Content from a given `data` byte string. This populates the Content with the hashes and length for the data @@ -636,10 +630,14 @@ class Content(BaseContent): """ d = cls._hash_data(data) d['status'] = status + d['ctime'] = ctime return cls(**d) @classmethod def from_dict(cls, d): + if isinstance(d.get('ctime'), str): + d = d.copy() + d['ctime'] = dateutil.parser.parse(d['ctime']) return super().from_dict(d, use_subclass=False) def with_data(self) -> 'Content': @@ -710,7 +708,11 @@ class SkippedContent(BaseContent): return content @classmethod - def from_data(cls, data, reason: str) -> 'SkippedContent': + def from_data( + cls, + data: bytes, + reason: str, + ctime: Optional[datetime.datetime] = None) -> 'SkippedContent': """Generate a SkippedContent from a given `data` byte string. This populates the SkippedContent with the hashes and length for the @@ -723,6 +725,7 @@ class SkippedContent(BaseContent): del d['data'] d['status'] = 'absent' d['reason'] = reason + d['ctime'] = ctime return cls(**d) @classmethod diff --git a/swh/model/tests/test_hypothesis_strategies.py b/swh/model/tests/test_hypothesis_strategies.py index 59b4eabc307e91aa4164eae7850d5a049dbf4d77..dd77c8de4e6c742233ade4d8617704b04bb22d41 100644 --- a/swh/model/tests/test_hypothesis_strategies.py +++ b/swh/model/tests/test_hypothesis_strategies.py @@ -44,15 +44,13 @@ def test_dicts_generation(obj_type_and_obj): (obj_type, object_) = obj_type_and_obj assert_nested_dict(object_) if obj_type == 'content': + COMMON_KEYS = set(DEFAULT_ALGORITHMS) | {'length', 'status', 'ctime'} if object_['status'] == 'visible': - assert set(object_) <= \ - set(DEFAULT_ALGORITHMS) | {'length', 'status', 'data'} + assert set(object_) <= COMMON_KEYS | {'data'} elif object_['status'] == 'absent': - assert set(object_) == \ - set(DEFAULT_ALGORITHMS) | {'length', 'status', 'reason'} + assert set(object_) == COMMON_KEYS | {'reason'} elif object_['status'] == 'hidden': - assert set(object_) <= \ - set(DEFAULT_ALGORITHMS) | {'length', 'status', 'data'} + assert set(object_) <= COMMON_KEYS | {'data'} else: assert False, object_ elif obj_type == 'release': @@ -68,15 +66,13 @@ def test_model_to_dicts(obj_type_and_obj): obj_dict = object_.to_dict() assert_nested_dict(obj_dict) if obj_type == 'content': + COMMON_KEYS = set(DEFAULT_ALGORITHMS) | {'length', 'status', 'ctime'} if obj_dict['status'] == 'visible': - assert set(obj_dict) == \ - set(DEFAULT_ALGORITHMS) | {'length', 'status', 'data'} + assert set(obj_dict) == COMMON_KEYS | {'data'} elif obj_dict['status'] == 'absent': - assert set(obj_dict) == \ - set(DEFAULT_ALGORITHMS) | {'length', 'status', 'reason'} + assert set(obj_dict) == COMMON_KEYS | {'reason'} elif obj_dict['status'] == 'hidden': - assert set(obj_dict) == \ - set(DEFAULT_ALGORITHMS) | {'length', 'status', 'data'} + assert set(obj_dict) == COMMON_KEYS | {'data'} else: assert False, obj_dict elif obj_type == 'release': diff --git a/swh/model/tests/test_model.py b/swh/model/tests/test_model.py index 4bbb1d0741dbbe1eaba84908632c9fa229d67e4e..f6164ef1a64bfa4890e8593a12019d87a3f17ef5 100644 --- a/swh/model/tests/test_model.py +++ b/swh/model/tests/test_model.py @@ -18,11 +18,7 @@ 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, - skipped_contents_d, timestamps -) +import swh.model.hypothesis_strategies as strategies from swh.model.identifiers import ( directory_identifier, revision_identifier, release_identifier, snapshot_identifier @@ -32,7 +28,7 @@ from swh.model.tests.test_identifiers import ( ) -@given(objects()) +@given(strategies.objects()) def test_todict_inverse_fromdict(objtype_and_obj): (obj_type, obj) = objtype_and_obj @@ -52,7 +48,7 @@ def test_todict_inverse_fromdict(objtype_and_obj): assert obj_as_dict == type(obj).from_dict(obj_as_dict).to_dict() -@given(origins()) +@given(strategies.origins()) def test_todict_origins(origin): obj = origin.to_dict() @@ -60,14 +56,14 @@ def test_todict_origins(origin): assert type(origin)(url=origin.url) == type(origin).from_dict(obj) -@given(origin_visits()) +@given(strategies.origin_visits()) def test_todict_origin_visits(origin_visit): obj = origin_visit.to_dict() assert origin_visit == type(origin_visit).from_dict(obj) -@given(origin_visit_updates()) +@given(strategies.origin_visit_updates()) def test_todict_origin_visit_updates(origin_visit_update): obj = origin_visit_update.to_dict() @@ -76,7 +72,7 @@ def test_todict_origin_visit_updates(origin_visit_update): # Timestamp -@given(timestamps()) +@given(strategies.timestamps()) def test_timestamps_strategy(timestamp): attr.validate(timestamp) @@ -326,6 +322,8 @@ def test_git_author_line_to_author(): assert expected_person == Person.from_fullname(person) +# Content + def test_content_get_hash(): hashes = dict( sha1=b'foo', sha1_git=b'bar', sha256=b'baz', blake2s256=b'qux') @@ -356,6 +354,33 @@ def test_content_data_missing(): c.with_data() +@given(strategies.present_contents_d()) +def test_content_from_dict(content_d): + c = Content.from_data(**content_d) + assert c + assert c.ctime == content_d['ctime'] + + content_d2 = c.to_dict() + c2 = Content.from_dict(content_d2) + assert c2.ctime == c.ctime + + +def test_content_from_dict_str_ctime(): + # test with ctime as a string + n = datetime.datetime(2020, 5, 6, 12, 34) + content_d = { + 'ctime': n.isoformat(), + 'data': b'', + 'length': 0, + 'sha1': b'\x00', + 'sha256': b'\x00', + 'sha1_git': b'\x00', + 'blake2s256': b'\x00', + } + c = Content.from_dict(content_d) + assert c.ctime == n + + @given(binary(max_size=4096)) def test_content_from_data(data): c = Content.from_data(data) @@ -376,6 +401,8 @@ def test_hidden_content_from_data(data): assert getattr(c, key) == value +# SkippedContent + @given(binary(max_size=4096)) def test_skipped_content_from_data(data): c = SkippedContent.from_data(data, reason='reason') @@ -386,7 +413,7 @@ def test_skipped_content_from_data(data): assert getattr(c, key) == value -@given(skipped_contents_d()) +@given(strategies.skipped_contents_d()) def test_skipped_content_origin_is_str(skipped_content_d): assert SkippedContent.from_dict(skipped_content_d)