diff --git a/Lib/test/symlink_support.py b/Lib/test/symlink_support.py new file mode 100644 index 00000000000..301d0f99a92 --- /dev/null +++ b/Lib/test/symlink_support.py @@ -0,0 +1,100 @@ +import os +import unittest +import platform + +from test.test_support import TESTFN + +def can_symlink(): + # cache the result in can_symlink.prev_val + prev_val = getattr(can_symlink, 'prev_val', None) + if prev_val is not None: + return prev_val + symlink_path = TESTFN + "can_symlink" + try: + symlink(TESTFN, symlink_path) + can = True + except (OSError, NotImplementedError, AttributeError): + can = False + else: + os.remove(symlink_path) + can_symlink.prev_val = can + return can + +def skip_unless_symlink(test): + """Skip decorator for tests that require functional symlink""" + ok = can_symlink() + msg = "Requires functional symlink implementation" + return test if ok else unittest.skip(msg)(test) + +def _symlink_win32(target, link, target_is_directory=False): + """ + Ctypes symlink implementation since Python doesn't support + symlinks in windows yet. Borrowed from jaraco.windows project. + """ + import ctypes.wintypes + CreateSymbolicLink = ctypes.windll.kernel32.CreateSymbolicLinkW + CreateSymbolicLink.argtypes = ( + ctypes.wintypes.LPWSTR, + ctypes.wintypes.LPWSTR, + ctypes.wintypes.DWORD, + ) + CreateSymbolicLink.restype = ctypes.wintypes.BOOLEAN + + def format_system_message(errno): + """ + Call FormatMessage with a system error number to retrieve + the descriptive error message. + """ + # first some flags used by FormatMessageW + ALLOCATE_BUFFER = 0x100 + ARGUMENT_ARRAY = 0x2000 + FROM_HMODULE = 0x800 + FROM_STRING = 0x400 + FROM_SYSTEM = 0x1000 + IGNORE_INSERTS = 0x200 + + # Let FormatMessageW allocate the buffer (we'll free it below) + # Also, let it know we want a system error message. + flags = ALLOCATE_BUFFER | FROM_SYSTEM + source = None + message_id = errno + language_id = 0 + result_buffer = ctypes.wintypes.LPWSTR() + buffer_size = 0 + arguments = None + bytes = ctypes.windll.kernel32.FormatMessageW( + flags, + source, + message_id, + language_id, + ctypes.byref(result_buffer), + buffer_size, + arguments, + ) + # note the following will cause an infinite loop if GetLastError + # repeatedly returns an error that cannot be formatted, although + # this should not happen. + handle_nonzero_success(bytes) + message = result_buffer.value + ctypes.windll.kernel32.LocalFree(result_buffer) + return message + + def handle_nonzero_success(result): + if result == 0: + value = ctypes.windll.kernel32.GetLastError() + strerror = format_system_message(value) + raise WindowsError(value, strerror) + + target_is_directory = target_is_directory or os.path.isdir(target) + handle_nonzero_success(CreateSymbolicLink(link, target, target_is_directory)) + +symlink = os.symlink if hasattr(os, 'symlink') else ( + _symlink_win32 if platform.system() == 'Windows' else None +) + +def remove_symlink(name): + # On Windows, to remove a directory symlink, one must use rmdir + try: + os.rmdir(name) + except OSError: + os.remove(name) diff --git a/Lib/test/test_import.py b/Lib/test/test_import.py index 88786591d30..1612a471858 100644 --- a/Lib/test/test_import.py +++ b/Lib/test/test_import.py @@ -12,6 +12,7 @@ import shutil from test.test_support import (unlink, TESTFN, unload, run_unittest, rmtree, is_jython, check_warnings, EnvironmentVarGuard) +from test import symlink_support from test import script_helper def remove_files(name): @@ -497,11 +498,9 @@ class TestSymbolicallyLinkedPackage(unittest.TestCase): if os.path.exists(self.tagged): shutil.rmtree(self.tagged) if os.path.exists(self.package_name): - self.remove_symlink(self.package_name) + symlink_support.remove_symlink(self.package_name) self.orig_sys_path = sys.path[:] - symlink = getattr(os, 'symlink', None) or self._symlink_win32 - # create a sample package; imagine you have a package with a tag and # you want to symbolically link it from its untagged name. os.mkdir(self.tagged) @@ -511,7 +510,7 @@ class TestSymbolicallyLinkedPackage(unittest.TestCase): # now create a symlink to the tagged package # sample -> sample-tagged - symlink(self.tagged, self.package_name) + symlink_support.symlink(self.tagged, self.package_name) assert os.path.isdir(self.package_name) assert os.path.isfile(os.path.join(self.package_name, '__init__.py')) @@ -520,74 +519,12 @@ class TestSymbolicallyLinkedPackage(unittest.TestCase): def tagged(self): return self.package_name + '-tagged' - @classmethod - def _symlink_win32(cls, target, link, target_is_directory=False): - """ - Ctypes symlink implementation since Python doesn't support - symlinks in windows yet. Borrowed from jaraco.windows project. - """ - import ctypes.wintypes - CreateSymbolicLink = ctypes.windll.kernel32.CreateSymbolicLinkW - CreateSymbolicLink.argtypes = ( - ctypes.wintypes.LPWSTR, - ctypes.wintypes.LPWSTR, - ctypes.wintypes.DWORD, - ) - CreateSymbolicLink.restype = ctypes.wintypes.BOOLEAN - - def format_system_message(errno): - """ - Call FormatMessage with a system error number to retrieve - the descriptive error message. - """ - # first some flags used by FormatMessageW - ALLOCATE_BUFFER = 0x100 - ARGUMENT_ARRAY = 0x2000 - FROM_HMODULE = 0x800 - FROM_STRING = 0x400 - FROM_SYSTEM = 0x1000 - IGNORE_INSERTS = 0x200 - - # Let FormatMessageW allocate the buffer (we'll free it below) - # Also, let it know we want a system error message. - flags = ALLOCATE_BUFFER | FROM_SYSTEM - source = None - message_id = errno - language_id = 0 - result_buffer = ctypes.wintypes.LPWSTR() - buffer_size = 0 - arguments = None - bytes = ctypes.windll.kernel32.FormatMessageW( - flags, - source, - message_id, - language_id, - ctypes.byref(result_buffer), - buffer_size, - arguments, - ) - # note the following will cause an infinite loop if GetLastError - # repeatedly returns an error that cannot be formatted, although - # this should not happen. - handle_nonzero_success(bytes) - message = result_buffer.value - ctypes.windll.kernel32.LocalFree(result_buffer) - return message - - def handle_nonzero_success(result): - if result == 0: - value = ctypes.windll.kernel32.GetLastError() - strerror = format_system_message(value) - raise WindowsError(value, strerror) - - target_is_directory = target_is_directory or os.path.isdir(target) - handle_nonzero_success(CreateSymbolicLink(link, target, target_is_directory)) - # regression test for issue6727 @unittest.skipUnless( not hasattr(sys, 'getwindowsversion') or sys.getwindowsversion() >= (6, 0), "Windows Vista or later required") + @symlink_support.skip_unless_symlink def test_symlinked_dir_importable(self): # make sure sample can only be imported from the current directory. sys.path[:] = ['.'] @@ -598,19 +535,11 @@ class TestSymbolicallyLinkedPackage(unittest.TestCase): def tearDown(self): # now cleanup if os.path.exists(self.package_name): - self.remove_symlink(self.package_name) + symlink_support.remove_symlink(self.package_name) if os.path.exists(self.tagged): shutil.rmtree(self.tagged) sys.path[:] = self.orig_sys_path - @staticmethod - def remove_symlink(name): - # On Windows, to remove a directory symlink, one must use rmdir - try: - os.rmdir(name) - except OSError: - os.remove(name) - def test_main(verbose=None): run_unittest(ImportTests, PycRewritingTests, PathsTests, RelativeImportTests, TestSymbolicallyLinkedPackage)