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(); +}