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