Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions tests/FSharp.Test.Utilities/FSharp.Test.Utilities.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
<Compile Include="TestFramework.fs" />
<Compile Include="ILChecker.fs" />
<Compile Include="Utilities.fs" />
<Compile Include="VSInstallDiscovery.fs" />
<Compile Include="CompilerAssert.fs" />
<Compile Include="ProjectGeneration.fs" />
<Compile Include="Assert.fs" />
Expand Down Expand Up @@ -93,6 +94,8 @@
<InternalsVisibleTo Include="FSharp.Tests.FSharpSuite" />
<InternalsVisibleTo Include="LanguageServiceProfiling" />
<InternalsVisibleTo Include="FSharp.Compiler.Benchmarks" />
<InternalsVisibleTo Include="FSharp.Editor.Tests" />
<InternalsVisibleTo Include="VisualFSharp.Salsa" />
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)' != '$(FSharpNetCoreProductTargetFramework)'">
Expand Down
136 changes: 136 additions & 0 deletions tests/FSharp.Test.Utilities/VSInstallDiscovery.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information.

namespace FSharp.Test

/// Test-only Visual Studio installation discovery infrastructure.
/// Provides a centralized, robust, and graceful discovery mechanism for Visual Studio installations
/// used by integration/editor/unit tests under vsintegration/tests.
module VSInstallDiscovery =

open System
open System.IO
open System.Diagnostics

/// Result of VS installation discovery
type VSInstallResult =
| Found of installPath: string * source: string
| NotFound of reason: string

/// Attempts to find a Visual Studio installation using multiple fallback strategies
let tryFindVSInstallation () : VSInstallResult =

/// Check if a path exists and looks like a valid VS installation
let validateVSPath path =
if String.IsNullOrEmpty(path) then false
else
try
let fullPath = Path.GetFullPath(path)
Directory.Exists(fullPath) &&
Directory.Exists(Path.Combine(fullPath, "IDE")) &&
(File.Exists(Path.Combine(fullPath, "IDE", "devenv.exe")) ||
File.Exists(Path.Combine(fullPath, "IDE", "VSIXInstaller.exe")))
with
| _ -> false

/// Strategy 1: VSAPPIDDIR (derive parent of Common7/IDE)
let tryVSAppIdDir () =
let envVar = Environment.GetEnvironmentVariable("VSAPPIDDIR")
if not (String.IsNullOrEmpty(envVar)) then
try
let parentPath = Path.Combine(envVar, "..")
if validateVSPath parentPath then
Some (Found (Path.GetFullPath(parentPath), "VSAPPIDDIR environment variable"))
else None
with
| _ -> None
else None

/// Strategy 2: Highest version among VS*COMNTOOLS environment variables
let tryVSCommonTools () =
let vsVersions = [
("VS180COMNTOOLS", 18) // Visual Studio 2026
("VS170COMNTOOLS", 17) // Visual Studio 2022
("VS160COMNTOOLS", 16) // Visual Studio 2019
("VS150COMNTOOLS", 15) // Visual Studio 2017
("VS140COMNTOOLS", 14) // Visual Studio 2015
("VS120COMNTOOLS", 12) // Visual Studio 2013
]

vsVersions
|> List.tryPick (fun (envName, version) ->
let envVar = Environment.GetEnvironmentVariable(envName)
if not (String.IsNullOrEmpty(envVar)) then
try
let parentPath = Path.Combine(envVar, "..")
if validateVSPath parentPath then
Some (Found (Path.GetFullPath(parentPath), $"{envName} environment variable (VS version {version})"))
else None
with
| _ -> None
else None)

/// Strategy 3: vswhere.exe (Visual Studio Installer)
let tryVSWhere () =
try
let programFiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86)
let vswherePath = Path.Combine(programFiles, "Microsoft Visual Studio", "Installer", "vswhere.exe")

if File.Exists(vswherePath) then
let startInfo = ProcessStartInfo(
FileName = vswherePath,
Arguments = "-latest -products * -requires Microsoft.Component.MSBuild -property installationPath",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
)

use proc = Process.Start(startInfo)
proc.WaitForExit(5000) |> ignore // 5 second timeout

if proc.ExitCode = 0 then
let output = proc.StandardOutput.ReadToEnd().Trim()
if validateVSPath output then
Some (Found (Path.GetFullPath(output), "vswhere.exe discovery"))
else None
else None
else None
with
| _ -> None

// Try each strategy in order of precedence
match tryVSAppIdDir () with
| Some result -> result
| None ->
match tryVSCommonTools () with
| Some result -> result
| None ->
match tryVSWhere () with
| Some result -> result
| None -> NotFound "No Visual Studio installation found using any discovery method"

/// Gets the VS installation directory, with graceful fallback behavior.
/// Returns None if no VS installation can be found, allowing callers to handle gracefully.
let tryGetVSInstallDir () : string option =
match tryFindVSInstallation () with
| Found (path, _) -> Some path
| NotFound _ -> None

/// Gets the VS installation directory with detailed logging.
/// Useful for debugging installation discovery issues in tests.
let getVSInstallDirWithLogging (logAction: string -> unit) : string option =
match tryFindVSInstallation () with
| Found (path, source) ->
logAction $"Visual Studio installation found at: {path} (via {source})"
Some path
| NotFound reason ->
logAction $"Visual Studio installation not found: {reason}"
None

/// Legacy compatibility function that maintains the old behavior of hard-failing.
/// Use this only for tests that must maintain the old behavior.
[<Obsolete("Use tryGetVSInstallDir() or getVSInstallDirWithLogging() for graceful error handling")>]
let getVSInstallDirOrFail () : string =
match tryFindVSInstallation () with
| Found (path, _) -> path
| NotFound reason -> failwith $"VS170COMNTOOLS and VSAPPIDDIR environment variables not found. {reason}"
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,26 @@ open System.Reflection

module AssemblyResolver =
open System.Globalization
open FSharp.Test.VSInstallDiscovery

let vsInstallDir =
// use the environment variable to find the VS installdir
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, "..")
// 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 =
[|
Expand Down
1 change: 1 addition & 0 deletions vsintegration/tests/Salsa/VisualFSharp.Salsa.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
<ProjectReference Include="$(FSharpSourcesRoot)\FSharp.Core\FSharp.Core.fsproj" />
<ProjectReference Include="$(FSharpSourcesRoot)\FSharp.Build\FSharp.Build.fsproj" />
<ProjectReference Include="$(FSharpSourcesRoot)\Compiler\FSharp.Compiler.Service.fsproj" />
<ProjectReference Include="..\..\..\tests\FSharp.Test.Utilities\FSharp.Test.Utilities.fsproj" />
<ProjectReference Include="..\..\src\FSharp.Editor\FSharp.Editor.fsproj" />
<ProjectReference Include="..\..\src\FSharp.LanguageService.Base\FSharp.LanguageService.Base.csproj" />
<ProjectReference Include="..\..\src\FSharp.LanguageService\FSharp.LanguageService.fsproj" />
Expand Down
22 changes: 13 additions & 9 deletions vsintegration/tests/Salsa/VsMocks.fs
Original file line number Diff line number Diff line change
Expand Up @@ -1651,15 +1651,19 @@ module internal VsActual =
member public _.JoinableTaskContext : JoinableTaskContext = jtc

let vsInstallDir =
// use the environment variable to find the VS installdir
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, "..")
// Use centralized VS installation discovery with graceful fallback
match FSharp.Test.VSInstallDiscovery.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 CreateEditorCatalog() =
let thisAssembly = Assembly.GetExecutingAssembly().Location
Expand Down
23 changes: 14 additions & 9 deletions vsintegration/tests/UnitTests/AssemblyResolver.fs
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,22 @@ open System.Reflection

module AssemblyResolver =
open System.Globalization
open FSharp.Test.VSInstallDiscovery

let vsInstallDir =
// use the environment variable to find the VS installdir
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, "..")
// 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")
Expand Down
Loading