From f3f1e3d588ec71cc5c8e12d0c92d1d28e8faa927 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 21 Oct 2026 01:31:09 +0000 Subject: [PATCH] PeriodicBlockStepper.step: mid-batch launch failure test; async-error docstring note Review follow-up (non-blocking items): a launch failure on the second of three steps fails closed without the batch wait, keeps step_count at its entry value and blocks download; the docstring now states, as for FixedPatchStepper, that async faults surface at the single wait. Co-Authored-By: Claude Opus 5.5 --- src/CelerisLab/lbm/block_stepper.py | 7 +++++-- tests/unit/test_block_step_sync.py | 19 +++++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/CelerisLab/lbm/block_stepper.py b/src/CelerisLab/lbm/block_stepper.py index 8389bd8..4c32bad 100644 --- a/src/CelerisLab/lbm/block_stepper.py +++ b/src/CelerisLab/lbm/block_stepper.py @@ -207,8 +207,11 @@ class PeriodicBlockStepper: """Advance n steps: halo refresh, then core pull/collide, per step. All n steps are enqueued in order on the one stream and the host waits - once before returning (same launches and bits as n step(1) calls). On a - failure the instance is failed and step_count keeps its entry value. + once before returning (same launches and bits as n step(1) calls). + Asynchronous device errors surface at that single wait, so they are not + attributed to the step that caused them; call step(1) repeatedly to + localize one. On any failure the instance is failed and step_count keeps + its entry value. """ if isinstance(n, bool) or not isinstance(n, Integral) or n < 0: raise ValueError("n must be a nonnegative integer") diff --git a/tests/unit/test_block_step_sync.py b/tests/unit/test_block_step_sync.py index 5779a70..51b76ca 100644 --- a/tests/unit/test_block_step_sync.py +++ b/tests/unit/test_block_step_sync.py @@ -78,6 +78,25 @@ class BlockStepSync(unittest.TestCase): run.step(2) self.assertEqual((run.step_count, run._status), (1, "failed")) + def test_launch_failure_mid_batch_fails_closed_without_waiting(self): + run, log = backend() + calls = [] + step = run._step + + def failing(*args, **kwargs): + calls.append(1) + if len(calls) == 2: + raise RuntimeError("launch failure") + return step(*args, **kwargs) + + run._step = failing + with self.assertRaises(RuntimeError): + run.step(3) + self.assertNotIn("sync", [name for name, _ in log]) + self.assertEqual((run.step_count, run._status), (0, "failed")) + with self.assertRaises(RuntimeError): + run.download() + if __name__ == "__main__": unittest.main()