diff --git a/docs/moving_bodies.md b/docs/moving_bodies.md index 2b0f069..8930a53 100644 --- a/docs/moving_bodies.md +++ b/docs/moving_bodies.md @@ -374,9 +374,13 @@ recorded as not runnable rather than replaced by another test. `MovingCircleLinks` (`u_w = u + omega x r` at the hit point) → static curved → `OneStep` → swap → force region → sensors → synchronize → read `no_donor` → commit (`applied := pending.offset`) → upload the hold record - (`c_old = c_new = applied`, `u = 0`). Hold steps launch no M1 and are the - S2a path. Any failure from the upload on marks the circles and the - Simulation failed; nothing is committed. + (`c_old = c_new = applied`, `u = 0`) → synchronize. Hold steps launch no M1 + and are the S2a path. Commit point: the advancing step-group sync plus a + zero `no_donor` count; a failure before it commits nothing. The hold-record + upload stages the next step after the commit: if it (or its sync) fails, + `applied` already matches the moved device geometry, but the Simulation is + still marked failed and refuses reuse. Any failure from the first upload on + marks the circles and the Simulation failed. - M1 classifies with the old/new predicates only (no flag reads). Covered cells add raw `sum c G` / `sum G` of `G^{n-1}` to `cover_*` and become `SOLID|OBSTACLE|BC_CURVED`. Uncovered cells: donors are fluid in both diff --git a/tests/unit/test_moving_bodies.py b/tests/unit/test_moving_bodies.py index 9f7a31d..5130928 100644 --- a/tests/unit/test_moving_bodies.py +++ b/tests/unit/test_moving_bodies.py @@ -254,7 +254,8 @@ class Tables: def upload_records(self, circles, stream): records = [m.record() for m, _ in circles] self.log.append(("upload", records)) - if self.fail == "upload": + uploads = sum(1 for w in self.log if isinstance(w, tuple) and w[0] == "upload") + if self.fail == "upload" or (self.fail == "hold_upload" and uploads == 2): raise RuntimeError("upload failed") return any(r[0] != r[1] for r in records) @@ -294,15 +295,18 @@ def test_run_one_advances_and_commits_after_sync(monkeypatch): assert ("step", 3, False) in writes and motion.applied == (0., .25) -@pytest.mark.parametrize("failure", ("upload", "no_donor")) -def test_failed_advance_commits_nothing_and_fails_simulation(monkeypatch, failure): - kwargs = dict(fail="upload") if failure == "upload" else dict(no_donor=1.) +@pytest.mark.parametrize("failure", ("upload", "no_donor", "hold_upload")) +def test_failed_advance_fails_simulation(monkeypatch, failure): + kwargs = dict(no_donor=1.) if failure == "no_donor" else dict(fail=failure) sim, writes, motion = advancing_simulation(monkeypatch, **kwargs) motion._on_failure = sim._fail_moving motion.set_target((0., .25), (0., .05)) with pytest.raises(RuntimeError): sim.run(1, stream=STREAM) - assert motion.applied == (0., 0.) and motion.failed and sim._failed is not None + # Before the step-group sync nothing is committed; a failed hold-record upload + # happens after the commit (applied already matches the moved device geometry). + assert motion.applied == ((0., .25) if failure == "hold_upload" else (0., 0.)) + assert motion.failed and sim._failed is not None with pytest.raises(RuntimeError, match="Simulation failed"): sim.run(1, stream=STREAM)