diff --git a/FsAutoComplete.sln b/FsAutoComplete.sln
index 880144ac3..8d8bd12ea 100644
--- a/FsAutoComplete.sln
+++ b/FsAutoComplete.sln
@@ -1,84 +1,155 @@
-
-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
-Global
- GlobalSection(SolutionConfigurationPlatforms) = preSolution
- Debug|Any CPU = Debug|Any CPU
- Release|Any CPU = Release|Any CPU
- 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}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {B6AB4EF3-8F60-41A1-AB0C-851A6DEB169E}.Release|Any CPU.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}.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
- {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}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {6FE2E08A-9F3F-4C7C-B190-77C46AE147D7}.Release|Any CPU.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}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {38C1F619-3E1E-4784-9833-E8A2AA95CDAE}.Release|Any CPU.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}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {14C55B44-2063-4891-98BE-8184CAB1BE87}.Release|Any CPU.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}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {C58701B0-D8E3-4B68-A7DE-8524C95F86C0}.Release|Any CPU.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}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {0CD029D8-B39E-4CBE-A190-C84A7A811180}.Release|Any CPU.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}
- 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
+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
diff --git a/paket.dependencies b/paket.dependencies
index 23f193530..10077961a 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
@@ -58,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 7aba40c53..5fd5f6626 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)
@@ -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/FsAutoComplete.Core.fsproj b/src/FsAutoComplete.Core/FsAutoComplete.Core.fsproj
index 04812cf9f..186db15fe 100644
--- a/src/FsAutoComplete.Core/FsAutoComplete.Core.fsproj
+++ b/src/FsAutoComplete.Core/FsAutoComplete.Core.fsproj
@@ -3,7 +3,8 @@
net8.0
net8.0;net9.0
false
- $(NoWarn);FS0057
+ $(NoWarn);FS0057
+
@@ -18,6 +19,8 @@
+
+
@@ -64,4 +67,4 @@
-
+
\ No newline at end of file
diff --git a/src/FsAutoComplete.Core/TestServer.fs b/src/FsAutoComplete.Core/TestServer.fs
new file mode 100644
index 000000000..961247503
--- /dev/null
+++ b/src/FsAutoComplete.Core/TestServer.fs
@@ -0,0 +1,88 @@
+namespace FsAutoComplete.TestServer
+
+open System
+
+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
+ }
+
+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
+
+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 }
diff --git a/src/FsAutoComplete.Core/VSTestWrapper.fs b/src/FsAutoComplete.Core/VSTestWrapper.fs
new file mode 100644
index 000000000..20ab6ca96
--- /dev/null
+++ b/src/FsAutoComplete.Core/VSTestWrapper.fs
@@ -0,0 +1,191 @@
+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 () -> vstest.CancelTestRun())
+
+ 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}"
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/src/FsAutoComplete/CommandResponse.fs b/src/FsAutoComplete/CommandResponse.fs
index bfc37efe7..b71d04ae7 100644
--- a/src/FsAutoComplete/CommandResponse.fs
+++ b/src/FsAutoComplete/CommandResponse.fs
@@ -702,3 +702,17 @@ module CommandResponse =
PrecedingNonPipeExprLine = pnp })
serialize { Kind = "pipelineHint"; Data = ctn }
+
+
+
+ type DiscoverTestsResponse = TestServer.TestItem list
+
+ let discoverTests (serialize: Serializer) (content: DiscoverTestsResponse) =
+ serialize
+ { Kind = "discoverTests"
+ Data = content }
+
+ let runTests (serialize: Serializer) (content: TestServer.TestResult list) =
+ serialize
+ { Kind = "runTests"
+ Data = content |> Array.ofList }
diff --git a/src/FsAutoComplete/CommandResponse.fsi b/src/FsAutoComplete/CommandResponse.fsi
index b68c16035..0868e154f 100644
--- a/src/FsAutoComplete/CommandResponse.fsi
+++ b/src/FsAutoComplete/CommandResponse.fsi
@@ -261,3 +261,8 @@ 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 DiscoverTestsResponse = TestServer.TestItem list
+
+ val discoverTests: serialize: Serializer -> content: DiscoverTestsResponse -> string
+ val runTests: serialize: Serializer -> content: TestServer.TestResult list -> string
diff --git a/src/FsAutoComplete/LspHelpers.fs b/src/FsAutoComplete/LspHelpers.fs
index 5baee17b4..9d7ffa24b 100644
--- a/src/FsAutoComplete/LspHelpers.fs
+++ b/src/FsAutoComplete/LspHelpers.fs
@@ -612,6 +612,22 @@ type TestDetectedNotification =
{ File: string
Tests: TestAdapter.TestAdapterEntry array }
+type TestRunRequest =
+ { LimitToProjects: FilePath list option
+ TestCaseFilter: string option
+ AttachDebugger: bool }
+
+type TestLogMessage = { Level: string; Message: string }
+
+type TestDiscoveryUpdateNotification =
+ { Tests: TestServer.TestItem array
+ TestLogs: TestLogMessage array }
+
+type TestRunProgress =
+ { TestLogs: TestLogMessage array
+ 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 0ba878d42..25b93cefe 100644
--- a/src/FsAutoComplete/LspHelpers.fsi
+++ b/src/FsAutoComplete/LspHelpers.fsi
@@ -198,6 +198,22 @@ type TestDetectedNotification =
{ File: string
Tests: TestAdapter.TestAdapterEntry array }
+type TestRunRequest =
+ { LimitToProjects: FilePath list option
+ TestCaseFilter: string option
+ AttachDebugger: bool }
+
+type TestLogMessage = { Level: string; Message: string }
+
+type TestDiscoveryUpdateNotification =
+ { Tests: TestServer.TestItem array
+ TestLogs: TestLogMessage array }
+
+type TestRunProgress =
+ { TestLogs: TestLogMessage array
+ TestResults: TestServer.TestResult array
+ ActiveTests: TestServer.TestItem array }
+
type ProjectParms =
{
/// Project file to compile
@@ -465,7 +481,6 @@ type FSharpInlayHintsRequest =
{ TextDocument: TextDocumentIdentifier
Range: Range }
-
[]
module Extensions =
diff --git a/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs b/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs
index 272dc44f4..4f3d90917 100644
--- a/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs
+++ b/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs
@@ -3056,6 +3056,52 @@ type AdaptiveFSharpLspServer
return! returnException e logCfg
}
+ override this.TestDiscoverTests() : Async> =
+ asyncResult {
+ 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! returnException e logCfg
+ }
+
+ override this.TestRunTests(p: TestRunRequest) : Async> =
+ asyncResult {
+ let tags = [ "TestRunRequest", box p ]
+ use trace = fsacActivitySource.StartActivityForType(thisType, tags = tags)
+
+ 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()
member this.WindowWorkDoneProgressCancel(param: WorkDoneProgressCancelParams) : Async =
@@ -3096,6 +3142,7 @@ type AdaptiveFSharpLspServer
member this.Progress(_arg1: ProgressParams) : Async = ignoreNotification
member this.SetTrace(_arg1: SetTraceParams) : Async = ignoreNotification
+
module AdaptiveFSharpLspServer =
open StreamJsonRpc
@@ -3185,6 +3232,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)))
let adaptiveServer lspClient =
let loader = workspaceLoaderFactory toolsPath
diff --git a/src/FsAutoComplete/LspServers/AdaptiveServerState.fs b/src/FsAutoComplete/LspServers/AdaptiveServerState.fs
index 0976b38f3..dc8405535 100644
--- a/src/FsAutoComplete/LspServers/AdaptiveServerState.fs
+++ b/src/FsAutoComplete/LspServers/AdaptiveServerState.fs
@@ -156,6 +156,13 @@ 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 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)
type AdaptiveState
(
@@ -801,6 +808,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}"
@@ -2567,6 +2575,153 @@ type AdaptiveState
member x.GlyphToSymbolKind = glyphToSymbolKind |> AVal.force
+ member state.DiscoverTests() =
+
+ asyncResult {
+ let! vstestBinary = TestServer.VSTestWrapper.tryFindVsTestFromDotnetRoot state.Config.DotNetRoot state.RootPath
+
+ let! projects = projectOptions |> AsyncAVal.forceAsync
+
+ let testProjects =
+ projects.ToValueList() |> List.filter TestProjectHelpers.isTestProject
+
+ 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
+
+ testCases
+ |> List.choose (TestServer.TestItem.tryTestCaseToDTO projectLookup.TryFind)
+
+ 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
+
+ let testDTOs: TestServer.TestItem list = testCases |> tryTestCasesToDTOs
+
+ return testDTOs
+ }
+
+ member state.RunTests (limitToProjects: FilePath list option) (testCaseFilter: string option) (shouldDebug: bool) =
+ asyncResult {
+ let! vstestBinary = TestServer.VSTestWrapper.tryFindVsTestFromDotnetRoot state.Config.DotNetRoot state.RootPath
+
+ let! projects = projectOptions |> AsyncAVal.forceAsync
+
+ let testProjects =
+ projects.ToValueList() |> List.filter TestProjectHelpers.isTestProject
+
+ 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 projectsByBinaryPath =
+ 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
+
+ testCases |> List.choose (tryTestResultToDTO projectsByBinaryPath)
+
+ use tokenSource = new CancellationTokenSource()
+
+ use! _onCancel = Async.OnCancel(fun _ -> tokenSource.Cancel())
+
+ let onTestRunProgress (runUpdate: TestServer.VSTestWrapper.TestRunUpdate) =
+ let dto =
+ match runUpdate with
+ | TestServer.VSTestWrapper.TestRunUpdate.Progress progress ->
+ { TestLogs = [||]
+ TestResults = progress.NewTestResults |> List.ofSeq |> tryTestResultsToDTOs |> Array.ofSeq
+ ActiveTests =
+ progress.ActiveTests
+ |> Seq.choose (TestServer.TestItem.tryTestCaseToDTO projectsByBinaryPath.TryFind)
+ |> Array.ofSeq }
+ | TestServer.VSTestWrapper.TestRunUpdate.LogMessage(level, message) ->
+ { TestLogs =
+ [| { Message = message
+ Level = string level } |]
+ TestResults = [||]
+ ActiveTests = [||] }
+
+ Async.RunSynchronously(async { do! lspClient.NotifyTestRunUpdate(dto) }, cancellationToken = tokenSource.Token)
+
+ let onAttachDebugger (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}"
+ )
+
+ false
+
+ let! testResults =
+ TestServer.VSTestWrapper.runTestsAsync
+ vstestBinary.FullName
+ onTestRunProgress
+ onAttachDebugger
+ testProjectBinaries
+ testCaseFilter
+ shouldDebug
+
+ 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 9ee4f4a73..edff7ef19 100644
--- a/src/FsAutoComplete/LspServers/AdaptiveServerState.fsi
+++ b/src/FsAutoComplete/LspServers/AdaptiveServerState.fsi
@@ -119,6 +119,8 @@ 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 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/src/FsAutoComplete/LspServers/FSharpLspClient.fs b/src/FsAutoComplete/LspServers/FSharpLspClient.fs
index 532298ed8..8b5cabd41 100644
--- a/src/FsAutoComplete/LspServers/FSharpLspClient.fs
+++ b/src/FsAutoComplete/LspServers/FSharpLspClient.fs
@@ -62,6 +62,17 @@ 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 __.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 })
+
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..0576df043 100644
--- a/src/FsAutoComplete/LspServers/FSharpLspClient.fsi
+++ b/src/FsAutoComplete/LspServers/FSharpLspClient.fsi
@@ -32,6 +32,9 @@ type FSharpLspClient =
member NotifyFileParsed: p: PlainNotification -> Async
member NotifyDocumentAnalyzed: p: DocumentAnalyzedNotification -> Async
member NotifyTestDetected: p: TestDetectedNotification -> Async
+ member NotifyTestDiscoveryUpdate: p: TestDiscoveryUpdateNotification -> Async
+ member NotifyTestRunUpdate: p: TestRunProgress -> Async
+ member AttachDebuggerForTestRun: processId: int -> AsyncLspResult
member CodeLensRefresh: unit -> Async
override WindowWorkDoneProgressCreate: WorkDoneProgressCreateParams -> AsyncLspResult
member Progress: ProgressToken * 'Progress -> Async
diff --git a/src/FsAutoComplete/LspServers/IFSharpLspServer.fs b/src/FsAutoComplete/LspServers/IFSharpLspServer.fs
index 90309b7ed..00d61e196 100644
--- a/src/FsAutoComplete/LspServers/IFSharpLspServer.fs
+++ b/src/FsAutoComplete/LspServers/IFSharpLspServer.fs
@@ -47,3 +47,5 @@ type IFSharpLspServer =
abstract FsProjAddFile: DotnetFileRequest -> Async>
abstract FsProjRemoveFile: DotnetFileRequest -> Async>
abstract FsProjAddExistingFile: DotnetFileRequest -> Async>
+ abstract TestDiscoverTests: unit -> Async>
+ abstract TestRunTests: TestRunRequest -> Async>
diff --git a/test/FsAutoComplete.Tests.Lsp/DotnetCli.fs b/test/FsAutoComplete.Tests.Lsp/DotnetCli.fs
new file mode 100644
index 000000000..df5f4bc16
--- /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 option) (processName: string) (processArgs: string) =
+ let psi = new Diagnostics.ProcessStartInfo(processName, processArgs)
+ psi.UseShellExecute <- false
+ psi.RedirectStandardOutput <- true
+ psi.RedirectStandardError <- true
+ psi.CreateNoWindow <- true
+ wd |> Option.iter (fun wd -> 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 None "dotnet" $"build {path}"
diff --git a/test/FsAutoComplete.Tests.Lsp/FsAutoComplete.Tests.Lsp.fsproj b/test/FsAutoComplete.Tests.Lsp/FsAutoComplete.Tests.Lsp.fsproj
index 14c0002c5..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/Program.fs b/test/FsAutoComplete.Tests.Lsp/Program.fs
index 6736ea79a..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
@@ -137,22 +137,20 @@ let lspTests =
EmptyFileTests.tests createServer
CallHierarchy.tests createServer
diagnosticsTest 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
@@ -165,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}"
@@ -281,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/SampleTestProjects/VSTest.XUnit.RunResults/Program.fs b/test/FsAutoComplete.Tests.Lsp/SampleTestProjects/VSTest.XUnit.RunResults/Program.fs
new file mode 100644
index 000000000..31dc4f735
--- /dev/null
+++ b/test/FsAutoComplete.Tests.Lsp/SampleTestProjects/VSTest.XUnit.RunResults/Program.fs
@@ -0,0 +1,4 @@
+module Program
+
+[]
+let main _ = 0
diff --git a/test/FsAutoComplete.Tests.Lsp/SampleTestProjects/VSTest.XUnit.RunResults/Tests.fs b/test/FsAutoComplete.Tests.Lsp/SampleTestProjects/VSTest.XUnit.RunResults/Tests.fs
new file mode 100644
index 000000000..28e514dbe
--- /dev/null
+++ b/test/FsAutoComplete.Tests.Lsp/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.Lsp/SampleTestProjects/VSTest.XUnit.RunResults/VSTest.XUnit.RunResults.fsproj b/test/FsAutoComplete.Tests.Lsp/SampleTestProjects/VSTest.XUnit.RunResults/VSTest.XUnit.RunResults.fsproj
new file mode 100644
index 000000000..1a4224efd
--- /dev/null
+++ b/test/FsAutoComplete.Tests.Lsp/SampleTestProjects/VSTest.XUnit.RunResults/VSTest.XUnit.RunResults.fsproj
@@ -0,0 +1,22 @@
+
+
+
+ net8.0
+ false
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/FsAutoComplete.Tests.Lsp/SampleTestProjects/VSTest.XUnit.Tests/Program.fs b/test/FsAutoComplete.Tests.Lsp/SampleTestProjects/VSTest.XUnit.Tests/Program.fs
new file mode 100644
index 000000000..31dc4f735
--- /dev/null
+++ b/test/FsAutoComplete.Tests.Lsp/SampleTestProjects/VSTest.XUnit.Tests/Program.fs
@@ -0,0 +1,4 @@
+module Program
+
+[]
+let main _ = 0
diff --git a/test/FsAutoComplete.Tests.Lsp/SampleTestProjects/VSTest.XUnit.Tests/Tests.fs b/test/FsAutoComplete.Tests.Lsp/SampleTestProjects/VSTest.XUnit.Tests/Tests.fs
new file mode 100644
index 000000000..7f70fae19
--- /dev/null
+++ b/test/FsAutoComplete.Tests.Lsp/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.Lsp/SampleTestProjects/VSTest.XUnit.Tests/VSTest.XUnit.Tests.fsproj b/test/FsAutoComplete.Tests.Lsp/SampleTestProjects/VSTest.XUnit.Tests/VSTest.XUnit.Tests.fsproj
new file mode 100644
index 000000000..1a4224efd
--- /dev/null
+++ b/test/FsAutoComplete.Tests.Lsp/SampleTestProjects/VSTest.XUnit.Tests/VSTest.XUnit.Tests.fsproj
@@ -0,0 +1,22 @@
+
+
+
+ net8.0
+ false
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/FsAutoComplete.Tests.Lsp/SampleTestProjects/paket.dependencies b/test/FsAutoComplete.Tests.Lsp/SampleTestProjects/paket.dependencies
new file mode 100644
index 000000000..e69de29bb
diff --git a/test/FsAutoComplete.Tests.Lsp/SampleTestProjects/paket.lock b/test/FsAutoComplete.Tests.Lsp/SampleTestProjects/paket.lock
new file mode 100644
index 000000000..d3f5a12fa
--- /dev/null
+++ b/test/FsAutoComplete.Tests.Lsp/SampleTestProjects/paket.lock
@@ -0,0 +1 @@
+
diff --git a/test/FsAutoComplete.Tests.Lsp/TestExplorerTests.fs b/test/FsAutoComplete.Tests.Lsp/TestExplorerTests.fs
new file mode 100644
index 000000000..0a518001e
--- /dev/null
+++ b/test/FsAutoComplete.Tests.Lsp/TestExplorerTests.fs
@@ -0,0 +1,339 @@
+module FsAutoComplete.Tests.TestExplorer
+
+open Expecto
+open Helpers
+open System.IO
+open FsAutoComplete.LspHelpers
+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}"
+
+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 VSTestXUnitRunResults =
+ [ "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 VSTestXunitTests =
+ [ "Tests.My test", FsAutoComplete.TestServer.TestOutcome.Passed ]
+
+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 {
+ let! (server, event) = serverInitialize workspaceRoot defaultConfigDto createServer
+ do! waitForWorkspaceFinishedParsing event
+
+ return (server, event)
+ }
+ |> Async.Cache
+
+ testSequenced
+ <| testList
+ "TestExplorerTests"
+ [ 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! server, _ = initializeServer workspaceRoot
+ use server = server
+
+ let! res = server.TestDiscoverTests()
+
+ Expect.isError res ""
+ }
+ testCaseAsync "it should discover tests in all projects"
+ <| async {
+ let workspaceRoot = Path.Combine(__SOURCE_DIRECTORY__, "SampleTestProjects")
+
+ let! server, _ = initializeServer workspaceRoot
+ use server = server
+
+ Workspace.build workspaceRoot
+
+ let! res = server.TestDiscoverTests()
+
+ let expected =
+ [ ExpectedTests.VSTestXUnitRunResults; ExpectedTests.VSTestXunitTests ]
+ |> List.concat
+ |> List.map (fun (testName, _) -> testName)
+
+ let actual =
+ res |> TestDiscoveryResult.tryUnwrapTestDiscoveryResult |> List.map _.FullName
+
+ 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")
+
+ 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 =
+ { LimitToProjects = None
+ TestCaseFilter = None
+ AttachDebugger = false }
+
+ let! res = server.TestRunTests(runRequest)
+
+ let actual =
+ TestRunResult.tryUnwrapTestRunResult res
+ |> List.map (fun tr -> tr.TestItem.FullName, tr.Outcome)
+
+ let expected = ExpectedTests.VSTestXUnitRunResults
+
+ 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")
+
+ let! server, clientNotifications = initializeServer workspaceRoot
+
+ 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
+ ))
+ ""
+
+ Expect.isSome processIdSpy ""
+
+ 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"
+ }
+
+ 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(
+ { LimitToProjects = None
+ 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", "")
+ }
+
+ 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.VSTestXUnitRunResults
+
+ 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"
+ } ] ]
diff --git a/test/FsAutoComplete.Tests.Lsp/Utils/Server.fs b/test/FsAutoComplete.Tests.Lsp/Utils/Server.fs
index aee896153..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 Helpers.defaultTimeout
+ return! doc |> waitForLatestDiagnostics (TimeSpan.FromSeconds(2.))
with :? TimeoutException ->
return failwith $"Timeout waiting for latest diagnostics for {doc.Uri}"
}
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..7e80883d0
--- /dev/null
+++ b/test/FsAutoComplete.Tests.TestExplorer/FsAutoComplete.Tests.TestExplorer.fsproj
@@ -0,0 +1,24 @@
+
+
+ Exe
+ net8.0
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+ dotnet build $(MSBuildProjectDirectory)/SampleTestProjects/VSTest.XUnit.Tests/
+ dotnet build $(MSBuildProjectDirectory)/SampleTestProjects/VSTest.XUnit.RunResults/
+ dotnet build $(MSBuildProjectDirectory)/SampleTestProjects/VSTest.NUnit/
+
+
+
+
\ No newline at end of file
diff --git a/test/FsAutoComplete.Tests.TestExplorer/Main.fs b/test/FsAutoComplete.Tests.TestExplorer/Main.fs
new file mode 100644
index 000000000..43147fdfd
--- /dev/null
+++ b/test/FsAutoComplete.Tests.TestExplorer/Main.fs
@@ -0,0 +1,7 @@
+namespace FsAutoComplete.Tests.TestExplorer
+
+open Expecto
+
+module Program =
+ []
+ let main argv = Tests.runTestsInAssemblyWithCLIArgs [] argv
diff --git a/test/FsAutoComplete.Tests.TestExplorer/ResourceLocators.fs b/test/FsAutoComplete.Tests.TestExplorer/ResourceLocators.fs
new file mode 100644
index 000000000..8d0cbbcbb
--- /dev/null
+++ b/test/FsAutoComplete.Tests.TestExplorer/ResourceLocators.fs
@@ -0,0 +1,18 @@
+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/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/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..07e4b673d
--- /dev/null
+++ 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/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/TestDiscoveryTests.fs b/test/FsAutoComplete.Tests.TestExplorer/TestDiscoveryTests.fs
new file mode 100644
index 000000000..f5cfbc326
--- /dev/null
+++ b/test/FsAutoComplete.Tests.TestExplorer/TestDiscoveryTests.fs
@@ -0,0 +1,33 @@
+module TestDiscoveryTests
+
+open Expecto
+open FsAutoComplete.TestServer
+open Microsoft.VisualStudio.TestPlatform.ObjectModel;
+open System.IO
+
+
+let vstestPath = ResourceLocators.tryFindVsTest ()
+
+[]
+let tests =
+ testList "VSTestWrapper Test Discovery" [
+ testCaseAsync "should return an empty list if given no projects" <| async {
+ let expected = []
+ let! actual = VSTestWrapper.discoverTestsAsync vstestPath ignore []
+ Expect.equal actual expected ""
+ }
+
+ 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.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
new file mode 100644
index 000000000..0b4f69113
--- /dev/null
+++ b/test/FsAutoComplete.Tests.TestExplorer/TestRunTests.fs
@@ -0,0 +1,159 @@
+module TestRunTests
+
+open Expecto
+open FsAutoComplete.TestServer
+open Microsoft.VisualStudio.TestPlatform.ObjectModel
+open System.IO
+
+let vstestPath = ResourceLocators.tryFindVsTest ()
+
+let nullAttachDebugger _ = false
+
+[]
+let tests =
+ testList
+ "VSTestWrapper Test Run"
+ [ 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 "it 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.Expects environment variable", TestOutcome.Failed ]
+
+ let sources =
+ [ Path.Combine(
+ ResourceLocators.sampleProjectsRootDir,
+ "VSTest.XUnit.RunResults/bin/Debug/net8.0/VSTest.XUnit.RunResults.dll"
+ ) ]
+
+ 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
+
+ Expect.equal (set actual) (set expected) ""
+ }
+
+ testCaseAsync "it 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
+ nullAttachDebugger
+ 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 "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! runResults =
+ VSTestWrapper.runTestsAsync
+ vstestPath
+ ignore
+ 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)
+
+ let mutable actualProcessId: int option = None
+
+ let updateSpy (processId: int) =
+ actualProcessId <- Some processId
+ tokenSource.Cancel()
+ false
+
+ use! _c = Async.OnCancel(fun _ -> tokenSource.Cancel())
+
+ Expect.throwsT
+ (fun () ->
+ let sources =
+ [ Path.Combine(
+ ResourceLocators.sampleProjectsRootDir,
+ "VSTest.XUnit.RunResults/bin/Debug/net8.0/VSTest.XUnit.RunResults.dll"
+ ) ]
+
+ Async.RunSynchronously(
+ VSTestWrapper.runTestsAsync vstestPath ignore updateSpy sources None true,
+ cancellationToken = tokenSource.Token
+ )
+ |> ignore)
+ ""
+
+ Expect.isSome actualProcessId "Expected runTest to report a processId"
+ }
+
+ testCaseAsync "it should report a processId only once per process"
+ <| async {
+ use tokenSource = new System.Threading.CancellationTokenSource(1000)
+
+ let mutable reportedProcessIds: int list = []
+
+ let updateSpy (processId: int) =
+ reportedProcessIds <- processId :: reportedProcessIds
+ tokenSource.Cancel()
+ false
+
+ use! _c = Async.OnCancel(fun _ -> tokenSource.Cancel())
+
+ Expect.throwsT
+ (fun () ->
+ let sources =
+ [ Path.Combine(
+ ResourceLocators.sampleProjectsRootDir,
+ "VSTest.XUnit.RunResults/bin/Debug/net8.0/VSTest.XUnit.RunResults.dll"
+ ) ]
+
+ Async.RunSynchronously(
+ VSTestWrapper.runTestsAsync vstestPath ignore updateSpy sources None true,
+ cancellationToken = tokenSource.Token
+ )
+ |> ignore)
+ ""
+
+ Expect.hasLength reportedProcessIds 1 "Expected runTest to report a processId"
+ } ]
diff --git a/test/FsAutoComplete.Tests.TestExplorer/paket.references b/test/FsAutoComplete.Tests.TestExplorer/paket.references
new file mode 100644
index 000000000..dfc4a00cb
--- /dev/null
+++ b/test/FsAutoComplete.Tests.TestExplorer/paket.references
@@ -0,0 +1,4 @@
+FSharp.Core content: once
+Microsoft.NET.Test.Sdk
+YoloDev.Expecto.TestSdk
+Microsoft.Bcl.AsyncInterfaces
\ No newline at end of file