Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions .github/workflows/coverage.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
name: Coverage

on:
push:
branches:
- v2
- code_coverage
pull_request:
branches:
- v2
workflow_dispatch:

jobs:
coverage:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # git describe needs the full tag history

- name: Install tools
run: |
sudo apt-get update -qq
sudo apt-get install -y ninja-build gcovr clang llvm libclang-rt-dev libssl-dev

- name: Cache FetchContent dependencies
uses: actions/cache@v4
with:
path: build-cov/_deps
key: deps-coverage-${{ hashFiles('vendors/CMakeLists.txt', 'tests/CMakeLists.txt') }}
restore-keys: deps-coverage-

- name: Configure
env:
CC: clang
CXX: clang++
run: |
cmake -B build-cov -GNinja \
-DCMAKE_BUILD_TYPE=Debug \
-DMADS_BUILD_TESTS=ON \
-DMADS_COVERAGE=ON \
-DMADS_BUILD_APPS=OFF \
-DMADS_DIRECTOR=OFF \
-DMADS_ENABLE_MONGOCXX=OFF

- name: Build
run: cmake --build build-cov -j$(nproc)

- name: Test
run: ctest --test-dir build-cov --output-on-failure

- name: Coverage report
run: |
gcovr --config gcovr.cfg --object-directory build-cov \
--gcov-executable "llvm-cov gcov"

- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v5
with:
files: coverage.xml
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: true
10 changes: 9 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,15 @@ TODO.md
!mads.ini
!rust/**/Cargo.toml
!share/templates/rust_Cargo.toml
!tests/fixtures/**

_*.md

/*.json
/*.json
# Coverage artifacts
coverage.xml
coverage_html/
*.gcda
*.gcno
*.gcov
*.profraw
7 changes: 7 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,20 @@ These changes summarize what was added or updated since `v2.3.1`.
- **High-resolution loop pacing.** `Agent::loop()` now paces its internal timing in nanoseconds instead of milliseconds (existing millisecond-based code keeps compiling and behaving as before). Agents can set a `time_step_us` key in their settings section (wins over `time_step`) for microsecond-granularity periods, and opt into `enable_high_res_loop()` (or the `high_res_loop`/`spin_margin_us` settings keys) to busy-spin the tail of each interval instead of sleeping through it, trading CPU time for microsecond-accurate wake-up timing.
- **`mads plugin --update` migration command.** C++ plugins can now be migrated in place to the current plugin protocol version instead of being rewritten by hand. It detects the plugin's current protocol from its `CMakeLists.txt`, chains the migration steps recorded in `share/plugin_migrations` up to the current protocol, bumps the pinned `GIT_TAG`, and rewrites affected method signatures (located structurally, so reformatted/multi-line declarations migrate correctly). Each modified file is saved as `*.bak`, and a change report plus manual follow-up checklist is printed; unless `--no-check`, the migrated plugin is then compiled so the compiler flags anything the rewrite could not handle. `--dry-run`, `--from`, and `--to` are also available. Rust plugins are unaffected (they track the `mads-plugin` crate version in `Cargo.toml`). See [share/man/mads-plugin.md](share/man/mads-plugin.md).
- **`plugin_deps.json` dependency manifest.** Scaffolded C++ plugins now pin their dependency versions (plugin protocol, `mads_plugin`/`pugg` git tags, bundled `nlohmann::json` version) from a single `share/plugin_deps.json` file, which also supplies the default target protocol for `mads plugin --update`, instead of being hard-coded into the CMake template.
- **`Mads::Runtime`.** Agent run state (the "keep going" flag consulted by `Agent::loop()`, the last-known-value drain thread, and the threaded remote control) is now owned by a `Mads::Runtime` object instead of a single process-global flag. Each `Agent` gets its own `Runtime` by default, so stopping or destroying one agent no longer stops the loops of other agents hosted in the same process, and a disconnected agent can reconnect and loop again. Agents that should stop together can share a `Runtime` via `Agent::set_runtime()`; `Mads::Runtime::stop_process()` (used by the SIGINT/SIGTERM handlers and the remote `shutdown`/`restart` commands) still stops every agent in the process. The previous `Mads::running` flag remains available as a `[[deprecated]]` alias of the process-wide state, so existing code keeps compiling and behaving as before, with a compile-time warning pointing at the replacement API. See [RUNTIME.md](RUNTIME.md).
- **Unit test suite and code coverage.** MADS gained an in-tree Catch2-based unit test suite (`tests/`, built with `-DMADS_BUILD_TESTS=ON`) covering the core library — settings loading, the broker request protocol, pub/sub and the wire codec over loopback ZeroMQ, the C ABI, CURVE key handling, and the plugin migration engine — with no external broker, database, or network access required. A `-DMADS_COVERAGE=ON` CMake option instruments the build for `gcovr`/lcov-style reports, and a new GitHub Actions workflow (`.github/workflows/coverage.yml`) runs the suite and uploads results to [codecov.io](https://codecov.io/gh/pbosetti/MADS) on every push.

## Improvements

- **Updated to `mads_plugin` v2.4-P8 (protocol P8) and `pugg` 1.2.0.** Plugin registration is simplified: the per-type `INSTALL_SOURCE_DRIVER`/`INSTALL_FILTER_DRIVER`/`INSTALL_SINK_DRIVER` macros are replaced by a single type-deducing `MADS_REGISTER_PLUGINS(klass, ...)` macro that can register several plugins from one library. Plugin child-class method signatures (`kind`/`load_data`/`process`/`get_output`/`set_params`/`info`) are unchanged from P7. The generated driver's `create()` now returns a `std::unique_ptr` rather than a raw pointer. A migration step (`P7-P8.json`) is provided for `mads plugin --update`.
- **More robust `GIT_TAG` rewriting during plugin migration.** The CMake `GIT_TAG` bump used by `mads plugin --update` no longer relies on a single fragile regex spanning the whole `FetchContent` block (which could match a tag mentioned in a comment, or mis-substitute a replacement value starting with a digit). It now locates the block header and then tokenizes it with a small CMake-aware scanner that honours `#` comments, quoted arguments, and nested parentheses, replacing exactly the `GIT_TAG` value token by position.
- **gv2fsm updated to v2.0.0.** The bundled finite-state-machine generator is updated to its latest release.

## Bug fixes

- **`Agent::query_broker()` could hang forever on shutdown.** The REQ socket used to fetch settings from a broker had no linger timeout; if the broker was unreachable, a queued but undelivered request kept the ZeroMQ context alive and `Agent::shutdown()` blocked indefinitely in context termination. The socket now sets `linger = 0`.
- **C API CURVE key setters could crash instead of returning an error.** `agent_set_client_public_key`/`agent_set_client_secret_key`/`agent_set_server_public_key` guarded against being called before `agent_setup_crypto()` by checking the address of the internal `CurveAuth` pointer, which is never null; calling any of them too early dereferenced a null `CurveAuth` and crashed the process instead of returning `-1` as documented.

---

# Release 2.3.1
Expand Down
58 changes: 58 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ include(GNUInstallDirs)
option(MADS_ENABLE_MONGOCXX "Enable MongoDB C++ driver dependency" ON)
option(MADS_BUILD_APPS "Build executables" ${PROJECT_IS_TOP_LEVEL})
option(MADS_DIRECTOR "Also build the director app (requires graphical interface)" ON)
option(MADS_BUILD_TESTS "Build unit tests (Catch2)" OFF)
option(MADS_COVERAGE "Instrument MadsCore and tests for coverage analysis" OFF)

set(APP_NAME ${LIB_NAME})
set(PREFIX "mads")
Expand Down Expand Up @@ -221,6 +223,12 @@ else()
list(REMOVE_ITEM LIB_SOURCES ${SOURCE_DIR}/https_client_macos.mm)
endif()

# mongo_fetch.cpp unconditionally includes <mongocxx/...>, so it can only be
# part of the library when the MongoDB driver is actually vendored in.
if(NOT MADS_ENABLE_MONGOCXX)
list(REMOVE_ITEM LIB_SOURCES ${SOURCE_DIR}/mongo_fetch.cpp)
endif()

add_library(
${LIB_NAME}
SHARED
Expand Down Expand Up @@ -288,6 +296,18 @@ if(APPLE)
endif()
endif()

if(MADS_COVERAGE)
if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
# Instrument only MadsCore (PRIVATE flags): vendored dependencies stay out
# of the report and build fast. Atomic profile updates are required because
# Agent spawns drain/remote-control/watchdog threads.
target_compile_options(${LIB_NAME} PRIVATE -O0 -g --coverage -fprofile-update=atomic)
target_link_options(${LIB_NAME} PRIVATE --coverage)
else()
message(WARNING "MADS_COVERAGE requires GCC or Clang; coverage flags not applied")
endif()
endif()

add_library(MadsMainDeps INTERFACE)
if(WIN32)
target_compile_definitions(${LIB_NAME} PUBLIC NOMINMAX WIN32_LEAN_AND_MEAN _HAS_STD_BYTE=0)
Expand Down Expand Up @@ -330,6 +350,44 @@ if(DEFINED _zmqpp_binary_dir AND _zmqpp_binary_dir)
)
endif()

#
# _____ _
# |_ _|__ ___| |_ ___
# | |/ _ \/ __| __/ __|
# | | __/\__ \ |_\__ \
# |_|\___||___/\__|___/
#
# Note: enable_testing() is used directly (not include(CTest)) because
# vendors/CMakeLists.txt forces BUILD_TESTING OFF in the cache to keep
# third-party test suites out of the build.
if(MADS_BUILD_TESTS)
enable_testing()
add_subdirectory(tests)
endif()

if(MADS_COVERAGE)
find_program(GCOVR_EXECUTABLE gcovr)
if(GCOVR_EXECUTABLE)
# clang emits gcov-compatible data but needs its own reader tool.
if(CMAKE_CXX_COMPILER_ID MATCHES "Clang")
set(MADS_GCOV_TOOL --gcov-executable "llvm-cov gcov")
else()
set(MADS_GCOV_TOOL)
endif()
add_custom_target(coverage
COMMAND ${CMAKE_CTEST_COMMAND} --test-dir ${CMAKE_BINARY_DIR} --output-on-failure
COMMAND ${GCOVR_EXECUTABLE} --config ${CMAKE_SOURCE_DIR}/gcovr.cfg
--object-directory ${CMAKE_BINARY_DIR} ${MADS_GCOV_TOOL}
--html-details ${CMAKE_BINARY_DIR}/coverage_html/index.html
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
COMMENT "Running tests and generating coverage report"
VERBATIM
)
else()
message(WARNING "gcovr not found: the 'coverage' target will not be available")
endif()
endif()

if(MADS_BUILD_APPS)
include(FetchContent)
FetchContent_Populate(plugin
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

[Documentation](https://mads-net.github.io) · [Compiling](COMPILE.md) · [Changelog](CHANGES.md) · [License](#license)

![C++20](https://img.shields.io/badge/C%2B%2B-20-blue) ![ZeroMQ](https://img.shields.io/badge/transport-ZeroMQ-orange) [![License: Apache 2.0](https://img.shields.io/badge/license-Apache%202-green)](http://www.apache.org/licenses/LICENSE-2.0)
![C++20](https://img.shields.io/badge/C%2B%2B-20-blue) ![ZeroMQ](https://img.shields.io/badge/transport-ZeroMQ-orange) [![codecov](https://codecov.io/gh/pbosetti/MADS/graph/badge.svg)](https://codecov.io/gh/pbosetti/MADS) [![License: Apache 2.0](https://img.shields.io/badge/license-Apache%202-green)](http://www.apache.org/licenses/LICENSE-2.0)

</div>

Expand Down
99 changes: 99 additions & 0 deletions RUNTIME.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# Run state and agent lifecycle

## Context

Since MADS v2.4.0, the "keep going" state that drives `Mads::Agent::loop()` is
owned by a dedicated object, `Mads::Runtime`, instead of a single
process-global flag. This page explains the model, how to control agent
lifecycles with it, and how the deprecated `Mads::running` flag maps onto it.

Historically, all loops in a process watched one global `Mads::running`
atomic, and `Agent::shutdown()`/`Agent::disconnect()` cleared it. That made
instance-level teardown mutate process-level state, with surprising effects
when a process hosted more than one agent: destroying one agent silently
ended every other agent's `loop()`, froze their last-known-value drain
threads on stale data, could arm another agent's watchdog into a forced
process exit, and a disconnected agent could never loop again.

## The model

There are three levels of run state, from narrowest to widest:

1. **Agent** — `Agent::shutdown()` and `Agent::disconnect()` raise a stop
request that affects *only that agent*. `Agent::connect()` clears it, so
an agent can be disconnected, reconfigured, reconnected, and looped again.
2. **Runtime (group)** — every agent references a `Mads::Runtime`; by default
each agent owns its own. Calling `runtime->stop()` ends the loops of every
agent attached to that Runtime. Attach several agents to one Runtime with
`Agent::set_runtime()` (before connecting) to stop them as a group.
3. **Process** — `Mads::Runtime::stop_process()` makes *every* Runtime's
`running()` report false. This is what the SIGINT/SIGTERM handlers
installed by `Agent::loop()` use, and what the remote-control `shutdown`
and `restart` commands request. A process-wide stop cannot be undone by
`Runtime::reset()`.

`Agent::loop()`, the last-known-value drain thread, and the threaded remote
control all keep going only while the agent's Runtime is running *and* no
per-agent stop was requested.

## Typical usage

A single-agent program needs no explicit Runtime management at all:

```cpp
Mads::Agent agent("my_agent", settings_uri);
agent.init();
agent.connect();
agent.loop([&]() {
// ... publish/receive ...
return std::chrono::milliseconds(100);
});
agent.shutdown(); // stops this agent only
```

To stop a loop from inside the lambda, stop the agent's own Runtime:

```cpp
agent.loop([&]() -> std::chrono::nanoseconds {
if (done()) agent.runtime()->stop();
return std::chrono::nanoseconds(0);
});
```

Two independent agents in one process no longer interfere: each has its own
Runtime, so destroying or disconnecting one leaves the other's loop, drain
thread, and watchdog untouched. If they *should* stop together, group them:

```cpp
Mads::Agent source("source", uri), sink("sink", uri);
sink.set_runtime(source.runtime()); // one shared group
// ...
source.runtime()->stop(); // stops both
```

For an orderly full-process shutdown from your own code (equivalent to
Ctrl-C):

```cpp
Mads::Runtime::stop_process();
```

## Migrating from `Mads::running`

`Mads::running` still exists as a **deprecated alias of the process-wide run
flag**, so existing code compiles and behaves as before — writing `false`
stops every agent in the process, and reading it reports the process-wide
state. Using it raises a compile-time deprecation warning. Replace it as
follows:

| Legacy | Replacement |
|---|---|
| `Mads::running = false;` (stop everything) | `Mads::Runtime::stop_process();` |
| `Mads::running = false;` (stop one agent's loop) | `agent.runtime()->stop();` |
| `while (Mads::running) { ... }` (app main loop) | `while (Mads::Runtime::process_running()) { ... }` |
| `while (Mads::running) { ... }` (per-agent thread) | `while (agent.runtime()->running()) { ... }` |

One behavioral difference is intentional: agent teardown no longer clears the
process-wide flag. Code that relied on "destroying an agent ends my own
`while (Mads::running)` loop" must now stop the process (or a shared
Runtime) explicitly.
35 changes: 35 additions & 0 deletions codecov.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
coverage:
precision: 1
round: down
range: "60...85"
status:
project:
default:
target: 80%
threshold: 2%
informational: false
patch:
default:
target: 70%
informational: true

# Curated coverage denominator: keep in lockstep with the excludes in gcovr.cfg
ignore:
- "vendors/**"
- "tests/**"
- "smoke_test/**"
- "tools/**"
- "rust/**"
- "src/stb_image.h"
- "src/service_discovery.cpp"
- "src/https_client*"
- "src/mongo_fetch.*"
- "src/metadata.hpp"
- "src/keypress.hpp"
- "src/image.hpp"
- "src/dummy.hpp"
- "src/logger.hpp"
- "src/TerminalLogoRenderer.hpp"
- "src/agent_app.hpp"
- "src/main/*.cpp"
- "src/plugin/**"
1 change: 1 addition & 0 deletions doc/Doxyfile
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ INPUT = README.md \
CHANGES.md \
COMPILE.md \
CRYPTO.md \
RUNTIME.md \
src \
rust/README.md
RECURSIVE = YES
Expand Down
24 changes: 24 additions & 0 deletions gcovr.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Coverage report configuration (used by the `coverage` CMake target and CI).
# The exclude list below defines the curated coverage denominator and must be
# kept in lockstep with the `ignore` list in codecov.yml.
root = .
filter = src/
# Vendored third-party code
exclude = src/stb_image.h
# Network / DB / hardware-bound units that cannot run in unit tests
exclude = src/service_discovery.cpp
exclude = src/https_client.*
exclude = src/mongo_fetch.*
exclude = src/metadata.hpp
exclude = src/keypress.hpp
exclude = src/image.hpp
exclude = src/dummy.hpp
exclude = src/logger.hpp
exclude = src/TerminalLogoRenderer.hpp
exclude = src/agent_app.hpp
# Executable glue and runtime-loaded plugins (integration-tested via smoke_test)
exclude = src/main/.*\.cpp
exclude = src/plugin/.*
print-summary = yes
xml = coverage.xml
xml-pretty = yes
Loading
Loading