Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support context parameters on Before/After methods, FeatureContainer #779

Merged
merged 17 commits into from May 2, 2017
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
9 changes: 0 additions & 9 deletions TechTalk.SpecFlow/Bindings/IBindingInvoker.cs
Expand Up @@ -9,13 +9,4 @@ public interface IBindingInvoker
{
object InvokeBinding(IBinding binding, IContextManager contextManager, object[] arguments, ITestTracer testTracer, out TimeSpan duration);
}

public static class BindingInvokerExtensions
{
public static void InvokeHook(this IBindingInvoker invoker, IHookBinding hookBinding, IContextManager contextManager, ITestTracer testTracer)
{
TimeSpan duration;
invoker.InvokeBinding(hookBinding, contextManager, null, testTracer, out duration);
}
}
}
2 changes: 2 additions & 0 deletions TechTalk.SpecFlow/Bindings/Reflection/RuntimeBindingType.cs
Expand Up @@ -43,5 +43,7 @@ public override int GetHashCode()
{
return (Type != null ? Type.GetHashCode() : 0);
}

public static readonly RuntimeBindingType Void = new RuntimeBindingType(typeof(void));
}
}
20 changes: 13 additions & 7 deletions TechTalk.SpecFlow/FeatureContext.cs
Expand Up @@ -3,19 +3,24 @@
#if SILVERLIGHT
using TechTalk.SpecFlow.Compatibility;
#endif
using System.Threading;

using System.Threading;
using BoDi;
using TechTalk.SpecFlow.Configuration;

namespace TechTalk.SpecFlow
{
public class FeatureContext : SpecFlowContext
{
public FeatureContext(FeatureInfo featureInfo, CultureInfo bindingCulture)
internal FeatureContext(IObjectContainer featureContainer, FeatureInfo featureInfo, RuntimeConfiguration runtimeConfiguration)
{
Stopwatch = new Stopwatch();
Stopwatch.Start();

BindingCulture = bindingCulture;
FeatureContainer = featureContainer;
FeatureInfo = featureInfo;
// The Generator defines the value of FeatureInfo.Language: either feature-language or language from App.config or the default
// The runtime can define the binding-culture: Value is configured on App.config, else it is null
BindingCulture = runtimeConfiguration.BindingCulture ?? featureInfo.Language;
}

#region Singleton
Expand Down Expand Up @@ -48,8 +53,9 @@ internal static void DisableSingletonInstance()
}
#endregion

public FeatureInfo FeatureInfo { get; private set; }
public CultureInfo BindingCulture { get; private set; }
internal Stopwatch Stopwatch { get; private set; }
public FeatureInfo FeatureInfo { get; }
public CultureInfo BindingCulture { get; }
public IObjectContainer FeatureContainer { get; }
internal Stopwatch Stopwatch { get; }
}
}
21 changes: 15 additions & 6 deletions TechTalk.SpecFlow/Infrastructure/ContainerBuilder.cs
Expand Up @@ -13,6 +13,7 @@ public interface IContainerBuilder
IObjectContainer CreateGlobalContainer(IRuntimeConfigurationProvider configurationProvider = null);
IObjectContainer CreateTestThreadContainer(IObjectContainer globalContainer);
IObjectContainer CreateScenarioContainer(IObjectContainer testThreadContainer, ScenarioInfo scenarioInfo);
IObjectContainer CreateFeatureContainer(IObjectContainer testThreadContainer, FeatureInfo featureInfo);
}

public class ContainerBuilder : IContainerBuilder
Expand Down Expand Up @@ -85,12 +86,6 @@ public virtual IObjectContainer CreateScenarioContainer(IObjectContainer testThr
var scenarioContainer = new ObjectContainer(testThreadContainer);
scenarioContainer.RegisterInstanceAs(scenarioInfo);

var contextManager = testThreadContainer.Resolve<IContextManager>();

var featureContext = contextManager.FeatureContext;
if (featureContext != null)
scenarioContainer.RegisterInstanceAs(featureContext);

scenarioContainer.ObjectCreated += obj =>
{
var containerDependentObject = obj as IContainerDependentObject;
Expand All @@ -104,6 +99,20 @@ public virtual IObjectContainer CreateScenarioContainer(IObjectContainer testThr
return scenarioContainer;
}

public IObjectContainer CreateFeatureContainer(IObjectContainer testThreadContainer, FeatureInfo featureInfo)
{
if (testThreadContainer == null)
throw new ArgumentNullException(nameof(testThreadContainer));

var featureContainer = new ObjectContainer(testThreadContainer);
featureContainer.RegisterInstanceAs(featureInfo);
// this registration is needed, otherwise the nested scenario container will create another instance
// is this a BoDi bug?
featureContainer.RegisterTypeAs<FeatureContext, FeatureContext>();

return featureContainer;
}

protected virtual void LoadPlugins(IRuntimeConfigurationProvider configurationProvider, ObjectContainer container, RuntimePluginEvents runtimePluginEvents)
{
// initialize plugins that were registered from code
Expand Down
33 changes: 14 additions & 19 deletions TechTalk.SpecFlow/Infrastructure/ContextManager.cs
Expand Up @@ -14,6 +14,7 @@ private class InternalContextManager<TContext>: IDisposable where TContext : Spe
{
private readonly ITestTracer testTracer;
private TContext instance;
private IDisposable disposable;

public InternalContextManager(ITestTracer testTracer)
{
Expand All @@ -25,14 +26,15 @@ public TContext Instance
get { return instance; }
}

public void Init(TContext newInstance)
public void Init(TContext newInstance, IDisposable newDisposable)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason newDisposable is a IDisposable and not an IObjectContainer?

{
if (instance != null)
{
testTracer.TraceWarning(string.Format("The previous {0} was not disposed.", typeof(TContext).Name));
DisposeInstance();
}
instance = newInstance;
disposable = newDisposable;
}

public void Cleanup()
Expand All @@ -47,8 +49,9 @@ public void Cleanup()

private void DisposeInstance()
{
((IDisposable) instance).Dispose();
disposable.Dispose();
instance = null;
disposable = null;
}

public void Dispose()
Expand Down Expand Up @@ -149,10 +152,11 @@ public ScenarioStepContext StepContext
get{return stepContextManager.Instance;}
}

public void InitializeFeatureContext(FeatureInfo featureInfo, CultureInfo bindingCulture)
public void InitializeFeatureContext(FeatureInfo featureInfo)
{
var newContext = new FeatureContext(featureInfo, bindingCulture);
featureContextManager.Init(newContext);
var featureContainer = containerBuilder.CreateFeatureContainer(testThreadContainer, featureInfo);
var newContext = featureContainer.Resolve<FeatureContext>();
featureContextManager.Init(newContext, featureContainer);
FeatureContext.Current = newContext;
}

Expand All @@ -163,9 +167,9 @@ public void CleanupFeatureContext()

public void InitializeScenarioContext(ScenarioInfo scenarioInfo)
{
var scenarioContainer = containerBuilder.CreateScenarioContainer(testThreadContainer, scenarioInfo);
var scenarioContainer = containerBuilder.CreateScenarioContainer(FeatureContext.FeatureContainer, scenarioInfo);
var newContext = scenarioContainer.Resolve<ScenarioContext>();
scenarioContextManager.Init(newContext);
scenarioContextManager.Init(newContext, scenarioContainer);
ScenarioContext.Current = newContext;

ResetCurrentStepStack();
Expand Down Expand Up @@ -201,18 +205,9 @@ public void CleanupStepContext()

public void Dispose()
{
if (featureContextManager != null)
{
featureContextManager.Dispose();
}
if (scenarioContextManager != null)
{
scenarioContextManager.Dispose();
}
if (stepContextManager != null)
{
stepContextManager.Dispose();
}
featureContextManager?.Dispose();
scenarioContextManager?.Dispose();
stepContextManager?.Dispose();
}
}
}
20 changes: 19 additions & 1 deletion TechTalk.SpecFlow/Infrastructure/IBindingInstanceResolver.cs
Expand Up @@ -3,8 +3,26 @@

namespace TechTalk.SpecFlow.Infrastructure
{
//TODO: rename this to ITestObjectResolver
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do you want to fix this todo as part of this PR?

/// <summary>
/// Resolves user created test objects from different scopes (scenario, feature, test thread).
/// </summary>
/// <remarks>
/// <para>Notes to the implementors:</para>
/// <para>
/// The test objects might be dependent on particular SpecFlow infrastructure, therefore the implemented
/// resolution logic should support resolving the following objects (from the provided SpecFlow container):
/// <see cref="ScenarioContext"/>, <see cref="FeatureContext"/>, <see cref="TestThreadContext"/> and
/// <see cref="IObjectContainer"/> (to be able to resolve any other SpecFlow infrastucture).
/// </para>
/// <para>
/// If the resolved (top level) object implements <see cref="IContainerDependentObject"/>, the method
/// <see cref="IContainerDependentObject.SetObjectContainer"/> must be called, passing in the original
/// SpecFlow container. (The <see cref="Steps"/> base class needs this.)
/// </para>
/// </remarks>
public interface IBindingInstanceResolver
{
object ResolveBindingInstance(Type bindingType, IObjectContainer scenarioContainer);
object ResolveBindingInstance(Type bindingType, IObjectContainer scenarioContainer); //TODO: rename parameter to "container"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do you want to fix this todo as part of this PR?

}
}
2 changes: 1 addition & 1 deletion TechTalk.SpecFlow/Infrastructure/IContextManager.cs
Expand Up @@ -13,7 +13,7 @@ public interface IContextManager
ScenarioStepContext StepContext { get; }
StepDefinitionType? CurrentTopLevelStepDefinitionType { get; }

void InitializeFeatureContext(FeatureInfo featureInfo, CultureInfo bindingCulture);
void InitializeFeatureContext(FeatureInfo featureInfo);
void CleanupFeatureContext();

void InitializeScenarioContext(ScenarioInfo scenarioInfo);
Expand Down
84 changes: 67 additions & 17 deletions TechTalk.SpecFlow/Infrastructure/TestExecutionEngine.cs
Expand Up @@ -2,7 +2,8 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Linq;
using BoDi;
using TechTalk.SpecFlow.BindingSkeletons;
using TechTalk.SpecFlow.Bindings;
using TechTalk.SpecFlow.Bindings.Reflection;
Expand All @@ -28,14 +29,15 @@ public class TestExecutionEngine : ITestExecutionEngine
private readonly IStepErrorHandler[] stepErrorHandlers;
private readonly IBindingInvoker bindingInvoker;
private readonly IStepDefinitionSkeletonProvider stepDefinitionSkeletonProvider;
private readonly IBindingInstanceResolver bindingInstanceResolver;

private ProgrammingLanguage defaultTargetLanguage = ProgrammingLanguage.CSharp;
private CultureInfo defaultBindingCulture = CultureInfo.CurrentCulture;

public TestExecutionEngine(IStepFormatter stepFormatter, ITestTracer testTracer, IErrorProvider errorProvider, IStepArgumentTypeConverter stepArgumentTypeConverter,
RuntimeConfiguration runtimeConfiguration, IBindingRegistry bindingRegistry, IUnitTestRuntimeProvider unitTestRuntimeProvider,
IStepDefinitionSkeletonProvider stepDefinitionSkeletonProvider, IContextManager contextManager, IStepDefinitionMatchService stepDefinitionMatchService,
IDictionary<string, IStepErrorHandler> stepErrorHandlers, IBindingInvoker bindingInvoker)
IDictionary<string, IStepErrorHandler> stepErrorHandlers, IBindingInvoker bindingInvoker, IBindingInstanceResolver bindingInstanceResolver = null, IObjectContainer testThreadContainer = null) //TODO: find a better way to access the container
{
this.errorProvider = errorProvider;
this.bindingInvoker = bindingInvoker;
Expand All @@ -49,6 +51,8 @@ public class TestExecutionEngine : ITestExecutionEngine
this.stepArgumentTypeConverter = stepArgumentTypeConverter;
this.stepErrorHandlers = stepErrorHandlers == null ? null : stepErrorHandlers.Values.ToArray();
this.stepDefinitionMatchService = stepDefinitionMatchService;
this.bindingInstanceResolver = bindingInstanceResolver;
this.TestThreadContainer = testThreadContainer;
}

public FeatureContext FeatureContext
Expand Down Expand Up @@ -82,20 +86,16 @@ public void OnFeatureStart(FeatureInfo featureInfo)
// if the unit test provider would execute the fixture teardown code
// only delayed (at the end of the execution), we automatically close
// the current feature if necessary
if (unitTestRuntimeProvider.DelayedFixtureTearDown &&
contextManager.FeatureContext != null)
if (unitTestRuntimeProvider.DelayedFixtureTearDown && FeatureContext != null)
{
OnFeatureEnd();
}

// The Generator defines the value of FeatureInfo.Language: either feature-language or language from App.config or the default
// The runtime can define the binding-culture: Value is configured on App.config, else it is null
CultureInfo bindingCulture = runtimeConfiguration.BindingCulture ?? featureInfo.Language;

defaultTargetLanguage = featureInfo.GenerationTargetLanguage;
defaultBindingCulture = bindingCulture;
contextManager.InitializeFeatureContext(featureInfo);

contextManager.InitializeFeatureContext(featureInfo, bindingCulture);
defaultBindingCulture = FeatureContext.BindingCulture;
defaultTargetLanguage = featureInfo.GenerationTargetLanguage;
FireEvents(HookType.BeforeFeature);
}

Expand All @@ -105,16 +105,16 @@ public void OnFeatureEnd()
// only delayed (at the end of the execution), we ignore the
// feature-end call, if the feature has been closed already
if (unitTestRuntimeProvider.DelayedFixtureTearDown &&
contextManager.FeatureContext == null)
FeatureContext == null)
return;

FireEvents(HookType.AfterFeature);

if (runtimeConfiguration.TraceTimings)
{
contextManager.FeatureContext.Stopwatch.Stop();
var duration = contextManager.FeatureContext.Stopwatch.Elapsed;
testTracer.TraceDuration(duration, "Feature: " + contextManager.FeatureContext.FeatureInfo.Title);
FeatureContext.Stopwatch.Stop();
var duration = FeatureContext.Stopwatch.Elapsed;
testTracer.TraceDuration(duration, "Feature: " + FeatureContext.FeatureInfo.Title);
}

contextManager.CleanupFeatureContext();
Expand Down Expand Up @@ -218,10 +218,60 @@ private void FireEvents(HookType bindingEvent)
if (eventBinding.IsScoped && !eventBinding.BindingScope.Match(stepContext, out scopeMatches))
continue;

bindingInvoker.InvokeHook(eventBinding, contextManager, testTracer);
InvokeHook(bindingInvoker, eventBinding, bindingEvent);
}
}

protected IObjectContainer TestThreadContainer { get; }

public void InvokeHook(IBindingInvoker invoker, IHookBinding hookBinding, HookType hookType)
{
var currentContainer = GetHookContainer(hookType);
var arguments = ResolveArguments(hookBinding, currentContainer);

TimeSpan duration;
invoker.InvokeBinding(hookBinding, contextManager, arguments, testTracer, out duration);
}

private IObjectContainer GetHookContainer(HookType hookType)
{
IObjectContainer currentContainer;
switch (hookType)
{
case HookType.BeforeTestRun:
case HookType.AfterTestRun:
currentContainer = TestThreadContainer;
break;
case HookType.BeforeFeature:
case HookType.AfterFeature:
currentContainer = FeatureContext.FeatureContainer;
break;
default: // scenario scoped hooks
currentContainer = ScenarioContext.ScenarioContainer;
break;
}
return currentContainer;
}

private object[] ResolveArguments(IHookBinding hookBinding, IObjectContainer currentContainer)
{
if (hookBinding.Method == null || !hookBinding.Method.Parameters.Any())
return null;
return hookBinding.Method.Parameters.Select(p => ResolveArgument(currentContainer, p)).ToArray();
}

private object ResolveArgument(IObjectContainer container, IBindingParameter parameter)
{
if (container == null) throw new ArgumentNullException(nameof(container));
if (parameter == null) throw new ArgumentNullException(nameof(parameter));

var runtimeParameterType = parameter.Type as RuntimeBindingType;
if (runtimeParameterType == null)
throw new SpecFlowException("Parameters can only be resolved for runtime methods.");

return bindingInstanceResolver.ResolveBindingInstance(runtimeParameterType.Type, container);
}

private IOrderedEnumerable<IHookBinding> GetOrderedHooks(HookType bindingEvent)
{
return bindingRegistry.GetHooks(bindingEvent).OrderBy(x => x.HookOrder);
Expand Down Expand Up @@ -309,7 +359,7 @@ protected virtual BindingMatch GetStepMatch(StepInstance stepInstance)
{
List<BindingMatch> candidatingMatches;
StepDefinitionAmbiguityReason ambiguityReason;
var match = stepDefinitionMatchService.GetBestMatch(stepInstance, contextManager.FeatureContext.BindingCulture, out ambiguityReason, out candidatingMatches);
var match = stepDefinitionMatchService.GetBestMatch(stepInstance, FeatureContext.BindingCulture, out ambiguityReason, out candidatingMatches);

if (match.Success)
return match;
Expand All @@ -323,7 +373,7 @@ protected virtual BindingMatch GetStepMatch(StepInstance stepInstance)
throw errorProvider.GetAmbiguousBecauseParamCheckMatchError(candidatingMatches, stepInstance);
}

testTracer.TraceNoMatchingStepDefinition(stepInstance, contextManager.FeatureContext.FeatureInfo.GenerationTargetLanguage, contextManager.FeatureContext.BindingCulture, candidatingMatches);
testTracer.TraceNoMatchingStepDefinition(stepInstance, FeatureContext.FeatureInfo.GenerationTargetLanguage, FeatureContext.BindingCulture, candidatingMatches);
contextManager.ScenarioContext.MissingSteps.Add(stepInstance);
throw errorProvider.GetMissingStepDefinitionError();
}
Expand Down