Docs menu

Run the tests

Run each package's tests on an isolated ROS domain, confirm every test was collected, and read a pass, a collection error and a failure.

Run tests with ROS sourced and on their own ROS domain. Without ROS sourced, the test files that import rclpy cannot be collected, which hides the node tests, including every deadman and watchdog test. With ROS sourced, those tests start real driving nodes, which join whatever ROS graph they can reach. On the shared default domain, that can be the car’s.

Run a package’s tests

source /opt/ros/jazzy/setup.bash
source ~/racerbot-ws/install/setup.bash
export ROS_DOMAIN_ID=79 ROS_AUTOMATIC_DISCOVERY_RANGE=LOCALHOST
cd ~/racerbot-ws
python3 -m pytest src/gap_follow/test/ -v

The export puts the tests on domain 79 and restricts discovery to the same computer, so they can never reach the car’s nodes. racerbot_doctor reports a terminal set to domain 79, so a test terminal is not mistaken for the car’s.

To run through colcon instead, with the same environment:

colcon test --packages-select gap_follow
colcon test-result --verbose

Why the domain matters

Every driving-node test publishes on /test_only/drive, never the real /drive. A test in gap_follow, test_no_test_here_builds_a_node_on_the_real_drive, enforces it. In a test that loads a parameter file, the drive_topic:=/test_only/drive remap must come after --params-file, or the YAML’s drive_topic: /drive wins.

The remap protects the motor, but the node still publishes latched topics, /racing_line and /drive_intent, that the dashboard shows. Until September 27, 2026, gap_follow’s node tests faked an LB press and published on the real /drive, which reached ackermann_mux with the driver stack up. The isolated domain keeps every test topic off the car.

Check that every test was collected

A collection error hides a whole file of tests. Compare the number pytest collects with the number of test functions in the files:

python3 -m pytest src/pure_pursuit/test/ --collect-only -q | tail -1
grep -c '^def test' src/pure_pursuit/test/*.py | awk -F: '{s+=$2} END {print s}'

The two counts should match, apart from parametrized tests, which collect once per case. Never use --continue-on-collection-errors: it turns a loud abort into a passing run over part of the suite. Unsourced, src/pure_pursuit/test/ collects only 185 of its tests and stops on 5 collection errors.

Read the output

A passing run

-v prints one line per test, with its file, its name and the result. The name is written to say what the test checks, so a passing list reads as a list of what the code is known to do. This is drive_intent’s throttle tests:

$ python3 -m pytest test/test_throttle.py -v
============================= test session starts ==============================
platform linux -- Python 3.12.3, pytest-9.1.1, pluggy-1.7.0
collecting ... collected 15 items

test/test_throttle.py::test_the_first_call_always_publishes PASSED       [  6%]
test/test_throttle.py::test_publishing_is_capped_at_the_configured_rate PASSED [ 13%]
...
test/test_throttle.py::test_sustained_failure_trips_the_latch_exactly_once PASSED [ 73%]
...
test/test_throttle.py::test_a_latch_that_could_never_tolerate_anything_is_rejected[0] PASSED [ 93%]
test/test_throttle.py::test_a_latch_that_could_never_tolerate_anything_is_rejected[-1] PASSED [100%]

============================== 15 passed in 0.03s ==============================

The file has 14 test functions but collects 15 items: the last test is parametrized, and runs once for 0 and once for -1, shown in square brackets. Without -v, each test prints a single . instead of a line.

The last line is the one to read: how many tests passed, failed, errored or were skipped. A run is clean only when it says passed and nothing else.

A collection error

A collection error means pytest could not import a test file, so none of the tests in it ran. This is imu_bridge’s suite run without ROS sourced:

==================================== ERRORS ====================================
________________ ERROR collecting test/test_imu_status_node.py _________________
ImportError while importing test module '.../src/imu_bridge/test/test_imu_status_node.py'.
Traceback:
test/test_imu_status_node.py:21: in <module>
    import rclpy
E   ModuleNotFoundError: No module named 'rclpy'
=========================== short test summary info ============================
ERROR test/test_imu_status_node.py
!!!!!!!!!!!!!!!!!!!! Interrupted: 1 error during collection !!!!!!!!!!!!!!!!!!!!
1 error in 0.28s

pytest stops before running anything, which is the safe behaviour: a partial run that reported “passed” would hide every node test in that file. No module named 'rclpy' means the terminal skipped source /opt/ros/jazzy/setup.bash. No module named followed by one of the workspace’s own packages means it skipped source ~/racerbot-ws/install/setup.bash, or the package was never built.

A failure

A failure means a test ran and an assertion did not hold. To show one, drive_intent’s failure latch was broken on purpose in a copy of the package: the comparison that trips it after max_failures failures in a row was changed from >= to >, so it trips one failure late. Breaking the code like this is the check the test quality standards require before a test counts as done. Two tests caught it:

    def test_sustained_failure_trips_the_latch_exactly_once():
        latch = FailureLatch(max_failures=3)
        assert latch.record_failure() is False
        assert latch.record_failure() is False
>       assert latch.record_failure() is True      # the tripping call
E       assert False is True
E        +  where False = record_failure()

test/test_throttle.py:119: AssertionError
...
=========================== short test summary info ============================
FAILED test/test_throttle.py::test_sustained_failure_trips_the_latch_exactly_once
FAILED test/test_throttle.py::test_success_after_the_latch_trips_does_not_re_enable_it
2 failed, 13 passed in 0.05s

Read a failure from the bottom up. The summary names each failing test. Above it, each failure prints the test’s source with > on the line that failed, then E lines with the values involved: here the third failure in a row should have tripped the latch (True) and did not (False). The last line of the block gives the file and line number.

What to do

Output Meaning Do this
N passed only Every collected test passed Check the collected count against the files, as above
ERROR collecting, No module named 'rclpy' ROS is not sourced Source both setup files and run again
ERROR collecting, any other import error A dependency is missing, or the package was not built Build the package, or install what the error names
FAILED The code does not do what the test says it should Treat it as a finding: read the assertion and fix the code
FAILED in a test that passed before a change The change broke a behaviour the test protects Fix the change, not the test
skipped or xfailed A test was deliberately not run, or is expected to fail Every skip must name its reason and the condition for removing it

A failing test is never fixed by loosening it, deleting it or skipping it in the same change. If the test itself looks wrong after a deliberate change of behaviour, raise it with the reason and where the new behaviour is written down. See Rule 2.

colcon’s output

colcon test runs each package’s tests and stores the results. It prints only a line per package, so read the details with colcon test-result:

colcon test --packages-select drive_intent
colcon test-result --verbose

colcon test-result ends with a summary line of the form Summary: N tests, 0 errors, 0 failures, 0 skipped. With --verbose it prints each failing test’s output above that line, in the same form as pytest’s.

Packages that need no ROS

drive_intent, odom_calibration, race_diagnostics, racerbot_sim and most of web_dashboard keep their logic free of rclpy, and their tests run unsourced:

python3 -m pytest src/drive_intent/test/ -v

This is the layout to follow for any new package with non-trivial mathematics or parsing. See Add your own code.

The tool suites

The simulators and the run analysis tools under tools/ have their own tests, and they are not colcon packages, so colcon test never runs them. Run all three suites with one command, sourced and on the isolated domain as above:

python3 -m pytest tools/f1tenth_sim/tests/ tools/racerbot_sim/tests/ tools/run_analysis/tests/ -q

On car 1 on October 29, 2026, this collected 472 tests from 382 test functions; the rest are parametrized cases.

Other test suites

Suite Command
The no-ROS simulator python3 tools/f1tenth_sim/run_validation.py --scenario all
The ROS simulator, end to end tools/racerbot_sim/run_auto_map_validation.py --scenario all
The car profiles and both cars’ hardware guard python3 -m pytest src/racerbot_launch/test/ src/racerbot_car/test/ -v
The neural racer training code See tools/neural_racer/README.mdGitHub

Checked against car 1 commit 8a88dcc and car 2 commit 80da6f5, before the two workspaces merged.