Add unit tests for SBOM ingestion and transformation
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
- Implement `SbomIngestServiceCollectionExtensionsTests` to verify the SBOM ingestion pipeline exports snapshots correctly. - Create `SbomIngestTransformerTests` to ensure the transformation produces expected nodes and edges, including deduplication of license nodes and normalization of timestamps. - Add `SbomSnapshotExporterTests` to test the export functionality for manifest, adjacency, nodes, and edges. - Introduce `VexOverlayTransformerTests` to validate the transformation of VEX nodes and edges. - Set up project file for the test project with necessary dependencies and configurations. - Include JSON fixture files for testing purposes.
This commit is contained in:
@@ -0,0 +1,234 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.Metrics;
|
||||
using System.Linq;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using StellaOps.Scheduler.Models;
|
||||
using StellaOps.Scheduler.Storage.Mongo.Repositories;
|
||||
|
||||
namespace StellaOps.Scheduler.WebService.PolicySimulations;
|
||||
|
||||
internal interface IPolicySimulationMetricsProvider
|
||||
{
|
||||
Task<PolicySimulationMetricsResponse> CaptureAsync(string tenantId, CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
internal interface IPolicySimulationMetricsRecorder
|
||||
{
|
||||
void RecordLatency(PolicyRunStatus status, DateTimeOffset observedAt);
|
||||
}
|
||||
|
||||
internal sealed class PolicySimulationMetricsProvider : IPolicySimulationMetricsProvider, IPolicySimulationMetricsRecorder, IDisposable
|
||||
{
|
||||
private static readonly PolicyRunJobStatus[] QueueStatuses =
|
||||
{
|
||||
PolicyRunJobStatus.Pending,
|
||||
PolicyRunJobStatus.Dispatching,
|
||||
PolicyRunJobStatus.Submitted,
|
||||
};
|
||||
|
||||
private static readonly PolicyRunJobStatus[] TerminalStatuses =
|
||||
{
|
||||
PolicyRunJobStatus.Completed,
|
||||
PolicyRunJobStatus.Failed,
|
||||
PolicyRunJobStatus.Cancelled,
|
||||
};
|
||||
|
||||
private readonly IPolicyRunJobRepository _repository;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly Meter _meter;
|
||||
private readonly ObservableGauge<long> _queueGauge;
|
||||
private readonly Histogram<double> _latencyHistogram;
|
||||
private readonly object _snapshotLock = new();
|
||||
private IReadOnlyDictionary<string, long> _latestQueueSnapshot = new Dictionary<string, long>(StringComparer.Ordinal);
|
||||
private bool _disposed;
|
||||
|
||||
public PolicySimulationMetricsProvider(IPolicyRunJobRepository repository, TimeProvider? timeProvider = null)
|
||||
{
|
||||
_repository = repository ?? throw new ArgumentNullException(nameof(repository));
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
_meter = new Meter("StellaOps.Scheduler.WebService.PolicySimulations");
|
||||
_queueGauge = _meter.CreateObservableGauge<long>(
|
||||
"policy_simulation_queue_depth",
|
||||
ObserveQueueDepth,
|
||||
unit: "runs",
|
||||
description: "Queued policy simulation jobs grouped by status.");
|
||||
_latencyHistogram = _meter.CreateHistogram<double>(
|
||||
"policy_simulation_latency",
|
||||
unit: "s",
|
||||
description: "End-to-end policy simulation latency (seconds).");
|
||||
}
|
||||
|
||||
public async Task<PolicySimulationMetricsResponse> CaptureAsync(string tenantId, CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
if (string.IsNullOrWhiteSpace(tenantId))
|
||||
{
|
||||
throw new ArgumentException("Tenant id must be provided.", nameof(tenantId));
|
||||
}
|
||||
|
||||
var queueCounts = new Dictionary<string, long>(StringComparer.OrdinalIgnoreCase);
|
||||
long totalQueueDepth = 0;
|
||||
|
||||
foreach (var status in QueueStatuses)
|
||||
{
|
||||
var count = await _repository.CountAsync(
|
||||
tenantId,
|
||||
PolicyRunMode.Simulate,
|
||||
new[] { status },
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
queueCounts[status.ToString().ToLowerInvariant()] = count;
|
||||
totalQueueDepth += count;
|
||||
}
|
||||
|
||||
lock (_snapshotLock)
|
||||
{
|
||||
_latestQueueSnapshot = queueCounts;
|
||||
}
|
||||
|
||||
var sampleSize = 200;
|
||||
var recentJobs = await _repository.ListAsync(
|
||||
tenantId,
|
||||
policyId: null,
|
||||
mode: PolicyRunMode.Simulate,
|
||||
statuses: TerminalStatuses,
|
||||
queuedAfter: null,
|
||||
limit: sampleSize,
|
||||
cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var durations = recentJobs
|
||||
.Select(job => CalculateLatencySeconds(job, _timeProvider.GetUtcNow()))
|
||||
.Where(duration => duration >= 0)
|
||||
.OrderBy(duration => duration)
|
||||
.ToArray();
|
||||
|
||||
var latencyMetrics = new PolicySimulationLatencyMetrics(
|
||||
durations.Length,
|
||||
Percentile(durations, 0.50),
|
||||
Percentile(durations, 0.90),
|
||||
Percentile(durations, 0.95),
|
||||
Percentile(durations, 0.99),
|
||||
Average(durations));
|
||||
|
||||
return new PolicySimulationMetricsResponse(
|
||||
new PolicySimulationQueueDepth(totalQueueDepth, queueCounts),
|
||||
latencyMetrics);
|
||||
}
|
||||
|
||||
public void RecordLatency(PolicyRunStatus status, DateTimeOffset observedAt)
|
||||
{
|
||||
if (status is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(status));
|
||||
}
|
||||
|
||||
var latencySeconds = CalculateLatencySeconds(status, observedAt);
|
||||
if (latencySeconds >= 0)
|
||||
{
|
||||
_latencyHistogram.Record(latencySeconds);
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerable<Measurement<long>> ObserveQueueDepth()
|
||||
{
|
||||
IReadOnlyDictionary<string, long> snapshot;
|
||||
lock (_snapshotLock)
|
||||
{
|
||||
snapshot = _latestQueueSnapshot;
|
||||
}
|
||||
|
||||
foreach (var pair in snapshot)
|
||||
{
|
||||
yield return new Measurement<long>(
|
||||
pair.Value,
|
||||
new KeyValuePair<string, object?>("status", pair.Key));
|
||||
}
|
||||
}
|
||||
|
||||
private static double CalculateLatencySeconds(PolicyRunJob job, DateTimeOffset now)
|
||||
{
|
||||
var started = job.QueuedAt ?? job.CreatedAt;
|
||||
var finished = job.CompletedAt ?? job.CancelledAt ?? job.UpdatedAt;
|
||||
if (started == default)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
var duration = (finished - started).TotalSeconds;
|
||||
return duration < 0 ? 0 : duration;
|
||||
}
|
||||
|
||||
private static double CalculateLatencySeconds(PolicyRunStatus status, DateTimeOffset now)
|
||||
{
|
||||
var started = status.QueuedAt;
|
||||
var finished = status.FinishedAt ?? now;
|
||||
if (started == default)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
var duration = (finished - started).TotalSeconds;
|
||||
return duration < 0 ? 0 : duration;
|
||||
}
|
||||
|
||||
private static double? Percentile(IReadOnlyList<double> values, double percentile)
|
||||
{
|
||||
if (values.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var position = percentile * (values.Count - 1);
|
||||
var lowerIndex = (int)Math.Floor(position);
|
||||
var upperIndex = (int)Math.Ceiling(position);
|
||||
|
||||
if (lowerIndex == upperIndex)
|
||||
{
|
||||
return Math.Round(values[lowerIndex], 4);
|
||||
}
|
||||
|
||||
var fraction = position - lowerIndex;
|
||||
var interpolated = values[lowerIndex] + (values[upperIndex] - values[lowerIndex]) * fraction;
|
||||
return Math.Round(interpolated, 4);
|
||||
}
|
||||
|
||||
private static double? Average(IReadOnlyList<double> values)
|
||||
{
|
||||
if (values.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var sum = values.Sum();
|
||||
return Math.Round(sum / values.Count, 4);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_meter.Dispose();
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed record PolicySimulationMetricsResponse(
|
||||
[property: JsonPropertyName("policy_simulation_queue_depth")] PolicySimulationQueueDepth QueueDepth,
|
||||
[property: JsonPropertyName("policy_simulation_latency")] PolicySimulationLatencyMetrics Latency);
|
||||
|
||||
internal sealed record PolicySimulationQueueDepth(
|
||||
[property: JsonPropertyName("total")] long Total,
|
||||
[property: JsonPropertyName("by_status")] IReadOnlyDictionary<string, long> ByStatus);
|
||||
|
||||
internal sealed record PolicySimulationLatencyMetrics(
|
||||
[property: JsonPropertyName("samples")] int Samples,
|
||||
[property: JsonPropertyName("p50_seconds")] double? P50,
|
||||
[property: JsonPropertyName("p90_seconds")] double? P90,
|
||||
[property: JsonPropertyName("p95_seconds")] double? P95,
|
||||
[property: JsonPropertyName("p99_seconds")] double? P99,
|
||||
[property: JsonPropertyName("mean_seconds")] double? Mean);
|
||||
Reference in New Issue
Block a user