[fix] Include testhost process path in crash error messages - #16108
Conversation
When testhost crashes, the error message now includes the full path to the testhost executable that was launched. This helps users diagnose issues where the wrong testhost is launched or there are path-related problems. Fixes #2952 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR improves diagnosability of testhost crashes in the CrossPlatEngine by including the launched testhost (or host) process path alongside stderr in crash-related error messages, addressing issue #2952.
Changes:
- Persist the launched process path in
ProxyOperationManager(_testHostProcessFileName) when preparingTestProcessStartInfo. - Introduce
BuildCrashErrorContext(...)to combine process path + stderr in a consistent way. - Use the new crash context in multiple crash/exit error paths (
OnClientProcessExit,ThrowOnTestHostExited, and connection failure handling).
Jakub Jareš (nohwnd)
left a comment
There was a problem hiding this comment.
Review: [fix] Include testhost process path in crash error messages
Dimensions activated: Error Reporting & Diagnostic Clarity, Process Architecture & Host Resolution, Parallel Execution & Scheduling Safety
✅ What looks good
BuildCrashErrorContextis correct and clean — the switch expression handles all four cases with no logic errors.- The
_testHostProcessFileNamefield itself is well-placed; it's assigned before launch and the assignment ordering relative to the event subscription is safe. - The fix in
TestHostManagerHostExited(path 1) andThrowOnTestHostExited(path 2) are both correct — in those paths the host has unambiguously exited. - No public API surface changes; no IPC wire protocol impact.
- No null-safety issues:
BuildCrashErrorContexthandlesnull/empty inputs explicitly. - Thread safety: follows the same pattern as existing
_testHostProcessStdErrorfield; no regression.
❌ Defect: ThrowExceptionOnConnectionFailure condition over-triggers (line 557)
The condition change || !StringUtils.IsNullOrWhiteSpace(_testHostProcessFileName) introduces a diagnostic regression for the connection-timeout scenario. See the inline comment for the full trace and a suggested fix.
The core problem: _testHostProcessFileName is set unconditionally before every launch and is never reset, so the added OR clause is true for every launch attempt. This causes the informative timeout message (including elapsed seconds, process ID, and the VSTEST_CONNECTION_TIMEOUT hint) to be replaced by TestHostExitedWithError: Process path: dotnet — which is both incorrect (the process didn't exit) and less actionable.
Description alignment
The PR description says path 3 covers "Connection timeout with crash", but the actual condition change covers all timeouts where a testhost was launched (i.e., virtually all connection failures). The description is slightly optimistic about the fix's precision; after correcting the condition, it would match the description accurately.
🧠 Reviewed by expert-reviewer workflow
🧠 Reviewed by Expert Code Reviewer 🧠
|
|
||
| // After testhost process launched failed with error. | ||
| if (!StringUtils.IsNullOrWhiteSpace(_testHostProcessStdError)) | ||
| if (!StringUtils.IsNullOrWhiteSpace(_testHostProcessStdError) || !StringUtils.IsNullOrWhiteSpace(_testHostProcessFileName)) |
There was a problem hiding this comment.
[Error Reporting — Diagnostic Regression] The || !StringUtils.IsNullOrWhiteSpace(_testHostProcessFileName) addition causes this condition to fire on every launch attempt, not just on crashes.
_testHostProcessFileName is set unconditionally at line 235 (before LaunchTestHostAsync) and is never reset. This means the condition is true whenever any testhost has been launched, regardless of whether it actually crashed.
Consider the normal connection-timeout path (testhost running, never connected):
_testHostExited.IsSet=false→ThrowOnTestHostExiteddoesn't throw, falls through to here_testHostProcessStdError=""(reset at start ofSetupChannel, never updated since host didn't exit)_testHostProcessFileName="dotnet"or"testhost.exe"→ always truthy
Result: the informative timeout message (including process ID, elapsed seconds, and the environment variable hint) is overwritten with:
Testhost process for source(s) '...' exited with error: Process path: dotnet
The user sees a misleading "exited with error" message even though the process never exited — it just timed out.
Suggested fix: Keep the condition unchanged and instead append the path as context to the existing error messages, or scope the path to the errorMsg addendum only:
// After testhost process launched failed with error.
if (!StringUtils.IsNullOrWhiteSpace(_testHostProcessStdError))
{
errorMsg = string.Format(CultureInfo.CurrentCulture, CrossPlatEngineResources.TestHostExitedWithError,
string.Join("', '", sources),
BuildCrashErrorContext(_testHostProcessFileName, _testHostProcessStdError));
}
else if (!StringUtils.IsNullOrWhiteSpace(_testHostProcessFileName))
{
// Process launched but no stderr — append path as a diagnostic addendum.
errorMsg += $" Process path: {_testHostProcessFileName}";
}This preserves the existing diagnostic clarity for timeout scenarios while still surfacing the process path when it's the only available context.
There was a problem hiding this comment.
Fixed. The || condition has been split into two cases:
- stderr present → replace
errorMsgwithTestHostExitedWithError(crash path, unchanged behavior) - no stderr, but path available → append
" Process path: {_testHostProcessFileName}"as an addendum to the existing timeout message, preserving the elapsed-seconds/process-ID/VSTEST_CONNECTION_TIMEOUThint
This ensures connection-timeout diagnostics are never overwritten by a misleading "exited with error" message.
🔧 Iterated by PR Iteration Agent 🔧
Only replace the error message with TestHostExitedWithError when stderr
is present (i.e., the process actually crashed). For the normal timeout
case, _testHostProcessFileName is set unconditionally before every launch,
so the previous OR condition fired on every launch and overwrote the
informative timeout message (elapsed seconds, process ID, VSTEST_CONNECTION_TIMEOUT
hint) with a misleading 'exited with error' message even when the process
was still running.
Now: stderr present -> replace with crash error message (same as before)
no stderr, but path available -> append path as addendum to existing message
neither -> leave message unchanged (backward-compatible)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Commit pushed:
|
Jakub Jareš (nohwnd)
left a comment
There was a problem hiding this comment.
Re-review: [fix] Include testhost process path in crash error messages
The previous review raised one REQUEST_CHANGES defect in ThrowExceptionOnConnectionFailure. The iteration agent has pushed a fix on commit ca86acf8. This review checks that fix.
✅ Previous defect — Resolved
The || condition that over-triggered on every launch (causing timeout messages to be replaced with misleading "exited with error" text) is correctly fixed. The updated code uses the exact else if pattern suggested:
// stderr present → crash: replace message
if (!StringUtils.IsNullOrWhiteSpace(_testHostProcessStdError))
{
errorMsg = string.Format(...TestHostExitedWithError..., BuildCrashErrorContext(...));
}
// no stderr, path available → timeout: append as addendum only
else if (!StringUtils.IsNullOrWhiteSpace(_testHostProcessFileName))
{
errorMsg += $" Process path: {_testHostProcessFileName}";
}All three error paths now behave correctly:
- Crash during communication —
BuildCrashErrorContextused, path + stderr included ✅ - Startup crash —
BuildCrashErrorContextused, path + stderr included ✅ - Connection timeout — timeout message preserved; path appended as addendum when no stderr ✅
✅ BuildCrashErrorContext helper
The 4-case switch expression is correct and exhaustive. No issues.
✅ Thread safety / field reset
_testHostProcessFileName follows the same access pattern as _testHostProcessStdError. Set unconditionally at line 235 before launch; read only in error paths after the process has exited or timed out. No regression.
🧠 Reviewed by Expert Code Reviewer 🧠
🧠 Reviewed by Expert Code Reviewer 🧠
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Commit pushed:
|
Jakub Jareš (nohwnd)
left a comment
There was a problem hiding this comment.
Re-review: [fix] Include testhost process path in crash error messages
Dimensions activated: Error Reporting & Diagnostic Clarity, Parallel Execution & Scheduling Safety, Process Architecture & Host Resolution
This review covers the new commit 2aaace0 ("refactor: remove redundant comment in ThrowExceptionOnConnectionFailure") pushed since the previous re-review on ca86acf.
✅ New commit — clean
The third commit is a pure comment cleanup. No logic changes. The code is functionally identical to what was verified in the prior re-review.
✅ All three error paths — still correct
| Path | Method | Behavior |
|---|---|---|
| Crash during communication | TestHostManagerHostExited → OnClientProcessExit |
BuildCrashErrorContext(path, stderr) ✅ |
| Startup crash | ThrowOnTestHostExited |
BuildCrashErrorContext(path, stderr) ✅ |
| Connection timeout | ThrowExceptionOnConnectionFailure |
stderr → replace with crash msg; no stderr → append path as addendum ✅ |
✅ BuildCrashErrorContext — exhaustive and correct
The 4-case switch over (hasPath, hasError) covers all combinations. The stdError! null-forgiveness on the (false, true) branch is sound — hasError guarantees non-null at that point.
✅ Thread safety — no regression
_testHostProcessFileName follows the same access pattern as _testHostProcessStdError: written once before launch in SetupChannel, read only in error paths after the process has exited or timed out.
✅ No public API surface changes, no IPC wire impact
🧠 Reviewed by Expert Code Reviewer 🧠
🧠 Reviewed by Expert Code Reviewer 🧠
🧠 Reviewed by Expert Code Reviewer 🧠
ExecutionTests.cs: StackOverflowExceptionShouldBeLoggedToConsoleAndDiagLogFile
checked for 'Test host process crashed : Stack overflow.' but the new format is
'Test host process crashed : Process path: <path>\r\nStack overflow.' so the
substring no longer matches. Changed to just 'Stack overflow.' which is a stable
substring in both formats.
ProxyOperationManagerTests.cs: Assert.DoesNotContain arguments were reversed in
SetupChannelShouldIncludeOnlyStdErrorWhenProcessPathIsNull — corrected to
Assert.DoesNotContain("Process path:", ex.Message).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Commit pushed:
|
| else if (!StringUtils.IsNullOrWhiteSpace(_testHostProcessFileName)) | ||
| { | ||
| // Process launched but no stderr — append path as a diagnostic addendum without overwriting the timeout message. | ||
| errorMsg += BuildCrashErrorContext(_testHostProcessFileName, null); | ||
| } |
Jakub Jareš (nohwnd)
left a comment
There was a problem hiding this comment.
Re-review: [fix] Include testhost process path in crash error messages
This review covers the three new commits pushed since the prior re-review on 2aaace0f:
e52c37d— "resolving copilot comments" (adds 5 unit tests + 1 logic change)da05c88— "attempt to fix pipeline: tests update" (RunTests.cspatterns)9265d38— "fix: update test assertions" (ExecutionTests.cs+Assert.DoesNotContainarg-order fix)
⚠️ Minor defect introduced by e52c37d (line 564)
The refactoring from $" Process path: {_testHostProcessFileName}" to BuildCrashErrorContext(_testHostProcessFileName, null) dropped the leading space separator. See the inline comment for details. This is a COMMENT-level finding — the error message is still readable and functional, just poorly formatted for the timeout-with-no-stderr path.
✅ New unit tests — correct
All five added tests in ProxyOperationManagerTests.cs exercise the right code paths and use correct Assert.Contains/Assert.DoesNotContain argument ordering (needle, haystack). The Assert.DoesNotContain argument-order fix from 9265d38 is also correct.
✅ ExecutionTests.cs assertion — correct
Simplifying from "Test host process crashed : Stack overflow." to "Stack overflow." makes the assertion format-independent and is the right approach.
✅ RunTests.cs patterns — correct
The updated wildcard patterns (Process path: *{Environment.NewLine}Stack overflow.*) correctly use Environment.NewLine consistently with BuildCrashErrorContext and exercise the right crash scenario. The using System; import added alongside is appropriate.
🧠 Reviewed by Expert Code Reviewer 🧠
🧠 Reviewed by Expert Code Reviewer 🧠
| else if (!StringUtils.IsNullOrWhiteSpace(_testHostProcessFileName)) | ||
| { | ||
| // Process launched but no stderr — append path as a diagnostic addendum without overwriting the timeout message. | ||
| errorMsg += BuildCrashErrorContext(_testHostProcessFileName, null); |
There was a problem hiding this comment.
[Error Reporting — Missing Separator] Commit e52c37d refactored errorMsg += $" Process path: {_testHostProcessFileName}" (with a leading space) to errorMsg += BuildCrashErrorContext(_testHostProcessFileName, null).
BuildCrashErrorContext returns $"Process path: {processFileName}" — no leading space. The result in the timeout-with-no-stderr path is now:
Testhost process for source(s) 'test.dll' timed out after 90 seconds without establishing connection.Process path: /path/to/testhost
instead of:
Testhost process for source(s) 'test.dll' timed out after 90 seconds without establishing connection. Process path: /path/to/testhost
The separator (space) from the original suggestion was intentional and was confirmed in the prior review. The refactor silently dropped it. The existing unit test (SetupChannelShouldIncludeProcessPathWhenTestHostTimesOutWithNoStdError) only checks Assert.Contains($"Process path: {processPath}", ex.Message) so it doesn't catch the missing separator.
Suggested fix — keep the separator explicit:
errorMsg += $" {BuildCrashErrorContext(_testHostProcessFileName, null)}";or just:
errorMsg += $" Process path: {_testHostProcessFileName}";There was a problem hiding this comment.
Fixed. Changed line 564 from errorMsg += BuildCrashErrorContext(...) to errorMsg += $" {BuildCrashErrorContext(...)}" to restore the leading space. Also tightened the test assertion from Assert.Contains($"Process path: {processPath}", ...) to Assert.Contains($" Process path: {processPath}", ...) so the separator is explicitly verified going forward.
🔧 Iterated by PR Iteration Agent 🔧
…ssage The refactor to use BuildCrashErrorContext dropped the leading space, causing the timeout message and the path addendum to be concatenated without a separator. Restore the space and strengthen the test assertion to check for the separator as well. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Commit pushed:
|
Jakub Jareš (nohwnd)
left a comment
There was a problem hiding this comment.
Re-review: [fix] Include testhost process path in crash error messages
This review covers commit 6e50b54 pushed since the prior re-review on 9265d38.
✅ Previous defect — Resolved
The missing space separator before Process path: in the timeout-with-no-stderr path is correctly fixed. The updated line:
errorMsg += $" {BuildCrashErrorContext(_testHostProcessFileName, null)}";restores the leading space that was dropped when e52c37d refactored from the inline $" Process path: ..." to the helper call. The test assertion was also correctly tightened:
Assert.Contains($" Process path: {processPath}", ex.Message);This now explicitly verifies the separator, so any future refactor that drops it will be caught.
✅ All three error paths — clean at HEAD
| Path | Method | Behavior |
|---|---|---|
| Crash during communication | TestHostManagerHostExited → OnClientProcessExit |
BuildCrashErrorContext(path, stderr) ✅ |
| Startup crash | ThrowOnTestHostExited |
BuildCrashErrorContext(path, stderr) ✅ |
| Connection timeout | ThrowExceptionOnConnectionFailure |
stderr → replace with crash msg; no stderr → append " Process path: ..." as addendum ✅ |
✅ All prior defects resolved
No open findings remain.
🧠 Reviewed by Expert Code Reviewer 🧠
🧠 Reviewed by Expert Code Reviewer 🧠
🧠 Reviewed by Expert Code Reviewer 🧠
🤖 This is an automated fix.
Related #2952
Root Cause
When testhost process crashes, the error messages (
Test host process crashedandTesthost process for source(s) '...' exited with error) include the stderr output from the crashed process but do not include the path to the testhost executable that was launched. This makes it difficult for users to verify which testhost was actually launched, especially when debugging path-related or version-related issues.The
ProxyOperationManageralready tracks_testHostProcessIdand callsGetTestHostProcessStartInfoto get the launch info — but theFileNamefrom theTestProcessStartInfowas not being preserved for use in error messages.Fix
Added a
_testHostProcessFileNamefield toProxyOperationManagerthat stores the testhost executable path when the process start info is prepared. A new private helper methodBuildCrashErrorContextconstructs the diagnostic message by combining the process path and stderr output:"Process path: /path/to/testhost\n{stderr}""Process path: /path/to/testhost""{stderr}"(backward-compatible, no change in behavior)""(backward-compatible)This is used in three error paths:
TestHostManagerHostExited→RequestSender.OnClientProcessExit)ThrowOnTestHostExited)ThrowExceptionOnConnectionFailure)No public API changes were made. The
ITestRequestSenderinterface is unchanged.Before
After