Files
git.stella-ops.org/src/Signals/__Tests/StellaOps.Signals.Ebpf.Tests/EbpfSignalMergerTests.cs

317 lines
10 KiB
C#

// <copyright file="EbpfSignalMergerTests.cs" company="StellaOps">
// SPDX-License-Identifier: BUSL-1.1
// </copyright>
namespace StellaOps.Signals.Ebpf.Tests;
using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.Scanner.Reachability;
using StellaOps.Scanner.Reachability.Runtime;
using StellaOps.Scanner.Reachability.Slices;
using StellaOps.Signals.Ebpf.Schema;
using Xunit;
/// <summary>
/// Tests for <see cref="EbpfSignalMerger"/>.
/// </summary>
public sealed class EbpfSignalMergerTests
{
private readonly EbpfSignalMerger _merger;
private readonly RuntimeStaticMerger _baseMerger;
public EbpfSignalMergerTests()
{
_baseMerger = new RuntimeStaticMerger();
_merger = new EbpfSignalMerger(
_baseMerger,
NullLogger<EbpfSignalMerger>.Instance);
}
[Fact]
public void Merge_WithNoSignals_ReturnsSameGraph()
{
var graph = CreateTestGraph();
var result = _merger.Merge(graph, null);
Assert.Same(graph, result.MergedGraph);
Assert.Empty(result.Evidence);
Assert.Equal(2, result.Statistics.StaticEdgeCount);
Assert.Equal(0, result.Statistics.RuntimeEventCount);
}
[Fact]
public void Merge_WithEmptySignals_ReturnsSameGraph()
{
var graph = CreateTestGraph();
var signals = new RuntimeSignalSummary
{
ContainerId = "container-123",
StartedAt = DateTimeOffset.UtcNow.AddMinutes(-5),
StoppedAt = DateTimeOffset.UtcNow,
TotalEvents = 0,
CallPaths = [],
ObservedSymbols = [],
};
var result = _merger.Merge(graph, signals);
Assert.Same(graph, result.MergedGraph);
Assert.Empty(result.Evidence);
}
[Fact]
public void Merge_WithMatchingSignals_CreatesConfirmedEvidence()
{
var graph = CreateTestGraph();
var signals = new RuntimeSignalSummary
{
ContainerId = "container-123",
StartedAt = DateTimeOffset.UtcNow.AddMinutes(-5),
StoppedAt = DateTimeOffset.UtcNow,
TotalEvents = 100,
CallPaths = new List<ObservedCallPath>
{
new()
{
Symbols = ["main", "processRequest"],
ObservationCount = 50,
FirstObservedAt = DateTimeOffset.UtcNow.AddMinutes(-5),
LastObservedAt = DateTimeOffset.UtcNow,
},
},
ObservedSymbols = ["main", "processRequest"],
};
var result = _merger.Merge(graph, signals);
Assert.NotEmpty(result.Evidence);
Assert.Contains(result.Evidence, e =>
e.Type == RuntimeEvidenceType.RuntimeConfirmed);
}
[Fact]
public void Merge_WithRuntimeOnlyPath_CreatesRuntimeOnlyEvidence()
{
var graph = CreateTestGraph();
var signals = new RuntimeSignalSummary
{
ContainerId = "container-123",
StartedAt = DateTimeOffset.UtcNow.AddMinutes(-5),
StoppedAt = DateTimeOffset.UtcNow,
TotalEvents = 100,
CallPaths = new List<ObservedCallPath>
{
new()
{
// Path not in static graph
Symbols = ["dynamic_dispatch", "hidden_method"],
ObservationCount = 20,
FirstObservedAt = DateTimeOffset.UtcNow.AddMinutes(-5),
LastObservedAt = DateTimeOffset.UtcNow,
},
},
ObservedSymbols = ["dynamic_dispatch", "hidden_method"],
};
var result = _merger.Merge(graph, signals);
Assert.Contains(result.Evidence, e =>
e.Type == RuntimeEvidenceType.RuntimeOnly);
Assert.True(result.Statistics.RuntimeOnlyPathCount > 0);
}
[Fact]
public void Merge_WithDetectedRuntimes_CreatesRuntimeDetectedEvidence()
{
var graph = CreateTestGraph();
var signals = new RuntimeSignalSummary
{
ContainerId = "container-123",
StartedAt = DateTimeOffset.UtcNow.AddMinutes(-5),
StoppedAt = DateTimeOffset.UtcNow,
TotalEvents = 100,
CallPaths = [],
ObservedSymbols = [],
DetectedRuntimes = [RuntimeType.Node, RuntimeType.Python],
};
var result = _merger.Merge(graph, signals);
Assert.Contains(result.Evidence, e =>
e.Type == RuntimeEvidenceType.RuntimeDetected &&
e.RuntimeType == "Node");
Assert.Contains(result.Evidence, e =>
e.Type == RuntimeEvidenceType.RuntimeDetected &&
e.RuntimeType == "Python");
}
[Fact]
public void ValidatePath_WithValidPath_ReturnsConfirmed()
{
var graph = CreateTestGraph();
var path = new ObservedCallPath
{
Symbols = ["main", "processRequest"],
ObservationCount = 10,
FirstObservedAt = DateTimeOffset.UtcNow,
LastObservedAt = DateTimeOffset.UtcNow,
};
var result = _merger.ValidatePath(graph, path);
Assert.True(result.IsValid);
Assert.Equal(PathType.Confirmed, result.PathType);
Assert.Equal(1.0, result.MatchRatio);
}
[Fact]
public void ValidatePath_WithUnknownPath_ReturnsRuntimeOnly()
{
var graph = CreateTestGraph();
var path = new ObservedCallPath
{
Symbols = ["unknown", "method"],
ObservationCount = 10,
FirstObservedAt = DateTimeOffset.UtcNow,
LastObservedAt = DateTimeOffset.UtcNow,
};
var result = _merger.ValidatePath(graph, path);
Assert.True(result.IsValid);
Assert.Equal(PathType.RuntimeOnly, result.PathType);
Assert.Equal(0.0, result.MatchRatio);
}
[Fact]
public void ValidatePath_WithShortPath_ReturnsInvalid()
{
var graph = CreateTestGraph();
var path = new ObservedCallPath
{
Symbols = ["single"],
ObservationCount = 10,
FirstObservedAt = DateTimeOffset.UtcNow,
LastObservedAt = DateTimeOffset.UtcNow,
};
var result = _merger.ValidatePath(graph, path);
Assert.False(result.IsValid);
Assert.Equal(PathType.Invalid, result.PathType);
}
[Fact]
public void Merge_StatisticsAreAccurate()
{
var graph = CreateTestGraph();
var signals = new RuntimeSignalSummary
{
ContainerId = "container-123",
StartedAt = DateTimeOffset.UtcNow.AddMinutes(-5),
StoppedAt = DateTimeOffset.UtcNow,
TotalEvents = 500,
CallPaths = new List<ObservedCallPath>
{
new()
{
Symbols = ["main", "processRequest"],
ObservationCount = 100,
FirstObservedAt = DateTimeOffset.UtcNow.AddMinutes(-5),
LastObservedAt = DateTimeOffset.UtcNow,
},
},
ObservedSymbols = ["main", "processRequest"],
DroppedEvents = 10,
};
var result = _merger.Merge(graph, signals);
Assert.Equal(2, result.Statistics.StaticEdgeCount);
Assert.Equal(500, result.Statistics.RuntimeEventCount);
Assert.Equal(1, result.Statistics.CallPathCount);
Assert.Equal(10, result.Statistics.DroppedEventCount);
}
[Fact]
public void RuntimeEvidence_ContainsContainerId()
{
var graph = CreateTestGraph();
var signals = new RuntimeSignalSummary
{
ContainerId = "my-container-id",
StartedAt = DateTimeOffset.UtcNow.AddMinutes(-5),
StoppedAt = DateTimeOffset.UtcNow,
TotalEvents = 100,
CallPaths = new List<ObservedCallPath>
{
new()
{
Symbols = ["main", "processRequest"],
ObservationCount = 10,
FirstObservedAt = DateTimeOffset.UtcNow.AddMinutes(-5),
LastObservedAt = DateTimeOffset.UtcNow,
},
},
ObservedSymbols = [],
};
var result = _merger.Merge(graph, signals);
Assert.All(result.Evidence, e =>
Assert.Equal("my-container-id", e.ContainerId));
}
[Fact]
public void EvidenceSource_IsEbpf()
{
var graph = CreateTestGraph();
var signals = new RuntimeSignalSummary
{
ContainerId = "container-123",
StartedAt = DateTimeOffset.UtcNow.AddMinutes(-5),
StoppedAt = DateTimeOffset.UtcNow,
TotalEvents = 100,
CallPaths = new List<ObservedCallPath>
{
new()
{
Symbols = ["main", "processRequest"],
ObservationCount = 10,
FirstObservedAt = DateTimeOffset.UtcNow.AddMinutes(-5),
LastObservedAt = DateTimeOffset.UtcNow,
},
},
ObservedSymbols = [],
};
var result = _merger.Merge(graph, signals);
Assert.All(result.Evidence, e =>
Assert.Equal(EvidenceSource.Ebpf, e.Source));
}
private static RichGraph CreateTestGraph()
{
return new RichGraph(
Nodes: new List<RichGraphNode>
{
new("main", "main", null, null, "native", "entrypoint", null, null, null, null, null),
new("processRequest", "processRequest", null, null, "native", "function", null, null, null, null, null),
new("handleError", "handleError", null, null, "native", "function", null, null, null, null, null),
},
Edges: new List<RichGraphEdge>
{
new("main", "processRequest", "call", null, null, null, 1.0, null),
new("processRequest", "handleError", "call", null, null, null, 0.8, null),
},
Roots: new List<RichGraphRoot>
{
new("main", "main", "entrypoint")
},
Analyzer: new RichGraphAnalyzer("test-analyzer", "1.0.0", null)
);
}
}