using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; using System.Runtime.CompilerServices; using UnitTestSharp; namespace Glass.UnitTests { public class ExceptionLogTests : TestFixture { public static readonly ExceptionSite SiteA = new ExceptionSite(new Control("a"), ExceptionPhase.Update); public static readonly ExceptionSite SiteB = new ExceptionSite(new Control("b"), ExceptionPhase.Update); /// /// One throw site for whatever exception it's handed. /// public static readonly Action Rethrow = exception => throw exception; public class ThrowingControl : Control { public override void CancelMouseInput() { throw new InvalidOperationException("Thrown by client code."); } } [MethodImpl(MethodImplOptions.NoInlining)] public static void ThrowFromClientHelper() { throw new InvalidOperationException("Thrown by a client helper."); } public const string Pedigree = "window > toolbar > okButton"; /// /// Runs behind a boundary filtered by , and returns whether /// the log claimed what it threw. /// public static bool Run(ExceptionLog log, ExceptionSite site, System.Action action) { try { action(); } catch (Exception exception) when (log.Claims(exception, site, Pedigree)) { return true; } return false; } public static bool Run(ExceptionLog log, ExceptionSite site, Action action, T argument, string pedigree = Pedigree) { try { action(argument); } catch (Exception exception) when (log.Claims(exception, site, pedigree)) { return true; } return false; } /// /// Has client code throw a new at , and returns the /// entry it landed in. /// public static ExceptionEntry Hit(ExceptionLog log, ExceptionSite site) where TException : Exception, new() { Run(log, site, Rethrow, new TException()); return log.Entries.Single(entry => entry.Exception.GetType() == typeof(TException)); } public class CtorTests : TestFixture { public void StartsAtFrameZero() { CheckEqual(0L, new ExceptionLog().Frame); } public void StartsAtTimeZero() { CheckEqual(TimeSpan.Zero, new ExceptionLog().Now); } public void StartsWithNoEntries() { CheckEqual(0, new ExceptionLog().Entries.Count); } } public class AdvanceTests : TestFixture { private ExceptionLog log; private List announced; public override void TestSetup() { log = new ExceptionLog(); announced = new List(); log.EntryAdded += entry => announced.Add(entry); } public void Always_CountsOneFrame() { log.Advance(TimeSpan.FromMilliseconds(16)); log.Advance(TimeSpan.FromMilliseconds(16)); CheckEqual(2L, log.Frame); } public void Always_AccumulatesTheElapsedTime() { log.Advance(TimeSpan.FromMilliseconds(16)); log.Advance(TimeSpan.FromMilliseconds(4)); CheckEqual(TimeSpan.FromMilliseconds(20), log.Now); } public void ZeroElapsed_StillCountsAFrame() { log.Advance(TimeSpan.Zero); CheckEqual(1L, log.Frame); CheckEqual(TimeSpan.Zero, log.Now); } public void NewEntry_IsAnnouncedOnlyOnceAdvanced() { ExceptionEntry entry = Hit(log, SiteA); CheckEqual(0, announced.Count); log.Advance(TimeSpan.Zero); CheckEqual(1, announced.Count); CheckEqual(entry, announced[0]); } public void EntryHitAgain_IsNotAnnouncedAgain() { Hit(log, SiteA); log.Advance(TimeSpan.Zero); Hit(log, SiteA); log.Advance(TimeSpan.Zero); CheckEqual(1, announced.Count); } public void EntryAddedWithNoSubscriber_IsNotAnnouncedToALaterOne() { var unsubscribed = new ExceptionLog(); Hit(unsubscribed, SiteA); unsubscribed.Advance(TimeSpan.Zero); unsubscribed.EntryAdded += entry => announced.Add(entry); unsubscribed.Advance(TimeSpan.Zero); CheckEqual(0, announced.Count); } public void SeveralNewEntries_AreAnnouncedInTheOrderTheyWereAdded() { ExceptionEntry first = Hit(log, SiteA); ExceptionEntry second = Hit(log, SiteA); log.Advance(TimeSpan.Zero); CheckEqual(2, announced.Count); CheckEqual(first, announced[0]); CheckEqual(second, announced[1]); } } public class SnoozeTests : TestFixture { public void Always_SnoozesUntilNowPlusTheDuration() { var log = new ExceptionLog(); log.Advance(TimeSpan.FromMilliseconds(16)); var entry = new ExceptionEntry(new InvalidOperationException(), log.Frame, log.Now); log.Snooze(entry, TimeSpan.FromMilliseconds(10)); CheckEqual(TimeSpan.FromMilliseconds(26), entry.SnoozedUntil); } } public class ClaimsTests : TestFixture { private ExceptionLog log; public override void TestSetup() { log = new ExceptionLog(); } public void ExceptionFromClientCode_IsClaimed() { CheckTrue(Run(log, SiteA, () => throw new InvalidOperationException())); CheckEqual(1, log.Entries.Count); } public void ExceptionFromGlassAlone_Propagates() { CheckThrow(typeof(ArgumentException)); Run(log, SiteA, new ControlContainer().MoveToFront, new Control()); } public void ExceptionFromALibraryBeneathGlassAlone_Propagates() { var list = new List(); IEnumerator enumerator = list.GetEnumerator(); list.Add(1); CheckThrow(typeof(InvalidOperationException)); Run(log, SiteA, enumerator.Reset); } public void GlassExceptionReachedThroughClientCode_IsClaimed() { CheckTrue(Run(log, SiteA, () => new ControlContainer().MoveToFront(new Control()))); } public void ClientExceptionReachedThroughGlass_IsClaimed() { var root = new Panel(); root.CanvasChildren.Add(new ThrowingControl()); CheckTrue(Run(log, SiteA, root.CancelMouseInputInSubtree)); } public void OutOfMemory_Propagates() { CheckThrow(typeof(OutOfMemoryException)); Run(log, SiteA, () => throw new OutOfMemoryException()); } public void ClientExceptionReachedThroughALibraryBeneathGlass_IsClaimed() { CheckTrue(Run>(log, SiteA, new List { 1 }.ForEach, _ => throw new InvalidOperationException())); } public void DynamicCodeCalledFromClientCode_IsClaimed() { System.Action compiled = Expression.Lambda( Expression.Throw(Expression.New(typeof(InvalidOperationException)))).Compile(); CheckTrue(Run(log, SiteA, () => compiled())); } public void SameDynamicThrowTwice_MakesOneEntry() { System.Action compiled = Expression.Lambda( Expression.Throw(Expression.New(typeof(InvalidOperationException)))).Compile(); System.Action callCompiled = () => compiled(); Run(log, SiteA, callCompiled); Run(log, SiteA, callCompiled); CheckEqual(1, log.Entries.Count); } public void DifferentThrowSites_MakeSeparateEntries() { Run(log, SiteA, () => throw new InvalidOperationException()); Run(log, SiteA, () => throw new InvalidOperationException()); CheckEqual(2, log.Entries.Count); } public void SameThrowSiteWithDifferentMessages_MakesOneEntry() { Run(log, SiteA, Rethrow, new InvalidOperationException("one")); Run(log, SiteA, Rethrow, new InvalidOperationException("two")); CheckEqual(1, log.Entries.Count); CheckEqual(2, log.Entries[0].Count); } public void SameThrowSiteWithDifferentExceptionTypes_MakesSeparateEntries() { Run(log, SiteA, Rethrow, new InvalidOperationException()); Run(log, SiteA, Rethrow, new ArgumentException()); CheckEqual(2, log.Entries.Count); } public void SameClientThrowAtDifferentDepthsInGlass_MakesOneEntry() { var shallow = new Panel(); shallow.CanvasChildren.Add(new ThrowingControl()); var deep = new Panel(); deep.CanvasChildren.Add(new Panel()).CanvasChildren.Add(new Panel()) .CanvasChildren.Add(new ThrowingControl()); Run(log, SiteA, shallow.CancelMouseInputInSubtree); Run(log, SiteB, deep.CancelMouseInputInSubtree); CheckEqual(1, log.Entries.Count); CheckEqual(2, log.Entries[0].Count); } public void SameThrowSiteReachedFromDifferentClientCode_MakesSeparateEntries() { Run(log, SiteA, () => ThrowFromClientHelper()); Run(log, SiteA, () => ThrowFromClientHelper()); CheckEqual(2, log.Entries.Count); } public void RepeatHit_KeepsTheFirstException() { var first = new InvalidOperationException("first"); Run(log, SiteA, Rethrow, first); Run(log, SiteA, Rethrow, new InvalidOperationException("second")); CheckEqual(first, log.Entries[0].Exception); } public void SecondSiteHittingTheSameBug_JoinsItsEntry() { Hit(log, SiteA); Hit(log, SiteB); CheckEqual(1, log.Entries.Count); CheckEqualCommutative(new[] { SiteA, SiteB }, log.Entries[0].Sites); } public void Always_RecordsThePedigreeAtTheSite() { Run(log, SiteA, Rethrow, new InvalidOperationException(), pedigree: "screen > okButton"); CheckEqual(new[] { "screen > okButton" }, log.Entries[0].PedigreesOf(SiteA).ToArray()); } } public class IsSkippedTests : TestFixture { private ExceptionLog log; public override void TestSetup() { log = new ExceptionLog(); } public void NothingEverThrown_IsNotSkipped() { CheckFalse(log.IsSkipped(SiteA)); } public void SiteOfAnActiveEntry_IsNotSkipped() { Hit(log, SiteA); CheckFalse(log.IsSkipped(SiteA)); } public void SiteOfAnIgnoredEntry_IsSkipped() { Hit(log, SiteA).Ignored = true; CheckTrue(log.IsSkipped(SiteA)); } public void SiteOfASnoozedEntry_IsSkipped() { log.Snooze(Hit(log, SiteA), TimeSpan.FromMilliseconds(10)); log.Advance(TimeSpan.FromMilliseconds(9)); CheckTrue(log.IsSkipped(SiteA)); } public void SiteOfAnExpiredSnooze_IsNotSkipped() { log.Snooze(Hit(log, SiteA), TimeSpan.FromMilliseconds(10)); log.Advance(TimeSpan.FromMilliseconds(10)); CheckFalse(log.IsSkipped(SiteA)); } public void SameTargetInAnotherPhase_IsNotSkipped() { Hit(log, SiteA).Ignored = true; CheckFalse(log.IsSkipped(new ExceptionSite(SiteA.Target, ExceptionPhase.Render))); } public void SiteThatNeverThrew_IsNotSkippedWhileAnotherIs() { Hit(log, SiteA).Ignored = true; CheckFalse(log.IsSkipped(SiteB)); } public void SiteOfAnActiveEntry_IsNotSkippedWhileAnotherEntryIs() { Hit(log, SiteA).Ignored = true; Hit(log, SiteB); CheckFalse(log.IsSkipped(SiteB)); } public void SiteWithAnIgnoredAndAnActiveEntry_IsNotSkipped() { Hit(log, SiteA).Ignored = true; Hit(log, SiteA); CheckFalse(log.IsSkipped(SiteA)); } public void SiteWithEveryEntryIgnoredOrSnoozed_IsSkipped() { Hit(log, SiteA).Ignored = true; log.Snooze(Hit(log, SiteA), TimeSpan.FromSeconds(1)); CheckTrue(log.IsSkipped(SiteA)); } } } }