Development
Contribute
Before pushing your code, please ensure that the code formatting is correct by running:
make
In case of errors you can run the autoformatter by executing:
make format
Testing
To run only specific tests:
#using py-test
colcon build --packages-up-to scenario_execution_ros && reset && pytest-3 -s scenario_execution_ros/test/<TEST>.py
#manual run
colcon build --packages-up-to scenario_execution_ros && reset && ros2 launch scenario_execution_ros scenario_launch.py scenario:=<...> debug:=True
#using colcon
colcon test --packages-select scenario_execution \
--event-handlers console_direct+ \
--return-code-on-test-failure \
--pytest-args -k <TEST> \
-s
#using colcon with filtered test
colcon test --packages-select scenario_execution \
--event-handlers console_direct+ \
--return-code-on-test-failure \
--pytest-args test/test_parameter_override.py::TestParameterOverride::test_base_params_success \
-s
Versioning
The version lives in package.xml and nowhere else: every setup.py reads its own
package's package.xml, so the two cannot disagree and a release changes one file per
package. Which packages a release touches is decided by that version, too — a package at the
release version is released, one at 0.0.0 never is (the examples, the *_test
packages, the simulation helpers, the tools, and the libraries kept out of the ROS build
farm). make release-list prints both sets. A new package goes to one or the other on
purpose, and to the release repository's <distro>.ignored for every supported distro if it
is the latter (see below).
Changelog
Each released package keeps a CHANGELOG.rst in the ROS (catkin) format. It is the
single source of truth for both the ROS release (consumed by bloom) and the PyPI
release: the scenario_execution package ships its CHANGELOG.rst in the sdist and
links to it from PyPI via the Changelog project URL. The entries are generated from the
git history at release time — one line per commit, its subject, so write subjects that read
as changelog lines. The Forthcoming section is what the next release names.
Releasing
A release goes to two places from one version: PyPI (the core scenario-execution
distribution, published by CI from the tag) and the ROS build farm (the ROS packages, via
bloom, by hand after the tag). It is one generated pull request, one candidate tried by
hand, one tag, and bloom — three make targets, each printing the next, the last printing
the bloom lines.
Prepare. On a branch from a clean
main:make release-prepare VERSION=1.6.0 # or VERSION=minor: major|minor|patch from the current
This fills each released package's
CHANGELOG.rstfrom the git history since the last tag, names that section1.6.0 (today), and sets<version>in the releasedpackage.xmlfiles — and nothing else. Review the diff (the changelog entries are commit subjects; edit them where a subject says less than a reader needs), then commit and open the pull request. It needscatkin_pkg(pip install catkin_pkg).Try it. Once the pull request is merged:
make release-rc VERSION=1.6.0
This checks the commit (on
main, CI green,package.xmlat 1.6.0, every package either released or in each distro's<distro>.ignored), publishes the wheel as1.6.0rcNto TestPyPI through the publish workflow, and lays out a bloom rehearsal: a scratch clone of the release repository pointed at a clean clone of the commit, with bloom and rosdep in an environment of their own (step 4). It prints two things to run by hand — apip installof the candidate from TestPyPI, and onebloom-release --pretendline per supported distro (Jazzy and Lyrical), each performing that distro's entire build-farm release and pushing nothing. Something wrong is a fix onmainand the next candidate; nothing has been consumed.Tag. When both hold:
make release-final VERSION=1.6.0 COMMIT=<the commit the candidate was built from>
The same checks, plus a candidate on TestPyPI, then the tags on that commit, all lightweight:
1.6.0, andjazzy-1.6.0andlyrical-1.6.0— one per distro, which bloom exports from, all on the same source.git describemust keep answering with the bare1.6.0(it prefers an annotated tag, and the changelog generator refuses one of the other shape). The push of1.6.0is what publishes to PyPI:.github/workflows/publish.ymlbuilds the wheel at the versionpackage.xmlsays, uploads it with trusted publishing, and installs it back from the index.It then creates the GitHub Release of
1.6.0, its notes every released package's entries for the version, each once, and how to install it. An existing release is left as it is;make release-github VERSION=1.6.0is the same step on its own, for a retry.The ROS build farm. Printed by the previous step, once per distro, with the tags on the upstream and
mainstill at this version. Each line runsbloom-releasein the environment the rehearsal ran in, which the previous step lays out again: bloom and rosdep in a venv, the default rosdep sources, and a rosdep cache of its own (ROS_HOME). Not the machine's bloom: a file in/etc/ros/rosdep/sources.list.dcan redefine ROS keys for some Ubuntu releases only, and every key then fails to resolve for a distro built on another one; and bloom runsrosdep updateitself, rewriting whatever cache it is pointed at.bloom-releaseis interactive, needs access to the release repository, reads the version from the tip ofmain, and opens therosdistropull request. Its questions and their answers are printed with the lines; for a distro new torosdistroit asks for the repository's documentation and source, which are this repository atmain. Where bloom cannot open the pull request, it is opened by hand from a fork. It releases every package it finds in the upstream except those named in the release repository's<distro>.ignored, and then insists the rest share one version — which is why the0.0.0set and each<distro>.ignoredmust agree, and why the rehearsal in step 2 exists. A distro is added once, by adding it toROS_DISTROSintools/release.pyand creating its track in the release repository; the release gate refuses until the track exists. The track is copied from an existing one, with only its distro and release tag changed:git clone https://github.com/ros2-gbp/scenario_execution-release.git cd scenario_execution-release git-bloom-config copy jazzy <distro> git-bloom-config edit <distro> # ROS Distro: <distro>, Release Tag: <distro>-:{version} git push origin master
Not
bloom-release --new-track: after creating the track it releases the version onmainstraight away, and the<distro>-<version>tag it exports from does not exist until that distro's first release is tagged.
Notes:
An upload to PyPI cannot be replaced, only yanked, and
pipstill installs a yanked wheel when pinned exactly. That is why the candidate goes to TestPyPI first, and why a broken release is followed by the next number, never by a re-upload.Only a subset of the workspace is published anywhere. What is released is what carries the version;
make release-listis the list, and it must mirror therelease/packageslist for this repository in rosdistro — that list is what bloom produces from the non-ignored packages.catkin_prepare_releasedoes not apply to this repository: it wants a literal version in everysetup.py, which is exactly the duplication thatpackage.xmlalone avoids.
Developing and Debugging with Visual Studio Code
To prevent certain issues, please use the following command for building (remove /build and /install` if another command was used before).
colcon build --symlink-install
In VSCode create new debugging configuration file: Run -> "Add Configuration..."
Add the following entry to the "configurations" element within the previously created launch.json file (replace the arguments as required):
{
"name": "scenario_execution_ros",
"type": "python",
"request": "launch",
"program": "./install/scenario_execution_ros/lib/scenario_execution_ros/scenario_execution_ros",
"console": "integratedTerminal",
"cwd": "${workspaceFolder}",
"args": ["-l", "TEST_SCENARIO.osc"],
}
Create an .env file by executing:
source /opt/ros/humble/setup.bash
source install/setup.bash
echo PYTHONPATH=$PYTHONPATH > .env
echo HOME=$HOME >> .env
echo AMENT_PREFIX_PATH=$AMENT_PREFIX_PATH >> .env
echo LD_LIBRARY_PATH=$LD_LIBRARY_PATH >> .env
In vscode, open user settings and enable the following settings:
"python.terminal.activateEnvInCurrentTerminal": true
To execute the debug configuration either switch to debug view (on the left) and click on "play" or press F5.
Best known Methods
Implement an Action
If an action's
setup()fails, raise an exceptionUse a state machine, if multiple steps are required
Implement a
shutdown()method to cleanup on scenario end.- For debugging/logging:
Make use of
self.feedback_messageMake use of
kwargs['logger'], available insetup()If you want to draw markers for RViz, use
kwargs['marker_handler'], available insetup()(with ROS backend)
Use arguments from
__init__()for a longer running initialization insetup()and the arguments fromexecute()to set values just before executing the action.__init__()andsetup()are called once,execute()might be called multiple times.osc2 arguments can only be consumed once, either in
__init__()orexecute(). Exception: If anassociated_actorexists, it's an argument of both methods.Arguments that need late resolving (e.g. referring to variables or external methods) need to be consumed in
execute().setup()provides several arguments that might be useful: -input_dir: Directory containing the scenario file -output_dir: If given on command-line, contains the directory to save output to -node: (scenario_execution_rosonly): ROS node to utilize (e.g. create subscribers)If your action makes use of variables, set
resolve_variable_reference_arguments_in_executeinBaseAction.__init()toFalse. Theexecute()method arguments will then contain resolved values as before, except for variable arguments which are accessible asVariableReference(with methodsset_value()andget_value()).
Implement an Action with Complex Behavior Tree
For actions that need to provide their own complex behavior tree implementation, inherit from BaseActionSubtree instead of BaseAction:
Override
create_subtree()method instead ofupdate()create_subtree()should return a completepy_trees.behaviour.Behaviourthat implements the action logicAll OSC2 parameters are passed to
create_subtree()method, similar toexecute()inBaseActionThe subtree is created once during action initialization and managed internally
Use
BaseActionSubtreewhen you need composite behaviors, decorators, or complex state machines that are better expressed as behavior trees rather than a single behavior'supdate()methodArguments follow the same rules as
BaseAction: can be consumed in__init__()orcreate_subtree(), but not both