|
| 1 | +namespace FsAutoComplete.TestServer |
| 2 | + |
| 3 | +open System |
| 4 | + |
| 5 | +module VSTestWrapper = |
| 6 | + open Microsoft.TestPlatform.VsTestConsole.TranslationLayer |
| 7 | + open Microsoft.VisualStudio.TestPlatform.ObjectModel |
| 8 | + open Microsoft.VisualStudio.TestPlatform.ObjectModel.Client |
| 9 | + open Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging |
| 10 | + open Microsoft.VisualStudio.TestPlatform.ObjectModel.Client.Interfaces |
| 11 | + |
| 12 | + type TestProjectDll = string |
| 13 | + |
| 14 | + type TestDiscoveryUpdate = |
| 15 | + | Progress of TestCase list |
| 16 | + | LogMessage of TestMessageLevel * string |
| 17 | + |
| 18 | + type private TestDiscoveryHandler(notifyDiscoveryProgress: TestDiscoveryUpdate -> unit) = |
| 19 | + |
| 20 | + member val DiscoveredTests: TestCase ResizeArray = ResizeArray() with get, set |
| 21 | + |
| 22 | + interface ITestDiscoveryEventsHandler with |
| 23 | + member this.HandleDiscoveredTests(discoveredTestCases: System.Collections.Generic.IEnumerable<TestCase>) : unit = |
| 24 | + if (not << isNull) discoveredTestCases then |
| 25 | + this.DiscoveredTests.AddRange(discoveredTestCases) |
| 26 | + notifyDiscoveryProgress (discoveredTestCases |> List.ofSeq |> Progress) |
| 27 | + |
| 28 | + member this.HandleDiscoveryComplete |
| 29 | + (_totalTests: int64, lastChunk: System.Collections.Generic.IEnumerable<TestCase>, _isAborted: bool) |
| 30 | + : unit = |
| 31 | + if (not << isNull) lastChunk then |
| 32 | + this.DiscoveredTests.AddRange(lastChunk) |
| 33 | + notifyDiscoveryProgress (lastChunk |> List.ofSeq |> Progress) |
| 34 | + |
| 35 | + member this.HandleLogMessage(level: TestMessageLevel, message: string) : unit = |
| 36 | + notifyDiscoveryProgress (LogMessage(level, message)) |
| 37 | + |
| 38 | + member this.HandleRawMessage(_rawMessage: string) : unit = () |
| 39 | + |
| 40 | + type ProcessId = int |
| 41 | + type DidDebuggerAttach = bool |
| 42 | + |
| 43 | + type TestRunUpdate = |
| 44 | + | Progress of TestRunChangedEventArgs |
| 45 | + | LogMessage of TestMessageLevel * string |
| 46 | + |
| 47 | + type private TestRunHandler(notifyTestRunProgress: TestRunUpdate -> unit) = |
| 48 | + |
| 49 | + member val TestResults: TestResult ResizeArray = ResizeArray() with get, set |
| 50 | + |
| 51 | + interface ITestRunEventsHandler with |
| 52 | + member _.HandleLogMessage(level: TestMessageLevel, message: string) : unit = |
| 53 | + notifyTestRunProgress (LogMessage(level, message)) |
| 54 | + |
| 55 | + member _.HandleRawMessage(_rawMessage: string) : unit = () |
| 56 | + |
| 57 | + member this.HandleTestRunComplete |
| 58 | + ( |
| 59 | + _testRunCompleteArgs: TestRunCompleteEventArgs, |
| 60 | + lastChunkArgs: TestRunChangedEventArgs, |
| 61 | + _runContextAttachments: System.Collections.Generic.ICollection<AttachmentSet>, |
| 62 | + _executorUris: System.Collections.Generic.ICollection<string> |
| 63 | + ) : unit = |
| 64 | + if ((not << isNull) lastChunkArgs && (not << isNull) lastChunkArgs.NewTestResults) then |
| 65 | + this.TestResults.AddRange(lastChunkArgs.NewTestResults) |
| 66 | + notifyTestRunProgress (Progress lastChunkArgs) |
| 67 | + |
| 68 | + member this.HandleTestRunStatsChange(testRunChangedArgs: TestRunChangedEventArgs) : unit = |
| 69 | + if |
| 70 | + ((not << isNull) testRunChangedArgs |
| 71 | + && (not << isNull) testRunChangedArgs.NewTestResults) |
| 72 | + then |
| 73 | + this.TestResults.AddRange(testRunChangedArgs.NewTestResults) |
| 74 | + notifyTestRunProgress (Progress testRunChangedArgs) |
| 75 | + |
| 76 | + member _.LaunchProcessWithDebuggerAttached(_testProcessStartInfo: TestProcessStartInfo) : int = |
| 77 | + raise (System.NotImplementedException()) |
| 78 | + |
| 79 | + type private TestHostLauncher(isDebug: bool, onAttachDebugger: ProcessId -> DidDebuggerAttach) = |
| 80 | + // IMPORTANT: RunTestsWithCustomTestHost says it takes an ITestHostLauncher, but it actually calls a method that is only available on ITestHostLauncher3 |
| 81 | + |
| 82 | + interface ITestHostLauncher3 with |
| 83 | + member _.IsDebug: bool = isDebug |
| 84 | + |
| 85 | + member _.LaunchTestHost(_defaultTestHostStartInfo: TestProcessStartInfo) : int = raise (NotImplementedException()) |
| 86 | + |
| 87 | + member _.LaunchTestHost |
| 88 | + (_defaultTestHostStartInfo: TestProcessStartInfo, _cancellationToken: Threading.CancellationToken) |
| 89 | + : int = |
| 90 | + raise (NotImplementedException()) |
| 91 | + |
| 92 | + member _.AttachDebuggerToProcess |
| 93 | + (attachDebuggerInfo: AttachDebuggerInfo, _cancellationToken: Threading.CancellationToken) |
| 94 | + : bool = |
| 95 | + onAttachDebugger attachDebuggerInfo.ProcessId |
| 96 | + |
| 97 | + member _.AttachDebuggerToProcess(pid: int) : bool = onAttachDebugger pid |
| 98 | + |
| 99 | + member _.AttachDebuggerToProcess(pid: int, _cancellationToken: Threading.CancellationToken) : bool = |
| 100 | + onAttachDebugger pid |
| 101 | + |
| 102 | + |
| 103 | + module TestPlatformOptions = |
| 104 | + let withTestCaseFilter (options: TestPlatformOptions) filterExpression = options.TestCaseFilter <- filterExpression |
| 105 | + |
| 106 | + module private RunSettings = |
| 107 | + let defaultRunSettings = |
| 108 | + "<RunSettings> |
| 109 | + <RunConfiguration> |
| 110 | + <DesignMode>False</DesignMode> |
| 111 | + </RunConfiguration> |
| 112 | +</RunSettings>" |
| 113 | + |
| 114 | + let discoverTestsAsync |
| 115 | + (vstestPath: string) |
| 116 | + (onDiscoveryProgress: TestDiscoveryUpdate -> unit) |
| 117 | + (sources: TestProjectDll list) |
| 118 | + : Async<TestCase list> = |
| 119 | + async { |
| 120 | + let consoleParams = ConsoleParameters() |
| 121 | + let vstest = new VsTestConsoleWrapper(vstestPath, consoleParams) |
| 122 | + let discoveryHandler = TestDiscoveryHandler(onDiscoveryProgress) |
| 123 | + |
| 124 | + use! _onCancel = Async.OnCancel(fun () -> vstest.CancelDiscovery()) |
| 125 | + |
| 126 | + vstest.DiscoverTests(sources, null, discoveryHandler) |
| 127 | + return discoveryHandler.DiscoveredTests |> List.ofSeq |
| 128 | + } |
| 129 | + |
| 130 | + /// onAttachDebugger assumes that the debugger is attached when the method returns. The test project will continue execution as soon as attachDebugger returns |
| 131 | + let runTestsAsync |
| 132 | + (vstestPath: string) |
| 133 | + (onTestRunProgress: TestRunUpdate -> unit) |
| 134 | + (onAttachDebugger: ProcessId -> DidDebuggerAttach) |
| 135 | + (sources: TestProjectDll list) |
| 136 | + (testCaseFilter: string option) |
| 137 | + (shouldDebug: bool) |
| 138 | + : Async<TestResult list> = |
| 139 | + async { |
| 140 | + let consoleParams = ConsoleParameters() |
| 141 | + let vstest = new VsTestConsoleWrapper(vstestPath, consoleParams) |
| 142 | + let runHandler = TestRunHandler(onTestRunProgress) |
| 143 | + let runSettings = RunSettings.defaultRunSettings |
| 144 | + |
| 145 | + let options = new TestPlatformOptions() |
| 146 | + testCaseFilter |> Option.iter (TestPlatformOptions.withTestCaseFilter options) |
| 147 | + |
| 148 | + use! _cancel = Async.OnCancel(fun () -> vstest.CancelTestRun()) |
| 149 | + |
| 150 | + if shouldDebug then |
| 151 | + let hostLauncher = TestHostLauncher(shouldDebug, onAttachDebugger) |
| 152 | + vstest.RunTestsWithCustomTestHost(sources, runSettings, options, runHandler, hostLauncher) |
| 153 | + else |
| 154 | + vstest.RunTests(sources, runSettings, options, runHandler) |
| 155 | + |
| 156 | + return runHandler.TestResults |> List.ofSeq |
| 157 | + } |
| 158 | + |
| 159 | + open System.IO |
| 160 | + |
| 161 | + let tryFindVsTestFromDotnetRoot (dotnetRoot: string) (workspaceRoot: string option) : Result<FileInfo, string> = |
| 162 | + let cwd = |
| 163 | + defaultArg workspaceRoot System.Environment.CurrentDirectory |> DirectoryInfo |
| 164 | + |
| 165 | + let dotnetBinary = |
| 166 | + if dotnetRoot |> Directory.Exists then |
| 167 | + if |
| 168 | + System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform( |
| 169 | + System.Runtime.InteropServices.OSPlatform.Windows |
| 170 | + ) |
| 171 | + then |
| 172 | + FileInfo(Path.Combine(dotnetRoot, "dotnet.exe")) |
| 173 | + else |
| 174 | + FileInfo(Path.Combine(dotnetRoot, "dotnet")) |
| 175 | + else |
| 176 | + dotnetRoot |> FileInfo |
| 177 | + |
| 178 | + match Ionide.ProjInfo.SdkDiscovery.versionAt cwd dotnetBinary with |
| 179 | + | Ok sdkVersion -> |
| 180 | + let sdks = Ionide.ProjInfo.SdkDiscovery.sdks dotnetBinary |
| 181 | + |
| 182 | + match sdks |> Array.tryFind (fun sdk -> sdk.Version = sdkVersion) with |
| 183 | + | Some sdk -> |
| 184 | + let vstestBinary = Path.Combine(sdk.Path.FullName, "vstest.console.dll") |> FileInfo |
| 185 | + |
| 186 | + if vstestBinary.Exists then |
| 187 | + Ok vstestBinary |
| 188 | + else |
| 189 | + Error $"Found the correct dotnet sdk, but vstest was not at the expected sub-path: {vstestBinary.FullName}" |
| 190 | + | None -> Error $"Couldn't find the install location for dotnet sdk version: {sdkVersion}" |
| 191 | + | Error _ -> Error $"Couldn't identify the dotnet version for working directory: {cwd.FullName}" |
0 commit comments