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

refactor: improve mutation score for Compression extensions #551

Merged
merged 4 commits into from
Apr 9, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
22 changes: 6 additions & 16 deletions Source/Testably.Abstractions.Compression/Internal/ZipUtilities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ namespace Testably.Abstractions.Internal;
internal static class ZipUtilities
{
private const string SearchPattern = "*";
private static readonly DateTime FallbackTime = new(1980, 1, 1, 0, 0, 0);

internal static IZipArchiveEntry CreateEntryFromFile(
IZipArchive destination,
Expand All @@ -32,7 +33,7 @@ internal static IZipArchiveEntry CreateEntryFromFile(

if (lastWrite.Year is < 1980 or > 2107)
{
lastWrite = new DateTime(1980, 1, 1, 0, 0, 0);
lastWrite = FallbackTime;
}

entry.LastWriteTime = new DateTimeOffset(lastWrite);
Expand Down Expand Up @@ -135,10 +136,7 @@ internal static void CreateFromDirectory(
ArgumentNullException.ThrowIfNull(destination);
if (!destination.CanWrite)
{
throw new ArgumentException("The stream is unwritable.", nameof(destination))
{
HResult = -2147024809
};
throw new ArgumentException("The stream is unwritable.", nameof(destination));
}

sourceDirectoryName = fileSystem.Path.GetFullPath(sourceDirectoryName);
Expand Down Expand Up @@ -206,13 +204,6 @@ internal static void ExtractRelativeToDirectory(this IZipArchiveEntry source,
source.FullName.TrimStart(
source.FileSystem.Path.DirectorySeparatorChar,
source.FileSystem.Path.AltDirectorySeparatorChar));
string? directoryPath =
source.FileSystem.Path.GetDirectoryName(fileDestinationPath);
if (directoryPath != null &&
!source.FileSystem.Directory.Exists(directoryPath))
{
source.FileSystem.Directory.CreateDirectory(directoryPath);
}

if (source.FullName.EndsWith('/'))
{
Expand All @@ -226,6 +217,8 @@ internal static void ExtractRelativeToDirectory(this IZipArchiveEntry source,
}
else
{
source.FileSystem.Directory.CreateDirectory(
source.FileSystem.Path.GetDirectoryName(fileDestinationPath) ?? ".");
ExtractToFile(source, fileDestinationPath, overwrite);
}
}
Expand Down Expand Up @@ -274,10 +267,7 @@ internal static void ExtractToDirectory(IFileSystem fileSystem,
ArgumentNullException.ThrowIfNull(source);
if (!source.CanRead)
{
throw new ArgumentException("The stream is unreadable.", nameof(source))
{
HResult = -2147024809
};
throw new ArgumentException("The stream is unreadable.", nameof(source));
}

using (ZipArchive archive = new(source, ZipArchiveMode.Read, true, entryNameEncoding))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,80 @@ namespace Testably.Abstractions.Compression.Tests.Internal;
public sealed class ExecuteTests
{
[Fact]
public void WhenRealFileSystem_MockFileSystem_ShouldExecuteOnMockFileSystem()
public void WhenRealFileSystem_MockFileSystem_WithActionCallback_ShouldExecuteOnMockFileSystem()
{
bool onRealFileSystemExecuted = false;
bool onMockFileSystemExecuted = false;
MockFileSystem fileSystem = new();
Execute.WhenRealFileSystem(fileSystem,
() => onRealFileSystemExecuted = true,
() => onMockFileSystemExecuted = true);
() =>
{
onRealFileSystemExecuted = true;
},
() =>
{
onMockFileSystemExecuted = true;
});

onRealFileSystemExecuted.Should().BeFalse();
onMockFileSystemExecuted.Should().BeTrue();
}

[Fact]
public void WhenRealFileSystem_RealFileSystem_ShouldExecuteOnRealFileSystem()
public void WhenRealFileSystem_MockFileSystem_WithFuncCallback_ShouldExecuteOnMockFileSystem()
{
bool onRealFileSystemExecuted = false;
bool onMockFileSystemExecuted = false;
MockFileSystem fileSystem = new();
Execute.WhenRealFileSystem(fileSystem,
() =>
{
return onRealFileSystemExecuted = true;
},
() =>
{
return onMockFileSystemExecuted = true;
});

onRealFileSystemExecuted.Should().BeFalse();
onMockFileSystemExecuted.Should().BeTrue();
}

[Fact]
public void WhenRealFileSystem_RealFileSystem_WithActionCallback_ShouldExecuteOnRealFileSystem()
{
bool onRealFileSystemExecuted = false;
bool onMockFileSystemExecuted = false;
RealFileSystem fileSystem = new();
Execute.WhenRealFileSystem(fileSystem,
() =>
{
onRealFileSystemExecuted = true;
},
() =>
{
onMockFileSystemExecuted = true;
});

onRealFileSystemExecuted.Should().BeTrue();
onMockFileSystemExecuted.Should().BeFalse();
}

[Fact]
public void WhenRealFileSystem_RealFileSystem_WithFuncCallback_ShouldExecuteOnRealFileSystem()
{
bool onRealFileSystemExecuted = false;
bool onMockFileSystemExecuted = false;
RealFileSystem fileSystem = new();
Execute.WhenRealFileSystem(fileSystem,
() => onRealFileSystemExecuted = true,
() => onMockFileSystemExecuted = true);
() =>
{
return onRealFileSystemExecuted = true;
},
() =>
{
return onMockFileSystemExecuted = true;
});

onRealFileSystemExecuted.Should().BeTrue();
onMockFileSystemExecuted.Should().BeFalse();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
using System.IO;
using Testably.Abstractions.Internal;

namespace Testably.Abstractions.Compression.Tests.Internal;

public sealed class ZipUtilitiesTests
{
[Theory]
[AutoData]
public void ExtractRelativeToDirectory_FileWithTrailingSlash_ShouldThrowIOException(
byte[] bytes)
{
MockFileSystem fileSystem = new();
using MemoryStream stream = new(bytes);
DummyZipArchiveEntry zipArchiveEntry = new(fileSystem, fullName: "foo/", stream: stream);

Exception? exception = Record.Exception(() =>
{
zipArchiveEntry.ExtractRelativeToDirectory("foo", false);
});

exception.Should().BeException<IOException>(
messageContains:
"Zip entry name ends in directory separator character but contains data");
}

[Fact]
public void ExtractRelativeToDirectory_WithSubdirectory_ShouldCreateSubdirectory()
{
MockFileSystem fileSystem = new();
DummyZipArchiveEntry zipArchiveEntry = new(fileSystem, fullName: "foo/");

zipArchiveEntry.ExtractRelativeToDirectory("bar", false);

fileSystem.Directory.Exists("bar").Should().BeTrue();
fileSystem.Directory.Exists("bar/foo").Should().BeTrue();
}

private sealed class DummyZipArchiveEntry(
IFileSystem fileSystem,
IZipArchive? archive = null,
string? fullName = "",
string? name = "",
string comment = "",
bool isEncrypted = false,
Stream? stream = null)
: IZipArchiveEntry
{
#region IZipArchiveEntry Members

/// <inheritdoc cref="IZipArchiveEntry.Archive" />
public IZipArchive Archive => archive ?? throw new NotSupportedException();

/// <inheritdoc cref="IZipArchiveEntry.Comment" />
public string Comment { get; set; } = comment;

/// <inheritdoc cref="IZipArchiveEntry.CompressedLength" />
public long CompressedLength => stream?.Length ?? 0L;

/// <inheritdoc cref="IZipArchiveEntry.Crc32" />
public uint Crc32 => 0u;

/// <inheritdoc cref="IZipArchiveEntry.ExternalAttributes" />
public int ExternalAttributes { get; set; }

/// <inheritdoc cref="IFileSystemEntity.FileSystem" />
public IFileSystem FileSystem { get; } = fileSystem;

/// <inheritdoc cref="IZipArchiveEntry.FullName" />
public string FullName { get; } = fullName ?? "";

/// <inheritdoc cref="IZipArchiveEntry.IsEncrypted" />
public bool IsEncrypted { get; } = isEncrypted;

/// <inheritdoc cref="IZipArchiveEntry.LastWriteTime" />
public DateTimeOffset LastWriteTime { get; set; }

/// <inheritdoc cref="IZipArchiveEntry.Length" />
public long Length => stream?.Length ?? 0L;

/// <inheritdoc cref="IZipArchiveEntry.Name" />
public string Name { get; } = name ?? "";

/// <inheritdoc cref="IZipArchiveEntry.Delete()" />
public void Delete()
=> throw new NotSupportedException();

/// <inheritdoc cref="IZipArchiveEntry.ExtractToFile(string)" />
public void ExtractToFile(string destinationFileName)
=> throw new NotSupportedException();

/// <inheritdoc cref="IZipArchiveEntry.ExtractToFile(string, bool)" />
public void ExtractToFile(string destinationFileName, bool overwrite)
=> throw new NotSupportedException();

/// <inheritdoc cref="IZipArchiveEntry.Open()" />
public Stream Open()
=> stream ?? throw new NotSupportedException();

#endregion
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,30 @@ public void CreateFromDirectory_ShouldZipDirectoryContent()
FileSystem.File.ReadAllBytes("foo/bar/test.txt"));
}

#if FEATURE_COMPRESSION_STREAM
[SkippableFact]
public void CreateFromDirectory_WithReadOnlyStream_ShouldThrowArgumentException()
{
FileSystem.Initialize()
.WithFile("target.zip")
.WithSubdirectory("foo").Initialized(s => s
.WithFile("test.txt"));
using FileSystemStream stream = FileSystem.FileStream.New(
"target.zip", FileMode.Open, FileAccess.Read);

Exception? exception = Record.Exception(() =>
{
// ReSharper disable once AccessToDisposedClosure
FileSystem.ZipFile().CreateFromDirectory("foo", stream);
});

exception.Should().BeException<ArgumentException>(
paramName: "destination",
hResult: -2147024809,
messageContains: "stream is unwritable");
}
#endif

#if FEATURE_COMPRESSION_STREAM
[SkippableTheory]
[AutoData]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -330,4 +330,30 @@ public void ExtractToDirectory_WithStream_WithoutOverwriteAndExistingFile_Should
.Should().NotBe(contents);
}
#endif

#if FEATURE_COMPRESSION_STREAM
[SkippableFact]
public void ExtractToDirectory_WithWriteOnlyStream_ShouldThrowArgumentException()
{
FileSystem.Initialize()
.WithFile("target.zip")
.WithSubdirectory("foo").Initialized(s => s
.WithFile("test.txt"));
using FileSystemStream stream = FileSystem.FileStream.New(
"target.zip", FileMode.Open, FileAccess.Write);

FileSystem.ZipFile().CreateFromDirectory("foo", stream);

Exception? exception = Record.Exception(() =>
{
// ReSharper disable once AccessToDisposedClosure
FileSystem.ZipFile().ExtractToDirectory(stream, "bar");
});

exception.Should().BeException<ArgumentException>(
paramName: "source",
hResult: -2147024809,
messageContains: "stream is unreadable");
}
#endif
}
Loading