Fix [ClassCleanup] leak when ITestFilter drops the last test of an initialized class - #9503

Merged
Amaury Levé (Evangelink) merged 1 commit into
mainfrom
dev/amauryleve/classcleanup-filter-investigation
Jun 29, 2026
Merged

Fix [ClassCleanup] leak when ITestFilter drops the last test of an initialized class#9503
Amaury Levé (Evangelink) merged 1 commit into
mainfrom
dev/amauryleve/classcleanup-filter-investigation

Conversation

@Evangelink

Copy link
Copy Markdown
Member

Summary

Targets dev/amauryleve/custom-test-filter (#8896). Fixes a [ClassCleanup] leak in the new ITestFilter drop path, reported by an internal team using per-worker test sharding (each worker enumerates the full set per class, runs its subset, and Drops the rest).

The bug

UnitTestRunner.FinishFilteredOutTestAsync assumed a filtered-out test never loaded its type and therefore skipped class cleanup, only calling MarkClassComplete. That assumption is false when a sibling test of the same class already ran in this worker: [ClassInitialize] executed and [ClassCleanup] is still owed.

Because ClassCleanupManager counts down over the full (pre-filter) test set, isLastTestInClass is independent of run-vs-dropped. So whenever a class's last-in-order test is dropped — while earlier tests of that class ran — the worker leaks the class cleanup. ForceCleanup is not a safety net (it only fires on IsGracefulStopRequested).

The analogous assembly-cleanup asymmetry was already guarded by _assemblyInitializeWasExecuted, which is why only class cleanup leaked.

The fix

When the dropped test is the last in its class, run [ClassCleanup] if the class was initialized in this worker. Detection reuses the existing _lastRunnableTestByClass map, which is populated only for classes that both have an executable cleanup method and ran a non-filtered test. The TestClassInfo is already cached in the TypeCache, so resolving it loads no new type — preserving the feature's "a Drop pays zero type-load / init cost" guarantee. A fully-dropped class (type never loaded) still correctly skips both init and cleanup.

Validation

  • Added acceptance test TestFilterClassCleanupTests reproducing the leak: a filter drops the last-in-order test of an initialized class and the only test of a fully-dropped class.
    • Against the unfixed adapter the test fails — output shows ClassInitialize + the run test but no ClassCleanup, exactly the reported symptom.
    • Against the fixed adapter it passes; the fully-dropped class runs neither init nor cleanup.
  • MSTestAdapter.PlatformServices.UnitTests green across net462/net48/net8.0/net9.0/net8.0-windows.

Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com

CopilotAI review requested due to automatic review settings June 29, 2026 12:54
@Evangelink
Amaury Levé (Evangelink) changed the base branch from main to dev/amauryleve/custom-test-filterJune 29, 2026 12:55
@Evangelink
Amaury Levé (Evangelink) marked this pull request as ready for review June 29, 2026 12:55
@EvangelinkAmaury Levé (Evangelink) added the state/needs-review Awaiting review from the team. label Jun 29, 2026

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes a regression in the MSTest adapter’s new ITestFilter “drop/skip before type-load” path where [ClassCleanup] could be skipped when the last-in-order test of an already-initialized class was filtered out (common in per-worker sharding scenarios).

Changes:

  • Update UnitTestRunner.FinishFilteredOutTestAsync to execute [ClassCleanup] when the filtered-out test is the last test in the class and the class was previously initialized in the current worker.
  • Add an acceptance test that reproduces the leak (drop last-in-order test of a partially-run class) and verifies no over-correction for fully-dropped classes.
Show a summary per file
FileDescription
test/IntegrationTests/MSTest.Acceptance.IntegrationTests/TestFilterClassCleanupTests.csAdds an acceptance regression test covering the “last-in-order test dropped” class-cleanup leak and the “fully dropped class” non-init/non-cleanup guard.
src/Adapter/MSTestAdapter.PlatformServices/Execution/UnitTestRunner.csRuns class cleanup from the filter drop/skip tail path when the class was initialized earlier in the worker and the dropped/skipped test is the last test in its class.

Review details

  • Files reviewed: 2/2 changed files
  • Comments generated: 0
  • Review effort level: Low

@Evangelink

Copy link
Copy Markdown
MemberAuthor

🧪 Test quality grade — PR #9503

ΔTestGradeBandNotes
newTestFilterClassCleanupTests.
ClassCleanup_
RunsWhenLastTestOfInitializedClassIsDropped
A90–100Rich assertion set covers both the positive fix (ClassCleanup runs for the initialized class) and the guard against over-correction (ClassCleanup does not run for the fully-dropped class); clear AAA structure with informative comments.

This advisory comment was generated automatically. Grades are heuristic
and informational — they do not block merging. Re-run with
/grade-tests.

🤖 Automated content by GitHub Copilot. Posted via a maintainer's GitHub token, so it appears under their account — the account owner did not write or approve this content personally. Generated by the Grade Tests on PR (on open / sync) workflow. · 179.3 AIC · ⌖ 12.7 AIC · ⊞ 43.7K · [◷]( · )

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

🤖 Automated review by GitHub Copilot. Posted via a maintainer's GitHub token, so it appears under their account — the account owner did not write or approve this content personally. Generated by the Expert Code Review workflow. To request a follow-up action, reply by tagging @copilot directly.

#DimensionVerdict
13Test Completeness & Coverage🟡 1 MODERATE
16Naming & Conventions💬 1 NIT
17Documentation Accuracy💬 1 NIT

✅ 19/22 dimensions clean.

  • Test Completeness — the cleanupResult is not null branch (cleanup fails in the drop path) has no acceptance-test coverage; AssociatedUnitTestElement attribution is unvalidated
  • Naming — the Test_A_Run / Test_Z_Dropped alphabetical-ordering assumption is undocumented
  • Documentation — successful [ClassCleanup] output is silently discarded when filterResult is empty; no comment explains this

Overall assessment: The fix is algorithmically correct and well-reasoned. The _lastRunnableTestByClass sentinel is the right signal: it's populated only when both conditions are true — the class has an executable cleanup method and at least one non-filtered test ran in this worker — so a fully-dropped class is still correctly skipped. The ExecuteClassCleanupAsync semaphore and IsClassCleanupExecuted flag in TestClassInfo prevent any risk of double execution even under concurrent test scheduling. Thread safety is maintained via ConcurrentDictionary.TryGetValue. The ConfigureAwait(false) and try/finally disposal patterns mirror the surrounding code correctly. The three items above are polish suggestions; none blocks the merge.

Base automatically changed from dev/amauryleve/custom-test-filter to mainJune 29, 2026 14:34
…alized class is dropped
FinishFilteredOutTestAsync assumed a filtered-out test never loaded its type and therefore skipped class cleanup. That is false when a sibling test of the same class already ran in this worker: [ClassInitialize] executed and [ClassCleanup] is still owed. Because ClassCleanupManager counts down over the full (pre-filter) test set, isLastTestInClass can land on a dropped test, leaking the class cleanup whenever a class's last-in-order test is filtered out.
Fix: when the dropped test is the last in its class, run [ClassCleanup] if the class was initialized in this worker (detected via _lastRunnableTestByClass, which is only populated for classes that have an executable cleanup method and ran a non-filtered test). The TestClassInfo is already cached, so no new type is loaded. Adds an acceptance test reproducing the leak.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink
Amaury Levé (Evangelink)force-pushed the dev/amauryleve/classcleanup-filter-investigation branch from 7c1cdff to dc17bcdCompareJune 29, 2026 14:51
@Evangelink
Amaury Levé (Evangelink) merged commit 3a1375a into mainJun 29, 2026
14 of 16 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the dev/amauryleve/classcleanup-filter-investigation branch June 29, 2026 14:52
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

state/needs-reviewAwaiting review from the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@Evangelink@0101
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Fix [ClassCleanup] leak when ITestFilter drops the last test of an initialized class - #9503

Merged
Amaury Levé (Evangelink) merged 1 commit into
mainfrom
dev/amauryleve/classcleanup-filter-investigation
Jun 29, 2026
Merged

Fix [ClassCleanup] leak when ITestFilter drops the last test of an initialized class#9503
Amaury Levé (Evangelink) merged 1 commit into
mainfrom
dev/amauryleve/classcleanup-filter-investigation

Conversation

@Evangelink

Copy link
Copy Markdown
Member

Summary

Targets dev/amauryleve/custom-test-filter (#8896). Fixes a [ClassCleanup] leak in the new ITestFilter drop path, reported by an internal team using per-worker test sharding (each worker enumerates the full set per class, runs its subset, and Drops the rest).

The bug

UnitTestRunner.FinishFilteredOutTestAsync assumed a filtered-out test never loaded its type and therefore skipped class cleanup, only calling MarkClassComplete. That assumption is false when a sibling test of the same class already ran in this worker: [ClassInitialize] executed and [ClassCleanup] is still owed.

Because ClassCleanupManager counts down over the full (pre-filter) test set, isLastTestInClass is independent of run-vs-dropped. So whenever a class's last-in-order test is dropped — while earlier tests of that class ran — the worker leaks the class cleanup. ForceCleanup is not a safety net (it only fires on IsGracefulStopRequested).

The analogous assembly-cleanup asymmetry was already guarded by _assemblyInitializeWasExecuted, which is why only class cleanup leaked.

The fix

When the dropped test is the last in its class, run [ClassCleanup] if the class was initialized in this worker. Detection reuses the existing _lastRunnableTestByClass map, which is populated only for classes that both have an executable cleanup method and ran a non-filtered test. The TestClassInfo is already cached in the TypeCache, so resolving it loads no new type — preserving the feature's "a Drop pays zero type-load / init cost" guarantee. A fully-dropped class (type never loaded) still correctly skips both init and cleanup.

Validation

  • Added acceptance test TestFilterClassCleanupTests reproducing the leak: a filter drops the last-in-order test of an initialized class and the only test of a fully-dropped class.
    • Against the unfixed adapter the test fails — output shows ClassInitialize + the run test but no ClassCleanup, exactly the reported symptom.
    • Against the fixed adapter it passes; the fully-dropped class runs neither init nor cleanup.
  • MSTestAdapter.PlatformServices.UnitTests green across net462/net48/net8.0/net9.0/net8.0-windows.

Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com

CopilotAI review requested due to automatic review settings June 29, 2026 12:54
@Evangelink
Amaury Levé (Evangelink) changed the base branch from main to dev/amauryleve/custom-test-filterJune 29, 2026 12:55
@Evangelink
Amaury Levé (Evangelink) marked this pull request as ready for review June 29, 2026 12:55
@EvangelinkAmaury Levé (Evangelink) added the state/needs-review Awaiting review from the team. label Jun 29, 2026

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes a regression in the MSTest adapter’s new ITestFilter “drop/skip before type-load” path where [ClassCleanup] could be skipped when the last-in-order test of an already-initialized class was filtered out (common in per-worker sharding scenarios).

Changes:

  • Update UnitTestRunner.FinishFilteredOutTestAsync to execute [ClassCleanup] when the filtered-out test is the last test in the class and the class was previously initialized in the current worker.
  • Add an acceptance test that reproduces the leak (drop last-in-order test of a partially-run class) and verifies no over-correction for fully-dropped classes.
Show a summary per file
FileDescription
test/IntegrationTests/MSTest.Acceptance.IntegrationTests/TestFilterClassCleanupTests.csAdds an acceptance regression test covering the “last-in-order test dropped” class-cleanup leak and the “fully dropped class” non-init/non-cleanup guard.
src/Adapter/MSTestAdapter.PlatformServices/Execution/UnitTestRunner.csRuns class cleanup from the filter drop/skip tail path when the class was initialized earlier in the worker and the dropped/skipped test is the last test in its class.

Review details

  • Files reviewed: 2/2 changed files
  • Comments generated: 0
  • Review effort level: Low

@Evangelink

Copy link
Copy Markdown
MemberAuthor

🧪 Test quality grade — PR #9503

ΔTestGradeBandNotes
newTestFilterClassCleanupTests.
ClassCleanup_
RunsWhenLastTestOfInitializedClassIsDropped
A90–100Rich assertion set covers both the positive fix (ClassCleanup runs for the initialized class) and the guard against over-correction (ClassCleanup does not run for the fully-dropped class); clear AAA structure with informative comments.

This advisory comment was generated automatically. Grades are heuristic
and informational — they do not block merging. Re-run with
/grade-tests.

🤖 Automated content by GitHub Copilot. Posted via a maintainer's GitHub token, so it appears under their account — the account owner did not write or approve this content personally. Generated by the Grade Tests on PR (on open / sync) workflow. · 179.3 AIC · ⌖ 12.7 AIC · ⊞ 43.7K · [◷]( · )

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

🤖 Automated review by GitHub Copilot. Posted via a maintainer's GitHub token, so it appears under their account — the account owner did not write or approve this content personally. Generated by the Expert Code Review workflow. To request a follow-up action, reply by tagging @copilot directly.

#DimensionVerdict
13Test Completeness & Coverage🟡 1 MODERATE
16Naming & Conventions💬 1 NIT
17Documentation Accuracy💬 1 NIT

✅ 19/22 dimensions clean.

  • Test Completeness — the cleanupResult is not null branch (cleanup fails in the drop path) has no acceptance-test coverage; AssociatedUnitTestElement attribution is unvalidated
  • Naming — the Test_A_Run / Test_Z_Dropped alphabetical-ordering assumption is undocumented
  • Documentation — successful [ClassCleanup] output is silently discarded when filterResult is empty; no comment explains this

Overall assessment: The fix is algorithmically correct and well-reasoned. The _lastRunnableTestByClass sentinel is the right signal: it's populated only when both conditions are true — the class has an executable cleanup method and at least one non-filtered test ran in this worker — so a fully-dropped class is still correctly skipped. The ExecuteClassCleanupAsync semaphore and IsClassCleanupExecuted flag in TestClassInfo prevent any risk of double execution even under concurrent test scheduling. Thread safety is maintained via ConcurrentDictionary.TryGetValue. The ConfigureAwait(false) and try/finally disposal patterns mirror the surrounding code correctly. The three items above are polish suggestions; none blocks the merge.

Base automatically changed from dev/amauryleve/custom-test-filter to mainJune 29, 2026 14:34
…alized class is dropped
FinishFilteredOutTestAsync assumed a filtered-out test never loaded its type and therefore skipped class cleanup. That is false when a sibling test of the same class already ran in this worker: [ClassInitialize] executed and [ClassCleanup] is still owed. Because ClassCleanupManager counts down over the full (pre-filter) test set, isLastTestInClass can land on a dropped test, leaking the class cleanup whenever a class's last-in-order test is filtered out.
Fix: when the dropped test is the last in its class, run [ClassCleanup] if the class was initialized in this worker (detected via _lastRunnableTestByClass, which is only populated for classes that have an executable cleanup method and ran a non-filtered test). The TestClassInfo is already cached, so no new type is loaded. Adds an acceptance test reproducing the leak.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink
Amaury Levé (Evangelink)force-pushed the dev/amauryleve/classcleanup-filter-investigation branch from 7c1cdff to dc17bcdCompareJune 29, 2026 14:51
@Evangelink
Amaury Levé (Evangelink) merged commit 3a1375a into mainJun 29, 2026
14 of 16 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the dev/amauryleve/classcleanup-filter-investigation branch June 29, 2026 14:52
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

state/needs-reviewAwaiting review from the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@Evangelink@0101
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Fix [ClassCleanup] leak when ITestFilter drops the last test of an initialized class - #9503

Merged
Amaury Levé (Evangelink) merged 1 commit into
mainfrom
dev/amauryleve/classcleanup-filter-investigation
Jun 29, 2026
Merged

Fix [ClassCleanup] leak when ITestFilter drops the last test of an initialized class#9503
Amaury Levé (Evangelink) merged 1 commit into
mainfrom
dev/amauryleve/classcleanup-filter-investigation

Conversation

@Evangelink

Copy link
Copy Markdown
Member

Summary

Targets dev/amauryleve/custom-test-filter (#8896). Fixes a [ClassCleanup] leak in the new ITestFilter drop path, reported by an internal team using per-worker test sharding (each worker enumerates the full set per class, runs its subset, and Drops the rest).

The bug

UnitTestRunner.FinishFilteredOutTestAsync assumed a filtered-out test never loaded its type and therefore skipped class cleanup, only calling MarkClassComplete. That assumption is false when a sibling test of the same class already ran in this worker: [ClassInitialize] executed and [ClassCleanup] is still owed.

Because ClassCleanupManager counts down over the full (pre-filter) test set, isLastTestInClass is independent of run-vs-dropped. So whenever a class's last-in-order test is dropped — while earlier tests of that class ran — the worker leaks the class cleanup. ForceCleanup is not a safety net (it only fires on IsGracefulStopRequested).

The analogous assembly-cleanup asymmetry was already guarded by _assemblyInitializeWasExecuted, which is why only class cleanup leaked.

The fix

When the dropped test is the last in its class, run [ClassCleanup] if the class was initialized in this worker. Detection reuses the existing _lastRunnableTestByClass map, which is populated only for classes that both have an executable cleanup method and ran a non-filtered test. The TestClassInfo is already cached in the TypeCache, so resolving it loads no new type — preserving the feature's "a Drop pays zero type-load / init cost" guarantee. A fully-dropped class (type never loaded) still correctly skips both init and cleanup.

Validation

  • Added acceptance test TestFilterClassCleanupTests reproducing the leak: a filter drops the last-in-order test of an initialized class and the only test of a fully-dropped class.
    • Against the unfixed adapter the test fails — output shows ClassInitialize + the run test but no ClassCleanup, exactly the reported symptom.
    • Against the fixed adapter it passes; the fully-dropped class runs neither init nor cleanup.
  • MSTestAdapter.PlatformServices.UnitTests green across net462/net48/net8.0/net9.0/net8.0-windows.

Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com

CopilotAI review requested due to automatic review settings June 29, 2026 12:54
@Evangelink
Amaury Levé (Evangelink) changed the base branch from main to dev/amauryleve/custom-test-filterJune 29, 2026 12:55
@Evangelink
Amaury Levé (Evangelink) marked this pull request as ready for review June 29, 2026 12:55
@EvangelinkAmaury Levé (Evangelink) added the state/needs-review Awaiting review from the team. label Jun 29, 2026

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes a regression in the MSTest adapter’s new ITestFilter “drop/skip before type-load” path where [ClassCleanup] could be skipped when the last-in-order test of an already-initialized class was filtered out (common in per-worker sharding scenarios).

Changes:

  • Update UnitTestRunner.FinishFilteredOutTestAsync to execute [ClassCleanup] when the filtered-out test is the last test in the class and the class was previously initialized in the current worker.
  • Add an acceptance test that reproduces the leak (drop last-in-order test of a partially-run class) and verifies no over-correction for fully-dropped classes.
Show a summary per file
FileDescription
test/IntegrationTests/MSTest.Acceptance.IntegrationTests/TestFilterClassCleanupTests.csAdds an acceptance regression test covering the “last-in-order test dropped” class-cleanup leak and the “fully dropped class” non-init/non-cleanup guard.
src/Adapter/MSTestAdapter.PlatformServices/Execution/UnitTestRunner.csRuns class cleanup from the filter drop/skip tail path when the class was initialized earlier in the worker and the dropped/skipped test is the last test in its class.

Review details

  • Files reviewed: 2/2 changed files
  • Comments generated: 0
  • Review effort level: Low

@Evangelink

Copy link
Copy Markdown
MemberAuthor

🧪 Test quality grade — PR #9503

ΔTestGradeBandNotes
newTestFilterClassCleanupTests.
ClassCleanup_
RunsWhenLastTestOfInitializedClassIsDropped
A90–100Rich assertion set covers both the positive fix (ClassCleanup runs for the initialized class) and the guard against over-correction (ClassCleanup does not run for the fully-dropped class); clear AAA structure with informative comments.

This advisory comment was generated automatically. Grades are heuristic
and informational — they do not block merging. Re-run with
/grade-tests.

🤖 Automated content by GitHub Copilot. Posted via a maintainer's GitHub token, so it appears under their account — the account owner did not write or approve this content personally. Generated by the Grade Tests on PR (on open / sync) workflow. · 179.3 AIC · ⌖ 12.7 AIC · ⊞ 43.7K · [◷]( · )

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

🤖 Automated review by GitHub Copilot. Posted via a maintainer's GitHub token, so it appears under their account — the account owner did not write or approve this content personally. Generated by the Expert Code Review workflow. To request a follow-up action, reply by tagging @copilot directly.

#DimensionVerdict
13Test Completeness & Coverage🟡 1 MODERATE
16Naming & Conventions💬 1 NIT
17Documentation Accuracy💬 1 NIT

✅ 19/22 dimensions clean.

  • Test Completeness — the cleanupResult is not null branch (cleanup fails in the drop path) has no acceptance-test coverage; AssociatedUnitTestElement attribution is unvalidated
  • Naming — the Test_A_Run / Test_Z_Dropped alphabetical-ordering assumption is undocumented
  • Documentation — successful [ClassCleanup] output is silently discarded when filterResult is empty; no comment explains this

Overall assessment: The fix is algorithmically correct and well-reasoned. The _lastRunnableTestByClass sentinel is the right signal: it's populated only when both conditions are true — the class has an executable cleanup method and at least one non-filtered test ran in this worker — so a fully-dropped class is still correctly skipped. The ExecuteClassCleanupAsync semaphore and IsClassCleanupExecuted flag in TestClassInfo prevent any risk of double execution even under concurrent test scheduling. Thread safety is maintained via ConcurrentDictionary.TryGetValue. The ConfigureAwait(false) and try/finally disposal patterns mirror the surrounding code correctly. The three items above are polish suggestions; none blocks the merge.

Base automatically changed from dev/amauryleve/custom-test-filter to mainJune 29, 2026 14:34
…alized class is dropped
FinishFilteredOutTestAsync assumed a filtered-out test never loaded its type and therefore skipped class cleanup. That is false when a sibling test of the same class already ran in this worker: [ClassInitialize] executed and [ClassCleanup] is still owed. Because ClassCleanupManager counts down over the full (pre-filter) test set, isLastTestInClass can land on a dropped test, leaking the class cleanup whenever a class's last-in-order test is filtered out.
Fix: when the dropped test is the last in its class, run [ClassCleanup] if the class was initialized in this worker (detected via _lastRunnableTestByClass, which is only populated for classes that have an executable cleanup method and ran a non-filtered test). The TestClassInfo is already cached, so no new type is loaded. Adds an acceptance test reproducing the leak.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink
Amaury Levé (Evangelink)force-pushed the dev/amauryleve/classcleanup-filter-investigation branch from 7c1cdff to dc17bcdCompareJune 29, 2026 14:51
@Evangelink
Amaury Levé (Evangelink) merged commit 3a1375a into mainJun 29, 2026
14 of 16 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the dev/amauryleve/classcleanup-filter-investigation branch June 29, 2026 14:52
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

state/needs-reviewAwaiting review from the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@Evangelink@0101
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Fix [ClassCleanup] leak when ITestFilter drops the last test of an initialized class - #9503

Merged
Amaury Levé (Evangelink) merged 1 commit into
mainfrom
dev/amauryleve/classcleanup-filter-investigation
Jun 29, 2026
Merged

Fix [ClassCleanup] leak when ITestFilter drops the last test of an initialized class#9503
Amaury Levé (Evangelink) merged 1 commit into
mainfrom
dev/amauryleve/classcleanup-filter-investigation

Conversation

@Evangelink

Copy link
Copy Markdown
Member

Summary

Targets dev/amauryleve/custom-test-filter (#8896). Fixes a [ClassCleanup] leak in the new ITestFilter drop path, reported by an internal team using per-worker test sharding (each worker enumerates the full set per class, runs its subset, and Drops the rest).

The bug

UnitTestRunner.FinishFilteredOutTestAsync assumed a filtered-out test never loaded its type and therefore skipped class cleanup, only calling MarkClassComplete. That assumption is false when a sibling test of the same class already ran in this worker: [ClassInitialize] executed and [ClassCleanup] is still owed.

Because ClassCleanupManager counts down over the full (pre-filter) test set, isLastTestInClass is independent of run-vs-dropped. So whenever a class's last-in-order test is dropped — while earlier tests of that class ran — the worker leaks the class cleanup. ForceCleanup is not a safety net (it only fires on IsGracefulStopRequested).

The analogous assembly-cleanup asymmetry was already guarded by _assemblyInitializeWasExecuted, which is why only class cleanup leaked.

The fix

When the dropped test is the last in its class, run [ClassCleanup] if the class was initialized in this worker. Detection reuses the existing _lastRunnableTestByClass map, which is populated only for classes that both have an executable cleanup method and ran a non-filtered test. The TestClassInfo is already cached in the TypeCache, so resolving it loads no new type — preserving the feature's "a Drop pays zero type-load / init cost" guarantee. A fully-dropped class (type never loaded) still correctly skips both init and cleanup.

Validation

  • Added acceptance test TestFilterClassCleanupTests reproducing the leak: a filter drops the last-in-order test of an initialized class and the only test of a fully-dropped class.
    • Against the unfixed adapter the test fails — output shows ClassInitialize + the run test but no ClassCleanup, exactly the reported symptom.
    • Against the fixed adapter it passes; the fully-dropped class runs neither init nor cleanup.
  • MSTestAdapter.PlatformServices.UnitTests green across net462/net48/net8.0/net9.0/net8.0-windows.

Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com

CopilotAI review requested due to automatic review settings June 29, 2026 12:54
@Evangelink
Amaury Levé (Evangelink) changed the base branch from main to dev/amauryleve/custom-test-filterJune 29, 2026 12:55
@Evangelink
Amaury Levé (Evangelink) marked this pull request as ready for review June 29, 2026 12:55
@EvangelinkAmaury Levé (Evangelink) added the state/needs-review Awaiting review from the team. label Jun 29, 2026

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes a regression in the MSTest adapter’s new ITestFilter “drop/skip before type-load” path where [ClassCleanup] could be skipped when the last-in-order test of an already-initialized class was filtered out (common in per-worker sharding scenarios).

Changes:

  • Update UnitTestRunner.FinishFilteredOutTestAsync to execute [ClassCleanup] when the filtered-out test is the last test in the class and the class was previously initialized in the current worker.
  • Add an acceptance test that reproduces the leak (drop last-in-order test of a partially-run class) and verifies no over-correction for fully-dropped classes.
Show a summary per file
FileDescription
test/IntegrationTests/MSTest.Acceptance.IntegrationTests/TestFilterClassCleanupTests.csAdds an acceptance regression test covering the “last-in-order test dropped” class-cleanup leak and the “fully dropped class” non-init/non-cleanup guard.
src/Adapter/MSTestAdapter.PlatformServices/Execution/UnitTestRunner.csRuns class cleanup from the filter drop/skip tail path when the class was initialized earlier in the worker and the dropped/skipped test is the last test in its class.

Review details

  • Files reviewed: 2/2 changed files
  • Comments generated: 0
  • Review effort level: Low

@Evangelink

Copy link
Copy Markdown
MemberAuthor

🧪 Test quality grade — PR #9503

ΔTestGradeBandNotes
newTestFilterClassCleanupTests.
ClassCleanup_
RunsWhenLastTestOfInitializedClassIsDropped
A90–100Rich assertion set covers both the positive fix (ClassCleanup runs for the initialized class) and the guard against over-correction (ClassCleanup does not run for the fully-dropped class); clear AAA structure with informative comments.

This advisory comment was generated automatically. Grades are heuristic
and informational — they do not block merging. Re-run with
/grade-tests.

🤖 Automated content by GitHub Copilot. Posted via a maintainer's GitHub token, so it appears under their account — the account owner did not write or approve this content personally. Generated by the Grade Tests on PR (on open / sync) workflow. · 179.3 AIC · ⌖ 12.7 AIC · ⊞ 43.7K · [◷]( · )

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

🤖 Automated review by GitHub Copilot. Posted via a maintainer's GitHub token, so it appears under their account — the account owner did not write or approve this content personally. Generated by the Expert Code Review workflow. To request a follow-up action, reply by tagging @copilot directly.

#DimensionVerdict
13Test Completeness & Coverage🟡 1 MODERATE
16Naming & Conventions💬 1 NIT
17Documentation Accuracy💬 1 NIT

✅ 19/22 dimensions clean.

  • Test Completeness — the cleanupResult is not null branch (cleanup fails in the drop path) has no acceptance-test coverage; AssociatedUnitTestElement attribution is unvalidated
  • Naming — the Test_A_Run / Test_Z_Dropped alphabetical-ordering assumption is undocumented
  • Documentation — successful [ClassCleanup] output is silently discarded when filterResult is empty; no comment explains this

Overall assessment: The fix is algorithmically correct and well-reasoned. The _lastRunnableTestByClass sentinel is the right signal: it's populated only when both conditions are true — the class has an executable cleanup method and at least one non-filtered test ran in this worker — so a fully-dropped class is still correctly skipped. The ExecuteClassCleanupAsync semaphore and IsClassCleanupExecuted flag in TestClassInfo prevent any risk of double execution even under concurrent test scheduling. Thread safety is maintained via ConcurrentDictionary.TryGetValue. The ConfigureAwait(false) and try/finally disposal patterns mirror the surrounding code correctly. The three items above are polish suggestions; none blocks the merge.

Base automatically changed from dev/amauryleve/custom-test-filter to mainJune 29, 2026 14:34
…alized class is dropped
FinishFilteredOutTestAsync assumed a filtered-out test never loaded its type and therefore skipped class cleanup. That is false when a sibling test of the same class already ran in this worker: [ClassInitialize] executed and [ClassCleanup] is still owed. Because ClassCleanupManager counts down over the full (pre-filter) test set, isLastTestInClass can land on a dropped test, leaking the class cleanup whenever a class's last-in-order test is filtered out.
Fix: when the dropped test is the last in its class, run [ClassCleanup] if the class was initialized in this worker (detected via _lastRunnableTestByClass, which is only populated for classes that have an executable cleanup method and ran a non-filtered test). The TestClassInfo is already cached, so no new type is loaded. Adds an acceptance test reproducing the leak.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink
Amaury Levé (Evangelink)force-pushed the dev/amauryleve/classcleanup-filter-investigation branch from 7c1cdff to dc17bcdCompareJune 29, 2026 14:51
@Evangelink
Amaury Levé (Evangelink) merged commit 3a1375a into mainJun 29, 2026
14 of 16 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the dev/amauryleve/classcleanup-filter-investigation branch June 29, 2026 14:52
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

state/needs-reviewAwaiting review from the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@Evangelink@0101
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Fix [ClassCleanup] leak when ITestFilter drops the last test of an initialized class - #9503

Merged
Amaury Levé (Evangelink) merged 1 commit into
mainfrom
dev/amauryleve/classcleanup-filter-investigation
Jun 29, 2026
Merged

Fix [ClassCleanup] leak when ITestFilter drops the last test of an initialized class#9503
Amaury Levé (Evangelink) merged 1 commit into
mainfrom
dev/amauryleve/classcleanup-filter-investigation

Conversation

@Evangelink

Copy link
Copy Markdown
Member

Summary

Targets dev/amauryleve/custom-test-filter (#8896). Fixes a [ClassCleanup] leak in the new ITestFilter drop path, reported by an internal team using per-worker test sharding (each worker enumerates the full set per class, runs its subset, and Drops the rest).

The bug

UnitTestRunner.FinishFilteredOutTestAsync assumed a filtered-out test never loaded its type and therefore skipped class cleanup, only calling MarkClassComplete. That assumption is false when a sibling test of the same class already ran in this worker: [ClassInitialize] executed and [ClassCleanup] is still owed.

Because ClassCleanupManager counts down over the full (pre-filter) test set, isLastTestInClass is independent of run-vs-dropped. So whenever a class's last-in-order test is dropped — while earlier tests of that class ran — the worker leaks the class cleanup. ForceCleanup is not a safety net (it only fires on IsGracefulStopRequested).

The analogous assembly-cleanup asymmetry was already guarded by _assemblyInitializeWasExecuted, which is why only class cleanup leaked.

The fix

When the dropped test is the last in its class, run [ClassCleanup] if the class was initialized in this worker. Detection reuses the existing _lastRunnableTestByClass map, which is populated only for classes that both have an executable cleanup method and ran a non-filtered test. The TestClassInfo is already cached in the TypeCache, so resolving it loads no new type — preserving the feature's "a Drop pays zero type-load / init cost" guarantee. A fully-dropped class (type never loaded) still correctly skips both init and cleanup.

Validation

  • Added acceptance test TestFilterClassCleanupTests reproducing the leak: a filter drops the last-in-order test of an initialized class and the only test of a fully-dropped class.
    • Against the unfixed adapter the test fails — output shows ClassInitialize + the run test but no ClassCleanup, exactly the reported symptom.
    • Against the fixed adapter it passes; the fully-dropped class runs neither init nor cleanup.
  • MSTestAdapter.PlatformServices.UnitTests green across net462/net48/net8.0/net9.0/net8.0-windows.

Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com

CopilotAI review requested due to automatic review settings June 29, 2026 12:54
@Evangelink
Amaury Levé (Evangelink) changed the base branch from main to dev/amauryleve/custom-test-filterJune 29, 2026 12:55
@Evangelink
Amaury Levé (Evangelink) marked this pull request as ready for review June 29, 2026 12:55
@EvangelinkAmaury Levé (Evangelink) added the state/needs-review Awaiting review from the team. label Jun 29, 2026

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes a regression in the MSTest adapter’s new ITestFilter “drop/skip before type-load” path where [ClassCleanup] could be skipped when the last-in-order test of an already-initialized class was filtered out (common in per-worker sharding scenarios).

Changes:

  • Update UnitTestRunner.FinishFilteredOutTestAsync to execute [ClassCleanup] when the filtered-out test is the last test in the class and the class was previously initialized in the current worker.
  • Add an acceptance test that reproduces the leak (drop last-in-order test of a partially-run class) and verifies no over-correction for fully-dropped classes.
Show a summary per file
FileDescription
test/IntegrationTests/MSTest.Acceptance.IntegrationTests/TestFilterClassCleanupTests.csAdds an acceptance regression test covering the “last-in-order test dropped” class-cleanup leak and the “fully dropped class” non-init/non-cleanup guard.
src/Adapter/MSTestAdapter.PlatformServices/Execution/UnitTestRunner.csRuns class cleanup from the filter drop/skip tail path when the class was initialized earlier in the worker and the dropped/skipped test is the last test in its class.

Review details

  • Files reviewed: 2/2 changed files
  • Comments generated: 0
  • Review effort level: Low

@Evangelink

Copy link
Copy Markdown
MemberAuthor

🧪 Test quality grade — PR #9503

ΔTestGradeBandNotes
newTestFilterClassCleanupTests.
ClassCleanup_
RunsWhenLastTestOfInitializedClassIsDropped
A90–100Rich assertion set covers both the positive fix (ClassCleanup runs for the initialized class) and the guard against over-correction (ClassCleanup does not run for the fully-dropped class); clear AAA structure with informative comments.

This advisory comment was generated automatically. Grades are heuristic
and informational — they do not block merging. Re-run with
/grade-tests.

🤖 Automated content by GitHub Copilot. Posted via a maintainer's GitHub token, so it appears under their account — the account owner did not write or approve this content personally. Generated by the Grade Tests on PR (on open / sync) workflow. · 179.3 AIC · ⌖ 12.7 AIC · ⊞ 43.7K · [◷]( · )

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

🤖 Automated review by GitHub Copilot. Posted via a maintainer's GitHub token, so it appears under their account — the account owner did not write or approve this content personally. Generated by the Expert Code Review workflow. To request a follow-up action, reply by tagging @copilot directly.

#DimensionVerdict
13Test Completeness & Coverage🟡 1 MODERATE
16Naming & Conventions💬 1 NIT
17Documentation Accuracy💬 1 NIT

✅ 19/22 dimensions clean.

  • Test Completeness — the cleanupResult is not null branch (cleanup fails in the drop path) has no acceptance-test coverage; AssociatedUnitTestElement attribution is unvalidated
  • Naming — the Test_A_Run / Test_Z_Dropped alphabetical-ordering assumption is undocumented
  • Documentation — successful [ClassCleanup] output is silently discarded when filterResult is empty; no comment explains this

Overall assessment: The fix is algorithmically correct and well-reasoned. The _lastRunnableTestByClass sentinel is the right signal: it's populated only when both conditions are true — the class has an executable cleanup method and at least one non-filtered test ran in this worker — so a fully-dropped class is still correctly skipped. The ExecuteClassCleanupAsync semaphore and IsClassCleanupExecuted flag in TestClassInfo prevent any risk of double execution even under concurrent test scheduling. Thread safety is maintained via ConcurrentDictionary.TryGetValue. The ConfigureAwait(false) and try/finally disposal patterns mirror the surrounding code correctly. The three items above are polish suggestions; none blocks the merge.

Base automatically changed from dev/amauryleve/custom-test-filter to mainJune 29, 2026 14:34
…alized class is dropped
FinishFilteredOutTestAsync assumed a filtered-out test never loaded its type and therefore skipped class cleanup. That is false when a sibling test of the same class already ran in this worker: [ClassInitialize] executed and [ClassCleanup] is still owed. Because ClassCleanupManager counts down over the full (pre-filter) test set, isLastTestInClass can land on a dropped test, leaking the class cleanup whenever a class's last-in-order test is filtered out.
Fix: when the dropped test is the last in its class, run [ClassCleanup] if the class was initialized in this worker (detected via _lastRunnableTestByClass, which is only populated for classes that have an executable cleanup method and ran a non-filtered test). The TestClassInfo is already cached, so no new type is loaded. Adds an acceptance test reproducing the leak.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink
Amaury Levé (Evangelink)force-pushed the dev/amauryleve/classcleanup-filter-investigation branch from 7c1cdff to dc17bcdCompareJune 29, 2026 14:51
@Evangelink
Amaury Levé (Evangelink) merged commit 3a1375a into mainJun 29, 2026
14 of 16 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the dev/amauryleve/classcleanup-filter-investigation branch June 29, 2026 14:52
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

state/needs-reviewAwaiting review from the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@Evangelink@0101
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Fix [ClassCleanup] leak when ITestFilter drops the last test of an initialized class - #9503

Merged
Amaury Levé (Evangelink) merged 1 commit into
mainfrom
dev/amauryleve/classcleanup-filter-investigation
Jun 29, 2026
Merged

Fix [ClassCleanup] leak when ITestFilter drops the last test of an initialized class#9503
Amaury Levé (Evangelink) merged 1 commit into
mainfrom
dev/amauryleve/classcleanup-filter-investigation

Conversation

@Evangelink

Copy link
Copy Markdown
Member

Summary

Targets dev/amauryleve/custom-test-filter (#8896). Fixes a [ClassCleanup] leak in the new ITestFilter drop path, reported by an internal team using per-worker test sharding (each worker enumerates the full set per class, runs its subset, and Drops the rest).

The bug

UnitTestRunner.FinishFilteredOutTestAsync assumed a filtered-out test never loaded its type and therefore skipped class cleanup, only calling MarkClassComplete. That assumption is false when a sibling test of the same class already ran in this worker: [ClassInitialize] executed and [ClassCleanup] is still owed.

Because ClassCleanupManager counts down over the full (pre-filter) test set, isLastTestInClass is independent of run-vs-dropped. So whenever a class's last-in-order test is dropped — while earlier tests of that class ran — the worker leaks the class cleanup. ForceCleanup is not a safety net (it only fires on IsGracefulStopRequested).

The analogous assembly-cleanup asymmetry was already guarded by _assemblyInitializeWasExecuted, which is why only class cleanup leaked.

The fix

When the dropped test is the last in its class, run [ClassCleanup] if the class was initialized in this worker. Detection reuses the existing _lastRunnableTestByClass map, which is populated only for classes that both have an executable cleanup method and ran a non-filtered test. The TestClassInfo is already cached in the TypeCache, so resolving it loads no new type — preserving the feature's "a Drop pays zero type-load / init cost" guarantee. A fully-dropped class (type never loaded) still correctly skips both init and cleanup.

Validation

  • Added acceptance test TestFilterClassCleanupTests reproducing the leak: a filter drops the last-in-order test of an initialized class and the only test of a fully-dropped class.
    • Against the unfixed adapter the test fails — output shows ClassInitialize + the run test but no ClassCleanup, exactly the reported symptom.
    • Against the fixed adapter it passes; the fully-dropped class runs neither init nor cleanup.
  • MSTestAdapter.PlatformServices.UnitTests green across net462/net48/net8.0/net9.0/net8.0-windows.

Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com

CopilotAI review requested due to automatic review settings June 29, 2026 12:54
@Evangelink
Amaury Levé (Evangelink) changed the base branch from main to dev/amauryleve/custom-test-filterJune 29, 2026 12:55
@Evangelink
Amaury Levé (Evangelink) marked this pull request as ready for review June 29, 2026 12:55
@EvangelinkAmaury Levé (Evangelink) added the state/needs-review Awaiting review from the team. label Jun 29, 2026

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes a regression in the MSTest adapter’s new ITestFilter “drop/skip before type-load” path where [ClassCleanup] could be skipped when the last-in-order test of an already-initialized class was filtered out (common in per-worker sharding scenarios).

Changes:

  • Update UnitTestRunner.FinishFilteredOutTestAsync to execute [ClassCleanup] when the filtered-out test is the last test in the class and the class was previously initialized in the current worker.
  • Add an acceptance test that reproduces the leak (drop last-in-order test of a partially-run class) and verifies no over-correction for fully-dropped classes.
Show a summary per file
FileDescription
test/IntegrationTests/MSTest.Acceptance.IntegrationTests/TestFilterClassCleanupTests.csAdds an acceptance regression test covering the “last-in-order test dropped” class-cleanup leak and the “fully dropped class” non-init/non-cleanup guard.
src/Adapter/MSTestAdapter.PlatformServices/Execution/UnitTestRunner.csRuns class cleanup from the filter drop/skip tail path when the class was initialized earlier in the worker and the dropped/skipped test is the last test in its class.

Review details

  • Files reviewed: 2/2 changed files
  • Comments generated: 0
  • Review effort level: Low

@Evangelink

Copy link
Copy Markdown
MemberAuthor

🧪 Test quality grade — PR #9503

ΔTestGradeBandNotes
newTestFilterClassCleanupTests.
ClassCleanup_
RunsWhenLastTestOfInitializedClassIsDropped
A90–100Rich assertion set covers both the positive fix (ClassCleanup runs for the initialized class) and the guard against over-correction (ClassCleanup does not run for the fully-dropped class); clear AAA structure with informative comments.

This advisory comment was generated automatically. Grades are heuristic
and informational — they do not block merging. Re-run with
/grade-tests.

🤖 Automated content by GitHub Copilot. Posted via a maintainer's GitHub token, so it appears under their account — the account owner did not write or approve this content personally. Generated by the Grade Tests on PR (on open / sync) workflow. · 179.3 AIC · ⌖ 12.7 AIC · ⊞ 43.7K · [◷]( · )

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

🤖 Automated review by GitHub Copilot. Posted via a maintainer's GitHub token, so it appears under their account — the account owner did not write or approve this content personally. Generated by the Expert Code Review workflow. To request a follow-up action, reply by tagging @copilot directly.

#DimensionVerdict
13Test Completeness & Coverage🟡 1 MODERATE
16Naming & Conventions💬 1 NIT
17Documentation Accuracy💬 1 NIT

✅ 19/22 dimensions clean.

  • Test Completeness — the cleanupResult is not null branch (cleanup fails in the drop path) has no acceptance-test coverage; AssociatedUnitTestElement attribution is unvalidated
  • Naming — the Test_A_Run / Test_Z_Dropped alphabetical-ordering assumption is undocumented
  • Documentation — successful [ClassCleanup] output is silently discarded when filterResult is empty; no comment explains this

Overall assessment: The fix is algorithmically correct and well-reasoned. The _lastRunnableTestByClass sentinel is the right signal: it's populated only when both conditions are true — the class has an executable cleanup method and at least one non-filtered test ran in this worker — so a fully-dropped class is still correctly skipped. The ExecuteClassCleanupAsync semaphore and IsClassCleanupExecuted flag in TestClassInfo prevent any risk of double execution even under concurrent test scheduling. Thread safety is maintained via ConcurrentDictionary.TryGetValue. The ConfigureAwait(false) and try/finally disposal patterns mirror the surrounding code correctly. The three items above are polish suggestions; none blocks the merge.

Base automatically changed from dev/amauryleve/custom-test-filter to mainJune 29, 2026 14:34
…alized class is dropped
FinishFilteredOutTestAsync assumed a filtered-out test never loaded its type and therefore skipped class cleanup. That is false when a sibling test of the same class already ran in this worker: [ClassInitialize] executed and [ClassCleanup] is still owed. Because ClassCleanupManager counts down over the full (pre-filter) test set, isLastTestInClass can land on a dropped test, leaking the class cleanup whenever a class's last-in-order test is filtered out.
Fix: when the dropped test is the last in its class, run [ClassCleanup] if the class was initialized in this worker (detected via _lastRunnableTestByClass, which is only populated for classes that have an executable cleanup method and ran a non-filtered test). The TestClassInfo is already cached, so no new type is loaded. Adds an acceptance test reproducing the leak.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink
Amaury Levé (Evangelink)force-pushed the dev/amauryleve/classcleanup-filter-investigation branch from 7c1cdff to dc17bcdCompareJune 29, 2026 14:51
@Evangelink
Amaury Levé (Evangelink) merged commit 3a1375a into mainJun 29, 2026
14 of 16 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the dev/amauryleve/classcleanup-filter-investigation branch June 29, 2026 14:52
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

state/needs-reviewAwaiting review from the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@Evangelink@0101
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Fix [ClassCleanup] leak when ITestFilter drops the last test of an initialized class - #9503

Merged
Amaury Levé (Evangelink) merged 1 commit into
mainfrom
dev/amauryleve/classcleanup-filter-investigation
Jun 29, 2026
Merged

Fix [ClassCleanup] leak when ITestFilter drops the last test of an initialized class#9503
Amaury Levé (Evangelink) merged 1 commit into
mainfrom
dev/amauryleve/classcleanup-filter-investigation

Conversation

@Evangelink

Copy link
Copy Markdown
Member

Summary

Targets dev/amauryleve/custom-test-filter (#8896). Fixes a [ClassCleanup] leak in the new ITestFilter drop path, reported by an internal team using per-worker test sharding (each worker enumerates the full set per class, runs its subset, and Drops the rest).

The bug

UnitTestRunner.FinishFilteredOutTestAsync assumed a filtered-out test never loaded its type and therefore skipped class cleanup, only calling MarkClassComplete. That assumption is false when a sibling test of the same class already ran in this worker: [ClassInitialize] executed and [ClassCleanup] is still owed.

Because ClassCleanupManager counts down over the full (pre-filter) test set, isLastTestInClass is independent of run-vs-dropped. So whenever a class's last-in-order test is dropped — while earlier tests of that class ran — the worker leaks the class cleanup. ForceCleanup is not a safety net (it only fires on IsGracefulStopRequested).

The analogous assembly-cleanup asymmetry was already guarded by _assemblyInitializeWasExecuted, which is why only class cleanup leaked.

The fix

When the dropped test is the last in its class, run [ClassCleanup] if the class was initialized in this worker. Detection reuses the existing _lastRunnableTestByClass map, which is populated only for classes that both have an executable cleanup method and ran a non-filtered test. The TestClassInfo is already cached in the TypeCache, so resolving it loads no new type — preserving the feature's "a Drop pays zero type-load / init cost" guarantee. A fully-dropped class (type never loaded) still correctly skips both init and cleanup.

Validation

  • Added acceptance test TestFilterClassCleanupTests reproducing the leak: a filter drops the last-in-order test of an initialized class and the only test of a fully-dropped class.
    • Against the unfixed adapter the test fails — output shows ClassInitialize + the run test but no ClassCleanup, exactly the reported symptom.
    • Against the fixed adapter it passes; the fully-dropped class runs neither init nor cleanup.
  • MSTestAdapter.PlatformServices.UnitTests green across net462/net48/net8.0/net9.0/net8.0-windows.

Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com

CopilotAI review requested due to automatic review settings June 29, 2026 12:54
@Evangelink
Amaury Levé (Evangelink) changed the base branch from main to dev/amauryleve/custom-test-filterJune 29, 2026 12:55
@Evangelink
Amaury Levé (Evangelink) marked this pull request as ready for review June 29, 2026 12:55
@EvangelinkAmaury Levé (Evangelink) added the state/needs-review Awaiting review from the team. label Jun 29, 2026

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes a regression in the MSTest adapter’s new ITestFilter “drop/skip before type-load” path where [ClassCleanup] could be skipped when the last-in-order test of an already-initialized class was filtered out (common in per-worker sharding scenarios).

Changes:

  • Update UnitTestRunner.FinishFilteredOutTestAsync to execute [ClassCleanup] when the filtered-out test is the last test in the class and the class was previously initialized in the current worker.
  • Add an acceptance test that reproduces the leak (drop last-in-order test of a partially-run class) and verifies no over-correction for fully-dropped classes.
Show a summary per file
FileDescription
test/IntegrationTests/MSTest.Acceptance.IntegrationTests/TestFilterClassCleanupTests.csAdds an acceptance regression test covering the “last-in-order test dropped” class-cleanup leak and the “fully dropped class” non-init/non-cleanup guard.
src/Adapter/MSTestAdapter.PlatformServices/Execution/UnitTestRunner.csRuns class cleanup from the filter drop/skip tail path when the class was initialized earlier in the worker and the dropped/skipped test is the last test in its class.

Review details

  • Files reviewed: 2/2 changed files
  • Comments generated: 0
  • Review effort level: Low

@Evangelink

Copy link
Copy Markdown
MemberAuthor

🧪 Test quality grade — PR #9503

ΔTestGradeBandNotes
newTestFilterClassCleanupTests.
ClassCleanup_
RunsWhenLastTestOfInitializedClassIsDropped
A90–100Rich assertion set covers both the positive fix (ClassCleanup runs for the initialized class) and the guard against over-correction (ClassCleanup does not run for the fully-dropped class); clear AAA structure with informative comments.

This advisory comment was generated automatically. Grades are heuristic
and informational — they do not block merging. Re-run with
/grade-tests.

🤖 Automated content by GitHub Copilot. Posted via a maintainer's GitHub token, so it appears under their account — the account owner did not write or approve this content personally. Generated by the Grade Tests on PR (on open / sync) workflow. · 179.3 AIC · ⌖ 12.7 AIC · ⊞ 43.7K · [◷]( · )

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

🤖 Automated review by GitHub Copilot. Posted via a maintainer's GitHub token, so it appears under their account — the account owner did not write or approve this content personally. Generated by the Expert Code Review workflow. To request a follow-up action, reply by tagging @copilot directly.

#DimensionVerdict
13Test Completeness & Coverage🟡 1 MODERATE
16Naming & Conventions💬 1 NIT
17Documentation Accuracy💬 1 NIT

✅ 19/22 dimensions clean.

  • Test Completeness — the cleanupResult is not null branch (cleanup fails in the drop path) has no acceptance-test coverage; AssociatedUnitTestElement attribution is unvalidated
  • Naming — the Test_A_Run / Test_Z_Dropped alphabetical-ordering assumption is undocumented
  • Documentation — successful [ClassCleanup] output is silently discarded when filterResult is empty; no comment explains this

Overall assessment: The fix is algorithmically correct and well-reasoned. The _lastRunnableTestByClass sentinel is the right signal: it's populated only when both conditions are true — the class has an executable cleanup method and at least one non-filtered test ran in this worker — so a fully-dropped class is still correctly skipped. The ExecuteClassCleanupAsync semaphore and IsClassCleanupExecuted flag in TestClassInfo prevent any risk of double execution even under concurrent test scheduling. Thread safety is maintained via ConcurrentDictionary.TryGetValue. The ConfigureAwait(false) and try/finally disposal patterns mirror the surrounding code correctly. The three items above are polish suggestions; none blocks the merge.

Base automatically changed from dev/amauryleve/custom-test-filter to mainJune 29, 2026 14:34
…alized class is dropped
FinishFilteredOutTestAsync assumed a filtered-out test never loaded its type and therefore skipped class cleanup. That is false when a sibling test of the same class already ran in this worker: [ClassInitialize] executed and [ClassCleanup] is still owed. Because ClassCleanupManager counts down over the full (pre-filter) test set, isLastTestInClass can land on a dropped test, leaking the class cleanup whenever a class's last-in-order test is filtered out.
Fix: when the dropped test is the last in its class, run [ClassCleanup] if the class was initialized in this worker (detected via _lastRunnableTestByClass, which is only populated for classes that have an executable cleanup method and ran a non-filtered test). The TestClassInfo is already cached, so no new type is loaded. Adds an acceptance test reproducing the leak.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink
Amaury Levé (Evangelink)force-pushed the dev/amauryleve/classcleanup-filter-investigation branch from 7c1cdff to dc17bcdCompareJune 29, 2026 14:51
@Evangelink
Amaury Levé (Evangelink) merged commit 3a1375a into mainJun 29, 2026
14 of 16 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the dev/amauryleve/classcleanup-filter-investigation branch June 29, 2026 14:52
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

state/needs-reviewAwaiting review from the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@Evangelink@0101
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Fix [ClassCleanup] leak when ITestFilter drops the last test of an initialized class - #9503

Merged
Amaury Levé (Evangelink) merged 1 commit into
mainfrom
dev/amauryleve/classcleanup-filter-investigation
Jun 29, 2026
Merged

Fix [ClassCleanup] leak when ITestFilter drops the last test of an initialized class#9503
Amaury Levé (Evangelink) merged 1 commit into
mainfrom
dev/amauryleve/classcleanup-filter-investigation

Conversation

@Evangelink

Copy link
Copy Markdown
Member

Summary

Targets dev/amauryleve/custom-test-filter (#8896). Fixes a [ClassCleanup] leak in the new ITestFilter drop path, reported by an internal team using per-worker test sharding (each worker enumerates the full set per class, runs its subset, and Drops the rest).

The bug

UnitTestRunner.FinishFilteredOutTestAsync assumed a filtered-out test never loaded its type and therefore skipped class cleanup, only calling MarkClassComplete. That assumption is false when a sibling test of the same class already ran in this worker: [ClassInitialize] executed and [ClassCleanup] is still owed.

Because ClassCleanupManager counts down over the full (pre-filter) test set, isLastTestInClass is independent of run-vs-dropped. So whenever a class's last-in-order test is dropped — while earlier tests of that class ran — the worker leaks the class cleanup. ForceCleanup is not a safety net (it only fires on IsGracefulStopRequested).

The analogous assembly-cleanup asymmetry was already guarded by _assemblyInitializeWasExecuted, which is why only class cleanup leaked.

The fix

When the dropped test is the last in its class, run [ClassCleanup] if the class was initialized in this worker. Detection reuses the existing _lastRunnableTestByClass map, which is populated only for classes that both have an executable cleanup method and ran a non-filtered test. The TestClassInfo is already cached in the TypeCache, so resolving it loads no new type — preserving the feature's "a Drop pays zero type-load / init cost" guarantee. A fully-dropped class (type never loaded) still correctly skips both init and cleanup.

Validation

  • Added acceptance test TestFilterClassCleanupTests reproducing the leak: a filter drops the last-in-order test of an initialized class and the only test of a fully-dropped class.
    • Against the unfixed adapter the test fails — output shows ClassInitialize + the run test but no ClassCleanup, exactly the reported symptom.
    • Against the fixed adapter it passes; the fully-dropped class runs neither init nor cleanup.
  • MSTestAdapter.PlatformServices.UnitTests green across net462/net48/net8.0/net9.0/net8.0-windows.

Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com

CopilotAI review requested due to automatic review settings June 29, 2026 12:54
@Evangelink
Amaury Levé (Evangelink) changed the base branch from main to dev/amauryleve/custom-test-filterJune 29, 2026 12:55
@Evangelink
Amaury Levé (Evangelink) marked this pull request as ready for review June 29, 2026 12:55
@EvangelinkAmaury Levé (Evangelink) added the state/needs-review Awaiting review from the team. label Jun 29, 2026

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes a regression in the MSTest adapter’s new ITestFilter “drop/skip before type-load” path where [ClassCleanup] could be skipped when the last-in-order test of an already-initialized class was filtered out (common in per-worker sharding scenarios).

Changes:

  • Update UnitTestRunner.FinishFilteredOutTestAsync to execute [ClassCleanup] when the filtered-out test is the last test in the class and the class was previously initialized in the current worker.
  • Add an acceptance test that reproduces the leak (drop last-in-order test of a partially-run class) and verifies no over-correction for fully-dropped classes.
Show a summary per file
FileDescription
test/IntegrationTests/MSTest.Acceptance.IntegrationTests/TestFilterClassCleanupTests.csAdds an acceptance regression test covering the “last-in-order test dropped” class-cleanup leak and the “fully dropped class” non-init/non-cleanup guard.
src/Adapter/MSTestAdapter.PlatformServices/Execution/UnitTestRunner.csRuns class cleanup from the filter drop/skip tail path when the class was initialized earlier in the worker and the dropped/skipped test is the last test in its class.

Review details

  • Files reviewed: 2/2 changed files
  • Comments generated: 0
  • Review effort level: Low

@Evangelink

Copy link
Copy Markdown
MemberAuthor

🧪 Test quality grade — PR #9503

ΔTestGradeBandNotes
newTestFilterClassCleanupTests.
ClassCleanup_
RunsWhenLastTestOfInitializedClassIsDropped
A90–100Rich assertion set covers both the positive fix (ClassCleanup runs for the initialized class) and the guard against over-correction (ClassCleanup does not run for the fully-dropped class); clear AAA structure with informative comments.

This advisory comment was generated automatically. Grades are heuristic
and informational — they do not block merging. Re-run with
/grade-tests.

🤖 Automated content by GitHub Copilot. Posted via a maintainer's GitHub token, so it appears under their account — the account owner did not write or approve this content personally. Generated by the Grade Tests on PR (on open / sync) workflow. · 179.3 AIC · ⌖ 12.7 AIC · ⊞ 43.7K · [◷]( · )

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

🤖 Automated review by GitHub Copilot. Posted via a maintainer's GitHub token, so it appears under their account — the account owner did not write or approve this content personally. Generated by the Expert Code Review workflow. To request a follow-up action, reply by tagging @copilot directly.

#DimensionVerdict
13Test Completeness & Coverage🟡 1 MODERATE
16Naming & Conventions💬 1 NIT
17Documentation Accuracy💬 1 NIT

✅ 19/22 dimensions clean.

  • Test Completeness — the cleanupResult is not null branch (cleanup fails in the drop path) has no acceptance-test coverage; AssociatedUnitTestElement attribution is unvalidated
  • Naming — the Test_A_Run / Test_Z_Dropped alphabetical-ordering assumption is undocumented
  • Documentation — successful [ClassCleanup] output is silently discarded when filterResult is empty; no comment explains this

Overall assessment: The fix is algorithmically correct and well-reasoned. The _lastRunnableTestByClass sentinel is the right signal: it's populated only when both conditions are true — the class has an executable cleanup method and at least one non-filtered test ran in this worker — so a fully-dropped class is still correctly skipped. The ExecuteClassCleanupAsync semaphore and IsClassCleanupExecuted flag in TestClassInfo prevent any risk of double execution even under concurrent test scheduling. Thread safety is maintained via ConcurrentDictionary.TryGetValue. The ConfigureAwait(false) and try/finally disposal patterns mirror the surrounding code correctly. The three items above are polish suggestions; none blocks the merge.

Base automatically changed from dev/amauryleve/custom-test-filter to mainJune 29, 2026 14:34
…alized class is dropped
FinishFilteredOutTestAsync assumed a filtered-out test never loaded its type and therefore skipped class cleanup. That is false when a sibling test of the same class already ran in this worker: [ClassInitialize] executed and [ClassCleanup] is still owed. Because ClassCleanupManager counts down over the full (pre-filter) test set, isLastTestInClass can land on a dropped test, leaking the class cleanup whenever a class's last-in-order test is filtered out.
Fix: when the dropped test is the last in its class, run [ClassCleanup] if the class was initialized in this worker (detected via _lastRunnableTestByClass, which is only populated for classes that have an executable cleanup method and ran a non-filtered test). The TestClassInfo is already cached, so no new type is loaded. Adds an acceptance test reproducing the leak.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink
Amaury Levé (Evangelink)force-pushed the dev/amauryleve/classcleanup-filter-investigation branch from 7c1cdff to dc17bcdCompareJune 29, 2026 14:51
@Evangelink
Amaury Levé (Evangelink) merged commit 3a1375a into mainJun 29, 2026
14 of 16 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the dev/amauryleve/classcleanup-filter-investigation branch June 29, 2026 14:52
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

state/needs-reviewAwaiting review from the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@Evangelink@0101