diff --git a/src/Build.OM.UnitTests/Definition/ProjectItem_Tests.cs b/src/Build.OM.UnitTests/Definition/ProjectItem_Tests.cs index 66b3cae779b..7ea14319510 100644 --- a/src/Build.OM.UnitTests/Definition/ProjectItem_Tests.cs +++ b/src/Build.OM.UnitTests/Definition/ProjectItem_Tests.cs @@ -13,6 +13,7 @@ using Microsoft.Build.Execution; using Microsoft.Build.Framework; using Microsoft.Build.Shared; +using Microsoft.Build.UnitTests.Shared; using Shouldly; using InvalidProjectFileException = Microsoft.Build.Exceptions.InvalidProjectFileException; using Xunit; @@ -55,7 +56,8 @@ public class ProjectItem_Tests : IDisposable "; - protected TestEnvironment _env; + protected readonly TestEnvironment _env; + private DummyMappedDrive _mappedDrive = null; public ProjectItem_Tests() { @@ -65,6 +67,7 @@ public ProjectItem_Tests() public void Dispose() { _env.Dispose(); + _mappedDrive?.Dispose(); } /// @@ -792,21 +795,22 @@ public void ProjectGetterResultsInDriveEnumerationException(string unevaluatedIn /// /// Project getter that renames an item to a drive enumerating wildcard that results in a logged warning. /// - [ActiveIssue("https://github.com/dotnet/msbuild/issues/7330")] [WindowsOnlyTheory] - [InlineData(@"z:\**\*.log")] - [InlineData(@"z:$(empty)\**\*.log")] - [InlineData(@"z:\**")] - [InlineData(@"z:\\**")] - [InlineData(@"z:\\\\\\\\**")] - [InlineData(@"z:\**\*.cs")] + [InlineData(@"%DRIVE%:\**\*.log")] + [InlineData(@"%DRIVE%:$(empty)\**\*.log")] + [InlineData(@"%DRIVE%:\**")] + [InlineData(@"%DRIVE%:\\**")] + [InlineData(@"%DRIVE%:\\\\\\\\**")] + [InlineData(@"%DRIVE%:\**\*.cs")] public void ProjectGetterResultsInWindowsDriveEnumerationWarning(string unevaluatedInclude) { + var mappedDrive = GetDummyMappedDrive(); + unevaluatedInclude = UpdatePathToMappedDrive(unevaluatedInclude, mappedDrive.MappedDriveLetter); ProjectGetterResultsInDriveEnumerationWarning(unevaluatedInclude); } - [ActiveIssue("https://github.com/dotnet/msbuild/issues/7330")] [UnixOnlyTheory] + [ActiveIssue("https://github.com/dotnet/msbuild/issues/8373")] [InlineData(@"/**/*.log")] [InlineData(@"$(empty)/**/*.log")] [InlineData(@"/$(empty)**/*.log")] @@ -875,32 +879,56 @@ public void ThrowExceptionUponProjectInstanceCreationFromDriveEnumeratingContent /// /// Project instance created from a file that contains a drive enumerating wildcard results in a logged warning on the Windows platform. /// - [ActiveIssue("https://github.com/dotnet/msbuild/issues/7330")] [WindowsOnlyTheory] [InlineData( ImportProjectElement, - @"z:\**\*.targets", + @"%DRIVE%:\**\*.targets", null)] // LazyItem.IncludeOperation [InlineData( ItemWithIncludeAndExclude, - @"z:$(Microsoft_WindowsAzure_EngSys)\**\*", + @"%DRIVE%:$(Microsoft_WindowsAzure_EngSys)\**\*", @"$(Microsoft_WindowsAzure_EngSys)\*.pdb;$(Microsoft_WindowsAzure_EngSys)\Microsoft.WindowsAzure.Storage.dll;$(Microsoft_WindowsAzure_EngSys)\Certificates\**\*")] // LazyItem.IncludeOperation for Exclude [InlineData( ItemWithIncludeAndExclude, @"$(EmptyProperty)\*.cs", - @"z:\$(Microsoft_WindowsAzure_EngSys)**")] + @"%DRIVE%:\$(Microsoft_WindowsAzure_EngSys)**")] public void LogWindowsWarningUponProjectInstanceCreationFromDriveEnumeratingContent(string content, string placeHolder, string excludePlaceHolder = null) { + var mappedDrive = GetDummyMappedDrive(); + placeHolder = UpdatePathToMappedDrive(placeHolder, mappedDrive.MappedDriveLetter); + excludePlaceHolder = UpdatePathToMappedDrive(excludePlaceHolder, mappedDrive.MappedDriveLetter); content = string.Format(content, placeHolder, excludePlaceHolder); CleanContentsAndCreateProjectInstanceFromFileWithDriveEnumeratingWildcard(content, false); } - [ActiveIssue("https://github.com/dotnet/msbuild/issues/7330")] + private DummyMappedDrive GetDummyMappedDrive() + { + if (NativeMethods.IsWindows) + { + // let's create the mapped drive only once it's needed by any test, then let's reuse; + _mappedDrive ??= new DummyMappedDrive(); + } + + return _mappedDrive; + } + + private static string UpdatePathToMappedDrive(string path, char driveLetter) + { + const string drivePlaceholder = "%DRIVE%"; + // if this seems to be rooted path - replace with the dummy mount + if (!string.IsNullOrEmpty(path) && path.StartsWith(drivePlaceholder)) + { + path = driveLetter + path.Substring(drivePlaceholder.Length); + } + return path; + } + [UnixOnlyTheory] + [ActiveIssue("https://github.com/dotnet/msbuild/issues/8373")] [InlineData( ImportProjectElement, @"\**\*.targets", diff --git a/src/Build.OM.UnitTests/Microsoft.Build.Engine.OM.UnitTests.csproj b/src/Build.OM.UnitTests/Microsoft.Build.Engine.OM.UnitTests.csproj index 44cc3c47e4e..14c83442af4 100644 --- a/src/Build.OM.UnitTests/Microsoft.Build.Engine.OM.UnitTests.csproj +++ b/src/Build.OM.UnitTests/Microsoft.Build.Engine.OM.UnitTests.csproj @@ -81,6 +81,8 @@ + + App.config Designer diff --git a/src/UnitTests.Shared/DriveMapping.cs b/src/UnitTests.Shared/DriveMapping.cs new file mode 100644 index 00000000000..81324086548 --- /dev/null +++ b/src/UnitTests.Shared/DriveMapping.cs @@ -0,0 +1,92 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#nullable enable +using System; +using System.Runtime.InteropServices; +using System.Runtime.Versioning; +using System.Text; + +namespace Microsoft.Build.UnitTests.Shared; + +internal static class DriveMapping +{ + private const int ERROR_FILE_NOT_FOUND = 2; + private const int ERROR_INSUFFICIENT_BUFFER = 122; + private const int DDD_REMOVE_DEFINITION = 2; + private const int DDD_NO_FLAG = 0; + // extra space for '\??\'. Not counting for long paths support in tests. + private const int MAX_PATH = 259; + + /// + /// Windows specific. Maps path to a requested drive. + /// + /// Drive letter + /// Path to be mapped + [SupportedOSPlatform("windows")] + public static void MapDrive(char letter, string path) + { + if (!DefineDosDevice(DDD_NO_FLAG, ToDeviceName(letter), path)) + { + NativeMethodsShared.ThrowExceptionForErrorCode(Marshal.GetLastWin32Error()); + } + } + + /// + /// Windows specific. Unmaps drive mapping. + /// + /// Drive letter. + [SupportedOSPlatform("windows")] + public static void UnmapDrive(char letter) + { + if (!DefineDosDevice(DDD_REMOVE_DEFINITION, ToDeviceName(letter), null)) + { + NativeMethodsShared.ThrowExceptionForErrorCode(Marshal.GetLastWin32Error()); + } + } + + /// + /// Windows specific. Fetches path mapped under specific drive letter. + /// + /// Drive letter. + /// Path mapped under specified letter. Empty string if mapping not found. + [SupportedOSPlatform("windows")] + public static string GetDriveMapping(char letter) + { + // since this is just for test purposes - let's not overcomplicate with long paths support + char[] buffer = new char[MAX_PATH]; + + while (QueryDosDevice(ToDeviceName(letter), buffer, buffer.Length) == 0) + { + // Return empty string if the drive is not mapped + int err = Marshal.GetLastWin32Error(); + if (err == ERROR_FILE_NOT_FOUND) + { + return string.Empty; + } + + if (err != ERROR_INSUFFICIENT_BUFFER) + { + NativeMethodsShared.ThrowExceptionForErrorCode(err); + } + + buffer = new char[buffer.Length * 4]; + } + + // Translate from the native path semantic - starting with '\??\' + return new string(buffer, 4, buffer.Length - 4); + } + + private static string ToDeviceName(char letter) + { + return $"{char.ToUpper(letter)}:"; + } + + [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)] + [SupportedOSPlatform("windows")] + private static extern bool DefineDosDevice([In] int flags, [In] string deviceName, [In] string? path); + + [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)] + [SupportedOSPlatform("windows")] + private static extern int QueryDosDevice([In] string deviceName, [Out] char[] buffer, [In] int bufSize); +} diff --git a/src/UnitTests.Shared/DummyMappedDrive.cs b/src/UnitTests.Shared/DummyMappedDrive.cs new file mode 100644 index 00000000000..ec7df37a00d --- /dev/null +++ b/src/UnitTests.Shared/DummyMappedDrive.cs @@ -0,0 +1,87 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#nullable enable +using System; +using System.Diagnostics; +using System.IO; +using System.Runtime.Versioning; + +namespace Microsoft.Build.UnitTests.Shared; + +/// +/// Windows specific. Class managing system resource - temporary local path mapped to available drive letter. +/// +public class DummyMappedDrive : IDisposable +{ + public char MappedDriveLetter { get; init; } = 'z'; + private readonly string _mappedPath; + private readonly bool _mapped; + + public DummyMappedDrive() + { + _mappedPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + + if (!NativeMethodsShared.IsWindows) + { + return; + } + + Directory.CreateDirectory(_mappedPath); + File.Create(Path.Combine(_mappedPath, "x")).Dispose(); + + for (char driveLetter = 'z'; driveLetter >= 'a'; driveLetter--) + { + if (DriveMapping.GetDriveMapping(driveLetter) == string.Empty) + { + DriveMapping.MapDrive(driveLetter, _mappedPath); + MappedDriveLetter = driveLetter; + _mapped = true; + return; + } + } + } + + private void ReleaseUnmanagedResources(bool disposing) + { + Exception? e = null; + if (Directory.Exists(_mappedPath)) + { + try + { + Directory.Delete(_mappedPath, true); + } + catch (Exception exc) + { + e = exc; + Debug.Fail("Exception in DummyMappedDrive finalizer: " + e.ToString()); + } + } + + if (_mapped && NativeMethodsShared.IsWindows) + { + try + { + DriveMapping.UnmapDrive(MappedDriveLetter); + } + catch (Exception exc) + { + e = e == null ? exc : new AggregateException(e, exc); + Debug.Fail("Exception in DummyMappedDrive finalizer: " + e.ToString()); + } + } + + if (disposing && e != null) + { + throw e; + } + } + + public void Dispose() + { + ReleaseUnmanagedResources(true); + GC.SuppressFinalize(this); + } + + ~DummyMappedDrive() => ReleaseUnmanagedResources(false); +}