release orchestrator v1 draft and build fixes
This commit is contained in:
@@ -0,0 +1,348 @@
|
||||
// Copyright (c) StellaOps. All rights reserved.
|
||||
// Licensed under AGPL-3.0-or-later. See LICENSE in the project root.
|
||||
|
||||
using FluentAssertions;
|
||||
using StellaOps.Scanner.ChangeTrace.ByteDiff;
|
||||
|
||||
namespace StellaOps.Scanner.ChangeTrace.Tests.ByteDiff;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for ByteLevelDiffer.
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class ByteLevelDifferTests
|
||||
{
|
||||
private readonly ByteLevelDiffer _differ = new();
|
||||
|
||||
[Fact]
|
||||
public async Task CompareAsync_IdenticalStreams_ReturnsNoDeltas()
|
||||
{
|
||||
// Arrange
|
||||
var data = CreateTestData(4096);
|
||||
using var fromStream = new MemoryStream(data);
|
||||
using var toStream = new MemoryStream(data);
|
||||
|
||||
// Act
|
||||
var deltas = await _differ.CompareAsync(fromStream, toStream);
|
||||
|
||||
// Assert
|
||||
deltas.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CompareAsync_DifferentStreams_ReturnsDeltas()
|
||||
{
|
||||
// Arrange
|
||||
var fromData = CreateTestData(4096);
|
||||
var toData = CreateTestData(4096, seed: 99); // Different seed = different data
|
||||
using var fromStream = new MemoryStream(fromData);
|
||||
using var toStream = new MemoryStream(toData);
|
||||
|
||||
// Act
|
||||
var deltas = await _differ.CompareAsync(fromStream, toStream);
|
||||
|
||||
// Assert
|
||||
deltas.Should().NotBeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CompareAsync_PartialChange_DetectsChangedWindow()
|
||||
{
|
||||
// Arrange
|
||||
var fromData = CreateTestData(8192);
|
||||
var toData = (byte[])fromData.Clone();
|
||||
// Modify only the second window (bytes 2048-4095)
|
||||
for (var i = 2048; i < 4096; i++)
|
||||
{
|
||||
toData[i] = (byte)(toData[i] ^ 0xFF);
|
||||
}
|
||||
|
||||
using var fromStream = new MemoryStream(fromData);
|
||||
using var toStream = new MemoryStream(toData);
|
||||
|
||||
// Act
|
||||
var deltas = await _differ.CompareAsync(fromStream, toStream);
|
||||
|
||||
// Assert
|
||||
deltas.Should().HaveCount(1);
|
||||
deltas[0].Offset.Should().Be(2048);
|
||||
deltas[0].Size.Should().Be(2048);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CompareAsync_TruncatedToFile_ReportsDeltas()
|
||||
{
|
||||
// Arrange
|
||||
var fromData = CreateTestData(8192);
|
||||
var toData = fromData[..4096]; // Truncated to half
|
||||
using var fromStream = new MemoryStream(fromData);
|
||||
using var toStream = new MemoryStream(toData);
|
||||
|
||||
// Act
|
||||
var deltas = await _differ.CompareAsync(fromStream, toStream);
|
||||
|
||||
// Assert
|
||||
deltas.Should().NotBeEmpty();
|
||||
deltas.Should().Contain(d => d.ToHash == "truncated");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CompareAsync_WithOptions_UsesWindowSize()
|
||||
{
|
||||
// Arrange
|
||||
var fromData = CreateTestData(4096);
|
||||
var toData = (byte[])fromData.Clone();
|
||||
// Modify only first 512 bytes
|
||||
for (var i = 0; i < 512; i++)
|
||||
{
|
||||
toData[i] = (byte)(toData[i] ^ 0xFF);
|
||||
}
|
||||
|
||||
using var fromStream = new MemoryStream(fromData);
|
||||
using var toStream = new MemoryStream(toData);
|
||||
|
||||
var options = new ByteDiffOptions
|
||||
{
|
||||
WindowSize = 1024,
|
||||
StepSize = 1024
|
||||
};
|
||||
|
||||
// Act
|
||||
var deltas = await _differ.CompareAsync(fromStream, toStream, options);
|
||||
|
||||
// Assert
|
||||
deltas.Should().HaveCount(1);
|
||||
deltas[0].Size.Should().Be(1024); // Reports full window even though only 512 changed
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CompareAsync_WithContext_IncludesContextDescription()
|
||||
{
|
||||
// Arrange
|
||||
var fromData = CreateTestData(4096);
|
||||
var toData = CreateTestData(4096, seed: 99);
|
||||
using var fromStream = new MemoryStream(fromData);
|
||||
using var toStream = new MemoryStream(toData);
|
||||
|
||||
var options = new ByteDiffOptions
|
||||
{
|
||||
IncludeContext = true
|
||||
};
|
||||
|
||||
// Act
|
||||
var deltas = await _differ.CompareAsync(fromStream, toStream, options);
|
||||
|
||||
// Assert
|
||||
// Context is included when there are multiple consecutive changes
|
||||
deltas.Should().NotBeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CompareAsync_LargeFiles_UsesSampling()
|
||||
{
|
||||
// Arrange
|
||||
var size = 11 * 1024 * 1024; // 11MB - over default 10MB limit
|
||||
var fromData = new byte[size];
|
||||
var toData = new byte[size];
|
||||
new Random(42).NextBytes(fromData);
|
||||
new Random(43).NextBytes(toData);
|
||||
|
||||
using var fromStream = new MemoryStream(fromData);
|
||||
using var toStream = new MemoryStream(toData);
|
||||
|
||||
var options = new ByteDiffOptions
|
||||
{
|
||||
MaxFileSize = 10 * 1024 * 1024
|
||||
};
|
||||
|
||||
// Act
|
||||
var deltas = await _differ.CompareAsync(fromStream, toStream, options);
|
||||
|
||||
// Assert - Should complete without memory issues and return sampled deltas
|
||||
deltas.Should().NotBeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CompareAsync_DisableSectionAnalysis_UsesFullBinaryComparison()
|
||||
{
|
||||
// Arrange
|
||||
var fromData = CreateTestData(4096);
|
||||
var toData = CreateTestData(4096, seed: 99);
|
||||
using var fromStream = new MemoryStream(fromData);
|
||||
using var toStream = new MemoryStream(toData);
|
||||
|
||||
var options = new ByteDiffOptions
|
||||
{
|
||||
AnalyzeBySections = false
|
||||
};
|
||||
|
||||
// Act
|
||||
var deltas = await _differ.CompareAsync(fromStream, toStream, options);
|
||||
|
||||
// Assert
|
||||
deltas.Should().NotBeEmpty();
|
||||
deltas.Should().AllSatisfy(d => d.Section.Should().BeNull());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CompareAsync_CancellationToken_Respected()
|
||||
{
|
||||
// Arrange
|
||||
var fromData = CreateTestData(4096);
|
||||
var toData = CreateTestData(4096, seed: 99);
|
||||
using var fromStream = new MemoryStream(fromData);
|
||||
using var toStream = new MemoryStream(toData);
|
||||
var cts = new CancellationTokenSource();
|
||||
cts.Cancel();
|
||||
|
||||
// Act & Assert
|
||||
var action = async () => await _differ.CompareAsync(fromStream, toStream, ct: cts.Token);
|
||||
await action.Should().ThrowAsync<OperationCanceledException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CompareAsync_NullFromStream_ThrowsArgumentNullException()
|
||||
{
|
||||
// Arrange
|
||||
using var toStream = new MemoryStream();
|
||||
|
||||
// Act & Assert
|
||||
var action = async () => await _differ.CompareAsync(null!, toStream);
|
||||
await action.Should().ThrowAsync<ArgumentNullException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CompareAsync_NullToStream_ThrowsArgumentNullException()
|
||||
{
|
||||
// Arrange
|
||||
using var fromStream = new MemoryStream();
|
||||
|
||||
// Act & Assert
|
||||
var action = async () => await _differ.CompareAsync(fromStream, null!);
|
||||
await action.Should().ThrowAsync<ArgumentNullException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CompareAsync_DeltaHasCorrectScope()
|
||||
{
|
||||
// Arrange
|
||||
var fromData = CreateTestData(4096);
|
||||
var toData = CreateTestData(4096, seed: 99);
|
||||
using var fromStream = new MemoryStream(fromData);
|
||||
using var toStream = new MemoryStream(toData);
|
||||
|
||||
// Act
|
||||
var deltas = await _differ.CompareAsync(fromStream, toStream);
|
||||
|
||||
// Assert
|
||||
deltas.Should().AllSatisfy(d => d.Scope.Should().Be("byte"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CompareAsync_DeltasAreDeterministic()
|
||||
{
|
||||
// Arrange
|
||||
var fromData = CreateTestData(8192);
|
||||
var toData = CreateTestData(8192, seed: 99);
|
||||
|
||||
// Act - Compare twice
|
||||
using var fromStream1 = new MemoryStream(fromData);
|
||||
using var toStream1 = new MemoryStream(toData);
|
||||
var deltas1 = await _differ.CompareAsync(fromStream1, toStream1);
|
||||
|
||||
using var fromStream2 = new MemoryStream(fromData);
|
||||
using var toStream2 = new MemoryStream(toData);
|
||||
var deltas2 = await _differ.CompareAsync(fromStream2, toStream2);
|
||||
|
||||
// Assert - Results should be identical
|
||||
deltas1.Should().HaveCount(deltas2.Count);
|
||||
for (var i = 0; i < deltas1.Count; i++)
|
||||
{
|
||||
deltas1[i].Offset.Should().Be(deltas2[i].Offset);
|
||||
deltas1[i].Size.Should().Be(deltas2[i].Size);
|
||||
deltas1[i].FromHash.Should().Be(deltas2[i].FromHash);
|
||||
deltas1[i].ToHash.Should().Be(deltas2[i].ToHash);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CompareAsync_HashesAreValidSha256Hex()
|
||||
{
|
||||
// Arrange
|
||||
var fromData = CreateTestData(4096);
|
||||
var toData = CreateTestData(4096, seed: 99);
|
||||
using var fromStream = new MemoryStream(fromData);
|
||||
using var toStream = new MemoryStream(toData);
|
||||
|
||||
// Act
|
||||
var deltas = await _differ.CompareAsync(fromStream, toStream);
|
||||
|
||||
// Assert
|
||||
deltas.Should().NotBeEmpty();
|
||||
foreach (var delta in deltas)
|
||||
{
|
||||
if (delta.FromHash != "absent" && delta.FromHash != "truncated" && delta.FromHash != "removed")
|
||||
{
|
||||
delta.FromHash.Should().MatchRegex("^[a-f0-9]{64}$");
|
||||
}
|
||||
if (delta.ToHash != "absent" && delta.ToHash != "truncated" && delta.ToHash != "removed")
|
||||
{
|
||||
delta.ToHash.Should().MatchRegex("^[a-f0-9]{64}$");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CompareAsync_MinConsecutiveChanges_MergesDeltas()
|
||||
{
|
||||
// Arrange
|
||||
var fromData = CreateTestData(8192);
|
||||
var toData = CreateTestData(8192, seed: 99); // Completely different
|
||||
using var fromStream = new MemoryStream(fromData);
|
||||
using var toStream = new MemoryStream(toData);
|
||||
|
||||
var options = new ByteDiffOptions
|
||||
{
|
||||
WindowSize = 1024,
|
||||
StepSize = 1024,
|
||||
MinConsecutiveChanges = 2
|
||||
};
|
||||
|
||||
// Act
|
||||
var deltas = await _differ.CompareAsync(fromStream, toStream, options);
|
||||
|
||||
// Assert - Should have merged consecutive changes
|
||||
deltas.Should().NotBeEmpty();
|
||||
// With 8192 bytes and 1024 window, there are 8 windows
|
||||
// All windows are different, so they should be merged into fewer deltas
|
||||
deltas.Count.Should().BeLessThan(8);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CompareAsync_AddedBytes_DetectedAsAbsent()
|
||||
{
|
||||
// Arrange
|
||||
var fromData = CreateTestData(4096);
|
||||
var toData = new byte[8192];
|
||||
Array.Copy(fromData, toData, fromData.Length);
|
||||
new Random(99).NextBytes(toData.AsSpan(4096)); // Add random data at end
|
||||
|
||||
using var fromStream = new MemoryStream(fromData);
|
||||
using var toStream = new MemoryStream(toData);
|
||||
|
||||
// Act
|
||||
var deltas = await _differ.CompareAsync(fromStream, toStream);
|
||||
|
||||
// Assert
|
||||
deltas.Should().Contain(d => d.FromHash == "absent");
|
||||
}
|
||||
|
||||
// Helper methods
|
||||
|
||||
private static byte[] CreateTestData(int size, int seed = 42)
|
||||
{
|
||||
var data = new byte[size];
|
||||
new Random(seed).NextBytes(data);
|
||||
return data;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user