Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions docs/articles/guides/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ How to troubleshoot the build process:
The recommended order of solving build issues:

1. Change the right settings in your project file which defines benchmarks to get it working.
2. Customize the `Job` settings using available options like `job.WithCustomBuildConfiguration($name)`or `job.With(new Argument[] { new MsBuildArgument("/p:SomeProperty=Value")})`.
2. Customize the `Job` settings using available options like `job.WithCustomBuildConfiguration($name)` or `job.WithArguments(new Argument[] { new MsBuildArgument("/p:SomeProperty=Value") })`. For list-like MSBuild properties that contain special characters (for example, `DefineConstants=TEST1;TEST2`), use `MsBuildProperty` (it escapes the value for you): `job.WithArguments(new Argument[] { new MsBuildProperty("DefineConstants", "TEST1", "TEST2") })`.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I though pre-defined MSBuild property(DefineConstants) should not be set via WithArgument API.
It might be better to use something like ExtraDefineConstants as examples.

Because it's handled as global MSBuild property, and properties that are defined at project level is ignored.

3. Implement your own `IToolchain` and generate and build all the right things in your way (you can use existing Builders and Generators and just override some methods to change specific behaviour).
4. Report a bug in BenchmarkDotNet repository.

Expand Down Expand Up @@ -94,4 +94,3 @@ public void Setup()
### One of the above, but with a Debug build

By default, BDN builds everything in Release. But debugging Release builds even with full symbols might be non-trivial. To enforce BDN to build the benchmark in Debug please use `DebugBuildConfig` and then attach the debugger.

93 changes: 92 additions & 1 deletion src/BenchmarkDotNet/Jobs/Argument.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Text;
using JetBrains.Annotations;

namespace BenchmarkDotNet.Jobs
Expand Down Expand Up @@ -47,6 +48,96 @@ public MonoArgument(string value) : base(value)
[PublicAPI]
public class MsBuildArgument : Argument
{
private readonly string? displayValue;

public MsBuildArgument(string value) : base(value) { }

public MsBuildArgument(string value, bool escapeSpecialCharacters) : base(escapeSpecialCharacters ? EscapeSpecialCharacters(value) : value)
{
if (escapeSpecialCharacters)
displayValue = value;
}

public override string ToString() => displayValue ?? base.ToString();

internal static string EscapeSpecialCharacters(string value)
{
if (value is null)
throw new ArgumentNullException(nameof(value));

var builder = new StringBuilder(value.Length);
foreach (var character in value)
{
switch (character)
{
// See: https://learn.microsoft.com/en-us/visualstudio/msbuild/special-characters-to-escape
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It's not documented thought, following chars need to be escaped also.

  • Space ( )
  • Comma (,)
  • Escaped double quote(")

I've tested following tests and it failed when above chars are contained.
https://github.com/dotnet/BenchmarkDotNet/pull/2730/changes#diff-f676d4da73a1a3aef25299c6073c3fc8fc33c925794de73338ba35e9c367719bR70-R82

case '%':
builder.Append("%25");
break;
case '$':
builder.Append("%24");
break;
case '@':
builder.Append("%40");
break;
case '(':
builder.Append("%28");
break;
case ')':
builder.Append("%29");
break;
case ';':
builder.Append("%3B");
break;
case '?':
builder.Append("%3F");
break;
case '*':
builder.Append("%2A");
break;
default:
builder.Append(character);
break;
}
}

return builder.ToString();
}
}

/// <summary>
/// An MSBuild property argument (<c>/p:name=value</c>).
/// It escapes MSBuild special characters in the value (for example, semicolons) so that
/// list-like values can be passed without manual encoding.
/// </summary>
[PublicAPI]
public class MsBuildProperty : MsBuildArgument
{
public MsBuildProperty(string name, params string[] values)
: base(CreateArgumentTextRepresentation(name, values), escapeSpecialCharacters: true)
{
}

private static string CreateArgumentTextRepresentation(string name, string[] values)
{
if (string.IsNullOrWhiteSpace(name))
throw new ArgumentException("Property name must be non-empty.", nameof(name));

if (values is null)
throw new ArgumentNullException(nameof(values));

for (int i = 0; i < values.Length; i++)
if (values[i] is null)
throw new ArgumentException("Property values can not contain null.", nameof(values));

string value = values.Length switch
{
0 => string.Empty,
1 => values[0],
_ => string.Join(";", values)
};

return $"/p:{name}={value}";
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@
<PropertyGroup Condition="'$(CustomProp)'=='true'">
<DefineConstants>$(DefineConstants);CUSTOM_PROP</DefineConstants>
</PropertyGroup>

<PropertyGroup Condition="'$(ExtraDefineConstants)'!=''">
<DefineConstants>$(DefineConstants);$(ExtraDefineConstants)</DefineConstants>
</PropertyGroup>

<ItemGroup>
<Compile Include="..\BenchmarkDotNet.IntegrationTests\BenchmarkTestExecutor.cs" Link="BenchmarkTestExecutor.cs" />
Expand Down Expand Up @@ -44,4 +48,4 @@
</PackageReference>
</ItemGroup>
<Import Project="..\..\build\common.targets" />
</Project>
</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,17 @@ public void MultipleProcessesAreBuiltWithCorrectProperties()
CanExecute<PropertyDefine>(config);
}

[Fact]
public void ProcessIsBuiltWithSemicolonSeparatedPropertyValues()
{
var config = ManualConfig.CreateEmpty()
.AddJob(Job.Dry
.WithArguments([new MsBuildProperty("ExtraDefineConstants", "TEST1", "TEST2")])
);

CanExecute<ExtraDefineConstants>(config);
}

public class PropertyDefine
{
private const bool customPropWasSet =
Expand All @@ -66,5 +77,18 @@ public void ThrowWhenWrong()
}
}
}

public class ExtraDefineConstants
{
[Benchmark]
public void ThrowWhenWrong()
{
#if TEST1 && TEST2
return;
#else
throw new InvalidOperationException("ExtraDefineConstants was not applied to the benchmark build.");
#endif
}
}
}
}
}
35 changes: 35 additions & 0 deletions tests/BenchmarkDotNet.Tests/Jobs/MsBuildArgumentTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using BenchmarkDotNet.Jobs;
using Xunit;

namespace BenchmarkDotNet.Tests.Jobs;

public class MsBuildArgumentTests
{
[Fact]
public void EscapeSpecialCharactersIsOptIn()
{
var argument = new MsBuildArgument("/p:DefineConstants=TEST1;TEST2");

Assert.Equal("/p:DefineConstants=TEST1;TEST2", argument.TextRepresentation);
Assert.Equal("/p:DefineConstants=TEST1;TEST2", argument.ToString());
}

[Fact]
public void EscapeSpecialCharactersEscapesAllMsBuildSpecialCharactersForCommandLine()
{
var argument = new MsBuildArgument("%$@();?*", escapeSpecialCharacters: true);

Assert.Equal("%25%24%40%28%29%3B%3F%2A", argument.TextRepresentation);
Assert.Equal("%$@();?*", argument.ToString());
}

[Fact]
public void MsBuildPropertyJoinsValuesWithSemicolonsAndEscapesThem()
{
var argument = new MsBuildProperty("DefineConstants", "TEST1", "TEST2");

Assert.Equal("/p:DefineConstants=TEST1%3BTEST2", argument.TextRepresentation);
Assert.Equal("/p:DefineConstants=TEST1;TEST2", argument.ToString());
}
}

Loading