Files
git.stella-ops.org/src/BinaryIndex/__Tests/StellaOps.BinaryIndex.Validation.Tests/MismatchAnalyzerTests.cs
2026-01-20 00:45:38 +02:00

277 lines
8.9 KiB
C#

using FluentAssertions;
using NSubstitute;
using StellaOps.BinaryIndex.Validation;
using StellaOps.BinaryIndex.Validation.Abstractions;
using Xunit;
namespace StellaOps.BinaryIndex.Validation.Tests;
/// <summary>
/// Tests for MismatchAnalyzer.
/// </summary>
public class MismatchAnalyzerTests
{
private readonly IMismatchCauseInferrer _causeInferrer;
private readonly MismatchAnalyzer _sut;
public MismatchAnalyzerTests()
{
_causeInferrer = Substitute.For<IMismatchCauseInferrer>();
_sut = new MismatchAnalyzer(_causeInferrer);
}
[Fact]
public async Task AnalyzeAsync_WithEmptyList_ReturnsEmptyBuckets()
{
// Arrange
var mismatches = new List<MatchResult>();
// 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<MatchResult>
{
CreateMismatch("func1@@GLIBC_2.17"),
CreateMismatch("func2@@GLIBC_2.34"),
CreateMismatch("small_func", size: 20)
};
_causeInferrer.InferCauseAsync(Arg.Any<MatchResult>(), Arg.Any<CancellationToken>())
.Returns(callInfo =>
{
var mismatch = callInfo.Arg<MatchResult>();
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<MatchResult>(), Arg.Any<CancellationToken>())
.Returns(callInfo =>
{
var index = int.Parse(callInfo.Arg<MatchResult>().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<MatchResult>(), Arg.Any<CancellationToken>())
.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<MatchResult>(), Arg.Any<CancellationToken>())
.Returns(callInfo =>
{
var index = int.Parse(callInfo.Arg<MatchResult>().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
};
}
}
/// <summary>
/// Tests for HeuristicMismatchCauseInferrer.
/// </summary>
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
};
}
}