Skip to content

Commit

Permalink
Add ability to create temp mapped drive for integration tests (#8366)
Browse files Browse the repository at this point in the history
Fixes #7330
(plus one subtask of #8329)

Tests only change (no production code affected)

Context
Drive enumeration integration tests need to simulate attempt to enumerate whole drive.
To prevent security and test runtime considerations - a dummy folder is created and mapped to a free letter to be offered to test as a drive for enumeration.

Changes Made
Added utility for mapping drives and mounted to affected unit tests.
  • Loading branch information
JanKrivanek authored Feb 8, 2023
1 parent f0f9c50 commit 51df476
Show file tree
Hide file tree
Showing 4 changed files with 223 additions and 14 deletions.
56 changes: 42 additions & 14 deletions src/Build.OM.UnitTests/Definition/ProjectItem_Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -55,7 +56,8 @@ public class ProjectItem_Tests : IDisposable
</Project>
";

protected TestEnvironment _env;
protected readonly TestEnvironment _env;
private DummyMappedDrive _mappedDrive = null;

public ProjectItem_Tests()
{
Expand All @@ -65,6 +67,7 @@ public ProjectItem_Tests()
public void Dispose()
{
_env.Dispose();
_mappedDrive?.Dispose();
}

/// <summary>
Expand Down Expand Up @@ -792,21 +795,22 @@ public void ProjectGetterResultsInDriveEnumerationException(string unevaluatedIn
/// <summary>
/// Project getter that renames an item to a drive enumerating wildcard that results in a logged warning.
/// </summary>
[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")]
Expand Down Expand Up @@ -875,32 +879,56 @@ public void ThrowExceptionUponProjectInstanceCreationFromDriveEnumeratingContent
/// <summary>
/// Project instance created from a file that contains a drive enumerating wildcard results in a logged warning on the Windows platform.
/// </summary>
[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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@
<Compile Include="..\Shared\ProcessExtensions.cs" />
<Compile Include="..\UnitTests.Shared\EnvironmentProvider.cs" />
<Compile Include="..\UnitTests.Shared\RunnerUtilities.cs" />
<Compile Include="..\UnitTests.Shared\DriveMapping.cs" />
<Compile Include="..\UnitTests.Shared\DummyMappedDrive.cs" />
<None Include="..\Shared\UnitTests\App.config">
<Link>App.config</Link>
<SubType>Designer</SubType>
Expand Down
92 changes: 92 additions & 0 deletions src/UnitTests.Shared/DriveMapping.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Windows specific. Maps path to a requested drive.
/// </summary>
/// <param name="letter">Drive letter</param>
/// <param name="path">Path to be mapped</param>
[SupportedOSPlatform("windows")]
public static void MapDrive(char letter, string path)
{
if (!DefineDosDevice(DDD_NO_FLAG, ToDeviceName(letter), path))
{
NativeMethodsShared.ThrowExceptionForErrorCode(Marshal.GetLastWin32Error());
}
}

/// <summary>
/// Windows specific. Unmaps drive mapping.
/// </summary>
/// <param name="letter">Drive letter.</param>
[SupportedOSPlatform("windows")]
public static void UnmapDrive(char letter)
{
if (!DefineDosDevice(DDD_REMOVE_DEFINITION, ToDeviceName(letter), null))
{
NativeMethodsShared.ThrowExceptionForErrorCode(Marshal.GetLastWin32Error());
}
}

/// <summary>
/// Windows specific. Fetches path mapped under specific drive letter.
/// </summary>
/// <param name="letter">Drive letter.</param>
/// <returns>Path mapped under specified letter. Empty string if mapping not found.</returns>
[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);
}
87 changes: 87 additions & 0 deletions src/UnitTests.Shared/DummyMappedDrive.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Windows specific. Class managing system resource - temporary local path mapped to available drive letter.
/// </summary>
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);
}

0 comments on commit 51df476

Please sign in to comment.