From 51fd5396c219831d33144135cee086935f33c929 Mon Sep 17 00:00:00 2001 From: hamdiakin Date: Tue, 20 Oct 2026 00:17:42 +0300 Subject: [PATCH 1/3] Tests: handle unavailable Windows symlink privilege --- CONTRIBUTING.md | 8 +++++++- TruePath.Tests/AbsolutePathTests.cs | 13 +------------ TruePath.Tests/Utils.cs | 19 +++++++++++++++++++ 3 files changed, 27 insertions(+), 13 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d475752..d9e6b46 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -26,7 +26,11 @@ Use the following shell command: ```console $ dotnet test ``` -Tests involving symlink creation require elevated privileges on Windows, as this is enforced at the OS level. + +On Windows, tests that create symbolic links require either [Developer Mode][windows-developer-mode] or an elevated +process. Developer Mode lets an unelevated process create symbolic links, as documented for the +[`CreateSymbolicLinkW`][create-symbolic-link] API. When neither is available, these tests are skipped locally; CI +runners fail instead, ensuring that the tests are not silently skipped in automation. Test Documentation ------------------ @@ -74,5 +78,7 @@ $ dotnet fsi scripts/github-actions.fsx ``` [dotnet-sdk]: https://dotnet.microsoft.com/en-us/download +[create-symbolic-link]: https://learn.microsoft.com/windows/win32/api/winbase/nf-winbase-createsymboliclinkw [powershell]: https://learn.microsoft.com/en-us/powershell/scripting/install/installing-powershell [reuse]: https://reuse.software/ +[windows-developer-mode]: https://learn.microsoft.com/windows/apps/get-started/enable-your-device-for-development diff --git a/TruePath.Tests/AbsolutePathTests.cs b/TruePath.Tests/AbsolutePathTests.cs index 78c3e01..a9284b0 100644 --- a/TruePath.Tests/AbsolutePathTests.cs +++ b/TruePath.Tests/AbsolutePathTests.cs @@ -135,18 +135,7 @@ public void ReadKind_IsSymlink() var tempDirectoryInfo = Path.GetTempPath(); - try - { - Directory.CreateSymbolicLink(currentDirectory, tempDirectoryInfo); - } - catch (IOException e) when (OperatingSystem.IsWindows() && !Utils.RunsOnCi()) - { - if (e.Message.Contains("privilege") && (e.StackTrace?.Contains("CreateSymbolicLink") ?? false)) - { - Assert.Skip("Tests involving symlink creation require elevated privileges on Windows, as this is enforced at the OS level."); - } - else throw; - } + Utils.CreateSymbolicLinkOrSkip(() => Directory.CreateSymbolicLink(currentDirectory, tempDirectoryInfo)); // Act var kind = localPath.ReadKind(); diff --git a/TruePath.Tests/Utils.cs b/TruePath.Tests/Utils.cs index 70da5b6..4b088ac 100644 --- a/TruePath.Tests/Utils.cs +++ b/TruePath.Tests/Utils.cs @@ -8,6 +8,8 @@ namespace TruePath.Tests; public static class Utils { + private const int ErrorPrivilegeNotHeldHResult = unchecked((int)0x80070522); + internal static string ToNonCanonicalCase(this string path) { var result = new char[path.Length]; @@ -44,6 +46,23 @@ internal static bool RunsOnCi() return Environment.GetEnvironmentVariable("CI") is not null; } + internal static void CreateSymbolicLinkOrSkip(Action createSymbolicLink) + { + try + { + createSymbolicLink(); + } + catch (IOException e) when (OperatingSystem.IsWindows() && e.HResult == ErrorPrivilegeNotHeldHResult) + { + Assert.False( + RunsOnCi(), + "CI runners must allow symbolic link creation so these tests are not skipped silently."); + Assert.Skip( + "Creating symbolic links on Windows requires Developer Mode or an elevated process. " + + "See CONTRIBUTING.md for setup instructions."); + } + } + /// /// Changes the current directory to , and restores the previous one on disposal. /// From 517229db3bb5136393cf6ec4d0dac2a3a9735fd1 Mon Sep 17 00:00:00 2001 From: Friedrich von Never Date: Sun, 25 Oct 2026 00:17:47 +0200 Subject: [PATCH 2/3] (#245) Tests: use ERROR_PRIVILEGE_NOT_HELD value from CsWin32 --- TruePath.Tests/Utils.cs | 7 ++++--- TruePath/NativeMethods.txt | 2 ++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/TruePath.Tests/Utils.cs b/TruePath.Tests/Utils.cs index 4b088ac..a3db168 100644 --- a/TruePath.Tests/Utils.cs +++ b/TruePath.Tests/Utils.cs @@ -2,14 +2,14 @@ // // SPDX-License-Identifier: MIT +using Windows.Win32; +using Windows.Win32.Foundation; using Xunit.v3; namespace TruePath.Tests; public static class Utils { - private const int ErrorPrivilegeNotHeldHResult = unchecked((int)0x80070522); - internal static string ToNonCanonicalCase(this string path) { var result = new char[path.Length]; @@ -52,7 +52,8 @@ internal static void CreateSymbolicLinkOrSkip(Action createSymbolicLink) { createSymbolicLink(); } - catch (IOException e) when (OperatingSystem.IsWindows() && e.HResult == ErrorPrivilegeNotHeldHResult) + catch (IOException e) when (OperatingSystem.IsWindows() + && e.HResult == PInvoke.HRESULT_FROM_WIN32(WIN32_ERROR.ERROR_PRIVILEGE_NOT_HELD).Value) { Assert.False( RunsOnCi(), diff --git a/TruePath/NativeMethods.txt b/TruePath/NativeMethods.txt index f638895..591618f 100644 --- a/TruePath/NativeMethods.txt +++ b/TruePath/NativeMethods.txt @@ -7,4 +7,6 @@ FILE_ACCESS_RIGHTS FILE_ATTRIBUTE_TAG_INFO GetFileInformationByHandleEx GetFinalPathNameByHandle +HRESULT_FROM_WIN32 IO_REPARSE_TAG_MOUNT_POINT +WIN32_ERROR From 83c42157be2f7547b91bf5d1d87d7387e154c530 Mon Sep 17 00:00:00 2001 From: Friedrich von Never Date: Sun, 25 Oct 2026 00:18:54 +0200 Subject: [PATCH 3/3] (#245) Docs: sort the links --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d9e6b46..ddbd46c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -77,8 +77,8 @@ Then run the following shell command: $ dotnet fsi scripts/github-actions.fsx ``` -[dotnet-sdk]: https://dotnet.microsoft.com/en-us/download [create-symbolic-link]: https://learn.microsoft.com/windows/win32/api/winbase/nf-winbase-createsymboliclinkw +[dotnet-sdk]: https://dotnet.microsoft.com/en-us/download [powershell]: https://learn.microsoft.com/en-us/powershell/scripting/install/installing-powershell [reuse]: https://reuse.software/ [windows-developer-mode]: https://learn.microsoft.com/windows/apps/get-started/enable-your-device-for-development