diff --git a/.clang-tidy b/.clang-tidy
new file mode 100644
index 00000000..4d501d46
--- /dev/null
+++ b/.clang-tidy
@@ -0,0 +1,30 @@
+# *******************************************************************************
+# Copyright (c) 2026 Contributors to the Eclipse Foundation
+#
+# See the NOTICE file(s) distributed with this work for additional
+# information regarding copyright ownership.
+#
+# This program and the accompanying materials are made available under the
+# terms of the Apache License Version 2.0 which is available at
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# SPDX-License-Identifier: Apache-2.0
+# *******************************************************************************
+# Local clang-tidy overrides for the score_time module.
+#
+# The S-CORE baseline from @score_cpp_policies//clang_tidy:.clang-tidy is
+# applied first; settings here are layered on top per clang-tidy's standard
+# merge rules. Only module-specific suppressions belong here — do not remove
+# baseline checks.
+
+# Only analyze headers inside the score/ source tree. Without this,
+# clang-tidy picks up warnings from externally provided headers
+# (e.g. toolchain redacted_dates.h) that we cannot fix.
+HeaderFilterRegex: 'score/'
+
+# Suppress the builtin-macro-redefined diagnostic emitted by the LLVM
+# toolchain's redacted_dates.h (which redefines __DATE__/__TIME__ for
+# deterministic builds). This is injected by the toolchain via -include
+# and is not user code, so suppressing it here is appropriate.
+Checks: >-
+ -clang-diagnostic-builtin-macro-redefined
diff --git a/BUILD b/BUILD
index facb8638..43338580 100644
--- a/BUILD
+++ b/BUILD
@@ -81,6 +81,8 @@ exports_files(
[
# Used by the @score_tooling coverage reporter to locate the workspace root.
"MODULE.bazel",
+ # Local clang-tidy overrides, layered on top of the S-CORE baseline.
+ ".clang-tidy",
],
)
diff --git a/MODULE.bazel b/MODULE.bazel
index 55354eed..7b2531ba 100644
--- a/MODULE.bazel
+++ b/MODULE.bazel
@@ -214,4 +214,23 @@ pip.parse(
python_version = PYTHON_VERSION,
requirements_lock = "//third_party/codeql:requirements_lock.txt",
)
-use_repo(pip, "codeql_coding_standards_pip_hub")
+
+# To regenerate the lock file after editing requirements.txt:
+# bazel run //score/time_daemon/tests/component_tests:requirements.update
+pip.parse(
+ hub_name = "pip_time_daemon_venv",
+ python_version = PYTHON_VERSION,
+ requirements_lock = "//score/time_daemon/tests/component_tests:requirements.txt.lock",
+)
+use_repo(pip, "codeql_coding_standards_pip_hub", "pip_time_daemon_venv")
+
+bazel_dep(name = "rules_cc", version = "0.2.17", dev_dependency = True)
+
+## Component Integration Test framework
+
+bazel_dep(name = "score_test_scenarios", version = "0.4.1", dev_dependency = True)
+git_override(
+ module_name = "score_test_scenarios",
+ commit = "fec712d490ff5d46fae91332bafac4777b300026", # tag v0.4.1
+ remote = "https://github.com/eclipse-score/testing_tools.git",
+)
diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock
index dd66805d..ffc01233 100644
--- a/MODULE.bazel.lock
+++ b/MODULE.bazel.lock
@@ -302,7 +302,8 @@
"https://bcr.bazel.build/modules/mbedtls/3.6.0/MODULE.bazel": "8e380e4698107c5f8766264d4df92e36766248447858db28187151d884995a09",
"https://bcr.bazel.build/modules/mbedtls/3.6.0/source.json": "1dbe7eb5258050afcc3806b9d43050f71c6f539ce0175535c670df606790b30c",
"https://bcr.bazel.build/modules/nlohmann_json/3.11.3/MODULE.bazel": "87023db2f55fc3a9949c7b08dc711fae4d4be339a80a99d04453c4bb3998eefc",
- "https://bcr.bazel.build/modules/nlohmann_json/3.11.3/source.json": "296c63a90c6813e53b3812d24245711981fc7e563d98fe15625f55181494488a",
+ "https://bcr.bazel.build/modules/nlohmann_json/3.12.0/MODULE.bazel": "21f19a4479e994c1546cf6f10c65d2fa464cd95f49eebad98dc5bac49c801dab",
+ "https://bcr.bazel.build/modules/nlohmann_json/3.12.0/source.json": "6bf17b358c467effad70c02ab43e2d65939d740f667157397f583435909cfae1",
"https://bcr.bazel.build/modules/nlohmann_json/3.6.1/MODULE.bazel": "6f7b417dcc794d9add9e556673ad25cb3ba835224290f4f848f8e2db1e1fca74",
"https://bcr.bazel.build/modules/opencensus-cpp/0.0.0-20230502-50eb5de/MODULE.bazel": "02201d2921dadb4ec90c4980eca4b2a02904eddcf6fa02f3da7594fb7b0d821c",
"https://bcr.bazel.build/modules/opencensus-cpp/0.0.0-20230502-50eb5de/source.json": "f50efc07822f5425bd1d3e40e977484f9c0142463052717d40ec85cd6744243e",
@@ -804,6 +805,7 @@
"https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/libpfm/4.11.0/MODULE.bazel": "not found",
"https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/mbedtls/3.6.0/MODULE.bazel": "not found",
"https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/nlohmann_json/3.11.3/MODULE.bazel": "not found",
+ "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/nlohmann_json/3.12.0/MODULE.bazel": "not found",
"https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/nlohmann_json/3.6.1/MODULE.bazel": "not found",
"https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/opencensus-cpp/0.0.0-20230502-50eb5de/MODULE.bazel": "not found",
"https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/opencensus-proto/0.4.1/MODULE.bazel": "not found",
@@ -1038,6 +1040,7 @@
"https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_crates/0.0.11/MODULE.bazel": "ef4590c417955837ce12549c6de26e008cb2957fe481844a45ae5f3d66e21211",
"https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_crates/0.0.11/source.json": "6b532c133e072aa45bab711afb5d911afe7e972c10702ed8c45c79b4cdd0dfcd",
"https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_crates/0.0.6/MODULE.bazel": "da72d24b2afb4456377f7ee13d0d95fb6bfc70dbfb949c7b8676618e661edf61",
+ "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_crates/0.0.7/MODULE.bazel": "4b08e2124d1627b08a9a5ce86e3693fe6eea302d0aad98c05216d26da3fced89",
"https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_crates/0.0.9/MODULE.bazel": "8f581e0a658a6dab149f381d783443cb00b559f4e9623956f8ff3de06108c550",
"https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_dash_license_checker/0.1.1/MODULE.bazel": "76681dbd2d45b5c540869a2337174086c56c54953aab1d02cd878b59d31d13a5",
"https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_devcontainer/1.11.0/MODULE.bazel": "fb6041c96e949a151307b4690ee81cf5d9254d6bf79540184b16567b04397171",
diff --git a/score/time_daemon/src/application/svt/BUILD b/score/time_daemon/src/application/svt/BUILD
index 44f97731..1d9f025f 100644
--- a/score/time_daemon/src/application/svt/BUILD
+++ b/score/time_daemon/src/application/svt/BUILD
@@ -27,7 +27,10 @@ load("@score_baselibs//score/language/safecpp:toolchain_features.bzl", "COMPILER
"svt_handler.h",
],
features = COMPILER_WARNING_FEATURES,
- visibility = ["//score/time_daemon/src/application:__pkg__"],
+ visibility = [
+ "//score/time_daemon/src/application:__pkg__",
+ "//score/time_daemon/tests/test_scenarios/cpp:__pkg__",
+ ],
deps = [
"//score/time_daemon/src/application:timebase_handler",
"//score/time_daemon/src/application/job_runner",
diff --git a/score/time_daemon/tests/component_tests/BUILD b/score/time_daemon/tests/component_tests/BUILD
new file mode 100644
index 00000000..e771668a
--- /dev/null
+++ b/score/time_daemon/tests/component_tests/BUILD
@@ -0,0 +1,49 @@
+# *******************************************************************************
+# Copyright (c) 2026 Contributors to the Eclipse Foundation
+#
+# See the NOTICE file(s) distributed with this work for additional
+# information regarding copyright ownership.
+#
+# This program and the accompanying materials are made available under the
+# terms of the Apache License Version 2.0 which is available at
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# SPDX-License-Identifier: Apache-2.0
+# *******************************************************************************
+
+load("@pip_time_daemon_venv//:requirements.bzl", "all_requirements")
+load("@rules_python//python:pip.bzl", "compile_pip_requirements")
+load("@score_tooling//python_basics:defs.bzl", "score_py_pytest", "score_virtualenv")
+
+# To regenerate requirements.txt.lock after editing requirements.txt, run:
+# bazel run //score/time_daemon/tests/component_tests:requirements.update
+compile_pip_requirements(
+ name = "requirements",
+ srcs = [
+ "requirements.txt",
+ "@score_tooling//python_basics:requirements.txt",
+ ],
+ requirements_txt = "requirements.txt.lock",
+ tags = ["manual"],
+)
+
+score_virtualenv(
+ name = "python_tc_venv",
+ reqs = all_requirements,
+ venv_name = ".python_tc_venv",
+)
+
+score_py_pytest(
+ name = "time_daemon_cit",
+ srcs = glob(["tests/**/*.py"]) + ["conftest.py"],
+ args = [
+ "--cpp-target-path=$(rootpath //score/time_daemon/tests/test_scenarios/cpp:test_scenarios)",
+ ],
+ data = [
+ ":python_tc_venv",
+ "//score/time_daemon/tests/test_scenarios/cpp:test_scenarios",
+ ],
+ pytest_config = ":pytest.ini",
+ tags = ["integration"],
+ deps = all_requirements,
+)
diff --git a/score/time_daemon/tests/component_tests/README.md b/score/time_daemon/tests/component_tests/README.md
new file mode 100644
index 00000000..4e2f4dff
--- /dev/null
+++ b/score/time_daemon/tests/component_tests/README.md
@@ -0,0 +1,66 @@
+
+
+# TimeDaemon Component Integration Tests (CIT)
+
+## Scope
+
+Component integration tests verify **behaviour across multiple modules inside the
+TimeDaemon component** — wiring, data flow, IPC, and lifecycle — from the
+component's public surface. They are NOT a replacement for unit tests.
+
+| Layer | Tool | Where it lives | What it covers |
+|-------|------|----------------|----------------|
+| Unit | gtest / gmock | `score/time_daemon/src/**/*_test.cpp` | Individual classes and small modules, white-box |
+| **CIT (this layer)** | **pytest + test_scenarios binary** | `score/time_daemon/tests/component_tests/` | **Multi-module integration, cross-module data flow, IPC, lifecycle** |
+| Module integration | pytest + ITF | `score/tests/module_integration_tests/` | TimeSlave ↔ TimeDaemon across components |
+| System | pytest + ITF + QEMU / Docker | `score/tests/gptp_pipeline/` | Multi-binary end-to-end across OS targets |
+
+**Rule of thumb** — if a scenario only instantiates a single production class and
+exercises its public API, it belongs in the gtest unit layer under `src/`, not
+here. A CIT scenario should exercise **two or more production objects wired
+together** (callback chaining, message-bus topics, shared-memory IPC, full daemon
+startup, etc.).
+
+## Architecture
+
+```
+pytest (Python) test runner, assertions, reporting
+ │
+ ▼ subprocess + structured-log parsing
+test_scenarios (C++ binary) scenario runner, builds real production objects
+ │
+ ▼ factory + public API
+TimeDaemon production code classes under test
+```
+
+- Python side lives under `tests/` and subclasses `testing_utils.Scenario`.
+- C++ side lives under `../test_scenarios/cpp/` and uses the
+ `@score_test_scenarios` framework (`Scenario`, `TRACING_INFO`, CLI runner).
+- Each pytest class declares a `scenario_name` fixture that selects which
+ C++ scenario to run; results come back as exit code + structured JSON log lines
+ parsed by `testing_utils.LogContainer`.
+
+## Scenarios
+
+| Scenario name | Modules involved | What it verifies |
+|---------------|-----------------|------------------|
+| `verification.pipeline` | `GPTPStubMachine` → `SvtVerificationMachine` (3 validators) | PTP data flows correctly through the full verification pipeline |
+| `ipc.shm_roundtrip` | `SvtPublisher` ↔ `SvtReceiver` via shared memory | Shared-memory IPC round-trip preserves data integrity |
+| `daemon.lifecycle` | `SvtHandler` + all subsystems (MessageBroker, machines, IPC) | Daemon initialises and shuts down cleanly through all lifecycle states |
+
+## Running
+
+```bash
+bazel test //score/time_daemon/tests/component_tests:time_daemon_cit
+```
diff --git a/score/time_daemon/tests/component_tests/conftest.py b/score/time_daemon/tests/component_tests/conftest.py
new file mode 100644
index 00000000..24da7369
--- /dev/null
+++ b/score/time_daemon/tests/component_tests/conftest.py
@@ -0,0 +1,141 @@
+# *******************************************************************************
+# Copyright (c) 2026 Contributors to the Eclipse Foundation
+#
+# See the NOTICE file(s) distributed with this work for additional
+# information regarding copyright ownership.
+#
+# This program and the accompanying materials are made available under the
+# terms of the Apache License Version 2.0 which is available at
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# SPDX-License-Identifier: Apache-2.0
+# *******************************************************************************
+import json
+import os
+from pathlib import Path
+
+import pytest
+from testing_utils import BazelTools
+
+FAILED_CONFIGS = []
+
+
+def pytest_addoption(parser):
+ parser.addoption(
+ "--traces",
+ choices=["none", "target", "all"],
+ default="none",
+ help=(
+ "Verbosity of traces in output. "
+ '"none" - show no traces, '
+ '"target" - show traces from test code, '
+ '"all" - show all traces.'
+ ),
+ )
+ parser.addoption(
+ "--cpp-target-name",
+ type=str,
+ default="//score/time_daemon/tests/test_scenarios/cpp:test_scenarios",
+ help="C++ test scenario executable Bazel target.",
+ )
+ parser.addoption(
+ "--cpp-target-path",
+ type=Path,
+ help="Path to the pre-built C++ test scenario executable.",
+ )
+ parser.addoption(
+ "--build-scenarios",
+ action="store_true",
+ help="Build test scenario executables before running tests.",
+ )
+ parser.addoption(
+ "--build-scenarios-timeout",
+ type=float,
+ default=180.0,
+ help="Build command timeout in seconds. Default: %(default)s",
+ )
+ parser.addoption(
+ "--default-execution-timeout",
+ type=float,
+ default=10.0,
+ help="Default scenario execution timeout in seconds. Default: %(default)s",
+ )
+ parser.addoption(
+ "--bazel-config",
+ type=str,
+ default="time-x86_64-linux",
+ help="Bazel config to use when building C++ test scenarios. Default: %(default)s",
+ )
+
+
+@pytest.hookimpl(tryfirst=True)
+def pytest_sessionstart(session):
+ try:
+ if session.config.getoption("--build-scenarios"):
+ build_timeout = session.config.getoption("--build-scenarios-timeout")
+ print("Building C++ test scenarios executable...")
+ bazel_config = session.config.getoption("--bazel-config")
+ bazel_tools = BazelTools(option_prefix="cpp", build_timeout=build_timeout)
+ cpp_target_name = session.config.getoption("--cpp-target-name")
+ bazel_tools.build(cpp_target_name, f"--config={bazel_config}")
+ except Exception as e:
+ pytest.exit(str(e), returncode=1)
+
+
+def pytest_html_report_title(report):
+ report.title = "Time Daemon Component Integration Tests Report"
+
+
+def pytest_html_results_table_header(cells):
+ cells.insert(1, "
Test Input | ")
+ cells.insert(2, "Description | ")
+ cells.insert(3, "Test Scenario Name | ")
+ cells.insert(4, "Test Scenario Command | ")
+
+
+def pytest_html_results_table_row(report, cells):
+ cells.insert(
+ 1,
+ f'{json.dumps(report.input)} | ',
+ )
+ cells.insert(2, f"{report.description} | ")
+ cells.insert(3, f"{report.scenario} | ")
+ cells.insert(4, f"{report.command} | ")
+
+
+@pytest.hookimpl(hookwrapper=True)
+def pytest_runtest_makereport(item, call):
+ outcome = yield
+ report = outcome.get_result()
+ report.description = str(item.function.__doc__)
+ report.scenario = item.funcargs.get("scenario_name", "")
+ report.input = item.funcargs.get("test_config", "")
+
+ command = []
+ for token in item.funcargs.get("command", ""):
+ if " " in token:
+ command.append(f"'{token}'")
+ else:
+ command.append(token)
+ report.command = " ".join(command)
+
+ if report.failed:
+ FAILED_CONFIGS.append(
+ {
+ "nodeid": report.nodeid,
+ "command": report.command,
+ }
+ )
+
+
+def pytest_terminal_summary(terminalreporter):
+ if not FAILED_CONFIGS:
+ return
+ terminalreporter.write_sep("=", "Failed tests reproduction info")
+ terminalreporter.write_line(
+ "Run failed scenarios from the repo root working directory\n"
+ )
+ for entry in FAILED_CONFIGS:
+ terminalreporter.write_line(
+ f"{entry['nodeid']} | Run command:\n{entry['command']}\n"
+ )
diff --git a/score/time_daemon/tests/component_tests/pytest.ini b/score/time_daemon/tests/component_tests/pytest.ini
new file mode 100644
index 00000000..6041fa74
--- /dev/null
+++ b/score/time_daemon/tests/component_tests/pytest.ini
@@ -0,0 +1,22 @@
+# *******************************************************************************
+# Copyright (c) 2026 Contributors to the Eclipse Foundation
+#
+# See the NOTICE file(s) distributed with this work for additional
+# information regarding copyright ownership.
+#
+# This program and the accompanying materials are made available under the
+# terms of the Apache License Version 2.0 which is available at
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# SPDX-License-Identifier: Apache-2.0
+# *******************************************************************************
+[pytest]
+addopts = -v
+testpaths = tests
+pythonpath =
+ .
+ tests
+junit_family = xunit1
+filterwarnings =
+ ignore:record_property is incompatible with junit_family:pytest.PytestWarning
+ ignore:record_xml_attribute is an experimental feature:pytest.PytestExperimentalApiWarning
diff --git a/score/time_daemon/tests/component_tests/requirements.txt b/score/time_daemon/tests/component_tests/requirements.txt
new file mode 100644
index 00000000..3b67174f
--- /dev/null
+++ b/score/time_daemon/tests/component_tests/requirements.txt
@@ -0,0 +1,4 @@
+psutil
+pytest-metadata
+pytest-env
+testing-utils @ git+https://github.com/eclipse-score/testing_tools.git@a2f9cded3deb636f5dc800bf7a47131487119721
diff --git a/score/time_daemon/tests/component_tests/requirements.txt.lock b/score/time_daemon/tests/component_tests/requirements.txt.lock
new file mode 100644
index 00000000..e04b73f0
--- /dev/null
+++ b/score/time_daemon/tests/component_tests/requirements.txt.lock
@@ -0,0 +1,161 @@
+#
+# This file is autogenerated by pip-compile with Python 3.12
+# by the following command:
+#
+# bazel run //score/time_daemon/tests/integration_tests:requirements.update
+#
+basedpyright==1.29.2 \
+ --hash=sha256:12c49186003b9f69a028615da883ef97035ea2119a9e3f93a00091b3a27088a6 \
+ --hash=sha256:f389e2997de33d038c5065fd85bff351fbdc62fa6d6371c7b947fc3bce8d437d
+ # via -r /home/pawel/.cache/bazel/_bazel_pawel/8e37fa08ddc84c04fc3d2fc587210beb/external/score_tooling~/python_basics/requirements.txt
+iniconfig==2.1.0 \
+ --hash=sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7 \
+ --hash=sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760
+ # via
+ # -r /home/pawel/.cache/bazel/_bazel_pawel/8e37fa08ddc84c04fc3d2fc587210beb/external/score_tooling~/python_basics/requirements.txt
+ # pytest
+jinja2==3.1.6 \
+ --hash=sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d \
+ --hash=sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67
+ # via pytest-html
+markupsafe==3.0.2 \
+ --hash=sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4 \
+ --hash=sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30 \
+ --hash=sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0 \
+ --hash=sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9 \
+ --hash=sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396 \
+ --hash=sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13 \
+ --hash=sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028 \
+ --hash=sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca \
+ --hash=sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557 \
+ --hash=sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832 \
+ --hash=sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0 \
+ --hash=sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b \
+ --hash=sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579 \
+ --hash=sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a \
+ --hash=sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c \
+ --hash=sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff \
+ --hash=sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c \
+ --hash=sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22 \
+ --hash=sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094 \
+ --hash=sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb \
+ --hash=sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e \
+ --hash=sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5 \
+ --hash=sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a \
+ --hash=sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d \
+ --hash=sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a \
+ --hash=sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b \
+ --hash=sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8 \
+ --hash=sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225 \
+ --hash=sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c \
+ --hash=sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144 \
+ --hash=sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f \
+ --hash=sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87 \
+ --hash=sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d \
+ --hash=sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93 \
+ --hash=sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf \
+ --hash=sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158 \
+ --hash=sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84 \
+ --hash=sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb \
+ --hash=sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48 \
+ --hash=sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171 \
+ --hash=sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c \
+ --hash=sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6 \
+ --hash=sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd \
+ --hash=sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d \
+ --hash=sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1 \
+ --hash=sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d \
+ --hash=sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca \
+ --hash=sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a \
+ --hash=sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29 \
+ --hash=sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe \
+ --hash=sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798 \
+ --hash=sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c \
+ --hash=sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8 \
+ --hash=sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f \
+ --hash=sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f \
+ --hash=sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a \
+ --hash=sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178 \
+ --hash=sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0 \
+ --hash=sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79 \
+ --hash=sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430 \
+ --hash=sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50
+ # via jinja2
+nodejs-wheel-binaries==22.16.0 \
+ --hash=sha256:2728972d336d436d39ee45988978d8b5d963509e06f063e80fe41b203ee80b28 \
+ --hash=sha256:2fffb4bf1066fb5f660da20819d754f1b424bca1b234ba0f4fa901c52e3975fb \
+ --hash=sha256:447ad796850eb52ca20356ad39b2d296ed8fef3f214921f84a1ccdad49f2eba1 \
+ --hash=sha256:4ae3cf22138891cb44c3ee952862a257ce082b098b29024d7175684a9a77b0c0 \
+ --hash=sha256:71f2de4dc0b64ae43e146897ce811f80ac4f9acfbae6ccf814226282bf4ef174 \
+ --hash=sha256:7f526ca6a132b0caf633566a2a78c6985fe92857e7bfdb37380f76205a10b808 \
+ --hash=sha256:986b715a96ed703f8ce0c15712f76fc42895cf09067d72b6ef29e8b334eccf64 \
+ --hash=sha256:d695832f026df3a0cf9a089d222225939de9d1b67f8f0a353b79f015aabbe7e2 \
+ --hash=sha256:dbfccbcd558d2f142ccf66d8c3a098022bf4436db9525b5b8d32169ce185d99e
+ # via
+ # -r /home/pawel/.cache/bazel/_bazel_pawel/8e37fa08ddc84c04fc3d2fc587210beb/external/score_tooling~/python_basics/requirements.txt
+ # basedpyright
+packaging==25.0 \
+ --hash=sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484 \
+ --hash=sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f
+ # via
+ # -r /home/pawel/.cache/bazel/_bazel_pawel/8e37fa08ddc84c04fc3d2fc587210beb/external/score_tooling~/python_basics/requirements.txt
+ # pytest
+pluggy==1.6.0 \
+ --hash=sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3 \
+ --hash=sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746
+ # via
+ # -r /home/pawel/.cache/bazel/_bazel_pawel/8e37fa08ddc84c04fc3d2fc587210beb/external/score_tooling~/python_basics/requirements.txt
+ # pytest
+psutil==7.1.3 \
+ --hash=sha256:0005da714eee687b4b8decd3d6cc7c6db36215c9e74e5ad2264b90c3df7d92dc \
+ --hash=sha256:1068c303be3a72f8e18e412c5b2a8f6d31750fb152f9cb106b54090296c9d251 \
+ --hash=sha256:18349c5c24b06ac5612c0428ec2a0331c26443d259e2a0144a9b24b4395b58fa \
+ --hash=sha256:19644c85dcb987e35eeeaefdc3915d059dac7bd1167cdcdbf27e0ce2df0c08c0 \
+ --hash=sha256:2bdbcd0e58ca14996a42adf3621a6244f1bb2e2e528886959c72cf1e326677ab \
+ --hash=sha256:31d77fcedb7529f27bb3a0472bea9334349f9a04160e8e6e5020f22c59893264 \
+ --hash=sha256:3792983e23b69843aea49c8f5b8f115572c5ab64c153bada5270086a2123c7e7 \
+ --hash=sha256:3bb428f9f05c1225a558f53e30ccbad9930b11c3fc206836242de1091d3e7dd3 \
+ --hash=sha256:56d974e02ca2c8eb4812c3f76c30e28836fffc311d55d979f1465c1feeb2b68b \
+ --hash=sha256:6c86281738d77335af7aec228328e944b30930899ea760ecf33a4dba66be5e74 \
+ --hash=sha256:8f33a3702e167783a9213db10ad29650ebf383946e91bc77f28a5eb083496bc9 \
+ --hash=sha256:95ef04cf2e5ba0ab9eaafc4a11eaae91b44f4ef5541acd2ee91d9108d00d59a7 \
+ --hash=sha256:ad81425efc5e75da3f39b3e636293360ad8d0b49bed7df824c79764fb4ba9b8b \
+ --hash=sha256:b403da1df4d6d43973dc004d19cee3b848e998ae3154cc8097d139b77156c353 \
+ --hash=sha256:bc31fa00f1fbc3c3802141eede66f3a2d51d89716a194bf2cd6fc68310a19880 \
+ --hash=sha256:bd0d69cee829226a761e92f28140bec9a5ee9d5b4fb4b0cc589068dbfff559b1 \
+ --hash=sha256:c525ffa774fe4496282fb0b1187725793de3e7c6b29e41562733cae9ada151ee \
+ --hash=sha256:f39c2c19fe824b47484b96f9692932248a54c43799a84282cfe58d05a6449efd \
+ --hash=sha256:fac9cd332c67f4422504297889da5ab7e05fd11e3c4392140f7370f4208ded1f
+ # via -r score/time_daemon/tests/integration_tests/requirements.txt
+pytest==8.3.5 \
+ --hash=sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820 \
+ --hash=sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845
+ # via
+ # -r /home/pawel/.cache/bazel/_bazel_pawel/8e37fa08ddc84c04fc3d2fc587210beb/external/score_tooling~/python_basics/requirements.txt
+ # pytest-env
+ # pytest-html
+ # pytest-metadata
+ # pytest-repeat
+ # testing-utils
+pytest-env==1.1.5 \
+ --hash=sha256:91209840aa0e43385073ac464a554ad2947cc2fd663a9debf88d03b01e0cc1cf \
+ --hash=sha256:ce90cf8772878515c24b31cd97c7fa1f4481cd68d588419fd45f10ecaee6bc30
+ # via -r score/time_daemon/tests/integration_tests/requirements.txt
+pytest-html==4.1.1 \
+ --hash=sha256:70a01e8ae5800f4a074b56a4cb1025c8f4f9b038bba5fe31e3c98eb996686f07 \
+ --hash=sha256:c8152cea03bd4e9bee6d525573b67bbc6622967b72b9628dda0ea3e2a0b5dd71
+ # via testing-utils
+pytest-metadata==3.1.1 \
+ --hash=sha256:c8e0844db684ee1c798cfa38908d20d67d0463ecb6137c72e91f418558dd5f4b \
+ --hash=sha256:d2a29b0355fbc03f168aa96d41ff88b1a3b44a3b02acbe491801c98a048017c8
+ # via
+ # -r score/time_daemon/tests/integration_tests/requirements.txt
+ # pytest-html
+pytest-repeat==0.9.4 \
+ --hash=sha256:c1738b4e412a6f3b3b9e0b8b29fcd7a423e50f87381ad9307ef6f5a8601139f3 \
+ --hash=sha256:d92ac14dfaa6ffcfe6917e5d16f0c9bc82380c135b03c2a5f412d2637f224485
+ # via testing-utils
+# WARNING: pip install will require the following package to be hashed.
+# Consider using a hashable URL like https://github.com/jazzband/pip-tools/archive/SOMECOMMIT.zip
+testing-utils @ git+https://github.com/eclipse-score/testing_tools.git@v0.3.0
+ # via -r score/time_daemon/tests/integration_tests/requirements.txt
diff --git a/score/time_daemon/tests/component_tests/tests/__init__.py b/score/time_daemon/tests/component_tests/tests/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/score/time_daemon/tests/component_tests/tests/cit_scenario.py b/score/time_daemon/tests/component_tests/tests/cit_scenario.py
new file mode 100644
index 00000000..0fbd9fb1
--- /dev/null
+++ b/score/time_daemon/tests/component_tests/tests/cit_scenario.py
@@ -0,0 +1,53 @@
+# *******************************************************************************
+# Copyright (c) 2026 Contributors to the Eclipse Foundation
+#
+# See the NOTICE file(s) distributed with this work for additional
+# information regarding copyright ownership.
+#
+# This program and the accompanying materials are made available under the
+# terms of the Apache License Version 2.0 which is available at
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# SPDX-License-Identifier: Apache-2.0
+# *******************************************************************************
+from pathlib import Path
+
+import pytest
+from testing_utils import BazelTools, BuildTools, LogContainer, Scenario
+
+
+class TimeDaemonCitScenario(Scenario):
+ """Base class for time_daemon component integration tests."""
+
+ @pytest.fixture(scope="class")
+ def build_tools(self) -> BuildTools:
+ return BazelTools(option_prefix="cpp")
+
+ @pytest.fixture(scope="class")
+ def logs_target(self, target_path: Path, logs: LogContainer) -> LogContainer:
+ return logs.get_logs(field="target", pattern=f"{target_path.name}.*")
+
+ @pytest.fixture(scope="class")
+ def logs_info_level(self, logs_target: LogContainer) -> LogContainer:
+ return logs_target.get_logs(field="level", value="INFO")
+
+ @pytest.fixture(autouse=True)
+ def print_to_report(
+ self,
+ request: pytest.FixtureRequest,
+ logs: LogContainer,
+ logs_target: LogContainer,
+ ) -> None:
+ traces_param = request.config.getoption("--traces")
+ match traces_param:
+ case "all":
+ traces = logs
+ case "target":
+ traces = logs_target
+ case "none":
+ traces = LogContainer()
+ case _:
+ raise RuntimeError(f'Invalid "--traces" value: {traces_param}')
+
+ for trace in traces:
+ print(trace)
diff --git a/score/time_daemon/tests/component_tests/tests/daemon/__init__.py b/score/time_daemon/tests/component_tests/tests/daemon/__init__.py
new file mode 100644
index 00000000..ca5de742
--- /dev/null
+++ b/score/time_daemon/tests/component_tests/tests/daemon/__init__.py
@@ -0,0 +1,12 @@
+# *******************************************************************************
+# Copyright (c) 2026 Contributors to the Eclipse Foundation
+#
+# See the NOTICE file(s) distributed with this work for additional
+# information regarding copyright ownership.
+#
+# This program and the accompanying materials are made available under the
+# terms of the Apache License Version 2.0 which is available at
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# SPDX-License-Identifier: Apache-2.0
+# *******************************************************************************
diff --git a/score/time_daemon/tests/component_tests/tests/daemon/test_lifecycle.py b/score/time_daemon/tests/component_tests/tests/daemon/test_lifecycle.py
new file mode 100644
index 00000000..ae7a386e
--- /dev/null
+++ b/score/time_daemon/tests/component_tests/tests/daemon/test_lifecycle.py
@@ -0,0 +1,78 @@
+# *******************************************************************************
+# Copyright (c) 2026 Contributors to the Eclipse Foundation
+#
+# See the NOTICE file(s) distributed with this work for additional
+# information regarding copyright ownership.
+#
+# This program and the accompanying materials are made available under the
+# terms of the Apache License Version 2.0 which is available at
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# SPDX-License-Identifier: Apache-2.0
+# *******************************************************************************
+from typing import Any
+
+import pytest
+from cit_scenario import TimeDaemonCitScenario
+from result_code import ResultCode
+from testing_utils import LogContainer, ScenarioResult
+
+
+class TestDaemonLifecycle(TimeDaemonCitScenario):
+ """Verifies that the TimeDaemon subsystem stack can be constructed, initialized,
+ and stopped cleanly without requiring IPC or external hardware.
+
+ The scenario exercises all four component factory functions:
+ - CreateGPTPStubMachine (PTP data source)
+ - CreateSvtVerificationMachine (time verification pipeline)
+ - CreateSvtPublisher (IPC publisher)
+ - CreatePtpControlFlowDivider (control flow routing)
+
+ It also validates that SvtHandler::Initialize() wires the full MessageBroker
+ pub-sub topology, and that Stop() called from the kIdle state is a safe no-op —
+ confirming the graceful-shutdown path before any async worker has been started.
+ """
+
+ @pytest.fixture(scope="class")
+ def scenario_name(self) -> str:
+ return "daemon.lifecycle"
+
+ @pytest.fixture(scope="class")
+ def test_config(self) -> dict[str, Any]:
+ return {}
+
+ def test_scenario_exits_successfully(self, results: ScenarioResult):
+ """Scenario binary must exit with code 0 — all lifecycle phases completed.
+
+ A non-zero exit code indicates that at least one factory function threw
+ an exception, Initialize() failed, or Stop() raised an error.
+ """
+ assert results.return_code == ResultCode.SUCCESS
+ assert not results.hang
+
+ def test_lifecycle_initialization_succeeds(
+ self, results: ScenarioResult, logs_info_level: LogContainer
+ ):
+ """TRACING_INFO must confirm that Initialize() completed without error.
+
+ Verifies that CreateSvtTimebase() constructs all four subsystems and
+ SvtHandler::Initialize() successfully wires the three MessageBroker
+ pub-sub topics (in_ptp_data, raw_ptp_data, validated_ptp_data).
+ """
+ assert results.return_code == ResultCode.SUCCESS
+ assert logs_info_level.contains_log(
+ field="lifecycle_initialize_ok", value="true"
+ )
+
+ def test_lifecycle_completes(
+ self, results: ScenarioResult, logs_info_level: LogContainer
+ ):
+ """TRACING_INFO must confirm that Stop() returned cleanly from kIdle state.
+
+ Stop() must be safe to call before any subsystem has been started (i.e.
+ before RunOnce() has transitioned the handler out of kIdle). This test
+ guards against regressions where Stop() incorrectly tries to stop machines
+ that were never started.
+ """
+ assert results.return_code == ResultCode.SUCCESS
+ assert logs_info_level.contains_log(field="lifecycle_complete", value="true")
diff --git a/score/time_daemon/tests/component_tests/tests/ipc/__init__.py b/score/time_daemon/tests/component_tests/tests/ipc/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/score/time_daemon/tests/component_tests/tests/ipc/test_shm_roundtrip.py b/score/time_daemon/tests/component_tests/tests/ipc/test_shm_roundtrip.py
new file mode 100644
index 00000000..24cdbc0f
--- /dev/null
+++ b/score/time_daemon/tests/component_tests/tests/ipc/test_shm_roundtrip.py
@@ -0,0 +1,107 @@
+# *******************************************************************************
+# Copyright (c) 2026 Contributors to the Eclipse Foundation
+#
+# See the NOTICE file(s) distributed with this work for additional
+# information regarding copyright ownership.
+#
+# This program and the accompanying materials are made available under the
+# terms of the Apache License Version 2.0 which is available at
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# SPDX-License-Identifier: Apache-2.0
+# *******************************************************************************
+from typing import Any
+
+import pytest
+from cit_scenario import TimeDaemonCitScenario
+from result_code import ResultCode
+from testing_utils import LogContainer, ScenarioResult
+
+
+class TestIpcShmRoundtrip(TimeDaemonCitScenario):
+ """Verifies the end-to-end SHM IPC pipeline: SvtPublisher writes a
+ PtpTimeInfo, SvtReceiver reads back a TimeBaseSnapshot, and all key
+ fields survive the DataConverter serialisation round-trip.
+
+ The scenario exercises the production IPC path in-process:
+ 1. SvtPublisher.Init() initialises the SharedMemoryHandler.
+ 2. SvtPublisher.OnMessage() converts PtpTimeInfo → TimeBaseSnapshot
+ via DataConverter and writes it to shared memory.
+ 3. SvtReceiver.Init() opens the same shared memory region.
+ 4. SvtReceiver.Receive() reads the TimeBaseSnapshot via the seqlock.
+ 5. Field values are compared against the originals.
+
+ Input data:
+ ptp_assumed_time = 1 000 000 000 ns (1 s)
+ status = {is_synchronized=true, is_correct=true}
+ sync_fup_data.sequence_id = 42
+
+ Assertions:
+ - Scenario exits with code 0.
+ - Receive() returns a value (SHM write/read path is operational).
+ - ptp_assumed_time is preserved through the conversion.
+ - is_synchronized and is_correct are preserved.
+ - sync_fup_data.sequence_id is preserved.
+ """
+
+ @pytest.fixture(scope="class")
+ def scenario_name(self) -> str:
+ return "ipc.shm_roundtrip"
+
+ @pytest.fixture(scope="class")
+ def test_config(self) -> dict[str, Any]:
+ return {}
+
+ def test_scenario_exits_successfully(self, results: ScenarioResult):
+ """Scenario binary must exit with code 0.
+
+ A non-zero exit indicates failure during Init(), OnMessage(), or
+ an unexpected exception in the IPC pipeline.
+ """
+ assert results.return_code == ResultCode.SUCCESS
+ assert not results.hang
+
+ def test_read_succeeded(
+ self, results: ScenarioResult, logs_info_level: LogContainer
+ ):
+ """Receive() must return a value after OnMessage() writes to SHM.
+
+ SharedMemoryHandler uses a seqlock; a successful write followed by a
+ read on the same path must always yield data.
+ """
+ assert results.return_code == ResultCode.SUCCESS
+ assert logs_info_level.contains_log(field="read_succeeded", value="true")
+
+ def test_ptp_time_preserved(
+ self, results: ScenarioResult, logs_info_level: LogContainer
+ ):
+ """ptp_assumed_time (1 s = 1 000 000 000 ns) must survive the roundtrip.
+
+ DataConverter maps PtpTimeInfo.ptp_assumed_time.count() to
+ TimeBaseSnapshot.ptp_assumed_time (uint64). The nanosecond value
+ must be bit-identical after serialisation.
+ """
+ assert results.return_code == ResultCode.SUCCESS
+ assert logs_info_level.contains_log(field="ptp_time_preserved", value="true")
+
+ def test_status_preserved(
+ self, results: ScenarioResult, logs_info_level: LogContainer
+ ):
+ """is_synchronized and is_correct must both be true after the roundtrip.
+
+ DataConverter maps PtpStatus fields to TimeBaseStatus fields.
+ A correctly functioning converter must preserve all boolean flags.
+ """
+ assert results.return_code == ResultCode.SUCCESS
+ assert logs_info_level.contains_log(field="status_preserved", value="true")
+
+ def test_seq_id_preserved(
+ self, results: ScenarioResult, logs_info_level: LogContainer
+ ):
+ """sync_fup_data.sequence_id (42) must survive the roundtrip.
+
+ DataConverter maps SyncFupData.sequence_id to SyncFupSnapshot.sequence_id.
+ The uint16 value must be identical after serialisation.
+ """
+ assert results.return_code == ResultCode.SUCCESS
+ assert logs_info_level.contains_log(field="seq_id_preserved", value="true")
diff --git a/score/time_daemon/tests/component_tests/tests/result_code.py b/score/time_daemon/tests/component_tests/tests/result_code.py
new file mode 100644
index 00000000..be70d7ab
--- /dev/null
+++ b/score/time_daemon/tests/component_tests/tests/result_code.py
@@ -0,0 +1,19 @@
+# *******************************************************************************
+# Copyright (c) 2026 Contributors to the Eclipse Foundation
+#
+# See the NOTICE file(s) distributed with this work for additional
+# information regarding copyright ownership.
+#
+# This program and the accompanying materials are made available under the
+# terms of the Apache License Version 2.0 which is available at
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# SPDX-License-Identifier: Apache-2.0
+# *******************************************************************************
+
+
+class ResultCode:
+ SUCCESS = 0
+ PANIC = 101
+ SIGKILL = -9
+ SIGABRT = -6
diff --git a/score/time_daemon/tests/component_tests/tests/verification/__init__.py b/score/time_daemon/tests/component_tests/tests/verification/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/score/time_daemon/tests/component_tests/tests/verification/test_pipeline.py b/score/time_daemon/tests/component_tests/tests/verification/test_pipeline.py
new file mode 100644
index 00000000..590d2e5d
--- /dev/null
+++ b/score/time_daemon/tests/component_tests/tests/verification/test_pipeline.py
@@ -0,0 +1,76 @@
+# *******************************************************************************
+# Copyright (c) 2026 Contributors to the Eclipse Foundation
+#
+# See the NOTICE file(s) distributed with this work for additional
+# information regarding copyright ownership.
+#
+# This program and the accompanying materials are made available under the
+# terms of the Apache License Version 2.0 which is available at
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# SPDX-License-Identifier: Apache-2.0
+# *******************************************************************************
+from typing import Any
+
+import pytest
+from cit_scenario import TimeDaemonCitScenario
+from result_code import ResultCode
+from testing_utils import LogContainer, ScenarioResult
+
+
+class TestVerificationPipeline(TimeDaemonCitScenario):
+ """Verifies that PtpTimeInfo flows correctly through the full verification pipeline.
+
+ GPTPStubMachine (PTP source) is wired to SvtVerificationMachine (three-stage validator).
+ The test confirms that data produced by the stub reaches and exits the pipeline with
+ the expected status fields, exercising the SynchronizationValidator, TimeoutValidator,
+ and TimeJumpsValidator stages in a real component integration.
+ """
+
+ @pytest.fixture(scope="class")
+ def scenario_name(self) -> str:
+ return "verification.pipeline"
+
+ @pytest.fixture(scope="class")
+ def test_config(self) -> dict[str, Any]:
+ return {}
+
+ def test_scenario_exits_successfully(self, results: ScenarioResult):
+ """Scenario binary must exit with code 0 — pipeline initialised and data received."""
+ assert results.return_code == ResultCode.SUCCESS
+ assert not results.hang
+
+ def test_pipeline_reached_active_state(
+ self, results: ScenarioResult, logs_info_level: LogContainer
+ ):
+ """TRACING_INFO must confirm the pipeline processed at least one data point."""
+ assert results.return_code == ResultCode.SUCCESS
+ assert logs_info_level.contains_log(field="pipeline_active", value="true")
+
+ def test_verification_reports_synchronized(
+ self, results: ScenarioResult, logs_info_level: LogContainer
+ ):
+ """SynchronizationValidator must propagate is_synchronized=true from stub data."""
+ assert results.return_code == ResultCode.SUCCESS
+ assert logs_info_level.contains_log(field="is_synchronized", value="true")
+
+ def test_verification_reports_correct_status(
+ self, results: ScenarioResult, logs_info_level: LogContainer
+ ):
+ """Verified data must carry is_correct=true — no validator flagged an anomaly."""
+ assert results.return_code == ResultCode.SUCCESS
+ assert logs_info_level.contains_log(field="is_correct", value="true")
+
+ def test_verification_no_timeout(
+ self, results: ScenarioResult, logs_info_level: LogContainer
+ ):
+ """TimeoutValidator must not trigger a timeout on the first data point from stub."""
+ assert results.return_code == ResultCode.SUCCESS
+ assert logs_info_level.contains_log(field="is_timeout", value="false")
+
+ def test_verification_positive_timestamp(
+ self, results: ScenarioResult, logs_info_level: LogContainer
+ ):
+ """PTP assumed time must remain positive after passing through the pipeline."""
+ assert results.return_code == ResultCode.SUCCESS
+ assert logs_info_level.contains_log(field="ptp_time_positive", value="true")
diff --git a/score/time_daemon/tests/test_scenarios/cpp/BUILD b/score/time_daemon/tests/test_scenarios/cpp/BUILD
new file mode 100644
index 00000000..bf11cdef
--- /dev/null
+++ b/score/time_daemon/tests/test_scenarios/cpp/BUILD
@@ -0,0 +1,33 @@
+# *******************************************************************************
+# Copyright (c) 2026 Contributors to the Eclipse Foundation
+#
+# See the NOTICE file(s) distributed with this work for additional
+# information regarding copyright ownership.
+#
+# This program and the accompanying materials are made available under the
+# terms of the Apache License Version 2.0 which is available at
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# SPDX-License-Identifier: Apache-2.0
+# *******************************************************************************
+
+load("@rules_cc//cc:cc_binary.bzl", "cc_binary")
+
+cc_binary(
+ name = "test_scenarios",
+ srcs = glob([
+ "src/**/*.cpp",
+ "src/**/*.hpp",
+ ]),
+ includes = ["src"],
+ visibility = ["//score/time_daemon/tests/component_tests:__pkg__"],
+ deps = [
+ "//score/time_daemon/src/application/svt:svt_handler",
+ "//score/time_daemon/src/ipc:svt_publisher",
+ "//score/time_daemon/src/ipc:svt_receiver",
+ "//score/time_daemon/src/ptp_machine/stub:gptp_stub_machine",
+ "//score/time_daemon/src/verification_machine/svt:svt_verification_machine",
+ "@score_baselibs//score/mw/log:console_only_backend",
+ "@score_test_scenarios//test_scenarios_cpp",
+ ],
+)
diff --git a/score/time_daemon/tests/test_scenarios/cpp/src/daemon/lifecycle.cpp b/score/time_daemon/tests/test_scenarios/cpp/src/daemon/lifecycle.cpp
new file mode 100644
index 00000000..4a2374c2
--- /dev/null
+++ b/score/time_daemon/tests/test_scenarios/cpp/src/daemon/lifecycle.cpp
@@ -0,0 +1,63 @@
+/********************************************************************************
+ * Copyright (c) 2026 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Apache License Version 2.0 which is available at
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ ********************************************************************************/
+#include "daemon/lifecycle.hpp"
+
+#include
+
+#include
+#include
+
+#include "score/time_daemon/src/application/svt/factory.h"
+
+namespace
+{
+const std::string kTargetName{"test_scenarios::daemon::lifecycle"};
+}
+
+// Verifies that the TimeDaemon subsystem stack can be constructed, initialized,
+// and stopped cleanly. Exercises all four component factory functions
+// (GPTPStubMachine, SvtVerificationMachine, IpcPublisher, PtpControlFlowDivider)
+// and the full MessageBroker pub-sub topology wiring performed by SvtHandler::Initialize().
+// Stop() is called from the kIdle state (no async work started), confirming that
+// the graceful-shutdown path is a safe no-op before any subsystem has been started.
+class DaemonLifecycle final : public Scenario
+{
+ public:
+ ~DaemonLifecycle() final = default;
+
+ std::string name() const final
+ {
+ return "lifecycle";
+ }
+
+ void run(const std::string& /*input*/) const final
+ {
+ // Phase 1: construct — calls all four subsystem factory functions
+ auto handler = score::td::CreateSvtTimebase();
+
+ // Phase 2: initialize — wires MessageBroker pub-sub topology
+ handler->Initialize();
+
+ // Phase 3: stop from kIdle — verifies graceful shutdown without starting async workers
+ handler->Stop();
+
+ TRACING_INFO(kTargetName,
+ std::pair{std::string{"lifecycle_initialize_ok"}, std::string{"true"}},
+ std::pair{std::string{"lifecycle_complete"}, std::string{"true"}});
+ }
+};
+
+ScenarioGroup::Ptr daemon_scenario_group()
+{
+ return ScenarioGroup::Ptr{new ScenarioGroupImpl{"daemon", {std::make_shared()}, {}}};
+}
diff --git a/score/time_daemon/tests/test_scenarios/cpp/src/daemon/lifecycle.hpp b/score/time_daemon/tests/test_scenarios/cpp/src/daemon/lifecycle.hpp
new file mode 100644
index 00000000..e5f96b8f
--- /dev/null
+++ b/score/time_daemon/tests/test_scenarios/cpp/src/daemon/lifecycle.hpp
@@ -0,0 +1,20 @@
+/********************************************************************************
+ * Copyright (c) 2026 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Apache License Version 2.0 which is available at
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ ********************************************************************************/
+#ifndef SCORE_TIME_DAEMON_TESTS_TEST_SCENARIOS_DAEMON_LIFECYCLE_HPP
+#define SCORE_TIME_DAEMON_TESTS_TEST_SCENARIOS_DAEMON_LIFECYCLE_HPP
+
+#include
+
+ScenarioGroup::Ptr daemon_scenario_group();
+
+#endif // SCORE_TIME_DAEMON_TESTS_TEST_SCENARIOS_DAEMON_LIFECYCLE_HPP
diff --git a/score/time_daemon/tests/test_scenarios/cpp/src/ipc/shm_roundtrip.cpp b/score/time_daemon/tests/test_scenarios/cpp/src/ipc/shm_roundtrip.cpp
new file mode 100644
index 00000000..1ae85aba
--- /dev/null
+++ b/score/time_daemon/tests/test_scenarios/cpp/src/ipc/shm_roundtrip.cpp
@@ -0,0 +1,91 @@
+/********************************************************************************
+ * Copyright (c) 2026 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Apache License Version 2.0 which is available at
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ ********************************************************************************/
+#include "ipc/shm_roundtrip.hpp"
+
+#include
+#include
+#include
+
+#include
+#include
+
+#include "score/time_daemon/src/common/data_types/ptp_time_info.h"
+#include "score/time_daemon/src/ipc/svt/publisher/factory.h"
+#include "score/time_daemon/src/ipc/svt/receiver/factory.h"
+
+namespace
+{
+const std::string kTargetName{"test_scenarios::ipc::shm_roundtrip"};
+}
+
+class ShmRoundtrip final : public Scenario
+{
+ public:
+ ~ShmRoundtrip() final = default;
+
+ std::string name() const final
+ {
+ return "shm_roundtrip";
+ }
+
+ void run(const std::string& /*input*/) const final
+ {
+ auto publisher = score::td::CreateSvtPublisher("cit_ipc_pub");
+ auto receiver = score::td::CreateSvtReceiver();
+
+ if (!publisher->Init())
+ {
+ throw std::runtime_error{"SvtPublisher::Init() failed"};
+ }
+ if (!receiver->Init())
+ {
+ throw std::runtime_error{"SvtReceiver::Init() failed"};
+ }
+
+ score::td::PtpTimeInfo info{};
+ info.ptp_assumed_time = std::chrono::nanoseconds{1'000'000'000LL};
+ info.local_time = score::td::PtpTimeInfo::ReferenceClock::time_point{std::chrono::nanoseconds{500'000'000LL}};
+ info.rate_deviation = 0.0;
+ info.status = {true, false, false, false, true};
+ info.sync_fup_data.sequence_id = 42U;
+ info.sync_fup_data.precise_origin_timestamp = 1'000'000'000ULL;
+
+ publisher->OnMessage(info);
+
+ const auto result = receiver->Receive();
+ const bool read_ok = result.has_value();
+
+ bool ptp_time_ok = false;
+ bool status_ok = false;
+ bool seq_ok = false;
+
+ if (read_ok)
+ {
+ const auto& snap = result.value();
+ ptp_time_ok = snap.ptp_assumed_time == static_cast(1'000'000'000LL);
+ status_ok = snap.status.is_synchronized && snap.status.is_correct;
+ seq_ok = snap.sync_fup_data.sequence_id == 42U;
+ }
+
+ TRACING_INFO(kTargetName,
+ std::pair{std::string{"read_succeeded"}, std::string{read_ok ? "true" : "false"}},
+ std::pair{std::string{"ptp_time_preserved"}, std::string{ptp_time_ok ? "true" : "false"}},
+ std::pair{std::string{"status_preserved"}, std::string{status_ok ? "true" : "false"}},
+ std::pair{std::string{"seq_id_preserved"}, std::string{seq_ok ? "true" : "false"}});
+ }
+};
+
+ScenarioGroup::Ptr ipc_scenario_group()
+{
+ return ScenarioGroup::Ptr{new ScenarioGroupImpl{"ipc", {std::make_shared()}, {}}};
+}
diff --git a/score/time_daemon/tests/test_scenarios/cpp/src/ipc/shm_roundtrip.hpp b/score/time_daemon/tests/test_scenarios/cpp/src/ipc/shm_roundtrip.hpp
new file mode 100644
index 00000000..21472a3d
--- /dev/null
+++ b/score/time_daemon/tests/test_scenarios/cpp/src/ipc/shm_roundtrip.hpp
@@ -0,0 +1,20 @@
+/********************************************************************************
+ * Copyright (c) 2026 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Apache License Version 2.0 which is available at
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ ********************************************************************************/
+#ifndef SCORE_TIME_DAEMON_TESTS_TEST_SCENARIOS_IPC_SHM_ROUNDTRIP_HPP
+#define SCORE_TIME_DAEMON_TESTS_TEST_SCENARIOS_IPC_SHM_ROUNDTRIP_HPP
+
+#include
+
+ScenarioGroup::Ptr ipc_scenario_group();
+
+#endif // SCORE_TIME_DAEMON_TESTS_TEST_SCENARIOS_IPC_SHM_ROUNDTRIP_HPP
diff --git a/score/time_daemon/tests/test_scenarios/cpp/src/main.cpp b/score/time_daemon/tests/test_scenarios/cpp/src/main.cpp
new file mode 100644
index 00000000..e3bb3ba6
--- /dev/null
+++ b/score/time_daemon/tests/test_scenarios/cpp/src/main.cpp
@@ -0,0 +1,46 @@
+/********************************************************************************
+ * Copyright (c) 2026 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Apache License Version 2.0 which is available at
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ ********************************************************************************/
+#include
+#include
+#include
+
+#include
+#include
+#include
+
+#include "daemon/lifecycle.hpp"
+#include "ipc/shm_roundtrip.hpp"
+#include "verification/pipeline.hpp"
+
+static constexpr int kFrameworkError = 101;
+
+int main(int argc, char** argv)
+{
+ try
+ {
+ std::vector raw_arguments{argv, argv + argc};
+
+ ScenarioGroup::Ptr verification_group{verification_scenario_group()};
+ ScenarioGroup::Ptr daemon_group{daemon_scenario_group()};
+ ScenarioGroup::Ptr ipc_group{ipc_scenario_group()};
+ ScenarioGroup::Ptr root_group{new ScenarioGroupImpl{"root", {}, {verification_group, daemon_group, ipc_group}}};
+
+ TestContext test_context{root_group};
+ run_cli_app(raw_arguments, test_context);
+ }
+ catch (const std::exception& ex)
+ {
+ std::cerr << ex.what() << std::endl;
+ return kFrameworkError;
+ }
+}
diff --git a/score/time_daemon/tests/test_scenarios/cpp/src/verification/pipeline.cpp b/score/time_daemon/tests/test_scenarios/cpp/src/verification/pipeline.cpp
new file mode 100644
index 00000000..67689329
--- /dev/null
+++ b/score/time_daemon/tests/test_scenarios/cpp/src/verification/pipeline.cpp
@@ -0,0 +1,102 @@
+/********************************************************************************
+ * Copyright (c) 2026 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Apache License Version 2.0 which is available at
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ ********************************************************************************/
+#include "verification/pipeline.hpp"
+
+#include
+#include
+#include
+#include
+
+#include
+#include
+
+#include "score/time_daemon/src/common/data_types/ptp_time_info.h"
+#include "score/time_daemon/src/ptp_machine/stub/factory.h"
+#include "score/time_daemon/src/verification_machine/svt/factory.h"
+
+namespace
+{
+const std::string kTargetName{"test_scenarios::verification::pipeline"};
+}
+
+// Verifies that PtpTimeInfo published by GPTPStubMachine flows correctly through
+// SvtVerificationMachine (SynchronizationValidator → TimeoutValidator → TimeJumpsValidator)
+// and emerges with the expected status fields intact.
+class VerificationPipeline final : public Scenario
+{
+ public:
+ ~VerificationPipeline() final = default;
+
+ std::string name() const final
+ {
+ return "pipeline";
+ }
+
+ void run(const std::string& /*input*/) const final
+ {
+ auto ptp_machine = score::td::CreateGPTPStubMachine("cit_verif_ptp");
+ auto verifier = score::td::CreateSvtVerificationMachine("cit_verif");
+
+ std::promise verified_promise;
+ auto verified_future = verified_promise.get_future();
+
+ // Wire PTP machine output into the verification pipeline.
+ ptp_machine->SetPublishCallback([&verifier](const score::td::PtpTimeInfo& data) {
+ verifier->OnMessage(data);
+ });
+
+ // Capture the first data point that exits the verification pipeline.
+ verifier->SetPublishCallback([&verified_promise](const score::td::PtpTimeInfo& data) {
+ try
+ {
+ verified_promise.set_value(data);
+ }
+ catch (const std::future_error&)
+ {
+ // Capture only the first verified data point.
+ }
+ });
+
+ verifier->Init();
+
+ if (!ptp_machine->Init())
+ {
+ throw std::runtime_error{"GPTPStubMachine::Init() failed"};
+ }
+ ptp_machine->Start();
+
+ if (verified_future.wait_for(std::chrono::seconds(5)) != std::future_status::ready)
+ {
+ ptp_machine->Stop();
+ throw std::runtime_error{"Timed out waiting for verified PTP data through pipeline"};
+ }
+
+ ptp_machine->Stop();
+
+ const auto data = verified_future.get();
+
+ TRACING_INFO(
+ kTargetName,
+ std::pair{std::string{"pipeline_active"}, std::string{"true"}},
+ std::pair{std::string{"is_synchronized"}, std::string{data.status.is_synchronized ? "true" : "false"}},
+ std::pair{std::string{"is_correct"}, std::string{data.status.is_correct ? "true" : "false"}},
+ std::pair{std::string{"is_timeout"}, std::string{data.status.is_timeout ? "true" : "false"}},
+ std::pair{std::string{"ptp_time_positive"},
+ std::string{data.ptp_assumed_time.count() > 0 ? "true" : "false"}});
+ }
+};
+
+ScenarioGroup::Ptr verification_scenario_group()
+{
+ return ScenarioGroup::Ptr{new ScenarioGroupImpl{"verification", {std::make_shared()}, {}}};
+}
diff --git a/score/time_daemon/tests/test_scenarios/cpp/src/verification/pipeline.hpp b/score/time_daemon/tests/test_scenarios/cpp/src/verification/pipeline.hpp
new file mode 100644
index 00000000..8eda862a
--- /dev/null
+++ b/score/time_daemon/tests/test_scenarios/cpp/src/verification/pipeline.hpp
@@ -0,0 +1,20 @@
+/********************************************************************************
+ * Copyright (c) 2026 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Apache License Version 2.0 which is available at
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ ********************************************************************************/
+#ifndef SCORE_TIME_DAEMON_TESTS_TEST_SCENARIOS_VERIFICATION_PIPELINE_HPP
+#define SCORE_TIME_DAEMON_TESTS_TEST_SCENARIOS_VERIFICATION_PIPELINE_HPP
+
+#include
+
+ScenarioGroup::Ptr verification_scenario_group();
+
+#endif // SCORE_TIME_DAEMON_TESTS_TEST_SCENARIOS_VERIFICATION_PIPELINE_HPP
diff --git a/tools/lint/linters.bzl b/tools/lint/linters.bzl
index 355cb388..07d2f82c 100644
--- a/tools/lint/linters.bzl
+++ b/tools/lint/linters.bzl
@@ -21,7 +21,7 @@ load("@score_cpp_policies//clang_tidy:defs.bzl", "make_clang_tidy_aspect", "make
clang_tidy_aspect = make_clang_tidy_aspect(
binary = Label("@llvm_toolchain//:clang-tidy"),
- # No local_configs: use only the S-CORE baseline from score_cpp_policies.
+ local_configs = [Label("//:.clang-tidy")],
)
clang_tidy_test = make_clang_tidy_test(aspect = clang_tidy_aspect)