[FileStream] add tests for device and UNC paths (#54545)
authorAdam Sitnik <adam.sitnik@gmail.com>
Thu, 24 Jun 2021 06:58:33 +0000 (08:58 +0200)
committerGitHub <noreply@github.com>
Thu, 24 Jun 2021 06:58:33 +0000 (08:58 +0200)
* add a test for unseekable device by using a path to named pipe

* add a test for seekable device by using DeviceID instead of drive letter

* add a test for a UNC file path (local file share)

src/libraries/Common/tests/TestUtilities/System/IO/FileCleanupTestBase.cs
src/libraries/System.IO.FileSystem/tests/FileStream/FileStreamConformanceTests.Windows.cs [new file with mode: 0644]
src/libraries/System.IO.FileSystem/tests/Net5CompatTests/System.IO.FileSystem.Net5Compat.Tests.csproj
src/libraries/System.IO.FileSystem/tests/System.IO.FileSystem.Tests.csproj

index a0542ef..02ffa60 100644 (file)
@@ -79,7 +79,7 @@ namespace System.IO
         /// <param name="index">An optional index value to use as a suffix on the file name.  Typically a loop index.</param>
         /// <param name="memberName">The member name of the function calling this method.</param>
         /// <param name="lineNumber">The line number of the function calling this method.</param>
-        protected string GetTestFilePath(int? index = null, [CallerMemberName] string memberName = null, [CallerLineNumber] int lineNumber = 0) =>
+        protected virtual string GetTestFilePath(int? index = null, [CallerMemberName] string memberName = null, [CallerLineNumber] int lineNumber = 0) =>
             Path.Combine(TestDirectory, GetTestFileName(index, memberName, lineNumber));
 
         /// <summary>Gets a test file name that is associated with the call site.</summary>
diff --git a/src/libraries/System.IO.FileSystem/tests/FileStream/FileStreamConformanceTests.Windows.cs b/src/libraries/System.IO.FileSystem/tests/FileStream/FileStreamConformanceTests.Windows.cs
new file mode 100644 (file)
index 0000000..1cfbd35
--- /dev/null
@@ -0,0 +1,179 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.Win32.SafeHandles;
+using System.ComponentModel;
+using System.IO.Pipes;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Text;
+using System.ServiceProcess;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace System.IO.Tests
+{
+    [PlatformSpecific(TestPlatforms.Windows)] // DOS device paths (\\.\ and \\?\) are a Windows concept
+    public class UnseekableDeviceFileStreamConnectedConformanceTests : ConnectedStreamConformanceTests
+    {
+        protected override async Task<StreamPair> CreateConnectedStreamsAsync()
+        {
+            string pipeName = FileSystemTest.GetNamedPipeServerStreamName();
+            string pipePath = Path.GetFullPath($@"\\.\pipe\{pipeName}");
+
+            var server = new NamedPipeServerStream(pipeName, PipeDirection.In);
+            var clienStream = new FileStream(File.OpenHandle(pipePath, FileMode.Open, FileAccess.Write, FileShare.None), FileAccess.Write);
+
+            await server.WaitForConnectionAsync();
+
+            var serverStrean = new FileStream(new SafeFileHandle(server.SafePipeHandle.DangerousGetHandle(), true), FileAccess.Read);
+
+            server.SafePipeHandle.SetHandleAsInvalid();
+
+            return (serverStrean, clienStream);
+        }
+
+        protected override Type UnsupportedConcurrentExceptionType => null;
+        protected override bool UsableAfterCanceledReads => false;
+        protected override bool FullyCancelableOperations => false;
+        protected override bool BlocksOnZeroByteReads => OperatingSystem.IsWindows();
+        protected override bool SupportsConcurrentBidirectionalUse => false;
+    }
+
+    [PlatformSpecific(TestPlatforms.Windows)] // DOS device paths (\\.\ and \\?\) are a Windows concept
+    public class SeekableDeviceFileStreamStandaloneConformanceTests : UnbufferedAsyncFileStreamStandaloneConformanceTests
+    {
+        protected override string GetTestFilePath(int? index = null, [CallerMemberName] string memberName = null, [CallerLineNumber] int lineNumber = 0)
+        {
+            string filePath = Path.GetFullPath(base.GetTestFilePath(index, memberName, lineNumber));
+            string drive = Path.GetPathRoot(filePath);
+            StringBuilder volumeNameBuffer = new StringBuilder(filePath.Length + 1024);
+
+            // the following method maps drive letter like "C:\" to a DeviceID (a DOS device path)
+            // example: "\\?\Volume{724edb31-eaa5-4728-a4e3-f2474fd34ae2}\"
+            if (!GetVolumeNameForVolumeMountPoint(drive, volumeNameBuffer, volumeNameBuffer.Capacity))
+            {
+                throw new Win32Exception(Marshal.GetLastPInvokeError(), "GetVolumeNameForVolumeMountPoint failed");
+            }
+
+            // instead of:
+            // 'C:\Users\x\AppData\Local\Temp\y\z
+            // we want something like:
+            // '\\.\Volume{724edb31-eaa5-4728-a4e3-f2474fd34ae2}\Users\x\AppData\Local\Temp\y\z
+            string devicePath = filePath.Replace(drive, volumeNameBuffer.ToString());
+            Assert.StartsWith(@"\\?\", devicePath);
+#if DEBUG
+            // we do want to test \\.\ prefix as well
+            devicePath = devicePath.Replace(@"\\?\", @"\\.\");
+#endif
+
+            return devicePath;
+        }
+
+        [DllImport(Interop.Libraries.Kernel32, EntryPoint = "GetVolumeNameForVolumeMountPointW", CharSet = CharSet.Unicode, BestFitMapping = false, SetLastError = true)]
+        private static extern bool GetVolumeNameForVolumeMountPoint(string volumeName, StringBuilder uniqueVolumeName, int uniqueNameBufferCapacity);
+    }
+
+    [PlatformSpecific(TestPlatforms.Windows)] // the test setup is Windows-specifc
+    [Collection("NoParallelTests")] // don't run in parallel, as file sharing logic is not thread-safe
+    [OuterLoop("Requires admin privileges to create a file share")]
+    [ConditionalClass(typeof(UncFilePathFileStreamStandaloneConformanceTests), nameof(CanShareFiles))]
+    public class UncFilePathFileStreamStandaloneConformanceTests : UnbufferedAsyncFileStreamStandaloneConformanceTests
+    {
+        public static bool CanShareFiles => _canShareFiles.Value;
+
+        private static Lazy<bool> _canShareFiles = new Lazy<bool>(() =>
+        {
+            if (!PlatformDetection.IsWindowsAndElevated || PlatformDetection.IsWindowsNanoServer)
+            {
+                return false;
+            }
+
+            // the "Server Service" allows for file sharing. It can be disabled on some of our CI machines.    
+            using (ServiceController sharingService = new ServiceController("Server"))
+            {
+                return sharingService.Status == ServiceControllerStatus.Running;
+            }
+        });
+
+        protected override string GetTestFilePath(int? index = null, [CallerMemberName] string memberName = null, [CallerLineNumber] int lineNumber = 0)
+        {
+            string testDirectoryPath = Path.GetFullPath(TestDirectory);
+            string shareName = new DirectoryInfo(testDirectoryPath).Name;
+            string fileName = GetTestFileName(index, memberName, lineNumber);
+
+            SHARE_INFO_502 shareInfo = default;
+            shareInfo.shi502_netname = shareName;
+            shareInfo.shi502_path = testDirectoryPath;
+            shareInfo.shi502_remark = "folder created to test UNC file paths";
+            shareInfo.shi502_max_uses = -1;
+
+            int infoSize = Marshal.SizeOf(shareInfo);
+            IntPtr infoBuffer = Marshal.AllocCoTaskMem(infoSize);
+
+            try
+            {
+                Marshal.StructureToPtr(shareInfo, infoBuffer, false);
+
+                int shareResult = NetShareAdd(string.Empty, 502, infoBuffer, IntPtr.Zero);
+
+                if (shareResult != 0 && shareResult != 2118) // is a failure that is not a NERR_DuplicateShare
+                {
+                    throw new Exception($"Failed to create a file share, NetShareAdd returned {shareResult}");
+                }
+            }
+            finally
+            {
+                Marshal.FreeCoTaskMem(infoBuffer);
+            }
+
+            // now once the folder has been shared we can use "localhost" to access it:
+            // both type of slashes are valid, so let's test one for Debug and another for other configs
+#if DEBUG
+            return @$"//localhost/{shareName}/{fileName}";
+#else
+            return @$"\\localhost\{shareName}\{fileName}";
+#endif
+        }
+
+        protected override void Dispose(bool disposing)
+        {
+            string testDirectoryPath = Path.GetFullPath(TestDirectory);
+            string shareName = new DirectoryInfo(testDirectoryPath).Name;
+
+            try
+            {
+                NetShareDel(string.Empty, shareName, 0);
+            }
+            finally
+            {
+                base.Dispose(disposing);
+            }
+        }
+
+        [StructLayout(LayoutKind.Sequential)]
+        public struct SHARE_INFO_502
+        {
+            [MarshalAs(UnmanagedType.LPWStr)]
+            public string shi502_netname;
+            public uint shi502_type;
+            [MarshalAs(UnmanagedType.LPWStr)]
+            public string shi502_remark;
+            public int shi502_permissions;
+            public int shi502_max_uses;
+            public int shi502_current_uses;
+            [MarshalAs(UnmanagedType.LPWStr)]
+            public string shi502_path;
+            public IntPtr shi502_passwd;
+            public int shi502_reserved;
+            public IntPtr shi502_security_descriptor;
+        }
+
+        [DllImport(Interop.Libraries.Netapi32)]
+        public static extern int NetShareAdd([MarshalAs(UnmanagedType.LPWStr)]string servername, int level, IntPtr buf, IntPtr parm_err);
+
+        [DllImport(Interop.Libraries.Netapi32)]
+        public static extern int NetShareDel([MarshalAs(UnmanagedType.LPWStr)] string servername, [MarshalAs(UnmanagedType.LPWStr)] string netname, int reserved);
+    }
+}
index 5866965..04d5762 100644 (file)
@@ -25,6 +25,7 @@
     <Compile Include="$(CommonPath)Interop\Windows\Kernel32\Interop.GetFileInformationByHandleEx.cs" Link="Common\Interop\Windows\Interop.GetFileInformationByHandleEx.cs" />
     <Compile Include="$(CommonPath)Interop\Windows\Kernel32\Interop.MemOptions.cs" Link="Common\Interop\Windows\Interop.MemOptions.cs" />
     <Compile Include="$(CommonPath)Interop\Windows\Kernel32\Interop.VirtualAlloc_Ptr.cs" Link="Common\Interop\Windows\Interop.VirtualAlloc_Ptr.cs" />
+    <ProjectReference Include="$(LibrariesProjectRoot)System.ServiceProcess.ServiceController\src\System.ServiceProcess.ServiceController.csproj" />
   </ItemGroup>
   <ItemGroup Condition="'$(TargetsBrowser)' == 'true'">
     <Compile Remove="..\**\*.Unix.cs" />
index 94cec86..bf845ce 100644 (file)
@@ -72,6 +72,7 @@
   <ItemGroup Condition="'$(TargetsWindows)' == 'true'">
     <Compile Include="FileSystemTest.Windows.cs" />
     <Compile Include="FileStream\ctor_options_as.Windows.cs" />
+    <Compile Include="FileStream\FileStreamConformanceTests.Windows.cs" />
     <Compile Include="RandomAccess\NoBuffering.Windows.cs" />
     <Compile Include="RandomAccess\SectorAlignedMemory.Windows.cs" />
     <Compile Include="$(CommonPath)Interop\Windows\Interop.BOOL.cs" Link="Common\Interop\Windows\Interop.BOOL.cs" />
@@ -80,6 +81,7 @@
     <Compile Include="$(CommonPath)Interop\Windows\Kernel32\Interop.GetFileInformationByHandleEx.cs" Link="Common\Interop\Windows\Interop.GetFileInformationByHandleEx.cs" />
     <Compile Include="$(CommonPath)Interop\Windows\Kernel32\Interop.MemOptions.cs" Link="Common\Interop\Windows\Interop.MemOptions.cs" />
     <Compile Include="$(CommonPath)Interop\Windows\Kernel32\Interop.VirtualAlloc_Ptr.cs" Link="Common\Interop\Windows\Interop.VirtualAlloc_Ptr.cs" />
+    <ProjectReference Include="$(LibrariesProjectRoot)System.ServiceProcess.ServiceController\src\System.ServiceProcess.ServiceController.csproj" />
   </ItemGroup>
   <ItemGroup Condition="'$(TargetsBrowser)' == 'true'">
     <Compile Include="FileSystemTest.Browser.cs" />