Skip to content

Commit a065eae

Browse files
authored
Make RuntimeHostInfoTests more robust (#81080)
1 parent af5cb34 commit a065eae

File tree

5 files changed

+131
-113
lines changed

5 files changed

+131
-113
lines changed

src/Compilers/Core/MSBuildTaskTests/RuntimeHostInfoTests.cs

Lines changed: 61 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33
// See the LICENSE file in the project root for more information.
44

5-
using System;
65
using System.IO;
76
using System.Runtime.InteropServices;
7+
using Microsoft.CodeAnalysis.CommandLine;
88
using Microsoft.CodeAnalysis.Test.Utilities;
99
using Roslyn.Test.Utilities;
1010
using Roslyn.Utilities;
@@ -13,75 +13,91 @@
1313

1414
namespace Microsoft.CodeAnalysis.BuildTasks.UnitTests;
1515

16-
public sealed class RuntimeHostInfoTests(ITestOutputHelper output)
16+
public sealed class RuntimeHostInfoTests(ITestOutputHelper output) : TestBase
1717
{
1818
private readonly ITestOutputHelper _output = output;
1919

20+
/// <summary>
21+
/// Normalizes a path by resolving any symbolic links or subst drives (on Windows).
22+
/// This ensures paths can be compared even when one is a subst drive (e.g., T:\ -> D:\a\_work\1\s\artifacts\tmp\Debug).
23+
/// </summary>
24+
private static string NormalizePath(string path)
25+
{
26+
// Our test infra uses `subst` which the .NET Core's implementation of `ResolveLinkTarget` wouldn't resolve,
27+
// hence we always use our Win32 polyfill on Windows to ensure paths are fully normalized and can be compared in tests.
28+
var resolvedPath = PlatformInformation.IsWindows
29+
? NativeMethods.ResolveLinkTargetWin32(path, returnFinalTarget: true)
30+
: File.ResolveLinkTarget(path, returnFinalTarget: true);
31+
if (resolvedPath != null)
32+
{
33+
return resolvedPath.FullName;
34+
}
35+
36+
return path;
37+
}
38+
2039
[Fact, WorkItem("https://github.com/dotnet/msbuild/issues/12669")]
2140
public void DotNetInPath()
2241
{
23-
var previousPath = Environment.GetEnvironmentVariable("PATH");
24-
try
25-
{
26-
using var tempRoot = new TempRoot();
27-
var testDir = tempRoot.CreateDirectory();
28-
var globalDotNetDir = testDir.CreateDirectory("global-dotnet");
29-
var globalDotNetExe = globalDotNetDir.CreateFile($"dotnet{PlatformInformation.ExeExtension}");
30-
Environment.SetEnvironmentVariable("PATH", globalDotNetDir.Path);
42+
using var tempRoot = new TempRoot();
43+
var testDir = tempRoot.CreateDirectory();
44+
var globalDotNetDir = testDir.CreateDirectory("global-dotnet");
45+
var globalDotNetExe = globalDotNetDir.CreateFile($"dotnet{PlatformInformation.ExeExtension}");
3146

32-
Assert.Equal(globalDotNetDir.Path, RuntimeHostInfo.GetToolDotNetRoot(_output.WriteLine));
33-
}
34-
finally
35-
{
36-
Environment.SetEnvironmentVariable("PATH", previousPath);
37-
}
47+
var result = ApplyEnvironmentVariables(
48+
[
49+
new("PATH", globalDotNetDir.Path),
50+
new(RuntimeHostInfo.DotNetHostPathEnvironmentName, ""),
51+
new(RuntimeHostInfo.DotNetExperimentalHostPathEnvironmentName, ""),
52+
],
53+
() => RuntimeHostInfo.GetToolDotNetRoot(_output.WriteLine));
54+
55+
Assert.NotNull(result);
56+
AssertEx.Equal(NormalizePath(globalDotNetDir.Path), NormalizePath(result));
3857
}
3958

4059
[Fact, WorkItem("https://github.com/dotnet/msbuild/issues/12669")]
4160
public void DotNetInPath_None()
4261
{
43-
var previousPath = Environment.GetEnvironmentVariable("PATH");
44-
try
45-
{
46-
Environment.SetEnvironmentVariable("PATH", "");
62+
var result = ApplyEnvironmentVariables(
63+
[
64+
new("PATH", ""),
65+
new(RuntimeHostInfo.DotNetHostPathEnvironmentName, ""),
66+
new(RuntimeHostInfo.DotNetExperimentalHostPathEnvironmentName, ""),
67+
],
68+
() => RuntimeHostInfo.GetToolDotNetRoot(_output.WriteLine));
4769

48-
Assert.Null(RuntimeHostInfo.GetToolDotNetRoot(_output.WriteLine));
49-
}
50-
finally
51-
{
52-
Environment.SetEnvironmentVariable("PATH", previousPath);
53-
}
70+
Assert.Null(result);
5471
}
5572

5673
[Fact, WorkItem("https://github.com/dotnet/msbuild/issues/12669")]
5774
public void DotNetInPath_Symlinked()
5875
{
59-
var previousPath = Environment.GetEnvironmentVariable("PATH");
60-
try
61-
{
62-
using var tempRoot = new TempRoot();
63-
var testDir = tempRoot.CreateDirectory();
64-
var globalDotNetDir = testDir.CreateDirectory("global-dotnet");
65-
var globalDotNetExe = globalDotNetDir.CreateFile($"dotnet{PlatformInformation.ExeExtension}");
66-
var binDir = testDir.CreateDirectory("bin");
67-
var symlinkPath = Path.Combine(binDir.Path, $"dotnet{PlatformInformation.ExeExtension}");
76+
using var tempRoot = new TempRoot();
77+
var testDir = tempRoot.CreateDirectory();
78+
var globalDotNetDir = testDir.CreateDirectory("global-dotnet");
79+
var globalDotNetExe = globalDotNetDir.CreateFile($"dotnet{PlatformInformation.ExeExtension}");
80+
var binDir = testDir.CreateDirectory("bin");
81+
var symlinkPath = Path.Combine(binDir.Path, $"dotnet{PlatformInformation.ExeExtension}");
6882

69-
// Create symlink from binDir to the actual dotnet executable
70-
File.CreateSymbolicLink(path: symlinkPath, pathToTarget: globalDotNetExe.Path);
83+
// Create symlink from binDir to the actual dotnet executable
84+
File.CreateSymbolicLink(path: symlinkPath, pathToTarget: globalDotNetExe.Path);
7185

72-
Environment.SetEnvironmentVariable("PATH", binDir.Path);
86+
var result = ApplyEnvironmentVariables(
87+
[
88+
new("PATH", binDir.Path),
89+
new(RuntimeHostInfo.DotNetHostPathEnvironmentName, ""),
90+
new(RuntimeHostInfo.DotNetExperimentalHostPathEnvironmentName, ""),
91+
],
92+
() => RuntimeHostInfo.GetToolDotNetRoot(_output.WriteLine));
7393

74-
Assert.Equal(globalDotNetDir.Path, RuntimeHostInfo.GetToolDotNetRoot(_output.WriteLine));
75-
}
76-
finally
77-
{
78-
Environment.SetEnvironmentVariable("PATH", previousPath);
79-
}
94+
Assert.NotNull(result);
95+
AssertEx.Equal(NormalizePath(globalDotNetDir.Path), NormalizePath(result));
8096
}
8197
}
8298

8399
#if !NET
84-
file static class NativeMethods
100+
file static class TestNativeMethods
85101
{
86102
extension(File)
87103
{

src/Compilers/Server/VBCSCompilerTests/CompilerServerTests.cs

Lines changed: 0 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,12 @@
66

77
using System;
88
using System.Collections.Generic;
9-
using System.Diagnostics;
109
using System.IO;
1110
using System.Linq;
1211
using System.Reflection;
13-
using System.Runtime.InteropServices;
1412
using System.Text;
15-
using System.Threading;
1613
using System.Threading.Tasks;
1714
using Basic.Reference.Assemblies;
18-
using Castle.Core.Resource;
1915
using Microsoft.CodeAnalysis.CommandLine;
2016
using Microsoft.CodeAnalysis.Test.Utilities;
2117
using Roslyn.Test.Utilities;
@@ -117,35 +113,6 @@ private static void CreateFiles(TempDirectory currentDirectory, IEnumerable<KeyV
117113
}
118114
}
119115

120-
private static T ApplyEnvironmentVariables<T>(
121-
IEnumerable<KeyValuePair<string, string>> environmentVariables,
122-
Func<T> func)
123-
{
124-
if (environmentVariables == null)
125-
{
126-
return func();
127-
}
128-
129-
var resetVariables = new Dictionary<string, string>();
130-
try
131-
{
132-
foreach (var variable in environmentVariables)
133-
{
134-
resetVariables.Add(variable.Key, Environment.GetEnvironmentVariable(variable.Key));
135-
Environment.SetEnvironmentVariable(variable.Key, variable.Value);
136-
}
137-
138-
return func();
139-
}
140-
finally
141-
{
142-
foreach (var variable in resetVariables)
143-
{
144-
Environment.SetEnvironmentVariable(variable.Key, variable.Value);
145-
}
146-
}
147-
}
148-
149116
private static (T Result, string Output) UseTextWriter<T>(Encoding encoding, Func<TextWriter, T> func)
150117
{
151118
MemoryStream memoryStream;

src/Compilers/Shared/NativeMethods.cs

Lines changed: 35 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -104,33 +104,43 @@ out PROCESS_INFORMATION lpProcessInformation
104104
{
105105
public static FileSystemInfo? ResolveLinkTarget(string path, bool returnFinalTarget)
106106
{
107-
if (!returnFinalTarget) throw new NotSupportedException();
108-
109-
using var handle = CreateFileW(
110-
lpFileName: path,
111-
dwDesiredAccess: FILE_READ_ATTRIBUTES,
112-
dwShareMode: FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
113-
lpSecurityAttributes: IntPtr.Zero,
114-
dwCreationDisposition: OPEN_EXISTING,
115-
dwFlagsAndAttributes: FILE_FLAG_BACKUP_SEMANTICS, // needed for directories
116-
hTemplateFile: IntPtr.Zero);
117-
118-
if (handle.IsInvalid)
119-
{
120-
return null;
121-
}
122-
123-
uint flags = FILE_NAME_NORMALIZED | VOLUME_NAME_DOS;
124-
uint needed = GetFinalPathNameByHandleW(hFile: handle, lpszFilePath: null, cchFilePath: 0, dwFlags: flags);
125-
if (needed == 0) return null;
126-
127-
var sb = new StringBuilder((int)needed + 1);
128-
uint len = GetFinalPathNameByHandleW(hFile: handle, lpszFilePath: sb, cchFilePath: (uint)sb.Capacity, dwFlags: flags);
129-
if (len == 0) return null;
130-
131-
return new FileInfo(TrimWin32ExtendedPrefix(sb.ToString()));
107+
return ResolveLinkTargetWin32(path, returnFinalTarget);
132108
}
133109
}
110+
#endif
111+
112+
/// <remarks>
113+
/// Unlike .NET Core's implementation of <c>File.ResolveLinkTarget</c>,
114+
/// this resolves virtual disk mappings (created via <c>subst</c>).
115+
/// </remarks>
116+
public static FileSystemInfo? ResolveLinkTargetWin32(string path, bool returnFinalTarget)
117+
{
118+
if (!returnFinalTarget) throw new NotSupportedException();
119+
120+
using var handle = CreateFileW(
121+
lpFileName: path,
122+
dwDesiredAccess: FILE_READ_ATTRIBUTES,
123+
dwShareMode: FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
124+
lpSecurityAttributes: IntPtr.Zero,
125+
dwCreationDisposition: OPEN_EXISTING,
126+
dwFlagsAndAttributes: FILE_FLAG_BACKUP_SEMANTICS, // needed for directories
127+
hTemplateFile: IntPtr.Zero);
128+
129+
if (handle.IsInvalid)
130+
{
131+
return null;
132+
}
133+
134+
uint flags = FILE_NAME_NORMALIZED | VOLUME_NAME_DOS;
135+
uint needed = GetFinalPathNameByHandleW(hFile: handle, lpszFilePath: null, cchFilePath: 0, dwFlags: flags);
136+
if (needed == 0) return null;
137+
138+
var sb = new StringBuilder((int)needed + 1);
139+
uint len = GetFinalPathNameByHandleW(hFile: handle, lpszFilePath: sb, cchFilePath: (uint)sb.Capacity, dwFlags: flags);
140+
if (len == 0) return null;
141+
142+
return new FileInfo(TrimWin32ExtendedPrefix(sb.ToString()));
143+
}
134144

135145
private static string TrimWin32ExtendedPrefix(string s)
136146
{
@@ -173,7 +183,6 @@ private static extern uint GetFinalPathNameByHandleW(
173183
StringBuilder? lpszFilePath,
174184
uint cchFilePath,
175185
uint dwFlags);
176-
#endif
177186

178187
}
179188
}

src/Compilers/Shared/RuntimeHostInfo.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ internal static class RuntimeHostInfo
2727
#endif
2828

2929
internal const string DotNetRootEnvironmentName = "DOTNET_ROOT";
30-
private const string DotNetHostPathEnvironmentName = "DOTNET_HOST_PATH";
31-
private const string DotNetExperimentalHostPathEnvironmentName = "DOTNET_EXPERIMENTAL_HOST_PATH";
30+
internal const string DotNetHostPathEnvironmentName = "DOTNET_HOST_PATH";
31+
internal const string DotNetExperimentalHostPathEnvironmentName = "DOTNET_EXPERIMENTAL_HOST_PATH";
3232

3333
/// <summary>
3434
/// The <c>DOTNET_ROOT</c> that should be used when launching executable tools.
@@ -48,7 +48,7 @@ internal static class RuntimeHostInfo
4848
}
4949
catch (Exception ex)
5050
{
51-
logger?.Invoke("Failed to resolve symbolic link for dotnet path '{0}': {1}", [dotNetPath, ex.Message]);
51+
logger?.Invoke("Failed to resolve symbolic link for dotnet path '{0}': {1}", [dotNetPath, ex]);
5252
return null;
5353
}
5454

src/Compilers/Test/Core/TestBase.cs

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,17 @@
33
// See the LICENSE file in the project root for more information.
44

55
using System;
6-
using System.Diagnostics;
6+
using System.Collections.Generic;
7+
using System.Globalization;
78
using System.IO;
89
using System.Reflection;
910
using System.Threading;
1011
using System.Xml.Linq;
12+
using Basic.Reference.Assemblies;
1113
using Microsoft.CodeAnalysis;
1214
using Microsoft.CodeAnalysis.CSharp;
1315
using Microsoft.CodeAnalysis.Test.Utilities;
1416
using Microsoft.CodeAnalysis.Text;
15-
using Microsoft.CodeAnalysis.VisualBasic;
16-
using static TestReferences.NetFx;
17-
using Basic.Reference.Assemblies;
18-
using Roslyn.Utilities;
19-
using System.Globalization;
2017

2118
namespace Roslyn.Test.Utilities
2219
{
@@ -363,5 +360,34 @@ internal static DiagnosticDescription Diagnostic(
363360
}
364361

365362
#endregion
363+
364+
public static T ApplyEnvironmentVariables<T>(
365+
IEnumerable<KeyValuePair<string, string?>> environmentVariables,
366+
Func<T> func)
367+
{
368+
if (environmentVariables == null)
369+
{
370+
return func();
371+
}
372+
373+
var resetVariables = new Dictionary<string, string?>();
374+
try
375+
{
376+
foreach (var variable in environmentVariables)
377+
{
378+
resetVariables.Add(variable.Key, Environment.GetEnvironmentVariable(variable.Key));
379+
Environment.SetEnvironmentVariable(variable.Key, variable.Value);
380+
}
381+
382+
return func();
383+
}
384+
finally
385+
{
386+
foreach (var variable in resetVariables)
387+
{
388+
Environment.SetEnvironmentVariable(variable.Key, variable.Value);
389+
}
390+
}
391+
}
366392
}
367393
}

0 commit comments

Comments
 (0)