Add tests for SBOM generation determinism across multiple formats

- Created `StellaOps.TestKit.Tests` project for unit tests related to determinism.
- Implemented `DeterminismManifestTests` to validate deterministic output for canonical bytes and strings, file read/write operations, and error handling for invalid schema versions.
- Added `SbomDeterminismTests` to ensure identical inputs produce consistent SBOMs across SPDX 3.0.1 and CycloneDX 1.6/1.7 formats, including parallel execution tests.
- Updated project references in `StellaOps.Integration.Determinism` to include the new determinism testing library.
This commit is contained in:
master
2025-12-23 18:56:12 +02:00
committed by StellaOps Bot
parent 7ac70ece71
commit 491e883653
409 changed files with 23797 additions and 17779 deletions

View File

@@ -0,0 +1,162 @@
using System.Diagnostics;
using OpenTelemetry;
using Xunit;
namespace StellaOps.TestKit.Observability;
/// <summary>
/// Captures OpenTelemetry traces and spans during test execution for assertion.
/// </summary>
/// <remarks>
/// Usage:
/// <code>
/// using var capture = new OtelCapture();
///
/// // Execute code that emits traces
/// await MyService.DoWorkAsync();
///
/// // Assert traces were emitted
/// capture.AssertHasSpan("MyService.DoWork");
/// capture.AssertHasTag("user_id", "123");
/// capture.AssertSpanCount(expectedCount: 3);
/// </code>
/// </remarks>
public sealed class OtelCapture : IDisposable
{
private readonly List<Activity> _capturedActivities = new();
private readonly ActivityListener _listener;
private bool _disposed;
/// <summary>
/// Creates a new OTel capture and starts listening for activities.
/// </summary>
/// <param name="activitySourceName">Optional activity source name filter. If null, captures all activities.</param>
public OtelCapture(string? activitySourceName = null)
{
_listener = new ActivityListener
{
ShouldListenTo = source => activitySourceName == null || source.Name == activitySourceName,
Sample = (ref ActivityCreationOptions<ActivityContext> _) => ActivitySamplingResult.AllDataAndRecorded,
ActivityStopped = activity =>
{
lock (_capturedActivities)
{
_capturedActivities.Add(activity);
}
}
};
ActivitySource.AddActivityListener(_listener);
}
/// <summary>
/// Gets all captured activities (spans).
/// </summary>
public IReadOnlyList<Activity> CapturedActivities
{
get
{
lock (_capturedActivities)
{
return _capturedActivities.ToList();
}
}
}
/// <summary>
/// Asserts that a span with the specified name was captured.
/// </summary>
public void AssertHasSpan(string spanName)
{
lock (_capturedActivities)
{
Assert.Contains(_capturedActivities, a => a.DisplayName == spanName || a.OperationName == spanName);
}
}
/// <summary>
/// Asserts that at least one span has the specified tag (attribute).
/// </summary>
public void AssertHasTag(string tagKey, string expectedValue)
{
lock (_capturedActivities)
{
var found = _capturedActivities.Any(a =>
a.Tags.Any(tag => tag.Key == tagKey && tag.Value == expectedValue));
Assert.True(found, $"No span found with tag {tagKey}={expectedValue}");
}
}
/// <summary>
/// Asserts that exactly the specified number of spans were captured.
/// </summary>
public void AssertSpanCount(int expectedCount)
{
lock (_capturedActivities)
{
Assert.Equal(expectedCount, _capturedActivities.Count);
}
}
/// <summary>
/// Asserts that a span with the specified name has the expected tag.
/// </summary>
public void AssertSpanHasTag(string spanName, string tagKey, string expectedValue)
{
lock (_capturedActivities)
{
var span = _capturedActivities.FirstOrDefault(a =>
a.DisplayName == spanName || a.OperationName == spanName);
Assert.NotNull(span);
var tag = span.Tags.FirstOrDefault(t => t.Key == tagKey);
Assert.True(tag.Key != null, $"Tag '{tagKey}' not found in span '{spanName}'");
Assert.Equal(expectedValue, tag.Value);
}
}
/// <summary>
/// Asserts that spans form a valid parent-child hierarchy.
/// </summary>
public void AssertHierarchy(string parentSpanName, string childSpanName)
{
lock (_capturedActivities)
{
var parent = _capturedActivities.FirstOrDefault(a =>
a.DisplayName == parentSpanName || a.OperationName == parentSpanName);
var child = _capturedActivities.FirstOrDefault(a =>
a.DisplayName == childSpanName || a.OperationName == childSpanName);
Assert.NotNull(parent);
Assert.NotNull(child);
Assert.Equal(parent.SpanId, child.ParentSpanId);
}
}
/// <summary>
/// Clears all captured activities.
/// </summary>
public void Clear()
{
lock (_capturedActivities)
{
_capturedActivities.Clear();
}
}
/// <summary>
/// Disposes the capture and stops listening for activities.
/// </summary>
public void Dispose()
{
if (_disposed)
{
return;
}
_listener?.Dispose();
_disposed = true;
}
}