Skip to content

Commit

Permalink
Fix bug with generator plugin loader when using plugin path and add s…
Browse files Browse the repository at this point in the history
…upport for global nuget packages (#1045)

* Fix bug with generator plugin loader when using plugin path and add support for global nuget packages

* Add better assertion message to unit tests for generator plugin loader

* Fix issue with tests not using correct assembly version

* Split GeneratorPluginLoader and GeneratorPluginLocator

* Remove global nuget generator probing and simplify environment variable expansion

* Update changelog

* Cleanup code and add test for relative plugin paths
  • Loading branch information
ChristopherHaws authored and SabotageAndi committed Mar 22, 2018
1 parent 97dcb79 commit eeeb80e
Show file tree
Hide file tree
Showing 13 changed files with 703 additions and 108 deletions.
11 changes: 7 additions & 4 deletions TechTalk.SpecFlow.Generator/DefaultDependencyProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
using TechTalk.SpecFlow.Generator.Plugins;
using TechTalk.SpecFlow.Generator.UnitTestConverter;
using TechTalk.SpecFlow.Tracing;
using TechTalk.SpecFlow.Utils;
using TechTalk.SpecFlow.Utils;

namespace TechTalk.SpecFlow.Generator
{
Expand All @@ -14,13 +14,16 @@ internal partial class DefaultDependencyProvider
partial void RegisterUnitTestGeneratorProviders(ObjectContainer container);

public virtual void RegisterDefaults(ObjectContainer container)
{
{
container.RegisterTypeAs<FileSystem, IFileSystem>();

container.RegisterTypeAs<GeneratorConfigurationProvider, IGeneratorConfigurationProvider>();
container.RegisterTypeAs<InProcGeneratorInfoProvider, IGeneratorInfoProvider>();
container.RegisterTypeAs<TestGenerator, ITestGenerator>();
container.RegisterTypeAs<TestHeaderWriter, ITestHeaderWriter>();
container.RegisterTypeAs<TestUpToDateChecker, ITestUpToDateChecker>();

container.RegisterTypeAs<TestUpToDateChecker, ITestUpToDateChecker>();

container.RegisterTypeAs<GeneratorPluginLocator, IGeneratorPluginLocator>();
container.RegisterTypeAs<GeneratorPluginLoader, IGeneratorPluginLoader>();
container.RegisterTypeAs<DefaultListener, ITraceListener>();

Expand Down
108 changes: 7 additions & 101 deletions TechTalk.SpecFlow.Generator/Plugins/GeneratorPluginLoader.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using TechTalk.SpecFlow.Generator.Interfaces;
using TechTalk.SpecFlow.Infrastructure;
Expand All @@ -11,39 +8,18 @@ namespace TechTalk.SpecFlow.Generator.Plugins
{
public class GeneratorPluginLoader : IGeneratorPluginLoader
{
/*
* Loading logic:
* <plugin-folder> = @path | <generator-folder> | <nuget-plugin-folder>
*
* <configured-path> = @path | <project-folder>\@path
*
* <nuget-plugin-folder> = <nuget-packages>\<plugin-name>.SpecFlowPlugin.<nuget-package-version> | <nuget-packages>\<plugin-name>.SpecFlow.<nuget-package-version> | <nuget-packages>\<plugin-name>.<nuget-package-version> // match first for the one with <specflow-version>
*
* <specflow-version> = n-n[-n] // e.g. 1-8-1
*
* <nuget-packages> = <generator-folder>\..\.. // assuming that SpecFlow was installed with nuget, generator is in the "tools" folder
*
* <nuget-package-version> = latest-of: n(.n)*[-tag]
*
* <plugin-generator-folder> = <plugin-folder> | <plugin-folder>\tools\SpecFlowPlugin[.<specflow-version>] | <plugin-folder>\tools | <plugin-folder>\lib\net45 | <plugin-folder>\lib\net40 | <plugin-folder>\lib\net35 | <plugin-folder>\lib
*
* <generator-plugin-assembly> = <plugin-generator-folder>\<plugin-name>.Generator.SpecFlowPlugin.dll | <generator-plugin-folder>\<plugin-name>.SpecFlowPlugin.dll
*/

private readonly string generatorFolder;
private readonly ProjectSettings projectSettings;
private readonly IGeneratorPluginLocator generatorPluginLocator;

public GeneratorPluginLoader(ProjectSettings projectSettings)
public GeneratorPluginLoader(ProjectSettings projectSettings, IGeneratorPluginLocator generatorPluginLocator)
{
this.projectSettings = projectSettings;
this.generatorFolder = Path.GetDirectoryName(new Uri(Assembly.GetExecutingAssembly().CodeBase).LocalPath);
this.generatorPluginLocator = generatorPluginLocator;
}

public IGeneratorPlugin LoadPlugin(PluginDescriptor pluginDescriptor)
{
var generatorPluginAssemblyPath = GetGeneratorPluginAssemblies(pluginDescriptor).FirstOrDefault();
if (generatorPluginAssemblyPath == null)
throw new SpecFlowException(string.Format("Unable to find plugin in the plugin search path: {0}. Please check http://go.specflow.org/doc-plugins for details.", pluginDescriptor.Name));
var generatorPluginAssemblyPath = this.generatorPluginLocator.LocatePluginAssembly(pluginDescriptor);

Assembly pluginAssembly;
try
Expand All @@ -52,15 +28,15 @@ public IGeneratorPlugin LoadPlugin(PluginDescriptor pluginDescriptor)
}
catch(Exception ex)
{
throw new SpecFlowException(string.Format("Unable to load plugin assembly: {0}. Please check http://go.specflow.org/doc-plugins for details.", generatorPluginAssemblyPath), ex);
throw new SpecFlowException($"Unable to load plugin assembly: {generatorPluginAssemblyPath}. Please check http://go.specflow.org/doc-plugins for details.", ex);
}

var pluginAttribute = (GeneratorPluginAttribute)Attribute.GetCustomAttribute(pluginAssembly, typeof(GeneratorPluginAttribute));
if (pluginAttribute == null)
throw new SpecFlowException("Missing [assembly:GeneratorPlugin] attribute in " + generatorPluginAssemblyPath);

if (!typeof(IGeneratorPlugin).IsAssignableFrom((pluginAttribute.PluginType)))
throw new SpecFlowException(string.Format("Invalid plugin attribute in {0}. Plugin type must implement IGeneratorPlugin. Please check http://go.specflow.org/doc-plugins for details.", generatorPluginAssemblyPath));
throw new SpecFlowException($"Invalid plugin attribute in {generatorPluginAssemblyPath}. Plugin type must implement IGeneratorPlugin. Please check http://go.specflow.org/doc-plugins for details.");

IGeneratorPlugin plugin;
try
Expand All @@ -69,80 +45,10 @@ public IGeneratorPlugin LoadPlugin(PluginDescriptor pluginDescriptor)
}
catch (Exception ex)
{
throw new SpecFlowException(string.Format("Invalid plugin in {0}. Plugin must have a default constructor that does not throw exception. Please check http://go.specflow.org/doc-plugins for details.", generatorPluginAssemblyPath), ex);
throw new SpecFlowException($"Invalid plugin in {generatorPluginAssemblyPath}. Plugin must have a default constructor that does not throw exception. Please check http://go.specflow.org/doc-plugins for details.", ex);
}

return plugin;
}

private IEnumerable<string> GetGeneratorPluginAssemblies(PluginDescriptor pluginDescriptor)
{
foreach (var pluginGeneratorFolder in GetPluginGeneratorFolders(pluginDescriptor))
{
string generatorSpecificAssembly = Path.GetFullPath(Path.Combine(pluginGeneratorFolder, string.Format("{0}.Generator.SpecFlowPlugin.dll", pluginDescriptor.Name)));
generatorSpecificAssembly = Environment.ExpandEnvironmentVariables(generatorSpecificAssembly);

if (File.Exists(generatorSpecificAssembly))
yield return generatorSpecificAssembly;

string genericAssembly = Path.GetFullPath(Path.Combine(pluginGeneratorFolder, string.Format("{0}.SpecFlowPlugin.dll", pluginDescriptor.Name)));
genericAssembly = Environment.ExpandEnvironmentVariables(genericAssembly);
if (File.Exists(genericAssembly))
yield return genericAssembly;
}
}

private IEnumerable<string> GetPluginGeneratorFolders(PluginDescriptor pluginDescriptor)
{
var pluginGeneratorFolders = (new[] { @"" })
.Concat(GetSpecFlowVersionSpecifiers().Select(v => @"tools\SpecFlowPlugin" + v))
.Concat(new[] { @"tools", @"lib\net45", @"lib\net40", @"lib\net35", @"lib"});

return GetPluginFolders(pluginDescriptor).SelectMany(pluginFolder => pluginGeneratorFolders, Path.Combine);
}

private IEnumerable<string> GetPluginFolders(PluginDescriptor pluginDescriptor)
{
if (pluginDescriptor.Path != null)
{
yield return Path.Combine(projectSettings.ProjectFolder, pluginDescriptor.Path);
yield break;
}

yield return generatorFolder;

foreach (var nuGetPluginFolder in GetNuGetPluginFolders(pluginDescriptor))
yield return nuGetPluginFolder;
}

private static readonly string[] pluginPostfixes = new[] { @".SpecFlowPlugin", @".SpecFlow", @"" };

private IEnumerable<string> GetNuGetPluginFolders(PluginDescriptor pluginDescriptor)
{
string nuGetPackagesFolder = GetNuGetPackagesFolder();

return pluginPostfixes
.Select(pluginPostfix => pluginDescriptor.Name + pluginPostfix)
.Select(packageName => GetLatestPackage(nuGetPackagesFolder, packageName))
.Where(pluginFolder => pluginFolder != null);
}

private static string GetLatestPackage(string nuGetPackagesFolder, string packageName)
{
return Directory.GetDirectories(nuGetPackagesFolder, packageName + ".*").OrderByDescending(d => d).FirstOrDefault();
}

private IEnumerable<string> GetSpecFlowVersionSpecifiers()
{
var version = Assembly.GetExecutingAssembly().GetName().Version;
yield return string.Format(".{0}-{1}-{2}", version.Major, version.Minor, version.Revision);
yield return string.Format(".{0}-{1}", version.Major, version.Minor);
yield return "";
}

private string GetNuGetPackagesFolder()
{
return Path.Combine(generatorFolder, @"..\.."); // assuming that SpecFlow was installed with nuget, generator is in the "tools" folder
}
}
}
136 changes: 136 additions & 0 deletions TechTalk.SpecFlow.Generator/Plugins/GeneratorPluginLocator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using TechTalk.SpecFlow.Generator.Interfaces;
using TechTalk.SpecFlow.Plugins;
using TechTalk.SpecFlow.Utils;

namespace TechTalk.SpecFlow.Generator.Plugins
{
/// <summary>
/// Loading logic:
/// <plugin-folder> = @path | <generator-folder> | <nuget-plugin-folder>
///
/// <configured-path> = @path | <project-folder>\@path
///
/// <nuget-plugin-folder> = <nuget-packages>\<plugin-name>.SpecFlowPlugin.<nuget-package-version> | <nuget-packages>\<plugin-name>.SpecFlow.<nuget-package-version> | <nuget-packages>\<plugin-name>.<nuget-package-version> // match first for the one with <specflow-version>
///
/// <specflow-version> = n-n[-n] // e.g. 1-8-1
///
/// <nuget-packages> = <generator-folder>\..\.. // assuming that SpecFlow was installed with nuget, generator is in the "tools" folder
///
/// <nuget-package-version> = latest-of: n(.n)*[-tag]
///
/// <plugin-generator-folder> = <plugin-folder> | <plugin-folder>\tools\SpecFlowPlugin[.<specflow-version>] | <plugin-folder>\tools | <plugin-folder>\lib\net45 | <plugin-folder>\lib\net40 | <plugin-folder>\lib\net35 | <plugin-folder>\lib
///
/// <generator-plugin-assembly> = <plugin-generator-folder>\<plugin-name>.Generator.SpecFlowPlugin.dll | <generator-plugin-folder>\<plugin-name>.SpecFlowPlugin.dll
/// </summary>
public class GeneratorPluginLocator : IGeneratorPluginLocator
{
private readonly string generatorFolder;
private readonly ProjectSettings projectSettings;
private readonly IFileSystem fileSystem;

public GeneratorPluginLocator(ProjectSettings projectSettings, IFileSystem fileSystem)
: this(projectSettings, null, fileSystem)
{
this.generatorFolder = Path.GetDirectoryName(new Uri(Assembly.GetExecutingAssembly().CodeBase).LocalPath);
}

internal GeneratorPluginLocator(ProjectSettings projectSettings, string generatorFolder, IFileSystem fileSystem)
{
this.projectSettings = projectSettings;
this.generatorFolder = generatorFolder;
this.fileSystem = fileSystem;
}

public String LocatePluginAssembly(PluginDescriptor pluginDescriptor)
{
var assemblyPath = GetGeneratorPluginAssemblies(pluginDescriptor).FirstOrDefault();

if (assemblyPath == null)
{
throw new SpecFlowException($"Unable to find plugin in the plugin search path: {pluginDescriptor.Name}. Please check http://go.specflow.org/doc-plugins for details.");
}

return assemblyPath;
}

private IEnumerable<string> GetGeneratorPluginAssemblies(PluginDescriptor pluginDescriptor)
{
foreach (var pluginGeneratorFolder in GetPluginGeneratorFolders(pluginDescriptor))
{
string generatorSpecificAssembly = Path.GetFullPath(Path.Combine(pluginGeneratorFolder, string.Format("{0}.Generator.SpecFlowPlugin.dll", pluginDescriptor.Name)));
generatorSpecificAssembly = Environment.ExpandEnvironmentVariables(generatorSpecificAssembly);

if (this.fileSystem.FileExists(generatorSpecificAssembly))
yield return generatorSpecificAssembly;

string genericAssembly = Path.GetFullPath(Path.Combine(pluginGeneratorFolder, string.Format("{0}.SpecFlowPlugin.dll", pluginDescriptor.Name)));
genericAssembly = Environment.ExpandEnvironmentVariables(genericAssembly);
if (this.fileSystem.FileExists(genericAssembly))
yield return genericAssembly;
}
}

private IEnumerable<string> GetPluginGeneratorFolders(PluginDescriptor pluginDescriptor)
{
var pluginGeneratorFolders = (new[] { @"" })
.Concat(GetSpecFlowVersionSpecifiers().Select(v => @"tools\SpecFlowPlugin" + v))
.Concat(new[] { @"tools", @"lib\net45", @"lib\net40", @"lib\net35", @"lib" });

return GetPluginFolders(pluginDescriptor).SelectMany(pluginFolder => pluginGeneratorFolders, Path.Combine);
}

private IEnumerable<string> GetPluginFolders(PluginDescriptor pluginDescriptor)
{
if (pluginDescriptor.Path != null)
{
var path = Environment.ExpandEnvironmentVariables(pluginDescriptor.Path);

yield return Path.IsPathRooted(path)
? path
: Path.Combine(projectSettings.ProjectFolder, path);

yield break;
}

yield return generatorFolder;

foreach (var nuGetPluginFolder in GetNuGetPluginFolders(pluginDescriptor))
yield return nuGetPluginFolder;
}

private static readonly string[] pluginPostfixes = new[] { @".SpecFlowPlugin", @".SpecFlow", @"" };

private IEnumerable<string> GetNuGetPluginFolders(PluginDescriptor pluginDescriptor)
{
string nuGetPackagesFolder = GetNuGetPackagesFolder();

return pluginPostfixes
.Select(pluginPostfix => pluginDescriptor.Name + pluginPostfix)
.Select(packageName => GetLatestPackage(nuGetPackagesFolder, packageName))
.Where(pluginFolder => pluginFolder != null);
}

private string GetLatestPackage(string nuGetPackagesFolder, string packageName)
{
return this.fileSystem.GetDirectories(nuGetPackagesFolder, packageName + ".*").OrderByDescending(d => d).FirstOrDefault();
}

private IEnumerable<string> GetSpecFlowVersionSpecifiers()
{
var version = Assembly.GetExecutingAssembly().GetName().Version;
yield return string.Format(".{0}-{1}-{2}", version.Major, version.Minor, version.Revision);
yield return string.Format(".{0}-{1}", version.Major, version.Minor);
yield return "";
}

private string GetNuGetPackagesFolder()
{
return Path.Combine(this.generatorFolder, "..", ".."); // assuming that SpecFlow was installed with nuget, generator is in the "tools" folder
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using TechTalk.SpecFlow.Plugins;

namespace TechTalk.SpecFlow.Generator.Plugins
{
public interface IGeneratorPluginLocator
{
string LocatePluginAssembly(PluginDescriptor pluginDescriptor);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,10 @@
<Compile Include="Plugins\GeneratorPluginEventArgs.cs" />
<Compile Include="Plugins\GeneratorPluginEvents.cs" />
<Compile Include="Plugins\GeneratorPluginLoader.cs" />
<Compile Include="Plugins\GeneratorPluginLocator.cs" />
<Compile Include="Plugins\GeneratorPluginParameters.cs" />
<Compile Include="Plugins\IGeneratorPlugin.cs" />
<Compile Include="Plugins\IGeneratorPluginLocator.cs" />
<Compile Include="Plugins\IGeneratorPluginLoader.cs" />
<Compile Include="Project\ISpecFlowProjectReader.cs" />
<Compile Include="Project\MsBuildProjectReader.cs" />
Expand Down
4 changes: 2 additions & 2 deletions TechTalk.SpecFlow.Utils/CodeDomHelper.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.CodeDom;
using System.CodeDom.Compiler;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using Microsoft.CSharp;
Expand Down
Loading

0 comments on commit eeeb80e

Please sign in to comment.