Follow-up to #8386: address review comments on TestContext.Properties flow#8396
Follow-up to #8386: address review comments on TestContext.Properties flow#8396Evangelink wants to merge 2 commits into
Conversation
Source changes: - TestAssemblyInfo.PostAssemblyInitProperties and TestClassInfo.PostClassInitProperties now use Volatile.Read/Write so callers on the cached-result fast paths (which intentionally bypass the per-class / per-assembly semaphores) safely observe the snapshot published by the thread that ran the init method. Addresses the safe-publication concern raised by the Copilot reviewer. - TestContextImplementation.CaptureLifecycleProperties enumerates _properties under a lock so two snapshot calls cannot trip over each other. Doc-comment is explicit that writes via TestContext.Properties bypass this lock and are out of scope (consistent with the long-standing thread-affinity expectation of AssemblyInitialize / ClassInitialize). - TestContextImplementation.MergeProperties XML doc now spells out merge precedence: lifecycle snapshots WIN over keys already present in the bag (e.g. runsettings TestRunParameters) on collision. Test additions: - MergePropertiesShouldOverrideSeededSourceLevelParameters: asserts lifecycle-snapshot values override the seeded source-level parameters. - TestContextPropertyFlowForceCleanupTests acceptance suite: triggers ClassCleanupManager.ForceCleanup via --maximum-failed-tests=1, asserts AssemblyInit / ClassInit properties flow into the ClassCleanup and AssemblyCleanup contexts invoked through the fallback path, and re-confirms that ClassInit-set properties never leak into AssemblyCleanup. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Evangelink
left a comment
There was a problem hiding this comment.
Review Summary — PR #8396
| Dimension | Result |
|---|---|
| 1. Algorithmic Correctness | ✅ |
| 2. Threading & Concurrency | ❌ See inline |
| 3. Error Handling | ✅ |
| 4. Public API Surface | ✅ (all changes are internal) |
5. No init on New Public API |
✅ N/A |
| 6. Localization | ✅ N/A (no user-facing strings) |
| 7. Performance | ✅ |
| 8. Cross-TFM Correctness | ✅ Volatile is correct on all TFMs; field keyword requires LangVersion>preview which is set |
| 9. IPC Contract | ✅ N/A |
| 10. Security | ✅ N/A |
| 11. Test Coverage | ✅ New unit test + acceptance test added |
| 12. Test Quality | ✅ TestFramework.ForTestingMSTest auto-discovers parameterless public methods; missing [TestMethod] is correct |
| 13. Documentation / Comments | ✅ Excellent XML docs — one inconsistency flagged in inline comment |
| 14. Naming Conventions | ✅ |
| 15. StyleCop / EditorConfig | ✅ |
| 16. Backward Compatibility | ✅ All internal |
| 17. Scope Discipline | ✅ Tight follow-up scope |
| 18. Code Simplicity | ✅ |
| 19. Resource / Disposal | ✅ N/A |
| 20. Async Correctness | ✅ N/A |
| 21. Build / Packaging | ✅ N/A |
One blocking issue
MergeProperties is not under the same lock as CaptureLifecycleProperties, yet the doc comment on CaptureLifecycleProperties explicitly claims safety against concurrent MergeProperties calls (line 339). This is either a documentation error or a missing lock. Since MergeProperties is internal and framework-controlled, adding lock (_properties) { ... } around its loop body is the correct fix and directly matches the documented contract.
Everything else looks good
Volatile.Read/Volatile.Writeon the property-backing fields are the correct primitives for the publish/subscribe pattern used here (one writer completes init under a semaphore, many readers later observe the result on the fast path). The acquire/release semantics are sufficient.CaptureLifecyclePropertieslocking on_propertiesis safe becauseDictionaryis a valid lock target as long as all code that iterates or structurally modifies it takes the same lock.- The
fieldkeyword (C# 13 semi-auto property) compiles because the project sets<LangVersion>preview</LangVersion>. - The acceptance test using
--maximum-failed-tests 1is a reasonable approach; the sentinel-string check viaAssert.ContainsonStandardOutputis consistent with the rest of the acceptance suite. - The new unit test without
[TestMethod]is intentional and correct forTestFramework.ForTestingMSTest(confirmed: "All public parameterless methods will be recognized as test methods").
Generated by Expert Code Review (on open) for issue #8396 · ● 5M
| /// </para> | ||
| /// <para> | ||
| /// Enumeration is performed under a lock on <c>_properties</c> so that snapshot capture | ||
| /// is safe against concurrent calls to this method or <see cref="MergeProperties"/> on |
There was a problem hiding this comment.
The XML doc states that CaptureLifecycleProperties is safe against concurrent calls to MergeProperties, but MergeProperties (line 306–323) does not acquire the lock on _properties. As written, a MergeProperties call racing with CaptureLifecycleProperties can corrupt the Dictionary iterator or cause a missed-write in the snapshot.
Either:
- Add
lock (_properties) { ... }around the loop body inMergeProperties, or - Weaken the doc comment to remove the claim of safety against concurrent
MergePropertiescalls.
If option 1 is chosen, note that the public Properties indexer still bypasses the lock — that is already documented as "user error" and is fine, but MergeProperties is framework-internal and the framework itself controls when it is called, so locking there is low-cost and closes the gap described in the doc.
There was a problem hiding this comment.
Already addressed in commit caed75f — MergeProperties now takes lock (_properties) { ... } around its loop body (see lines 313-329), matching the documented contract on CaptureLifecycleProperties. The doc-comment is intentionally left as-is. Resolving.
….WriteLine MSTest's ConsoleOutRouter captures Console.Out into the per-TestContext buffer during cleanup execution (via SetCurrentTestContext). On the ForceCleanup fallback path the captured output is never written to the process stdout, so the acceptance test's Assert.Contains on StandardOutput always fails. Replace the Console.WriteLine sentinel markers with file-based markers: the cleanup methods write a file to a temp directory (passed via an environment variable) only when their property-flow assertions pass. The test reads the marker files to verify cleanup ran successfully. Also add lock(_properties) around MergeProperties enumeration to match the lock in CaptureLifecycleProperties, preventing a race between concurrent merge and snapshot operations on the same context. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Follow-up to #8386 (which merged moments before the review feedback could be applied).
What this PR does
Source changes
TestAssemblyInfo.PostAssemblyInitPropertiesandTestClassInfo.PostClassInitPropertiesnow useVolatile.Read/Volatile.Writeso consumers on the cached-result fast paths (which intentionally bypass the per-class / per-assembly semaphores) safely observe the snapshot published by the thread that ran the init method.TestContextImplementation.CaptureLifecyclePropertiesnow enumerates_propertiesunder a lock so two snapshot calls cannot trip over each other. The doc-comment is explicit that writes via the publicTestContext.Propertiesindexer bypass this lock and are out of scope, which is consistent with the long-standing thread-affinity expectation ofAssemblyInitialize/ClassInitialize.TestContextImplementation.MergePropertiesXML doc now spells out that lifecycle snapshots WIN over keys already present in the bag (e.g. runsettingsTestRunParameters) on collision.Tests
MergePropertiesShouldOverrideSeededSourceLevelParameters— unit test asserting thatMergePropertiesoverwrites a key seeded at construction time (the runsettings precedence case).TestContextPropertyFlowForceCleanupTestsacceptance suite (addresses another of @Evangelink's findings: no test exercisedClassCleanupManager.ForceCleanup) — triggersForceCleanupvia--maximum-failed-tests=1, asserts thatAssemblyInit/ClassInitproperties flow into theClassCleanupandAssemblyCleanupcontexts invoked through the fallback path, and re-confirms thatClassInit-set properties never leak intoAssemblyCleanup(mirroring the normal-path scoping rule).Notes on the 17
[TestMethod]comments from @copilot-pull-request-reviewerThose are false positives —
MSTestAdapter.PlatformServices.UnitTestsuses the internalTestContainerbase fromTestFramework.ForTestingMSTest. Any public parameterless method is treated as a test method, no[TestMethod]attribute is required (the original 800+ tests in those files follow the same pattern). No change needed.Verification
dotnet buildofMSTestAdapter.PlatformServices.UnitTests(net9.0): 0 warnings, 0 errors.MSTestAdapter.PlatformServices.UnitTests(net9.0): 805 / 805 pass (was 804 — added the newMergePropertiesShouldOverrideSeededSourceLevelParameters).dotnet buildofMSTest.Acceptance.IntegrationTests: 0 warnings, 0 errors.Refs #8386. Fixes the open review threads on the same PR.