[minor] Validate the ImGuiTableColumn layout before reading it, and add a test project - #260
Conversation
SaveColumnWidth reads column widths out of ImGui's native structs with manual pointer arithmetic, using a stride of sizeof(ImGuiTableColumn) plus a hardcoded 8 bytes to compensate for a Hexa.NET.ImGui binding bug. Two Debug.Assert calls guarded the assumptions behind that offset. Debug.Assert is compiled out of a Release build, which is the only configuration users run. If Hexa.NET.ImGui fixed the binding, or Dear ImGui reordered the struct, Release would have gone on adding 8 bytes to a stride that no longer needed it and read a float from the wrong address -- silently, with nothing to notice it. The asserts only ever protected developers. Replaces them with ProbeNativeImGuiTableColumnSize, which runs once from a static initializer and returns a nullable stride. Null disables column width persistence, which GetColumnWidth already handles by falling back to the saved or default width, so the app keeps working and only stops tracking new widths. The probe measures the bug itself rather than inferring it from a size threshold. It sums the bytes the eight narrowly-bound index fields actually occupy: eight means the binding is still narrow and the stride is sizeof + 8, sixteen means it has been fixed so sizeof is correct and the workaround can be removed, and anything else is refused rather than guessed at. A missing field, a moved WidthGiven, or a Marshal failure all disable the read. Measuring rather than thresholding also settles an inconsistency in the original code, which asserted sizeof < 112 on the grounds that native is "about 112 bytes" while simultaneously adding 8 to a sizeof of 108, implying 116. Verified against Hexa.NET.ImGui 2.2.9: sizeof is 108, all eight affected fields are one byte at contiguous offsets 86-93, and the probe returns 116 -- the same stride the previous code computed, so behaviour is unchanged today. The Marshal calls are wrapped because they previously only ran under Debug.Assert and so never executed in Release. Running them unconditionally from a static initializer must not be able to fail the type initializer. Removes the dead "issues/XXX" placeholder link, which read as though an upstream issue existed. None has been filed; #258 tracks that. Part of #258 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DTHNXgSNEHUSQ5KMLgivno
The solution had one project and no tests, so SonarCloud reported 0% coverage on new code and failed the quality gate on the preceding commit. That gate cannot be satisfied by any change while the repository has no test project, so this is the fix rather than a widening. Adds BuildMonitor.Test with 32 tests over the two pieces of provider-independent logic that most affect what users see and how hard the APIs get hit. DurationEstimatorTests covers the estimator that drives the Estimate and ETA columns and the adaptive polling interval in RunSync: the three-sample floor, the IQR outlier filter, exponential weighting toward recent runs, that failed, canceled, pending and ongoing runs are never sampled, branch-specific estimation and its fallback when a branch has too little history, determinism across repeated calls, and that the estimate stays inside its own sample range. ColumnStrideTests covers the ImGuiTableColumn layout decision. To make that testable, the decision is extracted from ProbeNativeImGuiTableColumnSize into ResolveNativeColumnStride, which takes three plain integers. The probe needs unsafe, reflection and a real Hexa.NET.ImGui type, none of which a test can vary; the decision is the part worth testing, and separating it makes the corrected-binding and unrecognised-layout branches reachable at all, since neither can be produced with the binding currently referenced. Verified by mutation, by substitution rather than deletion. Making the stride resolver subtract the adjustment instead of adding it fails 5 tests; lowering MinSamplesForEstimate from 3 to 1 fails 3; letting the estimator sample every completed run rather than only successes fails 2. Two mutations initially ran without effect and are worth recording. Replacing the adjustment with a bare sizeof left ImGuiTableColumnSizeDifference unused and failed the build before any test ran, so the substitution has to keep the constant referenced. The success filter appears twice in DurationEstimator, so a whole-file replace asserting a single occurrence did nothing; only the EstimateDuration copy is the one under test. Fixes #259 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DTHNXgSNEHUSQ5KMLgivno
|
|
SonarCloud is red and I've stopped pushing at it. Here's exactly what's blocking, one condition at a time. Everything else is green — build, CodeQL, GitHub Advanced Security, and (newly) tests actually running on both Windows and Ubuntu. Before this PR the Condition 1 — coverage: real progress, but short
The test project (32 tests) covers
I could squeeze the number up by moving the message selection into the pure function too, but that would be chasing the metric rather than improving anything, and it would not clear condition 2 regardless. Condition 2 — security rating: I cannot diagnose this from here
I have not been able to identify the finding, and I'd rather say so than guess. What I can say:
What I need: someone with dashboard access to open the analysis and say which rule fired and on which line. If it is the I am keeping this PR watched until it is green, merged or closed. Generated by Claude Code |




Part of #258 — the half that lives in this repository. Fixes #259. Does not file the upstream Hexa.NET.ImGui issue; #258 stays open to track that.
Part 1 — the layout guard (#258)
SaveColumnWidthreads column widths out of ImGui's native structs with manual pointer arithmetic, usingsizeof(ImGuiTableColumn)plus a hardcoded 8 bytes to compensate for a Hexa.NET.ImGui binding bug. TwoDebug.Assertcalls guarded the assumptions behind that offset.Debug.Assertis compiled out of a Release build — the only configuration users run. If the binding were fixed, or Dear ImGui reordered the struct, Release would have gone on adding 8 bytes to a stride that no longer needed it and read afloatfrom the wrong address. Silently. The asserts only ever protected developers.ProbeNativeImGuiTableColumnSizenow runs once from a static initializer and returns a nullable stride. Null disables column-width persistence — whichGetColumnWidthalready handles by falling back to the saved or default width, so the app keeps working and merely stops tracking new widths. It fails closed instead of reading a wrong address.It measures the bug rather than inferring it
A size threshold — which the old code used (
sizeof < 112) — can only ever infer the bug; it cannot distinguish a fixed binding from one whose struct grew for an unrelated reason. Instead the probe sums the bytes the eight narrowly-bound index fields actually occupy:sizeof + 8sizeof(and logs that the workaround can go)null— disabledA missing field, a moved
WidthGiven, or aMarshalfailure also disable the read.This settled an inconsistency in the original numbers
The old code asserted
sizeof < 112on the grounds that native is "about 112 bytes", while adding 8 to asizeofof 108 — implying 116. Those disagree, and nothing reconciled them. Measured against Hexa.NET.ImGui 2.2.9:All eight are one byte, contiguous, exactly as the original comment described — so the bug is real and
+8is right; the "~112" figure was the loose part. The probe returns 116, the same stride the previous code computed, so behaviour is unchanged today. Only the failure modes change.A new failure path I had to guard
The
Marshalcalls previously sat insideDebug.Assert, so they never executed in Release at all. Running them unconditionally introduces a path that did not exist before — from a static initializer, where an escaping exception becomes aTypeInitializationExceptionthat kills the class. They are wrapped incatch (ArgumentException)and degrade tonull.Part 2 — the test project (#259)
32 tests,
net10.0, MSTest.Sdk. Most of this application cannot be unit tested (the UI needs a live ImGui context, the providers need credentials and network), so the tests target the provider-independent logic that decides what users see and how hard the APIs get hit.DurationEstimatorTests— the estimator drives the Estimate and ETA columns and the adaptive polling interval inRunSync, so a mistake shows up both as wrong numbers on screen and as the wrong request rate against a rate-limited API. Covers the three-sample floor, the IQR outlier filter, exponential weighting toward recent runs, that failed/canceled/pending/ongoing runs are never sampled, branch-specific estimation and its fallback, determinism across repeated calls, and that the estimate stays inside its own sample range.ColumnStrideTests— the Part 1 decision. To make it testable, the decision is extracted out of the probe intoResolveNativeColumnStride(int, int, int). The probe needsunsafe, reflection and a real Hexa.NET.ImGui type, none of which a test can vary; the decision is the part worth testing, and separating it is what makes the corrected-binding and unrecognised-layout branches reachable at all — neither can be produced with the binding currently referenced.Verification
Builds clean in both Debug and Release (Release specifically, since that is where the old asserts vanished). 32/32 tests pass.
Mutation-checked, by substitution rather than deletion:
MinSamplesForEstimatelowered from 3 to 1Two mutations initially ran without effect, which is worth recording since it is exactly the trap the ktsu mutation guidance warns about:
sizeofleftImGuiTableColumnSizeDifferenceunused and failed the build before any test ran. The substitution has to keep the constant referenced.DurationEstimator, so a whole-file replace asserting a single occurrence silently did nothing and reported a green 32/32. Only theEstimateDurationcopy is the one under test.The tree was restored from backups and re-verified after each run.
About the SonarCloud failure
The gate failed on two conditions. I could not read the detailed findings —
sonarcloud.iois blocked by this sandbox's egress proxy — so:unsafe; the pointer arithmetic was already there and is required by the feature. What changed is that rewriting the surrounding lines re-attributes them as "new code", so a pre-existing hotspot is now measured against the new-code gate. ExtractingResolveNativeColumnStrideshrinks the amount of new code inside theunsaferegion, which may help. If it still fails, the finding needs a look on the SonarCloud dashboard — and if it is theunsafeblock, it is a pre-existing property of this feature, not something this PR introduced.Caveats
ResolveNativeColumnStrideexercises them directly. The runtime read path needs a live ImGui context, which this sandbox has no display for, so a manual check that column widths still persist is worthwhile before merging.5.0.0.0) andktsu.Sdk.Analyzers2.28.0 requires5.9.0.0, so CSC refuses it withCS9057. CI is the first place KTSU0001–0007 see this, and the new test project is the likeliest place for a KTSU0001 complaint..../Hexa.NET.ImGui/issues/XXXplaceholder, which read as though an upstream issue existed. None has been filed — Report the Hexa.NET.ImGui ImGuiTableColumn struct-layout bug upstream and track removing the workaround #258 tracks that, and I deliberately have not opened it, since that is outside this organization.