diff --git a/.github/workflows/libero-e2e.yaml b/.github/workflows/libero-e2e.yaml index fe48cb385..97562add1 100644 --- a/.github/workflows/libero-e2e.yaml +++ b/.github/workflows/libero-e2e.yaml @@ -41,7 +41,7 @@ jobs: - name: Replay a LIBERO demo through the env server (software rendering) env: MUJOCO_GL: osmesa - run: uv run pytest positronic/simulator/libero/tests/test_e2e.py --no-cov -rs + run: uv run pytest positronic/simulator/libero/tests/test_e2e.py -rs # Reuses the LIBERO checkout the replay above cloned (``~/.cache/positronic/libero/src``). Validates the # command transform (FK/IK + each controller's set_goal inverse) and the gripper normalization against diff --git a/.github/workflows/unit-test.yaml b/.github/workflows/unit-test.yaml index 5ba68077b..c5389b8af 100644 --- a/.github/workflows/unit-test.yaml +++ b/.github/workflows/unit-test.yaml @@ -31,21 +31,10 @@ jobs: - name: Install dependencies run: uv sync --locked - - name: "Run tests (coverage: term, html)" + - name: Run tests env: PYTHONPATH: ${{ env.PYTHONPATH }}:$PWD - run: uv run pytest --cov-report=html - - - name: Coverage summary (job summary) - if: always() - run: uv run coverage report -m >> $GITHUB_STEP_SUMMARY - - - name: Upload coverage HTML artifact - if: always() - uses: actions/upload-artifact@v4 - with: - name: coverage-html-core-${{ matrix.os }}-${{ matrix.python }} - path: htmlcov + run: uv run pytest packaging: runs-on: ubuntu-latest @@ -81,7 +70,7 @@ jobs: - name: Run lerobot vendor tests env: PYTHONPATH: ${{ env.PYTHONPATH }}:$PWD - run: uv run pytest positronic/vendors/lerobot_0_3_3/tests --no-cov + run: uv run pytest positronic/vendors/lerobot_0_3_3/tests lerobot-latest: runs-on: ubuntu-latest @@ -101,7 +90,7 @@ jobs: - name: Run lerobot vendor tests env: PYTHONPATH: ${{ env.PYTHONPATH }}:$PWD - run: uv run pytest positronic/vendors/lerobot/tests --no-cov + run: uv run pytest positronic/vendors/lerobot/tests lockfile-portability: strategy: diff --git a/CLAUDE.md b/CLAUDE.md index 75c838f94..54c74288d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -15,8 +15,8 @@ # Commands - Every Python execution goes through `uv run --locked` — bare `python`/`pytest` bypasses the locked venv -- Run tests: `uv run --locked pytest --no-cov` -- Run single test file: `uv run --locked pytest path/to/test_file.py --no-cov` +- Run tests: `uv run --locked pytest` +- Run single test file: `uv run --locked pytest path/to/test_file.py` - Lint: `uv run --locked ruff check --fix .` - Format: `uv run --locked ruff format .` - Run any Python: `uv run --locked python script.py` diff --git a/README.md b/README.md index 5627200ef..41c3bd4a5 100644 --- a/README.md +++ b/README.md @@ -321,7 +321,7 @@ repoints it at the tool. Run tests and linters from the root directory: ```bash -uv run --locked pytest --no-cov +uv run --locked pytest uv run --locked ruff check . uv run --locked ruff format . ``` diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 792e369f5..a39422062 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -151,7 +151,7 @@ uv run pytest With coverage report: ```bash -uv run pytest --cov=positronic --cov=pimm --cov-report=term-missing +uv run pytest --cov --cov-report=term-missing ``` ## Pull Request Guidelines diff --git a/pimm/tests/test_world.py b/pimm/tests/test_world.py index 3b1b5c71a..7423a0e71 100644 --- a/pimm/tests/test_world.py +++ b/pimm/tests/test_world.py @@ -2,6 +2,7 @@ import multiprocessing as mp import struct import time +from functools import partial from queue import Empty, Full from unittest.mock import Mock, patch @@ -228,6 +229,15 @@ def test_world_defaults_to_system_clock(self): assert isinstance(world.clock, SystemClock) +# Module scope: `start_in_subprocess` pickles the loop to reach a spawned child, and a definition inside +# the test would not pickle. +def heartbeat_loop(emitter, stop_reader, clock): + """Control loop announcing every iteration of its body.""" + while not stop_reader.read().data: + emitter.emit('beat') + yield Sleep(0.01) + + class TestWorld: """Test the World class.""" @@ -244,17 +254,22 @@ def test_background_process(self): """Test that background processes will run simple control loop.""" world = World() with world: - world.start_in_subprocess(dummy_process) + emitter, receiver = world.mp_pipes() + assert isinstance(receiver, SignalReceiver) + world.start_in_subprocess(partial(heartbeat_loop, emitter)) - time.sleep(0.2) # Some time to let the process run assert len(world.background_processes) == 1 + # Stopping before the child reaches its loop body would leave the loop untested: the generator + # sees a set event on its first condition and returns without ever running the body. + deadline = time.monotonic() + 30 + while receiver.read() is None: + assert time.monotonic() < deadline, 'background process never entered its control loop' + time.sleep(0.01) + # We have to set the private event manually, because out of the scope of the context manager # we can't access exit code of the process world._stop_event.set() - # The child is spawned, so it boots a fresh interpreter and imports this module before it can - # observe the stop event — on a slow runner that outlasts any tight deadline. `join` returns the - # moment it exits, so a generous ceiling costs a passing run nothing. world.background_processes[0].join(timeout=30) assert not world.background_processes[0].is_alive() assert world.background_processes[0].exitcode == 0 diff --git a/positronic/simulator/libero/tests/test_e2e.py b/positronic/simulator/libero/tests/test_e2e.py index e639d9d0a..62c8835ab 100644 --- a/positronic/simulator/libero/tests/test_e2e.py +++ b/positronic/simulator/libero/tests/test_e2e.py @@ -13,7 +13,7 @@ then run by explicit path (macOS renders via GLFW, no env var; Linux needs ``MUJOCO_GL=osmesa``):: - uv run --locked pytest positronic/simulator/libero/tests/test_e2e.py --no-cov + uv run --locked pytest positronic/simulator/libero/tests/test_e2e.py """ import os diff --git a/pyproject.toml b/pyproject.toml index f0ff38849..0deede18b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -203,10 +203,6 @@ addopts = [ # The suite waits on spawned processes and sockets about as much as it computes, so it scales with cores. # `-n0` puts it back in one process, which live logs and `--pdb` need. "-n", "auto", - "--cov=pimm", - "--cov=platform_client", - "--cov=positronic", - "--cov-report=term-missing", # Needs the `libero` extra and OSMesa; `libero-e2e.yaml` runs it by path, which `--ignore` allows. "--ignore=positronic/simulator/libero/tests/test_e2e.py", ]