Skip to content

Commit

Permalink
Discovery provider service
Browse files Browse the repository at this point in the history
  • Loading branch information
gasparnagy committed Jan 22, 2024
1 parent 8aacd1d commit c3ecf32
Show file tree
Hide file tree
Showing 21 changed files with 334 additions and 63 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ namespace Reqnroll.Bindings.Discovery
{
public interface IRuntimeBindingRegistryBuilder
{
Assembly[] GetBindingAssemblies(Assembly testAssembly);
void BuildBindingsFromAssembly(Assembly assembly);
void BuildingCompleted();
}
Expand Down
18 changes: 17 additions & 1 deletion Reqnroll/Bindings/Discovery/RuntimeBindingRegistryBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,36 @@
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using BoDi;
using Reqnroll.Bindings.Reflection;
using Reqnroll.Compatibility;
using Reqnroll.Configuration;
using Reqnroll.Infrastructure;

namespace Reqnroll.Bindings.Discovery
{
public class RuntimeBindingRegistryBuilder : IRuntimeBindingRegistryBuilder
{
private readonly IRuntimeBindingSourceProcessor _bindingSourceProcessor;
private readonly IReqnrollAttributesFilter _reqnrollAttributesFilter;
private readonly IBindingAssemblyLoader _bindingAssemblyLoader;
private readonly ReqnrollConfiguration _reqnrollConfiguration;

public RuntimeBindingRegistryBuilder(IRuntimeBindingSourceProcessor bindingSourceProcessor, IReqnrollAttributesFilter reqnrollAttributesFilter)
public RuntimeBindingRegistryBuilder(IRuntimeBindingSourceProcessor bindingSourceProcessor, IReqnrollAttributesFilter reqnrollAttributesFilter, IBindingAssemblyLoader bindingAssemblyLoader, ReqnrollConfiguration reqnrollConfiguration)
{
_bindingSourceProcessor = bindingSourceProcessor;
_reqnrollAttributesFilter = reqnrollAttributesFilter;
_bindingAssemblyLoader = bindingAssemblyLoader;
_reqnrollConfiguration = reqnrollConfiguration;
}

public Assembly[] GetBindingAssemblies(Assembly testAssembly)
{
var bindingAssemblies = new List<Assembly> { testAssembly };

bindingAssemblies.AddRange(
_reqnrollConfiguration.AdditionalStepAssemblies.Select(_bindingAssemblyLoader.Load));
return bindingAssemblies.ToArray();
}

public void BuildBindingsFromAssembly(Assembly assembly)
Expand Down
23 changes: 23 additions & 0 deletions Reqnroll/Bindings/DryRunBindingInvoker.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using System;
using System.Threading.Tasks;
using Reqnroll.Infrastructure;
using Reqnroll.Tracing;

namespace Reqnroll.Bindings;

#pragma warning disable CS0618 // Type or member is obsolete
public class DryRunBindingInvoker : IAsyncBindingInvoker, IBindingInvoker
#pragma warning restore CS0618 // Type or member is obsolete
{
public object InvokeBinding(IBinding binding, IContextManager contextManager, object[] arguments, ITestTracer testTracer, out TimeSpan duration)
{
duration = TimeSpan.Zero;
return null;
}

public Task<object> InvokeBindingAsync(IBinding binding, IContextManager contextManager, object[] arguments, ITestTracer testTracer, DurationHolder durationHolder)
{
durationHolder.Duration = TimeSpan.Zero;
return Task.FromResult((object)null);
}
}
150 changes: 150 additions & 0 deletions Reqnroll/Bindings/Provider/BindingProviderService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
using System;
using System.Linq;
using System.Reflection;
using BoDi;
using Reqnroll.Bindings.Discovery;
using Reqnroll.Bindings.Provider.Data;
using Reqnroll.Bindings.Reflection;
using Reqnroll.Configuration;
using Reqnroll.Infrastructure;
using SpecFlow.Internal.Json;

namespace Reqnroll.Bindings.Provider;
public class BindingProviderService
{
public static string DiscoverBindings(Assembly testAssembly, string jsonConfiguration)
{
if (string.IsNullOrWhiteSpace(jsonConfiguration)) jsonConfiguration = "{}";
var globalContainer = CreateGlobalContainer(testAssembly, jsonConfiguration);
var bindingRegistryBuilder = globalContainer.Resolve<IRuntimeBindingRegistryBuilder>();
BuildBindingRegistry(testAssembly, bindingRegistryBuilder);
var bindingRegistry = globalContainer.Resolve<IBindingRegistry>();
var resultData = ParseDiscoveryResult(bindingRegistry, testAssembly);
var jsonString = resultData.ToJson();
return jsonString;
}

private static BindingData ParseDiscoveryResult(IBindingRegistry bindingRegistry, Assembly testAssembly)
{
var resultData = new BindingData();

StepDefinitionData CreateStepDefinition(IStepDefinitionBinding stepDefinitionBinding)
{
var stepDefinition = new StepDefinitionData
{
Source = GetSource(stepDefinitionBinding.Method),
Type = stepDefinitionBinding.StepDefinitionType.ToString(),
Regex = stepDefinitionBinding.Regex?.ToString(),
ParamTypes = GetParamTypes(stepDefinitionBinding.Method),
Scope = GetScope(stepDefinitionBinding),
Expression = stepDefinitionBinding.SourceExpression,
Error = stepDefinitionBinding.ErrorMessage
};

return stepDefinition;
}

HookData CreateHook(IHookBinding hookBinding)
{
var hook = new HookData
{
Source = GetSource(hookBinding.Method),
Type = hookBinding.HookType.ToString(),
HookOrder = hookBinding.HookOrder,
Scope = GetScope(hookBinding),
};

return hook;
}

StepArgumentTransformationData CreateStepArgumentTransformation(IStepArgumentTransformationBinding stepArgumentTransformationBinding)
{
var stepArgumentTransformation = new StepArgumentTransformationData
{
Source = GetSource(stepArgumentTransformationBinding.Method),
Name = stepArgumentTransformationBinding.Name,
Regex = stepArgumentTransformationBinding.Regex?.ToString(),
ParamTypes = GetParamTypes(stepArgumentTransformationBinding.Method),
};

return stepArgumentTransformation;
}

string[] GetParamTypes(IBindingMethod bindingMethod)
{
return bindingMethod.Parameters.Select(p => p.Type.FullName).ToArray();
}

BindingScopeData GetScope(IScopedBinding scopedBinding)
{
if (!scopedBinding.IsScoped)
return null;

return new BindingScopeData
{
Tag = scopedBinding.BindingScope.Tag == null
? null
: "@" + scopedBinding.BindingScope.Tag,
FeatureTitle = scopedBinding.BindingScope.FeatureTitle,
ScenarioTitle = scopedBinding.BindingScope.ScenarioTitle
};
}

BindingSourceData GetSource(IBindingMethod bindingMethod)
{
if (bindingMethod is not RuntimeBindingMethod runtimeBindingMethod ||
runtimeBindingMethod.MethodInfo.DeclaringType == null) return null;

var methodInfo = runtimeBindingMethod.MethodInfo;
return new BindingSourceData
{
Method = new BindingSourceMethodData
{
Type = methodInfo.DeclaringType!.FullName,
Assembly = methodInfo.DeclaringType.Assembly == testAssembly ? null : methodInfo.DeclaringType.Assembly.FullName,
MetadataToken = methodInfo.MetadataToken,
FullName = methodInfo.ToString()
}
};
}

resultData.StepDefinitions = bindingRegistry.GetStepDefinitions().Select(CreateStepDefinition).ToArray();
resultData.Hooks = bindingRegistry.GetHooks().Select(CreateHook).ToArray();
resultData.StepArgumentTransformations = bindingRegistry.GetStepTransformations().Select(CreateStepArgumentTransformation).ToArray();
resultData.Errors = bindingRegistry.GetErrorMessages().Select(e => $"{e.Type}: {e.Message}").ToArray();
return resultData;
}

private static void BuildBindingRegistry(Assembly testAssembly, IRuntimeBindingRegistryBuilder bindingRegistryBuilder)
{
var bindingAssemblies = bindingRegistryBuilder.GetBindingAssemblies(testAssembly);
foreach (Assembly assembly in bindingAssemblies)
{
bindingRegistryBuilder.BuildBindingsFromAssembly(assembly);
}
bindingRegistryBuilder.BuildingCompleted();
}

class BindingDiscoveryDependencyProvider : DefaultDependencyProvider
{
public override void RegisterGlobalContainerDefaults(ObjectContainer container)
{
base.RegisterGlobalContainerDefaults(container);
container.RegisterTypeAs<DryRunBindingInvoker, IAsyncBindingInvoker>();
#pragma warning disable CS0618
container.RegisterTypeAs<DryRunBindingInvoker, IBindingInvoker>();
#pragma warning restore CS0618
}
}

private static IObjectContainer CreateGlobalContainer(Assembly testAssembly, string jsonConfiguration)
{
var containerBuilder = new ContainerBuilder(new BindingDiscoveryDependencyProvider())
{
SkipLoadingProvider = true
};
var configurationProvider = new JsonStringRuntimeConfigurationProvider(jsonConfiguration);
return containerBuilder.CreateGlobalContainer(testAssembly, configurationProvider);
}

}
11 changes: 11 additions & 0 deletions Reqnroll/Bindings/Provider/Data/BindingData.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#nullable disable
namespace Reqnroll.Bindings.Provider.Data;

public class BindingData
{
public string[] Errors { get; set; }
public string[] Warnings { get; set; }
public StepDefinitionData[] StepDefinitions { get; set; }
public HookData[] Hooks { get; set; }
public StepArgumentTransformationData[] StepArgumentTransformations { get; set; }
}
9 changes: 9 additions & 0 deletions Reqnroll/Bindings/Provider/Data/BindingScopeData.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#nullable disable
namespace Reqnroll.Bindings.Provider.Data;

public class BindingScopeData
{
public string Tag { get; set; }
public string FeatureTitle { get; set; }
public string ScenarioTitle { get; set; }
}
8 changes: 8 additions & 0 deletions Reqnroll/Bindings/Provider/Data/BindingSourceData.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#nullable disable
namespace Reqnroll.Bindings.Provider.Data;

public class BindingSourceData
{
public BindingSourceMethodData Method { get; set; }
public string SourceLocation { get; set; }
}
10 changes: 10 additions & 0 deletions Reqnroll/Bindings/Provider/Data/BindingSourceMethodData.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#nullable disable
namespace Reqnroll.Bindings.Provider.Data;

public class BindingSourceMethodData
{
public string Type { get; set; } // full type name (with namespace)
public string Assembly { get; set; } // assembly name, null if the assembly is the main test assembly
public string FullName { get; set; } // method name with signature: <return-type> <name>(<param-type1>,<param-type2>)
public int? MetadataToken { get; set; } // MethodInfo.MetadataToken
}
9 changes: 9 additions & 0 deletions Reqnroll/Bindings/Provider/Data/HookData.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#nullable disable
namespace Reqnroll.Bindings.Provider.Data;
public class HookData
{
public BindingSourceData Source { get; set; }
public BindingScopeData Scope { get; set; }
public string Type { get; set; }
public int HookOrder { get; set; }
}
12 changes: 12 additions & 0 deletions Reqnroll/Bindings/Provider/Data/StepArgumentTransformationData.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#nullable disable
namespace Reqnroll.Bindings.Provider.Data;
public class StepArgumentTransformationData
{
public BindingSourceData Source { get; set; }

public string Name { get; set; }

public string Regex { get; set; }

public string[] ParamTypes { get; set; }
}
14 changes: 14 additions & 0 deletions Reqnroll/Bindings/Provider/Data/StepDefinitionData.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#nullable disable
namespace Reqnroll.Bindings.Provider.Data;

public class StepDefinitionData
{
public BindingSourceData Source { get; set; }
public string Type { get; set; }
public string Expression { get; set; }
public string Regex { get; set; }
public string[] ParamTypes { get; set; }
public BindingScopeData Scope { get; set; }

public string Error { get; set; }
}
20 changes: 20 additions & 0 deletions Reqnroll/Configuration/JsonStringRuntimeConfigurationProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using System;
using Reqnroll.Configuration.JsonConfig;

namespace Reqnroll.Configuration;
public class JsonStringRuntimeConfigurationProvider(string jsonConfigFileContent) : IRuntimeConfigurationProvider
{
public ReqnrollConfiguration LoadConfiguration(ReqnrollConfiguration reqnrollConfiguration)
{
if (reqnrollConfiguration == null)
throw new ArgumentNullException(nameof(reqnrollConfiguration));

if (jsonConfigFileContent == null) return reqnrollConfiguration;

if (!jsonConfigFileContent.Trim().StartsWith("{"))
throw new NotSupportedException($"Only JSON config value can be provided! Provided value: {jsonConfigFileContent}");

var jsonConfigurationLoader = new JsonConfigurationLoader();
return jsonConfigurationLoader.LoadJson(reqnrollConfiguration, jsonConfigFileContent);
}
}
10 changes: 6 additions & 4 deletions Reqnroll/Infrastructure/ContainerBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ public class ContainerBuilder : IContainerBuilder

private readonly IDefaultDependencyProvider _defaultDependencyProvider;

public bool SkipLoadingProvider { get; set; } = false;

public ContainerBuilder(IDefaultDependencyProvider defaultDependencyProvider = null)
{
_defaultDependencyProvider = defaultDependencyProvider ?? DefaultDependencyProvider;
Expand Down Expand Up @@ -54,18 +56,18 @@ public virtual IObjectContainer CreateGlobalContainer(Assembly testAssembly, IRu
container.RegisterFromConfiguration(reqnrollConfiguration.CustomDependencies);
}

var unitTestProviderConfigration = container.Resolve<UnitTestProviderConfiguration>();
var unitTestProviderConfiguration = container.Resolve<UnitTestProviderConfiguration>();

LoadPlugins(configurationProvider, container, runtimePluginEvents, reqnrollConfiguration, unitTestProviderConfigration, testAssembly);
LoadPlugins(configurationProvider, container, runtimePluginEvents, reqnrollConfiguration, unitTestProviderConfiguration, testAssembly);

runtimePluginEvents.RaiseConfigurationDefaults(reqnrollConfiguration);

runtimePluginEvents.RaiseRegisterGlobalDependencies(container);

container.RegisterInstanceAs(reqnrollConfiguration);

if (unitTestProviderConfigration != null)
container.RegisterInstanceAs(container.Resolve<IUnitTestRuntimeProvider>(unitTestProviderConfigration.UnitTestProvider ?? ConfigDefaults.UnitTestProviderName));
if (!SkipLoadingProvider)
container.RegisterInstanceAs(container.Resolve<IUnitTestRuntimeProvider>(unitTestProviderConfiguration.UnitTestProvider ?? ConfigDefaults.UnitTestProviderName));

runtimePluginEvents.RaiseCustomizeGlobalDependencies(container, reqnrollConfiguration);

Expand Down
2 changes: 1 addition & 1 deletion Reqnroll/Reqnroll.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@

<Target Name="RunTokenReplace" AfterTargets="GetBuildVersion" BeforeTargets="BeforeCompile" Condition="$(DesignTimeBuild) != 'true' OR '$(BuildingProject)' == 'true'">
<ReplaceTextInFileTask InputFile="$(ProjectDir)Analytics/AppInsights/AppInsightsInstrumentationKey.template.cs" OutputFile="$(ProjectDir)Analytics/AppInsights/AppInsightsInstrumentationKey.cs" TextToReplace="&lt;InstrumentationKeyGoesHere&gt;" TextToReplaceWith="$(AppInsightsInstrumentationKey)" WriteOnlyWhenChanged="true" />
<ItemGroup >
<ItemGroup>
<Compile Include="$(ProjectDir)Analytics/AppInsights/AppInsightsInstrumentationKey.cs" />
</ItemGroup>
</Target>
Expand Down
12 changes: 1 addition & 11 deletions Reqnroll/TestRunnerManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ public virtual ITestRunner CreateTestRunner(string testWorkerId = "default-worke

protected virtual void InitializeBindingRegistry(ITestRunner testRunner)
{
BindingAssemblies = GetBindingAssemblies();
BindingAssemblies = _bindingRegistryBuilder.GetBindingAssemblies(TestAssembly);
BuildBindingRegistry(BindingAssemblies);

void DomainUnload(object sender, EventArgs e)
Expand All @@ -86,16 +86,6 @@ void DomainUnload(object sender, EventArgs e)
AppDomain.CurrentDomain.ProcessExit += DomainUnload;
}

protected virtual Assembly[] GetBindingAssemblies()
{
var bindingAssemblies = new List<Assembly> { TestAssembly };

var assemblyLoader = _globalContainer.Resolve<IBindingAssemblyLoader>();
bindingAssemblies.AddRange(
_reqnrollConfiguration.AdditionalStepAssemblies.Select(assemblyLoader.Load));
return bindingAssemblies.ToArray();
}

protected virtual void BuildBindingRegistry(IEnumerable<Assembly> bindingAssemblies)
{
foreach (Assembly assembly in bindingAssemblies)
Expand Down
Loading

0 comments on commit c3ecf32

Please sign in to comment.