280 lines
8.5 KiB
C#
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();
|
|
}
|
|
}
|
|
}
|