save progress
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
// <copyright file="IReplayOrchestrator.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under AGPL-3.0-or-later.
|
||||
// </copyright>
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using StellaOps.Replay.Anonymization;
|
||||
using StellaOps.Testing.Temporal;
|
||||
|
||||
namespace StellaOps.Testing.Replay;
|
||||
|
||||
/// <summary>
|
||||
/// Orchestrates replay of anonymized traces for testing.
|
||||
/// </summary>
|
||||
public interface IReplayOrchestrator
|
||||
{
|
||||
/// <summary>
|
||||
/// Replay an anonymized trace.
|
||||
/// </summary>
|
||||
/// <param name="trace">The trace to replay.</param>
|
||||
/// <param name="timeProvider">Time provider for simulated time.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>The replay result.</returns>
|
||||
Task<ReplayResult> ReplayAsync(
|
||||
AnonymizedTrace trace,
|
||||
SimulatedTimeProvider timeProvider,
|
||||
CancellationToken ct = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of a trace replay.
|
||||
/// </summary>
|
||||
/// <param name="Success">Whether replay succeeded.</param>
|
||||
/// <param name="OutputHash">Hash of replay output.</param>
|
||||
/// <param name="Duration">Duration of replay.</param>
|
||||
/// <param name="FailureReason">Reason for failure, if any.</param>
|
||||
/// <param name="Warnings">Warnings generated during replay.</param>
|
||||
/// <param name="SpanResults">Results for individual spans.</param>
|
||||
public sealed record ReplayResult(
|
||||
bool Success,
|
||||
string OutputHash,
|
||||
TimeSpan Duration,
|
||||
string? FailureReason,
|
||||
ImmutableArray<string> Warnings,
|
||||
ImmutableArray<SpanReplayResult> SpanResults);
|
||||
|
||||
/// <summary>
|
||||
/// Result of replaying a single span.
|
||||
/// </summary>
|
||||
/// <param name="SpanId">The span identifier.</param>
|
||||
/// <param name="Success">Whether span replay succeeded.</param>
|
||||
/// <param name="Duration">Duration of span replay.</param>
|
||||
/// <param name="DurationDelta">Difference from original duration.</param>
|
||||
/// <param name="OutputHash">Hash of span output.</param>
|
||||
public sealed record SpanReplayResult(
|
||||
string SpanId,
|
||||
bool Success,
|
||||
TimeSpan Duration,
|
||||
TimeSpan DurationDelta,
|
||||
string OutputHash);
|
||||
@@ -0,0 +1,126 @@
|
||||
// <copyright file="ITraceCorpusManager.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under AGPL-3.0-or-later.
|
||||
// </copyright>
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using StellaOps.Replay.Anonymization;
|
||||
|
||||
namespace StellaOps.Testing.Replay;
|
||||
|
||||
/// <summary>
|
||||
/// Manages corpus of anonymized traces for replay testing.
|
||||
/// </summary>
|
||||
public interface ITraceCorpusManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Import anonymized trace into corpus.
|
||||
/// </summary>
|
||||
/// <param name="trace">The anonymized trace.</param>
|
||||
/// <param name="classification">Classification of the trace.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>The corpus entry.</returns>
|
||||
Task<TraceCorpusEntry> ImportAsync(
|
||||
AnonymizedTrace trace,
|
||||
TraceClassification classification,
|
||||
CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Query traces by classification for test scenarios.
|
||||
/// </summary>
|
||||
/// <param name="query">The query parameters.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>Matching corpus entries.</returns>
|
||||
IAsyncEnumerable<TraceCorpusEntry> QueryAsync(
|
||||
TraceQuery query,
|
||||
CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Get trace statistics for corpus health.
|
||||
/// </summary>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>Corpus statistics.</returns>
|
||||
Task<TraceCorpusStatistics> GetStatisticsAsync(CancellationToken ct = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An entry in the trace corpus.
|
||||
/// </summary>
|
||||
/// <param name="EntryId">Unique entry identifier.</param>
|
||||
/// <param name="Trace">The anonymized trace.</param>
|
||||
/// <param name="Classification">Trace classification.</param>
|
||||
/// <param name="ImportedAt">When the trace was imported.</param>
|
||||
/// <param name="ExpectedOutputHash">Expected output hash for determinism verification.</param>
|
||||
public sealed record TraceCorpusEntry(
|
||||
string EntryId,
|
||||
AnonymizedTrace Trace,
|
||||
TraceClassification Classification,
|
||||
DateTimeOffset ImportedAt,
|
||||
string? ExpectedOutputHash);
|
||||
|
||||
/// <summary>
|
||||
/// Classification for a trace.
|
||||
/// </summary>
|
||||
/// <param name="Category">Trace category.</param>
|
||||
/// <param name="Complexity">Trace complexity level.</param>
|
||||
/// <param name="Tags">Additional tags.</param>
|
||||
/// <param name="FailureMode">Expected failure mode, if any.</param>
|
||||
public sealed record TraceClassification(
|
||||
TraceCategory Category,
|
||||
TraceComplexity Complexity,
|
||||
ImmutableArray<string> Tags,
|
||||
string? FailureMode);
|
||||
|
||||
/// <summary>
|
||||
/// Category of trace.
|
||||
/// </summary>
|
||||
public enum TraceCategory
|
||||
{
|
||||
Scan,
|
||||
Attestation,
|
||||
VexConsensus,
|
||||
Advisory,
|
||||
Evidence,
|
||||
Auth,
|
||||
MultiModule
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Complexity level of a trace.
|
||||
/// </summary>
|
||||
public enum TraceComplexity
|
||||
{
|
||||
Simple,
|
||||
Medium,
|
||||
Complex,
|
||||
EdgeCase
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Query parameters for trace corpus.
|
||||
/// </summary>
|
||||
/// <param name="Category">Filter by category.</param>
|
||||
/// <param name="MinComplexity">Minimum complexity level.</param>
|
||||
/// <param name="RequiredTags">Tags that must be present.</param>
|
||||
/// <param name="FailureMode">Filter by failure mode.</param>
|
||||
/// <param name="Limit">Maximum results to return.</param>
|
||||
public sealed record TraceQuery(
|
||||
TraceCategory? Category = null,
|
||||
TraceComplexity? MinComplexity = null,
|
||||
ImmutableArray<string> RequiredTags = default,
|
||||
string? FailureMode = null,
|
||||
int Limit = 100);
|
||||
|
||||
/// <summary>
|
||||
/// Statistics about the trace corpus.
|
||||
/// </summary>
|
||||
/// <param name="TotalTraces">Total number of traces.</param>
|
||||
/// <param name="TracesByCategory">Count by category.</param>
|
||||
/// <param name="TracesByComplexity">Count by complexity.</param>
|
||||
/// <param name="OldestTrace">Timestamp of oldest trace.</param>
|
||||
/// <param name="NewestTrace">Timestamp of newest trace.</param>
|
||||
public sealed record TraceCorpusStatistics(
|
||||
int TotalTraces,
|
||||
ImmutableDictionary<TraceCategory, int> TracesByCategory,
|
||||
ImmutableDictionary<TraceComplexity, int> TracesByComplexity,
|
||||
DateTimeOffset? OldestTrace,
|
||||
DateTimeOffset? NewestTrace);
|
||||
@@ -0,0 +1,187 @@
|
||||
// <copyright file="ReplayIntegrationTestBase.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under AGPL-3.0-or-later.
|
||||
// </copyright>
|
||||
// Sprint: SPRINT_20260105_002_002_TEST_trace_replay_evidence
|
||||
// Task: TREP-007, TREP-008
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using StellaOps.Replay.Anonymization;
|
||||
using StellaOps.Testing.Temporal;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Testing.Replay;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for integration tests that replay production traces.
|
||||
/// </summary>
|
||||
public abstract class ReplayIntegrationTestBase : IAsyncLifetime
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the trace corpus manager.
|
||||
/// </summary>
|
||||
protected ITraceCorpusManager CorpusManager { get; private set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the replay orchestrator.
|
||||
/// </summary>
|
||||
protected IReplayOrchestrator ReplayOrchestrator { get; private set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the simulated time provider.
|
||||
/// </summary>
|
||||
protected SimulatedTimeProvider TimeProvider { get; private set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the service provider.
|
||||
/// </summary>
|
||||
protected IServiceProvider Services { get; private set; } = null!;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual async ValueTask InitializeAsync()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
ConfigureServices(services);
|
||||
|
||||
Services = services.BuildServiceProvider();
|
||||
CorpusManager = Services.GetRequiredService<ITraceCorpusManager>();
|
||||
ReplayOrchestrator = Services.GetRequiredService<IReplayOrchestrator>();
|
||||
TimeProvider = Services.GetRequiredService<SimulatedTimeProvider>();
|
||||
|
||||
await OnInitializedAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configure services for the test.
|
||||
/// </summary>
|
||||
/// <param name="services">The service collection.</param>
|
||||
protected virtual void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
services.AddReplayTesting();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called after initialization is complete.
|
||||
/// </summary>
|
||||
protected virtual Task OnInitializedAsync() => Task.CompletedTask;
|
||||
|
||||
/// <summary>
|
||||
/// Replay a trace and verify behavior matches expected outcome.
|
||||
/// </summary>
|
||||
/// <param name="trace">The trace to replay.</param>
|
||||
/// <param name="expectation">Expected outcome.</param>
|
||||
/// <returns>The replay result.</returns>
|
||||
protected async Task<ReplayResult> ReplayAndVerifyAsync(
|
||||
TraceCorpusEntry trace,
|
||||
ReplayExpectation expectation)
|
||||
{
|
||||
var result = await ReplayOrchestrator.ReplayAsync(
|
||||
trace.Trace,
|
||||
TimeProvider);
|
||||
|
||||
VerifyExpectation(result, expectation);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Replay all traces matching query and collect results.
|
||||
/// </summary>
|
||||
/// <param name="query">Query for traces to replay.</param>
|
||||
/// <param name="expectationFactory">Factory to create expectations per trace.</param>
|
||||
/// <returns>Batch replay results.</returns>
|
||||
protected async Task<ReplayBatchResult> ReplayBatchAsync(
|
||||
TraceQuery query,
|
||||
Func<TraceCorpusEntry, ReplayExpectation> expectationFactory)
|
||||
{
|
||||
var results = new List<(TraceCorpusEntry Trace, ReplayResult Result, bool Passed)>();
|
||||
|
||||
await foreach (var trace in CorpusManager.QueryAsync(query))
|
||||
{
|
||||
var expectation = expectationFactory(trace);
|
||||
var result = await ReplayOrchestrator.ReplayAsync(trace.Trace, TimeProvider);
|
||||
|
||||
var passed = VerifyExpectationSafe(result, expectation);
|
||||
results.Add((trace, result, passed));
|
||||
}
|
||||
|
||||
return new ReplayBatchResult([.. results]);
|
||||
}
|
||||
|
||||
private static void VerifyExpectation(ReplayResult result, ReplayExpectation expectation)
|
||||
{
|
||||
if (expectation.ShouldSucceed)
|
||||
{
|
||||
result.Success.Should().BeTrue(
|
||||
$"Replay should succeed: {result.FailureReason}");
|
||||
}
|
||||
else
|
||||
{
|
||||
result.Success.Should().BeFalse(
|
||||
$"Replay should fail with: {expectation.ExpectedFailure}");
|
||||
}
|
||||
|
||||
if (expectation.ExpectedOutputHash is not null)
|
||||
{
|
||||
result.OutputHash.Should().Be(expectation.ExpectedOutputHash,
|
||||
"Output hash should match expected");
|
||||
}
|
||||
}
|
||||
|
||||
private static bool VerifyExpectationSafe(ReplayResult result, ReplayExpectation expectation)
|
||||
{
|
||||
try
|
||||
{
|
||||
VerifyExpectation(result, expectation);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual ValueTask DisposeAsync() => ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Expected outcome of a trace replay.
|
||||
/// </summary>
|
||||
/// <param name="ShouldSucceed">Whether replay should succeed.</param>
|
||||
/// <param name="ExpectedFailure">Expected failure reason, if should fail.</param>
|
||||
/// <param name="ExpectedOutputHash">Expected output hash for determinism check.</param>
|
||||
/// <param name="ExpectedWarnings">Expected warnings.</param>
|
||||
public sealed record ReplayExpectation(
|
||||
bool ShouldSucceed,
|
||||
string? ExpectedFailure = null,
|
||||
string? ExpectedOutputHash = null,
|
||||
ImmutableArray<string> ExpectedWarnings = default);
|
||||
|
||||
/// <summary>
|
||||
/// Result of a batch replay operation.
|
||||
/// </summary>
|
||||
/// <param name="Results">Individual trace results.</param>
|
||||
public sealed record ReplayBatchResult(
|
||||
ImmutableArray<(TraceCorpusEntry Trace, ReplayResult Result, bool Passed)> Results)
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the total number of traces replayed.
|
||||
/// </summary>
|
||||
public int TotalCount => Results.Length;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of traces that passed.
|
||||
/// </summary>
|
||||
public int PassedCount => Results.Count(r => r.Passed);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of traces that failed.
|
||||
/// </summary>
|
||||
public int FailedCount => Results.Count(r => !r.Passed);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the pass rate as a decimal (0-1).
|
||||
/// </summary>
|
||||
public decimal PassRate => TotalCount > 0 ? (decimal)PassedCount / TotalCount : 0;
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
// <copyright file="ServiceCollectionExtensions.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under AGPL-3.0-or-later.
|
||||
// </copyright>
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Immutable;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using StellaOps.Replay.Anonymization;
|
||||
using StellaOps.Testing.Temporal;
|
||||
|
||||
namespace StellaOps.Testing.Replay;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for configuring replay testing services.
|
||||
/// </summary>
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Add replay testing services to the service collection.
|
||||
/// </summary>
|
||||
/// <param name="services">The service collection.</param>
|
||||
/// <returns>The service collection for chaining.</returns>
|
||||
public static IServiceCollection AddReplayTesting(this IServiceCollection services)
|
||||
{
|
||||
services.AddSingleton<SimulatedTimeProvider>(sp =>
|
||||
new SimulatedTimeProvider(DateTimeOffset.UtcNow));
|
||||
services.AddSingleton<TimeProvider>(sp =>
|
||||
sp.GetRequiredService<SimulatedTimeProvider>());
|
||||
|
||||
services.AddSingleton<ITraceCorpusManager, InMemoryTraceCorpusManager>();
|
||||
services.AddSingleton<IReplayOrchestrator, DefaultReplayOrchestrator>();
|
||||
services.AddSingleton<ITraceAnonymizer, TraceAnonymizer>();
|
||||
|
||||
services.AddSingleton(typeof(ILogger<>), typeof(NullLogger<>));
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// In-memory implementation of trace corpus manager for testing.
|
||||
/// </summary>
|
||||
internal sealed class InMemoryTraceCorpusManager : ITraceCorpusManager
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, TraceCorpusEntry> _traces = new();
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private int _nextId;
|
||||
|
||||
public InMemoryTraceCorpusManager(TimeProvider timeProvider)
|
||||
{
|
||||
_timeProvider = timeProvider;
|
||||
}
|
||||
|
||||
public Task<TraceCorpusEntry> ImportAsync(
|
||||
AnonymizedTrace trace,
|
||||
TraceClassification classification,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var entryId = $"corpus-{Interlocked.Increment(ref _nextId):D6}";
|
||||
|
||||
var entry = new TraceCorpusEntry(
|
||||
EntryId: entryId,
|
||||
Trace: trace,
|
||||
Classification: classification,
|
||||
ImportedAt: _timeProvider.GetUtcNow(),
|
||||
ExpectedOutputHash: null);
|
||||
|
||||
_traces[entryId] = entry;
|
||||
|
||||
return Task.FromResult(entry);
|
||||
}
|
||||
|
||||
public async IAsyncEnumerable<TraceCorpusEntry> QueryAsync(
|
||||
TraceQuery query,
|
||||
[EnumeratorCancellation] CancellationToken ct = default)
|
||||
{
|
||||
var results = _traces.Values.AsEnumerable();
|
||||
|
||||
if (query.Category is not null)
|
||||
{
|
||||
results = results.Where(e => e.Classification.Category == query.Category);
|
||||
}
|
||||
|
||||
if (query.MinComplexity is not null)
|
||||
{
|
||||
results = results.Where(e => e.Classification.Complexity >= query.MinComplexity);
|
||||
}
|
||||
|
||||
if (!query.RequiredTags.IsDefaultOrEmpty)
|
||||
{
|
||||
results = results.Where(e =>
|
||||
query.RequiredTags.All(t => e.Classification.Tags.Contains(t)));
|
||||
}
|
||||
|
||||
if (query.FailureMode is not null)
|
||||
{
|
||||
results = results.Where(e => e.Classification.FailureMode == query.FailureMode);
|
||||
}
|
||||
|
||||
var limited = results.Take(query.Limit);
|
||||
|
||||
foreach (var entry in limited)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
await Task.Yield();
|
||||
yield return entry;
|
||||
}
|
||||
}
|
||||
|
||||
public Task<TraceCorpusStatistics> GetStatisticsAsync(CancellationToken ct = default)
|
||||
{
|
||||
var entries = _traces.Values.ToList();
|
||||
|
||||
var byCategory = entries
|
||||
.GroupBy(e => e.Classification.Category)
|
||||
.ToImmutableDictionary(g => g.Key, g => g.Count());
|
||||
|
||||
var byComplexity = entries
|
||||
.GroupBy(e => e.Classification.Complexity)
|
||||
.ToImmutableDictionary(g => g.Key, g => g.Count());
|
||||
|
||||
var oldest = entries.Count > 0 ? entries.Min(e => e.ImportedAt) : (DateTimeOffset?)null;
|
||||
var newest = entries.Count > 0 ? entries.Max(e => e.ImportedAt) : (DateTimeOffset?)null;
|
||||
|
||||
return Task.FromResult(new TraceCorpusStatistics(
|
||||
TotalTraces: entries.Count,
|
||||
TracesByCategory: byCategory,
|
||||
TracesByComplexity: byComplexity,
|
||||
OldestTrace: oldest,
|
||||
NewestTrace: newest));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Default implementation of replay orchestrator.
|
||||
/// </summary>
|
||||
internal sealed class DefaultReplayOrchestrator : IReplayOrchestrator
|
||||
{
|
||||
private readonly ILogger<DefaultReplayOrchestrator> _logger;
|
||||
|
||||
public DefaultReplayOrchestrator(ILogger<DefaultReplayOrchestrator> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public Task<ReplayResult> ReplayAsync(
|
||||
AnonymizedTrace trace,
|
||||
SimulatedTimeProvider timeProvider,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var startTime = timeProvider.GetUtcNow();
|
||||
var spanResults = new List<SpanReplayResult>();
|
||||
var warnings = new List<string>();
|
||||
|
||||
foreach (var span in trace.Spans)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
// Simulate span execution
|
||||
timeProvider.Advance(span.Duration);
|
||||
|
||||
var replayDuration = span.Duration; // In simulation, same duration
|
||||
var delta = TimeSpan.Zero;
|
||||
|
||||
spanResults.Add(new SpanReplayResult(
|
||||
SpanId: span.SpanId,
|
||||
Success: true,
|
||||
Duration: replayDuration,
|
||||
DurationDelta: delta,
|
||||
OutputHash: ComputeSpanHash(span)));
|
||||
}
|
||||
|
||||
var endTime = timeProvider.GetUtcNow();
|
||||
var totalDuration = endTime - startTime;
|
||||
|
||||
var outputHash = ComputeOutputHash(spanResults);
|
||||
|
||||
_logger.LogDebug(
|
||||
"Replayed trace {TraceId} with {SpanCount} spans in {Duration}",
|
||||
trace.TraceId, trace.Spans.Length, totalDuration);
|
||||
|
||||
return Task.FromResult(new ReplayResult(
|
||||
Success: true,
|
||||
OutputHash: outputHash,
|
||||
Duration: totalDuration,
|
||||
FailureReason: null,
|
||||
Warnings: [.. warnings],
|
||||
SpanResults: [.. spanResults]));
|
||||
}
|
||||
|
||||
private static string ComputeSpanHash(AnonymizedSpan span)
|
||||
{
|
||||
var input = $"{span.SpanId}:{span.OperationName}:{span.Duration.Ticks}";
|
||||
var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(input));
|
||||
return Convert.ToHexString(bytes).ToLowerInvariant()[..16];
|
||||
}
|
||||
|
||||
private static string ComputeOutputHash(List<SpanReplayResult> results)
|
||||
{
|
||||
var input = string.Join("|", results.Select(r => r.OutputHash));
|
||||
var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(input));
|
||||
return Convert.ToHexString(bytes).ToLowerInvariant();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<OutputType>Exe</OutputType>
|
||||
<UseAppHost>true</UseAppHost>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<IsPackable>true</IsPackable>
|
||||
<Description>Infrastructure for replay-based integration testing using production traces</Description>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<InternalsVisibleTo Include="StellaOps.Testing.Replay.Tests" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
|
||||
<PackageReference Include="xunit.v3.assert" PrivateAssets="all" />
|
||||
<PackageReference Include="xunit.v3.core" PrivateAssets="all" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\StellaOps.Testing.Temporal\StellaOps.Testing.Temporal.csproj" />
|
||||
<ProjectReference Include="..\..\..\Replay\__Libraries\StellaOps.Replay.Anonymization\StellaOps.Replay.Anonymization.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Reference in New Issue
Block a user