Repository navigation
fix(#412): one NaN reading no longer fails an SD import — dropped and reported, as the live path does (#294) - #413
Conversation
…ain with 'Cannot store NaN values' Adds a logger seam to the importer's internal constructor so a test can see which level an outcome was logged at. WIP: the guard follows. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… whole import, and says so Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
/agentic_review |
PR Summary by QodoSkip NaN readings during SD card imports
AI Description
Diagram
High-Level Assessment
Files changed (2)
|
Code Review by Qodo🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0)
Great, no issues found!Qodo reviewed your code and found no material issues that require reviewTip of the day💡 Did you know, you can describe a rule in plain language on the Rules page and Qodo drafts it for you |
|
/agentic_review |
|
Code review by qodo was updated up to the latest commit 789d566 |
|
Qodo-clean, CI green — ready for review. Two clean rounds on head |
|
7-way sweep —
|
|
Sweep: the combination now includes #415 — 8-way merge verified. Nothing merged, nothing pushed. The earlier sweep built #405 #406 #407 #408 #411 #413 #414 onto State re-derived
#415 on its own
8-way mergeTwo orders — octopus Full gate under one
Warnings improve, and nothing is addedBoth ends re-measured here with The one cross-PR hazard checked, and cleared#415 annotates production members nullable while #407/#408/#413 own the Test count reconciles1057 ( Not covered#363 (dependabot, Throwaway branches deleted. Nothing merged, nothing pushed. |
Sweep: #413 re-verified and built with the other five, onto today's
|
| check | result |
|---|---|
| ready-note SHA = head | ✅ note at 21:08:15Z names 789d566 = current head, no push since |
| CI green on that SHA | ✅ run 35468880064 "Build", head_sha 789d566, success |
| mergeable | ✅ MERGEABLE / CLEAN |
| qodo threads | ✅ 0 threads, 0 unresolved (author-leg control #196 returns 3, so the filter is live) |
| stale pipeline | ✅ newest .github/ change on main is 2026-09-08 |
| stale base |
⚠️ This PR's green predates all three merges
#405 merged 04:22:46Z, #406 04:25:27Z, #407 05:00:43Z (2026-09-20). This PR's newest Build run was created 2026-09-19T20:55:46Z — before all three. The badge is displayed against a main that no longer exists (strict: false), so the local build below is the only current evidence for this PR. Same is true of #408, #411, #414 and #415; only #416's run postdates the merges.
Note also that the ready note quotes 1065/1065, measured on the older base. On today's main the six-way tree runs 1116/1116, and this PR's own contribution re-derives from its diff as +8 (SdCardImportNaNTests, eight [Fact]).
Specific to this PR
#413 edits SdCardSessionImporter.cs, a production file inside the scope of the repo-wide BindingFacts source scanner that #405 tightened. That guard was run explicitly on the merged tree (--filter "FullyQualifiedName~Binding" → 175 passed, 0 failed). The new type SdCardImportNaNTests does not exist on main and is not introduced by any other PR in the set.
The 6-way merge
origin/main d8ef0f9 ← #408 + #411 + #413 + #414 + #415 + #416, built in two orders
(octopus, and sequential 416→415→414→413→411→408). Both land on the same tree
ebe16170827e2f2c2cdb745a2ec41885a8921f40. One auto-merge, SerialPortPollingDeviceWatcherTests.cs.
git diff c15f693 ebe1617 — against the tree the 8-way sweep verified — is one file, +650/−0: BootloaderHoldServiceTests.cs. So #405/#406/#407 merged as squashes into exactly the content that sweep predicted, and #416 is textually orthogonal to the other five.
Full gate on ebe1617 — judged by exit code, not by grepping for Build succeeded
| leg | result |
|---|---|
desktop --no-incremental |
exit 0, 155 warning(s) |
iOS restore (no -r) |
exit 0 |
iOS Release -r iossimulator-arm64 --no-restore --no-incremental |
exit 0, 156 warning(s) |
| HeadlessBench restore | exit 0 |
HeadlessBench Release --no-incremental |
exit 0, 156 warning(s) |
dotnet test |
1116 passed / 0 failed / 0 skipped |
Working tree clean, no packages.lock.json drift. The HeadlessBench leg demonstrably compiled — its output carries Program.cs(2060,13): warning CS0618, not a stale-assembly no-op.
Test reconciliation — each delta re-derived from that PR's own diff
main d8ef0f9 = 1057 (at 858d143) + 19 (#405/#406/#407 together) = 1076.
| #408 | #411 | #413 | #414 | #415 | #416 |
|---|---|---|---|---|---|
| +0 | +3 | +8 | +9 | +0 | +20 |
1076 + 0 + 3 + 8 + 9 + 0 + 20 = 1116 ✅
Hazards checked by compilation, not by argument
- chore(warning-zero): CS8625 ×6 — the members that are documented as null-when-empty are now declared nullable (8 lines, 4 files) #415's nullable annotations × the test-side PRs.
Daqifi.Avalonia.Tests.csprojhas<Nullable>enable</Nullable>+<TreatWarningsAsErrors>true</TreatWarningsAsErrors>, so anyCS8602/CS8604/CS8767cascade into test: the last two private IAppLogger fakes fold into RecordingAppLogger — one logger double in the suite (−44) #408's folded logger, test(#409): the flash probe's cancellation token and the no-document warning are now pinned through app code, not only Core #411's harnesses, fix(#412): one NaN reading no longer fails an SD import — dropped and reported, as the live path does (#294) #413's/fix(#410): Device Logs keeps the device you picked when another one connects or drops, and stops re-listing its SD card #414's/test(coverage-gap): pin BootloaderHoldService — the hold that decides whether a firmware flash can proceed #416's new tests would have failed the build. It compiled. Cleared. - test: the last two private IAppLogger fakes fold into RecordingAppLogger — one logger double in the suite (−44) #408 and fix(#410): Device Logs keeps the device you picked when another one connects or drops, and stops re-listing its SD card #414 both alter shared test doubles — different files, zero overlap (
RecordingAppLogger.csvsDevice/RecordingStreamingDevice.cs). The suite-wide consumer risk is what the compile + 1116 tests rule out. Cleared. - Duplicate top-level types (fix(notifications): the flyout names its empty state instead of rendering a blank panel #258 shape). test(coverage-gap): pin BootloaderHoldService — the hold that decides whether a firmware flash can proceed #416's new file declares exactly one top-level type;
FakeHidTransport/ConnectCallare nested. No name introduced by any of the six exists onmainor in another PR. Cleared. - test(#388): the dialog view-model guard requires the initializer to END at the construction — new VM(...).ToString() no longer passes #405's repo-wide guard.
BindingFacts.cs:270scansDaqifi.Avalonia/**/*.cs— production, so fix(#410): Device Logs keeps the device you picked when another one connects or drops, and stops re-listing its SD card #414's and chore(warning-zero): CS8625 ×6 — the members that are documented as null-when-empty are now declared nullable (8 lines, 4 files) #415's edits are in scope. Run explicitly on the merged tree:--filter "FullyQualifiedName~Binding"→ 175 passed, 0 failed, includingBindingFactsConstructionTestsand the dialog/DataContext guards. Cleared.
Not claimed
#363 (dependabot, UNKNOWN) was not built with any of these. Android not built here — CI covers it. No visual re-run and none needed: the merged diff is 15 files, all .cs; no .axaml and no baseline manifest. No bench run — none of the six is device-observable. main's own warning baseline was not re-measured, so no warning delta is claimed.
Full log: ~/.claude-daqifi/avalonia-loop/logs/20260920T053803Z-sweep-6way.md.
Nothing merged, nothing pushed.
Closes #412. Not merging — for review.
What was wrong
If an SD card log held even one
NaNreading, the whole import failed with Import Failed. Microsoft.Data.Sqlite refuses to bind a NaN (Cannot store 'NaN' values.), so the 1,000-sample batch holding it failed, along with everything the parser had not reached yet. A NaN in the first batch meant nothing was imported. A later NaN left anImportFailedfragment with the rest of the file dropped. Either way the failure was logged atError, which makes it a Sentry event. Core's parsers pass NaN through: the CSV parser acceptsNaN/nan, and the.binfloat leg takes the float as-is.The fix
SdCardSessionImporter.WriteSamplesAsyncnow applies the live path's rule from #294 (SessionSampleWriter.Add): it skips a NaN reading, counts it, and imports everything else.NaN only, not
IsFinite. I measured this instead of assuming it:Infinities_are_stored_not_droppedimports1e400and-Infinitythrough the real CSV parser and reads+∞/-∞back from the database. The database refuses only NaN, and fix(logging): one unstorable reading threw away the rest of the session — #247's characterization, replaced #294's guard is NaN-only for the same reason. The issue suggestedIsFinite, following fix(#295): one bad reading made the Summary Average read NaN for the rest of the session — refuse it where it enters the app #298, but fix(#295): one bad reading made the Summary Average read NaN for the rest of the session — refuse it where it enters the app #298 guardsActiveSample(the running statistics), not storage. So an imported session can still contain±∞. That is unchanged, and it is a deliberate choice, not an oversight.The user is told.
OutcomeGuidancegains the sentence "Some readings in the log were NaN (they had no numeric value) and were left out; every other reading was imported." All three import surfaces already showOutcomeGuidance:DaqifiViewModelimport-from-disk,:1523DeviceLogsViewModel.ImportFile,:562RecordNotice→BuildImportAllSummary,:670Like the other guidance sentences it names no count and no file, so Import All shows it once however many files are affected.
When every reading is NaN, the dialog no longer says "The log contained no samples". It says every reading was NaN, so nothing could be stored and no session was created.
Logging: one
Warningper import with the count. There is noError, so no Sentry event, and no line per sample.No shared helper with fix(logging): one unstorable reading threw away the rest of the session — #247's characterization, replaced #294. The duplicated part is
double.IsNaN(value). The reporting has a different shape on each side: fix(logging): one unstorable reading threw away the rest of the session — #247's characterization, replaced #294 is rate-limited per stream on a long-running writer, while this is one summary for a finite import. A helper would add a layer without removing any code.Test seam: the existing internal constructor takes an optional
IAppLogger(defaultAppLogger.Instance), so a test can check which level an outcome was logged at. The production constructor is unchanged.Diffstat: +61/−12 in
SdCardSessionImporter.cs, plus a new 257-line test file.Verification
8 new tests in
SdCardImportNaNTests. I ran them against the unchanged code, with only the logger seam added. 6 of 8 fail, all withInvalidOperationException: Cannot store 'NaN' values.:nanthroughImportFromFileAsyncThe other 2 pass on
main, as they should: a clean log produces no guidance, and infinities store. They pin behaviour that must stay the same.Mutation control: I changed the NaN summary from
WarningtoError. That killed 3 tests, all on theAssert.Empty(_log.Errors)line, so the no-Sentry assertion does catch a regression.Build gate green locally under a
build.Nlock: desktop, iOS restore, iOS Releaseiossimulator-arm64. Full suite 1065/1065 passed. Lock files unchanged. Android is left to CI.No board attached. This change can't be observed on the device anyway: the import path runs from a file, and the tests go through the real parser.
Warnings, taken from CI's own logs at both ends (
Library + Desktop headjob): merge-base858d143166, this head789d566166. The importer has the same 3 pre-existing warnings on both sides (CS8625 plus two CS8601), each shifted +48 lines. The new test file adds none. Tests went from 1057 to 1065 (+8).🤖 Generated with Claude Code