pebble/tests/fw/javascript/test_rocky_api_app_message.c
2025-01-27 11:38:16 -08:00

971 lines
34 KiB
C

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