Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 39 additions & 13 deletions tests/unit/test_filesystem.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import contextlib
import os
import stat
import subprocess
import time
from collections.abc import Generator
from typing import Optional, cast
from unittest import mock
from unittest.mock import MagicMock
Expand Down Expand Up @@ -65,6 +67,17 @@ def fixture_copy_tree_paths(tmppath: Path) -> CopyTreePathConfig:
return source_dir, dest_dir, symlinks_supported


@pytest.fixture(name="tmpdir_creator")
def fixture_tmpdir_creator(tmppath: Path) -> tmt.utils.filesystem.TmpDirCreator:
@contextlib.contextmanager
def _tmpdir_creator(
prefix: Optional[str] = None, suffix: Optional[str] = None
) -> Generator[Path, None, None]:
yield tmppath

return _tmpdir_creator


def _assert_permissions_copied(src_path: Path, dest_path: Path) -> None:
"""
Assert that file/directory permissions are copied correctly.
Expand Down Expand Up @@ -121,6 +134,7 @@ def _run_metadata_test_for_item(
@pytest.mark.parametrize('strategy', _STRATEGIES)
def test_copy_tree_basic(
strategy: tmt.utils.filesystem.CopyStrategy,
tmpdir_creator: tmt.utils.filesystem.TmpDirCreator,
copy_tree_paths: CopyTreePathConfig,
root_logger: tmt.log.Logger,
) -> None:
Expand All @@ -130,7 +144,7 @@ def test_copy_tree_basic(

source_dir, dest_dir, symlinks_supported = copy_tree_paths

strategy(source_dir, dest_dir, root_logger)
strategy(src=source_dir, dst=dest_dir, tmpdir_creator=tmpdir_creator, logger=root_logger)

# Check if all files were copied and their content is correct
for path, content in _EXPECTED_TEST_FILES.items():
Expand All @@ -148,7 +162,10 @@ def test_copy_tree_basic(

@pytest.mark.parametrize('strategy', _STRATEGIES)
def test_copy_empty_source_directory(
strategy: tmt.utils.filesystem.CopyStrategy, tmppath: Path, root_logger: tmt.log.Logger
strategy: tmt.utils.filesystem.CopyStrategy,
tmpdir_creator: tmt.utils.filesystem.TmpDirCreator,
tmppath: Path,
root_logger: tmt.log.Logger,
) -> None:
"""
Test copying an empty source directory.
Expand All @@ -158,7 +175,7 @@ def test_copy_empty_source_directory(
empty_dst = tmppath / "empty_dst"
empty_src.mkdir()

strategy(empty_src, empty_dst, root_logger)
strategy(src=empty_src, dst=empty_dst, tmpdir_creator=tmpdir_creator, logger=root_logger)

# Verify the destination directory exists and is empty
assert empty_dst.exists()
Expand All @@ -168,7 +185,10 @@ def test_copy_empty_source_directory(

@pytest.mark.parametrize('strategy', _STRATEGIES)
def test_deeply_nested_directories(
strategy: tmt.utils.filesystem.CopyStrategy, tmppath: Path, root_logger: tmt.log.Logger
strategy: tmt.utils.filesystem.CopyStrategy,
tmpdir_creator: tmt.utils.filesystem.TmpDirCreator,
tmppath: Path,
root_logger: tmt.log.Logger,
) -> None:
"""Test copying deeply nested directory structures."""
deep_src = tmppath / "deep_src"
Expand All @@ -182,7 +202,7 @@ def test_deeply_nested_directories(
current_dir.mkdir()
(current_dir / f"file_at_level_{level}.txt").write_text(f"{test_content} at level {level}")

strategy(deep_src, deep_dst, root_logger)
strategy(src=deep_src, dst=deep_dst, tmpdir_creator=tmpdir_creator, logger=root_logger)

# Check if the deepest directory and file exist in the copied structure
deepest_path = Path(
Expand Down Expand Up @@ -213,7 +233,7 @@ def test_permission_error_handling(
subprocess.check_call(['chattr', '+i', str(dest_dir)])

with pytest.raises(tmt.utils.GeneralError, match=r'(?i)Failed to copy tree'):
tmt.utils.filesystem.copy_tree(source_dir, dest_dir, root_logger)
tmt.utils.filesystem.copy_tree(src=source_dir, dst=dest_dir, logger=root_logger)


def test_nonexistent_source_directory(tmppath: Path, root_logger: tmt.log.Logger) -> None:
Expand All @@ -225,7 +245,7 @@ def test_nonexistent_source_directory(tmppath: Path, root_logger: tmt.log.Logger
destination = tmppath / "destination"

with pytest.raises(tmt.utils.GeneralError, match=r'.*not a directory or does not exist.*'):
tmt.utils.filesystem.copy_tree(nonexistent_src, destination, root_logger)
tmt.utils.filesystem.copy_tree(src=nonexistent_src, dst=destination, logger=root_logger)


@mock.patch(
Expand All @@ -247,10 +267,14 @@ def test_fallback(

mock_copy_tree_cp, mock_copy_tree_shutil = tmt.utils.filesystem._COPY_TREE_STRATEGIES

tmt.utils.filesystem.copy_tree(source_dir, dest_dir, root_logger)
tmt.utils.filesystem.copy_tree(src=source_dir, dst=dest_dir, logger=root_logger)

cast(MagicMock, mock_copy_tree_cp).assert_called_once_with(source_dir, dest_dir, mock.ANY)
cast(MagicMock, mock_copy_tree_shutil).assert_called_once_with(source_dir, dest_dir, mock.ANY)
cast(MagicMock, mock_copy_tree_cp).assert_called_once_with(
src=source_dir, dst=dest_dir, tmpdir_creator=None, logger=mock.ANY
)
cast(MagicMock, mock_copy_tree_shutil).assert_called_once_with(
src=source_dir, dst=dest_dir, tmpdir_creator=None, logger=mock.ANY
)

# Verify files were copied using the fallback approach (shutil.copytree)
for file_path in _EXPECTED_TEST_FILES:
Expand All @@ -260,6 +284,7 @@ def test_fallback(
@pytest.mark.parametrize('strategy', _STRATEGIES)
def test_metadata_preservation(
strategy: tmt.utils.filesystem.CopyStrategy,
tmpdir_creator: tmt.utils.filesystem.TmpDirCreator,
copy_tree_paths: CopyTreePathConfig,
root_logger: tmt.log.Logger,
) -> None:
Expand All @@ -283,7 +308,7 @@ def test_metadata_preservation(
source_dir, "meta_dir_shutil", is_dir=True, mode=0o500, atime=timestamp, mtime=timestamp
)

strategy(source_dir, dest_dir, root_logger)
strategy(src=source_dir, dst=dest_dir, tmpdir_creator=tmpdir_creator, logger=root_logger)

_run_metadata_test_for_item(dest_dir, test_file)
_run_metadata_test_for_item(dest_dir, test_dir)
Expand All @@ -308,7 +333,7 @@ def test_all_strategies_fail(
mock_copy_tree_cp, mock_copy_tree_shutil = tmt.utils.filesystem._COPY_TREE_STRATEGIES

with pytest.raises(tmt.utils.GeneralError):
tmt.utils.filesystem.copy_tree(source_dir, dest_dir, root_logger)
tmt.utils.filesystem.copy_tree(src=source_dir, dst=dest_dir, logger=root_logger)

cast(MagicMock, mock_copy_tree_cp).assert_called_once()
cast(MagicMock, mock_copy_tree_shutil).assert_called_once()
Expand All @@ -317,6 +342,7 @@ def test_all_strategies_fail(
@pytest.mark.parametrize('strategy', _STRATEGIES)
def test_copy_to_existing_destination(
strategy: tmt.utils.filesystem.CopyStrategy,
tmpdir_creator: tmt.utils.filesystem.TmpDirCreator,
copy_tree_paths: CopyTreePathConfig,
root_logger: tmt.log.Logger,
) -> None:
Expand All @@ -332,7 +358,7 @@ def test_copy_to_existing_destination(
(dest_dir / "subdir" / "existing_in_subdir.txt").write_text("pre-existing in subdir")
(dest_dir / "file1.txt").write_text("old file1 content")

strategy(source_dir, dest_dir, root_logger)
strategy(src=source_dir, dst=dest_dir, tmpdir_creator=tmpdir_creator, logger=root_logger)

# Check that source files were copied and overwrite conflicting ones
assert (dest_dir / "file1.txt").read_text() == _EXPECTED_TEST_FILES["file1.txt"]
Expand Down
26 changes: 15 additions & 11 deletions tmt/libraries/beakerlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -432,7 +432,9 @@ def _do_fetch(self, directory: Path) -> None:
self.parent.debug(f"Failed to find library {self} at {self.url}")
raise LibraryError
self.parent.debug(f"Library {self} is copied into {directory}")
tmt.utils.filesystem.copy_tree(library_path, local_library_path, self._logger)
tmt.utils.filesystem.copy_tree(
src=library_path, dst=local_library_path, logger=self._logger
)

self.parent.verbose(
'using remote git library',
Expand Down Expand Up @@ -462,15 +464,15 @@ def _do_fetch(self, directory: Path) -> None:

# Copy fmf metadata
tmt.utils.filesystem.copy_tree(
clone_dir / '.fmf',
directory / '.fmf',
self._logger,
src=clone_dir / '.fmf',
dst=directory / '.fmf',
logger=self._logger,
)
if self.path:
tmt.utils.filesystem.copy_tree(
clone_dir / self.path.unrooted() / '.fmf',
directory / self.path.unrooted() / '.fmf',
self._logger,
src=clone_dir / self.path.unrooted() / '.fmf',
dst=directory / self.path.unrooted() / '.fmf',
logger=self._logger,
)


Expand Down Expand Up @@ -530,12 +532,14 @@ def _do_fetch(self, directory: Path) -> None:

self.parent.debug(f"Copy local library '{self.fmf_node_path}' to '{directory}'.", level=3)
# Copy only the required library
tmt.utils.filesystem.copy_tree(library_path, local_library_path, self._logger)
tmt.utils.filesystem.copy_tree(
src=library_path, dst=local_library_path, logger=self._logger
)
# Remove metadata file(s) and create one with full data
self._merge_metadata(library_path, local_library_path)
# Copy fmf metadata
tmt.utils.filesystem.copy_tree(
self.path / '.fmf',
directory / '.fmf',
self._logger,
src=self.path / '.fmf',
dst=directory / '.fmf',
logger=self._logger,
)
14 changes: 7 additions & 7 deletions tmt/steps/discover/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -386,9 +386,9 @@ def prune_tree(
# Save fmf metadata
for file_path in tmt.utils.filter_paths(tree_path, [r'\.fmf']):
tmt.utils.filesystem.copy_tree(
file_path,
clone_tree_path / file_path.relative_to(tree_path),
self._logger,
src=file_path,
dst=clone_tree_path / file_path.relative_to(tree_path),
logger=self._logger,
)

# Save upgrade plan
Expand All @@ -405,9 +405,9 @@ def prune_tree(
assert test.path is not None # narrow type
relative_test_path = test.path.unrooted()
tmt.utils.filesystem.copy_tree(
tree_path / relative_test_path,
clone_tree_path / relative_test_path,
self._logger,
src=tree_path / relative_test_path,
dst=clone_tree_path / relative_test_path,
logger=self._logger,
)

# Copy all parent main.fmf files
Expand All @@ -425,7 +425,7 @@ def prune_tree(
# Clean phase.test_dir and copy back only required tests and files from clone_dir
# This is to have correct paths in tests
shutil.rmtree(self.test_dir, ignore_errors=True)
tmt.utils.filesystem.copy_tree(clone_dir, self.test_dir, self._logger)
tmt.utils.filesystem.copy_tree(src=clone_dir, dst=self.test_dir, logger=self._logger)

if self.clone_dirpath.exists():
shutil.rmtree(self.clone_dirpath, ignore_errors=True)
Expand Down
18 changes: 10 additions & 8 deletions tmt/steps/discover/fmf.py
Original file line number Diff line number Diff line change
Expand Up @@ -627,7 +627,7 @@ def _fetch_local_repository(self) -> Optional[Path]:
directory = fmf_root
self.info('directory', directory, 'green')
self.debug(f"Copy '{directory}' to '{self.test_dir}'.")
tmt.utils.filesystem.copy_tree(directory, self.test_dir, self._logger)
tmt.utils.filesystem.copy_tree(src=directory, dst=self.test_dir, logger=self._logger)
return path

def go(self, *, path: Optional[Path] = None, logger: Optional[tmt.log.Logger] = None) -> None:
Expand Down Expand Up @@ -676,9 +676,9 @@ def process_distgit_source(self, distgit_dir: Path) -> None:

# Copy rest of files so TMT_SOURCE_DIR has patches, sources and spec file
tmt.utils.filesystem.copy_tree(
distgit_dir,
self.source_dir,
self._logger,
src=distgit_dir,
dst=self.source_dir,
logger=self._logger,
)

# patch & rediscover will happen later in the prepare step
Expand Down Expand Up @@ -902,7 +902,9 @@ def post_dist_git(self, created_content: list[Path]) -> None:
f"Directory '{self.step.plan.node.root}' is not in a git repository."
) from error
self.debug(f"Copy '{git_root}' to '{self.test_dir}'.")
tmt.utils.filesystem.copy_tree(git_root, self.test_dir, self._logger)
tmt.utils.filesystem.copy_tree(
src=git_root, dst=self.test_dir, logger=self._logger
)
else:
if not dist_git_merge:
if self.data.path:
Expand All @@ -926,9 +928,9 @@ def post_dist_git(self, created_content: list[Path]) -> None:
src = self.source_dir / to_copy
if src.is_dir():
tmt.utils.filesystem.copy_tree(
self.source_dir / to_copy,
self.test_dir if flatten else self.test_dir / to_copy,
self._logger,
src=self.source_dir / to_copy,
dst=self.test_dir if flatten else self.test_dir / to_copy,
logger=self._logger,
)
else:
shutil.copyfile(src, self.test_dir / to_copy)
Expand Down
Loading
Loading