Skip to content

Commit 7ceb6d8

Browse files
authored
Test debugging (#1927)
* Test debugging works but is messy * Clean up test debugging Remove duplicated code and improve separation of concerns * Change test debug boolean to union flags for clarity * Add comments clarifying test debug gotchas
1 parent 2be2d7b commit 7ceb6d8

File tree

1 file changed

+101
-15
lines changed

1 file changed

+101
-15
lines changed

src/Components/TestExplorer.fs

Lines changed: 101 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)