diff --git a/configs/ci/docs/lv_conf_docs.defaults b/configs/ci/docs/lv_conf_docs.defaults index b4af46f533..29f336ac5e 100644 --- a/configs/ci/docs/lv_conf_docs.defaults +++ b/configs/ci/docs/lv_conf_docs.defaults @@ -63,6 +63,8 @@ LV_USE_FRAGMENT 1 LV_USE_IMGFONT 1 LV_USE_IME_PINYIN 1 LV_USE_FILE_EXPLORER 1 +LV_USE_TEST 1 +LV_USE_TEST_SCREENSHOT_COMPARE 1 LV_USE_XML 1 LV_USE_SDL 1 diff --git a/configs/ci/examples/lv_conf_examples.defaults b/configs/ci/examples/lv_conf_examples.defaults index 4f5950cce6..57da88fab4 100644 --- a/configs/ci/examples/lv_conf_examples.defaults +++ b/configs/ci/examples/lv_conf_examples.defaults @@ -123,5 +123,7 @@ LV_USE_OBSERVER 1 LV_USE_IME_PINYIN 1 LV_USE_FILE_EXPLORER 1 LV_USE_FONT_MANAGER 1 +LV_USE_TEST 1 +LV_USE_TEST_SCREENSHOT_COMPARE 1 LV_USE_XML 1 LV_BUILD_EXAMPLES 1 diff --git a/docs/src/details/auxiliary-modules/test.rst b/docs/src/details/auxiliary-modules/test.rst index de74ad27d9..f47fd9affb 100644 --- a/docs/src/details/auxiliary-modules/test.rst +++ b/docs/src/details/auxiliary-modules/test.rst @@ -153,10 +153,10 @@ To read and decode PNG images and to store the converted rendered image, a few M (not :cpp:expr:`lv_malloc`). -The screenshot comparison uses `lodepng` which is built-in to LVGL and just needs to be enabled with +The screenshot comparison uses ``lodepng`` which is built-in to LVGL and just needs to be ebnabled with ``LV_USE_LODEPNG``. -To avoid making the entire Test module dependent on `lodepng`, screenshot comparison can be individually enabled by +To avoid making the entire Test module dependent on ``lodepng``, screenshot comparison can be individually enabled by ``LV_USE_TEST_SCREENSHOT_COMPARE``. API diff --git a/docs/src/details/auxiliary-modules/xml/index.rst b/docs/src/details/auxiliary-modules/xml/index.rst index 8600fa95c9..b647ded83e 100644 --- a/docs/src/details/auxiliary-modules/xml/index.rst +++ b/docs/src/details/auxiliary-modules/xml/index.rst @@ -28,4 +28,3 @@ XML - Declarative UI animations translations license - diff --git a/docs/src/details/auxiliary-modules/xml/test.rst b/docs/src/details/auxiliary-modules/xml/test.rst new file mode 100644 index 0000000000..f2771c0fba --- /dev/null +++ b/docs/src/details/auxiliary-modules/xml/test.rst @@ -0,0 +1,131 @@ +.. _xml_test: + +==== +Test +==== + +Overview +******** + +The XML test module is a powerful and flexible way to define functional UI tests. + +Test XML files are similar to components but are wrapped in a `` tag and consist of two main parts: + +- **UI Definition**: Use ``, ``, and `` to define how the UI should look. This is identical to how ``s are structured. +- **Test Steps**: Encapsulated in a `` tag, these define the actions and assertions for the test. + +Step Types +---------- + +The following step types are currently supported (with example parameters): + +- ````: Move the mouse to the specified screen coordinates. +- ````: Simulates a click at the specified screen coordinates. +- ````: Press on the current screen coordinate. +- ````: Release on the current screen coordinate. +- ````: Waits for the given number of milliseconds. LVGL continues running, including animations and timers. +- ````: Pauses the UI and LVGL's internal time. Useful for visual debugging. +- ```` Compare an integer or strings subject's value against a reference value. +- ````: Compares the current screen with a reference image. + - If the image doesn't exist, it is created. + - If the comparison fails, an image with `_err` suffix is saved for inspection. + +Example: + +.. code-block:: xml + + + + + + + + + + + + + + + + + + +Registering Tests +----------------- + +Tests can be registered in two ways: + +- From file: :cpp:expr:`lv_xml_test_register_from_file("path/to/test.xml", "A:ref_images")` +- From string: :cpp:expr:`lv_xml_test_register_from_data(xml_data, "A:ref_images")` + +The second parameter specifies a prefix for screenshot comparison paths. + +To unregister tests, use: + +- :cpp:expr:`lv_xml_test_unregister()` + +This is called automatically when registering a new test, so manual calls are usually unnecessary. + +Running Tests +------------- + +All steps +^^^^^^^^^ + +To execute the registered test, use :cpp:expr:`lv_xml_test_run_all(slowdown)`. + +It will clean the screen and create a fresh instance of the ``view`` to be tested. + +The `slowdown` parameter controls playback speed: + +- `0`: Maximum speed +- `1`: Real-time speed +- `2`: Half-speed +- `10`: 10× slower +- ...and so on + +`lv_xml_test_run_all()` blocks until all steps are completed. It is safe to call it multiple times. + +The return value is the number of failed tests. + +Step-by-step +^^^^^^^^^^^^ + +It's also possible to run each step separately, one after another. + +First, call :cpp:expr:`lv_xml_test_run_init()` to prepare for executing the steps. +It will clean the screen and create a fresh instance of the ``view`` to be tested. + +Get the number of steps using :cpp:expr:`lv_xml_test_get_step_count()` +and then call :cpp:expr:`lv_xml_test_run_next(slowdown)` as many times. + +:cpp:expr:`lv_xml_test_run_next()` returns ``true`` if the given step passed, or ``false`` if it failed. + +Finally, call :cpp:expr:`lv_xml_test_run_stop();` to clean up and exit testing mode. + + +Getting the Test Results +------------------------ + +Currently, only `screenshot_compare` steps can fail. However, the result of each step can be queried: + +.. code-block:: c + + uint32_t step_cnt = lv_xml_test_get_step_count(); + for(uint32_t i = 0; i < step_cnt; i++) { + if(lv_xml_test_get_step_type(i) == LV_XML_TEST_STEP_TYPE_SCREENSHOT_COMPARE) { + if(lv_xml_test_get_status(i)) + printf("Step %d passed\n", i); + else + printf("Step %d failed\n", i); + } else { + printf("Step %d is not a screenshot comparison\n", i); + } + } + +.. _lv_xml_test_api: + +API +*** diff --git a/examples/others/xml/lv_example_xml_2.c b/examples/others/xml/lv_example_xml_2.c index f037a54ac3..2f23ee2481 100644 --- a/examples/others/xml/lv_example_xml_2.c +++ b/examples/others/xml/lv_example_xml_2.c @@ -13,34 +13,20 @@ void lv_example_xml_2(void) } lv_xml_component_register_from_file("A:lvgl/examples/others/xml/my_card.xml"); lv_xml_component_register_from_file("A:lvgl/examples/others/xml/my_button.xml"); - lv_xml_component_register_from_file("A:lvgl/examples/others/xml/view.xml"); lv_xml_register_font(NULL, "lv_montserrat_18", &lv_font_montserrat_18); - lv_obj_t * obj = (lv_obj_t *) lv_xml_create(lv_screen_active(), "view", NULL); - lv_obj_set_pos(obj, 10, 10); + lv_subject_t s1; + lv_subject_t s2; + static char buf[200]; + lv_subject_init_string(&s1, buf, NULL, 200, "Waaaa"); + lv_subject_init_int(&s2, 25); - const char * my_button_attrs[] = { - "x", "10", - "y", "-10", - "align", "bottom_left", - "btn_text", "New button", - NULL, NULL, - }; + lv_xml_register_subject(NULL, "s1", &s2); - lv_xml_component_unregister("my_button"); + lv_xml_test_register_from_file("A:lvgl/examples/others/xml/view.xml", "A:"); - lv_xml_create(lv_screen_active(), "my_button", my_button_attrs); + lv_xml_test_run_all(1); - const char * slider_attrs[] = { - "x", "200", - "y", "-15", - "align", "bottom_left", - "value", "30", - NULL, NULL, - }; - - lv_obj_t * slider = (lv_obj_t *) lv_xml_create(lv_screen_active(), "lv_slider", slider_attrs); - lv_obj_set_width(slider, 100); } #endif diff --git a/src/lv_init.c b/src/lv_init.c index 790652d6a0..e33415c76c 100644 --- a/src/lv_init.c +++ b/src/lv_init.c @@ -506,6 +506,10 @@ void lv_deinit(void) lv_objid_builtin_destroy(); #endif +#if LV_USE_XML + lv_xml_test_unregister(); +#endif + lv_mem_deinit(); lv_initialized = false; diff --git a/src/others/test/lv_test_indev.c b/src/others/test/lv_test_indev.c index 11bdb14b25..742e27d38d 100644 --- a/src/others/test/lv_test_indev.c +++ b/src/others/test/lv_test_indev.c @@ -55,6 +55,24 @@ void lv_test_indev_create_all(void) lv_indev_set_read_cb(_state.encoder_indev, lv_test_encoder_read_cb); } +void lv_test_indev_delete_all(void) +{ + if(_state.mouse_indev) { + lv_indev_delete(_state.mouse_indev); + _state.mouse_indev = NULL; + } + + if(_state.keypad_indev) { + lv_indev_delete(_state.keypad_indev); + _state.keypad_indev = NULL; + } + + if(_state.encoder_indev) { + lv_indev_delete(_state.encoder_indev); + _state.encoder_indev = NULL; + } +} + lv_indev_t * lv_test_indev_get_indev(lv_indev_type_t type) { switch(type) { diff --git a/src/others/test/lv_test_indev.h b/src/others/test/lv_test_indev.h index 7d833fff48..63c44ab462 100644 --- a/src/others/test/lv_test_indev.h +++ b/src/others/test/lv_test_indev.h @@ -37,6 +37,11 @@ extern "C" { */ void lv_test_indev_create_all(void); +/** + * Delete all test input devices + */ +void lv_test_indev_delete_all(void); + /** * Get one of the indev created in `lv_test_indev_create_all` * @param type type of the indev to get diff --git a/src/others/test/lv_test_screenshot_compare.c b/src/others/test/lv_test_screenshot_compare.c index d0b3aed674..2896151940 100644 --- a/src/others/test/lv_test_screenshot_compare.c +++ b/src/others/test/lv_test_screenshot_compare.c @@ -81,6 +81,7 @@ bool lv_test_screenshot_compare(const char * fn_ref) lv_obj_t * scr = lv_screen_active(); lv_obj_invalidate(scr); + lv_refr_now(NULL); pass = screenshot_compare(fn_ref, REF_IMG_TOLERANCE); if(!pass) return false; @@ -104,9 +105,8 @@ static bool screenshot_compare(const char * fn_ref, uint8_t tolerance) create_folders_if_needed(fn_ref_full); - lv_refr_now(NULL); - lv_draw_buf_t * draw_buf = lv_display_get_buf_active(NULL); + uint8_t * screen_buf_xrgb8888 = lv_malloc(draw_buf->header.w * draw_buf->header.h * 4); buf_to_xrgb8888(draw_buf, screen_buf_xrgb8888); @@ -115,12 +115,18 @@ static bool screenshot_compare(const char * fn_ref, uint8_t tolerance) unsigned ref_img_height = 0; unsigned res = read_png_file(&ref_draw_buf, &ref_img_width, &ref_img_height, fn_ref_full); if(res) { - LV_LOG_ERROR("%s%s", fn_ref_full, " was not found, creating is now from the rendered screen"); + LV_LOG_WARN("%s%s", fn_ref_full, " was not found, creating it now from the rendered screen"); write_png_file(screen_buf_xrgb8888, draw_buf->header.w, draw_buf->header.h, fn_ref_full); lv_free(screen_buf_xrgb8888); return true; } + if(ref_img_width != draw_buf->header.w || ref_img_height != draw_buf->header.h) { + LV_LOG_WARN("The dimensions of the rendered and the %s reference image don't match", fn_ref); + return false; + } + + unsigned x, y; bool err = false; for(y = 0; y < ref_img_height; y++) { diff --git a/src/others/test/lv_test_screenshot_compare.h b/src/others/test/lv_test_screenshot_compare.h index b962beceff..b37fe3ee3e 100644 --- a/src/others/test/lv_test_screenshot_compare.h +++ b/src/others/test/lv_test_screenshot_compare.h @@ -37,6 +37,8 @@ extern "C" { * * @param fn_ref path to the reference image. Will be appended toREF_IMGS_PATH if set. * @return true: the reference image and the display are the same; false: they are different (`_err.png` is created). + * @note This function assumes that the default display is the test display that was created by + * `lv_test_display_create()` */ bool lv_test_screenshot_compare(const char * fn_ref); diff --git a/src/others/xml/lv_xml.h b/src/others/xml/lv_xml.h index a26e3055ae..02cd689b85 100644 --- a/src/others/xml/lv_xml.h +++ b/src/others/xml/lv_xml.h @@ -18,6 +18,7 @@ extern "C" { #include "../../others/observer/lv_observer.h" #if LV_USE_XML +#include "lv_xml_test.h" /********************* * DEFINES diff --git a/src/others/xml/lv_xml_component.c b/src/others/xml/lv_xml_component.c index 5351725c88..2e8d845d1e 100644 --- a/src/others/xml/lv_xml_component.c +++ b/src/others/xml/lv_xml_component.c @@ -660,6 +660,7 @@ static void start_metadata_handler(void * user_data, const char * name, const ch } if(lv_streq(name, "widget")) state->scope.is_widget = 1; + else if(lv_streq(name, "screen")) state->scope.is_screen = 1; /* Process elements based on current context */ switch(state->section) { diff --git a/src/others/xml/lv_xml_component_private.h b/src/others/xml/lv_xml_component_private.h index 9b84eb4ee0..903d2635f4 100644 --- a/src/others/xml/lv_xml_component_private.h +++ b/src/others/xml/lv_xml_component_private.h @@ -39,7 +39,8 @@ struct _lv_xml_component_scope_t { lv_ll_t event_ll; const char * view_def; const char * extends; - uint32_t is_widget : 1; /*1: not component but widget registered as a component for preview*/ + uint32_t is_widget : 1; + uint32_t is_screen : 1; struct _lv_xml_component_scope_t * next; }; diff --git a/src/others/xml/lv_xml_test.c b/src/others/xml/lv_xml_test.c new file mode 100644 index 0000000000..72293f887d --- /dev/null +++ b/src/others/xml/lv_xml_test.c @@ -0,0 +1,549 @@ +/** + * @file lv_xml_test.c + * + */ + +/********************* + * INCLUDES + *********************/ +#include "lv_xml_test.h" +#if LV_USE_XML && LV_USE_TEST + +#include "../../lvgl.h" +#include "lv_xml.h" +#include "lv_xml_utils.h" +#include "lv_xml_component_private.h" +#include "../../misc/lv_fs.h" +#include "../../libs/expat/expat.h" +#include "../../display/lv_display_private.h" + +/********************* + * DEFINES + *********************/ +#define LV_TEST_NAME "__test__" + +/********************** + * TYPEDEFS + **********************/ + +typedef struct { + lv_xml_test_step_type_t type; + union { + struct { + int32_t x; + int32_t y; + } mouse_pos; + + struct { + int32_t ms; + } wait; + + struct { + int32_t ms; + } freeze; + + struct { + const char * path; + } screenshot_compare; + + struct { + lv_subject_t * subject; + const char * value; + } subject_set; + struct { + lv_subject_t * subject; + const char * value; + } subject_compare; + } param; + uint32_t passed : 1; +} lv_xml_test_step_t; + +typedef struct { + const char * ref_image_path_prefix; + uint32_t step_cnt; + uint32_t step_act; + lv_xml_test_step_t * steps; + uint32_t processing_steps : 1; +} lv_xml_test_t; + +/********************** + * STATIC PROTOTYPES + **********************/ +static lv_obj_t * create_cursor(lv_obj_t * parent); +static bool execute_step(lv_xml_test_step_t * step, uint32_t slowdown); +static void start_metadata_handler(void * user_data, const char * name, const char ** attrs); +static void end_metadata_handler(void * user_data, const char * name); + +/********************** + * STATIC VARIABLES + **********************/ +static lv_xml_test_t test; +static lv_display_t * test_display; +static lv_obj_t * cursor; +static lv_tick_get_cb_t tick_cb_original; + +/********************** + * MACROS + **********************/ + +/********************** + * GLOBAL FUNCTIONS + **********************/ + + +lv_result_t lv_xml_test_register_from_data(const char * xml_def, const char * ref_image_path_prefix) +{ + /*Cleanup the previous test*/ + lv_xml_test_unregister(); + + test.ref_image_path_prefix = ref_image_path_prefix; + + /*Register as a component first to allow creating the view of the test later*/ + lv_result_t res = lv_xml_component_register_from_data(LV_TEST_NAME, xml_def); + if(res != LV_RESULT_OK) { + LV_LOG_WARN("Couldn't register the test as a component"); + return LV_RESULT_INVALID; + } + + /* Parse the XML to extract metadata */ + XML_Memory_Handling_Suite mem_handlers; + mem_handlers.malloc_fcn = lv_malloc; + mem_handlers.realloc_fcn = lv_realloc; + mem_handlers.free_fcn = lv_free; + XML_Parser parser = XML_ParserCreate_MM(NULL, &mem_handlers, NULL); + XML_SetElementHandler(parser, start_metadata_handler, end_metadata_handler); + + + if(XML_Parse(parser, xml_def, lv_strlen(xml_def), XML_TRUE) == XML_STATUS_ERROR) { + LV_LOG_ERROR("XML parsing error: %s on line %lu", + XML_ErrorString(XML_GetErrorCode(parser)), + (unsigned long)XML_GetCurrentLineNumber(parser)); + XML_ParserFree(parser); + test.ref_image_path_prefix = NULL; + return LV_RESULT_INVALID; + } + + XML_ParserFree(parser); + test.ref_image_path_prefix = NULL; + + return LV_RESULT_OK; +} + + +lv_result_t lv_xml_test_register_from_file(const char * path, const char * ref_image_path_prefix) +{ + lv_fs_res_t fs_res; + lv_fs_file_t f; + fs_res = lv_fs_open(&f, path, LV_FS_MODE_RD); + if(fs_res != LV_FS_RES_OK) { + LV_LOG_WARN("Failed to open %s", path); + return LV_RESULT_INVALID; + } + + /* Determine file size */ + lv_fs_seek(&f, 0, LV_FS_SEEK_END); + uint32_t file_size = 0; + lv_fs_tell(&f, &file_size); + lv_fs_seek(&f, 0, LV_FS_SEEK_SET); + + /* Create the buffer */ + char * xml_buf = lv_zalloc(file_size + 1); + if(xml_buf == NULL) { + LV_LOG_WARN("Memory allocation failed for file %s (%d bytes)", path, file_size + 1); + lv_fs_close(&f); + return LV_RESULT_INVALID; + } + + /* Read the file content */ + uint32_t rn; + lv_fs_read(&f, xml_buf, file_size, &rn); + if(rn != file_size) { + LV_LOG_WARN("Couldn't read %s fully", path); + lv_free(xml_buf); + lv_fs_close(&f); + return LV_RESULT_INVALID; + } + + /* Null-terminate the buffer */ + xml_buf[rn] = '\0'; + + /* Register the test */ + lv_result_t res = lv_xml_test_register_from_data(xml_buf, ref_image_path_prefix); + + /* Housekeeping */ + lv_free(xml_buf); + lv_fs_close(&f); + + return res; +} + +void lv_xml_test_unregister(void) +{ + uint32_t i; + for(i = 0; i < test.step_cnt; i++) { + lv_xml_test_step_type_t type = test.steps[i].type; + if(type == LV_XML_TEST_STEP_TYPE_SCREENSHOT_COMPARE) { + lv_free((void *)test.steps[i].param.screenshot_compare.path); + } + if(type == LV_XML_TEST_STEP_TYPE_SUBJECT_SET) { + lv_free((void *)test.steps[i].param.subject_set.value); + } + if(type == LV_XML_TEST_STEP_TYPE_SUBJECT_COMPARE) { + lv_free((void *)test.steps[i].param.subject_compare.value); + } + } + lv_free(test.steps); + test.steps = NULL; + test.step_cnt = 0; + + lv_xml_component_unregister(LV_TEST_NAME); +} + +void lv_xml_test_run_init(void) +{ + lv_display_t * normal_display = lv_display_get_default(); + test_display = lv_test_display_create(normal_display->hor_res, normal_display->ver_res); + + /*The test will control the ticks*/ + tick_cb_original = lv_tick_get_cb(); + lv_tick_set_cb(NULL); + + lv_test_indev_create_all(); + cursor = create_cursor(lv_display_get_layer_sys(normal_display)); + + lv_xml_component_scope_t * scope = lv_xml_component_get_scope(LV_TEST_NAME); + lv_xml_component_scope_t * extends_scope = lv_xml_component_get_scope(scope->extends); + lv_obj_t * act_screen = lv_screen_active(); + if(extends_scope && extends_scope->is_screen) { + lv_obj_t * test_screen = lv_xml_create(NULL, LV_TEST_NAME, NULL); + lv_screen_load(test_screen); + lv_obj_delete(act_screen); + } + else { + lv_obj_clean(act_screen); + lv_xml_create(act_screen, LV_TEST_NAME, NULL); + } + lv_refr_now(normal_display); + test.step_act = 0; +} + +bool lv_xml_test_run_next(uint32_t slowdown) +{ + bool passed = execute_step(&test.steps[test.step_act], slowdown); + test.steps[test.step_act].passed = passed; + if(!test.steps[test.step_act].passed) { + LV_LOG_WARN("Step %d failed", test.step_act); + } + + test.step_act++; + return passed; +} + +void lv_xml_test_run_stop(void) +{ + lv_obj_delete(cursor); + lv_tick_set_cb(tick_cb_original); + lv_display_delete(test_display); + lv_test_indev_delete_all(); +} + + +uint32_t lv_xml_test_run_all(uint32_t slowdown) +{ + lv_xml_test_run_init(); + + uint32_t failed_cnt = 0; + uint32_t i; + for(i = 0; i < test.step_cnt; i++) { + bool passed = lv_xml_test_run_next(slowdown); + if(!passed) failed_cnt++; + } + + lv_xml_test_run_stop(); + + return failed_cnt; +} + + +uint32_t lv_xml_test_get_step_count(void) +{ + return test.step_cnt; +} + +lv_xml_test_step_type_t lv_xml_test_get_step_type(uint32_t idx) +{ + if(idx >= test.step_cnt) return LV_XML_TEST_STEP_TYPE_NONE; + + return test.steps[idx].type; + +} + +bool lv_xml_test_get_status(uint32_t idx) +{ + if(idx >= test.step_cnt) return LV_XML_TEST_STEP_TYPE_NONE; + + return test.steps[idx].passed; +} + +/********************** + * STATIC FUNCTIONS + **********************/ + +static void lv_xml_test_wait(uint32_t ms, uint32_t slowdown) +{ + lv_test_wait(ms); + lv_delay_ms(ms * slowdown); +} + +static bool execute_step(lv_xml_test_step_t * step, uint32_t slowdown) +{ + bool res = true; + + if(step->type == LV_XML_TEST_STEP_TYPE_CLICK_AT) { + int32_t x = step->param.mouse_pos.x; + int32_t y = step->param.mouse_pos.y; + + lv_obj_remove_state(cursor, LV_STATE_PRESSED); + lv_test_mouse_release(); + lv_xml_test_wait(50, slowdown); + lv_test_mouse_move_to(x, y); + lv_test_mouse_press(); + lv_obj_set_pos(cursor, x, y); + lv_obj_add_state(cursor, LV_STATE_PRESSED); + lv_xml_test_wait(100, slowdown); + lv_test_mouse_release(); + lv_xml_test_wait(50, slowdown); + lv_obj_remove_state(cursor, LV_STATE_PRESSED); + lv_refr_now(NULL); + } + else if(step->type == LV_XML_TEST_STEP_TYPE_PRESS) { + lv_obj_add_state(cursor, LV_STATE_PRESSED); + lv_test_mouse_press(); + } + else if(step->type == LV_XML_TEST_STEP_TYPE_RELEASE) { + lv_obj_remove_state(cursor, LV_STATE_PRESSED); + lv_test_mouse_release(); + } + else if(step->type == LV_XML_TEST_STEP_TYPE_MOVE_TO) { + int32_t x = step->param.mouse_pos.x; + int32_t y = step->param.mouse_pos.y; + lv_test_mouse_move_to(x, y); + lv_obj_set_pos(cursor, x, y); + } + else if(step->type == LV_XML_TEST_STEP_TYPE_SCREENSHOT_COMPARE) { + + /*Set the act_screen's pointer to for the test display so that it will render it + *for screenshot compare*/ + lv_obj_t * act_screen_original = test_display->act_scr; + test_display->act_scr = lv_screen_active(); + + /*lv_test_screenshot_compare assumes that the default display is test_display*/ + lv_display_t * default_display = lv_display_get_default(); + lv_display_set_default(test_display); + + /*Make sure that both displays will be updated. Don't invalidate by + *using the act_screen as now it belongs to both displays.*/ + lv_obj_invalidate(lv_display_get_layer_sys(default_display)); + lv_obj_invalidate(lv_display_get_layer_sys(test_display)); + + /*Do the actual screenshot compare*/ + res = lv_test_screenshot_compare(step->param.screenshot_compare.path); + if(!res) { + LV_LOG_WARN("screenshot compare of `%s` failed", step->param.screenshot_compare.path); + } + + /*Restore*/ + lv_display_set_default(default_display); + test_display->act_scr = act_screen_original; + + } + else if(step->type == LV_XML_TEST_STEP_TYPE_WAIT) { + lv_xml_test_wait(step->param.wait.ms, slowdown); + } + else if(step->type == LV_XML_TEST_STEP_TYPE_FREEZE) { + lv_delay_ms(step->param.freeze.ms * slowdown); + } + else if(step->type == LV_XML_TEST_STEP_TYPE_SUBJECT_SET) { + lv_subject_type_t type = step->param.subject_set.subject->type; + if(type == LV_SUBJECT_TYPE_INT) { + lv_subject_set_int(step->param.subject_set.subject, lv_xml_atoi(step->param.subject_set.value)); + } + else if(type == LV_SUBJECT_TYPE_STRING) { + lv_subject_copy_string(step->param.subject_set.subject, step->param.subject_set.value); + } + else { + LV_LOG_WARN("Not supported subject type %d", type); + } + } + else if(step->type == LV_XML_TEST_STEP_TYPE_SUBJECT_COMPARE) { + lv_subject_type_t type = step->param.subject_compare.subject->type; + if(type == LV_SUBJECT_TYPE_INT) { + int32_t v = lv_subject_get_int(step->param.subject_compare.subject); + if(v != lv_xml_atoi(step->param.subject_compare.value)) { + LV_LOG_WARN("Subject compare failed. Expected: %s, Actual: %d", step->param.subject_compare.value, v); + res = false; + } + } + else if(type == LV_SUBJECT_TYPE_STRING) { + const char * v = lv_subject_get_string(step->param.subject_compare.subject); + if(lv_streq(v, step->param.subject_compare.value) == 0) { + LV_LOG_WARN("Subject compare failed. Expected: %s, Actual: %s", step->param.subject_compare.value, v); + res = false; + } + } + else { + LV_LOG_WARN("Not supported subject type %d", type); + } + } + + return res; +} + +static lv_obj_t * create_cursor(lv_obj_t * parent) +{ + lv_obj_t * obj = lv_obj_create(parent); + lv_obj_remove_flag(obj, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_remove_flag(obj, LV_OBJ_FLAG_CLICKABLE); + lv_obj_set_size(obj, 30, 30); + lv_obj_set_style_opa(obj, LV_OPA_50, 0); + lv_obj_set_style_bg_color(obj, lv_palette_main(LV_PALETTE_ORANGE), 0); + lv_obj_set_style_bg_color(obj, lv_palette_main(LV_PALETTE_DEEP_ORANGE), LV_STATE_PRESSED); + lv_obj_set_style_border_color(obj, lv_palette_main(LV_PALETTE_BLUE), 0); + lv_obj_set_style_translate_x(obj, lv_pct(-50), 0); + lv_obj_set_style_translate_y(obj, lv_pct(-50), 0); + lv_obj_set_style_radius(obj, LV_RADIUS_CIRCLE, 0); + lv_obj_set_style_transform_width(obj, -5, LV_STATE_PRESSED); + lv_obj_set_style_transform_height(obj, -5, LV_STATE_PRESSED); + + return obj; +} + +static void start_metadata_handler(void * user_data, const char * name, const char ** attrs) +{ + LV_UNUSED(user_data); + + if(lv_streq(name, "test")) { + return; + } + + if(lv_streq(name, "steps")) { + test.processing_steps = 1; + return; + } + + /*Process the steps only*/ + if(test.processing_steps == 0) return; + + if(lv_streq(name, "click_at")) { + test.step_cnt++; + const char * x = lv_xml_get_value_of(attrs, "x"); + const char * y = lv_xml_get_value_of(attrs, "y"); + test.steps = lv_realloc(test.steps, sizeof(lv_xml_test_step_t) * test.step_cnt); + uint32_t idx = test.step_cnt - 1; + test.steps[idx].type = LV_XML_TEST_STEP_TYPE_CLICK_AT; + test.steps[idx].param.mouse_pos.x = lv_xml_atoi(x); + test.steps[idx].param.mouse_pos.y = lv_xml_atoi(y); + } + else if(lv_streq(name, "move_to")) { + test.step_cnt++; + const char * x = lv_xml_get_value_of(attrs, "x"); + const char * y = lv_xml_get_value_of(attrs, "y"); + test.steps = lv_realloc(test.steps, sizeof(lv_xml_test_step_t) * test.step_cnt); + uint32_t idx = test.step_cnt - 1; + test.steps[idx].type = LV_XML_TEST_STEP_TYPE_MOVE_TO; + test.steps[idx].param.mouse_pos.x = lv_xml_atoi(x); + test.steps[idx].param.mouse_pos.y = lv_xml_atoi(y); + } + else if(lv_streq(name, "press")) { + test.step_cnt++; + test.steps = lv_realloc(test.steps, sizeof(lv_xml_test_step_t) * test.step_cnt); + uint32_t idx = test.step_cnt - 1; + test.steps[idx].type = LV_XML_TEST_STEP_TYPE_PRESS; + } + else if(lv_streq(name, "release")) { + test.step_cnt++; + test.steps = lv_realloc(test.steps, sizeof(lv_xml_test_step_t) * test.step_cnt); + uint32_t idx = test.step_cnt - 1; + test.steps[idx].type = LV_XML_TEST_STEP_TYPE_RELEASE; + } + else if(lv_streq(name, "screenshot_compare")) { + test.step_cnt++; + const char * path = lv_xml_get_value_of(attrs, "path"); + test.steps = lv_realloc(test.steps, sizeof(lv_xml_test_step_t) * test.step_cnt); + uint32_t idx = test.step_cnt - 1; + test.steps[idx].type = LV_XML_TEST_STEP_TYPE_SCREENSHOT_COMPARE; + + char buf[256]; + lv_snprintf(buf, sizeof(buf), "%s%s", test.ref_image_path_prefix, path); + + test.steps[idx].param.screenshot_compare.path = lv_strdup(buf); + } + else if(lv_streq(name, "wait")) { + test.step_cnt++; + const char * ms = lv_xml_get_value_of(attrs, "ms"); + test.steps = lv_realloc(test.steps, sizeof(lv_xml_test_step_t) * test.step_cnt); + uint32_t idx = test.step_cnt - 1; + test.steps[idx].type = LV_XML_TEST_STEP_TYPE_WAIT; + test.steps[idx].param.wait.ms = lv_xml_atoi(ms); + } + else if(lv_streq(name, "freeze")) { + test.step_cnt++; + const char * ms = lv_xml_get_value_of(attrs, "ms"); + test.steps = lv_realloc(test.steps, sizeof(lv_xml_test_step_t) * test.step_cnt); + uint32_t idx = test.step_cnt - 1; + test.steps[idx].type = LV_XML_TEST_STEP_TYPE_FREEZE; + test.steps[idx].param.freeze.ms = lv_xml_atoi(ms); + } + else if(lv_streq(name, "subject_set")) { + const char * subject_str = lv_xml_get_value_of(attrs, "subject"); + const char * value_str = lv_xml_get_value_of(attrs, "value"); + if(subject_str == NULL) { + LV_LOG_WARN("subject is not set in `%s`", name); + return; + } + lv_subject_t * subject = lv_xml_get_subject(NULL, subject_str); + if(subject == NULL) { + LV_LOG_WARN("`%s` subject is not found in `%s`", subject_str, name); + return; + } + + test.step_cnt++; + test.steps = lv_realloc(test.steps, sizeof(lv_xml_test_step_t) * test.step_cnt); + uint32_t idx = test.step_cnt - 1; + test.steps[idx].type = LV_XML_TEST_STEP_TYPE_SUBJECT_SET; + test.steps[idx].param.subject_set.subject = subject; + test.steps[idx].param.subject_set.value = lv_strdup(value_str); + } + else if(lv_streq(name, "subject_compare")) { + const char * subject_str = lv_xml_get_value_of(attrs, "subject"); + const char * value_str = lv_xml_get_value_of(attrs, "value"); + if(subject_str == NULL) { + LV_LOG_WARN("subject is not set in `%s`", name); + return; + } + lv_subject_t * subject = lv_xml_get_subject(NULL, subject_str); + if(subject == NULL) { + LV_LOG_WARN("`%s` subject is not found in `%s`", subject_str, name); + return; + } + + test.step_cnt++; + test.steps = lv_realloc(test.steps, sizeof(lv_xml_test_step_t) * test.step_cnt); + uint32_t idx = test.step_cnt - 1; + test.steps[idx].type = LV_XML_TEST_STEP_TYPE_SUBJECT_COMPARE; + test.steps[idx].param.subject_compare.subject = subject; + test.steps[idx].param.subject_compare.value = lv_strdup(value_str); + } +} + +static void end_metadata_handler(void * user_data, const char * name) +{ + LV_UNUSED(user_data); + if(lv_streq(name, "steps")) { + test.processing_steps = 0; + return; + } +} + + +#endif /* LV_USE_XML */ diff --git a/src/others/xml/lv_xml_test.h b/src/others/xml/lv_xml_test.h new file mode 100644 index 0000000000..e215ee8ee9 --- /dev/null +++ b/src/others/xml/lv_xml_test.h @@ -0,0 +1,119 @@ +/** + * @file lv_xml_test.h + * + */ + +#ifndef LV_XML_TEST_H +#define LV_XML_TEST_H + +#ifdef __cplusplus +extern "C" { +#endif + +/********************* + * INCLUDES + *********************/ +#include "../../misc/lv_types.h" +#if LV_USE_XML && LV_USE_TEST + +/********************** + * TYPEDEFS + **********************/ + +typedef enum { + LV_XML_TEST_STEP_TYPE_NONE, + LV_XML_TEST_STEP_TYPE_MOVE_TO, + LV_XML_TEST_STEP_TYPE_PRESS, + LV_XML_TEST_STEP_TYPE_RELEASE, + LV_XML_TEST_STEP_TYPE_CLICK_AT, + LV_XML_TEST_STEP_TYPE_WAIT, + LV_XML_TEST_STEP_TYPE_FREEZE, + LV_XML_TEST_STEP_TYPE_SCREENSHOT_COMPARE, + LV_XML_TEST_STEP_TYPE_SUBJECT_SET, + LV_XML_TEST_STEP_TYPE_SUBJECT_COMPARE, +} lv_xml_test_step_type_t; + +/********************** + * GLOBAL PROTOTYPES + **********************/ + +/** + * Load the styles, constants, another data of the test. It needs to be called only once for each test. + * @param xml_def the XML definition of the test as a NULL terminated string + * @param ref_image_path_prefix prefix for the path of reference images + * @return LV_RES_OK: loaded successfully, LV_RES_INVALID: otherwise + */ +lv_result_t lv_xml_test_register_from_data(const char * xml_def, const char * ref_image_path_prefix); + +/** + * Load the styles, constants, another data of the test. It needs to be called only once for each test. + * @param path path to an XML file + * @param ref_image_path_prefix prefix for the path of reference images + * @return LV_RES_OK: loaded successfully, LV_RES_INVALID: otherwise + */ +lv_result_t lv_xml_test_register_from_file(const char * path, const char * ref_image_path_prefix); + +/** + * Free resources allocated for testing. + */ +void lv_xml_test_unregister(void); + +/** + * Switch to testing mode. Needs to be called to use `lv_xml_test_run_next()` + */ +void lv_xml_test_run_init(void); + +/** + * Run the next test step. + * @param slowdown 0: max speed, 1: real speed, 2: half speed, ... ,10: ten times slower + * @return true: the step passed; false: the step failed + */ +bool lv_xml_test_run_next(uint32_t slowdown); + +/** + * Leave testing mode. + */ +void lv_xml_test_run_stop(void); + +/** + * Run all the test steps. + * It calls `lv_xml_test_run_init()`, `lv_xml_test_run_next()`, and `lv_xml_test_run_stop` + * internally so no further preparation or cleanup is needed. + * @param slowdown 0: max speed, 1: real speed, 2: half speed, ... ,10: ten times slower + * @return number of failed tests + */ +uint32_t lv_xml_test_run_all(uint32_t slowdown); + +/** + * Get the number of steps in a test + * @return the number of ``s + */ +uint32_t lv_xml_test_get_step_count(void); + +/** + * Get the type of a step + * @param idx the index of a step (< step_count) + * @return element of `lv_xml_test_step_type_t` + */ +lv_xml_test_step_type_t lv_xml_test_get_step_type(uint32_t idx); + +/** + * Check if the a step was passed. Can be called after lv_xml_test_run() + * @param idx the index of a step (< step_count) + * @return true: the step passed, false: the step failed + */ +bool lv_xml_test_get_status(uint32_t idx); + +/********************** + * MACROS + **********************/ + +#endif /* LV_USE_XML */ + +#ifdef __cplusplus +} /*extern "C"*/ +#endif + +#endif /*LV_XML_TEST_H*/ + + diff --git a/src/tick/lv_tick.c b/src/tick/lv_tick.c index 21e22959d9..ed0fe0ada9 100644 --- a/src/tick/lv_tick.c +++ b/src/tick/lv_tick.c @@ -103,6 +103,11 @@ void lv_tick_set_cb(lv_tick_get_cb_t cb) state.tick_get_cb = cb; } +lv_tick_get_cb_t lv_tick_get_cb(void) +{ + return state.tick_get_cb; +} + void lv_delay_set_cb(lv_delay_cb_t cb) { state.delay_cb = cb; diff --git a/src/tick/lv_tick.h b/src/tick/lv_tick.h index 048b93a31a..b660e60c55 100644 --- a/src/tick/lv_tick.h +++ b/src/tick/lv_tick.h @@ -62,17 +62,26 @@ uint32_t lv_tick_elaps(uint32_t prev_tick); */ void lv_delay_ms(uint32_t ms); +/** + * Set a callback for a blocking delay + * @param cb pointer to a callback + */ +void lv_delay_set_cb(lv_delay_cb_t cb); + /** * Set the custom callback for 'lv_tick_get' * @param cb call this callback on 'lv_tick_get' */ void lv_tick_set_cb(lv_tick_get_cb_t cb); + /** - * Set a custom callback for 'lv_delay_ms' - * @param cb call this callback in 'lv_delay_ms' + * Get the custom callback for 'lv_tick_get' + * @return call this callback on 'lv_tick_get' */ -void lv_delay_set_cb(lv_delay_cb_t cb); +lv_tick_get_cb_t lv_tick_get_cb(void); + + /********************** * MACROS