Skip to content

Add better error messages for dotnet test with solution/project/directory/dll arguments in Microsoft.Testing.Platform (MTP) #50108

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

Merged
merged 9 commits into from
Aug 12, 2025
80 changes: 80 additions & 0 deletions src/Cli/dotnet/Commands/Test/TestCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,13 @@ public static int Run(ParseResult parseResult)
// all parameters before --
args = [.. args.TakeWhile(a => a != "--")];

// Check for common patterns that suggest users need to use specific flags
var validationResult = ValidateArgumentsForRequiredFlags(args);
if (validationResult != 0)
{
return validationResult;
}

// Fix for https://github.com/Microsoft/vstest/issues/1453
// Run dll/exe directly using the VSTestForwardingApp
if (ContainsBuiltTestSources(args))
Expand Down Expand Up @@ -341,6 +348,79 @@ private static Dictionary<string, string> GetUserSpecifiedExplicitMSBuildPropert
}
return globalProperties;
}

/// <summary>
/// Validates that arguments requiring specific flags are used correctly.
/// Provides helpful error messages when users provide file/directory arguments without proper flags.
/// </summary>
/// <returns>0 if validation passes, non-zero error code if validation fails</returns>
private static int ValidateArgumentsForRequiredFlags(string[] args)
{
foreach (string arg in args)
{
if (arg.StartsWith("-"))
{
// Skip options/flags
continue;
}

string? errorMessage = null;
string? suggestedUsage = null;

// Check for .sln files
if (arg.EndsWith(".sln", StringComparison.OrdinalIgnoreCase) && File.Exists(arg))
{
errorMessage = $"Solution file '{arg}' was provided as a positional argument.";
suggestedUsage = $"Consider passing the solution as the default target by running: dotnet test (from the solution directory)\n" +
$"Or use the new Testing Platform with: --solution {arg}\n" +
"To enable Testing Platform, create a 'dotnet.config' file with:\n" +
"[dotnet.test.runner]\n" +
"name = Microsoft.Testing.Platform";
}
// Check for .csproj/.vbproj/.fsproj files
else if ((arg.EndsWith(".csproj", StringComparison.OrdinalIgnoreCase) ||
arg.EndsWith(".vbproj", StringComparison.OrdinalIgnoreCase) ||
arg.EndsWith(".fsproj", StringComparison.OrdinalIgnoreCase)) && File.Exists(arg))
{
errorMessage = $"Project file '{arg}' was provided as a positional argument.";
suggestedUsage = $"Consider passing the project as the default target by running: dotnet test (from the project directory)\n" +
$"Or use the new Testing Platform with: --project {arg}\n" +
"To enable Testing Platform, create a 'dotnet.config' file with:\n" +
"[dotnet.test.runner]\n" +
"name = Microsoft.Testing.Platform";
}
// Check for directories (if they exist)
else if (Directory.Exists(arg))
{
errorMessage = $"Directory '{arg}' was provided as a positional argument.";
suggestedUsage = $"Use the new Testing Platform with: --directory {arg}\n" +
"To enable Testing Platform, create a 'dotnet.config' file with:\n" +
"[dotnet.test.runner]\n" +
"name = Microsoft.Testing.Platform";
}
// Check for .dll or .exe files (but not for VSTest forwarding case)
else if ((arg.EndsWith(".dll", StringComparison.OrdinalIgnoreCase) ||
arg.EndsWith(".exe", StringComparison.OrdinalIgnoreCase)) &&
File.Exists(arg))
{
errorMessage = $"Test assembly '{arg}' was provided as a positional argument.";
suggestedUsage = $"Use the new Testing Platform with: --test-modules {arg}\n" +
"To enable Testing Platform, create a 'dotnet.config' file with:\n" +
"[dotnet.test.runner]\n" +
"name = Microsoft.Testing.Platform";
}

if (errorMessage != null && suggestedUsage != null)
{
Reporter.Error.WriteLine(errorMessage);
Reporter.Error.WriteLine(suggestedUsage);
Reporter.Error.WriteLine("\nFor more information about the available options, run 'dotnet test --help'.");
return 1;
}
}

return 0;
}
}

public class TerminalLoggerDetector
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

using System.CommandLine;
using Microsoft.DotNet.Cli.Commands.Test;
using Microsoft.DotNet.Cli;

namespace Microsoft.DotNet.Cli.Test.Tests
{
Expand Down
103 changes: 103 additions & 0 deletions test/dotnet.Tests/CommandTests/Test/TestCommandValidationTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.DotNet.Tools.Test.Utilities;

namespace Microsoft.DotNet.Cli.Test.Tests
{
public class TestCommandValidationTests : SdkTest
{
public TestCommandValidationTests(ITestOutputHelper log) : base(log)
{
}

[Theory]
[InlineData("MySolution.sln", "Solution file 'MySolution.sln' was provided as a positional argument.")]
[InlineData("MyProject.csproj", "Project file 'MyProject.csproj' was provided as a positional argument.")]
[InlineData("MyProject.vbproj", "Project file 'MyProject.vbproj' was provided as a positional argument.")]
[InlineData("MyProject.fsproj", "Project file 'MyProject.fsproj' was provided as a positional argument.")]
public void TestCommandShouldValidateFileArgumentsAndProvideHelpfulMessages(string filename, string expectedErrorStart)
{
var testDir = _testAssetsManager.CreateTestDirectory();

// Create the test file
var testFilePath = Path.Combine(testDir.Path, filename);
File.WriteAllText(testFilePath, "dummy content");

var result = new DotnetTestCommand(Log, disableNewOutput: true)
.WithWorkingDirectory(testDir.Path)
.Execute(filename);

result.ExitCode.Should().Be(1);
result.StdErr.Should().Contain(expectedErrorStart);
result.StdErr.Should().Contain("Testing Platform");
result.StdErr.Should().Contain("dotnet.config");
}

[Fact]
public void TestCommandShouldValidateDirectoryArgumentAndProvideHelpfulMessage()
{
var testDir = _testAssetsManager.CreateTestDirectory();
var subDir = Path.Combine(testDir.Path, "test_directory");
Directory.CreateDirectory(subDir);

var result = new DotnetTestCommand(Log, disableNewOutput: true)
.WithWorkingDirectory(testDir.Path)
.Execute("test_directory");

result.ExitCode.Should().Be(1);
result.StdErr.Should().Contain("Directory 'test_directory' was provided as a positional argument.");
result.StdErr.Should().Contain("--directory test_directory");
result.StdErr.Should().Contain("Testing Platform");
}

[Fact]
public void TestCommandShouldValidateDllArgumentAndProvideHelpfulMessage()
{
var testDir = _testAssetsManager.CreateTestDirectory();

// Create a dummy dll file
var dllPath = Path.Combine(testDir.Path, "test.dll");
File.WriteAllText(dllPath, "dummy dll content");

var result = new DotnetTestCommand(Log, disableNewOutput: true)
.WithWorkingDirectory(testDir.Path)
.Execute("test.dll");

result.ExitCode.Should().Be(1);
result.StdErr.Should().Contain("Test assembly 'test.dll' was provided as a positional argument.");
result.StdErr.Should().Contain("--test-modules test.dll");
result.StdErr.Should().Contain("Testing Platform");
}

[Fact]
public void TestCommandShouldAllowNormalOptionsWithoutValidation()
{
var testDir = _testAssetsManager.CreateTestDirectory();

// Test that normal options like --help still work without triggering validation
var result = new DotnetTestCommand(Log, disableNewOutput: true)
.Execute("--help");

result.ExitCode.Should().Be(0);
result.StdOut.Should().Contain("Usage:");
result.StdOut.Should().Contain("dotnet test");
}

[Fact]
public void TestCommandShouldNotValidateNonExistentFiles()
{
var testDir = _testAssetsManager.CreateTestDirectory();

// Test that non-existent files with project extensions don't trigger validation
var result = new DotnetTestCommand(Log, disableNewOutput: true)
.WithWorkingDirectory(testDir.Path)
.Execute("NonExistent.csproj");

// This should pass through to MSBuild and give a different error, not our validation error
result.ExitCode.Should().Be(1);
result.StdErr.Should().NotContain("was provided as a positional argument");
result.StdErr.Should().NotContain("Testing Platform");
}
}
}
Loading