diff --git a/.github/workflows/native_full_build.yml b/.github/workflows/native_full_build.yml new file mode 100644 index 0000000..e93d8c2 --- /dev/null +++ b/.github/workflows/native_full_build.yml @@ -0,0 +1,62 @@ +name: Build Component in Native Environment + +on: + workflow_dispatch: + inputs: + container_image: + description: Container image used for native build + required: false + default: ghcr.io/rdkcentral/docker-rdk-ci:latest + type: string + skip_build_dependencies: + description: Skip build_dependencies.sh + required: false + default: false + type: boolean + strict_transport_bootstrap: + description: Force transport bootstrap and ignore host/system package paths + required: false + default: true + type: boolean + force_release_transport: + description: Force transport bootstrap from pinned release archive (.transport.version) + required: false + default: false + type: boolean + push: + branches: [ main, '*.*.x-maintenance' ] + paths: ['**/*.c', '**/*.cpp', '**/*.cc', '**/*.cxx', '**/*.h', '**/*.hpp', 'CMakeLists.txt', 'cmake/**', 'build_dependencies.sh', 'cov_build.sh', '.github/workflows/native_full_build.yml'] + pull_request: + branches: [ main, '*.*.x-maintenance' ] + paths: ['**/*.c', '**/*.cpp', '**/*.cc', '**/*.cxx', '**/*.h', '**/*.hpp', 'CMakeLists.txt', 'cmake/**', 'build_dependencies.sh', 'cov_build.sh', '.github/workflows/native_full_build.yml'] + +permissions: + contents: read + +defaults: + run: + shell: bash + +jobs: + native-build: + name: Build firebolt-cpp-client in native environment + runs-on: ubuntu-latest + container: + image: ${{ github.event_name == 'workflow_dispatch' && inputs.container_image || 'ghcr.io/rdkcentral/docker-rdk-ci:latest' }} + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install build dependencies + if: ${{ github.event_name != 'workflow_dispatch' || !fromJSON(github.event.inputs.skip_build_dependencies || 'false') }} + run: bash -x build_dependencies.sh + + - name: Build + run: bash -x cov_build.sh + env: + COV_DEPS_PREFIX: ${{ github.workspace }}/.cov-deps + COV_FORCE_BOOTSTRAP_TRANSPORT: ${{ github.event_name == 'workflow_dispatch' && inputs.strict_transport_bootstrap && '1' || '0' }} + COV_SKIP_SYSTEM_TRANSPORT: ${{ github.event_name == 'workflow_dispatch' && inputs.strict_transport_bootstrap && '1' || '0' }} + COV_FORCE_RELEASE_TRANSPORT: ${{ github.event_name == 'workflow_dispatch' && inputs.force_release_transport && '1' || '0' }} + GITHUB_TOKEN: ${{ secrets.RDKCM_RDKE || github.token }} diff --git a/.gitignore b/.gitignore index 7ccfb8d..ee2b100 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ node_modules/ build/ build-*/ +.cov-deps*/ +act-*.log diff --git a/CHANGELOG.md b/CHANGELOG.md index 58e532d..767263f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## [0.6.1](https://github.com/rdkcentral/firebolt-cpp-client/compare/v0.6.0...v0.6.1) + +### Added +- `Discovery.watchedV2`: same as `Discovery.watched` but returns `Result` - compatibility shim for callers migrating away from the pre-v0.6.0 boolean return type + ## [0.6.0](https://github.com/rdkcentral/firebolt-cpp-client/compare/v0.5.5...v0.6.0) ### Added diff --git a/CMakeLists.txt b/CMakeLists.txt index 553f29e..b421e72 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -45,6 +45,27 @@ list(APPEND CMAKE_MODULE_PATH "${SYSROOT_PATH}/usr/lib/cmake" ) +set(FIREBOLT_TRANSPORT_ROOT "" CACHE PATH + "Optional install prefix for FireboltTransport (contains lib/cmake/FireboltTransport)" +) + +if(NOT FIREBOLT_TRANSPORT_ROOT AND DEFINED ENV{FIREBOLT_TRANSPORT_ROOT} AND NOT "$ENV{FIREBOLT_TRANSPORT_ROOT}" STREQUAL "") + set(FIREBOLT_TRANSPORT_ROOT "$ENV{FIREBOLT_TRANSPORT_ROOT}" CACHE PATH + "Optional install prefix for FireboltTransport (contains lib/cmake/FireboltTransport)" FORCE) +endif() + +if(FIREBOLT_TRANSPORT_ROOT) + list(INSERT CMAKE_PREFIX_PATH 0 "${FIREBOLT_TRANSPORT_ROOT}") +endif() + +if(NOT FireboltTransport_ROOT AND DEFINED ENV{FireboltTransport_ROOT}) + set(FireboltTransport_ROOT "$ENV{FireboltTransport_ROOT}") +endif() + +if(NOT FireboltTransport_ROOT AND FIREBOLT_TRANSPORT_ROOT) + set(FireboltTransport_ROOT "${FIREBOLT_TRANSPORT_ROOT}") +endif() + find_package(nlohmann_json CONFIG REQUIRED) file(READ "${CMAKE_CURRENT_SOURCE_DIR}/.transport.version" FIREBOLT_TRANSPORT_VERSION_RAW) diff --git a/COVERITY.md b/COVERITY.md new file mode 100644 index 0000000..dcdbcd0 --- /dev/null +++ b/COVERITY.md @@ -0,0 +1,150 @@ +# Coverity Build Guide + +This document describes the Coverity-friendly build flow for firebolt-cpp-client, including fully unattended transport dependency provisioning. + +## Purpose + +`cov_build.sh` is designed to run in clean/off nodes where FireboltTransport may not already be installed. + +The script configures and builds a Debug test-enabled build so Coverity can capture both library and test compilation units. + +## Quick Start + +Run from repo root: + +```bash +./cov_build.sh +``` + +Strict no-host-dependency mode: + +```bash +COV_FORCE_BOOTSTRAP_TRANSPORT=1 \ +COV_SKIP_SYSTEM_TRANSPORT=1 \ +COV_FORCE_RELEASE_TRANSPORT=1 \ +./cov_build.sh +``` + +Local ad-hoc workflow test via act: + +```bash +make -f Makefile.act act-native +``` + +Faster local loop when dependencies are already present in the container image: + +```bash +make -f Makefile.act act-native-fast +``` + +## Dependency Resolution Order + +`cov_build.sh` resolves FireboltTransport in this order. + +1. Explicit `FireboltTransport_DIR` if provided. +2. Local bootstrap prefix (`COV_DEPS_PREFIX`, default `.cov-deps`). +3. System CMake package paths (`/usr/local/...`, `/usr/...`) unless disabled. +4. Sibling repo bootstrap from `../firebolt-cpp-transport`. +5. Release tarball bootstrap using pinned version from `.transport.version`. + +After bootstrap, the script passes `-DFireboltTransport_DIR=` to CMake for deterministic package selection. + +## Hands-Off Bootstrap Paths + +### Sibling repo bootstrap + +If `../firebolt-cpp-transport` exists, the script builds and installs it into `COV_DEPS_PREFIX`. + +### Release tarball bootstrap + +If sibling repo is unavailable or `COV_FORCE_RELEASE_TRANSPORT=1` is set: + +1. Read pinned version from `.transport.version`. +2. Download release archive from GitHub releases. +3. Extract source into `.cov-deps/src/`. +4. Build/install into `COV_DEPS_PREFIX`. +5. Re-run client configure/build against the installed package config. + +The transport bootstrap passes: + +```bash +-DFIREBOLT_TRANSPORT_VERSION= +``` + +This keeps package version compatibility aligned with client `find_package(FireboltTransport CONFIG REQUIRED)`. + +## Environment Variables + +### `COV_DEPS_PREFIX` + +Install/bootstrap prefix for local dependencies. + +Default: + +```bash +/.cov-deps +``` + +### `COV_SKIP_SYSTEM_TRANSPORT` + +When set to `1`, skip system package search paths for FireboltTransport. + +Use this to guarantee host-independent behavior. + +### `COV_FORCE_BOOTSTRAP_TRANSPORT` + +When set to `1`, do not reuse an already-discovered transport package. Force bootstrap flow. + +### `COV_FORCE_RELEASE_TRANSPORT` + +When set to `1`, bypass sibling repo bootstrap and force release tarball bootstrap. + +## CMake Root Hinting + +In addition to script bootstrap, CMake supports explicit root hinting: + +- `FIREBOLT_TRANSPORT_ROOT` (project-level convenience variable) +- `FireboltTransport_ROOT` (package-native CMake variable) + +Either can point to an install prefix containing `lib/cmake/FireboltTransport`. + +## CI Recommendations + +Use strict mode for reproducibility: + +```bash +COV_FORCE_BOOTSTRAP_TRANSPORT=1 \ +COV_SKIP_SYSTEM_TRANSPORT=1 \ +COV_FORCE_RELEASE_TRANSPORT=1 \ +./cov_build.sh +``` + +This prevents accidental coupling to preinstalled host packages. + +## Troubleshooting + +### Missing `.transport.version` + +Symptom: bootstrap fails before download. + +Fix: ensure `.transport.version` exists and contains a non-empty version. + +### Release download failure + +Symptom: both release URL patterns fail. + +Fix: verify pinned version exists in `rdkcentral/firebolt-cpp-transport` releases and that node has outbound network access. + +### Version mismatch shown during configure + +Symptom: `installed version` differs from `expected`. + +Fix: use strict mode and a clean `COV_DEPS_PREFIX`, or set `COV_FORCE_RELEASE_TRANSPORT=1` to rebuild from pinned release. + +## Artifacts + +When release bootstrap is used, expected local artifacts include: + +- `.cov-deps/src/firebolt-cpp-transport-/` +- `.cov-deps/lib/cmake/FireboltTransport/FireboltTransportConfig.cmake` +- `.cov-deps/lib/libFireboltTransport.so*` diff --git a/Makefile.act b/Makefile.act new file mode 100644 index 0000000..382a69c --- /dev/null +++ b/Makefile.act @@ -0,0 +1,26 @@ +ACT ?= act +WORKFLOW ?= .github/workflows/native_full_build.yml +JOB ?= native-build + +ACT_CONTAINER_IMAGE ?= ghcr.io/rdkcentral/docker-rdk-ci:latest +ACT_SKIP_BUILD_DEPS ?= false +ACT_STRICT_TRANSPORT_BOOTSTRAP ?= true +ACT_FORCE_RELEASE_TRANSPORT ?= true + +.PHONY: act-list act-native act-native-fast + +act-list: + $(ACT) -l + +act-native: + $(ACT) workflow_dispatch \ + -W $(WORKFLOW) \ + -j $(JOB) \ + --input skip_build_dependencies=$(ACT_SKIP_BUILD_DEPS) \ + --input strict_transport_bootstrap=$(ACT_STRICT_TRANSPORT_BOOTSTRAP) \ + --input force_release_transport=$(ACT_FORCE_RELEASE_TRANSPORT) \ + --input container_image=$(ACT_CONTAINER_IMAGE) + +# Faster local loop if your container image already has dependencies (including FireboltTransport). +act-native-fast: + $(MAKE) act-native ACT_SKIP_BUILD_DEPS=true ACT_STRICT_TRANSPORT_BOOTSTRAP=false ACT_FORCE_RELEASE_TRANSPORT=false diff --git a/README.md b/README.md index b6289a6..085c0dc 100644 --- a/README.md +++ b/README.md @@ -30,3 +30,9 @@ Examples: - `./lint.sh --tidy-only` - `./lint.sh --tidy-only --fix` - `./lint.sh --cppcheck-only` + +## Coverity + +For Coverity build and fully unattended dependency bootstrap instructions, see: + +- [COVERITY.md](COVERITY.md) diff --git a/build_dependencies.sh b/build_dependencies.sh new file mode 100755 index 0000000..8ded0d8 --- /dev/null +++ b/build_dependencies.sh @@ -0,0 +1,128 @@ +#!/usr/bin/env bash + +# Copyright 2026 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 + +# build_dependencies.sh — install all build dependencies for firebolt-cpp-client +# +# Installs to the system prefix (/usr/local) and is intentionally idempotent: +# running it multiple times is safe. +# +# Note: this installs the core C/C++ build dependencies used by this repo's CI +# image (see .github/Dockerfile), but it does not attempt to reproduce every +# Dockerfile step (for example, it does not install Node/nvm or FireboltTransport). +# +# +# Usage: sudo ./build_dependencies.sh +# (run as root, or with sudo, from any directory) +set -euo pipefail +set -x + +if [[ ${EUID:-$(id -u)} -ne 0 ]]; then + echo "This script must be run as root (try: sudo ./build_dependencies.sh)" >&2 + exit 1 +fi + +DEPS_GOOGLETEST_V="1.15.2" +DEPS_NLOHMANN_JSON_V="3.11.3" +DEPS_JSON_SCHEMA_VALIDATOR_V="2.3.0" +DEPS_WEBSOCKETPP_V="0.8.2" + +# --------------------------------------------------------------------------- +# 1. System packages +# --------------------------------------------------------------------------- +apt-get update +apt-get install -y --no-install-recommends --fix-missing \ + build-essential ca-certificates \ + cmake pkg-config clang-format \ + libboost-all-dev \ + libcurl4-openssl-dev \ + curl wget git jq netcat-openbsd \ + python3-pip + +if python3 -m pip help install | grep -q -- '--break-system-packages'; then + python3 -m pip install --break-system-packages gcovr +else + python3 -m pip install gcovr +fi + +# --------------------------------------------------------------------------- +# 2. googletest +# --------------------------------------------------------------------------- +WORK_DIR="$(mktemp -d)" +trap 'rm -rf "$WORK_DIR"' EXIT + +download_archive() { + local url="$1" + local archive_path="$2" + + curl -fsSL --retry 5 --retry-delay 1 --retry-connrefused \ + --output "$archive_path" \ + "$url" +} + +dir="googletest-${DEPS_GOOGLETEST_V}" +archive="$WORK_DIR/${dir}.tar.gz" +download_archive "https://github.com/google/googletest/releases/download/v${DEPS_GOOGLETEST_V}/${dir}.tar.gz" "$archive" +tar xzf "$archive" -C "$WORK_DIR" +cmake -B "$WORK_DIR/build/${dir}" \ + -DCMAKE_BUILD_TYPE=Release \ + -DBUILD_SHARED_LIBS=ON \ + "$WORK_DIR/${dir}" +cmake --build "$WORK_DIR/build/${dir}" --target install + +# --------------------------------------------------------------------------- +# 3. nlohmann/json +# --------------------------------------------------------------------------- +dir="nlohmann-json-${DEPS_NLOHMANN_JSON_V}" +git clone --depth 1 --branch "v${DEPS_NLOHMANN_JSON_V}" \ + "https://github.com/nlohmann/json" "$WORK_DIR/${dir}" +cmake -B "$WORK_DIR/build/${dir}" \ + -DCMAKE_BUILD_TYPE=Release \ + -DBUILD_SHARED_LIBS=ON \ + -DJSON_BuildTests=OFF \ + "$WORK_DIR/${dir}" +cmake --build "$WORK_DIR/build/${dir}" --target install + +# --------------------------------------------------------------------------- +# 4. json-schema-validator +# --------------------------------------------------------------------------- +dir="json-schema-validator-${DEPS_JSON_SCHEMA_VALIDATOR_V}" +archive="$WORK_DIR/${dir}.tar.gz" +download_archive "https://github.com/pboettch/json-schema-validator/archive/refs/tags/${DEPS_JSON_SCHEMA_VALIDATOR_V}.tar.gz" "$archive" +tar xzf "$archive" -C "$WORK_DIR" +cmake -B "$WORK_DIR/build/${dir}" \ + -DCMAKE_BUILD_TYPE=Release \ + -DBUILD_SHARED_LIBS=ON \ + -DJSON_VALIDATOR_BUILD_TESTS=OFF \ + -DJSON_VALIDATOR_BUILD_EXAMPLES=OFF \ + "$WORK_DIR/${dir}" +cmake --build "$WORK_DIR/build/${dir}" --target install + +# --------------------------------------------------------------------------- +# 5. websocketpp (header-only, cmake install registers package config) +# --------------------------------------------------------------------------- +dir="websocketpp-${DEPS_WEBSOCKETPP_V}" +archive="$WORK_DIR/${dir}.tar.gz" +download_archive "https://github.com/zaphoyd/websocketpp/archive/refs/tags/${DEPS_WEBSOCKETPP_V}.tar.gz" "$archive" +tar xzf "$archive" -C "$WORK_DIR" +cmake -B "$WORK_DIR/build/${dir}" \ + -DCMAKE_BUILD_TYPE=Release \ + -DBUILD_SHARED_LIBS=ON \ + -DBUILD_TESTS=OFF \ + -DBUILD_EXAMPLES=OFF \ + "$WORK_DIR/${dir}" +cmake --build "$WORK_DIR/build/${dir}" --target install diff --git a/cov_build.sh b/cov_build.sh new file mode 100755 index 0000000..7e70d6e --- /dev/null +++ b/cov_build.sh @@ -0,0 +1,194 @@ +#!/usr/bin/env bash + +# Copyright 2026 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 + +# cov_build.sh — configure and build firebolt-cpp-client +# +# Run from the repo root after build_dependencies.sh has prepared the +# environment. Produces a Debug build with tests enabled so that +# Coverity can intercept the full compilation including test code. +# +# Usage: ./cov_build.sh +set -euo pipefail +set -x + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +GITHUB_WORKSPACE="${GITHUB_WORKSPACE:-${SCRIPT_DIR}}" +cd "${GITHUB_WORKSPACE}" + +find_transport_config_dir() { + local candidates=() + local candidate + local skip_system="${COV_SKIP_SYSTEM_TRANSPORT:-0}" + + if [[ -n "${FireboltTransport_DIR:-}" ]]; then + candidates+=("${FireboltTransport_DIR}") + fi + + if [[ -n "${COV_DEPS_PREFIX:-}" ]]; then + candidates+=( + "${COV_DEPS_PREFIX}/lib/cmake/FireboltTransport" + "${COV_DEPS_PREFIX}/lib64/cmake/FireboltTransport" + ) + fi + + if [[ "${skip_system}" != "1" ]]; then + candidates+=( + "/usr/local/lib/cmake/FireboltTransport" + "/usr/local/lib64/cmake/FireboltTransport" + "/usr/lib/cmake/FireboltTransport" + "/usr/lib64/cmake/FireboltTransport" + "/usr/lib/x86_64-linux-gnu/cmake/FireboltTransport" + ) + fi + + for candidate in "${candidates[@]}"; do + if [[ -f "${candidate}/FireboltTransportConfig.cmake" ]]; then + echo "${candidate}" + return 0 + fi + done + + return 1 +} + +bootstrap_transport_if_missing() { + local transport_repo="${GITHUB_WORKSPACE}/../firebolt-cpp-transport" + local transport_build_dir="${transport_repo}/build-cov" + local transport_src_dir="" + local transport_version="" + local release_dir="" + local release_archive="" + local release_url="" + local release_url_nov="" + local release_url_v="" + local transport_config_dir="" + local transport_cmake_args=() + local force_bootstrap="${COV_FORCE_BOOTSTRAP_TRANSPORT:-0}" + local force_release="${COV_FORCE_RELEASE_TRANSPORT:-0}" + + COV_DEPS_PREFIX="${COV_DEPS_PREFIX:-${GITHUB_WORKSPACE}/.cov-deps}" + transport_src_dir="${COV_DEPS_PREFIX}/src" + + if [[ -f "${GITHUB_WORKSPACE}/.transport.version" ]]; then + transport_version="$(tr -d '[:space:]' < "${GITHUB_WORKSPACE}/.transport.version")" + fi + + if [[ "${force_bootstrap}" != "1" ]]; then + if transport_config_dir="$(find_transport_config_dir)"; then + echo "Using FireboltTransport from ${transport_config_dir}" + FireboltTransport_DIR="${transport_config_dir}" + return 0 + fi + fi + + if [[ "${force_release}" == "1" ]]; then + transport_repo="" + fi + + if [[ ! -d "${transport_repo}" ]]; then + if [[ ! -f "${GITHUB_WORKSPACE}/.transport.version" ]]; then + echo "FireboltTransport not found and no .transport.version file is available." >&2 + echo "Provide FireboltTransport_DIR, add sibling firebolt-cpp-transport, or add .transport.version." >&2 + return 1 + fi + + if [[ -z "${transport_version}" ]]; then + echo ".transport.version is empty; cannot resolve transport release." >&2 + return 1 + fi + + release_dir="firebolt-cpp-transport-${transport_version}" + release_archive="${transport_src_dir}/${release_dir}.tar.gz" + release_url_nov="https://github.com/rdkcentral/firebolt-cpp-transport/releases/download/v${transport_version}/${release_dir}.tar.gz" + release_url_v="https://github.com/rdkcentral/firebolt-cpp-transport/releases/download/v${transport_version}/firebolt-cpp-transport-v${transport_version}.tar.gz" + + mkdir -p "${transport_src_dir}" + + if [[ ! -f "${release_archive}" ]]; then + local tmp_archive="${release_archive}.tmp" + rm -f "${tmp_archive}" + if curl -fsSL --retry 5 --retry-delay 1 --retry-connrefused -o "${tmp_archive}" "${release_url_nov}"; then + release_url="${release_url_nov}" + elif curl -fsSL --retry 5 --retry-delay 1 --retry-connrefused -o "${tmp_archive}" "${release_url_v}"; then + release_url="${release_url_v}" + else + rm -f "${tmp_archive}" + echo "Failed to download FireboltTransport release for version ${transport_version}" >&2 + echo "Tried: ${release_url_nov}" >&2 + echo "Tried: ${release_url_v}" >&2 + return 1 + fi + mv -f "${tmp_archive}" "${release_archive}" + echo "Downloaded FireboltTransport release from ${release_url}" + fi + + release_dir="$(tar -tzf "${release_archive}" | sed -e 's|^\./||' | awk -F/ 'NF{print $1; exit}')" + if [[ -z "${release_dir}" ]]; then + echo "Transport archive appears to be empty: ${release_archive}" >&2 + return 1 + fi + + rm -rf "${transport_src_dir:?}/${release_dir}" + tar -xzf "${release_archive}" -C "${transport_src_dir}" + + transport_repo="${transport_src_dir}/${release_dir}" + transport_build_dir="${transport_repo}/build-cov" + + if [[ ! -d "${transport_repo}" ]]; then + echo "Transport source extraction failed: ${transport_repo} missing" >&2 + return 1 + fi + fi + + if [[ -n "${transport_version}" ]]; then + transport_cmake_args+=("-DFIREBOLT_TRANSPORT_VERSION=${transport_version}") + fi + + cmake -S "${transport_repo}" -B "${transport_build_dir}" \ + -DCMAKE_BUILD_TYPE=Debug \ + -DCMAKE_INSTALL_PREFIX="${COV_DEPS_PREFIX}" \ + "${transport_cmake_args[@]}" + + cmake --build "${transport_build_dir}" --parallel + cmake --install "${transport_build_dir}" + + if [[ "${force_bootstrap}" == "1" ]]; then + if transport_config_dir="$(FireboltTransport_DIR="" find_transport_config_dir)"; then + FireboltTransport_DIR="${transport_config_dir}" + echo "Bootstrapped FireboltTransport at ${FireboltTransport_DIR}" + return 0 + fi + elif transport_config_dir="$(find_transport_config_dir)"; then + FireboltTransport_DIR="${transport_config_dir}" + echo "Bootstrapped FireboltTransport at ${FireboltTransport_DIR}" + return 0 + fi + + echo "FireboltTransport bootstrap completed but config was not found." >&2 + return 1 +} + +bootstrap_transport_if_missing + +cmake -B build-dev -S . \ + -UGTest_DIR \ + -DCMAKE_BUILD_TYPE=Debug \ + -DFireboltTransport_DIR="${FireboltTransport_DIR}" \ + -DENABLE_TESTS=ON + +cmake --build build-dev --parallel diff --git a/coverity_local.sh b/coverity_local.sh new file mode 100755 index 0000000..712fef6 --- /dev/null +++ b/coverity_local.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +# Copyright 2026 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 + +# coverity_local.sh - local wrapper for Coverity build prep. +# +# Delegates to cov_build.sh so there is a single source of truth for +# CMake configuration flags and build behavior. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +exec "${SCRIPT_DIR}/cov_build.sh" "$@" diff --git a/docs/openrpc/openrpc/discovery.json b/docs/openrpc/openrpc/discovery.json index 8862606..6f80f61 100644 --- a/docs/openrpc/openrpc/discovery.json +++ b/docs/openrpc/openrpc/discovery.json @@ -122,6 +122,124 @@ } } ] + }, + { + "name": "watchedV2", + "summary": "Notify the platform that content was partially or completely watched, returns whether the notification was accepted", + "tags": [ + { + "name": "polymorphic-reducer" + }, + { + "name": "capabilities", + "x-uses": [ + "xrn:firebolt:capability:discovery:watched" + ] + } + ], + "params": [ + { + "name": "entityId", + "required": true, + "schema": { + "type": "string" + }, + "summary": "The entity Id of the watched content." + }, + { + "name": "progress", + "summary": "How much of the content has been watched (percentage as (0-0.999) for VOD, number of seconds for live)", + "schema": { + "type": "number", + "minimum": 0 + } + }, + { + "name": "completed", + "summary": "Whether or not this viewing is considered \"complete,\" per the app's definition thereof", + "schema": { + "type": "boolean" + } + }, + { + "name": "watchedOn", + "summary": "Date/Time the content was watched, ISO 8601 Date/Time", + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "name": "agePolicy", + "description": "The age policy associated with the watch event. The age policy describes the age groups to which content may be directed.", + "schema": { + "$ref": "https://meta.comcast.com/firebolt/policies#/definitions/AgePolicy" + } + } + ], + "result": { + "name": "result", + "summary": "Whether the platform accepted the watched notification", + "schema": { + "type": "boolean" + } + }, + "examples": [ + { + "name": "Notify the platform of watched content (v2)", + "params": [ + { + "name": "entityId", + "value": "partner.com/entity/123" + }, + { + "name": "progress", + "value": 0.95 + }, + { + "name": "completed", + "value": true + }, + { + "name": "watchedOn", + "value": "2021-04-23T18:25:43.511Z" + } + ], + "result": { + "name": "result", + "value": true + } + }, + { + "name": "Notify the platform that child-directed content was watched (v2)", + "params": [ + { + "name": "entityId", + "value": "partner.com/entity/123" + }, + { + "name": "progress", + "value": 0.95 + }, + { + "name": "completed", + "value": true + }, + { + "name": "watchedOn", + "value": "2021-04-23T18:25:43.511Z" + }, + { + "name": "agePolicy", + "value": "app:child" + } + ], + "result": { + "name": "result", + "value": true + } + } + ] } ] } diff --git a/docs/openrpc/the-spec/firebolt-open-rpc.json b/docs/openrpc/the-spec/firebolt-open-rpc.json index 9ad9084..1315678 100644 --- a/docs/openrpc/the-spec/firebolt-open-rpc.json +++ b/docs/openrpc/the-spec/firebolt-open-rpc.json @@ -644,6 +644,124 @@ } ] }, + { + "name": "Discovery.watchedV2", + "summary": "Notify the platform that content was partially or completely watched, returns whether the notification was accepted", + "tags": [ + { + "name": "polymorphic-reducer" + }, + { + "name": "capabilities", + "x-uses": [ + "xrn:firebolt:capability:discovery:watched" + ] + } + ], + "params": [ + { + "name": "entityId", + "required": true, + "schema": { + "type": "string" + }, + "summary": "The entity Id of the watched content." + }, + { + "name": "progress", + "summary": "How much of the content has been watched (percentage as (0-0.999) for VOD, number of seconds for live)", + "schema": { + "type": "number", + "minimum": 0 + } + }, + { + "name": "completed", + "summary": "Whether or not this viewing is considered \"complete,\" per the app's definition thereof", + "schema": { + "type": "boolean" + } + }, + { + "name": "watchedOn", + "summary": "Date/Time the content was watched, ISO 8601 Date/Time", + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "name": "agePolicy", + "description": "The age policy associated with the watch event. The age policy describes the age groups to which content may be directed.", + "schema": { + "$ref": "#/x-schemas/Policies/AgePolicy" + } + } + ], + "result": { + "name": "result", + "summary": "Whether the platform accepted the watched notification", + "schema": { + "type": "boolean" + } + }, + "examples": [ + { + "name": "Notify the platform of watched content (v2)", + "params": [ + { + "name": "entityId", + "value": "partner.com/entity/123" + }, + { + "name": "progress", + "value": 0.95 + }, + { + "name": "completed", + "value": true + }, + { + "name": "watchedOn", + "value": "2021-04-23T18:25:43.511Z" + } + ], + "result": { + "name": "result", + "value": true + } + }, + { + "name": "Notify the platform that child-directed content was watched (v2)", + "params": [ + { + "name": "entityId", + "value": "partner.com/entity/123" + }, + { + "name": "progress", + "value": 0.95 + }, + { + "name": "completed", + "value": true + }, + { + "name": "watchedOn", + "value": "2021-04-23T18:25:43.511Z" + }, + { + "name": "agePolicy", + "value": "app:child" + } + ], + "result": { + "name": "result", + "value": true + } + } + ] + }, { "name": "Display.edid", "summary": "Returns the EDID (and extensions) of the connected or integral display, as a Base64 encoded string", diff --git a/include/firebolt/discovery.h b/include/firebolt/discovery.h index 885380e..894a768 100644 --- a/include/firebolt/discovery.h +++ b/include/firebolt/discovery.h @@ -44,5 +44,23 @@ class IDiscovery virtual Result watched(const std::string& entityId, std::optional progress, std::optional completed, std::optional watchedOn, std::optional agePolicy) const = 0; + + /** + * @brief Notify the platform that content was partially or completely watched, returns whether the notification + * was accepted + * + * @param[in] entityId : The entity Id of the watched content + * @param[in] progress : How much of the content has been watched (percentage as (0-0.999) for VOD, number of + * seconds for live) + * @param[in] completed : Whether or not this viewing is considered "complete" per the app's definition thereof + * @param[in] watchedOn : The ISO 8601 timestamp of when the content was watched + * @param[in] agePolicy : The age policy associated with the watch event. The age policy describes the age groups + * to which content may be directed + * + * @retval Whether the platform accepted the watched notification, or an error + */ + virtual Result watchedV2(const std::string& entityId, std::optional progress, + std::optional completed, std::optional watchedOn, + std::optional agePolicy) const = 0; }; } // namespace Firebolt::Discovery diff --git a/src/discovery_impl.cpp b/src/discovery_impl.cpp index e653777..3504399 100644 --- a/src/discovery_impl.cpp +++ b/src/discovery_impl.cpp @@ -52,4 +52,30 @@ Result DiscoveryImpl::watched(const std::string& entityId, std::optional DiscoveryImpl::watchedV2(const std::string& entityId, std::optional progress, + std::optional completed, std::optional watchedOn, + std::optional agePolicy) const +{ + nlohmann::json parameters; + parameters["entityId"] = entityId; + if (progress) + { + parameters["progress"] = *progress; + } + if (completed) + { + parameters["completed"] = *completed; + } + if (watchedOn) + { + parameters["watchedOn"] = *watchedOn; + } + if (agePolicy) + { + parameters["agePolicy"] = Firebolt::JSON::toString(Firebolt::JsonData::AgePolicyEnum, *agePolicy); + } + + return helper_.get("Discovery.watchedV2", parameters); +} } // namespace Firebolt::Discovery diff --git a/src/discovery_impl.h b/src/discovery_impl.h index 34e12f1..96b2d70 100644 --- a/src/discovery_impl.h +++ b/src/discovery_impl.h @@ -37,6 +37,10 @@ class DiscoveryImpl : public IDiscovery std::optional watchedOn, std::optional agePolicy) const override; + Result watchedV2(const std::string& entityId, std::optional progress, std::optional completed, + std::optional watchedOn, + std::optional agePolicy) const override; + private: Firebolt::Helpers::IHelper& helper_; }; diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index a27713b..bc6eea3 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -15,7 +15,39 @@ # SPDX-License-Identifier: Apache-2.0 find_package(Curl REQUIRED ) -find_package(GTest CONFIG REQUIRED) + +set(FIREBOLT_GTEST_PREFIXES + /usr/local + $ENV{HOME}/.local +) + +set(FIREBOLT_GTEST_CMAKE_SUBDIRS + lib/cmake/GTest + lib64/cmake/GTest + share/cmake/GTest +) + +set(FIREBOLT_GTEST_LOCAL_GTEST_DIR "") +if(NOT SYSROOT_PATH AND NOT CMAKE_CROSSCOMPILING) + foreach(prefix IN LISTS FIREBOLT_GTEST_PREFIXES) + foreach(subdir IN LISTS FIREBOLT_GTEST_CMAKE_SUBDIRS) + if(EXISTS "${prefix}/include/gtest/gtest.h" AND EXISTS "${prefix}/${subdir}/GTestConfig.cmake") + set(FIREBOLT_GTEST_LOCAL_GTEST_DIR "${prefix}/${subdir}") + break() + endif() + endforeach() + if(FIREBOLT_GTEST_LOCAL_GTEST_DIR) + break() + endif() + endforeach() +endif() + +if(FIREBOLT_GTEST_LOCAL_GTEST_DIR AND NOT DEFINED GTest_DIR AND NOT SYSROOT_PATH) + find_package(GTest CONFIG REQUIRED PATHS "${FIREBOLT_GTEST_LOCAL_GTEST_DIR}" NO_DEFAULT_PATH) +else() + find_package(GTest CONFIG REQUIRED) +endif() + find_package(nlohmann_json_schema_validator CONFIG REQUIRED) include(GoogleTest) diff --git a/test/api_test_app/apis/discoveryDemo.cpp b/test/api_test_app/apis/discoveryDemo.cpp index 5e0b9d6..98b199a 100644 --- a/test/api_test_app/apis/discoveryDemo.cpp +++ b/test/api_test_app/apis/discoveryDemo.cpp @@ -33,6 +33,7 @@ DiscoveryDemo::DiscoveryDemo() : DemoBase("Discovery") { methods_.push_back("Discovery.watched"); + methods_.push_back("Discovery.watchedV2"); } void DiscoveryDemo::runOption(const std::string& method) @@ -64,4 +65,29 @@ void DiscoveryDemo::runOption(const std::string& method) std::cout << "Discovery.watched: Success" << std::endl; } } + else if (method == "Discovery.watchedV2") + { + std::string entityId = paramFromConsole("entityId", "exampleEntityId"); + std::optional progress = 0.5; + try + { + progress = std::stod( + paramFromConsole("progress (percentage as (0-0.999) for VOD, number of seconds for live)", "0.5")); + } + catch (const std::exception&) + { + } + std::optional completed = paramFromConsole("completed (true/false)", "false") == "true"; + std::string watchedOn = paramFromConsole("watchedOn (ISO 8601 timestamp)", "2024-01-01T00:00:00Z"); + + std::optional agePolicyOpt = + chooseEnumFromList(Firebolt::JsonData::AgePolicyEnum, "Choose an age policy for the watch event:"); + + auto r = Firebolt::IFireboltAccessor::Instance().DiscoveryInterface().watchedV2(entityId, progress, completed, + watchedOn, agePolicyOpt); + if (succeed(r)) + { + std::cout << "Discovery.watchedV2: " << (*r ? "true" : "false") << std::endl; + } + } } diff --git a/test/component/discoveryTest.cpp b/test/component/discoveryTest.cpp index c1cfcf1..e0ae6f5 100644 --- a/test/component/discoveryTest.cpp +++ b/test/component/discoveryTest.cpp @@ -18,10 +18,13 @@ #include "firebolt/discovery.h" #include "firebolt/firebolt.h" +#include "json_engine.h" #include class DiscoveryCTest : public ::testing::Test { +protected: + JsonEngine jsonEngine; }; TEST_F(DiscoveryCTest, Watched) @@ -31,3 +34,13 @@ TEST_F(DiscoveryCTest, Watched) Firebolt::AgePolicy::ADULT); ASSERT_TRUE(result) << "Failed to call watched"; } + +TEST_F(DiscoveryCTest, WatchedV2) +{ + auto expectedValue = jsonEngine.get_value("Discovery.watchedV2"); + auto result = Firebolt::IFireboltAccessor::Instance().DiscoveryInterface().watchedV2("entity123", 0.75f, true, + "2024-10-01T12:00:00Z", + Firebolt::AgePolicy::ADULT); + ASSERT_TRUE(result) << "Failed to call watchedV2"; + EXPECT_EQ(*result, expectedValue.get()); +} diff --git a/test/unit/discoveryTest.cpp b/test/unit/discoveryTest.cpp index 9b7b1b1..46fb672 100644 --- a/test/unit/discoveryTest.cpp +++ b/test/unit/discoveryTest.cpp @@ -70,3 +70,42 @@ TEST_F(DiscoveryUTest, watched_payload) auto result = discoveryImpl_.watched(entityId, progress, completed, watchedOn, agePolicy); ASSERT_TRUE(result) << "Error on watched"; } + +TEST_F(DiscoveryUTest, watchedV2) +{ + mock("Discovery.watchedV2"); + std::string entityId = "content123"; + std::optional progress = 0.75f; + std::optional completed = true; + std::optional watchedOn = "2024-06-01T12:00:00Z"; + std::optional agePolicy = Firebolt::AgePolicy::ADULT; + auto result = discoveryImpl_.watchedV2(entityId, progress, completed, watchedOn, agePolicy); + ASSERT_TRUE(result) << "Error on watchedV2"; + EXPECT_TRUE(*result); +} + +TEST_F(DiscoveryUTest, watchedV2_payload) +{ + nlohmann::json expected; + expected["entityId"] = "content123"; + expected["progress"] = 0.75f; + expected["completed"] = true; + expected["watchedOn"] = "2024-06-01T12:00:00Z"; + expected["agePolicy"] = "app:adult"; + EXPECT_CALL(mockHelper, getJson("Discovery.watchedV2", _)) + .WillOnce(Invoke( + [&](const std::string& /* methodName */, const nlohmann::json& parameters) + { + EXPECT_EQ(parameters, expected) << "Parameters do not match expected payload: " << expected.dump() + << " but got: " << parameters.dump(); + return Firebolt::Result{nlohmann::json(true)}; + })); + std::string entityId = "content123"; + std::optional progress = 0.75f; + std::optional completed = true; + std::optional watchedOn = "2024-06-01T12:00:00Z"; + std::optional agePolicy = Firebolt::AgePolicy::ADULT; + auto result = discoveryImpl_.watchedV2(entityId, progress, completed, watchedOn, agePolicy); + ASSERT_TRUE(result) << "Error on watchedV2"; + EXPECT_TRUE(*result); +}