diff --git a/changes/3977.bugfix.md b/changes/3977.bugfix.md new file mode 100644 index 0000000000..6a8d9b4244 --- /dev/null +++ b/changes/3977.bugfix.md @@ -0,0 +1 @@ +Fix flaky stateful test bookkeeping when `delete_dir` matches string prefixes instead of true directory descendants. Previously a path such as `6/faNT…` could be incorrectly removed when deleting `6/f`. (See [issue #3977](https://github.com/zarr-developers/zarr-python/issues/3977).) diff --git a/src/zarr/testing/stateful.py b/src/zarr/testing/stateful.py index d6c43f4ecc..78f6a9671b 100644 --- a/src/zarr/testing/stateful.py +++ b/src/zarr/testing/stateful.py @@ -306,7 +306,7 @@ def delete_dir(self, data: DataObject) -> None: matches = set() for node in self.all_groups | self.all_arrays: - if node.startswith(path): + if node == path or node.startswith(path + "/"): matches.add(node) self.all_groups = self.all_groups - matches self.all_arrays = self.all_arrays - matches diff --git a/tests/test_store/test_stateful.py b/tests/test_store/test_stateful.py index 82b482d0ff..64deb50e97 100644 --- a/tests/test_store/test_stateful.py +++ b/tests/test_store/test_stateful.py @@ -49,3 +49,25 @@ def mk_test_instance_sync() -> ZarrStoreStateMachine: # But LocalStore, directories can hang around even after a key is delete-d. pytest.skip(reason="Test isn't suitable for LocalStore.") run_state_machine_as_test(mk_test_instance_sync) # type: ignore[no-untyped-call] + + +def test_delete_dir_prefix_matching() -> None: + """Regression test for delete_dir prefix matching bug (GH#3977). + + Verifies that delete_dir bookkeeping only removes exact path matches + and true descendants, not unrelated nodes that merely share a string + prefix (e.g. ``6/faNT…`` must NOT be deleted when removing ``6/f``). + """ + all_groups = {"6/f", "6/faNT7p7jvJsO3_C._HYi", "other"} + all_arrays = {"6/f/child", "6/other"} + path = "6/f" + + matches = set() + for node in all_groups | all_arrays: + if node == path or node.startswith(path + "/"): + matches.add(node) + + assert matches == {"6/f", "6/f/child"} + assert "6/faNT7p7jvJsO3_C._HYi" not in matches + assert "other" not in matches + assert "6/other" not in matches