acdream/tests/AcDream.Headless.Tests/HeadlessProcessSchedulerTests.cs

280 lines
8.5 KiB
C#

using System.Net;
using AcDream.Core.Net;
using AcDream.Core.Net.Messages;
using AcDream.Headless.Configuration;
using AcDream.Headless.Credentials;
using AcDream.Headless.Diagnostics;
using AcDream.Headless.Hosting;
using AcDream.Headless.Platform;
using AcDream.Runtime;
using AcDream.Runtime.Session;
namespace AcDream.Headless.Tests;
public sealed class HeadlessProcessSchedulerTests
{
[Fact]
public void AbsoluteDeadlinesTickSessionsInStableElapsedTime()
{
var time = new ManualTimeProvider();
var operations = new FixtureSessionOperations();
using HeadlessSessionHost first =
CreateSession("first", time, operations);
using HeadlessSessionHost second =
CreateSession("second", time, operations);
Assert.Equal(
RuntimeSessionStartStatus.Connected,
first.Start().Status);
Assert.Equal(
RuntimeSessionStartStatus.Connected,
second.Start().Status);
var scheduler = new HeadlessProcessScheduler(
[first, second],
time);
Assert.False(scheduler.DispatchDue(time.GetTimestamp()));
time.Advance(TimeSpan.FromMilliseconds(15));
Assert.True(scheduler.DispatchDue(time.GetTimestamp()));
Assert.Equal(1UL, first.Runtime.Clock.FrameNumber);
Assert.Equal(1UL, second.Runtime.Clock.FrameNumber);
Assert.Equal(
0.015d,
first.Runtime.Clock.SimulationTimeSeconds,
precision: 9);
HeadlessSchedulerSnapshot snapshot =
scheduler.CaptureSnapshot();
Assert.Equal(2, snapshot.TurnCount);
Assert.Equal(0, snapshot.CatchUpCollapseCount);
}
[Fact]
public void LongPauseUsesBoundedCatchUpAndPreservesElapsedTime()
{
var time = new ManualTimeProvider();
var operations = new FixtureSessionOperations();
using HeadlessSessionHost session =
CreateSession("catch-up", time, operations);
Assert.Equal(
RuntimeSessionStartStatus.Connected,
session.Start().Status);
var scheduler = new HeadlessProcessScheduler(
[session],
time,
maximumCatchUpTurns: 4);
time.Advance(TimeSpan.FromSeconds(1));
Assert.True(scheduler.DispatchDue(time.GetTimestamp()));
Assert.Equal(4UL, session.Runtime.Clock.FrameNumber);
Assert.Equal(
1d,
session.Runtime.Clock.SimulationTimeSeconds,
precision: 9);
Assert.Equal(
1,
scheduler.CaptureSnapshot().CatchUpCollapseCount);
}
[Fact]
public void ReconnectQuiescenceIsAMonotonicDeadlineNotABlockingWait()
{
var time = new ManualTimeProvider();
var operations = new FixtureSessionOperations();
using HeadlessSessionHost session = CreateSession(
"reconnect",
time,
operations,
TimeSpan.FromMilliseconds(2500));
Assert.Equal(
RuntimeSessionStartStatus.Connected,
session.Start().Status);
var scheduler = new HeadlessProcessScheduler(
[session],
time);
RuntimeSessionStartResult reconnect = session.Reconnect();
Assert.Equal(
RuntimeSessionStartStatus.Deferred,
reconnect.Status);
Assert.True(session.IsReconnectPending);
Assert.Equal(1, operations.CreatedSessionCount);
Assert.Equal(1, operations.DisposedSessionCount);
time.Advance(TimeSpan.FromMilliseconds(2499));
Assert.False(scheduler.DispatchDue(time.GetTimestamp()));
Assert.Equal(1, operations.CreatedSessionCount);
time.Advance(TimeSpan.FromMilliseconds(1));
Assert.True(scheduler.DispatchDue(time.GetTimestamp()));
Assert.False(session.IsReconnectPending);
Assert.Equal(2, operations.CreatedSessionCount);
Assert.True(session.Runtime.Session.IsInWorld);
}
[Fact]
public async Task ProcessHostRunsMultipleSessionsOnOneScheduler()
{
var configuration = new HeadlessConfiguration
{
Version = 1,
Sessions =
[
Descriptor(
"first",
"first-stdin"),
Descriptor(
"second",
"second-stdin"),
],
};
var operations = new FixtureSessionOperations();
using var diagnostics = new StringWriter();
using var host = new HeadlessProcessHost(
configuration,
HeadlessPathSet.Resolve(new HeadlessPathOverrides()),
new StringReader(
"first-password"
+ Environment.NewLine
+ "second-password"
+ Environment.NewLine),
diagnostics,
operations);
using var cancellation = new CancellationTokenSource();
cancellation.Cancel();
HeadlessExitCode result =
await host.RunAsync(cancellation.Token);
Assert.Equal(HeadlessExitCode.Success, result);
Assert.Equal(2, host.Sessions.Count);
Assert.All(
host.Sessions,
session => Assert.True(session.Runtime.Session.IsInWorld));
Assert.Equal(2, operations.CreatedSessionCount);
Assert.DoesNotContain(
"password",
diagnostics.ToString(),
StringComparison.Ordinal);
}
private static HeadlessSessionHost CreateSession(
string id,
TimeProvider time,
ILiveSessionOperations operations,
TimeSpan? reconnectQuiescence = null)
{
var credential = new HeadlessCredentialSecret(
$"{id}-credential",
"password");
try
{
return new HeadlessSessionHost(
Descriptor(id, $"{id}-credential"),
credential,
new HeadlessDiagnosticWriter(TextWriter.Null),
operations,
time,
reconnectQuiescence);
}
catch
{
credential.Dispose();
throw;
}
}
private static HeadlessSessionDescriptor Descriptor(
string id,
string credentialReference) => new()
{
Id = id,
Endpoint = new HeadlessEndpointDescriptor
{
Host = "127.0.0.1",
Port = 9000,
},
Account = $"{id}-account",
Character = new HeadlessCharacterSelector
{
Index = 0,
},
Policy = new HeadlessBotPolicyDescriptor
{
Id = "idle",
},
Credential = new HeadlessCredentialReference
{
Provider = HeadlessCredentialProviderKind.StandardInput,
Reference = credentialReference,
},
};
private sealed class ManualTimeProvider : TimeProvider
{
private long _timestamp;
public override long TimestampFrequency =>
TimeSpan.TicksPerSecond;
public override long GetTimestamp() => _timestamp;
internal void Advance(TimeSpan duration) =>
_timestamp = checked(_timestamp + duration.Ticks);
}
private sealed class FixtureSessionOperations : ILiveSessionOperations
{
public int CreatedSessionCount { get; private set; }
public int DisposedSessionCount { get; private set; }
public IPEndPoint ResolveEndpoint(string host, int port) =>
new(IPAddress.Loopback, port);
public WorldSession CreateSession(IPEndPoint endpoint)
{
CreatedSessionCount++;
return new WorldSession(endpoint);
}
public void Connect(
WorldSession session,
string user,
string password)
{
}
public CharacterList.Parsed GetCharacters(
WorldSession session) =>
new(
0u,
[
new CharacterList.Character(
0x50000001u,
"Headless",
0u),
],
[],
11,
"account",
true,
true);
public void EnterWorld(
WorldSession session,
int activeCharacterIndex)
{
}
public void Tick(WorldSession session)
{
}
public void DisposeSession(WorldSession session)
{
DisposedSessionCount++;
session.Dispose();
}
}
}