add AOT smoke and missing tests

This commit is contained in:
Adam Hathcock
2026-05-26 15:47:44 +01:00
parent 98b4ffc807
commit 55df0d30d9
6 changed files with 483 additions and 7 deletions

View File

@@ -45,6 +45,12 @@ jobs:
# Build and test
- name: Build and Test
run: dotnet run --project build/build.csproj
- name: Validate AOT Smoke Test
if: matrix.os == 'ubuntu-latest'
run: |
dotnet publish tests/SharpCompress.AotSmoke/SharpCompress.AotSmoke.csproj --configuration Release --runtime linux-x64 --self-contained true --output artifacts/aot-smoke
./artifacts/aot-smoke/SharpCompress.AotSmoke
# Upload artifacts for verification
- name: Upload NuGet Package

View File

@@ -0,0 +1,59 @@
using System;
using System.IO;
using System.Text;
using SharpCompress.Archives;
using SharpCompress.Common;
using SharpCompress.Readers;
using SharpCompress.Writers;
var original = "SharpCompress AOT smoke test";
using var archiveStream = new MemoryStream();
using (
var writer = WriterFactory.OpenWriter(
archiveStream,
ArchiveType.Zip,
new WriterOptions(CompressionType.Deflate) { LeaveStreamOpen = true }
)
)
{
using var entryStream = new MemoryStream(Encoding.UTF8.GetBytes(original));
writer.Write("payload.txt", entryStream, DateTime.UtcNow);
}
archiveStream.Position = 0;
using (var reader = ReaderFactory.OpenReader(archiveStream, ReaderOptions.ForExternalStream))
{
if (!reader.MoveToNextEntry() || reader.Entry.IsDirectory)
{
throw new InvalidOperationException("Expected a file entry.");
}
using var extracted = new MemoryStream();
reader.WriteEntryTo(extracted);
var actual = Encoding.UTF8.GetString(extracted.ToArray());
if (!string.Equals(original, actual, StringComparison.Ordinal))
{
throw new InvalidOperationException("ReaderFactory round-trip content mismatch.");
}
}
archiveStream.Position = 0;
using (var archive = ArchiveFactory.OpenArchive(archiveStream, ReaderOptions.ForExternalStream))
{
var entryCount = 0;
foreach (var entry in archive.Entries)
{
if (!entry.IsDirectory)
{
entryCount++;
}
}
if (entryCount != 1)
{
throw new InvalidOperationException("ArchiveFactory did not see the expected entry.");
}
}
Console.WriteLine("SharpCompress AOT smoke test passed.");

View File

@@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<PublishAot>true</PublishAot>
<PublishTrimmed>true</PublishTrimmed>
<TrimMode>full</TrimMode>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\SharpCompress\SharpCompress.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,84 @@
{
"version": 2,
"dependencies": {
"net10.0": {
"Microsoft.DotNet.ILCompiler": {
"type": "Direct",
"requested": "[10.0.6, )",
"resolved": "10.0.6",
"contentHash": "nBOzxOys8OeyJ+Nsi/uYlI/5TSsvwjaM/p5m4dTL6khCLx9UuP3b2ec3HeuBw/+F7hHCAZG1yFx8VBeoRAX+EQ=="
},
"Microsoft.NET.ILLink.Tasks": {
"type": "Direct",
"requested": "[10.0.6, )",
"resolved": "10.0.6",
"contentHash": "QKuvS0LWX4fjFqeDkyM7Kqt8P3wYTiPD4nwU+9y59n0sCiG714fxDgbbN82vDnzq89AF/PiHl92TP2C4aFDUQA=="
},
"Microsoft.NETFramework.ReferenceAssemblies": {
"type": "Direct",
"requested": "[1.0.3, )",
"resolved": "1.0.3",
"contentHash": "vUc9Npcs14QsyOD01tnv/m8sQUnGTGOw1BCmKcv77LBJY7OxhJ+zJF7UD/sCL3lYNFuqmQEVlkfS4Quif6FyYg==",
"dependencies": {
"Microsoft.NETFramework.ReferenceAssemblies.net461": "1.0.3"
}
},
"Microsoft.SourceLink.GitHub": {
"type": "Direct",
"requested": "[10.0.102, )",
"resolved": "10.0.102",
"contentHash": "Oxq3RCIJSdtpIU4hLqO7XaDe/Ra3HS9Wi8rJl838SAg6Zu1iQjerA0+xXWBgUFYbgknUGCLOU0T+lzMLkvY9Qg==",
"dependencies": {
"Microsoft.Build.Tasks.Git": "10.0.102",
"Microsoft.SourceLink.Common": "10.0.102"
}
},
"Microsoft.VisualStudio.Threading.Analyzers": {
"type": "Direct",
"requested": "[17.14.15, )",
"resolved": "17.14.15",
"contentHash": "mXQPJsbuUD2ydq4/ffd8h8tSOFCXec+2xJOVNCvXjuMOq/+5EKHq3D2m2MC2+nUaXeFMSt66VS/J4HdKBixgcw=="
},
"PolySharp": {
"type": "Direct",
"requested": "[1.15.0, )",
"resolved": "1.15.0",
"contentHash": "FbU0El+EEjdpuIX4iDbeS7ki1uzpJPx8vbqOzEtqnl1GZeAGJfq+jCbxeJL2y0EPnUNk8dRnnqR2xnYXg9Tf+g=="
},
"Microsoft.Build.Tasks.Git": {
"type": "Transitive",
"resolved": "10.0.102",
"contentHash": "0i81LYX31U6UiXz4NOLbvc++u+/mVDmOt+PskrM/MygpDxkv9THKQyRUmavBpLK6iBV0abNWnn+CQgSRz//Pwg=="
},
"Microsoft.NETFramework.ReferenceAssemblies.net461": {
"type": "Transitive",
"resolved": "1.0.3",
"contentHash": "AmOJZwCqnOCNp6PPcf9joyogScWLtwy0M1WkqfEQ0M9nYwyDD7EX9ZjscKS5iYnyvteX7kzSKFCKt9I9dXA6mA=="
},
"Microsoft.SourceLink.Common": {
"type": "Transitive",
"resolved": "10.0.102",
"contentHash": "Mk1IMb9q5tahC2NltxYXFkLBtuBvfBoCQ3pIxYQWfzbCE9o1OB9SsHe0hnNGo7lWgTA/ePbFAJLWu6nLL9K17A=="
},
"sharpcompress": {
"type": "Project"
}
},
"net10.0/osx-arm64": {
"Microsoft.DotNet.ILCompiler": {
"type": "Direct",
"requested": "[10.0.6, )",
"resolved": "10.0.6",
"contentHash": "nBOzxOys8OeyJ+Nsi/uYlI/5TSsvwjaM/p5m4dTL6khCLx9UuP3b2ec3HeuBw/+F7hHCAZG1yFx8VBeoRAX+EQ==",
"dependencies": {
"runtime.osx-arm64.Microsoft.DotNet.ILCompiler": "10.0.6"
}
},
"runtime.osx-arm64.Microsoft.DotNet.ILCompiler": {
"type": "Transitive",
"resolved": "10.0.6",
"contentHash": "+yovwOAlIpfIcH+ZWmLYXWTSWYJ93wcQxF/RVk+X4MXgLASeosCJYVLqP20g0cufKjoRqvCmnklR6y9Su3ORtA=="
}
}
}
}

View File

@@ -3,6 +3,7 @@ using System.IO;
using System.Linq;
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
using SharpCompress.Archives;
using SharpCompress.Archives.Tar;
using SharpCompress.Common;
using SharpCompress.Compressors;
@@ -67,11 +68,11 @@ public class TarBenchmarks : ArchiveBenchmarkBase
[Benchmark(Description = "Tar: Extract all entries (Archive API) - SystemGzip")]
public void SystemTarExtractArchiveApi()
{
using var stream = new MemoryStream(_tarBytes);
using var archive = TarArchive.OpenArchive(
using var stream = new MemoryStream(_tarGzBytes);
using var archive = ArchiveFactory.OpenArchive(
stream,
ReaderOptions.ForExternalStream.WithProviders(
CompressionProviderRegistry.Empty.With(new SystemGZipCompressionProvider())
CompressionProviderRegistry.Default.With(new SystemGZipCompressionProvider())
)
);
foreach (var entry in archive.Entries.Where(e => !e.IsDirectory))
@@ -84,11 +85,11 @@ public class TarBenchmarks : ArchiveBenchmarkBase
[Benchmark(Description = "Tar: Extract all entries (Reader API) - SystemGzip")]
public void SystemTarExtractReaderApi()
{
using var stream = new MemoryStream(_tarBytes);
using var stream = new MemoryStream(_tarGzBytes);
using var reader = ReaderFactory.OpenReader(
stream,
ReaderOptions.ForExternalStream.WithProviders(
CompressionProviderRegistry.Empty.With(new SystemGZipCompressionProvider())
CompressionProviderRegistry.Default.With(new SystemGZipCompressionProvider())
)
);
while (reader.MoveToNextEntry())
@@ -104,7 +105,7 @@ public class TarBenchmarks : ArchiveBenchmarkBase
public void TarGzipExtract()
{
using var stream = new MemoryStream(_tarGzBytes);
using var archive = TarArchive.OpenArchive(stream);
using var archive = ArchiveFactory.OpenArchive(stream);
foreach (var entry in archive.Entries.Where(e => !e.IsDirectory))
{
using var entryStream = entry.OpenEntryStream();
@@ -116,7 +117,9 @@ public class TarBenchmarks : ArchiveBenchmarkBase
public async Task TarGzipExtractAsync()
{
using var stream = new MemoryStream(_tarGzBytes);
await using var archive = await TarArchive.OpenAsyncArchive(stream).ConfigureAwait(false);
await using var archive = await ArchiveFactory
.OpenAsyncArchive(stream)
.ConfigureAwait(false);
await foreach (var entry in archive.EntriesAsync.Where(e => !e.IsDirectory))
{
await using var entryStream = await entry.OpenEntryStreamAsync().ConfigureAwait(false);

View File

@@ -0,0 +1,312 @@
#if NET8_0_OR_GREATER
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Archives;
using SharpCompress.Common;
using SharpCompress.Readers;
using SharpCompress.Test.Mocks;
using SharpCompress.Writers;
using Xunit;
namespace SharpCompress.Test;
public class AsyncParityAndCancellationTests : TestBase
{
[Theory]
[InlineData("Zip.deflate.zip")]
[InlineData("Tar.tar")]
[InlineData("Tar.tar.gz")]
[InlineData("Rar.rar")]
[InlineData("7Zip.nonsolid.7z")]
public async Task ArchiveAsyncEntries_ShouldMatchSyncEntries(string archiveName)
{
var archivePath = Path.Combine(TEST_ARCHIVES_PATH, archiveName);
var syncEntries = ReadArchiveEntries(archivePath);
var asyncEntries = await ReadArchiveEntriesAsync(archivePath);
Assert.Equal(syncEntries, asyncEntries);
}
[Theory]
[InlineData("Zip.deflate.zip")]
[InlineData("Tar.tar")]
[InlineData("Tar.tar.gz")]
[InlineData("Rar.rar")]
public async Task ReaderAsyncEntries_ShouldMatchSyncEntries(string archiveName)
{
var archivePath = Path.Combine(TEST_ARCHIVES_PATH, archiveName);
var syncEntries = ReadReaderEntries(archivePath);
var asyncEntries = await ReadReaderEntriesAsync(archivePath);
Assert.Equal(syncEntries, asyncEntries);
}
[Fact]
public async Task AsyncReaderExtraction_ShouldRespectCancellationBeforeStart()
{
var archivePath = Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.zip");
await using var stream = File.OpenRead(archivePath);
await using var reader = await ReaderFactory.OpenAsyncReader(stream);
using var cts = new CancellationTokenSource();
cts.Cancel();
await Assert.ThrowsAnyAsync<OperationCanceledException>(async () =>
await reader.WriteAllToDirectoryAsync(SCRATCH_FILES_PATH, cancellationToken: cts.Token)
);
}
[Fact]
public async Task AsyncArchiveExtraction_ShouldRespectCancellationBeforeStart()
{
var archivePath = Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.zip");
await using var archive = await ArchiveFactory.OpenAsyncArchive(archivePath);
using var cts = new CancellationTokenSource();
cts.Cancel();
await Assert.ThrowsAnyAsync<OperationCanceledException>(async () =>
await archive.WriteToDirectoryAsync(SCRATCH_FILES_PATH, cancellationToken: cts.Token)
);
}
[Fact]
public async Task AsyncReaderExtraction_ShouldRespectCancellationDuringRead()
{
var archiveBytes = CreateLargeTarArchive();
using var cts = new CancellationTokenSource();
await using var stream = new CancelAfterBytesReadStream(
new MemoryStream(archiveBytes),
cts,
cancelAfterBytes: 2048
);
await Assert.ThrowsAnyAsync<OperationCanceledException>(async () =>
{
await using var reader = await ReaderFactory.OpenAsyncReader(
stream,
cancellationToken: cts.Token
);
await reader.WriteAllToDirectoryAsync(SCRATCH_FILES_PATH, cancellationToken: cts.Token);
});
}
[Fact]
public async Task OpenAsyncReader_CallerProvidedStream_ShouldRemainOpenByDefault()
{
var archiveBytes = await File.ReadAllBytesAsync(
Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.zip")
);
var stream = new TestStream(new MemoryStream(archiveBytes));
await using (var reader = await ReaderFactory.OpenAsyncReader(stream))
{
Assert.True(await reader.MoveToNextEntryAsync());
}
Assert.False(stream.IsDisposed);
}
[Fact]
public async Task OpenAsyncArchive_CallerProvidedStream_ShouldRemainOpenByDefault()
{
var archiveBytes = await File.ReadAllBytesAsync(
Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.zip")
);
var stream = new TestStream(new MemoryStream(archiveBytes));
await using (var archive = await ArchiveFactory.OpenAsyncArchive(stream))
{
await foreach (var _ in archive.EntriesAsync)
{
break;
}
}
Assert.False(stream.IsDisposed);
}
private static List<EntrySnapshot> ReadArchiveEntries(string archivePath)
{
using var archive = ArchiveFactory.OpenArchive(archivePath);
return archive
.Entries.Where(entry => !entry.IsDirectory)
.Select(entry =>
{
using var stream = entry.OpenEntryStream();
using var memory = new MemoryStream();
stream.CopyTo(memory);
return new EntrySnapshot(
entry.Key ?? string.Empty,
entry.Size,
entry.CompressionType,
Convert.ToBase64String(memory.ToArray())
);
})
.OrderBy(entry => entry.Key, StringComparer.Ordinal)
.ToList();
}
private static async Task<List<EntrySnapshot>> ReadArchiveEntriesAsync(string archivePath)
{
await using var archive = await ArchiveFactory.OpenAsyncArchive(archivePath);
var entries = new List<EntrySnapshot>();
await foreach (var entry in archive.EntriesAsync)
{
if (entry.IsDirectory)
{
continue;
}
await using var stream = await entry.OpenEntryStreamAsync();
using var memory = new MemoryStream();
await stream.CopyToAsync(memory);
entries.Add(
new EntrySnapshot(
entry.Key ?? string.Empty,
entry.Size,
entry.CompressionType,
Convert.ToBase64String(memory.ToArray())
)
);
}
return entries.OrderBy(entry => entry.Key, StringComparer.Ordinal).ToList();
}
private static List<EntrySnapshot> ReadReaderEntries(string archivePath)
{
using var stream = File.OpenRead(archivePath);
using var reader = ReaderFactory.OpenReader(stream);
var entries = new List<EntrySnapshot>();
while (reader.MoveToNextEntry())
{
if (reader.Entry.IsDirectory)
{
continue;
}
using var memory = new MemoryStream();
reader.WriteEntryTo(memory);
entries.Add(
new EntrySnapshot(
reader.Entry.Key ?? string.Empty,
reader.Entry.Size,
reader.Entry.CompressionType,
Convert.ToBase64String(memory.ToArray())
)
);
}
return entries.OrderBy(entry => entry.Key, StringComparer.Ordinal).ToList();
}
private static async Task<List<EntrySnapshot>> ReadReaderEntriesAsync(string archivePath)
{
await using var stream = File.OpenRead(archivePath);
await using var reader = await ReaderFactory.OpenAsyncReader(stream);
var entries = new List<EntrySnapshot>();
while (await reader.MoveToNextEntryAsync())
{
if (reader.Entry.IsDirectory)
{
continue;
}
using var memory = new MemoryStream();
await reader.WriteEntryToAsync(memory);
entries.Add(
new EntrySnapshot(
reader.Entry.Key ?? string.Empty,
reader.Entry.Size,
reader.Entry.CompressionType,
Convert.ToBase64String(memory.ToArray())
)
);
}
return entries.OrderBy(entry => entry.Key, StringComparer.Ordinal).ToList();
}
private static byte[] CreateLargeTarArchive()
{
using var stream = new MemoryStream();
using (
var writer = WriterFactory.OpenWriter(
stream,
ArchiveType.Tar,
new WriterOptions(CompressionType.None)
)
)
{
writer.Write("large.bin", new MemoryStream(new byte[64 * 1024]));
}
return stream.ToArray();
}
private sealed record EntrySnapshot(
string Key,
long Size,
CompressionType CompressionType,
string Content
);
private sealed class CancelAfterBytesReadStream(
Stream stream,
CancellationTokenSource cancellationTokenSource,
long cancelAfterBytes
) : Stream
{
private long _bytesRead;
public override bool CanRead => stream.CanRead;
public override bool CanSeek => stream.CanSeek;
public override bool CanWrite => false;
public override long Length => stream.Length;
public override long Position
{
get => stream.Position;
set => stream.Position = value;
}
public override void Flush() => stream.Flush();
public override int Read(byte[] buffer, int offset, int count) =>
throw new NotSupportedException("Use async reads for this test stream.");
public override async ValueTask<int> ReadAsync(
Memory<byte> buffer,
CancellationToken cancellationToken = default
)
{
cancellationToken.ThrowIfCancellationRequested();
var read = await stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
_bytesRead = read;
if (_bytesRead > cancelAfterBytes)
{
cancellationTokenSource.Cancel();
cancellationToken.ThrowIfCancellationRequested();
}
return read;
}
public override Task<int> ReadAsync(
byte[] buffer,
int offset,
int count,
CancellationToken cancellationToken
) => ReadAsync(buffer.AsMemory(offset, count), cancellationToken).AsTask();
public override long Seek(long offset, SeekOrigin origin) => stream.Seek(offset, origin);
public override void SetLength(long value) => throw new NotSupportedException();
public override void Write(byte[] buffer, int offset, int count) =>
throw new NotSupportedException();
}
}
#endif