From 126cb45cdd30f5515203a1bde671e0227e2ba2ed Mon Sep 17 00:00:00 2001 From: Nicolas Roggeman Date: Thu, 8 Aug 2024 10:31:39 +0200 Subject: [PATCH] Add UX Sync unit tests --- .github/workflows/unit_tests.yml | 10 +- lib_ux_sync/include/ux_sync.h | 3 + lib_ux_sync/src/ux_sync.c | 30 ++++- unit-tests/lib_ux_sync/CMakeLists.txt | 58 ++++++++ unit-tests/lib_ux_sync/README.md | 36 +++++ unit-tests/lib_ux_sync/nbgl_stubs.c | 181 +++++++++++++++++++++++++ unit-tests/lib_ux_sync/test_ux_sync.c | 185 ++++++++++++++++++++++++++ 7 files changed, 501 insertions(+), 2 deletions(-) create mode 100644 unit-tests/lib_ux_sync/CMakeLists.txt create mode 100644 unit-tests/lib_ux_sync/README.md create mode 100644 unit-tests/lib_ux_sync/nbgl_stubs.c create mode 100644 unit-tests/lib_ux_sync/test_ux_sync.c diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index 1b0592b94..155dc7cd0 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -27,6 +27,8 @@ jobs: cd ../lib_nbgl/ cmake -Bbuild -H. && make -C build && CTEST_OUTPUT_ON_FAILURE=1 make -C build test STAX=1 cmake -Bbuild -H. && make -C build && CTEST_OUTPUT_ON_FAILURE=1 make -C build test + cd ../lib_ux_sync/ + cmake -Bbuild -H. && make -C build && make -C build test - name: Generate code coverage run: | @@ -42,6 +44,12 @@ jobs: lcov --directory . -b "$(realpath build/)" --add-tracefile coverage.base --add-tracefile coverage.capture -o coverage.info && \ lcov --directory . -b "$(realpath build/)" --remove coverage.info '*/unit-tests/*' -o coverage.info && \ genhtml coverage.info -o coverage + cd ../lib_ux_sync/ + lcov --directory . -b "$(realpath build/)" --capture --initial -o coverage.base && \ + lcov --rc lcov_branch_coverage=1 --directory . -b "$(realpath build/)" --capture -o coverage.capture && \ + lcov --directory . -b "$(realpath build/)" --add-tracefile coverage.base --add-tracefile coverage.capture -o coverage.info && \ + lcov --directory . -b "$(realpath build/)" --remove coverage.info '*/unit-tests/*' -o coverage.info && \ + genhtml coverage.info -o coverage - uses: actions/upload-artifact@v3 @@ -53,7 +61,7 @@ jobs: uses: codecov/codecov-action@v3 with: token: ${{ secrets.CODECOV_TOKEN }} - files: ./unit-tests/lib_standard_app/coverage.info,./unit-tests/lib_nbgl/coverage.info + files: ./unit-tests/lib_standard_app/coverage.info,./unit-tests/lib_nbgl/coverage.info,./unit-tests/lib_ux_sync/coverage.info flags: unittests name: codecov-app-boilerplate fail_ci_if_error: true diff --git a/lib_ux_sync/include/ux_sync.h b/lib_ux_sync/include/ux_sync.h index 37931e722..ac13c7510 100644 --- a/lib_ux_sync/include/ux_sync.h +++ b/lib_ux_sync/include/ux_sync.h @@ -59,6 +59,9 @@ ux_sync_ret_t ux_sync_reviewStreamingStart(nbgl_operationType_t operationT ux_sync_ret_t ux_sync_reviewStreamingContinue(const nbgl_contentTagValueList_t *tagValueList); +ux_sync_ret_t ux_sync_reviewStreamingContinueExt(const nbgl_contentTagValueList_t *tagValueList, + const char *finishTitle); + ux_sync_ret_t ux_sync_reviewStreamingFinish(const char *finishTitle); ux_sync_ret_t ux_sync_genericReview(const nbgl_genericContents_t *contents, const char *rejectText); diff --git a/lib_ux_sync/src/ux_sync.c b/lib_ux_sync/src/ux_sync.c index ddf3d2e52..acd1ee784 100644 --- a/lib_ux_sync/src/ux_sync.c +++ b/lib_ux_sync/src/ux_sync.c @@ -4,6 +4,7 @@ static ux_sync_ret_t g_ret; static bool g_ended; +static const char *g_finish_title; static void choice_callback(bool confirm) { @@ -17,6 +18,11 @@ static void choice_callback(bool confirm) g_ended = true; } +static void skip_callback(void) +{ + nbgl_useCaseReviewStreamingFinish(g_finish_title, choice_callback); +} + static void quit_callback(void) { g_ret = UX_SYNC_RET_QUITTED; @@ -308,9 +314,31 @@ ux_sync_ret_t ux_sync_reviewStreamingStart(nbgl_operationType_t operationT */ ux_sync_ret_t ux_sync_reviewStreamingContinue(const nbgl_contentTagValueList_t *tagValueList) +{ + return ux_sync_reviewStreamingContinueExt(tagValueList, NULL); +} + +/** + * @brief Continue drawing the flow of pages of a review. + * @note This should be called after a call to nbgl_useCaseReviewStreamingStart with @ref + * SKIPPABLE_OPERATION and can be followed by others calls to nbgl_useCaseReviewStreamingContinue + * and finally to nbgl_useCaseReviewStreamingFinish. If "Skip" is touched, a choice will be offered + * to go directly to last page, using the given finish title. + * + * @param tagValueList list of tag/value pairs + * @param finishTitle string used in the last review page if "skip" is used + * + * @return ret code: + * - UX_SYNC_RET_APPROVED + * - UX_SYNC_RET_REJECTED + */ +ux_sync_ret_t ux_sync_reviewStreamingContinueExt(const nbgl_contentTagValueList_t *tagValueList, + const char *finishTitle) + { ux_sync_init(); - nbgl_useCaseReviewStreamingContinue(tagValueList, choice_callback); + g_finish_title = finishTitle; + nbgl_useCaseReviewStreamingContinueExt(tagValueList, choice_callback, skip_callback); return ux_sync_wait(false); } diff --git a/unit-tests/lib_ux_sync/CMakeLists.txt b/unit-tests/lib_ux_sync/CMakeLists.txt new file mode 100644 index 000000000..579dd4507 --- /dev/null +++ b/unit-tests/lib_ux_sync/CMakeLists.txt @@ -0,0 +1,58 @@ +cmake_minimum_required(VERSION 3.10) + +if(${CMAKE_VERSION} VERSION_LESS 3.10) + cmake_policy(VERSION ${CMAKE_MAJOR_VERSION}.${CMAKE_MINOR_VERSION}) +endif() + +# project information +project(unit_tests + VERSION 0.1 + DESCRIPTION "Unit tests for UX_SYNC" + LANGUAGES C) + + +# guard against bad build-type strings +if (NOT CMAKE_BUILD_TYPE) + set(CMAKE_BUILD_TYPE "Debug") +endif() + +include(CTest) +ENABLE_TESTING() + +# specify C standard +set(CMAKE_C_STANDARD 11) +set(CMAKE_C_STANDARD_REQUIRED True) +add_compile_definitions(SCREEN_SIZE_WALLET) +add_compile_definitions(USB_SEGMENT_SIZE=64) +add_compile_definitions(HAVE_NBGL) +add_compile_definitions(WITH_STDIO) + +set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -Wall ${DEFINES} -g -O0 --coverage") + +set(GCC_COVERAGE_LINK_FLAGS "--coverage -lgcov") +set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} ${GCC_COVERAGE_LINK_FLAGS}") +set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} ${GCC_COVERAGE_LINK_FLAGS}") + +# guard against in-source builds +if(${CMAKE_SOURCE_DIR} STREQUAL ${CMAKE_BINARY_DIR}) + message(FATAL_ERROR "In-source builds not allowed. Please make a new directory (called a build directory) and run CMake from there. You may need to remove CMakeCache.txt. ") +endif() + +add_compile_definitions(TEST) + +include_directories(.) +include_directories(../../target/stax/include) +include_directories(../../include) +include_directories(../../lib_nbgl/include) +include_directories(../../lib_nbgl/include/fonts) +include_directories(../../lib_ux_nbgl) +include_directories(../../lib_ux_sync/include) + +add_executable(test_ux_sync test_ux_sync.c) + +add_library(nbgl_stubs SHARED nbgl_stubs.c) +add_library(ux_sync SHARED ../../lib_ux_sync/src/ux_sync.c) + +target_link_libraries(test_ux_sync PUBLIC cmocka gcov ux_sync nbgl_stubs) + +add_test(test_ux_sync test_ux_sync) diff --git a/unit-tests/lib_ux_sync/README.md b/unit-tests/lib_ux_sync/README.md new file mode 100644 index 000000000..3587430c5 --- /dev/null +++ b/unit-tests/lib_ux_sync/README.md @@ -0,0 +1,36 @@ +# Unit tests + +## Prerequisite + +Be sure to have installed: + +- CMake >= 3.10 +- CMocka >= 1.1.5 + +and for code coverage generation: + +- lcov >= 1.14 + +## Overview + +In `unit-tests/lib_ux_sync` folder, compile with: + +``` +cmake -Bbuild -H. && make -C build +``` + +and run tests with: + +``` +CTEST_OUTPUT_ON_FAILURE=1 make -C build test +``` + +## Generate code coverage + +Just execute in `unit-tests` folder + +``` +./gen_coverage.sh +``` + +it will output `coverage.total` and `coverage/` folder with HTML details (in `coverage/index.html`). diff --git a/unit-tests/lib_ux_sync/nbgl_stubs.c b/unit-tests/lib_ux_sync/nbgl_stubs.c new file mode 100644 index 000000000..f8ef14741 --- /dev/null +++ b/unit-tests/lib_ux_sync/nbgl_stubs.c @@ -0,0 +1,181 @@ +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "nbgl_use_case.h" +#include "ux_sync.h" + +static nbgl_choiceCallback_t g_choiceCallback; +static nbgl_callback_t g_rejectCallback; + +void *pic(void *addr) +{ + return addr; +} + +bool io_recv_and_process_event(void) +{ + bool ret = (bool) mock_type(bool); + if (!ret && g_choiceCallback) { + g_choiceCallback(true); + } + return ret; +} + +void nbgl_useCaseStatus(const char *message, bool isSuccess, nbgl_callback_t quitCallback) +{ + check_expected(message); + check_expected(isSuccess); + g_rejectCallback = quitCallback; +} + +void nbgl_useCaseReviewStatus(nbgl_reviewStatusType_t reviewStatusType, + nbgl_callback_t quitCallback) +{ + check_expected(reviewStatusType); + g_rejectCallback = quitCallback; +} + +void nbgl_useCaseGenericConfiguration(const char *title, + uint8_t initPage, + const nbgl_genericContents_t *contents, + nbgl_callback_t quitCallback) +{ + check_expected(title); + check_expected(initPage); + check_expected(contents); + check_expected(quitCallback); +} + +void nbgl_useCaseChoice(const nbgl_icon_details_t *icon, + const char *message, + const char *subMessage, + const char *confirmText, + const char *cancelText, + nbgl_choiceCallback_t callback) +{ + check_expected(icon); + check_expected(message); + check_expected(subMessage); + check_expected(confirmText); + check_expected(cancelText); + g_choiceCallback = callback; +} + +void nbgl_useCaseHomeAndSettings(const char *appName, + const nbgl_icon_details_t *appIcon, + const char *tagline, + const uint8_t initSettingPage, + const nbgl_genericContents_t *settingContents, + const nbgl_contentInfoList_t *infosList, + const nbgl_homeAction_t *action, + nbgl_callback_t quitCallback) +{ + check_expected(appName); + check_expected(appIcon); + check_expected(tagline); + check_expected(initSettingPage); + check_expected(settingContents); + check_expected(infosList); + check_expected(action); +} + +void nbgl_useCaseReview(nbgl_operationType_t operationType, + const nbgl_contentTagValueList_t *tagValueList, + const nbgl_icon_details_t *icon, + const char *reviewTitle, + const char *reviewSubTitle, + const char *finishTitle, + nbgl_choiceCallback_t choiceCallback) +{ + check_expected(operationType); + check_expected(tagValueList); + check_expected(icon); + check_expected(reviewTitle); + check_expected(reviewSubTitle); + check_expected(finishTitle); + g_choiceCallback = choiceCallback; +} + +void nbgl_useCaseReviewLight(nbgl_operationType_t operationType, + const nbgl_contentTagValueList_t *tagValueList, + const nbgl_icon_details_t *icon, + const char *reviewTitle, + const char *reviewSubTitle, + const char *finishTitle, + nbgl_choiceCallback_t choiceCallback) +{ + check_expected(operationType); + check_expected(tagValueList); + check_expected(icon); + check_expected(reviewTitle); + check_expected(reviewSubTitle); + check_expected(finishTitle); + g_choiceCallback = choiceCallback; +} + +void nbgl_useCaseGenericReview(const nbgl_genericContents_t *contents, + const char *rejectText, + nbgl_callback_t rejectCallback) +{ + check_expected(contents); + check_expected(rejectText); + g_rejectCallback = rejectCallback; +} + +void nbgl_useCaseReviewStreamingStart(nbgl_operationType_t operationType, + const nbgl_icon_details_t *icon, + const char *reviewTitle, + const char *reviewSubTitle, + nbgl_choiceCallback_t choiceCallback) +{ + check_expected(operationType); + check_expected(icon); + check_expected(reviewTitle); + check_expected(reviewSubTitle); + g_choiceCallback = choiceCallback; +} + +void nbgl_useCaseReviewStreamingContinueExt(const nbgl_contentTagValueList_t *tagValueList, + nbgl_choiceCallback_t choiceCallback, + nbgl_callback_t skipCallback) +{ + check_expected(tagValueList); + g_choiceCallback = choiceCallback; + g_rejectCallback = skipCallback; +} + +void nbgl_useCaseReviewStreamingContinue(const nbgl_contentTagValueList_t *tagValueList, + nbgl_choiceCallback_t choiceCallback) +{ + check_expected(tagValueList); + g_choiceCallback = choiceCallback; +} + +void nbgl_useCaseReviewStreamingFinish(const char *finishTitle, + nbgl_choiceCallback_t choiceCallback) +{ + check_expected(finishTitle); + g_choiceCallback = choiceCallback; +} + +void nbgl_useCaseAddressReview(const char *address, + const nbgl_contentTagValueList_t *additionalTagValueList, + const nbgl_icon_details_t *icon, + const char *reviewTitle, + const char *reviewSubTitle, + nbgl_choiceCallback_t choiceCallback) +{ + check_expected(address); + check_expected(additionalTagValueList); + check_expected(icon); + check_expected(reviewTitle); + check_expected(reviewSubTitle); + g_choiceCallback = choiceCallback; +} diff --git a/unit-tests/lib_ux_sync/test_ux_sync.c b/unit-tests/lib_ux_sync/test_ux_sync.c new file mode 100644 index 000000000..e976b3c93 --- /dev/null +++ b/unit-tests/lib_ux_sync/test_ux_sync.c @@ -0,0 +1,185 @@ +#include +#include +#include +#include + +#include +#include +#include +#include +#include "nbgl_screen.h" +#include "nbgl_debug.h" +#include "ux_loc.h" +#include "ux_sync.h" + +#define UNUSED(x) (void) x + +unsigned long gLogger = 0; + +const char *g_appName = "appName"; +const nbgl_icon_details_t g_appIcon; +const char *g_tagline = "tag line"; +nbgl_genericContents_t g_settingContents; +nbgl_contentInfoList_t g_infosList; +const nbgl_contentTagValueList_t g_tagValueList; + +static void test_home(void **state __attribute__((unused))) +{ + ux_sync_ret_t ret; + + expect_string(nbgl_useCaseHomeAndSettings, appName, g_appName); + expect_memory(nbgl_useCaseHomeAndSettings, appIcon, &g_appIcon, sizeof(g_appIcon)); + expect_string(nbgl_useCaseHomeAndSettings, tagline, g_tagline); + expect_value(nbgl_useCaseHomeAndSettings, initSettingPage, 0); + expect_memory(nbgl_useCaseHomeAndSettings, + settingContents, + &g_settingContents, + sizeof(g_settingContents)); + expect_memory(nbgl_useCaseHomeAndSettings, infosList, &g_infosList, sizeof(g_infosList)); + expect_value(nbgl_useCaseHomeAndSettings, action, NULL); + + will_return(io_recv_and_process_event, true); + ret = ux_sync_homeAndSettings( + g_appName, &g_appIcon, g_tagline, 0, &g_settingContents, &g_infosList, NULL); + assert_int_equal(ret, UX_SYNC_RET_APDU_RECEIVED); +} + +static void test_review(void **state __attribute__((unused))) +{ + ux_sync_ret_t ret; + + expect_value(nbgl_useCaseReview, operationType, TYPE_TRANSACTION); + expect_memory(nbgl_useCaseReview, tagValueList, &g_tagValueList, sizeof(g_tagValueList)); + expect_memory(nbgl_useCaseReview, icon, &g_appIcon, sizeof(g_appIcon)); + expect_string(nbgl_useCaseReview, reviewTitle, "Review Title"); + expect_string(nbgl_useCaseReview, reviewSubTitle, "Review Sub-title"); + expect_string(nbgl_useCaseReview, finishTitle, "Finish Title"); + + will_return(io_recv_and_process_event, false); + ret = ux_sync_review(TYPE_TRANSACTION, + &g_tagValueList, + &g_appIcon, + "Review Title", + "Review Sub-title", + "Finish Title"); + assert_int_equal(ret, UX_SYNC_RET_APPROVED); +} + +static void test_review_light(void **state __attribute__((unused))) +{ + ux_sync_ret_t ret; + + expect_value(nbgl_useCaseReviewLight, operationType, TYPE_TRANSACTION); + expect_memory(nbgl_useCaseReviewLight, tagValueList, &g_tagValueList, sizeof(g_tagValueList)); + expect_memory(nbgl_useCaseReviewLight, icon, &g_appIcon, sizeof(g_appIcon)); + expect_string(nbgl_useCaseReviewLight, reviewTitle, "Review Title"); + expect_string(nbgl_useCaseReviewLight, reviewSubTitle, "Review Sub-title"); + expect_string(nbgl_useCaseReviewLight, finishTitle, "Finish Title"); + + will_return(io_recv_and_process_event, false); + ret = ux_sync_reviewLight(TYPE_TRANSACTION, + &g_tagValueList, + &g_appIcon, + "Review Title", + "Review Sub-title", + "Finish Title"); + assert_int_equal(ret, UX_SYNC_RET_APPROVED); +} + +static void test_address_review(void **state __attribute__((unused))) +{ + ux_sync_ret_t ret; + + expect_string(nbgl_useCaseAddressReview, address, "My address"); + expect_memory( + nbgl_useCaseAddressReview, additionalTagValueList, &g_tagValueList, sizeof(g_tagValueList)); + expect_memory(nbgl_useCaseAddressReview, icon, &g_appIcon, sizeof(g_appIcon)); + expect_string(nbgl_useCaseAddressReview, reviewTitle, "Review Title"); + expect_string(nbgl_useCaseAddressReview, reviewSubTitle, "Review Sub-title"); + + will_return(io_recv_and_process_event, false); + ret = ux_sync_addressReview( + "My address", &g_tagValueList, &g_appIcon, "Review Title", "Review Sub-title"); + assert_int_equal(ret, UX_SYNC_RET_APPROVED); +} + +static void test_review_status(void **state __attribute__((unused))) +{ + ux_sync_ret_t ret; + + expect_value(nbgl_useCaseReviewStatus, reviewStatusType, STATUS_TYPE_TRANSACTION_SIGNED); + + will_return(io_recv_and_process_event, false); + ret = ux_sync_reviewStatus(STATUS_TYPE_TRANSACTION_SIGNED); + assert_int_equal(ret, UX_SYNC_RET_APPROVED); +} + +static void test_status(void **state __attribute__((unused))) +{ + ux_sync_ret_t ret; + + expect_string(nbgl_useCaseStatus, message, "My status"); + expect_value(nbgl_useCaseStatus, isSuccess, true); + + will_return(io_recv_and_process_event, false); + ret = ux_sync_status("My status", true); + assert_int_equal(ret, UX_SYNC_RET_APPROVED); +} + +static void test_choice(void **state __attribute__((unused))) +{ + ux_sync_ret_t ret; + + expect_memory(nbgl_useCaseChoice, icon, &g_appIcon, sizeof(g_appIcon)); + expect_string(nbgl_useCaseChoice, message, "Message"); + expect_string(nbgl_useCaseChoice, subMessage, "Sub Message"); + expect_string(nbgl_useCaseChoice, confirmText, "Confirm"); + expect_string(nbgl_useCaseChoice, cancelText, "Reject"); + + will_return(io_recv_and_process_event, false); + ret = ux_sync_choice(&g_appIcon, "Message", "Sub Message", "Confirm", "Reject"); + assert_int_equal(ret, UX_SYNC_RET_APPROVED); +} + +static void test_streaming(void **state __attribute__((unused))) +{ + ux_sync_ret_t ret; + + expect_value(nbgl_useCaseReviewStreamingStart, operationType, TYPE_TRANSACTION); + expect_memory(nbgl_useCaseReviewStreamingStart, icon, &g_appIcon, sizeof(g_appIcon)); + expect_string(nbgl_useCaseReviewStreamingStart, reviewTitle, "Review Title"); + expect_string(nbgl_useCaseReviewStreamingStart, reviewSubTitle, "Review Sub-title"); + + will_return(io_recv_and_process_event, false); + ret = ux_sync_reviewStreamingStart( + TYPE_TRANSACTION, &g_appIcon, "Review Title", "Review Sub-title"); + assert_int_equal(ret, UX_SYNC_RET_APPROVED); + + expect_memory(nbgl_useCaseReviewStreamingContinueExt, + tagValueList, + &g_tagValueList, + sizeof(g_tagValueList)); + + will_return(io_recv_and_process_event, false); + ret = ux_sync_reviewStreamingContinue(&g_tagValueList); + assert_int_equal(ret, UX_SYNC_RET_APPROVED); + + expect_string(nbgl_useCaseReviewStreamingFinish, finishTitle, "Finish Title"); + + will_return(io_recv_and_process_event, false); + ret = ux_sync_reviewStreamingFinish("Finish Title"); + assert_int_equal(ret, UX_SYNC_RET_APPROVED); +} + +int main(int argc, char **argv) +{ + const struct CMUnitTest tests[] = {cmocka_unit_test(test_home), + cmocka_unit_test(test_review), + cmocka_unit_test(test_review_light), + cmocka_unit_test(test_address_review), + cmocka_unit_test(test_review_status), + cmocka_unit_test(test_status), + cmocka_unit_test(test_choice), + cmocka_unit_test(test_streaming)}; + return cmocka_run_group_tests(tests, NULL, NULL); +}