…anded in the transfer
The bridge set *outCancelled only at its own explicit UserBreak() checkpoints, so which error a
cancelled import reported depended on which phase the cancellation happened to land in. A break
during the transfer leaves TransferRoots reporting zero roots, and that exit returned "failed"
with the flag still false: ImportError.importFailed, for a readable file the caller had itself
stopped. Measured on the #300 fixture -- cancel-on-first-poll and cancel-at-0.25 both returned
.importFailed, cancel-at-0.60 returned .cancelled.
Every failure exit below the indicator's construction now reports cancellation: the zero-roots
exit, a null shape, a non-Done status, and the catch (...) handler (which needed the indicator
hoisted out of the try). Applied across all twelve *Progress entry points, not only the two
robust importers, since they share the shape. OCCTBridge.h has documented exactly this contract
since v0.168.0, so this is the implementation catching up to it, not an API change.
A second defect surfaced while probing the first: UserBreak() re-asked the caller at every
checkpoint and believed the latest answer, so a caller that answers true once -- a one-shot flag,
an already-consumed Task.isCancelled -- had that answer overwritten. OCCT aborted the phase, the
next poll said "no break", and the half-repaired shape came back as a success (measured: 20,197
polls and a returned shape, against 20,436 uncancelled). The break is now latched, atomic because
OCCT documents UserBreak() as callable concurrently.
Both #300 regression tests are rewritten off the clock. That the repair phase lies inside the
caller's range is checked by the silence that would follow the last progress report if it did not:
1.3% (STEP) and 3.4% (IGES) of the call with the fix, 35-40% with the #300 defect reintroduced.
That a cancellation there stops the repair is checked against the uncancelled run's poll count, a
count of work items rather than a duration. Progress names cannot substitute for the phase: both
readers run a ShapeFix_Shape of their own during the transfer, so "Fixing face"/"Fixing edge"/
"Update tolerances" are already reported from fraction ~0.09.
Each new test was verified to fail against the defect it covers, re-injected one at a time. The
previously flaky suites ran 12/12 clean; full swift test 4631 tests pass.
Bridge-only: no kernel patch, no OCCT.xcframework rebuild. OCCTBridge.xcframework needs a
republish at release time since OCCTBridge_IO.mm changed.
Closes #525
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
What
A cancelled import could report
ImportError.importFailedinstead ofImportError.cancelled,depending on which phase the cancellation landed in. Filed from a 1-in-9 flake in #300's own
regression test; reachable by any caller whose deadline expires early.
Root cause
OCCTBridge_IO.mmset*outCancelledonly at its own explicitUserBreak()checkpoints. Thoseare not the only way out of these functions:
TransferRoots(...) == 0.importFailedDonestatus.importFailedcatch (...).importFailedMeasured on the #300 fixture: cancel-on-first-poll and cancel-at-fraction-0.25 both returned
.importFailed; cancel-at-0.60 (inside the repair half) returned.cancelled. The flaky testderived its deadline from a wall-clock measurement of a previous import, so machine load decided
which of those it hit.
The header has documented the intended contract since v0.168.0 — "the
*Progressentry pointsreturn NULL and set
*outCancelled=true; if the import otherwise fails,*outCancelledstaysfalse" — so this is the implementation catching up to it, not an API change.
Second defect, found while probing the first
BridgeProgressIndicator::UserBreak()re-asked the caller at every checkpoint and believed thelatest answer. A caller that answers
trueonce — a one-shot flag, an already-consumedTask.isCancelled— had that answer overwritten: OCCT aborted the phase, the next poll said "nobreak", and the partially-repaired shape came back as a success. Measured: 20,197 polls and a
returned shape, against 20,436 for an uncancelled run.
ImportProgress.shouldCanceldocuments the opposite ("the loader throwsImportError.cancelledon the next boundary after this returns
true"), so the break is now latched —std::atomic<bool>,since OCCT documents
UserBreak()as callable concurrently.Fix
UserBreak()latches the firsttrue;Cancelled()reads the latch without re-polling.including the
catch (...)handlers, which needed the indicator hoisted out of thetry.*Progressentry points, not only the two robust importers: same shape,same defect.
Bridge-only: no kernel patch, no
OCCT.xcframeworkrebuild.OCCTBridge.xcframeworkneeds arepublish at release time since
OCCTBridge_IO.mmchanged.Tests
Both #300 regression tests are rewritten off the clock:
progress report if it were not. Measured 1.3% (STEP) / 3.4% (IGES) of the call with the fix,
35–40% with the Sweep: ShapeFix/Sewing run outside the caller's progress range in the Robust *Progress import paths (#286 follow-up) #300 defect reintroduced. A ratio taken within one call, so a slow machine
stretches both halves of it.
count of work items rather than a duration (measured identical across runs: 20,436 STEP /
38,817 IGES).
Step names cannot stand in for the phase, tempting as they look: both readers run a
ShapeFix_Shapeof their own during the transfer, soFixing face/Fixing edge/Update tolerancesare already being reported from fraction ~0.09.New suite
CancellationReportingTests(#525): transfer-phase cancellation onloadRobust/loadIGESRobust/loadSTEP, and the one-shot canceller.Each new test was verified to fail against the defect it covers, re-injected one at a time:
.importFailedmessage from the issueUserBreak()stops latchingPreviously flaky suites: 12/12 clean. Full
swift test: see below.Closes #525
🤖 Generated with Claude Code