From d492d748f39aaed2ac82c7025667597afcd30d48 Mon Sep 17 00:00:00 2001 From: NIK-TIGER-BILL Date: Fri, 22 May 2026 23:10:25 +0000 Subject: [PATCH 1/2] fix(testing): correct delete_dir prefix matching in stateful tests Fix flaky stateful test bookkeeping when delete_dir matches string prefixes instead of true directory descendants. A path such as 6/faNT... could be incorrectly removed when deleting 6/f. Closes #3977 Signed-off-by: NIK-TIGER-BILL --- changes/3977.bugfix.md | 1 + src/zarr/testing/stateful.py | 2 +- tests/test_store/test_stateful.py | 22 ++++++++++++++++++++++ 3 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 changes/3977.bugfix.md 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 From dcf6b16db675f7d3d93f70fe317670cc1204173b Mon Sep 17 00:00:00 2001 From: NIK-TIGER-BILL Date: Mon, 25 May 2026 03:04:58 +0000 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20address=20review=20comment=20?= =?UTF-8?q?=E2=80=94=20remove=20standalone=20regression=20test=20per=20mai?= =?UTF-8?q?ntainer=20feedback?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: NIK-TIGER-BILL --- tests/test_store/test_stateful.py | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/tests/test_store/test_stateful.py b/tests/test_store/test_stateful.py index 64deb50e97..3232ef5120 100644 --- a/tests/test_store/test_stateful.py +++ b/tests/test_store/test_stateful.py @@ -51,23 +51,3 @@ def mk_test_instance_sync() -> ZarrStoreStateMachine: 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