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)