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