feat(headless): share immutable process content
This commit is contained in:
parent
fbdb58a962
commit
12b500d383
11 changed files with 617 additions and 18 deletions
|
|
@ -84,6 +84,46 @@ public sealed class HeadlessEntryPointTests
|
|||
Assert.Equal(string.Empty, error.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidateAcceptsCompleteProcessContentDescriptor()
|
||||
{
|
||||
using var file = TemporaryConfiguration.Create(
|
||||
"""{"version":1,"process":{"content":{"datDirectory":"C:\\AC","preparedAssetPath":"C:\\AC\\acdream.pak"}},"sessions":[]}""");
|
||||
using var output = new StringWriter();
|
||||
using var error = new StringWriter();
|
||||
|
||||
int exitCode = HeadlessEntryPoint.Run(
|
||||
["validate", "--config", file.Path],
|
||||
output,
|
||||
error);
|
||||
|
||||
Assert.Equal(0, exitCode);
|
||||
Assert.Contains("0 session(s)", output.ToString());
|
||||
Assert.Equal(string.Empty, error.ToString());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("""{"version":1,"process":{"content":{"datDirectory":"","preparedAssetPath":"package.pak"}},"sessions":[]}""")]
|
||||
[InlineData("""{"version":1,"process":{"content":{"datDirectory":"dats","preparedAssetPath":""}},"sessions":[]}""")]
|
||||
public void ValidateRejectsIncompleteProcessContentDescriptor(string json)
|
||||
{
|
||||
using var file = TemporaryConfiguration.Create(json);
|
||||
using var output = new StringWriter();
|
||||
using var error = new StringWriter();
|
||||
|
||||
int exitCode = HeadlessEntryPoint.Run(
|
||||
["validate", "--config", file.Path],
|
||||
output,
|
||||
error);
|
||||
|
||||
Assert.Equal(3, exitCode);
|
||||
Assert.Contains(
|
||||
"process.content",
|
||||
error.ToString(),
|
||||
StringComparison.Ordinal);
|
||||
Assert.Equal(string.Empty, output.ToString());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("duplicate-id")]
|
||||
[InlineData("duplicate-credential")]
|
||||
|
|
|
|||
256
tests/AcDream.Headless.Tests/HeadlessProcessContentOwnerTests.cs
Normal file
256
tests/AcDream.Headless.Tests/HeadlessProcessContentOwnerTests.cs
Normal file
|
|
@ -0,0 +1,256 @@
|
|||
using System.Reflection;
|
||||
using AcDream.Content;
|
||||
using AcDream.Headless.Configuration;
|
||||
using AcDream.Headless.Hosting;
|
||||
using AcDream.Headless.Platform;
|
||||
|
||||
namespace AcDream.Headless.Tests;
|
||||
|
||||
public sealed class HeadlessProcessContentOwnerTests
|
||||
{
|
||||
[Fact]
|
||||
public void DisposeRequestRetiresResourcesAfterExactFinalLease()
|
||||
{
|
||||
var factory = new FixtureContentFactory();
|
||||
var owner = new HeadlessProcessContentOwner(
|
||||
ContentDescriptor(),
|
||||
_ => { },
|
||||
factory);
|
||||
using var first = owner.AcquireLease("first");
|
||||
using var second = owner.AcquireLease("second");
|
||||
|
||||
Assert.Equal(1, factory.OpenCount);
|
||||
Assert.Equal(
|
||||
new HeadlessProcessContentSnapshot(
|
||||
LeaseCount: 2,
|
||||
IsDisposeRequested: false,
|
||||
IsDisposed: false,
|
||||
MappedVirtualBytes: 4096L),
|
||||
owner.CaptureSnapshot());
|
||||
|
||||
owner.Dispose();
|
||||
|
||||
Assert.True(owner.CaptureSnapshot().IsDisposeRequested);
|
||||
Assert.False(owner.CaptureSnapshot().IsDisposed);
|
||||
Assert.Throws<ObjectDisposedException>(
|
||||
() => owner.AcquireLease("late"));
|
||||
|
||||
first.Dispose();
|
||||
Assert.Equal(1, owner.CaptureSnapshot().LeaseCount);
|
||||
Assert.Equal(0, factory.Dats.DisposeSuccessCount);
|
||||
Assert.Equal(0, factory.Prepared.DisposeSuccessCount);
|
||||
|
||||
second.Dispose();
|
||||
|
||||
Assert.True(owner.CaptureSnapshot().IsConverged);
|
||||
Assert.Equal(1, factory.Dats.DisposeSuccessCount);
|
||||
Assert.Equal(1, factory.Prepared.DisposeSuccessCount);
|
||||
|
||||
owner.Dispose();
|
||||
first.Dispose();
|
||||
second.Dispose();
|
||||
Assert.Equal(1, factory.Dats.DisposeSuccessCount);
|
||||
Assert.Equal(1, factory.Prepared.DisposeSuccessCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FailedFinalDrainRetainsRetryableOwnerSuffix()
|
||||
{
|
||||
var factory = new FixtureContentFactory();
|
||||
var owner = new HeadlessProcessContentOwner(
|
||||
ContentDescriptor(),
|
||||
_ => { },
|
||||
factory);
|
||||
using var lease = owner.AcquireLease("retry");
|
||||
owner.Dispose();
|
||||
factory.Prepared.FailNextDispose = true;
|
||||
|
||||
Assert.Throws<IOException>(lease.Dispose);
|
||||
|
||||
HeadlessProcessContentSnapshot failed = owner.CaptureSnapshot();
|
||||
Assert.Equal(0, failed.LeaseCount);
|
||||
Assert.True(failed.IsDisposeRequested);
|
||||
Assert.False(failed.IsDisposed);
|
||||
Assert.Equal(0, factory.Dats.DisposeSuccessCount);
|
||||
|
||||
owner.Dispose();
|
||||
|
||||
Assert.True(owner.CaptureSnapshot().IsConverged);
|
||||
Assert.Equal(1, factory.Prepared.DisposeSuccessCount);
|
||||
Assert.Equal(1, factory.Dats.DisposeSuccessCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FiveSessionsShareContentButNotMutableRuntimeOwners()
|
||||
{
|
||||
const int sessionCount = 5;
|
||||
var factory = new FixtureContentFactory();
|
||||
var configuration = new HeadlessConfiguration
|
||||
{
|
||||
Version = 1,
|
||||
Process = new HeadlessProcessSettings
|
||||
{
|
||||
Content = ContentDescriptor(),
|
||||
},
|
||||
Sessions = Enumerable.Range(0, sessionCount)
|
||||
.Select(index => Session(index))
|
||||
.Cast<HeadlessSessionDescriptor?>()
|
||||
.ToList(),
|
||||
};
|
||||
using var input = new StringReader(string.Concat(
|
||||
Enumerable.Repeat(
|
||||
"fixture-password" + Environment.NewLine,
|
||||
sessionCount)));
|
||||
using var host = new HeadlessProcessHost(
|
||||
configuration,
|
||||
HeadlessPathSet.Resolve(new HeadlessPathOverrides()),
|
||||
input,
|
||||
TextWriter.Null,
|
||||
contentFactory: factory);
|
||||
|
||||
Assert.Equal(1, factory.OpenCount);
|
||||
Assert.Equal(sessionCount, host.Sessions.Count);
|
||||
Assert.Equal(sessionCount, host.Content?.LeaseCount);
|
||||
Assert.All(
|
||||
host.Sessions,
|
||||
session =>
|
||||
{
|
||||
Assert.Same(
|
||||
factory.DatsResource,
|
||||
session.Content?.Dats);
|
||||
Assert.Same(
|
||||
factory.PreparedResource,
|
||||
session.Content?.PreparedAssets);
|
||||
});
|
||||
Assert.Equal(
|
||||
sessionCount,
|
||||
host.Sessions
|
||||
.Select(static session => session.Runtime)
|
||||
.Distinct(ReferenceEqualityComparer.Instance)
|
||||
.Count());
|
||||
Assert.Equal(
|
||||
sessionCount,
|
||||
host.Sessions
|
||||
.Select(static session =>
|
||||
session.Runtime.EntityObjects.Physics.Engine)
|
||||
.Distinct(ReferenceEqualityComparer.Instance)
|
||||
.Count());
|
||||
Assert.Equal(
|
||||
sessionCount,
|
||||
host.Sessions
|
||||
.Select(static session =>
|
||||
session.Runtime.EntityObjects.Physics.DataCache)
|
||||
.Distinct(ReferenceEqualityComparer.Instance)
|
||||
.Count());
|
||||
|
||||
HeadlessSessionHost[] sessions = host.Sessions.ToArray();
|
||||
host.Dispose();
|
||||
|
||||
Assert.True(host.Content?.IsConverged);
|
||||
Assert.All(
|
||||
sessions,
|
||||
static session =>
|
||||
Assert.True(
|
||||
session.Runtime.CaptureOwnership().IsConverged));
|
||||
Assert.Equal(1, factory.Prepared.DisposeSuccessCount);
|
||||
Assert.Equal(1, factory.Dats.DisposeSuccessCount);
|
||||
}
|
||||
|
||||
private static HeadlessContentDescriptor ContentDescriptor() => new()
|
||||
{
|
||||
DatDirectory = "fixture-dats",
|
||||
PreparedAssetPath = "fixture.pak",
|
||||
};
|
||||
|
||||
private static HeadlessSessionDescriptor Session(int index) => new()
|
||||
{
|
||||
Id = $"bot-{index}",
|
||||
Endpoint = new HeadlessEndpointDescriptor
|
||||
{
|
||||
Host = "127.0.0.1",
|
||||
Port = 9000,
|
||||
},
|
||||
Account = $"account-{index}",
|
||||
Character = new HeadlessCharacterSelector
|
||||
{
|
||||
Index = 0,
|
||||
},
|
||||
Policy = new HeadlessBotPolicyDescriptor
|
||||
{
|
||||
Id = "idle",
|
||||
},
|
||||
Credential = new HeadlessCredentialReference
|
||||
{
|
||||
Provider = HeadlessCredentialProviderKind.StandardInput,
|
||||
Reference = $"stdin-{index}",
|
||||
},
|
||||
};
|
||||
|
||||
private sealed class FixtureContentFactory
|
||||
: IHeadlessProcessContentFactory
|
||||
{
|
||||
internal FixtureContentFactory()
|
||||
{
|
||||
DatsResource =
|
||||
DispatchProxy.Create<IDatReaderWriter, TestResourceProxy>();
|
||||
PreparedResource =
|
||||
DispatchProxy.Create<ITestPreparedSource, TestResourceProxy>();
|
||||
Dats = TestResourceProxy.For(DatsResource);
|
||||
Prepared = TestResourceProxy.For(PreparedResource);
|
||||
Prepared.MappedVirtualBytes = 4096L;
|
||||
}
|
||||
|
||||
internal int OpenCount { get; private set; }
|
||||
internal IDatReaderWriter DatsResource { get; }
|
||||
internal ITestPreparedSource PreparedResource { get; }
|
||||
internal TestResourceProxy Dats { get; }
|
||||
internal TestResourceProxy Prepared { get; }
|
||||
|
||||
public (IDatReaderWriter Dats, IPreparedAssetSource Prepared) Open(
|
||||
HeadlessContentDescriptor descriptor,
|
||||
Action<string> diagnostic)
|
||||
{
|
||||
OpenCount++;
|
||||
return (DatsResource, PreparedResource);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public interface ITestPreparedSource
|
||||
: IPreparedAssetSource, IPreparedCollisionSource;
|
||||
|
||||
public class TestResourceProxy : DispatchProxy
|
||||
{
|
||||
public int DisposeSuccessCount { get; private set; }
|
||||
public bool FailNextDispose { get; set; }
|
||||
public long MappedVirtualBytes { get; set; }
|
||||
|
||||
public static TestResourceProxy For<T>(T resource)
|
||||
where T : class =>
|
||||
(TestResourceProxy)(object)resource;
|
||||
|
||||
protected override object? Invoke(
|
||||
MethodInfo? targetMethod,
|
||||
object?[]? args)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(targetMethod);
|
||||
if (targetMethod.Name == nameof(IDisposable.Dispose))
|
||||
{
|
||||
if (FailNextDispose)
|
||||
{
|
||||
FailNextDispose = false;
|
||||
throw new IOException("fixture disposal failure");
|
||||
}
|
||||
DisposeSuccessCount++;
|
||||
MappedVirtualBytes = 0L;
|
||||
return null;
|
||||
}
|
||||
if (targetMethod.Name == "get_MappedVirtualBytes")
|
||||
return MappedVirtualBytes;
|
||||
|
||||
Type returnType = targetMethod.ReturnType;
|
||||
return returnType == typeof(void) || !returnType.IsValueType
|
||||
? null
|
||||
: Activator.CreateInstance(returnType);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue