Skip to content

Commit d496d07

Browse files
committed
Add rsync-based "copy tree" strategy
It is the preferred strategy, because, in the future, it will allow excluding files and directories (think ".gitignore"). However, since `rsync` requires a temporary directory to play correctly with runners like Testing Farm, the strategy will do nothing when caller provides no way to create such a temporary directory.
1 parent d720de2 commit d496d07

5 files changed

Lines changed: 148 additions & 45 deletions

File tree

tests/unit/test_filesystem.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ def _run_metadata_test_for_item(
106106
def test_copy_tree_basic(copy_tree_paths: CopyTreePathConfig, root_logger: tmt.log.Logger):
107107
"""Test basic copy operation with default parameters."""
108108
source_dir, dest_dir, symlinks_supported = copy_tree_paths
109-
tmt.utils.filesystem.copy_tree(source_dir, dest_dir, root_logger)
109+
tmt.utils.filesystem.copy_tree(src=source_dir, dst=dest_dir, logger=root_logger)
110110

111111
# Check if all files were copied and their content is correct
112112
for path, content in _EXPECTED_TEST_FILES.items():
@@ -128,7 +128,7 @@ def test_copy_empty_source_directory(tmppath: Path, root_logger: tmt.log.Logger)
128128
empty_dst = tmppath / "empty_dst"
129129
empty_src.mkdir()
130130

131-
tmt.utils.filesystem.copy_tree(empty_src, empty_dst, root_logger)
131+
tmt.utils.filesystem.copy_tree(src=empty_src, dst=empty_dst, logger=root_logger)
132132

133133
# Verify the destination directory exists and is empty
134134
assert empty_dst.exists()
@@ -149,7 +149,7 @@ def test_deeply_nested_directories(tmppath: Path, root_logger: tmt.log.Logger):
149149
current_dir.mkdir()
150150
(current_dir / f"file_at_level_{level}.txt").write_text(f"{test_content} at level {level}")
151151

152-
tmt.utils.filesystem.copy_tree(deep_src, deep_dst, root_logger)
152+
tmt.utils.filesystem.copy_tree(src=deep_src, dst=deep_dst, logger=root_logger)
153153

154154
# Check if the deepest directory and file exist in the copied structure
155155
deepest_path = Path(
@@ -179,7 +179,7 @@ def test_permission_error_handling(
179179
"""Test handling of permission errors during copy."""
180180
source_dir, dest_dir, _ = copy_tree_paths
181181
with pytest.raises(tmt.utils.GeneralError) as excinfo:
182-
tmt.utils.filesystem.copy_tree(source_dir, dest_dir, root_logger)
182+
tmt.utils.filesystem.copy_tree(src=source_dir, dst=dest_dir, logger=root_logger)
183183

184184
assert (
185185
"permission error" in str(excinfo.value).lower()
@@ -193,7 +193,7 @@ def test_nonexistent_source_directory(tmppath: Path, root_logger: tmt.log.Logger
193193
destination = tmppath / "destination"
194194

195195
with pytest.raises(tmt.utils.GeneralError) as excinfo:
196-
tmt.utils.filesystem.copy_tree(nonexistent_src, destination, root_logger)
196+
tmt.utils.filesystem.copy_tree(src=nonexistent_src, dst=destination, logger=root_logger)
197197

198198
assert "not a directory or does not exist" in str(excinfo.value)
199199

@@ -213,7 +213,7 @@ def test_fallback_to_shutil_copy_from_cp_failure(
213213
source_dir, dest_dir, _ = copy_tree_paths
214214
mock_copy_tree_cp, mock_copy_tree_shutil = tmt.utils.filesystem._COPY_TREE_STRATEGIES
215215

216-
tmt.utils.filesystem.copy_tree(source_dir, dest_dir, root_logger)
216+
tmt.utils.filesystem.copy_tree(src=source_dir, dst=dest_dir, logger=root_logger)
217217

218218
mock_copy_tree_cp.assert_called_once_with(source_dir, dest_dir, mock.ANY)
219219
mock_copy_tree_shutil.assert_called_once_with(source_dir, dest_dir, mock.ANY)
@@ -235,7 +235,7 @@ def test_metadata_cp_reflink(copy_tree_paths: CopyTreePathConfig, root_logger: t
235235
source_dir, "meta_dir", is_dir=True, mode=0o750, atime=timestamp, mtime=timestamp
236236
)
237237

238-
tmt.utils.filesystem.copy_tree(source_dir, dest_dir, root_logger)
238+
tmt.utils.filesystem.copy_tree(src=source_dir, dst=dest_dir, logger=root_logger)
239239

240240
_run_metadata_test_for_item(dest_dir, test_file)
241241
_run_metadata_test_for_item(dest_dir, test_dir)
@@ -270,7 +270,7 @@ def test_metadata_preservation_on_cp_failure_fallback_to_shutil(
270270
source_dir, "meta_dir_shutil", is_dir=True, mode=0o500, atime=timestamp, mtime=timestamp
271271
)
272272

273-
tmt.utils.filesystem.copy_tree(source_dir, dest_dir, root_logger)
273+
tmt.utils.filesystem.copy_tree(src=source_dir, dst=dest_dir, logger=root_logger)
274274

275275
mock_copy_tree_cp.assert_called_once_with(source_dir, dest_dir, mock.ANY)
276276
mock_copy_tree_shutil.assert_called_once_with(source_dir, dest_dir, mock.ANY)
@@ -294,7 +294,7 @@ def test_all_strategies_fail(
294294
mock_copy_tree_cp, mock_copy_tree_shutil = tmt.utils.filesystem._COPY_TREE_STRATEGIES
295295

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

299299
mock_copy_tree_cp.assert_called_once()
300300
mock_copy_tree_shutil.assert_called_once()
@@ -312,7 +312,7 @@ def test_copy_to_existing_destination(
312312
(dest_dir / "subdir" / "existing_in_subdir.txt").write_text("pre-existing in subdir")
313313
(dest_dir / "file1.txt").write_text("old file1 content")
314314

315-
tmt.utils.filesystem.copy_tree(source_dir, dest_dir, root_logger)
315+
tmt.utils.filesystem.copy_tree(src=source_dir, dst=dest_dir, logger=root_logger)
316316

317317
# Check that source files were copied and overwrite conflicting ones
318318
assert (dest_dir / "file1.txt").read_text() == _EXPECTED_TEST_FILES["file1.txt"]

tmt/libraries/beakerlib.py

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -432,7 +432,9 @@ def _do_fetch(self, directory: Path) -> None:
432432
self.parent.debug(f"Failed to find library {self} at {self.url}")
433433
raise LibraryError
434434
self.parent.debug(f"Library {self} is copied into {directory}")
435-
tmt.utils.filesystem.copy_tree(library_path, local_library_path, self._logger)
435+
tmt.utils.filesystem.copy_tree(
436+
src=library_path, dst=local_library_path, logger=self._logger
437+
)
436438

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

463465
# Copy fmf metadata
464466
tmt.utils.filesystem.copy_tree(
465-
clone_dir / '.fmf',
466-
directory / '.fmf',
467-
self._logger,
467+
src=clone_dir / '.fmf',
468+
dst=directory / '.fmf',
469+
logger=self._logger,
468470
)
469471
if self.path:
470472
tmt.utils.filesystem.copy_tree(
471-
clone_dir / self.path.unrooted() / '.fmf',
472-
directory / self.path.unrooted() / '.fmf',
473-
self._logger,
473+
src=clone_dir / self.path.unrooted() / '.fmf',
474+
dst=directory / self.path.unrooted() / '.fmf',
475+
logger=self._logger,
474476
)
475477

476478

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

531533
self.parent.debug(f"Copy local library '{self.fmf_node_path}' to '{directory}'.", level=3)
532534
# Copy only the required library
533-
tmt.utils.filesystem.copy_tree(library_path, local_library_path, self._logger)
535+
tmt.utils.filesystem.copy_tree(
536+
src=library_path, dst=local_library_path, logger=self._logger
537+
)
534538
# Remove metadata file(s) and create one with full data
535539
self._merge_metadata(library_path, local_library_path)
536540
# Copy fmf metadata
537541
tmt.utils.filesystem.copy_tree(
538-
self.path / '.fmf',
539-
directory / '.fmf',
540-
self._logger,
542+
src=self.path / '.fmf',
543+
dst=directory / '.fmf',
544+
logger=self._logger,
541545
)

tmt/steps/discover/__init__.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -386,9 +386,9 @@ def prune_tree(
386386
# Save fmf metadata
387387
for file_path in tmt.utils.filter_paths(tree_path, [r'\.fmf']):
388388
tmt.utils.filesystem.copy_tree(
389-
file_path,
390-
clone_tree_path / file_path.relative_to(tree_path),
391-
self._logger,
389+
src=file_path,
390+
dst=clone_tree_path / file_path.relative_to(tree_path),
391+
logger=self._logger,
392392
)
393393

394394
# Save upgrade plan
@@ -405,9 +405,9 @@ def prune_tree(
405405
assert test.path is not None # narrow type
406406
relative_test_path = test.path.unrooted()
407407
tmt.utils.filesystem.copy_tree(
408-
tree_path / relative_test_path,
409-
clone_tree_path / relative_test_path,
410-
self._logger,
408+
src=tree_path / relative_test_path,
409+
dst=clone_tree_path / relative_test_path,
410+
logger=self._logger,
411411
)
412412

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

430430
if self.clone_dirpath.exists():
431431
shutil.rmtree(self.clone_dirpath, ignore_errors=True)

tmt/steps/discover/fmf.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -627,7 +627,7 @@ def _fetch_local_repository(self) -> Optional[Path]:
627627
directory = fmf_root
628628
self.info('directory', directory, 'green')
629629
self.debug(f"Copy '{directory}' to '{self.test_dir}'.")
630-
tmt.utils.filesystem.copy_tree(directory, self.test_dir, self._logger)
630+
tmt.utils.filesystem.copy_tree(src=directory, dst=self.test_dir, logger=self._logger)
631631
return path
632632

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

677677
# Copy rest of files so TMT_SOURCE_DIR has patches, sources and spec file
678678
tmt.utils.filesystem.copy_tree(
679-
distgit_dir,
680-
self.source_dir,
681-
self._logger,
679+
src=distgit_dir,
680+
dst=self.source_dir,
681+
logger=self._logger,
682682
)
683683

684684
# patch & rediscover will happen later in the prepare step
@@ -902,7 +902,9 @@ def post_dist_git(self, created_content: list[Path]) -> None:
902902
f"Directory '{self.step.plan.node.root}' is not in a git repository."
903903
) from error
904904
self.debug(f"Copy '{git_root}' to '{self.test_dir}'.")
905-
tmt.utils.filesystem.copy_tree(git_root, self.test_dir, self._logger)
905+
tmt.utils.filesystem.copy_tree(
906+
src=git_root, dst=self.test_dir, logger=self._logger
907+
)
906908
else:
907909
if not dist_git_merge:
908910
if self.data.path:
@@ -926,9 +928,9 @@ def post_dist_git(self, created_content: list[Path]) -> None:
926928
src = self.source_dir / to_copy
927929
if src.is_dir():
928930
tmt.utils.filesystem.copy_tree(
929-
self.source_dir / to_copy,
930-
self.test_dir if flatten else self.test_dir / to_copy,
931-
self._logger,
931+
src=self.source_dir / to_copy,
932+
dst=self.test_dir if flatten else self.test_dir / to_copy,
933+
logger=self._logger,
932934
)
933935
else:
934936
shutil.copyfile(src, self.test_dir / to_copy)

0 commit comments

Comments
 (0)