From c9d2a922e7e90f812fedf965d2de962eccdaa3be Mon Sep 17 00:00:00 2001 From: Chris Langhans Date: Mon, 27 Jul 2026 11:48:03 +0200 Subject: [PATCH 1/2] test(fit): add score_time clock integration test Add a feature integration test for the score_time clock library, which was already pinned in known_good.json but had no test coverage. Three C++ scenarios exercise the public Clock API and self-validate: - system_clock_now: reading tracks the host system clock within tolerance - steady_clock_now: two readings are monotonic - high_res_steady_clock_now: reading is non-zero and monotonic A pytest FIT case drives each scenario and asserts the exit code plus the emitted reading. Wire the scenarios into the root group and add the score_time clock deps to the cpp_test_scenarios binary. --- .../test_cases/tests/time/test_clock_now.py | 100 ++++++++++++++++++ .../test_scenarios/cpp/BUILD | 3 + .../cpp/src/internals/time/clock_log.h | 63 +++++++++++ .../test_scenarios/cpp/src/scenarios/mod.cpp | 3 +- .../time/high_res_steady_clock_now.cpp | 79 ++++++++++++++ .../cpp/src/scenarios/time/mod.cpp | 31 ++++++ .../src/scenarios/time/steady_clock_now.cpp | 75 +++++++++++++ .../src/scenarios/time/system_clock_now.cpp | 85 +++++++++++++++ 8 files changed, 438 insertions(+), 1 deletion(-) create mode 100644 feature_integration_tests/test_cases/tests/time/test_clock_now.py create mode 100644 feature_integration_tests/test_scenarios/cpp/src/internals/time/clock_log.h create mode 100644 feature_integration_tests/test_scenarios/cpp/src/scenarios/time/high_res_steady_clock_now.cpp create mode 100644 feature_integration_tests/test_scenarios/cpp/src/scenarios/time/mod.cpp create mode 100644 feature_integration_tests/test_scenarios/cpp/src/scenarios/time/steady_clock_now.cpp create mode 100644 feature_integration_tests/test_scenarios/cpp/src/scenarios/time/system_clock_now.cpp diff --git a/feature_integration_tests/test_cases/tests/time/test_clock_now.py b/feature_integration_tests/test_cases/tests/time/test_clock_now.py new file mode 100644 index 00000000000..3b157f94795 --- /dev/null +++ b/feature_integration_tests/test_cases/tests/time/test_clock_now.py @@ -0,0 +1,100 @@ +# ******************************************************************************* +# 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 fit_scenario import FitScenario, ResultCode +from testing_utils import BazelTools, BuildTools, LogContainer, ScenarioResult + +# score_time is C++ only; there is no Rust variant of the clock library. +pytestmark = pytest.mark.parametrize("version", ["cpp"], scope="class") + + +class ClockScenario(FitScenario): + """Common base for score_time clock scenarios (no scenario input required).""" + + @pytest.fixture(scope="class") + def build_tools(self, version: str) -> BuildTools: + # Consume the parametrized `version` and select the C++ scenario binary. + return BazelTools(option_prefix=version) + + @pytest.fixture(scope="class") + def test_config(self) -> dict[str, Any]: + return {} + + +class TestSystemClockNow(ClockScenario): + """ + Verify SystemClock::Now() returns a live reading through the public Clock API. + + The C++ scenario compares the reading against the host system clock within + tolerance and fails the process otherwise. Python confirms the process + succeeded and that the reading was emitted. + """ + + @pytest.fixture(scope="class") + def scenario_name(self) -> str: + return "time.system_clock_now" + + def test_returns_success(self, results: ScenarioResult) -> None: + assert results.return_code == ResultCode.SUCCESS + + def test_reading_logged(self, logs_info_level: LogContainer) -> None: + log = logs_info_level.find_log("clock", value="system") + assert log is not None + assert log.value_ns > 0 + + +class TestSteadyClockNow(ClockScenario): + """ + Verify SteadyClock::Now() is monotonic across two consecutive readings. + + The C++ scenario fails the process if the second reading precedes the first. + Python confirms success and that the logged readings are non-decreasing. + """ + + @pytest.fixture(scope="class") + def scenario_name(self) -> str: + return "time.steady_clock_now" + + def test_returns_success(self, results: ScenarioResult) -> None: + assert results.return_code == ResultCode.SUCCESS + + def test_readings_monotonic(self, logs_info_level: LogContainer) -> None: + log = logs_info_level.find_log("clock", value="steady") + assert log is not None + assert log.second_ns >= log.first_ns + + +class TestHighResSteadyClockNow(ClockScenario): + """ + Verify HighResSteadyClock::Now() is live and monotonic. + + The C++ scenario fails the process on a zero or non-monotonic reading. + Python confirms success and that the logged readings are non-zero and + non-decreasing. + """ + + @pytest.fixture(scope="class") + def scenario_name(self) -> str: + return "time.high_res_steady_clock_now" + + def test_returns_success(self, results: ScenarioResult) -> None: + assert results.return_code == ResultCode.SUCCESS + + def test_readings_monotonic(self, logs_info_level: LogContainer) -> None: + log = logs_info_level.find_log("clock", value="high_res_steady") + assert log is not None + assert log.first_ns > 0 + assert log.second_ns >= log.first_ns diff --git a/feature_integration_tests/test_scenarios/cpp/BUILD b/feature_integration_tests/test_scenarios/cpp/BUILD index 3f06998b7f6..0e9bedb944d 100644 --- a/feature_integration_tests/test_scenarios/cpp/BUILD +++ b/feature_integration_tests/test_scenarios/cpp/BUILD @@ -27,5 +27,8 @@ cc_binary( "@score_baselibs//score/mw/log:backend_stub_testutil", "@score_persistency//:kvs_cpp", "@score_test_scenarios//test_scenarios_cpp", + "@score_time//score/time/high_res_steady_time", + "@score_time//score/time/steady_time", + "@score_time//score/time/system_time", ], ) diff --git a/feature_integration_tests/test_scenarios/cpp/src/internals/time/clock_log.h b/feature_integration_tests/test_scenarios/cpp/src/internals/time/clock_log.h new file mode 100644 index 00000000000..803e3df9735 --- /dev/null +++ b/feature_integration_tests/test_scenarios/cpp/src/internals/time/clock_log.h @@ -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 + ********************************************************************************/ + +#ifndef INTERNALS_TIME_CLOCK_LOG_H_ +#define INTERNALS_TIME_CLOCK_LOG_H_ + +#include +#include +#include +#include + +namespace time_log { + +/** + * @brief Return the current UNIX timestamp in microseconds. + * + * The FIT LogContainer parses the "timestamp" field as microseconds, so the + * C++ scenarios emit microseconds to keep log ordering consistent with the + * Rust tracing JSON shape. + * + * @return Microseconds since the UNIX epoch. + */ +inline std::int64_t unix_micros() { + const auto now = std::chrono::system_clock::now(); + return std::chrono::duration_cast(now.time_since_epoch()).count(); +} + +/** + * @brief Emit a structured JSON INFO log line to stdout. + * + * Matches the Rust tracing JSON format expected by the FIT LogContainer so + * that Python test assertions can use find_log() uniformly for both Rust and + * C++ scenarios. + * + * Example output: + * @code + * {"timestamp":"1700000000000000","level":"INFO","fields":{"clock":"system","value_ns":1700000000000000000}, + * "target":"cpp_test_scenarios::scenarios::time::system_clock_now","threadId":"ThreadId(1)"} + * @endcode + * + * @param fields JSON fragment for the "fields" object, e.g. @c "\"clock\":\"system\",\"value_ns\":1" + * @param target Module target string embedded in the log line. + */ +inline void log_info(const std::string& fields, const std::string& target) { + std::cout << "{\"timestamp\":\"" << unix_micros() + << "\",\"level\":\"INFO\",\"fields\":{" << fields + << "},\"target\":\"" << target + << "\",\"threadId\":\"ThreadId(1)\"}\n"; +} + +} // namespace time_log + +#endif // INTERNALS_TIME_CLOCK_LOG_H_ diff --git a/feature_integration_tests/test_scenarios/cpp/src/scenarios/mod.cpp b/feature_integration_tests/test_scenarios/cpp/src/scenarios/mod.cpp index 83a32e5af8e..e0a3b92f8bd 100644 --- a/feature_integration_tests/test_scenarios/cpp/src/scenarios/mod.cpp +++ b/feature_integration_tests/test_scenarios/cpp/src/scenarios/mod.cpp @@ -23,6 +23,7 @@ Scenario::Ptr make_utf8_default_value_get_scenario(); Scenario::Ptr make_multi_instance_isolation_scenario(); ScenarioGroup::Ptr supported_datatypes_group(); ScenarioGroup::Ptr default_values_group(); +ScenarioGroup::Ptr time_scenario_group(); ScenarioGroup::Ptr persistency_scenario_group() { return std::make_shared( @@ -42,5 +43,5 @@ ScenarioGroup::Ptr root_scenario_group() { return std::make_shared( "root", std::vector{}, - std::vector{persistency_scenario_group()}); + std::vector{persistency_scenario_group(), time_scenario_group()}); } diff --git a/feature_integration_tests/test_scenarios/cpp/src/scenarios/time/high_res_steady_clock_now.cpp b/feature_integration_tests/test_scenarios/cpp/src/scenarios/time/high_res_steady_clock_now.cpp new file mode 100644 index 00000000000..73b4c0b9cef --- /dev/null +++ b/feature_integration_tests/test_scenarios/cpp/src/scenarios/time/high_res_steady_clock_now.cpp @@ -0,0 +1,79 @@ +/******************************************************************************** + * 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 "../../internals/time/clock_log.h" + +#include "score/time/high_res_steady_time/src/high_res_steady_clock.h" + +#include + +#include +#include +#include +#include + +namespace { + +/// Read the HighResSteadyClock twice via the public Clock API and verify it is +/// live and monotonic. +/// +/// The high-resolution steady clock must return a non-zero reading and never +/// move backwards. This proves the high-resolution backend (system-clock based +/// on Linux) links and behaves monotonically in the reference integration build. +class HighResSteadyClockNow final : public Scenario { +public: + /** + * @brief Return the scenario name used to identify this scenario in the runner. + * @return Scenario name string. + */ + std::string name() const final { return "high_res_steady_clock_now"; } + + /** + * @brief Execute the high-resolution steady-clock scenario. + * + * Takes two consecutive snapshots and verifies the first is non-zero and the + * second is not earlier than the first, then logs both readings for Python + * inspection. + * + * @param input Unused; this scenario takes no configuration. + * @throws std::runtime_error if the reading is zero or non-monotonic. + */ + void run(const std::string& /*input*/) const final { + const auto clock = score::time::HighResSteadyClock::GetInstance(); + const std::int64_t first_ns = clock.Now().TimePointNs().count(); + const std::int64_t second_ns = clock.Now().TimePointNs().count(); + + if (first_ns == 0) { + throw std::runtime_error("HighResSteadyClock::Now() returned a zero reading"); + } + if (second_ns < first_ns) { + throw std::runtime_error( + "HighResSteadyClock::Now() is not monotonic: second reading precedes the first"); + } + + time_log::log_info( + "\"clock\":\"high_res_steady\",\"first_ns\":" + std::to_string(first_ns) + + ",\"second_ns\":" + std::to_string(second_ns), + "cpp_test_scenarios::scenarios::time::high_res_steady_clock_now"); + } +}; + +} // namespace + +/** + * @brief Factory function for the HighResSteadyClockNow scenario. + * @return Shared pointer to the constructed scenario. + */ +Scenario::Ptr make_high_res_steady_clock_now_scenario() { + return std::make_shared(); +} diff --git a/feature_integration_tests/test_scenarios/cpp/src/scenarios/time/mod.cpp b/feature_integration_tests/test_scenarios/cpp/src/scenarios/time/mod.cpp new file mode 100644 index 00000000000..d2974a34792 --- /dev/null +++ b/feature_integration_tests/test_scenarios/cpp/src/scenarios/time/mod.cpp @@ -0,0 +1,31 @@ +/******************************************************************************** + * 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 + +Scenario::Ptr make_system_clock_now_scenario(); +Scenario::Ptr make_steady_clock_now_scenario(); +Scenario::Ptr make_high_res_steady_clock_now_scenario(); + +ScenarioGroup::Ptr time_scenario_group() { + return std::make_shared( + "time", + std::vector{ + make_system_clock_now_scenario(), + make_steady_clock_now_scenario(), + make_high_res_steady_clock_now_scenario(), + }, + std::vector{}); +} diff --git a/feature_integration_tests/test_scenarios/cpp/src/scenarios/time/steady_clock_now.cpp b/feature_integration_tests/test_scenarios/cpp/src/scenarios/time/steady_clock_now.cpp new file mode 100644 index 00000000000..27c414cfb87 --- /dev/null +++ b/feature_integration_tests/test_scenarios/cpp/src/scenarios/time/steady_clock_now.cpp @@ -0,0 +1,75 @@ +/******************************************************************************** + * 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 "../../internals/time/clock_log.h" + +#include "score/time/steady_time/src/steady_clock.h" + +#include + +#include +#include +#include +#include + +namespace { + +/// Read the SteadyClock twice via the public Clock API and verify monotonicity. +/// +/// A steady (monotonic) clock must never move backwards. Two consecutive +/// Clock::Now() readings must be non-decreasing. This proves the +/// steady clock backend links and behaves monotonically in the reference +/// integration build. +class SteadyClockNow final : public Scenario { +public: + /** + * @brief Return the scenario name used to identify this scenario in the runner. + * @return Scenario name string. + */ + std::string name() const final { return "steady_clock_now"; } + + /** + * @brief Execute the steady-clock monotonicity scenario. + * + * Takes two consecutive snapshots and verifies the second is not earlier + * than the first, then logs both readings for Python inspection. + * + * @param input Unused; this scenario takes no configuration. + * @throws std::runtime_error if the second reading precedes the first. + */ + void run(const std::string& /*input*/) const final { + const auto clock = score::time::SteadyClock::GetInstance(); + const std::int64_t first_ns = clock.Now().TimePointNs().count(); + const std::int64_t second_ns = clock.Now().TimePointNs().count(); + + if (second_ns < first_ns) { + throw std::runtime_error( + "SteadyClock::Now() is not monotonic: second reading precedes the first"); + } + + time_log::log_info( + "\"clock\":\"steady\",\"first_ns\":" + std::to_string(first_ns) + + ",\"second_ns\":" + std::to_string(second_ns), + "cpp_test_scenarios::scenarios::time::steady_clock_now"); + } +}; + +} // namespace + +/** + * @brief Factory function for the SteadyClockNow scenario. + * @return Shared pointer to the constructed scenario. + */ +Scenario::Ptr make_steady_clock_now_scenario() { + return std::make_shared(); +} diff --git a/feature_integration_tests/test_scenarios/cpp/src/scenarios/time/system_clock_now.cpp b/feature_integration_tests/test_scenarios/cpp/src/scenarios/time/system_clock_now.cpp new file mode 100644 index 00000000000..39ca737814d --- /dev/null +++ b/feature_integration_tests/test_scenarios/cpp/src/scenarios/time/system_clock_now.cpp @@ -0,0 +1,85 @@ +/******************************************************************************** + * 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 "../../internals/time/clock_log.h" + +#include "score/time/system_time/src/system_clock.h" + +#include + +#include +#include +#include +#include +#include +#include + +namespace { + +/// Read the SystemClock via the public Clock API and verify it tracks the host +/// wall clock. +/// +/// score_time's SystemClock wraps std::chrono::system_clock, so a reading taken +/// through Clock::Now() must fall within a generous tolerance of a +/// direct std::chrono::system_clock::now() call. This proves the library links +/// its production backend and returns a live reading (not an epoch-zero stub) +/// when integrated into the reference integration build. +class SystemClockNow final : public Scenario { +public: + /** + * @brief Return the scenario name used to identify this scenario in the runner. + * @return Scenario name string. + */ + std::string name() const final { return "system_clock_now"; } + + /** + * @brief Execute the system-clock reading scenario. + * + * Reads the SystemClock snapshot, compares it against the host system clock + * within a 60-second tolerance, and logs the reading for Python inspection. + * + * @param input Unused; this scenario takes no configuration. + * @throws std::runtime_error if the reading is non-positive or outside tolerance. + */ + void run(const std::string& /*input*/) const final { + const auto snapshot = score::time::SystemClock::GetInstance().Now(); + const std::int64_t reading_ns = snapshot.TimePointNs().count(); + + const std::int64_t reference_ns = + std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()) + .count(); + + // Generous tolerance absorbs scheduling jitter while still catching a + // broken backend (e.g. an epoch-zero stub or an unlinked clock). + constexpr std::int64_t tolerance_ns = 60LL * 1000 * 1000 * 1000; + if (reading_ns <= 0 || std::llabs(reference_ns - reading_ns) > tolerance_ns) { + throw std::runtime_error( + "SystemClock::Now() reading is not within tolerance of the host system clock"); + } + + time_log::log_info( + "\"clock\":\"system\",\"value_ns\":" + std::to_string(reading_ns), + "cpp_test_scenarios::scenarios::time::system_clock_now"); + } +}; + +} // namespace + +/** + * @brief Factory function for the SystemClockNow scenario. + * @return Shared pointer to the constructed scenario. + */ +Scenario::Ptr make_system_clock_now_scenario() { + return std::make_shared(); +} From c04f5c095b155cf3ca62014b8c9173e1a3b66dcc Mon Sep 17 00:00:00 2001 From: Chris Langhans Date: Sun, 4 Oct 2026 20:45:14 +0200 Subject: [PATCH 2/2] test(fit): address clock PR review findings - Drop redundant ClockScenario.build_tools override; FitScenario already resolves the parametrized version. Keep version on test_config so it stays in the fixture dependency chain. - Add @add_test_properties mapping to score_time feature requirements: FR-9 (SystemClock), FR-13 (HighResSteadyClock), FR-14 (SteadyClock). - Add steady_clock_progression scenario and TestSteadyClockProgression: take N readings asserting non-decreasing, sleep a known duration, take a final reading and assert progression. Covers TC-FR14-001. --- .../test_cases/tests/time/test_clock_now.py | 82 +++++++++-- .../cpp/src/scenarios/time/mod.cpp | 2 + .../time/steady_clock_progression.cpp | 134 ++++++++++++++++++ 3 files changed, 209 insertions(+), 9 deletions(-) create mode 100644 feature_integration_tests/test_scenarios/cpp/src/scenarios/time/steady_clock_progression.cpp diff --git a/feature_integration_tests/test_cases/tests/time/test_clock_now.py b/feature_integration_tests/test_cases/tests/time/test_clock_now.py index 3b157f94795..8a3b9047385 100644 --- a/feature_integration_tests/test_cases/tests/time/test_clock_now.py +++ b/feature_integration_tests/test_cases/tests/time/test_clock_now.py @@ -15,32 +15,40 @@ import pytest from fit_scenario import FitScenario, ResultCode -from testing_utils import BazelTools, BuildTools, LogContainer, ScenarioResult +from test_properties import add_test_properties +from testing_utils import LogContainer, ScenarioResult # score_time is C++ only; there is no Rust variant of the clock library. pytestmark = pytest.mark.parametrize("version", ["cpp"], scope="class") class ClockScenario(FitScenario): - """Common base for score_time clock scenarios (no scenario input required).""" + """Common base for score_time clock scenarios. - @pytest.fixture(scope="class") - def build_tools(self, version: str) -> BuildTools: - # Consume the parametrized `version` and select the C++ scenario binary. - return BazelTools(option_prefix=version) + ``build_tools`` is inherited from ``FitScenario``. The ``version`` parameter + is declared on ``test_config`` so it appears in the fixture dependency chain; + ``FitScenario.build_tools`` then resolves it dynamically via + ``request.getfixturevalue("version")`` and selects the C++ target. + """ @pytest.fixture(scope="class") - def test_config(self) -> dict[str, Any]: + def test_config(self, version: str) -> dict[str, Any]: return {} +@add_test_properties( + partially_verifies=["FR-9"], + test_type="requirements-based", + derivation_technique="requirements-analysis", +) class TestSystemClockNow(ClockScenario): """ Verify SystemClock::Now() returns a live reading through the public Clock API. The C++ scenario compares the reading against the host system clock within tolerance and fails the process otherwise. Python confirms the process - succeeded and that the reading was emitted. + succeeded and that the reading was emitted. Partially verifies FR-9 + (Absolute Time Base API): a live, non-zero reading through the public API. """ @pytest.fixture(scope="class") @@ -56,12 +64,18 @@ def test_reading_logged(self, logs_info_level: LogContainer) -> None: assert log.value_ns > 0 +@add_test_properties( + partially_verifies=["FR-14"], + test_type="requirements-based", + derivation_technique="requirements-analysis", +) class TestSteadyClockNow(ClockScenario): """ Verify SteadyClock::Now() is monotonic across two consecutive readings. The C++ scenario fails the process if the second reading precedes the first. Python confirms success and that the logged readings are non-decreasing. + Partially verifies FR-14 (Monotonic Clock API): non-decreasing readings. """ @pytest.fixture(scope="class") @@ -77,13 +91,19 @@ def test_readings_monotonic(self, logs_info_level: LogContainer) -> None: assert log.second_ns >= log.first_ns +@add_test_properties( + partially_verifies=["FR-13"], + test_type="requirements-based", + derivation_technique="requirements-analysis", +) class TestHighResSteadyClockNow(ClockScenario): """ Verify HighResSteadyClock::Now() is live and monotonic. The C++ scenario fails the process on a zero or non-monotonic reading. Python confirms success and that the logged readings are non-zero and - non-decreasing. + non-decreasing. Partially verifies FR-13 (High Precision Clock API): a + non-zero, non-decreasing high-resolution reading. """ @pytest.fixture(scope="class") @@ -98,3 +118,47 @@ def test_readings_monotonic(self, logs_info_level: LogContainer) -> None: assert log is not None assert log.first_ns > 0 assert log.second_ns >= log.first_ns + + +@add_test_properties( + partially_verifies=["FR-14"], + test_type="requirements-based", + derivation_technique="requirements-analysis", +) +class TestSteadyClockProgression(ClockScenario): + """ + Verify SteadyClock::Now() stays monotonic across a burst of ticks and + progresses by roughly a known sleep duration. + + The C++ scenario takes several readings, asserts the sequence is + non-decreasing, sleeps for a known duration, takes a final reading and + asserts it does not precede the last pre-sleep reading. Python confirms + success and that the post-sleep reading advanced by roughly the sleep + duration. Partially verifies FR-14 (Monotonic Clock API) / TC-FR14-001: + repeated Now() calls are non-decreasing and the clock progresses over time. + """ + + @pytest.fixture(scope="class") + def scenario_name(self) -> str: + return "time.steady_clock_progression" + + @pytest.fixture(scope="class") + def test_config(self, version: str) -> dict[str, Any]: + return {"tick_count": 5, "sleep_ms": 100} + + def test_returns_success(self, results: ScenarioResult) -> None: + assert results.return_code == ResultCode.SUCCESS + + def test_readings_monotonic(self, logs_info_level: LogContainer) -> None: + log = logs_info_level.find_log("clock", value="steady") + assert log is not None + assert log.last_ns >= log.first_ns + assert log.final_ns >= log.last_ns + + def test_clock_progressed_during_sleep(self, logs_info_level: LogContainer) -> None: + log = logs_info_level.find_log("clock", value="steady") + assert log is not None + expected_ns = log.sleep_ms * 1_000_000 + # Allow generous scheduling jitter: the clock must have advanced by at + # least half the sleep, proving the elapsed time is observable. + assert log.final_ns - log.last_ns >= expected_ns // 2 diff --git a/feature_integration_tests/test_scenarios/cpp/src/scenarios/time/mod.cpp b/feature_integration_tests/test_scenarios/cpp/src/scenarios/time/mod.cpp index d2974a34792..859e2178d06 100644 --- a/feature_integration_tests/test_scenarios/cpp/src/scenarios/time/mod.cpp +++ b/feature_integration_tests/test_scenarios/cpp/src/scenarios/time/mod.cpp @@ -18,6 +18,7 @@ Scenario::Ptr make_system_clock_now_scenario(); Scenario::Ptr make_steady_clock_now_scenario(); Scenario::Ptr make_high_res_steady_clock_now_scenario(); +Scenario::Ptr make_steady_clock_progression_scenario(); ScenarioGroup::Ptr time_scenario_group() { return std::make_shared( @@ -26,6 +27,7 @@ ScenarioGroup::Ptr time_scenario_group() { make_system_clock_now_scenario(), make_steady_clock_now_scenario(), make_high_res_steady_clock_now_scenario(), + make_steady_clock_progression_scenario(), }, std::vector{}); } diff --git a/feature_integration_tests/test_scenarios/cpp/src/scenarios/time/steady_clock_progression.cpp b/feature_integration_tests/test_scenarios/cpp/src/scenarios/time/steady_clock_progression.cpp new file mode 100644 index 00000000000..782c0ae2717 --- /dev/null +++ b/feature_integration_tests/test_scenarios/cpp/src/scenarios/time/steady_clock_progression.cpp @@ -0,0 +1,134 @@ +/******************************************************************************** + * 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 "../../internals/time/clock_log.h" + +#include "score/json/json_parser.h" +#include "score/time/steady_time/src/steady_clock.h" + +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace { + +/// Read the SteadyClock repeatedly, sleep a known duration, then read once more. +/// +/// A steady (monotonic) clock must never move backwards. This scenario takes a +/// series of readings, asserts the sequence is non-decreasing, sleeps for a +/// known duration, takes one final reading and asserts the clock has progressed +/// beyond the last pre-sleep reading. This exercises the monotonicity and +/// progression guarantees of FR-14 (Monotonic Clock API), including the +/// TC-FR14-001 "call Now() repeatedly, assert strictly increasing" case. +class SteadyClockProgression final : public Scenario { +public: + /** + * @brief Return the scenario name used to identify this scenario in the runner. + * @return Scenario name string. + */ + std::string name() const final { return "steady_clock_progression"; } + + /** + * @brief Execute the steady-clock progression scenario. + * + * Takes ``tick_count`` consecutive readings and verifies the sequence is + * non-decreasing, sleeps for ``sleep_ms`` milliseconds, then takes a final + * reading and verifies it is not earlier than the last pre-sleep reading. + * Both values are read from the scenario input, defaulting to 5 ticks and a + * 100 ms sleep when absent. + * + * @param input JSON object with optional ``tick_count`` and ``sleep_ms`` fields. + * @throws std::invalid_argument if the configured values are out of range. + * @throws std::runtime_error if any reading violates monotonicity. + */ + void run(const std::string& input) const final { + // Defaults; overridable via the test config. + std::int64_t tick_count = 5; + std::int64_t sleep_ms = 100; + + const score::json::JsonParser parser; + if (const auto root_any_res = parser.FromBuffer(input); root_any_res.has_value()) { + if (const auto root_obj_res = root_any_res.value().As(); + root_obj_res.has_value()) { + const auto& root = root_obj_res.value().get(); + if (const auto it = root.find("tick_count"); it != root.end()) { + if (const auto v = it->second.As(); v.has_value()) { + tick_count = v.value(); + } + } + if (const auto it = root.find("sleep_ms"); it != root.end()) { + if (const auto v = it->second.As(); v.has_value()) { + sleep_ms = v.value(); + } + } + } + } + + if (tick_count < 2) { + throw std::invalid_argument("tick_count must be at least 2"); + } + if (sleep_ms <= 0) { + throw std::invalid_argument("sleep_ms must be positive"); + } + + const auto clock = score::time::SteadyClock::GetInstance(); + + std::vector ticks; + ticks.reserve(static_cast(tick_count)); + for (std::int64_t i = 0; i < tick_count; ++i) { + ticks.push_back(clock.Now().TimePointNs().count()); + } + + for (std::size_t i = 1; i < ticks.size(); ++i) { + if (ticks[i] < ticks[i - 1]) { + throw std::runtime_error( + "SteadyClock::Now() is not monotonic: reading " + std::to_string(i) + + " precedes the previous one"); + } + } + + const std::int64_t first_ns = ticks.front(); + const std::int64_t last_ns = ticks.back(); + std::this_thread::sleep_for(std::chrono::milliseconds(sleep_ms)); + const std::int64_t final_ns = clock.Now().TimePointNs().count(); + + if (final_ns < last_ns) { + throw std::runtime_error( + "SteadyClock::Now() did not progress after sleep: final reading precedes the last"); + } + + time_log::log_info( + "\"clock\":\"steady\",\"tick_count\":" + std::to_string(tick_count) + + ",\"first_ns\":" + std::to_string(first_ns) + + ",\"last_ns\":" + std::to_string(last_ns) + + ",\"final_ns\":" + std::to_string(final_ns) + + ",\"sleep_ms\":" + std::to_string(sleep_ms), + "cpp_test_scenarios::scenarios::time::steady_clock_progression"); + } +}; + +} // namespace + +/** + * @brief Factory function for the SteadyClockProgression scenario. + * @return Shared pointer to the constructed scenario. + */ +Scenario::Ptr make_steady_clock_progression_scenario() { + return std::make_shared(); +}