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 +}