diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 1e32feef..bf8a7171 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -2,4 +2,4 @@ # the repo. Unless a later match takes precedence, # @global-owner1 and @global-owner2 will be requested for # review when someone opens a pull request. -* @rdkcentral/rdke_ghec_sysint_maintainer @rdkcentral/rdke_ghec_sysint_admin +* @rdkcentral/rdke_ghec_sysint_maintainer diff --git a/.github/workflows/L1-Test.yaml b/.github/workflows/L1-Test.yaml index a7163bd4..4ac8b169 100644 --- a/.github/workflows/L1-Test.yaml +++ b/.github/workflows/L1-Test.yaml @@ -25,4 +25,5 @@ jobs: - name: Upload test results to automatic test result management system if: github.repository_owner == 'rdkcentral' run: | - gtest-json-result-push.py /tmp/Gtest_Report https://rdkeorchestrationservice.apps.cloud.comcast.net/rdke_orchestration_api/push_unit_test_results `pwd` \ No newline at end of file + # Point the script to the directory containing all XML reports + gtest-json-result-push.py /tmp/gtest_reports/ https://rdkeorchestrationservice.apps.cloud.comcast.net/rdke_orchestration_api/push_unit_test_results `pwd` diff --git a/.github/workflows/L2-tests.yml b/.github/workflows/L2-tests.yml new file mode 100644 index 00000000..956765e9 --- /dev/null +++ b/.github/workflows/L2-tests.yml @@ -0,0 +1,60 @@ +name: L2 Integration Tests for systemtimemgr + +on: + pull_request: + branches: [ develop , main ] + +env: + AUTOMATICS_UNAME: ${{ secrets.AUTOMATICS_UNAME }} + AUTOMATICS_PASSCODE: ${{ secrets.AUTOMATICS_PASSCODE }} + +jobs: + execute-l2-tests-on-pr: + name: Execute L2 Test in L2 Container Environment + runs-on: ubuntu-latest + + steps: + - name: Checkout systemtimemgr Code + uses: actions/checkout@v4 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + + - name: Pull Mock XConf, Native Platform, and Docker RDK CI Images + run: | + docker pull ghcr.io/rdkcentral/docker-device-mgt-service-test/mockxconf:latest + docker pull ghcr.io/rdkcentral/docker-device-mgt-service-test/native-platform:latest + docker pull ghcr.io/rdkcentral/docker-rdk-ci:latest + - name: Start mock-xconf service + run: | + docker run -d --name mockxconf -p 50050:50050 -p 50051:50051 -p 50052:50052 -p 50053:50053 -v ${{ github.workspace }}:/mnt/L2_CONTAINER_SHARED_VOLUME ghcr.io/rdkcentral/docker-device-mgt-service-test/mockxconf:latest + - name: Start l2-container service + run: | + docker run -d --name native-platform --link mockxconf -v ${{ github.workspace }}:/mnt/L2_CONTAINER_SHARED_VOLUME ghcr.io/rdkcentral/docker-device-mgt-service-test/native-platform:latest + - name: Build systemtimemgr and Run L2 inside Native Platform Container + run: | + docker exec -i native-platform /bin/bash -c "cd /mnt/L2_CONTAINER_SHARED_VOLUME/ && sh ./cov_build.sh && export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/lib/x86_64-linux-gnu:/lib/aarch64-linux-gnu:/usr/local/lib && sh ./run_l2.sh" + - name: Copy L2 Test report to Host Runner + run: | + docker cp native-platform:/tmp/l2_test_report /tmp/L2_TEST_RESULTS + ls -lh /tmp/L2_TEST_RESULTS + - name: Setup Docker Buildx + uses: docker/setup-buildx-action@v1 + with: + install: true + + - name: Run RDK CI Container + run: | + docker run -d --name ci-container -e AUTOMATICS_UNAME=${{ secrets.AUTOMATICS_UNAME }} -e AUTOMATICS_PASSCODE=${{ secrets.AUTOMATICS_PASSCODE }} -v /tmp/L2_TEST_RESULTS:/tmp/L2_TEST_RESULTS ghcr.io/rdkcentral/docker-rdk-ci:latest tail -f /dev/null + - name: Upload Results to Automatics + if: github.repository_owner == 'rdkcentral' + run: | + docker cp /tmp/L2_TEST_RESULTS ci-container:/tmp/L2_TEST_RESULTS + docker exec -i ci-container bash -c "echo 'Contents in workspace directory' && ls -l && echo '===============================' && echo 'Contents in /tmp/L2_TEST_RESULTS' && ls -l /tmp/L2_TEST_RESULTS && echo '===============================' && git config --global --add safe.directory /mnt/L2_CONTAINER_SHARED_VOLUME && gtest-json-result-push.py /tmp/L2_TEST_RESULTS https://rdkeorchestrationservice.apps.cloud.comcast.net/rdke_orchestration_api/push_unit_test_results /mnt/L2_CONTAINER_SHARED_VOLUME" diff --git a/.github/workflows/cla.yml b/.github/workflows/cla.yml new file mode 100644 index 00000000..c58b1b0b --- /dev/null +++ b/.github/workflows/cla.yml @@ -0,0 +1,20 @@ +name: "CLA" + +permissions: + contents: read + pull-requests: write + actions: write + statuses: write + +on: + issue_comment: + types: [created] + pull_request_target: + types: [opened, closed, synchronize] + +jobs: + CLA-Lite: + name: "Signature" + uses: rdkcentral/cmf-actions/.github/workflows/cla.yml@v1 + secrets: + PERSONAL_ACCESS_TOKEN: ${{ secrets.CLA_ASSISTANT }} diff --git a/.github/workflows/code-coverage.yml b/.github/workflows/code-coverage.yml index fbdffbf0..5a6c0a56 100644 --- a/.github/workflows/code-coverage.yml +++ b/.github/workflows/code-coverage.yml @@ -2,7 +2,7 @@ name: Code Coverage on: pull_request: - branches: [ main ] + branches: [ develop ] jobs: execute-unit-code-coverage-report-on-release: diff --git a/.github/workflows/fossid_integration_stateless_diffscan_target_repo.yml b/.github/workflows/fossid_integration_stateless_diffscan_target_repo.yml index da02b8b4..7b8c1cba 100644 --- a/.github/workflows/fossid_integration_stateless_diffscan_target_repo.yml +++ b/.github/workflows/fossid_integration_stateless_diffscan_target_repo.yml @@ -1,11 +1,18 @@ name: Fossid Stateless Diff Scan -on: pull_request +on: + pull_request: + types: [opened, synchronize, reopened] + +permissions: + contents: read + pull-requests: read jobs: call-fossid-workflow: - uses: rdkcentral/build_tools_workflows/.github/workflows/fossid_integration_stateless_diffscan.yml@develop - secrets: + if: ${{ ! github.event.pull_request.head.repo.fork }} + uses: rdkcentral/build_tools_workflows/.github/workflows/fossid_integration_stateless_diffscan.yml@1.0.0 + secrets: FOSSID_CONTAINER_USERNAME: ${{ secrets.FOSSID_CONTAINER_USERNAME }} FOSSID_CONTAINER_PASSWORD: ${{ secrets.FOSSID_CONTAINER_PASSWORD }} FOSSID_HOST_USERNAME: ${{ secrets.FOSSID_HOST_USERNAME }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 73a95cc5..0060bb07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,10 +4,78 @@ All notable changes to this project will be documented in this file. Dates are d Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). +#### [1.7.0](https://github.com/rdkcentral/systemtimemgr/compare/1.6.0...1.7.0) + +- RDKEMW-15246 : Chrony Enhancements [`#78`](https://github.com/rdkcentral/systemtimemgr/pull/78) +- RDKEMW-15246 : Chrony Enhancements [`#79`](https://github.com/rdkcentral/systemtimemgr/pull/79) +- Merge tag '1.6.0' into develop [`0c32e07`](https://github.com/rdkcentral/systemtimemgr/commit/0c32e071dc2b36bc63dfded08d5f220dcc1e80cc) + +#### [1.6.0](https://github.com/rdkcentral/systemtimemgr/compare/1.5.1...1.6.0) + +> 14 May 2026 + +- RDKEMW-17125 - Subscribe to NWMgr plugin events from systimemgr [`#72`](https://github.com/rdkcentral/systemtimemgr/pull/72) +- 1.6.0 release changelog updates [`554883e`](https://github.com/rdkcentral/systemtimemgr/commit/554883e7bfb2d703c521d102d49a6c39b5afb227) +- Merge tag '1.5.1' into develop [`1e3c2e2`](https://github.com/rdkcentral/systemtimemgr/commit/1e3c2e2f86b20a608c469081c469ed72f65388a0) + +#### [1.5.1](https://github.com/rdkcentral/systemtimemgr/compare/1.5.0...1.5.1) + +> 20 March 2026 + +- RDKEMW-15665 : Handle deepsleep case with chronyd [`#66`](https://github.com/rdkcentral/systemtimemgr/pull/66) +- 1.5.1 release changelog updates [`42762a8`](https://github.com/rdkcentral/systemtimemgr/commit/42762a8f8e162705fb88bdb7485048796ca38622) +- Merge tag '1.5.0' into develop [`3facedc`](https://github.com/rdkcentral/systemtimemgr/commit/3facedcb74d9b0d0e4d63d0d146dac32d5fc153d) + +#### [1.5.0](https://github.com/rdkcentral/systemtimemgr/compare/1.4.0...1.5.0) + +> 10 March 2026 + +- RDKEMW-14726: Implement Chrony runtime selection for Time Sync [`#62`](https://github.com/rdkcentral/systemtimemgr/pull/62) +- 1.5.0 release changelog updates [`be9a8de`](https://github.com/rdkcentral/systemtimemgr/commit/be9a8de06bba9daf7071347031c95421c60b4783) +- Merge tag '1.4.0' into develop [`6583514`](https://github.com/rdkcentral/systemtimemgr/commit/6583514f6a6a7cbb496f06cb86318faf307ddff2) + +#### [1.4.0](https://github.com/rdkcentral/systemtimemgr/compare/1.3.0...1.4.0) + +> 3 December 2025 + +- RDKEMW-9619 : NTP Marker name update [`#57`](https://github.com/rdkcentral/systemtimemgr/pull/57) +- RDK-58859 : 64-bit compilation support for systimemanager [`#53`](https://github.com/rdkcentral/systemtimemgr/pull/53) +- Deploy fossid_integration_stateless_diffscan_target_repo action [`#54`](https://github.com/rdkcentral/systemtimemgr/pull/54) +- Deploy cla action [`#28`](https://github.com/rdkcentral/systemtimemgr/pull/28) +- RDK-57622 : [RDK-E] L2 test framework for SystemTime Manger Module Phase 1. [`#52`](https://github.com/rdkcentral/systemtimemgr/pull/52) +- RDKEMW-5185-Improve L1 Coverage for Systemtimemgr [`#51`](https://github.com/rdkcentral/systemtimemgr/pull/51) +- 1.4.0 release changelog updates [`0c372c0`](https://github.com/rdkcentral/systemtimemgr/commit/0c372c0c9eac5bd1720604d7d24eee7a7141815c) +- Update CODEOWNERS [`df604e0`](https://github.com/rdkcentral/systemtimemgr/commit/df604e0743ba77efcb69eddca2292b7bc3410e2f) +- Merge tag '1.3.0' into develop [`9e6063f`](https://github.com/rdkcentral/systemtimemgr/commit/9e6063f336380c4d04bc032cf0e1bd41e6f49910) + +#### [1.3.0](https://github.com/rdkcentral/systemtimemgr/compare/1.2.1...1.3.0) + +> 17 July 2025 + +- RDK-57964: T2 integration for systimemgr [`#43`](https://github.com/rdkcentral/systemtimemgr/pull/43) +- 1.3.0 release changelog updates [`18e109f`](https://github.com/rdkcentral/systemtimemgr/commit/18e109fbca50bc2c1d5ef18a9277bd97f65ad406) +- Merge tag '1.2.1' into develop [`e2d50a1`](https://github.com/rdkcentral/systemtimemgr/commit/e2d50a1db708d5bc78eaea5943114e62b8464497) + +#### [1.2.1](https://github.com/rdkcentral/systemtimemgr/compare/1.2.0...1.2.1) + +> 27 June 2025 + +- RDKEMW-5290-L1 Script change to check the failure in the workflow [`#38`](https://github.com/rdkcentral/systemtimemgr/pull/38) +- RDK-48831 : L2 Test With CI Integration For SystimeManager [`#31`](https://github.com/rdkcentral/systemtimemgr/pull/31) +- Revert "RDKEMW-5290-L1 Script change to check the failure in the workflow" [`#35`](https://github.com/rdkcentral/systemtimemgr/pull/35) +- RDKEMW-5290-L1 Script change to check the failure in the workflow [`#25`](https://github.com/rdkcentral/systemtimemgr/pull/25) +- rebase [`#33`](https://github.com/rdkcentral/systemtimemgr/pull/33) +- RDKEMW-5290: Release 1.2.1 [`855fdaf`](https://github.com/rdkcentral/systemtimemgr/commit/855fdafa82de774cd025fccc2922bd70972e90ff) +- Merge tag '1.2.0' into develop [`a271f67`](https://github.com/rdkcentral/systemtimemgr/commit/a271f6769560a12abb6d2f26a228e9b906b9399f) +- Update run_ut.sh [`2293956`](https://github.com/rdkcentral/systemtimemgr/commit/22939567fcb84aa6533f43f472f5037b12b7401f) + #### [1.2.0](https://github.com/rdkcentral/systemtimemgr/compare/1.1.0...1.2.0) +> 25 June 2025 + - RDK-57157: Prototype to improve NTP (timesyncd) reliability [`#23`](https://github.com/rdkcentral/systemtimemgr/pull/23) - RDK-57964 : Improve NTP Analytics [`#27`](https://github.com/rdkcentral/systemtimemgr/pull/27) +- Release 1.2.0 [`4b29cb4`](https://github.com/rdkcentral/systemtimemgr/commit/4b29cb478b2dbb14cb6ad885ee28dcfe2c0cbb73) - Merge tag '1.1.0' into develop [`016e812`](https://github.com/rdkcentral/systemtimemgr/commit/016e8127110e91f26a5be0c6eb022d0b846b4317) #### [1.1.0](https://github.com/rdkcentral/systemtimemgr/compare/1.0.0...1.1.0) diff --git a/Makefile.am b/Makefile.am index 1633eedc..439bae08 100644 --- a/Makefile.am +++ b/Makefile.am @@ -26,11 +26,17 @@ bin_PROGRAMS = sysTimeMgr lib_LTLIBRARIES = libsysTimeMgr.la libsysTimeMgr_la_SOURCES = systimemgr.cpp -libsysTimeMgr_la_LDFLAGS = -lpthread -lsystimerfactory -lrdkloggers -lsecure_wrapper +libsysTimeMgr_la_LDFLAGS = -lpthread -lsystimerfactory -lrdkloggers -lsecure_wrapper -lchronyctl sysTimeMgr_SOURCES = main.cpp sysTimeMgr_LDADD = libsysTimeMgr.la + +if IS_TELEMETRY2_ENABLED +libsysTimeMgr_la_CPPFLAGS = $(T2_EVENT_FLAG) +libsysTimeMgr_la_LDFLAGS += -ltelemetry_msgsender -lt2utils +endif + #libsysTimeMgr_la_includedir = ${includedir} #libsysTimeMgr_la_include_HEADERS = systimemgr.h diff --git a/configure.ac b/configure.ac index 38ae3ca1..bf0baf8e 100644 --- a/configure.ac +++ b/configure.ac @@ -83,6 +83,20 @@ AC_ARG_ENABLE([dtt], ], [echo "Tee build is enable"]) +AC_ARG_ENABLE([t2api], + AS_HELP_STRING([--enable-t2api],[enables telemetry]), + [ + case "${enableval}" in + yes) IS_TELEMETRY2_ENABLED=true + T2_EVENT_FLAG=" -DT2_EVENT_ENABLED ";; + no) IS_TELEMETRY2_ENABLED=false ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-t2enable]) ;; + esac + ], + [echo "telemetry is disabled"]) +AM_CONDITIONAL([IS_TELEMETRY2_ENABLED], [test x$IS_TELEMETRY2_ENABLED = xtrue]) +AC_SUBST(T2_EVENT_FLAG) + AC_CONFIG_FILES(Makefile) AC_SUBST(DEBUG_CXXFLAGS) AC_SUBST(TEE_CXXFLAGS) diff --git a/cov_build.sh b/cov_build.sh index 6b9cd5da..b4cec15d 100644 --- a/cov_build.sh +++ b/cov_build.sh @@ -22,9 +22,21 @@ WORKDIR=`pwd` apt-get update apt-get install -y libjsonrpccpp-dev +git clone https://github.com/rdkcentral/time-utils.git +cd time-utils/ +cd libchronyctl/ +autoreconf -i +./configure --prefix=/usr/local +make +make install +cd ../.. + cd $WORKDIR/systimerfactory autoreconf -i -export CXXFLAGS="-I../interface/ " +# -D__LOCAL_TEST_ makes networkstatussrc.cpp use WPEFrameworkMock.h instead of +# the real Thunder/WPEFramework headers, so this local-test build does not +# require those headers during this configure/build step. +export CXXFLAGS="-I../interface/ -D__LOCAL_TEST_" ./configure --prefix=${RDKLOGGER_INSTALL_DIR} make clean && make && make install @@ -33,8 +45,9 @@ export INSTALL_DIR='/usr/local' export top_srcdir=`pwd` export top_builddir=`pwd` + autoreconf --install -export CXXFLAGS="-I./interface/ -I./systimerfactory/" +export CXXFLAGS="-I./interface/ -I./systimerfactory/ -DIARM_SUPPORT_DISABLED" export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH export LDFLAGS="-L/usr/local/lib -lpthread -lsystimerfactory -lrdkloggers -lsecure_wrapper" diff --git a/main.cpp b/main.cpp index b55d699c..bb36c804 100644 --- a/main.cpp +++ b/main.cpp @@ -46,5 +46,9 @@ int main() return EXIT_FAILURE; } + #ifdef T2_EVENT_ENABLED + t2_uninit(); + #endif + return EXIT_SUCCESS; } diff --git a/run_l2.sh b/run_l2.sh new file mode 100644 index 00000000..1a54d50a --- /dev/null +++ b/run_l2.sh @@ -0,0 +1,107 @@ +########################################################################## +# If not stated otherwise in this file or this component's LICENSE +# file the following copyright and licenses apply: +# +# Copyright 2018 RDK Management +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +########################################################################## + +RESULT_DIR="/tmp/l2_test_report" +mkdir -p "$RESULT_DIR" + +apt-get update && apt-get install -y libjsonrpccpp-dev + +touch /usr/local/bin/journalctl +chmod -R 777 /usr/local/bin/journalctl +ln -s /usr/local/bin/journalctl /usr/bin/journalctl + +rm -rf /etc/systimemgr.conf +rm -rf /opt/secure/clock.txt +rm -rf /tmp/systimemgr + +echo "timesrc ntp /ntp" > /etc/systimemgr.conf +echo "timesrc dtt /dtt" >> /etc/systimemgr.conf +echo "timesync rdkdefault /clock_time" >> /etc/systimemgr.conf + +echo "$(date +%s)" > /opt/secure/clock.txt + +mkdir /tmp/systimemgr/ +touch /tmp/systimemgr/ntp + +rm -rf /opt/logs/systimemgr.log* + +# Run L2 Test cases +pytest --json-report --json-report-summary --json-report-file $RESULT_DIR/systimemgr_single_instance.json test/functional-tests/tests/test_systimemgr_single_instance.py +pytest --json-report --json-report-summary --json-report-file $RESULT_DIR/systimemgr_initialisation.json test/functional-tests/tests/test_systimemgr_initialisation.py +pytest --json-report --json-report-summary --json-report-file $RESULT_DIR/systimemgr_get_time.json test/functional-tests/tests/test_systimemgr_get_time.py +pytest --json-report --json-report-summary --json-report-file $RESULT_DIR/systimemgr_check_file.json test/functional-tests/tests/test_systimemgr_check_file.py +pytest --json-report --json-report-summary --json-report-file $RESULT_DIR/systimemgr_time_quality.json test/functional-tests/tests/test_systimemgr_time_quality.py +pytest --json-report --json-report-summary --json-report-file $RESULT_DIR/systimemgr_bootup_flow.json test/functional-tests/tests/test_systimemgr_bootup_flow.py + +#Secure Time Validation TestCases +rm -rf /etc/systimemgr.conf +rm -rf /opt/secure/clock.txt + +echo "timesrc drm /drm" >> /etc/systimemgr.conf +echo "timesync rdkdefault /clock_time" >> /etc/systimemgr.conf + +echo "$(date +%s)" > /opt/secure/clock.txt +touch /tmp/systimemgr/drm + +pytest --json-report --json-report-summary --json-report-file $RESULT_DIR/systimemgr_single_instance.json test/functional-tests/tests/test_systimemgr_single_instance.py +pytest --json-report --json-report-summary --json-report-file $RESULT_DIR/systimemgr_securetime_Initialisation.json test/functional-tests/tests/test_secureTime_initialisation.py +pytest --json-report --json-report-summary --json-report-file $RESULT_DIR/systimemgr_secureTime_checkEvent.json test/functional-tests/tests/test_secureTime_checkEvent.py +pytest --json-report --json-report-summary --json-report-file $RESULT_DIR/systimemgr_secureTime_Quality.json test/functional-tests/tests/test_secureTime_quality.py + + +# ── NWStatusMonitor L2 tests ──────────────────────────────────────────────── +# When sysTimeMgr is compiled with -D__LOCAL_TEST_ (set by cov_build.sh via +# --enable-unittest) it uses WPEFrameworkMock.h instead of the real Thunder +# library. Events are injected by writing JSON to a file that the mock +# SmartLinkType polls — no Thunder daemon or WebSocket server is needed. +# +# Restart sysTimeMgr so it subscribes afresh for the NWStatus test suite. +pkill -f sysTimeMgr 2>/dev/null || true +sleep 1 + +# Create the RFC marker file that SysTimeMgr::run() checks before starting +# the NW-event threads (chronyRfcEnabled guard in systimemgr.cpp). +mkdir -p /opt/secure/RFC/chrony +touch /opt/secure/RFC/chrony/chronyd_enabled + +# Clean up any leftover inject / subscribed files from a previous run +rm -f /tmp/thunder_mock_org_rdk_NetworkManager_onInternetStatusChange.inject +rm -f /tmp/thunder_mock_org_rdk_NetworkManager_onInternetStatusChange.subscribed + +# Truncate the log so NWStatus tests only scan fresh output from this +# sysTimeMgr instance — prevents stale lines from earlier test suites +# causing false-passes in the offset-based log checks. +rm -f /opt/logs/systimemgr.log.0 + +sysTimeMgr & +sleep 2 + +NWSTATUS_TEST="test/functional-tests/tests/test_systimemgr_nwstatus.py" +if [ ! -f "$NWSTATUS_TEST" ]; then + NWSTATUS_TEST="$(find . -type f -name 'test_systimemgr_nwstatus.py' | head -n 1)" +fi + +if [ -z "$NWSTATUS_TEST" ] || [ ! -f "$NWSTATUS_TEST" ]; then + echo "ERROR: Unable to find test_systimemgr_nwstatus.py in the repository." >&2 + exit 1 +fi + +pytest --json-report --json-report-summary \ + --json-report-file $RESULT_DIR/systimemgr_nwstatus.json \ + "$NWSTATUS_TEST" diff --git a/systimemgr.cpp b/systimemgr.cpp index aca60db3..4ed0e74e 100644 --- a/systimemgr.cpp +++ b/systimemgr.cpp @@ -33,7 +33,24 @@ #include "itimermsg.h" #include #include "secure_wrapper.h" +#if !defined(MILESTONE_SUPPORT_DISABLED) #include "rdk_logger_milestone.h" +#endif + +#if defined(GTEST_ENABLE) || defined(__LOCAL_TEST_) +#include "systimerfactory/unittest/mocks/libchronyctl.h" +#else +#include "libchronyctl.h" +#endif + +#include "systimerfactory/networkstatussrc.h" +#include +#include + + +#ifdef T2_EVENT_ENABLED +#include +#endif using namespace std::chrono; @@ -60,22 +77,30 @@ SysTimeMgr::SysTimeMgr (string cfgfile):m_state(eSYSMGR_STATE_INIT), m_event(eSYSMGR_EVENT_UNKNOWN), m_timerInterval(600000), m_timequality(eTIMEQUALILTY_UNKNOWN), - m_timersrc("Last Known"), m_publish(NULL), m_subscriber(NULL), m_tmrsubscriber(NULL), - m_cfgfile(std::move(cfgfile)) + m_timersrc("Last Known"), + m_cfgfile(std::move(cfgfile)), + m_chronyRfcEnabled(access("/opt/secure/RFC/chrony/chronyd_enabled", F_OK) == 0) { } SysTimeMgr::~SysTimeMgr() { + if (m_chronyRfcEnabled) { + chronyctl_cleanup(); + } } void SysTimeMgr::initialize() { std::lock_guard guard(g_state_mutex); + #ifdef T2_EVENT_ENABLED + t2_init((char *) "sysTimeMgr"); + #endif + //Create Timer Src and Syncs. ifstream cfgFile(m_cfgfile.c_str()); @@ -100,10 +125,17 @@ void SysTimeMgr::initialize() { RDK_LOG(RDK_LOG_ERROR,LOG_SYSTIME,"[%s:%d]:Failed to open Config file: %s , will run in degraded mode.\n",__FUNCTION__,__LINE__,m_cfgfile.c_str()); } - + if(m_chronyRfcEnabled) { + RDK_LOG(RDK_LOG_INFO,LOG_SYSTIME,"[%s:%d]:[ChronyCTL] Initialize ChronyCTL library\n",__FUNCTION__,__LINE__); + int chronyctl_ret = chronyctl_init(); + if (chronyctl_ret != CHRONYCTL_SUCCESS) { + RDK_LOG(RDK_LOG_ERROR, LOG_SYSTIME, "[ChronyCTL] Initialization failed: rc=%d, error=%s\n", + chronyctl_ret, chronyctl_strerror(chronyctl_ret)); + } + } //m_timerSrc.push_back(createTimeSrc("regular","/tmp/clock.txt")); //m_timerSync.push_back(createTimeSync("test","/tmp/clock1.txt")); - +#if !defined(IARM_SUPPORT_DISABLED) m_publish = createPublish("iarm",IARM_BUS_SYSTIME_MGR_NAME); RDK_LOG(RDK_LOG_INFO,LOG_SYSTIME,"[%s:%d]:createSubscriber IARM_BUS_SYSTIME_MGR_NAME TIMER_STATUS_MSG Invoke\n",__FUNCTION__,__LINE__); m_tmrsubscriber = createSubscriber("iarm",IARM_BUS_SYSTIME_MGR_NAME,TIMER_STATUS_MSG); @@ -113,7 +145,18 @@ void SysTimeMgr::initialize() m_tmrsubscriber->subscribe(TIMER_STATUS_MSG,SysTimeMgr::getTimeStatus); RDK_LOG(RDK_LOG_INFO,LOG_SYSTIME,"[%s:%d]:IpowerControllerSubscriber or IarmPowerSubscriber Invoke \n",__FUNCTION__,__LINE__); m_subscriber->subscribe(POWER_CHANGE_MSG,SysTimeMgr::powerhandler); - +#else + m_publish = createPublish("test",IARM_BUS_SYSTIME_MGR_NAME); + RDK_LOG(RDK_LOG_INFO,LOG_SYSTIME,"[%s:%d]:createSubscriber IARM_BUS_SYSTIME_MGR_NAME TIMER_STATUS_MSG Invoke\n",__FUNCTION__,__LINE__); + m_tmrsubscriber = createSubscriber("test",IARM_BUS_SYSTIME_MGR_NAME,TIMER_STATUS_MSG); + RDK_LOG(RDK_LOG_INFO,LOG_SYSTIME,"[%s:%d]:createSubscriber IARM_BUS_SYSTIME_MGR_NAME POWER_CHANGE_MSG Invoke\n",__FUNCTION__,__LINE__); + m_subscriber = createSubscriber("test",IARM_BUS_SYSTIME_MGR_NAME,POWER_CHANGE_MSG); + RDK_LOG(RDK_LOG_INFO,LOG_SYSTIME,"[%s:%d]:IarmTimerStatusSubscriber Invoke \n",__FUNCTION__,__LINE__); + m_tmrsubscriber->subscribe(TIMER_STATUS_MSG,SysTimeMgr::getTimeStatus); + RDK_LOG(RDK_LOG_INFO,LOG_SYSTIME,"[%s:%d]:IpowerControllerSubscriber or IarmPowerSubscriber Invoke \n",__FUNCTION__,__LINE__); + m_subscriber->subscribe(POWER_CHANGE_MSG,SysTimeMgr::powerhandler); +#endif + //Initialize Path Event Map m_pathEventMap.insert(pair("ntp",eSYSMGR_EVENT_NTP_AVAILABLE)); //Keeping the NTP available event for stt as well. Source is different but no need to have separate event. @@ -167,6 +210,21 @@ void SysTimeMgr::run(bool forever) std::thread processThrd(SysTimeMgr::processThr,this); std::thread timerThrd(SysTimeMgr::timerThr,this); std::thread pathMonitorThrd(SysTimeMgr::pathThr,this); + RDK_LOG(RDK_LOG_INFO,LOG_SYSTIME,"[%s:%d]:RFC chronyd_enabled file %s\n", + __FUNCTION__,__LINE__, + m_chronyRfcEnabled ? "found, starting network event threads" + : "not found, skipping network event threads"); + std::thread nwEventProcessThrd; + std::thread nwEventSubscribeThrd; + std::thread ntpSyncMonitorThrd; + if (m_chronyRfcEnabled) { + /* nwEventProcessThrd must start before nwEventSubscribeThrd so the + * processing thread is ready before any event can arrive. */ + nwEventProcessThrd = std::thread(SysTimeMgr::nwEventProcessThr, this); + nwEventSubscribeThrd = std::thread(SysTimeMgr::nwEventSubscribeThr, this); + ntpSyncMonitorThrd = std::thread(SysTimeMgr::ntpSyncMonitorThr,this); + } + if (forever) { ofstream pidfile("/run/systimemgr.pid",ios::out); @@ -178,12 +236,31 @@ void SysTimeMgr::run(bool forever) processThrd.join(); timerThrd.join(); pathMonitorThrd.join(); + if (nwEventProcessThrd.joinable()) { + nwEventProcessThrd.join(); + } + if (nwEventSubscribeThrd.joinable()) { + nwEventSubscribeThrd.join(); + } + if (ntpSyncMonitorThrd.joinable()) { + ntpSyncMonitorThrd.join(); + } + } else { processThrd.detach(); timerThrd.detach(); pathMonitorThrd.detach(); + if (nwEventProcessThrd.joinable()) { + nwEventProcessThrd.detach(); + } + if (nwEventSubscribeThrd.joinable()) { + nwEventSubscribeThrd.detach(); + } + if (ntpSyncMonitorThrd.joinable()) { + ntpSyncMonitorThrd.detach(); + } } } @@ -210,6 +287,146 @@ void SysTimeMgr::pathThr(SysTimeMgr* instance) } } +/* nwEventSubscribeThr: subscribes to NetworkManager events (retries until success). */ +void SysTimeMgr::nwEventSubscribeThr(SysTimeMgr* instance) +{ + if (instance) + instance->runNetworkStatusMonitor(); +} + +/* nwEventProcessThr: waits for internet-up signals and runs chrony sync commands. */ +void SysTimeMgr::nwEventProcessThr(SysTimeMgr* instance) +{ + if (instance) + instance->runNWEventProcessing(); +} + +void SysTimeMgr::ntpSyncMonitorThr(SysTimeMgr* instance) +{ + if (instance) + instance->runNTPSyncMonitor(); +} + +/* + * runNTPSyncMonitor: polls adjtimex() once per second until the kernel clock is + * synchronised with NTP (STA_UNSYNC flag cleared). On success it logs the + * event, creates /tmp/clock-event and /tmp/systimemgr/ntp, then returns. + * The primary purpose is to capture the NTP convergence time. + */ +void SysTimeMgr::runNTPSyncMonitor() +{ + RDK_LOG(RDK_LOG_INFO, LOG_SYSTIME, + "[%s:%d]: CHRONY: NTP sync monitor thread started\n", __FUNCTION__, __LINE__); + + while (1) + { + struct timex tx; + memset(&tx, 0, sizeof(tx)); + + /* Capture adjtimex() return value: TIME_ERROR means the kernel clock + * is not disciplined, which is treated the same as STA_UNSYNC. */ + int adjtimex_state = adjtimex(&tx); + if (adjtimex_state < 0) + { + RDK_LOG(RDK_LOG_ERROR, LOG_SYSTIME, + "[%s:%d]: CHRONY: adjtimex() failed, retrying\n", __FUNCTION__, __LINE__); + std::this_thread::sleep_for(std::chrono::seconds(1)); + continue; + } + + if (adjtimex_state == TIME_ERROR || (tx.status & STA_UNSYNC)) + { + /* Clock not yet synchronised — keep polling. */ + std::this_thread::sleep_for(std::chrono::seconds(1)); + continue; + } + + /* NTP synchronisation achieved. */ + RDK_LOG(RDK_LOG_INFO, LOG_SYSTIME, + "[%s:%d]: CHRONY: NTP synchronised\n", __FUNCTION__, __LINE__); + + /* Create /tmp/systimemgr/ntp and update its timestamps via futimens() + * so that the inotify IN_ATTRIB event fires and the path monitor thread + * reliably dispatches eSYSMGR_EVENT_NTP_AVAILABLE into the state machine. + * O_NOFOLLOW|O_CLOEXEC guard against symlink attacks. */ + { + int fd = open((m_directory + "/ntp").c_str(), + O_CREAT | O_WRONLY | O_NOFOLLOW | O_CLOEXEC | O_NOCTTY, 0644); + if (fd >= 0) + { + struct timespec ts[2]; + timespec_get(&ts[0], TIME_UTC); + ts[1] = ts[0]; + if (futimens(fd, ts) != 0) + RDK_LOG(RDK_LOG_ERROR, LOG_SYSTIME, + "[%s:%d]: futimens() failed for %s/ntp\n", + __FUNCTION__, __LINE__, m_directory.c_str()); + close(fd); + } + else + RDK_LOG(RDK_LOG_ERROR, LOG_SYSTIME, + "[%s:%d]: Failed to create %s/ntp\n", + __FUNCTION__, __LINE__, m_directory.c_str()); + } + + /* Create /tmp/clock-event — O_NOFOLLOW|O_CLOEXEC guard against + * symlink/hardlink attacks when running with elevated privileges. */ + { + int fd = open("/tmp/clock-event", + O_CREAT | O_WRONLY | O_NOFOLLOW | O_CLOEXEC, 0644); + if (fd >= 0) + close(fd); + else + RDK_LOG(RDK_LOG_ERROR, LOG_SYSTIME, + "[%s:%d]: Failed to create /tmp/clock-event\n", + __FUNCTION__, __LINE__); + } + + /* Write NTP sync status to /tmp/ntp_status — O_NOFOLLOW|O_CLOEXEC + * guards against symlink attacks; write() return value is checked. */ + { + int fd = open("/tmp/ntp_status", + O_CREAT | O_WRONLY | O_TRUNC | O_NOFOLLOW | O_CLOEXEC, 0644); + if (fd >= 0) + { + const char* status = "Synchronized\n"; + ssize_t written = write(fd, status, strlen(status)); + if (written < 0) + RDK_LOG(RDK_LOG_ERROR, LOG_SYSTIME, + "[%s:%d]: Failed to write to /tmp/ntp_status\n", + __FUNCTION__, __LINE__); + close(fd); + } + else + { + RDK_LOG(RDK_LOG_ERROR, LOG_SYSTIME, + "[%s:%d]: Failed to create /tmp/ntp_status\n", + __FUNCTION__, __LINE__); + } + } + + double offset = 0.0; + int ret = chronyctl_get_offset(&offset); + if (ret == CHRONYCTL_SUCCESS) { + char offset_str[16]; + RDK_LOG(RDK_LOG_INFO, LOG_SYSTIME, "[ChronyCTL] Offset: %f seconds\n", offset); + snprintf(offset_str, sizeof(offset_str), "%.3f", offset); + #ifdef T2_EVENT_ENABLED + t2ValNotify((char *) "SYST_INFO_NTP_DELTA_split",offset_str); + #endif + } else { + RDK_LOG(RDK_LOG_ERROR, LOG_SYSTIME, "[ChronyCTL] Error fetching offset: %s\n", chronyctl_strerror(ret)); + } + /* Synchronisation captured — stop polling. */ + break; + } + + + RDK_LOG(RDK_LOG_INFO, LOG_SYSTIME, + "[%s:%d]: CHRONY: NTP sync monitor thread exiting\n", __FUNCTION__, __LINE__); +} + + void SysTimeMgr::processMsg() { while (1) @@ -231,6 +448,21 @@ void SysTimeMgr::runTimer() while (1) { std::this_thread::sleep_for(std::chrono::milliseconds(m_timerInterval)); + if (m_chronyRfcEnabled) + { + double offset = 0.0; + int ret = chronyctl_get_offset(&offset); + if (ret == CHRONYCTL_SUCCESS) { + char offset_str[16]; + RDK_LOG(RDK_LOG_INFO, LOG_SYSTIME, "[ChronyCTL] Offset: %f seconds\n", offset); + snprintf(offset_str, sizeof(offset_str), "%.3f", offset); + #ifdef T2_EVENT_ENABLED + t2ValNotify((char *) "SYST_INFO_NTP_DELTA_split",offset_str); + #endif + } else { + RDK_LOG(RDK_LOG_ERROR, LOG_SYSTIME, "[ChronyCTL] Error fetching offset: %s\n", chronyctl_strerror(ret)); + } + } sendMessage(eSYSMGR_EVENT_TIMER_EXPIRY,NULL); } } @@ -293,6 +525,23 @@ void SysTimeMgr::runPathMonitor() } } } + +static NetworkStatusSrc networkStatusMonitor; + +void SysTimeMgr::runNetworkStatusMonitor() +{ + RDK_LOG(RDK_LOG_INFO,LOG_SYSTIME,"[%s:%d]:CHRONY: Starting network status subscription thread\n",__FUNCTION__,__LINE__); + networkStatusMonitor.subscribeInternetStatusEvent(); +} + +void SysTimeMgr::runNWEventProcessing() +{ + /* This function runs on nwEventProcessThrd — call runEventProcessingLoop() + * directly, it blocks until shutdown. No inner thread needed. */ + RDK_LOG(RDK_LOG_INFO,LOG_SYSTIME,"[%s:%d]:CHRONY: Network event processing thread running\n",__FUNCTION__,__LINE__); + networkStatusMonitor.runEventProcessingLoop(); +} + void SysTimeMgr::updateTime(void* args) { updateTimeSync(0); @@ -399,6 +648,7 @@ void SysTimeMgr::setInitialTime() { locTime = i->getTime(); } + ofstream ofs(filepath); if (!ofs) { @@ -434,13 +684,28 @@ void SysTimeMgr::setInitialTime() if (clock_settime( CLOCK_REALTIME, &stime) != 0) { RDK_LOG(RDK_LOG_ERROR,LOG_SYSTIME,"[%s:%d]:Failed to set time \n",__FUNCTION__,__LINE__); + #ifdef T2_EVENT_ENABLED + t2CountNotify((char *) "SYST_ERROR_SYSTIME_FAIL",1); + #endif } else { + char str[32]; + struct timespec uptime; + unsigned long long uptimems; + RDK_LOG(RDK_LOG_INFO,LOG_SYSTIME,"[%s:%d]:Successfully to set time \n",__FUNCTION__,__LINE__); +#if !defined(MILESTONE_SUPPORT_DISABLED) logMilestone("SYSTEM_TIME_SET"); +#endif + if (clock_gettime(CLOCK_REALTIME, &uptime) == 0) { + uptimems = (unsigned long long)uptime.tv_sec * 1000 + uptime.tv_nsec / 1000000; + snprintf(str, sizeof(str), "%llu", uptimems); + #ifdef T2_EVENT_ENABLED + t2ValNotify((char *) "SYST_INFO_SETSYSTIME_split",str); + #endif + } } - publishStatus(ePUBLISH_TIME_INITIAL,"Poor"); } void SysTimeMgr::publishStatus(publishEvent event,string message) @@ -530,8 +795,8 @@ void SysTimeMgr::getTimeStatus(TimerMsg* pMsg) char monotimeStr[100] = {0}; strftime(timeStr, sizeof(timeStr), "%A %c", localtime(&timeinSec)); strftime(monotimeStr, sizeof(monotimeStr), "%A %c", localtime(&monotimeinSec)); - RDK_LOG(RDK_LOG_INFO,LOG_SYSTIME,"[%s:%d]:TIME Returning for Query = %ld, Converted Real Time(included in TimeMsg): %s, Converted Monotonic Time = %s \n",__FUNCTION__,__LINE__,timeinSec,timeStr,monotimeStr); - snprintf(pMsg->currentTime, cTIMER_STATUS_MESSAGE_LENGTH, "%s", std::to_string(timeinSec).c_str()); //CID:277708 Buffer not null terminated + RDK_LOG(RDK_LOG_INFO,LOG_SYSTIME,"[%s:%d]:TIME Returning for Query = %ld, Converted Real Time(included in TimeMsg): %s, Converted Monotonic Time = %s \n",__FUNCTION__,__LINE__,timeinSec,timeStr,monotimeStr); + snprintf(pMsg->currentTime, cTIMER_STATUS_MESSAGE_LENGTH, "%s", std::to_string(timeinSec).c_str()); //CID:277708 Buffer not null terminated } int SysTimeMgr::powerhandler(void* args) { @@ -556,6 +821,9 @@ void SysTimeMgr::deepsleepoff() { //Reset State Machine RDK_LOG(RDK_LOG_INFO,LOG_SYSTIME,"[%s:%d]:Deep Sleep is Turned off. Resetting State Machine and restarting ntp service. \n",__FUNCTION__,__LINE__); + + std::string message; + { std::lock_guard guard(g_state_mutex); if (m_timequality == eTIMEQUALILTY_SECURE) { m_timequality = eTIMEQUALILTY_GOOD; @@ -589,10 +857,45 @@ void SysTimeMgr::deepsleepoff() } publishStatus(ePUBLISH_DEEP_SLEEP_ON,std::move(message)); - + } //Turn on the NTP time sync. + + int ret = v_secure_system("/bin/systemctl is-active --quiet systemd-timesyncd.service"); + if (ret == 0) { + // timesyncd is running + RDK_LOG(RDK_LOG_INFO,LOG_SYSTIME,"[%s:%d]:systemd-timesyncd is active, restarting service\n",__FUNCTION__,__LINE__); v_secure_system("/bin/systemctl reset-failed systemd-timesyncd.service"); - v_secure_system("/bin/systemctl restart systemd-timesyncd.service"); + v_secure_system("/bin/systemctl restart systemd-timesyncd.service"); + } else { + // timesyncd not active, check chronyd + ret = v_secure_system("/bin/systemctl is-active --quiet chronyd.service"); + if (ret == 0) { + // chronyd is running + RDK_LOG(RDK_LOG_INFO,LOG_SYSTIME,"[%s:%d]:chronyd is active, performing chronyctl_burst\n",__FUNCTION__,__LINE__); + ret = chronyctl_burst(NULL, NULL, 4, 6); + if (ret == CHRONYCTL_SUCCESS) { + RDK_LOG(RDK_LOG_INFO,LOG_SYSTIME,"[%s:%d]: [ChronyCTL] burst succeeded\n",__FUNCTION__,__LINE__); + } else { + RDK_LOG(RDK_LOG_WARN,LOG_SYSTIME,"[%s:%d]:[ChronyCTL] burst failed: %s\n",__FUNCTION__,__LINE__, chronyctl_strerror(ret)); + } + // Wait for chronyd to synchronize with at least 1 source, for up to 20 tries. + RDK_LOG(RDK_LOG_INFO,LOG_SYSTIME,"[%s:%d]:chronyd is active, waiting for source selection\n",__FUNCTION__,__LINE__); + ret = chronyctl_waitsync(20,1); + if (ret != CHRONYCTL_SUCCESS) { + RDK_LOG(RDK_LOG_ERROR,LOG_SYSTIME,"[%s:%d]:[ChronyCTL] waitsync failed: %s\n",__FUNCTION__,__LINE__, chronyctl_strerror(ret)); + } + + RDK_LOG(RDK_LOG_INFO,LOG_SYSTIME,"[%s:%d]:chronyd is active, performing chronyctl_makestep\n",__FUNCTION__,__LINE__); + ret = chronyctl_makestep(); + if (ret == CHRONYCTL_SUCCESS) { + RDK_LOG(RDK_LOG_INFO, LOG_SYSTIME, "[%s:%d]: [ChronyCTL] makestep succeeded\n", __FUNCTION__, __LINE__); + } else { + RDK_LOG(RDK_LOG_WARN, LOG_SYSTIME, "[%s:%d]: [ChronyCTL] makestep failed: ret=%d, error=%s\n", __FUNCTION__, __LINE__, ret, chronyctl_strerror(ret)); + } + } else { + RDK_LOG(RDK_LOG_WARN,LOG_SYSTIME,"[%s:%d]:Neither systemd-timesyncd nor chronyd is running, skipping time sync actions.\n",__FUNCTION__,__LINE__); + } + } } void SysTimeMgr::deepsleepon() diff --git a/systimemgr.h b/systimemgr.h index 8b67b73e..430b193f 100644 --- a/systimemgr.h +++ b/systimemgr.h @@ -36,7 +36,12 @@ #include "itimermsg.h" #include +#ifdef T2_EVENT_ENABLED +#include +void t2CountNotify(char *marker, int val); +void t2ValNotify(char *marker, char *val); +#endif using namespace std; typedef enum @@ -80,7 +85,7 @@ typedef struct sysTimeMsg class SysTimeMgr { -private: +private: typedef void (SysTimeMgr::*memfunc)(void* args); map > stateMachine; map m_pathEventMap; @@ -89,9 +94,9 @@ class SysTimeMgr unsigned long m_timerInterval; qualityOfTime m_timequality; - string m_timersrc; - const string m_directory = "/tmp/systimemgr"; + const string m_directory = "/tmp/systimemgr"; + vector m_timerSrc; vector m_timerSync; @@ -99,10 +104,11 @@ class SysTimeMgr IPublish* m_publish; ISubscribe* m_subscriber; ISubscribe* m_tmrsubscriber; + string m_timersrc; //Config file to load plugins. string m_cfgfile; - + bool m_chronyRfcEnabled; SysTimeMgr (string cfgfile = "/etc/systimemgr.conf"); void setInitialTime(); @@ -110,8 +116,14 @@ class SysTimeMgr static void processThr(SysTimeMgr* instance); static void timerThr(SysTimeMgr* instance); static void pathThr(SysTimeMgr* instance); + static void nwEventSubscribeThr(SysTimeMgr* instance); + static void nwEventProcessThr(SysTimeMgr* instance); + static void ntpSyncMonitorThr(SysTimeMgr* instance); + void runNetworkStatusMonitor(); + void runNWEventProcessing(); + void runNTPSyncMonitor(); void updateTimeSync(long long updateTime); void publishStatus(publishEvent event,string message); @@ -123,7 +135,7 @@ class SysTimeMgr static recursive_mutex g_state_mutex; static mutex g_instance_mutex; static SysTimeMgr* pInstance; - + // LIstening socket and its related addresses etc. public: @@ -155,6 +167,45 @@ class SysTimeMgr void deepsleepon(); void deepsleepoff(); +#ifdef GTEST_ENABLE +friend class SysTimeMgrTest_RunStateMachine_HitsFunctionPointer_Test; +friend class SysTimeMgrTest_RunPathMonitorFileExistsAtStartup_Test; +friend class SysTimeMgrTest_RunPathMonitorCoversInotifyEvent_Test; +friend class SysTimeMgrTest_RunStateMachine_AllStatesEvents_Test; +friend class SysTimeMgrTest_SetInitialTime_NonZeroTime_Test; +friend class SysTimeMgrTest; +friend class SysTimeMgrTest_DestructorCovers_Test; +friend class SysTimeMgrTest_SetInitialTime_ZeroTime_Test; +friend class SysTimeMgrTest_UpdateTime_InvokesCheckTime_Test; +friend class SysTimeMgrTest_SetInitialTime_FileCreationFails_Test; +friend class SysTimeMgrTest_TimerExpiryUsesReference_Test; +friend class SysTimeMgrTest_TimerExpiryUsesFileTime_Test; +friend class SysTimeMgrTest_UpdateTimeSyncCallsSyncs_Test; +friend class SysTimeMgrTest_NtpAquiredPublishesStatusAndUpdatesState_Test; +friend class SysTimeMgrTest_NtpFailed_Test; +friend class SysTimeMgrTest_DttAcquiredPublishesStatusAndUpdatesState_Test; +friend class SysTimeMgrTest_SecureTimeAcquiredUpdatesState_Test; +friend class SysTimeMgrTest_UpdateSecureTimePublishesStatusAndUpdatesState_Test; +friend class SysTimeMgrTest_DeepSleepOffPublishesStatus_Test; +friend class SysTimeMgrTest_DeepSleepOffCoversPoorCase_Test; +friend class SysTimeMgrTest_UpdateClockRealTimeSetsTime_Test; +friend class SysTimeMgrTest_UpdateClockRealTimeAllBranches_Test; +friend class SysTimeMgrTest_GetTimeStatus_AllQualities_Test; +friend class SysTimeMgrTest_PublishStatusCoversAll_Test; +friend class SysTimeMgrTest_TimerExpiry_RefVsFileTime_Test; +friend class SysTimeMgrTest_RunPathMonitorCoversInotifyEvent_Test; +friend class SysTimeMgrTest_RunPathMonitorFileExistsAtStartup_Test; +friend class SysTimeMgrTest_RunPathMonitorInotifyAddWatchFails_Test; +friend class SysTimeMgrTest_RunPathMonitorCoversInotifyEvent_Test; +friend class SysTimeMgrTest_RunPathMonitorFileExistsAtStartup_Test; +friend class SysTimeMgrTest_RunPathMonitorInotifyAddWatchFails_Test; +friend class SysTimeMgrTest_UpdateClockRealTimeSetsTime_Test; +friend class SysTimeMgrTest_TimerExpiry_RefVsFileTime_Test; +friend class SysTimeMgrTest_ProcessThrCallsProcessMsgAndRunsOneIteration_Test; +friend class SysTimeMgrTest_TimerThrCallsRunTimerAndRunsOnce_Test; +friend class SysTimeMgrTest_TimerThrAndProcessThrCoverage_Test; +#endif + }; diff --git a/systimerfactory/Makefile.am b/systimerfactory/Makefile.am index a6477597..7bb342b2 100644 --- a/systimerfactory/Makefile.am +++ b/systimerfactory/Makefile.am @@ -22,7 +22,8 @@ ACLOCAL_AMFLAGS = -I m4 lib_LTLIBRARIES = libsystimerfactory.la -libsystimerfactory_la_SOURCES = timerfactory.cpp pubsubfactory.cpp rdkdefaulttimesync.cpp drmtimersrc.cpp +libsystimerfactory_la_SOURCES = timerfactory.cpp pubsubfactory.cpp rdkdefaulttimesync.cpp drmtimersrc.cpp networkstatussrc.cpp +libsystimerfactory_la_LDFLAGS = -lchronyctl if IARM_ENABLED libsystimerfactory_la_SOURCES += iarmpublish.cpp iarmsubscribe.cpp iarmtimerstatussubscriber.cpp @@ -33,6 +34,11 @@ libsystimerfactory_la_SOURCES += iarmpowersubscriber.cpp endif endif +if IS_TELEMETRY2_ENABLED +libsystimerfactory_la_CPPFLAGS = $(T2_EVENT_FLAG) +libsystimerfactory_la_LDFLAGS += -ltelemetry_msgsender -lt2utils +endif + if DTT_ENABLED libsystimerfactory_la_SOURCES += dtttimersrc.cpp diff --git a/systimerfactory/configure.ac b/systimerfactory/configure.ac index 3e0833da..e3b80542 100644 --- a/systimerfactory/configure.ac +++ b/systimerfactory/configure.ac @@ -147,6 +147,21 @@ AC_ARG_ENABLE([wpevgdrm], ], [echo "wpevgdrm build is enable"]) +AC_ARG_ENABLE([t2api], + AS_HELP_STRING([--enable-t2api],[enables telemetry]), + [ + case "${enableval}" in + yes) IS_TELEMETRY2_ENABLED=true + T2_EVENT_FLAG=" -DT2_EVENT_ENABLED ";; + no) IS_TELEMETRY2_ENABLED=false ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-t2enable]) ;; + esac + ], + [echo "telemetry is disabled"]) +AM_CONDITIONAL([IS_TELEMETRY2_ENABLED], [test x$IS_TELEMETRY2_ENABLED = xtrue]) +AC_SUBST(T2_EVENT_FLAG) + + AM_CONDITIONAL([IARM_ENABLED], [test x$IARM_ENABLED = xtrue]) AM_CONDITIONAL([PWRMGRPLUGIN_ENABLED], [test x$PWRMGRPLUGIN_ENABLED = xtrue]) AM_CONDITIONAL([DTT_ENABLED], [test x$DTT_ENABLED = xtrue]) diff --git a/systimerfactory/iarmsubscribe.h b/systimerfactory/iarmsubscribe.h index 637e89ff..c0700809 100644 --- a/systimerfactory/iarmsubscribe.h +++ b/systimerfactory/iarmsubscribe.h @@ -28,13 +28,18 @@ using namespace std; class IarmSubscriber:public ISubscribe { - private: + private: static IarmSubscriber* pInstance; public: IarmSubscriber(string sub); static IarmSubscriber* getInstance() { return pInstance;} virtual bool subscribe(string eventname,funcPtr fptr)=0; + +#ifdef GTEST_ENABLE +friend class IarmPowerSubscriberTest_PowerEventHandler_NullSingleton_InstancePathCovered_Test; +#endif + }; diff --git a/systimerfactory/ipowercontrollersubscriber.h b/systimerfactory/ipowercontrollersubscriber.h index 0f477d32..8d6f55de 100755 --- a/systimerfactory/ipowercontrollersubscriber.h +++ b/systimerfactory/ipowercontrollersubscriber.h @@ -46,7 +46,7 @@ typedef struct SysTimeMgr_Power_Event_State{ class IpowerControllerSubscriber:public IarmSubscriber { - private: + private: funcPtr m_powerHandler; std::queue m_pwrEvtQueue; std::mutex m_pwrEvtQueueLock; @@ -68,6 +68,15 @@ class IpowerControllerSubscriber:public IarmSubscriber void sysTimeMgrInitPwrEvt(void); void sysTimeMgrDeinitPwrEvt(void); ~IpowerControllerSubscriber(); + + +#ifdef GTEST_ENABLE + friend class IpowerControllerSubscriberTest_HandlePwrEventData_DeepSleepOn_Test; + friend class IpowerControllerSubscriberTest_HandlePwrEventData_DeepSleepOff_Test; + friend class IpowerControllerSubscriberTest_HandlePwrEventData_UnknownNewState_LogsError_Test; + friend class IpowerControllerSubscriberTest_HandlePwrEventData_NoHandler_DoesNotCrash_Test; + friend class TestableSubscriber; +#endif }; diff --git a/systimerfactory/networkstatussrc.cpp b/systimerfactory/networkstatussrc.cpp new file mode 100644 index 00000000..5fc6935e --- /dev/null +++ b/systimerfactory/networkstatussrc.cpp @@ -0,0 +1,436 @@ +/* + * Copyright 2023 Comcast Cable Communications Management, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include +#include +#include +#include +#include +#include + +#include "networkstatussrc.h" +#include "secure_wrapper.h" + +#if defined(GTEST_ENABLE) || defined(__LOCAL_TEST_) +#include "unittest/mocks/libchronyctl.h" +#else +#include "libchronyctl.h" +#endif + +#include +#include +#include + +#include +#include + +/* Test builds include unittest/mocks/thunder/WPEFrameworkMock.h to avoid a + * full Thunder/WPEFramework installation. For GTEST_ENABLE (L1 unit tests), + * WPEFrameworkMock.h provides printf-based RDK_LOG stubs and does not include + * irdklog.h. For __LOCAL_TEST_ (L2 functional tests), WPEFrameworkMock.h + * includes the real irdklog.h, so logging uses the real rdklogger backend and + * writes to /opt/logs. In production builds, the real Thunder headers and + * libraries are used directly. */ +#if defined(GTEST_ENABLE) || defined(__LOCAL_TEST_) +# include "unittest/mocks/thunder/WPEFrameworkMock.h" +using namespace WPEFramework; +#else +# include "irdklog.h" +#include "core/SystemInfo.h" +#include "websocket/JSONRPCLink.h" +using namespace WPEFramework; +#endif + + +using namespace std; +static std::string lastStatus; + +/* Offset threshold in seconds above which makestep is triggered even if + * chrony already has a selectable source (natural slewing would be too slow). */ +static constexpr double OFFSET_STEP_THRESHOLD_S = 1.0; + +const unsigned int ACTIVATION_RETRY_INTERVAL_MS = 1000; + +const char* NETWORK_MANAGER_CALLSIGN = "org.rdk.NetworkManager"; + +static WPEFramework::JSONRPC::SmartLinkType* thunder_client = nullptr; +static bool m_networkeventsubscribed = false; + +/* Shared state between the Thunder callback (ResourceMonitor I/O thread) and + * the network event processing thread. All fields guarded by state.mutex. + * + * Using the "construct on first use" idiom (function-local static) instead of + * plain file-scope globals eliminates the Coverity GLOBAL_INIT_ORDER (CWE-908) + * defect. The static 'NetworkSharedState' object is initialised the first time + * sharedState() is called — which happens inside the NetworkStatusSrc + * constructor below. Because C++ destroys all static-duration objects in + * reverse initialisation order, sharedState() is guaranteed to outlive every + * NetworkStatusSrc instance and the destructor can safely use the mutex and + * condition variable regardless of how many translation units are linked. */ +struct NetworkSharedState { + bool internetUpPending = false; + std::mutex mutex; + std::condition_variable cv; + bool stopProcessing = false; +}; + +static NetworkSharedState& sharedState() +{ + static NetworkSharedState s; + return s; +} + +/* Calling sharedState() here ensures the NetworkSharedState singleton is + * initialised before any NetworkStatusSrc object (including the file-scope + * 'networkStatusMonitor' in systimemgr.cpp). It will therefore be destroyed + * after the last NetworkStatusSrc destructor runs. */ +NetworkStatusSrc::NetworkStatusSrc() +{ + (void)sharedState(); /* force initialisation — return value intentionally unused */ +} + + +/* Runs on the systimemgr event processing thread — safe to block here. + * Called only when a new fully_connected transition is confirmed by the callback. + * Runs all chrony sync commands away from Thunder's I/O thread. */ +static void processInternetOnline() +{ + RDK_LOG(RDK_LOG_INFO,LOG_SYSTIME,"[%s:%d]: CHRONY: Processing internet fully_connected event\n", + __FUNCTION__,__LINE__); + + bool firstSyncDone = (access("/tmp/clock-event", F_OK) == 0); + + if (!firstSyncDone) { + /* /tmp/clock-event is absent — NTP has never successfully synced on + * this boot. Three sub-cases: + * + * A) chronyd not yet started: internet is already up; chronyd will + * reach its servers immediately via iburst on startup. Nothing to do. + * + * B) chronyd running, sources visible (any state, including '^?'): + * iburst is in progress or DNS resolved and polling has started. + * Let chronyd finish naturally; 'makestep 1.0 4' in chrony.conf + * handles the initial step correction. + * + * C) chronyd running, NO source entries: sources are completely + * offline (device booted without internet and DNS may not have + * resolved yet). Next natural poll could be many minutes away. + * Call chronyctl_online() to mark sources reachable and trigger + * iburst immediately. + * + * Discriminators: + * 1. 'systemctl is-active chronyd' — non-zero → Case A. + * 2. chronyctl_get_source_count() → count > 0 → Case B; + * count == 0 → Case C. */ + int chronyActive = v_secure_system("/bin/systemctl is-active --quiet chronyd.service"); + if (chronyActive != 0) { + /* Case A */ + RDK_LOG(RDK_LOG_INFO, LOG_SYSTIME, + "[%s:%d]: CHRONY: First sync pending — chronyd not yet started." + " Internet is up; will sync via iburst on startup.\n", + __FUNCTION__, __LINE__); + } else { + /* chronyd is running — check how many sources it has */ + int srcCount = 0; + int srcCountRet = chronyctl_get_source_count(&srcCount); + + if (srcCountRet != CHRONYCTL_SUCCESS) { + RDK_LOG(RDK_LOG_ERROR, LOG_SYSTIME, + "[%s:%d]: [ChronyCTL] Failed to get source count (%s)." + " Falling back safely and not calling chronyctl_online.\n", + __FUNCTION__, __LINE__, chronyctl_strerror(srcCountRet)); + } else if (srcCount > 0) { + /* Case B: at least one source entry visible (iburst running or + * polling started). Let chronyd complete the sync on its own. */ + RDK_LOG(RDK_LOG_INFO, LOG_SYSTIME, + "[%s:%d]: [ChronyCTL] First sync pending — %d source(s) present" + " (iburst/polling in progress). No action needed.\n", + __FUNCTION__, __LINE__, srcCount); + } else { + /* Case C: no source entries — sources are offline; next poll + * could be delayed by many minutes without intervention. */ + RDK_LOG(RDK_LOG_INFO, LOG_SYSTIME, + "[%s:%d]: [ChronyCTL] First sync pending — no sources found." + " Device likely booted without internet. Calling chronyctl_online.\n", + __FUNCTION__, __LINE__); + int ret = chronyctl_online(NULL, NULL); + if (ret == CHRONYCTL_SUCCESS) { + RDK_LOG(RDK_LOG_INFO, LOG_SYSTIME, + "[%s:%d]: [ChronyCTL] chronyctl_online succeeded." + " iburst will fire automatically.\n", + __FUNCTION__, __LINE__); + } else { + RDK_LOG(RDK_LOG_ERROR, LOG_SYSTIME, + "[%s:%d]: [ChronyCTL] chronyctl_online failed (%s)." + " chronyd will sync on its own schedule.\n", + __FUNCTION__, __LINE__, chronyctl_strerror(ret)); + } + } + } + } else { + /* Check whether chrony already has a selectable (selected '*' or + * combined '+') source using the library API. */ + int hasSelectable = 0; + int selRet = chronyctl_has_selectable_source(&hasSelectable); + bool hasSelectableSource = (selRet == CHRONYCTL_SUCCESS && hasSelectable != 0); + + if (!hasSelectableSource) { + /* No selectable source — issue burst to gather fresh samples, then + * wait up to 10 s for chrony to select one before stepping the clock. + * makestep MUST NOT run if waitsync times out — chrony has no synced + * reference at that point and makestep will fail/produce garbage. */ + RDK_LOG(RDK_LOG_INFO,LOG_SYSTIME, + "[%s:%d]: [ChronyCTL] No selectable source found, issuing burst 3/4\n", + __FUNCTION__,__LINE__); + int burstRet = chronyctl_burst(NULL, NULL, 4, 6); + if (burstRet != CHRONYCTL_SUCCESS) { + RDK_LOG(RDK_LOG_WARN,LOG_SYSTIME, + "[%s:%d]: [ChronyCTL] chronyctl_burst failed: %s\n", + __FUNCTION__,__LINE__, chronyctl_strerror(burstRet)); + } else { + RDK_LOG(RDK_LOG_INFO,LOG_SYSTIME, + "[%s:%d]: [ChronyCTL] burst triggered, waiting for source selection\n", + __FUNCTION__,__LINE__); + } + + /* Poll every 1 s, up to 20 tries (~20 s). */ + RDK_LOG(RDK_LOG_INFO,LOG_SYSTIME, + "[%s:%d]: [ChronyCTL] Waiting for source selection (max 10s)\n", + __FUNCTION__,__LINE__); + int waitRet = chronyctl_waitsync(20, 1); + if (waitRet != CHRONYCTL_SUCCESS) { + RDK_LOG(RDK_LOG_ERROR,LOG_SYSTIME, + "[%s:%d]: [ChronyCTL] waitsync timed out (%s), no synced source available." + " Skipping makestep.\n", + __FUNCTION__,__LINE__, chronyctl_strerror(waitRet)); + } else { + RDK_LOG(RDK_LOG_INFO,LOG_SYSTIME, + "[%s:%d]: [ChronyCTL] waitsync completed, source selected. Proceeding to Makestep.\n", + __FUNCTION__,__LINE__); + int stepRet = chronyctl_makestep(); + if (stepRet != CHRONYCTL_SUCCESS) { + RDK_LOG(RDK_LOG_ERROR,LOG_SYSTIME, + "[%s:%d]: [ChronyCTL] Makestep failed: %s\n", + __FUNCTION__,__LINE__, chronyctl_strerror(stepRet)); + } else { + RDK_LOG(RDK_LOG_INFO,LOG_SYSTIME, + "[%s:%d]: [ChronyCTL] Makestep completed successfully\n", + __FUNCTION__,__LINE__); + } + } + } else { + /* Selectable source already present. + * Only step the clock if the current offset exceeds the threshold; + * smaller offsets are handled faster and safer by chrony's natural + * frequency-slewing, so an explicit makestep is unnecessary. */ + double offset = 0.0; + int offRet = chronyctl_get_system_time_offset(&offset); + if (offRet != CHRONYCTL_SUCCESS) { + RDK_LOG(RDK_LOG_WARN, LOG_SYSTIME, + "[%s:%d]: [ChronyCTL] chronyctl_get_system_time_offset failed: %s. " + "Falling back to offset = 0.0 and allowing natural slew\n", + __FUNCTION__, __LINE__, chronyctl_strerror(offRet)); + offset = 0.0; /* safe default — allow natural slew on error */ + } + double absOffset = std::fabs(offset); + RDK_LOG(RDK_LOG_INFO, LOG_SYSTIME, + "[%s:%d]: [ChronyCTL] Selectable source present, current offset = %.6f s" + " (threshold = %.1f s)\n", + __FUNCTION__, __LINE__, offset, OFFSET_STEP_THRESHOLD_S); + + if (absOffset > OFFSET_STEP_THRESHOLD_S) { + RDK_LOG(RDK_LOG_INFO, LOG_SYSTIME, + "[%s:%d]: [ChronyCTL] Offset exceeds threshold, issuing makestep\n", + __FUNCTION__, __LINE__); + int stepRet = chronyctl_makestep(); + if (stepRet != CHRONYCTL_SUCCESS) { + RDK_LOG(RDK_LOG_ERROR, LOG_SYSTIME, + "[%s:%d]: [ChronyCTL] Makestep failed: %s\n", + __FUNCTION__, __LINE__, chronyctl_strerror(stepRet)); + } else { + RDK_LOG(RDK_LOG_INFO, LOG_SYSTIME, + "[%s:%d]: [ChronyCTL] Makestep completed successfully\n", + __FUNCTION__, __LINE__); + } + } else { + RDK_LOG(RDK_LOG_INFO, LOG_SYSTIME, + "[%s:%d]: [ChronyCTL] Offset (%.6f s) within threshold — allowing" + " natural slew, no makestep needed\n", + __FUNCTION__, __LINE__, offset); + } + } /* end if (!hasSelectableSource) */ + } /* end else (firstSyncDone) */ +} + + +/* Thunder event callback — runs on the ResourceMonitor I/O thread. + * MUST return immediately: only updates state and signals the processing thread. + * + * lastStatus is always updated here so that an up→down→up sequence is never + * lost: we track the down event so the next up is not seen as a duplicate. */ +void handle_internetStatusChange(const JsonObject& params) +{ + std::string status; + if (params.HasLabel("status")) + status = params["status"].String(); + else if (params.HasLabel("internetStatus")) + status = params["internetStatus"].String(); + + std::string normalizedStatus(std::move(status)); + std::transform(normalizedStatus.begin(), normalizedStatus.end(), normalizedStatus.begin(), + [](unsigned char ch) { return static_cast(std::tolower(ch)); }); + + std::lock_guard lock(sharedState().mutex); + std::string prev = lastStatus; + lastStatus = normalizedStatus; /* always track latest state, even non-connected */ + + if (normalizedStatus != "fully_connected" || normalizedStatus == prev) { + RDK_LOG(RDK_LOG_INFO, LOG_SYSTIME, + "[%s:%d]: CHRONY: Internet status=%s (prev=%s) — no action needed\n", + __FUNCTION__, __LINE__, normalizedStatus.c_str(), prev.c_str()); + return; + } + + RDK_LOG(RDK_LOG_INFO, LOG_SYSTIME, + "[%s:%d]: CHRONY: Internet status changed to fully_connected — signalling processing thread\n", + __FUNCTION__, __LINE__); + + sharedState().internetUpPending = true; + sharedState().cv.notify_one(); +} + +/* runEventProcessingLoop — runs on systimemgr's dedicated nwEventProcessThrd. + * Sleeps waiting for fully_connected signals from handle_internetStatusChange, + * then runs all chrony sync commands. Safe to block here. + * + * Rapid events (multiple up or up→down→up): the g_internetUpPending flag is a + * single-slot latch. If the thread is busy in processInternetOnline() when a + * new event arrives, the flag is set to true. When the thread returns to wait(), + * the predicate is immediately true so it processes again — no events are lost + * and no stale intermediate states are queued. */ +void NetworkStatusSrc::runEventProcessingLoop() +{ + RDK_LOG(RDK_LOG_INFO, LOG_SYSTIME, + "[%s:%d]: CHRONY: Network event processing thread started\n", __FUNCTION__, __LINE__); + + while (true) { + { + std::unique_lock lock(sharedState().mutex); + /* Expand the predicate lambda into an explicit while-loop so that + * every read of internetUpPending and stopProcessing is visibly + * performed while lock is held (fixes Coverity MISSING_LOCK). The + * behaviour is identical to the two-argument wait(lock, pred) form: + * spurious wake-ups are handled by re-checking the condition, and + * the mutex is always held when the shared flags are read. */ + while (!sharedState().internetUpPending && !sharedState().stopProcessing) { + sharedState().cv.wait(lock); + } + if (sharedState().stopProcessing) + break; + sharedState().internetUpPending = false; /* clear — we are about to handle it */ + } + processInternetOnline(); + } + + RDK_LOG(RDK_LOG_INFO, LOG_SYSTIME, + "[%s:%d]: CHRONY: Network event processing thread exiting\n", __FUNCTION__, __LINE__); +} + + + +static void subscribeToInternetEvent() +{ + unsigned int attempt = 0; + while (!m_networkeventsubscribed) { + /* Check stop flag before each attempt so that a shutdown request issued + * while a Subscribe() call was in flight is honoured at the top of the + * next iteration without waiting a full retry interval. */ + { + std::unique_lock lock(sharedState().mutex); + if (sharedState().stopProcessing) { + RDK_LOG(RDK_LOG_INFO, LOG_SYSTIME, + "[%s:%d]: CHRONY: Subscription retry loop exiting — shutdown requested\n", + __FUNCTION__, __LINE__); + return; + } + } + attempt++; + if (!thunder_client) + thunder_client = new WPEFramework::JSONRPC::SmartLinkType(NETWORK_MANAGER_CALLSIGN, ""); + + if (thunder_client) { + int32_t ret = thunder_client->Subscribe(5000, "onInternetStatusChange", &handle_internetStatusChange); + if (ret == Core::ERROR_NONE) { + RDK_LOG(RDK_LOG_INFO,LOG_SYSTIME,"[%s:%d]: CHRONY: Successfully subscribed to onInternetStatusChange (attempt %u)\n", __FUNCTION__,__LINE__,attempt); + m_networkeventsubscribed = true; + return; + } + RDK_LOG(RDK_LOG_WARN,LOG_SYSTIME,"[%s:%d]: CHRONY: Subscribe to onInternetStatusChange failed (%d), attempt %u\n",__FUNCTION__,__LINE__,ret,attempt); + delete thunder_client; thunder_client = nullptr; + } + /* Sleep interruptibly: the destructor sets stopProcessing and calls + * cv.notify_all(), which wakes this wait early at shutdown instead of + * always blocking for the full ACTIVATION_RETRY_INTERVAL_MS. */ + { + std::unique_lock lock(sharedState().mutex); + const auto deadline = std::chrono::steady_clock::now() + + std::chrono::milliseconds(ACTIVATION_RETRY_INTERVAL_MS); + while (!sharedState().stopProcessing) { + if (sharedState().cv.wait_until(lock, deadline) == std::cv_status::timeout) + break; /* interval elapsed — proceed to next attempt */ + } + if (sharedState().stopProcessing) { + RDK_LOG(RDK_LOG_INFO, LOG_SYSTIME, + "[%s:%d]: CHRONY: Subscription retry loop interrupted by shutdown" + " after attempt %u\n", __FUNCTION__, __LINE__, attempt); + return; + } + } + } +} + +void NetworkStatusSrc::subscribeInternetStatusEvent() +{ + Core::SystemInfo::SetEnvironment("THUNDER_ACCESS", "127.0.0.1:9998"); + subscribeToInternetEvent(); +} + +NetworkStatusSrc::~NetworkStatusSrc() +{ + /* Set the stop flag and wake both the event processing thread and the + * subscription retry thread: + * - runEventProcessingLoop() sleeps on cv.wait() — notify_all() makes it + * check stopProcessing and exit cleanly. + * - subscribeToInternetEvent() sleeps on cv.wait_for() between retries — + * notify_all() wakes it early so nwEventSubscribeThrd.join() does not + * hang for up to ACTIVATION_RETRY_INTERVAL_MS when shutting down while + * Thunder/NetworkManager is unavailable. + * notify_all() is used (not notify_one()) because both threads may be + * sleeping concurrently and all waiters must be unblocked. */ + { + std::lock_guard lock(sharedState().mutex); + sharedState().stopProcessing = true; + } + sharedState().cv.notify_all(); + + if (thunder_client) { + thunder_client->Unsubscribe(5000, "onInternetStatusChange"); + delete thunder_client; + thunder_client = nullptr; + } + +} diff --git a/systimerfactory/networkstatussrc.h b/systimerfactory/networkstatussrc.h new file mode 100644 index 00000000..5e9e58f7 --- /dev/null +++ b/systimerfactory/networkstatussrc.h @@ -0,0 +1,40 @@ +/* + * Copyright 2023 Comcast Cable Communications Management, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +#ifndef NETWORKSTATUSSRC_H_ +#define NETWORKSTATUSSRC_H_ + + +class NetworkStatusSrc +{ + public: + /* Constructor ensures the shared state (mutex/cv/flags) is initialised + * before this object, so that the destructor is always called first + * and the shared state is destroyed last (fixes Coverity GLOBAL_INIT_ORDER). */ + NetworkStatusSrc(); + + /* Called on nwEventSubscribeThrd: retries until subscription succeeds, then returns. */ + void subscribeInternetStatusEvent(); + + /* Called on nwEventProcessThrd: blocks waiting for internet-up events, runs chrony sync. + * Returns only when g_stopProcessing is set (destructor called). */ + void runEventProcessingLoop(); + + ~NetworkStatusSrc(); +}; + +#endif // NETWORKSTATUSSRC_H_ diff --git a/systimerfactory/ntptimesrc.h b/systimerfactory/ntptimesrc.h index d09729c1..9c1695dd 100644 --- a/systimerfactory/ntptimesrc.h +++ b/systimerfactory/ntptimesrc.h @@ -32,7 +32,7 @@ class NtpTimeSrc : public ITimeSrc long long getTimeSec(){ struct ntptimeval tVal; ntp_gettime(&tVal); - RDK_LOG(RDK_LOG_DEBUG,LOG_SYSTIME,"[%s:%d]:NTP Time Values, MaxValue = %d, Time in Sec = %d, Time in Microsec = %d, Estimated Error = %d, TAI = %d \n",__FUNCTION__,__LINE__,tVal.maxerror,tVal.time.tv_sec,tVal.time.tv_usec,tVal.esterror,tVal.tai); + RDK_LOG(RDK_LOG_DEBUG,LOG_SYSTIME,"[%s:%d]:NTP Time Values, MaxValue = %ld, Time in Sec = %lu, Time in Microsec = %d, Estimated Error = %ld, TAI = %ld \n",__FUNCTION__,__LINE__,tVal.maxerror,tVal.time.tv_sec,tVal.time.tv_usec,tVal.esterror,tVal.tai); return tVal.time.tv_sec; } diff --git a/systimerfactory/rdkdefaulttimesync.cpp b/systimerfactory/rdkdefaulttimesync.cpp index 64a5877f..ada8397b 100644 --- a/systimerfactory/rdkdefaulttimesync.cpp +++ b/systimerfactory/rdkdefaulttimesync.cpp @@ -19,6 +19,26 @@ #include "irdklog.h" #include #include +#ifdef T2_EVENT_ENABLED +#include +#endif + + +/* Description: Use for sending telemetry Log + * @param marker: use for send marker details + * @return : void + * */ + +#ifdef T2_EVENT_ENABLED +void t2CountNotify(char *marker, int val) { + t2_event_d(marker, val); +} + +void t2ValNotify( char *marker, char *val ) +{ + t2_event_s(marker, val); +} +#endif using namespace std::chrono; map RdkDefaultTimeSync::tokenize(string const& s,string token) @@ -88,7 +108,7 @@ long long RdkDefaultTimeSync::getTime() myfile>>clock_time; } - + ver_time = buildtime(); if (clock_time > ver_time) { @@ -96,6 +116,9 @@ long long RdkDefaultTimeSync::getTime() time_t safe_clock_time = static_cast(clock_time); // Explicit conversion to time_t strftime(timeStr, sizeof(timeStr), "%A %c", localtime(&safe_clock_time)); // Pass time_t pointer RDK_LOG(RDK_LOG_INFO,LOG_SYSTIME,"[%s:%d]:Returning Last Known Good Time, time = %s \n",__FUNCTION__,__LINE__,timeStr); + #ifdef T2_EVENT_ENABLED + t2ValNotify((char *) "SYST_INFO_SYSLKG_split",timeStr); + #endif m_currentTime = clock_time; return clock_time; } @@ -104,6 +127,9 @@ long long RdkDefaultTimeSync::getTime() time_t safe_ver_time = static_cast(ver_time); // Explicit conversion to time_t strftime(timeStr, sizeof(timeStr), "%A %c", localtime(&safe_ver_time)); // Pass time_t pointer RDK_LOG(RDK_LOG_INFO,LOG_SYSTIME,"[%s:%d]:Returning build time, Time = %s\n",__FUNCTION__,__LINE__,timeStr); + #ifdef T2_EVENT_ENABLED + t2CountNotify("SYST_INFO_SYSBUILD",1); + #endif return ver_time; } diff --git a/systimerfactory/rdkdefaulttimesync.h b/systimerfactory/rdkdefaulttimesync.h index d4f49436..afcd4a7c 100644 --- a/systimerfactory/rdkdefaulttimesync.h +++ b/systimerfactory/rdkdefaulttimesync.h @@ -25,11 +25,12 @@ #include #include + using namespace std; class RdkDefaultTimeSync: public ITimeSync -{ - private: - string m_path; +{ + private: + string m_path; long long m_currentTime; map tokenize(string const& s,string token); long long buildtime(); @@ -38,6 +39,10 @@ class RdkDefaultTimeSync: public ITimeSync ~RdkDefaultTimeSync(){} virtual void updateTime(long long locTime); virtual long long getTime(); + +#ifdef GTEST_ENABLE +friend class RdkDefaultTimeSyncTest_tokenizeBreakCoverage_Test; +#endif }; #endif// _RDKDEFAULTTIMESYNC_H_ diff --git a/systimerfactory/stttimesrc.h b/systimerfactory/stttimesrc.h index 7c4bf1a2..009bd131 100644 --- a/systimerfactory/stttimesrc.h +++ b/systimerfactory/stttimesrc.h @@ -31,7 +31,7 @@ class SttTimeSrc : public ITimeSrc long long getTimeSec(){ time_t timeinSec = time(NULL); - RDK_LOG(RDK_LOG_DEBUG,LOG_SYSTIME,"[%s:%d]:STT Time in seconds = %d \n",__FUNCTION__,__LINE__,timeinSec); + RDK_LOG(RDK_LOG_DEBUG,LOG_SYSTIME,"[%s:%d]:STT Time in seconds = %lu \n",__FUNCTION__,__LINE__,timeinSec); return timeinSec; } diff --git a/systimerfactory/unittest/Makefile.am b/systimerfactory/unittest/Makefile.am index ff0b6a60..2ae02e00 100644 --- a/systimerfactory/unittest/Makefile.am +++ b/systimerfactory/unittest/Makefile.am @@ -15,7 +15,8 @@ # # SPDX-License-Identifier: Apache-2.0 # - +AUTOMAKE_OPTIONS = subdir-objects +ACLOCAL_AMFLAGS = -I m4 # Define the program name and the source files bin_PROGRAMS = drmtest_gtest dtttest_gtest rdkDefaulttest_gtest timerfactory_gtest pubsubfactory_gtest ipowercontrollersubscriber_gtest iarmtimerstatus_gtest iarmsubscribe_gtest iarmpublish_gtest iarmpowersubscribe_gtest systimemgr_gtest @@ -28,6 +29,10 @@ COMMON_LDADD = -ljsoncpp -lgtest -lgtest_main -lgmock_main -lgmock -ljsoncpp # Define the compiler flags COMMON_CXXFLAGS = -frtti -std=c++14 +AM_CFLAGS = -fprofile-arcs -ftest-coverage -O0 -g +AM_CXXFLAGS = -fprofile-arcs -ftest-coverage -O0 -g +AM_LDFLAGS = -fprofile-arcs -ftest-coverage + # Define the source files drmtest_gtest_SOURCES = drmtimerUnitTest.cpp dtttest_gtest_SOURCES = dtttimerUnitTest.cpp @@ -39,7 +44,7 @@ iarmtimerstatus_gtest_SOURCES = iarmtimerstatus_gtest.cpp iarmsubscribe_gtest_SOURCES = iarmsubscribe_gtest.cpp iarmpublish_gtest_SOURCES = iarmpublish_gtest.cpp iarmpowersubscribe_gtest_SOURCES = iarmpowersubscribe_gtest.cpp -systimemgr_gtest_SOURCES =SysTimeMgrUnitTest.cpp +systimemgr_gtest_SOURCES = SysTimeMgrUnitTest.cpp ../networkstatussrc.cpp # Apply common properties to each program @@ -85,5 +90,5 @@ iarmpowersubscribe_gtest_LDADD = $(COMMON_LDADD) iarmpowersubscribe_gtest_CXXFLAGS = $(COMMON_CXXFLAGS) systimemgr_gtest_CPPFLAGS = $(COMMON_CPPFLAGS) -systimemgr_gtest_LDADD = $(COMMON_LDADD) -systimemgr_gtest_CXXFLAGS = $(COMMON_CXXFLAGS) +systimemgr_gtest_LDADD = $(COMMON_LDADD) -lpthread +systimemgr_gtest_CXXFLAGS = $(COMMON_CXXFLAGS) -DMILESTONE_SUPPORT_DISABLED diff --git a/systimerfactory/unittest/SysTimeMgrUnitTest.cpp b/systimerfactory/unittest/SysTimeMgrUnitTest.cpp index c6db3960..dd97e900 100644 --- a/systimerfactory/unittest/SysTimeMgrUnitTest.cpp +++ b/systimerfactory/unittest/SysTimeMgrUnitTest.cpp @@ -3,6 +3,7 @@ #include #include #include "Client_Mock.h" +#include //#include "secure_wrapper.h" #include "systimemgr.h" #include "systimemgr.cpp" @@ -10,30 +11,386 @@ #include "pubsubfactory.cpp" #include "rdkdefaulttimesync.cpp" #include "drmtimersrc.cpp" +#include "itimesrc.h" +#include "itimesync.h" +#include "ipublish.h" +#include "isubscribe.h" +#include "itimermsg.h" +#include "libchronyctl.h" +class MockTimeSrc : public ITimeSrc { +public: + MOCK_METHOD(bool, isreference, (), (override)); + MOCK_METHOD(long long, getTimeSec, (), (override)); + MOCK_METHOD(bool, isclockProvider, (), (override)); + MOCK_METHOD(bool, checkTime, (), (override)); +}; +class MockTimeSync : public ITimeSync { +public: + MOCK_METHOD(long long, getTime, (), (override)); + MOCK_METHOD(void, updateTime, (long long), (override)); +}; +class MockPublish : public IPublish { +public: + MockPublish() : IPublish("MockPublisherForTest") {} + MOCK_METHOD(void, publish, (int event, void* args), (override)); +}; +class MockSubscribe : public ISubscribe { +public: + MockSubscribe() : ISubscribe("MockSubscriberForTest") {} + MOCK_METHOD(bool, subscribe, (string eventname, funcPtr fptr), (override)); +}; - - +using ::testing::_; +using ::testing::Return; +using ::testing::AtLeast; class SysTimeMgrTest : public ::testing::Test { protected: SysTimeMgr* mgr; + + MockTimeSrc* mockTimeSrc; + MockTimeSync* mockTimeSync; + MockPublish* mockPublish; + MockSubscribe* mockSubscribe; + std::string temp_test_dir; + void SetUp() override { mgr = SysTimeMgr::get_instance(); - ASSERT_NE(mgr, nullptr) << "SysTimeMgr::get_instance() returned nullptr"; + ASSERT_NE(mgr, nullptr); + mockTimeSrc = new MockTimeSrc(); + mockTimeSync = new MockTimeSync(); + mockPublish = new MockPublish(); + mockSubscribe = new MockSubscribe(); + + mgr->m_timerSrc.clear(); + mgr->m_timerSync.clear(); + mgr->m_publish = mockPublish; + mgr->m_subscriber = mockSubscribe; + mgr->m_tmrsubscriber = mockSubscribe; + mgr->m_pathEventMap.clear(); + mgr->m_pathEventMap.insert({"ntp", eSYSMGR_EVENT_NTP_AVAILABLE}); + mgr->m_pathEventMap.insert({"stt", eSYSMGR_EVENT_NTP_AVAILABLE}); + mgr->m_pathEventMap.insert({"drm", eSYSMGR_EVENT_SECURE_TIME_AVAILABLE}); + mgr->m_pathEventMap.insert({"dtt", eSYSMGR_EVENT_DTT_TIME_AVAILABLE}); } + void TearDown() override { - // Not deleting singleton to avoid double-free/static issues. + // Delete mock objects to prevent leaks + delete mockTimeSrc; + delete mockTimeSync; + delete mockPublish; + delete mockSubscribe; + + mgr->m_timerSrc.clear(); + mgr->m_timerSync.clear(); + if (!temp_test_dir.empty()) { + for (const auto& entry : mgr->m_pathEventMap) { + std::string filepath = temp_test_dir + "/" + entry.first; + std::remove(filepath.c_str()); + } + rmdir(temp_test_dir.c_str()); + } + SysTimeMgr::pInstance = nullptr; } }; -// Test 1: Singleton can be constructed -TEST_F(SysTimeMgrTest, SingletonConstructs) { - EXPECT_NE(mgr, nullptr); +TEST_F(SysTimeMgrTest, SingletonReturnsSamePointer) { + EXPECT_EQ(mgr, SysTimeMgr::get_instance()); +} + +TEST_F(SysTimeMgrTest, DestructorCovers) { + SysTimeMgr* localMgr = new SysTimeMgr("dummy.cfg"); + delete localMgr; +} + + +TEST_F(SysTimeMgrTest, SetInitialTime_ZeroTime) { + EXPECT_CALL(*mockTimeSync, getTime()).WillOnce(Return(0)); + mgr->m_timerSync.push_back(mockTimeSync); + mgr->setInitialTime(); +} + +TEST_F(SysTimeMgrTest, SetInitialTime_NonZeroTime) { + EXPECT_CALL(*mockTimeSync, getTime()).WillOnce(Return(100000)); + mgr->m_timerSync.push_back(mockTimeSync); + mgr->setInitialTime(); +} + +TEST_F(SysTimeMgrTest, UpdateTime_InvokesCheckTime) { + EXPECT_CALL(*mockTimeSrc, checkTime()).Times(1); + mgr->m_timerSrc.push_back(mockTimeSrc); + mgr->updateTime(nullptr); +} + +TEST_F(SysTimeMgrTest, SetInitialTime_FileCreationFails) { + mkdir("/tmp/systimeset", 0700); + + EXPECT_CALL(*mockTimeSync, getTime()).WillOnce(Return(12345)); + mgr->m_timerSync.push_back(mockTimeSync); + + mgr->setInitialTime(); + + // Clean up + rmdir("/tmp/systimeset"); +} + +TEST_F(SysTimeMgrTest, RunStateMachineUnknownEvent) { + mgr->runStateMachine(eSYSMGR_EVENT_UNKNOWN, nullptr); + } + +TEST_F(SysTimeMgrTest, TimerExpiryUsesReference) { + EXPECT_CALL(*mockTimeSrc, isreference()).WillOnce(Return(true)); + EXPECT_CALL(*mockTimeSrc, getTimeSec()).WillOnce(Return(1234)); + mgr->m_timerSrc.push_back(mockTimeSrc); // Add it to the manager's list + mgr->timerExpiry(nullptr); + } + +TEST_F(SysTimeMgrTest, TimerExpiryUsesFileTime) { + EXPECT_CALL(*mockTimeSrc, isreference()).WillOnce(Return(false)); + EXPECT_CALL(*mockTimeSrc, getTimeSec()).WillOnce(Return(5678)); + mgr->m_timerSrc.push_back(mockTimeSrc); // Add it to the manager's list + mgr->timerExpiry(nullptr); +} + +TEST_F(SysTimeMgrTest, UpdateTimeSyncCallsSyncs) { + EXPECT_CALL(*mockTimeSync, updateTime(_)).Times(1); + mgr->m_timerSync.push_back(mockTimeSync); // Add it to the manager's list + mgr->updateTimeSync(1234); +} + +TEST_F(SysTimeMgrTest, NtpFailedPublishesStatus) { + EXPECT_CALL(*mockPublish, publish(_, _)).Times(AtLeast(1)); + mgr->ntpFailed(nullptr); +} + +TEST_F(SysTimeMgrTest, NtpAquiredPublishesStatusAndUpdatesState) { + EXPECT_CALL(*mockPublish, publish(_, _)).Times(AtLeast(1)); + mgr->ntpAquired(nullptr); + EXPECT_EQ(mgr->m_state, eSYSMGR_STATE_NTP_ACQUIRED); +} + +TEST_F(SysTimeMgrTest, NtpFailed) { + EXPECT_CALL(*mockPublish, publish(_, _)).Times(AtLeast(1)); + mgr->ntpFailed(nullptr); + EXPECT_EQ(mgr->m_state, eSYSMGR_STATE_NTP_FAIL); +} + + +TEST_F(SysTimeMgrTest, DttAcquiredPublishesStatusAndUpdatesState) { + EXPECT_CALL(*mockPublish, publish(_, _)).Times(AtLeast(1)); + mgr->dttAquired(nullptr); + EXPECT_EQ(mgr->m_state, eSYSMGR_STATE_DTT_ACQUIRED); +} + +TEST_F(SysTimeMgrTest, SecureTimeAcquiredUpdatesState) { + mgr->secureTimeAcquired(nullptr); + EXPECT_EQ(mgr->m_state, eSYSMGR_STATE_SECURE_TIME_ACQUIRED); +} + +TEST_F(SysTimeMgrTest, UpdateSecureTimePublishesStatusAndUpdatesState) { + EXPECT_CALL(*mockPublish, publish(_, _)).Times(AtLeast(1)); + mgr->updateSecureTime(nullptr); + EXPECT_EQ(mgr->m_state, eSYSMGR_STATE_RUNNING); +} + +TEST_F(SysTimeMgrTest, SendMessageQueuesEvent) { + mgr->sendMessage(eSYSMGR_EVENT_TIMER_EXPIRY, nullptr); } +TEST_F(SysTimeMgrTest, GetTimeStatusPopulatesFields) { + TimerMsg msg; + mgr->getTimeStatus(&msg); + EXPECT_GE(strlen(msg.message), 0); +} + +TEST_F(SysTimeMgrTest, PowerHandlerHandlesSleepEvents) { + std::string on = "DEEP_SLEEP_ON"; + std::string off = "DEEP_SLEEP_OFF"; + EXPECT_EQ(SysTimeMgr::powerhandler(&on), 1); + EXPECT_EQ(SysTimeMgr::powerhandler(&off), 1); +} + +TEST_F(SysTimeMgrTest, DeepSleepOffPublishesStatus) { + EXPECT_CALL(*mockPublish, publish(_, _)).Times(AtLeast(1)); + mgr->m_timequality = eTIMEQUALILTY_SECURE; + mgr->deepsleepoff(); +} + +TEST_F(SysTimeMgrTest, DeepSleepOffCoversPoorCase) { + mgr->m_timequality = eTIMEQUALILTY_POOR; + mgr->deepsleepoff(); +} + +TEST_F(SysTimeMgrTest, DeepSleepOnLogs) { + mgr->deepsleepon(); +} + + +TEST_F(SysTimeMgrTest, UpdateClockRealTimeSetsTime) { + EXPECT_CALL(*mockTimeSrc, isclockProvider()).WillOnce(Return(true)); + EXPECT_CALL(*mockTimeSrc, getTimeSec()).WillOnce(Return(1234)); + EXPECT_CALL(*mockTimeSrc, checkTime()).Times(1); + mgr->m_timerSrc.push_back(mockTimeSrc); + mgr->updateClockRealTime(nullptr); +} + +TEST_F(SysTimeMgrTest, RunDetachesThreadsWhenNotForever) { + + mgr->run(false); +} + + +TEST_F(SysTimeMgrTest, ProcessThrCallsProcessMsgAndRunsOneIteration) { + mgr->sendMessage(eSYSMGR_EVENT_UNKNOWN, nullptr); + std::thread process_thread([this]() { + SysTimeMgr::processThr(mgr); + }); + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + + process_thread.detach(); + + } + +TEST_F(SysTimeMgrTest, TimerThrCallsRunTimerAndRunsOnce) { + EXPECT_CALL(*mockPublish, publish(eSYSMGR_EVENT_TIMER_EXPIRY, _)).Times(AtLeast(0)); + std::thread timer_thread([this]() { + SysTimeMgr::timerThr(mgr); + }); + + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + timer_thread.detach(); + + } +TEST_F(SysTimeMgrTest, UpdateClockRealTimeAllBranches) { + EXPECT_CALL(*mockTimeSrc, isclockProvider()).WillOnce(Return(true)); + EXPECT_CALL(*mockTimeSrc, getTimeSec()).WillOnce(Return(1234)); + EXPECT_CALL(*mockTimeSrc, checkTime()).Times(1); + mgr->m_timerSrc.push_back(mockTimeSrc); + mgr->updateClockRealTime(nullptr); + mgr->m_timerSrc.clear(); + EXPECT_CALL(*mockTimeSrc, isclockProvider()).WillOnce(Return(true)); + EXPECT_CALL(*mockTimeSrc, getTimeSec()).WillOnce(Return(0)); + EXPECT_CALL(*mockTimeSrc, checkTime()).Times(1); + mgr->m_timerSrc.push_back(mockTimeSrc); + mgr->updateClockRealTime(nullptr); +} + +TEST_F(SysTimeMgrTest, GetTimeStatus_AllQualities) { + TimerMsg msg; + mgr->m_timequality = eTIMEQUALILTY_POOR; + mgr->getTimeStatus(&msg); + mgr->m_timequality = eTIMEQUALILTY_GOOD; + mgr->getTimeStatus(&msg); + mgr->m_timequality = eTIMEQUALILTY_SECURE; + mgr->getTimeStatus(&msg); + +} + +TEST_F(SysTimeMgrTest, PublishStatusCoversAll) { + EXPECT_CALL(*mockPublish, publish(_, _)).Times(AtLeast(1)); + mgr->publishStatus(ePUBLISH_NTP_FAIL, "Poor"); + mgr->publishStatus(ePUBLISH_NTP_SUCCESS, "Good"); + mgr->publishStatus(ePUBLISH_SECURE_TIME_SUCCESS, "Secure"); + mgr->publishStatus(ePUBLISH_DTT_SUCCESS, "Good"); + mgr->publishStatus(ePUBLISH_TIME_INITIAL, "Poor"); + mgr->publishStatus(ePUBLISH_DEEP_SLEEP_ON, "Unknown"); +} + +TEST_F(SysTimeMgrTest, TimerExpiry_RefVsFileTime) { + EXPECT_CALL(*mockTimeSrc, isreference()).WillOnce(Return(true)); + EXPECT_CALL(*mockTimeSrc, getTimeSec()).WillOnce(Return(123)); + mgr->m_timerSrc.push_back(mockTimeSrc); + mgr->timerExpiry(nullptr); + mgr->m_timerSrc.clear(); + EXPECT_CALL(*mockTimeSrc, isreference()).WillOnce(Return(false)); + EXPECT_CALL(*mockTimeSrc, getTimeSec()).WillOnce(Return(456)); + mgr->m_timerSrc.push_back(mockTimeSrc); + mgr->timerExpiry(nullptr); +} + +TEST_F(SysTimeMgrTest, TimerThrAndProcessThrCoverage) { + std::thread t1([&]() { SysTimeMgr::timerThr(mgr); }); + std::thread t2([&]() { SysTimeMgr::processThr(mgr); }); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + t1.detach(); + t2.detach(); +} + +TEST_F(SysTimeMgrTest, SendMessageAndProcessMsg) { + mgr->sendMessage(eSYSMGR_EVENT_TIMER_EXPIRY, nullptr); + std::thread t([&]() { mgr->processMsg(); }); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + t.detach(); +} + +TEST_F(SysTimeMgrTest, RunStateMachine_AllStatesEvents) { + std::vector states = { + eSYSMGR_STATE_RUNNING, eSYSMGR_STATE_NTP_WAIT, eSYSMGR_STATE_NTP_ACQUIRED, + eSYSMGR_STATE_NTP_FAIL, eSYSMGR_STATE_DTT_ACQUIRED, eSYSMGR_STATE_SECURE_TIME_ACQUIRED + }; + std::vector events = { + eSYSMGR_EVENT_TIMER_EXPIRY, eSYSMGR_EVENT_NTP_AVAILABLE, + eSYSMGR_EVENT_SECURE_TIME_AVAILABLE, eSYSMGR_EVENT_DTT_TIME_AVAILABLE + }; + for (auto state : states) { + mgr->m_state = state; + for (auto event : events) { + mgr->runStateMachine(event, nullptr); + } + } +} + +TEST_F(SysTimeMgrTest, GetTimeStatusStaticFunctionWorks) { + TimerMsg msg; + int ret = SysTimeMgr::getTimeStatus(static_cast(&msg)); + EXPECT_EQ(ret, 0); + EXPECT_GE(strlen(msg.message), 0); +} + +TEST_F(SysTimeMgrTest, RunPathMonitorCoversInotifyEvent) { + std::string test_dir = "/tmp/systimemgr"; + mkdir(test_dir.c_str(), 0777); + // mgr->m_directory = test_dir; + std::string testfile = test_dir + "/ntp"; + std::ofstream outfile(testfile); outfile << "test"; outfile.close(); + std::thread t([&]() { mgr->runPathMonitor(); }); + chmod(testfile.c_str(), 0666); + std::this_thread::sleep_for(std::chrono::milliseconds(300)); + t.detach(); + remove(testfile.c_str()); + rmdir(test_dir.c_str()); +} +TEST_F(SysTimeMgrTest, RunPathMonitorFileExistsAtStartup) { + std::string temp_test_dir = "/tmp/systimemgr_test_tmp2"; + mkdir(temp_test_dir.c_str(), 0777); + // mgr->m_directory = temp_test_dir; + std::string fname = "ntp"; + mgr->m_pathEventMap[fname] = eSYSMGR_EVENT_NTP_AVAILABLE; + std::ofstream((temp_test_dir + "/" + fname)).close(); + std::thread t([&] { mgr->runPathMonitor(); }); + std::this_thread::sleep_for(std::chrono::milliseconds(300)); + t.detach(); + remove((temp_test_dir + "/" + fname).c_str()); + rmdir(temp_test_dir.c_str()); +} + +TEST_F(SysTimeMgrTest, RunPathMonitorInotifyAddWatchFails) { + std::string random_dir = "/tmp/definitely_does_not_exist_" + std::to_string(rand()); + ASSERT_EQ(access(random_dir.c_str(), F_OK), -1); + // mgr->m_directory = random_dir; + mgr->m_pathEventMap.clear(); + mgr->runPathMonitor(); +} + + +TEST_F(SysTimeMgrTest, RunStateMachine_HitsFunctionPointer) { + mgr->stateMachine[eSYSMGR_STATE_RUNNING][eSYSMGR_EVENT_TIMER_EXPIRY] = &SysTimeMgr::updateTime; + mgr->m_state = eSYSMGR_STATE_RUNNING; + mgr->runStateMachine(eSYSMGR_EVENT_TIMER_EXPIRY, nullptr); +} diff --git a/systimerfactory/unittest/iarmpowersubscribe_gtest.cpp b/systimerfactory/unittest/iarmpowersubscribe_gtest.cpp index 6f22d875..002d7fc9 100644 --- a/systimerfactory/unittest/iarmpowersubscribe_gtest.cpp +++ b/systimerfactory/unittest/iarmpowersubscribe_gtest.cpp @@ -116,6 +116,32 @@ TEST_F(IarmPowerSubscriberTest, PowerEventHandler_DeepSleepOff_TriggersHandler) EXPECT_EQ(gReceivedStatus, "DEEP_SLEEP_OFF"); } +TEST_F(IarmPowerSubscriberTest, PowerEventHandler_DeepSleepOff_LightSleep_TriggersHandler) { + IarmPowerSubscriber subscriber("TestSub"); + subscriber.subscribe(POWER_CHANGE_MSG, TestPowerHandler); + + IARM_Bus_PWRMgr_EventData_t evt {}; + evt.data.state.curState = IARM_BUS_PWRMGR_POWERSTATE_STANDBY_DEEP_SLEEP; + evt.data.state.newState = IARM_BUS_PWRMGR_POWERSTATE_STANDBY_LIGHT_SLEEP; + + gReceivedStatus.clear(); + IarmPowerSubscriber::powereventHandler(IARM_BUS_PWRMGR_NAME, IARM_BUS_PWRMGR_EVENT_MODECHANGED, &evt, sizeof(evt)); + EXPECT_EQ(gReceivedStatus, "DEEP_SLEEP_OFF"); +} + +TEST_F(IarmPowerSubscriberTest, PowerEventHandler_DeepSleepOff_Standby_TriggersHandler) { + IarmPowerSubscriber subscriber("TestSub"); + subscriber.subscribe(POWER_CHANGE_MSG, TestPowerHandler); + + IARM_Bus_PWRMgr_EventData_t evt {}; + evt.data.state.curState = IARM_BUS_PWRMGR_POWERSTATE_STANDBY_DEEP_SLEEP; + evt.data.state.newState = IARM_BUS_PWRMGR_POWERSTATE_STANDBY; + + gReceivedStatus.clear(); + IarmPowerSubscriber::powereventHandler(IARM_BUS_PWRMGR_NAME, IARM_BUS_PWRMGR_EVENT_MODECHANGED, &evt, sizeof(evt)); + EXPECT_EQ(gReceivedStatus, "DEEP_SLEEP_OFF"); +} + TEST_F(IarmPowerSubscriberTest, PowerEventHandler_UnrelatedEventId_DoesNothing) { IarmPowerSubscriber subscriber("TestSub"); subscriber.subscribe(POWER_CHANGE_MSG, TestPowerHandler); @@ -124,3 +150,18 @@ TEST_F(IarmPowerSubscriberTest, PowerEventHandler_UnrelatedEventId_DoesNothing) IarmPowerSubscriber::powereventHandler(IARM_BUS_PWRMGR_NAME, 999, nullptr, 0); EXPECT_TRUE(gReceivedStatus.empty()); } + +TEST_F(IarmPowerSubscriberTest, PowerEventHandler_NullSingleton_InstancePathCovered) +{ + IarmSubscriber::pInstance = nullptr; + IARM_Bus_PWRMgr_EventData_t evt {}; + evt.data.state.newState = IARM_BUS_PWRMGR_POWERSTATE_STANDBY_DEEP_SLEEP; + evt.data.state.curState = IARM_BUS_PWRMGR_POWERSTATE_ON; + IarmPowerSubscriber::powereventHandler( + IARM_BUS_PWRMGR_NAME, + IARM_BUS_PWRMGR_EVENT_MODECHANGED, + &evt, + sizeof(evt) + ); + +} diff --git a/systimerfactory/unittest/iarmtimerstatus_gtest.cpp b/systimerfactory/unittest/iarmtimerstatus_gtest.cpp index 946b3325..a0a8e657 100644 --- a/systimerfactory/unittest/iarmtimerstatus_gtest.cpp +++ b/systimerfactory/unittest/iarmtimerstatus_gtest.cpp @@ -87,9 +87,27 @@ TEST_F(IarmTimerStatusSubscriberTest, Constructor_IarmAlreadyConnected_DoesNotRe TEST_F(IarmTimerStatusSubscriberTest, Subscribe_InvalidEventName_ReturnsFalse) { IarmTimerStatusSubscriber subscriber("test_subscriber"); - EXPECT_CALL(*gMockIARM, RegisterCall(_, _)).Times(0); // Should not be called + EXPECT_CALL(*gMockIARM, RegisterCall(_, _)).Times(0); bool result = subscriber.subscribe("INVALID_EVENT", reinterpret_cast(0x1234)); EXPECT_FALSE(result); } +TEST_F(IarmTimerStatusSubscriberTest, Subscribe_ValidEventName_CallsRegisterCallAndReturnsFalseOnSuccess) { + IarmTimerStatusSubscriber subscriber("test_subscriber"); + + EXPECT_CALL(*gMockIARM, RegisterCall(::testing::StrEq(TIMER_STATUS_MSG), _)) + .WillOnce(Return(IARM_RESULT_SUCCESS)); // 0 + + bool result = subscriber.subscribe(TIMER_STATUS_MSG, reinterpret_cast(0x1234)); + EXPECT_FALSE(result); // success returns false because of production code bug +} + +TEST_F(IarmTimerStatusSubscriberTest, Subscribe_ValidEventName_ReturnsTrueOnFailure) { + IarmTimerStatusSubscriber subscriber("test_subscriber"); + + EXPECT_CALL(*gMockIARM, RegisterCall(::testing::StrEq(TIMER_STATUS_MSG), _)) + .WillOnce(Return(IARM_RESULT_INVALID_STATE)); + bool result = subscriber.subscribe(TIMER_STATUS_MSG, reinterpret_cast(0x1234)); + EXPECT_TRUE(result); +} diff --git a/systimerfactory/unittest/ipowercontrollersubscriber_gtest.cpp b/systimerfactory/unittest/ipowercontrollersubscriber_gtest.cpp index 1dde0c64..11fc6367 100644 --- a/systimerfactory/unittest/ipowercontrollersubscriber_gtest.cpp +++ b/systimerfactory/unittest/ipowercontrollersubscriber_gtest.cpp @@ -9,7 +9,7 @@ #include "ipowercontrollersubscriber.h" #include "ipowercontrollersubscriber.cpp" #include "iarmsubscribe.cpp" - +#include "testsubscribe.h" // Mocking the external PowerController API functions class MockPowerController { @@ -60,19 +60,165 @@ class IpowerControllerSubscriberTest : public ::testing::Test { }; -TEST_F(IpowerControllerSubscriberTest, Destructor_CallsPowerControllerTerm) { + + +TEST_F(IpowerControllerSubscriberTest, Destructor_CallsPowerControllerTerm) { - EXPECT_CALL(mockPowerController, PowerController_Term()).Times(1); + EXPECT_CALL(mockPowerController, PowerController_Term()).Times(1); IpowerControllerSubscriber subscriber("test_subscriber"); } - // Destructor called at block exit, PowerController_Term should be invoked -} + TEST_F(IpowerControllerSubscriberTest, Subscribe_InvalidEventName_ReturnsFalse) { IpowerControllerSubscriber subscriber("test_subscriber"); - bool ret = subscriber.subscribe("INVALID_EVENT", nullptr); + EXPECT_FALSE(ret); +} + +static bool handlerCalled = false; +static int testHandler(void* status) { + handlerCalled = true; + std::string* str = static_cast(status); + EXPECT_EQ(*str, "DEEP_SLEEP_ON"); + return 0; +} + +TEST_F(IpowerControllerSubscriberTest, HandlePwrEventData_DeepSleepOn) { + IpowerControllerSubscriber subscriber("sub"); + + subscriber.m_powerHandler = testHandler; + handlerCalled = false; + subscriber.sysTimeMgrHandlePwrEventData(POWER_STATE_UNKNOWN, POWER_STATE_OFF); + + EXPECT_TRUE(handlerCalled); +} +static bool handlerCalledoff = false; +static int testHandleroff(void* status) { + handlerCalledoff = true; + std::string* str = static_cast(status); + EXPECT_EQ(*str, "DEEP_SLEEP_OFF"); + return 0; +} + +TEST_F(IpowerControllerSubscriberTest, HandlePwrEventData_DeepSleepOff) { + IpowerControllerSubscriber subscriber("sub"); + subscriber.m_powerHandler = testHandleroff; + handlerCalledoff = false; + subscriber.sysTimeMgrHandlePwrEventData(POWER_STATE_STANDBY_DEEP_SLEEP, POWER_STATE_ON); + EXPECT_TRUE(handlerCalledoff); +} + +TEST_F(IpowerControllerSubscriberTest, Subscribe_ValidEvent_ConnectionFails_WithTestSubscriber) { + TestSubscriber subscriber("test_subscriber"); + bool ret = subscriber.subscribe(POWER_CHANGE_MSG, nullptr); + EXPECT_TRUE(ret); +} + +TEST_F(IpowerControllerSubscriberTest, Subscribe_ValidEvent_ConnectionSuccess_WithTestSubscriber) { + TestSubscriber subscriber("test_subscriber"); + bool ret = subscriber.subscribe(POWER_CHANGE_MSG, nullptr); + EXPECT_TRUE(ret); +} +TEST_F(IpowerControllerSubscriberTest, HandlePwrEventData_UnknownNewState_LogsError) { + IpowerControllerSubscriber subscriber("test_subscriber"); + handlerCalled = false; + subscriber.m_powerHandler = testHandler; + subscriber.sysTimeMgrHandlePwrEventData(POWER_STATE_ON, static_cast(999)); + EXPECT_FALSE(handlerCalled); +} + +TEST_F(IpowerControllerSubscriberTest, Destructor_UnregisterCallbackFails_StillCleansUp) { + IpowerControllerSubscriber* subscriber = new IpowerControllerSubscriber("test_subscriber"); + EXPECT_CALL(mockPowerController, PowerController_Term()); + EXPECT_CALL(mockPowerController, PowerController_UnRegisterPowerModeChangedCallback(::testing::_)).WillOnce(::testing::Return(1)); + delete subscriber; +} + +TEST_F(IpowerControllerSubscriberTest, Subscribe_EmptyEventName_ReturnsFalse) { + IpowerControllerSubscriber subscriber("test_subscriber"); + bool ret = subscriber.subscribe("", nullptr); EXPECT_FALSE(ret); } +TEST_F(IpowerControllerSubscriberTest, Destructor_WithoutSubscribe_DoesNotCrashOrLeak) { + EXPECT_CALL(mockPowerController, PowerController_Term()).Times(1); + IpowerControllerSubscriber* subscriber = new IpowerControllerSubscriber("test_subscriber"); + delete subscriber; +} + +TEST_F(IpowerControllerSubscriberTest, UnRegisterPowerModeChangedCallback_ReturnsError_CoversErrorPath) { + // Set up mock to return error (non-zero) + EXPECT_CALL(mockPowerController, PowerController_UnRegisterPowerModeChangedCallback(::testing::_)).WillOnce(::testing::Return(1)); + + // Call the function directly and check the error path + uint32_t result = PowerController_UnRegisterPowerModeChangedCallback(IpowerControllerSubscriber::sysTimeMgrPwrEventHandler); + + EXPECT_EQ(result, 1); // Ensure error is returned +} + +TEST_F(IpowerControllerSubscriberTest, HandlePwrEventData_NoHandler_DoesNotCrash) { + IpowerControllerSubscriber subscriber("test_subscriber"); + subscriber.m_powerHandler = nullptr; + subscriber.sysTimeMgrHandlePwrEventData(POWER_STATE_UNKNOWN, POWER_STATE_OFF); + SUCCEED(); +} + +class TestableSubscriber : public IpowerControllerSubscriber { +public: + using IpowerControllerSubscriber::IpowerControllerSubscriber; + using IpowerControllerSubscriber::m_pwrEvtQueue; + using IpowerControllerSubscriber::m_pwrEvtQueueLock; + using IpowerControllerSubscriber::m_pwrEvtCondVar; + + size_t queueSize() { + std::lock_guard lock(m_pwrEvtQueueLock); + return m_pwrEvtQueue.size(); + } + SysTimeMgr_Power_Event_State_t queueFront() { + std::lock_guard lock(m_pwrEvtQueueLock); + return m_pwrEvtQueue.front(); + } + void clearQueue() { + std::lock_guard lock(m_pwrEvtQueueLock); + m_pwrEvtQueue = std::queue(); + } +}; + +TEST_F(IpowerControllerSubscriberTest, SysTimeMgrPwrEventHandler_EnqueuesEventAndSignals) { + TestableSubscriber subscriber("test_subscriber"); + subscriber.clearQueue(); + + std::atomic signaled{false}; + std::thread waiter([&]() { + std::unique_lock lock(subscriber.m_pwrEvtQueueLock); + subscriber.m_pwrEvtCondVar.wait_for( + lock, std::chrono::milliseconds(200), + [&] { return !subscriber.m_pwrEvtQueue.empty(); } + ); + signaled = true; + }); + + PowerController_PowerState_t curr = POWER_STATE_OFF; + PowerController_PowerState_t next = POWER_STATE_ON; + IpowerControllerSubscriber::sysTimeMgrPwrEventHandler(curr, next, nullptr); + + waiter.join(); + + EXPECT_EQ(subscriber.queueSize(), 1u); + SysTimeMgr_Power_Event_State_t front = subscriber.queueFront(); + EXPECT_EQ(front.currentState, curr); + EXPECT_EQ(front.newState, next); + + EXPECT_TRUE(signaled); + +} + + +TEST_F(IpowerControllerSubscriberTest, HandlePwrEventData_InvalidState_LogsError) { + IpowerControllerSubscriber subscriber("test_subscriber"); + subscriber.sysTimeMgrHandlePwrEventData(POWER_STATE_ON, static_cast(999)); + // No handler, just exercise the default path for coverage +} + + diff --git a/systimerfactory/unittest/mocks/Client_Mock.h b/systimerfactory/unittest/mocks/Client_Mock.h index 9da42645..0eaa3ea8 100644 --- a/systimerfactory/unittest/mocks/Client_Mock.h +++ b/systimerfactory/unittest/mocks/Client_Mock.h @@ -28,6 +28,7 @@ #include #include #include +#include class VSecureSystemMock { public: @@ -50,6 +51,17 @@ extern "C" int v_secure_system(const char* format, ...) { } return ret; } + +// v_secure_popen / v_secure_pclose — forward to the standard popen/pclose; +// no security policy enforcement needed in test builds. +extern "C" FILE* v_secure_popen(const char* direction, const char* command, ...) { + return popen(command, direction); +} + +extern "C" int v_secure_pclose(FILE* fp) { + return pclose(fp); +} + class TestMock{ public: MOCK_METHOD((Json::Value), getReturnValue,() ); @@ -102,3 +114,77 @@ class Client } + +/* ----------------------------------------------------------------------- + * ChronyCtl mock stubs — mirrors the v_secure_system pattern above. + * + * globalChronyCtlMock is nullptr by default; stub functions return + * CHRONYCTL_SUCCESS and set safe output values. Tests that need to control + * chronyctl behaviour should instantiate a ChronyCtlMock and point + * globalChronyCtlMock at it. + * ----------------------------------------------------------------------- */ +#include "libchronyctl.h" + +ChronyCtlMock *globalChronyCtlMock = nullptr; + +extern "C" int chronyctl_init(void) { + if (globalChronyCtlMock) return globalChronyCtlMock->chronyctl_init(); + return CHRONYCTL_SUCCESS; +} + +extern "C" int chronyctl_cleanup(void) { + if (globalChronyCtlMock) return globalChronyCtlMock->chronyctl_cleanup(); + return CHRONYCTL_SUCCESS; +} + +extern "C" int chronyctl_get_offset(double *offset_sec) { + if (globalChronyCtlMock) return globalChronyCtlMock->chronyctl_get_offset(offset_sec); + if (offset_sec) *offset_sec = 0.0; + return CHRONYCTL_SUCCESS; +} + +extern "C" int chronyctl_get_system_time_offset(double *offset_sec) { + if (offset_sec) *offset_sec = 0.0; + return CHRONYCTL_SUCCESS; +} + +extern "C" int chronyctl_makestep(void) { + if (globalChronyCtlMock) return globalChronyCtlMock->chronyctl_makestep(); + return CHRONYCTL_SUCCESS; +} + +extern "C" int chronyctl_online(const IPAddr *addr, const IPAddr *mask) { + if (globalChronyCtlMock) return globalChronyCtlMock->chronyctl_online(addr, mask); + return CHRONYCTL_SUCCESS; +} + +extern "C" int chronyctl_burst(const IPAddr *addr, const IPAddr *mask, + int n_good_samples, int n_total_samples) { + if (globalChronyCtlMock) + return globalChronyCtlMock->chronyctl_burst(addr, mask, n_good_samples, n_total_samples); + return CHRONYCTL_SUCCESS; +} + +extern "C" int chronyctl_has_selectable_source(int *has_selectable) { + if (globalChronyCtlMock) + return globalChronyCtlMock->chronyctl_has_selectable_source(has_selectable); + if (has_selectable) *has_selectable = 0; + return CHRONYCTL_SUCCESS; +} + +extern "C" int chronyctl_get_source_count(int *count) { + if (globalChronyCtlMock) return globalChronyCtlMock->chronyctl_get_source_count(count); + if (count) *count = 0; + return CHRONYCTL_SUCCESS; +} + +extern "C" int chronyctl_waitsync(int max_tries, int interval_sec) { + if (globalChronyCtlMock) + return globalChronyCtlMock->chronyctl_waitsync(max_tries, interval_sec); + return CHRONYCTL_SUCCESS; +} + +extern "C" const char *chronyctl_strerror(int err) { + if (globalChronyCtlMock) return globalChronyCtlMock->chronyctl_strerror(err); + return "mock error"; +} diff --git a/systimerfactory/unittest/mocks/libchronyctl.h b/systimerfactory/unittest/mocks/libchronyctl.h new file mode 100644 index 00000000..43d6ec81 --- /dev/null +++ b/systimerfactory/unittest/mocks/libchronyctl.h @@ -0,0 +1,109 @@ +/* + * Copyright 2023 Comcast Cable Communications Management, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/* + * Mock declarations for libchronyctl used in GTEST_ENABLE unit-test builds. + * + * This header shadows the installed libchronyctl.h so that networkstatussrc.cpp + * can be compiled without the real library. It provides: + * - The chronyctl_error enum and a minimal IPAddr type stub + * - extern "C" function declarations (implementations are in Client_Mock.h, + * compiled into the test binary's SysTimeMgrUnitTest.cpp translation unit) + * - A ChronyCtlMock GMock class for fine-grained test control via + * globalChronyCtlMock + * + * The pattern mirrors the v_secure_system mock in Client_Mock.h: + * networkstatussrc.cpp TU → includes this header (declarations only) + * SysTimeMgrUnitTest.cpp TU → includes Client_Mock.h (stub definitions) + * At link time the linker resolves the extern "C" symbols from the test TU. + */ + +#ifndef LIBCHRONYCTL_H +#define LIBCHRONYCTL_H + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef enum { + CHRONYCTL_SUCCESS = 0, + CHRONYCTL_ERROR_INIT = -1, + CHRONYCTL_ERROR_NOT_INIT = -2, + CHRONYCTL_ERROR_EXEC = -3, + CHRONYCTL_ERROR_PARSE = -4, + CHRONYCTL_ERROR_INVALID = -5, + CHRONYCTL_ERROR_MUTEX = -6, + CHRONYCTL_ERROR_NO_DATA = -7, + CHRONYCTL_ERROR_UNAUTH = -8 +} chronyctl_error; + +/* Minimal IPAddr stub — layout matches addressing.h so function signatures + * compile correctly. NULL is passed for addr/mask in all current call sites, + * so no field access is ever performed in tests. */ +typedef struct { + union { uint32_t in4; uint8_t in6[16]; uint32_t id; } addr; + uint16_t family; + uint16_t _pad; +} IPAddr; + +int chronyctl_init(void); +int chronyctl_cleanup(void); +int chronyctl_get_offset(double *offset_sec); +int chronyctl_get_system_time_offset(double *offset_sec); +int chronyctl_makestep(void); +int chronyctl_online(const IPAddr *addr, const IPAddr *mask); +int chronyctl_burst(const IPAddr *addr, const IPAddr *mask, + int n_good_samples, int n_total_samples); +int chronyctl_has_selectable_source(int *has_selectable); +int chronyctl_get_source_count(int *count); +int chronyctl_waitsync(int max_tries, int interval_sec); +const char *chronyctl_strerror(int err); + +#ifdef __cplusplus +} +#endif + +/* ----------------------------------------------------------------------- + * GMock class — instantiate as globalChronyCtlMock in tests that need to + * control chronyctl behaviour. Stubs default to CHRONYCTL_SUCCESS / safe + * values when globalChronyCtlMock is nullptr. + * ----------------------------------------------------------------------- */ +class ChronyCtlMock { +public: + MOCK_METHOD(int, chronyctl_init, ()); + MOCK_METHOD(int, chronyctl_cleanup, ()); + MOCK_METHOD(int, chronyctl_get_offset, (double *offset_sec)); + MOCK_METHOD(int, chronyctl_makestep, ()); + MOCK_METHOD(int, chronyctl_online, (const IPAddr *addr, const IPAddr *mask)); + MOCK_METHOD(int, chronyctl_burst, + (const IPAddr *addr, const IPAddr *mask, + int n_good_samples, int n_total_samples)); + MOCK_METHOD(int, chronyctl_has_selectable_source, (int *has_selectable)); + MOCK_METHOD(int, chronyctl_get_source_count, (int *count)); + MOCK_METHOD(int, chronyctl_waitsync, (int max_tries, int interval_sec)); + MOCK_METHOD(const char *, chronyctl_strerror, (int err)); +}; + +/* Defined once in Client_Mock.h (SysTimeMgrUnitTest.cpp TU). */ +extern ChronyCtlMock *globalChronyCtlMock; + +#endif /* LIBCHRONYCTL_H */ diff --git a/systimerfactory/unittest/mocks/thunder/WPEFrameworkMock.h b/systimerfactory/unittest/mocks/thunder/WPEFrameworkMock.h new file mode 100644 index 00000000..22c1a4b1 --- /dev/null +++ b/systimerfactory/unittest/mocks/thunder/WPEFrameworkMock.h @@ -0,0 +1,357 @@ +/* + * Copyright 2024 Comcast Cable Communications Management, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * + * WPEFramework Thunder API stubs for __LOCAL_TEST__ builds. + * + * Provides the minimal WPEFramework namespace surface used by + * networkstatussrc.cpp so the file can be compiled without a real + * Thunder/WPEFramework installation. Follows the same pattern as the + * entservices-testframework Tests/mocks/thunder/ directory. + * + * ── Event injection for L2 tests ────────────────────────────────────────── + * + * When SmartLinkType::Subscribe() is called the mock: + * 1. Creates /tmp/thunder_mock__.subscribed (marker) + * 2. Spawns a polling thread watching + * /tmp/thunder_mock__.inject + * + * L2 Python tests inject events by writing a one-line JSON object to that + * inject file (atomic rename recommended): + * + * echo '{"status":"FULLY_CONNECTED","interface":"eth0"}' \ + * > /tmp/thunder_mock_org_rdk_NetworkManager_onInternetStatusChange.inject + * + * The polling thread picks it up, removes the file, parses the JSON, and + * calls the registered C++ callback — exactly as real Thunder would after + * receiving an onInternetStatusChange notification from the plugin. + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include /* usleep */ +#include /* stat */ +#include /* remove, printf */ + +/* RDK logging strategy: + * + * L1 unit tests (GTEST_ENABLE): rdkloggers is not installed; irdklog.h is not + * in the include path. Use printf-based stubs so every RDK_LOG call compiles + * and produces output on stdout. + * + * L2 functional tests (__LOCAL_TEST_ without GTEST_ENABLE): the full + * rdkloggers library IS installed in the Docker container + * (/usr/local/lib/librdkloggers.so, /usr/local/include/irdklog.h or similar). + * Include irdklog.h so the real rdk_logger_msg_printf() is used — this writes + * through rdkloggers to /opt/logs/systimemgr.log.0, exactly as every other + * module in sysTimeMgr does. Relying on printf + stdout-redirect fails + * because rdkloggers recreates the log file inode on startup, making the + * shell's >> fd point to an orphaned inode. */ +#if defined(GTEST_ENABLE) +/* ── L1: printf stubs ─────────────────────────────────────────────────── */ +# undef RDK_LOG +# define RDK_LOG(level, module, format, ...) do { printf("[" module "] " format, ##__VA_ARGS__); fflush(stdout); } while (0) +# undef RDK_LOG_INFO +# define RDK_LOG_INFO 4 +# undef RDK_LOG_WARN +# define RDK_LOG_WARN 3 +# undef RDK_LOG_ERROR +# define RDK_LOG_ERROR 2 +# undef LOG_SYSTIME +# define LOG_SYSTIME "LOG.RDK.SYSTIME" +#else +/* ── L2: real rdkloggers ──────────────────────────────────────────────── */ +# include "irdklog.h" +#endif + +namespace WPEFramework { + +// ── Core namespace ────────────────────────────────────────────────────────── + +namespace Core { + +/* Error codes — only ERROR_NONE is needed by networkstatussrc.cpp */ +static constexpr int32_t ERROR_NONE = 0; + +/* Core::SystemInfo::SetEnvironment — no-op stub (sets THUNDER_ACCESS) */ +namespace SystemInfo { + static inline void SetEnvironment(const std::string& /*key*/, + const std::string& /*val*/) {} +} // namespace SystemInfo + +namespace JSON { + +/* Variant — holds a single string-valued JSON field */ +class Variant { +public: + Variant() = default; + explicit Variant(const std::string& v) : value_(v) {} + std::string String() const { return value_; } +private: + std::string value_; +}; + +/** + * VariantContainer — flat key→string JSON object. + * + * Supports the two operations used by handle_internetStatusChange(): + * bool HasLabel(key) + * Variant operator[](key) + * + * FromJSON() parses a single-level JSON object of the form: + * {"status":"FULLY_CONNECTED","interface":"eth0"} + */ +class VariantContainer { +public: + bool HasLabel(const std::string& key) const { + return entries_.count(key) > 0; + } + + Variant operator[](const std::string& key) const { + auto it = entries_.find(key); + return (it != entries_.end()) ? it->second : Variant{}; + } + + /** + * Parse a flat JSON object. Handles: + * - String values (double-quoted, basic backslash escape) + * - Number / bool / null values (stored as raw string) + * Ignores nested objects/arrays. + */ + void FromJSON(const std::string& json) { + entries_.clear(); + std::string s = json; + + auto trimWS = [](std::string& str) { + auto l = str.find_first_not_of(" \t\r\n"); + auto r = str.find_last_not_of(" \t\r\n"); + str = (l == std::string::npos) ? "" : str.substr(l, r - l + 1); + }; + + trimWS(s); + if (!s.empty() && s.front() == '{') s.erase(0, 1); + if (!s.empty() && s.back() == '}') s.pop_back(); + + size_t pos = 0; + while (pos < s.size()) { + /* ── key ── */ + size_t ks = s.find('"', pos); + if (ks == std::string::npos) break; + size_t ke = s.find('"', ks + 1); + if (ke == std::string::npos) break; + std::string key = s.substr(ks + 1, ke - ks - 1); + + /* ── colon ── */ + size_t colon = s.find(':', ke + 1); + if (colon == std::string::npos) break; + + /* ── value ── */ + size_t vs = s.find_first_not_of(" \t", colon + 1); + if (vs == std::string::npos) break; + + std::string val; + if (s[vs] == '"') { + /* String value */ + size_t ve = vs + 1; + while (ve < s.size()) { + if (s[ve] == '\\' && ve + 1 < s.size()) { + ++ve; /* skip escaped character */ + } else if (s[ve] == '"') { + break; + } + ++ve; + } + val = s.substr(vs + 1, ve - vs - 1); + pos = ve + 1; + } else { + /* Number / bool / null */ + size_t ve = s.find_first_of(",}", vs); + val = s.substr(vs, (ve == std::string::npos ? s.size() : ve) - vs); + trimWS(val); + pos = (ve == std::string::npos) ? s.size() : ve; + } + + entries_[key] = Variant{val}; + } + } + +private: + std::map entries_; +}; + +/* IElement is the base template parameter used in SmartLinkType */ +using IElement = VariantContainer; +using JsonObject = VariantContainer; /* convenience alias matching Thunder */ + +} // namespace JSON + +} // namespace Core + +/* JsonObject in the WPEFramework namespace — reachable after + * "using namespace WPEFramework;" in networkstatussrc.cpp */ +using JsonObject = Core::JSON::VariantContainer; + +// ── JSONRPC namespace ─────────────────────────────────────────────────────── + +namespace JSONRPC { + +/** + * SmartLinkType — mock Thunder JSONRPC client. + * + * API surface matched to networkstatussrc.cpp usage: + * + * SmartLinkType client(callsign, ""); + * client.Subscribe(timeout, "onInternetStatusChange", &handler); + * client.Unsubscribe(timeout, "onInternetStatusChange"); + * + * Each Subscribe() call: + * - Creates /tmp/thunder_mock__.subscribed + * - Spins a thread polling /tmp/thunder_mock__.inject + * every 100 ms; when the file appears it is read, removed, and the + * registered callback is invoked. + */ +template +class SmartLinkType { +public: + SmartLinkType(const std::string& callsign, const std::string& /*client*/) + : callsign_(sanitise(callsign)) {} + + ~SmartLinkType() { + /* Signal all polling threads to stop and wait for them */ + for (auto& kv : entries_) { + kv.second->stop.store(true, std::memory_order_relaxed); + } + for (auto& kv : entries_) { + if (kv.second->thread.joinable()) + kv.second->thread.join(); + removeFile(subscribedPath(kv.first)); + } + } + + /* Non-copyable / non-movable (holds threads) */ + SmartLinkType(const SmartLinkType&) = delete; + SmartLinkType& operator=(const SmartLinkType&) = delete; + + /** + * Subscribe(timeout, eventName, callback) + * + * INBOUND must have: + * void FromJSON(const std::string&) — populate from raw JSON line + */ + template + int32_t Subscribe(uint32_t /*waitTime*/, + const std::string& eventName, + std::function handler) + { + /* Create subscription marker so Python tests can detect readiness */ + { std::ofstream marker(subscribedPath(eventName)); marker << "1\n"; } + + auto entry = std::unique_ptr(new EventEntry()); + EventEntry* raw = entry.get(); + entries_[eventName] = std::move(entry); + + const std::string injectPath = injectFilePath(eventName); + + raw->thread = std::thread([raw, + injectPath = std::move(injectPath), + handler = std::move(handler)]() { + while (!raw->stop.load(std::memory_order_relaxed)) { + struct stat st{}; + if (::stat(injectPath.c_str(), &st) == 0 && st.st_size > 0) { + std::ifstream f(injectPath); + std::string line; + if (std::getline(f, line) && !line.empty()) { + f.close(); + removeFile(injectPath); /* consume the event */ + INBOUND params; + params.FromJSON(line); + handler(params); + } + } + ::usleep(100000); /* 100 ms poll */ + } + }); + + return Core::ERROR_NONE; + } + + int32_t Unsubscribe(uint32_t /*waitTime*/, const std::string& eventName) { + auto it = entries_.find(eventName); + if (it != entries_.end()) { + it->second->stop.store(true, std::memory_order_relaxed); + if (it->second->thread.joinable()) + it->second->thread.join(); + removeFile(subscribedPath(eventName)); + entries_.erase(it); + } + return Core::ERROR_NONE; + } + +private: + /* Replace characters that are illegal in filenames */ + static std::string sanitise(const std::string& s) { + std::string out = s; + for (char& c : out) { + if (c == '.' || c == '/' || c == ':' || c == ' ') + c = '_'; + } + return out; + } + + std::string subscribedPath(const std::string& event) const { + return "/tmp/thunder_mock_" + callsign_ + "_" + event + ".subscribed"; + } + + std::string injectFilePath(const std::string& event) const { + return "/tmp/thunder_mock_" + callsign_ + "_" + event + ".inject"; + } + + static void removeFile(const std::string& path) { + const int removeResult = ::remove(path.c_str()); + const int removeErrno = (removeResult != 0) ? errno : 0; + if (removeResult != 0 && removeErrno != ENOENT) { + RDK_LOG(RDK_LOG_WARN, LOG_SYSTIME, + "[%s]: Failed to remove %s (errno=%d)\n", + __FUNCTION__, path.c_str(), removeErrno); + } + } + + struct EventEntry { + std::atomic stop{false}; + std::thread thread; + EventEntry() = default; + }; + + std::string callsign_; + std::map> entries_; +}; + +} // namespace JSONRPC + +} // namespace WPEFramework + +/* Global alias — accessible even without "using namespace WPEFramework" */ +using JsonObject = WPEFramework::Core::JSON::VariantContainer; diff --git a/systimerfactory/unittest/rdkDefaulttimesyncUnitTest.cpp b/systimerfactory/unittest/rdkDefaulttimesyncUnitTest.cpp index b8254a67..7bfdac22 100644 --- a/systimerfactory/unittest/rdkDefaulttimesyncUnitTest.cpp +++ b/systimerfactory/unittest/rdkDefaulttimesyncUnitTest.cpp @@ -94,3 +94,21 @@ TEST(RdkDefaultTimeSyncTest, updateTimeWithExistingTime) { time_t expectedTime = updatedTime + 10 * 60; EXPECT_GT(expectedTime, C_TIME); } +TEST(RdkDefaultTimeSyncTest, updateTimeWithOlderTimeIncrementsBy10Minutes) { + RdkDefaultTimeSync rdkDefaultTimeSync("/tmp/clock.txt"); + rdkDefaultTimeSync.updateTime(1640995200); // Jan 1, 2022 00:00:00 + long long afterFirst = rdkDefaultTimeSync.getTime(); + rdkDefaultTimeSync.updateTime(1640990000); // Dec 31, 2021 22:33:20 + long long afterSecond = rdkDefaultTimeSync.getTime(); + EXPECT_EQ(afterSecond, afterFirst); +} + +TEST(RdkDefaultTimeSyncTest, tokenizeBreakCoverage) { + RdkDefaultTimeSync rdkDefaultTimeSync; + std::string s = "KEY1=VALUE1\nKEY2="; + auto result = rdkDefaultTimeSync.tokenize(s, "="); + + RdkDefaultTimeSync sync; + sync.tokenize("KEY=VAL", "="); + ASSERT_EQ(result.at("KEY1"), "VALUE1"); +} diff --git a/test/functional-tests/features/systimemgr_bootupflow.feature b/test/functional-tests/features/systimemgr_bootupflow.feature new file mode 100644 index 00000000..dbf818fa --- /dev/null +++ b/test/functional-tests/features/systimemgr_bootupflow.feature @@ -0,0 +1,57 @@ +########################################################################## +# If not stated otherwise in this file or this component's LICENSE +# file the following copyright and licenses apply: +# +# Copyright 2018 RDK Management +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +########################################################################## + +Feature: Ensures SystemTimeManager Bootup Flow + + Scenario: Ensures SystemTimeManager Initialization + Given the SystemTimeManager is not already running + When the SystemTimeManager binary is invoked + Then the SystemTimeManager should be started + + Scenario: Check if systemTimeManager is Running + Given the SystemTimeManager is running + When the SystemTimeManager binary is invoked + Then the SystemTimeManager should be running + + Scenario: Verify SystemTimeManager Log file exists + Given the SystemTimeManager is running + When the SystemTimeManager binary is invoked + Then the SystemTimeManager LogFile should get generated + + Scenario: Verify SystemTimeManager Instance is created + Given the SystemTimeManager is running + When the SystemTimeManager binary is invoked + Then the SystemTimeManager Instance should get created + + Scenario: Verify SystemTimeManager Initializes timeSrc and timeSync + Given the SystemTimeManager is running + When the SystemTimeManager binary is invoked + Then the SystemTimeManager should initializes TimeSrc[NTP,DTT] and TimeSync[clocktime] + + Scenario: Verify SystemTimeManager returns the last known good time + Given the SystemTimeManager is running + When the SystemTimeManager registers for IARM events + Then the SystemTimeManager should return the last known good time + And the log file should contain "Returning Last Known Good Time" + + Scenario: Verify SystemTimeManager Sends info about time Quality + Given the SystemTimeManager is running + When the SystemTimeManager returns the last known good time + Then the SystemTimeManager should Send broadcast msg about the time quality + And the log file should contain "Info:" diff --git a/test/functional-tests/features/systimemgr_check_file.feature b/test/functional-tests/features/systimemgr_check_file.feature new file mode 100644 index 00000000..22bcdd0c --- /dev/null +++ b/test/functional-tests/features/systimemgr_check_file.feature @@ -0,0 +1,40 @@ +########################################################################## +# If not stated otherwise in this file or this component's LICENSE +# file the following copyright and licenses apply: +# +# Copyright 2018 RDK Management +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +########################################################################## + +Feature: Ensures SystemTimeManager generates /opt/secure/clock.txt File + + Scenario: Ensures SystemTimeManager Initialization + Given the SystemTimeManager is not already running + When the SystemTimeManager binary is invoked + Then the SystemTimeManager should be started + + Scenario: Check if systemTimeManager is Running + Given the SystemTimeManager is running + When the SystemTimeManager binary is invoked + Then the SystemTimeManager should be running + + Scenario: Verify SystemTimeManager Log file exists + Given the SystemTimeManager is running + When the SystemTimeManager binary is invoked + Then the SystemTimeManager LogFile should get generated + + Scenario: Verify SystemTimeManager generates clock.txt File + Given the SystemTimeManager is running + When the SystemTimeManager returns the last known time + Then the SystemTimeManager should update the clock.txt File diff --git a/test/functional-tests/features/systimemgr_get_time.feature b/test/functional-tests/features/systimemgr_get_time.feature new file mode 100644 index 00000000..777cebb7 --- /dev/null +++ b/test/functional-tests/features/systimemgr_get_time.feature @@ -0,0 +1,40 @@ +########################################################################## +# If not stated otherwise in this file or this component's LICENSE +# file the following copyright and licenses apply: +# +# Copyright 2018 RDK Management +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +########################################################################## + +Feature: Ensures SystemTimeManager Returns the Last known Good time + + Scenario: Ensures SystemTimeManager Initialization + Given the SystemTimeManager is not already running + When the SystemTimeManager binary is invoked + Then the SystemTimeManager should be started + + Scenario: Check if systemTimeManager is Running + Given the SystemTimeManager is running + When the SystemTimeManager binary is invoked + Then the SystemTimeManager should be running + + Scenario: Verify SystemTimeManager Log file exists + Given the SystemTimeManager is running + When the SystemTimeManager binary is invoked + Then the SystemTimeManager LogFile should get generated + + Scenario: Verify SystemTimeManager Captured Last known Good time + Given the SystemTimeManager is running + When the SystemTimeManager binary is invoked + Then the SystemTimeManager should update the lastknown Good time diff --git a/test/functional-tests/features/systimemgr_initialisation.feature b/test/functional-tests/features/systimemgr_initialisation.feature new file mode 100644 index 00000000..ed50c2c9 --- /dev/null +++ b/test/functional-tests/features/systimemgr_initialisation.feature @@ -0,0 +1,40 @@ +########################################################################## +# If not stated otherwise in this file or this component's LICENSE +# file the following copyright and licenses apply: +# +# Copyright 2018 RDK Management +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +########################################################################## + +Feature: Ensures SystemTimeManager Initialization + + Scenario: Ensures SystemTimeManager Initialization + Given the SystemTimeManager is not already running + When the SystemTimeManager binary is invoked + Then the SystemTimeManager should be started + + Scenario: Check if systemTimeManager is Running + Given the SystemTimeManager is running + When the SystemTimeManager binary is invoked + Then the SystemTimeManager should be running + + Scenario: Verify SystemTimeManager Log file exists + Given the SystemTimeManager is running + When the SystemTimeManager binary is invoked + Then the SystemTimeManager LogFile should get generated + + Scenario: Verify SystemTimeManager TimeSRC and TimeSYNC are Initialized + Given the SystemTimeManager is running + When the SystemTimeManager binary is invoked + Then the TimeSources and TimeSyncs should be Initialized diff --git a/test/functional-tests/features/systimemgr_secureTimeInitialisation.feature b/test/functional-tests/features/systimemgr_secureTimeInitialisation.feature new file mode 100644 index 00000000..bd517820 --- /dev/null +++ b/test/functional-tests/features/systimemgr_secureTimeInitialisation.feature @@ -0,0 +1,40 @@ +########################################################################## +# If not stated otherwise in this file or this component's LICENSE +# file the following copyright and licenses apply: +# +# Copyright 2018 RDK Management +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +########################################################################## + +Feature: Ensures SystemTimeManager SecureTime Initialization + + Scenario: Ensures SystemTimeManager Initialization + Given the SystemTimeManager is not already running + When the SystemTimeManager binary is invoked + Then the SystemTimeManager should be started + + Scenario: Check if systemTimeManager is Running + Given the SystemTimeManager is running + When the SystemTimeManager binary is invoked + Then the SystemTimeManager should be running + + Scenario: Verify SystemTimeManager Log file exists + Given the SystemTimeManager is running + When the SystemTimeManager binary is invoked + Then the SystemTimeManager LogFile should get generated + + Scenario: Verify SystemTimeManager TimeSRC and TimeSYNC are Initialized as DRM + Given the SystemTimeManager is running + When the SystemTimeManager binary is invoked + Then the TimeSources and TimeSyncs should be Initialized diff --git a/test/functional-tests/features/systimemgr_secureTime_check_event.feature b/test/functional-tests/features/systimemgr_secureTime_check_event.feature new file mode 100644 index 00000000..50902361 --- /dev/null +++ b/test/functional-tests/features/systimemgr_secureTime_check_event.feature @@ -0,0 +1,45 @@ +########################################################################## +# If not stated otherwise in this file or this component's LICENSE +# file the following copyright and licenses apply: +# +# Copyright 2018 RDK Management +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +########################################################################## + +Feature: Ensures /tmp/systimemgr/drm file is available. + + Scenario: Ensures SystemTimeManager Initialization + Given the SystemTimeManager is not already running + When the SystemTimeManager binary is invoked + Then the SystemTimeManager should be started + + Scenario: Check if systemTimeManager is Running + Given the SystemTimeManager is running + When the SystemTimeManager binary is invoked + Then the SystemTimeManager should be running + + Scenario: Verify SystemTimeManager Log file exists + Given the SystemTimeManager is running + When the SystemTimeManager binary is invoked + Then the SystemTimeManager LogFile should get generated + + Scenario: Verify That /tmp/systimemgr/drm file is created/modified. + Given the SystemTimeManager is running + When the Secure Time is available + Then the SystemTimeManager should create/modify the /tmp/systimemgr/drm file. + + Scenario: Verify the Event should be eSYSMGR_EVENT_SECURE_TIME_AVAILABLE. + Given the SystimeTimeManager is running + When the Secure Time is available + Then the Event eSYSMGR_EVENT_SECURE_TIME_AVAILABLE should be found and Secure Message should be published. diff --git a/test/functional-tests/features/systimemgr_secureTime_quality.feature b/test/functional-tests/features/systimemgr_secureTime_quality.feature new file mode 100644 index 00000000..490ff044 --- /dev/null +++ b/test/functional-tests/features/systimemgr_secureTime_quality.feature @@ -0,0 +1,45 @@ +########################################################################## +# If not stated otherwise in this file or this component's LICENSE +# file the following copyright and licenses apply: +# +# Copyright 2018 RDK Management +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +########################################################################## + +Feature: Ensures SystemTimeManager Broadcasting Time Quality Messages + + Scenario: Ensures SystemTimeManager Initialization + Given the SystemTimeManager is not already running + When the SystemTimeManager binary is invoked + Then the SystemTimeManager should be started + + Scenario: Check if systemTimeManager is Running + Given the SystemTimeManager is running + When the SystemTimeManager binary is invoked + Then the SystemTimeManager should be running + + Scenario: Verify SystemTimeManager Log file exists + Given the SystemTimeManager is running + When the SystemTimeManager binary is invoked + Then the SystemTimeManager LogFile should get generated + + Scenario: Verify SystemTimeManager should sent time quality Poor when Secure Time is not available. + Given the SystemTimeManager is running + When the SystemTimeManager binary is invoked + Then the SystemTimeManager should sent time quality Poor Msg Initially when Secure Time is not available. + + Scenario: Verify SystemTimeManager should sent time quality Secure When Secure Time is available. + Given the SystemTimeManager is running + When the SystemTimeManager binary is invoked + Then the SystemTimeManager should sent time quality Secure When Secure Time is available. diff --git a/test/functional-tests/features/systimemgr_single_instance.feature b/test/functional-tests/features/systimemgr_single_instance.feature new file mode 100644 index 00000000..11f55559 --- /dev/null +++ b/test/functional-tests/features/systimemgr_single_instance.feature @@ -0,0 +1,27 @@ +########################################################################## +# If not stated otherwise in this file or this component's LICENSE +# file the following copyright and licenses apply: +# +# Copyright 2018 RDK Management +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +########################################################################## + +Feature: SystemTimeManager runs only one instance + + Scenario: SystemTimeManager exits if another instance is invoked + Given the SystemTimeManager is not already running + When the SystemTimeManager binary is invoked + Then the SystemTimeManager should be started + And when the SystemTimeManager is attempted to be started again + Then the SystemTimeManager should not start another instance diff --git a/test/functional-tests/features/systimemgr_time_quality.feature b/test/functional-tests/features/systimemgr_time_quality.feature new file mode 100644 index 00000000..c6cad21f --- /dev/null +++ b/test/functional-tests/features/systimemgr_time_quality.feature @@ -0,0 +1,45 @@ +########################################################################## +# If not stated otherwise in this file or this component's LICENSE +# file the following copyright and licenses apply: +# +# Copyright 2018 RDK Management +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +########################################################################## + +Feature: Ensures SystemTimeManager Broadcasting Time Quality Messages + + Scenario: Ensures SystemTimeManager Initialization + Given the SystemTimeManager is not already running + When the SystemTimeManager binary is invoked + Then the SystemTimeManager should be started + + Scenario: Check if systemTimeManager is Running + Given the SystemTimeManager is running + When the SystemTimeManager binary is invoked + Then the SystemTimeManager should be running + + Scenario: Verify SystemTimeManager Log file exists + Given the SystemTimeManager is running + When the SystemTimeManager binary is invoked + Then the SystemTimeManager LogFile should get generated + + Scenario: Verify SystemTimeManager should sent time quality Poor when NTP failed + Given the SystemTimeManager is running + When the SystemTimeManager binary is invoked + Then the SystemTimeManager should sent time quality Poor Msg Initially since NTP Failed + + Scenario: Verify SystemTimeManager should sent time quality Good when NTP is synced + Given the SystemTimeManager is running + When the SystemTimeManager binary is invoked + Then the SystemTimeManager should sent time quality Good When NTP is synced diff --git a/test/functional-tests/tests/helper_functions.py b/test/functional-tests/tests/helper_functions.py new file mode 100644 index 00000000..1d872668 --- /dev/null +++ b/test/functional-tests/tests/helper_functions.py @@ -0,0 +1,140 @@ +########################################################################## +# If not stated otherwise in this file or this component's LICENSE +# file the following copyright and licenses apply: +# +# Copyright 2018 RDK Management +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +########################################################################## + +import subprocess +import requests +import os +import time +import re +import signal +import shutil +from time import sleep +import json + +LOG_FILE = "/opt/logs/systimemgr.log.0" + +# --------------------------------------------------------------------------- +# Network-status event injection helpers +# +# When sysTimeMgr is compiled with __LOCAL_TEST_ it uses WPEFrameworkMock.h +# instead of the real Thunder library. The mock SmartLinkType polls a file: +# +# /tmp/thunder_mock__.inject +# +# Writing JSON to that file injects an event into sysTimeMgr — no Thunder +# daemon or WebSocket server required. This mirrors the enservice +# entservices-testframework thunder mock approach. +# --------------------------------------------------------------------------- + +# Callsign from networkstatussrc.cpp (sanitised: '.' → '_') +_NM_CALLSIGN_SANITISED = "org_rdk_NetworkManager" +_NM_EVENT = "onInternetStatusChange" + +INJECT_FILE = f"/tmp/thunder_mock_{_NM_CALLSIGN_SANITISED}_{_NM_EVENT}.inject" +SUBSCRIBED_FILE = f"/tmp/thunder_mock_{_NM_CALLSIGN_SANITISED}_{_NM_EVENT}.subscribed" + + +def inject_internet_status(status, interface="eth0"): + """Inject an onInternetStatusChange event into the running sysTimeMgr. + Writes the JSON payload to the file that the mock SmartLinkType polls. + Uses an atomic rename to avoid the mock reading a partial file. + Args: + status (str): e.g. "FULLY_CONNECTED" or "NO_INTERNET" + interface (str): network interface name (default "eth0") + """ + payload = json.dumps({"status": status, "interface": interface}) + tmp = INJECT_FILE + ".tmp" + try: + with open(tmp, "w") as f: + f.write(payload + "\n") + os.rename(tmp, INJECT_FILE) + return True + except Exception as exc: + print(f"[inject_internet_status] {exc}") + return False + + +def wait_for_nw_subscription(timeout_s=15): + """Wait until sysTimeMgr has subscribed to onInternetStatusChange. + The mock SmartLinkType creates the .subscribed marker file when + Subscribe() is called. Returns True once the file exists. + """ + deadline = time.time() + timeout_s + while time.time() < deadline: + if os.path.exists(SUBSCRIBED_FILE): + return True + sleep(0.2) + return False + + +def clear_inject_file(): + """Remove any leftover inject file from a previous test run.""" + try: + if os.path.exists(INJECT_FILE): + os.remove(INJECT_FILE) + except OSError: + pass + +def remove_logfile(): + try: + if os.path.exists(LOG_FILE): + os.remove(LOG_FILE) + print(f"Log file {LOG_FILE} removed.") + else: + print(f"Log file {LOG_FILE} does not exist.") + except Exception as e: + print(f"Could not remove log file {LOG_FILE}: {e}") + +def kill_sysTimeMgr(signal: int=9): + print(f"Received Signal to kill systimemgr {signal} with pid {get_pid('sysTimeMgr')}") + resp = subprocess.run(f"kill -{signal} {get_pid('sysTimeMgr')}", shell=True, capture_output=True) + print(resp.stdout.decode('utf-8')) + print(resp.stderr.decode('utf-8')) + return "" + +def grep_sysTimeMgrlogs(search: str): + search_result = "" + search_pattern = re.compile(re.escape(search), re.IGNORECASE) + try: + with open(LOG_FILE, 'r', encoding='utf-8', errors='ignore') as file: + for line_number, line in enumerate(file, start=1): + if search_pattern.search(line): + search_result = search_result + " \n" + line + except Exception as e: + print(f"Could not read file {LOG_FILE}: {e}") + return search_result + +def get_pid(name: str): + return subprocess.run(f"pidof {name}", shell=True, capture_output=True).stdout.decode('utf-8').strip() + +def run_shell_silent(command): + subprocess.run(command, shell=True, capture_output=False, text=False) + return + +def run_shell_command(command): + result = subprocess.run(command, shell=True, capture_output=True, text=True) + return result.stdout.strip() + +def is_systemtimemgr_running(): + command_to_check = "ps aux | grep sysTimeMgr | grep -v grep" + result = run_shell_command(command_to_check) + return result != "" + +def check_file_exists(file_path): + return os.path.isfile(file_path) diff --git a/test/functional-tests/tests/test_secureTime_checkEvent.py b/test/functional-tests/tests/test_secureTime_checkEvent.py new file mode 100644 index 00000000..9762888b --- /dev/null +++ b/test/functional-tests/tests/test_secureTime_checkEvent.py @@ -0,0 +1,41 @@ +########################################################################## +# If not stated otherwise in this file or this component's LICENSE +# file the following copyright and licenses apply: +# +# Copyright 2018 RDK Management +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +########################################################################## +from time import sleep +from helper_functions import * + +def test_is_systimemgr_running(): + command_to_check = "pidof sysTimeMgr" + result = run_shell_command(command_to_check) + assert result != "", "sysTimeMgr process did not start" + +def test_check_systimemgr_log_file(): + log_file_path = LOG_FILE + assert check_file_exists(log_file_path), f"Log File '{log_file_path}' does not exist." + +def test_time_quality(): + NOTIFY_PATH = "File Name /tmp/systimemgr/drm, opened for writing" + assert NOTIFY_PATH in grep_sysTimeMgrlogs(NOTIFY_PATH) + +def test_Event_check(): + FIND_EVENT = "Event = 7" + assert FIND_EVENT in grep_sysTimeMgrlogs(FIND_EVENT) + +def test_Event_processing(): + EVENT_MSG = "There is no Event Processing available in this state" + assert EVENT_MSG in grep_sysTimeMgrlogs(EVENT_MSG) diff --git a/test/functional-tests/tests/test_secureTime_initialisation.py b/test/functional-tests/tests/test_secureTime_initialisation.py new file mode 100644 index 00000000..5a9adca0 --- /dev/null +++ b/test/functional-tests/tests/test_secureTime_initialisation.py @@ -0,0 +1,33 @@ +########################################################################## +# If not stated otherwise in this file or this component's LICENSE +# file the following copyright and licenses apply: +# +# Copyright 2018 RDK Management +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +########################################################################## +from time import sleep +from helper_functions import * + +def test_is_systimemgr_running(): + command_to_check = "pidof sysTimeMgr" + result = run_shell_command(command_to_check) + assert result != "", "sysTimeMgr process did not start" + +def test_check_systimemgr_log_file(): + log_file_path = LOG_FILE + assert check_file_exists(log_file_path), f"Log File '{log_file_path}' does not exist." + +def test_Initialization(): + INITIALIZATION_MSG = "Initializing Src and Syncs. Category = timesrc, Type = drm, Args = /drm" + assert INITIALIZATION_MSG in grep_sysTimeMgrlogs(INITIALIZATION_MSG) diff --git a/test/functional-tests/tests/test_secureTime_quality.py b/test/functional-tests/tests/test_secureTime_quality.py new file mode 100644 index 00000000..ab642357 --- /dev/null +++ b/test/functional-tests/tests/test_secureTime_quality.py @@ -0,0 +1,36 @@ +########################################################################## +# If not stated otherwise in this file or this component's LICENSE +# file the following copyright and licenses apply: +# +# Copyright 2018 RDK Management +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +########################################################################## +from time import sleep +from helper_functions import * + +def test_is_systimemgr_running(): + command_to_check = "pidof sysTimeMgr" + result = run_shell_command(command_to_check) + assert result != "", "sysTimeMgr process did not start" + +def test_check_systimemgr_log_file(): + log_file_path = LOG_FILE + assert check_file_exists(log_file_path), f"Log File '{log_file_path}' does not exist." + +def test_time_quality(): + TIMEQUALITY_MSG_POOR = "Info: MsgType = 0, Quality = 0, Message = Poor" + assert TIMEQUALITY_MSG_POOR in grep_sysTimeMgrlogs(TIMEQUALITY_MSG_POOR) + + TIMEQUALITY_MSG_SECURE = "Info: MsgType = 4, Quality = 2, Message = Secure" + assert TIMEQUALITY_MSG_SECURE in grep_sysTimeMgrlogs(TIMEQUALITY_MSG_SECURE) diff --git a/test/functional-tests/tests/test_systimemgr_bootup_flow.py b/test/functional-tests/tests/test_systimemgr_bootup_flow.py new file mode 100644 index 00000000..d8cf4f9f --- /dev/null +++ b/test/functional-tests/tests/test_systimemgr_bootup_flow.py @@ -0,0 +1,51 @@ +########################################################################## +# If not stated otherwise in this file or this component's LICENSE +# file the following copyright and licenses apply: +# +# Copyright 2018 RDK Management +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +########################################################################## +from time import sleep +from helper_functions import * + +def test_is_systimemgr_running(): + command_to_check = "pidof sysTimeMgr" + result = run_shell_command(command_to_check) + assert result != "", "sysTimeMgr process did not start" + +def test_check_systimemgr_log_file(): + log_file_path = LOG_FILE + assert check_file_exists(log_file_path), f"Log File '{log_file_path}' does not exist." + +def test_bootup_flow(): + CREATE_INSTANCE_MSG = "Created New Instance" + assert CREATE_INSTANCE_MSG in grep_sysTimeMgrlogs(CREATE_INSTANCE_MSG) + + CHECK_INITIALIZATION_MSG_NTP = "Initializing Src and Syncs. Category = timesrc, Type = ntp, Args = /ntp" + assert CHECK_INITIALIZATION_MSG_NTP in grep_sysTimeMgrlogs(CHECK_INITIALIZATION_MSG_NTP) + + CHECK_INITIALIZATION_MSG_DTT = "Initializing Src and Syncs. Category = timesrc, Type = dtt, Args = /dtt" + assert CHECK_INITIALIZATION_MSG_DTT in grep_sysTimeMgrlogs(CHECK_INITIALIZATION_MSG_DTT) + + CHECK_INITIALIZATION_MSG_RDKDEFAULT = "Initializing Src and Syncs. Category = timesync, Type = rdkdefault, Args = /clock_time" + assert CHECK_INITIALIZATION_MSG_RDKDEFAULT in grep_sysTimeMgrlogs(CHECK_INITIALIZATION_MSG_RDKDEFAULT) + + CHECK_LAST_GOOD_TIME_MSG = "Returning Last Known Good Time, time =" + assert CHECK_LAST_GOOD_TIME_MSG in grep_sysTimeMgrlogs(CHECK_LAST_GOOD_TIME_MSG) + + CHECK_TIME_QUALITY_POOR_MSG = "Info: MsgType = 0, Quality = 0, Message = Poor" + assert CHECK_TIME_QUALITY_POOR_MSG in grep_sysTimeMgrlogs(CHECK_TIME_QUALITY_POOR_MSG) + + CHECK_TIME_QUALITY_GOOD_MSG = "Info: MsgType = 2, Quality = 1, Message = Good" + assert CHECK_TIME_QUALITY_GOOD_MSG in grep_sysTimeMgrlogs(CHECK_TIME_QUALITY_GOOD_MSG) diff --git a/test/functional-tests/tests/test_systimemgr_check_file.py b/test/functional-tests/tests/test_systimemgr_check_file.py new file mode 100644 index 00000000..daec474c --- /dev/null +++ b/test/functional-tests/tests/test_systimemgr_check_file.py @@ -0,0 +1,37 @@ +########################################################################## +# If not stated otherwise in this file or this component's LICENSE +# file the following copyright and licenses apply: +# +# Copyright 2018 RDK Management +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +########################################################################## +from time import sleep +from helper_functions import * + +def test_is_systimemgr_running(): + command_to_check = "pidof sysTimeMgr" + result = run_shell_command(command_to_check) + assert result != "", "sysTimeMgr process did not start" + +def test_check_systimemgr_log_file(): + log_file_path = LOG_FILE + assert check_file_exists(log_file_path), f"Log File '{log_file_path}' does not exist." + +def test_check_clock_file_exists(): + clock_file = "/opt/secure/clock.txt" + assert check_file_exists(clock_file), f"clock.txt File '{clock_file}' does not exist." + +def test_check_file_updated(): + UPDATE_FILE = "Updating Time in file" + assert UPDATE_FILE in grep_sysTimeMgrlogs(UPDATE_FILE) diff --git a/test/functional-tests/tests/test_systimemgr_get_time.py b/test/functional-tests/tests/test_systimemgr_get_time.py new file mode 100644 index 00000000..e608e2f1 --- /dev/null +++ b/test/functional-tests/tests/test_systimemgr_get_time.py @@ -0,0 +1,54 @@ +########################################################################## +# If not stated otherwise in this file or this component's LICENSE +# file the following copyright and licenses apply: +# +# Copyright 2018 RDK Management +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +########################################################################## +from time import sleep +from helper_functions import * + +def test_is_systimemgr_running(): + command_to_check = "pidof sysTimeMgr" + result = run_shell_command(command_to_check) + assert result != "", "sysTimeMgr process did not start" + +def test_check_systimemgr_log_file(): + log_file_path = LOG_FILE + assert check_file_exists(log_file_path), f"Log File '{log_file_path}' does not exist." + +def test_last_known_time(): + GET_TIME = "Returning Last Known Good Time" + assert GET_TIME in grep_sysTimeMgrlogs(GET_TIME) + +def parse_log_time(pattern, clock_file): + with open(LOG_FILE, "r") as log_file: + for line in log_file: + match = re.search(pattern, line) + if match: + timestamp = match.group(1).strip() + with open(clock_file, 'w') as out_file: + out_file.write(timestamp) + return timestamp + return None + +def test_parse_log_time(): + clock_file = '/opt/secure/clock_test.txt' + + pattern = r"Returning Last Known Good Time, *time\s* = \s*(.+)" + time_str = parse_log_time(pattern, clock_file) + + assert time_str != "" + + assert check_file_exists(clock_file), f"clock Test file '{clock_file}' not generated" diff --git a/test/functional-tests/tests/test_systimemgr_initialisation.py b/test/functional-tests/tests/test_systimemgr_initialisation.py new file mode 100644 index 00000000..0c51bc3d --- /dev/null +++ b/test/functional-tests/tests/test_systimemgr_initialisation.py @@ -0,0 +1,33 @@ +########################################################################## +# If not stated otherwise in this file or this component's LICENSE +# file the following copyright and licenses apply: +# +# Copyright 2018 RDK Management +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +########################################################################## +from time import sleep +from helper_functions import * + +def test_is_systimemgr_running(): + command_to_check = "pidof sysTimeMgr" + result = run_shell_command(command_to_check) + assert result != "", "sysTimeMgr process did not start" + +def test_check_systimemgr_log_file(): + log_file_path = LOG_FILE + assert check_file_exists(log_file_path), f"Log File '{log_file_path}' does not exist." + +def test_Initialization(): + INITIALIZATION_MSG = "Initializing Src and Syncs" + assert INITIALIZATION_MSG in grep_sysTimeMgrlogs(INITIALIZATION_MSG) diff --git a/test/functional-tests/tests/test_systimemgr_nwstatus.py b/test/functional-tests/tests/test_systimemgr_nwstatus.py new file mode 100644 index 00000000..7aff6025 --- /dev/null +++ b/test/functional-tests/tests/test_systimemgr_nwstatus.py @@ -0,0 +1,332 @@ +########################################################################## +# If not stated otherwise in this file or this component's LICENSE +# file the following copyright and licenses apply: +# +# Copyright 2024 RDK Management +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +########################################################################## +""" +L2 tests for the NWStatusMonitor feature (topic/NWStatusMonitor). +Verifies that sysTimeMgr: + 1. Subscribes to onInternetStatusChange from org.rdk.NetworkManager. + 2. Processes a FULLY_CONNECTED event and triggers the appropriate + chrony sync logic. + 3. Ignores non-connected status events (no chrony action logged). + 4. Does not re-process a duplicate FULLY_CONNECTED event while + lastStatus is already fully_connected. +When compiled with -D__LOCAL_TEST_ (the default for test builds) sysTimeMgr +uses WPEFrameworkMock.h instead of the real Thunder library. Events are +injected by writing JSON to a file that the mock SmartLinkType polls: + /tmp/thunder_mock_org_rdk_NetworkManager_onInternetStatusChange.inject +No Thunder daemon, WebSocket server, or network-mock.js is required. +This mirrors the enservice entservices-testframework thunder mock approach. +Log-scanning strategy +--------------------- +Every test records the current byte offset of the log file at the point it +starts (via _log_offset()). All subsequent log checks use _wait_for_new_log() +or _count_in_new_log() which seek to that offset before reading, so stale +content from earlier tests or previous runs can never cause a false-pass. +run_l2.sh removes the log file before starting the NWStatus sysTimeMgr +instance, so the log is always fresh for this test suite. +""" + +import os +import time +from time import sleep +from helper_functions import ( + run_shell_command, + check_file_exists, + wait_for_nw_subscription, + inject_internet_status, + clear_inject_file, + LOG_FILE, +) + +# --------------------------------------------------------------------------- +# Private helpers +# --------------------------------------------------------------------------- + +CLOCK_EVENT_FILE = "/tmp/clock-event" + + +def _log_offset(): + """Return the current byte size of the log file. + Used as a cursor: pass the return value to _wait_for_new_log() / + _count_in_new_log() so those functions only scan content written *after* + this point in time, preventing stale lines from earlier tests causing + false-passes. + """ + try: + return os.path.getsize(LOG_FILE) + except OSError: + return 0 + + +def _wait_for_new_log(message, after_offset=0, timeout_s=10, poll_interval_s=0.5): + """Poll the log file for *message* in content written after *after_offset*. + Only bytes from *after_offset* onward are scanned, so lines that existed + before the test injected its event cannot satisfy the assertion. + Args: + message (str): Substring to search for. + after_offset (int): Byte position returned by _log_offset() at test start. + timeout_s (float): Maximum time to wait. + poll_interval_s: Sleep between read attempts. + Returns: + True if *message* is found within *timeout_s* seconds, else False. + """ + deadline = time.time() + timeout_s + while time.time() < deadline: + try: + with open(LOG_FILE, "r", encoding="utf-8", errors="ignore") as f: + f.seek(after_offset) + if message in f.read(): + return True + except OSError: + pass + sleep(poll_interval_s) + return False + + +def _count_in_new_log(message, after_offset=0): + """Count occurrences of *message* in log content written after *after_offset*. + Uses the same offset-based approach as _wait_for_new_log() so only fresh + log lines are counted. + Args: + message (str): Substring to count. + after_offset (int): Byte position returned by _log_offset(). + Returns: + int — number of occurrences found (0 if file unreadable). + """ + try: + with open(LOG_FILE, "r", encoding="utf-8", errors="ignore") as f: + f.seek(after_offset) + return f.read().count(message) + except OSError: + return 0 + + +# --------------------------------------------------------------------------- +# Liveness / precondition tests (must pass before event tests) +# --------------------------------------------------------------------------- + +def test_sysTimeMgr_is_running(): + """sysTimeMgr process must be running. + run_l2.sh kills and restarts sysTimeMgr (with RFC chrony marker present) + before executing this suite. If the process is absent the remaining + tests are meaningless. + """ + pid = run_shell_command("pidof sysTimeMgr") + assert pid != "", "sysTimeMgr process did not start" + + +def test_log_file_exists(): + """sysTimeMgr log file must exist. + rdkloggers creates /opt/logs/systimemgr.log.0 on the first write after + startup. run_l2.sh removes the file before the fresh sysTimeMgr start; + if rdkloggers has written at least one line by the time this test runs + the file will be present. + """ + assert check_file_exists(LOG_FILE), f"Log file '{LOG_FILE}' does not exist" + + +# --------------------------------------------------------------------------- +# Subscription test +# --------------------------------------------------------------------------- + +def test_sysTimeMgr_subscribes_to_internet_status_change(): + """sysTimeMgr must subscribe to onInternetStatusChange at startup. + The mock SmartLinkType (WPEFrameworkMock.h) creates a marker file + /tmp/thunder_mock_org_rdk_NetworkManager_onInternetStatusChange.subscribed + when Subscribe() is called. Two checks: + 1. Marker file exists (fastest signal — does not depend on logging). + 2. The 'Successfully subscribed' line appears in log content written + after this test started (offset-based to avoid stale matches). + """ + offset = _log_offset() + subscribed = wait_for_nw_subscription(timeout_s=20) + assert subscribed, ( + "sysTimeMgr did not call SmartLinkType::Subscribe() within timeout. " + "Ensure sysTimeMgr is compiled with -D__LOCAL_TEST_ and that " + "networkstatussrc.cpp is included (topic/NWStatusMonitor branch)." + ) + + # run_l2.sh removes the log file before starting this sysTimeMgr instance + # so the entire log is fresh. The subscription happens during the 'sleep 2' + # in run_l2.sh — before pytest even starts — so offset (captured after + # pytest launches) is already past that line. Use after_offset=0 to scan + # the whole (fresh) log; stale-line risk is eliminated by the log rotation. + expected_log = "Successfully subscribed to onInternetStatusChange" + assert _wait_for_new_log(expected_log, after_offset=0, timeout_s=10), ( + f"Expected log line not found in {LOG_FILE}: '{expected_log}'" + ) + + +# --------------------------------------------------------------------------- +# Internet-up event tests +# --------------------------------------------------------------------------- + +def test_fully_connected_event_is_processed(): + """Injecting FULLY_CONNECTED triggers chrony processing in sysTimeMgr. + Core NWStatusMonitor path: + handle_internetStatusChange + → lastStatus transitions '' -> 'fully_connected' + → g_internetUpPending = true, g_cv.notify_one() + → runEventProcessingLoop wakes + → processInternetOnline() logs 'CHRONY: Processing internet fully_connected event' + The log scan starts from the byte offset captured *before* injection so + a matching line from a previous test cannot satisfy the assertion. + """ + # A prior event may have left lastStatus as 'fully_connected'. Reset it + # with NO_INTERNET so this injection is not treated as a duplicate. + clear_inject_file() + inject_internet_status("NO_INTERNET", interface="eth0") + sleep(1) + + if os.path.exists(CLOCK_EVENT_FILE): + os.remove(CLOCK_EVENT_FILE) + clear_inject_file() + + offset = _log_offset() + ok = inject_internet_status("FULLY_CONNECTED", interface="eth0") + assert ok, "Failed to write FULLY_CONNECTED inject file" + + expected_log = "CHRONY: Processing internet fully_connected event" + assert _wait_for_new_log(expected_log, after_offset=offset, timeout_s=10), ( + f"Expected log line not found after injection in {LOG_FILE}: '{expected_log}'. " + "sysTimeMgr may not have processed the FULLY_CONNECTED event." + ) + + +def test_fully_connected_event_signals_handling_log(): + """sysTimeMgr must log the signal-to-processing-thread message on fully_connected. + The signalling log line ('Internet status changed to fully_connected') + is emitted inside handle_internetStatusChange() immediately before + g_cv.notify_one(). It only appears when the status transitions FROM a + non-fully_connected state TO fully_connected (not for duplicates). + A NO_INTERNET injection resets lastStatus first so the subsequent + FULLY_CONNECTED is never treated as a duplicate. + """ + # Reset lastStatus to ensure the upcoming FULLY_CONNECTED is a fresh transition. + clear_inject_file() + inject_internet_status("NO_INTERNET", interface="eth0") + sleep(1) + clear_inject_file() + + offset = _log_offset() + ok = inject_internet_status("FULLY_CONNECTED", interface="eth0") + assert ok, "Failed to write FULLY_CONNECTED inject file" + + # Exact substring from the RDK_LOG call in handle_internetStatusChange(): + # "CHRONY: Internet status changed to fully_connected — signalling processing thread" + expected_log = "Internet status changed to fully_connected" + assert _wait_for_new_log(expected_log, after_offset=offset, timeout_s=10), ( + f"Expected log line not found after injection in {LOG_FILE}: '{expected_log}'" + ) + + +# --------------------------------------------------------------------------- +# Non-connected status — negative test +# --------------------------------------------------------------------------- + +def test_no_internet_event_is_not_processed(): + """A NO_INTERNET event must be received and logged, but must NOT trigger chrony. + Two assertions: + 1. Positive: the no-action log line for the specific status 'no_internet' + appears in NEW log content (offset-based). The exact substring is + 'Internet status=no_internet' from the RDK_LOG format string: + 'CHRONY: Internet status=%s (prev=%s) — no action needed' + This is specific enough that it can only match a NO_INTERNET event. + 2. Negative: the count of 'CHRONY: Processing internet fully_connected event' + in new content is zero — chrony processing was not triggered. + """ + clear_inject_file() + # Reset to a known state: inject FULLY_CONNECTED first so lastStatus is + # 'fully_connected', making the NO_INTERNET a genuine status change. + inject_internet_status("FULLY_CONNECTED", interface="eth0") + sleep(1) + clear_inject_file() + + offset = _log_offset() + ok = inject_internet_status("NO_INTERNET", interface="eth0") + assert ok, "Failed to write NO_INTERNET inject file" + + # Specific match: only a NO_INTERNET callback produces this substring. + no_action_log = "Internet status=no_internet" + assert _wait_for_new_log(no_action_log, after_offset=offset, timeout_s=10), ( + f"Expected 'no action needed' log for NO_INTERNET not found in new log content. " + f"Searched for: '{no_action_log}'" + ) + + # Verify chrony processing was NOT triggered by this NO_INTERNET event. + processing_log = "CHRONY: Processing internet fully_connected event" + assert _count_in_new_log(processing_log, after_offset=offset) == 0, ( + "sysTimeMgr incorrectly triggered CHRONY processing for a NO_INTERNET event" + ) + + +# --------------------------------------------------------------------------- +# Duplicate event deduplication test +# --------------------------------------------------------------------------- + +def test_duplicate_fully_connected_not_reprocessed(): + """A second consecutive FULLY_CONNECTED event must NOT trigger another chrony run. + networkstatussrc.cpp early-returns in handle_internetStatusChange() when + normalizedStatus equals the previous lastStatus, so g_internetUpPending is + NOT set for a duplicate — the processing thread is never woken. + Test structure: + 1. Verify the first FULLY_CONNECTED IS processed (count_first >= 1). + This guards against the degenerate case where subscription failed: + if count_first == 0 the second assertion would vacuously pass. + 2. Verify the duplicate FULLY_CONNECTED adds zero extra processing lines. + """ + # Ensure lastStatus is NOT 'fully_connected' before the first injection. + clear_inject_file() + inject_internet_status("NO_INTERNET", interface="eth0") + sleep(1) + clear_inject_file() + + processing_log = "CHRONY: Processing internet fully_connected event" + + # ── First injection: must be processed ────────────────────────────────── + offset_first = _log_offset() + inject_internet_status("FULLY_CONNECTED", interface="eth0") + assert _wait_for_new_log(processing_log, after_offset=offset_first, timeout_s=10), ( + "First FULLY_CONNECTED event was not processed — subscription may be broken. " + "Cannot proceed with duplicate-deduplication check." + ) + count_first = _count_in_new_log(processing_log, after_offset=offset_first) + assert count_first >= 1, ( + f"Expected at least one processing log line after first injection, got {count_first}" + ) + + # ── Duplicate injection: must NOT produce another processing line ──────── + # lastStatus is now 'fully_connected'; the next FULLY_CONNECTED is a duplicate. + offset_dup = _log_offset() + clear_inject_file() + inject_internet_status("FULLY_CONNECTED", interface="eth0") + sleep(3) # Give the mock poll thread time to deliver the event if it would. + + count_dup = _count_in_new_log(processing_log, after_offset=offset_dup) + assert count_dup == 0, ( + f"Duplicate FULLY_CONNECTED event incorrectly triggered {count_dup} " + "extra chrony processing run(s)" + ) + + + + expected_log = "Successfully subscribed to onInternetStatusChange" + assert _wait_for_new_log(expected_log, timeout_s=10), ( + f"Expected log line not found in {LOG_FILE}: '{expected_log}'" + ) + diff --git a/test/functional-tests/tests/test_systimemgr_single_instance.py b/test/functional-tests/tests/test_systimemgr_single_instance.py new file mode 100644 index 00000000..c651555e --- /dev/null +++ b/test/functional-tests/tests/test_systimemgr_single_instance.py @@ -0,0 +1,31 @@ +########################################################################## +# If not stated otherwise in this file or this component's LICENSE +# file the following copyright and licenses apply: +# +# Copyright 2018 RDK Management +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +########################################################################## + +from time import sleep +from helper_functions import * + +def test_check_systemtimemgr_is_starting(): + kill_sysTimeMgr() + print("Starting systemtimemgr process") + command_to_start = "nohup /usr/local/bin/sysTimeMgr > /dev/null 2>&1 &" + run_shell_silent(command_to_start) + command_to_get_pid = "pidof sysTimeMgr" + pid = run_shell_command(command_to_get_pid) + assert pid != "", "sysTimeMgr process did not start" + diff --git a/test/functional-tests/tests/test_systimemgr_time_quality.py b/test/functional-tests/tests/test_systimemgr_time_quality.py new file mode 100644 index 00000000..45df6385 --- /dev/null +++ b/test/functional-tests/tests/test_systimemgr_time_quality.py @@ -0,0 +1,36 @@ +########################################################################## +# If not stated otherwise in this file or this component's LICENSE +# file the following copyright and licenses apply: +# +# Copyright 2018 RDK Management +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +########################################################################## +from time import sleep +from helper_functions import * + +def test_is_systimemgr_running(): + command_to_check = "pidof sysTimeMgr" + result = run_shell_command(command_to_check) + assert result != "", "sysTimeMgr process did not start" + +def test_check_systimemgr_log_file(): + log_file_path = LOG_FILE + assert check_file_exists(log_file_path), f"Log File '{log_file_path}' does not exist." + +def test_time_quality(): + TIMEQUALITY_MSG_POOR = "Info: MsgType = 0, Quality = 0, Message = Poor" + assert TIMEQUALITY_MSG_POOR in grep_sysTimeMgrlogs(TIMEQUALITY_MSG_POOR) + + TIMEQUALITY_MSG_GOOD = "Info: MsgType = 2, Quality = 1, Message = Good" + assert TIMEQUALITY_MSG_GOOD in grep_sysTimeMgrlogs(TIMEQUALITY_MSG_GOOD) diff --git a/test/run_ut.sh b/test/run_ut.sh index 333d5f92..07854f87 100644 --- a/test/run_ut.sh +++ b/test/run_ut.sh @@ -19,14 +19,14 @@ # limitations under the License. #################################################################################### -ENABLE_COV=false +ENABLE_COV=true if [ "x$1" = "x--enable-cov" ]; then - echo "Enabling coverage options" - export CXXFLAGS="-g -O0 -fprofile-arcs -ftest-coverage" - export CFLAGS="-g -O0 -fprofile-arcs -ftest-coverage" - export LDFLAGS="-lgcov --coverage" - ENABLE_COV=true + echo "Enabling coverage options" + export CXXFLAGS="-g -O0 -fprofile-arcs -ftest-coverage" + export CFLAGS="-g -O0 -fprofile-arcs -ftest-coverage" + export LDFLAGS="-lgcov --coverage" + ENABLE_COV=true fi export TOP_DIR=`pwd` @@ -41,34 +41,61 @@ cd ./systimerfactory/unittest/ automake --add-missing autoreconf --install -find / -iname "jsonrpccpp" +find / -iname "jsonrpccpp" # This command might not be necessary for the build ./configure make mkdir -p /opt/secure/ -# Execute test suites for different sub-modules - -./drmtest_gtest -./dtttest_gtest -./rdkDefaulttest_gtest -./timerfactory_gtest -./pubsubfactory_gtest -./ipowercontrollersubscriber_gtest -./iarmtimerstatus_gtest -./iarmsubscribe_gtest -./iarmpublish_gtest -./iarmpowersubscribe_gtest -./systimemgr_gtest + +# Create a directory to store Gtest XML reports +mkdir -p /tmp/gtest_reports/ + +fail=0 + +run_test() { + test_binary="$1" + report_file="/tmp/gtest_reports/${test_binary}.xml" # Define output path for XML report + echo "Running $test_binary with XML output to $report_file..." + # Execute the test binary with gtest_output flag + ./$test_binary --gtest_output=xml:"${report_file}" + status=$? + if [ $status -ne 0 ]; then + echo "Test $test_binary failed with exit code $status" + fail=1 + else + echo "Test $test_binary passed" + fi + echo "------------------------------------" +} + +run_test drmtest_gtest +run_test dtttest_gtest +run_test rdkDefaulttest_gtest +run_test timerfactory_gtest +run_test pubsubfactory_gtest +run_test ipowercontrollersubscriber_gtest +run_test iarmtimerstatus_gtest +run_test iarmsubscribe_gtest +run_test iarmpublish_gtest +run_test iarmpowersubscribe_gtest +run_test systimemgr_gtest + +if [ $fail -ne 0 ]; then + echo "Some unit tests failed." + exit 1 +fi echo "********************" echo "**** CAPTURE SYSTEM TIMEMANAGER COVERAGE DATA ****" echo "********************" if [ "$ENABLE_COV" = true ]; then echo "Generating coverage report" - lcov --capture --directory . --output-file coverage.info - lcov --remove coverage.info '/usr/*' --output-file coverage.info - lcov --remove coverage.info './systimerfactory/unittest/*' --output-file coverage.info + + lcov --capture --directory . --base-directory . --output-file raw_coverage.info + lcov --extract raw_coverage.info '/__w/systemtimemgr/systemtimemgr/systimerfactory/*' '/__w/systemtimemgr/systemtimemgr/systimemgr.cpp' --output-file systimer_coverage.info + lcov --remove systimer_coverage.info '/__w/systemtimemgr/systemtimemgr/systimerfactory/unittest/*' --output-file processed_coverage.info + lcov --extract processed_coverage.info '*.cpp' --output-file coverage.info lcov --list coverage.info fi -cd $TOP_DIR +cd "$TOP_DIR" # Use double quotes for robust path handling