diff --git a/FSharp.Editor.sln b/FSharp.Editor.sln
index 70ffe6bb0ea..f96eabefefd 100644
--- a/FSharp.Editor.sln
+++ b/FSharp.Editor.sln
@@ -1,4 +1,5 @@
-Microsoft Visual Studio Solution File, Format Version 12.00
+
+Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.1.32113.165
MinimumVisualStudioVersion = 10.0.40219.1
@@ -18,54 +19,62 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FSharp.UIResources", "vsint
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "FSharp.VS.FSI", "vsintegration\src\FSharp.VS.FSI\FSharp.VS.FSI.fsproj", "{EAC029EB-4A8F-4966-9B38-60D73D8E20D1}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FSharp.Editor.IntegrationTests", "vsintegration\tests\FSharp.Editor.IntegrationTests\FSharp.Editor.IntegrationTests.csproj", "{42BE0F2F-BC45-437B-851D-E88A83201339}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
- Release|Any CPU = Release|Any CPU
Proto|Any CPU = Proto|Any CPU
+ Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{321F47BC-8148-4C8D-B340-08B7BF07D31D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{321F47BC-8148-4C8D-B340-08B7BF07D31D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {321F47BC-8148-4C8D-B340-08B7BF07D31D}.Proto|Any CPU.ActiveCfg = Debug|Any CPU
{321F47BC-8148-4C8D-B340-08B7BF07D31D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{321F47BC-8148-4C8D-B340-08B7BF07D31D}.Release|Any CPU.Build.0 = Release|Any CPU
- {321F47BC-8148-4C8D-B340-08B7BF07D31D}.Proto|Any CPU.ActiveCfg = Debug|Any CPU
{AD603EF2-FAC6-48D1-AAEB-A6CF898062A9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{AD603EF2-FAC6-48D1-AAEB-A6CF898062A9}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {AD603EF2-FAC6-48D1-AAEB-A6CF898062A9}.Proto|Any CPU.ActiveCfg = Debug|Any CPU
{AD603EF2-FAC6-48D1-AAEB-A6CF898062A9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AD603EF2-FAC6-48D1-AAEB-A6CF898062A9}.Release|Any CPU.Build.0 = Release|Any CPU
- {AD603EF2-FAC6-48D1-AAEB-A6CF898062A9}.Proto|Any CPU.ActiveCfg = Debug|Any CPU
{67DA0BF3-AAD3-47F4-9EC6-AD8EC532B5A9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{67DA0BF3-AAD3-47F4-9EC6-AD8EC532B5A9}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {67DA0BF3-AAD3-47F4-9EC6-AD8EC532B5A9}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {67DA0BF3-AAD3-47F4-9EC6-AD8EC532B5A9}.Release|Any CPU.Build.0 = Release|Any CPU
{67DA0BF3-AAD3-47F4-9EC6-AD8EC532B5A9}.Proto|Any CPU.ActiveCfg = Proto|Any CPU
{67DA0BF3-AAD3-47F4-9EC6-AD8EC532B5A9}.Proto|Any CPU.Build.0 = Proto|Any CPU
+ {67DA0BF3-AAD3-47F4-9EC6-AD8EC532B5A9}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {67DA0BF3-AAD3-47F4-9EC6-AD8EC532B5A9}.Release|Any CPU.Build.0 = Release|Any CPU
{24399E68-9000-4556-BDDD-8D74A9660D28}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{24399E68-9000-4556-BDDD-8D74A9660D28}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {24399E68-9000-4556-BDDD-8D74A9660D28}.Proto|Any CPU.ActiveCfg = Debug|Any CPU
{24399E68-9000-4556-BDDD-8D74A9660D28}.Release|Any CPU.ActiveCfg = Release|Any CPU
{24399E68-9000-4556-BDDD-8D74A9660D28}.Release|Any CPU.Build.0 = Release|Any CPU
- {24399E68-9000-4556-BDDD-8D74A9660D28}.Proto|Any CPU.ActiveCfg = Debug|Any CPU
{86E148BE-92C8-47CC-A070-11D769C6D898}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{86E148BE-92C8-47CC-A070-11D769C6D898}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {86E148BE-92C8-47CC-A070-11D769C6D898}.Proto|Any CPU.ActiveCfg = Debug|Any CPU
{86E148BE-92C8-47CC-A070-11D769C6D898}.Release|Any CPU.ActiveCfg = Release|Any CPU
{86E148BE-92C8-47CC-A070-11D769C6D898}.Release|Any CPU.Build.0 = Release|Any CPU
- {86E148BE-92C8-47CC-A070-11D769C6D898}.Proto|Any CPU.ActiveCfg = Debug|Any CPU
{4FFA5E03-4128-48C9-8FCD-D7C60729ED74}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4FFA5E03-4128-48C9-8FCD-D7C60729ED74}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {4FFA5E03-4128-48C9-8FCD-D7C60729ED74}.Proto|Any CPU.ActiveCfg = Debug|Any CPU
{4FFA5E03-4128-48C9-8FCD-D7C60729ED74}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4FFA5E03-4128-48C9-8FCD-D7C60729ED74}.Release|Any CPU.Build.0 = Release|Any CPU
- {4FFA5E03-4128-48C9-8FCD-D7C60729ED74}.Proto|Any CPU.ActiveCfg = Debug|Any CPU
{DA9495E6-BEAA-42A4-AD3B-170D2005AF4B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DA9495E6-BEAA-42A4-AD3B-170D2005AF4B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {DA9495E6-BEAA-42A4-AD3B-170D2005AF4B}.Proto|Any CPU.ActiveCfg = Debug|Any CPU
{DA9495E6-BEAA-42A4-AD3B-170D2005AF4B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DA9495E6-BEAA-42A4-AD3B-170D2005AF4B}.Release|Any CPU.Build.0 = Release|Any CPU
- {DA9495E6-BEAA-42A4-AD3B-170D2005AF4B}.Proto|Any CPU.ActiveCfg = Debug|Any CPU
{EAC029EB-4A8F-4966-9B38-60D73D8E20D1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{EAC029EB-4A8F-4966-9B38-60D73D8E20D1}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {EAC029EB-4A8F-4966-9B38-60D73D8E20D1}.Proto|Any CPU.ActiveCfg = Debug|Any CPU
{EAC029EB-4A8F-4966-9B38-60D73D8E20D1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EAC029EB-4A8F-4966-9B38-60D73D8E20D1}.Release|Any CPU.Build.0 = Release|Any CPU
- {EAC029EB-4A8F-4966-9B38-60D73D8E20D1}.Proto|Any CPU.ActiveCfg = Debug|Any CPU
+ {42BE0F2F-BC45-437B-851D-E88A83201339}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {42BE0F2F-BC45-437B-851D-E88A83201339}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {42BE0F2F-BC45-437B-851D-E88A83201339}.Proto|Any CPU.ActiveCfg = Debug|Any CPU
+ {42BE0F2F-BC45-437B-851D-E88A83201339}.Proto|Any CPU.Build.0 = Debug|Any CPU
+ {42BE0F2F-BC45-437B-851D-E88A83201339}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {42BE0F2F-BC45-437B-851D-E88A83201339}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/VisualFSharp.sln b/VisualFSharp.sln
index 821fb3e510a..8c5d917b927 100644
--- a/VisualFSharp.sln
+++ b/VisualFSharp.sln
@@ -195,6 +195,8 @@ Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "Fsharp.ProfilingStartpointP
EndProject
Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "FSharp.Editor.Tests", "vsintegration\tests\FSharp.Editor.Tests\FSharp.Editor.Tests.fsproj", "{CBC96CC7-65AB-46EA-A82E-F6A788DABF80}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FSharp.Editor.IntegrationTests", "vsintegration\tests\FSharp.Editor.IntegrationTests\FSharp.Editor.IntegrationTests.csproj", "{E31F9B59-FCF1-4D04-8762-C7BB60285A7B}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -1033,6 +1035,18 @@ Global
{CBC96CC7-65AB-46EA-A82E-F6A788DABF80}.Release|Any CPU.Build.0 = Release|Any CPU
{CBC96CC7-65AB-46EA-A82E-F6A788DABF80}.Release|x86.ActiveCfg = Release|Any CPU
{CBC96CC7-65AB-46EA-A82E-F6A788DABF80}.Release|x86.Build.0 = Release|Any CPU
+ {E31F9B59-FCF1-4D04-8762-C7BB60285A7B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {E31F9B59-FCF1-4D04-8762-C7BB60285A7B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E31F9B59-FCF1-4D04-8762-C7BB60285A7B}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {E31F9B59-FCF1-4D04-8762-C7BB60285A7B}.Debug|x86.Build.0 = Debug|Any CPU
+ {E31F9B59-FCF1-4D04-8762-C7BB60285A7B}.Proto|Any CPU.ActiveCfg = Debug|Any CPU
+ {E31F9B59-FCF1-4D04-8762-C7BB60285A7B}.Proto|Any CPU.Build.0 = Debug|Any CPU
+ {E31F9B59-FCF1-4D04-8762-C7BB60285A7B}.Proto|x86.ActiveCfg = Debug|Any CPU
+ {E31F9B59-FCF1-4D04-8762-C7BB60285A7B}.Proto|x86.Build.0 = Debug|Any CPU
+ {E31F9B59-FCF1-4D04-8762-C7BB60285A7B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {E31F9B59-FCF1-4D04-8762-C7BB60285A7B}.Release|Any CPU.Build.0 = Release|Any CPU
+ {E31F9B59-FCF1-4D04-8762-C7BB60285A7B}.Release|x86.ActiveCfg = Release|Any CPU
+ {E31F9B59-FCF1-4D04-8762-C7BB60285A7B}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -1114,6 +1128,7 @@ Global
{39CDF34B-FB23-49AE-AB27-0975DA379BB5} = {DFB6ADD7-3149-43D9-AFA0-FC4A818B472B}
{FE23BB65-276A-4E41-8CC7-F7752241DEBA} = {39CDF34B-FB23-49AE-AB27-0975DA379BB5}
{CBC96CC7-65AB-46EA-A82E-F6A788DABF80} = {F7876C9B-FB6A-4EFB-B058-D6967DB75FB2}
+ {E31F9B59-FCF1-4D04-8762-C7BB60285A7B} = {F7876C9B-FB6A-4EFB-B058-D6967DB75FB2}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {48EDBBBE-C8EE-4E3C-8B19-97184A487B37}
diff --git a/azure-pipelines.yml b/azure-pipelines.yml
index 9e7825f03b9..d1ee7e1e3c4 100644
--- a/azure-pipelines.yml
+++ b/azure-pipelines.yml
@@ -332,7 +332,7 @@ stages:
demands: ImageOverride -equals $(WindowsMachineQueueName)
timeoutInMinutes: 120
strategy:
- maxParallel: 4
+ maxParallel: 5
matrix:
desktop_release:
_configuration: Release
@@ -346,6 +346,9 @@ stages:
vs_release:
_configuration: Release
_testKind: testVs
+ inttests_release:
+ _configuration: Release
+ _testKind: testIntegration
steps:
- checkout: self
clean: true
diff --git a/eng/Build.ps1 b/eng/Build.ps1
index 43dfa6bc6b1..e8cbd5363a7 100644
--- a/eng/Build.ps1
+++ b/eng/Build.ps1
@@ -55,6 +55,7 @@ param (
[switch]$testCompilerComponentTests,
[switch]$testFSharpCore,
[switch]$testFSharpQA,
+ [switch]$testIntegration,
[switch]$testScripting,
[switch]$testVs,
[switch]$testAll,
@@ -102,6 +103,7 @@ function Print-Usage() {
Write-Host " -testCoreClr Run tests against CoreCLR"
Write-Host " -testFSharpCore Run FSharpCore unit tests"
Write-Host " -testFSharpQA Run F# Cambridge tests"
+ Write-Host " -testIntegration Run F# integration tests"
Write-Host " -testScripting Run Scripting tests"
Write-Host " -testVs Run F# editor unit tests"
Write-Host " -testpack Verify built packages"
@@ -142,6 +144,7 @@ function Process-Arguments() {
$script:testDesktop = $True
$script:testCoreClr = $True
$script:testFSharpQA = $True
+ $script:testIntegration = $True
$script:testVs = $True
}
@@ -154,6 +157,7 @@ function Process-Arguments() {
$script:testCoreClr = $False
$script:testFSharpCore = $False
$script:testFSharpQA = $False
+ $script:testIntegration = $False
$script:testVs = $False
$script:testpack = $False
$script:verifypackageshipstatus = $True
@@ -614,6 +618,10 @@ try {
TestUsingNUnit -testProject "$RepoRoot\vsintegration\tests\UnitTests\VisualFSharp.UnitTests.fsproj" -targetFramework $desktopTargetFramework -testadapterpath "$ArtifactsDir\bin\VisualFSharp.UnitTests\"
TestUsingXUnit -testProject "$RepoRoot\vsintegration\tests\FSharp.Editor.Tests\FSharp.Editor.Tests.fsproj" -targetFramework $desktopTargetFramework -testadapterpath "$ArtifactsDir\bin\FSharp.Editor.Tests\FSharp.Editor.Tests.fsproj"
}
+
+ if ($testIntegration) {
+ TestUsingXUnit -testProject "$RepoRoot\vsintegration\tests\FSharp.Editor.IntegrationTests\FSharp.Editor.IntegrationTests.csproj" -targetFramework $desktopTargetFramework -testadapterpath "$ArtifactsDir\bin\FSharp.Editor.IntegrationTests\"
+ }
# verify nupkgs have access to the source code
$nupkgtestFailed = $false
diff --git a/eng/Versions.props b/eng/Versions.props
index 0939c8b5671..dfba5d9f243 100644
--- a/eng/Versions.props
+++ b/eng/Versions.props
@@ -164,6 +164,10 @@
$(VisualStudioEditorPackagesVersion)
$(VisualStudioEditorPackagesVersion)
$(VisualStudioEditorPackagesVersion)
+ 5.6.0
+ 0.1.162-beta
+ $(MicrosoftVisualStudioExtensibilityTestingVersion)
+ $(MicrosoftVisualStudioExtensibilityTestingVersion)
$(MicrosoftVisualStudioThreadingPackagesVersion)
diff --git a/vsintegration/tests/FSharp.Editor.IntegrationTests/AbstractIntegrationTest.cs b/vsintegration/tests/FSharp.Editor.IntegrationTests/AbstractIntegrationTest.cs
new file mode 100644
index 00000000000..29109cbb826
--- /dev/null
+++ b/vsintegration/tests/FSharp.Editor.IntegrationTests/AbstractIntegrationTest.cs
@@ -0,0 +1,14 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using Microsoft.VisualStudio.Extensibility.Testing;
+using Xunit;
+
+namespace Microsoft.CodeAnalysis.Testing
+{
+ [IdeSettings(MinVersion = VisualStudioVersion.VS2022)]
+ public abstract class AbstractIntegrationTest : AbstractIdeIntegrationTest
+ {
+ }
+}
diff --git a/vsintegration/tests/FSharp.Editor.IntegrationTests/BuildProjectTests.cs b/vsintegration/tests/FSharp.Editor.IntegrationTests/BuildProjectTests.cs
new file mode 100644
index 00000000000..f84ee5d5ccb
--- /dev/null
+++ b/vsintegration/tests/FSharp.Editor.IntegrationTests/BuildProjectTests.cs
@@ -0,0 +1,69 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using Microsoft.CodeAnalysis.Testing;
+using Microsoft.VisualStudio.Extensibility.Testing;
+using Microsoft.VisualStudio.Shell.Interop;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace FSharp.Editor.IntegrationTests;
+
+public class BuildProjectTests : AbstractIntegrationTest
+{
+ [IdeFact]
+ public async Task SuccessfulBuild_Async()
+ {
+ var token = HangMitigatingCancellationToken;
+ var template = WellKnownProjectTemplates.FSharpNetCoreClassLibrary;
+ var solutionExplorer = TestServices.SolutionExplorer;
+ var editor = TestServices.Editor;
+ var code = """
+module Test
+
+let answer = 42
+""";
+
+ var expectedBuildSummary = "========== Build: 1 succeeded, 0 failed, 0 up-to-date, 0 skipped ==========";
+
+ await solutionExplorer.CreateSolutionAsync(nameof(BuildProjectTests), token);
+ await solutionExplorer.AddProjectAsync("Library", template, token);
+ await solutionExplorer.RestoreNuGetPackagesAsync(token);
+ await editor.SetTextAsync(code, token);
+
+ var actualBuildSummary = await solutionExplorer.BuildSolutionAsync(token);
+
+ Assert.Contains(expectedBuildSummary, actualBuildSummary);
+ }
+
+ [IdeFact]
+ public async Task FailedBuild_Async()
+ {
+ var token = HangMitigatingCancellationToken;
+ var template = WellKnownProjectTemplates.FSharpNetCoreClassLibrary;
+ var solutionExplorer = TestServices.SolutionExplorer;
+ var editor = TestServices.Editor;
+ var errorList = TestServices.ErrorList;
+ var code = """
+module Test
+
+let answer =
+""";
+
+ var expectedBuildSummary = "========== Build: 0 succeeded, 1 failed, 0 up-to-date, 0 skipped ==========";
+ var expectedError = "(Compiler) Library.fs(3, 1): error FS0010: Incomplete structured construct at or before this point in binding";
+
+ await solutionExplorer.CreateSolutionAsync(nameof(BuildProjectTests), token);
+ await solutionExplorer.AddProjectAsync("Library", template, token);
+ await solutionExplorer.RestoreNuGetPackagesAsync(token);
+ await editor.SetTextAsync(code, token);
+
+ var actualBuildSummary = await solutionExplorer.BuildSolutionAsync(token);
+ Assert.Contains(expectedBuildSummary, actualBuildSummary);
+
+ await errorList.ShowBuildErrorsAsync(token);
+ var errors = await errorList.GetBuildErrorsAsync(__VSERRORCATEGORY.EC_ERROR, token);
+ Assert.Contains(expectedError, errors);
+ }
+}
\ No newline at end of file
diff --git a/vsintegration/tests/FSharp.Editor.IntegrationTests/CreateProjectTests.cs b/vsintegration/tests/FSharp.Editor.IntegrationTests/CreateProjectTests.cs
new file mode 100644
index 00000000000..d519d1329b9
--- /dev/null
+++ b/vsintegration/tests/FSharp.Editor.IntegrationTests/CreateProjectTests.cs
@@ -0,0 +1,88 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using Microsoft.CodeAnalysis.Testing;
+using Microsoft.VisualStudio.Extensibility.Testing;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace FSharp.Editor.IntegrationTests;
+
+public class CreateProjectTests : AbstractIntegrationTest
+{
+ [IdeFact]
+ public async Task ClassLibrary_Async()
+ {
+ var token = HangMitigatingCancellationToken;
+ var template = WellKnownProjectTemplates.FSharpNetCoreClassLibrary;
+ var solutionExplorer = TestServices.SolutionExplorer;
+ var editor = TestServices.Editor;
+
+ var expectedCode = """
+namespace Library
+
+module Say =
+ let hello name =
+ printfn "Hello %s" name
+
+""";
+
+ await solutionExplorer.CreateSolutionAsync(nameof(CreateProjectTests), token);
+ await solutionExplorer.AddProjectAsync("Library", template, token);
+
+ var actualCode = await editor.GetTextAsync(token);
+
+ Assert.Equal(expectedCode, actualCode);
+ }
+
+ [IdeFact]
+ public async Task ConsoleApp_Async()
+ {
+ var token = HangMitigatingCancellationToken;
+ var template = WellKnownProjectTemplates.FSharpNetCoreConsoleApplication;
+ var solutionExplorer = TestServices.SolutionExplorer;
+ var editor = TestServices.Editor;
+
+ var expectedCode = """
+// For more information see https://aka.ms/fsharp-console-apps
+printfn "Hello from F#"
+
+""";
+
+ await solutionExplorer.CreateSolutionAsync(nameof(CreateProjectTests), token);
+ await solutionExplorer.AddProjectAsync("ConsoleApp", template, token);
+
+ var actualCode = await editor.GetTextAsync(token);
+
+ Assert.Equal(expectedCode, actualCode);
+ }
+
+ [IdeFact]
+ public async Task XUnitTestProject_Async()
+ {
+ var token = HangMitigatingCancellationToken;
+ var template = WellKnownProjectTemplates.FSharpNetCoreXUnitTest;
+ var solutionExplorer = TestServices.SolutionExplorer;
+ var editor = TestServices.Editor;
+
+ var expectedCode = """
+module Tests
+
+open System
+open Xunit
+
+[]
+let ``My test`` () =
+ Assert.True(true)
+
+""";
+
+ await solutionExplorer.CreateSolutionAsync(nameof(CreateProjectTests), token);
+ await solutionExplorer.AddProjectAsync("ConsoleApp", template, token);
+
+ var actualCode = await editor.GetTextAsync(token);
+
+ Assert.Equal(expectedCode, actualCode);
+ }
+}
\ No newline at end of file
diff --git a/vsintegration/tests/FSharp.Editor.IntegrationTests/FSharp.Editor.IntegrationTests.csproj b/vsintegration/tests/FSharp.Editor.IntegrationTests/FSharp.Editor.IntegrationTests.csproj
new file mode 100644
index 00000000000..f52bfdf9bad
--- /dev/null
+++ b/vsintegration/tests/FSharp.Editor.IntegrationTests/FSharp.Editor.IntegrationTests.csproj
@@ -0,0 +1,32 @@
+
+
+
+ net472
+ preview
+ enable
+ xunit
+ false
+ false
+ true
+
+
+
+
+
+
+
+
+ PreserveNewest
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/vsintegration/tests/FSharp.Editor.IntegrationTests/InProcess/EditorInProcess.cs b/vsintegration/tests/FSharp.Editor.IntegrationTests/InProcess/EditorInProcess.cs
new file mode 100644
index 00000000000..004953d5797
--- /dev/null
+++ b/vsintegration/tests/FSharp.Editor.IntegrationTests/InProcess/EditorInProcess.cs
@@ -0,0 +1,31 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.VisualStudio.Text;
+
+namespace Microsoft.VisualStudio.Extensibility.Testing;
+
+internal partial class EditorInProcess
+{
+ public async Task GetTextAsync(CancellationToken cancellationToken)
+ {
+ await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
+
+ var view = await GetActiveTextViewAsync(cancellationToken);
+ var textSnapshot = view.TextSnapshot;
+ return textSnapshot.GetText();
+ }
+
+ public async Task SetTextAsync(string text, CancellationToken cancellationToken)
+ {
+ await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
+
+ var view = await GetActiveTextViewAsync(cancellationToken);
+ var textSnapshot = view.TextSnapshot;
+ var replacementSpan = new SnapshotSpan(textSnapshot, 0, textSnapshot.Length);
+ view.TextBuffer.Replace(replacementSpan, text);
+ }
+}
diff --git a/vsintegration/tests/FSharp.Editor.IntegrationTests/InProcess/ErrorListExtensions.cs b/vsintegration/tests/FSharp.Editor.IntegrationTests/InProcess/ErrorListExtensions.cs
new file mode 100644
index 00000000000..6477d9d2ec9
--- /dev/null
+++ b/vsintegration/tests/FSharp.Editor.IntegrationTests/InProcess/ErrorListExtensions.cs
@@ -0,0 +1,73 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using Microsoft.VisualStudio.Shell.Interop;
+using Microsoft.VisualStudio.Shell.TableManager;
+
+namespace Microsoft.CodeAnalysis.Testing.InProcess
+{
+ internal static class ErrorListExtensions
+ {
+ public static __VSERRORCATEGORY GetCategory(this ITableEntry tableEntry)
+ {
+ return tableEntry.GetValueOrDefault(StandardTableKeyNames.ErrorSeverity, (__VSERRORCATEGORY)(-1));
+ }
+
+ public static string GetBuildTool(this ITableEntry tableEntry)
+ {
+ return tableEntry.GetValueOrDefault(StandardTableKeyNames.BuildTool, "");
+ }
+
+ public static string? GetPath(this ITableEntry tableEntry)
+ {
+ return tableEntry.GetValueOrDefault(StandardTableKeyNames.Path, null);
+ }
+
+ public static string? GetDocumentName(this ITableEntry tableEntry)
+ {
+ return tableEntry.GetValueOrDefault(StandardTableKeyNames.DocumentName, null);
+ }
+
+ public static int? GetLine(this ITableEntry tableEntry)
+ {
+ return tableEntry.GetValueOrNull(StandardTableKeyNames.Line);
+ }
+
+ public static int? GetColumn(this ITableEntry tableEntry)
+ {
+ return tableEntry.GetValueOrNull(StandardTableKeyNames.Column);
+ }
+
+ public static string? GetErrorCode(this ITableEntry tableEntry)
+ {
+ return tableEntry.GetValueOrDefault(StandardTableKeyNames.ErrorCode, null);
+ }
+
+ public static string? GetText(this ITableEntry tableEntry)
+ {
+ return tableEntry.GetValueOrDefault(StandardTableKeyNames.Text, null);
+ }
+
+ private static T GetValueOrDefault(this ITableEntry tableEntry, string keyName, T defaultValue)
+ {
+ if (!tableEntry.TryGetValue(keyName, out T value))
+ {
+ value = defaultValue;
+ }
+
+ return value;
+ }
+
+ private static T? GetValueOrNull(this ITableEntry tableEntry, string keyName)
+ where T : struct
+ {
+ if (!tableEntry.TryGetValue(keyName, out T value))
+ {
+ return null;
+ }
+
+ return value;
+ }
+ }
+}
diff --git a/vsintegration/tests/FSharp.Editor.IntegrationTests/InProcess/ErrorListInProcess.cs b/vsintegration/tests/FSharp.Editor.IntegrationTests/InProcess/ErrorListInProcess.cs
new file mode 100644
index 00000000000..ca81bfbd8a1
--- /dev/null
+++ b/vsintegration/tests/FSharp.Editor.IntegrationTests/InProcess/ErrorListInProcess.cs
@@ -0,0 +1,107 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.VisualStudio.Extensibility.Testing;
+using Microsoft.VisualStudio.Shell;
+using Microsoft.VisualStudio.Shell.Interop;
+using Microsoft.VisualStudio.Shell.TableControl;
+using Microsoft.VisualStudio.Shell.TableManager;
+using Task = System.Threading.Tasks.Task;
+
+namespace Microsoft.CodeAnalysis.Testing.InProcess
+{
+ [TestService]
+ internal partial class ErrorListInProcess
+ {
+ public async Task ShowBuildErrorsAsync(CancellationToken cancellationToken)
+ {
+ await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
+
+ var errorList = await GetRequiredGlobalServiceAsync(cancellationToken);
+ errorList.AreBuildErrorSourceEntriesShown = true;
+ errorList.AreOtherErrorSourceEntriesShown = false;
+ errorList.AreErrorsShown = true;
+ errorList.AreMessagesShown = true;
+ errorList.AreWarningsShown = false;
+ }
+
+ public Task> GetBuildErrorsAsync(CancellationToken cancellationToken)
+ {
+ return GetBuildErrorsAsync(__VSERRORCATEGORY.EC_WARNING, cancellationToken);
+ }
+
+ public async Task> GetBuildErrorsAsync(__VSERRORCATEGORY minimumSeverity, CancellationToken cancellationToken)
+ {
+ await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
+
+ var errorItems = await GetErrorItemsAsync(cancellationToken);
+ var list = new List();
+
+ foreach (var item in errorItems)
+ {
+ if (item.GetCategory() > minimumSeverity)
+ {
+ continue;
+ }
+
+ if (!item.TryGetValue(StandardTableKeyNames.ErrorSource, out var errorSource)
+ || (ErrorSource)errorSource != ErrorSource.Build)
+ {
+ continue;
+ }
+
+ var source = item.GetBuildTool();
+ var document = Path.GetFileName(item.GetPath() ?? item.GetDocumentName()) ?? "";
+ var line = item.GetLine() ?? -1;
+ var column = item.GetColumn() ?? -1;
+ var errorCode = item.GetErrorCode() ?? "";
+ var text = item.GetText() ?? "";
+ var severity = item.GetCategory() switch
+ {
+ __VSERRORCATEGORY.EC_ERROR => "error",
+ __VSERRORCATEGORY.EC_WARNING => "warning",
+ __VSERRORCATEGORY.EC_MESSAGE => "info",
+ var unknown => unknown.ToString(),
+ };
+
+ var message = $"({source}) {document}({line + 1}, {column + 1}): {severity} {errorCode}: {text}";
+ list.Add(message);
+ }
+
+ return list
+ .OrderBy(x => x, StringComparer.OrdinalIgnoreCase)
+ .ThenBy(x => x, StringComparer.Ordinal)
+ .ToImmutableArray();
+ }
+
+ public Task GetErrorCountAsync(CancellationToken cancellationToken)
+ {
+ return GetErrorCountAsync(__VSERRORCATEGORY.EC_WARNING, cancellationToken);
+ }
+
+ public async Task GetErrorCountAsync(__VSERRORCATEGORY minimumSeverity, CancellationToken cancellationToken)
+ {
+ await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
+
+ var errorItems = await GetErrorItemsAsync(cancellationToken);
+ return errorItems.Count(e => e.GetCategory() <= minimumSeverity);
+ }
+
+ private async Task> GetErrorItemsAsync(CancellationToken cancellationToken)
+ {
+ await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
+
+ var errorList = await GetRequiredGlobalServiceAsync(cancellationToken);
+ var args = await errorList.TableControl.ForceUpdateAsync();
+ return args.AllEntries.ToImmutableArray();
+ }
+ }
+}
diff --git a/vsintegration/tests/FSharp.Editor.IntegrationTests/InProcess/SolutionExplorerInProcess.cs b/vsintegration/tests/FSharp.Editor.IntegrationTests/InProcess/SolutionExplorerInProcess.cs
new file mode 100644
index 00000000000..40835e07185
--- /dev/null
+++ b/vsintegration/tests/FSharp.Editor.IntegrationTests/InProcess/SolutionExplorerInProcess.cs
@@ -0,0 +1,353 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.VisualStudio.OperationProgress;
+using Microsoft.VisualStudio.Shell;
+using Microsoft.VisualStudio.Shell.Interop;
+using Microsoft.VisualStudio.TextManager.Interop;
+using Microsoft.VisualStudio.Threading;
+using NuGet.SolutionRestoreManager;
+using Task = System.Threading.Tasks.Task;
+
+namespace Microsoft.VisualStudio.Extensibility.Testing;
+
+internal partial class SolutionExplorerInProcess
+{
+ public async Task CreateSolutionAsync(string solutionName, CancellationToken cancellationToken)
+ {
+ await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
+
+ var solutionPath = CreateTemporaryPath();
+ await CreateSolutionAsync(solutionPath, solutionName, cancellationToken);
+ }
+
+ private async Task CreateSolutionAsync(string solutionPath, string solutionName, CancellationToken cancellationToken)
+ {
+ await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
+
+ await CloseSolutionAsync(cancellationToken);
+
+ var solutionFileName = Path.ChangeExtension(solutionName, ".sln");
+ Directory.CreateDirectory(solutionPath);
+
+ var solution = await GetRequiredGlobalServiceAsync(cancellationToken);
+ ErrorHandler.ThrowOnFailure(solution.CreateSolution(solutionPath, solutionFileName, (uint)__VSCREATESOLUTIONFLAGS.CSF_SILENT));
+ ErrorHandler.ThrowOnFailure(solution.SaveSolutionElement((uint)__VSSLNSAVEOPTIONS.SLNSAVEOPT_ForceSave, null, 0));
+ }
+
+ private async Task GetDirectoryNameAsync(CancellationToken cancellationToken)
+ {
+ await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
+
+ var solution = await GetRequiredGlobalServiceAsync(cancellationToken);
+ ErrorHandler.ThrowOnFailure(solution.GetSolutionInfo(out _, out var solutionFileFullPath, out _));
+ if (string.IsNullOrEmpty(solutionFileFullPath))
+ {
+ throw new InvalidOperationException();
+ }
+
+ return Path.GetDirectoryName(solutionFileFullPath);
+ }
+
+ public async Task AddProjectAsync(string projectName, string projectTemplate, CancellationToken cancellationToken)
+ {
+ await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
+
+ var projectPath = Path.Combine(await GetDirectoryNameAsync(cancellationToken), projectName);
+ var projectTemplatePath = await GetProjectTemplatePathAsync(projectTemplate, cancellationToken);
+ var solution = await GetRequiredGlobalServiceAsync(cancellationToken);
+ ErrorHandler.ThrowOnFailure(solution.AddNewProjectFromTemplate(projectTemplatePath, null, null, projectPath, projectName, null, out _));
+ }
+
+ private async Task GetProjectTemplatePathAsync(string projectTemplate, CancellationToken cancellationToken)
+ {
+ await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
+
+ var dte = await GetRequiredGlobalServiceAsync(cancellationToken);
+ var solution = (EnvDTE80.Solution2)dte.Solution;
+
+ return solution.GetProjectTemplate(projectTemplate, "FSharp");
+ }
+
+ public async Task RestoreNuGetPackagesAsync(CancellationToken cancellationToken)
+ {
+ await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
+
+ var dte = await GetRequiredGlobalServiceAsync(cancellationToken);
+ var solution = (EnvDTE80.Solution2)dte.Solution;
+ foreach (var project in solution.Projects.OfType())
+ {
+ await RestoreNuGetPackagesAsync(project.FullName, cancellationToken);
+ }
+ }
+
+ public async Task RestoreNuGetPackagesAsync(string projectName, CancellationToken cancellationToken)
+ {
+ await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
+
+ var operationProgressStatus = await GetRequiredGlobalServiceAsync(cancellationToken);
+ var stageStatus = operationProgressStatus.GetStageStatus(CommonOperationProgressStageIds.Intellisense);
+ await stageStatus.WaitForCompletionAsync();
+
+ var solutionRestoreService = await GetComponentModelServiceAsync(cancellationToken);
+ await solutionRestoreService.CurrentRestoreOperation;
+
+ var projectFullPath = (await GetProjectAsync(projectName, cancellationToken)).FullName;
+ var solutionRestoreStatusProvider = await GetComponentModelServiceAsync(cancellationToken);
+ if (await solutionRestoreStatusProvider.IsRestoreCompleteAsync(cancellationToken))
+ {
+ return;
+ }
+
+ var solutionRestoreService2 = (IVsSolutionRestoreService2)solutionRestoreService;
+ await solutionRestoreService2.NominateProjectAsync(projectFullPath, cancellationToken);
+
+ while (true)
+ {
+ if (await solutionRestoreStatusProvider.IsRestoreCompleteAsync(cancellationToken))
+ {
+ return;
+ }
+
+ await Task.Delay(TimeSpan.FromMilliseconds(50), cancellationToken);
+ }
+ }
+
+ public async Task?> BuildSolutionAsync(CancellationToken cancellationToken)
+ {
+ await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
+
+ var buildOutputWindowPane = await GetBuildOutputWindowPaneAsync(cancellationToken);
+ buildOutputWindowPane.Clear();
+
+ await TestServices.Shell.ExecuteCommandAsync(VSConstants.VSStd97CmdID.BuildSln, cancellationToken);
+ return await WaitForBuildToFinishAsync(buildOutputWindowPane, cancellationToken);
+ }
+
+ public async Task GetBuildOutputWindowPaneAsync(CancellationToken cancellationToken)
+ {
+ await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
+
+ var outputWindow = await GetRequiredGlobalServiceAsync(cancellationToken);
+ ErrorHandler.ThrowOnFailure(outputWindow.GetPane(VSConstants.OutputWindowPaneGuid.BuildOutputPane_guid, out var pane));
+ return pane;
+ }
+
+ private async Task> WaitForBuildToFinishAsync(IVsOutputWindowPane buildOutputWindowPane, CancellationToken cancellationToken)
+ {
+ await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
+
+ var buildManager = await GetRequiredGlobalServiceAsync(cancellationToken);
+ using var semaphore = new SemaphoreSlim(1);
+ using var solutionEvents = new UpdateSolutionEvents(buildManager);
+
+ await semaphore.WaitAsync();
+
+ void HandleUpdateSolutionDone(bool succeeded, bool modified, bool canceled) => semaphore.Release();
+ solutionEvents.OnUpdateSolutionDone += HandleUpdateSolutionDone;
+ try
+ {
+ await semaphore.WaitAsync();
+ }
+ finally
+ {
+ solutionEvents.OnUpdateSolutionDone -= HandleUpdateSolutionDone;
+ }
+
+ // Force the error list to update
+ ErrorHandler.ThrowOnFailure(buildOutputWindowPane.FlushToTaskList());
+
+ var textView = (IVsTextView)buildOutputWindowPane;
+ var wpfTextViewHost = await textView.GetTextViewHostAsync(JoinableTaskFactory, cancellationToken);
+ var lines = wpfTextViewHost.TextView.TextViewLines;
+ if (lines.Count < 1)
+ {
+ return Enumerable.Empty();
+ }
+
+ return lines.Select(line => line.Extent.GetText());
+ }
+
+ private string CreateTemporaryPath()
+ {
+ return Path.Combine(Path.GetTempPath(), "fsharp-test", Path.GetRandomFileName());
+ }
+
+ private async Task GetProjectAsync(string nameOrFileName, CancellationToken cancellationToken)
+ {
+ await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
+
+ var dte = await GetRequiredGlobalServiceAsync(cancellationToken);
+ var solution = (EnvDTE80.Solution2)dte.Solution;
+ return solution.Projects.OfType().First(
+ project =>
+ {
+ ThreadHelper.ThrowIfNotOnUIThread();
+ return string.Equals(project.FileName, nameOrFileName, StringComparison.OrdinalIgnoreCase)
+ || string.Equals(project.Name, nameOrFileName, StringComparison.OrdinalIgnoreCase);
+ });
+ }
+
+ internal sealed class UpdateSolutionEvents : IVsUpdateSolutionEvents, IVsUpdateSolutionEvents2, IDisposable
+ {
+ private uint _cookie;
+ private IVsSolutionBuildManager2 _solutionBuildManager;
+
+ internal delegate void UpdateSolutionDoneEvent(bool succeeded, bool modified, bool canceled);
+
+ internal delegate void UpdateSolutionBeginEvent(ref bool cancel);
+
+ internal delegate void UpdateSolutionStartUpdateEvent(ref bool cancel);
+
+ internal delegate void UpdateProjectConfigDoneEvent(IVsHierarchy projectHierarchy, IVsCfg projectConfig, int success);
+
+ internal delegate void UpdateProjectConfigBeginEvent(IVsHierarchy projectHierarchy, IVsCfg projectConfig);
+
+ public event UpdateSolutionDoneEvent? OnUpdateSolutionDone;
+
+ public event UpdateSolutionBeginEvent? OnUpdateSolutionBegin;
+
+ public event UpdateSolutionStartUpdateEvent? OnUpdateSolutionStartUpdate;
+
+ public event Action? OnActiveProjectConfigurationChange;
+
+ public event Action? OnUpdateSolutionCancel;
+
+ public event UpdateProjectConfigDoneEvent? OnUpdateProjectConfigDone;
+
+ public event UpdateProjectConfigBeginEvent? OnUpdateProjectConfigBegin;
+
+ internal UpdateSolutionEvents(IVsSolutionBuildManager2 solutionBuildManager)
+ {
+ ThreadHelper.ThrowIfNotOnUIThread();
+
+ _solutionBuildManager = solutionBuildManager;
+ ErrorHandler.ThrowOnFailure(solutionBuildManager.AdviseUpdateSolutionEvents(this, out _cookie));
+ }
+
+ int IVsUpdateSolutionEvents.UpdateSolution_Begin(ref int pfCancelUpdate)
+ {
+ var cancel = false;
+ OnUpdateSolutionBegin?.Invoke(ref cancel);
+ if (cancel)
+ {
+ pfCancelUpdate = 1;
+ }
+
+ return 0;
+ }
+
+ int IVsUpdateSolutionEvents.UpdateSolution_Done(int fSucceeded, int fModified, int fCancelCommand)
+ {
+ OnUpdateSolutionDone?.Invoke(fSucceeded != 0, fModified != 0, fCancelCommand != 0);
+ return 0;
+ }
+
+ int IVsUpdateSolutionEvents.UpdateSolution_StartUpdate(ref int pfCancelUpdate)
+ {
+ return UpdateSolution_StartUpdate(ref pfCancelUpdate);
+ }
+
+ int IVsUpdateSolutionEvents.UpdateSolution_Cancel()
+ {
+ OnUpdateSolutionCancel?.Invoke();
+ return 0;
+ }
+
+ int IVsUpdateSolutionEvents.OnActiveProjectCfgChange(IVsHierarchy pIVsHierarchy)
+ {
+ return OnActiveProjectCfgChange(pIVsHierarchy);
+ }
+
+ int IVsUpdateSolutionEvents2.UpdateSolution_Begin(ref int pfCancelUpdate)
+ {
+ var cancel = false;
+ OnUpdateSolutionBegin?.Invoke(ref cancel);
+ if (cancel)
+ {
+ pfCancelUpdate = 1;
+ }
+
+ return 0;
+ }
+
+ int IVsUpdateSolutionEvents2.UpdateSolution_Done(int fSucceeded, int fModified, int fCancelCommand)
+ {
+ OnUpdateSolutionDone?.Invoke(fSucceeded != 0, fModified != 0, fCancelCommand != 0);
+ return 0;
+ }
+
+ int IVsUpdateSolutionEvents2.UpdateSolution_StartUpdate(ref int pfCancelUpdate)
+ {
+ return UpdateSolution_StartUpdate(ref pfCancelUpdate);
+ }
+
+ int IVsUpdateSolutionEvents2.UpdateSolution_Cancel()
+ {
+ OnUpdateSolutionCancel?.Invoke();
+ return 0;
+ }
+
+ int IVsUpdateSolutionEvents2.OnActiveProjectCfgChange(IVsHierarchy pIVsHierarchy)
+ {
+ return OnActiveProjectCfgChange(pIVsHierarchy);
+ }
+
+ int IVsUpdateSolutionEvents2.UpdateProjectCfg_Begin(IVsHierarchy pHierProj, IVsCfg pCfgProj, IVsCfg pCfgSln, uint dwAction, ref int pfCancel)
+ {
+ OnUpdateProjectConfigBegin?.Invoke(pHierProj, pCfgProj);
+ return 0;
+ }
+
+ int IVsUpdateSolutionEvents2.UpdateProjectCfg_Done(IVsHierarchy pHierProj, IVsCfg pCfgProj, IVsCfg pCfgSln, uint dwAction, int fSuccess, int fCancel)
+ {
+ OnUpdateProjectConfigDone?.Invoke(pHierProj, pCfgProj, fSuccess);
+ return 0;
+ }
+
+ private int UpdateSolution_StartUpdate(ref int pfCancelUpdate)
+ {
+ var cancel = false;
+ OnUpdateSolutionStartUpdate?.Invoke(ref cancel);
+ if (cancel)
+ {
+ pfCancelUpdate = 1;
+ }
+
+ return 0;
+ }
+
+ private int OnActiveProjectCfgChange(IVsHierarchy pIVsHierarchy)
+ {
+ OnActiveProjectConfigurationChange?.Invoke();
+ return 0;
+ }
+
+ void IDisposable.Dispose()
+ {
+ ThreadHelper.ThrowIfNotOnUIThread();
+
+ OnUpdateSolutionDone = null;
+ OnUpdateSolutionBegin = null;
+ OnUpdateSolutionStartUpdate = null;
+ OnActiveProjectConfigurationChange = null;
+ OnUpdateSolutionCancel = null;
+ OnUpdateProjectConfigDone = null;
+ OnUpdateProjectConfigBegin = null;
+
+ if (_cookie != 0)
+ {
+ var tempCookie = _cookie;
+ _cookie = 0;
+ ErrorHandler.ThrowOnFailure(_solutionBuildManager.UnadviseUpdateSolutionEvents(tempCookie));
+ }
+ }
+ }
+}
diff --git a/vsintegration/tests/FSharp.Editor.IntegrationTests/InProcess/TelemetryInProcess.cs b/vsintegration/tests/FSharp.Editor.IntegrationTests/InProcess/TelemetryInProcess.cs
new file mode 100644
index 00000000000..a8209cdc513
--- /dev/null
+++ b/vsintegration/tests/FSharp.Editor.IntegrationTests/InProcess/TelemetryInProcess.cs
@@ -0,0 +1,80 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.VisualStudio.Telemetry;
+using Microsoft.VisualStudio.Threading;
+using IAsyncDisposable = System.IAsyncDisposable;
+
+namespace Microsoft.VisualStudio.Extensibility.Testing;
+
+[TestService]
+internal partial class TelemetryInProcess
+{
+ internal async Task EnableTestTelemetryChannelAsync(CancellationToken cancellationToken)
+ {
+ await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
+ TelemetryService.DetachTestChannel(LoggerTestChannel.Instance);
+ LoggerTestChannel.Instance.Clear();
+ TelemetryService.AttachTestChannel(LoggerTestChannel.Instance);
+ return new TelemetryChannel(TestServices);
+ }
+
+ internal async Task DisableTestTelemetryChannelAsync(CancellationToken cancellationToken)
+ {
+ await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
+ TelemetryService.DetachTestChannel(LoggerTestChannel.Instance);
+ LoggerTestChannel.Instance.Clear();
+ }
+
+ public async Task TryWaitForTelemetryEventsAsync(string eventName, CancellationToken cancellationToken)
+ => await LoggerTestChannel.Instance.TryWaitForEventsAsync(eventName, cancellationToken);
+
+ public class TelemetryChannel : IAsyncDisposable
+ {
+ internal TestServices _testServices;
+
+ public TelemetryChannel(TestServices testServices)
+ {
+ _testServices = testServices;
+ }
+
+ public async ValueTask DisposeAsync()
+ => await _testServices.Telemetry.DisableTestTelemetryChannelAsync(CancellationToken.None);
+
+ public async Task GetEventAsync(string eventName, CancellationToken cancellationToken)
+ => await _testServices.Telemetry.TryWaitForTelemetryEventsAsync(eventName, cancellationToken);
+ }
+
+ private sealed class LoggerTestChannel : ITelemetryTestChannel
+ {
+ public static readonly LoggerTestChannel Instance = new();
+
+ private AsyncQueue _eventsQueue = new();
+
+ public async Task TryWaitForEventsAsync(string eventName, CancellationToken cancellationToken)
+ {
+ while (true)
+ {
+ var result = await _eventsQueue.DequeueAsync(cancellationToken);
+ if (result.Name == eventName)
+ {
+ return result;
+ }
+ }
+ }
+
+ public void Clear()
+ {
+ _eventsQueue.Complete();
+ _eventsQueue = new AsyncQueue();
+ }
+
+ void ITelemetryTestChannel.OnPostEvent(object sender, TelemetryTestChannelEventArgs e)
+ {
+ _eventsQueue.Enqueue(e.Event);
+ }
+ }
+}
diff --git a/vsintegration/tests/FSharp.Editor.IntegrationTests/TelemetryTests.cs b/vsintegration/tests/FSharp.Editor.IntegrationTests/TelemetryTests.cs
new file mode 100644
index 00000000000..ddb11260bf1
--- /dev/null
+++ b/vsintegration/tests/FSharp.Editor.IntegrationTests/TelemetryTests.cs
@@ -0,0 +1,35 @@
+using Microsoft.CodeAnalysis.Testing;
+using System;
+using System.Diagnostics;
+using System.IO;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace FSharp.Editor.IntegrationTests;
+
+public class TelemetryTests : AbstractIntegrationTest
+{
+ [IdeFact]
+ public async Task BasicFSharpTelemetry_Async()
+ {
+ var token = HangMitigatingCancellationToken;
+ var template = WellKnownProjectTemplates.FSharpNetCoreClassLibrary;
+ var solutionExplorer = TestServices.SolutionExplorer;
+ var editor = TestServices.Editor;
+ var telemetry = TestServices.Telemetry;
+
+ await using var telemetryChannel = await telemetry.EnableTestTelemetryChannelAsync(token);
+ await solutionExplorer.CreateSolutionAsync(nameof(TelemetryTests), token);
+ await solutionExplorer.AddProjectAsync("Library", template, token);
+ await solutionExplorer.BuildSolutionAsync(token);
+
+ var eventName = "vs/projectsystem/cps/loadcpsproject";
+ var @event = await telemetryChannel.GetEventAsync(eventName, token);
+
+ var propKey = "VS.ProjectSystem.Cps.Project.Extension";
+ Assert.True(@event.Properties.ContainsKey(propKey));
+
+ var propValue = @event.Properties[propKey].ToString();
+ Assert.Equal("fsproj", propValue);
+ }
+}
diff --git a/vsintegration/tests/FSharp.Editor.IntegrationTests/WellKnownProjectTemplates.cs b/vsintegration/tests/FSharp.Editor.IntegrationTests/WellKnownProjectTemplates.cs
new file mode 100644
index 00000000000..25681a013be
--- /dev/null
+++ b/vsintegration/tests/FSharp.Editor.IntegrationTests/WellKnownProjectTemplates.cs
@@ -0,0 +1,12 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+namespace FSharp.Editor.IntegrationTests;
+
+internal static class WellKnownProjectTemplates
+{
+ public const string FSharpNetCoreClassLibrary = "Microsoft.FSharp.NETCore.ClassLibrary";
+ public const string FSharpNetCoreConsoleApplication = "Microsoft.FSharp.NETCore.ConsoleApplication";
+ public const string FSharpNetCoreXUnitTest = "Microsoft.FSharp.NETCore.XUnitTest";
+}
diff --git a/vsintegration/tests/FSharp.Editor.IntegrationTests/xunit.runner.json b/vsintegration/tests/FSharp.Editor.IntegrationTests/xunit.runner.json
new file mode 100644
index 00000000000..2d07715ae5f
--- /dev/null
+++ b/vsintegration/tests/FSharp.Editor.IntegrationTests/xunit.runner.json
@@ -0,0 +1,7 @@
+{
+ "$schema": "https://xunit.net/schema/current/xunit.runner.schema.json",
+ "appDomain": "ifAvailable",
+ "shadowCopy": false,
+ "parallelizeTestCollections": false,
+ "maxParallelThreads": 1
+}