using Xunit; namespace AcDream.App.Tests; /// /// Guards the apparatus itself (docs/ISSUES.md #250). A probe that smooths away /// measurement noise is only worth having if it still reports real allocation, /// so these pin both directions: a genuinely allocating step must be reported /// above zero, and the assertion helper built on it must actually throw. /// Without this, a future edit could quietly turn the whole zero-allocation /// family into tests that cannot fail. /// public class ZeroAllocationProbeTests { [Fact] public void GenuinelyAllocatingStep_IsReportedAboveZero() { // Allocates on every invocation, so every window sees it and the // minimum cannot be zero no matter how long the warmup runs. object? sink = null; long allocated = ZeroAllocationProbe.MeasureWarmed( () => sink = new byte[1024]); Assert.NotNull(sink); Assert.True( allocated >= 1024, $"Expected at least the 1 KiB the step allocates, measured {allocated:N0}."); } [Fact] public void GenuinelyAllocatingStep_FailsTheAssertion() { object? sink = null; var failure = Assert.Throws( () => ZeroAllocationProbe.AssertAllocatesNothing( "deliberately allocating step", () => sink = new byte[1024])); Assert.Contains("deliberately allocating step", failure.Message); Assert.Contains("expected 0", failure.Message); } [Fact] public void NonAllocatingStep_IsReportedAsZero() { int counter = 0; long allocated = ZeroAllocationProbe.MeasureWarmed(() => counter++); Assert.True( counter > ZeroAllocationProbe.DefaultBatchSize * ZeroAllocationProbe.DefaultWarmupBatches); Assert.Equal(0, allocated); } [Fact] public void OneTimeCost_IsExcluded() { // Half of the distinction the probe exists to draw: a step that // allocates only on its first invocation is a startup cost, is gone by // the time the first window opens, and must read as zero. int invocations = 0; object? sink = null; long allocated = ZeroAllocationProbe.MeasureWarmed(() => { if (invocations++ == 0) sink = new byte[4096]; }); Assert.NotNull(sink); Assert.Equal(0, allocated); } [Fact] public void PeriodicCost_ShorterThanTheBatch_IsNotExcluded() { // The other half, and the reason a window is a batch rather than a // single invocation. A cost recurring every tenth call is steady-state. // Minimising over single invocations would miss it — nine windows in // ten are clean — so the probe sums a batch of 32 first, which cannot // avoid containing at least three of them. int invocations = 0; object? sink = null; long allocated = ZeroAllocationProbe.MeasureWarmed(() => { if (invocations++ % 10 == 0) sink = new byte[4096]; }); Assert.NotNull(sink); Assert.True( allocated > 0, "A cost recurring every tenth invocation is steady-state and must " + $"not read as zero; measured {allocated:N0}."); } [Fact] public void PeriodicCost_IsCaughtWheneverTheBatchCoversThePeriod() { // Pins the stated limit rather than leaving it as prose: the batch has // to cover the period. A cost every 8th call is caught by a batch of // 16; the identical cost is invisible to a batch of 4, which is why // DefaultBatchSize sits well above the periods in the paths under test. Assert.True(MeasurePeriodicCost(period: 8, batchSize: 16) > 0); Assert.Equal(0, MeasurePeriodicCost(period: 8, batchSize: 4)); static long MeasurePeriodicCost(int period, int batchSize) { int invocations = 0; object? sink = null; long allocated = ZeroAllocationProbe.MeasureWarmed( () => { // Offset so the first invocation of a batch is never the // allocating one; otherwise the small-batch case would // catch it by alignment rather than by coverage. if (invocations++ % period == period - 1) sink = new byte[4096]; }, batchSize: batchSize); Assert.NotNull(sink); return allocated; } } }