Skip to content

Commit 08cf03a

Browse files
authored
Merge pull request #7 from samsrabin/add-testing
Add testing of relink.py
2 parents af27fd9 + cf76f90 commit 08cf03a

7 files changed

Lines changed: 439 additions & 11 deletions

File tree

.github/workflows/pytest.yml

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
name: Run Tests
2+
3+
on:
4+
pull_request:
5+
branches:
6+
- main
7+
push:
8+
branches:
9+
- main
10+
11+
jobs:
12+
test:
13+
runs-on: ${{ matrix.os }}
14+
strategy:
15+
matrix:
16+
os: [ubuntu-latest, macos-latest]
17+
python-version: ['3.9', '3.10', '3.11', '3.12']
18+
fail-fast: false
19+
20+
steps:
21+
- name: Checkout code
22+
uses: actions/checkout@v4
23+
24+
- name: Set up Python ${{ matrix.python-version }}
25+
uses: actions/setup-python@v5
26+
with:
27+
python-version: ${{ matrix.python-version }}
28+
cache: 'pip'
29+
30+
- name: Install dependencies
31+
run: |
32+
python -m pip install --upgrade pip
33+
pip install pytest
34+
35+
- name: Run tests
36+
run: |
37+
pytest tests/ -v
38+
39+
- name: Run tests with coverage
40+
if: matrix.python-version == '3.11' && matrix.os == 'ubuntu-latest'
41+
run: |
42+
pip install pytest-cov
43+
pytest tests/ --cov=. --cov-report=xml --cov-report=term
44+
45+
- name: Upload coverage reports
46+
if: success() && matrix.python-version == '3.11' && matrix.os == 'ubuntu-latest'
47+
uses: codecov/codecov-action@v4
48+
with:
49+
file: ./coverage.xml
50+
fail_ci_if_error: false
51+
continue-on-error: true

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
__pycache__

relink.py

Lines changed: 47 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
1+
"""
2+
Finds files owned by a specific user in a source directory tree,
3+
deletes them, and replaces them with symbolic links to the same
4+
relative path in a target directory tree.
5+
"""
6+
17
import os
2-
import shutil
38
import pwd
9+
import argparse
10+
11+
DEFAULT_SOURCE_ROOT = '/glade/campaign/cesm/cesmdata/cseg/inputdata/'
12+
DEFAULT_TARGET_ROOT = '/glade/campaign/collections/gdex/data/d651077/cesmdata/inputdata/'
413

514
def find_and_replace_owned_files(source_dir, target_dir, username):
615
"""
@@ -25,10 +34,10 @@ def find_and_replace_owned_files(source_dir, target_dir, username):
2534

2635
print(f"Searching for files owned by '{username}' (UID: {user_uid}) in '{source_dir}'...")
2736

28-
for dirpath, dirnames, filenames in os.walk(source_dir):
37+
for dirpath, _, filenames in os.walk(source_dir):
2938
for filename in filenames:
3039
file_path = os.path.join(dirpath, filename)
31-
40+
3241
# Use os.stat().st_uid to get the file's owner UID
3342
try:
3443
if os.path.islink(file_path):
@@ -38,14 +47,14 @@ def find_and_replace_owned_files(source_dir, target_dir, username):
3847
file_uid = os.stat(file_path).st_uid
3948
except FileNotFoundError:
4049
continue # Skip if file was deleted during traversal
41-
50+
4251
if file_uid == user_uid:
4352
print(f"Found owned file: {file_path}")
4453

4554
# Determine the relative path and the new link's destination
4655
relative_path = os.path.relpath(file_path, source_dir)
4756
link_target = os.path.join(target_dir, relative_path)
48-
57+
4958
# Check if the target file actually exists
5059
if not os.path.exists(link_target):
5160
print(f"Warning: Corresponding file not found in '{target_dir}' for '{file_path}'. Skipping.")
@@ -73,14 +82,41 @@ def find_and_replace_owned_files(source_dir, target_dir, username):
7382
os.rename(link_name+".tmp", link_name)
7483
print(f"Error creating symlink for {link_name}: {e}. Skipping.")
7584

85+
def parse_arguments():
86+
"""
87+
Parse command-line arguments.
88+
89+
Returns:
90+
argparse.Namespace: Parsed arguments containing source_root
91+
and target_root.
92+
"""
93+
parser = argparse.ArgumentParser(
94+
description=(
95+
'Find files owned by a user and replace them with symbolic links to a target directory.'
96+
)
97+
)
98+
parser.add_argument(
99+
'--source-root',
100+
default=DEFAULT_SOURCE_ROOT,
101+
help=(
102+
f'The root of the directory tree to search for files (default: {DEFAULT_SOURCE_ROOT})'
103+
)
104+
)
105+
parser.add_argument(
106+
'--target-root',
107+
default=DEFAULT_TARGET_ROOT,
108+
help=(
109+
f'The root of the directory tree where files should be moved to '
110+
f'(default: {DEFAULT_TARGET_ROOT})'
111+
)
112+
)
113+
114+
return parser.parse_args()
115+
76116
if __name__ == '__main__':
77117
# --- Configuration ---
78-
# Replace these with your actual directories and username
79-
source_root = '/glade/campaign/cesm/cesmdata/cseg/inputdata/'
80-
target_root = '/glade/campaign/collections/gdex/data/d651077/cesmdata/inputdata/'
118+
args = parse_arguments()
81119
my_username = os.environ['USER']
82120

83121
# --- Execution ---
84-
find_and_replace_owned_files(source_root, target_root, my_username)
85-
86-
122+
find_and_replace_owned_files(args.source_root, args.target_root, my_username)

tests/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Test using `pytest` from this dir or repo top-level.

tests/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Tests package for inputdataTools."""

tests/conftest.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
"""
2+
Pytest configuration and shared fixtures for relink tests.
3+
"""
4+
5+
import os
6+
7+
import pytest
8+
9+
10+
@pytest.fixture(scope="session")
11+
def workspace_root():
12+
"""Return the root directory of the workspace."""
13+
return os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

0 commit comments

Comments
 (0)