Add property-based tests for SBOM/VEX document ordering and Unicode normalization determinism
- Implement `SbomVexOrderingDeterminismProperties` for testing component list and vulnerability metadata hash consistency. - Create `UnicodeNormalizationDeterminismProperties` to validate NFC normalization and Unicode string handling. - Add project file for `StellaOps.Testing.Determinism.Properties` with necessary dependencies. - Introduce CI/CD template validation tests including YAML syntax checks and documentation content verification. - Create validation script for CI/CD templates ensuring all required files and structures are present.
This commit is contained in:
@@ -0,0 +1,308 @@
|
||||
using StellaOps.Signals.Services;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Signals.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for SlimSymbolCache.
|
||||
/// Sprint: SPRINT_20251226_010_SIGNALS_runtime_stack
|
||||
/// Task: STACK-17
|
||||
/// </summary>
|
||||
public sealed class SlimSymbolCacheTests : IDisposable
|
||||
{
|
||||
private readonly SlimSymbolCache _cache;
|
||||
private readonly string _tempPath;
|
||||
|
||||
public SlimSymbolCacheTests()
|
||||
{
|
||||
_tempPath = Path.Combine(Path.GetTempPath(), $"symbol-cache-test-{Guid.NewGuid()}");
|
||||
_cache = new SlimSymbolCache(new SlimSymbolCacheOptions
|
||||
{
|
||||
MaxEntries = 100,
|
||||
EntryTtl = TimeSpan.FromHours(1),
|
||||
PersistencePath = _tempPath,
|
||||
PersistOnAdd = true,
|
||||
EnableAutoCleanup = false,
|
||||
});
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_cache.Dispose();
|
||||
if (Directory.Exists(_tempPath))
|
||||
{
|
||||
Directory.Delete(_tempPath, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Add_ShouldAddSymbolsToCache()
|
||||
{
|
||||
// Arrange
|
||||
var buildId = "abcd1234";
|
||||
var symbols = new List<SymbolEntry>
|
||||
{
|
||||
new() { StartAddress = 0x1000, Size = 100, Name = "main" },
|
||||
new() { StartAddress = 0x1100, Size = 50, Name = "parse" },
|
||||
};
|
||||
|
||||
// Act
|
||||
_cache.Add(buildId, "libtest.so", symbols);
|
||||
|
||||
// Assert
|
||||
Assert.True(_cache.Contains(buildId));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryResolve_ShouldResolveKnownAddress()
|
||||
{
|
||||
// Arrange
|
||||
var buildId = "abcd1234";
|
||||
var symbols = new List<SymbolEntry>
|
||||
{
|
||||
new() { StartAddress = 0x1000, Size = 100, Name = "main" },
|
||||
};
|
||||
_cache.Add(buildId, "libtest.so", symbols);
|
||||
|
||||
// Act
|
||||
var found = _cache.TryResolve(buildId, 0x1050, out var symbol);
|
||||
|
||||
// Assert
|
||||
Assert.True(found);
|
||||
Assert.NotNull(symbol);
|
||||
Assert.Equal("main", symbol.FunctionName);
|
||||
Assert.Equal(0x50UL, symbol.Offset);
|
||||
Assert.Equal(buildId, symbol.BuildId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryResolve_ShouldReturnFalseForUnknownBuildId()
|
||||
{
|
||||
// Act
|
||||
var found = _cache.TryResolve("unknown", 0x1000, out var symbol);
|
||||
|
||||
// Assert
|
||||
Assert.False(found);
|
||||
Assert.Null(symbol);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryResolve_ShouldReturnFalseForAddressOutsideSymbols()
|
||||
{
|
||||
// Arrange
|
||||
var buildId = "abcd1234";
|
||||
var symbols = new List<SymbolEntry>
|
||||
{
|
||||
new() { StartAddress = 0x1000, Size = 100, Name = "main" },
|
||||
};
|
||||
_cache.Add(buildId, "libtest.so", symbols);
|
||||
|
||||
// Act
|
||||
var found = _cache.TryResolve(buildId, 0x2000, out var symbol);
|
||||
|
||||
// Assert
|
||||
Assert.False(found);
|
||||
Assert.Null(symbol);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryResolve_ShouldUseBinarySearchForLargeSymbolTable()
|
||||
{
|
||||
// Arrange
|
||||
var buildId = "large-table";
|
||||
var symbols = Enumerable.Range(0, 1000)
|
||||
.Select(i => new SymbolEntry
|
||||
{
|
||||
StartAddress = (ulong)(i * 100),
|
||||
Size = 50,
|
||||
Name = $"func_{i}",
|
||||
})
|
||||
.ToList();
|
||||
|
||||
_cache.Add(buildId, "liblarge.so", symbols);
|
||||
|
||||
// Act - resolve address in the middle
|
||||
var found = _cache.TryResolve(buildId, 50025, out var symbol);
|
||||
|
||||
// Assert
|
||||
Assert.True(found);
|
||||
Assert.NotNull(symbol);
|
||||
Assert.Equal("func_500", symbol.FunctionName);
|
||||
Assert.Equal(25UL, symbol.Offset);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetStatistics_ShouldReturnCorrectStats()
|
||||
{
|
||||
// Arrange
|
||||
var symbols = new List<SymbolEntry>
|
||||
{
|
||||
new() { StartAddress = 0x1000, Size = 100, Name = "main" },
|
||||
new() { StartAddress = 0x1100, Size = 50, Name = "parse" },
|
||||
};
|
||||
_cache.Add("build1", "lib1.so", symbols);
|
||||
_cache.Add("build2", "lib2.so", symbols);
|
||||
|
||||
_cache.TryResolve("build1", 0x1050, out _); // hit
|
||||
_cache.TryResolve("unknown", 0x1000, out _); // miss
|
||||
|
||||
// Act
|
||||
var stats = _cache.GetStatistics();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2, stats.EntryCount);
|
||||
Assert.Equal(4, stats.TotalSymbols);
|
||||
Assert.Equal(1, stats.HitCount);
|
||||
Assert.Equal(1, stats.MissCount);
|
||||
Assert.Equal(0.5, stats.HitRate);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Clear_ShouldRemoveAllEntries()
|
||||
{
|
||||
// Arrange
|
||||
var symbols = new List<SymbolEntry>
|
||||
{
|
||||
new() { StartAddress = 0x1000, Size = 100, Name = "main" },
|
||||
};
|
||||
_cache.Add("build1", "lib1.so", symbols);
|
||||
_cache.Add("build2", "lib2.so", symbols);
|
||||
|
||||
// Act
|
||||
_cache.Clear();
|
||||
|
||||
// Assert
|
||||
Assert.False(_cache.Contains("build1"));
|
||||
Assert.False(_cache.Contains("build2"));
|
||||
var stats = _cache.GetStatistics();
|
||||
Assert.Equal(0, stats.EntryCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Remove_ShouldRemoveSpecificEntry()
|
||||
{
|
||||
// Arrange
|
||||
var symbols = new List<SymbolEntry>
|
||||
{
|
||||
new() { StartAddress = 0x1000, Size = 100, Name = "main" },
|
||||
};
|
||||
_cache.Add("build1", "lib1.so", symbols);
|
||||
_cache.Add("build2", "lib2.so", symbols);
|
||||
|
||||
// Act
|
||||
var removed = _cache.Remove("build1");
|
||||
|
||||
// Assert
|
||||
Assert.True(removed);
|
||||
Assert.False(_cache.Contains("build1"));
|
||||
Assert.True(_cache.Contains("build2"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Persistence_ShouldWriteToDisk()
|
||||
{
|
||||
// Arrange
|
||||
var symbols = new List<SymbolEntry>
|
||||
{
|
||||
new() { StartAddress = 0x1000, Size = 100, Name = "main" },
|
||||
};
|
||||
|
||||
// Act
|
||||
_cache.Add("persist-test", "libtest.so", symbols);
|
||||
|
||||
// Assert
|
||||
var files = Directory.GetFiles(_tempPath, "*.symbols");
|
||||
Assert.NotEmpty(files);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TrustedSymbols_ShouldBeFlaggedCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var symbols = new List<SymbolEntry>
|
||||
{
|
||||
new() { StartAddress = 0x1000, Size = 100, Name = "main" },
|
||||
};
|
||||
_cache.Add("trusted-build", "libtrusted.so", symbols, isTrusted: true);
|
||||
|
||||
// Act
|
||||
_cache.TryResolve("trusted-build", 0x1050, out var symbol);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(symbol);
|
||||
Assert.True(symbol.IsTrusted);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests for CanonicalSymbol.
|
||||
/// </summary>
|
||||
public sealed class CanonicalSymbolTests
|
||||
{
|
||||
[Fact]
|
||||
public void ToCanonicalString_ShouldFormatCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var symbol = new CanonicalSymbol
|
||||
{
|
||||
Address = 0x1234,
|
||||
BuildId = "abcdef1234567890",
|
||||
FunctionName = "process_request",
|
||||
Offset = 0x50,
|
||||
};
|
||||
|
||||
// Act
|
||||
var canonical = symbol.ToCanonicalString();
|
||||
|
||||
// Assert
|
||||
Assert.Equal("abcdef1234567890:process_request+0x50", canonical);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_ShouldParseCanonicalString()
|
||||
{
|
||||
// Arrange
|
||||
var canonical = "abcdef12:main+0x100";
|
||||
|
||||
// Act
|
||||
var symbol = CanonicalSymbol.Parse(canonical);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(symbol);
|
||||
Assert.Equal("abcdef12", symbol.BuildId);
|
||||
Assert.Equal("main", symbol.FunctionName);
|
||||
Assert.Equal(0x100UL, symbol.Offset);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_ShouldReturnNullForInvalidFormat()
|
||||
{
|
||||
Assert.Null(CanonicalSymbol.Parse(""));
|
||||
Assert.Null(CanonicalSymbol.Parse("no-colon"));
|
||||
Assert.Null(CanonicalSymbol.Parse("build:no-plus"));
|
||||
Assert.Null(CanonicalSymbol.Parse("build:func+invalid"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RoundTrip_ShouldPreserveData()
|
||||
{
|
||||
// Arrange
|
||||
var original = new CanonicalSymbol
|
||||
{
|
||||
Address = 0,
|
||||
BuildId = "deadbeef",
|
||||
FunctionName = "test_func",
|
||||
Offset = 0x42,
|
||||
};
|
||||
|
||||
// Act
|
||||
var canonical = original.ToCanonicalString();
|
||||
var parsed = CanonicalSymbol.Parse(canonical);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(parsed);
|
||||
Assert.Equal(original.BuildId, parsed.BuildId);
|
||||
Assert.Equal(original.FunctionName, parsed.FunctionName);
|
||||
Assert.Equal(original.Offset, parsed.Offset);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user