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;
}
}
}