For esmini developers and contributors
Brief info on how to contribute is found here: https://github.com/esmini/esmini/blob/master/CONTRIBUTING.md
Complementing the Build guide this section contains additional information regarding branch strategy, formatting and test.
Branch strategy
From v2.30.0 the dev branch was introduced as the new top branch for continuous integration of features and bug fixes.
dev branch contains the latest integrated features and bug fixes. New features should be developed in branches created from dev. master is the default branch and primarily reserved for official releases.
Continuous integration job will be launched for pushed commits on the following branches:
-
master -
dev -
any other branch starting with
feature/, e.g.feature/support_emergency_vehicles
Pull requests should be targeting the dev branch.
Master branch represent the latest released software. It should be in synch with dev.
Branch out of synch
dev and master branches should be in perfect sync, i.e. based on common history. Unfortunately, to keep this true and to avoid messy history and lost tags, branch history might be changed in rare cases. That can lead to local branch clones out of synch. If that is the case, follow these steps to restore sync of your local branch:
-
git branch backup(only needed if you have local commits you want to preserve for later cherry-picking) -
git fetch origin -
git reset --hard origin/<branch name>
<branch name> might be master or dev, or whatever branch to be restored.
Formatting and code analysis
From esmini v2.30.0 the CI will include formatting checks, in addition to static analysis and tests.
From v2.48.0, esmini utilizes pre-commit hooks for these checks. For details of previous implementation, see older versions of this document, e.g. v2.47.0.
The code styles are defined by following schemes:
| C++ | |
| Python | |
| cmake | |
| cppcheck |
esmini uses pre-commit hooks to automatically perform code formatting and static code analysis. To enable this functionality, install the pre-commit package:
pip install pre-commit
Then, run the following command to install the pre-commit hooks in esmini root folder. This sets up the pre-commit hooks, enabling automatic checks when making commits:
pre-commit install
Dependencies
Install dependent Python packages
pip install -r ./support/python/requirements.txt
clang-format
| Windows |
Download and install LLVM-15.0.7-win64.exe |
| Linux |
|
cppcheck
| Windows |
Download and install cppcheck-2.17.1-x64-Setup.msi |
| Linux |
|
Cppcheck version in esmini CI/CD
The CI/CD pipeline uses Cppcheck version 2.17 for static code analysis. If you require a different version of Cppcheck, perhaps due to specific dependencies, it is recommended to leverage the esmini CI/CD environment itself. To do this, create a fork of the esmini repository and run your static analysis within that environment simply by pushing your code changes. This ensures consistency with the CI/CD setup.
Using a different Cppcheck version locally
If you prefer to use a different version of Cppcheck locally or do not want to install version 2.17 system-wide, follow the instructions below.
If you have a different version installed or don’t want to install 2.17 system-wide, see steps below for your platform.
For Linux and macOS
Example how to clone and build cppcheck 2.17.1, from home directory:
git clone --branch 2.17.1 --depth 1 https://github.com/danmar/cppcheck.git
cd cppcheck
mkdir build
cd build
cmake ..
cmake --build .
To temporarily use your locally built Cppcheck 2.17 without changing your system default, run the following from your esmini root folder:
PATH="$HOME/cppcheck/build/bin:$PATH" pre-commit run cppcheck --all-files
To make this change more permanent, you can add the following line to your shell configuration file (e.g. ~/.bashrc):
export PATH="$HOME/cppcheck/build/bin":$PATH
For Windows
Download the official Cppcheck 2.17 installer from:
Make sure to install it to a separate directory than any previous Cppcheck version you want to keep.
Manually apply specific format
To run formatting locally, follow steps below.
pre-commit run clang-format --all-files
pre-commit run cmake-format --all-files
pre-commit run black --all-files
Manually analyse code
pre-commit run cppcheck --all-files
or formatting and code analysis can be done in one step by:
pre-commit run --all-files
Automatic formatting and code analysis
pre-commit will automatically run on each commit on all staged files. Sometimes automatic application maybe not needed for instance work in progress etc. In that case, you can skip the pre-commit hook by using the --no-verify option when committing:
git commit -m "my commit message" --no-verify
Test
Test frameworks
There are two sets of tests on different frameworks:
-
Unit tests (white box)
-
Smoke tests (black box)
The unit tests suites are basically C++ code modules of test cases based on the Google Test framework. They range from true unit tests (testing individual classes or functions) to system level tests reading a scenario and inspecting output, e.g. log files or visualization pixel data. The key feature of the unit tests is the possibility to inspect internal objects and variables.
Example: Unittest/RoadManager_test.cpp
The smoke test suite is basically execution of a selection of scenarios with some strategic inspections of the result. Scenario elements and timing can be checked against log file while object info such as position, rotation and speed can be checked against dat file entries. A Python framework handles the execution and gathering of result for convenient checking.
The smoke test script is found here: test/smoke_test.py
In addition, there’s a basic test of the C# esminiLib wrapper. It’s available on Windows. Compile and run from esmini root folder as:
cmake test/CSharpWrappers -B test/CSharpWrappers/build
cmake --build test/CSharpWrappers/build --config Release
cd bin
../test/CSharpWrappers/build/Release/libesmini_cs_wrapper_test.exe
Run tests
For easy execution of all tests, both unit tests and smoke tests, run this main test script: scripts/run_tests.sh, from esmini root folder:
./scripts/run_tests.sh
Tip: On Windows, run it from GIT bash.
Run XML schema validation tests
XML schema validation ensures scenario files (xosc and xodr) complies with the OpenSCENARIO and OpenDrive XSD schemas. The checks are done by the Python script run_schema_comply.py located in the scripts directory. The schema files are located in the resources/schema directory. From release v2.40, all esmini scenario files are validated against the schemas as part of CI.
The script depends on the xmlschema and lxml Python packages. To install required packages, run:
pip install -r support/python/requirements.txt
Run the XML validation tests:
python3 ./scripts/run_schema_comply.py <path1> [path2] [path3] …
where path can be an XML file or a directory. Any directory subfolders will be included recursively.
Example 1, check single file:
python3 scripts/run_schema_comply.py resources/xosc/cut-in.xosc
Example 2, check two files:
python3 scripts/run_schema_comply.py resources/xosc/cut-in.xosc resources/xodr/two_plus_one.xodr
Example 3, check two directories:
python3 scripts/run_schema_comply.py resources EnvironmentSimulator
Example 4, check one file and one directory:
python3 scripts/run_schema_comply.py EnvironmentSimulator/Unittest/xosc/conflicting-domains.xosc resources
Tip: Use bash find command to apply finer filtering. Example:
python3 scripts/run_schema_comply.py $(find ./resources ./EnvironmentSimulator -name "follow_route*.xosc" -or -name "*500*.xodr" -or -name "*signs.xodr")
validates all OpenSCENARIO files starting with "follow_route" and all OpenDRIVE files containing "500" or ending basename with "signs" or containing "500", in and under the resource and EnvironmentSimulator folders.
Sanitizers
Google sanitizers framework is utilized to detect memory related errors, for example memory leaks. It’s executed nighly as a GitHub CI actions job on dev branch. The job can also be trigged manually, but will only run checks on dev branch. Sanitizers is currently only applied to Linux builds.
You can also run the sanitizer checks locally (on any branch, even uncommitted code) on Ubuntu:
-
Set the following environment variables, run from esmini root:
export LSAN_OPTIONS="print_suppressions=false:suppressions=$(pwd)/scripts/LSAN.supp"
export ASAN_OPTIONS="detect_invalid_pointer_pairs=1:strict_string_checks=1:detect_stack_use_after_return=1:check_initialization_order=1:strict_init_order=1:fast_unwind_on_malloc=0:suppressions=$(pwd)/scripts/ASAN.supp"
-
Configure and build esmini with sanitizers enabled
cd build
cmake -G Ninja -DUSE_OSG=FALSE -DCMAKE_BUILD_TYPE=RelWithDebInfo -DENABLE_SANITIZERS=TRUE ..
ninja install
cd ..
-
Run the test suites, including both unit- and smoke tests, from esmini root:
./scripts/run_tests.sh
Troubleshooting
On Ubuntu some combination of gcc and sanitizer randomly cause sanitizer run failure with error message: AddressSanitizer:DEADLYSIGNAL.
If this happens, try the following:
setarch $(uname -m) -R
bash
setarch will affect reported architecture from system to sanitizer. bash is to restore shell environment, it can of course be replaced by any shell program, e.g. tcsh.
To revert settings, simply run:
exit
exit
First exit will escape the latter bash environment, the second reverts effect of the setarch command.
Source of setarch tip: https://stackoverflow.com/questions/77894856/possible-bug-in-gcc-sanitizers
Implementation guidelines
Floating point precision
To achieve consistent results, esmini applies a tolerance of 1e-6 (0.000001) for floating point comparisons, allowing for slight differences due to precision limitations. Examples:
| x > y |
is considered true if x > y + 1e-6 |
| x ≥ y |
is considered true if x > y - 1e-6 |
| x == y |
is considered true if abs(x - y) < 1e-6 |
In code the macros SMALL_NUMBER, for doubles, and SMALL_NUMBERF, for floats should be used for this purpose. Example:
if (x > y + SMALL_NUMBER)
There is also macros NEAR_NUMBERS(x, y) and NEAR_NUMBERSF(x, y), that can be used for checking if numbers are effectively equal. It utilizes the same tolerance. Example:
if (NEAR_NUMBERS(x, y))
Options
Using options (in code)
You can fetch the value of an option basically in two ways:
std::string GetOptionValue(std::string option_name)
or
std::string GetOptionValueByEnum(CONFIG_ENUM option_enum);
The Enum variant should be preferred whenever an option is frequently accessed. Otherwise, e.g. in a setup phase, the difference in terms of performance is negligible.
Adding options (in code)
Steps:
-
Create the option
-
Add the option to desired applications
-
Document the option
Details of each step:
Create the option
In EnumConfig.hpp:
-
Add the option to the
CONFIG_ENUM -
Connect the option name to the enum in
configStrKeyEnumMap
Add the option to desired applications
using the function opt.AddOption()
-
esmini: in playerbase.cpp,
PlayerBase::Init() -
replayer: in replayer/main.cpp,
main() -
odrviewer: in odrviewer/main.cpp,
main()
Document the option
Update:
-
esmini: update docs/commands.txt
-
replayer: update replayer/readme.txt
-
odrviewer: update odrviewer/readme.txt
Two tips to get the text:
-
Run corresponding application with no args or
--helpand copy the content from console output -
Press
H(shift-h) key in the application window to output the same help info to console
By adding the option to the .txt files, the option will automatically be added in next generation of Command reference as well.