work work hard work
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
using StellaOps.Scanner.CallGraph;
|
||||
using StellaOps.Scanner.CallGraph.Node;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scanner.CallGraph.Tests;
|
||||
|
||||
public class BenchmarkIntegrationTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("unsafe-eval", true)]
|
||||
[InlineData("guarded-eval", false)]
|
||||
public async Task NodeTraceExtractor_AlignsWithBenchmarkReachability(string caseName, bool expectSinkReachable)
|
||||
{
|
||||
var repoRoot = FindRepoRoot();
|
||||
var caseDir = Path.Combine(repoRoot, "bench", "reachability-benchmark", "cases", "js", caseName);
|
||||
|
||||
var extractor = new NodeCallGraphExtractor();
|
||||
var snapshot = await extractor.ExtractAsync(new CallGraphExtractionRequest(
|
||||
ScanId: $"bench-{caseName}",
|
||||
Language: "node",
|
||||
TargetPath: caseDir));
|
||||
|
||||
var analyzer = new ReachabilityAnalyzer();
|
||||
var result = analyzer.Analyze(snapshot);
|
||||
|
||||
Assert.Equal(expectSinkReachable, result.ReachableSinkIds.Length > 0);
|
||||
}
|
||||
|
||||
private static string FindRepoRoot()
|
||||
{
|
||||
var directory = new DirectoryInfo(AppContext.BaseDirectory);
|
||||
while (directory is not null)
|
||||
{
|
||||
if (Directory.Exists(Path.Combine(directory.FullName, "bench", "reachability-benchmark")))
|
||||
{
|
||||
return directory.FullName;
|
||||
}
|
||||
|
||||
directory = directory.Parent;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException("Unable to locate repository root for benchmark integration tests.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
using StellaOps.Scanner.CallGraph.Caching;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scanner.CallGraph.Tests;
|
||||
|
||||
public class CircuitBreakerStateTests
|
||||
{
|
||||
[Fact]
|
||||
public void RecordFailure_TripsOpen_AfterThreshold()
|
||||
{
|
||||
var config = new CircuitBreakerConfig
|
||||
{
|
||||
FailureThreshold = 2,
|
||||
TimeoutSeconds = 60,
|
||||
HalfOpenTimeout = 10
|
||||
};
|
||||
|
||||
var cb = new CircuitBreakerState(config);
|
||||
Assert.Equal(CircuitState.Closed, cb.State);
|
||||
|
||||
cb.RecordFailure();
|
||||
Assert.Equal(CircuitState.Closed, cb.State);
|
||||
|
||||
cb.RecordFailure();
|
||||
Assert.Equal(CircuitState.Open, cb.State);
|
||||
Assert.True(cb.IsOpen);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RecordSuccess_ResetsToClosed()
|
||||
{
|
||||
var config = new CircuitBreakerConfig { FailureThreshold = 1, TimeoutSeconds = 60, HalfOpenTimeout = 10 };
|
||||
var cb = new CircuitBreakerState(config);
|
||||
cb.RecordFailure();
|
||||
Assert.True(cb.IsOpen);
|
||||
|
||||
cb.RecordSuccess();
|
||||
Assert.Equal(CircuitState.Closed, cb.State);
|
||||
Assert.False(cb.IsOpen);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
using StellaOps.Scanner.CallGraph;
|
||||
using StellaOps.Scanner.CallGraph.DotNet;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scanner.CallGraph.Tests;
|
||||
|
||||
public class DotNetCallGraphExtractorTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task ExtractAsync_SimpleProject_ProducesEntrypointAndSink()
|
||||
{
|
||||
await using var temp = await TempDirectory.CreateAsync();
|
||||
|
||||
var csprojPath = Path.Combine(temp.Path, "App.csproj");
|
||||
await File.WriteAllTextAsync(csprojPath, """
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
""");
|
||||
|
||||
await File.WriteAllTextAsync(Path.Combine(temp.Path, "Program.cs"), """
|
||||
using System;
|
||||
|
||||
public sealed class HttpGetAttribute : Attribute { }
|
||||
|
||||
namespace System.Diagnostics
|
||||
{
|
||||
public static class Process
|
||||
{
|
||||
public static void Start(string cmd) { }
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class FooController
|
||||
{
|
||||
[HttpGet]
|
||||
public void Get()
|
||||
{
|
||||
Helper();
|
||||
}
|
||||
|
||||
private void Helper()
|
||||
{
|
||||
System.Diagnostics.Process.Start("cmd.exe");
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
var fixedTime = DateTimeOffset.Parse("2025-12-17T00:00:00Z");
|
||||
var extractor = new DotNetCallGraphExtractor(new FixedTimeProvider(fixedTime));
|
||||
|
||||
var snapshot = await extractor.ExtractAsync(new CallGraphExtractionRequest(
|
||||
ScanId: "scan-001",
|
||||
Language: "dotnet",
|
||||
TargetPath: csprojPath));
|
||||
|
||||
Assert.Equal("scan-001", snapshot.ScanId);
|
||||
Assert.Equal("dotnet", snapshot.Language);
|
||||
Assert.False(string.IsNullOrWhiteSpace(snapshot.GraphDigest));
|
||||
Assert.NotEmpty(snapshot.Nodes);
|
||||
Assert.NotEmpty(snapshot.Edges);
|
||||
|
||||
Assert.Contains(snapshot.Nodes, n => n.IsEntrypoint && n.EntrypointType == EntrypointType.HttpHandler);
|
||||
Assert.Contains(snapshot.Nodes, n => n.IsSink);
|
||||
Assert.NotEmpty(snapshot.SinkIds);
|
||||
Assert.NotEmpty(snapshot.EntrypointIds);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExtractAsync_IsDeterministic_ForSameInputs()
|
||||
{
|
||||
await using var temp = await TempDirectory.CreateAsync();
|
||||
|
||||
var csprojPath = Path.Combine(temp.Path, "App.csproj");
|
||||
await File.WriteAllTextAsync(csprojPath, """
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
""");
|
||||
|
||||
await File.WriteAllTextAsync(Path.Combine(temp.Path, "Program.cs"), """
|
||||
public static class Program
|
||||
{
|
||||
public static void Main()
|
||||
{
|
||||
A();
|
||||
}
|
||||
|
||||
private static void A()
|
||||
{
|
||||
B();
|
||||
}
|
||||
|
||||
private static void B()
|
||||
{
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
var extractor = new DotNetCallGraphExtractor();
|
||||
var request = new CallGraphExtractionRequest("scan-001", "dotnet", csprojPath);
|
||||
|
||||
var first = await extractor.ExtractAsync(request);
|
||||
var second = await extractor.ExtractAsync(request);
|
||||
|
||||
Assert.Equal(first.GraphDigest, second.GraphDigest);
|
||||
Assert.Equal(first.Nodes.Select(n => n.NodeId), second.Nodes.Select(n => n.NodeId));
|
||||
Assert.Equal(first.Edges.Select(e => (e.SourceId, e.TargetId, e.CallKind)), second.Edges.Select(e => (e.SourceId, e.TargetId, e.CallKind)));
|
||||
}
|
||||
|
||||
private sealed class FixedTimeProvider : TimeProvider
|
||||
{
|
||||
private readonly DateTimeOffset _instant;
|
||||
|
||||
public FixedTimeProvider(DateTimeOffset instant)
|
||||
{
|
||||
_instant = instant;
|
||||
}
|
||||
|
||||
public override DateTimeOffset GetUtcNow() => _instant;
|
||||
}
|
||||
|
||||
private sealed class TempDirectory : IAsyncDisposable
|
||||
{
|
||||
public string Path { get; }
|
||||
|
||||
private TempDirectory(string path)
|
||||
{
|
||||
Path = path;
|
||||
}
|
||||
|
||||
public static Task<TempDirectory> CreateAsync()
|
||||
{
|
||||
var root = System.IO.Path.Combine(System.IO.Path.GetTempPath(), $"stella_callgraph_{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(root);
|
||||
return Task.FromResult(new TempDirectory(root));
|
||||
}
|
||||
|
||||
public ValueTask DisposeAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Directory.Exists(Path))
|
||||
{
|
||||
Directory.Delete(Path, recursive: true);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// best effort cleanup
|
||||
}
|
||||
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
using System.Collections.Immutable;
|
||||
using StellaOps.Scanner.CallGraph;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scanner.CallGraph.Tests;
|
||||
|
||||
public class ReachabilityAnalyzerTests
|
||||
{
|
||||
[Fact]
|
||||
public void Analyze_WhenSinkReachable_ReturnsShortestPath()
|
||||
{
|
||||
var entry = CallGraphNodeIds.Compute("dotnet:test:entry");
|
||||
var mid = CallGraphNodeIds.Compute("dotnet:test:mid");
|
||||
var sink = CallGraphNodeIds.Compute("dotnet:test:sink");
|
||||
|
||||
var snapshot = new CallGraphSnapshot(
|
||||
ScanId: "scan-1",
|
||||
GraphDigest: "sha256:placeholder",
|
||||
Language: "dotnet",
|
||||
ExtractedAt: DateTimeOffset.UtcNow,
|
||||
Nodes:
|
||||
[
|
||||
new CallGraphNode(entry, "Entry", "file.cs", 1, "app", Visibility.Public, true, EntrypointType.HttpHandler, false, null),
|
||||
new CallGraphNode(mid, "Mid", "file.cs", 2, "app", Visibility.Public, false, null, false, null),
|
||||
new CallGraphNode(sink, "Sink", "file.cs", 3, "System", Visibility.Public, false, null, true, StellaOps.Scanner.Reachability.SinkCategory.CmdExec),
|
||||
],
|
||||
Edges:
|
||||
[
|
||||
new CallGraphEdge(entry, mid, CallKind.Direct),
|
||||
new CallGraphEdge(mid, sink, CallKind.Direct),
|
||||
],
|
||||
EntrypointIds: [entry],
|
||||
SinkIds: [sink]);
|
||||
|
||||
var analyzer = new ReachabilityAnalyzer();
|
||||
var result = analyzer.Analyze(snapshot);
|
||||
|
||||
Assert.Contains(sink, result.ReachableSinkIds);
|
||||
Assert.Single(result.Paths);
|
||||
Assert.Equal(entry, result.Paths[0].EntrypointId);
|
||||
Assert.Equal(sink, result.Paths[0].SinkId);
|
||||
Assert.Equal(ImmutableArray.Create(entry, mid, sink), result.Paths[0].NodeIds);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Analyze_WhenNoEntrypoints_ReturnsEmpty()
|
||||
{
|
||||
var snapshot = new CallGraphSnapshot(
|
||||
ScanId: "scan-1",
|
||||
GraphDigest: "sha256:placeholder",
|
||||
Language: "dotnet",
|
||||
ExtractedAt: DateTimeOffset.UtcNow,
|
||||
Nodes: ImmutableArray<CallGraphNode>.Empty,
|
||||
Edges: ImmutableArray<CallGraphEdge>.Empty,
|
||||
EntrypointIds: ImmutableArray<string>.Empty,
|
||||
SinkIds: ImmutableArray<string>.Empty);
|
||||
|
||||
var analyzer = new ReachabilityAnalyzer();
|
||||
var result = analyzer.Analyze(snapshot);
|
||||
|
||||
Assert.Empty(result.ReachableNodeIds);
|
||||
Assert.Empty(result.ReachableSinkIds);
|
||||
Assert.Empty(result.Paths);
|
||||
Assert.False(string.IsNullOrWhiteSpace(result.ResultDigest));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.0" />
|
||||
<PackageReference Include="xunit" Version="2.9.2" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\\..\\__Libraries\\StellaOps.Scanner.CallGraph\\StellaOps.Scanner.CallGraph.csproj" />
|
||||
<ProjectReference Include="..\\..\\..\\__Libraries\\StellaOps.Messaging.Testing\\StellaOps.Messaging.Testing.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Messaging.Testing.Fixtures;
|
||||
using StellaOps.Scanner.CallGraph;
|
||||
using StellaOps.Scanner.CallGraph.Caching;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scanner.CallGraph.Tests;
|
||||
|
||||
[Collection(nameof(ValkeyFixtureCollection))]
|
||||
public class ValkeyCallGraphCacheServiceTests : IAsyncLifetime
|
||||
{
|
||||
private readonly ValkeyFixture _fixture;
|
||||
private ValkeyCallGraphCacheService _cache = null!;
|
||||
|
||||
public ValkeyCallGraphCacheServiceTests(ValkeyFixture fixture)
|
||||
{
|
||||
_fixture = fixture;
|
||||
}
|
||||
|
||||
public Task InitializeAsync()
|
||||
{
|
||||
var options = Options.Create(new CallGraphCacheConfig
|
||||
{
|
||||
Enabled = true,
|
||||
ConnectionString = _fixture.ConnectionString,
|
||||
KeyPrefix = "test:callgraph:",
|
||||
TtlSeconds = 60,
|
||||
EnableGzip = true,
|
||||
CircuitBreaker = new CircuitBreakerConfig { FailureThreshold = 3, TimeoutSeconds = 30, HalfOpenTimeout = 10 }
|
||||
});
|
||||
|
||||
_cache = new ValkeyCallGraphCacheService(options, NullLogger<ValkeyCallGraphCacheService>.Instance);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task DisposeAsync()
|
||||
{
|
||||
await _cache.DisposeAsync();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SetThenGet_CallGraph_RoundTrips()
|
||||
{
|
||||
var nodeId = CallGraphNodeIds.Compute("dotnet:test:entry");
|
||||
var snapshot = new CallGraphSnapshot(
|
||||
ScanId: "scan-cache-1",
|
||||
GraphDigest: "sha256:cg",
|
||||
Language: "dotnet",
|
||||
ExtractedAt: DateTimeOffset.UtcNow,
|
||||
Nodes: [new CallGraphNode(nodeId, "Entry", "file.cs", 1, "app", Visibility.Public, true, EntrypointType.HttpHandler, false, null)],
|
||||
Edges: [],
|
||||
EntrypointIds: [nodeId],
|
||||
SinkIds: []);
|
||||
|
||||
await _cache.SetCallGraphAsync(snapshot);
|
||||
var loaded = await _cache.TryGetCallGraphAsync("scan-cache-1", "dotnet");
|
||||
|
||||
Assert.NotNull(loaded);
|
||||
Assert.Equal(snapshot.ScanId, loaded!.ScanId);
|
||||
Assert.Equal(snapshot.Language, loaded.Language);
|
||||
Assert.Equal(snapshot.GraphDigest, loaded.GraphDigest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SetThenGet_ReachabilityResult_RoundTrips()
|
||||
{
|
||||
var result = new ReachabilityAnalysisResult(
|
||||
ScanId: "scan-cache-2",
|
||||
GraphDigest: "sha256:cg",
|
||||
Language: "dotnet",
|
||||
ComputedAt: DateTimeOffset.UtcNow,
|
||||
ReachableNodeIds: [],
|
||||
ReachableSinkIds: [],
|
||||
Paths: [],
|
||||
ResultDigest: "sha256:r");
|
||||
|
||||
await _cache.SetReachabilityResultAsync(result);
|
||||
var loaded = await _cache.TryGetReachabilityResultAsync("scan-cache-2", "dotnet");
|
||||
|
||||
Assert.NotNull(loaded);
|
||||
Assert.Equal(result.ResultDigest, loaded!.ResultDigest);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user