Skip to content

feat(quickfiler-test): add WinForms message-pump test seam, reduce QfcItemController exemption boundary 19 to 11 (#230) - #479

Merged
drmoisan merged 4 commits into
mainfrom
feature/winforms-message-pump-test-seam-230
Aug 8, 2026
Merged

feat(quickfiler-test): add WinForms message-pump test seam, reduce QfcItemController exemption boundary 19 to 11 (#230)#479
drmoisan merged 4 commits into
mainfrom
feature/winforms-message-pump-test-seam-230

Conversation

@drmoisan

@drmoisan drmoisan commented Aug 8, 2026

Copy link
Copy Markdown
Owner

Summary

  • Adds WinFormsPumpHost, a test-support seam that runs a real WinForms Application.Run() message pump on a dedicated STA background thread, so tests can deterministically await continuations posted through a WindowsFormsSynchronizationContext instead of hanging indefinitely.
  • Reduces the ratified QfcItemController coverage-exemption boundary from 19 sites to 11, de-exempting 8 of the 9 members named in issue Build a WinForms message-pump test seam (Application.Run() background thread) to unblock 9 QfcItemController orchestration members #230. Each [ExcludeFromCodeCoverage] attribute was removed in the same change as the test that covers it.
  • Adds optional seam parameters to the CreateAsync / CreateSequentialAsync static factories in QfcItemController, mirroring the primary constructor's existing pattern. This is additive and non-breaking; no existing call site changes.
  • Fixes a real test-isolation defect: two test classes that swap the process-wide static UtilitiesCS.UiThread.Dispatcher could deadlock under class-level parallelization.
  • Adds 21 tests (suite 6272 → 6293, all passing) and raises line coverage 85.6453% → 85.8333% while growing the coverage denominator by 172 previously-uninstrumented lines.
  • InitializeWebViewAsync remains exempt by design — see Backward Compatibility / Scope Notes.

Why

Issue #227 reduced QfcItemController's exemption boundary from 103 to 19 members. The 9 residual members were all blocked by one structural gap: their continuations are posted through a WindowsFormsSynchronizationContext, and no thread in an MSTest run pumps WinForms messages, so awaiting them hangs indefinitely.

The repository had already solved the equivalent WPF problem (StaDispatcherHost in UtilitiesCS.Test/Threading/WpfUiDispatcherTests.cs; StartRunningDispatcher() in QuickFiler.Test/Controllers/QfcItemController.TestSupport.cs), but the WinForms analogue was explicitly deferred out of #227 cycle 5 into this issue.

Until it existed, those members stayed exempt for an infrastructure reason rather than a genuine testability barrier — and every contributor touching WinForms-context async code faced the same hang hazard with no sanctioned tool.

What Changed

Core test infrastructure

  • QuickFiler.Test/TestSupport/WinFormsPumpHost.cs (new) — internal sealed class implementing IDisposable. Dedicated background thread, IsBackground = true, named, SetApartmentState(ApartmentState.STA) before Start(). The thread body explicitly installs a WindowsFormsSynchronizationContext, signals readiness, then enters Application.Run(new ApplicationContext()). Exposes SyncContext, ThreadId, two InvokeAsync overloads, two RunAsync overloads, StopAsync, and an idempotent Dispose.
  • QuickFiler.Test/TestSupport/WinFormsPumpHostTests.cs (new) — self-tests for the seam: thread-id assertions proving work executes on the pump thread, fault marshalling, readiness handshake, and deterministic shutdown.

Production change (additive)

  • QuickFiler/Controllers/QfcItemController.Initialization.cs — optional seam parameters on CreateAsync / CreateSequentialAsync; 7 [ExcludeFromCodeCoverage] removals. This file now carries zero exemption sites.
  • QuickFiler/Controllers/QfcItemController.ViewerSetup.cs — 1 [ExcludeFromCodeCoverage] removal.

Tests

  • QfcItemController.InitializationTests.Part2.cs (new) — shared pump fixture and the SemaphoreSlim(1,1) isolation gate.
  • QfcItemController.InitializationTests.Part3.cs (new) — 5 pump-driven tests.
  • QfcItemController.SeamFactoryTests.cs, QfcItemController.InitializationTests.cs, QfcItemController.ViewerSetupTests.cs — extended.
  • QuickFiler.Test/QuickFiler.Test.csproj<Compile Include> wiring for all 4 new files. This project is legacy-format, so an unlisted .cs file silently does not compile.

Docs and evidence

Feature folder docs/features/active/2026-08-07-winforms-message-pump-test-seam-230/: issue, spec, user story, atomic plan, research artifact, three review audits, and 25 evidence artifacts including baseline and final Cobertura coverage XMLs.

Architecture / How It Fits Together

ItemViewer captures SynchronizationContext.Current in its constructor and exposes it as UiSyncContext. WinForms auto-install replaces the ambient context during Control construction, so the captured context is a WindowsFormsSynchronizationContext whose posted continuations are drained only by a message loop running on the constructing thread.

WinFormsPumpHost supplies that loop. Tests construct the ItemViewer on the pump thread via host.InvokeAsync, so the context it captures belongs to a thread that is actively pumping. Continuations then drain normally and the awaiting test thread completes deterministically. No SynchronizationContext is ever mutated on the MSTest thread itself.

Coordination is entirely event-based: ManualResetEventSlim for readiness, TaskCompletionSource<T> with RunContinuationsAsynchronously for completion and shutdown. Shutdown posts Application.ExitThread, awaits the stopped signal, joins the thread, and asserts liveness.

Verification

Completed

Gate Result
CSharpier format + check EXIT 0, 9/9 clean
.NET analyzers EXIT 0 — 0 errors, 5 warnings
Nullable analysis (TreatWarningsAsErrors) EXIT 0 — 0 errors
Full suite + coverage EXIT 0 — 6293/6293 passing (6272 → 6293)
Line coverage 85.6453% → 85.8333% raw; 85.8223% denominator-adjusted
Branch coverage 79.0039% → 79.2226%
Changed executable lines 6/6 = 100%
De-exempted member aggregate 159/171 = 92.98%
Exemption census 19 → 11, target met
Compile-include wiring 0 unwired files; 21 static [TestMethod] names, 0 missing from /ListTests discovery
File-size cap All ≤ 500 lines (max 489)
Determinism audit 0 banned timing/temp-file APIs across all 9 changed .cs files; all 21 new tests carry [Timeout]

The coverage denominator grew by 172 lines from de-exemption, so the raw before/after comparison is not denominator-stable; both figures are reported above and in evidence/qa-gates/coverage-delta.2026-08-08T00-15.md.

The Phase 8 toolchain loop failed its first iteration and restarted, as the plan requires. See Risks and Mitigations.

Recommended

dotnet tool run csharpier check .
msbuild TaskMaster.sln /t:Build /p:Configuration=Debug /p:Platform="Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true
msbuild TaskMaster.sln /t:Build /p:Configuration=Debug /p:Platform="Any CPU" /p:Nullable=enable /p:TreatWarningsAsErrors=true
pwsh -File scripts/vscode/Invoke-MSTestWithCoverage.ps1

Backward Compatibility / Scope Notes

  • No breaking changes. The static-factory seam parameters are optional with defaults preserving current behavior.
  • InitializeWebViewAsync remains exempt — a documented non-goal, not an omission. It cannot be covered without the real WebView2 runtime: after the mocked IWebViewCoreInitializer calls, ((ItemViewer)_itemViewer).L0v2h2_WebView2.CoreWebView2 is null, and the unit-test policy bars external-dependency tests. Issue Build a WinForms message-pump test seam (Application.Run() background thread) to unblock 9 QfcItemController orchestration members #230 flags this member's residual barrier as tracked separately. Achievable outcome is therefore 8 of 9 members.
  • CreateAsync and InitializeAsync have a structurally unreachable terminal statement under unit-test conditions (the tail after InitializeWebViewAsync), so their per-member coverage is partial by construction rather than incomplete by neglect.
  • Census drift. The controller partials carried 19 attribute sites against an 18-member boundary ratified 2026-07-02. EnsureBreadcrumbPipeline (QfcItemController.ViewerSetup.cs) was added by Feature: quickfiler-breadcrumb-webview2 #351 after that ratification; it is documented as out of scope and is unchanged by this PR.

Maintainer action required: the reduced 19 → 11 exemption boundary requires re-ratification during review. spec.md defines this approval step.

Risks and Mitigations

Risk Mitigation
A test hangs instead of failing, converting a failure into a CI timeout All coordination is event-based with bounded waits; every new test carries [Timeout]; faults are marshalled from the pump thread to the awaiting test thread via TaskCompletionSource
Pump thread leaks across tests IsBackground = true; shutdown posts Application.ExitThread, awaits the stopped signal, joins, and asserts liveness; Dispose is idempotent
SynchronizationContext bleed into sibling tests The MSTest thread's context is never mutated — the ItemViewer is constructed on the pump thread via InvokeAsync
Static UiThread.Dispatcher swap race between parallel test classes This actually occurred and was fixed, not retried past. The Phase 8 loop failed iteration 1 with two [Timeout] expiries: one class's restore reverted the static to a never-pumped dispatcher while another was still awaiting a dispatcher operation. Fixed with a static SemaphoreSlim(1,1) gate held from fixture build through an idempotent restore
Unwired test file silently not compiling (legacy csproj) Proven by two wiring-sensitive checks: csproj enumeration against the git-derived added-file list, and vstest /ListTests discovery of every statically-enumerated new [TestMethod] name

Rollback: revert the two commits. The production change is additive, so reverting restores the prior exemption boundary and factory signatures without call-site changes.

Review Guide

Suggested order:

  1. QuickFiler.Test/TestSupport/WinFormsPumpHost.cs — the core seam; read the readiness handshake and shutdown paths first.
  2. QuickFiler.Test/TestSupport/WinFormsPumpHostTests.cs — confirms the seam behaves as claimed.
  3. QuickFiler/Controllers/QfcItemController.Initialization.cs — the additive factory seam plus 7 attribute removals.
  4. QfcItemController.InitializationTests.Part2.cs — the isolation gate (the subtlest part of the change).
  5. Remaining test files and the csproj wiring.
  6. evidence/qa-gates/coverage-delta.2026-08-08T00-15.md and exclusion-census-post.2026-08-07T23-30.md for the numbers.

Diff noise: two committed Cobertura XMLs (coverage-baseline.cobertura.xml, coverage-final.cobertura.xml) account for ~374,000 of the ~379,000 added lines. They are raw coverage evidence, consistent with the precedent set by #420 and #424. Everything else is ~5,000 lines.

Follow-ups

Out-of-scope findings recorded during execution, to be routed as separate issues:

  • UtilitiesCS nullable debt. A forced -t:Rebuild under /p:Nullable=enable /p:TreatWarningsAsErrors=true produces 195 errors, all in UtilitiesCS.csproj (0 in QuickFiler / QuickFiler.Test). The prescribed /t:Build gate passes only because the incremental up-to-date check does not recompile, so the repo-wide nullable gate is weaker than it appears. Recorded in evidence/baseline/baseline-nullable.2026-08-07T21-45.md.
  • CS2002 duplicate <Compile> entry for UtilitiesCS.Test/OutlookObjects/Folder/PercentageFormatterTests.cs — pre-existing, present at baseline and after.
  • QfcItemControllerTestSupport.EnsureUiThreadDispatcher mutates the process-wide UiThread._dispatcher without restoring it. This pre-existing pattern is what made the deadlock above possible; this PR's fixture restores, but the shared helper still does not.
  • WinFormsPumpHost promotion to a shared test-support project if UtilitiesCS.Test gains a consumer (spec Non-Goal 3).
  • QfcItemController.ViewerSetup.cs remains at 81.88% line / 62.96% branch, below the per-file floor but improved from 74.37% / 56.00% with zero changed-line regression. The residue is pre-existing Refactor: com-vsto-coverage-exemption #197 debt in untouched members; the reviewer dispositioned it non-blocking.

GitHub Auto-close

drmoisan and others added 4 commits August 7, 2026 22:50
- Add WinFormsPumpHost STA background-thread Application.Run() seam
  in QuickFiler.Test/TestSupport, analogous to StartRunningDispatcher
- Add optional seam parameters to QfcItemController CreateAsync and
  CreateSequentialAsync factories to make them injectable
- Remove ExcludeFromCodeCoverage from 8 previously unreachable
  QfcItemController orchestration members and add covering tests
- Fix static UiThread.Dispatcher swap race between test classes using
  a SemaphoreSlim gate with idempotent restore
- Add feature folder artifacts (issue, spec, user story, plan,
  research, and QA-gate evidence) for the change

Refs: #230

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y7hgSxkKfeXX3gn8jdmo4v
Policy audit, code review, and feature audit for the WinForms
message-pump test seam (19/19 AC PASS; GO recommendation with
maintainer boundary re-ratification). Includes feature-review
agent memory updates.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y7hgSxkKfeXX3gn8jdmo4v
…493, #494)

Record the three latent defects surfaced during issue #230 execution as
promoted potential-bug entries so they survive the feature-folder merge:

- #492 nullable gate reports EXIT 0 without compiling UtilitiesCS because
  the prescribed command uses /t:Build; a forced /t:Rebuild yields 195
  errors, all in UtilitiesCS.csproj
- #493 EnsureUiThreadDispatcher mutates the process-wide static
  UiThread._dispatcher without restoring it, which caused a real deadlock
  in #230 Phase 8 iteration 1
- #494 CLAUDE.md and .claude/rules disagree on coverage thresholds
  (80/90 vs 85/75) and on exclusion policy

Refs: #230

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y7hgSxkKfeXX3gn8jdmo4v
Record the maintainer's ratification of the QfcItemController
coverage-exemption boundary reduced from 19 sites to 11 by PR #479,
superseding the 19-member boundary ratified 2026-07-02 under #227.

Discloses two limits rather than smoothing them over:

- EnsureBreadcrumbPipeline was added by #351 after the 2026-07-02
  ratification, so the branch carried 19 sites against a historically
  ratified 18; the count of 11 is measured against the actual 19.
- CreateAsync and InitializeAsync have a structurally unreachable
  terminal statement under unit-test conditions, so their per-member
  coverage is partial by construction.

Refs: #230

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y7hgSxkKfeXX3gn8jdmo4v
@drmoisan
drmoisan merged commit 003c571 into main Aug 8, 2026
2 checks passed
@drmoisan
drmoisan deleted the feature/winforms-message-pump-test-seam-230 branch August 8, 2026 13:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Build a WinForms message-pump test seam (Application.Run() background thread) to unblock 9 QfcItemController orchestration members

1 participant