Skip to content

Commit e6974d5

Browse files
authored
Fix InvokeTestingPlatformTask to handle running with msbuild.exe and 'Test' target (#4840)
1 parent 13331af commit e6974d5

File tree

6 files changed

+126
-49
lines changed

6 files changed

+126
-49
lines changed

src/Platform/Microsoft.Testing.Platform.MSBuild/Microsoft.Testing.Platform.MSBuild.csproj

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22
<PropertyGroup>
33
<TargetFrameworks>$(MicrosoftTestingTargetFrameworks);netstandard2.0</TargetFrameworks>
44
<DefineConstants>PLATFORM_MSBUILD</DefineConstants>
@@ -47,6 +47,7 @@ This package provides MSBuild integration of the platform, its extensions and co
4747
<ItemGroup>
4848
<TfmSpecificPackageFile Include="@(ReferenceCopyLocalPaths->WithMetadataValue('ReferenceSourceTarget', 'ProjectReference')->WithMetadataValue('PrivateAssets', 'All'))" PackagePath="lib\$(TargetFramework)\%(ReferenceCopyLocalPaths.DestinationSubDirectory)" />
4949
<BuildOutputInPackage Include="@(ReferenceCopyLocalPaths->WithMetadataValue('ReferenceSourceTarget', 'ProjectReference')->WithMetadataValue('CopyToBuildOutput', 'true'))" TargetPath="%(ReferenceCopyLocalPaths.DestinationSubDirectory)" />
50+
<BuildOutputInPackage Include="@(RuntimeTargetsCopyLocalItems->HasMetadata('NuGetPackageId'))" TargetPath="%(RuntimeCopyLocalPaths.DestinationSubPath)" />
5051
</ItemGroup>
5152
</Target>
5253

src/Platform/Microsoft.Testing.Platform.MSBuild/Tasks/DotnetMuxerLocator.cs

+29
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,19 @@ public bool TryGetDotnetPathByArchitecture(
126126

127127
_resolutionLog("DotnetHostHelper.TryGetDotnetPathByArchitecture: Muxer was not found using DOTNET_ROOT* env variables.");
128128

129+
// Search in path
130+
muxerPath = GetMuxerFromPath();
131+
if (muxerPath is not null)
132+
{
133+
if (IsValidArchitectureMuxer(targetArchitecture, muxerPath))
134+
{
135+
_resolutionLog($"DotnetHostHelper.TryGetDotnetPathByArchitecture: Muxer compatible with '{targetArchitecture}' resolved from PATH: '{muxerPath}'");
136+
return true;
137+
}
138+
139+
_resolutionLog($"DotnetHostHelper.TryGetDotnetPathByArchitecture: Muxer resolved using PATH is not compatible with the target architecture: '{muxerPath}'");
140+
}
141+
129142
// Try to search for global registration
130143
muxerPath = isWinOs ? GetMuxerFromGlobalRegistrationWin(targetArchitecture) : GetMuxerFromGlobalRegistrationOnUnix(targetArchitecture);
131144

@@ -205,6 +218,22 @@ public bool TryGetDotnetPathByArchitecture(
205218
return true;
206219
}
207220

221+
private string? GetMuxerFromPath()
222+
{
223+
string values = Environment.GetEnvironmentVariable("PATH")!;
224+
foreach (string? p in values.Split(Path.PathSeparator))
225+
{
226+
string fullPath = Path.Combine(p, _muxerName);
227+
if (File.Exists(fullPath))
228+
{
229+
_resolutionLog($"DotnetMuxerLocator.GetMuxerFromPath: Found {_muxerName} using PATH environment variable: '{fullPath}'");
230+
return fullPath;
231+
}
232+
}
233+
234+
return null;
235+
}
236+
208237
private string? GetMuxerFromGlobalRegistrationWin(PlatformArchitecture targetArchitecture)
209238
{
210239
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))

src/Platform/Microsoft.Testing.Platform.MSBuild/Tasks/InvokeTestingPlatformTask.cs

+33-45
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,14 @@ namespace Microsoft.Testing.Platform.MSBuild;
2323
public class InvokeTestingPlatformTask : Build.Utilities.ToolTask, IDisposable
2424
{
2525
private const string MonoRunnerName = "mono";
26-
private const string DotnetRunnerName = "dotnet";
26+
private static readonly string DotnetRunnerName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "dotnet.exe" : "dotnet";
2727

2828
private readonly IFileSystem _fileSystem;
2929
private readonly PipeNameDescription _pipeNameDescription;
3030
private readonly CancellationTokenSource _waitForConnections = new();
3131
private readonly List<NamedPipeServer> _connections = new();
3232
private readonly StringBuilder _output = new();
3333
private readonly Lock _initLock = new();
34-
private readonly Process _currentProcess = Process.GetCurrentProcess();
3534
private readonly Architecture _currentProcessArchitecture = RuntimeInformation.ProcessArchitecture;
3635

3736
private Task? _connectionLoopTask;
@@ -90,7 +89,7 @@ protected override string ToolName
9089
if (TargetPath.ItemSpec.EndsWith(".dll", StringComparison.InvariantCultureIgnoreCase))
9190
{
9291
Log.LogMessage(MessageImportance.Low, $"Target path is a dll '{TargetPath.ItemSpec}'");
93-
return DotnetRunnerName + (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ".exe" : string.Empty);
92+
return DotnetRunnerName;
9493
}
9594

9695
// If the target is an exe and we're not on Windows we try with the mono runner.
@@ -121,39 +120,27 @@ protected override string ToolName
121120
// We look for dotnet muxer only if we're not running with mono.
122121
if (dotnetRunnerName != MonoRunnerName)
123122
{
124-
if (!IsCurrentProcessArchitectureCompatible())
123+
if (DotnetHostPath is not null && File.Exists(DotnetHostPath.ItemSpec) && IsCurrentProcessArchitectureCompatible())
125124
{
126-
Log.LogMessage(MessageImportance.Low, $"Current process architecture '{_currentProcessArchitecture}' is not compatible with '{TestArchitecture.ItemSpec}'");
127-
PlatformArchitecture targetArchitecture = EnumPolyfill.Parse<PlatformArchitecture>(TestArchitecture.ItemSpec, ignoreCase: true);
128-
StringBuilder resolutionLog = new();
129-
DotnetMuxerLocator dotnetMuxerLocator = new(log => resolutionLog.AppendLine(log));
130-
if (dotnetMuxerLocator.TryGetDotnetPathByArchitecture(targetArchitecture, out string? dotnetPath))
131-
{
132-
Log.LogMessage(MessageImportance.Low, resolutionLog.ToString());
133-
Log.LogMessage(MessageImportance.Low, $"dotnet muxer tool path found using architecture: '{TestArchitecture.ItemSpec}' '{dotnetPath}'");
134-
return dotnetPath;
135-
}
136-
else
137-
{
138-
Log.LogMessage(MessageImportance.Low, resolutionLog.ToString());
139-
Log.LogError(string.Format(CultureInfo.InvariantCulture, Resources.MSBuildResources.IncompatibleArchitecture, dotnetRunnerName, TestArchitecture.ItemSpec));
140-
return null;
141-
}
125+
Log.LogMessage(MessageImportance.Low, $"dotnet muxer tool path found using DOTNET_HOST_PATH environment variable: '{DotnetHostPath.ItemSpec}'");
126+
return DotnetHostPath.ItemSpec;
127+
}
128+
129+
Log.LogMessage(MessageImportance.Low, $"Current process architecture '{_currentProcessArchitecture}'. Requested test architecture '{TestArchitecture.ItemSpec}'");
130+
PlatformArchitecture targetArchitecture = EnumPolyfill.Parse<PlatformArchitecture>(TestArchitecture.ItemSpec, ignoreCase: true);
131+
StringBuilder resolutionLog = new();
132+
DotnetMuxerLocator dotnetMuxerLocator = new(log => resolutionLog.AppendLine(log));
133+
if (dotnetMuxerLocator.TryGetDotnetPathByArchitecture(targetArchitecture, out string? dotnetPath))
134+
{
135+
Log.LogMessage(MessageImportance.Low, resolutionLog.ToString());
136+
Log.LogMessage(MessageImportance.Low, $"dotnet muxer tool path found using architecture: '{TestArchitecture.ItemSpec}' '{dotnetPath}'");
137+
return dotnetPath;
142138
}
143139
else
144140
{
145-
if (DotnetHostPath is not null && File.Exists(DotnetHostPath.ItemSpec))
146-
{
147-
Log.LogMessage(MessageImportance.Low, $"dotnet muxer tool path found using DOTNET_HOST_PATH environment variable: '{DotnetHostPath.ItemSpec}'");
148-
return DotnetHostPath.ItemSpec;
149-
}
150-
151-
ProcessModule? mainModule = _currentProcess.MainModule;
152-
if (mainModule != null && Path.GetFileName(mainModule.FileName)!.Equals(dotnetRunnerName, StringComparison.OrdinalIgnoreCase))
153-
{
154-
Log.LogMessage(MessageImportance.Low, $"dotnet muxer tool path found using current process: '{mainModule.FileName}' architecture: '{_currentProcessArchitecture}'");
155-
return mainModule.FileName;
156-
}
141+
Log.LogMessage(MessageImportance.Low, resolutionLog.ToString());
142+
Log.LogError(string.Format(CultureInfo.InvariantCulture, Resources.MSBuildResources.IncompatibleArchitecture, dotnetRunnerName, TestArchitecture.ItemSpec));
143+
return null;
157144
}
158145
}
159146

@@ -180,24 +167,25 @@ protected override string GenerateCommandLineCommands()
180167
{
181168
Build.Utilities.CommandLineBuilder builder = new();
182169

183-
if (IsNetCoreApp)
170+
if (ToolName == DotnetRunnerName && IsNetCoreApp)
184171
{
185-
string dotnetRunnerName = ToolName;
186-
if (dotnetRunnerName != MonoRunnerName && Path.GetFileName(_currentProcess.MainModule!.FileName!).Equals(dotnetRunnerName, StringComparison.OrdinalIgnoreCase))
187-
{
188-
builder.AppendSwitch("exec");
189-
builder.AppendFileNameIfNotNull(TargetPath.ItemSpec);
190-
}
172+
// In most cases, if ToolName is "dotnet.exe", that means we are given a "dll" file.
173+
// In turn, that means we are not .NET Framework (because we will use Exe otherwise).
174+
// In case ToolName ended up being "dotnet.exe" and we are
175+
// .NET Framework, that means it's the user's assembly that is named "dotnet".
176+
// In that case, we want to execute the tool (user's executable) directly.
177+
// So, we only only "exec" if we are .NETCoreApp
178+
builder.AppendSwitch("exec");
179+
builder.AppendFileNameIfNotNull(TargetPath.ItemSpec);
191180
}
192-
else
181+
else if (ToolName == MonoRunnerName)
193182
{
194-
// If the target is an exe and we're not on Windows we try with the mono runner and so we pass the test module to the mono runner.
195-
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && TargetPath.ItemSpec.EndsWith(".exe", StringComparison.InvariantCultureIgnoreCase))
196-
{
197-
builder.AppendFileNameIfNotNull(TargetPath.ItemSpec);
198-
}
183+
// If ToolName is "mono", that means TargetPath is an "exe" file and we are not running on Windows.
184+
// In this case, we use the given exe file as an argument to mono.
185+
builder.AppendFileNameIfNotNull(TargetPath.ItemSpec);
199186
}
200187

188+
// If we are not "dotnet.exe" and not "mono", then we are given an executable from user and we are running on Windows.
201189
builder.AppendSwitchIfNotNull($"--{MSBuildConstants.MSBuildNodeOptionKey} ", _pipeNameDescription.Name);
202190

203191
if (!string.IsNullOrEmpty(TestingPlatformCommandLineArguments?.ItemSpec))

test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/Helpers/AcceptanceTestBase.cs

+25
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,31 @@ public static void ClassCleanup()
141141
}
142142
}
143143

144+
// https://github.com/NuGet/NuGet.Client/blob/c5934bdcbc578eec1e2921f49e6a5d53481c5099/test/NuGet.Core.FuncTests/Msbuild.Integration.Test/MsbuildIntegrationTestFixture.cs#L65-L94
145+
private protected static async Task<string> FindMsbuildWithVsWhereAsync()
146+
{
147+
string vswherePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86), "Microsoft Visual Studio", "Installer", "vswhere.exe");
148+
var commandLine = new TestInfrastructure.CommandLine();
149+
await commandLine.RunAsync($"\"{vswherePath}\" -latest -prerelease -requires Microsoft.Component.MSBuild -find MSBuild\\**\\Bin\\MSBuild.exe");
150+
151+
string? path = null;
152+
using (var stringReader = new StringReader(commandLine.StandardOutput))
153+
{
154+
string? line;
155+
while ((line = await stringReader.ReadLineAsync()) != null)
156+
{
157+
if (path != null)
158+
{
159+
throw new Exception("vswhere returned more than 1 line");
160+
}
161+
162+
path = line;
163+
}
164+
}
165+
166+
return path!;
167+
}
168+
144169
private static string ExtractVersionFromPackage(string rootFolder, string packagePrefixName)
145170
{
146171
string[] matches = Directory.GetFiles(rootFolder, packagePrefixName + "*" + NuGetPackageExtensionName, SearchOption.TopDirectoryOnly);

test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/MSBuildTests.Test.cs

+23
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,29 @@ private async Task InvokeTestingPlatform_Target_Should_Build_Without_Warnings_An
9999
}
100100
}
101101

102+
[TestMethod]
103+
[OSCondition(OperatingSystems.Windows)]
104+
public async Task RunUsingTestTargetWithNetfxMSBuild()
105+
{
106+
using TestAsset testAsset = await TestAsset.GenerateAssetAsync(
107+
AssetName,
108+
SourceCode
109+
.PatchCodeWithReplace("$PlatformTarget$", string.Empty)
110+
.PatchCodeWithReplace("$TargetFrameworks$", $"<TargetFramework>{TargetFrameworks.NetCurrent}</TargetFramework>")
111+
.PatchCodeWithReplace("$AssertValue$", bool.TrueString.ToLowerInvariant())
112+
.PatchCodeWithReplace("$MicrosoftTestingPlatformVersion$", MicrosoftTestingPlatformVersion));
113+
114+
string msbuildExe = await FindMsbuildWithVsWhereAsync();
115+
var commandLine = new TestInfrastructure.CommandLine();
116+
string binlogFile = Path.Combine(testAsset.TargetAssetPath, Guid.NewGuid().ToString("N"), "msbuild.binlog");
117+
await commandLine.RunAsync($"\"{msbuildExe}\" {testAsset.TargetAssetPath} /t:Restore");
118+
await commandLine.RunAsync($"\"{msbuildExe}\" {testAsset.TargetAssetPath} /t:\"Build;Test\" /bl:\"{binlogFile}\"", environmentVariables: new Dictionary<string, string?>()
119+
{
120+
["DOTNET_ROOT"] = string.Empty,
121+
});
122+
StringAssert.Contains(commandLine.StandardOutput, "Tests succeeded");
123+
}
124+
102125
[TestMethod]
103126
public async Task Invoke_DotnetTest_With_Arch_Switch_x86_Should_Work()
104127
{

test/Utilities/Microsoft.Testing.TestInfrastructure/CommandLine.cs

+14-3
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,19 @@ public async Task RunAsync(
5454
}
5555
}
5656

57+
private static (string Command, string Arguments) GetCommandAndArguments(string commandLine)
58+
{
59+
// Hacky way to split command and arguments that works with the limited cases we use in our tests.
60+
if (!commandLine.StartsWith('"'))
61+
{
62+
string[] tokens = commandLine.Split(' ');
63+
return (tokens[0], string.Join(" ", tokens.Skip(1)));
64+
}
65+
66+
int endQuote = commandLine.IndexOf('"', 1);
67+
return (commandLine.Substring(1, endQuote - 1), commandLine.Substring(endQuote + 2));
68+
}
69+
5770
public async Task<int> RunAsyncAndReturnExitCodeAsync(
5871
string commandLine,
5972
IDictionary<string, string?>? environmentVariables = null,
@@ -65,9 +78,7 @@ public async Task<int> RunAsyncAndReturnExitCodeAsync(
6578
try
6679
{
6780
Interlocked.Increment(ref s_totalProcessesAttempt);
68-
string[] tokens = commandLine.Split(' ');
69-
string command = tokens[0];
70-
string arguments = string.Join(" ", tokens.Skip(1));
81+
(string command, string arguments) = GetCommandAndArguments(commandLine);
7182
_errorOutputLines.Clear();
7283
_standardOutputLines.Clear();
7384
ProcessConfiguration startInfo = new(command)

0 commit comments

Comments
 (0)