Files
git.stella-ops.org/src/__Tests/__Libraries/StellaOps.Testing.Replay/ReplayIntegrationTestBase.cs
StellaOps Bot 37e11918e0 save progress
2026-01-06 09:42:20 +02:00

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;
}