test: fix shared state, CI-vacuous tests and harness duplication (#2722)
Follow-up to #2721. It fixes how the remaining tests behave and changes no production code. ## CI - Both `dotnet test` steps pass `--filter "Category!=Manual"`. Before this, Manual-tagged tests ran on CI anyway. - The one surviving movement-throttle latency test is the only coverage of burst forgiveness over a full, wrapped history. It loses its Manual tag, so it runs on CI. ## Tests that passed on CI without testing anything - **Corpse and throwing tests.** They relied on tiledata for the weapon `Layer`, so on CI the equip silently failed and they asserted against empty lists. They now set `Layer` explicitly and assert the equip took effect. `TestThrown` also pins its own strength and range values, so a shard retuning SoulGlaive no longer breaks them. - **Healthbar poison rows.** Server.Tests registers no poisons, so every row tested `null`. They now use real poison levels. - **Account packet tests.** The mock accounts reported `Length` 0, so the character-list tests only ever encoded empty slots. They now encode the real characters. - **Pathing and AI tests that only mean something on real Trammel/Felucca terrain** are now guarded with `TileDataRequirement.SkipIfMissing()`: Britain desk and clutter scenes, house footprints (no MultiData on CI) and fingerprints. Tests that are valid on flat blank terrain stay unguarded. (A synthetic tiledata fixture to run these on CI is a planned follow-up.) - **Assertions that couldn't fail now check their claim.** Affected tests include the `DefaultWalker` cache use, cache on/off parity requiring a path, the recorder capability flags, the idle-pet gate, the monthly DST recurrence, the AdvancedSearch `Record.Exception` wrappers, `CanFitItem` with no branch-guarded assert, the Firewall callback, and `Import_NoMap`. The Geometry Parse asserts also now use the same input as their TryParse. ## Shared state Every test that changes process-wide state now restores it, even when an assert fails: - **The clock.** A new `TimerWheelScope` saves and restores `Core._tickCount`, `Core._now` and the timer wheel. `SimulatedClockScope` covers decay tests. - **StepCache settings and caches**, via `StepCacheStateScope`. - **Network state.** The ManualAllowlist, LoginAllowlist, firewall and BanChannel reporters are restored. Global packet handlers are now registered per test and removed afterwards, so they no longer stay registered for later tests. - **Data and config tables.** `ArchiveJournal` statics, `TileData.ItemTable`, `ObjectPropertyList.Enabled`, `Core.Expansion` and the localization table are restored. One `MobileStatusVersion` restore was resetting a different expansion than the one the test changed. - **Entities.** Tests with fixed serials now delete what they create. Fixed mobile serials moved to `World.NewMobile`, because deleting a hard-coded serial could unregister a live entity. - **Collection race.** `SpiralScanTests` no longer clears the spawn cache from a separate collection that could run in parallel with the cache-manager tests. - **Shared statics.** Classes reading shared static allowlists joined the sequential collection. ## Duplication New shared helpers replace copy-pasted harness code: - **Server.Tests:** `PacketAssert.Sends`, used by about 130 packet tests; `TestClientHarness`, `PersistencePipelineScope`, `NetTestHelpers`, `EntityCleanup` and one `TestMulti`. - **UOContent.Tests:** `TimerWheelScope`, which also holds one wraparound-safe `RunFor`/`RunUntil`; `AiTestStubs`, which merges six walker copies and five identical creature stubs; `MobileTestHarness`, `CommandTestHarness` and `TempDirectory`. Dead oracle packet classes are removed. ## Cost | Test | Before | After | |---|---|---| | `VirtualSerialTests` | 1.57s | 2ms (starts near the wrap via reflection instead of drawing ~17.9M serials) | | Event scheduler recurrence loop | 473ms | 1ms | | CrowdSec drain | 517ms | 16ms (event-driven instead of a fixed 500ms wait) | | AuthId tests | 248ms | 99ms (SHA2 except the two GameLogin tests) | | Container cap tests | 5,000 items | 3,449 / 3,277 (one past each protocol cap) | | Bulk removal | 100k children | 20k children | | OPL hash distribution | 60k builds | 10k builds | ## Shard-tuning decoupling - AI assertions read `ReacquireDelay` and `FollowMoveSpeed` instead of hard-coding production tuning. - The guard friendly-fire test uses an `AI_Mage` stub instead of WhiteWyrm. - Argon2 assertions pin the `argon2id` variant and `v=19` but read the cost parameters from the configured hasher. ## Tests | Run | Server.Tests | UOContent.Tests | |---|---|---| | With client files | 812 pass | 1,123 pass | | Without client files (CI simulation) | 800 pass, 12 skip | 1,021 pass, 102 skip | - The UOContent fixture reads `MODERNUO_TEST_DATA_DIR`; Server.Tests reads `MODERNUO_CLIENT_PATH`. - The 15 new UOContent skips are the map-dependent guards above. - Total run time with client files is about 5s, down from about 8s.
This commit is contained in:
parent
79f736aa83
commit
00134cac33
135 changed files with 3164 additions and 4020 deletions
|
|
@ -6,32 +6,43 @@ namespace Server.Tests;
|
|||
[Collection("Sequential Server Tests")]
|
||||
public class ContainerBulkRemovalTests
|
||||
{
|
||||
// Large enough that a quadratic child removal (a front-to-back IndexOf per child) costs
|
||||
// roughly 2 * 10^8 comparisons and shows up in the test's run time.
|
||||
private const int ChildCount = 20_000;
|
||||
|
||||
[Fact]
|
||||
public void DeletingLargeContainer_DeletesAllChildren_AndTotalsStayConsistent()
|
||||
{
|
||||
var outer = new Container(0xE75);
|
||||
var pack = new Container(0xE75);
|
||||
outer.DropItem(pack);
|
||||
|
||||
var children = new Item[100_000];
|
||||
for (var i = 0; i < children.Length; i++)
|
||||
try
|
||||
{
|
||||
children[i] = new Item(0x1234);
|
||||
pack.DropItem(children[i]);
|
||||
var pack = new Container(0xE75);
|
||||
outer.DropItem(pack);
|
||||
|
||||
var children = new Item[ChildCount];
|
||||
for (var i = 0; i < children.Length; i++)
|
||||
{
|
||||
children[i] = new Item(0x1234);
|
||||
pack.DropItem(children[i]);
|
||||
}
|
||||
|
||||
Assert.Equal(children.Length + 1, outer.TotalItems);
|
||||
|
||||
pack.Delete();
|
||||
|
||||
for (var i = 0; i < children.Length; i++)
|
||||
{
|
||||
Assert.True(children[i].Deleted);
|
||||
}
|
||||
|
||||
Assert.Empty(outer.Items);
|
||||
Assert.Equal(0, outer.TotalItems);
|
||||
}
|
||||
|
||||
Assert.Equal(children.Length + 1, outer.TotalItems);
|
||||
|
||||
pack.Delete();
|
||||
|
||||
for (var i = 0; i < children.Length; i++)
|
||||
finally
|
||||
{
|
||||
Assert.True(children[i].Deleted);
|
||||
outer.Delete();
|
||||
}
|
||||
|
||||
Assert.Empty(outer.Items);
|
||||
Assert.Equal(0, outer.TotalItems);
|
||||
outer.Delete();
|
||||
}
|
||||
|
||||
// The tail fast path must not change which entry is removed when the item is elsewhere.
|
||||
|
|
@ -39,20 +50,27 @@ public class ContainerBulkRemovalTests
|
|||
public void RemovingFromFrontMiddleAndEnd_RemovesExactlyThatItem()
|
||||
{
|
||||
var pack = new Container(0xE75);
|
||||
var items = new Item[5];
|
||||
for (var i = 0; i < items.Length; i++)
|
||||
|
||||
try
|
||||
{
|
||||
items[i] = new Item(0x1234);
|
||||
pack.DropItem(items[i]);
|
||||
var items = new Item[5];
|
||||
for (var i = 0; i < items.Length; i++)
|
||||
{
|
||||
items[i] = new Item(0x1234);
|
||||
pack.DropItem(items[i]);
|
||||
}
|
||||
|
||||
items[0].Delete();
|
||||
items[2].Delete();
|
||||
items[4].Delete();
|
||||
|
||||
Assert.Equal(2, pack.Items.Count);
|
||||
Assert.Same(items[1], pack.Items[0]);
|
||||
Assert.Same(items[3], pack.Items[1]);
|
||||
}
|
||||
finally
|
||||
{
|
||||
pack.Delete();
|
||||
}
|
||||
|
||||
items[0].Delete();
|
||||
items[2].Delete();
|
||||
items[4].Delete();
|
||||
|
||||
Assert.Equal(2, pack.Items.Count);
|
||||
Assert.Same(items[1], pack.Items[0]);
|
||||
Assert.Same(items[3], pack.Items[1]);
|
||||
pack.Delete();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
using System;
|
||||
using Server.Items;
|
||||
using Server.Network;
|
||||
using Server.Tests.Network;
|
||||
using Xunit;
|
||||
using static Server.Tests.TestClientHarness;
|
||||
|
||||
namespace Server.Tests;
|
||||
|
||||
|
|
@ -44,19 +43,6 @@ public class ContainerChildRemovalTests
|
|||
|
||||
private static readonly Point3D _ownerLoc = new(1500, 1500, 0);
|
||||
|
||||
private static (NetState, Mobile) CreateClient(Point3D location)
|
||||
{
|
||||
var ns = PacketTestUtilities.CreateTestNetState();
|
||||
ns.Account = new MockAccount();
|
||||
|
||||
var mobile = new Mobile(World.NewMobile);
|
||||
mobile.DefaultMobileInit();
|
||||
ns.Mobile = mobile;
|
||||
mobile.NetState = ns;
|
||||
mobile.MoveToWorld(location, Map.Felucca);
|
||||
return (ns, mobile);
|
||||
}
|
||||
|
||||
private static Mobile CreateOwnerWith(Container pack)
|
||||
{
|
||||
var owner = new Mobile(World.NewMobile);
|
||||
|
|
@ -68,44 +54,6 @@ public class ContainerChildRemovalTests
|
|||
return owner;
|
||||
}
|
||||
|
||||
private static bool ReceivedRemove(NetState ns, Serial serial)
|
||||
{
|
||||
Span<byte> expected = stackalloc byte[OutgoingEntityPackets.RemoveEntityLength];
|
||||
OutgoingEntityPackets.CreateRemoveEntity(expected, serial);
|
||||
return ns.SendBuffer.GetReadSpan().IndexOf(expected) >= 0;
|
||||
}
|
||||
|
||||
private static int CountRemoves(NetState ns, Serial serial)
|
||||
{
|
||||
Span<byte> expected = stackalloc byte[OutgoingEntityPackets.RemoveEntityLength];
|
||||
OutgoingEntityPackets.CreateRemoveEntity(expected, serial);
|
||||
|
||||
var span = ns.SendBuffer.GetReadSpan();
|
||||
var count = 0;
|
||||
|
||||
while (true)
|
||||
{
|
||||
var index = span.IndexOf(expected);
|
||||
|
||||
if (index < 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
count++;
|
||||
span = span[(index + expected.Length)..];
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
private static void DisposeClient(NetState ns, Mobile mobile)
|
||||
{
|
||||
ns.Mobile = null;
|
||||
ns.Dispose();
|
||||
mobile.Delete();
|
||||
}
|
||||
|
||||
private static void Cleanup(NetState ns, Mobile client, Mobile owner)
|
||||
{
|
||||
DisposeClient(ns, client);
|
||||
|
|
|
|||
|
|
@ -5,11 +5,15 @@ using Xunit;
|
|||
namespace Server.Tests;
|
||||
|
||||
[Collection("Sequential Server Tests")]
|
||||
public class DecayRegistrationTests
|
||||
public class DecayRegistrationTests : IDisposable
|
||||
{
|
||||
private readonly EntityCleanup _cleanup = new();
|
||||
|
||||
// The wheel is static and these tests move the clock, so start each from a known schedule.
|
||||
public DecayRegistrationTests() => DecayScheduler.ResetForTests();
|
||||
|
||||
public void Dispose() => _cleanup.Dispose();
|
||||
|
||||
// Stands in for a region that refuses decay, e.g. HouseRegion over a locked down item.
|
||||
private class RefusesDecayItem : Item
|
||||
{
|
||||
|
|
@ -20,61 +24,34 @@ public class DecayRegistrationTests
|
|||
public override bool OnDecay() => false;
|
||||
}
|
||||
|
||||
// The item is already dequeued, so a refusal that does not reschedule strands it forever.
|
||||
// Drives the scheduler and the game clock without depending on the shared timer wheel.
|
||||
private static void AdvanceDecay(DateTime start, TimeSpan duration, Item item)
|
||||
{
|
||||
var deadline = start + duration;
|
||||
|
||||
for (var now = start; now <= deadline && !item.Deleted; now += TimeSpan.FromMilliseconds(256))
|
||||
{
|
||||
Core._now = now;
|
||||
DecayScheduler.ProcessTick(now);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ItemOnGround_ActuallyDecaysAfterDecayTime()
|
||||
{
|
||||
var start = Core._now;
|
||||
using var clock = new SimulatedClockScope();
|
||||
|
||||
try
|
||||
{
|
||||
var item = new Item(0x1234);
|
||||
item.MoveToWorld(new Point3D(100, 100, 0), Map.Felucca);
|
||||
var item = _cleanup.Track(new Item(0x1234));
|
||||
item.MoveToWorld(new Point3D(100, 100, 0), Map.Felucca);
|
||||
|
||||
Assert.True(DecayScheduler.IsRegistered(item));
|
||||
Assert.True(DecayScheduler.IsRegistered(item));
|
||||
|
||||
AdvanceDecay(start, item.DecayTime + TimeSpan.FromMinutes(2), item);
|
||||
clock.AdvanceDecay(item.DecayTime + TimeSpan.FromMinutes(2), item);
|
||||
|
||||
Assert.True(item.Deleted, "Item on the ground must decay once DecayTime elapses.");
|
||||
}
|
||||
finally
|
||||
{
|
||||
Core._now = start;
|
||||
}
|
||||
Assert.True(item.Deleted, "Item on the ground must decay once DecayTime elapses.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ItemOnGround_DoesNotDecayBeforeDecayTime()
|
||||
{
|
||||
var start = Core._now;
|
||||
using var clock = new SimulatedClockScope();
|
||||
|
||||
try
|
||||
{
|
||||
var item = new Item(0x1234);
|
||||
item.MoveToWorld(new Point3D(102, 100, 0), Map.Felucca);
|
||||
var item = _cleanup.Track(new Item(0x1234));
|
||||
item.MoveToWorld(new Point3D(102, 100, 0), Map.Felucca);
|
||||
|
||||
AdvanceDecay(start, item.DecayTime - TimeSpan.FromMinutes(2), item);
|
||||
clock.AdvanceDecay(item.DecayTime - TimeSpan.FromMinutes(2), item);
|
||||
|
||||
Assert.False(item.Deleted, "Item must not decay before DecayTime elapses.");
|
||||
Assert.False(item.Deleted, "Item must not decay before DecayTime elapses.");
|
||||
|
||||
item.Delete();
|
||||
}
|
||||
finally
|
||||
{
|
||||
Core._now = start;
|
||||
}
|
||||
item.Delete();
|
||||
}
|
||||
|
||||
// The real client path: Mobile.Lift internalizes, then Mobile.Drop calls MoveToWorld.
|
||||
|
|
@ -84,7 +61,7 @@ public class DecayRegistrationTests
|
|||
[Fact]
|
||||
public void DecaySlot_AgreesWithStructures_AcrossLifecycle()
|
||||
{
|
||||
var item = new Item(0x1234);
|
||||
var item = _cleanup.Track(new Item(0x1234));
|
||||
|
||||
item.MoveToWorld(new Point3D(103, 100, 0), Map.Felucca);
|
||||
Assert.NotEqual(DecayScheduler.SlotNone, item.DecaySlot);
|
||||
|
|
@ -108,10 +85,10 @@ public class DecayRegistrationTests
|
|||
[Fact]
|
||||
public void ContainerDestroy_ContentsDroppedToGroundAreTracked()
|
||||
{
|
||||
var pack = new Container(0xE75);
|
||||
var pack = _cleanup.Track(new Container(0xE75));
|
||||
pack.MoveToWorld(new Point3D(105, 100, 0), Map.Felucca);
|
||||
|
||||
var item = new Item(0x1234);
|
||||
var item = _cleanup.Track(new Item(0x1234));
|
||||
pack.AddItem(item);
|
||||
Assert.False(DecayScheduler.IsRegistered(item));
|
||||
|
||||
|
|
@ -128,163 +105,127 @@ public class DecayRegistrationTests
|
|||
[Fact]
|
||||
public void StaleImmovableItemMadeMovable_GetsAFreshDecayWindow()
|
||||
{
|
||||
var start = Core._now;
|
||||
using var clock = new SimulatedClockScope();
|
||||
|
||||
try
|
||||
{
|
||||
var item = new Item(0x1234);
|
||||
item.MoveToWorld(new Point3D(107, 100, 0), Map.Felucca);
|
||||
var item = _cleanup.Track(new Item(0x1234));
|
||||
item.MoveToWorld(new Point3D(107, 100, 0), Map.Felucca);
|
||||
|
||||
item.Movable = false;
|
||||
Assert.False(DecayScheduler.IsRegistered(item), "A frozen item must not be tracked for decay.");
|
||||
item.Movable = false;
|
||||
Assert.False(DecayScheduler.IsRegistered(item), "A frozen item must not be tracked for decay.");
|
||||
|
||||
Core._now = start + TimeSpan.FromDays(30);
|
||||
var flipped = Core._now;
|
||||
clock.Now = clock.Start + TimeSpan.FromDays(30);
|
||||
|
||||
item.Movable = true;
|
||||
item.Movable = true;
|
||||
|
||||
Assert.True(DecayScheduler.IsRegistered(item), "An unfrozen item must be tracked for decay.");
|
||||
Assert.True(DecayScheduler.IsRegistered(item), "An unfrozen item must be tracked for decay.");
|
||||
|
||||
AdvanceDecay(flipped, item.DecayTime - TimeSpan.FromMinutes(2), item);
|
||||
Assert.False(item.Deleted, "An unfrozen item must get a full decay window, not vanish immediately.");
|
||||
clock.AdvanceDecay(item.DecayTime - TimeSpan.FromMinutes(2), item);
|
||||
Assert.False(item.Deleted, "An unfrozen item must get a full decay window, not vanish immediately.");
|
||||
|
||||
AdvanceDecay(Core._now, TimeSpan.FromMinutes(4), item);
|
||||
Assert.True(item.Deleted, "An unfrozen item must still decay once the fresh window elapses.");
|
||||
}
|
||||
finally
|
||||
{
|
||||
Core._now = start;
|
||||
}
|
||||
clock.AdvanceDecay(TimeSpan.FromMinutes(4), item);
|
||||
Assert.True(item.Deleted, "An unfrozen item must still decay once the fresh window elapses.");
|
||||
}
|
||||
|
||||
// Same transition through the Visible setter: unhiding a long-hidden item.
|
||||
[Fact]
|
||||
public void StaleHiddenItemMadeVisible_GetsAFreshDecayWindow()
|
||||
{
|
||||
var start = Core._now;
|
||||
using var clock = new SimulatedClockScope();
|
||||
|
||||
try
|
||||
{
|
||||
var item = new Item(0x1234);
|
||||
item.MoveToWorld(new Point3D(109, 100, 0), Map.Felucca);
|
||||
var item = _cleanup.Track(new Item(0x1234));
|
||||
item.MoveToWorld(new Point3D(109, 100, 0), Map.Felucca);
|
||||
|
||||
item.Visible = false;
|
||||
Assert.False(DecayScheduler.IsRegistered(item), "A hidden item must not be tracked for decay.");
|
||||
item.Visible = false;
|
||||
Assert.False(DecayScheduler.IsRegistered(item), "A hidden item must not be tracked for decay.");
|
||||
|
||||
Core._now = start + TimeSpan.FromDays(30);
|
||||
var flipped = Core._now;
|
||||
clock.Now = clock.Start + TimeSpan.FromDays(30);
|
||||
|
||||
item.Visible = true;
|
||||
item.Visible = true;
|
||||
|
||||
Assert.True(DecayScheduler.IsRegistered(item), "An unhidden item must be tracked for decay.");
|
||||
Assert.True(DecayScheduler.IsRegistered(item), "An unhidden item must be tracked for decay.");
|
||||
|
||||
AdvanceDecay(flipped, item.DecayTime - TimeSpan.FromMinutes(2), item);
|
||||
Assert.False(item.Deleted, "An unhidden item must get a full decay window, not vanish immediately.");
|
||||
clock.AdvanceDecay(item.DecayTime - TimeSpan.FromMinutes(2), item);
|
||||
Assert.False(item.Deleted, "An unhidden item must get a full decay window, not vanish immediately.");
|
||||
|
||||
AdvanceDecay(Core._now, TimeSpan.FromMinutes(4), item);
|
||||
Assert.True(item.Deleted, "An unhidden item must still decay once the fresh window elapses.");
|
||||
}
|
||||
finally
|
||||
{
|
||||
Core._now = start;
|
||||
}
|
||||
clock.AdvanceDecay(TimeSpan.FromMinutes(4), item);
|
||||
Assert.True(item.Deleted, "An unhidden item must still decay once the fresh window elapses.");
|
||||
}
|
||||
|
||||
// A refusal restarts the countdown without rewriting LastMoved.
|
||||
// A refusal restarts the countdown without rewriting LastMoved. The item is already dequeued
|
||||
// when OnDecay refuses, so a refusal that does not reschedule strands it forever.
|
||||
[Fact]
|
||||
public void RefusedDecay_DoesNotRewriteLastMoved()
|
||||
{
|
||||
var start = Core._now;
|
||||
using var clock = new SimulatedClockScope();
|
||||
|
||||
try
|
||||
{
|
||||
var item = new RefusesDecayItem();
|
||||
item.MoveToWorld(new Point3D(110, 100, 0), Map.Felucca);
|
||||
var lastMoved = item.LastMoved;
|
||||
var item = _cleanup.Track(new RefusesDecayItem());
|
||||
item.MoveToWorld(new Point3D(110, 100, 0), Map.Felucca);
|
||||
var lastMoved = item.LastMoved;
|
||||
|
||||
AdvanceDecay(start, item.DecayTime + TimeSpan.FromMinutes(2), item);
|
||||
clock.AdvanceDecay(item.DecayTime + TimeSpan.FromMinutes(2), item);
|
||||
|
||||
Assert.False(item.Deleted, "A refused decay must not delete the item.");
|
||||
Assert.True(DecayScheduler.IsRegistered(item), "A refused decay must leave the item tracked.");
|
||||
Assert.Equal(lastMoved, item.LastMoved);
|
||||
Assert.False(item.Deleted, "A refused decay must not delete the item.");
|
||||
Assert.True(DecayScheduler.IsRegistered(item), "A refused decay must leave the item tracked.");
|
||||
Assert.Equal(lastMoved, item.LastMoved);
|
||||
|
||||
item.Delete();
|
||||
}
|
||||
finally
|
||||
{
|
||||
Core._now = start;
|
||||
}
|
||||
item.Delete();
|
||||
}
|
||||
|
||||
// The fresh window must survive a save/load cycle, or a restart mid-window deletes the item.
|
||||
[Fact]
|
||||
public void FreshDecayWindow_SurvivesSerialization()
|
||||
{
|
||||
var start = Core._now;
|
||||
using var clock = new SimulatedClockScope();
|
||||
|
||||
try
|
||||
{
|
||||
var item = new Item(0x1234);
|
||||
item.MoveToWorld(new Point3D(111, 100, 0), Map.Felucca);
|
||||
var item = _cleanup.Track(new Item(0x1234));
|
||||
item.MoveToWorld(new Point3D(111, 100, 0), Map.Felucca);
|
||||
|
||||
item.Movable = false;
|
||||
Core._now = start + TimeSpan.FromDays(30);
|
||||
item.Movable = true;
|
||||
item.Movable = false;
|
||||
clock.Now = clock.Start + TimeSpan.FromDays(30);
|
||||
item.Movable = true;
|
||||
|
||||
var expected = item.ScheduledDecayTime;
|
||||
var expected = item.ScheduledDecayTime;
|
||||
|
||||
var writer = new BufferWriter(new byte[512], true);
|
||||
item.Serialize(writer);
|
||||
var writer = new BufferWriter(new byte[512], true);
|
||||
item.Serialize(writer);
|
||||
|
||||
var copy = new Item(item.Serial);
|
||||
copy.Deserialize(new BufferReader(writer.Buffer));
|
||||
var copy = new Item(item.Serial);
|
||||
copy.Deserialize(new BufferReader(writer.Buffer));
|
||||
|
||||
// The stamp is stored as a delta, so it ages only by the real time between
|
||||
// write and read - milliseconds here, the downtime in production.
|
||||
Assert.True(
|
||||
(copy.ScheduledDecayTime - expected).Duration() <= TimeSpan.FromSeconds(5),
|
||||
"The restarted decay window must survive a save/load cycle."
|
||||
);
|
||||
// The stamp is stored as a delta, so it ages only by the real time between
|
||||
// write and read - milliseconds here, the downtime in production.
|
||||
Assert.True(
|
||||
(copy.ScheduledDecayTime - expected).Duration() <= TimeSpan.FromSeconds(5),
|
||||
"The restarted decay window must survive a save/load cycle."
|
||||
);
|
||||
|
||||
item.Delete();
|
||||
copy.Delete();
|
||||
}
|
||||
finally
|
||||
{
|
||||
Core._now = start;
|
||||
}
|
||||
item.Delete();
|
||||
copy.Delete();
|
||||
}
|
||||
|
||||
// A real move supersedes the reset stamp; it must be dropped so the CompactInfo can collapse.
|
||||
[Fact]
|
||||
public void MovingAnItem_ClearsASupersededDecayResetStamp()
|
||||
{
|
||||
var start = Core._now;
|
||||
using var clock = new SimulatedClockScope();
|
||||
|
||||
try
|
||||
{
|
||||
var item = new Item(0x1234);
|
||||
item.MoveToWorld(new Point3D(112, 100, 0), Map.Felucca);
|
||||
var item = _cleanup.Track(new Item(0x1234));
|
||||
item.MoveToWorld(new Point3D(112, 100, 0), Map.Felucca);
|
||||
|
||||
item.Movable = false;
|
||||
Core._now = start + TimeSpan.FromDays(30);
|
||||
item.Movable = true;
|
||||
item.Movable = false;
|
||||
clock.Now = clock.Start + TimeSpan.FromDays(30);
|
||||
item.Movable = true;
|
||||
|
||||
Assert.NotEqual(default, item.DecayResetTime);
|
||||
Assert.NotEqual(default, item.DecayResetTime);
|
||||
|
||||
Core._now += TimeSpan.FromMinutes(1);
|
||||
item.MoveToWorld(new Point3D(113, 100, 0), Map.Felucca);
|
||||
clock.Now += TimeSpan.FromMinutes(1);
|
||||
item.MoveToWorld(new Point3D(113, 100, 0), Map.Felucca);
|
||||
|
||||
Assert.Equal(default, item.DecayResetTime);
|
||||
Assert.Equal(item.LastMoved + item.DecayTime, item.ScheduledDecayTime);
|
||||
Assert.True(DecayScheduler.IsRegistered(item));
|
||||
Assert.Equal(default, item.DecayResetTime);
|
||||
Assert.Equal(item.LastMoved + item.DecayTime, item.ScheduledDecayTime);
|
||||
Assert.True(DecayScheduler.IsRegistered(item));
|
||||
|
||||
item.Delete();
|
||||
}
|
||||
finally
|
||||
{
|
||||
Core._now = start;
|
||||
}
|
||||
item.Delete();
|
||||
}
|
||||
|
||||
// Losing decay eligibility makes the stamp meaningless; it must be dropped so the
|
||||
|
|
@ -292,29 +233,22 @@ public class DecayRegistrationTests
|
|||
[Fact]
|
||||
public void ItemBecomingIneligible_DropsTheDecayResetStamp()
|
||||
{
|
||||
var start = Core._now;
|
||||
using var clock = new SimulatedClockScope();
|
||||
|
||||
try
|
||||
{
|
||||
var item = new Item(0x1234);
|
||||
item.MoveToWorld(new Point3D(115, 100, 0), Map.Felucca);
|
||||
var item = _cleanup.Track(new Item(0x1234));
|
||||
item.MoveToWorld(new Point3D(115, 100, 0), Map.Felucca);
|
||||
|
||||
item.Movable = false;
|
||||
Core._now = start + TimeSpan.FromDays(30);
|
||||
item.Movable = true;
|
||||
item.Movable = false;
|
||||
clock.Now = clock.Start + TimeSpan.FromDays(30);
|
||||
item.Movable = true;
|
||||
|
||||
Assert.NotEqual(default, item.DecayResetTime);
|
||||
Assert.NotEqual(default, item.DecayResetTime);
|
||||
|
||||
item.Movable = false;
|
||||
item.Movable = false;
|
||||
|
||||
Assert.Equal(default, item.DecayResetTime);
|
||||
Assert.Equal(default, item.DecayResetTime);
|
||||
|
||||
item.Delete();
|
||||
}
|
||||
finally
|
||||
{
|
||||
Core._now = start;
|
||||
}
|
||||
item.Delete();
|
||||
}
|
||||
|
||||
// Moving a stamped item into a container programmatically (no drop, no SetLastMoved)
|
||||
|
|
@ -322,32 +256,25 @@ public class DecayRegistrationTests
|
|||
[Fact]
|
||||
public void StampedItemAddedToContainer_DropsTheDecayResetStamp()
|
||||
{
|
||||
var start = Core._now;
|
||||
using var clock = new SimulatedClockScope();
|
||||
|
||||
try
|
||||
{
|
||||
var pack = new Container(0xE75);
|
||||
pack.MoveToWorld(new Point3D(116, 100, 0), Map.Felucca);
|
||||
var pack = _cleanup.Track(new Container(0xE75));
|
||||
pack.MoveToWorld(new Point3D(116, 100, 0), Map.Felucca);
|
||||
|
||||
var item = new Item(0x1234);
|
||||
item.MoveToWorld(new Point3D(117, 100, 0), Map.Felucca);
|
||||
var item = _cleanup.Track(new Item(0x1234));
|
||||
item.MoveToWorld(new Point3D(117, 100, 0), Map.Felucca);
|
||||
|
||||
item.Movable = false;
|
||||
Core._now = start + TimeSpan.FromDays(30);
|
||||
item.Movable = true;
|
||||
item.Movable = false;
|
||||
clock.Now = clock.Start + TimeSpan.FromDays(30);
|
||||
item.Movable = true;
|
||||
|
||||
Assert.NotEqual(default, item.DecayResetTime);
|
||||
Assert.NotEqual(default, item.DecayResetTime);
|
||||
|
||||
pack.AddItem(item);
|
||||
pack.AddItem(item);
|
||||
|
||||
Assert.Equal(default, item.DecayResetTime);
|
||||
Assert.Equal(default, item.DecayResetTime);
|
||||
|
||||
pack.Delete();
|
||||
}
|
||||
finally
|
||||
{
|
||||
Core._now = start;
|
||||
}
|
||||
pack.Delete();
|
||||
}
|
||||
|
||||
// A raw Map assignment (e.g. a GM changing Map through props) is a move: it must
|
||||
|
|
@ -355,7 +282,7 @@ public class DecayRegistrationTests
|
|||
[Fact]
|
||||
public void ItemMovedToRealMapViaMapSetter_IsRegisteredForDecay()
|
||||
{
|
||||
var item = new Item(0x1234);
|
||||
var item = _cleanup.Track(new Item(0x1234));
|
||||
Assert.False(DecayScheduler.IsRegistered(item));
|
||||
|
||||
item.Map = Map.Felucca;
|
||||
|
|
@ -371,10 +298,10 @@ public class DecayRegistrationTests
|
|||
[Fact]
|
||||
public void PartialLiftOfGroundStack_LeavesRemainderRegisteredForDecay()
|
||||
{
|
||||
var stack = new Item(0x1234) { Stackable = true, Amount = 10 };
|
||||
var stack = _cleanup.Track(new Item(0x1234) { Stackable = true, Amount = 10 });
|
||||
stack.MoveToWorld(new Point3D(114, 100, 0), Map.Felucca);
|
||||
|
||||
var remainder = Mobile.LiftItemDupe(stack, 3);
|
||||
var remainder = _cleanup.Track(Mobile.LiftItemDupe(stack, 3));
|
||||
|
||||
Assert.NotNull(remainder);
|
||||
Assert.Equal(7, remainder.Amount);
|
||||
|
|
@ -388,6 +315,4 @@ public class DecayRegistrationTests
|
|||
stack.Delete();
|
||||
remainder.Delete();
|
||||
}
|
||||
|
||||
// Dropping into a container must untrack; taking it back out to the ground must re-track.
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,7 @@
|
|||
using System;
|
||||
using System.Buffers;
|
||||
using Server.Items;
|
||||
using Server.Network;
|
||||
using Server.Tests.Network;
|
||||
using Xunit;
|
||||
using static Server.Tests.TestClientHarness;
|
||||
|
||||
namespace Server.Tests;
|
||||
|
||||
|
|
@ -12,51 +10,6 @@ public class LiftRejectResyncTests
|
|||
{
|
||||
private static readonly Point3D _baseLoc = new(2200, 2200, 0);
|
||||
|
||||
private static (NetState, Mobile) CreateClient(Point3D location)
|
||||
{
|
||||
var ns = PacketTestUtilities.CreateTestNetState();
|
||||
ns.Account = new MockAccount();
|
||||
|
||||
var mobile = new Mobile(World.NewMobile);
|
||||
mobile.DefaultMobileInit();
|
||||
ns.Mobile = mobile;
|
||||
mobile.NetState = ns;
|
||||
mobile.MoveToWorld(location, Map.Felucca);
|
||||
return (ns, mobile);
|
||||
}
|
||||
|
||||
private static void DisposeClient(NetState ns, Mobile mobile)
|
||||
{
|
||||
ns.Mobile = null;
|
||||
ns.Dispose();
|
||||
mobile.Delete();
|
||||
}
|
||||
|
||||
private static bool ReceivedContentUpdate(NetState ns, Serial serial)
|
||||
{
|
||||
Span<byte> expected = stackalloc byte[5];
|
||||
var writer = new SpanWriter(expected);
|
||||
writer.Write((byte)0x25); // ContainerContentUpdate packet ID
|
||||
writer.Write(serial);
|
||||
return ns.SendBuffer.GetReadSpan().IndexOf(expected) >= 0;
|
||||
}
|
||||
|
||||
private static bool ReceivedWorldItem(NetState ns, Item item)
|
||||
{
|
||||
Span<byte> expected = stackalloc byte[OutgoingEntityPackets.MaxWorldEntityPacketLength];
|
||||
var length = OutgoingItemPackets.CreateWorldItem(expected, item);
|
||||
return ns.SendBuffer.GetReadSpan().IndexOf(expected[..length]) >= 0;
|
||||
}
|
||||
|
||||
private static bool ReceivedEquipUpdate(NetState ns, Serial serial)
|
||||
{
|
||||
Span<byte> expected = stackalloc byte[5];
|
||||
var writer = new SpanWriter(expected);
|
||||
writer.Write((byte)0x2E); // EquipUpdate packet ID
|
||||
writer.Write(serial);
|
||||
return ns.SendBuffer.GetReadSpan().IndexOf(expected) >= 0;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PrivateNestedItem_RejectedLift_DoesNotResyncBystander()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -5,8 +5,10 @@ using Xunit;
|
|||
namespace Server.Tests;
|
||||
|
||||
[Collection("Sequential Server Tests")]
|
||||
public class LockedDownContainerDecayTests
|
||||
public class LockedDownContainerDecayTests : IDisposable
|
||||
{
|
||||
private readonly EntityCleanup _cleanup = new();
|
||||
|
||||
public LockedDownContainerDecayTests()
|
||||
{
|
||||
DecayScheduler.ResetForTests();
|
||||
|
|
@ -19,6 +21,8 @@ public class LockedDownContainerDecayTests
|
|||
}
|
||||
}
|
||||
|
||||
public void Dispose() => _cleanup.Dispose();
|
||||
|
||||
private class TestChest : Container
|
||||
{
|
||||
public TestChest() : base(0xE43)
|
||||
|
|
@ -35,11 +39,11 @@ public class LockedDownContainerDecayTests
|
|||
public override bool ContentsDecay => false;
|
||||
}
|
||||
|
||||
private static Item NewLoot() => new Item(0x1F03) { Movable = true, Visible = true };
|
||||
private Item NewLoot() => _cleanup.Track(new Item(0x1F03) { Movable = true, Visible = true });
|
||||
|
||||
private static TestChest PlaceLockedDownChest(int x)
|
||||
private TestChest PlaceLockedDownChest(int x)
|
||||
{
|
||||
var chest = new TestChest { Movable = false };
|
||||
var chest = _cleanup.Track(new TestChest { Movable = false });
|
||||
chest.MoveToWorld(new Point3D(x, 100, 0), Map.Felucca);
|
||||
chest.IsLockedDown = true;
|
||||
return chest;
|
||||
|
|
@ -62,7 +66,7 @@ public class LockedDownContainerDecayTests
|
|||
[Fact]
|
||||
public void ItemInOrdinaryContainer_IsNotTracked()
|
||||
{
|
||||
var chest = new TestChest();
|
||||
var chest = _cleanup.Track(new TestChest());
|
||||
chest.MoveToWorld(new Point3D(121, 100, 0), Map.Felucca);
|
||||
var loot = NewLoot();
|
||||
|
||||
|
|
@ -77,7 +81,7 @@ public class LockedDownContainerDecayTests
|
|||
[Fact]
|
||||
public void LockingAndReleasingContainer_RegistersAndUnregistersContents()
|
||||
{
|
||||
var chest = new TestChest { Movable = false };
|
||||
var chest = _cleanup.Track(new TestChest { Movable = false });
|
||||
chest.MoveToWorld(new Point3D(122, 100, 0), Map.Felucca);
|
||||
var loot = NewLoot();
|
||||
chest.AddItem(loot);
|
||||
|
|
@ -102,7 +106,7 @@ public class LockedDownContainerDecayTests
|
|||
[Fact]
|
||||
public void ContainerThatKeepsContents_DoesNotTrackThem()
|
||||
{
|
||||
var board = new KeepsContentsChest { Movable = false };
|
||||
var board = _cleanup.Track(new KeepsContentsChest { Movable = false });
|
||||
board.MoveToWorld(new Point3D(123, 100, 0), Map.Felucca);
|
||||
board.IsLockedDown = true;
|
||||
var piece = NewLoot();
|
||||
|
|
@ -118,7 +122,7 @@ public class LockedDownContainerDecayTests
|
|||
public void NestedContainerContents_AreNotTracked()
|
||||
{
|
||||
var chest = PlaceLockedDownChest(124);
|
||||
var bag = new TestChest();
|
||||
var bag = _cleanup.Track(new TestChest());
|
||||
chest.AddItem(bag);
|
||||
var loot = NewLoot();
|
||||
bag.AddItem(loot);
|
||||
|
|
@ -145,29 +149,17 @@ public class LockedDownContainerDecayTests
|
|||
[Fact]
|
||||
public void TrackedContents_DecayOnSchedule()
|
||||
{
|
||||
var start = Core._now;
|
||||
using var clock = new SimulatedClockScope();
|
||||
|
||||
try
|
||||
{
|
||||
var chest = PlaceLockedDownChest(126);
|
||||
var loot = NewLoot();
|
||||
chest.AddItem(loot);
|
||||
var chest = PlaceLockedDownChest(126);
|
||||
var loot = NewLoot();
|
||||
chest.AddItem(loot);
|
||||
|
||||
var deadline = start + loot.DecayTime + TimeSpan.FromMinutes(2);
|
||||
for (var now = start; now <= deadline && !loot.Deleted; now += TimeSpan.FromMilliseconds(256))
|
||||
{
|
||||
Core._now = now;
|
||||
DecayScheduler.ProcessTick(now);
|
||||
}
|
||||
clock.AdvanceDecay(loot.DecayTime + TimeSpan.FromMinutes(2), loot);
|
||||
|
||||
Assert.True(loot.Deleted, "Contents of a locked-down container must decay.");
|
||||
Assert.False(chest.Deleted, "The locked-down container itself must not decay.");
|
||||
Assert.True(loot.Deleted, "Contents of a locked-down container must decay.");
|
||||
Assert.False(chest.Deleted, "The locked-down container itself must not decay.");
|
||||
|
||||
chest.Delete();
|
||||
}
|
||||
finally
|
||||
{
|
||||
Core._now = start;
|
||||
}
|
||||
chest.Delete();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,24 @@
|
|||
using System;
|
||||
using System.Buffers;
|
||||
using Server.Items;
|
||||
using Server.Network;
|
||||
using Server.Tests.Network;
|
||||
using Xunit;
|
||||
using static Server.Tests.TestClientHarness;
|
||||
|
||||
namespace Server.Tests;
|
||||
|
||||
[Collection("Sequential Server Tests")]
|
||||
public class PrivateChildPropertiesTests
|
||||
public class PrivateChildPropertiesTests : IDisposable
|
||||
{
|
||||
private readonly bool _oplWasEnabled;
|
||||
|
||||
public PrivateChildPropertiesTests()
|
||||
{
|
||||
_oplWasEnabled = ObjectPropertyList.Enabled;
|
||||
ObjectPropertyList.Enabled = true;
|
||||
}
|
||||
|
||||
public void Dispose() => ObjectPropertyList.Enabled = _oplWasEnabled;
|
||||
|
||||
private class PublicContainer : Container
|
||||
{
|
||||
public PublicContainer() : base(0xE75)
|
||||
|
|
@ -21,35 +30,6 @@ public class PrivateChildPropertiesTests
|
|||
|
||||
private static readonly Point3D _ownerLoc = new(1600, 1600, 0);
|
||||
|
||||
private static (NetState, Mobile) CreateClient(Point3D location)
|
||||
{
|
||||
var ns = PacketTestUtilities.CreateTestNetState();
|
||||
ns.Account = new MockAccount();
|
||||
|
||||
var mobile = new Mobile(World.NewMobile);
|
||||
mobile.DefaultMobileInit();
|
||||
ns.Mobile = mobile;
|
||||
mobile.NetState = ns;
|
||||
mobile.MoveToWorld(location, Map.Felucca);
|
||||
return (ns, mobile);
|
||||
}
|
||||
|
||||
private static void DisposeClient(NetState ns, Mobile mobile)
|
||||
{
|
||||
ns.Mobile = null;
|
||||
ns.Dispose();
|
||||
mobile.Delete();
|
||||
}
|
||||
|
||||
private static bool ReceivedOplInfo(NetState ns, Serial serial)
|
||||
{
|
||||
Span<byte> expected = stackalloc byte[5];
|
||||
var writer = new SpanWriter(expected);
|
||||
writer.Write((byte)0xDC); // OPL info packet ID
|
||||
writer.Write(serial);
|
||||
return ns.SendBuffer.GetReadSpan().IndexOf(expected) >= 0;
|
||||
}
|
||||
|
||||
// AddItem/MoveToWorld already queue their own Update (+Properties) delta as a side effect of the
|
||||
// Map setter. Draining it first isolates the properties-only delta the test actually exercises;
|
||||
// otherwise the leftover Update flag routes through the Update branch and the test would pass or
|
||||
|
|
@ -68,9 +48,6 @@ public class PrivateChildPropertiesTests
|
|||
[Fact]
|
||||
public void PrivateChildInOwnerBackpack_PropertiesOnlyUpdate_ReachesOwnerNotBystander()
|
||||
{
|
||||
var wasEnabled = ObjectPropertyList.Enabled;
|
||||
ObjectPropertyList.Enabled = true;
|
||||
|
||||
var (ownerNs, owner) = CreateClient(_ownerLoc);
|
||||
var (bystanderNs, bystander) = CreateClient(new Point3D(_ownerLoc.X + 1, _ownerLoc.Y, 0));
|
||||
|
||||
|
|
@ -93,16 +70,12 @@ public class PrivateChildPropertiesTests
|
|||
child.Delete();
|
||||
DisposeClient(ownerNs, owner);
|
||||
DisposeClient(bystanderNs, bystander);
|
||||
ObjectPropertyList.Enabled = wasEnabled;
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PrivateGroundContainer_PropertiesOnlyUpdate_ReachesOpenerNotNonOpener()
|
||||
{
|
||||
var wasEnabled = ObjectPropertyList.Enabled;
|
||||
ObjectPropertyList.Enabled = true;
|
||||
|
||||
var (openerNs, opener) = CreateClient(_ownerLoc);
|
||||
var (bystanderNs, bystander) = CreateClient(new Point3D(_ownerLoc.X + 2, _ownerLoc.Y, 0));
|
||||
|
||||
|
|
@ -126,16 +99,12 @@ public class PrivateChildPropertiesTests
|
|||
container.Delete();
|
||||
DisposeClient(openerNs, opener);
|
||||
DisposeClient(bystanderNs, bystander);
|
||||
ObjectPropertyList.Enabled = wasEnabled;
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PublicContainerChild_PropertiesOnlyUpdate_StillReachesBystander()
|
||||
{
|
||||
var wasEnabled = ObjectPropertyList.Enabled;
|
||||
ObjectPropertyList.Enabled = true;
|
||||
|
||||
var (bystanderNs, bystander) = CreateClient(_ownerLoc);
|
||||
|
||||
var pack = new PublicContainer();
|
||||
|
|
@ -155,16 +124,12 @@ public class PrivateChildPropertiesTests
|
|||
{
|
||||
pack.Delete();
|
||||
DisposeClient(bystanderNs, bystander);
|
||||
ObjectPropertyList.Enabled = wasEnabled;
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GroundItem_PropertiesOnlyUpdate_StillReachesBystander()
|
||||
{
|
||||
var wasEnabled = ObjectPropertyList.Enabled;
|
||||
ObjectPropertyList.Enabled = true;
|
||||
|
||||
var (bystanderNs, bystander) = CreateClient(_ownerLoc);
|
||||
|
||||
var item = new Item(0x1234);
|
||||
|
|
@ -181,7 +146,6 @@ public class PrivateChildPropertiesTests
|
|||
{
|
||||
item.Delete();
|
||||
DisposeClient(bystanderNs, bystander);
|
||||
ObjectPropertyList.Enabled = wasEnabled;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue