diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..e80616f --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,84 @@ +# firebolt-cpp-client Copilot Instructions + +Scope: This file applies to the firebolt-cpp-client repository. + +## Primary goals + +- Preserve API contract correctness across interface, implementation, tests, and OpenRPC fixtures. +- Keep generated surfaces and bespoke conventions aligned. +- Prefer minimal, targeted changes. + +## High-signal workflow + +1. For API-facing changes, update all of the following in one pass: + - `include/firebolt/*.h` + - `src/*_impl.h` and `src/*_impl.cpp` + - `test/unit/*Test.cpp` and `test/component/*Test.cpp` + - `docs/openrpc/the-spec/firebolt-open-rpc.json` when component tests depend on fixture shape. +2. Run component tests after edits. +3. If behavior is generator-owned, patch generator code in sibling repo and regenerate module artifacts. + +## Test commands + +- Local one-shot (current preferred): + - `./run-component-tests-local.sh` + - `./run-component-tests-local.sh --skip-image-build` +- Legacy wrappers may still exist in conversation history; prefer the local script in this repo. +- Unit-only: + - `./run-unit-tests.sh` + +## Actions module rules (important) + +- `Actions.intent` is getter-only: + - takes no parameters + - returns `Result` +- `Actions.onIntent` callback payload is a string value. +- Component event trigger for `Actions.onIntent` should use a string JSON payload (for example `"launch"`), not an object. + +## Generated-code conventions that must be preserved + +- `*Impl` classes should delete copy constructor and copy assignment: + - `ClassName(const ClassName&) = delete;` + - `ClassName& operator=(const ClassName&) = delete;` +- Unless explicitly justified as safe, `*Impl` classes should also delete move operations: + - `ClassName(ClassName&&) = delete;` + - `ClassName& operator=(ClassName&&) = delete;` +- Keep include hygiene strict: + - include `` when using `std::move` + - remove unused includes such as `` when not used +- Keep test names in consistent CamelCase for filtering. + +## Component test expectations + +- Red schema validation lines in logs can be expected for negative-path tests. +- Negative tests must still verify runtime behavior (callbacks not delivered for invalid payloads), not just compile-time surface checks. + +## OpenRPC fixture expectations + +- Module descriptions must match actual API behavior. +- Getter-style methods should carry property tags consistent with the rest of the file (for example `property:readonly` where applicable). +- Keep notifier/subscriber metadata aligned (`x-notifier`, `x-subscriber-for`). + +## Regeneration notes (sibling repo) + +When a change is generator-owned, use `firebolt-sdk-gen` and apply module-scoped output back into this repo. + +Typical flow: + +- From `../firebolt-sdk-gen`: + - `./sync-plan-checklist.sh --profile core --module actions --apply --no-accessor-touchpoints --target-root ../firebolt-cpp-client` + +This keeps migration incremental and avoids unrelated accessor touchpoint churn. + +## CI parity reminders + +- CI uses Dockerized build/test flow and mock-firebolt integration. +- Keep changes compatible with: + - `.github/workflows/ci.yml` + - `.github/scripts/run-component-tests.sh` + +## PR hygiene + +- If a review asks for include-file fixes, prefer precise header/source edits and re-run component tests. +- Do not relax negative tests just to suppress red validation logs. +- Keep commit messages scoped and explicit (example: `fix(actions): address include review comments`). diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 01baa9a..0c7d124 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,9 +12,9 @@ on: - rpc_v2 - legacy push: - branches: [ main, next ] + branches: [ main, develop, next ] pull_request: - branches: [ main, next ] + branches: [ main, develop, next ] defaults: run: diff --git a/CHANGELOG.md b/CHANGELOG.md index 8863674..58e532d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +## [0.6.0](https://github.com/rdkcentral/firebolt-cpp-client/compare/v0.5.5...v0.6.0) + +### Added +- New APIs + - `Actions.intent` (no parameters, returns string) + - `Actions.onIntent` event + +### Changed +- **Breaking**: `Discovery.watched` and all `Metrics.*` methods now return `Result` (previously `Result`) + ## [0.5.5](https://github.com/rdkcentral/firebolt-cpp-client/compare/v0.5.4...v0.5.5) ### Changed 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/docs/openrpc/openrpc/discovery.json b/docs/openrpc/openrpc/discovery.json index b6f22c0..8862606 100644 --- a/docs/openrpc/openrpc/discovery.json +++ b/docs/openrpc/openrpc/discovery.json @@ -61,10 +61,9 @@ } ], "result": { - "name": "success", - "summary": "whether the call was successful or not", + "name": "result", "schema": { - "type": "boolean" + "type": "null" } }, "examples": [ @@ -89,8 +88,8 @@ } ], "result": { - "name": "success", - "value": true + "name": "result", + "value": null } }, { @@ -118,8 +117,8 @@ } ], "result": { - "name": "success", - "value": true + "name": "result", + "value": null } } ] diff --git a/docs/openrpc/openrpc/metrics.json b/docs/openrpc/openrpc/metrics.json index fe89723..b1d705b 100644 --- a/docs/openrpc/openrpc/metrics.json +++ b/docs/openrpc/openrpc/metrics.json @@ -19,9 +19,9 @@ "summary": "Inform the platform that your app is minimally usable. This method is called automatically by `Lifecycle.ready()`", "params": [], "result": { - "name": "success", + "name": "result", "schema": { - "type": "boolean" + "type": "null" } }, "examples": [ @@ -29,8 +29,8 @@ "name": "Send ready metric", "params": [], "result": { - "name": "success", - "value": true + "name": "result", + "value": null } } ] @@ -51,9 +51,9 @@ "summary": "Log a sign in event, called by Discovery.signIn().", "params": [], "result": { - "name": "success", + "name": "result", "schema": { - "type": "boolean" + "type": "null" } }, "examples": [ @@ -61,8 +61,8 @@ "name": "Send signIn metric", "params": [], "result": { - "name": "success", - "value": true + "name": "result", + "value": null } } ] @@ -83,9 +83,9 @@ "summary": "Log a sign out event, called by Discovery.signOut().", "params": [], "result": { - "name": "success", + "name": "result", "schema": { - "type": "boolean" + "type": "null" } }, "examples": [ @@ -93,8 +93,8 @@ "name": "Send signOut metric", "params": [], "result": { - "name": "success", - "value": true + "name": "result", + "value": null } } ] @@ -128,9 +128,9 @@ } ], "result": { - "name": "success", + "name": "result", "schema": { - "type": "boolean" + "type": "null" } }, "examples": [ @@ -138,8 +138,8 @@ "name": "Send startContent metric", "params": [], "result": { - "name": "success", - "value": true + "name": "result", + "value": null } }, { @@ -151,8 +151,8 @@ } ], "result": { - "name": "success", - "value": true + "name": "result", + "value": null } }, { @@ -168,8 +168,8 @@ } ], "result": { - "name": "success", - "value": true + "name": "result", + "value": null } } ] @@ -203,9 +203,9 @@ } ], "result": { - "name": "success", + "name": "result", "schema": { - "type": "boolean" + "type": "null" } }, "examples": [ @@ -213,8 +213,8 @@ "name": "Send stopContent metric", "params": [], "result": { - "name": "success", - "value": true + "name": "result", + "value": null } }, { @@ -226,8 +226,8 @@ } ], "result": { - "name": "success", - "value": true + "name": "result", + "value": null } } ] @@ -261,9 +261,9 @@ } ], "result": { - "name": "success", + "name": "result", "schema": { - "type": "boolean" + "type": "null" } }, "examples": [ @@ -276,8 +276,8 @@ } ], "result": { - "name": "success", - "value": true + "name": "result", + "value": null } }, { @@ -289,8 +289,8 @@ } ], "result": { - "name": "success", - "value": true + "name": "result", + "value": null } } ] @@ -356,9 +356,9 @@ } ], "result": { - "name": "success", + "name": "result", "schema": { - "type": "boolean" + "type": "null" } }, "examples": [ @@ -383,8 +383,8 @@ } ], "result": { - "name": "success", - "value": true + "name": "result", + "value": null } } ] @@ -418,9 +418,9 @@ } ], "result": { - "name": "success", + "name": "result", "schema": { - "type": "boolean" + "type": "null" } }, "examples": [ @@ -433,8 +433,8 @@ } ], "result": { - "name": "success", - "value": true + "name": "result", + "value": null } } ] @@ -468,9 +468,9 @@ } ], "result": { - "name": "success", + "name": "result", "schema": { - "type": "boolean" + "type": "null" } }, "examples": [ @@ -483,8 +483,8 @@ } ], "result": { - "name": "success", - "value": true + "name": "result", + "value": null } } ] @@ -518,9 +518,9 @@ } ], "result": { - "name": "success", + "name": "result", "schema": { - "type": "boolean" + "type": "null" } }, "examples": [ @@ -533,8 +533,8 @@ } ], "result": { - "name": "success", - "value": true + "name": "result", + "value": null } } ] @@ -568,9 +568,9 @@ } ], "result": { - "name": "success", + "name": "result", "schema": { - "type": "boolean" + "type": "null" } }, "examples": [ @@ -583,8 +583,8 @@ } ], "result": { - "name": "success", - "value": true + "name": "result", + "value": null } } ] @@ -618,9 +618,9 @@ } ], "result": { - "name": "success", + "name": "result", "schema": { - "type": "boolean" + "type": "null" } }, "examples": [ @@ -633,8 +633,8 @@ } ], "result": { - "name": "success", - "value": true + "name": "result", + "value": null } } ] @@ -676,9 +676,9 @@ } ], "result": { - "name": "success", + "name": "result", "schema": { - "type": "boolean" + "type": "null" } }, "examples": [ @@ -691,12 +691,12 @@ }, { "name": "target", - "value": 0.50 + "value": 0.5 } ], "result": { - "name": "success", - "value": true + "name": "result", + "value": null } } ] @@ -738,9 +738,9 @@ } ], "result": { - "name": "success", + "name": "result", "schema": { - "type": "boolean" + "type": "null" } }, "examples": [ @@ -757,8 +757,8 @@ } ], "result": { - "name": "success", - "value": true + "name": "result", + "value": null } } ] @@ -800,9 +800,9 @@ } ], "result": { - "name": "success", + "name": "result", "schema": { - "type": "boolean" + "type": "null" } }, "examples": [ @@ -819,8 +819,8 @@ } ], "result": { - "name": "success", - "value": true + "name": "result", + "value": null } } ] @@ -886,9 +886,9 @@ } ], "result": { - "name": "success", + "name": "result", "schema": { - "type": "boolean" + "type": "null" } }, "examples": [ @@ -917,8 +917,8 @@ } ], "result": { - "name": "success", - "value": true + "name": "result", + "value": null } } ] @@ -952,9 +952,9 @@ } ], "result": { - "name": "success", + "name": "result", "schema": { - "type": "boolean" + "type": "null" } }, "examples": [ @@ -967,8 +967,8 @@ } ], "result": { - "name": "success", - "value": true + "name": "result", + "value": null } } ] @@ -1013,7 +1013,7 @@ "result": { "name": "result", "schema": { - "type": "boolean" + "type": "null" } }, "examples": [ @@ -1031,7 +1031,7 @@ ], "result": { "name": "result", - "value": true + "value": null } } ] @@ -1135,4 +1135,4 @@ } } } -} \ No newline at end of file +} diff --git a/docs/openrpc/the-spec/firebolt-open-rpc.json b/docs/openrpc/the-spec/firebolt-open-rpc.json index 05a1fc6..9ad9084 100644 --- a/docs/openrpc/the-spec/firebolt-open-rpc.json +++ b/docs/openrpc/the-spec/firebolt-open-rpc.json @@ -5,6 +5,7 @@ "version": "", "x-module-descriptions": { "Accessibility": "The `Accessibility` module provides access to the user/device settings for closed captioning and voice guidance.\n\nApps **SHOULD** attempt o respect these settings, rather than manage and persist seprate settings, which would be different per-app.", + "Actions": "Methods for getting and observing app intents.", "Advertising": "A module for platform provided advertising settings and functionality.", "Device": "A module for querying about the device and it's capabilities.", "Discovery": "Your App likely wants to integrate with the Platform's discovery capabilities. For example to add a \"Watch Next\" tile that links to your app from the platform's home screen.\n\nGetting access to this information requires to connect to lower level APIs made available by the platform. Since implementations differ between operators and platforms, the Firebolt SDK offers a Discovery module, that exposes a generic, agnostic interface to the developer.\n\nUnder the hood, an underlaying transport layer will then take care of calling the right APIs for the actual platform implementation that your App is running on.\n\nThe Discovery plugin is used to _send_ information to the Platform.\n\n### Localization\nApps should provide all user-facing strings in the device's language, as specified by the Firebolt `Localization.language` property.\n\nApps should provide prices in the same currency presented in the app. If multiple currencies are supported in the app, the app should provide prices in the user's current default currency.", @@ -48,6 +49,85 @@ } ] }, + { + "name": "Actions.intent", + "summary": "Returns the current intent.", + "tags": [ + { + "name": "property:readonly" + }, + { + "name": "capabilities", + "x-uses": [ + "xrn:firebolt:capability:actions:intent" + ] + } + ], + "params": [], + "result": { + "name": "intent", + "summary": "The current intent.", + "schema": { + "type": "string" + } + }, + "examples": [ + { + "name": "Get the current intent", + "result": { + "name": "Default Result", + "value": "launch" + } + } + ] + }, + { + "name": "Actions.onIntent", + "tags": [ + { + "name": "event", + "x-notifier": "Actions.onIntent", + "x-subscriber-for": "Actions.intent" + }, + { + "name": "capabilities", + "x-uses": [ + "xrn:firebolt:capability:actions:intent" + ] + } + ], + "summary": "Notifies when the current intent changes.", + "params": [ + { + "name": "listen", + "schema": { + "type": "boolean" + } + } + ], + "result": { + "name": "intent", + "summary": "The current intent.", + "schema": { + "type": "string" + } + }, + "examples": [ + { + "name": "Listen for intent changes", + "params": [ + { + "name": "listen", + "value": true + } + ], + "result": { + "name": "Default Result", + "value": "launch" + } + } + ] + }, { "name": "Accessibility.audioDescription", "summary": "Returns the audio description setting of the device", @@ -502,10 +582,9 @@ } ], "result": { - "name": "success", - "summary": "whether the call was successful or not", + "name": "result", "schema": { - "type": "boolean" + "type": "null" } }, "examples": [ @@ -530,8 +609,8 @@ } ], "result": { - "name": "success", - "value": true + "name": "result", + "value": null } }, { @@ -559,8 +638,8 @@ } ], "result": { - "name": "success", - "value": true + "name": "result", + "value": null } } ] @@ -902,9 +981,9 @@ "summary": "Inform the platform that your app is minimally usable. This method is called automatically by `Lifecycle.ready()`", "params": [], "result": { - "name": "success", + "name": "result", "schema": { - "type": "boolean" + "type": "null" } }, "examples": [ @@ -912,8 +991,8 @@ "name": "Send ready metric", "params": [], "result": { - "name": "success", - "value": true + "name": "result", + "value": null } } ] @@ -934,9 +1013,9 @@ "summary": "Log a sign in event, called by Discovery.signIn().", "params": [], "result": { - "name": "success", + "name": "result", "schema": { - "type": "boolean" + "type": "null" } }, "examples": [ @@ -944,8 +1023,8 @@ "name": "Send signIn metric", "params": [], "result": { - "name": "success", - "value": true + "name": "result", + "value": null } } ] @@ -966,9 +1045,9 @@ "summary": "Log a sign out event, called by Discovery.signOut().", "params": [], "result": { - "name": "success", + "name": "result", "schema": { - "type": "boolean" + "type": "null" } }, "examples": [ @@ -976,8 +1055,8 @@ "name": "Send signOut metric", "params": [], "result": { - "name": "success", - "value": true + "name": "result", + "value": null } } ] @@ -1011,9 +1090,9 @@ } ], "result": { - "name": "success", + "name": "result", "schema": { - "type": "boolean" + "type": "null" } }, "examples": [ @@ -1021,8 +1100,8 @@ "name": "Send startContent metric", "params": [], "result": { - "name": "success", - "value": true + "name": "result", + "value": null } }, { @@ -1034,8 +1113,8 @@ } ], "result": { - "name": "success", - "value": true + "name": "result", + "value": null } }, { @@ -1051,8 +1130,8 @@ } ], "result": { - "name": "success", - "value": true + "name": "result", + "value": null } } ] @@ -1086,9 +1165,9 @@ } ], "result": { - "name": "success", + "name": "result", "schema": { - "type": "boolean" + "type": "null" } }, "examples": [ @@ -1096,8 +1175,8 @@ "name": "Send stopContent metric", "params": [], "result": { - "name": "success", - "value": true + "name": "result", + "value": null } }, { @@ -1109,8 +1188,8 @@ } ], "result": { - "name": "success", - "value": true + "name": "result", + "value": null } } ] @@ -1144,9 +1223,9 @@ } ], "result": { - "name": "success", + "name": "result", "schema": { - "type": "boolean" + "type": "null" } }, "examples": [ @@ -1159,8 +1238,8 @@ } ], "result": { - "name": "success", - "value": true + "name": "result", + "value": null } }, { @@ -1172,8 +1251,8 @@ } ], "result": { - "name": "success", - "value": true + "name": "result", + "value": null } } ] @@ -1239,9 +1318,9 @@ } ], "result": { - "name": "success", + "name": "result", "schema": { - "type": "boolean" + "type": "null" } }, "examples": [ @@ -1266,8 +1345,8 @@ } ], "result": { - "name": "success", - "value": true + "name": "result", + "value": null } } ] @@ -1301,9 +1380,9 @@ } ], "result": { - "name": "success", + "name": "result", "schema": { - "type": "boolean" + "type": "null" } }, "examples": [ @@ -1316,8 +1395,8 @@ } ], "result": { - "name": "success", - "value": true + "name": "result", + "value": null } } ] @@ -1351,9 +1430,9 @@ } ], "result": { - "name": "success", + "name": "result", "schema": { - "type": "boolean" + "type": "null" } }, "examples": [ @@ -1366,8 +1445,8 @@ } ], "result": { - "name": "success", - "value": true + "name": "result", + "value": null } } ] @@ -1401,9 +1480,9 @@ } ], "result": { - "name": "success", + "name": "result", "schema": { - "type": "boolean" + "type": "null" } }, "examples": [ @@ -1416,8 +1495,8 @@ } ], "result": { - "name": "success", - "value": true + "name": "result", + "value": null } } ] @@ -1451,9 +1530,9 @@ } ], "result": { - "name": "success", + "name": "result", "schema": { - "type": "boolean" + "type": "null" } }, "examples": [ @@ -1466,8 +1545,8 @@ } ], "result": { - "name": "success", - "value": true + "name": "result", + "value": null } } ] @@ -1501,9 +1580,9 @@ } ], "result": { - "name": "success", + "name": "result", "schema": { - "type": "boolean" + "type": "null" } }, "examples": [ @@ -1516,8 +1595,8 @@ } ], "result": { - "name": "success", - "value": true + "name": "result", + "value": null } } ] @@ -1559,9 +1638,9 @@ } ], "result": { - "name": "success", + "name": "result", "schema": { - "type": "boolean" + "type": "null" } }, "examples": [ @@ -1578,8 +1657,8 @@ } ], "result": { - "name": "success", - "value": true + "name": "result", + "value": null } } ] @@ -1621,9 +1700,9 @@ } ], "result": { - "name": "success", + "name": "result", "schema": { - "type": "boolean" + "type": "null" } }, "examples": [ @@ -1640,8 +1719,8 @@ } ], "result": { - "name": "success", - "value": true + "name": "result", + "value": null } } ] @@ -1683,9 +1762,9 @@ } ], "result": { - "name": "success", + "name": "result", "schema": { - "type": "boolean" + "type": "null" } }, "examples": [ @@ -1702,8 +1781,8 @@ } ], "result": { - "name": "success", - "value": true + "name": "result", + "value": null } } ] @@ -1769,9 +1848,9 @@ } ], "result": { - "name": "success", + "name": "result", "schema": { - "type": "boolean" + "type": "null" } }, "examples": [ @@ -1800,8 +1879,8 @@ } ], "result": { - "name": "success", - "value": true + "name": "result", + "value": null } } ] @@ -1835,9 +1914,9 @@ } ], "result": { - "name": "success", + "name": "result", "schema": { - "type": "boolean" + "type": "null" } }, "examples": [ @@ -1850,8 +1929,8 @@ } ], "result": { - "name": "success", - "value": true + "name": "result", + "value": null } } ] @@ -1896,7 +1975,7 @@ "result": { "name": "result", "schema": { - "type": "boolean" + "type": "null" } }, "examples": [ @@ -1914,7 +1993,7 @@ ], "result": { "name": "result", - "value": true + "value": null } } ] diff --git a/include/firebolt/actions.h b/include/firebolt/actions.h new file mode 100644 index 0000000..660abd6 --- /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 = 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/discovery.h b/include/firebolt/discovery.h index 50cecb9..885380e 100644 --- a/include/firebolt/discovery.h +++ b/include/firebolt/discovery.h @@ -39,9 +39,9 @@ class IDiscovery * @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 The status. + * @retval An ok Result on success, or an error; no value is returned */ - virtual Result watched(const std::string& entityId, std::optional progress, + virtual Result watched(const std::string& entityId, std::optional progress, std::optional completed, std::optional watchedOn, std::optional agePolicy) const = 0; }; 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/include/firebolt/metrics.h b/include/firebolt/metrics.h index d14c5bf..6927674 100644 --- a/include/firebolt/metrics.h +++ b/include/firebolt/metrics.h @@ -43,23 +43,23 @@ class IMetrics /** * @brief Informs the platform that the app is minimally usable * - * @retval The success state or error + * @retval An ok Result on success, or an error; no value is returned */ - virtual Result ready() const = 0; + virtual Result ready() const = 0; /** * @brief Logs a sign in event * - * @retval The success state or error + * @retval An ok Result on success, or an error; no value is returned */ - virtual Result signIn() const = 0; + virtual Result signIn() const = 0; /** * @brief Logs a sign out event * - * @retval The success state or error + * @retval An ok Result on success, or an error; no value is returned */ - virtual Result signOut() const = 0; + virtual Result signOut() const = 0; /** * @brief Informs the platform that your user has started content @@ -68,9 +68,9 @@ class IMetrics * @param[in] agePolicy : The age policy to associate with the metrics event, the age policy describes the age group * to which content is directed * - * @retval The success state or error + * @retval An ok Result on success, or an error; no value is returned */ - virtual Result startContent(const std::optional& entityId, + virtual Result startContent(const std::optional& entityId, const std::optional agePolicy) const = 0; /** @@ -80,9 +80,9 @@ class IMetrics * @param[in] agePolicy : The age policy to associate with the metrics event, the age policy describes the age group * to which content is directed * - * @retval The success state or error + * @retval An ok Result on success, or an error; no value is returned */ - virtual Result stopContent(const std::optional& entityId, + virtual Result stopContent(const std::optional& entityId, const std::optional agePolicy) const = 0; /** @@ -92,9 +92,9 @@ class IMetrics * @param[in] agePolicy : The age policy to associate with the metrics event, the age policy describes the age group * to which content is directed * - * @retval The success state or error + * @retval An ok Result on success, or an error; no value is returned */ - virtual Result page(const std::string& pageId, const std::optional& agePolicy) const = 0; + virtual Result page(const std::string& pageId, const std::optional& agePolicy) const = 0; /** * @brief Informs the platform of an error that has occurred in your app @@ -107,9 +107,9 @@ class IMetrics * @param[in] agePolicy : The age policy to associate with the metrics event, the age policy describes the age * group to which content is directed * - * @retval The success state or error + * @retval An ok Result on success, or an error; no value is returned */ - virtual Result error(const ErrorType type, const std::string& code, const std::string& description, + virtual Result error(const ErrorType type, const std::string& code, const std::string& description, const bool visible, const std::optional>& parameters, const std::optional& agePolicy) const = 0; @@ -120,9 +120,9 @@ class IMetrics * @param[in] agePolicy : The age policy to associate with the metrics event, the age policy describes the age group * to which content is directed * - * @retval The success state or error + * @retval An ok Result on success, or an error; no value is returned */ - virtual Result mediaLoadStart(const std::string& entityId, + virtual Result mediaLoadStart(const std::string& entityId, const std::optional& agePolicy) const = 0; /** @@ -133,9 +133,9 @@ class IMetrics * @param[in] agePolicy : The age policy to associate with the metrics event, the age policy describes the age group * to which content is directed * - * @retval The success state or error + * @retval An ok Result on success, or an error; no value is returned */ - virtual Result mediaPlaying(const std::string& entityId, + virtual Result mediaPlaying(const std::string& entityId, const std::optional& agePolicy) const = 0; /** @@ -145,9 +145,9 @@ class IMetrics * @param[in] agePolicy : The age policy to associate with the metrics event, the age policy describes the age group * to which content is directed * - * @retval The success state or error + * @retval An ok Result on success, or an error; no value is returned */ - virtual Result mediaPlay(const std::string& entityId, + virtual Result mediaPlay(const std::string& entityId, const std::optional& agePolicy) const = 0; /** @@ -157,9 +157,9 @@ class IMetrics * @param[in] agePolicy : The age policy to associate with the metrics event, the age policy describes the age group * to which content is directed * - * @retval The success state or error + * @retval An ok Result on success, or an error; no value is returned */ - virtual Result mediaPause(const std::string& entityId, + virtual Result mediaPause(const std::string& entityId, const std::optional& agePolicy) const = 0; /** @@ -169,9 +169,9 @@ class IMetrics * @param[in] agePolicy : The age policy to associate with the metrics event, the age policy describes the age group * to which content is directed * - * @retval The success state or error + * @retval An ok Result on success, or an error; no value is returned */ - virtual Result mediaWaiting(const std::string& entityId, + virtual Result mediaWaiting(const std::string& entityId, const std::optional& agePolicy) const = 0; /** @@ -183,9 +183,9 @@ class IMetrics * @param[in] agePolicy : The age policy to associate with the metrics event, the age policy describes the age group * to which content is directed * - * @retval The success state or error + * @retval An ok Result on success, or an error; no value is returned */ - virtual Result mediaSeeking(const std::string& entityId, const double target, + virtual Result mediaSeeking(const std::string& entityId, const double target, const std::optional& agePolicy) const = 0; /** @@ -198,9 +198,9 @@ class IMetrics * @param[in] agePolicy : The age policy to associate with the metrics event, the age policy describes the age group * to which content is directed * - * @retval The success state or error + * @retval An ok Result on success, or an error; no value is returned */ - virtual Result mediaSeeked(const std::string& entityId, const double position, + virtual Result mediaSeeked(const std::string& entityId, const double position, const std::optional& agePolicy) const = 0; /** @@ -211,9 +211,9 @@ class IMetrics * @param[in] agePolicy : The age policy to associate with the metrics event, the age policy describes the age group * to which content is directed * - * @retval The success state or error + * @retval An ok Result on success, or an error; no value is returned */ - virtual Result mediaRateChanged(const std::string& entityId, const double rate, + virtual Result mediaRateChanged(const std::string& entityId, const double rate, const std::optional& agePolicy) const = 0; /** @@ -227,9 +227,9 @@ class IMetrics * @param[in] agePolicy : The age policy to associate with the metrics event, the age policy describes the age group * to which content is directed * - * @retval The success state or error + * @retval An ok Result on success, or an error; no value is returned */ - virtual Result mediaRenditionChanged(const std::string& entityId, const unsigned bitrate, const unsigned width, + virtual Result mediaRenditionChanged(const std::string& entityId, const unsigned bitrate, const unsigned width, const unsigned height, const std::optional& profile, const std::optional& agePolicy) const = 0; @@ -240,9 +240,9 @@ class IMetrics * @param[in] agePolicy : The age policy to associate with the metrics event, the age policy describes the age group * to which content is directed * - * @retval The success state or error + * @retval An ok Result on success, or an error; no value is returned */ - virtual Result mediaEnded(const std::string& entityId, + virtual Result mediaEnded(const std::string& entityId, const std::optional& agePolicy) const = 0; /** @@ -253,9 +253,9 @@ class IMetrics * @param[in] agePolicy : The age policy to associate with the metrics event, the age policy describes the age group * to which content is directed * - * @retval The success state or error + * @retval An ok Result on success, or an error; no value is returned */ - virtual Result event(const std::string& schema, const std::string& data, + virtual Result event(const std::string& schema, const std::string& data, const std::optional& agePolicy) const = 0; /** @@ -263,7 +263,7 @@ class IMetrics * * @param[in] build : The build / version of this app * - * @retval The status + * @retval An ok Result on success, or an error; no value is returned */ virtual Result appInfo(const std::string& build) const = 0; }; 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-local.sh b/run-component-tests-local.sh new file mode 100755 index 0000000..1752276 --- /dev/null +++ b/run-component-tests-local.sh @@ -0,0 +1,151 @@ +#!/usr/bin/env bash + +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +PROTOCOL="rpc_v2" +MOCK_DIR="/tmp/mock-firebolt-firebolt-cpp-client" +IMAGE_TAG="firebolt-client-ci:local" +SKIP_IMAGE_BUILD="false" + +usage() { + cat < RPC protocol (default: rpc_v2) + --mock-dir Host directory for mock-firebolt cache + (default: /tmp/mock-firebolt-firebolt-cpp-client) + --image-tag Docker image tag to use/build + (default: firebolt-client-ci:local) + --skip-image-build Reuse existing image tag; do not run docker build + --help Show this help + +Examples: + ./run-component-tests-local.sh + ./run-component-tests-local.sh --protocol legacy + ./run-component-tests-local.sh --skip-image-build --image-tag firebolt-client-ci:local +EOF +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --protocol) + [[ $# -ge 2 ]] || { echo "Missing value for --protocol" >&2; exit 1; } + PROTOCOL="$2" + shift + ;; + --mock-dir) + [[ $# -ge 2 ]] || { echo "Missing value for --mock-dir" >&2; exit 1; } + MOCK_DIR="$2" + shift + ;; + --image-tag) + [[ $# -ge 2 ]] || { echo "Missing value for --image-tag" >&2; exit 1; } + IMAGE_TAG="$2" + shift + ;; + --skip-image-build) + SKIP_IMAGE_BUILD="true" + ;; + --help|-h) + usage + exit 0 + ;; + *) + echo "Unknown option: $1" >&2 + usage + exit 1 + ;; + esac + shift +done + +if [[ "$PROTOCOL" != "rpc_v2" && "$PROTOCOL" != "legacy" ]]; then + echo "Invalid --protocol '$PROTOCOL'. Expected 'rpc_v2' or 'legacy'." >&2 + exit 1 +fi + +cd "$ROOT_DIR" + +if [[ ! -f .transport.version ]]; then + echo "Missing .transport.version in $ROOT_DIR" >&2 + exit 1 +fi + +TRANSPORT_VERSION="$(cat .transport.version)" +MOCK_SHA1SUM="1fec7b75190e75ac8ea8ebf9f3e00c0a070b2566" +MOCK_BRANCH="topic/changes-for-bidirectional" +NODE_VERSION="24.11.0" + +mkdir -p "$MOCK_DIR" + +echo "[1/4] Docker image: $IMAGE_TAG" +if [[ "$SKIP_IMAGE_BUILD" == "false" ]]; then + docker build \ + -f "$ROOT_DIR/.github/Dockerfile" \ + -t "$IMAGE_TAG" \ + --build-arg "DEPS_TRANSPORT_V=$TRANSPORT_VERSION" \ + --build-arg "DEPS_TRANSPORT_PROTOCOL=$PROTOCOL" \ + "$ROOT_DIR" +else + echo "Skipping image build (--skip-image-build)" +fi + +echo "[2/4] Preparing mock-firebolt in $MOCK_DIR" +docker run --rm --user "$(id -u):$(id -g)" \ + -v "$ROOT_DIR:/workspace" \ + -v "$MOCK_DIR:/mock-host" \ + "$IMAGE_TAG" \ + bash -c ' + set -e + if [ ! -d /mock-host/.git ]; then + git clone --depth 1 --branch '"$MOCK_BRANCH"' \ + https://github.com/rdkcentral/mock-firebolt.git /mock-host + fi + cd /mock-host + git fetch --depth 1 origin '"$MOCK_SHA1SUM"' + git -c advice.detachedHead=false checkout '"$MOCK_SHA1SUM"' + source /usr/local/nvm/nvm.sh + nvm use --delete-prefix '"$NODE_VERSION"' >/dev/null + cd server + npm ci + ' + +BUILD_SUBDIR="build-docker" + +echo "[3/4] Building ctApp (build dir: $BUILD_SUBDIR)" +docker run --rm --user "$(id -u):$(id -g)" \ + -v "$ROOT_DIR:/workspace" \ + "$IMAGE_TAG" \ + bash -c ' + set -e + cd /workspace + rm -rf '"$BUILD_SUBDIR"' + cmake -B '"$BUILD_SUBDIR"' -S . -DCMAKE_BUILD_TYPE=Debug -DENABLE_TESTS=ON + cp docs/openrpc/the-spec/firebolt-open-rpc.json '"$BUILD_SUBDIR"'/test/ + cmake --build '"$BUILD_SUBDIR"' --parallel + chmod +x '"$BUILD_SUBDIR"'/test/ctApp + ' + +echo "[4/4] Running component tests" +docker run --rm --user "$(id -u):$(id -g)" \ + -v "$ROOT_DIR:/workspace" \ + -v "$MOCK_DIR:/mock" \ + "$IMAGE_TAG" \ + /workspace/.github/scripts/run-component-tests.sh \ + --mock /mock \ + --protocol "$PROTOCOL" \ + --config /workspace/.github/mock-firebolt/config.json \ + --openrpc /workspace/docs/openrpc/the-spec/firebolt-open-rpc.json \ + --app-openrpc /workspace/docs/openrpc/the-spec/firebolt-app-open-rpc.json \ + --test-exe /workspace/"$BUILD_SUBDIR"/test/ctApp + +echo "Done." 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..b699bca --- /dev/null +++ b/src/actions_impl.cpp @@ -0,0 +1,55 @@ +/** + * 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 + +namespace Firebolt::Actions +{ + +ActionsImpl::ActionsImpl(Firebolt::Helpers::IHelper& helper) + : helper_(helper), + subscriptionManager_(helper, this) +{ +} + +Result ActionsImpl::intent() const +{ + return helper_.get("Actions.intent"); +} + +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..4c8d6ba --- /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 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/discovery_impl.cpp b/src/discovery_impl.cpp index cc209c0..e653777 100644 --- a/src/discovery_impl.cpp +++ b/src/discovery_impl.cpp @@ -27,7 +27,7 @@ DiscoveryImpl::DiscoveryImpl(Firebolt::Helpers::IHelper& helper) { } -Result DiscoveryImpl::watched(const std::string& entityId, std::optional progress, +Result DiscoveryImpl::watched(const std::string& entityId, std::optional progress, std::optional completed, std::optional watchedOn, std::optional agePolicy) const { @@ -50,11 +50,6 @@ Result DiscoveryImpl::watched(const std::string& entityId, std::optional result = helper_.get("Discovery.watched", parameters); - if (!result) - { - return Result{result.error()}; - } - return Result{result.value()}; + return helper_.invoke("Discovery.watched", parameters); } } // namespace Firebolt::Discovery diff --git a/src/discovery_impl.h b/src/discovery_impl.h index 30a78f8..34e12f1 100644 --- a/src/discovery_impl.h +++ b/src/discovery_impl.h @@ -33,7 +33,7 @@ class DiscoveryImpl : public IDiscovery ~DiscoveryImpl() override = default; - Result watched(const std::string& entityId, std::optional progress, std::optional completed, + Result watched(const std::string& entityId, std::optional progress, std::optional completed, std::optional watchedOn, std::optional agePolicy) const override; 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/src/metrics_impl.cpp b/src/metrics_impl.cpp index 892be95..0e04139 100644 --- a/src/metrics_impl.cpp +++ b/src/metrics_impl.cpp @@ -28,22 +28,22 @@ MetricsImpl::MetricsImpl(Firebolt::Helpers::IHelper& helper) { } -Result MetricsImpl::ready() const +Result MetricsImpl::ready() const { - return helper_.get("Metrics.ready"); + return helper_.invoke("Metrics.ready", nlohmann::json()); } -Result MetricsImpl::signIn() const +Result MetricsImpl::signIn() const { - return helper_.get("Metrics.signIn"); + return helper_.invoke("Metrics.signIn", nlohmann::json()); } -Result MetricsImpl::signOut() const +Result MetricsImpl::signOut() const { - return helper_.get("Metrics.signOut"); + return helper_.invoke("Metrics.signOut", nlohmann::json()); } -Result MetricsImpl::startContent(const std::optional& entityId, +Result MetricsImpl::startContent(const std::optional& entityId, const std::optional agePolicy) const { nlohmann::json parameters; @@ -55,10 +55,10 @@ Result MetricsImpl::startContent(const std::optional& entityI { parameters["agePolicy"] = Firebolt::JSON::toString(Firebolt::JsonData::AgePolicyEnum, *agePolicy); } - return helper_.get("Metrics.startContent", parameters); + return helper_.invoke("Metrics.startContent", parameters); } -Result MetricsImpl::stopContent(const std::optional& entityId, +Result MetricsImpl::stopContent(const std::optional& entityId, const std::optional agePolicy) const { nlohmann::json parameters; @@ -70,10 +70,10 @@ Result MetricsImpl::stopContent(const std::optional& entityId { parameters["agePolicy"] = Firebolt::JSON::toString(Firebolt::JsonData::AgePolicyEnum, *agePolicy); } - return helper_.get("Metrics.stopContent", parameters); + return helper_.invoke("Metrics.stopContent", parameters); } -Result MetricsImpl::page(const std::string& pageId, const std::optional& agePolicy) const +Result MetricsImpl::page(const std::string& pageId, const std::optional& agePolicy) const { nlohmann::json parameters; parameters["pageId"] = pageId; @@ -81,10 +81,10 @@ Result MetricsImpl::page(const std::string& pageId, const std::optional("Metrics.page", parameters); + return helper_.invoke("Metrics.page", parameters); } -Result MetricsImpl::error(const ErrorType type, const std::string& code, const std::string& description, +Result MetricsImpl::error(const ErrorType type, const std::string& code, const std::string& description, const bool visible, const std::optional>& parameters, const std::optional& agePolicy) const { @@ -106,10 +106,10 @@ Result MetricsImpl::error(const ErrorType type, const std::string& code, c { jsonParameters["agePolicy"] = Firebolt::JSON::toString(Firebolt::JsonData::AgePolicyEnum, *agePolicy); } - return helper_.get("Metrics.error", jsonParameters); + return helper_.invoke("Metrics.error", jsonParameters); } -Result MetricsImpl::mediaLoadStart(const std::string& entityId, +Result MetricsImpl::mediaLoadStart(const std::string& entityId, const std::optional& agePolicy) const { nlohmann::json parameters; @@ -118,10 +118,10 @@ Result MetricsImpl::mediaLoadStart(const std::string& entityId, { parameters["agePolicy"] = Firebolt::JSON::toString(Firebolt::JsonData::AgePolicyEnum, *agePolicy); } - return helper_.get("Metrics.mediaLoadStart", parameters); + return helper_.invoke("Metrics.mediaLoadStart", parameters); } -Result MetricsImpl::mediaPlay(const std::string& entityId, const std::optional& agePolicy) const +Result MetricsImpl::mediaPlay(const std::string& entityId, const std::optional& agePolicy) const { nlohmann::json parameters; parameters["entityId"] = entityId; @@ -129,10 +129,10 @@ Result MetricsImpl::mediaPlay(const std::string& entityId, const std::opti { parameters["agePolicy"] = Firebolt::JSON::toString(Firebolt::JsonData::AgePolicyEnum, *agePolicy); } - return helper_.get("Metrics.mediaPlay", parameters); + return helper_.invoke("Metrics.mediaPlay", parameters); } -Result MetricsImpl::mediaPlaying(const std::string& entityId, +Result MetricsImpl::mediaPlaying(const std::string& entityId, const std::optional& agePolicy) const { nlohmann::json parameters; @@ -141,10 +141,10 @@ Result MetricsImpl::mediaPlaying(const std::string& entityId, { parameters["agePolicy"] = Firebolt::JSON::toString(Firebolt::JsonData::AgePolicyEnum, *agePolicy); } - return helper_.get("Metrics.mediaPlaying", parameters); + return helper_.invoke("Metrics.mediaPlaying", parameters); } -Result MetricsImpl::mediaPause(const std::string& entityId, const std::optional& agePolicy) const +Result MetricsImpl::mediaPause(const std::string& entityId, const std::optional& agePolicy) const { nlohmann::json parameters; parameters["entityId"] = entityId; @@ -152,10 +152,10 @@ Result MetricsImpl::mediaPause(const std::string& entityId, const std::opt { parameters["agePolicy"] = Firebolt::JSON::toString(Firebolt::JsonData::AgePolicyEnum, *agePolicy); } - return helper_.get("Metrics.mediaPause", parameters); + return helper_.invoke("Metrics.mediaPause", parameters); } -Result MetricsImpl::mediaWaiting(const std::string& entityId, +Result MetricsImpl::mediaWaiting(const std::string& entityId, const std::optional& agePolicy) const { nlohmann::json parameters; @@ -164,10 +164,10 @@ Result MetricsImpl::mediaWaiting(const std::string& entityId, { parameters["agePolicy"] = Firebolt::JSON::toString(Firebolt::JsonData::AgePolicyEnum, *agePolicy); } - return helper_.get("Metrics.mediaWaiting", parameters); + return helper_.invoke("Metrics.mediaWaiting", parameters); } -Result MetricsImpl::mediaSeeking(const std::string& entityId, const double target, +Result MetricsImpl::mediaSeeking(const std::string& entityId, const double target, const std::optional& agePolicy) const { nlohmann::json parameters; @@ -177,10 +177,10 @@ Result MetricsImpl::mediaSeeking(const std::string& entityId, const double { parameters["agePolicy"] = Firebolt::JSON::toString(Firebolt::JsonData::AgePolicyEnum, *agePolicy); } - return helper_.get("Metrics.mediaSeeking", parameters); + return helper_.invoke("Metrics.mediaSeeking", parameters); } -Result MetricsImpl::mediaSeeked(const std::string& entityId, const double position, +Result MetricsImpl::mediaSeeked(const std::string& entityId, const double position, const std::optional& agePolicy) const { nlohmann::json parameters; @@ -190,10 +190,10 @@ Result MetricsImpl::mediaSeeked(const std::string& entityId, const double { parameters["agePolicy"] = Firebolt::JSON::toString(Firebolt::JsonData::AgePolicyEnum, *agePolicy); } - return helper_.get("Metrics.mediaSeeked", parameters); + return helper_.invoke("Metrics.mediaSeeked", parameters); } -Result MetricsImpl::mediaRateChanged(const std::string& entityId, const double rate, +Result MetricsImpl::mediaRateChanged(const std::string& entityId, const double rate, const std::optional& agePolicy) const { nlohmann::json parameters; @@ -203,10 +203,10 @@ Result MetricsImpl::mediaRateChanged(const std::string& entityId, const do { parameters["agePolicy"] = Firebolt::JSON::toString(Firebolt::JsonData::AgePolicyEnum, *agePolicy); } - return helper_.get("Metrics.mediaRateChanged", parameters); + return helper_.invoke("Metrics.mediaRateChanged", parameters); } -Result MetricsImpl::mediaRenditionChanged(const std::string& entityId, const unsigned bitrate, const unsigned width, +Result MetricsImpl::mediaRenditionChanged(const std::string& entityId, const unsigned bitrate, const unsigned width, const unsigned height, const std::optional& profile, const std::optional& agePolicy) const { @@ -223,10 +223,10 @@ Result MetricsImpl::mediaRenditionChanged(const std::string& entityId, con { parameters["agePolicy"] = Firebolt::JSON::toString(Firebolt::JsonData::AgePolicyEnum, *agePolicy); } - return helper_.get("Metrics.mediaRenditionChanged", parameters); + return helper_.invoke("Metrics.mediaRenditionChanged", parameters); } -Result MetricsImpl::mediaEnded(const std::string& entityId, const std::optional& agePolicy) const +Result MetricsImpl::mediaEnded(const std::string& entityId, const std::optional& agePolicy) const { nlohmann::json parameters; parameters["entityId"] = entityId; @@ -234,10 +234,10 @@ Result MetricsImpl::mediaEnded(const std::string& entityId, const std::opt { parameters["agePolicy"] = Firebolt::JSON::toString(Firebolt::JsonData::AgePolicyEnum, *agePolicy); } - return helper_.get("Metrics.mediaEnded", parameters); + return helper_.invoke("Metrics.mediaEnded", parameters); } -Result MetricsImpl::event(const std::string& schema, const std::string& data, +Result MetricsImpl::event(const std::string& schema, const std::string& data, const std::optional& agePolicy) const { nlohmann::json parameters; @@ -247,7 +247,7 @@ Result MetricsImpl::event(const std::string& schema, const std::string& da { parameters["agePolicy"] = Firebolt::JSON::toString(Firebolt::JsonData::AgePolicyEnum, *agePolicy); } - return helper_.get("Metrics.event", parameters); + return helper_.invoke("Metrics.event", parameters); } Result MetricsImpl::appInfo(const std::string& build) const diff --git a/src/metrics_impl.h b/src/metrics_impl.h index 4eb0da6..ace303b 100644 --- a/src/metrics_impl.h +++ b/src/metrics_impl.h @@ -32,39 +32,39 @@ class MetricsImpl : public IMetrics ~MetricsImpl() override = default; - Result ready() const override; - Result signIn() const override; - Result signOut() const override; - Result startContent(const std::optional& entityId, + Result ready() const override; + Result signIn() const override; + Result signOut() const override; + Result startContent(const std::optional& entityId, const std::optional agePolicy) const override; - Result stopContent(const std::optional& entityId, + Result stopContent(const std::optional& entityId, const std::optional agePolicy) const override; - Result page(const std::string& pageId, const std::optional& agePolicy) const override; - Result error(const ErrorType type, const std::string& code, const std::string& description, + Result page(const std::string& pageId, const std::optional& agePolicy) const override; + Result error(const ErrorType type, const std::string& code, const std::string& description, const bool visible, const std::optional>& parameters, const std::optional& agePolicy) const override; - Result mediaLoadStart(const std::string& entityId, + Result mediaLoadStart(const std::string& entityId, const std::optional& agePolicy) const override; - Result mediaPlay(const std::string& entityId, + Result mediaPlay(const std::string& entityId, const std::optional& agePolicy) const override; - Result mediaPlaying(const std::string& entityId, + Result mediaPlaying(const std::string& entityId, const std::optional& agePolicy) const override; - Result mediaPause(const std::string& entityId, + Result mediaPause(const std::string& entityId, const std::optional& agePolicy) const override; - Result mediaWaiting(const std::string& entityId, + Result mediaWaiting(const std::string& entityId, const std::optional& agePolicy) const override; - Result mediaSeeking(const std::string& entityId, const double target, + Result mediaSeeking(const std::string& entityId, const double target, const std::optional& agePolicy) const override; - Result mediaSeeked(const std::string& entityId, const double position, + Result mediaSeeked(const std::string& entityId, const double position, const std::optional& agePolicy) const override; - Result mediaRateChanged(const std::string& entityId, const double rate, + Result mediaRateChanged(const std::string& entityId, const double rate, const std::optional& agePolicy) const override; - Result mediaRenditionChanged(const std::string& entityId, const unsigned bitrate, const unsigned width, + Result mediaRenditionChanged(const std::string& entityId, const unsigned bitrate, const unsigned width, const unsigned height, const std::optional& profile, const std::optional& agePolicy) const override; - Result mediaEnded(const std::string& entityId, + Result mediaEnded(const std::string& entityId, const std::optional& agePolicy) const override; - Result event(const std::string& schema, const std::string& data, + Result event(const std::string& schema, const std::string& data, const std::optional& agePolicy) const override; Result appInfo(const std::string& build) const override; 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/api_test_app/apis/discoveryDemo.cpp b/test/api_test_app/apis/discoveryDemo.cpp index 0e3afba..5e0b9d6 100644 --- a/test/api_test_app/apis/discoveryDemo.cpp +++ b/test/api_test_app/apis/discoveryDemo.cpp @@ -61,7 +61,7 @@ void DiscoveryDemo::runOption(const std::string& method) watchedOn, agePolicyOpt); if (succeed(r)) { - std::cout << "Discovery.watched result: " << std::boolalpha << *r << std::endl; + std::cout << "Discovery.watched: Success" << std::endl; } } } diff --git a/test/api_test_app/apis/metricsDemo.cpp b/test/api_test_app/apis/metricsDemo.cpp index e494cd0..9ac520d 100644 --- a/test/api_test_app/apis/metricsDemo.cpp +++ b/test/api_test_app/apis/metricsDemo.cpp @@ -56,7 +56,7 @@ void MetricsDemo::runOption(const std::string& method) auto r = Firebolt::IFireboltAccessor::Instance().MetricsInterface().ready(); if (succeed(r)) { - std::cout << "Metrics Ready: " << std::boolalpha << *r << std::endl; + std::cout << "Metrics Ready: Success" << std::endl; } } else if (method == "Metrics.signIn") @@ -64,7 +64,7 @@ void MetricsDemo::runOption(const std::string& method) auto r = Firebolt::IFireboltAccessor::Instance().MetricsInterface().signIn(); if (succeed(r)) { - std::cout << "Metrics Sign In: " << std::boolalpha << *r << std::endl; + std::cout << "Metrics Sign In: Success" << std::endl; } } else if (method == "Metrics.signOut") @@ -72,7 +72,7 @@ void MetricsDemo::runOption(const std::string& method) auto r = Firebolt::IFireboltAccessor::Instance().MetricsInterface().signOut(); if (succeed(r)) { - std::cout << "Metrics Sign Out: " << std::boolalpha << *r << std::endl; + std::cout << "Metrics Sign Out: Success" << std::endl; } } else if (method == "Metrics.startContent") @@ -80,7 +80,7 @@ void MetricsDemo::runOption(const std::string& method) auto r = Firebolt::IFireboltAccessor::Instance().MetricsInterface().startContent("entity123", std::nullopt); if (succeed(r)) { - std::cout << "Metrics Start Content: " << std::boolalpha << *r << std::endl; + std::cout << "Metrics Start Content: Success" << std::endl; } } else if (method == "Metrics.stopContent") @@ -88,7 +88,7 @@ void MetricsDemo::runOption(const std::string& method) auto r = Firebolt::IFireboltAccessor::Instance().MetricsInterface().stopContent("entity123", std::nullopt); if (succeed(r)) { - std::cout << "Metrics Stop Content: " << std::boolalpha << *r << std::endl; + std::cout << "Metrics Stop Content: Success" << std::endl; } } else if (method == "Metrics.page") @@ -96,7 +96,7 @@ void MetricsDemo::runOption(const std::string& method) auto r = Firebolt::IFireboltAccessor::Instance().MetricsInterface().page("page123", std::nullopt); if (succeed(r)) { - std::cout << "Metrics Page: " << std::boolalpha << *r << std::endl; + std::cout << "Metrics Page: Success" << std::endl; } } else if (method == "Metrics.error") @@ -106,7 +106,7 @@ void MetricsDemo::runOption(const std::string& method) std::nullopt, std::nullopt); if (succeed(r)) { - std::cout << "Metrics Error: " << std::boolalpha << *r << std::endl; + std::cout << "Metrics Error: Success" << std::endl; } } else if (method == "Metrics.mediaLoadStart") @@ -114,7 +114,7 @@ void MetricsDemo::runOption(const std::string& method) auto r = Firebolt::IFireboltAccessor::Instance().MetricsInterface().mediaLoadStart("entity123", std::nullopt); if (succeed(r)) { - std::cout << "Metrics Media Load Start: " << std::boolalpha << *r << std::endl; + std::cout << "Metrics Media Load Start: Success" << std::endl; } } else if (method == "Metrics.mediaPlay") @@ -122,7 +122,7 @@ void MetricsDemo::runOption(const std::string& method) auto r = Firebolt::IFireboltAccessor::Instance().MetricsInterface().mediaPlay("entity123", std::nullopt); if (succeed(r)) { - std::cout << "Metrics Media Play: " << std::boolalpha << *r << std::endl; + std::cout << "Metrics Media Play: Success" << std::endl; } } else if (method == "Metrics.mediaPlaying") @@ -130,7 +130,7 @@ void MetricsDemo::runOption(const std::string& method) auto r = Firebolt::IFireboltAccessor::Instance().MetricsInterface().mediaPlaying("entity123", std::nullopt); if (succeed(r)) { - std::cout << "Metrics Media Playing: " << std::boolalpha << *r << std::endl; + std::cout << "Metrics Media Playing: Success" << std::endl; } } else if (method == "Metrics.mediaPause") @@ -138,7 +138,7 @@ void MetricsDemo::runOption(const std::string& method) auto r = Firebolt::IFireboltAccessor::Instance().MetricsInterface().mediaPause("entity123", std::nullopt); if (succeed(r)) { - std::cout << "Metrics Media Pause: " << std::boolalpha << *r << std::endl; + std::cout << "Metrics Media Pause: Success" << std::endl; } } else if (method == "Metrics.mediaWaiting") @@ -146,7 +146,7 @@ void MetricsDemo::runOption(const std::string& method) auto r = Firebolt::IFireboltAccessor::Instance().MetricsInterface().mediaWaiting("entity123", std::nullopt); if (succeed(r)) { - std::cout << "Metrics Media Waiting: " << std::boolalpha << *r << std::endl; + std::cout << "Metrics Media Waiting: Success" << std::endl; } } else if (method == "Metrics.mediaSeeking") @@ -154,7 +154,7 @@ void MetricsDemo::runOption(const std::string& method) auto r = Firebolt::IFireboltAccessor::Instance().MetricsInterface().mediaSeeking("entity123", 0.5, std::nullopt); if (succeed(r)) { - std::cout << "Metrics Media Seeking: " << std::boolalpha << *r << std::endl; + std::cout << "Metrics Media Seeking: Success" << std::endl; } } else if (method == "Metrics.mediaSeeked") @@ -162,7 +162,7 @@ void MetricsDemo::runOption(const std::string& method) auto r = Firebolt::IFireboltAccessor::Instance().MetricsInterface().mediaSeeked("entity123", 0.5, std::nullopt); if (succeed(r)) { - std::cout << "Metrics Media Seeked: " << std::boolalpha << *r << std::endl; + std::cout << "Metrics Media Seeked: Success" << std::endl; } } else if (method == "Metrics.mediaRateChanged") @@ -171,7 +171,7 @@ void MetricsDemo::runOption(const std::string& method) Firebolt::IFireboltAccessor::Instance().MetricsInterface().mediaRateChanged("entity123", 1.5, std::nullopt); if (succeed(r)) { - std::cout << "Metrics Media Rate Changed: " << std::boolalpha << *r << std::endl; + std::cout << "Metrics Media Rate Changed: Success" << std::endl; } } else if (method == "Metrics.mediaRenditionChanged") @@ -181,7 +181,7 @@ void MetricsDemo::runOption(const std::string& method) std::nullopt); if (succeed(r)) { - std::cout << "Metrics Media Rendition Changed: " << std::boolalpha << *r << std::endl; + std::cout << "Metrics Media Rendition Changed: Success" << std::endl; } } else if (method == "Metrics.mediaEnded") @@ -189,7 +189,7 @@ void MetricsDemo::runOption(const std::string& method) auto r = Firebolt::IFireboltAccessor::Instance().MetricsInterface().mediaEnded("entity123", std::nullopt); if (succeed(r)) { - std::cout << "Metrics Media Ended: " << std::boolalpha << *r << std::endl; + std::cout << "Metrics Media Ended: Success" << std::endl; } } else if (method == "Metrics.event") @@ -198,7 +198,7 @@ void MetricsDemo::runOption(const std::string& method) "{\"key\":\"value\"}", std::nullopt); if (succeed(r)) { - std::cout << "Metrics Event: " << std::boolalpha << *r << std::endl; + std::cout << "Metrics Event: Success" << std::endl; } } else if (method == "Metrics.appInfo") diff --git a/test/component/actionsGeneratedTest.cpp b/test/component/actionsGeneratedTest.cpp new file mode 100644 index 0000000..38a5356 --- /dev/null +++ b/test/component/actionsGeneratedTest.cpp @@ -0,0 +1,63 @@ +/** + * 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 +#include +#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(); + ASSERT_TRUE(result) << toError(result); + EXPECT_EQ(*result, "launch"); +} + +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"("launch")"); + verifyEventReceived(mtx, cv, eventReceived); + + auto result = Firebolt::IFireboltAccessor::Instance().ActionsInterface().unsubscribe(id.value()); + verifyUnsubscribeResult(result); +} diff --git a/test/component/discoveryTest.cpp b/test/component/discoveryTest.cpp index bc118b6..c1cfcf1 100644 --- a/test/component/discoveryTest.cpp +++ b/test/component/discoveryTest.cpp @@ -18,12 +18,10 @@ #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) @@ -32,7 +30,4 @@ TEST_F(DiscoveryCTest, Watched) "2024-10-01T12:00:00Z", Firebolt::AgePolicy::ADULT); ASSERT_TRUE(result) << "Failed to call watched"; - - auto expectedValue = jsonEngine.get_value("Discovery.watched"); - EXPECT_EQ(*result, expectedValue.get()); } diff --git a/test/component/metricsTest.cpp b/test/component/metricsTest.cpp index e626a6d..9e1d6fc 100644 --- a/test/component/metricsTest.cpp +++ b/test/component/metricsTest.cpp @@ -17,7 +17,6 @@ */ #include "firebolt/firebolt.h" -#include "json_engine.h" #include "utils.h" #include @@ -25,235 +24,185 @@ class MetricsCTest : public ::testing::Test { protected: void SetUp() override {} - - JsonEngine jsonEngine; }; TEST_F(MetricsCTest, Ready) { - auto expectedValue = jsonEngine.get_value("Metrics.ready"); auto result = Firebolt::IFireboltAccessor::Instance().MetricsInterface().ready(); ASSERT_TRUE(result) << "MetricsImpl::ready() returned an error"; - EXPECT_EQ(*result, expectedValue); } TEST_F(MetricsCTest, SignIn) { - auto expectedValue = jsonEngine.get_value("Metrics.signIn"); auto result = Firebolt::IFireboltAccessor::Instance().MetricsInterface().signIn(); ASSERT_TRUE(result) << "MetricsImpl::signIn() returned an error"; - EXPECT_EQ(*result, expectedValue); } TEST_F(MetricsCTest, SignOut) { - auto expectedValue = jsonEngine.get_value("Metrics.signOut"); auto result = Firebolt::IFireboltAccessor::Instance().MetricsInterface().signOut(); ASSERT_TRUE(result) << "MetricsImpl::signOut() returned an error"; - EXPECT_EQ(*result, expectedValue); } TEST_F(MetricsCTest, StartContent) { - auto expectedValue = jsonEngine.get_value("Metrics.startContent"); auto result = Firebolt::IFireboltAccessor::Instance().MetricsInterface().startContent("entity123", Firebolt::AgePolicy::ADULT); ASSERT_TRUE(result) << "MetricsImpl::startContent() returned an error"; - EXPECT_EQ(*result, expectedValue); } TEST_F(MetricsCTest, StopContent) { - auto expectedValue = jsonEngine.get_value("Metrics.stopContent"); auto result = Firebolt::IFireboltAccessor::Instance().MetricsInterface().stopContent("entity123", Firebolt::AgePolicy::ADULT); ASSERT_TRUE(result) << "MetricsImpl::stopContent() returned an error"; - EXPECT_EQ(*result, expectedValue); } TEST_F(MetricsCTest, Page) { - auto expectedValue = jsonEngine.get_value("Metrics.page"); auto result = Firebolt::IFireboltAccessor::Instance().MetricsInterface().page("homePage", Firebolt::AgePolicy::ADULT); ASSERT_TRUE(result) << "MetricsImpl::page() returned an error"; - EXPECT_EQ(*result, expectedValue); } TEST_F(MetricsCTest, Error) { - auto expectedValue = jsonEngine.get_value("Metrics.error"); auto result = Firebolt::IFireboltAccessor::Instance() .MetricsInterface() .error(Firebolt::Metrics::ErrorType::Network, "ERR001", "Network error occurred", true, std::map{{"param1", "value1"}, {"param2", "value2"}}, Firebolt::AgePolicy::ADULT); ASSERT_TRUE(result) << "MetricsImpl::error() returned an error"; - EXPECT_EQ(*result, expectedValue); } TEST_F(MetricsCTest, ErrorNoParameters) { - auto expectedValue = jsonEngine.get_value("Metrics.error"); auto result = Firebolt::IFireboltAccessor::Instance().MetricsInterface().error(Firebolt::Metrics::ErrorType::Network, "ERR001", "Network error occurred", true, std::nullopt, Firebolt::AgePolicy::ADULT); ASSERT_TRUE(result) << "MetricsImpl::error() returned an error"; - EXPECT_EQ(*result, expectedValue); } TEST_F(MetricsCTest, ErrorNoAgePolicy) { - auto expectedValue = jsonEngine.get_value("Metrics.error"); auto result = Firebolt::IFireboltAccessor::Instance() .MetricsInterface() .error(Firebolt::Metrics::ErrorType::Network, "ERR001", "Network error occurred", true, std::map{{"param1", "value1"}, {"param2", "value2"}}, std::nullopt); ASSERT_TRUE(result) << "MetricsImpl::error() returned an error"; - EXPECT_EQ(*result, expectedValue); } TEST_F(MetricsCTest, MediaLoadStart) { - auto expectedValue = jsonEngine.get_value("Metrics.mediaLoadStart"); auto result = Firebolt::IFireboltAccessor::Instance().MetricsInterface().mediaLoadStart("mediaEntity123", Firebolt::AgePolicy::ADULT); ASSERT_TRUE(result) << "MetricsImpl::mediaLoadStart() returned an error"; - EXPECT_EQ(*result, expectedValue); } TEST_F(MetricsCTest, MediaPlay) { - auto expectedValue = jsonEngine.get_value("Metrics.mediaPlay"); auto result = Firebolt::IFireboltAccessor::Instance().MetricsInterface().mediaPlay("mediaEntity123", Firebolt::AgePolicy::ADULT); ASSERT_TRUE(result) << "MetricsImpl::mediaPlay() returned an error"; - EXPECT_EQ(*result, expectedValue); } TEST_F(MetricsCTest, MediaPlaying) { - auto expectedValue = jsonEngine.get_value("Metrics.mediaPlaying"); auto result = Firebolt::IFireboltAccessor::Instance().MetricsInterface().mediaPlaying("mediaEntity123", Firebolt::AgePolicy::ADULT); ASSERT_TRUE(result) << "MetricsImpl::mediaPlaying() returned an error"; - EXPECT_EQ(*result, expectedValue); } TEST_F(MetricsCTest, MediaPause) { - auto expectedValue = jsonEngine.get_value("Metrics.mediaPause"); auto result = Firebolt::IFireboltAccessor::Instance().MetricsInterface().mediaPause("mediaEntity123", Firebolt::AgePolicy::ADULT); ASSERT_TRUE(result) << "MetricsImpl::mediaPause() returned an error"; - EXPECT_EQ(*result, expectedValue); } TEST_F(MetricsCTest, MediaWaiting) { - auto expectedValue = jsonEngine.get_value("Metrics.mediaWaiting"); auto result = Firebolt::IFireboltAccessor::Instance().MetricsInterface().mediaWaiting("mediaEntity123", Firebolt::AgePolicy::ADULT); ASSERT_TRUE(result) << "MetricsImpl::mediaWaiting() returned an error"; - EXPECT_EQ(*result, expectedValue); } TEST_F(MetricsCTest, MediaSeeking) { - auto expectedValue = jsonEngine.get_value("Metrics.mediaSeeking"); auto result = Firebolt::IFireboltAccessor::Instance().MetricsInterface().mediaSeeking("mediaEntity123", 0.5, Firebolt::AgePolicy::ADULT); ASSERT_TRUE(result) << "MetricsImpl::mediaSeeking() returned an error"; - EXPECT_EQ(*result, expectedValue); } TEST_F(MetricsCTest, MediaSeekingInt) { - auto expectedValue = jsonEngine.get_value("Metrics.mediaSeeking"); auto result = Firebolt::IFireboltAccessor::Instance().MetricsInterface().mediaSeeking("mediaEntity123", 500, Firebolt::AgePolicy::ADULT); ASSERT_TRUE(result) << "MetricsImpl::mediaSeeking() returned an error"; - EXPECT_EQ(*result, expectedValue); } TEST_F(MetricsCTest, MediaSeeked) { - auto expectedValue = jsonEngine.get_value("Metrics.mediaSeeked"); auto result = Firebolt::IFireboltAccessor::Instance().MetricsInterface().mediaSeeked("mediaEntity123", 0.5, Firebolt::AgePolicy::ADULT); ASSERT_TRUE(result) << "MetricsImpl::mediaSeeked() returned an error"; - EXPECT_EQ(*result, expectedValue); } TEST_F(MetricsCTest, MediaSeekedInt) { - auto expectedValue = jsonEngine.get_value("Metrics.mediaSeeked"); auto result = Firebolt::IFireboltAccessor::Instance().MetricsInterface().mediaSeeked("mediaEntity123", 500, Firebolt::AgePolicy::ADULT); ASSERT_TRUE(result) << "MetricsImpl::mediaSeeked() returned an error"; - EXPECT_EQ(*result, expectedValue); } TEST_F(MetricsCTest, MediaRateChanged) { - auto expectedValue = jsonEngine.get_value("Metrics.mediaRateChanged"); auto result = Firebolt::IFireboltAccessor::Instance().MetricsInterface().mediaRateChanged("mediaEntity123", 1.5, Firebolt::AgePolicy::ADULT); ASSERT_TRUE(result) << "MetricsImpl::mediaRateChanged() returned an error"; - EXPECT_EQ(*result, expectedValue); } TEST_F(MetricsCTest, MediaRenditionChanged) { - auto expectedValue = jsonEngine.get_value("Metrics.mediaRenditionChanged"); auto result = Firebolt::IFireboltAccessor::Instance().MetricsInterface().mediaRenditionChanged("mediaEntity123", 3000, 1920, 1080, "HDR", Firebolt::AgePolicy::ADULT); ASSERT_TRUE(result) << "MetricsImpl::mediaRenditionChanged() returned an error"; - EXPECT_EQ(*result, expectedValue); } TEST_F(MetricsCTest, MediaRenditionChangedNoProfile) { - auto expectedValue = jsonEngine.get_value("Metrics.mediaRenditionChanged"); auto result = Firebolt::IFireboltAccessor::Instance().MetricsInterface().mediaRenditionChanged("mediaEntity123", 3000, 1920, 1080, std::nullopt, Firebolt::AgePolicy::ADULT); ASSERT_TRUE(result) << "MetricsImpl::mediaRenditionChanged() returned an error"; - EXPECT_EQ(*result, expectedValue); } TEST_F(MetricsCTest, MediaRenditionChangedNoAgePolicy) { - auto expectedValue = jsonEngine.get_value("Metrics.mediaRenditionChanged"); auto result = Firebolt::IFireboltAccessor::Instance().MetricsInterface().mediaRenditionChanged("mediaEntity123", 3000, 1920, 1080, std::nullopt, std::nullopt); ASSERT_TRUE(result) << "MetricsImpl::mediaRenditionChanged() returned an error"; - EXPECT_EQ(*result, expectedValue); } TEST_F(MetricsCTest, MediaEnded) { - auto expectedValue = jsonEngine.get_value("Metrics.mediaEnded"); auto result = Firebolt::IFireboltAccessor::Instance().MetricsInterface().mediaEnded("mediaEntity123", Firebolt::AgePolicy::ADULT); ASSERT_TRUE(result) << "MetricsImpl::mediaEnded() returned an error"; - EXPECT_EQ(*result, expectedValue); } TEST_F(MetricsCTest, Event) { - auto expectedValue = jsonEngine.get_value("Metrics.event"); auto result = Firebolt::IFireboltAccessor::Instance().MetricsInterface().event("https://com.example.event", "{\"key\":\"value\"}", Firebolt::AgePolicy::ADULT); ASSERT_TRUE(result) << "MetricsImpl::event() returned an error"; - EXPECT_EQ(*result, expectedValue); } TEST_F(MetricsCTest, AppInfo) diff --git a/test/unit/actionsGeneratedTest.cpp b/test/unit/actionsGeneratedTest.cpp new file mode 100644 index 0000000..a9f8ba7 --- /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, getJson("Actions.intent", ::testing::_)) + .WillOnce(::testing::Invoke( + [](const std::string& /*method*/, const nlohmann::json& params) + { + EXPECT_TRUE(params.is_null() || (params.is_object() && params.empty())) + << "Actions.intent getter should not send request params"; + return Firebolt::Result{Firebolt::Error::General}; + })); + + auto result = impl.intent(); + EXPECT_FALSE(result) << "Expected error propagation when helper getJson fails"; +} diff --git a/test/unit/actionsTest.cpp b/test/unit/actionsTest.cpp new file mode 100644 index 0000000..1b87480 --- /dev/null +++ b/test/unit/actionsTest.cpp @@ -0,0 +1,50 @@ +/** + * 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) +{ + mock_with_response("Actions.intent", "launch"); + + auto result = actionsImpl_.intent(); + ASSERT_TRUE(result) << "ActionsImpl::intent() returned an error"; + EXPECT_EQ(*result, "launch"); +} + +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/unit/discoveryTest.cpp b/test/unit/discoveryTest.cpp index 4e1c3ce..9b7b1b1 100644 --- a/test/unit/discoveryTest.cpp +++ b/test/unit/discoveryTest.cpp @@ -36,7 +36,7 @@ TEST_F(DiscoveryUTest, checkEnums) TEST_F(DiscoveryUTest, watched) { - mock("Discovery.watched"); + mockInvoke("Discovery.watched"); std::string entityId = "content123"; std::optional progress = 0.75f; std::optional completed = true; @@ -44,9 +44,6 @@ TEST_F(DiscoveryUTest, watched) std::optional agePolicy = Firebolt::AgePolicy::ADULT; auto result = discoveryImpl_.watched(entityId, progress, completed, watchedOn, agePolicy); ASSERT_TRUE(result) << "Error on watched"; - Firebolt::JSON::Boolean boolJson; - boolJson.fromJson(jsonEngine.get_value("Discovery.watched")); - EXPECT_EQ(boolJson.value(), *result); } TEST_F(DiscoveryUTest, watched_payload) @@ -57,14 +54,13 @@ TEST_F(DiscoveryUTest, watched_payload) expected["completed"] = true; expected["watchedOn"] = "2024-06-01T12:00:00Z"; expected["agePolicy"] = "app:adult"; - EXPECT_CALL(mockHelper, getJson("Discovery.watched", _)) + EXPECT_CALL(mockHelper, invoke("Discovery.watched", _)) .WillOnce(Invoke( [&](const std::string& /* methodName */, const nlohmann::json& parameters) { - bool res = parameters == expected; EXPECT_EQ(parameters, expected) << "Parameters do not match expected payload: " << expected.dump() << " but got: " << parameters.dump(); - return Firebolt::Result{res}; + return Firebolt::Result{Firebolt::Error::None}; })); std::string entityId = "content123"; std::optional progress = 0.75f; @@ -73,5 +69,4 @@ TEST_F(DiscoveryUTest, watched_payload) std::optional agePolicy = Firebolt::AgePolicy::ADULT; auto result = discoveryImpl_.watched(entityId, progress, completed, watchedOn, agePolicy); ASSERT_TRUE(result) << "Error on watched"; - EXPECT_EQ(true, *result); } diff --git a/test/unit/metricsTest.cpp b/test/unit/metricsTest.cpp index 321c6d1..a7dbcab 100644 --- a/test/unit/metricsTest.cpp +++ b/test/unit/metricsTest.cpp @@ -33,49 +33,49 @@ TEST_F(MetricsUTest, checkEnums) TEST_F(MetricsUTest, Ready) { - mock("Metrics.ready"); + mockInvoke("Metrics.ready"); auto result = metricsImpl_.ready(); EXPECT_TRUE(result); } TEST_F(MetricsUTest, SignIn) { - mock("Metrics.signIn"); + mockInvoke("Metrics.signIn"); auto result = metricsImpl_.signIn(); EXPECT_TRUE(result); } TEST_F(MetricsUTest, SignOut) { - mock("Metrics.signOut"); + mockInvoke("Metrics.signOut"); auto result = metricsImpl_.signOut(); EXPECT_TRUE(result); } TEST_F(MetricsUTest, StartContent) { - mock("Metrics.startContent"); + mockInvoke("Metrics.startContent"); auto result = metricsImpl_.startContent("entity123", Firebolt::AgePolicy::ADULT); EXPECT_TRUE(result); } TEST_F(MetricsUTest, StopContent) { - mock("Metrics.stopContent"); + mockInvoke("Metrics.stopContent"); auto result = metricsImpl_.stopContent("entity123", Firebolt::AgePolicy::ADULT); EXPECT_TRUE(result); } TEST_F(MetricsUTest, Page) { - mock("Metrics.page"); + mockInvoke("Metrics.page"); auto result = metricsImpl_.page("homePage", Firebolt::AgePolicy::ADULT); EXPECT_TRUE(result); } TEST_F(MetricsUTest, Error) { - mock("Metrics.error"); + mockInvoke("Metrics.error"); auto result = metricsImpl_.error(Firebolt::Metrics::ErrorType::Network, "ERR001", "Network error occurred", true, std::map{{"param1", "value1"}, {"param2", "value2"}}, Firebolt::AgePolicy::ADULT); @@ -84,7 +84,7 @@ TEST_F(MetricsUTest, Error) TEST_F(MetricsUTest, ErrorNoParameters) { - mock("Metrics.error"); + mockInvoke("Metrics.error"); auto result = metricsImpl_.error(Firebolt::Metrics::ErrorType::Network, "ERR001", "Network error occurred", true, std::nullopt, Firebolt::AgePolicy::ADULT); EXPECT_TRUE(result); @@ -92,7 +92,7 @@ TEST_F(MetricsUTest, ErrorNoParameters) TEST_F(MetricsUTest, ErrorNoAgePolicy) { - mock("Metrics.error"); + mockInvoke("Metrics.error"); auto result = metricsImpl_.error(Firebolt::Metrics::ErrorType::Network, "ERR001", "Network error occurred", true, std::map{{"param1", "value1"}, {"param2", "value2"}}, std::nullopt); @@ -101,63 +101,63 @@ TEST_F(MetricsUTest, ErrorNoAgePolicy) TEST_F(MetricsUTest, MediaLoadStart) { - mock("Metrics.mediaLoadStart"); + mockInvoke("Metrics.mediaLoadStart"); auto result = metricsImpl_.mediaLoadStart("mediaEntity123", Firebolt::AgePolicy::ADULT); EXPECT_TRUE(result); } TEST_F(MetricsUTest, MediaPlay) { - mock("Metrics.mediaPlay"); + mockInvoke("Metrics.mediaPlay"); auto result = metricsImpl_.mediaPlay("mediaEntity123", Firebolt::AgePolicy::ADULT); EXPECT_TRUE(result); } TEST_F(MetricsUTest, MediaPlaying) { - mock("Metrics.mediaPlaying"); + mockInvoke("Metrics.mediaPlaying"); auto result = metricsImpl_.mediaPlaying("mediaEntity123", Firebolt::AgePolicy::ADULT); EXPECT_TRUE(result); } TEST_F(MetricsUTest, MediaPause) { - mock("Metrics.mediaPause"); + mockInvoke("Metrics.mediaPause"); auto result = metricsImpl_.mediaPause("mediaEntity123", Firebolt::AgePolicy::ADULT); EXPECT_TRUE(result); } TEST_F(MetricsUTest, MediaWaiting) { - mock("Metrics.mediaWaiting"); + mockInvoke("Metrics.mediaWaiting"); auto result = metricsImpl_.mediaWaiting("mediaEntity123", Firebolt::AgePolicy::ADULT); EXPECT_TRUE(result); } TEST_F(MetricsUTest, MediaSeeking) { - mock("Metrics.mediaSeeking"); + mockInvoke("Metrics.mediaSeeking"); auto result = metricsImpl_.mediaSeeking("mediaEntity123", 0.5, Firebolt::AgePolicy::ADULT); EXPECT_TRUE(result); } TEST_F(MetricsUTest, MediaSeeked) { - mock("Metrics.mediaSeeked"); + mockInvoke("Metrics.mediaSeeked"); auto result = metricsImpl_.mediaSeeked("mediaEntity123", 0.5, Firebolt::AgePolicy::ADULT); EXPECT_TRUE(result); } TEST_F(MetricsUTest, MediaRateChanged) { - mock("Metrics.mediaRateChanged"); + mockInvoke("Metrics.mediaRateChanged"); auto result = metricsImpl_.mediaRateChanged("mediaEntity123", 1.5, Firebolt::AgePolicy::ADULT); EXPECT_TRUE(result); } TEST_F(MetricsUTest, MediaRenditionChanged) { - mock("Metrics.mediaRenditionChanged"); + mockInvoke("Metrics.mediaRenditionChanged"); auto result = metricsImpl_.mediaRenditionChanged("mediaEntity123", 3000, 1920, 1080, "HDR", Firebolt::AgePolicy::ADULT); EXPECT_TRUE(result); @@ -165,14 +165,14 @@ TEST_F(MetricsUTest, MediaRenditionChanged) TEST_F(MetricsUTest, MediaEnded) { - mock("Metrics.mediaEnded"); + mockInvoke("Metrics.mediaEnded"); auto result = metricsImpl_.mediaEnded("mediaEntity123", Firebolt::AgePolicy::ADULT); EXPECT_TRUE(result); } TEST_F(MetricsUTest, Event) { - mock("Metrics.event"); + mockInvoke("Metrics.event"); auto result = metricsImpl_.event("https://com.example.schema", "{\"key\":\"value\"}", Firebolt::AgePolicy::ADULT); EXPECT_TRUE(result); } diff --git a/test/unit/mock_helper.h b/test/unit/mock_helper.h index 9f9183e..00598e5 100644 --- a/test/unit/mock_helper.h +++ b/test/unit/mock_helper.h @@ -104,6 +104,28 @@ class MockBase { return Firebolt::Result{response}; })); } + void mockInvoke(const std::string& methodName) + { + EXPECT_CALL(mockHelper, invoke(methodName, _)) + .WillOnce(Invoke( + [&](const std::string& invokedMethodName, const nlohmann::json& parameters) + { + nlohmann::json message = { + {"jsonrpc", "2.0"}, + {"id", "0"}, + {"method", invokedMethodName}, + }; + + if (!parameters.is_null()) + { + message["params"] = parameters; + } + + Firebolt::Error err = jsonEngine.MockResponse(message); + return Firebolt::Result{err}; + })); + } + void mockSubscribe(const std::string& eventName) { EXPECT_CALL(mockHelper, subscribe(_, eventName, _, _)) diff --git a/test/utils.cpp b/test/utils.cpp index 792bf4f..c24a4e1 100644 --- a/test/utils.cpp +++ b/test/utils.cpp @@ -136,9 +136,9 @@ void verifyEventReceived(std::mutex& mtx, std::condition_variable& cv, bool& eve void verifyEventNotReceived(std::mutex& mtx, std::condition_variable& cv, bool& eventReceived) { - // Wait for the event to be received or timeout after 5 seconds + // Wait for the event to be received or timeout after EventWaitTime. std::unique_lock lock(mtx); - if (cv.wait_for(lock, std::chrono::seconds(EventWaitTime), [&] { return eventReceived; })) + if (cv.wait_for(lock, EventWaitTime, [&] { return eventReceived; })) { FAIL() << "Unexpectedly received event"; }