Skip to content

CI/CD Tuning PR - pytest collection bug fix - #384

Merged
andrewjong merged 6 commits into
developfrom
ci/reliable-pr-validation
Aug 18, 2026
Merged

CI/CD Tuning PR - pytest collection bug fix#384
andrewjong merged 6 commits into
developfrom
ci/reliable-pr-validation

Conversation

@pvkumara

Copy link
Copy Markdown
Collaborator

What features did you add and/or bugs did you address?

  • Which GitHub issue does this address?

No dedicated issue. This consolidates the unit-test collection improvements from PR #383 and addresses the CI collection/reporting failures encountered while testing PR #376.

  • Additional description if not fully described in the GitHub issue

This PR prevents bare /pytest runs from accidentally scanning the entire repository and importing ROS, Isaac Sim, Torch, and other container-only dependencies on the host runner.

It also:

  1. Runs Python unit tests automatically on ubuntu-latest.
  2. Runs build_packages automatically on OSMO for same-repository PR updates.
  3. Keeps expensive simulation campaigns selectable through /pytest.
  4. Distinguishes real policy failures from collection, setup, timeout, cancellation, and partial-run failures.
  5. Compares metrics only between identical simulation campaigns.
  6. Finalizes the PR check only after metrics processing completes.
  • Please add videos and images to demonstrate the feature. Please upload videos to somewhere persistent (e.g. YouTube or Vimeo) for archival purposes.

No videos or images are applicable because this is CI/test infrastructure rather than a visual feature.

How did you implement it?

  • Algorithm details, design decisions, engineering notes, and any other relevant information about the implementation should be included

The implementation combines PR #383’s co-located unit-test discovery with additional CI safety and reporting changes:

  • collection_is_broad() now injects co-located unit tests only for the complete tests/ harness. Empty paths, repository-root paths, multiple paths, and narrowed paths cannot accidentally expand collection.
  • system-tests.yml serializes empty PYTEST_ARGS as zero arguments rather than one empty positional argument.
  • .github/workflows/unit-tests.yml runs pytest tests/ -m unit on ubuntu-latest for PRs targeting main or develop.
  • Automatic OSMO PR runs use -m build_packages. GPU simulations are requested explicitly.
  • run_meta.json records pytest exit status, selected/completed tests, simulation execution counts, phase outcomes, and a campaign fingerprint.
  • Setup/teardown errors, collection failures, interrupted runs, truncated artifacts, and partially executed campaigns are marked non-comparable.
  • Real simulation assertion failures retain their pass rate and recorded metrics.
  • Baseline comparison requires identical selected tests and parameters.
  • Artifact names, report comments, and custom checks use the actual tested revision.
  • The custom /pytest check is finalized after regression analysis rather than before it.

How do you run and use it?

  • What commands and button presses do you use to manually launch the stack to use your new feature?

No manual stack launch is required for Python unit tests. Opening or updating a PR targeting main or develop automatically starts:

pytest tests/ -m unit

Same-repository PRs also automatically run the OSMO package gate:

pytest tests/ -m build_packages

To launch an OptiTrack simulation campaign, add this PR comment:

/pytest -m optitrack --sim isaacsim --num-robots 1 --stress-iterations 1

A bare comment safely runs the default suite:

/pytest

  • Write a detailed procedure with EXACT BASH COMMANDS so that another maintainer can replicate and understand the benefits of your feature, and reproduce the videos and images you added above.

From the repository root:

git fetch origin
git checkout ci/reliable-pr-validation

python3 -m venv /tmp/airstack-ci-tests
source /tmp/airstack-ci-tests/bin/activate
python -m pip install -r tests/requirements.txt

export AIRSTACK_ROOT="$PWD"

pytest tests/ -m unit -q
pytest tests/ --collect-only -q
pytest tests/system/test_liveliness.py --collect-only -q

The equivalent AirStack CLI command is:

airstack test -m unit -v

After opening the PR, confirm the Unit Tests and System Tests checks appear. Use a /pytest comment for the specific simulation mark affected by the PR.

Testing with PyTest

  • What pytests did you add to ensure the feature is reliable and robust? What metrics are used?
  1. Full-harness versus narrowed collection.
  2. Empty and repository-root paths are rejected.
  3. CI invokes the correct collection root.
  4. Automatic PR triggers and runner selection.
  5. Tested SHA and PR-number propagation.
  6. Supported cross-run artifact inputs.
  7. PR checks are finalized after metrics analysis.

Metrics include:

  1. Pytest exit status.
  2. Selected and completed test counts.
  3. Selected and completed simulation counts.
  4. Setup/call/teardown outcomes.
  5. Campaign fingerprint.
  6. Existing simulation metrics such as trajectory success, cross-track error, path RMSE, timing, topic rates, and compute usage.
  • What's the exact command to run the pytests that test your feature? i.e. airstack test -m ...

airstack test -m unit -v

Direct host-side equivalent:

export AIRSTACK_ROOT="$PWD"
pytest tests/ -m unit -q

Contract tests only:

export AIRSTACK_ROOT="$PWD"
pytest
tests/meta/test_collection_contract.py
tests/meta/test_metrics_reporting_contract.py
-q

  • What are the expected results of the tests? What should a maintainer look at to understand whether the test succeeded?

Local validation produced:

193 passed unit/contract tests.
97 correctly deselected non-unit tests.
290 tests collected by the bare harness without import errors.
16 tests collected by the narrowed liveliness command.

Maintainers should verify:

unit-tests.yml passes on ubuntu-latest.
The automatic OSMO build_packages check passes.
Bare collection does not import unrelated repository tests.
Narrowed collection remains narrow.
A real simulation assertion failure retains pass rates and metrics.
Infrastructure failures display “Simulation metrics are not comparable.”
Regression tables appear only when campaign fingerprints match.

Documentation

  • Was mkdocs.yml updated? (y/n)

No. No new documentation page was added, so no navigation change was required.

  • Do the docs have sufficient scope such that a newcomer can easily reproduce and use your feature?

Yes. The testing documentation, tests/README.md, AGENTS.md, OSMO documentation, changelog, and testing skills now explain:

Automatic unit and package-build gates.
How to request simulation campaigns.
Exact local and CI commands.
How co-located tests are discovered.
How to interpret run_meta.json.
How infrastructure failures differ from policy regressions.

  • Is there sufficient visual media?

Visual media is not applicable because this change affects CI workflows, pytest collection, and metrics reporting. The relevant outputs are GitHub checks, PR comments, job summaries, and test artifacts.

Versioning

Yes. The branch uses:

VERSION="0.19.0-alpha.17"

JohnYanxinLiu and others added 6 commits August 17, 2026 16:13
Unit test source moved into <package>/test/ and is collected from
colcon_unit_test_packages.yaml, but the surrounding documentation still described
the mirror-directory-and-proxy scheme that replaced. Six per-layer stubs under
tests/robot/ told authors to add tests in directories tests no longer live in, and
tests/sim/motive_emulator/README.md proposed a NatNet emulator that was built at
simulation/isaac-sim/extensions/optitrack.natnet.emulator/ instead. Remove them and
rewrite the two tree READMEs as signposts.

Correct the add-unit-tests and run-system-tests skills, which future agents read to
work in this area, on four points they had wrong:

- Running them. `pytest tests/` does not collect co-located unit tests — the
  injection in conftest.pytest_configure is skipped whenever a path is given on the
  command line. It reports "no tests collected" and exits 5, which reads as a
  failure but means nothing ran. `airstack test -m unit` and `cd tests && pytest -m
  unit` are the working forms; verified 155 passed vs exit 5.
- CI. No workflow runs unit tests. system-tests.yml invokes `pytest tests/`, and
  fires only on PR-open, /pytest, or workflow_dispatch.
- The mark. pytest_itemcollected applies @pytest.mark.unit by file location, so
  test sources should not declare it. The skill previously said "always decorate",
  which is where the redundant declarations came from.
- colcon. It runs only what a package's CMakeLists registers. natnet_ros2 has
  ament_add_gtest but no ament_add_pytest_test, so its Python tests run only under
  the root harness.

Also fixes a pytest_args example that would silently do nothing (`-m not linter`;
ament's pytest runner ignores -m via PYTEST_ADDOPTS, and the real value is []), and
the same stale layout claim in the testing docs and the emulator README.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
C++ gtests run under colcon test, which CI executes inside the robot container via
the build_packages mark (test_build_packages.py::test_colcon_test_robot). Python unit
tests run under the root harness, which no workflow invokes.

Whether colcon test also picks up a package's Python tests depends on its build type:
lidar_point_cloud_filter is ament_python and exposes them via setup.cfg
(testpaths = test), so they run in both places; natnet_ros2 is ament_cmake and
registers only ament_add_gtest, so its Python tests run nowhere in CI.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Unit-test source lives outside tests/, so pytest_configure appends it to the
collection args. That injection was gated on args_source != ARGS, which pytest sets
for any positional path — including `tests/`. The intent was that
`pytest tests/system/foo.py` should not drag in 155 unrelated tests, but the guard
could not tell narrowing from naming the whole suite, so CI's `pytest tests/`
collected 97 of 252 items and the Python unit tests ran nowhere.

Decide on the paths instead: a positional is broad when it names tests/ itself or an
ancestor, narrow otherwise. `pytest tests/` and `pytest .` inject; `pytest tests/system`,
a single file, and a node id do not. Node ids are split on `::` first, since only the
part before it addresses the filesystem.

`any` rather than `all` is deliberate — pytest_configure appends the co-located files
(narrow, absolute) to config.args, so `all` would flip the answer for anything
re-deriving it after that mutation. The decision is also stashed on config for the
contract test to read.

tests/meta/test_collection_contract.py pins the behaviour: a table over broad/narrow
invocations, a check that the command in system-tests.yml is classified broad (the
test that would have caught this), and a check that every discovered file produced
collected items. It lives under tests/ on purpose — co-located, it would stop being
collected at the same moment it stopped guarding anything.

Verified: `pytest tests/ -m unit` 0 -> 170 passed; `cd tests && pytest -m unit`
unchanged at 170; `pytest tests/system/test_liveliness.py` still collects 16.

Unit tests now run with every system-tests.yml invocation. That workflow's triggers
are unchanged and intentional — PR open, /pytest, workflow_dispatch — since the same
run drives the GPU system tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The split was documented as a fact without its reason. A gtest is a binary compiled
against the package's headers and rclcpp, so it can only run where the ROS toolchain
is — colcon test inside the robot container, which build_packages reaches after
building with -DBUILD_TESTING=ON. Python unit tests stub ROS at the import boundary
and touch no ROS runtime, so they need neither a build nor a container, which is what
keeps the suite under a second.

State the invariant that follows: a Python test needing a live ROS node belongs in
tests/integration/ or tests/system/, not in a package test/ dir.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
They are hermetic and they guard the collection of everything above them, so
running them after the GPU sim suites is backwards — a hung flight test would mean
they never execute. Rank them in _MODULE_ORDER right after the co-located unit
tests, ahead of system.test_build_docker.

Also drop the `from conftest import repo_path` in favour of harness.discovery,
which the module already imports from — one less thing between the test and the
function it needs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Run fast unit checks automatically, constrain host collection, and distinguish infrastructure failures from comparable simulation results.
@github-actions

Copy link
Copy Markdown

Test Metrics — e75710a8aa9ac30d94551ffcf4fc089b9e1a4cd1

No metrics report generated.

@andrewjong
andrewjong merged commit f469726 into develop Aug 18, 2026
4 of 5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants