ModernUO/Projects/Server.Tests/Tests/PropertyList/ObjectPropertyListReentrancyTests.cs
Kamron Batman b6c0a0bde8
docs: tighten comments to the non-obvious why
Cuts explanation that restated the code or repeated itself, and keeps the parts
a future change would otherwise undo.

The same fact was asserted in three places: BanChannel.IsExempt, BanExemptions
and LoginAllowlist each explained that an exemption withholds escalation only.
It now lives on the seam that defines it, and the other two point at it.

The seed short-read comment was eight lines arguing a decision; the load-bearing
part is that waiting costs a connection slot per one- or two-byte client, so
that is what it says now. The new test-collection markers are one line each
rather than a summary of how STArrayPool works internally.

Also drops a fully-qualified System.Func in BanExemptions in favour of a using.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 18:40:02 -07:00

145 lines
4 KiB
C#

using System;
using Xunit;
namespace Server.Tests;
/// <summary>
/// The interpolation buffer is rented by the handler ctor and returned by the closing Add, so every
/// hole is evaluated while it is live. A Reset()/Dispose() landing in that window used to leave the
/// next Append* spanning a null array: ArgumentNullException, parameter "array".
/// </summary>
// Sequential: building a list rents from STArrayPool, which is not thread-safe.
[Collection("Sequential Server Tests")]
public class ObjectPropertyListReentrancyTests
{
// Stands in for a property getter that invalidates while its own tooltip is being built.
private static string ResettingHole(ObjectPropertyList list, string value)
{
list.Reset();
return value;
}
private static string DisposingHole(ObjectPropertyList list, string value)
{
list.Dispose();
return value;
}
[Fact]
public void InterpolatedAdd_ResetMidHole_DoesNotThrow()
{
var opl = new ObjectPropertyList(null);
var ex = Record.Exception(
() => opl.Add(1060776, $"{ResettingHole(opl, "Knight")}\t{"Council of Mages"}")
);
Assert.Null(ex);
}
[Fact]
public void InterpolatedAdd_DisposeMidHole_DoesNotThrow()
{
var opl = new ObjectPropertyList(null);
var ex = Record.Exception(
() => opl.Add(1060776, $"{DisposingHole(opl, "Knight")}\t{"Council of Mages"}")
);
Assert.Null(ex);
}
}
/// <summary>
/// The guard is per-list, so nested builds (a GetProperties override that reads another entity's
/// PropertyList) cannot unguard the outer one the way a single shared slot would.
/// </summary>
[Collection("Sequential Server Tests")]
public class ObjectPropertyListNestedBuildTests
{
[Fact]
public void NestedBuild_DoesNotUnguardTheOuterList()
{
var outer = new ObjectPropertyList(null);
var inner = new ObjectPropertyList(null);
outer.IsBuilding = true;
inner.IsBuilding = true; // another entity starts building, and finishes
inner.IsBuilding = false;
Assert.True(outer.IsBuilding);
}
[Fact]
public void Reset_MidInterpolation_LeavesTheListUsable()
{
var opl = new ObjectPropertyList(null);
opl.Add(1060776, $"{Reset(opl, "Knight")}\t{"Council of Mages"}");
opl.Add(1042971, "still working");
opl.Terminate();
Assert.NotNull(opl.Buffer);
}
private static string Reset(ObjectPropertyList list, string value)
{
list.Reset();
return value;
}
}
/// <summary>
/// Invalidating from inside GetProperties is a defect in the getter, not a case to recover from:
/// DEBUG throws, RELEASE keeps a possibly stale tooltip without crashing or leaking.
/// </summary>
[Collection("Sequential Server Tests")]
public class PropertyListInvalidationDuringBuildTests
{
private class SelfInvalidatingMobile : Mobile
{
public int Builds;
public override void GetProperties(IPropertyList list)
{
Builds++;
base.GetProperties(list);
InvalidateProperties();
list.Add(1060776, $"{"Knight"}\t{"Council of Mages"}");
}
}
private static SelfInvalidatingMobile Place(int x)
{
var m = new SelfInvalidatingMobile();
m.MoveToWorld(new Point3D(x, 1000, 0), Map.Felucca);
return m;
}
[Fact]
public void InvalidatingFromGetProperties_FailsLoudlyWithoutTearingDownTheBuild()
{
var wasEnabled = ObjectPropertyList.Enabled;
ObjectPropertyList.Enabled = true;
try
{
var m = Place(1000);
#if DEBUG
Assert.Throws<InvalidOperationException>(() => _ = m.PropertyList);
#else
Assert.Null(Record.Exception(() => _ = m.PropertyList));
#endif
// Refused, not retried.
Assert.Equal(1, m.Builds);
m.Delete();
}
finally
{
ObjectPropertyList.Enabled = wasEnabled;
}
}
}