@@ -406,6 +406,8 @@ module DotnetCli =
406406 open Ionide.VSCode .Helpers .Process
407407 open Node.ChildProcess
408408
409+ let private cancelErrorMessage = " SIGINT"
410+
409411 /// <summary>
410412 /// Fire off a command and gather the error, if any, and the stdout and stderr streams.
411413 /// The command is fired from the workspace's root path.
@@ -416,13 +418,15 @@ module DotnetCli =
416418 let execWithCancel
417419 command
418420 args
421+ ( env : obj option )
422+ ( outputCallback : Node.Buffer.Buffer -> unit )
419423 ( cancellationToken : CancellationToken )
420424 : JS.Promise < ExecError option * string * string > =
421- let cancelErrorMessage = " SIGINT"
422425
423426 if not cancellationToken.isCancellationRequested then
424427 let options = createEmpty< ExecOptions>
425428 options.cwd <- workspace.rootPath
429+ env |> Option.iter ( fun env -> options?env <- env)
426430
427431 Promise.create ( fun resolve reject ->
428432 let stdout = ResizeArray()
@@ -431,7 +435,9 @@ module DotnetCli =
431435
432436 let childProcess =
433437 crossSpawn.spawn ( command, args, options = options)
434- |> onOutput ( fun e -> stdout.Add( string e))
438+ |> onOutput ( fun e ->
439+ outputCallback e
440+ stdout.Add( string e))
435441 |> onError ( fun e -> error <- Some e)
436442 |> onErrorOutput ( fun e -> stderr.Add( string e))
437443 |> onClose ( fun code signal ->
@@ -446,16 +452,60 @@ module DotnetCli =
446452 else
447453 promise { return ( None, " " , " " ) }
448454
455+
449456 let restore
450457 ( projectPath : string )
451458 : JS.Promise < Node.ChildProcess.ExecError option * StandardOutput * StandardError > =
452459 Process.exec " dotnet" ( ResizeArray([| " restore" ; projectPath |]))
453460
461+ let private debugProcessIdRegex = RegularExpressions.Regex( @" Process Id: (.*)," )
462+
463+ let private tryGetDebugProcessId consoleOutput =
464+ let m = debugProcessIdRegex.Match( consoleOutput)
465+
466+ if m.Success then
467+ let processId = m.Groups.[ 1 ]. Value
468+ Some processId
469+ else
470+ None
471+
472+ let private launchDebugger processId =
473+ let launchRequest : DebugConfiguration =
474+ {| name = " .NET Core Attach"
475+ `` type `` = " coreclr"
476+ request = " attach"
477+ processId = processId |}
478+ |> box
479+ |> unbox
480+
481+ let folder = workspace.workspaceFolders.Value.[ 0 ]
482+
483+ promise {
484+ let! _ =
485+ Vscode.debug.startDebugging ( Some folder, U2.Case2 launchRequest)
486+ |> Promise.ofThenable
487+
488+ // NOTE: Have to wait or it'll continue before the debugger reaches the stop on entry point.
489+ // That'll leave the debugger in a confusing state where it shows it's attached but
490+ // no breakpoints are hit and the breakpoints show as disabled
491+ do ! Promise.sleep 2000
492+ Vscode.commands.executeCommand ( " workbench.action.debug.continue" ) |> ignore
493+ }
494+ |> ignore
495+
496+ type DebugTests =
497+ | Debug
498+ | NoDebug
499+
500+ module DebugTests =
501+ let ofBool bool = if bool then Debug else NoDebug
502+
454503 let private dotnetTest
455504 ( cancellationToken : CancellationToken )
456505 ( projectPath : string )
457506 ( targetFramework : string )
458507 ( trxOutputPath : string option )
508+ ( shouldDebug : DebugTests )
459509 ( additionalArgs : string array )
460510 : JS.Promise < Node.ChildProcess.ExecError option * StandardOutput * StandardError > =
461511
@@ -471,7 +521,24 @@ module DotnetCli =
471521 let argString = String.Join( " " , args)
472522 logger.Debug( $" Running `dotnet {argString}`" )
473523
474- Process.execWithCancel " dotnet" ( ResizeArray( args)) cancellationToken
524+ match shouldDebug with
525+ | Debug ->
526+ let mutable isDebuggerStarted = false
527+
528+ let tryLaunchDebugger ( consoleOutput : Node.Buffer.Buffer ) =
529+ if not isDebuggerStarted then
530+ // NOTE: the processId we need to attach to is not the one we started for `dotnet test`.
531+ // Dotnet test will return the correct process id if (and only if) we are in debug mode
532+ match tryGetDebugProcessId ( string consoleOutput) with
533+ | None -> ()
534+ | Some processId ->
535+ launchDebugger processId
536+ isDebuggerStarted <- true
537+
538+ let env = {| VSTEST_ HOST_ DEBUG = 1 |} |> box |> Some
539+ Process.execWithCancel " dotnet" ( ResizeArray( args)) env tryLaunchDebugger cancellationToken
540+ | NoDebug -> Process.execWithCancel " dotnet" ( ResizeArray( args)) None ignore cancellationToken
541+
475542
476543 type TrxPath = string
477544 type ConsoleOutput = string
@@ -481,6 +548,7 @@ module DotnetCli =
481548 ( targetFramework : string )
482549 ( trxOutputPath : string option )
483550 ( filterExpression : string option )
551+ ( shouldDebug : DebugTests )
484552 ( cancellationToken : CancellationToken )
485553 : JS.Promise < ConsoleOutput > =
486554 promise {
@@ -495,7 +563,13 @@ module DotnetCli =
495563 logger.Debug( " Filter" , filter)
496564
497565 let! _ , stdOutput , stdError =
498- dotnetTest cancellationToken projectPath targetFramework trxOutputPath [| " --no-build" ; yield ! filter |]
566+ dotnetTest
567+ cancellationToken
568+ projectPath
569+ targetFramework
570+ trxOutputPath
571+ shouldDebug
572+ [| " --no-build" ; yield ! filter |]
499573
500574 logger.Debug( " Test run exitCode" , stdError)
501575
@@ -515,6 +589,7 @@ module DotnetCli =
515589 projectPath
516590 targetFramework
517591 None
592+ NoDebug
518593 [| " --list-tests" ; yield ! additionalArgs |]
519594
520595 let testNames =
@@ -1007,6 +1082,7 @@ module Interactions =
10071082 ProjectPath: ProjectPath
10081083 /// examples: net6.0, net7.0, netcoreapp2.0, etc
10091084 TargetFramework: TargetFramework
1085+ ShouldDebug: bool
10101086 Tests: TestItem array
10111087 /// The Tests are listed due to a include filter, so when running the tests the --filter should be added
10121088 HasIncludeFilter: bool
@@ -1211,6 +1287,7 @@ module Interactions =
12111287 projectRunRequest.TargetFramework
12121288 ( Some trxPath)
12131289 filterExpression
1290+ ( projectRunRequest.ShouldDebug |> DotnetCli.DebugTests.ofBool)
12141291 cancellationToken
12151292
12161293 TestRun.appendOutputLine testRun output
@@ -1264,9 +1341,14 @@ module Interactions =
12641341 [| testItem |])
12651342 |> Array.distinctBy TestItem.getId
12661343
1344+ let shouldDebug =
1345+ runRequest.profile
1346+ |> Option.map ( fun p -> p.kind = TestRunProfileKind.Debug)
1347+ |> Option.defaultValue false
12671348
12681349 { ProjectPath = projectPath
12691350 TargetFramework = project.Info.TargetFramework
1351+ ShouldDebug = shouldDebug
12701352 HasIncludeFilter = Option.isSome runRequest.`` include ``
12711353 Tests = replaceProjectRootIfPresent tests })
12721354
@@ -1441,7 +1523,14 @@ module Interactions =
14411523 let projectPath = project.Project
14421524 report $" Discovering tests for {projectPath}"
14431525 let trxPath = makeTrxPath projectPath |> Some
1444- DotnetCli.test projectPath project.Info.TargetFramework trxPath None cancellationToken)
1526+
1527+ DotnetCli.test
1528+ projectPath
1529+ project.Info.TargetFramework
1530+ trxPath
1531+ None
1532+ DotnetCli.DebugTests.NoDebug
1533+ cancellationToken)
14451534
14461535 let trxDiscoveredTests =
14471536 TestDiscovery.discoverFromTrx testItemFactory tryGetLocation makeTrxPath trxDiscoveryProjects
@@ -1584,21 +1673,18 @@ let activate (context: ExtensionContext) =
15841673
15851674 let tryGetLocation = Interactions.tryGetLocation locationCache
15861675
1587- testController.createRunProfile (
1588- " Run F# Tests" ,
1589- TestRunProfileKind.Run,
1590- Interactions.runHandler testController tryGetLocation makeTrxPath,
1591- true
1592- )
1676+ let runHandler = Interactions.runHandler testController tryGetLocation makeTrxPath
1677+
1678+ testController.createRunProfile ( " Run F# Tests" , TestRunProfileKind.Run, runHandler, true )
15931679 |> unbox
15941680 |> context.subscriptions.Add
15951681
1596- // testController.createRunProfile ("Debug F# Tests", TestRunProfileKind.Debug, runHandler testController, true )
1597- // |> unbox
1598- // |> context.subscriptions.Add
1682+ testController.createRunProfile ( " Debug F# Tests" , TestRunProfileKind.Debug, runHandler, false )
1683+ |> unbox
1684+ |> context.subscriptions.Add
15991685
16001686 let testsPerFileCache = Collections.Generic.Dictionary< string, TestItem array>()
1601- // Multiple result items might point to the same code location, but there will never be mroe than one code-located test per result-based test item
1687+ // Multiple result items might point to the same code location, but there will never be more than one code-located test per result-based test item
16021688 let displacedFragmentMapCache =
16031689 Collections.Generic.Dictionary< ResultBasedTestId, CodeBasedTestId>()
16041690
0 commit comments