Skip to content

Commit

Permalink
refactor: improve mutation score for Compression extensions (#551)
Browse files Browse the repository at this point in the history
Add tests for missing cases in Compression extensions, detected by Stryker.NET.
  • Loading branch information
vbreuss authored Apr 9, 2024
1 parent fb0420f commit cd3f09e
Show file tree
Hide file tree
Showing 5 changed files with 216 additions and 22 deletions.
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
}

0 comments on commit cd3f09e

Please sign in to comment.