Skip to content

Commit e693c47

Browse files
committed
Ensure that unchanged logs are still generated on request even if there are no changes in the entire diff
1 parent 4d86874 commit e693c47

File tree

3 files changed

+71
-30
lines changed

3 files changed

+71
-30
lines changed

diffsync/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -484,6 +484,9 @@ def sync_complete(
484484
):
485485
"""Callback triggered after a `sync_from` operation has completed and updated the model data of this instance.
486486
487+
Note that this callback is **only** triggered if the sync actually resulted in data changes. If there are no
488+
detected changes, this callback will **not** be called.
489+
487490
The default implementation does nothing, but a subclass could use this, for example, to perform bulk updates
488491
to a backend (such as a file) that doesn't readily support incremental updates to individual records.
489492

diffsync/workers.py

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -240,21 +240,25 @@ def __init__(
240240
self.action: Optional[str] = None
241241

242242
def perform_sync(self) -> bool:
243-
"""Perform data synchronization based on the provided diff."""
244-
if not self.diff.has_diffs():
245-
self.base_logger.info("No changes to synchronize")
246-
return False
243+
"""Perform data synchronization based on the provided diff.
247244
245+
Returns:
246+
bool: True if any changes were actually performed, else False.
247+
"""
248+
changed = False
248249
self.base_logger.info("Beginning sync")
249250
for element in self.diff.get_children():
250-
self.sync_diff_element(element)
251+
changed |= self.sync_diff_element(element)
251252
self.base_logger.info("Sync complete")
252-
return True
253+
return changed
253254

254-
def sync_diff_element(self, element: DiffElement, parent_model: "DiffSyncModel" = None):
255+
def sync_diff_element(self, element: DiffElement, parent_model: "DiffSyncModel" = None) -> bool:
255256
"""Recursively synchronize the given DiffElement and its children, if any, into the dst_diffsync.
256257
257258
Helper method to `perform_sync`.
259+
260+
Returns:
261+
bool: True if this element or any of its children resulted in actual changes, else False.
258262
"""
259263
self.model_class = getattr(self.dst_diffsync, element.type)
260264
diffs = element.get_attrs_diffs()
@@ -276,12 +280,12 @@ def sync_diff_element(self, element: DiffElement, parent_model: "DiffSyncModel"
276280
except ObjectNotFound:
277281
model = None
278282

279-
modified_model = self.sync_model(model, ids, attrs)
283+
changed, modified_model = self.sync_model(model, ids, attrs)
280284
model = modified_model or model
281285

282286
if not modified_model or not model:
283287
self.logger.warning("No object resulted from sync, will not process child objects.")
284-
return
288+
return changed
285289

286290
if self.action == "create":
287291
if parent_model:
@@ -293,22 +297,29 @@ def sync_diff_element(self, element: DiffElement, parent_model: "DiffSyncModel"
293297
if model.model_flags & DiffSyncModelFlags.SKIP_CHILDREN_ON_DELETE:
294298
# We don't need to process the child objects, but we do need to discard them from the dst_diffsync
295299
self.dst_diffsync.remove(model, remove_children=True)
296-
return
300+
return changed
297301
self.dst_diffsync.remove(model)
298302

299303
for child in element.get_children():
300-
self.sync_diff_element(child, parent_model=model)
304+
changed |= self.sync_diff_element(child, parent_model=model)
301305

302-
def sync_model(self, model: Optional["DiffSyncModel"], ids: Mapping, attrs: Mapping) -> Optional["DiffSyncModel"]:
306+
return changed
307+
308+
def sync_model(
309+
self, model: Optional["DiffSyncModel"], ids: Mapping, attrs: Mapping
310+
) -> Tuple[bool, Optional["DiffSyncModel"]]:
303311
"""Create/update/delete the current DiffSyncModel with current ids/attrs, and update self.status and self.message.
304312
305313
Helper method to `sync_diff_element`.
314+
315+
Returns:
316+
tuple: (changed, model) where model may be None if an error occurred
306317
"""
307318
if self.action is None:
308319
status = DiffSyncStatus.SUCCESS
309320
message = "No changes to apply; no action needed"
310321
self.log_sync_status(self.action, status, message)
311-
return model
322+
return (False, model)
312323

313324
try:
314325
self.logger.debug(f"Attempting model {self.action}")
@@ -338,12 +349,12 @@ def sync_model(self, model: Optional["DiffSyncModel"], ids: Mapping, attrs: Mapp
338349
message = str(exception)
339350
self.log_sync_status(self.action, status, message)
340351
if self.flags & DiffSyncFlags.CONTINUE_ON_FAILURE:
341-
return None
352+
return (True, None)
342353
raise
343354

344355
self.log_sync_status(self.action, status, message)
345356

346-
return model
357+
return (True, model)
347358

348359
def log_sync_status(self, action: Optional[str], status: DiffSyncStatus, message: str):
349360
"""Log the current sync status at the appropriate verbosity with appropriate context.

tests/unit/test_diffsync.py

Lines changed: 42 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,23 @@ def test_diffsync_sync_from(backend_a, backend_b):
368368
backend_a.get_by_uids(["nyc", "sfo"], "device")
369369

370370

371+
def check_successful_sync_log_sanity(log, src, dst, flags):
372+
"""Given a successful sync, make sure the captured structlogs are correct at a high level."""
373+
# All logs generated during the sync should include the src, dst, and flags data
374+
for event in log.events:
375+
assert "src" in event and event["src"] == src
376+
assert "dst" in event and event["dst"] == dst
377+
assert "flags" in event and event["flags"] == flags
378+
379+
# No warnings or errors should have been logged in a fully successful sync
380+
assert all(event["level"] == "debug" or event["level"] == "info" for event in log.events)
381+
382+
# Logs for beginning and end of diff and sync should have been generated
383+
assert log.has("Beginning diff calculation", level="info")
384+
assert log.has("Diff calculation complete", level="info")
385+
assert log.has("Beginning sync", level="info")
386+
387+
371388
def check_sync_logs_against_diff(diffsync, diff, log, errors_permitted=False):
372389
"""Given a Diff, make sure the captured structlogs correctly correspond to its contents/actions."""
373390
for element in diff.get_children():
@@ -441,29 +458,39 @@ def check_sync_logs_against_diff(diffsync, diff, log, errors_permitted=False):
441458
check_sync_logs_against_diff(diffsync, element.child_diff, log, errors_permitted)
442459

443460

444-
def test_diffsync_sync_from_successful_logging(log, backend_a, backend_b):
445-
diff = backend_a.diff_from(backend_b)
461+
def test_diffsync_no_log_unchanged_by_default(log, backend_a):
462+
backend_a.sync_from(backend_a)
463+
464+
# Make sure logs were accurately generated
465+
check_successful_sync_log_sanity(log, backend_a, backend_a, DiffSyncFlags.NONE)
466+
467+
# Since there were no changes, and we didn't set LOG_UNCHANGED_RECORDS, there should be no "unchanged" logs
468+
assert not any(event for event in log.events if "action" in event)
469+
assert not any(event for event in log.events if "status" in event)
470+
471+
472+
def test_diffsync_log_unchanged_even_if_no_changes_overall(log, backend_a):
473+
diff = backend_a.diff_from(backend_a)
474+
assert not diff.has_diffs()
446475
# Discard logs generated during diff calculation
447476
log.events = []
448477

449-
backend_a.sync_from(backend_b, flags=DiffSyncFlags.LOG_UNCHANGED_RECORDS)
478+
backend_a.sync_from(backend_a, flags=DiffSyncFlags.LOG_UNCHANGED_RECORDS)
450479

451-
# All logs generated during the sync should include the src, dst, and flags data
452-
for event in log.events:
453-
assert "src" in event and event["src"] == backend_b
454-
assert "dst" in event and event["dst"] == backend_a
455-
assert "flags" in event and event["flags"] == DiffSyncFlags.LOG_UNCHANGED_RECORDS
480+
# Make sure logs were accurately generated
481+
check_successful_sync_log_sanity(log, backend_a, backend_a, DiffSyncFlags.LOG_UNCHANGED_RECORDS)
482+
check_sync_logs_against_diff(backend_a, diff, log)
456483

457-
# No warnings or errors should have been logged in a fully successful sync
458-
assert all(event["level"] == "debug" or event["level"] == "info" for event in log.events)
459484

460-
# Logs for beginning and end of diff and sync should have been generated
461-
assert log.has("Beginning diff calculation", level="info")
462-
assert log.has("Diff calculation complete", level="info")
463-
assert log.has("Beginning sync", level="info")
464-
assert log.has("Sync complete", level="info")
485+
def test_diffsync_sync_from_successful_logging(log, backend_a, backend_b):
486+
diff = backend_a.diff_from(backend_b)
487+
# Discard logs generated during diff calculation
488+
log.events = []
489+
490+
backend_a.sync_from(backend_b, flags=DiffSyncFlags.LOG_UNCHANGED_RECORDS)
465491

466492
# Make sure logs were accurately generated
493+
check_successful_sync_log_sanity(log, backend_b, backend_a, DiffSyncFlags.LOG_UNCHANGED_RECORDS)
467494
check_sync_logs_against_diff(backend_a, diff, log)
468495

469496

0 commit comments

Comments
 (0)