Skip to content

Commit 61c1a22

Browse files
authored
Centralize Visual Studio installation discovery and assembly resolution in test infrastructure (#19085)
1 parent 7367446 commit 61c1a22

File tree

6 files changed

+67
-122
lines changed

6 files changed

+67
-122
lines changed

tests/FSharp.Test.Utilities/FSharp.Test.Utilities.fsproj

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,8 @@
102102
<PackageReference Include="System.Diagnostics.DiagnosticSource" Version="$(SystemDiagnosticsDiagnosticSourceVersion)" />
103103
</ItemGroup>
104104
<PropertyGroup>
105-
<NoWarn>$(NoWarn);NU1510</NoWarn> <!-- NU1510: Project is explicitly referencing the runtime assembly 'System.Collections.Immutable', however, if we remove it, it tries to find it on the wrong path. Also, local NoWarn does not help - This is just me trying to enforce it -->
105+
<NoWarn>$(NoWarn);NU1510;44</NoWarn> <!-- NU1510: Project is explicitly referencing the runtime assembly 'System.Collections.Immutable', however, if we remove it, it tries to find it on the wrong path. Also, local NoWarn does not help - This is just me trying to enforce it -->
106+
<!-- 44: AssemblyName.CodeBase is deprecated but needed for assembly resolution in VS integration tests -->
106107
</PropertyGroup>
107108
<ItemGroup>
108109
<PackageReference Include="System.Collections.Immutable" Version="$(SystemCollectionsImmutableVersion)" GeneratePathProperty="true" />

tests/FSharp.Test.Utilities/VSInstallDiscovery.fs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,3 +126,55 @@ module VSInstallDiscovery =
126126
| NotFound reason ->
127127
logAction $"Visual Studio installation not found: {reason}"
128128
None
129+
130+
/// Gets the VS installation directory or fails with a detailed error message.
131+
/// This is the recommended method for test scenarios that require VS to be installed.
132+
let getVSInstallDirOrFail () : string =
133+
match tryFindVSInstallation () with
134+
| Found (path, _) -> path
135+
| NotFound reason ->
136+
failwith $"Visual Studio installation not found: {reason}. Ensure VS is installed or environment variables (VSAPPIDDIR, VS*COMNTOOLS) are set."
137+
138+
/// Assembly resolver for Visual Studio test infrastructure.
139+
/// Provides centralized assembly resolution for VS integration tests.
140+
module VSAssemblyResolver =
141+
open System
142+
open System.IO
143+
open System.Reflection
144+
open System.Globalization
145+
146+
/// Adds an assembly resolver that probes Visual Studio installation directories.
147+
/// This should be called early in test initialization to ensure VS assemblies can be loaded.
148+
let addResolver () =
149+
let vsInstallDir = VSInstallDiscovery.getVSInstallDirOrFail ()
150+
151+
let probingPaths =
152+
[|
153+
Path.Combine(vsInstallDir, @"IDE\CommonExtensions\Microsoft\Editor")
154+
Path.Combine(vsInstallDir, @"IDE\PublicAssemblies")
155+
Path.Combine(vsInstallDir, @"IDE\PrivateAssemblies")
156+
Path.Combine(vsInstallDir, @"IDE\CommonExtensions\Microsoft\ManagedLanguages\VBCSharp\LanguageServices")
157+
Path.Combine(vsInstallDir, @"IDE\Extensions\Microsoft\CodeSense\Framework")
158+
Path.Combine(vsInstallDir, @"IDE")
159+
|]
160+
161+
AppDomain.CurrentDomain.add_AssemblyResolve(fun _ args ->
162+
let found () =
163+
probingPaths
164+
|> Seq.tryPick (fun p ->
165+
try
166+
let name = AssemblyName(args.Name)
167+
let codebase = Path.GetFullPath(Path.Combine(p, name.Name) + ".dll")
168+
if File.Exists(codebase) then
169+
name.CodeBase <- codebase
170+
name.CultureInfo <- Unchecked.defaultof<CultureInfo>
171+
name.Version <- Unchecked.defaultof<Version>
172+
Some name
173+
else
174+
None
175+
with _ ->
176+
None)
177+
178+
match found () with
179+
| None -> Unchecked.defaultof<Assembly>
180+
| Some name -> Assembly.Load(name))

vsintegration/tests/FSharp.Editor.Tests/Helpers/AssemblyResolver.fs

Lines changed: 4 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -2,62 +2,9 @@
22

33
namespace FSharp.Editor.Tests.Helpers
44

5-
open System
6-
open System.IO
7-
open System.Reflection
8-
95
module AssemblyResolver =
10-
open System.Globalization
11-
open FSharp.Test.VSInstallDiscovery
12-
13-
let vsInstallDir =
14-
// Use centralized VS installation discovery with graceful fallback
15-
match tryGetVSInstallDir () with
16-
| Some dir -> dir
17-
| None ->
18-
// Fallback to legacy behavior for backward compatibility
19-
let vsvar =
20-
let var = Environment.GetEnvironmentVariable("VS170COMNTOOLS")
21-
22-
if String.IsNullOrEmpty var then
23-
Environment.GetEnvironmentVariable("VSAPPIDDIR")
24-
else
25-
var
26-
27-
if String.IsNullOrEmpty vsvar then
28-
failwith "VS170COMNTOOLS and VSAPPIDDIR environment variables not found."
29-
30-
Path.Combine(vsvar, "..")
31-
32-
let probingPaths =
33-
[|
34-
Path.Combine(vsInstallDir, @"IDE\CommonExtensions\Microsoft\Editor")
35-
Path.Combine(vsInstallDir, @"IDE\PublicAssemblies")
36-
Path.Combine(vsInstallDir, @"IDE\PrivateAssemblies")
37-
Path.Combine(vsInstallDir, @"IDE\CommonExtensions\Microsoft\ManagedLanguages\VBCSharp\LanguageServices")
38-
Path.Combine(vsInstallDir, @"IDE\Extensions\Microsoft\CodeSense\Framework")
39-
Path.Combine(vsInstallDir, @"IDE")
40-
|]
41-
42-
let addResolver () =
43-
AppDomain.CurrentDomain.add_AssemblyResolve (fun h args ->
44-
let found () =
45-
(probingPaths)
46-
|> Seq.tryPick (fun p ->
47-
try
48-
let name = AssemblyName(args.Name)
49-
let codebase = Path.GetFullPath(Path.Combine(p, name.Name) + ".dll")
50-
51-
if File.Exists(codebase) then
52-
name.CodeBase <- codebase
53-
name.CultureInfo <- Unchecked.defaultof<CultureInfo>
54-
name.Version <- Unchecked.defaultof<Version>
55-
Some(name)
56-
else
57-
None
58-
with _ ->
59-
None)
6+
open FSharp.Test.VSAssemblyResolver
607

61-
match found () with
62-
| None -> Unchecked.defaultof<Assembly>
63-
| Some name -> Assembly.Load(name))
8+
/// Adds an assembly resolver that probes Visual Studio installation directories.
9+
/// This is a compatibility shim that delegates to the centralized implementation.
10+
let addResolver = addResolver

vsintegration/tests/Salsa/VisualFSharp.Salsa.fsproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@
2424
<Compile Include="..\unittests\TestLib.Utils.fs">
2525
<Link>UnitTests.TestLib.Utils.fs</Link>
2626
</Compile>
27+
<Compile Include="..\..\..\tests\FSharp.Test.Utilities\VSInstallDiscovery.fs">
28+
<Link>VSInstallDiscovery.fs</Link>
29+
</Compile>
2730
<Compile Include="FSharpLanguageServiceTestable.fs" />
2831
<Compile Include="VsMocks.fs" />
2932
<Compile Include="Salsa.fs" />

vsintegration/tests/Salsa/VsMocks.fs

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1642,6 +1642,7 @@ module internal VsActual =
16421642
open System.ComponentModel.Composition.Primitives
16431643
open Microsoft.VisualStudio.Text
16441644
open Microsoft.VisualStudio.Threading
1645+
open FSharp.Test.VSInstallDiscovery
16451646

16461647
type TestExportJoinableTaskContext () =
16471648

@@ -1650,22 +1651,7 @@ module internal VsActual =
16501651
[<System.ComponentModel.Composition.Export(typeof<JoinableTaskContext>)>]
16511652
member public _.JoinableTaskContext : JoinableTaskContext = jtc
16521653

1653-
let vsInstallDir =
1654-
// use the environment variable to find the VS installdir
1655-
let vsvar =
1656-
// Try VS180COMNTOOLS first, then VS170COMNTOOLS, then VSAPPIDDIR
1657-
// TODO : use tryGetVSInstallDir from test utils instead
1658-
let var18 = Environment.GetEnvironmentVariable("VS180COMNTOOLS")
1659-
if String.IsNullOrEmpty var18 then
1660-
let var17 = Environment.GetEnvironmentVariable("VS170COMNTOOLS")
1661-
if String.IsNullOrEmpty var17 then
1662-
Environment.GetEnvironmentVariable("VSAPPIDDIR")
1663-
else
1664-
var17
1665-
else
1666-
var18
1667-
if String.IsNullOrEmpty vsvar then failwith "VS180COMNTOOLS, VS170COMNTOOLS and VSAPPIDDIR environment variables not found."
1668-
Path.Combine(vsvar, "..")
1654+
let vsInstallDir = getVSInstallDirOrFail ()
16691655

16701656
let CreateEditorCatalog() =
16711657
let thisAssembly = Assembly.GetExecutingAssembly().Location
Lines changed: 4 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,8 @@
11
namespace Microsoft.VisualStudio.FSharp
22

3-
open System
4-
open System.IO
5-
open System.Reflection
6-
73
module AssemblyResolver =
8-
open System.Globalization
9-
open FSharp.Test.VSInstallDiscovery
10-
11-
let vsInstallDir =
12-
// Use centralized VS installation discovery with graceful fallback
13-
match tryGetVSInstallDir () with
14-
| Some dir -> dir
15-
| None ->
16-
// Fallback to legacy behavior for backward compatibility
17-
let vsvar =
18-
let var = Environment.GetEnvironmentVariable("VS170COMNTOOLS")
19-
if String.IsNullOrEmpty var then
20-
Environment.GetEnvironmentVariable("VSAPPIDDIR")
21-
else
22-
var
23-
if String.IsNullOrEmpty vsvar then failwith "VS170COMNTOOLS and VSAPPIDDIR environment variables not found."
24-
Path.Combine(vsvar, "..")
25-
26-
let probingPaths = [|
27-
Path.Combine(vsInstallDir, @"IDE\CommonExtensions\Microsoft\Editor")
28-
Path.Combine(vsInstallDir, @"IDE\PublicAssemblies")
29-
Path.Combine(vsInstallDir, @"IDE\PrivateAssemblies")
30-
Path.Combine(vsInstallDir, @"IDE\CommonExtensions\Microsoft\ManagedLanguages\VBCSharp\LanguageServices")
31-
Path.Combine(vsInstallDir, @"IDE\Extensions\Microsoft\CodeSense\Framework")
32-
Path.Combine(vsInstallDir, @"IDE")
33-
|]
4+
open FSharp.Test.VSAssemblyResolver
345

35-
let addResolver () =
36-
AppDomain.CurrentDomain.add_AssemblyResolve(fun h args ->
37-
let found () =
38-
(probingPaths ) |> Seq.tryPick(fun p ->
39-
try
40-
let name = AssemblyName(args.Name)
41-
let codebase = Path.GetFullPath(Path.Combine(p, name.Name) + ".dll")
42-
if File.Exists(codebase) then
43-
name.CodeBase <- codebase
44-
name.CultureInfo <- Unchecked.defaultof<CultureInfo>
45-
name.Version <- Unchecked.defaultof<Version>
46-
Some (name)
47-
else None
48-
with | _ -> None
49-
)
50-
match found() with
51-
| None -> Unchecked.defaultof<Assembly>
52-
| Some name -> Assembly.Load(name) )
6+
/// Adds an assembly resolver that probes Visual Studio installation directories.
7+
/// This is a compatibility shim that delegates to the centralized implementation.
8+
let addResolver = addResolver

0 commit comments

Comments
 (0)