test(device): bound the two unbounded awaits in the live-stream device tests - #453
Conversation
…e tests Two tests in DaqifiStreamingDeviceLiveStreamTests awaited without a bound, so a regression in the code they cover would hang CI indefinitely rather than fail it: - Cancellation_EndsEnumeration_ButNotDeviceStream awaited the pending MoveNextAsync directly. If cancellation ever stopped ending the read, that await parks forever. - InvalidBufferCapacity_Throws ran an `await foreach` inside ThrowsAsync. If the capacity validation stopped throwing, the enumeration blocks on an empty buffer that nothing ever writes to. The repo has no global xUnit timeout and no [Fact(Timeout=...)] usage, so nothing else bounded them. Applies the pattern already established for the extracted collaborator in LiveSampleStreamTests (#440): one named MoveNextTimeout field, `.AsTask() .WaitAsync(...)` on the cancellation await, and the `await foreach` lifted into a local ConsumeAsync() that WaitAsync can bound. The five inline TimeSpan.FromSeconds(5) literals already in this file collapse into the same field. The cancellation test now disposes its enumerator explicitly instead of via `await using`. Under a real regression the read stays parked, and disposing an async iterator whose MoveNextAsync is still in flight throws NotSupportedException from the finally — masking the TimeoutException that actually names the problem. Verified by simulating each regression in turn (dropping the Cancel() call, and passing a valid bufferCapacity): each test fails in ~5s with TimeoutException instead of hanging. Restored, and the full suite passes in Release on net9.0 and net10.0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
PR Summary by QodoBound unbounded awaits in live-stream device tests to prevent CI hangs
AI Description
Diagram
High-Level Assessment
Files changed (1)
|
Code Review by Qodo
1.
|
DiscoverAsync_HungPort_TimesOutAndStillReturnsHealthyDevices failed on #453 with Assert.Single() finding an empty collection — on net9.0 while net10.0 passed the same test in the same run. #453 touches the live-stream device tests and nothing in discovery, so this is a pre-existing flake, not a regression. Every probe, healthy ones included, is dispatched through Task.Run and so must be handed a thread-pool thread before it can complete. That handoff races Task.Delay(PortProbeHardTimeoutMs), and SerialDeviceFinder abandons a probe still waiting for a thread when the ceiling expires — which is exactly an empty result for COM_OK. Under a saturated pool, thread injection is throttled to roughly one new thread per second, so a 300ms ceiling can expire before the delegate starts. The file already half-knew this: the cross-sweep tests were relaxed to hungProbeCalls <= 1 because "under thread-pool contention the hung probe may not have STARTED", but the healthy-device assertions were left racing the same clock. Raise the ceiling to 2000ms on the four sites that assert a healthy device IS reported, and document the constraint on CreateFinderWithProbes so the next test picks a value deliberately. DiscoverAsync_QuarantineTtl_AllowsPeriodicRetry keeps its 100ms: it only counts probes of a wedged port and depends on the short window. Also tighten the readiness budgets from the previous commit, 60s/30s to 15s/10s, per review: those also bound how long a real JumpingToApp regression takes to surface, and 10s is already an order of magnitude past the stall that broke the 1s budget. Not reproduced locally — a 12-core dev box does not starve the pool the way a 2-core runner running 2700 parallel tests does. The diagnosis rests on the failure signature, the abandon-on-timeout path, and the net9.0/net10.0 split. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…on failure Addresses Qodo's "cleanup skipped on timeout" finding. No behavior change: the unsubscribe is unreachable on the timeout path by construction, so the comment documents the deliberate choice rather than papering over it with try/finally that cannot actually clean up. Also corrects the previous comment's mechanism. DisposeAsync throws NotSupportedException from its own guard *before* the iterator body's finally runs — it does not throw from the finally, and it does not unsubscribe. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
/agentic_review |
|
Code review by qodo was updated up to the latest commit 938a3c9 |
What
Two tests in
DaqifiStreamingDeviceLiveStreamTestsawaited without a bound, so a regression in the code they cover would hang CI indefinitely rather than fail it:StreamSamplesAsync_Cancellation_EndsEnumeration_ButNotDeviceStreamawait Assert.ThrowsAnyAsync<OperationCanceledException>(async () => await moveNext);StreamSamplesAsync_InvalidBufferCapacity_Throwsawait foreach (...bufferCapacity: 0)insideThrowsAsyncThe repo has no global xUnit timeout and no
[Fact(Timeout=...)]usage, so nothing else bounded them.How
Applies the pattern already established for the extracted collaborator in
LiveSampleStreamTests(#440):MoveNextTimeoutfield, documented as to why it exists. The five inlineTimeSpan.FromSeconds(5)literals already in this file collapse into it, so the file has one bound rather than a field plus scattered literals..AsTask().WaitAsync(MoveNextTimeout)on the cancellation await.await foreachlifted into a localConsumeAsync()thatWaitAsynccan bound.One deviation worth calling out: the cancellation test now disposes its enumerator explicitly rather than via
await using. Under a real regression the read stays parked, and disposing an async iterator whoseMoveNextAsyncis still in flight throwsNotSupportedExceptionfrom thefinally— which replaces and masks theTimeoutExceptionthat actually names the problem. Verified empirically; withawait usingthe regression reportedNotSupportedException, and with the explicit dispose it reportsTimeoutException. This matches how the equivalent test inLiveSampleStreamTestsis already structured.Verification
Each regression simulated in turn, then restored:
cts.Cancel()callAssert.ThrowsAny() Failure ... Actual: typeof(System.TimeoutException)— failed in 5sbufferCapacity: 4Assert.Throws() Failure ... Actual: typeof(System.TimeoutException)— failed in 5sBoth previously would have hung. Restored to
bufferCapacity: 0and theCancel()call; the class passes 6/6 on both TFMs.Release build: 0 warnings, 0 errors. Full suite in Release on net9.0 and net10.0: 2587 passed, 0 failed, 2 skipped each, plus
Daqifi.Mcp.Tests23/23.Heads-up:
mainis currently red, unrelated to this PRFive tests in
FirmwareUpdateServiceTestsfail on a clean checkout oforigin/main(verified by stashing this change — same five fail without it):UpdateWifiModuleAsync_WhenFlashToolFails_TakesDeviceBackOutOfLanUpdateModeUpdateWifiModuleAsync_WhenCanceledAfterEnteringUpdateMode_StillTakesDeviceBackOutUpdateWifiModuleAsync_WhenCanceledDuringTransparentModeExitSettle_LeavesLanRestoreUnsentUpdateWifiModuleAsync_WhenRecoveryBudgetExpiresWaitingForReconnect_SendsNoBridgeExitUpdateWifiModuleAsync_WhenRecoveryBudgetExpiresAfterReconnect_StillFinishesTheBridgeExitSemantic merge race between two PRs that were each green on their own base: #445 added these tests with the pre-power-up sequences, then #444 made the prep sequence prepend
SYSTem:POWer:STATe 1. Out of scope here — tracked separately.The full-suite numbers quoted above therefore exclude
FirmwareUpdateServiceTests; everything else is green on both TFMs.🤖 Generated with Claude Code