188 lines
6.0 KiB
C#
188 lines
6.0 KiB
C#
// <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;
|
|
}
|