Skip to content

Commit 41135e0

Browse files
authored
Handle escaping of command-line arguments in dotnet test (#50026)
1 parent 62a31fc commit 41135e0

File tree

5 files changed

+66
-5
lines changed

5 files changed

+66
-5
lines changed

src/Cli/dotnet/Commands/Test/TestApplication.cs

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -94,17 +94,20 @@ private string GetArguments(TestOptions testOptions)
9494
// In the case of UseAppHost=false, RunArguments is set to `exec $(TargetPath)`:
9595
// https://github.com/dotnet/sdk/blob/333388c31d811701e3b6be74b5434359151424dc/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Sdk.targets#L1411
9696
// So, we keep that first always.
97+
// RunArguments is intentionally not escaped. It can contain multiple arguments and spaces there shouldn't cause the whole
98+
// value to be wrapped in double quotes. This matches dotnet run behavior.
99+
// In short, it's expected to already be escaped properly.
97100
StringBuilder builder = new(Module.RunProperties.RunArguments);
98101

99102
if (testOptions.IsHelp)
100103
{
101-
builder.Append($" {TestingPlatformOptions.HelpOption.Name} ");
104+
builder.Append($" {TestingPlatformOptions.HelpOption.Name}");
102105
}
103106

104-
var args = _buildOptions.UnmatchedTokens;
105-
builder.Append(args.Count != 0
106-
? args.Aggregate((a, b) => $"{a} {b}")
107-
: string.Empty);
107+
foreach (var arg in _buildOptions.UnmatchedTokens)
108+
{
109+
builder.Append($" {ArgumentEscaper.EscapeSingleArg(arg)}");
110+
}
108111

109112
builder.Append($" {CliConstants.ServerOptionKey} {CliConstants.ServerOptionValue} {CliConstants.DotNetTestPipeOptionKey} {ArgumentEscaper.EscapeSingleArg(_pipeNameDescription.Name)}");
110113

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// Copyright (c) .NET Foundation and contributors. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
4+
using System;
5+
using System.Text;
6+
7+
var builder = new StringBuilder();
8+
for (int i = 0; i < args.Length; i++)
9+
{
10+
builder.AppendLine($"args[{i}]={args[i]}");
11+
}
12+
13+
throw new Exception(builder.ToString());
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<Project Sdk="Microsoft.NET.Sdk" ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
2+
<Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), testAsset.props))\testAsset.props" />
3+
4+
<PropertyGroup>
5+
<TargetFramework>$(CurrentTargetFramework)</TargetFramework>
6+
<OutputType>Exe</OutputType>
7+
<IsTestingPlatformApplication>true</IsTestingPlatformApplication>
8+
</PropertyGroup>
9+
</Project>
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[dotnet.test.runner]
2+
name= "Microsoft.Testing.Platform"

test/dotnet.Tests/CommandTests/Test/GivenDotnetTestBuildsAndDiscoversTests.cs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,5 +150,39 @@ public void DiscoverTestProjectsWithHybridModeTestRunners_ShouldReturnExitCodeGe
150150

151151
result.ExitCode.Should().Be(ExitCodes.GenericFailure);
152152
}
153+
154+
[InlineData(TestingConstants.Debug)]
155+
[InlineData(TestingConstants.Release)]
156+
[Theory]
157+
public void DiscoverTestProjectWithCustomRunArgumentsAndTestEscaping(string configuration)
158+
{
159+
TestAsset testInstance = _testAssetsManager.CopyTestAsset("TestAppPrintingCommandLineArguments", Guid.NewGuid().ToString())
160+
.WithSource();
161+
162+
CommandResult result = new DotnetTestCommand(Log, disableNewOutput: false)
163+
.WithWorkingDirectory(testInstance.Path)
164+
.Execute(TestingPlatformOptions.ListTestsOption.Name,
165+
TestingPlatformOptions.ConfigurationOption.Name, configuration,
166+
"-p:RunArguments=--hello world \"\" world2",
167+
"Another arg with spaces",
168+
"My other arg with spaces",
169+
"Arg ending with backslash and containing spaces\\",
170+
"ArgWithoutSpacesEndingWith\\");
171+
172+
result.StdOut.Should().Contain("""
173+
args[0]=--hello
174+
args[1]=world
175+
args[2]=
176+
args[3]=world2
177+
args[4]=--list-tests
178+
args[5]=Another arg with spaces
179+
args[6]=My other arg with spaces
180+
args[7]=Arg ending with backslash and containing spaces\
181+
args[8]=ArgWithoutSpacesEndingWith\
182+
args[9]=--server
183+
args[10]=dotnettestcli
184+
args[11]=--dotnet-test-pipe
185+
""");
186+
}
153187
}
154188
}

0 commit comments

Comments
 (0)