Add integration tests for Proof Chain and Reachability workflows

- Implement ProofChainTestFixture for PostgreSQL-backed integration tests.
- Create StellaOps.Integration.ProofChain project with necessary dependencies.
- Add ReachabilityIntegrationTests to validate call graph extraction and reachability analysis.
- Introduce ReachabilityTestFixture for managing corpus and fixture paths.
- Establish StellaOps.Integration.Reachability project with required references.
- Develop UnknownsWorkflowTests to cover the unknowns lifecycle: detection, ranking, escalation, and resolution.
- Create StellaOps.Integration.Unknowns project with dependencies for unknowns workflow.
This commit is contained in:
StellaOps Bot
2025-12-20 22:19:26 +02:00
parent 3c6e14fca5
commit efe9bd8cfe
86 changed files with 9616 additions and 323 deletions

View File

@@ -0,0 +1,453 @@
// =============================================================================
// StellaOps.Integration.Performance - Performance Baseline Tests
// Sprint 3500.0004.0003 - T7: Performance Baseline Tests
// =============================================================================
using FluentAssertions;
using System.Diagnostics;
using System.Text.Json;
using Xunit;
namespace StellaOps.Integration.Performance;
/// <summary>
/// Performance baseline tests to establish and validate performance characteristics.
/// Uses timing measurements against known baselines with 20% regression threshold.
/// </summary>
/// <remarks>
/// T7-AC1: Score computation time baseline
/// T7-AC2: Proof bundle generation baseline
/// T7-AC3: Call graph extraction baseline
/// T7-AC4: Reachability computation baseline
/// T7-AC5: Regression alerts on >20% degradation
/// </remarks>
[Trait("Category", "Performance")]
[Trait("Category", "Integration")]
public class PerformanceBaselineTests : IClassFixture<PerformanceTestFixture>
{
private readonly PerformanceTestFixture _fixture;
private const double RegressionThresholdPercent = 20.0;
public PerformanceBaselineTests(PerformanceTestFixture fixture)
{
_fixture = fixture;
}
#region T7-AC1: Score Computation Baseline
[Fact(DisplayName = "T7-AC1.1: Score computation completes within baseline")]
public async Task ScoreComputation_CompletesWithinBaseline()
{
// Arrange
var baseline = _fixture.GetBaseline("score_computation_ms");
var findings = GenerateSampleFindings(100);
// Act
var sw = Stopwatch.StartNew();
var score = await ComputeScoreAsync(findings);
sw.Stop();
// Assert
var actualMs = sw.ElapsedMilliseconds;
var threshold = baseline * (1 + RegressionThresholdPercent / 100);
actualMs.Should().BeLessThanOrEqualTo((long)threshold,
$"Score computation took {actualMs}ms, exceeding baseline {baseline}ms + {RegressionThresholdPercent}% threshold");
// Record for baseline updates
_fixture.RecordMeasurement("score_computation_ms", actualMs);
}
[Fact(DisplayName = "T7-AC1.2: Score computation scales linearly with findings")]
public async Task ScoreComputation_ScalesLinearly()
{
// Arrange
var sizes = new[] { 10, 50, 100, 200 };
var times = new List<(int size, long ms)>();
// Act
foreach (var size in sizes)
{
var findings = GenerateSampleFindings(size);
var sw = Stopwatch.StartNew();
await ComputeScoreAsync(findings);
sw.Stop();
times.Add((size, sw.ElapsedMilliseconds));
}
// Assert - verify roughly linear scaling (within 3x of linear)
var baseRatio = times[0].ms / (double)times[0].size;
foreach (var (size, ms) in times.Skip(1))
{
var actualRatio = ms / (double)size;
var scaleFactor = actualRatio / baseRatio;
scaleFactor.Should().BeLessThan(3.0,
$"Score computation at size {size} shows non-linear scaling (factor: {scaleFactor:F2}x)");
}
}
[Fact(DisplayName = "T7-AC1.3: Score computation handles large finding sets")]
public async Task ScoreComputation_HandlesLargeSets()
{
// Arrange
var baseline = _fixture.GetBaseline("score_computation_large_ms");
var findings = GenerateSampleFindings(1000);
// Act
var sw = Stopwatch.StartNew();
var score = await ComputeScoreAsync(findings);
sw.Stop();
// Assert
var threshold = baseline * (1 + RegressionThresholdPercent / 100);
sw.ElapsedMilliseconds.Should().BeLessThanOrEqualTo((long)threshold);
_fixture.RecordMeasurement("score_computation_large_ms", sw.ElapsedMilliseconds);
}
#endregion
#region T7-AC2: Proof Bundle Generation Baseline
[Fact(DisplayName = "T7-AC2.1: Proof bundle generation completes within baseline")]
public async Task ProofBundleGeneration_CompletesWithinBaseline()
{
// Arrange
var baseline = _fixture.GetBaseline("proof_bundle_generation_ms");
var manifest = GenerateSampleManifest();
// Act
var sw = Stopwatch.StartNew();
var bundle = await GenerateProofBundleAsync(manifest);
sw.Stop();
// Assert
var threshold = baseline * (1 + RegressionThresholdPercent / 100);
sw.ElapsedMilliseconds.Should().BeLessThanOrEqualTo((long)threshold,
$"Proof bundle generation took {sw.ElapsedMilliseconds}ms, exceeding baseline {baseline}ms");
_fixture.RecordMeasurement("proof_bundle_generation_ms", sw.ElapsedMilliseconds);
}
[Fact(DisplayName = "T7-AC2.2: Proof signing performance within baseline")]
public async Task ProofSigning_WithinBaseline()
{
// Arrange
var baseline = _fixture.GetBaseline("proof_signing_ms");
var payload = GenerateSamplePayload(10 * 1024); // 10KB payload
// Act
var sw = Stopwatch.StartNew();
var signature = await SignPayloadAsync(payload);
sw.Stop();
// Assert
var threshold = baseline * (1 + RegressionThresholdPercent / 100);
sw.ElapsedMilliseconds.Should().BeLessThanOrEqualTo((long)threshold);
_fixture.RecordMeasurement("proof_signing_ms", sw.ElapsedMilliseconds);
}
#endregion
#region T7-AC3: Call Graph Extraction Baseline
[Fact(DisplayName = "T7-AC3.1: .NET call graph extraction within baseline")]
public async Task DotNetCallGraphExtraction_WithinBaseline()
{
// Arrange
var baseline = _fixture.GetBaseline("dotnet_callgraph_extraction_ms");
var assemblyPath = _fixture.GetTestAssemblyPath("DotNetSample");
// Act
var sw = Stopwatch.StartNew();
var graph = await ExtractDotNetCallGraphAsync(assemblyPath);
sw.Stop();
// Assert
var threshold = baseline * (1 + RegressionThresholdPercent / 100);
sw.ElapsedMilliseconds.Should().BeLessThanOrEqualTo((long)threshold,
$"Call graph extraction took {sw.ElapsedMilliseconds}ms, exceeding baseline {baseline}ms");
_fixture.RecordMeasurement("dotnet_callgraph_extraction_ms", sw.ElapsedMilliseconds);
}
[Fact(DisplayName = "T7-AC3.2: Call graph scales with assembly size")]
public async Task CallGraphExtraction_ScalesWithSize()
{
// Arrange
var assemblies = _fixture.GetTestAssemblies();
var results = new List<(string name, int nodes, long ms)>();
// Act
foreach (var assembly in assemblies)
{
var sw = Stopwatch.StartNew();
var graph = await ExtractDotNetCallGraphAsync(assembly.Path);
sw.Stop();
results.Add((assembly.Name, graph.NodeCount, sw.ElapsedMilliseconds));
}
// Assert - log results for baseline establishment
foreach (var (name, nodes, ms) in results)
{
_fixture.RecordMeasurement($"callgraph_{name}_ms", ms);
}
// Verify no catastrophic performance (>10s for any assembly)
results.Should().AllSatisfy(r => r.ms.Should().BeLessThan(10000));
}
#endregion
#region T7-AC4: Reachability Computation Baseline
[Fact(DisplayName = "T7-AC4.1: Reachability computation within baseline")]
public async Task ReachabilityComputation_WithinBaseline()
{
// Arrange
var baseline = _fixture.GetBaseline("reachability_computation_ms");
var callGraph = GenerateSampleCallGraph(500, 1000); // 500 nodes, 1000 edges
// Act
var sw = Stopwatch.StartNew();
var result = await ComputeReachabilityAsync(callGraph);
sw.Stop();
// Assert
var threshold = baseline * (1 + RegressionThresholdPercent / 100);
sw.ElapsedMilliseconds.Should().BeLessThanOrEqualTo((long)threshold,
$"Reachability computation took {sw.ElapsedMilliseconds}ms, exceeding baseline {baseline}ms");
_fixture.RecordMeasurement("reachability_computation_ms", sw.ElapsedMilliseconds);
}
[Fact(DisplayName = "T7-AC4.2: Large graph reachability within baseline")]
public async Task LargeGraphReachability_WithinBaseline()
{
// Arrange
var baseline = _fixture.GetBaseline("reachability_large_graph_ms");
var callGraph = GenerateSampleCallGraph(2000, 5000); // 2000 nodes, 5000 edges
// Act
var sw = Stopwatch.StartNew();
var result = await ComputeReachabilityAsync(callGraph);
sw.Stop();
// Assert
var threshold = baseline * (1 + RegressionThresholdPercent / 100);
sw.ElapsedMilliseconds.Should().BeLessThanOrEqualTo((long)threshold,
$"Large graph reachability took {sw.ElapsedMilliseconds}ms, exceeding baseline {baseline}ms");
_fixture.RecordMeasurement("reachability_large_graph_ms", sw.ElapsedMilliseconds);
}
[Fact(DisplayName = "T7-AC4.3: Reachability with deep paths within baseline")]
public async Task DeepPathReachability_WithinBaseline()
{
// Arrange
var baseline = _fixture.GetBaseline("reachability_deep_path_ms");
var callGraph = GenerateDeepCallGraph(100); // 100 levels deep
// Act
var sw = Stopwatch.StartNew();
var result = await ComputeReachabilityAsync(callGraph);
sw.Stop();
// Assert
var threshold = baseline * (1 + RegressionThresholdPercent / 100);
sw.ElapsedMilliseconds.Should().BeLessThanOrEqualTo((long)threshold);
_fixture.RecordMeasurement("reachability_deep_path_ms", sw.ElapsedMilliseconds);
}
#endregion
#region T7-AC5: Regression Alerts
[Fact(DisplayName = "T7-AC5.1: All baselines within threshold")]
public void AllBaselines_WithinThreshold()
{
// Arrange
var measurements = _fixture.GetAllMeasurements();
var regressions = new List<string>();
// Act & Assert
foreach (var (metric, measured) in measurements)
{
var baseline = _fixture.GetBaseline(metric);
var threshold = baseline * (1 + RegressionThresholdPercent / 100);
if (measured > threshold)
{
var regression = (measured - baseline) / baseline * 100;
regressions.Add($"{metric}: {measured}ms vs baseline {baseline}ms (+{regression:F1}%)");
}
}
regressions.Should().BeEmpty(
$"Performance regressions detected (>{RegressionThresholdPercent}%):\n" +
string.Join("\n", regressions));
}
[Fact(DisplayName = "T7-AC5.2: Generate regression report")]
public void GenerateRegressionReport()
{
// Arrange
var measurements = _fixture.GetAllMeasurements();
// Act
var report = new PerformanceReport
{
GeneratedAt = DateTime.UtcNow,
ThresholdPercent = RegressionThresholdPercent,
Metrics = measurements.Select(m => new MetricReport
{
Name = m.metric,
Baseline = _fixture.GetBaseline(m.metric),
Measured = m.value,
DeltaPercent = (m.value - _fixture.GetBaseline(m.metric)) / _fixture.GetBaseline(m.metric) * 100
}).ToList()
};
// Assert - report should be valid
report.Metrics.Should().NotBeEmpty();
// Write report for CI consumption
var json = JsonSerializer.Serialize(report, new JsonSerializerOptions { WriteIndented = true });
_fixture.SaveReport("performance-report.json", json);
}
#endregion
#region Helper Methods
private static List<SampleFinding> GenerateSampleFindings(int count)
{
return Enumerable.Range(1, count)
.Select(i => new SampleFinding
{
Id = $"finding-{i:D4}",
CveId = $"CVE-2024-{i:D5}",
Severity = (i % 4) switch
{
0 => "CRITICAL",
1 => "HIGH",
2 => "MEDIUM",
_ => "LOW"
},
CvssScore = 10.0 - (i % 10)
})
.ToList();
}
private static async Task<double> ComputeScoreAsync(List<SampleFinding> findings)
{
// Simulated score computation
await Task.Delay(findings.Count / 10); // ~10 findings per ms
return findings.Sum(f => f.CvssScore) / findings.Count;
}
private static SampleManifest GenerateSampleManifest()
{
return new SampleManifest
{
Id = Guid.NewGuid().ToString(),
CreatedAt = DateTime.UtcNow,
Findings = GenerateSampleFindings(50)
};
}
private static async Task<byte[]> GenerateProofBundleAsync(SampleManifest manifest)
{
await Task.Delay(50); // Simulated bundle generation
return JsonSerializer.SerializeToUtf8Bytes(manifest);
}
private static byte[] GenerateSamplePayload(int sizeBytes)
{
var random = new Random(42);
var buffer = new byte[sizeBytes];
random.NextBytes(buffer);
return buffer;
}
private static async Task<byte[]> SignPayloadAsync(byte[] payload)
{
await Task.Delay(10); // Simulated signing
using var sha256 = System.Security.Cryptography.SHA256.Create();
return sha256.ComputeHash(payload);
}
private static async Task<SampleCallGraph> ExtractDotNetCallGraphAsync(string assemblyPath)
{
await Task.Delay(100); // Simulated extraction
return new SampleCallGraph { NodeCount = 100, EdgeCount = 250 };
}
private static SampleCallGraph GenerateSampleCallGraph(int nodes, int edges)
{
return new SampleCallGraph { NodeCount = nodes, EdgeCount = edges };
}
private static SampleCallGraph GenerateDeepCallGraph(int depth)
{
return new SampleCallGraph { NodeCount = depth, EdgeCount = depth - 1, Depth = depth };
}
private static async Task<ReachabilityResult> ComputeReachabilityAsync(SampleCallGraph graph)
{
// Simulated reachability - O(V + E) complexity
var delay = (graph.NodeCount + graph.EdgeCount) / 100;
await Task.Delay(Math.Max(1, delay));
return new ReachabilityResult { ReachableNodes = graph.NodeCount / 2 };
}
#endregion
#region Sample Types
private record SampleFinding
{
public string Id { get; init; } = "";
public string CveId { get; init; } = "";
public string Severity { get; init; } = "";
public double CvssScore { get; init; }
}
private record SampleManifest
{
public string Id { get; init; } = "";
public DateTime CreatedAt { get; init; }
public List<SampleFinding> Findings { get; init; } = new();
}
private record SampleCallGraph
{
public int NodeCount { get; init; }
public int EdgeCount { get; init; }
public int Depth { get; init; }
}
private record ReachabilityResult
{
public int ReachableNodes { get; init; }
}
private record PerformanceReport
{
public DateTime GeneratedAt { get; init; }
public double ThresholdPercent { get; init; }
public List<MetricReport> Metrics { get; init; } = new();
}
private record MetricReport
{
public string Name { get; init; } = "";
public double Baseline { get; init; }
public double Measured { get; init; }
public double DeltaPercent { get; init; }
}
#endregion
}