Fix build and code structure improvements. New but essential UI functionality. CI improvements. Documentation improvements. AI module improvements.
This commit is contained in:
@@ -0,0 +1,316 @@
|
||||
// <copyright file="EbpfSignalMergerTests.cs" company="StellaOps">
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// </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)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
// <copyright file="RuntimeSignalCollectorTests.cs" company="StellaOps">
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// </copyright>
|
||||
|
||||
namespace StellaOps.Signals.Ebpf.Tests;
|
||||
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using StellaOps.Signals.Ebpf.Probes;
|
||||
using StellaOps.Signals.Ebpf.Schema;
|
||||
using StellaOps.Signals.Ebpf.Services;
|
||||
using Xunit;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for <see cref="RuntimeSignalCollector"/>.
|
||||
/// </summary>
|
||||
public sealed class RuntimeSignalCollectorTests
|
||||
{
|
||||
private readonly RuntimeSignalCollector _collector;
|
||||
private readonly MockProbeLoader _probeLoader;
|
||||
|
||||
public RuntimeSignalCollectorTests()
|
||||
{
|
||||
_probeLoader = new MockProbeLoader();
|
||||
_collector = new RuntimeSignalCollector(
|
||||
NullLogger<RuntimeSignalCollector>.Instance,
|
||||
_probeLoader);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsSupported_ReturnsCorrectValue()
|
||||
{
|
||||
// On non-Linux systems, eBPF is not supported
|
||||
var isSupported = _collector.IsSupported();
|
||||
|
||||
// This will be false on Windows/macOS test runners
|
||||
Assert.True(isSupported == false || Environment.OSVersion.Platform == PlatformID.Unix);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetSupportedProbeTypes_ReturnsEmptyOnUnsupportedPlatform()
|
||||
{
|
||||
var probeTypes = _collector.GetSupportedProbeTypes();
|
||||
|
||||
// On non-Linux, should be empty
|
||||
if (!_collector.IsSupported())
|
||||
{
|
||||
Assert.Empty(probeTypes);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RuntimeCallEvent_HasCorrectProperties()
|
||||
{
|
||||
var evt = new RuntimeCallEvent
|
||||
{
|
||||
EventId = Guid.NewGuid(),
|
||||
ContainerId = "container-123",
|
||||
Pid = 1234,
|
||||
Tid = 5678,
|
||||
TimestampNs = 1000000000,
|
||||
Symbol = "malloc",
|
||||
FunctionAddress = 0x7fff12345678,
|
||||
StackTrace = [0x7fff00001000, 0x7fff00002000],
|
||||
RuntimeType = RuntimeType.Native,
|
||||
Library = "libc.so.6",
|
||||
Purl = "pkg:deb/ubuntu/libc6@2.31",
|
||||
};
|
||||
|
||||
Assert.Equal("container-123", evt.ContainerId);
|
||||
Assert.Equal(1234, evt.Pid);
|
||||
Assert.Equal("malloc", evt.Symbol);
|
||||
Assert.Equal(RuntimeType.Native, evt.RuntimeType);
|
||||
Assert.Equal(2, evt.StackTrace.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RuntimeSignalSummary_HasCorrectProperties()
|
||||
{
|
||||
var summary = new RuntimeSignalSummary
|
||||
{
|
||||
ContainerId = "container-456",
|
||||
StartedAt = DateTimeOffset.UtcNow.AddMinutes(-5),
|
||||
StoppedAt = DateTimeOffset.UtcNow,
|
||||
TotalEvents = 1000,
|
||||
CallPaths = new List<ObservedCallPath>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Symbols = ["main", "processRequest", "vulnerable_func"],
|
||||
ObservationCount = 50,
|
||||
Purl = "pkg:npm/lodash@4.17.20",
|
||||
},
|
||||
},
|
||||
ObservedSymbols = ["main", "processRequest", "vulnerable_func"],
|
||||
DroppedEvents = 10,
|
||||
DetectedRuntimes = [RuntimeType.Node],
|
||||
};
|
||||
|
||||
Assert.Equal(1000, summary.TotalEvents);
|
||||
Assert.Single(summary.CallPaths);
|
||||
Assert.Equal(3, summary.CallPaths[0].Symbols.Count);
|
||||
Assert.Equal(50, summary.CallPaths[0].ObservationCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RuntimeSignalOptions_HasDefaultValues()
|
||||
{
|
||||
var options = new RuntimeSignalOptions();
|
||||
|
||||
Assert.Empty(options.TargetSymbols);
|
||||
Assert.Equal(10000, options.MaxEventsPerSecond);
|
||||
Assert.Null(options.MaxDuration);
|
||||
Assert.True(options.ResolveSymbols);
|
||||
Assert.Equal(16, options.MaxStackDepth);
|
||||
Assert.Equal(256 * 1024, options.RingBufferSize);
|
||||
Assert.Equal(1, options.SampleRate);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SignalStatistics_CapturesMetrics()
|
||||
{
|
||||
var stats = new SignalStatistics
|
||||
{
|
||||
TotalEvents = 5000,
|
||||
EventsPerSecond = 250.5,
|
||||
UniqueCallPaths = 42,
|
||||
BufferUtilization = 0.35,
|
||||
DroppedEvents = 5,
|
||||
CpuOverheadPercent = 0.1,
|
||||
MemoryUsageBytes = 1024 * 1024,
|
||||
};
|
||||
|
||||
Assert.Equal(5000, stats.TotalEvents);
|
||||
Assert.Equal(250.5, stats.EventsPerSecond);
|
||||
Assert.Equal(42, stats.UniqueCallPaths);
|
||||
Assert.Equal(0.35, stats.BufferUtilization);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ObservedCallPath_TracksObservations()
|
||||
{
|
||||
var path = new ObservedCallPath
|
||||
{
|
||||
Symbols = ["entry", "middle", "vulnerable"],
|
||||
ObservationCount = 100,
|
||||
Purl = "pkg:golang/example.com/pkg@1.0.0",
|
||||
RuntimeType = RuntimeType.Go,
|
||||
FirstObservedAt = DateTimeOffset.UtcNow.AddHours(-1),
|
||||
LastObservedAt = DateTimeOffset.UtcNow,
|
||||
};
|
||||
|
||||
Assert.Equal(3, path.Symbols.Count);
|
||||
Assert.Equal(100, path.ObservationCount);
|
||||
Assert.Equal(RuntimeType.Go, path.RuntimeType);
|
||||
Assert.True(path.LastObservedAt > path.FirstObservedAt);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(RuntimeType.Native, "Native")]
|
||||
[InlineData(RuntimeType.Jvm, "Jvm")]
|
||||
[InlineData(RuntimeType.Node, "Node")]
|
||||
[InlineData(RuntimeType.Python, "Python")]
|
||||
[InlineData(RuntimeType.DotNet, "DotNet")]
|
||||
[InlineData(RuntimeType.Go, "Go")]
|
||||
[InlineData(RuntimeType.Ruby, "Ruby")]
|
||||
[InlineData(RuntimeType.Unknown, "Unknown")]
|
||||
public void RuntimeType_HasCorrectStringRepresentation(RuntimeType type, string expected)
|
||||
{
|
||||
Assert.Equal(expected, type.ToString());
|
||||
}
|
||||
|
||||
private sealed class MockProbeLoader : IEbpfProbeLoader
|
||||
{
|
||||
public Task<EbpfProbeHandle> LoadAndAttachAsync(
|
||||
string containerId,
|
||||
RuntimeSignalOptions options,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
return Task.FromResult(new EbpfProbeHandle
|
||||
{
|
||||
ProbeId = Guid.NewGuid(),
|
||||
ContainerId = containerId,
|
||||
TracedPids = [],
|
||||
});
|
||||
}
|
||||
|
||||
public Task DetachAsync(EbpfProbeHandle handle, CancellationToken ct = default)
|
||||
=> Task.CompletedTask;
|
||||
|
||||
public async IAsyncEnumerable<ReadOnlyMemory<byte>> ReadEventsAsync(
|
||||
EbpfProbeHandle handle,
|
||||
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken ct = default)
|
||||
{
|
||||
await Task.Yield();
|
||||
yield break;
|
||||
}
|
||||
|
||||
public (string? Symbol, string? Library, string? Purl) ResolveSymbol(int pid, ulong address)
|
||||
=> (null, null, null);
|
||||
|
||||
public double GetBufferUtilization(EbpfProbeHandle handle) => 0.0;
|
||||
|
||||
public double GetCpuOverhead(EbpfProbeHandle handle) => 0.0;
|
||||
|
||||
public long GetMemoryUsage(EbpfProbeHandle handle) => 0;
|
||||
|
||||
public IReadOnlyList<ProbeType> GetSupportedProbeTypes() => [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="xunit.runner.visualstudio" >
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="coverlet.collector" >
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="FluentAssertions" />
|
||||
<PackageReference Include="Moq" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\__Libraries\StellaOps.Signals.Ebpf\StellaOps.Signals.Ebpf.csproj" />
|
||||
<ProjectReference Include="..\..\..\Scanner\__Libraries\StellaOps.Scanner.Reachability\StellaOps.Scanner.Reachability.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,190 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using MicrosoftOptions = Microsoft.Extensions.Options;
|
||||
using StellaOps.Signals.Models;
|
||||
using StellaOps.Signals.Persistence.Postgres;
|
||||
using StellaOps.Signals.Persistence.Postgres.Repositories;
|
||||
using StellaOps.Signals.Services;
|
||||
using StellaOps.TestKit;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace StellaOps.Signals.Persistence.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Integration tests for callgraph projection to relational tables.
|
||||
/// </summary>
|
||||
[Collection(SignalsPostgresCollection.Name)]
|
||||
public sealed class CallGraphProjectionIntegrationTests : IAsyncLifetime
|
||||
{
|
||||
private readonly SignalsPostgresFixture _fixture;
|
||||
private readonly ITestOutputHelper _output;
|
||||
private readonly SignalsDataSource _dataSource;
|
||||
private readonly PostgresCallGraphQueryRepository _queryRepository;
|
||||
private readonly CallGraphSyncService _service;
|
||||
|
||||
public CallGraphProjectionIntegrationTests(SignalsPostgresFixture fixture, ITestOutputHelper output)
|
||||
{
|
||||
_fixture = fixture;
|
||||
_output = output;
|
||||
|
||||
var options = fixture.Fixture.CreateOptions();
|
||||
options.SchemaName = fixture.SchemaName;
|
||||
_dataSource = new SignalsDataSource(MicrosoftOptions.Options.Create(options), NullLogger<SignalsDataSource>.Instance);
|
||||
|
||||
var projectionRepository = new PostgresCallGraphProjectionRepository(
|
||||
_dataSource,
|
||||
NullLogger<PostgresCallGraphProjectionRepository>.Instance);
|
||||
|
||||
_queryRepository = new PostgresCallGraphQueryRepository(
|
||||
_dataSource,
|
||||
NullLogger<PostgresCallGraphQueryRepository>.Instance);
|
||||
|
||||
_service = new CallGraphSyncService(
|
||||
projectionRepository,
|
||||
TimeProvider.System,
|
||||
NullLogger<CallGraphSyncService>.Instance);
|
||||
}
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
await _fixture.ExecuteSqlAsync("TRUNCATE TABLE signals.scans CASCADE;");
|
||||
}
|
||||
|
||||
public async Task DisposeAsync()
|
||||
{
|
||||
await _dataSource.DisposeAsync();
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task SyncAsync_ProjectsNodesToRelationalTable()
|
||||
{
|
||||
var scanId = Guid.NewGuid();
|
||||
var document = CreateSampleDocument();
|
||||
|
||||
// Act
|
||||
var result = await _service.SyncAsync(scanId, "sha256:test-digest", document);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.WasUpdated);
|
||||
Assert.Equal(document.Nodes.Count, result.NodesProjected);
|
||||
Assert.Equal(document.Edges.Count, result.EdgesProjected);
|
||||
Assert.Equal(document.Entrypoints.Count, result.EntrypointsProjected);
|
||||
Assert.True(result.DurationMs >= 0);
|
||||
|
||||
_output.WriteLine($"Projected {result.NodesProjected} nodes, {result.EdgesProjected} edges, {result.EntrypointsProjected} entrypoints in {result.DurationMs}ms");
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task SyncAsync_IsIdempotent_DoesNotCreateDuplicates()
|
||||
{
|
||||
var scanId = Guid.NewGuid();
|
||||
var document = CreateSampleDocument();
|
||||
|
||||
// Act - project twice
|
||||
var result1 = await _service.SyncAsync(scanId, "sha256:test-digest", document);
|
||||
var result2 = await _service.SyncAsync(scanId, "sha256:test-digest", document);
|
||||
|
||||
// Assert - second run should update, not duplicate
|
||||
Assert.Equal(result1.NodesProjected, result2.NodesProjected);
|
||||
Assert.Equal(result1.EdgesProjected, result2.EdgesProjected);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task SyncAsync_WithEntrypoints_ProjectsEntrypointsCorrectly()
|
||||
{
|
||||
var scanId = Guid.NewGuid();
|
||||
var document = new CallgraphDocument
|
||||
{
|
||||
Id = Guid.NewGuid().ToString("N"),
|
||||
Language = "csharp",
|
||||
GraphHash = "test-hash",
|
||||
Nodes = new List<CallgraphNode>
|
||||
{
|
||||
new() { Id = "node-1", Name = "GetUsers", Namespace = "Api.Controllers" },
|
||||
new() { Id = "node-2", Name = "CreateUser", Namespace = "Api.Controllers" }
|
||||
},
|
||||
Edges = new List<CallgraphEdge>(),
|
||||
Entrypoints = new List<CallgraphEntrypoint>
|
||||
{
|
||||
new() { NodeId = "node-1", Kind = EntrypointKind.Http, Route = "/api/users", HttpMethod = "GET", Order = 0 },
|
||||
new() { NodeId = "node-2", Kind = EntrypointKind.Http, Route = "/api/users", HttpMethod = "POST", Order = 1 }
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await _service.SyncAsync(scanId, "sha256:test-digest", document);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2, result.EntrypointsProjected);
|
||||
_output.WriteLine($"Projected {result.EntrypointsProjected} HTTP entrypoints");
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task DeleteByScanAsync_RemovesAllProjectedData()
|
||||
{
|
||||
var scanId = Guid.NewGuid();
|
||||
var document = CreateSampleDocument();
|
||||
|
||||
// Project first
|
||||
await _service.SyncAsync(scanId, "sha256:test-digest", document);
|
||||
|
||||
// Act
|
||||
await _service.DeleteByScanAsync(scanId);
|
||||
|
||||
// Assert - query should return empty stats
|
||||
var stats = await _queryRepository.GetStatsAsync(scanId);
|
||||
Assert.Equal(0, stats.NodeCount);
|
||||
Assert.Equal(0, stats.EdgeCount);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task QueryRepository_CanQueryProjectedData()
|
||||
{
|
||||
var scanId = Guid.NewGuid();
|
||||
var document = CreateSampleDocument();
|
||||
|
||||
// Project
|
||||
await _service.SyncAsync(scanId, "sha256:test-digest", document);
|
||||
|
||||
// Act
|
||||
var stats = await _queryRepository.GetStatsAsync(scanId);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(document.Nodes.Count, stats.NodeCount);
|
||||
Assert.Equal(document.Edges.Count, stats.EdgeCount);
|
||||
_output.WriteLine($"Query returned: {stats.NodeCount} nodes, {stats.EdgeCount} edges");
|
||||
}
|
||||
|
||||
private static CallgraphDocument CreateSampleDocument()
|
||||
{
|
||||
return new CallgraphDocument
|
||||
{
|
||||
Id = Guid.NewGuid().ToString("N"),
|
||||
Language = "csharp",
|
||||
GraphHash = "sha256:sample-graph-hash",
|
||||
Nodes = new List<CallgraphNode>
|
||||
{
|
||||
new() { Id = "node-1", Name = "Main", Kind = "method", Namespace = "Program", Visibility = SymbolVisibility.Public, IsEntrypointCandidate = true },
|
||||
new() { Id = "node-2", Name = "DoWork", Kind = "method", Namespace = "Service", Visibility = SymbolVisibility.Internal },
|
||||
new() { Id = "node-3", Name = "ProcessData", Kind = "method", Namespace = "Core", Visibility = SymbolVisibility.Private }
|
||||
},
|
||||
Edges = new List<CallgraphEdge>
|
||||
{
|
||||
new() { SourceId = "node-1", TargetId = "node-2", Kind = EdgeKind.Static, Reason = EdgeReason.DirectCall, Weight = 1.0 },
|
||||
new() { SourceId = "node-2", TargetId = "node-3", Kind = EdgeKind.Static, Reason = EdgeReason.DirectCall, Weight = 1.0 }
|
||||
},
|
||||
Entrypoints = new List<CallgraphEntrypoint>
|
||||
{
|
||||
new() { NodeId = "node-1", Kind = EntrypointKind.Main, Phase = EntrypointPhase.AppStart, Order = 0 }
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using MicrosoftOptions = Microsoft.Extensions.Options;
|
||||
using StellaOps.Signals.Models;
|
||||
using StellaOps.Signals.Persistence.Postgres;
|
||||
using StellaOps.Signals.Persistence.Postgres.Repositories;
|
||||
using StellaOps.Signals.Services;
|
||||
using StellaOps.TestKit;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Signals.Persistence.Tests;
|
||||
|
||||
[Collection(SignalsPostgresCollection.Name)]
|
||||
public sealed class CallGraphSyncServiceTests : IAsyncLifetime
|
||||
{
|
||||
private readonly SignalsPostgresFixture _fixture;
|
||||
private readonly SignalsDataSource _dataSource;
|
||||
private readonly CallGraphSyncService _syncService;
|
||||
private readonly PostgresCallGraphQueryRepository _queryRepository;
|
||||
|
||||
public CallGraphSyncServiceTests(SignalsPostgresFixture fixture)
|
||||
{
|
||||
_fixture = fixture;
|
||||
|
||||
var options = fixture.Fixture.CreateOptions();
|
||||
options.SchemaName = fixture.SchemaName;
|
||||
_dataSource = new SignalsDataSource(MicrosoftOptions.Options.Create(options), NullLogger<SignalsDataSource>.Instance);
|
||||
|
||||
var projectionRepository = new PostgresCallGraphProjectionRepository(
|
||||
_dataSource,
|
||||
NullLogger<PostgresCallGraphProjectionRepository>.Instance);
|
||||
|
||||
_syncService = new CallGraphSyncService(
|
||||
projectionRepository,
|
||||
TimeProvider.System,
|
||||
NullLogger<CallGraphSyncService>.Instance);
|
||||
_queryRepository = new PostgresCallGraphQueryRepository(_dataSource, NullLogger<PostgresCallGraphQueryRepository>.Instance);
|
||||
}
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
await _fixture.ExecuteSqlAsync("TRUNCATE TABLE signals.scans CASCADE;");
|
||||
}
|
||||
|
||||
public async Task DisposeAsync()
|
||||
{
|
||||
await _dataSource.DisposeAsync();
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task SyncAsync_ProjectsCallgraph_AndQueryRepositoryReturnsStats()
|
||||
{
|
||||
var scanId = Guid.NewGuid();
|
||||
|
||||
var document = new CallgraphDocument
|
||||
{
|
||||
Id = "callgraph-1",
|
||||
Language = "nodejs",
|
||||
Component = "pkg:npm/demo-app@1.0.0",
|
||||
Version = "1.0.0",
|
||||
Artifact = new CallgraphArtifactMetadata
|
||||
{
|
||||
Hash = "deadbeef",
|
||||
Path = "cas/reachability/graphs/deadbeef/callgraph.json",
|
||||
Length = 128,
|
||||
ContentType = "application/json"
|
||||
},
|
||||
Nodes = new List<CallgraphNode>
|
||||
{
|
||||
new("n1", "Main", "function", "Demo", "index.js", 1)
|
||||
{
|
||||
SymbolKey = "Demo::Main()",
|
||||
Visibility = SymbolVisibility.Public,
|
||||
IsEntrypointCandidate = true,
|
||||
},
|
||||
new("n2", "Helper", "function", "Demo", "lib.js", 10)
|
||||
{
|
||||
SymbolKey = "Demo::Helper()",
|
||||
Visibility = SymbolVisibility.Internal,
|
||||
Purl = "pkg:npm/lodash@4.17.21",
|
||||
IsEntrypointCandidate = false,
|
||||
}
|
||||
},
|
||||
Edges = new List<CallgraphEdge>
|
||||
{
|
||||
new("n1", "n2", "call")
|
||||
{
|
||||
Kind = EdgeKind.Static,
|
||||
Reason = EdgeReason.DirectCall,
|
||||
Weight = 1.0,
|
||||
IsResolved = true
|
||||
}
|
||||
},
|
||||
Entrypoints = new List<CallgraphEntrypoint>
|
||||
{
|
||||
new()
|
||||
{
|
||||
NodeId = "n1",
|
||||
Kind = EntrypointKind.Http,
|
||||
Framework = EntrypointFramework.Express,
|
||||
Route = "/",
|
||||
HttpMethod = "GET",
|
||||
Phase = EntrypointPhase.Runtime,
|
||||
Order = 0
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var result1 = await _syncService.SyncAsync(scanId, document.Artifact.Hash, document, CancellationToken.None);
|
||||
result1.WasUpdated.Should().BeTrue();
|
||||
result1.ScanId.Should().Be(scanId);
|
||||
|
||||
var stats = await _queryRepository.GetStatsAsync(scanId, CancellationToken.None);
|
||||
stats.NodeCount.Should().Be(2);
|
||||
stats.EdgeCount.Should().Be(1);
|
||||
stats.EntrypointCount.Should().Be(1);
|
||||
stats.UniquePurls.Should().Be(1);
|
||||
stats.HeuristicEdgeCount.Should().Be(0);
|
||||
stats.UnresolvedEdgeCount.Should().Be(0);
|
||||
|
||||
var reachable = await _queryRepository.GetReachableSymbolsAsync(scanId, "n1", cancellationToken: CancellationToken.None);
|
||||
reachable.Should().Contain("n2");
|
||||
|
||||
var result2 = await _syncService.SyncAsync(scanId, document.Artifact.Hash, document, CancellationToken.None);
|
||||
result2.ScanId.Should().Be(result1.ScanId);
|
||||
|
||||
var stats2 = await _queryRepository.GetStatsAsync(scanId, CancellationToken.None);
|
||||
stats2.NodeCount.Should().Be(2);
|
||||
stats2.EdgeCount.Should().Be(1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using MicrosoftOptions = Microsoft.Extensions.Options;
|
||||
using StellaOps.Signals.Models;
|
||||
using StellaOps.Signals.Persistence.Postgres;
|
||||
using StellaOps.Signals.Persistence.Postgres.Repositories;
|
||||
using StellaOps.TestKit;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Signals.Persistence.Tests;
|
||||
|
||||
[Collection(SignalsPostgresCollection.Name)]
|
||||
public sealed class PostgresCallgraphRepositoryTests : IAsyncLifetime
|
||||
{
|
||||
private readonly SignalsPostgresFixture _fixture;
|
||||
private readonly PostgresCallgraphRepository _repository;
|
||||
|
||||
public PostgresCallgraphRepositoryTests(SignalsPostgresFixture fixture)
|
||||
{
|
||||
_fixture = fixture;
|
||||
|
||||
var options = fixture.Fixture.CreateOptions();
|
||||
options.SchemaName = fixture.SchemaName;
|
||||
var dataSource = new SignalsDataSource(MicrosoftOptions.Options.Create(options), NullLogger<SignalsDataSource>.Instance);
|
||||
_repository = new PostgresCallgraphRepository(dataSource, NullLogger<PostgresCallgraphRepository>.Instance);
|
||||
}
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
await _fixture.TruncateAllTablesAsync();
|
||||
}
|
||||
|
||||
public Task DisposeAsync() => Task.CompletedTask;
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task UpsertAndGetById_RoundTripsCallgraphDocument()
|
||||
{
|
||||
// Arrange
|
||||
var id = "callgraph-" + Guid.NewGuid().ToString("N");
|
||||
var document = new CallgraphDocument
|
||||
{
|
||||
Id = id,
|
||||
Language = "javascript",
|
||||
Component = "pkg:npm/lodash@4.17.21",
|
||||
Version = "4.17.21",
|
||||
IngestedAt = DateTimeOffset.UtcNow,
|
||||
GraphHash = "sha256:abc123",
|
||||
Nodes = new List<CallgraphNode>
|
||||
{
|
||||
new("fn1", "main", "function", "lodash", "index.js", 1),
|
||||
new("fn2", "helper", "function", "lodash", "utils.js", 10)
|
||||
},
|
||||
Edges = new List<CallgraphEdge>
|
||||
{
|
||||
new("fn1", "fn2", "call")
|
||||
},
|
||||
Metadata = new Dictionary<string, string?> { ["version"] = "1.0" }
|
||||
};
|
||||
|
||||
// Act
|
||||
await _repository.UpsertAsync(document, CancellationToken.None);
|
||||
var fetched = await _repository.GetByIdAsync(id, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
fetched.Should().NotBeNull();
|
||||
fetched!.Id.Should().Be(id);
|
||||
fetched.Language.Should().Be("javascript");
|
||||
fetched.Component.Should().Be("pkg:npm/lodash@4.17.21");
|
||||
fetched.Nodes.Should().HaveCount(2);
|
||||
fetched.Edges.Should().HaveCount(1);
|
||||
fetched.Metadata.Should().ContainKey("version");
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task UpsertAsync_UpdatesExistingDocument()
|
||||
{
|
||||
// Arrange
|
||||
var id = "callgraph-" + Guid.NewGuid().ToString("N");
|
||||
var document1 = new CallgraphDocument
|
||||
{
|
||||
Id = id,
|
||||
Language = "javascript",
|
||||
Component = "pkg:npm/express@4.18.0",
|
||||
Version = "4.18.0",
|
||||
IngestedAt = DateTimeOffset.UtcNow,
|
||||
GraphHash = "hash1",
|
||||
Nodes = new List<CallgraphNode> { new("fn1", "old", "function", null, "a.js", 1) },
|
||||
Edges = new List<CallgraphEdge>()
|
||||
};
|
||||
|
||||
var document2 = new CallgraphDocument
|
||||
{
|
||||
Id = id,
|
||||
Language = "typescript",
|
||||
Component = "pkg:npm/express@4.19.0",
|
||||
Version = "4.19.0",
|
||||
IngestedAt = DateTimeOffset.UtcNow,
|
||||
GraphHash = "hash2",
|
||||
Nodes = new List<CallgraphNode>
|
||||
{
|
||||
new("fn1", "new1", "function", null, "b.js", 1),
|
||||
new("fn2", "new2", "function", null, "b.js", 5)
|
||||
},
|
||||
Edges = new List<CallgraphEdge>()
|
||||
};
|
||||
|
||||
// Act
|
||||
await _repository.UpsertAsync(document1, CancellationToken.None);
|
||||
await _repository.UpsertAsync(document2, CancellationToken.None);
|
||||
var fetched = await _repository.GetByIdAsync(id, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
fetched.Should().NotBeNull();
|
||||
fetched!.Language.Should().Be("typescript");
|
||||
fetched.Version.Should().Be("4.19.0");
|
||||
fetched.Nodes.Should().HaveCount(2);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetByIdAsync_ReturnsNullForNonExistentId()
|
||||
{
|
||||
// Act
|
||||
var fetched = await _repository.GetByIdAsync("nonexistent-id", CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
fetched.Should().BeNull();
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task UpsertAsync_GeneratesIdIfMissing()
|
||||
{
|
||||
// Arrange
|
||||
var document = new CallgraphDocument
|
||||
{
|
||||
Id = string.Empty, // empty ID should be replaced
|
||||
Language = "python",
|
||||
Component = "pkg:pypi/requests@2.28.0",
|
||||
Version = "2.28.0",
|
||||
IngestedAt = DateTimeOffset.UtcNow,
|
||||
GraphHash = "hash123",
|
||||
Nodes = new List<CallgraphNode>(),
|
||||
Edges = new List<CallgraphEdge>()
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await _repository.UpsertAsync(document, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Id.Should().NotBeNullOrWhiteSpace();
|
||||
result.Id.Should().HaveLength(32); // GUID without hyphens
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using System.Reflection;
|
||||
using StellaOps.Infrastructure.Postgres.Testing;
|
||||
using StellaOps.Signals.Persistence.Postgres;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Signals.Persistence.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// PostgreSQL integration test fixture for the Signals module.
|
||||
/// </summary>
|
||||
public sealed class SignalsPostgresFixture : PostgresIntegrationFixture, ICollectionFixture<SignalsPostgresFixture>
|
||||
{
|
||||
protected override Assembly? GetMigrationAssembly()
|
||||
=> typeof(SignalsDataSource).Assembly;
|
||||
|
||||
protected override string GetModuleName() => "Signals";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Collection definition for Signals PostgreSQL integration tests.
|
||||
/// Tests in this collection share a single PostgreSQL container instance.
|
||||
/// </summary>
|
||||
[CollectionDefinition(Name)]
|
||||
public sealed class SignalsPostgresCollection : ICollectionFixture<SignalsPostgresFixture>
|
||||
{
|
||||
public const string Name = "SignalsPostgres";
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" ?>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\__Libraries\StellaOps.Signals.Persistence\StellaOps.Signals.Persistence.csproj" />
|
||||
<ProjectReference Include="..\..\..\__Tests\__Libraries\StellaOps.Infrastructure.Postgres.Testing\StellaOps.Infrastructure.Postgres.Testing.csproj" />
|
||||
<ProjectReference Include="..\..\..\__Libraries\StellaOps.TestKit\StellaOps.TestKit.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
@@ -136,7 +136,6 @@ public class CallgraphIngestionServiceTests
|
||||
if (request.ManifestContent is not null)
|
||||
{
|
||||
using var manifestMs = new MemoryStream();
|
||||
using StellaOps.TestKit;
|
||||
request.ManifestContent.CopyTo(manifestMs);
|
||||
manifests[request.Hash] = manifestMs.ToArray();
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
@@ -244,7 +244,6 @@ public class EdgeBundleIngestionServiceTests
|
||||
using var stream1 = new MemoryStream(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(bundle1)));
|
||||
using var stream2 = new MemoryStream(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(bundle2)));
|
||||
|
||||
using StellaOps.TestKit;
|
||||
// Act
|
||||
await _service.IngestAsync(TestTenantId, stream1, null);
|
||||
await _service.IngestAsync(TestTenantId, stream2, null);
|
||||
|
||||
@@ -321,7 +321,7 @@ public class InMemoryEvidenceWeightPolicyProviderTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Clear_RemovesAllPolicies()
|
||||
public async Task Clear_RemovesAllPolicies()
|
||||
{
|
||||
var provider = new InMemoryEvidenceWeightPolicyProvider();
|
||||
provider.SetPolicy(new EvidenceWeightPolicy
|
||||
@@ -339,7 +339,7 @@ public class InMemoryEvidenceWeightPolicyProviderTests
|
||||
|
||||
provider.Clear();
|
||||
|
||||
provider.PolicyExistsAsync(null, "production").Result.Should().BeFalse();
|
||||
provider.PolicyExistsAsync(null, "development").Result.Should().BeFalse();
|
||||
(await provider.PolicyExistsAsync(null, "production")).Should().BeFalse();
|
||||
(await provider.PolicyExistsAsync(null, "development")).Should().BeFalse();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ public class EvidenceWeightedScoreCalculatorTests
|
||||
var result = _calculator.Calculate(input, _defaultPolicy);
|
||||
|
||||
// Without MIT, sum of weights = 0.95 (default) → 95%
|
||||
result.Score.Should().BeGreaterOrEqualTo(90);
|
||||
result.Score.Should().BeGreaterThanOrEqualTo(90);
|
||||
result.Bucket.Should().Be(ScoreBucket.ActNow);
|
||||
}
|
||||
|
||||
@@ -217,7 +217,7 @@ public class EvidenceWeightedScoreCalculatorTests
|
||||
|
||||
var result = _calculator.Calculate(input, _defaultPolicy);
|
||||
|
||||
result.Score.Should().BeLessOrEqualTo(15);
|
||||
result.Score.Should().BeLessThanOrEqualTo(15);
|
||||
result.Caps.NotAffectedCap.Should().BeTrue();
|
||||
result.Flags.Should().Contain("vendor-na");
|
||||
}
|
||||
@@ -229,7 +229,7 @@ public class EvidenceWeightedScoreCalculatorTests
|
||||
|
||||
var result = _calculator.Calculate(input, _defaultPolicy);
|
||||
|
||||
result.Score.Should().BeGreaterOrEqualTo(60);
|
||||
result.Score.Should().BeGreaterThanOrEqualTo(60);
|
||||
result.Caps.RuntimeFloor.Should().BeTrue();
|
||||
}
|
||||
|
||||
@@ -242,7 +242,7 @@ public class EvidenceWeightedScoreCalculatorTests
|
||||
var result = _calculator.Calculate(input, _defaultPolicy);
|
||||
|
||||
// Since RTS >= 0.8, runtime floor should apply (floor at 60)
|
||||
result.Score.Should().BeGreaterOrEqualTo(60);
|
||||
result.Score.Should().BeGreaterThanOrEqualTo(60);
|
||||
result.Caps.RuntimeFloor.Should().BeTrue();
|
||||
// Speculative cap shouldn't apply because RTS > 0
|
||||
result.Caps.SpeculativeCap.Should().BeFalse();
|
||||
@@ -311,7 +311,7 @@ public class EvidenceWeightedScoreCalculatorTests
|
||||
var result = _calculator.Calculate(input, _defaultPolicy);
|
||||
|
||||
result.Should().NotBeNull();
|
||||
result.Score.Should().BeGreaterOrEqualTo(0);
|
||||
result.Score.Should().BeGreaterThanOrEqualTo(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -57,7 +57,7 @@ public class GroundTruthValidatorTests
|
||||
/// </summary>
|
||||
[Theory]
|
||||
[MemberData(nameof(GetGroundTruthSamples))]
|
||||
public void GroundTruth_HasValidUncertaintyTiers(string samplePath, GroundTruthDocument document)
|
||||
public void GroundTruth_HasValidUncertaintyTiers(string _samplePath, GroundTruthDocument document)
|
||||
{
|
||||
if (document.ExpectedUncertainty is null)
|
||||
{
|
||||
@@ -139,7 +139,7 @@ public class GroundTruthValidatorTests
|
||||
/// </summary>
|
||||
[Theory]
|
||||
[MemberData(nameof(GetGroundTruthSamples))]
|
||||
public void GroundTruth_EntryPointsHaveValidPhases(string samplePath, GroundTruthDocument document)
|
||||
public void GroundTruth_EntryPointsHaveValidPhases(string _samplePath, GroundTruthDocument document)
|
||||
{
|
||||
var validPhases = new[] { "load", "init", "runtime", "main", "fini" };
|
||||
|
||||
|
||||
@@ -140,7 +140,7 @@ public class ReachabilityLatticeStateExtensionsTests
|
||||
[InlineData(null, ReachabilityLatticeState.Unknown)]
|
||||
public void FromCode_ReturnsExpectedState(string? code, ReachabilityLatticeState expected)
|
||||
{
|
||||
Assert.Equal(expected, ReachabilityLatticeStateExtensions.FromCode(code));
|
||||
Assert.Equal(expected, ReachabilityLatticeStateExtensions.FromCode(code!));
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Text.Json;
|
||||
@@ -88,7 +88,6 @@ public class ReachabilityUnionIngestionServiceTests
|
||||
private static string ComputeSha(string content)
|
||||
{
|
||||
using var sha = System.Security.Cryptography.SHA256.Create();
|
||||
using StellaOps.TestKit;
|
||||
var bytes = System.Text.Encoding.UTF8.GetBytes(content);
|
||||
return Convert.ToHexString(sha.ComputeHash(bytes)).ToLowerInvariant();
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Linq;
|
||||
@@ -48,7 +48,6 @@ public class RouterEventsPublisherTests
|
||||
var options = CreateOptions();
|
||||
var handler = new StubHandler(HttpStatusCode.InternalServerError, "boom");
|
||||
using var httpClient = new HttpClient(handler) { BaseAddress = new Uri(options.Events.Router.BaseUrl) };
|
||||
using StellaOps.TestKit;
|
||||
var logger = new ListLogger<RouterEventsPublisher>();
|
||||
var builder = new ReachabilityFactEventBuilder(options, TimeProvider.System);
|
||||
var publisher = new RouterEventsPublisher(builder, options, httpClient, logger);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.IO.Compression;
|
||||
using System.IO.Compression;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
@@ -252,7 +252,6 @@ public class RuntimeFactsBatchIngestionTests
|
||||
public async Task<StoredRuntimeFactsArtifact> SaveAsync(RuntimeFactsArtifactSaveRequest request, Stream content, CancellationToken cancellationToken)
|
||||
{
|
||||
using var ms = new MemoryStream();
|
||||
using StellaOps.TestKit;
|
||||
await content.CopyToAsync(ms, cancellationToken);
|
||||
|
||||
var artifact = new StoredRuntimeFactsArtifact(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.IO;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@@ -48,7 +48,6 @@ public sealed class SimpleJsonCallgraphParserGateTests
|
||||
|
||||
var parser = new SimpleJsonCallgraphParser("csharp");
|
||||
await using var stream = new MemoryStream(Encoding.UTF8.GetBytes(json), writable: false);
|
||||
using StellaOps.TestKit;
|
||||
var parsed = await parser.ParseAsync(stream, CancellationToken.None);
|
||||
|
||||
parsed.Edges.Should().ContainSingle();
|
||||
|
||||
@@ -6,24 +6,27 @@
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
<!-- Disable Concelier shared test infra to avoid pulling unrelated projects into the Signals test graph -->
|
||||
<UseConcelierTestInfra>false</UseConcelierTestInfra>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.0" />
|
||||
<PackageReference Include="FluentAssertions" Version="6.12.0" />
|
||||
<PackageReference Include="xunit" Version="2.9.2" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.4" />
|
||||
<PackageReference Include="FluentAssertions" />
|
||||
<!-- FsCheck for property-based testing (EvidenceWeightedScore) -->
|
||||
<PackageReference Include="FsCheck" Version="3.0.0-rc3" />
|
||||
<PackageReference Include="FsCheck.Xunit" Version="3.0.0-rc3" />
|
||||
<PackageReference Include="FsCheck" />
|
||||
<PackageReference Include="FsCheck.Xunit" />
|
||||
<!-- Verify for snapshot testing (EvidenceWeightedScore) -->
|
||||
<PackageReference Include="Verify.Xunit" Version="28.7.2" />
|
||||
<PackageReference Include="Verify.Xunit" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" >
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="coverlet.collector" >
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../../StellaOps.Signals/StellaOps.Signals.csproj" />
|
||||
<ProjectReference Include="../../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
@@ -310,7 +310,6 @@ public class UnknownsDecayServiceTests
|
||||
}
|
||||
|
||||
using var cts = new CancellationTokenSource();
|
||||
using StellaOps.TestKit;
|
||||
cts.Cancel();
|
||||
|
||||
await Assert.ThrowsAsync<OperationCanceledException>(() =>
|
||||
|
||||
Reference in New Issue
Block a user