diff --git a/src/libraries/Common/tests/System/IO/VirtualDriveHelper.Windows.cs b/src/libraries/Common/tests/System/IO/VirtualDriveHelper.Windows.cs new file mode 100644 index 0000000000000..9a221a2488c7c --- /dev/null +++ b/src/libraries/Common/tests/System/IO/VirtualDriveHelper.Windows.cs @@ -0,0 +1,148 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Runtime.Versioning; + +namespace System.IO +{ + // Adds test helper APIs to manipulate Windows virtual drives via SUBST. + [SupportedOSPlatform("windows")] + public class VirtualDriveHelper : IDisposable + { + // Temporary Windows directory that can be mounted to a drive letter using the subst command + private string? _virtualDriveTargetDir = null; + // Windows drive letter that points to a mounted directory using the subst command + private char _virtualDriveLetter = default; + + /// + /// If there is a SUBST'ed drive, Dispose unmounts it to free the drive letter. + /// + public void Dispose() + { + try + { + if (VirtualDriveLetter != default) + { + DeleteVirtualDrive(VirtualDriveLetter); + Directory.Delete(VirtualDriveTargetDir, recursive: true); + } + } + catch { } // avoid exceptions on dispose + } + + /// + /// Returns the path of a folder that is to be mounted using SUBST. + /// + public string VirtualDriveTargetDir + { + get + { + if (_virtualDriveTargetDir == null) + { + // Create a folder inside the temp directory so that it can be mounted to a drive letter with subst + _virtualDriveTargetDir = Path.Join(Path.GetTempPath(), Path.GetRandomFileName()); + Directory.CreateDirectory(_virtualDriveTargetDir); + } + + return _virtualDriveTargetDir; + } + } + + /// + /// Returns the drive letter of a drive letter that represents a mounted folder using SUBST. + /// + public char VirtualDriveLetter + { + get + { + if (_virtualDriveLetter == default) + { + // Mount the folder to a drive letter + _virtualDriveLetter = CreateVirtualDrive(VirtualDriveTargetDir); + } + return _virtualDriveLetter; + } + } + + /// + /// On Windows, mounts a folder to an assigned virtual drive letter using the subst command. + /// subst is not available in Windows Nano. + /// + private static char CreateVirtualDrive(string targetDir) + { + char driveLetter = GetNextAvailableDriveLetter(); + bool success = RunProcess(CreateProcessStartInfo("cmd", "/c", SubstPath, $"{driveLetter}:", targetDir)); + if (!success || !DriveInfo.GetDrives().Any(x => x.Name[0] == driveLetter)) + { + throw new InvalidOperationException($"Could not create virtual drive {driveLetter}: with subst"); + } + return driveLetter; + + // Finds the next unused drive letter and returns it. + char GetNextAvailableDriveLetter() + { + List existingDrives = DriveInfo.GetDrives().Select(x => x.Name[0]).ToList(); + + // A,B are reserved, C is usually reserved + IEnumerable range = Enumerable.Range('D', 'Z' - 'D'); + IEnumerable castRange = range.Select(x => Convert.ToChar(x)); + IEnumerable allDrivesLetters = castRange.Except(existingDrives); + + if (!allDrivesLetters.Any()) + { + throw new ArgumentOutOfRangeException("No drive letters available"); + } + + return allDrivesLetters.First(); + } + } + + /// + /// On Windows, unassigns the specified virtual drive letter from its mounted folder. + /// + private static void DeleteVirtualDrive(char driveLetter) + { + bool success = RunProcess(CreateProcessStartInfo("cmd", "/c", SubstPath, "/d", $"{driveLetter}:")); + if (!success || DriveInfo.GetDrives().Any(x => x.Name[0] == driveLetter)) + { + throw new InvalidOperationException($"Could not delete virtual drive {driveLetter}: with subst"); + } + } + + private static ProcessStartInfo CreateProcessStartInfo(string fileName, params string[] arguments) + { + var info = new ProcessStartInfo + { + FileName = fileName, + UseShellExecute = false, + RedirectStandardOutput = true + }; + + foreach (var argument in arguments) + { + info.ArgumentList.Add(argument); + } + + return info; + } + + private static bool RunProcess(ProcessStartInfo startInfo) + { + using var process = Process.Start(startInfo); + process.WaitForExit(); + return process.ExitCode == 0; + } + + private static string SubstPath + { + get + { + string systemRoot = Environment.GetEnvironmentVariable("SystemRoot") ?? @"C:\Windows"; + return Path.Join(systemRoot, "System32", "subst.exe"); + } + } + } +} \ No newline at end of file diff --git a/src/libraries/System.IO.FileSystem.DriveInfo/tests/DriveInfo.Unix.Tests.cs b/src/libraries/System.IO.FileSystem.DriveInfo/tests/DriveInfo.Unix.Tests.cs index 11c6f45cab9a7..e7723229c0d2d 100644 --- a/src/libraries/System.IO.FileSystem.DriveInfo/tests/DriveInfo.Unix.Tests.cs +++ b/src/libraries/System.IO.FileSystem.DriveInfo/tests/DriveInfo.Unix.Tests.cs @@ -5,7 +5,7 @@ using System.Linq; using Xunit; -namespace System.IO.FileSystem.DriveInfoTests +namespace System.IO.FileSystem.Tests { public partial class DriveInfoUnixTests { diff --git a/src/libraries/System.IO.FileSystem.DriveInfo/tests/DriveInfo.Windows.Tests.cs b/src/libraries/System.IO.FileSystem.DriveInfo/tests/DriveInfo.Windows.Tests.cs index c52980953e9e0..06dee252d848e 100644 --- a/src/libraries/System.IO.FileSystem.DriveInfo/tests/DriveInfo.Windows.Tests.cs +++ b/src/libraries/System.IO.FileSystem.DriveInfo/tests/DriveInfo.Windows.Tests.cs @@ -1,18 +1,17 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; -using System.IO; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Runtime.InteropServices; using System.Security; -using Xunit; using System.Text; +using Xunit; -namespace System.IO.FileSystem.DriveInfoTests +namespace System.IO.FileSystem.Tests { + [PlatformSpecific(TestPlatforms.Windows)] public class DriveInfoWindowsTests { [Theory] @@ -35,7 +34,6 @@ public void Ctor_InvalidPath_ThrowsArgumentException(string driveName) } [Fact] - [PlatformSpecific(TestPlatforms.Windows)] public void TestConstructor() { string[] variableInput = { "{0}", "{0}", "{0}:", "{0}:", @"{0}:\", @"{0}:\\", "{0}://" }; @@ -54,7 +52,6 @@ public void TestConstructor() } [Fact] - [PlatformSpecific(TestPlatforms.Windows)] public void TestGetDrives() { var validExpectedDrives = GetValidDriveLettersOnMachine(); @@ -97,7 +94,6 @@ public void TestDriveProperties_AppContainer() } [Fact] - [PlatformSpecific(TestPlatforms.Windows)] public void TestDriveFormat() { DriveInfo validDrive = DriveInfo.GetDrives().Where(d => d.DriveType == DriveType.Fixed).First(); @@ -124,7 +120,6 @@ public void TestDriveFormat() } [Fact] - [PlatformSpecific(TestPlatforms.Windows)] public void TestDriveType() { var validDrive = DriveInfo.GetDrives().Where(d => d.DriveType == DriveType.Fixed).First(); @@ -137,7 +132,6 @@ public void TestDriveType() } [Fact] - [PlatformSpecific(TestPlatforms.Windows)] public void TestValidDiskSpaceProperties() { bool win32Result; @@ -169,7 +163,6 @@ public void TestValidDiskSpaceProperties() } [Fact] - [PlatformSpecific(TestPlatforms.Windows)] public void TestInvalidDiskProperties() { string invalidDriveName = GetInvalidDriveLettersOnMachine().First().ToString(); @@ -189,7 +182,6 @@ public void TestInvalidDiskProperties() } [Fact] - [PlatformSpecific(TestPlatforms.Windows)] public void GetVolumeLabel_Returns_CorrectLabel() { void DoDriveCheck() @@ -225,7 +217,6 @@ void DoDriveCheck() } [Fact] - [PlatformSpecific(TestPlatforms.Windows)] public void SetVolumeLabel_Roundtrips() { DriveInfo drive = DriveInfo.GetDrives().Where(d => d.DriveType == DriveType.Fixed).First(); @@ -246,7 +237,6 @@ public void SetVolumeLabel_Roundtrips() } [Fact] - [PlatformSpecific(TestPlatforms.Windows)] public void VolumeLabelOnNetworkOrCdRom_Throws() { // Test setting the volume label on a Network or CD-ROM diff --git a/src/libraries/System.IO.FileSystem.DriveInfo/tests/System.IO.FileSystem.DriveInfo.Tests.csproj b/src/libraries/System.IO.FileSystem.DriveInfo/tests/System.IO.FileSystem.DriveInfo.Tests.csproj index be64739138482..3d282e1aea7cd 100644 --- a/src/libraries/System.IO.FileSystem.DriveInfo/tests/System.IO.FileSystem.DriveInfo.Tests.csproj +++ b/src/libraries/System.IO.FileSystem.DriveInfo/tests/System.IO.FileSystem.DriveInfo.Tests.csproj @@ -5,5 +5,7 @@ + + \ No newline at end of file diff --git a/src/libraries/System.IO.FileSystem.DriveInfo/tests/VirtualDrives.Windows.Tests.cs b/src/libraries/System.IO.FileSystem.DriveInfo/tests/VirtualDrives.Windows.Tests.cs new file mode 100644 index 0000000000000..57fe6141fc8a9 --- /dev/null +++ b/src/libraries/System.IO.FileSystem.DriveInfo/tests/VirtualDrives.Windows.Tests.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Security; +using System.Text; +using Xunit; + +namespace System.IO.FileSystem.Tests +{ + // Separate class from the rest of the DriveInfo tests to prevent adding an extra virtual drive to GetDrives(). + public class DriveInfoVirtualDriveTests + { + // Cannot set the volume label on a SUBST'ed folder + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsSubstAvailable))] + [PlatformSpecific(TestPlatforms.Windows)] + public void SetVolumeLabel_OnVirtualDrive_Throws() + { + using VirtualDriveHelper virtualDrive = new(); + char letter = virtualDrive.VirtualDriveLetter; // Trigger calling subst + DriveInfo drive = DriveInfo.GetDrives().Where(d => d.RootDirectory.FullName[0] == letter).FirstOrDefault(); + Assert.NotNull(drive); + Assert.Throws(() => drive.VolumeLabel = "impossible"); + } + } +} \ No newline at end of file diff --git a/src/libraries/System.IO.FileSystem/tests/PortedCommon/ReparsePointUtilities.cs b/src/libraries/System.IO.FileSystem/tests/PortedCommon/ReparsePointUtilities.cs index 8c7f3608d35ce..87c96cfed7916 100644 --- a/src/libraries/System.IO.FileSystem/tests/PortedCommon/ReparsePointUtilities.cs +++ b/src/libraries/System.IO.FileSystem/tests/PortedCommon/ReparsePointUtilities.cs @@ -61,61 +61,6 @@ public static bool CreateJunction(string junctionPath, string targetPath) return RunProcess(CreateProcessStartInfo("cmd", "/c", "mklink", "/J", junctionPath, targetPath)); } - /// - /// On Windows, mounts a folder to an assigned virtual drive letter using the subst command. - /// subst is not available in Windows Nano. - /// - public static char CreateVirtualDrive(string targetDir) - { - if (!OperatingSystem.IsWindows()) - { - throw new PlatformNotSupportedException(); - } - - char driveLetter = GetNextAvailableDriveLetter(); - bool success = RunProcess(CreateProcessStartInfo("cmd", "/c", SubstPath, $"{driveLetter}:", targetDir)); - if (!success || !DriveInfo.GetDrives().Any(x => x.Name[0] == driveLetter)) - { - throw new InvalidOperationException($"Could not create virtual drive {driveLetter}: with subst"); - } - return driveLetter; - - // Finds the next unused drive letter and returns it. - char GetNextAvailableDriveLetter() - { - List existingDrives = DriveInfo.GetDrives().Select(x => x.Name[0]).ToList(); - - // A,B are reserved, C is usually reserved - IEnumerable range = Enumerable.Range('D', 'Z' - 'D'); - IEnumerable castRange = range.Select(x => Convert.ToChar(x)); - IEnumerable allDrivesLetters = castRange.Except(existingDrives); - - if (!allDrivesLetters.Any()) - { - throw new ArgumentOutOfRangeException("No drive letters available"); - } - - return allDrivesLetters.First(); - } - } - - /// - /// On Windows, unassigns the specified virtual drive letter from its mounted folder. - /// - public static void DeleteVirtualDrive(char driveLetter) - { - if (!OperatingSystem.IsWindows()) - { - throw new PlatformNotSupportedException(); - } - - bool success = RunProcess(CreateProcessStartInfo("cmd", "/c", SubstPath, "/d", $"{driveLetter}:")); - if (!success || DriveInfo.GetDrives().Any(x => x.Name[0] == driveLetter)) - { - throw new InvalidOperationException($"Could not delete virtual drive {driveLetter}: with subst"); - } - } - public static void Mount(string volumeName, string mountPoint) { if (volumeName[volumeName.Length - 1] != Path.DirectorySeparatorChar) @@ -173,21 +118,6 @@ private static bool RunProcess(ProcessStartInfo startInfo) return process.ExitCode == 0; } - private static string SubstPath - { - get - { - if (!OperatingSystem.IsWindows()) - { - throw new PlatformNotSupportedException(); - } - - string systemRoot = Environment.GetEnvironmentVariable("SystemRoot") ?? @"C:\Windows"; - string system32 = Path.Join(systemRoot, "System32"); - return Path.Join(system32, "subst.exe"); - } - } - /// For standalone debugging help. Change Main0 to Main public static void Main0(string[] args) { diff --git a/src/libraries/System.IO.FileSystem/tests/System.IO.FileSystem.Tests.csproj b/src/libraries/System.IO.FileSystem/tests/System.IO.FileSystem.Tests.csproj index abf73ff35986c..5180426ca1044 100644 --- a/src/libraries/System.IO.FileSystem/tests/System.IO.FileSystem.Tests.csproj +++ b/src/libraries/System.IO.FileSystem/tests/System.IO.FileSystem.Tests.csproj @@ -97,6 +97,7 @@ + diff --git a/src/libraries/System.IO.FileSystem/tests/VirtualDriveSymbolicLinks.Windows.cs b/src/libraries/System.IO.FileSystem/tests/VirtualDriveSymbolicLinks.Windows.cs index d88425086b702..fb9997e6ab17e 100644 --- a/src/libraries/System.IO.FileSystem/tests/VirtualDriveSymbolicLinks.Windows.cs +++ b/src/libraries/System.IO.FileSystem/tests/VirtualDriveSymbolicLinks.Windows.cs @@ -11,17 +11,11 @@ namespace System.IO.Tests [ConditionalClass(typeof(PlatformDetection), nameof(PlatformDetection.IsSubstAvailable))] public class VirtualDrive_SymbolicLinks : BaseSymbolicLinks { + private VirtualDriveHelper VirtualDrive { get; } = new VirtualDriveHelper(); + protected override void Dispose(bool disposing) { - try - { - if (VirtualDriveLetter != default) - { - MountHelper.DeleteVirtualDrive(VirtualDriveLetter); - Directory.Delete(VirtualDriveTargetDir, recursive: true); - } - } - catch { } // avoid exceptions on dispose + VirtualDrive.Dispose(); base.Dispose(disposing); } @@ -210,38 +204,6 @@ public void VirtualDrive_SymbolicLinks_WithIndirection( Assert.Equal(expectedTargetDirectoryInfoFullName, targetFileInfoFromDirectory.FullName); } - private string GetVirtualOrRealPath(bool condition) => condition ? $"{VirtualDriveLetter}:" : VirtualDriveTargetDir; - - // Temporary Windows directory that can be mounted to a drive letter using the subst command - private string? _virtualDriveTargetDir = null; - private string VirtualDriveTargetDir - { - get - { - if (_virtualDriveTargetDir == null) - { - // Create a folder inside the temp directory so that it can be mounted to a drive letter with subst - _virtualDriveTargetDir = Path.Join(Path.GetTempPath(), GetRandomDirName()); - Directory.CreateDirectory(_virtualDriveTargetDir); - } - - return _virtualDriveTargetDir; - } - } - - // Windows drive letter that points to a mounted directory using the subst command - private char _virtualDriveLetter = default; - private char VirtualDriveLetter - { - get - { - if (_virtualDriveLetter == default) - { - // Mount the folder to a drive letter - _virtualDriveLetter = MountHelper.CreateVirtualDrive(VirtualDriveTargetDir); - } - return _virtualDriveLetter; - } - } + private string GetVirtualOrRealPath(bool condition) => condition ? $"{VirtualDrive.VirtualDriveLetter}:" : VirtualDrive.VirtualDriveTargetDir; } }