From 5b8b3b8fa320db580b69c6d5f688e851108492a7 Mon Sep 17 00:00:00 2001 From: Spencer Farley <2847259+farlee2121@users.noreply.github.com> Date: Wed, 30 Apr 2025 19:40:56 -0500 Subject: [PATCH 01/39] Set up the stub of a test explorer test project --- .../FsAutoComplete.Tests.TestExplorer.fsproj | 17 +++++++++ .../FsAutoComplete.Tests.TestExplorer/Main.fs | 6 ++++ .../Sample.fs | 36 +++++++++++++++++++ .../paket.references | 3 ++ 4 files changed, 62 insertions(+) create mode 100644 test/FsAutoComplete.Tests.TestExplorer/FsAutoComplete.Tests.TestExplorer.fsproj create mode 100644 test/FsAutoComplete.Tests.TestExplorer/Main.fs create mode 100644 test/FsAutoComplete.Tests.TestExplorer/Sample.fs create mode 100644 test/FsAutoComplete.Tests.TestExplorer/paket.references diff --git a/test/FsAutoComplete.Tests.TestExplorer/FsAutoComplete.Tests.TestExplorer.fsproj b/test/FsAutoComplete.Tests.TestExplorer/FsAutoComplete.Tests.TestExplorer.fsproj new file mode 100644 index 000000000..4451176a4 --- /dev/null +++ b/test/FsAutoComplete.Tests.TestExplorer/FsAutoComplete.Tests.TestExplorer.fsproj @@ -0,0 +1,17 @@ + + + + Exe + net8.0 + false + + + + + + + + + + + diff --git a/test/FsAutoComplete.Tests.TestExplorer/Main.fs b/test/FsAutoComplete.Tests.TestExplorer/Main.fs new file mode 100644 index 000000000..b2099cc91 --- /dev/null +++ b/test/FsAutoComplete.Tests.TestExplorer/Main.fs @@ -0,0 +1,6 @@ +module FsAutoComplete.Tests.TestExplorer +open Expecto + +[] +let main argv = + Tests.runTestsInAssemblyWithCLIArgs [] argv diff --git a/test/FsAutoComplete.Tests.TestExplorer/Sample.fs b/test/FsAutoComplete.Tests.TestExplorer/Sample.fs new file mode 100644 index 000000000..e38ed22c1 --- /dev/null +++ b/test/FsAutoComplete.Tests.TestExplorer/Sample.fs @@ -0,0 +1,36 @@ +module Tests + +open Expecto + +[] +let tests = + testList "samples" [ + testCase "universe exists (╭ರᴥ•́)" <| fun _ -> + let subject = true + Expect.isTrue subject "I compute, therefore I am." + + testCase "when true is not (should fail)" <| fun _ -> + let subject = false + Expect.isTrue subject "I should fail because the subject is false" + + testCase "I'm skipped (should skip)" <| fun _ -> + Tests.skiptest "Yup, waiting for a sunny day..." + + testCase "I'm always fail (should fail)" <| fun _ -> + Tests.failtest "This was expected..." + + testCase "contains things" <| fun _ -> + Expect.containsAll [| 2; 3; 4 |] [| 2; 4 |] + "This is the case; {2,3,4} contains {2,4}" + + testCase "contains things (should fail)" <| fun _ -> + Expect.containsAll [| 2; 3; 4 |] [| 2; 4; 1 |] + "Expecting we have one (1) in there" + + testCase "Sometimes I want to ༼ノಠل͟ಠ༽ノ ︵ ┻━┻" <| fun _ -> + Expect.equal "abcdëf" "abcdef" "These should equal" + + test "I am (should fail)" { + "╰〳 ಠ 益 ಠೃ 〵╯" |> Expect.equal true false + } + ] diff --git a/test/FsAutoComplete.Tests.TestExplorer/paket.references b/test/FsAutoComplete.Tests.TestExplorer/paket.references new file mode 100644 index 000000000..1a4000a66 --- /dev/null +++ b/test/FsAutoComplete.Tests.TestExplorer/paket.references @@ -0,0 +1,3 @@ +FSharp.Core content: once +Microsoft.NET.Test.Sdk +YoloDev.Expecto.TestSdk \ No newline at end of file From 6018b129133badf8438887f0000a300ec60cb946 Mon Sep 17 00:00:00 2001 From: Spencer Farley <2847259+farlee2121@users.noreply.github.com> Date: Wed, 30 Apr 2025 19:45:36 -0500 Subject: [PATCH 02/39] Add the test explorer tests to the sln --- FsAutoComplete.sln | 79 +++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 75 insertions(+), 4 deletions(-) diff --git a/FsAutoComplete.sln b/FsAutoComplete.sln index 880144ac3..4fd2ccb49 100644 --- a/FsAutoComplete.sln +++ b/FsAutoComplete.sln @@ -27,44 +27,114 @@ Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "FsAutoComplete.DependencyMa EndProject Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "benchmarks", "benchmarks\benchmarks.fsproj", "{0CD029D8-B39E-4CBE-A190-C84A7A811180}" EndProject +Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "FsAutoComplete.Tests.TestExplorer", "test\FsAutoComplete.Tests.TestExplorer\FsAutoComplete.Tests.TestExplorer.fsproj", "{C6CE23AF-EDCE-49ED-AC2F-8AD595C30CD8}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {B6AB4EF3-8F60-41A1-AB0C-851A6DEB169E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B6AB4EF3-8F60-41A1-AB0C-851A6DEB169E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B6AB4EF3-8F60-41A1-AB0C-851A6DEB169E}.Debug|x64.ActiveCfg = Debug|Any CPU + {B6AB4EF3-8F60-41A1-AB0C-851A6DEB169E}.Debug|x64.Build.0 = Debug|Any CPU + {B6AB4EF3-8F60-41A1-AB0C-851A6DEB169E}.Debug|x86.ActiveCfg = Debug|Any CPU + {B6AB4EF3-8F60-41A1-AB0C-851A6DEB169E}.Debug|x86.Build.0 = Debug|Any CPU {B6AB4EF3-8F60-41A1-AB0C-851A6DEB169E}.Release|Any CPU.ActiveCfg = Release|Any CPU {B6AB4EF3-8F60-41A1-AB0C-851A6DEB169E}.Release|Any CPU.Build.0 = Release|Any CPU + {B6AB4EF3-8F60-41A1-AB0C-851A6DEB169E}.Release|x64.ActiveCfg = Release|Any CPU + {B6AB4EF3-8F60-41A1-AB0C-851A6DEB169E}.Release|x64.Build.0 = Release|Any CPU + {B6AB4EF3-8F60-41A1-AB0C-851A6DEB169E}.Release|x86.ActiveCfg = Release|Any CPU + {B6AB4EF3-8F60-41A1-AB0C-851A6DEB169E}.Release|x86.Build.0 = Release|Any CPU {4E4786F3-4566-44E1-8787-91790007F0F6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {4E4786F3-4566-44E1-8787-91790007F0F6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4E4786F3-4566-44E1-8787-91790007F0F6}.Debug|x64.ActiveCfg = Debug|Any CPU + {4E4786F3-4566-44E1-8787-91790007F0F6}.Debug|x64.Build.0 = Debug|Any CPU + {4E4786F3-4566-44E1-8787-91790007F0F6}.Debug|x86.ActiveCfg = Debug|Any CPU + {4E4786F3-4566-44E1-8787-91790007F0F6}.Debug|x86.Build.0 = Debug|Any CPU {4E4786F3-4566-44E1-8787-91790007F0F6}.Release|Any CPU.ActiveCfg = Release|Any CPU {4E4786F3-4566-44E1-8787-91790007F0F6}.Release|Any CPU.Build.0 = Release|Any CPU - {775C1714-AD5F-4A4C-B613-0AE08F51E17A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {775C1714-AD5F-4A4C-B613-0AE08F51E17A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {775C1714-AD5F-4A4C-B613-0AE08F51E17A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {775C1714-AD5F-4A4C-B613-0AE08F51E17A}.Release|Any CPU.Build.0 = Release|Any CPU + {4E4786F3-4566-44E1-8787-91790007F0F6}.Release|x64.ActiveCfg = Release|Any CPU + {4E4786F3-4566-44E1-8787-91790007F0F6}.Release|x64.Build.0 = Release|Any CPU + {4E4786F3-4566-44E1-8787-91790007F0F6}.Release|x86.ActiveCfg = Release|Any CPU + {4E4786F3-4566-44E1-8787-91790007F0F6}.Release|x86.Build.0 = Release|Any CPU {6FE2E08A-9F3F-4C7C-B190-77C46AE147D7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {6FE2E08A-9F3F-4C7C-B190-77C46AE147D7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6FE2E08A-9F3F-4C7C-B190-77C46AE147D7}.Debug|x64.ActiveCfg = Debug|Any CPU + {6FE2E08A-9F3F-4C7C-B190-77C46AE147D7}.Debug|x64.Build.0 = Debug|Any CPU + {6FE2E08A-9F3F-4C7C-B190-77C46AE147D7}.Debug|x86.ActiveCfg = Debug|Any CPU + {6FE2E08A-9F3F-4C7C-B190-77C46AE147D7}.Debug|x86.Build.0 = Debug|Any CPU {6FE2E08A-9F3F-4C7C-B190-77C46AE147D7}.Release|Any CPU.ActiveCfg = Release|Any CPU {6FE2E08A-9F3F-4C7C-B190-77C46AE147D7}.Release|Any CPU.Build.0 = Release|Any CPU + {6FE2E08A-9F3F-4C7C-B190-77C46AE147D7}.Release|x64.ActiveCfg = Release|Any CPU + {6FE2E08A-9F3F-4C7C-B190-77C46AE147D7}.Release|x64.Build.0 = Release|Any CPU + {6FE2E08A-9F3F-4C7C-B190-77C46AE147D7}.Release|x86.ActiveCfg = Release|Any CPU + {6FE2E08A-9F3F-4C7C-B190-77C46AE147D7}.Release|x86.Build.0 = Release|Any CPU {38C1F619-3E1E-4784-9833-E8A2AA95CDAE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {38C1F619-3E1E-4784-9833-E8A2AA95CDAE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {38C1F619-3E1E-4784-9833-E8A2AA95CDAE}.Debug|x64.ActiveCfg = Debug|Any CPU + {38C1F619-3E1E-4784-9833-E8A2AA95CDAE}.Debug|x64.Build.0 = Debug|Any CPU + {38C1F619-3E1E-4784-9833-E8A2AA95CDAE}.Debug|x86.ActiveCfg = Debug|Any CPU + {38C1F619-3E1E-4784-9833-E8A2AA95CDAE}.Debug|x86.Build.0 = Debug|Any CPU {38C1F619-3E1E-4784-9833-E8A2AA95CDAE}.Release|Any CPU.ActiveCfg = Release|Any CPU {38C1F619-3E1E-4784-9833-E8A2AA95CDAE}.Release|Any CPU.Build.0 = Release|Any CPU + {38C1F619-3E1E-4784-9833-E8A2AA95CDAE}.Release|x64.ActiveCfg = Release|Any CPU + {38C1F619-3E1E-4784-9833-E8A2AA95CDAE}.Release|x64.Build.0 = Release|Any CPU + {38C1F619-3E1E-4784-9833-E8A2AA95CDAE}.Release|x86.ActiveCfg = Release|Any CPU + {38C1F619-3E1E-4784-9833-E8A2AA95CDAE}.Release|x86.Build.0 = Release|Any CPU {14C55B44-2063-4891-98BE-8184CAB1BE87}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {14C55B44-2063-4891-98BE-8184CAB1BE87}.Debug|Any CPU.Build.0 = Debug|Any CPU + {14C55B44-2063-4891-98BE-8184CAB1BE87}.Debug|x64.ActiveCfg = Debug|Any CPU + {14C55B44-2063-4891-98BE-8184CAB1BE87}.Debug|x64.Build.0 = Debug|Any CPU + {14C55B44-2063-4891-98BE-8184CAB1BE87}.Debug|x86.ActiveCfg = Debug|Any CPU + {14C55B44-2063-4891-98BE-8184CAB1BE87}.Debug|x86.Build.0 = Debug|Any CPU {14C55B44-2063-4891-98BE-8184CAB1BE87}.Release|Any CPU.ActiveCfg = Release|Any CPU {14C55B44-2063-4891-98BE-8184CAB1BE87}.Release|Any CPU.Build.0 = Release|Any CPU + {14C55B44-2063-4891-98BE-8184CAB1BE87}.Release|x64.ActiveCfg = Release|Any CPU + {14C55B44-2063-4891-98BE-8184CAB1BE87}.Release|x64.Build.0 = Release|Any CPU + {14C55B44-2063-4891-98BE-8184CAB1BE87}.Release|x86.ActiveCfg = Release|Any CPU + {14C55B44-2063-4891-98BE-8184CAB1BE87}.Release|x86.Build.0 = Release|Any CPU {C58701B0-D8E3-4B68-A7DE-8524C95F86C0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C58701B0-D8E3-4B68-A7DE-8524C95F86C0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C58701B0-D8E3-4B68-A7DE-8524C95F86C0}.Debug|x64.ActiveCfg = Debug|Any CPU + {C58701B0-D8E3-4B68-A7DE-8524C95F86C0}.Debug|x64.Build.0 = Debug|Any CPU + {C58701B0-D8E3-4B68-A7DE-8524C95F86C0}.Debug|x86.ActiveCfg = Debug|Any CPU + {C58701B0-D8E3-4B68-A7DE-8524C95F86C0}.Debug|x86.Build.0 = Debug|Any CPU {C58701B0-D8E3-4B68-A7DE-8524C95F86C0}.Release|Any CPU.ActiveCfg = Release|Any CPU {C58701B0-D8E3-4B68-A7DE-8524C95F86C0}.Release|Any CPU.Build.0 = Release|Any CPU + {C58701B0-D8E3-4B68-A7DE-8524C95F86C0}.Release|x64.ActiveCfg = Release|Any CPU + {C58701B0-D8E3-4B68-A7DE-8524C95F86C0}.Release|x64.Build.0 = Release|Any CPU + {C58701B0-D8E3-4B68-A7DE-8524C95F86C0}.Release|x86.ActiveCfg = Release|Any CPU + {C58701B0-D8E3-4B68-A7DE-8524C95F86C0}.Release|x86.Build.0 = Release|Any CPU {0CD029D8-B39E-4CBE-A190-C84A7A811180}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {0CD029D8-B39E-4CBE-A190-C84A7A811180}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0CD029D8-B39E-4CBE-A190-C84A7A811180}.Debug|x64.ActiveCfg = Debug|Any CPU + {0CD029D8-B39E-4CBE-A190-C84A7A811180}.Debug|x64.Build.0 = Debug|Any CPU + {0CD029D8-B39E-4CBE-A190-C84A7A811180}.Debug|x86.ActiveCfg = Debug|Any CPU + {0CD029D8-B39E-4CBE-A190-C84A7A811180}.Debug|x86.Build.0 = Debug|Any CPU {0CD029D8-B39E-4CBE-A190-C84A7A811180}.Release|Any CPU.ActiveCfg = Release|Any CPU {0CD029D8-B39E-4CBE-A190-C84A7A811180}.Release|Any CPU.Build.0 = Release|Any CPU + {0CD029D8-B39E-4CBE-A190-C84A7A811180}.Release|x64.ActiveCfg = Release|Any CPU + {0CD029D8-B39E-4CBE-A190-C84A7A811180}.Release|x64.Build.0 = Release|Any CPU + {0CD029D8-B39E-4CBE-A190-C84A7A811180}.Release|x86.ActiveCfg = Release|Any CPU + {0CD029D8-B39E-4CBE-A190-C84A7A811180}.Release|x86.Build.0 = Release|Any CPU + {C6CE23AF-EDCE-49ED-AC2F-8AD595C30CD8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C6CE23AF-EDCE-49ED-AC2F-8AD595C30CD8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C6CE23AF-EDCE-49ED-AC2F-8AD595C30CD8}.Debug|x64.ActiveCfg = Debug|Any CPU + {C6CE23AF-EDCE-49ED-AC2F-8AD595C30CD8}.Debug|x64.Build.0 = Debug|Any CPU + {C6CE23AF-EDCE-49ED-AC2F-8AD595C30CD8}.Debug|x86.ActiveCfg = Debug|Any CPU + {C6CE23AF-EDCE-49ED-AC2F-8AD595C30CD8}.Debug|x86.Build.0 = Debug|Any CPU + {C6CE23AF-EDCE-49ED-AC2F-8AD595C30CD8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C6CE23AF-EDCE-49ED-AC2F-8AD595C30CD8}.Release|Any CPU.Build.0 = Release|Any CPU + {C6CE23AF-EDCE-49ED-AC2F-8AD595C30CD8}.Release|x64.ActiveCfg = Release|Any CPU + {C6CE23AF-EDCE-49ED-AC2F-8AD595C30CD8}.Release|x64.Build.0 = Release|Any CPU + {C6CE23AF-EDCE-49ED-AC2F-8AD595C30CD8}.Release|x86.ActiveCfg = Release|Any CPU + {C6CE23AF-EDCE-49ED-AC2F-8AD595C30CD8}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -74,6 +144,7 @@ Global {38C1F619-3E1E-4784-9833-E8A2AA95CDAE} = {BA56455D-4AEA-45FC-A569-027A68A76BA6} {14C55B44-2063-4891-98BE-8184CAB1BE87} = {443E0B8D-9AD0-436E-A331-E8CC12965F07} {C58701B0-D8E3-4B68-A7DE-8524C95F86C0} = {443E0B8D-9AD0-436E-A331-E8CC12965F07} + {C6CE23AF-EDCE-49ED-AC2F-8AD595C30CD8} = {443E0B8D-9AD0-436E-A331-E8CC12965F07} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {1C4EE83B-632A-4929-8C96-38F14254229E} From 1b1bb7488214cb0723494ae4314f970bb5994c82 Mon Sep 17 00:00:00 2001 From: farlee2121 <2847259+farlee2121@users.noreply.github.com> Date: Wed, 7 May 2025 17:12:26 -0500 Subject: [PATCH 03/39] Set up a trival test case to scaffold for VSTestAdapter work --- .../FsAutoComplete.Core.fsproj | 136 +++++++++--------- src/FsAutoComplete.Core/VSTestAdapter.fs | 7 + .../Sample.fs | 35 +---- 3 files changed, 82 insertions(+), 96 deletions(-) create mode 100644 src/FsAutoComplete.Core/VSTestAdapter.fs diff --git a/src/FsAutoComplete.Core/FsAutoComplete.Core.fsproj b/src/FsAutoComplete.Core/FsAutoComplete.Core.fsproj index 04812cf9f..68c83bdc9 100644 --- a/src/FsAutoComplete.Core/FsAutoComplete.Core.fsproj +++ b/src/FsAutoComplete.Core/FsAutoComplete.Core.fsproj @@ -1,67 +1,69 @@ - - - net8.0 - net8.0;net9.0 - false - $(NoWarn);FS0057 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + net8.0 + net8.0;net9.0 + false + $(NoWarn);FS0057 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/FsAutoComplete.Core/VSTestAdapter.fs b/src/FsAutoComplete.Core/VSTestAdapter.fs new file mode 100644 index 000000000..c8dca6d50 --- /dev/null +++ b/src/FsAutoComplete.Core/VSTestAdapter.fs @@ -0,0 +1,7 @@ +namespace FsAutoComplete.VSTestAdapter + + +module VSTestWrapper = + open FsAutoComplete.Utils + + let discoverTests (_: ProjectFilePath list) = [] \ No newline at end of file diff --git a/test/FsAutoComplete.Tests.TestExplorer/Sample.fs b/test/FsAutoComplete.Tests.TestExplorer/Sample.fs index e38ed22c1..1b0449451 100644 --- a/test/FsAutoComplete.Tests.TestExplorer/Sample.fs +++ b/test/FsAutoComplete.Tests.TestExplorer/Sample.fs @@ -1,36 +1,13 @@ module Tests open Expecto +open FsAutoComplete.VSTestAdapter [] let tests = - testList "samples" [ - testCase "universe exists (╭ರᴥ•́)" <| fun _ -> - let subject = true - Expect.isTrue subject "I compute, therefore I am." - - testCase "when true is not (should fail)" <| fun _ -> - let subject = false - Expect.isTrue subject "I should fail because the subject is false" - - testCase "I'm skipped (should skip)" <| fun _ -> - Tests.skiptest "Yup, waiting for a sunny day..." - - testCase "I'm always fail (should fail)" <| fun _ -> - Tests.failtest "This was expected..." - - testCase "contains things" <| fun _ -> - Expect.containsAll [| 2; 3; 4 |] [| 2; 4 |] - "This is the case; {2,3,4} contains {2,4}" - - testCase "contains things (should fail)" <| fun _ -> - Expect.containsAll [| 2; 3; 4 |] [| 2; 4; 1 |] - "Expecting we have one (1) in there" - - testCase "Sometimes I want to ༼ノಠل͟ಠ༽ノ ︵ ┻━┻" <| fun _ -> - Expect.equal "abcdëf" "abcdef" "These should equal" - - test "I am (should fail)" { - "╰〳 ಠ 益 ಠೃ 〵╯" |> Expect.equal true false - } + testList "Test Discovery" [ + testCase "No projects, no tests" <| fun _ -> + let expected = [] + let actual = VSTestWrapper.discoverTests [] + Expect.equal actual expected "" ] From f3d2f3f0cd3df27973968c8bbf78318e98f199f5 Mon Sep 17 00:00:00 2001 From: farlee2121 <2847259+farlee2121@users.noreply.github.com> Date: Fri, 9 May 2025 16:02:17 -0500 Subject: [PATCH 04/39] Update Expecto for performance improvements --- paket.dependencies | 1 + paket.lock | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/paket.dependencies b/paket.dependencies index 23f193530..f6f9f536c 100644 --- a/paket.dependencies +++ b/paket.dependencies @@ -44,6 +44,7 @@ nuget Microsoft.NET.Test.Sdk >= 17.4 nuget Dotnet.ReproducibleBuilds copy_local:true nuget Ionide.KeepAChangelog.Tasks copy_local: true +nuget Expecto ~> 10 nuget Expecto.Diff nuget YoloDev.Expecto.TestSdk nuget AltCover diff --git a/paket.lock b/paket.lock index 7aba40c53..02b55f5fc 100644 --- a/paket.lock +++ b/paket.lock @@ -36,7 +36,7 @@ NUGET Serilog (>= 3.1.1) DiffPlex (1.7.2) - restriction: || (== net8.0) (== net9.0) (&& (== netstandard2.0) (>= net6.0)) (&& (== netstandard2.1) (>= net6.0)) Dotnet.ReproducibleBuilds (1.2.25) - copy_local: true - Expecto (10.2.1) - restriction: || (== net8.0) (== net9.0) (&& (== netstandard2.0) (>= net6.0)) (&& (== netstandard2.1) (>= net6.0)) + Expecto (10.2.3) FSharp.Core (>= 7.0.200) - restriction: || (== net8.0) (== net9.0) (&& (== netstandard2.0) (>= net6.0)) (&& (== netstandard2.1) (>= net6.0)) Mono.Cecil (>= 0.11.4 < 1.0) - restriction: || (== net8.0) (== net9.0) (&& (== netstandard2.0) (>= net6.0)) (&& (== netstandard2.1) (>= net6.0)) Expecto.Diff (10.2.1) From 3a386f693f66d48904329b4f36920f9ae80033e6 Mon Sep 17 00:00:00 2001 From: farlee2121 <2847259+farlee2121@users.noreply.github.com> Date: Mon, 21 Jul 2025 15:42:45 -0500 Subject: [PATCH 05/39] Demonstrate the most basic test discovery with vstest --- paket.dependencies | 5 ++ paket.lock | 4 ++ src/FsAutoComplete.Core/VSTestAdapter.fs | 40 ++++++++++++++- src/FsAutoComplete.Core/paket.references | 3 ++ .../FsAutoComplete.Tests.TestExplorer.fsproj | 3 +- .../Sample.fs | 13 ----- .../VSTest.XUnit.Tests/Program.fs | 4 ++ .../VSTest.XUnit.Tests/Tests.fs | 8 +++ .../VSTest.XUnit.Tests.fsproj | 22 +++++++++ .../SampleTestProjects/paket.dependencies | 0 .../SampleTestProjects/paket.lock | 1 + .../TestDiscovery.fs | 49 +++++++++++++++++++ .../paket.references | 3 +- 13 files changed, 138 insertions(+), 17 deletions(-) delete mode 100644 test/FsAutoComplete.Tests.TestExplorer/Sample.fs create mode 100644 test/FsAutoComplete.Tests.TestExplorer/SampleTestProjects/VSTest.XUnit.Tests/Program.fs create mode 100644 test/FsAutoComplete.Tests.TestExplorer/SampleTestProjects/VSTest.XUnit.Tests/Tests.fs create mode 100644 test/FsAutoComplete.Tests.TestExplorer/SampleTestProjects/VSTest.XUnit.Tests/VSTest.XUnit.Tests.fsproj create mode 100644 test/FsAutoComplete.Tests.TestExplorer/SampleTestProjects/paket.dependencies create mode 100644 test/FsAutoComplete.Tests.TestExplorer/SampleTestProjects/paket.lock create mode 100644 test/FsAutoComplete.Tests.TestExplorer/TestDiscovery.fs diff --git a/paket.dependencies b/paket.dependencies index f6f9f536c..10077961a 100644 --- a/paket.dependencies +++ b/paket.dependencies @@ -59,3 +59,8 @@ nuget CommunityToolkit.HighPerformance nuget System.Security.Cryptography.Pkcs nuget System.Net.Http nuget System.Text.RegularExpressions + + +## Test Explorer +nuget Microsoft.TestPlatform.TranslationLayer +nuget Microsoft.TestPlatform.ObjectModel \ No newline at end of file diff --git a/paket.lock b/paket.lock index 02b55f5fc..5fd5f6626 100644 --- a/paket.lock +++ b/paket.lock @@ -400,6 +400,8 @@ NUGET Microsoft.TestPlatform.TestHost (17.12) - restriction: || (== net8.0) (== net9.0) (&& (== netstandard2.0) (>= netcoreapp3.1)) (&& (== netstandard2.1) (>= netcoreapp3.1)) Microsoft.TestPlatform.ObjectModel (>= 17.12) - restriction: || (== net8.0) (== net9.0) (&& (== netstandard2.0) (>= netcoreapp3.1)) (&& (== netstandard2.1) (>= netcoreapp3.1)) Newtonsoft.Json (>= 13.0.1) - restriction: || (== net8.0) (== net9.0) (&& (== netstandard2.0) (>= netcoreapp3.1)) (&& (== netstandard2.1) (>= netcoreapp3.1)) + Microsoft.TestPlatform.TranslationLayer (17.13) + NETStandard.Library (>= 2.0) Microsoft.VisualStudio.SolutionPersistence (1.0.28) - restriction: || (== net8.0) (== net9.0) (&& (== netstandard2.0) (>= net8.0)) (&& (== netstandard2.1) (>= net8.0)) Microsoft.VisualStudio.Threading (17.12.19) Microsoft.Bcl.AsyncInterfaces (>= 6.0) - restriction: || (&& (== net8.0) (>= net472)) (&& (== net8.0) (< net6.0)) (&& (== net9.0) (>= net472)) (&& (== net9.0) (< net6.0)) (== netstandard2.0) (== netstandard2.1) @@ -423,6 +425,8 @@ NUGET Microsoft.VisualStudio.Validation (>= 17.8.8) System.IO.Pipelines (>= 8.0) System.Runtime.CompilerServices.Unsafe (>= 6.0) - restriction: || (&& (== net8.0) (< net6.0)) (&& (== net8.0) (< netstandard2.1)) (&& (== net9.0) (< net6.0)) (&& (== net9.0) (< netstandard2.1)) (== netstandard2.0) (== netstandard2.1) + NETStandard.Library (2.0.3) + Microsoft.NETCore.Platforms (>= 1.1) Newtonsoft.Json (13.0.3) Nuget.Frameworks (6.12.1) - copy_local: false OpenTelemetry (1.10) diff --git a/src/FsAutoComplete.Core/VSTestAdapter.fs b/src/FsAutoComplete.Core/VSTestAdapter.fs index c8dca6d50..f17274e95 100644 --- a/src/FsAutoComplete.Core/VSTestAdapter.fs +++ b/src/FsAutoComplete.Core/VSTestAdapter.fs @@ -1,7 +1,43 @@ namespace FsAutoComplete.VSTestAdapter +open Microsoft.TestPlatform.VsTestConsole.TranslationLayer; +open Microsoft.VisualStudio.TestPlatform.ObjectModel; +open Microsoft.VisualStudio.TestPlatform.ObjectModel.Client; +open Microsoft.VisualStudio.TestPlatform.ObjectModel.Client.Interfaces; +open Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging; module VSTestWrapper = - open FsAutoComplete.Utils - let discoverTests (_: ProjectFilePath list) = [] \ No newline at end of file + type TestProjectDll = string + + type private TestDiscoveryHandler () = + + member val DiscoveredTests : TestCase ResizeArray = ResizeArray() with get,set + + interface ITestDiscoveryEventsHandler with + member this.HandleDiscoveredTests (discoveredTestCases: System.Collections.Generic.IEnumerable): unit = + if (not << isNull) discoveredTestCases then + this.DiscoveredTests.AddRange(discoveredTestCases) + + member this.HandleDiscoveryComplete (_totalTests: int64, lastChunk: System.Collections.Generic.IEnumerable, _isAborted: bool): unit = + if (not << isNull) lastChunk then + this.DiscoveredTests.AddRange(lastChunk) + + member this.HandleLogMessage (_level: TestMessageLevel, _message: string): unit = + () + + member this.HandleRawMessage (_rawMessage: string): unit = + () + + open System.Linq + let discoverTests (vstestPath: string) (sources: TestProjectDll list) : TestCase list = + let consoleParams = ConsoleParameters() + + let vstest = new VsTestConsoleWrapper(vstestPath, consoleParams) + let discoveryHandler = TestDiscoveryHandler() + + vstest.DiscoverTests(sources, null, discoveryHandler) + discoveryHandler.DiscoveredTests |> List.ofSeq + + + diff --git a/src/FsAutoComplete.Core/paket.references b/src/FsAutoComplete.Core/paket.references index 4c77fac04..deecc0716 100644 --- a/src/FsAutoComplete.Core/paket.references +++ b/src/FsAutoComplete.Core/paket.references @@ -17,3 +17,6 @@ FSharp.Analyzers.SDK Ionide.Analyzers FSharp.Analyzers.Build Serilog.Extensions.Logging + +Microsoft.TestPlatform.ObjectModel +Microsoft.TestPlatform.TranslationLayer \ No newline at end of file diff --git a/test/FsAutoComplete.Tests.TestExplorer/FsAutoComplete.Tests.TestExplorer.fsproj b/test/FsAutoComplete.Tests.TestExplorer/FsAutoComplete.Tests.TestExplorer.fsproj index 4451176a4..f1041fdb6 100644 --- a/test/FsAutoComplete.Tests.TestExplorer/FsAutoComplete.Tests.TestExplorer.fsproj +++ b/test/FsAutoComplete.Tests.TestExplorer/FsAutoComplete.Tests.TestExplorer.fsproj @@ -7,8 +7,9 @@ - + + diff --git a/test/FsAutoComplete.Tests.TestExplorer/Sample.fs b/test/FsAutoComplete.Tests.TestExplorer/Sample.fs deleted file mode 100644 index 1b0449451..000000000 --- a/test/FsAutoComplete.Tests.TestExplorer/Sample.fs +++ /dev/null @@ -1,13 +0,0 @@ -module Tests - -open Expecto -open FsAutoComplete.VSTestAdapter - -[] -let tests = - testList "Test Discovery" [ - testCase "No projects, no tests" <| fun _ -> - let expected = [] - let actual = VSTestWrapper.discoverTests [] - Expect.equal actual expected "" - ] diff --git a/test/FsAutoComplete.Tests.TestExplorer/SampleTestProjects/VSTest.XUnit.Tests/Program.fs b/test/FsAutoComplete.Tests.TestExplorer/SampleTestProjects/VSTest.XUnit.Tests/Program.fs new file mode 100644 index 000000000..31dc4f735 --- /dev/null +++ b/test/FsAutoComplete.Tests.TestExplorer/SampleTestProjects/VSTest.XUnit.Tests/Program.fs @@ -0,0 +1,4 @@ +module Program + +[] +let main _ = 0 diff --git a/test/FsAutoComplete.Tests.TestExplorer/SampleTestProjects/VSTest.XUnit.Tests/Tests.fs b/test/FsAutoComplete.Tests.TestExplorer/SampleTestProjects/VSTest.XUnit.Tests/Tests.fs new file mode 100644 index 000000000..7f70fae19 --- /dev/null +++ b/test/FsAutoComplete.Tests.TestExplorer/SampleTestProjects/VSTest.XUnit.Tests/Tests.fs @@ -0,0 +1,8 @@ +module Tests + +open System +open Xunit + +[] +let ``My test`` () = + Assert.True(true) diff --git a/test/FsAutoComplete.Tests.TestExplorer/SampleTestProjects/VSTest.XUnit.Tests/VSTest.XUnit.Tests.fsproj b/test/FsAutoComplete.Tests.TestExplorer/SampleTestProjects/VSTest.XUnit.Tests/VSTest.XUnit.Tests.fsproj new file mode 100644 index 000000000..1a4224efd --- /dev/null +++ b/test/FsAutoComplete.Tests.TestExplorer/SampleTestProjects/VSTest.XUnit.Tests/VSTest.XUnit.Tests.fsproj @@ -0,0 +1,22 @@ + + + + net8.0 + false + false + + + + + + + + + + + + + + + + diff --git a/test/FsAutoComplete.Tests.TestExplorer/SampleTestProjects/paket.dependencies b/test/FsAutoComplete.Tests.TestExplorer/SampleTestProjects/paket.dependencies new file mode 100644 index 000000000..e69de29bb diff --git a/test/FsAutoComplete.Tests.TestExplorer/SampleTestProjects/paket.lock b/test/FsAutoComplete.Tests.TestExplorer/SampleTestProjects/paket.lock new file mode 100644 index 000000000..d3f5a12fa --- /dev/null +++ b/test/FsAutoComplete.Tests.TestExplorer/SampleTestProjects/paket.lock @@ -0,0 +1 @@ + diff --git a/test/FsAutoComplete.Tests.TestExplorer/TestDiscovery.fs b/test/FsAutoComplete.Tests.TestExplorer/TestDiscovery.fs new file mode 100644 index 000000000..d97356261 --- /dev/null +++ b/test/FsAutoComplete.Tests.TestExplorer/TestDiscovery.fs @@ -0,0 +1,49 @@ +module Tests + +open Expecto +open FsAutoComplete.VSTestAdapter +open Microsoft.VisualStudio.TestPlatform.ObjectModel; +open System.IO + + +let tryFindVsTest () : string = + let dotnetBinary = + Ionide.ProjInfo.Paths.dotnetRoot.Value + |> Option.defaultWith (fun () -> failwith "Couldn't find dotnet root. The dotnet sdk must be installed to run these tests") + + let cwd = System.Environment.CurrentDirectory |> DirectoryInfo + + match Ionide.ProjInfo.SdkDiscovery.versionAt cwd dotnetBinary with + | Ok sdkVersion -> + let sdks = Ionide.ProjInfo.SdkDiscovery.sdks dotnetBinary + + match sdks |> Array.tryFind (fun sdk -> sdk.Version = sdkVersion) with + | Some sdk -> + Path.Combine(sdk.Path.FullName, "vstest.console.dll") + | None -> failwith $"Couldn't install location for dotnet sdk {sdkVersion}" + | Error _ -> failwith $"Couldn't identify the dotnet version for directory {cwd.FullName}" + + +let vstestPath = tryFindVsTest () + +[] +let tests = + testList "VSTestWrapper Test Discovery" [ + testCase "should return an empty list if given no projects" <| fun () -> + let expected = [] + let actual = VSTestWrapper.discoverTests vstestPath [] + Expect.equal actual expected "" + + testCase "should discover tests given a single xunit project" <| fun () -> + let expectedTestIds = ["Tests.My test"] + + let sourceDir = __SOURCE_DIRECTORY__ + let sources = [ + Path.Combine(sourceDir, "SampleTestProjects/VSTest.XUnit.Tests/bin/Debug/net8.0/VSTest.XUnit.Tests.dll") + ] + + let discovered = VSTestWrapper.discoverTests vstestPath sources + let actual = discovered |> List.map _.FullyQualifiedName + + Expect.equal actual expectedTestIds "" + ] diff --git a/test/FsAutoComplete.Tests.TestExplorer/paket.references b/test/FsAutoComplete.Tests.TestExplorer/paket.references index 1a4000a66..dfc4a00cb 100644 --- a/test/FsAutoComplete.Tests.TestExplorer/paket.references +++ b/test/FsAutoComplete.Tests.TestExplorer/paket.references @@ -1,3 +1,4 @@ FSharp.Core content: once Microsoft.NET.Test.Sdk -YoloDev.Expecto.TestSdk \ No newline at end of file +YoloDev.Expecto.TestSdk +Microsoft.Bcl.AsyncInterfaces \ No newline at end of file From 6b6c243d77839a7cf3561cfd220c2c3cf4775fb9 Mon Sep 17 00:00:00 2001 From: farlee2121 <2847259+farlee2121@users.noreply.github.com> Date: Tue, 22 Jul 2025 12:23:22 -0500 Subject: [PATCH 06/39] Demonstrate connection between ionide and language server --- src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs | 7 +++++++ src/FsAutoComplete/LspServers/IFSharpLspServer.fs | 1 + 2 files changed, 8 insertions(+) diff --git a/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs b/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs index 272dc44f4..4b1829a20 100644 --- a/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs +++ b/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs @@ -3056,6 +3056,11 @@ type AdaptiveFSharpLspServer return! returnException e logCfg } + override this.TestDiscoverTests (): Async> = + asyncResult { + return Some { Content = "hello testing"} + } + override x.Dispose() = disposables.Dispose() member this.WindowWorkDoneProgressCancel(param: WorkDoneProgressCancelParams) : Async = @@ -3096,6 +3101,7 @@ type AdaptiveFSharpLspServer member this.Progress(_arg1: ProgressParams) : Async = ignoreNotification member this.SetTrace(_arg1: SetTraceParams) : Async = ignoreNotification + module AdaptiveFSharpLspServer = open StreamJsonRpc @@ -3185,6 +3191,7 @@ module AdaptiveFSharpLspServer = |> Map.add "fsproj/addExistingFile" (serverRequestHandling (fun s p -> s.FsProjAddExistingFile(p))) |> Map.add "fsproj/renameFile" (serverRequestHandling (fun s p -> s.FsProjRenameFile(p))) |> Map.add "fsproj/removeFile" (serverRequestHandling (fun s p -> s.FsProjRemoveFile(p))) + |> Map.add "test/discoverTests" (serverRequestHandling (fun s p -> s.TestDiscoverTests() )) let adaptiveServer lspClient = let loader = workspaceLoaderFactory toolsPath diff --git a/src/FsAutoComplete/LspServers/IFSharpLspServer.fs b/src/FsAutoComplete/LspServers/IFSharpLspServer.fs index 90309b7ed..d48055a1f 100644 --- a/src/FsAutoComplete/LspServers/IFSharpLspServer.fs +++ b/src/FsAutoComplete/LspServers/IFSharpLspServer.fs @@ -47,3 +47,4 @@ type IFSharpLspServer = abstract FsProjAddFile: DotnetFileRequest -> Async> abstract FsProjRemoveFile: DotnetFileRequest -> Async> abstract FsProjAddExistingFile: DotnetFileRequest -> Async> + abstract TestDiscoverTests: unit -> Async> From 0210fd9f7bac95e935c669418399091beec3b8a1 Mon Sep 17 00:00:00 2001 From: farlee2121 <2847259+farlee2121@users.noreply.github.com> Date: Fri, 25 Jul 2025 15:53:27 -0500 Subject: [PATCH 07/39] Implement and mvp test discovery endpoint on the Language Server --- src/FsAutoComplete.Core/VSTestAdapter.fs | 27 +++++++++++ src/FsAutoComplete/CommandResponse.fs | 22 +++++++++ src/FsAutoComplete/CommandResponse.fsi | 21 ++++++++ src/FsAutoComplete/LspHelpers.fsi | 1 - .../LspServers/AdaptiveFSharpLspServer.fs | 48 ++++++++++++++++++- .../TestDiscovery.fs | 14 ++---- 6 files changed, 121 insertions(+), 12 deletions(-) diff --git a/src/FsAutoComplete.Core/VSTestAdapter.fs b/src/FsAutoComplete.Core/VSTestAdapter.fs index f17274e95..3ac93d30a 100644 --- a/src/FsAutoComplete.Core/VSTestAdapter.fs +++ b/src/FsAutoComplete.Core/VSTestAdapter.fs @@ -38,6 +38,33 @@ module VSTestWrapper = vstest.DiscoverTests(sources, null, discoveryHandler) discoveryHandler.DiscoveredTests |> List.ofSeq + + open System.IO + open FsToolkit.ErrorHandling + let tryFindVsTestFromDotnetRoot (dotnetRoot: string) (workspaceRoot: string option) : Result = + let cwd = defaultArg workspaceRoot System.Environment.CurrentDirectory |> DirectoryInfo + let dotnetBinary = + if dotnetRoot |> Directory.Exists then + if System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Windows) + then + FileInfo(Path.Combine(dotnetRoot, "dotnet.exe")) + else + FileInfo(Path.Combine(dotnetRoot, "dotnet")) + else dotnetRoot |> FileInfo + + match Ionide.ProjInfo.SdkDiscovery.versionAt cwd dotnetBinary with + | Ok sdkVersion -> + let sdks = Ionide.ProjInfo.SdkDiscovery.sdks dotnetBinary + + match sdks |> Array.tryFind (fun sdk -> sdk.Version = sdkVersion) with + | Some sdk -> + let vstestBinary = Path.Combine(sdk.Path.FullName, "vstest.console.dll") |> FileInfo + if vstestBinary.Exists + then Ok vstestBinary + else Error $"Found the correct dotnet sdk, but vstest was not at the expected sub-path: {vstestBinary.FullName}" + | None -> Error $"Couldn't find the install location for dotnet sdk version: {sdkVersion}" + | Error _ -> Error $"Couldn't identify the dotnet version for working directory: {cwd.FullName}" + diff --git a/src/FsAutoComplete/CommandResponse.fs b/src/FsAutoComplete/CommandResponse.fs index bfc37efe7..12c7fba7c 100644 --- a/src/FsAutoComplete/CommandResponse.fs +++ b/src/FsAutoComplete/CommandResponse.fs @@ -702,3 +702,25 @@ module CommandResponse = PrecedingNonPipeExprLine = pnp }) serialize { Kind = "pipelineHint"; Data = ctn } + + type TestFileRange = { + StartLine: int + EndLine: int + } + type TestItem = { + FullName : string + DisplayName : string + /// Identifies the test adapter that ran the tests + /// Example: executor://xunit/VsTestRunner2/netcoreapp + /// Used for determining the test library, which effects how tests names are broken down + ExecutorUri : string + ProjectFilePath : string + TargetFramework : string + CodeFilePath : string option + CodeLocationRange : TestFileRange option + } + + type DiscoverTestsResponse = TestItem list + + let discoverTests (serialize: Serializer) (content: DiscoverTestsResponse) = + serialize { Kind = "discoverTests"; Data = content } \ No newline at end of file diff --git a/src/FsAutoComplete/CommandResponse.fsi b/src/FsAutoComplete/CommandResponse.fsi index b68c16035..9846203d2 100644 --- a/src/FsAutoComplete/CommandResponse.fsi +++ b/src/FsAutoComplete/CommandResponse.fsi @@ -261,3 +261,24 @@ module CommandResponse = val compile: serialize: Serializer -> errors: #FSharpDiagnostic array * code: int -> string val fsharpLiterate: serialize: Serializer -> content: string -> string val pipelineHint: serialize: Serializer -> content: (int * int option * string list)[] -> string + + type TestFileRange = { + StartLine: int + EndLine: int + } + type TestItem = { + FullName : string + DisplayName : string + /// Identifies the test adapter that ran the tests + /// Example: executor://xunit/VsTestRunner2/netcoreapp + /// Used for determining the test library, which effects how tests names are broken down + ExecutorUri : string + ProjectFilePath : string + TargetFramework : string + CodeFilePath : string option + CodeLocationRange : TestFileRange option + } + + type DiscoverTestsResponse = TestItem list + + val discoverTests: serialize: Serializer -> content: DiscoverTestsResponse -> string \ No newline at end of file diff --git a/src/FsAutoComplete/LspHelpers.fsi b/src/FsAutoComplete/LspHelpers.fsi index 0ba878d42..6b22cbb23 100644 --- a/src/FsAutoComplete/LspHelpers.fsi +++ b/src/FsAutoComplete/LspHelpers.fsi @@ -465,7 +465,6 @@ type FSharpInlayHintsRequest = { TextDocument: TextDocumentIdentifier Range: Range } - [] module Extensions = diff --git a/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs b/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs index 4b1829a20..96b54cc70 100644 --- a/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs +++ b/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs @@ -3057,8 +3057,54 @@ type AdaptiveFSharpLspServer } override this.TestDiscoverTests (): Async> = + let tryGetWorkspaceProjects (workspace: WorkspaceChosen) = + match workspace with + | WorkspaceChosen.NotChosen -> Error "No workspace loaded. Can't discover tests" + | WorkspaceChosen.Projs projectPaths -> + projectPaths |> List.ofSeq |> List.map string |> workspaceLoader.LoadProjects |> Ok + + let isTestProject (project: Types.ProjectOptions) = + let testProjectIndicators = + set [ "Microsoft.TestPlatform.TestHost"; "Microsoft.NET.Test.Sdk" ] + + project.PackageReferences + |> List.exists (fun pr -> Set.contains pr.Name testProjectIndicators) + asyncResult { - return Some { Content = "hello testing"} + let! vstestBinary = VSTestAdapter.VSTestWrapper.tryFindVsTestFromDotnetRoot state.Config.DotNetRoot state.RootPath |> Result.mapError (fun msg -> Error.InternalError msg) + + let! projects = tryGetWorkspaceProjects state.WorkspacePaths |> Result.mapError (fun msg -> Error.InternalError msg) + let testProjects = projects |> List.ofSeq |> List.filter isTestProject + + let testProjectBinaries = testProjects |> List.map _.TargetPath + + let projectLookup = + projects |> Seq.map (fun p -> p.TargetPath, p) |> Map.ofSeq + + + + let testCases = + VSTestAdapter.VSTestWrapper.discoverTests vstestBinary.FullName testProjectBinaries + + let testDTOs : CommandResponse.TestItem list = + testCases |> List.choose (fun testCase -> + + match projectLookup |> Map.tryFind testCase.Source with + | None -> None // this should never happen. We pass VsTest the list of executables, so all the possible sources should be known to us + | Some project -> + Some { + FullName = testCase.FullyQualifiedName + DisplayName = testCase.DisplayName + // + ExecutorUri = testCase.ExecutorUri |> string + ProjectFilePath = project.ProjectFileName + TargetFramework = project.TargetFramework + CodeFilePath = Some testCase.CodeFilePath + CodeLocationRange = Some { StartLine = testCase.LineNumber; EndLine = testCase.LineNumber } + } + ) + + return Some { Content = CommandResponse.discoverTests FsAutoComplete.JsonSerializer.writeJson testDTOs} } override x.Dispose() = disposables.Dispose() diff --git a/test/FsAutoComplete.Tests.TestExplorer/TestDiscovery.fs b/test/FsAutoComplete.Tests.TestExplorer/TestDiscovery.fs index d97356261..6e8c10c11 100644 --- a/test/FsAutoComplete.Tests.TestExplorer/TestDiscovery.fs +++ b/test/FsAutoComplete.Tests.TestExplorer/TestDiscovery.fs @@ -11,17 +11,11 @@ let tryFindVsTest () : string = Ionide.ProjInfo.Paths.dotnetRoot.Value |> Option.defaultWith (fun () -> failwith "Couldn't find dotnet root. The dotnet sdk must be installed to run these tests") - let cwd = System.Environment.CurrentDirectory |> DirectoryInfo + let cwd = System.Environment.CurrentDirectory |> Some - match Ionide.ProjInfo.SdkDiscovery.versionAt cwd dotnetBinary with - | Ok sdkVersion -> - let sdks = Ionide.ProjInfo.SdkDiscovery.sdks dotnetBinary - - match sdks |> Array.tryFind (fun sdk -> sdk.Version = sdkVersion) with - | Some sdk -> - Path.Combine(sdk.Path.FullName, "vstest.console.dll") - | None -> failwith $"Couldn't install location for dotnet sdk {sdkVersion}" - | Error _ -> failwith $"Couldn't identify the dotnet version for directory {cwd.FullName}" + VSTestWrapper.tryFindVsTestFromDotnetRoot dotnetBinary.FullName cwd + |> Result.defaultWith failwith + |> _.FullName let vstestPath = tryFindVsTest () From 3712a46887aeaefb48bc219cbc3daad59f479e9e Mon Sep 17 00:00:00 2001 From: farlee2121 <2847259+farlee2121@users.noreply.github.com> Date: Sun, 27 Jul 2025 19:41:56 -0500 Subject: [PATCH 08/39] Push test discovery down to AdaptiveState This is preparation for adding notifications for incremental discovery reporting --- .../LspServers/AdaptiveFSharpLspServer.fs | 46 +----------------- .../LspServers/AdaptiveServerState.fs | 47 +++++++++++++++++++ .../LspServers/AdaptiveServerState.fsi | 1 + 3 files changed, 49 insertions(+), 45 deletions(-) diff --git a/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs b/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs index 96b54cc70..9c3c979fd 100644 --- a/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs +++ b/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs @@ -3057,52 +3057,8 @@ type AdaptiveFSharpLspServer } override this.TestDiscoverTests (): Async> = - let tryGetWorkspaceProjects (workspace: WorkspaceChosen) = - match workspace with - | WorkspaceChosen.NotChosen -> Error "No workspace loaded. Can't discover tests" - | WorkspaceChosen.Projs projectPaths -> - projectPaths |> List.ofSeq |> List.map string |> workspaceLoader.LoadProjects |> Ok - - let isTestProject (project: Types.ProjectOptions) = - let testProjectIndicators = - set [ "Microsoft.TestPlatform.TestHost"; "Microsoft.NET.Test.Sdk" ] - - project.PackageReferences - |> List.exists (fun pr -> Set.contains pr.Name testProjectIndicators) - asyncResult { - let! vstestBinary = VSTestAdapter.VSTestWrapper.tryFindVsTestFromDotnetRoot state.Config.DotNetRoot state.RootPath |> Result.mapError (fun msg -> Error.InternalError msg) - - let! projects = tryGetWorkspaceProjects state.WorkspacePaths |> Result.mapError (fun msg -> Error.InternalError msg) - let testProjects = projects |> List.ofSeq |> List.filter isTestProject - - let testProjectBinaries = testProjects |> List.map _.TargetPath - - let projectLookup = - projects |> Seq.map (fun p -> p.TargetPath, p) |> Map.ofSeq - - - - let testCases = - VSTestAdapter.VSTestWrapper.discoverTests vstestBinary.FullName testProjectBinaries - - let testDTOs : CommandResponse.TestItem list = - testCases |> List.choose (fun testCase -> - - match projectLookup |> Map.tryFind testCase.Source with - | None -> None // this should never happen. We pass VsTest the list of executables, so all the possible sources should be known to us - | Some project -> - Some { - FullName = testCase.FullyQualifiedName - DisplayName = testCase.DisplayName - // - ExecutorUri = testCase.ExecutorUri |> string - ProjectFilePath = project.ProjectFileName - TargetFramework = project.TargetFramework - CodeFilePath = Some testCase.CodeFilePath - CodeLocationRange = Some { StartLine = testCase.LineNumber; EndLine = testCase.LineNumber } - } - ) + let! testDTOs = state.DiscoverTests() |> AsyncResult.mapError (fun msg -> JsonRpc.Error.InternalError msg) return Some { Content = CommandResponse.discoverTests FsAutoComplete.JsonSerializer.writeJson testDTOs} } diff --git a/src/FsAutoComplete/LspServers/AdaptiveServerState.fs b/src/FsAutoComplete/LspServers/AdaptiveServerState.fs index 0976b38f3..536174e5c 100644 --- a/src/FsAutoComplete/LspServers/AdaptiveServerState.fs +++ b/src/FsAutoComplete/LspServers/AdaptiveServerState.fs @@ -2567,6 +2567,53 @@ type AdaptiveState member x.GlyphToSymbolKind = glyphToSymbolKind |> AVal.force + member state.DiscoverTests () = + let tryGetWorkspaceProjects (workspace: WorkspaceChosen) = + match workspace with + | WorkspaceChosen.NotChosen -> Error "No workspace loaded. Can't discover tests" + | WorkspaceChosen.Projs projectPaths -> + projectPaths |> List.ofSeq |> List.map string |> workspaceLoader.LoadProjects |> Ok + + let isTestProject (project: Types.ProjectOptions) = + let testProjectIndicators = + set [ "Microsoft.TestPlatform.TestHost"; "Microsoft.NET.Test.Sdk" ] + + project.PackageReferences + |> List.exists (fun pr -> Set.contains pr.Name testProjectIndicators) + + asyncResult { + let! vstestBinary = VSTestAdapter.VSTestWrapper.tryFindVsTestFromDotnetRoot state.Config.DotNetRoot state.RootPath + + let! projects = tryGetWorkspaceProjects state.WorkspacePaths + let testProjects = projects |> List.ofSeq |> List.filter isTestProject + + let testProjectBinaries = testProjects |> List.map _.TargetPath + + let projectLookup = + projects |> Seq.map (fun p -> p.TargetPath, p) |> Map.ofSeq + + let testCases = + VSTestAdapter.VSTestWrapper.discoverTests vstestBinary.FullName testProjectBinaries + + let testDTOs : CommandResponse.TestItem list = + testCases |> List.choose (fun testCase -> + match projectLookup |> Map.tryFind testCase.Source with + | None -> None // this should never happen. We pass VsTest the list of executables to test, so all the possible sources should be known to us + | Some project -> + Some { + FullName = testCase.FullyQualifiedName + DisplayName = testCase.DisplayName + ExecutorUri = testCase.ExecutorUri |> string + ProjectFilePath = project.ProjectFileName + TargetFramework = project.TargetFramework + CodeFilePath = Some testCase.CodeFilePath + CodeLocationRange = Some { StartLine = testCase.LineNumber; EndLine = testCase.LineNumber } + } + ) + + return testDTOs + } + member x.CancelServerProgress(progressToken: ProgressToken) = progressLookup.Cancel progressToken diff --git a/src/FsAutoComplete/LspServers/AdaptiveServerState.fsi b/src/FsAutoComplete/LspServers/AdaptiveServerState.fsi index 9ee4f4a73..5b4f00940 100644 --- a/src/FsAutoComplete/LspServers/AdaptiveServerState.fsi +++ b/src/FsAutoComplete/LspServers/AdaptiveServerState.fsi @@ -119,6 +119,7 @@ type AdaptiveState = member GetDeclarations: filename: string -> Async> member GetAllDeclarations: unit -> Async<(string * NavigationTopLevelDeclaration array) array> member GlyphToSymbolKind: (FSharpGlyph -> SymbolKind option) + member DiscoverTests: unit -> Async> /// /// Signals the server to cancel an operation that is associated with the given progress token. /// From f73c2bbb33c03bfa710a3b67b79b99e558224793 Mon Sep 17 00:00:00 2001 From: farlee2121 <2847259+farlee2121@users.noreply.github.com> Date: Mon, 28 Jul 2025 17:04:14 -0500 Subject: [PATCH 09/39] Publish incremental test discovery updates --- src/FsAutoComplete.Core/VSTestAdapter.fs | 29 ++++++++++-- src/FsAutoComplete/CommandResponse.fs | 19 +------- src/FsAutoComplete/CommandResponse.fsi | 19 +------- src/FsAutoComplete/LspHelpers.fs | 3 ++ src/FsAutoComplete/LspHelpers.fsi | 3 ++ .../LspServers/AdaptiveFSharpLspServer.fs | 2 +- .../LspServers/AdaptiveServerState.fs | 46 +++++++++++-------- .../LspServers/AdaptiveServerState.fsi | 2 +- .../LspServers/FSharpLspClient.fs | 3 ++ .../LspServers/FSharpLspClient.fsi | 1 + .../TestDiscovery.fs | 4 +- 11 files changed, 68 insertions(+), 63 deletions(-) diff --git a/src/FsAutoComplete.Core/VSTestAdapter.fs b/src/FsAutoComplete.Core/VSTestAdapter.fs index 3ac93d30a..bec25fbac 100644 --- a/src/FsAutoComplete.Core/VSTestAdapter.fs +++ b/src/FsAutoComplete.Core/VSTestAdapter.fs @@ -10,18 +10,20 @@ module VSTestWrapper = type TestProjectDll = string - type private TestDiscoveryHandler () = + type private TestDiscoveryHandler (notifyIncrementalUpdate: TestCase list -> unit) = member val DiscoveredTests : TestCase ResizeArray = ResizeArray() with get,set interface ITestDiscoveryEventsHandler with member this.HandleDiscoveredTests (discoveredTestCases: System.Collections.Generic.IEnumerable): unit = if (not << isNull) discoveredTestCases then - this.DiscoveredTests.AddRange(discoveredTestCases) + this.DiscoveredTests.AddRange(discoveredTestCases) + notifyIncrementalUpdate (discoveredTestCases |> List.ofSeq) member this.HandleDiscoveryComplete (_totalTests: int64, lastChunk: System.Collections.Generic.IEnumerable, _isAborted: bool): unit = if (not << isNull) lastChunk then this.DiscoveredTests.AddRange(lastChunk) + notifyIncrementalUpdate (lastChunk |> List.ofSeq) member this.HandleLogMessage (_level: TestMessageLevel, _message: string): unit = () @@ -30,11 +32,11 @@ module VSTestWrapper = () open System.Linq - let discoverTests (vstestPath: string) (sources: TestProjectDll list) : TestCase list = + let discoverTests (vstestPath: string) (incrementalUpdateHandler: TestCase list -> unit) (sources: TestProjectDll list) : TestCase list = let consoleParams = ConsoleParameters() let vstest = new VsTestConsoleWrapper(vstestPath, consoleParams) - let discoveryHandler = TestDiscoveryHandler() + let discoveryHandler = TestDiscoveryHandler(incrementalUpdateHandler) vstest.DiscoverTests(sources, null, discoveryHandler) discoveryHandler.DiscoveredTests |> List.ofSeq @@ -65,6 +67,23 @@ module VSTestWrapper = | None -> Error $"Couldn't find the install location for dotnet sdk version: {sdkVersion}" | Error _ -> Error $"Couldn't identify the dotnet version for working directory: {cwd.FullName}" - + +type TestFileRange = { + StartLine: int + EndLine: int + } +type TestItem = { + FullName : string + DisplayName : string + /// Identifies the test adapter that ran the tests + /// Example: executor://xunit/VsTestRunner2/netcoreapp + /// Used for determining the test library, which effects how tests names are broken down + ExecutorUri : string + ProjectFilePath : string + TargetFramework : string + CodeFilePath : string option + CodeLocationRange : TestFileRange option +} + diff --git a/src/FsAutoComplete/CommandResponse.fs b/src/FsAutoComplete/CommandResponse.fs index 12c7fba7c..5fa660c11 100644 --- a/src/FsAutoComplete/CommandResponse.fs +++ b/src/FsAutoComplete/CommandResponse.fs @@ -703,24 +703,9 @@ module CommandResponse = serialize { Kind = "pipelineHint"; Data = ctn } - type TestFileRange = { - StartLine: int - EndLine: int - } - type TestItem = { - FullName : string - DisplayName : string - /// Identifies the test adapter that ran the tests - /// Example: executor://xunit/VsTestRunner2/netcoreapp - /// Used for determining the test library, which effects how tests names are broken down - ExecutorUri : string - ProjectFilePath : string - TargetFramework : string - CodeFilePath : string option - CodeLocationRange : TestFileRange option - } + - type DiscoverTestsResponse = TestItem list + type DiscoverTestsResponse = VSTestAdapter.TestItem list let discoverTests (serialize: Serializer) (content: DiscoverTestsResponse) = serialize { Kind = "discoverTests"; Data = content } \ No newline at end of file diff --git a/src/FsAutoComplete/CommandResponse.fsi b/src/FsAutoComplete/CommandResponse.fsi index 9846203d2..8b14e938f 100644 --- a/src/FsAutoComplete/CommandResponse.fsi +++ b/src/FsAutoComplete/CommandResponse.fsi @@ -262,23 +262,6 @@ module CommandResponse = val fsharpLiterate: serialize: Serializer -> content: string -> string val pipelineHint: serialize: Serializer -> content: (int * int option * string list)[] -> string - type TestFileRange = { - StartLine: int - EndLine: int - } - type TestItem = { - FullName : string - DisplayName : string - /// Identifies the test adapter that ran the tests - /// Example: executor://xunit/VsTestRunner2/netcoreapp - /// Used for determining the test library, which effects how tests names are broken down - ExecutorUri : string - ProjectFilePath : string - TargetFramework : string - CodeFilePath : string option - CodeLocationRange : TestFileRange option - } - - type DiscoverTestsResponse = TestItem list + type DiscoverTestsResponse = VSTestAdapter.TestItem list val discoverTests: serialize: Serializer -> content: DiscoverTestsResponse -> string \ No newline at end of file diff --git a/src/FsAutoComplete/LspHelpers.fs b/src/FsAutoComplete/LspHelpers.fs index 5baee17b4..ead51ae11 100644 --- a/src/FsAutoComplete/LspHelpers.fs +++ b/src/FsAutoComplete/LspHelpers.fs @@ -612,6 +612,9 @@ type TestDetectedNotification = { File: string Tests: TestAdapter.TestAdapterEntry array } +type TestDiscoveryUpdateNotification = + { Tests: VSTestAdapter.TestItem array } + type ProjectParms = { /// Project file to compile diff --git a/src/FsAutoComplete/LspHelpers.fsi b/src/FsAutoComplete/LspHelpers.fsi index 6b22cbb23..d24ca9481 100644 --- a/src/FsAutoComplete/LspHelpers.fsi +++ b/src/FsAutoComplete/LspHelpers.fsi @@ -198,6 +198,9 @@ type TestDetectedNotification = { File: string Tests: TestAdapter.TestAdapterEntry array } +type TestDiscoveryUpdateNotification = + { Tests: VSTestAdapter.TestItem array } + type ProjectParms = { /// Project file to compile diff --git a/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs b/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs index 9c3c979fd..3950d3d75 100644 --- a/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs +++ b/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs @@ -3193,7 +3193,7 @@ module AdaptiveFSharpLspServer = |> Map.add "fsproj/addExistingFile" (serverRequestHandling (fun s p -> s.FsProjAddExistingFile(p))) |> Map.add "fsproj/renameFile" (serverRequestHandling (fun s p -> s.FsProjRenameFile(p))) |> Map.add "fsproj/removeFile" (serverRequestHandling (fun s p -> s.FsProjRemoveFile(p))) - |> Map.add "test/discoverTests" (serverRequestHandling (fun s p -> s.TestDiscoverTests() )) + |> Map.add "test/discoverTests" (serverRequestHandling (fun s _ -> s.TestDiscoverTests() )) let adaptiveServer lspClient = let loader = workspaceLoaderFactory toolsPath diff --git a/src/FsAutoComplete/LspServers/AdaptiveServerState.fs b/src/FsAutoComplete/LspServers/AdaptiveServerState.fs index 536174e5c..38825658e 100644 --- a/src/FsAutoComplete/LspServers/AdaptiveServerState.fs +++ b/src/FsAutoComplete/LspServers/AdaptiveServerState.fs @@ -801,6 +801,7 @@ type AdaptiveState { File = Path.LocalPathToUri file Tests = tests |> Array.map map } |> lspClient.NotifyTestDetected + with ex -> logger.error ( Log.setMessage "Exception while handling command event {evt}: {ex}" @@ -2580,6 +2581,20 @@ type AdaptiveState project.PackageReferences |> List.exists (fun pr -> Set.contains pr.Name testProjectIndicators) + + let tryTestCaseToDTO (projectLookup: Map) (testCase: Microsoft.VisualStudio.TestPlatform.ObjectModel.TestCase) : VSTestAdapter.TestItem option= + match projectLookup |> Map.tryFind testCase.Source with + | None -> None // this should never happen. We pass VsTest the list of executables to test, so all the possible sources should be known to us + | Some project -> + Some { + FullName = testCase.FullyQualifiedName + DisplayName = testCase.DisplayName + ExecutorUri = testCase.ExecutorUri |> string + ProjectFilePath = project.ProjectFileName + TargetFramework = project.TargetFramework + CodeFilePath = Some testCase.CodeFilePath + CodeLocationRange = Some { StartLine = testCase.LineNumber; EndLine = testCase.LineNumber } + } asyncResult { let! vstestBinary = VSTestAdapter.VSTestWrapper.tryFindVsTestFromDotnetRoot state.Config.DotNetRoot state.RootPath @@ -2589,27 +2604,20 @@ type AdaptiveState let testProjectBinaries = testProjects |> List.map _.TargetPath - let projectLookup = - projects |> Seq.map (fun p -> p.TargetPath, p) |> Map.ofSeq + let tryTestCasesToDTOs testCases = + let projectLookup = + projects |> Seq.map (fun p -> p.TargetPath, p) |> Map.ofSeq + testCases |> List.choose (tryTestCaseToDTO projectLookup) + + let incrementalUpdateHandler (tests: Microsoft.VisualStudio.TestPlatform.ObjectModel.TestCase list) = + lspClient.NotifyTestDiscoveryUpdate({ Tests = tests |> tryTestCasesToDTOs |> Array.ofList}) + |> Async.RunSynchronously let testCases = - VSTestAdapter.VSTestWrapper.discoverTests vstestBinary.FullName testProjectBinaries - - let testDTOs : CommandResponse.TestItem list = - testCases |> List.choose (fun testCase -> - match projectLookup |> Map.tryFind testCase.Source with - | None -> None // this should never happen. We pass VsTest the list of executables to test, so all the possible sources should be known to us - | Some project -> - Some { - FullName = testCase.FullyQualifiedName - DisplayName = testCase.DisplayName - ExecutorUri = testCase.ExecutorUri |> string - ProjectFilePath = project.ProjectFileName - TargetFramework = project.TargetFramework - CodeFilePath = Some testCase.CodeFilePath - CodeLocationRange = Some { StartLine = testCase.LineNumber; EndLine = testCase.LineNumber } - } - ) + VSTestAdapter.VSTestWrapper.discoverTests vstestBinary.FullName incrementalUpdateHandler testProjectBinaries + + let testDTOs : VSTestAdapter.TestItem list = + testCases |> tryTestCasesToDTOs return testDTOs } diff --git a/src/FsAutoComplete/LspServers/AdaptiveServerState.fsi b/src/FsAutoComplete/LspServers/AdaptiveServerState.fsi index 5b4f00940..097529b30 100644 --- a/src/FsAutoComplete/LspServers/AdaptiveServerState.fsi +++ b/src/FsAutoComplete/LspServers/AdaptiveServerState.fsi @@ -119,7 +119,7 @@ type AdaptiveState = member GetDeclarations: filename: string -> Async> member GetAllDeclarations: unit -> Async<(string * NavigationTopLevelDeclaration array) array> member GlyphToSymbolKind: (FSharpGlyph -> SymbolKind option) - member DiscoverTests: unit -> Async> + member DiscoverTests: unit -> Async> /// /// Signals the server to cancel an operation that is associated with the given progress token. /// diff --git a/src/FsAutoComplete/LspServers/FSharpLspClient.fs b/src/FsAutoComplete/LspServers/FSharpLspClient.fs index 532298ed8..9dc545511 100644 --- a/src/FsAutoComplete/LspServers/FSharpLspClient.fs +++ b/src/FsAutoComplete/LspServers/FSharpLspClient.fs @@ -62,6 +62,9 @@ type FSharpLspClient(sendServerNotification: ClientNotificationSender, sendServe member __.NotifyTestDetected(p: TestDetectedNotification) = sendServerNotification "fsharp/testDetected" (box p) |> Async.Ignore + member __.NotifyTestDiscoveryUpdate(p: TestDiscoveryUpdateNotification) = + sendServerNotification "test/testDiscoveryUpdate" (box { Content = JsonSerializer.writeJson p}) |> Async.Ignore + member x.CodeLensRefresh() = match x.ClientCapabilities with | Some { Workspace = Some { CodeLens = Some { RefreshSupport = Some true } } } -> diff --git a/src/FsAutoComplete/LspServers/FSharpLspClient.fsi b/src/FsAutoComplete/LspServers/FSharpLspClient.fsi index b41b37464..ecd9c29ba 100644 --- a/src/FsAutoComplete/LspServers/FSharpLspClient.fsi +++ b/src/FsAutoComplete/LspServers/FSharpLspClient.fsi @@ -32,6 +32,7 @@ type FSharpLspClient = member NotifyFileParsed: p: PlainNotification -> Async member NotifyDocumentAnalyzed: p: DocumentAnalyzedNotification -> Async member NotifyTestDetected: p: TestDetectedNotification -> Async + member NotifyTestDiscoveryUpdate: p: TestDiscoveryUpdateNotification -> Async member CodeLensRefresh: unit -> Async override WindowWorkDoneProgressCreate: WorkDoneProgressCreateParams -> AsyncLspResult member Progress: ProgressToken * 'Progress -> Async diff --git a/test/FsAutoComplete.Tests.TestExplorer/TestDiscovery.fs b/test/FsAutoComplete.Tests.TestExplorer/TestDiscovery.fs index 6e8c10c11..49b79710f 100644 --- a/test/FsAutoComplete.Tests.TestExplorer/TestDiscovery.fs +++ b/test/FsAutoComplete.Tests.TestExplorer/TestDiscovery.fs @@ -25,7 +25,7 @@ let tests = testList "VSTestWrapper Test Discovery" [ testCase "should return an empty list if given no projects" <| fun () -> let expected = [] - let actual = VSTestWrapper.discoverTests vstestPath [] + let actual = VSTestWrapper.discoverTests vstestPath ignore [] Expect.equal actual expected "" testCase "should discover tests given a single xunit project" <| fun () -> @@ -36,7 +36,7 @@ let tests = Path.Combine(sourceDir, "SampleTestProjects/VSTest.XUnit.Tests/bin/Debug/net8.0/VSTest.XUnit.Tests.dll") ] - let discovered = VSTestWrapper.discoverTests vstestPath sources + let discovered = VSTestWrapper.discoverTests vstestPath ignore sources let actual = discovered |> List.map _.FullyQualifiedName Expect.equal actual expectedTestIds "" From e054ef6b73a76fad27c7961b8ad9d2bef0ae0bf9 Mon Sep 17 00:00:00 2001 From: farlee2121 <2847259+farlee2121@users.noreply.github.com> Date: Tue, 29 Jul 2025 17:07:45 -0500 Subject: [PATCH 10/39] Demonstration a test run using VSTest --- src/FsAutoComplete.Core/VSTestAdapter.fs | 33 ++++++++++-- .../FsAutoComplete.Tests.TestExplorer.fsproj | 3 +- .../VSTest.XUnit.RunResults/Program.fs | 4 ++ .../VSTest.XUnit.RunResults/Tests.fs | 20 ++++++++ .../VSTest.XUnit.RunResults.fsproj | 22 ++++++++ ...TestDiscovery.fs => TestDiscoveryTests.fs} | 2 +- .../TestRunTests.fs | 50 +++++++++++++++++++ 7 files changed, 129 insertions(+), 5 deletions(-) create mode 100644 test/FsAutoComplete.Tests.TestExplorer/SampleTestProjects/VSTest.XUnit.RunResults/Program.fs create mode 100644 test/FsAutoComplete.Tests.TestExplorer/SampleTestProjects/VSTest.XUnit.RunResults/Tests.fs create mode 100644 test/FsAutoComplete.Tests.TestExplorer/SampleTestProjects/VSTest.XUnit.RunResults/VSTest.XUnit.RunResults.fsproj rename test/FsAutoComplete.Tests.TestExplorer/{TestDiscovery.fs => TestDiscoveryTests.fs} (98%) create mode 100644 test/FsAutoComplete.Tests.TestExplorer/TestRunTests.fs diff --git a/src/FsAutoComplete.Core/VSTestAdapter.fs b/src/FsAutoComplete.Core/VSTestAdapter.fs index bec25fbac..4cd7b0aae 100644 --- a/src/FsAutoComplete.Core/VSTestAdapter.fs +++ b/src/FsAutoComplete.Core/VSTestAdapter.fs @@ -31,18 +31,45 @@ module VSTestWrapper = member this.HandleRawMessage (_rawMessage: string): unit = () - open System.Linq let discoverTests (vstestPath: string) (incrementalUpdateHandler: TestCase list -> unit) (sources: TestProjectDll list) : TestCase list = let consoleParams = ConsoleParameters() - let vstest = new VsTestConsoleWrapper(vstestPath, consoleParams) let discoveryHandler = TestDiscoveryHandler(incrementalUpdateHandler) vstest.DiscoverTests(sources, null, discoveryHandler) discoveryHandler.DiscoveredTests |> List.ofSeq + + type TestRunHandler() = + member val TestResults : TestResult ResizeArray = ResizeArray() with get,set + + interface ITestRunEventsHandler with + member _.HandleLogMessage (_level: TestMessageLevel, _message: string): unit = + () + + member _.HandleRawMessage (_rawMessage: string): unit = + () + + member this.HandleTestRunComplete (_testRunCompleteArgs: TestRunCompleteEventArgs, lastChunkArgs: TestRunChangedEventArgs, _runContextAttachments: System.Collections.Generic.ICollection, _executorUris: System.Collections.Generic.ICollection): unit = + if((not << isNull) lastChunkArgs && (not << isNull) lastChunkArgs.NewTestResults) then + this.TestResults.AddRange(lastChunkArgs.NewTestResults) + + member this.HandleTestRunStatsChange (testRunChangedArgs: TestRunChangedEventArgs): unit = + if((not << isNull) testRunChangedArgs && (not << isNull) testRunChangedArgs.NewTestResults) then + this.TestResults.AddRange(testRunChangedArgs.NewTestResults) + + member _.LaunchProcessWithDebuggerAttached (_testProcessStartInfo: TestProcessStartInfo): int = + raise (System.NotImplementedException()) + + let runTests (vstestPath: string) (sources: TestProjectDll list) = + let consoleParams = ConsoleParameters() + let vstest = new VsTestConsoleWrapper(vstestPath, consoleParams) + let runHandler = TestRunHandler() + + vstest.RunTests(sources, null, runHandler) + runHandler.TestResults |> List.ofSeq + open System.IO - open FsToolkit.ErrorHandling let tryFindVsTestFromDotnetRoot (dotnetRoot: string) (workspaceRoot: string option) : Result = let cwd = defaultArg workspaceRoot System.Environment.CurrentDirectory |> DirectoryInfo let dotnetBinary = diff --git a/test/FsAutoComplete.Tests.TestExplorer/FsAutoComplete.Tests.TestExplorer.fsproj b/test/FsAutoComplete.Tests.TestExplorer/FsAutoComplete.Tests.TestExplorer.fsproj index f1041fdb6..100441ab8 100644 --- a/test/FsAutoComplete.Tests.TestExplorer/FsAutoComplete.Tests.TestExplorer.fsproj +++ b/test/FsAutoComplete.Tests.TestExplorer/FsAutoComplete.Tests.TestExplorer.fsproj @@ -7,7 +7,8 @@ - + + diff --git a/test/FsAutoComplete.Tests.TestExplorer/SampleTestProjects/VSTest.XUnit.RunResults/Program.fs b/test/FsAutoComplete.Tests.TestExplorer/SampleTestProjects/VSTest.XUnit.RunResults/Program.fs new file mode 100644 index 000000000..31dc4f735 --- /dev/null +++ b/test/FsAutoComplete.Tests.TestExplorer/SampleTestProjects/VSTest.XUnit.RunResults/Program.fs @@ -0,0 +1,4 @@ +module Program + +[] +let main _ = 0 diff --git a/test/FsAutoComplete.Tests.TestExplorer/SampleTestProjects/VSTest.XUnit.RunResults/Tests.fs b/test/FsAutoComplete.Tests.TestExplorer/SampleTestProjects/VSTest.XUnit.RunResults/Tests.fs new file mode 100644 index 000000000..198cd03ac --- /dev/null +++ b/test/FsAutoComplete.Tests.TestExplorer/SampleTestProjects/VSTest.XUnit.RunResults/Tests.fs @@ -0,0 +1,20 @@ +module Tests + +open System +open Xunit + +[] +let ``My test`` () = + Assert.True(true) + +[] +let ``Fails`` () = + Assert.True(false) + +[] +let ``Skipped`` () = + Assert.True(true) + +[] +let ``Exception`` () = + failwith "Report as an exception" diff --git a/test/FsAutoComplete.Tests.TestExplorer/SampleTestProjects/VSTest.XUnit.RunResults/VSTest.XUnit.RunResults.fsproj b/test/FsAutoComplete.Tests.TestExplorer/SampleTestProjects/VSTest.XUnit.RunResults/VSTest.XUnit.RunResults.fsproj new file mode 100644 index 000000000..1a4224efd --- /dev/null +++ b/test/FsAutoComplete.Tests.TestExplorer/SampleTestProjects/VSTest.XUnit.RunResults/VSTest.XUnit.RunResults.fsproj @@ -0,0 +1,22 @@ + + + + net8.0 + false + false + + + + + + + + + + + + + + + + diff --git a/test/FsAutoComplete.Tests.TestExplorer/TestDiscovery.fs b/test/FsAutoComplete.Tests.TestExplorer/TestDiscoveryTests.fs similarity index 98% rename from test/FsAutoComplete.Tests.TestExplorer/TestDiscovery.fs rename to test/FsAutoComplete.Tests.TestExplorer/TestDiscoveryTests.fs index 49b79710f..ac65f3136 100644 --- a/test/FsAutoComplete.Tests.TestExplorer/TestDiscovery.fs +++ b/test/FsAutoComplete.Tests.TestExplorer/TestDiscoveryTests.fs @@ -1,4 +1,4 @@ -module Tests +module TestDiscoveryTests open Expecto open FsAutoComplete.VSTestAdapter diff --git a/test/FsAutoComplete.Tests.TestExplorer/TestRunTests.fs b/test/FsAutoComplete.Tests.TestExplorer/TestRunTests.fs new file mode 100644 index 000000000..76e8cbf86 --- /dev/null +++ b/test/FsAutoComplete.Tests.TestExplorer/TestRunTests.fs @@ -0,0 +1,50 @@ +module TestRunTests + +open Expecto +open FsAutoComplete.VSTestAdapter +open Microsoft.VisualStudio.TestPlatform.ObjectModel; +open System.IO + + +let tryFindVsTest () : string = + let dotnetBinary = + Ionide.ProjInfo.Paths.dotnetRoot.Value + |> Option.defaultWith (fun () -> failwith "Couldn't find dotnet root. The dotnet sdk must be installed to run these tests") + + let cwd = System.Environment.CurrentDirectory |> Some + + VSTestWrapper.tryFindVsTestFromDotnetRoot dotnetBinary.FullName cwd + |> Result.defaultWith failwith + |> _.FullName + + +let vstestPath = tryFindVsTest () + +[] +let tests = + testList "VSTestWrapper Test Run" [ + testCase "should return an empty list if given no projects" <| fun () -> + let expected = [] + let actual = VSTestWrapper.discoverTests vstestPath ignore [] + Expect.equal actual expected "" + + testCase "should be able to report expected test run outcomes" <| fun () -> + let expected = [ + ("Tests.My test", TestOutcome.Passed) + ("Tests.Fails", TestOutcome.Failed) + ("Tests.Skipped", TestOutcome.Skipped) + ("Tests.Exception", TestOutcome.Failed) + ] + + let sourceDir = __SOURCE_DIRECTORY__ + let sources = [ + Path.Combine(sourceDir, "SampleTestProjects/VSTest.XUnit.RunResults/bin/Debug/net8.0/VSTest.XUnit.RunResults.dll") + ] + + let discovered = VSTestWrapper.runTests vstestPath sources + + let likenessOfTestResult (result: TestResult) = (result.TestCase.FullyQualifiedName, result.Outcome) + let actual = discovered |> List.map likenessOfTestResult + + Expect.equal (set actual) (set expected) "" + ] From 8fd2f863087ab5d3f71b25cdfadeb5018652344d Mon Sep 17 00:00:00 2001 From: farlee2121 <2847259+farlee2121@users.noreply.github.com> Date: Wed, 30 Jul 2025 08:48:37 -0500 Subject: [PATCH 11/39] Rename the namespace for test features. VSTestAdapter is too specific. We already know we'll need to expand to accomodate Microsoft.Testing.Platform --- .../FsAutoComplete.Core.fsproj | 136 +++++++++--------- .../{VSTestAdapter.fs => TestServer.fs} | 19 +-- src/FsAutoComplete/CommandResponse.fs | 2 +- src/FsAutoComplete/CommandResponse.fsi | 2 +- src/FsAutoComplete/LspHelpers.fs | 2 +- src/FsAutoComplete/LspHelpers.fsi | 2 +- .../LspServers/AdaptiveServerState.fs | 8 +- .../LspServers/AdaptiveServerState.fsi | 2 +- .../TestDiscoveryTests.fs | 2 +- .../TestRunTests.fs | 4 +- 10 files changed, 87 insertions(+), 92 deletions(-) rename src/FsAutoComplete.Core/{VSTestAdapter.fs => TestServer.fs} (93%) diff --git a/src/FsAutoComplete.Core/FsAutoComplete.Core.fsproj b/src/FsAutoComplete.Core/FsAutoComplete.Core.fsproj index 68c83bdc9..d4c262f77 100644 --- a/src/FsAutoComplete.Core/FsAutoComplete.Core.fsproj +++ b/src/FsAutoComplete.Core/FsAutoComplete.Core.fsproj @@ -1,69 +1,69 @@ - - - net8.0 - net8.0;net9.0 - false - $(NoWarn);FS0057 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + net8.0 + net8.0;net9.0 + false + $(NoWarn);FS0057 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/FsAutoComplete.Core/VSTestAdapter.fs b/src/FsAutoComplete.Core/TestServer.fs similarity index 93% rename from src/FsAutoComplete.Core/VSTestAdapter.fs rename to src/FsAutoComplete.Core/TestServer.fs index 4cd7b0aae..3dff38232 100644 --- a/src/FsAutoComplete.Core/VSTestAdapter.fs +++ b/src/FsAutoComplete.Core/TestServer.fs @@ -1,12 +1,10 @@ -namespace FsAutoComplete.VSTestAdapter - -open Microsoft.TestPlatform.VsTestConsole.TranslationLayer; -open Microsoft.VisualStudio.TestPlatform.ObjectModel; -open Microsoft.VisualStudio.TestPlatform.ObjectModel.Client; -open Microsoft.VisualStudio.TestPlatform.ObjectModel.Client.Interfaces; -open Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging; +namespace FsAutoComplete.TestServer module VSTestWrapper = + open Microsoft.TestPlatform.VsTestConsole.TranslationLayer; + open Microsoft.VisualStudio.TestPlatform.ObjectModel; + open Microsoft.VisualStudio.TestPlatform.ObjectModel.Client; + open Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging; type TestProjectDll = string @@ -61,7 +59,7 @@ module VSTestWrapper = member _.LaunchProcessWithDebuggerAttached (_testProcessStartInfo: TestProcessStartInfo): int = raise (System.NotImplementedException()) - let runTests (vstestPath: string) (sources: TestProjectDll list) = + let runTests (vstestPath: string) (sources: TestProjectDll list) : TestResult list = let consoleParams = ConsoleParameters() let vstest = new VsTestConsoleWrapper(vstestPath, consoleParams) let runHandler = TestRunHandler() @@ -110,7 +108,4 @@ type TestItem = { TargetFramework : string CodeFilePath : string option CodeLocationRange : TestFileRange option -} - - - +} diff --git a/src/FsAutoComplete/CommandResponse.fs b/src/FsAutoComplete/CommandResponse.fs index 5fa660c11..3fd2874c0 100644 --- a/src/FsAutoComplete/CommandResponse.fs +++ b/src/FsAutoComplete/CommandResponse.fs @@ -705,7 +705,7 @@ module CommandResponse = - type DiscoverTestsResponse = VSTestAdapter.TestItem list + type DiscoverTestsResponse = TestServer.TestItem list let discoverTests (serialize: Serializer) (content: DiscoverTestsResponse) = serialize { Kind = "discoverTests"; Data = content } \ No newline at end of file diff --git a/src/FsAutoComplete/CommandResponse.fsi b/src/FsAutoComplete/CommandResponse.fsi index 8b14e938f..0bffb01f9 100644 --- a/src/FsAutoComplete/CommandResponse.fsi +++ b/src/FsAutoComplete/CommandResponse.fsi @@ -262,6 +262,6 @@ module CommandResponse = val fsharpLiterate: serialize: Serializer -> content: string -> string val pipelineHint: serialize: Serializer -> content: (int * int option * string list)[] -> string - type DiscoverTestsResponse = VSTestAdapter.TestItem list + type DiscoverTestsResponse = TestServer.TestItem list val discoverTests: serialize: Serializer -> content: DiscoverTestsResponse -> string \ No newline at end of file diff --git a/src/FsAutoComplete/LspHelpers.fs b/src/FsAutoComplete/LspHelpers.fs index ead51ae11..fcb1a278c 100644 --- a/src/FsAutoComplete/LspHelpers.fs +++ b/src/FsAutoComplete/LspHelpers.fs @@ -613,7 +613,7 @@ type TestDetectedNotification = Tests: TestAdapter.TestAdapterEntry array } type TestDiscoveryUpdateNotification = - { Tests: VSTestAdapter.TestItem array } + { Tests: TestServer.TestItem array } type ProjectParms = { diff --git a/src/FsAutoComplete/LspHelpers.fsi b/src/FsAutoComplete/LspHelpers.fsi index d24ca9481..65227d334 100644 --- a/src/FsAutoComplete/LspHelpers.fsi +++ b/src/FsAutoComplete/LspHelpers.fsi @@ -199,7 +199,7 @@ type TestDetectedNotification = Tests: TestAdapter.TestAdapterEntry array } type TestDiscoveryUpdateNotification = - { Tests: VSTestAdapter.TestItem array } + { Tests: TestServer.TestItem array } type ProjectParms = { diff --git a/src/FsAutoComplete/LspServers/AdaptiveServerState.fs b/src/FsAutoComplete/LspServers/AdaptiveServerState.fs index 38825658e..074715ed7 100644 --- a/src/FsAutoComplete/LspServers/AdaptiveServerState.fs +++ b/src/FsAutoComplete/LspServers/AdaptiveServerState.fs @@ -2582,7 +2582,7 @@ type AdaptiveState project.PackageReferences |> List.exists (fun pr -> Set.contains pr.Name testProjectIndicators) - let tryTestCaseToDTO (projectLookup: Map) (testCase: Microsoft.VisualStudio.TestPlatform.ObjectModel.TestCase) : VSTestAdapter.TestItem option= + let tryTestCaseToDTO (projectLookup: Map) (testCase: Microsoft.VisualStudio.TestPlatform.ObjectModel.TestCase) : TestServer.TestItem option= match projectLookup |> Map.tryFind testCase.Source with | None -> None // this should never happen. We pass VsTest the list of executables to test, so all the possible sources should be known to us | Some project -> @@ -2597,7 +2597,7 @@ type AdaptiveState } asyncResult { - let! vstestBinary = VSTestAdapter.VSTestWrapper.tryFindVsTestFromDotnetRoot state.Config.DotNetRoot state.RootPath + let! vstestBinary = TestServer.VSTestWrapper.tryFindVsTestFromDotnetRoot state.Config.DotNetRoot state.RootPath let! projects = tryGetWorkspaceProjects state.WorkspacePaths let testProjects = projects |> List.ofSeq |> List.filter isTestProject @@ -2614,9 +2614,9 @@ type AdaptiveState |> Async.RunSynchronously let testCases = - VSTestAdapter.VSTestWrapper.discoverTests vstestBinary.FullName incrementalUpdateHandler testProjectBinaries + TestServer.VSTestWrapper.discoverTests vstestBinary.FullName incrementalUpdateHandler testProjectBinaries - let testDTOs : VSTestAdapter.TestItem list = + let testDTOs : TestServer.TestItem list = testCases |> tryTestCasesToDTOs return testDTOs diff --git a/src/FsAutoComplete/LspServers/AdaptiveServerState.fsi b/src/FsAutoComplete/LspServers/AdaptiveServerState.fsi index 097529b30..6a17f9ce6 100644 --- a/src/FsAutoComplete/LspServers/AdaptiveServerState.fsi +++ b/src/FsAutoComplete/LspServers/AdaptiveServerState.fsi @@ -119,7 +119,7 @@ type AdaptiveState = member GetDeclarations: filename: string -> Async> member GetAllDeclarations: unit -> Async<(string * NavigationTopLevelDeclaration array) array> member GlyphToSymbolKind: (FSharpGlyph -> SymbolKind option) - member DiscoverTests: unit -> Async> + member DiscoverTests: unit -> Async> /// /// Signals the server to cancel an operation that is associated with the given progress token. /// diff --git a/test/FsAutoComplete.Tests.TestExplorer/TestDiscoveryTests.fs b/test/FsAutoComplete.Tests.TestExplorer/TestDiscoveryTests.fs index ac65f3136..50d3ec096 100644 --- a/test/FsAutoComplete.Tests.TestExplorer/TestDiscoveryTests.fs +++ b/test/FsAutoComplete.Tests.TestExplorer/TestDiscoveryTests.fs @@ -1,7 +1,7 @@ module TestDiscoveryTests open Expecto -open FsAutoComplete.VSTestAdapter +open FsAutoComplete.TestServer open Microsoft.VisualStudio.TestPlatform.ObjectModel; open System.IO diff --git a/test/FsAutoComplete.Tests.TestExplorer/TestRunTests.fs b/test/FsAutoComplete.Tests.TestExplorer/TestRunTests.fs index 76e8cbf86..5ec7b1283 100644 --- a/test/FsAutoComplete.Tests.TestExplorer/TestRunTests.fs +++ b/test/FsAutoComplete.Tests.TestExplorer/TestRunTests.fs @@ -1,7 +1,7 @@ module TestRunTests open Expecto -open FsAutoComplete.VSTestAdapter +open FsAutoComplete.TestServer open Microsoft.VisualStudio.TestPlatform.ObjectModel; open System.IO @@ -28,7 +28,7 @@ let tests = let actual = VSTestWrapper.discoverTests vstestPath ignore [] Expect.equal actual expected "" - testCase "should be able to report expected test run outcomes" <| fun () -> + testCase "should be able to report basic test run outcomes" <| fun () -> let expected = [ ("Tests.My test", TestOutcome.Passed) ("Tests.Fails", TestOutcome.Failed) From 724c158012d65f729fc0ede306543016ff7474d7 Mon Sep 17 00:00:00 2001 From: farlee2121 <2847259+farlee2121@users.noreply.github.com> Date: Wed, 30 Jul 2025 15:31:18 -0500 Subject: [PATCH 12/39] Add an lsp endpoint for running tests --- src/FsAutoComplete.Core/TestServer.fs | 65 +++++++++++++++- src/FsAutoComplete/CommandResponse.fs | 5 +- src/FsAutoComplete/CommandResponse.fsi | 3 +- .../LspServers/AdaptiveFSharpLspServer.fs | 8 ++ .../LspServers/AdaptiveServerState.fs | 77 +++++++++++-------- .../LspServers/AdaptiveServerState.fsi | 1 + .../LspServers/IFSharpLspServer.fs | 1 + .../VSTest.XUnit.RunResults/Tests.fs | 3 +- .../TestRunTests.fs | 4 +- 9 files changed, 131 insertions(+), 36 deletions(-) diff --git a/src/FsAutoComplete.Core/TestServer.fs b/src/FsAutoComplete.Core/TestServer.fs index 3dff38232..0e5cf2c90 100644 --- a/src/FsAutoComplete.Core/TestServer.fs +++ b/src/FsAutoComplete.Core/TestServer.fs @@ -1,5 +1,7 @@ namespace FsAutoComplete.TestServer +open System + module VSTestWrapper = open Microsoft.TestPlatform.VsTestConsole.TranslationLayer; open Microsoft.VisualStudio.TestPlatform.ObjectModel; @@ -65,7 +67,7 @@ module VSTestWrapper = let runHandler = TestRunHandler() vstest.RunTests(sources, null, runHandler) - runHandler.TestResults |> List.ofSeq + runHandler.TestResults |> List.ofSeq open System.IO let tryFindVsTestFromDotnetRoot (dotnetRoot: string) (workspaceRoot: string option) : Result = @@ -109,3 +111,64 @@ type TestItem = { CodeFilePath : string option CodeLocationRange : TestFileRange option } + +module TestItem = + let ofVsTestCase (projFilePath: string) (targetFramework: string) (testCase: Microsoft.VisualStudio.TestPlatform.ObjectModel.TestCase) : TestItem = + { + FullName = testCase.FullyQualifiedName + DisplayName = testCase.DisplayName + ExecutorUri = testCase.ExecutorUri |> string + ProjectFilePath = projFilePath + TargetFramework = targetFramework + CodeFilePath = Some testCase.CodeFilePath + CodeLocationRange = Some { StartLine = testCase.LineNumber; EndLine = testCase.LineNumber } + } + +[] +type TestOutcome = + | Failed = 0 + | Passed = 1 + | Skipped = 2 + | None = 3 + | NotFound = 4 + +module TestOutcome = + type VSTestOutcome = Microsoft.VisualStudio.TestPlatform.ObjectModel.TestOutcome + let ofVSTestOutcome (vsTestOutcome: VSTestOutcome) = + match vsTestOutcome with + | VSTestOutcome.Passed -> TestOutcome.Passed + | VSTestOutcome.Failed -> TestOutcome.Failed + | VSTestOutcome.Skipped -> TestOutcome.Skipped + | VSTestOutcome.NotFound -> TestOutcome.NotFound + | VSTestOutcome.None -> TestOutcome.None + | _ -> TestOutcome.None + +type TestResult = { + TestItem : TestItem + Outcome : TestOutcome + ErrorMessage: string option + ErrorStackTrace: string option + AdditionalOutput: string option + Duration: TimeSpan +} + +module TestResult = + type VSTestResult = Microsoft.VisualStudio.TestPlatform.ObjectModel.TestResult + + let ofVsTestResult (projFilePath: string) (targetFramework: string) (vsTestResult: VSTestResult) : TestResult = + let stringToOption (text: string) = + if String.IsNullOrEmpty(text) + then None + else Some text + + { + Outcome = TestOutcome.ofVSTestOutcome vsTestResult.Outcome + ErrorMessage = vsTestResult.ErrorMessage |> stringToOption + ErrorStackTrace = vsTestResult.ErrorStackTrace |> stringToOption + AdditionalOutput = + match vsTestResult.Messages |> Seq.toList with + | [] -> None + | messages -> messages |> List.map _.Text |> String.concat Environment.NewLine |> Some + Duration = vsTestResult.Duration + TestItem = TestItem.ofVsTestCase projFilePath targetFramework vsTestResult.TestCase + } \ No newline at end of file diff --git a/src/FsAutoComplete/CommandResponse.fs b/src/FsAutoComplete/CommandResponse.fs index 3fd2874c0..50d7d1920 100644 --- a/src/FsAutoComplete/CommandResponse.fs +++ b/src/FsAutoComplete/CommandResponse.fs @@ -708,4 +708,7 @@ module CommandResponse = type DiscoverTestsResponse = TestServer.TestItem list let discoverTests (serialize: Serializer) (content: DiscoverTestsResponse) = - serialize { Kind = "discoverTests"; Data = content } \ No newline at end of file + serialize { Kind = "discoverTests"; Data = content } + + let runTests (serialize: Serializer) (content: TestServer.TestResult list) = + serialize { Kind = "discoverTests"; Data = content |> Array.ofList } \ No newline at end of file diff --git a/src/FsAutoComplete/CommandResponse.fsi b/src/FsAutoComplete/CommandResponse.fsi index 0bffb01f9..7daeb7548 100644 --- a/src/FsAutoComplete/CommandResponse.fsi +++ b/src/FsAutoComplete/CommandResponse.fsi @@ -264,4 +264,5 @@ module CommandResponse = type DiscoverTestsResponse = TestServer.TestItem list - val discoverTests: serialize: Serializer -> content: DiscoverTestsResponse -> string \ No newline at end of file + val discoverTests: serialize: Serializer -> content: DiscoverTestsResponse -> string + val runTests: serialize: Serializer -> content: TestServer.TestResult list -> string \ No newline at end of file diff --git a/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs b/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs index 3950d3d75..aabe6cca0 100644 --- a/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs +++ b/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs @@ -3063,6 +3063,13 @@ type AdaptiveFSharpLspServer return Some { Content = CommandResponse.discoverTests FsAutoComplete.JsonSerializer.writeJson testDTOs} } + override this.TestRunTests (): Async> = + asyncResult { + let! testDTOs = state.RunTests() |> AsyncResult.mapError (fun msg -> JsonRpc.Error.InternalError msg) + + return Some { Content = CommandResponse.runTests FsAutoComplete.JsonSerializer.writeJson testDTOs} + } + override x.Dispose() = disposables.Dispose() member this.WindowWorkDoneProgressCancel(param: WorkDoneProgressCancelParams) : Async = @@ -3194,6 +3201,7 @@ module AdaptiveFSharpLspServer = |> Map.add "fsproj/renameFile" (serverRequestHandling (fun s p -> s.FsProjRenameFile(p))) |> Map.add "fsproj/removeFile" (serverRequestHandling (fun s p -> s.FsProjRemoveFile(p))) |> Map.add "test/discoverTests" (serverRequestHandling (fun s _ -> s.TestDiscoverTests() )) + |> Map.add "test/runTests" (serverRequestHandling (fun s _ -> s.TestRunTests() )) let adaptiveServer lspClient = let loader = workspaceLoaderFactory toolsPath diff --git a/src/FsAutoComplete/LspServers/AdaptiveServerState.fs b/src/FsAutoComplete/LspServers/AdaptiveServerState.fs index 074715ed7..f268d6b4d 100644 --- a/src/FsAutoComplete/LspServers/AdaptiveServerState.fs +++ b/src/FsAutoComplete/LspServers/AdaptiveServerState.fs @@ -156,6 +156,25 @@ type FindFirstProject() = $"Couldn't find a corresponding project for {sourceFile}. \n Projects include {allProjects}. \nHave the projects loaded yet or have you tried restoring your project/solution?") +module TestProjectHelpers = + let tryGetWorkspaceProjects (workspaceLoader: IWorkspaceLoader) (workspace: WorkspaceChosen) = + match workspace with + | WorkspaceChosen.NotChosen -> Error "No workspace loaded. Can't discover tests" + | WorkspaceChosen.Projs projectPaths -> + projectPaths |> List.ofSeq |> List.map string |> workspaceLoader.LoadProjects |> Ok + + let isTestProject (project: Types.ProjectOptions) = + let testProjectIndicators = + set [ "Microsoft.TestPlatform.TestHost"; "Microsoft.NET.Test.Sdk" ] + + project.PackageReferences + |> List.exists (fun pr -> Set.contains pr.Name testProjectIndicators) + + let tryGetTestProjects (workspaceLoader: IWorkspaceLoader) (workspace: WorkspaceChosen) = + result { + let! projects = tryGetWorkspaceProjects workspaceLoader workspace + return projects |> List.ofSeq |> List.filter isTestProject + } type AdaptiveState ( @@ -2569,44 +2588,20 @@ type AdaptiveState member x.GlyphToSymbolKind = glyphToSymbolKind |> AVal.force member state.DiscoverTests () = - let tryGetWorkspaceProjects (workspace: WorkspaceChosen) = - match workspace with - | WorkspaceChosen.NotChosen -> Error "No workspace loaded. Can't discover tests" - | WorkspaceChosen.Projs projectPaths -> - projectPaths |> List.ofSeq |> List.map string |> workspaceLoader.LoadProjects |> Ok - - let isTestProject (project: Types.ProjectOptions) = - let testProjectIndicators = - set [ "Microsoft.TestPlatform.TestHost"; "Microsoft.NET.Test.Sdk" ] - - project.PackageReferences - |> List.exists (fun pr -> Set.contains pr.Name testProjectIndicators) - - let tryTestCaseToDTO (projectLookup: Map) (testCase: Microsoft.VisualStudio.TestPlatform.ObjectModel.TestCase) : TestServer.TestItem option= - match projectLookup |> Map.tryFind testCase.Source with - | None -> None // this should never happen. We pass VsTest the list of executables to test, so all the possible sources should be known to us - | Some project -> - Some { - FullName = testCase.FullyQualifiedName - DisplayName = testCase.DisplayName - ExecutorUri = testCase.ExecutorUri |> string - ProjectFilePath = project.ProjectFileName - TargetFramework = project.TargetFramework - CodeFilePath = Some testCase.CodeFilePath - CodeLocationRange = Some { StartLine = testCase.LineNumber; EndLine = testCase.LineNumber } - } asyncResult { let! vstestBinary = TestServer.VSTestWrapper.tryFindVsTestFromDotnetRoot state.Config.DotNetRoot state.RootPath - let! projects = tryGetWorkspaceProjects state.WorkspacePaths - let testProjects = projects |> List.ofSeq |> List.filter isTestProject - + let! testProjects = TestProjectHelpers.tryGetTestProjects workspaceLoader state.WorkspacePaths let testProjectBinaries = testProjects |> List.map _.TargetPath let tryTestCasesToDTOs testCases = + let tryTestCaseToDTO (projectLookup: Map) (testCase: Microsoft.VisualStudio.TestPlatform.ObjectModel.TestCase) : TestServer.TestItem option= + match projectLookup |> Map.tryFind testCase.Source with + | None -> None // this should never happen. We pass VsTest the list of executables to test, so all the possible sources should be known to us + | Some project -> TestServer.TestItem.ofVsTestCase project.ProjectFileName project.TargetFramework testCase |> Some let projectLookup = - projects |> Seq.map (fun p -> p.TargetPath, p) |> Map.ofSeq + testProjects |> Seq.map (fun p -> p.TargetPath, p) |> Map.ofSeq testCases |> List.choose (tryTestCaseToDTO projectLookup) let incrementalUpdateHandler (tests: Microsoft.VisualStudio.TestPlatform.ObjectModel.TestCase list) = @@ -2622,6 +2617,28 @@ type AdaptiveState return testDTOs } + member state.RunTests () = + asyncResult { + let! vstestBinary = TestServer.VSTestWrapper.tryFindVsTestFromDotnetRoot state.Config.DotNetRoot state.RootPath + let! testProjects = TestProjectHelpers.tryGetTestProjects workspaceLoader state.WorkspacePaths + let testProjectBinaries = testProjects |> List.map _.TargetPath + + let testResults = + TestServer.VSTestWrapper.runTests vstestBinary.FullName testProjectBinaries + + let tryTestResultsToDTOs testCases = + let tryTestResultToDTO (projectLookup: Map) (testResult: Microsoft.VisualStudio.TestPlatform.ObjectModel.TestResult) : TestServer.TestResult option = + match projectLookup |> Map.tryFind testResult.TestCase.Source with + | None -> None // this should never happen. We pass VsTest the list of executables to test, so all the possible sources should be known to us + | Some project -> TestServer.TestResult.ofVsTestResult project.ProjectFileName project.TargetFramework testResult |> Some + let projectLookup = + testProjects |> Seq.map (fun p -> p.TargetPath, p) |> Map.ofSeq + testCases |> List.choose (tryTestResultToDTO projectLookup) + + let resultDtos = testResults |> tryTestResultsToDTOs + return resultDtos + } + member x.CancelServerProgress(progressToken: ProgressToken) = progressLookup.Cancel progressToken diff --git a/src/FsAutoComplete/LspServers/AdaptiveServerState.fsi b/src/FsAutoComplete/LspServers/AdaptiveServerState.fsi index 6a17f9ce6..035e6f988 100644 --- a/src/FsAutoComplete/LspServers/AdaptiveServerState.fsi +++ b/src/FsAutoComplete/LspServers/AdaptiveServerState.fsi @@ -120,6 +120,7 @@ type AdaptiveState = member GetAllDeclarations: unit -> Async<(string * NavigationTopLevelDeclaration array) array> member GlyphToSymbolKind: (FSharpGlyph -> SymbolKind option) member DiscoverTests: unit -> Async> + member RunTests: unit -> Async> /// /// Signals the server to cancel an operation that is associated with the given progress token. /// diff --git a/src/FsAutoComplete/LspServers/IFSharpLspServer.fs b/src/FsAutoComplete/LspServers/IFSharpLspServer.fs index d48055a1f..ee56d976a 100644 --- a/src/FsAutoComplete/LspServers/IFSharpLspServer.fs +++ b/src/FsAutoComplete/LspServers/IFSharpLspServer.fs @@ -48,3 +48,4 @@ type IFSharpLspServer = abstract FsProjRemoveFile: DotnetFileRequest -> Async> abstract FsProjAddExistingFile: DotnetFileRequest -> Async> abstract TestDiscoverTests: unit -> Async> + abstract TestRunTests: unit -> Async> diff --git a/test/FsAutoComplete.Tests.TestExplorer/SampleTestProjects/VSTest.XUnit.RunResults/Tests.fs b/test/FsAutoComplete.Tests.TestExplorer/SampleTestProjects/VSTest.XUnit.RunResults/Tests.fs index 198cd03ac..9a638d581 100644 --- a/test/FsAutoComplete.Tests.TestExplorer/SampleTestProjects/VSTest.XUnit.RunResults/Tests.fs +++ b/test/FsAutoComplete.Tests.TestExplorer/SampleTestProjects/VSTest.XUnit.RunResults/Tests.fs @@ -5,6 +5,7 @@ open Xunit [] let ``My test`` () = + System.Console.WriteLine("Where do I show up in the results") Assert.True(true) [] @@ -16,5 +17,5 @@ let ``Skipped`` () = Assert.True(true) [] -let ``Exception`` () = +let ``Exception`` () : unit = failwith "Report as an exception" diff --git a/test/FsAutoComplete.Tests.TestExplorer/TestRunTests.fs b/test/FsAutoComplete.Tests.TestExplorer/TestRunTests.fs index 5ec7b1283..2aa540faf 100644 --- a/test/FsAutoComplete.Tests.TestExplorer/TestRunTests.fs +++ b/test/FsAutoComplete.Tests.TestExplorer/TestRunTests.fs @@ -41,10 +41,10 @@ let tests = Path.Combine(sourceDir, "SampleTestProjects/VSTest.XUnit.RunResults/bin/Debug/net8.0/VSTest.XUnit.RunResults.dll") ] - let discovered = VSTestWrapper.runTests vstestPath sources + let runResults = VSTestWrapper.runTests vstestPath sources let likenessOfTestResult (result: TestResult) = (result.TestCase.FullyQualifiedName, result.Outcome) - let actual = discovered |> List.map likenessOfTestResult + let actual = runResults |> List.map likenessOfTestResult Expect.equal (set actual) (set expected) "" ] From 24ceafc915447e99b96ddbb8d04837ab785d1583 Mon Sep 17 00:00:00 2001 From: farlee2121 <2847259+farlee2121@users.noreply.github.com> Date: Mon, 4 Aug 2025 16:43:22 -0500 Subject: [PATCH 13/39] Fix bug where the TestExplorer tests don't build the required sample projects --- FsAutoComplete.sln | 343 ++++++++++-------- .../FsAutoComplete.Tests.TestExplorer.fsproj | 10 +- .../ResourceLocators.fs | 16 + .../TestDiscoveryTests.fs | 17 +- .../TestRunTests.fs | 3 +- .../VSTest.XUnit.RunResults/Program.fs | 0 .../VSTest.XUnit.RunResults/Tests.fs | 0 .../VSTest.XUnit.RunResults.fsproj | 0 .../VSTest.XUnit.Tests/Program.fs | 0 .../VSTest.XUnit.Tests/Tests.fs | 0 .../VSTest.XUnit.Tests.fsproj | 0 .../SampleTestProjects/paket.dependencies | 0 .../SampleTestProjects/paket.lock | 0 13 files changed, 212 insertions(+), 177 deletions(-) create mode 100644 test/FsAutoComplete.Tests.TestExplorer/ResourceLocators.fs rename test/{FsAutoComplete.Tests.TestExplorer => }/SampleTestProjects/VSTest.XUnit.RunResults/Program.fs (100%) rename test/{FsAutoComplete.Tests.TestExplorer => }/SampleTestProjects/VSTest.XUnit.RunResults/Tests.fs (100%) rename test/{FsAutoComplete.Tests.TestExplorer => }/SampleTestProjects/VSTest.XUnit.RunResults/VSTest.XUnit.RunResults.fsproj (100%) rename test/{FsAutoComplete.Tests.TestExplorer => }/SampleTestProjects/VSTest.XUnit.Tests/Program.fs (100%) rename test/{FsAutoComplete.Tests.TestExplorer => }/SampleTestProjects/VSTest.XUnit.Tests/Tests.fs (100%) rename test/{FsAutoComplete.Tests.TestExplorer => }/SampleTestProjects/VSTest.XUnit.Tests/VSTest.XUnit.Tests.fsproj (100%) rename test/{FsAutoComplete.Tests.TestExplorer => }/SampleTestProjects/paket.dependencies (100%) rename test/{FsAutoComplete.Tests.TestExplorer => }/SampleTestProjects/paket.lock (100%) diff --git a/FsAutoComplete.sln b/FsAutoComplete.sln index 4fd2ccb49..c9895c88a 100644 --- a/FsAutoComplete.sln +++ b/FsAutoComplete.sln @@ -1,155 +1,188 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.28803.452 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".paket", ".paket", "{1BE8AF57-B314-4C92-82A9-64CD9B7A4990}" - ProjectSection(SolutionItems) = preProject - paket.dependencies = paket.dependencies - paket.lock = paket.lock - EndProjectSection -EndProject -Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "FsAutoComplete", "src\FsAutoComplete\FsAutoComplete.fsproj", "{B6AB4EF3-8F60-41A1-AB0C-851A6DEB169E}" -EndProject -Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "FsAutoComplete.Core", "src\FsAutoComplete.Core\FsAutoComplete.Core.fsproj", "{4E4786F3-4566-44E1-8787-91790007F0F6}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{443E0B8D-9AD0-436E-A331-E8CC12965F07}" -EndProject -Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "FsAutoComplete.Tests.Lsp", "test\FsAutoComplete.Tests.Lsp\FsAutoComplete.Tests.Lsp.fsproj", "{6FE2E08A-9F3F-4C7C-B190-77C46AE147D7}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{BA56455D-4AEA-45FC-A569-027A68A76BA6}" -EndProject -Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "FsAutoComplete.Logging", "src\FsAutoComplete.Logging\FsAutoComplete.Logging.fsproj", "{38C1F619-3E1E-4784-9833-E8A2AA95CDAE}" -EndProject -Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "OptionAnalyzer", "test\OptionAnalyzer\OptionAnalyzer.fsproj", "{14C55B44-2063-4891-98BE-8184CAB1BE87}" -EndProject -Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "FsAutoComplete.DependencyManager.Dummy", "test\FsAutoComplete.DependencyManager.Dummy\FsAutoComplete.DependencyManager.Dummy.fsproj", "{C58701B0-D8E3-4B68-A7DE-8524C95F86C0}" -EndProject -Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "benchmarks", "benchmarks\benchmarks.fsproj", "{0CD029D8-B39E-4CBE-A190-C84A7A811180}" -EndProject -Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "FsAutoComplete.Tests.TestExplorer", "test\FsAutoComplete.Tests.TestExplorer\FsAutoComplete.Tests.TestExplorer.fsproj", "{C6CE23AF-EDCE-49ED-AC2F-8AD595C30CD8}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Debug|x64 = Debug|x64 - Debug|x86 = Debug|x86 - Release|Any CPU = Release|Any CPU - Release|x64 = Release|x64 - Release|x86 = Release|x86 - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {B6AB4EF3-8F60-41A1-AB0C-851A6DEB169E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B6AB4EF3-8F60-41A1-AB0C-851A6DEB169E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B6AB4EF3-8F60-41A1-AB0C-851A6DEB169E}.Debug|x64.ActiveCfg = Debug|Any CPU - {B6AB4EF3-8F60-41A1-AB0C-851A6DEB169E}.Debug|x64.Build.0 = Debug|Any CPU - {B6AB4EF3-8F60-41A1-AB0C-851A6DEB169E}.Debug|x86.ActiveCfg = Debug|Any CPU - {B6AB4EF3-8F60-41A1-AB0C-851A6DEB169E}.Debug|x86.Build.0 = Debug|Any CPU - {B6AB4EF3-8F60-41A1-AB0C-851A6DEB169E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B6AB4EF3-8F60-41A1-AB0C-851A6DEB169E}.Release|Any CPU.Build.0 = Release|Any CPU - {B6AB4EF3-8F60-41A1-AB0C-851A6DEB169E}.Release|x64.ActiveCfg = Release|Any CPU - {B6AB4EF3-8F60-41A1-AB0C-851A6DEB169E}.Release|x64.Build.0 = Release|Any CPU - {B6AB4EF3-8F60-41A1-AB0C-851A6DEB169E}.Release|x86.ActiveCfg = Release|Any CPU - {B6AB4EF3-8F60-41A1-AB0C-851A6DEB169E}.Release|x86.Build.0 = Release|Any CPU - {4E4786F3-4566-44E1-8787-91790007F0F6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {4E4786F3-4566-44E1-8787-91790007F0F6}.Debug|Any CPU.Build.0 = Debug|Any CPU - {4E4786F3-4566-44E1-8787-91790007F0F6}.Debug|x64.ActiveCfg = Debug|Any CPU - {4E4786F3-4566-44E1-8787-91790007F0F6}.Debug|x64.Build.0 = Debug|Any CPU - {4E4786F3-4566-44E1-8787-91790007F0F6}.Debug|x86.ActiveCfg = Debug|Any CPU - {4E4786F3-4566-44E1-8787-91790007F0F6}.Debug|x86.Build.0 = Debug|Any CPU - {4E4786F3-4566-44E1-8787-91790007F0F6}.Release|Any CPU.ActiveCfg = Release|Any CPU - {4E4786F3-4566-44E1-8787-91790007F0F6}.Release|Any CPU.Build.0 = Release|Any CPU - {4E4786F3-4566-44E1-8787-91790007F0F6}.Release|x64.ActiveCfg = Release|Any CPU - {4E4786F3-4566-44E1-8787-91790007F0F6}.Release|x64.Build.0 = Release|Any CPU - {4E4786F3-4566-44E1-8787-91790007F0F6}.Release|x86.ActiveCfg = Release|Any CPU - {4E4786F3-4566-44E1-8787-91790007F0F6}.Release|x86.Build.0 = Release|Any CPU - {6FE2E08A-9F3F-4C7C-B190-77C46AE147D7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6FE2E08A-9F3F-4C7C-B190-77C46AE147D7}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6FE2E08A-9F3F-4C7C-B190-77C46AE147D7}.Debug|x64.ActiveCfg = Debug|Any CPU - {6FE2E08A-9F3F-4C7C-B190-77C46AE147D7}.Debug|x64.Build.0 = Debug|Any CPU - {6FE2E08A-9F3F-4C7C-B190-77C46AE147D7}.Debug|x86.ActiveCfg = Debug|Any CPU - {6FE2E08A-9F3F-4C7C-B190-77C46AE147D7}.Debug|x86.Build.0 = Debug|Any CPU - {6FE2E08A-9F3F-4C7C-B190-77C46AE147D7}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6FE2E08A-9F3F-4C7C-B190-77C46AE147D7}.Release|Any CPU.Build.0 = Release|Any CPU - {6FE2E08A-9F3F-4C7C-B190-77C46AE147D7}.Release|x64.ActiveCfg = Release|Any CPU - {6FE2E08A-9F3F-4C7C-B190-77C46AE147D7}.Release|x64.Build.0 = Release|Any CPU - {6FE2E08A-9F3F-4C7C-B190-77C46AE147D7}.Release|x86.ActiveCfg = Release|Any CPU - {6FE2E08A-9F3F-4C7C-B190-77C46AE147D7}.Release|x86.Build.0 = Release|Any CPU - {38C1F619-3E1E-4784-9833-E8A2AA95CDAE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {38C1F619-3E1E-4784-9833-E8A2AA95CDAE}.Debug|Any CPU.Build.0 = Debug|Any CPU - {38C1F619-3E1E-4784-9833-E8A2AA95CDAE}.Debug|x64.ActiveCfg = Debug|Any CPU - {38C1F619-3E1E-4784-9833-E8A2AA95CDAE}.Debug|x64.Build.0 = Debug|Any CPU - {38C1F619-3E1E-4784-9833-E8A2AA95CDAE}.Debug|x86.ActiveCfg = Debug|Any CPU - {38C1F619-3E1E-4784-9833-E8A2AA95CDAE}.Debug|x86.Build.0 = Debug|Any CPU - {38C1F619-3E1E-4784-9833-E8A2AA95CDAE}.Release|Any CPU.ActiveCfg = Release|Any CPU - {38C1F619-3E1E-4784-9833-E8A2AA95CDAE}.Release|Any CPU.Build.0 = Release|Any CPU - {38C1F619-3E1E-4784-9833-E8A2AA95CDAE}.Release|x64.ActiveCfg = Release|Any CPU - {38C1F619-3E1E-4784-9833-E8A2AA95CDAE}.Release|x64.Build.0 = Release|Any CPU - {38C1F619-3E1E-4784-9833-E8A2AA95CDAE}.Release|x86.ActiveCfg = Release|Any CPU - {38C1F619-3E1E-4784-9833-E8A2AA95CDAE}.Release|x86.Build.0 = Release|Any CPU - {14C55B44-2063-4891-98BE-8184CAB1BE87}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {14C55B44-2063-4891-98BE-8184CAB1BE87}.Debug|Any CPU.Build.0 = Debug|Any CPU - {14C55B44-2063-4891-98BE-8184CAB1BE87}.Debug|x64.ActiveCfg = Debug|Any CPU - {14C55B44-2063-4891-98BE-8184CAB1BE87}.Debug|x64.Build.0 = Debug|Any CPU - {14C55B44-2063-4891-98BE-8184CAB1BE87}.Debug|x86.ActiveCfg = Debug|Any CPU - {14C55B44-2063-4891-98BE-8184CAB1BE87}.Debug|x86.Build.0 = Debug|Any CPU - {14C55B44-2063-4891-98BE-8184CAB1BE87}.Release|Any CPU.ActiveCfg = Release|Any CPU - {14C55B44-2063-4891-98BE-8184CAB1BE87}.Release|Any CPU.Build.0 = Release|Any CPU - {14C55B44-2063-4891-98BE-8184CAB1BE87}.Release|x64.ActiveCfg = Release|Any CPU - {14C55B44-2063-4891-98BE-8184CAB1BE87}.Release|x64.Build.0 = Release|Any CPU - {14C55B44-2063-4891-98BE-8184CAB1BE87}.Release|x86.ActiveCfg = Release|Any CPU - {14C55B44-2063-4891-98BE-8184CAB1BE87}.Release|x86.Build.0 = Release|Any CPU - {C58701B0-D8E3-4B68-A7DE-8524C95F86C0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C58701B0-D8E3-4B68-A7DE-8524C95F86C0}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C58701B0-D8E3-4B68-A7DE-8524C95F86C0}.Debug|x64.ActiveCfg = Debug|Any CPU - {C58701B0-D8E3-4B68-A7DE-8524C95F86C0}.Debug|x64.Build.0 = Debug|Any CPU - {C58701B0-D8E3-4B68-A7DE-8524C95F86C0}.Debug|x86.ActiveCfg = Debug|Any CPU - {C58701B0-D8E3-4B68-A7DE-8524C95F86C0}.Debug|x86.Build.0 = Debug|Any CPU - {C58701B0-D8E3-4B68-A7DE-8524C95F86C0}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C58701B0-D8E3-4B68-A7DE-8524C95F86C0}.Release|Any CPU.Build.0 = Release|Any CPU - {C58701B0-D8E3-4B68-A7DE-8524C95F86C0}.Release|x64.ActiveCfg = Release|Any CPU - {C58701B0-D8E3-4B68-A7DE-8524C95F86C0}.Release|x64.Build.0 = Release|Any CPU - {C58701B0-D8E3-4B68-A7DE-8524C95F86C0}.Release|x86.ActiveCfg = Release|Any CPU - {C58701B0-D8E3-4B68-A7DE-8524C95F86C0}.Release|x86.Build.0 = Release|Any CPU - {0CD029D8-B39E-4CBE-A190-C84A7A811180}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0CD029D8-B39E-4CBE-A190-C84A7A811180}.Debug|Any CPU.Build.0 = Debug|Any CPU - {0CD029D8-B39E-4CBE-A190-C84A7A811180}.Debug|x64.ActiveCfg = Debug|Any CPU - {0CD029D8-B39E-4CBE-A190-C84A7A811180}.Debug|x64.Build.0 = Debug|Any CPU - {0CD029D8-B39E-4CBE-A190-C84A7A811180}.Debug|x86.ActiveCfg = Debug|Any CPU - {0CD029D8-B39E-4CBE-A190-C84A7A811180}.Debug|x86.Build.0 = Debug|Any CPU - {0CD029D8-B39E-4CBE-A190-C84A7A811180}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0CD029D8-B39E-4CBE-A190-C84A7A811180}.Release|Any CPU.Build.0 = Release|Any CPU - {0CD029D8-B39E-4CBE-A190-C84A7A811180}.Release|x64.ActiveCfg = Release|Any CPU - {0CD029D8-B39E-4CBE-A190-C84A7A811180}.Release|x64.Build.0 = Release|Any CPU - {0CD029D8-B39E-4CBE-A190-C84A7A811180}.Release|x86.ActiveCfg = Release|Any CPU - {0CD029D8-B39E-4CBE-A190-C84A7A811180}.Release|x86.Build.0 = Release|Any CPU - {C6CE23AF-EDCE-49ED-AC2F-8AD595C30CD8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C6CE23AF-EDCE-49ED-AC2F-8AD595C30CD8}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C6CE23AF-EDCE-49ED-AC2F-8AD595C30CD8}.Debug|x64.ActiveCfg = Debug|Any CPU - {C6CE23AF-EDCE-49ED-AC2F-8AD595C30CD8}.Debug|x64.Build.0 = Debug|Any CPU - {C6CE23AF-EDCE-49ED-AC2F-8AD595C30CD8}.Debug|x86.ActiveCfg = Debug|Any CPU - {C6CE23AF-EDCE-49ED-AC2F-8AD595C30CD8}.Debug|x86.Build.0 = Debug|Any CPU - {C6CE23AF-EDCE-49ED-AC2F-8AD595C30CD8}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C6CE23AF-EDCE-49ED-AC2F-8AD595C30CD8}.Release|Any CPU.Build.0 = Release|Any CPU - {C6CE23AF-EDCE-49ED-AC2F-8AD595C30CD8}.Release|x64.ActiveCfg = Release|Any CPU - {C6CE23AF-EDCE-49ED-AC2F-8AD595C30CD8}.Release|x64.Build.0 = Release|Any CPU - {C6CE23AF-EDCE-49ED-AC2F-8AD595C30CD8}.Release|x86.ActiveCfg = Release|Any CPU - {C6CE23AF-EDCE-49ED-AC2F-8AD595C30CD8}.Release|x86.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {6FE2E08A-9F3F-4C7C-B190-77C46AE147D7} = {443E0B8D-9AD0-436E-A331-E8CC12965F07} - {38C1F619-3E1E-4784-9833-E8A2AA95CDAE} = {BA56455D-4AEA-45FC-A569-027A68A76BA6} - {14C55B44-2063-4891-98BE-8184CAB1BE87} = {443E0B8D-9AD0-436E-A331-E8CC12965F07} - {C58701B0-D8E3-4B68-A7DE-8524C95F86C0} = {443E0B8D-9AD0-436E-A331-E8CC12965F07} - {C6CE23AF-EDCE-49ED-AC2F-8AD595C30CD8} = {443E0B8D-9AD0-436E-A331-E8CC12965F07} - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {1C4EE83B-632A-4929-8C96-38F14254229E} - EndGlobalSection - GlobalSection(MonoDevelopProperties) = preSolution - StartupItem = FsAutoComplete\FsAutoComplete.fsproj - EndGlobalSection -EndGlobal + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.28803.452 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".paket", ".paket", "{1BE8AF57-B314-4C92-82A9-64CD9B7A4990}" + ProjectSection(SolutionItems) = preProject + paket.dependencies = paket.dependencies + paket.lock = paket.lock + EndProjectSection +EndProject +Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "FsAutoComplete", "src\FsAutoComplete\FsAutoComplete.fsproj", "{B6AB4EF3-8F60-41A1-AB0C-851A6DEB169E}" +EndProject +Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "FsAutoComplete.Core", "src\FsAutoComplete.Core\FsAutoComplete.Core.fsproj", "{4E4786F3-4566-44E1-8787-91790007F0F6}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{443E0B8D-9AD0-436E-A331-E8CC12965F07}" +EndProject +Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "FsAutoComplete.Tests.Lsp", "test\FsAutoComplete.Tests.Lsp\FsAutoComplete.Tests.Lsp.fsproj", "{6FE2E08A-9F3F-4C7C-B190-77C46AE147D7}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{BA56455D-4AEA-45FC-A569-027A68A76BA6}" +EndProject +Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "FsAutoComplete.Logging", "src\FsAutoComplete.Logging\FsAutoComplete.Logging.fsproj", "{38C1F619-3E1E-4784-9833-E8A2AA95CDAE}" +EndProject +Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "OptionAnalyzer", "test\OptionAnalyzer\OptionAnalyzer.fsproj", "{14C55B44-2063-4891-98BE-8184CAB1BE87}" +EndProject +Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "FsAutoComplete.DependencyManager.Dummy", "test\FsAutoComplete.DependencyManager.Dummy\FsAutoComplete.DependencyManager.Dummy.fsproj", "{C58701B0-D8E3-4B68-A7DE-8524C95F86C0}" +EndProject +Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "benchmarks", "benchmarks\benchmarks.fsproj", "{0CD029D8-B39E-4CBE-A190-C84A7A811180}" +EndProject +Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "FsAutoComplete.Tests.TestExplorer", "test\FsAutoComplete.Tests.TestExplorer\FsAutoComplete.Tests.TestExplorer.fsproj", "{C6CE23AF-EDCE-49ED-AC2F-8AD595C30CD8}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SampleTestProjects", "SampleTestProjects", "{B093584B-EEA2-D526-B6EB-76B356047302}" +EndProject +Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "VSTest.XUnit.Tests", "test\SampleTestProjects\VSTest.XUnit.Tests\VSTest.XUnit.Tests.fsproj", "{2C67BFBE-0963-4BD6-B292-7CF838C153C3}" +EndProject +Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "VSTest.XUnit.RunResults", "test\SampleTestProjects\VSTest.XUnit.RunResults\VSTest.XUnit.RunResults.fsproj", "{C7B0B178-79BD-4071-B134-17ABA3168EC4}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {B6AB4EF3-8F60-41A1-AB0C-851A6DEB169E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B6AB4EF3-8F60-41A1-AB0C-851A6DEB169E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B6AB4EF3-8F60-41A1-AB0C-851A6DEB169E}.Debug|x64.ActiveCfg = Debug|Any CPU + {B6AB4EF3-8F60-41A1-AB0C-851A6DEB169E}.Debug|x64.Build.0 = Debug|Any CPU + {B6AB4EF3-8F60-41A1-AB0C-851A6DEB169E}.Debug|x86.ActiveCfg = Debug|Any CPU + {B6AB4EF3-8F60-41A1-AB0C-851A6DEB169E}.Debug|x86.Build.0 = Debug|Any CPU + {B6AB4EF3-8F60-41A1-AB0C-851A6DEB169E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B6AB4EF3-8F60-41A1-AB0C-851A6DEB169E}.Release|Any CPU.Build.0 = Release|Any CPU + {B6AB4EF3-8F60-41A1-AB0C-851A6DEB169E}.Release|x64.ActiveCfg = Release|Any CPU + {B6AB4EF3-8F60-41A1-AB0C-851A6DEB169E}.Release|x64.Build.0 = Release|Any CPU + {B6AB4EF3-8F60-41A1-AB0C-851A6DEB169E}.Release|x86.ActiveCfg = Release|Any CPU + {B6AB4EF3-8F60-41A1-AB0C-851A6DEB169E}.Release|x86.Build.0 = Release|Any CPU + {4E4786F3-4566-44E1-8787-91790007F0F6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4E4786F3-4566-44E1-8787-91790007F0F6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4E4786F3-4566-44E1-8787-91790007F0F6}.Debug|x64.ActiveCfg = Debug|Any CPU + {4E4786F3-4566-44E1-8787-91790007F0F6}.Debug|x64.Build.0 = Debug|Any CPU + {4E4786F3-4566-44E1-8787-91790007F0F6}.Debug|x86.ActiveCfg = Debug|Any CPU + {4E4786F3-4566-44E1-8787-91790007F0F6}.Debug|x86.Build.0 = Debug|Any CPU + {4E4786F3-4566-44E1-8787-91790007F0F6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4E4786F3-4566-44E1-8787-91790007F0F6}.Release|Any CPU.Build.0 = Release|Any CPU + {4E4786F3-4566-44E1-8787-91790007F0F6}.Release|x64.ActiveCfg = Release|Any CPU + {4E4786F3-4566-44E1-8787-91790007F0F6}.Release|x64.Build.0 = Release|Any CPU + {4E4786F3-4566-44E1-8787-91790007F0F6}.Release|x86.ActiveCfg = Release|Any CPU + {4E4786F3-4566-44E1-8787-91790007F0F6}.Release|x86.Build.0 = Release|Any CPU + {6FE2E08A-9F3F-4C7C-B190-77C46AE147D7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6FE2E08A-9F3F-4C7C-B190-77C46AE147D7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6FE2E08A-9F3F-4C7C-B190-77C46AE147D7}.Debug|x64.ActiveCfg = Debug|Any CPU + {6FE2E08A-9F3F-4C7C-B190-77C46AE147D7}.Debug|x64.Build.0 = Debug|Any CPU + {6FE2E08A-9F3F-4C7C-B190-77C46AE147D7}.Debug|x86.ActiveCfg = Debug|Any CPU + {6FE2E08A-9F3F-4C7C-B190-77C46AE147D7}.Debug|x86.Build.0 = Debug|Any CPU + {6FE2E08A-9F3F-4C7C-B190-77C46AE147D7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6FE2E08A-9F3F-4C7C-B190-77C46AE147D7}.Release|Any CPU.Build.0 = Release|Any CPU + {6FE2E08A-9F3F-4C7C-B190-77C46AE147D7}.Release|x64.ActiveCfg = Release|Any CPU + {6FE2E08A-9F3F-4C7C-B190-77C46AE147D7}.Release|x64.Build.0 = Release|Any CPU + {6FE2E08A-9F3F-4C7C-B190-77C46AE147D7}.Release|x86.ActiveCfg = Release|Any CPU + {6FE2E08A-9F3F-4C7C-B190-77C46AE147D7}.Release|x86.Build.0 = Release|Any CPU + {38C1F619-3E1E-4784-9833-E8A2AA95CDAE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {38C1F619-3E1E-4784-9833-E8A2AA95CDAE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {38C1F619-3E1E-4784-9833-E8A2AA95CDAE}.Debug|x64.ActiveCfg = Debug|Any CPU + {38C1F619-3E1E-4784-9833-E8A2AA95CDAE}.Debug|x64.Build.0 = Debug|Any CPU + {38C1F619-3E1E-4784-9833-E8A2AA95CDAE}.Debug|x86.ActiveCfg = Debug|Any CPU + {38C1F619-3E1E-4784-9833-E8A2AA95CDAE}.Debug|x86.Build.0 = Debug|Any CPU + {38C1F619-3E1E-4784-9833-E8A2AA95CDAE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {38C1F619-3E1E-4784-9833-E8A2AA95CDAE}.Release|Any CPU.Build.0 = Release|Any CPU + {38C1F619-3E1E-4784-9833-E8A2AA95CDAE}.Release|x64.ActiveCfg = Release|Any CPU + {38C1F619-3E1E-4784-9833-E8A2AA95CDAE}.Release|x64.Build.0 = Release|Any CPU + {38C1F619-3E1E-4784-9833-E8A2AA95CDAE}.Release|x86.ActiveCfg = Release|Any CPU + {38C1F619-3E1E-4784-9833-E8A2AA95CDAE}.Release|x86.Build.0 = Release|Any CPU + {14C55B44-2063-4891-98BE-8184CAB1BE87}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {14C55B44-2063-4891-98BE-8184CAB1BE87}.Debug|Any CPU.Build.0 = Debug|Any CPU + {14C55B44-2063-4891-98BE-8184CAB1BE87}.Debug|x64.ActiveCfg = Debug|Any CPU + {14C55B44-2063-4891-98BE-8184CAB1BE87}.Debug|x64.Build.0 = Debug|Any CPU + {14C55B44-2063-4891-98BE-8184CAB1BE87}.Debug|x86.ActiveCfg = Debug|Any CPU + {14C55B44-2063-4891-98BE-8184CAB1BE87}.Debug|x86.Build.0 = Debug|Any CPU + {14C55B44-2063-4891-98BE-8184CAB1BE87}.Release|Any CPU.ActiveCfg = Release|Any CPU + {14C55B44-2063-4891-98BE-8184CAB1BE87}.Release|Any CPU.Build.0 = Release|Any CPU + {14C55B44-2063-4891-98BE-8184CAB1BE87}.Release|x64.ActiveCfg = Release|Any CPU + {14C55B44-2063-4891-98BE-8184CAB1BE87}.Release|x64.Build.0 = Release|Any CPU + {14C55B44-2063-4891-98BE-8184CAB1BE87}.Release|x86.ActiveCfg = Release|Any CPU + {14C55B44-2063-4891-98BE-8184CAB1BE87}.Release|x86.Build.0 = Release|Any CPU + {C58701B0-D8E3-4B68-A7DE-8524C95F86C0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C58701B0-D8E3-4B68-A7DE-8524C95F86C0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C58701B0-D8E3-4B68-A7DE-8524C95F86C0}.Debug|x64.ActiveCfg = Debug|Any CPU + {C58701B0-D8E3-4B68-A7DE-8524C95F86C0}.Debug|x64.Build.0 = Debug|Any CPU + {C58701B0-D8E3-4B68-A7DE-8524C95F86C0}.Debug|x86.ActiveCfg = Debug|Any CPU + {C58701B0-D8E3-4B68-A7DE-8524C95F86C0}.Debug|x86.Build.0 = Debug|Any CPU + {C58701B0-D8E3-4B68-A7DE-8524C95F86C0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C58701B0-D8E3-4B68-A7DE-8524C95F86C0}.Release|Any CPU.Build.0 = Release|Any CPU + {C58701B0-D8E3-4B68-A7DE-8524C95F86C0}.Release|x64.ActiveCfg = Release|Any CPU + {C58701B0-D8E3-4B68-A7DE-8524C95F86C0}.Release|x64.Build.0 = Release|Any CPU + {C58701B0-D8E3-4B68-A7DE-8524C95F86C0}.Release|x86.ActiveCfg = Release|Any CPU + {C58701B0-D8E3-4B68-A7DE-8524C95F86C0}.Release|x86.Build.0 = Release|Any CPU + {0CD029D8-B39E-4CBE-A190-C84A7A811180}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0CD029D8-B39E-4CBE-A190-C84A7A811180}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0CD029D8-B39E-4CBE-A190-C84A7A811180}.Debug|x64.ActiveCfg = Debug|Any CPU + {0CD029D8-B39E-4CBE-A190-C84A7A811180}.Debug|x64.Build.0 = Debug|Any CPU + {0CD029D8-B39E-4CBE-A190-C84A7A811180}.Debug|x86.ActiveCfg = Debug|Any CPU + {0CD029D8-B39E-4CBE-A190-C84A7A811180}.Debug|x86.Build.0 = Debug|Any CPU + {0CD029D8-B39E-4CBE-A190-C84A7A811180}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0CD029D8-B39E-4CBE-A190-C84A7A811180}.Release|Any CPU.Build.0 = Release|Any CPU + {0CD029D8-B39E-4CBE-A190-C84A7A811180}.Release|x64.ActiveCfg = Release|Any CPU + {0CD029D8-B39E-4CBE-A190-C84A7A811180}.Release|x64.Build.0 = Release|Any CPU + {0CD029D8-B39E-4CBE-A190-C84A7A811180}.Release|x86.ActiveCfg = Release|Any CPU + {0CD029D8-B39E-4CBE-A190-C84A7A811180}.Release|x86.Build.0 = Release|Any CPU + {C6CE23AF-EDCE-49ED-AC2F-8AD595C30CD8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C6CE23AF-EDCE-49ED-AC2F-8AD595C30CD8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C6CE23AF-EDCE-49ED-AC2F-8AD595C30CD8}.Debug|x64.ActiveCfg = Debug|Any CPU + {C6CE23AF-EDCE-49ED-AC2F-8AD595C30CD8}.Debug|x64.Build.0 = Debug|Any CPU + {C6CE23AF-EDCE-49ED-AC2F-8AD595C30CD8}.Debug|x86.ActiveCfg = Debug|Any CPU + {C6CE23AF-EDCE-49ED-AC2F-8AD595C30CD8}.Debug|x86.Build.0 = Debug|Any CPU + {C6CE23AF-EDCE-49ED-AC2F-8AD595C30CD8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C6CE23AF-EDCE-49ED-AC2F-8AD595C30CD8}.Release|Any CPU.Build.0 = Release|Any CPU + {C6CE23AF-EDCE-49ED-AC2F-8AD595C30CD8}.Release|x64.ActiveCfg = Release|Any CPU + {C6CE23AF-EDCE-49ED-AC2F-8AD595C30CD8}.Release|x64.Build.0 = Release|Any CPU + {C6CE23AF-EDCE-49ED-AC2F-8AD595C30CD8}.Release|x86.ActiveCfg = Release|Any CPU + {C6CE23AF-EDCE-49ED-AC2F-8AD595C30CD8}.Release|x86.Build.0 = Release|Any CPU + {2C67BFBE-0963-4BD6-B292-7CF838C153C3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2C67BFBE-0963-4BD6-B292-7CF838C153C3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2C67BFBE-0963-4BD6-B292-7CF838C153C3}.Debug|x64.ActiveCfg = Debug|Any CPU + {2C67BFBE-0963-4BD6-B292-7CF838C153C3}.Debug|x64.Build.0 = Debug|Any CPU + {2C67BFBE-0963-4BD6-B292-7CF838C153C3}.Debug|x86.ActiveCfg = Debug|Any CPU + {2C67BFBE-0963-4BD6-B292-7CF838C153C3}.Debug|x86.Build.0 = Debug|Any CPU + {2C67BFBE-0963-4BD6-B292-7CF838C153C3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2C67BFBE-0963-4BD6-B292-7CF838C153C3}.Release|Any CPU.Build.0 = Release|Any CPU + {2C67BFBE-0963-4BD6-B292-7CF838C153C3}.Release|x64.ActiveCfg = Release|Any CPU + {2C67BFBE-0963-4BD6-B292-7CF838C153C3}.Release|x64.Build.0 = Release|Any CPU + {2C67BFBE-0963-4BD6-B292-7CF838C153C3}.Release|x86.ActiveCfg = Release|Any CPU + {2C67BFBE-0963-4BD6-B292-7CF838C153C3}.Release|x86.Build.0 = Release|Any CPU + {C7B0B178-79BD-4071-B134-17ABA3168EC4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C7B0B178-79BD-4071-B134-17ABA3168EC4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C7B0B178-79BD-4071-B134-17ABA3168EC4}.Debug|x64.ActiveCfg = Debug|Any CPU + {C7B0B178-79BD-4071-B134-17ABA3168EC4}.Debug|x64.Build.0 = Debug|Any CPU + {C7B0B178-79BD-4071-B134-17ABA3168EC4}.Debug|x86.ActiveCfg = Debug|Any CPU + {C7B0B178-79BD-4071-B134-17ABA3168EC4}.Debug|x86.Build.0 = Debug|Any CPU + {C7B0B178-79BD-4071-B134-17ABA3168EC4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C7B0B178-79BD-4071-B134-17ABA3168EC4}.Release|Any CPU.Build.0 = Release|Any CPU + {C7B0B178-79BD-4071-B134-17ABA3168EC4}.Release|x64.ActiveCfg = Release|Any CPU + {C7B0B178-79BD-4071-B134-17ABA3168EC4}.Release|x64.Build.0 = Release|Any CPU + {C7B0B178-79BD-4071-B134-17ABA3168EC4}.Release|x86.ActiveCfg = Release|Any CPU + {C7B0B178-79BD-4071-B134-17ABA3168EC4}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {6FE2E08A-9F3F-4C7C-B190-77C46AE147D7} = {443E0B8D-9AD0-436E-A331-E8CC12965F07} + {38C1F619-3E1E-4784-9833-E8A2AA95CDAE} = {BA56455D-4AEA-45FC-A569-027A68A76BA6} + {14C55B44-2063-4891-98BE-8184CAB1BE87} = {443E0B8D-9AD0-436E-A331-E8CC12965F07} + {C58701B0-D8E3-4B68-A7DE-8524C95F86C0} = {443E0B8D-9AD0-436E-A331-E8CC12965F07} + {C6CE23AF-EDCE-49ED-AC2F-8AD595C30CD8} = {443E0B8D-9AD0-436E-A331-E8CC12965F07} + {B093584B-EEA2-D526-B6EB-76B356047302} = {443E0B8D-9AD0-436E-A331-E8CC12965F07} + {2C67BFBE-0963-4BD6-B292-7CF838C153C3} = {B093584B-EEA2-D526-B6EB-76B356047302} + {C7B0B178-79BD-4071-B134-17ABA3168EC4} = {B093584B-EEA2-D526-B6EB-76B356047302} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {1C4EE83B-632A-4929-8C96-38F14254229E} + EndGlobalSection + GlobalSection(MonoDevelopProperties) = preSolution + StartupItem = FsAutoComplete\FsAutoComplete.fsproj + EndGlobalSection +EndGlobal diff --git a/test/FsAutoComplete.Tests.TestExplorer/FsAutoComplete.Tests.TestExplorer.fsproj b/test/FsAutoComplete.Tests.TestExplorer/FsAutoComplete.Tests.TestExplorer.fsproj index 100441ab8..001354ab7 100644 --- a/test/FsAutoComplete.Tests.TestExplorer/FsAutoComplete.Tests.TestExplorer.fsproj +++ b/test/FsAutoComplete.Tests.TestExplorer/FsAutoComplete.Tests.TestExplorer.fsproj @@ -1,19 +1,19 @@ - - + Exe net8.0 false - + - + + - + \ No newline at end of file diff --git a/test/FsAutoComplete.Tests.TestExplorer/ResourceLocators.fs b/test/FsAutoComplete.Tests.TestExplorer/ResourceLocators.fs new file mode 100644 index 000000000..19255000a --- /dev/null +++ b/test/FsAutoComplete.Tests.TestExplorer/ResourceLocators.fs @@ -0,0 +1,16 @@ +module internal ResourceLocators + + open FsAutoComplete.TestServer + open System.IO + let tryFindVsTest () : string = + let dotnetBinary = + Ionide.ProjInfo.Paths.dotnetRoot.Value + |> Option.defaultWith (fun () -> failwith "Couldn't find dotnet root. The dotnet sdk must be installed to run these tests") + + let cwd = System.Environment.CurrentDirectory |> Some + + VSTestWrapper.tryFindVsTestFromDotnetRoot dotnetBinary.FullName cwd + |> Result.defaultWith failwith + |> _.FullName + + let sampleProjectsRootDir = Path.Combine(__SOURCE_DIRECTORY__, "../SampleTestProjects") diff --git a/test/FsAutoComplete.Tests.TestExplorer/TestDiscoveryTests.fs b/test/FsAutoComplete.Tests.TestExplorer/TestDiscoveryTests.fs index 50d3ec096..872a9f5ff 100644 --- a/test/FsAutoComplete.Tests.TestExplorer/TestDiscoveryTests.fs +++ b/test/FsAutoComplete.Tests.TestExplorer/TestDiscoveryTests.fs @@ -4,21 +4,9 @@ open Expecto open FsAutoComplete.TestServer open Microsoft.VisualStudio.TestPlatform.ObjectModel; open System.IO - - -let tryFindVsTest () : string = - let dotnetBinary = - Ionide.ProjInfo.Paths.dotnetRoot.Value - |> Option.defaultWith (fun () -> failwith "Couldn't find dotnet root. The dotnet sdk must be installed to run these tests") - - let cwd = System.Environment.CurrentDirectory |> Some - - VSTestWrapper.tryFindVsTestFromDotnetRoot dotnetBinary.FullName cwd - |> Result.defaultWith failwith - |> _.FullName -let vstestPath = tryFindVsTest () +let vstestPath = ResourceLocators.tryFindVsTest () [] let tests = @@ -31,9 +19,8 @@ let tests = testCase "should discover tests given a single xunit project" <| fun () -> let expectedTestIds = ["Tests.My test"] - let sourceDir = __SOURCE_DIRECTORY__ let sources = [ - Path.Combine(sourceDir, "SampleTestProjects/VSTest.XUnit.Tests/bin/Debug/net8.0/VSTest.XUnit.Tests.dll") + Path.Combine(ResourceLocators.sampleProjectsRootDir, "VSTest.XUnit.Tests/bin/Debug/net8.0/VSTest.XUnit.Tests.dll") ] let discovered = VSTestWrapper.discoverTests vstestPath ignore sources diff --git a/test/FsAutoComplete.Tests.TestExplorer/TestRunTests.fs b/test/FsAutoComplete.Tests.TestExplorer/TestRunTests.fs index 2aa540faf..715aa9cee 100644 --- a/test/FsAutoComplete.Tests.TestExplorer/TestRunTests.fs +++ b/test/FsAutoComplete.Tests.TestExplorer/TestRunTests.fs @@ -36,9 +36,8 @@ let tests = ("Tests.Exception", TestOutcome.Failed) ] - let sourceDir = __SOURCE_DIRECTORY__ let sources = [ - Path.Combine(sourceDir, "SampleTestProjects/VSTest.XUnit.RunResults/bin/Debug/net8.0/VSTest.XUnit.RunResults.dll") + Path.Combine(ResourceLocators.sampleProjectsRootDir, "VSTest.XUnit.RunResults/bin/Debug/net8.0/VSTest.XUnit.RunResults.dll") ] let runResults = VSTestWrapper.runTests vstestPath sources diff --git a/test/FsAutoComplete.Tests.TestExplorer/SampleTestProjects/VSTest.XUnit.RunResults/Program.fs b/test/SampleTestProjects/VSTest.XUnit.RunResults/Program.fs similarity index 100% rename from test/FsAutoComplete.Tests.TestExplorer/SampleTestProjects/VSTest.XUnit.RunResults/Program.fs rename to test/SampleTestProjects/VSTest.XUnit.RunResults/Program.fs diff --git a/test/FsAutoComplete.Tests.TestExplorer/SampleTestProjects/VSTest.XUnit.RunResults/Tests.fs b/test/SampleTestProjects/VSTest.XUnit.RunResults/Tests.fs similarity index 100% rename from test/FsAutoComplete.Tests.TestExplorer/SampleTestProjects/VSTest.XUnit.RunResults/Tests.fs rename to test/SampleTestProjects/VSTest.XUnit.RunResults/Tests.fs diff --git a/test/FsAutoComplete.Tests.TestExplorer/SampleTestProjects/VSTest.XUnit.RunResults/VSTest.XUnit.RunResults.fsproj b/test/SampleTestProjects/VSTest.XUnit.RunResults/VSTest.XUnit.RunResults.fsproj similarity index 100% rename from test/FsAutoComplete.Tests.TestExplorer/SampleTestProjects/VSTest.XUnit.RunResults/VSTest.XUnit.RunResults.fsproj rename to test/SampleTestProjects/VSTest.XUnit.RunResults/VSTest.XUnit.RunResults.fsproj diff --git a/test/FsAutoComplete.Tests.TestExplorer/SampleTestProjects/VSTest.XUnit.Tests/Program.fs b/test/SampleTestProjects/VSTest.XUnit.Tests/Program.fs similarity index 100% rename from test/FsAutoComplete.Tests.TestExplorer/SampleTestProjects/VSTest.XUnit.Tests/Program.fs rename to test/SampleTestProjects/VSTest.XUnit.Tests/Program.fs diff --git a/test/FsAutoComplete.Tests.TestExplorer/SampleTestProjects/VSTest.XUnit.Tests/Tests.fs b/test/SampleTestProjects/VSTest.XUnit.Tests/Tests.fs similarity index 100% rename from test/FsAutoComplete.Tests.TestExplorer/SampleTestProjects/VSTest.XUnit.Tests/Tests.fs rename to test/SampleTestProjects/VSTest.XUnit.Tests/Tests.fs diff --git a/test/FsAutoComplete.Tests.TestExplorer/SampleTestProjects/VSTest.XUnit.Tests/VSTest.XUnit.Tests.fsproj b/test/SampleTestProjects/VSTest.XUnit.Tests/VSTest.XUnit.Tests.fsproj similarity index 100% rename from test/FsAutoComplete.Tests.TestExplorer/SampleTestProjects/VSTest.XUnit.Tests/VSTest.XUnit.Tests.fsproj rename to test/SampleTestProjects/VSTest.XUnit.Tests/VSTest.XUnit.Tests.fsproj diff --git a/test/FsAutoComplete.Tests.TestExplorer/SampleTestProjects/paket.dependencies b/test/SampleTestProjects/paket.dependencies similarity index 100% rename from test/FsAutoComplete.Tests.TestExplorer/SampleTestProjects/paket.dependencies rename to test/SampleTestProjects/paket.dependencies diff --git a/test/FsAutoComplete.Tests.TestExplorer/SampleTestProjects/paket.lock b/test/SampleTestProjects/paket.lock similarity index 100% rename from test/FsAutoComplete.Tests.TestExplorer/SampleTestProjects/paket.lock rename to test/SampleTestProjects/paket.lock From 5842f8214c0518f181812e1537a56ce935e856a4 Mon Sep 17 00:00:00 2001 From: farlee2121 <2847259+farlee2121@users.noreply.github.com> Date: Mon, 4 Aug 2025 17:51:10 -0500 Subject: [PATCH 14/39] Demonstrate basic streaming of test run results from langauge server --- src/FsAutoComplete.Core/TestServer.fs | 13 +++++++--- src/FsAutoComplete/LspHelpers.fs | 4 +++ src/FsAutoComplete/LspHelpers.fsi | 4 +++ .../LspServers/AdaptiveServerState.fs | 25 +++++++++++-------- .../LspServers/FSharpLspClient.fs | 3 +++ .../LspServers/FSharpLspClient.fsi | 1 + .../TestRunTests.fs | 2 +- 7 files changed, 37 insertions(+), 15 deletions(-) diff --git a/src/FsAutoComplete.Core/TestServer.fs b/src/FsAutoComplete.Core/TestServer.fs index 0e5cf2c90..b1106a292 100644 --- a/src/FsAutoComplete.Core/TestServer.fs +++ b/src/FsAutoComplete.Core/TestServer.fs @@ -40,7 +40,7 @@ module VSTestWrapper = discoveryHandler.DiscoveredTests |> List.ofSeq - type TestRunHandler() = + type TestRunHandler(notifyIncrementalUpdate: TestRunChangedEventArgs -> unit) = member val TestResults : TestResult ResizeArray = ResizeArray() with get,set interface ITestRunEventsHandler with @@ -53,18 +53,20 @@ module VSTestWrapper = member this.HandleTestRunComplete (_testRunCompleteArgs: TestRunCompleteEventArgs, lastChunkArgs: TestRunChangedEventArgs, _runContextAttachments: System.Collections.Generic.ICollection, _executorUris: System.Collections.Generic.ICollection): unit = if((not << isNull) lastChunkArgs && (not << isNull) lastChunkArgs.NewTestResults) then this.TestResults.AddRange(lastChunkArgs.NewTestResults) + notifyIncrementalUpdate lastChunkArgs member this.HandleTestRunStatsChange (testRunChangedArgs: TestRunChangedEventArgs): unit = if((not << isNull) testRunChangedArgs && (not << isNull) testRunChangedArgs.NewTestResults) then this.TestResults.AddRange(testRunChangedArgs.NewTestResults) + notifyIncrementalUpdate testRunChangedArgs member _.LaunchProcessWithDebuggerAttached (_testProcessStartInfo: TestProcessStartInfo): int = raise (System.NotImplementedException()) - let runTests (vstestPath: string) (sources: TestProjectDll list) : TestResult list = + let runTests (vstestPath: string) (incrementalUpdateHandler: TestRunChangedEventArgs -> unit) (sources: TestProjectDll list) : TestResult list = let consoleParams = ConsoleParameters() let vstest = new VsTestConsoleWrapper(vstestPath, consoleParams) - let runHandler = TestRunHandler() + let runHandler = TestRunHandler(incrementalUpdateHandler) vstest.RunTests(sources, null, runHandler) runHandler.TestResults |> List.ofSeq @@ -124,6 +126,11 @@ module TestItem = CodeLocationRange = Some { StartLine = testCase.LineNumber; EndLine = testCase.LineNumber } } + let tryTestCaseToDTO (projectLookup: string -> Ionide.ProjInfo.Types.ProjectOptions option) (testCase: Microsoft.VisualStudio.TestPlatform.ObjectModel.TestCase) : TestItem option = + match projectLookup testCase.Source with + | None -> None // this should never happen. We pass VsTest the list of executables to test, so all the possible sources should be known to us + | Some project -> ofVsTestCase project.ProjectFileName project.TargetFramework testCase |> Some + [] type TestOutcome = | Failed = 0 diff --git a/src/FsAutoComplete/LspHelpers.fs b/src/FsAutoComplete/LspHelpers.fs index fcb1a278c..06eb33057 100644 --- a/src/FsAutoComplete/LspHelpers.fs +++ b/src/FsAutoComplete/LspHelpers.fs @@ -615,6 +615,10 @@ type TestDetectedNotification = type TestDiscoveryUpdateNotification = { Tests: TestServer.TestItem array } +type TestRunUpdateNotification = + { TestResults: TestServer.TestResult array + ActiveTests: TestServer.TestItem array } + type ProjectParms = { /// Project file to compile diff --git a/src/FsAutoComplete/LspHelpers.fsi b/src/FsAutoComplete/LspHelpers.fsi index 65227d334..f9b93aad0 100644 --- a/src/FsAutoComplete/LspHelpers.fsi +++ b/src/FsAutoComplete/LspHelpers.fsi @@ -201,6 +201,10 @@ type TestDetectedNotification = type TestDiscoveryUpdateNotification = { Tests: TestServer.TestItem array } +type TestRunUpdateNotification = + { TestResults: TestServer.TestResult array + ActiveTests: TestServer.TestItem array } + type ProjectParms = { /// Project file to compile diff --git a/src/FsAutoComplete/LspServers/AdaptiveServerState.fs b/src/FsAutoComplete/LspServers/AdaptiveServerState.fs index f268d6b4d..b4d5b3d38 100644 --- a/src/FsAutoComplete/LspServers/AdaptiveServerState.fs +++ b/src/FsAutoComplete/LspServers/AdaptiveServerState.fs @@ -2596,13 +2596,9 @@ type AdaptiveState let testProjectBinaries = testProjects |> List.map _.TargetPath let tryTestCasesToDTOs testCases = - let tryTestCaseToDTO (projectLookup: Map) (testCase: Microsoft.VisualStudio.TestPlatform.ObjectModel.TestCase) : TestServer.TestItem option= - match projectLookup |> Map.tryFind testCase.Source with - | None -> None // this should never happen. We pass VsTest the list of executables to test, so all the possible sources should be known to us - | Some project -> TestServer.TestItem.ofVsTestCase project.ProjectFileName project.TargetFramework testCase |> Some let projectLookup = testProjects |> Seq.map (fun p -> p.TargetPath, p) |> Map.ofSeq - testCases |> List.choose (tryTestCaseToDTO projectLookup) + testCases |> List.choose (TestServer.TestItem.tryTestCaseToDTO projectLookup.TryFind) let incrementalUpdateHandler (tests: Microsoft.VisualStudio.TestPlatform.ObjectModel.TestCase list) = lspClient.NotifyTestDiscoveryUpdate({ Tests = tests |> tryTestCasesToDTOs |> Array.ofList}) @@ -2622,19 +2618,26 @@ type AdaptiveState let! vstestBinary = TestServer.VSTestWrapper.tryFindVsTestFromDotnetRoot state.Config.DotNetRoot state.RootPath let! testProjects = TestProjectHelpers.tryGetTestProjects workspaceLoader state.WorkspacePaths let testProjectBinaries = testProjects |> List.map _.TargetPath - - let testResults = - TestServer.VSTestWrapper.runTests vstestBinary.FullName testProjectBinaries - + + let projectLookup = + testProjects |> Seq.map (fun p -> p.TargetPath, p) |> Map.ofSeq let tryTestResultsToDTOs testCases = let tryTestResultToDTO (projectLookup: Map) (testResult: Microsoft.VisualStudio.TestPlatform.ObjectModel.TestResult) : TestServer.TestResult option = match projectLookup |> Map.tryFind testResult.TestCase.Source with | None -> None // this should never happen. We pass VsTest the list of executables to test, so all the possible sources should be known to us | Some project -> TestServer.TestResult.ofVsTestResult project.ProjectFileName project.TargetFramework testResult |> Some - let projectLookup = - testProjects |> Seq.map (fun p -> p.TargetPath, p) |> Map.ofSeq testCases |> List.choose (tryTestResultToDTO projectLookup) + let incrementalUpdateHandler (runUpdate: Microsoft.VisualStudio.TestPlatform.ObjectModel.Client.TestRunChangedEventArgs) = + lspClient.NotifyTestRunUpdate({ + TestResults = runUpdate.NewTestResults |> List.ofSeq |> tryTestResultsToDTOs |> Array.ofSeq + ActiveTests = runUpdate.ActiveTests |> Seq.choose (TestServer.TestItem.tryTestCaseToDTO projectLookup.TryFind) |> Array.ofSeq + }) + |> Async.RunSynchronously + + let testResults = + TestServer.VSTestWrapper.runTests vstestBinary.FullName incrementalUpdateHandler testProjectBinaries + let resultDtos = testResults |> tryTestResultsToDTOs return resultDtos } diff --git a/src/FsAutoComplete/LspServers/FSharpLspClient.fs b/src/FsAutoComplete/LspServers/FSharpLspClient.fs index 9dc545511..a2858ad77 100644 --- a/src/FsAutoComplete/LspServers/FSharpLspClient.fs +++ b/src/FsAutoComplete/LspServers/FSharpLspClient.fs @@ -65,6 +65,9 @@ type FSharpLspClient(sendServerNotification: ClientNotificationSender, sendServe member __.NotifyTestDiscoveryUpdate(p: TestDiscoveryUpdateNotification) = sendServerNotification "test/testDiscoveryUpdate" (box { Content = JsonSerializer.writeJson p}) |> Async.Ignore + member __.NotifyTestRunUpdate(p: TestRunUpdateNotification) = + sendServerNotification "test/testRunUpdate" (box { Content = JsonSerializer.writeJson p}) |> Async.Ignore + member x.CodeLensRefresh() = match x.ClientCapabilities with | Some { Workspace = Some { CodeLens = Some { RefreshSupport = Some true } } } -> diff --git a/src/FsAutoComplete/LspServers/FSharpLspClient.fsi b/src/FsAutoComplete/LspServers/FSharpLspClient.fsi index ecd9c29ba..0c492568d 100644 --- a/src/FsAutoComplete/LspServers/FSharpLspClient.fsi +++ b/src/FsAutoComplete/LspServers/FSharpLspClient.fsi @@ -33,6 +33,7 @@ type FSharpLspClient = member NotifyDocumentAnalyzed: p: DocumentAnalyzedNotification -> Async member NotifyTestDetected: p: TestDetectedNotification -> Async member NotifyTestDiscoveryUpdate: p: TestDiscoveryUpdateNotification -> Async + member NotifyTestRunUpdate: p: TestRunUpdateNotification -> Async member CodeLensRefresh: unit -> Async override WindowWorkDoneProgressCreate: WorkDoneProgressCreateParams -> AsyncLspResult member Progress: ProgressToken * 'Progress -> Async diff --git a/test/FsAutoComplete.Tests.TestExplorer/TestRunTests.fs b/test/FsAutoComplete.Tests.TestExplorer/TestRunTests.fs index 715aa9cee..f701884dc 100644 --- a/test/FsAutoComplete.Tests.TestExplorer/TestRunTests.fs +++ b/test/FsAutoComplete.Tests.TestExplorer/TestRunTests.fs @@ -40,7 +40,7 @@ let tests = Path.Combine(ResourceLocators.sampleProjectsRootDir, "VSTest.XUnit.RunResults/bin/Debug/net8.0/VSTest.XUnit.RunResults.dll") ] - let runResults = VSTestWrapper.runTests vstestPath sources + let runResults = VSTestWrapper.runTests vstestPath ignore sources let likenessOfTestResult (result: TestResult) = (result.TestCase.FullyQualifiedName, result.Outcome) let actual = runResults |> List.map likenessOfTestResult From 768acb4a7d457531d2a9d18371ad61967f04356b Mon Sep 17 00:00:00 2001 From: farlee2121 <2847259+farlee2121@users.noreply.github.com> Date: Tue, 5 Aug 2025 10:05:46 -0500 Subject: [PATCH 15/39] Add test filtering to the VSTest wrapper --- src/FsAutoComplete.Core/TestServer.fs | 11 +++++++-- .../LspServers/AdaptiveServerState.fs | 2 +- .../FsAutoComplete.Tests.TestExplorer.fsproj | 4 ++-- .../TestRunTests.fs | 23 +++++++++++++++++-- .../VSTest.XUnit.RunResults/Tests.fs | 9 ++++++++ 5 files changed, 42 insertions(+), 7 deletions(-) diff --git a/src/FsAutoComplete.Core/TestServer.fs b/src/FsAutoComplete.Core/TestServer.fs index b1106a292..bcecd0916 100644 --- a/src/FsAutoComplete.Core/TestServer.fs +++ b/src/FsAutoComplete.Core/TestServer.fs @@ -63,12 +63,19 @@ module VSTestWrapper = member _.LaunchProcessWithDebuggerAttached (_testProcessStartInfo: TestProcessStartInfo): int = raise (System.NotImplementedException()) - let runTests (vstestPath: string) (incrementalUpdateHandler: TestRunChangedEventArgs -> unit) (sources: TestProjectDll list) : TestResult list = + module TestPlatformOptions = + let withTestCaseFilter (options: TestPlatformOptions) filterExpression = + options.TestCaseFilter <- filterExpression + + let runTests (vstestPath: string) (incrementalUpdateHandler: TestRunChangedEventArgs -> unit) (sources: TestProjectDll list) (testCaseFilter: string option): TestResult list = let consoleParams = ConsoleParameters() let vstest = new VsTestConsoleWrapper(vstestPath, consoleParams) let runHandler = TestRunHandler(incrementalUpdateHandler) - vstest.RunTests(sources, null, runHandler) + let options = new TestPlatformOptions() + testCaseFilter |> Option.iter (TestPlatformOptions.withTestCaseFilter options) + + vstest.RunTests(sources, null, options, runHandler) runHandler.TestResults |> List.ofSeq open System.IO diff --git a/src/FsAutoComplete/LspServers/AdaptiveServerState.fs b/src/FsAutoComplete/LspServers/AdaptiveServerState.fs index b4d5b3d38..e31b64979 100644 --- a/src/FsAutoComplete/LspServers/AdaptiveServerState.fs +++ b/src/FsAutoComplete/LspServers/AdaptiveServerState.fs @@ -2636,7 +2636,7 @@ type AdaptiveState |> Async.RunSynchronously let testResults = - TestServer.VSTestWrapper.runTests vstestBinary.FullName incrementalUpdateHandler testProjectBinaries + TestServer.VSTestWrapper.runTests vstestBinary.FullName incrementalUpdateHandler testProjectBinaries None let resultDtos = testResults |> tryTestResultsToDTOs return resultDtos diff --git a/test/FsAutoComplete.Tests.TestExplorer/FsAutoComplete.Tests.TestExplorer.fsproj b/test/FsAutoComplete.Tests.TestExplorer/FsAutoComplete.Tests.TestExplorer.fsproj index 001354ab7..e04d4326f 100644 --- a/test/FsAutoComplete.Tests.TestExplorer/FsAutoComplete.Tests.TestExplorer.fsproj +++ b/test/FsAutoComplete.Tests.TestExplorer/FsAutoComplete.Tests.TestExplorer.fsproj @@ -12,8 +12,8 @@ - - + + \ No newline at end of file diff --git a/test/FsAutoComplete.Tests.TestExplorer/TestRunTests.fs b/test/FsAutoComplete.Tests.TestExplorer/TestRunTests.fs index f701884dc..5a4e5949a 100644 --- a/test/FsAutoComplete.Tests.TestExplorer/TestRunTests.fs +++ b/test/FsAutoComplete.Tests.TestExplorer/TestRunTests.fs @@ -25,7 +25,7 @@ let tests = testList "VSTestWrapper Test Run" [ testCase "should return an empty list if given no projects" <| fun () -> let expected = [] - let actual = VSTestWrapper.discoverTests vstestPath ignore [] + let actual = VSTestWrapper.runTests vstestPath ignore [] None Expect.equal actual expected "" testCase "should be able to report basic test run outcomes" <| fun () -> @@ -34,13 +34,32 @@ let tests = ("Tests.Fails", TestOutcome.Failed) ("Tests.Skipped", TestOutcome.Skipped) ("Tests.Exception", TestOutcome.Failed) + ("Tests+Nested.Test 1", TestOutcome.Passed) + ("Tests+Nested.Test 2", TestOutcome.Passed) ] let sources = [ Path.Combine(ResourceLocators.sampleProjectsRootDir, "VSTest.XUnit.RunResults/bin/Debug/net8.0/VSTest.XUnit.RunResults.dll") ] - let runResults = VSTestWrapper.runTests vstestPath ignore sources + let runResults = VSTestWrapper.runTests vstestPath ignore sources None + + let likenessOfTestResult (result: TestResult) = (result.TestCase.FullyQualifiedName, result.Outcome) + let actual = runResults |> List.map likenessOfTestResult + + Expect.equal (set actual) (set expected) "" + + testCase "should run only tests that match the case filter" <| fun () -> + let expected = [ + ("Tests+Nested.Test 1", TestOutcome.Passed) + ("Tests+Nested.Test 2", TestOutcome.Passed) + ] + + let sources = [ + Path.Combine(ResourceLocators.sampleProjectsRootDir, "VSTest.XUnit.RunResults/bin/Debug/net8.0/VSTest.XUnit.RunResults.dll") + ] + + let runResults = VSTestWrapper.runTests vstestPath ignore sources (Some "FullyQualifiedName~Tests+Nested") let likenessOfTestResult (result: TestResult) = (result.TestCase.FullyQualifiedName, result.Outcome) let actual = runResults |> List.map likenessOfTestResult diff --git a/test/SampleTestProjects/VSTest.XUnit.RunResults/Tests.fs b/test/SampleTestProjects/VSTest.XUnit.RunResults/Tests.fs index 9a638d581..56ac5909b 100644 --- a/test/SampleTestProjects/VSTest.XUnit.RunResults/Tests.fs +++ b/test/SampleTestProjects/VSTest.XUnit.RunResults/Tests.fs @@ -19,3 +19,12 @@ let ``Skipped`` () = [] let ``Exception`` () : unit = failwith "Report as an exception" + +module Nested = + [] + let ``Test 1`` () : unit = + () + + [] + let ``Test 2`` () : unit = + () From 487a6ce4e9fc7fcd7e23cec9d630bf49887f50d1 Mon Sep 17 00:00:00 2001 From: farlee2121 <2847259+farlee2121@users.noreply.github.com> Date: Tue, 5 Aug 2025 10:56:23 -0500 Subject: [PATCH 16/39] Add testCaseFilter support to test Runs Still has a bug where NUnit doesn't respect the filter, but works for all other frameworks --- src/FsAutoComplete/LspHelpers.fs | 3 +++ src/FsAutoComplete/LspHelpers.fsi | 3 +++ src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs | 6 +++--- src/FsAutoComplete/LspServers/AdaptiveServerState.fs | 4 ++-- src/FsAutoComplete/LspServers/AdaptiveServerState.fsi | 2 +- src/FsAutoComplete/LspServers/IFSharpLspServer.fs | 2 +- 6 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/FsAutoComplete/LspHelpers.fs b/src/FsAutoComplete/LspHelpers.fs index 06eb33057..e1d296f14 100644 --- a/src/FsAutoComplete/LspHelpers.fs +++ b/src/FsAutoComplete/LspHelpers.fs @@ -612,6 +612,9 @@ type TestDetectedNotification = { File: string Tests: TestAdapter.TestAdapterEntry array } +type TestRunRequest = + { TestCaseFilter: string option } + type TestDiscoveryUpdateNotification = { Tests: TestServer.TestItem array } diff --git a/src/FsAutoComplete/LspHelpers.fsi b/src/FsAutoComplete/LspHelpers.fsi index f9b93aad0..f3e54bb8e 100644 --- a/src/FsAutoComplete/LspHelpers.fsi +++ b/src/FsAutoComplete/LspHelpers.fsi @@ -198,6 +198,9 @@ type TestDetectedNotification = { File: string Tests: TestAdapter.TestAdapterEntry array } +type TestRunRequest = + { TestCaseFilter: string option } + type TestDiscoveryUpdateNotification = { Tests: TestServer.TestItem array } diff --git a/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs b/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs index aabe6cca0..abd670c9d 100644 --- a/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs +++ b/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs @@ -3063,9 +3063,9 @@ type AdaptiveFSharpLspServer return Some { Content = CommandResponse.discoverTests FsAutoComplete.JsonSerializer.writeJson testDTOs} } - override this.TestRunTests (): Async> = + override this.TestRunTests (p: TestRunRequest): Async> = asyncResult { - let! testDTOs = state.RunTests() |> AsyncResult.mapError (fun msg -> JsonRpc.Error.InternalError msg) + let! testDTOs = state.RunTests(p.TestCaseFilter) |> AsyncResult.mapError (fun msg -> JsonRpc.Error.InternalError msg) return Some { Content = CommandResponse.runTests FsAutoComplete.JsonSerializer.writeJson testDTOs} } @@ -3201,7 +3201,7 @@ module AdaptiveFSharpLspServer = |> Map.add "fsproj/renameFile" (serverRequestHandling (fun s p -> s.FsProjRenameFile(p))) |> Map.add "fsproj/removeFile" (serverRequestHandling (fun s p -> s.FsProjRemoveFile(p))) |> Map.add "test/discoverTests" (serverRequestHandling (fun s _ -> s.TestDiscoverTests() )) - |> Map.add "test/runTests" (serverRequestHandling (fun s _ -> s.TestRunTests() )) + |> Map.add "test/runTests" (serverRequestHandling (fun s p -> s.TestRunTests(p) )) let adaptiveServer lspClient = let loader = workspaceLoaderFactory toolsPath diff --git a/src/FsAutoComplete/LspServers/AdaptiveServerState.fs b/src/FsAutoComplete/LspServers/AdaptiveServerState.fs index e31b64979..54c39d126 100644 --- a/src/FsAutoComplete/LspServers/AdaptiveServerState.fs +++ b/src/FsAutoComplete/LspServers/AdaptiveServerState.fs @@ -2613,7 +2613,7 @@ type AdaptiveState return testDTOs } - member state.RunTests () = + member state.RunTests (testCaseFilter: string option) = asyncResult { let! vstestBinary = TestServer.VSTestWrapper.tryFindVsTestFromDotnetRoot state.Config.DotNetRoot state.RootPath let! testProjects = TestProjectHelpers.tryGetTestProjects workspaceLoader state.WorkspacePaths @@ -2636,7 +2636,7 @@ type AdaptiveState |> Async.RunSynchronously let testResults = - TestServer.VSTestWrapper.runTests vstestBinary.FullName incrementalUpdateHandler testProjectBinaries None + TestServer.VSTestWrapper.runTests vstestBinary.FullName incrementalUpdateHandler testProjectBinaries testCaseFilter let resultDtos = testResults |> tryTestResultsToDTOs return resultDtos diff --git a/src/FsAutoComplete/LspServers/AdaptiveServerState.fsi b/src/FsAutoComplete/LspServers/AdaptiveServerState.fsi index 035e6f988..d27332291 100644 --- a/src/FsAutoComplete/LspServers/AdaptiveServerState.fsi +++ b/src/FsAutoComplete/LspServers/AdaptiveServerState.fsi @@ -120,7 +120,7 @@ type AdaptiveState = member GetAllDeclarations: unit -> Async<(string * NavigationTopLevelDeclaration array) array> member GlyphToSymbolKind: (FSharpGlyph -> SymbolKind option) member DiscoverTests: unit -> Async> - member RunTests: unit -> Async> + member RunTests: string option -> Async> /// /// Signals the server to cancel an operation that is associated with the given progress token. /// diff --git a/src/FsAutoComplete/LspServers/IFSharpLspServer.fs b/src/FsAutoComplete/LspServers/IFSharpLspServer.fs index ee56d976a..00d61e196 100644 --- a/src/FsAutoComplete/LspServers/IFSharpLspServer.fs +++ b/src/FsAutoComplete/LspServers/IFSharpLspServer.fs @@ -48,4 +48,4 @@ type IFSharpLspServer = abstract FsProjRemoveFile: DotnetFileRequest -> Async> abstract FsProjAddExistingFile: DotnetFileRequest -> Async> abstract TestDiscoverTests: unit -> Async> - abstract TestRunTests: unit -> Async> + abstract TestRunTests: TestRunRequest -> Async> From 5d9e2f36f82c1012dba7c6184fecc52c1f365ed4 Mon Sep 17 00:00:00 2001 From: farlee2121 <2847259+farlee2121@users.noreply.github.com> Date: Wed, 6 Aug 2025 08:27:02 -0500 Subject: [PATCH 17/39] Prove that I can scrape test host process Ids Also required figuring out graceful cancellation behavior for test runs --- src/FsAutoComplete.Core/TestServer.fs | 57 +++++++++++++++++-- .../LspServers/AdaptiveServerState.fs | 23 +++++--- .../TestRunTests.fs | 32 +++++++++-- 3 files changed, 93 insertions(+), 19 deletions(-) diff --git a/src/FsAutoComplete.Core/TestServer.fs b/src/FsAutoComplete.Core/TestServer.fs index bcecd0916..cb3a73b0f 100644 --- a/src/FsAutoComplete.Core/TestServer.fs +++ b/src/FsAutoComplete.Core/TestServer.fs @@ -7,6 +7,7 @@ module VSTestWrapper = open Microsoft.VisualStudio.TestPlatform.ObjectModel; open Microsoft.VisualStudio.TestPlatform.ObjectModel.Client; open Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging; + open System.Text.RegularExpressions type TestProjectDll = string @@ -39,13 +40,31 @@ module VSTestWrapper = vstest.DiscoverTests(sources, null, discoveryHandler) discoveryHandler.DiscoveredTests |> List.ofSeq + type ProcessId = string + type TestRunUpdate = + | Progress of TestRunChangedEventArgs + | AttachDebugProcess of ProcessId + + type TestRunHandler(notifyIncrementalUpdate: TestRunUpdate -> unit) = + + let debugProcessIdRegex = Regex(@"Process Id: (.*),") + + let tryGetDebugProcessId consoleOutput = + let m = debugProcessIdRegex.Match(consoleOutput) + + if m.Success then + let processId = m.Groups.[1].Value + Some processId + else + None - type TestRunHandler(notifyIncrementalUpdate: TestRunChangedEventArgs -> unit) = member val TestResults : TestResult ResizeArray = ResizeArray() with get,set interface ITestRunEventsHandler with - member _.HandleLogMessage (_level: TestMessageLevel, _message: string): unit = - () + member _.HandleLogMessage (_level: TestMessageLevel, message: string): unit = + match tryGetDebugProcessId message with + | Some processId -> notifyIncrementalUpdate (AttachDebugProcess processId) + | None -> () member _.HandleRawMessage (_rawMessage: string): unit = () @@ -53,12 +72,12 @@ module VSTestWrapper = member this.HandleTestRunComplete (_testRunCompleteArgs: TestRunCompleteEventArgs, lastChunkArgs: TestRunChangedEventArgs, _runContextAttachments: System.Collections.Generic.ICollection, _executorUris: System.Collections.Generic.ICollection): unit = if((not << isNull) lastChunkArgs && (not << isNull) lastChunkArgs.NewTestResults) then this.TestResults.AddRange(lastChunkArgs.NewTestResults) - notifyIncrementalUpdate lastChunkArgs + notifyIncrementalUpdate (Progress lastChunkArgs) member this.HandleTestRunStatsChange (testRunChangedArgs: TestRunChangedEventArgs): unit = if((not << isNull) testRunChangedArgs && (not << isNull) testRunChangedArgs.NewTestResults) then this.TestResults.AddRange(testRunChangedArgs.NewTestResults) - notifyIncrementalUpdate testRunChangedArgs + notifyIncrementalUpdate (Progress testRunChangedArgs) member _.LaunchProcessWithDebuggerAttached (_testProcessStartInfo: TestProcessStartInfo): int = raise (System.NotImplementedException()) @@ -67,8 +86,12 @@ module VSTestWrapper = let withTestCaseFilter (options: TestPlatformOptions) filterExpression = options.TestCaseFilter <- filterExpression - let runTests (vstestPath: string) (incrementalUpdateHandler: TestRunChangedEventArgs -> unit) (sources: TestProjectDll list) (testCaseFilter: string option): TestResult list = + let runTests (vstestPath: string) (incrementalUpdateHandler: TestRunUpdate -> unit) (sources: TestProjectDll list) (testCaseFilter: string option) (shouldDebug: bool): TestResult list = let consoleParams = ConsoleParameters() + if shouldDebug then + consoleParams.EnvironmentVariables <- [ + "VSTEST_HOST_DEBUG", "1" + ] |> dict |> System.Collections.Generic.Dictionary let vstest = new VsTestConsoleWrapper(vstestPath, consoleParams) let runHandler = TestRunHandler(incrementalUpdateHandler) @@ -78,6 +101,28 @@ module VSTestWrapper = vstest.RunTests(sources, null, options, runHandler) runHandler.TestResults |> List.ofSeq + let runTestsAsync (vstestPath: string) (incrementalUpdateHandler: TestRunUpdate -> unit) (sources: TestProjectDll list) (testCaseFilter: string option) (shouldDebug: bool) : Async = + async { + let consoleParams = ConsoleParameters() + if shouldDebug then + consoleParams.EnvironmentVariables <- [ + "VSTEST_HOST_DEBUG", "1" + ] |> dict |> System.Collections.Generic.Dictionary + let vstest = new VsTestConsoleWrapper(vstestPath, consoleParams) + let runHandler = TestRunHandler(incrementalUpdateHandler) + + let options = new TestPlatformOptions() + testCaseFilter |> Option.iter (TestPlatformOptions.withTestCaseFilter options) + + use! _cancel = Async.OnCancel(fun () -> + printfn "Cancelling test run" + vstest.CancelTestRun() + printfn "Test Run Cancelled") + vstest.RunTests(sources, null, options, runHandler) + return runHandler.TestResults |> List.ofSeq + } + + open System.IO let tryFindVsTestFromDotnetRoot (dotnetRoot: string) (workspaceRoot: string option) : Result = let cwd = defaultArg workspaceRoot System.Environment.CurrentDirectory |> DirectoryInfo diff --git a/src/FsAutoComplete/LspServers/AdaptiveServerState.fs b/src/FsAutoComplete/LspServers/AdaptiveServerState.fs index 54c39d126..3afa4505e 100644 --- a/src/FsAutoComplete/LspServers/AdaptiveServerState.fs +++ b/src/FsAutoComplete/LspServers/AdaptiveServerState.fs @@ -2628,15 +2628,20 @@ type AdaptiveState | Some project -> TestServer.TestResult.ofVsTestResult project.ProjectFileName project.TargetFramework testResult |> Some testCases |> List.choose (tryTestResultToDTO projectLookup) - let incrementalUpdateHandler (runUpdate: Microsoft.VisualStudio.TestPlatform.ObjectModel.Client.TestRunChangedEventArgs) = - lspClient.NotifyTestRunUpdate({ - TestResults = runUpdate.NewTestResults |> List.ofSeq |> tryTestResultsToDTOs |> Array.ofSeq - ActiveTests = runUpdate.ActiveTests |> Seq.choose (TestServer.TestItem.tryTestCaseToDTO projectLookup.TryFind) |> Array.ofSeq - }) - |> Async.RunSynchronously - - let testResults = - TestServer.VSTestWrapper.runTests vstestBinary.FullName incrementalUpdateHandler testProjectBinaries testCaseFilter + let incrementalUpdateHandler (runUpdate: TestServer.VSTestWrapper.TestRunUpdate) = + match runUpdate with + | TestServer.VSTestWrapper.TestRunUpdate.Progress progress -> + lspClient.NotifyTestRunUpdate(TestRunUpdateNotification.Progress { + TestResults = progress.NewTestResults |> List.ofSeq |> tryTestResultsToDTOs |> Array.ofSeq + ActiveTests = progress.ActiveTests |> Seq.choose (TestServer.TestItem.tryTestCaseToDTO projectLookup.TryFind) |> Array.ofSeq + }) + |> Async.RunSynchronously + | TestServer.VSTestWrapper.TestRunUpdate.AttachDebugProcess processId -> + () // TODO: + + + let! testResults = + TestServer.VSTestWrapper.runTestsAsync vstestBinary.FullName incrementalUpdateHandler testProjectBinaries testCaseFilter attachDebugger let resultDtos = testResults |> tryTestResultsToDTOs return resultDtos diff --git a/test/FsAutoComplete.Tests.TestExplorer/TestRunTests.fs b/test/FsAutoComplete.Tests.TestExplorer/TestRunTests.fs index 5a4e5949a..c5b396c36 100644 --- a/test/FsAutoComplete.Tests.TestExplorer/TestRunTests.fs +++ b/test/FsAutoComplete.Tests.TestExplorer/TestRunTests.fs @@ -25,7 +25,7 @@ let tests = testList "VSTestWrapper Test Run" [ testCase "should return an empty list if given no projects" <| fun () -> let expected = [] - let actual = VSTestWrapper.runTests vstestPath ignore [] None + let actual = VSTestWrapper.runTests vstestPath ignore [] None false Expect.equal actual expected "" testCase "should be able to report basic test run outcomes" <| fun () -> @@ -42,9 +42,9 @@ let tests = Path.Combine(ResourceLocators.sampleProjectsRootDir, "VSTest.XUnit.RunResults/bin/Debug/net8.0/VSTest.XUnit.RunResults.dll") ] - let runResults = VSTestWrapper.runTests vstestPath ignore sources None + let runResults = VSTestWrapper.runTests vstestPath ignore sources None false - let likenessOfTestResult (result: TestResult) = (result.TestCase.FullyQualifiedName, result.Outcome) + let likenessOfTestResult (result: TestResult) = (result.TestCase.FullyQualifiedName, result.Outcome) let actual = runResults |> List.map likenessOfTestResult Expect.equal (set actual) (set expected) "" @@ -59,10 +59,34 @@ let tests = Path.Combine(ResourceLocators.sampleProjectsRootDir, "VSTest.XUnit.RunResults/bin/Debug/net8.0/VSTest.XUnit.RunResults.dll") ] - let runResults = VSTestWrapper.runTests vstestPath ignore sources (Some "FullyQualifiedName~Tests+Nested") + let runResults = VSTestWrapper.runTests vstestPath ignore sources (Some "FullyQualifiedName~Tests+Nested") false let likenessOfTestResult (result: TestResult) = (result.TestCase.FullyQualifiedName, result.Outcome) let actual = runResults |> List.map likenessOfTestResult Expect.equal (set actual) (set expected) "" + + testCaseAsync "should report processIds when debugging is on" <| async { + use tokenSource = new System.Threading.CancellationTokenSource(2000) + + let mutable actualProcessId : string option = None + let updateSpy (update: VSTestWrapper.TestRunUpdate) = + match update with + | VSTestWrapper.TestRunUpdate.AttachDebugProcess processId -> + actualProcessId <- Some processId + tokenSource.Cancel() + | _ -> () + + use! _c = Async.OnCancel(fun _ -> + tokenSource.Cancel()) + + Expect.throws (fun () -> + let sources = [ + Path.Combine(ResourceLocators.sampleProjectsRootDir, "VSTest.XUnit.RunResults/bin/Debug/net8.0/VSTest.XUnit.RunResults.dll") + ] + Async.RunSynchronously (VSTestWrapper.runTestsAsync vstestPath updateSpy sources None true, 2000, tokenSource.Token) |> ignore + ) "" + + Expect.isSome actualProcessId "Expected runTest to report a processId" + } ] From 7de08328803796a82e61fcf5db610ce25afdab6f Mon Sep 17 00:00:00 2001 From: farlee2121 <2847259+farlee2121@users.noreply.github.com> Date: Wed, 6 Aug 2025 08:34:44 -0500 Subject: [PATCH 18/39] Convert VsTestWrapper methods to async-only for graceful cancellation --- src/FsAutoComplete.Core/TestServer.fs | 35 +++++++------------ .../LspServers/AdaptiveServerState.fs | 7 ++-- .../TestDiscoveryTests.fs | 11 +++--- .../TestRunTests.fs | 15 ++++---- 4 files changed, 31 insertions(+), 37 deletions(-) diff --git a/src/FsAutoComplete.Core/TestServer.fs b/src/FsAutoComplete.Core/TestServer.fs index cb3a73b0f..02a937e6b 100644 --- a/src/FsAutoComplete.Core/TestServer.fs +++ b/src/FsAutoComplete.Core/TestServer.fs @@ -32,13 +32,17 @@ module VSTestWrapper = member this.HandleRawMessage (_rawMessage: string): unit = () - let discoverTests (vstestPath: string) (incrementalUpdateHandler: TestCase list -> unit) (sources: TestProjectDll list) : TestCase list = - let consoleParams = ConsoleParameters() - let vstest = new VsTestConsoleWrapper(vstestPath, consoleParams) - let discoveryHandler = TestDiscoveryHandler(incrementalUpdateHandler) - - vstest.DiscoverTests(sources, null, discoveryHandler) - discoveryHandler.DiscoveredTests |> List.ofSeq + let discoverTestsAsync (vstestPath: string) (incrementalUpdateHandler: TestCase list -> unit) (sources: TestProjectDll list) : Async = + async { + let consoleParams = ConsoleParameters() + let vstest = new VsTestConsoleWrapper(vstestPath, consoleParams) + let discoveryHandler = TestDiscoveryHandler(incrementalUpdateHandler) + + use! _onCancel = Async.OnCancel(fun () -> vstest.CancelDiscovery()) + + vstest.DiscoverTests(sources, null, discoveryHandler) + return discoveryHandler.DiscoveredTests |> List.ofSeq + } type ProcessId = string type TestRunUpdate = @@ -48,7 +52,6 @@ module VSTestWrapper = type TestRunHandler(notifyIncrementalUpdate: TestRunUpdate -> unit) = let debugProcessIdRegex = Regex(@"Process Id: (.*),") - let tryGetDebugProcessId consoleOutput = let m = debugProcessIdRegex.Match(consoleOutput) @@ -86,21 +89,6 @@ module VSTestWrapper = let withTestCaseFilter (options: TestPlatformOptions) filterExpression = options.TestCaseFilter <- filterExpression - let runTests (vstestPath: string) (incrementalUpdateHandler: TestRunUpdate -> unit) (sources: TestProjectDll list) (testCaseFilter: string option) (shouldDebug: bool): TestResult list = - let consoleParams = ConsoleParameters() - if shouldDebug then - consoleParams.EnvironmentVariables <- [ - "VSTEST_HOST_DEBUG", "1" - ] |> dict |> System.Collections.Generic.Dictionary - let vstest = new VsTestConsoleWrapper(vstestPath, consoleParams) - let runHandler = TestRunHandler(incrementalUpdateHandler) - - let options = new TestPlatformOptions() - testCaseFilter |> Option.iter (TestPlatformOptions.withTestCaseFilter options) - - vstest.RunTests(sources, null, options, runHandler) - runHandler.TestResults |> List.ofSeq - let runTestsAsync (vstestPath: string) (incrementalUpdateHandler: TestRunUpdate -> unit) (sources: TestProjectDll list) (testCaseFilter: string option) (shouldDebug: bool) : Async = async { let consoleParams = ConsoleParameters() @@ -118,6 +106,7 @@ module VSTestWrapper = printfn "Cancelling test run" vstest.CancelTestRun() printfn "Test Run Cancelled") + vstest.RunTests(sources, null, options, runHandler) return runHandler.TestResults |> List.ofSeq } diff --git a/src/FsAutoComplete/LspServers/AdaptiveServerState.fs b/src/FsAutoComplete/LspServers/AdaptiveServerState.fs index 3afa4505e..8f7739039 100644 --- a/src/FsAutoComplete/LspServers/AdaptiveServerState.fs +++ b/src/FsAutoComplete/LspServers/AdaptiveServerState.fs @@ -2604,8 +2604,8 @@ type AdaptiveState lspClient.NotifyTestDiscoveryUpdate({ Tests = tests |> tryTestCasesToDTOs |> Array.ofList}) |> Async.RunSynchronously - let testCases = - TestServer.VSTestWrapper.discoverTests vstestBinary.FullName incrementalUpdateHandler testProjectBinaries + let! testCases = + TestServer.VSTestWrapper.discoverTestsAsync vstestBinary.FullName incrementalUpdateHandler testProjectBinaries let testDTOs : TestServer.TestItem list = testCases |> tryTestCasesToDTOs @@ -2639,9 +2639,8 @@ type AdaptiveState | TestServer.VSTestWrapper.TestRunUpdate.AttachDebugProcess processId -> () // TODO: - let! testResults = - TestServer.VSTestWrapper.runTestsAsync vstestBinary.FullName incrementalUpdateHandler testProjectBinaries testCaseFilter attachDebugger + TestServer.VSTestWrapper.runTestsAsync vstestBinary.FullName incrementalUpdateHandler testProjectBinaries testCaseFilter false let resultDtos = testResults |> tryTestResultsToDTOs return resultDtos diff --git a/test/FsAutoComplete.Tests.TestExplorer/TestDiscoveryTests.fs b/test/FsAutoComplete.Tests.TestExplorer/TestDiscoveryTests.fs index 872a9f5ff..f5cfbc326 100644 --- a/test/FsAutoComplete.Tests.TestExplorer/TestDiscoveryTests.fs +++ b/test/FsAutoComplete.Tests.TestExplorer/TestDiscoveryTests.fs @@ -11,20 +11,23 @@ let vstestPath = ResourceLocators.tryFindVsTest () [] let tests = testList "VSTestWrapper Test Discovery" [ - testCase "should return an empty list if given no projects" <| fun () -> + testCaseAsync "should return an empty list if given no projects" <| async { let expected = [] - let actual = VSTestWrapper.discoverTests vstestPath ignore [] + let! actual = VSTestWrapper.discoverTestsAsync vstestPath ignore [] Expect.equal actual expected "" + } - testCase "should discover tests given a single xunit project" <| fun () -> + testCaseAsync "should discover tests given a single xunit project" <| async { let expectedTestIds = ["Tests.My test"] let sources = [ Path.Combine(ResourceLocators.sampleProjectsRootDir, "VSTest.XUnit.Tests/bin/Debug/net8.0/VSTest.XUnit.Tests.dll") ] - let discovered = VSTestWrapper.discoverTests vstestPath ignore sources + let! discovered = VSTestWrapper.discoverTestsAsync vstestPath ignore sources let actual = discovered |> List.map _.FullyQualifiedName Expect.equal actual expectedTestIds "" + } + ] diff --git a/test/FsAutoComplete.Tests.TestExplorer/TestRunTests.fs b/test/FsAutoComplete.Tests.TestExplorer/TestRunTests.fs index c5b396c36..0bd4f149a 100644 --- a/test/FsAutoComplete.Tests.TestExplorer/TestRunTests.fs +++ b/test/FsAutoComplete.Tests.TestExplorer/TestRunTests.fs @@ -23,12 +23,13 @@ let vstestPath = tryFindVsTest () [] let tests = testList "VSTestWrapper Test Run" [ - testCase "should return an empty list if given no projects" <| fun () -> + testCaseAsync "should return an empty list if given no projects" <| async { let expected = [] - let actual = VSTestWrapper.runTests vstestPath ignore [] None false + let! actual = VSTestWrapper.runTestsAsync vstestPath ignore [] None false Expect.equal actual expected "" + } - testCase "should be able to report basic test run outcomes" <| fun () -> + testCaseAsync "should be able to report basic test run outcomes" <| async { let expected = [ ("Tests.My test", TestOutcome.Passed) ("Tests.Fails", TestOutcome.Failed) @@ -42,14 +43,15 @@ let tests = Path.Combine(ResourceLocators.sampleProjectsRootDir, "VSTest.XUnit.RunResults/bin/Debug/net8.0/VSTest.XUnit.RunResults.dll") ] - let runResults = VSTestWrapper.runTests vstestPath ignore sources None false + let! runResults = VSTestWrapper.runTestsAsync vstestPath ignore sources None false let likenessOfTestResult (result: TestResult) = (result.TestCase.FullyQualifiedName, result.Outcome) let actual = runResults |> List.map likenessOfTestResult Expect.equal (set actual) (set expected) "" + } - testCase "should run only tests that match the case filter" <| fun () -> + testCaseAsync "should run only tests that match the case filter" <| async { let expected = [ ("Tests+Nested.Test 1", TestOutcome.Passed) ("Tests+Nested.Test 2", TestOutcome.Passed) @@ -59,12 +61,13 @@ let tests = Path.Combine(ResourceLocators.sampleProjectsRootDir, "VSTest.XUnit.RunResults/bin/Debug/net8.0/VSTest.XUnit.RunResults.dll") ] - let runResults = VSTestWrapper.runTests vstestPath ignore sources (Some "FullyQualifiedName~Tests+Nested") false + let! runResults = VSTestWrapper.runTestsAsync vstestPath ignore sources (Some "FullyQualifiedName~Tests+Nested") false let likenessOfTestResult (result: TestResult) = (result.TestCase.FullyQualifiedName, result.Outcome) let actual = runResults |> List.map likenessOfTestResult Expect.equal (set actual) (set expected) "" + } testCaseAsync "should report processIds when debugging is on" <| async { use tokenSource = new System.Threading.CancellationTokenSource(2000) From 80c6eada83cdb49f2b50d7ae19c6ba05c9a088f5 Mon Sep 17 00:00:00 2001 From: farlee2121 <2847259+farlee2121@users.noreply.github.com> Date: Wed, 6 Aug 2025 16:53:30 -0500 Subject: [PATCH 19/39] Pass a server-level test for the attaching debugger to test run. --- src/FsAutoComplete.Core/TestServer.fs | 7 +- src/FsAutoComplete/LspHelpers.fs | 9 ++- src/FsAutoComplete/LspHelpers.fsi | 9 ++- .../LspServers/AdaptiveFSharpLspServer.fs | 2 +- .../LspServers/AdaptiveServerState.fs | 33 +++++++--- .../LspServers/AdaptiveServerState.fsi | 2 +- .../LspServers/FSharpLspClient.fs | 6 +- .../FsAutoComplete.Tests.Lsp.fsproj | 2 +- test/FsAutoComplete.Tests.Lsp/Program.fs | 2 + .../TestExplorerTests.fs | 64 +++++++++++++++++++ .../TestRunTests.fs | 28 +++++++- 11 files changed, 140 insertions(+), 24 deletions(-) create mode 100644 test/FsAutoComplete.Tests.Lsp/TestExplorerTests.fs diff --git a/src/FsAutoComplete.Core/TestServer.fs b/src/FsAutoComplete.Core/TestServer.fs index 02a937e6b..73b45247c 100644 --- a/src/FsAutoComplete.Core/TestServer.fs +++ b/src/FsAutoComplete.Core/TestServer.fs @@ -8,7 +8,7 @@ module VSTestWrapper = open Microsoft.VisualStudio.TestPlatform.ObjectModel.Client; open Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging; open System.Text.RegularExpressions - + type TestProjectDll = string type private TestDiscoveryHandler (notifyIncrementalUpdate: TestCase list -> unit) = @@ -47,7 +47,7 @@ module VSTestWrapper = type ProcessId = string type TestRunUpdate = | Progress of TestRunChangedEventArgs - | AttachDebugProcess of ProcessId + | ProcessWaitingForDebugger of ProcessId type TestRunHandler(notifyIncrementalUpdate: TestRunUpdate -> unit) = @@ -66,7 +66,7 @@ module VSTestWrapper = interface ITestRunEventsHandler with member _.HandleLogMessage (_level: TestMessageLevel, message: string): unit = match tryGetDebugProcessId message with - | Some processId -> notifyIncrementalUpdate (AttachDebugProcess processId) + | Some processId -> notifyIncrementalUpdate (ProcessWaitingForDebugger processId) | None -> () member _.HandleRawMessage (_rawMessage: string): unit = @@ -88,6 +88,7 @@ module VSTestWrapper = module TestPlatformOptions = let withTestCaseFilter (options: TestPlatformOptions) filterExpression = options.TestCaseFilter <- filterExpression + let runTestsAsync (vstestPath: string) (incrementalUpdateHandler: TestRunUpdate -> unit) (sources: TestProjectDll list) (testCaseFilter: string option) (shouldDebug: bool) : Async = async { diff --git a/src/FsAutoComplete/LspHelpers.fs b/src/FsAutoComplete/LspHelpers.fs index e1d296f14..e5e88d07e 100644 --- a/src/FsAutoComplete/LspHelpers.fs +++ b/src/FsAutoComplete/LspHelpers.fs @@ -613,15 +613,20 @@ type TestDetectedNotification = Tests: TestAdapter.TestAdapterEntry array } type TestRunRequest = - { TestCaseFilter: string option } + { TestCaseFilter: string option + AttachDebugger: bool} type TestDiscoveryUpdateNotification = { Tests: TestServer.TestItem array } -type TestRunUpdateNotification = +type TestRunProgress = { TestResults: TestServer.TestResult array ActiveTests: TestServer.TestItem array } +type TestRunUpdateNotification = + | Progress of TestRunProgress + | ProcessWaitingForDebugger of processId: string + type ProjectParms = { /// Project file to compile diff --git a/src/FsAutoComplete/LspHelpers.fsi b/src/FsAutoComplete/LspHelpers.fsi index f3e54bb8e..791bec7da 100644 --- a/src/FsAutoComplete/LspHelpers.fsi +++ b/src/FsAutoComplete/LspHelpers.fsi @@ -199,15 +199,20 @@ type TestDetectedNotification = Tests: TestAdapter.TestAdapterEntry array } type TestRunRequest = - { TestCaseFilter: string option } + { TestCaseFilter: string option + AttachDebugger: bool} type TestDiscoveryUpdateNotification = { Tests: TestServer.TestItem array } -type TestRunUpdateNotification = +type TestRunProgress = { TestResults: TestServer.TestResult array ActiveTests: TestServer.TestItem array } +type TestRunUpdateNotification = + | Progress of TestRunProgress + | ProcessWaitingForDebugger of processId: string + type ProjectParms = { /// Project file to compile diff --git a/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs b/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs index abd670c9d..f6d003547 100644 --- a/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs +++ b/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs @@ -3065,7 +3065,7 @@ type AdaptiveFSharpLspServer override this.TestRunTests (p: TestRunRequest): Async> = asyncResult { - let! testDTOs = state.RunTests(p.TestCaseFilter) |> AsyncResult.mapError (fun msg -> JsonRpc.Error.InternalError msg) + let! testDTOs = state.RunTests p.TestCaseFilter p.AttachDebugger |> AsyncResult.mapError (fun msg -> JsonRpc.Error.InternalError msg) return Some { Content = CommandResponse.runTests FsAutoComplete.JsonSerializer.writeJson testDTOs} } diff --git a/src/FsAutoComplete/LspServers/AdaptiveServerState.fs b/src/FsAutoComplete/LspServers/AdaptiveServerState.fs index 8f7739039..6d06aad3d 100644 --- a/src/FsAutoComplete/LspServers/AdaptiveServerState.fs +++ b/src/FsAutoComplete/LspServers/AdaptiveServerState.fs @@ -2613,7 +2613,7 @@ type AdaptiveState return testDTOs } - member state.RunTests (testCaseFilter: string option) = + member state.RunTests (testCaseFilter: string option) (attachDebugger: bool) = asyncResult { let! vstestBinary = TestServer.VSTestWrapper.tryFindVsTestFromDotnetRoot state.Config.DotNetRoot state.RootPath let! testProjects = TestProjectHelpers.tryGetTestProjects workspaceLoader state.WorkspacePaths @@ -2628,20 +2628,33 @@ type AdaptiveState | Some project -> TestServer.TestResult.ofVsTestResult project.ProjectFileName project.TargetFramework testResult |> Some testCases |> List.choose (tryTestResultToDTO projectLookup) + use tokenSource = new CancellationTokenSource() + use! _onCancel = Async.OnCancel(fun _ -> + printfn "Sya: cancelling update handlers" + tokenSource.Cancel() + printfn "Sya: update handlers cancelled") let incrementalUpdateHandler (runUpdate: TestServer.VSTestWrapper.TestRunUpdate) = - match runUpdate with - | TestServer.VSTestWrapper.TestRunUpdate.Progress progress -> - lspClient.NotifyTestRunUpdate(TestRunUpdateNotification.Progress { + let dto = + match runUpdate with + | TestServer.VSTestWrapper.TestRunUpdate.Progress progress -> + TestRunUpdateNotification.Progress { TestResults = progress.NewTestResults |> List.ofSeq |> tryTestResultsToDTOs |> Array.ofSeq ActiveTests = progress.ActiveTests |> Seq.choose (TestServer.TestItem.tryTestCaseToDTO projectLookup.TryFind) |> Array.ofSeq - }) - |> Async.RunSynchronously - | TestServer.VSTestWrapper.TestRunUpdate.AttachDebugProcess processId -> - () // TODO: - + } + | TestServer.VSTestWrapper.TestRunUpdate.ProcessWaitingForDebugger processId -> + printfn $"Sya: processId reported: {processId}" + TestRunUpdateNotification.ProcessWaitingForDebugger processId + Async.RunSynchronously(async { + use! _c = Async.OnCancel(fun _ -> printfn "Sya: Cancelled notification") + do! lspClient.NotifyTestRunUpdate(dto) + }, cancellationToken = tokenSource.Token) + + printfn "Sya: running tests" let! testResults = - TestServer.VSTestWrapper.runTestsAsync vstestBinary.FullName incrementalUpdateHandler testProjectBinaries testCaseFilter false + TestServer.VSTestWrapper.runTestsAsync vstestBinary.FullName incrementalUpdateHandler testProjectBinaries testCaseFilter attachDebugger + printfn "Sya: test results returned" + let resultDtos = testResults |> tryTestResultsToDTOs return resultDtos } diff --git a/src/FsAutoComplete/LspServers/AdaptiveServerState.fsi b/src/FsAutoComplete/LspServers/AdaptiveServerState.fsi index d27332291..3a4b0620d 100644 --- a/src/FsAutoComplete/LspServers/AdaptiveServerState.fsi +++ b/src/FsAutoComplete/LspServers/AdaptiveServerState.fsi @@ -120,7 +120,7 @@ type AdaptiveState = member GetAllDeclarations: unit -> Async<(string * NavigationTopLevelDeclaration array) array> member GlyphToSymbolKind: (FSharpGlyph -> SymbolKind option) member DiscoverTests: unit -> Async> - member RunTests: string option -> Async> + member RunTests: string option -> bool -> Async> /// /// Signals the server to cancel an operation that is associated with the given progress token. /// diff --git a/src/FsAutoComplete/LspServers/FSharpLspClient.fs b/src/FsAutoComplete/LspServers/FSharpLspClient.fs index a2858ad77..b5dd5d4f2 100644 --- a/src/FsAutoComplete/LspServers/FSharpLspClient.fs +++ b/src/FsAutoComplete/LspServers/FSharpLspClient.fs @@ -66,7 +66,11 @@ type FSharpLspClient(sendServerNotification: ClientNotificationSender, sendServe sendServerNotification "test/testDiscoveryUpdate" (box { Content = JsonSerializer.writeJson p}) |> Async.Ignore member __.NotifyTestRunUpdate(p: TestRunUpdateNotification) = - sendServerNotification "test/testRunUpdate" (box { Content = JsonSerializer.writeJson p}) |> Async.Ignore + match p with + | Progress progress -> + sendServerNotification "test/testRunProgressUpdate" (box { Content = JsonSerializer.writeJson progress}) |> Async.Ignore + | ProcessWaitingForDebugger processId -> + sendServerNotification "test/processWaitingForDebugger" (box { Content = JsonSerializer.writeJson processId}) |> Async.Ignore member x.CodeLensRefresh() = match x.ClientCapabilities with diff --git a/test/FsAutoComplete.Tests.Lsp/FsAutoComplete.Tests.Lsp.fsproj b/test/FsAutoComplete.Tests.Lsp/FsAutoComplete.Tests.Lsp.fsproj index 14c0002c5..b098ce207 100644 --- a/test/FsAutoComplete.Tests.Lsp/FsAutoComplete.Tests.Lsp.fsproj +++ b/test/FsAutoComplete.Tests.Lsp/FsAutoComplete.Tests.Lsp.fsproj @@ -58,4 +58,4 @@ - + \ No newline at end of file diff --git a/test/FsAutoComplete.Tests.Lsp/Program.fs b/test/FsAutoComplete.Tests.Lsp/Program.fs index 6736ea79a..aa1b73618 100644 --- a/test/FsAutoComplete.Tests.Lsp/Program.fs +++ b/test/FsAutoComplete.Tests.Lsp/Program.fs @@ -137,6 +137,8 @@ let lspTests = EmptyFileTests.tests createServer CallHierarchy.tests createServer diagnosticsTest createServer + + TestExplorer.tests createServer ] ] ] /// Tests that do not require a LSP server diff --git a/test/FsAutoComplete.Tests.Lsp/TestExplorerTests.fs b/test/FsAutoComplete.Tests.Lsp/TestExplorerTests.fs new file mode 100644 index 000000000..a3ce01922 --- /dev/null +++ b/test/FsAutoComplete.Tests.Lsp/TestExplorerTests.fs @@ -0,0 +1,64 @@ +module FsAutoComplete.Tests.TestExplorer + +open Expecto +open Helpers +open System.IO +open FsAutoComplete.LspHelpers +open System.Threading +open Helpers.Expecto.ShadowedTimeouts + +let tests createServer = + let initializeServer workspaceRoot = + async { + let! (server, event) = serverInitialize workspaceRoot defaultConfigDto createServer + do! waitForWorkspaceFinishedParsing event + + return (server, event) + } |> Async.Cache + + testSequenced <| testList "TestExplorerTests" + [ testCaseAsync "it should report a processId when debug a test project" <| async { + // let workspaceRoot = Path.Combine(__SOURCE_DIRECTORY__, "TestCases", "XunitTests") + //X:\source\dotnet\FsAutoComplete\test\FsAutoComplete.Tests.Lsp\TestCases\XunitTests\bin\Debug\net8.0 + //X:\source\dotnet\FsAutoComplete\test\SampleTestProjects\VSTest.XUnit.RunResults\bin\Debug\net8.0 + let workspaceRoot = Path.Combine(__SOURCE_DIRECTORY__, "..", "SampleTestProjects", "VSTest.XUnit.RunResults") + let! server, clientNotifications = initializeServer workspaceRoot + + use server = server + + use tokenSource = new CancellationTokenSource() + let mutable processIdSpy : string option = None + use! _onCancel = Async.OnCancel(fun () -> tokenSource.Cancel()) + use _ = clientNotifications.Subscribe(fun (msgType: string, data: obj) -> + if msgType = "test/processWaitingForDebugger" then + printfn $"Sya: update message {data}" + let processId : string = data :?> PlainNotification |> _.Content |> FsAutoComplete.JsonSerializer.readJson + processIdSpy <- Some processId + tokenSource.Cancel() + let tryParseProcessId (str: string) = + let (success, value) = System.Int32.TryParse(str) + printfn $"Sya: parsed process id: {success}, {value}. Original: {str}" + if success then Some value else None + + printfn $"Sya: process spy {processIdSpy}" + processId |> tryParseProcessId |> Option.iter(fun pid -> + try + printfn $"Sya: trying to kill process {pid}" + System.Diagnostics.Process.GetProcessById(pid).Kill(true) + printfn $"Sya: killed process {pid}" + with e -> + printfn $"Sya: failed to kill process {pid}" + ) + ) + Expect.throws (fun () -> + let runRequest : TestRunRequest = { + TestCaseFilter = None + AttachDebugger = true + } + Async.RunSynchronously(server.TestRunTests(runRequest) |> Async.Ignore, cancellationToken = tokenSource.Token) + ) "" + printfn "Sya: Server test run closed" + Expect.isSome processIdSpy "" + printfn "Sya: test complete" + }] + diff --git a/test/FsAutoComplete.Tests.TestExplorer/TestRunTests.fs b/test/FsAutoComplete.Tests.TestExplorer/TestRunTests.fs index 0bd4f149a..969caf52f 100644 --- a/test/FsAutoComplete.Tests.TestExplorer/TestRunTests.fs +++ b/test/FsAutoComplete.Tests.TestExplorer/TestRunTests.fs @@ -5,7 +5,6 @@ open FsAutoComplete.TestServer open Microsoft.VisualStudio.TestPlatform.ObjectModel; open System.IO - let tryFindVsTest () : string = let dotnetBinary = Ionide.ProjInfo.Paths.dotnetRoot.Value @@ -75,7 +74,7 @@ let tests = let mutable actualProcessId : string option = None let updateSpy (update: VSTestWrapper.TestRunUpdate) = match update with - | VSTestWrapper.TestRunUpdate.AttachDebugProcess processId -> + | VSTestWrapper.TestRunUpdate.ProcessWaitingForDebugger processId -> actualProcessId <- Some processId tokenSource.Cancel() | _ -> () @@ -87,9 +86,32 @@ let tests = let sources = [ Path.Combine(ResourceLocators.sampleProjectsRootDir, "VSTest.XUnit.RunResults/bin/Debug/net8.0/VSTest.XUnit.RunResults.dll") ] - Async.RunSynchronously (VSTestWrapper.runTestsAsync vstestPath updateSpy sources None true, 2000, tokenSource.Token) |> ignore + Async.RunSynchronously (VSTestWrapper.runTestsAsync vstestPath updateSpy sources None true, cancellationToken = tokenSource.Token) |> ignore ) "" Expect.isSome actualProcessId "Expected runTest to report a processId" } + + testCaseAsync "should report a processId only once per process" <| async { + use tokenSource = new System.Threading.CancellationTokenSource(1000) + + let mutable reportedProcessIds : string list = [] + let updateSpy (update: VSTestWrapper.TestRunUpdate) = + match update with + | VSTestWrapper.TestRunUpdate.ProcessWaitingForDebugger processId -> + reportedProcessIds <- processId :: reportedProcessIds + | _ -> () + + use! _c = Async.OnCancel(fun _ -> + tokenSource.Cancel()) + + Expect.throws (fun () -> + let sources = [ + Path.Combine(ResourceLocators.sampleProjectsRootDir, "VSTest.XUnit.RunResults/bin/Debug/net8.0/VSTest.XUnit.RunResults.dll") + ] + Async.RunSynchronously (VSTestWrapper.runTestsAsync vstestPath updateSpy sources None true, cancellationToken = tokenSource.Token) |> ignore + ) "" + + Expect.hasLength reportedProcessIds 1 "Expected runTest to report a processId" + } ] From 8c99c72e73eecbfa253c078c7d0e4b142f03c0f3 Mon Sep 17 00:00:00 2001 From: farlee2121 <2847259+farlee2121@users.noreply.github.com> Date: Thu, 7 Aug 2025 08:57:28 -0500 Subject: [PATCH 20/39] Format files --- src/FsAutoComplete.Core/TestServer.fs | 454 ++++++++++-------- src/FsAutoComplete/LspHelpers.fs | 11 +- .../LspServers/AdaptiveFSharpLspServer.fs | 20 +- .../LspServers/AdaptiveServerState.fs | 109 +++-- .../LspServers/FSharpLspClient.fs | 13 +- test/FsAutoComplete.Tests.Lsp/Program.fs | 79 ++- .../TestExplorerTests.fs | 128 ++--- .../TestRunTests.fs | 223 +++++---- 8 files changed, 570 insertions(+), 467 deletions(-) diff --git a/src/FsAutoComplete.Core/TestServer.fs b/src/FsAutoComplete.Core/TestServer.fs index 73b45247c..0ba7586ac 100644 --- a/src/FsAutoComplete.Core/TestServer.fs +++ b/src/FsAutoComplete.Core/TestServer.fs @@ -2,222 +2,254 @@ namespace FsAutoComplete.TestServer open System -module VSTestWrapper = - open Microsoft.TestPlatform.VsTestConsole.TranslationLayer; - open Microsoft.VisualStudio.TestPlatform.ObjectModel; - open Microsoft.VisualStudio.TestPlatform.ObjectModel.Client; - open Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging; - open System.Text.RegularExpressions - - type TestProjectDll = string - - type private TestDiscoveryHandler (notifyIncrementalUpdate: TestCase list -> unit) = - - member val DiscoveredTests : TestCase ResizeArray = ResizeArray() with get,set - - interface ITestDiscoveryEventsHandler with - member this.HandleDiscoveredTests (discoveredTestCases: System.Collections.Generic.IEnumerable): unit = - if (not << isNull) discoveredTestCases then - this.DiscoveredTests.AddRange(discoveredTestCases) - notifyIncrementalUpdate (discoveredTestCases |> List.ofSeq) - - member this.HandleDiscoveryComplete (_totalTests: int64, lastChunk: System.Collections.Generic.IEnumerable, _isAborted: bool): unit = - if (not << isNull) lastChunk then - this.DiscoveredTests.AddRange(lastChunk) - notifyIncrementalUpdate (lastChunk |> List.ofSeq) - - member this.HandleLogMessage (_level: TestMessageLevel, _message: string): unit = - () - - member this.HandleRawMessage (_rawMessage: string): unit = - () - - let discoverTestsAsync (vstestPath: string) (incrementalUpdateHandler: TestCase list -> unit) (sources: TestProjectDll list) : Async = - async { - let consoleParams = ConsoleParameters() - let vstest = new VsTestConsoleWrapper(vstestPath, consoleParams) - let discoveryHandler = TestDiscoveryHandler(incrementalUpdateHandler) - - use! _onCancel = Async.OnCancel(fun () -> vstest.CancelDiscovery()) - - vstest.DiscoverTests(sources, null, discoveryHandler) - return discoveryHandler.DiscoveredTests |> List.ofSeq - } - - type ProcessId = string - type TestRunUpdate = - | Progress of TestRunChangedEventArgs - | ProcessWaitingForDebugger of ProcessId - - type TestRunHandler(notifyIncrementalUpdate: TestRunUpdate -> unit) = - - let debugProcessIdRegex = Regex(@"Process Id: (.*),") - let tryGetDebugProcessId consoleOutput = - let m = debugProcessIdRegex.Match(consoleOutput) - - if m.Success then - let processId = m.Groups.[1].Value - Some processId - else - None - - member val TestResults : TestResult ResizeArray = ResizeArray() with get,set - - interface ITestRunEventsHandler with - member _.HandleLogMessage (_level: TestMessageLevel, message: string): unit = - match tryGetDebugProcessId message with - | Some processId -> notifyIncrementalUpdate (ProcessWaitingForDebugger processId) - | None -> () - - member _.HandleRawMessage (_rawMessage: string): unit = - () - - member this.HandleTestRunComplete (_testRunCompleteArgs: TestRunCompleteEventArgs, lastChunkArgs: TestRunChangedEventArgs, _runContextAttachments: System.Collections.Generic.ICollection, _executorUris: System.Collections.Generic.ICollection): unit = - if((not << isNull) lastChunkArgs && (not << isNull) lastChunkArgs.NewTestResults) then - this.TestResults.AddRange(lastChunkArgs.NewTestResults) - notifyIncrementalUpdate (Progress lastChunkArgs) - - member this.HandleTestRunStatsChange (testRunChangedArgs: TestRunChangedEventArgs): unit = - if((not << isNull) testRunChangedArgs && (not << isNull) testRunChangedArgs.NewTestResults) then - this.TestResults.AddRange(testRunChangedArgs.NewTestResults) - notifyIncrementalUpdate (Progress testRunChangedArgs) - - member _.LaunchProcessWithDebuggerAttached (_testProcessStartInfo: TestProcessStartInfo): int = - raise (System.NotImplementedException()) - - module TestPlatformOptions = - let withTestCaseFilter (options: TestPlatformOptions) filterExpression = - options.TestCaseFilter <- filterExpression - - - let runTestsAsync (vstestPath: string) (incrementalUpdateHandler: TestRunUpdate -> unit) (sources: TestProjectDll list) (testCaseFilter: string option) (shouldDebug: bool) : Async = - async { - let consoleParams = ConsoleParameters() - if shouldDebug then - consoleParams.EnvironmentVariables <- [ - "VSTEST_HOST_DEBUG", "1" - ] |> dict |> System.Collections.Generic.Dictionary - let vstest = new VsTestConsoleWrapper(vstestPath, consoleParams) - let runHandler = TestRunHandler(incrementalUpdateHandler) - - let options = new TestPlatformOptions() - testCaseFilter |> Option.iter (TestPlatformOptions.withTestCaseFilter options) - - use! _cancel = Async.OnCancel(fun () -> - printfn "Cancelling test run" - vstest.CancelTestRun() - printfn "Test Run Cancelled") - - vstest.RunTests(sources, null, options, runHandler) - return runHandler.TestResults |> List.ofSeq - } - - - open System.IO - let tryFindVsTestFromDotnetRoot (dotnetRoot: string) (workspaceRoot: string option) : Result = - let cwd = defaultArg workspaceRoot System.Environment.CurrentDirectory |> DirectoryInfo - let dotnetBinary = - if dotnetRoot |> Directory.Exists then - if System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Windows) - then - FileInfo(Path.Combine(dotnetRoot, "dotnet.exe")) - else - FileInfo(Path.Combine(dotnetRoot, "dotnet")) - else dotnetRoot |> FileInfo - - match Ionide.ProjInfo.SdkDiscovery.versionAt cwd dotnetBinary with - | Ok sdkVersion -> - let sdks = Ionide.ProjInfo.SdkDiscovery.sdks dotnetBinary - - match sdks |> Array.tryFind (fun sdk -> sdk.Version = sdkVersion) with - | Some sdk -> - let vstestBinary = Path.Combine(sdk.Path.FullName, "vstest.console.dll") |> FileInfo - if vstestBinary.Exists - then Ok vstestBinary - else Error $"Found the correct dotnet sdk, but vstest was not at the expected sub-path: {vstestBinary.FullName}" - | None -> Error $"Couldn't find the install location for dotnet sdk version: {sdkVersion}" - | Error _ -> Error $"Couldn't identify the dotnet version for working directory: {cwd.FullName}" - - -type TestFileRange = { - StartLine: int - EndLine: int - } -type TestItem = { - FullName : string - DisplayName : string +module VSTestWrapper = + open Microsoft.TestPlatform.VsTestConsole.TranslationLayer + open Microsoft.VisualStudio.TestPlatform.ObjectModel + open Microsoft.VisualStudio.TestPlatform.ObjectModel.Client + open Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging + open System.Text.RegularExpressions + + type TestProjectDll = string + + type private TestDiscoveryHandler(notifyIncrementalUpdate: TestCase list -> unit) = + + member val DiscoveredTests: TestCase ResizeArray = ResizeArray() with get, set + + interface ITestDiscoveryEventsHandler with + member this.HandleDiscoveredTests(discoveredTestCases: System.Collections.Generic.IEnumerable) : unit = + if (not << isNull) discoveredTestCases then + this.DiscoveredTests.AddRange(discoveredTestCases) + notifyIncrementalUpdate (discoveredTestCases |> List.ofSeq) + + member this.HandleDiscoveryComplete + (_totalTests: int64, lastChunk: System.Collections.Generic.IEnumerable, _isAborted: bool) + : unit = + if (not << isNull) lastChunk then + this.DiscoveredTests.AddRange(lastChunk) + notifyIncrementalUpdate (lastChunk |> List.ofSeq) + + member this.HandleLogMessage(_level: TestMessageLevel, _message: string) : unit = () + + member this.HandleRawMessage(_rawMessage: string) : unit = () + + let discoverTestsAsync + (vstestPath: string) + (incrementalUpdateHandler: TestCase list -> unit) + (sources: TestProjectDll list) + : Async = + async { + let consoleParams = ConsoleParameters() + let vstest = new VsTestConsoleWrapper(vstestPath, consoleParams) + let discoveryHandler = TestDiscoveryHandler(incrementalUpdateHandler) + + use! _onCancel = Async.OnCancel(fun () -> vstest.CancelDiscovery()) + + vstest.DiscoverTests(sources, null, discoveryHandler) + return discoveryHandler.DiscoveredTests |> List.ofSeq + } + + type ProcessId = string + + type TestRunUpdate = + | Progress of TestRunChangedEventArgs + | ProcessWaitingForDebugger of ProcessId + + type TestRunHandler(notifyIncrementalUpdate: TestRunUpdate -> unit) = + + let debugProcessIdRegex = Regex(@"Process Id: (.*),") + + let tryGetDebugProcessId consoleOutput = + let m = debugProcessIdRegex.Match(consoleOutput) + + if m.Success then + let processId = m.Groups.[1].Value + Some processId + else + None + + member val TestResults: TestResult ResizeArray = ResizeArray() with get, set + + interface ITestRunEventsHandler with + member _.HandleLogMessage(_level: TestMessageLevel, message: string) : unit = + match tryGetDebugProcessId message with + | Some processId -> notifyIncrementalUpdate (ProcessWaitingForDebugger processId) + | None -> () + + member _.HandleRawMessage(_rawMessage: string) : unit = () + + member this.HandleTestRunComplete + ( + _testRunCompleteArgs: TestRunCompleteEventArgs, + lastChunkArgs: TestRunChangedEventArgs, + _runContextAttachments: System.Collections.Generic.ICollection, + _executorUris: System.Collections.Generic.ICollection + ) : unit = + if ((not << isNull) lastChunkArgs && (not << isNull) lastChunkArgs.NewTestResults) then + this.TestResults.AddRange(lastChunkArgs.NewTestResults) + notifyIncrementalUpdate (Progress lastChunkArgs) + + member this.HandleTestRunStatsChange(testRunChangedArgs: TestRunChangedEventArgs) : unit = + if + ((not << isNull) testRunChangedArgs + && (not << isNull) testRunChangedArgs.NewTestResults) + then + this.TestResults.AddRange(testRunChangedArgs.NewTestResults) + notifyIncrementalUpdate (Progress testRunChangedArgs) + + member _.LaunchProcessWithDebuggerAttached(_testProcessStartInfo: TestProcessStartInfo) : int = + raise (System.NotImplementedException()) + + module TestPlatformOptions = + let withTestCaseFilter (options: TestPlatformOptions) filterExpression = options.TestCaseFilter <- filterExpression + + + let runTestsAsync + (vstestPath: string) + (incrementalUpdateHandler: TestRunUpdate -> unit) + (sources: TestProjectDll list) + (testCaseFilter: string option) + (shouldDebug: bool) + : Async = + async { + let consoleParams = ConsoleParameters() + + if shouldDebug then + consoleParams.EnvironmentVariables <- + [ "VSTEST_HOST_DEBUG", "1" ] |> dict |> System.Collections.Generic.Dictionary + + let vstest = new VsTestConsoleWrapper(vstestPath, consoleParams) + let runHandler = TestRunHandler(incrementalUpdateHandler) + + let options = new TestPlatformOptions() + testCaseFilter |> Option.iter (TestPlatformOptions.withTestCaseFilter options) + + use! _cancel = + Async.OnCancel(fun () -> + printfn "Cancelling test run" + vstest.CancelTestRun() + printfn "Test Run Cancelled") + + vstest.RunTests(sources, null, options, runHandler) + return runHandler.TestResults |> List.ofSeq + } + + + open System.IO + + let tryFindVsTestFromDotnetRoot (dotnetRoot: string) (workspaceRoot: string option) : Result = + let cwd = + defaultArg workspaceRoot System.Environment.CurrentDirectory |> DirectoryInfo + + let dotnetBinary = + if dotnetRoot |> Directory.Exists then + if + System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform( + System.Runtime.InteropServices.OSPlatform.Windows + ) + then + FileInfo(Path.Combine(dotnetRoot, "dotnet.exe")) + else + FileInfo(Path.Combine(dotnetRoot, "dotnet")) + else + dotnetRoot |> FileInfo + + match Ionide.ProjInfo.SdkDiscovery.versionAt cwd dotnetBinary with + | Ok sdkVersion -> + let sdks = Ionide.ProjInfo.SdkDiscovery.sdks dotnetBinary + + match sdks |> Array.tryFind (fun sdk -> sdk.Version = sdkVersion) with + | Some sdk -> + let vstestBinary = Path.Combine(sdk.Path.FullName, "vstest.console.dll") |> FileInfo + + if vstestBinary.Exists then + Ok vstestBinary + else + Error $"Found the correct dotnet sdk, but vstest was not at the expected sub-path: {vstestBinary.FullName}" + | None -> Error $"Couldn't find the install location for dotnet sdk version: {sdkVersion}" + | Error _ -> Error $"Couldn't identify the dotnet version for working directory: {cwd.FullName}" + + +type TestFileRange = { StartLine: int; EndLine: int } + +type TestItem = + { + FullName: string + DisplayName: string /// Identifies the test adapter that ran the tests - /// Example: executor://xunit/VsTestRunner2/netcoreapp + /// Example: executor://xunit/VsTestRunner2/netcoreapp /// Used for determining the test library, which effects how tests names are broken down - ExecutorUri : string - ProjectFilePath : string - TargetFramework : string - CodeFilePath : string option - CodeLocationRange : TestFileRange option -} - -module TestItem = - let ofVsTestCase (projFilePath: string) (targetFramework: string) (testCase: Microsoft.VisualStudio.TestPlatform.ObjectModel.TestCase) : TestItem = - { - FullName = testCase.FullyQualifiedName - DisplayName = testCase.DisplayName - ExecutorUri = testCase.ExecutorUri |> string - ProjectFilePath = projFilePath - TargetFramework = targetFramework - CodeFilePath = Some testCase.CodeFilePath - CodeLocationRange = Some { StartLine = testCase.LineNumber; EndLine = testCase.LineNumber } - } - - let tryTestCaseToDTO (projectLookup: string -> Ionide.ProjInfo.Types.ProjectOptions option) (testCase: Microsoft.VisualStudio.TestPlatform.ObjectModel.TestCase) : TestItem option = - match projectLookup testCase.Source with - | None -> None // this should never happen. We pass VsTest the list of executables to test, so all the possible sources should be known to us - | Some project -> ofVsTestCase project.ProjectFileName project.TargetFramework testCase |> Some + ExecutorUri: string + ProjectFilePath: string + TargetFramework: string + CodeFilePath: string option + CodeLocationRange: TestFileRange option + } + +module TestItem = + let ofVsTestCase + (projFilePath: string) + (targetFramework: string) + (testCase: Microsoft.VisualStudio.TestPlatform.ObjectModel.TestCase) + : TestItem = + { FullName = testCase.FullyQualifiedName + DisplayName = testCase.DisplayName + ExecutorUri = testCase.ExecutorUri |> string + ProjectFilePath = projFilePath + TargetFramework = targetFramework + CodeFilePath = Some testCase.CodeFilePath + CodeLocationRange = + Some + { StartLine = testCase.LineNumber + EndLine = testCase.LineNumber } } + + let tryTestCaseToDTO + (projectLookup: string -> Ionide.ProjInfo.Types.ProjectOptions option) + (testCase: Microsoft.VisualStudio.TestPlatform.ObjectModel.TestCase) + : TestItem option = + match projectLookup testCase.Source with + | None -> None // this should never happen. We pass VsTest the list of executables to test, so all the possible sources should be known to us + | Some project -> ofVsTestCase project.ProjectFileName project.TargetFramework testCase |> Some [] -type TestOutcome = - | Failed = 0 - | Passed = 1 - | Skipped = 2 - | None = 3 - | NotFound = 4 - +type TestOutcome = + | Failed = 0 + | Passed = 1 + | Skipped = 2 + | None = 3 + | NotFound = 4 + module TestOutcome = - type VSTestOutcome = Microsoft.VisualStudio.TestPlatform.ObjectModel.TestOutcome - let ofVSTestOutcome (vsTestOutcome: VSTestOutcome) = - match vsTestOutcome with - | VSTestOutcome.Passed -> TestOutcome.Passed - | VSTestOutcome.Failed -> TestOutcome.Failed - | VSTestOutcome.Skipped -> TestOutcome.Skipped - | VSTestOutcome.NotFound -> TestOutcome.NotFound - | VSTestOutcome.None -> TestOutcome.None - | _ -> TestOutcome.None - -type TestResult = { - TestItem : TestItem - Outcome : TestOutcome + type VSTestOutcome = Microsoft.VisualStudio.TestPlatform.ObjectModel.TestOutcome + + let ofVSTestOutcome (vsTestOutcome: VSTestOutcome) = + match vsTestOutcome with + | VSTestOutcome.Passed -> TestOutcome.Passed + | VSTestOutcome.Failed -> TestOutcome.Failed + | VSTestOutcome.Skipped -> TestOutcome.Skipped + | VSTestOutcome.NotFound -> TestOutcome.NotFound + | VSTestOutcome.None -> TestOutcome.None + | _ -> TestOutcome.None + +type TestResult = + { TestItem: TestItem + Outcome: TestOutcome ErrorMessage: string option ErrorStackTrace: string option AdditionalOutput: string option - Duration: TimeSpan -} - -module TestResult = - type VSTestResult = Microsoft.VisualStudio.TestPlatform.ObjectModel.TestResult - - let ofVsTestResult (projFilePath: string) (targetFramework: string) (vsTestResult: VSTestResult) : TestResult = - let stringToOption (text: string) = - if String.IsNullOrEmpty(text) - then None - else Some text - - { - Outcome = TestOutcome.ofVSTestOutcome vsTestResult.Outcome - ErrorMessage = vsTestResult.ErrorMessage |> stringToOption - ErrorStackTrace = vsTestResult.ErrorStackTrace |> stringToOption - AdditionalOutput = - match vsTestResult.Messages |> Seq.toList with - | [] -> None - | messages -> messages |> List.map _.Text |> String.concat Environment.NewLine |> Some - Duration = vsTestResult.Duration - TestItem = TestItem.ofVsTestCase projFilePath targetFramework vsTestResult.TestCase - } \ No newline at end of file + Duration: TimeSpan } + +module TestResult = + type VSTestResult = Microsoft.VisualStudio.TestPlatform.ObjectModel.TestResult + + let ofVsTestResult (projFilePath: string) (targetFramework: string) (vsTestResult: VSTestResult) : TestResult = + let stringToOption (text: string) = if String.IsNullOrEmpty(text) then None else Some text + + { Outcome = TestOutcome.ofVSTestOutcome vsTestResult.Outcome + ErrorMessage = vsTestResult.ErrorMessage |> stringToOption + ErrorStackTrace = vsTestResult.ErrorStackTrace |> stringToOption + AdditionalOutput = + match vsTestResult.Messages |> Seq.toList with + | [] -> None + | messages -> messages |> List.map _.Text |> String.concat Environment.NewLine |> Some + Duration = vsTestResult.Duration + TestItem = TestItem.ofVsTestCase projFilePath targetFramework vsTestResult.TestCase } diff --git a/src/FsAutoComplete/LspHelpers.fs b/src/FsAutoComplete/LspHelpers.fs index e5e88d07e..d04dedc8a 100644 --- a/src/FsAutoComplete/LspHelpers.fs +++ b/src/FsAutoComplete/LspHelpers.fs @@ -612,14 +612,13 @@ type TestDetectedNotification = { File: string Tests: TestAdapter.TestAdapterEntry array } -type TestRunRequest = - { TestCaseFilter: string option - AttachDebugger: bool} +type TestRunRequest = + { TestCaseFilter: string option + AttachDebugger: bool } -type TestDiscoveryUpdateNotification = - { Tests: TestServer.TestItem array } +type TestDiscoveryUpdateNotification = { Tests: TestServer.TestItem array } -type TestRunProgress = +type TestRunProgress = { TestResults: TestServer.TestResult array ActiveTests: TestServer.TestItem array } diff --git a/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs b/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs index f6d003547..4934ba74e 100644 --- a/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs +++ b/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs @@ -3056,18 +3056,22 @@ type AdaptiveFSharpLspServer return! returnException e logCfg } - override this.TestDiscoverTests (): Async> = + override this.TestDiscoverTests() : Async> = asyncResult { - let! testDTOs = state.DiscoverTests() |> AsyncResult.mapError (fun msg -> JsonRpc.Error.InternalError msg) + let! testDTOs = + state.DiscoverTests() + |> AsyncResult.mapError (fun msg -> JsonRpc.Error.InternalError msg) - return Some { Content = CommandResponse.discoverTests FsAutoComplete.JsonSerializer.writeJson testDTOs} + return Some { Content = CommandResponse.discoverTests FsAutoComplete.JsonSerializer.writeJson testDTOs } } - override this.TestRunTests (p: TestRunRequest): Async> = + override this.TestRunTests(p: TestRunRequest) : Async> = asyncResult { - let! testDTOs = state.RunTests p.TestCaseFilter p.AttachDebugger |> AsyncResult.mapError (fun msg -> JsonRpc.Error.InternalError msg) + let! testDTOs = + state.RunTests p.TestCaseFilter p.AttachDebugger + |> AsyncResult.mapError (fun msg -> JsonRpc.Error.InternalError msg) - return Some { Content = CommandResponse.runTests FsAutoComplete.JsonSerializer.writeJson testDTOs} + return Some { Content = CommandResponse.runTests FsAutoComplete.JsonSerializer.writeJson testDTOs } } override x.Dispose() = disposables.Dispose() @@ -3200,8 +3204,8 @@ module AdaptiveFSharpLspServer = |> Map.add "fsproj/addExistingFile" (serverRequestHandling (fun s p -> s.FsProjAddExistingFile(p))) |> Map.add "fsproj/renameFile" (serverRequestHandling (fun s p -> s.FsProjRenameFile(p))) |> Map.add "fsproj/removeFile" (serverRequestHandling (fun s p -> s.FsProjRemoveFile(p))) - |> Map.add "test/discoverTests" (serverRequestHandling (fun s _ -> s.TestDiscoverTests() )) - |> Map.add "test/runTests" (serverRequestHandling (fun s p -> s.TestRunTests(p) )) + |> Map.add "test/discoverTests" (serverRequestHandling (fun s _ -> s.TestDiscoverTests())) + |> Map.add "test/runTests" (serverRequestHandling (fun s p -> s.TestRunTests(p))) let adaptiveServer lspClient = let loader = workspaceLoaderFactory toolsPath diff --git a/src/FsAutoComplete/LspServers/AdaptiveServerState.fs b/src/FsAutoComplete/LspServers/AdaptiveServerState.fs index 6d06aad3d..1be08a94f 100644 --- a/src/FsAutoComplete/LspServers/AdaptiveServerState.fs +++ b/src/FsAutoComplete/LspServers/AdaptiveServerState.fs @@ -156,23 +156,27 @@ type FindFirstProject() = $"Couldn't find a corresponding project for {sourceFile}. \n Projects include {allProjects}. \nHave the projects loaded yet or have you tried restoring your project/solution?") -module TestProjectHelpers = +module TestProjectHelpers = let tryGetWorkspaceProjects (workspaceLoader: IWorkspaceLoader) (workspace: WorkspaceChosen) = match workspace with | WorkspaceChosen.NotChosen -> Error "No workspace loaded. Can't discover tests" - | WorkspaceChosen.Projs projectPaths -> - projectPaths |> List.ofSeq |> List.map string |> workspaceLoader.LoadProjects |> Ok + | WorkspaceChosen.Projs projectPaths -> + projectPaths + |> List.ofSeq + |> List.map string + |> workspaceLoader.LoadProjects + |> Ok let isTestProject (project: Types.ProjectOptions) = let testProjectIndicators = - set [ "Microsoft.TestPlatform.TestHost"; "Microsoft.NET.Test.Sdk" ] + set [ "Microsoft.TestPlatform.TestHost"; "Microsoft.NET.Test.Sdk" ] project.PackageReferences |> List.exists (fun pr -> Set.contains pr.Name testProjectIndicators) - let tryGetTestProjects (workspaceLoader: IWorkspaceLoader) (workspace: WorkspaceChosen) = + let tryGetTestProjects (workspaceLoader: IWorkspaceLoader) (workspace: WorkspaceChosen) = result { - let! projects = tryGetWorkspaceProjects workspaceLoader workspace + let! projects = tryGetWorkspaceProjects workspaceLoader workspace return projects |> List.ofSeq |> List.filter isTestProject } @@ -2587,28 +2591,28 @@ type AdaptiveState member x.GlyphToSymbolKind = glyphToSymbolKind |> AVal.force - member state.DiscoverTests () = - + member state.DiscoverTests() = + asyncResult { - let! vstestBinary = TestServer.VSTestWrapper.tryFindVsTestFromDotnetRoot state.Config.DotNetRoot state.RootPath - - let! testProjects = TestProjectHelpers.tryGetTestProjects workspaceLoader state.WorkspacePaths + let! vstestBinary = TestServer.VSTestWrapper.tryFindVsTestFromDotnetRoot state.Config.DotNetRoot state.RootPath + + let! testProjects = TestProjectHelpers.tryGetTestProjects workspaceLoader state.WorkspacePaths let testProjectBinaries = testProjects |> List.map _.TargetPath let tryTestCasesToDTOs testCases = - let projectLookup = - testProjects |> Seq.map (fun p -> p.TargetPath, p) |> Map.ofSeq - testCases |> List.choose (TestServer.TestItem.tryTestCaseToDTO projectLookup.TryFind) + let projectLookup = testProjects |> Seq.map (fun p -> p.TargetPath, p) |> Map.ofSeq + + testCases + |> List.choose (TestServer.TestItem.tryTestCaseToDTO projectLookup.TryFind) - let incrementalUpdateHandler (tests: Microsoft.VisualStudio.TestPlatform.ObjectModel.TestCase list) = - lspClient.NotifyTestDiscoveryUpdate({ Tests = tests |> tryTestCasesToDTOs |> Array.ofList}) + let incrementalUpdateHandler (tests: Microsoft.VisualStudio.TestPlatform.ObjectModel.TestCase list) = + lspClient.NotifyTestDiscoveryUpdate({ Tests = tests |> tryTestCasesToDTOs |> Array.ofList }) |> Async.RunSynchronously let! testCases = TestServer.VSTestWrapper.discoverTestsAsync vstestBinary.FullName incrementalUpdateHandler testProjectBinaries - let testDTOs : TestServer.TestItem list = - testCases |> tryTestCasesToDTOs + let testDTOs: TestServer.TestItem list = testCases |> tryTestCasesToDTOs return testDTOs } @@ -2616,45 +2620,66 @@ type AdaptiveState member state.RunTests (testCaseFilter: string option) (attachDebugger: bool) = asyncResult { let! vstestBinary = TestServer.VSTestWrapper.tryFindVsTestFromDotnetRoot state.Config.DotNetRoot state.RootPath - let! testProjects = TestProjectHelpers.tryGetTestProjects workspaceLoader state.WorkspacePaths + let! testProjects = TestProjectHelpers.tryGetTestProjects workspaceLoader state.WorkspacePaths let testProjectBinaries = testProjects |> List.map _.TargetPath - - let projectLookup = - testProjects |> Seq.map (fun p -> p.TargetPath, p) |> Map.ofSeq + + let projectLookup = testProjects |> Seq.map (fun p -> p.TargetPath, p) |> Map.ofSeq + let tryTestResultsToDTOs testCases = - let tryTestResultToDTO (projectLookup: Map) (testResult: Microsoft.VisualStudio.TestPlatform.ObjectModel.TestResult) : TestServer.TestResult option = + let tryTestResultToDTO + (projectLookup: Map) + (testResult: Microsoft.VisualStudio.TestPlatform.ObjectModel.TestResult) + : TestServer.TestResult option = match projectLookup |> Map.tryFind testResult.TestCase.Source with | None -> None // this should never happen. We pass VsTest the list of executables to test, so all the possible sources should be known to us - | Some project -> TestServer.TestResult.ofVsTestResult project.ProjectFileName project.TargetFramework testResult |> Some - testCases |> List.choose (tryTestResultToDTO projectLookup) + | Some project -> + TestServer.TestResult.ofVsTestResult project.ProjectFileName project.TargetFramework testResult + |> Some + + testCases |> List.choose (tryTestResultToDTO projectLookup) use tokenSource = new CancellationTokenSource() - use! _onCancel = Async.OnCancel(fun _ -> - printfn "Sya: cancelling update handlers" - tokenSource.Cancel() - printfn "Sya: update handlers cancelled") - let incrementalUpdateHandler (runUpdate: TestServer.VSTestWrapper.TestRunUpdate) = - let dto = + + use! _onCancel = + Async.OnCancel(fun _ -> + printfn "Sya: cancelling update handlers" + tokenSource.Cancel() + printfn "Sya: update handlers cancelled") + + let incrementalUpdateHandler (runUpdate: TestServer.VSTestWrapper.TestRunUpdate) = + let dto = match runUpdate with | TestServer.VSTestWrapper.TestRunUpdate.Progress progress -> - TestRunUpdateNotification.Progress { - TestResults = progress.NewTestResults |> List.ofSeq |> tryTestResultsToDTOs |> Array.ofSeq - ActiveTests = progress.ActiveTests |> Seq.choose (TestServer.TestItem.tryTestCaseToDTO projectLookup.TryFind) |> Array.ofSeq - } + TestRunUpdateNotification.Progress + { TestResults = progress.NewTestResults |> List.ofSeq |> tryTestResultsToDTOs |> Array.ofSeq + ActiveTests = + progress.ActiveTests + |> Seq.choose (TestServer.TestItem.tryTestCaseToDTO projectLookup.TryFind) + |> Array.ofSeq } | TestServer.VSTestWrapper.TestRunUpdate.ProcessWaitingForDebugger processId -> printfn $"Sya: processId reported: {processId}" - TestRunUpdateNotification.ProcessWaitingForDebugger processId - Async.RunSynchronously(async { - use! _c = Async.OnCancel(fun _ -> printfn "Sya: Cancelled notification") - do! lspClient.NotifyTestRunUpdate(dto) - }, cancellationToken = tokenSource.Token) + TestRunUpdateNotification.ProcessWaitingForDebugger processId + + Async.RunSynchronously( + async { + use! _c = Async.OnCancel(fun _ -> printfn "Sya: Cancelled notification") + do! lspClient.NotifyTestRunUpdate(dto) + }, + cancellationToken = tokenSource.Token + ) printfn "Sya: running tests" + let! testResults = - TestServer.VSTestWrapper.runTestsAsync vstestBinary.FullName incrementalUpdateHandler testProjectBinaries testCaseFilter attachDebugger + TestServer.VSTestWrapper.runTestsAsync + vstestBinary.FullName + incrementalUpdateHandler + testProjectBinaries + testCaseFilter + attachDebugger printfn "Sya: test results returned" - + let resultDtos = testResults |> tryTestResultsToDTOs return resultDtos } diff --git a/src/FsAutoComplete/LspServers/FSharpLspClient.fs b/src/FsAutoComplete/LspServers/FSharpLspClient.fs index b5dd5d4f2..6e44ff179 100644 --- a/src/FsAutoComplete/LspServers/FSharpLspClient.fs +++ b/src/FsAutoComplete/LspServers/FSharpLspClient.fs @@ -63,14 +63,17 @@ type FSharpLspClient(sendServerNotification: ClientNotificationSender, sendServe sendServerNotification "fsharp/testDetected" (box p) |> Async.Ignore member __.NotifyTestDiscoveryUpdate(p: TestDiscoveryUpdateNotification) = - sendServerNotification "test/testDiscoveryUpdate" (box { Content = JsonSerializer.writeJson p}) |> Async.Ignore + sendServerNotification "test/testDiscoveryUpdate" (box { Content = JsonSerializer.writeJson p }) + |> Async.Ignore member __.NotifyTestRunUpdate(p: TestRunUpdateNotification) = match p with - | Progress progress -> - sendServerNotification "test/testRunProgressUpdate" (box { Content = JsonSerializer.writeJson progress}) |> Async.Ignore - | ProcessWaitingForDebugger processId -> - sendServerNotification "test/processWaitingForDebugger" (box { Content = JsonSerializer.writeJson processId}) |> Async.Ignore + | Progress progress -> + sendServerNotification "test/testRunProgressUpdate" (box { Content = JsonSerializer.writeJson progress }) + |> Async.Ignore + | ProcessWaitingForDebugger processId -> + sendServerNotification "test/processWaitingForDebugger" (box { Content = JsonSerializer.writeJson processId }) + |> Async.Ignore member x.CodeLensRefresh() = match x.ClientCapabilities with diff --git a/test/FsAutoComplete.Tests.Lsp/Program.fs b/test/FsAutoComplete.Tests.Lsp/Program.fs index aa1b73618..a495a7ff9 100644 --- a/test/FsAutoComplete.Tests.Lsp/Program.fs +++ b/test/FsAutoComplete.Tests.Lsp/Program.fs @@ -35,22 +35,28 @@ let testTimeout = // delay in ms between workspace start + stop notifications because the system goes too fast :-/ Environment.SetEnvironmentVariable("FSAC_WORKSPACELOAD_DELAY", "250") -let getEnvVarAsStr name = - Environment.GetEnvironmentVariable(name) - |> Option.ofObj +let getEnvVarAsStr name = Environment.GetEnvironmentVariable(name) |> Option.ofObj let (|EqIC|_|) (a: string) (b: string) = - if String.Equals(a, b, StringComparison.OrdinalIgnoreCase) then Some () else None + if String.Equals(a, b, StringComparison.OrdinalIgnoreCase) then + Some() + else + None let loaders = match getEnvVarAsStr "USE_WORKSPACE_LOADER" with - | Some (EqIC "WorkspaceLoader") -> [ "Ionide WorkspaceLoader", (fun toolpath -> WorkspaceLoader.Create(toolpath, FsAutoComplete.Core.ProjectLoader.globalProperties)) ] - | Some (EqIC "ProjectGraph") -> [ "MSBuild Project Graph WorkspaceLoader", (fun toolpath -> WorkspaceLoaderViaProjectGraph.Create(toolpath, FsAutoComplete.Core.ProjectLoader.globalProperties)) ] + | Some(EqIC "WorkspaceLoader") -> + [ "Ionide WorkspaceLoader", + (fun toolpath -> WorkspaceLoader.Create(toolpath, FsAutoComplete.Core.ProjectLoader.globalProperties)) ] + | Some(EqIC "ProjectGraph") -> + [ "MSBuild Project Graph WorkspaceLoader", + (fun toolpath -> + WorkspaceLoaderViaProjectGraph.Create(toolpath, FsAutoComplete.Core.ProjectLoader.globalProperties)) ] | _ -> - [ - "Ionide WorkspaceLoader", (fun toolpath -> WorkspaceLoader.Create(toolpath, FsAutoComplete.Core.ProjectLoader.globalProperties)) + [ "Ionide WorkspaceLoader", + (fun toolpath -> WorkspaceLoader.Create(toolpath, FsAutoComplete.Core.ProjectLoader.globalProperties)) // "MSBuild Project Graph WorkspaceLoader", (fun toolpath -> WorkspaceLoaderViaProjectGraph.Create(toolpath, FsAutoComplete.Core.ProjectLoader.globalProperties)) - ] + ] let adaptiveLspServerFactory toolsPath workspaceLoaderFactory sourceTextFactory = @@ -65,28 +71,22 @@ let mutable toolsPath = let compilers = match getEnvVarAsStr "USE_TRANSPARENT_COMPILER" with - | Some (EqIC "TransparentCompiler") -> ["TransparentCompiler", true ] - | Some (EqIC "BackgroundCompiler") -> [ "BackgroundCompiler", false ] - | _ -> - [ - "BackgroundCompiler", false - "TransparentCompiler", true - ] + | Some(EqIC "TransparentCompiler") -> [ "TransparentCompiler", true ] + | Some(EqIC "BackgroundCompiler") -> [ "BackgroundCompiler", false ] + | _ -> [ "BackgroundCompiler", false; "TransparentCompiler", true ] let lspTests = - testSequenced <| - testList + testSequenced + <| testList "lsp" [ for (loaderName, workspaceLoaderFactory) in loaders do testList $"{loaderName}" - [ - for (compilerName, useTransparentCompiler) in compilers do + [ for (compilerName, useTransparentCompiler) in compilers do testList $"{compilerName}" - [ - Templates.tests () + [ Templates.tests () let createServer () = adaptiveLspServerFactory toolsPath workspaceLoaderFactory sourceTextFactory useTransparentCompiler @@ -138,23 +138,19 @@ let lspTests = CallHierarchy.tests createServer diagnosticsTest createServer - TestExplorer.tests createServer - ] ] ] + TestExplorer.tests createServer ] ] ] /// Tests that do not require a LSP server -let generalTests = testList "general" [ - testList (nameof (Utils)) [ Utils.Tests.Utils.tests; Utils.Tests.TextEdit.tests ] - UtilsTests.allTests - InlayHintTests.explicitTypeInfoTests sourceTextFactory - FindReferences.tryFixupRangeTests sourceTextFactory -] +let generalTests = + testList + "general" + [ testList (nameof (Utils)) [ Utils.Tests.Utils.tests; Utils.Tests.TextEdit.tests ] + InlayHintTests.explicitTypeInfoTests sourceTextFactory + FindReferences.tryFixupRangeTests sourceTextFactory ] [] -let tests = testList "FSAC" [ - generalTests - lspTests - SnapshotTests.snapshotTests loaders toolsPath - ] +let tests = + testList "FSAC" [ generalTests; lspTests; SnapshotTests.snapshotTests loaders toolsPath ] open OpenTelemetry open OpenTelemetry.Resources @@ -167,18 +163,19 @@ open FsAutoComplete.Telemetry [] let main args = let serviceName = "FsAutoComplete.Tests.Lsp" + use traceProvider = let version = FsAutoComplete.Utils.Version.info().Version + Sdk .CreateTracerProviderBuilder() .AddSource(FsAutoComplete.Utils.Tracing.serviceName, Tracing.fscServiceName, serviceName) .SetResourceBuilder( - ResourceBuilder - .CreateDefault() - .AddService(serviceName = serviceName, serviceVersion = version) + ResourceBuilder.CreateDefault().AddService(serviceName = serviceName, serviceVersion = version) ) .AddOtlpExporter() .Build() + let outputTemplate = "[{Timestamp:HH:mm:ss} {Level:u3}] [{SourceContext}] {Message:lj}{NewLine}{Exception}" @@ -283,11 +280,9 @@ let main args = use activitySource = new ActivitySource(serviceName) let cliArgs = - [ - CLIArguments.Printer(Expecto.Impl.TestPrinters.summaryWithLocationPrinter defaultConfig.printer) + [ CLIArguments.Printer(Expecto.Impl.TestPrinters.summaryWithLocationPrinter defaultConfig.printer) CLIArguments.Verbosity Expecto.Logging.LogLevel.Info - CLIArguments.Parallel - ] + CLIArguments.Parallel ] // let trace = traceProvider.GetTracer("FsAutoComplete.Tests.Lsp") // use span = trace.StartActiveSpan("runTests", SpanKind.Internal) use span = activitySource.StartActivity("runTests") diff --git a/test/FsAutoComplete.Tests.Lsp/TestExplorerTests.fs b/test/FsAutoComplete.Tests.Lsp/TestExplorerTests.fs index a3ce01922..dda2490d5 100644 --- a/test/FsAutoComplete.Tests.Lsp/TestExplorerTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/TestExplorerTests.fs @@ -8,57 +8,77 @@ open System.Threading open Helpers.Expecto.ShadowedTimeouts let tests createServer = - let initializeServer workspaceRoot = - async { - let! (server, event) = serverInitialize workspaceRoot defaultConfigDto createServer - do! waitForWorkspaceFinishedParsing event - - return (server, event) - } |> Async.Cache - - testSequenced <| testList "TestExplorerTests" - [ testCaseAsync "it should report a processId when debug a test project" <| async { - // let workspaceRoot = Path.Combine(__SOURCE_DIRECTORY__, "TestCases", "XunitTests") - //X:\source\dotnet\FsAutoComplete\test\FsAutoComplete.Tests.Lsp\TestCases\XunitTests\bin\Debug\net8.0 - //X:\source\dotnet\FsAutoComplete\test\SampleTestProjects\VSTest.XUnit.RunResults\bin\Debug\net8.0 - let workspaceRoot = Path.Combine(__SOURCE_DIRECTORY__, "..", "SampleTestProjects", "VSTest.XUnit.RunResults") - let! server, clientNotifications = initializeServer workspaceRoot - - use server = server - - use tokenSource = new CancellationTokenSource() - let mutable processIdSpy : string option = None - use! _onCancel = Async.OnCancel(fun () -> tokenSource.Cancel()) - use _ = clientNotifications.Subscribe(fun (msgType: string, data: obj) -> - if msgType = "test/processWaitingForDebugger" then - printfn $"Sya: update message {data}" - let processId : string = data :?> PlainNotification |> _.Content |> FsAutoComplete.JsonSerializer.readJson - processIdSpy <- Some processId - tokenSource.Cancel() - let tryParseProcessId (str: string) = - let (success, value) = System.Int32.TryParse(str) - printfn $"Sya: parsed process id: {success}, {value}. Original: {str}" - if success then Some value else None - - printfn $"Sya: process spy {processIdSpy}" - processId |> tryParseProcessId |> Option.iter(fun pid -> - try - printfn $"Sya: trying to kill process {pid}" - System.Diagnostics.Process.GetProcessById(pid).Kill(true) - printfn $"Sya: killed process {pid}" - with e -> - printfn $"Sya: failed to kill process {pid}" - ) - ) - Expect.throws (fun () -> - let runRequest : TestRunRequest = { - TestCaseFilter = None - AttachDebugger = true - } - Async.RunSynchronously(server.TestRunTests(runRequest) |> Async.Ignore, cancellationToken = tokenSource.Token) - ) "" - printfn "Sya: Server test run closed" - Expect.isSome processIdSpy "" - printfn "Sya: test complete" - }] - + let initializeServer workspaceRoot = + async { + let! (server, event) = serverInitialize workspaceRoot defaultConfigDto createServer + do! waitForWorkspaceFinishedParsing event + + return (server, event) + } + |> Async.Cache + + testSequenced + <| testList + "TestExplorerTests" + [ testCaseAsync "it should report a processId when debug a test project" + <| async { + // let workspaceRoot = Path.Combine(__SOURCE_DIRECTORY__, "TestCases", "XunitTests") + //X:\source\dotnet\FsAutoComplete\test\FsAutoComplete.Tests.Lsp\TestCases\XunitTests\bin\Debug\net8.0 + //X:\source\dotnet\FsAutoComplete\test\SampleTestProjects\VSTest.XUnit.RunResults\bin\Debug\net8.0 + let workspaceRoot = + Path.Combine(__SOURCE_DIRECTORY__, "..", "SampleTestProjects", "VSTest.XUnit.RunResults") + + let! server, clientNotifications = initializeServer workspaceRoot + + use server = server + + use tokenSource = new CancellationTokenSource() + let mutable processIdSpy: string option = None + use! _onCancel = Async.OnCancel(fun () -> tokenSource.Cancel()) + + use _ = + clientNotifications.Subscribe(fun (msgType: string, data: obj) -> + if msgType = "test/processWaitingForDebugger" then + printfn $"Sya: update message {data}" + + let processId: string = + data :?> PlainNotification + |> _.Content + |> FsAutoComplete.JsonSerializer.readJson + + processIdSpy <- Some processId + tokenSource.Cancel() + + let tryParseProcessId (str: string) = + let (success, value) = System.Int32.TryParse(str) + printfn $"Sya: parsed process id: {success}, {value}. Original: {str}" + if success then Some value else None + + printfn $"Sya: process spy {processIdSpy}" + + processId + |> tryParseProcessId + |> Option.iter (fun pid -> + try + printfn $"Sya: trying to kill process {pid}" + System.Diagnostics.Process.GetProcessById(pid).Kill(true) + printfn $"Sya: killed process {pid}" + with e -> + printfn $"Sya: failed to kill process {pid}")) + + Expect.throws + (fun () -> + let runRequest: TestRunRequest = + { TestCaseFilter = None + AttachDebugger = true } + + Async.RunSynchronously( + server.TestRunTests(runRequest) |> Async.Ignore, + cancellationToken = tokenSource.Token + )) + "" + + printfn "Sya: Server test run closed" + Expect.isSome processIdSpy "" + printfn "Sya: test complete" + } ] diff --git a/test/FsAutoComplete.Tests.TestExplorer/TestRunTests.fs b/test/FsAutoComplete.Tests.TestExplorer/TestRunTests.fs index 969caf52f..1a7adcebd 100644 --- a/test/FsAutoComplete.Tests.TestExplorer/TestRunTests.fs +++ b/test/FsAutoComplete.Tests.TestExplorer/TestRunTests.fs @@ -2,116 +2,141 @@ module TestRunTests open Expecto open FsAutoComplete.TestServer -open Microsoft.VisualStudio.TestPlatform.ObjectModel; +open Microsoft.VisualStudio.TestPlatform.ObjectModel open System.IO let tryFindVsTest () : string = - let dotnetBinary = + let dotnetBinary = Ionide.ProjInfo.Paths.dotnetRoot.Value - |> Option.defaultWith (fun () -> failwith "Couldn't find dotnet root. The dotnet sdk must be installed to run these tests") + |> Option.defaultWith (fun () -> + failwith "Couldn't find dotnet root. The dotnet sdk must be installed to run these tests") let cwd = System.Environment.CurrentDirectory |> Some - + VSTestWrapper.tryFindVsTestFromDotnetRoot dotnetBinary.FullName cwd |> Result.defaultWith failwith |> _.FullName - + let vstestPath = tryFindVsTest () [] let tests = - testList "VSTestWrapper Test Run" [ - testCaseAsync "should return an empty list if given no projects" <| async { - let expected = [] - let! actual = VSTestWrapper.runTestsAsync vstestPath ignore [] None false - Expect.equal actual expected "" - } - - testCaseAsync "should be able to report basic test run outcomes" <| async { - let expected = [ - ("Tests.My test", TestOutcome.Passed) - ("Tests.Fails", TestOutcome.Failed) - ("Tests.Skipped", TestOutcome.Skipped) - ("Tests.Exception", TestOutcome.Failed) - ("Tests+Nested.Test 1", TestOutcome.Passed) - ("Tests+Nested.Test 2", TestOutcome.Passed) - ] - - let sources = [ - Path.Combine(ResourceLocators.sampleProjectsRootDir, "VSTest.XUnit.RunResults/bin/Debug/net8.0/VSTest.XUnit.RunResults.dll") - ] - - let! runResults = VSTestWrapper.runTestsAsync vstestPath ignore sources None false - - let likenessOfTestResult (result: TestResult) = (result.TestCase.FullyQualifiedName, result.Outcome) - let actual = runResults |> List.map likenessOfTestResult - - Expect.equal (set actual) (set expected) "" - } - - testCaseAsync "should run only tests that match the case filter" <| async { - let expected = [ - ("Tests+Nested.Test 1", TestOutcome.Passed) - ("Tests+Nested.Test 2", TestOutcome.Passed) - ] - - let sources = [ - Path.Combine(ResourceLocators.sampleProjectsRootDir, "VSTest.XUnit.RunResults/bin/Debug/net8.0/VSTest.XUnit.RunResults.dll") - ] - - let! runResults = VSTestWrapper.runTestsAsync vstestPath ignore sources (Some "FullyQualifiedName~Tests+Nested") false - - let likenessOfTestResult (result: TestResult) = (result.TestCase.FullyQualifiedName, result.Outcome) - let actual = runResults |> List.map likenessOfTestResult - - Expect.equal (set actual) (set expected) "" - } - - testCaseAsync "should report processIds when debugging is on" <| async { - use tokenSource = new System.Threading.CancellationTokenSource(2000) - - let mutable actualProcessId : string option = None - let updateSpy (update: VSTestWrapper.TestRunUpdate) = - match update with - | VSTestWrapper.TestRunUpdate.ProcessWaitingForDebugger processId -> - actualProcessId <- Some processId - tokenSource.Cancel() - | _ -> () - - use! _c = Async.OnCancel(fun _ -> - tokenSource.Cancel()) - - Expect.throws (fun () -> - let sources = [ - Path.Combine(ResourceLocators.sampleProjectsRootDir, "VSTest.XUnit.RunResults/bin/Debug/net8.0/VSTest.XUnit.RunResults.dll") - ] - Async.RunSynchronously (VSTestWrapper.runTestsAsync vstestPath updateSpy sources None true, cancellationToken = tokenSource.Token) |> ignore - ) "" - - Expect.isSome actualProcessId "Expected runTest to report a processId" - } - - testCaseAsync "should report a processId only once per process" <| async { - use tokenSource = new System.Threading.CancellationTokenSource(1000) - - let mutable reportedProcessIds : string list = [] - let updateSpy (update: VSTestWrapper.TestRunUpdate) = - match update with - | VSTestWrapper.TestRunUpdate.ProcessWaitingForDebugger processId -> - reportedProcessIds <- processId :: reportedProcessIds - | _ -> () - - use! _c = Async.OnCancel(fun _ -> - tokenSource.Cancel()) - - Expect.throws (fun () -> - let sources = [ - Path.Combine(ResourceLocators.sampleProjectsRootDir, "VSTest.XUnit.RunResults/bin/Debug/net8.0/VSTest.XUnit.RunResults.dll") - ] - Async.RunSynchronously (VSTestWrapper.runTestsAsync vstestPath updateSpy sources None true, cancellationToken = tokenSource.Token) |> ignore - ) "" - - Expect.hasLength reportedProcessIds 1 "Expected runTest to report a processId" - } - ] + testList + "VSTestWrapper Test Run" + [ testCaseAsync "should return an empty list if given no projects" + <| async { + let expected = [] + let! actual = VSTestWrapper.runTestsAsync vstestPath ignore [] None false + Expect.equal actual expected "" + } + + testCaseAsync "should be able to report basic test run outcomes" + <| async { + let expected = + [ ("Tests.My test", TestOutcome.Passed) + ("Tests.Fails", TestOutcome.Failed) + ("Tests.Skipped", TestOutcome.Skipped) + ("Tests.Exception", TestOutcome.Failed) + ("Tests+Nested.Test 1", TestOutcome.Passed) + ("Tests+Nested.Test 2", TestOutcome.Passed) ] + + let sources = + [ Path.Combine( + ResourceLocators.sampleProjectsRootDir, + "VSTest.XUnit.RunResults/bin/Debug/net8.0/VSTest.XUnit.RunResults.dll" + ) ] + + let! runResults = VSTestWrapper.runTestsAsync vstestPath ignore sources None false + + let likenessOfTestResult (result: TestResult) = (result.TestCase.FullyQualifiedName, result.Outcome) + let actual = runResults |> List.map likenessOfTestResult + + Expect.equal (set actual) (set expected) "" + } + + testCaseAsync "should run only tests that match the case filter" + <| async { + let expected = + [ ("Tests+Nested.Test 1", TestOutcome.Passed) + ("Tests+Nested.Test 2", TestOutcome.Passed) ] + + let sources = + [ Path.Combine( + ResourceLocators.sampleProjectsRootDir, + "VSTest.XUnit.RunResults/bin/Debug/net8.0/VSTest.XUnit.RunResults.dll" + ) ] + + let! runResults = + VSTestWrapper.runTestsAsync vstestPath ignore sources (Some "FullyQualifiedName~Tests+Nested") false + + let likenessOfTestResult (result: TestResult) = (result.TestCase.FullyQualifiedName, result.Outcome) + let actual = runResults |> List.map likenessOfTestResult + + Expect.equal (set actual) (set expected) "" + } + + testCaseAsync "should report processIds when debugging is on" + <| async { + use tokenSource = new System.Threading.CancellationTokenSource(2000) + + let mutable actualProcessId: string option = None + + let updateSpy (update: VSTestWrapper.TestRunUpdate) = + match update with + | VSTestWrapper.TestRunUpdate.ProcessWaitingForDebugger processId -> + actualProcessId <- Some processId + tokenSource.Cancel() + | _ -> () + + use! _c = Async.OnCancel(fun _ -> tokenSource.Cancel()) + + Expect.throws + (fun () -> + let sources = + [ Path.Combine( + ResourceLocators.sampleProjectsRootDir, + "VSTest.XUnit.RunResults/bin/Debug/net8.0/VSTest.XUnit.RunResults.dll" + ) ] + + Async.RunSynchronously( + VSTestWrapper.runTestsAsync vstestPath updateSpy sources None true, + cancellationToken = tokenSource.Token + ) + |> ignore) + "" + + Expect.isSome actualProcessId "Expected runTest to report a processId" + } + + testCaseAsync "should report a processId only once per process" + <| async { + use tokenSource = new System.Threading.CancellationTokenSource(1000) + + let mutable reportedProcessIds: string list = [] + + let updateSpy (update: VSTestWrapper.TestRunUpdate) = + match update with + | VSTestWrapper.TestRunUpdate.ProcessWaitingForDebugger processId -> + reportedProcessIds <- processId :: reportedProcessIds + | _ -> () + + use! _c = Async.OnCancel(fun _ -> tokenSource.Cancel()) + + Expect.throws + (fun () -> + let sources = + [ Path.Combine( + ResourceLocators.sampleProjectsRootDir, + "VSTest.XUnit.RunResults/bin/Debug/net8.0/VSTest.XUnit.RunResults.dll" + ) ] + + Async.RunSynchronously( + VSTestWrapper.runTestsAsync vstestPath updateSpy sources None true, + cancellationToken = tokenSource.Token + ) + |> ignore) + "" + + Expect.hasLength reportedProcessIds 1 "Expected runTest to report a processId" + } ] From 7065eaa8d8cfe1183e29ec39062cf5af8fb47ca1 Mon Sep 17 00:00:00 2001 From: farlee2121 <2847259+farlee2121@users.noreply.github.com> Date: Sun, 10 Aug 2025 12:26:20 -0500 Subject: [PATCH 21/39] Delete some diagnostic code --- test/FsAutoComplete.Tests.Lsp/TestExplorerTests.fs | 3 --- 1 file changed, 3 deletions(-) diff --git a/test/FsAutoComplete.Tests.Lsp/TestExplorerTests.fs b/test/FsAutoComplete.Tests.Lsp/TestExplorerTests.fs index dda2490d5..31eb9d17d 100644 --- a/test/FsAutoComplete.Tests.Lsp/TestExplorerTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/TestExplorerTests.fs @@ -22,9 +22,6 @@ let tests createServer = "TestExplorerTests" [ testCaseAsync "it should report a processId when debug a test project" <| async { - // let workspaceRoot = Path.Combine(__SOURCE_DIRECTORY__, "TestCases", "XunitTests") - //X:\source\dotnet\FsAutoComplete\test\FsAutoComplete.Tests.Lsp\TestCases\XunitTests\bin\Debug\net8.0 - //X:\source\dotnet\FsAutoComplete\test\SampleTestProjects\VSTest.XUnit.RunResults\bin\Debug\net8.0 let workspaceRoot = Path.Combine(__SOURCE_DIRECTORY__, "..", "SampleTestProjects", "VSTest.XUnit.RunResults") From 8114ca91e96bc9a5832f88ebcdd69df636948952 Mon Sep 17 00:00:00 2001 From: farlee2121 <2847259+farlee2121@users.noreply.github.com> Date: Fri, 29 Aug 2025 16:07:19 -0500 Subject: [PATCH 22/39] fix incorrect message type on the runTests lsp message --- src/FsAutoComplete/CommandResponse.fs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/FsAutoComplete/CommandResponse.fs b/src/FsAutoComplete/CommandResponse.fs index 50d7d1920..b71d04ae7 100644 --- a/src/FsAutoComplete/CommandResponse.fs +++ b/src/FsAutoComplete/CommandResponse.fs @@ -703,12 +703,16 @@ module CommandResponse = serialize { Kind = "pipelineHint"; Data = ctn } - + type DiscoverTestsResponse = TestServer.TestItem list let discoverTests (serialize: Serializer) (content: DiscoverTestsResponse) = - serialize { Kind = "discoverTests"; Data = content } + serialize + { Kind = "discoverTests" + Data = content } let runTests (serialize: Serializer) (content: TestServer.TestResult list) = - serialize { Kind = "discoverTests"; Data = content |> Array.ofList } \ No newline at end of file + serialize + { Kind = "runTests" + Data = content |> Array.ofList } From b49d397d9008aa5127c77308cc41a8f8df43b1d2 Mon Sep 17 00:00:00 2001 From: farlee2121 <2847259+farlee2121@users.noreply.github.com> Date: Fri, 29 Aug 2025 16:10:48 -0500 Subject: [PATCH 23/39] Pass unit tests using RunTestsWithCustomTestHost RunTestsWithCustomTestHost is the official way to attach a debugger with VSTest --- src/FsAutoComplete.Core/TestServer.fs | 66 +++++++++++++------ src/FsAutoComplete/LspHelpers.fs | 2 +- src/FsAutoComplete/LspHelpers.fsi | 13 ++-- .../LspServers/FSharpLspClient.fs | 2 +- .../TestRunTests.fs | 5 +- 5 files changed, 56 insertions(+), 32 deletions(-) diff --git a/src/FsAutoComplete.Core/TestServer.fs b/src/FsAutoComplete.Core/TestServer.fs index 0ba7586ac..4b31b5b56 100644 --- a/src/FsAutoComplete.Core/TestServer.fs +++ b/src/FsAutoComplete.Core/TestServer.fs @@ -8,6 +8,7 @@ module VSTestWrapper = open Microsoft.VisualStudio.TestPlatform.ObjectModel.Client open Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging open System.Text.RegularExpressions + open Microsoft.VisualStudio.TestPlatform.ObjectModel.Client.Interfaces type TestProjectDll = string @@ -48,7 +49,7 @@ module VSTestWrapper = return discoveryHandler.DiscoveredTests |> List.ofSeq } - type ProcessId = string + type ProcessId = int type TestRunUpdate = | Progress of TestRunChangedEventArgs @@ -56,24 +57,10 @@ module VSTestWrapper = type TestRunHandler(notifyIncrementalUpdate: TestRunUpdate -> unit) = - let debugProcessIdRegex = Regex(@"Process Id: (.*),") - - let tryGetDebugProcessId consoleOutput = - let m = debugProcessIdRegex.Match(consoleOutput) - - if m.Success then - let processId = m.Groups.[1].Value - Some processId - else - None - member val TestResults: TestResult ResizeArray = ResizeArray() with get, set interface ITestRunEventsHandler with - member _.HandleLogMessage(_level: TestMessageLevel, message: string) : unit = - match tryGetDebugProcessId message with - | Some processId -> notifyIncrementalUpdate (ProcessWaitingForDebugger processId) - | None -> () + member _.HandleLogMessage(_level: TestMessageLevel, _message: string) : unit = () member _.HandleRawMessage(_rawMessage: string) : unit = () @@ -99,6 +86,37 @@ module VSTestWrapper = member _.LaunchProcessWithDebuggerAttached(_testProcessStartInfo: TestProcessStartInfo) : int = raise (System.NotImplementedException()) + type TestHostLauncher(isDebug: bool, notifyProcessWaitingForDebugger: ProcessId -> unit) = + // IMPORTANT: RunTestsWithCustomTestHost says it takes an ITestHostLauncher, but it actually calls a method that is only available on ITestHostLauncher3 + + interface ITestHostLauncher3 with + member _.IsDebug: bool = isDebug + + member _.LaunchTestHost(_defaultTestHostStartInfo: TestProcessStartInfo) : int = raise (NotImplementedException()) + + member _.LaunchTestHost + (_defaultTestHostStartInfo: TestProcessStartInfo, _cancellationToken: Threading.CancellationToken) + : int = + raise (NotImplementedException()) + + member _.AttachDebuggerToProcess + (attachDebuggerInfo: AttachDebuggerInfo, _cancellationToken: Threading.CancellationToken) + : bool = + printfn $"Sya: AttachDebuggerToProcess 3" + notifyProcessWaitingForDebugger attachDebuggerInfo.ProcessId + true + + member _.AttachDebuggerToProcess(pid: int) : bool = + printfn $"Sya: AttachDebuggerToProcess pid only" + notifyProcessWaitingForDebugger pid + true + + member _.AttachDebuggerToProcess(pid: int, _cancellationToken: Threading.CancellationToken) : bool = + printfn $"Sya: AttachDebuggerToProcess pid + cancel token" + notifyProcessWaitingForDebugger pid + true + + module TestPlatformOptions = let withTestCaseFilter (options: TestPlatformOptions) filterExpression = options.TestCaseFilter <- filterExpression @@ -113,10 +131,6 @@ module VSTestWrapper = async { let consoleParams = ConsoleParameters() - if shouldDebug then - consoleParams.EnvironmentVariables <- - [ "VSTEST_HOST_DEBUG", "1" ] |> dict |> System.Collections.Generic.Dictionary - let vstest = new VsTestConsoleWrapper(vstestPath, consoleParams) let runHandler = TestRunHandler(incrementalUpdateHandler) @@ -129,7 +143,17 @@ module VSTestWrapper = vstest.CancelTestRun() printfn "Test Run Cancelled") - vstest.RunTests(sources, null, options, runHandler) + if shouldDebug then + printfn "Sya: Running debug" + + let hostLauncher = + TestHostLauncher(shouldDebug, fun pid -> incrementalUpdateHandler (ProcessWaitingForDebugger pid)) + + vstest.RunTestsWithCustomTestHost(sources, null, options, runHandler, hostLauncher) + else + printfn "Sya: Running non-debug" + vstest.RunTests(sources, null, options, runHandler) + return runHandler.TestResults |> List.ofSeq } diff --git a/src/FsAutoComplete/LspHelpers.fs b/src/FsAutoComplete/LspHelpers.fs index d04dedc8a..a3acce15d 100644 --- a/src/FsAutoComplete/LspHelpers.fs +++ b/src/FsAutoComplete/LspHelpers.fs @@ -624,7 +624,7 @@ type TestRunProgress = type TestRunUpdateNotification = | Progress of TestRunProgress - | ProcessWaitingForDebugger of processId: string + | ProcessWaitingForDebugger of processId: int type ProjectParms = { diff --git a/src/FsAutoComplete/LspHelpers.fsi b/src/FsAutoComplete/LspHelpers.fsi index 791bec7da..f50d29f2c 100644 --- a/src/FsAutoComplete/LspHelpers.fsi +++ b/src/FsAutoComplete/LspHelpers.fsi @@ -198,20 +198,19 @@ type TestDetectedNotification = { File: string Tests: TestAdapter.TestAdapterEntry array } -type TestRunRequest = - { TestCaseFilter: string option - AttachDebugger: bool} +type TestRunRequest = + { TestCaseFilter: string option + AttachDebugger: bool } -type TestDiscoveryUpdateNotification = - { Tests: TestServer.TestItem array } +type TestDiscoveryUpdateNotification = { Tests: TestServer.TestItem array } -type TestRunProgress = +type TestRunProgress = { TestResults: TestServer.TestResult array ActiveTests: TestServer.TestItem array } type TestRunUpdateNotification = | Progress of TestRunProgress - | ProcessWaitingForDebugger of processId: string + | ProcessWaitingForDebugger of processId: int type ProjectParms = { diff --git a/src/FsAutoComplete/LspServers/FSharpLspClient.fs b/src/FsAutoComplete/LspServers/FSharpLspClient.fs index 6e44ff179..aa0c3a49b 100644 --- a/src/FsAutoComplete/LspServers/FSharpLspClient.fs +++ b/src/FsAutoComplete/LspServers/FSharpLspClient.fs @@ -72,7 +72,7 @@ type FSharpLspClient(sendServerNotification: ClientNotificationSender, sendServe sendServerNotification "test/testRunProgressUpdate" (box { Content = JsonSerializer.writeJson progress }) |> Async.Ignore | ProcessWaitingForDebugger processId -> - sendServerNotification "test/processWaitingForDebugger" (box { Content = JsonSerializer.writeJson processId }) + sendServerNotification "test/processWaitingForDebugger" (box { Content = string processId }) |> Async.Ignore member x.CodeLensRefresh() = diff --git a/test/FsAutoComplete.Tests.TestExplorer/TestRunTests.fs b/test/FsAutoComplete.Tests.TestExplorer/TestRunTests.fs index 1a7adcebd..5bd8abd1c 100644 --- a/test/FsAutoComplete.Tests.TestExplorer/TestRunTests.fs +++ b/test/FsAutoComplete.Tests.TestExplorer/TestRunTests.fs @@ -80,7 +80,7 @@ let tests = <| async { use tokenSource = new System.Threading.CancellationTokenSource(2000) - let mutable actualProcessId: string option = None + let mutable actualProcessId: int option = None let updateSpy (update: VSTestWrapper.TestRunUpdate) = match update with @@ -113,12 +113,13 @@ let tests = <| async { use tokenSource = new System.Threading.CancellationTokenSource(1000) - let mutable reportedProcessIds: string list = [] + let mutable reportedProcessIds: int list = [] let updateSpy (update: VSTestWrapper.TestRunUpdate) = match update with | VSTestWrapper.TestRunUpdate.ProcessWaitingForDebugger processId -> reportedProcessIds <- processId :: reportedProcessIds + tokenSource.Cancel() | _ -> () use! _c = Async.OnCancel(fun _ -> tokenSource.Cancel()) From 3cae415434d08a89d405d859c0e3d17bbb8c2a53 Mon Sep 17 00:00:00 2001 From: farlee2121 <2847259+farlee2121@users.noreply.github.com> Date: Tue, 2 Sep 2025 11:12:12 -0500 Subject: [PATCH 24/39] Debug test runs Broke out the debug attach message because it needs to be a blocking call. VSTest will continue executing the tests as soon as AttachDebuggerToProcess returns I also fixed the LSP level test explorer tests, which were failing because the sample test projects needed to be built at part of the unit test, since the build artifacts are being cleared before every test run --- src/FsAutoComplete.Core/TestServer.fs | 30 ++--- src/FsAutoComplete/LspHelpers.fs | 4 +- src/FsAutoComplete/LspHelpers.fsi | 4 +- .../LspServers/AdaptiveServerState.fs | 38 +++--- .../LspServers/FSharpLspClient.fs | 6 +- .../LspServers/FSharpLspClient.fsi | 1 + test/FsAutoComplete.Tests.Lsp/DotnetCli.fs | 26 ++++ .../FsAutoComplete.Tests.Lsp.fsproj | 117 +++++++++--------- test/FsAutoComplete.Tests.Lsp/GoToTests.fs | 23 +--- .../TestExplorerTests.fs | 84 +++++++++---- .../TestRunTests.fs | 42 ++++--- 11 files changed, 200 insertions(+), 175 deletions(-) create mode 100644 test/FsAutoComplete.Tests.Lsp/DotnetCli.fs diff --git a/src/FsAutoComplete.Core/TestServer.fs b/src/FsAutoComplete.Core/TestServer.fs index 4b31b5b56..6bf94d5e9 100644 --- a/src/FsAutoComplete.Core/TestServer.fs +++ b/src/FsAutoComplete.Core/TestServer.fs @@ -50,10 +50,9 @@ module VSTestWrapper = } type ProcessId = int + type DidDebuggerAttach = bool - type TestRunUpdate = - | Progress of TestRunChangedEventArgs - | ProcessWaitingForDebugger of ProcessId + type TestRunUpdate = Progress of TestRunChangedEventArgs type TestRunHandler(notifyIncrementalUpdate: TestRunUpdate -> unit) = @@ -86,7 +85,7 @@ module VSTestWrapper = member _.LaunchProcessWithDebuggerAttached(_testProcessStartInfo: TestProcessStartInfo) : int = raise (System.NotImplementedException()) - type TestHostLauncher(isDebug: bool, notifyProcessWaitingForDebugger: ProcessId -> unit) = + type TestHostLauncher(isDebug: bool, onAttachDebugger: ProcessId -> DidDebuggerAttach) = // IMPORTANT: RunTestsWithCustomTestHost says it takes an ITestHostLauncher, but it actually calls a method that is only available on ITestHostLauncher3 interface ITestHostLauncher3 with @@ -102,35 +101,29 @@ module VSTestWrapper = member _.AttachDebuggerToProcess (attachDebuggerInfo: AttachDebuggerInfo, _cancellationToken: Threading.CancellationToken) : bool = - printfn $"Sya: AttachDebuggerToProcess 3" - notifyProcessWaitingForDebugger attachDebuggerInfo.ProcessId - true + onAttachDebugger attachDebuggerInfo.ProcessId - member _.AttachDebuggerToProcess(pid: int) : bool = - printfn $"Sya: AttachDebuggerToProcess pid only" - notifyProcessWaitingForDebugger pid - true + member _.AttachDebuggerToProcess(pid: int) : bool = onAttachDebugger pid member _.AttachDebuggerToProcess(pid: int, _cancellationToken: Threading.CancellationToken) : bool = - printfn $"Sya: AttachDebuggerToProcess pid + cancel token" - notifyProcessWaitingForDebugger pid - true + onAttachDebugger pid module TestPlatformOptions = let withTestCaseFilter (options: TestPlatformOptions) filterExpression = options.TestCaseFilter <- filterExpression + /// attachDebugger assumes that the debugger is attached when the method returns. The test project will continue execution as soon as attachDebugger returns let runTestsAsync (vstestPath: string) (incrementalUpdateHandler: TestRunUpdate -> unit) + (onAttachDebugger: ProcessId -> DidDebuggerAttach) (sources: TestProjectDll list) (testCaseFilter: string option) (shouldDebug: bool) : Async = async { let consoleParams = ConsoleParameters() - let vstest = new VsTestConsoleWrapper(vstestPath, consoleParams) let runHandler = TestRunHandler(incrementalUpdateHandler) @@ -144,14 +137,9 @@ module VSTestWrapper = printfn "Test Run Cancelled") if shouldDebug then - printfn "Sya: Running debug" - - let hostLauncher = - TestHostLauncher(shouldDebug, fun pid -> incrementalUpdateHandler (ProcessWaitingForDebugger pid)) - + let hostLauncher = TestHostLauncher(shouldDebug, onAttachDebugger) vstest.RunTestsWithCustomTestHost(sources, null, options, runHandler, hostLauncher) else - printfn "Sya: Running non-debug" vstest.RunTests(sources, null, options, runHandler) return runHandler.TestResults |> List.ofSeq diff --git a/src/FsAutoComplete/LspHelpers.fs b/src/FsAutoComplete/LspHelpers.fs index a3acce15d..f07267a3b 100644 --- a/src/FsAutoComplete/LspHelpers.fs +++ b/src/FsAutoComplete/LspHelpers.fs @@ -622,9 +622,7 @@ type TestRunProgress = { TestResults: TestServer.TestResult array ActiveTests: TestServer.TestItem array } -type TestRunUpdateNotification = - | Progress of TestRunProgress - | ProcessWaitingForDebugger of processId: int +type TestRunUpdateNotification = Progress of TestRunProgress type ProjectParms = { diff --git a/src/FsAutoComplete/LspHelpers.fsi b/src/FsAutoComplete/LspHelpers.fsi index f50d29f2c..f9740452b 100644 --- a/src/FsAutoComplete/LspHelpers.fsi +++ b/src/FsAutoComplete/LspHelpers.fsi @@ -208,9 +208,7 @@ type TestRunProgress = { TestResults: TestServer.TestResult array ActiveTests: TestServer.TestItem array } -type TestRunUpdateNotification = - | Progress of TestRunProgress - | ProcessWaitingForDebugger of processId: int +type TestRunUpdateNotification = Progress of TestRunProgress type ProjectParms = { diff --git a/src/FsAutoComplete/LspServers/AdaptiveServerState.fs b/src/FsAutoComplete/LspServers/AdaptiveServerState.fs index 1be08a94f..d33199389 100644 --- a/src/FsAutoComplete/LspServers/AdaptiveServerState.fs +++ b/src/FsAutoComplete/LspServers/AdaptiveServerState.fs @@ -2617,7 +2617,7 @@ type AdaptiveState return testDTOs } - member state.RunTests (testCaseFilter: string option) (attachDebugger: bool) = + member state.RunTests (testCaseFilter: string option) (shouldDebug: bool) = asyncResult { let! vstestBinary = TestServer.VSTestWrapper.tryFindVsTestFromDotnetRoot state.Config.DotNetRoot state.RootPath let! testProjects = TestProjectHelpers.tryGetTestProjects workspaceLoader state.WorkspacePaths @@ -2640,11 +2640,7 @@ type AdaptiveState use tokenSource = new CancellationTokenSource() - use! _onCancel = - Async.OnCancel(fun _ -> - printfn "Sya: cancelling update handlers" - tokenSource.Cancel() - printfn "Sya: update handlers cancelled") + use! _onCancel = Async.OnCancel(fun _ -> tokenSource.Cancel()) let incrementalUpdateHandler (runUpdate: TestServer.VSTestWrapper.TestRunUpdate) = let dto = @@ -2656,29 +2652,31 @@ type AdaptiveState progress.ActiveTests |> Seq.choose (TestServer.TestItem.tryTestCaseToDTO projectLookup.TryFind) |> Array.ofSeq } - | TestServer.VSTestWrapper.TestRunUpdate.ProcessWaitingForDebugger processId -> - printfn $"Sya: processId reported: {processId}" - TestRunUpdateNotification.ProcessWaitingForDebugger processId - Async.RunSynchronously( - async { - use! _c = Async.OnCancel(fun _ -> printfn "Sya: Cancelled notification") - do! lspClient.NotifyTestRunUpdate(dto) - }, - cancellationToken = tokenSource.Token - ) + Async.RunSynchronously(async { do! lspClient.NotifyTestRunUpdate(dto) }, cancellationToken = tokenSource.Token) + + let attachDebugger (processId: int) : bool = + let result = + Async.RunSynchronously(lspClient.AttachDebuggerForTestRun(processId), cancellationToken = tokenSource.Token) + + match result with + | Ok didAttach -> didAttach + | Error err -> + logger.warn ( + Log.setMessageI + $"Failed to attach debugger for test run with Process Id: {processId}; Error Code: {err.Code}; Error message: {err.Message}" + ) - printfn "Sya: running tests" + false let! testResults = TestServer.VSTestWrapper.runTestsAsync vstestBinary.FullName incrementalUpdateHandler + attachDebugger testProjectBinaries testCaseFilter - attachDebugger - - printfn "Sya: test results returned" + shouldDebug let resultDtos = testResults |> tryTestResultsToDTOs return resultDtos diff --git a/src/FsAutoComplete/LspServers/FSharpLspClient.fs b/src/FsAutoComplete/LspServers/FSharpLspClient.fs index aa0c3a49b..0ff850dbd 100644 --- a/src/FsAutoComplete/LspServers/FSharpLspClient.fs +++ b/src/FsAutoComplete/LspServers/FSharpLspClient.fs @@ -71,9 +71,9 @@ type FSharpLspClient(sendServerNotification: ClientNotificationSender, sendServe | Progress progress -> sendServerNotification "test/testRunProgressUpdate" (box { Content = JsonSerializer.writeJson progress }) |> Async.Ignore - | ProcessWaitingForDebugger processId -> - sendServerNotification "test/processWaitingForDebugger" (box { Content = string processId }) - |> Async.Ignore + + member __.AttachDebuggerForTestRun(processId: int) : AsyncLspResult = + sendServerRequest.Send "test/processWaitingForDebugger" (box { Content = string processId }) member x.CodeLensRefresh() = match x.ClientCapabilities with diff --git a/src/FsAutoComplete/LspServers/FSharpLspClient.fsi b/src/FsAutoComplete/LspServers/FSharpLspClient.fsi index 0c492568d..1a94a4de8 100644 --- a/src/FsAutoComplete/LspServers/FSharpLspClient.fsi +++ b/src/FsAutoComplete/LspServers/FSharpLspClient.fsi @@ -34,6 +34,7 @@ type FSharpLspClient = member NotifyTestDetected: p: TestDetectedNotification -> Async member NotifyTestDiscoveryUpdate: p: TestDiscoveryUpdateNotification -> Async member NotifyTestRunUpdate: p: TestRunUpdateNotification -> Async + member AttachDebuggerForTestRun: processId: int -> AsyncLspResult member CodeLensRefresh: unit -> Async override WindowWorkDoneProgressCreate: WorkDoneProgressCreateParams -> AsyncLspResult member Progress: ProgressToken * 'Progress -> Async diff --git a/test/FsAutoComplete.Tests.Lsp/DotnetCli.fs b/test/FsAutoComplete.Tests.Lsp/DotnetCli.fs new file mode 100644 index 000000000..e069c80ab --- /dev/null +++ b/test/FsAutoComplete.Tests.Lsp/DotnetCli.fs @@ -0,0 +1,26 @@ +namespace FsAutoComplete.Tests.Lsp.Helpers + +module DotnetCli = + open System + + let private executeProcess (wd: string) (processName: string) (processArgs: string) = + let psi = new Diagnostics.ProcessStartInfo(processName, processArgs) + psi.UseShellExecute <- false + psi.RedirectStandardOutput <- true + psi.RedirectStandardError <- true + psi.CreateNoWindow <- true + psi.WorkingDirectory <- wd + let proc = Diagnostics.Process.Start(psi) + let output = new Text.StringBuilder() + let error = new Text.StringBuilder() + proc.OutputDataReceived.Add(fun args -> output.Append(args.Data) |> ignore) + proc.ErrorDataReceived.Add(fun args -> error.Append(args.Data) |> ignore) + proc.BeginErrorReadLine() + proc.BeginOutputReadLine() + proc.WaitForExit() + + {| ExitCode = proc.ExitCode + StdOut = output.ToString() + StdErr = error.ToString() |} + + let build path = executeProcess path "dotnet" "build" diff --git a/test/FsAutoComplete.Tests.Lsp/FsAutoComplete.Tests.Lsp.fsproj b/test/FsAutoComplete.Tests.Lsp/FsAutoComplete.Tests.Lsp.fsproj index b098ce207..8b0202113 100644 --- a/test/FsAutoComplete.Tests.Lsp/FsAutoComplete.Tests.Lsp.fsproj +++ b/test/FsAutoComplete.Tests.Lsp/FsAutoComplete.Tests.Lsp.fsproj @@ -1,61 +1,62 @@ - - - Exe - net8.0 - net8.0;net9.0 - false - LatestMajor - true - true - true + + + Exe + net8.0 + net8.0;net9.0 + false + LatestMajor + true + true + true - - $(NoWarn);FS0075 - $(OtherFlags) --test:GraphBasedChecking --test:DumpCheckingGraph - - - - - FsAutoComplete.fsproj - - - FsAutoComplete.Core.fsproj - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + in .net 6 the TestCases projects don't know about these flags --> + + $(NoWarn);FS0075 + $(OtherFlags) --test:GraphBasedChecking --test:DumpCheckingGraph + + + + + FsAutoComplete.fsproj + + + FsAutoComplete.Core.fsproj + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - + project uses it's own set of Microsoft.Build dependencies which causes loading conflicts --> + + + + \ No newline at end of file diff --git a/test/FsAutoComplete.Tests.Lsp/GoToTests.fs b/test/FsAutoComplete.Tests.Lsp/GoToTests.fs index b5bd4bb1a..2a2fa472b 100644 --- a/test/FsAutoComplete.Tests.Lsp/GoToTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/GoToTests.fs @@ -12,26 +12,7 @@ open Utils.Server open Utils.Utils open Utils.TextEdit open Helpers.Expecto.ShadowedTimeouts - -let executeProcess (wd: string) (processName: string) (processArgs: string) = - let psi = new Diagnostics.ProcessStartInfo(processName, processArgs) - psi.UseShellExecute <- false - psi.RedirectStandardOutput <- true - psi.RedirectStandardError <- true - psi.CreateNoWindow <- true - psi.WorkingDirectory <- wd - let proc = Diagnostics.Process.Start(psi) - let output = new Text.StringBuilder() - let error = new Text.StringBuilder() - proc.OutputDataReceived.Add(fun args -> output.Append(args.Data) |> ignore) - proc.ErrorDataReceived.Add(fun args -> error.Append(args.Data) |> ignore) - proc.BeginErrorReadLine() - proc.BeginOutputReadLine() - proc.WaitForExit() - - {| ExitCode = proc.ExitCode - StdOut = output.ToString() - StdErr = error.ToString() |} +open FsAutoComplete.Tests.Lsp.Helpers ///GoTo tests let private gotoTest state = @@ -40,7 +21,7 @@ let private gotoTest state = let path = Path.Combine(__SOURCE_DIRECTORY__, "TestCases", "GoToTests") let csharpPath = Path.Combine(__SOURCE_DIRECTORY__, "TestCases", "GoToCSharp") - let _buildInfo = executeProcess csharpPath "dotnet" "build" + let _buildInfo = DotnetCli.build csharpPath let! (server, event) = serverInitialize path defaultConfigDto state do! waitForWorkspaceFinishedParsing event diff --git a/test/FsAutoComplete.Tests.Lsp/TestExplorerTests.fs b/test/FsAutoComplete.Tests.Lsp/TestExplorerTests.fs index 31eb9d17d..017dea93c 100644 --- a/test/FsAutoComplete.Tests.Lsp/TestExplorerTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/TestExplorerTests.fs @@ -6,6 +6,7 @@ open System.IO open FsAutoComplete.LspHelpers open System.Threading open Helpers.Expecto.ShadowedTimeouts +open FsAutoComplete.Tests.Lsp.Helpers let tests createServer = let initializeServer workspaceRoot = @@ -20,7 +21,48 @@ let tests createServer = testSequenced <| testList "TestExplorerTests" - [ testCaseAsync "it should report a processId when debug a test project" + [ testCaseAsync "it should report tests of all basic outcomes" + <| async { + let workspaceRoot = + Path.Combine(__SOURCE_DIRECTORY__, "..", "SampleTestProjects", "VSTest.XUnit.RunResults") + + let! server, _ = initializeServer workspaceRoot + use server = server + + let buildResult = DotnetCli.build workspaceRoot + Expect.equal 0 buildResult.ExitCode $"Build failed with: {buildResult.StdErr}" + + let runRequest: TestRunRequest = + { TestCaseFilter = None + AttachDebugger = false } + + let! res = server.TestRunTests(runRequest) + + let actual = + match res with + | Ok plainNotification -> + plainNotification + |> Option.get + |> _.Content + |> FsAutoComplete.JsonSerializer.readJson< + FsAutoComplete.CommandResponse.ResponseMsg + > + |> _.Data + |> List.map (fun tr -> tr.TestItem.FullName, tr.Outcome) + | Error err -> failwith $"TestRunTests returne error: {err.Message}" + + let expected = + [ ("Tests.My test", FsAutoComplete.TestServer.TestOutcome.Passed) + ("Tests.Fails", FsAutoComplete.TestServer.TestOutcome.Failed) + ("Tests.Skipped", FsAutoComplete.TestServer.TestOutcome.Skipped) + ("Tests.Exception", FsAutoComplete.TestServer.TestOutcome.Failed) + ("Tests+Nested.Test 1", FsAutoComplete.TestServer.TestOutcome.Passed) + ("Tests+Nested.Test 2", FsAutoComplete.TestServer.TestOutcome.Passed) ] + + Expect.equal (set actual) (set expected) "" + } + + testCaseAsync "it should report a processId when debugging a test project" <| async { let workspaceRoot = Path.Combine(__SOURCE_DIRECTORY__, "..", "SampleTestProjects", "VSTest.XUnit.RunResults") @@ -29,41 +71,25 @@ let tests createServer = use server = server + let buildResult = DotnetCli.build workspaceRoot + Expect.equal 0 buildResult.ExitCode $"Build failed with: {buildResult.StdErr}" + use tokenSource = new CancellationTokenSource() - let mutable processIdSpy: string option = None + let mutable processIdSpy: int option = None use! _onCancel = Async.OnCancel(fun () -> tokenSource.Cancel()) use _ = clientNotifications.Subscribe(fun (msgType: string, data: obj) -> if msgType = "test/processWaitingForDebugger" then - printfn $"Sya: update message {data}" - - let processId: string = + let processId: int = data :?> PlainNotification |> _.Content |> FsAutoComplete.JsonSerializer.readJson processIdSpy <- Some processId - tokenSource.Cancel() - - let tryParseProcessId (str: string) = - let (success, value) = System.Int32.TryParse(str) - printfn $"Sya: parsed process id: {success}, {value}. Original: {str}" - if success then Some value else None - - printfn $"Sya: process spy {processIdSpy}" - - processId - |> tryParseProcessId - |> Option.iter (fun pid -> - try - printfn $"Sya: trying to kill process {pid}" - System.Diagnostics.Process.GetProcessById(pid).Kill(true) - printfn $"Sya: killed process {pid}" - with e -> - printfn $"Sya: failed to kill process {pid}")) - - Expect.throws + tokenSource.Cancel()) + + Expect.throwsT (fun () -> let runRequest: TestRunRequest = { TestCaseFilter = None @@ -75,7 +101,11 @@ let tests createServer = )) "" - printfn "Sya: Server test run closed" Expect.isSome processIdSpy "" - printfn "Sya: test complete" + + let maybeHangingTestProcess = + System.Diagnostics.Process.GetProcesses() + |> Array.tryFind (fun p -> Some p.Id = processIdSpy) + + Expect.isNone maybeHangingTestProcess "All test processes should be canceled with the test run" } ] diff --git a/test/FsAutoComplete.Tests.TestExplorer/TestRunTests.fs b/test/FsAutoComplete.Tests.TestExplorer/TestRunTests.fs index 5bd8abd1c..dad683a81 100644 --- a/test/FsAutoComplete.Tests.TestExplorer/TestRunTests.fs +++ b/test/FsAutoComplete.Tests.TestExplorer/TestRunTests.fs @@ -20,6 +20,8 @@ let tryFindVsTest () : string = let vstestPath = tryFindVsTest () +let nullAttachDebugger _ = false + [] let tests = testList @@ -27,7 +29,7 @@ let tests = [ testCaseAsync "should return an empty list if given no projects" <| async { let expected = [] - let! actual = VSTestWrapper.runTestsAsync vstestPath ignore [] None false + let! actual = VSTestWrapper.runTestsAsync vstestPath ignore nullAttachDebugger [] None false Expect.equal actual expected "" } @@ -47,7 +49,7 @@ let tests = "VSTest.XUnit.RunResults/bin/Debug/net8.0/VSTest.XUnit.RunResults.dll" ) ] - let! runResults = VSTestWrapper.runTestsAsync vstestPath ignore sources None false + let! runResults = VSTestWrapper.runTestsAsync vstestPath ignore nullAttachDebugger sources None false let likenessOfTestResult (result: TestResult) = (result.TestCase.FullyQualifiedName, result.Outcome) let actual = runResults |> List.map likenessOfTestResult @@ -68,7 +70,13 @@ let tests = ) ] let! runResults = - VSTestWrapper.runTestsAsync vstestPath ignore sources (Some "FullyQualifiedName~Tests+Nested") false + VSTestWrapper.runTestsAsync + vstestPath + ignore + nullAttachDebugger + sources + (Some "FullyQualifiedName~Tests+Nested") + false let likenessOfTestResult (result: TestResult) = (result.TestCase.FullyQualifiedName, result.Outcome) let actual = runResults |> List.map likenessOfTestResult @@ -82,16 +90,14 @@ let tests = let mutable actualProcessId: int option = None - let updateSpy (update: VSTestWrapper.TestRunUpdate) = - match update with - | VSTestWrapper.TestRunUpdate.ProcessWaitingForDebugger processId -> - actualProcessId <- Some processId - tokenSource.Cancel() - | _ -> () + let updateSpy (processId: int) = + actualProcessId <- Some processId + tokenSource.Cancel() + false use! _c = Async.OnCancel(fun _ -> tokenSource.Cancel()) - Expect.throws + Expect.throwsT (fun () -> let sources = [ Path.Combine( @@ -100,7 +106,7 @@ let tests = ) ] Async.RunSynchronously( - VSTestWrapper.runTestsAsync vstestPath updateSpy sources None true, + VSTestWrapper.runTestsAsync vstestPath ignore updateSpy sources None true, cancellationToken = tokenSource.Token ) |> ignore) @@ -115,16 +121,14 @@ let tests = let mutable reportedProcessIds: int list = [] - let updateSpy (update: VSTestWrapper.TestRunUpdate) = - match update with - | VSTestWrapper.TestRunUpdate.ProcessWaitingForDebugger processId -> - reportedProcessIds <- processId :: reportedProcessIds - tokenSource.Cancel() - | _ -> () + let updateSpy (processId: int) = + reportedProcessIds <- processId :: reportedProcessIds + tokenSource.Cancel() + false use! _c = Async.OnCancel(fun _ -> tokenSource.Cancel()) - Expect.throws + Expect.throwsT (fun () -> let sources = [ Path.Combine( @@ -133,7 +137,7 @@ let tests = ) ] Async.RunSynchronously( - VSTestWrapper.runTestsAsync vstestPath updateSpy sources None true, + VSTestWrapper.runTestsAsync vstestPath ignore updateSpy sources None true, cancellationToken = tokenSource.Token ) |> ignore) From 0be6678e75da361f027a044039ee28c1475ebc85 Mon Sep 17 00:00:00 2001 From: farlee2121 <2847259+farlee2121@users.noreply.github.com> Date: Tue, 2 Sep 2025 11:43:02 -0500 Subject: [PATCH 25/39] Test that client environment variables are available in a test run --- .../TestExplorerTests.fs | 74 ++++++++++++++----- .../TestRunTests.fs | 13 ++-- .../VSTest.XUnit.RunResults/Tests.fs | 32 ++++---- 3 files changed, 80 insertions(+), 39 deletions(-) diff --git a/test/FsAutoComplete.Tests.Lsp/TestExplorerTests.fs b/test/FsAutoComplete.Tests.Lsp/TestExplorerTests.fs index 017dea93c..9808f4909 100644 --- a/test/FsAutoComplete.Tests.Lsp/TestExplorerTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/TestExplorerTests.fs @@ -8,6 +8,21 @@ open System.Threading open Helpers.Expecto.ShadowedTimeouts open FsAutoComplete.Tests.Lsp.Helpers +module TestRunResult = + open Ionide.LanguageServerProtocol.JsonRpc + + let tryUnwrapTestRunResult (res: LspResult) = + match res with + | Ok plainNotification -> + plainNotification + |> Option.get + |> _.Content + |> FsAutoComplete.JsonSerializer.readJson< + FsAutoComplete.CommandResponse.ResponseMsg + > + |> _.Data + | Error err -> failwith $"TestRunTests returned error: {err.Message}" + let tests createServer = let initializeServer workspaceRoot = async { @@ -39,25 +54,17 @@ let tests createServer = let! res = server.TestRunTests(runRequest) let actual = - match res with - | Ok plainNotification -> - plainNotification - |> Option.get - |> _.Content - |> FsAutoComplete.JsonSerializer.readJson< - FsAutoComplete.CommandResponse.ResponseMsg - > - |> _.Data - |> List.map (fun tr -> tr.TestItem.FullName, tr.Outcome) - | Error err -> failwith $"TestRunTests returne error: {err.Message}" + TestRunResult.tryUnwrapTestRunResult res + |> List.map (fun tr -> tr.TestItem.FullName, tr.Outcome) let expected = - [ ("Tests.My test", FsAutoComplete.TestServer.TestOutcome.Passed) - ("Tests.Fails", FsAutoComplete.TestServer.TestOutcome.Failed) - ("Tests.Skipped", FsAutoComplete.TestServer.TestOutcome.Skipped) - ("Tests.Exception", FsAutoComplete.TestServer.TestOutcome.Failed) - ("Tests+Nested.Test 1", FsAutoComplete.TestServer.TestOutcome.Passed) - ("Tests+Nested.Test 2", FsAutoComplete.TestServer.TestOutcome.Passed) ] + [ "Tests.My test", FsAutoComplete.TestServer.TestOutcome.Passed + "Tests.Fails", FsAutoComplete.TestServer.TestOutcome.Failed + "Tests.Skipped", FsAutoComplete.TestServer.TestOutcome.Skipped + "Tests.Exception", FsAutoComplete.TestServer.TestOutcome.Failed + "Tests+Nested.Test 1", FsAutoComplete.TestServer.TestOutcome.Passed + "Tests+Nested.Test 2", FsAutoComplete.TestServer.TestOutcome.Passed + "Tests.Expects environment variable", FsAutoComplete.TestServer.TestOutcome.Failed ] Expect.equal (set actual) (set expected) "" } @@ -108,4 +115,37 @@ let tests createServer = |> Array.tryFind (fun p -> Some p.Id = processIdSpy) Expect.isNone maybeHangingTestProcess "All test processes should be canceled with the test run" + } + + testCaseAsync + "it should inherit environment variables from it's parent, allowing tests to depend on environment variables" + <| async { + let workspaceRoot = + Path.Combine(__SOURCE_DIRECTORY__, "..", "SampleTestProjects", "VSTest.XUnit.RunResults") + + let! server, _ = initializeServer workspaceRoot + + use server = server + + let buildResult = DotnetCli.build workspaceRoot + Expect.equal 0 buildResult.ExitCode $"Build failed with: {buildResult.StdErr}" + + System.Environment.SetEnvironmentVariable("dd586685-08f6-410c-a9f1-84530af117ab", "Set me") + + let! response = + server.TestRunTests( + { TestCaseFilter = Some "FullyQualifiedName~Tests.Expects environment variable" + AttachDebugger = false } + ) + + let expected = + [ "Tests.Expects environment variable", FsAutoComplete.TestServer.TestOutcome.Passed ] + + let actual = + TestRunResult.tryUnwrapTestRunResult response + |> List.map (fun tr -> tr.TestItem.FullName, tr.Outcome) + + Expect.equal (set actual) (set expected) "" + + System.Environment.SetEnvironmentVariable("dd586685-08f6-410c-a9f1-84530af117ab", "") } ] diff --git a/test/FsAutoComplete.Tests.TestExplorer/TestRunTests.fs b/test/FsAutoComplete.Tests.TestExplorer/TestRunTests.fs index dad683a81..bbb0b26dd 100644 --- a/test/FsAutoComplete.Tests.TestExplorer/TestRunTests.fs +++ b/test/FsAutoComplete.Tests.TestExplorer/TestRunTests.fs @@ -36,12 +36,13 @@ let tests = testCaseAsync "should be able to report basic test run outcomes" <| async { let expected = - [ ("Tests.My test", TestOutcome.Passed) - ("Tests.Fails", TestOutcome.Failed) - ("Tests.Skipped", TestOutcome.Skipped) - ("Tests.Exception", TestOutcome.Failed) - ("Tests+Nested.Test 1", TestOutcome.Passed) - ("Tests+Nested.Test 2", TestOutcome.Passed) ] + [ "Tests.My test", TestOutcome.Passed + "Tests.Fails", TestOutcome.Failed + "Tests.Skipped", TestOutcome.Skipped + "Tests.Exception", TestOutcome.Failed + "Tests+Nested.Test 1", TestOutcome.Passed + "Tests+Nested.Test 2", TestOutcome.Passed + "Tests.Expects environment variable", TestOutcome.Failed ] let sources = [ Path.Combine( diff --git a/test/SampleTestProjects/VSTest.XUnit.RunResults/Tests.fs b/test/SampleTestProjects/VSTest.XUnit.RunResults/Tests.fs index 56ac5909b..28e514dbe 100644 --- a/test/SampleTestProjects/VSTest.XUnit.RunResults/Tests.fs +++ b/test/SampleTestProjects/VSTest.XUnit.RunResults/Tests.fs @@ -5,26 +5,26 @@ open Xunit [] let ``My test`` () = - System.Console.WriteLine("Where do I show up in the results") - Assert.True(true) + System.Console.WriteLine("Where do I show up in the results") + Assert.True(true) [] -let ``Fails`` () = - Assert.True(false) +let ``Fails`` () = Assert.True(false) -[] -let ``Skipped`` () = - Assert.True(true) +[] +let ``Skipped`` () = Assert.True(true) [] -let ``Exception`` () : unit = - failwith "Report as an exception" +let ``Exception`` () : unit = failwith "Report as an exception" -module Nested = - [] - let ``Test 1`` () : unit = - () +[] +let ``Expects environment variable`` () : unit = + Assert.Equal("Set me", Environment.GetEnvironmentVariable("dd586685-08f6-410c-a9f1-84530af117ab")) + + +module Nested = + [] + let ``Test 1`` () : unit = () - [] - let ``Test 2`` () : unit = - () + [] + let ``Test 2`` () : unit = () From 7c7b2fd7c170a4e2fe56d1c368d3a4a1fb6049ba Mon Sep 17 00:00:00 2001 From: farlee2121 <2847259+farlee2121@users.noreply.github.com> Date: Tue, 2 Sep 2025 12:41:47 -0500 Subject: [PATCH 26/39] Decouple SampleTestProjects for Lsp and TestExplorer test projects FsAutoComplete.Tests.TestExplorer expects the sample projects to be built once before the tests are run, but FsAutoComplete.Tests.Lsp expects the sample projects to be build for every test. Splitting up the sample projects reduces chances that they could break each other when the run in parallel or in random order --- FsAutoComplete.sln | 33 --------------- .../VSTest.XUnit.RunResults/Program.fs | 0 .../VSTest.XUnit.RunResults/Tests.fs | 0 .../VSTest.XUnit.RunResults.fsproj | 0 .../VSTest.XUnit.Tests/Program.fs | 0 .../VSTest.XUnit.Tests/Tests.fs | 0 .../VSTest.XUnit.Tests.fsproj | 0 .../SampleTestProjects/paket.dependencies | 0 .../SampleTestProjects/paket.lock | 0 .../TestExplorerTests.fs | 6 +-- .../FsAutoComplete.Tests.TestExplorer.fsproj | 40 ++++++++++--------- .../FsAutoComplete.Tests.TestExplorer/Main.fs | 9 +++-- .../ResourceLocators.fs | 28 +++++++------ .../VSTest.XUnit.RunResults/Program.fs | 4 ++ .../VSTest.XUnit.RunResults/Tests.fs | 30 ++++++++++++++ .../VSTest.XUnit.RunResults.fsproj | 22 ++++++++++ .../VSTest.XUnit.Tests/Program.fs | 4 ++ .../VSTest.XUnit.Tests/Tests.fs | 8 ++++ .../VSTest.XUnit.Tests.fsproj | 22 ++++++++++ .../SampleTestProjects/paket.dependencies | 0 .../SampleTestProjects/paket.lock | 1 + .../TestRunTests.fs | 15 +------ 22 files changed, 137 insertions(+), 85 deletions(-) rename test/{ => FsAutoComplete.Tests.Lsp}/SampleTestProjects/VSTest.XUnit.RunResults/Program.fs (100%) rename test/{ => FsAutoComplete.Tests.Lsp}/SampleTestProjects/VSTest.XUnit.RunResults/Tests.fs (100%) rename test/{ => FsAutoComplete.Tests.Lsp}/SampleTestProjects/VSTest.XUnit.RunResults/VSTest.XUnit.RunResults.fsproj (100%) rename test/{ => FsAutoComplete.Tests.Lsp}/SampleTestProjects/VSTest.XUnit.Tests/Program.fs (100%) rename test/{ => FsAutoComplete.Tests.Lsp}/SampleTestProjects/VSTest.XUnit.Tests/Tests.fs (100%) rename test/{ => FsAutoComplete.Tests.Lsp}/SampleTestProjects/VSTest.XUnit.Tests/VSTest.XUnit.Tests.fsproj (100%) rename test/{ => FsAutoComplete.Tests.Lsp}/SampleTestProjects/paket.dependencies (100%) rename test/{ => FsAutoComplete.Tests.Lsp}/SampleTestProjects/paket.lock (100%) create mode 100644 test/FsAutoComplete.Tests.TestExplorer/SampleTestProjects/VSTest.XUnit.RunResults/Program.fs create mode 100644 test/FsAutoComplete.Tests.TestExplorer/SampleTestProjects/VSTest.XUnit.RunResults/Tests.fs create mode 100644 test/FsAutoComplete.Tests.TestExplorer/SampleTestProjects/VSTest.XUnit.RunResults/VSTest.XUnit.RunResults.fsproj create mode 100644 test/FsAutoComplete.Tests.TestExplorer/SampleTestProjects/VSTest.XUnit.Tests/Program.fs create mode 100644 test/FsAutoComplete.Tests.TestExplorer/SampleTestProjects/VSTest.XUnit.Tests/Tests.fs create mode 100644 test/FsAutoComplete.Tests.TestExplorer/SampleTestProjects/VSTest.XUnit.Tests/VSTest.XUnit.Tests.fsproj create mode 100644 test/FsAutoComplete.Tests.TestExplorer/SampleTestProjects/paket.dependencies create mode 100644 test/FsAutoComplete.Tests.TestExplorer/SampleTestProjects/paket.lock diff --git a/FsAutoComplete.sln b/FsAutoComplete.sln index c9895c88a..8d8bd12ea 100644 --- a/FsAutoComplete.sln +++ b/FsAutoComplete.sln @@ -29,12 +29,6 @@ Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "benchmarks", "benchmarks\be EndProject Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "FsAutoComplete.Tests.TestExplorer", "test\FsAutoComplete.Tests.TestExplorer\FsAutoComplete.Tests.TestExplorer.fsproj", "{C6CE23AF-EDCE-49ED-AC2F-8AD595C30CD8}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SampleTestProjects", "SampleTestProjects", "{B093584B-EEA2-D526-B6EB-76B356047302}" -EndProject -Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "VSTest.XUnit.Tests", "test\SampleTestProjects\VSTest.XUnit.Tests\VSTest.XUnit.Tests.fsproj", "{2C67BFBE-0963-4BD6-B292-7CF838C153C3}" -EndProject -Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "VSTest.XUnit.RunResults", "test\SampleTestProjects\VSTest.XUnit.RunResults\VSTest.XUnit.RunResults.fsproj", "{C7B0B178-79BD-4071-B134-17ABA3168EC4}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -141,30 +135,6 @@ Global {C6CE23AF-EDCE-49ED-AC2F-8AD595C30CD8}.Release|x64.Build.0 = Release|Any CPU {C6CE23AF-EDCE-49ED-AC2F-8AD595C30CD8}.Release|x86.ActiveCfg = Release|Any CPU {C6CE23AF-EDCE-49ED-AC2F-8AD595C30CD8}.Release|x86.Build.0 = Release|Any CPU - {2C67BFBE-0963-4BD6-B292-7CF838C153C3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2C67BFBE-0963-4BD6-B292-7CF838C153C3}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2C67BFBE-0963-4BD6-B292-7CF838C153C3}.Debug|x64.ActiveCfg = Debug|Any CPU - {2C67BFBE-0963-4BD6-B292-7CF838C153C3}.Debug|x64.Build.0 = Debug|Any CPU - {2C67BFBE-0963-4BD6-B292-7CF838C153C3}.Debug|x86.ActiveCfg = Debug|Any CPU - {2C67BFBE-0963-4BD6-B292-7CF838C153C3}.Debug|x86.Build.0 = Debug|Any CPU - {2C67BFBE-0963-4BD6-B292-7CF838C153C3}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2C67BFBE-0963-4BD6-B292-7CF838C153C3}.Release|Any CPU.Build.0 = Release|Any CPU - {2C67BFBE-0963-4BD6-B292-7CF838C153C3}.Release|x64.ActiveCfg = Release|Any CPU - {2C67BFBE-0963-4BD6-B292-7CF838C153C3}.Release|x64.Build.0 = Release|Any CPU - {2C67BFBE-0963-4BD6-B292-7CF838C153C3}.Release|x86.ActiveCfg = Release|Any CPU - {2C67BFBE-0963-4BD6-B292-7CF838C153C3}.Release|x86.Build.0 = Release|Any CPU - {C7B0B178-79BD-4071-B134-17ABA3168EC4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C7B0B178-79BD-4071-B134-17ABA3168EC4}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C7B0B178-79BD-4071-B134-17ABA3168EC4}.Debug|x64.ActiveCfg = Debug|Any CPU - {C7B0B178-79BD-4071-B134-17ABA3168EC4}.Debug|x64.Build.0 = Debug|Any CPU - {C7B0B178-79BD-4071-B134-17ABA3168EC4}.Debug|x86.ActiveCfg = Debug|Any CPU - {C7B0B178-79BD-4071-B134-17ABA3168EC4}.Debug|x86.Build.0 = Debug|Any CPU - {C7B0B178-79BD-4071-B134-17ABA3168EC4}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C7B0B178-79BD-4071-B134-17ABA3168EC4}.Release|Any CPU.Build.0 = Release|Any CPU - {C7B0B178-79BD-4071-B134-17ABA3168EC4}.Release|x64.ActiveCfg = Release|Any CPU - {C7B0B178-79BD-4071-B134-17ABA3168EC4}.Release|x64.Build.0 = Release|Any CPU - {C7B0B178-79BD-4071-B134-17ABA3168EC4}.Release|x86.ActiveCfg = Release|Any CPU - {C7B0B178-79BD-4071-B134-17ABA3168EC4}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -175,9 +145,6 @@ Global {14C55B44-2063-4891-98BE-8184CAB1BE87} = {443E0B8D-9AD0-436E-A331-E8CC12965F07} {C58701B0-D8E3-4B68-A7DE-8524C95F86C0} = {443E0B8D-9AD0-436E-A331-E8CC12965F07} {C6CE23AF-EDCE-49ED-AC2F-8AD595C30CD8} = {443E0B8D-9AD0-436E-A331-E8CC12965F07} - {B093584B-EEA2-D526-B6EB-76B356047302} = {443E0B8D-9AD0-436E-A331-E8CC12965F07} - {2C67BFBE-0963-4BD6-B292-7CF838C153C3} = {B093584B-EEA2-D526-B6EB-76B356047302} - {C7B0B178-79BD-4071-B134-17ABA3168EC4} = {B093584B-EEA2-D526-B6EB-76B356047302} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {1C4EE83B-632A-4929-8C96-38F14254229E} diff --git a/test/SampleTestProjects/VSTest.XUnit.RunResults/Program.fs b/test/FsAutoComplete.Tests.Lsp/SampleTestProjects/VSTest.XUnit.RunResults/Program.fs similarity index 100% rename from test/SampleTestProjects/VSTest.XUnit.RunResults/Program.fs rename to test/FsAutoComplete.Tests.Lsp/SampleTestProjects/VSTest.XUnit.RunResults/Program.fs diff --git a/test/SampleTestProjects/VSTest.XUnit.RunResults/Tests.fs b/test/FsAutoComplete.Tests.Lsp/SampleTestProjects/VSTest.XUnit.RunResults/Tests.fs similarity index 100% rename from test/SampleTestProjects/VSTest.XUnit.RunResults/Tests.fs rename to test/FsAutoComplete.Tests.Lsp/SampleTestProjects/VSTest.XUnit.RunResults/Tests.fs diff --git a/test/SampleTestProjects/VSTest.XUnit.RunResults/VSTest.XUnit.RunResults.fsproj b/test/FsAutoComplete.Tests.Lsp/SampleTestProjects/VSTest.XUnit.RunResults/VSTest.XUnit.RunResults.fsproj similarity index 100% rename from test/SampleTestProjects/VSTest.XUnit.RunResults/VSTest.XUnit.RunResults.fsproj rename to test/FsAutoComplete.Tests.Lsp/SampleTestProjects/VSTest.XUnit.RunResults/VSTest.XUnit.RunResults.fsproj diff --git a/test/SampleTestProjects/VSTest.XUnit.Tests/Program.fs b/test/FsAutoComplete.Tests.Lsp/SampleTestProjects/VSTest.XUnit.Tests/Program.fs similarity index 100% rename from test/SampleTestProjects/VSTest.XUnit.Tests/Program.fs rename to test/FsAutoComplete.Tests.Lsp/SampleTestProjects/VSTest.XUnit.Tests/Program.fs diff --git a/test/SampleTestProjects/VSTest.XUnit.Tests/Tests.fs b/test/FsAutoComplete.Tests.Lsp/SampleTestProjects/VSTest.XUnit.Tests/Tests.fs similarity index 100% rename from test/SampleTestProjects/VSTest.XUnit.Tests/Tests.fs rename to test/FsAutoComplete.Tests.Lsp/SampleTestProjects/VSTest.XUnit.Tests/Tests.fs diff --git a/test/SampleTestProjects/VSTest.XUnit.Tests/VSTest.XUnit.Tests.fsproj b/test/FsAutoComplete.Tests.Lsp/SampleTestProjects/VSTest.XUnit.Tests/VSTest.XUnit.Tests.fsproj similarity index 100% rename from test/SampleTestProjects/VSTest.XUnit.Tests/VSTest.XUnit.Tests.fsproj rename to test/FsAutoComplete.Tests.Lsp/SampleTestProjects/VSTest.XUnit.Tests/VSTest.XUnit.Tests.fsproj diff --git a/test/SampleTestProjects/paket.dependencies b/test/FsAutoComplete.Tests.Lsp/SampleTestProjects/paket.dependencies similarity index 100% rename from test/SampleTestProjects/paket.dependencies rename to test/FsAutoComplete.Tests.Lsp/SampleTestProjects/paket.dependencies diff --git a/test/SampleTestProjects/paket.lock b/test/FsAutoComplete.Tests.Lsp/SampleTestProjects/paket.lock similarity index 100% rename from test/SampleTestProjects/paket.lock rename to test/FsAutoComplete.Tests.Lsp/SampleTestProjects/paket.lock diff --git a/test/FsAutoComplete.Tests.Lsp/TestExplorerTests.fs b/test/FsAutoComplete.Tests.Lsp/TestExplorerTests.fs index 9808f4909..b8e517af2 100644 --- a/test/FsAutoComplete.Tests.Lsp/TestExplorerTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/TestExplorerTests.fs @@ -39,7 +39,7 @@ let tests createServer = [ testCaseAsync "it should report tests of all basic outcomes" <| async { let workspaceRoot = - Path.Combine(__SOURCE_DIRECTORY__, "..", "SampleTestProjects", "VSTest.XUnit.RunResults") + Path.Combine(__SOURCE_DIRECTORY__, "SampleTestProjects", "VSTest.XUnit.RunResults") let! server, _ = initializeServer workspaceRoot use server = server @@ -72,7 +72,7 @@ let tests createServer = testCaseAsync "it should report a processId when debugging a test project" <| async { let workspaceRoot = - Path.Combine(__SOURCE_DIRECTORY__, "..", "SampleTestProjects", "VSTest.XUnit.RunResults") + Path.Combine(__SOURCE_DIRECTORY__, "SampleTestProjects", "VSTest.XUnit.RunResults") let! server, clientNotifications = initializeServer workspaceRoot @@ -121,7 +121,7 @@ let tests createServer = "it should inherit environment variables from it's parent, allowing tests to depend on environment variables" <| async { let workspaceRoot = - Path.Combine(__SOURCE_DIRECTORY__, "..", "SampleTestProjects", "VSTest.XUnit.RunResults") + Path.Combine(__SOURCE_DIRECTORY__, "SampleTestProjects", "VSTest.XUnit.RunResults") let! server, _ = initializeServer workspaceRoot diff --git a/test/FsAutoComplete.Tests.TestExplorer/FsAutoComplete.Tests.TestExplorer.fsproj b/test/FsAutoComplete.Tests.TestExplorer/FsAutoComplete.Tests.TestExplorer.fsproj index e04d4326f..cabe97df5 100644 --- a/test/FsAutoComplete.Tests.TestExplorer/FsAutoComplete.Tests.TestExplorer.fsproj +++ b/test/FsAutoComplete.Tests.TestExplorer/FsAutoComplete.Tests.TestExplorer.fsproj @@ -1,19 +1,23 @@ - - - Exe - net8.0 - false - - - - - - - - - - - - - + + + Exe + net8.0 + false + + + + + + + + + + + + + dotnet build $(MSBuildProjectDirectory)/SampleTestProjects/VSTest.XUnit.Tests/ + dotnet build $(MSBuildProjectDirectory)/SampleTestProjects/VSTest.XUnit.RunResults/ + + + \ No newline at end of file diff --git a/test/FsAutoComplete.Tests.TestExplorer/Main.fs b/test/FsAutoComplete.Tests.TestExplorer/Main.fs index b2099cc91..43147fdfd 100644 --- a/test/FsAutoComplete.Tests.TestExplorer/Main.fs +++ b/test/FsAutoComplete.Tests.TestExplorer/Main.fs @@ -1,6 +1,7 @@ -module FsAutoComplete.Tests.TestExplorer +namespace FsAutoComplete.Tests.TestExplorer + open Expecto -[] -let main argv = - Tests.runTestsInAssemblyWithCLIArgs [] argv +module Program = + [] + let main argv = Tests.runTestsInAssemblyWithCLIArgs [] argv diff --git a/test/FsAutoComplete.Tests.TestExplorer/ResourceLocators.fs b/test/FsAutoComplete.Tests.TestExplorer/ResourceLocators.fs index 19255000a..8d0cbbcbb 100644 --- a/test/FsAutoComplete.Tests.TestExplorer/ResourceLocators.fs +++ b/test/FsAutoComplete.Tests.TestExplorer/ResourceLocators.fs @@ -1,16 +1,18 @@ -module internal ResourceLocators +module internal ResourceLocators - open FsAutoComplete.TestServer - open System.IO - let tryFindVsTest () : string = - let dotnetBinary = - Ionide.ProjInfo.Paths.dotnetRoot.Value - |> Option.defaultWith (fun () -> failwith "Couldn't find dotnet root. The dotnet sdk must be installed to run these tests") +open FsAutoComplete.TestServer +open System.IO - let cwd = System.Environment.CurrentDirectory |> Some - - VSTestWrapper.tryFindVsTestFromDotnetRoot dotnetBinary.FullName cwd - |> Result.defaultWith failwith - |> _.FullName +let tryFindVsTest () : string = + let dotnetBinary = + Ionide.ProjInfo.Paths.dotnetRoot.Value + |> Option.defaultWith (fun () -> + failwith "Couldn't find dotnet root. The dotnet sdk must be installed to run these tests") - let sampleProjectsRootDir = Path.Combine(__SOURCE_DIRECTORY__, "../SampleTestProjects") + let cwd = System.Environment.CurrentDirectory |> Some + + VSTestWrapper.tryFindVsTestFromDotnetRoot dotnetBinary.FullName cwd + |> Result.defaultWith failwith + |> _.FullName + +let sampleProjectsRootDir = Path.Combine(__SOURCE_DIRECTORY__, "SampleTestProjects") diff --git a/test/FsAutoComplete.Tests.TestExplorer/SampleTestProjects/VSTest.XUnit.RunResults/Program.fs b/test/FsAutoComplete.Tests.TestExplorer/SampleTestProjects/VSTest.XUnit.RunResults/Program.fs new file mode 100644 index 000000000..31dc4f735 --- /dev/null +++ b/test/FsAutoComplete.Tests.TestExplorer/SampleTestProjects/VSTest.XUnit.RunResults/Program.fs @@ -0,0 +1,4 @@ +module Program + +[] +let main _ = 0 diff --git a/test/FsAutoComplete.Tests.TestExplorer/SampleTestProjects/VSTest.XUnit.RunResults/Tests.fs b/test/FsAutoComplete.Tests.TestExplorer/SampleTestProjects/VSTest.XUnit.RunResults/Tests.fs new file mode 100644 index 000000000..28e514dbe --- /dev/null +++ b/test/FsAutoComplete.Tests.TestExplorer/SampleTestProjects/VSTest.XUnit.RunResults/Tests.fs @@ -0,0 +1,30 @@ +module Tests + +open System +open Xunit + +[] +let ``My test`` () = + System.Console.WriteLine("Where do I show up in the results") + Assert.True(true) + +[] +let ``Fails`` () = Assert.True(false) + +[] +let ``Skipped`` () = Assert.True(true) + +[] +let ``Exception`` () : unit = failwith "Report as an exception" + +[] +let ``Expects environment variable`` () : unit = + Assert.Equal("Set me", Environment.GetEnvironmentVariable("dd586685-08f6-410c-a9f1-84530af117ab")) + + +module Nested = + [] + let ``Test 1`` () : unit = () + + [] + let ``Test 2`` () : unit = () diff --git a/test/FsAutoComplete.Tests.TestExplorer/SampleTestProjects/VSTest.XUnit.RunResults/VSTest.XUnit.RunResults.fsproj b/test/FsAutoComplete.Tests.TestExplorer/SampleTestProjects/VSTest.XUnit.RunResults/VSTest.XUnit.RunResults.fsproj new file mode 100644 index 000000000..1a4224efd --- /dev/null +++ b/test/FsAutoComplete.Tests.TestExplorer/SampleTestProjects/VSTest.XUnit.RunResults/VSTest.XUnit.RunResults.fsproj @@ -0,0 +1,22 @@ + + + + net8.0 + false + false + + + + + + + + + + + + + + + + diff --git a/test/FsAutoComplete.Tests.TestExplorer/SampleTestProjects/VSTest.XUnit.Tests/Program.fs b/test/FsAutoComplete.Tests.TestExplorer/SampleTestProjects/VSTest.XUnit.Tests/Program.fs new file mode 100644 index 000000000..31dc4f735 --- /dev/null +++ b/test/FsAutoComplete.Tests.TestExplorer/SampleTestProjects/VSTest.XUnit.Tests/Program.fs @@ -0,0 +1,4 @@ +module Program + +[] +let main _ = 0 diff --git a/test/FsAutoComplete.Tests.TestExplorer/SampleTestProjects/VSTest.XUnit.Tests/Tests.fs b/test/FsAutoComplete.Tests.TestExplorer/SampleTestProjects/VSTest.XUnit.Tests/Tests.fs new file mode 100644 index 000000000..7f70fae19 --- /dev/null +++ b/test/FsAutoComplete.Tests.TestExplorer/SampleTestProjects/VSTest.XUnit.Tests/Tests.fs @@ -0,0 +1,8 @@ +module Tests + +open System +open Xunit + +[] +let ``My test`` () = + Assert.True(true) diff --git a/test/FsAutoComplete.Tests.TestExplorer/SampleTestProjects/VSTest.XUnit.Tests/VSTest.XUnit.Tests.fsproj b/test/FsAutoComplete.Tests.TestExplorer/SampleTestProjects/VSTest.XUnit.Tests/VSTest.XUnit.Tests.fsproj new file mode 100644 index 000000000..1a4224efd --- /dev/null +++ b/test/FsAutoComplete.Tests.TestExplorer/SampleTestProjects/VSTest.XUnit.Tests/VSTest.XUnit.Tests.fsproj @@ -0,0 +1,22 @@ + + + + net8.0 + false + false + + + + + + + + + + + + + + + + diff --git a/test/FsAutoComplete.Tests.TestExplorer/SampleTestProjects/paket.dependencies b/test/FsAutoComplete.Tests.TestExplorer/SampleTestProjects/paket.dependencies new file mode 100644 index 000000000..e69de29bb diff --git a/test/FsAutoComplete.Tests.TestExplorer/SampleTestProjects/paket.lock b/test/FsAutoComplete.Tests.TestExplorer/SampleTestProjects/paket.lock new file mode 100644 index 000000000..d3f5a12fa --- /dev/null +++ b/test/FsAutoComplete.Tests.TestExplorer/SampleTestProjects/paket.lock @@ -0,0 +1 @@ + diff --git a/test/FsAutoComplete.Tests.TestExplorer/TestRunTests.fs b/test/FsAutoComplete.Tests.TestExplorer/TestRunTests.fs index bbb0b26dd..809acd7ee 100644 --- a/test/FsAutoComplete.Tests.TestExplorer/TestRunTests.fs +++ b/test/FsAutoComplete.Tests.TestExplorer/TestRunTests.fs @@ -5,20 +5,7 @@ open FsAutoComplete.TestServer open Microsoft.VisualStudio.TestPlatform.ObjectModel open System.IO -let tryFindVsTest () : string = - let dotnetBinary = - Ionide.ProjInfo.Paths.dotnetRoot.Value - |> Option.defaultWith (fun () -> - failwith "Couldn't find dotnet root. The dotnet sdk must be installed to run these tests") - - let cwd = System.Environment.CurrentDirectory |> Some - - VSTestWrapper.tryFindVsTestFromDotnetRoot dotnetBinary.FullName cwd - |> Result.defaultWith failwith - |> _.FullName - - -let vstestPath = tryFindVsTest () +let vstestPath = ResourceLocators.tryFindVsTest () let nullAttachDebugger _ = false From 43c398e6287238b8b6eda965be16591f5bc2d15f Mon Sep 17 00:00:00 2001 From: farlee2121 <2847259+farlee2121@users.noreply.github.com> Date: Tue, 2 Sep 2025 13:42:01 -0500 Subject: [PATCH 27/39] Forward test logs --- src/FsAutoComplete.Core/TestServer.fs | 27 ++++++++++-------- src/FsAutoComplete/LspHelpers.fs | 5 ++-- src/FsAutoComplete/LspHelpers.fsi | 5 ++-- .../LspServers/AdaptiveServerState.fs | 28 +++++++++++-------- .../LspServers/FSharpLspClient.fs | 8 ++---- .../LspServers/FSharpLspClient.fsi | 2 +- 6 files changed, 39 insertions(+), 36 deletions(-) diff --git a/src/FsAutoComplete.Core/TestServer.fs b/src/FsAutoComplete.Core/TestServer.fs index 6bf94d5e9..99d485a69 100644 --- a/src/FsAutoComplete.Core/TestServer.fs +++ b/src/FsAutoComplete.Core/TestServer.fs @@ -12,7 +12,7 @@ module VSTestWrapper = type TestProjectDll = string - type private TestDiscoveryHandler(notifyIncrementalUpdate: TestCase list -> unit) = + type private TestDiscoveryHandler(notifyDiscoveryProgress: TestCase list -> unit) = member val DiscoveredTests: TestCase ResizeArray = ResizeArray() with get, set @@ -20,14 +20,14 @@ module VSTestWrapper = member this.HandleDiscoveredTests(discoveredTestCases: System.Collections.Generic.IEnumerable) : unit = if (not << isNull) discoveredTestCases then this.DiscoveredTests.AddRange(discoveredTestCases) - notifyIncrementalUpdate (discoveredTestCases |> List.ofSeq) + notifyDiscoveryProgress (discoveredTestCases |> List.ofSeq) member this.HandleDiscoveryComplete (_totalTests: int64, lastChunk: System.Collections.Generic.IEnumerable, _isAborted: bool) : unit = if (not << isNull) lastChunk then this.DiscoveredTests.AddRange(lastChunk) - notifyIncrementalUpdate (lastChunk |> List.ofSeq) + notifyDiscoveryProgress (lastChunk |> List.ofSeq) member this.HandleLogMessage(_level: TestMessageLevel, _message: string) : unit = () @@ -35,13 +35,13 @@ module VSTestWrapper = let discoverTestsAsync (vstestPath: string) - (incrementalUpdateHandler: TestCase list -> unit) + (onDiscoveryProgress: TestCase list -> unit) (sources: TestProjectDll list) : Async = async { let consoleParams = ConsoleParameters() let vstest = new VsTestConsoleWrapper(vstestPath, consoleParams) - let discoveryHandler = TestDiscoveryHandler(incrementalUpdateHandler) + let discoveryHandler = TestDiscoveryHandler(onDiscoveryProgress) use! _onCancel = Async.OnCancel(fun () -> vstest.CancelDiscovery()) @@ -52,14 +52,17 @@ module VSTestWrapper = type ProcessId = int type DidDebuggerAttach = bool - type TestRunUpdate = Progress of TestRunChangedEventArgs + type TestRunUpdate = + | Progress of TestRunChangedEventArgs + | LogMessage of string - type TestRunHandler(notifyIncrementalUpdate: TestRunUpdate -> unit) = + type TestRunHandler(notifyTestRunProgress: TestRunUpdate -> unit) = member val TestResults: TestResult ResizeArray = ResizeArray() with get, set interface ITestRunEventsHandler with - member _.HandleLogMessage(_level: TestMessageLevel, _message: string) : unit = () + member _.HandleLogMessage(level: TestMessageLevel, message: string) : unit = + notifyTestRunProgress (LogMessage $"[{level}] {message}") member _.HandleRawMessage(_rawMessage: string) : unit = () @@ -72,7 +75,7 @@ module VSTestWrapper = ) : unit = if ((not << isNull) lastChunkArgs && (not << isNull) lastChunkArgs.NewTestResults) then this.TestResults.AddRange(lastChunkArgs.NewTestResults) - notifyIncrementalUpdate (Progress lastChunkArgs) + notifyTestRunProgress (Progress lastChunkArgs) member this.HandleTestRunStatsChange(testRunChangedArgs: TestRunChangedEventArgs) : unit = if @@ -80,7 +83,7 @@ module VSTestWrapper = && (not << isNull) testRunChangedArgs.NewTestResults) then this.TestResults.AddRange(testRunChangedArgs.NewTestResults) - notifyIncrementalUpdate (Progress testRunChangedArgs) + notifyTestRunProgress (Progress testRunChangedArgs) member _.LaunchProcessWithDebuggerAttached(_testProcessStartInfo: TestProcessStartInfo) : int = raise (System.NotImplementedException()) @@ -116,7 +119,7 @@ module VSTestWrapper = /// attachDebugger assumes that the debugger is attached when the method returns. The test project will continue execution as soon as attachDebugger returns let runTestsAsync (vstestPath: string) - (incrementalUpdateHandler: TestRunUpdate -> unit) + (onTestRunProgress: TestRunUpdate -> unit) (onAttachDebugger: ProcessId -> DidDebuggerAttach) (sources: TestProjectDll list) (testCaseFilter: string option) @@ -125,7 +128,7 @@ module VSTestWrapper = async { let consoleParams = ConsoleParameters() let vstest = new VsTestConsoleWrapper(vstestPath, consoleParams) - let runHandler = TestRunHandler(incrementalUpdateHandler) + let runHandler = TestRunHandler(onTestRunProgress) let options = new TestPlatformOptions() testCaseFilter |> Option.iter (TestPlatformOptions.withTestCaseFilter options) diff --git a/src/FsAutoComplete/LspHelpers.fs b/src/FsAutoComplete/LspHelpers.fs index f07267a3b..d3c2d7d8a 100644 --- a/src/FsAutoComplete/LspHelpers.fs +++ b/src/FsAutoComplete/LspHelpers.fs @@ -619,11 +619,10 @@ type TestRunRequest = type TestDiscoveryUpdateNotification = { Tests: TestServer.TestItem array } type TestRunProgress = - { TestResults: TestServer.TestResult array + { TestLogs: string array + TestResults: TestServer.TestResult array ActiveTests: TestServer.TestItem array } -type TestRunUpdateNotification = Progress of TestRunProgress - type ProjectParms = { /// Project file to compile diff --git a/src/FsAutoComplete/LspHelpers.fsi b/src/FsAutoComplete/LspHelpers.fsi index f9740452b..196272ce3 100644 --- a/src/FsAutoComplete/LspHelpers.fsi +++ b/src/FsAutoComplete/LspHelpers.fsi @@ -205,11 +205,10 @@ type TestRunRequest = type TestDiscoveryUpdateNotification = { Tests: TestServer.TestItem array } type TestRunProgress = - { TestResults: TestServer.TestResult array + { TestLogs: string array + TestResults: TestServer.TestResult array ActiveTests: TestServer.TestItem array } -type TestRunUpdateNotification = Progress of TestRunProgress - type ProjectParms = { /// Project file to compile diff --git a/src/FsAutoComplete/LspServers/AdaptiveServerState.fs b/src/FsAutoComplete/LspServers/AdaptiveServerState.fs index d33199389..8ddf86460 100644 --- a/src/FsAutoComplete/LspServers/AdaptiveServerState.fs +++ b/src/FsAutoComplete/LspServers/AdaptiveServerState.fs @@ -2605,12 +2605,12 @@ type AdaptiveState testCases |> List.choose (TestServer.TestItem.tryTestCaseToDTO projectLookup.TryFind) - let incrementalUpdateHandler (tests: Microsoft.VisualStudio.TestPlatform.ObjectModel.TestCase list) = + let onDiscoveryProgress (tests: Microsoft.VisualStudio.TestPlatform.ObjectModel.TestCase list) = lspClient.NotifyTestDiscoveryUpdate({ Tests = tests |> tryTestCasesToDTOs |> Array.ofList }) |> Async.RunSynchronously let! testCases = - TestServer.VSTestWrapper.discoverTestsAsync vstestBinary.FullName incrementalUpdateHandler testProjectBinaries + TestServer.VSTestWrapper.discoverTestsAsync vstestBinary.FullName onDiscoveryProgress testProjectBinaries let testDTOs: TestServer.TestItem list = testCases |> tryTestCasesToDTOs @@ -2642,20 +2642,24 @@ type AdaptiveState use! _onCancel = Async.OnCancel(fun _ -> tokenSource.Cancel()) - let incrementalUpdateHandler (runUpdate: TestServer.VSTestWrapper.TestRunUpdate) = + let onTestRunProgress (runUpdate: TestServer.VSTestWrapper.TestRunUpdate) = let dto = match runUpdate with | TestServer.VSTestWrapper.TestRunUpdate.Progress progress -> - TestRunUpdateNotification.Progress - { TestResults = progress.NewTestResults |> List.ofSeq |> tryTestResultsToDTOs |> Array.ofSeq - ActiveTests = - progress.ActiveTests - |> Seq.choose (TestServer.TestItem.tryTestCaseToDTO projectLookup.TryFind) - |> Array.ofSeq } + { TestLogs = [||] + TestResults = progress.NewTestResults |> List.ofSeq |> tryTestResultsToDTOs |> Array.ofSeq + ActiveTests = + progress.ActiveTests + |> Seq.choose (TestServer.TestItem.tryTestCaseToDTO projectLookup.TryFind) + |> Array.ofSeq } + | TestServer.VSTestWrapper.TestRunUpdate.LogMessage message -> + { TestLogs = [| message |] + TestResults = [||] + ActiveTests = [||] } Async.RunSynchronously(async { do! lspClient.NotifyTestRunUpdate(dto) }, cancellationToken = tokenSource.Token) - let attachDebugger (processId: int) : bool = + let onAttachDebugger (processId: int) : bool = let result = Async.RunSynchronously(lspClient.AttachDebuggerForTestRun(processId), cancellationToken = tokenSource.Token) @@ -2672,8 +2676,8 @@ type AdaptiveState let! testResults = TestServer.VSTestWrapper.runTestsAsync vstestBinary.FullName - incrementalUpdateHandler - attachDebugger + onTestRunProgress + onAttachDebugger testProjectBinaries testCaseFilter shouldDebug diff --git a/src/FsAutoComplete/LspServers/FSharpLspClient.fs b/src/FsAutoComplete/LspServers/FSharpLspClient.fs index 0ff850dbd..8b5cabd41 100644 --- a/src/FsAutoComplete/LspServers/FSharpLspClient.fs +++ b/src/FsAutoComplete/LspServers/FSharpLspClient.fs @@ -66,11 +66,9 @@ type FSharpLspClient(sendServerNotification: ClientNotificationSender, sendServe sendServerNotification "test/testDiscoveryUpdate" (box { Content = JsonSerializer.writeJson p }) |> Async.Ignore - member __.NotifyTestRunUpdate(p: TestRunUpdateNotification) = - match p with - | Progress progress -> - sendServerNotification "test/testRunProgressUpdate" (box { Content = JsonSerializer.writeJson progress }) - |> Async.Ignore + member __.NotifyTestRunUpdate(p: TestRunProgress) = + sendServerNotification "test/testRunProgressUpdate" (box { Content = JsonSerializer.writeJson p }) + |> Async.Ignore member __.AttachDebuggerForTestRun(processId: int) : AsyncLspResult = sendServerRequest.Send "test/processWaitingForDebugger" (box { Content = string processId }) diff --git a/src/FsAutoComplete/LspServers/FSharpLspClient.fsi b/src/FsAutoComplete/LspServers/FSharpLspClient.fsi index 1a94a4de8..0576df043 100644 --- a/src/FsAutoComplete/LspServers/FSharpLspClient.fsi +++ b/src/FsAutoComplete/LspServers/FSharpLspClient.fsi @@ -33,7 +33,7 @@ type FSharpLspClient = member NotifyDocumentAnalyzed: p: DocumentAnalyzedNotification -> Async member NotifyTestDetected: p: TestDetectedNotification -> Async member NotifyTestDiscoveryUpdate: p: TestDiscoveryUpdateNotification -> Async - member NotifyTestRunUpdate: p: TestRunUpdateNotification -> Async + member NotifyTestRunUpdate: p: TestRunProgress -> Async member AttachDebuggerForTestRun: processId: int -> AsyncLspResult member CodeLensRefresh: unit -> Async override WindowWorkDoneProgressCreate: WorkDoneProgressCreateParams -> AsyncLspResult From d4bb314f638dac569ee1ef8eceb3e63b1f00279b Mon Sep 17 00:00:00 2001 From: farlee2121 <2847259+farlee2121@users.noreply.github.com> Date: Thu, 4 Sep 2025 14:42:41 -0500 Subject: [PATCH 28/39] Allow client to limit which test projects are run This was mainly motivated by test debugging. Before this feature, the debugger would launch for every test project even if you only wanted to debug one test. With this change, it'll only launch the one project. This also partially mitigates the bug where NUnit doesn't respect filters, now those test won't show unless some tests from an nunit project are selected --- src/FsAutoComplete/LspHelpers.fs | 3 +- src/FsAutoComplete/LspHelpers.fsi | 3 +- .../LspServers/AdaptiveFSharpLspServer.fs | 2 +- .../LspServers/AdaptiveServerState.fs | 12 +- .../LspServers/AdaptiveServerState.fsi | 2 +- test/FsAutoComplete.Tests.Lsp/DotnetCli.fs | 6 +- .../TestExplorerTests.fs | 154 ++++++++++++++++-- 7 files changed, 162 insertions(+), 20 deletions(-) diff --git a/src/FsAutoComplete/LspHelpers.fs b/src/FsAutoComplete/LspHelpers.fs index d3c2d7d8a..a02632a1a 100644 --- a/src/FsAutoComplete/LspHelpers.fs +++ b/src/FsAutoComplete/LspHelpers.fs @@ -613,7 +613,8 @@ type TestDetectedNotification = Tests: TestAdapter.TestAdapterEntry array } type TestRunRequest = - { TestCaseFilter: string option + { LimitToProjects: FilePath list option + TestCaseFilter: string option AttachDebugger: bool } type TestDiscoveryUpdateNotification = { Tests: TestServer.TestItem array } diff --git a/src/FsAutoComplete/LspHelpers.fsi b/src/FsAutoComplete/LspHelpers.fsi index 196272ce3..c42073ab7 100644 --- a/src/FsAutoComplete/LspHelpers.fsi +++ b/src/FsAutoComplete/LspHelpers.fsi @@ -199,7 +199,8 @@ type TestDetectedNotification = Tests: TestAdapter.TestAdapterEntry array } type TestRunRequest = - { TestCaseFilter: string option + { LimitToProjects: FilePath list option + TestCaseFilter: string option AttachDebugger: bool } type TestDiscoveryUpdateNotification = { Tests: TestServer.TestItem array } diff --git a/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs b/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs index 4934ba74e..b07c2f207 100644 --- a/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs +++ b/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs @@ -3068,7 +3068,7 @@ type AdaptiveFSharpLspServer override this.TestRunTests(p: TestRunRequest) : Async> = asyncResult { let! testDTOs = - state.RunTests p.TestCaseFilter p.AttachDebugger + state.RunTests p.LimitToProjects p.TestCaseFilter p.AttachDebugger |> AsyncResult.mapError (fun msg -> JsonRpc.Error.InternalError msg) return Some { Content = CommandResponse.runTests FsAutoComplete.JsonSerializer.writeJson testDTOs } diff --git a/src/FsAutoComplete/LspServers/AdaptiveServerState.fs b/src/FsAutoComplete/LspServers/AdaptiveServerState.fs index 8ddf86460..b216cab4b 100644 --- a/src/FsAutoComplete/LspServers/AdaptiveServerState.fs +++ b/src/FsAutoComplete/LspServers/AdaptiveServerState.fs @@ -2617,11 +2617,19 @@ type AdaptiveState return testDTOs } - member state.RunTests (testCaseFilter: string option) (shouldDebug: bool) = + member state.RunTests (limitToProjects: FilePath list option) (testCaseFilter: string option) (shouldDebug: bool) = asyncResult { let! vstestBinary = TestServer.VSTestWrapper.tryFindVsTestFromDotnetRoot state.Config.DotNetRoot state.RootPath let! testProjects = TestProjectHelpers.tryGetTestProjects workspaceLoader state.WorkspacePaths - let testProjectBinaries = testProjects |> List.map _.TargetPath + + let filteredTestProjects = + match limitToProjects with + | None -> testProjects + | Some specifiedProjects -> + let specifiedProjectsSet = specifiedProjects |> List.map Path.GetFullPath |> set + testProjects |> List.filter (_.ProjectFileName >> specifiedProjectsSet.Contains) + + let testProjectBinaries = filteredTestProjects |> List.map _.TargetPath let projectLookup = testProjects |> Seq.map (fun p -> p.TargetPath, p) |> Map.ofSeq diff --git a/src/FsAutoComplete/LspServers/AdaptiveServerState.fsi b/src/FsAutoComplete/LspServers/AdaptiveServerState.fsi index 3a4b0620d..edff7ef19 100644 --- a/src/FsAutoComplete/LspServers/AdaptiveServerState.fsi +++ b/src/FsAutoComplete/LspServers/AdaptiveServerState.fsi @@ -120,7 +120,7 @@ type AdaptiveState = member GetAllDeclarations: unit -> Async<(string * NavigationTopLevelDeclaration array) array> member GlyphToSymbolKind: (FSharpGlyph -> SymbolKind option) member DiscoverTests: unit -> Async> - member RunTests: string option -> bool -> Async> + member RunTests: FilePath list option -> string option -> bool -> Async> /// /// Signals the server to cancel an operation that is associated with the given progress token. /// diff --git a/test/FsAutoComplete.Tests.Lsp/DotnetCli.fs b/test/FsAutoComplete.Tests.Lsp/DotnetCli.fs index e069c80ab..df5f4bc16 100644 --- a/test/FsAutoComplete.Tests.Lsp/DotnetCli.fs +++ b/test/FsAutoComplete.Tests.Lsp/DotnetCli.fs @@ -3,13 +3,13 @@ namespace FsAutoComplete.Tests.Lsp.Helpers module DotnetCli = open System - let private executeProcess (wd: string) (processName: string) (processArgs: string) = + let private executeProcess (wd: string option) (processName: string) (processArgs: string) = let psi = new Diagnostics.ProcessStartInfo(processName, processArgs) psi.UseShellExecute <- false psi.RedirectStandardOutput <- true psi.RedirectStandardError <- true psi.CreateNoWindow <- true - psi.WorkingDirectory <- wd + wd |> Option.iter (fun wd -> psi.WorkingDirectory <- wd) let proc = Diagnostics.Process.Start(psi) let output = new Text.StringBuilder() let error = new Text.StringBuilder() @@ -23,4 +23,4 @@ module DotnetCli = StdOut = output.ToString() StdErr = error.ToString() |} - let build path = executeProcess path "dotnet" "build" + let build path = executeProcess None "dotnet" $"build {path}" diff --git a/test/FsAutoComplete.Tests.Lsp/TestExplorerTests.fs b/test/FsAutoComplete.Tests.Lsp/TestExplorerTests.fs index b8e517af2..84373af6e 100644 --- a/test/FsAutoComplete.Tests.Lsp/TestExplorerTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/TestExplorerTests.fs @@ -23,6 +23,34 @@ module TestRunResult = |> _.Data | Error err -> failwith $"TestRunTests returned error: {err.Message}" +module ExpectedTests = + let XUnitRunResults = + [ "Tests.My test", FsAutoComplete.TestServer.TestOutcome.Passed + "Tests.Fails", FsAutoComplete.TestServer.TestOutcome.Failed + "Tests.Skipped", FsAutoComplete.TestServer.TestOutcome.Skipped + "Tests.Exception", FsAutoComplete.TestServer.TestOutcome.Failed + "Tests+Nested.Test 1", FsAutoComplete.TestServer.TestOutcome.Passed + "Tests+Nested.Test 2", FsAutoComplete.TestServer.TestOutcome.Passed + "Tests.Expects environment variable", FsAutoComplete.TestServer.TestOutcome.Failed ] + +module Workspace = + + let build workspaceRoot = + let dir = DirectoryInfo workspaceRoot + + if not dir.Exists then + failwith $"Target workspace doesn't exist: {workspaceRoot}" + + let projects = dir.GetFiles("*.?sproj", SearchOption.AllDirectories) + + for project in projects do + let buildResult = DotnetCli.build project.FullName + + Expect.equal + 0 + buildResult.ExitCode + $"Workspace build failed with: {buildResult.StdErr} \nProject: {project.FullName}" + let tests createServer = let initializeServer workspaceRoot = async { @@ -48,7 +76,8 @@ let tests createServer = Expect.equal 0 buildResult.ExitCode $"Build failed with: {buildResult.StdErr}" let runRequest: TestRunRequest = - { TestCaseFilter = None + { LimitToProjects = None + TestCaseFilter = None AttachDebugger = false } let! res = server.TestRunTests(runRequest) @@ -57,14 +86,7 @@ let tests createServer = TestRunResult.tryUnwrapTestRunResult res |> List.map (fun tr -> tr.TestItem.FullName, tr.Outcome) - let expected = - [ "Tests.My test", FsAutoComplete.TestServer.TestOutcome.Passed - "Tests.Fails", FsAutoComplete.TestServer.TestOutcome.Failed - "Tests.Skipped", FsAutoComplete.TestServer.TestOutcome.Skipped - "Tests.Exception", FsAutoComplete.TestServer.TestOutcome.Failed - "Tests+Nested.Test 1", FsAutoComplete.TestServer.TestOutcome.Passed - "Tests+Nested.Test 2", FsAutoComplete.TestServer.TestOutcome.Passed - "Tests.Expects environment variable", FsAutoComplete.TestServer.TestOutcome.Failed ] + let expected = ExpectedTests.XUnitRunResults Expect.equal (set actual) (set expected) "" } @@ -99,7 +121,8 @@ let tests createServer = Expect.throwsT (fun () -> let runRequest: TestRunRequest = - { TestCaseFilter = None + { LimitToProjects = None + TestCaseFilter = None AttachDebugger = true } Async.RunSynchronously( @@ -134,7 +157,8 @@ let tests createServer = let! response = server.TestRunTests( - { TestCaseFilter = Some "FullyQualifiedName~Tests.Expects environment variable" + { LimitToProjects = None + TestCaseFilter = Some "FullyQualifiedName~Tests.Expects environment variable" AttachDebugger = false } ) @@ -148,4 +172,112 @@ let tests createServer = Expect.equal (set actual) (set expected) "" System.Environment.SetEnvironmentVariable("dd586685-08f6-410c-a9f1-84530af117ab", "") + } + + testCaseAsync "it should ignore test project filters that aren't projects in the workspace" + <| async { + let workspaceRoot = + Path.Combine(__SOURCE_DIRECTORY__, "SampleTestProjects", "VSTest.XUnit.RunResults") + + let! server, _ = initializeServer workspaceRoot + + use server = server + + let buildResult = DotnetCli.build workspaceRoot + Expect.equal 0 buildResult.ExitCode $"Build failed with: {buildResult.StdErr}" + + let! response = + server.TestRunTests( + { LimitToProjects = Some [ Path.Combine(__SOURCE_DIRECTORY__, "SampleTestProjects", "Nope", "Nope.fsproj") ] + TestCaseFilter = None + AttachDebugger = false } + ) + + let expected = [] + + let actual = + TestRunResult.tryUnwrapTestRunResult response + |> List.map (fun tr -> tr.TestItem.FullName, tr.Outcome) + + Expect.equal (set actual) (set expected) "" + } + + testCaseAsync "it should run only test projects in the project filter when specified" + <| async { + let workspaceRoot = Path.Combine(__SOURCE_DIRECTORY__, "SampleTestProjects") + + let! server, _ = initializeServer workspaceRoot + + use server = server + + Workspace.build workspaceRoot + + let! response = + server.TestRunTests( + { LimitToProjects = + Some + [ Path.Combine( + __SOURCE_DIRECTORY__, + "SampleTestProjects", + "VSTest.XUnit.RunResults", + "VSTest.XUnit.RunResults.fsproj" + ) ] + TestCaseFilter = None + AttachDebugger = false } + ) + + let expected = ExpectedTests.XUnitRunResults + + let actual = + TestRunResult.tryUnwrapTestRunResult response + |> List.map (fun tr -> tr.TestItem.FullName, tr.Outcome) + + Expect.equal (set actual) (set expected) "" + } + testCaseAsync "it should only attach the debugger for projects in the project filter if filter is specified" + <| async { + let workspaceRoot = Path.Combine(__SOURCE_DIRECTORY__, "SampleTestProjects") + + let! server, clientNotifications = initializeServer workspaceRoot + + use server = server + + Workspace.build workspaceRoot + + use tokenSource = new CancellationTokenSource() + let mutable processIdSpy: int list = [] + use! _onCancel = Async.OnCancel(fun () -> tokenSource.Cancel()) + + use _ = + clientNotifications.Subscribe(fun (msgType: string, data: obj) -> + if msgType = "test/processWaitingForDebugger" then + let processId: int = + data :?> PlainNotification + |> _.Content + |> FsAutoComplete.JsonSerializer.readJson + + processIdSpy <- processId :: processIdSpy + tokenSource.Cancel()) + + Expect.throwsT + (fun () -> + let runRequest: TestRunRequest = + { LimitToProjects = + Some + [ Path.Combine( + __SOURCE_DIRECTORY__, + "SampleTestProjects", + "VSTest.XUnit.RunResults", + "VSTest.XUnit.RunResults.fsproj" + ) ] + TestCaseFilter = None + AttachDebugger = true } + + Async.RunSynchronously( + server.TestRunTests(runRequest) |> Async.Ignore, + cancellationToken = tokenSource.Token + )) + "" + + Expect.hasLength processIdSpy 1 "Should only launch one process to debug" } ] From 16d7222641e3d657c9c0322d09e134e2655578f9 Mon Sep 17 00:00:00 2001 From: farlee2121 <2847259+farlee2121@users.noreply.github.com> Date: Thu, 4 Sep 2025 19:31:50 -0500 Subject: [PATCH 29/39] Workaround for NUnit not respecting test filters NUnit doesn't respect test filters when VSTest is in DesignMode, which it is by default with VsTestConsoleWrapper https://github.com/ionide/FsAutoComplete/pull/1383#issuecomment-3245590606 --- src/FsAutoComplete.Core/TestServer.fs | 16 ++++++-- .../FsAutoComplete.Tests.TestExplorer.fsproj | 1 + .../VSTest.NUnit/UnitTest1.fs | 12 ++++++ .../VSTest.NUnit/VSTest.NUnit.fsproj | 24 ++++++++++++ .../SampleTestProjects/paket.dependencies | 2 + .../TestRunTests.fs | 39 ++++++++++++++++--- 6 files changed, 85 insertions(+), 9 deletions(-) create mode 100644 test/FsAutoComplete.Tests.TestExplorer/SampleTestProjects/VSTest.NUnit/UnitTest1.fs create mode 100644 test/FsAutoComplete.Tests.TestExplorer/SampleTestProjects/VSTest.NUnit/VSTest.NUnit.fsproj diff --git a/src/FsAutoComplete.Core/TestServer.fs b/src/FsAutoComplete.Core/TestServer.fs index 99d485a69..37673c9b0 100644 --- a/src/FsAutoComplete.Core/TestServer.fs +++ b/src/FsAutoComplete.Core/TestServer.fs @@ -115,8 +115,15 @@ module VSTestWrapper = module TestPlatformOptions = let withTestCaseFilter (options: TestPlatformOptions) filterExpression = options.TestCaseFilter <- filterExpression - - /// attachDebugger assumes that the debugger is attached when the method returns. The test project will continue execution as soon as attachDebugger returns + module RunSettings = + let defaultRunSettings = + " + + False + +" + + /// onAttachDebugger assumes that the debugger is attached when the method returns. The test project will continue execution as soon as attachDebugger returns let runTestsAsync (vstestPath: string) (onTestRunProgress: TestRunUpdate -> unit) @@ -129,6 +136,7 @@ module VSTestWrapper = let consoleParams = ConsoleParameters() let vstest = new VsTestConsoleWrapper(vstestPath, consoleParams) let runHandler = TestRunHandler(onTestRunProgress) + let runSettings = RunSettings.defaultRunSettings let options = new TestPlatformOptions() testCaseFilter |> Option.iter (TestPlatformOptions.withTestCaseFilter options) @@ -141,9 +149,9 @@ module VSTestWrapper = if shouldDebug then let hostLauncher = TestHostLauncher(shouldDebug, onAttachDebugger) - vstest.RunTestsWithCustomTestHost(sources, null, options, runHandler, hostLauncher) + vstest.RunTestsWithCustomTestHost(sources, runSettings, options, runHandler, hostLauncher) else - vstest.RunTests(sources, null, options, runHandler) + vstest.RunTests(sources, runSettings, options, runHandler) return runHandler.TestResults |> List.ofSeq } diff --git a/test/FsAutoComplete.Tests.TestExplorer/FsAutoComplete.Tests.TestExplorer.fsproj b/test/FsAutoComplete.Tests.TestExplorer/FsAutoComplete.Tests.TestExplorer.fsproj index cabe97df5..7e80883d0 100644 --- a/test/FsAutoComplete.Tests.TestExplorer/FsAutoComplete.Tests.TestExplorer.fsproj +++ b/test/FsAutoComplete.Tests.TestExplorer/FsAutoComplete.Tests.TestExplorer.fsproj @@ -17,6 +17,7 @@ dotnet build $(MSBuildProjectDirectory)/SampleTestProjects/VSTest.XUnit.Tests/ dotnet build $(MSBuildProjectDirectory)/SampleTestProjects/VSTest.XUnit.RunResults/ + dotnet build $(MSBuildProjectDirectory)/SampleTestProjects/VSTest.NUnit/ diff --git a/test/FsAutoComplete.Tests.TestExplorer/SampleTestProjects/VSTest.NUnit/UnitTest1.fs b/test/FsAutoComplete.Tests.TestExplorer/SampleTestProjects/VSTest.NUnit/UnitTest1.fs new file mode 100644 index 000000000..69949b6d0 --- /dev/null +++ b/test/FsAutoComplete.Tests.TestExplorer/SampleTestProjects/VSTest.NUnit/UnitTest1.fs @@ -0,0 +1,12 @@ +module VSTest.NUnit + +open NUnit.Framework + +[] +let Setup () = () + +[] +let Test1 () = Assert.Pass() + +[] +let Test2 () = Assert.Pass() diff --git a/test/FsAutoComplete.Tests.TestExplorer/SampleTestProjects/VSTest.NUnit/VSTest.NUnit.fsproj b/test/FsAutoComplete.Tests.TestExplorer/SampleTestProjects/VSTest.NUnit/VSTest.NUnit.fsproj new file mode 100644 index 000000000..a6bee0e16 --- /dev/null +++ b/test/FsAutoComplete.Tests.TestExplorer/SampleTestProjects/VSTest.NUnit/VSTest.NUnit.fsproj @@ -0,0 +1,24 @@ + + + + net8.0 + latest + false + true + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + diff --git a/test/FsAutoComplete.Tests.TestExplorer/SampleTestProjects/paket.dependencies b/test/FsAutoComplete.Tests.TestExplorer/SampleTestProjects/paket.dependencies index e69de29bb..07e4b673d 100644 --- a/test/FsAutoComplete.Tests.TestExplorer/SampleTestProjects/paket.dependencies +++ b/test/FsAutoComplete.Tests.TestExplorer/SampleTestProjects/paket.dependencies @@ -0,0 +1,2 @@ +# SampleTestProjects aren't managed with paket +# This is here to prevent the FsAutoComplete paket from trying manage sample project dependencies \ No newline at end of file diff --git a/test/FsAutoComplete.Tests.TestExplorer/TestRunTests.fs b/test/FsAutoComplete.Tests.TestExplorer/TestRunTests.fs index 809acd7ee..180e1f5dd 100644 --- a/test/FsAutoComplete.Tests.TestExplorer/TestRunTests.fs +++ b/test/FsAutoComplete.Tests.TestExplorer/TestRunTests.fs @@ -13,14 +13,14 @@ let nullAttachDebugger _ = false let tests = testList "VSTestWrapper Test Run" - [ testCaseAsync "should return an empty list if given no projects" + [ testCaseAsync "it should return an empty list if given no projects" <| async { let expected = [] let! actual = VSTestWrapper.runTestsAsync vstestPath ignore nullAttachDebugger [] None false Expect.equal actual expected "" } - testCaseAsync "should be able to report basic test run outcomes" + testCaseAsync "it should be able to report basic test run outcomes" <| async { let expected = [ "Tests.My test", TestOutcome.Passed @@ -45,7 +45,7 @@ let tests = Expect.equal (set actual) (set expected) "" } - testCaseAsync "should run only tests that match the case filter" + testCaseAsync "it should run only tests that match the case filter" <| async { let expected = [ ("Tests+Nested.Test 1", TestOutcome.Passed) @@ -72,7 +72,36 @@ let tests = Expect.equal (set actual) (set expected) "" } - testCaseAsync "should report processIds when debugging is on" + testCaseAsync "it should respect test filters on NUnit projects" + <| async { + // NOTE: This is an NUnit bug. NUnit doesn't respect filters when VSTest is in Design Mode, which VsTestConsoleWrapper is by default + // https://github.com/ionide/FsAutoComplete/pull/1383#issuecomment-3245590606 + let expected = [ "VSTest.NUnit.Test1", TestOutcome.Passed ] + + let sources = + [ Path.Combine(ResourceLocators.sampleProjectsRootDir, "VSTest.NUnit/bin/Debug/net8.0/VSTest.NUnit.dll") ] + + let onTestRunProgress (progress: VSTestWrapper.TestRunUpdate) = + match progress with + | VSTestWrapper.TestRunUpdate.LogMessage message -> printfn $"Sya: {message}" + | _ -> () + + let! runResults = + VSTestWrapper.runTestsAsync + vstestPath + onTestRunProgress + nullAttachDebugger + sources + (Some "FullyQualifiedName~Test1") + false + + let likenessOfTestResult (result: TestResult) = (result.TestCase.FullyQualifiedName, result.Outcome) + let actual = runResults |> List.map likenessOfTestResult + + Expect.equal (set actual) (set expected) "" + } + + testCaseAsync "it should report processIds when debugging is on" <| async { use tokenSource = new System.Threading.CancellationTokenSource(2000) @@ -103,7 +132,7 @@ let tests = Expect.isSome actualProcessId "Expected runTest to report a processId" } - testCaseAsync "should report a processId only once per process" + testCaseAsync "it should report a processId only once per process" <| async { use tokenSource = new System.Threading.CancellationTokenSource(1000) From 740cfb15dd57d2448718075eadeaf3d620ae19e6 Mon Sep 17 00:00:00 2001 From: farlee2121 <2847259+farlee2121@users.noreply.github.com> Date: Tue, 9 Sep 2025 17:04:57 -0500 Subject: [PATCH 30/39] Forward test discovery logs to the client for improved error diagnostics --- src/FsAutoComplete.Core/TestServer.fs | 19 +++++++++------ src/FsAutoComplete/LspHelpers.fs | 8 +++++-- src/FsAutoComplete/LspHelpers.fsi | 8 +++++-- .../LspServers/AdaptiveServerState.fs | 23 +++++++++++++++---- .../TestRunTests.fs | 7 +----- 5 files changed, 43 insertions(+), 22 deletions(-) diff --git a/src/FsAutoComplete.Core/TestServer.fs b/src/FsAutoComplete.Core/TestServer.fs index 37673c9b0..3401f0e0b 100644 --- a/src/FsAutoComplete.Core/TestServer.fs +++ b/src/FsAutoComplete.Core/TestServer.fs @@ -12,7 +12,11 @@ module VSTestWrapper = type TestProjectDll = string - type private TestDiscoveryHandler(notifyDiscoveryProgress: TestCase list -> unit) = + type TestDiscoveryUpdate = + | Progress of TestCase list + | LogMessage of TestMessageLevel * string + + type private TestDiscoveryHandler(notifyDiscoveryProgress: TestDiscoveryUpdate -> unit) = member val DiscoveredTests: TestCase ResizeArray = ResizeArray() with get, set @@ -20,22 +24,23 @@ module VSTestWrapper = member this.HandleDiscoveredTests(discoveredTestCases: System.Collections.Generic.IEnumerable) : unit = if (not << isNull) discoveredTestCases then this.DiscoveredTests.AddRange(discoveredTestCases) - notifyDiscoveryProgress (discoveredTestCases |> List.ofSeq) + notifyDiscoveryProgress (discoveredTestCases |> List.ofSeq |> Progress) member this.HandleDiscoveryComplete (_totalTests: int64, lastChunk: System.Collections.Generic.IEnumerable, _isAborted: bool) : unit = if (not << isNull) lastChunk then this.DiscoveredTests.AddRange(lastChunk) - notifyDiscoveryProgress (lastChunk |> List.ofSeq) + notifyDiscoveryProgress (lastChunk |> List.ofSeq |> Progress) - member this.HandleLogMessage(_level: TestMessageLevel, _message: string) : unit = () + member this.HandleLogMessage(level: TestMessageLevel, message: string) : unit = + notifyDiscoveryProgress (LogMessage(level, message)) member this.HandleRawMessage(_rawMessage: string) : unit = () let discoverTestsAsync (vstestPath: string) - (onDiscoveryProgress: TestCase list -> unit) + (onDiscoveryProgress: TestDiscoveryUpdate -> unit) (sources: TestProjectDll list) : Async = async { @@ -54,7 +59,7 @@ module VSTestWrapper = type TestRunUpdate = | Progress of TestRunChangedEventArgs - | LogMessage of string + | LogMessage of TestMessageLevel * string type TestRunHandler(notifyTestRunProgress: TestRunUpdate -> unit) = @@ -62,7 +67,7 @@ module VSTestWrapper = interface ITestRunEventsHandler with member _.HandleLogMessage(level: TestMessageLevel, message: string) : unit = - notifyTestRunProgress (LogMessage $"[{level}] {message}") + notifyTestRunProgress (LogMessage(level, message)) member _.HandleRawMessage(_rawMessage: string) : unit = () diff --git a/src/FsAutoComplete/LspHelpers.fs b/src/FsAutoComplete/LspHelpers.fs index a02632a1a..9d7ffa24b 100644 --- a/src/FsAutoComplete/LspHelpers.fs +++ b/src/FsAutoComplete/LspHelpers.fs @@ -617,10 +617,14 @@ type TestRunRequest = TestCaseFilter: string option AttachDebugger: bool } -type TestDiscoveryUpdateNotification = { Tests: TestServer.TestItem array } +type TestLogMessage = { Level: string; Message: string } + +type TestDiscoveryUpdateNotification = + { Tests: TestServer.TestItem array + TestLogs: TestLogMessage array } type TestRunProgress = - { TestLogs: string array + { TestLogs: TestLogMessage array TestResults: TestServer.TestResult array ActiveTests: TestServer.TestItem array } diff --git a/src/FsAutoComplete/LspHelpers.fsi b/src/FsAutoComplete/LspHelpers.fsi index c42073ab7..25b93cefe 100644 --- a/src/FsAutoComplete/LspHelpers.fsi +++ b/src/FsAutoComplete/LspHelpers.fsi @@ -203,10 +203,14 @@ type TestRunRequest = TestCaseFilter: string option AttachDebugger: bool } -type TestDiscoveryUpdateNotification = { Tests: TestServer.TestItem array } +type TestLogMessage = { Level: string; Message: string } + +type TestDiscoveryUpdateNotification = + { Tests: TestServer.TestItem array + TestLogs: TestLogMessage array } type TestRunProgress = - { TestLogs: string array + { TestLogs: TestLogMessage array TestResults: TestServer.TestResult array ActiveTests: TestServer.TestItem array } diff --git a/src/FsAutoComplete/LspServers/AdaptiveServerState.fs b/src/FsAutoComplete/LspServers/AdaptiveServerState.fs index b216cab4b..7ed72b109 100644 --- a/src/FsAutoComplete/LspServers/AdaptiveServerState.fs +++ b/src/FsAutoComplete/LspServers/AdaptiveServerState.fs @@ -2605,9 +2605,20 @@ type AdaptiveState testCases |> List.choose (TestServer.TestItem.tryTestCaseToDTO projectLookup.TryFind) - let onDiscoveryProgress (tests: Microsoft.VisualStudio.TestPlatform.ObjectModel.TestCase list) = - lspClient.NotifyTestDiscoveryUpdate({ Tests = tests |> tryTestCasesToDTOs |> Array.ofList }) - |> Async.RunSynchronously + let onDiscoveryProgress (update: TestServer.VSTestWrapper.TestDiscoveryUpdate) = + let dto = + match update with + | TestServer.VSTestWrapper.TestDiscoveryUpdate.Progress tests -> + { Tests = tests |> tryTestCasesToDTOs |> Array.ofList + TestLogs = [||] } + | TestServer.VSTestWrapper.TestDiscoveryUpdate.LogMessage(level, message) -> + { Tests = [||] + TestLogs = + [| { Message = message + Level = string level } |] } + + + lspClient.NotifyTestDiscoveryUpdate(dto) |> Async.RunSynchronously let! testCases = TestServer.VSTestWrapper.discoverTestsAsync vstestBinary.FullName onDiscoveryProgress testProjectBinaries @@ -2660,8 +2671,10 @@ type AdaptiveState progress.ActiveTests |> Seq.choose (TestServer.TestItem.tryTestCaseToDTO projectLookup.TryFind) |> Array.ofSeq } - | TestServer.VSTestWrapper.TestRunUpdate.LogMessage message -> - { TestLogs = [| message |] + | TestServer.VSTestWrapper.TestRunUpdate.LogMessage(level, message) -> + { TestLogs = + [| { Message = message + Level = string level } |] TestResults = [||] ActiveTests = [||] } diff --git a/test/FsAutoComplete.Tests.TestExplorer/TestRunTests.fs b/test/FsAutoComplete.Tests.TestExplorer/TestRunTests.fs index 180e1f5dd..0b4f69113 100644 --- a/test/FsAutoComplete.Tests.TestExplorer/TestRunTests.fs +++ b/test/FsAutoComplete.Tests.TestExplorer/TestRunTests.fs @@ -81,15 +81,10 @@ let tests = let sources = [ Path.Combine(ResourceLocators.sampleProjectsRootDir, "VSTest.NUnit/bin/Debug/net8.0/VSTest.NUnit.dll") ] - let onTestRunProgress (progress: VSTestWrapper.TestRunUpdate) = - match progress with - | VSTestWrapper.TestRunUpdate.LogMessage message -> printfn $"Sya: {message}" - | _ -> () - let! runResults = VSTestWrapper.runTestsAsync vstestPath - onTestRunProgress + ignore nullAttachDebugger sources (Some "FullyQualifiedName~Test1") From c967543552dd91e8385ad27c79c1f6cb270a6ae8 Mon Sep 17 00:00:00 2001 From: farlee2121 <2847259+farlee2121@users.noreply.github.com> Date: Wed, 10 Sep 2025 16:27:18 -0500 Subject: [PATCH 31/39] Show error notification in client for expectable test discovery issues (i.e projects not restored or built) --- .../LspServers/AdaptiveServerState.fs | 22 +++++++++++++++++++ .../TestExplorerTests.fs | 14 +++++++++++- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/src/FsAutoComplete/LspServers/AdaptiveServerState.fs b/src/FsAutoComplete/LspServers/AdaptiveServerState.fs index 7ed72b109..a39d31ec2 100644 --- a/src/FsAutoComplete/LspServers/AdaptiveServerState.fs +++ b/src/FsAutoComplete/LspServers/AdaptiveServerState.fs @@ -2599,6 +2599,28 @@ type AdaptiveState let! testProjects = TestProjectHelpers.tryGetTestProjects workspaceLoader state.WorkspacePaths let testProjectBinaries = testProjects |> List.map _.TargetPath + if testProjects |> List.isEmpty then + let message = "No test projects found. Make sure you've restored your projects" + + do! + lspClient.WindowShowMessage( + { Type = MessageType.Error + Message = message } + ) + + return! (Error message) + elif testProjectBinaries |> List.filter File.Exists |> List.isEmpty then + let message = + "No binaries found for test projects. Make sure you've built your projects" + + do! + lspClient.WindowShowMessage( + { Type = MessageType.Error + Message = message } + ) + + return! (Error message) + let tryTestCasesToDTOs testCases = let projectLookup = testProjects |> Seq.map (fun p -> p.TargetPath, p) |> Map.ofSeq diff --git a/test/FsAutoComplete.Tests.Lsp/TestExplorerTests.fs b/test/FsAutoComplete.Tests.Lsp/TestExplorerTests.fs index 84373af6e..5f835313f 100644 --- a/test/FsAutoComplete.Tests.Lsp/TestExplorerTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/TestExplorerTests.fs @@ -64,7 +64,19 @@ let tests createServer = testSequenced <| testList "TestExplorerTests" - [ testCaseAsync "it should report tests of all basic outcomes" + [ testCaseAsync "it should error if the workspace hasn't been built" + <| async { + let workspaceRoot = + Path.Combine(__SOURCE_DIRECTORY__, "SampleTestProjects", "VSTest.XUnit.RunResults") + + let! server, _ = initializeServer workspaceRoot + use server = server + + let! res = server.TestDiscoverTests() + + Expect.isTrue res.IsError "" + } + testCaseAsync "it should report tests of all basic outcomes" <| async { let workspaceRoot = Path.Combine(__SOURCE_DIRECTORY__, "SampleTestProjects", "VSTest.XUnit.RunResults") From 72728d2f2dccf2ef4ba41cd05987219982b3b11a Mon Sep 17 00:00:00 2001 From: farlee2121 <2847259+farlee2121@users.noreply.github.com> Date: Thu, 11 Sep 2025 17:52:39 -0500 Subject: [PATCH 32/39] Separate discovery vs run lsp-level test explorer tests --- .../TestExplorerTests.fs | 406 ++++++++++-------- 1 file changed, 225 insertions(+), 181 deletions(-) diff --git a/test/FsAutoComplete.Tests.Lsp/TestExplorerTests.fs b/test/FsAutoComplete.Tests.Lsp/TestExplorerTests.fs index 5f835313f..0a518001e 100644 --- a/test/FsAutoComplete.Tests.Lsp/TestExplorerTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/TestExplorerTests.fs @@ -23,8 +23,23 @@ module TestRunResult = |> _.Data | Error err -> failwith $"TestRunTests returned error: {err.Message}" +module TestDiscoveryResult = + open Ionide.LanguageServerProtocol.JsonRpc + + let tryUnwrapTestDiscoveryResult (res: LspResult) = + match res with + | Ok plainNotification -> + plainNotification + |> Option.get + |> _.Content + |> FsAutoComplete.JsonSerializer.readJson< + FsAutoComplete.CommandResponse.ResponseMsg + > + |> _.Data + | Error err -> failwith $"TestDiscoverTests returned error: {err.Message}" + module ExpectedTests = - let XUnitRunResults = + let VSTestXUnitRunResults = [ "Tests.My test", FsAutoComplete.TestServer.TestOutcome.Passed "Tests.Fails", FsAutoComplete.TestServer.TestOutcome.Failed "Tests.Skipped", FsAutoComplete.TestServer.TestOutcome.Skipped @@ -33,6 +48,9 @@ module ExpectedTests = "Tests+Nested.Test 2", FsAutoComplete.TestServer.TestOutcome.Passed "Tests.Expects environment variable", FsAutoComplete.TestServer.TestOutcome.Failed ] + let VSTestXunitTests = + [ "Tests.My test", FsAutoComplete.TestServer.TestOutcome.Passed ] + module Workspace = let build workspaceRoot = @@ -64,232 +82,258 @@ let tests createServer = testSequenced <| testList "TestExplorerTests" - [ testCaseAsync "it should error if the workspace hasn't been built" - <| async { - let workspaceRoot = - Path.Combine(__SOURCE_DIRECTORY__, "SampleTestProjects", "VSTest.XUnit.RunResults") - - let! server, _ = initializeServer workspaceRoot - use server = server + [ testList + "DiscoverTests" + [ testCaseAsync "it should error if the workspace hasn't been built" + <| async { + let workspaceRoot = + Path.Combine(__SOURCE_DIRECTORY__, "SampleTestProjects", "VSTest.XUnit.RunResults") - let! res = server.TestDiscoverTests() + let! server, _ = initializeServer workspaceRoot + use server = server - Expect.isTrue res.IsError "" - } - testCaseAsync "it should report tests of all basic outcomes" - <| async { - let workspaceRoot = - Path.Combine(__SOURCE_DIRECTORY__, "SampleTestProjects", "VSTest.XUnit.RunResults") + let! res = server.TestDiscoverTests() - let! server, _ = initializeServer workspaceRoot - use server = server + Expect.isError res "" + } + testCaseAsync "it should discover tests in all projects" + <| async { + let workspaceRoot = Path.Combine(__SOURCE_DIRECTORY__, "SampleTestProjects") - let buildResult = DotnetCli.build workspaceRoot - Expect.equal 0 buildResult.ExitCode $"Build failed with: {buildResult.StdErr}" + let! server, _ = initializeServer workspaceRoot + use server = server - let runRequest: TestRunRequest = - { LimitToProjects = None - TestCaseFilter = None - AttachDebugger = false } + Workspace.build workspaceRoot - let! res = server.TestRunTests(runRequest) + let! res = server.TestDiscoverTests() - let actual = - TestRunResult.tryUnwrapTestRunResult res - |> List.map (fun tr -> tr.TestItem.FullName, tr.Outcome) + let expected = + [ ExpectedTests.VSTestXUnitRunResults; ExpectedTests.VSTestXunitTests ] + |> List.concat + |> List.map (fun (testName, _) -> testName) - let expected = ExpectedTests.XUnitRunResults + let actual = + res |> TestDiscoveryResult.tryUnwrapTestDiscoveryResult |> List.map _.FullName - Expect.equal (set actual) (set expected) "" - } + Expect.equal (set actual) (set expected) "" + } ] + testList + "RunTests" + [ testCaseAsync "it should report tests of all basic outcomes" + <| async { + let workspaceRoot = + Path.Combine(__SOURCE_DIRECTORY__, "SampleTestProjects", "VSTest.XUnit.RunResults") - testCaseAsync "it should report a processId when debugging a test project" - <| async { - let workspaceRoot = - Path.Combine(__SOURCE_DIRECTORY__, "SampleTestProjects", "VSTest.XUnit.RunResults") + let! server, _ = initializeServer workspaceRoot + use server = server - let! server, clientNotifications = initializeServer workspaceRoot + let buildResult = DotnetCli.build workspaceRoot + Expect.equal 0 buildResult.ExitCode $"Build failed with: {buildResult.StdErr}" - use server = server - - let buildResult = DotnetCli.build workspaceRoot - Expect.equal 0 buildResult.ExitCode $"Build failed with: {buildResult.StdErr}" - - use tokenSource = new CancellationTokenSource() - let mutable processIdSpy: int option = None - use! _onCancel = Async.OnCancel(fun () -> tokenSource.Cancel()) - - use _ = - clientNotifications.Subscribe(fun (msgType: string, data: obj) -> - if msgType = "test/processWaitingForDebugger" then - let processId: int = - data :?> PlainNotification - |> _.Content - |> FsAutoComplete.JsonSerializer.readJson - - processIdSpy <- Some processId - tokenSource.Cancel()) - - Expect.throwsT - (fun () -> let runRequest: TestRunRequest = { LimitToProjects = None TestCaseFilter = None - AttachDebugger = true } - - Async.RunSynchronously( - server.TestRunTests(runRequest) |> Async.Ignore, - cancellationToken = tokenSource.Token - )) - "" + AttachDebugger = false } - Expect.isSome processIdSpy "" + let! res = server.TestRunTests(runRequest) - let maybeHangingTestProcess = - System.Diagnostics.Process.GetProcesses() - |> Array.tryFind (fun p -> Some p.Id = processIdSpy) + let actual = + TestRunResult.tryUnwrapTestRunResult res + |> List.map (fun tr -> tr.TestItem.FullName, tr.Outcome) - Expect.isNone maybeHangingTestProcess "All test processes should be canceled with the test run" - } + let expected = ExpectedTests.VSTestXUnitRunResults - testCaseAsync - "it should inherit environment variables from it's parent, allowing tests to depend on environment variables" - <| async { - let workspaceRoot = - Path.Combine(__SOURCE_DIRECTORY__, "SampleTestProjects", "VSTest.XUnit.RunResults") + Expect.equal (set actual) (set expected) "" + } - let! server, _ = initializeServer workspaceRoot + testCaseAsync "it should report a processId when debugging a test project" + <| async { + let workspaceRoot = + Path.Combine(__SOURCE_DIRECTORY__, "SampleTestProjects", "VSTest.XUnit.RunResults") - use server = server + let! server, clientNotifications = initializeServer workspaceRoot - let buildResult = DotnetCli.build workspaceRoot - Expect.equal 0 buildResult.ExitCode $"Build failed with: {buildResult.StdErr}" + use server = server - System.Environment.SetEnvironmentVariable("dd586685-08f6-410c-a9f1-84530af117ab", "Set me") + let buildResult = DotnetCli.build workspaceRoot + Expect.equal 0 buildResult.ExitCode $"Build failed with: {buildResult.StdErr}" - let! response = - server.TestRunTests( - { LimitToProjects = None - TestCaseFilter = Some "FullyQualifiedName~Tests.Expects environment variable" - AttachDebugger = false } - ) + use tokenSource = new CancellationTokenSource() + let mutable processIdSpy: int option = None + use! _onCancel = Async.OnCancel(fun () -> tokenSource.Cancel()) - let expected = - [ "Tests.Expects environment variable", FsAutoComplete.TestServer.TestOutcome.Passed ] + use _ = + clientNotifications.Subscribe(fun (msgType: string, data: obj) -> + if msgType = "test/processWaitingForDebugger" then + let processId: int = + data :?> PlainNotification + |> _.Content + |> FsAutoComplete.JsonSerializer.readJson - let actual = - TestRunResult.tryUnwrapTestRunResult response - |> List.map (fun tr -> tr.TestItem.FullName, tr.Outcome) + processIdSpy <- Some processId + tokenSource.Cancel()) - Expect.equal (set actual) (set expected) "" + Expect.throwsT + (fun () -> + let runRequest: TestRunRequest = + { LimitToProjects = None + TestCaseFilter = None + AttachDebugger = true } - System.Environment.SetEnvironmentVariable("dd586685-08f6-410c-a9f1-84530af117ab", "") - } + Async.RunSynchronously( + server.TestRunTests(runRequest) |> Async.Ignore, + cancellationToken = tokenSource.Token + )) + "" - testCaseAsync "it should ignore test project filters that aren't projects in the workspace" - <| async { - let workspaceRoot = - Path.Combine(__SOURCE_DIRECTORY__, "SampleTestProjects", "VSTest.XUnit.RunResults") + Expect.isSome processIdSpy "" - let! server, _ = initializeServer workspaceRoot + let maybeHangingTestProcess = + System.Diagnostics.Process.GetProcesses() + |> Array.tryFind (fun p -> Some p.Id = processIdSpy) - use server = server + Expect.isNone maybeHangingTestProcess "All test processes should be canceled with the test run" + } - let buildResult = DotnetCli.build workspaceRoot - Expect.equal 0 buildResult.ExitCode $"Build failed with: {buildResult.StdErr}" + testCaseAsync + "it should inherit environment variables from it's parent, allowing tests to depend on environment variables" + <| async { + let workspaceRoot = + Path.Combine(__SOURCE_DIRECTORY__, "SampleTestProjects", "VSTest.XUnit.RunResults") - let! response = - server.TestRunTests( - { LimitToProjects = Some [ Path.Combine(__SOURCE_DIRECTORY__, "SampleTestProjects", "Nope", "Nope.fsproj") ] - TestCaseFilter = None - AttachDebugger = false } - ) + let! server, _ = initializeServer workspaceRoot - let expected = [] + use server = server - let actual = - TestRunResult.tryUnwrapTestRunResult response - |> List.map (fun tr -> tr.TestItem.FullName, tr.Outcome) + let buildResult = DotnetCli.build workspaceRoot + Expect.equal 0 buildResult.ExitCode $"Build failed with: {buildResult.StdErr}" - Expect.equal (set actual) (set expected) "" - } + System.Environment.SetEnvironmentVariable("dd586685-08f6-410c-a9f1-84530af117ab", "Set me") - testCaseAsync "it should run only test projects in the project filter when specified" - <| async { - let workspaceRoot = Path.Combine(__SOURCE_DIRECTORY__, "SampleTestProjects") + let! response = + server.TestRunTests( + { LimitToProjects = None + TestCaseFilter = Some "FullyQualifiedName~Tests.Expects environment variable" + AttachDebugger = false } + ) - let! server, _ = initializeServer workspaceRoot + let expected = + [ "Tests.Expects environment variable", FsAutoComplete.TestServer.TestOutcome.Passed ] - use server = server + let actual = + TestRunResult.tryUnwrapTestRunResult response + |> List.map (fun tr -> tr.TestItem.FullName, tr.Outcome) - Workspace.build workspaceRoot + Expect.equal (set actual) (set expected) "" - let! response = - server.TestRunTests( - { LimitToProjects = - Some - [ Path.Combine( - __SOURCE_DIRECTORY__, - "SampleTestProjects", - "VSTest.XUnit.RunResults", - "VSTest.XUnit.RunResults.fsproj" - ) ] - TestCaseFilter = None - AttachDebugger = false } - ) + System.Environment.SetEnvironmentVariable("dd586685-08f6-410c-a9f1-84530af117ab", "") + } - let expected = ExpectedTests.XUnitRunResults + testCaseAsync "it should ignore test project filters that aren't projects in the workspace" + <| async { + let workspaceRoot = + Path.Combine(__SOURCE_DIRECTORY__, "SampleTestProjects", "VSTest.XUnit.RunResults") - let actual = - TestRunResult.tryUnwrapTestRunResult response - |> List.map (fun tr -> tr.TestItem.FullName, tr.Outcome) + let! server, _ = initializeServer workspaceRoot - Expect.equal (set actual) (set expected) "" - } - testCaseAsync "it should only attach the debugger for projects in the project filter if filter is specified" - <| async { - let workspaceRoot = Path.Combine(__SOURCE_DIRECTORY__, "SampleTestProjects") + use server = server + + let buildResult = DotnetCli.build workspaceRoot + Expect.equal 0 buildResult.ExitCode $"Build failed with: {buildResult.StdErr}" - let! server, clientNotifications = initializeServer workspaceRoot + let! response = + server.TestRunTests( + { LimitToProjects = + Some [ Path.Combine(__SOURCE_DIRECTORY__, "SampleTestProjects", "Nope", "Nope.fsproj") ] + TestCaseFilter = None + AttachDebugger = false } + ) + + let expected = [] - use server = server + let actual = + TestRunResult.tryUnwrapTestRunResult response + |> List.map (fun tr -> tr.TestItem.FullName, tr.Outcome) + + Expect.equal (set actual) (set expected) "" + } + + testCaseAsync "it should run only test projects in the project filter when specified" + <| async { + let workspaceRoot = Path.Combine(__SOURCE_DIRECTORY__, "SampleTestProjects") + + let! server, _ = initializeServer workspaceRoot + + use server = server + + Workspace.build workspaceRoot + + let! response = + server.TestRunTests( + { LimitToProjects = + Some + [ Path.Combine( + __SOURCE_DIRECTORY__, + "SampleTestProjects", + "VSTest.XUnit.RunResults", + "VSTest.XUnit.RunResults.fsproj" + ) ] + TestCaseFilter = None + AttachDebugger = false } + ) + + let expected = ExpectedTests.VSTestXUnitRunResults - Workspace.build workspaceRoot - - use tokenSource = new CancellationTokenSource() - let mutable processIdSpy: int list = [] - use! _onCancel = Async.OnCancel(fun () -> tokenSource.Cancel()) - - use _ = - clientNotifications.Subscribe(fun (msgType: string, data: obj) -> - if msgType = "test/processWaitingForDebugger" then - let processId: int = - data :?> PlainNotification - |> _.Content - |> FsAutoComplete.JsonSerializer.readJson - - processIdSpy <- processId :: processIdSpy - tokenSource.Cancel()) - - Expect.throwsT - (fun () -> - let runRequest: TestRunRequest = - { LimitToProjects = - Some - [ Path.Combine( - __SOURCE_DIRECTORY__, - "SampleTestProjects", - "VSTest.XUnit.RunResults", - "VSTest.XUnit.RunResults.fsproj" - ) ] - TestCaseFilter = None - AttachDebugger = true } + let actual = + TestRunResult.tryUnwrapTestRunResult response + |> List.map (fun tr -> tr.TestItem.FullName, tr.Outcome) - Async.RunSynchronously( - server.TestRunTests(runRequest) |> Async.Ignore, - cancellationToken = tokenSource.Token - )) - "" + Expect.equal (set actual) (set expected) "" + } + testCaseAsync "it should only attach the debugger for projects in the project filter if filter is specified" + <| async { + let workspaceRoot = Path.Combine(__SOURCE_DIRECTORY__, "SampleTestProjects") + + let! server, clientNotifications = initializeServer workspaceRoot + + use server = server + + Workspace.build workspaceRoot - Expect.hasLength processIdSpy 1 "Should only launch one process to debug" - } ] + use tokenSource = new CancellationTokenSource() + let mutable processIdSpy: int list = [] + use! _onCancel = Async.OnCancel(fun () -> tokenSource.Cancel()) + + use _ = + clientNotifications.Subscribe(fun (msgType: string, data: obj) -> + if msgType = "test/processWaitingForDebugger" then + let processId: int = + data :?> PlainNotification + |> _.Content + |> FsAutoComplete.JsonSerializer.readJson + + processIdSpy <- processId :: processIdSpy + tokenSource.Cancel()) + + Expect.throwsT + (fun () -> + let runRequest: TestRunRequest = + { LimitToProjects = + Some + [ Path.Combine( + __SOURCE_DIRECTORY__, + "SampleTestProjects", + "VSTest.XUnit.RunResults", + "VSTest.XUnit.RunResults.fsproj" + ) ] + TestCaseFilter = None + AttachDebugger = true } + + Async.RunSynchronously( + server.TestRunTests(runRequest) |> Async.Ignore, + cancellationToken = tokenSource.Token + )) + "" + + Expect.hasLength processIdSpy 1 "Should only launch one process to debug" + } ] ] From 85a4e0fd8db38a83cb2ecbbcda16494a68b4759e Mon Sep 17 00:00:00 2001 From: farlee2121 <2847259+farlee2121@users.noreply.github.com> Date: Sun, 14 Sep 2025 17:55:14 -0500 Subject: [PATCH 33/39] Split the VSTest wrapper code from TestServer contracts for clarity --- .../FsAutoComplete.Core.fsproj | 1 + src/FsAutoComplete.Core/TestServer.fs | 195 ------------------ src/FsAutoComplete.Core/VSTestWrapper.fs | 195 ++++++++++++++++++ 3 files changed, 196 insertions(+), 195 deletions(-) create mode 100644 src/FsAutoComplete.Core/VSTestWrapper.fs diff --git a/src/FsAutoComplete.Core/FsAutoComplete.Core.fsproj b/src/FsAutoComplete.Core/FsAutoComplete.Core.fsproj index d4c262f77..186db15fe 100644 --- a/src/FsAutoComplete.Core/FsAutoComplete.Core.fsproj +++ b/src/FsAutoComplete.Core/FsAutoComplete.Core.fsproj @@ -19,6 +19,7 @@ + diff --git a/src/FsAutoComplete.Core/TestServer.fs b/src/FsAutoComplete.Core/TestServer.fs index 3401f0e0b..961247503 100644 --- a/src/FsAutoComplete.Core/TestServer.fs +++ b/src/FsAutoComplete.Core/TestServer.fs @@ -2,201 +2,6 @@ namespace FsAutoComplete.TestServer open System -module VSTestWrapper = - open Microsoft.TestPlatform.VsTestConsole.TranslationLayer - open Microsoft.VisualStudio.TestPlatform.ObjectModel - open Microsoft.VisualStudio.TestPlatform.ObjectModel.Client - open Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging - open System.Text.RegularExpressions - open Microsoft.VisualStudio.TestPlatform.ObjectModel.Client.Interfaces - - type TestProjectDll = string - - type TestDiscoveryUpdate = - | Progress of TestCase list - | LogMessage of TestMessageLevel * string - - type private TestDiscoveryHandler(notifyDiscoveryProgress: TestDiscoveryUpdate -> unit) = - - member val DiscoveredTests: TestCase ResizeArray = ResizeArray() with get, set - - interface ITestDiscoveryEventsHandler with - member this.HandleDiscoveredTests(discoveredTestCases: System.Collections.Generic.IEnumerable) : unit = - if (not << isNull) discoveredTestCases then - this.DiscoveredTests.AddRange(discoveredTestCases) - notifyDiscoveryProgress (discoveredTestCases |> List.ofSeq |> Progress) - - member this.HandleDiscoveryComplete - (_totalTests: int64, lastChunk: System.Collections.Generic.IEnumerable, _isAborted: bool) - : unit = - if (not << isNull) lastChunk then - this.DiscoveredTests.AddRange(lastChunk) - notifyDiscoveryProgress (lastChunk |> List.ofSeq |> Progress) - - member this.HandleLogMessage(level: TestMessageLevel, message: string) : unit = - notifyDiscoveryProgress (LogMessage(level, message)) - - member this.HandleRawMessage(_rawMessage: string) : unit = () - - let discoverTestsAsync - (vstestPath: string) - (onDiscoveryProgress: TestDiscoveryUpdate -> unit) - (sources: TestProjectDll list) - : Async = - async { - let consoleParams = ConsoleParameters() - let vstest = new VsTestConsoleWrapper(vstestPath, consoleParams) - let discoveryHandler = TestDiscoveryHandler(onDiscoveryProgress) - - use! _onCancel = Async.OnCancel(fun () -> vstest.CancelDiscovery()) - - vstest.DiscoverTests(sources, null, discoveryHandler) - return discoveryHandler.DiscoveredTests |> List.ofSeq - } - - type ProcessId = int - type DidDebuggerAttach = bool - - type TestRunUpdate = - | Progress of TestRunChangedEventArgs - | LogMessage of TestMessageLevel * string - - type TestRunHandler(notifyTestRunProgress: TestRunUpdate -> unit) = - - member val TestResults: TestResult ResizeArray = ResizeArray() with get, set - - interface ITestRunEventsHandler with - member _.HandleLogMessage(level: TestMessageLevel, message: string) : unit = - notifyTestRunProgress (LogMessage(level, message)) - - member _.HandleRawMessage(_rawMessage: string) : unit = () - - member this.HandleTestRunComplete - ( - _testRunCompleteArgs: TestRunCompleteEventArgs, - lastChunkArgs: TestRunChangedEventArgs, - _runContextAttachments: System.Collections.Generic.ICollection, - _executorUris: System.Collections.Generic.ICollection - ) : unit = - if ((not << isNull) lastChunkArgs && (not << isNull) lastChunkArgs.NewTestResults) then - this.TestResults.AddRange(lastChunkArgs.NewTestResults) - notifyTestRunProgress (Progress lastChunkArgs) - - member this.HandleTestRunStatsChange(testRunChangedArgs: TestRunChangedEventArgs) : unit = - if - ((not << isNull) testRunChangedArgs - && (not << isNull) testRunChangedArgs.NewTestResults) - then - this.TestResults.AddRange(testRunChangedArgs.NewTestResults) - notifyTestRunProgress (Progress testRunChangedArgs) - - member _.LaunchProcessWithDebuggerAttached(_testProcessStartInfo: TestProcessStartInfo) : int = - raise (System.NotImplementedException()) - - type TestHostLauncher(isDebug: bool, onAttachDebugger: ProcessId -> DidDebuggerAttach) = - // IMPORTANT: RunTestsWithCustomTestHost says it takes an ITestHostLauncher, but it actually calls a method that is only available on ITestHostLauncher3 - - interface ITestHostLauncher3 with - member _.IsDebug: bool = isDebug - - member _.LaunchTestHost(_defaultTestHostStartInfo: TestProcessStartInfo) : int = raise (NotImplementedException()) - - member _.LaunchTestHost - (_defaultTestHostStartInfo: TestProcessStartInfo, _cancellationToken: Threading.CancellationToken) - : int = - raise (NotImplementedException()) - - member _.AttachDebuggerToProcess - (attachDebuggerInfo: AttachDebuggerInfo, _cancellationToken: Threading.CancellationToken) - : bool = - onAttachDebugger attachDebuggerInfo.ProcessId - - member _.AttachDebuggerToProcess(pid: int) : bool = onAttachDebugger pid - - member _.AttachDebuggerToProcess(pid: int, _cancellationToken: Threading.CancellationToken) : bool = - onAttachDebugger pid - - - module TestPlatformOptions = - let withTestCaseFilter (options: TestPlatformOptions) filterExpression = options.TestCaseFilter <- filterExpression - - module RunSettings = - let defaultRunSettings = - " - - False - -" - - /// onAttachDebugger assumes that the debugger is attached when the method returns. The test project will continue execution as soon as attachDebugger returns - let runTestsAsync - (vstestPath: string) - (onTestRunProgress: TestRunUpdate -> unit) - (onAttachDebugger: ProcessId -> DidDebuggerAttach) - (sources: TestProjectDll list) - (testCaseFilter: string option) - (shouldDebug: bool) - : Async = - async { - let consoleParams = ConsoleParameters() - let vstest = new VsTestConsoleWrapper(vstestPath, consoleParams) - let runHandler = TestRunHandler(onTestRunProgress) - let runSettings = RunSettings.defaultRunSettings - - let options = new TestPlatformOptions() - testCaseFilter |> Option.iter (TestPlatformOptions.withTestCaseFilter options) - - use! _cancel = - Async.OnCancel(fun () -> - printfn "Cancelling test run" - vstest.CancelTestRun() - printfn "Test Run Cancelled") - - if shouldDebug then - let hostLauncher = TestHostLauncher(shouldDebug, onAttachDebugger) - vstest.RunTestsWithCustomTestHost(sources, runSettings, options, runHandler, hostLauncher) - else - vstest.RunTests(sources, runSettings, options, runHandler) - - return runHandler.TestResults |> List.ofSeq - } - - - open System.IO - - let tryFindVsTestFromDotnetRoot (dotnetRoot: string) (workspaceRoot: string option) : Result = - let cwd = - defaultArg workspaceRoot System.Environment.CurrentDirectory |> DirectoryInfo - - let dotnetBinary = - if dotnetRoot |> Directory.Exists then - if - System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform( - System.Runtime.InteropServices.OSPlatform.Windows - ) - then - FileInfo(Path.Combine(dotnetRoot, "dotnet.exe")) - else - FileInfo(Path.Combine(dotnetRoot, "dotnet")) - else - dotnetRoot |> FileInfo - - match Ionide.ProjInfo.SdkDiscovery.versionAt cwd dotnetBinary with - | Ok sdkVersion -> - let sdks = Ionide.ProjInfo.SdkDiscovery.sdks dotnetBinary - - match sdks |> Array.tryFind (fun sdk -> sdk.Version = sdkVersion) with - | Some sdk -> - let vstestBinary = Path.Combine(sdk.Path.FullName, "vstest.console.dll") |> FileInfo - - if vstestBinary.Exists then - Ok vstestBinary - else - Error $"Found the correct dotnet sdk, but vstest was not at the expected sub-path: {vstestBinary.FullName}" - | None -> Error $"Couldn't find the install location for dotnet sdk version: {sdkVersion}" - | Error _ -> Error $"Couldn't identify the dotnet version for working directory: {cwd.FullName}" - - type TestFileRange = { StartLine: int; EndLine: int } type TestItem = diff --git a/src/FsAutoComplete.Core/VSTestWrapper.fs b/src/FsAutoComplete.Core/VSTestWrapper.fs new file mode 100644 index 000000000..b59427c3f --- /dev/null +++ b/src/FsAutoComplete.Core/VSTestWrapper.fs @@ -0,0 +1,195 @@ +namespace FsAutoComplete.TestServer + +open System + +module VSTestWrapper = + open Microsoft.TestPlatform.VsTestConsole.TranslationLayer + open Microsoft.VisualStudio.TestPlatform.ObjectModel + open Microsoft.VisualStudio.TestPlatform.ObjectModel.Client + open Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging + open Microsoft.VisualStudio.TestPlatform.ObjectModel.Client.Interfaces + + type TestProjectDll = string + + type TestDiscoveryUpdate = + | Progress of TestCase list + | LogMessage of TestMessageLevel * string + + type private TestDiscoveryHandler(notifyDiscoveryProgress: TestDiscoveryUpdate -> unit) = + + member val DiscoveredTests: TestCase ResizeArray = ResizeArray() with get, set + + interface ITestDiscoveryEventsHandler with + member this.HandleDiscoveredTests(discoveredTestCases: System.Collections.Generic.IEnumerable) : unit = + if (not << isNull) discoveredTestCases then + this.DiscoveredTests.AddRange(discoveredTestCases) + notifyDiscoveryProgress (discoveredTestCases |> List.ofSeq |> Progress) + + member this.HandleDiscoveryComplete + (_totalTests: int64, lastChunk: System.Collections.Generic.IEnumerable, _isAborted: bool) + : unit = + if (not << isNull) lastChunk then + this.DiscoveredTests.AddRange(lastChunk) + notifyDiscoveryProgress (lastChunk |> List.ofSeq |> Progress) + + member this.HandleLogMessage(level: TestMessageLevel, message: string) : unit = + notifyDiscoveryProgress (LogMessage(level, message)) + + member this.HandleRawMessage(_rawMessage: string) : unit = () + + type ProcessId = int + type DidDebuggerAttach = bool + + type TestRunUpdate = + | Progress of TestRunChangedEventArgs + | LogMessage of TestMessageLevel * string + + type private TestRunHandler(notifyTestRunProgress: TestRunUpdate -> unit) = + + member val TestResults: TestResult ResizeArray = ResizeArray() with get, set + + interface ITestRunEventsHandler with + member _.HandleLogMessage(level: TestMessageLevel, message: string) : unit = + notifyTestRunProgress (LogMessage(level, message)) + + member _.HandleRawMessage(_rawMessage: string) : unit = () + + member this.HandleTestRunComplete + ( + _testRunCompleteArgs: TestRunCompleteEventArgs, + lastChunkArgs: TestRunChangedEventArgs, + _runContextAttachments: System.Collections.Generic.ICollection, + _executorUris: System.Collections.Generic.ICollection + ) : unit = + if ((not << isNull) lastChunkArgs && (not << isNull) lastChunkArgs.NewTestResults) then + this.TestResults.AddRange(lastChunkArgs.NewTestResults) + notifyTestRunProgress (Progress lastChunkArgs) + + member this.HandleTestRunStatsChange(testRunChangedArgs: TestRunChangedEventArgs) : unit = + if + ((not << isNull) testRunChangedArgs + && (not << isNull) testRunChangedArgs.NewTestResults) + then + this.TestResults.AddRange(testRunChangedArgs.NewTestResults) + notifyTestRunProgress (Progress testRunChangedArgs) + + member _.LaunchProcessWithDebuggerAttached(_testProcessStartInfo: TestProcessStartInfo) : int = + raise (System.NotImplementedException()) + + type private TestHostLauncher(isDebug: bool, onAttachDebugger: ProcessId -> DidDebuggerAttach) = + // IMPORTANT: RunTestsWithCustomTestHost says it takes an ITestHostLauncher, but it actually calls a method that is only available on ITestHostLauncher3 + + interface ITestHostLauncher3 with + member _.IsDebug: bool = isDebug + + member _.LaunchTestHost(_defaultTestHostStartInfo: TestProcessStartInfo) : int = raise (NotImplementedException()) + + member _.LaunchTestHost + (_defaultTestHostStartInfo: TestProcessStartInfo, _cancellationToken: Threading.CancellationToken) + : int = + raise (NotImplementedException()) + + member _.AttachDebuggerToProcess + (attachDebuggerInfo: AttachDebuggerInfo, _cancellationToken: Threading.CancellationToken) + : bool = + onAttachDebugger attachDebuggerInfo.ProcessId + + member _.AttachDebuggerToProcess(pid: int) : bool = onAttachDebugger pid + + member _.AttachDebuggerToProcess(pid: int, _cancellationToken: Threading.CancellationToken) : bool = + onAttachDebugger pid + + + module TestPlatformOptions = + let withTestCaseFilter (options: TestPlatformOptions) filterExpression = options.TestCaseFilter <- filterExpression + + module private RunSettings = + let defaultRunSettings = + " + + False + +" + + let discoverTestsAsync + (vstestPath: string) + (onDiscoveryProgress: TestDiscoveryUpdate -> unit) + (sources: TestProjectDll list) + : Async = + async { + let consoleParams = ConsoleParameters() + let vstest = new VsTestConsoleWrapper(vstestPath, consoleParams) + let discoveryHandler = TestDiscoveryHandler(onDiscoveryProgress) + + use! _onCancel = Async.OnCancel(fun () -> vstest.CancelDiscovery()) + + vstest.DiscoverTests(sources, null, discoveryHandler) + return discoveryHandler.DiscoveredTests |> List.ofSeq + } + + /// onAttachDebugger assumes that the debugger is attached when the method returns. The test project will continue execution as soon as attachDebugger returns + let runTestsAsync + (vstestPath: string) + (onTestRunProgress: TestRunUpdate -> unit) + (onAttachDebugger: ProcessId -> DidDebuggerAttach) + (sources: TestProjectDll list) + (testCaseFilter: string option) + (shouldDebug: bool) + : Async = + async { + let consoleParams = ConsoleParameters() + let vstest = new VsTestConsoleWrapper(vstestPath, consoleParams) + let runHandler = TestRunHandler(onTestRunProgress) + let runSettings = RunSettings.defaultRunSettings + + let options = new TestPlatformOptions() + testCaseFilter |> Option.iter (TestPlatformOptions.withTestCaseFilter options) + + use! _cancel = + Async.OnCancel(fun () -> + printfn "Cancelling test run" + vstest.CancelTestRun() + printfn "Test Run Cancelled") + + if shouldDebug then + let hostLauncher = TestHostLauncher(shouldDebug, onAttachDebugger) + vstest.RunTestsWithCustomTestHost(sources, runSettings, options, runHandler, hostLauncher) + else + vstest.RunTests(sources, runSettings, options, runHandler) + + return runHandler.TestResults |> List.ofSeq + } + + open System.IO + + let tryFindVsTestFromDotnetRoot (dotnetRoot: string) (workspaceRoot: string option) : Result = + let cwd = + defaultArg workspaceRoot System.Environment.CurrentDirectory |> DirectoryInfo + + let dotnetBinary = + if dotnetRoot |> Directory.Exists then + if + System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform( + System.Runtime.InteropServices.OSPlatform.Windows + ) + then + FileInfo(Path.Combine(dotnetRoot, "dotnet.exe")) + else + FileInfo(Path.Combine(dotnetRoot, "dotnet")) + else + dotnetRoot |> FileInfo + + match Ionide.ProjInfo.SdkDiscovery.versionAt cwd dotnetBinary with + | Ok sdkVersion -> + let sdks = Ionide.ProjInfo.SdkDiscovery.sdks dotnetBinary + + match sdks |> Array.tryFind (fun sdk -> sdk.Version = sdkVersion) with + | Some sdk -> + let vstestBinary = Path.Combine(sdk.Path.FullName, "vstest.console.dll") |> FileInfo + + if vstestBinary.Exists then + Ok vstestBinary + else + Error $"Found the correct dotnet sdk, but vstest was not at the expected sub-path: {vstestBinary.FullName}" + | None -> Error $"Couldn't find the install location for dotnet sdk version: {sdkVersion}" + | Error _ -> Error $"Couldn't identify the dotnet version for working directory: {cwd.FullName}" From ee7dc5a37c63a2dd89d9c946c2c27e4f7e748981 Mon Sep 17 00:00:00 2001 From: farlee2121 <2847259+farlee2121@users.noreply.github.com> Date: Sun, 14 Sep 2025 18:01:43 -0500 Subject: [PATCH 34/39] Clarify dictionary name (projectLookup -> projectsByBinaryPath) --- src/FsAutoComplete/LspServers/AdaptiveServerState.fs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/FsAutoComplete/LspServers/AdaptiveServerState.fs b/src/FsAutoComplete/LspServers/AdaptiveServerState.fs index a39d31ec2..810f8b229 100644 --- a/src/FsAutoComplete/LspServers/AdaptiveServerState.fs +++ b/src/FsAutoComplete/LspServers/AdaptiveServerState.fs @@ -2664,7 +2664,8 @@ type AdaptiveState let testProjectBinaries = filteredTestProjects |> List.map _.TargetPath - let projectLookup = testProjects |> Seq.map (fun p -> p.TargetPath, p) |> Map.ofSeq + let projectsByBinaryPath = + testProjects |> Seq.map (fun p -> p.TargetPath, p) |> Map.ofSeq let tryTestResultsToDTOs testCases = let tryTestResultToDTO @@ -2677,7 +2678,7 @@ type AdaptiveState TestServer.TestResult.ofVsTestResult project.ProjectFileName project.TargetFramework testResult |> Some - testCases |> List.choose (tryTestResultToDTO projectLookup) + testCases |> List.choose (tryTestResultToDTO projectsByBinaryPath) use tokenSource = new CancellationTokenSource() @@ -2691,7 +2692,7 @@ type AdaptiveState TestResults = progress.NewTestResults |> List.ofSeq |> tryTestResultsToDTOs |> Array.ofSeq ActiveTests = progress.ActiveTests - |> Seq.choose (TestServer.TestItem.tryTestCaseToDTO projectLookup.TryFind) + |> Seq.choose (TestServer.TestItem.tryTestCaseToDTO projectsByBinaryPath.TryFind) |> Array.ofSeq } | TestServer.VSTestWrapper.TestRunUpdate.LogMessage(level, message) -> { TestLogs = From fbc5868ebdf201223ae07d34971c834b4fd1b278 Mon Sep 17 00:00:00 2001 From: farlee2121 <2847259+farlee2121@users.noreply.github.com> Date: Mon, 15 Sep 2025 09:01:51 -0500 Subject: [PATCH 35/39] Satisfy fantomas formatting check --- src/FsAutoComplete/CommandResponse.fsi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/FsAutoComplete/CommandResponse.fsi b/src/FsAutoComplete/CommandResponse.fsi index 7daeb7548..0868e154f 100644 --- a/src/FsAutoComplete/CommandResponse.fsi +++ b/src/FsAutoComplete/CommandResponse.fsi @@ -265,4 +265,4 @@ module CommandResponse = type DiscoverTestsResponse = TestServer.TestItem list val discoverTests: serialize: Serializer -> content: DiscoverTestsResponse -> string - val runTests: serialize: Serializer -> content: TestServer.TestResult list -> string \ No newline at end of file + val runTests: serialize: Serializer -> content: TestServer.TestResult list -> string From 23bf36423c1b500cb1251b77dab0bd0fd20d3bfd Mon Sep 17 00:00:00 2001 From: farlee2121 <2847259+farlee2121@users.noreply.github.com> Date: Thu, 18 Sep 2025 14:46:06 -0500 Subject: [PATCH 36/39] Remove print statements per copilot review --- src/FsAutoComplete.Core/VSTestWrapper.fs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/FsAutoComplete.Core/VSTestWrapper.fs b/src/FsAutoComplete.Core/VSTestWrapper.fs index b59427c3f..20ab6ca96 100644 --- a/src/FsAutoComplete.Core/VSTestWrapper.fs +++ b/src/FsAutoComplete.Core/VSTestWrapper.fs @@ -145,11 +145,7 @@ module VSTestWrapper = let options = new TestPlatformOptions() testCaseFilter |> Option.iter (TestPlatformOptions.withTestCaseFilter options) - use! _cancel = - Async.OnCancel(fun () -> - printfn "Cancelling test run" - vstest.CancelTestRun() - printfn "Test Run Cancelled") + use! _cancel = Async.OnCancel(fun () -> vstest.CancelTestRun()) if shouldDebug then let hostLauncher = TestHostLauncher(shouldDebug, onAttachDebugger) From 1bdb095da8f39bdbba5d02a5f8f68067ec714fc7 Mon Sep 17 00:00:00 2001 From: farlee2121 <2847259+farlee2121@users.noreply.github.com> Date: Tue, 23 Sep 2025 21:08:15 -0500 Subject: [PATCH 37/39] Add OpenTelemetry to the Test Explorer langauge server endpoints --- .../LspServers/AdaptiveFSharpLspServer.fs | 44 +++++++++++++++---- test/FsAutoComplete.Tests.Lsp/Utils/Server.fs | 2 +- 2 files changed, 37 insertions(+), 9 deletions(-) diff --git a/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs b/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs index b07c2f207..4f3d90917 100644 --- a/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs +++ b/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs @@ -3058,20 +3058,48 @@ type AdaptiveFSharpLspServer override this.TestDiscoverTests() : Async> = asyncResult { - let! testDTOs = - state.DiscoverTests() - |> AsyncResult.mapError (fun msg -> JsonRpc.Error.InternalError msg) + use trace = fsacActivitySource.StartActivityForType(thisType) + + try + logger.info (Log.setMessage "TestDiscoverTests Request") + + let! testDTOs = + state.DiscoverTests() + |> AsyncResult.mapError (fun msg -> JsonRpc.Error.InternalError msg) + + return Some { Content = CommandResponse.discoverTests FsAutoComplete.JsonSerializer.writeJson testDTOs } + with e -> + trace |> Tracing.recordException e + + let logCfg = Log.setMessage "TestDiscoverTests Request Errored" - return Some { Content = CommandResponse.discoverTests FsAutoComplete.JsonSerializer.writeJson testDTOs } + return! returnException e logCfg } override this.TestRunTests(p: TestRunRequest) : Async> = asyncResult { - let! testDTOs = - state.RunTests p.LimitToProjects p.TestCaseFilter p.AttachDebugger - |> AsyncResult.mapError (fun msg -> JsonRpc.Error.InternalError msg) + let tags = [ "TestRunRequest", box p ] + use trace = fsacActivitySource.StartActivityForType(thisType, tags = tags) - return Some { Content = CommandResponse.runTests FsAutoComplete.JsonSerializer.writeJson testDTOs } + try + logger.info ( + Log.setMessage "TestRunTests Request: {params}" + >> Log.addContextDestructured "params" p + ) + + let! testDTOs = + state.RunTests p.LimitToProjects p.TestCaseFilter p.AttachDebugger + |> AsyncResult.mapError (fun msg -> JsonRpc.Error.InternalError msg) + + return Some { Content = CommandResponse.runTests FsAutoComplete.JsonSerializer.writeJson testDTOs } + with e -> + trace |> Tracing.recordException e + + let logCfg = + Log.setMessage "TestRunTests Request Errored {p}" + >> Log.addContextDestructured "p" p + + return! returnException e logCfg } override x.Dispose() = disposables.Dispose() diff --git a/test/FsAutoComplete.Tests.Lsp/Utils/Server.fs b/test/FsAutoComplete.Tests.Lsp/Utils/Server.fs index aee896153..13ff41618 100644 --- a/test/FsAutoComplete.Tests.Lsp/Utils/Server.fs +++ b/test/FsAutoComplete.Tests.Lsp/Utils/Server.fs @@ -322,7 +322,7 @@ module Document = do! doc.Server.Server.TextDocumentDidOpen p try - return! doc |> waitForLatestDiagnostics Helpers.defaultTimeout + return! doc |> waitForLatestDiagnostics (TimeSpan.FromSeconds(2)) with :? TimeoutException -> return failwith $"Timeout waiting for latest diagnostics for {doc.Uri}" } From edccad066c781d1418ffcca4bf8cc33b1ab704b7 Mon Sep 17 00:00:00 2001 From: farlee2121 <2847259+farlee2121@users.noreply.github.com> Date: Tue, 23 Sep 2025 21:18:15 -0500 Subject: [PATCH 38/39] Avoid duplicate project load when discovery test projects --- .../LspServers/AdaptiveServerState.fs | 28 +++++++------------ 1 file changed, 10 insertions(+), 18 deletions(-) diff --git a/src/FsAutoComplete/LspServers/AdaptiveServerState.fs b/src/FsAutoComplete/LspServers/AdaptiveServerState.fs index 810f8b229..dc8405535 100644 --- a/src/FsAutoComplete/LspServers/AdaptiveServerState.fs +++ b/src/FsAutoComplete/LspServers/AdaptiveServerState.fs @@ -157,16 +157,6 @@ type FindFirstProject() = $"Couldn't find a corresponding project for {sourceFile}. \n Projects include {allProjects}. \nHave the projects loaded yet or have you tried restoring your project/solution?") module TestProjectHelpers = - let tryGetWorkspaceProjects (workspaceLoader: IWorkspaceLoader) (workspace: WorkspaceChosen) = - match workspace with - | WorkspaceChosen.NotChosen -> Error "No workspace loaded. Can't discover tests" - | WorkspaceChosen.Projs projectPaths -> - projectPaths - |> List.ofSeq - |> List.map string - |> workspaceLoader.LoadProjects - |> Ok - let isTestProject (project: Types.ProjectOptions) = let testProjectIndicators = set [ "Microsoft.TestPlatform.TestHost"; "Microsoft.NET.Test.Sdk" ] @@ -174,12 +164,6 @@ module TestProjectHelpers = project.PackageReferences |> List.exists (fun pr -> Set.contains pr.Name testProjectIndicators) - let tryGetTestProjects (workspaceLoader: IWorkspaceLoader) (workspace: WorkspaceChosen) = - result { - let! projects = tryGetWorkspaceProjects workspaceLoader workspace - return projects |> List.ofSeq |> List.filter isTestProject - } - type AdaptiveState ( lspClient: FSharpLspClient, @@ -2596,7 +2580,11 @@ type AdaptiveState asyncResult { let! vstestBinary = TestServer.VSTestWrapper.tryFindVsTestFromDotnetRoot state.Config.DotNetRoot state.RootPath - let! testProjects = TestProjectHelpers.tryGetTestProjects workspaceLoader state.WorkspacePaths + let! projects = projectOptions |> AsyncAVal.forceAsync + + let testProjects = + projects.ToValueList() |> List.filter TestProjectHelpers.isTestProject + let testProjectBinaries = testProjects |> List.map _.TargetPath if testProjects |> List.isEmpty then @@ -2653,7 +2641,11 @@ type AdaptiveState member state.RunTests (limitToProjects: FilePath list option) (testCaseFilter: string option) (shouldDebug: bool) = asyncResult { let! vstestBinary = TestServer.VSTestWrapper.tryFindVsTestFromDotnetRoot state.Config.DotNetRoot state.RootPath - let! testProjects = TestProjectHelpers.tryGetTestProjects workspaceLoader state.WorkspacePaths + + let! projects = projectOptions |> AsyncAVal.forceAsync + + let testProjects = + projects.ToValueList() |> List.filter TestProjectHelpers.isTestProject let filteredTestProjects = match limitToProjects with From c4c534564651d87bec25abafacc18680ba46ba65 Mon Sep 17 00:00:00 2001 From: Jimmy Byrd Date: Sat, 27 Sep 2025 14:45:37 -0400 Subject: [PATCH 39/39] Fix net9 build --- test/FsAutoComplete.Tests.Lsp/Utils/Server.fs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/FsAutoComplete.Tests.Lsp/Utils/Server.fs b/test/FsAutoComplete.Tests.Lsp/Utils/Server.fs index 13ff41618..3705aed69 100644 --- a/test/FsAutoComplete.Tests.Lsp/Utils/Server.fs +++ b/test/FsAutoComplete.Tests.Lsp/Utils/Server.fs @@ -322,7 +322,7 @@ module Document = do! doc.Server.Server.TextDocumentDidOpen p try - return! doc |> waitForLatestDiagnostics (TimeSpan.FromSeconds(2)) + return! doc |> waitForLatestDiagnostics (TimeSpan.FromSeconds(2.)) with :? TimeoutException -> return failwith $"Timeout waiting for latest diagnostics for {doc.Uri}" }