1010import tempfile
1111from typing import Any
1212from unittest import mock
13+ import warnings
1314
1415from deepdiff import DeepDiff
1516from deepdiff .model import DiffLevel
@@ -184,6 +185,42 @@ def _assert_allotrope_dicts_equal(
184185 raise AssertionError (msg )
185186
186187
188+ def _check_allotrope_dicts_for_deletions (
189+ expected : DictType ,
190+ actual : DictType ,
191+ ) -> tuple [bool , list [str ]]:
192+ """
193+ Check if the actual dict has any deleted fields compared to expected dict.
194+
195+ Returns:
196+ tuple[bool, list[str]]: (has_deletions, deleted_paths)
197+ - has_deletions: True if any fields were deleted
198+ - deleted_paths: List of JSON paths for deleted fields
199+ """
200+ expected_replaced = _replace_asm_converter_version (expected )
201+ ddiff = DeepDiff (
202+ expected_replaced ,
203+ actual ,
204+ ignore_type_in_groups = [(float , np .float64 )],
205+ ignore_nan_inequality = True ,
206+ custom_operators = [DEEPDIFF_PATH_COMPARATOR ],
207+ )
208+
209+ deleted_paths = []
210+ if ddiff :
211+ # Check for dictionary item removals
212+ if "dictionary_item_removed" in ddiff :
213+ for path in ddiff ["dictionary_item_removed" ]:
214+ deleted_paths .append (str (path ))
215+
216+ # Check for iterable item removals (list items)
217+ if "iterable_item_removed" in ddiff :
218+ for path in ddiff ["iterable_item_removed" ]:
219+ deleted_paths .append (str (path ))
220+
221+ return len (deleted_paths ) > 0 , deleted_paths
222+
223+
187224class TestIdGenerator :
188225 next_id : int
189226 prefix : str | None
@@ -251,6 +288,7 @@ def validate_contents(
251288 allotrope_dict : DictType ,
252289 expected_file : Path | str ,
253290 write_actual_to_expected_on_fail : bool = False , # noqa: FBT001, FBT002
291+ force_overwrite : bool = False , # noqa: FBT001, FBT002
254292) -> None :
255293 """Use the newly created allotrope_dict to validate the contents inside expected_file."""
256294 # Ensure that allotrope_dict can be written via json.dump()
@@ -260,16 +298,67 @@ def validate_contents(
260298 try :
261299 with open (expected_file , encoding = DEFAULT_ENCODING ) as f :
262300 expected_dict = json .load (f )
263- _assert_allotrope_dicts_equal ( expected_dict , allotrope_dict )
264- except Exception as e :
301+
302+ # Check for deletions before doing the assertion if we're going to overwrite
265303 if write_actual_to_expected_on_fail :
304+ has_deletions , deleted_paths = _check_allotrope_dicts_for_deletions (
305+ expected_dict , allotrope_dict
306+ )
307+
308+ if has_deletions and not force_overwrite :
309+ # Show a summary of deleted fields instead of the full list
310+ if len (deleted_paths ) <= 10 :
311+ # Show all paths if there are few
312+ deleted_paths_str = "\n " .join (
313+ f" - { path } " for path in deleted_paths
314+ )
315+ summary_msg = f"Fields that will be deleted:\n { deleted_paths_str } "
316+ else :
317+ # Show first few and summary for many deletions
318+ first_few = "\n " .join (f" - { path } " for path in deleted_paths [:5 ])
319+ summary_msg = (
320+ f"Fields that will be deleted (showing first 5 of { len (deleted_paths )} ):\n "
321+ f"{ first_few } \n "
322+ f" ... and { len (deleted_paths ) - 5 } more fields"
323+ )
324+
325+ error_msg = (
326+ f"DELETION DETECTED: Cannot overwrite test data for '{ expected_file } '\n "
327+ f"Total fields to be deleted: { len (deleted_paths )} \n \n "
328+ f"{ summary_msg } \n \n "
329+ f"To proceed anyway, use the --force-overwrite flag.\n \n "
330+ f"If you use --force-overwrite, the full list will be provided for your PR description."
331+ )
332+ raise AssertionError (error_msg )
333+ elif has_deletions and force_overwrite :
334+ # Issue warning about deletions for PR description
335+ deleted_paths_str = "\n " .join (f" - { path } " for path in deleted_paths )
336+ warning_msg = (
337+ f"Fields will be deleted from '{ expected_file } ':\n \n "
338+ f"Please copy the following information into your PR description:\n \n "
339+ f"**Fields deleted in test data:**\n "
340+ f"{ deleted_paths_str } "
341+ )
342+ warnings .warn (warning_msg , UserWarning , stacklevel = 2 )
343+
344+ # Only do the assertion if we're not force overwriting
345+ if not (write_actual_to_expected_on_fail and force_overwrite ):
346+ _assert_allotrope_dicts_equal (expected_dict , allotrope_dict )
347+ elif write_actual_to_expected_on_fail and force_overwrite :
348+ # Force overwrite - write the file and continue
266349 _write_actual_to_expected (allotrope_dict , expected_file )
350+
351+ except Exception as e :
352+ if write_actual_to_expected_on_fail :
267353 if isinstance (e , FileNotFoundError ):
354+ # File doesn't exist, safe to create
355+ _write_actual_to_expected (allotrope_dict , expected_file )
268356 msg = f"Missing expected output file '{ expected_file } ', writing expected output because 'write_actual_to_expected_on_fail=True'"
269357 raise AssertionError (msg ) from e
270- if isinstance (e , AssertionError ) and "allotropy output != expected:" in str (
271- e
272- ):
358+ elif isinstance (
359+ e , AssertionError
360+ ) and "allotropy output != expected:" in str (e ):
361+ _write_actual_to_expected (allotrope_dict , expected_file )
273362 msg = f"Mismatch between actual and expected for '{ expected_file } ', writing expected output because 'write_actual_to_expected_on_fail=True'\n \n { e } "
274363 raise AssertionError (msg ) from e
275364 raise
0 commit comments