198 lines
7.2 KiB
C#
198 lines
7.2 KiB
C#
using System.Net;
|
|
using System.Net.Http.Json;
|
|
using FluentAssertions;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.Hosting;
|
|
using StackExchange.Redis;
|
|
|
|
[Collection("Integration")]
|
|
public class SimulationRunnerTests : IAsyncLifetime
|
|
{
|
|
private readonly ApiFixture _fixture;
|
|
private ISimulationRunner _runner = null!;
|
|
private TestSimulationClientFactory _clientFactory = null!;
|
|
|
|
public SimulationRunnerTests(ApiFixture fixture) => _fixture = fixture;
|
|
|
|
public async Task InitializeAsync()
|
|
{
|
|
using var scope = _fixture.Services.CreateScope();
|
|
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
|
var redis = scope.ServiceProvider.GetRequiredService<IConnectionMultiplexer>();
|
|
await DbResetHelper.ResetAsync(db);
|
|
await DataSeeder.SeedThresholdsOnlyAsync(db, redis);
|
|
|
|
_runner = _fixture.Services.GetRequiredService<ISimulationRunner>();
|
|
_clientFactory = _fixture.SimulationClientFactory;
|
|
_clientFactory.HangOnCreate = false;
|
|
_clientFactory.FailOnCreate = false;
|
|
|
|
foreach (var run in _runner.ListRuns())
|
|
_runner.Cancel(run.RunId);
|
|
}
|
|
|
|
public Task DisposeAsync()
|
|
{
|
|
_clientFactory.HangOnCreate = false;
|
|
_clientFactory.FailOnCreate = false;
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Start_UnknownScenario_Returns422()
|
|
{
|
|
var act = () => _runner.StartAsync("does-not-exist", 60, "tester", CancellationToken.None);
|
|
|
|
var ex = await act.Should().ThrowAsync<ValidationException>();
|
|
ex.Which.ErrorCode.Should().Be("SIMULATION_SCENARIO_UNKNOWN");
|
|
|
|
using var scope = _fixture.Services.CreateScope();
|
|
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
|
(await db.SimulationRuns.CountAsync()).Should().Be(0);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Start_SpeedAboveMax_Returns422()
|
|
{
|
|
var act = () => _runner.StartAsync("minimal-sim-01", 601, "tester", CancellationToken.None);
|
|
|
|
var ex = await act.Should().ThrowAsync<ValidationException>();
|
|
ex.Which.ErrorCode.Should().Be("SIMULATION_SPEED_INVALID");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Start_AtConcurrencyLimit_Returns409()
|
|
{
|
|
_clientFactory.HangOnCreate = true;
|
|
|
|
await _runner.StartAsync("minimal-sim-01", 60, "tester", CancellationToken.None);
|
|
await _runner.StartAsync("minimal-sim-01", 60, "tester", CancellationToken.None);
|
|
|
|
var act = () => _runner.StartAsync("minimal-sim-01", 60, "tester", CancellationToken.None);
|
|
var ex = await act.Should().ThrowAsync<ConflictException>();
|
|
ex.Which.ErrorCode.Should().Be("SIMULATION_CONCURRENCY_LIMIT");
|
|
|
|
foreach (var run in _runner.ListRuns())
|
|
_runner.Cancel(run.RunId);
|
|
|
|
await WaitForAsync(() => _runner.ListRuns().All(r =>
|
|
r.Status is SimulationRunStatus.Cancelled or SimulationRunStatus.Failed
|
|
or SimulationRunStatus.Completed));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Start_CreatesPatientMarkedSimulated()
|
|
{
|
|
var state = await _runner.StartAsync("minimal-sim-01", 600, "tester", CancellationToken.None);
|
|
var completed = await WaitForRunAsync(state.RunId, SimulationRunStatus.Completed);
|
|
|
|
completed.PatientId.Should().NotBeNull();
|
|
|
|
using var scope = _fixture.Services.CreateScope();
|
|
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
|
var patient = await db.Patients.SingleAsync(p => p.Id == completed.PatientId);
|
|
patient.IsSimulated.Should().BeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Run_ProgressAdvances()
|
|
{
|
|
var state = await _runner.StartAsync("minimal-sim-01", 600, "tester", CancellationToken.None);
|
|
|
|
await WaitForAsync(() =>
|
|
{
|
|
var current = _runner.GetRun(state.RunId);
|
|
return current is not null && current.LastOffsetMinutes > 0;
|
|
});
|
|
|
|
var mid = _runner.GetRun(state.RunId)!;
|
|
mid.LastOffsetMinutes.Should().BeGreaterThan(0);
|
|
|
|
var completed = await WaitForRunAsync(state.RunId, SimulationRunStatus.Completed);
|
|
completed.ProgressPercent.Should().Be(100);
|
|
completed.ObservationsSent.Should().Be(4);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Stop_CancelsRun_StatusCancelled()
|
|
{
|
|
_clientFactory.HangOnCreate = true;
|
|
var state = await _runner.StartAsync("minimal-sim-01", 60, "tester", CancellationToken.None);
|
|
|
|
_runner.Cancel(state.RunId).Should().BeTrue();
|
|
var cancelled = await WaitForRunAsync(state.RunId, SimulationRunStatus.Cancelled);
|
|
cancelled.Status.Should().Be(SimulationRunStatus.Cancelled);
|
|
|
|
using var scope = _fixture.Services.CreateScope();
|
|
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
|
(await db.Observations.CountAsync()).Should().Be(0);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Stop_AlreadyCompleted_IsNoOpSuccess()
|
|
{
|
|
var state = await _runner.StartAsync("minimal-sim-01", 600, "tester", CancellationToken.None);
|
|
await WaitForRunAsync(state.RunId, SimulationRunStatus.Completed);
|
|
|
|
_runner.Cancel(state.RunId).Should().BeTrue();
|
|
var after = _runner.GetRun(state.RunId)!;
|
|
after.Status.Should().Be(SimulationRunStatus.Completed);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Run_Failure_RecordsFailureReason()
|
|
{
|
|
_clientFactory.FailOnCreate = true;
|
|
var state = await _runner.StartAsync("minimal-sim-01", 600, "tester", CancellationToken.None);
|
|
|
|
var failed = await WaitForRunAsync(state.RunId, SimulationRunStatus.Failed);
|
|
failed.FailureReason.Should().NotBeNullOrWhiteSpace();
|
|
failed.FailureReason.Should().Contain("Login failed");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Shutdown_CancelsActiveRuns()
|
|
{
|
|
_clientFactory.HangOnCreate = true;
|
|
var state = await _runner.StartAsync("minimal-sim-01", 60, "tester", CancellationToken.None);
|
|
|
|
var hosted = (IHostedService)_fixture.Services.GetRequiredService<SimulationRunner>();
|
|
await hosted.StopAsync(CancellationToken.None);
|
|
|
|
var after = await WaitForRunAsync(state.RunId, SimulationRunStatus.Cancelled);
|
|
after.Status.Should().Be(SimulationRunStatus.Cancelled);
|
|
}
|
|
|
|
private async Task<SimulationRunState> WaitForRunAsync(
|
|
Guid runId, SimulationRunStatus expected, TimeSpan? timeout = null)
|
|
{
|
|
try
|
|
{
|
|
await WaitForAsync(() => _runner.GetRun(runId)?.Status == expected, timeout);
|
|
}
|
|
catch (TimeoutException)
|
|
{
|
|
var actual = _runner.GetRun(runId);
|
|
throw new TimeoutException(
|
|
$"Expected run {runId} status {expected}, but was {actual?.Status}. " +
|
|
$"FailureReason={actual?.FailureReason}");
|
|
}
|
|
|
|
return _runner.GetRun(runId)!;
|
|
}
|
|
|
|
private static async Task WaitForAsync(Func<bool> condition, TimeSpan? timeout = null)
|
|
{
|
|
var deadline = DateTimeOffset.UtcNow + (timeout ?? TimeSpan.FromSeconds(15));
|
|
while (DateTimeOffset.UtcNow < deadline)
|
|
{
|
|
if (condition())
|
|
return;
|
|
await Task.Delay(25);
|
|
}
|
|
|
|
throw new TimeoutException("Condition was not met within the timeout.");
|
|
}
|
|
}
|