Skip to content

Commit

Permalink
Further work on SpecFlow plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
craigfowler committed Sep 14, 2024
1 parent a3a30cd commit 1895ebe
Show file tree
Hide file tree
Showing 21 changed files with 225 additions and 1,008 deletions.
50 changes: 50 additions & 0 deletions CSF.Screenplay.SpecFlow/ScenarioAndFeatureContextKey.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using System;
using TechTalk.SpecFlow;

namespace CSF.Screenplay
{
/// <summary>
/// Simple key class for a combination of Scenario &amp; Feature contexts.
/// </summary>
internal sealed class ScenarioAndFeatureContextKey : IEquatable<ScenarioAndFeatureContextKey>
{
readonly int cachedHashCode;

/// <summary>Gets the scenario</summary>
public ScenarioContext Scenario { get; }

/// <summary>Gets the feature</summary>
public FeatureContext Feature { get; }

/// <inheritdoc/>
public override bool Equals(object obj) => Equals(obj as ScenarioAndFeatureContextKey);

/// <inheritdoc/>
public bool Equals(ScenarioAndFeatureContextKey other)
{
if(ReferenceEquals(this, other)) return true;
if(ReferenceEquals(null, other)) return false;

return ReferenceEquals(Scenario, other.Scenario) && ReferenceEquals(Feature, other.Feature);
}

/// <inheritdoc/>
public override int GetHashCode() => cachedHashCode;

/// <summary>
/// Initializes a new instance of <see cref="ScenarioAndFeatureContextKey"/>.
/// </summary>
/// <param name="scenario">The scenario</param>
/// <param name="feature">The feature</param>
/// <exception cref="ArgumentNullException">If either parameter is <see langword="null" />.</exception>
public ScenarioAndFeatureContextKey(ScenarioContext scenario, FeatureContext feature)
{
if(scenario is null) throw new ArgumentNullException(nameof(scenario));
if(feature is null) throw new ArgumentNullException(nameof(feature));

Scenario = scenario;
Feature = feature;
cachedHashCode = scenario.GetHashCode() ^ feature.GetHashCode();
}
}
}
59 changes: 59 additions & 0 deletions CSF.Screenplay.SpecFlow/ScreenplayBinding.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
using System;
using Microsoft.Extensions.DependencyInjection;
using TechTalk.SpecFlow;

namespace CSF.Screenplay
{
/// <summary>
/// SpecFlow binding which uses hooks to coordinate the relevant <see cref="Screenplay"/> &amp; <see cref="IPerformance"/> event invokers.
/// </summary>
[Binding]
public class ScreenplayBinding
{
readonly IServiceProvider serviceProvider;

/// <summary>
/// Executed before each scenario.
/// </summary>
[BeforeScenario]
public void BeforeScenario()
{
var performance = serviceProvider.GetRequiredService<IPerformance>();
performance.BeginPerformance();
}

/// <summary>
/// Executed after each scenario.
/// </summary>
[AfterScenario]
public void AfterScenario()
{
var performance = serviceProvider.GetRequiredService<IPerformance>();
var scenarioContext = serviceProvider.GetRequiredService<ScenarioContext>();
var success = scenarioContext.TestError is null;
performance.FinishPerformance(success);
}

/// <summary>
/// Executed before a test run.
/// </summary>
[BeforeTestRun]
public static void BeforeTestRun() => ScreenplayPlugin.Screenplay.BeginScreenplay();

/// <summary>
/// Executed after a test run.
/// </summary>
[AfterTestRun]
public static void AfterTestRun() => ScreenplayPlugin.Screenplay.CompleteScreenplay();

/// <summary>
/// Initialises a new instance of <see cref="ScreenplayBinding"/>.
/// </summary>
/// <param name="serviceProvider">The service provider</param>
/// <exception cref="ArgumentNullException">If the <paramref name="serviceProvider"/> is <see langword="null" />.</exception>
public ScreenplayBinding(IServiceProvider serviceProvider)
{
this.serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
}
}
}
88 changes: 84 additions & 4 deletions CSF.Screenplay.SpecFlow/ScreenplayPlugin.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
using System;
using System.Collections.Concurrent;
using System.Reflection;
using System.Threading;
using BoDi;
using CSF.Screenplay.Actors;
using CSF.Screenplay.Performances;
using TechTalk.SpecFlow;
using TechTalk.SpecFlow.Plugins;
using TechTalk.SpecFlow.UnitTestProvider;

Expand Down Expand Up @@ -34,28 +40,64 @@ namespace CSF.Screenplay
public class ScreenplayPlugin : IRuntimePlugin
{
readonly ReaderWriterLockSlim syncRoot = new ReaderWriterLockSlim();
readonly ConcurrentDictionary<FeatureContext, Guid> featureContextIds = new ConcurrentDictionary<FeatureContext, Guid>();
readonly ConcurrentDictionary<ScenarioAndFeatureContextKey, Guid> scenarioContextIds = new ConcurrentDictionary<ScenarioAndFeatureContextKey, Guid>();

bool initialised;

/// <summary>
/// Provides static access to the Screenplay instance.
/// </summary>
/// <remarks>
/// <para>
/// This is required because the bindings for beginning/ending the Screenplay in <see cref="ScreenplayBinding"/> must be <c>static</c>:
/// <see href="https://docs.specflow.org/projects/specflow/en/latest/Bindings/Hooks.html#supported-hook-attributes"/>.
/// </para>
/// </remarks>
static internal Screenplay Screenplay { get; private set; }

/// <inheritdoc/>
public void Initialize(RuntimePluginEvents runtimePluginEvents,
RuntimePluginParameters runtimePluginParameters,
UnitTestProviderConfiguration unitTestProviderConfiguration)
{
runtimePluginEvents.CustomizeGlobalDependencies += OnCustomizeGlobalDependencies;
throw new NotImplementedException();
runtimePluginEvents.CustomizeScenarioDependencies += OnCustomizeScenarioDependencies;
runtimePluginEvents.ConfigurationDefaults += OnConfigurationDefaults;
}

private void OnConfigurationDefaults(object sender, ConfigurationDefaultsEventArgs e)
{
e.SpecFlowConfiguration.AdditionalStepAssemblies.Add(Assembly.GetExecutingAssembly().FullName);
}

/// <summary>
/// Event handler for the <c>CustomizeGlobalDependencies</c> runtime plugin event.
/// </summary>
/// <remarks>
/// <para>
/// It is a known/documented issue that this event may be triggered more than once in a single run of SpecFlow:
/// <see href="https://github.com/techtalk/SpecFlow/issues/948"/>.
/// Thus, to prevent double-initialisation, this method occurs in a thread-safe manner which ensures that even if it
/// is executed more than once, there is no adverse consequence.
/// </para>
/// </remarks>
/// <param name="sender">The event sender</param>
/// <param name="args">Event args to customize the global dependencies</param>
void OnCustomizeGlobalDependencies(object sender, CustomizeGlobalDependenciesEventArgs args)
{
try
{
syncRoot.EnterUpgradeableReadLock();
if (initialised) return;

syncRoot.EnterWriteLock();
var serviceCollection = new ServiceCollectionAdapter(args.ObjectContainer);
if (initialised) return;

var container = args.ObjectContainer;
var serviceCollection = new ServiceCollectionAdapter(container);
serviceCollection.AddScreenplay();
args.ObjectContainer.RegisterFactoryAs<IServiceProvider>(container => new ServiceProviderAdapter(container));
container.RegisterFactoryAs<IServiceProvider>(c => new ServiceProviderAdapter(c));
Screenplay = container.Resolve<Screenplay>();
initialised = true;
}
finally
Expand All @@ -66,5 +108,43 @@ void OnCustomizeGlobalDependencies(object sender, CustomizeGlobalDependenciesEve
syncRoot.ExitUpgradeableReadLock();
}
}

void OnCustomizeScenarioDependencies(object sender, CustomizeScenarioDependenciesEventArgs args)
{
var container = args.ObjectContainer;
var services = new ServiceProviderAdapter(container);
container.RegisterInstanceAs<IServiceProvider>(services);

var performanceFactory = container.Resolve<ICreatesPerformance>();
var performance = performanceFactory.CreatePerformance();
performance.NamingHierarchy.Add(GetFeatureIdAndName(container));
performance.NamingHierarchy.Add(GetScenarioIdAndName(container));

container.RegisterInstanceAs(performance);
container.RegisterFactoryAs<ICast>(c => new Cast(c.Resolve<IServiceProvider>(), c.Resolve<IPerformance>().PerformanceIdentity));
container.RegisterTypeAs<Stage, IStage>();
}

IdentifierAndName GetFeatureIdAndName(IObjectContainer container)
{
var featureContext = container.Resolve<FeatureContext>();
return new IdentifierAndName(GetFeatureId(featureContext).ToString(),
featureContext.FeatureInfo.Title,
true);
}

Guid GetFeatureId(FeatureContext featureContext) => featureContextIds.GetOrAdd(featureContext, _ => Guid.NewGuid());

IdentifierAndName GetScenarioIdAndName(IObjectContainer container)
{
var featureContext = container.Resolve<FeatureContext>();
var scenarioContext = container.Resolve<ScenarioContext>();
return new IdentifierAndName(GetScenarioId(featureContext, scenarioContext).ToString(),
scenarioContext.ScenarioInfo.Title,
true);
}

Guid GetScenarioId(FeatureContext featureContext, ScenarioContext scenarioContext)
=> scenarioContextIds.GetOrAdd(new ScenarioAndFeatureContextKey(scenarioContext, featureContext), _ => Guid.NewGuid());
}
}
32 changes: 32 additions & 0 deletions CSF.Screenplay.SpecFlow/ScreenplaySteps.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using CSF.Screenplay.Actors;
using TechTalk.SpecFlow;

namespace CSF.Screenplay
{
/// <summary>
/// Convenience subclass of <c>TechTalk.SpecFlow.Steps</c> making it possible to call <c>Given, When &amp; Then</c>
/// methods without conflicting with the built-in methods of the same names.
/// </summary>
public class ScreenplaySteps : Steps
{
/// <summary>
/// Returns the actor instance, as an <see cref="ICanPerformGiven"/>, in order to perform precondition actions.
/// </summary>
/// <param name="actor">The actor.</param>
public ICanPerformGiven Given(Actor actor) => PerformanceStarter.Given(actor);

/// <summary>
/// Returns the actor instance, as an <see cref="ICanPerformWhen"/>, in order to perform actions which exercise the
/// system under test.
/// </summary>
/// <param name="actor">The actor.</param>
public ICanPerformWhen When(Actor actor) => PerformanceStarter.When(actor);

/// <summary>
/// Returns the actor instance, as an <see cref="ICanPerformThen"/>, in order to get information which are required to
/// make assertions that the scenario has completed successfully.
/// </summary>
/// <param name="actor">The actor.</param>
public ICanPerformThen Then(Actor actor) => PerformanceStarter.Then(actor);
}
}
95 changes: 0 additions & 95 deletions CSF.Screenplay.SpecFlow_old/BoDiContainerProxy.cs

This file was deleted.

Loading

0 comments on commit 1895ebe

Please sign in to comment.