This is an automated email from the ASF dual-hosted git repository.

tqchen pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tvm-ffi.git


The following commit(s) were added to refs/heads/main by this push:
     new 021d78d  [Test] Add test for filelock utility (#216)
021d78d is described below

commit 021d78dbb8c5c5ba9ea07f8035eb9ce01a48bbc7
Author: Yaoyao Ding <[email protected]>
AuthorDate: Tue Nov 4 11:42:34 2025 -0500

    [Test] Add test for filelock utility (#216)
    
    This PR adds tests for the filelock utility, to make sure it works well
    on multiple platforms.
---
 python/tvm_ffi/utils/lockfile.py      |  41 +++++++++--
 tests/python/utils/filelock_worker.py |  49 +++++++++++++
 tests/python/utils/test_filelock.py   | 126 ++++++++++++++++++++++++++++++++++
 3 files changed, 210 insertions(+), 6 deletions(-)

diff --git a/python/tvm_ffi/utils/lockfile.py b/python/tvm_ffi/utils/lockfile.py
index da98491..5fa69d3 100644
--- a/python/tvm_ffi/utils/lockfile.py
+++ b/python/tvm_ffi/utils/lockfile.py
@@ -34,7 +34,9 @@ class FileLock:
     """Provide a cross-platform file locking mechanism using Python's stdlib.
 
     This class implements an advisory lock, which must be respected by all
-    cooperating processes.
+    cooperating processes. Please note that this lock does not prevent the 
same process
+    from acquiring the lock multiple times; it is the caller's responsibility 
to
+    manage this.
 
     Examples
     --------
@@ -71,9 +73,20 @@ class FileLock:
     def acquire(self) -> bool:
         """Acquire an exclusive, non-blocking lock on the file.
 
-        Returns True if the lock was acquired, False otherwise.
+        Returns
+        -------
+        ret: bool
+            True if the lock was acquired, False otherwise.
+
+        Raises
+        ------
+        RuntimeError
+            If an unexpected error occurs during lock acquisition.
+
         """
         try:
+            if self._file_descriptor is not None:
+                return False  # Lock is already held by this instance
             if sys.platform == "win32":
                 self._file_descriptor = os.open(
                     self.lock_file_path, os.O_RDWR | os.O_CREAT | os.O_BINARY
@@ -97,12 +110,28 @@ class FileLock:
     def blocking_acquire(self, timeout: float | None = None, poll_interval: 
float = 0.1) -> bool:
         """Wait until an exclusive lock can be acquired, with an optional 
timeout.
 
-        Args:
-            timeout (float): The maximum time to wait for the lock in seconds.
-                             A value of None means wait indefinitely.
-            poll_interval (float): The time to wait between lock attempts in 
seconds.
+        Parameters
+        ----------
+        timeout: float, optional
+            The maximum time to wait for the lock in seconds.  A value of None 
means wait indefinitely.
+        poll_interval: float
+            The time to wait between lock attempts in seconds.
+
+        Returns
+        -------
+        ret: bool
+            True if the lock was acquired.
+
+        Raises
+        ------
+        TimeoutError
+            If the lock is not acquired within the timeout period.
+        RuntimeError
+            If the lock is already held by this instance.
 
         """
+        if self._file_descriptor is not None:
+            raise RuntimeError("Lock is already held by this instance.")
         start_time = time.time()
         while True:
             if self.acquire():
diff --git a/tests/python/utils/filelock_worker.py 
b/tests/python/utils/filelock_worker.py
new file mode 100644
index 0000000..46ff29b
--- /dev/null
+++ b/tests/python/utils/filelock_worker.py
@@ -0,0 +1,49 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import random
+import sys
+import time
+
+from tvm_ffi.utils.lockfile import FileLock
+
+
+def worker(worker_id: int, lock_path: str, counter_file: str) -> int:
+    """Worker function that tries to acquire lock and increment counter."""
+    try:
+        with FileLock(lock_path):
+            # Critical section - read, increment, write counter
+            with open(counter_file) as f:  # noqa: PTH123
+                current_value = int(f.read().strip())
+
+            time.sleep(random.uniform(0.01, 0.1))  # Simulate some work
+
+            with open(counter_file, "w") as f:  # noqa: PTH123
+                f.write(str(current_value + 1))
+
+            print(f"Worker {worker_id}: success")
+            return 0
+    except Exception as e:
+        print(f"Worker {worker_id}: error: {e}")
+        return 1
+
+
+if __name__ == "__main__":
+    worker_id = int(sys.argv[1])
+    lock_path = sys.argv[2]
+    counter_file = sys.argv[3]
+    sys.exit(worker(worker_id, lock_path, counter_file))
diff --git a/tests/python/utils/test_filelock.py 
b/tests/python/utils/test_filelock.py
new file mode 100644
index 0000000..fa68edf
--- /dev/null
+++ b/tests/python/utils/test_filelock.py
@@ -0,0 +1,126 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+"""Tests for the FileLock utility."""
+
+import subprocess
+import sys
+import tempfile
+from pathlib import Path
+
+import pytest
+from tvm_ffi.utils.lockfile import FileLock
+
+
+def test_basic_lock_acquire_and_release() -> None:
+    """Test basic lock acquisition and release."""
+    with tempfile.TemporaryDirectory() as temp_dir:
+        lock_path = Path(temp_dir) / "test.lock"
+        lock = FileLock(str(lock_path))
+
+        # Test acquire
+        assert lock.acquire() is True
+        assert lock._file_descriptor is not None
+
+        # Test release
+        lock.release()
+        assert lock._file_descriptor is None
+
+
+def test_context_manager() -> None:
+    """Test FileLock as a context manager."""
+    with tempfile.TemporaryDirectory() as temp_dir:
+        lock_path = Path(temp_dir) / "test.lock"
+
+        with FileLock(str(lock_path)) as lock:
+            assert lock._file_descriptor is not None
+            # Lock should be acquired
+            assert lock_path.exists()
+
+        # Lock should be released after exiting context
+        # Note: file may still exist but should be unlocked
+
+
+def test_multiple_acquire_attempts() -> None:
+    """Test multiple acquire attempts on the same lock instance."""
+    with tempfile.TemporaryDirectory() as temp_dir:
+        lock_path = Path(temp_dir) / "test.lock"
+        lock = FileLock(str(lock_path))
+
+        # First acquire should succeed
+        assert lock.acquire() is True
+
+        # Second acquire on same instance should fail
+        # (can't acquire same lock twice)
+        assert lock.acquire() is False
+
+        lock.release()
+
+
+def test_exception_in_context_manager() -> None:
+    """Test that lock is properly released even when exception occurs."""
+    with tempfile.TemporaryDirectory() as temp_dir:
+        lock_path = Path(temp_dir) / "test.lock"
+
+        # Test that exception is propagated and lock is released
+        with pytest.raises(ValueError, match="test exception"):
+            with FileLock(str(lock_path)) as lock:
+                assert lock._file_descriptor is not None
+                raise ValueError("test exception")
+
+        # Lock should be released, so we can acquire it again
+        lock2 = FileLock(str(lock_path))
+        assert lock2.acquire() is True
+        lock2.release()
+
+
+def test_concurrent_access() -> None:
+    """Test concurrent access from multiple processes."""
+    with tempfile.TemporaryDirectory() as temp_dir:
+        lock_path = Path(temp_dir) / "test.lock"
+        counter_file = Path(temp_dir) / "counter.txt"
+
+        # Initialize counter file
+        counter_file.write_text("0")
+
+        # Create worker script content
+
+        # Write worker script to a temporary file
+        worker_script_path = Path(__file__).parent / "filelock_worker.py"
+
+        # Run multiple worker processes concurrently
+        num_workers = 16
+        processes = []
+        for i in range(num_workers):
+            p = subprocess.Popen(
+                [sys.executable, str(worker_script_path), str(i), 
str(lock_path), str(counter_file)]
+            )
+            processes.append(p)
+
+        # Wait for all processes to complete
+        for p in processes:
+            p.wait(timeout=60.0)
+            assert p.returncode == 0, f"Worker process failed with return code 
{p.returncode}"
+
+        # Check final counter value
+        final_count = int(counter_file.read_text().strip())
+
+        # Counter should equal number of workers (no race conditions)
+        assert final_count == num_workers
+
+
+if __name__ == "__main__":
+    pytest.main([__file__])

Reply via email to