diff --git a/tests/FSharp.Test.Utilities/FSharp.Test.Utilities.fsproj b/tests/FSharp.Test.Utilities/FSharp.Test.Utilities.fsproj
index 7bc4f6e058f..d899b551e30 100644
--- a/tests/FSharp.Test.Utilities/FSharp.Test.Utilities.fsproj
+++ b/tests/FSharp.Test.Utilities/FSharp.Test.Utilities.fsproj
@@ -102,7 +102,8 @@
- $(NoWarn);NU1510
+ $(NoWarn);NU1510;44
+
diff --git a/tests/FSharp.Test.Utilities/VSInstallDiscovery.fs b/tests/FSharp.Test.Utilities/VSInstallDiscovery.fs
index e491ea329bd..e59eec8eed2 100644
--- a/tests/FSharp.Test.Utilities/VSInstallDiscovery.fs
+++ b/tests/FSharp.Test.Utilities/VSInstallDiscovery.fs
@@ -126,3 +126,55 @@ module VSInstallDiscovery =
| NotFound reason ->
logAction $"Visual Studio installation not found: {reason}"
None
+
+ /// Gets the VS installation directory or fails with a detailed error message.
+ /// This is the recommended method for test scenarios that require VS to be installed.
+ let getVSInstallDirOrFail () : string =
+ match tryFindVSInstallation () with
+ | Found (path, _) -> path
+ | NotFound reason ->
+ failwith $"Visual Studio installation not found: {reason}. Ensure VS is installed or environment variables (VSAPPIDDIR, VS*COMNTOOLS) are set."
+
+/// Assembly resolver for Visual Studio test infrastructure.
+/// Provides centralized assembly resolution for VS integration tests.
+module VSAssemblyResolver =
+ open System
+ open System.IO
+ open System.Reflection
+ open System.Globalization
+
+ /// Adds an assembly resolver that probes Visual Studio installation directories.
+ /// This should be called early in test initialization to ensure VS assemblies can be loaded.
+ let addResolver () =
+ let vsInstallDir = VSInstallDiscovery.getVSInstallDirOrFail ()
+
+ let probingPaths =
+ [|
+ Path.Combine(vsInstallDir, @"IDE\CommonExtensions\Microsoft\Editor")
+ Path.Combine(vsInstallDir, @"IDE\PublicAssemblies")
+ Path.Combine(vsInstallDir, @"IDE\PrivateAssemblies")
+ Path.Combine(vsInstallDir, @"IDE\CommonExtensions\Microsoft\ManagedLanguages\VBCSharp\LanguageServices")
+ Path.Combine(vsInstallDir, @"IDE\Extensions\Microsoft\CodeSense\Framework")
+ Path.Combine(vsInstallDir, @"IDE")
+ |]
+
+ AppDomain.CurrentDomain.add_AssemblyResolve(fun _ args ->
+ let found () =
+ probingPaths
+ |> Seq.tryPick (fun p ->
+ try
+ let name = AssemblyName(args.Name)
+ let codebase = Path.GetFullPath(Path.Combine(p, name.Name) + ".dll")
+ if File.Exists(codebase) then
+ name.CodeBase <- codebase
+ name.CultureInfo <- Unchecked.defaultof
+ name.Version <- Unchecked.defaultof
+ Some name
+ else
+ None
+ with _ ->
+ None)
+
+ match found () with
+ | None -> Unchecked.defaultof
+ | Some name -> Assembly.Load(name))
diff --git a/vsintegration/tests/FSharp.Editor.Tests/Helpers/AssemblyResolver.fs b/vsintegration/tests/FSharp.Editor.Tests/Helpers/AssemblyResolver.fs
index 78a57793517..ef8c01ca908 100644
--- a/vsintegration/tests/FSharp.Editor.Tests/Helpers/AssemblyResolver.fs
+++ b/vsintegration/tests/FSharp.Editor.Tests/Helpers/AssemblyResolver.fs
@@ -2,62 +2,9 @@
namespace FSharp.Editor.Tests.Helpers
-open System
-open System.IO
-open System.Reflection
-
module AssemblyResolver =
- open System.Globalization
- open FSharp.Test.VSInstallDiscovery
-
- let vsInstallDir =
- // Use centralized VS installation discovery with graceful fallback
- match tryGetVSInstallDir () with
- | Some dir -> dir
- | None ->
- // Fallback to legacy behavior for backward compatibility
- let vsvar =
- let var = Environment.GetEnvironmentVariable("VS170COMNTOOLS")
-
- if String.IsNullOrEmpty var then
- Environment.GetEnvironmentVariable("VSAPPIDDIR")
- else
- var
-
- if String.IsNullOrEmpty vsvar then
- failwith "VS170COMNTOOLS and VSAPPIDDIR environment variables not found."
-
- Path.Combine(vsvar, "..")
-
- let probingPaths =
- [|
- Path.Combine(vsInstallDir, @"IDE\CommonExtensions\Microsoft\Editor")
- Path.Combine(vsInstallDir, @"IDE\PublicAssemblies")
- Path.Combine(vsInstallDir, @"IDE\PrivateAssemblies")
- Path.Combine(vsInstallDir, @"IDE\CommonExtensions\Microsoft\ManagedLanguages\VBCSharp\LanguageServices")
- Path.Combine(vsInstallDir, @"IDE\Extensions\Microsoft\CodeSense\Framework")
- Path.Combine(vsInstallDir, @"IDE")
- |]
-
- let addResolver () =
- AppDomain.CurrentDomain.add_AssemblyResolve (fun h args ->
- let found () =
- (probingPaths)
- |> Seq.tryPick (fun p ->
- try
- let name = AssemblyName(args.Name)
- let codebase = Path.GetFullPath(Path.Combine(p, name.Name) + ".dll")
-
- if File.Exists(codebase) then
- name.CodeBase <- codebase
- name.CultureInfo <- Unchecked.defaultof
- name.Version <- Unchecked.defaultof
- Some(name)
- else
- None
- with _ ->
- None)
+ open FSharp.Test.VSAssemblyResolver
- match found () with
- | None -> Unchecked.defaultof
- | Some name -> Assembly.Load(name))
+ /// Adds an assembly resolver that probes Visual Studio installation directories.
+ /// This is a compatibility shim that delegates to the centralized implementation.
+ let addResolver = addResolver
diff --git a/vsintegration/tests/Salsa/VisualFSharp.Salsa.fsproj b/vsintegration/tests/Salsa/VisualFSharp.Salsa.fsproj
index f7410214430..d41b116c21e 100644
--- a/vsintegration/tests/Salsa/VisualFSharp.Salsa.fsproj
+++ b/vsintegration/tests/Salsa/VisualFSharp.Salsa.fsproj
@@ -24,6 +24,9 @@
UnitTests.TestLib.Utils.fs
+
+ VSInstallDiscovery.fs
+
diff --git a/vsintegration/tests/Salsa/VsMocks.fs b/vsintegration/tests/Salsa/VsMocks.fs
index 1ce67c04763..54fe8b906de 100644
--- a/vsintegration/tests/Salsa/VsMocks.fs
+++ b/vsintegration/tests/Salsa/VsMocks.fs
@@ -1642,6 +1642,7 @@ module internal VsActual =
open System.ComponentModel.Composition.Primitives
open Microsoft.VisualStudio.Text
open Microsoft.VisualStudio.Threading
+ open FSharp.Test.VSInstallDiscovery
type TestExportJoinableTaskContext () =
@@ -1650,22 +1651,7 @@ module internal VsActual =
[)>]
member public _.JoinableTaskContext : JoinableTaskContext = jtc
- let vsInstallDir =
- // use the environment variable to find the VS installdir
- let vsvar =
- // Try VS180COMNTOOLS first, then VS170COMNTOOLS, then VSAPPIDDIR
- // TODO : use tryGetVSInstallDir from test utils instead
- let var18 = Environment.GetEnvironmentVariable("VS180COMNTOOLS")
- if String.IsNullOrEmpty var18 then
- let var17 = Environment.GetEnvironmentVariable("VS170COMNTOOLS")
- if String.IsNullOrEmpty var17 then
- Environment.GetEnvironmentVariable("VSAPPIDDIR")
- else
- var17
- else
- var18
- if String.IsNullOrEmpty vsvar then failwith "VS180COMNTOOLS, VS170COMNTOOLS and VSAPPIDDIR environment variables not found."
- Path.Combine(vsvar, "..")
+ let vsInstallDir = getVSInstallDirOrFail ()
let CreateEditorCatalog() =
let thisAssembly = Assembly.GetExecutingAssembly().Location
diff --git a/vsintegration/tests/UnitTests/AssemblyResolver.fs b/vsintegration/tests/UnitTests/AssemblyResolver.fs
index 38b5ee45290..cf36b723e40 100644
--- a/vsintegration/tests/UnitTests/AssemblyResolver.fs
+++ b/vsintegration/tests/UnitTests/AssemblyResolver.fs
@@ -1,52 +1,8 @@
namespace Microsoft.VisualStudio.FSharp
-open System
-open System.IO
-open System.Reflection
-
module AssemblyResolver =
- open System.Globalization
- open FSharp.Test.VSInstallDiscovery
-
- let vsInstallDir =
- // Use centralized VS installation discovery with graceful fallback
- match tryGetVSInstallDir () with
- | Some dir -> dir
- | None ->
- // Fallback to legacy behavior for backward compatibility
- let vsvar =
- let var = Environment.GetEnvironmentVariable("VS170COMNTOOLS")
- if String.IsNullOrEmpty var then
- Environment.GetEnvironmentVariable("VSAPPIDDIR")
- else
- var
- if String.IsNullOrEmpty vsvar then failwith "VS170COMNTOOLS and VSAPPIDDIR environment variables not found."
- Path.Combine(vsvar, "..")
-
- let probingPaths = [|
- Path.Combine(vsInstallDir, @"IDE\CommonExtensions\Microsoft\Editor")
- Path.Combine(vsInstallDir, @"IDE\PublicAssemblies")
- Path.Combine(vsInstallDir, @"IDE\PrivateAssemblies")
- Path.Combine(vsInstallDir, @"IDE\CommonExtensions\Microsoft\ManagedLanguages\VBCSharp\LanguageServices")
- Path.Combine(vsInstallDir, @"IDE\Extensions\Microsoft\CodeSense\Framework")
- Path.Combine(vsInstallDir, @"IDE")
- |]
+ open FSharp.Test.VSAssemblyResolver
- let addResolver () =
- AppDomain.CurrentDomain.add_AssemblyResolve(fun h args ->
- let found () =
- (probingPaths ) |> Seq.tryPick(fun p ->
- try
- let name = AssemblyName(args.Name)
- let codebase = Path.GetFullPath(Path.Combine(p, name.Name) + ".dll")
- if File.Exists(codebase) then
- name.CodeBase <- codebase
- name.CultureInfo <- Unchecked.defaultof
- name.Version <- Unchecked.defaultof
- Some (name)
- else None
- with | _ -> None
- )
- match found() with
- | None -> Unchecked.defaultof
- | Some name -> Assembly.Load(name) )
+ /// Adds an assembly resolver that probes Visual Studio installation directories.
+ /// This is a compatibility shim that delegates to the centralized implementation.
+ let addResolver = addResolver