using System; using Xunit; namespace Server.Tests; /// /// 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". /// // 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); } } /// /// 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. /// [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; } } /// /// 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. /// [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(() => _ = m.PropertyList); #else Assert.Null(Record.Exception(() => _ = m.PropertyList)); #endif // Refused, not retried. Assert.Equal(1, m.Builds); m.Delete(); } finally { ObjectPropertyList.Enabled = wasEnabled; } } }