using System;
using System.Diagnostics;
using System.Threading;
using UnitTestSharp;
namespace TestsForUnitTestSharp
{
public class TimedInvokerTests : TestFixture
{
public const int GenerousBudget = 10000;
///
/// Outlasts TimedInvoker's 50ms maximum poll, so the budget checks actually run - an action that finishes
/// inside one poll never enters the loop body and would pass no matter what those checks decided.
///
public const int LongerThanAPoll = 150;
///
/// Caps the runaway actions so a failed abort costs two seconds instead of wedging the suite.
///
public const int AbortSafetyNet = 2000;
///
/// GetThreadTimes resolves to the scheduler tick (~15.6ms), so a thread that burned nothing still can't be
/// asserted at exactly zero.
///
public const long NoiseFloorMilliseconds = 50;
///
/// Long enough that a broken worker fails rather than hangs; every healthy case returns immediately.
///
public const int Grace = 5000;
public static void BurnCpu()
{
var elapsed = Stopwatch.StartNew();
double accumulator = 0;
while (elapsed.ElapsedMilliseconds < LongerThanAPoll)
{
accumulator += Math.Sqrt(accumulator + 1);
}
}
public static void BurnCpuUntilAborted()
{
var elapsed = Stopwatch.StartNew();
double accumulator = 0;
while (elapsed.ElapsedMilliseconds < AbortSafetyNet)
{
accumulator += Math.Sqrt(accumulator + 1);
}
}
public static TestWorker WorkerThatHasBurnedCpu()
{
var worker = new TestWorker();
worker.Begin(BurnCpu);
worker.WaitForCompletion(Grace);
return worker;
}
public class PollIntervalTests : TestFixture
{
public void NonPositiveTimeout_IsTheMaximum()
{
CheckEqual(TimedInvoker.MaximumPollIntervalInMilliseconds, TimedInvoker.PollInterval(0));
CheckEqual(TimedInvoker.MaximumPollIntervalInMilliseconds,
TimedInvoker.PollInterval(Timeout.Infinite));
}
public void ModerateTimeout_IsAQuarterOfTheBudget()
{
CheckEqual(25, TimedInvoker.PollInterval(100));
}
public void TimeoutSmallerThanFourMilliseconds_IsFlooredAtOne()
{
CheckEqual(1, TimedInvoker.PollInterval(1));
CheckEqual(1, TimedInvoker.PollInterval(3));
}
public void HugeTimeout_IsCappedAtTheMaximum()
{
CheckEqual(TimedInvoker.MaximumPollIntervalInMilliseconds, TimedInvoker.PollInterval(GenerousBudget));
}
public void TimeoutExactlyFourTimesTheMaximum_IsTheMaximum()
{
CheckEqual(TimedInvoker.MaximumPollIntervalInMilliseconds,
TimedInvoker.PollInterval(TimedInvoker.MaximumPollIntervalInMilliseconds * 4));
}
}
public class ExceededTests : TestFixture
{
public void UnmeasuredUse_IsFalse()
{
CheckFalse(TimedInvoker.Exceeded(null, 1));
}
public void NonPositiveLimit_IsFalse()
{
CheckFalse(TimedInvoker.Exceeded(GenerousBudget, 0));
CheckFalse(TimedInvoker.Exceeded(GenerousBudget, Timeout.Infinite));
}
public void UseBelowTheLimit_IsFalse()
{
CheckFalse(TimedInvoker.Exceeded(99, 100));
}
public void UseExactlyAtTheLimit_IsTrue()
{
CheckTrue(TimedInvoker.Exceeded(100, 100));
}
public void UseBeyondTheLimit_IsTrue()
{
CheckTrue(TimedInvoker.Exceeded(101, 100));
}
public void UnmeasuredUseAgainstAnUnlimitedBudget_IsFalse()
{
CheckFalse(TimedInvoker.Exceeded(null, 0));
}
}
public class CpuMillisecondsUsedTests : TestFixture
{
public void FirstPoll_SeedsTheBaseline()
{
TimeSpan? baseline = null;
TimedInvoker.CpuMillisecondsUsed(new TestWorker(), ref baseline, 0);
CheckNotNull(baseline);
}
public void FirstPoll_ReportsOnlyThePollInterval()
{
TimeSpan? baseline = null;
CheckEqual(7, TimedInvoker.CpuMillisecondsUsed(WorkerThatHasBurnedCpu(), ref baseline, 7));
}
public void BaselineAlreadySeeded_LeavesItAlone()
{
TimeSpan? baseline = TimeSpan.FromHours(1);
TimedInvoker.CpuMillisecondsUsed(new TestWorker(), ref baseline, 0);
CheckEqual(TimeSpan.FromHours(1), baseline);
}
public void WorkerBurnsCpuBetweenPolls_GrowsPastThePollInterval()
{
var worker = new TestWorker();
TimeSpan? baseline = null;
TimedInvoker.CpuMillisecondsUsed(worker, ref baseline, 0);
worker.Begin(BurnCpu);
worker.WaitForCompletion(Grace);
CheckGreater(TimedInvoker.CpuMillisecondsUsed(worker, ref baseline, 0), NoiseFloorMilliseconds);
}
public void WorkerIdleBetweenPolls_StaysAtThePollInterval()
{
var worker = new TestWorker();
TimeSpan? baseline = null;
TimedInvoker.CpuMillisecondsUsed(worker, ref baseline, 5);
Thread.Sleep(LongerThanAPoll);
CheckLess(TimedInvoker.CpuMillisecondsUsed(worker, ref baseline, 5), 5 + NoiseFloorMilliseconds);
}
}
public class AbandonTests : TestFixture
{
public void Always_ReturnsATimeoutNamingTheLimit()
{
Exception result = TimedInvoker.Abandon(new TestWorker(), "running time", 1234, 0);
CheckEqual(typeof(TimeoutException), result?.GetType());
CheckEqualRegex("running time", Convert.ToString(result));
CheckEqualRegex("1234ms", Convert.ToString(result));
}
public void MeasuredCpu_ReportsHowMuchWasUsed()
{
Exception result = TimedInvoker.Abandon(new TestWorker(), "CPU time", 500, 42);
CheckEqualRegex("42ms of CPU", Convert.ToString(result));
}
public void UnmeasuredCpu_SaysSoRatherThanReportingZero()
{
Exception result = TimedInvoker.Abandon(new TestWorker(), "CPU time", 500, null);
CheckEqualRegex("CPU unmeasured", Convert.ToString(result));
}
public void RunningWorker_AbortsIt()
{
var worker = new TestWorker();
var running = new ManualResetEventSlim(false);
var neverSet = new ManualResetEventSlim(false);
worker.Begin(() =>
{
running.Set();
neverSet.Wait();
});
running.Wait();
TimedInvoker.Abandon(worker, "CPU time", 500, 0);
CheckTrue(worker.WaitForCompletion(Grace));
CheckEqual(typeof(ThreadAbortException), worker.Result?.GetType());
}
}
public class InvokeTests : TestFixture
{
static dynamic[] unlimitedBudgets = new[]
{
new { budget = 0 },
new { budget = Timeout.Infinite },
};
[IgnoreTest]
private void CheckAbandonedFor(string limitDescription, Exception result)
{
CheckEqual(typeof(TimeoutException), result?.GetType());
CheckEqualRegex(limitDescription, Convert.ToString(result));
CheckEqualRegex("ms of CPU", Convert.ToString(result));
}
public void ActionSucceeds_ReturnsNull()
{
CheckNull(TimedInvoker.Invoke(() => { },
cpuTimeoutInMilliseconds: GenerousBudget,
wallClockTimeoutInMilliseconds: GenerousBudget));
}
public void ActionThrows_ReturnsTheThrownException()
{
var thrown = new InvalidOperationException("boom");
CheckEqual(thrown, TimedInvoker.Invoke(() => { throw thrown; },
cpuTimeoutInMilliseconds: GenerousBudget,
wallClockTimeoutInMilliseconds: GenerousBudget));
}
public void ExceedsCpuBudget_AbandonsNamingCpuTime()
{
CheckAbandonedFor("CPU time", TimedInvoker.Invoke(BurnCpuUntilAborted,
cpuTimeoutInMilliseconds: 50,
wallClockTimeoutInMilliseconds: GenerousBudget));
}
public void ExceedsWallClockBudget_AbandonsNamingRunningTime()
{
CheckAbandonedFor("running time",
TimedInvoker.Invoke(() => Thread.Sleep(AbortSafetyNet),
cpuTimeoutInMilliseconds: GenerousBudget,
wallClockTimeoutInMilliseconds: 50));
}
public void SleepingLongerThanItsCpuBudget_DoesNotAbandon()
{
CheckNull(TimedInvoker.Invoke(() => Thread.Sleep(LongerThanAPoll),
cpuTimeoutInMilliseconds: 50,
wallClockTimeoutInMilliseconds: GenerousBudget));
}
[Parameterized("unlimitedBudgets")]
public void UnlimitedBudget_DoesNotAbandon(dynamic row)
{
int budget = (int)row.budget;
CheckNull(TimedInvoker.Invoke(() => Thread.Sleep(LongerThanAPoll),
cpuTimeoutInMilliseconds: budget,
wallClockTimeoutInMilliseconds: budget));
}
public void CalledRepeatedly_ReusesTheWorker()
{
int first = 0;
int second = 0;
TimedInvoker.Invoke(() => first = Thread.CurrentThread.ManagedThreadId,
cpuTimeoutInMilliseconds: GenerousBudget,
wallClockTimeoutInMilliseconds: GenerousBudget);
TimedInvoker.Invoke(() => second = Thread.CurrentThread.ManagedThreadId,
cpuTimeoutInMilliseconds: GenerousBudget,
wallClockTimeoutInMilliseconds: GenerousBudget);
CheckEqual(first, second);
}
public void AfterATimeout_NextCallStillSucceeds()
{
TimedInvoker.Invoke(() => Thread.Sleep(AbortSafetyNet),
cpuTimeoutInMilliseconds: GenerousBudget,
wallClockTimeoutInMilliseconds: 50);
CheckNull(TimedInvoker.Invoke(() => { },
cpuTimeoutInMilliseconds: GenerousBudget,
wallClockTimeoutInMilliseconds: GenerousBudget));
}
}
}
}