|
8 | 8 |
|
9 | 9 | import json |
10 | 10 | import os |
| 11 | +import subprocess |
| 12 | +import sys |
11 | 13 | import time |
12 | 14 | from typing import Set |
13 | 15 |
|
@@ -181,3 +183,95 @@ def test_remove_aux_process_timeout(self, impl: str, env: dict) -> None: |
181 | 183 | os.path.exists(mount2), |
182 | 184 | "second mount point should be removed", |
183 | 185 | ) |
| 186 | + |
| 187 | + @parameterized.expand( |
| 188 | + [ |
| 189 | + ("rust", {"EDENFSCTL_ONLY_RUST": "1"}), |
| 190 | + ("python", {"EDENFSCTL_SKIP_RUST": "1"}), |
| 191 | + ] |
| 192 | + ) |
| 193 | + def test_remove_with_busy_bind_mount(self, impl: str, env: dict) -> None: |
| 194 | + """Test eden rm behavior when a bind mount is actively in use. |
| 195 | +
|
| 196 | + Creates a bind redirection and holds an open file handle on a file in |
| 197 | + the bind mount. This simulates an aux process (like buck) actively |
| 198 | + using the redirection. |
| 199 | +
|
| 200 | + This test only runs on macOS because unmount behavior differs by platform: |
| 201 | + - macOS: uses MNT_FORCE for unmount, which can hang if the mount is busy |
| 202 | + - Linux: uses MNT_DETACH (lazy unmount), always succeeds immediately |
| 203 | + - Windows: uses symlinks instead of bind mounts, unlink() is instant |
| 204 | +
|
| 205 | + The test verifies that eden rm with --timeout completes without hanging |
| 206 | + indefinitely, even when the bind mount is in active use. |
| 207 | + """ |
| 208 | + if sys.platform != "darwin": |
| 209 | + self.skipTest( |
| 210 | + "Busy bind mount test is macOS-only (Linux/Windows unmounts don't hang)" |
| 211 | + ) |
| 212 | + |
| 213 | + # Setup: add a bind redirection |
| 214 | + repo_path = f"busy-{impl}" |
| 215 | + self.eden.run_cmd("redirect", "add", "--mount", self.mount, repo_path, "bind") |
| 216 | + |
| 217 | + # Get the mount point path (inside the checkout) |
| 218 | + mount_point = os.path.join(self.mount, repo_path) |
| 219 | + self.assertTrue( |
| 220 | + os.path.isdir(mount_point), f"Mount point should exist: {mount_point}" |
| 221 | + ) |
| 222 | + |
| 223 | + # Keep the mount busy by holding an open file handle. |
| 224 | + test_file = os.path.join(mount_point, "busy_file") |
| 225 | + with open(test_file, "w") as f: |
| 226 | + f.write("x") |
| 227 | + busy_proc = subprocess.Popen( |
| 228 | + ["tail", "-f", test_file], |
| 229 | + stdout=subprocess.DEVNULL, |
| 230 | + stderr=subprocess.DEVNULL, |
| 231 | + ) |
| 232 | + |
| 233 | + try: |
| 234 | + # Give the process a moment to start and open the file |
| 235 | + time.sleep(0.5) |
| 236 | + |
| 237 | + # Verify the process is running and has the file open |
| 238 | + self.assertIsNone(busy_proc.poll(), "busy process should still be running") |
| 239 | + |
| 240 | + # Run eden rm with a timeout |
| 241 | + start_time = time.time() |
| 242 | + result = self.eden.run_unchecked( |
| 243 | + "remove", |
| 244 | + "--yes", |
| 245 | + "--timeout", |
| 246 | + "1", |
| 247 | + self.mount, |
| 248 | + env=env, |
| 249 | + capture_output=True, |
| 250 | + text=True, |
| 251 | + ) |
| 252 | + elapsed_time = time.time() - start_time |
| 253 | + |
| 254 | + # The operation should complete (not hang indefinitely) |
| 255 | + # On macOS, MNT_FORCE could hang on busy mounts, but --timeout should prevent that (timeout + overhead = ~2s) |
| 256 | + self.assertLess( |
| 257 | + elapsed_time, |
| 258 | + 2.0, |
| 259 | + f"eden rm ({impl}) should not hang indefinitely with busy bind mount", |
| 260 | + ) |
| 261 | + |
| 262 | + # Log the output for debugging |
| 263 | + output = result.stderr or "" |
| 264 | + |
| 265 | + # Verify the mount was removed despite the busy bind mount |
| 266 | + self.assertFalse( |
| 267 | + os.path.exists(self.mount), |
| 268 | + f"mount point should be removed after eden rm ({impl}) with busy bind mount. Output: {output}", |
| 269 | + ) |
| 270 | + finally: |
| 271 | + # Clean up the busy process |
| 272 | + busy_proc.terminate() |
| 273 | + try: |
| 274 | + busy_proc.wait(timeout=5) |
| 275 | + except subprocess.TimeoutExpired: |
| 276 | + busy_proc.kill() |
| 277 | + busy_proc.wait() |
0 commit comments