diff --git a/README.md b/README.md index 5af09fa..b6289a6 100644 --- a/README.md +++ b/README.md @@ -6,3 +6,27 @@ Implementation of Firebolt C++ Client. For usage instructions of the API test application, see: - [test/api_test_app/README.md](test/api_test_app/README.md) + +## Test Runner + +Use `run-all-test.sh` to build and run unit/component tests locally. + +Examples: + +- `./run-all-test.sh` +- `./run-unit-tests.sh --unit-filter "ActionsUTest.*"` +- `./run-component-tests.sh --component-filter "*Localization*"` + +For the device websocket tunnel, use `setup-device-tunnel.sh`. +Before running it, export `DEVICE_SSH_USER`, `DEVICE_SSH_HOST`, and `DEVICE_SSH_PORT`. + +## Lint + +Use `lint.sh` to run local static analysis for C/C++ sources. + +Examples: + +- `./lint.sh` +- `./lint.sh --tidy-only` +- `./lint.sh --tidy-only --fix` +- `./lint.sh --cppcheck-only` diff --git a/build.sh b/build.sh index 3b1f1c7..6dac83d 100755 --- a/build.sh +++ b/build.sh @@ -51,8 +51,21 @@ while [[ ! -z $1 ]]; do esac; shift done -[[ ! -z $SYSROOT_PATH ]] || { echo "SYSROOT_PATH not set" >/dev/stderr; exit 1; } -[[ -e $SYSROOT_PATH ]] || { echo "SYSROOT_PATH not exist ($SYSROOT_PATH)" >/dev/stderr; exit 1; } +if [[ -n "$SYSROOT_PATH" ]]; then + [[ -e "$SYSROOT_PATH" ]] || { echo "SYSROOT_PATH not exist ($SYSROOT_PATH)" >/dev/stderr; exit 1; } + params+=" -DSYSROOT_PATH=$SYSROOT_PATH" +else + if $do_install; then + echo "--install requires --sysroot to be set; refusing to install without SYSROOT_PATH" >/dev/stderr + exit 1 + fi + echo "SYSROOT_PATH not set; building without SYSROOT_PATH override" +fi + +if [[ "$do_install" == true && "$bdir" == "build" && -z "${SYSROOT_PATH:-}" ]]; then + echo "Refusing --install without --sysroot to avoid host install into /usr" >&2 + exit 1 +fi $cleanFirst && rm -rf $bdir @@ -61,7 +74,6 @@ if [[ ! -e "$bdir" || -n "$@" ]]; then command -v ccache >/dev/null 2>&1 && params+=" -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache" cmake -B $bdir \ -DCMAKE_BUILD_TYPE=$buildType \ - -DSYSROOT_PATH=$SYSROOT_PATH \ $params \ "$@" || exit $? fi diff --git a/include/firebolt/actions.h b/include/firebolt/actions.h new file mode 100644 index 0000000..9c39198 --- /dev/null +++ b/include/firebolt/actions.h @@ -0,0 +1,56 @@ +/** + * 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 + */ +// +// ============================================================================ +// AUTO-GENERATED by fb-gen — DO NOT EDIT +// ============================================================================ +#ifndef FIREBOLT_ACTIONS_H +#define FIREBOLT_ACTIONS_H + +#include +#include +#include +#include +#include +#include +#include + +namespace Firebolt::Actions +{ + +class IActions +{ +public: + virtual ~IActions() = default; + + virtual Result intent(const std::string& intent) const = 0; + + virtual Result subscribeOnIntent(std::function&& notification) = 0; + virtual Result subscribeOnIntentChanged(std::function&& notification) + { + return subscribeOnIntent(std::move(notification)); + } + + virtual Result unsubscribe(SubscriptionId id) = 0; + virtual void unsubscribeAll() = 0; + +}; // class IActions + +} // namespace Firebolt::Actions + +#endif // FIREBOLT_ACTIONS_H diff --git a/include/firebolt/firebolt.h b/include/firebolt/firebolt.h index f51a7fd..d889ec0 100644 --- a/include/firebolt/firebolt.h +++ b/include/firebolt/firebolt.h @@ -19,6 +19,7 @@ #pragma once #include "firebolt/accessibility.h" +#include "firebolt/actions.h" #include "firebolt/advertising.h" #include "firebolt/client_export.h" #include "firebolt/device.h" @@ -161,5 +162,12 @@ class FIREBOLTCLIENT_EXPORT IFireboltAccessor * @return Reference to TextToSpeech interface */ virtual TextToSpeech::ITextToSpeech& TextToSpeechInterface() = 0; + + /** + * @brief Returns instance of Actions interface + * + * @return Reference to Actions interface + */ + virtual Actions::IActions& ActionsInterface() = 0; }; } // namespace Firebolt diff --git a/lint.sh b/lint.sh new file mode 100755 index 0000000..552384d --- /dev/null +++ b/lint.sh @@ -0,0 +1,303 @@ +#!/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 + +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +BUILD_DIR="build-dev" +NO_BUILD=false +CLEAN=false +RUN_CLANG_TIDY=true +RUN_CPPCHECK=true +RUN_CLANG_FORMAT=true +APPLY_FIXES=false +FORMAT_FIX=false +CLANG_TIDY_PATHS=(src include test/unit test/component) +CLANG_FORMAT_PATHS=(src include test) + +usage() { + cat < Build directory containing compile_commands.json (default: build-dev) + --tidy-path

Add path for clang-tidy scan (repeatable) + --format-path

Add path for clang-format scan (repeatable) + --fix Apply clang-tidy fix-its (clang-tidy only) + --format-fix Apply clang-format fixes in-place + --format-only Run clang-format only + --no-format Skip clang-format checks + --tidy-only Run clang-tidy only + --cppcheck-only Run cppcheck only + --help Show this help + +Examples: + ./lint.sh + ./lint.sh --tidy-only + ./lint.sh --tidy-only --fix + ./lint.sh --format-only + ./lint.sh --format-fix + ./lint.sh --tidy-path test/api_test_app + ./lint.sh --format-path include/firebolt + ./lint.sh --no-build --build-dir build-dev +EOF +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --clean) + CLEAN=true + ;; + --no-build) + NO_BUILD=true + ;; + --build-dir) + if [[ $# -lt 2 || -z "${2:-}" || "$2" == --* ]]; then + echo "Missing value for --build-dir" >&2 + usage + exit 1 + fi + BUILD_DIR="${2:-}" + shift + ;; + --tidy-path) + if [[ $# -lt 2 || -z "${2:-}" || "$2" == --* ]]; then + echo "Missing value for --tidy-path" >&2 + usage + exit 1 + fi + CLANG_TIDY_PATHS+=("${2:-}") + shift + ;; + --format-path) + if [[ $# -lt 2 || -z "${2:-}" || "$2" == --* ]]; then + echo "Missing value for --format-path" >&2 + usage + exit 1 + fi + CLANG_FORMAT_PATHS+=("${2:-}") + shift + ;; + --fix) + APPLY_FIXES=true + ;; + --format-fix) + FORMAT_FIX=true + RUN_CLANG_FORMAT=true + ;; + --format-only) + RUN_CLANG_FORMAT=true + RUN_CLANG_TIDY=false + RUN_CPPCHECK=false + ;; + --no-format) + RUN_CLANG_FORMAT=false + ;; + --tidy-only) + RUN_CLANG_FORMAT=false + RUN_CLANG_TIDY=true + RUN_CPPCHECK=false + ;; + --cppcheck-only) + RUN_CLANG_FORMAT=false + RUN_CLANG_TIDY=false + RUN_CPPCHECK=true + ;; + --help|-h) + usage + exit 0 + ;; + *) + echo "Unknown option: $1" >&2 + usage + exit 1 + ;; + esac + shift +done + +cd "$ROOT_DIR" + +if [[ "$RUN_CLANG_TIDY" == true && "$NO_BUILD" == false && "$BUILD_DIR" != "build-dev" ]]; then + echo "--build-dir is only supported with --no-build (build step always uses build-dev)." >&2 + exit 1 +fi + +if [[ "$RUN_CLANG_FORMAT" == false && "$RUN_CLANG_TIDY" == false && "$RUN_CPPCHECK" == false ]]; then + echo "Nothing to run: clang-format, clang-tidy, and cppcheck are all disabled." >&2 + exit 1 +fi + +if [[ "$APPLY_FIXES" == true && "$RUN_CLANG_TIDY" == false ]]; then + echo "--fix requires clang-tidy to be enabled (remove --cppcheck-only)." >&2 + exit 1 +fi + +if [[ "$FORMAT_FIX" == true && "$RUN_CLANG_FORMAT" == false ]]; then + echo "--format-fix requires clang-format to be enabled (remove --no-format)." >&2 + exit 1 +fi + +if [[ "$RUN_CLANG_TIDY" == true ]] && ! command -v clang-tidy >/dev/null 2>&1; then + echo "clang-tidy not found. Install it (e.g. apt install clang-tidy)." >&2 + exit 1 +fi + +if [[ "$RUN_CPPCHECK" == true ]] && ! command -v cppcheck >/dev/null 2>&1; then + echo "cppcheck not found. Install it (e.g. apt install cppcheck)." >&2 + exit 1 +fi + +if [[ "$RUN_CLANG_FORMAT" == true ]] && ! command -v clang-format >/dev/null 2>&1; then + echo "clang-format not found. Install it (e.g. apt install clang-format)." >&2 + exit 1 +fi + +if [[ "$CLEAN" == true ]]; then + rm -rf "$BUILD_DIR" +fi + +if [[ "$NO_BUILD" == false && "$RUN_CLANG_TIDY" == true ]]; then + ./build.sh +tests +fi + +if [[ "$RUN_CLANG_TIDY" == true && ! -f "$BUILD_DIR/compile_commands.json" ]]; then + echo "Missing $BUILD_DIR/compile_commands.json. Run ./build.sh +tests first." >&2 + exit 1 +fi + +if [[ "$RUN_CLANG_FORMAT" == true ]]; then + if [[ "$FORMAT_FIX" == true ]]; then + echo "[lint] Running clang-format with fixes enabled" + else + echo "[lint] Running clang-format check" + fi + + format_paths=() + for p in "${CLANG_FORMAT_PATHS[@]}"; do + if [[ -e "$p" ]]; then + format_paths+=("$p") + fi + done + + if [[ ${#format_paths[@]} -eq 0 ]]; then + echo "No valid clang-format paths found." >&2 + exit 1 + fi + + mapfile -t format_files < <( + find "${format_paths[@]}" -type f \( -name "*.h" -o -name "*.hh" -o -name "*.hpp" -o -name "*.hxx" -o -name "*.c" -o -name "*.cc" -o -name "*.cpp" -o -name "*.cxx" \) | sort + ) + + if [[ ${#format_files[@]} -eq 0 ]]; then + echo "No C/C++ files found for clang-format." >&2 + exit 1 + fi + + clang_format_failed=0 + total_format_files=${#format_files[@]} + format_index=0 + for f in "${format_files[@]}"; do + format_index=$((format_index + 1)) + echo "[lint][clang-format] ${format_index}/${total_format_files}: $f" + if [[ "$FORMAT_FIX" == true ]]; then + clang-format -i "$f" + else + if ! clang-format --dry-run --Werror "$f"; then + clang_format_failed=1 + fi + fi + done + + if [[ "$FORMAT_FIX" == false && $clang_format_failed -ne 0 ]]; then + echo "clang-format reported issues." >&2 + echo "Run ./lint.sh --format-fix to apply formatting automatically." >&2 + exit 1 + fi +fi + +if [[ "$RUN_CLANG_TIDY" == true ]]; then + if [[ "$APPLY_FIXES" == true ]]; then + echo "[lint] Running clang-tidy with fixes enabled" + else + echo "[lint] Running clang-tidy" + fi + + existing_paths=() + for p in "${CLANG_TIDY_PATHS[@]}"; do + if [[ -e "$p" ]]; then + existing_paths+=("$p") + fi + done + + if [[ ${#existing_paths[@]} -eq 0 ]]; then + echo "No valid clang-tidy paths found." >&2 + exit 1 + fi + + mapfile -t source_files < <( + find "${existing_paths[@]}" -type f \( -name "*.c" -o -name "*.cc" -o -name "*.cpp" -o -name "*.cxx" \) | sort + ) + + if [[ ${#source_files[@]} -eq 0 ]]; then + echo "No C/C++ source files found for clang-tidy." >&2 + exit 1 + fi + + clang_tidy_failed=0 + total_files=${#source_files[@]} + index=0 + for f in "${source_files[@]}"; do + index=$((index + 1)) + echo "[lint][clang-tidy] ${index}/${total_files}: $f" + clang_tidy_cmd=(clang-tidy -p "$BUILD_DIR") + if [[ "$APPLY_FIXES" == true ]]; then + clang_tidy_cmd+=("-fix") + fi + clang_tidy_cmd+=("$f") + if ! "${clang_tidy_cmd[@]}"; then + clang_tidy_failed=1 + fi + done + + if [[ $clang_tidy_failed -ne 0 ]]; then + echo "clang-tidy reported issues." >&2 + exit 1 + fi +fi + +if [[ "$RUN_CPPCHECK" == true ]]; then + echo "[lint] Running cppcheck" + cppcheck \ + --enable=warning,style,performance,portability \ + --std=c++17 \ + --language=c++ \ + --inline-suppr \ + --error-exitcode=1 \ + -I include \ + -I src \ + src include test +fi + +echo "[lint] Completed successfully" diff --git a/run-all-test.sh b/run-all-test.sh new file mode 100755 index 0000000..bc0b324 --- /dev/null +++ b/run-all-test.sh @@ -0,0 +1,161 @@ +#!/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 + +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +RUN_UNIT=true +RUN_COMPONENT=true +BUILD_ONLY=false +CLEAN=false +UNIT_FILTER="" +COMPONENT_FILTER="" + +usage() { + cat < GTest filter passed to utApp + --component-filter GTest filter passed to ctApp + --help Show this help + +Examples: + ./run-all-test.sh + ./run-all-test.sh --unit-only --unit-filter "ActionsUTest.*" + ./run-all-test.sh --component-only --component-filter "*Localization*" +EOF +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --clean) + CLEAN=true + ;; + --build-only) + BUILD_ONLY=true + ;; + --unit-only) + RUN_COMPONENT=false + ;; + --component-only) + RUN_UNIT=false + ;; + --unit-filter) + if [[ $# -lt 2 || -z "${2:-}" || "$2" == --* ]]; then + echo "Missing value for --unit-filter" >&2 + usage + exit 1 + fi + UNIT_FILTER="${2:-}" + shift + ;; + --component-filter) + if [[ $# -lt 2 || -z "${2:-}" || "$2" == --* ]]; then + echo "Missing value for --component-filter" >&2 + usage + exit 1 + fi + COMPONENT_FILTER="${2:-}" + shift + ;; + --help|-h) + usage + exit 0 + ;; + *) + echo "Unknown option: $1" >&2 + usage + exit 1 + ;; + esac + shift +done + +if [[ "$RUN_UNIT" == false && "$RUN_COMPONENT" == false ]]; then + echo "Nothing to run: both unit and component test execution are disabled." >&2 + exit 1 +fi + +run_unit_script="$ROOT_DIR/run-unit-tests.sh" +run_component_script="$ROOT_DIR/run-component-tests.sh" + +bootstrap_args=() +if [[ "$CLEAN" == true ]]; then + bootstrap_args+=(--clean) +fi + +if [[ "$RUN_UNIT" == true && "$RUN_COMPONENT" == true ]]; then + echo "[run-all-test] Bootstrapping build once via run-unit-tests.sh --build-only" + "$run_unit_script" "${bootstrap_args[@]}" --build-only + + if [[ "$BUILD_ONLY" == true ]]; then + echo "Build complete. Skipping test execution (--build-only)." + exit 0 + fi + + unit_args=(--no-build) + if [[ -n "$UNIT_FILTER" ]]; then + unit_args+=(--unit-filter "$UNIT_FILTER") + fi + echo "[run-all-test] Running unit tests via run-unit-tests.sh" + "$run_unit_script" "${unit_args[@]}" + + component_args=(--no-build) + if [[ -n "$COMPONENT_FILTER" ]]; then + component_args+=(--component-filter "$COMPONENT_FILTER") + fi + echo "[run-all-test] Running component tests via run-component-tests.sh" + "$run_component_script" "${component_args[@]}" + exit 0 +fi + +if [[ "$RUN_UNIT" == true ]]; then + unit_args=() + if [[ "$CLEAN" == true ]]; then + unit_args+=(--clean) + fi + if [[ "$BUILD_ONLY" == true ]]; then + unit_args+=(--build-only) + fi + if [[ -n "$UNIT_FILTER" ]]; then + unit_args+=(--unit-filter "$UNIT_FILTER") + fi + echo "[run-all-test] Delegating to run-unit-tests.sh" + "$run_unit_script" "${unit_args[@]}" + exit 0 +fi + +component_args=() +if [[ "$CLEAN" == true ]]; then + component_args+=(--clean) +fi +if [[ "$BUILD_ONLY" == true ]]; then + component_args+=(--build-only) +fi +if [[ -n "$COMPONENT_FILTER" ]]; then + component_args+=(--component-filter "$COMPONENT_FILTER") +fi +echo "[run-all-test] Delegating to run-component-tests.sh" +"$run_component_script" "${component_args[@]}" diff --git a/run-component-tests.sh b/run-component-tests.sh new file mode 100755 index 0000000..859d91a --- /dev/null +++ b/run-component-tests.sh @@ -0,0 +1,103 @@ +#!/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 + +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +BUILD_DIR="build-dev" +BUILD_ONLY=false +NO_BUILD=false +CLEAN=false +COMPONENT_FILTER="" + +usage() { + cat < GTest filter passed to ctApp + --help Show this help + +Examples: + ./run-component-tests.sh + FIREBOLT_ENDPOINT=ws://127.0.0.1:3474/ ./run-component-tests.sh --component-filter "*Localization*" +EOF +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --clean) + CLEAN=true + ;; + --build-only) + BUILD_ONLY=true + ;; + --no-build) + NO_BUILD=true + ;; + --component-filter) + if [[ $# -lt 2 || -z "${2:-}" || "$2" == --* ]]; then + echo "Missing value for --component-filter" >&2 + usage + exit 1 + fi + COMPONENT_FILTER="${2:-}" + shift + ;; + --help|-h) + usage + exit 0 + ;; + *) + echo "Unknown option: $1" >&2 + usage + exit 1 + ;; + esac + shift +done + +cd "$ROOT_DIR" + +if [[ "$CLEAN" == true ]]; then + rm -rf "$BUILD_DIR" +fi + +if [[ "$NO_BUILD" == false ]]; then + ./build.sh +tests +fi + +if [[ "$BUILD_ONLY" == true ]]; then + echo "Build complete. Skipping test execution (--build-only)." + exit 0 +fi + +cd "$BUILD_DIR/test" +export LD_LIBRARY_PATH="../src:${LD_LIBRARY_PATH:-}" + +component_cmd=(./ctApp) +if [[ -n "$COMPONENT_FILTER" ]]; then + component_cmd+=("--gtest_filter=$COMPONENT_FILTER") +fi +echo "[run-component-tests] Running component tests: ${component_cmd[*]}" +"${component_cmd[@]}" \ No newline at end of file diff --git a/run-unit-tests.sh b/run-unit-tests.sh new file mode 100755 index 0000000..0c36a6e --- /dev/null +++ b/run-unit-tests.sh @@ -0,0 +1,103 @@ +#!/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 + +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +BUILD_DIR="build-dev" +BUILD_ONLY=false +NO_BUILD=false +CLEAN=false +UNIT_FILTER="" + +usage() { + cat < GTest filter passed to utApp + --help Show this help + +Examples: + ./run-unit-tests.sh + ./run-unit-tests.sh --unit-filter "ActionsUTest.*" +EOF +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --clean) + CLEAN=true + ;; + --build-only) + BUILD_ONLY=true + ;; + --no-build) + NO_BUILD=true + ;; + --unit-filter) + if [[ $# -lt 2 || -z "${2:-}" || "$2" == --* ]]; then + echo "Missing value for --unit-filter" >&2 + usage + exit 1 + fi + UNIT_FILTER="${2:-}" + shift + ;; + --help|-h) + usage + exit 0 + ;; + *) + echo "Unknown option: $1" >&2 + usage + exit 1 + ;; + esac + shift +done + +cd "$ROOT_DIR" + +if [[ "$CLEAN" == true ]]; then + rm -rf "$BUILD_DIR" +fi + +if [[ "$NO_BUILD" == false ]]; then + ./build.sh +tests +fi + +if [[ "$BUILD_ONLY" == true ]]; then + echo "Build complete. Skipping test execution (--build-only)." + exit 0 +fi + +cd "$BUILD_DIR/test" +export LD_LIBRARY_PATH="../src:${LD_LIBRARY_PATH:-}" + +unit_cmd=(./utApp) +if [[ -n "$UNIT_FILTER" ]]; then + unit_cmd+=("--gtest_filter=$UNIT_FILTER") +fi +echo "[run-unit-tests] Running unit tests: ${unit_cmd[*]}" +"${unit_cmd[@]}" \ No newline at end of file diff --git a/setup-device-tunnel.sh b/setup-device-tunnel.sh new file mode 100755 index 0000000..e3f7843 --- /dev/null +++ b/setup-device-tunnel.sh @@ -0,0 +1,132 @@ +#!/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 + +set -euo pipefail + +LOCAL_PORT="3474" +REMOTE_PORT="3474" +REMOTE_BIND_ADDR="127.0.0.1" +BACKGROUND=false + +require_env() { + local name="$1" + local value="${!name:-}" + if [[ -z "$value" ]]; then + echo "Error: required environment variable $name is not set" >&2 + exit 1 + fi +} + +usage() { + cat < -> : on + +Required environment variables: + DEVICE_SSH_USER SSH username + DEVICE_SSH_HOST Remote SSH host + DEVICE_SSH_PORT SSH port + +Defaults: + local-port ${LOCAL_PORT} + remote-port ${REMOTE_PORT} + remote-bind-addr ${REMOTE_BIND_ADDR} + +Options: + --local-port Local forwarded port (default: ${LOCAL_PORT}) + --remote-port Remote websocket port (default: ${REMOTE_PORT}) + --remote-bind Remote bind address (default: ${REMOTE_BIND_ADDR}) + --background Run ssh tunnel in background + --help Show this help + +Examples: + export DEVICE_SSH_USER=root + export DEVICE_SSH_HOST=192.168.201.170 + export DEVICE_SSH_PORT=10022 + ./setup-device-tunnel.sh + +After tunnel is up, run component tests with: + FIREBOLT_ENDPOINT=ws://127.0.0.1:${LOCAL_PORT}/ ./run-component-tests.sh +EOF +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --local-port) + if [[ $# -lt 2 || -z "${2:-}" || "${2:0:1}" == "-" ]]; then + echo "Missing value for --local-port" >&2 + usage + exit 1 + fi + LOCAL_PORT="${2:-}" + shift + ;; + --remote-port) + if [[ $# -lt 2 || -z "${2:-}" || "${2:0:1}" == "-" ]]; then + echo "Missing value for --remote-port" >&2 + usage + exit 1 + fi + REMOTE_PORT="${2:-}" + shift + ;; + --remote-bind) + if [[ $# -lt 2 || -z "${2:-}" || "${2:0:1}" == "-" ]]; then + echo "Missing value for --remote-bind" >&2 + usage + exit 1 + fi + REMOTE_BIND_ADDR="${2:-}" + shift + ;; + --background) + BACKGROUND=true + ;; + --help|-h) + usage + exit 0 + ;; + *) + echo "Unknown option: $1" >&2 + usage + exit 1 + ;; + esac + shift +done + +require_env DEVICE_SSH_USER +require_env DEVICE_SSH_HOST +require_env DEVICE_SSH_PORT + +TARGET="${DEVICE_SSH_USER}@${DEVICE_SSH_HOST}" +FORWARD_SPEC="${LOCAL_PORT}:${REMOTE_BIND_ADDR}:${REMOTE_PORT}" +SSH_ARGS=(-N -o BatchMode=yes -o StrictHostKeyChecking=accept-new -o ExitOnForwardFailure=yes -p "$DEVICE_SSH_PORT" -L "$FORWARD_SPEC" "$TARGET") + +echo "Opening SSH tunnel: localhost:${LOCAL_PORT} -> ${REMOTE_BIND_ADDR}:${REMOTE_PORT} on ${TARGET}" + +if [[ "$BACKGROUND" == true ]]; then + # -f backgrounds only after authentication and forwarding are set up. + ssh -f "${SSH_ARGS[@]}" + echo "Tunnel established in background mode." + exit 0 +fi + +exec ssh "${SSH_ARGS[@]}" diff --git a/src/actions_impl.cpp b/src/actions_impl.cpp new file mode 100644 index 0000000..431496f --- /dev/null +++ b/src/actions_impl.cpp @@ -0,0 +1,58 @@ +/** + * 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 + */ +// +// ============================================================================ +// AUTO-GENERATED by fb-gen — DO NOT EDIT +// ============================================================================ +#include "actions_impl.h" +#include "json_types/actions.h" +#include +#include + +namespace Firebolt::Actions +{ + +ActionsImpl::ActionsImpl(Firebolt::Helpers::IHelper& helper) + : helper_(helper), + subscriptionManager_(helper, this) +{ +} + +Result ActionsImpl::intent(const std::string& intent) const +{ + nlohmann::json params; + params["intent"] = intent; + return helper_.invoke("Actions.intent", params); +} + +Result ActionsImpl::subscribeOnIntent(std::function&& notification) +{ + return subscriptionManager_.subscribe("Actions.onIntent", std::move(notification)); +} + +Result ActionsImpl::unsubscribe(SubscriptionId id) +{ + return subscriptionManager_.unsubscribe(id); +} + +void ActionsImpl::unsubscribeAll() +{ + subscriptionManager_.unsubscribeAll(); +} + +} // namespace Firebolt::Actions diff --git a/src/actions_impl.h b/src/actions_impl.h new file mode 100644 index 0000000..e7eaf61 --- /dev/null +++ b/src/actions_impl.h @@ -0,0 +1,53 @@ +/** + * 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 + */ +// +// ============================================================================ +// AUTO-GENERATED by fb-gen — DO NOT EDIT +// ============================================================================ +#ifndef FIREBOLT_ACTIONS_IMPL_H +#define FIREBOLT_ACTIONS_IMPL_H + +#include "firebolt/actions.h" +#include + +namespace Firebolt::Actions +{ + +class ActionsImpl : public IActions +{ +public: + explicit ActionsImpl(Firebolt::Helpers::IHelper& helper); + ActionsImpl(const ActionsImpl&) = delete; + ActionsImpl& operator=(const ActionsImpl&) = delete; + ~ActionsImpl() override = default; + + Result intent(const std::string& intent) const override; + + Result subscribeOnIntent(std::function&& notification) override; + + Result unsubscribe(SubscriptionId id) override; + void unsubscribeAll() override; + +private: + Firebolt::Helpers::IHelper& helper_; + Firebolt::Helpers::SubscriptionManager subscriptionManager_; +}; + +} // namespace Firebolt::Actions + +#endif // FIREBOLT_ACTIONS_IMPL_H diff --git a/src/firebolt.cpp b/src/firebolt.cpp index ba0470c..0afad53 100644 --- a/src/firebolt.cpp +++ b/src/firebolt.cpp @@ -18,6 +18,7 @@ #include "firebolt/firebolt.h" #include "accessibility_impl.h" +#include "actions_impl.h" #include "advertising_impl.h" #include "device_impl.h" #include "discovery_impl.h" @@ -40,6 +41,7 @@ class FireboltAccessorImpl : public IFireboltAccessor FireboltAccessorImpl() : accessibility_(Firebolt::Helpers::GetHelperInstance()), advertising_(Firebolt::Helpers::GetHelperInstance()), + actions_(Firebolt::Helpers::GetHelperInstance()), device_(Firebolt::Helpers::GetHelperInstance()), discovery_(Firebolt::Helpers::GetHelperInstance()), display_(Firebolt::Helpers::GetHelperInstance()), @@ -83,11 +85,13 @@ class FireboltAccessorImpl : public IFireboltAccessor Presentation::IPresentation& PresentationInterface() override { return presentation_; } Stats::IStats& StatsInterface() override { return stats_; } TextToSpeech::ITextToSpeech& TextToSpeechInterface() override { return textToSpeech_; } + Actions::IActions& ActionsInterface() override { return actions_; } private: void unsubscribeAll() { accessibility_.unsubscribeAll(); + actions_.unsubscribeAll(); lifecycle_.unsubscribeAll(); localization_.unsubscribeAll(); network_.unsubscribeAll(); @@ -98,6 +102,7 @@ class FireboltAccessorImpl : public IFireboltAccessor private: Accessibility::AccessibilityImpl accessibility_; Advertising::AdvertisingImpl advertising_; + Actions::ActionsImpl actions_; Device::DeviceImpl device_; Discovery::DiscoveryImpl discovery_; Display::DisplayImpl display_; diff --git a/src/json_types/actions.h b/src/json_types/actions.h new file mode 100644 index 0000000..60c76ce --- /dev/null +++ b/src/json_types/actions.h @@ -0,0 +1,41 @@ +/** + * 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 + */ +// +// ============================================================================ +// AUTO-GENERATED by fb-gen — DO NOT EDIT +// ============================================================================ +#ifndef FIREBOLT_ACTIONS_JSON_H +#define FIREBOLT_ACTIONS_JSON_H + +#pragma once +#include "firebolt/actions.h" +#include +#include +#include + +namespace Firebolt::Actions +{ + +namespace JsonData +{ + +} // namespace JsonData + +} // namespace Firebolt::Actions + +#endif // FIREBOLT_ACTIONS_JSON_H diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 9b60ab9..a27713b 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -22,7 +22,7 @@ include(GoogleTest) enable_testing() -add_compile_definitions(UT_OPEN_RPC_FILE="firebolt-open-rpc.json") +add_compile_definitions(UT_OPEN_RPC_FILE="${CMAKE_SOURCE_DIR}/docs/openrpc/the-spec/firebolt-open-rpc.json") set(UNIT_TESTS_APP utApp) diff --git a/test/ComponentTestsMain.cpp b/test/ComponentTestsMain.cpp index 0346a2b..578ae28 100644 --- a/test/ComponentTestsMain.cpp +++ b/test/ComponentTestsMain.cpp @@ -18,6 +18,7 @@ #include "firebolt/firebolt.h" #include "gtest/gtest.h" +#include #include #include #include @@ -68,7 +69,9 @@ bool waitOnConnectionReady() int main(int argc, char** argv) { - string url = "ws://127.0.0.1:9998/"; + const char* endpoint = std::getenv("FIREBOLT_ENDPOINT"); + string url = (endpoint && endpoint[0] != '\0') ? endpoint : "ws://127.0.0.1:9998/"; + cout << "Using Firebolt endpoint: " << url << endl; createFireboltInstance(url); std::this_thread::sleep_for(std::chrono::seconds(1)); diff --git a/test/component/actionsGeneratedTest.cpp b/test/component/actionsGeneratedTest.cpp new file mode 100644 index 0000000..6ce5e5d --- /dev/null +++ b/test/component/actionsGeneratedTest.cpp @@ -0,0 +1,60 @@ +/** + * 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 + */ + +#include "firebolt/firebolt.h" +#include "utils.h" +#include + +class ActionsGeneratedCTest : public ::testing::Test +{ +protected: + void SetUp() override { eventReceived = false; } + + std::condition_variable cv; + std::mutex mtx; + bool eventReceived; +}; + +TEST_F(ActionsGeneratedCTest, Intent) +{ + auto result = Firebolt::IFireboltAccessor::Instance().ActionsInterface().intent("launch"); + EXPECT_TRUE(result) << toError(result); +} + +TEST_F(ActionsGeneratedCTest, SubscribeOnIntent) +{ + auto id = Firebolt::IFireboltAccessor::Instance().ActionsInterface().subscribeOnIntent( + [&](const std::string& intent) + { + EXPECT_EQ(intent, "launch"); + { + std::lock_guard lock(mtx); + eventReceived = true; + } + cv.notify_one(); + }); + + ASSERT_TRUE(id) << toError(id); + verifyEventSubscription(id); + + triggerEvent("Actions.onIntent", R"({"value":"launch"})"); + verifyEventReceived(mtx, cv, eventReceived); + + auto result = Firebolt::IFireboltAccessor::Instance().ActionsInterface().unsubscribe(id.value()); + verifyUnsubscribeResult(result); +} diff --git a/test/unit/actionsGeneratedTest.cpp b/test/unit/actionsGeneratedTest.cpp new file mode 100644 index 0000000..86006f3 --- /dev/null +++ b/test/unit/actionsGeneratedTest.cpp @@ -0,0 +1,56 @@ +/** + * 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 + */ + +#include "actions_impl.h" +#include "mock_helper.h" +#include + +class ActionsGeneratedUTest : public ::testing::Test +{ +protected: + ::testing::NiceMock mockHelper; + Firebolt::Actions::ActionsImpl impl{mockHelper}; +}; + +TEST_F(ActionsGeneratedUTest, Constructs) +{ + SUCCEED(); +} + +TEST_F(ActionsGeneratedUTest, UnsubscribeForwardsToHelper) +{ + EXPECT_CALL(mockHelper, unsubscribe(7)).WillOnce(::testing::Return(Firebolt::Result{Firebolt::Error::None})); + + auto result = impl.unsubscribe(7); + ASSERT_TRUE(result) << "unsubscribe should return success when helper succeeds"; +} + +TEST_F(ActionsGeneratedUTest, ForwardsintentTransportErrors) +{ + EXPECT_CALL(mockHelper, invoke("Actions.intent", ::testing::_)) + .WillOnce(::testing::Invoke( + [](const std::string& /*method*/, const nlohmann::json& params) + { + EXPECT_TRUE(params.is_object()) << "Expected params object for method call"; + EXPECT_TRUE(params.contains("intent")) << "Missing expected param key: intent"; + return Firebolt::Result{Firebolt::Error::General}; + })); + + auto result = impl.intent({}); + EXPECT_FALSE(result) << "Expected error propagation when helper invoke fails"; +} diff --git a/test/unit/actionsTest.cpp b/test/unit/actionsTest.cpp new file mode 100644 index 0000000..7295886 --- /dev/null +++ b/test/unit/actionsTest.cpp @@ -0,0 +1,53 @@ +/** + * Copyright 2025 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 "actions_impl.h" +#include "json_engine.h" +#include "mock_helper.h" + +class ActionsUTest : public ::testing::Test, protected MockBase +{ +protected: + Firebolt::Actions::ActionsImpl actionsImpl_{mockHelper}; +}; + +TEST_F(ActionsUTest, Start) +{ + nlohmann::json expectedParams; + expectedParams["intent"] = "launch"; + EXPECT_CALL(mockHelper, invoke("Actions.intent", expectedParams)) + .WillOnce(Invoke([&](const std::string& /*methodName*/, const nlohmann::json& /*parameters*/) + { return Firebolt::Result{Firebolt::Error::None}; })); + + auto result = actionsImpl_.intent("launch"); + EXPECT_TRUE(result); +} + +TEST_F(ActionsUTest, SubscribeOnIntent) +{ + nlohmann::json expectedValue = 1; + mockSubscribe("Actions.onIntent"); + + auto result = actionsImpl_.subscribeOnIntent([&](const std::string& /*value*/) {}); + + ASSERT_TRUE(result) << "ActionsImpl::subscribeOnIntent() returned an error"; + EXPECT_EQ(*result, expectedValue); + + auto unsubResult = actionsImpl_.unsubscribe(*result); + ASSERT_TRUE(unsubResult) << "ActionsImpl::unsubscribe() returned an error"; +} diff --git a/test/utils.cpp b/test/utils.cpp index 792bf4f..60de1be 100644 --- a/test/utils.cpp +++ b/test/utils.cpp @@ -125,7 +125,7 @@ void verifyUnsubscribeResult(const Firebolt::Result& result) FAIL() << "Unsubscribe failed." + toError(result); } } -void verifyEventReceived(std::mutex& mtx, std::condition_variable& cv, bool& eventReceived) +void verifyEventReceived(std::mutex& mtx, std::condition_variable& cv, const bool& eventReceived) { std::unique_lock lock(mtx); if (!cv.wait_for(lock, EventWaitTime, [&] { return eventReceived; })) @@ -134,7 +134,7 @@ void verifyEventReceived(std::mutex& mtx, std::condition_variable& cv, bool& eve } } -void verifyEventNotReceived(std::mutex& mtx, std::condition_variable& cv, bool& eventReceived) +void verifyEventNotReceived(std::mutex& mtx, std::condition_variable& cv, const bool& eventReceived) { // Wait for the event to be received or timeout after 5 seconds std::unique_lock lock(mtx); diff --git a/test/utils.h b/test/utils.h index bd1e2e3..b5d0693 100644 --- a/test/utils.h +++ b/test/utils.h @@ -32,8 +32,8 @@ void triggerRaw(const std::string& payload); void verifyEventSubscription(const Firebolt::Result& id); void verifyUnsubscribeResult(const Firebolt::Result& result); -void verifyEventReceived(std::mutex& mtx, std::condition_variable& cv, bool& eventReceived); -void verifyEventNotReceived(std::mutex& mtx, std::condition_variable& cv, bool& eventReceived); +void verifyEventReceived(std::mutex& mtx, std::condition_variable& cv, const bool& eventReceived); +void verifyEventNotReceived(std::mutex& mtx, std::condition_variable& cv, const bool& eventReceived); template inline std::string toError(const Firebolt::Result& result) {