Skip to content

Commit

Permalink
Merge pull request #14 from Advanced-Systems/compression
Browse files Browse the repository at this point in the history
Compression
  • Loading branch information
StefanGreve authored Aug 28, 2024
2 parents b3cf6ae + a5e49d2 commit 10b72eb
Show file tree
Hide file tree
Showing 8 changed files with 329 additions and 1 deletion.
49 changes: 49 additions & 0 deletions AdvancedSystems.Core.Abstractions/ICompressionService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using System;
using System.IO.Compression;

namespace AdvancedSystems.Core.Abstractions;

/// <summary>
/// Defines methods for compressing and decompressing data.
/// </summary>
public interface ICompressionService
{
#region Methods

/// <summary>
/// Compresses the given byte array using the specified compression level.
/// </summary>
/// <param name="expandedBuffer">
/// The data to be compressed.
/// </param>
/// <param name="compressionLevel">
/// The level of compression to apply.
/// </param>
/// <returns>
/// A byte array containing the compressed data.
/// </returns>
/// <exception cref="ArgumentException">
/// Thrown when <paramref name="expandedBuffer"/> is empty.
/// </exception>
byte[] Compress(ReadOnlyMemory<byte> expandedBuffer, CompressionLevel compressionLevel);

/// <summary>
/// Expands (decompresses) the given byte array that has been compressed.
/// </summary>
/// <param name="compressedBuffer">
/// The compressed data to be expanded
/// </param>
/// <returns>
/// A byte array containing the decompressed data.
/// </returns>
/// <exception cref="ArgumentException">
/// Thrown when <paramref name="compressedBuffer"/> is empty.
/// </exception>
/// <remarks>
/// This method assumes that the provided <paramref name="compressedBuffer"/> contains data that was compressed using
/// a compatible compression algorithm (see also: <seealso cref="Compress(ReadOnlyMemory{byte}, CompressionLevel)"/>).
/// </remarks>
byte[] Expand(ReadOnlyMemory<byte> compressedBuffer);

#endregion
}
76 changes: 76 additions & 0 deletions AdvancedSystems.Core.Tests/Common/ArchiveTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
using System;
using System.IO.Compression;
using System.Linq;

using AdvancedSystems.Core.Common;

using Xunit;

namespace AdvancedSystems.Core.Tests.Common;

public class ArchiveTests
{
#region Tests

[Fact]
public void TestCompress()
{
// Arrange
byte[] buffer = Enumerable.Repeat<byte>(0xFF, 100).ToArray();

// Act
byte[] compressedBuffer = Archive.Compress(buffer, CompressionLevel.Optimal);

// Assert
Assert.NotEmpty(compressedBuffer);
Assert.True(buffer.Length > compressedBuffer.Length);
}

[Fact]
public void TestCompress_ThrowsIfEmpty()
{
// Arrange
byte[] buffer = Enumerable.Empty<byte>().ToArray();

// Act
void Compress()
{
Archive.Compress(buffer, CompressionLevel.Optimal);
}

// Assert
Assert.Throws<ArgumentException>(Compress);
}

[Fact]
public void TestExpand()
{
// Arrange
byte[] buffer = Enumerable.Repeat<byte>(0xFF, 100).ToArray();

// Act
byte[] compressedBuffer = Archive.Compress(buffer, CompressionLevel.Optimal);
byte[] expandedBuffer = Archive.Expand(compressedBuffer);

// Assert
Assert.Equal(buffer, expandedBuffer);
}

[Fact]
public void TestExpand_ThrowsIfEmpty()
{
// Arrange
byte[] buffer = Enumerable.Empty<byte>().ToArray();

// Act
void Expand()
{
Archive.Expand(buffer);
}

// Assert
Assert.Throws<ArgumentException>(Expand);
}

#endregion
}
24 changes: 24 additions & 0 deletions AdvancedSystems.Core.Tests/Fixtures/CompressionServiceFixture.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using AdvancedSystems.Core.Abstractions;
using AdvancedSystems.Core.Services;

using Microsoft.Extensions.Logging;

using Moq;

namespace AdvancedSystems.Core.Tests.Fixtures;

public class CompressionServiceFixture
{
public CompressionServiceFixture()
{
this.CompressionService = new CompressionService(this.Logger.Object);
}

#region Properties

public Mock<ILogger<CompressionService>> Logger { get; private set; } = new();

public ICompressionService CompressionService { get; private set; }

#endregion
}
85 changes: 85 additions & 0 deletions AdvancedSystems.Core.Tests/Services/CompressionServiceTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
using System;
using System.IO.Compression;
using System.Linq;
using System.Threading.Tasks;

using AdvancedSystems.Core.Abstractions;
using AdvancedSystems.Core.DependencyInjection;
using AdvancedSystems.Core.Tests.Fixtures;

using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

using Moq;

using Xunit;

namespace AdvancedSystems.Core.Tests.Services;

public class CompressionServiceTests : IClassFixture<CompressionServiceFixture>
{
private readonly CompressionServiceFixture _sut;

public CompressionServiceTests(CompressionServiceFixture fixture)
{
this._sut = fixture;
}

#region Tests

[Fact]
public void TestCompressionRoundtrip()
{
// Arrange
var buffer = Enumerable.Repeat<byte>(0xFF, 100).ToArray();
var compressionLevel = CompressionLevel.Optimal;

// Act
var compressedBuffer = this._sut.CompressionService.Compress(buffer, compressionLevel);
var expandedBuffer = this._sut.CompressionService.Expand(compressedBuffer);

// Assert
Assert.NotEmpty(compressedBuffer);
Assert.True(buffer.Length > compressedBuffer.Length);
Assert.Equal(buffer, expandedBuffer);
this._sut.Logger.Verify(x => x.Log(
LogLevel.Trace,
It.IsAny<EventId>(),
It.Is<It.IsAnyType>((v, t) => v.ToString()!.Contains(Enum.GetName(compressionLevel)!)),
It.IsAny<Exception>(),
It.Is<Func<It.IsAnyType, Exception?, string>>((v, t) => true))
);
}

[Fact]
public async Task TestAddCompressionService()
{
// Arrange
using var hostBuilder = await new HostBuilder()
.ConfigureWebHost(builder =>
{
builder.UseTestServer();
builder.ConfigureServices(services =>
{
services.AddCompressionService();
});
builder.Configure(app =>
{

});
})
.StartAsync();

// Act
var compressionService = hostBuilder.Services.GetService<ICompressionService>();

// Assert
Assert.NotNull(compressionService);
await hostBuilder.StopAsync();
}

#endregion
}
2 changes: 1 addition & 1 deletion AdvancedSystems.Core/AdvancedSystems.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<Title>Advanced Systems Core Library</Title>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AdvancedSystems.Core.Abstractions" Version="8.0.0-alpha.5" />
<PackageReference Include="AdvancedSystems.Core.Abstractions" Version="8.0.0-alpha.6" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.1" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.0" />
Expand Down
51 changes: 51 additions & 0 deletions AdvancedSystems.Core/Common/Archive.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
using System;
using System.IO;
using System.IO.Compression;

using AdvancedSystems.Core.Abstractions;

namespace AdvancedSystems.Core.Common;

/// <inheritdoc cref="ICompressionService" />
public static class Archive
{
/// <inheritdoc cref="ICompressionService.Compress(ReadOnlyMemory{byte}, CompressionLevel)" />
public static byte[] Compress(ReadOnlyMemory<byte> expandedBuffer, CompressionLevel compressionLevel)
{
if (expandedBuffer.IsEmpty) throw new ArgumentException("Buffer to compress cannot be empty.", nameof(expandedBuffer));
using var compressedStream = new MemoryStream();

unsafe
{
fixed (byte* pointer = &expandedBuffer.Span[0])
{
using var expandedStream = new UnmanagedMemoryStream(pointer, expandedBuffer.Length);

using var gzipStream = new GZipStream(compressedStream, compressionLevel, leaveOpen: false);
expandedStream.CopyTo(gzipStream);
}
}

return compressedStream.ToArray();
}

/// <inheritdoc cref="ICompressionService.Expand(ReadOnlyMemory{byte})" />
public static byte[] Expand(ReadOnlyMemory<byte> compressedBuffer)
{
if (compressedBuffer.IsEmpty) throw new ArgumentException("Buffer to expand cannot be empty.", nameof(compressedBuffer));
using var expandedStream = new MemoryStream();

unsafe
{
fixed (byte* pointer = &compressedBuffer.Span[0])
{
using var compressedStream = new UnmanagedMemoryStream(pointer, compressedBuffer.Length);

using var gzipStream = new GZipStream(compressedStream, CompressionMode.Decompress, leaveOpen: false);
gzipStream.CopyTo(expandedStream);
}
}

return expandedStream.ToArray();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ public static IServiceCollection AddCachingService(this IServiceCollection servi
return services;
}

public static IServiceCollection AddCompressionService(this IServiceCollection services)
{
services.TryAdd(ServiceDescriptor.Scoped<ICompressionService, CompressionService>());
return services;
}

[Experimental("Preview008")]
public static IServiceCollection AddMessageBus(this IServiceCollection services)
{
Expand Down
37 changes: 37 additions & 0 deletions AdvancedSystems.Core/Services/CompressionService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using System;
using System.IO.Compression;

using AdvancedSystems.Core.Abstractions;
using AdvancedSystems.Core.Common;

using Microsoft.Extensions.Logging;

namespace AdvancedSystems.Core.Services;

/// <inheritdoc cref="ICompressionService" />
public sealed class CompressionService : ICompressionService
{
private readonly ILogger<CompressionService> _logger;

public CompressionService(ILogger<CompressionService> logger)
{
this._logger = logger;
}

#region Methods

/// <inheritdoc />
public byte[] Compress(ReadOnlyMemory<byte> expandedBuffer, CompressionLevel compressionLevel)
{
this._logger.LogTrace("Compression Level = {CompressionLevel}", compressionLevel);
return Archive.Compress(expandedBuffer, compressionLevel);
}

/// <inheritdoc />
public byte[] Expand(ReadOnlyMemory<byte> compressedBuffer)
{
return Archive.Expand(compressedBuffer);
}

#endregion
}

0 comments on commit 10b72eb

Please sign in to comment.