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

Add support for user-supplied project file detection #2684

Merged
merged 9 commits into from
Jan 7, 2025
Merged
7 changes: 7 additions & 0 deletions BenchmarkDotNet.sln
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BenchmarkDotNet.Exporters.P
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BenchmarkDotNet.Exporters.Plotting.Tests", "tests\BenchmarkDotNet.Exporters.Plotting.Tests\BenchmarkDotNet.Exporters.Plotting.Tests.csproj", "{199AC83E-30BD-40CD-87CE-0C838AC0320D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BenchmarkDotNet.IntegrationTests.FileLocators", "tests\BenchmarkDotNet.IntegrationTests.FileLocators\BenchmarkDotNet.IntegrationTests.FileLocators.csproj", "{7AD9FCF9-69B5-4984-93AC-D6E30344DADC}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -161,6 +163,10 @@ Global
{199AC83E-30BD-40CD-87CE-0C838AC0320D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{199AC83E-30BD-40CD-87CE-0C838AC0320D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{199AC83E-30BD-40CD-87CE-0C838AC0320D}.Release|Any CPU.Build.0 = Release|Any CPU
{7AD9FCF9-69B5-4984-93AC-D6E30344DADC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7AD9FCF9-69B5-4984-93AC-D6E30344DADC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7AD9FCF9-69B5-4984-93AC-D6E30344DADC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7AD9FCF9-69B5-4984-93AC-D6E30344DADC}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -190,6 +196,7 @@ Global
{2E2283A3-6DA6-4482-8518-99D6D9F689AB} = {D6597E3A-6892-4A68-8E14-042FC941FDA2}
{B92ECCEF-7C27-4012-9E19-679F3C40A6A6} = {D6597E3A-6892-4A68-8E14-042FC941FDA2}
{199AC83E-30BD-40CD-87CE-0C838AC0320D} = {14195214-591A-45B7-851A-19D3BA2413F9}
{7AD9FCF9-69B5-4984-93AC-D6E30344DADC} = {14195214-591A-45B7-851A-19D3BA2413F9}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {4D9AF12B-1F7F-45A7-9E8C-E4E46ADCBD1F}
Expand Down
2 changes: 2 additions & 0 deletions src/BenchmarkDotNet/Configs/DebugConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using BenchmarkDotNet.Exporters;
using BenchmarkDotNet.Filters;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Locators;
using BenchmarkDotNet.Loggers;
using BenchmarkDotNet.Order;
using BenchmarkDotNet.Reports;
Expand Down Expand Up @@ -58,6 +59,7 @@ public abstract class DebugConfig : IConfig
public IEnumerable<IValidator> GetValidators() => Array.Empty<IValidator>();
public IEnumerable<IColumnProvider> GetColumnProviders() => DefaultColumnProviders.Instance;
public IEnumerable<IExporter> GetExporters() => Array.Empty<IExporter>();
public IEnumerable<IFileLocator> GetFileLocators() => Array.Empty<IFileLocator>();
public IEnumerable<ILogger> GetLoggers() => new[] { ConsoleLogger.Default };
public IEnumerable<IDiagnoser> GetDiagnosers() => Array.Empty<IDiagnoser>();
public IEnumerable<IAnalyser> GetAnalysers() => Array.Empty<IAnalyser>();
Expand Down
3 changes: 3 additions & 0 deletions src/BenchmarkDotNet/Configs/DefaultConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using BenchmarkDotNet.Exporters.Csv;
using BenchmarkDotNet.Filters;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Locators;
using BenchmarkDotNet.Loggers;
using BenchmarkDotNet.Order;
using BenchmarkDotNet.Portability;
Expand Down Expand Up @@ -40,6 +41,8 @@ public IEnumerable<IExporter> GetExporters()
yield return HtmlExporter.Default;
}

public IEnumerable<IFileLocator> GetFileLocators() => Array.Empty<IFileLocator>();

public IEnumerable<ILogger> GetLoggers()
{
if (LinqPadLogger.IsAvailable)
Expand Down
2 changes: 2 additions & 0 deletions src/BenchmarkDotNet/Configs/IConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using BenchmarkDotNet.Exporters;
using BenchmarkDotNet.Filters;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Locators;
using BenchmarkDotNet.Loggers;
using BenchmarkDotNet.Order;
using BenchmarkDotNet.Reports;
Expand All @@ -20,6 +21,7 @@ public interface IConfig
{
IEnumerable<IColumnProvider> GetColumnProviders();
IEnumerable<IExporter> GetExporters();
IEnumerable<IFileLocator> GetFileLocators();
IEnumerable<ILogger> GetLoggers();
IEnumerable<IDiagnoser> GetDiagnosers();
IEnumerable<IAnalyser> GetAnalysers();
Expand Down
7 changes: 6 additions & 1 deletion src/BenchmarkDotNet/Configs/ImmutableConfig.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Globalization;
Expand All @@ -10,6 +10,7 @@
using BenchmarkDotNet.Exporters;
using BenchmarkDotNet.Filters;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Locators;
using BenchmarkDotNet.Loggers;
using BenchmarkDotNet.Order;
using BenchmarkDotNet.Reports;
Expand All @@ -24,6 +25,7 @@ public sealed class ImmutableConfig : IConfig
// if something is an array here instead of hashset it means it must have a guaranteed order of elements
private readonly ImmutableArray<IColumnProvider> columnProviders;
private readonly ImmutableArray<IExporter> exporters;
private readonly ImmutableArray<IFileLocator> fileLocators;
private readonly ImmutableHashSet<ILogger> loggers;
private readonly ImmutableHashSet<IDiagnoser> diagnosers;
private readonly ImmutableHashSet<IAnalyser> analysers;
Expand All @@ -41,6 +43,7 @@ internal ImmutableConfig(
ImmutableHashSet<HardwareCounter> uniqueHardwareCounters,
ImmutableHashSet<IDiagnoser> uniqueDiagnosers,
ImmutableArray<IExporter> uniqueExporters,
ImmutableArray<IFileLocator> uniqueFileLocators,
ImmutableHashSet<IAnalyser> uniqueAnalyzers,
ImmutableHashSet<IValidator> uniqueValidators,
ImmutableHashSet<IFilter> uniqueFilters,
Expand All @@ -63,6 +66,7 @@ internal ImmutableConfig(
hardwareCounters = uniqueHardwareCounters;
diagnosers = uniqueDiagnosers;
exporters = uniqueExporters;
fileLocators = uniqueFileLocators;
analysers = uniqueAnalyzers;
validators = uniqueValidators;
filters = uniqueFilters;
Expand Down Expand Up @@ -92,6 +96,7 @@ internal ImmutableConfig(

public IEnumerable<IColumnProvider> GetColumnProviders() => columnProviders;
public IEnumerable<IExporter> GetExporters() => exporters;
public IEnumerable<IFileLocator> GetFileLocators() => fileLocators;
public IEnumerable<ILogger> GetLoggers() => loggers;
public IEnumerable<IDiagnoser> GetDiagnosers() => diagnosers;
public IEnumerable<IAnalyser> GetAnalysers() => analysers;
Expand Down
4 changes: 3 additions & 1 deletion src/BenchmarkDotNet/Configs/ImmutableConfigBuilder.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System.Collections.Generic;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using BenchmarkDotNet.Analysers;
Expand Down Expand Up @@ -44,6 +44,7 @@ public static ImmutableConfig Create(IConfig source)
var uniqueHardwareCounters = source.GetHardwareCounters().Where(counter => counter != HardwareCounter.NotSet).ToImmutableHashSet();
var uniqueDiagnosers = GetDiagnosers(source.GetDiagnosers(), uniqueHardwareCounters);
var uniqueExporters = GetExporters(source.GetExporters(), uniqueDiagnosers, configAnalyse);
var uniqueFileLocators = source.GetFileLocators().ToImmutableArray();
var uniqueAnalyzers = GetAnalysers(source.GetAnalysers(), uniqueDiagnosers);

var uniqueValidators = GetValidators(source.GetValidators(), MandatoryValidators, source.Options);
Expand All @@ -61,6 +62,7 @@ public static ImmutableConfig Create(IConfig source)
uniqueHardwareCounters,
uniqueDiagnosers,
uniqueExporters,
uniqueFileLocators,
uniqueAnalyzers,
uniqueValidators,
uniqueFilters,
Expand Down
12 changes: 11 additions & 1 deletion src/BenchmarkDotNet/Configs/ManualConfig.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
Expand All @@ -11,6 +11,7 @@
using BenchmarkDotNet.Extensions;
using BenchmarkDotNet.Filters;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Locators;
using BenchmarkDotNet.Loggers;
using BenchmarkDotNet.Order;
using BenchmarkDotNet.Reports;
Expand All @@ -26,6 +27,7 @@ public class ManualConfig : IConfig

private readonly List<IColumnProvider> columnProviders = new List<IColumnProvider>();
private readonly List<IExporter> exporters = new List<IExporter>();
private readonly List<IFileLocator> locators = new List<IFileLocator>();
private readonly List<ILogger> loggers = new List<ILogger>();
private readonly List<IDiagnoser> diagnosers = new List<IDiagnoser>();
private readonly List<IAnalyser> analysers = new List<IAnalyser>();
Expand All @@ -39,6 +41,7 @@ public class ManualConfig : IConfig

public IEnumerable<IColumnProvider> GetColumnProviders() => columnProviders;
public IEnumerable<IExporter> GetExporters() => exporters;
public IEnumerable<IFileLocator> GetFileLocators() => locators;
public IEnumerable<ILogger> GetLoggers() => loggers;
public IEnumerable<IDiagnoser> GetDiagnosers() => diagnosers;
public IEnumerable<IAnalyser> GetAnalysers() => analysers;
Expand Down Expand Up @@ -139,6 +142,12 @@ public ManualConfig AddExporter(params IExporter[] newExporters)
return this;
}

public ManualConfig AddFileLocator(params IFileLocator[] newLocators)
{
locators.AddRange(newLocators);
return this;
}

[EditorBrowsable(EditorBrowsableState.Never)]
[Obsolete("This method will soon be removed, please start using .AddLogger() instead.")]
public void Add(params ILogger[] newLoggers) => AddLogger(newLoggers);
Expand Down Expand Up @@ -256,6 +265,7 @@ public void Add(IConfig config)
{
columnProviders.AddRange(config.GetColumnProviders());
exporters.AddRange(config.GetExporters());
locators.AddRange(config.GetFileLocators());
loggers.AddRange(config.GetLoggers());
diagnosers.AddRange(config.GetDiagnosers());
analysers.AddRange(config.GetAnalysers());
Expand Down
16 changes: 16 additions & 0 deletions src/BenchmarkDotNet/Locators/FileLocatorArgs.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using BenchmarkDotNet.Loggers;
using BenchmarkDotNet.Running;

namespace BenchmarkDotNet.Locators;

public class FileLocatorArgs
{
public FileLocatorArgs(BenchmarkCase benchmarkCase, ILogger logger)
{
BenchmarkCase = benchmarkCase;
Logger = logger;
}

public BenchmarkCase BenchmarkCase { get; }
public ILogger Logger { get; }
}
6 changes: 6 additions & 0 deletions src/BenchmarkDotNet/Locators/FileLocatorType.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace BenchmarkDotNet.Locators;

public enum FileLocatorType
{
Project
}
22 changes: 22 additions & 0 deletions src/BenchmarkDotNet/Locators/IFileLocator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using System.IO;

namespace BenchmarkDotNet.Locators;

/// <summary>
/// Locators can be used to extend the default behavior of finding files
/// </summary>
public interface IFileLocator
{
/// <summary>
/// The type of locator
/// </summary>
FileLocatorType LocatorType { get; }

/// <summary>
/// Tries to locate a file
/// </summary>
/// <param name="fileLocatorArgs">The arguments such as benchmark and logger</param>
/// <param name="fileInfo">The file is provided by the implementation</param>
/// <returns>True when a file was successfully found, False otherwise.</returns>
bool TryLocate(FileLocatorArgs fileLocatorArgs, out FileInfo fileInfo);
}
42 changes: 35 additions & 7 deletions src/BenchmarkDotNet/Toolchains/CsProj/CsProjGenerator.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
Expand All @@ -11,6 +11,7 @@
using BenchmarkDotNet.Extensions;
using BenchmarkDotNet.Helpers;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Locators;
using BenchmarkDotNet.Loggers;
using BenchmarkDotNet.Running;
using BenchmarkDotNet.Toolchains.DotNetCli;
Expand Down Expand Up @@ -71,7 +72,7 @@ protected override string GetIntermediateDirectoryPath(string buildArtifactsDire
protected override void GenerateProject(BuildPartition buildPartition, ArtifactsPaths artifactsPaths, ILogger logger)
{
var benchmark = buildPartition.RepresentativeBenchmarkCase;
var projectFile = GetProjectFilePath(benchmark.Descriptor.Type, logger);
var projectFile = GetProjectFilePath(benchmark, logger);

var xmlDoc = new XmlDocument();
xmlDoc.Load(projectFile.FullName);
Expand Down Expand Up @@ -246,8 +247,29 @@ private static string GetIndentedXmlString(XmlDocument doc)
/// returns a path to the project file which defines the benchmarks
/// </summary>
[PublicAPI]
protected virtual FileInfo GetProjectFilePath(Type benchmarkTarget, ILogger logger)
protected virtual FileInfo GetProjectFilePath(BenchmarkCase benchmark, ILogger logger)
{
Copy link
Collaborator

Choose a reason for hiding this comment

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

Noting the breaking change to a public API.

var args = new FileLocatorArgs(benchmark, logger);

// Try locators first. Logic is provided by the user for uses-cases such as they have set AssemblyName to a custom value.
var notFound = new List<string>();
foreach (var locator in benchmark.Config.GetFileLocators())
{
if (locator.LocatorType != FileLocatorType.Project)
{
continue;
}

if (locator.TryLocate(args, out var fileInfo))
{
if (fileInfo.Exists)
return fileInfo;

notFound.Add(fileInfo.FullName);
}
}

// Fall back to default project detection logic
if (!GetSolutionRootDirectory(out var rootDirectory) && !GetProjectRootDirectory(out rootDirectory))
{
logger.WriteLineError(
Expand All @@ -256,7 +278,7 @@ protected virtual FileInfo GetProjectFilePath(Type benchmarkTarget, ILogger logg
}

// important assumption! project's file name === output dll name
string projectName = benchmarkTarget.GetTypeInfo().Assembly.GetName().Name;
string projectName = benchmark.Descriptor.Type.GetTypeInfo().Assembly.GetName().Name;

var possibleNames = new HashSet<string> { $"{projectName}.csproj", $"{projectName}.fsproj", $"{projectName}.vbproj" };
var projectFiles = rootDirectory
Expand All @@ -266,12 +288,18 @@ protected virtual FileInfo GetProjectFilePath(Type benchmarkTarget, ILogger logg

if (projectFiles.Length == 0)
{
throw new NotSupportedException(
$"Unable to find {projectName} in {rootDirectory.FullName} and its subfolders. Most probably the name of output exe is different than the name of the .(c/f)sproj");
string message;

if (notFound.Count > 0)
message = $"Unable to find {projectName} in any of the paths: {string.Join(", ", notFound)} or in {rootDirectory.FullName} and its subfolders";
else
message = $"Unable to find {projectName} in {rootDirectory.FullName} and its subfolders. Most probably the name of output exe is different than the name of the .(c/f)sproj. You can add an IFileLocator to the config if this is on purpose.";

throw new FileNotFoundException(message);
}
else if (projectFiles.Length > 1)
{
throw new NotSupportedException(
throw new InvalidOperationException(
$"Found more than one matching project file for {projectName} in {rootDirectory.FullName} and its subfolders: {string.Join(",", projectFiles.Select(pf => $"'{pf.FullName}'"))}. Benchmark project names needs to be unique.");
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public MonoAotLLVMGenerator(string targetFrameworkMoniker, string cliPath, strin
protected override void GenerateProject(BuildPartition buildPartition, ArtifactsPaths artifactsPaths, ILogger logger)
{
BenchmarkCase benchmark = buildPartition.RepresentativeBenchmarkCase;
var projectFile = GetProjectFilePath(benchmark.Descriptor.Type, logger);
var projectFile = GetProjectFilePath(benchmark, logger);

string useLLVM = AotCompilerMode == MonoAotCompilerMode.llvm ? "true" : "false";

Expand Down
2 changes: 1 addition & 1 deletion src/BenchmarkDotNet/Toolchains/MonoWasm/WasmGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ protected override void GenerateProject(BuildPartition buildPartition, Artifacts
protected void GenerateProjectFile(BuildPartition buildPartition, ArtifactsPaths artifactsPaths, bool aot, ILogger logger)
{
BenchmarkCase benchmark = buildPartition.RepresentativeBenchmarkCase;
var projectFile = GetProjectFilePath(benchmark.Descriptor.Type, logger);
var projectFile = GetProjectFilePath(benchmark, logger);

WasmRuntime runtime = (WasmRuntime) buildPartition.Runtime;

Expand Down
10 changes: 5 additions & 5 deletions src/BenchmarkDotNet/Toolchains/NativeAot/Generator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -151,17 +151,17 @@ private string GenerateProjectForNuGetBuild(BuildPartition buildPartition, Artif
</ItemGroup>
<ItemGroup>
{GetILCompilerPackageReference()}
<ProjectReference Include=""{GetProjectFilePath(buildPartition.RepresentativeBenchmarkCase.Descriptor.Type, logger).FullName}"" />
<ProjectReference Include=""{GetProjectFilePath(buildPartition.RepresentativeBenchmarkCase, logger).FullName}"" />
</ItemGroup>
<ItemGroup>
{string.Join(Environment.NewLine, GetRdXmlFiles(buildPartition.RepresentativeBenchmarkCase.Descriptor.Type, logger).Select(file => $"<RdXmlFile Include=\"{file}\" />"))}
{string.Join(Environment.NewLine, GetRdXmlFiles(buildPartition.RepresentativeBenchmarkCase, logger).Select(file => $"<RdXmlFile Include=\"{file}\" />"))}
</ItemGroup>
{GetCustomProperties(buildPartition, logger)}
</Project>";

private string GetCustomProperties(BuildPartition buildPartition, ILogger logger)
{
var projectFile = GetProjectFilePath(buildPartition.RepresentativeBenchmarkCase.Descriptor.Type, logger);
var projectFile = GetProjectFilePath(buildPartition.RepresentativeBenchmarkCase, logger);
var xmlDoc = new XmlDocument();
xmlDoc.Load(projectFile.FullName);

Expand All @@ -186,11 +186,11 @@ private string GetInstructionSetSettings(BuildPartition buildPartition)
return !string.IsNullOrEmpty(instructionSet) ? $"<IlcInstructionSet>{instructionSet}</IlcInstructionSet>" : "";
}

public IEnumerable<string> GetRdXmlFiles(Type benchmarkTarget, ILogger logger)
public IEnumerable<string> GetRdXmlFiles(BenchmarkCase benchmark, ILogger logger)
{
yield return GeneratedRdXmlFileName;

var projectFile = GetProjectFilePath(benchmarkTarget, logger);
var projectFile = GetProjectFilePath(benchmark, logger);
var projectFileFolder = projectFile.DirectoryName;
var rdXml = Path.Combine(projectFileFolder, "rd.xml");
if (File.Exists(rdXml))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using BenchmarkDotNet.Attributes;

namespace BenchmarkDotNet.IntegrationTests.FileLocators
{
public class AssemblyNameIsSetBenchmarks
{
[Benchmark]
public string Benchmark()
{
return "This will only run when a FileLocator is set due to <AssemblyName> in the csproj";
}
}
}
Loading
Loading