acdream/tests/AcDream.App.Tests/World/CreateAuthorityDriftModelContractTests.cs

113 lines
5.1 KiB
C#

using System.Reflection;
using System.Reflection.Emit;
using AcDream.App.Tests.Architecture;
using AcDream.Runtime.Entities;
namespace AcDream.App.Tests.World;
/// <summary>
/// The create-authority drift probes in the expectation-item 6/8 tests
/// (`LiveEntityHydrationControllerTests` and
/// `LiveEntityCreateSupersessionRecoveryTests`) hand-call
/// <see cref="RuntimeEntityRecord.AdvanceCreateAuthority"/> as an honest model
/// of the executor drain's advance: the sole production drain site that
/// advances create authority for an existing incarnation. A nested production
/// OnCreate cannot produce that drift because the initial-residence
/// registration route branches around the ordinary registration-time advance.
/// This compiled contract flags the model as stale if either route changes, so
/// those probes can be re-derived from the new production boundary.
/// </summary>
public sealed class CreateAuthorityDriftModelContractTests
{
private const BindingFlags Declared = BindingFlags.Instance
| BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic
| BindingFlags.DeclaredOnly;
[Fact]
public void HandCalledDriftProbe_StillModelsTheExecutorDrainAdvance()
{
MethodInfo drain = RequiredMethod(
typeof(RuntimeInitialCreateContinuationExecutor),
"ApplyWeenieDescriptionAction");
AssertCallOrder(
drain,
(typeof(RuntimeEntityDirectory),
nameof(RuntimeEntityDirectory.ApplyAcceptedWeenieDescriptionSnapshot)),
(typeof(RuntimeEntityDirectory), nameof(RuntimeEntityDirectory.RefreshSnapshot)),
(typeof(RuntimeEntityDirectory),
nameof(RuntimeEntityDirectory.AdvanceCreateAuthority)),
(typeof(RuntimeInitialCreateResidenceState), "AdvanceExecutorBaseline"));
Assert.Single(
CompiledCallGraph.Read(drain),
call => IsAuthorityAdvance(call.Target));
MethodInfo registration = RequiredMethod(
typeof(RuntimeEntityObjectLifetime),
"RegisterEntityCore");
ParameterInfo residenceFlag = Assert.Single(
registration.GetParameters(),
parameter => parameter.Name == "beginInitialResidence"
&& parameter.ParameterType == typeof(bool));
Assert.NotNull(residenceFlag);
IReadOnlyList<CompiledCall> registrationCalls =
CompiledCallGraph.Read(registration);
CompiledCall refresh = Assert.Single(
registrationCalls,
call => call.Target.DeclaringType == typeof(RuntimeEntityDirectory)
&& call.Target.Name == nameof(RuntimeEntityDirectory.RefreshSnapshot));
CompiledCall ordinaryAdvance = Assert.Single(
registrationCalls,
call => IsAuthorityAdvance(call.Target));
Assert.True(refresh.Offset < ordinaryAdvance.Offset);
Assert.Contains(
CompiledCallGraph.ReadBranches(registration),
branch => branch.OpCode.FlowControl == FlowControl.Cond_Branch
&& branch.Offset > refresh.Offset
&& branch.Offset < ordinaryAdvance.Offset
&& branch.TargetOffset > ordinaryAdvance.Offset);
MethodBase[] productionCallSites =
MethodsCallingRuntimeAuthorityAdvance().ToArray();
Assert.Equal(2, productionCallSites.Length);
Assert.Contains(drain, productionCallSites);
Assert.Contains(registration, productionCallSites);
}
private static IEnumerable<MethodBase> MethodsCallingRuntimeAuthorityAdvance() =>
typeof(RuntimeEntityObjectLifetime).Assembly.GetTypes()
.SelectMany(type => type.GetMethods(Declared)
.Cast<MethodBase>()
.Concat(type.GetConstructors(Declared)))
.Where(method => method.GetMethodBody() is not null)
.Where(method => CompiledCallGraph.Read(method).Any(call =>
IsAuthorityAdvance(call.Target)));
private static bool IsAuthorityAdvance(MethodBase method) =>
method.DeclaringType == typeof(RuntimeEntityDirectory)
&& method.Name == nameof(RuntimeEntityDirectory.AdvanceCreateAuthority);
private static MethodInfo RequiredMethod(Type owner, string name) =>
owner.GetMethod(
name,
BindingFlags.Instance | BindingFlags.Static
| BindingFlags.Public | BindingFlags.NonPublic)
?? throw new MissingMethodException(owner.FullName, name);
private static void AssertCallOrder(
MethodBase method,
params (Type Type, string Method)[] expected)
{
IReadOnlyList<CompiledCall> calls = CompiledCallGraph.Read(method);
int cursor = -1;
foreach ((Type type, string name) in expected)
{
int found = Enumerable.Range(cursor + 1, calls.Count - cursor - 1)
.FirstOrDefault(index => calls[index].Target.DeclaringType == type
&& calls[index].Target.Name == name, -1);
Assert.True(found > cursor,
$"Missing compiled edge after {cursor}: {type.FullName}.{name}.");
cursor = found;
}
}
}