using FluentAssertions; using NSubstitute; using StellaOps.BinaryIndex.Validation; using StellaOps.BinaryIndex.Validation.Abstractions; using Xunit; namespace StellaOps.BinaryIndex.Validation.Tests; /// /// Tests for MismatchAnalyzer. /// public class MismatchAnalyzerTests { private readonly IMismatchCauseInferrer _causeInferrer; private readonly MismatchAnalyzer _sut; public MismatchAnalyzerTests() { _causeInferrer = Substitute.For(); _sut = new MismatchAnalyzer(_causeInferrer); } [Fact] public async Task AnalyzeAsync_WithEmptyList_ReturnsEmptyBuckets() { // Arrange var mismatches = new List(); // Act var analysis = await _sut.AnalyzeAsync(mismatches, maxExamplesPerBucket: 5); // Assert analysis.Buckets.Should().BeEmpty(); analysis.TotalMismatches.Should().Be(0); analysis.DominantCause.Should().BeNull(); } [Fact] public async Task AnalyzeAsync_GroupsMismatchesByCause() { // Arrange var mismatches = new List { CreateMismatch("func1@@GLIBC_2.17"), CreateMismatch("func2@@GLIBC_2.34"), CreateMismatch("small_func", size: 20) }; _causeInferrer.InferCauseAsync(Arg.Any(), Arg.Any()) .Returns(callInfo => { var mismatch = callInfo.Arg(); if (mismatch.SourceFunction.Name.Contains("@@")) return (MismatchCause.SymbolVersioning, 0.9); return (MismatchCause.Inlining, 0.6); }); // Act var analysis = await _sut.AnalyzeAsync(mismatches, maxExamplesPerBucket: 5); // Assert analysis.Buckets.Should().HaveCount(2); analysis.TotalMismatches.Should().Be(3); analysis.Buckets[MismatchCause.SymbolVersioning].Count.Should().Be(2); analysis.Buckets[MismatchCause.Inlining].Count.Should().Be(1); } [Fact] public async Task AnalyzeAsync_DominantCause_IsHighestCount() { // Arrange var mismatches = Enumerable.Range(0, 10) .Select(i => CreateMismatch($"func_{i}")) .ToList(); _causeInferrer.InferCauseAsync(Arg.Any(), Arg.Any()) .Returns(callInfo => { var index = int.Parse(callInfo.Arg().SourceFunction.Name.Split('_')[1]); return index < 7 ? (MismatchCause.OptimizationLevel, 0.8) : (MismatchCause.CompilerVersion, 0.7); }); // Act var analysis = await _sut.AnalyzeAsync(mismatches, maxExamplesPerBucket: 3); // Assert analysis.DominantCause.Should().Be(MismatchCause.OptimizationLevel); analysis.Buckets[MismatchCause.OptimizationLevel].Count.Should().Be(7); analysis.Buckets[MismatchCause.CompilerVersion].Count.Should().Be(3); } [Fact] public async Task AnalyzeAsync_LimitsExamplesPerBucket() { // Arrange var mismatches = Enumerable.Range(0, 20) .Select(i => CreateMismatch($"func_{i}")) .ToList(); _causeInferrer.InferCauseAsync(Arg.Any(), Arg.Any()) .Returns((MismatchCause.Unknown, 0.5)); // Act var analysis = await _sut.AnalyzeAsync(mismatches, maxExamplesPerBucket: 5); // Assert analysis.Buckets[MismatchCause.Unknown].Examples.Should().HaveCount(5); analysis.Buckets[MismatchCause.Unknown].Count.Should().Be(20); } [Fact] public async Task AnalyzeAsync_CalculatesPercentages() { // Arrange var mismatches = Enumerable.Range(0, 100) .Select(i => CreateMismatch($"func_{i}")) .ToList(); _causeInferrer.InferCauseAsync(Arg.Any(), Arg.Any()) .Returns(callInfo => { var index = int.Parse(callInfo.Arg().SourceFunction.Name.Split('_')[1]); return index < 60 ? (MismatchCause.Inlining, 0.8) : (MismatchCause.LinkTimeOptimization, 0.7); }); // Act var analysis = await _sut.AnalyzeAsync(mismatches, maxExamplesPerBucket: 5); // Assert analysis.Buckets[MismatchCause.Inlining].Percentage.Should().BeApproximately(60, 0.1); analysis.Buckets[MismatchCause.LinkTimeOptimization].Percentage.Should().BeApproximately(40, 0.1); } private static MatchResult CreateMismatch(string functionName, ulong? size = null) { return new MatchResult { Id = Guid.NewGuid(), RunId = Guid.NewGuid(), SecurityPairId = Guid.NewGuid(), SourceFunction = new FunctionIdentifier { Name = functionName, Address = 0x1000, Size = size, BuildId = "abc123", BinaryName = "test.so" }, ExpectedTarget = new FunctionIdentifier { Name = functionName.Replace("@@GLIBC_2.17", "").Replace("@@GLIBC_2.34", ""), Address = 0x2000, BuildId = "def456", BinaryName = "test.so" }, Outcome = MatchOutcome.FalseNegative }; } } /// /// Tests for HeuristicMismatchCauseInferrer. /// public class HeuristicMismatchCauseInferrerTests { private readonly HeuristicMismatchCauseInferrer _sut = new(); [Theory] [InlineData("printf@@GLIBC_2.17", MismatchCause.SymbolVersioning)] [InlineData("malloc@@GLIBC_2.34", MismatchCause.SymbolVersioning)] public async Task InferCauseAsync_SymbolVersioning_DetectedCorrectly(string name, MismatchCause expected) { // Arrange var mismatch = CreateMismatch(name); // Act var (cause, confidence) = await _sut.InferCauseAsync(mismatch); // Assert cause.Should().Be(expected); confidence.Should().BeGreaterThan(0.8); } [Theory] [InlineData("small_func", 20, MismatchCause.Inlining)] [InlineData("tiny_func", 10, MismatchCause.Inlining)] public async Task InferCauseAsync_SmallFunction_InfersInlining(string name, ulong size, MismatchCause expected) { // Arrange var mismatch = CreateMismatch(name, size); // Act var (cause, _) = await _sut.InferCauseAsync(mismatch); // Assert cause.Should().Be(expected); } [Theory] [InlineData("func.cold", MismatchCause.FunctionSplit)] [InlineData("func.isra.0", MismatchCause.FunctionSplit)] [InlineData("func.part.1", MismatchCause.FunctionSplit)] public async Task InferCauseAsync_SplitFunction_DetectedCorrectly(string name, MismatchCause expected) { // Arrange var mismatch = CreateMismatch(name, 500); // Act var (cause, confidence) = await _sut.InferCauseAsync(mismatch); // Assert cause.Should().Be(expected); confidence.Should().BeGreaterThan(0.7); } [Theory] [InlineData("__asan_load8", MismatchCause.SanitizerInstrumentation)] [InlineData("__tsan_write4", MismatchCause.SanitizerInstrumentation)] [InlineData("__ubsan_handle_divrem_overflow", MismatchCause.SanitizerInstrumentation)] public async Task InferCauseAsync_Sanitizer_DetectedCorrectly(string name, MismatchCause expected) { // Arrange var mismatch = CreateMismatch(name, 100); // Act var (cause, confidence) = await _sut.InferCauseAsync(mismatch); // Assert cause.Should().Be(expected); confidence.Should().BeGreaterThan(0.9); } [Fact] public async Task InferCauseAsync_UnknownPattern_ReturnsUnknown() { // Arrange var mismatch = CreateMismatch("normal_large_function", 1000); // Act var (cause, confidence) = await _sut.InferCauseAsync(mismatch); // Assert cause.Should().Be(MismatchCause.Unknown); confidence.Should().BeLessThan(0.5); } private static MatchResult CreateMismatch(string name, ulong? size = null) { return new MatchResult { Id = Guid.NewGuid(), RunId = Guid.NewGuid(), SecurityPairId = Guid.NewGuid(), SourceFunction = new FunctionIdentifier { Name = name, Address = 0x1000, Size = size, BuildId = "abc123", BinaryName = "test.so" }, ExpectedTarget = new FunctionIdentifier { Name = name, Address = 0x2000, BuildId = "def456", BinaryName = "test.so" }, Outcome = MatchOutcome.FalseNegative }; } }