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
9 changes: 4 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ jobs:
run: cmake --preset debug

- name: Build and test
run: cmake --build --preset debug --target junit ExampleTests SolidSyslogExample SolidSyslogThreadedExample
run: cmake --build --preset debug --target junit ExampleTests SolidSyslogThreadedExample

- name: Run example tests
run: cd build/debug && ./Tests/Example/ExampleTests -ojunit -k ExampleTests
Expand All @@ -56,7 +56,6 @@ jobs:
with:
name: solid-syslog-examples
path: |
build/debug/Example/SolidSyslogExample
build/debug/Example/SolidSyslogThreadedExample
retention-days: 1
compression-level: 0
Expand Down Expand Up @@ -622,8 +621,8 @@ jobs:
name: solid-syslog-examples
path: build/debug/Example/

- name: Make example binaries executable
run: chmod +x build/debug/Example/SolidSyslogExample build/debug/Example/SolidSyslogThreadedExample
- name: Make example binary executable
run: chmod +x build/debug/Example/SolidSyslogThreadedExample

- name: Prepare BDD output directory
run: mkdir -p Bdd/junit && chmod 777 Bdd/junit
Expand Down Expand Up @@ -678,7 +677,7 @@ jobs:
run: cmake --preset debug

- name: Build and test
run: cmake --build --preset debug --target junit ExampleTests SolidSyslogExample SolidSyslogThreadedExample
run: cmake --build --preset debug --target junit ExampleTests SolidSyslogThreadedExample

build-freertos-target:
runs-on: ubuntu-latest
Expand Down
2 changes: 1 addition & 1 deletion Bdd/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ same feature files run on every target — the active runner is selected by the

| `BDD_TARGET` | Example-under-test | Oracle |
|---|---|---|
| `linux` (default) | `build/debug/Example/SolidSyslogExample` (native subprocess) | `syslog-ng-linux` |
| `linux` (default) | `build/debug/Example/SolidSyslogThreadedExample` (native subprocess) | `syslog-ng-linux` |
| `windows` | `build/msvc-debug/Example/Debug/SolidSyslogExample.exe` (native subprocess) | `otelcol-contrib` |
| `freertos` | `build/freertos-cross/Example/FreeRtos/SingleTask/SolidSyslogFreeRtosSingleTask.elf` driven through `qemu-system-arm` | `syslog-ng-freertos` |

Expand Down
2 changes: 1 addition & 1 deletion Bdd/features/environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ def before_all(context):
context.target = os.environ.get("BDD_TARGET", "linux").lower()

default_binaries = {
"linux": "build/debug/Example/SolidSyslogExample",
"linux": "build/debug/Example/SolidSyslogThreadedExample",
"windows": "build/msvc-debug/Example/Debug/SolidSyslogExample.exe",
"freertos": (
"build/freertos-cross/Example/FreeRtos/SingleTask/"
Expand Down
102 changes: 42 additions & 60 deletions Bdd/features/steps/syslog_steps.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,11 @@ def per_transport_log(context, transport):
return PER_TRANSPORT_LOG_OTEL[transport]
return PER_TRANSPORT_LOG_SYSLOG_NG[transport]

# POSIX-only paths used by the @buffered/threaded scenarios. The cross-platform
# scenarios use context.example_binary / context.received_log instead, set in
# environment.before_all from EXAMPLE_BINARY / RECEIVED_LOG / ORACLE_FORMAT.
THREADED_BINARY = "build/debug/Example/SolidSyslogThreadedExample"
# Paths used by the threaded/buffered scenarios. context.example_binary
# (set in environment.before_all from EXAMPLE_BINARY / per-target default) is
# the single binary path on every runner now — S24.04 collapsed Linux onto the
# Threaded binary, matching Windows (single buffered example since S13.20) and
# FreeRTOS (single buffered example since S08.04).
SYSLOG_NG_CTL = "/var/lib/syslog-ng/syslog-ng.ctl"
SYSLOG_NG_CONF = "Bdd/syslog-ng/syslog-ng.conf"
SYSLOG_NG_FULL_CONF = "Bdd/syslog-ng/syslog-ng-full.conf"
Expand Down Expand Up @@ -386,35 +387,42 @@ def wait_for_messages(context, expected_messages):
context.message_count = len(context.all_lines)


def run_example(context, extra_args=None, binary=None, expected_messages=1):
"""Run the example via the prompt protocol — works for both NullBuffer
(synchronous send, Linux SingleTask) and CircularBuffer + service thread
(asynchronous, Windows buffered example post-S13.18).
def run_example(context, extra_args=None, expected_messages=1):
"""Run the example via the prompt protocol.

Every supported runner ships a single buffered example binary now —
Linux (pthread + PosixMessageQueueBuffer, S24.04), Windows (Win32 +
CircularBuffer, S13.20), FreeRTOS-on-QEMU (Service task +
CircularBuffer, S08.04) — so context.example_binary is the only
binary path on any target.

Pin --app-name "SolidSyslogExample" on Linux/Windows so the RFC 5424
APP-NAME field stays runner-independent. Without it, Linux records
would carry the binary basename "SolidSyslogThreadedExample" and
Windows would carry "SolidSyslogExample", breaking the
`the app name is "SolidSyslogExample"` assertions in the
platform-agnostic features. FreeRTOS hardcodes the same string in
Example/FreeRtos/SingleTask/main.c and doesn't accept --app-name, so
skip the flag on that runner.

The pre-S13.18 implementation wrote `send N\\nquit\\n` upfront via
process.communicate() and relied on NullBuffer's synchronous Send to
guarantee delivery before exit. That assumption broke on Windows once
SolidSyslogExample.exe became buffered — `quit` could land before the
service thread had drained, losing the UDP packet. The prompt protocol
guarantee delivery before exit. That assumption broke once the
example became buffered — `quit` could land before the service
thread had drained, losing the UDP packet. The prompt protocol
coordinates around it: wait for the oracle to confirm receipt before
sending `quit`.
"""
binary = binary or context.example_binary
_run_with_prompt_protocol(context, binary, "Example", extra_args, expected_messages)


def _run_with_prompt_protocol(context, binary, label, extra_args, expected_messages):
"""Run a binary that speaks the SolidSyslog> prompt protocol.

Shared body for run_threaded_example (Linux pthread binary) and
run_buffered_example (cross-platform — Linux pthread or Windows Win32
threaded). The protocol is: wait initial prompt, send "send N", wait
next prompt, wait until oracle has confirmed N messages, then "quit".
"""
binary = context.example_binary
assert os.path.exists(binary), (
f"{label} binary not found at {binary} — build with cmake first"
f"Example binary not found at {binary} — build with cmake first"
)

if context.target != "freertos":
extra_args = list(extra_args or [])
if "--app-name" not in extra_args:
extra_args += ["--app-name", "SolidSyslogExample"]

process = spawn_example_process(context, extra_args=extra_args, binary=binary)
context.example_pid = process.pid

Expand All @@ -431,7 +439,7 @@ def _run_with_prompt_protocol(context, binary, label, extra_args, expected_messa
# binaries exit cleanly on `quit` and their return code is
# meaningful.
assert returncode in (0, None), (
f"{label} failed with exit code {returncode}"
f"Example failed with exit code {returncode}"
)
finally:
# Don't let an intermediate exception leak the helper into later
Expand All @@ -441,34 +449,6 @@ def _run_with_prompt_protocol(context, binary, label, extra_args, expected_messa
process.wait(timeout=5)


def run_threaded_example(context, extra_args=None, expected_messages=1):
"""Run the Linux pthread-driven Threaded example via the prompt protocol.
Used for features that are pthread-specific (file store, switching,
syslog-ng reload), which stay Linux-only and keep "the threaded example"
prose in their .feature files.
"""
_run_with_prompt_protocol(
context, THREADED_BINARY, "Threaded example", extra_args, expected_messages
)


def run_buffered_example(context, extra_args=None, expected_messages=1):
"""Run the buffered example via the prompt protocol — cross-platform.

Linux runner (syslog-ng oracle): the pthread-driven Threaded example.
Windows runner (OTel oracle): the Win32-thread-driven example wired by
S13.18 — context.example_binary already points at it.
FreeRTOS-on-QEMU runner (syslog-ng oracle): the SingleTask example is
itself buffered (CircularBuffer + FreeRtosMutex + Service task, S08.04)
— context.example_binary already points at the .elf. Lets the same
.feature scenario exercise the buffering path on all three runners.
"""
binary = THREADED_BINARY if context.target == "linux" else context.example_binary
_run_with_prompt_protocol(
context, binary, "Buffered example", extra_args, expected_messages
)


def syslog_ng_reload():
"""Send RELOAD to syslog-ng via its Unix control socket."""
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock:
Expand Down Expand Up @@ -530,8 +510,10 @@ def build_threaded_command(context, transport, no_sd=False):
Linux runner (syslog-ng oracle): the pthread-driven Threaded example.
Windows runner (OTel oracle): the unified SolidSyslogExample.exe — its
BlockStore + SwitchingSender wiring (S13.20) accepts the same flags.
Both resolve through context.example_binary now that S24.04 retired the
Linux SingleTask binary.
"""
binary = THREADED_BINARY if context.oracle_format == "syslog-ng" else context.example_binary
binary = context.example_binary
assert os.path.exists(binary), (
f"Threaded binary not found at {binary} — build with cmake first"
)
Expand Down Expand Up @@ -778,32 +760,32 @@ def step_example_sends_with_transport(context, transport):

@when("the threaded example sends a syslog message")
def step_threaded_sends_message(context):
run_threaded_example(context)
run_example(context)


@when("the threaded example sends a syslog message with transport {transport}")
def step_threaded_sends_with_transport(context, transport):
run_threaded_example(context, ["--transport", transport])
run_example(context, ["--transport", transport])


@when("the threaded example sends {count:d} syslog messages")
def step_threaded_sends_multiple(context, count):
run_threaded_example(context, expected_messages=count)
run_example(context, expected_messages=count)


@when("the buffered example sends a syslog message")
def step_buffered_sends_message(context):
run_buffered_example(context)
run_example(context)


@when("the buffered example sends a syslog message with transport {transport}")
def step_buffered_sends_with_transport(context, transport):
run_buffered_example(context, ["--transport", transport])
run_example(context, ["--transport", transport])


@when("the buffered example sends {count:d} syslog messages")
def step_buffered_sends_multiple(context, count):
run_buffered_example(context, expected_messages=count)
run_example(context, expected_messages=count)


@when("the example program sends a message with facility {facility:d} and severity {severity:d}")
Expand Down
12 changes: 5 additions & 7 deletions Bdd/features/steps/target_driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,13 +70,11 @@ def spawn_example_process(context, extra_args=None, binary=None):
commands instead — they are delivered via apply_extra_args after
the prompt is up.

binary defaults to context.example_binary (the standard SingleTask
binary or its FreeRTOS .elf equivalent); callers that drive a
different example (e.g. the Linux Threaded binary) pass it
explicitly. On FreeRTOS, the SingleTask .elf is itself buffered
(CircularBuffer + FreeRtosMutex + Service task, S08.04) so it
serves both the @buffered scenarios and the plain single-message
path — there is no separate FreeRTOS threaded binary.
binary defaults to context.example_binary. Each supported runner
ships a single buffered example binary now — Linux's pthread
Threaded example (S24.04), Windows's Win32 buffered example
(S13.20), and the FreeRTOS CircularBuffer + Service-task .elf
(S08.04) — so callers don't override the default in normal use.
"""
target = getattr(context, "target", "linux")
if binary is None:
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ Coverage report: `cmake --preset coverage && cmake --build --preset coverage --t
Core/Interface/ — Public headers of the core library. No implementation. This is the API boundary.
Core/Source/ — Core library implementation. Compiled into a static library.
Platform/ — Platform-specific code (Posix, Windows, OpenSsl) — each a subfolder with its own Interface/ and Source/.
Example/ — Example applications (Common, SingleTask, Threaded).
Example/ — Example applications (Common, Threaded, Windows, FreeRtos).
Tests/ — CppUTest unit tests. Never link production code directly; always via the library.
Tests/Support/ — PosixFakes static lib (SocketFake, ClockFake) — shared across test executables.
Tests/Example/ — Example code unit tests (ExampleTests executable).
Expand Down
65 changes: 65 additions & 0 deletions DEVLOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6036,3 +6036,68 @@ tag in the same pass.
`SOLIDSYSLOG_MAX_MESSAGE_SIZE` / SD-count knobs in the resource-sizing
backlog? Trivial to change later, but worth flagging for the resource-
sizing epic when it lands.

## 2026-05-11 — S24.04 retire Linux SingleTask binary

### Decisions

- **Linux BDD now drives the Threaded binary by default.**
`environment.before_all`'s `BDD_TARGET=linux` default flipped from
`build/debug/Example/SolidSyslogExample` to
`build/debug/Example/SolidSyslogThreadedExample`. The Threaded
binary's SwitchingSender + BlockStore wiring is a strict superset of
what the SingleTask NullBuffer binary covered after S15, so the
cross-platform `the example program sends ...` scenarios run
unchanged. Windows BDD already collapsed to one binary in S13.20,
FreeRTOS in S08.04 slice 2 — Linux was the last platform carrying
two.
- **Pin `--app-name "SolidSyslogExample"` in `run_example` for
non-FreeRTOS targets.** Without it Linux records would carry the
basename `SolidSyslogThreadedExample` and Windows would carry
`SolidSyslogExample`, breaking the platform-agnostic
`the app name is "SolidSyslogExample"` assertions. FreeRTOS
hardcodes the same string in its example main and has no `getopt`
port, so skip the injection on that runner. Lighter touch than
rewriting the feature lines and keeps `SolidSyslogExample` as the
user-visible APP-NAME until S24.05 finishes the rename pass.
- **Collapsed `run_example` / `run_threaded_example` /
`run_buffered_example` into a single helper.** The three feature
phrasings — *the example program*, *the threaded example*, *the
buffered example* — stay distinct in `.feature` prose so the
scenario intent is still readable, but all three Python decorators
now dispatch to `run_example`. Same for `build_threaded_command`,
which dropped its `oracle_format` branch. `THREADED_BINARY`
constant gone.
- **Pre-flip audit confirmed no implicit-synchronous-send fragility
in `run_example` callers.** Every caller already routes through
`_run_with_prompt_protocol` → `wait_for_messages`, which polls
oracle-receipt before issuing `quit`. NullBuffer's synchronous-send
semantics were never load-bearing on the assertions; the prompt
protocol coordinates the buffered drain. Confirmed locally by
re-running the Linux BDD suite (`not @wip and not @windows_wip and
not @freertoswip and not @no_rtc`): 44 scenarios pass.
- **Deleted `Example/SingleTask/` and
`Tests/Example/SolidSyslogExampleTest.cpp`** plus the matching
CMake entries and CI workflow references (`SolidSyslogExample`
target name, artifact path, `chmod +x` step). Existing
`analyze-format` / `analyze-tidy` / `analyze-cppcheck` / `sanitize`
/ `coverage` (99.5% lines, 98.9% functions) presets all green
locally.

### Deferred

- Renaming or moving the Threaded binary out of `Example/` — that's
S24.05 (issue #333), which depends on this story landing first.
- DEVLOG and historical blog references to the SingleTask binary
remain untouched per *Never rewrite history*. Live docs
(`README.md`, `Bdd/README.md`, `docs/bdd.md`, `CLAUDE.md`) updated
to reflect the post-S24.04 layout.

### Open questions

- None — the residual `SolidSyslogExample` string mentions in the
repo are all either the Windows binary's `OUTPUT_NAME`, the
hardcoded `OriginSdConfig.software` value, the FreeRTOS app-name
global, or the `--app-name` pin in `run_example`. They are not
references to the (now-deleted) SingleTask binary and S24.05's
rename pass will revisit them.
11 changes: 3 additions & 8 deletions Example/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,12 @@ if(SOLIDSYSLOG_POSIX)
Common/ExampleSwitchConfig.c
)

# Single-task example: NullBuffer, direct send, bare-metal model
add_executable(SolidSyslogExample SingleTask/main.c SingleTask/SolidSyslogExample.c ${COMMON_SOURCES})
target_link_libraries(SolidSyslogExample PRIVATE ${PROJECT_NAME})
target_include_directories(SolidSyslogExample PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/SingleTask
${CMAKE_CURRENT_SOURCE_DIR}/Common
)

# Threaded example: PosixMessageQueueBuffer, two pthreads (requires POSIX threads and mqueue).
# TLS transport is selected at link time via Common/ExampleTlsSender_<backend>.c
# — OpenSSL+PosixTcp when SOLIDSYSLOG_OPENSSL, nil-object stub otherwise.
# This is the only Linux example binary since S24.04 retired the
# SingleTask + NullBuffer variant — its SwitchingSender + BlockStore
# wiring is a strict superset of what SingleTask used to provide.
set(THREADED_SOURCES Threaded/main.c ${COMMON_SOURCES})
if(SOLIDSYSLOG_OPENSSL)
list(APPEND THREADED_SOURCES
Expand Down
Loading
Loading