Skip to content
Merged
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
54 changes: 16 additions & 38 deletions azure-pipelines-PR.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ variables:
value: Release
- name: _PublishUsingPipelines
value: true
- name: _WindowsMachineQueueName
value: windows.vs2026preview.scout.amd64.open
- name: VisualStudioDropName
value: Products/$(System.TeamProject)/$(Build.Repository.Name)/$(Build.SourceBranchName)/$(Build.BuildNumber)
- name: Codeql.Enabled
Expand Down Expand Up @@ -101,7 +103,7 @@ stages:
value: Test
pool:
name: $(DncEngPublicBuildPool)
demands: ImageOverride -equals $(WindowsMachineQueueName)
demands: ImageOverride -equals $(_WindowsMachineQueueName)
timeoutInMinutes: 90
strategy:
maxParallel: 2
Expand Down Expand Up @@ -216,12 +218,8 @@ stages:

- job: WindowsLangVersionPreview
pool:
# The PR build definition sets this variable:
# WindowsMachineQueueName=Windows.vs2022.amd64.open
# and there is an alternate build definition that sets this to a queue that is always scouting the
# next preview of Visual Studio.
name: $(DncEngPublicBuildPool)
demands: ImageOverride -equals $(WindowsMachineQueueName)
demands: ImageOverride -equals $(_WindowsMachineQueueName)
timeoutInMinutes: 120
steps:
- checkout: self
Expand Down Expand Up @@ -256,12 +254,8 @@ stages:

- job: WindowsNoRealsig_testCoreclr
pool:
# The PR build definition sets this variable:
# WindowsMachineQueueName=Windows.vs2022.amd64.open
# and there is an alternate build definition that sets this to a queue that is always scouting the
# next preview of Visual Studio.
name: $(DncEngPublicBuildPool)
demands: ImageOverride -equals $(WindowsMachineQueueName)
demands: ImageOverride -equals $(_WindowsMachineQueueName)
timeoutInMinutes: 120
steps:
- checkout: self
Expand Down Expand Up @@ -306,12 +300,8 @@ stages:

- job: WindowsNoRealsig_testDesktop
pool:
# The PR build definition sets this variable:
# WindowsMachineQueueName=Windows.vs2022.amd64.open
# and there is an alternate build definition that sets this to a queue that is always scouting the
# next preview of Visual Studio.
name: $(DncEngPublicBuildPool)
demands: ImageOverride -equals $(WindowsMachineQueueName)
demands: ImageOverride -equals $(_WindowsMachineQueueName)
timeoutInMinutes: 120
strategy:
parallel: 4
Expand Down Expand Up @@ -359,12 +349,8 @@ stages:

- job: WindowsStrictIndentation
pool:
# The PR build definition sets this variable:
# WindowsMachineQueueName=Windows.vs2022.amd64.open
# and there is an alternate build definition that sets this to a queue that is always scouting the
# next preview of Visual Studio.
name: $(DncEngPublicBuildPool)
demands: ImageOverride -equals $(WindowsMachineQueueName)
demands: ImageOverride -equals $(_WindowsMachineQueueName)
timeoutInMinutes: 120
steps:
- checkout: self
Expand Down Expand Up @@ -400,7 +386,7 @@ stages:
- job: WindowsNoStrictIndentation
pool:
name: $(DncEngPublicBuildPool)
demands: ImageOverride -equals $(WindowsMachineQueueName)
demands: ImageOverride -equals $(_WindowsMachineQueueName)
timeoutInMinutes: 120
steps:
- checkout: self
Expand Down Expand Up @@ -441,12 +427,8 @@ stages:
- name: __VSNeverShowWhatsNew
value: 1
pool:
# The PR build definition sets this variable:
# WindowsMachineQueueName=Windows.vs2022.amd64.open
# and there is an alternate build definition that sets this to a queue that is always scouting the
# next preview of Visual Studio.
name: $(DncEngPublicBuildPool)
demands: ImageOverride -equals $(WindowsMachineQueueName)
demands: ImageOverride -equals $(_WindowsMachineQueueName)
timeoutInMinutes: 120
strategy:
matrix:
Expand Down Expand Up @@ -546,12 +528,8 @@ stages:
- name: __VSNeverShowWhatsNew
value: 1
pool:
# The PR build definition sets this variable:
# WindowsMachineQueueName=Windows.vs2022.amd64.open
# and there is an alternate build definition that sets this to a queue that is always scouting the
# next preview of Visual Studio.
name: $(DncEngPublicBuildPool)
demands: ImageOverride -equals $(WindowsMachineQueueName)
demands: ImageOverride -equals $(_WindowsMachineQueueName)
timeoutInMinutes: 120
strategy:
parallel: 4
Expand Down Expand Up @@ -622,7 +600,7 @@ stages:
- job: MockOfficial
pool:
name: $(DncEngPublicBuildPool)
demands: ImageOverride -equals $(WindowsMachineQueueName)
demands: ImageOverride -equals $(_WindowsMachineQueueName)
steps:
- checkout: self
clean: true
Expand Down Expand Up @@ -721,7 +699,7 @@ stages:
- job: EndToEndBuildTests
pool:
name: $(DncEngPublicBuildPool)
demands: ImageOverride -equals $(WindowsMachineQueueName)
demands: ImageOverride -equals $(_WindowsMachineQueueName)
strategy:
maxParallel: 2
matrix:
Expand Down Expand Up @@ -761,7 +739,7 @@ stages:
- job: Plain_Build_Windows
pool:
name: $(DncEngPublicBuildPool)
demands: ImageOverride -equals $(WindowsMachineQueueName)
demands: ImageOverride -equals $(_WindowsMachineQueueName)
variables:
- name: _BuildConfig
value: Debug
Expand Down Expand Up @@ -816,7 +794,7 @@ stages:
- job: Benchmarks
pool:
name: $(DncEngPublicBuildPool)
demands: ImageOverride -equals $(WindowsMachineQueueName)
demands: ImageOverride -equals $(_WindowsMachineQueueName)
variables:
- name: _BuildConfig
value: Release
Expand All @@ -832,7 +810,7 @@ stages:
- job: Build_And_Test_AOT_Windows
pool:
name: $(DncEngPublicBuildPool)
demands: ImageOverride -equals $(WindowsMachineQueueName)
demands: ImageOverride -equals $(_WindowsMachineQueueName)
strategy:
maxParallel: 2
matrix:
Expand Down Expand Up @@ -873,7 +851,7 @@ stages:
- job: ILVerify
pool:
name: $(DncEngPublicBuildPool)
demands: ImageOverride -equals $(WindowsMachineQueueName)
demands: ImageOverride -equals $(_WindowsMachineQueueName)
steps:
- checkout: self
clean: true
Expand Down
5 changes: 4 additions & 1 deletion 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,14 +94,16 @@
<InternalsVisibleTo Include="FSharp.Tests.FSharpSuite" />
<InternalsVisibleTo Include="LanguageServiceProfiling" />
<InternalsVisibleTo Include="FSharp.Compiler.Benchmarks" />
<InternalsVisibleTo Include="FSharp.Editor.Tests" />
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)' != '$(FSharpNetCoreProductTargetFramework)'">
<PackageReference Include="System.Reflection.Metadata" Version="$(SystemReflectionMetadataVersion)" />
<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
180 changes: 180 additions & 0 deletions tests/FSharp.Test.Utilities/VSInstallDiscovery.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
// 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

/// 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))
Loading
Loading