Import of the watch repository from Pebble

This commit is contained in:
Matthieu Jeanson 2024-12-12 16:43:03 -08:00 committed by Katharine Berry
commit 3b92768480
10334 changed files with 2564465 additions and 0 deletions

View file

@ -0,0 +1,29 @@
/*
* Copyright 2024 Google 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.
*/
#pragma once
#include "jmem-heap.h"
static int s_app_heap_analytics_log_stats_to_app_heartbeat_call_count;
void app_heap_analytics_log_stats_to_app_heartbeat(bool is_rocky_app) {
s_app_heap_analytics_log_stats_to_app_heartbeat_call_count++;
}
static int s_app_heap_analytics_log_rocky_heap_oom_fault_call_count;
void app_heap_analytics_log_rocky_heap_oom_fault(void) {
s_app_heap_analytics_log_rocky_heap_oom_fault_call_count++;
}

View file

@ -0,0 +1,588 @@
/*
* Copyright 2024 Google 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.
*/
#include "clar.h"
#include "test_jerry_port_common.h"
#include "test_rocky_common.h"
#include "applib/graphics/gpath.h"
#include "applib/preferred_content_size.h"
#include "applib/rockyjs/api/rocky_api.h"
#include "applib/rockyjs/api/rocky_api_global.h"
#include "applib/rockyjs/api/rocky_api_timers.h"
#include "applib/rockyjs/api/rocky_api_graphics.h"
#include "applib/rockyjs/api/rocky_api_tickservice.h"
#include "applib/rockyjs/api/rocky_api_util.h"
#include "applib/rockyjs/pbl_jerry_port.h"
#include "syscall/syscall.h"
// Standard
#include "string.h"
#include "applib/rockyjs/rocky.h"
// Fakes
#include "fake_app_timer.h"
#include "fake_pbl_malloc.h"
#include "fake_time.h"
#include "fake_logging.h"
// Stubs
#include "stubs_app_manager.h"
#include "stubs_app_state.h"
#include "stubs_logging.h"
#include "stubs_passert.h"
#include "stubs_resources.h"
#include "stubs_sleep.h"
#include "stubs_serial.h"
#include "stubs_syscalls.h"
#include "stubs_sys_exit.h"
const RockyGlobalAPI APP_MESSAGE_APIS = {};
const RockyGlobalAPI WATCHINFO_APIS = {};
size_t heap_bytes_free(void) {
return 123456;
}
void sys_analytics_inc(AnalyticsMetric metric, AnalyticsClient client) {
}
bool sys_get_current_app_is_rocky_app(void) {
return true;
}
void tick_timer_service_subscribe(TimeUnits tick_units, TickHandler handler) {}
static Window s_app_window_stack_get_top_window;
Window *app_window_stack_get_top_window() {
return &s_app_window_stack_get_top_window;
}
PreferredContentSize preferred_content_size(void) {
return PreferredContentSizeMedium;
}
static MockCallRecordings s_layer_mark_dirty;
void layer_mark_dirty(Layer *layer) {
s_layer_mark_dirty.call_count++;
s_layer_mark_dirty.last_call = (MockCallRecording){.layer = layer};
}
static MockCallRecordings s_graphics_context_set_fill_color;
void graphics_context_set_fill_color(GContext* ctx, GColor color) {
s_graphics_context_set_fill_color.call_count++;
s_graphics_context_set_fill_color.last_call = (MockCallRecording){.ctx = ctx, .color = color};
}
static MockCallRecordings s_graphics_context_set_stroke_color;
void graphics_context_set_stroke_color(GContext* ctx, GColor color) {
s_graphics_context_set_stroke_color.call_count++;
s_graphics_context_set_stroke_color.last_call = (MockCallRecording){.ctx = ctx, .color = color};
}
static MockCallRecordings s_graphics_context_set_stroke_width;
void graphics_context_set_stroke_width(GContext* ctx, uint8_t stroke_width) {
s_graphics_context_set_stroke_width.call_count++;
s_graphics_context_set_stroke_width.last_call =
(MockCallRecording){.ctx = ctx, .width =stroke_width};
}
static MockCallRecordings s_graphics_line_draw_precise_stroked;
void graphics_line_draw_precise_stroked(GContext* ctx, GPointPrecise p0, GPointPrecise p1) {
record_mock_call(s_graphics_line_draw_precise_stroked) {
.ctx = ctx, .pp0 = p0, .pp1 = p1,
};
}
static MockCallRecordings s_graphics_draw_line;
void graphics_draw_line(GContext* ctx, GPoint p0, GPoint p1) {
s_graphics_draw_line.call_count++;
s_graphics_draw_line.last_call =
(MockCallRecording){.ctx = ctx, .p0 = p0, .p1 = p1};
}
static MockCallRecordings s_graphics_fill_rect;
void graphics_fill_rect(GContext *ctx, const GRect *rect) {
s_graphics_fill_rect.call_count++;
s_graphics_fill_rect.last_call = (MockCallRecording){.ctx = ctx, .rect = *rect};
}
static MockCallRecordings s_graphics_fill_rect;
void graphics_fill_round_rect_by_value(GContext* ctx, GRect rect, uint16_t radius,
GCornerMask corner_mask) {
s_graphics_fill_rect.call_count++;
s_graphics_fill_rect.last_call = (MockCallRecording) {
.ctx = ctx,
.rect = rect,
.radius = radius,
.corner_mask = corner_mask,
};
}
GPointPrecise gpoint_from_polar_precise(const GPointPrecise *precise_center,
uint16_t precise_radius, int32_t angle) {
return GPointPreciseFromGPoint(GPointZero);
}
void graphics_draw_arc_precise_internal(GContext *ctx, GPointPrecise center, Fixed_S16_3 radius,
int32_t angle_start, int32_t angle_end) {}
void graphics_draw_rect_precise(GContext *ctx, const GRectPrecise *rect) {}
void graphics_fill_radial_precise_internal(GContext *ctx, GPointPrecise center,
Fixed_S16_3 radius_inner, Fixed_S16_3 radius_outer,
int32_t angle_start, int32_t angle_end) {}
void gpath_draw_filled(GContext* ctx, GPath *path) {}
void layer_get_unobstructed_bounds(const Layer *layer, GRect *bounds_out) {
*bounds_out = layer->bounds;
}
GFont fonts_get_system_font(const char *font_key) {
return (GFont)123;
}
void graphics_draw_text(GContext *ctx, const char *text, GFont const font, const GRect box,
const GTextOverflowMode overflow_mode, const GTextAlignment alignment,
GTextAttributes *text_attributes) {}
void graphics_text_attributes_destroy(GTextAttributes *text_attributes) {}
GSize graphics_text_layout_get_max_used_size(GContext *ctx, const char *text,
GFont const font, const GRect box,
const GTextOverflowMode overflow_mode,
const GTextAlignment alignment,
GTextLayoutCacheRef layout) {
return GSizeZero;
}
uint32_t resource_storage_get_num_entries(ResAppNum app_num, uint32_t resource_id) {
return 0;
}
static bool s_skip_mem_leak_check;
static void prv_init(void) {
rocky_runtime_context_init();
jerry_init(JERRY_INIT_EMPTY);
}
static void prv_deinit(void) {
jerry_cleanup();
rocky_runtime_context_deinit();
}
void test_js__initialize(void) {
fake_pbl_malloc_clear_tracking();
s_skip_mem_leak_check = false;
fake_app_timer_init();
prv_init();
s_app_window_stack_get_top_window = (Window){};
s_app_state_get_graphics_context = NULL;
s_layer_mark_dirty = (MockCallRecordings){};
s_graphics_context_set_fill_color = (MockCallRecordings){};
s_graphics_context_set_stroke_color = (MockCallRecordings){};
s_graphics_context_set_stroke_width = (MockCallRecordings){};
s_graphics_line_draw_precise_stroked = (MockCallRecordings){};
s_graphics_draw_line = (MockCallRecordings){};
s_graphics_fill_rect = (MockCallRecordings){};
s_app_event_loop_callback = NULL;
s_log_internal__expected = NULL;
s_app_heap_analytics_log_stats_to_app_heartbeat_call_count = 0;
s_app_heap_analytics_log_rocky_heap_oom_fault_call_count = 0;
}
void test_js__cleanup(void) {
fake_app_timer_deinit();
s_log_internal__expected = NULL;
// some tests deinitialize the engine, avoid double de-init
if (app_state_get_rocky_runtime_context() != NULL) {
prv_deinit();
}
// PBL-40702: test_js__init_deinit is leaking memory...
if (!s_skip_mem_leak_check) {
fake_pbl_malloc_check_net_allocs();
}
}
void test_js__addition(void) {
char script[] = "var a = 1; var b = 2; var c = a + b;";
EXECUTE_SCRIPT(script);
ASSERT_JS_GLOBAL_EQUALS_I("c", 3.0);
}
void test_js__eval_error(void) {
prv_deinit(); // engine will be re-initialized in rocky_event_loop~
char script[] = "function f({;";
s_log_internal__expected = (const char *[]){
"Not a snapshot, interpreting buffer as JS source code",
"Exception while Evaluating JS",
"SyntaxError: Identifier expected. [line: 1, column: 12]",
NULL };
rocky_event_loop_with_string_or_snapshot(script, sizeof(script));
cl_assert(*s_log_internal__expected == NULL);
}
AppTimer *rocky_timer_get_app_timer(void *data);
void test_js__init_deinit(void) {
// PBL-40702: test_js__init_deinit is leaking memory...
s_skip_mem_leak_check = true;
prv_deinit();
char *script =
"var num_times = 0;"
"var extra_arg = 0;"
"var timer = setInterval(function(extra) {"
"num_times++;"
"extra_arg = extra;"
"}, 1000, 5);";
for (int i = 0; i < 30; ++i) {
prv_init();
TIMER_APIS.init();
EXECUTE_SCRIPT(script);
prv_deinit();
}
prv_init();
}
static char *prv_load_js(char *suffix) {
char path[512] = {0};
snprintf(path, sizeof(path), "%s/js/tictoc~rect~%s.js", CLAR_FIXTURE_PATH, suffix);
FILE *f = fopen(path, "r");
cl_assert(f);
fseek(f, 0, SEEK_END);
size_t length = (size_t)ftell(f);
fseek (f, 0, SEEK_SET);
char *buffer = malloc(length + 1);
memset(buffer, 0, length + 1);
cl_assert(buffer);
fread (buffer, 1, length, f);
fclose (f);
return buffer;
}
void test_js__call_cleanup_twice(void) {
prv_deinit();
char *script = "function f(i) { return i * 4; } f(5);";
bool result = rocky_event_loop_with_string_or_snapshot(script, strlen(script));
cl_assert(result);
}
static bool s_tictoc_callback_is_color;
static void prv_rocky_tictoc_callback(void) {
Layer *root_layer = &s_app_window_stack_get_top_window.layer;
root_layer->bounds = GRect(10, 20, 30, 40);
cl_assert(root_layer->update_proc);
GContext ctx = {.lock = true};
root_layer->update_proc(root_layer, &ctx);
if (s_tictoc_callback_is_color) {
cl_assert_equal_i(1, s_graphics_fill_rect.call_count);
cl_assert_equal_i(4, s_graphics_line_draw_precise_stroked.call_count);
cl_assert_equal_i(0, s_graphics_draw_line.call_count);
cl_assert_equal_i(1, s_graphics_context_set_fill_color.call_count);
cl_assert_equal_i(4, s_graphics_context_set_stroke_color.call_count);
cl_assert_equal_i(4, s_graphics_context_set_stroke_width.call_count);
} else {
cl_assert_equal_i(2, s_graphics_fill_rect.call_count);
cl_assert_equal_i(0, s_graphics_line_draw_precise_stroked.call_count);
cl_assert_equal_i(0, s_graphics_draw_line.call_count);
cl_assert_equal_i(1, s_graphics_context_set_fill_color.call_count);
cl_assert_equal_i(0, s_graphics_context_set_stroke_color.call_count);
cl_assert_equal_i(0, s_graphics_context_set_stroke_width.call_count);
}
// run update proc multiple times to verify we don't have a memory leak
for (int i = 1024; i >=0; i--) {
root_layer->update_proc(root_layer, &ctx);
}
}
void test_js__rocky_tictoc_color(void) {
prv_deinit(); // engine will be re-initialized in rocky_event_loop~
char *script = prv_load_js("color");
s_tictoc_callback_is_color = true;
s_app_event_loop_callback = prv_rocky_tictoc_callback;
bool result = rocky_event_loop_with_string_or_snapshot(script, strlen(script));
cl_assert(result);
}
void test_js__rocky_tictoc_bw(void) {
GContext ctx = {};
s_app_state_get_graphics_context = &ctx;
prv_deinit(); // engine will be re-initialized in rocky_event_loop~
char *script = prv_load_js("bw");
s_tictoc_callback_is_color = false;
s_app_event_loop_callback = prv_rocky_tictoc_callback;
bool result = rocky_event_loop_with_string_or_snapshot(script, strlen(script));
cl_assert(result);
}
void test_js__recursion(void) {
const char script[] =
"function f(i) { \n"
" if (i == 0) {_rocky.requestDraw();} \n"
" else {f(i-1)}\n"
"}\n"
"f(10)";
static const RockyGlobalAPI *apis[] = {
&GRAPHIC_APIS,
NULL,
};
rocky_global_init(apis);
EXECUTE_SCRIPT(script);
cl_assert_equal_i(1, s_layer_mark_dirty.call_count);
}
void test_js__no_print_builtin(void) {
JS_VAR global_obj = jerry_get_global_object();
JS_VAR print_builtin = jerry_get_object_field(global_obj, "print");
cl_assert_equal_b(true, jerry_value_is_undefined(print_builtin));
}
void test_js__sin_cos(void) {
EXECUTE_SCRIPT(
"var s1 = 100 + 50 * Math.sin(0);\n"
"var s2 = 100 + 50 * Math.sin(2 * Math.PI);\n"
"var c1 = 100 + 50 * Math.cos(0);\n"
"var c2 = 100 + 50 * Math.cos(2 * Math.PI);\n"
);
cl_assert_equal_i(100, (int32_t)jerry_get_number_value(prv_js_global_get_value("s1")));
cl_assert_equal_i(99, (int32_t)jerry_get_number_value(prv_js_global_get_value("s2")));
cl_assert_equal_i(150, (int32_t)jerry_get_number_value(prv_js_global_get_value("c1")));
cl_assert_equal_i(150, (int32_t)jerry_get_number_value(prv_js_global_get_value("c2")));
cl_assert_equal_i(100, jerry_get_int32_value(prv_js_global_get_value("s1")));
cl_assert_equal_i(100, jerry_get_int32_value(prv_js_global_get_value("s2")));
cl_assert_equal_i(150, jerry_get_int32_value(prv_js_global_get_value("c1")));
cl_assert_equal_i(150, jerry_get_int32_value(prv_js_global_get_value("c2")));
}
void test_js__date(void) {
const time_t cur_time = 1458250851; // Thu Mar 17 21:40:51 2016 UTC
// Thu Mar 17 14:40:51 2016 PDT
const uint16_t cur_millis = 123;
fake_time_init(cur_time, cur_millis);
fake_time_set_gmtoff(-8 * 60 * 60); // PST
fake_time_set_dst(1 * 60 * 60, 1458111600, 1465628400); // PDT 3/16 -> 11/6 2016
char *script =
"var date_now = new Date();"
"var now = date_now.getTime();"
"var local_day = date_now.getDay();"
"var local_hour = date_now.getHours();";
EXECUTE_SCRIPT(script);
ASSERT_JS_GLOBAL_EQUALS_D("now", (double)cur_time * 1000.0 + (double)cur_millis);
ASSERT_JS_GLOBAL_EQUALS_D("local_day", 4.0); // Thursday
ASSERT_JS_GLOBAL_EQUALS_D("local_hour", 14.0); // 1pm
}
void test_js__log_exception(void) {
char *script =
"var e1;\n"
"var f1 = function(){throw new Error('test')};\n"
"var f2 = function(){throw new 'test';};\n"
"var f2 = function(){throw new 123;};\n"
"try {f1();} catch(e) {e1 = e;}\n"
"try {f2();} catch(e) {e2 = e;}\n"
"try {f3();} catch(e) {e3 = e;}\n";
EXECUTE_SCRIPT(script);
jerry_value_t e1 = prv_js_global_get_value("e1");
jerry_value_t e2 = prv_js_global_get_value("e2");
jerry_value_t e3 = prv_js_global_get_value("e3");
// error
s_log_internal__expected = (const char *[]){
"Exception while e1", "Error: test", NULL,
};
rocky_log_exception("e1", e1);
cl_assert(*s_log_internal__expected == NULL);
// string
s_log_internal__expected = (const char *[]){
"Exception while e2", "TypeError", NULL,
};
rocky_log_exception("e2", e2);
cl_assert(*s_log_internal__expected == NULL);
// number
s_log_internal__expected = (const char *[]){
"Exception while e3", "ReferenceError", NULL,
};
rocky_log_exception("e3", e3);
cl_assert(*s_log_internal__expected == NULL);
}
/*
* FIXME: JS Tests should be built in a 32-bit env
void test_js__size(void) {
cl_assert_equal_i(4, sizeof(size_t));
}
*/
void test_js__snapshot(void) {
prv_deinit();
rocky_runtime_context_init();
jerry_init(JERRY_INIT_SHOW_OPCODES);
char *const script = prv_load_js("color");
uint8_t snapshot[65536] = { 0 };
// make sure snapshot data starts with expected Rocky header
const size_t header_size = sizeof(ROCKY_EXPECTED_SNAPSHOT_HEADER);
cl_assert_equal_i(8, header_size);
// NOTE: the snapshot header in this unit test is fixed to
// CAPABILITY_JAVASCRIPT_BYTECODE_VERSION=1 only use the resulting binary
// if the true JS version matches
memcpy(snapshot, &ROCKY_EXPECTED_SNAPSHOT_HEADER, header_size);
const size_t snapshot_size = jerry_parse_and_save_snapshot((const jerry_char_t *)script,
strlen(script),
true, /* is_for_global */
false, /* is_strict */
snapshot + header_size,
sizeof(snapshot) - header_size);
cl_assert(snapshot_size > 512); // make sure it contains "something" and compiling didn't fail
prv_deinit();
bool result = rocky_event_loop_with_string_or_snapshot(snapshot, snapshot_size);
cl_assert(result);
}
static int s_cleanup_calls;
static void prv_cleanup_cb(const uintptr_t native_p) {
++s_cleanup_calls;
}
void test_js__js_value_cleanup(void) {
s_cleanup_calls = 0;
{
// Sanity check:
// we don't clean up when a bare jerry_value_t goes out of scope.
jerry_value_t value = jerry_create_object();
jerry_set_object_native_handle(value, 0, prv_cleanup_cb);
}
jerry_gc(); // Perform GC in case refcount = 0
cl_assert_equal_i(s_cleanup_calls, 0); // Never release()d, so wasn't cleaned.
{
// When this goes out of scope, we do clean up
JS_VAR value = jerry_create_object();
jerry_set_object_native_handle(value, 0, prv_cleanup_cb);
}
jerry_gc(); // Perform GC so it will be cleaned up if refcount = 0
cl_assert_equal_i(s_cleanup_calls, 1); // Make sure that it was cleaned up
{
// Create a regular value, attach the native handle
jerry_value_t value = jerry_create_object();
jerry_set_object_native_handle(value, 0, prv_cleanup_cb);
// Create an autoreleased variable that points to the same, it will be cleaned up.
JS_UNUSED_VAL = value;
}
jerry_gc();
cl_assert_equal_i(s_cleanup_calls, 2);
{
// Naming check on unused variables, shouldn't clash.
// This is really just a compile-time test.
JS_UNUSED_VAL = jerry_create_object();
JS_UNUSED_VAL = jerry_create_object();
}
}
void test_js__get_global_builtin(void) {
jerry_value_t date_builtin = jerry_get_global_builtin((const jerry_char_t *)"Date");
cl_assert(!jerry_value_is_undefined(date_builtin));
cl_assert(jerry_value_is_constructor(date_builtin));
jerry_release_value(date_builtin);
jerry_value_t json_builtin = jerry_get_global_builtin((const jerry_char_t *)"JSON");
cl_assert(jerry_value_is_object(json_builtin));
jerry_release_value(json_builtin);
jerry_value_t not_builtin = jerry_get_global_builtin((const jerry_char_t *)"_not_builtin_");
cl_assert(jerry_value_is_undefined(not_builtin));
}
void test_js__get_global_builtin_compare(void) {
jerry_value_t date_builtin = jerry_get_global_builtin((const jerry_char_t *)"Date");
jerry_value_t global_object = jerry_get_global_object();
// Compare that the global Date is the same object as the builtin
jerry_value_t global_date = jerry_get_object_field(global_object, "Date");
cl_assert(date_builtin == global_date);
jerry_release_value(global_date);
jerry_release_value(global_object);
jerry_release_value(date_builtin);
}
void test_js__get_global_builtin_changed(void) {
jerry_value_t date_builtin = jerry_get_global_builtin((const jerry_char_t *)"Date");
jerry_value_t global_object = jerry_get_global_object();
const char *source = "Date = 'some string';";
jerry_eval((const jerry_char_t *)source, strlen(source), false);
// After changing the global date object, it should not match our builtin
jerry_value_t global_date = jerry_get_object_field(global_object, "Date");
cl_assert(jerry_value_is_string(global_date));
cl_assert(date_builtin != global_date);
jerry_release_value(global_date);
jerry_release_value(global_object);
jerry_release_value(date_builtin);
}
void test_js__capture_mem_stats_upon_exiting_event_loop(void) {
prv_deinit();
s_app_event_loop_callback = NULL;
const char *source = ";";
cl_assert_equal_b(true, rocky_event_loop_with_string_or_snapshot(source, strlen(source)));
cl_assert_equal_i(s_app_heap_analytics_log_stats_to_app_heartbeat_call_count, 1);
}
void test_js__jmem_heap_stats_largest_free_block_bytes(void) {
jmem_heap_stats_t stats = {};
jmem_heap_get_stats(&stats);
// Note: this might fail in the future if JerryScript would happen to cause fragmentation right
// upon initializing the engine:
cl_assert_equal_i(stats.size - stats.allocated_bytes, stats.largest_free_block_bytes);
}
void test_js__capture_jerry_heap_oom_stats(void) {
const char *source = "var big = []; for (;;) { big += 'bigger'; };";
cl_assert_passert(jerry_eval((const jerry_char_t *)source, strlen(source), false));
cl_assert_equal_i(s_app_heap_analytics_log_rocky_heap_oom_fault_call_count, 1);
}

View file

@ -0,0 +1,971 @@
/*
* Copyright 2024 Google 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.
*/
#include "clar.h"
#include "test_jerry_port_common.h"
#include "test_rocky_common.h"
#include "applib/rockyjs/api/rocky_api_global.h"
#include "applib/rockyjs/api/rocky_api_app_message.h"
#include "applib/rockyjs/pbl_jerry_port.h"
#include "applib/app_message/app_message.h"
#include "util/dict.h"
#include "util/size.h"
#include <string.h>
// Fakes
#include "fake_app_timer.h"
#include "fake_event_service.h"
#include "fake_pbl_malloc.h"
#include "fake_time.h"
// Stubs
#include "stubs_app_state.h"
#include "stubs_comm_session.h"
#include "stubs_logging.h"
#include "stubs_passert.h"
#include "stubs_serial.h"
#include "stubs_sys_exit.h"
extern PostMessageState rocky_api_app_message_get_state(void);
extern AppTimer *rocky_api_app_message_get_app_msg_retry_timer(void);
extern AppTimer *rocky_api_app_message_get_session_closed_object_queue_timer(void);
T_STATIC jerry_value_t prv_json_stringify(jerry_value_t object);
T_STATIC jerry_value_t prv_json_parse(const char *);
T_STATIC void prv_handle_connection(void);
T_STATIC void prv_handle_disconnection(void);
// App message mocks
static AppMessageInboxReceived s_received_callback;
AppMessageInboxReceived app_message_register_inbox_received(
AppMessageInboxReceived received_callback) {
AppMessageInboxReceived prev_cb = s_received_callback;
s_received_callback = received_callback;
return prev_cb;
}
static AppMessageInboxDropped s_dropped_callback;
AppMessageInboxDropped app_message_register_inbox_dropped(AppMessageInboxDropped dropped_callback) {
AppMessageInboxDropped prev_cb = s_dropped_callback;
s_dropped_callback = dropped_callback;
return prev_cb;
}
static AppMessageOutboxSent s_sent_callback;
AppMessageOutboxSent app_message_register_outbox_sent(AppMessageOutboxSent sent_callback) {
AppMessageOutboxSent prev_cb = s_sent_callback;
s_sent_callback = sent_callback;
return prev_cb;
}
static AppMessageOutboxFailed s_failed_callback;
AppMessageOutboxFailed app_message_register_outbox_failed(AppMessageOutboxFailed failed_callback) {
AppMessageOutboxFailed prev_cb = s_failed_callback;
s_failed_callback = failed_callback;
return prev_cb;
}
void app_message_deregister_callbacks(void) {
s_received_callback = NULL;
s_dropped_callback = NULL;
s_sent_callback = NULL;
s_failed_callback = NULL;
}
static uint32_t s_inbox_size;
static uint32_t s_outbox_size;
AppMessageResult app_message_open(const uint32_t size_inbound, const uint32_t size_outbound) {
s_inbox_size = size_inbound;
s_outbox_size = size_outbound;
return APP_MSG_OK;
}
static bool s_is_outbox_message_pending;
static DictionaryIterator s_outbox_iterator;
static uint8_t *s_outbox_buffer;
AppMessageResult app_message_outbox_begin(DictionaryIterator **iterator) {
cl_assert_equal_b(s_is_outbox_message_pending, false);
if (!s_outbox_buffer) {
s_outbox_buffer = malloc(s_outbox_size);
}
dict_write_begin(&s_outbox_iterator, s_outbox_buffer, s_outbox_size);
*iterator = &s_outbox_iterator;
return APP_MSG_OK;
}
static int s_app_message_outbox_send_call_count;
AppMessageResult app_message_outbox_send(void) {
++s_app_message_outbox_send_call_count;
s_is_outbox_message_pending = true;
return APP_MSG_OK;
}
static bool s_comm_session_connected;
CommSession *sys_app_pp_get_comm_session(void) {
return (CommSession *)s_comm_session_connected;
}
static void prv_rcv_app_message_ack(AppMessageResult result) {
void *context = NULL;
cl_assert_equal_b(s_is_outbox_message_pending, true);
s_is_outbox_message_pending = false;
if (result == APP_MSG_OK) {
s_sent_callback(&s_outbox_iterator, context);
} else {
s_failed_callback(&s_outbox_iterator, result, context);
}
}
static void prv_app_message_setup(void) {
s_inbox_size = 0;
s_outbox_size = 0;
s_outbox_buffer = NULL;
s_app_message_outbox_send_call_count = 0;
s_is_outbox_message_pending = false;
app_message_deregister_callbacks();
}
static void prv_app_message_teardown(void) {
if (s_outbox_buffer) {
free(s_outbox_buffer);
}
}
// Statics and Utilities
static void prv_init_and_goto_session_open(void);
static void prv_simulate_transport_connection_event(bool is_connected) {
// FIXME: use events here instead of poking at the internals!
if (is_connected) {
prv_handle_connection();
} else {
prv_handle_disconnection();
}
}
static const RockyGlobalAPI *s_app_message_api[] = {
&APP_MESSAGE_APIS,
NULL,
};
static void prv_init_api(bool start_connected) {
s_comm_session_connected = start_connected;
rocky_global_init(s_app_message_api);
}
// Setup
void test_rocky_api_app_message__initialize(void) {
fake_app_timer_init();
fake_pbl_malloc_clear_tracking();
prv_app_message_setup();
s_process_manager_callback = NULL;
s_process_manager_callback_data = NULL;
rocky_runtime_context_init();
jerry_init(JERRY_INIT_EMPTY);
}
void test_rocky_api_app_message__cleanup(void) {
rocky_global_deinit();
jerry_cleanup();
rocky_runtime_context_deinit();
prv_app_message_teardown();
fake_pbl_malloc_check_net_allocs();
fake_app_timer_deinit();
}
static const PostMessageResetCompletePayload VALID_RESET_COMPLETE = {
.min_supported_version = POSTMESSAGE_PROTOCOL_MIN_VERSION,
.max_supported_version = POSTMESSAGE_PROTOCOL_MAX_VERSION,
.max_tx_chunk_size = POSTMESSAGE_PROTOCOL_MAX_TX_CHUNK_SIZE,
.max_rx_chunk_size = POSTMESSAGE_PROTOCOL_MAX_RX_CHUNK_SIZE,
};
static const size_t TINY_CHUNK_SIZE = 4;
static const PostMessageResetCompletePayload TINY_RESET_COMPLETE = {
.min_supported_version = POSTMESSAGE_PROTOCOL_MIN_VERSION,
.max_supported_version = POSTMESSAGE_PROTOCOL_MAX_VERSION,
.max_tx_chunk_size = TINY_CHUNK_SIZE,
.max_rx_chunk_size = TINY_CHUNK_SIZE,
};
#define RCV_APP_MESSAGE(...) \
do { \
Tuplet tuplets[] = { __VA_ARGS__ }; \
uint32_t buffer_size = dict_calc_buffer_size_from_tuplets(tuplets, ARRAY_LENGTH(tuplets)); \
uint8_t buffer[buffer_size]; \
DictionaryIterator it; \
const DictionaryResult result = \
dict_serialize_tuplets_to_buffer_with_iter(&it, tuplets, ARRAY_LENGTH(tuplets), \
buffer, &buffer_size); \
cl_assert_equal_i(DICT_OK, result); \
if (s_received_callback) { \
s_received_callback(&it, NULL); \
} \
} while(0);
#define RCV_RESET_REQUEST() \
RCV_APP_MESSAGE(TupletBytes(PostMessageKeyResetRequest, NULL, 0));
#define RCV_RESET_COMPLETE() \
RCV_APP_MESSAGE(TupletBytes(PostMessageKeyResetComplete, \
(const uint8_t *)&VALID_RESET_COMPLETE, sizeof(VALID_RESET_COMPLETE)));
#define RCV_DUMMY_CHUNK() \
do { \
PostMessageChunkPayload chunk = {}; \
RCV_APP_MESSAGE(TupletBytes(PostMessageKeyChunk, (const uint8_t *) &chunk, sizeof(chunk))); \
} while(0);
//! Asserts whether the outbox has a pending message containing the tuples passed to this macro.
//! The value and type of the tuples is also asserted.
//! @note Only asserts if expected tuples are MISSING. It will not trip if there are other
//! (non-expected) tuples in the set.
#define EXPECT_OUTBOX_MESSAGE_PENDING(...) \
do { \
cl_assert_equal_b(true, s_is_outbox_message_pending); \
/* The cursor must be updated! */ \
cl_assert(s_outbox_iterator.cursor != s_outbox_iterator.dictionary->head); \
Tuplet tuplets[] = { __VA_ARGS__ }; \
uint32_t buffer_size = dict_calc_buffer_size_from_tuplets(tuplets, ARRAY_LENGTH(tuplets)); \
uint8_t buffer[buffer_size]; \
DictionaryIterator expected_it; \
const DictionaryResult result = \
dict_serialize_tuplets_to_buffer_with_iter(&expected_it, tuplets, ARRAY_LENGTH(tuplets), \
buffer, &buffer_size); \
cl_assert_equal_i(DICT_OK, result); \
for (Tuple *expected_t = dict_read_first(&expected_it); expected_t != NULL; \
expected_t = dict_read_next(&expected_it)) { \
Tuple *found_t = dict_find(&s_outbox_iterator, expected_t->key); \
cl_assert(found_t); \
cl_assert_equal_i(found_t->type, expected_t->type); \
cl_assert_equal_i(found_t->length, expected_t->length); \
if (expected_t->length) { \
cl_assert_equal_i(0, memcmp(found_t->value[0].data, expected_t->value[0].data, \
expected_t->length)); \
} \
} \
} while (0);
#define EXPECT_OUTBOX_NO_MESSAGE_PENDING() \
cl_assert_equal_b(false, s_is_outbox_message_pending);
#define EXPECT_OUTBOX_RESET_REQUEST() \
EXPECT_OUTBOX_MESSAGE_PENDING(TupletBytes(PostMessageKeyResetRequest, NULL, 0));
#define EXPECT_OUTBOX_RESET_COMPLETE_PENDING() \
EXPECT_OUTBOX_MESSAGE_PENDING(TupletBytes(PostMessageKeyResetComplete, \
(const uint8_t *) &VALID_RESET_COMPLETE, \
sizeof(VALID_RESET_COMPLETE)));
////////////////////////////////////////////////////////////////////////////////
// Negotiation Steps
////////////////////////////////////////////////////////////////////////////////
void test_rocky_api_app_message__disconnected__ignore_any_app_message(void) {
prv_init_api(false /* start_connected */);
for (PostMessageKey key = PostMessageKeyResetRequest; key < PostMessageKey_Count; ++key) {
uint8_t dummy_data[] = {0, 1, 2};
RCV_APP_MESSAGE(TupletBytes(key, dummy_data, sizeof(dummy_data)));
}
cl_assert_equal_i(0, s_app_message_outbox_send_call_count);
cl_assert_equal_i(rocky_api_app_message_get_state(), PostMessageStateDisconnected);
}
void test_rocky_api_app_message__awaiting_reset_request__receive_reset_request(void) {
prv_init_api(true /* start_connected */);
RCV_RESET_REQUEST();
// Expect responding with a ResetComplete:
EXPECT_OUTBOX_RESET_COMPLETE_PENDING();
cl_assert_equal_i(rocky_api_app_message_get_state(),
PostMessageStateAwaitingResetCompleteRemoteInitiated);
}
void test_rocky_api_app_message__awaiting_reset_request__receive_chunk(void) {
prv_init_api(false /* start_connected */);
prv_simulate_transport_connection_event(true /* is_connected */);
RCV_DUMMY_CHUNK();
// https://pebbletechnology.atlassian.net/browse/PBL-42466
// TODO: assert that app message was NACK'd
// Expect responding with a ResetRequest:
EXPECT_OUTBOX_RESET_REQUEST();
EXPECT_OUTBOX_MESSAGE_PENDING(TupletBytes(PostMessageKeyResetRequest, NULL, 0));
// TODO: check fields
cl_assert_equal_i(rocky_api_app_message_get_state(),
PostMessageStateAwaitingResetCompleteLocalInitiated);
}
void test_rocky_api_app_message__awaiting_reset_request__disconnect(void) {
prv_init_api(false /* start_connected */);
prv_simulate_transport_connection_event(true /* is_connected */);
prv_simulate_transport_connection_event(false /* is_connected */);
EXPECT_OUTBOX_NO_MESSAGE_PENDING();
cl_assert_equal_i(rocky_api_app_message_get_state(), PostMessageStateDisconnected);
}
static void prv_init_and_goto_awaiting_reset_complete_remote_initiated(void) {
prv_init_api(true /* start_connected */);
RCV_RESET_REQUEST();
EXPECT_OUTBOX_RESET_COMPLETE_PENDING();
prv_rcv_app_message_ack(APP_MSG_OK);
cl_assert_equal_i(rocky_api_app_message_get_state(),
PostMessageStateAwaitingResetCompleteRemoteInitiated);
}
static void prv_init_and_goto_awaiting_reset_complete_local_initiated(void) {
prv_init_and_goto_awaiting_reset_complete_remote_initiated();
RCV_DUMMY_CHUNK();
EXPECT_OUTBOX_RESET_REQUEST();
prv_rcv_app_message_ack(APP_MSG_OK);
cl_assert_equal_i(rocky_api_app_message_get_state(),
PostMessageStateAwaitingResetCompleteLocalInitiated);
}
void test_rocky_api_app_message__awaiting_reset_complete_rem_init__receive_complete_valid_version(void) {
prv_init_and_goto_awaiting_reset_complete_remote_initiated();
RCV_RESET_COMPLETE();
cl_assert_equal_i(rocky_api_app_message_get_state(), PostMessageStateSessionOpen);
EXPECT_OUTBOX_NO_MESSAGE_PENDING();
}
void test_rocky_api_app_message__awaiting_reset_complete_rem_init__receive_complete_unsupported_ver(void) {
prv_init_and_goto_awaiting_reset_complete_remote_initiated();
const PostMessageResetCompletePayload unsupported = {
.min_supported_version = POSTMESSAGE_PROTOCOL_MAX_VERSION + 1,
.max_supported_version = POSTMESSAGE_PROTOCOL_MAX_VERSION + 1,
.max_tx_chunk_size = POSTMESSAGE_PROTOCOL_MAX_TX_CHUNK_SIZE,
.max_rx_chunk_size = POSTMESSAGE_PROTOCOL_MAX_RX_CHUNK_SIZE,
};
RCV_APP_MESSAGE(TupletBytes(PostMessageKeyResetComplete,
(const uint8_t *)&unsupported, sizeof(unsupported)));
// Expect No UnsupportedError!
// Immediately go back to AwaitingResetRequest:
cl_assert_equal_i(rocky_api_app_message_get_state(), PostMessageStateAwaitingResetRequest);
EXPECT_OUTBOX_NO_MESSAGE_PENDING();
}
void test_rocky_api_app_message__awaiting_reset_complete_rem_init__malformed_reset_complete(void) {
prv_init_and_goto_awaiting_reset_complete_remote_initiated();
// Receive malformed ResetComplete:
uint8_t malformed_payload[sizeof(PostMessageResetCompletePayload) - 1] = {};
RCV_APP_MESSAGE(TupletBytes(PostMessageKeyResetComplete,
malformed_payload, sizeof(malformed_payload)));
// Expect Error:
const PostMessageUnsupportedErrorPayload expected_error = {
.error_code = PostMessageErrorMalformedResetComplete,
};
EXPECT_OUTBOX_MESSAGE_PENDING(TupletBytes(PostMessageKeyUnsupportedError,
(const uint8_t *) &expected_error,
sizeof(expected_error)));
// Immediately go back to AwaitingResetRequest:
cl_assert_equal_i(rocky_api_app_message_get_state(), PostMessageStateAwaitingResetRequest);
prv_rcv_app_message_ack(APP_MSG_OK);
EXPECT_OUTBOX_NO_MESSAGE_PENDING();
}
void test_rocky_api_app_message__awaiting_reset_complete_rem_init__receive_request(void) {
prv_init_and_goto_awaiting_reset_complete_remote_initiated();
RCV_RESET_REQUEST();
cl_assert_equal_i(rocky_api_app_message_get_state(),
PostMessageStateAwaitingResetCompleteRemoteInitiated);
EXPECT_OUTBOX_RESET_COMPLETE_PENDING();
}
void test_rocky_api_app_message__awaiting_reset_complete_rem_init__receive_chunk(void) {
prv_init_and_goto_awaiting_reset_complete_remote_initiated();
RCV_DUMMY_CHUNK();
EXPECT_OUTBOX_RESET_REQUEST();
cl_assert_equal_i(rocky_api_app_message_get_state(),
PostMessageStateAwaitingResetCompleteLocalInitiated);
// Receive yet another chunk in "Awaiting Reset Complete Local Initiated":
RCV_DUMMY_CHUNK();
// https://pebbletechnology.atlassian.net/browse/PBL-42466
// TODO: assert that chunk is NACKd
// Receive ACK for the ResetRequest:
prv_rcv_app_message_ack(APP_MSG_OK);
// Chunk is ignored, no new reset request is sent out.
EXPECT_OUTBOX_NO_MESSAGE_PENDING();
// TODO: timeout + retry ResetRequest if no ResetComplete follows within N secs.
}
void test_rocky_api_app_message__awaiting_reset_complete_loc_init__(void) {
prv_init_and_goto_awaiting_reset_complete_local_initiated();
}
void test_rocky_api_app_message__awaiting_reset_complete_loc_init__rcv_reset_request(void) {
prv_init_and_goto_awaiting_reset_complete_local_initiated();
RCV_RESET_REQUEST();
EXPECT_OUTBOX_RESET_COMPLETE_PENDING();
prv_rcv_app_message_ack(APP_MSG_OK);
cl_assert_equal_i(rocky_api_app_message_get_state(),
PostMessageStateAwaitingResetCompleteRemoteInitiated);
RCV_RESET_COMPLETE();
cl_assert_equal_i(rocky_api_app_message_get_state(), PostMessageStateSessionOpen);
}
void test_rocky_api_app_message__awaiting_reset_complete_loc_init__rcv_chunk(void) {
prv_init_and_goto_awaiting_reset_complete_local_initiated();
RCV_DUMMY_CHUNK();
// https://pebbletechnology.atlassian.net/browse/PBL-42466
// TODO: assert that chunk is NACK'd
// Chunk is ignored, state isn't changed:
cl_assert_equal_i(rocky_api_app_message_get_state(),
PostMessageStateAwaitingResetCompleteLocalInitiated);
EXPECT_OUTBOX_NO_MESSAGE_PENDING();
}
static void prv_init_and_goto_session_open(void) {
prv_init_and_goto_awaiting_reset_complete_remote_initiated();
RCV_RESET_COMPLETE();
cl_assert_equal_i(rocky_api_app_message_get_state(), PostMessageStateSessionOpen);
}
void test_rocky_api_app_message__session_open__rcv_reset_request(void) {
prv_init_and_goto_session_open();
EXECUTE_SCRIPT("var isCalled = false;"
"_rocky.on('postmessagedisconnected', function() { isCalled = true; });");
ASSERT_JS_GLOBAL_EQUALS_B("isCalled", false);
RCV_RESET_REQUEST();
cl_assert_equal_i(rocky_api_app_message_get_state(),
PostMessageStateAwaitingResetCompleteRemoteInitiated);
EXPECT_OUTBOX_RESET_COMPLETE_PENDING();
ASSERT_JS_GLOBAL_EQUALS_B("isCalled", true);
// TODO: assert:
// - flushed recv chunk reassembly buffer
}
void test_rocky_api_app_message__session_open__rcv_reset_complete(void) {
prv_init_and_goto_session_open();
EXECUTE_SCRIPT("var isCalled = false;"
"_rocky.on('postmessagedisconnected', function() { isCalled = true; });");
ASSERT_JS_GLOBAL_EQUALS_B("isCalled", false);
RCV_RESET_COMPLETE();
cl_assert_equal_i(rocky_api_app_message_get_state(),
PostMessageStateAwaitingResetCompleteLocalInitiated);
EXPECT_OUTBOX_RESET_REQUEST();
ASSERT_JS_GLOBAL_EQUALS_B("isCalled", true);
// TODO: assert:
// - flushed recv chunk reassembly buffer
}
////////////////////////////////////////////////////////////////////////////////
// postmessageconnected / postmessagedisconnected
////////////////////////////////////////////////////////////////////////////////
static void prv_postmessageconnected_postmessagedisconnected_init(bool start_connected) {
prv_init_api(start_connected);
EXECUTE_SCRIPT("var c = 0; var d = 0;\n"
"_rocky.on('postmessageconnected', function() { c++; });\n"
"_rocky.on('postmessagedisconnected', function() { d++; });\n");
// Make sure this race is handled (see comment in prv_handle_connection()):
prv_simulate_transport_connection_event(start_connected /* is_connected */);
}
static void prv_postmessageconnected_postmessagedisconnected_negotiate_to_open_session(void) {
// Negotiate:
RCV_RESET_REQUEST();
EXPECT_OUTBOX_RESET_COMPLETE_PENDING();
prv_rcv_app_message_ack(APP_MSG_OK);
cl_assert_equal_i(rocky_api_app_message_get_state(),
PostMessageStateAwaitingResetCompleteRemoteInitiated);
RCV_RESET_COMPLETE();
cl_assert_equal_i(rocky_api_app_message_get_state(), PostMessageStateSessionOpen);
}
void test_rocky_api_app_message__postmessageconnected_and_postmessagedisconnected_remote_rr(void) {
prv_postmessageconnected_postmessagedisconnected_init(false /* start_connected */);
ASSERT_JS_GLOBAL_EQUALS_I("d", 1);
prv_simulate_transport_connection_event(true /* is_connected */);
ASSERT_JS_GLOBAL_EQUALS_I("c", 0);
ASSERT_JS_GLOBAL_EQUALS_I("d", 1);
prv_postmessageconnected_postmessagedisconnected_negotiate_to_open_session();
ASSERT_JS_GLOBAL_EQUALS_I("c", 1);
ASSERT_JS_GLOBAL_EQUALS_I("d", 1);
// Get a ResetRequest:
RCV_RESET_REQUEST();
ASSERT_JS_GLOBAL_EQUALS_I("d", 2);
}
void test_rocky_api_app_message__postmessageconnected_and_postmessagedisconnected_local_rr(void) {
prv_postmessageconnected_postmessagedisconnected_init(false /* start_connected */);
ASSERT_JS_GLOBAL_EQUALS_I("d", 1);
prv_simulate_transport_connection_event(true /* is_connected */);
ASSERT_JS_GLOBAL_EQUALS_I("c", 0);
ASSERT_JS_GLOBAL_EQUALS_I("d", 1);
prv_postmessageconnected_postmessagedisconnected_negotiate_to_open_session();
ASSERT_JS_GLOBAL_EQUALS_I("c", 1);
ASSERT_JS_GLOBAL_EQUALS_I("d", 1);
// Get a ResetComplete (unexpected message), should trigger initiating (local) ResetRequest:
RCV_RESET_COMPLETE();
ASSERT_JS_GLOBAL_EQUALS_I("d", 2);
}
void test_rocky_api_app_message__postmessageconnected_and_postmessagedisconnected_start_conn(void) {
prv_postmessageconnected_postmessagedisconnected_init(true /* start_connected */);
ASSERT_JS_GLOBAL_EQUALS_I("c", 0);
ASSERT_JS_GLOBAL_EQUALS_I("d", 1);
prv_postmessageconnected_postmessagedisconnected_negotiate_to_open_session();
ASSERT_JS_GLOBAL_EQUALS_I("c", 1);
ASSERT_JS_GLOBAL_EQUALS_I("d", 1);
}
// TODO: test various min/max version combos
// TODO: test RX/TX buffer size combos
////////////////////////////////////////////////////////////////////////////////
// Generic Tests
////////////////////////////////////////////////////////////////////////////////
void test_rocky_api_app_message__json_stringify(void) {
JS_VAR obj = jerry_create_object();
JS_VAR json_str = prv_json_stringify(obj);
char *json_c_str = rocky_string_alloc_and_copy(json_str);
cl_assert_equal_s(json_c_str, "{}");
task_free(json_c_str);
}
void test_rocky_api_app_message__json_parse(void) {
JS_VAR number = prv_json_parse("1");
cl_assert(jerry_value_is_number(number));
cl_assert_equal_d(jerry_get_number_value(number), 1.0);
JS_VAR object = prv_json_parse("{ \"x\" : 42 }");
cl_assert(jerry_value_is_object(object));
JS_VAR x = jerry_get_object_field(object, "x");
cl_assert(jerry_value_is_number(x));
cl_assert_equal_d(jerry_get_number_value(x), 42.0);
}
void test_rocky_api_app_message__json_parse_stress(void) {
const int num_attempts = 0x3ff + 10; // Want this to be higher than the max refcount,
// which will also be high enough for a memory stress test
for (int i = 0; i < num_attempts; ++i) {
JS_UNUSED_VAL = prv_json_parse(
"var msg = { "
"\"key\" : "
"\"Bacon ipsum dolor amet kevin filet mignon id ut, aute sausage tri-tip "
"frankfurter pork loin. Boudin ullamco landjaeger, kevin tongue minim tri-tip "
"ground round dolore. Ham hock tongue swine, cillum jowl pancetta fugiat "
"deserunt sirloin fatback tenderloin culpa andouille. Incididunt qui bacon "
"nostrud ham hock adipisicing et ham. Ullamco esse eu capicola, ea culpa irure "
"meatball proident laboris ut reprehenderit ex incididunt.\" };\n");
}
}
////////////////////////////////////////////////////////////////////////////////
// .postMessage() Tests
////////////////////////////////////////////////////////////////////////////////
#define SIMPLE_TEST_OBJECT "{ \"x\" : 1 }"
static void prv_assert_simple_test_object_pending(void) {
const char * const expected_json = "{\"x\":1}";
const size_t expected_json_size = strlen(expected_json) + 1;
const size_t expected_size = sizeof(PostMessageChunkPayload) + strlen(expected_json) + 1;
uint8_t *buffer = task_malloc(expected_size);
PostMessageChunkPayload *chunk = (PostMessageChunkPayload *)buffer;
*chunk = (PostMessageChunkPayload) {
.total_size_bytes = expected_json_size,
.is_first = true,
};
memcpy(chunk->chunk_data, expected_json, expected_json_size);
EXPECT_OUTBOX_MESSAGE_PENDING(TupletBytes(PostMessageKeyChunk,
(const uint8_t *) chunk, expected_size));
// Compare with hard-coded byte array, to catch accidental changes to the ABI:
const uint8_t raw_bytes_v1[] = {
0x08, 0x00, 0x00, 0x80, 0x7b, 0x22, 0x78, 0x22, 0x3a, 0x31, 0x7d, 0x00,
};
cl_assert_equal_i(sizeof(raw_bytes_v1), expected_size);
cl_assert_equal_m(raw_bytes_v1, buffer, expected_size);
task_free(buffer);
}
void test_rocky_api_app_message__postmessage_just_before_connected(void) {
prv_init_api(false /* start_connected */);
EXECUTE_SCRIPT("var x = " SIMPLE_TEST_OBJECT ";"
"var hasError = false;"
"_rocky.on('postmessageerror', function() { hasError = true; });"
"_rocky.postMessage(x);");
// First send attempt fails because not in SessionOpen
ASSERT_JS_GLOBAL_EQUALS_B("hasError", false);
prv_simulate_transport_connection_event(true /* is_connected */);
prv_postmessageconnected_postmessagedisconnected_negotiate_to_open_session();
prv_assert_simple_test_object_pending();
prv_rcv_app_message_ack(APP_MSG_OK);
EXPECT_OUTBOX_NO_MESSAGE_PENDING();
ASSERT_JS_GLOBAL_EQUALS_B("hasError", false);
}
void test_rocky_api_app_message__post_message_single_chunk(void) {
prv_init_and_goto_session_open();
EXECUTE_SCRIPT("var x = " SIMPLE_TEST_OBJECT "; _rocky.postMessage(x);");
prv_assert_simple_test_object_pending();
prv_rcv_app_message_ack(APP_MSG_OK);
EXPECT_OUTBOX_NO_MESSAGE_PENDING();
}
static void prv_init_and_goto_session_open_with_tiny_buffers(void) {
prv_init_and_goto_awaiting_reset_complete_remote_initiated();
RCV_APP_MESSAGE(TupletBytes(PostMessageKeyResetComplete, \
(const uint8_t *)&TINY_RESET_COMPLETE, sizeof(TINY_RESET_COMPLETE)));
cl_assert_equal_i(rocky_api_app_message_get_state(), PostMessageStateSessionOpen);
}
void test_rocky_api_app_message__post_message_multi_chunk(void) {
prv_init_and_goto_session_open_with_tiny_buffers();
EXECUTE_SCRIPT("var x = { \"x\" : 123 }; _rocky.postMessage(x);");
const char * const expected_json = "{\"x\":123}";
const size_t expected_json_size = strlen(expected_json) + 1;
size_t json_bytes_remaining = expected_json_size;
uint8_t *buffer = task_malloc(sizeof(PostMessageChunkPayload) + TINY_CHUNK_SIZE);
// Chunk 1:
{
const size_t json_bytes_size = MIN(TINY_CHUNK_SIZE, json_bytes_remaining);
const size_t expected_size = sizeof(PostMessageChunkPayload) + json_bytes_size;
PostMessageChunkPayload *chunk = (PostMessageChunkPayload *)buffer;
*chunk = (PostMessageChunkPayload) {
.total_size_bytes = expected_json_size,
.is_first = true,
};
memcpy(chunk->chunk_data, expected_json, TINY_CHUNK_SIZE);
EXPECT_OUTBOX_MESSAGE_PENDING(TupletBytes(PostMessageKeyChunk,
(const uint8_t *) chunk, expected_size));
// Compare with hard-coded byte array, to catch accidental changes to the ABI:
const uint8_t raw_bytes_v1[] = {
0x0a, 0x00, 0x00, 0x80, '{', '"', 'x', '"',
};
cl_assert_equal_i(sizeof(raw_bytes_v1), expected_size);
cl_assert_equal_m(raw_bytes_v1, buffer, expected_size);
prv_rcv_app_message_ack(APP_MSG_OK);
json_bytes_remaining -= json_bytes_size;
}
// Chunk 2:
{
const size_t json_bytes_size = MIN(TINY_CHUNK_SIZE, json_bytes_remaining);
const size_t expected_size = sizeof(PostMessageChunkPayload) + json_bytes_size;
const int payload_offset = expected_json_size - json_bytes_remaining;
PostMessageChunkPayload *chunk = (PostMessageChunkPayload *)buffer;
*chunk = (PostMessageChunkPayload) {
.offset_bytes = payload_offset,
.continuation_is_first = false,
};
memcpy(chunk->chunk_data, expected_json + payload_offset, TINY_CHUNK_SIZE);
EXPECT_OUTBOX_MESSAGE_PENDING(TupletBytes(PostMessageKeyChunk,
(const uint8_t *) chunk, expected_size));
// Compare with hard-coded byte array, to catch accidental changes to the ABI:
const uint8_t raw_bytes_v1[] = {
0x04, 0x00, 0x00, 0x00, ':', '1', '2', '3',
};
cl_assert_equal_i(sizeof(raw_bytes_v1), expected_size);
cl_assert_equal_m(raw_bytes_v1, buffer, expected_size);
prv_rcv_app_message_ack(APP_MSG_OK);
json_bytes_remaining -= json_bytes_size;
}
// Chunk 3:
{
const size_t json_bytes_size = MIN(TINY_CHUNK_SIZE, json_bytes_remaining);
const size_t expected_size = sizeof(PostMessageChunkPayload) + json_bytes_size;
const int payload_offset = expected_json_size - json_bytes_remaining;
PostMessageChunkPayload *chunk = (PostMessageChunkPayload *)buffer;
*chunk = (PostMessageChunkPayload) {
.offset_bytes = payload_offset,
.continuation_is_first = false,
};
memcpy(chunk->chunk_data, expected_json + payload_offset, TINY_CHUNK_SIZE);
EXPECT_OUTBOX_MESSAGE_PENDING(TupletBytes(PostMessageKeyChunk,
(const uint8_t *) chunk, expected_size));
// Compare with hard-coded byte array, to catch accidental changes to the ABI:
const uint8_t raw_bytes_v1[] = {
0x08, 0x00, 0x00, 0x00, '}', '\0',
};
cl_assert_equal_i(sizeof(raw_bytes_v1), expected_size);
cl_assert_equal_m(raw_bytes_v1, buffer, expected_size);
prv_rcv_app_message_ack(APP_MSG_OK);
json_bytes_remaining -= json_bytes_size;
}
EXPECT_OUTBOX_NO_MESSAGE_PENDING();
task_free(buffer);
}
void test_rocky_api_app_message__postmessage_not_jsonable(void) {
prv_init_and_goto_session_open();
const char *not_jsonable_error =
"TypeError: Argument at index 0 is not a JSON.stringify()-able object";
EXECUTE_SCRIPT_EXPECT_ERROR("_rocky.postMessage(undefined);", not_jsonable_error);
EXECUTE_SCRIPT_EXPECT_ERROR("_rocky.postMessage(function() {});", not_jsonable_error);
EXECUTE_SCRIPT_EXPECT_ERROR("_rocky.postMessage({toJSON: function() {throw 'toJSONError';}});",
"toJSONError");
}
void test_rocky_api_app_message__postmessage_no_args(void) {
prv_init_api(false /* start_connected */);
EXECUTE_SCRIPT_EXPECT_ERROR("_rocky.postMessage();", "TypeError: Not enough arguments");
}
void test_rocky_api_app_message__postmessage_oom(void) {
prv_init_api(false /* start_connected */);
fake_malloc_set_largest_free_block(0);
EXECUTE_SCRIPT_EXPECT_ERROR("_rocky.postMessage('x');",
"RangeError: Out of memory: can't postMessage() -- object too large");
}
////////////////////////////////////////////////////////////////////////////////
// Receive Tests
////////////////////////////////////////////////////////////////////////////////
void test_rocky_api_app_message__receive_message_multi_chunk(void) {
prv_init_and_goto_session_open_with_tiny_buffers();
EXECUTE_SCRIPT("var event = null;\n"
"var json_str = null;\n"
"_rocky.on('message', function(e) {\n"
" json_str = JSON.stringify(e.data);\n" // stringify again to make assert simple
" event = e;\n"
"});");
JS_VAR event_null = prv_js_global_get_value("event");
cl_assert_equal_b(true, jerry_value_is_null(event_null));
// Get 3x the same message in a row:
for (int j = 0; j < 3; ++j) {
// Chunks for: {"x":123}
const struct {
uint8_t byte_array[8];
size_t length;
} chunk_msg_defs[] = {
{
.byte_array = {0x0a, 0x00, 0x00, 0x80, '{', '"', 'x', '"'},
.length = 8,
},
{
.byte_array = {0x04, 0x00, 0x00, 0x00, ':', '1', '2', '3'},
.length = 8,
},
{
.byte_array = {0x08, 0x00, 0x00, 0x00, '}', '\0'},
.length = 6,
}
};
for (int i = 0; i < ARRAY_LENGTH(chunk_msg_defs); ++i) {
RCV_APP_MESSAGE(TupletBytes(PostMessageKeyChunk,
(const uint8_t *) chunk_msg_defs[i].byte_array,
chunk_msg_defs[i].length));
}
JS_VAR event_valid = prv_js_global_get_value("event");
cl_assert_equal_b(true, jerry_value_is_object(event_valid));
// Make sure that there is a "data" property
JS_VAR data_prop = jerry_get_object_field(event_valid, "data");
cl_assert_equal_b(true, jerry_value_is_object(data_prop));
// Make sure the re-serialized object matches:
ASSERT_JS_GLOBAL_EQUALS_S("json_str", "{\"x\":123}");
EXECUTE_SCRIPT("json_str = null;\n"
"event = null");
}
}
////////////////////////////////////////////////////////////////////////////////
// "postmessageerror" event
////////////////////////////////////////////////////////////////////////////////
void test_rocky_api_app_message__postmessageerror(void) {
prv_init_and_goto_session_open();
EXECUTE_SCRIPT("var didError = false;"
"var x = { \"x\" : 1 };"
"var dataJSON = undefined;"
"_rocky.on('postmessageerror', "
" function(e) { didError = true; dataJSON = JSON.stringify(e.data); });"
"_rocky.postMessage(x);"
"x.x = 2;");
ASSERT_JS_GLOBAL_EQUALS_B("didError", false);
for (int i = 0; i < 3; ++i) {
cl_assert_equal_b(s_is_outbox_message_pending, true);
// NACK
prv_rcv_app_message_ack(APP_MSG_BUSY);
AppTimer *t = rocky_api_app_message_get_app_msg_retry_timer();
cl_assert(t != EVENTED_TIMER_INVALID_ID);
cl_assert_equal_b(fake_app_timer_is_scheduled(t), true);
// Enqueuing more objects shouldn't affect the pace at which things are retried:
EXECUTE_SCRIPT("_rocky.postMessage('')");
EXPECT_OUTBOX_NO_MESSAGE_PENDING();
cl_assert_equal_b(app_timer_trigger(t), true);
}
ASSERT_JS_GLOBAL_EQUALS_B("didError", true);
ASSERT_JS_GLOBAL_EQUALS_S("dataJSON", "{\"x\":1}");
}
void test_rocky_api_app_message__postmessageerror_while_disconnected(void) {
prv_init_api(false /* start_connected */);
EXECUTE_SCRIPT("var didError = false;"
"var x = " SIMPLE_TEST_OBJECT ";"
"_rocky.on('postmessageerror', "
" function(e) { didError = true; dataJSON = JSON.stringify(e.data); });"
/* 3x postMessage(): */
"_rocky.postMessage(x);"
"_rocky.postMessage(x);"
"_rocky.postMessage(x);");
// Let the first 2 timeout:
for (int i = 0; i < 2; ++i) {
ASSERT_JS_GLOBAL_EQUALS_B("didError", false);
AppTimer *t = rocky_api_app_message_get_session_closed_object_queue_timer();
cl_assert(t != EVENTED_TIMER_INVALID_ID);
cl_assert_equal_b(fake_app_timer_is_scheduled(t), true);
EXPECT_OUTBOX_NO_MESSAGE_PENDING();
cl_assert_equal_b(app_timer_trigger(t), true);
ASSERT_JS_GLOBAL_EQUALS_B("didError", true);
EXECUTE_SCRIPT("didError = false;");
}
// Timer for the 3rd should be set:
AppTimer *t = rocky_api_app_message_get_session_closed_object_queue_timer();
cl_assert(t != EVENTED_TIMER_INVALID_ID);
cl_assert_equal_b(fake_app_timer_is_scheduled(t), true);
// Connect:
prv_simulate_transport_connection_event(true /* is_connected */);
prv_postmessageconnected_postmessagedisconnected_negotiate_to_open_session();
// Timer for the 3rd should be cancelled now:
cl_assert(EVENTED_TIMER_INVALID_ID == rocky_api_app_message_get_session_closed_object_queue_timer());
prv_assert_simple_test_object_pending();
}

View file

@ -0,0 +1,122 @@
/*
* Copyright 2024 Google 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.
*/
#include "clar.h"
#include "test_jerry_port_common.h"
#include "test_rocky_common.h"
#include "applib/rockyjs/api/rocky_api_global.h"
#include "applib/rockyjs/api/rocky_api_console.h"
#include "applib/rockyjs/pbl_jerry_port.h"
// Standard
#include "string.h"
// Fakes
#include "fake_app_timer.h"
#include "fake_time.h"
#include "fake_logging.h"
// Stubs
#include "stubs_app_manager.h"
#include "stubs_app_state.h"
#include "stubs_logging.h"
#include "stubs_passert.h"
#include "stubs_pbl_malloc.h"
#include "stubs_resources.h"
#include "stubs_sleep.h"
#include "stubs_serial.h"
#include "stubs_syscalls.h"
#include "stubs_sys_exit.h"
size_t heap_bytes_free(void) {
return 123456;
}
static const RockyGlobalAPI *s_api[] = {
&CONSOLE_APIS,
NULL,
};
void test_rocky_api_console__initialize(void) {
rocky_runtime_context_init();
jerry_init(JERRY_INIT_EMPTY);
s_log_internal__expected = NULL;
}
void test_rocky_api_console__cleanup(void) {
jerry_cleanup();
rocky_runtime_context_deinit();
}
void test_rocky_api_console__functions_exist(void) {
rocky_global_init(s_api);
EXECUTE_SCRIPT(
"var c = typeof console;\n"
"var cl = typeof console.log;\n"
"var cw = typeof console.warn;\n"
"var ce = typeof console.error;\n"
);
ASSERT_JS_GLOBAL_EQUALS_S("c", "object");
ASSERT_JS_GLOBAL_EQUALS_S("cl", "function");
ASSERT_JS_GLOBAL_EQUALS_S("cw", "function");
ASSERT_JS_GLOBAL_EQUALS_S("ce", "function");
}
void test_rocky_api_console__logs_single_values(void) {
rocky_global_init(s_api);
s_log_internal__expected = (const char *[]){
"some string",
"1234",
"true",
"undefined",
"[object Object]",
NULL
};
EXECUTE_SCRIPT(
"console.log('some string');\n"
"console.log(1230 + 4);\n"
"console.log(1 == 1);\n"
"console.log(undefined);\n"
"console.log({a:123, b:[1,2]});\n"
);
cl_assert_equal_s(NULL, *s_log_internal__expected);
}
void test_rocky_api_console__warn_error_multiple(void) {
rocky_global_init(s_api);
s_log_internal__expected = (const char *[]){
"foo",
"1",
"2",
"true",
"false",
NULL
};
EXECUTE_SCRIPT(
"console.warn('foo', 1, 2);\n"
"console.error(true, false);\n"
);
cl_assert_equal_s(NULL, *s_log_internal__expected);
}

View file

@ -0,0 +1,326 @@
/*
* Copyright 2024 Google 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.
*/
#include "clar.h"
#include "test_jerry_port_common.h"
#include "test_rocky_common.h"
#include "applib/rockyjs/api/rocky_api_global.h"
#include "applib/rockyjs/api/rocky_api_datetime.h"
#include "applib/rockyjs/pbl_jerry_port.h"
// Standard
#include <string.h>
// Fakes
#include "fake_app_timer.h"
#include "fake_time.h"
// Stubs
#include "stubs_app_manager.h"
#include "stubs_app_state.h"
#include "stubs_logging.h"
#include "stubs_passert.h"
#include "stubs_pbl_malloc.h"
#include "stubs_resources.h"
#include "stubs_sleep.h"
#include "stubs_serial.h"
#include "stubs_syscalls.h"
#include "stubs_sys_exit.h"
size_t heap_bytes_free(void) {
return 123456;
}
static bool s_clock_is_24h_style;
bool clock_is_24h_style() {
return s_clock_is_24h_style;
}
void test_rocky_api_datetime__initialize(void) {
// Mon Jul 25 2005 20:04:05 GMT-03:00
s_time = 1122332645;
s_gmt_off = -3 * 60 * 60;
s_clock_is_24h_style = false;
fake_app_timer_init();
rocky_runtime_context_init();
jerry_init(JERRY_INIT_EMPTY);
}
void test_rocky_api_datetime__cleanup(void) {
jerry_cleanup();
rocky_runtime_context_deinit();
}
static const RockyGlobalAPI *s_api[] = {
&DATETIME_APIS,
NULL,
};
void test_rocky_api_datetime__jerry_script_default(void) {
static const RockyGlobalAPI *apis[] = {NULL};
rocky_global_init(apis);
EXECUTE_SCRIPT(
"var d = new Date();\n"
"var s1 = d.toString();\n"
"var f = typeof(d.toLocaleTimeString);\n"
"var s2 = d.toLocaleTimeString();\n"
"var s3 = d.toLocaleDateString();\n"
"var s4 = d.toLocaleString();\n"
);
ASSERT_JS_GLOBAL_EQUALS_S("s1", "Mon Jul 25 2005 20:04:05 GMT-03:00");
ASSERT_JS_GLOBAL_EQUALS_S("f", "function");
// yes, JerryScript provides some default behavior but it's not what we want
ASSERT_JS_GLOBAL_EQUALS_S("s2", "23:04:05.000");
ASSERT_JS_GLOBAL_EQUALS_S("s3", "2005-07-25");
ASSERT_JS_GLOBAL_EQUALS_S("s4", "Mon Jul 25 2005 20:04:05 GMT-03:00");
}
void test_rocky_api_datetime__locale_time_string_12h(void) {
s_clock_is_24h_style = false;
rocky_global_init(s_api);
EXECUTE_SCRIPT(
"var d = new Date();\n"
"var s = d.toLocaleTimeString();\n"
);
ASSERT_JS_GLOBAL_EQUALS_S("s", "8:04:05 PM");
s_time += 4 * SECONDS_PER_HOUR;
EXECUTE_SCRIPT(
"var d = new Date();\n"
"var s = d.toLocaleTimeString();\n"
);
ASSERT_JS_GLOBAL_EQUALS_S("s", "12:04:05 AM");
}
void test_rocky_api_datetime__locale_time_string_24h(void) {
s_clock_is_24h_style = true;
rocky_global_init(s_api);
EXECUTE_SCRIPT(
"var d = new Date();\n"
"var s = d.toLocaleTimeString();\n"
);
ASSERT_JS_GLOBAL_EQUALS_S("s", "20:04:05");
s_time += 4 * SECONDS_PER_HOUR;
EXECUTE_SCRIPT(
"var d = new Date();\n"
"var s = d.toLocaleTimeString();\n"
);
ASSERT_JS_GLOBAL_EQUALS_S("s", "00:04:05");
}
void test_rocky_api_datetime__locale(void) {
rocky_global_init(s_api);
EXECUTE_SCRIPT(
"var d = new Date();\n"
"d.toLocaleTimeString(undefined);\n"
);
EXECUTE_SCRIPT_EXPECT_ERROR(
"d.toLocaleTimeString('en-us');",
"TypeError: Unsupported locale"
);
EXECUTE_SCRIPT(
"var d = new Date();\n"
"d.toLocaleDateString(undefined);\n"
);
EXECUTE_SCRIPT_EXPECT_ERROR(
"d.toLocaleDateString('de');",
"TypeError: Unsupported locale"
);
EXECUTE_SCRIPT(
"var d = new Date();\n"
"d.toLocaleString(undefined);\n"
);
EXECUTE_SCRIPT_EXPECT_ERROR(
"d.toLocaleString('de');",
"TypeError: Unsupported locale"
);
}
void test_rocky_api_datetime__locale_time_string_options(void) {
rocky_global_init(s_api);
EXECUTE_SCRIPT("var d = new Date();");
EXECUTE_SCRIPT("s = d.toLocaleTimeString(undefined, {second: 'numeric'});");
ASSERT_JS_GLOBAL_EQUALS_S("s", "5");
EXECUTE_SCRIPT("s = d.toLocaleTimeString(undefined, {second: '2-digit'});");
ASSERT_JS_GLOBAL_EQUALS_S("s", "05");
EXECUTE_SCRIPT("s = d.toLocaleTimeString(undefined, {minute: 'numeric'});");
ASSERT_JS_GLOBAL_EQUALS_S("s", "4");
EXECUTE_SCRIPT("s = d.toLocaleTimeString(undefined, {minute: '2-digit'});");
ASSERT_JS_GLOBAL_EQUALS_S("s", "04");
s_clock_is_24h_style = false;
EXECUTE_SCRIPT("s = d.toLocaleTimeString(undefined, {hour: 'numeric'});");
ASSERT_JS_GLOBAL_EQUALS_S("s", "8 PM");
EXECUTE_SCRIPT("s = d.toLocaleTimeString(undefined, "
"{hour: 'numeric', hour12: true});");
ASSERT_JS_GLOBAL_EQUALS_S("s", "8 PM");
EXECUTE_SCRIPT("s = d.toLocaleTimeString(undefined, "
"{hour: 'numeric', hour12: false});");
ASSERT_JS_GLOBAL_EQUALS_S("s", "20");
EXECUTE_SCRIPT("s = d.toLocaleTimeString(undefined, {hour: '2-digit'});");
ASSERT_JS_GLOBAL_EQUALS_S("s", "08 PM");
EXECUTE_SCRIPT("s = d.toLocaleTimeString(undefined, {});");
ASSERT_JS_GLOBAL_EQUALS_S("s", "8:04:05 PM");
EXECUTE_SCRIPT("s = d.toLocaleTimeString(undefined, "
"{hour: undefined, minute: undefined, second: undefined});");
ASSERT_JS_GLOBAL_EQUALS_S("s", "8:04:05 PM");
s_clock_is_24h_style = true;
EXECUTE_SCRIPT("s = d.toLocaleTimeString(undefined, {hour: 'numeric'});");
ASSERT_JS_GLOBAL_EQUALS_S("s", "20");
EXECUTE_SCRIPT("s = d.toLocaleTimeString(undefined, "
"{hour: 'numeric', hour12: true});");
ASSERT_JS_GLOBAL_EQUALS_S("s", "8 PM");
EXECUTE_SCRIPT("s = d.toLocaleTimeString(undefined, "
"{hour: 'numeric', hour12: false});");
ASSERT_JS_GLOBAL_EQUALS_S("s", "20");
EXECUTE_SCRIPT("s = d.toLocaleTimeString(undefined, {hour: '2-digit'});");
ASSERT_JS_GLOBAL_EQUALS_S("s", "20");
EXECUTE_SCRIPT("s = d.toLocaleTimeString(undefined, {});");
ASSERT_JS_GLOBAL_EQUALS_S("s", "20:04:05");
EXECUTE_SCRIPT("s = d.toLocaleTimeString(undefined, "
"{hour: undefined, minute: undefined, second: undefined});");
ASSERT_JS_GLOBAL_EQUALS_S("s", "20:04:05");
EXECUTE_SCRIPT_EXPECT_ERROR(
"d.toLocaleTimeString(undefined, {minute: 'numeric', hour: '2-digit'})",
"TypeError: Unsupported options"
);
}
void test_rocky_api_datetime__locale_time_string_date_options(void) {
rocky_global_init(s_api);
EXECUTE_SCRIPT("var d = new Date();");
EXECUTE_SCRIPT("s = d.toLocaleTimeString(undefined, {day: 'short'});");
ASSERT_JS_GLOBAL_EQUALS_S("s", "Mon, 8:04:05 PM");
}
void test_rocky_api_datetime__locale_date_string(void) {
rocky_global_init(s_api);
EXECUTE_SCRIPT(
"var d = new Date();\n"
"var s = d.toLocaleDateString();\n"
);
ASSERT_JS_GLOBAL_EQUALS_S("s", "07/25/05");
}
void test_rocky_api_datetime__locale_date_string_options(void) {
rocky_global_init(s_api);
EXECUTE_SCRIPT("var d = new Date();");
EXECUTE_SCRIPT("s = d.toLocaleDateString(undefined, {day: 'numeric'});");
ASSERT_JS_GLOBAL_EQUALS_S("s", "25");
EXECUTE_SCRIPT("s = d.toLocaleDateString(undefined, {day: '2-digit'});");
ASSERT_JS_GLOBAL_EQUALS_S("s", "25");
EXECUTE_SCRIPT("s = d.toLocaleDateString(undefined, {day: 'short'});");
ASSERT_JS_GLOBAL_EQUALS_S("s", "Mon");
EXECUTE_SCRIPT("s = d.toLocaleDateString(undefined, {day: 'long'});");
ASSERT_JS_GLOBAL_EQUALS_S("s", "Monday");
EXECUTE_SCRIPT("s = d.toLocaleDateString(undefined, {month: 'numeric'});");
ASSERT_JS_GLOBAL_EQUALS_S("s", "7");
EXECUTE_SCRIPT("s = d.toLocaleDateString(undefined, {month: '2-digit'});");
ASSERT_JS_GLOBAL_EQUALS_S("s", "07");
EXECUTE_SCRIPT("s = d.toLocaleDateString(undefined, {month: 'short'});");
ASSERT_JS_GLOBAL_EQUALS_S("s", "Jul");
EXECUTE_SCRIPT("s = d.toLocaleDateString(undefined, {month: 'long'});");
ASSERT_JS_GLOBAL_EQUALS_S("s", "July");
EXECUTE_SCRIPT("s = d.toLocaleDateString(undefined, {year: 'numeric'});");
ASSERT_JS_GLOBAL_EQUALS_S("s", "2005");
EXECUTE_SCRIPT("s = d.toLocaleDateString(undefined, {year: '2-digit'});");
ASSERT_JS_GLOBAL_EQUALS_S("s", "05");
}
void test_rocky_api_datetime__locale_date_string_time_options(void) {
rocky_global_init(s_api);
EXECUTE_SCRIPT("var d = new Date();");
EXECUTE_SCRIPT("s = d.toLocaleDateString(undefined, {hour: 'numeric'});");
ASSERT_JS_GLOBAL_EQUALS_S("s", "07/25/05, 8 PM");
}
void test_rocky_api_datetime__locale_string_options(void) {
rocky_global_init(s_api);
EXECUTE_SCRIPT("var d = new Date();");
s_clock_is_24h_style = false;
EXECUTE_SCRIPT("s = d.toLocaleString(undefined, {});");
ASSERT_JS_GLOBAL_EQUALS_S("s", "07/25/05, 8:04:05 PM");
s_clock_is_24h_style = true;
EXECUTE_SCRIPT("s = d.toLocaleString(undefined, {});");
ASSERT_JS_GLOBAL_EQUALS_S("s", "07/25/05, 20:04:05");
EXECUTE_SCRIPT("s = d.toLocaleTimeString(undefined, {second: 'numeric'});");
ASSERT_JS_GLOBAL_EQUALS_S("s", "5");
EXECUTE_SCRIPT("s = d.toLocaleTimeString(undefined, {second: '2-digit'});");
ASSERT_JS_GLOBAL_EQUALS_S("s", "05");
EXECUTE_SCRIPT("s = d.toLocaleTimeString(undefined, {minute: 'numeric'});");
ASSERT_JS_GLOBAL_EQUALS_S("s", "4");
EXECUTE_SCRIPT("s = d.toLocaleTimeString(undefined, {minute: '2-digit'});");
ASSERT_JS_GLOBAL_EQUALS_S("s", "04");
s_clock_is_24h_style = false;
EXECUTE_SCRIPT("s = d.toLocaleTimeString(undefined, {hour: 'numeric'});");
ASSERT_JS_GLOBAL_EQUALS_S("s", "8 PM");
EXECUTE_SCRIPT("s = d.toLocaleTimeString(undefined, {hour: '2-digit'});");
ASSERT_JS_GLOBAL_EQUALS_S("s", "08 PM");
s_clock_is_24h_style = true;
EXECUTE_SCRIPT("s = d.toLocaleTimeString(undefined, {hour: 'numeric'});");
ASSERT_JS_GLOBAL_EQUALS_S("s", "20");
EXECUTE_SCRIPT("s = d.toLocaleTimeString(undefined, {hour: '2-digit'});");
ASSERT_JS_GLOBAL_EQUALS_S("s", "20");
EXECUTE_SCRIPT("s = d.toLocaleDateString(undefined, {day: 'numeric'});");
ASSERT_JS_GLOBAL_EQUALS_S("s", "25");
EXECUTE_SCRIPT("s = d.toLocaleDateString(undefined, {day: '2-digit'});");
ASSERT_JS_GLOBAL_EQUALS_S("s", "25");
EXECUTE_SCRIPT("s = d.toLocaleDateString(undefined, {day: 'short'});");
ASSERT_JS_GLOBAL_EQUALS_S("s", "Mon");
EXECUTE_SCRIPT("s = d.toLocaleDateString(undefined, {day: 'long'});");
ASSERT_JS_GLOBAL_EQUALS_S("s", "Monday");
EXECUTE_SCRIPT("s = d.toLocaleDateString(undefined, {month: 'numeric'});");
ASSERT_JS_GLOBAL_EQUALS_S("s", "7");
EXECUTE_SCRIPT("s = d.toLocaleDateString(undefined, {month: '2-digit'});");
ASSERT_JS_GLOBAL_EQUALS_S("s", "07");
EXECUTE_SCRIPT("s = d.toLocaleDateString(undefined, {month: 'short'});");
ASSERT_JS_GLOBAL_EQUALS_S("s", "Jul");
EXECUTE_SCRIPT("s = d.toLocaleDateString(undefined, {month: 'long'});");
ASSERT_JS_GLOBAL_EQUALS_S("s", "July");
EXECUTE_SCRIPT("s = d.toLocaleDateString(undefined, {year: 'numeric'});");
ASSERT_JS_GLOBAL_EQUALS_S("s", "2005");
EXECUTE_SCRIPT("s = d.toLocaleDateString(undefined, {year: '2-digit'});");
ASSERT_JS_GLOBAL_EQUALS_S("s", "05");
}

View file

@ -0,0 +1,315 @@
/*
* Copyright 2024 Google 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.
*/
#include "clar.h"
#include "test_jerry_port_common.h"
#include "test_rocky_common.h"
#include "applib/rockyjs/api/rocky_api_global.h"
#include "applib/rockyjs/pbl_jerry_port.h"
// Standard
#include "string.h"
// Fakes
#include "fake_app_timer.h"
#include "fake_time.h"
#include "fake_logging.h"
// Stubs
#include "stubs_app_manager.h"
#include "stubs_app_state.h"
#include "stubs_logging.h"
#include "stubs_passert.h"
#include "stubs_pbl_malloc.h"
#include "stubs_resources.h"
#include "stubs_sleep.h"
#include "stubs_serial.h"
#include "stubs_syscalls.h"
#include "stubs_sys_exit.h"
size_t heap_bytes_free(void) {
return 123456;
}
static Window s_app_window_stack_get_top_window;
Window *app_window_stack_get_top_window() {
return &s_app_window_stack_get_top_window;
}
static int s_prv_api_init__callcount;
static void prv_api_init(void) {
s_prv_api_init__callcount++;
}
static int s_prv_api_add__callcount;
static bool s_prv_api_add__result;
static bool prv_api_add(const char *event_name, jerry_value_t handler) {
s_prv_api_add__callcount++;
return s_prv_api_add__result;
}
static int s_prv_api_remove__callcount;
static void prv_api_remove(const char *event_name, jerry_value_t handler) {
s_prv_api_remove__callcount++;
}
int s_prv_listener_a1__callcount;
JERRY_FUNCTION(prv_listener_a1) {
s_prv_listener_a1__callcount++;
return jerry_create_undefined();
}
int s_prv_listener_a2__callcount;
JERRY_FUNCTION(prv_listener_a2) {
s_prv_listener_a2__callcount++;
return jerry_create_undefined();
}
int s_prv_listener_b__callcount;
JERRY_FUNCTION(prv_listener_b) {
s_prv_listener_b__callcount++;
return jerry_create_undefined();
}
void test_rocky_api_global__initialize(void) {
fake_app_timer_init();
rocky_runtime_context_init();
jerry_init(JERRY_INIT_EMPTY);
s_app_window_stack_get_top_window = (Window){};
s_app_event_loop_callback = NULL;
s_log_internal__expected = NULL;
s_prv_api_init__callcount = 0;
s_prv_api_add__callcount = 0;
s_prv_api_add__result = false;
s_prv_api_remove__callcount = 0;
s_prv_listener_a1__callcount = 0;
s_prv_listener_a2__callcount = 0;
s_prv_listener_b__callcount = 0;
}
void test_rocky_api_global__cleanup(void) {
fake_app_timer_deinit();
s_log_internal__expected = NULL;
jerry_cleanup();
rocky_runtime_context_deinit();
rocky_global_deinit();
}
void test_rocky_api_global__global(void) {
char test_object[] = "var t = typeof _rocky";
// global doesn't exist in plain Jerry context
EXECUTE_SCRIPT(test_object);
ASSERT_JS_GLOBAL_EQUALS_S("t", "undefined");
// rocky_global_init() injects global...
static const RockyGlobalAPI *apis[] = {
NULL,
};
rocky_global_init(apis);
EXECUTE_SCRIPT(test_object);
ASSERT_JS_GLOBAL_EQUALS_S("t", "object");
// ...which also has a method .on()
EXECUTE_SCRIPT("var t = typeof _rocky.on");
ASSERT_JS_GLOBAL_EQUALS_S("t", "function");
/// ...which is an alias of .addEventListener()
EXECUTE_SCRIPT("var a = (_rocky.on === _rocky.addEventListener);");
ASSERT_JS_GLOBAL_EQUALS_B("a", true);
}
void test_rocky_api_global__calls_init_and_notifies_about_apis(void) {
static const RockyGlobalAPI api = {
.init = prv_api_init,
.add_handler = prv_api_add,
};
static const RockyGlobalAPI *apis[] = {
&api,
NULL,
};
rocky_global_init(apis);
cl_assert_equal_i(1, s_prv_api_init__callcount);
s_prv_api_add__result = true;
s_log_internal__expected = (const char *[]){NULL};
EXECUTE_SCRIPT("_rocky.on('foo', function(){})");
cl_assert_equal_i(1, s_prv_api_add__callcount);
cl_assert_equal_b(true, rocky_global_has_event_handlers("foo"));
s_prv_api_add__result = false;
s_log_internal__expected = (const char *[]){
"Unknown event 'bar'",
NULL };
EXECUTE_SCRIPT("_rocky.on('bar', function(){})");
cl_assert_equal_i(2, s_prv_api_add__callcount);
cl_assert_equal_b(false, rocky_global_has_event_handlers("bar"));
cl_assert(*s_log_internal__expected == NULL);
}
void test_rocky_api_global__can_unsubsribe_event_handlers(void) {
static const RockyGlobalAPI api = {
.add_handler = prv_api_add,
.remove_handler = prv_api_remove,
};
static const RockyGlobalAPI *apis[] = {
&api,
NULL,
};
rocky_global_init(apis);
s_prv_api_add__result = true;
EXECUTE_SCRIPT(
"var f1 = function(){};\n"
"var f2 = function(){};\n"
"_rocky.on('foo', f1)\n"
);
cl_assert_equal_i(1, s_prv_api_add__callcount);
cl_assert_equal_b(true, rocky_global_has_event_handlers("foo"));
// variables f1, f2 continue to exist between EXECUTE_SCRIPT calls
EXECUTE_SCRIPT("var t = typeof f2;");
ASSERT_JS_GLOBAL_EQUALS_S("t", "function");
// rocky.off exists
EXECUTE_SCRIPT(
"t = typeof _rocky.off;\n"
"var eq = _rocky.off === _rocky.removeEventListener;\n"
);
ASSERT_JS_GLOBAL_EQUALS_S("t", "function");
ASSERT_JS_GLOBAL_EQUALS_B("eq", true);
// from MDN docs:
// Calling removeEventListener() with arguments that do not identify
// any currently registered EventListener on the EventTarget has no effect.
EXECUTE_SCRIPT(
"_rocky.off('foo', f2);\n"
"_rocky.off('unknownevent', f1);\n"
);
cl_assert_equal_i(0, s_prv_api_remove__callcount);
cl_assert_equal_b(true, rocky_global_has_event_handlers("foo"));
EXECUTE_SCRIPT("_rocky.off('foo', f1);\n");
cl_assert_equal_i(1, s_prv_api_remove__callcount);
cl_assert_equal_b(false, rocky_global_has_event_handlers("foo"));
}
void prv_add_event_listener_to_list(const char *event_name, jerry_value_t listener);
int jerry_obj_refcount(jerry_value_t o);
void test_rocky_api_global__refcount(void) {
jerry_value_t o = jerry_create_object();
cl_assert_equal_i(1, jerry_obj_refcount(o));
jerry_acquire_value(o);
cl_assert_equal_i(2, jerry_obj_refcount(o));
jerry_acquire_value(o);
cl_assert_equal_i(3, jerry_obj_refcount(o));
jerry_release_value(o);
cl_assert_equal_i(2, jerry_obj_refcount(o));
jerry_release_value(o);
cl_assert_equal_i(1, jerry_obj_refcount(o));
jerry_release_value(o);
cl_assert_equal_i(0, jerry_obj_refcount(o));
}
void test_rocky_api_global__calls_listeners(void) {
static const RockyGlobalAPI *apis[] = {NULL};
rocky_global_init(apis);
prv_add_event_listener_to_list("a", jerry_create_external_function(prv_listener_a1));
cl_assert_equal_b(true, rocky_global_has_event_handlers("a"));
cl_assert_equal_b(false, rocky_global_has_event_handlers("b"));
prv_add_event_listener_to_list("b", jerry_create_external_function(prv_listener_b));
cl_assert_equal_b(true, rocky_global_has_event_handlers("a"));
cl_assert_equal_b(true, rocky_global_has_event_handlers("b"));
prv_add_event_listener_to_list("a", jerry_create_external_function(prv_listener_a2));
cl_assert_equal_b(true, rocky_global_has_event_handlers("a"));
cl_assert_equal_b(true, rocky_global_has_event_handlers("b"));
jerry_value_t a_event = rocky_global_create_event("a");
rocky_global_call_event_handlers(a_event);
cl_assert_equal_i(1, s_prv_listener_a1__callcount);
cl_assert_equal_i(1, s_prv_listener_a2__callcount);
cl_assert_equal_i(0, s_prv_listener_b__callcount);
jerry_release_value(a_event);
jerry_value_t b_event = rocky_global_create_event("b");
rocky_global_call_event_handlers(b_event);
cl_assert_equal_i(1, s_prv_listener_a1__callcount);
cl_assert_equal_i(1, s_prv_listener_a2__callcount);
cl_assert_equal_i(1, s_prv_listener_b__callcount);
jerry_release_value(b_event);
}
void test_rocky_api_global__adds_listener_only_once(void) {
static const RockyGlobalAPI *apis[] = {NULL};
rocky_global_init(apis);
const jerry_value_t f = jerry_create_external_function(prv_listener_a1);
prv_add_event_listener_to_list("a", f);
prv_add_event_listener_to_list("a", f);
cl_assert_equal_b(true, rocky_global_has_event_handlers("a"));
jerry_value_t a_event = rocky_global_create_event("a");
rocky_global_call_event_handlers(a_event);
// as second .on('a', f) "replaces" first, f will only be called once
cl_assert_equal_i(1, s_prv_listener_a1__callcount);
jerry_release_value(a_event);
}
void test_rocky_api_global__event_constructor(void) {
static const RockyGlobalAPI *apis[] = {NULL};
rocky_global_init(apis);
EXECUTE_SCRIPT(
"_rocky.Event.prototype.myCustomThing = 'xyz';\n"
"var e = new _rocky.Event('myevent');\n"
"var t = e.type;\n"
"var c = e.myCustomThing;\n"
);
ASSERT_JS_GLOBAL_EQUALS_S("t", "myevent");
ASSERT_JS_GLOBAL_EQUALS_S("c", "xyz");
}
void test_rocky_api_global__call_event_handlers_async(void) {
static const RockyGlobalAPI api = {
.init = prv_api_init,
.add_handler = prv_api_add,
};
static const RockyGlobalAPI *apis[] = {
&api,
NULL,
};
rocky_global_init(apis);
s_prv_api_add__result = true;
EXECUTE_SCRIPT("var is_called = false; _rocky.on('a', function(e) { is_called = true; });");
jerry_value_t a_event = rocky_global_create_event("a");
rocky_global_call_event_handlers_async(a_event);
ASSERT_JS_GLOBAL_EQUALS_B("is_called", false);
s_process_manager_callback(s_process_manager_callback_data);
ASSERT_JS_GLOBAL_EQUALS_B("is_called", true);
}

View file

@ -0,0 +1,768 @@
/*
* Copyright 2024 Google 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.
*/
#include "clar.h"
#include "test_jerry_port_common.h"
#include "test_rocky_common.h"
#include "applib/graphics/gtypes.h"
#include "applib/rockyjs/api/rocky_api.h"
#include "applib/rockyjs/api/rocky_api_global.h"
#include "applib/rockyjs/api/rocky_api_graphics.h"
#include "applib/rockyjs/api/rocky_api_graphics_text.h"
#include "applib/rockyjs/pbl_jerry_port.h"
#include "util/trig.h"
// Standard
#include "string.h"
// Fakes
#include "fake_app_timer.h"
#include "fake_time.h"
// Stubs
#include "stubs_app_manager.h"
#include "stubs_app_state.h"
#include "stubs_logging.h"
#include "stubs_passert.h"
#include "stubs_pbl_malloc.h"
#include "stubs_resources.h"
#include "stubs_sleep.h"
#include "stubs_serial.h"
#include "stubs_syscalls.h"
#include "stubs_sys_exit.h"
size_t heap_bytes_free(void) {
return 123456;
}
static Window s_app_window_stack_get_top_window;
Window *app_window_stack_get_top_window() {
return &s_app_window_stack_get_top_window;
}
void rocky_api_graphics_path2d_add_canvas_methods(jerry_value_t obj) {}
void rocky_api_graphics_path2d_cleanup(void) {}
void rocky_api_graphics_path2d_reset_state(void) {}
GContext s_context;
// mocks
static MockCallRecordings s_graphics_context_set_fill_color;
void graphics_context_set_fill_color(GContext* ctx, GColor color) {
record_mock_call(s_graphics_context_set_fill_color) {
.ctx = ctx, .color = color,
};
ctx->draw_state.fill_color = color;
}
static MockCallRecordings s_graphics_context_set_stroke_color;
void graphics_context_set_stroke_color(GContext* ctx, GColor color) {
record_mock_call(s_graphics_context_set_stroke_color) {
.ctx = ctx, .color = color,
};
ctx->draw_state.stroke_color = color;
}
static MockCallRecordings s_graphics_context_set_stroke_width;
void graphics_context_set_stroke_width(GContext* ctx, uint8_t stroke_width) {
record_mock_call(s_graphics_context_set_stroke_width) {
.ctx = ctx, .width = stroke_width,
};
ctx->draw_state.stroke_width = stroke_width;
}
static MockCallRecordings s_graphics_fill_rect;
static GColor s_graphics_fill_rect__color;
void graphics_fill_rect(GContext *ctx, const GRect *rect) {
s_graphics_fill_rect__color = s_context.draw_state.fill_color;
record_mock_call(s_graphics_fill_rect) {
.ctx = ctx, .rect = *rect,
};
}
static MockCallRecordings s_graphics_draw_rect_precise;
void graphics_draw_rect_precise(GContext *ctx, const GRectPrecise *rect) {
record_mock_call(s_graphics_draw_rect_precise) {
.ctx = ctx, .prect = *rect,
};
}
static MockCallRecordings s_graphics_fill_radial_precise_internal;
void graphics_fill_radial_precise_internal(GContext *ctx, GPointPrecise center,
Fixed_S16_3 radius_inner, Fixed_S16_3 radius_outer,
int32_t angle_start, int32_t angle_end) {
record_mock_call(s_graphics_fill_radial_precise_internal) {
.ctx = ctx,
.fill_radial_precise.center = center,
.fill_radial_precise.radius_inner = radius_inner,
.fill_radial_precise.radius_outer = radius_outer,
.fill_radial_precise.angle_start = angle_start,
.fill_radial_precise.angle_end = angle_end,
};
}
void graphics_fill_round_rect_by_value(GContext *ctx, GRect rect, uint16_t corner_radius,
GCornerMask corner_mask) {}
static MockCallRecordings s_layer_mark_dirty;
void layer_mark_dirty(Layer *layer) {
record_mock_call(s_layer_mark_dirty){
.layer = layer
};
}
static MockCallRecordings s_fonts_get_system_font;
GFont s_fonts_get_system_font__result;
GFont fonts_get_system_font(const char *font_key) {
record_mock_call(s_fonts_get_system_font){
.font_key = font_key
};
return s_fonts_get_system_font__result;
}
static MockCallRecordings s_graphics_draw_text;
void graphics_draw_text(GContext *ctx, const char *text, GFont const font, const GRect box,
const GTextOverflowMode overflow_mode, const GTextAlignment alignment,
GTextAttributes *text_attributes) {
record_mock_call(s_graphics_draw_text){
.draw_text.box = box,
.draw_text.color = ctx->draw_state.text_color,
};
strncpy(s_graphics_draw_text.last_call.draw_text.text,
text, sizeof(s_graphics_draw_text.last_call.draw_text.text));
}
static MockCallRecordings s_graphics_text_attributes_destroy;
void graphics_text_attributes_destroy(GTextAttributes *text_attributes) {
record_mock_call(s_graphics_text_attributes_destroy){};
}
static MockCallRecordings s_graphics_text_layout_get_max_used_size;
static GSize s_graphics_text_layout_get_max_used_size__result;
GSize graphics_text_layout_get_max_used_size(GContext *ctx, const char *text,
GFont const font, const GRect box,
const GTextOverflowMode overflow_mode,
const GTextAlignment alignment,
GTextLayoutCacheRef layout) {
record_mock_call(s_graphics_text_layout_get_max_used_size){
.max_used_size.font = font,
.max_used_size.box = box,
.max_used_size.overflow_mode = overflow_mode,
.max_used_size.alignment = alignment,
};
strncpy(s_graphics_text_layout_get_max_used_size.last_call.max_used_size.text,
text, sizeof(s_graphics_text_layout_get_max_used_size.last_call.max_used_size.text));
return s_graphics_text_layout_get_max_used_size__result;
}
void test_rocky_api_graphics__initialize(void) {
fake_app_timer_init();
rocky_runtime_context_init();
jerry_init(JERRY_INIT_EMPTY);
s_app_window_stack_get_top_window = (Window){};
s_context = (GContext){};
s_app_state_get_graphics_context = &s_context;
s_app_event_loop_callback = NULL;
s_graphics_context_set_stroke_color = (MockCallRecordings){0};
s_graphics_context_set_stroke_width = (MockCallRecordings){0};
s_graphics_context_set_fill_color = (MockCallRecordings){0};
s_graphics_fill_rect = (MockCallRecordings){0};
s_graphics_fill_rect__color = GColorClear;
s_graphics_draw_rect_precise = (MockCallRecordings){0};
s_graphics_fill_radial_precise_internal = (MockCallRecordings){0};
s_layer_mark_dirty = (MockCallRecordings){0};
s_fonts_get_system_font = (MockCallRecordings){0};
s_graphics_draw_text = (MockCallRecordings){0};
s_graphics_text_attributes_destroy = (MockCallRecordings){0};
s_graphics_text_layout_get_max_used_size = (MockCallRecordings){0};
s_graphics_text_layout_get_max_used_size__result = (GSize){0};
}
void test_rocky_api_graphics__cleanup(void) {
fake_app_timer_deinit();
// some tests deinitialize the engine, avoid double de-init
if (app_state_get_rocky_runtime_context() != NULL) {
jerry_cleanup();
rocky_runtime_context_deinit();
}
}
static const RockyGlobalAPI *s_graphics_api[] = {
&GRAPHIC_APIS,
NULL,
};
extern RockyAPITextState s_rocky_text_state;
void test_rocky_api_graphics__handles_text_state(void) {
cl_assert_equal_i(0, s_fonts_get_system_font.call_count);
cl_assert_equal_i(0, s_graphics_text_attributes_destroy.call_count);
rocky_global_init(s_graphics_api);
cl_assert_equal_i(1, s_fonts_get_system_font.call_count);
cl_assert_equal_i(0, s_graphics_text_attributes_destroy.call_count);
rocky_global_deinit();
cl_assert_equal_i(1, s_fonts_get_system_font.call_count);
cl_assert_equal_i(0, s_graphics_text_attributes_destroy.call_count);
s_rocky_text_state.text_attributes = (GTextAttributes *)123;
rocky_global_deinit();
cl_assert_equal_i(1, s_fonts_get_system_font.call_count);
cl_assert_equal_i(1, s_graphics_text_attributes_destroy.call_count);
}
void test_rocky_api_graphics__request_draw(void) {
rocky_global_init(s_graphics_api);
cl_assert_equal_i(0, s_layer_mark_dirty.call_count);
EXECUTE_SCRIPT("_rocky.requestDraw();");
cl_assert_equal_i(1, s_layer_mark_dirty.call_count);
cl_assert_equal_p(&s_app_window_stack_get_top_window.layer, s_layer_mark_dirty.last_call.layer);
}
void test_rocky_api_graphics__provides_draw_event(void) {
rocky_global_init(s_graphics_api);
cl_assert_equal_b(false, rocky_global_has_event_handlers("draw"));
EXECUTE_SCRIPT("_rocky.on('draw', function() {});");
cl_assert_equal_b(true, rocky_global_has_event_handlers("draw"));
}
void test_rocky_api_graphics__draw_event_has_ctx(void) {
rocky_global_init(s_graphics_api);
EXECUTE_SCRIPT(
"var event = null;\n"
"_rocky.on('draw', function(e) {event = e;});"
);
const jerry_value_t event_null = prv_js_global_get_value("event");
cl_assert_equal_b(true, jerry_value_is_null(event_null));
jerry_release_value(event_null);
Layer *l = &app_window_stack_get_top_window()->layer;
l->update_proc(l, NULL);
const jerry_value_t event = prv_js_global_get_value("event");
cl_assert_equal_b(true, jerry_value_is_object(event));
const jerry_value_t context_2d = jerry_get_object_field(event, "context");
cl_assert_equal_b(true, jerry_value_is_object(context_2d));
jerry_release_value(context_2d);
jerry_release_value(event);
}
jerry_value_t prv_create_canvas_context_2d_for_layer(Layer *layer);
void layer_get_unobstructed_bounds(const Layer *layer, GRect *bounds_out) {
*bounds_out = GRect(5, 6, 7, 8);
}
void test_rocky_api_graphics__canvas_offers_size(void) {
rocky_global_init(s_graphics_api);
Layer l = {.bounds = GRect(1, 2, 3, 4)};
const jerry_value_t ctx = prv_create_canvas_context_2d_for_layer(&l);
jerry_set_object_field(jerry_get_global_object(), "ctx", ctx);
EXECUTE_SCRIPT(
"var w = ctx.canvas.clientWidth;\n"
"var h = ctx.canvas.clientHeight;\n"
"var uol = ctx.canvas.unobstructedLeft;\n"
"var uot = ctx.canvas.unobstructedTop;\n"
"var uow = ctx.canvas.unobstructedWidth;\n"
"var uoh = ctx.canvas.unobstructedHeight;\n"
);
ASSERT_JS_GLOBAL_EQUALS_I("w", 3);
ASSERT_JS_GLOBAL_EQUALS_I("h", 4);
ASSERT_JS_GLOBAL_EQUALS_I("uol", 5);
ASSERT_JS_GLOBAL_EQUALS_I("uot", 6);
ASSERT_JS_GLOBAL_EQUALS_I("uow", 7);
ASSERT_JS_GLOBAL_EQUALS_I("uoh", 8);
}
static const jerry_value_t prv_global_init_and_set_ctx(void) {
rocky_global_init(s_graphics_api);
// make this easily testable by putting it int JS context as global
Layer l = {.bounds = GRect(0, 0, 144, 168)};
const jerry_value_t ctx = prv_create_canvas_context_2d_for_layer(&l);
cl_assert_equal_b(jerry_value_is_object(ctx), true);
jerry_set_object_field(jerry_get_global_object(), "ctx", ctx);
return ctx;
}
void test_rocky_api_graphics__drawing_rects(void) {
prv_global_init_and_set_ctx();
s_context.draw_state.fill_color = GColorJaegerGreen;
EXECUTE_SCRIPT(
"ctx.clearRect(1, 2, 3, 4);\n"
);
cl_assert_equal_i(1, s_graphics_fill_rect.call_count);
cl_assert_equal_rect(GRect(1, 2, 3, 4), s_graphics_fill_rect.last_call.rect);
cl_assert_equal_i(GColorBlackARGB8, s_graphics_fill_rect__color.argb);
cl_assert_equal_i(GColorJaegerGreenARGB8, s_context.draw_state.fill_color.argb);
s_graphics_fill_rect = (MockCallRecordings){};
EXECUTE_SCRIPT(
"ctx.fillRect(5, 6, 7, 8);\n"
);
cl_assert_equal_i(1, s_graphics_fill_rect.call_count);
cl_assert_equal_rect(GRect(5, 6, 7, 8), s_graphics_fill_rect.last_call.rect);
s_graphics_draw_rect_precise = (MockCallRecordings){};
EXECUTE_SCRIPT(
"ctx.strokeRect(9, 10.2, 11.5, 12.8);\n"
);
cl_assert_equal_i(1, s_graphics_draw_rect_precise.call_count);
GRectPrecise expected_rect = {(int)(8.5*8), 78, (int)(11.5*8), (int)(12.8*8)};
cl_assert_equal_rect_precise(expected_rect, s_graphics_draw_rect_precise.last_call.prect);
}
#define PP(x, y) (GPointPrecise( \
(int16_t)((x) * FIXED_S16_3_FACTOR), \
(int16_t)((y) * FIXED_S16_3_FACTOR)))
void test_rocky_api_graphics__fill_radial(void) {
prv_global_init_and_set_ctx();
EXECUTE_SCRIPT(
"ctx.rockyFillRadial(30, 40, 10, 20, 0, Math.PI);\n"
);
cl_assert_equal_i(1, s_graphics_fill_radial_precise_internal.call_count);
MockCallRecording *const lc = &s_graphics_fill_radial_precise_internal.last_call;
cl_assert_equal_point_precise(PP(29.5, 39.5), lc->fill_radial_precise.center);
cl_assert_equal_i(10*8, lc->fill_radial_precise.radius_inner.raw_value);
cl_assert_equal_i(20*8, lc->fill_radial_precise.radius_outer.raw_value);
cl_assert_equal_i(TRIG_MAX_ANGLE * 1 / 4, lc->fill_radial_precise.angle_start);
cl_assert_equal_i(TRIG_MAX_ANGLE * 3 / 4, lc->fill_radial_precise.angle_end);
EXECUTE_SCRIPT(
"ctx.rockyFillRadial(30, 40, 10, 30, 0, 2 * Math.PI);\n"
);
cl_assert_equal_i(2, s_graphics_fill_radial_precise_internal.call_count);
cl_assert_equal_point_precise(PP(29.5, 39.5), lc->fill_radial_precise.center);
cl_assert_equal_i(10*8, lc->fill_radial_precise.radius_inner.raw_value);
cl_assert_equal_i(30*8, lc->fill_radial_precise.radius_outer.raw_value);
cl_assert_equal_i(TRIG_MAX_ANGLE * 1 / 4, lc->fill_radial_precise.angle_start);
cl_assert_equal_i(TRIG_MAX_ANGLE * 5 / 4, lc->fill_radial_precise.angle_end);
EXECUTE_SCRIPT(
"ctx.rockyFillRadial(30.5, 40.1, 30, 10, 0, 2 * Math.PI);\n"
);
cl_assert_equal_i(3, s_graphics_fill_radial_precise_internal.call_count);
cl_assert_equal_point_precise(PP(30, 39.625), lc->fill_radial_precise.center);
cl_assert_equal_i(10*8, lc->fill_radial_precise.radius_inner.raw_value);
cl_assert_equal_i(30*8, lc->fill_radial_precise.radius_outer.raw_value);
}
void test_rocky_api_graphics__fill_radial_not_enough_args(void) {
prv_global_init_and_set_ctx();
EXECUTE_SCRIPT_EXPECT_ERROR(
"ctx.rockyFillRadial(30, 40, 10, 20, 0);\n",
"TypeError: Not enough arguments"
);
}
void test_rocky_api_graphics__fill_radial_type_error(void) {
prv_global_init_and_set_ctx();
EXECUTE_SCRIPT_EXPECT_ERROR(
"ctx.rockyFillRadial(30, 40, 10, 20, 0, false);\n",
"TypeError: Argument at index 5 is not a Number"
);
}
void test_rocky_api_graphics__fill_radial_range_check(void) {
prv_global_init_and_set_ctx();
EXECUTE_SCRIPT_EXPECT_ERROR(
"ctx.rockyFillRadial(4096, 40, 10, 20, 0, false);\n",
"TypeError: Argument at index 0 is invalid: Value out of bounds for native type"
);
}
void test_rocky_api_graphics__fill_radial_zero_radius(void) {
prv_global_init_and_set_ctx();
// inner radius = 0
EXECUTE_SCRIPT(
"ctx.rockyFillRadial(30, 40, 0, 20, 0, Math.PI);\n"
);
MockCallRecording *const lc = &s_graphics_fill_radial_precise_internal.last_call;
cl_assert_equal_i(1, s_graphics_fill_radial_precise_internal.call_count);
cl_assert_equal_point_precise((PP(29.5, 39.5)), lc->fill_radial_precise.center);
cl_assert_equal_i(0, lc->fill_radial_precise.radius_inner.raw_value);
cl_assert_equal_i(20 * 8, lc->fill_radial_precise.radius_outer.raw_value);
// inner radius capped to >= 0
EXECUTE_SCRIPT(
"ctx.rockyFillRadial(30, 40, -10, 20, 0, Math.PI);\n"
);
cl_assert_equal_i(2, s_graphics_fill_radial_precise_internal.call_count);
cl_assert_equal_point_precise((PP(29.5, 39.5)), lc->fill_radial_precise.center);
cl_assert_equal_i(0, lc->fill_radial_precise.radius_inner.raw_value);
cl_assert_equal_i(20 * 8, lc->fill_radial_precise.radius_outer.raw_value);
// outer radius capped to >= 0
EXECUTE_SCRIPT(
"ctx.rockyFillRadial(30, 40, -10, -20, 0, Math.PI);\n"
);
cl_assert_equal_i(3, s_graphics_fill_radial_precise_internal.call_count);
cl_assert_equal_point_precise((PP(29.5, 39.5)), lc->fill_radial_precise.center);
cl_assert_equal_i(0, lc->fill_radial_precise.radius_inner.raw_value);
cl_assert_equal_i(0, lc->fill_radial_precise.radius_outer.raw_value);
}
void test_rocky_api_graphics__line_styles(void) {
prv_global_init_and_set_ctx();
EXECUTE_SCRIPT(
"ctx.lineWidth = 8;\n"
"var w = ctx.lineWidth;\n"
);
cl_assert_equal_i(1, s_graphics_context_set_stroke_width.call_count);
cl_assert_equal_i(8, s_graphics_context_set_stroke_width.last_call.width);
ASSERT_JS_GLOBAL_EQUALS_I("w", s_graphics_context_set_stroke_width.last_call.width);
EXECUTE_SCRIPT(
"ctx.lineWidth = 2.1;\n"
"var w = ctx.lineWidth;\n"
);
ASSERT_JS_GLOBAL_EQUALS_I("w", 2);
EXECUTE_SCRIPT_EXPECT_ERROR(
"ctx.lineWidth = -4;\n",
"TypeError: Argument at index 0 is invalid: Value out of bounds for native type"
);
EXECUTE_SCRIPT("var w = ctx.lineWidth;\n");
ASSERT_JS_GLOBAL_EQUALS_I("w", 2);
}
void test_rocky_api_graphics__line_styles_check_bounds(void) {
prv_global_init_and_set_ctx();
EXECUTE_SCRIPT_EXPECT_ERROR(
"ctx.lineWidth = -1;",
"TypeError: Argument at index 0 is invalid: Value out of bounds for native type"
);
EXECUTE_SCRIPT_EXPECT_ERROR(
"ctx.lineWidth = 256;",
"TypeError: Argument at index 0 is invalid: Value out of bounds for native type"
);
}
void test_rocky_api_graphics__fill_and_stroke_styles(void) {
prv_global_init_and_set_ctx();
EXECUTE_SCRIPT(
"ctx.fillStyle = '#f00';\n"
"ctx.strokeStyle = 'white';\n"
"var c = ctx.fillStyle;\n"
);
cl_assert_equal_i(1, s_graphics_context_set_fill_color.call_count);
cl_assert_equal_i(GColorRedARGB8, s_graphics_context_set_fill_color.last_call.color.argb);
cl_assert_equal_i(1, s_graphics_context_set_stroke_color.call_count);
cl_assert_equal_i(GColorWhiteARGB8, s_graphics_context_set_stroke_color.last_call.color.argb);
// ignores invalid values
EXECUTE_SCRIPT(
"ctx.fillStyle = 'unknown';\n"
"ctx.strokeStyle = '4%2F';\n"
);
cl_assert_equal_i(1, s_graphics_context_set_fill_color.call_count);
cl_assert_equal_i(1, s_graphics_context_set_stroke_color.call_count);
}
void test_rocky_api_graphics__canvas_state(void) {
prv_global_init_and_set_ctx();
// calling restore if nothing was stored is a no-op
s_context.draw_state.fill_color.argb = 1;
EXECUTE_SCRIPT("ctx.restore()\n");
cl_assert_equal_i(1, s_context.draw_state.fill_color.argb);
EXECUTE_SCRIPT("ctx.save()\n"); // 1
s_context.draw_state.fill_color.argb = 2;
EXECUTE_SCRIPT("ctx.save()\n"); // 2
s_context.draw_state.fill_color.argb = 3;
EXECUTE_SCRIPT("ctx.restore()\n"); // -> 2 (one element left)
cl_assert_equal_i(2, s_context.draw_state.fill_color.argb);
EXECUTE_SCRIPT("ctx.restore()\n"); // -> 1 (no element left)
cl_assert_equal_i(1, s_context.draw_state.fill_color.argb);
EXECUTE_SCRIPT("ctx.restore()\n"); // no-op
cl_assert_equal_i(1, s_context.draw_state.fill_color.argb);
}
static const int16_t large_int = 10000;
void test_rocky_api_graphics__fill_text(void) {
prv_global_init_and_set_ctx();
// we do this in C and not JS as color binding is not linked in this unit-test
// what we want to test though is that the text color is taken from fill color
rocky_api_graphics_get_gcontext()->draw_state.fill_color = GColorRed;
EXECUTE_SCRIPT(
"ctx.fillText('some text', 10, 10);\n"
);
cl_assert_equal_i(1, s_graphics_draw_text.call_count);
cl_assert_equal_s("some text", s_graphics_draw_text.last_call.draw_text.text);
cl_assert_equal_i(GColorRedARGB8, s_graphics_draw_text.last_call.draw_text.color.argb);
cl_assert_equal_rect((GRect(10, 10, large_int, large_int)),
s_graphics_draw_text.last_call.draw_text.box);
rocky_api_graphics_get_gcontext()->draw_state.fill_color = GColorBlue;
EXECUTE_SCRIPT(
"ctx.fillText('more text', -10.5, 5000, 60);\n"
);
cl_assert_equal_i(2, s_graphics_draw_text.call_count);
cl_assert_equal_s("more text", s_graphics_draw_text.last_call.draw_text.text);
cl_assert_equal_i(GColorBlueARGB8, s_graphics_draw_text.last_call.draw_text.color.argb);
cl_assert_equal_rect((GRect(-11, 5000, 60, large_int)),
s_graphics_draw_text.last_call.draw_text.box);
}
void test_rocky_api_graphics__fill_text_coordinates(void) {
prv_global_init_and_set_ctx();
EXECUTE_SCRIPT("ctx.fillText('some text', 0, 1.5);");
cl_assert_equal_rect((GRect(0, 2, large_int, large_int)),
s_graphics_draw_text.last_call.draw_text.box);
EXECUTE_SCRIPT("ctx.fillText('some text', -0.2, 1.2, 10.5);");
cl_assert_equal_rect((GRect(0, 1, 11, large_int)),
s_graphics_draw_text.last_call.draw_text.box);
EXECUTE_SCRIPT("ctx.fillText('some text', -0.5, 1.2, -0.5);");
cl_assert_equal_rect((GRect(-1, 1, -1, large_int)),
s_graphics_draw_text.last_call.draw_text.box);
}
void test_rocky_api_graphics__fill_text_aligned(void) {
prv_global_init_and_set_ctx();
// we do this in C and not JS as color binding is not linked in this unit-test
// what we want to test though is that the text color is taken from fill color
EXECUTE_SCRIPT(
"ctx.textAlign = 'left';\n"
"ctx.fillText('some text', 100, 100);\n"
);
cl_assert_equal_i(1, s_graphics_draw_text.call_count);
cl_assert_equal_rect((GRect(100, 100, large_int, large_int)),
s_graphics_draw_text.last_call.draw_text.box);
EXECUTE_SCRIPT(
"ctx.textAlign = 'center';\n"
"ctx.fillText('some text', 100, 100);\n"
);
cl_assert_equal_i(2, s_graphics_draw_text.call_count);
cl_assert_equal_rect((GRect(-4900, 100, large_int, large_int)),
s_graphics_draw_text.last_call.draw_text.box);
EXECUTE_SCRIPT(
"ctx.textAlign = 'right';\n"
"ctx.fillText('some text', 100, 100);\n"
);
cl_assert_equal_i(3, s_graphics_draw_text.call_count);
cl_assert_equal_rect((GRect(-9900, 100, large_int, large_int)),
s_graphics_draw_text.last_call.draw_text.box);
}
void test_rocky_api_graphics__text_align(void) {
prv_global_init_and_set_ctx();
// intial value
cl_assert_equal_i(GTextAlignmentLeft, s_rocky_text_state.alignment);
s_rocky_text_state.alignment = (GTextAlignment)-1;
// unsupported values don't change the value
EXECUTE_SCRIPT("ctx.textAlign = 123;\n");
cl_assert_equal_i(-1, s_rocky_text_state.alignment);
EXECUTE_SCRIPT("ctx.textAlign = 'unknown';\n");
cl_assert_equal_i(-1, s_rocky_text_state.alignment);
EXECUTE_SCRIPT("ctx.textAlign = 'left';\nvar a = ctx.textAlign;\n");
cl_assert_equal_i(GTextAlignmentLeft, s_rocky_text_state.alignment);
ASSERT_JS_GLOBAL_EQUALS_S("a", "left");
EXECUTE_SCRIPT("ctx.textAlign = 'right';\nvar a = ctx.textAlign;\n");
cl_assert_equal_i(GTextAlignmentRight, s_rocky_text_state.alignment);
ASSERT_JS_GLOBAL_EQUALS_S("a", "right");
EXECUTE_SCRIPT("ctx.textAlign = 'center';\nvar a = ctx.textAlign;\n");
cl_assert_equal_i(GTextAlignmentCenter, s_rocky_text_state.alignment);
ASSERT_JS_GLOBAL_EQUALS_S("a", "center");
// we only support LTR
EXECUTE_SCRIPT("ctx.textAlign = 'start';\nvar a = ctx.textAlign;\n");
cl_assert_equal_i(GTextAlignmentLeft, s_rocky_text_state.alignment);
ASSERT_JS_GLOBAL_EQUALS_S("a", "left");
EXECUTE_SCRIPT("ctx.textAlign = 'end';\nvar a = ctx.textAlign;\n");
cl_assert_equal_i(GTextAlignmentRight, s_rocky_text_state.alignment);
ASSERT_JS_GLOBAL_EQUALS_S("a", "right");
}
void test_rocky_api_graphics__text_font(void) {
cl_assert_equal_i(0, s_fonts_get_system_font.call_count);
s_fonts_get_system_font__result = (GFont)123;
rocky_global_init(s_graphics_api);
cl_assert_equal_i(1, s_fonts_get_system_font.call_count);
cl_assert_equal_p((GFont)123, s_rocky_text_state.font);
// make this easily testable by putting it int JS context as global
Layer l = {.bounds = GRect(0, 0, 144, 168)};
const jerry_value_t ctx = prv_create_canvas_context_2d_for_layer(&l);
jerry_set_object_field(jerry_get_global_object(), "ctx", ctx);
s_rocky_text_state.font = (GFont)-1;
// unsupported values don't change the value
EXECUTE_SCRIPT("ctx.font = 123;\n");
cl_assert_equal_p((GFont)-1, s_rocky_text_state.font);
EXECUTE_SCRIPT("ctx.font = 'unknown';\n");
cl_assert_equal_p((GFont)-1, s_rocky_text_state.font);
cl_assert_equal_i(1, s_fonts_get_system_font.call_count);
EXECUTE_SCRIPT("ctx.font = '14px bold Gothic';\n");
cl_assert_equal_i(2, s_fonts_get_system_font.call_count);
cl_assert_equal_p(FONT_KEY_GOTHIC_14_BOLD, s_fonts_get_system_font.last_call.font_key);
EXECUTE_SCRIPT("ctx.font = '28px Gothic';\nvar f = ctx.font;\n");
ASSERT_JS_GLOBAL_EQUALS_S("f", "28px Gothic");
}
extern T_STATIC void prv_graphics_color_to_char_buffer(GColor8 color, char *buf_out);
#define TEST_COLOR_STRING(gcolor, expect_str) do { \
char buf[12]; \
prv_graphics_color_to_char_buffer(gcolor, buf); \
cl_assert_equal_s(buf, expect_str); \
} while(0);
void test_rocky_api_graphics__color_names(void) {
TEST_COLOR_STRING(GColorClear, "transparent");
TEST_COLOR_STRING((GColor){ .a = 1 }, "transparent");
TEST_COLOR_STRING(GColorRed, "#FF0000");
TEST_COLOR_STRING(GColorMalachite, "#00FF55");
}
extern T_STATIC const RockyAPISystemFontDefinition s_font_definitions[];
bool prv_font_definition_from_value(jerry_value_t value, RockyAPISystemFontDefinition **result);
void test_rocky_api_graphics__text_font_names_unique(void) {
rocky_global_init(s_graphics_api);
const RockyAPISystemFontDefinition *def = s_font_definitions;
while (def->js_name) {
const jerry_value_t name_js = jerry_create_string((jerry_char_t *)def->js_name);
RockyAPISystemFontDefinition *cmp_def = NULL;
bool actual = prv_font_definition_from_value(name_js, &cmp_def);
cl_assert_equal_b(true, actual);
cl_assert_equal_s(cmp_def->res_key, def->res_key);
jerry_release_value(name_js);
def++;
}
}
void test_rocky_api_graphics__measure_text(void) {
prv_global_init_and_set_ctx();
// fill text_state with unique values we can test against
s_rocky_text_state = (RockyAPITextState) {
.font = (GFont)-1,
.overflow_mode = (GTextOverflowMode)-2,
.alignment = (GTextAlignment)-3,
.text_attributes = (GTextAttributes *)-4,
};
s_graphics_text_layout_get_max_used_size__result = GSize(123, 456);
EXECUTE_SCRIPT(
"var tm = ctx.measureText('foo');\n"
"var tm_w = tm.width;\n"
"var tm_h = tm.height;\n"
);
ASSERT_JS_GLOBAL_EQUALS_I("tm_w", 123);
ASSERT_JS_GLOBAL_EQUALS_I("tm_h", 456);
cl_assert_equal_i(1, s_graphics_text_layout_get_max_used_size.call_count);
const MockCallRecording *lc = &s_graphics_text_layout_get_max_used_size.last_call;
cl_assert_equal_s("foo", lc->max_used_size.text);
cl_assert_equal_p(s_rocky_text_state.font, lc->max_used_size.font);
cl_assert_equal_rect((GRect(0, 0, INT16_MAX, INT16_MAX)), lc->max_used_size.box);
cl_assert_equal_i(s_rocky_text_state.overflow_mode, lc->max_used_size.overflow_mode);
cl_assert_equal_i(s_rocky_text_state.alignment, lc->max_used_size.alignment);
}
void test_rocky_api_graphics__state_initialized_between_renders(void) {
prv_global_init_and_set_ctx();
// fill text_state with unique values we can test against
s_rocky_text_state = (RockyAPITextState) {
.font = (GFont)-1,
.overflow_mode = (GTextOverflowMode)-2,
.alignment = (GTextAlignment)-3,
.text_attributes = (GTextAttributes *)-4,
};
EXECUTE_SCRIPT("_rocky.on('draw', function(e) {});");
Layer *l = &app_window_stack_get_top_window()->layer;
l->update_proc(l, NULL);
cl_assert_equal_i(1, s_fonts_get_system_font.call_count);
cl_assert_equal_i(GTextAlignmentLeft, s_rocky_text_state.alignment);
cl_assert_equal_i(GTextOverflowModeWordWrap, s_rocky_text_state.overflow_mode);
cl_assert_equal_p(NULL, s_rocky_text_state.text_attributes);
}
void test_rocky_api_graphics__context_2d_prototype_wrap_function(void) {
prv_global_init_and_set_ctx();
EXECUTE_SCRIPT("var origFillRect = _rocky.CanvasRenderingContext2D.prototype.fillRect;\n"
"_rocky.CanvasRenderingContext2D.prototype.fillRect = function(x, y, w, h) {\n"
" w *= 2;\n"
" h *= 2;\n"
" origFillRect.call(this, x, y, w, h);\n"
"};\n"
"ctx.fillRect(5, 6, 7, 8);\n"
);
cl_assert_equal_i(1, s_graphics_fill_rect.call_count);
cl_assert_equal_rect(GRect(5, 6, 7 * 2, 8 * 2), s_graphics_fill_rect.last_call.rect);
}

View file

@ -0,0 +1,194 @@
/*
* Copyright 2024 Google 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.
*/
#include "clar.h"
#include "test_jerry_port_common.h"
#include "test_rocky_common.h"
#include "applib/graphics/gtypes.h"
#include "applib/rockyjs/api/rocky_api_global.h"
#include "applib/rockyjs/api/rocky_api_graphics.h"
#include "applib/rockyjs/api/rocky_api_graphics_color.h"
#include "applib/rockyjs/pbl_jerry_port.h"
// Standard
#include "string.h"
// Fakes
#include "fake_app_timer.h"
#include "fake_time.h"
// Stubs
#include "stubs_app_manager.h"
#include "stubs_app_state.h"
#include "stubs_logging.h"
#include "stubs_passert.h"
#include "stubs_pbl_malloc.h"
#include "stubs_resources.h"
#include "stubs_sleep.h"
#include "stubs_serial.h"
#include "stubs_syscalls.h"
#include "stubs_sys_exit.h"
size_t heap_bytes_free(void) {
return 123456;
}
void layer_get_unobstructed_bounds(const Layer *layer, GRect *bounds_out) {
*bounds_out = layer->bounds;
}
static Window s_app_window_stack_get_top_window;
Window *app_window_stack_get_top_window() {
return &s_app_window_stack_get_top_window;
}
GContext s_context;
// mocks
static MockCallRecordings s_graphics_context_set_fill_color;
void graphics_context_set_fill_color(GContext* ctx, GColor color) {
record_mock_call(s_graphics_context_set_fill_color) {
.ctx = ctx, .color = color,
};
}
static MockCallRecordings s_graphics_context_set_stroke_color;
void graphics_context_set_stroke_color(GContext* ctx, GColor color) {
record_mock_call(s_graphics_context_set_stroke_color) {
.ctx = ctx, .color = color,
};
}
void graphics_context_set_stroke_width(GContext* ctx, uint8_t stroke_width) {}
void graphics_draw_line(GContext* ctx, GPoint p0, GPoint p1) {}
void graphics_fill_rect(GContext *ctx, const GRect *rect) {}
void graphics_fill_round_rect_by_value(GContext *ctx, GRect rect, uint16_t corner_radius,
GCornerMask corner_mask) {}
void graphics_draw_rect_precise(GContext *ctx, const GRectPrecise *rect) {}
void graphics_fill_radial_precise_internal(GContext *ctx, GPointPrecise center,
Fixed_S16_3 radius_inner, Fixed_S16_3 radius_outer,
int32_t angle_start, int32_t angle_end) {}
void layer_mark_dirty(Layer *layer) {}
void rocky_api_graphics_path2d_add_canvas_methods(jerry_value_t obj) {}
void rocky_api_graphics_path2d_cleanup(void) {}
void rocky_api_graphics_path2d_reset_state(void) {}
void rocky_api_graphics_text_init(void) {}
void rocky_api_graphics_text_deinit(void) {}
void rocky_api_graphics_text_add_canvas_methods(jerry_value_t obj) {}
void rocky_api_graphics_text_reset_state(void) {}
jerry_value_t prv_rocky_api_graphics_get_canvas_context_2d(void);
void test_rocky_api_graphics_color__initialize(void) {
fake_app_timer_init();
rocky_runtime_context_init();
jerry_init(JERRY_INIT_EMPTY);
s_app_window_stack_get_top_window = (Window){};
s_context = (GContext){};
s_app_state_get_graphics_context = &s_context;
s_app_event_loop_callback = NULL;
s_graphics_context_set_stroke_color = (MockCallRecordings){0};
s_graphics_context_set_fill_color = (MockCallRecordings){0};
}
void test_rocky_api_graphics_color__cleanup(void) {
fake_app_timer_deinit();
// some tests deinitialize the engine, avoid double de-init
if (app_state_get_rocky_runtime_context() != NULL) {
jerry_cleanup();
rocky_runtime_context_deinit();
}
}
static const RockyGlobalAPI *s_graphics_api[] = {
&GRAPHIC_APIS,
NULL,
};
#define cl_assert_parsed_color(str, expected_color_ptr) do { \
GColor actual_color = {0}; \
actual_color.argb = 123; \
const bool actual_bool = rocky_api_graphics_color_parse(str, &actual_color); \
if (expected_color_ptr) { \
cl_assert_equal_b(true, actual_bool); \
cl_assert_equal_i(((GColor*)(expected_color_ptr))->argb, actual_color.argb); \
} else { \
cl_assert_equal_b(false, actual_bool); \
}\
} while(0)
void test_rocky_api_graphics_color__parse_names(void) {
rocky_global_init(s_graphics_api);
cl_assert_parsed_color("unknown", NULL);
cl_assert_parsed_color("clear", &GColorClear);
cl_assert_parsed_color("black", &GColorBlack);
cl_assert_parsed_color("red", &GColorRed);
cl_assert_parsed_color("white", &GColorWhite);
cl_assert_parsed_color("gray", &GColorLightGray);
}
extern const RockyAPIGraphicsColorDefinition s_color_definitions[];
void test_rocky_api_graphics_color__color_names_consistent(void) {
rocky_global_init(s_graphics_api);
const RockyAPIGraphicsColorDefinition *def = s_color_definitions;
while (def->name) {
GColor8 actual;
const bool result = rocky_api_graphics_color_parse(def->name, &actual);
cl_assert_equal_b(true, result);
cl_assert_equal_i(def->value, actual.argb);
def++;
}
}
void test_rocky_api_graphics_color__hex(void) {
// invalid cases
cl_assert_parsed_color("#", NULL);
cl_assert_parsed_color("##q3", NULL);
cl_assert_parsed_color("", NULL);
cl_assert_parsed_color("#00zz10", NULL);
cl_assert_parsed_color("#123456789", NULL);
// different lengths
cl_assert_parsed_color("#f00", &GColorRed);
cl_assert_parsed_color("#FF0000", &GColorRed);
cl_assert_parsed_color("#F00f", &GColorRed);
cl_assert_parsed_color("#FF0000FF", &GColorRed);
// discard rgb components if alpha == 0
cl_assert_parsed_color("#12345600", &GColorClear);
cl_assert_parsed_color("#1230", &GColorClear);
// correctly assign different components
cl_assert_parsed_color("#00FF00", &GColorGreen);
cl_assert_parsed_color("#0000FF", &GColorBlue);
}

View file

@ -0,0 +1,472 @@
/*
* Copyright 2024 Google 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.
*/
#include <applib/rockyjs/api/rocky_api_graphics_path2d.h>
#include "clar.h"
#include "test_jerry_port_common.h"
#include "test_rocky_common.h"
#include "applib/graphics/gpath.h"
#include "applib/graphics/gtypes.h"
#include "applib/rockyjs/api/rocky_api_global.h"
#include "applib/rockyjs/api/rocky_api_graphics.h"
#include "applib/rockyjs/api/rocky_api_graphics_color.h"
#include "applib/rockyjs/pbl_jerry_port.h"
#include "util/trig.h"
// Standard
#include "string.h"
// Fakes
#include "fake_app_timer.h"
#include "fake_logging.h"
#include "fake_pbl_malloc.h"
#include "fake_time.h"
// Stubs
#include "stubs_app_manager.h"
#include "stubs_app_state.h"
#include "stubs_logging.h"
#include "stubs_passert.h"
#include "stubs_resources.h"
#include "stubs_sleep.h"
#include "stubs_serial.h"
#include "stubs_syscalls.h"
#include "stubs_sys_exit.h"
size_t heap_bytes_free(void) {
return 123456;
}
void layer_get_unobstructed_bounds(const Layer *layer, GRect *bounds_out) {
*bounds_out = layer->bounds;
}
bool rocky_api_graphics_color_parse(const char *color_value, GColor8 *parsed_color) {
return false;
}
bool rocky_api_graphics_color_from_value(jerry_value_t value, GColor *result) {
return false;
}
static Window s_app_window_stack_get_top_window;
Window *app_window_stack_get_top_window() {
return &s_app_window_stack_get_top_window;
}
GPointPrecise gpoint_from_polar_precise(const GPointPrecise *precise_center,
uint16_t precise_radius, int32_t angle) {
return GPointPreciseFromGPoint(GPointZero);
}
GContext s_context;
void rocky_api_graphics_text_init(void) {}
void rocky_api_graphics_text_deinit(void) {}
void rocky_api_graphics_text_add_canvas_methods(jerry_value_t obj) {}
void rocky_api_graphics_text_reset_state(void) {}
void graphics_context_set_fill_color(GContext* ctx, GColor color) {}
void graphics_context_set_stroke_color(GContext* ctx, GColor color) {}
void graphics_context_set_stroke_width(GContext* ctx, uint8_t stroke_width) {}
// mocks
static MockCallRecordings s_graphics_line_draw_precise_stroked;
void graphics_line_draw_precise_stroked(GContext* ctx, GPointPrecise p0, GPointPrecise p1) {
record_mock_call(s_graphics_line_draw_precise_stroked) {
.ctx = ctx, .pp0 = p0, .pp1 = p1,
};
}
void graphics_draw_line(GContext* ctx, GPoint p0, GPoint p1) {
// TODO: remove me PBL-42458 (still used for drawing arc)
record_mock_call(s_graphics_line_draw_precise_stroked) {.ctx = ctx};
}
MockCallRecordings s_graphics_draw_arc_precise;
void graphics_draw_arc_precise_internal(GContext *ctx, GPointPrecise center, Fixed_S16_3 radius,
int32_t angle_start, int32_t angle_end) {
record_mock_call(s_graphics_draw_arc_precise) {
.draw_arc.center = center,
.draw_arc.radius = radius,
.draw_arc.angle_start = angle_start,
.draw_arc.angle_end = angle_end,
};
}
MockCallRecordings s_gpath_draw_filled;
void gpath_draw_filled(GContext* ctx, GPath *path) {
record_mock_call(s_gpath_draw_filled) {
.path.num_points = path->num_points,
};
memcpy(s_gpath_draw_filled.last_call.path.points,
path->points, sizeof(path->points[0]) * path->num_points);
}
void graphics_fill_rect(GContext *ctx, const GRect *rect) {}
void graphics_fill_round_rect_by_value(GContext *ctx, GRect rect, uint16_t corner_radius,
GCornerMask corner_mask) {}
void graphics_draw_rect_precise(GContext *ctx, const GRectPrecise *rect) {}
void graphics_fill_radial_precise_internal(GContext *ctx, GPointPrecise center,
Fixed_S16_3 radius_inner, Fixed_S16_3 radius_outer,
int32_t angle_start, int32_t angle_end) {}
void layer_mark_dirty(Layer *layer) {}
jerry_value_t prv_create_canvas_context_2d_for_layer(Layer *layer);
static void prv_create_global_ctx(void) {
// make this easily testable by putting it int JS context as global
Layer l = {.bounds = GRect(0, 0, 144, 168)};
const jerry_value_t ctx = prv_create_canvas_context_2d_for_layer(&l);
cl_assert_equal_b(jerry_value_is_object(ctx), true);
jerry_set_object_field(jerry_get_global_object(), "ctx", ctx);
}
void test_rocky_api_graphics_path2d__initialize(void) {
fake_malloc_set_largest_free_block(~0);
s_log_internal__expected = NULL;
rocky_runtime_context_init();
fake_app_timer_init();
jerry_init(JERRY_INIT_EMPTY);
s_app_window_stack_get_top_window = (Window){};
s_context = (GContext){};
s_app_state_get_graphics_context = &s_context;
s_app_event_loop_callback = NULL;
s_graphics_line_draw_precise_stroked = (MockCallRecordings){0};
s_graphics_draw_arc_precise = (MockCallRecordings){0};
s_gpath_draw_filled = (MockCallRecordings){0};
}
void test_rocky_api_graphics_path2d__cleanup(void) {
fake_app_timer_deinit();
// Frees the internal path steps array ():
rocky_api_graphics_path2d_reset_state();
// some tests deinitialize the engine, avoid double de-init
if (app_state_get_rocky_runtime_context() != NULL) {
jerry_cleanup();
rocky_runtime_context_deinit();
}
fake_pbl_malloc_check_net_allocs();
}
static const RockyGlobalAPI *s_graphics_api[] = {
&GRAPHIC_APIS,
NULL,
};
#define PP(x, y) \
GPointPrecise((int16_t)(((x)) * FIXED_S16_3_FACTOR), \
(int16_t)(((y)) * FIXED_S16_3_FACTOR))
void test_rocky_api_graphics_path2d__invalid_coords(void) {
rocky_global_init(s_graphics_api);
prv_create_global_ctx();
EXECUTE_SCRIPT("ctx.moveTo(4095.375, -4095.5);");
EXECUTE_SCRIPT_EXPECT_ERROR("ctx.moveTo(4096.5, 0);", "TypeError: Value out of bounds");
EXECUTE_SCRIPT_EXPECT_ERROR("ctx.moveTo(0, -4095.625);", "TypeError: Value out of bounds");
EXECUTE_SCRIPT("ctx.lineTo(4095.375, -4095.5);");
EXECUTE_SCRIPT_EXPECT_ERROR("ctx.lineTo(4096.5, 0);", "TypeError: Value out of bounds");
EXECUTE_SCRIPT_EXPECT_ERROR("ctx.lineTo(0, -4095.625);", "TypeError: Value out of bounds");
}
void test_rocky_api_graphics_path2d__minimal_path(void) {
rocky_global_init(s_graphics_api);
prv_create_global_ctx();
EXECUTE_SCRIPT(
"ctx.beginPath();\n"
"ctx.moveTo(1, 2);\n"
"ctx.lineTo(3.5, -4.5);\n"
"ctx.stroke();\n"
);
cl_assert_equal_i(1, s_graphics_line_draw_precise_stroked.call_count);
cl_assert_equal_point_precise(PP(0.5, 1.5), s_graphics_line_draw_precise_stroked.last_call.pp0);
cl_assert_equal_point_precise(PP(3, -5), s_graphics_line_draw_precise_stroked.last_call.pp1);
EXECUTE_SCRIPT(
"ctx.fill();\n"
);
cl_assert_equal_i(0, s_gpath_draw_filled.call_count);
}
void test_rocky_api_graphics_path2d__more_lines(void) {
rocky_global_init(s_graphics_api);
prv_create_global_ctx();
EXECUTE_SCRIPT(
"ctx.beginPath();\n"
"ctx.moveTo(1, 2);\n"
"ctx.lineTo(3, 4);\n"
"ctx.lineTo(5, 6);\n"
"ctx.lineTo(7, 8);\n"
"ctx.moveTo(9, 10);\n"
"ctx.lineTo(11, 12);\n"
"ctx.stroke();\n"
);
cl_assert_equal_i(4, s_graphics_line_draw_precise_stroked.call_count);
cl_assert_equal_point_precise(PP(8.5, 9.5), s_graphics_line_draw_precise_stroked.last_call.pp0);
cl_assert_equal_point_precise(PP(10.5, 11.5), s_graphics_line_draw_precise_stroked.last_call.pp1);
EXECUTE_SCRIPT(
"ctx.fill();\n"
);
// only first shape has at least 3 points
cl_assert_equal_i(1, s_gpath_draw_filled.call_count);
MockCallRecording *lc = &s_gpath_draw_filled.last_call;
cl_assert_equal_i(4, lc->path.num_points);
cl_assert_equal_point(GPoint(0, 1), lc->path.points[0]);
cl_assert_equal_point(GPoint(2, 3), lc->path.points[1]);
cl_assert_equal_point(GPoint(4, 5), lc->path.points[2]);
cl_assert_equal_point(GPoint(6, 7), lc->path.points[3]);
}
void test_rocky_api_graphics_path2d__fill(void) {
rocky_global_init(s_graphics_api);
prv_create_global_ctx();
EXECUTE_SCRIPT(
"ctx.moveTo(1, 2);\n"
"ctx.lineTo(3, 4);\n"
"ctx.fill();\n"
);
// only 2 points
cl_assert_equal_i(0, s_gpath_draw_filled.call_count);
EXECUTE_SCRIPT(
"ctx.lineTo(5, 6);\n"
"ctx.fill();\n"
);
cl_assert_equal_i(1, s_gpath_draw_filled.call_count);
MockCallRecording *lc = &s_gpath_draw_filled.last_call;
cl_assert_equal_i(3, lc->path.num_points);
cl_assert_equal_point(GPoint(0, 1), lc->path.points[0]);
cl_assert_equal_point(GPoint(2, 3), lc->path.points[1]);
cl_assert_equal_point(GPoint(4, 5), lc->path.points[2]);
s_gpath_draw_filled.call_count = 0;
EXECUTE_SCRIPT(
"ctx.moveTo(7, 8);\n"
"ctx.lineTo(9, 10);\n"
"ctx.fill();\n"
);
// still only the first part (before the .moveTo()) as the second only has two points
cl_assert_equal_i(1, s_gpath_draw_filled.call_count);
cl_assert_equal_i(3, lc->path.num_points);
cl_assert_equal_point(GPoint(0, 1), lc->path.points[0]);
cl_assert_equal_point(GPoint(2, 3), lc->path.points[1]);
cl_assert_equal_point(GPoint(4, 5), lc->path.points[2]);
s_gpath_draw_filled.call_count = 0;
EXECUTE_SCRIPT(
"ctx.lineTo(11.5, 12.7);\n"
"ctx.fill();\n"
);
// still only the first part (before the .moveTo()) as the second only has two points
cl_assert_equal_i(2, s_gpath_draw_filled.call_count);
cl_assert_equal_i(3, lc->path.num_points);
cl_assert_equal_point(GPoint(6, 7), lc->path.points[0]);
cl_assert_equal_point(GPoint(8, 9), lc->path.points[1]);
cl_assert_equal_point(GPoint(11, 12), lc->path.points[2]);
EXECUTE_SCRIPT_EXPECT_ERROR(
"ctx.arc(1, 2, 3, 4, 5);\n"
"ctx.fill();\n"
, "TypeError: fill() does not support arc()");
}
void test_rocky_api_graphics_path2d__fill_oom(void) {
rocky_global_init(s_graphics_api);
prv_create_global_ctx();
EXECUTE_SCRIPT(
"ctx.moveTo(1, 2);\n"
"ctx.lineTo(3, 4);\n"
"ctx.lineTo(5, 6);\n"
);
// OOM!
fake_malloc_set_largest_free_block(0);
// Call implementation directly instead of executing a script, to avoid mallocs by the VM itself:
extern jerry_value_t rocky_api_graphics_path2d_call_fill(void);
const jerry_value_t error_value = rocky_api_graphics_path2d_call_fill();
ASSERT_JS_ERROR(error_value, "RangeError: Out of memory: too many points to fill");
}
void test_rocky_api_graphics_path2d__arc(void) {
rocky_global_init(s_graphics_api);
prv_create_global_ctx();
EXECUTE_SCRIPT(
"ctx.beginPath();\n"
"ctx.moveTo(1, 2);\n"
"ctx.arc(50, 40, 30, Math.PI, 0);\n"
"ctx.arc(60, 80.1, 20.5, 0, Math.PI, false);\n"
"ctx.stroke();\n"
);
cl_assert_equal_i(2, s_graphics_line_draw_precise_stroked.call_count);
cl_assert_equal_i(2, s_graphics_draw_arc_precise.call_count);
MockCallRecording *lc = &s_graphics_draw_arc_precise.last_call;
cl_assert_equal_point_precise(PP(59.5, 79.625), lc->draw_arc.center);
cl_assert_equal_i(20.5 * 8, lc->draw_arc.radius.raw_value);
cl_assert_equal_i(TRIG_MAX_ANGLE * 1 / 4, lc->draw_arc.angle_start);
cl_assert_equal_i(TRIG_MAX_ANGLE * 3 / 4, lc->draw_arc.angle_end);
}
void test_rocky_api_graphics_path2d__anti_clockwise(void) {
rocky_global_init(s_graphics_api);
prv_create_global_ctx();
EXECUTE_SCRIPT(
"ctx.beginPath();\n"
"ctx.moveTo(80, 40);\n"
"ctx.arc(60, 80, 20, 0, Math.PI, true);\n"
"ctx.stroke();\n"
);
cl_assert_equal_i(1, s_graphics_line_draw_precise_stroked.call_count);
cl_assert_equal_i(1, s_graphics_draw_arc_precise.call_count);
MockCallRecording *lc = &s_graphics_draw_arc_precise.last_call;
cl_assert_equal_point_precise(PP(59.5, 79.5), lc->draw_arc.center);
cl_assert_equal_i(20 * 8, lc->draw_arc.radius.raw_value);
cl_assert_equal_i(TRIG_MAX_ANGLE * 3 / 4, lc->draw_arc.angle_start);
cl_assert_equal_i(TRIG_MAX_ANGLE * 5 / 4, lc->draw_arc.angle_end);
}
void test_rocky_api_graphics_path2d__unsupported(void) {
rocky_global_init(s_graphics_api);
prv_create_global_ctx();
EXECUTE_SCRIPT_EXPECT_UNDEFINED("ctx.arcTo");
EXECUTE_SCRIPT_EXPECT_UNDEFINED("ctx.bezierCurveTo");
EXECUTE_SCRIPT_EXPECT_UNDEFINED("ctx.quadraticCurveTo");
}
extern size_t s_rocky_path_steps_num;
extern RockyAPIPathStep *s_rocky_path_steps;
void test_rocky_api_graphics_path2d__state_initialized_between_renders(void) {
rocky_global_init(s_graphics_api);
s_rocky_path_steps_num = 2;
EXECUTE_SCRIPT("_rocky.on('draw', function(e) {});");
Layer *l = &app_window_stack_get_top_window()->layer;
l->update_proc(l, NULL);
cl_assert_equal_i(0, s_rocky_path_steps_num);
}
void test_rocky_api_graphics_path2d__rect(void) {
rocky_global_init(s_graphics_api);
prv_create_global_ctx();
cl_assert_equal_i(0, s_rocky_path_steps_num);
EXECUTE_SCRIPT(
"ctx.moveTo(1, 2);\n"
"ctx.rect(3, 4, 5, 6);\n"
);
cl_assert_equal_i(6, s_rocky_path_steps_num);
EXECUTE_SCRIPT(
"ctx.rect(7, 8, 9, 10);\n"
);
cl_assert_equal_i(11, s_rocky_path_steps_num);
cl_assert_equal_i(RockyAPIPathStepType_MoveTo, s_rocky_path_steps[0].type);
cl_assert_equal_i(RockyAPIPathStepType_MoveTo, s_rocky_path_steps[1].type);
cl_assert_equal_i(RockyAPIPathStepType_LineTo, s_rocky_path_steps[5].type);
cl_assert_equal_i(RockyAPIPathStepType_MoveTo, s_rocky_path_steps[6].type);
cl_assert_equal_point_precise((GPointPrecise(20, 28)), s_rocky_path_steps[1].pt.xy);
cl_assert_equal_point_precise((GPointPrecise(60, 76)), s_rocky_path_steps[3].pt.xy);
cl_assert_equal_point_precise((GPointPrecise(20, 28)), s_rocky_path_steps[5].pt.xy);
// actual correctness of these values is test in test_rocky_api_graphics_rendering.c
cl_assert_equal_vector_precise((GVectorPrecise(0, 8)), s_rocky_path_steps[1].pt.fill_delta);
cl_assert_equal_vector_precise((GVectorPrecise(8, 0)), s_rocky_path_steps[3].pt.fill_delta);
cl_assert_equal_vector_precise((GVectorPrecise(0, 8)), s_rocky_path_steps[5].pt.fill_delta);
}
void test_rocky_api_graphics_path2d__close_path(void) {
rocky_global_init(s_graphics_api);
prv_create_global_ctx();
cl_assert_equal_i(0, s_rocky_path_steps_num);
EXECUTE_SCRIPT(
"ctx.moveTo(1, 2);\n"
"ctx.closePath();\n"
);
cl_assert_equal_i(1, s_rocky_path_steps_num);
EXECUTE_SCRIPT(
"ctx.lineTo(3, 4);\n"
"ctx.closePath();\n"
);
cl_assert_equal_i(3, s_rocky_path_steps_num);
cl_assert_equal_i(RockyAPIPathStepType_LineTo, s_rocky_path_steps[2].type);
cl_assert_equal_point_precise(GPointPrecise(4, 12), (s_rocky_path_steps[0].pt.xy));
cl_assert_equal_point_precise(GPointPrecise(4, 12), (s_rocky_path_steps[2].pt.xy));
}
extern jerry_value_t rocky_api_graphics_path2d_try_allocate_steps(size_t increment_steps);
extern size_t rocky_api_graphics_path2d_min_array_len(void);
extern size_t rocky_api_graphics_path2d_array_len(void);
void test_rocky_api_graphics_path2d__initial_increment_larger_than_initial_size(void) {
cl_assert_equal_i(rocky_api_graphics_path2d_array_len(), 0);
const size_t min_size = rocky_api_graphics_path2d_min_array_len();
const jerry_value_t rv = rocky_api_graphics_path2d_try_allocate_steps(min_size + 1);
ASSERT_JS_ERROR(rv, NULL);
jerry_release_value(rv);
const size_t actual_size = rocky_api_graphics_path2d_array_len();
cl_assert(actual_size >= min_size + 1);
}
void test_rocky_api_graphics_path2d__array_realloc_oom(void) {
fake_malloc_set_largest_free_block(0);
const jerry_value_t rv = rocky_api_graphics_path2d_try_allocate_steps(1);
ASSERT_JS_ERROR(rv, "RangeError: Out of memory: can't create more path steps");
jerry_release_value(rv);
}

View file

@ -0,0 +1,312 @@
/*
* Copyright 2024 Google 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.
*/
#include "clar.h"
#include "test_jerry_port_common.h"
#define DO_NOT_STUB_LEGACY2 1
#include "test_rocky_common.h"
#include "applib/graphics/gtypes.h"
#include "applib/rockyjs/api/rocky_api.h"
#include "applib/rockyjs/api/rocky_api_global.h"
#include "applib/rockyjs/api/rocky_api_graphics.h"
#include "applib/rockyjs/api/rocky_api_graphics_text.h"
#include "applib/rockyjs/pbl_jerry_port.h"
#include "util/trig.h"
#include "applib/graphics/framebuffer.h"
#include "../graphics/util.h"
// Standard
#include "string.h"
// Fakes
#include "fake_app_timer.h"
#include "fake_time.h"
// Stubs
#include "stubs_app_manager.h"
#include "stubs_app_state.h"
#include "stubs_logging.h"
#include "stubs_passert.h"
#include "stubs_pbl_malloc.h"
#include "stubs_resources.h"
#include "stubs_sleep.h"
#include "stubs_serial.h"
#include "stubs_syscalls.h"
#include "stubs_sys_exit.h"
size_t heap_bytes_free(void) {
return 123456;
}
bool gbitmap_init_with_png_data(GBitmap *bitmap, const uint8_t *data, size_t data_size) {
return false;
}
bool gbitmap_png_data_is_png(const uint8_t *data, size_t data_size) {
return false;
}
void layer_get_unobstructed_bounds(const Layer *layer, GRect *bounds_out) {
*bounds_out = layer->bounds;
}
void layer_mark_dirty(Layer *layer) {}
static Window s_app_window_stack_get_top_window;
Window *app_window_stack_get_top_window() {
return &s_app_window_stack_get_top_window;
}
// no text rendering in this test
void rocky_api_graphics_text_init(void) {}
void rocky_api_graphics_text_deinit(void) {}
void rocky_api_graphics_text_add_canvas_methods(jerry_value_t obj) {}
void rocky_api_graphics_text_reset_state(void) {}
//GBitmap s_bitmap;
//uint8_t s_bitmap_data[DISPLAY_FRAMEBUFFER_BYTES];
GContext s_context;
FrameBuffer *s_framebuffer;
GBitmap *s_pixels;
static void prv_init_gcontext(GSize size) {
graphics_context_init(&s_context, s_framebuffer, GContextInitializationMode_App);
framebuffer_clear(s_framebuffer);
if (s_pixels) {
gbitmap_destroy(s_pixels);
}
s_pixels = gbitmap_create_blank(size, GBITMAP_NATIVE_FORMAT);
memset(s_pixels->addr, 0xff, size.h * s_pixels->row_size_bytes);
s_context.dest_bitmap = *s_pixels;
s_context.draw_state.clip_box = (GRect){.size = size};
s_context.draw_state.drawing_box = s_context.draw_state.clip_box;
s_app_state_get_graphics_context = &s_context;
}
void test_rocky_api_graphics_rendering__initialize(void) {
fake_app_timer_init();
rocky_runtime_context_init();
jerry_init(JERRY_INIT_EMPTY);
s_framebuffer = malloc(sizeof(FrameBuffer));
framebuffer_init(s_framebuffer, &(GSize) { DISP_COLS, DISP_ROWS });
s_app_window_stack_get_top_window = (Window){};
prv_init_gcontext((GSize) { DISP_COLS, DISP_ROWS });
s_app_event_loop_callback = NULL;
}
void test_rocky_api_graphics_rendering__cleanup(void) {
fake_app_timer_deinit();
// some tests deinitialize the engine, avoid double de-init
if (app_state_get_rocky_runtime_context() != NULL) {
jerry_cleanup();
rocky_runtime_context_deinit();
}
gbitmap_destroy(s_pixels);
s_pixels = NULL;
free(s_framebuffer);
}
static const RockyGlobalAPI *s_graphics_api[] = {
&GRAPHIC_APIS,
NULL,
};
jerry_value_t prv_create_canvas_context_2d_for_layer(Layer *layer);
static const jerry_value_t prv_global_init_and_set_ctx(void) {
rocky_global_init(s_graphics_api);
// make this easily testable by putting it int JS context as global
Layer l = {.bounds = GRect(0, 0, 144, 168)};
const jerry_value_t ctx = prv_create_canvas_context_2d_for_layer(&l);
jerry_set_object_field(jerry_get_global_object(), "ctx", ctx);
return ctx;
}
void test_rocky_api_graphics_rendering__lines(void) {
prv_global_init_and_set_ctx();
// taken from http://fiddle.jshell.net/9298zub9/2/
EXECUTE_SCRIPT(
"var t1 = 10;\n"
"var b1 = 20.5;\n"
"var t2 = 30.5;\n"
"var b2 = 40;\n"
" \n"
"for (var i = 1; i <= 5; i++) {\n"
" ctx.beginPath();\n"
" var x1 = 20 * i;\n"
" var x2 = x1 + 10.5; \n"
" ctx.moveTo(x1, t1);\n"
" ctx.lineTo(x1, b1);\n"
" ctx.moveTo(x2, t1);\n"
" ctx.lineTo(x2, b1);\n"
"\n"
" ctx.moveTo(x1, t2);\n"
" ctx.lineTo(x1, b2);\n"
" ctx.moveTo(x2, t2);\n"
" ctx.lineTo(x2, b2);\n"
"\n"
" ctx.lineWidth = i;\n"
" ctx.stroke();\n"
"}\n"
"for (var i = 1; i <= 5; i++) {\n"
" ctx.beginPath();\n"
" var y1 = 40 + i * 20;\n"
" var y2 = y1 + 10.5;\n"
" ctx.moveTo(t1, y1);\n"
" ctx.lineTo(b1, y1);\n"
" ctx.moveTo(t1, y2);\n"
" ctx.lineTo(b1, y2);\n"
" \n"
" ctx.moveTo(t2, y1);\n"
" ctx.lineTo(b2, y1);\n"
" ctx.moveTo(t2, y2);\n"
" ctx.lineTo(b2, y2);\n"
"\n"
" ctx.lineWidth = i;\n"
" ctx.stroke();\n"
"}\n"
"for (var i = 1; i <= 5; i++) {\n"
" ctx.beginPath();\n"
" var xx = 50;\n"
" var yy = 50;\n"
" var d = 15 * i;\n"
" ctx.moveTo(xx, yy + d);\n"
" ctx.lineTo(xx + d, yy);\n"
"\n"
" ctx.lineWidth = i;\n"
" ctx.stroke();\n"
"}"
);
const bool eq_result =
gbitmap_pbi_eq(&s_context.dest_bitmap, TEST_NAMED_PBI_FILE("rocky_rendering_lines"));
cl_check(eq_result);
}
void test_rocky_api_graphics_rendering__rect(void) {
prv_init_gcontext(GSize(500, 150));
prv_global_init_and_set_ctx();
// taken from http://fiddle.jshell.net/a5gjzb7c/6/
EXECUTE_SCRIPT(
"function render(x, y, f) {\n"
" f(x + 10, y + 10, 10, 10);\n"
" f(x + 30.2, y + 10, 10, 10.2);\n"
" f(x + 50.5, y + 10, 10, 10);\n"
" f(x + 70.7, y + 10, 10.5, 10.8);\n"
" f(x + 10, y + 30.5, 10, 10);\n"
" f(x + 30.2, y + 30.5, 10, 10.2);\n"
" f(x + 50.5, y + 30.5, 10, 10);\n"
" f(x + 70.7, y + 30.5, 10.5, 10.8);\n"
" \n"
" f(x + 90, y + 10, 0, 0);\n"
" f(x + 110, y + 10, 0.5, 0.5);\n"
" f(x + 90, y + 30, -2, -2);\n"
" f(x + 110, y + 30, -5.5, -6);\n"
"}"
"\n"
"for (var i = 0; i <= 3; i++) {\n"
" ctx.lineWidth = i;\n"
" var x = 120 * i;\n"
" render(x, 0, ctx[i == 0 ? 'fillRect' : 'strokeRect'].bind(ctx));\n"
" render(x, 50, function(x,y,w,h) {\n"
" ctx.beginPath();\n"
" ctx.rect(x, y, w, h);\n"
" ctx[i == 0? 'fill' : 'stroke'](); \n"
" });\n"
" render(x, 100, function r(x, y, w, h) {\n"
" ctx.beginPath();\n"
" ctx.moveTo(x, y);\n"
" ctx.lineTo(x + w, y);\n"
" ctx.lineTo(x + w, y + h);\n"
" ctx.lineTo(x, y + h);\n"
" ctx.lineTo(x, y);\n"
" ctx[i == 0? 'fill' : 'stroke'](); \n"
" });\n"
"}"
);
const bool eq_result =
gbitmap_pbi_eq(&s_context.dest_bitmap, TEST_NAMED_PBI_FILE("rocky_rendering_rect"));
cl_check(eq_result);
}
void test_rocky_api_graphics_rendering__arc(void) {
prv_init_gcontext(GSize(500, 300));
prv_global_init_and_set_ctx();
// http://fiddle.jshell.net/uopr1ez2/2/
EXECUTE_SCRIPT(
"var xx = 200;\n"
"\n"
"function f(x, y, r, a1, a2) {\n"
" ctx.beginPath();\n"
" ctx.arc(x, y, r, a1, a2, false);\n"
" ctx.stroke();\n"
"\n"
" ctx.rockyFillRadial(x + xx, y, 0, r, a1, a2);\n"
"}\n"
"\n"
"function g(x, y, a1, a2) {\n"
" f(x, y, 5, a1, a2);\n"
" f(x, y, 15.5, a1, a2);\n"
" f(x, y, 25.2, a1, a2);\n"
" f(x, y, 34.8, a1, a2);\n"
"}\n"
"\n"
"function h(x, y, a1, a2) {\n"
" for (var i = 0; i < 4; i++) {\n"
" ctx.lineWidth = i + 1;\n"
" g(x, y + 40 * i, a1, a2);\n"
" }\n"
"}\n"
"\n"
"h(2, 2, 0, 0.5 * Math.PI);\n"
"h(50.5, 2.5, 0, 0.5 * Math.PI);\n"
"h(100.2, 2.2, 0, 0.5 * Math.PI);\n"
"h(150.8, 2.8, 0, 0.5 * Math.PI);\n"
"\n"
"ctx.lineWidth = 1;\n"
"f(20, 200, 10, 0, 2 * Math.PI);\n"
"f(60.5, 200, 10, 0, 2 * Math.PI);\n"
"f(100.5, 200.5, 10, 0, 2 * Math.PI);\n"
"f(140, 200.5, 10, 0, 2 * Math.PI);\n"
"\n"
"f(20, 240, 11, 0, 2 * Math.PI);\n"
"f(60.5, 240, 11, 0, 2 * Math.PI);\n"
"f(100.5, 240.5, 11, 0, 2 * Math.PI);\n"
"f(140, 240.5, 11, 0, 2 * Math.PI);\n"
"\n"
"f(20, 280, 11, 0, -0.5 * Math.PI);"
);
const bool eq_result =
gbitmap_pbi_eq(&s_context.dest_bitmap, TEST_NAMED_PBI_FILE("rocky_rendering_arc"));
cl_check(eq_result);
}

View file

@ -0,0 +1,265 @@
/*
* Copyright 2024 Google 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.
*/
#include "clar.h"
#include "test_jerry_port_common.h"
#include "test_rocky_common.h"
#include "applib/rockyjs/api/rocky_api_global.h"
#include "applib/rockyjs/api/rocky_api_memory.h"
#include "applib/rockyjs/pbl_jerry_port.h"
#include "syscall/syscall.h"
#include <jmem/jmem-heap.h>
// Standard
#include <string.h>
// Fakes
#include "fake_app_timer.h"
#include "fake_logging.h"
#include "fake_pbl_malloc.h"
#include "fake_time.h"
// Stubs
#include "stubs_app_manager.h"
#include "stubs_app_state.h"
#include "stubs_logging.h"
#include "stubs_passert.h"
#include "stubs_resources.h"
#include "stubs_sleep.h"
#include "stubs_serial.h"
#include "stubs_syscalls.h"
size_t heap_bytes_free(void) {
return 123456;
}
static int s_sys_analytics_inc_call_count;
void sys_analytics_inc(AnalyticsMetric metric, AnalyticsClient client) {
cl_assert_equal_i(metric, ANALYTICS_APP_METRIC_MEM_ROCKY_RECURSIVE_MEMORYPRESSURE_EVENT_COUNT);
++s_sys_analytics_inc_call_count;
}
static const RockyGlobalAPI *s_api[] = {
&MEMORY_APIS,
NULL,
};
static bool s_skip_pbl_malloc_check;
#define assert_oom_app_fault() \
cl_assert_equal_i(s_app_heap_analytics_log_rocky_heap_oom_fault_call_count, 1)
#define assert_no_oom_app_fault() \
cl_assert_equal_i(s_app_heap_analytics_log_rocky_heap_oom_fault_call_count, 0)
void test_rocky_api_memory__initialize(void) {
s_sys_analytics_inc_call_count = 0;
s_app_heap_analytics_log_rocky_heap_oom_fault_call_count = 0;
fake_pbl_malloc_clear_tracking();
s_skip_pbl_malloc_check = false;
s_log_internal__expected = NULL;
rocky_runtime_context_init();
jerry_init(JERRY_INIT_EMPTY);
rocky_global_init(s_api);
}
void test_rocky_api_memory__cleanup(void) {
rocky_global_deinit();
jerry_cleanup();
rocky_runtime_context_deinit();
if (!s_skip_pbl_malloc_check) {
fake_pbl_malloc_check_net_allocs();
}
}
void test_rocky_api_memory__event(void) {
cl_assert_equal_b(false, rocky_global_has_event_handlers("memorypressure"));
jmem_heap_stats_t before_stats = {};
jmem_heap_get_stats(&before_stats);
EXECUTE_SCRIPT("_rocky.on('memorypressure', function(){});");
// After registering a handler for 'memorypressure', expect a drop of more than N bytes of heap
// because of the reservation of headroom space:
jmem_heap_stats_t after_stats = {};
jmem_heap_get_stats(&after_stats);
cl_assert(after_stats.allocated_bytes - before_stats.allocated_bytes >=
ROCKY_API_MEMORY_HEADROOM_DESIRED_SIZE_BYTES);
cl_assert_equal_b(true, rocky_global_has_event_handlers("memorypressure"));
}
void test_rocky_api_memory__oom_app_fault_if_handler_allocates_more_than_headroom(void) {
s_skip_pbl_malloc_check = true;
cl_assert_passert(EXECUTE_SCRIPT(
"var data = [];\n"
"_rocky.on('memorypressure', function(){\n"
" var handlerData = [];\n"
" for (var i = 0; i < 100000; i++) {handlerData.push(i);}\n"
"});\n"
"for (var i = 0; i < 100000; i++) {data.push(i);}\n"
));
assert_oom_app_fault();
cl_assert_equal_i(s_sys_analytics_inc_call_count, 1);
}
void test_rocky_api_memory__no_oom_app_fault_if_handler_frees_up_enough_memory_empty_array(void) {
// Note to the reader: the lifecycle of `data` is not what you might think it is on first sight:
// When `data = [];` executes, the original `data` will still be retained, because the original
// execution context is still on the stack. Only after the the 'memorypressure' handler returns
// and that for(){} block finishes, is the original `data` released!
EXECUTE_SCRIPT(
"var data = [];\n"
"var level = undefined;"
"_rocky.on('memorypressure', function(e){\n"
" level = e.level;\n"
" data = [];\n"
"});\n"
"for (var i = 0; i < 100000; i++) {data.push(i);}\n"
);
assert_no_oom_app_fault();
ASSERT_JS_GLOBAL_EQUALS_S("level", "high");
cl_assert_equal_i(s_sys_analytics_inc_call_count, 0);
}
void test_rocky_api_memory__no_oom_app_fault_if_handler_frees_up_enough_memory_empty_object(void) {
EXECUTE_SCRIPT(
"var data = {};\n"
"_rocky.on('memorypressure', function(e){\n"
" data = {};\n"
"});\n"
"for (var i = 0; i < 100000; i++) {data[i] = i;}\n"
);
assert_no_oom_app_fault();
cl_assert_equal_i(s_sys_analytics_inc_call_count, 0);
}
void test_rocky_api_memory__no_oom_app_fault_if_handler_frees_up_enough_memory_put_props_for(void) {
// This example uses a lot of properties on an Object to store things.
// When running out of memory, these are dropped to free up memory, using the `delete` operator.
EXECUTE_SCRIPT(
"var first = 0;\n"
"var i = 0;\n"
"var obj = {};\n"
"_rocky.on('memorypressure', function(e){\n"
" for (var j = first; j < i; j++) {\n"
" delete obj[j];\n"
" }\n"
" first = i;\n"
"});\n"
"for (i = first; i < 100000; i++) {\n"
" obj[i] = i;"
"}\n"
);
assert_no_oom_app_fault();
cl_assert_equal_i(s_sys_analytics_inc_call_count, 0);
}
#if 0
// This doesn't work at the moment, because the `in` operator allocates a ton of memory... :( for
// the same reason as why Array.pop() has a footprint that's proportional to the number of elements.
void test_rocky_api_memory
__no_oom_app_fault_if_handler_frees_up_enough_mem_put_props_for_in(void) {
EXECUTE_SCRIPT(
"var obj = {};\n"
"_rocky.on('memorypressure', function(e){\n"
" for (var p in obj) {\n"
" delete obj[p];\n"
" }\n"
"});\n"
"for (var i = 0; i < 100000; i++) {\n"
" obj['' + i] = i;"
"}\n"
);
assert_no_oom_app_fault();
cl_assert_equal_i(s_sys_analytics_inc_call_count, 0);
}
#endif
#if 0
// This doesn't work because the putting the `length` property of an array end up calling
// ecma_op_object_get_property_names, which is has a memory footprint proportional to the number of
// elements/properties..
void test_rocky_api_memory
__no_oom_app_fault_if_handler_frees_up_enough_memory_put_length(void) {
EXECUTE_SCRIPT(
"var cache = [];\n"
"_rocky.on('memorypressure', function(event) {\n"
" while (cache.length > 0) {\n"
" delete cache[cache.length - 1];\n"
" --cache.length;\n"
" }\n"
"})\n;"
"for (var i = 0; i < 100000; i++) {\n"
" cache.push(i);\n"
"}\n"
);
assert_no_oom_app_fault();
cl_assert_equal_i(s_sys_analytics_inc_call_count, 0);
}
#endif
#if 0
// This doesn't work at the moment, because https://github.com/Samsung/jerryscript/issues/1370
void test_rocky_api_memory
__no_oom_app_fault_if_handler_frees_up_enough_memory_simple(void) {
EXECUTE_SCRIPT(
"var cache = [];\n"
"_rocky.on('memorypressure', function(event) {\n"
" while (cache.length > 0) {\n"
" cache.pop();\n"
" }\n"
"})\n;"
"for (var i = 0; i < 100000; i++) {\n"
" cache.push(i);\n"
"}\n"
);
assert_no_oom_app_fault();
cl_assert_equal_i(s_sys_analytics_inc_call_count, 0);
}
#endif
void test_rocky_api_memory__oom_app_fault_if_handler_does_not_free_up_enough_memory(void) {
s_skip_pbl_malloc_check = true;
s_log_internal__expected_regex = (const char *[]){
"Memory pressure level: high",
"heap size: [0-9]+, alloc'd: [0-9]+, waste: [0-9]+, largest free block: [0-9]+,",
"used blocks: [0-9]+, free blocks: [0-9]+",
"Fatal Error: 10",
NULL };
cl_assert_passert(EXECUTE_SCRIPT(
"var data = [];\n"
"var shouldContinue = true;\n"
"_rocky.on('memorypressure', function(){\n"
" shouldContinue = false;\n"
"});\n"
"for (var i = 0; shouldContinue && i < 100000; i++) {data.push(i);}\n"
));
assert_oom_app_fault();
cl_assert_equal_i(s_sys_analytics_inc_call_count, 0);
cl_assert(*s_log_internal__expected_regex == NULL);
}

View file

@ -0,0 +1,113 @@
/*
* Copyright 2024 Google 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.
*/
#include "clar.h"
#include "test_jerry_port_common.h"
#include "test_rocky_common.h"
#include "applib/rockyjs/api/rocky_api_global.h"
#include "applib/rockyjs/api/rocky_api_preferences.h"
#include "applib/rockyjs/pbl_jerry_port.h"
#include <string.h>
#include <applib/preferred_content_size.h>
// Fakes
#include "fake_app_timer.h"
#include "fake_time.h"
// Stubs
#include "stubs_app_state.h"
#include "stubs_logging.h"
#include "stubs_passert.h"
#include "stubs_pbl_malloc.h"
#include "stubs_serial.h"
#include "stubs_sys_exit.h"
////////////////////////////////////////////////////////////////////////////////
// Fakes / Stubs
////////////////////////////////////////////////////////////////////////////////
static PreferredContentSize s_preferred_content_size;
PreferredContentSize preferred_content_size(void) {
return s_preferred_content_size;
}
static const RockyGlobalAPI *s_preferences_api[] = {
&PREFERENCES_APIS,
NULL,
};
void test_rocky_api_preferences__initialize(void) {
rocky_runtime_context_init();
jerry_init(JERRY_INIT_EMPTY);
s_preferred_content_size = PreferredContentSizeMedium;
}
void test_rocky_api_preferences__cleanup(void) {
if (app_state_get_rocky_runtime_context() != NULL) {
jerry_cleanup();
rocky_runtime_context_deinit();
}
}
void test_rocky_api_preferences__unknown(void) {
s_preferred_content_size = (PreferredContentSize) -1;
rocky_global_init(s_preferences_api);
EXECUTE_SCRIPT("var size = _rocky.userPreferences.contentSize");
ASSERT_JS_GLOBAL_EQUALS_S("size", "medium");
}
void test_rocky_api_preferences__always_valid(void) {
s_preferred_content_size = NumPreferredContentSizes;
rocky_global_init(s_preferences_api);
EXECUTE_SCRIPT("var size = _rocky.userPreferences.contentSize");
ASSERT_JS_GLOBAL_EQUALS_S("size", "medium");
}
void test_rocky_api_preferences__small(void) {
s_preferred_content_size = PreferredContentSizeSmall;
rocky_global_init(s_preferences_api);
EXECUTE_SCRIPT("var size = _rocky.userPreferences.contentSize");
ASSERT_JS_GLOBAL_EQUALS_S("size", "small");
}
void test_rocky_api_preferences__medium(void) {
s_preferred_content_size = PreferredContentSizeMedium;
rocky_global_init(s_preferences_api);
EXECUTE_SCRIPT("var size = _rocky.userPreferences.contentSize");
ASSERT_JS_GLOBAL_EQUALS_S("size", "medium");
}
void test_rocky_api_preferences__large(void) {
s_preferred_content_size = PreferredContentSizeLarge;
rocky_global_init(s_preferences_api);
EXECUTE_SCRIPT("var size = _rocky.userPreferences.contentSize");
ASSERT_JS_GLOBAL_EQUALS_S("size", "large");
}
void test_rocky_api_preferences__extra_large(void) {
s_preferred_content_size = PreferredContentSizeExtraLarge;
rocky_global_init(s_preferences_api);
EXECUTE_SCRIPT("var size = _rocky.userPreferences.contentSize");
ASSERT_JS_GLOBAL_EQUALS_S("size", "x-large");
}

View file

@ -0,0 +1,280 @@
/*
* Copyright 2024 Google 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.
*/
#include "clar.h"
#include "test_jerry_port_common.h"
#include "test_rocky_common.h"
#include "applib/rockyjs/api/rocky_api_global.h"
#include "applib/rockyjs/api/rocky_api_tickservice.h"
#include "applib/rockyjs/pbl_jerry_port.h"
// Standard
#include "string.h"
// Fakes
#include "fake_app_timer.h"
#include "fake_logging.h"
#if EMSCRIPTEN
#include "fake_time_timeshift_js.h"
#else
#include "fake_time.h"
#endif
// Stubs
#include "stubs_app_manager.h"
#include "stubs_app_state.h"
#include "stubs_logging.h"
#include "stubs_passert.h"
#include "stubs_pbl_malloc.h"
#include "stubs_resources.h"
#include "stubs_sleep.h"
#include "stubs_serial.h"
#include "stubs_syscalls.h"
#include "stubs_sys_exit.h"
size_t heap_bytes_free(void) {
return 123456;
}
void tick_timer_service_handle_time_change(void) {}
MockCallRecordings s_tick_timer_service_subscribe;
void tick_timer_service_subscribe(TimeUnits tick_units, TickHandler handler) {
record_mock_call(s_tick_timer_service_subscribe) {
.tick_units = tick_units,
};
}
void test_rocky_api_tickservice__initialize(void) {
fake_app_timer_init();
rocky_runtime_context_init();
jerry_init(JERRY_INIT_EMPTY);
s_tick_timer_service_subscribe = (MockCallRecordings){0};
s_log_internal__expected = NULL;
}
void test_rocky_api_tickservice__cleanup(void) {
jerry_cleanup();
rocky_runtime_context_deinit();
}
static const RockyGlobalAPI *s_api[] = {
&TICKSERVICE_APIS,
NULL,
};
void test_rocky_api_tickservice__provides_events(void) {
rocky_global_init(s_api);
cl_assert_equal_i(0, s_tick_timer_service_subscribe.call_count);
cl_assert_equal_b(false, rocky_global_has_event_handlers("secondchange"));
cl_assert_equal_b(false, rocky_global_has_event_handlers("minutechange"));
cl_assert_equal_b(false, rocky_global_has_event_handlers("hourchange"));
cl_assert_equal_b(false, rocky_global_has_event_handlers("daychange"));
EXECUTE_SCRIPT("_rocky.on('daychange', function() {});");
cl_assert_equal_b(false, rocky_global_has_event_handlers("secondchange"));
cl_assert_equal_b(false, rocky_global_has_event_handlers("minutechange"));
cl_assert_equal_b(false, rocky_global_has_event_handlers("hourchange"));
cl_assert_equal_b(true, rocky_global_has_event_handlers("daychange"));
cl_assert_equal_i(1, s_tick_timer_service_subscribe.call_count);
cl_assert_equal_i(DAY_UNIT | MONTH_UNIT | YEAR_UNIT,
s_tick_timer_service_subscribe.last_call.tick_units);
EXECUTE_SCRIPT(
"var hourHandler = function() {};\n"
"_rocky.on('hourchange', hourHandler);\n"
);
cl_assert_equal_b(false, rocky_global_has_event_handlers("secondchange"));
cl_assert_equal_b(false, rocky_global_has_event_handlers("minutechange"));
cl_assert_equal_b(true, rocky_global_has_event_handlers("hourchange"));
cl_assert_equal_b(true, rocky_global_has_event_handlers("daychange"));
cl_assert_equal_i(2, s_tick_timer_service_subscribe.call_count);
cl_assert_equal_i(HOUR_UNIT | DAY_UNIT | MONTH_UNIT | YEAR_UNIT,
s_tick_timer_service_subscribe.last_call.tick_units);
EXECUTE_SCRIPT("_rocky.on('minutechange', function() {});");
cl_assert_equal_b(false, rocky_global_has_event_handlers("secondchange"));
cl_assert_equal_b(true, rocky_global_has_event_handlers("minutechange"));
cl_assert_equal_b(true, rocky_global_has_event_handlers("hourchange"));
cl_assert_equal_b(true, rocky_global_has_event_handlers("daychange"));
cl_assert_equal_i(3, s_tick_timer_service_subscribe.call_count);
cl_assert_equal_i(MINUTE_UNIT | HOUR_UNIT | DAY_UNIT | MONTH_UNIT | YEAR_UNIT,
s_tick_timer_service_subscribe.last_call.tick_units);
// register for minute again
EXECUTE_SCRIPT("_rocky.on('minutechange', function() {});");
cl_assert_equal_b(false, rocky_global_has_event_handlers("secondchange"));
cl_assert_equal_b(true, rocky_global_has_event_handlers("minutechange"));
cl_assert_equal_b(true, rocky_global_has_event_handlers("hourchange"));
cl_assert_equal_b(true, rocky_global_has_event_handlers("daychange"));
cl_assert_equal_i(4, s_tick_timer_service_subscribe.call_count);
cl_assert_equal_i(MINUTE_UNIT | HOUR_UNIT | DAY_UNIT | MONTH_UNIT | YEAR_UNIT,
s_tick_timer_service_subscribe.last_call.tick_units);
EXECUTE_SCRIPT("_rocky.on('secondchange', function() {});");
cl_assert_equal_b(true, rocky_global_has_event_handlers("secondchange"));
cl_assert_equal_b(true, rocky_global_has_event_handlers("minutechange"));
cl_assert_equal_b(true, rocky_global_has_event_handlers("hourchange"));
cl_assert_equal_b(true, rocky_global_has_event_handlers("daychange"));
cl_assert_equal_i(5, s_tick_timer_service_subscribe.call_count);
cl_assert_equal_i(SECOND_UNIT | MINUTE_UNIT | HOUR_UNIT | DAY_UNIT | MONTH_UNIT | YEAR_UNIT,
s_tick_timer_service_subscribe.last_call.tick_units);
}
void prv_tick_handler(struct tm *tick_time, TimeUnits units_changed);
void test_rocky_api_tickservice__calls_handlers(void) {
rocky_global_init(s_api);
EXECUTE_SCRIPT(
"var s = 0;\n"
"var m = 0;\n"
"var h = 0;\n"
"var d = 0;\n"
"_rocky.on('secondchange', function(e) {s++;});"
"_rocky.on('minutechange', function(e) {m++;});"
"_rocky.on('hourchange', function(e) {h++;});"
"_rocky.on('daychange', function(e) {d++;});"
);
// subscribing already triggers a call
ASSERT_JS_GLOBAL_EQUALS_I("s", 1);
ASSERT_JS_GLOBAL_EQUALS_I("m", 1);
ASSERT_JS_GLOBAL_EQUALS_I("h", 1);
ASSERT_JS_GLOBAL_EQUALS_I("d", 1);
// all handlers will be called as year change means minute change
prv_tick_handler(NULL, YEAR_UNIT);
ASSERT_JS_GLOBAL_EQUALS_I("s", 2);
ASSERT_JS_GLOBAL_EQUALS_I("m", 2);
ASSERT_JS_GLOBAL_EQUALS_I("h", 2);
ASSERT_JS_GLOBAL_EQUALS_I("d", 2);
// same here, each time a day changes, a second changes, too
prv_tick_handler(NULL, MINUTE_UNIT | DAY_UNIT);
ASSERT_JS_GLOBAL_EQUALS_I("s", 3);
ASSERT_JS_GLOBAL_EQUALS_I("m", 3);
ASSERT_JS_GLOBAL_EQUALS_I("h", 3);
ASSERT_JS_GLOBAL_EQUALS_I("d", 3);
prv_tick_handler(NULL, HOUR_UNIT);
ASSERT_JS_GLOBAL_EQUALS_I("s", 4);
ASSERT_JS_GLOBAL_EQUALS_I("m", 4);
ASSERT_JS_GLOBAL_EQUALS_I("h", 4);
ASSERT_JS_GLOBAL_EQUALS_I("d", 3);
prv_tick_handler(NULL, MINUTE_UNIT);
ASSERT_JS_GLOBAL_EQUALS_I("s", 5);
ASSERT_JS_GLOBAL_EQUALS_I("m", 5);
ASSERT_JS_GLOBAL_EQUALS_I("h", 4);
ASSERT_JS_GLOBAL_EQUALS_I("d", 3);
prv_tick_handler(NULL, SECOND_UNIT);
ASSERT_JS_GLOBAL_EQUALS_I("s", 6);
ASSERT_JS_GLOBAL_EQUALS_I("m", 5);
ASSERT_JS_GLOBAL_EQUALS_I("h", 4);
ASSERT_JS_GLOBAL_EQUALS_I("d", 3);
}
void test_rocky_api_tickservice__event_types(void) {
rocky_global_init(s_api);
EXECUTE_SCRIPT(
"var s = null;\n"
"var m = null;\n"
"var h = null;\n"
"var d = null;\n"
"_rocky.on('secondchange', function(e) {s = e.type;});"
"_rocky.on('minutechange', function(e) {m = e.type;});"
"_rocky.on('hourchange', function(e) {h = e.type;});"
"_rocky.on('daychange', function(e) {d = e.type;});"
);
// subscribing already triggers a call
ASSERT_JS_GLOBAL_EQUALS_S("s", "secondchange");
ASSERT_JS_GLOBAL_EQUALS_S("m", "minutechange");
ASSERT_JS_GLOBAL_EQUALS_S("h", "hourchange");
ASSERT_JS_GLOBAL_EQUALS_S("d", "daychange");
}
void test_rocky_api_tickservice__error_in_handler_on_register(void) {
rocky_global_init(s_api);
s_log_internal__expected = (const char *[]){
"Unhandled exception",
" secondchange",
NULL
};
EXECUTE_SCRIPT(
"_rocky.on('secondchange', function(e) { throw e.type; });"
);
cl_assert(*s_log_internal__expected == NULL);
}
void test_rocky_api_tickservice__provides_event_date(void) {
rocky_global_init(s_api);
s_log_internal__expected = (const char *[]){ NULL };
const time_t cur_time = 1458250851; // Thu Mar 17 21:40:51 2016 UTC
// Thu Mar 17 14:40:51 2016 PDT
const uint16_t cur_millis = 123;
fake_time_init(cur_time, cur_millis);
EXECUTE_SCRIPT(
"var s = null;\n"
"var m = null;\n"
"var h = null;\n"
"var d = null;\n"
"_rocky.on('secondchange', function(e) { s = e.date.getSeconds(); });\n"
"_rocky.on('minutechange', function(e) { m = e.date.getMinutes(); });\n"
"_rocky.on('hourchange', function(e) { h = e.date.getHours(); });\n"
"_rocky.on('daychange', function(e) { d = e.date.getDate(); });\n"
);
ASSERT_JS_GLOBAL_EQUALS_D("s", 51.0);
ASSERT_JS_GLOBAL_EQUALS_D("m", 40.0);
ASSERT_JS_GLOBAL_EQUALS_D("h", 21.0);
ASSERT_JS_GLOBAL_EQUALS_D("d", 17.0);
EXECUTE_SCRIPT(
"s = null;\n"
"m = null;\n"
"h = null;\n"
"d = null;\n"
);
struct tm tm = {
.tm_sec = 1,
.tm_min = 2,
.tm_hour = 3,
.tm_mday = 4,
.tm_mon = 5,
.tm_year = 116, // 2016
};
prv_tick_handler(&tm, SECOND_UNIT | MINUTE_UNIT | HOUR_UNIT | DAY_UNIT);
ASSERT_JS_GLOBAL_EQUALS_D("s", 1.0);
ASSERT_JS_GLOBAL_EQUALS_D("m", 2.0);
ASSERT_JS_GLOBAL_EQUALS_D("h", 3.0);
ASSERT_JS_GLOBAL_EQUALS_D("d", 4.0);
cl_assert(*s_log_internal__expected == NULL);
}

View file

@ -0,0 +1,115 @@
/*
* Copyright 2024 Google 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.
*/
#include "clar.h"
#include "test_jerry_port_common.h"
#include "test_rocky_common.h"
#include "applib/rockyjs/api/rocky_api_timers.h"
#include "applib/rockyjs/pbl_jerry_port.h"
// Fakes
#include "fake_app_timer.h"
#include "fake_pbl_malloc.h"
#include "fake_time.h"
// Stubs
#include "stubs_app_manager.h"
#include "stubs_app_state.h"
#include "stubs_logging.h"
#include "stubs_passert.h"
#include "stubs_resources.h"
#include "stubs_sleep.h"
#include "stubs_serial.h"
#include "stubs_syscalls.h"
#include "stubs_sys_exit.h"
void test_rocky_api_timers__initialize(void) {
fake_pbl_malloc_clear_tracking();
fake_app_timer_init();
rocky_runtime_context_init();
jerry_init(JERRY_INIT_EMPTY);
TIMER_APIS.init();
}
void test_rocky_api_timers__cleanup(void) {
jerry_cleanup();
rocky_runtime_context_deinit();
fake_app_timer_deinit();
fake_pbl_malloc_check_net_allocs();
}
void test_rocky_api_timers__setInterval(void) {
char *script =
"var num_times = 0;"
"var extra_arg = 0;"
"var timer = setInterval(function(extra) {"
"num_times++;"
"extra_arg = extra;"
"}, 1000, 5);";
EXECUTE_SCRIPT(script);
AppTimer *timer = (AppTimer *)(uintptr_t)prv_js_global_get_double("timer");
for (double d = 0.0; d < 5.0; d += 1.0) {
ASSERT_JS_GLOBAL_EQUALS_I("num_times", d);
cl_assert(fake_app_timer_is_scheduled(timer));
cl_assert(app_timer_trigger(timer));
ASSERT_JS_GLOBAL_EQUALS_I("extra_arg", 5.0);
}
script = "clearInterval(timer);";
EXECUTE_SCRIPT(script);
cl_assert(fake_app_timer_is_scheduled(timer) == false);
}
void test_rocky_api_timers__setTimeout(void) {
char *script =
"var num_times = 0;"
"var f = function(extra) {"
" num_times++;"
"};"
"var timer = setTimeout('f()', '1000');";
EXECUTE_SCRIPT(script);
AppTimer *timer = (AppTimer *)(uintptr_t)prv_js_global_get_double("timer");
cl_assert_equal_i(fake_app_timer_get_timeout(timer), 1000);
cl_assert(fake_app_timer_is_scheduled(timer));
cl_assert(app_timer_trigger(timer));
ASSERT_JS_GLOBAL_EQUALS_I("num_times", 1.0);
// Verified timer will not trigger again
cl_assert(fake_app_timer_is_scheduled(timer) == false);
}
void test_rocky_api_timers__bogus_clearInterval(void) {
EXECUTE_SCRIPT_EXPECT_UNDEFINED("clearInterval(0)");
EXECUTE_SCRIPT_EXPECT_UNDEFINED("clearInterval(1234)");
EXECUTE_SCRIPT_EXPECT_UNDEFINED("clearInterval(-1234)");
EXECUTE_SCRIPT_EXPECT_UNDEFINED("clearInterval(undefined)");
}
void test_rocky_api_timers__bogus_clearTimeout(void) {
EXECUTE_SCRIPT_EXPECT_UNDEFINED("clearTimeout(0)");
EXECUTE_SCRIPT_EXPECT_UNDEFINED("clearTimeout(1234)");
EXECUTE_SCRIPT_EXPECT_UNDEFINED("clearTimeout(-1234)");
EXECUTE_SCRIPT_EXPECT_UNDEFINED("clearTimeout(undefined)");
}

View file

@ -0,0 +1,330 @@
/*
* Copyright 2024 Google 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.
*/
#include "test_jerry_port_common.h"
#include "test_rocky_common.h"
#include "applib/rockyjs/pbl_jerry_port.h"
#include "vendor/jerryscript/jerry-core/ecma/builtin-objects/ecma-builtin-helpers.h"
#include "applib/rockyjs/api/rocky_api_global.h"
#include <clar.h>
#include <math.h>
// Fakes
#include "fake_logging.h"
#include "fake_pbl_malloc.h"
#if EMSCRIPTEN
#include "fake_time_timeshift_js.h"
#else
#include "fake_time.h"
#endif
// Stubs
#include "stubs_app_manager.h"
#include "stubs_app_state.h"
#include "stubs_logging.h"
#include "stubs_passert.h"
#include "stubs_serial.h"
#include "stubs_sys_exit.h"
#define FUNC_NAME "f"
#define ERROR_STRING "Oops!"
T_STATIC void prv_log_uncaught_error(const jerry_value_t result);
static int s_test_func_imp_call_count;
static int s_method_func_imp_call_count;
void tick_timer_service_handle_time_change(void) {}
////////////////////////////////////////////////////////////////////////////////
// Initialization & Setup
////////////////////////////////////////////////////////////////////////////////
void test_rocky_api_util__initialize(void) {
s_test_func_imp_call_count = 0;
s_method_func_imp_call_count = 0;
rocky_runtime_context_init();
jerry_init(JERRY_INIT_EMPTY);
s_log_internal__expected = NULL;
}
void test_rocky_api_util__cleanup(void) {
jerry_cleanup();
rocky_runtime_context_deinit();
fake_pbl_malloc_check_net_allocs();
s_log_internal__expected = NULL;
}
////////////////////////////////////////////////////////////////////////////////
// Helpers for Tests
////////////////////////////////////////////////////////////////////////////////
static void prv_do_call_user_function(const char *script) {
const jerry_value_t rv = jerry_eval((jerry_char_t *)script, strlen(script),
false /* is_strict */);
cl_assert_equal_b(jerry_value_has_error_flag(rv), false);
jerry_release_value(rv);
const jerry_value_t func = JS_GLOBAL_GET_VALUE(FUNC_NAME);
rocky_util_call_user_function_and_log_uncaught_error(func, jerry_create_undefined(), NULL, 0);
jerry_release_value(func);
}
static void prv_do_eval(const char *eval_str) {
rocky_util_eval_and_log_uncaught_error((const jerry_char_t *)eval_str, strlen(eval_str));
}
////////////////////////////////////////////////////////////////////////////////
// Tests
////////////////////////////////////////////////////////////////////////////////
JERRY_FUNCTION(test_func_imp) {
++s_test_func_imp_call_count;
return jerry_create_undefined();
}
JERRY_FUNCTION(method_func_imp) {
++s_method_func_imp_call_count;
return jerry_create_undefined();
}
void test_rocky_api_util__rocky_add_constructor(void) {
static const RockyGlobalAPI *s_api[] = {
NULL,
};
rocky_global_init(s_api);
JS_VAR prototype = rocky_add_constructor("test", test_func_imp);
cl_assert_equal_b(jerry_value_is_object(prototype), true);
EXECUTE_SCRIPT("_rocky.test();");
cl_assert_equal_i(1, s_test_func_imp_call_count);
rocky_add_function(prototype, "method", method_func_imp);
EXECUTE_SCRIPT("var y = new _rocky.test(); y.method();");
cl_assert_equal_i(1, s_method_func_imp_call_count);
}
void test_rocky_api_util__error_print(void) {
s_log_internal__expected = (const char *[]){
"Unhandled Error",
" "ERROR_STRING,
NULL
};
jerry_value_t error_val =
jerry_create_error(JERRY_ERROR_COMMON, (const jerry_char_t *)ERROR_STRING);
cl_assert(jerry_value_has_error_flag(error_val));
// NOTE: prv_log_uncaught_error() will call jerry_release_value(), so don't use error_val after
// this call returns:
prv_log_uncaught_error(error_val);
cl_assert(*s_log_internal__expected == NULL);
}
void test_rocky_api_util__call_no_error(void) {
s_log_internal__expected = (const char *[]){ NULL };
const char *script = "var "FUNC_NAME" = function() { return 1; };";
prv_do_call_user_function(script);
cl_assert(*s_log_internal__expected == NULL);
}
void test_rocky_api_util__call_throw_string(void) {
s_log_internal__expected = (const char *[]){
"Unhandled exception",
" "ERROR_STRING,
NULL
};
const char *script = "var "FUNC_NAME" = function() { throw '"ERROR_STRING"'; };";
prv_do_call_user_function(script);
cl_assert(*s_log_internal__expected == NULL);
}
void test_rocky_api_util__call_throw_number(void) {
s_log_internal__expected = (const char *[]){
"Unhandled exception",
" 1",
NULL
};
const char *script = "var "FUNC_NAME" = function() { throw 1; };";
prv_do_call_user_function(script);
cl_assert(*s_log_internal__expected == NULL);
}
void test_rocky_api_util__call_throw_error(void) {
s_log_internal__expected = (const char *[]){
"Unhandled Error",
" "ERROR_STRING,
NULL
};
const char *script = "var "FUNC_NAME" = function() { throw new Error('"ERROR_STRING"'); };";
prv_do_call_user_function(script);
cl_assert(*s_log_internal__expected == NULL);
}
void test_rocky_api_util__eval_no_error(void) {
s_log_internal__expected = (const char *[]){ NULL };
prv_do_eval("1+1;");
cl_assert(*s_log_internal__expected == NULL);
}
void test_rocky_api_util__eval_throw_string(void) {
s_log_internal__expected = (const char *[]){
"Unhandled exception",
" "ERROR_STRING,
NULL
};
prv_do_eval("throw '"ERROR_STRING"';");
cl_assert(*s_log_internal__expected == NULL);
}
void test_rocky_api_util__eval_throw_error(void) {
s_log_internal__expected = (const char *[]){
"Unhandled Error",
" "ERROR_STRING,
NULL
};
prv_do_eval("throw new Error('"ERROR_STRING"');");
cl_assert(*s_log_internal__expected == NULL);
}
void test_rocky_api_util__create_date_now(void) {
const time_t cur_time = 1458250851; // Thu Mar 17 21:40:51 2016 UTC
// Thu Mar 17 14:40:51 2016 PDT
const uint16_t cur_millis = 123;
fake_time_init(cur_time, cur_millis);
jerry_value_t now = rocky_util_create_date(NULL);
jerry_value_t getSeconds = jerry_get_object_field(now, "getSeconds");
jerry_value_t getMinutes = jerry_get_object_field(now, "getMinutes");
jerry_value_t getHours = jerry_get_object_field(now, "getHours");
jerry_value_t getDate = jerry_get_object_field(now, "getDate");
jerry_value_t result_seconds = jerry_call_function(getSeconds, now, NULL, 0);
jerry_value_t result_minutes = jerry_call_function(getMinutes, now, NULL, 0);
jerry_value_t result_hours = jerry_call_function(getHours, now, NULL, 0);
jerry_value_t result_date = jerry_call_function(getDate, now, NULL, 0);
cl_assert(jerry_get_number_value(result_seconds) == 51.0);
cl_assert(jerry_get_number_value(result_minutes) == 40.0);
cl_assert(jerry_get_number_value(result_hours) == 21.0);
cl_assert(jerry_get_number_value(result_date) == 17.0);
jerry_release_value(result_date);
jerry_release_value(result_hours);
jerry_release_value(result_minutes);
jerry_release_value(result_seconds);
jerry_release_value(getDate);
jerry_release_value(getHours);
jerry_release_value(getMinutes);
jerry_release_value(getSeconds);
jerry_release_value(now);
}
void test_rocky_api_util__ecma_date_make_day(void) {
#ifdef EMSCRIPTEN
printf("Skipping test %s", __FUNCTION__);
#else
cl_assert_equal_d(16861, ecma_date_make_day(2016, 2, 1)); // JerryScript's unit-test
cl_assert_equal_d(-25294, ecma_date_make_day(1900, 9, 1)); // not a leap year!
cl_assert_equal_d(17075, ecma_date_make_day(2016, 8, 31)); // Sept-31 == Oct-01
cl_assert_equal_d(17075, ecma_date_make_day(2016, 9, 1)); // Oct-01
cl_assert_equal_d(17045, ecma_date_make_day(2016, 8, 1)); // Sept-01
#endif // EMSCRIPTEN
}
void test_rocky_api_util__ecma_date_make_day_list(void) {
#ifdef EMSCRIPTEN
printf("Skipping test %s", __FUNCTION__);
#else
int fail_count = 0;
for(int y = 1950; y < 2050; y++) {
for(int m = 0; m < 12; m++) {
for (int d = 1; d < 32; d++) {
const ecma_number_t result = ecma_date_make_day(y, m, d);
if (isnan(result)) {
printf("failed for %04d-%02d-%02d\n", y, (m + 1), d);
fail_count++;
} else {
// printf("passed for %04d-%02d-%02d: %d\n", y, (m + 1), d, (int)result);
}
}
}
}
cl_assert_equal_i(0, fail_count);
#endif // EMSCRIPTEN
}
void test_rocky_api_util__create_date_tm(void) {
const time_t cur_time = 1458250851; // Thu Mar 17 21:40:51 2016 UTC
// Thu Mar 17 14:40:51 2016 PDT
const uint16_t cur_millis = 123;
fake_time_init(cur_time, cur_millis);
struct tm tick_time = {
.tm_sec = 28,
.tm_min = 38,
.tm_hour = 18,
.tm_mday = 30,
.tm_mon = 9,
.tm_year = 116,
.tm_wday = 1,
.tm_yday = 275,
.tm_zone = "\000\000\000\000\000",
};
jerry_value_t now = rocky_util_create_date(&tick_time);
cl_assert(jerry_value_is_object(now));
jerry_value_t getSeconds = jerry_get_object_field(now, "getSeconds");
jerry_value_t getMinutes = jerry_get_object_field(now, "getMinutes");
jerry_value_t getHours = jerry_get_object_field(now, "getHours");
jerry_value_t getDate = jerry_get_object_field(now, "getDate");
jerry_value_t getMonth = jerry_get_object_field(now, "getMonth");
jerry_value_t getYear = jerry_get_object_field(now, "getYear");
jerry_value_t result_seconds = jerry_call_function(getSeconds, now, NULL, 0);
jerry_value_t result_minutes = jerry_call_function(getMinutes, now, NULL, 0);
jerry_value_t result_hours = jerry_call_function(getHours, now, NULL, 0);
jerry_value_t result_date = jerry_call_function(getDate, now, NULL, 0);
jerry_value_t result_month = jerry_call_function(getMonth, now, NULL, 0);
jerry_value_t result_year = jerry_call_function(getYear, now, NULL, 0);
cl_assert_equal_d(jerry_get_number_value(result_seconds), 28.0);
cl_assert_equal_d(jerry_get_number_value(result_minutes), 38.0);
cl_assert_equal_d(jerry_get_number_value(result_hours), 18.0);
cl_assert_equal_d(jerry_get_number_value(result_date), 30.0);
cl_assert_equal_d(jerry_get_number_value(result_month), 9.0);
cl_assert_equal_d(jerry_get_number_value(result_year), 116.0);
jerry_release_value(result_year);
jerry_release_value(result_month);
jerry_release_value(result_date);
jerry_release_value(result_hours);
jerry_release_value(result_minutes);
jerry_release_value(result_seconds);
jerry_release_value(getYear);
jerry_release_value(getMonth);
jerry_release_value(getDate);
jerry_release_value(getHours);
jerry_release_value(getMinutes);
jerry_release_value(getSeconds);
jerry_release_value(now);
}

View file

@ -0,0 +1,620 @@
/*
* Copyright 2024 Google 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.
*/
#include "clar.h"
#include "applib/rockyjs/api/rocky_api_util_args.h"
#include "applib/rockyjs/api/rocky_api_errors.h"
#include "applib/rockyjs/api/rocky_api_util.h"
#include "applib/rockyjs/pbl_jerry_port.h"
#include "test_jerry_port_common.h"
#include "test_rocky_common.h"
#include <util/size.h>
#include <limits.h>
#include <stdint.h>
// Fakes
#include "fake_pbl_malloc.h"
#include "fake_time.h"
// Stubs
#include "stubs_app_state.h"
#include "stubs_passert.h"
#include "stubs_logging.h"
#include "stubs_serial.h"
#include "stubs_sys_exit.h"
#define JERRY_ARGS_MAKE(...) \
jerry_value_t argv[] = { \
__VA_ARGS__ \
}; \
jerry_length_t argc = ARRAY_LENGTH(argv);
#define JERRY_ARGS_RELEASE() \
while(argc--) { \
jerry_release_value(argv[argc]); \
argv[argc] = 0; \
}
#define ROCKY_ARGS_ASSIGN(...) \
const RockyArgBinding bindings[] = { \
__VA_ARGS__ \
}; \
JS_VAR error_value = \
rocky_args_assign(argc, argv, bindings, ARRAY_LENGTH(bindings)); \
void test_rocky_api_util_args__initialize(void) {
rocky_runtime_context_init();
jerry_init(JERRY_INIT_EMPTY);
}
void test_rocky_api_util_args__cleanup(void) {
jerry_cleanup();
rocky_runtime_context_deinit();
fake_pbl_malloc_check_net_allocs(); // Make sure no memory was leaked
}
void test_rocky_api_util_args__missing_args(void) {
JERRY_ARGS_MAKE(/* argc == 0 */);
uint8_t v;
ROCKY_ARGS_ASSIGN(ROCKY_ARG(v));
ASSERT_JS_ERROR(error_value, "TypeError: Not enough arguments");
}
void test_rocky_api_util_args__numbers_get_rounded_when_converting_to_integer(void) {
struct {
double input;
int16_t expected_output;
} cases[] = {
{
.input = 0.5,
.expected_output = 1,
},
{
.input = -0.5,
.expected_output = -1,
},
{
.input = 0.0,
.expected_output = 0,
},
{
.input = -0.3,
.expected_output = 0,
},
};
for (int i = 0; i < ARRAY_LENGTH(cases); ++i) {
int16_t output = ~0;
jerry_value_t v = jerry_create_number(cases[i].input);
JERRY_ARGS_MAKE(v);
ROCKY_ARGS_ASSIGN(ROCKY_ARG(output));
cl_assert_equal_i(output, cases[i].expected_output);
ASSERT_JS_ERROR(error_value, NULL);
jerry_release_value(v);
}
}
void test_rocky_api_util_args__numbers(void) {
uint8_t u8;
uint16_t u16;
uint32_t u32;
uint64_t u64;
int8_t s8;
int16_t s16;
int32_t s32;
int64_t s64;
double d;
typedef struct {
long double u8;
long double u16;
long double u32;
long double u64;
long double s8;
long double s16;
long double s32;
long double s64;
double d;
char *expected_error_msg;
} NumbersTestCase;
enum {
WithinLowerBounds,
WithinUpperBounds,
UnderLowerBounds,
OverUpperBounds,
};
// FIXME: fix limits.h / stdint.h / float.h so we can use the standard defines for these values..
NumbersTestCase cases[4] = {
[WithinLowerBounds] = {
.u8 = 0.0,
.u16 = 0.0,
.u32 = 0.0,
.u64 = 0.0,
.s8 = -128.0,
.s16 = -32768.0,
.s32 = -2147483648.0,
.s64 = -9223372036854775808.0,
.d = 2.2250738585072014e-308,
.expected_error_msg = NULL,
},
[WithinUpperBounds] = {
.u8 = 255.0,
.u16 = 65535.0,
.u32 = 4294967295.0,
.u64 = 9223372036854775807.0,
.s8 = 127.0,
.s16 = 32767.0,
.s32 = 2147483647.0,
.s64 = 9223372036854775807.0,
.d = 1.7976931348623157e+308,
.expected_error_msg = NULL,
},
};
const long double margin = 0.001;
cases[UnderLowerBounds] = (NumbersTestCase) {
.u8 = cases[WithinLowerBounds].u8 - margin,
.u16 = cases[WithinLowerBounds].u16 - margin,
.u32 = cases[WithinLowerBounds].u32 - margin,
.u64 = cases[WithinLowerBounds].u64 - margin,
.s8 = cases[WithinLowerBounds].s8 - margin,
.s16 = cases[WithinLowerBounds].s16 - margin,
.s32 = cases[WithinLowerBounds].s32 - margin,
.s64 = cases[WithinLowerBounds].s64 - margin,
.d = cases[WithinLowerBounds].d - margin,
.expected_error_msg =
"TypeError: Argument at index 0 is invalid: Value out of bounds for native type",
};
cases[OverUpperBounds] = (NumbersTestCase) {
.u8 = cases[WithinUpperBounds].u8 + margin,
.u16 = cases[WithinUpperBounds].u16 + margin,
.u32 = cases[WithinUpperBounds].u32 + margin,
.u64 = cases[WithinUpperBounds].u64 + margin,
.s8 = cases[WithinUpperBounds].s8 + margin,
.s16 = cases[WithinUpperBounds].s16 + margin,
.s32 = cases[WithinUpperBounds].s32 + margin,
.s64 = cases[WithinUpperBounds].s64 + margin,
.d = cases[WithinUpperBounds].d - margin,
.expected_error_msg =
"TypeError: Argument at index 0 is invalid: Value out of bounds for native type",
};
for (int i = 0; i < ARRAY_LENGTH(cases); ++i) {
NumbersTestCase *c = &cases[i];
// Initialize to something that's not the expected value:
u8 = 1 + (uint8_t)c->u8;
u16 = 1 + (uint16_t)c->u16;
u32 = 1 + (uint16_t)c->u32;
u64 = 1 + (uint16_t)c->u64;
s8 = 1 + (int8_t)c->s8;
s16 = 1 + (int16_t)c->s16;
s32 = 1 + (int16_t)c->s32;
s64 = 1 + (int16_t)c->s64;
d = 1 + (double)c->d;
JERRY_ARGS_MAKE(
jerry_create_number(c->u8),
jerry_create_number(c->u16),
jerry_create_number(c->u32),
jerry_create_number(c->u64),
jerry_create_number(c->s8),
jerry_create_number(c->s16),
jerry_create_number(c->s32),
jerry_create_number(c->s64),
jerry_create_number(c->d),
);
ROCKY_ARGS_ASSIGN(
ROCKY_ARG(u8),
ROCKY_ARG(u16),
ROCKY_ARG(u32),
ROCKY_ARG(u64),
ROCKY_ARG(s8),
ROCKY_ARG(s16),
ROCKY_ARG(s32),
ROCKY_ARG(s64),
ROCKY_ARG(d),
);
ASSERT_JS_ERROR(error_value, c->expected_error_msg);
if (!c->expected_error_msg) {
cl_assert_equal_i(u8, (uint8_t)c->u8);
cl_assert_equal_i(u16, (uint16_t)c->u16);
cl_assert_equal_i(u32, (uint32_t)c->u32);
cl_assert_equal_i(u64, (uint64_t)c->u64);
cl_assert_equal_i(s8, (int8_t)c->s8);
cl_assert_equal_i(s16, (int16_t)c->s16);
cl_assert_equal_i(s32, (int32_t)c->s32);
cl_assert_equal_i(s64, (int64_t)c->s64);
cl_assert_equal_d(d, (double)c->d);
}
JERRY_ARGS_RELEASE();
}
}
void test_rocky_api_util_args__number_type_mismatch(void) {
jerry_value_t mismatch_args[] = {
jerry_create_null(),
jerry_create_string((const jerry_char_t *)"one"),
jerry_create_string((const jerry_char_t *)"1"),
jerry_create_array(1),
jerry_create_boolean(true),
jerry_create_object(),
};
for (int i = 0; i < ARRAY_LENGTH(mismatch_args); ++i) {
jerry_value_t arg = mismatch_args[i];
JERRY_ARGS_MAKE(arg);
for (RockyArgType type = RockyArgTypeUInt8; type <= RockyArgTypeDouble; ++type) {
uint8_t buffer_untouched[8]; // The type check fails, so nothing is supposed to be written.
ROCKY_ARGS_ASSIGN(
ROCKY_ARG_MAKE(buffer_untouched, type, {}),
);
ASSERT_JS_ERROR(error_value, "TypeError: Argument at index 0 is not a Number");
}
jerry_release_value(arg);
mismatch_args[i] = 0;
}
}
static jerry_value_t prv_dummy(const jerry_value_t function_obj_p,
const jerry_value_t this_val,
const jerry_value_t args_p[],
const jerry_length_t args_count) {
return 0;
}
void test_rocky_api_util_args__boolean(void) {
// No API to create NaN :(
char *nan_script = "Number.NaN";
jerry_value_t nan = jerry_eval((jerry_char_t *)nan_script,
strlen(nan_script),
false /* is_strict */);
struct {
jerry_value_t input;
bool expected_output;
} cases[] = {
// Falsy: false, 0, "", null, undefined, and NaN:
{
.input = jerry_create_boolean(false),
.expected_output = false,
},
{
.input = jerry_create_number(0.0),
.expected_output = false,
},
{
.input = jerry_create_string((const jerry_char_t *)""),
.expected_output = false,
},
{
.input = jerry_create_null(),
.expected_output = false,
},
{
.input = jerry_create_undefined(),
.expected_output = false,
},
{
.input = nan,
.expected_output = false,
},
// Truthy values:
{
.input = jerry_create_boolean(true),
.expected_output = true,
},
{
.input = jerry_create_number(1.0),
.expected_output = true,
},
{
.input = jerry_create_string((const jerry_char_t *)" "),
.expected_output = true,
},
{
.input = jerry_create_array(0),
.expected_output = true,
},
{
.input = jerry_create_object(),
.expected_output = true,
},
{
.input = jerry_create_external_function(prv_dummy),
.expected_output = true,
},
};
for (int i = 0; i < ARRAY_LENGTH(cases); ++i) {
bool output = !cases[i].expected_output;
jerry_value_t input = cases[i].input;
JERRY_ARGS_MAKE(input);
ROCKY_ARGS_ASSIGN(ROCKY_ARG(output));
cl_assert_equal_b(output, cases[i].expected_output);
ASSERT_JS_ERROR(error_value, NULL /* Never errors out! */);
jerry_release_value(input);
cases[i].input = 0;
}
}
void test_rocky_api_util_args__string(void) {
struct {
jerry_value_t input;
char *expected_output;
} cases[] = {
{
.input = jerry_create_boolean(false),
.expected_output = "false",
},
{
.input = jerry_create_number(0.0),
.expected_output = "0",
},
{
.input = jerry_create_number(1.234e+60),
.expected_output = "1.234e+60",
},
{
.input = jerry_create_string((const jerry_char_t *)""),
.expected_output = "",
},
{
.input = jerry_create_string((const jerry_char_t *)"js"),
.expected_output = "js",
},
{
.input = jerry_create_null(),
.expected_output = "null",
},
{
.input = jerry_create_undefined(),
.expected_output = "undefined",
},
{
.input = jerry_create_array(0),
.expected_output = "", // Kinda weird?
},
{
.input = jerry_create_object(),
.expected_output = "[object Object]",
},
};
for (int i = 0; i < ARRAY_LENGTH(cases); ++i) {
jerry_value_t input = cases[i].input;
JERRY_ARGS_MAKE(input);
// Exercise ROCKY_ARG (automatic binding creation, defaults to malloc'd string for char *):
{
char *output = NULL;
ROCKY_ARGS_ASSIGN(ROCKY_ARG(output));
cl_assert_equal_s(output, cases[i].expected_output);
ASSERT_JS_ERROR(error_value, NULL /* Never errors out! */);
task_free(output);
}
// Exercise ROCKY_ARG (automatic binding creation, defaults to copy/no-malloc for char[]):
{
char output[16];
ROCKY_ARGS_ASSIGN(ROCKY_ARG(output));
cl_assert_equal_s(output, cases[i].expected_output);
ASSERT_JS_ERROR(error_value, NULL /* Never errors out! */);
}
// Exercise ROCKY_ARG_STR (no malloc, explicit binding creation):
{
char output[16];
ROCKY_ARGS_ASSIGN(ROCKY_ARG_STR(output, sizeof(output)));
cl_assert_equal_s(output, cases[i].expected_output);
ASSERT_JS_ERROR(error_value, NULL /* Never errors out! */);
}
// Exercise ROCKY_ARG_STR (too small buffer provided):
{
char output[1] = {0xff};
ROCKY_ARGS_ASSIGN(ROCKY_ARG_STR(output, 0));
cl_assert_equal_s(output, "");
ASSERT_JS_ERROR(error_value, NULL /* Never errors out! */);
}
jerry_release_value(input);
cases[i].input = 0;
}
}
#define PP(_x, _y, _w, _h) { \
.origin.x.raw_value = (_x), \
.origin.y.raw_value = (_y), \
.size.w.raw_value = (_w), \
.size.h.raw_value = (_h), \
}
void test_rocky_api_util_args__grect_precise(void) {
struct {
jerry_value_t argv[5];
size_t argc;
GRectPrecise expected_output;
char *error_msg;
} cases[] = {
{
.argv = {
[0] = jerry_create_number(0.0),
[1] = jerry_create_number(0.0),
[2] = jerry_create_number(0.0),
[3] = jerry_create_number(0.0),
},
.argc = 4,
.expected_output = PP(0, 0, 0, 0),
},
{
.argv = {
[0] = jerry_create_number(-0.5),
[1] = jerry_create_number(-0.2),
[2] = jerry_create_number(0.3),
[3] = jerry_create_number(0.5),
},
.argc = 4,
.expected_output = PP(-4, -2, 2, 4),
},
{
.argv = {
[0] = jerry_create_number(-4096.0),
[1] = jerry_create_number(-4096.0),
[2] = jerry_create_number(4095.875),
[3] = jerry_create_number(4095.875),
},
.argc = 4,
.expected_output = PP(-32768, -32768, 32767, 32767),
},
{
.argv = {
[0] = jerry_create_number(0),
[1] = jerry_create_number(0),
[2] = jerry_create_number(0),
[3] = jerry_create_number(4096.0),
},
.argc = 4,
.error_msg = "TypeError: Argument at index 3 is invalid: Value out of bounds for native type",
},
{
.argv = {
[0] = jerry_create_number(0),
[1] = jerry_create_number(0),
[2] = jerry_create_number(0),
},
.argc = 3,
.error_msg = "TypeError: Not enough arguments",
},
{
.argv = {
[0] = jerry_create_number(0),
[1] = jerry_create_number(0),
[2] = jerry_create_number(0),
[3] = jerry_create_null(),
},
.argc = 4,
.error_msg = "TypeError: Argument at index 3 is not a Number",
},
{
.argv = {
[0] = jerry_create_number(0),
[1] = jerry_create_number(0),
[2] = jerry_create_number(0),
[3] = jerry_create_string((const jerry_char_t *)"123"),
},
.argc = 4,
.error_msg = "TypeError: Argument at index 3 is not a Number",
},
};
for (int i = 0; i < ARRAY_LENGTH(cases); ++i) {
jerry_value_t *argv = cases[i].argv;
jerry_length_t argc = cases[i].argc;
GRectPrecise output;
memset(&output, 0x55, sizeof(output));
ROCKY_ARGS_ASSIGN(ROCKY_ARG(output));
ASSERT_JS_ERROR(error_value, cases[i].error_msg);
if (!cases[i].error_msg) {
cl_assert_equal_i(output.origin.x.raw_value, cases[i].expected_output.origin.x.raw_value);
cl_assert_equal_i(output.origin.y.raw_value, cases[i].expected_output.origin.y.raw_value);
cl_assert_equal_i(output.size.w.raw_value, cases[i].expected_output.size.w.raw_value);
cl_assert_equal_i(output.size.h.raw_value, cases[i].expected_output.size.h.raw_value);
}
for (uint32_t j = 0; j < argc; ++j) {
jerry_release_value(argv[j]);
}
}
}
void test_rocky_api_util_args__gcolor(void) {
const char *type_error_msg =
"TypeError: Argument at index 0 is not a String ('color name' or '#hex') or Number";
const char *invalid_value_msg =
"TypeError: Argument at index 0 is invalid: " \
"Expecting String ('color name' or '#hex') or Number";
struct {
jerry_value_t input;
GColor expected_output;
const char *error_msg;
} cases[] = {
{
.input = jerry_create_number(0.0),
.expected_output = {
.argb = 0,
},
},
{
.input = jerry_create_number(GColorJaegerGreenARGB8),
.expected_output = {
.argb = GColorJaegerGreenARGB8,
}
},
{
.input = jerry_create_string((const jerry_char_t *)"red"),
.expected_output = {
.r = 0b11,
.g = 0,
.b = 0,
.a = 0b11,
}
},
{
.input = jerry_create_string((const jerry_char_t *)"unknown-color"),
.error_msg = invalid_value_msg,
},
{
.input = jerry_create_null(),
.error_msg = type_error_msg,
},
};
for (int i = 0; i < ARRAY_LENGTH(cases); ++i) {
jerry_value_t input = cases[i].input;
JERRY_ARGS_MAKE(input);
GColor output;
memset(&output, 0x55, sizeof(output));
ROCKY_ARGS_ASSIGN(ROCKY_ARG(output));
ASSERT_JS_ERROR(error_value, cases[i].error_msg);
if (!cases[i].error_msg) {
cl_assert_equal_i(output.a, cases[i].expected_output.a);
cl_assert_equal_i(output.r, cases[i].expected_output.r);
cl_assert_equal_i(output.g, cases[i].expected_output.g);
cl_assert_equal_i(output.b, cases[i].expected_output.b);
}
jerry_release_value(cases[i].input);
cases[i].input = 0;
}
}

View file

@ -0,0 +1,158 @@
/*
* Copyright 2024 Google 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.
*/
#include "clar.h"
#include "test_jerry_port_common.h"
#include "test_rocky_common.h"
#include "applib/rockyjs/api/rocky_api_global.h"
#include "applib/rockyjs/api/rocky_api_watchinfo.h"
#include "applib/rockyjs/pbl_jerry_port.h"
#include "applib/app_watch_info.h"
#include "system/version.h"
#include <string.h>
// Fakes
#include "fake_app_timer.h"
#include "fake_time.h"
// Stubs
#include "stubs_app_state.h"
#include "stubs_logging.h"
#include "stubs_passert.h"
#include "stubs_pbl_malloc.h"
#include "stubs_serial.h"
#include "stubs_sys_exit.h"
////////////////////////////////////////////////////////////////////////////////
// Fakes / Stubs
////////////////////////////////////////////////////////////////////////////////
#define TEST_LOCALE "test_locale"
#define VERSION_PREFIX "v4.0"
#define VERSION_SUFFIX "beta5"
#define VERSION_TAG VERSION_PREFIX"-"VERSION_SUFFIX
#define VERSION_MAJOR 4
#define VERSION_MINOR 0
#define VERSION_PATCH 122
char *app_get_system_locale(void) {
return TEST_LOCALE;
}
bool version_copy_running_fw_metadata(FirmwareMetadata *out_metadata) {
strncpy(out_metadata->version_tag, VERSION_TAG, FW_METADATA_VERSION_TAG_BYTES);
return true;
}
WatchInfoVersion watch_info_get_firmware_version(void) {
return (WatchInfoVersion) {
.major = VERSION_MAJOR,
.minor = VERSION_MINOR,
.patch = VERSION_PATCH
};
}
static WatchInfoColor s_watch_info_color;
WatchInfoColor sys_watch_info_get_color(void) {
return s_watch_info_color;
}
static WatchInfoModel s_watch_info_model;
WatchInfoModel watch_info_get_model(void) {
return s_watch_info_model;
}
static PlatformType s_current_app_sdk_platform;
PlatformType sys_get_current_app_sdk_platform(void) {
return s_current_app_sdk_platform;
}
static const RockyGlobalAPI *s_watchinfo_api[] = {
&WATCHINFO_APIS,
NULL,
};
void test_rocky_api_watchinfo__initialize(void) {
rocky_runtime_context_init();
jerry_init(JERRY_INIT_EMPTY);
s_watch_info_model = WATCH_INFO_MODEL_PEBBLE_TIME_STEEL;
s_watch_info_color = WATCH_INFO_COLOR_TIME_STEEL_GOLD;
s_current_app_sdk_platform = PlatformTypeBasalt;
}
void test_rocky_api_watchinfo__cleanup(void) {
if (app_state_get_rocky_runtime_context() != NULL) {
jerry_cleanup();
rocky_runtime_context_deinit();
}
}
void test_rocky_api_watchinfo__model(void) {
rocky_global_init(s_watchinfo_api);
EXECUTE_SCRIPT("var model = _rocky.watchInfo.model");
ASSERT_JS_GLOBAL_EQUALS_S("model", "pebble_time_steel_gold");
}
void test_rocky_api_watchinfo__qemu_model(void) {
s_watch_info_color = -1;
rocky_global_init(s_watchinfo_api);
EXECUTE_SCRIPT("var model = _rocky.watchInfo.model");
ASSERT_JS_GLOBAL_EQUALS_S("model", "qemu_platform_basalt");
}
void test_rocky_api_watchinfo__language(void) {
rocky_global_init(s_watchinfo_api);
EXECUTE_SCRIPT("var language = _rocky.watchInfo.language");
ASSERT_JS_GLOBAL_EQUALS_S("language", TEST_LOCALE);
}
void test_rocky_api_watchinfo__platform(void) {
rocky_global_init(s_watchinfo_api);
EXECUTE_SCRIPT("var platform = _rocky.watchInfo.platform");
ASSERT_JS_GLOBAL_EQUALS_S("platform", "basalt");
}
void test_rocky_api_watchinfo__platform_unknown(void) {
s_current_app_sdk_platform = -1; // Some unknown / invalid
rocky_global_init(s_watchinfo_api);
EXECUTE_SCRIPT("var platform = _rocky.watchInfo.platform");
ASSERT_JS_GLOBAL_EQUALS_S("platform", "unknown");
}
void test_rocky_api_watchinfo__fw_version(void) {
rocky_global_init(s_watchinfo_api);
EXECUTE_SCRIPT("var major = _rocky.watchInfo.firmware.major");
ASSERT_JS_GLOBAL_EQUALS_I("major", VERSION_MAJOR);
EXECUTE_SCRIPT("var minor = _rocky.watchInfo.firmware.minor");
ASSERT_JS_GLOBAL_EQUALS_I("minor", VERSION_MINOR);
EXECUTE_SCRIPT("var patch = _rocky.watchInfo.firmware.patch");
ASSERT_JS_GLOBAL_EQUALS_I("patch", VERSION_PATCH);
EXECUTE_SCRIPT("var suffix = _rocky.watchInfo.firmware.suffix");
ASSERT_JS_GLOBAL_EQUALS_S("suffix", VERSION_SUFFIX);
}

View file

@ -0,0 +1,296 @@
/*
* Copyright 2024 Google 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.
*/
#pragma once
#include "applib/graphics/gtypes.h"
#include "applib/graphics/gcontext.h"
#include "applib/graphics/graphics_circle.h"
#include "applib/rockyjs/api/rocky_api_util.h"
#include "applib/tick_timer_service.h"
#include "applib/ui/layer.h"
#include "kernel/events.h"
#include "syscall/syscall.h"
#include <clar.h>
#include <util/attributes.h>
#include <string.h>
#define ASSERT_JS_GLOBAL_EQUALS_B(name, value) \
cl_assert_equal_i(prv_js_global_get_boolean(name), value);
#define ASSERT_JS_GLOBAL_EQUALS_I(name, value) \
cl_assert_equal_i(prv_js_global_get_double(name), value);
#define ASSERT_JS_GLOBAL_EQUALS_D(name, value) \
cl_assert_equal_d(prv_js_global_get_double(name), value);
#define ASSERT_JS_GLOBAL_EQUALS_S(name, value) \
do { \
char str_buffer[1024] = {}; \
prv_js_global_get_string(name, str_buffer, sizeof(str_buffer)); \
cl_assert_equal_s(str_buffer, value); \
} while(0)
#define JS_GLOBAL_GET_VALUE(name) \
prv_js_global_get_value(name)
#define ASSERT_JS_ERROR(error_value, expected_error_string) \
do { \
const bool expects_error = (expected_error_string) != NULL; \
const bool is_error = jerry_value_has_error_flag(error_value); \
if (is_error) { \
if (expects_error) { \
jerry_char_t buffer[100] = {0}; \
jerry_object_to_string_to_utf8_char_buffer(error_value, buffer, sizeof(buffer)); \
cl_assert_equal_s((char *)(expected_error_string), (char *)buffer); \
} else {\
rocky_log_exception("ASSERT_JS_ERROR", error_value); \
cl_fail("Error value while no error was expected!"); \
} \
} else { \
if (expects_error) { \
cl_fail("expected error during JS execution did not occur"); \
} \
} \
} while(0)
#define EXECUTE_SCRIPT_EXPECT_UNDEFINED(script) \
do { \
JS_VAR rv = jerry_eval((jerry_char_t *)script, \
strlen(script), \
false /* is_strict */); \
cl_assert_equal_b(true, jerry_value_is_undefined(rv)); \
} while(0)
#define EXECUTE_SCRIPT_EXPECT_ERROR(script, expected_error) \
do { \
JS_VAR rv = jerry_eval((jerry_char_t *)script, \
strlen(script), \
false /* is_strict */); \
ASSERT_JS_ERROR(rv, expected_error); \
} while(0)
#define EXECUTE_SCRIPT(script) EXECUTE_SCRIPT_EXPECT_ERROR((script), NULL)
#define EXECUTE_SCRIPT_AND_ASSERT_RV_EQUALS_S(script, expected_c_string) \
do { \
JS_VAR rv = jerry_eval((jerry_char_t *)script, \
strlen(script), false /* is_strict */); \
ASSERT_JS_ERROR(rv, NULL); \
JS_VAR rv_string = jerry_value_to_string(rv); \
jerry_size_t sz = jerry_get_utf8_string_size(rv_string); \
cl_assert(sz); \
jerry_char_t *buffer = malloc(sz + 1); \
memset(buffer, 0, sz + 1); \
cl_assert(buffer); \
cl_assert_equal_i(sz, jerry_string_to_utf8_char_buffer(rv_string, buffer, sz)); \
cl_assert_equal_s((char *)buffer, expected_c_string); \
free(buffer); \
} while(0)
#ifndef DO_NOT_STUB_LEGACY2
UNUSED bool process_manager_compiled_with_legacy2_sdk(void) {
return false;
}
#endif
UNUSED static jerry_value_t prv_js_global_get_value(char *name) {
JS_VAR global_obj = jerry_get_global_object();
cl_assert_equal_b(jerry_value_is_undefined(global_obj), false);
JS_VAR val = jerry_get_object_field(global_obj, name);
cl_assert_equal_b(jerry_value_is_undefined(val), false);
return jerry_acquire_value(val);
}
UNUSED static bool prv_js_global_get_boolean(char *name) {
JS_VAR val = prv_js_global_get_value(name);
cl_assert(jerry_value_is_boolean(val));
double rv = jerry_get_boolean_value(val);
return rv;
}
UNUSED static double prv_js_global_get_double(char *name) {
JS_VAR val = prv_js_global_get_value(name);
cl_assert(jerry_value_is_number(val));
double rv = jerry_get_number_value(val);
return rv;
}
UNUSED static void prv_js_global_get_string(char *name, char *buffer, size_t buffer_size) {
JS_VAR val = prv_js_global_get_value(name);
cl_assert(jerry_value_is_string(val));
ssize_t num_bytes = jerry_string_to_char_buffer(val, (jerry_char_t *)buffer, buffer_size);
cl_assert(num_bytes <= buffer_size);
}
void (*s_app_event_loop_callback)(void);
void app_event_loop_common(void) {
if (s_app_event_loop_callback) {
s_app_event_loop_callback();
}
}
#define cl_assert_equal_point(a, b) \
do { \
const GPoint __pt_a = (a); \
const GPoint __pt_b = (b); \
cl_assert_equal_i(__pt_a.x, __pt_b.x); \
cl_assert_equal_i(__pt_a.y, __pt_b.y); \
} while(0)
#define cl_assert_equal_point_precise(a, b) \
do { \
const GPointPrecise __pt_a = (a); \
const GPointPrecise __pt_b = (b); \
cl_assert_equal_i(__pt_a.x.raw_value, __pt_b.x.raw_value); \
cl_assert_equal_i(__pt_a.y.raw_value, __pt_b.y.raw_value); \
} while(0)
#define cl_assert_equal_vector_precise(a, b) \
do { \
const GVectorPrecise __a = (a); \
const GVectorPrecise __b = (b); \
cl_assert_equal_i(__a.dx.raw_value, __b.dx.raw_value); \
cl_assert_equal_i(__a.dy.raw_value, __b.dy.raw_value); \
} while(0)
#define cl_assert_equal_size(a, b) \
do { \
const GSize __sz_a = (a); \
const GSize __sz_b = (b); \
cl_assert_equal_i(__sz_a.w, __sz_b.w); \
cl_assert_equal_i(__sz_a.h, __sz_b.h); \
} while(0)
#define cl_assert_equal_size_precise(a, b) \
do { \
const GSizePrecise __sz_a = (a); \
const GSizePrecise __sz_b = (b); \
cl_assert_equal_i(__sz_a.w.raw_value, __sz_b.w.raw_value); \
cl_assert_equal_i(__sz_a.h.raw_value, __sz_b.h.raw_value); \
} while(0)
#define cl_assert_equal_rect(a, b) \
do { \
const GRect __a = (a); \
const GRect __b = (b); \
cl_assert_equal_point(__a.origin, __b.origin); \
cl_assert_equal_size(__a.size, __b.size); \
} while(0)
#define cl_assert_equal_rect_precise(a, b) \
do { \
const GRectPrecise __a = (a); \
const GRectPrecise __b = (b); \
cl_assert_equal_point_precise(__a.origin, __b.origin); \
cl_assert_equal_size_precise(__a.size, __b.size); \
} while(0)
typedef struct {
union {
Layer *layer;
GContext *ctx;
};
union {
GColor color;
uint8_t width;
struct {
GPoint p0;
GPoint p1;
};
struct {
GPointPrecise pp0;
GPointPrecise pp1;
};
struct {
GPointPrecise center;
Fixed_S16_3 radius;
int32_t angle_start;
int32_t angle_end;
} draw_arc;
struct {
GRect rect;
uint16_t radius;
GCornerMask corner_mask;
};
struct {
GRectPrecise prect;
};
TimeUnits tick_units;
const char *font_key;
struct {
char text[200];
GRect box;
GColor color;
} draw_text;
struct {
char text[200];
GFont font;
GRect box;
GTextOverflowMode overflow_mode;
GTextAlignment alignment;
} max_used_size;
struct {
GPoint points[200];
size_t num_points;
} path;
struct {
GPointPrecise center;
Fixed_S16_3 radius_inner;
Fixed_S16_3 radius_outer;
int32_t angle_start;
int32_t angle_end;
} fill_radial_precise;
};
} MockCallRecording;
typedef struct {
int call_count;
MockCallRecording last_call;
} MockCallRecordings;
#define record_mock_call(var) \
var.call_count++; \
var.last_call = (MockCallRecording)
// Handy for poking at .js things when debugging a unit test with gdb, for example:
// (gdb) call js_eval("1 + 1")
// 2
// (gdb) call js_eval("Date()")
// Thu Jan 01 1970 00:00:00 GMT+00:00
void js_eval(const char *src) {
JS_VAR rv = jerry_eval((const jerry_char_t *)src, strlen(src), false);
char buf[256] = {};
jerry_object_to_string_to_utf8_char_buffer(rv, (jerry_char_t *)buf, sizeof(buf));
printf("%s\n", buf);
}
static void (*s_process_manager_callback)(void *data);
static void *s_process_manager_callback_data;
void sys_current_process_schedule_callback(CallbackEventCallback async_cb,
void *ctx) {
s_process_manager_callback = async_cb;
s_process_manager_callback_data = ctx;
}

View file

@ -0,0 +1,121 @@
/*
* Copyright 2024 Google 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.
*/
#include "clar.h"
#include "test_jerry_port_common.h"
#include "applib/rockyjs/rocky.h"
#include "applib/rockyjs/rocky_res.h"
// Fakes
#include "fake_app_timer.h"
#include "fake_time.h"
// Stubs
#include "stubs_app_manager.h"
#include "stubs_app_state.h"
#include "stubs_logging.h"
#include "stubs_passert.h"
#include "stubs_pbl_malloc.h"
#include "stubs_sleep.h"
#include "stubs_serial.h"
#include "stubs_syscalls.h"
#include "stubs_sys_exit.h"
// instead of including internal jerry script headers here and pulling the whole dependency we will
// duplicate this value here instead
// if this fails, duplicate value from src/fw/vendor/jerryscript/jerry-core/jerry-snapshot.h
#define JERRY_SNAPSHOT_VERSION (6u)
void app_event_loop_common(void) {}
bool sys_get_current_app_is_rocky_app(void) {
return true;
}
size_t heap_bytes_free(void) {
return 123456;
}
static uint32_t s_resource_storage_get_num_entries__result;
uint32_t resource_storage_get_num_entries(ResAppNum app_num, uint32_t resource_id) {
return s_resource_storage_get_num_entries__result;
}
void rocky_api_watchface_init(void){}
void rocky_api_deinit(void){}
size_t resource_size(ResAppNum app_num, uint32_t id) {
return 0;
}
bool resource_is_valid(ResAppNum app_num, uint32_t resource_id) {
return true;
}
int process_metadata_get_res_bank_num(const PebbleProcessMd *md) {
return 123;
}
size_t resource_load_byte_range_system(ResAppNum app_num, uint32_t id, uint32_t start_offset, uint8_t *data, size_t num_bytes) {
const size_t some_bytes = 20; // a real snapshot is larger than just the header
// in our test setup, we will treat resource
// 10 as an invalid snapshot header, and
// 20 as a valid one
switch (id) {
case 10: {
cl_assert(num_bytes >= sizeof(RockySnapshotHeader));
*(RockySnapshotHeader*)data = (RockySnapshotHeader) {
.version = 123, // invalid
};
return sizeof(RockySnapshotHeader);
}
case 20: {
const size_t result = sizeof(RockySnapshotHeader) + sizeof(uint64_t);
cl_assert(num_bytes >= result);
RockySnapshotHeader *header = (RockySnapshotHeader*)data;
*header = ROCKY_EXPECTED_SNAPSHOT_HEADER;
// first uint64_t after our header is the jerry script buffer which starts with a version
*(uint64_t*)(header + 1) = JERRY_SNAPSHOT_VERSION;
return result;
}
default:
return 0;
}
}
void test_rocky_res__initialize(void) {
s_resource_storage_get_num_entries__result = 0;
}
void test_rocky_res__no_snapshot(void) {
s_resource_storage_get_num_entries__result = 5;
cl_assert_equal_b(false, rocky_app_has_compatible_bytecode_res(123));
}
void test_rocky_res__only_invalid_snapshot(void) {
s_resource_storage_get_num_entries__result = 15;
cl_assert_equal_b(false, rocky_app_has_compatible_bytecode_res(123));
}
void test_rocky_res__valid_snapshot(void) {
s_resource_storage_get_num_entries__result = 25;
cl_assert_equal_b(true, rocky_app_has_compatible_bytecode_res(123));
}

View file

@ -0,0 +1,220 @@
/*
* Copyright 2024 Google 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.
*/
#include "test_jerry_port_common.h"
#include "test_rocky_common.h"
#include "applib/rockyjs/pbl_jerry_port.h"
#include "jerry-api.h"
#include <util/size.h>
#include <clar.h>
#include <stdio.h>
// Fakes
#include "fake_time.h"
#include "fake_logging.h"
#include "fake_pbl_malloc.h"
// Stubs
#include "stubs_app_state.h"
#include "stubs_logging.h"
#include "stubs_passert.h"
// Great read-up on JavaScript and its text encoding quirks:
// https://mathiasbynens.be/notes/javascript-unicode
void test_rocky_text_encoding__initialize(void) {
fake_pbl_malloc_clear_tracking();
rocky_runtime_context_init();
jerry_init(JERRY_INIT_EMPTY);
}
void test_rocky_text_encoding__cleanup(void) {
jerry_cleanup();
rocky_runtime_context_deinit();
fake_pbl_malloc_check_net_allocs();
}
void test_rocky_text_encoding__jerry_handles_cesu8_strings_in_source(void) {
// Although CESU-8 and UTF-8 are not compatible on paper, JerryScript's lexer doesn't mind if
// we feed it CESU-8 encoded strings... Test this, so we know when this changes in the future:
EXECUTE_SCRIPT("var pileOfPooCESU8 = '\xed\xa0\xbd\xed\xb2\xa9';");
// Expect a pair of surrogate code points:
EXECUTE_SCRIPT_AND_ASSERT_RV_EQUALS_S("pileOfPooCESU8.charCodeAt(0).toString(16)", "d83d");
EXECUTE_SCRIPT_AND_ASSERT_RV_EQUALS_S("pileOfPooCESU8.charCodeAt(1).toString(16)", "dca9");
}
void test_rocky_text_encoding__jerry_handles_utf8_strings_in_source(void) {
// Source is be UTF-8 encoded.
// Have a string variable with Pile of Poo (💩) or U+1F4A9 in it, encoded using 4-bytes:
EXECUTE_SCRIPT("var pileOfPooUTF8 = '\xF0\x9F\x92\xA9';");
// Expect a pair of surrogate code points:
EXECUTE_SCRIPT_AND_ASSERT_RV_EQUALS_S("pileOfPooUTF8.charCodeAt(0).toString(16)", "d83d");
EXECUTE_SCRIPT_AND_ASSERT_RV_EQUALS_S("pileOfPooUTF8.charCodeAt(1).toString(16)", "dca9");
}
void test_rocky_text_encoding__jerry_asserts_utf8_non_bmp_codepoint_in_identifier(void) {
// It's forbidden to have an identifier contain a non-BMP codepoint (UTF-8 encoded):
EXECUTE_SCRIPT_EXPECT_ERROR("var poo\xF0\x9F\x92\xA9poo = 'pileOfPoo';",
"SyntaxError: Invalid (unexpected) character. [line: 1, column: 8]");
}
void test_rocky_text_encoding__jerry_asserts_cesu8_non_bmp_codepoint_in_identifier(void) {
// It's forbidden to have an identifier contain a non-BMP codepoint (CESU-8 encoded):
EXECUTE_SCRIPT_EXPECT_ERROR("var poo\xed\xa0\xbd\xed\xb2\xa9poo = 'pileOfPoo';",
"SyntaxError: Invalid (unexpected) character. [line: 1, column: 8]");
}
void test_rocky_text_encoding__string_length(void) {
EXECUTE_SCRIPT("var pileOfPooUTF8 = '\xF0\x9F\x92\xA9';");
// String.length is expected to count the surrogate code points that make up a non-BMP codepoint:
EXECUTE_SCRIPT_AND_ASSERT_RV_EQUALS_S("pileOfPooUTF8.length.toString()", "2");
}
void test_rocky_text_encoding__jerry_cesu8_to_utf8_conversion(void) {
struct {
const char *const script;
size_t expected_utf_size;
const char *const expected_utf_data;
} cases[] = {
[0] = {
.script = "var str = '\\uDCA9';", // low surrogate only
.expected_utf_size = 0,
},
[1] = {
.script = "var str = '\\uD83D';", // high surrogate only
.expected_utf_size = 0,
},
[2] = {
.script = "var str = '\\uDCA9\\uD83D';", // reversed order
.expected_utf_size = 0,
},
[3] = {
.script = "var str = '\\uD83Dx\\uDCA9';", // non-surrogate in between pair
.expected_utf_size = 1,
.expected_utf_data = "x",
},
[4] = {
.script = "var str = '\\uD83Dx';", // high surrogate followed by non-surrogate
.expected_utf_size = 1,
.expected_utf_data = "x",
},
[5] = {
.script = "var str = '\\uDCA9x';", // low surrogate followed by non-surrogate
.expected_utf_size = 1,
.expected_utf_data = "x",
},
[6] = {
.script = "var str = 'AB';",
.expected_utf_size = 2,
.expected_utf_data = "AB",
},
[7] = {
.script = "var str = '\xC4\x91';", // 2-byte codepoint (U+0111)
.expected_utf_size = 2,
.expected_utf_data = "\xC4\x91",
},
[8] = {
.script = "var str = '\xE0\xA0\x95';", // 3-byte codepoint (U+0815)
.expected_utf_size = 3,
.expected_utf_data = "\xE0\xA0\x95",
},
[9] = {
.script = "var str = '\\uD83D\\uDCA9';", // 4-byte codepoint (U+1F4A9, escaped data)
.expected_utf_size = 4,
.expected_utf_data = "\xF0\x9F\x92\xA9",
},
[10] = {
.script = "var str = '\xF0\x9F\x92\xA9';", // 4-byte codepoint (U+1F4A9, UTF-8 data in source)
.expected_utf_size = 4,
.expected_utf_data = "\xF0\x9F\x92\xA9",
},
};
for (int j = 0; j < 2; ++j) {
const bool is_overflow_test = (j == 1);
for (int i = 0; i < ARRAY_LENGTH(cases); ++i) {
printf("Case %i (is_overflow_test=%u): %s\n", i, is_overflow_test, cases[i].script);
EXECUTE_SCRIPT(cases[i].script);
const jerry_value_t s = JS_GLOBAL_GET_VALUE("str");
const jerry_size_t utf8_size = jerry_get_utf8_string_size(s);
// U+1F4A9 is expected to get encoded into 4 bytes of UTF-8:
cl_assert_equal_i(utf8_size, cases[i].expected_utf_size);
const size_t buffer_size = utf8_size ? (is_overflow_test ? (utf8_size - 1) : utf8_size) : 0;
// malloc, so DUMA will detect buffer overflows:
jerry_char_t *utf8_buffer = malloc(buffer_size);
const jerry_size_t copied_size =
jerry_string_to_utf8_char_buffer(s, utf8_buffer, buffer_size);
if (!is_overflow_test) {
cl_assert_equal_i(copied_size, cases[i].expected_utf_size);
if (cases[i].expected_utf_size) {
cl_assert_equal_m(utf8_buffer, cases[i].expected_utf_data, cases[i].expected_utf_size);
}
} else {
// When buffer is too small, expect 0 bytes copied:
cl_assert_equal_i(copied_size, 0);
}
jerry_release_value(s);
free(utf8_buffer);
}
}
}
void test_rocky_text_encoding__jerry_utf8_to_cesu8_conversion(void) {
struct {
const char *const utf8_input;
const char *const cesu8_output;
} cases[] = {
{
.utf8_input = "",
.cesu8_output = "",
},
{
.utf8_input = "abc",
.cesu8_output = "abc",
},
{
// U+1F4A9 expands to surrogate pair:
.utf8_input = "abc\xF0\x9F\x92\xA9xyz",
.cesu8_output = "abc\xed\xa0\xbd\xed\xb2\xa9xyz",
},
{
// Be lax with surrogates: even though they're not supposed to appear in UTF-8,
// just copy them over to the CESU-8 output, even a "half pair":
.utf8_input = "\xed\xa0\xbd",
.cesu8_output = "\xed\xa0\xbd",
},
};
for (int i = 0; i < ARRAY_LENGTH(cases); ++i) {
jerry_char_t output[32] = {};
const jerry_value_t s = jerry_create_string_utf8((const jerry_char_t *)cases[i].utf8_input);
const jerry_size_t copied_bytes = jerry_string_to_char_buffer(s, output, sizeof(output));
cl_assert_equal_i(copied_bytes, strlen(cases[i].cesu8_output));
if (copied_bytes) {
cl_assert_equal_m(output, cases[i].cesu8_output, copied_bytes);
}
// TODO: test equality/hash
jerry_release_value(s);
}
}

231
tests/fw/javascript/wscript Normal file
View file

@ -0,0 +1,231 @@
import os
from waftools.pebble_test import clar
import sh
def rocky_clar(ctx, **kwargs):
if ctx.variant == 'test_rocky_emx':
kwargs["sources_ant_glob"] += " applib-targets/emscripten/emscripten_jerry_port.c"
jerry_uses = ['emscripten_jerry_api', 'jerry_port_includes']
else:
kwargs["sources_ant_glob"] += " src/fw/applib/rockyjs/jerry_port.c"
jerry_uses = ['jerry_port_includes', 'jerry_core', 'jerry_libm']
kwargs["use"] = jerry_uses + (kwargs["use"] if "use" in kwargs else [])
jerry_defines = ['CAPABILITY_HAS_JAVASCRIPT=1', 'CAPABILITY_JAVASCRIPT_BYTECODE_VERSION=1',
'JMEM_STATS=1']
kwargs["defines"] = jerry_defines + kwargs.get("defines", [])
kwargs["defines"].extend(ctx.env.test_image_defines)
clar(ctx, **kwargs)
def build(ctx):
if ctx.env.NOJS:
return
rocky_clar(ctx,
sources_ant_glob =
" tests/fakes/fake_applib_resource.c"
" src/fw/applib/rockyjs/api/rocky_api_global.c"
" src/fw/applib/rockyjs/api/rocky_api_errors.c"
" src/fw/applib/rockyjs/api/rocky_api_util.c",
test_sources_ant_glob = "test_rocky_api_global.c")
rocky_clar(ctx,
sources_ant_glob =
" tests/fakes/fake_applib_resource.c"
" src/fw/applib/rockyjs/api/rocky_api_global.c"
" src/fw/applib/rockyjs/api/rocky_api_graphics.c"
" src/fw/applib/rockyjs/api/rocky_api_graphics_color.c"
" src/fw/applib/rockyjs/api/rocky_api_graphics_text.c"
" src/fw/applib/rockyjs/api/rocky_api_errors.c"
" src/fw/applib/rockyjs/api/rocky_api_util.c"
" src/fw/applib/rockyjs/api/rocky_api_util_args.c",
test_sources_ant_glob = "test_rocky_api_graphics.c")
rocky_clar(ctx,
sources_ant_glob =
" tests/fakes/fake_applib_resource.c"
" src/fw/applib/rockyjs/api/rocky_api_global.c"
" src/fw/applib/rockyjs/api/rocky_api_graphics.c"
" src/fw/applib/rockyjs/api/rocky_api_graphics_color.c"
" src/fw/applib/rockyjs/api/rocky_api_errors.c"
" src/fw/applib/rockyjs/api/rocky_api_util.c"
" src/fw/applib/rockyjs/api/rocky_api_util_args.c",
test_sources_ant_glob = "test_rocky_api_graphics_color.c")
rocky_clar(ctx,
sources_ant_glob =
" tests/fakes/fake_applib_resource.c"
" src/fw/applib/graphics/gtypes.c"
" src/fw/applib/rockyjs/api/rocky_api_global.c"
" src/fw/applib/rockyjs/api/rocky_api_graphics.c"
" src/fw/applib/rockyjs/api/rocky_api_graphics_path2d.c"
" src/fw/applib/rockyjs/api/rocky_api_errors.c"
" src/fw/applib/rockyjs/api/rocky_api_util.c"
" src/fw/applib/rockyjs/api/rocky_api_util_args.c",
test_sources_ant_glob = "test_rocky_api_graphics_path2d.c")
rocky_clar(ctx,
sources_ant_glob =
" tests/fakes/fake_applib_resource.c"
" src/fw/applib/graphics/gpath.c"
" src/fw/applib/graphics/graphics_circle.c"
" src/fw/applib/graphics/graphics.c"
" src/fw/applib/graphics/bitblt.c"
" src/fw/applib/graphics/8_bit/bitblt_private.c"
" src/fw/applib/graphics/8_bit/framebuffer.c"
" src/fw/applib/graphics/framebuffer.c"
" src/fw/applib/graphics/graphics_private_raw.c"
" src/fw/applib/graphics/graphics_private_raw_mask.c"
" src/fw/applib/graphics/gbitmap.c"
" src/fw/applib/graphics/gtypes.c"
" src/fw/applib/graphics/gcolor_definitions.c"
" src/fw/applib/graphics/graphics_line.c"
" src/fw/applib/graphics/graphics_private.c"
" src/fw/applib/rockyjs/api/rocky_api_global.c"
" src/fw/applib/rockyjs/api/rocky_api_graphics.c"
" src/fw/applib/rockyjs/api/rocky_api_graphics_color.c"
" src/fw/applib/rockyjs/api/rocky_api_graphics_path2d.c"
" src/fw/applib/rockyjs/api/rocky_api_errors.c"
" src/fw/applib/rockyjs/api/rocky_api_util.c"
" src/fw/applib/rockyjs/api/rocky_api_util_args.c",
test_sources_ant_glob = "test_rocky_api_graphics_rendering.c"
)
rocky_clar(ctx,
sources_ant_glob =
" tests/fakes/fake_applib_resource.c"
" src/fw/applib/rockyjs/api/rocky_api_global.c"
" src/fw/applib/rockyjs/api/rocky_api_tickservice.c"
" src/fw/applib/rockyjs/api/rocky_api_errors.c"
" src/fw/applib/rockyjs/api/rocky_api_util.c",
test_sources_ant_glob = "test_rocky_api_tickservice.c")
rocky_clar(ctx,
sources_ant_glob =
" src/fw/applib/rockyjs/api/rocky_api_global.c"
" src/fw/applib/rockyjs/api/rocky_api_preferences.c"
" src/fw/applib/rockyjs/api/rocky_api_errors.c"
" src/fw/applib/rockyjs/api/rocky_api_util.c",
test_sources_ant_glob = "test_rocky_api_preferences.c")
rocky_clar(ctx,
sources_ant_glob =
" src/fw/util/dict.c"
" tests/fakes/fake_events.c"
" src/fw/applib/rockyjs/api/rocky_api_global.c"
" src/fw/applib/rockyjs/api/rocky_api_errors.c"
" src/fw/applib/rockyjs/api/rocky_api_util.c"
" src/fw/applib/rockyjs/api/rocky_api_app_message.c",
test_sources_ant_glob = "test_rocky_api_app_message.c")
rocky_clar(ctx,
sources_ant_glob =
" src/fw/util/dict.c"
" src/fw/applib/rockyjs/api/rocky_api_global.c"
" src/fw/applib/rockyjs/api/rocky_api_errors.c"
" src/fw/applib/rockyjs/api/rocky_api_util.c"
" src/fw/applib/rockyjs/api/rocky_api_watchinfo.c",
test_sources_ant_glob = "test_rocky_api_watchinfo.c")
rocky_clar(ctx,
sources_ant_glob =
" src/fw/applib/rockyjs/api/rocky_api_errors.c"
" src/fw/applib/rockyjs/api/rocky_api_global.c"
" src/fw/applib/rockyjs/api/rocky_api_util.c",
test_sources_ant_glob = "test_rocky_api_util.c")
rocky_clar(ctx,
sources_ant_glob =
" src/fw/applib/rockyjs/api/rocky_api_errors.c"
" src/fw/applib/rockyjs/api/rocky_api_graphics_color.c"
" src/fw/applib/rockyjs/api/rocky_api_util.c"
" src/fw/applib/rockyjs/api/rocky_api_util_args.c",
test_sources_ant_glob = "test_rocky_api_util_args.c")
# Tests that should be skipped when running unit tests with the fake
# JerryScript (emscripten_jerry_api.c):
if ctx.variant != 'test_rocky_emx':
rocky_clar(ctx,
sources_ant_glob =
" tests/fakes/fake_applib_resource.c" \
" tests/fakes/fake_clock.c" \
" src/fw/applib/graphics/gtypes.c" \
" src/fw/applib/rockyjs/rocky.c" \
" src/fw/applib/rockyjs/api/rocky_api.c" \
" src/fw/applib/rockyjs/api/rocky_api_datetime.c" \
" src/fw/applib/rockyjs/api/rocky_api_global.c" \
" src/fw/applib/rockyjs/api/rocky_api_errors.c" \
" src/fw/applib/rockyjs/api/rocky_api_tickservice.c" \
" src/fw/applib/rockyjs/api/rocky_api_timers.c" \
" src/fw/applib/rockyjs/api/rocky_api_graphics.c" \
" src/fw/applib/rockyjs/api/rocky_api_graphics_path2d.c" \
" src/fw/applib/rockyjs/api/rocky_api_graphics_color.c" \
" src/fw/applib/rockyjs/api/rocky_api_graphics_text.c" \
" src/fw/applib/rockyjs/api/rocky_api_memory.c" \
" src/fw/applib/rockyjs/api/rocky_api_console.c" \
" src/fw/applib/rockyjs/api/rocky_api_preferences.c" \
" src/fw/applib/rockyjs/api/rocky_api_util.c" \
" src/fw/applib/rockyjs/api/rocky_api_util_args.c",
test_sources_ant_glob = "test_js.c")
# No snapshot support in emscripten_jerry_api.c :D
rocky_clar(ctx,
sources_ant_glob =
" tests/fakes/fake_applib_resource.c"
" src/fw/applib/rockyjs/rocky.c"
" src/fw/applib/rockyjs/rocky_res.c"
" src/fw/applib/rockyjs/api/rocky_api_errors.c"
" src/fw/applib/rockyjs/api/rocky_api_util.c",
test_sources_ant_glob = "test_rocky_res.c")
# When building unit tests with emscripten, skip this one because we're
# using the console.log/warn/error implementations of the browser/node.
rocky_clar(ctx,
sources_ant_glob =
" tests/fakes/fake_applib_resource.c"
" src/fw/applib/rockyjs/api/rocky_api_global.c"
" src/fw/applib/rockyjs/api/rocky_api_console.c"
" src/fw/applib/rockyjs/api/rocky_api_errors.c"
" src/fw/applib/rockyjs/api/rocky_api_util.c",
test_sources_ant_glob = "test_rocky_api_console.c")
rocky_clar(ctx,
sources_ant_glob =
" tests/fakes/fake_applib_resource.c"
" tests/fakes/fake_clock.c"
" src/fw/applib/rockyjs/api/rocky_api_global.c"
" src/fw/applib/rockyjs/api/rocky_api_datetime.c"
" src/fw/applib/rockyjs/api/rocky_api_errors.c"
" src/fw/applib/rockyjs/api/rocky_api_util.c",
test_sources_ant_glob = "test_rocky_api_datetime.c",
defines=['CAPABILITY_HAS_JAVASCRIPT=1',
'CAPABILITY_JAVASCRIPT_BYTECODE_VERSION=1'])
rocky_clar(ctx,
sources_ant_glob=" tests/fakes/fake_applib_resource.c"
" src/fw/applib/rockyjs/api/rocky_api_global.c"
" src/fw/applib/rockyjs/api/rocky_api_memory.c"
" src/fw/applib/rockyjs/api/rocky_api_errors.c"
" src/fw/applib/rockyjs/api/rocky_api_util.c",
test_sources_ant_glob = "test_rocky_api_memory.c")
# Currently we're leaving the browser/node's setTimeout + friends alone
rocky_clar(ctx,
sources_ant_glob =
" src/fw/applib/rockyjs/api/rocky_api_errors.c"
" src/fw/applib/rockyjs/api/rocky_api_timers.c"
" src/fw/applib/rockyjs/api/rocky_api_util.c",
test_sources_ant_glob = "test_rocky_api_timers.c")
# Emscripten transpiled jerry-api uses UTF8 internally.
rocky_clar(ctx,
sources_ant_glob=
" src/fw/applib/rockyjs/api/rocky_api_errors.c"
" src/fw/applib/rockyjs/api/rocky_api_util.c",
test_sources_ant_glob = "test_rocky_text_encoding.c")
# vim:filetype=python