Skip to content
Open
3 changes: 2 additions & 1 deletion tests/FSharp.Test.Utilities/FSharp.Test.Utilities.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,8 @@
<PackageReference Include="System.Diagnostics.DiagnosticSource" Version="$(SystemDiagnosticsDiagnosticSourceVersion)" />
</ItemGroup>
<PropertyGroup>
<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 -->
<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 -->
<!-- 44: AssemblyName.CodeBase is deprecated but needed for assembly resolution in VS integration tests -->
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.Collections.Immutable" Version="$(SystemCollectionsImmutableVersion)" GeneratePathProperty="true" />
Expand Down
52 changes: 52 additions & 0 deletions tests/FSharp.Test.Utilities/VSInstallDiscovery.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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<CultureInfo>
name.Version <- Unchecked.defaultof<Version>
Some name
else
None
with _ ->
None)

match found () with
| None -> Unchecked.defaultof<Assembly>
| Some name -> Assembly.Load(name))
Original file line number Diff line number Diff line change
Expand Up @@ -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<CultureInfo>
name.Version <- Unchecked.defaultof<Version>
Some(name)
else
None
with _ ->
None)
open FSharp.Test.VSAssemblyResolver

match found () with
| None -> Unchecked.defaultof<Assembly>
| 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
3 changes: 3 additions & 0 deletions vsintegration/tests/Salsa/VisualFSharp.Salsa.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@
<Compile Include="..\unittests\TestLib.Utils.fs">
<Link>UnitTests.TestLib.Utils.fs</Link>
</Compile>
<Compile Include="..\..\..\tests\FSharp.Test.Utilities\VSInstallDiscovery.fs">
<Link>VSInstallDiscovery.fs</Link>
</Compile>
<Compile Include="FSharpLanguageServiceTestable.fs" />
<Compile Include="VsMocks.fs" />
<Compile Include="Salsa.fs" />
Expand Down
18 changes: 2 additions & 16 deletions vsintegration/tests/Salsa/VsMocks.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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 () =

Expand All @@ -1650,22 +1651,7 @@ module internal VsActual =
[<System.ComponentModel.Composition.Export(typeof<JoinableTaskContext>)>]
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
Expand Down
52 changes: 4 additions & 48 deletions vsintegration/tests/UnitTests/AssemblyResolver.fs
Original file line number Diff line number Diff line change
@@ -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<CultureInfo>
name.Version <- Unchecked.defaultof<Version>
Some (name)
else None
with | _ -> None
)
match found() with
| None -> Unchecked.defaultof<Assembly>
| 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
Loading