Skip to content

Commit e28e5a9

Browse files
committed
Add test coverage
1 parent aa94735 commit e28e5a9

File tree

6 files changed

+108
-16
lines changed

6 files changed

+108
-16
lines changed

diffsync/__init__.py

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -460,18 +460,18 @@ def sync_from(
460460
source: "DiffSync",
461461
diff_class: Type[Diff] = Diff,
462462
flags: DiffSyncFlags = DiffSyncFlags.NONE,
463-
callback: Optional[Callable[[int, int], None]] = None,
463+
callback: Optional[Callable[[Text, int, int], None]] = None,
464464
):
465465
"""Synchronize data from the given source DiffSync object into the current DiffSync object.
466466
467467
Args:
468468
source (DiffSync): object to sync data from into this one
469469
diff_class (class): Diff or subclass thereof to use to calculate the diffs to use for synchronization
470470
flags (DiffSyncFlags): Flags influencing the behavior of this sync.
471-
callback (function): Function with parameters (current, total), to be called at intervals as the
472-
calculation of the diff proceeds.
471+
callback (function): Function with parameters (stage, current, total), to be called at intervals as the
472+
calculation of the diff and subsequent sync proceed.
473473
"""
474-
diff = self.diff_from(source, diff_class=diff_class, flags=flags)
474+
diff = self.diff_from(source, diff_class=diff_class, flags=flags, callback=callback)
475475
syncer = DiffSyncSyncer(diff=diff, src_diffsync=source, dst_diffsync=self, flags=flags, callback=callback)
476476
result = syncer.perform_sync()
477477
if result:
@@ -482,16 +482,16 @@ def sync_to(
482482
target: "DiffSync",
483483
diff_class: Type[Diff] = Diff,
484484
flags: DiffSyncFlags = DiffSyncFlags.NONE,
485-
callback: Optional[Callable[[int, int], None]] = None,
485+
callback: Optional[Callable[[Text, int, int], None]] = None,
486486
):
487487
"""Synchronize data from the current DiffSync object into the given target DiffSync object.
488488
489489
Args:
490490
target (DiffSync): object to sync data into from this one.
491491
diff_class (class): Diff or subclass thereof to use to calculate the diffs to use for synchronization
492492
flags (DiffSyncFlags): Flags influencing the behavior of this sync.
493-
callback (function): Function with parameters (current, total), to be called at intervals as the
494-
calculation of the diff proceeds.
493+
callback (function): Function with parameters (stage, current, total), to be called at intervals as the
494+
calculation of the diff and subsequent sync proceed.
495495
"""
496496
target.sync_from(self, diff_class=diff_class, flags=flags, callback=callback)
497497

@@ -526,15 +526,15 @@ def diff_from(
526526
source: "DiffSync",
527527
diff_class: Type[Diff] = Diff,
528528
flags: DiffSyncFlags = DiffSyncFlags.NONE,
529-
callback: Optional[Callable[[int, int], None]] = None,
529+
callback: Optional[Callable[[Text, int, int], None]] = None,
530530
) -> Diff:
531531
"""Generate a Diff describing the difference from the other DiffSync to this one.
532532
533533
Args:
534534
source (DiffSync): Object to diff against.
535535
diff_class (class): Diff or subclass thereof to use for diff calculation and storage.
536536
flags (DiffSyncFlags): Flags influencing the behavior of this diff operation.
537-
callback (function): Function with parameters (current, total), to be called at intervals as the
537+
callback (function): Function with parameters (stage, current, total), to be called at intervals as the
538538
calculation of the diff proceeds.
539539
"""
540540
differ = DiffSyncDiffer(
@@ -547,15 +547,15 @@ def diff_to(
547547
target: "DiffSync",
548548
diff_class: Type[Diff] = Diff,
549549
flags: DiffSyncFlags = DiffSyncFlags.NONE,
550-
callback: Optional[Callable[[int, int], None]] = None,
550+
callback: Optional[Callable[[Text, int, int], None]] = None,
551551
) -> Diff:
552552
"""Generate a Diff describing the difference from this DiffSync to another one.
553553
554554
Args:
555555
target (DiffSync): Object to diff against.
556556
diff_class (class): Diff or subclass thereof to use for diff calculation and storage.
557557
flags (DiffSyncFlags): Flags influencing the behavior of this diff operation.
558-
callback (function): Function with parameters (current, total), to be called at intervals as the
558+
callback (function): Function with parameters (stage, current, total), to be called at intervals as the
559559
calculation of the diff proceeds.
560560
"""
561561
return target.diff_from(self, diff_class=diff_class, flags=flags, callback=callback)

diffsync/helpers.py

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
from .diff import Diff, DiffElement
2323
from .enum import DiffSyncModelFlags, DiffSyncFlags, DiffSyncStatus
2424
from .exceptions import ObjectNotFound, ObjectNotCreated, ObjectNotUpdated, ObjectNotDeleted, ObjectCrudException
25-
from .utils import intersection
25+
from .utils import intersection, symmetric_difference
2626

2727
if TYPE_CHECKING: # pragma: no cover
2828
# For type annotation purposes, we have a circular import loop between __init__.py and this file.
@@ -41,7 +41,7 @@ def __init__( # pylint: disable=too-many-arguments
4141
dst_diffsync: "DiffSync",
4242
flags: DiffSyncFlags,
4343
diff_class: Type[Diff] = Diff,
44-
callback: Optional[Callable[[int, int], None]] = None,
44+
callback: Optional[Callable[[str, int, int], None]] = None,
4545
):
4646
"""Create a DiffSyncDiffer for calculating diffs between the provided DiffSync instances."""
4747
self.src_diffsync = src_diffsync
@@ -62,7 +62,7 @@ def incr_models_processed(self, delta: int = 1):
6262
if delta:
6363
self.models_processed += delta
6464
if self.callback:
65-
self.callback(self.models_processed, self.total_models)
65+
self.callback("diff", self.models_processed, self.total_models)
6666

6767
def calculate_diffs(self) -> Diff:
6868
"""Calculate diffs between the src and dst DiffSync objects and return the resulting Diff."""
@@ -73,6 +73,16 @@ def calculate_diffs(self) -> Diff:
7373

7474
self.logger.info("Beginning diff calculation")
7575
self.diff = self.diff_class()
76+
77+
skipped_types = symmetric_difference(self.dst_diffsync.top_level, self.src_diffsync.top_level)
78+
# This won't count everything, since these top-level types may have child types which are
79+
# implicitly also skipped as well, but we don't want to waste too much time on this calculation.
80+
for skipped_type in skipped_types:
81+
if skipped_type in self.dst_diffsync.top_level:
82+
self.incr_models_processed(len(self.dst_diffsync.get_all(skipped_type)))
83+
elif skipped_type in self.src_diffsync.top_level:
84+
self.incr_models_processed(len(self.src_diffsync.get_all(skipped_type)))
85+
7686
for obj_type in intersection(self.dst_diffsync.top_level, self.src_diffsync.top_level):
7787
diff_elements = self.diff_object_list(
7888
src=self.src_diffsync.get_all(obj_type), dst=self.dst_diffsync.get_all(obj_type),
@@ -170,15 +180,19 @@ def diff_object_pair(
170180
log = self.logger.bind(model=model, unique_id=unique_id)
171181
if self.flags & DiffSyncFlags.SKIP_UNMATCHED_SRC and not dst_obj:
172182
log.debug("Skipping unmatched source object")
183+
self.incr_models_processed()
173184
return None
174185
if self.flags & DiffSyncFlags.SKIP_UNMATCHED_DST and not src_obj:
175186
log.debug("Skipping unmatched dest object")
187+
self.incr_models_processed()
176188
return None
177189
if src_obj and src_obj.model_flags & DiffSyncModelFlags.IGNORE:
178190
log.debug("Skipping due to IGNORE flag on source object")
191+
self.incr_models_processed()
179192
return None
180193
if dst_obj and dst_obj.model_flags & DiffSyncModelFlags.IGNORE:
181194
log.debug("Skipping due to IGNORE flag on dest object")
195+
self.incr_models_processed()
182196
return None
183197

184198
diff_element = DiffElement(
@@ -224,6 +238,11 @@ def diff_child_objects(
224238
for child_type, child_fieldname in src_mapping.items():
225239
if child_type in dst_mapping:
226240
children_mapping[child_type] = child_fieldname
241+
else:
242+
self.incr_models_processed(len(getattr(src_obj, child_fieldname)))
243+
for child_type, child_fieldname in dst_mapping.items():
244+
if child_type not in src_mapping:
245+
self.incr_models_processed(len(getattr(dst_obj, child_fieldname)))
227246
elif src_obj:
228247
children_mapping = src_obj.get_children_mapping()
229248
elif dst_obj:
@@ -257,7 +276,7 @@ def __init__( # pylint: disable=too-many-arguments
257276
src_diffsync: "DiffSync",
258277
dst_diffsync: "DiffSync",
259278
flags: DiffSyncFlags,
260-
callback: Optional[Callable[[int, int], None]] = None,
279+
callback: Optional[Callable[[str, int, int], None]] = None,
261280
):
262281
"""Create a DiffSyncSyncer instance, ready to call `perform_sync()` against."""
263282
self.diff = diff
@@ -280,7 +299,7 @@ def incr_elements_processed(self, delta: int = 1):
280299
if delta:
281300
self.elements_processed += delta
282301
if self.callback:
283-
self.callback(self.elements_processed, self.total_elements)
302+
self.callback("sync", self.elements_processed, self.total_elements)
284303

285304
def perform_sync(self) -> bool:
286305
"""Perform data synchronization based on the provided diff.

diffsync/utils.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ def intersection(lst1, lst2) -> List:
2525
return lst3
2626

2727

28+
def symmetric_difference(lst1, lst2) -> List:
29+
"""Calculate the symmetric difference of two lists."""
30+
return sorted(set(lst1) ^ set(lst2))
31+
32+
2833
class OrderedDefaultDict(OrderedDict):
2934
"""A combination of collections.OrderedDict and collections.DefaultDict behavior."""
3035

tests/unit/test_diff.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,12 @@ def test_diff_dict_with_no_diffs():
4949
assert diff.dict() == {}
5050

5151

52+
def test_diff_len_with_no_diffs():
53+
diff = Diff()
54+
55+
assert len(diff) == 0
56+
57+
5258
def test_diff_children():
5359
"""Test the basic functionality of the Diff class when adding child elements."""
5460
diff = Diff()
@@ -117,6 +123,11 @@ def test_diff_dict_with_diffs(diff_with_children):
117123
}
118124

119125

126+
def test_diff_len_with_diffs(diff_with_children):
127+
assert len(diff_with_children) == 5
128+
assert len(diff_with_children) == sum(count for count in diff_with_children.summary().values())
129+
130+
120131
def test_order_children_default(backend_a, backend_b):
121132
"""Test that order_children_default is properly called when calling get_children."""
122133

tests/unit/test_diff_element.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,11 @@ def test_diff_element_dict_with_no_diffs():
5757
assert element.dict() == {}
5858

5959

60+
def test_diff_element_len_with_no_diffs():
61+
element = DiffElement("interface", "eth0", {"device_name": "device1", "name": "eth0"})
62+
assert len(element) == 1
63+
64+
6065
def test_diff_element_attrs():
6166
"""Test the basic functionality of the DiffElement class when setting and retrieving attrs."""
6267
element = DiffElement("interface", "eth0", {"device_name": "device1", "name": "eth0"})
@@ -111,6 +116,13 @@ def test_diff_element_dict_with_diffs():
111116
assert element.dict() == {"-": {"description": "your interface"}, "+": {"description": "my interface"}}
112117

113118

119+
def test_diff_element_len_with_diffs():
120+
element = DiffElement("interface", "eth0", {"device_name": "device1", "name": "eth0"})
121+
element.add_attrs(source={"interface_type": "ethernet", "description": "my interface"})
122+
element.add_attrs(dest={"description": "your interface"})
123+
assert len(element) == 1
124+
125+
114126
def test_diff_element_dict_with_diffs_no_attrs():
115127
element = DiffElement("interface", "eth0", {"device_name": "device1", "name": "eth0"})
116128
element.add_attrs(source={})
@@ -172,3 +184,8 @@ def test_diff_element_dict_with_child_diffs(diff_element_with_children):
172184
"lo1": {"-": {"description": "Loopback 1"}},
173185
},
174186
}
187+
188+
189+
def test_diff_element_len_with_child_diffs(diff_element_with_children):
190+
assert len(diff_element_with_children) == 5
191+
assert len(diff_element_with_children) == sum(count for count in diff_element_with_children.summary().values())

tests/unit/test_diffsync.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ def test_diffsync_str_with_no_data(generic_diffsync):
4343
assert generic_diffsync.str() == ""
4444

4545

46+
def test_diffsync_len_with_no_data(generic_diffsync):
47+
assert len(generic_diffsync) == 0
48+
49+
4650
def test_diffsync_diff_self_with_no_data_has_no_diffs(generic_diffsync):
4751
assert generic_diffsync.diff_from(generic_diffsync).has_diffs() is False
4852
assert generic_diffsync.diff_to(generic_diffsync).has_diffs() is False
@@ -285,6 +289,10 @@ def test_diffsync_str_with_data(backend_a):
285289
)
286290

287291

292+
def test_diffsync_len_with_data(backend_a):
293+
assert len(backend_a) == 23
294+
295+
288296
def test_diffsync_diff_self_with_data_has_no_diffs(backend_a):
289297
# Self diff should always show no diffs!
290298
assert backend_a.diff_from(backend_a).has_diffs() is False
@@ -328,6 +336,24 @@ def test_diffsync_diff_from_with_custom_diff_class(backend_a, backend_b):
328336
assert diff_ba.is_complete is True
329337

330338

339+
def test_diffsync_diff_with_callback(backend_a, backend_b):
340+
last_value = {"current": 0, "total": 0}
341+
342+
def callback(stage, current, total):
343+
assert stage == "diff"
344+
last_value["current"] = current
345+
last_value["total"] = total
346+
347+
expected = len(backend_a) + len(backend_b)
348+
349+
backend_a.diff_from(backend_b, callback=callback)
350+
assert last_value == {"current": expected, "total": expected}
351+
352+
last_value = {"current": 0, "total": 0}
353+
backend_a.diff_to(backend_b, callback=callback)
354+
assert last_value == {"current": expected, "total": expected}
355+
356+
331357
def test_diffsync_sync_from(backend_a, backend_b):
332358
backend_a.sync_complete = mock.Mock()
333359
backend_b.sync_complete = mock.Mock()
@@ -368,6 +394,20 @@ def test_diffsync_sync_from(backend_a, backend_b):
368394
backend_a.get_by_uids(["nyc", "sfo"], "device")
369395

370396

397+
def test_diffsync_sync_with_callback(backend_a, backend_b):
398+
last_value = {"current": 0, "total": 0}
399+
400+
def callback(stage, current, total):
401+
assert stage in ("diff", "sync")
402+
last_value["current"] = current
403+
last_value["total"] = total
404+
405+
expected = len(backend_a.diff_from(backend_b))
406+
407+
backend_a.sync_from(backend_b, callback=callback)
408+
assert last_value == {"current": expected, "total": expected}
409+
410+
371411
def check_successful_sync_log_sanity(log, src, dst, flags):
372412
"""Given a successful sync, make sure the captured structlogs are correct at a high level."""
373413
# All logs generated during the sync should include the src, dst, and flags data

0 commit comments

Comments
 (0)