pebble/tests/fw/javascript/test_js.c

589 lines
20 KiB
C
Raw Permalink Normal View History

/*
* 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);
}