From e608383cf18514afbebacdf8bbd75cfaddd7244e Mon Sep 17 00:00:00 2001 From: Haresh Nasit <84355507+hnnasit@users.noreply.github.com> Date: Wed, 14 Aug 2024 10:50:41 -0700 Subject: [PATCH] fix: Fix copying symlink files for Windows (#7351) * fix: Fix copying symlink files for Windows * make black happy --- samcli/lib/utils/osutils.py | 11 ++++- tests/unit/lib/utils/test_osutils.py | 63 ++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 1 deletion(-) diff --git a/samcli/lib/utils/osutils.py b/samcli/lib/utils/osutils.py index bef779ef68..f891c725ad 100644 --- a/samcli/lib/utils/osutils.py +++ b/samcli/lib/utils/osutils.py @@ -2,6 +2,7 @@ Common OS utilities """ +import errno import io import logging import os @@ -178,7 +179,15 @@ def copytree(source, destination, ignore=None): if os.path.isdir(new_source): copytree(new_source, new_destination, ignore=ignore) else: - shutil.copy2(new_source, new_destination) + try: + shutil.copy2(new_source, new_destination) + except OSError as e: + if e.errno != errno.EINVAL: + raise e + + # Symlinks do not get copied for Windows using shutil.copy2, which is why + # they are handled separately here. + create_symlink_or_copy(new_source, new_destination) def convert_files_to_unix_line_endings(path: str, target_files: Optional[List[str]] = None) -> None: diff --git a/tests/unit/lib/utils/test_osutils.py b/tests/unit/lib/utils/test_osutils.py index 6f7a6cf4df..2e06ef7f4c 100644 --- a/tests/unit/lib/utils/test_osutils.py +++ b/tests/unit/lib/utils/test_osutils.py @@ -90,6 +90,69 @@ def test_must_delete_if_path_exist(self, patched_rmtree, patched_path): patched_rmtree.assert_called_with(mock_path_obj) +class Test_copytree(TestCase): + @patch("samcli.lib.utils.osutils.Path") + @patch("samcli.lib.utils.osutils.os") + @patch("samcli.lib.utils.osutils.shutil.copy2") + def test_must_copytree(self, patched_copy2, patched_os, patched_path): + source_path = "mock-source/path" + destination_path = "mock-destination/path" + mock_path_obj = Mock() + patched_path.exists.return_value = True + patched_os.path.return_value = mock_path_obj + + patched_os.path.join.side_effect = [source_path, destination_path] + patched_os.path.isdir.return_value = False + patched_os.listdir.return_value = ["mock-source-file1"] + osutils.copytree(source_path, destination_path) + + patched_os.path.join.assert_called() + patched_copy2.assert_called_with(source_path, destination_path) + + @patch("samcli.lib.utils.osutils.Path") + @patch("samcli.lib.utils.osutils.os") + @patch("samcli.lib.utils.osutils.shutil.copy2") + def test_copytree_throws_oserror_path_exists(self, patched_copy2, patched_os, patched_path): + source_path = "mock-source/path" + destination_path = "mock-destination/path" + mock_path_obj = Mock() + patched_path.exists.return_value = True + patched_os.path.return_value = mock_path_obj + patched_copy2.side_effect = OSError("mock-os-error") + + patched_os.path.join.side_effect = [source_path, destination_path] + patched_os.path.isdir.return_value = False + patched_os.listdir.return_value = ["mock-source-file1"] + with self.assertRaises(OSError): + osutils.copytree(source_path, destination_path) + + patched_os.path.join.assert_called() + patched_copy2.assert_called_with(source_path, destination_path) + + @patch("samcli.lib.utils.osutils.create_symlink_or_copy") + @patch("samcli.lib.utils.osutils.Path") + @patch("samcli.lib.utils.osutils.os") + @patch("samcli.lib.utils.osutils.shutil.copy2") + def test_copytree_symlink_copy_error_handling( + self, patched_copy2, patched_os, patched_path, patched_create_symlink_or_copy + ): + source_path = "mock-source/path" + destination_path = "mock-destination/path" + mock_path_obj = Mock() + patched_path.exists.return_value = True + patched_os.path.return_value = mock_path_obj + patched_copy2.side_effect = OSError(22, "mock-os-error") + + patched_os.path.join.side_effect = [source_path, destination_path] + patched_os.path.isdir.return_value = False + patched_os.listdir.return_value = ["mock-source-file1"] + osutils.copytree(source_path, destination_path) + + patched_os.path.join.assert_called() + patched_copy2.assert_called_with(source_path, destination_path) + patched_create_symlink_or_copy.assert_called_with(source_path, destination_path) + + class Test_create_symlink_or_copy(TestCase): @patch("samcli.lib.utils.osutils.Path") @patch("samcli.lib.utils.osutils.os")