From 9ac362f67e3048d8dcd8db653148d6a7bec9b02f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Lozier?= Date: Wed, 24 Jun 2026 21:02:56 -0400 Subject: [PATCH 1/3] Enable test_os --- src/core/IronPython.Modules/nt.cs | 169 ++++++++++-------- .../Cases/CPythonCasesManifest.ini | 3 - tests/suite/stdlib/test_os.py | 110 ++++++++++++ 3 files changed, 202 insertions(+), 80 deletions(-) create mode 100644 tests/suite/stdlib/test_os.py diff --git a/src/core/IronPython.Modules/nt.cs b/src/core/IronPython.Modules/nt.cs index dbc80ff0d..8a1181da8 100644 --- a/src/core/IronPython.Modules/nt.cs +++ b/src/core/IronPython.Modules/nt.cs @@ -285,8 +285,8 @@ public static bool access(CodeContext context, object? path, int mode, [ParamDic #if FEATURE_FILESYSTEM public static void chdir([NotNone] string path) { - if (String.IsNullOrEmpty(path)) { - throw PythonOps.OSError(PythonExceptions._OSError.ERROR_INVALID_NAME, "Path cannot be an empty string", path, PythonExceptions._OSError.ERROR_INVALID_NAME); + if (string.IsNullOrEmpty(path)) { + throw GetOsOrWinError(PythonErrno.ENOENT, PythonExceptions._OSError.ERROR_INVALID_NAME, path); } try { @@ -553,7 +553,7 @@ public static PythonList listdir(CodeContext/*!*/ context, string? path = null) } if (path == string.Empty) { - throw PythonOps.OSError(PythonExceptions._OSError.ERROR_PATH_NOT_FOUND, "The system cannot find the path specified", path, PythonExceptions._OSError.ERROR_PATH_NOT_FOUND); + throw GetOsOrWinError(PythonErrno.ENOENT, PythonExceptions._OSError.ERROR_PATH_NOT_FOUND, path); } #if !NETFRAMEWORK @@ -628,7 +628,7 @@ internal DirEntry(CodeContext context, FileSystemInfo info, bool asBytes) { [LightThrowing] public object? inode() { - var obj = stat(follow_symlinks: false); + var obj = PythonNT.stat(info.FullName, new Dictionary()); if (obj is stat_result res) return res.st_ino; return obj; } @@ -640,9 +640,15 @@ internal DirEntry(CodeContext context, FileSystemInfo info, bool asBytes) { public bool is_symlink() => info.Attributes.HasFlag(FileAttributes.ReparsePoint) ? throw new NotImplementedException() : false; [LightThrowing] - public object? stat(bool follow_symlinks = true) => PythonNT.stat(info.FullName, new Dictionary()); + public object? stat(bool follow_symlinks = true) { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + return statWindowsImpl(info); + return PythonNT.stat(info.FullName, new Dictionary()); + } public string __repr__(CodeContext context) => $""; + + public object __fspath__(CodeContext context) => path; } [PythonType, PythonHidden] @@ -675,6 +681,8 @@ internal ScandirIterator(CodeContext context, IEnumerable list, [PythonHidden] public void Reset() => enumerator.Reset(); + + public void close() => Dispose(); } public static ScandirIterator scandir(CodeContext context, string? path = null) @@ -689,7 +697,7 @@ private static IEnumerable ScandirHelper(CodeContext context, st } if (path == string.Empty) { - throw PythonOps.OSError(PythonExceptions._OSError.ERROR_PATH_NOT_FOUND, "The system cannot find the path specified", path, PythonExceptions._OSError.ERROR_PATH_NOT_FOUND); + throw GetOsOrWinError(PythonErrno.ENOENT, PythonExceptions._OSError.ERROR_PATH_NOT_FOUND, path); } #if !NETFRAMEWORK @@ -1396,8 +1404,8 @@ public PythonTuple __reduce__() { } } - private static bool HasExecutableExtension(string path) { - string extension = Path.GetExtension(path).ToLower(CultureInfo.InvariantCulture); + private static bool HasExecutableExtension(string extension) { + extension = extension.ToLower(CultureInfo.InvariantCulture); return (extension == ".exe" || extension == ".dll" || extension == ".com" || extension == ".bat"); } @@ -1456,50 +1464,22 @@ public static object stat([NotNone] string path, [ParamDictionary] IDictionary dict) => stat(path.ToFsString(context), dict); @@ -1663,7 +1683,20 @@ public static void remove([NotNone] string path, [ParamDictionary] IDictionary kwargs) => unlink(ConvertToFsString(context, path, nameof(path)), kwargs); - - private static void UnlinkWorker(string path) { - if (path == null) { - throw new ArgumentNullException(nameof(path)); - } else if (path.IndexOfAny(Path.GetInvalidPathChars()) != -1 || Path.GetFileName(path).IndexOfAny(Path.GetInvalidFileNameChars()) != -1) { - throw PythonOps.OSError(PythonExceptions._OSError.ERROR_INVALID_NAME, "The filename, directory name, or volume label syntax is incorrect", path, PythonExceptions._OSError.ERROR_INVALID_NAME); - } - - bool existing = File.Exists(path); // will return false also on access denied - try { - File.Delete(path); // will throw an exception on access denied, no exception on file not existing - } catch (Exception e) { - throw ToPythonException(e, path); - } - if (!existing) { // file was not existing in the first place - throw PythonOps.OSError(PythonExceptions._OSError.ERROR_FILE_NOT_FOUND, "The system cannot find the file specified", path, PythonExceptions._OSError.ERROR_FILE_NOT_FOUND); - } - } #endif #if FEATURE_PROCESS @@ -2127,15 +2142,13 @@ private static Exception ToPythonException(Exception e, string? filename = null) message = e.Message; isWindowsError = true; } else if (e is UnauthorizedAccessException unauth) { - return RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? - GetWin32Error(PythonExceptions._OSError.ERROR_ACCESS_DENIED, filename) : - GetOsError(PythonErrno.EACCES, filename); + return GetOsOrWinError(PythonErrno.EACCES, PythonExceptions._OSError.ERROR_ACCESS_DENIED, filename); } else { var ioe = e as IOException; Exception? pe = IOExceptionToPythonException(ioe, error, filename); if (pe != null) return pe; - errorCode = System.Runtime.InteropServices.Marshal.GetHRForException(e); + errorCode = Marshal.GetHRForException(e); if ((errorCode & ~0xfff) == (unchecked((int)0x80070000))) { // Win32 HR, translate HR to Python error code if possible, otherwise @@ -2310,19 +2323,21 @@ private static bool TryGetShellCommand(string command, [NotNullWhen(true)] out s #endif - private static Exception DirectoryExistsError(string? filename) { + private static Exception DirectoryExistsError(string? filename) + => GetOsOrWinError(PythonErrno.EEXIST, PythonExceptions._OSError.ERROR_ALREADY_EXISTS, filename); + + internal static Exception GetOsError(int errno, string? filename = null, string? filename2 = null) + => PythonOps.OSError(errno, strerror(errno), filename, null, filename2); + + internal static Exception GetOsOrWinError(int errno, int winerror, string? filename = null, string? filename2 = null) { #if FEATURE_NATIVE if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - return GetWin32Error(PythonExceptions._OSError.ERROR_ALREADY_EXISTS, filename); + return GetWin32Error(winerror, filename, filename2); } #endif - return GetOsError(PythonErrno.EEXIST, filename); + return PythonOps.OSError(errno, strerror(errno), filename, null, filename2); } - - internal static Exception GetOsError(int errno, string? filename = null, string? filename2 = null) - => PythonOps.OSError(errno, strerror(errno), filename, null, filename2); - #if FEATURE_NATIVE || FEATURE_CTYPES [SupportedOSPlatform("windows")] diff --git a/tests/IronPython.Tests/Cases/CPythonCasesManifest.ini b/tests/IronPython.Tests/Cases/CPythonCasesManifest.ini index d970bf63e..33510bf3b 100644 --- a/tests/IronPython.Tests/Cases/CPythonCasesManifest.ini +++ b/tests/IronPython.Tests/Cases/CPythonCasesManifest.ini @@ -640,9 +640,6 @@ RunCondition=$(IS_POSIX) Ignore=true Reason=unittest.case.SkipTest: os.openpty() not available. -[CPython.test_os] -Ignore=true - [CPython.test_ossaudiodev] # Module has been removed in 3.13 - https://github.com/IronLanguages/ironpython3/issues/1352 Ignore=true Reason=unittest.case.SkipTest: No module named 'ossaudiodev' diff --git a/tests/suite/stdlib/test_os.py b/tests/suite/stdlib/test_os.py new file mode 100644 index 000000000..37eb2d830 --- /dev/null +++ b/tests/suite/stdlib/test_os.py @@ -0,0 +1,110 @@ +# Licensed to the .NET Foundation under one or more agreements. +# The .NET Foundation licenses this file to you under the Apache 2.0 License. +# See the LICENSE file in the project root for more information. + +## +## Run selected tests from test_os from StdLib +## + +import sys + +from iptest import is_ironpython, generate_suite, run_test, is_posix + +import test.test_os + +def load_tests(loader, standard_tests, pattern): + tests = loader.loadTestsFromModule(test.test_os, pattern=pattern) + + if is_ironpython: + failing_tests = [ + test.test_os.DeviceEncodingTests('test_bad_fd'), # AttributeError: 'module' object has no attribute 'device_encoding' + test.test_os.DeviceEncodingTests('test_device_encoding'), # AttributeError: 'module' object has no attribute 'device_encoding' + test.test_os.ExecTests('test_execvpe_with_bad_arglist'), # NameError: name 'execv' is not defined + test.test_os.ExecTests('test_execvpe_with_bad_program'), # NameError: name 'execv' is not defined + test.test_os.ExecTests('test_internal_execvpe_str'), # NameError: name 'execv' is not defined + test.test_os.FDInheritanceTests('test_dup'), # AttributeError: 'module' object has no attribute 'get_inheritable' + test.test_os.FDInheritanceTests('test_dup2'), # AttributeError: 'module' object has no attribute 'get_inheritable' + test.test_os.FDInheritanceTests('test_get_set_inheritable'), # AttributeError: 'module' object has no attribute 'get_inheritable' + test.test_os.FDInheritanceTests('test_open'), # AttributeError: 'module' object has no attribute 'get_inheritable' + test.test_os.FDInheritanceTests('test_pipe'), # AttributeError: 'module' object has no attribute 'get_inheritable' + test.test_os.FileTests('test_open_keywords'), # IndexError: Index was outside the bounds of the array. + test.test_os.FileTests('test_symlink_keywords'), # IndexError: Index was outside the bounds of the array. + test.test_os.OSErrorTests('test_oserror_filename'), # AssertionError: '@test_732_tmp' is not b'@test_732_tmp' + test.test_os.StatAttributeTests('test_stat_result_pickle'), # AssertionError + test.test_os.UtimeTests('test_utime'), # OSError: [WinError 87] The parameter is incorrect. + test.test_os.UtimeTests('test_utime_by_indexed'), # OSError: [WinError 87] The parameter is incorrect. + test.test_os.UtimeTests('test_utime_by_times'), # OSError: [WinError 87] The parameter is incorrect. + test.test_os.UtimeTests('test_utime_directory'), # OSError: [WinError 87] The parameter is incorrect. + test.test_os.Win32KillTests('test_kill_int'), # AssertionError + test.test_os.Win32KillTests('test_kill_sigterm'), # AssertionError + ] + if is_posix: + failing_tests += [ + test.test_os.EnvironTests('test_unset_error'), # ValueError: Environment variable name cannot contain equal character. (Parameter 'variable') + test.test_os.FDInheritanceTests('test_get_inheritable_cloexec'), # AttributeError: 'module' object has no attribute 'get_inheritable' + test.test_os.FDInheritanceTests('test_set_inheritable_cloexec'), # AssertionError + ] + else: + failing_tests += [ + test.test_os.UtimeTests('test_utime_current'), # AssertionError + test.test_os.UtimeTests('test_utime_current_old'), # AssertionError + ] + if sys.version_info < (3, 6): + failing_tests += [ + test.test_os.Win32DeprecatedBytesAPI('test_deprecated'), # AttributeError: 'module' object has no attribute '_isdir' + ] + if sys.version_info >= (3, 6): + failing_tests += [ + test.test_os.BytesWalkTests('test_walk_bottom_up'), # AssertionError + test.test_os.ExecTests('test_execv_with_bad_arglist'), # NameError: name 'execv' is not defined + test.test_os.ExecTests('test_execve_invalid_env'), # NameError: name 'execve' is not defined + test.test_os.ExecTests('test_execve_with_empty_path'), # NameError: name 'execve' is not defined + test.test_os.PathTConverterTests('test_path_t_converter'), # AssertionError + test.test_os.StatAttributeTests('test_file_attributes'), # AssertionError + test.test_os.TestInvalidFD('test_inheritable'), # AttributeError: 'module' object has no attribute 'get_inheritable' + test.test_os.TestScandir('test_attributes'), # OSError: [WinError 1] Incorrect function + test.test_os.TestScandir('test_resource_warning'), # AssertionError: ResourceWarning not triggered + test.test_os.UtimeTests('test_utime_invalid_arguments'), # OSError: [WinError 87] The parameter is incorrect. + test.test_os.WalkTests('test_walk_bottom_up'), # AssertionError + test.test_os.Win32JunctionTests('test_create_junction'), # AttributeError: 'module' object has no attribute 'CreateJunction' + test.test_os.Win32JunctionTests('test_unlink_removes_junction'), # AttributeError: 'module' object has no attribute 'CreateJunction' + ] + if is_posix: + failing_tests += [ + test.test_os.TestScandir('test_removed_dir'), # FileNotFoundError + test.test_os.TestScandir('test_removed_file'), # FileNotFoundError + ] + + skip_tests = [ + # these require symlink support on drive + test.test_os.LinkTests('test_link'), + test.test_os.LinkTests('test_link_bytes'), + test.test_os.LinkTests('test_unicode_name'), + ] + if sys.version_info >= (3, 6): + skip_tests += [ + # SpawnTests seem to create a console + test.test_os.SpawnTests('test_nowait'), + test.test_os.SpawnTests('test_spawnl'), + test.test_os.SpawnTests('test_spawnl_noargs'), + test.test_os.SpawnTests('test_spawnle'), + test.test_os.SpawnTests('test_spawnle_noargs'), + test.test_os.SpawnTests('test_spawnlp'), + test.test_os.SpawnTests('test_spawnlpe'), + test.test_os.SpawnTests('test_spawnv'), + test.test_os.SpawnTests('test_spawnv_noargs'), + test.test_os.SpawnTests('test_spawnve'), + test.test_os.SpawnTests('test_spawnve_bytes'), + test.test_os.SpawnTests('test_spawnve_invalid_env'), + test.test_os.SpawnTests('test_spawnve_noargs'), + test.test_os.SpawnTests('test_spawnvp'), + test.test_os.SpawnTests('test_spawnvpe'), + test.test_os.SpawnTests('test_spawnvpe_invalid_env'), + ] + + return generate_suite(tests, failing_tests, skip_tests) + + else: + return tests + +run_test(__name__) From ba629a5d6466ac77345722d4fa75af0b0ebcea05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Lozier?= Date: Wed, 24 Jun 2026 21:29:25 -0400 Subject: [PATCH 2/3] Bump stdlib --- src/core/IronPython.StdLib | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/IronPython.StdLib b/src/core/IronPython.StdLib index 817e9c810..8602dbfbc 160000 --- a/src/core/IronPython.StdLib +++ b/src/core/IronPython.StdLib @@ -1 +1 @@ -Subproject commit 817e9c81088396e7dd8f495dec62d5584d940ac9 +Subproject commit 8602dbfbc2813b9d93224664454df2c75073a9bd From 857ae629ca8e5c7af9c1cdc5072c070fc25b1a8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Lozier?= Date: Thu, 25 Jun 2026 21:55:14 -0400 Subject: [PATCH 3/3] Skip tests only on exFAT --- src/core/IronPython.StdLib | 2 +- tests/suite/stdlib/test_os.py | 42 ++++++++++++++++++++++------------- 2 files changed, 27 insertions(+), 17 deletions(-) diff --git a/src/core/IronPython.StdLib b/src/core/IronPython.StdLib index 8602dbfbc..7eb9e6fcc 160000 --- a/src/core/IronPython.StdLib +++ b/src/core/IronPython.StdLib @@ -1 +1 @@ -Subproject commit 8602dbfbc2813b9d93224664454df2c75073a9bd +Subproject commit 7eb9e6fccf93c9230450f2ac47e2cdf819f6426f diff --git a/tests/suite/stdlib/test_os.py b/tests/suite/stdlib/test_os.py index 37eb2d830..88143ec93 100644 --- a/tests/suite/stdlib/test_os.py +++ b/tests/suite/stdlib/test_os.py @@ -8,10 +8,22 @@ import sys -from iptest import is_ironpython, generate_suite, run_test, is_posix +from iptest import is_ironpython, generate_suite, run_test, is_posix, is_windows import test.test_os +def is_exfat(): + import clr + clr.AddReference("System.IO.FileSystem.DriveInfo") + import os + import System.IO + try: + drive = os.path.splitdrive(os.getcwd())[0] + format = System.IO.DriveInfo(drive).DriveFormat.lower() + return format == "exfat" + except: + return False + def load_tests(loader, standard_tests, pattern): tests = loader.loadTestsFromModule(test.test_os, pattern=pattern) @@ -31,10 +43,6 @@ def load_tests(loader, standard_tests, pattern): test.test_os.FileTests('test_symlink_keywords'), # IndexError: Index was outside the bounds of the array. test.test_os.OSErrorTests('test_oserror_filename'), # AssertionError: '@test_732_tmp' is not b'@test_732_tmp' test.test_os.StatAttributeTests('test_stat_result_pickle'), # AssertionError - test.test_os.UtimeTests('test_utime'), # OSError: [WinError 87] The parameter is incorrect. - test.test_os.UtimeTests('test_utime_by_indexed'), # OSError: [WinError 87] The parameter is incorrect. - test.test_os.UtimeTests('test_utime_by_times'), # OSError: [WinError 87] The parameter is incorrect. - test.test_os.UtimeTests('test_utime_directory'), # OSError: [WinError 87] The parameter is incorrect. test.test_os.Win32KillTests('test_kill_int'), # AssertionError test.test_os.Win32KillTests('test_kill_sigterm'), # AssertionError ] @@ -44,11 +52,6 @@ def load_tests(loader, standard_tests, pattern): test.test_os.FDInheritanceTests('test_get_inheritable_cloexec'), # AttributeError: 'module' object has no attribute 'get_inheritable' test.test_os.FDInheritanceTests('test_set_inheritable_cloexec'), # AssertionError ] - else: - failing_tests += [ - test.test_os.UtimeTests('test_utime_current'), # AssertionError - test.test_os.UtimeTests('test_utime_current_old'), # AssertionError - ] if sys.version_info < (3, 6): failing_tests += [ test.test_os.Win32DeprecatedBytesAPI('test_deprecated'), # AttributeError: 'module' object has no attribute '_isdir' @@ -75,12 +78,19 @@ def load_tests(loader, standard_tests, pattern): test.test_os.TestScandir('test_removed_file'), # FileNotFoundError ] - skip_tests = [ - # these require symlink support on drive - test.test_os.LinkTests('test_link'), - test.test_os.LinkTests('test_link_bytes'), - test.test_os.LinkTests('test_unicode_name'), - ] + skip_tests = [] + if is_windows and is_exfat(): + skip_tests += [ + test.test_os.LinkTests('test_link'), + test.test_os.LinkTests('test_link_bytes'), + test.test_os.LinkTests('test_unicode_name'), + test.test_os.UtimeTests('test_utime'), + test.test_os.UtimeTests('test_utime_by_indexed'), + test.test_os.UtimeTests('test_utime_by_times'), + test.test_os.UtimeTests('test_utime_directory'), + test.test_os.UtimeTests('test_utime_current'), + test.test_os.UtimeTests('test_utime_current_old'), + ] if sys.version_info >= (3, 6): skip_tests += [ # SpawnTests seem to create a console