Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ability to create temp mapped drive for integration tests #8366

Merged
merged 9 commits into from
Feb 8, 2023
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 35 additions & 9 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,7 +795,6 @@ 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")]
Expand All @@ -802,10 +804,11 @@ public void ProjectGetterResultsInDriveEnumerationException(string unevaluatedIn
[InlineData(@"z:\**\*.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]
[InlineData(@"/**/*.log")]
[InlineData(@"$(empty)/**/*.log")]
Expand Down Expand Up @@ -875,7 +878,6 @@ 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,
Expand All @@ -895,16 +897,40 @@ public void ThrowExceptionUponProjectInstanceCreationFromDriveEnumeratingContent
@"z:\$(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)
{
// if this seems to be rooted path - replace with the dummy mount
if (!string.IsNullOrEmpty(path) && path.Length > 1 && path[1] == ':')
{
path = driveLetter + path.Substring(1);
}
return path;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If path is empty, should we return "" or driveLetter + ":"? I'm curious if this could artificially make the drive enumeration tests pass.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Empty paths are valid test case scenarios (e.g. unspecified exclude pattern). So it intentionaly leaves unspecified or unrooted paths unaffected

}

[UnixOnlyTheory]
[InlineData(
ImportProjectElement,
@"\**\*.targets",
null)]
// Failing: https://github.com/dotnet/msbuild/issues/8373
// [InlineData(
// ImportProjectElement,
// @"\**\*.targets",
// null)]

// LazyItem.IncludeOperation
[InlineData(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@
</Compile>
<Compile Include="..\Shared\ProcessExtensions.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
84 changes: 84 additions & 0 deletions src/UnitTests.Shared/DriveMapping.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// 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)
JanKrivanek marked this conversation as resolved.
Show resolved Hide resolved
{
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
JanKrivanek marked this conversation as resolved.
Show resolved Hide resolved
char[] buffer = new char[MAX_PATH];
if (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;
}
NativeMethodsShared.ThrowExceptionForErrorCode(err);
}
// Translate from the native path semantic - starting with '\??\'
return new string(buffer, 4, buffer.Length - 4);
}

private static string ToDeviceName(char letter)
{
return new string(char.ToUpper(letter), 1) + ":";
JanKrivanek marked this conversation as resolved.
Show resolved Hide resolved
}

[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);
JanKrivanek marked this conversation as resolved.
Show resolved Hide resolved
}
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);
}