diff --git a/sln/src/NSpec/Domain/ActChain.cs b/sln/src/NSpec/Domain/ActChain.cs new file mode 100644 index 00000000..9458eb1b --- /dev/null +++ b/sln/src/NSpec/Domain/ActChain.cs @@ -0,0 +1,20 @@ +namespace NSpec.Domain +{ + public class ActChain : TraversingHookChain + { + protected override bool CanRun(nspec instance) + { + return context.BeforeAllChain.AnyThrew() + ? false + : !context.BeforeChain.AnyThrew(); + } + + public ActChain(Context context, Conventions conventions) + : base(context, "act", "actAsync", "act_each") + { + methodSelector = conventions.GetMethodLevelAct; + asyncMethodSelector = conventions.GetAsyncMethodLevelAct; + chainSelector = c => c.ActChain; + } + } +} diff --git a/sln/src/NSpec/Domain/AfterAllChain.cs b/sln/src/NSpec/Domain/AfterAllChain.cs new file mode 100644 index 00000000..420f9e42 --- /dev/null +++ b/sln/src/NSpec/Domain/AfterAllChain.cs @@ -0,0 +1,34 @@ +using System; + +namespace NSpec.Domain +{ + public class AfterAllChain : HookChainBase + { + protected override bool CanRun(nspec instance) + { + return context.BeforeAllChain.AncestorsThrew() + ? false + : context.AnyUnfilteredExampleInSubTree(instance); + } + + public bool AnyThrew() + { + return (AnyException() != null); + } + + public override Exception AnyException() + { + // when hook chain is NOT traversed, build up exceptions along ancestor chain + + // give precedence to Exception closer in the chain + return Exception ?? context.Parent?.AfterAllChain.AnyException(); + } + + public AfterAllChain(Context context, Conventions conventions) + : base(context, "afterAll", "afterAllAsync", "after_all", reversed: true) + { + methodSelector = conventions.GetMethodLevelAfterAll; + asyncMethodSelector = conventions.GetAsyncMethodLevelAfterAll; + } + } +} diff --git a/sln/src/NSpec/Domain/AfterChain.cs b/sln/src/NSpec/Domain/AfterChain.cs new file mode 100644 index 00000000..4c13234a --- /dev/null +++ b/sln/src/NSpec/Domain/AfterChain.cs @@ -0,0 +1,18 @@ +namespace NSpec.Domain +{ + public class AfterChain : TraversingHookChain + { + protected override bool CanRun(nspec instance) + { + return !context.BeforeAllChain.AnyThrew(); + } + + public AfterChain(Context context, Conventions conventions) + : base(context, "after", "afterAsync", "after_each", reversed: true) + { + methodSelector = conventions.GetMethodLevelAfter; + asyncMethodSelector = conventions.GetAsyncMethodLevelAfter; + chainSelector = c => c.AfterChain; + } + } +} diff --git a/sln/src/NSpec/Domain/AsyncActionRegister.cs b/sln/src/NSpec/Domain/AsyncActionRegister.cs index f643a584..f5128615 100644 --- a/sln/src/NSpec/Domain/AsyncActionRegister.cs +++ b/sln/src/NSpec/Domain/AsyncActionRegister.cs @@ -1,7 +1,4 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; using System.Threading.Tasks; namespace NSpec.Domain diff --git a/sln/src/NSpec/Domain/AsyncMethodLevelHooks.cs b/sln/src/NSpec/Domain/AsyncMethodLevelHooks.cs index 95a4bd92..7ac51421 100644 --- a/sln/src/NSpec/Domain/AsyncMethodLevelHooks.cs +++ b/sln/src/NSpec/Domain/AsyncMethodLevelHooks.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using System.Text; -using System.Threading.Tasks; +using System.Reflection; namespace NSpec.Domain { diff --git a/sln/src/NSpec/Domain/AsyncMismatchException.cs b/sln/src/NSpec/Domain/AsyncMismatchException.cs index c5fd09c6..8090f8ed 100644 --- a/sln/src/NSpec/Domain/AsyncMismatchException.cs +++ b/sln/src/NSpec/Domain/AsyncMismatchException.cs @@ -1,8 +1,4 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace NSpec.Domain { diff --git a/sln/src/NSpec/Domain/BeforeAllChain.cs b/sln/src/NSpec/Domain/BeforeAllChain.cs new file mode 100644 index 00000000..424c997e --- /dev/null +++ b/sln/src/NSpec/Domain/BeforeAllChain.cs @@ -0,0 +1,39 @@ +using System; + +namespace NSpec.Domain +{ + public class BeforeAllChain : HookChainBase + { + protected override bool CanRun(nspec instance) + { + return AncestorsThrew() + ? false + : context.AnyUnfilteredExampleInSubTree(instance); + } + + public bool AnyThrew() + { + return (Exception != null || AncestorsThrew()); + } + + public bool AncestorsThrew() + { + return (context.Parent?.BeforeAllChain.AnyThrew() ?? false); + } + + public override Exception AnyException() + { + // when hook chain is NOT traversed, build up exceptions along ancestor chain + + // give precedence to Exception farther up in the chain + return context.Parent?.BeforeAllChain.AnyException() ?? Exception; + } + + public BeforeAllChain(Context context, Conventions conventions) + : base(context, "beforeAll", "beforeAllAsync", "before_all") + { + methodSelector = conventions.GetMethodLevelBeforeAll; + asyncMethodSelector = conventions.GetAsyncMethodLevelBeforeAll; + } + } +} diff --git a/sln/src/NSpec/Domain/BeforeChain.cs b/sln/src/NSpec/Domain/BeforeChain.cs new file mode 100644 index 00000000..25970901 --- /dev/null +++ b/sln/src/NSpec/Domain/BeforeChain.cs @@ -0,0 +1,18 @@ +namespace NSpec.Domain +{ + public class BeforeChain : TraversingHookChain + { + protected override bool CanRun(nspec instance) + { + return !context.BeforeAllChain.AnyThrew(); + } + + public BeforeChain(Context context, Conventions conventions) + : base(context, "before", "beforeAsync", "before_each") + { + methodSelector = conventions.GetMethodLevelBefore; + asyncMethodSelector = conventions.GetAsyncMethodLevelBefore; + chainSelector = c => c.BeforeChain; + } + } +} diff --git a/sln/src/NSpec/Domain/ClassContext.cs b/sln/src/NSpec/Domain/ClassContext.cs index 055a8f16..ad9b54d1 100644 --- a/sln/src/NSpec/Domain/ClassContext.cs +++ b/sln/src/NSpec/Domain/ClassContext.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Reflection; using NSpec.Domain.Extensions; @@ -10,15 +9,15 @@ public class ClassContext : Context { public override void Build(nspec unused = null) { - BuildMethodLevelBefore(); + BeforeAllChain.BuildMethodLevel(classHierarchy); - BuildMethodLevelBeforeAll(); + BeforeChain.BuildMethodLevel(classHierarchy); - BuildMethodLevelAct(); + ActChain.BuildMethodLevel(classHierarchy); - BuildMethodLevelAfter(); + AfterChain.BuildMethodLevel(classHierarchy); - BuildMethodLevelAfterAll(); + AfterAllChain.BuildMethodLevel(classHierarchy); try { @@ -46,96 +45,6 @@ public override bool IsSub(Type baseType) return baseType == SpecType; } - IEnumerable GetMethodsFromHierarchy(Func methodAccessor) - { - return classHierarchyToClass.Select(methodAccessor).Where(mi => mi != null); - } - - void BuildMethodLevelBefore() - { - var befores = GetMethodsFromHierarchy(conventions.GetMethodLevelBefore).ToList(); - - if (befores.Count > 0) - { - BeforeInstance = instance => befores.Do(b => b.Invoke(instance, null)); - } - - var asyncBefores = GetMethodsFromHierarchy(conventions.GetAsyncMethodLevelBefore).ToList(); - - if (asyncBefores.Count > 0) - { - BeforeInstanceAsync = instance => asyncBefores.Do(b => new AsyncMethodLevelBefore(b).Run(instance)); - } - } - - void BuildMethodLevelBeforeAll() - { - var beforeAlls = GetMethodsFromHierarchy(conventions.GetMethodLevelBeforeAll).ToList(); - - if (beforeAlls.Count > 0) - { - BeforeAllInstance = instance => beforeAlls.Do(a => a.Invoke(instance, null)); - } - - var asyncBeforeAlls = GetMethodsFromHierarchy(conventions.GetAsyncMethodLevelBeforeAll).ToList(); - - if (asyncBeforeAlls.Count > 0) - { - BeforeAllInstanceAsync = instance => asyncBeforeAlls.Do(b => new AsyncMethodLevelBeforeAll(b).Run(instance)); - } - } - - void BuildMethodLevelAct() - { - var acts = GetMethodsFromHierarchy(conventions.GetMethodLevelAct).ToList(); - - if (acts.Count > 0) - { - ActInstance = instance => acts.Do(a => a.Invoke(instance, null)); - } - - var asyncActs = GetMethodsFromHierarchy(conventions.GetAsyncMethodLevelAct).ToList(); - - if (asyncActs.Count > 0) - { - ActInstanceAsync = instance => asyncActs.Do(a => new AsyncMethodLevelAct(a).Run(instance)); - } - } - - void BuildMethodLevelAfter() - { - var afters = GetMethodsFromHierarchy(conventions.GetMethodLevelAfter).Reverse().ToList(); - - if (afters.Count > 0) - { - AfterInstance = instance => afters.Do(a => a.Invoke(instance, null)); - } - - var asyncAfters = GetMethodsFromHierarchy(conventions.GetAsyncMethodLevelAfter).Reverse().ToList(); - - if (asyncAfters.Count > 0) - { - AfterInstanceAsync = instance => asyncAfters.Do(a => new AsyncMethodLevelAfter(a).Run(instance)); - } - } - - void BuildMethodLevelAfterAll() - { - var afterAlls = GetMethodsFromHierarchy(conventions.GetMethodLevelAfterAll).Reverse().ToList(); - - if (afterAlls.Count > 0) - { - AfterAllInstance = instance => afterAlls.Do(a => a.Invoke(instance, null)); - } - - var asyncAfterAlls = GetMethodsFromHierarchy(conventions.GetAsyncMethodLevelAfterAll).Reverse().ToList(); - - if (asyncAfterAlls.Count > 0) - { - AfterAllInstanceAsync = instance => asyncAfterAlls.Do(a => new AsyncMethodLevelAfterAll(a).Run(instance)); - } - } - void AddFailingExample(Exception targetEx) { var reportedEx = (targetEx.InnerException != null) @@ -149,6 +58,9 @@ void AddFailingExample(Exception targetEx) var failingExample = new Example(exampleName, action: emptyAction) { + // flag the one and only failing example as being run; + // nothing else is needed: no parents, no childs, no before/after hooks + HasRun = true, Exception = new ContextBareCodeException(reportedEx), }; @@ -157,38 +69,29 @@ void AddFailingExample(Exception targetEx) public override void Run(bool failFast, nspec instance = null, bool recurse = true) { - if (cantCreateInstance) - { - // flag the one and only failing example as being run; - // nothing else is needed: no parents, no childs, no before/after hooks - Examples.Single().HasRun = true; - } - else - { - base.Run(failFast, instance, recurse); - } + if (cantCreateInstance) return; + + base.Run(failFast, instance, recurse); } public ClassContext(Type type, Conventions conventions = null, Tags tagsFilter = null, string tags = null) - : base(type.CleanName(), tags) + : base(type.CleanName(), tags, false, conventions) { this.SpecType = type; - this.conventions = conventions ?? new DefaultConventions().Initialize(); - this.tagsFilter = tagsFilter; - if (type != typeof(nspec)) - { - classHierarchyToClass.AddRange(type.GetAbstractBaseClassChainWithClass()); - } + this.classHierarchy = (type == typeof(nspec)) + ? new List() + : new List(type.GetAbstractBaseClassChainWithClass()); + + cantCreateInstance = false; } public Type SpecType; Tags tagsFilter; - List classHierarchyToClass = new List(); - Conventions conventions; - bool cantCreateInstance = false; + List classHierarchy; + bool cantCreateInstance; } } diff --git a/sln/src/NSpec/Domain/Context.cs b/sln/src/NSpec/Domain/Context.cs index 3f858cb8..0625113a 100644 --- a/sln/src/NSpec/Domain/Context.cs +++ b/sln/src/NSpec/Domain/Context.cs @@ -1,203 +1,20 @@ -using NSpec.Domain.Extensions; -using NSpec.Domain.Formatters; +using NSpec.Domain.Formatters; using System; using System.Collections.Generic; using System.Linq; -using System.Reflection; using System.Threading.Tasks; -using System.Diagnostics; namespace NSpec.Domain { public class Context { - public void RunBefores(nspec instance) - { - // parent chain - - RecurseAncestors(c => c.RunBefores(instance)); - - // class (method-level) - - if (BeforeInstance != null && BeforeInstanceAsync != null) - { - throw new AsyncMismatchException( - "A spec class with all its ancestors cannot set both sync and async " + - "class-level 'before_each' hooks, they should either be all sync or all async"); - } - - BeforeInstance.SafeInvoke(instance); - - BeforeInstanceAsync.SafeInvoke(instance); - - // context-level - - if (Before != null && BeforeAsync != null) - { - throw new AsyncMismatchException( - "A single context cannot set both a 'before' and an 'beforeAsync', please pick one of the two"); - } - - if (Before != null && Before.IsAsync()) - { - throw new AsyncMismatchException( - "'before' cannot be set to an async delegate, please use 'beforeAsync' instead"); - } - - Before.SafeInvoke(); - - BeforeAsync.SafeInvoke(); - } - - void RunBeforeAll(nspec instance) - { - // context-level - - if (BeforeAll != null && BeforeAllAsync != null) - { - throw new AsyncMismatchException( - "A single context cannot set both a 'beforeAll' and an 'beforeAllAsync', please pick one of the two"); - } - - if (BeforeAll != null && BeforeAll.IsAsync()) - { - throw new AsyncMismatchException( - "'beforeAll' cannot be set to an async delegate, please use 'beforeAllAsync' instead"); - } - - BeforeAll.SafeInvoke(); - - BeforeAllAsync.SafeInvoke(); - - // class (method-level) - - if (BeforeAllInstance != null && BeforeAllInstanceAsync != null) - { - throw new AsyncMismatchException( - "A spec class with all its ancestors cannot set both sync and async class-level 'before_all' hooks, they should either be all sync or all async"); - } - - BeforeAllInstance.SafeInvoke(instance); - - BeforeAllInstanceAsync.SafeInvoke(instance); - } - - public void RunActs(nspec instance) - { - // parent chain - - RecurseAncestors(c => c.RunActs(instance)); - - // class (method-level) - - if (ActInstance != null && ActInstanceAsync != null) - { - throw new AsyncMismatchException( - "A spec class with all its ancestors cannot set both sync and async class-level 'act_each' hooks, they should either be all sync or all async"); - } - - ActInstance.SafeInvoke(instance); - - ActInstanceAsync.SafeInvoke(instance); - - // context-level - - if (Act != null && ActAsync != null) - { - throw new AsyncMismatchException( - "A single context cannot set both an 'act' and an 'actAsync', please pick one of the two"); - } - - if (Act != null && Act.IsAsync()) - { - throw new AsyncMismatchException( - "'act' cannot be set to an async delegate, please use 'actAsync' instead"); - } - - Act.SafeInvoke(); - - ActAsync.SafeInvoke(); - } - - public void RunAfters(nspec instance) - { - // context-level - - if (After != null && AfterAsync != null) - { - throw new AsyncMismatchException( - "A single context cannot set both an 'after' and an 'afterAsync', please pick one of the two"); - } - - if (After != null && After.IsAsync()) - { - throw new AsyncMismatchException( - "'after' cannot be set to an async delegate, please use 'afterAsync' instead"); - } - - After.SafeInvoke(); - - AfterAsync.SafeInvoke(); - - // class (method-level) - - if (AfterInstance != null && AfterInstanceAsync != null) - { - throw new AsyncMismatchException( - "A spec class with all its ancestors cannot set both sync and async class-level 'after_each' hooks, they should either be all sync or all async"); - } - - AfterInstance.SafeInvoke(instance); - - AfterInstanceAsync.SafeInvoke(instance); - - // parent chain - - RecurseAncestors(c => c.RunAfters(instance)); - } - - public void RunAfterAll(nspec instance) - { - // context-level - - if (AfterAll != null && AfterAllAsync != null) - { - throw new AsyncMismatchException( - "A single context cannot set both an 'afterAll' and an 'afterAllAsync', please pick one of the two"); - } - - if (AfterAll != null && AfterAll.IsAsync()) - { - throw new AsyncMismatchException( - "'afterAll' cannot be set to an async delegate, please use 'afterAllAsync' instead"); - } - - AfterAll.SafeInvoke(); - - AfterAllAsync.SafeInvoke(); - - // class (method-level) - - if (AfterAllInstance != null && AfterAllInstanceAsync != null) - { - throw new AsyncMismatchException( - "A spec class with all its ancestors cannot set both sync and async class-level 'after_all' hooks, they should either be all sync or all async"); - } - - AfterAllInstance.SafeInvoke(instance); - - AfterAllInstanceAsync.SafeInvoke(instance); - } - public void AddExample(ExampleBase example) { - example.Context = this; - - example.Tags.AddRange(Tags); + example.AddTo(this); Examples.Add(example); - example.Pending |= IsPending(); + runnables.Add(new RunnableExample(this, example)); } public IEnumerable AllExamples() @@ -221,7 +38,7 @@ public void AddContext(Context child) child.Parent = this; - child.Tags.AddRange(child.Parent.Tags); + child.Tags.AddRange(Tags); Contexts.Add(child); } @@ -237,38 +54,28 @@ public virtual void Run(bool failFast, nspec instance = null, bool recurse = tru { if (failFast && Parent.HasAnyFailures()) return; - var nspec = savedInstance ?? instance; - - bool runBeforeAfterAll = !AnyBeforeAllThrew() && AnyUnfilteredExampleInSubTree(nspec); + var runningInstance = builtInstance ?? instance; using (new ConsoleCatcher(output => this.CapturedOutput = output)) { - if (runBeforeAfterAll) RunAndHandleException(RunBeforeAll, nspec, ref ExceptionBeforeAll); + BeforeAllChain.Run(runningInstance); } - // evaluate again, after running this context `beforeAll` - bool anyBeforeAllThrew = AnyBeforeAllThrew(); - // intentionally using for loop to prevent collection was modified error in sample specs - for (int i = 0; i < Examples.Count; i++) + for (int i = 0; i < runnables.Count; i++) { - var example = Examples[i]; - - if (failFast && example.Context.HasAnyFailures()) return; + if (failFast && HasAnyFailures()) return; - using (new ConsoleCatcher(output => example.CapturedOutput = output)) - { - Exercise(example, nspec, anyBeforeAllThrew); - } + runnables[i].Exercise(runningInstance); } if (recurse) { - Contexts.Do(c => c.Run(failFast, nspec, recurse)); + Contexts.Do(c => c.Run(failFast, runningInstance, recurse)); } // TODO wrap this as well in a ConsoleCatcher, not before adding tests about it - if (runBeforeAfterAll) RunAndHandleException(RunAfterAll, nspec, ref ExceptionAfterAll); + AfterAllChain.Run(runningInstance); } /// @@ -281,33 +88,17 @@ public virtual void Run(bool failFast, nspec instance = null, bool recurse = tru /// public virtual void AssignExceptions(bool recurse = true) { - AssignExceptions(null, null, recurse); - } - - protected virtual void AssignExceptions(Exception inheritedBeforeAllException, Exception inheritedAfterAllException, bool recurse) - { - inheritedBeforeAllException = inheritedBeforeAllException ?? ExceptionBeforeAll; - inheritedAfterAllException = ExceptionAfterAll ?? inheritedAfterAllException; - - // if an exception was thrown before the example (either `before` or `act`) but was expected, ignore it - Exception unexpectedException = ClearExpectedException ? null : ExceptionBeforeAct; + var beforeAllException = BeforeAllChain.AnyException(); + var afterAllException = AfterAllChain.AnyException(); - Exception previousException = inheritedBeforeAllException ?? unexpectedException; - Exception followingException = ExceptionAfter ?? inheritedAfterAllException; - - for (int i = 0; i < Examples.Count; i++) + for (int i = 0; i < runnables.Count; i++) { - var example = Examples[i]; - - if (!example.Pending) - { - example.AssignProperException(previousException, followingException); - } + runnables[i].AssignException(beforeAllException, afterAllException); } if (recurse) { - Contexts.Do(c => c.AssignExceptions(inheritedBeforeAllException, inheritedAfterAllException, recurse)); + Contexts.Do(c => c.AssignExceptions(recurse)); } } @@ -322,14 +113,7 @@ public virtual void Write(ILiveFormatter formatter, bool recurse = true) { for (int i = 0; i < Examples.Count; i++) { - var example = Examples[i]; - - if (example.HasRun && !alreadyWritten) - { - WriteAncestors(formatter); - } - - if (example.HasRun) formatter.Write(example, Level); + runnables[i].Write(formatter); } if (recurse) @@ -342,7 +126,7 @@ public virtual void Build(nspec instance = null) { instance.Context = this; - savedInstance = instance; + builtInstance = instance; Contexts.Do(c => c.Build(instance)); } @@ -352,70 +136,6 @@ public string FullContext() return Parent != null ? Parent.FullContext() + ". " + Name : Name; } - static bool RunAndHandleException(Action action, nspec nspec, ref Exception exceptionToSet) - { - bool hasThrown = false; - - try - { - action(nspec); - } - catch (TargetInvocationException invocationException) - { - if (exceptionToSet == null) exceptionToSet = nspec.ExceptionToReturn(invocationException.InnerException); - - hasThrown = true; - } - catch (Exception exception) - { - if (exceptionToSet == null) exceptionToSet = nspec.ExceptionToReturn(exception); - - hasThrown = true; - } - - return hasThrown; - } - - public void Exercise(ExampleBase example, nspec nspec, bool anyBeforeAllThrew) - { - if (example.ShouldSkip(nspec.tagsFilter)) - { - return; - } - - example.HasRun = true; - - if (example.Pending) - { - RunAndHandleException(example.RunPending, nspec, ref example.Exception); - - return; - } - - var stopWatch = example.StartTiming(); - - if (!anyBeforeAllThrew) - { - bool exceptionThrownInBefores = RunAndHandleException(RunBefores, nspec, ref ExceptionBeforeAct); - - if (!exceptionThrownInBefores) - { - RunAndHandleException(RunActs, nspec, ref ExceptionBeforeAct); - - RunAndHandleException(example.Run, nspec, ref example.Exception); - } - - bool exceptionThrownInAfters = RunAndHandleException(RunAfters, nspec, ref ExceptionAfter); - - // when an expected exception is thrown and is set to be cleared by 'expect<>', - // a subsequent exception thrown in 'after' hooks would go unnoticed, so do not clear in this case - - if (exceptionThrownInAfters) ClearExpectedException = false; - } - - example.StopTiming(stopWatch); - } - public virtual bool IsSub(Type baseType) { return false; @@ -423,7 +143,7 @@ public virtual bool IsSub(Type baseType) public nspec GetInstance() { - return savedInstance ?? Parent.GetInstance(); + return builtInstance ?? Parent.GetInstance(); } public IEnumerable AllContexts() @@ -455,44 +175,43 @@ public void TrimSkippedDescendants() Contexts.Do(c => c.TrimSkippedDescendants()); } - bool AnyUnfilteredExampleInSubTree(nspec instance) + public bool AnyUnfilteredExampleInSubTree(nspec instance) { Func shouldNotSkip = e => e.ShouldNotSkip(instance.tagsFilter); - bool anyExampleOrSubExample = Examples.Any(shouldNotSkip) || Contexts.Examples().Any(shouldNotSkip); + bool anyExampleOrSubExample = + Examples.Any(shouldNotSkip) || + Contexts.Examples().Any(shouldNotSkip); return anyExampleOrSubExample; } - bool AnyBeforeAllThrew() - { - return - ExceptionBeforeAll != null || - (Parent != null && Parent.AnyBeforeAllThrew()); - } - public override string ToString() { string levelText = $"L{Level}"; string exampleText = $"{Examples.Count} exm"; - string contextText = $"{Contexts.Count} exm"; + string contextText = $"{Contexts.Count} ctx"; + + var exception = + BeforeAllChain.AnyException() ?? + BeforeChain.AnyException() ?? + ActChain.AnyException() ?? + AfterChain.AnyException() ?? + AfterAllChain.AnyException(); - var exception = ExceptionBeforeAct ?? ExceptionAfter; string exceptionText = exception?.GetType().Name ?? String.Empty; return String.Join(",", new [] { - Name, levelText, exampleText, contextText, exceptionText, + Name, levelText, exampleText, contextText, exceptionText, }); } - void RecurseAncestors(Action ancestorAction) + public void WriteAncestors(ILiveFormatter formatter) { - if (Parent != null) ancestorAction(Parent); - } + if (alreadyWritten) return; - void WriteAncestors(ILiveFormatter formatter) - { + // when hitting root `nspec` context, skip it if (Parent == null) { alreadyWritten = true; @@ -501,35 +220,166 @@ void WriteAncestors(ILiveFormatter formatter) Parent.WriteAncestors(formatter); - if (!alreadyWritten) formatter.Write(this); + formatter.Write(this); alreadyWritten = true; } - public Context(string name = "", string tags = null, bool isPending = false) + // Context-level hook wrappers + + public Action BeforeAll + { + get { return BeforeAllChain.Hook; } + set { BeforeAllChain.Hook = value; } + } + + public Action Before + { + get { return BeforeChain.Hook; } + set { BeforeChain.Hook = value; } + } + + public Action Act + { + get { return ActChain.Hook; } + set { ActChain.Hook = value; } + } + + public Action After + { + get { return AfterChain.Hook; } + set { AfterChain.Hook = value; } + } + + public Action AfterAll + { + get { return AfterAllChain.Hook; } + set { AfterAllChain.Hook = value; } + } + + // Class/method-level hook wrappers + + public Action BeforeAllInstance + { + get { return BeforeAllChain.ClassHook; } + } + + public Action BeforeInstance + { + get { return BeforeChain.ClassHook; } + } + + public Action ActInstance + { + get { return ActChain.ClassHook; } + } + + public Action AfterInstance + { + get { return AfterChain.ClassHook; } + } + + public Action AfterAllInstance + { + get { return AfterAllChain.ClassHook; } + } + + // Context-level async hook wrappers + + public Func BeforeAllAsync + { + get { return BeforeAllChain.AsyncHook; } + set { BeforeAllChain.AsyncHook = value; } + } + + public Func BeforeAsync + { + get { return BeforeChain.AsyncHook; } + set { BeforeChain.AsyncHook = value; } + } + + public Func ActAsync + { + get { return ActChain.AsyncHook; } + set { ActChain.AsyncHook = value; } + } + + public Func AfterAsync + { + get { return AfterChain.AsyncHook; } + set { AfterChain.AsyncHook = value; } + } + + public Func AfterAllAsync + { + get { return AfterAllChain.AsyncHook; } + set { AfterAllChain.AsyncHook = value; } + } + + // Class/method-level async hook wrappers + + public Action BeforeAllInstanceAsync + { + get { return BeforeAllChain.AsyncClassHook; } + } + + public Action BeforeInstanceAsync + { + get { return BeforeChain.AsyncClassHook; } + } + + public Action ActInstanceAsync + { + get { return ActChain.AsyncClassHook; } + } + + public Action AfterInstanceAsync + { + get { return AfterChain.AsyncClassHook; } + } + + public Action AfterAllInstanceAsync + { + get { return AfterAllChain.AsyncClassHook; } + } + + public Context(string name = "", string tags = null, bool isPending = false, Conventions conventions = null) { Name = name.Replace("_", " "); - Examples = new List(); - Contexts = new ContextCollection(); Tags = Domain.Tags.ParseTags(tags); this.isPending = isPending; + + Examples = new List(); + Contexts = new ContextCollection(); + + if (conventions == null) conventions = new DefaultConventions().Initialize(); + + runnables = new List(); + + BeforeAllChain = new BeforeAllChain(this, conventions); + BeforeChain = new BeforeChain(this, conventions); + ActChain = new ActChain(this, conventions); + AfterChain = new AfterChain(this, conventions); + AfterAllChain = new AfterAllChain(this, conventions); } - public string Name; - public int Level; - public List Tags; - public List Examples; - public ContextCollection Contexts; - public Action Before, Act, After, BeforeAll, AfterAll; - public Action BeforeInstance, ActInstance, AfterInstance, BeforeAllInstance, AfterAllInstance; - public Func BeforeAsync, ActAsync, AfterAsync, BeforeAllAsync, AfterAllAsync; - public Action BeforeInstanceAsync, ActInstanceAsync, AfterInstanceAsync, BeforeAllInstanceAsync, AfterAllInstanceAsync; - public Context Parent; - public Exception ExceptionBeforeAll, ExceptionBeforeAct, ExceptionAfter, ExceptionAfterAll; + public string Name { get; protected set; } + public int Level { get; protected set; } + public List Tags { get; protected set; } + public List Examples { get; protected set; } + public ContextCollection Contexts { get; protected set; } + public BeforeAllChain BeforeAllChain { get; protected set; } + public BeforeChain BeforeChain { get; protected set; } + public ActChain ActChain { get; protected set; } + public AfterChain AfterChain { get; protected set; } + public AfterAllChain AfterAllChain { get; protected set; } public bool ClearExpectedException; - public string CapturedOutput; + public string CapturedOutput { get; protected set; } + public Context Parent { get; protected set; } - nspec savedInstance; - bool alreadyWritten, isPending; + protected List runnables; + protected nspec builtInstance; + protected bool alreadyWritten; + protected bool isPending; } -} \ No newline at end of file +} diff --git a/sln/src/NSpec/Domain/ContextBuilder.cs b/sln/src/NSpec/Domain/ContextBuilder.cs index ccf6916a..21278b50 100644 --- a/sln/src/NSpec/Domain/ContextBuilder.cs +++ b/sln/src/NSpec/Domain/ContextBuilder.cs @@ -33,7 +33,7 @@ public ClassContext CreateClassContext(Type type) type.GetAbstractBaseClassChainWithClass() .Where(s => s != type) - .Each(s => tagAttributes.Add(new TagAttribute(s.Name))); + .Do(s => tagAttributes.Add(new TagAttribute(s.Name))); var tags = TagStringFor(tagAttributes); diff --git a/sln/src/NSpec/Domain/ContextUtils.cs b/sln/src/NSpec/Domain/ContextUtils.cs new file mode 100644 index 00000000..de6d0745 --- /dev/null +++ b/sln/src/NSpec/Domain/ContextUtils.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace NSpec.Domain +{ + public static class ContextUtils + { + public static List GetMethodsFromHierarchy( + List classHierarchy, Func selectMethod) + { + return classHierarchy + .Select(selectMethod) + .Where(m => m != null) + .ToList(); + } + + public static bool RunAndHandleException(Action action, nspec instance, ref Exception targetException) + { + bool hasThrown = false; + Exception exceptionToSet = null; + + try + { + action(instance); + } + catch (TargetInvocationException invocationException) + { + exceptionToSet = instance.ExceptionToReturn(invocationException.InnerException); + + hasThrown = true; + } + catch (Exception exception) + { + exceptionToSet = instance.ExceptionToReturn(exception); + + hasThrown = true; + } + + if (targetException == null && exceptionToSet != null) + { + targetException = exceptionToSet; + } + + return hasThrown; + } + } +} diff --git a/sln/src/NSpec/Domain/ExampleBase.cs b/sln/src/NSpec/Domain/ExampleBase.cs index 4676bdb3..00646f6e 100644 --- a/sln/src/NSpec/Domain/ExampleBase.cs +++ b/sln/src/NSpec/Domain/ExampleBase.cs @@ -24,6 +24,7 @@ public static string Parse(Expression expressionBody) { sentence = Regex.Replace(sentence, parensPattern, @"$1"); } + sentence = Regex.Replace(sentence, otherSeparatorsPattern, " "); sentence = Regex.Replace(sentence, commmasPattern, ","); sentence = Regex.Replace(sentence, multiSpacesPattern, " "); @@ -36,15 +37,21 @@ public static string Parse(Expression exp) return Parse(exp.Body); } + public void AddTo(Context context) + { + Context = context; + + Tags.AddRange(context.Tags); + + Pending |= context.IsPending(); + } + public abstract void Run(nspec nspec); public abstract void RunPending(nspec nspec); public abstract bool IsAsync { get; } public abstract MethodInfo BodyMethodInfo { get; } - public TimeSpan Duration { get; set; } - public string CapturedOutput { get; set; } - public string FullName() { return Context.FullContext() + ". " + Spec + "."; @@ -70,45 +77,6 @@ public void StopTiming(Stopwatch stopWatch) Duration = stopWatch.Elapsed; } - public void AssignProperException(Exception previousException, Exception followingException) - { - if (previousException == null && followingException == null) - { - // stick with whatever exception may or may not be set on this example - return; - } - - if (previousException != null) - { - var contextException = previousException; - - if (Exception != null && Exception.GetType() != typeof(ExceptionNotThrown)) - { - Exception = new ExampleFailureException( - "Context Failure: " + contextException.Message + ", Example Failure: " + Exception.Message, - contextException); - } - - if (Exception == null) - { - Exception = new ExampleFailureException( - "Context Failure: " + contextException.Message, - contextException); - } - } - else - { - var contextException = followingException; - - if (Exception == null) - { - Exception = new ExampleFailureException( - "Context Failure: " + contextException.Message, - contextException); - } - } - } - public bool ShouldSkip(Tags tagsFilter) { return tagsFilter.ShouldSkip(Tags); @@ -137,11 +105,13 @@ public ExampleBase(string name = "", string tags = "", bool pending = false) Pending = pending; } - public bool Pending; + public TimeSpan Duration { get; protected set; } + public string CapturedOutput; + public bool Pending { get; protected set; } public bool HasRun; - public string Spec; - public List Tags; + public string Spec { get; protected set; } + public List Tags { get; protected set; } public Exception Exception; - public Context Context; + public Context Context { get; protected set; } } } \ No newline at end of file diff --git a/sln/src/NSpec/Domain/ExampleFailureException.cs b/sln/src/NSpec/Domain/ExampleFailureException.cs index f3bde9dd..50b04007 100644 --- a/sln/src/NSpec/Domain/ExampleFailureException.cs +++ b/sln/src/NSpec/Domain/ExampleFailureException.cs @@ -4,7 +4,23 @@ namespace NSpec.Domain { public class ExampleFailureException : Exception { + public static ExampleFailureException FromContext(Exception contextException) + { + return new ExampleFailureException( + $"Context Failure: {contextException.Message}", + contextException); + } + + public static ExampleFailureException FromContextAndExample( + Exception contextException, Exception exampleException) + { + return new ExampleFailureException( + $"Context Failure: {contextException.Message}, Example Failure: {exampleException.Message}", + contextException); + } + public ExampleFailureException(string message, Exception innerException) - : base(message, innerException) {} + : base(message, innerException) + { } } -} \ No newline at end of file +} diff --git a/sln/src/NSpec/Domain/Extensions/DomainExtensions.cs b/sln/src/NSpec/Domain/Extensions/DomainExtensions.cs index ca8a9809..2e6a25ff 100644 --- a/sln/src/NSpec/Domain/Extensions/DomainExtensions.cs +++ b/sln/src/NSpec/Domain/Extensions/DomainExtensions.cs @@ -74,14 +74,6 @@ public static string CleanMessage(this Exception exception) return exc; } - public static void Each(this IEnumerable enumerable, Action action) - { - foreach (var t in enumerable) - { - action(t); - } - } - public static bool IsAsync(this MethodInfo method) { // Inspired from: https://github.com/nunit/nunit/blob/master/src/NUnitFramework/framework/Internal/AsyncInvocationRegion.cs diff --git a/sln/src/NSpec/Domain/HookChainBase.cs b/sln/src/NSpec/Domain/HookChainBase.cs new file mode 100644 index 00000000..2332a68f --- /dev/null +++ b/sln/src/NSpec/Domain/HookChainBase.cs @@ -0,0 +1,124 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Threading.Tasks; +using NSpec.Domain.Extensions; + +namespace NSpec.Domain +{ + public abstract class HookChainBase + { + public void BuildMethodLevel(List classHierarchy) + { + var methods = ContextUtils.GetMethodsFromHierarchy(classHierarchy, methodSelector); + + var asyncMethods = ContextUtils.GetMethodsFromHierarchy(classHierarchy, asyncMethodSelector); + + if (reversed) + { + methods.Reverse(); + asyncMethods.Reverse(); + } + + if (methods.Count > 0) + { + ClassHook = instance => methods.Do(m => m.Invoke(instance, null)); + } + + if (asyncMethods.Count > 0) + { + AsyncClassHook = instance => asyncMethods.Do(m => new AsyncMethodLevelBefore(m).Run(instance)); + } + } + + protected abstract bool CanRun(nspec instance); + + public void Run(nspec instance) + { + if (CanRun(instance)) + { + ContextUtils.RunAndHandleException(InvokeHooks, instance, ref exception); + } + } + + protected virtual void InvokeHooks(nspec instance) + { + // class (method-level) + InvokeClassHooks(instance); + + // context-level + InvokeContextHooks(); + } + + protected void InvokeClassHooks(nspec instance) + { + // class (method-level) + + if (ClassHook != null && AsyncClassHook != null) + { + throw new AsyncMismatchException( + $"A spec class with all its ancestors cannot set both sync and async " + + $"class-level '{classHookName}' hooks, they should either be all sync or all async"); + } + + ClassHook.SafeInvoke(instance); + + AsyncClassHook.SafeInvoke(instance); + } + + protected void InvokeContextHooks() + { + // context-level + + if (Hook != null && AsyncHook != null) + { + throw new AsyncMismatchException( + $"A single context cannot set both a '{hookName}' and an '{asyncHookName}', please pick one of the two"); + } + + if (Hook != null && Hook.IsAsync()) + { + throw new AsyncMismatchException( + $"'{hookName}' cannot be set to an async delegate, please use '{asyncHookName}' instead"); + } + + Hook.SafeInvoke(); + + AsyncHook.SafeInvoke(); + } + + public Exception Exception + { + get { return exception; } + protected set { exception = value; } + } + + public abstract Exception AnyException(); + + public HookChainBase(Context context, + string hookName, string asyncHookName, string classHookName, bool reversed = false) + { + this.context = context; + this.hookName = hookName; + this.asyncHookName = asyncHookName; + this.classHookName = classHookName; + this.reversed = reversed; + } + + public Action Hook; + public Func AsyncHook; + + public Action ClassHook { get; protected set; } + public Action AsyncClassHook { get; protected set; } + + protected Exception exception; + protected Func methodSelector; + protected Func asyncMethodSelector; + + protected readonly Context context; + protected readonly bool reversed; + protected readonly string hookName; + protected readonly string asyncHookName; + protected readonly string classHookName; + } +} diff --git a/sln/src/NSpec/Domain/MethodContext.cs b/sln/src/NSpec/Domain/MethodContext.cs index 9456d89a..5bebd71f 100644 --- a/sln/src/NSpec/Domain/MethodContext.cs +++ b/sln/src/NSpec/Domain/MethodContext.cs @@ -19,12 +19,6 @@ public override void Build(nspec instance) } } - public MethodContext(MethodInfo method, string tags = null) - : base(method.Name, tags) - { - this.method = method; - } - static void AddFailingExample(nspec instance, Exception targetEx) { var reportedEx = (targetEx.InnerException != null) @@ -36,6 +30,12 @@ static void AddFailingExample(nspec instance, Exception targetEx) instance.it[exampleName] = () => { throw new ContextBareCodeException(reportedEx); }; } + public MethodContext(MethodInfo method, string tags = null) + : base(method.Name, tags) + { + this.method = method; + } + MethodInfo method; } } diff --git a/sln/src/NSpec/Domain/RunnableExample.cs b/sln/src/NSpec/Domain/RunnableExample.cs new file mode 100644 index 00000000..c6754a01 --- /dev/null +++ b/sln/src/NSpec/Domain/RunnableExample.cs @@ -0,0 +1,123 @@ +using System; +using NSpec.Domain.Formatters; + +namespace NSpec.Domain +{ + public class RunnableExample + { + public void Exercise(nspec instance) + { + if (example.ShouldSkip(instance.tagsFilter)) + { + return; + } + + example.HasRun = true; + + if (example.Pending) + { + ContextUtils.RunAndHandleException(example.RunPending, instance, ref example.Exception); + + return; + } + + var stopWatch = example.StartTiming(); + + using (new ConsoleCatcher(output => example.CapturedOutput = output)) + { + context.BeforeChain.Run(instance); + + context.ActChain.Run(instance); + + if (CanRun()) + { + ContextUtils.RunAndHandleException(example.Run, instance, ref example.Exception); + } + + context.AfterChain.Run(instance); + } + + // when an expected exception is thrown and is set to be cleared by 'expect<>', + // a subsequent exception thrown in 'after' hooks would go unnoticed, so do not clear in this case + + if (context.AfterChain.AnyThrew()) context.ClearExpectedException = false; + + example.StopTiming(stopWatch); + } + + bool CanRun() + { + return + !context.BeforeAllChain.AnyThrew() && + !context.BeforeChain.AnyThrew(); + } + + public void AssignException(Exception beforeAllException, Exception afterAllException) + { + if (example.Pending) return; + + // if an exception was thrown before the example (only in `act`) but was expected, ignore it + Exception unexpectedException = context.ClearExpectedException + ? null + : context.ActChain.AnyException(); + + Exception previousException = + beforeAllException ?? + context.BeforeChain.AnyException() ?? + unexpectedException; + + Exception followingException = + context.AfterChain.AnyException() ?? + afterAllException; + + if (previousException == null && followingException == null) + { + // stick with whatever exception may or may not be set on this example + return; + } + + if (previousException != null) + { + if (example.Exception != null && example.Exception.GetType() != typeof(ExceptionNotThrown)) + { + example.Exception = ExampleFailureException + .FromContextAndExample(previousException, example.Exception); + + return; + } + + if (example.Exception == null) + { + example.Exception = ExampleFailureException + .FromContext(previousException); + + return; + } + } + + if (example.Exception == null) + { + example.Exception = ExampleFailureException.FromContext(followingException); + } + } + + public void Write(ILiveFormatter formatter) + { + if (example.HasRun) + { + context.WriteAncestors(formatter); + + formatter.Write(example, context.Level); + } + } + + public RunnableExample(Context context, ExampleBase example) + { + this.context = context; + this.example = example; + } + + readonly Context context; + readonly ExampleBase example; + } +} diff --git a/sln/src/NSpec/Domain/TraversingHookChain.cs b/sln/src/NSpec/Domain/TraversingHookChain.cs new file mode 100644 index 00000000..66e9a221 --- /dev/null +++ b/sln/src/NSpec/Domain/TraversingHookChain.cs @@ -0,0 +1,53 @@ +using System; + +namespace NSpec.Domain +{ + public abstract class TraversingHookChain : HookChainBase + { + protected override void InvokeHooks(nspec instance) + { + if (!reversed) + { + // parent chain first, then current chain + + RecurseAncestors(c => chainSelector(c).InvokeHooks(instance)); + + base.InvokeHooks(instance); + } + else + { + // current chain first, then parent chain + + base.InvokeHooks(instance); + + RecurseAncestors(c => chainSelector(c).InvokeHooks(instance)); + } + } + + protected void RecurseAncestors(Action ancestorAction) + { + if (context.Parent != null) ancestorAction(context.Parent); + } + + public override Exception AnyException() + { + // when hook chain is traversed, this chain exception holds any ancestor exception + + return Exception; + } + + public bool AnyThrew() + { + // when hook chain is traversed, this chain exception holds any ancestor exception + + return (Exception != null); + } + + public TraversingHookChain(Context context, + string hookName, string asyncHookName, string classHookName, bool reversed = false) + : base(context, hookName, asyncHookName, classHookName, reversed) + { } + + protected Func chainSelector; + } +} diff --git a/sln/src/NSpec/nspec.cs b/sln/src/NSpec/nspec.cs index 2b7873a9..33f1d000 100644 --- a/sln/src/NSpec/nspec.cs +++ b/sln/src/NSpec/nspec.cs @@ -309,10 +309,11 @@ public virtual Action expect(string expectedMessage) where T : Exception return () => { - if (specContext.ExceptionBeforeAct == null) - throw new ExceptionNotThrown(IncorrectType()); + var actException = specContext.ActChain.AnyException(); + + if (actException == null) throw new ExceptionNotThrown(IncorrectType()); - AssertExpectedException(specContext.ExceptionBeforeAct, expectedMessage); + AssertExpectedException(actException, expectedMessage); // do not clear exception right now, during first phase, but leave a note for second phase specContext.ClearExpectedException = true; diff --git a/sln/test/NSpec.Tests/ExampleBaseWrap.cs b/sln/test/NSpec.Tests/ExampleBaseWrap.cs index b16e1d83..ced48bee 100644 --- a/sln/test/NSpec.Tests/ExampleBaseWrap.cs +++ b/sln/test/NSpec.Tests/ExampleBaseWrap.cs @@ -10,15 +10,10 @@ namespace NSpec.Tests /// class ExampleBaseWrap : ExampleBase { - public ExampleBaseWrap(string name) - : base(name) + public ExampleBaseWrap(string name = "", bool pending = false) + : base(name, pending: pending) { } - - public ExampleBaseWrap() - { - } - public override void Run(nspec nspec) { throw new NotImplementedException(); diff --git a/sln/test/NSpec.Tests/WrittenContext.cs b/sln/test/NSpec.Tests/WrittenContext.cs index b00fd1fe..dee02618 100644 --- a/sln/test/NSpec.Tests/WrittenContext.cs +++ b/sln/test/NSpec.Tests/WrittenContext.cs @@ -14,10 +14,16 @@ public WrittenContext(Context context) Name = context.Name; Level = context.Level; Tags = new List(context.Tags); - ExceptionBeforeAll = context.ExceptionBeforeAll; - ExceptionBeforeAct = context.ExceptionBeforeAct; - ExceptionAfter = context.ExceptionAfter; - ExceptionAfterAll = context.ExceptionAfterAll; + BeforeAllException = context.BeforeAllChain.Exception; + AnyBeforeAllException = context.BeforeAllChain.AnyException(); + BeforeException = context.BeforeChain.Exception; + AnyBeforeException = context.BeforeChain.AnyException(); + ActException = context.ActChain.Exception; + AnyActException = context.ActChain.AnyException(); + AfterException = context.AfterChain.Exception; + AnyAfterException = context.AfterChain.AnyException(); + AfterAllException = context.AfterAllChain.Exception; + AnyAfterAllException = context.AfterAllChain.AnyException(); ClearExpectedException = context.ClearExpectedException; CapturedOutput = context.CapturedOutput; IsPending = context.IsPending(); @@ -32,13 +38,20 @@ public WrittenContext(Context context) public List Tags { get; private set; } - public Exception ExceptionBeforeAll { get; private set; } + public Exception BeforeAllException { get; private set; } + public Exception AnyBeforeAllException { get; private set; } - public Exception ExceptionBeforeAct { get; private set; } + public Exception BeforeException { get; private set; } + public Exception AnyBeforeException { get; private set; } - public Exception ExceptionAfter { get; private set; } + public Exception ActException { get; private set; } + public Exception AnyActException { get; private set; } - public Exception ExceptionAfterAll { get; private set; } + public Exception AfterException { get; private set; } + public Exception AnyAfterException { get; private set; } + + public Exception AfterAllException { get; private set; } + public Exception AnyAfterAllException { get; private set; } public bool ClearExpectedException { get; private set; } diff --git a/sln/test/NSpec.Tests/describe_Context.cs b/sln/test/NSpec.Tests/describe_Context.cs index a38b148e..ba02d694 100644 --- a/sln/test/NSpec.Tests/describe_Context.cs +++ b/sln/test/NSpec.Tests/describe_Context.cs @@ -76,7 +76,7 @@ public void setup() [Test] public void should_run_the_acts_in_the_right_order() { - childContext.RunActs(instance); + childContext.ActChain.Run(instance); instance.actResult.Should().Be("parentchild"); } @@ -178,7 +178,7 @@ public void setup() [Test] public void should_run_the_befores_in_the_proper_order() { - childContext.RunBefores(instance); + childContext.BeforeChain.Run(instance); instance.beforeResult.Should().Be("parentchild"); } diff --git a/sln/test/NSpec.Tests/describe_ContextCollection.cs b/sln/test/NSpec.Tests/describe_ContextCollection.cs index cd2f21d9..45e39bb2 100644 --- a/sln/test/NSpec.Tests/describe_ContextCollection.cs +++ b/sln/test/NSpec.Tests/describe_ContextCollection.cs @@ -21,7 +21,7 @@ public void setup() context.AddExample(new ExampleBaseWrap()); - context.AddExample(new ExampleBaseWrap { Pending = true }); + context.AddExample(new ExampleBaseWrap(pending: true)); context.AddExample(new ExampleBaseWrap { Exception = new KnownException() });