Skip to content

Commit

Permalink
Align preallocationSize behavior (dotnet#58726)
Browse files Browse the repository at this point in the history
Co-authored-by: Stephen Toub <[email protected]>
  • Loading branch information
2 people authored and jozkee committed Sep 24, 2021
1 parent 1d67e27 commit ee72add
Show file tree
Hide file tree
Showing 12 changed files with 108 additions and 160 deletions.

This file was deleted.

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,29 +1,16 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.IO.Pipes;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using Xunit;

namespace System.IO.Tests
{
public partial class FileStream_ctor_options_as : FileStream_ctor_options_as_base
public partial class FileStream_ctor_options_as
{
protected override long PreallocationSize => 10;

protected override long InitialLength => 0; // Windows modifies AllocationSize, but not EndOfFile (file length)

private long GetExpectedFileLength(long preallocationSize) => 0; // Windows modifies AllocationSize, but not EndOfFile (file length)

private unsafe long GetActualPreallocationSize(FileStream fileStream)
{
Interop.Kernel32.FILE_STANDARD_INFO info;

Assert.True(Interop.Kernel32.GetFileInformationByHandleEx(fileStream.SafeFileHandle, Interop.Kernel32.FileStandardInfo, &info, (uint)sizeof(Interop.Kernel32.FILE_STANDARD_INFO)));

return info.AllocationSize;
}

[Theory]
[InlineData(@"\\?\")]
[InlineData(@"\??\")]
Expand All @@ -36,7 +23,24 @@ public void ExtendedPathsAreSupported(string prefix)

using (var fs = new FileStream(filePath, GetOptions(FileMode.CreateNew, FileAccess.Write, FileShare.None, FileOptions.None, preallocationSize)))
{
Assert.True(GetActualPreallocationSize(fs) >= preallocationSize, $"Provided {preallocationSize}, actual: {GetActualPreallocationSize(fs)}");
Assert.Equal(preallocationSize, fs.Length);
}
}

[Fact]
public async Task PreallocationSizeIsIgnoredForNonSeekableFiles()
{
string pipeName = GetNamedPipeServerStreamName();
string pipePath = Path.GetFullPath($@"\\.\pipe\{pipeName}");

FileStreamOptions options = new() { Mode = FileMode.Open, Access = FileAccess.Write, Share = FileShare.None, PreallocationSize = 123 };

using (var server = new NamedPipeServerStream(pipeName, PipeDirection.In))
using (var clienStream = new FileStream(pipePath, options))
{
await server.WaitForConnectionAsync();

Assert.False(clienStream.CanSeek);
}
}

Expand Down
103 changes: 57 additions & 46 deletions src/libraries/System.IO.FileSystem/tests/FileStream/ctor_options_as.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Linq;
using System.Security.Cryptography;
using Xunit;

namespace System.IO.Tests
Expand Down Expand Up @@ -68,6 +70,10 @@ public partial class NoParallelTests { }
[Collection("NoParallelTests")]
public partial class FileStream_ctor_options_as : FileStream_ctor_options_as_base
{
protected override long PreallocationSize => 10;

protected override long InitialLength => 10;

[Fact]
public virtual void NegativePreallocationSizeThrows()
{
Expand All @@ -80,57 +86,68 @@ public virtual void NegativePreallocationSizeThrows()
[InlineData(FileMode.Create, 0L)]
[InlineData(FileMode.CreateNew, 0L)]
[InlineData(FileMode.OpenOrCreate, 0L)]
public void WhenFileIsCreatedWithoutPreallocationSizeSpecifiedThePreallocationSizeIsNotSet(FileMode mode, long preallocationSize)
public void WhenFileIsCreatedWithoutPreallocationSizeSpecifiedItsLengthIsZero(FileMode mode, long preallocationSize)
{
using (var fs = new FileStream(GetPathToNonExistingFile(), GetOptions(mode, FileAccess.Write, FileShare.None, FileOptions.None, preallocationSize)))
{
Assert.Equal(0, GetActualPreallocationSize(fs));
Assert.Equal(0, fs.Length);
Assert.Equal(0, fs.Position);
}
}

[Theory]
[InlineData(FileMode.Open, 0L)]
[InlineData(FileMode.Open, 1L)]
[InlineData(FileMode.OpenOrCreate, 0L)]
[InlineData(FileMode.OpenOrCreate, 1L)]
[InlineData(FileMode.Append, 0L)]
[InlineData(FileMode.Append, 1L)]
public void WhenExistingFileIsBeingOpenedWithPreallocationSizeSpecifiedThePreallocationSizeIsNotChanged(FileMode mode, long preallocationSize)
[InlineData(FileMode.Open, 20L)]
[InlineData(FileMode.Open, 5L)]
[InlineData(FileMode.Append, 20L)]
[InlineData(FileMode.Append, 5L)]
public void PreallocationSizeIsIgnoredForFileModeOpenAndAppend(FileMode mode, long preallocationSize)
{
const int initialSize = 1;
const int initialSize = 10;
string filePath = GetPathToNonExistingFile();
File.WriteAllBytes(filePath, new byte[initialSize]);
long initialPreallocationSize;

using (var fs = new FileStream(filePath, GetOptions(mode, FileAccess.Write, FileShare.None, FileOptions.None, 0))) // preallocationSize NOT provided
using (var fs = new FileStream(filePath, GetOptions(mode, FileAccess.Write, FileShare.None, FileOptions.None, preallocationSize)))
{
initialPreallocationSize = GetActualPreallocationSize(fs); // just read it to ensure it's not being changed
Assert.Equal(initialSize, fs.Length); // it has NOT been changed
Assert.Equal(mode == FileMode.Append ? initialSize : 0, fs.Position);
}
}

using (var fs = new FileStream(filePath, GetOptions(mode, FileAccess.Write, FileShare.None, FileOptions.None, preallocationSize)))
[Theory]
[InlineData(FileMode.OpenOrCreate, 20L)] // preallocationSize > initialSize
[InlineData(FileMode.OpenOrCreate, 5L)] // preallocationSize < initialSize
public void WhenExistingFileIsBeingOpenedWithOpenOrCreateModeTheLengthRemainsUnchanged(FileMode mode, long preallocationSize)
{
const int initialSize = 10;
string filePath = GetPathToNonExistingFile();
byte[] initialData = RandomNumberGenerator.GetBytes(initialSize);
File.WriteAllBytes(filePath, initialData);

using (var fs = new FileStream(filePath, GetOptions(mode, FileAccess.ReadWrite, FileShare.None, FileOptions.None, preallocationSize)))
{
Assert.Equal(initialPreallocationSize, GetActualPreallocationSize(fs)); // it has NOT been changed
Assert.Equal(initialSize, fs.Length);
Assert.Equal(mode == FileMode.Append ? initialSize : 0, fs.Position);
Assert.Equal(initialSize, fs.Length); // it was not changed
Assert.Equal(0, fs.Position);

byte[] actualContent = new byte[initialData.Length];
Assert.Equal(actualContent.Length, fs.Read(actualContent));
AssertExtensions.SequenceEqual(initialData, actualContent); // the initial content was not changed
}
}

[Theory]
[InlineData(FileMode.Create)]
[InlineData(FileMode.CreateNew)]
[InlineData(FileMode.OpenOrCreate)]
public void WhenFileIsCreatedWithPreallocationSizeSpecifiedThePreallocationSizeIsSet(FileMode mode)
public void WhenFileIsCreatedWithPreallocationSizeSpecifiedTheLengthIsSetAndTheContentIsZeroed(FileMode mode)
{
const long preallocationSize = 123;

using (var fs = new FileStream(GetPathToNonExistingFile(), GetOptions(mode, FileAccess.Write, FileShare.None, FileOptions.None, preallocationSize)))
using (var fs = new FileStream(GetPathToNonExistingFile(), GetOptions(mode, FileAccess.ReadWrite, FileShare.None, FileOptions.None, preallocationSize)))
{
// OS might allocate MORE than we have requested
Assert.True(GetActualPreallocationSize(fs) >= preallocationSize, $"Provided {preallocationSize}, actual: {GetActualPreallocationSize(fs)}");
Assert.Equal(GetExpectedFileLength(preallocationSize), fs.Length);
Assert.Equal(preallocationSize, fs.Length);
Assert.Equal(0, fs.Position);

AssertFileContentHasBeenZeroed(0, (int)fs.Length, fs);
}
}

Expand All @@ -153,7 +170,7 @@ public void WhenDiskIsFullTheErrorMessageContainsAllDetails(FileMode mode)
Assert.Contains(filePath, ex.Message);
Assert.Contains(tooMuch.ToString(), ex.Message);

// ensure it was NOT created (provided OOTB by Windows, emulated on Unix)
// ensure it was NOT created
bool exists = File.Exists(filePath);
if (exists)
{
Expand All @@ -163,37 +180,20 @@ public void WhenDiskIsFullTheErrorMessageContainsAllDetails(FileMode mode)
}

[Fact]
public void WhenFileIsTruncatedWithoutPreallocationSizeSpecifiedThePreallocationSizeIsNotSet()
{
const int initialSize = 10_000;

string filePath = GetPathToNonExistingFile();
File.WriteAllBytes(filePath, new byte[initialSize]);

using (var fs = new FileStream(filePath, GetOptions(FileMode.Truncate, FileAccess.Write, FileShare.None, FileOptions.None, 0)))
{
Assert.Equal(0, GetActualPreallocationSize(fs));
Assert.Equal(0, fs.Length);
Assert.Equal(0, fs.Position);
}
}

[Fact]
public void WhenFileIsTruncatedWithPreallocationSizeSpecifiedThePreallocationSizeIsSet()
public void WhenFileIsTruncatedWithPreallocationSizeSpecifiedTheLengthIsSetAndTheContentIsZeroed()
{
const int initialSize = 10_000; // this must be more than 4kb which seems to be minimum allocation size on Windows
const long preallocationSize = 100;

string filePath = GetPathToNonExistingFile();
File.WriteAllBytes(filePath, new byte[initialSize]);
File.WriteAllBytes(filePath, Enumerable.Repeat((byte)1, initialSize).ToArray());

using (var fs = new FileStream(filePath, GetOptions(FileMode.Truncate, FileAccess.Write, FileShare.None, FileOptions.None, preallocationSize)))
using (var fs = new FileStream(filePath, GetOptions(FileMode.Truncate, FileAccess.ReadWrite, FileShare.None, FileOptions.None, preallocationSize)))
{
Assert.True(GetActualPreallocationSize(fs) >= preallocationSize, $"Provided {preallocationSize}, actual: {GetActualPreallocationSize(fs)}");
// less than initial file size (file got truncated)
Assert.True(GetActualPreallocationSize(fs) < initialSize, $"initialSize {initialSize}, actual: {GetActualPreallocationSize(fs)}");
Assert.Equal(GetExpectedFileLength(preallocationSize), fs.Length);
Assert.Equal(preallocationSize, fs.Length);
Assert.Equal(0, fs.Position);

AssertFileContentHasBeenZeroed(0, (int)fs.Length, fs);
}
}

Expand All @@ -208,5 +208,16 @@ private string GetPathToNonExistingFile()

return filePath;
}

private static void AssertFileContentHasBeenZeroed(int from, int to, FileStream fs)
{
int expectedByteCount = to - from;
int extraByteCount = 1;
byte[] content = Enumerable.Repeat((byte)1, expectedByteCount + extraByteCount).ToArray();
fs.Position = from;
Assert.Equal(expectedByteCount, fs.Read(content));
Assert.All(content.SkipLast(extraByteCount), @byte => Assert.Equal(0, @byte));
Assert.Equal(to, fs.Position);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<IncludeRemoteExecutor>true</IncludeRemoteExecutor>
Expand All @@ -14,7 +14,6 @@
<Compile Remove="..\**\*.Windows.cs" />
<Compile Remove="..\**\*.Browser.cs" />
<Compile Include="$(CommonPath)Interop\Unix\Interop.Libraries.cs" Link="Interop\Unix\Interop.Libraries.cs" />
<Compile Include="$(CommonPath)Interop\Unix\System.Native\Interop.Stat.cs" Link="Interop\Unix\System.Native\Interop.Stat.cs" />
</ItemGroup>
<ItemGroup Condition="'$(TargetsWindows)' == 'true'">
<Compile Remove="..\**\*.Unix.cs" />
Expand All @@ -24,8 +23,6 @@
<Compile Include="$(CommonPath)Interop\Windows\Interop.BOOL.cs" Link="Common\Interop\Windows\Interop.BOOL.cs" />
<Compile Include="$(CommonPath)Interop\Windows\Interop.Libraries.cs" Link="Common\Interop\Windows\Interop.Libraries.cs" />
<Compile Include="$(CommonPath)Interop\Windows\Kernel32\Interop.CreateFile.cs" Link="Common\Interop\Windows\Interop.CreateFile.cs" />
<Compile Include="$(CommonPath)Interop\Windows\Kernel32\Interop.FILE_STANDARD_INFO.cs" Link="Common\Interop\Windows\Interop.FILE_STANDARD_INFO.cs" />
<Compile Include="$(CommonPath)Interop\Windows\Kernel32\Interop.GetFileInformationByHandleEx.cs" Link="Common\Interop\Windows\Interop.GetFileInformationByHandleEx.cs" />
<Compile Include="$(CommonPath)Interop\Windows\Kernel32\Interop.GetFinalPathNameByHandle.cs" Link="Common\Interop\Windows\Interop.GetFinalPathNameByHandle.cs" />
<Compile Include="$(CommonPath)Interop\Windows\Kernel32\Interop.MemOptions.cs" Link="Common\Interop\Windows\Interop.MemOptions.cs" />
<Compile Include="$(CommonPath)Interop\Windows\Kernel32\Interop.SECURITY_ATTRIBUTES.cs" Link="Common\Interop\Windows\Interop.SECURITY_ATTRIBUTES.cs" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,7 @@
<ItemGroup Condition="'$(TargetsUnix)' == 'true'">
<Compile Include="Base\SymbolicLinks\BaseSymbolicLinks.Unix.cs" />
<Compile Include="FileSystemTest.Unix.cs" />
<Compile Include="FileStream\ctor_options_as.Unix.cs" />
<Compile Include="$(CommonPath)Interop\Unix\Interop.Libraries.cs" Link="Interop\Unix\Interop.Libraries.cs" />
<Compile Include="$(CommonPath)Interop\Unix\System.Native\Interop.Stat.cs" Link="Interop\Unix\System.Native\Interop.Stat.cs" />
</ItemGroup>
<ItemGroup Condition="'$(TargetsWindows)' == 'true'">
<Compile Include="Base\SymbolicLinks\BaseSymbolicLinks.Windows.cs" />
Expand All @@ -88,8 +86,6 @@
<Compile Include="$(CommonPath)Interop\Windows\Interop.BOOL.cs" Link="Common\Interop\Windows\Interop.BOOL.cs" />
<Compile Include="$(CommonPath)Interop\Windows\Interop.Libraries.cs" Link="Common\Interop\Windows\Interop.Libraries.cs" />
<Compile Include="$(CommonPath)Interop\Windows\Kernel32\Interop.CreateFile.cs" Link="Common\Interop\Windows\Interop.CreateFile.cs" />
<Compile Include="$(CommonPath)Interop\Windows\Kernel32\Interop.FILE_STANDARD_INFO.cs" Link="Common\Interop\Windows\Interop.FILE_STANDARD_INFO.cs" />
<Compile Include="$(CommonPath)Interop\Windows\Kernel32\Interop.GetFileInformationByHandleEx.cs" Link="Common\Interop\Windows\Interop.GetFileInformationByHandleEx.cs" />
<Compile Include="$(CommonPath)Interop\Windows\Kernel32\Interop.GetFinalPathNameByHandle.cs" Link="Common\Interop\Windows\Interop.GetFinalPathNameByHandle.cs" />
<Compile Include="$(CommonPath)Interop\Windows\Kernel32\Interop.MemOptions.cs" Link="Common\Interop\Windows\Interop.MemOptions.cs" />
<Compile Include="$(CommonPath)Interop\Windows\Kernel32\Interop.SECURITY_ATTRIBUTES.cs" Link="Common\Interop\Windows\Interop.SECURITY_ATTRIBUTES.cs" />
Expand All @@ -103,7 +99,6 @@
<ItemGroup Condition="'$(TargetsBrowser)' == 'true'">
<Compile Include="Base\SymbolicLinks\BaseSymbolicLinks.Unix.cs" />
<Compile Include="FileSystemTest.Browser.cs" />
<Compile Include="FileStream\ctor_options_as.Browser.cs" />
</ItemGroup>
<ItemGroup>
<!-- Rewritten -->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,7 @@ private void Init(string path, FileMode mode, FileAccess access, FileShare share
}

// If preallocationSize has been provided for a creatable and writeable file
if (FileStreamHelpers.ShouldPreallocate(preallocationSize, access, mode))
if (FileStreamHelpers.ShouldPreallocate(preallocationSize, access, mode, this))
{
int fallocateResult = Interop.Sys.PosixFAllocate(this, 0, preallocationSize);
if (fallocateResult != 0)
Expand Down
Loading

0 comments on commit ee72add

Please sign in to comment.