Skip to content

Commit 881a3ee

Browse files
committed
Add a function to construct a mapping of paths to inodes, and tests
1 parent 5b8a282 commit 881a3ee

File tree

2 files changed

+76
-0
lines changed

2 files changed

+76
-0
lines changed

atr/util.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -785,6 +785,21 @@ async def paths_recursive_all(base_path: pathlib.Path) -> AsyncGenerator[pathlib
785785
queue.append(entry_abs_path)
786786

787787

788+
def paths_to_inodes(directory: pathlib.Path) -> dict[str, int]:
789+
result: dict[str, int] = {}
790+
stack: list[pathlib.Path] = [directory]
791+
while stack:
792+
current = stack.pop()
793+
with os.scandir(current) as entries:
794+
for entry in entries:
795+
if entry.is_file(follow_symlinks=False):
796+
rel_path = str(pathlib.Path(entry.path).relative_to(directory))
797+
result[rel_path] = entry.stat(follow_symlinks=False).st_ino
798+
elif entry.is_dir(follow_symlinks=False):
799+
stack.append(pathlib.Path(entry.path))
800+
return result
801+
802+
788803
def permitted_announce_recipients(asf_uid: str) -> list[str]:
789804
return [
790805
# f"dev@{committee.name}.apache.org",

tests/unit/test_stat_tree.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# Licensed to the Apache Software Foundation (ASF) under one
2+
# or more contributor license agreements. See the NOTICE file
3+
# distributed with this work for additional information
4+
# regarding copyright ownership. The ASF licenses this file
5+
# to you under the Apache License, Version 2.0 (the
6+
# "License"); you may not use this file except in compliance
7+
# with the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing,
12+
# software distributed under the License is distributed on an
13+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
# KIND, either express or implied. See the License for the
15+
# specific language governing permissions and limitations
16+
# under the License.
17+
18+
import os
19+
import pathlib
20+
21+
import atr.util as util
22+
23+
24+
def test_paths_to_inodes_empty_directory(tmp_path: pathlib.Path):
25+
result = util.paths_to_inodes(tmp_path)
26+
assert result == {}
27+
28+
29+
def test_paths_to_inodes_hard_links_share_inode(tmp_path: pathlib.Path):
30+
original = tmp_path / "original.txt"
31+
original.write_text("shared content")
32+
linked = tmp_path / "linked.txt"
33+
os.link(original, linked)
34+
35+
result = util.paths_to_inodes(tmp_path)
36+
37+
assert result["original.txt"] == result["linked.txt"]
38+
39+
40+
def test_paths_to_inodes_nested_directories_excluded(tmp_path: pathlib.Path):
41+
(tmp_path / "apple").mkdir()
42+
(tmp_path / "apple" / "banana").mkdir()
43+
(tmp_path / "cherry.txt").write_text("cherry")
44+
(tmp_path / "apple" / "date.txt").write_text("date")
45+
(tmp_path / "apple" / "banana" / "elderberry.txt").write_text("elderberry")
46+
47+
result = util.paths_to_inodes(tmp_path)
48+
49+
assert set(result.keys()) == {"cherry.txt", "apple/date.txt", "apple/banana/elderberry.txt"}
50+
51+
52+
def test_paths_to_inodes_returns_correct_paths_and_inodes(tmp_path: pathlib.Path):
53+
(tmp_path / "a.txt").write_text("alpha")
54+
(tmp_path / "b.txt").write_text("bravo")
55+
56+
result = util.paths_to_inodes(tmp_path)
57+
58+
assert set(result.keys()) == {"a.txt", "b.txt"}
59+
assert result["a.txt"] == (tmp_path / "a.txt").stat().st_ino
60+
assert result["b.txt"] == (tmp_path / "b.txt").stat().st_ino
61+
assert result["a.txt"] != result["b.txt"]

0 commit comments

Comments
 (0)