[fix] Fix flaky PortArgumentProcessor test: cache port locally instead of re-reading singleton#16050
[fix] Fix flaky PortArgumentProcessor test: cache port locally instead of re-reading singleton#16050nohwnd wants to merge 1 commit into
Conversation
PortArgumentExecutor.Execute() was reading _commandLineOptions.Port from the shared CommandLineOptions.Instance singleton. When InProcessVsTestConsoleWrapper runs executor.Execute(args) on a background Task.Run thread, it initializes the port argument (e.g. port=1234), which overwrites CommandLineOptions.Instance.Port. If this races with another test that set Port=2345, the wrong port is passed to ConnectToClientAndProcessRequests. Fix: capture the port number in a local _port field during Initialize() and use that captured value in Execute() instead of re-reading the shared singleton. Fixes #15730 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR fixes a flaky PortArgumentProcessor unit test by removing a race on the shared CommandLineOptions.Instance.Port value when vstest.console is run in-process on background threads.
Changes:
- Capture the port value in
PortArgumentExecutor.Initialize()into a private_portfield. - Use the captured
_portinExecute()(both forConnectToClientAndProcessRequestsand the timeout error message) instead of re-readingCommandLineOptions.Instance.Port. - Preserve the existing assignment to
_commandLineOptions.Portfor compatibility with any other code that reads the singleton.
nohwnd
left a comment
There was a problem hiding this comment.
Review: src/vstest.console/ — Process Architecture & Scheduling Safety
Activated dimensions: Parallel Execution & Scheduling Safety, Process Architecture & Host Resolution
Analysis
The fix is correct and minimal. PortArgumentExecutor.Execute() was reading _commandLineOptions.Port from the shared CommandLineOptions.Instance singleton, which is mutable across test runs in InProcessVsTestConsoleWrapper. Caching the value in the instance field _port during Initialize() eliminates the TOCTOU race.
Checked items:
- ✅ Race condition eliminated —
_portis an instance field, so eachPortArgumentExecutorinstance holds its own captured value. The write inInitialize()happens-before the read inExecute()within the same logical call chain. - ✅ Default value is safe — If
Execute()is called withoutInitialize(),_portis 0, which matches prior behavior (the singleton would also have been 0 or unset). No regression. - ✅ Singleton assignment preserved —
_commandLineOptions.Port = portNumberis still set, so callers reading the singleton continue to work correctly. - ✅ Both uses of the port updated — Both
ConnectToClientAndProcessRequestsand theTimeoutExceptionmessage now use_port. No dangling read of the singleton inExecute(). - ✅ PR description matches the diff — The root cause, fix, and affected test are accurately described.
No issues found.
🧠 Reviewed by Expert Code Reviewer
🧠 Reviewed by Expert Code Reviewer 🧠
🤖 This is an automated fix generated by the Issue Repro Triage agent.
Fixes #15730
Root Cause
PortArgumentExecutor.Execute()reads_commandLineOptions.Portfrom the sharedCommandLineOptions.Instancesingleton.InProcessVsTestConsoleWrapperstarts vstest.console in-process by callingexecutor.Execute(args)on a backgroundTask.Runthread. This eventually callsPortArgumentExecutor.Initialize("1234"), which setsCommandLineOptions.Instance.Port = 1234.If this background thread races with another test that called
Initialize("2345")and is about to callExecute(), the background thread overwritesCommandLineOptions.Instance.Portfrom 2345 to 1234 beforeExecute()reads it — causing theConnectToClientAndProcessRequestsmock to be invoked with the wrong port.Fix
Capture the port number in a private
_portfield duringInitialize()and use that captured value inExecute()instead of re-reading the shared singleton. The assignment to_commandLineOptions.Portis preserved for callers that read it fromCommandLineOptions.Instance.Test
The 11 existing
PortArgumentProcessorTestsall pass. The flaky testExecutorExecuteForValidConnectionReturnsArgumentProcessorResultSuccessis directly covered by the fix — it now uses the locally-captured port rather than the shared mutable singleton.