From 6f07402407928c2da883cd13acbb3fc8bd67aa89 Mon Sep 17 00:00:00 2001
From: Dan Helfman <witten@torsion.org>
Date: Sun, 30 Mar 2025 19:04:36 -0700
Subject: [PATCH] Fix end-to-end tests and don't stat() directories that don't
 exist (#1048).

---
 borgmatic/hooks/data_source/snapshot.py       |  2 +-
 tests/unit/hooks/data_source/test_snapshot.py | 24 +++++++++++++++++++
 2 files changed, 25 insertions(+), 1 deletion(-)

diff --git a/borgmatic/hooks/data_source/snapshot.py b/borgmatic/hooks/data_source/snapshot.py
index 4b67784f..a89b85df 100644
--- a/borgmatic/hooks/data_source/snapshot.py
+++ b/borgmatic/hooks/data_source/snapshot.py
@@ -32,7 +32,7 @@ def get_contained_patterns(parent_directory, candidate_patterns):
     if not candidate_patterns:
         return ()
 
-    parent_device = os.stat(parent_directory).st_dev
+    parent_device = os.stat(parent_directory).st_dev if os.path.exists(parent_directory) else None
 
     contained_patterns = tuple(
         candidate
diff --git a/tests/unit/hooks/data_source/test_snapshot.py b/tests/unit/hooks/data_source/test_snapshot.py
index 889a5642..8cb98ded 100644
--- a/tests/unit/hooks/data_source/test_snapshot.py
+++ b/tests/unit/hooks/data_source/test_snapshot.py
@@ -6,6 +6,7 @@ from borgmatic.hooks.data_source import snapshot as module
 
 def test_get_contained_patterns_without_candidates_returns_empty():
     flexmock(module.os).should_receive('stat').and_return(flexmock(st_dev=flexmock()))
+    flexmock(module.os.path).should_receive('exists').and_return(True)
 
     assert module.get_contained_patterns('/mnt', {}) == ()
 
@@ -13,6 +14,7 @@ def test_get_contained_patterns_without_candidates_returns_empty():
 def test_get_contained_patterns_with_self_candidate_returns_self():
     device = flexmock()
     flexmock(module.os).should_receive('stat').and_return(flexmock(st_dev=device))
+    flexmock(module.os.path).should_receive('exists').and_return(True)
     candidates = {
         Pattern('/foo', device=device),
         Pattern('/mnt', device=device),
@@ -26,6 +28,7 @@ def test_get_contained_patterns_with_self_candidate_returns_self():
 def test_get_contained_patterns_with_self_candidate_and_caret_prefix_returns_self():
     device = flexmock()
     flexmock(module.os).should_receive('stat').and_return(flexmock(st_dev=device))
+    flexmock(module.os.path).should_receive('exists').and_return(True)
     candidates = {
         Pattern('^/foo', device=device),
         Pattern('^/mnt', device=device),
@@ -39,6 +42,7 @@ def test_get_contained_patterns_with_self_candidate_and_caret_prefix_returns_sel
 def test_get_contained_patterns_with_child_candidate_returns_child():
     device = flexmock()
     flexmock(module.os).should_receive('stat').and_return(flexmock(st_dev=device))
+    flexmock(module.os.path).should_receive('exists').and_return(True)
     candidates = {
         Pattern('/foo', device=device),
         Pattern('/mnt/subdir', device=device),
@@ -54,6 +58,7 @@ def test_get_contained_patterns_with_child_candidate_returns_child():
 def test_get_contained_patterns_with_grandchild_candidate_returns_child():
     device = flexmock()
     flexmock(module.os).should_receive('stat').and_return(flexmock(st_dev=device))
+    flexmock(module.os.path).should_receive('exists').and_return(True)
     candidates = {
         Pattern('/foo', device=device),
         Pattern('/mnt/sub/dir', device=device),
@@ -70,6 +75,7 @@ def test_get_contained_patterns_ignores_child_candidate_on_another_device():
     one_device = flexmock()
     another_device = flexmock()
     flexmock(module.os).should_receive('stat').and_return(flexmock(st_dev=one_device))
+    flexmock(module.os.path).should_receive('exists').and_return(True)
     candidates = {
         Pattern('/foo', device=one_device),
         Pattern('/mnt/subdir', device=another_device),
@@ -82,3 +88,21 @@ def test_get_contained_patterns_ignores_child_candidate_on_another_device():
         Pattern('/mnt/subdir', device=another_device),
         Pattern('/bar', device=one_device),
     }
+
+
+def test_get_contained_patterns_with_non_existent_parent_directory_ignores_child_candidate():
+    device = flexmock()
+    flexmock(module.os).should_receive('stat').and_return(flexmock(st_dev=device))
+    flexmock(module.os.path).should_receive('exists').and_return(False)
+    candidates = {
+        Pattern('/foo', device=device),
+        Pattern('/mnt/subdir', device=device),
+        Pattern('/bar', device=device),
+    }
+
+    assert module.get_contained_patterns('/mnt', candidates) == ()
+    assert candidates == {
+        Pattern('/foo', device=device),
+        Pattern('/mnt/subdir', device=device),
+        Pattern('/bar', device=device),
+    }