using System; using System.Text.RegularExpressions; using UnitTestSharp; namespace TestsForUnitTestSharp { public class AllocationTestAttributeTests : TestFixture { /// /// Hands out a scripted sequence of readings, so a test can describe how a body allocates without having /// to allocate anything to produce it. The last scripted value repeats, so a script only has to describe /// the runs that differ. /// public class ScriptedCounter { private readonly long[] _bytesAllocatedPerRun; private long _total; private int _run = -1; private bool _openingTheRegion = true; public ScriptedCounter(params long[] bytesAllocatedPerRun) { _bytesAllocatedPerRun = bytesAllocatedPerRun; } private long BytesForRun(int run) { if (_bytesAllocatedPerRun.Length == 0) { return 0; } return _bytesAllocatedPerRun[Math.Min(run, _bytesAllocatedPerRun.Length - 1)]; } /// /// The counter is read exactly twice a run: once to open the measured region, once by the check that /// closes it. The scripted bytes land between the two. /// private long Read() { if (_openingTheRegion) { ++_run; _openingTheRegion = false; return _total; } _openingTheRegion = true; _total += BytesForRun(_run); return _total; } public Func AsCounter() { return Read; } } [IgnoreFixture] public class SubjectFixture : TestFixture { public static int BodyRuns; [AllocationTest] public void ExpectsNothing() { ++BodyRuns; CheckNoAllocations(); } [AllocationTest] public void ExpectsWithinABudget() { ++BodyRuns; CheckAllocationsAtMost(maxBytesPerRun: 100); } [AllocationTest(measuredIterations: 4)] public void ExpectsNothingOverFourRuns() { ++BodyRuns; CheckNoAllocations(); } [AllocationTest] public void NeverChecksAnything() { ++BodyRuns; } public void ChecksWithoutTheAttribute() { ++BodyRuns; CheckNoAllocations(); } [AllocationTest] public void AssertsAfterTheAllocationCheck() { ++BodyRuns; CheckNoAllocations(); CheckEqual(1, 2); } private object _sink; [AllocationTest] public void ReallyAllocates() { ++BodyRuns; _sink = new byte[64]; CheckNoAllocations(); } [AllocationTest] public void ChecksTwice() { ++BodyRuns; CheckNoAllocations(); CheckNoAllocations(); } [AllocationTest] public void ReallyAllocatesWithinABudget() { ++BodyRuns; _sink = new byte[64]; CheckAllocationsAtMost(maxBytesPerRun: 1024); } [AllocationTest] public void AlwaysFailsANonAllocationCheck() { ++BodyRuns; CheckTrue(false); CheckNoAllocations(); } [AllocationTest] public void AlwaysThrows() { ++BodyRuns; throw new InvalidOperationException("boom"); } } /// /// Drives one SubjectFixture test through the real runner with a scripted counter in place. /// [IgnoreTest] public static bool RunSubjectTest(string methodName, Func counter, out string output) { Func oldSource = AllocationCounter.Source; bool oldIgnoreDebugger = TestRunner.IgnoreDebugger; AllocationCounter.Source = counter; TestRunner.IgnoreDebugger = true; SubjectFixture.BodyRuns = 0; try { int failedChecks = 0; string captured = ""; bool passed = TestRunner.RunTest(new SubjectFixture(), typeof(SubjectFixture).GetMethod(methodName), null, 0, (outputString) => captured += outputString, ref failedChecks); output = captured; return passed; } finally { AllocationCounter.Source = oldSource; TestRunner.IgnoreDebugger = oldIgnoreDebugger; } } public class CheckNoAllocationsTests : TestFixture { public void BodyAllocatesNothing_Passes() { Check(RunSubjectTest("ExpectsNothing", new ScriptedCounter(0).AsCounter(), out string output)); CheckNotEqualRegex("error", output); } public void BodyAllocatesEveryRun_Fails() { CheckFalse(RunSubjectTest("ExpectsNothing", new ScriptedCounter(64).AsCounter(), out string output)); CheckEqualRegex("Expected to allocate at most 0 bytes per run", output); } public void BodyAllocatesEveryRun_BlamesTheTestBodyRatherThanTheCheck() { RunSubjectTest("ExpectsNothing", new ScriptedCounter(64).AsCounter(), out string output); CheckEqualRegex(@"AllocationTestAttributeTests\.cs\(", output); CheckNotEqualRegex(@"Assert\.cs", output); } public void BodyAllocatesOnlyWhileWarmingUp_Passes() { Check(RunSubjectTest("ExpectsNothing", new ScriptedCounter(500, 500, 0).AsCounter(), out string output)); CheckNotEqualRegex("error", output); } public void BodyAllocatesOnJustOneMeasuredRun_Fails() { CheckFalse(RunSubjectTest("ExpectsNothingOverFourRuns", new ScriptedCounter(0, 0, 0, 0, 0, 0, 40, 0).AsCounter(), out string output)); CheckEqualRegex("40 bytes", output); } public void WithoutTheAttribute_FailsRatherThanPassingSilently() { CheckFalse(RunSubjectTest("ChecksWithoutTheAttribute", new ScriptedCounter(0).AsCounter(), out string output)); CheckEqualRegex("AllocationTest", output); } public void WithNoCounterOnThisRuntime_FailsRatherThanPassingSilently() { CheckFalse(RunSubjectTest("ExpectsNothing", null, out string output)); CheckEqualRegex("can't be measured", output); } public void WithNoCounterOnThisRuntime_DoesNotAlsoClaimTheTestNeverChecked() { RunSubjectTest("ExpectsNothing", null, out string output); CheckNotEqualRegex("never called", output); } } public class CheckAllocationsAtMostTests : TestFixture { public void UnderTheBudget_Passes() { Check(RunSubjectTest("ExpectsWithinABudget", new ScriptedCounter(99).AsCounter(), out string output)); CheckNotEqualRegex("error", output); } public void ExactlyTheBudget_Passes() { Check(RunSubjectTest("ExpectsWithinABudget", new ScriptedCounter(100).AsCounter(), out string output)); CheckNotEqualRegex("error", output); } public void OneByteOverTheBudget_Fails() { CheckFalse(RunSubjectTest("ExpectsWithinABudget", new ScriptedCounter(101).AsCounter(), out string output)); CheckEqualRegex("at most 100 bytes per run", output); } public void OverTheBudget_ReportsThePerRunAverage() { CheckFalse(RunSubjectTest("ExpectsWithinABudget", new ScriptedCounter(250).AsCounter(), out string output)); CheckEqualRegex("250 per run", output); } } public class MeasuredIterationsTests : TestFixture { /// /// One value per warmup run plus one, so no three consecutive readings can ever match and warmup /// always runs out the clock. /// private static long[] NeverSettlingScript() { var script = new long[AllocationTestAttribute.DefaultMaximumWarmupIterations + 1]; for (int i = 0; i < script.Length; ++i) { script[i] = i + 1; } return script; } public void Default_RunsTheBodyOncePerSettlingReadingAndPerMeasuredRun() { RunSubjectTest("ExpectsNothing", new ScriptedCounter(0).AsCounter(), out string output); CheckEqual(AllocationTestAttribute.DefaultSettledReadingsRequired + AllocationTestAttribute.DefaultMeasuredIterations, SubjectFixture.BodyRuns); } public void Overridden_MeasuresOverThatManyRuns() { RunSubjectTest("ExpectsNothingOverFourRuns", new ScriptedCounter(0).AsCounter(), out string output); CheckEqual(AllocationTestAttribute.DefaultSettledReadingsRequired + 4, SubjectFixture.BodyRuns); } public void ReadingsNeverSettle_StopsWarmingUpAtTheCapAndFailsWithoutMeasuring() { CheckFalse(RunSubjectTest("ExpectsNothing", new ScriptedCounter(NeverSettlingScript()).AsCounter(), out string output)); CheckEqualRegex("never settled", output); CheckEqual(AllocationTestAttribute.DefaultMaximumWarmupIterations, SubjectFixture.BodyRuns); } public void ReadingsNeverSettle_ReportsAsAnOrdinaryFailureNotAnInternalError() { CheckFalse(RunSubjectTest("ExpectsNothing", new ScriptedCounter(NeverSettlingScript()).AsCounter(), out string output)); CheckNotEqualRegex("internal error", output); } } public class AbortsEarlyOnFailureTests : TestFixture { public void NonAllocationFailureDuringWarmup_AbortsWithoutFurtherWarmupRuns() { CheckFalse(RunSubjectTest("AlwaysFailsANonAllocationCheck", new ScriptedCounter(0).AsCounter(), out string output)); CheckEqualRegex("\"false\"", output); CheckEqual(1, SubjectFixture.BodyRuns); } public void LiveRunFailsBudget_AbortsWithoutFurtherMeasuredRuns() { CheckFalse(RunSubjectTest("ExpectsNothing", new ScriptedCounter(0, 0, 0, 40).AsCounter(), out string output)); CheckEqualRegex("1 runs allocated 40 bytes", output); CheckEqual(AllocationTestAttribute.DefaultSettledReadingsRequired + 1, SubjectFixture.BodyRuns); } /// /// A thrown exception aborts warmup exactly like a failed check does - via the same /// thrown-or-TestFailed condition - rather than being swallowed or left to run out the clock. /// public void ExceptionDuringWarmup_AbortsWithoutFurtherWarmupRuns() { CheckFalse(RunSubjectTest("AlwaysThrows", new ScriptedCounter(0).AsCounter(), out string output)); CheckEqualRegex("InvalidOperationException", output); CheckEqualRegex("boom", output); CheckEqual(1, SubjectFixture.BodyRuns); } } public class AllocationCheckFiredTests : TestFixture { public void MarkedButNeverChecked_Fails() { CheckFalse(RunSubjectTest("NeverChecksAnything", new ScriptedCounter(0).AsCounter(), out string output)); CheckEqualRegex("never called", output); } /// /// Warmup settles fine (nothing about "never calls the check" is visible until the measured phase /// actually needs it), then the first measured run's own failure to call it aborts immediately - /// same as any other failed run, not eight wasted measured runs before saying so. /// public void MarkedButNeverChecked_AbortsAfterOneMeasuredRun() { RunSubjectTest("NeverChecksAnything", new ScriptedCounter(0).AsCounter(), out string output); CheckEqual(AllocationTestAttribute.DefaultSettledReadingsRequired + 1, SubjectFixture.BodyRuns); } public void MarkedButNeverChecked_ReportsNoSourceLocation() { RunSubjectTest("NeverChecksAnything", new ScriptedCounter(0).AsCounter(), out string output); CheckNotEqualRegex(@"\.cs\(", output); } public void ClosingTheRegionTwiceInOneBody_Fails() { CheckFalse(RunSubjectTest("ChecksTwice", new ScriptedCounter(0).AsCounter(), out string output)); CheckEqualRegex("only measure", output); CheckEqualRegex("once", output); } public void AssertionsAfterTheAllocationCheck_AreReportedExactlyOnce() { CheckFalse(RunSubjectTest("AssertsAfterTheAllocationCheck", new ScriptedCounter(0).AsCounter(), out string output)); CheckEqual(1, Regex.Matches(output, "Expected \"1\" but was \"2\"").Count); } } /// /// A real test fixture instance runs every one of its test methods in turn - RunSubjectTest hides that /// by handing each test a fresh SubjectFixture, so these drive TestRunner.RunTest directly, twice, on /// one shared instance, to prove state from the first run can't leak into the second. /// public class CrossTestIsolationTests : TestFixture { [IgnoreFixture] public class TwoAllocationTestsFixture : TestFixture { private object _sink; [AllocationTest] public void FirstAllocates() { _sink = new byte[128]; CheckAllocationsAtMost(maxBytesPerRun: 10000); } [AllocationTest] public void SecondExpectsNothing() { CheckNoAllocations(); } public void ThirdChecksWithoutTheAttribute() { CheckNoAllocations(); } } public void MeasurementFromAnEarlierTest_DoesNotLeakIntoTheNext() { var fixture = new TwoAllocationTestsFixture(); int failedChecks = 0; string ignoredOutput = ""; TestRunner.RunTest(fixture, typeof(TwoAllocationTestsFixture).GetMethod("FirstAllocates"), null, 0, s => ignoredOutput += s, ref failedChecks); string output = ""; bool passed = TestRunner.RunTest(fixture, typeof(TwoAllocationTestsFixture).GetMethod("SecondExpectsNothing"), null, 0, s => output += s, ref failedChecks); Check(passed); CheckNotEqualRegex("error", output); } public void AllocationTestAttribute_DoesNotLeakIntoAPlainTestAfterIt() { var fixture = new TwoAllocationTestsFixture(); int failedChecks = 0; string ignoredOutput = ""; TestRunner.RunTest(fixture, typeof(TwoAllocationTestsFixture).GetMethod("FirstAllocates"), null, 0, s => ignoredOutput += s, ref failedChecks); string output = ""; bool passed = TestRunner.RunTest(fixture, typeof(TwoAllocationTestsFixture).GetMethod("ThirdChecksWithoutTheAttribute"), null, 0, s => output += s, ref failedChecks); CheckFalse(passed); CheckEqualRegex("AllocationTest", output); } } /// /// Everything above scripts the counter. These drive the real one, so a break in the wiring between the /// attribute, the runner's loop and the counter fails here rather than passing on a fake. /// public class AgainstTheRealCounterTests : TestFixture { private int _accumulator; [AllocationTest] public void IncrementingAnIntAllocatesNothing() { ++_accumulator; CheckNoAllocations(); } public void BodyThatReallyAllocates_Fails() { CheckFalse(RunSubjectTest("ReallyAllocates", AllocationCounter.BindToRuntime(), out string output)); CheckEqualRegex("Expected to allocate at most 0 bytes per run", output); } public void BodyThatReallyAllocates_PassesWithinABudgetThatCoversIt() { Check(RunSubjectTest("ReallyAllocatesWithinABudget", AllocationCounter.BindToRuntime(), out string output)); CheckNotEqualRegex("error", output); } } public class BindToRuntimeTests : TestFixture { public void OnThisRuntime_BindsACounterThatDoesNotAllocateAndOnlyRises() { Func counter = AllocationCounter.BindToRuntime(); CheckNotNull(counter); long first = counter(); long second = counter(); CheckEqual(0L, second - first); var allocated = new byte[10000]; CheckGreater(counter(), second); GC.KeepAlive(allocated); } } } }