Restructure solution layout by module
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
using System.Text;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using StellaOps.Scanner.Cache;
|
||||
using StellaOps.Scanner.Cache.Abstractions;
|
||||
using StellaOps.Scanner.Cache.FileCas;
|
||||
using StellaOps.Scanner.Cache.LayerCache;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scanner.Cache.Tests;
|
||||
|
||||
public sealed class LayerCacheRoundTripTests : IAsyncLifetime
|
||||
{
|
||||
private readonly string _rootPath;
|
||||
private readonly FakeTimeProvider _timeProvider;
|
||||
private readonly IOptions<ScannerCacheOptions> _options;
|
||||
private readonly LayerCacheStore _layerCache;
|
||||
private readonly FileContentAddressableStore _fileCas;
|
||||
|
||||
public LayerCacheRoundTripTests()
|
||||
{
|
||||
_rootPath = Path.Combine(Path.GetTempPath(), "stellaops-cache-tests", Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(_rootPath);
|
||||
|
||||
_timeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 10, 19, 12, 0, 0, TimeSpan.Zero));
|
||||
|
||||
var optionsValue = new ScannerCacheOptions
|
||||
{
|
||||
RootPath = _rootPath,
|
||||
LayerTtl = TimeSpan.FromHours(1),
|
||||
FileTtl = TimeSpan.FromHours(2),
|
||||
MaxBytes = 512 * 1024, // 512 KiB
|
||||
WarmBytesThreshold = 256 * 1024,
|
||||
ColdBytesThreshold = 400 * 1024,
|
||||
MaintenanceInterval = TimeSpan.FromMinutes(5)
|
||||
};
|
||||
|
||||
_options = Options.Create(optionsValue);
|
||||
_layerCache = new LayerCacheStore(_options, NullLogger<LayerCacheStore>.Instance, _timeProvider);
|
||||
_fileCas = new FileContentAddressableStore(_options, NullLogger<FileContentAddressableStore>.Instance, _timeProvider);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RoundTrip_Succeeds_And_Respects_Ttl_And_ImportExport()
|
||||
{
|
||||
var layerDigest = "sha256:abcd1234";
|
||||
var metadata = new Dictionary<string, string>
|
||||
{
|
||||
["image"] = "ghcr.io/stella/sample:1",
|
||||
["schema"] = "1.0"
|
||||
};
|
||||
|
||||
using var inventoryStream = CreateStream("inventory" + Environment.NewLine + "component:libfoo" + Environment.NewLine);
|
||||
using var usageStream = CreateStream("usage" + Environment.NewLine + "component:bin" + Environment.NewLine);
|
||||
|
||||
var request = new LayerCachePutRequest(
|
||||
layerDigest,
|
||||
architecture: "linux/amd64",
|
||||
mediaType: "application/vnd.oci.image.layer.v1.tar",
|
||||
metadata,
|
||||
new List<LayerCacheArtifactContent>
|
||||
{
|
||||
new("inventory.cdx.json", inventoryStream, "application/json"),
|
||||
new("usage.cdx.json", usageStream, "application/json")
|
||||
});
|
||||
|
||||
var stored = await _layerCache.PutAsync(request, CancellationToken.None);
|
||||
stored.LayerDigest.Should().Be(layerDigest);
|
||||
stored.Artifacts.Should().ContainKey("inventory.cdx.json");
|
||||
stored.TotalSizeBytes.Should().BeGreaterThan(0);
|
||||
|
||||
var cached = await _layerCache.TryGetAsync(layerDigest, CancellationToken.None);
|
||||
cached.Should().NotBeNull();
|
||||
cached!.Metadata.Should().ContainKey("image");
|
||||
|
||||
await using (var artifact = await _layerCache.OpenArtifactAsync(layerDigest, "inventory.cdx.json", CancellationToken.None))
|
||||
{
|
||||
artifact.Should().NotBeNull();
|
||||
using var reader = new StreamReader(artifact!, Encoding.UTF8);
|
||||
var content = await reader.ReadToEndAsync();
|
||||
content.Should().Contain("component:libfoo");
|
||||
}
|
||||
|
||||
// Store file CAS entry and validate export/import lifecycle.
|
||||
var casHash = "sha256:" + new string('f', 64);
|
||||
using var casStream = CreateStream("some-cas-content");
|
||||
await _fileCas.PutAsync(new FileCasPutRequest(casHash, casStream), CancellationToken.None);
|
||||
|
||||
var exportPath = Path.Combine(_rootPath, "export");
|
||||
var exportCount = await _fileCas.ExportAsync(exportPath, CancellationToken.None);
|
||||
exportCount.Should().Be(1);
|
||||
|
||||
await _fileCas.RemoveAsync(casHash, CancellationToken.None);
|
||||
(await _fileCas.TryGetAsync(casHash, CancellationToken.None)).Should().BeNull();
|
||||
|
||||
var importCount = await _fileCas.ImportAsync(exportPath, CancellationToken.None);
|
||||
importCount.Should().Be(1);
|
||||
var imported = await _fileCas.TryGetAsync(casHash, CancellationToken.None);
|
||||
imported.Should().NotBeNull();
|
||||
imported!.RelativePath.Should().EndWith("content.bin");
|
||||
|
||||
// TTL eviction
|
||||
_timeProvider.Advance(TimeSpan.FromHours(2));
|
||||
await _layerCache.EvictExpiredAsync(CancellationToken.None);
|
||||
(await _layerCache.TryGetAsync(layerDigest, CancellationToken.None)).Should().BeNull();
|
||||
|
||||
// Compaction removes CAS entry once over threshold.
|
||||
// Force compaction by writing a large entry.
|
||||
using var largeStream = CreateStream(new string('x', 400_000));
|
||||
var largeHash = "sha256:" + new string('e', 64);
|
||||
await _fileCas.PutAsync(new FileCasPutRequest(largeHash, largeStream), CancellationToken.None);
|
||||
_timeProvider.Advance(TimeSpan.FromMinutes(1));
|
||||
await _fileCas.CompactAsync(CancellationToken.None);
|
||||
(await _fileCas.TryGetAsync(casHash, CancellationToken.None)).Should().BeNull();
|
||||
}
|
||||
|
||||
public Task InitializeAsync() => Task.CompletedTask;
|
||||
|
||||
public Task DisposeAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Directory.Exists(_rootPath))
|
||||
{
|
||||
Directory.Delete(_rootPath, recursive: true);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignored – best effort cleanup.
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private static MemoryStream CreateStream(string content)
|
||||
=> new(Encoding.UTF8.GetBytes(content));
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" Version="6.12.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0-rc.2.25502.107" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Remove="..\StellaOps.Concelier.Tests.Shared\AssemblyInfo.cs" />
|
||||
<Compile Remove="..\StellaOps.Concelier.Tests.Shared\MongoFixtureCollection.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Remove="..\StellaOps.Concelier.Testing\StellaOps.Concelier.Testing.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Using Remove="StellaOps.Concelier.Testing" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Scanner.Cache/StellaOps.Scanner.Cache.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
Reference in New Issue
Block a user