Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions tests/cli/test_serve_terminal.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@

pytestmark = [needs_pty, pytest.mark.own_child_cache]
HANG = 600
#: How long, once the child has exited, the reader has to take what it wrote and see the terminal report its end: a guard
#: against a descriptor that something else holds open, not a bound on the reading (which ends with the terminal's end).
END_WITHIN = 30.0


@pytest.fixture(scope="module")
Expand Down Expand Up @@ -77,7 +80,6 @@ def __init__(self, folder: Path, tmp: Path, *, terminals: tuple[str, ...], colum
env=child_environment(home, **(env or {})), cwd=tmp, close_fds=True, start_new_session=True)
os.close(slave)
self._open = True
self._last = time.monotonic()
self._reader = threading.Thread(target=self._read, daemon=True)
self._reader.start()
self.piped: tuple[str, str] | None = None
Expand Down Expand Up @@ -107,7 +109,6 @@ def _read(self) -> None:
return
with self._lock:
self._held += more
self._last = time.monotonic()

@property
def tty(self) -> str:
Expand All @@ -133,8 +134,11 @@ def until(self, wanted, what: str) -> str:
def finish(self, limit: float | None = None) -> tuple[str, str, int]:
"""Wait for the exit; what the pipes received (empty for a terminal) and the exit code."""
out, err = self.process.communicate(timeout=HANG if limit is None else limit)
while time.monotonic() - self._last < 0.5: # what the child wrote last is still on its way through the terminal
time.sleep(0.05)
# What the child wrote last is in the terminal, not yet in ``tty``, and a quiet spell does not say it has arrived (a
# reader that a loaded machine held up for longer than the spell lost the end: "the live region is gone"). The
# event is the terminal's own end, which the reader returns on once it has taken everything before it. A reader
# that was stopped on purpose (``stop_reading``) is already over, and this returns at once.
self._reader.join(timeout=END_WITHIN)
self._open = False
self._reader.join(timeout=2)
return (out or b"").decode("utf-8"), (err or b"").decode("utf-8"), self.process.returncode
Expand Down
3 changes: 2 additions & 1 deletion tests/duration_allowlist.txt
Original file line number Diff line number Diff line change
Expand Up @@ -608,4 +608,5 @@ tests/docs/test_build_wiring.py::test_a_path_of_another_machine_in_a_page_stops_
tests/docs/test_build_wiring.py::test_the_passing_arm_a_relative_path_in_a_page_builds_and_checks # kept: the build-wiring check, which runs the real mime docs build and mime docs check over the checkout in a fresh process after planting an entry (it imports JAX, builds the pages and collects the test suite for the evidence stage), so that a guard dropped from its caller fails in the command and not only as a function; it took 15.7 to 16.7 s alone and 27 to 32 s in its shard on the maddening-main lane's runners (runs 41631822884 and 41635496576), which with the shard's other slow tests put six over 20 s, past the budget report's limit of five for forgiving a stall by re-running it
tests/viz/test_recording_time_base.py::test_a_live_sessions_codes_over_the_rate_are_seconds_from_where_it_began_recording[4] # kept: a live session's codes over the rate count seconds from where it began recording, here for the fourth case; tracing-bound, not a cache: 21.4 s with 57 % in tracing and lowering (a program rebuilt per case) in default 3.12 shard 5 of run 41731349577; this file's tests also ran 20.4 and 20.5 s in openmaterica-main shard 5 of run 41674916914. To make faster: one traced session shared by the cases.
tests/saved/test_reason_sites.py::test_a_condition_s_reason_is_saved_and_judged_after_a_load # kept: a condition's reason is saved and judged after a load, through a real build, save and load; 20.1 s (61 % running) in default 3.12 shard 5 of run 41731349577, and up to 25.8 s on cold shards in the durations analysis of 2026-11-03 (16 s warm). To make faster: a module fixture so the first compile is paid once.
tests/saved/test_duration_saved.py::test_the_control_a_folder_with_no_duration_still_needs_one # kept: the control that a saved folder declaring no run length is refused by `mime run` and runs once it is given one, without which the run-length tests could not fail: one save and two in-process `mime run`s, each loading and building the settling ball; under 10 s warm, and over the 20 s line on a cold compilation cache, where it fails v0.3's own push run at f7ab1c3 too
tests/saved/test_switch_harness.py::test_control_a_guard_no_point_reaches_false_or_a_nonzero_norm_at_zero_fails # kept: the control of the coverage check on the guards: it reaches every guard point of the script's graph in the test-only setting (each point traced afresh, with the module fixture's build of the graph in its setup) and then plants an unreached guard and a point at zero, which the check must name; without it the check that every guard is reached is not evidence. 1.8 min (50 % running) in default 3.14 shard 10 of v0.3 push run 41767802990, 1.7 min in run 41762639938, 1.6 min in run 41774881679; alone on a laptop with CI's cache settings it takes 68.8 s cold (31.1 s compiling, 16.0 s tracing, 3.1 s lowering) and 38.4 s warm (2.5 s compiling, 15.6 s tracing, 2.7 s lowering, the remaining 17 s its own running), so the time is tracing and its own work with a compile on top when cold, and a warm cache removes only the compile. To make faster (dev2): it reaches every guard point again, the same list that test_every_guard_the_scan_lists_is_reached_false_and_every_nonzero_norm_away_from_zero (also listed) reaches just before it; one module fixture that reaches them once would serve both and halve the two
tests/saved/test_duration_saved.py::test_the_control_a_folder_with_no_duration_still_needs_one # kept: the control of the saved run length: a folder saved with no duration is refused by mime run for it, and the same folder runs once given one, so that the two runs are the evidence that the length is what the folder declared; each of the two is a real build, compile and run of the settling ball. 28.8 s (88 % running) in default 3.12 shard 9 of v0.3 push run 41767802990; alone on a laptop it takes 11.0 s cold and 8.9 s warm (0.4 s compiling, 0.6 s tracing warm), with 77 to 85 CPU seconds in those 9 to 11 s of wall time, so the time is the runs' own multi-threaded compute (on a 4-core runner 77 CPU seconds alone are 19 s of wall time), which no cache removes
42 changes: 35 additions & 7 deletions tests/oracles/test_dynamic_arm.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@

import multibody_cases as cases
from mime.core import quaternion
from mime.core.multibody import gravity_forces, inverse_dynamics, mass_matrix, read_urdf
from mime.core.multibody import forward_dynamics, gravity_forces, inverse_dynamics, mass_matrix, read_urdf
from mime.core.multibody._kinematics import body_poses, constants
from mime.nodes.actuation import DynamicArmNode

Expand Down Expand Up @@ -171,10 +171,12 @@ def gives_energy(self, p, q, free, inverse, dt):


@st.composite
def runs(draw, name):
def runs(draw, name, at_a_stop: bool | None = None):
"""A start within the middle half of each joint's range, a speed of at least a tenth of ``SPEED`` on each joint,
so that the arm moves and gravity works on it, and torques of the joint's own scale; on half the draws, one joint
starts near a stop, turning into it and pushed into it, 10 to 50 % of the run's travel at its speed away."""
so that the arm moves and gravity works on it, and torques of the joint's own scale. On half the draws one joint
starts near a stop, turning into it and pushed into it, 10 to 50 % of the run's travel at its speed away.
``at_a_stop`` fixes that half: false for none, true for every draw, and then built so that the run reaches the stop
(see :func:`into_a_stop`) and the test that needs a stop to be reached throws no draw away."""
tree = TREES[name]
a = tree.articulation
lower, upper = np.maximum(a.lower, -math.pi), np.minimum(a.upper, math.pi)
Expand All @@ -185,14 +187,39 @@ def runs(draw, name):
speed = draw(unit)
qd0 = SPEED * np.sign(speed + 0.5) * (0.1 + 0.9 * np.abs(speed))
tau = ACCELERATION * inertia * draw(unit)
if draw(st.booleans()):
if at_a_stop:
joint = draw(st.integers(0, a.dofs - 1))
side = draw(st.sampled_from([-1.0, 1.0]))
into_a_stop(tree, q0, qd0, tau, joint, side, draw(st.floats(0.1, 0.5)), lower, upper)
elif at_a_stop is None and draw(st.booleans()):
joint = draw(st.integers(0, a.dofs - 1))
side = draw(st.sampled_from([-1.0, 1.0]))
q0[joint] = (upper if side > 0 else lower)[joint] - side * abs(qd0[joint]) * HORIZON * draw(st.floats(0.1, 0.5))
qd0[joint], tau[joint] = side * abs(qd0[joint]), side * abs(tau[joint])
return name, q0, qd0, tau


def into_a_stop(tree, q0, qd0, tau, joint: int, side: float, fraction: float, lower, upper) -> None:
"""Turn ``joint`` toward the stop on ``side`` and push it there so that the run reaches it, in place: the speed along
``side``, the torque raised until the joint's own acceleration at the start (``M^-1 (tau - damping qd - h)``, with the
weight and the others' coupling in ``h``, taken at the stop) is at least ``ACCELERATION`` toward the stop, and the
joint put ``fraction`` of the run's travel from it. A torque that only has the sign of the stop can be outweighed by
the weight or the coupling, and then the joint is carried away from it (a third to a half of such draws reached no
stop at all, so the test that needs one threw them away)."""
a = tree.articulation
stop = float((upper if side > 0 else lower)[joint])
damping = np.asarray([tree.joint(n).damping for n in a.dof_names])
q0[joint], qd0[joint] = stop, side * abs(qd0[joint])
at_stop = jnp.asarray(q0)
qdd = np.asarray(forward_dynamics(a, at_stop, jnp.asarray(qd0), jnp.asarray(tau - damping * qd0), GRAVITY))
towards = side * qdd[joint]
if towards < ACCELERATION:
compliance = float(np.linalg.inv(np.asarray(mass_matrix(a, at_stop)))[joint, joint])
tau[joint] += side * (ACCELERATION - towards) / compliance
towards = ACCELERATION
q0[joint] = stop - side * (abs(qd0[joint]) * HORIZON + 0.5 * towards * HORIZON ** 2) * fraction


class Found(NamedTuple):
"""What a run shows at the three step sizes: each step size's summed residual of the steps that do not end on a
stop, the order each halving shows, whether a stop was reached, at the finest step size gravity's summed work, the
Expand Down Expand Up @@ -275,7 +302,7 @@ def test_control_the_arm_with_its_gravity_dropped_fails_the_balance_on_every_dra


@PROPERTY
@given(st.sampled_from(sorted(TREES)).flatmap(runs))
@given(st.sampled_from(sorted(TREES)).flatmap(lambda name: runs(name, at_a_stop=True)))
def test_control_a_stop_that_gives_energy_fails_the_balance_on_every_draw(drawn):
"""A run that reaches a stop at all three step sizes, with a residual the rounding does not hide (a gift of
``GIFT`` times the unplanted run's largest at any of its step sizes, well above ``ROUNDING`` units in the last place
Expand All @@ -286,7 +313,8 @@ def test_control_a_stop_that_gives_energy_fails_the_balance_on_every_draw(drawn)
only a residual above the integrator's own, could not see it: the plant below the oracle's resolution."""
name, q0, qd0, tau = drawn
plain = orders(BALANCES[name], q0, qd0, tau)
assume(all(at_stops > -math.inf for at_stops in plain.stops) and plain.largest > 1e3 * ROUNDING * EPS * plain.scale)
assert all(at_stops > -math.inf for at_stops in plain.stops), f"{name}: the draw was built to reach a stop: {plain.stops}"
assert plain.largest > 1e3 * ROUNDING * EPS * plain.scale, f"{name}: a residual the rounding hides: {plain.largest}"
planted = orders(GIVERS[name], q0, qd0, tau, GIFT * plain.peak)
assert stops_not_falling(planted), f"{name}: at the stops, at each step size, {planted.stops}"

Expand Down
Loading