Import of the watch repository from Pebble

This commit is contained in:
Matthieu Jeanson
2025-01-27 11:38:16 -08:00
committed by Katharine Berry
commit 3b92768480
10334 changed files with 2564465 additions and 0 deletions
@@ -0,0 +1,333 @@
/*
* 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/bluetooth/ble_ad_parse.h"
#include "system/hexdump.h"
#include "clar.h"
#include <btutil/bt_uuid.h>
// Stubs
///////////////////////////////////////////////////////////
#include "stubs_ble_syscalls.h"
#include "stubs_ble_syscalls.h"
#include "stubs_logging.h"
#include "stubs_passert.h"
#include "stubs_pbl_malloc.h"
#include "stubs_pebble_tasks.h"
#include "stubs_print.h"
#include "stubs_prompt.h"
#include "stubs_rand_ptr.h"
#include "stubs_serial.h"
// The test data and descriptions in this file are captured using the FrontLine
// Bluetooth sniffer.
static const size_t s_buffer_size = sizeof(BLEAdData) +
(2 * GAP_LE_AD_REPORT_DATA_MAX_LENGTH);
static uint8_t s_buffer[s_buffer_size];
static BLEAdData * const s_ad_data = (BLEAdData *)s_buffer;
static void set_ad_data(uint8_t *data, size_t length) {
memcpy(s_ad_data->data, data, length);
s_ad_data->ad_data_length = length;
}
void test_ble_ad_parse__initialize(void) {
memset(s_ad_data, 0, sizeof(s_buffer_size));
}
// -----------------------------------------------------------------------------
// Consuming BLEAdData:
// -----------------------------------------------------------------------------
void test_ble_ad_parse__16_bit_uuid_and_device_name(void) {
// AD Element, Length: 2, AD Type: Flags (0x1a)
// AD Element, Length: 3, AD Type: Complete list of 16-bit UUID, [0x7b29]
// AD Element, Length: 10, AD Type: Complete local name, Text: LightBlue
uint8_t data[] =
"\x02\x01\x1a\x03\x03\x29\x7b\x0a\x09\x4c\x69\x67\x68\x74\x42\x6c\x75\x65";
set_ad_data(data, sizeof(data));
// Test ble_ad_get_raw_data_size:
cl_assert_equal_i(ble_ad_get_raw_data_size(s_ad_data), sizeof(data));
// Test ble_ad_copy_raw_data:
uint8_t buffer[GAP_LE_AD_REPORT_DATA_MAX_LENGTH * 2];
size_t size = ble_ad_copy_raw_data(s_ad_data, buffer, sizeof(buffer));
cl_assert_equal_i(size, sizeof(data));
cl_assert_equal_i(memcmp(buffer, data, sizeof(data)), 0);
// Test ble_ad_copy_local_name, destination buffer large enough:
char local_name[64];
size = ble_ad_copy_local_name(s_ad_data, local_name, 64);
cl_assert_equal_s(local_name, "LightBlue");
cl_assert_equal_i(size, strlen("LightBlue") + 1);
// Test ble_ad_copy_local_name, destination buffer too small:
size = ble_ad_copy_local_name(s_ad_data, local_name, 6);
cl_assert_equal_s(local_name, "Light");
cl_assert_equal_i(size, strlen("Light") + 1);
// Test ble_ad_includes_service:
Uuid included_uuid = bt_uuid_expand_16bit(0x7b29);
cl_assert(ble_ad_includes_service(s_ad_data, &included_uuid));
Uuid missing_uuid = bt_uuid_expand_16bit(0xabcd);
cl_assert(!ble_ad_includes_service(s_ad_data, &missing_uuid));
// Test ble_ad_copy_service_uuids, destination array sized larged enough:
const uint8_t count = 4;
Uuid copied_uuids[count];
uint8_t found = ble_ad_copy_service_uuids(s_ad_data, copied_uuids, count);
cl_assert_equal_i(found, 1);
// Test ble_ad_copy_service_uuids, destination array too small:
found = ble_ad_copy_service_uuids(s_ad_data, copied_uuids, 0);
cl_assert_equal_i(found, 1);
// Test ble_ad_get_tx_power_level returns false, when no TX Power Level:
int8_t tx_power_level_out;
cl_assert(!ble_ad_get_tx_power_level(s_ad_data, &tx_power_level_out));
}
void test_ble_ad_parse__128_bit_uuid(void) {
// AD Element, Length: 2, AD Type: Flags
// AD Element, Length: 17, AD Type: More 128-bit UUIDs available,
// Value: 0x68753a444d6f12269c600050e4c00067
uint8_t data[GAP_LE_AD_REPORT_DATA_MAX_LENGTH] =
"\x02\x01\x1a\x11\x06\x67\x00\xc0\xe4\x50\x00\x60\x9c\x26\x12\x6f\x4d\x44" \
"\x3a\x75\x68";
set_ad_data(data, sizeof(data));
// Test ble_ad_includes_service:
uint8_t uuid_bytes[] = "\x68\x75\x3a\x44\x4d\x6f\x12\x26\x9c\x60\x00\x50" \
"\xe4\xc0\x00\x67";
Uuid included_uuid = UuidMakeFromBEBytes(uuid_bytes);
cl_assert(ble_ad_includes_service(s_ad_data, &included_uuid));
Uuid missing_uuid = bt_uuid_expand_16bit(0xabcd);
cl_assert(!ble_ad_includes_service(s_ad_data, &missing_uuid));
}
// -----------------------------------------------------------------------------
// Creating BLEAdData:
// -----------------------------------------------------------------------------
void test_ble_ad_parse__ad_and_scan_resp_boundaries(void) {
}
void test_ble_ad_parse__start_scan_response(void) {
BLEAdData *ad = ble_ad_create();
ble_ad_start_scan_response(ad);
uint8_t expected_scan_resp_data[] = {
1 /* +1 for Type byte */ + strlen("Pebble 1234"),
0x09, // Local Name, Complete
'P', 'e', 'b', 'b', 'l', 'e', ' ', '1', '2', '3', '4'
};
// Should fit fine, expect true:
cl_assert_equal_b(ble_ad_set_local_name(ad, "Pebble 1234"), true);
// Expect no advertisement data:
cl_assert_equal_i(ad->ad_data_length, 0);
// Expect scan response data:
cl_assert_equal_i(ad->scan_resp_data_length, sizeof(expected_scan_resp_data));
// Compare scan response data:
cl_assert_equal_i(memcmp(expected_scan_resp_data,
ad->data + ad->ad_data_length,
ad->scan_resp_data_length), 0);
ble_ad_destroy(ad);
}
void test_ble_ad_parse__set_service_uuids_128_bit(void) {
BLEAdData *ad = ble_ad_create();
uint8_t uuid_bytes[] = "\x97\x6e\xbb\x18\xd3\xe9\x43\xc0\x8a\x63\x8d\x2b" \
"\x60\xd9\x04\x2a";
Uuid uuid[2];
uuid[0] = UuidMakeFromBEBytes(uuid_bytes);
uuid[1] = UuidMakeFromLEBytes(uuid_bytes); // just reversed, laziness
// 2x 128-bit UUIDs is not going to fit, expect false:
cl_assert_equal_b(ble_ad_set_service_uuids(ad, uuid, 2), false);
// Hand-construct expected raw advertisement data:
uint8_t expected_ad_data[sizeof(Uuid) + 2 /* +1 Length, +1 Type bytes */] = {
sizeof(Uuid) + 1 /* +1 for Type byte */,
0x07, // Service UUIDs, 128-bit, Complete
};
memcpy(&expected_ad_data[2], uuid_bytes, sizeof(Uuid));
// One should fit though:
cl_assert_equal_b(ble_ad_set_service_uuids(ad, uuid, 1), true);
cl_assert_equal_i(memcmp(expected_ad_data, ad->data,
sizeof(expected_ad_data)), 0);
cl_assert_equal_i(ad->ad_data_length, sizeof(expected_ad_data));
cl_assert_equal_i(ad->scan_resp_data_length, 0);
ble_ad_destroy(ad);
}
void test_ble_ad_parse__set_service_uuids_32_bit(void) {
BLEAdData *ad;
Uuid uuid[8];
for (int i = 0; i < 8; ++i) {
uuid[i] = bt_uuid_expand_32bit(0x12346700 + i);
}
// Hand-construct expected raw advertisement data:
uint8_t expected_ad_data[] = {
(2 * sizeof(uint32_t)) + 1 /* +1 for Type byte */,
0x05, // Service UUIDs, 32-bit, Complete
0x00, 0x67, 0x34, 0x12, // Little endian
0x01, 0x67, 0x34, 0x12,
};
// 2x 32-bit UUIDs should fit fine, expect true:
ad = ble_ad_create();
cl_assert_equal_b(ble_ad_set_service_uuids(ad, uuid, 2), true);
cl_assert_equal_i(memcmp(expected_ad_data, ad->data,
sizeof(expected_ad_data)), 0);
cl_assert_equal_i(ad->ad_data_length, sizeof(expected_ad_data));
cl_assert_equal_i(ad->scan_resp_data_length, 0);
ble_ad_destroy(ad);
// 7x 32-bit UUIDs should fit, expect true:
ad = ble_ad_create();
cl_assert_equal_b(ble_ad_set_service_uuids(ad, uuid, 7), true);
ble_ad_destroy(ad);
// 8x 32-bit UUIDs does not fit, expect false:
ad = ble_ad_create();
cl_assert_equal_b(ble_ad_set_service_uuids(ad, uuid, 8), false);
ble_ad_destroy(ad);
}
void test_ble_ad_parse__set_service_uuids_16_bit(void) {
BLEAdData *ad;
Uuid uuid[15];
for (int i = 0; i < 15; ++i) {
uuid[i] = bt_uuid_expand_16bit(0x1800 + i);
}
// Hand-construct expected raw advertisement data:
uint8_t expected_ad_data[] = {
(2 * sizeof(uint16_t)) + 1 /* +1 for Type byte */,
0x03, // Service UUIDs, 16-bit, Complete
0x00, 0x18, // Little endian
0x01, 0x18,
};
// 2x 16-bit UUIDs should fit fine, expect true:
ad = ble_ad_create();
cl_assert_equal_b(ble_ad_set_service_uuids(ad, uuid, 2), true);
cl_assert_equal_i(memcmp(expected_ad_data, ad->data,
sizeof(expected_ad_data)), 0);
cl_assert_equal_i(ad->ad_data_length, sizeof(expected_ad_data));
cl_assert_equal_i(ad->scan_resp_data_length, 0);
ble_ad_destroy(ad);
// 14x 16-bit UUIDs should fit, expect true:
ad = ble_ad_create();
cl_assert_equal_b(ble_ad_set_service_uuids(ad, uuid, 14), true);
ble_ad_destroy(ad);
// 15x 16-bit UUIDs does not fit, expect false:
ad = ble_ad_create();
cl_assert_equal_b(ble_ad_set_service_uuids(ad, uuid, 15), false);
ble_ad_destroy(ad);
}
void test_ble_ad_parse__set_local_name(void) {
BLEAdData *ad;
ad = ble_ad_create();
uint8_t expected_ad_data[] = {
1 /* +1 for Type byte */ + strlen("Pebble 1234"),
0x09, // Local Name, Complete
'P', 'e', 'b', 'b', 'l', 'e', ' ', '1', '2', '3', '4'
};
// Should fit fine, expect true:
cl_assert_equal_b(ble_ad_set_local_name(ad, "Pebble 1234"), true);
cl_assert_equal_i(memcmp(expected_ad_data, ad->data, ad->ad_data_length), 0);
ble_ad_destroy(ad);
}
void test_ble_ad_parse__set_tx_power_level(void) {
BLEAdData *ad;
ad = ble_ad_create();
uint8_t expected_ad_data[] = {
1 /* +1 for Type byte */ + 1 /* int8_t with value */,
0x0a, // TX Power Level
-55,
};
// Should fit fine, expect true:
cl_assert_equal_b(ble_ad_set_tx_power_level(ad), true);
cl_assert_equal_i(memcmp(expected_ad_data, ad->data, ad->ad_data_length), 0);
ble_ad_destroy(ad);
}
void test_ble_ad_parse__set_manufacturer_specific_data(void) {
BLEAdData *ad;
ad = ble_ad_create();
uint8_t expected_ad_data[] = {
1 /* +1 for Type byte */ + 13 /* Company ID + data */,
0xff, // Manufacturer Specific data
0x34, 0x12,
'h', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd',
};
// Should fit fine, expect true:
cl_assert_equal_b(ble_ad_set_manufacturer_specific_data(ad,
0x1234,
(uint8_t *) "hello world",
11), true);
cl_assert_equal_i(memcmp(expected_ad_data, ad->data, ad->ad_data_length), 0);
ble_ad_destroy(ad);
}
void test_ble_ad_parse__set_flags(void) {
BLEAdData *ad;
ad = ble_ad_create();
const uint8_t flags = 0x03;
const uint8_t expected_ad_data[] = {
1 /* +1 for Type byte */ + 1 /* uint8_t with value */,
0x01, // Flags type
flags,
};
// Should fit fine, expect true:
cl_assert_equal_b(ble_ad_set_flags(ad, flags), true);
cl_assert_equal_i(memcmp(expected_ad_data, ad->data, ad->ad_data_length), 0);
ble_ad_destroy(ad);
}
@@ -0,0 +1,132 @@
/*
* 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/bluetooth/ble_ibeacon.h"
#include <btutil/bt_device.h>
#include "clar.h"
// Stubs
///////////////////////////////////////////////////////////
#include "stubs_logging.h"
#include "stubs_passert.h"
#include "stubs_pbl_malloc.h"
#include "stubs_pebble_tasks.h"
#include "stubs_rand_ptr.h"
#include "stubs_ble_syscalls.h"
// The test data and descriptions in this file are captured using the FrontLine
// Bluetooth sniffer.
// AD Element, Length: 26, AD Type: Manufacturer Specific, Manufacturer ID:
// Apple, Inc. (0x004c) Additional Data: 0x 02 15 97 6e bb 18 d3 e9 43 c0 8a
// 63 8d 2b 60 d9 04 2a 00 0c 00 22 c5
// This excludes the "Manufacturer Specific" opcode (0xff) but it
// includes the Apple Inc company ID:
static BLEAdData *create_apple_ibeacon_ad_data(void) {
static const uint8_t apple_ibeacon_ad_element[] =
{
0x1a, // 26 bytes
0xff, // Manufacturer Specific AD Type
0x4c, 0x00, // Apple
0x02, // iBeacon
0x15, // Number of bytes to follow
0x97, 0x6e, 0xbb, 0x18, 0xd3, 0xe9, 0x43, 0xc0, // Proximity UUID
0x8a, 0x63, 0x8d, 0x2b, 0x60, 0xd9, 0x04, 0x2a,
0x00, 0x0c, // Minor (BE)
0x00, 0x22, // Major (BE)
0xc5 // TX Power
};
const size_t ad_data_length = sizeof(apple_ibeacon_ad_element);
BLEAdData *ad_data = (BLEAdData *) malloc(sizeof(BLEAdData) + ad_data_length);
ad_data->ad_data_length = ad_data_length;
ad_data->scan_resp_data_length = 0;
memcpy(ad_data->data, apple_ibeacon_ad_element, ad_data_length);
return ad_data;
}
void test_ble_ibeacon__parse_ibeacon_data(void) {
BLEAdData *apple_ibeacon_ad_data = create_apple_ibeacon_ad_data();
BLEiBeacon ibeacon;
const int8_t rssi = -60;
bool is_ibeacon = ble_ibeacon_parse(apple_ibeacon_ad_data, rssi, &ibeacon);
cl_assert(is_ibeacon);
uint8_t uuid_bytes[] = "\x97\x6e\xbb\x18\xd3\xe9\x43\xc0\x8a\x63\x8d\x2b" \
"\x60\xd9\x04\x2a";
Uuid uuid = UuidMakeFromBEBytes(uuid_bytes);
cl_assert(uuid_equal(&ibeacon.uuid, &uuid));
cl_assert_equal_i(ibeacon.major, 12);
cl_assert_equal_i(ibeacon.minor, 34);
cl_assert_equal_i(ibeacon.rssi, -60);
cl_assert_equal_i(ibeacon.calibrated_tx_power, -59);
// cl_assert_equal_i(ibeacon.distance_cm, 110);
free(apple_ibeacon_ad_data);
}
void test_ble_ibeacon__ibeacon_compose(void) {
BLEAdData *apple_ibeacon_ad_data = create_apple_ibeacon_ad_data();
BLEiBeacon ibeacon;
const int8_t rssi = -60;
ble_ibeacon_parse(apple_ibeacon_ad_data, rssi, &ibeacon);
BLEAdData *new_ibeacon_ad_data = ble_ad_create();
cl_assert_equal_b(ble_ibeacon_compose(&ibeacon, new_ibeacon_ad_data), true);
const size_t ad_data_size = apple_ibeacon_ad_data->ad_data_length +
apple_ibeacon_ad_data->scan_resp_data_length;
cl_assert_equal_i(new_ibeacon_ad_data->ad_data_length,
apple_ibeacon_ad_data->ad_data_length);
cl_assert_equal_i(new_ibeacon_ad_data->scan_resp_data_length,
apple_ibeacon_ad_data->scan_resp_data_length);
cl_assert_equal_i(memcmp(new_ibeacon_ad_data->data,
apple_ibeacon_ad_data->data,
ad_data_size), 0);
free(apple_ibeacon_ad_data);
ble_ad_destroy(new_ibeacon_ad_data);
}
static BLEAdData *create_too_short_ad_data(void) {
static const uint8_t too_short_ad_element[] =
{ 0x1a, // 26 bytes
0xff, // Manufacturer Specific AD Type
0x4c, 0x00, // Apple
0x02, // iBeacon
0x14, // Number of bytes to follow -- Internally inconsistent!
0x97, 0x6e, 0xbb, 0x18, 0xd3, 0xe9, 0x43, 0xc0, // Proximity UUID
0x8a, 0x63, 0x8d, 0x2b, 0x60, 0xd9, 0x04, 0x2a,
0x00, 0x0c, // Minor (BE)
0x00, 0x22, // Major (BE)
0xc5 // TX Power
};
BLEAdData *ad_data = (BLEAdData *) malloc(sizeof(BLEAdData) +
sizeof(too_short_ad_element));
ad_data->ad_data_length = sizeof(too_short_ad_element);
ad_data->scan_resp_data_length = 0;
return ad_data;
}
void test_ble_ibeacon__ibeacon_data_too_short(void) {
BLEAdData *too_short_to_ibeacon = create_too_short_ad_data();
bool is_ibeacon = ble_ibeacon_parse(too_short_to_ibeacon, 0, NULL);
cl_assert(!is_ibeacon);
free(too_short_to_ibeacon);
}
+23
View File
@@ -0,0 +1,23 @@
from waftools.pebble_test import clar
def build(ctx):
clar(ctx,
sources_ant_glob =
"src/fw/applib/bluetooth/ble_ad_parse.c "
"src/fw/util/rand/rand.c "
"src/fw/vendor/tinymt32/tinymt32.c "
"src/fw/system/hexdump.c "
"src/fw/util/buffer.c "
"tests/fakes/fake_rtc.c",
test_sources_ant_glob = "test_ble_ad_parse.c")
clar(ctx,
sources_ant_glob =
"src/fw/applib/bluetooth/ble_ibeacon.c "
"src/fw/applib/bluetooth/ble_ad_parse.c "
"src/fw/util/rand/rand.c "
"src/fw/vendor/tinymt32/tinymt32.c "
"tests/fakes/fake_rtc.c",
test_sources_ant_glob = "test_ble_ibeacon.c")
# vim:filetype=python
+319
View File
@@ -0,0 +1,319 @@
/*
* 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/app_glance.h"
#include "drivers/rtc.h"
#include "kernel/pbl_malloc.h"
#include "process_management/app_install_manager.h"
#include "resource/resource_ids.auto.h"
#include "resource/timeline_resource_ids.auto.h"
#include "services/normal/app_glances/app_glance_service.h"
#include "services/normal/blob_db/app_glance_db.h"
#include "services/normal/blob_db/app_glance_db_private.h"
#include "services/normal/filesystem/pfs.h"
#include "services/normal/timeline/timeline_resources.h"
#include "util/uuid.h"
#define APP_GLANCE_TEST_UUID \
(UuidMake(0x3d, 0xc6, 0xb9, 0x4c, 0x4, 0x2, 0x48, 0xf4, \
0xbe, 0x14, 0x81, 0x17, 0xf1, 0xa, 0xa9, 0xc4))
// Fakes
////////////////////////////////////////////////////////////////
#include "fake_rtc.h"
#include "fake_settings_file.h"
void sys_get_app_uuid(Uuid *uuid) {
if (uuid) {
*uuid = APP_GLANCE_TEST_UUID;
}
}
typedef struct AppGlanceTestState {
bool resource_is_valid;
void *context;
bool reload_callback_was_called;
} AppGlanceTestState;
static AppGlanceTestState s_test_state;
ResAppNum sys_get_current_resource_num(void) {
return 0;
}
void sys_timeline_resources_get_id(const TimelineResourceInfo *timeline_res,
TimelineResourceSize size, AppResourceInfo *res_info) {
if (!res_info) {
return;
}
// Just fill the output resource ID with some number so it's considered "valid"
res_info->res_id = s_test_state.resource_is_valid ? 1337 : 0;
}
// Stubs
////////////////////////////////////////////////////////////////
#include "stubs_app_cache.h"
#include "stubs_app_install_manager.h"
#include "stubs_app_state.h"
#include "stubs_events.h"
#include "stubs_event_service_client.h"
#include "stubs_i18n.h"
#include "stubs_logging.h"
#include "stubs_mutex.h"
#include "stubs_passert.h"
#include "stubs_pbl_malloc.h"
status_t pfs_remove(const char *name) {
fake_settings_file_reset();
return S_SUCCESS;
}
// Setup
////////////////////////////////////////////////////////////////
void test_app_glance__initialize(void) {
fake_rtc_init(0, 1337);
fake_settings_file_reset();
app_glance_db_init();
app_glance_service_init();
s_test_state = (AppGlanceTestState) {};
}
void app_glance_db_deinit(void);
void test_app_glance__cleanup(void) {
app_glance_db_deinit();
}
void prv_basic_reload_cb(AppGlanceReloadSession *session, size_t limit, void *context) {
s_test_state.reload_callback_was_called = true;
s_test_state.resource_is_valid = true;
AppGlanceSlice slice = (AppGlanceSlice) {
.expiration_time = rtc_get_time() + 10,
.layout.icon = TIMELINE_RESOURCE_HOTEL_RESERVATION,
.layout.subtitle_template_string = "Test subtitle",
};
cl_assert_equal_i(app_glance_add_slice(session, slice), APP_GLANCE_RESULT_SUCCESS);
slice = (AppGlanceSlice) {
.expiration_time = rtc_get_time() + 20,
.layout.icon = APP_GLANCE_SLICE_DEFAULT_ICON,
};
cl_assert_equal_i(app_glance_add_slice(session, slice), APP_GLANCE_RESULT_SUCCESS);
}
void test_app_glance__basic_reload(void) {
// Reload the glance with two slices
app_glance_reload(prv_basic_reload_cb, s_test_state.context);
cl_assert_equal_b(s_test_state.reload_callback_was_called, true);
// Read the glance back
AppGlance glance = {};
cl_assert_equal_i(app_glance_db_read_glance(&APP_GLANCE_TEST_UUID, &glance), S_SUCCESS);
// Compare the glance read back with the expected glance below
AppGlance expected_glance = (AppGlance) {
.num_slices = 2,
.slices = {
{
.expiration_time = rtc_get_time() + 10,
.type = AppGlanceSliceType_IconAndSubtitle,
.icon_and_subtitle.icon_resource_id = TIMELINE_RESOURCE_HOTEL_RESERVATION,
.icon_and_subtitle.template_string = "Test subtitle",
},
{
.expiration_time = rtc_get_time() + 20,
.type = AppGlanceSliceType_IconAndSubtitle,
.icon_and_subtitle.icon_resource_id = APP_GLANCE_SLICE_DEFAULT_ICON,
}
},
};
cl_assert_equal_m(&glance, &expected_glance, sizeof(AppGlance));
}
void prv_reload_with_validation_cb(AppGlanceReloadSession *session, size_t limit, void *context) {
s_test_state.reload_callback_was_called = true;
// Check that the context here is the context we passed to `app_glance_reload()`
cl_assert_equal_p(context, s_test_state.context);
// Check that the limit passed in matches the max slices per glance
cl_assert_equal_i(limit, APP_GLANCE_DB_MAX_SLICES_PER_GLANCE);
unsigned int num_slices_added = 0;
AppGlanceSlice slice = {};
// Check that using a bogus session variable fails
AppGlanceReloadSession bogus_session;
cl_assert(app_glance_add_slice(&bogus_session, slice) & APP_GLANCE_RESULT_INVALID_SESSION);
// Check that adding a slice with APP_GLANCE_SLICE_DEFAULT_ICON as the icon succeeds
slice = (AppGlanceSlice) {
.expiration_time = APP_GLANCE_SLICE_NO_EXPIRATION,
.layout.icon = APP_GLANCE_SLICE_DEFAULT_ICON,
.layout.subtitle_template_string = "Test subtitle {time_until(500)|format('%uS')}",
};
cl_assert_equal_i(app_glance_add_slice(session, slice), APP_GLANCE_RESULT_SUCCESS);
num_slices_added++;
// Check that adding a slice with a NULL subtitle succeeds
s_test_state.resource_is_valid = true;
slice = (AppGlanceSlice) {
.expiration_time = APP_GLANCE_SLICE_NO_EXPIRATION,
.layout.icon = TIMELINE_RESOURCE_BIRTHDAY_EVENT,
.layout.subtitle_template_string = NULL,
};
cl_assert_equal_i(app_glance_add_slice(session, slice), APP_GLANCE_RESULT_SUCCESS);
num_slices_added++;
// Check that adding a slice with an invalid icon fails
s_test_state.resource_is_valid = false;
slice = (AppGlanceSlice) {
.expiration_time = APP_GLANCE_SLICE_NO_EXPIRATION,
.layout.icon = RESOURCE_ID_SETTINGS_ICON_AIRPLANE,
.layout.subtitle_template_string = "Test subtitle",
};
cl_assert(app_glance_add_slice(session, slice) & APP_GLANCE_RESULT_INVALID_ICON);
// Check that adding a slice with a subtitle that's too long fails
const char *really_long_subtitle = "This is a really really really really really really really "
"really really really really really really really really "
"really really really really really really really really "
"really really really really really really really really "
"really long subtitle.";
slice = (AppGlanceSlice) {
.expiration_time = APP_GLANCE_SLICE_NO_EXPIRATION,
.layout.icon = APP_GLANCE_SLICE_DEFAULT_ICON,
.layout.subtitle_template_string = really_long_subtitle,
};
cl_assert(app_glance_add_slice(session, slice) & APP_GLANCE_RESULT_TEMPLATE_STRING_TOO_LONG);
// Check that adding a slice with a bad template string fails
const char *invalid_template_subtitle = "How much time? {time_until(500)|format('%uS',)}";
slice = (AppGlanceSlice) {
.expiration_time = APP_GLANCE_SLICE_NO_EXPIRATION,
.layout.icon = APP_GLANCE_SLICE_DEFAULT_ICON,
.layout.subtitle_template_string = invalid_template_subtitle,
};
cl_assert(app_glance_add_slice(session, slice) & APP_GLANCE_RESULT_INVALID_TEMPLATE_STRING);
// Check that adding a slice that expires in the past fails
slice = (AppGlanceSlice) {
.expiration_time = rtc_get_time() - 10,
.layout.icon = APP_GLANCE_SLICE_DEFAULT_ICON,
.layout.subtitle_template_string = "Test subtitle",
};
cl_assert(app_glance_add_slice(session, slice) & APP_GLANCE_RESULT_EXPIRES_IN_THE_PAST);
// At this point we've actually filled up the glance to the capacity
cl_assert_equal_i(num_slices_added, limit);
// So adding one more slice to the glance should fail
s_test_state.resource_is_valid = true;
slice = (AppGlanceSlice) {
.expiration_time = APP_GLANCE_SLICE_NO_EXPIRATION,
.layout.subtitle_template_string = NULL,
};
cl_assert(app_glance_add_slice(session, slice) & APP_GLANCE_RESULT_SLICE_CAPACITY_EXCEEDED);
// Check that we can get reports of multiple kinds of failures at the same time
s_test_state.resource_is_valid = false;
slice = (AppGlanceSlice) {
.expiration_time = rtc_get_time() - 10,
.layout.icon = RESOURCE_ID_SETTINGS_ICON_AIRPLANE,
.layout.subtitle_template_string = really_long_subtitle,
};
const AppGlanceResult result = app_glance_add_slice(session, slice);
cl_assert(result & APP_GLANCE_RESULT_EXPIRES_IN_THE_PAST);
cl_assert(result & APP_GLANCE_RESULT_SLICE_CAPACITY_EXCEEDED);
cl_assert(result & APP_GLANCE_RESULT_INVALID_ICON);
cl_assert(result & APP_GLANCE_RESULT_TEMPLATE_STRING_TOO_LONG);
}
void test_app_glance__reload_with_validation_callback(void) {
app_glance_reload(prv_reload_with_validation_cb, s_test_state.context);
cl_assert_equal_b(s_test_state.reload_callback_was_called, true);
}
static void prv_glance_clear_test(AppGlanceReloadCallback reload_cb) {
// Insert some slices for the glance
const AppGlance glance = (AppGlance) {
.num_slices = 2,
.slices = {
{
.expiration_time = 1464734504, // (Tue, 31 May 2016 22:41:44 GMT)
.type = AppGlanceSliceType_IconAndSubtitle,
.icon_and_subtitle = {
.template_string = "Test subtitle 2",
},
},
{
.expiration_time = 1464734484, // (Tue, 31 May 2016 22:41:24 GMT)
.type = AppGlanceSliceType_IconAndSubtitle,
.icon_and_subtitle = {
.template_string = "Test subtitle 1",
},
},
},
};
cl_assert_equal_i(app_glance_db_insert_glance(&APP_GLANCE_TEST_UUID, &glance), S_SUCCESS);
// Request the current slice for this glance; this should match the earliest-expiring slice in
// the glance we just inserted above
AppGlanceSliceInternal slice_out;
cl_assert_equal_b(app_glance_service_get_current_slice(&APP_GLANCE_TEST_UUID, &slice_out), true);
cl_assert_equal_m(&slice_out, &glance.slices[1], sizeof(slice_out));
// Let some time "pass" so that the creation time of this next reload doesn't get ignored
fake_rtc_increment_time(10);
// Reload the glance using the provided callback; this should empty the slices in the glance
app_glance_reload(reload_cb, NULL);
// Read the glance back and check that it doesn't have any slices anymore
AppGlance glance_read = {};
cl_assert_equal_i(app_glance_db_read_glance(&APP_GLANCE_TEST_UUID, &glance_read), S_SUCCESS);
cl_assert_equal_i(glance_read.num_slices, 0);
for (unsigned int i = 0; i < sizeof(glance_read.slices); i++) {
const uint8_t byte = ((uint8_t *)glance_read.slices)[i];
cl_assert_equal_i(byte, 0);
}
// Request the current slice for this glance again; this should return false since there aren't
// any slices in the glance anymore
cl_assert_equal_b(app_glance_service_get_current_slice(&APP_GLANCE_TEST_UUID, &slice_out), false);
}
void test_app_glance__reload_with_null_callback_empties_slices(void) {
prv_glance_clear_test(NULL);
}
static void prv_reload_with_no_slices_added_cb(AppGlanceReloadSession *session, size_t limit,
void *context) {
// We don't add any slices in this callback on purpose
return;
}
void test_app_glance__reload_with_no_slices_added_empties_slices(void) {
prv_glance_clear_test(prv_reload_with_no_slices_added_cb);
}
+599
View File
@@ -0,0 +1,599 @@
/*
* 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/app_inbox.h"
#include "kernel/events.h"
#include "services/normal/app_inbox_service.h"
#include "util/list.h"
extern bool app_inbox_service_has_inbox_for_tag(AppInboxServiceTag tag);
extern bool app_inbox_service_has_inbox_for_storage(uint8_t *storage);
extern bool app_inbox_service_is_being_written_for_tag(AppInboxServiceTag tag);
extern size_t app_inbox_service_num_failed_for_tag(AppInboxServiceTag tag);
extern size_t app_inbox_service_num_success_for_tag(AppInboxServiceTag tag);
////////////////////////////////////////////////////////////////////////////////////////////////////
// Fakes & Stubs
#include "fake_kernel_malloc.h"
#include "fake_pebble_tasks.h"
#include "stubs_logging.h"
#include "stubs_mutex.h"
#include "stubs_passert.h"
#include "stubs_syscall_internal.h"
#define BUFFER_SIZE (32)
#define NOT_PERMITTED_MSG_HANDLER ((AppInboxMessageHandler)~0)
#define NOT_PERMITTED_DROP_HANDLER ((AppInboxDroppedHandler)~0)
#define TEST_TARGET_TASK (PebbleTask_App)
typedef struct {
ListNode node;
PebbleEvent event;
} EventNode;
static EventNode *s_event_head;
static bool s_can_send_event;
bool process_manager_send_event_to_process(PebbleTask task, PebbleEvent* e) {
cl_assert_equal_i(PEBBLE_CALLBACK_EVENT, e->type);
cl_assert_equal_i(task, TEST_TARGET_TASK);
if (s_can_send_event) {
EventNode *node = (EventNode *)malloc(sizeof(EventNode));
*node = (const EventNode) {
.event = *e,
};
s_event_head = (EventNode *)list_prepend((ListNode *)s_event_head, (ListNode *)node);
}
return s_can_send_event;
}
static void prv_process_callback_events_alt(bool should_execute_callback) {
EventNode *node = s_event_head;
while (node) {
EventNode *next = (EventNode *) node->node.next;
if (should_execute_callback) {
node->event.callback.callback(node->event.callback.data);
}
free(node);
node = next;
}
s_event_head = NULL;
}
static void prv_process_callback_events(void) {
prv_process_callback_events_alt(true /* should_execute_callback */);
}
static void prv_cleanup_callback_events(void) {
prv_process_callback_events_alt(false /* should_execute_callback */);
}
#define assert_num_callback_events(num) \
cl_assert_equal_i(list_count((ListNode *)s_event_head), num);
////////////////////////////////////////////////////////////////////////////////////////////////////
// Inbox Service Stubs
void app_message_receiver_message_handler(const uint8_t *data, size_t length,
AppInboxConsumerInfo *consumer_info) {
}
void app_message_receiver_dropped_handler(uint32_t num_dropped_messages) {
}
////////////////////////////////////////////////////////////////////////////////////////////////////
// Test Inbox Service Handlers
#define TEST_ARRAY_SIZE (4)
static int s_message_idx;
static struct {
uint8_t data[BUFFER_SIZE];
size_t length;
} s_messages[TEST_ARRAY_SIZE];
static int s_num_messages_to_consume_from_handler;
void test_message_handler(const uint8_t *data, size_t length, AppInboxConsumerInfo *consumer_info) {
cl_assert(s_message_idx < TEST_ARRAY_SIZE);
s_messages[s_message_idx].length = length;
memcpy(s_messages[s_message_idx].data, data, length);
++s_message_idx;
if (s_num_messages_to_consume_from_handler--) {
app_inbox_consume(consumer_info);
}
}
#define assert_message(idx, dd, ll) \
{ \
cl_assert(idx <= s_message_idx); \
cl_assert_equal_i(ll, s_messages[idx].length); \
cl_assert_equal_m(dd, s_messages[idx].data, ll); \
}
#define assert_num_message_callbacks(num_cbs) \
{ \
cl_assert_equal_i(num_cbs, s_message_idx); \
}
static int s_dropped_idx;
static uint32_t s_dropped_messages[TEST_ARRAY_SIZE];
void test_dropped_handler(uint32_t num_dropped_messages) {
cl_assert(s_dropped_idx < TEST_ARRAY_SIZE);
s_dropped_messages[s_dropped_idx++] = num_dropped_messages;
}
#define assert_dropped(idx, num) \
{ \
cl_assert(idx <= s_dropped_idx); \
cl_assert_equal_i(num, s_dropped_messages[idx]); \
}
#define assert_num_dropped_callbacks(num_cbs) \
{ \
cl_assert_equal_i(num_cbs, s_dropped_idx); \
}
void test_alt_message_handler(const uint8_t *data, size_t length,
AppInboxConsumerInfo *consumer_info) {
cl_assert(false);
}
void test_alt_dropped_handler(uint32_t num_dropped_messages) {
cl_assert(false);
}
////////////////////////////////////////////////////////////////////////////////////////////////////
// Tests
void test_app_inbox__initialize(void) {
fake_kernel_malloc_init();
fake_kernel_malloc_enable_stats(true);
stub_pebble_tasks_set_current(TEST_TARGET_TASK);
s_num_messages_to_consume_from_handler = 0;
s_can_send_event = true;
s_dropped_idx = 0;
memset(s_dropped_messages, 0, sizeof(s_dropped_messages));
s_message_idx = 0;
memset(s_messages, 0, sizeof(s_messages));
}
void test_app_inbox__cleanup(void) {
app_inbox_service_unregister_all();
fake_kernel_malloc_deinit();
prv_cleanup_callback_events();
}
////////////////////////////////////////////////////////////////////////////////////////////////////
// app_inbox_create_and_register
void test_app_inbox__app_inbox_create_and_register_zero_buffer_size(void) {
void *result = app_inbox_create_and_register(0, 1, test_message_handler, test_dropped_handler);
cl_assert_equal_p(result, NULL);
}
void test_app_inbox__app_inbox_create_and_register_zero_min_num_messages(void) {
void *result = app_inbox_create_and_register(BUFFER_SIZE, 0,
test_message_handler, test_dropped_handler);
cl_assert_equal_p(result, NULL);
}
void test_app_inbox__app_inbox_create_and_register_null_message_handler(void) {
void *result = app_inbox_create_and_register(BUFFER_SIZE, 1, NULL, test_dropped_handler);
cl_assert_equal_p(result, NULL);
}
void test_app_inbox__app_inbox_create_and_register_oom(void) {
// FIXME: No support for OOM simulation in applib_.. stub/fake
return;
void *result = app_inbox_create_and_register(BUFFER_SIZE, 1,
test_message_handler,
test_dropped_handler);
cl_assert_equal_p(result, NULL);
}
void test_app_inbox__app_inbox_create_and_register_msg_handler_not_permitted(void) {
// The syscall_failed() fake will trigger passert:
cl_assert_passert(app_inbox_create_and_register(BUFFER_SIZE, 1,
NOT_PERMITTED_MSG_HANDLER,
test_dropped_handler));
}
void test_app_inbox__app_inbox_create_and_register_drop_handler_not_permitted(void) {
// The syscall_failed() fake will trigger passert:
cl_assert_passert(app_inbox_create_and_register(BUFFER_SIZE, 1,
test_message_handler,
NOT_PERMITTED_DROP_HANDLER));
}
void test_app_inbox__app_inbox_create_and_register_happy_case(void) {
void *result = app_inbox_create_and_register(BUFFER_SIZE, 1,
test_message_handler, test_dropped_handler);
cl_assert(result != NULL);
cl_assert_equal_b(true, app_inbox_service_has_inbox_for_tag(AppInboxServiceTagUnitTest));
}
void test_app_inbox__app_inbox_create_and_register_kernel_oom(void) {
fake_kernel_malloc_set_largest_free_block(0);
void *result = app_inbox_create_and_register(BUFFER_SIZE, 1,
test_message_handler,
test_dropped_handler);
cl_assert_equal_p(result, NULL);
}
////////////////////////////////////////////////////////////////////////////////////////////////////
// app_inbox_service_register
void test_app_inbox__app_inbox_create_and_register_storage_already_associated(void) {
bool success;
uint8_t storage[BUFFER_SIZE];
success = app_inbox_service_register(storage, sizeof(storage),
test_message_handler, test_dropped_handler,
AppInboxServiceTagUnitTest);
cl_assert_equal_b(success, true);
cl_assert_equal_b(true, app_inbox_service_has_inbox_for_storage(storage));
cl_assert_equal_b(true, app_inbox_service_has_inbox_for_tag(AppInboxServiceTagUnitTest));
fake_kernel_malloc_mark();
success = app_inbox_service_register(storage, sizeof(storage),
test_alt_message_handler, test_alt_dropped_handler,
AppInboxServiceTagUnitTestAlt);
cl_assert_equal_b(success, false);
cl_assert_equal_b(false, app_inbox_service_has_inbox_for_tag(AppInboxServiceTagUnitTestAlt));
fake_kernel_malloc_mark_assert_equal();
}
void test_app_inbox__app_inbox_create_and_register_tag_already_associated(void) {
bool success;
uint8_t storage[BUFFER_SIZE];
success = app_inbox_service_register(storage, sizeof(storage),
test_message_handler, test_dropped_handler,
AppInboxServiceTagUnitTest);
cl_assert_equal_b(success, true);
fake_kernel_malloc_mark();
uint8_t storage_alt[BUFFER_SIZE];
success = app_inbox_service_register(storage_alt, sizeof(storage_alt),
test_alt_message_handler, test_alt_dropped_handler,
AppInboxServiceTagUnitTest /* same tag! */);
cl_assert_equal_b(success, false);
cl_assert_equal_b(false, app_inbox_service_has_inbox_for_storage(storage_alt));
fake_kernel_malloc_mark_assert_equal();
}
////////////////////////////////////////////////////////////////////////////////////////////////////
// app_inbox_service_begin
static void *s_writer = (void *) 0xaabbccdd;
static void *s_inbox;
static void prv_create_test_inbox(void) {
s_inbox = app_inbox_create_and_register(BUFFER_SIZE, 1,
test_message_handler, test_dropped_handler);
cl_assert(s_inbox != NULL);
}
void test_app_inbox__app_inbox_service_begin_null_writer(void) {
prv_create_test_inbox();
cl_assert_equal_b(false, app_inbox_service_begin(AppInboxServiceTagUnitTest,
BUFFER_SIZE, NULL));
cl_assert_equal_b(false, app_inbox_service_is_being_written_for_tag(AppInboxServiceTagUnitTest));
}
void test_app_inbox__app_inbox_service_begin_no_inbox(void) {
cl_assert_equal_b(false, app_inbox_service_begin(AppInboxServiceTagUnitTest,
BUFFER_SIZE, s_writer));
cl_assert_equal_b(false, app_inbox_service_is_being_written_for_tag(AppInboxServiceTagUnitTest));
}
void test_app_inbox__app_inbox_service_begin_already_being_written(void) {
prv_create_test_inbox();
cl_assert_equal_b(true, app_inbox_service_begin(AppInboxServiceTagUnitTest,
BUFFER_SIZE, s_writer));
cl_assert_equal_b(true, app_inbox_service_is_being_written_for_tag(AppInboxServiceTagUnitTest));
// Call ...begin() again:
cl_assert_equal_b(false, app_inbox_service_begin(AppInboxServiceTagUnitTest,
BUFFER_SIZE, s_writer));
cl_assert_equal_b(true, app_inbox_service_is_being_written_for_tag(AppInboxServiceTagUnitTest));
cl_assert_equal_i(1, app_inbox_service_num_failed_for_tag(AppInboxServiceTagUnitTest));
}
void test_app_inbox__app_inbox_service_begin_not_enough_storage_space(void) {
prv_create_test_inbox();
cl_assert_equal_b(false, app_inbox_service_begin(AppInboxServiceTagUnitTest,
BUFFER_SIZE + 1, s_writer));
cl_assert_equal_b(false, app_inbox_service_is_being_written_for_tag(AppInboxServiceTagUnitTest));
cl_assert_equal_i(1, app_inbox_service_num_failed_for_tag(AppInboxServiceTagUnitTest));
// Drop should be reported immediately (not after the next write finishes):
prv_process_callback_events();
assert_dropped(0, 1);
}
void test_app_inbox__app_inbox_service_begin_happy_case(void) {
prv_create_test_inbox();
cl_assert_equal_b(true, app_inbox_service_begin(AppInboxServiceTagUnitTest,
BUFFER_SIZE, s_writer));
cl_assert_equal_b(true, app_inbox_service_is_being_written_for_tag(AppInboxServiceTagUnitTest));
}
////////////////////////////////////////////////////////////////////////////////////////////////////
// app_inbox_service_write / app_inbox_service_end
static const uint8_t s_test_data[] = {
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x09,
0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11,
0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19
};
static void prv_create_test_inbox_and_begin_write(void) {
prv_create_test_inbox();
cl_assert_equal_b(true, app_inbox_service_begin(AppInboxServiceTagUnitTest,
BUFFER_SIZE, s_writer));
}
void test_app_inbox__app_inbox_service_write_inbox_closed_in_mean_time(void) {
prv_create_test_inbox_and_begin_write();
app_inbox_destroy_and_deregister(s_inbox);
cl_assert_equal_b(false, app_inbox_service_write(AppInboxServiceTagUnitTest,
s_test_data, BUFFER_SIZE));
}
void test_app_inbox__app_inbox_service_write_not_enough_space(void) {
prv_create_test_inbox_and_begin_write();
cl_assert_equal_b(false, app_inbox_service_write(AppInboxServiceTagUnitTest,
s_test_data, BUFFER_SIZE + 1));
// A continuation should also fail, even though there is enough space for it:
cl_assert_equal_b(false, app_inbox_service_write(AppInboxServiceTagUnitTest,
s_test_data, 1));
// After ending the write, expect num_failed to be incremented by one:
app_inbox_service_end(AppInboxServiceTagUnitTest);
cl_assert_equal_i(1, app_inbox_service_num_failed_for_tag(AppInboxServiceTagUnitTest));
cl_assert_equal_i(0, app_inbox_service_num_success_for_tag(AppInboxServiceTagUnitTest));
prv_process_callback_events();
assert_num_dropped_callbacks(1);
assert_num_message_callbacks(0);
}
void test_app_inbox__app_inbox_service_write_happy_case(void) {
prv_create_test_inbox_and_begin_write();
cl_assert_equal_b(true, app_inbox_service_write(AppInboxServiceTagUnitTest,
s_test_data, BUFFER_SIZE));
// After ending the write, expect num_success to be incremented by one:
app_inbox_service_end(AppInboxServiceTagUnitTest);
cl_assert_equal_i(1, app_inbox_service_num_success_for_tag(AppInboxServiceTagUnitTest));
cl_assert_equal_i(0, app_inbox_service_num_failed_for_tag(AppInboxServiceTagUnitTest));
prv_process_callback_events();
assert_message(0, s_test_data, BUFFER_SIZE);
assert_num_message_callbacks(1);
assert_num_dropped_callbacks(0);
}
////////////////////////////////////////////////////////////////////////////////////////////////////
// app_inbox_service_cancel
void test_app_inbox__app_inbox_service_cancel(void) {
prv_create_test_inbox_and_begin_write();
// Start writing a message that occupies the complete buffer, then cancel it:
cl_assert_equal_b(true, app_inbox_service_write(AppInboxServiceTagUnitTest,
s_test_data, BUFFER_SIZE));
app_inbox_service_cancel(AppInboxServiceTagUnitTest);
// No events expected:
assert_num_callback_events(0);
// The buffer should be completely available again:
cl_assert_equal_b(true, app_inbox_service_begin(AppInboxServiceTagUnitTest,
BUFFER_SIZE, s_writer));
}
void test_app_inbox__app_inbox_service_cancel_non_existing_inbox(void) {
app_inbox_service_cancel(AppInboxServiceTagUnitTest);
// No events expected:
assert_num_callback_events(0);
}
////////////////////////////////////////////////////////////////////////////////////////////////////
// Consuming writes
void test_app_inbox__multiple_writes_while_consuming(void) {
prv_create_test_inbox_and_begin_write();
// Message 1:
cl_assert_equal_b(true, app_inbox_service_write(AppInboxServiceTagUnitTest, s_test_data, 1));
cl_assert_equal_b(true, app_inbox_service_end(AppInboxServiceTagUnitTest));
// Message 2:
cl_assert_equal_b(true, app_inbox_service_begin(AppInboxServiceTagUnitTest, 1, s_writer));
cl_assert_equal_b(true, app_inbox_service_write(AppInboxServiceTagUnitTest, s_test_data, 1));
cl_assert_equal_b(true, app_inbox_service_end(AppInboxServiceTagUnitTest));
// No space:
cl_assert_equal_b(false, app_inbox_service_begin(AppInboxServiceTagUnitTest,
BUFFER_SIZE + 1, s_writer));
// Shouldn't call ..._end() here because ..._begin() failed.
// Message 3:
cl_assert_equal_b(true, app_inbox_service_begin(AppInboxServiceTagUnitTest, 1, s_writer));
cl_assert_equal_b(true, app_inbox_service_write(AppInboxServiceTagUnitTest, s_test_data, 1));
// ... still writing when event gets processed below
// Only one callback event scheduled:
assert_num_callback_events(1);
prv_process_callback_events();
assert_num_callback_events(0);
// Expect 2 message callbacks and 1 drop callback:
assert_num_message_callbacks(2);
assert_message(0, s_test_data, 1);
assert_message(1, s_test_data, 1);
assert_num_dropped_callbacks(1);
assert_dropped(0, 1);
// Finish message 3, should be able to write (BUFFER_SIZE - 1) again,
// because the message 1 and 2 are consumed now:
cl_assert_equal_b(true, app_inbox_service_write(AppInboxServiceTagUnitTest, s_test_data + 1,
BUFFER_SIZE - 1));
cl_assert_equal_b(true, app_inbox_service_end(AppInboxServiceTagUnitTest));
// One callback event scheduled:
assert_num_callback_events(1);
prv_process_callback_events();
assert_num_callback_events(0);
// Expect 3rd message callbacks and still 1 drop callback (same as before):
assert_num_message_callbacks(3);
assert_message(2, s_test_data, BUFFER_SIZE);
assert_num_dropped_callbacks(1);
}
void test_app_inbox__multiple_writes_consume_from_message_handler(void) {
prv_create_test_inbox_and_begin_write();
// Message 1:
cl_assert_equal_b(true, app_inbox_service_write(AppInboxServiceTagUnitTest, s_test_data, 1));
cl_assert_equal_b(true, app_inbox_service_end(AppInboxServiceTagUnitTest));
// Message 2:
cl_assert_equal_b(true, app_inbox_service_begin(AppInboxServiceTagUnitTest, 1, s_writer));
cl_assert_equal_b(true, app_inbox_service_write(AppInboxServiceTagUnitTest, s_test_data, 1));
cl_assert_equal_b(true, app_inbox_service_end(AppInboxServiceTagUnitTest));
// Only one callback event scheduled:
assert_num_callback_events(1);
s_num_messages_to_consume_from_handler = 1;
prv_process_callback_events();
assert_num_callback_events(0);
// Expect 2 message callbacks and 1 drop callback:
assert_num_message_callbacks(2);
assert_message(0, s_test_data, 1);
assert_message(1, s_test_data, 1);
// Should be able to write (BUFFER_SIZE) again,
// because the message 1 and 2 are consumed now:
cl_assert_equal_b(true, app_inbox_service_begin(AppInboxServiceTagUnitTest,
BUFFER_SIZE, s_writer));
cl_assert_equal_b(true, app_inbox_service_write(AppInboxServiceTagUnitTest, s_test_data,
BUFFER_SIZE));
cl_assert_equal_b(true, app_inbox_service_end(AppInboxServiceTagUnitTest));
// One callback event scheduled:
assert_num_callback_events(1);
prv_process_callback_events();
assert_num_callback_events(0);
}
void test_app_inbox__consume_inbox_closed_in_mean_time(void) {
prv_create_test_inbox_and_begin_write();
cl_assert_equal_b(true, app_inbox_service_write(AppInboxServiceTagUnitTest, s_test_data, 1));
cl_assert_equal_b(true, app_inbox_service_end(AppInboxServiceTagUnitTest));
cl_assert_equal_i(1, app_inbox_destroy_and_deregister(s_inbox));
assert_num_callback_events(1);
prv_process_callback_events();
assert_num_dropped_callbacks(0);
assert_num_message_callbacks(0);
}
////////////////////////////////////////////////////////////////////////////////////////////////////
// app_inbox_destroy_and_deregister / app_inbox_service_unregister_by_storage
void test_app_inbox__app_inbox_destroy_and_deregister_cleans_up_kernel_heap(void) {
fake_kernel_malloc_mark();
void *result = app_inbox_create_and_register(BUFFER_SIZE, 1,
test_message_handler, test_dropped_handler);
cl_assert_equal_i(app_inbox_destroy_and_deregister(result), 0);
fake_kernel_malloc_mark_assert_equal();
}
void test_app_inbox__app_inbox_destroy_and_deregister_cleans_up_app_heap(void) {
// TODO: No allocation tracking ability in applib_... stub/fake :(
}
void test_app_inbox__app_inbox_service_end_inbox_closed_in_mean_time(void) {
prv_create_test_inbox_and_begin_write();
// Expect to return 1, because one message is being dropped, the currently written one:
cl_assert_equal_i(1, app_inbox_destroy_and_deregister(s_inbox));
cl_assert_equal_b(false, app_inbox_service_end(AppInboxServiceTagUnitTest));
}
void test_app_inbox__app_inbox_service_end_inbox_closed_in_mean_time_with_pending_success(void) {
prv_create_test_inbox_and_begin_write();
// One message:
cl_assert_equal_b(true, app_inbox_service_write(AppInboxServiceTagUnitTest,
s_test_data, 1));
cl_assert_equal_b(true, app_inbox_service_end(AppInboxServiceTagUnitTest));
// Begin another one:
cl_assert_equal_b(true, app_inbox_service_begin(AppInboxServiceTagUnitTest,
1, s_writer));
// Expect to return 2, because two messages are being dropped, the successful one that was not
// yet processed and the currently written one:
cl_assert_equal_i(2, app_inbox_destroy_and_deregister(s_inbox));
cl_assert_equal_b(false, app_inbox_service_end(AppInboxServiceTagUnitTest));
}
void test_app_inbox__app_inbox_service_end_inbox_closed_in_mean_time_with_pending_failure(void) {
prv_create_test_inbox_and_begin_write();
// One message, too large, so it should get dropped:
cl_assert_equal_b(false, app_inbox_service_write(AppInboxServiceTagUnitTest,
s_test_data, BUFFER_SIZE + 1));
cl_assert_equal_b(false, app_inbox_service_end(AppInboxServiceTagUnitTest));
// Begin another one:
cl_assert_equal_b(true, app_inbox_service_begin(AppInboxServiceTagUnitTest,
1, s_writer));
// Expect to return 2, because two messages are being dropped, the failed one that was not
// yet processed and the currently written one:
cl_assert_equal_i(2, app_inbox_destroy_and_deregister(s_inbox));
cl_assert_equal_b(false, app_inbox_service_end(AppInboxServiceTagUnitTest));
}
void test_app_inbox__app_inbox_service_unregister_by_storage_unknown_storage(void) {
uint8_t storage[BUFFER_SIZE];
cl_assert_equal_i(app_inbox_service_unregister_by_storage(storage), 0);
}
+802
View File
@@ -0,0 +1,802 @@
/*
* 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/app_message/app_message_internal.h"
#include "kernel/events.h"
#include "system/logging.h"
#include "util/attributes.h"
#include "util/math.h"
#include <stddef.h>
#include <limits.h>
extern AppTimer *app_message_outbox_get_ack_nack_timer(void);
// Stubs
////////////////////////////////////
#include "stubs_logging.h"
#include "stubs_passert.h"
#include "stubs_rand_ptr.h"
#include "fake_pbl_malloc.h"
// Fakes
////////////////////////////////////
#include "fake_app_timer.h"
#include "fake_pebble_tasks.h"
// Structures and Externs
////////////////////////////////////
typedef struct PACKED {
AppMessageCmd command:8;
uint8_t transaction_id;
union PACKED {
struct PACKED {
Uuid uuid;
Dictionary dictionary; //!< Variable length!
} push; //!< valid for CMD_PUSH only
struct PACKED {} ack;
} payload[];
} AppMessage;
extern AppTimer *app_message_ack_timer_id(void);
extern bool app_message_is_accepting_inbound(void);
extern bool app_message_is_accepting_outbound(void);
extern bool app_message_is_closed_inbound(void);
extern bool app_message_is_closed_outbound(void);
extern void app_message_monitor_reset(void);
// Globals
////////////////////////////////////
static const uint16_t ENDPOINT_ID = 0x30;
static const uint16_t MAX_SIZE_INBOUND = 32;
static const uint16_t MAX_SIZE_OUTBOUND = 32;
static const char *TEST_DATA = "01234567890123456789012345678901234567890123456789"
"0123456789012345678901234567890123456789";
static const uint32_t TEST_KEY = 0xbeefbabe;
static const uint8_t TEST_TRANSACTION_ID_1 = 0x11; // msgs with this ID are asserted to be ack'd
static const uint8_t TEST_TRANSACTION_ID_2 = 0x22; // msgs with this ID are asserted to be nack'd
static const uint16_t MAX_DATA_SIZE = MAX_SIZE_OUTBOUND - sizeof(Dictionary) - sizeof(Tuple);
static int s_context;
static DictionaryIterator s_expected_iter;
uint8_t s_expected_buffer[MAX_SIZE_OUTBOUND];
static int s_out_sent_call_count = 0;
static int s_out_failed_call_count = 0;
static AppMessageResult s_failure_result = APP_MSG_OK;
static bool s_ack_sent_is_called = false;
static bool s_nack_sent_is_called = false;
static bool s_in_received_is_called = false;
static bool s_in_dropped_is_called = false;
static bool s_ack_received_for_id_1 = false;
static bool s_nack_received_for_id_2 = false;
static AppMessageResult s_dropped_reason = APP_MSG_OK;
static AppMessageCtx s_app_message_ctx;
typedef void (*RemoteReceiveHandler)(uint16_t endpoint_id,
const uint8_t* data, unsigned int length);
static RemoteReceiveHandler s_remote_receive_handler;
// UUID: 6bf6215b-c97f-409e-8c31-4f55657222b4
static Uuid simplicity_uuid = (Uuid){ 0x6b, 0xf6, 0x21, 0x5b, 0xc9, 0x7f, 0x40, 0x9e,
0x8c, 0x31, 0x4f, 0x55, 0x65, 0x72, 0x22, 0xb4 };
static CommSession *s_fake_app_comm_session = (CommSession *) 0xaabbccdd;
static bool s_is_connected;
static bool s_is_app_message_receiver_open;
static Uuid s_app_uuid;
static Uuid s_remote_app_uuid;
static bool s_app_receiver_oom;
// Utils
////////////////////////////////////
static void prv_set_app_uuid(Uuid uuid) {
s_app_uuid = uuid;
}
static void prv_set_remote_app_uuid(Uuid uuid) {
s_remote_app_uuid = uuid;
}
//! @note Assumes same order of tuples in both dictionaries!
static void prv_assert_dict_equal(DictionaryIterator *a, DictionaryIterator *b) {
Tuple *a_tuple = dict_read_first(a);
Tuple *b_tuple = dict_read_first(b);
while (b_tuple && a_tuple) {
cl_assert_equal_i(a_tuple->key, b_tuple->key);
cl_assert_equal_i(a_tuple->length, b_tuple->length);
cl_assert_equal_i(a_tuple->type, b_tuple->type);
cl_assert_equal_m(a_tuple->value, b_tuple->value, a_tuple->length);
a_tuple = dict_read_next(a);
b_tuple = dict_read_next(b);
}
if (b_tuple) {
cl_fail("Dictionary `B` contained more tuples than dictionary `A`.");
} else if (a_tuple) {
cl_fail("Dictionary `A` contained more tuples than dictionary `B`.");
}
}
// Callbacks
////////////////////////////////////
static void prv_out_sent_callback(DictionaryIterator *sent, void *context) {
s_out_sent_call_count++;
cl_assert_equal_p(context, &s_context);
prv_assert_dict_equal(sent, &s_expected_iter);
// When the outbox sent callback is called, the outbox should be in the
// ACCEPTING state again.
cl_assert_equal_b(app_message_is_accepting_outbound(), true);
}
static void prv_out_failed_callback(DictionaryIterator *failed,
AppMessageResult reason, void *context) {
s_out_failed_call_count++;
cl_assert_equal_p(context, &s_context);
prv_assert_dict_equal(failed, &s_expected_iter);
s_failure_result = reason;
// When the outbox failed callback is called, the outbox should be in the
// ACCEPTING state again.
cl_assert_equal_b(app_message_is_accepting_outbound(), true);
}
static void prv_in_received_callback(DictionaryIterator *received, void *context) {
cl_assert_equal_p(context, &s_context);
prv_assert_dict_equal(received, &s_expected_iter);
s_in_received_is_called = true;
}
static void prv_in_dropped_callback(AppMessageResult reason, void *context) {
cl_assert_equal_p(context, &s_context);
cl_assert_equal_b(s_in_dropped_is_called, false);
s_in_dropped_is_called = true;
s_dropped_reason = reason;
}
static void prv_send_ack_nack(uint16_t endpoint_id, const uint8_t* data,
unsigned int length, bool nack) {
const int o = offsetof(AppMessage, payload[0].push.dictionary);
cl_assert_equal_i(length, o + dict_calc_buffer_size(1, MAX_DATA_SIZE));
CommSession *session = s_fake_app_comm_session;
AppMessage *message = (AppMessage*)data;
AppMessage ack = {
.command = nack ? CMD_NACK : CMD_ACK,
.transaction_id = message->transaction_id,
};
if (endpoint_id == ENDPOINT_ID) {
app_message_app_protocol_msg_callback(session, (const uint8_t*)&ack, sizeof(AppMessage), NULL);
} else {
cl_fail("Unhandled endpoint");
}
}
static void prv_nack_sent_callback(uint16_t endpoint_id, const uint8_t* data, unsigned int length) {
s_nack_sent_is_called = true;
prv_send_ack_nack(endpoint_id, data, length, true);
}
static void prv_ack_sent_callback(uint16_t endpoint_id, const uint8_t* data, unsigned int length) {
s_ack_sent_is_called = true;
prv_send_ack_nack(endpoint_id, data, length, false);
}
static void prv_receive_test_data(uint8_t transaction_id, const bool oversized) {
const uint16_t dict_length = dict_calc_buffer_size(1, MAX_DATA_SIZE);
const uint16_t message_length = offsetof(AppMessage, payload[0].push.dictionary) +
+ dict_length + (oversized ? 20 : 0);
uint8_t buffer[message_length];
AppMessage *message = (AppMessage*)buffer;
message->command = CMD_PUSH;
message->transaction_id = transaction_id;
message->payload->push.uuid = s_remote_app_uuid;
memcpy(&message->payload->push.dictionary, s_expected_buffer, dict_length);
PBL_LOG(LOG_LEVEL_DEBUG, "message->transaction_id = %"PRIu32, message->transaction_id);
CommSession *session = s_fake_app_comm_session;
app_message_app_protocol_msg_callback(session, buffer, message_length, NULL);
}
static void prv_receive_ack_nack_callback(uint16_t endpoint_id,
const uint8_t* data, unsigned int length) {
AppMessage *message = (AppMessage*)data;
cl_assert(length == sizeof(AppMessage));
PBL_LOG(LOG_LEVEL_DEBUG, "message %"PRIu32", id1 %"PRIu32", id2 %"PRIu32, message->transaction_id,
TEST_TRANSACTION_ID_1, TEST_TRANSACTION_ID_2);
if (message->transaction_id == TEST_TRANSACTION_ID_1) {
cl_assert_equal_b(s_ack_received_for_id_1, false);
s_ack_received_for_id_1 = true;
cl_assert_equal_i(message->command, CMD_ACK);
} else if (message->transaction_id == TEST_TRANSACTION_ID_2) {
cl_assert_equal_b(s_nack_received_for_id_2, false);
s_nack_received_for_id_2 = true;
cl_assert_equal_i(message->command, CMD_NACK);
} else {
cl_assert(false);
}
}
static void prv_no_reply_callback(uint16_t endpoint_id,
const uint8_t* data, unsigned int length) {
}
// Overrides
///////////////////////////////////
bool sys_app_pp_has_capability(CommSessionCapability capability) {
return true;
}
static int s_sys_psleep_last_millis;
void sys_psleep(int millis) {
s_sys_psleep_last_millis = millis;
}
AppMessageCtx *app_state_get_app_message_ctx(void) {
return &s_app_message_ctx;
}
bool app_message_receiver_open(size_t buffer_size) {
if (s_app_receiver_oom) {
return false;
}
s_is_app_message_receiver_open = true;
return true;
}
void app_message_receiver_close(void) {
s_is_app_message_receiver_open = false;
}
size_t sys_app_pp_app_message_inbox_size_maximum(void) {
return 600;
}
void sys_app_pp_app_message_analytics_count_drop(void) {
}
bool sys_get_current_app_is_js_allowed(void) {
return false;
}
Version sys_get_current_app_sdk_version(void) {
return (Version) {};
}
static uint16_t s_sent_endpoint_id;
static uint8_t *s_sent_data;
static uint16_t s_sent_data_length;
void prv_send_data(uint16_t endpoint_id, const uint8_t* data, uint16_t length) {
const size_t header_size =
(uintptr_t)(((AppMessage *)0)->payload[0].push.dictionary.head[0].value->data);
const uint16_t max_length = (header_size + MAX_DATA_SIZE);
if (length > max_length) {
// Using cl_assert_equal_i for the nicer printing.
// when getting at this point, it will always trip:
cl_assert_equal_i(length, max_length);
}
cl_assert_equal_p(s_sent_data, NULL);
s_sent_data = kernel_malloc(length);
cl_assert(s_sent_data);
memcpy(s_sent_data, data, length);
s_sent_data_length = length;
s_sent_endpoint_id = endpoint_id;
}
bool sys_app_pp_send_data(CommSession *session, uint16_t endpoint_id,
const uint8_t* data, uint16_t length) {
if (!s_is_connected) {
return false;
}
prv_send_data(endpoint_id, data, length);
return true;
}
static AppOutboxSentHandler s_app_outbox_sent_handler;
static void *s_app_outbox_ctx;
static void prv_call_outbox_sent(int status) {
cl_assert(s_app_outbox_sent_handler);
s_app_outbox_sent_handler(status, s_app_outbox_ctx);
}
void app_outbox_send(const uint8_t *data, size_t length,
AppOutboxSentHandler sent_handler, void *cb_ctx) {
if (!s_is_connected) {
sent_handler(AppOutboxStatusConsumerDoesNotExist, cb_ctx);
return;
}
s_app_outbox_sent_handler = sent_handler;
s_app_outbox_ctx = cb_ctx;
AppMessageAppOutboxData *outbox_data = (AppMessageAppOutboxData *)data;
prv_send_data(outbox_data->endpoint_id,
outbox_data->payload, length - sizeof(AppMessageAppOutboxData));
}
static void prv_process_sent_data(void) {
if (!s_sent_data) {
return;
}
if (!s_is_connected) {
return;
}
if (!s_is_app_message_receiver_open) {
return;
}
cl_assert(s_remote_receive_handler);
s_remote_receive_handler(s_sent_endpoint_id, s_sent_data, s_sent_data_length);
kernel_free(s_sent_data);
s_sent_data = NULL;
}
void sys_get_app_uuid(Uuid *uuid) {
cl_assert(uuid);
*uuid = s_app_uuid;
}
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) {
// Expecting the stub to be called only once durning a test:
cl_assert_equal_p(s_process_manager_callback, NULL);
cl_assert_equal_p(s_process_manager_callback_data, NULL);
s_process_manager_callback = async_cb;
s_process_manager_callback_data = ctx;
}
static int s_app_inbox_consume_call_count;
void app_inbox_consume(AppInboxConsumerInfo *consumer_info) {
++s_app_inbox_consume_call_count;
}
// Setup
////////////////////////////////////
void test_app_message__initialize(void) {
prv_set_app_uuid(simplicity_uuid);
prv_set_remote_app_uuid(simplicity_uuid);
fake_app_timer_init();
s_app_receiver_oom = false;
s_sys_psleep_last_millis = 0;
s_app_inbox_consume_call_count = 0;
app_message_init();
app_message_set_context(&s_context);
cl_assert_equal_i(app_message_open(MAX_SIZE_INBOUND, MAX_SIZE_OUTBOUND), APP_MSG_OK);
cl_assert_equal_p(app_message_register_outbox_sent(prv_out_sent_callback), NULL);
cl_assert_equal_p(app_message_register_outbox_failed(prv_out_failed_callback), NULL);
cl_assert_equal_p(app_message_register_inbox_dropped(prv_in_dropped_callback), NULL);
cl_assert_equal_p(app_message_register_inbox_received(prv_in_received_callback), NULL);
s_out_sent_call_count = 0;
s_out_failed_call_count = 0;
s_ack_sent_is_called = false;
s_nack_sent_is_called = false;
s_in_received_is_called = false;
s_in_dropped_is_called = false;
s_ack_received_for_id_1 = false;
s_nack_received_for_id_2 = false;
s_remote_receive_handler = NULL;
s_dropped_reason = APP_MSG_OK;
s_failure_result = APP_MSG_OK;
s_process_manager_callback = NULL;
s_process_manager_callback_data = NULL;
s_is_connected = true;
// Create the dictionary that is used to compare with what has been received:
dict_write_begin(&s_expected_iter, s_expected_buffer, MAX_SIZE_OUTBOUND);
cl_assert_equal_i(DICT_OK, dict_write_data(&s_expected_iter, TEST_KEY,
(const uint8_t*)TEST_DATA, MAX_DATA_SIZE));
dict_write_end(&s_expected_iter);
}
void test_app_message__cleanup(void) {
app_message_close();
cl_assert_equal_b(app_message_is_closed_inbound(), true);
cl_assert_equal_b(app_message_is_closed_outbound(), true);
fake_app_timer_deinit();
kernel_free(s_sent_data);
s_sent_data = NULL;
}
// Test OUTBOUND (watch->phone):
////////////////////////////////////
static void prv_send_test_data_expecting_result(AppMessageResult result) {
DictionaryIterator *iter;
cl_assert_equal_i(app_message_outbox_begin(&iter), APP_MSG_OK);
cl_assert_equal_i(dict_write_data(iter, TEST_KEY, (const uint8_t*)TEST_DATA, MAX_DATA_SIZE),
DICT_OK);
cl_assert_equal_i(app_message_outbox_send(), result);
}
static void prv_send_test_data(void) {
prv_send_test_data_expecting_result(APP_MSG_OK);
}
static void prv_set_remote_receive_handler(RemoteReceiveHandler handler) {
s_remote_receive_handler = handler;
}
void test_app_message__send_happy_case_outbox_sent_then_ack(void) {
prv_set_remote_receive_handler(prv_ack_sent_callback);
prv_send_test_data();
prv_call_outbox_sent(AppOutboxStatusSuccess);
prv_process_sent_data();
// After the ACK has been received, we should have been called
cl_assert_equal_b(s_ack_sent_is_called, true);
// Since that callback schedules another callback, we have to invoke
// system tasks again to get th actual callback to trigger.
cl_assert_equal_i(s_out_sent_call_count, 1);
// Check that the state is reset properly after everything
cl_assert_equal_b(app_message_is_accepting_outbound(), true);
}
void test_app_message__send_happy_case_ack_then_outbox_sent(void) {
prv_set_remote_receive_handler(prv_ack_sent_callback);
prv_send_test_data();
prv_process_sent_data();
// With certain PP transports (i.e. PPoGATT), the 'consuming' of the outbound data / outbox sent
// callback can fire after the AppMessage (N)ACK has been received.
cl_assert_equal_b(app_message_is_accepting_outbound(), false);
prv_call_outbox_sent(AppOutboxStatusSuccess);
// After the ACK has been received, we should have been called
cl_assert_equal_b(s_ack_sent_is_called, true);
// Since that callback schedules another callback, we have to invoke
// system tasks again to get th actual callback to trigger.
cl_assert_equal_i(s_out_sent_call_count, 1);
// Check that the state is reset properly after everything
cl_assert_equal_b(app_message_is_accepting_outbound(), true);
}
void test_app_message__cancel_timer(void) {
prv_set_remote_receive_handler(prv_ack_sent_callback);
prv_send_test_data();
prv_call_outbox_sent(AppOutboxStatusSuccess);
prv_process_sent_data();
// After the ACK has been received, we should have been called
cl_assert_equal_b(s_ack_sent_is_called, true);
// Check that we were called
cl_assert_equal_i(s_out_sent_call_count, 1);
// Timer should be invalid
cl_assert_equal_b(!fake_app_timer_is_scheduled(app_message_ack_timer_id()), true);
// Check the state is reset properly
cl_assert_equal_b(app_message_is_accepting_outbound(), true);
}
void test_app_message__send_ack_timeout(void) {
// We'll send the ack right after the timeout
prv_set_remote_receive_handler(prv_ack_sent_callback);
prv_send_test_data();
prv_call_outbox_sent(AppOutboxStatusSuccess);
// Fire the timeout and send the data
app_timer_trigger(app_message_ack_timer_id());
prv_process_sent_data();
cl_assert_equal_i(s_out_sent_call_count, 0);
cl_assert_equal_i(s_out_failed_call_count, 1);
cl_assert_equal_i(s_failure_result, APP_MSG_SEND_TIMEOUT);
// Check the state is reset properly
cl_assert_equal_b(app_message_is_accepting_outbound(), true);
}
void test_app_message__send_rejected(void) {
// Sending ack on timeout, but reject the send
prv_set_remote_receive_handler(prv_nack_sent_callback);
prv_send_test_data();
prv_call_outbox_sent(AppOutboxStatusSuccess);
prv_process_sent_data();
// Fire the ack timeout after receiving the nack
app_timer_trigger(app_message_ack_timer_id());
cl_assert_equal_b(s_nack_sent_is_called, true);
cl_assert_equal_i(s_out_sent_call_count, 0);
cl_assert_equal_i(s_out_failed_call_count, 1);
cl_assert_equal_i(s_failure_result, APP_MSG_SEND_REJECTED);
// Check the state is reset properly
cl_assert_equal_b(app_message_is_accepting_outbound(), true);
}
void test_app_message__nack_then_outbox_sent(void) {
// Sending ack on timeout, but reject the send
prv_set_remote_receive_handler(prv_nack_sent_callback);
prv_send_test_data();
prv_process_sent_data();
cl_assert_equal_b(app_message_is_accepting_outbound(), false);
prv_call_outbox_sent(AppOutboxStatusSuccess);
cl_assert_equal_b(s_nack_sent_is_called, true);
cl_assert_equal_i(s_out_sent_call_count, 0);
cl_assert_equal_i(s_out_failed_call_count, 1);
cl_assert_equal_i(s_failure_result, APP_MSG_SEND_REJECTED);
// Check the state is reset properly
cl_assert_equal_b(app_message_is_accepting_outbound(), true);
}
void test_app_message__busy(void) {
DictionaryIterator *iter;
prv_set_remote_receive_handler(prv_no_reply_callback);
prv_send_test_data();
prv_process_sent_data();
// Can't get or send again if still sending
cl_assert_equal_i(app_message_outbox_begin(&iter), APP_MSG_BUSY);
cl_assert_equal_i(app_message_outbox_send(), APP_MSG_BUSY);
// Can't get or send again if waiting on the ACK
cl_assert_equal_i(app_message_outbox_begin(&iter), APP_MSG_BUSY);
cl_assert_equal_i(app_message_outbox_send(), APP_MSG_BUSY);
}
void test_app_message__send_disconnected(void) {
prv_set_remote_receive_handler(prv_nack_sent_callback);
// Disconnect the comm session
s_is_connected = false;
// The return value should be APP_MSG_OK, even though we already know it's going to fail.
// The failure should be delivered after returning from app_message_outbox_send(), because
// some apps call .._send() again from the failed_callback.
prv_send_test_data_expecting_result(APP_MSG_OK);
// Make fake remote send any outstanding data (none expected)
prv_process_sent_data();
cl_assert_equal_i(s_out_sent_call_count, 0);
// failed_callback not called yet:
cl_assert_equal_i(s_out_failed_call_count, 0);
// Now process the scheduled callback event:
cl_assert(s_process_manager_callback);
s_process_manager_callback(s_process_manager_callback_data);
// Check that the ack/nack timer is removed:
cl_assert_equal_p(app_message_outbox_get_ack_nack_timer(), NULL);
cl_assert_equal_i(1, s_out_failed_call_count);
cl_assert_equal_i(s_failure_result, APP_MSG_NOT_CONNECTED);
cl_assert_equal_b(s_nack_sent_is_called, false);
// Check the state is reset properly
cl_assert_equal_b(app_message_is_accepting_outbound(), true);
}
void test_app_message__send_while_closing_and_while_being_disconnected(void) {
prv_set_remote_receive_handler(prv_nack_sent_callback);
prv_send_test_data();
// Disconnect the comm session and remove the
// app message context
s_is_connected = false;
app_message_close();
// Make fake remote send any outstanding data (none expected)
prv_process_sent_data();
// No app_message callbacks are expected to be called, as we closed the context
cl_assert_equal_i(s_out_sent_call_count, 0);
cl_assert_equal_b(s_nack_sent_is_called, false);
cl_assert_equal_i(s_out_failed_call_count, 0);
cl_assert_equal_b(app_message_is_closed_outbound(), true);
}
void test_app_message__send_while_closing(void) {
prv_set_remote_receive_handler(prv_ack_sent_callback);
prv_send_test_data();
// Close the AppMessage context
app_message_close();
// Make fake remote send the ack if something has been sent (not expected)
prv_process_sent_data();
// Test that timer has been invalidated
cl_assert_equal_b(!fake_app_timer_is_scheduled(app_message_ack_timer_id()), true);
cl_assert_equal_b(s_ack_sent_is_called, false);
cl_assert_equal_i(s_out_sent_call_count, 0);
cl_assert_equal_i(s_out_failed_call_count, 0);
cl_assert_equal_b(app_message_is_closed_outbound(), true);
cl_assert_equal_b(app_message_is_closed_inbound(), true);
}
void test_app_message__throttle_repeated_outbox_begin_calls(void) {
prv_set_remote_receive_handler(prv_no_reply_callback);
prv_send_test_data();
// Expect exponential back-off:
for (int i = 1; i <= 128; i *= 2) {
DictionaryIterator *iter;
cl_assert_equal_i(app_message_outbox_begin(&iter), APP_MSG_BUSY);
cl_assert_equal_i(MIN(i, 100), s_sys_psleep_last_millis);
}
}
// Test INBOUND (phone->watch):
////////////////////////////////////
static void check_in_accepting_again(void) {
cl_assert(app_message_is_accepting_inbound() == true);
}
void test_app_message__receive_happy_case(void) {
prv_set_remote_receive_handler(prv_receive_ack_nack_callback);
prv_receive_test_data(TEST_TRANSACTION_ID_1, false);
cl_assert_equal_i(s_app_inbox_consume_call_count, 1);
prv_process_sent_data();
// First Message
cl_assert(s_in_received_is_called == true);
// ACK the messages
// Check that state was reset properly
check_in_accepting_again();
}
void test_app_message__receive_dropped_because_buffer_too_small(void) {
// FIXME:
// https://pebbletechnology.atlassian.net/browse/PBL-22925
return;
prv_set_remote_receive_handler(prv_receive_ack_nack_callback);
prv_receive_test_data(TEST_TRANSACTION_ID_2, true);
// Message should be dropped due to buffer overflow
cl_assert_equal_b(s_in_dropped_is_called, true);
cl_assert_equal_b(s_in_received_is_called, false);
cl_assert_equal_i(s_dropped_reason, APP_MSG_BUFFER_OVERFLOW);
cl_assert_equal_b(s_nack_received_for_id_2, true);
// Check that the state was reset
check_in_accepting_again();
}
void test_app_message__receive_app_not_running(void) {
// FIXME:
// https://pebbletechnology.atlassian.net/browse/PBL-22925
return;
prv_set_remote_receive_handler(prv_receive_ack_nack_callback);
prv_receive_test_data(TEST_TRANSACTION_ID_2, false);
cl_assert_equal_b(s_in_received_is_called, false);
cl_assert_equal_b(s_in_dropped_is_called, false);
cl_assert_equal_b(s_nack_received_for_id_2, true);
// Check that the state is reset
check_in_accepting_again();
}
void test_app_message__receive_app_uuid_mismatch(void) {
// Change the current app uuid
prv_set_app_uuid(UuidMake(0xF6, 0x2C, 0xB7, 0xBA, 0x1B, 0x8D, 0x46, 0x10,
0xBE, 0xC5, 0xDE, 0xC6, 0x5A, 0xD3, 0x18, 0x29));
prv_set_remote_receive_handler(prv_receive_ack_nack_callback);
prv_receive_test_data(TEST_TRANSACTION_ID_2, false);
prv_process_sent_data();
cl_assert_equal_b(s_in_received_is_called, false);
cl_assert_equal_b(s_in_dropped_is_called, false);
cl_assert_equal_b(s_nack_received_for_id_2, true);
// Check that the state is reset
check_in_accepting_again();
}
void test_app_message__get_context(void) {
cl_assert_equal_p(app_message_get_context(), &s_context);
}
void test_app_message__open_while_already_open(void) {
cl_assert_equal_i(app_message_open(MAX_SIZE_INBOUND, MAX_SIZE_OUTBOUND), APP_MSG_INVALID_STATE);
}
void test_app_message__begin_while_already_begun(void) {
DictionaryIterator *iterator;
cl_assert_equal_i(app_message_outbox_begin(&iterator), APP_MSG_OK);
cl_assert_equal_i(app_message_outbox_begin(&iterator), APP_MSG_INVALID_STATE);
}
void test_app_message__begin_null_iterator(void) {
cl_assert_equal_i(app_message_outbox_begin(NULL), APP_MSG_INVALID_ARGS);
}
void test_app_message__send_while_not_begun(void) {
cl_assert_equal_i(app_message_outbox_send(), APP_MSG_INVALID_STATE);
}
void test_app_message__zero_inbox(void) {
app_message_close();
cl_assert_equal_i(app_message_open(0, MAX_SIZE_OUTBOUND), APP_MSG_OK);
cl_assert_equal_b(app_message_is_closed_inbound(), true);
cl_assert_equal_b(app_message_is_closed_outbound(), false);
}
void test_app_message__zero_outbox(void) {
app_message_close();
cl_assert_equal_i(app_message_open(MAX_SIZE_INBOUND, 0), APP_MSG_OK);
cl_assert_equal_b(app_message_is_closed_inbound(), false);
cl_assert_equal_b(app_message_is_closed_outbound(), true);
DictionaryIterator *iterator;
cl_assert_equal_i(app_message_outbox_begin(&iterator), APP_MSG_INVALID_STATE);
}
void test_app_message__oom(void) {
s_app_receiver_oom = true;
app_message_close();
cl_assert_equal_i(app_message_open(MAX_SIZE_INBOUND, MAX_SIZE_OUTBOUND), APP_MSG_OUT_OF_MEMORY);
cl_assert_equal_b(app_message_is_closed_inbound(), true);
cl_assert_equal_b(app_message_is_closed_outbound(), true);
}
void test_app_message__kernel_nack_handler(void) {
prv_set_remote_receive_handler(prv_receive_ack_nack_callback);
const AppMessagePush push = {
.header = {
.command = CMD_PUSH,
.transaction_id = TEST_TRANSACTION_ID_2,
},
};
app_message_app_protocol_system_nack_callback(s_fake_app_comm_session,
(const uint8_t *)&push, sizeof(push));
prv_process_sent_data();
cl_assert_equal_b(s_nack_received_for_id_2, true);
}
+274
View File
@@ -0,0 +1,274 @@
/*
* 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/app_outbox.h"
#include "applib/event_service_client.h"
#include "services/normal/app_outbox_service.h"
#include "clar.h"
extern void app_outbox_service_deinit(void);
extern uint32_t app_outbox_service_max_pending_messages(AppOutboxServiceTag tag);
extern uint32_t app_outbox_service_max_message_length(AppOutboxServiceTag tag);
////////////////////////////////////////////////////////////////////////////////////////////////////
// Fakes & Stubs
#include "fake_kernel_malloc.h"
#include "fake_pebble_tasks.h"
#include "stubs_logging.h"
#include "stubs_mutex.h"
#include "stubs_passert.h"
#include "stubs_syscall_internal.h"
static EventServiceInfo s_app_state_app_outbox_subscription_info;
EventServiceInfo *app_state_get_app_outbox_subscription_info(void) {
return &s_app_state_app_outbox_subscription_info;
}
void event_service_client_subscribe(EventServiceInfo * service_info) {
}
void sys_send_pebble_event_to_kernel(PebbleEvent* event) {
cl_assert_equal_i(event->type, PEBBLE_APP_OUTBOX_MSG_EVENT);
event->callback.callback(event->callback.data);
}
static int s_num_app_outbox_events_sent;
bool process_manager_send_event_to_process(PebbleTask task, PebbleEvent* e) {
cl_assert_equal_i(e->type, PEBBLE_APP_OUTBOX_SENT_EVENT);
cl_assert(e->app_outbox_sent.sent_handler);
e->app_outbox_sent.sent_handler(e->app_outbox_sent.status,
e->app_outbox_sent.cb_ctx);
++s_num_app_outbox_events_sent;
return true;
}
void app_message_outbox_handle_app_outbox_message_sent(AppOutboxStatus status, void *cb_ctx) {
}
////////////////////////////////////////////////////////////////////////////////////////////////////
// Helpers
static int s_num_message_handler_calls;
static AppOutboxMessage *s_last_message;
static void prv_message_handler(AppOutboxMessage *message) {
s_last_message = message;
++s_num_message_handler_calls;
}
static int s_num_sent_handler_called;
static AppOutboxStatus s_last_sent_status;
static void *s_expected_cb_ctx = (void *)0x77777777;
void test_app_outbox_sent_handler(AppOutboxStatus status, void *cb_ctx) {
cl_assert_equal_p(s_expected_cb_ctx, cb_ctx);
s_last_sent_status = status;
++s_num_sent_handler_called;
}
#define assert_sent_cb_last_status(expected_status) \
cl_assert_equal_i(s_last_sent_status, expected_status)
////////////////////////////////////////////////////////////////////////////////////////////////////
// Tests
static uint8_t *s_test_data;
static size_t s_test_data_length;
void test_app_outbox__initialize(void) {
fake_kernel_malloc_init();
fake_kernel_malloc_enable_stats(true);
s_test_data_length = app_outbox_service_max_message_length(AppOutboxServiceTagUnitTest);
s_test_data = kernel_malloc(s_test_data_length);
memset(s_test_data, 0x88, s_test_data_length);
s_num_sent_handler_called = 0;
s_num_app_outbox_events_sent = 0;
s_num_message_handler_calls = 0;
s_last_message = NULL;
stubs_syscall_init();
s_app_state_app_outbox_subscription_info = (EventServiceInfo) {};
// set to something that is not expected anywhere in the tests:
s_last_sent_status = AppOutboxStatusUserRangeEnd;
app_outbox_service_init();
app_outbox_init();
}
void test_app_outbox__cleanup(void) {
app_outbox_service_deinit();
kernel_free(s_test_data);
}
static size_t s_consumer_data_length = 1;
static void prv_register(void) {
app_outbox_service_register(AppOutboxServiceTagUnitTest,
prv_message_handler,
PebbleTask_KernelMain,
s_consumer_data_length);
}
void test_app_outbox__register_twice_asserts(void) {
prv_register();
cl_assert_passert(prv_register());
}
void test_app_outbox__send_not_user_space_buffer(void) {
// TODO: really implement privilege escalation in unit tests. See PBL-9688
return;
cl_assert_passert(app_outbox_send(NULL, s_test_data_length,
test_app_outbox_sent_handler, s_expected_cb_ctx));
assert_syscall_failed();
}
// Disallowed, because it's not white-listed in app_outbox_service.c
static void prv_disallowed_sent_handler(AppOutboxStatus status, void *cb_ctx) {
}
void test_app_outbox__send_disallowed_sent_handler(void) {
prv_register();
cl_assert_passert(app_outbox_send(s_test_data, s_test_data_length,
prv_disallowed_sent_handler, s_expected_cb_ctx));
assert_syscall_failed();
}
void test_app_outbox__send_max_length_exceeded(void) {
prv_register();
cl_assert_passert(app_outbox_send(s_test_data, s_test_data_length + 1,
test_app_outbox_sent_handler, s_expected_cb_ctx));
assert_syscall_failed();
}
void test_app_outbox__send_but_consumer_not_registered(void) {
prv_register();
app_outbox_service_unregister(AppOutboxServiceTagUnitTest);
app_outbox_send(s_test_data, s_test_data_length,
test_app_outbox_sent_handler, s_expected_cb_ctx);
assert_sent_cb_last_status(AppOutboxStatusConsumerDoesNotExist);
}
void test_app_outbox__send_but_max_pending_messages_reached(void) {
prv_register();
uint32_t max_pending_messages =
app_outbox_service_max_pending_messages(AppOutboxServiceTagUnitTest);
for (uint32_t i = 0; i < max_pending_messages; ++i) {
app_outbox_send(s_test_data, s_test_data_length,
test_app_outbox_sent_handler, s_expected_cb_ctx);
cl_assert_equal_i(s_num_sent_handler_called, 0);
}
app_outbox_send(s_test_data, s_test_data_length,
test_app_outbox_sent_handler, s_expected_cb_ctx);
assert_sent_cb_last_status(AppOutboxStatusOutOfResources);
}
void test_app_outbox__send_but_oom(void) {
prv_register();
fake_kernel_malloc_set_largest_free_block(0);
app_outbox_send(s_test_data, s_test_data_length,
test_app_outbox_sent_handler, s_expected_cb_ctx);
assert_sent_cb_last_status(AppOutboxStatusOutOfMemory);
}
void test_app_outbox__send_but_null_sent_handler(void) {
prv_register();
// Invalid data, so normally an event would get put to invoke the sent_handler,
// but sent handler is NULL. Expect no events to be put.
cl_assert_passert(app_outbox_send(NULL, 0, NULL, s_expected_cb_ctx));
cl_assert_equal_i(s_num_app_outbox_events_sent, 0);
}
void test_app_outbox__send(void) {
fake_kernel_malloc_mark();
prv_register();
uint32_t max_pending_messages =
app_outbox_service_max_pending_messages(AppOutboxServiceTagUnitTest);
AppOutboxMessage *message[max_pending_messages];
for (uint32_t i = 0; i < max_pending_messages; ++i) {
app_outbox_send(s_test_data, s_test_data_length,
test_app_outbox_sent_handler, s_expected_cb_ctx);
cl_assert_equal_i(s_num_app_outbox_events_sent, 0);
cl_assert_equal_i(s_num_message_handler_calls, i + 1);
cl_assert(s_last_message);
cl_assert_equal_p(s_test_data, s_last_message->data);
cl_assert_equal_i(s_test_data_length, s_last_message->length);
cl_assert_equal_b(false, app_outbox_service_is_message_cancelled(s_last_message));
message[i] = s_last_message;
}
for (uint32_t i = 0; i < max_pending_messages; ++i) {
app_outbox_service_consume_message(message[i], AppOutboxStatusSuccess);
cl_assert_equal_i(s_num_app_outbox_events_sent, i + 1);
assert_sent_cb_last_status(AppOutboxStatusSuccess);
}
fake_kernel_malloc_mark_assert_equal();
}
void test_app_outbox__unregister_with_pending_message(void) {
fake_kernel_malloc_mark();
prv_register();
app_outbox_send(s_test_data, s_test_data_length,
test_app_outbox_sent_handler, s_expected_cb_ctx);
cl_assert(s_last_message);
app_outbox_service_unregister(AppOutboxServiceTagUnitTest);
cl_assert_equal_i(s_num_app_outbox_events_sent, 1);
assert_sent_cb_last_status(AppOutboxStatusConsumerDoesNotExist);
cl_assert_equal_b(true, app_outbox_service_is_message_cancelled(s_last_message));
// the consumer must call ..._consume_message(), to free the resources:
app_outbox_service_consume_message(s_last_message, AppOutboxStatusSuccess);
// sent_handler shouldn't get called again, it's already been called:
cl_assert_equal_i(s_num_app_outbox_events_sent, 1);
fake_kernel_malloc_mark_assert_equal();
}
void test_app_outbox__cleanup_all_with_pending_message(void) {
fake_kernel_malloc_mark();
prv_register();
app_outbox_send(s_test_data, s_test_data_length,
test_app_outbox_sent_handler, s_expected_cb_ctx);
cl_assert(s_last_message);
app_outbox_service_cleanup_all_pending_messages();
// sent_handler shouldn't get called when cleaning up:
cl_assert_equal_i(s_num_app_outbox_events_sent, 0);
cl_assert_equal_b(true, app_outbox_service_is_message_cancelled(s_last_message));
// the consumer must call ..._consume_message(), to free the resources:
app_outbox_service_consume_message(s_last_message, AppOutboxStatusSuccess);
fake_kernel_malloc_mark_assert_equal();
}
+386
View File
@@ -0,0 +1,386 @@
/*
* 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/event_service_client.h"
#include "services/normal/accessory/smartstrap_attribute.h"
#include "kernel/pbl_malloc.h"
#include "fake_smartstrap_profiles.h"
#include "fake_smartstrap_state.h"
#include "fake_system_task.h"
#include "stubs_app_state.h"
#include "stubs_logging.h"
#include "stubs_mutex.h"
#include "stubs_passert.h"
#include "stubs_pbl_malloc.h"
#include "stubs_serial.h"
#define NON_NULL_MBUF ((MBuf *)1)
#define assert_result_ok(result) cl_assert(result == SmartstrapResultOk)
#define assert_result_invalid(result) cl_assert(result == SmartstrapResultInvalidArgs)
#define assert_result_busy(result) cl_assert(result == SmartstrapResultBusy)
typedef void (*EventServiceEventHandler)(PebbleEvent *e, void *context);
typedef struct {
bool active;
SmartstrapAttribute *attribute;
size_t length;
} PendingInfo;
static EventServiceEventHandler s_event_handler;
static PendingInfo s_pending_did_read;
static PendingInfo s_pending_did_write;
static PendingInfo s_pending_notified;
// Stubs / fakes
void event_service_client_subscribe(EventServiceInfo *info) {
cl_assert(info->type == PEBBLE_SMARTSTRAP_EVENT);
s_event_handler = info->handler;
}
void event_service_client_unsubscribe(EventServiceInfo *info) {
cl_assert(info->type == PEBBLE_SMARTSTRAP_EVENT);
s_event_handler = NULL;
}
bool process_manager_send_event_to_process(PebbleTask task, PebbleEvent *e) {
cl_assert(task == PebbleTask_App);
cl_assert(e->type == PEBBLE_SMARTSTRAP_EVENT);
cl_assert(s_event_handler);
s_event_handler(e, NULL);
return true;
}
void smartstrap_cancel_send(void) {
}
// Helper functions
static void prv_prepare_for_did_read(SmartstrapAttribute *attribute, uint16_t read_length) {
cl_assert(!s_pending_did_read.active);
s_pending_did_read = (PendingInfo) {
.active = true,
.attribute = attribute,
.length = read_length
};
}
static void prv_did_read_handler(SmartstrapAttribute *attribute, SmartstrapResult result,
const uint8_t *data, size_t length) {
cl_assert(data == (uint8_t *)attribute);
cl_assert(result == SmartstrapResultOk);
cl_assert(s_pending_did_read.active);
cl_assert(s_pending_did_read.attribute == attribute);
cl_assert(s_pending_did_read.length == length);
s_pending_did_read.active = false;
}
static void prv_prepare_for_did_write(SmartstrapAttribute *attribute) {
cl_assert(!s_pending_did_write.active);
s_pending_did_write = (PendingInfo) {
.active = true,
.attribute = attribute
};
}
static void prv_did_write_handler(SmartstrapAttribute *attribute, SmartstrapResult result) {
cl_assert(result == SmartstrapResultOk);
cl_assert(s_pending_did_write.active);
cl_assert(s_pending_did_write.attribute == attribute);
s_pending_did_write.active = false;
}
static void prv_prepare_for_notified(SmartstrapAttribute *attribute) {
cl_assert(!s_pending_notified.active);
s_pending_notified = (PendingInfo) {
.active = true,
.attribute = attribute
};
}
static void prv_notified_handler(SmartstrapAttribute *attribute) {
cl_assert(s_pending_notified.active);
cl_assert(s_pending_notified.attribute == attribute);
s_pending_notified.active = false;
}
// Setup
void test_app_smartstrap__initialize(void) {
smartstrap_attribute_init();
app_smartstrap_subscribe((SmartstrapHandlers) {
.did_read = prv_did_read_handler,
.did_write = prv_did_write_handler,
.notified = prv_notified_handler
});
}
void test_app_smartstrap__cleanup(void) {
}
// Tests
void test_app_smartstrap__invalid_args(void) {
// create test attribute
SmartstrapAttribute *attr = app_smartstrap_attribute_create(0x1111, 0x2222, 100);
// smartstrap_attribute_create()
cl_assert(app_smartstrap_attribute_create(0x1111, 0x2222, 0) == NULL);
// smartstrap_attribute_destroy()
app_smartstrap_attribute_destroy(NULL);
// smartstrap_attribute_get_*_id()
cl_assert(app_smartstrap_attribute_get_service_id(NULL) == 0);
cl_assert(app_smartstrap_attribute_get_attribute_id(NULL) == 0);
// smartstrap_attribute_begin_write()
uint8_t *buffer;
size_t buffer_len;
assert_result_invalid(app_smartstrap_attribute_begin_write(NULL, NULL, NULL));
assert_result_invalid(app_smartstrap_attribute_begin_write(NULL, &buffer, NULL));
assert_result_invalid(app_smartstrap_attribute_begin_write(NULL, NULL, &buffer_len));
assert_result_invalid(app_smartstrap_attribute_begin_write(NULL, &buffer, &buffer_len));
assert_result_invalid(app_smartstrap_attribute_begin_write(attr, NULL, NULL));
assert_result_invalid(app_smartstrap_attribute_begin_write(attr, &buffer, NULL));
assert_result_invalid(app_smartstrap_attribute_begin_write(attr, NULL, &buffer_len));
// smartstrap_attribute_end_write()
assert_result_invalid(app_smartstrap_attribute_end_write(NULL, 0, false));
assert_result_invalid(app_smartstrap_attribute_end_write(NULL, 0, true));
assert_result_invalid(app_smartstrap_attribute_end_write(NULL, 100, false));
assert_result_invalid(app_smartstrap_attribute_end_write(NULL, 100, true));
assert_result_invalid(app_smartstrap_attribute_end_write(attr, 0, false));
assert_result_invalid(app_smartstrap_attribute_end_write(attr, 0, true));
assert_result_invalid(app_smartstrap_attribute_end_write(attr, 100, false));
assert_result_invalid(app_smartstrap_attribute_end_write(attr, 100, true));
// smartstrap_attribute_read()
assert_result_invalid(app_smartstrap_attribute_read(NULL));
// destroy test attribute
app_smartstrap_attribute_destroy(attr);
}
void test_app_smartstrap__check_ids(void) {
// craete an attribute
SmartstrapAttribute *attr = app_smartstrap_attribute_create(0x1111, 0x2222, 100);
cl_assert(attr != NULL);
// verify the ids
cl_assert(app_smartstrap_attribute_get_service_id(attr) == 0x1111);
cl_assert(app_smartstrap_attribute_get_attribute_id(attr) == 0x2222);
// destroy the attribute
app_smartstrap_attribute_destroy(attr);
// verify that we can no longer get the ids
cl_assert(app_smartstrap_attribute_get_service_id(attr) == 0);
cl_assert(app_smartstrap_attribute_get_attribute_id(attr) == 0);
}
void test_app_smartstrap__create_duplicate(void) {
// create the attribute once
SmartstrapAttribute *attr = app_smartstrap_attribute_create(0x1111, 0x2222, 100);
cl_assert(attr != NULL);
// try to create it again
SmartstrapAttribute *attr_dup = app_smartstrap_attribute_create(0x1111, 0x2222, 100);
cl_assert(attr_dup == NULL);
// destroy the attribute
app_smartstrap_attribute_destroy(attr);
// destroy again (shouldn't do anything bad)
app_smartstrap_attribute_destroy(attr);
}
void test_app_smartstrap__read(void) {
// create the attribute
SmartstrapAttribute *attr = app_smartstrap_attribute_create(0x1111, 0x2222, 100);
// start a read request
stub_pebble_tasks_set_current(PebbleTask_App);
assert_result_ok(app_smartstrap_attribute_read(attr));
// attempt to issue another read request
assert_result_busy(app_smartstrap_attribute_read(attr));
// trigger the read request to be sent and expect a did_write handler call
stub_pebble_tasks_set_current(PebbleTask_KernelBackground);
prv_prepare_for_did_write(attr);
cl_assert(smartstrap_attribute_send_pending());
cl_assert(!s_pending_did_write.active);
// attempt to issue another read request (should report busy)
assert_result_busy(app_smartstrap_attribute_read(attr));
// check that it was sent successfully
SmartstrapRequest request = {
.service_id = 0x1111,
.attribute_id = 0x2222,
.write_mbuf = NULL,
.read_mbuf = NON_NULL_MBUF,
.timeout_ms = SMARTSTRAP_TIMEOUT_DEFAULT
};
fake_smartstrap_profiles_check_request_params(&request);
// fake the response and expect a did_read handler call
prv_prepare_for_did_read(attr, 10);
smartstrap_attribute_send_event(SmartstrapDataReceivedEvent, SmartstrapProfileGenericService,
SmartstrapResultOk, 0x1111, 0x2222, 10);
cl_assert(!s_pending_did_read.active);
// destroy the attribute
app_smartstrap_attribute_destroy(attr);
}
void test_app_smartstrap__write(void) {
// create the attribute
SmartstrapAttribute *attr = app_smartstrap_attribute_create(0x1111, 0x2222, 100);
// start a write request
stub_pebble_tasks_set_current(PebbleTask_App);
uint8_t *write_buffer = NULL;
size_t write_length = 0;
assert_result_ok(app_smartstrap_attribute_begin_write(attr, &write_buffer, &write_length));
cl_assert(write_buffer == (uint8_t *)attr);
cl_assert(write_length == 100);
// attempt to start another write request
assert_result_busy(app_smartstrap_attribute_read(attr));
uint8_t *write_buffer2 = NULL;
size_t write_length2 = 0;
assert_result_busy(app_smartstrap_attribute_begin_write(attr, &write_buffer2, &write_length2));
cl_assert(write_buffer2 == NULL);
cl_assert(write_length2 == 0);
// end the write request without sending anything
assert_result_invalid(app_smartstrap_attribute_end_write(attr, 0, false));
// start the write request again
write_buffer = NULL;
write_length = 0;
assert_result_ok(app_smartstrap_attribute_begin_write(attr, &write_buffer, &write_length));
cl_assert(write_buffer == (uint8_t *)attr);
cl_assert(write_length == 100);
// end the write request
assert_result_ok(app_smartstrap_attribute_end_write(attr, 100, false));
// trigger the write request to be sent
stub_pebble_tasks_set_current(PebbleTask_KernelBackground);
cl_assert(smartstrap_attribute_send_pending());
// check that it was sent successfully
SmartstrapRequest request = {
.service_id = 0x1111,
.attribute_id = 0x2222,
.write_mbuf = NON_NULL_MBUF,
.read_mbuf = NULL,
.timeout_ms = SMARTSTRAP_TIMEOUT_DEFAULT
};
// fake the ACK and expect a did_write handler call
fake_smartstrap_profiles_check_request_params(&request);
prv_prepare_for_did_write(attr);
smartstrap_attribute_send_event(SmartstrapDataReceivedEvent, SmartstrapProfileGenericService,
SmartstrapResultOk, 0x1111, 0x2222, 100);
cl_assert(!s_pending_did_write.active);
// destroy the attribute
app_smartstrap_attribute_destroy(attr);
}
void test_app_smartstrap__write_read(void) {
// create the attribute
SmartstrapAttribute *attr = app_smartstrap_attribute_create(0x1111, 0x2222, 100);
// start a write request
stub_pebble_tasks_set_current(PebbleTask_App);
uint8_t *write_buffer = NULL;
size_t write_length = 0;
assert_result_ok(app_smartstrap_attribute_begin_write(attr, &write_buffer, &write_length));
cl_assert(write_buffer == (uint8_t *)attr);
cl_assert(write_length == 100);
// end the write request without sending anything
assert_result_invalid(app_smartstrap_attribute_end_write(attr, 0, true));
// start the write request again
write_buffer = NULL;
write_length = 0;
assert_result_ok(app_smartstrap_attribute_begin_write(attr, &write_buffer, &write_length));
cl_assert(write_buffer == (uint8_t *)attr);
cl_assert(write_length == 100);
// end the write request with request_read=true
assert_result_ok(app_smartstrap_attribute_end_write(attr, 100, true));
// trigger the write request to be sent and expect a did_write handler call
stub_pebble_tasks_set_current(PebbleTask_KernelBackground);
prv_prepare_for_did_write(attr);
cl_assert(smartstrap_attribute_send_pending());
cl_assert(!s_pending_did_write.active);
// check that it was sent successfully
SmartstrapRequest request = {
.service_id = 0x1111,
.attribute_id = 0x2222,
.write_mbuf = NON_NULL_MBUF,
.read_mbuf = NON_NULL_MBUF,
.timeout_ms = SMARTSTRAP_TIMEOUT_DEFAULT
};
// fake the response and expect a did_write and a did_read handler call
fake_smartstrap_profiles_check_request_params(&request);
prv_prepare_for_did_read(attr, 100);
smartstrap_attribute_send_event(SmartstrapDataReceivedEvent, SmartstrapProfileGenericService,
SmartstrapResultOk, 0x1111, 0x2222, 100);
cl_assert(!s_pending_did_read.active);
// destroy the attribute
app_smartstrap_attribute_destroy(attr);
}
void test_app_smartstrap__notify(void) {
// create the attribute
SmartstrapAttribute *attr = app_smartstrap_attribute_create(0x1111, 0x2222, 100);
// send a notification and expect a notified handler call
prv_prepare_for_notified(attr);
smartstrap_attribute_send_event(SmartstrapNotifyEvent, SmartstrapProfileGenericService,
SmartstrapResultOk, 0x1111, 0x2222, 0);
cl_assert(!s_pending_notified.active);
// send a notification for a non-created attribute which shouldn't cause a notified handler call
smartstrap_attribute_send_event(SmartstrapNotifyEvent, SmartstrapProfileGenericService,
SmartstrapResultOk, 0x1111, 0x3333, 0);
// destroy the attribute
app_smartstrap_attribute_destroy(attr);
// send a notification for the destroyed attribute which shouldn't cause a notified handler call
smartstrap_attribute_send_event(SmartstrapNotifyEvent, SmartstrapProfileGenericService,
SmartstrapResultOk, 0x1111, 0x2222, 0);
}
+96
View File
@@ -0,0 +1,96 @@
/*
* 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/graphics/graphics.h"
#include "applib/ui/bitmap_layer.h"
// Stubs
/////////////////////
#include "stubs_app_state.h"
#include "stubs_graphics.h"
#include "stubs_graphics_context.h"
#include "stubs_heap.h"
#include "stubs_logging.h"
#include "stubs_passert.h"
#include "stubs_pbl_malloc.h"
#include "stubs_pebble_tasks.h"
#include "stubs_resources.h"
#include "stubs_syscalls.h"
#include "stubs_ui_window.h"
#include "stubs_unobstructed_area.h"
// Fakes
/////////////////////
static GRect s_graphics_draw_bitmap_in_rect__rect = GRectZero;
void graphics_draw_bitmap_in_rect(GContext* ctx, const GBitmap *src_bitmap, const GRect *rect) {
s_graphics_draw_bitmap_in_rect__rect = *rect;
}
bool process_manager_compiled_with_legacy2_sdk(void) {
return cl_mock_type(bool);
}
// Test boilerplate
/////////////////////
void test_bitmap_layer__initialize(void) {
}
void test_bitmap_layer__cleanup(void) {
}
// Tests
//////////////////////
// Test inspired by PBL-19136. Check that bitmaps get drawn in the right rect
// on recent SDKs but that a previous bug is kept for 2.x SDK
void test_bitmap_layer__nonzero_bounds(void) {
GContext ctx = {
.draw_state = (GDrawState) {
.clip_box = GRect(0, 0, 144, 168),
.drawing_box = GRect(0, 0, 144, 168),
},
};
static const GRect BITMAP_LAYER_FRAME = GRect(0, 0, 640, 64);
static const GRect BITMAP_LAYER_BOUNDS = GRect(-32, 0, 640, 64);
static const GRect BITMAP_BOUNDS = GRect(0, 0, 640, 64);
GBitmap bitmap = {
.bounds = BITMAP_BOUNDS,
};
BitmapLayer layer;
bitmap_layer_init(&layer, &BITMAP_LAYER_FRAME);
bitmap_layer_set_bitmap(&layer, &bitmap);
// set bounds with non-zero origin
layer_set_bounds((Layer *)&layer, &BITMAP_LAYER_BOUNDS);
// !legacy2
cl_will_return(process_manager_compiled_with_legacy2_sdk, false);
layer_render_tree((Layer *)&layer, &ctx);
GRect expected_rect = BITMAP_BOUNDS;
cl_assert(grect_equal(&s_graphics_draw_bitmap_in_rect__rect, &expected_rect));
// legacy2
cl_will_return(process_manager_compiled_with_legacy2_sdk, true);
layer_render_tree((Layer *)&layer, &ctx);
expected_rect = BITMAP_LAYER_BOUNDS;
cl_assert(grect_equal(&s_graphics_draw_bitmap_in_rect__rect, &expected_rect));
}
+256
View File
@@ -0,0 +1,256 @@
/*
* 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/cpu_cache.h"
#include "mcu/cache.h"
#include "clar.h"
// Fakes
//////////////////////////////////////////////////////////
static uintptr_t s_user_start, s_user_size;
static size_t s_flush_size, s_invalidate_size;
static uintptr_t s_flush_addr, s_invalidate_addr;
typedef enum {
UserSpaceBuffer_Unchecked,
UserSpaceBuffer_Valid,
UserSpaceBuffer_Invalid,
UserSpaceBuffer_NotRun,
} UserSpaceBufferValidity;
static UserSpaceBufferValidity s_addr_result;
uint32_t dcache_line_size(void) {
return cl_mock_type(size_t);
}
uint32_t icache_line_size(void) {
return cl_mock_type(size_t);
}
bool dcache_is_enabled(void) {
return cl_mock_type(bool);
}
bool icache_is_enabled(void) {
return cl_mock_type(bool);
}
void icache_invalidate(void *addr, size_t size) {
s_invalidate_size = size;
s_invalidate_addr = (uintptr_t)addr;
}
void dcache_flush(const void *addr, size_t size) {
s_flush_size = size;
s_flush_addr = (uintptr_t)addr;
}
bool syscall_internal_check_return_address(void * ret_addr) {
s_addr_result = UserSpaceBuffer_Unchecked;
return cl_mock_type(bool);
}
void syscall_assert_userspace_buffer(const void* buf, size_t num_bytes) {
uintptr_t addr = (uintptr_t)buf;
uintptr_t end = addr + num_bytes - 1;
if ((addr >= s_user_start) && (addr < (s_user_start + s_user_size)) &&
(end >= s_user_start) && (end < (s_user_start + s_user_size))) {
s_addr_result = UserSpaceBuffer_Valid;
} else {
s_addr_result = UserSpaceBuffer_Invalid;
}
}
// Tests
////////////////////////////////////
void test_cpu_cache__alignment(void) {
cl_will_return(icache_is_enabled, true);
cl_will_return(dcache_is_enabled, true);
cl_will_return(icache_line_size, 8);
cl_will_return(dcache_line_size, 16);
cl_will_return(syscall_internal_check_return_address, true);
s_user_start = 0x00;
s_user_size = 0x20;
memory_cache_flush((void*)0x0F, 0x2);
cl_assert_equal_i(s_flush_addr, 0x00);
cl_assert_equal_i(s_flush_size, 0x20);
cl_assert_equal_i(s_invalidate_addr, 0x00);
cl_assert_equal_i(s_invalidate_size, 0x20);
cl_assert_equal_i(s_addr_result, UserSpaceBuffer_Valid);
}
void test_cpu_cache__userspace_fail_from_size(void) {
cl_will_return(icache_is_enabled, true);
cl_will_return(dcache_is_enabled, true);
cl_will_return(icache_line_size, 1);
cl_will_return(dcache_line_size, 1);
cl_will_return(syscall_internal_check_return_address, true);
s_user_start = 0x00;
s_user_size = 0x20;
memory_cache_flush((void*)0x1F, 0x2);
cl_assert_equal_i(s_flush_addr, 0x1F);
cl_assert_equal_i(s_flush_size, 0x02);
cl_assert_equal_i(s_invalidate_addr, 0x1F);
cl_assert_equal_i(s_invalidate_size, 0x02);
cl_assert_equal_i(s_addr_result, UserSpaceBuffer_Invalid);
}
void test_cpu_cache__userspace_fail_from_addr(void) {
cl_will_return(icache_is_enabled, true);
cl_will_return(dcache_is_enabled, true);
cl_will_return(icache_line_size, 1);
cl_will_return(dcache_line_size, 1);
cl_will_return(syscall_internal_check_return_address, true);
s_user_start = 0x00;
s_user_size = 0x20;
memory_cache_flush((void*)0x20, 0x1);
cl_assert_equal_i(s_flush_addr, 0x20);
cl_assert_equal_i(s_flush_size, 0x01);
cl_assert_equal_i(s_invalidate_addr, 0x20);
cl_assert_equal_i(s_invalidate_size, 0x01);
cl_assert_equal_i(s_addr_result, UserSpaceBuffer_Invalid);
}
void test_cpu_cache__userspace_aligned_fail(void) {
cl_will_return(icache_is_enabled, true);
cl_will_return(dcache_is_enabled, true);
cl_will_return(icache_line_size, 8);
cl_will_return(dcache_line_size, 8);
cl_will_return(syscall_internal_check_return_address, true);
s_user_start = 0x24;
s_user_size = 0x20;
memory_cache_flush((void*)0x26, 0x2);
cl_assert_equal_i(s_flush_addr, 0x20);
cl_assert_equal_i(s_flush_size, 0x08);
cl_assert_equal_i(s_invalidate_addr, 0x20);
cl_assert_equal_i(s_invalidate_size, 0x08);
cl_assert_equal_i(s_addr_result, UserSpaceBuffer_Invalid);
}
void test_cpu_cache__userspace_ignore(void) {
cl_will_return(icache_is_enabled, true);
cl_will_return(dcache_is_enabled, true);
cl_will_return(icache_line_size, 1);
cl_will_return(dcache_line_size, 1);
cl_will_return(syscall_internal_check_return_address, false);
s_user_start = 0x00;
s_user_size = 0x04;
memory_cache_flush((void*)0x00, 0x10);
cl_assert_equal_i(s_flush_addr, 0x00);
cl_assert_equal_i(s_flush_size, 0x10);
cl_assert_equal_i(s_invalidate_addr, 0x00);
cl_assert_equal_i(s_invalidate_size, 0x10);
cl_assert_equal_i(s_addr_result, UserSpaceBuffer_Unchecked);
}
void test_cpu_cache__without_icache(void) {
cl_will_return(icache_is_enabled, false);
cl_will_return(dcache_is_enabled, true);
cl_will_return(icache_line_size, 1);
cl_will_return(dcache_line_size, 1);
s_invalidate_addr = s_flush_addr = 0xAA55;
s_invalidate_size = s_flush_size = 0x55AA;
cl_will_return(syscall_internal_check_return_address, true);
s_user_start = 0x00;
s_user_size = 0x20;
memory_cache_flush((void*)0x20, 0x1);
cl_assert_equal_i(s_flush_addr, 0x20);
cl_assert_equal_i(s_flush_size, 0x01);
cl_assert_equal_i(s_invalidate_addr, 0xAA55);
cl_assert_equal_i(s_invalidate_size, 0x55AA);
cl_assert_equal_i(s_addr_result, UserSpaceBuffer_Invalid);
}
void test_cpu_cache__without_dcache(void) {
cl_will_return(icache_is_enabled, true);
cl_will_return(dcache_is_enabled, false);
cl_will_return(icache_line_size, 1);
cl_will_return(dcache_line_size, 1);
s_invalidate_addr = s_flush_addr = 0xAA55;
s_invalidate_size = s_flush_size = 0x55AA;
cl_will_return(syscall_internal_check_return_address, true);
s_user_start = 0x00;
s_user_size = 0x20;
memory_cache_flush((void*)0x20, 0x1);
cl_assert_equal_i(s_flush_addr, 0xAA55);
cl_assert_equal_i(s_flush_size, 0x55AA);
cl_assert_equal_i(s_invalidate_addr, 0x20);
cl_assert_equal_i(s_invalidate_size, 0x01);
cl_assert_equal_i(s_addr_result, UserSpaceBuffer_Invalid);
}
void test_cpu_cache__without_cache(void) {
cl_will_return(icache_is_enabled, false);
cl_will_return(dcache_is_enabled, false);
cl_will_return(icache_line_size, 1);
cl_will_return(dcache_line_size, 1);
s_invalidate_addr = s_flush_addr = 0xAA55;
s_invalidate_size = s_flush_size = 0x55AA;
cl_will_return(syscall_internal_check_return_address, true);
s_user_start = 0x00;
s_user_size = 0x20;
memory_cache_flush((void*)0x20, 0x1);
cl_assert_equal_i(s_flush_addr, 0xAA55);
cl_assert_equal_i(s_flush_size, 0x55AA);
cl_assert_equal_i(s_invalidate_addr, 0xAA55);
cl_assert_equal_i(s_invalidate_size, 0x55AA);
cl_assert_equal_i(s_addr_result, UserSpaceBuffer_Invalid);
}
+185
View File
@@ -0,0 +1,185 @@
/*
* 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/pbl_std/pbl_std.h"
#include "applib/pbl_std/locale.h"
#include "clar.h"
// Stubs
//////////////////////////////////////////////////////////
#include "stubs_heap.h"
#include "stubs_hexdump.h"
#include "stubs_logging.h"
#include "stubs_mutex.h"
#include "stubs_passert.h"
#include "stubs_pbl_malloc.h"
#include "stubs_pebble_tasks.h"
#include "stubs_print.h"
#include "stubs_prompt.h"
#include "stubs_serial.h"
#include "stubs_sleep.h"
#include "stubs_syscall_internal.h"
#include "stubs_system_reset.h"
#include "stubs_task_watchdog.h"
#include "stubs_app_state.h"
#include "stubs_worker_state.h"
// Overrides
//////////////////////////////////////////////////////////
void sys_get_time_ms(time_t *t, uint16_t *out_ms) {}
time_t sys_time_utc_to_local(time_t t) {
return t;
}
int localized_strftime(char* s, size_t maxsize, const char* format,
const struct tm* tim_p, char *locale) { return 0; }
const char *get_timezone_abbr(void) {
static const char s_timezone_abbr[] = "A";
return s_timezone_abbr;
}
void sys_copy_timezone_abbr(char* timezone_abbr, time_t time) {
const char* sys_tz = get_timezone_abbr();
strncpy(timezone_abbr, sys_tz, TZ_LEN);
}
struct tm *sys_gmtime_r(const time_t *timep, struct tm *result) {
return gmtime_r(timep, result);
}
struct tm *sys_localtime_r(const time_t *timep, struct tm *result) {
return localtime_r(timep, result);
}
// Tests
////////////////////////////////////
void test_pbl_std__get_id(void) {
const int STR_SIZE = 100;
char str[STR_SIZE];
// This is the message we should get back if we try and use floating point
const char* fp_msg = "floating point not supported in snprintf";
//----------------------------------------------------
pbl_snprintf(str, STR_SIZE, "%", 1); // Make sure we don't barf if no type
cl_assert_equal_s(str, "");
pbl_snprintf(str, STR_SIZE, "%%", 1);
cl_assert_equal_s(str, "%");
//----------------------------------------------------
pbl_snprintf(str, STR_SIZE, "%f", 1.0);
cl_assert_equal_s(str, fp_msg);
//----------------------------------------------------
pbl_snprintf(str, STR_SIZE, "%s%f%s", "a", 1.0, "b");
cl_assert_equal_s(str, fp_msg);
pbl_snprintf(str, STR_SIZE, "%s%d%s", "a", 1, "b");
cl_assert_equal_s(str, "a1b");
//----------------------------------------------------
pbl_snprintf(str, STR_SIZE, "abc%s %0.1f%s", "a", 1.0, "b");
cl_assert_equal_s(str, fp_msg);
pbl_snprintf(str, STR_SIZE, "abc%s %d%s", "a", 42, "b");
cl_assert_equal_s(str, "abca 42b");
//----------------------------------------------------
pbl_snprintf(str, STR_SIZE, "abc %3g", 1.0);
cl_assert_equal_s(str, fp_msg);
pbl_snprintf(str, STR_SIZE, "abc %3d", 42);
cl_assert_equal_s(str, "abc 42");
//----------------------------------------------------
pbl_snprintf(str, STR_SIZE, "%d %0.12G%s", 4, 1.0, "b");
cl_assert_equal_s(str, fp_msg);
pbl_snprintf(str, STR_SIZE, "%d %td%s", 4, 42, "b");
cl_assert_equal_s(str, "4 42b");
//----------------------------------------------------
pbl_snprintf(str, STR_SIZE, "ab%%c % E zz", 1.0);
cl_assert_equal_s(str, fp_msg);
pbl_snprintf(str, STR_SIZE, "ab%%c % d zz", 42);
cl_assert_equal_s(str, "ab%c 42 zz");
//----------------------------------------------------
pbl_snprintf(str, STR_SIZE, "abc %-5e%s", "a", 1.0, "b");
cl_assert_equal_s(str, fp_msg);
pbl_snprintf(str, STR_SIZE, "abc %-5d%s", 42, "b");
cl_assert_equal_s(str, "abc 42 b");
//----------------------------------------------------
pbl_snprintf(str, STR_SIZE, "abc %+f%s", "a", 1.0, "b");
cl_assert_equal_s(str, fp_msg);
pbl_snprintf(str, STR_SIZE, "abc %+d%s", 42, "b");
cl_assert_equal_s(str, "abc +42b");
//----------------------------------------------------
pbl_snprintf(str, STR_SIZE, "abc %lf%s", "a", 1.0, "b");
cl_assert_equal_s(str, fp_msg);
pbl_snprintf(str, STR_SIZE, "abc %ld%s", 42, "b");
cl_assert_equal_s(str, "abc 42b");
//----------------------------------------------------
pbl_snprintf(str, STR_SIZE, "abc %Lf%s", "a", 1.0, "b");
cl_assert_equal_s(str, fp_msg);
pbl_snprintf(str, STR_SIZE, "abc %Ld%s", 42, "b");
cl_assert_equal_s(str, "abc 42b");
//----------------------------------------------------
pbl_snprintf(str, STR_SIZE, "abc %hf%s", "a", 1.0, "b");
cl_assert_equal_s(str, fp_msg);
pbl_snprintf(str, STR_SIZE, "abc %hd%s", 42, "b");
cl_assert_equal_s(str, "abc 42b");
//----------------------------------------------------
pbl_snprintf(str, STR_SIZE, "abc %a%s", "a", 1.0, "b");
cl_assert_equal_s(str, fp_msg);
pbl_snprintf(str, STR_SIZE, "abc %jd%s", 42, "b");
cl_assert_equal_s(str, "abc 42b");
//----------------------------------------------------
pbl_snprintf(str, STR_SIZE, "abc %A%s", "a", 1.0, "b");
cl_assert_equal_s(str, fp_msg);
pbl_snprintf(str, STR_SIZE, "abc %zd%s", 42, "b");
cl_assert_equal_s(str, "abc 42b");
}
void test_pbl_std__verify_memcpy_handles_bogus_parameters(void) {
// See PBL-7873
uint8_t from = 1;
uint8_t to;
// Make sure a normal copy works
pbl_memcpy(&to, &from, sizeof(from));
cl_assert_equal_i(to, 1);
// Make sure a copy with a negative size is a no-op.
to = 0;
pbl_memcpy(&to, &from, -sizeof(from));
cl_assert_equal_i(to, 0);
}
void test_pbl_std__verify_difftime_double_conversion(void) {
// Can only test positive diffs because of 64 bit vs 32 bit time_t
cl_assert_equal_i(pbl_override_difftime(30, 10), 20);
cl_assert_equal_i(pbl_override_difftime(22222222, 1), 22222222 - 1);
cl_assert_equal_i(pbl_override_difftime(0, 0), 0);
cl_assert_equal_i(pbl_override_difftime(1, 0), 1);
cl_assert_equal_i(pbl_override_difftime(2147483647, 0), 2147483647);
}
+293
View File
@@ -0,0 +1,293 @@
/*
* 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 <stdio.h>
#include <string.h>
#include "applib/persist.h"
#include "flash_region/flash_region.h"
#include "process_management/app_install_manager.h"
#include "process_management/pebble_process_md.h"
#include "services/normal/filesystem/pfs.h"
#include "services/normal/persist.h"
#include "system/logging.h"
// Stubs
////////////////////////////////////
#include "fake_rtc.h"
#include "fake_spi_flash.h"
#include "stubs_analytics.h"
#include "stubs_hexdump.h"
#include "stubs_logging.h"
#include "stubs_mutex.h"
#include "stubs_passert.h"
#include "stubs_pbl_malloc.h"
#include "stubs_pebble_tasks.h"
#include "stubs_print.h"
#include "stubs_prompt.h"
#include "stubs_rand_ptr.h"
#include "stubs_serial.h"
#include "stubs_sleep.h"
#include "stubs_system_reset.h"
#include "stubs_task_watchdog.h"
static PebbleProcessMd __pbl_app_info;
const PebbleProcessMd* sys_process_manager_get_current_process_md(void) {
return &__pbl_app_info;
}
// Tests
////////////////////////////////////
#define TEST_UUID_A { 0x2F, 0xF7, 0xFA, 0x04, 0x60, 0x11, 0x4A, 0x98, 0x8A, 0x3B, 0xA8, 0x26, 0xA4, 0xB8, 0x99, 0xF8 }
static const int system_uuid_id = 0;
static const Uuid system_uuid = UUID_SYSTEM;
static const int test_uuid_a_id = 1;
static const Uuid test_uuid_a = TEST_UUID_A;
static const int test_uuid_b_id = 2;
static const Uuid test_uuid_b = { 0xC3, 0x0D, 0xBA, 0xF1, 0x5F, 0x6F, 0x4F, 0x22, 0xBA, 0xAA, 0x8C, 0x2A, 0x96, 0x8C, 0xFC, 0x28 };
static const int test_uuid_c_id = 3;
static const Uuid test_uuid_c = { 0x1D, 0x6C, 0x7F, 0x01, 0xD9, 0x48, 0x42, 0xA6, 0xAA, 0x4E, 0xB2, 0x08, 0x42, 0x10, 0xEB, 0xBC };
static PebbleProcessMd __pbl_app_info = {
.uuid = TEST_UUID_A,
};
const char lipsum[] = "Lorem ipsum dolor sit amet, consectetur "
"adipiscing elit. Nam dignissim ullamcorper sollicitudin. Suspendisse at "
"urna suscipit, congue purus a, posuere eros. Nulla eros urna, vestibulum "
"a dictum a, maximus sed nibh. Ut ut dui finibus, tincidunt ligula quis, "
"ornare mi. Pellentesque sagittis suscipit lacus nec consectetur. Nunc et "
"commodo neque. Vestibulum vitae dignissim sapien. Nulla scelerisque "
"finibus nisl. Suspendisse ac massa lacus. In hac habitasse platea "
"dictumst. Ut condimentum urna eros. Fusce ipsum metus, vehicula eu tortor "
"sed, congue tempus mauris. Maecenas mollis lacus non cursus bibendum. "
"Etiam id dolor lorem. Aenean scelerisque nulla sed tristique posuere. "
"Proin dui magna, gravida faucibus ultricies non, tincidunt id metus. "
"Integer a laoreet dolor, eu vulputate enim. Ut vitae hendrerit nunc, in "
"bibendum eros. Pellentesque congue ut quam id sollicitudin. Cras "
"malesuada arcu nec imperdiet cursus. Donec vitae ex eget mi imperdiet "
"efficitur id eu velit. Proin pretium ipsum sed convallis efficitur. Morbi "
"non feugiat erat. Ut ut efficitur massa. Sed eu auctor felis. Vestibulum "
"magna orci, placerat nec risus nec, ultricies congue ex. Morbi in "
"vestibulum leo. Nullam non dapibus lorem. Suspendisse blandit diam "
"posuere suscipit malesuada. Maecenas vehicula felis eu posuere euismod. "
"Fusce at velit ultrices, sagittis enim ac, ultrices lorem. Quisque "
"tincidunt fringilla suscipit. Curabitur tempus lorem metus, sed venenatis "
"augue maximus a. Duis venenatis tortor sit amet justo sodales suscipit. "
"Morbi tincidunt rutrum nisl, eget placerat nisi condimentum a. Vestibulum "
"ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia "
"Curae; Cras varius sagittis mauris, in consequat sapien tincidunt vitae. "
"Duis ipsum nunc, tristique sit amet blandit non, scelerisque non diam. "
"Etiam condimentum aliquam dictum. Nam nisi ex, cursus in ligula sit amet, "
"ultricies egestas libero. Aliquam luctus, metus quis ultricies sagittis, "
"nisi orci viverra felis, vitae luctus massa dolor sit amet dolor. Cras "
"mattis velit vitae pretium pulvinar. Pellentesque auctor, turpis at cras "
"amet.";
_Static_assert(sizeof(lipsum) > PERSIST_STRING_MAX_LENGTH,
"lipsum string is not long enough for persist tests");
void test_persist__initialize(void) {
fake_spi_flash_init(0, 0x1000000);
pfs_init(false);
persist_service_init();
persist_service_client_open(&test_uuid_a);
}
void test_persist__cleanup(void) {
persist_service_client_close(&test_uuid_a);
}
void test_persist__int(void) {
const uint32_t key = 0;
const uint32_t value = ~0;
cl_assert_equal_i(persist_read_int(key), 0);
cl_assert_equal_i(persist_write_int(key, value), sizeof(int));
cl_assert_equal_i(persist_get_size(key), sizeof(int));
cl_assert_equal_i(persist_read_int(key), value);
}
void test_persist__bool(void) {
const uint32_t key = 0;
cl_assert_equal_i(persist_read_bool(key), false);
cl_assert_equal_i(persist_write_bool(key, true), sizeof(bool));
cl_assert_equal_i(persist_get_size(key), sizeof(bool));
cl_assert_equal_i(persist_read_bool(key), true);
}
void test_persist__data(void) {
const uint32_t key = 0;
const int size = sizeof(test_uuid_a);
Uuid uuid_buffer;
cl_assert_equal_i(persist_read_data(key, &uuid_buffer, sizeof(uuid_buffer)), E_DOES_NOT_EXIST);
cl_assert_equal_i(persist_write_data(key, &test_uuid_a, sizeof(test_uuid_a)), size);
cl_assert_equal_i(persist_get_size(key), size);
cl_assert_equal_i(persist_read_data(key, &uuid_buffer, sizeof(uuid_buffer)), size);
cl_assert(uuid_equal(&test_uuid_a, &uuid_buffer));
}
void test_persist__data_too_big(void) {
char buf[PERSIST_DATA_MAX_LENGTH+2];
memset(buf, '~', sizeof(buf));
cl_assert_equal_i(persist_write_data(0, lipsum, sizeof(lipsum)),
PERSIST_DATA_MAX_LENGTH);
cl_assert_equal_i(persist_read_data(0, buf, sizeof(buf)),
PERSIST_DATA_MAX_LENGTH);
cl_assert(memcmp(lipsum, buf, PERSIST_DATA_MAX_LENGTH) == 0);
for (size_t i = PERSIST_DATA_MAX_LENGTH; i < sizeof(buf); ++i) {
cl_assert_(buf[i] == '~',
"persist_read_data writes past the end of destination buffer");
}
}
void test_persist__string_does_not_exist(void) {
char string_buffer[PERSIST_STRING_MAX_LENGTH];
memset(string_buffer, '~', sizeof(string_buffer));
cl_assert_equal_i(
persist_read_string(0, string_buffer, sizeof(string_buffer)),
E_DOES_NOT_EXIST);
for (size_t i = 0; i < sizeof(string_buffer); ++i) {
if (string_buffer[i] != '~') {
char error_msg[132];
snprintf(error_msg, sizeof(error_msg), "persist_read_string clobbers "
"destination buffer at %zd when key does not exist", i);
cl_fail(error_msg);
}
}
}
void test_persist__string_write_unterminated_string(void) {
char string_buffer[PERSIST_STRING_MAX_LENGTH + 2];
memset(string_buffer, '~', sizeof(string_buffer));
cl_assert_equal_i(persist_write_string(0, lipsum), PERSIST_STRING_MAX_LENGTH);
cl_assert_equal_i(persist_get_size(0), PERSIST_STRING_MAX_LENGTH);
cl_assert_equal_i(
persist_read_string(0, string_buffer, sizeof(string_buffer)),
PERSIST_STRING_MAX_LENGTH);
cl_assert_equal_i(string_buffer[PERSIST_STRING_MAX_LENGTH - 1], '\0');
cl_assert(strncmp(lipsum, string_buffer, PERSIST_STRING_MAX_LENGTH - 1) == 0);
for (size_t i = PERSIST_STRING_MAX_LENGTH; i < sizeof(string_buffer); ++i) {
cl_assert_(string_buffer[i] == '~',
"persist_read_string writes past the end of destination buffer");
}
}
void test_persist__size_of_nonexistent_key(void) {
cl_assert_equal_i(persist_get_size(0), E_DOES_NOT_EXIST);
}
void test_persist__size(void) {
char data[] = { 1, 2, 3, 4, 5, 6 };
cl_assert_equal_i(persist_write_data(0, data, sizeof(data)), sizeof(data));
cl_assert_equal_i(persist_get_size(0), sizeof(data));
}
void test_persist__exists(void) {
cl_assert_equal_i(persist_exists(0), S_FALSE);
cl_assert(PASSED(persist_write_int(0, 0)));
cl_assert_equal_i(persist_exists(0), S_TRUE);
}
void test_persist__delete(void) {
cl_assert_equal_i(persist_delete(0), E_DOES_NOT_EXIST);
cl_assert(PASSED(persist_write_int(0, 0)));
cl_assert_equal_i(persist_delete(0), S_TRUE);
cl_assert_equal_i(persist_delete(0), E_DOES_NOT_EXIST);
}
/*
* Confirm that fields can be reassigned values.
*/
void test_persist__overwrite(void) {
const uint32_t key = 0;
cl_assert_equal_i(persist_write_int(key, 1), sizeof(int));
cl_assert_equal_i(persist_read_int(key), 1);
cl_assert_equal_i(persist_write_int(key, 2), sizeof(int));
cl_assert_equal_i(persist_read_int(key), 2);
}
/*
* Confirm that overwriting with a smaller data size does not break tuple finding.
*/
void test_persist__overwrite_shrink(void) {
cl_assert(PASSED(persist_write_int(0, 1)));
cl_assert(PASSED(persist_write_bool(0, false)));
cl_assert(PASSED(persist_write_int(1, 2)));
cl_assert(persist_read_int(1) == 2);
}
/*
* Confirm that loading a smaller amount of data, then a larger amount of data,
* always returns the appropriate amount of data.
*/
void test_persist__partial_read_extension(void) {
char buffer[] = "Hello thar";
// Write out data
cl_assert_equal_i(persist_write_string(0, buffer), sizeof(buffer));
// Clear the cache (which has the entire string right now)
persist_service_client_close(&test_uuid_a);
persist_service_client_open(&test_uuid_a);
// Reset out buffer so we can check the read's honesty.
memset(buffer, 0, strlen(buffer));
// Read part of the data we wrote
cl_assert_equal_i(persist_read_string(0, buffer, 2), 2);
cl_assert_equal_i(strlen(buffer), 2 - 1); // -1 because null termination
cl_assert_equal_i(buffer[0], 'H');
// Then attempt to read back the entire thing
cl_assert_equal_i(persist_read_string(0, buffer, sizeof(buffer)), sizeof(buffer));
cl_assert_equal_i(strlen(buffer), 10);
cl_assert(strcmp(buffer, "Hello thar") == 0);
}
void test_persist__legacy2_max_usage(void) {
uint8_t buffer[256];
memset(buffer, 1, sizeof(buffer));
// The maximum amount of 'buffer' sized fields allowed by the old persist
// storage backend.
int n = (4 * 1024) / (9 + sizeof(buffer));
PBL_LOG_VERBOSE("n = %d", n);
for (int i = 0; i < n; ++i) {
PBL_LOG(LOG_LEVEL_DEBUG, "i = %d", i);
cl_assert_equal_i(persist_write_data(i, &buffer, sizeof(buffer)), sizeof(buffer));
}
// Don't be too strict about preventing apps from using more persist than they
// had available under the old implementation.
// cl_assert_equal_i(persist_write_data(n + 1, &buffer, sizeof(buffer)),
// E_OUT_OF_STORAGE);
}
+753
View File
@@ -0,0 +1,753 @@
/*
* 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/template_string.h"
#include "applib/template_string_private.h"
#include "util/size.h"
#include <limits.h>
#include <string.h>
#define DEBUG_PRINTING 1
#if DEBUG_PRINTING
# include <stdio.h>
#endif
void prv_template_evaluate_filter(TemplateStringState *state, const char *filter_name,
const char *params);
static const char *s_error_strings[] = {
"Success.",
"Can't resolve.",
"Missing closing brace.",
"Missing argument.",
"No result generated.",
"Unknown filter.",
"format() was not last filter.",
"Time unit in predicate is invalid.",
"Escape character at end of string.",
"Opening parenthesis for filter was missing.",
"Closing parenthesis for filter was missing.",
"Invalid conversion specifier for format.",
"Invalid parameter.",
"Opening quote for filter was missing.",
"Closing quote for filter was missing.",
"Invalid argument separator.",
};
////////////////////////////////////////////////////////////////////////////////////////////////////
// Fakes & Stubs
#include "stubs_passert.h"
#include "stubs_i18n.h"
////////////////////////////////////////////////////////////////////////////////////////////////////
// Test setup
#define EVAL_FALL_THROUGH -1337
#define EVAL_DEFAULT 0
static TemplateStringState s_state;
static char s_output[256];
static TemplateStringVars s_vars;
static TemplateStringError s_error;
static TemplateStringEvalConditions s_cond;
static void prv_state_init(void) {
s_state = (TemplateStringState){
.output = s_output,
.output_remaining = sizeof(s_output),
.vars = &s_vars,
.error = &s_error,
.eval_cond = &s_cond,
.time_was_until = false,
.filter_state = 0,
.filters_complete = false,
};
memset(s_output, 'Z', sizeof(s_output));
memset(&s_vars, 0, sizeof(s_vars));
memset(&s_error, 0, sizeof(s_error));
memset(&s_cond, 0, sizeof(s_cond));
s_cond.eval_time = INT_MAX;
}
void test_template_string__initialize(void) {
}
void test_template_string__cleanup(void) {
}
////////////////////////////////////////////////////////////////////////////////////////////////////
// Test truncation
static const struct {
size_t size;
time_t intime;
const char *instr;
const char *output;
} s_truncation_tests[] = {
{ .size = 1, .intime = 1000,
.instr = "foo",
.output = "",
},
{ .size = 3, .intime = 1000,
.instr = "foo",
.output = "fo",
},
{ .size = 3, .intime = 1000,
.instr = "{format('foo')}",
.output = "fo",
},
{ .size = 1, .intime = 1000,
.instr = "{time_until(1004)|format('%S')}",
.output = "",
},
{ .size = 2, .intime = 1000,
.instr = "{time_until(1040)|format('%S')}",
.output = "4",
},
{ .size = 6, .intime = 1000,
.instr = "{time_until(1040)|format('%uS')}",
.output = "40 se",
},
};
void test_template_string__truncation(void) {
for(size_t i = 0; i < ARRAY_LENGTH(s_truncation_tests); i++) {
#if DEBUG_PRINTING
printf("size: %zu\n", s_truncation_tests[i].size);
printf("intime: %jd\n", s_truncation_tests[i].intime);
printf("input: \"%s\"\n", s_truncation_tests[i].instr);
printf("result: \"%s\"\n", s_truncation_tests[i].output);
#endif
TemplateStringVars vars = {};
TemplateStringError err = {};
TemplateStringEvalConditions cond = {};
vars.current_time = s_truncation_tests[i].intime;
cond.eval_time = EVAL_FALL_THROUGH;
memset(s_output, 'Z', sizeof(s_output));
template_string_evaluate(s_truncation_tests[i].instr, s_output, s_truncation_tests[i].size,
&cond, &vars, &err);
cl_assert_equal_s(s_output, s_truncation_tests[i].output);
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////
// Test NULL arguments
void test_template_string__null_arguments(void) {
{
TemplateStringVars vars = {};
TemplateStringError err = {};
TemplateStringEvalConditions cond = {};
vars.current_time = 0;
cond.eval_time = EVAL_FALL_THROUGH;
cond.force_eval_on_time = true;
memset(s_output, 'Z', sizeof(s_output));
bool ret = template_string_evaluate("test string {time_until(5)|format('%uS')}", s_output,
sizeof(s_output), &cond, &vars, &err);
cl_assert_equal_b(ret, true);
cl_assert_equal_s(s_output, "test string 5 seconds");
cl_assert_equal_i(cond.eval_time, 1);
cl_assert_equal_b(cond.force_eval_on_time, true);
}
{
TemplateStringVars vars = {};
TemplateStringError err = {};
TemplateStringEvalConditions cond = {};
vars.current_time = 0;
cond.eval_time = EVAL_FALL_THROUGH;
cond.force_eval_on_time = true;
strcpy(s_output, "hurf");
bool ret = template_string_evaluate("test string {time_until(5)|format('%uS')}", s_output, 0,
&cond, &vars, &err);
cl_assert_equal_b(ret, true);
cl_assert_equal_s(s_output, "hurf");
cl_assert_equal_i(cond.eval_time, 1);
cl_assert_equal_b(cond.force_eval_on_time, true);
}
{
TemplateStringVars vars = {};
TemplateStringError err = {};
TemplateStringEvalConditions cond = {};
vars.current_time = 0;
cond.eval_time = EVAL_FALL_THROUGH;
cond.force_eval_on_time = true;
strcpy(s_output, "hurf");
bool ret = template_string_evaluate("test string {time_until(5)|format('%uS')}", NULL,
sizeof(s_output), &cond, &vars, &err);
cl_assert_equal_b(ret, true);
cl_assert_equal_s(s_output, "hurf");
cl_assert_equal_i(cond.eval_time, 1);
cl_assert_equal_b(cond.force_eval_on_time, true);
}
{
TemplateStringVars vars = {};
TemplateStringError err = {};
vars.current_time = 0;
memset(s_output, 'Z', sizeof(s_output));
bool ret = template_string_evaluate("test string {time_until(5)|format('%uS')}", s_output,
sizeof(s_output), NULL, &vars, &err);
cl_assert_equal_b(ret, true);
cl_assert_equal_s(s_output, "test string 5 seconds");
}
{
TemplateStringVars vars = {};
TemplateStringError err = {};
vars.current_time = 0;
bool ret = template_string_evaluate("test string {time_until(5)|format('%uS',)}", NULL, 0,
NULL, &vars, &err);
cl_assert_equal_b(ret, false);
cl_assert_equal_i(err.status, TemplateStringErrorStatus_MissingArgument);
cl_assert_equal_i(err.index_in_string, 40);
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////
// Test `time_since` and `time_until` filters
static const struct {
time_t current_time;
const char *params;
intmax_t done_state;
} s_time_since_tests[] = {
{ .current_time = 1234567,
.params = "1234567)",
.done_state = 0,
},
{ .current_time = 1234567,
.params = "1234560)",
.done_state = 7,
},
{ .current_time = 1234567,
.params = "1234570)",
.done_state = -3,
},
{ .current_time = 234567,
.params = "1234567)",
.done_state = -1000000,
},
};
void test_template_string__time_since_until(void) {
for(size_t i = 0; i < ARRAY_LENGTH(s_time_since_tests); i++) {
#if DEBUG_PRINTING
printf("current_time: %ld\n", s_time_since_tests[i].current_time);
printf("parameter: \"%s\"\n", s_time_since_tests[i].params);
printf("result: %jd\n", s_time_since_tests[i].done_state);
#endif
prv_state_init();
s_vars.current_time = s_time_since_tests[i].current_time;
prv_template_evaluate_filter(&s_state, "time_since", s_time_since_tests[i].params);
cl_assert_equal_b(s_state.filters_complete, false);
cl_assert_equal_i(s_state.filter_state, s_time_since_tests[i].done_state);
prv_state_init();
s_vars.current_time = s_time_since_tests[i].current_time;
prv_template_evaluate_filter(&s_state, "time_until", s_time_since_tests[i].params);
cl_assert_equal_b(s_state.filters_complete, false);
cl_assert_equal_i(s_state.filter_state, -s_time_since_tests[i].done_state);
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////
// Test `format` filter
typedef struct FormatTestData {
const char *params;
intmax_t filter_state;
bool time_was_until;
const char *expect_str;
time_t expect_eval_time;
TemplateStringErrorStatus expect_status;
size_t expect_index;
} FormatTestData;
static const FormatTestData s_format_tests[] = {
// Simple text tests
{ "'doo')", 3600, true,
"doo",
INT_MAX,
},
// Some error testing
{ ">5H'%T')", 1, true,
"",
INT_MAX,
TemplateStringErrorStatus_InvalidTimeUnit,
3,
},
{ "'%T',)", 1, true,
"1",
1,
TemplateStringErrorStatus_MissingArgument,
5,
},
{ "'%T'fj)", 1, true,
"1",
1,
TemplateStringErrorStatus_InvalidArgumentSeparator,
4,
},
// Basic %T tests
{ "'%T')", 1, true,
"1",
1,
},
{ "'%T')", 60, true,
"1:00",
1,
},
{ "'%T')", 3600, true,
"1:00:00",
1,
},
{ "'%T')", -3666, true,
"-1:01:06",
1,
},
// Basic %R tests
{ "'%R')", 1, true,
"0",
2,
},
{ "'%R')", 66, true,
"1",
7,
},
{ "'%R')", 3607, true,
"1:00",
8,
},
{ "'%R')", -3666, true,
"-1:01",
7,
},
// Advanced %T tests
{ "'%0T')", 3666, true,
"01:01:06",
1,
},
{ "'%uT')", 3666, true,
"1 hour, 1 minute, and 6 seconds",
1,
},
{ "'%aT')", 3666, true,
"1 hr 1 min 6 sec",
1,
},
{ "'%auT')", 3666, true,
"1 hour, 1 minute, and 6 seconds",
1,
},
{ "'%0uT')", 3666, true,
"01 hour, 01 minute, and 06 seconds",
1,
},
{ "'%fT')", 129666, true,
"36:01:06",
1,
},
{ "'%T')", 129666, true,
"12:01:06",
1,
},
// Advanced %R tests
{ "'%0R')", 3666, true,
"01:01",
7,
},
{ "'%uR')", 3666, true,
"1 hour, and 1 minute",
7,
},
{ "'%aR')", 3666, true,
"1 hr 1 min",
7,
},
{ "'%auR')", 3666, true,
"1 hour, and 1 minute",
7,
},
{ "'%0uR')", 3666, true,
"01 hour, and 01 minute",
7,
},
{ "'%fR')", 129666, true,
"36:01",
7,
},
{ "'%R')", 129666, true,
"12:01",
7,
},
// Predicate tests
{ ">1d12H:'%0ud',<0S:'%-uS since',<60S:'%uS')", 9, true,
"9 seconds",
1,
},
{ ">1d12H:'%0ud',<0S:'%-uS since',<60S:'%0uS')", 129600, true,
"",
129601 - 60, // Time left until we hit <60S
TemplateStringErrorStatus_CantResolve,
42,
},
{ ">1d12H:'%0fud',<0S:'%-uS since',<60S:'%uS')", 129601, true,
"01 day",
1,
},
// 1d12H1S
{ ">=1d14H:'%0fud',<1d13H:'%0fud',>1d12H:'%0fud')", 129601, true,
"01 day",
43202, // 12H2S (time=1d-1S)
},
// 1d12H
{ ">=1d14H:'%0fud',<1d13H:'%0fud',>1d12H:'%0fud')", 129600, true,
"01 day",
43201, // 12H1S (time=1d-1S)
},
// 1d13H-100S
{ ">=1d14H:'%0fud',<1d13H:'%0fud',>1d12H:'%0fud')", 133100, true,
"01 day",
46701, // 13H-99S (time=1d12H)
},
// 1d13H100S
{ ">=1d14H:'%0fud',<1d13H:'%0fud',>1d12H:'%0fud')", 133300, true,
"01 day",
101, // time=1d13H-1S
},
// 1d14H100S
{ ">=1d14H:'%0fud',<1d13H:'%0fud',>1d12H:'%0fud')", 136900, true,
"01 day",
101, // time=1d14H-1S
},
// Predicate tests w/ since
// 1d14H
{ ">=1d14H:'%0fud',<1d13H:'%0fud',>1d12H:'%0fud')", 136800, false,
"01 day",
36000, // 2D
},
// 1d14H-100S
{ ">=1d14H:'%0fud',<1d13H:'%0fud',>1d12H:'%0fud')", 136700, false,
"01 day",
100, // time=1d14H
},
// 1d13H
{ ">=1d14H:'%0fud',<1d13H:'%0fud',>1d12H:'%0fud')", 133200, false,
"01 day",
3600, // 1H (time=1d14H)
},
// 1d13H-10S
{ ">=1d14H:'%0fud',<1d13H:'%0fud',>1d12H:'%0fud')", 133190, false,
"01 day",
10, // 10S (time=1d13H)
},
// 1d12H
{ ">=1d14H:'%0fud',<1d13H:'%0fud',>1d12H:'%0fud')", 129600, false,
"01 day",
1, // (time=1d12H1S)
},
};
void test_template_string__format(void) {
for(size_t i = 0; i < ARRAY_LENGTH(s_format_tests); i++) {
const FormatTestData *test = &s_format_tests[i];
prv_state_init();
s_state.filter_state = test->filter_state;
s_state.time_was_until = test->time_was_until;
prv_template_evaluate_filter(&s_state, "format", test->params);
// The filter isn't required to NUL-terminate, so we gotta do it manually sometimes.
if (*s_state.output != 'Z') {
cl_assert_equal_i(*s_state.output, '\0');
} else {
*s_state.output = '\0';
}
size_t err_index = s_state.position - test->params;
#if DEBUG_PRINTING
printf("parameter: \"%s\"\n", test->params);
printf("filter_state: %jd %s\n", test->filter_state,
test->time_was_until ? "until" : "since");
printf("expect: \"%s\" err %d @ %zu eval@%jd\n", test->expect_str,
test->expect_status, test->expect_index,
test->expect_eval_time);
printf("got : \"%s\" err %d @ %zu eval@%jd\n", s_output,
s_error.status, err_index, s_cond.eval_time);
#endif
if (s_error.status) {
#if DEBUG_PRINTING
printf("\"%s\"\n", test->params);
printf("%*s^\n", (int)err_index + 1, "");
if (s_error.status >= TemplateStringErrorStatusCount) {
printf("Invalid status code\n");
} else {
printf("%s\n", s_error_strings[s_error.status]);
}
#endif
cl_assert_equal_i(s_error.status, test->expect_status);
cl_assert_equal_i(err_index, test->expect_index);
}
cl_assert_equal_s(s_output, test->expect_str);
cl_assert_equal_i(s_cond.eval_time, test->expect_eval_time);
#if DEBUG_PRINTING
printf("\n");
#endif
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////
// Test pipeline parser
static const struct {
const char *instr;
time_t intime;
const char *expect_str;
time_t expect_time;
bool expect_rv;
TemplateStringErrorStatus expect_status;
size_t expect_index;
} s_full_tests[] = {
{ "Basicist test~", 1000000000,
"Basicist test~",
0,
true,
},
{ "\\\\\\", 1000000000,
"\\",
EVAL_DEFAULT,
false,
TemplateStringErrorStatus_InvalidEscapeCharacter,
3,
},
{ "\\e", 1000000000,
"e",
0,
true,
},
{ "\\\\\\{}", 1000000000,
"\\{}",
0,
true,
},
{ "\\\\{end()}", 1000000000,
"\\",
0,
true,
},
{ "\\{end()}", 1000000000,
"{end()}",
0,
true,
},
{ "Harder test {} bazza", 1000000000,
"Harder test ",
EVAL_DEFAULT,
false,
TemplateStringErrorStatus_NoResultGenerated,
13,
},
{ "Failer {time_until}", 1000000000,
"Failer ",
EVAL_DEFAULT,
false,
TemplateStringErrorStatus_MissingOpeningParen,
8,
},
{ "B {time_until(1)|format('\\\\')}", 0,
"B \\",
0,
true,
},
{ "B {time_until(1)|format('\\%foo')}", 0,
"B %foo",
0,
true,
},
{ "B {time_until(1)|format('%%foo')}", 0,
"B %foo",
0,
true,
},
{ "B {time_until(1)|format('\\'')}", 0,
"B '",
0,
true,
},
{ "B {time_until(1)|format('\\)}", 0,
"B )}",
EVAL_DEFAULT,
false,
TemplateStringErrorStatus_MissingClosingQuote,
28,
},
{ "B {time_until(1)|format('%T')}", 0,
"B 1",
1,
true,
},
{ "B {time_until(1)|format('%K')}", 0,
"B ",
EVAL_DEFAULT,
false,
TemplateStringErrorStatus_InvalidConversionSpecifier,
26,
},
{ "B {time_until(1)|format('%f')}", 0,
"B ",
EVAL_DEFAULT,
false,
TemplateStringErrorStatus_InvalidConversionSpecifier,
27,
},
{ "F {time_until(100)}", 1000000000,
"F ",
EVAL_DEFAULT,
false,
TemplateStringErrorStatus_NoResultGenerated,
18,
},
{ "{end()", 1000000000,
"",
EVAL_DEFAULT,
false,
TemplateStringErrorStatus_MissingClosingBrace,
6,
},
{ "{end(hurf", 1000000000,
"",
EVAL_DEFAULT,
false,
TemplateStringErrorStatus_MissingClosingParen,
5,
},
{ "{end}", 1000000000,
"",
EVAL_DEFAULT,
false,
TemplateStringErrorStatus_MissingOpeningParen,
1,
},
{ "B {time_until(129666)|format('%T')}", 0,
"B 12:01:06",
1,
true,
},
{ "Countdown: {time_until(1)|format(>1d12H:'%0ud',<0S:'%-uS since',<60S:'%uS')} foof",
10,
"Countdown: 9 seconds since foof",
10 + 1,
true,
},
{ "Countdown: {time_until(129601)|format(>1d12H:'%0ud',<0S:'%-uS since',<60S:'%0uS')} foof",
1,
"Countdown: ",
1 + 129601 - 60, // Time left until we hit <60S
false,
TemplateStringErrorStatus_CantResolve,
80,
},
{ "B {time_until(129666)|format('boop)I\\'m a filter')}", 0,
"B boop)I'm a filter",
0,
true,
},
{ "B {time_until(129666)|format('%T')} AND {time_until(129660)|format('%T')}", 0,
"B 12:01:06 AND 12:01:00",
1,
true,
},
};
void test_template_string__full_test(void) {
for(int i = 0; i < ARRAY_LENGTH(s_full_tests); i++) {
TemplateStringVars vars = {};
TemplateStringError err = {};
TemplateStringEvalConditions cond = {};
vars.current_time = s_full_tests[i].intime;
cond.eval_time = EVAL_FALL_THROUGH;
memset(s_output, 'Z', sizeof(s_output));
bool rv = template_string_evaluate(s_full_tests[i].instr, s_output, sizeof(s_output),
&cond, &vars, &err);
#if DEBUG_PRINTING
printf("instr: \"%s\"\n", s_full_tests[i].instr);
printf("outstr: \"%s\"\n", s_output);
printf("next_eval: %ld\n", cond.eval_time);
printf("rv: %s\n", rv ? "true" : "false");
if (!rv) {
printf("err.status: %X\n", err.status);
printf("err.index: %zu\n", err.index_in_string);
printf("\"%s\"\n", s_full_tests[i].instr);
printf("%*s^\n", (int)err.index_in_string + 1, "");
if (err.status >= TemplateStringErrorStatusCount) {
printf("Invalid status code\n");
} else {
printf("%s\n", s_error_strings[err.status]);
}
}
printf("\n");
#endif
cl_assert_equal_s(s_output, s_full_tests[i].expect_str);
cl_assert_equal_i(rv, s_full_tests[i].expect_rv);
cl_assert_equal_i(cond.eval_time, s_full_tests[i].expect_time);
if (cond.eval_time != 0) {
cl_assert_equal_b(cond.force_eval_on_time, true);
} else {
cl_assert_equal_b(cond.force_eval_on_time, false);
}
if (!rv) {
cl_assert_equal_i(err.status, s_full_tests[i].expect_status);
cl_assert_equal_i(err.index_in_string, s_full_tests[i].expect_index);
}
}
}
+116
View File
@@ -0,0 +1,116 @@
/*
* 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/graphics/gtypes.h"
#include "applib/graphics/text_render.h"
#include "clar.h"
#include "stubs_applib_resource.h"
#include "stubs_app_state.h"
#include "stubs_compiled_with_legacy2_sdk.h"
#include "stubs_heap.h"
#include "stubs_logging.h"
#include "stubs_passert.h"
#include "stubs_resources.h"
#include "stubs_syscalls.h"
GBitmap* graphics_context_get_bitmap(GContext* ctx) { return NULL; }
void graphics_context_mark_dirty_rect(GContext* ctx, GRect rect) {}
const GlyphData* text_resources_get_glyph(FontCache* font_cache, const Codepoint codepoint,
FontInfo* fontinfo) { return NULL; }
extern int32_t prv_convert_1bit_addr_to_8bit_x(GBitmap *dest_bitmap, uint32_t *block_addr,
int32_t y_offset);
static int32_t prv_get_8bit_x_from_1bit_x(int32_t dest_1bit_x) {
return (((dest_1bit_x / 32) * 4)) * 8;
}
void test_text_render__convert_1bit_to_8bit_144x168(void) {
GSize size = GSize(144, 168);
const int row_1bit_size_words = 1 + (size.w - 1) / 32;
GBitmap *bitmap = gbitmap_create_blank(size, GBitmapFormat8Bit);
uintptr_t base = (uintptr_t)bitmap->addr;
int dest_x = 0;
int dest_y = 0;
uint32_t *block_addr = NULL;
block_addr = (uint32_t *)(uintptr_t)(((dest_y * row_1bit_size_words) + (dest_x / 32)) * 4);
cl_assert_equal_i(prv_convert_1bit_addr_to_8bit_x(bitmap, block_addr, dest_y),
prv_get_8bit_x_from_1bit_x(dest_x));
dest_x = 50;
dest_y = 0;
block_addr = (uint32_t *)(uintptr_t)(((dest_y * row_1bit_size_words) + (dest_x / 32)) * 4);
cl_assert_equal_i(prv_convert_1bit_addr_to_8bit_x(bitmap, block_addr, dest_y),
prv_get_8bit_x_from_1bit_x(dest_x));
dest_x = 0;
dest_y = 50;
block_addr = (uint32_t *)(uintptr_t)(((dest_y * row_1bit_size_words) + (dest_x / 32)) * 4);
cl_assert_equal_i(prv_convert_1bit_addr_to_8bit_x(bitmap, block_addr, dest_y),
prv_get_8bit_x_from_1bit_x(dest_x));
dest_x = 20;
dest_y = 100;
block_addr = (uint32_t *)(uintptr_t)(((dest_y * row_1bit_size_words) + (dest_x / 32)) * 4);
cl_assert_equal_i(prv_convert_1bit_addr_to_8bit_x(bitmap, block_addr, dest_y),
prv_get_8bit_x_from_1bit_x(dest_x));
gbitmap_destroy(bitmap);
}
void test_text_render__convert_1bit_to_8bit_180x180(void) {
GSize size = GSize(180, 180);
const int row_1bit_size_words = 1 + (size.w - 1) / 32;
GBitmap *bitmap = gbitmap_create_blank(size, GBitmapFormat8Bit);
uintptr_t base = (uintptr_t)bitmap->addr;
int dest_x = 0;
int dest_y = 0;
uint32_t *block_addr = NULL;
block_addr = (uint32_t *)(uintptr_t)(((dest_y * row_1bit_size_words) + (dest_x / 32)) * 4);
cl_assert_equal_i(prv_convert_1bit_addr_to_8bit_x(bitmap, block_addr, dest_y),
prv_get_8bit_x_from_1bit_x(dest_x));
dest_x = 50;
dest_y = 0;
block_addr = (uint32_t *)(uintptr_t)(((dest_y * row_1bit_size_words) + (dest_x / 32)) * 4);
cl_assert_equal_i(prv_convert_1bit_addr_to_8bit_x(bitmap, block_addr, dest_y),
prv_get_8bit_x_from_1bit_x(dest_x));
dest_x = 0;
dest_y = 50;
block_addr = (uint32_t *)(uintptr_t)(((dest_y * row_1bit_size_words) + (dest_x / 32)) * 4);
cl_assert_equal_i(prv_convert_1bit_addr_to_8bit_x(bitmap, block_addr, dest_y),
prv_get_8bit_x_from_1bit_x(dest_x));
dest_x = 20;
dest_y = 100;
block_addr = (uint32_t *)(uintptr_t)(((dest_y * row_1bit_size_words) + (dest_x / 32)) * 4);
cl_assert_equal_i(prv_convert_1bit_addr_to_8bit_x(bitmap, block_addr, dest_y),
prv_get_8bit_x_from_1bit_x(dest_x));
gbitmap_destroy(bitmap);
}
+328
View File
@@ -0,0 +1,328 @@
/*
* 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 "fixtures/load_test_resources.h"
// FW headers
#include "applib/graphics/text_resources.h"
#include "resource/resource.h"
#include "resource/resource_ids.auto.h"
#include "resource/system_resource.h"
#include "util/size.h"
// Fakes
#include "fake_app_manager.h"
// Stubs
#include "stubs_analytics.h"
#include "stubs_bootbits.h"
#include "stubs_logging.h"
#include "stubs_mutex.h"
#include "stubs_pbl_malloc.h"
#include "stubs_pebble_tasks.h"
#include "stubs_passert.h"
#include "stubs_print.h"
#include "stubs_prompt.h"
#include "stubs_queue.h"
#include "stubs_serial.h"
#include "stubs_sleep.h"
#include "stubs_syscalls.h"
#include "stubs_prompt.h"
#include "stubs_task_watchdog.h"
#include "stubs_memory_layout.h"
#define WILDCARD_CODEPOINT 0x25AF
static FontCache s_font_cache;
static FontInfo s_font_info;
#define FONT_COMPRESSION_FIXTURE_PATH "font_compression"
// Helpers
////////////////////////////////////
static uint8_t glyph_get_size_bytes(const GlyphData *glyph) {
return ((glyph->header.width_px * glyph->header.height_px) + (8 - 1)) / 8;
}
void test_text_resources__initialize(void) {
fake_spi_flash_init(0, 0x1000000);
pfs_init(false);
pfs_format(true /* write erase headers */);
load_resource_fixture_in_flash(RESOURCES_FIXTURE_PATH, SYSTEM_RESOURCES_FIXTURE_NAME, false /* is_next */);
load_resource_fixture_on_pfs(RESOURCES_FIXTURE_PATH, CHINESE_FIXTURE_NAME, "lang");
//cl_assert(resource_has_valid_system_resources());
memset(&s_font_info, 0, sizeof(s_font_info));
memset(&s_font_cache, 0, sizeof(s_font_cache));
FontCache *font_cache = &s_font_cache;
memset(font_cache->cache_keys, 0, sizeof(font_cache->cache_keys));
memset(font_cache->cache_data, 0, sizeof(font_cache->cache_data));
keyed_circular_cache_init(&font_cache->line_cache, font_cache->cache_keys,
font_cache->cache_data, sizeof(LineCacheData), LINE_CACHE_SIZE);
resource_init();
}
void test_text_resources__cleanup(void) {
}
void test_text_resources__init_font(void) {
uint32_t gothic_18_handle = RESOURCE_ID_GOTHIC_18;
cl_assert(text_resources_init_font(0, gothic_18_handle, 0, &s_font_info));
cl_assert_equal_i(FONT_VERSION(s_font_info.base.md.version), 3);
cl_assert_equal_i(s_font_info.base.md.wildcard_codepoint, WILDCARD_CODEPOINT);
cl_assert_equal_i(s_font_info.base.md.codepoint_bytes, 2);
}
void test_text_resources__horiz_advance(void) {
uint32_t gothic_18_handle = RESOURCE_ID_GOTHIC_18;
cl_assert(text_resources_init_font(0, gothic_18_handle, 0, &s_font_info));
int8_t horiz_advance = text_resources_get_glyph_horiz_advance(&s_font_cache, 'a', &s_font_info);
cl_assert(horiz_advance != 0);
cl_assert_equal_i(horiz_advance,
text_resources_get_glyph_horiz_advance(&s_font_cache, 'a', &s_font_info));
cl_assert_equal_i(horiz_advance,
text_resources_get_glyph_horiz_advance(&s_font_cache, 'a', &s_font_info));
}
void test_text_resources__horiz_advance_multiple(void) {
uint32_t gothic_18_handle = RESOURCE_ID_GOTHIC_18;
cl_assert(text_resources_init_font(0, gothic_18_handle, 0, &s_font_info));
int8_t horiz_advance = text_resources_get_glyph_horiz_advance(&s_font_cache, 'a', &s_font_info);
cl_assert(horiz_advance != 0);
cl_assert_equal_i(horiz_advance,
text_resources_get_glyph_horiz_advance(&s_font_cache, 'a', &s_font_info));
horiz_advance = text_resources_get_glyph_horiz_advance(&s_font_cache, 'b', &s_font_info);
cl_assert(horiz_advance != 0);
cl_assert_equal_i(horiz_advance,
text_resources_get_glyph_horiz_advance(&s_font_cache, 'b', &s_font_info));
horiz_advance = text_resources_get_glyph_horiz_advance(&s_font_cache, 'c', &s_font_info);
cl_assert(horiz_advance != 0);
cl_assert_equal_i(horiz_advance,
text_resources_get_glyph_horiz_advance(&s_font_cache, 'c', &s_font_info));
}
void test_text_resources__get_glyph_multiple(void) {
const uint8_t a_glyph_data_bytes[] = {0x2e, 0x42, 0x2e, 0x63, 0xb6};
const uint8_t b_glyph_data_bytes[] = {0x21, 0x84, 0x36, 0x63, 0x8c, 0x71, 0x36};
const uint8_t c_glyph_data_bytes[] = {0x2e, 0x86, 0x10, 0x42, 0x74};
uint32_t gothic_18_handle = RESOURCE_ID_GOTHIC_18;
cl_assert(text_resources_init_font(0, gothic_18_handle, 0, &s_font_info));
uint8_t glyph_size_bytes;
const GlyphData *glyph;
glyph = text_resources_get_glyph(&s_font_cache, 'a', &s_font_info);
glyph_size_bytes = glyph_get_size_bytes(glyph);
cl_assert_equal_m(a_glyph_data_bytes, glyph->data, glyph_size_bytes);
glyph = text_resources_get_glyph(&s_font_cache, 'b', &s_font_info);
glyph_size_bytes = glyph_get_size_bytes(glyph);
cl_assert_equal_m(b_glyph_data_bytes, glyph->data, glyph_size_bytes);
glyph = text_resources_get_glyph(&s_font_cache, 'c', &s_font_info);
glyph_size_bytes = glyph_get_size_bytes(glyph);
cl_assert_equal_m(c_glyph_data_bytes, glyph->data, glyph_size_bytes);
}
void test_text_resources__init_backup_font(void) {
// load the built in fallback font
uint32_t font_fallback = RESOURCE_ID_FONT_FALLBACK_INTERNAL;
cl_assert(text_resources_init_font(0, font_fallback, 0, &s_font_info));
cl_assert_equal_i(FONT_VERSION(s_font_info.base.md.version), 3);
cl_assert_equal_i(s_font_info.base.md.wildcard_codepoint, WILDCARD_CODEPOINT);
}
void test_text_resources__test_backup_wildcard(void) {
const uint8_t wildcard_bytes[] = {0x3f, 0xc6, 0x18, 0x63, 0x8c, 0x31, 0xc6, 0x0f};
uint32_t font_fallback = RESOURCE_ID_FONT_FALLBACK_INTERNAL;
cl_assert(text_resources_init_font(0, font_fallback, 0, &s_font_info));
int8_t horiz_advance = text_resources_get_glyph_horiz_advance(&s_font_cache,
WILDCARD_CODEPOINT, &s_font_info);
cl_assert(horiz_advance != 0);
cl_assert_equal_i(horiz_advance,
text_resources_get_glyph_horiz_advance(&s_font_cache, WILDCARD_CODEPOINT, &s_font_info));
const GlyphData *glyph = text_resources_get_glyph(&s_font_cache,
WILDCARD_CODEPOINT, &s_font_info);
cl_assert_equal_i(glyph->header.width_px, 5);
cl_assert_equal_i(glyph->header.height_px, 12);
uint8_t glyph_size_bytes = glyph_get_size_bytes(glyph);
cl_assert_equal_m(wildcard_bytes, glyph->data, glyph_size_bytes);
}
void test_text_resources__test_gothic_wildcard(void) {
uint8_t wildcard_bytes[] = {0xff, 0x60, 0x30, 0x18, 0x0c, 0x06, 0x83, 0xc1, 0x60, 0x30, 0x18, 0x0c, 0xfe, 0x01};
uint32_t gothic_18_handle = RESOURCE_ID_GOTHIC_18;
cl_assert(text_resources_init_font(0, gothic_18_handle, 0, &s_font_info));
int8_t horiz_advance = text_resources_get_glyph_horiz_advance(&s_font_cache,
WILDCARD_CODEPOINT,
&s_font_info);
cl_assert(horiz_advance != 0);
cl_assert_equal_i(horiz_advance,
text_resources_get_glyph_horiz_advance(&s_font_cache, WILDCARD_CODEPOINT, &s_font_info));
const GlyphData *glyph = text_resources_get_glyph(&s_font_cache,
WILDCARD_CODEPOINT,
&s_font_info);
cl_assert_equal_i(glyph->header.width_px, 7);
cl_assert_equal_i(glyph->header.height_px, 15);
uint8_t glyph_size_bytes = glyph_get_size_bytes(glyph);
cl_assert_equal_m(wildcard_bytes, glyph->data, glyph_size_bytes);
}
void test_text_resources__extended_font(void) {
const uint8_t chinese_wildcard_bytes[] = {0xff, 0x60, 0x30, 0x18, 0x0c, 0x06, 0x83, 0xc1, 0x60, 0x30, 0x18, 0x0c, 0xfe, 0x01};
const uint8_t a_glyph_data_bytes[] = {0x2e, 0x42, 0x2e, 0x63, 0xb6};
const uint8_t chinese_glyph_data_bytes[] = {0x00, 0x0C, 0xE2, 0x01, 0x0F, 0x80, 0x30, 0x40,
0x08, 0x10, 0x04, 0x08, 0x82, 0xFC, 0xFF, 0x80,
0x00, 0x44, 0x00, 0x26, 0x01, 0x11, 0x41, 0x08,
0x11, 0x84, 0x04, 0x82, 0xC0, 0x01, 0x40, 0x00};
uint32_t gothic_18_bold_handle = RESOURCE_ID_GOTHIC_18;
uint32_t gothic_18_bold_extended_handle = RESOURCE_ID_GOTHIC_18_EXTENDED;
cl_assert(text_resources_init_font(0, gothic_18_bold_handle, gothic_18_bold_extended_handle, &s_font_info));
cl_assert(s_font_info.loaded);
cl_assert(s_font_info.extended);
uint8_t glyph_size_bytes;
const GlyphData *glyph;
glyph = text_resources_get_glyph(&s_font_cache, 'a', &s_font_info);
cl_assert(glyph != NULL);
glyph_size_bytes = glyph_get_size_bytes(glyph);
cl_assert_equal_m(a_glyph_data_bytes, glyph->data, glyph_size_bytes);
glyph = text_resources_get_glyph(&s_font_cache, 0x4E50 /* 乐 */, &s_font_info);
// the chinese pbpack contains the letter 你, it should succeed
cl_assert(glyph != NULL);
glyph_size_bytes = glyph_get_size_bytes(glyph);
cl_assert_equal_m(chinese_glyph_data_bytes, glyph->data, glyph_size_bytes);
glyph = text_resources_get_glyph(&s_font_cache, 0x8888 /* 袈 */, &s_font_info);
// the chinese pbpack does not contain the letter 袈, it should return the wildcard
cl_assert(glyph != NULL);
glyph_size_bytes = glyph_get_size_bytes(glyph);
cl_assert_equal_m(chinese_wildcard_bytes, glyph->data, glyph_size_bytes);
}
void test_text_resources__test_emoji_font(void) {
const uint8_t phone_bytes[] = {0xfe, 0x81, 0x81, 0x3c, 0x66, 0x42, 0xc3, 0xe7, 0xff, 0x00, 0x00, 0x00};
uint32_t gothic_18_emoji_handle = RESOURCE_ID_GOTHIC_18_EMOJI;
cl_assert(text_resources_init_font(0, gothic_18_emoji_handle, 0, &s_font_info));
uint8_t glyph_size_bytes;
const GlyphData *glyph;
const Codepoint PHONE_CODEPOINT = 0x260E;
glyph = text_resources_get_glyph(&s_font_cache, PHONE_CODEPOINT, &s_font_info);
cl_assert(glyph != NULL);
glyph_size_bytes = glyph_get_size_bytes(glyph);
cl_assert_equal_m(phone_bytes, glyph->data, glyph_size_bytes);
}
void DISABLED_test_text_resources__test_emoji_fallback(void) {
const uint8_t phone_bytes[] = {0xfe, 0x81, 0x81, 0x3c, 0x66, 0x42, 0xc3, 0xe7, 0xff, 0x00, 0x00, 0x00};
uint32_t gothic_18_handle = RESOURCE_ID_GOTHIC_18;
cl_assert(text_resources_init_font(0, gothic_18_handle, 0, &s_font_info));
uint8_t glyph_size_bytes;
const GlyphData *glyph;
const Codepoint PHONE_CODEPOINT = 0x260E;
glyph = text_resources_get_glyph(&s_font_cache, PHONE_CODEPOINT, &s_font_info);
cl_assert(glyph != NULL);
glyph_size_bytes = glyph_get_size_bytes(glyph);
cl_assert_equal_m(phone_bytes, glyph->data, glyph_size_bytes);
}
void test_text_resources__test_glyph_decompression(void) {
// There is no way to get the list of glyphs present in a font with the existing API. This list
// of ranges lists the 371 glyphs currently in fontname.ttf
typedef struct Codepoint_Range {
uint16_t start;
uint16_t end;
} CodePoint_Range;
const CodePoint_Range codepoint_range[] = {
{ 0x0020, 0x007E }, { 0x00A0, 0x00AC }, { 0x00AE, 0x00D6 }, { 0x00D9, 0x017F },
{ 0x0192, 0x0192 }, { 0x01FC, 0x01FF }, { 0x0218, 0x021B }, { 0x02C6, 0x02DD },
{ 0x03C0, 0x03C0 }, { 0x2013, 0x2014 }, { 0x2018, 0x201A }, { 0x201C, 0x201E },
{ 0x2020, 0x2022 }, { 0x2026, 0x2026 }, { 0x2030, 0x2030 }, { 0x2039, 0x203A },
{ 0x2044, 0x2044 }, { 0x20AC, 0x20AC }, { 0x2122, 0x2122 }, { 0x2126, 0x2126 },
{ 0x2202, 0x2202 }, { 0x2206, 0x2206 }, { 0x220F, 0x220F }, { 0x2211, 0x2212 },
{ 0x221A, 0x221A }, { 0x221E, 0x221E }, { 0x222B, 0x222B }, { 0x2248, 0x2248 },
{ 0x2260, 0x2260 }, { 0x2264, 0x2265 }, { 0x25AF, 0x25AF }, { 0x25CA, 0x25CA },
{ 0xF6C3, 0xF6C3 }, { 0xFB01, 0xFB02 }
};
// Create a second FontInfo for the compressed font.
// The uncompressed font will use the global.
FontInfo font_info_compressed;
memset(&font_info_compressed, 0, sizeof(font_info_compressed));
// Load GOTHIC_18
uint32_t gothic_18_handle = RESOURCE_ID_GOTHIC_18;
cl_assert(text_resources_init_font(0, gothic_18_handle, 0, &s_font_info));
cl_assert_equal_i(FONT_VERSION(s_font_info.base.md.version), 3);
cl_assert(!HAS_FEATURE(s_font_info.base.md.version, VERSION_FIELD_FEATURE_RLE4));
// Load GOTHIC_18_COMPRESSED. This is the same font, added by hand to the system resource pack.
// To do this, simply copy the GOTHIC_18 stanza in resource/normal/base/resource_map.json, change
// the name to include _COMPRESSED, and add the field: "compress": "RLE4". Rebuild, and run
// ./tools/update_system_pbpack.sh
uint32_t gothic_18_compressed_handle = RESOURCE_ID_GOTHIC_18_COMPRESSED; // Read source to fix
cl_assert(text_resources_init_font(0, gothic_18_compressed_handle, 0, &font_info_compressed));
cl_assert_equal_i(FONT_VERSION(font_info_compressed.base.md.version), 3);
cl_assert(HAS_FEATURE(font_info_compressed.base.md.version, VERSION_FIELD_FEATURE_RLE4));
// For each glyph in the font, get both the compressed and uncompressed bit field & header, and
// assert that they are identical (ignoring any possible garbage after the bitmap).
// Load the uncompressed glyph into this local glyph_buffer, and use the font cache for the
// compressed glyph.
uint8_t glyph_buffer[sizeof(GlyphHeaderData) + CACHE_GLYPH_SIZE];
for (unsigned index = 0; index < ARRAY_LENGTH(codepoint_range); ++index) {
for (unsigned codepoint = codepoint_range[index].start;
codepoint <= codepoint_range[index].end; ++codepoint) {
const GlyphData *glyph = text_resources_get_glyph(&s_font_cache, codepoint, &s_font_info);
cl_assert(glyph);
unsigned glyph_size = sizeof(GlyphHeaderData) + glyph_get_size_bytes(glyph);
memcpy(glyph_buffer, glyph->data, glyph_size);
glyph = text_resources_get_glyph(&s_font_cache, codepoint, &font_info_compressed);
cl_assert(glyph);
cl_assert_equal_m(glyph->data, glyph_buffer, glyph_size);
}
}
}
@@ -0,0 +1,352 @@
/*
* 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 "pebble_asserts.h"
#include "applib/unobstructed_area_service.h"
// Stubs
/////////////////////
#include "stubs_app.h"
#include "stubs_app_manager.h"
#include "stubs_app_state.h"
#include "stubs_events.h"
#include "stubs_framebuffer.h"
#include "stubs_graphics.h"
#include "stubs_logging.h"
#include "stubs_passert.h"
#include "stubs_pbl_malloc.h"
#include "stubs_pebble_tasks.h"
#include "stubs_process_manager.h"
#include "stubs_ui_window.h"
// Fakes
/////////////////////
#include "fake_event_service.h"
// Statics
/////////////////////
void *s_context_target;
typedef struct UnobstructedAreaTestData {
void *context;
int num_will_change_calls;
int num_change_calls;
int num_did_change_calls;
GRect last_will_change_final_area;
AnimationProgress last_change_progress;
} UnobstructedAreaTestData;
UnobstructedAreaTestData s_data;
static void prv_will_change(GRect final_area, void *context) {
s_data.last_will_change_final_area = final_area;
cl_assert_equal_p(context, s_data.context);
s_data.num_will_change_calls++;
}
static void prv_change(AnimationProgress progress, void *context) {
s_data.last_change_progress = progress;
cl_assert_equal_p(context, s_data.context);
s_data.num_change_calls++;
}
static void prv_did_change(void *context) {
cl_assert_equal_p(context, s_data.context);
s_data.num_did_change_calls++;
}
// Test boilerplate
/////////////////////
void test_unobstructed_area_service__initialize(void) {
s_data = (UnobstructedAreaTestData) {
.context = &s_context_target,
.last_change_progress = -1,
};
fake_event_service_init();
s_app_state_framebuffer = &(FrameBuffer) { .size = DISP_FRAME.size };
unobstructed_area_service_init(app_state_get_unobstructed_area_state(), DISP_ROWS);
}
void test_unobstructed_area_service__cleanup(void) {
app_unobstructed_area_service_unsubscribe();
unobstructed_area_service_deinit(app_state_get_unobstructed_area_state());
}
// Tests
//////////////////////
void test_unobstructed_area_service__subscribe(void) {
// Unsubscribing first should not crash
app_unobstructed_area_service_unsubscribe();
cl_assert(!app_state_get_unobstructed_area_state()->handlers.will_change);
cl_assert(!app_state_get_unobstructed_area_state()->handlers.change);
cl_assert(!app_state_get_unobstructed_area_state()->handlers.did_change);
// Subscribing should use the event service
UnobstructedAreaHandlers handlers = {
.will_change = prv_will_change,
.change = prv_change,
.did_change = prv_did_change,
};
app_unobstructed_area_service_subscribe(handlers, s_data.context);
cl_assert(fake_event_service_get_info(PEBBLE_UNOBSTRUCTED_AREA_EVENT)->handler);
cl_assert_equal_p(app_state_get_unobstructed_area_state()->handlers.will_change, prv_will_change);
cl_assert_equal_p(app_state_get_unobstructed_area_state()->handlers.change, prv_change);
cl_assert_equal_p(app_state_get_unobstructed_area_state()->handlers.did_change, prv_did_change);
// Unsubscribing after subscription should cancel subscriptions
app_unobstructed_area_service_unsubscribe();
cl_assert(!app_state_get_unobstructed_area_state()->handlers.will_change);
cl_assert(!app_state_get_unobstructed_area_state()->handlers.change);
cl_assert(!app_state_get_unobstructed_area_state()->handlers.did_change);
// Unsubscribing again should not crash
app_unobstructed_area_service_unsubscribe();
cl_assert(!app_state_get_unobstructed_area_state()->handlers.will_change);
cl_assert(!app_state_get_unobstructed_area_state()->handlers.change);
cl_assert(!app_state_get_unobstructed_area_state()->handlers.did_change);
}
void test_unobstructed_area_service__will_change(void) {
UnobstructedAreaHandlers handlers = {
.will_change = prv_will_change,
};
app_unobstructed_area_service_subscribe(handlers, s_data.context);
cl_assert(fake_event_service_get_info(PEBBLE_UNOBSTRUCTED_AREA_EVENT)->handler);
cl_assert_equal_p(app_state_get_unobstructed_area_state()->handlers.will_change, prv_will_change);
const GRect from_area = GRect(0, 0, DISP_COLS, 400);
const GRect to_area = GRect(0, 0, DISP_COLS, 200);
unobstructed_area_service_will_change(from_area.size.h, to_area.size.h);
fake_event_service_handle_last();
cl_assert_equal_i(s_data.num_will_change_calls, 1);
const GRect to_area_expected = GRect(0, 0, DISP_COLS, MIN(200, DISP_ROWS));
cl_assert_equal_grect(s_data.last_will_change_final_area, to_area_expected);
}
void test_unobstructed_area_service__will_change_twice(void) {
UnobstructedAreaHandlers handlers = {
.will_change = prv_will_change,
};
app_unobstructed_area_service_subscribe(handlers, s_data.context);
cl_assert(fake_event_service_get_info(PEBBLE_UNOBSTRUCTED_AREA_EVENT)->handler);
cl_assert_equal_p(app_state_get_unobstructed_area_state()->handlers.will_change, prv_will_change);
const GRect from_area = GRect(0, 0, DISP_COLS, 400);
const GRect to_area = GRect(0, 0, DISP_COLS, 200);
unobstructed_area_service_will_change(from_area.size.h, to_area.size.h);
fake_event_service_handle_last();
unobstructed_area_service_will_change(from_area.size.h, to_area.size.h);
cl_assert_passert(fake_event_service_handle_last());
}
void test_unobstructed_area_service__change(void) {
UnobstructedAreaHandlers handlers = {
.change = prv_change,
};
app_unobstructed_area_service_subscribe(handlers, s_data.context);
cl_assert(fake_event_service_get_info(PEBBLE_UNOBSTRUCTED_AREA_EVENT)->handler);
cl_assert_equal_p(app_state_get_unobstructed_area_state()->handlers.change, prv_change);
const GRect from_area = GRect(0, 0, DISP_COLS, 400);
const GRect to_area = GRect(0, 0, DISP_COLS, 200);
unobstructed_area_service_will_change(from_area.size.h, to_area.size.h);
fake_event_service_handle_last();
const GRect area = GRect(0, 0, DISP_COLS, 200);
const AnimationProgress progress = ANIMATION_NORMALIZED_MAX / 2;
unobstructed_area_service_change(area.size.h, to_area.size.h, progress);
fake_event_service_handle_last();
cl_assert_equal_i(s_data.num_change_calls, 1);
cl_assert_equal_i(s_data.last_change_progress, progress);
}
void test_unobstructed_area_service__change_after_subscribe(void) {
const GRect from_area = GRect(0, 0, DISP_COLS, 400);
const GRect to_area = GRect(0, 0, DISP_COLS, 200);
unobstructed_area_service_will_change(from_area.size.h, to_area.size.h);
UnobstructedAreaHandlers handlers = {
.will_change = prv_will_change,
.change = prv_change,
};
app_unobstructed_area_service_subscribe(handlers, s_data.context);
cl_assert(fake_event_service_get_info(PEBBLE_UNOBSTRUCTED_AREA_EVENT)->handler);
cl_assert_equal_p(app_state_get_unobstructed_area_state()->handlers.change, prv_change);
const GRect area = GRect(0, 0, DISP_COLS, 200);
const AnimationProgress progress = ANIMATION_NORMALIZED_MAX / 2;
unobstructed_area_service_change(area.size.h, to_area.size.h, progress);
fake_event_service_handle_last();
cl_assert_equal_i(s_data.num_will_change_calls, 1);
cl_assert_equal_i(s_data.num_change_calls, 1);
cl_assert_equal_i(s_data.last_change_progress, progress);
}
void test_unobstructed_area_service__change_no_will(void) {
UnobstructedAreaHandlers handlers = {
.will_change = prv_will_change,
.change = prv_change,
};
app_unobstructed_area_service_subscribe(handlers, s_data.context);
cl_assert(fake_event_service_get_info(PEBBLE_UNOBSTRUCTED_AREA_EVENT)->handler);
cl_assert_equal_p(app_state_get_unobstructed_area_state()->handlers.change, prv_change);
const GRect to_area = GRect(0, 0, DISP_COLS, 200);
const GRect area = GRect(0, 0, DISP_COLS, 200);
const AnimationProgress progress = ANIMATION_NORMALIZED_MAX / 2;
unobstructed_area_service_change(area.size.h, to_area.size.h, progress);
fake_event_service_handle_last();
cl_assert_equal_i(s_data.num_will_change_calls, 1);
cl_assert_equal_i(s_data.num_change_calls, 1);
cl_assert_equal_i(s_data.last_change_progress, progress);
}
void test_unobstructed_area_service__did_change(void) {
UnobstructedAreaHandlers handlers = {
.did_change = prv_did_change,
};
app_unobstructed_area_service_subscribe(handlers, s_data.context);
cl_assert(fake_event_service_get_info(PEBBLE_UNOBSTRUCTED_AREA_EVENT)->handler);
cl_assert_equal_p(app_state_get_unobstructed_area_state()->handlers.did_change, prv_did_change);
const GRect from_area = GRect(0, 0, DISP_COLS, 400);
const GRect to_area = GRect(0, 0, DISP_COLS, 200);
unobstructed_area_service_will_change(from_area.size.h, to_area.size.h);
fake_event_service_handle_last();
unobstructed_area_service_did_change(to_area.size.h);
fake_event_service_handle_last();
cl_assert_equal_i(s_data.num_did_change_calls, 1);
}
void test_unobstructed_area_service__did_change_after_subscribe(void) {
const GRect from_area = GRect(0, 0, DISP_COLS, 400);
const GRect to_area = GRect(0, 0, DISP_COLS, 200);
unobstructed_area_service_will_change(from_area.size.h, to_area.size.h);
UnobstructedAreaHandlers handlers = {
.will_change = prv_will_change,
.did_change = prv_did_change,
};
app_unobstructed_area_service_subscribe(handlers, s_data.context);
cl_assert(fake_event_service_get_info(PEBBLE_UNOBSTRUCTED_AREA_EVENT)->handler);
cl_assert_equal_p(app_state_get_unobstructed_area_state()->handlers.did_change, prv_did_change);
unobstructed_area_service_did_change(to_area.size.h);
fake_event_service_handle_last();
cl_assert_equal_i(s_data.num_will_change_calls, 1);
cl_assert_equal_i(s_data.num_did_change_calls, 1);
}
void test_unobstructed_area_service__did_change_no_will(void) {
UnobstructedAreaHandlers handlers = {
.will_change = prv_will_change,
.did_change = prv_did_change,
};
app_unobstructed_area_service_subscribe(handlers, s_data.context);
cl_assert(fake_event_service_get_info(PEBBLE_UNOBSTRUCTED_AREA_EVENT)->handler);
cl_assert_equal_p(app_state_get_unobstructed_area_state()->handlers.did_change, prv_did_change);
const GRect to_area = GRect(0, 0, DISP_COLS, 200);
unobstructed_area_service_did_change(to_area.size.h);
fake_event_service_handle_last();
cl_assert_equal_i(s_data.num_will_change_calls, 1);
cl_assert_equal_i(s_data.num_did_change_calls, 1);
}
void test_unobstructed_area_service__layer_no_clip(void) {
app_state_get_unobstructed_area_state()->area = GRect(0, 0, 400, 400);
Layer root_layer = {
.bounds = GRect(100, 100, 200, 200),
};
GRect unobstructed_bounds;
layer_get_unobstructed_bounds(&root_layer, &unobstructed_bounds);
cl_assert_equal_grect(unobstructed_bounds, root_layer.bounds);
}
void test_unobstructed_area_service__layer_clip_x_y(void) {
app_state_get_unobstructed_area_state()->area = GRect(0, 0, 400, 400);
Layer root_layer = {
.bounds = GRect(210, 220, 300, 300),
};
GRect unobstructed_bounds;
layer_get_unobstructed_bounds(&root_layer, &unobstructed_bounds);
cl_assert_equal_grect(unobstructed_bounds, GRect(210, 220, 190, 180));
}
void test_unobstructed_area_service__layer_clip_nx_ny(void) {
app_state_get_unobstructed_area_state()->area = GRect(0, 0, 400, 400);
Layer root_layer = {
.bounds = GRect(-110, -120, 300, 300),
};
GRect unobstructed_bounds;
layer_get_unobstructed_bounds(&root_layer, &unobstructed_bounds);
cl_assert_equal_grect(unobstructed_bounds, GRect(0, 0, 190, 180));
}
void test_unobstructed_area_service__nested_layer_no_clip(void) {
app_state_get_unobstructed_area_state()->area = GRect(0, 0, 400, 400);
Layer root_layer = {
.bounds = GRect(30, 30, 30, 30),
};
Layer layer = {
.bounds = GRect(20, 20, 20, 20),
};
layer_add_child(&root_layer, &layer);
GRect unobstructed_bounds;
layer_get_unobstructed_bounds(&layer, &unobstructed_bounds);
cl_assert_equal_grect(unobstructed_bounds, layer.bounds);
}
void test_unobstructed_area_service__nested_layer_clip_x_y(void) {
app_state_get_unobstructed_area_state()->area = GRect(0, 0, 400, 400);
Layer root_layer = {
.bounds = GRect(150, 120, 10, 10), // The size of the parent layer has no affect
};
Layer layer = {
.bounds = GRect(110, 130, 300, 200),
};
layer_add_child(&root_layer, &layer);
GRect unobstructed_bounds;
layer_get_unobstructed_bounds(&layer, &unobstructed_bounds);
cl_assert_equal_grect(unobstructed_bounds, GRect(110, 130, 140, 150));
}
void test_unobstructed_area_service__nested_layer_clip_nx_ny(void) {
app_state_get_unobstructed_area_state()->area = GRect(0, 0, 400, 400);
Layer root_layer = {
.bounds = GRect(-150, -120, 10, 10), // The size of the parent layer has no affect
};
Layer layer = {
.bounds = GRect(-110, -130, 300, 290),
};
layer_add_child(&root_layer, &layer);
GRect unobstructed_bounds;
layer_get_unobstructed_bounds(&layer, &unobstructed_bounds);
cl_assert_equal_grect(unobstructed_bounds, GRect(150, 120, 40, 40));
}
+161
View File
@@ -0,0 +1,161 @@
from waftools.pebble_test import clar
def build(ctx):
clar(ctx,
sources_ant_glob=" src/fw/applib/app_message/app_message.c"
" src/fw/applib/app_message/app_message_outbox.c"
" src/fw/applib/app_message/app_message_inbox.c"
" src/fw/util/rand/rand.c"
" src/fw/vendor/tinymt32/tinymt32.c"
" src/fw/process_management/pebble_process_info.c"
" src/fw/util/dict.c",
test_sources_ant_glob="test_app_message.c")
clar(ctx,
sources_ant_glob=(" src/fw/applib/app_inbox.c"
" src/fw/services/normal/app_inbox_service.c"
),
test_sources_ant_glob="test_app_inbox.c",
override_includes=['dummy_board'])
clar(ctx,
sources_ant_glob=(" src/fw/applib/app_outbox.c"
" src/fw/services/normal/app_outbox_service.c"
),
test_sources_ant_glob="test_app_outbox.c",
override_includes=['dummy_board'])
clar(ctx,
sources_ant_glob=(" src/fw/applib/cpu_cache.c"
" src/libos/mcu/cache_arm.c"
),
test_sources_ant_glob="test_cpu_cache.c",
defines=["UNITTEST_WITH_SYSCALL_PRIVILEGES=1"],
override_includes=['dummy_board'])
clar(ctx,
sources_ant_glob = (
" src/fw/flash_region/filesystem_regions.c"
" src/fw/flash_region/flash_region.c "
" src/fw/util/dict.c"
" src/fw/applib/persist.c"
" src/fw/util/rand/rand.c"
" src/fw/vendor/tinymt32/tinymt32.c"
" src/fw/services/normal/filesystem/app_file.c"
" src/fw/services/normal/filesystem/flash_translation.c"
" src/fw/services/normal/filesystem/pfs.c"
" src/fw/services/normal/legacy/persist_map.c"
" src/fw/services/normal/persist.c"
" src/fw/services/normal/settings/settings_file.c"
" src/fw/services/normal/settings/settings_raw_iter.c"
" src/fw/util/crc8.c"
" src/fw/util/legacy_checksum.c"
" tests/fakes/fake_rtc.c"
" tests/fakes/fake_spi_flash.c"
" tests/fixtures/resources/builtin_resources.auto.c"
),
test_sources_ant_glob = "test_persist.c",
override_includes=['dummy_board'])
clar(ctx,
sources_ant_glob = (
" src/fw/flash_region/flash_region.c"
" src/fw/applib/pbl_std/pbl_std.c"
" tests/fakes/fake_rtc.c"
" tests/fakes/fake_spi_flash.c"
),
test_sources_ant_glob = "test_pbl_std.c",
override_includes=['dummy_board'])
clar(ctx,
sources_ant_glob = (
" src/fw/applib/graphics/gbitmap.c"
" src/fw/applib/graphics/gcolor_definitions.c"
" src/fw/applib/graphics/gtypes.c"
" src/fw/applib/graphics/text_render.c"
" src/fw/applib/fonts/codepoint.c"
" tests/fakes/fake_gbitmap_png.c"
),
test_sources_ant_glob = "test_text_render.c",
override_includes=['dummy_board'])
clar(ctx,
sources_ant_glob = " src/fw/flash_region/flash_region.c" \
" src/fw/flash_region/filesystem_regions.c" \
" src/fw/system/hexdump.c" \
" src/fw/applib/fonts/codepoint.c" \
" src/fw/applib/fonts/fonts.c" \
" src/fw/applib/graphics/text_resources.c" \
" src/fw/resource/resource.c" \
" src/fw/resource/resource_storage.c" \
" src/fw/resource/resource_storage_builtin.c" \
" src/fw/resource/resource_storage_file.c" \
" src/fw/resource/resource_storage_flash.c" \
" src/fw/services/normal/filesystem/flash_translation.c" \
" src/fw/services/normal/filesystem/pfs.c" \
" src/fw/services/normal/filesystem/app_file.c" \
" src/fw/util/crc8.c" \
" src/fw/util/legacy_checksum.c" \
" src/fw/drivers/flash/flash_crc.c" \
" tests/fakes/fake_rtc.c" \
" tests/fakes/fake_spi_flash.c" \
" tests/fixtures/resources/builtin_resources.auto.c" \
" tests/fixtures/resources/pfs_resource_table.c",
test_sources_ant_glob = "test_text_resources.c",
override_includes=['dummy_board'])
clar(ctx,
sources_ant_glob = " src/fw/applib/ui/bitmap_layer.c" \
" src/fw/applib/ui/layer.c" \
" tests/fakes/fake_gbitmap_png.c" \
" src/fw/applib/graphics/gcolor_definitions.c" \
" src/fw/applib/graphics/gtypes.c",
test_sources_ant_glob = "test_bitmap_layer.c",
override_includes=['dummy_board'])
clar(ctx,
sources_ant_glob = "src/fw/applib/app_smartstrap.c" \
" src/fw/services/normal/accessory/smartstrap_attribute.c" \
" src/fw/util/mbuf.c" \
" tests/fakes/fake_smartstrap_connection.c" \
" tests/fakes/fake_smartstrap_profiles.c" \
" tests/fakes/fake_smartstrap_state.c",
test_sources_ant_glob = "test_app_smartstrap.c",
defines=["CAPABILITY_HAS_ACCESSORY_CONNECTOR=1"],
override_includes=['dummy_board'])
clar(ctx,
sources_ant_glob=(
"src/fw/applib/graphics/gtypes.c "
"src/fw/applib/ui/layer.c "
"src/fw/applib/unobstructed_area_service.c "
"tests/fakes/fake_events.c "
),
test_sources_ant_glob="test_unobstructed_area_service.c",
override_includes=['dummy_board'])
clar(ctx,
sources_ant_glob=(
" src/fw/applib/app_glance.c"
" src/fw/applib/template_string.c"
" src/fw/services/normal/app_glances/app_glance_service.c"
" src/fw/services/normal/blob_db/app_glance_db.c"
" src/fw/services/normal/timeline/attribute.c"
" src/fw/util/crc8.c"
" src/fw/util/lru_cache.c"
" tests/fakes/fake_rtc.c"
" tests/fakes/fake_settings_file.c"
),
test_sources_ant_glob="test_app_glance.c",
defines=["CAPABILITY_HAS_APP_GLANCES=1"],
override_includes=['dummy_board'])
clar(ctx,
sources_ant_glob=(
"src/fw/applib/template_string.c "
),
test_sources_ant_glob="test_template_string.c",
override_includes=['dummy_board'])
# vim:filetype=python
@@ -0,0 +1,136 @@
/*
* 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 "apps/system_apps/health/health_activity_detail_card.h"
#include "apps/system_apps/health/health_detail_card.h"
#include "apps/system_apps/health/health_data.h"
#include "apps/system_apps/health/health_data_private.h"
#include "apps/system_apps/health/health_progress.h"
#include "test_health_app_includes.h"
// Setup and Teardown
////////////////////////////////////
static GContext s_ctx;
static FrameBuffer s_fb;
GContext *graphics_context_get_current_context(void) {
return &s_ctx;
}
void test_health_activity_detail_card__initialize(void) {
// Setup graphics context
framebuffer_init(&s_fb, &(GSize) {DISP_COLS, DISP_ROWS});
framebuffer_clear(&s_fb);
graphics_context_init(&s_ctx, &s_fb, GContextInitializationMode_App);
s_app_state_get_graphics_context = &s_ctx;
// Setup resources
fake_spi_flash_init(0 /* offset */, 0x1000000 /* length */);
pfs_init(false /* run filesystem check */);
pfs_format(true /* write erase headers */);
load_resource_fixture_in_flash(RESOURCES_FIXTURE_PATH, SYSTEM_RESOURCES_FIXTURE_NAME,
false /* is_next */);
resource_init();
// Setup content indicator
ContentIndicatorsBuffer *buffer = content_indicator_get_current_buffer();
content_indicator_init_buffer(buffer);
}
void test_health_activity_detail_card__cleanup(void) {
}
// Helpers
//////////////////////
static Window* prv_create_card_and_render(HealthData *health_data) {
Window *window = health_activity_detail_card_create(health_data);
window_set_on_screen(window, true, true);
window_render(window, &s_ctx);
return window;
}
// Tests
//////////////////////
void test_health_activity_detail_card__render_no_data(void) {
prv_create_card_and_render(&(HealthData) {});
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_health_activity_detail_card__render_current_calories_and_distance(void) {
HealthData health_data = {
.current_calories = 123,
.current_distance_meters = 4000,
};
prv_create_card_and_render(&health_data);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_health_activity_detail_card__render_no_calories(void) {
HealthData health_data = {
.current_calories = 0,
};
prv_create_card_and_render(&health_data);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_health_activity_detail_card__render_no_distance(void) {
HealthData health_data = {
.current_distance_meters = 0,
};
prv_create_card_and_render(&health_data);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_health_activity_detail_card__render_step_data(void) {
HealthData health_data = {
.step_data = {600, 900, 700, 1200, 1400, 1300, 1000},
.monthly_step_average = 1000,
};
HealthDetailCard *card = (HealthDetailCard *)prv_create_card_and_render(&health_data);
#if PBL_ROUND
menu_layer_set_selected_index(&card->menu_layer, MenuIndex(0, 3), MenuRowAlignCenter, false);
#endif
window_render(&card->window, &s_ctx);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_health_activity_detail_card__render_day_label_no_steps(void) {
HealthData health_data = {
.step_data = {600, 0, 700},
.monthly_step_average = 1000,
};
HealthDetailCard *card = (HealthDetailCard *)prv_create_card_and_render(&health_data);
#if PBL_ROUND
menu_layer_set_selected_index(&card->menu_layer, MenuIndex(0, 2), MenuRowAlignCenter, false);
#endif
window_render(&card->window, &s_ctx);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
@@ -0,0 +1,340 @@
/*
* 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 "apps/system_apps/health/health_data.h"
#include "apps/system_apps/health/health_data_private.h"
#include "apps/system_apps/health/health_activity_summary_card.h"
#include "apps/system_apps/health/health_activity_detail_card.h"
#include "apps/system_apps/health/health_detail_card.h"
#include "test_health_app_includes.h"
// Setup and Teardown
////////////////////////////////////
static GContext s_ctx;
static FrameBuffer s_fb;
GContext *graphics_context_get_current_context(void) {
return &s_ctx;
}
void test_health_activity_summary_card__initialize(void) {
// Setup graphics context
framebuffer_init(&s_fb, &(GSize) {DISP_COLS, DISP_ROWS});
framebuffer_clear(&s_fb);
graphics_context_init(&s_ctx, &s_fb, GContextInitializationMode_App);
s_app_state_get_graphics_context = &s_ctx;
// Setup resources
fake_spi_flash_init(0 /* offset */, 0x1000000 /* length */);
pfs_init(false /* run filesystem check */);
pfs_format(true /* write erase headers */);
load_resource_fixture_in_flash(RESOURCES_FIXTURE_PATH, SYSTEM_RESOURCES_FIXTURE_NAME,
false /* is_next */);
resource_init();
// Setup content indicator
ContentIndicatorsBuffer *buffer = content_indicator_get_current_buffer();
content_indicator_init_buffer(buffer);
}
void test_health_activity_summary_card__cleanup(void) {
}
// Helpers
//////////////////////
static void prv_create_card_and_render(HealthData *health_data) {
Window window;
window_init(&window, WINDOW_NAME("Health"));
Layer *window_layer = window_get_root_layer(&window);
Layer *card_layer = health_activity_summary_card_create(health_data);
layer_set_frame(card_layer, &window_layer->bounds);
layer_add_child(window_layer, card_layer);
window_set_background_color(&window, health_activity_summary_card_get_bg_color(card_layer));
window_set_on_screen(&window, true, true);
window_render(&window, &s_ctx);
}
// Tests
//////////////////////
void test_health_activity_summary_card__render_no_data(void) {
prv_create_card_and_render(&(HealthData) {});
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_health_activity_summary_card__no_current_steps(void) {
HealthData health_data = {
.step_data = 0,
.step_averages = {10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 50}, // 1000
.current_step_average = 750,
};
prv_create_card_and_render(&health_data);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_health_activity_summary_card__render_current_behind_typical1(void) {
HealthData health_data = {
.step_data = 170,
.step_averages = {10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 50}, // 1000
.current_step_average = 340,
};
prv_create_card_and_render(&health_data);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_health_activity_summary_card__render_current_behind_typical2(void) {
HealthData health_data = {
.step_data = 320,
.step_averages = {10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 50}, // 1000
.current_step_average = 340,
};
prv_create_card_and_render(&health_data);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_health_activity_summary_card__render_current_behind_typical3(void) {
HealthData health_data = {
.step_data = 460,
.step_averages = {10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 50}, // 1000
.current_step_average = 555,
};
prv_create_card_and_render(&health_data);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_health_activity_summary_card__render_current_behind_typical4(void) {
HealthData health_data = {
.step_data = 699,
.step_averages = {10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 50}, // 1000
.current_step_average = 840,
};
prv_create_card_and_render(&health_data);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_health_activity_summary_card__render_current_behind_typical5(void) {
HealthData health_data = {
.step_data = 837,
.step_averages = {10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 50}, // 1000
.current_step_average = 914,
};
prv_create_card_and_render(&health_data);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_health_activity_summary_card__render_current_equals_typical(void) {
HealthData health_data = {
.step_data = 837,
.step_averages = {10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 50}, // 1000
.current_step_average = 837,
};
prv_create_card_and_render(&health_data);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_health_activity_summary_card__render_current_above_typical1(void) {
HealthData health_data = {
.step_data = 340,
.step_averages = {10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 50}, // 1000
.current_step_average = 170,
};
prv_create_card_and_render(&health_data);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_health_activity_summary_card__render_current_above_typical2(void) {
HealthData health_data = {
.step_data = 400,
.step_averages = {10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 50}, // 1000
.current_step_average = 379,
};
prv_create_card_and_render(&health_data);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_health_activity_summary_card__render_current_above_typical3(void) {
HealthData health_data = {
.step_data = 780,
.step_averages = {10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 50}, // 1000
.current_step_average = 480,
};
prv_create_card_and_render(&health_data);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_health_activity_summary_card__render_current_above_typical4(void) {
HealthData health_data = {
.step_data = 866,
.step_averages = {10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 50}, // 1000
.current_step_average = 700,
};
prv_create_card_and_render(&health_data);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_health_activity_summary_card__render_current_above_typical5(void) {
HealthData health_data = {
.step_data = 970,
.step_averages = {10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 50}, // 1000
.current_step_average = 900,
};
prv_create_card_and_render(&health_data);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_health_activity_summary_card__render_current_above_expected(void) {
HealthData health_data = {
.step_data = {2000},
.step_averages = {10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 50}, // 1000
.current_step_average = 800,
};
prv_create_card_and_render(&health_data);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
@@ -0,0 +1,64 @@
/*
* 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/ui/window_private.h"
#include "util/size.h"
#include "clar.h"
// Fakes
/////////////////////
#include "fake_content_indicator.h"
#include "fake_rtc.h"
#include "fake_spi_flash.h"
#include "fixtures/load_test_resources.h"
// Stubs
/////////////////////
#include "stubs_activity.h"
#include "stubs_analytics.h"
#include "stubs_animation_timing.h"
#include "stubs_app_install_manager.h"
#include "stubs_app_launch_reason.h"
#include "stubs_app_timer.h"
#include "stubs_app_window_stack.h"
#include "stubs_bootbits.h"
#include "stubs_click.h"
#include "stubs_health_service.h"
#include "stubs_layer.h"
#include "stubs_logging.h"
#include "stubs_memory_layout.h"
#include "stubs_mutex.h"
#include "stubs_passert.h"
#include "stubs_pbl_malloc.h"
#include "stubs_pebble_process_info.h"
#include "stubs_pebble_tasks.h"
#include "stubs_prompt.h"
#include "stubs_serial.h"
#include "stubs_shell_prefs.h"
#include "stubs_sleep.h"
#include "stubs_status_bar_layer.h"
#include "stubs_syscalls.h"
#include "stubs_task_watchdog.h"
#include "stubs_window_manager.h"
#include "stubs_window_stack.h"
// Helper Functions
/////////////////////
#include "fw/graphics/util.h"
@@ -0,0 +1,86 @@
/*
* 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 "apps/system_apps/health/health_card_view.h"
#include "apps/system_apps/health/health_data.h"
#include "apps/system_apps/health/health_data_private.h"
#include "apps/system_apps/health/health_detail_card.h"
#include "apps/system_apps/health/health_activity_summary_card.h"
#include "apps/system_apps/health/health_activity_detail_card.h"
#include "apps/system_apps/health/health_hr_summary_card.h"
#include "apps/system_apps/health/health_sleep_summary_card.h"
#include "apps/system_apps/health/health_sleep_detail_card.h"
#include "test_health_app_includes.h"
// Fakes
////////////////////////////////////
void clock_get_until_time_without_fulltime(char *buffer, int buf_size, time_t timestamp,
int max_relative_hrs) {
snprintf(buffer, buf_size, "5 MIN AGO");
}
// Setup and Teardown
////////////////////////////////////
static GContext s_ctx;
static FrameBuffer s_fb;
GContext *graphics_context_get_current_context(void) {
return &s_ctx;
}
void test_health_card_view__initialize(void) {
// Setup graphics context
framebuffer_init(&s_fb, &(GSize) {DISP_COLS, DISP_ROWS});
framebuffer_clear(&s_fb);
graphics_context_init(&s_ctx, &s_fb, GContextInitializationMode_App);
s_app_state_get_graphics_context = &s_ctx;
// Setup resources
fake_spi_flash_init(0 /* offset */, 0x1000000 /* length */);
pfs_init(false /* run filesystem check */);
pfs_format(true /* write erase headers */);
load_resource_fixture_in_flash(RESOURCES_FIXTURE_PATH, SYSTEM_RESOURCES_FIXTURE_NAME,
false /* is_next */);
resource_init();
// Setup content indicator
ContentIndicatorsBuffer *buffer = content_indicator_get_current_buffer();
content_indicator_init_buffer(buffer);
}
void test_health_card_view__cleanup(void) {
}
// Helpers
//////////////////////
static Window* prv_create_card_and_render(HealthData *health_data) {
Window *window = (Window *)health_card_view_create(health_data);
window_set_on_screen(window, true, true);
window_render(window, &s_ctx);
return window;
}
// Tests
//////////////////////
void test_health_card_view__render_indicators(void) {
prv_create_card_and_render(&(HealthData) {});
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
@@ -0,0 +1,343 @@
/*
* 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 "apps/system_apps/health/health_detail_card.h"
#include "apps/system_apps/health/health_progress.h"
#include "test_health_app_includes.h"
#define BG_COLOR PBL_IF_COLOR_ELSE(GColorBlueMoon, GColorWhite)
#define FILL_COLOR PBL_IF_COLOR_ELSE(GColorKellyGreen, GColorDarkGray)
#define TODAY_FILL_COLOR PBL_IF_COLOR_ELSE(GColorMediumAquamarine, GColorDarkGray)
#define DEFAULT_ZONES { \
{ .label = "Today", .progress = 700, .fill_color = TODAY_FILL_COLOR, .hide_typical = true }, \
{ .label = "Wed", .progress = 1100, .fill_color = FILL_COLOR }, \
{ .label = "Tue", .progress = 400, .fill_color = FILL_COLOR }, \
{ .label = "Mon", .progress = 1300, .fill_color = FILL_COLOR }, \
{ .label = "Sun", .progress = 800, .fill_color = FILL_COLOR }, \
{ .label = "Sat", .progress = 700, .fill_color = FILL_COLOR }, \
{ .label = "Fri", .progress = 1200, .fill_color = FILL_COLOR }, \
}
static HealthDetailZone s_zones[] = DEFAULT_ZONES;
// Setup and Teardown
////////////////////////////////////
static GContext s_ctx;
static FrameBuffer s_fb;
GContext *graphics_context_get_current_context(void) {
return &s_ctx;
}
void test_health_detail_card__initialize(void) {
// Setup graphics context
framebuffer_init(&s_fb, &(GSize) {DISP_COLS, DISP_ROWS});
framebuffer_clear(&s_fb);
graphics_context_init(&s_ctx, &s_fb, GContextInitializationMode_App);
s_app_state_get_graphics_context = &s_ctx;
// Setup resources
fake_spi_flash_init(0 /* offset */, 0x1000000 /* length */);
pfs_init(false /* run filesystem check */);
pfs_format(true /* write erase headers */);
load_resource_fixture_in_flash(RESOURCES_FIXTURE_PATH, SYSTEM_RESOURCES_FIXTURE_NAME,
false /* is_next */);
resource_init();
// Setup content indicator
ContentIndicatorsBuffer *buffer = content_indicator_get_current_buffer();
content_indicator_init_buffer(buffer);
}
void test_health_detail_card__cleanup(void) {
}
// Helpers
//////////////////////
static Window* prv_create_card_and_render(HealthDetailCardConfig *config) {
Window *window = (Window *)health_detail_card_create(config);
window_set_on_screen(window, true, true);
window_render(window, &s_ctx);
return window;
}
// Tests
//////////////////////
void test_health_detail_card__render_no_data(void) {
prv_create_card_and_render(&(HealthDetailCardConfig) {});
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_health_detail_card__render_one_heading(void) {
HealthDetailCardConfig config = {
.num_headings = 1,
.headings = &(HealthDetailHeading) {
.primary_label = "LABEL1",
.primary_value = "value1",
.fill_color = GColorWhite,
#if PBL_BW
.outline_color = GColorBlack,
#endif
},
.bg_color = BG_COLOR,
};
prv_create_card_and_render(&config);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_health_detail_card__render_two_headings(void) {
HealthDetailCardConfig config = {
.num_headings = 1,
.headings = &(HealthDetailHeading) {
.primary_label = "LABEL1",
.primary_value = "value1",
.secondary_label = "LABEL2",
.secondary_value = "value2",
.fill_color = GColorWhite,
#if PBL_BW
.outline_color = GColorBlack,
#endif
},
.bg_color = BG_COLOR,
};
prv_create_card_and_render(&config);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_health_detail_card__render_subtitle_text(void) {
HealthDetailCardConfig config = {
.num_headings = 1,
.headings = &(HealthDetailHeading) {
.primary_label = "LABEL1",
.primary_value = "value1",
.secondary_label = "LABEL2",
.secondary_value = "value2",
.fill_color = GColorWhite,
#if PBL_BW
.outline_color = GColorBlack,
#endif
},
.num_subtitles = 1,
.subtitles = &(HealthDetailSubtitle) {
.label = "30 DAY AVG",
.fill_color = PBL_IF_COLOR_ELSE(GColorYellow, GColorBlack),
},
.bg_color = BG_COLOR,
};
prv_create_card_and_render(&config);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_health_detail_card__render_no_subtitle(void) {
HealthDetailCardConfig config = {
.num_headings = 1,
.headings = &(HealthDetailHeading) {
.primary_label = "LABEL1",
.primary_value = "value1",
.fill_color = GColorWhite,
#if PBL_BW
.outline_color = GColorBlack,
#endif
},
.bg_color = BG_COLOR,
.daily_avg = 900,
.weekly_max = 1300,
.num_zones = ARRAY_LENGTH(s_zones),
.zones = s_zones,
};
prv_create_card_and_render(&config);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_health_detail_card__render_zones(void) {
HealthDetailCardConfig config = {
.num_headings = 1,
.headings = &(HealthDetailHeading) {
.primary_label = "LABEL1",
.primary_value = "value1",
.secondary_label = "LABEL2",
.secondary_value = "value2",
.fill_color = GColorWhite,
#if PBL_BW
.outline_color = GColorBlack,
#endif
},
.num_subtitles = 1,
.subtitles = &(HealthDetailSubtitle) {
.label = "30 DAY AVG",
.fill_color = PBL_IF_COLOR_ELSE(GColorYellow, GColorBlack),
},
.bg_color = BG_COLOR,
.daily_avg = 900,
.weekly_max = 1300,
.num_zones = ARRAY_LENGTH(s_zones),
.zones = s_zones,
};
HealthDetailCard *card = (HealthDetailCard *)prv_create_card_and_render(&config);
#if PBL_ROUND
menu_layer_set_selected_index(&card->menu_layer, MenuIndex(0, 3), MenuRowAlignCenter, false);
#endif
window_render(&card->window, &s_ctx);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_health_detail_card__render_bg_and_zone_colors(void) {
HealthDetailCardConfig config = {
.num_headings = 1,
.headings = &(HealthDetailHeading) {
.primary_label = "LABEL1",
.primary_value = "value1",
.fill_color = GColorWhite,
#if PBL_BW
.outline_color = GColorBlack,
#endif
},
.num_subtitles = 1,
.subtitles = &(HealthDetailSubtitle) {
.label = "30 DAY AVG",
.fill_color = PBL_IF_COLOR_ELSE(GColorYellow, GColorBlack),
},
.bg_color = BG_COLOR,
.daily_avg = 900,
.weekly_max = 1300,
.num_zones = ARRAY_LENGTH(s_zones),
.zones = s_zones,
};
HealthDetailCard *card = (HealthDetailCard *)prv_create_card_and_render(&config);
#if PBL_ROUND
menu_layer_set_selected_index(&card->menu_layer, MenuIndex(0, 2), MenuRowAlignCenter, false);
#endif
window_render(&card->window, &s_ctx);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_health_detail_card__render_crown(void) {
HealthDetailZone zones[] = DEFAULT_ZONES;
zones[1].show_crown = true;
HealthDetailCardConfig config = {
.num_headings = 1,
.headings = &(HealthDetailHeading) {
.primary_label = "LABEL1",
.primary_value = "value1",
.fill_color = GColorWhite,
#if PBL_BW
.outline_color = GColorBlack,
#endif
},
.num_subtitles = 1,
.subtitles = &(HealthDetailSubtitle) {
.label = "30 DAY AVG",
.fill_color = PBL_IF_COLOR_ELSE(GColorYellow, GColorBlack),
},
.bg_color = BG_COLOR,
.daily_avg = 900,
.weekly_max = 1300,
.num_zones = ARRAY_LENGTH(zones),
.zones = zones,
};
HealthDetailCard *card = (HealthDetailCard *)prv_create_card_and_render(&config);
#if PBL_ROUND
menu_layer_set_selected_index(&card->menu_layer, MenuIndex(0, 2), MenuRowAlignCenter, false);
#endif
window_render(&card->window, &s_ctx);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_health_detail_card__render_zone_hide_typical(void) {
HealthDetailCardConfig config = {
.num_headings = 1,
.headings = &(HealthDetailHeading) {
.primary_label = "LABEL1",
.primary_value = "value1",
.fill_color = GColorWhite,
#if PBL_BW
.outline_color = GColorBlack,
#endif
},
.bg_color = BG_COLOR,
.daily_avg = 900,
.weekly_max = 1300,
.num_zones = ARRAY_LENGTH(s_zones),
.zones = s_zones,
};
HealthDetailCard *card = (HealthDetailCard *)prv_create_card_and_render(&config);
#if PBL_ROUND
menu_layer_set_selected_index(&card->menu_layer, MenuIndex(0, 1), MenuRowAlignCenter, false);
#endif
window_render(&card->window, &s_ctx);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_health_detail_card__scroll_down(void) {
HealthDetailCardConfig config = {
.num_headings = 1,
.headings = &(HealthDetailHeading) {
.primary_label = "LABEL1",
.primary_value = "value1",
.fill_color = GColorWhite,
#if PBL_BW
.outline_color = GColorBlack,
#endif
},
.bg_color = BG_COLOR,
.daily_avg = 900,
.weekly_max = 1300,
.num_zones = ARRAY_LENGTH(s_zones),
.zones = s_zones,
};
HealthDetailCard *card = (HealthDetailCard *)prv_create_card_and_render(&config);
#if PBL_ROUND
menu_layer_set_selected_index(&card->menu_layer, MenuIndex(0, 7), MenuRowAlignCenter, false);
#else
GPoint offset = scroll_layer_get_content_offset(&card->scroll_layer);
offset.y -= scroll_layer_get_content_size(&card->scroll_layer).h;
scroll_layer_set_content_offset(&card->scroll_layer, offset, false);
#endif
window_render(&card->window, &s_ctx);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
@@ -0,0 +1,109 @@
/*
* 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 "apps/system_apps/health/health_hr_detail_card.h"
#include "apps/system_apps/health/health_detail_card.h"
#include "apps/system_apps/health/health_data.h"
#include "apps/system_apps/health/health_data_private.h"
#include "test_health_app_includes.h"
// Setup and Teardown
////////////////////////////////////
static GContext s_ctx;
static FrameBuffer s_fb;
GContext *graphics_context_get_current_context(void) {
return &s_ctx;
}
void test_health_hr_detail_card__initialize(void) {
// Setup graphics context
framebuffer_init(&s_fb, &(GSize) {DISP_COLS, DISP_ROWS});
framebuffer_clear(&s_fb);
graphics_context_init(&s_ctx, &s_fb, GContextInitializationMode_App);
s_app_state_get_graphics_context = &s_ctx;
// Setup resources
fake_spi_flash_init(0 /* offset */, 0x1000000 /* length */);
pfs_init(false /* run filesystem check */);
pfs_format(true /* write erase headers */);
load_resource_fixture_in_flash(RESOURCES_FIXTURE_PATH, SYSTEM_RESOURCES_FIXTURE_NAME,
false /* is_next */);
resource_init();
// Setup content indicator
ContentIndicatorsBuffer *buffer = content_indicator_get_current_buffer();
content_indicator_init_buffer(buffer);
}
void test_health_hr_detail_card__cleanup(void) {
}
// Helpers
//////////////////////
static Window* prv_create_card_and_render(HealthData *health_data) {
Window *window = (Window *)health_hr_detail_card_create(health_data);
window_set_on_screen(window, true, true);
window_render(window, &s_ctx);
return window;
}
// Tests
//////////////////////
void test_health_hr_detail_card__render_no_data(void) {
prv_create_card_and_render(&(HealthData) {});
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_health_hr_detail_card__render_zones(void) {
HealthData health_data = {
.hr_zone1_minutes = 21,
.hr_zone2_minutes = 13,
.hr_zone3_minutes = 6,
};
HealthDetailCard *card = (HealthDetailCard *)prv_create_card_and_render(&health_data);
#if PBL_ROUND
menu_layer_set_selected_index(&card->menu_layer, MenuIndex(0, 2), MenuRowAlignCenter, false);
#endif
window_render(&card->window, &s_ctx);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_health_hr_detail_card__render_zones2(void) {
HealthData health_data = {
.hr_zone1_minutes = 30,
.hr_zone2_minutes = 35,
.hr_zone3_minutes = 40,
};
HealthDetailCard *card = (HealthDetailCard *)prv_create_card_and_render(&health_data);
#if PBL_ROUND
menu_layer_set_selected_index(&card->menu_layer, MenuIndex(0, 2), MenuRowAlignCenter, false);
#endif
window_render(&card->window, &s_ctx);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
@@ -0,0 +1,108 @@
/*
* 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 "apps/system_apps/health/health_data.h"
#include "apps/system_apps/health/health_data_private.h"
#include "apps/system_apps/health/health_hr_summary_card.h"
#include "apps/system_apps/health/health_hr_detail_card.h"
#include "apps/system_apps/health/health_detail_card.h"
#include "test_health_app_includes.h"
// Fakes
////////////////////////////////////
void clock_get_until_time_without_fulltime(char *buffer, int buf_size, time_t timestamp,
int max_relative_hrs) {
snprintf(buffer, buf_size, "5 MIN AGO");
}
// Setup and Teardown
////////////////////////////////////
static GContext s_ctx;
static FrameBuffer s_fb;
GContext *graphics_context_get_current_context(void) {
return &s_ctx;
}
void test_health_hr_summary_card__initialize(void) {
// Setup graphics context
framebuffer_init(&s_fb, &(GSize) {DISP_COLS, DISP_ROWS});
framebuffer_clear(&s_fb);
graphics_context_init(&s_ctx, &s_fb, GContextInitializationMode_App);
s_app_state_get_graphics_context = &s_ctx;
// Setup resources
fake_spi_flash_init(0 /* offset */, 0x1000000 /* length */);
pfs_init(false /* run filesystem check */);
pfs_format(true /* write erase headers */);
load_resource_fixture_in_flash(RESOURCES_FIXTURE_PATH, SYSTEM_RESOURCES_FIXTURE_NAME,
false /* is_next */);
resource_init();
// Setup content indicator
ContentIndicatorsBuffer *buffer = content_indicator_get_current_buffer();
content_indicator_init_buffer(buffer);
}
void test_health_hr_summary_card__cleanup(void) {
}
// Helpers
//////////////////////
static void prv_create_card_and_render(HealthData *health_data) {
Window window;
window_init(&window, WINDOW_NAME("Health"));
Layer *window_layer = window_get_root_layer(&window);
Layer *card_layer = health_hr_summary_card_create(health_data);
layer_set_frame(card_layer, &window_layer->bounds);
layer_add_child(window_layer, card_layer);
window_set_background_color(&window, health_hr_summary_card_get_bg_color(card_layer));
window_set_on_screen(&window, true, true);
window_render(&window, &s_ctx);
}
// Tests
//////////////////////
void test_health_hr_summary_card__render_no_data(void) {
prv_create_card_and_render(&(HealthData) {});
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_health_hr_summary_card__render_current_bpm(void) {
HealthData health_data = {
.current_hr_bpm = 110,
};
prv_create_card_and_render(&health_data);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_health_hr_summary_card__render_timestamp(void) {
rtc_set_time(SECONDS_PER_DAY + (SECONDS_PER_HOUR * 12));
HealthData health_data = {
.current_hr_bpm = 110,
.hr_last_updated = rtc_get_time() - (SECONDS_PER_MINUTE * 5),
};
prv_create_card_and_render(&health_data);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
@@ -0,0 +1,173 @@
/*
* 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 "apps/system_apps/health/health_sleep_detail_card.h"
#include "apps/system_apps/health/health_detail_card.h"
#include "apps/system_apps/health/health_data.h"
#include "apps/system_apps/health/health_data_private.h"
#include "test_health_app_includes.h"
// Setup and Teardown
////////////////////////////////////
static GContext s_ctx;
static FrameBuffer s_fb;
GContext *graphics_context_get_current_context(void) {
return &s_ctx;
}
void test_health_sleep_detail_card__initialize(void) {
// Setup graphics context
framebuffer_init(&s_fb, &(GSize) {DISP_COLS, DISP_ROWS});
framebuffer_clear(&s_fb);
graphics_context_init(&s_ctx, &s_fb, GContextInitializationMode_App);
s_app_state_get_graphics_context = &s_ctx;
// Setup resources
fake_spi_flash_init(0 /* offset */, 0x1000000 /* length */);
pfs_init(false /* run filesystem check */);
pfs_format(true /* write erase headers */);
load_resource_fixture_in_flash(RESOURCES_FIXTURE_PATH, SYSTEM_RESOURCES_FIXTURE_NAME,
false /* is_next */);
resource_init();
// Setup content indicator
ContentIndicatorsBuffer *buffer = content_indicator_get_current_buffer();
content_indicator_init_buffer(buffer);
}
void test_health_sleep_detail_card__cleanup(void) {
}
// Helpers
//////////////////////
// static void prv_create_card_and_render(HealthData *health_data) {
// Window *window = health_sleep_detail_card_create(health_data);
// window_set_on_screen(window, true, true);
// window_render(window, &s_ctx);
// }
static Window* prv_create_card_and_render(HealthData *health_data) {
Window *window = (Window *)health_sleep_detail_card_create(health_data);
window_set_on_screen(window, true, true);
window_render(window, &s_ctx);
return window;
}
// Tests
//////////////////////
void test_health_sleep_detail_card__render_no_data(void) {
prv_create_card_and_render(&(HealthData) {});
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_health_sleep_detail_card__render_sleep_session(void) {
HealthData health_data = {
.sleep_start = (23 * SECONDS_PER_HOUR) + (3 * SECONDS_PER_MINUTE),
.sleep_end = (7 * SECONDS_PER_HOUR) + (45 * SECONDS_PER_MINUTE),
};
prv_create_card_and_render(&health_data);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_health_sleep_detail_card__render_sleep_session_same_start_end_time(void) {
HealthData health_data = {
.sleep_start = (16 * SECONDS_PER_HOUR),
.sleep_end = (16 * SECONDS_PER_HOUR),
};
prv_create_card_and_render(&health_data);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_health_sleep_detail_card__render_30_day_avg(void) {
HealthData health_data = {
.monthly_sleep_average = (8 * SECONDS_PER_HOUR) + (17 * SECONDS_PER_MINUTE),
};
prv_create_card_and_render(&health_data);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_health_sleep_detail_card__render_deep_sleep(void) {
HealthData health_data = {
.deep_sleep = (3 * SECONDS_PER_HOUR) + (23 * SECONDS_PER_MINUTE),
};
prv_create_card_and_render(&health_data);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_health_sleep_detail_card__render_sleep_data_1(void) {
HealthData health_data = {
.sleep_data[0] = (7 * SECONDS_PER_HOUR) + (11 * SECONDS_PER_MINUTE),
.sleep_data[1] = (6 * SECONDS_PER_HOUR) + (52 * SECONDS_PER_MINUTE),
.sleep_data[2] = (7 * SECONDS_PER_HOUR) + (13 * SECONDS_PER_MINUTE),
.sleep_data[3] = (9 * SECONDS_PER_HOUR) + (21 * SECONDS_PER_MINUTE),
.sleep_data[4] = (9 * SECONDS_PER_HOUR) + (18 * SECONDS_PER_MINUTE),
.monthly_sleep_average = (8 * SECONDS_PER_HOUR) + (17 * SECONDS_PER_MINUTE),
};
HealthDetailCard *card = (HealthDetailCard *)prv_create_card_and_render(&health_data);
#if PBL_ROUND
menu_layer_set_selected_index(&card->menu_layer, MenuIndex(0, 1), MenuRowAlignCenter, false);
#else
GPoint offset = scroll_layer_get_content_offset(&card->scroll_layer);
// scroll past sleep session, deep sleep and avg
offset.y -= 114;
scroll_layer_set_content_offset(&card->scroll_layer, offset, false);
#endif
window_render(&card->window, &s_ctx);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_health_sleep_detail_card__render_sleep_data_2(void) {
HealthData health_data = {
.sleep_data[0] = (7 * SECONDS_PER_HOUR) + (14 * SECONDS_PER_MINUTE),
.sleep_data[1] = (4 * SECONDS_PER_HOUR) + (59 * SECONDS_PER_MINUTE),
.sleep_data[2] = (8 * SECONDS_PER_HOUR) + (17 * SECONDS_PER_MINUTE),
.sleep_data[3] = (5 * SECONDS_PER_HOUR) + (34 * SECONDS_PER_MINUTE),
.sleep_data[4] = (7 * SECONDS_PER_HOUR) + (12 * SECONDS_PER_MINUTE),
.sleep_data[5] = (8 * SECONDS_PER_HOUR) + (12 * SECONDS_PER_MINUTE),
.sleep_data[6] = (10 * SECONDS_PER_HOUR) + (11 * SECONDS_PER_MINUTE),
.monthly_sleep_average = (8 * SECONDS_PER_HOUR) + (36 * SECONDS_PER_MINUTE),
};
HealthDetailCard *card = (HealthDetailCard *)prv_create_card_and_render(&health_data);
#if PBL_ROUND
menu_layer_set_selected_index(&card->menu_layer, MenuIndex(0, 7), MenuRowAlignCenter, false);
#else
GPoint offset = scroll_layer_get_content_offset(&card->scroll_layer);
offset.y -= scroll_layer_get_content_size(&card->scroll_layer).h;
scroll_layer_set_content_offset(&card->scroll_layer, offset, false);
#endif
window_render(&card->window, &s_ctx);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
@@ -0,0 +1,279 @@
/*
* 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 "apps/system_apps/health/health_data.h"
#include "apps/system_apps/health/health_data_private.h"
#include "apps/system_apps/health/health_sleep_summary_card.h"
#include "test_health_app_includes.h"
// Setup and Teardown
////////////////////////////////////
static GContext s_ctx;
static FrameBuffer s_fb;
const int s_now_utc = 1467763200; // July 6, 2016
GContext *graphics_context_get_current_context(void) {
return &s_ctx;
}
void test_health_sleep_summary_card__initialize(void) {
// Setup graphics context
framebuffer_init(&s_fb, &(GSize) {DISP_COLS, DISP_ROWS});
framebuffer_clear(&s_fb);
graphics_context_init(&s_ctx, &s_fb, GContextInitializationMode_App);
s_app_state_get_graphics_context = &s_ctx;
// Setup resources
fake_spi_flash_init(0 /* offset */, 0x1000000 /* length */);
pfs_init(false /* run filesystem check */);
pfs_format(true /* write erase headers */);
load_resource_fixture_in_flash(RESOURCES_FIXTURE_PATH, SYSTEM_RESOURCES_FIXTURE_NAME,
false /* is_next */);
resource_init();
// Setup content indicator
ContentIndicatorsBuffer *buffer = content_indicator_get_current_buffer();
content_indicator_init_buffer(buffer);
}
void test_health_sleep_summary_card__cleanup(void) {
}
// Helpers
//////////////////////
static void prv_create_card_and_render(HealthData *health_data) {
Window window;
window_init(&window, WINDOW_NAME("Health"));
Layer *window_layer = window_get_root_layer(&window);
Layer *card_layer = health_sleep_summary_card_create(health_data);
layer_set_frame(card_layer, &window_layer->bounds);
layer_add_child(window_layer, card_layer);
window_set_background_color(&window, health_sleep_summary_card_get_bg_color(card_layer));
window_set_on_screen(&window, true, true);
window_render(&window, &s_ctx);
}
// Tests
//////////////////////
void test_health_sleep_summary_card__render_no_data(void) {
prv_create_card_and_render(&(HealthData) {});
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_health_sleep_summary_card__render_no_typical(void) {
HealthData health_data = {
.monthly_sleep_average = (9 * SECONDS_PER_HOUR),
};
prv_create_card_and_render(&health_data);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_health_sleep_summary_card__render_no_sleep_last_night(void) {
HealthData health_data = {
// Used for text
.typical_sleep = (10 * SECONDS_PER_HOUR),
.monthly_sleep_average = (300 * SECONDS_PER_HOUR),
// Used for typical
.typical_sleep_start = -4 * SECONDS_PER_HOUR,
.typical_sleep_end = 7 * SECONDS_PER_HOUR,
};
prv_create_card_and_render(&health_data);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_health_sleep_summary_card__render_sleep_late_start_early_end1(void) {
const time_t start_of_today = time_util_get_midnight_of(s_now_utc);
HealthData health_data = {
// Used for text
.sleep_data[0] = (8 * SECONDS_PER_HOUR) + (12 * SECONDS_PER_MINUTE),
.typical_sleep = (10 * SECONDS_PER_HOUR),
.monthly_sleep_average = (300 * SECONDS_PER_HOUR),
// Used for typical
.sleep_start = -3 * SECONDS_PER_HOUR,
.sleep_end = 5 * SECONDS_PER_HOUR,
.typical_sleep_start = -4 * SECONDS_PER_HOUR,
.typical_sleep_end = 7 * SECONDS_PER_HOUR,
// The sleep ring are filled by sleep sessions
.num_activity_sessions = 3,
.activity_sessions[0] = {
.start_utc = start_of_today - (3 * SECONDS_PER_HOUR), // 9pm
.length_min = (3 * MINUTES_PER_HOUR),
.type = ActivitySessionType_Sleep,
},
.activity_sessions[1] = {
.start_utc = start_of_today + (1 * SECONDS_PER_HOUR), // 1am
.length_min = (4 * MINUTES_PER_HOUR),
.type = ActivitySessionType_Sleep,
},
.activity_sessions[2] = {
.start_utc = start_of_today + (2 * SECONDS_PER_HOUR), // 2am
.length_min = (1 * MINUTES_PER_HOUR) + 30,
.type = ActivitySessionType_RestfulSleep,
},
};
prv_create_card_and_render(&health_data);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_health_sleep_summary_card__render_sleep_late_start_early_end2(void) {
const time_t start_of_today = time_util_get_midnight_of(s_now_utc);
HealthData health_data = {
// Used for text
.sleep_data[0] = (8 * SECONDS_PER_HOUR) + (12 * SECONDS_PER_MINUTE),
.typical_sleep = (10 * SECONDS_PER_HOUR),
.monthly_sleep_average = (300 * SECONDS_PER_HOUR),
// Used for typical
.sleep_start = 0 * SECONDS_PER_HOUR,
.sleep_end = 7 * SECONDS_PER_HOUR,
.typical_sleep_start = -1 * SECONDS_PER_HOUR,
.typical_sleep_end = 8 * SECONDS_PER_HOUR,
// The sleep ring are filled by sleep sessions
.num_activity_sessions = 3,
.activity_sessions[0] = {
.start_utc = start_of_today - (0 * SECONDS_PER_HOUR), // 12am
.length_min = (7 * MINUTES_PER_HOUR),
.type = ActivitySessionType_Sleep,
},
.activity_sessions[1] = {
.start_utc = start_of_today + (2 * SECONDS_PER_HOUR), // 2am
.length_min = (1 * MINUTES_PER_HOUR) + 40,
.type = ActivitySessionType_RestfulSleep,
},
.activity_sessions[2] = {
.start_utc = start_of_today + (4 * SECONDS_PER_HOUR), // 4am
.length_min = (2 * MINUTES_PER_HOUR),
.type = ActivitySessionType_RestfulSleep,
},
};
prv_create_card_and_render(&health_data);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_health_sleep_summary_card__render_sleep_early_start_early_end1(void) {
const time_t start_of_today = time_util_get_midnight_of(s_now_utc);
HealthData health_data = {
// Used for text
.sleep_data[0] = (8 * SECONDS_PER_HOUR) + (12 * SECONDS_PER_MINUTE),
.typical_sleep = (10 * SECONDS_PER_HOUR),
.monthly_sleep_average = (300 * SECONDS_PER_HOUR),
// Used for typical
.sleep_start = -3 * SECONDS_PER_HOUR,
.sleep_end = 7 * SECONDS_PER_HOUR,
.typical_sleep_start = -1 * SECONDS_PER_HOUR,
.typical_sleep_end = 8 * SECONDS_PER_HOUR,
// The sleep ring are filled by sleep sessions
.num_activity_sessions = 3,
.activity_sessions[0] = {
.start_utc = start_of_today - (3 * SECONDS_PER_HOUR), // 9pm
.length_min = (10 * MINUTES_PER_HOUR),
.type = ActivitySessionType_Sleep,
},
.activity_sessions[1] = {
.start_utc = start_of_today - (2 * SECONDS_PER_HOUR), // 10pm
.length_min = (1 * MINUTES_PER_HOUR),
.type = ActivitySessionType_RestfulSleep,
},
.activity_sessions[2] = {
.start_utc = start_of_today + (3 * SECONDS_PER_HOUR), // 3am
.length_min = (1 * MINUTES_PER_HOUR) + 15,
.type = ActivitySessionType_RestfulSleep,
},
};
prv_create_card_and_render(&health_data);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_health_sleep_summary_card__render_sleep_early_start_late_end1(void) {
const time_t start_of_today = time_util_get_midnight_of(s_now_utc);
HealthData health_data = {
// Used for text
.sleep_data[0] = (8 * SECONDS_PER_HOUR) + (12 * SECONDS_PER_MINUTE),
.typical_sleep = (10 * SECONDS_PER_HOUR),
.monthly_sleep_average = (300 * SECONDS_PER_HOUR),
// Used for typical
.sleep_start = -3 * SECONDS_PER_HOUR,
.sleep_end = 7 * SECONDS_PER_HOUR,
.typical_sleep_start = -4 * SECONDS_PER_HOUR,
.typical_sleep_end = 5 * SECONDS_PER_HOUR,
// The sleep ring are filled by sleep sessions
.num_activity_sessions = 2,
.activity_sessions[0] = {
.start_utc = start_of_today - (3 * SECONDS_PER_HOUR),
.length_min = (3 * MINUTES_PER_HOUR),
.type = ActivitySessionType_Sleep,
},
.activity_sessions[1] = {
.start_utc = start_of_today + (1 * SECONDS_PER_HOUR),
.length_min = (6 * MINUTES_PER_HOUR),
.type = ActivitySessionType_Sleep,
},
};
prv_create_card_and_render(&health_data);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_health_sleep_summary_card__render_sleep_late_start_late_end1(void) {
const time_t start_of_today = time_util_get_midnight_of(s_now_utc);
HealthData health_data = {
// Used for text
.sleep_data[0] = (8 * SECONDS_PER_HOUR) + (12 * SECONDS_PER_MINUTE),
.typical_sleep = (10 * SECONDS_PER_HOUR),
.monthly_sleep_average = (300 * SECONDS_PER_HOUR),
// Used for typical
.sleep_start = -4 * SECONDS_PER_HOUR,
.sleep_end = 4 * SECONDS_PER_HOUR,
.typical_sleep_start = -3 * SECONDS_PER_HOUR,
.typical_sleep_end = 3 * SECONDS_PER_HOUR,
// The sleep ring are filled by sleep sessions
.num_activity_sessions = 1,
.activity_sessions[0] = {
.start_utc = start_of_today - (4 * SECONDS_PER_HOUR),
.length_min = (8 * MINUTES_PER_HOUR),
.type = ActivitySessionType_Sleep,
}
};
prv_create_card_and_render(&health_data);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
+193
View File
@@ -0,0 +1,193 @@
from waftools.pebble_test import clar
def build(ctx):
rendering_sources = \
" src/fw/applib/fonts/codepoint.c" \
" src/fw/applib/graphics/${BITDEPTH}_bit/framebuffer.c" \
" src/fw/applib/graphics/${BITDEPTH}_bit/bitblt_private.c" \
" src/fw/applib/graphics/bitblt.c" \
" src/fw/applib/graphics/framebuffer.c" \
" src/fw/applib/graphics/gbitmap.c" \
" src/fw/applib/graphics/gbitmap_png.c" \
" src/fw/applib/graphics/gbitmap_sequence.c" \
" src/fw/applib/graphics/gcolor_definitions.c" \
" src/fw/applib/graphics/gdraw_command.c" \
" src/fw/applib/graphics/gdraw_command_frame.c" \
" src/fw/applib/graphics/gdraw_command_image.c" \
" src/fw/applib/graphics/gdraw_command_list.c" \
" src/fw/applib/graphics/gdraw_command_sequence.c" \
" src/fw/applib/graphics/gpath.c" \
" src/fw/applib/graphics/graphics.c" \
" src/fw/applib/graphics/graphics_bitmap.c" \
" src/fw/applib/graphics/graphics_circle.c" \
" src/fw/applib/graphics/graphics_line.c" \
" src/fw/applib/graphics/graphics_private.c" \
" src/fw/applib/graphics/graphics_private_raw.c" \
" src/fw/applib/graphics/gtransform.c" \
" src/fw/applib/graphics/gtypes.c" \
" src/fw/applib/graphics/perimeter.c" \
" src/fw/applib/graphics/text_layout.c" \
" src/fw/applib/graphics/text_render.c" \
" src/fw/applib/graphics/text_resources.c" \
" src/fw/applib/graphics/utf8.c" \
" src/fw/applib/ui/kino/kino_reel.c" \
" src/fw/applib/ui/kino/kino_reel_gbitmap.c" \
" src/fw/applib/ui/kino/kino_reel_gbitmap_sequence.c" \
" src/fw/applib/ui/kino/kino_reel_pdci.c" \
" src/fw/applib/ui/kino/kino_reel_pdcs.c" \
" src/fw/applib/ui/layer.c" \
" src/fw/applib/ui/menu_layer_system_cells.c" \
" src/fw/applib/vendor/tinflate/tinflate.c" \
" src/fw/applib/vendor/uPNG/upng.c" \
" src/fw/board/displays/display_spalding.c" \
" src/fw/drivers/flash/flash_crc.c" \
" src/fw/flash_region/filesystem_regions.c" \
" src/fw/flash_region/flash_region.c" \
" src/fw/resource/resource.c" \
" src/fw/resource/resource_storage.c" \
" src/fw/resource/resource_storage_builtin.c" \
" src/fw/resource/resource_storage_file.c" \
" src/fw/resource/resource_storage_flash.c" \
" src/fw/services/normal/filesystem/app_file.c" \
" src/fw/services/normal/filesystem/flash_translation.c" \
" src/fw/services/normal/filesystem/pfs.c" \
" src/fw/system/hexdump.c" \
" src/fw/util/crc8.c" \
" src/fw/util/legacy_checksum.c" \
" tests/fakes/fake_applib_resource.c" \
" tests/fakes/fake_display.c" \
" tests/fakes/fake_gbitmap_get_data_row.c" \
" tests/fakes/fake_rtc.c" \
" tests/fakes/fake_spi_flash.c" \
" tests/fixtures/resources/builtin_resources.auto.c" \
" tests/fixtures/resources/pfs_resource_table.c" \
" tests/stubs/stubs_animation.c" \
" tests/stubs/stubs_system_theme.c"
health_app_sources = \
" src/fw/applib/graphics/gpath_builder.c" \
" src/fw/applib/ui/action_button.c" \
" src/fw/applib/ui/animation_interpolate.c" \
" src/fw/applib/ui/content_indicator.c" \
" src/fw/applib/ui/inverter_layer.c" \
" src/fw/applib/ui/kino/kino_layer.c" \
" src/fw/applib/ui/kino/kino_player.c" \
" src/fw/applib/ui/kino/kino_reel/transform.c" \
" src/fw/applib/ui/kino/kino_reel_custom.c" \
" src/fw/applib/ui/menu_layer.c" \
" src/fw/applib/ui/scroll_layer.c" \
" src/fw/applib/ui/shadows.c" \
" src/fw/applib/ui/text_layer.c" \
" src/fw/applib/ui/text_layer_flow.c" \
" src/fw/applib/ui/window.c" \
" src/fw/apps/system_apps/health/health_data.c" \
" src/fw/apps/system_apps/health/health_progress.c" \
" src/fw/apps/system_apps/health/health_ui.c" \
" src/fw/apps/system_apps/timeline/text_node.c" \
" src/fw/services/normal/activity/health_util.c" \
" src/fw/services/normal/timeline/timeline_resources.c" \
" src/fw/util/buffer.c" \
" src/fw/util/stats.c" \
" src/fw/util/time/mktime.c" \
" src/fw/util/time/time.c" \
" tests/fakes/fake_clock.c" \
" tests/fakes/fake_fonts.c" \
" tests/fixtures/resources/timeline_resource_table.auto.c"
clar(ctx,
sources_ant_glob = rendering_sources + \
health_app_sources + \
" src/fw/apps/system_apps/health/health_activity_detail_card.c" \
" src/fw/apps/system_apps/health/health_activity_summary_card.c" \
" src/fw/apps/system_apps/health/health_hr_detail_card.c" \
" src/fw/apps/system_apps/health/health_hr_summary_card.c" \
" src/fw/apps/system_apps/health/health_sleep_detail_card.c" \
" src/fw/apps/system_apps/health/health_sleep_summary_card.c" \
" src/fw/apps/system_apps/health/health_detail_card.c" \
" src/fw/apps/system_apps/health/health_card_view.c",
test_sources_ant_glob = "test_health_card_view.c",
defines=ctx.env.test_image_defines + ["USE_DISPLAY_PERIMETER_ON_FONT_LAYOUT=1"],
runtime_deps=ctx.env.test_pngs + ctx.env.test_pbis + ctx.env.test_pfos,
override_includes=['dummy_board'],
platforms=['snowy', 'spalding', 'silk'])
clar(ctx,
sources_ant_glob = rendering_sources + \
health_app_sources + \
" src/fw/apps/system_apps/health/health_detail_card.c" \
" src/fw/apps/system_apps/health/health_activity_detail_card.c" \
" src/fw/apps/system_apps/health/health_activity_summary_card.c",
test_sources_ant_glob = "test_health_activity_summary_card.c",
defines=ctx.env.test_image_defines + ["USE_DISPLAY_PERIMETER_ON_FONT_LAYOUT=1"],
runtime_deps=ctx.env.test_pngs + ctx.env.test_pbis + ctx.env.test_pfos,
override_includes=['dummy_board'],
platforms=['snowy', 'spalding', 'silk'])
clar(ctx,
sources_ant_glob = rendering_sources + \
health_app_sources + \
" src/fw/apps/system_apps/health/health_detail_card.c" \
" src/fw/apps/system_apps/health/health_sleep_detail_card.c" \
" src/fw/apps/system_apps/health/health_sleep_summary_card.c",
test_sources_ant_glob = "test_health_sleep_summary_card.c",
defines=ctx.env.test_image_defines + ["USE_DISPLAY_PERIMETER_ON_FONT_LAYOUT=1"],
runtime_deps=ctx.env.test_pngs + ctx.env.test_pbis + ctx.env.test_pfos,
override_includes=['dummy_board'],
platforms=['snowy', 'spalding', 'silk'])
clar(ctx,
sources_ant_glob = rendering_sources + \
health_app_sources + \
" src/fw/apps/system_apps/health/health_detail_card.c" \
" src/fw/apps/system_apps/health/health_hr_detail_card.c" \
" src/fw/apps/system_apps/health/health_hr_summary_card.c",
test_sources_ant_glob = "test_health_hr_summary_card.c",
defines=ctx.env.test_image_defines + ["USE_DISPLAY_PERIMETER_ON_FONT_LAYOUT=1"],
runtime_deps=ctx.env.test_pngs + ctx.env.test_pbis + ctx.env.test_pfos,
override_includes=['dummy_board'],
platforms=['silk'])
clar(ctx,
sources_ant_glob = rendering_sources + \
health_app_sources + \
" src/fw/apps/system_apps/health/health_detail_card.c" \
" src/fw/apps/system_apps/health/health_activity_detail_card.c",
test_sources_ant_glob = "test_health_activity_detail_card.c",
defines=ctx.env.test_image_defines + ["USE_DISPLAY_PERIMETER_ON_FONT_LAYOUT=1"],
runtime_deps=ctx.env.test_pngs + ctx.env.test_pbis + ctx.env.test_pfos,
override_includes=['dummy_board'],
platforms=['snowy', 'spalding', 'silk'])
clar(ctx,
sources_ant_glob = rendering_sources + \
health_app_sources + \
" src/fw/apps/system_apps/health/health_detail_card.c" \
" src/fw/apps/system_apps/health/health_sleep_detail_card.c",
test_sources_ant_glob = "test_health_sleep_detail_card.c",
defines=ctx.env.test_image_defines + ["USE_DISPLAY_PERIMETER_ON_FONT_LAYOUT=1"],
runtime_deps=ctx.env.test_pngs + ctx.env.test_pbis + ctx.env.test_pfos,
override_includes=['dummy_board'],
platforms=['snowy', 'spalding', 'silk'])
clar(ctx,
sources_ant_glob = rendering_sources + \
health_app_sources + \
" src/fw/apps/system_apps/health/health_detail_card.c" \
" src/fw/apps/system_apps/health/health_hr_detail_card.c",
test_sources_ant_glob = "test_health_hr_detail_card.c",
defines=ctx.env.test_image_defines + ["USE_DISPLAY_PERIMETER_ON_FONT_LAYOUT=1"],
runtime_deps=ctx.env.test_pngs + ctx.env.test_pbis + ctx.env.test_pfos,
override_includes=['dummy_board'],
platforms=['snowy', 'spalding', 'silk'])
clar(ctx,
sources_ant_glob = rendering_sources + \
health_app_sources + \
" src/fw/apps/system_apps/health/health_detail_card.c",
test_sources_ant_glob = "test_health_detail_card.c",
defines=ctx.env.test_image_defines + ["USE_DISPLAY_PERIMETER_ON_FONT_LAYOUT=1"],
runtime_deps=ctx.env.test_pngs + ctx.env.test_pbis + ctx.env.test_pfos,
override_includes=['dummy_board'],
platforms=['snowy', 'spalding', 'silk'])
# vim:filetype=python
@@ -0,0 +1,442 @@
/*
* 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/ui/window_private.h"
#include "apps/system_apps/launcher/default/launcher_menu_layer.h"
#include "resource/resource_ids.auto.h"
#include "services/normal/app_glances/app_glance_service.h"
#include "services/normal/blob_db/app_glance_db.h"
#include "util/size.h"
static GContext s_ctx;
// Fakes
/////////////////////
#include "fake_content_indicator.h"
#include "fake_settings_file.h"
#include "fake_spi_flash.h"
#include "fixtures/load_test_resources.h"
#include "services/normal/timeline/timeline_resources.h"
extern const uint16_t g_timeline_resources[][TimelineResourceSizeCount];
#define TIMELINE_RESOURCE_TEST_FAKE_PNG (9999 | 0x80000000)
//! Add more values to this enum and the array below to add new apps to the launcher in these
//! unit tests.
typedef enum LauncherMenuLayerTestApp {
LauncherMenuLayerTestApp_Watchfaces,
LauncherMenuLayerTestApp_LongTitle,
LauncherMenuLayerTestApp_InteriorApp,
LauncherMenuLayerTestApp_Travel,
LauncherMenuLayerTestApp_NoIcon,
LauncherMenuLayerTestAppCount
} LauncherMenuLayerTestApp;
typedef struct LauncherMenuLayerTestAppNode {
AppMenuNode node;
uint32_t bitmap_icon_resource_id;
uint32_t pdc_icon_resource_id;
uint32_t bitmap_slice_icon_resource_id;
uint32_t pdc_slice_icon_resource_id;
} LauncherMenuLayerTestAppNode;
static bool s_use_pdc_icons;
static const LauncherMenuLayerTestAppNode s_fake_app_nodes[LauncherMenuLayerTestAppCount] = {
[LauncherMenuLayerTestApp_Watchfaces] = {
.node = {
.name = "Watchfaces",
.uuid = (Uuid) {0xc3, 0xcf, 0xda, 0xa9, 0x76, 0x1f, 0x49, 0x89,
0x99, 0x4c, 0x30, 0x13, 0xcd, 0xc3, 0xef, 0xb9},
},
.bitmap_icon_resource_id = RESOURCE_ID_MENU_LAYER_GENERIC_WATCHFACE_ICON,
.pdc_icon_resource_id = RESOURCE_ID_ALARM_CLOCK_TINY,
.bitmap_slice_icon_resource_id = TIMELINE_RESOURCE_TEST_FAKE_PNG,
.pdc_slice_icon_resource_id = TIMELINE_RESOURCE_BASKETBALL,
},
[LauncherMenuLayerTestApp_LongTitle] = {
.node = {
.name = "Really really long title",
.uuid = (Uuid) {0xd4, 0x17, 0x61, 0x3c, 0x43, 0x31, 0x44, 0x90,
0xa1, 0x68, 0xf2, 0x46, 0x53, 0xd3, 0x76, 0x3a},
},
// these icons are too big and will be replaced with the generic app icon
.bitmap_icon_resource_id = RESOURCE_ID_SETTINGS_ICON_BLUETOOTH,
.pdc_icon_resource_id = RESOURCE_ID_AMERICAN_FOOTBALL_SMALL,
.bitmap_slice_icon_resource_id = TIMELINE_RESOURCE_TEST_FAKE_PNG,
.pdc_slice_icon_resource_id = TIMELINE_RESOURCE_BASKETBALL,
},
[LauncherMenuLayerTestApp_InteriorApp] = {
.node = {
.name = "Interior App",
.uuid = (Uuid) {0x11, 0xcf, 0xac, 0x66, 0x29, 0x9c, 0x4a, 0xa6,
0x94, 0x5d, 0xf0, 0x53, 0x6e, 0xd1, 0x4e, 0xe8},
},
// these icons are too big and will be replaced with the generic app icon
.bitmap_icon_resource_id = RESOURCE_ID_SETTINGS_ICON_BLUETOOTH_ALT,
.pdc_icon_resource_id = RESOURCE_ID_BASEBALL_GAME_SMALL,
.bitmap_slice_icon_resource_id = TIMELINE_RESOURCE_TEST_FAKE_PNG,
.pdc_slice_icon_resource_id = TIMELINE_RESOURCE_BASKETBALL,
},
[LauncherMenuLayerTestApp_Travel] = {
.node = {
.name = "Travel",
.uuid = (Uuid) {0x27, 0x53, 0xd0, 0xc, 0x65, 0xbb, 0x41, 0x83,
0x9c, 0xf1, 0x17, 0x3e, 0x6, 0xdf, 0xda, 0xde},
},
.bitmap_icon_resource_id = RESOURCE_ID_SETTINGS_ICON_AIRPLANE,
.pdc_icon_resource_id = RESOURCE_ID_SCHEDULED_FLIGHT_TINY,
.bitmap_slice_icon_resource_id = TIMELINE_RESOURCE_TEST_FAKE_PNG,
.pdc_slice_icon_resource_id = TIMELINE_RESOURCE_BASKETBALL,
},
[LauncherMenuLayerTestApp_NoIcon] = {
.node = {
.name = "No Icon",
.uuid = (Uuid) {0x7f, 0x4f, 0xc1, 0x32, 0x32, 0x78, 0x47, 0xec,
0x91, 0x64, 0xf1, 0x76, 0xf9, 0xea, 0x1f, 0xc2},
},
}
};
AppMenuNode* app_menu_data_source_get_node_at_index(AppMenuDataSource *source,
uint16_t row_index) {
cl_assert(source);
cl_assert(row_index < ARRAY_LENGTH(s_fake_app_nodes));
const LauncherMenuLayerTestAppNode *test_node = &s_fake_app_nodes[row_index];
static AppMenuNode node_copy;
node_copy = test_node->node;
node_copy.icon_resource_id =
s_use_pdc_icons ? test_node->pdc_icon_resource_id : test_node->bitmap_icon_resource_id;
return &node_copy;
}
uint16_t app_menu_data_source_get_count(AppMenuDataSource *source) {
cl_assert(source);
return ARRAY_LENGTH(s_fake_app_nodes);
}
static GBitmap s_default_app_icon_bitmap;
void app_menu_data_source_enable_icons(AppMenuDataSource *source, uint32_t fallback_icon_id) {
cl_assert(source);
source->show_icons = true;
gbitmap_deinit(&s_default_app_icon_bitmap);
gbitmap_init_with_resource_system(&s_default_app_icon_bitmap, SYSTEM_APP, fallback_icon_id);
source->default_icon = &s_default_app_icon_bitmap;
}
static GBitmap s_app_icon_bitmap;
GBitmap *app_menu_data_source_get_node_icon(AppMenuDataSource *source, AppMenuNode *node) {
cl_assert(source);
cl_assert(node);
if (!node->icon_resource_id) {
return &s_default_app_icon_bitmap;
}
gbitmap_deinit(&s_app_icon_bitmap);
gbitmap_init_with_resource(&s_app_icon_bitmap, node->icon_resource_id);
return &s_app_icon_bitmap;
}
//! We use this function in the app glance service to create a key (the install ID) for an app
//! glance cache entry; just fake it by constructing a 32-bit number from the first 4 bytes of the
//! app's UUID
AppInstallId app_install_get_id_for_uuid(const Uuid *uuid) {
if (!uuid) {
return INSTALL_ID_INVALID;
}
return ((uuid->byte3 << 24) | (uuid->byte2 << 16) | (uuid->byte1 << 8) | uuid->byte0);
}
bool timeline_resources_get_id_system(TimelineResourceId timeline_id, TimelineResourceSize size,
ResAppNum res_app_num, AppResourceInfo *res_info_out) {
if (timeline_id == TIMELINE_RESOURCE_TEST_FAKE_PNG) {
// random PNG resource for testing since no timeline resources use PNGs
res_info_out->res_id = RESOURCE_ID_MUSIC_APP_GLANCE_PLAY;
} else {
res_info_out->res_id = g_timeline_resources[timeline_id & 0x7FFFFFFF][size];
}
return true;
}
bool timeline_resources_is_system(TimelineResourceId timeline_id) {
return false;
}
// Stubs
/////////////////////
#include "stubs_app_cache.h"
#include "stubs_alarm.h"
#include "stubs_alerts.h"
#include "stubs_analytics.h"
#include "stubs_app_manager.h"
#include "stubs_app_window_stack.h"
#include "stubs_app_timer.h"
#include "stubs_battery_state_service.h"
#include "stubs_bluetooth_ctl.h"
#include "stubs_bootbits.h"
#include "stubs_click.h"
#include "stubs_clock.h"
#include "stubs_do_not_disturb.h"
#include "stubs_events.h"
#include "stubs_event_service_client.h"
#include "stubs_health_util.h"
#include "stubs_i18n.h"
#include "stubs_kino_player.h"
#include "stubs_layer.h"
#include "stubs_logging.h"
#include "stubs_memory_layout.h"
#include "stubs_music.h"
#include "stubs_mutex.h"
#include "stubs_notification_storage.h"
#include "stubs_passert.h"
#include "stubs_pebble_process_info.h"
#include "stubs_pebble_tasks.h"
#include "stubs_pbl_malloc.h"
#include "stubs_prompt.h"
#include "stubs_serial.h"
#include "stubs_session.h"
#include "stubs_sleep.h"
#include "stubs_status_bar_layer.h"
#include "stubs_system_theme.h"
#include "stubs_syscalls.h"
#include "stubs_task_watchdog.h"
#include "stubs_tick.h"
#include "stubs_time.h"
#include "stubs_watchface.h"
#include "stubs_weather_service.h"
#include "stubs_weather_types.h"
#include "stubs_window_manager.h"
#include "stubs_window_stack.h"
#include "stubs_workout_service.h"
#include "stubs_workout_utils.h"
// We can't include stubs_process_manager.h because it conflicts with the two helper includes below
void process_manager_send_callback_event_to_process(PebbleTask task, void (*callback)(void *),
void *data) {}
// Helper Functions
/////////////////////
#include "../../../graphics/test_graphics.h"
#include "../../../graphics/util.h"
// Setup and Teardown
////////////////////////////////////
static FrameBuffer *fb = NULL;
GContext *graphics_context_get_current_context(void) {
return &s_ctx;
}
void test_launcher_menu_layer__initialize(void) {
// Setup framebuffer and graphics context
fb = malloc(sizeof(FrameBuffer));
framebuffer_init(fb, &(GSize) {DISP_COLS, DISP_ROWS});
test_graphics_context_init(&s_ctx, fb);
graphics_context_set_antialiased(&s_ctx, true);
// Setup resources
fake_spi_flash_init(0, 0x1000000);
pfs_init(false);
pfs_format(true /* write erase headers */);
load_resource_fixture_in_flash(RESOURCES_FIXTURE_PATH, SYSTEM_RESOURCES_FIXTURE_NAME,
false /* is_next */);
resource_init();
// Setup content indicators buffer
ContentIndicatorsBuffer *buffer = content_indicator_get_current_buffer();
content_indicator_init_buffer(buffer);
// Setup AppGlanceDB
fake_settings_file_reset();
app_glance_db_init();
// Setup AppGlanceService
app_glance_service_init();
// Default to showing bitmap icons
s_use_pdc_icons = false;
}
void app_glance_db_deinit(void);
void test_launcher_menu_layer__cleanup(void) {
app_glance_db_deinit();
gbitmap_deinit(&s_app_icon_bitmap);
gbitmap_deinit(&s_default_app_icon_bitmap);
free(fb);
}
// Helpers
//////////////////////
//! Declared T_STATIC in launcher_menu_layer.c so we can easily change the launcher's selected index
//! from unit tests without also specifying the y offset for the scroll layer that is required by
//! `launcher_menu_layer_set_selection_state()`.
void prv_launcher_menu_layer_set_selection_index(LauncherMenuLayer *launcher_menu_layer,
uint16_t index, MenuRowAlign row_align,
bool animated);
void prv_render_launcher_menu_layer(uint16_t selected_index) {
AppMenuDataSource data_source = {};
app_menu_data_source_init(&data_source, NULL, NULL);
app_menu_data_source_enable_icons(&data_source, RESOURCE_ID_MENU_LAYER_GENERIC_WATCHAPP_ICON);
LauncherMenuLayer launcher_menu_layer = {};
launcher_menu_layer_init(&launcher_menu_layer, &data_source);
const bool animated = false;
// If we used MenuRowAlignCenter on rect then the test images would show the top and bottom
// rows being clipped by the edge of the screen
const MenuRowAlign row_align = PBL_IF_RECT_ELSE(MenuRowAlignTop, MenuRowAlignCenter);
prv_launcher_menu_layer_set_selection_index(&launcher_menu_layer, selected_index,
row_align, animated);
layer_render_tree(launcher_menu_layer_get_layer(&launcher_menu_layer), &s_ctx);
launcher_menu_layer_deinit(&launcher_menu_layer);
app_menu_data_source_deinit(&data_source);
}
// Tests
//////////////////////
void test_launcher_menu_layer__long_title(void) {
prv_render_launcher_menu_layer(LauncherMenuLayerTestApp_LongTitle);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_launcher_menu_layer__no_icon(void) {
prv_render_launcher_menu_layer(LauncherMenuLayerTestApp_NoIcon);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_launcher_menu_layer__interior_app(void) {
prv_render_launcher_menu_layer(LauncherMenuLayerTestApp_InteriorApp);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_launcher_menu_layer__no_icon_app_with_glance(void) {
// Insert a glance with a slice for the app that doesn't have a default icon
const AppGlance glance = (AppGlance) {
.num_slices = 1,
.slices = {
{
.expiration_time = 1464734484, // (Tue, 31 May 2016 22:41:24 GMT)
.type = AppGlanceSliceType_IconAndSubtitle,
.icon_and_subtitle = {
.icon_resource_id = TIMELINE_RESOURCE_SCHEDULED_FLIGHT,
.template_string = "Glances baby!",
},
},
},
};
cl_assert_equal_i(app_glance_db_insert_glance(
&s_fake_app_nodes[LauncherMenuLayerTestApp_NoIcon].node.uuid, &glance), S_SUCCESS);
prv_render_launcher_menu_layer(LauncherMenuLayerTestApp_NoIcon);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
static void prv_insert_glances_for_app_selected_and_apps_above_and_below_with_glances_test(void) {
// Insert glances with 1 slice for the app above the interior app, the interior app, and the app
// below the interior app so we can see that the subtitle is positioned properly in all 3 cases
cl_assert(LauncherMenuLayerTestApp_InteriorApp > 0);
for (LauncherMenuLayerTestApp i = LauncherMenuLayerTestApp_InteriorApp - 1;
i <= LauncherMenuLayerTestApp_InteriorApp + 1; i++) {
const LauncherMenuLayerTestAppNode *test_node = &s_fake_app_nodes[i];
const uint32_t icon_resource_id = s_use_pdc_icons ? test_node->pdc_slice_icon_resource_id :
test_node->bitmap_slice_icon_resource_id;
AppGlance glance = (AppGlance) {
.num_slices = 1,
.slices = {
{
.expiration_time = 1464734484, // (Tue, 31 May 2016 22:41:24 GMT)
.type = AppGlanceSliceType_IconAndSubtitle,
.icon_and_subtitle = {
// Just continue using their default icon, we care more about the subtitle in this test
.icon_resource_id = icon_resource_id,
},
},
},
};
snprintf(glance.slices[0].icon_and_subtitle.template_string,
sizeof(glance.slices[0].icon_and_subtitle.template_string),
"%s glance", s_fake_app_nodes[i].node.name);
cl_assert_equal_i(app_glance_db_insert_glance(&s_fake_app_nodes[i].node.uuid, &glance),
S_SUCCESS);
}
}
void test_launcher_menu_layer__app_selected_and_apps_above_and_below_with_glances(void) {
prv_insert_glances_for_app_selected_and_apps_above_and_below_with_glances_test();
prv_render_launcher_menu_layer(LauncherMenuLayerTestApp_InteriorApp);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_launcher_menu_layer__long_title_pdc(void) {
s_use_pdc_icons = true;
prv_render_launcher_menu_layer(LauncherMenuLayerTestApp_LongTitle);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_launcher_menu_layer__no_icon_pdc(void) {
s_use_pdc_icons = true;
prv_render_launcher_menu_layer(LauncherMenuLayerTestApp_NoIcon);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_launcher_menu_layer__interior_app_pdc(void) {
s_use_pdc_icons = true;
prv_render_launcher_menu_layer(LauncherMenuLayerTestApp_InteriorApp);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_launcher_menu_layer__no_icon_app_with_glance_pdc(void) {
s_use_pdc_icons = true;
// Insert a glance with a slice for the app that doesn't have a default icon
const AppGlance glance = (AppGlance) {
.num_slices = 1,
.slices = {
{
.expiration_time = 1464734484, // (Tue, 31 May 2016 22:41:24 GMT)
.type = AppGlanceSliceType_IconAndSubtitle,
.icon_and_subtitle = {
.icon_resource_id = TIMELINE_RESOURCE_SCHEDULED_FLIGHT,
.template_string = "Glances baby!",
},
},
},
};
cl_assert_equal_i(app_glance_db_insert_glance(
&s_fake_app_nodes[LauncherMenuLayerTestApp_NoIcon].node.uuid, &glance), S_SUCCESS);
prv_render_launcher_menu_layer(LauncherMenuLayerTestApp_NoIcon);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_launcher_menu_layer__app_selected_and_apps_above_and_below_with_glances_pdc(void) {
s_use_pdc_icons = true;
prv_insert_glances_for_app_selected_and_apps_above_and_below_with_glances_test();
prv_render_launcher_menu_layer(LauncherMenuLayerTestApp_InteriorApp);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
+109
View File
@@ -0,0 +1,109 @@
from waftools.pebble_test import clar
def build(ctx):
common_sources_ant_glob = (
" src/fw/applib/fonts/codepoint.c"
" src/fw/applib/graphics/${BITDEPTH}_bit/framebuffer.c"
" src/fw/applib/graphics/${BITDEPTH}_bit/bitblt_private.c"
" src/fw/applib/graphics/bitblt.c"
" src/fw/applib/graphics/gbitmap.c"
" src/fw/applib/graphics/gcolor_definitions.c"
" src/fw/applib/graphics/gdraw_command.c"
" src/fw/applib/graphics/gdraw_command_frame.c"
" src/fw/applib/graphics/gdraw_command_image.c"
" src/fw/applib/graphics/gdraw_command_list.c"
" src/fw/applib/graphics/gdraw_command_sequence.c"
" src/fw/applib/graphics/gdraw_command_transforms.c"
" src/fw/applib/graphics/gpath.c"
" src/fw/applib/graphics/graphics.c"
" src/fw/applib/graphics/graphics_bitmap.c"
" src/fw/applib/graphics/graphics_circle.c"
" src/fw/applib/graphics/graphics_line.c"
" src/fw/applib/graphics/graphics_private.c"
" src/fw/applib/graphics/graphics_private_raw.c"
" src/fw/applib/graphics/gtypes.c"
" src/fw/applib/graphics/text_layout.c"
" src/fw/applib/graphics/text_render.c"
" src/fw/applib/graphics/text_resources.c"
" src/fw/applib/graphics/utf8.c"
" src/fw/applib/ui/animation_interpolate.c"
" src/fw/applib/ui/animation_timing.c"
" src/fw/applib/ui/inverter_layer.c"
" src/fw/applib/ui/layer.c"
" src/fw/applib/ui/menu_layer.c"
" src/fw/applib/ui/scroll_layer.c"
" src/fw/applib/ui/shadows.c"
" src/fw/applib/ui/window.c"
" src/fw/drivers/flash/flash_crc.c"
" src/fw/flash_region/filesystem_regions.c"
" src/fw/flash_region/flash_region.c"
" src/fw/resource/resource.c"
" src/fw/resource/resource_storage.c"
" src/fw/resource/resource_storage_builtin.c"
" src/fw/resource/resource_storage_file.c"
" src/fw/resource/resource_storage_flash.c"
" src/fw/services/normal/filesystem/flash_translation.c"
" src/fw/services/normal/filesystem/pfs.c"
" src/fw/system/hexdump.c"
" src/fw/util/crc8.c"
" src/fw/util/legacy_checksum.c"
" tests/fakes/fake_display.c"
" tests/fakes/fake_rtc.c"
" tests/fakes/fake_spi_flash.c"
" tests/fixtures/resources/builtin_resources.auto.c"
" tests/fixtures/resources/pfs_resource_table.c"
)
clar(ctx,
sources_ant_glob=(
common_sources_ant_glob +
" src/fw/applib/graphics/framebuffer.c"
" src/fw/applib/graphics/gbitmap_png.c"
" src/fw/applib/graphics/gbitmap_sequence.c"
" src/fw/applib/template_string.c"
" src/fw/applib/ui/content_indicator.c"
" src/fw/applib/ui/kino/kino_reel.c"
" src/fw/applib/ui/kino/kino_reel_custom.c"
" src/fw/applib/ui/kino/kino_reel_gbitmap.c"
" src/fw/applib/ui/kino/kino_reel_gbitmap_sequence.c"
" src/fw/applib/ui/kino/kino_reel_pdci.c"
" src/fw/applib/ui/kino/kino_reel_pdcs.c"
" src/fw/applib/ui/menu_layer_system_cells.c"
" src/fw/applib/vendor/tinflate/tinflate.c"
" src/fw/applib/vendor/uPNG/upng.c"
" src/fw/apps/system_apps/launcher/default/launcher_app_glance.c"
" src/fw/apps/system_apps/launcher/default/launcher_app_glance_alarms.c"
" src/fw/apps/system_apps/launcher/default/launcher_app_glance_generic.c"
" src/fw/apps/system_apps/launcher/default/launcher_app_glance_music.c"
" src/fw/apps/system_apps/launcher/default/launcher_app_glance_notifications.c"
" src/fw/apps/system_apps/launcher/default/launcher_app_glance_private.c"
" src/fw/apps/system_apps/launcher/default/launcher_app_glance_service.c"
" src/fw/apps/system_apps/launcher/default/launcher_app_glance_settings.c"
" src/fw/apps/system_apps/launcher/default/launcher_app_glance_structured.c"
" src/fw/apps/system_apps/launcher/default/launcher_app_glance_watchfaces.c"
" src/fw/apps/system_apps/launcher/default/launcher_app_glance_weather.c"
" src/fw/apps/system_apps/launcher/default/launcher_app_glance_workout.c"
" src/fw/apps/system_apps/launcher/default/launcher_menu_layer.c"
" src/fw/apps/system_apps/timeline/text_node.c"
" src/fw/board/displays/display_spalding.c"
" src/fw/services/normal/app_glances/app_glance_service.c"
" src/fw/services/normal/blob_db/app_glance_db.c"
" src/fw/services/normal/filesystem/app_file.c"
" src/fw/services/normal/timeline/attribute.c"
" src/fw/util/buffer.c"
" src/fw/util/lru_cache.c"
" tests/fakes/fake_applib_resource.c"
" tests/fakes/fake_fonts.c"
" tests/fakes/fake_settings_file.c"
" tests/fixtures/resources/timeline_resource_table.auto.c"
" tests/stubs/stubs_animation.c"
" tests/stubs/stubs_app_install_manager.c"
" tests/stubs/stubs_app_menu_data_source.c"
),
test_sources_ant_glob="test_launcher_menu_layer.c",
defines=ctx.env.test_image_defines,
runtime_deps=(ctx.env.test_pngs + ctx.env.test_pbis + ctx.env.test_pfos),
override_includes=["dummy_board"],
platforms=["silk", "snowy", "spalding", "robert"])
# vim:filetype=python
@@ -0,0 +1,98 @@
/*
* 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/ui/window_private.h"
#include "util/size.h"
#include "clar.h"
// Fakes
/////////////////////
#include "fake_animation.h"
#include "fake_app_state.h"
#include "fake_content_indicator.h"
#include "fake_evented_timer.h"
#include "fake_graphics_context.h"
#include "fake_rtc.h"
#include "fake_spi_flash.h"
#include "fixtures/load_test_resources.h"
// Stubs
/////////////////////
#include "stubs_action_menu.h"
#include "stubs_activity.h"
#include "stubs_analytics.h"
#include "stubs_ancs.h"
#include "stubs_animation_timing.h"
#include "stubs_animation_timing.h"
#include "stubs_app.h"
#include "stubs_app_cache.h"
#include "stubs_app_install_manager.h"
#include "stubs_app_launch_reason.h"
#include "stubs_app_manager.h"
#include "stubs_app_timer.h"
#include "stubs_app_window_stack.h"
#include "stubs_blob_db.h"
#include "stubs_blob_db_sync.h"
#include "stubs_blob_db_sync_util.h"
#include "stubs_bootbits.h"
#include "stubs_click.h"
#include "stubs_compositor_transitions.h"
#include "stubs_event_loop.h"
#include "stubs_event_service_client.h"
#include "stubs_events.h"
#include "stubs_i18n.h"
#include "stubs_layer.h"
#include "stubs_logging.h"
#include "stubs_memory_layout.h"
#include "stubs_modal_manager.h"
#include "stubs_mutex.h"
#include "stubs_notification_storage.h"
#include "stubs_notifications.h"
#include "stubs_passert.h"
#include "stubs_pbl_malloc.h"
#include "stubs_pebble_process_info.h"
#include "stubs_pebble_tasks.h"
#include "stubs_phone_call_util.h"
#include "stubs_process_manager.h"
#include "stubs_prompt.h"
#include "stubs_property_animation.h"
#include "stubs_regular_timer.h"
#include "stubs_reminder_db.h"
#include "stubs_serial.h"
#include "stubs_session.h"
#include "stubs_shell_prefs.h"
#include "stubs_sleep.h"
#include "stubs_syscalls.h"
#include "stubs_system_theme.h"
#include "stubs_task_watchdog.h"
#include "stubs_timeline.h"
#include "stubs_timeline_actions.h"
#include "stubs_timeline_layout.h"
#include "stubs_timeline_layout_animations.h"
#include "stubs_timeline_peek.h"
#include "stubs_timezone_database.h"
#include "stubs_wakeup.h"
#include "stubs_watchface.h"
#include "stubs_window_manager.h"
#include "stubs_window_stack.h"
// Helper Functions
/////////////////////
#include "fw/graphics/util.h"
@@ -0,0 +1,271 @@
/*
* 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 "apps/system_apps/timeline/timeline.h"
#include "resource/resource.h"
#include "resource/resource_ids.auto.h"
#include "services/normal/timeline/timeline_resources.h"
#include "test_timeline_app_includes.h"
// Setup and Teardown
////////////////////////////////////
typedef struct TimelineTestData {
TimelineModel model;
} TimelineTestData;
TimelineTestData s_data;
void test_timeline_list_view__initialize(void) {
fake_app_state_init();
load_system_resources_fixture();
s_data = (TimelineTestData) {};
rtc_set_time(3 * SECONDS_PER_DAY);
}
void test_timeline_list_view__cleanup(void) {
}
// Helpers
//////////////////////
typedef struct TimelineItemConfig {
time_t relative_timestamp;
uint16_t duration;
const char *title;
const char *subtitle;
TimelineResourceId icon;
} TimelineItemConfig;
typedef struct ListViewConfig {
TimelineItemConfig *pins[2];
bool past;
bool day_separator;
} ListViewConfig;
static void prv_add_timeline_item(const TimelineItemConfig *config, bool past) {
PBL_ASSERTN(config);
TimelineItem *item = NULL;
const time_t now = rtc_get_time();
const time_t timestamp = now + ((past ? -1 : 1) * config->relative_timestamp);
if (config) {
AttributeList list;
attribute_list_init_list(0 /* num_attributes */, &list);
attribute_list_add_cstring(&list, AttributeIdTitle, config->title);
if (config->subtitle) {
attribute_list_add_cstring(&list, AttributeIdSubtitle, config->subtitle);
}
attribute_list_add_uint32(&list, AttributeIdIconPin, config->icon);
item = timeline_item_create_with_attributes(timestamp, config->duration,
TimelineItemTypePin, LayoutIdGeneric,
&list, NULL);
attribute_list_destroy_list(&list);
PBL_ASSERTN(item);
}
if (item) {
pin_db_insert_item(item);
timeline_item_destroy(item);
}
}
static void prv_create_list_view_and_render(ListViewConfig *config) {
pin_db_init();
for (int i = 0; i < (int)ARRAY_LENGTH(config->pins); i++) {
if (config->pins[i]) {
prv_add_timeline_item(config->pins[i], config->past);
}
}
s_data.model = (TimelineModel) {};
s_data.model.direction = config->past ? TimelineIterDirectionPast : TimelineIterDirectionFuture;
timeline_model_init(rtc_get_time(), &s_data.model);
Window window;
window_init(&window, "Timeline");
TimelineLayer timeline_layer = {};
const TimelineScrollDirection scroll_direction =
config->past ? TimelineScrollDirectionUp : TimelineScrollDirectionDown;
timeline_layer_init(&timeline_layer, &window.layer.frame, scroll_direction);
const GColor color = config->past ? TIMELINE_PAST_COLOR : TIMELINE_FUTURE_COLOR;
timeline_layer_set_sidebar_color(&timeline_layer, color);
timeline_layer_set_sidebar_width(&timeline_layer, timeline_layer_get_ideal_sidebar_width());
layer_add_child(&window.layer, &timeline_layer.layer);
timeline_layer_reset(&timeline_layer);
if (config->day_separator) {
// Simulate showing the day separator
int new_idx;
bool has_new;
cl_assert(timeline_model_iter_next(&new_idx, &has_new));
if (has_new) {
timeline_layer_set_next_item(&timeline_layer, new_idx);
}
timeline_layer_move_data(&timeline_layer, 1);
cl_assert(timeline_layer_should_animate_day_separator(&timeline_layer));
fake_animation_complete(timeline_layer_create_day_sep_show(&timeline_layer));
fake_animation_complete(timeline_layer.day_separator.kino_layer.player.animation);
timeline_layer_set_layouts_hidden(&timeline_layer, true);
}
window_set_on_screen(&window, true, true);
window_render(&window, fake_graphics_context_get_context());
timeline_layer_deinit(&timeline_layer);
timeline_model_deinit();
pin_db_flush();
}
// Tests
//////////////////////
static void prv_create_and_render_title_and_subtitle(bool past, uint16_t first_duration_m) {
prv_create_list_view_and_render(&(ListViewConfig) {
.pins = {
&(TimelineItemConfig) {
.relative_timestamp = (11 * SECONDS_PER_HOUR) + (30 * SECONDS_PER_MINUTE),
.duration = first_duration_m,
.title = "Jon Byrd birthday party",
.subtitle = "Kaboom, Redwood City",
.icon = TIMELINE_RESOURCE_TIMELINE_CALENDAR,
}, &(TimelineItemConfig) {
.relative_timestamp = 12 * SECONDS_PER_HOUR,
.duration = MINUTES_PER_HOUR,
.title = "Design Review Meeting",
.subtitle = "Batavia, Palo Alto",
.icon = TIMELINE_RESOURCE_TIMELINE_CALENDAR,
}
},
.past = past,
});
}
void test_timeline_list_view__title_and_subtitle_overlap_future(void) {
#if !PLATFORM_SPALDING
prv_create_and_render_title_and_subtitle(false /* past */, MINUTES_PER_HOUR);
FAKE_GRAPHICS_CONTEXT_CHECK_DEST_BITMAP_FILE();
#endif
}
void test_timeline_list_view__title_and_subtitle_back_to_back_future(void) {
#if !PLATFORM_SPALDING
prv_create_and_render_title_and_subtitle(false /* past */, MINUTES_PER_HOUR / 2);
FAKE_GRAPHICS_CONTEXT_CHECK_DEST_BITMAP_FILE();
#endif
}
void test_timeline_list_view__title_and_subtitle_free_time_future(void) {
prv_create_and_render_title_and_subtitle(false /* past */, 5 /* first_duration_m */);
FAKE_GRAPHICS_CONTEXT_CHECK_DEST_BITMAP_FILE();
}
void test_timeline_list_view__title_and_subtitle_free_time_past(void) {
prv_create_and_render_title_and_subtitle(true /* past */, 5 /* first_duration_m */);
FAKE_GRAPHICS_CONTEXT_CHECK_DEST_BITMAP(TEST_PBI_FILE);
}
void prv_create_and_render_pin_and_dot(bool past) {
prv_create_list_view_and_render(&(ListViewConfig) {
.pins = {
&(TimelineItemConfig) {
.relative_timestamp = (11 * SECONDS_PER_HOUR) + (30 * SECONDS_PER_MINUTE),
.duration = MINUTES_PER_HOUR,
.title = "Jon Byrd birthday party",
.subtitle = "Kaboom, Redwood City",
.icon = TIMELINE_RESOURCE_TIMELINE_CALENDAR,
}, &(TimelineItemConfig) {
.relative_timestamp = SECONDS_PER_DAY + SECONDS_PER_HOUR,
.duration = MINUTES_PER_HOUR,
.title = "Design Review Meeting",
.subtitle = "Batavia, Palo Alto",
.icon = TIMELINE_RESOURCE_TIMELINE_CALENDAR,
}
},
.past = past,
});
}
void test_timeline_list_view__pin_and_dot_future(void) {
prv_create_and_render_pin_and_dot(false /* past */);
FAKE_GRAPHICS_CONTEXT_CHECK_DEST_BITMAP_FILE();
}
void test_timeline_list_view__pin_and_dot_past(void) {
prv_create_and_render_pin_and_dot(true /* past */);
FAKE_GRAPHICS_CONTEXT_CHECK_DEST_BITMAP_FILE();
}
void prv_create_and_render_day_sep_tomorrow(bool past) {
prv_create_list_view_and_render(&(ListViewConfig) {
.pins = {
&(TimelineItemConfig) {
.relative_timestamp = (11 * SECONDS_PER_HOUR) + (30 * SECONDS_PER_MINUTE),
.duration = MINUTES_PER_HOUR,
.title = "Jon Byrd birthday party",
.subtitle = "Kaboom, Redwood City",
.icon = TIMELINE_RESOURCE_TIMELINE_CALENDAR,
}, &(TimelineItemConfig) {
.relative_timestamp = ((11 * SECONDS_PER_HOUR) + (30 * SECONDS_PER_MINUTE) +
SECONDS_PER_DAY),
.duration = MINUTES_PER_HOUR,
.title = "Design Review Meeting",
.subtitle = "Batavia, Palo Alto",
.icon = TIMELINE_RESOURCE_TIMELINE_CALENDAR,
}
},
.past = past,
.day_separator = true,
});
}
void test_timeline_list_view__day_sep_tomorrow_future(void) {
prv_create_and_render_day_sep_tomorrow(false /* past */);
FAKE_GRAPHICS_CONTEXT_CHECK_DEST_BITMAP_FILE();
}
void test_timeline_list_view__day_sep_tomorrow_past(void) {
prv_create_and_render_day_sep_tomorrow(true /* past */);
FAKE_GRAPHICS_CONTEXT_CHECK_DEST_BITMAP_FILE();
}
void prv_create_and_render_pin_and_fin(bool past) {
prv_create_list_view_and_render(&(ListViewConfig) {
.pins = {
&(TimelineItemConfig) {
.relative_timestamp = (11 * SECONDS_PER_HOUR) + (30 * SECONDS_PER_MINUTE),
.title = "Jon Byrd birthday party",
.duration = MINUTES_PER_HOUR,
.subtitle = "Kaboom, Redwood City",
.icon = TIMELINE_RESOURCE_TIMELINE_CALENDAR,
}
},
.past = past,
});
}
void test_timeline_list_view__pin_and_fin_future(void) {
prv_create_and_render_pin_and_fin(false /* past */);
FAKE_GRAPHICS_CONTEXT_CHECK_DEST_BITMAP_FILE();
}
void test_timeline_list_view__pin_and_fin_past(void) {
prv_create_and_render_pin_and_fin(true /* past */);
FAKE_GRAPHICS_CONTEXT_CHECK_DEST_BITMAP_FILE();
}
@@ -0,0 +1,77 @@
/*
* 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 "apps/system_apps/timeline/timeline.h"
#include "resource/resource.h"
#include "resource/resource_ids.auto.h"
#include "services/normal/timeline/timeline_resources.h"
#include "test_timeline_app_includes.h"
// Setup and Teardown
////////////////////////////////////
typedef struct TimelineTestData {
TimelineAppData app;
} TimelineTestData;
TimelineTestData s_data;
void test_timeline_no_events__initialize(void) {
fake_app_state_init();
load_system_resources_fixture();
s_data = (TimelineTestData) {};
}
void test_timeline_no_events__cleanup(void) {
}
// Helpers
//////////////////////
void prv_init_peek_layer(TimelineAppData *data);
void prv_setup_no_events_peek(TimelineAppData *data);
static void prv_create_no_events_and_render(bool past) {
Window *window = &s_data.app.timeline_window;
window_init(window, "Timeline");
const GColor color = past ? TIMELINE_PAST_COLOR : TIMELINE_FUTURE_COLOR;
window_set_background_color(window, color);
prv_init_peek_layer(&s_data.app);
prv_setup_no_events_peek(&s_data.app);
peek_layer_play(&s_data.app.peek_layer);
fake_animation_complete(s_data.app.peek_layer.kino_layer.player.animation);
fake_evented_timer_trigger(s_data.app.peek_layer.hidden_fields_timer);
window_set_on_screen(window, true, true);
window_render(window, fake_graphics_context_get_context());
}
// Tests
//////////////////////
void test_timeline_no_events__future(void) {
prv_create_no_events_and_render(false /* past */);
FAKE_GRAPHICS_CONTEXT_CHECK_DEST_BITMAP_FILE();
}
void test_timeline_no_events__past(void) {
prv_create_no_events_and_render(true /* past */);
FAKE_GRAPHICS_CONTEXT_CHECK_DEST_BITMAP_FILE();
}
+148
View File
@@ -0,0 +1,148 @@
from waftools.pebble_test import clar
def build(ctx):
rendering_sources = (
"src/fw/applib/fonts/codepoint.c "
"src/fw/applib/graphics/${BITDEPTH}_bit/bitblt_private.c "
"src/fw/applib/graphics/${BITDEPTH}_bit/framebuffer.c "
"src/fw/applib/graphics/bitblt.c "
"src/fw/applib/graphics/framebuffer.c "
"src/fw/applib/graphics/gbitmap.c "
"src/fw/applib/graphics/gbitmap_png.c "
"src/fw/applib/graphics/gbitmap_sequence.c "
"src/fw/applib/graphics/gcolor_definitions.c "
"src/fw/applib/graphics/gdraw_command.c "
"src/fw/applib/graphics/gdraw_command_frame.c "
"src/fw/applib/graphics/gdraw_command_image.c "
"src/fw/applib/graphics/gdraw_command_list.c "
"src/fw/applib/graphics/gdraw_command_sequence.c "
"src/fw/applib/graphics/gpath.c "
"src/fw/applib/graphics/graphics.c "
"src/fw/applib/graphics/graphics_bitmap.c "
"src/fw/applib/graphics/graphics_circle.c "
"src/fw/applib/graphics/graphics_line.c "
"src/fw/applib/graphics/graphics_private.c "
"src/fw/applib/graphics/graphics_private_raw.c "
"src/fw/applib/graphics/gtransform.c "
"src/fw/applib/graphics/gtypes.c "
"src/fw/applib/graphics/perimeter.c "
"src/fw/applib/graphics/text_layout.c "
"src/fw/applib/graphics/text_render.c "
"src/fw/applib/graphics/text_resources.c "
"src/fw/applib/graphics/utf8.c "
"src/fw/applib/ui/kino/kino_reel.c "
"src/fw/applib/ui/kino/kino_reel_gbitmap.c "
"src/fw/applib/ui/kino/kino_reel_gbitmap_sequence.c "
"src/fw/applib/ui/kino/kino_reel_pdci.c "
"src/fw/applib/ui/kino/kino_reel_pdcs.c "
"src/fw/applib/ui/layer.c "
"src/fw/applib/ui/menu_layer_system_cells.c "
"src/fw/applib/vendor/tinflate/tinflate.c "
"src/fw/applib/vendor/uPNG/upng.c "
"src/fw/board/displays/display_spalding.c "
"src/fw/drivers/flash/flash_crc.c "
"src/fw/flash_region/filesystem_regions.c "
"src/fw/flash_region/flash_region.c "
"src/fw/resource/resource.c "
"src/fw/resource/resource_storage.c "
"src/fw/resource/resource_storage_builtin.c "
"src/fw/resource/resource_storage_file.c "
"src/fw/resource/resource_storage_flash.c "
"src/fw/services/normal/filesystem/app_file.c "
"src/fw/services/normal/filesystem/flash_translation.c "
"src/fw/services/normal/filesystem/pfs.c "
"src/fw/system/hexdump.c "
"src/fw/util/crc8.c "
"src/fw/util/legacy_checksum.c "
"tests/fakes/fake_animation.c "
"tests/fakes/fake_applib_resource.c "
"tests/fakes/fake_display.c "
"tests/fakes/fake_gbitmap_get_data_row.c "
"tests/fakes/fake_graphics_context.c "
"tests/fakes/fake_property_animation.c "
"tests/fakes/fake_rtc.c "
"tests/fakes/fake_spi_flash.c "
"tests/fixtures/resources/builtin_resources.auto.c "
"tests/fixtures/resources/pfs_resource_table.c "
"tests/stubs/stubs_animation.c "
)
timeline_app_sources = (
"src/fw/applib/graphics/gdraw_command_transforms.c "
"src/fw/applib/graphics/gpath_builder.c "
"src/fw/applib/ui/action_button.c "
"src/fw/applib/ui/animation_interpolate.c "
"src/fw/applib/ui/content_indicator.c "
"src/fw/applib/ui/inverter_layer.c "
"src/fw/applib/ui/kino/kino_layer.c "
"src/fw/applib/ui/kino/kino_player.c "
"src/fw/applib/ui/kino/kino_reel/scale_segmented.c "
"src/fw/applib/ui/kino/kino_reel/transform.c "
"src/fw/applib/ui/kino/kino_reel/unfold.c "
"src/fw/applib/ui/kino/kino_reel_custom.c "
"src/fw/applib/ui/menu_layer.c "
"src/fw/applib/ui/scroll_layer.c "
"src/fw/applib/ui/shadows.c "
"src/fw/applib/ui/status_bar_layer.c "
"src/fw/applib/ui/text_layer.c "
"src/fw/applib/ui/text_layer_flow.c "
"src/fw/applib/ui/window.c "
"src/fw/apps/system_apps/timeline/peek_layer.c "
"src/fw/apps/system_apps/timeline/pin_window.c "
"src/fw/apps/system_apps/timeline/text_node.c "
"src/fw/apps/system_apps/timeline/timeline.c "
"src/fw/apps/system_apps/timeline/timeline_animations.c "
"src/fw/apps/system_apps/timeline/timeline_layer.c "
"src/fw/apps/system_apps/timeline/timeline_model.c "
"src/fw/apps/system_apps/timeline/timeline_relbar.c "
"src/fw/popups/timeline/timeline_item_layer.c "
"src/fw/services/common/clock.c "
"src/fw/services/normal/blob_db/pin_db.c "
"src/fw/services/normal/blob_db/timeline_item_storage.c "
"src/fw/services/normal/timeline/attribute.c "
"src/fw/services/normal/timeline/attribute_group.c "
"src/fw/services/normal/timeline/attributes_actions.c "
"src/fw/services/normal/timeline/generic_layout.c "
"src/fw/services/normal/timeline/item.c "
"src/fw/services/normal/timeline/layout_layer.c "
"src/fw/services/normal/timeline/layout_node.c "
"src/fw/services/normal/timeline/timeline.c "
"src/fw/services/normal/timeline/timeline_layout.c "
"src/fw/services/normal/timeline/timeline_resources.c "
"src/fw/shell/system_theme.c "
"src/fw/util/buffer.c "
"src/fw/util/stats.c "
"src/fw/util/stringlist.c "
"src/fw/util/time/mktime.c "
"src/fw/util/time/time.c "
"tests/fakes/fake_fonts.c "
"tests/fakes/fake_settings_file.c "
"tests/fixtures/resources/timeline_resource_table.auto.c "
"tests/stubs/stubs_evented_timer.c "
)
clar(ctx,
sources_ant_glob=(
rendering_sources +
timeline_app_sources
),
test_sources_ant_glob="test_timeline_list_view.c",
defines=ctx.env.test_image_defines + ["USE_DISPLAY_PERIMETER_ON_FONT_LAYOUT=1"],
runtime_deps=ctx.env.test_pngs + ctx.env.test_pbis + ctx.env.test_pfos,
override_includes=['dummy_board'],
platforms=['snowy', 'spalding', 'silk', 'robert'])
clar(ctx,
sources_ant_glob=(
rendering_sources +
timeline_app_sources
),
test_sources_ant_glob="test_timeline_no_events.c",
defines=ctx.env.test_image_defines + ["USE_DISPLAY_PERIMETER_ON_FONT_LAYOUT=1"],
runtime_deps=ctx.env.test_pngs + ctx.env.test_pbis + ctx.env.test_pfos,
override_includes=['dummy_board'],
# The no events is a builtin on silk and thus cannot be used in a unit test currently
platforms=['snowy', 'spalding', 'robert'])
# vim:filetype=python
@@ -0,0 +1,377 @@
/*
* 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/graphics/framebuffer.h"
#include "applib/graphics/graphics.h"
#include "apps/system_apps/weather/weather_app_layout.h"
#include "applib/ui/app_window_stack.h"
#include "applib/ui/content_indicator.h"
#include "applib/ui/content_indicator_private.h"
#include "applib/ui/dialogs/simple_dialog.h"
#include "applib/ui/text_layer.h"
#include "applib/ui/window_private.h"
#include "resource/resource.h"
#include "resource/resource_ids.auto.h"
#include "services/normal/timeline/timeline_resources.h"
#include "shell/system_theme.h"
#include "util/buffer.h"
#include "util/graphics.h"
#include "util/hash.h"
#include "util/math.h"
#include "util/size.h"
#include "clar.h"
#include <stdio.h>
// Fakes
/////////////////////
#include "fake_content_indicator.h"
#include "fake_spi_flash.h"
#include "fixtures/load_test_resources.h"
// Stubs
/////////////////////
#include "stubs_analytics.h"
#include "stubs_animation_timing.h"
#include "stubs_app_install_manager.h"
#include "stubs_app_state.h"
#include "stubs_app_timer.h"
#include "stubs_bootbits.h"
#include "stubs_click.h"
#include "stubs_i18n.h"
#include "stubs_layer.h"
#include "stubs_logging.h"
#include "stubs_memory_layout.h"
#include "stubs_mutex.h"
#include "stubs_passert.h"
#include "stubs_pbl_malloc.h"
#include "stubs_pebble_process_info.h"
#include "stubs_pebble_tasks.h"
#include "stubs_prompt.h"
#include "stubs_serial.h"
#include "stubs_sleep.h"
#include "stubs_status_bar_layer.h"
#include "stubs_syscalls.h"
#include "stubs_system_theme.h"
#include "stubs_task_watchdog.h"
#include "stubs_window_manager.h"
#include "stubs_window_stack.h"
// Helper Functions
/////////////////////
#include "fw/graphics/test_graphics.h"
#include "fw/graphics/util.h"
// Setup and Teardown
////////////////////////////////////
static GContext s_ctx;
static FrameBuffer *fb = NULL;
KinoReel *kino_reel_morph_square_create(KinoReel *from_reel, bool take_ownership) {
return from_reel;
}
GContext *graphics_context_get_current_context(void) {
return &s_ctx;
}
void test_weather_app_layout__initialize(void) {
fb = malloc(sizeof(FrameBuffer));
framebuffer_init(fb, &(GSize) {DISP_COLS, DISP_ROWS});
const GContextInitializationMode context_init_mode = GContextInitializationMode_System;
graphics_context_init(&s_ctx, fb, context_init_mode);
framebuffer_clear(fb);
// Setup resources
fake_spi_flash_init(0, 0x1000000);
pfs_init(false);
pfs_format(true /* write erase headers */);
load_resource_fixture_in_flash(RESOURCES_FIXTURE_PATH, SYSTEM_RESOURCES_FIXTURE_NAME, false /* is_next */);
resource_init();
ContentIndicatorsBuffer *buffer = content_indicator_get_current_buffer();
content_indicator_init_buffer(buffer);
}
void test_weather_app_layout__cleanup(void) {
free(fb);
}
// Helpers
//////////////////////
static void prv_create_layout_for_forecast(const WeatherLocationForecast *forecast,
WeatherAppLayout *layout, Window *window) {
window_init(window, WINDOW_NAME("Weather"));
weather_app_layout_init(layout, &s_ctx.dest_bitmap.bounds);
weather_app_layout_set_data(layout, forecast);
window_set_user_data(window, layout);
Layer *window_root_layer = window_get_root_layer(window);
layer_add_child(window_root_layer, &layout->root_layer);
window_set_on_screen(window, true, true);
}
static void prv_create_layout_for_forecast_and_render(const WeatherLocationForecast *forecast) {
Window window;
WeatherAppLayout layout = {};
prv_create_layout_for_forecast(forecast, &layout, &window);
window_render(&window, &s_ctx);
}
static void prv_create_layout_for_forecast_and_render_with_down_arrow_indicator(
const WeatherLocationForecast *forecast) {
Window window;
WeatherAppLayout layout = {};
prv_create_layout_for_forecast(forecast, &layout, &window);
weather_app_layout_set_down_arrow_visible(&layout, true);
window_render(&window, &s_ctx);
}
// Tests
//////////////////////
void test_weather_app_layout__render_palo_alto(void) {
const WeatherLocationForecast forecast = {
.location_name = "PALO ALTO",
.current_temp = 68,
.today_high = 68,
.today_low = 58,
.current_weather_type = WeatherType_Sun,
.current_weather_phrase = "Sunny",
.tomorrow_high = 62,
.tomorrow_low = 52,
.tomorrow_weather_type = WeatherType_PartlyCloudy,
};
prv_create_layout_for_forecast_and_render(&forecast);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
static void prv_render_long_strings_test(bool is_current_location) {
const WeatherLocationForecast forecast = {
.location_name = "QWERTYUIO ASEDDFFGHHJ",
.is_current_location = is_current_location,
.current_temp = 68,
.today_high = 68,
.today_low = 58,
.current_weather_type = WeatherType_PartlyCloudy,
.current_weather_phrase = "Cloudy with 90% chance of meatballs",
.tomorrow_high = 62,
.tomorrow_low = 52,
.tomorrow_weather_type = WeatherType_Sun,
};
prv_create_layout_for_forecast_and_render(&forecast);
}
void test_weather_app_layout__render_longer_strings(void) {
const bool is_current_location = false;
prv_render_long_strings_test(is_current_location);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_weather_app_layout__render_longer_strings_for_current_location(void) {
const bool is_current_location = true;
prv_render_long_strings_test(is_current_location);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_weather_app_layout__render_large_numbers(void) {
const WeatherLocationForecast forecast = {
.location_name = "PALO ALTO",
.current_temp = -88,
.today_high = -88,
.today_low = -88,
.current_weather_type = WeatherType_Sun,
.current_weather_phrase = "Sunny",
.tomorrow_high = -99,
.tomorrow_low = -99,
.tomorrow_weather_type = WeatherType_PartlyCloudy,
};
prv_create_layout_for_forecast_and_render(&forecast);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_weather_app_layout__render_cloudy_light_snow(void) {
const WeatherLocationForecast forecast = {
.location_name = "PALO ALTO",
.current_temp = -88,
.today_high = -88,
.today_low = -88,
.current_weather_type = WeatherType_CloudyDay,
.current_weather_phrase = "Cloudy",
.tomorrow_high = -99,
.tomorrow_low = -99,
.tomorrow_weather_type = WeatherType_LightSnow,
};
prv_create_layout_for_forecast_and_render(&forecast);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_weather_app_layout__render_light_rain_heavy_rain(void) {
const WeatherLocationForecast forecast = {
.location_name = "PALO ALTO",
.current_temp = -88,
.today_high = -88,
.today_low = -88,
.current_weather_type = WeatherType_LightRain,
.current_weather_phrase = "Light Rain",
.tomorrow_high = -99,
.tomorrow_low = -99,
.tomorrow_weather_type = WeatherType_HeavyRain,
};
prv_create_layout_for_forecast_and_render(&forecast);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_weather_app_layout__render_generic_generic(void) {
const WeatherLocationForecast forecast = {
.location_name = "HOUSTON",
.current_temp = 110,
.today_high = 120,
.today_low = 85,
.current_weather_type = WeatherType_Generic,
.current_weather_phrase = "Humid AF",
.tomorrow_high = 500,
.tomorrow_low = 100,
.tomorrow_weather_type = WeatherType_Generic,
};
prv_create_layout_for_forecast_and_render(&forecast);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_weather_app_layout__render_heavy_snow_rain_snow(void) {
const WeatherLocationForecast forecast = {
.location_name = "PALO ALTO",
.current_temp = -88,
.today_high = -88,
.today_low = -88,
.current_weather_type = WeatherType_HeavySnow,
.current_weather_phrase = "Heavy Snow",
.tomorrow_high = -99,
.tomorrow_low = -99,
.tomorrow_weather_type = WeatherType_RainAndSnow,
};
prv_create_layout_for_forecast_and_render(&forecast);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_weather_app_layout__render_down_arrow(void) {
const WeatherLocationForecast forecast = {
.location_name = "PALO ALTO",
.current_temp = -88,
.today_high = -88,
.today_low = -88,
.current_weather_type = WeatherType_HeavySnow,
.current_weather_phrase = "Heavy Snow",
.tomorrow_high = -99,
.tomorrow_low = -99,
.tomorrow_weather_type = WeatherType_RainAndSnow,
};
prv_create_layout_for_forecast_and_render_with_down_arrow_indicator(&forecast);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_weather_app_layout__render_all_unknown_values(void) {
const WeatherLocationForecast forecast = {
.location_name = "PALO ALTO",
.current_temp = WEATHER_SERVICE_LOCATION_FORECAST_UNKNOWN_TEMP,
.today_high = WEATHER_SERVICE_LOCATION_FORECAST_UNKNOWN_TEMP,
.today_low = WEATHER_SERVICE_LOCATION_FORECAST_UNKNOWN_TEMP,
.current_weather_type = WeatherType_Unknown,
.current_weather_phrase = "",
.tomorrow_high = WEATHER_SERVICE_LOCATION_FORECAST_UNKNOWN_TEMP,
.tomorrow_low = WEATHER_SERVICE_LOCATION_FORECAST_UNKNOWN_TEMP,
.tomorrow_weather_type = WeatherType_Unknown,
};
prv_create_layout_for_forecast_and_render(&forecast);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_weather_app_layout__render_some_unknown_values(void) {
const WeatherLocationForecast forecast = {
.location_name = "PALO ALTO",
.current_temp = WEATHER_SERVICE_LOCATION_FORECAST_UNKNOWN_TEMP,
.today_high = 99,
.today_low = WEATHER_SERVICE_LOCATION_FORECAST_UNKNOWN_TEMP,
.current_weather_type = WeatherType_Sun,
.current_weather_phrase = "",
.tomorrow_high = WEATHER_SERVICE_LOCATION_FORECAST_UNKNOWN_TEMP,
.tomorrow_low = -99,
.tomorrow_weather_type = WeatherType_Unknown,
};
prv_create_layout_for_forecast_and_render(&forecast);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_weather_app_layout__render_current_location(void) {
const WeatherLocationForecast forecast = {
.location_name = "PHILADELPHIA",
.is_current_location = true,
.current_temp = 13,
.today_high = 15,
.today_low = -2,
.current_weather_type = WeatherType_HeavySnow,
.current_weather_phrase = "Heavy Snow",
.tomorrow_high = 26,
.tomorrow_low = 3,
.tomorrow_weather_type = WeatherType_RainAndSnow,
};
prv_create_layout_for_forecast_and_render_with_down_arrow_indicator(&forecast);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_weather_app_layout__render_long_current_location_name_pbl_38049(void) {
const WeatherLocationForecast forecast = {
.location_name = "DA'AN DISTRICT",
.is_current_location = true,
.current_temp = 30,
.today_high = 33,
.today_low = 26,
.current_weather_type = WeatherType_CloudyDay,
.current_weather_phrase = "M Cloudy",
.tomorrow_high = 34,
.tomorrow_low = 26,
.tomorrow_weather_type = WeatherType_HeavyRain,
};
prv_create_layout_for_forecast_and_render_with_down_arrow_indicator(&forecast);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
// renders a blank image
void test_weather_app_layout__render_empty_view(void) {
prv_create_layout_for_forecast_and_render(NULL);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
+92
View File
@@ -0,0 +1,92 @@
from waftools.pebble_test import clar
def build(ctx):
rendering_sources = \
" src/fw/applib/fonts/codepoint.c" \
" src/fw/applib/graphics/${BITDEPTH}_bit/framebuffer.c" \
" src/fw/applib/graphics/8_bit/bitblt_private.c" \
" src/fw/applib/graphics/bitblt.c" \
" src/fw/applib/graphics/framebuffer.c" \
" src/fw/applib/graphics/gbitmap.c" \
" src/fw/applib/graphics/gbitmap_png.c" \
" src/fw/applib/graphics/gbitmap_sequence.c" \
" src/fw/applib/graphics/gcolor_definitions.c" \
" src/fw/applib/graphics/gdraw_command.c" \
" src/fw/applib/graphics/gdraw_command_frame.c" \
" src/fw/applib/graphics/gdraw_command_image.c" \
" src/fw/applib/graphics/gdraw_command_list.c" \
" src/fw/applib/graphics/gdraw_command_sequence.c" \
" src/fw/applib/graphics/gpath.c" \
" src/fw/applib/graphics/graphics.c" \
" src/fw/applib/graphics/graphics_bitmap.c" \
" src/fw/applib/graphics/graphics_circle.c" \
" src/fw/applib/graphics/graphics_line.c" \
" src/fw/applib/graphics/graphics_private.c" \
" src/fw/applib/graphics/graphics_private_raw.c" \
" src/fw/applib/graphics/gtransform.c" \
" src/fw/applib/graphics/gtypes.c" \
" src/fw/applib/graphics/perimeter.c" \
" src/fw/applib/graphics/text_layout.c" \
" src/fw/applib/graphics/text_render.c" \
" src/fw/applib/graphics/text_resources.c" \
" src/fw/applib/graphics/utf8.c" \
" src/fw/applib/ui/kino/kino_reel.c" \
" src/fw/applib/ui/kino/kino_reel_gbitmap.c" \
" src/fw/applib/ui/kino/kino_reel_gbitmap_sequence.c" \
" src/fw/applib/ui/kino/kino_reel_pdci.c" \
" src/fw/applib/ui/kino/kino_reel_pdcs.c" \
" src/fw/applib/ui/layer.c" \
" src/fw/apps/system_apps/timeline/text_node.c" \
" src/fw/board/displays/display_spalding.c" \
" src/fw/drivers/flash/flash_crc.c" \
" src/fw/flash_region/filesystem_regions.c" \
" src/fw/flash_region/flash_region.c" \
" src/fw/resource/resource.c" \
" src/fw/resource/resource_storage.c" \
" src/fw/resource/resource_storage_builtin.c" \
" src/fw/resource/resource_storage_file.c" \
" src/fw/resource/resource_storage_flash.c" \
" src/fw/services/normal/filesystem/app_file.c" \
" src/fw/services/normal/filesystem/flash_translation.c" \
" src/fw/services/normal/filesystem/pfs.c" \
" src/fw/system/hexdump.c" \
" src/fw/util/crc8.c" \
" src/fw/util/legacy_checksum.c" \
" src/fw/applib/vendor/tinflate/tinflate.c" \
" src/fw/applib/vendor/uPNG/upng.c" \
" tests/fakes/fake_applib_resource.c" \
" tests/fakes/fake_display.c" \
" tests/fakes/fake_gbitmap_get_data_row.c" \
" tests/fakes/fake_rtc.c" \
" tests/fakes/fake_spi_flash.c" \
" tests/fixtures/resources/builtin_resources.auto.c" \
" tests/fixtures/resources/pfs_resource_table.c" \
" tests/stubs/stubs_animation.c"
clar(ctx,
sources_ant_glob = rendering_sources + \
" src/fw/applib/ui/animation_interpolate.c" \
" src/fw/applib/ui/kino/kino_layer.c" \
" src/fw/applib/ui/kino/kino_player.c" \
" src/fw/applib/ui/kino/kino_reel_custom.c" \
" src/fw/applib/ui/kino/kino_reel/transform.c" \
" src/fw/applib/ui/text_layer.c" \
" src/fw/applib/ui/text_layer_flow.c" \
" src/fw/applib/ui/window.c" \
" src/fw/applib/ui/scroll_layer.c" \
" src/fw/applib/ui/inverter_layer.c" \
" src/fw/applib/ui/content_indicator.c" \
" tests/fakes/fake_fonts.c" \
" src/fw/applib/ui/shadows.c" \
" src/fw/apps/system_apps/weather/weather_app_layout.c" \
" src/fw/services/normal/timeline/timeline_resources.c" \
" src/fw/services/normal/weather/weather_types.c" \
" src/fw/util/buffer.c" \
" tests/fixtures/resources/timeline_resource_table.auto.c",
test_sources_ant_glob = "test_weather_app_layout.c",
defines=ctx.env.test_image_defines + ["USE_DISPLAY_PERIMETER_ON_FONT_LAYOUT=1"],
runtime_deps=ctx.env.test_pngs + ctx.env.test_pbis + ctx.env.test_pfos,
override_includes=['dummy_board'],
platforms=['snowy', 'spalding', 'silk'])
# vim:filetype=python
@@ -0,0 +1,595 @@
/*
* 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 "apps/system_apps/workout/workout.h"
#include "apps/system_apps/workout/workout_active.h"
#include "apps/system_apps/workout/workout_data.h"
#include "apps/system_apps/workout/workout_dialog.h"
#include "services/normal/activity/health_util.h"
#include "test_workout_app_includes.h"
#include "stubs_window_manager.h"
bool s_hrm_is_present;
// Fakes
/////////////////////
extern void prv_cycle_scrollable_metrics(WorkoutActiveWindow *active_window);
bool activity_is_hrm_present(void) {
return s_hrm_is_present;
}
uint16_t time_ms(time_t *tloc, uint16_t *out_ms) {
return 0;
}
void workout_push_summary_window(void) {
return;
}
static WorkoutData s_workout_data;
static WorkoutController s_workout_controller = {
.is_paused = workout_service_is_paused,
.pause = workout_service_pause_workout,
.stop = workout_service_stop_workout,
.update_data = workout_data_update,
.metric_to_string = workout_data_fill_metric_value,
.get_metric_value = workout_data_get_metric_value,
.get_distance_string = health_util_get_distance_string,
};
typedef struct SportsData {
int32_t current_bpm;
char *duration_string;
char *distance_string;
char *pace_string;
char *custom_label_string;
char *custom_value_string;
} SportsData;
static SportsData s_sports_data;
static bool prv_is_sports_paused(void) { return false; }
static bool prv_sports_pause(bool should_be_paused) { return false; }
static void prv_metric_to_string(WorkoutMetricType type, char *buffer, size_t buffer_size,
void *i18n_owner, void *sports_data) {
SportsData *data = sports_data;
switch (type) {
case WorkoutMetricType_Hr:
{
snprintf(buffer, buffer_size, "%d", data->current_bpm);
break;
}
case WorkoutMetricType_Speed:
case WorkoutMetricType_Pace:
{
strncpy(buffer, data->pace_string, buffer_size);
break;
}
case WorkoutMetricType_Distance:
{
strncpy(buffer, data->distance_string, buffer_size);
break;
}
case WorkoutMetricType_Duration:
{
strncpy(buffer, data->duration_string, buffer_size);
break;
}
case WorkoutMetricType_Custom:
{
strncpy(buffer, data->custom_value_string, buffer_size);
break;
}
// Not supported by the sports API
case WorkoutMetricType_Steps:
case WorkoutMetricType_AvgPace:
case WorkoutMetricType_None:
case WorkoutMetricTypeCount:
break;
}
}
static int32_t prv_sports_get_value(WorkoutMetricType type, void *sports_data) {
SportsData *data = sports_data;
switch (type) {
case WorkoutMetricType_Hr:
return data->current_bpm;
default:
return 0;
}
}
static char *prv_get_custom_metric_label_string(void) {
return s_sports_data.custom_label_string;
}
static WorkoutController s_sports_controller = {
.is_paused = prv_is_sports_paused,
.pause = prv_sports_pause,
.stop = NULL,
.update_data = NULL,
.metric_to_string = prv_metric_to_string,
.get_metric_value = prv_sports_get_value,
.get_distance_string = health_util_get_distance_string,
.get_custom_metric_label_string = prv_get_custom_metric_label_string,
};
// Setup and Teardown
////////////////////////////////////
static GContext s_ctx;
static FrameBuffer s_fb;
GContext *graphics_context_get_current_context(void) {
return &s_ctx;
}
void test_workout_active__initialize(void) {
s_hrm_is_present = true;
s_workout_data = (WorkoutData) {};
s_sports_data = (SportsData) {};
// Setup graphics context
framebuffer_init(&s_fb, &(GSize) {DISP_COLS, DISP_ROWS});
framebuffer_clear(&s_fb);
graphics_context_init(&s_ctx, &s_fb, GContextInitializationMode_App);
s_app_state_get_graphics_context = &s_ctx;
// Setup resources
fake_spi_flash_init(0 /* offset */, 0x1000000 /* length */);
pfs_init(false /* run filesystem check */);
pfs_format(true /* write erase headers */);
load_resource_fixture_in_flash(RESOURCES_FIXTURE_PATH, SYSTEM_RESOURCES_FIXTURE_NAME,
false /* is_next */);
resource_init();
// Setup content indicator
ContentIndicatorsBuffer *buffer = content_indicator_get_current_buffer();
content_indicator_init_buffer(buffer);
}
void test_workout_active__cleanup(void) {
}
// Helpers
//////////////////////
static void prv_create_window_and_render(WorkoutActiveWindow *active_window,
int seconday_metric_idx) {
for (int i = 0; i < seconday_metric_idx; i++) {
prv_cycle_scrollable_metrics(active_window);
}
Window *window = (Window *)active_window;
window_set_on_screen(window, true, true);
window_render(window, &s_ctx);
}
// Workout Tests
//////////////////////
void test_workout_active__workout_render_no_data(void) {
s_workout_data = (WorkoutData) {};
WorkoutActiveWindow *active_window = workout_active_create_for_activity_type(
ActivitySessionType_Run, &s_workout_data, &s_workout_controller);
prv_create_window_and_render(active_window, 0);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_workout_active__workout_render_walk(void) {
s_workout_data = (WorkoutData) {
.steps = 567,
.duration_s = 84,
.distance_m = 1234,
.avg_pace = health_util_get_pace(84, 1234),
.bpm = 71,
.hr_zone = 0,
};
WorkoutActiveWindow *active_window = workout_active_create_for_activity_type(
ActivitySessionType_Walk, &s_workout_data, &s_workout_controller);
prv_create_window_and_render(active_window, 0);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_workout_active__workout_render_walk_no_hrm(void) {
s_hrm_is_present = false;
s_workout_data = (WorkoutData) {
.steps = 567,
.duration_s = 84,
.distance_m = 1234,
.avg_pace = health_util_get_pace(84, 1234),
.bpm = 71,
.hr_zone = 0,
};
WorkoutActiveWindow *active_window = workout_active_create_for_activity_type(
ActivitySessionType_Walk, &s_workout_data, &s_workout_controller);
prv_create_window_and_render(active_window, 0);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_workout_active__workout_render_run(void) {
s_workout_data = (WorkoutData) {
.steps = 567,
.duration_s = 84,
.distance_m = 1234,
.avg_pace = health_util_get_pace(84, 1234),
.bpm = 71,
.hr_zone = 0,
};
WorkoutActiveWindow *active_window = workout_active_create_for_activity_type(
ActivitySessionType_Run, &s_workout_data, &s_workout_controller);
prv_create_window_and_render(active_window, 0);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_workout_active__workout_render_run_no_hrm(void) {
s_hrm_is_present = false;
s_workout_data = (WorkoutData) {
.steps = 567,
.duration_s = 84,
.distance_m = 1234,
.avg_pace = health_util_get_pace(84, 1234),
.bpm = 71,
.hr_zone = 0,
};
WorkoutActiveWindow *active_window = workout_active_create_for_activity_type(
ActivitySessionType_Run, &s_workout_data, &s_workout_controller);
prv_create_window_and_render(active_window, 0);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_workout_active__workout_render_open_workout(void) {
s_workout_data = (WorkoutData) {
.steps = 0,
.duration_s = 84,
.distance_m = 0,
.avg_pace = health_util_get_pace(84, 0),
.bpm = 92,
.hr_zone = 0,
};
WorkoutActiveWindow *active_window = workout_active_create_for_activity_type(
ActivitySessionType_Open, &s_workout_data, &s_workout_controller);
prv_create_window_and_render(active_window, 0);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_workout_active__workout_render_open_workout_no_hrm(void) {
s_hrm_is_present = false;
s_workout_data = (WorkoutData) {
.steps = 0,
.duration_s = 84,
.distance_m = 0,
.avg_pace = health_util_get_pace(84, 0),
.bpm = 92,
.hr_zone = 0,
};
WorkoutActiveWindow *active_window = workout_active_create_for_activity_type(
ActivitySessionType_Open, &s_workout_data, &s_workout_controller);
prv_create_window_and_render(active_window, 0);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_workout_active__workout_render_hr_zone_1(void) {
s_workout_data = (WorkoutData) {
.steps = 567,
.duration_s = 789,
.distance_m = 234,
.avg_pace = health_util_get_pace(789, 234),
.bpm = 148,
.hr_zone = 1,
};
WorkoutActiveWindow *active_window = workout_active_create_for_activity_type(
ActivitySessionType_Run, &s_workout_data, &s_workout_controller);
prv_create_window_and_render(active_window, 0);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_workout_active__workout_render_hr_zone_2(void) {
s_workout_data = (WorkoutData) {
.steps = 567,
.duration_s = 789,
.distance_m = 234,
.avg_pace = health_util_get_pace(789, 234),
.bpm = 167,
.hr_zone = 2,
};
WorkoutActiveWindow *active_window = workout_active_create_for_activity_type(
ActivitySessionType_Run, &s_workout_data, &s_workout_controller);
prv_create_window_and_render(active_window, 0);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_workout_active__workout_render_hr_zone_3(void) {
s_workout_data = (WorkoutData) {
.steps = 567,
.duration_s = 789,
.distance_m = 234,
.avg_pace = health_util_get_pace(789, 234),
.bpm = 197,
.hr_zone = 3,
};
WorkoutActiveWindow *active_window = workout_active_create_for_activity_type(
ActivitySessionType_Run, &s_workout_data, &s_workout_controller);
prv_create_window_and_render(active_window, 0);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_workout_active__workout_render_very_slow_pace(void) {
s_workout_data = (WorkoutData) {
.steps = 0,
.duration_s = SECONDS_PER_HOUR,
.distance_m = 1609,
.avg_pace = health_util_get_pace(SECONDS_PER_HOUR, 1609),
.bpm = 0,
.hr_zone = 0,
};
WorkoutActiveWindow *active_window = workout_active_create_for_activity_type(
ActivitySessionType_Walk, &s_workout_data, &s_workout_controller);
prv_create_window_and_render(active_window, 2);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
// Workout Tests
//////////////////////
void test_workout_active__sports_pace(void) {
s_sports_data = (SportsData) {
.current_bpm = 71,
.duration_string = "30:00",
.distance_string = "5.0",
.pace_string = "6:00",
};
WorkoutMetricType top_metric = WorkoutMetricType_Duration;
WorkoutMetricType middle_metric = WorkoutMetricType_Distance;
WorkoutMetricType scrollable_metrics[] = {WorkoutMetricType_Pace,
WorkoutMetricType_Hr};
WorkoutActiveWindow *active_window = workout_active_create_tripple_layout(
top_metric, middle_metric, ARRAY_LENGTH(scrollable_metrics), scrollable_metrics,
&s_sports_data, &s_sports_controller);
prv_create_window_and_render(active_window, 0);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_workout_active__sports_pace_long_values(void) {
s_sports_data = (SportsData) {
.current_bpm = 71,
.duration_string = "04:20:39",
.distance_string = "115.12",
.pace_string = "12:34",
};
WorkoutMetricType top_metric = WorkoutMetricType_Duration;
WorkoutMetricType middle_metric = WorkoutMetricType_Distance;
WorkoutMetricType scrollable_metrics[] = {WorkoutMetricType_Pace,
WorkoutMetricType_Hr};
WorkoutActiveWindow *active_window = workout_active_create_tripple_layout(
top_metric, middle_metric, ARRAY_LENGTH(scrollable_metrics), scrollable_metrics,
&s_sports_data, &s_sports_controller);
prv_create_window_and_render(active_window, 0);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_workout_active__sports_speed(void) {
s_sports_data = (SportsData) {
.current_bpm = 71,
.duration_string = "20:00",
.distance_string = "18.9",
.pace_string = "35.3",
};
WorkoutMetricType top_metric = WorkoutMetricType_Duration;
WorkoutMetricType middle_metric = WorkoutMetricType_Distance;
WorkoutMetricType scrollable_metrics[] = {WorkoutMetricType_Speed,
WorkoutMetricType_Hr};
WorkoutActiveWindow *active_window = workout_active_create_tripple_layout(
top_metric, middle_metric, ARRAY_LENGTH(scrollable_metrics), scrollable_metrics,
&s_sports_data, &s_sports_controller);
prv_create_window_and_render(active_window, 0);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_workout_active__sports_no_hrm(void) {
s_hrm_is_present = false;
s_sports_data = (SportsData) {
.current_bpm = 71,
.duration_string = "30:00",
.distance_string = "5.0",
.pace_string = "6:00",
};
WorkoutMetricType top_metric = WorkoutMetricType_Duration;
WorkoutMetricType middle_metric = WorkoutMetricType_Distance;
WorkoutMetricType scrollable_metrics[] = {WorkoutMetricType_Pace};
WorkoutActiveWindow *active_window = workout_active_create_tripple_layout(
top_metric, middle_metric, ARRAY_LENGTH(scrollable_metrics), scrollable_metrics,
&s_sports_data, &s_sports_controller);
prv_create_window_and_render(active_window, 0);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_workout_active__sports_hr_z0(void) {
s_sports_data = (SportsData) {
.current_bpm = 71,
.duration_string = "30:00",
.distance_string = "5.0",
.pace_string = "6:00",
};
WorkoutMetricType top_metric = WorkoutMetricType_Duration;
WorkoutMetricType middle_metric = WorkoutMetricType_Distance;
WorkoutMetricType scrollable_metrics[] = {WorkoutMetricType_Pace,
WorkoutMetricType_Hr};
WorkoutActiveWindow *active_window = workout_active_create_tripple_layout(
top_metric, middle_metric, ARRAY_LENGTH(scrollable_metrics), scrollable_metrics,
&s_sports_data, &s_sports_controller);
prv_create_window_and_render(active_window, 1);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_workout_active__sports_hr_z1(void) {
s_sports_data = (SportsData) {
.current_bpm = 135,
.duration_string = "30:00",
.distance_string = "5.0",
.pace_string = "6:00",
};
WorkoutMetricType top_metric = WorkoutMetricType_Duration;
WorkoutMetricType middle_metric = WorkoutMetricType_Distance;
WorkoutMetricType scrollable_metrics[] = {WorkoutMetricType_Pace,
WorkoutMetricType_Hr};
WorkoutActiveWindow *active_window = workout_active_create_tripple_layout(
top_metric, middle_metric, ARRAY_LENGTH(scrollable_metrics), scrollable_metrics,
&s_sports_data, &s_sports_controller);
prv_create_window_and_render(active_window, 1);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_workout_active__sports_hr_z2(void) {
s_sports_data = (SportsData) {
.current_bpm = 165,
.duration_string = "30:00",
.distance_string = "5.0",
.pace_string = "6:00",
};
WorkoutMetricType top_metric = WorkoutMetricType_Duration;
WorkoutMetricType middle_metric = WorkoutMetricType_Distance;
WorkoutMetricType scrollable_metrics[] = {WorkoutMetricType_Pace,
WorkoutMetricType_Hr};
WorkoutActiveWindow *active_window = workout_active_create_tripple_layout(
top_metric, middle_metric, ARRAY_LENGTH(scrollable_metrics), scrollable_metrics,
&s_sports_data, &s_sports_controller);
prv_create_window_and_render(active_window, 1);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_workout_active__sports_hr_z3(void) {
s_sports_data = (SportsData) {
.current_bpm = 180,
.duration_string = "30:00",
.distance_string = "5.0",
.pace_string = "6:00",
};
WorkoutMetricType top_metric = WorkoutMetricType_Duration;
WorkoutMetricType middle_metric = WorkoutMetricType_Distance;
WorkoutMetricType scrollable_metrics[] = {WorkoutMetricType_Pace,
WorkoutMetricType_Hr};
WorkoutActiveWindow *active_window = workout_active_create_tripple_layout(
top_metric, middle_metric, ARRAY_LENGTH(scrollable_metrics), scrollable_metrics,
&s_sports_data, &s_sports_controller);
prv_create_window_and_render(active_window, 1);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_workout_active__sports_custom_field(void) {
s_sports_data = (SportsData) {
.current_bpm = 71,
.duration_string = "30:00",
.distance_string = "5.0",
.pace_string = "6:00",
.custom_label_string = "CUSTOM",
.custom_value_string = "000000",
};
WorkoutMetricType top_metric = WorkoutMetricType_Duration;
WorkoutMetricType middle_metric = WorkoutMetricType_Distance;
WorkoutMetricType scrollable_metrics[] = {WorkoutMetricType_Pace,
WorkoutMetricType_Custom};
WorkoutActiveWindow *active_window = workout_active_create_tripple_layout(
top_metric, middle_metric, ARRAY_LENGTH(scrollable_metrics), scrollable_metrics,
&s_sports_data, &s_sports_controller);
prv_create_window_and_render(active_window, 1);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_workout_active__sports_custom_long_values(void) {
s_sports_data = (SportsData) {
.current_bpm = 71,
.duration_string = "30:00",
.distance_string = "5.0",
.pace_string = "6:00",
.custom_label_string = "CUSTOM FIELD LABEL",
.custom_value_string = "0000000000000000000",
};
WorkoutMetricType top_metric = WorkoutMetricType_Duration;
WorkoutMetricType middle_metric = WorkoutMetricType_Distance;
WorkoutMetricType scrollable_metrics[] = {WorkoutMetricType_Pace,
WorkoutMetricType_Custom};
WorkoutActiveWindow *active_window = workout_active_create_tripple_layout(
top_metric, middle_metric, ARRAY_LENGTH(scrollable_metrics), scrollable_metrics,
&s_sports_data, &s_sports_controller);
prv_create_window_and_render(active_window, 1);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_workout_active__sports_custom_hanging_label(void) {
s_sports_data = (SportsData) {
.current_bpm = 71,
.duration_string = "30:00",
.distance_string = "5.0",
.pace_string = "6:00",
.custom_label_string = "Hanging Field",
.custom_value_string = "000000",
};
WorkoutMetricType top_metric = WorkoutMetricType_Duration;
WorkoutMetricType middle_metric = WorkoutMetricType_Distance;
WorkoutMetricType scrollable_metrics[] = {WorkoutMetricType_Pace,
WorkoutMetricType_Custom};
WorkoutActiveWindow *active_window = workout_active_create_tripple_layout(
top_metric, middle_metric, ARRAY_LENGTH(scrollable_metrics), scrollable_metrics,
&s_sports_data, &s_sports_controller);
prv_create_window_and_render(active_window, 1);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
@@ -0,0 +1,86 @@
/*
* 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/ui/window_private.h"
#include "util/size.h"
#include "clar.h"
// Fakes
/////////////////////
#include "fake_content_indicator.h"
#include "fake_rtc.h"
#include "fake_pbl_std.h"
#include "fake_regular_timer.h"
#include "fake_spi_flash.h"
#include "fake_workout_service.h"
#include "fixtures/load_test_resources.h"
// Stubs
/////////////////////
#include "stubs_activity.h"
#include "stubs_action_menu.h"
#include "stubs_analytics.h"
#include "stubs_animation_timing.h"
#include "stubs_app_install_manager.h"
#include "stubs_app_timer.h"
#include "stubs_app_window_stack.h"
#include "stubs_attribute.h"
#include "stubs_bootbits.h"
#include "stubs_click.h"
#include "stubs_event_service_client.h"
#include "stubs_health_service.h"
#include "stubs_i18n.h"
#include "stubs_layer.h"
#include "stubs_logging.h"
#include "stubs_memory_layout.h"
#include "stubs_mutex.h"
#include "stubs_notifications.h"
#include "stubs_passert.h"
#include "stubs_pbl_malloc.h"
#include "stubs_pebble_process_info.h"
#include "stubs_pebble_tasks.h"
#include "stubs_process_manager.h"
#include "stubs_prompt.h"
#include "stubs_serial.h"
#include "stubs_shell_prefs.h"
#include "stubs_sleep.h"
#include "stubs_syscalls.h"
#include "stubs_task_watchdog.h"
#include "stubs_timeline_item.h"
#include "stubs_vibes.h"
#include "stubs_window_manager.h"
#include "stubs_window_stack.h"
typedef struct KinoReel KinoReel;
KinoReel *kino_reel_scale_segmented_create(KinoReel *from_reel, bool take_ownership,
GRect screen_frame) {
return NULL;
}
void kino_reel_scale_segmented_set_deflate_effect(KinoReel *reel, int16_t expand) {}
bool kino_reel_scale_segmented_set_delay_by_distance(KinoReel *reel, GPoint target) {
return false;
}
// Helper Functions
/////////////////////
#include "fw/graphics/util.h"
@@ -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 "apps/system_apps/workout/workout_dialog.h"
#include "resource/resource_ids.auto.h"
#include "test_workout_app_includes.h"
// Fakes
/////////////////////
uint16_t time_ms(time_t *tloc, uint16_t *out_ms) {
return 0;
}
// Setup and Teardown
////////////////////////////////////
static GContext s_ctx;
static FrameBuffer s_fb;
GContext *graphics_context_get_current_context(void) {
return &s_ctx;
}
void test_workout_dialog__initialize(void) {
// Setup graphics context
framebuffer_init(&s_fb, &(GSize) {DISP_COLS, DISP_ROWS});
framebuffer_clear(&s_fb);
graphics_context_init(&s_ctx, &s_fb, GContextInitializationMode_App);
s_app_state_get_graphics_context = &s_ctx;
// Setup resources
fake_spi_flash_init(0 /* offset */, 0x1000000 /* length */);
pfs_init(false /* run filesystem check */);
pfs_format(true /* write erase headers */);
load_resource_fixture_in_flash(RESOURCES_FIXTURE_PATH, SYSTEM_RESOURCES_FIXTURE_NAME,
false /* is_next */);
resource_init();
// Setup content indicator
ContentIndicatorsBuffer *buffer = content_indicator_get_current_buffer();
content_indicator_init_buffer(buffer);
}
void test_workout_dialog__cleanup(void) {
}
// Tests
//////////////////////
void test_workout_dialog__render_end_workout(void) {
WorkoutDialog *workout_dialog = workout_dialog_create("Workout End");
Dialog *dialog = workout_dialog_get_dialog(workout_dialog);
dialog_show_status_bar_layer(dialog, true);
dialog_set_fullscreen(dialog, true);
dialog_set_text(dialog, "End Workout?");
dialog_set_background_color(dialog, PBL_IF_COLOR_ELSE(GColorYellow, GColorWhite));
dialog_set_text_color(dialog, GColorBlack);
dialog_set_icon(dialog, RESOURCE_ID_WORKOUT_APP_END);
dialog_set_icon_animate_direction(dialog, DialogIconAnimateNone);
window_set_on_screen(&dialog->window, true, true);
window_render(&dialog->window, &s_ctx);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_workout_dialog__render_detected_workout(void) {
WorkoutDialog *workout_dialog = workout_dialog_create("Workout Detected");
Dialog *dialog = workout_dialog_get_dialog(workout_dialog);
dialog_show_status_bar_layer(dialog, true);
dialog_set_fullscreen(dialog, true);
dialog_set_background_color(dialog, PBL_IF_COLOR_ELSE(GColorYellow, GColorWhite));
dialog_set_text_color(dialog, GColorBlack);
dialog_set_icon(dialog, RESOURCE_ID_WORKOUT_APP_DETECTED);
dialog_set_icon_animate_direction(dialog, DialogIconAnimateNone);
workout_dialog_set_text(workout_dialog, "Run\nDetected");
workout_dialog_set_subtext(workout_dialog, "03:42");
window_set_on_screen(&dialog->window, true, true);
window_render(&dialog->window, &s_ctx);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_workout_dialog__render_workout_ended(void) {
WorkoutDialog *workout_dialog = workout_dialog_create("Workout Ended");
Dialog *dialog = workout_dialog_get_dialog(workout_dialog);
dialog_show_status_bar_layer(dialog, true);
dialog_set_fullscreen(dialog, true);
dialog_set_background_color(dialog, PBL_IF_COLOR_ELSE(GColorYellow, GColorWhite));
dialog_set_text_color(dialog, GColorBlack);
dialog_set_icon(dialog, RESOURCE_ID_WORKOUT_APP_DETECTED);
dialog_set_icon_animate_direction(dialog, DialogIconAnimateNone);
workout_dialog_set_text(workout_dialog, "Workout\nEnded");
workout_dialog_set_action_bar_hidden(workout_dialog, true);
window_set_on_screen(&dialog->window, true, true);
window_render(&dialog->window, &s_ctx);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
@@ -0,0 +1,102 @@
/*
* 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 "apps/system_apps/workout/workout_selection.h"
#include "test_workout_app_includes.h"
typedef struct WorkoutSelectionWindow {
Window window;
MenuLayer menu_layer;
} WorkoutSelectionWindow;
// Fakes
/////////////////////
uint16_t time_ms(time_t *tloc, uint16_t *out_ms) {
return 0;
}
bool workout_service_is_workout_type_supported(ActivitySessionType type) {
return true;
}
// Setup and Teardown
////////////////////////////////////
static GContext s_ctx;
static FrameBuffer s_fb;
GContext *graphics_context_get_current_context(void) {
return &s_ctx;
}
void test_workout_selection__initialize(void) {
// Setup graphics context
framebuffer_init(&s_fb, &(GSize) {DISP_COLS, DISP_ROWS});
framebuffer_clear(&s_fb);
graphics_context_init(&s_ctx, &s_fb, GContextInitializationMode_App);
s_app_state_get_graphics_context = &s_ctx;
// Setup resources
fake_spi_flash_init(0 /* offset */, 0x1000000 /* length */);
pfs_init(false /* run filesystem check */);
pfs_format(true /* write erase headers */);
load_resource_fixture_in_flash(RESOURCES_FIXTURE_PATH, SYSTEM_RESOURCES_FIXTURE_NAME,
false /* is_next */);
resource_init();
// Setup content indicator
ContentIndicatorsBuffer *buffer = content_indicator_get_current_buffer();
content_indicator_init_buffer(buffer);
}
void test_workout_selection__cleanup(void) {
}
// Helpers
//////////////////////
static void prv_select_workout_cb(ActivitySessionType type) { }
static void prv_create_window_and_render(uint16_t row) {
WorkoutSelectionWindow *selection_window = workout_selection_push(prv_select_workout_cb);
Window *window = &selection_window->window;
MenuLayer *menu_layer = &selection_window->menu_layer;
menu_layer_set_selected_index(menu_layer, MenuIndex(0, row), MenuRowAlignCenter, false);
window_set_on_screen(window, true, true);
window_render(window, &s_ctx);
}
// Tests
//////////////////////
void test_workout_selection__render_run_selected(void) {
prv_create_window_and_render(0);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_workout_selection__render_walk_selected(void) {
prv_create_window_and_render(1);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_workout_selection__render_workout_selected(void) {
prv_create_window_and_render(2);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
@@ -0,0 +1,96 @@
/*
* 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 "apps/system_apps/workout/workout_summary.h"
#include "apps/system_apps/workout/workout_utils.h"
#include "test_workout_app_includes.h"
// Fakes
/////////////////////
uint16_t time_ms(time_t *tloc, uint16_t *out_ms) {
return 0;
}
bool workout_service_is_workout_type_supported(ActivitySessionType type) {
return true;
}
// Setup and Teardown
////////////////////////////////////
static GContext s_ctx;
static FrameBuffer s_fb;
GContext *graphics_context_get_current_context(void) {
return &s_ctx;
}
void test_workout_summary__initialize(void) {
// Setup graphics context
framebuffer_init(&s_fb, &(GSize) {DISP_COLS, DISP_ROWS});
framebuffer_clear(&s_fb);
graphics_context_init(&s_ctx, &s_fb, GContextInitializationMode_App);
s_app_state_get_graphics_context = &s_ctx;
// Setup resources
fake_spi_flash_init(0 /* offset */, 0x1000000 /* length */);
pfs_init(false /* run filesystem check */);
pfs_format(true /* write erase headers */);
load_resource_fixture_in_flash(RESOURCES_FIXTURE_PATH, SYSTEM_RESOURCES_FIXTURE_NAME,
false /* is_next */);
resource_init();
// Setup content indicator
ContentIndicatorsBuffer *buffer = content_indicator_get_current_buffer();
content_indicator_init_buffer(buffer);
}
void test_workout_summary__cleanup(void) {
}
// Helpers
//////////////////////
static void prv_start_workout_cb(ActivitySessionType type) { }
static void prv_select_workout_cb(ActivitySessionType type) { }
static void prv_create_window_and_render(ActivitySessionType activity_type) {
Window *window = (Window *)workout_summary_window_create(activity_type,
prv_start_workout_cb,
prv_select_workout_cb);
window_set_on_screen(window, true, true);
window_render(window, &s_ctx);
}
// Tests
//////////////////////
void test_workout_summary__render_open_workout(void) {
prv_create_window_and_render(ActivitySessionType_Open);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_workout_summary__render_walk(void) {
prv_create_window_and_render(ActivitySessionType_Walk);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_workout_summary__render_run(void) {
prv_create_window_and_render(ActivitySessionType_Run);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
@@ -0,0 +1,109 @@
/*
* 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 "apps/system_apps/workout/workout_utils.h"
#include "clar.h"
#include "services/normal/activity/activity.h"
// ---------------------------------------------------------------------------------------
#include "stubs_attribute.h"
#include "stubs_i18n.h"
#include "stubs_notifications.h"
#include "stubs_passert.h"
#include "stubs_pbl_malloc.h"
#include "stubs_rtc.h"
#include "stubs_timeline_item.h"
// Stubs
///////////////////////////////////////////////////////////
bool workout_service_is_workout_type_supported(ActivitySessionType type) {
return true;
}
// Fakes
///////////////////////////////////////////////////////////
static ActivitySession s_sessions[ACTIVITY_MAX_ACTIVITY_SESSIONS_COUNT];
static uint32_t s_num_sessions = 0;
static void prv_add_session(ActivitySession *session) {
memcpy(&s_sessions[s_num_sessions++], session, sizeof(ActivitySession));
}
// ---------------------------------------------------------------------------------------
bool activity_get_sessions(uint32_t *session_entries, ActivitySession *sessions) {
memcpy(sessions, s_sessions, s_num_sessions * sizeof(ActivitySession));
*session_entries = s_num_sessions;
return true;
}
// ---------------------------------------------------------------------------------------
void test_workout_utils__initialize(void) {
s_num_sessions = 0;
}
void test_workout_utils__cleanup(void) {
}
// ---------------------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------------------
void test_workout_utils__find_ongoing_activity_session(void) {
bool found_session;
// Check if it can handle NULL session
found_session = workout_utils_find_ongoing_activity_session(NULL);
cl_assert_equal_b(found_session, false);
// Make sure there are no sessions
cl_assert_equal_i(s_num_sessions, 0);
// Add a non-ongoing walk session
prv_add_session(&(ActivitySession){
.type = ActivitySessionType_Walk,
.ongoing = false,
});
// Make sure the session was added
cl_assert_equal_i(s_num_sessions, 1);
// Find the session we just added
ActivitySession walk_session = {};
found_session = workout_utils_find_ongoing_activity_session(&walk_session);
// Made sure non-ongoing sessions are not returned
cl_assert_equal_b(found_session, false);
// Add an ongoing run session
prv_add_session(&(ActivitySession){
.type = ActivitySessionType_Run,
.ongoing = true,
});
// Make sure the session was added
cl_assert_equal_i(s_num_sessions, 2);
// Find the session we just added
ActivitySession run_session = {};
found_session = workout_utils_find_ongoing_activity_session(&run_session);
// Make sure the function returned true and the returned session is of type run
cl_assert_equal_b(found_session, true);
cl_assert_equal_i(run_session.type, ActivitySessionType_Run);
}
+155
View File
@@ -0,0 +1,155 @@
from waftools.pebble_test import clar
def build(ctx):
rendering_sources = \
" src/fw/applib/fonts/codepoint.c" \
" src/fw/applib/graphics/${BITDEPTH}_bit/framebuffer.c" \
" src/fw/applib/graphics/${BITDEPTH}_bit/bitblt_private.c" \
" src/fw/applib/graphics/bitblt.c" \
" src/fw/applib/graphics/framebuffer.c" \
" src/fw/applib/graphics/gbitmap.c" \
" src/fw/applib/graphics/gbitmap_png.c" \
" src/fw/applib/graphics/gbitmap_sequence.c" \
" src/fw/applib/graphics/gcolor_definitions.c" \
" src/fw/applib/graphics/gdraw_command.c" \
" src/fw/applib/graphics/gdraw_command_frame.c" \
" src/fw/applib/graphics/gdraw_command_image.c" \
" src/fw/applib/graphics/gdraw_command_list.c" \
" src/fw/applib/graphics/gdraw_command_sequence.c" \
" src/fw/applib/graphics/gpath.c" \
" src/fw/applib/graphics/graphics.c" \
" src/fw/applib/graphics/graphics_bitmap.c" \
" src/fw/applib/graphics/graphics_circle.c" \
" src/fw/applib/graphics/graphics_line.c" \
" src/fw/applib/graphics/graphics_private.c" \
" src/fw/applib/graphics/graphics_private_raw.c" \
" src/fw/applib/graphics/gtransform.c" \
" src/fw/applib/graphics/gtypes.c" \
" src/fw/applib/graphics/perimeter.c" \
" src/fw/applib/graphics/text_layout.c" \
" src/fw/applib/graphics/text_render.c" \
" src/fw/applib/graphics/text_resources.c" \
" src/fw/applib/graphics/utf8.c" \
" src/fw/applib/ui/action_bar_layer.c" \
" src/fw/applib/ui/status_bar_layer.c" \
" src/fw/applib/ui/kino/kino_reel.c" \
" src/fw/applib/ui/kino/kino_reel_gbitmap.c" \
" src/fw/applib/ui/kino/kino_reel_gbitmap_sequence.c" \
" src/fw/applib/ui/kino/kino_reel_pdci.c" \
" src/fw/applib/ui/kino/kino_reel_pdcs.c" \
" src/fw/applib/ui/layer.c" \
" src/fw/applib/ui/menu_layer_system_cells.c" \
" src/fw/applib/vendor/tinflate/tinflate.c" \
" src/fw/applib/vendor/uPNG/upng.c" \
" src/fw/board/displays/display_spalding.c" \
" src/fw/drivers/flash/flash_crc.c" \
" src/fw/flash_region/filesystem_regions.c" \
" src/fw/flash_region/flash_region.c" \
" src/fw/resource/resource.c" \
" src/fw/resource/resource_storage.c" \
" src/fw/resource/resource_storage_builtin.c" \
" src/fw/resource/resource_storage_file.c" \
" src/fw/resource/resource_storage_flash.c" \
" src/fw/services/normal/filesystem/app_file.c" \
" src/fw/services/normal/filesystem/flash_translation.c" \
" src/fw/services/normal/filesystem/pfs.c" \
" src/fw/system/hexdump.c" \
" src/fw/util/crc8.c" \
" src/fw/util/legacy_checksum.c" \
" tests/fakes/fake_applib_resource.c" \
" tests/fakes/fake_display.c" \
" tests/fakes/fake_gbitmap_get_data_row.c" \
" tests/fakes/fake_rtc.c" \
" tests/fakes/fake_spi_flash.c" \
" tests/fixtures/resources/builtin_resources.auto.c" \
" tests/fixtures/resources/pfs_resource_table.c" \
" tests/stubs/stubs_animation.c" \
" tests/stubs/stubs_system_theme.c"
workout_app_sources = \
" src/fw/applib/graphics/gpath_builder.c" \
" src/fw/applib/ui/animation_interpolate.c" \
" src/fw/applib/ui/content_indicator.c" \
" src/fw/applib/ui/dialogs/dialog.c" \
" src/fw/applib/ui/dialogs/dialog_private.c" \
" src/fw/applib/ui/inverter_layer.c" \
" src/fw/applib/ui/kino/kino_layer.c" \
" src/fw/applib/ui/kino/kino_player.c" \
" src/fw/applib/ui/kino/kino_reel/transform.c" \
" src/fw/applib/ui/kino/kino_reel_custom.c" \
" src/fw/applib/ui/scroll_layer.c" \
" src/fw/applib/ui/shadows.c" \
" src/fw/applib/ui/text_layer.c" \
" src/fw/applib/ui/text_layer_flow.c" \
" src/fw/applib/ui/window.c" \
" src/fw/apps/system_apps/timeline/text_node.c" \
" src/fw/services/normal/activity/health_util.c" \
" src/fw/services/normal/activity/hr_util.c" \
" src/fw/services/normal/timeline/timeline_resources.c" \
" src/fw/util/buffer.c" \
" src/fw/util/stats.c" \
" src/fw/util/time/mktime.c" \
" src/fw/util/time/time.c" \
" tests/fakes/fake_clock.c" \
" tests/fakes/fake_fonts.c" \
" tests/fakes/fake_workout_service.c" \
" tests/fixtures/resources/timeline_resource_table.auto.c"
clar(ctx,
sources_ant_glob = rendering_sources + \
workout_app_sources + \
" src/fw/applib/ui/menu_layer.c" \
" src/fw/apps/system_apps/workout/workout_countdown.c" + \
" src/fw/apps/system_apps/workout/workout_selection.c" + \
" src/fw/apps/system_apps/workout/workout_dialog.c" + \
" src/fw/apps/system_apps/workout/workout_utils.c" + \
" src/fw/apps/system_apps/workout/workout_summary.c",
test_sources_ant_glob = "test_workout_summary.c",
defines=ctx.env.test_image_defines + ["USE_DISPLAY_PERIMETER_ON_FONT_LAYOUT=1"],
runtime_deps=ctx.env.test_pngs + ctx.env.test_pbis + ctx.env.test_pfos,
override_includes=['dummy_board'],
platforms=['snowy', 'spalding', 'silk'])
clar(ctx,
sources_ant_glob = rendering_sources + \
workout_app_sources + \
" src/fw/applib/ui/menu_layer.c" \
" src/fw/apps/system_apps/workout/workout_data.c" + \
" src/fw/apps/system_apps/workout/workout_dialog.c" + \
" src/fw/apps/system_apps/workout/workout_active.c",
test_sources_ant_glob = "test_workout_active.c",
defines=ctx.env.test_image_defines + ["USE_DISPLAY_PERIMETER_ON_FONT_LAYOUT=1"],
runtime_deps=ctx.env.test_pngs + ctx.env.test_pbis + ctx.env.test_pfos,
override_includes=['dummy_board'],
platforms=['snowy', 'spalding', 'silk'])
clar(ctx,
sources_ant_glob = rendering_sources + \
workout_app_sources + \
" src/fw/applib/ui/menu_layer.c" \
" src/fw/apps/system_apps/workout/workout_data.c" + \
" src/fw/apps/system_apps/workout/workout_dialog.c",
test_sources_ant_glob = "test_workout_dialog.c",
defines=ctx.env.test_image_defines + ["USE_DISPLAY_PERIMETER_ON_FONT_LAYOUT=1"],
runtime_deps=ctx.env.test_pngs + ctx.env.test_pbis + ctx.env.test_pfos,
override_includes=['dummy_board'],
platforms=['snowy', 'spalding', 'silk'])
clar(ctx,
sources_ant_glob = rendering_sources + \
workout_app_sources + \
" src/fw/applib/ui/menu_layer.c" \
" src/fw/apps/system_apps/workout/workout_utils.c" + \
" src/fw/apps/system_apps/workout/workout_selection.c",
test_sources_ant_glob = "test_workout_selection.c",
defines=ctx.env.test_image_defines + ["USE_DISPLAY_PERIMETER_ON_FONT_LAYOUT=1"],
runtime_deps=ctx.env.test_pngs + ctx.env.test_pbis + ctx.env.test_pfos,
override_includes=['dummy_board'],
platforms=['snowy', 'spalding', 'silk'])
clar(ctx,
sources_ant_glob = " src/fw/apps/system_apps/workout/workout_utils.c",
test_sources_ant_glob = "test_workout_utils.c",
override_includes=['dummy_board'])
# vim:filetype=python
@@ -0,0 +1,236 @@
/*
* 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 "apps/watch/kickstart/kickstart.h"
#include "popups/timeline/peek.h"
#include "applib/ui/window_private.h"
#include "fw/graphics/util.h"
#include "util/size.h"
#include "clar.h"
extern void prv_window_load_handler(Window *window);
extern void prv_window_unload_handler(Window *window);
extern void prv_set_unobstructed_area_height(int16_t height);
extern void prv_set_data(KickstartData *data, int32_t current_steps,
int32_t typical_steps, int32_t daily_steps_avg, int32_t current_bpm);
// Fakes
/////////////////////
#include "fake_pbl_std.h"
#include "fake_spi_flash.h"
#include "fixtures/load_test_resources.h"
static bool s_clock_is_24h_style = false;
bool clock_is_24h_style(void) {
return s_clock_is_24h_style;
}
// Stubs
/////////////////////
#include "stubs_analytics.h"
#include "stubs_app.h"
#include "stubs_app_install_manager.h"
#include "stubs_app_timer.h"
#include "stubs_app_window_stack.h"
#include "stubs_bootbits.h"
#include "stubs_click.h"
#include "stubs_event_service_client.h"
#include "stubs_health_service.h"
#include "stubs_layer.h"
#include "stubs_logging.h"
#include "stubs_memory_layout.h"
#include "stubs_mutex.h"
#include "stubs_passert.h"
#include "stubs_pbl_malloc.h"
#include "stubs_pebble_process_info.h"
#include "stubs_pebble_tasks.h"
#include "stubs_process_manager.h"
#include "stubs_prompt.h"
#include "stubs_serial.h"
#include "stubs_shell_prefs.h"
#include "stubs_sleep.h"
#include "stubs_syscalls.h"
#include "stubs_task_watchdog.h"
#include "stubs_text_layer_flow.h"
#include "stubs_vibes.h"
#include "stubs_window_manager.h"
#include "stubs_window_stack.h"
void tick_timer_service_subscribe(TimeUnits tick_units, TickHandler handler) {}
void tick_timer_service_unsubscribe(void) {}
// Setup and Teardown
////////////////////////////////////
static GContext s_ctx;
static FrameBuffer s_fb;
static KickstartData s_data;
GContext *graphics_context_get_current_context(void) {
return &s_ctx;
}
void test_kickstart__initialize(void) {
// Setup graphics context
framebuffer_init(&s_fb, &(GSize) {DISP_COLS, DISP_ROWS});
framebuffer_clear(&s_fb);
graphics_context_init(&s_ctx, &s_fb, GContextInitializationMode_App);
s_app_state_get_graphics_context = &s_ctx;
// Setup resources
fake_spi_flash_init(0 /* offset */, 0x1000000 /* length */);
pfs_init(false /* run filesystem check */);
pfs_format(true /* write erase headers */);
load_resource_fixture_in_flash(RESOURCES_FIXTURE_PATH, SYSTEM_RESOURCES_FIXTURE_NAME,
false /* is_next */);
resource_init();
// Reset data
prv_set_data(&s_data, 0, 0, 0, 0);
prv_set_unobstructed_area_height(0);
s_clock_is_24h_style = false;
// Init window
window_init(&s_data.window, WINDOW_NAME("Kickstart"));
window_set_user_data(&s_data.window, &s_data);
window_set_window_handlers(&s_data.window, &(WindowHandlers){
.load = prv_window_load_handler,
.unload = prv_window_unload_handler,
});
}
void test_kickstart__cleanup(void) {
window_deinit(&s_data.window);
}
// Tests
//////////////////////
void test_kickstart__render_no_data(void) {
window_set_on_screen(&s_data.window, true, true);
window_render(&s_data.window, &s_ctx);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_kickstart__render_obstructed_area(void) {
#if !PBL_ROUND
prv_set_unobstructed_area_height(TIMELINE_PEEK_HEIGHT);
prv_set_data(&s_data, 5543, 6500, 8000, 0);
window_set_on_screen(&s_data.window, true, true);
window_render(&s_data.window, &s_ctx);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
#endif
}
void test_kickstart__render_steps_above_typical(void) {
prv_set_data(&s_data, 3528, 2500, 8000, 0);
window_set_on_screen(&s_data.window, true, true);
window_render(&s_data.window, &s_ctx);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_kickstart__render_steps_below_typical(void) {
prv_set_data(&s_data, 5543, 6500, 8000, 0);
window_set_on_screen(&s_data.window, true, true);
window_render(&s_data.window, &s_ctx);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_kickstart__render_steps_above_daily_avg(void) {
prv_set_data(&s_data, 10323, 7500, 8000, 0);
window_set_on_screen(&s_data.window, true, true);
window_render(&s_data.window, &s_ctx);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_kickstart__render_hr_bpm(void) {
#if (PLATFORM_SILK || PLATFORM_ROBERT)
prv_set_data(&s_data, 10323, 7500, 13000, 82);
window_set_on_screen(&s_data.window, true, true);
window_render(&s_data.window, &s_ctx);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
#endif
}
void test_kickstart__render_hr_bpm_obstructed(void) {
#if (PLATFORM_SILK || PLATFORM_ROBERT)
prv_set_data(&s_data, 10323, 7500, 13000, 82);
prv_set_unobstructed_area_height(TIMELINE_PEEK_HEIGHT);
window_set_on_screen(&s_data.window, true, true);
window_render(&s_data.window, &s_ctx);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
#endif
}
void test_kickstart__render_steps_above_daily_avg_24h(void) {
s_clock_is_24h_style = true;
prv_set_data(&s_data, 10323, 7500, 8000, 0);
window_set_on_screen(&s_data.window, true, true);
window_render(&s_data.window, &s_ctx);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_kickstart__render_hr_bpm_24h(void) {
#if (PLATFORM_SILK || PLATFORM_ROBERT)
s_clock_is_24h_style = true;
prv_set_data(&s_data, 10323, 7500, 13000, 82);
window_set_on_screen(&s_data.window, true, true);
window_render(&s_data.window, &s_ctx);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
#endif
}
void test_kickstart__render_hr_bpm_obstructed_24h(void) {
#if (PLATFORM_SILK || PLATFORM_ROBERT)
s_clock_is_24h_style = true;
prv_set_data(&s_data, 10323, 7500, 13000, 82);
prv_set_unobstructed_area_height(TIMELINE_PEEK_HEIGHT);
window_set_on_screen(&s_data.window, true, true);
window_render(&s_data.window, &s_ctx);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
#endif
}
void test_kickstart__render_PBL_43681(void) {
prv_set_data(&s_data, 0, 1098, 8, 0);
window_set_on_screen(&s_data.window, true, true);
window_render(&s_data.window, &s_ctx);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_kickstart__render_PBL_43717(void) {
prv_set_data(&s_data, 7, 0, 7, 0);
window_set_on_screen(&s_data.window, true, true);
window_render(&s_data.window, &s_ctx);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
+87
View File
@@ -0,0 +1,87 @@
from waftools.pebble_test import clar
def build(ctx):
rendering_sources = \
" src/fw/applib/fonts/codepoint.c" \
" src/fw/applib/graphics/${BITDEPTH}_bit/framebuffer.c" \
" src/fw/applib/graphics/${BITDEPTH}_bit/bitblt_private.c" \
" src/fw/applib/graphics/bitblt.c" \
" src/fw/applib/graphics/framebuffer.c" \
" src/fw/applib/graphics/gbitmap.c" \
" src/fw/applib/graphics/gbitmap_png.c" \
" src/fw/applib/graphics/gbitmap_sequence.c" \
" src/fw/applib/graphics/gcolor_definitions.c" \
" src/fw/applib/graphics/gdraw_command.c" \
" src/fw/applib/graphics/gdraw_command_frame.c" \
" src/fw/applib/graphics/gdraw_command_image.c" \
" src/fw/applib/graphics/gdraw_command_list.c" \
" src/fw/applib/graphics/gdraw_command_sequence.c" \
" src/fw/applib/graphics/gpath.c" \
" src/fw/applib/graphics/graphics.c" \
" src/fw/applib/graphics/graphics_bitmap.c" \
" src/fw/applib/graphics/graphics_circle.c" \
" src/fw/applib/graphics/graphics_line.c" \
" src/fw/applib/graphics/graphics_private.c" \
" src/fw/applib/graphics/graphics_private_raw.c" \
" src/fw/applib/graphics/gtransform.c" \
" src/fw/applib/graphics/gtypes.c" \
" src/fw/applib/graphics/perimeter.c" \
" src/fw/applib/graphics/text_layout.c" \
" src/fw/applib/graphics/text_render.c" \
" src/fw/applib/graphics/text_resources.c" \
" src/fw/applib/graphics/utf8.c" \
" src/fw/applib/ui/status_bar_layer.c" \
" src/fw/applib/ui/layer.c" \
" src/fw/applib/vendor/tinflate/tinflate.c" \
" src/fw/applib/vendor/uPNG/upng.c" \
" src/fw/board/displays/display_spalding.c" \
" src/fw/drivers/flash/flash_crc.c" \
" src/fw/flash_region/filesystem_regions.c" \
" src/fw/flash_region/flash_region.c" \
" src/fw/resource/resource.c" \
" src/fw/resource/resource_storage.c" \
" src/fw/resource/resource_storage_builtin.c" \
" src/fw/resource/resource_storage_file.c" \
" src/fw/resource/resource_storage_flash.c" \
" src/fw/services/normal/filesystem/app_file.c" \
" src/fw/services/normal/filesystem/flash_translation.c" \
" src/fw/services/normal/filesystem/pfs.c" \
" src/fw/system/hexdump.c" \
" src/fw/util/crc8.c" \
" src/fw/util/legacy_checksum.c" \
" tests/fakes/fake_applib_resource.c" \
" tests/fakes/fake_display.c" \
" tests/fakes/fake_gbitmap_get_data_row.c" \
" tests/fakes/fake_rtc.c" \
" tests/fakes/fake_spi_flash.c" \
" tests/fixtures/resources/builtin_resources.auto.c" \
" tests/fixtures/resources/pfs_resource_table.c" \
" tests/stubs/stubs_animation.c" \
" tests/stubs/stubs_system_theme.c"
kickstart_app_sources = \
" src/fw/applib/graphics/gpath_builder.c" \
" src/fw/applib/ui/animation_interpolate.c" \
" src/fw/applib/ui/shadows.c" \
" src/fw/applib/ui/text_layer.c" \
" src/fw/applib/ui/window.c" \
" src/fw/apps/system_apps/timeline/text_node.c" \
" src/fw/services/normal/activity/health_util.c" \
" src/fw/util/buffer.c" \
" src/fw/util/stats.c" \
" src/fw/util/time/mktime.c" \
" src/fw/util/time/time.c" \
" tests/fakes/fake_clock.c" \
" tests/fakes/fake_fonts.c"
clar(ctx,
sources_ant_glob = rendering_sources + \
kickstart_app_sources + \
" src/fw/apps/watch/kickstart/kickstart.c",
test_sources_ant_glob = "test_kickstart.c",
defines=ctx.env.test_image_defines + ["USE_DISPLAY_PERIMETER_ON_FONT_LAYOUT=1"],
runtime_deps=ctx.env.test_pngs + ctx.env.test_pbis + ctx.env.test_pfos,
override_includes=['dummy_board'],
platforms=['snowy', 'spalding', 'silk', 'robert'])
# vim:filetype=python
+681
View File
@@ -0,0 +1,681 @@
/*
* 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 <stdint.h>
#include "resource/timeline_resource_ids.auto.h"
#include "util/attributes.h"
// Chunked dictionary 1/2 sent by ANCS (w/header)
// 00 97 00 00 00 00 13 00 63 6f 6d 2e 61 70 70 6c ........ com.appl
// 65 2e 4d 6f 62 69 6c 65 53 4d 53 01 10 00 72 6a e.Mobile SMS...rj
// 63 61 73 65 40 67 6d 61 69 6c 2e 63 6f 6d 02 00 case@gma il.com..
// 00 03 c8 00 54 68 69 73 20 69 73 20 61 20 76 65 ....This is a ve
// 72 79 20 63 6f 6d 70 6c 69 63 61 74 65 64 20 63 ry compl icated c
// 61 73 65 2c 20 4d 61 75 64 65 2e 20 59 6f 75 20 ase, Mau de. You
// 6b 6e 6f 77 2c 20 61 20 6c 6f 74 74 61 20 69 6e know, a lotta in
// 73 2c 20 6c 6f 74 74 61 20 6f 75 74 73 2c 20 6c s, lotta outs, l
// 6f 74 74 61 20 77 68 61 74 2d 68 61 76 65 2d 79 otta wha t-have-y
// 6f 75 27 73 2e 20 41 6e 64 2c 20 ou's. An d,
// NB header has been removed
static const uint8_t s_chunked_dict_part_one[] = {
0x00, 0x97, 0x00, 0x00, 0x00, 0x00, 0x13, 0x00, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c,
0x65, 0x2e, 0x4d, 0x6f, 0x62, 0x69, 0x6c, 0x65, 0x53, 0x4d, 0x53, 0x01, 0x10, 0x00, 0x72, 0x6a,
0x63, 0x61, 0x73, 0x65, 0x40, 0x67, 0x6d, 0x61, 0x69, 0x6c, 0x2e, 0x63, 0x6f, 0x6d, 0x02, 0x00,
0x00, 0x03, 0xc8, 0x00, 0x54, 0x68, 0x69, 0x73, 0x20, 0x69, 0x73, 0x20, 0x61, 0x20, 0x76, 0x65,
0x72, 0x79, 0x20, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x65, 0x64, 0x20, 0x63,
0x61, 0x73, 0x65, 0x2c, 0x20, 0x4d, 0x61, 0x75, 0x64, 0x65, 0x2e, 0x20, 0x59, 0x6f, 0x75, 0x20,
0x6b, 0x6e, 0x6f, 0x77, 0x2c, 0x20, 0x61, 0x20, 0x6c, 0x6f, 0x74, 0x74, 0x61, 0x20, 0x69, 0x6e,
0x73, 0x2c, 0x20, 0x6c, 0x6f, 0x74, 0x74, 0x61, 0x20, 0x6f, 0x75, 0x74, 0x73, 0x2c, 0x20, 0x6c,
0x6f, 0x74, 0x74, 0x61, 0x20, 0x77, 0x68, 0x61, 0x74, 0x2d, 0x68, 0x61, 0x76, 0x65, 0x2d, 0x79,
0x6f, 0x75, 0x27, 0x73, 0x2e, 0x20, 0x41, 0x6e, 0x64, 0x2c, 0x20
};
// Chunked dictionary 2/2 sent by ANCS
// 75 68 2c 20 6c 6f 74 74 61 20 73 74 72 61 6e 64 uh, lott a strand
// 73 20 74 6f 20 6b 65 65 70 20 69 6e 20 6d 79 20 s to kee p in my
// 68 65 61 64 2c 20 6d 61 6e 2e 20 4c 6f 74 74 61 head, ma n. Lotta
// 20 73 74 72 61 6e 64 73 20 69 6e 20 6f 6c 64 20 strands in old
// 44 75 64 65 72 27 73 20 68 65 61 64 2e 20 4c 75 Duder's head. Lu
// 63 6b 69 6c 79 20 49 27 6d 20 61 64 68 65 72 69 ckily I' m adheri
// 6e n
static const uint8_t s_chunked_dict_part_two[] = {
0x75, 0x68, 0x2c, 0x20, 0x6c, 0x6f, 0x74, 0x74, 0x61, 0x20, 0x73, 0x74, 0x72, 0x61, 0x6e, 0x64,
0x73, 0x20, 0x74, 0x6f, 0x20, 0x6b, 0x65, 0x65, 0x70, 0x20, 0x69, 0x6e, 0x20, 0x6d, 0x79, 0x20,
0x68, 0x65, 0x61, 0x64, 0x2c, 0x20, 0x6d, 0x61, 0x6e, 0x2e, 0x20, 0x4c, 0x6f, 0x74, 0x74, 0x61,
0x20, 0x73, 0x74, 0x72, 0x61, 0x6e, 0x64, 0x73, 0x20, 0x69, 0x6e, 0x20, 0x6f, 0x6c, 0x64, 0x20,
0x44, 0x75, 0x64, 0x65, 0x72, 0x27, 0x73, 0x20, 0x68, 0x65, 0x61, 0x64, 0x2e, 0x20, 0x4c, 0x75,
0x63, 0x6b, 0x69, 0x6c, 0x79, 0x20, 0x49, 0x27, 0x6d, 0x20, 0x61, 0x64, 0x68, 0x65, 0x72, 0x69,
0x6e, /* date */ 0x05, 0x0f, 0x00, 0x32, 0x30, 0x31,
0x33, 0x31, 0x31, 0x30, 0x35, 0x54, 0x31, 0x38, 0x31, 0x39, 0x32, 0x38
};
// Complete dictionary sent by ANCS
// 00 98 00 00 00 00 13 00 63 6f 6d 2e 61 70 70 6c ........ com.appl
// 65 2e 4d 6f 62 69 6c 65 53 4d 53 01 10 00 72 6a e.Mobile SMS...rj
// 63 61 73 65 40 67 6d 61 69 6c 2e 63 6f 6d 02 00 case@gma il.com..
// 00 03 26 00 43 61 72 65 66 75 6c 2c 20 6d 61 6e ..&.Care ful, man
// 2c 20 74 68 65 72 65 27 73 20 61 20 62 65 76 65 , there' s a beve
// 72 61 67 65 20 68 65 72 65 21 rage her e! // FIXME 4th
// attr has been appended
// Has header removed:
static const uint8_t s_complete_dict[] = {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x13, 0x00, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c,
0x65, 0x2e, 0x4d, 0x6f, 0x62, 0x69, 0x6c, 0x65, 0x53, 0x4d, 0x53, 0x01, 0x10, 0x00, 0x72, 0x6a,
0x63, 0x61, 0x73, 0x65, 0x40, 0x67, 0x6d, 0x61, 0x69, 0x6c, 0x2e, 0x63, 0x6f, 0x6d, 0x02, 0x00,
0x00, 0x03, 0x26, 0x00, 0x43, 0x61, 0x72, 0x65, 0x66, 0x75, 0x6c, 0x2c, 0x20, 0x6d, 0x61, 0x6e,
0x2c, 0x20, 0x74, 0x68, 0x65, 0x72, 0x65, 0x27, 0x73, 0x20, 0x61, 0x20, 0x62, 0x65, 0x76, 0x65,
0x72, 0x61, 0x67, 0x65, 0x20, 0x68, 0x65, 0x72, 0x65, 0x21, /* date */ 0x05, 0x0f, 0x00, 0x32, 0x30, 0x31,
0x33, 0x31, 0x31, 0x30, 0x35, 0x54, 0x31, 0x38, 0x31, 0x39, 0x32, 0x38
};
//! 00 3c 00 00 00 05 0f 00 32 30 31 36 30 33 30 31 ........ 20160301
//! 54 31 34 31 35 32 33 01 08 00 4f 76 65 72 63 61 T141523. ..Overca
//! 73 74 02 00 00 03 2b 00 54 68 65 20 4d 6f 74 68 st....+. The Moth
//! 20 50 6f 64 63 61 73 74 3a 20 4d 6f 6e 69 63 61 Podcast : Monica
//! 20 43 68 65 72 75 2d 4d 70 61 6d 62 61 77 61 73 Cheru-M pambawas
//! e2 80 a6 04 03 00 31 31 33 ......11 3
static const uint8_t s_message_size_attr_dict[] = {
0x00, 0x3c, 0x00, 0x00, 0x00, 0x05, 0x0f, 0x00, 0x32, 0x30, 0x31, 0x36, 0x30, 0x33, 0x30, 0x31,
0x54, 0x31, 0x34, 0x31, 0x35, 0x32, 0x33, 0x01, 0x08, 0x00, 0x4f, 0x76, 0x65, 0x72, 0x63, 0x61,
0x73, 0x74, 0x02, 0x00, 0x00, 0x03, 0x2b, 0x00, 0x54, 0x68, 0x65, 0x20, 0x4d, 0x6f, 0x74, 0x68,
0x20, 0x50, 0x6f, 0x64, 0x63, 0x61, 0x73, 0x74, 0x3a, 0x20, 0x4d, 0x6f, 0x6e, 0x69, 0x63, 0x61,
0x20, 0x43, 0x68, 0x65, 0x72, 0x75, 0x2d, 0x4d, 0x70, 0x61, 0x6d, 0x62, 0x61, 0x77, 0x61, 0x73,
0xe2, 0x80, 0xa6, 0x04, 0x03, 0x00, 0x31, 0x31, 0x33,
/* adding an app_id because it didnt' come with one */
0x00, 0x01, 0x00, 0x50,
};
//! Complete dict for a messages app notification
//! 00 1a 00 00 00 06 00 00 07 05 00 43 6c 65 61 72 ........ ...Clear
//! 00 13 00 63 6f 6d 2e 61 70 70 6c 65 2e 4d 6f 62 ...com.a pple.Mob
//! 69 6c 65 53 4d 53 01 13 00 46 72 61 6e 63 6f 69 ileSMS.. .Francoi
//! 73 20 42 61 6c 64 61 73 73 61 72 69 02 00 00 03 s Baldas sari....
//! 1b 00 74 68 69 73 20 69 73 20 61 20 74 65 73 74 ..this i s a test
//! 20 6e 6f 74 69 66 69 63 61 74 69 6f 6e 05 0f 00 notific ation...
//! 32 30 31 35 30 36 31 35 54 31 36 31 38 33 37 20150615 T161837
static const uint8_t s_message_dict[] = {
0x00, 0x1a, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x07, 0x05, 0x00, 0x43, 0x6c, 0x65, 0x61, 0x72,
0x00, 0x13, 0x00, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x4d, 0x6f, 0x62,
0x69, 0x6c, 0x65, 0x53, 0x4d, 0x53, 0x01, 0x13, 0x00, 0x46, 0x72, 0x61, 0x6e, 0x63, 0x6f, 0x69,
0x73, 0x20, 0x42, 0x61, 0x6c, 0x64, 0x61, 0x73, 0x73, 0x61, 0x72, 0x69, 0x02, 0x00, 0x00, 0x03,
0x1b, 0x00, 0x74, 0x68, 0x69, 0x73, 0x20, 0x69, 0x73, 0x20, 0x61, 0x20, 0x74, 0x65, 0x73, 0x74,
0x20, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x05, 0x0f, 0x00,
0x32, 0x30, 0x31, 0x35, 0x30, 0x36, 0x31, 0x35, 0x54, 0x31, 0x36, 0x31, 0x38, 0x33, 0x37
};
static TimelineItem s_message_parsed_item = {
.header = {
.id = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
.ancs_uid = 0x1a,
.timestamp = 1434385117,
.duration = 0,
.type = TimelineItemTypeNotification,
.layout = LayoutIdNotification,
.ancs_notif = 1
},
.attr_list = (AttributeList) {
.num_attributes = 6,
.attributes = (Attribute[6]) {
{ .id = AttributeIdSender, .cstring = "Francois Baldassari" },
{ .id = AttributeIdTitle, .cstring = "Francois Baldassari" },
{ .id = AttributeIdBody, .cstring = "this is a test notification" },
{ .id = AttributeIdiOSAppIdentifier, .cstring = "com.apple.MobileSMS" },
{ .id = AttributeIdIconTiny, .uint32 = TIMELINE_RESOURCE_GENERIC_SMS },
{ .id = AttributeIdBgColor, .uint8 = GColorIslamicGreenARGB8 }
}
},
.action_group = (TimelineItemActionGroup) {
.num_actions = 1,
.actions = (TimelineItemAction[1]) {
{
.id = 0,
.type = TimelineItemActionTypeAncsNegative,
.attr_list = (AttributeList) {
.num_attributes = 2,
.attributes = (Attribute[2]) {
{ .id = AttributeIdAncsAction, .uint8 = ActionIDNegative},
{ .id = AttributeIdTitle, .cstring = "Dismiss" }}
}
}
}
}
};
//! Complete dict for the messages app attributes
//! 01 63 6f 6d 2e 61 70 70 6c 65 2e 4d 6f 62 69 6c .com.app le.Mobil
//! 65 53 4d 53 00 00 08 00 4d 65 73 73 61 67 65 73 eSMS.... Messages
static const uint8_t s_message_app_info_dict[] = {
0x01, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x4d, 0x6f, 0x62, 0x69, 0x6c,
0x65, 0x53, 0x4d, 0x53, 0x00, 0x00, 0x08, 0x00, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73
};
//! Complete dict for the unrecognized fake app attributes
//! 01 63 6f 6d 2e 74 65 73 74 73 2e 4e 6f 74 41 6e .com.tes ts.NotAn
//! 41 70 70 00 00 07 00 46 61 6b 65 41 70 70 App....F akeApp
static const uint8_t s_fake_app_info_dict[] = {
0x01, 0x63, 0x6f, 0x6d, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x73, 0x2e, 0x4e, 0x6f, 0x74, 0x41, 0x6e,
0x41, 0x70, 0x70, 0x00, 0x00, 0x07, 0x00, 0x46, 0x61, 0x6b, 0x65, 0x41, 0x70, 0x70,
};
// Complete app dictionary sent by ANCS
// 01 63 6f 6d 2e 67 65 74 70 65 62 62 6c 65 2e 70 .com.get pebble.p
// 65 62 62 6c 65 74 69 6d 65 2e 65 6e 74 65 72 70 ebbletim e.enterp
// 72 69 73 65 00 00 0e 00 50 65 62 62 6c 65 20 54 rise.... Pebble T
// 69 6d 65 20 ce b2 ime ..
static const uint8_t s_app_attr_dict[] = {
0x01, 0x63, 0x6f, 0x6d, 0x2e, 0x67, 0x65, 0x74, 0x70, 0x65, 0x62, 0x62, 0x6c, 0x65, 0x2e, 0x70,
0x65, 0x62, 0x62, 0x6c, 0x65, 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x65, 0x6e, 0x74, 0x65, 0x72, 0x70,
0x72, 0x69, 0x73, 0x65, 0x00, 0x00, 0x0e, 0x00, 0x50, 0x65, 0x62, 0x62, 0x6c, 0x65, 0x20, 0x54,
0x69, 0x6d, 0x65, 0x20, 0xce, 0xb2
};
static const uint8_t s_missing_last_attribute[] = {
0x00, 0x95, 0x00, 0x00, 0x00, 0x00, 0x13, 0x00, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c,
0x65, 0x2e, 0x4d, 0x6f, 0x62, 0x69, 0x6c, 0x65, 0x53, 0x4d, 0x53, 0x01, 0x10, 0x00, 0x72, 0x6a,
0x63, 0x61, 0x73, 0x65, 0x40, 0x67, 0x6d, 0x61, 0x69, 0x6c, 0x2e, 0x63, 0x6f, 0x6d, 0x02, 0x00,
0x00, 0x03, 0x26, 0x00, 0x43, 0x61, 0x72, 0x65, 0x66, 0x75, 0x6c, 0x2c, 0x20, 0x6d, 0x61, 0x6e,
0x2c, 0x20, 0x74, 0x68, 0x65, 0x72, 0x65, 0x27, 0x73, 0x20, 0x61, 0x20, 0x62, 0x65, 0x76, 0x65,
0x72, 0x61, 0x67, 0x65, 0x20, 0x68, 0x65, 0x72, 0x65, 0x21
};
// Length of attribute too long
static const uint8_t s_invalid_attribute_length[] = {
0x00, 0x94, 0x00, 0x00, 0x00, 0x00, 0x13, 0x00, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c,
0x65, 0x2e, 0x4d, 0x6f, 0x62, 0x69, 0x6c, 0x65, 0x53, 0x4d, 0x53, 0x01, 0x29, 0x00, 0x72, 0x6a,
0x63, 0x61, 0x73, 0x65, 0x40, 0x67, 0x6d, 0x61, 0x69, 0x6c, 0x2e, 0x63, 0x6f, 0x6d, 0x20, 0x20,
0x20, 0x20, 0x20, 0x20, 0x43, 0x61, 0x72, 0x65, 0x66, 0x75, 0x6c, 0x2c, 0x20, 0x6d, 0x61, 0x6e,
0x2c, 0x20, 0x74, 0x68, 0x65, 0x72, 0x65, 0x00, 0x73, 0x20, 0x61, 0x20, 0x62, 0x65, 0x76, 0x65,
0x72, 0x61, 0x67, 0x65, 0x20, 0x68, 0x65, 0x72, 0x65, 0x21, 0x02, 0x00, 0x00, 0x03, 0x26, 0x00,
0x43, 0x61, 0x72, 0x65, 0x66, 0x75, 0x6c, 0x2c, 0x20, 0x6d, 0x61, 0x6e, 0x2c, 0x20, 0x74, 0x68,
0x65, 0x72, 0x65, 0x27, 0x73, 0x20, 0x61, 0x20, 0x62, 0x65, 0x76, 0x65, 0x72, 0x61, 0x67, 0x65,
0x20, 0x68, 0x65, 0x72, 0x65, 0x21, 0x05, 0x0f, 0x00, 0x32, 0x30, 0x31, 0x33, 0x31, 0x31, 0x30,
0x35, 0x54, 0x31, 0x38, 0x31, 0x39, 0x32, 0x38
};
// Chunked dictionary 1/2 sent by ANCS (w/header)
// 00 0d 00 00 00 00 1b 00 63 6f 6d 2e 6b 69 63 6b ........ com.kick
// 73 74 61 72 74 65 72 2e 6b 69 63 6b 73 74 61 72 starter. kickstar
// 74 65 72 01 0b 00 4b 69 63 6b 73 74 61 72 74 65 ter...Ki ckstarte
// 72 02 00 00 03 56 00 52 65 73 70 6f 6e 73 65 20 r....V.R esponse
// 6e 65 65 64 65 64 21 20 47 65 74 20 79 6f 75 72 needed! Get your
// 20 72 65 77 61 72 64 20 66 6f 72 20 62 61 63 6b reward for back
// 69 6e 67 20 50 65 62 62 6c 65 3a 20 45 2d 50 61 ing Pebb le: E-Pa
// 70 65 72 79 20 57 61 74 63 68 20 66 6f 72 20 69 pery Wat ch for i
// 4f 53 20 61 6e 64 20 44 72 6f 69 64 2e 05 0f 00 OS and D roid....
// 32 30 31 34 30 36 31 30 54 32 32 20140610 T22
static const uint8_t s_split_timestamp_dict_part_one[] = {
0x00, 0x0d, 0x00, 0x00, 0x00, 0x00, 0x1b, 0x00, 0x63, 0x6f, 0x6d, 0x2e, 0x6b, 0x69, 0x63, 0x6b,
0x73, 0x74, 0x61, 0x72, 0x74, 0x65, 0x72, 0x2e, 0x6b, 0x69, 0x63, 0x6b, 0x73, 0x74, 0x61, 0x72,
0x74, 0x65, 0x72, 0x01, 0x0b, 0x00, 0x4b, 0x69, 0x63, 0x6b, 0x73, 0x74, 0x61, 0x72, 0x74, 0x65,
0x72, 0x02, 0x00, 0x00, 0x03, 0x56, 0x00, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x20,
0x6e, 0x65, 0x65, 0x64, 0x65, 0x64, 0x21, 0x20, 0x47, 0x65, 0x74, 0x20, 0x79, 0x6f, 0x75, 0x72,
0x20, 0x72, 0x65, 0x77, 0x61, 0x72, 0x64, 0x20, 0x66, 0x6f, 0x72, 0x20, 0x62, 0x61, 0x63, 0x6b,
0x69, 0x6e, 0x67, 0x20, 0x50, 0x65, 0x62, 0x62, 0x6c, 0x65, 0x3a, 0x20, 0x45, 0x2d, 0x50, 0x61,
0x70, 0x65, 0x72, 0x79, 0x20, 0x57, 0x61, 0x74, 0x63, 0x68, 0x20, 0x66, 0x6f, 0x72, 0x20, 0x69,
0x4f, 0x53, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x44, 0x72, 0x6f, 0x69, 0x64, 0x2e, 0x05, 0x0f, 0x00,
0x32, 0x30, 0x31, 0x34, 0x30, 0x36, 0x31, 0x30, 0x54, 0x32, 0x32
};
// Chunked dictionary 2/2 sent by ANCS
// 32 30 34 38 2048
static const uint8_t s_split_timestamp_dict_part_two[] = {
0x32, 0x30, 0x34, 0x38
};
static const uint8_t s_invalid_attribute_length_fixed[] = {
0x00, 0x94, 0x00, 0x00, 0x00, 0x00, 0x13, 0x00, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c,
0x65, 0x2e, 0x4d, 0x6f, 0x62, 0x69, 0x6c, 0x65, 0x53, 0x4d, 0x53, 0x01, 0x10, 0x00, 0x72, 0x6a,
0x63, 0x61, 0x73, 0x65, 0x40, 0x67, 0x6d, 0x61, 0x69, 0x6c, 0x2e, 0x63, 0x6f, 0x6d, 0x02, 0x00,
0x00, 0x03, 0x26, 0x00, 0x43, 0x61, 0x72, 0x65, 0x66, 0x75, 0x6c, 0x2c, 0x20, 0x6d, 0x61, 0x6e,
0x2c, 0x20, 0x74, 0x68, 0x65, 0x72, 0x65, 0x27, 0x73, 0x20, 0x61, 0x20, 0x62, 0x65, 0x76, 0x65,
0x72, 0x61, 0x67, 0x65, 0x20, 0x68, 0x65, 0x72, 0x65, 0x21, 0x05, 0x0f, 0x00, 0x32, 0x30, 0x31,
0x33, 0x31, 0x31, 0x30, 0x35, 0x54, 0x31, 0x38, 0x31, 0x39, 0x32, 0x38
};
// 00 22 00 00 00 07 07 00 44 69 73 6d 69 73 73 00 ."...... Dismiss.
// 14 00 63 6f 6d 2e 61 70 70 6c 65 2e 6d 6f 62 69 ..com.ap ple.mobi
// 6c 65 6d 61 69 6c 01 15 00 41 61 72 6f 6e 20 48 lemail.. .Aaron H
// 65 63 6b 6d 61 6e 6e 20 28 4a 49 52 41 29 02 28 eckmann (JIRA).(
// 00 5b 4a 49 52 41 5d 20 28 50 42 4c 2d 38 37 37 .[JIRA] (PBL-877
// 36 29 20 45 72 72 6f 72 20 73 63 72 65 65 6e 20 6) Error screen
// 61 66 74 65 72 20 6c 6f 67 03 1c 00 54 68 69 73 after lo g...This
// 20 6d 65 73 73 61 67 65 20 68 61 73 20 6e 6f 20 message has no
// 63 6f 6e 74 65 6e 74 2e 05 0f 00 32 30 31 34 30 content. ...20140
// 39 32 36 54 31 30 34 33 30 30 926T1043 00
static const uint8_t s_this_message_has_no_content_response[] = {
0x00, 0x22, 0x00, 0x00, 0x00, 0x07, 0x07, 0x00, 0x44, 0x69, 0x73, 0x6d, 0x69, 0x73, 0x73, 0x00,
0x14, 0x00, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x6d, 0x6f, 0x62, 0x69,
0x6c, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x01, 0x15, 0x00, 0x41, 0x61, 0x72, 0x6f, 0x6e, 0x20, 0x48,
0x65, 0x63, 0x6b, 0x6d, 0x61, 0x6e, 0x6e, 0x20, 0x28, 0x4a, 0x49, 0x52, 0x41, 0x29, 0x02, 0x28,
0x00, 0x5b, 0x4a, 0x49, 0x52, 0x41, 0x5d, 0x20, 0x28, 0x50, 0x42, 0x4c, 0x2d, 0x38, 0x37, 0x37,
0x36, 0x29, 0x20, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x20, 0x73, 0x63, 0x72, 0x65, 0x65, 0x6e, 0x20,
0x61, 0x66, 0x74, 0x65, 0x72, 0x20, 0x6c, 0x6f, 0x67, 0x03, 0x1c, 0x00, 0x54, 0x68, 0x69, 0x73,
0x20, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x20, 0x68, 0x61, 0x73, 0x20, 0x6e, 0x6f, 0x20,
0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x2e, 0x05, 0x0f, 0x00, 0x32, 0x30, 0x31, 0x34, 0x30,
0x39, 0x32, 0x36, 0x54, 0x31, 0x30, 0x34, 0x33, 0x30, 0x30
};
// 00 19 00 00 00 07 07 00 44 69 73 6d 69 73 73 00 ........ Dismiss.
// 14 00 63 6f 6d 2e 61 70 70 6c 65 2e 6d 6f 62 69 ..com.ap ple.mobi
// 6c 65 6d 61 69 6c 01 05 00 4d 61 72 74 79 02 04 lemail.. .Marty..
// 00 74 65 73 74 03 0a 00 4c 6f 61 64 69 6e 67 e2 .test... Loading.
// 80 a6 05 0f 00 32 30 31 34 30 39 32 39 54 31 35 .....201 40929T15
// 31 35 31 30 1510
static const uint8_t s_loading_response[] = {
0x00, 0x19, 0x00, 0x00, 0x00, 0x07, 0x07, 0x00, 0x44, 0x69, 0x73, 0x6d, 0x69, 0x73, 0x73, 0x00,
0x14, 0x00, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x6d, 0x6f, 0x62, 0x69,
0x6c, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x01, 0x05, 0x00, 0x4d, 0x61, 0x72, 0x74, 0x79, 0x02, 0x04,
0x00, 0x74, 0x65, 0x73, 0x74, 0x03, 0x0a, 0x00, 0x4c, 0x6f, 0x61, 0x64, 0x69, 0x6e, 0x67, 0xe2,
0x80, 0xa6, 0x05, 0x0f, 0x00, 0x32, 0x30, 0x31, 0x34, 0x30, 0x39, 0x32, 0x39, 0x54, 0x31, 0x35,
0x31, 0x35, 0x31, 0x30,
};
// 00 aa 00 00 00 07 07 00 44 69 73 6d 69 73 73 00 ........ Dismiss.
// 14 00 63 6f 6d 2e 61 70 70 6c 65 2e 6d 6f 62 69 ..com.ap ple.mobi
// 6c 65 6d 61 69 6c 01 16 00 63 61 6c 65 6e 64 61 lemail.. .calenda
// 72 40 63 73 61 69 6c 2e 6d 69 74 2e 65 64 75 02 r@csail. mit.edu.
// 28 00 54 41 4c 4b 3a 20 54 75 65 73 64 61 79 20 (.TALK: Tuesday
// 31 30 2d 32 38 2d 32 30 31 34 20 53 63 69 65 6e 10-28-20 14 Scien
// 74 69 66 69 63 20 55 74 6f 70 03 c8 00 53 63 69 tific Ut op...Sci
// 65 6e 74 69 66 69 63 20 55 74 6f 70 69 61 3a 20 entific Utopia:
// 49 6d 70 72 6f 76 69 6e 67 20 4f 70 65 6e 6e 65 Improvin g Openne
// 73 73 20 61 6e 64 20 52 65 70 72 6f 64 75 63 69 ss and R eproduci
// 62 69 6c 69 74 79 20 69 6e 20 53 63 69 65 6e 74 bility i n Scient
// 69 66 69 63 20 52 65 73 65 61 72 63 68 20 53 70 ific Res earch Sp
// 65 61 6b 65 72 3a 20 42 72 69 61 6e 20 4e 6f 73 eaker: B rian Nos
// 65 6b 2c 20 55 6e 69 76 65 72 73 69 74 79 20 6f ek, Univ ersity o
// 66 20 56 69 72 67 69 6e 69 61 20 61 6e 64 20 43 f Virgin ia and C
// 65 6e 74 65 72 20 66 6f 72 20 4f 70 65 6e 20 53 enter fo r Open S
// 63 69 65 6e 63 65 20 53 70 65 61 6b 65 72 20 41 cience S peaker A
// 66 66 69 6c 69 61 74 69 6f 6e 3a 20 55 6e 69 76 ffiliati on: Univ
// 65 72 73 69 74 79 20 6f 66 20 56 69 72 67 69 6e ersity o f Virgin
// 69 61 20 48 6f 05 ia Ho.
typedef struct PACKED s_sample_memory_layout {
const uint8_t attribute_data[310];
const uint8_t out_of_bounds_memory[6];
} s_sample_memory_layout;
s_sample_memory_layout memory_with_attribute_id_at_end = {
.attribute_data = {
0x00, 0xaa, 0x00, 0x00, 0x00, 0x07, 0x07, 0x00, 0x44, 0x69, 0x73, 0x6d, 0x69, 0x73, 0x73, 0x00,
0x14, 0x00, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x6d, 0x6f, 0x62, 0x69,
0x6c, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x01, 0x16, 0x00, 0x63, 0x61, 0x6c, 0x65, 0x6e, 0x64, 0x61,
0x72, 0x40, 0x63, 0x73, 0x61, 0x69, 0x6c, 0x2e, 0x6d, 0x69, 0x74, 0x2e, 0x65, 0x64, 0x75, 0x02,
0x28, 0x00, 0x54, 0x41, 0x4c, 0x4b, 0x3a, 0x20, 0x54, 0x75, 0x65, 0x73, 0x64, 0x61, 0x79, 0x20,
0x31, 0x30, 0x2d, 0x32, 0x38, 0x2d, 0x32, 0x30, 0x31, 0x34, 0x20, 0x53, 0x63, 0x69, 0x65, 0x6e,
0x74, 0x69, 0x66, 0x69, 0x63, 0x20, 0x55, 0x74, 0x6f, 0x70, 0x03, 0xc8, 0x00, 0x53, 0x63, 0x69,
0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x63, 0x20, 0x55, 0x74, 0x6f, 0x70, 0x69, 0x61, 0x3a, 0x20,
0x49, 0x6d, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x6e, 0x67, 0x20, 0x4f, 0x70, 0x65, 0x6e, 0x6e, 0x65,
0x73, 0x73, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x52, 0x65, 0x70, 0x72, 0x6f, 0x64, 0x75, 0x63, 0x69,
0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x20, 0x69, 0x6e, 0x20, 0x53, 0x63, 0x69, 0x65, 0x6e, 0x74,
0x69, 0x66, 0x69, 0x63, 0x20, 0x52, 0x65, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x20, 0x53, 0x70,
0x65, 0x61, 0x6b, 0x65, 0x72, 0x3a, 0x20, 0x42, 0x72, 0x69, 0x61, 0x6e, 0x20, 0x4e, 0x6f, 0x73,
0x65, 0x6b, 0x2c, 0x20, 0x55, 0x6e, 0x69, 0x76, 0x65, 0x72, 0x73, 0x69, 0x74, 0x79, 0x20, 0x6f,
0x66, 0x20, 0x56, 0x69, 0x72, 0x67, 0x69, 0x6e, 0x69, 0x61, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x43,
0x65, 0x6e, 0x74, 0x65, 0x72, 0x20, 0x66, 0x6f, 0x72, 0x20, 0x4f, 0x70, 0x65, 0x6e, 0x20, 0x53,
0x63, 0x69, 0x65, 0x6e, 0x63, 0x65, 0x20, 0x53, 0x70, 0x65, 0x61, 0x6b, 0x65, 0x72, 0x20, 0x41,
0x66, 0x66, 0x69, 0x6c, 0x69, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x3a, 0x20, 0x55, 0x6e, 0x69, 0x76,
0x65, 0x72, 0x73, 0x69, 0x74, 0x79, 0x20, 0x6f, 0x66, 0x20, 0x56, 0x69, 0x72, 0x67, 0x69, 0x6e,
0x69, 0x61, 0x20, 0x48, 0x6f, 0x05,
},
.out_of_bounds_memory = {
0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
},
};
static const uint8_t memory_with_attribute_id_at_end_p2[] = {
0x0f, 0x00, 0x32, 0x30, 0x31, 0x33, 0x31, 0x31, 0x30, 0x35, 0x54, 0x31, 0x38, 0x31, 0x39, 0x32, 0x38
};
// 00 74 00 00 00 06 06 00 41 6e 73 77 65 72 07 07 .t...... Answer..
// 00 44 65 63 6c 69 6e 65 00 15 00 63 6f 6d 2e 61 .Decline ...com.a
// 70 70 6c 65 2e 6d 6f 62 69 6c 65 70 68 6f 6e 65 pple.mob ilephone
// 01 03 00 49 6d 6d 02 06 00 6d 6f 62 69 6c 65 03 ...Imm.. .mobile.
// 0d 00 49 6e 63 6f 6d 69 6e 67 20 43 61 6c 6c 05 ..Incomi ng Call.
// 00 00 00 75 00 00 00 07 07 00 44 69 73 6d 69 73 ...u.... ..Dismis
// 73 00 13 00 63 6f 6d 2e 61 70 70 6c 65 2e 6d 6f s...com. apple.mo
// 62 69 6c 65 63 61 6c 01 11 00 48 61 69 6c 65 79 bilecal. ..Hailey
// 27 73 20 70 72 61 63 74 69 63 65 02 00 00 03 0a 's pract ice.....
// 00 52 65 64 20 4d 6f 72 74 6f 6e 05 0f 00 32 30 .Red Mor ton...20
// 31 34 31 30 32 38 54 31 36 30 30 30 30 00 76 00 141028T1 60000.v.
// 00 00 06 06 00 41 6e 73 77 65 72 07 07 00 44 65 .....Ans wer...De
// 63 6c 69 6e 65 00 15 00 63 6f 6d 2e 61 70 70 6c cline... com.appl
// 65 2e 6d 6f 62 69 6c 65 70 68 6f 6e 58 01 03 00 e.mobile phonX... //phone -> phonX for testing
// 49 6d 6d 02 06 00 6d 6f 62 69 6c 65 03 0d 00 49 Imm...mo bile...I
// 6e 63 6f 6d 69 6e 67 20 43 61 6c 6c 05 00 00 ncoming Call...
static const uint8_t s_multiple_complete_dicts[] = {
// Dict 1
0x00, 0x74, 0x00, 0x00, 0x00, 0x06, 0x06, 0x00, 0x41, 0x6e, 0x73, 0x77, 0x65, 0x72, 0x07, 0x07,
0x00, 0x44, 0x65, 0x63, 0x6c, 0x69, 0x6e, 0x65, 0x00, 0x15, 0x00, 0x63, 0x6f, 0x6d, 0x2e, 0x61,
0x70, 0x70, 0x6c, 0x65, 0x2e, 0x6d, 0x6f, 0x62, 0x69, 0x6c, 0x65, 0x70, 0x68, 0x6f, 0x6e, 0x65,
0x01, 0x03, 0x00, 0x49, 0x6d, 0x6d, 0x02, 0x06, 0x00, 0x6d, 0x6f, 0x62, 0x69, 0x6c, 0x65, 0x03,
0x0d, 0x00, 0x49, 0x6e, 0x63, 0x6f, 0x6d, 0x69, 0x6e, 0x67, 0x20, 0x43, 0x61, 0x6c, 0x6c, 0x05,
0x00, 0x00,
// Dict 2
0x00, 0x75, 0x00, 0x00, 0x00, 0x07, 0x07, 0x00, 0x44, 0x69, 0x73, 0x6d, 0x69, 0x73,
0x73, 0x00, 0x13, 0x00, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x6d, 0x6f,
0x62, 0x69, 0x6c, 0x65, 0x63, 0x61, 0x6c, 0x01, 0x11, 0x00, 0x48, 0x61, 0x69, 0x6c, 0x65, 0x79,
0x27, 0x73, 0x20, 0x70, 0x72, 0x61, 0x63, 0x74, 0x69, 0x63, 0x65, 0x02, 0x00, 0x00, 0x03, 0x0a,
0x00, 0x52, 0x65, 0x64, 0x20, 0x4d, 0x6f, 0x72, 0x74, 0x6f, 0x6e, 0x05, 0x0f, 0x00, 0x32, 0x30,
0x31, 0x34, 0x31, 0x30, 0x32, 0x38, 0x54, 0x31, 0x36, 0x30, 0x30, 0x30, 0x30,
//Dict 3
0x00, 0x76, 0x00,
0x00, 0x00, 0x06, 0x06, 0x00, 0x41, 0x6e, 0x73, 0x77, 0x65, 0x72, 0x07, 0x07, 0x00, 0x44, 0x65,
0x63, 0x6c, 0x69, 0x6e, 0x65, 0x00, 0x15, 0x00, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c,
0x65, 0x2e, 0x6d, 0x6f, 0x62, 0x69, 0x6c, 0x65, 0x70, 0x68, 0x6f, 0x6e, 0x64, 0x01, 0x03, 0x00,
0x49, 0x6d, 0x6d, 0x02, 0x06, 0x00, 0x6d, 0x6f, 0x62, 0x69, 0x6c, 0x65, 0x03, 0x0d, 0x00, 0x49,
0x6e, 0x63, 0x6f, 0x6d, 0x69, 0x6e, 0x67, 0x20, 0x43, 0x61, 0x6c, 0x6c, 0x05, 0x00, 0x00
};
//! Complete dict for a messages app notification with a duplicated title
//! 00 1b 00 00 00 06 00 00 07 05 00 43 6c 65 61 72 ........ ...Clear
//! 00 13 00 63 6f 6d 2e 61 70 70 6c 65 2e 4d 6f 62 ...com.a pple.Mob
//! 69 6c 65 53 4d 53 01 08 00 4D 65 73 73 61 67 65 ileSMS.. .Message
//! 73 02 00 00 03 1b 00 74 68 69 73 20 69 73 20 61 s......t his is a
//! 20 74 65 73 74 20 6e 6f 74 69 66 69 63 61 74 69 test no tificati
//! 6f 6e 05 0f 00 32 30 31 35 30 36 31 35 54 31 36 on...201 50615T16
//! 31 38 33 37 1837
static const uint8_t s_app_name_title_dict[] = {
0x00, 0x1b, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x07, 0x05, 0x00, 0x43, 0x6c, 0x65, 0x61, 0x72,
0x00, 0x13, 0x00, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x4d, 0x6f, 0x62,
0x69, 0x6c, 0x65, 0x53, 0x4d, 0x53, 0x01, 0x08, 0x00, 0x4D, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65,
0x73, 0x02, 0x00, 0x00, 0x03, 0x1b, 0x00, 0x74, 0x68, 0x69, 0x73, 0x20, 0x69, 0x73, 0x20, 0x61,
0x20, 0x74, 0x65, 0x73, 0x74, 0x20, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69,
0x6f, 0x6e, 0x05, 0x0f, 0x00, 0x32, 0x30, 0x31, 0x35, 0x30, 0x36, 0x31, 0x35, 0x54, 0x31, 0x36,
0x31, 0x38, 0x33, 0x37
};
static TimelineItem s_app_name_title_parsed_item = {
.header = {
.id = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
.ancs_uid = 0x1b,
.timestamp = 1434385117,
.duration = 0,
.type = TimelineItemTypeNotification,
.layout = LayoutIdNotification,
.ancs_notif = 1
},
.attr_list = (AttributeList) {
.num_attributes = 4,
.attributes = (Attribute[4]) {
{ .id = AttributeIdBody, .cstring = "this is a test notification" },
{ .id = AttributeIdiOSAppIdentifier, .cstring = "com.apple.MobileSMS" },
{ .id = AttributeIdIconTiny, .uint32 = TIMELINE_RESOURCE_GENERIC_SMS },
{ .id = AttributeIdBgColor, .uint8 = GColorIslamicGreenARGB8 },
}
},
.action_group = (TimelineItemActionGroup) {
.num_actions = 1,
.actions = (TimelineItemAction[1]) {
{
.id = 0,
.type = TimelineItemActionTypeAncsNegative,
.attr_list = (AttributeList) {
.num_attributes = 2,
.attributes = (Attribute[2]) {
{ .id = AttributeIdAncsAction, .uint8 = ActionIDNegative},
{ .id = AttributeIdTitle, .cstring = "Dismiss" }}
}
}
}
}
};
//! Complete dict for a notification from an unrecognized app with a duplicated title
//! 00 1c 00 00 00 06 04 00 4c 69 6b 65 07 05 00 43 ........ Like...C
//! 6c 65 61 72 00 12 00 63 6f 6d 2e 74 65 73 74 73 lear...com.t ests
//! 2e 4e 6f 74 41 6e 41 70 70 01 07 00 46 61 6b 65 .NotAnApp... Fake
//! 41 70 70 02 00 00 03 1b 00 74 68 69 73 20 69 73 App......thi s is
//! 20 61 20 74 65 73 74 20 6e 6f 74 69 66 69 63 61 a test noti fica
//! 74 69 6f 6e 05 0f 00 32 30 31 35 30 36 31 35 54 tion...20150 615T
//! 31 36 31 38 33 37 161837
static const uint8_t s_unknown_app_dict[] = {
0x00, 0x1c, 0x00, 0x00, 0x00, 0x06, 0x04, 0x00, 0x4c, 0x69, 0x6b, 0x65, 0x07, 0x05, 0x00, 0x43,
0x6c, 0x65, 0x61, 0x72, 0x00, 0x12, 0x00, 0x63, 0x6f, 0x6d, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x73,
0x2e, 0x4e, 0x6f, 0x74, 0x41, 0x6e, 0x41, 0x70, 0x70, 0x01, 0x07, 0x00, 0x46, 0x61, 0x6b, 0x65,
0x41, 0x70, 0x70, 0x02, 0x00, 0x00, 0x03, 0x1b, 0x00, 0x74, 0x68, 0x69, 0x73, 0x20, 0x69, 0x73,
0x20, 0x61, 0x20, 0x74, 0x65, 0x73, 0x74, 0x20, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61,
0x74, 0x69, 0x6f, 0x6e, 0x05, 0x0f, 0x00, 0x32, 0x30, 0x31, 0x35, 0x30, 0x36, 0x31, 0x35, 0x54,
0x31, 0x36, 0x31, 0x38, 0x33, 0x37
};
static TimelineItem s_unknown_app_parsed_item = {
.header = {
.id = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
.ancs_uid = 0x1c,
.timestamp = 1434385117,
.duration = 0,
.type = TimelineItemTypeNotification,
.layout = LayoutIdNotification,
.ancs_notif = 1
},
.attr_list = (AttributeList) {
.num_attributes = 4,
.attributes = (Attribute[4]) {
{ .id = AttributeIdAppName, .cstring = "FakeApp" },
{ .id = AttributeIdBody, .cstring = "this is a test notification" },
{ .id = AttributeIdiOSAppIdentifier, .cstring = "com.tests.NotAnApp" },
{ .id = AttributeIdIconTiny, .uint32 = TIMELINE_RESOURCE_NOTIFICATION_GENERIC },
}
},
.action_group = (TimelineItemActionGroup) {
.num_actions = 2,
.actions = (TimelineItemAction[2]) {
{
.id = 0,
.type = TimelineItemActionTypeAncsPositive,
.attr_list = (AttributeList) {
.num_attributes = 2,
.attributes = (Attribute[2]) {
{ .id = AttributeIdAncsAction, .uint8 = ActionIDPositive},
{ .id = AttributeIdTitle, .cstring = "Like" }}
}
},
{
.id = 0,
.type = TimelineItemActionTypeAncsNegative,
.attr_list = (AttributeList) {
.num_attributes = 2,
.attributes = (Attribute[2]) {
{ .id = AttributeIdAncsAction, .uint8 = ActionIDNegative},
{ .id = AttributeIdTitle, .cstring = "Dismiss" }}
}
}
}
}
};
//! Complete dict for a notification from an unrecognized app with a unique title
//! 00 1d 00 00 00 06 00 00 07 05 00 43 6c 65 61 72 ........ ...Clear
//! 00 12 00 63 6f 6d 2e 74 65 73 74 73 2e 4e 6f 74 ...com.t ests.Not
//! 41 6e 41 70 70 01 0d 00 4d 65 73 73 61 67 65 20 AnApp... Message
//! 54 69 74 6c 65 02 00 00 03 1b 00 74 68 69 73 20 Title... ...this
//! 69 73 20 61 20 74 65 73 74 20 6e 6f 74 69 66 69 is a tes t notifi
//! 63 61 74 69 6f 6e 05 0f 00 32 30 31 35 30 36 31 cation.. .2015061
//! 35 54 31 36 31 38 33 37 5T161837
static const uint8_t s_unknown_app_unique_title_dict[] = {
0x00, 0x1d, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x07, 0x05, 0x00, 0x43, 0x6c, 0x65, 0x61, 0x72,
0x00, 0x12, 0x00, 0x63, 0x6f, 0x6d, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x73, 0x2e, 0x4e, 0x6f, 0x74,
0x41, 0x6e, 0x41, 0x70, 0x70, 0x01, 0x0d, 0x00, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x20,
0x54, 0x69, 0x74, 0x6c, 0x65, 0x02, 0x00, 0x00, 0x03, 0x1b, 0x00, 0x74, 0x68, 0x69, 0x73, 0x20,
0x69, 0x73, 0x20, 0x61, 0x20, 0x74, 0x65, 0x73, 0x74, 0x20, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69,
0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x05, 0x0f, 0x00, 0x32, 0x30, 0x31, 0x35, 0x30, 0x36, 0x31,
0x35, 0x54, 0x31, 0x36, 0x31, 0x38, 0x33, 0x37
};
static TimelineItem s_unknown_app_unique_title_parsed_item = {
.header = {
.id = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
.ancs_uid = 0x1d,
.timestamp = 1434385117,
.duration = 0,
.type = TimelineItemTypeNotification,
.layout = LayoutIdNotification,
.ancs_notif = 1
},
.attr_list = (AttributeList) {
.num_attributes = 5,
.attributes = (Attribute[5]) {
{ .id = AttributeIdTitle, .cstring = "Message Title" },
{ .id = AttributeIdAppName, .cstring = "FakeApp" },
{ .id = AttributeIdBody, .cstring = "this is a test notification" },
{ .id = AttributeIdiOSAppIdentifier, .cstring = "com.tests.NotAnApp" },
{ .id = AttributeIdIconTiny, .uint32 = TIMELINE_RESOURCE_NOTIFICATION_GENERIC },
}
},
.action_group = (TimelineItemActionGroup) {
.num_actions = 1,
.actions = (TimelineItemAction[1]) {
{
.id = 0,
.type = TimelineItemActionTypeAncsNegative,
.attr_list = (AttributeList) {
.num_attributes = 2,
.attributes = (Attribute[2]) {
{ .id = AttributeIdAncsAction, .uint8 = ActionIDNegative},
{ .id = AttributeIdTitle, .cstring = "Dismiss" }}
}
}
}
}
};
//! Complete dict for an MMS with an image
//! 00 1f 00 00 00 06 00 00 07 05 00 43 6c 65 61 72 .4.........Clear
//! 00 13 00 63 6f 6d 2e 61 70 70 6c 65 2e 4d 6f 62 ...com.apple.Mob
//! 69 6c 65 53 4d 53 01 08 00 48 75 79 20 54 72 61 ileSMS...Huy Tra
//! 6e 02 00 00 03 00 00 05 0f 00 32 30 31 36 30 31 n.........201601
//! 31 31 54 31 38 34 38 35 32 11T184852
static const uint8_t s_mms_no_caption_dict[] = {
0x00, 0x1f, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x07, 0x05, 0x00, 0x43, 0x6c, 0x65, 0x61, 0x72,
0x00, 0x13, 0x00, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x4d, 0x6f, 0x62,
0x69, 0x6c, 0x65, 0x53, 0x4d, 0x53, 0x01, 0x08, 0x00, 0x48, 0x75, 0x79, 0x20, 0x54, 0x72, 0x61,
0x6e, 0x02, 0x00, 0x00, 0x03, 0x00, 0x00, 0x05, 0x0f, 0x00, 0x32, 0x30, 0x31, 0x36, 0x30, 0x31,
0x31, 0x31, 0x54, 0x31, 0x38, 0x34, 0x38, 0x35, 0x32
};
static TimelineItem s_mms_no_caption_parsed_item = {
.header = {
.id = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
.ancs_uid = 0x1f,
.timestamp = 1452538132,
.duration = 0,
.type = TimelineItemTypeNotification,
.layout = LayoutIdNotification,
.ancs_notif = 1
},
.attr_list = (AttributeList) {
.num_attributes = 5,
.attributes = (Attribute[5]) {
{ .id = AttributeIdSender, .cstring = "Huy Tran" },
{ .id = AttributeIdBody, .cstring = "Huy Tran sent an attachment 🎁" },
{ .id = AttributeIdiOSAppIdentifier, .cstring = "com.apple.MobileSMS" },
{ .id = AttributeIdIconTiny, .uint32 = TIMELINE_RESOURCE_GENERIC_SMS },
{ .id = AttributeIdBgColor, .uint8 = GColorIslamicGreenARGB8 }
}
},
.action_group = (TimelineItemActionGroup) {
.num_actions = 1,
.actions = (TimelineItemAction[1]) {
{
.id = 0,
.type = TimelineItemActionTypeAncsNegative,
.attr_list = (AttributeList) {
.num_attributes = 2,
.attributes = (Attribute[2]) {
{ .id = AttributeIdAncsAction, .uint8 = ActionIDNegative },
{ .id = AttributeIdTitle, .cstring = "Dismiss" }
}
}
}
}
}
};
//! Complete dict for an MMS with an image and caption
//! 00 20 00 00 00 06 00 00 07 05 00 43 6c 65 61 72 .'...... ...Clear
//! 00 13 00 63 6f 6d 2e 61 70 70 6c 65 2e 4d 6f 62 ...com.a pple.Mob
//! 69 6c 65 53 4d 53 01 08 00 48 75 79 20 54 72 61 ileSMS.. .Huy Tra
//! 6e 02 00 00 03 10 00 20 43 68 65 63 6b 20 74 68 n...... Check th
//! 69 73 20 6f 75 74 21 05 0f 00 32 30 31 36 30 31 is out!. ..201601
//! 31 31 54 32 31 30 34 31 35 11T21041 5
static const uint8_t s_mms_with_caption_dict[] = {
0x00, 0x20, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x07, 0x05, 0x00, 0x43, 0x6c, 0x65, 0x61, 0x72,
0x00, 0x13, 0x00, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x4d, 0x6f, 0x62,
0x69, 0x6c, 0x65, 0x53, 0x4d, 0x53, 0x01, 0x08, 0x00, 0x48, 0x75, 0x79, 0x20, 0x54, 0x72, 0x61,
0x6e, 0x02, 0x00, 0x00, 0x03, 0x10, 0x00, 0x20, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x20, 0x74, 0x68,
0x69, 0x73, 0x20, 0x6f, 0x75, 0x74, 0x21, 0x05, 0x0f, 0x00, 0x32, 0x30, 0x31, 0x36, 0x30, 0x31,
0x31, 0x31, 0x54, 0x32, 0x31, 0x30, 0x34, 0x31, 0x35
};
static TimelineItem s_mms_with_caption_parsed_item = {
.header = {
.id = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
.ancs_uid = 0x20,
.timestamp = 1452546255,
.duration = 0,
.type = TimelineItemTypeNotification,
.layout = LayoutIdNotification,
.ancs_notif = 1
},
.attr_list = (AttributeList) {
.num_attributes = 5,
.attributes = (Attribute[5]) {
{ .id = AttributeIdSender, .cstring = "Huy Tran" },
{ .id = AttributeIdBody, .cstring = "Huy Tran sent an attachment 🎁\n\"Check this out!\"" },
{ .id = AttributeIdiOSAppIdentifier, .cstring = "com.apple.MobileSMS" },
{ .id = AttributeIdIconTiny, .uint32 = TIMELINE_RESOURCE_GENERIC_SMS },
{ .id = AttributeIdBgColor, .uint8 = GColorIslamicGreenARGB8 }
}
},
.action_group = (TimelineItemActionGroup) {
.num_actions = 1,
.actions = (TimelineItemAction[1]) {
{
.id = 0,
.type = TimelineItemActionTypeAncsNegative,
.attr_list = (AttributeList) {
.num_attributes = 2,
.attributes = (Attribute[2]) {
{ .id = AttributeIdAncsAction, .uint8 = ActionIDNegative },
{ .id = AttributeIdTitle, .cstring = "Dismiss" }
}
}
}
}
}
};
+720
View File
@@ -0,0 +1,720 @@
/*
* 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 "comm/ble/kernel_le_client/ams/ams.h"
#include "comm/bt_conn_mgr.h"
#include "services/normal/music.h"
#include "services/normal/music_internal.h"
#include "clar.h"
// Stubs & Fakes
///////////////////////////////////////////////////////////
#include "fake_events.h"
#include "fake_gatt_client_operations.h"
#include "fake_gatt_client_subscriptions.h"
#include "fake_pebble_tasks.h"
#include "fake_rtc.h"
#include "stubs_app_install_manager.h"
#include "stubs_app_manager.h"
#include "stubs_bt_lock.h"
#include "stubs_hexdump.h"
#include "stubs_logging.h"
#include "stubs_mutex.h"
#include "stubs_passert.h"
#include "stubs_pbl_malloc.h"
#include "stubs_serial.h"
#include "stubs_tick.h"
void analytics_event_ams(uint8_t type, int32_t aux_info) {
}
struct {
ResponseTimeState state;
uint16_t max_period_secs;
} s_conn_mgr_states[NumBtConsumer];
void conn_mgr_set_ble_conn_response_time(GAPLEConnection *hdl, BtConsumer consumer,
ResponseTimeState state, uint16_t max_period_secs) {
s_conn_mgr_states[consumer].state = state;
s_conn_mgr_states[consumer].max_period_secs = max_period_secs;
}
GAPLEConnection *gap_le_connection_by_device(const BTDeviceInternal *device) {
return NULL;
}
BTDeviceInternal gatt_client_characteristic_get_device(BLECharacteristic characteristic_ref) {
return (BTDeviceInternal) {
.address.octets = {
0x11, 0x22, 0x33, 0x44, 0x55, 0x66,
},
};
}
static void (*s_launcher_task_callback)(void *data);
static void *s_launcher_task_callback_data;
void launcher_task_add_callback(void (*callback)(void *data), void *data) {
// Simple fake, can only handle one call
cl_assert_equal_p(s_launcher_task_callback, NULL);
s_launcher_task_callback = callback;
s_launcher_task_callback_data = data;
}
// Tests: Disover AMS
///////////////////////////////////////////////////////////
#define NUM_AMS_INSTANCES 2
static BLECharacteristic s_characteristics[NUM_AMS_INSTANCES][NumAMSCharacteristic] = {
// AMS instance one:
[0] = {
[AMSCharacteristicRemoteCommand] = 1,
[AMSCharacteristicEntityUpdate] = 2,
[AMSCharacteristicEntityAttribute] = 3,
},
// AMS instance two:
[1] = {
[AMSCharacteristicRemoteCommand] = 4,
[AMSCharacteristicEntityUpdate] = 5,
[AMSCharacteristicEntityAttribute] = 6,
},
};
static const BLECharacteristic s_unknown_characteristic = 999;
static void prv_assert_can_handle_characteristics(uint32_t instance_idx, bool expect_can_handle) {
for (AMSCharacteristic c = 0; c < NumAMSCharacteristic; ++c) {
cl_assert_equal_b(ams_can_handle_characteristic(s_characteristics[instance_idx][c]),
expect_can_handle);
}
}
static void prv_discover_ams(uint8_t num_instances) {
cl_assert(num_instances <= 2);
if (num_instances) {
for (int i = 0; i < num_instances && i < NUM_AMS_INSTANCES; i++) {
ams_handle_service_discovered(&s_characteristics[i][0]);
}
} else {
ams_invalidate_all_references();
}
}
void test_ams__cannot_handle_any_characteristic_after_destroy(void) {
prv_discover_ams(1);
ams_destroy();
prv_assert_can_handle_characteristics(0, false /* expect_can_handle */);
}
void test_ams__cannot_handle_unknown_characteristic(void) {
prv_discover_ams(1);
cl_assert_equal_b(ams_can_handle_characteristic(s_unknown_characteristic), false);
}
void test_ams__discover_of_ams_should_subscribe_to_entity_update_characteristic(void) {
// Pass in 2 instances, it should be able to cope with this
prv_discover_ams(2 /* num_instances */);
// Assert ams.c can now handle the characteristic reference for the first instance:
prv_assert_can_handle_characteristics(0, true /* expect_can_handle */);
// Assert ams.c can not handle the characteristic reference for the second instance:
prv_assert_can_handle_characteristics(1, false /* expect_can_handle */);
// The first instance is expected to be used.
BLECharacteristic entity_update = s_characteristics[0][AMSCharacteristicEntityUpdate];
fake_gatt_client_subscriptions_assert_subscribe(entity_update,
BLESubscriptionNotifications, GAPLEClientKernel);
}
void test_ams__connect_to_music_service_upon_subscribing_entity_update_characteristic(void) {
prv_discover_ams(1 /* num_instances */);
// Not connected yet (still need to subscribe):
cl_assert_equal_s(music_get_connected_server_debug_name(), NULL);
// Simulate successful subscription:
BLECharacteristic entity_update = s_characteristics[0][AMSCharacteristicEntityUpdate];
ams_handle_subscribe(entity_update, BLESubscriptionNotifications, BLEGATTErrorSuccess);
// Music service should be connected now:
cl_assert_equal_s(music_get_connected_server_debug_name(), ams_music_server_debug_name());
// Rediscovery will disconnect music service (until resubscribed):
ams_invalidate_all_references();
prv_discover_ams(1 /* num_instances */);
cl_assert_equal_s(music_get_connected_server_debug_name(), NULL);
}
void test_ams__dont_connect_music_service_if_subscribe_entity_update_characteristic_fails(void) {
prv_discover_ams(1 /* num_instances */);
// Simulate failed subscription:
BLECharacteristic entity_update = s_characteristics[0][AMSCharacteristicEntityUpdate];
ams_handle_subscribe(entity_update, BLESubscriptionNotifications, BLEGATTErrorUnlikelyError);
// Not connected because subscription failed:
cl_assert_equal_s(music_get_connected_server_debug_name(), NULL);
}
void test_ams__update_characteristics_ams_not_found(void) {
// AMS Found:
prv_discover_ams(1 /* num_instances */);
// AMS Disappeared:
prv_discover_ams(0 /* num_instances */);
// Verify ams.c cannot handle the previous characteristics any more:
prv_assert_can_handle_characteristics(0, false /* expect_can_handle */);
}
// Tests: Register for Entity Updates
///////////////////////////////////////////////////////////
static const uint8_t s_register_player_entity[] = {0x00,
// Apple bug #21283910
// See ams.c, prv_get_registration_cmd_for_entity
// 0x00,
0x01, 0x02};
static const uint8_t s_register_queue_entity[] = {0x01, 0x00, 0x01, 0x02, 0x03};
static const uint8_t s_register_track_entity[] = {0x02, 0x00, 0x01, 0x02, 0x03};
void test_ams__register_for_entity_updates(void) {
prv_discover_ams(1 /* num_instances */);
cl_assert_equal_b(ams_is_registered_for_all_entity_updates(), false);
// Simulate successful subscription:
BLECharacteristic entity_update = s_characteristics[0][AMSCharacteristicEntityUpdate];
ams_handle_subscribe(entity_update, BLESubscriptionNotifications, BLEGATTErrorSuccess);
// Expect to have written the command to register for the Player entity:
fake_gatt_client_op_assert_write(entity_update, s_register_player_entity,
sizeof(s_register_player_entity), GAPLEClientKernel,
true /* is_response_required */);
ams_handle_write_response(entity_update, BLEGATTErrorSuccess);
cl_assert_equal_b(ams_is_registered_for_all_entity_updates(), false);
// Expect to have written the command to register for the Queue entity:
fake_gatt_client_op_assert_write(entity_update, s_register_queue_entity,
sizeof(s_register_queue_entity), GAPLEClientKernel,
true /* is_response_required */);
ams_handle_write_response(entity_update, BLEGATTErrorSuccess);
cl_assert_equal_b(ams_is_registered_for_all_entity_updates(), false);
// Expect to have written the command to register for the Track entity:
fake_gatt_client_op_assert_write(entity_update, s_register_track_entity,
sizeof(s_register_track_entity), GAPLEClientKernel,
true /* is_response_required */);
ams_handle_write_response(entity_update, BLEGATTErrorSuccess);
cl_assert_equal_b(ams_is_registered_for_all_entity_updates(), true);
// After GATT re-discovery, the registration needs to happen again
ams_invalidate_all_references();
prv_discover_ams(1 /* num_instances */);
cl_assert_equal_b(ams_is_registered_for_all_entity_updates(), false);
}
void test_ams__register_for_entity_updates_retry_if_out_of_resources(void) {
prv_discover_ams(1 /* num_instances */);
// Simulate not having enough resources to process the request:
fake_gatt_client_op_set_write_return_value(BTErrnoNotEnoughResources);
// Simulate successful subscription:
BLECharacteristic entity_update = s_characteristics[0][AMSCharacteristicEntityUpdate];
ams_handle_subscribe(entity_update, BLESubscriptionNotifications, BLEGATTErrorSuccess);
// Nothing should have been written because out of resources:
fake_gatt_client_op_assert_no_write();
// Resources come available again:
fake_gatt_client_op_set_write_return_value(BTErrnoOK);
// Simulate processing the callback to retry:
cl_assert(s_launcher_task_callback != NULL);
s_launcher_task_callback(s_launcher_task_callback_data);
// Expect to have written the command to register for the Player entity:
fake_gatt_client_op_assert_write(entity_update, s_register_player_entity,
sizeof(s_register_player_entity), GAPLEClientKernel,
true /* is_response_required */);
ams_handle_write_response(entity_update, BLEGATTErrorSuccess);
}
static const MusicServerImplementation s_dummy_server_implementation = {};
static void prv_set_dummy_server_connected(bool connected) {
music_set_connected_server(&s_dummy_server_implementation,
connected /* connected */);
}
void test_ams__dont_register_if_another_music_server_is_already_connected(void) {
prv_discover_ams(1 /* num_instances */);
// Not connected yet (still need to subscribe):
cl_assert_equal_s(music_get_connected_server_debug_name(), NULL);
prv_set_dummy_server_connected(true /* connected */);
// Simulate successful subscription:
BLECharacteristic entity_update = s_characteristics[0][AMSCharacteristicEntityUpdate];
ams_handle_subscribe(entity_update, BLESubscriptionNotifications, BLEGATTErrorSuccess);
// Nothing should have been written, because there's already a music server connected.
fake_gatt_client_op_assert_no_write();
// Clean up after ourselves:
prv_set_dummy_server_connected(false /* connected */);
}
// Tests: Sending Remote Commands
///////////////////////////////////////////////////////////
static void prv_connect_ams(void) {
prv_discover_ams(1 /* num_instances */);
// Simulate successful subscription:
BLECharacteristic entity_update = s_characteristics[0][AMSCharacteristicEntityUpdate];
ams_handle_subscribe(entity_update, BLESubscriptionNotifications, BLEGATTErrorSuccess);
// Simulate successful write responses (for the Entity Update registration write requests):
ams_handle_write_response(entity_update, BLEGATTErrorSuccess);
ams_handle_write_response(entity_update, BLEGATTErrorSuccess);
ams_handle_write_response(entity_update, BLEGATTErrorSuccess);
cl_assert_equal_b(ams_is_registered_for_all_entity_updates(), true);
fake_gatt_client_op_clear_write_list();
}
static uint8_t prv_ams_command_for_music_command(MusicCommand command) {
switch (command) {
case MusicCommandPlay:
return 0x00;
case MusicCommandPause:
return 0x01;
case MusicCommandTogglePlayPause:
return 0x02;
case MusicCommandNextTrack:
return 0x03;
case MusicCommandPreviousTrack:
return 0x04;
case MusicCommandVolumeUp:
return 0x05;
case MusicCommandVolumeDown:
return 0x06;
case MusicCommandAdvanceRepeatMode:
return 0x07;
case MusicCommandAdvanceShuffleMode:
return 0x08;
case MusicCommandSkipForward:
return 0x09;
case MusicCommandSkipBackward:
return 0x0A;
case MusicCommandLike:
return 0x0B;
case MusicCommandDislike:
return 0x0C;
case MusicCommandBookmark:
return 0x0D;
default:
cl_assert(false);
return 0xFF;
}
}
void test_ams__send_remote_command(void) {
prv_connect_ams();
// Exercise all MusicCommand types (currently they are all supported):
for (MusicCommand music_cmd = 0; music_cmd < NumMusicCommand; ++music_cmd) {
music_command_send(music_cmd);
BLECharacteristic remote_command = s_characteristics[0][AMSCharacteristicRemoteCommand];
const uint8_t ams_cmd = prv_ams_command_for_music_command(music_cmd);
fake_gatt_client_op_assert_write(remote_command, &ams_cmd, sizeof(ams_cmd),
GAPLEClientKernel, true /* is_response_required */);
// Simulate receiving the response:
ams_handle_write_response(remote_command, BLEGATTErrorSuccess);
}
// Invalid/Unsupported command:
music_command_send(NumMusicCommand);
fake_gatt_client_op_assert_no_write();
}
void test_ams__send_remote_command_non_kernel_main_task(void) {
prv_connect_ams();
// Simulate Music app calling music_command_send():
stub_pebble_tasks_set_current(PebbleTask_App);
music_command_send(MusicCommandPlay);
// Process the KernelMain callback:
cl_assert(s_launcher_task_callback != NULL);
stub_pebble_tasks_set_current(PebbleTask_KernelMain);
s_launcher_task_callback(s_launcher_task_callback_data);
BLECharacteristic remote_command = s_characteristics[0][AMSCharacteristicRemoteCommand];
const uint8_t ams_cmd = prv_ams_command_for_music_command(MusicCommandPlay);
fake_gatt_client_op_assert_write(remote_command, &ams_cmd, sizeof(ams_cmd),
GAPLEClientKernel, true /* is_response_required */);
}
void test_ams__send_remote_command_non_kernel_main_task_then_disconnect(void) {
prv_connect_ams();
// Simulate Music app calling music_command_send():
stub_pebble_tasks_set_current(PebbleTask_App);
music_command_send(MusicCommandPlay);
// Simulate disconnecting:
ams_destroy();
// Process the KernelMain callback:
cl_assert(s_launcher_task_callback != NULL);
stub_pebble_tasks_set_current(PebbleTask_KernelMain);
s_launcher_task_callback(s_launcher_task_callback_data);
// No crashes, no writes
fake_gatt_client_op_assert_no_write();
}
// Tests: music_needs_user_to_start_playback_on_phone
///////////////////////////////////////////////////////////
void test_ams__music_needs_user_to_start_playback_on_phone(void) {
prv_connect_ams();
// No metadata received yet, user needs to start playback on phone
cl_assert_equal_b(music_needs_user_to_start_playback_on_phone(), true);
// Received metadata
// cl_assert_equal_b(music_needs_user_to_start_playback_on_phone(), false);
// Metadata cleared
cl_assert_equal_b(music_needs_user_to_start_playback_on_phone(), true);
}
// Tests: music_request_..._latency
///////////////////////////////////////////////////////////
void test_ams__music_request_reduced_latency(void) {
prv_connect_ams();
music_request_reduced_latency(true /* reduced_latency */);
cl_assert_equal_i(s_conn_mgr_states[BtConsumerMusicServiceIndefinite].state,
ResponseTimeMiddle);
cl_assert_equal_i(s_conn_mgr_states[BtConsumerMusicServiceIndefinite].max_period_secs,
MAX_PERIOD_RUN_FOREVER);
music_request_reduced_latency(false /* reduced_latency */);
cl_assert_equal_i(s_conn_mgr_states[BtConsumerMusicServiceIndefinite].state,
ResponseTimeMax);
}
void test_ams__music_request_low_latency_for_period(void) {
prv_connect_ams();
const uint32_t period_s = 1234;
music_request_low_latency_for_period(period_s * 1000);
cl_assert_equal_i(s_conn_mgr_states[BtConsumerMusicServiceMomentary].state,
ResponseTimeMin);
cl_assert_equal_i(s_conn_mgr_states[BtConsumerMusicServiceMomentary].max_period_secs,
period_s);
}
// Tests: Receiving Player updates (the happy paths)
///////////////////////////////////////////////////////////
static void prv_receive_entity_update(const uint8_t *update, uint16_t update_length) {
BLECharacteristic entity_update = s_characteristics[0][AMSCharacteristicEntityUpdate];
ams_handle_read_or_notification(entity_update, update, update_length, BLEGATTErrorSuccess);
}
void test_ams__receive_player_name_update(void) {
prv_connect_ams();
// 0000 00 00 00 4d 75 73 69 63 ...Music
uint8_t player_name_update[] = {
0x00, 0x00, 0x00, 0x4d, 0x75, 0x73, 0x69, 0x63,
};
prv_receive_entity_update(player_name_update, sizeof(player_name_update));
char player_name[MUSIC_BUFFER_LENGTH];
cl_assert_equal_b(music_get_player_name(player_name), true);
cl_assert_equal_s(player_name, "Music");
}
void test_ams__receive_player_playback_info_update(void) {
prv_connect_ams();
// Receive: playing, 200% playback rate, elapsed time 184.755s
// 0000 00 01 00 31 2c 32 2e 30 2c 31 38 34 2e 37 35 35 ...1,2.0 ,184.755
uint8_t playback_info_update[] = {
0x00, 0x01, 0x00, 0x31, 0x2c, 0x32, 0x2e, 0x30,
0x2c, 0x31, 0x38, 0x34, 0x2e, 0x37, 0x35, 0x35,
};
prv_receive_entity_update(playback_info_update, sizeof(playback_info_update));
cl_assert_equal_i(music_get_playback_state(), MusicPlayStatePlaying);
// music_get_pos relies on having a sensible track duration, so simulate receiving this too:
// 02 03 00 33 31 39 2e 35 30 37 ...319.5 07
uint8_t track_duration_update[] = {
0x02, 0x03, 0x00, 0x33, 0x31, 0x39, 0x2e, 0x35,
0x30, 0x37,
};
prv_receive_entity_update(track_duration_update, sizeof(track_duration_update));
uint32_t track_pos_ms;
uint32_t track_duration_ms;
music_get_pos(&track_pos_ms, &track_duration_ms);
cl_assert_equal_i(track_pos_ms, 184755);
cl_assert_equal_i(track_duration_ms, 319507);
cl_assert_equal_i(music_get_playback_rate_percent(), 200);
}
void test_ams__receive_player_playback_info_update_nulls(void) {
prv_connect_ams();
// Receive: paused, empty string, empty string
// 0000 00 01 00 30 2c 2c ...0,,
uint8_t playback_info_update[] = {
0x00, 0x01, 0x00, 0x30, 0x2c, 0x2c,
};
prv_receive_entity_update(playback_info_update, sizeof(playback_info_update));
cl_assert_equal_i(music_get_playback_state(), MusicPlayStatePaused);
uint32_t track_pos_ms;
uint32_t track_duration_ms;
music_get_pos(&track_pos_ms, &track_duration_ms);
cl_assert_equal_i(track_pos_ms, 0);
cl_assert_equal_i(track_duration_ms, 0);
cl_assert_equal_i(music_get_playback_rate_percent(), 0);
}
void test_ams__receive_player_volume_update(void) {
prv_connect_ams();
// Receive volume of 0.604925
// 00 02 00 30 2e 36 30 34 39 32 35 ...0.604 925
uint8_t volume_update[] = {
0x00, 0x02, 0x00, 0x30, 0x2e, 0x36, 0x30, 0x34,
0x39, 0x32, 0x35,
};
prv_receive_entity_update(volume_update, sizeof(volume_update));
cl_assert_equal_i(music_get_volume_percent(), 60);
}
// Tests: Receiving Player updates (the unhappy paths)
///////////////////////////////////////////////////////////
static void prv_assert_initial_playback_state(void) {
cl_assert_equal_i(music_get_playback_state(), MusicPlayStateUnknown);
uint32_t track_pos_ms;
uint32_t track_duration_ms;
music_get_pos(&track_pos_ms, &track_duration_ms);
cl_assert_equal_i(track_pos_ms, 0);
cl_assert_equal_i(track_duration_ms, 0);
cl_assert_equal_i(music_get_playback_rate_percent(), 0);
}
void test_ams__receive_non_numeric_player_playback_info_update(void) {
prv_connect_ams();
// Receive: 'A', 'B.0' playback rate, elapsed time 184.755s
// 0000 00 01 00 41 2c 42 2e 30 2c 31 38 34 2e 37 35 35 ...A,B.0 ,184.755
uint8_t nan_playback_info_update[] = {
0x00, 0x01, 0x00, 0x41, 0x2c, 0x42, 0x2e, 0x30,
0x2c, 0x31, 0x38, 0x34, 0x2e, 0x37, 0x35, 0x35,
};
prv_receive_entity_update(nan_playback_info_update, sizeof(nan_playback_info_update));
prv_assert_initial_playback_state();
}
void test_ams__receive_incomplete_csv_list_player_playback_info_update(void) {
prv_connect_ams();
// Receive: playing, 200% playback rate
// 0000 00 01 00 31 2c 32 2e 30 ...1,2.0
uint8_t incomplete_playback_info_update[] = {
0x00, 0x01, 0x00, 0x31, 0x2c, 0x32, 0x2e, 0x30,
};
prv_receive_entity_update(incomplete_playback_info_update,
sizeof(incomplete_playback_info_update));
prv_assert_initial_playback_state();
}
void test_ams__receive_malformed_player_volume_update(void) {
prv_connect_ams();
cl_assert_equal_i(music_get_volume_percent(), 0);
// Receive volume of 0.604925
// 00 02 00 30 2e 41 30 34 39 32 35 ...0.A04 925
uint8_t volume_update[] = {
0x00, 0x02, 0x00, 0x30, 0x2e, 0x41, 0x30, 0x34,
0x39, 0x32, 0x35,
};
prv_receive_entity_update(volume_update, sizeof(volume_update));
cl_assert_equal_i(music_get_volume_percent(), 0);
}
// Tests: Receiving Track updates
///////////////////////////////////////////////////////////
void test_ams__receive_track_artist_update(void) {
prv_connect_ams();
// 0000 02 00 00 4d 69 6c 65 73 20 44 61 76 69 73 ...Miles Davis
uint8_t track_artist_update[] = {
0x02, 0x00, 0x00, 0x4d, 0x69, 0x6c, 0x65, 0x73,
0x20, 0x44, 0x61, 0x76, 0x69, 0x73,
};
prv_receive_entity_update(track_artist_update, sizeof(track_artist_update));
char artist[MUSIC_BUFFER_LENGTH];
music_get_now_playing(NULL, artist, NULL);
cl_assert_equal_s(artist, "Miles Davis");
}
void test_ams__receive_track_title_update(void) {
prv_connect_ams();
// 0000 02 02 00 53 6f 20 57 68 61 74 ...So Wh at
uint8_t track_title_update[] = {
0x02, 0x02, 0x00, 0x53, 0x6f, 0x20, 0x57, 0x68,
0x61, 0x74,
};
prv_receive_entity_update(track_title_update, sizeof(track_title_update));
char title[MUSIC_BUFFER_LENGTH];
music_get_now_playing(title, NULL, NULL);
cl_assert_equal_s(title, "So What");
}
void test_ams__receive_track_album_update(void) {
prv_connect_ams();
// 0000 02 01 00 4b 69 6e 64 20 4f 66 20 42 6c 75 65 20 ...Kind Of Blue
// 0010 28 4c 65 67 61 63 79 20 45 64 69 74 69 6f 6e 29 (Legacy Edition)
uint8_t track_album_update[] = {
0x02, 0x01, 0x00, 0x4b, 0x69, 0x6e, 0x64, 0x20,
0x4f, 0x66, 0x20, 0x42, 0x6c, 0x75, 0x65, 0x20,
0x28, 0x4c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x20,
0x45, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x29,
};
prv_receive_entity_update(track_album_update, sizeof(track_album_update));
char album[MUSIC_BUFFER_LENGTH];
music_get_now_playing(NULL, NULL, album);
cl_assert_equal_s(album, "Kind Of Blue (Legacy Edition)");
}
// Tests: Music service capabilities
///////////////////////////////////////////////////////////
void test_ams__supported_capabilities(void) {
prv_connect_ams();
// music_is_progress_reporting_supported() relies on a valid track duration
uint8_t track_duration_update[] = {
0x02, 0x03, 0x00, 0x33, 0x31, 0x39, 0x2e, 0x35,
0x30, 0x37,
};
prv_receive_entity_update(track_duration_update, sizeof(track_duration_update));
cl_assert_equal_b(music_is_playback_state_reporting_supported(), true);
cl_assert_equal_b(music_is_progress_reporting_supported(), true);
cl_assert_equal_b(music_is_volume_reporting_supported(), true);
cl_assert_equal_b(music_needs_user_to_start_playback_on_phone(), true);
for (MusicCommand cmd = 0; cmd < NumMusicCommand; ++cmd) {
cl_assert_equal_b(music_is_command_supported(cmd), true);
}
cl_assert_equal_b(music_is_command_supported(NumMusicCommand), false);
}
// Tests: Create & Destroy
///////////////////////////////////////////////////////////
void test_ams__create_again_trips_assert(void) {
cl_assert_passert(ams_create());
}
void test_ams__create_works_again_after_destroy(void) {
ams_destroy();
ams_create();
// No assert hit.
}
void test_ams__destroy_after_destroy_is_fine(void) {
ams_destroy();
ams_destroy();
// No assert hit.
}
void test_ams__destroy_disconnects_from_music_service(void) {
prv_discover_ams(1 /* num_instances */);
// Simulate successful subscription:
BLECharacteristic entity_update = s_characteristics[0][AMSCharacteristicEntityUpdate];
ams_handle_subscribe(entity_update, BLESubscriptionNotifications, BLEGATTErrorSuccess);
ams_destroy();
cl_assert_equal_s(music_get_connected_server_debug_name(), NULL);
}
// Test setup
///////////////////////////////////////////////////////////
void test_ams__initialize(void) {
s_launcher_task_callback = NULL;
memset(s_conn_mgr_states, 0, sizeof(s_conn_mgr_states));
fake_rtc_init(1234, 5678);
fake_event_init();
fake_gatt_client_subscriptions_init();
fake_gatt_client_op_init();
stub_pebble_tasks_set_current(PebbleTask_KernelMain);
ams_create();
cl_assert_equal_s(music_get_connected_server_debug_name(), NULL);
}
void test_ams__cleanup(void) {
ams_destroy();
fake_gatt_client_op_deinit();
fake_gatt_client_subscriptions_deinit();
}
+263
View File
@@ -0,0 +1,263 @@
/*
* 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 "comm/ble/kernel_le_client/ams/ams_util.h"
#include "clar.h"
// Stubs
///////////////////////////////////////////////////////////
#include "stubs_logging.h"
// Helpers
///////////////////////////////////////////////////////////
static char s_results[10][32];
static uint32_t s_results_lengths[10];
static uint8_t s_results_count;
static uint32_t s_max_results_count;
static bool prv_result_callback(const char *value, uint32_t value_length,
uint32_t index, void *context) {
strcpy(s_results[s_results_count], value);
s_results_lengths[s_results_count] = value_length;
cl_assert_equal_i(index, s_results_count);
++s_results_count;
return (s_max_results_count > s_results_count);
}
// Tests
///////////////////////////////////////////////////////////
void test_ams_util__initialize(void) {
memset(s_results, 0, sizeof(s_results));
s_results_count = 0;
s_max_results_count = ~0;
}
void test_ams_util__cleanup(void) {
}
// ams_util_float_string_parse() tests
///////////////////////////////////////////////////////////
#define assert_float_parse(in, mul, succeeds, expected_result) \
{ \
const char *input = (const char *)in; \
const uint32_t input_length = input ? sizeof(in) : 0; \
const uint32_t multiplier = mul; \
int32_t result = 0; \
cl_assert_equal_b(succeeds, \
ams_util_float_string_parse((const char *)in, input_length, multiplier, &result)); \
if (succeeds) { \
cl_assert_equal_i(result, expected_result); \
} \
}
void test_ams_util__float_string_parse_negative_number(void) {
// "-1" * 3
assert_float_parse("-1", 3, true, -1 * 3);
}
void test_ams_util__float_string_parse_only_minus_sign(void) {
// "-" * 3
assert_float_parse("-", 3, false, 0);
}
void test_ams_util__float_string_parse_negative_number_nothing_before_separator(void) {
// "-.1" * 30
assert_float_parse("-.1", 30, true, -3);
}
void test_ams_util__float_string_parse_multiple_minusses(void) {
// "--.1" * 30
assert_float_parse("--.1", 30, false, 0);
}
void test_ams_util__float_string_parse_null(void) {
// NULL * 3
assert_float_parse(NULL, 3, false, 0);
}
void test_ams_util__float_string_parse_not_zero_terminated(void) {
uint8_t buffer[] = {'1'};
assert_float_parse(buffer, 3, true, 3);
}
void test_ams_util__float_string_parse_null_in_the_middle(void) {
uint8_t buffer[] = {'1', 0, '2'};
assert_float_parse(buffer, 3, true, 3);
}
void test_ams_util__float_string_parse_empty_string(void) {
// "" * 3
assert_float_parse("", 3, false, 0);
}
void test_ams_util__float_string_parse_not_a_number(void) {
// "hello" * 3
assert_float_parse("hello", 3, false, 0);
// " " * 3
assert_float_parse(" ", 3, false, 0);
}
void test_ams_util__float_string_parse_no_fraction(void) {
// "42" * 3
assert_float_parse("42", 3, true, 42 * 3);
}
void test_ams_util__float_string_parse_separator_but_no_fraction(void) {
// "21." * 3
assert_float_parse("21.", 3, true, 21 * 3);
}
void test_ams_util__float_string_parse_comma_decimal_separator(void) {
// "1.23456" * 3 is approx 3.7036800000000003, round up to 4
assert_float_parse("1,23456", 3, true, 4);
}
void test_ams_util__float_string_parse_period_decimal_separator(void) {
// "1.23456" * 3 is approx 3.7036800000000003, round up to 4
assert_float_parse("1.23456", 3, true, 4);
}
void test_ams_util__float_string_parse_round_down(void) {
// "0.74995" * 2 is approx 1.4999, round down to 1
assert_float_parse("0.74995", 2, true, 1);
}
void test_ams_util__float_string_parse_multiple_separators(void) {
// "1.2.2" * 3
assert_float_parse("1.2.2", 2, false, 0);
}
void test_ams_util__float_string_positive_multiplier(void) {
// "1.654321" * 33 is approx 54.592593, round up to 55
assert_float_parse("1.654321", 33, true, 55);
}
void test_ams_util__float_string_negative_multiplier(void) {
// "1.987622" * -33 is approx -65.591526, round to -66
assert_float_parse("1.987622", -33, true, -66);
}
void test_ams_util__float_string_parse_overflow_positive(void) {
// fails:
// "2147483648" * 1
assert_float_parse("2147483648", 1, false, 0);
// succeeds:
// "2147483647" * 1
assert_float_parse("2147483647", 1, true, 2147483647);
}
void test_ams_util__float_string_parse_overflow_negative(void) {
// fails:
// "-2147483649" * 1
assert_float_parse("-2147483649", 1, false, 0);
// succeeds:
// "-2147483648" * 1
assert_float_parse("-2147483648", 1, true, -2147483648);
assert_float_parse("2147483648", -1, true, -2147483648);
}
// ams_util_csv_parse() tests
///////////////////////////////////////////////////////////
#define assert_result(idx, val, len); \
cl_assert_equal_i(len, s_results_lengths[idx]); \
cl_assert_equal_i(memcmp(val, s_results[idx], len), 0);
void test_ams_util__csv_empty_string(void) {
const char empty_string[] = "";
const uint8_t count = ams_util_csv_parse(empty_string, sizeof(empty_string), NULL,
prv_result_callback);
cl_assert_equal_i(s_results_count, 1);
cl_assert_equal_i(count, 1);
cl_assert_equal_s(empty_string, s_results[0]);
}
void test_ams_util__csv_empty_values(void) {
const char one_value[] = ",";
const uint8_t count = ams_util_csv_parse(one_value, sizeof(one_value), NULL,
prv_result_callback);
cl_assert_equal_i(s_results_count, 2);
cl_assert_equal_i(count, 2);
assert_result(0, "", 0);
assert_result(1, "", 0);
}
void test_ams_util__csv_null(void) {
const uint8_t count = ams_util_csv_parse(NULL, 0, NULL,
prv_result_callback);
cl_assert_equal_i(s_results_count, 0);
cl_assert_equal_i(count, 0);
}
void test_ams_util__csv_one_value(void) {
const char one_value[] = "A";
const uint8_t count = ams_util_csv_parse(one_value, sizeof(one_value), NULL,
prv_result_callback);
cl_assert_equal_i(s_results_count, 1);
cl_assert_equal_i(count, 1);
cl_assert_equal_s(one_value, s_results[0]);
}
void test_ams_util__csv_multiple_values(void) {
const char multi_values[] = "A,B,C";
const uint8_t count = ams_util_csv_parse(multi_values, sizeof(multi_values), NULL,
prv_result_callback);
cl_assert_equal_i(s_results_count, 3);
cl_assert_equal_i(count, 3);
assert_result(0, "A", 1);
assert_result(1, "B", 1);
assert_result(2, "C", 1);
}
void test_ams_util__csv_stop_after_one_value(void) {
const char multi_values[] = "A,B,C";
s_max_results_count = 1;
const uint8_t count = ams_util_csv_parse(multi_values, sizeof(multi_values), NULL,
prv_result_callback);
cl_assert_equal_i(s_results_count, 1);
cl_assert_equal_i(count, 1);
assert_result(0, "A", 1);
}
void test_ams_util__csv_null_in_the_middle(void) {
const char null_middle_value[] = "A\x00 BCD,1234";
cl_assert(sizeof(null_middle_value) > 2);
const uint8_t count = ams_util_csv_parse(null_middle_value, sizeof(null_middle_value), NULL,
prv_result_callback);
cl_assert_equal_i(s_results_count, 1);
cl_assert_equal_i(count, 1);
assert_result(0, "A", 1);
}
void test_ams_util__csv_buffer_not_zero_terminated(void) {
const char one_value[] = "ABCDEF";
cl_assert(sizeof(one_value) > 2);
const uint8_t count = ams_util_csv_parse(one_value,
sizeof(one_value) - 1 /* omit zero teminator */, NULL,
prv_result_callback);
cl_assert_equal_i(s_results_count, 1);
cl_assert_equal_i(count, 1);
assert_result(0, "ABCDEF", 6);
}
+644
View File
@@ -0,0 +1,644 @@
/*
* 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 "comm/ble/kernel_le_client/ancs/ancs.h"
#include "comm/ble/kernel_le_client/ancs/ancs_util.h"
#include "comm/ble/kernel_le_client/ancs/ancs_definition.h"
#include "comm/ble/gap_le_connection.h"
#include "comm/ble/gap_le_task.h"
#include "services/common/evented_timer.h"
#include "services/common/regular_timer.h"
#include "services/normal/notifications/ancs/ancs_notifications.h"
#include "util/size.h"
#include "clar.h"
// Stubs
///////////////////////////////////////////////////////////
#include "stubs_analytics.h"
#include "stubs_ios_notif_pref_db.h"
#include "stubs_bt_stack.h"
#include "stubs_ble.h"
#include "stubs_pin_db.h"
#include "stubs_i18n.h"
#include "stubs_layout_layer.h"
#include "stubs_logging.h"
#include "stubs_mutex.h"
#include "stubs_passert.h"
#include "stubs_pebble_tasks.h"
#include "stubs_pebble_pairing_service.h"
#include "stubs_prompt.h"
#include "stubs_timeline.h"
#include "stubs_queue.h"
#include "stubs_rand_ptr.h"
#include "stubs_reconnect.h"
#include "stubs_reminder_db.h"
#include "stubs_reminders.h"
#include "stubs_serial.h"
#include "stubs_sleep.h"
#include "stubs_system_reset.h"
#include "stubs_task_watchdog.h"
#include "stubs_nexmo.h"
#include "stubs_codepoint.h"
#include "stubs_utf8.h"
void launcher_task_add_callback(void (*callback)(void *data), void *data) {
callback(data);
}
PebblePhoneCaller* phone_call_util_create_caller(const char *number, const char *name) {
return NULL;
}
// Fakes
///////////////////////////////////////////////////////////
#include "fake_events.h"
#include "fake_kernel_services_notifications.h"
#include "fake_new_timer.h"
#include "fake_notification_storage.h"
#include "fake_pbl_malloc.h"
#include "fake_spi_flash.h"
bool shell_prefs_get_language_english(void) {
return false;
}
static bool s_block_event_callback = false;
EventedTimerID evented_timer_register(uint32_t timeout_ms,
bool repeating,
EventedTimerCallback callback,
void* callback_data) {
if (!s_block_event_callback) {
callback(callback_data);
}
return 0;
}
// Test data
///////////////////////////////////////////////////////////
#include "ancs_test_data.h"
const uint32_t s_invalid_param_uid = 0x12;
const uint32_t s_get_wrong_data_uid = 0xee;
static BLECharacteristic s_characteristics[NumANCSCharacteristic] = { 1, 2, 3 };
// Helper Functions
///////////////////////////////////////////////////////////
static int s_num_requested_app_attributes;
static int s_num_requested_notif_attributes;
static int s_num_ds_notifications_received;
static bool s_gatt_client_op_write_should_fail_unlimited = false;
static bool s_gatt_client_op_write_should_fail_once = false;
static void prv_fake_receiving_ds_notification(size_t value_length, uint8_t *value) {
BLECharacteristic characteristic = s_characteristics[ANCSCharacteristicData];
ancs_handle_read_or_notification(characteristic, (const uint8_t *) value, value_length, 0);
}
static void prv_fake_receiving_ns_notification(size_t value_length, uint8_t *value) {
BLECharacteristic characteristic = s_characteristics[ANCSCharacteristicNotification];
ancs_handle_read_or_notification(characteristic, (const uint8_t *) value, value_length, 0);
}
static void prv_send_notification_with_event_flags(const uint8_t *ancs_notification_dict,
int event_flags) {
NSNotification ns_notification = {
.event_id = EventIDNotificationAdded,
.event_flags = event_flags,
.category_id = CategoryIDSocial,
.category_count = 1,
.uid = 1,
};
const uint32_t ancs_notification_dict_uid =
((GetNotificationAttributesMsg *)ancs_notification_dict)->notification_uid;
ns_notification.uid = ancs_notification_dict_uid;
prv_fake_receiving_ns_notification(sizeof(ns_notification), (uint8_t*) &ns_notification);
}
static void prv_send_notification(const uint8_t *ancs_notification_dict) {
prv_send_notification_with_event_flags(ancs_notification_dict, 0);
}
static uint8_t *prv_serialize_timeline_item(TimelineItem *item, size_t *size_out) {
size_t payload_size = timeline_item_get_serialized_payload_size(item);
*size_out = sizeof(SerializedTimelineItemHeader) + payload_size;
uint8_t *buffer = malloc(*size_out);
timeline_item_serialize_header(item, (SerializedTimelineItemHeader *)buffer);
timeline_item_serialize_payload(item, buffer + sizeof(SerializedTimelineItemHeader),
payload_size);
return buffer;
}
void prv_cmp_last_received_notification(TimelineItem *item) {
TimelineItem *notification = fake_notification_storage_get_last_notification();
size_t size1, size2;
// Clear out the id since it is auto-generated
memset(&notification->header.id, 0, sizeof(notification->header.id));
uint8_t *buf1 = prv_serialize_timeline_item(notification, &size1);
uint8_t *buf2 = prv_serialize_timeline_item(item, &size2);
cl_assert_equal_i(size1, size2);
cl_assert_equal_m(buf1, buf2, size1);
free(buf1);
free(buf2);
}
// Called from inside prv_write_control_point_request.
// If this function is called we have requested a ds_notification
BTErrno gatt_client_op_write(BLECharacteristic characteristic,
const uint8_t *buffer,
size_t length,
GAPLEClient client) {
cl_assert_equal_i(characteristic, s_characteristics[ANCSCharacteristicControl]);
if (s_gatt_client_op_write_should_fail_once) {
s_gatt_client_op_write_should_fail_once = false;
return BTErrnoInvalidParameter;
}
if (s_gatt_client_op_write_should_fail_unlimited) {
return BTErrnoInvalidParameter;
}
const uint32_t comple_dict_uid = ((GetNotificationAttributesMsg*)s_complete_dict)->notification_uid;
const uint32_t chunked_dict_uid = ((GetNotificationAttributesMsg*)s_chunked_dict_part_one)->notification_uid;
const uint32_t message_size_attr_dict_uid = ((GetNotificationAttributesMsg*)s_message_size_attr_dict)->notification_uid;
const uint32_t invalid_dict_uid = ((GetNotificationAttributesMsg*)s_invalid_attribute_length)->notification_uid;
const uint32_t attribute_at_end_uid = ((GetNotificationAttributesMsg*)memory_with_attribute_id_at_end.attribute_data)->notification_uid;
const uint32_t loading_uid = ((GetNotificationAttributesMsg*)s_loading_response)->notification_uid;
const uint32_t no_content_uid = ((GetNotificationAttributesMsg*)s_this_message_has_no_content_response)->notification_uid;
const uint32_t multiple_complete_dict_uid = ((GetNotificationAttributesMsg*)s_multiple_complete_dicts)->notification_uid;
const uint32_t split_timestamp_uid = ((GetNotificationAttributesMsg*)s_split_timestamp_dict_part_one)->notification_uid;
const uint32_t message_dict_uid = ((GetNotificationAttributesMsg*)s_message_dict)->notification_uid;
const uint32_t app_name_title_dict_uid = ((GetNotificationAttributesMsg*)s_app_name_title_dict)->notification_uid;
const uint32_t unknown_app_message_dict_uid = ((GetNotificationAttributesMsg*)s_unknown_app_dict)->notification_uid;
const uint32_t unknown_app_unique_title_dict_uid = ((GetNotificationAttributesMsg*)s_unknown_app_unique_title_dict)->notification_uid;
const uint32_t mms_no_caption_dict_uid = ((GetNotificationAttributesMsg*)s_mms_no_caption_dict)->notification_uid;
const uint32_t mms_with_caption_dict_uid = ((GetNotificationAttributesMsg*)s_mms_with_caption_dict)->notification_uid;
const CPDSMessage *cmd_header = (const CPDSMessage *)buffer;
if (cmd_header->command_id == CommandIDGetAppAttributes) {
s_num_requested_app_attributes++;
if (strcmp((const char *)cmd_header->data, "com.tests.NotAnApp") == 0) {
prv_fake_receiving_ds_notification(ARRAY_LENGTH(s_fake_app_info_dict),
(uint8_t *)s_fake_app_info_dict);
} else {
prv_fake_receiving_ds_notification(ARRAY_LENGTH(s_message_app_info_dict),
(uint8_t *)s_message_app_info_dict);
}
return BTErrnoOK;
}
// else: notif request
uint32_t uid = ((GetNotificationAttributesMsg *)buffer)->notification_uid;
s_num_requested_notif_attributes++;
if (uid == comple_dict_uid) {
prv_fake_receiving_ds_notification(ARRAY_LENGTH(s_complete_dict), (uint8_t*) s_complete_dict);
s_num_ds_notifications_received++;
} else if (uid == chunked_dict_uid) {
prv_fake_receiving_ds_notification(ARRAY_LENGTH(s_chunked_dict_part_one), (uint8_t*) s_chunked_dict_part_one);
prv_fake_receiving_ds_notification(ARRAY_LENGTH(s_chunked_dict_part_two), (uint8_t*) s_chunked_dict_part_two);
s_num_ds_notifications_received += 2;
} else if (uid == message_size_attr_dict_uid) {
prv_fake_receiving_ds_notification(ARRAY_LENGTH(s_message_size_attr_dict), (uint8_t*) s_message_size_attr_dict);
s_num_ds_notifications_received++;
} else if (uid == attribute_at_end_uid) {
prv_fake_receiving_ds_notification(ARRAY_LENGTH(memory_with_attribute_id_at_end.attribute_data), (uint8_t*) memory_with_attribute_id_at_end.attribute_data);
prv_fake_receiving_ds_notification(ARRAY_LENGTH(memory_with_attribute_id_at_end_p2), (uint8_t*) memory_with_attribute_id_at_end_p2);
s_num_ds_notifications_received += 2;
} else if (uid == invalid_dict_uid) {
prv_fake_receiving_ds_notification(ARRAY_LENGTH(s_invalid_attribute_length), (uint8_t*) s_invalid_attribute_length);
s_num_ds_notifications_received++;
} else if (uid == loading_uid) {
prv_fake_receiving_ds_notification(ARRAY_LENGTH(s_loading_response),
(uint8_t*) s_loading_response);
s_num_ds_notifications_received++;
} else if (uid == no_content_uid) {
prv_fake_receiving_ds_notification(ARRAY_LENGTH(s_this_message_has_no_content_response),
(uint8_t*) s_this_message_has_no_content_response);
s_num_ds_notifications_received++;
} else if (uid == s_invalid_param_uid) {
ancs_handle_write_response(0, 0xA2);
s_num_ds_notifications_received++;
} else if (uid == multiple_complete_dict_uid) {
prv_fake_receiving_ds_notification(ARRAY_LENGTH(s_multiple_complete_dicts), (uint8_t*) s_multiple_complete_dicts);
s_num_ds_notifications_received += 3;
} else if (uid == split_timestamp_uid) {
prv_fake_receiving_ds_notification(ARRAY_LENGTH(s_split_timestamp_dict_part_one), (uint8_t*) s_split_timestamp_dict_part_one);
prv_fake_receiving_ds_notification(ARRAY_LENGTH(s_split_timestamp_dict_part_two), (uint8_t*) s_split_timestamp_dict_part_two);
s_num_ds_notifications_received += 2;
} else if (uid == message_dict_uid) {
prv_fake_receiving_ds_notification(ARRAY_LENGTH(s_message_dict), (uint8_t*) s_message_dict);
s_num_ds_notifications_received++;
} else if (uid == app_name_title_dict_uid) {
prv_fake_receiving_ds_notification(ARRAY_LENGTH(s_app_name_title_dict), (uint8_t *)s_app_name_title_dict);
s_num_ds_notifications_received++;
} else if (uid == unknown_app_message_dict_uid) {
prv_fake_receiving_ds_notification(ARRAY_LENGTH(s_unknown_app_dict), (uint8_t *)s_unknown_app_dict);
s_num_ds_notifications_received++;
} else if (uid == unknown_app_unique_title_dict_uid) {
prv_fake_receiving_ds_notification(ARRAY_LENGTH(s_unknown_app_unique_title_dict), (uint8_t *)s_unknown_app_unique_title_dict);
s_num_ds_notifications_received++;
} else if (uid == mms_no_caption_dict_uid) {
prv_fake_receiving_ds_notification(ARRAY_LENGTH(s_mms_no_caption_dict), (uint8_t *)s_mms_no_caption_dict);
s_num_ds_notifications_received++;
} else if (uid == mms_with_caption_dict_uid) {
prv_fake_receiving_ds_notification(ARRAY_LENGTH(s_mms_with_caption_dict), (uint8_t *)s_mms_with_caption_dict);
s_num_ds_notifications_received++;
} else if (uid == s_get_wrong_data_uid) {
// We wanted a notification attributes message, but got a app attributes message...
prv_fake_receiving_ds_notification(ARRAY_LENGTH(s_message_app_info_dict),
(uint8_t *)s_message_app_info_dict);
s_num_ds_notifications_received++;
}
return BTErrnoOK;
}
// Tests
///////////////////////////////////////////////////////////
#define TEST_START FILESYSTEM_FILE_TEST_SPACE_BEGIN
#define TEST_SIZE (FILESYSTEM_FILE_TEST_SPACE_END - \
FILESYSTEM_FILE_TEST_SPACE_BEGIN)
void test_ancs__initialize(void) {
s_block_event_callback = false;
regular_timer_init();
s_num_requested_notif_attributes = 0;
s_num_requested_app_attributes = 0;
s_num_ds_notifications_received = 0;
s_gatt_client_op_write_should_fail_once = false;
s_gatt_client_op_write_should_fail_unlimited = false;
fake_kernel_services_notifications_reset();
fake_notification_storage_reset();
fake_event_init();
ancs_create();
ancs_handle_service_discovered(s_characteristics);
}
void test_ancs__cleanup(void) {
ancs_destroy();
cl_assert_equal_i(regular_timer_seconds_count(), 0);
cl_assert_equal_i(regular_timer_minutes_count(), 0);
regular_timer_deinit();
}
// Janky black box smoke-test to exercise the ANCS message re-assembly state
// machine
void test_ancs__should_handle_small_and_large_messages(void) {
// Get 4 complete notifications
prv_send_notification((uint8_t *)&s_complete_dict);
prv_send_notification((uint8_t *)&s_complete_dict);
prv_send_notification((uint8_t *)&s_complete_dict);
prv_send_notification((uint8_t *)&s_complete_dict);
cl_assert_equal_i(s_num_requested_notif_attributes, 4);
cl_assert_equal_i(s_num_ds_notifications_received, 4);
cl_assert_equal_i(fake_kernel_services_notifications_ancs_notifications_count(), 4);
// Get 4 2-part notifications
prv_send_notification((uint8_t *)&s_chunked_dict_part_one);
prv_send_notification((uint8_t *)&s_chunked_dict_part_one);
prv_send_notification((uint8_t *)&s_chunked_dict_part_one);
prv_send_notification((uint8_t *)&s_chunked_dict_part_one);
cl_assert_equal_i(s_num_requested_notif_attributes, 4 + 4);
cl_assert_equal_i(s_num_ds_notifications_received, 4 + 2*4);
cl_assert_equal_i(fake_kernel_services_notifications_ancs_notifications_count(), 4 + 4);
// Some alternating complete / 2-part notifications
prv_send_notification((uint8_t *)&s_complete_dict);
prv_send_notification((uint8_t *)&s_chunked_dict_part_one);
prv_send_notification((uint8_t *)&s_complete_dict);
prv_send_notification((uint8_t *)&s_chunked_dict_part_one);
cl_assert_equal_i(s_num_requested_notif_attributes, 8 + 4);
cl_assert_equal_i(s_num_ds_notifications_received, 12 + 1 + 2 + 1 + 2);
cl_assert_equal_i(fake_kernel_services_notifications_ancs_notifications_count(), 4 + 4 + 4);
// Send a "corrupted" notification.
prv_send_notification((uint8_t *)&s_invalid_attribute_length);
cl_assert_equal_i(s_num_requested_notif_attributes, 12 + 1);
cl_assert_equal_i(s_num_ds_notifications_received, 18 + 1);
// No increment:
cl_assert_equal_i(fake_kernel_services_notifications_ancs_notifications_count(), 4 + 4 + 4);
}
void test_ancs__should_handle_message_size_attribtue(void) {
prv_send_notification((uint8_t *)&s_message_size_attr_dict);
cl_assert_equal_i(s_num_requested_notif_attributes, 1);
cl_assert_equal_i(s_num_ds_notifications_received, 1);
cl_assert_equal_i(fake_kernel_services_notifications_ancs_notifications_count(), 1);
}
void test_ancs__should_filter_out_loading_messages_from_mail_app(void) {
// Get notification for which we'll get a "Loading..." response:
prv_send_notification((uint8_t *)&s_loading_response);
cl_assert_equal_i(s_num_requested_notif_attributes, 1);
cl_assert_equal_i(s_num_ds_notifications_received, 1);
// Assert it got filtered out:
cl_assert_equal_i(fake_kernel_services_notifications_ancs_notifications_count(), 0);
// Get notification for which we'll get a "This message has no content." response:
prv_send_notification((uint8_t *)&s_this_message_has_no_content_response);
cl_assert_equal_i(s_num_requested_notif_attributes, 2);
cl_assert_equal_i(s_num_ds_notifications_received, 2);
// Assert it got filtered out:
cl_assert_equal_i(fake_kernel_services_notifications_ancs_notifications_count(), 0);
}
void test_ancs__should_filter_out_duplicate_messages(void) {
// With an empty db, new notifications should be added as usual
prv_send_notification((uint8_t *)&s_complete_dict);
cl_assert_equal_i(fake_kernel_services_notifications_ancs_notifications_count(), 1);
cl_assert_equal_i(fake_notification_storage_get_store_count(), 1);
cl_assert_equal_i(fake_notification_storage_get_remove_count(), 0);
// We should reject any notification that matches and has the exact same uid
uint32_t uid = ((GetNotificationAttributesMsg *)&s_complete_dict)->notification_uid;
fake_notification_storage_set_existing_ancs_notification(&(Uuid)UUID_SYSTEM, uid);
prv_send_notification((uint8_t *)&s_complete_dict);
cl_assert_equal_i(fake_kernel_services_notifications_ancs_notifications_count(), 1);
cl_assert_equal_i(fake_notification_storage_get_store_count(), 1);
cl_assert_equal_i(fake_notification_storage_get_remove_count(), 0);
// If there's a notification that matches with a different uid, we update the notification by
// removing and then storing again (we don't send a NotificationAdded event)
fake_notification_storage_set_existing_ancs_notification(&(Uuid)UUID_SYSTEM, UINT32_MAX);
prv_send_notification((uint8_t *)&s_complete_dict);
cl_assert_equal_i(fake_kernel_services_notifications_ancs_notifications_count(), 1);
cl_assert_equal_i(fake_notification_storage_get_store_count(), 2);
cl_assert_equal_i(fake_notification_storage_get_remove_count(), 1);
}
void test_ancs__should_handle_split_timestamp_messages(void) {
prv_send_notification((uint8_t *)&s_split_timestamp_dict_part_one);
cl_assert_equal_i(s_num_requested_notif_attributes, 1);
cl_assert_equal_i(s_num_ds_notifications_received, 2);
cl_assert_equal_i(fake_kernel_services_notifications_ancs_notifications_count(), 1);
}
void test_ancs__attribute_at_end(void) {
prv_send_notification((uint8_t *)&memory_with_attribute_id_at_end.attribute_data);
cl_assert_equal_i(s_num_requested_notif_attributes, 1 );
cl_assert_equal_i(s_num_ds_notifications_received, 2);
}
void test_ancs__app_name_cache(void) {
prv_send_notification((uint8_t *)&s_message_dict);
prv_send_notification((uint8_t *)&s_message_dict);
cl_assert_equal_i(s_num_requested_notif_attributes, 2);
// should have gotten cached the second time around
cl_assert_equal_i(s_num_requested_app_attributes, 1);
cl_assert_equal_i(s_num_ds_notifications_received, 2);
}
void test_ancs__ancs_invalid_param(void) {
NSNotification ns_notification = {
.event_id = EventIDNotificationAdded,
.event_flags = 0,
.category_id = CategoryIDSocial,
.category_count = 1,
.uid = 0,
};
const uint32_t comple_dict_uid = ((GetNotificationAttributesMsg*)s_complete_dict)->notification_uid;
ns_notification.uid = s_invalid_param_uid;
// This will return with an error ANCS_INVALID_PARAM
// Should not get re-requested
prv_fake_receiving_ns_notification(sizeof(ns_notification), (uint8_t*) &ns_notification);
cl_assert_equal_i(s_num_requested_notif_attributes, 1 );
cl_assert_equal_i(s_num_ds_notifications_received, 1);
ns_notification.uid = comple_dict_uid;
prv_fake_receiving_ns_notification(sizeof(ns_notification), (uint8_t*) &ns_notification);
cl_assert_equal_i(s_num_requested_notif_attributes, 2);
cl_assert_equal_i(s_num_ds_notifications_received, 2);
ns_notification.uid = s_invalid_param_uid;
// This will return with an error ANCS_INVALID_PARAM
// Should not get re-requested
prv_fake_receiving_ns_notification(sizeof(ns_notification), (uint8_t*) &ns_notification);
cl_assert_equal_i(s_num_requested_notif_attributes, 3);
cl_assert_equal_i(s_num_ds_notifications_received, 3);
ns_notification.uid = s_invalid_param_uid;
// This will return with an error ANCS_INVALID_PARAM
// Should not get re-requested
prv_fake_receiving_ns_notification(sizeof(ns_notification), (uint8_t*) &ns_notification);
cl_assert_equal_i(s_num_requested_notif_attributes, 4);
cl_assert_equal_i(s_num_ds_notifications_received, 4);
ns_notification.uid = comple_dict_uid;
prv_fake_receiving_ns_notification(sizeof(ns_notification), (uint8_t*) &ns_notification);
cl_assert_equal_i(s_num_requested_notif_attributes, 5);
cl_assert_equal_i(s_num_ds_notifications_received, 5);
}
extern ANCSClientState prv_get_state(void);
extern void prv_check_ancs_alive(void);
void test_ancs__alive_check_disconnection(void) {
prv_check_ancs_alive();
// check we're in the alive check state and we sent a single request
cl_assert_equal_i(prv_get_state(), ANCSClientStateAliveCheck);
cl_assert_equal_i(s_num_requested_notif_attributes, 1);
// simulate a disconnection/reconnection
ancs_handle_service_removed(s_characteristics, NumANCSCharacteristic);
ancs_handle_service_discovered(s_characteristics);
// we should be back in the Idle state
cl_assert_equal_i(prv_get_state(), ANCSClientStateIdle);
// Make sure we can still receive notifications
prv_send_notification((uint8_t *)&s_complete_dict);
cl_assert_equal_i(s_num_requested_notif_attributes, 2);
cl_assert_equal_i(s_num_ds_notifications_received, 1);
cl_assert_equal_i(fake_kernel_services_notifications_ancs_notifications_count(), 1);
}
void test_ancs__notification_dismissal(void) {
NSNotification ns_notification = {
.event_id = EventIDNotificationRemoved,
.event_flags = 0,
.category_id = CategoryIDSocial,
.category_count = 1,
};
// Notification removal without DIS service - notification shouldn't be acted upon
prv_fake_receiving_ns_notification(sizeof(ns_notification), (uint8_t*) &ns_notification);
cl_assert_equal_i(fake_kernel_services_notifications_acted_upon_count(), 0);
// DIS service / iOS 9+ detected - enabling notification dismissal
ancs_handle_ios9_or_newer_detected();
prv_fake_receiving_ns_notification(sizeof(ns_notification), (uint8_t*) &ns_notification);
cl_assert_equal_i(fake_kernel_services_notifications_acted_upon_count(), 1);
}
void test_ancs__notification_parsing(void) {
// Test a recognized app with a duplicated title
// Run multiple times to make sure we're not corrupting the app name cache
for (int i = 0; i < 4; ++i) {
prv_send_notification((uint8_t *)&s_app_name_title_dict);
prv_cmp_last_received_notification(&s_app_name_title_parsed_item);
}
// Test an unrecognized app with a duplicated title
prv_send_notification((uint8_t *)&s_unknown_app_dict);
prv_cmp_last_received_notification(&s_unknown_app_parsed_item);
// Make sure both apps attributes were requested (Messages and FakeApp)
cl_assert_equal_i(s_num_requested_app_attributes, 2);
// Test a recognized app with a unique title
prv_send_notification((uint8_t *)&s_message_dict);
prv_cmp_last_received_notification(&s_message_parsed_item);
// Test an unrecognized app with a unique title
prv_send_notification((uint8_t *)&s_unknown_app_unique_title_dict);
prv_cmp_last_received_notification(&s_unknown_app_unique_title_parsed_item);
// Test an MMS without a caption
prv_send_notification_with_event_flags((uint8_t *)&s_mms_no_caption_dict, EventFlagMultiMedia);
prv_cmp_last_received_notification(&s_mms_no_caption_parsed_item);
// Test an MMS with a caption
prv_send_notification_with_event_flags((uint8_t *)&s_mms_with_caption_dict, EventFlagMultiMedia);
prv_cmp_last_received_notification(&s_mms_with_caption_parsed_item);
// Test a third party notification with the MultiMedia EventFlag
prv_send_notification_with_event_flags((uint8_t *)&s_unknown_app_unique_title_dict, EventFlagMultiMedia);
prv_cmp_last_received_notification(&s_unknown_app_unique_title_parsed_item);
}
// Make sure we send an ANCS_DISCONNECTED event whenever our session goes away
void test_ancs__disconnection(void) {
// Simulate a disconnection/reconnection
ancs_handle_service_removed(s_characteristics, NumANCSCharacteristic);
ancs_handle_service_discovered(s_characteristics);
cl_assert_equal_i(fake_event_get_last().type, PEBBLE_ANCS_DISCONNECTED_EVENT);
fake_event_clear_last();
// If we unexpectedly register another session, make sure we send the event
ancs_handle_service_discovered(s_characteristics);
cl_assert_equal_i(fake_event_get_last().type, PEBBLE_ANCS_DISCONNECTED_EVENT);
fake_event_clear_last();
ancs_invalidate_all_references();
cl_assert_equal_i(fake_event_get_last().type, PEBBLE_ANCS_DISCONNECTED_EVENT);
// Make sure that losing BT altogether sends the event
ancs_destroy();
cl_assert_equal_i(fake_event_get_last().type, PEBBLE_ANCS_DISCONNECTED_EVENT);
fake_event_clear_last();
}
void test_ancs__unrequested_notifications(void) {
prv_fake_receiving_ds_notification(ARRAY_LENGTH(s_complete_dict), (uint8_t*) s_complete_dict);
cl_assert_equal_i(fake_kernel_services_notifications_ancs_notifications_count(), 0);
prv_fake_receiving_ds_notification(ARRAY_LENGTH(s_message_app_info_dict),
(uint8_t *)s_message_app_info_dict);
cl_assert_equal_i(fake_kernel_services_notifications_ancs_notifications_count(), 0);
}
void test_ancs__handle_unexpected_notifications(void) {
NSNotification ns_notification = {
.event_id = EventIDNotificationAdded,
.event_flags = 0,
.category_id = CategoryIDSocial,
.category_count = 1,
.uid = s_get_wrong_data_uid,
};
prv_fake_receiving_ns_notification(sizeof(ns_notification), (uint8_t*) &ns_notification);
cl_assert_equal_i(fake_kernel_services_notifications_ancs_notifications_count(), 0);
// And make sure we get to a state where we can handle more messages
prv_send_notification((uint8_t *)&s_complete_dict);
cl_assert_equal_i(s_num_requested_notif_attributes, 2);
cl_assert_equal_i(s_num_ds_notifications_received, 2);
cl_assert_equal_i(fake_kernel_services_notifications_ancs_notifications_count(), 1);
}
void test_ancs__get_notif_attributes_retry(void) {
s_gatt_client_op_write_should_fail_once = true;
prv_send_notification((uint8_t *)&s_complete_dict);
// We will be successful on the retry (second attempt)
cl_assert_equal_i(fake_kernel_services_notifications_ancs_notifications_count(), 1);
s_gatt_client_op_write_should_fail_unlimited = true;
prv_send_notification((uint8_t *)&s_complete_dict);
// The retry fails and we give up on this one
cl_assert_equal_i(fake_kernel_services_notifications_ancs_notifications_count(), 1);
// And make sure we get to a state where we can handle more messages
s_gatt_client_op_write_should_fail_unlimited = false;
prv_send_notification((uint8_t *)&s_complete_dict);
cl_assert_equal_i(fake_kernel_services_notifications_ancs_notifications_count(), 2);
}
void test_ancs__reset_after_retry(void) {
s_block_event_callback = true;
s_gatt_client_op_write_should_fail_once = true;
prv_send_notification((uint8_t *)&s_complete_dict);
ancs_invalidate_all_references();
cl_assert_equal_i(prv_get_state(), ANCSClientStateIdle);
}
// No Longer Supported
//void test_ancs__should_handle_response_with_multiple_notifications(void) {
//
// NSNotification ns_notification = {
// .event_id = EventIDNotificationAdded,
// .event_flags = 0,
// .category_id = CategoryIDSocial,
// .category_count = 1,
// .uid = 0,
// };
//
// const uint32_t multiple_complete_dict_uid = ((GetNotificationAttributesMsg*)s_multiple_complete_dicts)->notification_uid;
// ns_notification.uid = multiple_complete_dict_uid;
// prv_fake_receiving_ns_notification(sizeof(ns_notification), (uint8_t*) &ns_notification);
// cl_assert_equal_i(s_num_requested_notif_attributes, 1);
// cl_assert_equal_i(s_num_ds_notifications_received, 3);
//
// // The last one was a phone notification but I changed it so it no longer is
// cl_assert_equal_i(fake_kernel_services_notifications_ancs_notifications_count(), 3);
//}
+196
View File
@@ -0,0 +1,196 @@
/*
* 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/persist_map.h"
#include "applib/persist_private.h"
#include "comm/ble/app_profiles/ancs_app_storage.h"
#include "drivers/crc.h"
#include "kernel/services/file.h"
#include "system/filesystem.h"
#include "system/logging.h"
#include "util/size.h"
#include <ctype.h>
#include <stdio.h>
#include <string.h>
// Stubs
////////////////////////////////////
#include "stubs_passert.h"
#include "stubs_serial.h"
#include "stubs_logging.h"
#include "stubs_mutex.h"
#include "stubs_system_reset.h"
#include "stubs_task_watchdog.h"
// Fakes
////////////////////////////////////
#include "fake_session.h"
#include "fake_spi_flash.h"
#include "fake_system_task.h"
#include "fake_new_timer.h"
#include "fake_pbl_malloc.h"
// Tests
////////////////////////////////////
#define TEST_START FILESYSTEM_FILE_TEST_SPACE_BEGIN
#define TEST_SIZE (FILESYSTEM_FILE_TEST_SPACE_END - \
FILESYSTEM_FILE_TEST_SPACE_BEGIN)
void test_ancs_app_storage__initialize(void) {
fake_spi_flash_init(TEST_START, TEST_SIZE);
file_system_format();
file_system_reset();
ancs_app_storage_init();
}
void test_ancs_app_storage__cleanup(void) {
ancs_app_storage_deinit();
}
void test_ancs_app_storage__one_app(void) {
ANCSAppData app_data = {
.bundle_id = "com.getpebble.PebbleApp",
.display_name = "Pebble",
.flags = ANCSAppIsBlocked,
};
app_data.is_meta_changed = true;
ancs_app_storage_save(&app_data);
ANCSAppData app_data_out = { 0 };
ancs_app_storage_load(app_data.bundle_id, &app_data_out);
cl_assert_equal_s(app_data.bundle_id, app_data_out.bundle_id);
cl_assert_equal_i(app_data.flags, app_data_out.flags);
cl_assert_equal_s(app_data.display_name, app_data_out.display_name);
ancs_app_destroy_buffer(&app_data_out);
}
void test_ancs_app_storage__overwrite(void) {
ANCSAppData app_data = {
.bundle_id = "com.getpebble.PebbleApp",
.display_name = "Pebble",
.flags = ANCSAppIsBlocked,
};
app_data.is_meta_changed = true;
ancs_app_storage_save(&app_data);
app_data.display_name = "Pebble 2";
app_data.is_meta_changed = true;
ancs_app_storage_save(&app_data);
ANCSAppData app_data_out = { 0 };
ancs_app_storage_load(app_data.bundle_id, &app_data_out);
cl_assert_equal_s(app_data.bundle_id, app_data_out.bundle_id);
cl_assert_equal_i(app_data.flags, app_data_out.flags);
cl_assert_equal_s(app_data.display_name, app_data_out.display_name);
ancs_app_destroy_buffer(&app_data_out);
}
static uint32_t get_key(const char* bundle_id) {
return legacy_defective_checksum_memory(bundle_id, strlen(bundle_id));
}
void test_ancs_app_storage__hash_collisions(void) {
ANCSAppData app_data = {
.flags = ANCSAppFlagNone,
};
// Courtesy of http://programmers.stackexchange.com/questions/49550/which-hashing-algorithm-is-best-for-uniqueness-and-speed
char* collide_pairs[][2] = {
{ "codding", "gnu" },
{ "exhibiters", "schlager" },
};
for (unsigned int i = 0; i < ARRAY_LENGTH(collide_pairs); ++i) {
for (unsigned int j = 0; j < 2; ++j) {
char* name = collide_pairs[i][j];
app_data.bundle_id = name;
app_data.display_name = name;
app_data.is_meta_changed = true;
uint32_t key = get_key(name);
PBL_LOG(LOG_LEVEL_DEBUG, "name: %s, key: %u", name, (unsigned) key);
ancs_app_storage_save(&app_data);
}
}
ANCSAppData app_data_out = { 0 };
for (unsigned int i = 0; i < ARRAY_LENGTH(collide_pairs); ++i) {
for (unsigned int j = 0; j < 2; ++j) {
char* name = collide_pairs[i][j];
app_data.bundle_id = name;
app_data.display_name = name;
ancs_app_storage_load(name, &app_data_out);
cl_assert_equal_s(app_data.bundle_id, app_data_out.bundle_id);
cl_assert_equal_i(app_data.flags, app_data_out.flags);
cl_assert_equal_s(app_data.display_name, app_data_out.display_name);
ancs_app_destroy_buffer(&app_data_out);
}
}
}
void test_ancs_app_storage__iter(void) {
ANCSAppData apps[] = {
{ .bundle_id = "com.apple.MobileSMS", .display_name = "Messages" },
{ .bundle_id = "com.apple.facetime", .display_name = "FaceTime" },
{ .bundle_id = "com.facebook.Messenger", .display_name = "Facebook" },
{ .bundle_id = "com.atebits.Tweetie2", .display_name = "Twitter" },
{ .bundle_id = "com.apple.mobilecal", .display_name = "Calender" },
{ .bundle_id = "com.blackberry.bbm1", .display_name = "BBM" },
{ .bundle_id = "net.whatsapp.WhatsApp", .display_name = "WhatsApp" },
{ .bundle_id = "com.toyopagroup.picaboo", .display_name = "Snapchat" },
{ .bundle_id = "com.kik.chat", .display_name = "Kik Chat" },
{ .bundle_id = "com.apple.mobilemail", .display_name = "Mail" },
{ .bundle_id = "com.yahoo.Aerogram", .display_name = "YMail" },
{ .bundle_id = "co.inboxapp.inbox", .display_name = "Inbox" },
{ .bundle_id = "com.google.Gmail", .display_name = "Gmail" },
};
for (unsigned int i = 0; i < ARRAY_LENGTH(apps); ++i) {
apps[i].is_meta_changed = true;
apps[i].is_local_changed = true;
ancs_app_storage_save(&apps[i]);
}
ANCSAppData app_data_out = { 0 };
for (unsigned int i = 0; i < ARRAY_LENGTH(apps); ++i) {
PBL_LOG(LOG_LEVEL_DEBUG, "i: %d, name: %s", i, apps[i].bundle_id);
ancs_app_storage_load(apps[i].bundle_id, &app_data_out);
cl_assert_equal_s(apps[i].bundle_id, app_data_out.bundle_id);
cl_assert_equal_i(apps[i].flags, app_data_out.flags);
cl_assert_equal_s(apps[i].display_name, app_data_out.display_name);
ancs_app_destroy_buffer(&app_data_out);
}
ancs_app_storage_iter_begin();
for (unsigned int i = 0; ancs_app_storage_next(&app_data_out); ++i) {
PBL_LOG(LOG_LEVEL_DEBUG, "i: %d, name: %s, name_out: %s", i, apps[i].bundle_id, app_data_out.bundle_id);
cl_assert_equal_s(apps[i].bundle_id, app_data_out.bundle_id);
}
}
+181
View File
@@ -0,0 +1,181 @@
/*
* 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 "comm/ble/kernel_le_client/ancs/ancs.h"
#include "comm/ble/kernel_le_client/ancs/ancs_types.h"
#include "comm/ble/kernel_le_client/ancs/ancs_util.h"
#include "util/buffer.h"
#include "clar.h"
#include <string.h>
// Stubs
///////////////////////////////////////////////////////////
#include "stubs_analytics.h"
#include "stubs_ble.h"
#include "stubs_bt_stack.h"
#include "stubs_logging.h"
#include "stubs_serial.h"
#include "stubs_passert.h"
#include "stubs_queue.h"
#include "stubs_mutex.h"
#include "stubs_system_reset.h"
#include "stubs_task_watchdog.h"
#include "stubs_pbl_malloc.h"
#include "stubs_reconnect.h"
// Test data
///////////////////////////////////////////////////////////
#include "ancs_test_data.h"
// Tests
///////////////////////////////////////////////////////////
void test_ancs_util__should_parse_complete_notif_attr_dict(void) {
bool error = false;
cl_assert(ancs_util_is_complete_notif_attr_response(s_complete_dict, sizeof(s_complete_dict), &error));
cl_assert(!error);
}
void test_ancs_util__should_identify_missing_attribute(void) {
bool error = false;
cl_assert(!ancs_util_is_complete_notif_attr_response(s_missing_last_attribute, sizeof(s_missing_last_attribute),
&error));
cl_assert(!error);
}
void test_ancs_util__should_identify_invalid_attr_length(void) {
bool error = false;
cl_assert(!ancs_util_is_complete_notif_attr_response(s_invalid_attribute_length, sizeof(s_invalid_attribute_length),
&error));
cl_assert(error);
}
void test_ancs_util__should_parse_incomplete_last_attribute(void) {
bool error = false;
cl_assert(!ancs_util_is_complete_notif_attr_response(s_chunked_dict_part_one, sizeof(s_chunked_dict_part_one),
&error));
cl_assert(!error);
}
void test_ancs_util__should_not_parse_malformed_notif_attr_dict(void) {
bool error = false;
cl_assert(!ancs_util_is_complete_notif_attr_response(s_chunked_dict_part_two, sizeof(s_chunked_dict_part_two),
&error));
cl_assert(error);
}
void test_ancs_util__should_extract_dict_from_buffer(void) {
static const char* expected_message = "This is a very complicated case, Maude. You know, a lotta ins, lotta outs, lotta what-have-you's. And, uh, lotta strands to keep in my head, man. Lotta strands in old Duder's head. Luckily I'm adherin";
int bytes_written;
Buffer* b = buffer_create(500);
bytes_written = buffer_add(b, s_chunked_dict_part_one, sizeof(s_chunked_dict_part_one));
cl_assert_equal_i(bytes_written, sizeof(s_chunked_dict_part_one));
bytes_written = buffer_add(b, s_chunked_dict_part_two, sizeof(s_chunked_dict_part_two));
cl_assert_equal_i(bytes_written, sizeof(s_chunked_dict_part_two));
bool error = false;
cl_assert(ancs_util_is_complete_notif_attr_response(b->data, b->bytes_written, &error));
cl_assert(!error);
ANCSAttribute* attr_ptrs[NUM_FETCHED_NOTIF_ATTRIBUTES];
// Only pass the attribute data to ancs_util_get_attr_ptrs:
const GetNotificationAttributesMsg *msg =
(const GetNotificationAttributesMsg*) b->data;
const size_t attributes_length = b->bytes_written - sizeof(*msg);
cl_assert(ancs_util_get_attr_ptrs(msg->attributes_data, attributes_length, s_fetched_notif_attributes,
NUM_FETCHED_NOTIF_ATTRIBUTES, attr_ptrs, &error));
cl_assert(!error);
ANCSAttribute* message = (ANCSAttribute*) attr_ptrs[3];
cl_assert(strncmp((char*) message->value, expected_message, message->length) == 0);
free(b);
}
void test_ancs_util__should_detect_duplicate_message(void) {
#if(0)
// Extract the dict
static const char* expected_message = "This is a very complicated case, Maude. You know, a lotta ins, lotta outs, lotta what-have-you's. And, uh, lotta strands to keep in my head, man. Lotta strands in old Duder's head. Luckily I'm adherin";
ancs_util_reset_notification_cache();
Buffer* b = buffer_create(500);
cl_assert_equal_i(buffer_add(b, s_chunked_dict_part_one, sizeof(s_chunked_dict_part_one)), sizeof(s_chunked_dict_part_one));
cl_assert_equal_i(buffer_add(b, s_chunked_dict_part_two, sizeof(s_chunked_dict_part_two)), sizeof(s_chunked_dict_part_two));
bool error = false;
cl_assert(ancs_util_is_complete_notif_attr_response(b->data, b->bytes_written, &error));
cl_assert(!error);
ANCSAttribute *attr_ptrs[NUM_FETCHED_NOTIF_ATTRIBUTES];
const GetNotificationAttributesMsg *msg =
(const GetNotificationAttributesMsg*) b->data;
const size_t attributes_length = b->bytes_written - sizeof(*msg);
cl_assert(ancs_util_get_attr_ptrs(msg->attributes_data, attributes_length, s_fetched_notif_attributes,
NUM_FETCHED_NOTIF_ATTRIBUTES, attr_ptrs, &error));
cl_assert(!error);
ANCSAttribute* message = (ANCSAttribute*) attr_ptrs[3];
cl_assert(strncmp((char*) message->value, expected_message, message->length) == 0);
// Ensure the dupe is detected
bool was_added_to_cache = ancs_util_cache_notification(
attr_ptrs[FetchedAttributeIndexAppID],
attr_ptrs[FetchedAttributeIndexTitle],
attr_ptrs[FetchedAttributeIndexSubtitle],
attr_ptrs[FetchedAttributeIndexMessage],
attr_ptrs[FetchedAttributeIndexDate]
);
cl_assert(was_added_to_cache);
was_added_to_cache = ancs_util_cache_notification(
attr_ptrs[FetchedAttributeIndexAppID],
attr_ptrs[FetchedAttributeIndexTitle],
attr_ptrs[FetchedAttributeIndexSubtitle],
attr_ptrs[FetchedAttributeIndexMessage],
attr_ptrs[FetchedAttributeIndexDate]
);
cl_assert(was_added_to_cache == false);
// Ensure the dupe is no longer detected
ancs_util_reset_notification_cache();
was_added_to_cache = ancs_util_cache_notification(
attr_ptrs[FetchedAttributeIndexAppID],
attr_ptrs[FetchedAttributeIndexTitle],
attr_ptrs[FetchedAttributeIndexSubtitle],
attr_ptrs[FetchedAttributeIndexMessage],
attr_ptrs[FetchedAttributeIndexDate]
);
cl_assert(was_added_to_cache);
free(b);
#endif
}
+216
View File
@@ -0,0 +1,216 @@
/*
* 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 "comm/ble/gap_le_connection.h"
#include "comm/bt_conn_mgr.h"
#include "comm/bt_conn_mgr_impl.h"
#include "services/common/regular_timer.h"
// Fakes
#include "fake_gap_le_connect_params.h"
#include "fake_new_timer.h"
#include "fake_rtc.h"
#include "fake_system_task.h"
#include "fake_pbl_malloc.h"
// Stubs
#include "stubs_bluetopia_interface.h"
#include "stubs_bt_lock.h"
#include "stubs_logging.h"
#include "stubs_mutex.h"
#include "stubs_passert.h"
extern void conn_mgr_handle_desired_state_granted(GAPLEConnection *hdl,
ResponseTimeState granted_state);
// Stubs
/////
bool gap_le_connection_is_valid(const GAPLEConnection *conn) {
return (conn != NULL);
}
GAPLEConnection *gap_le_connection_any(void) {
return NULL;
}
void prv_regular_timer_spend_seconds(uint32_t seconds) {
for (uint32_t i = 0; i < seconds; ++i) {
fake_rtc_increment_ticks(RTC_TICKS_HZ);
regular_timer_fire_seconds(1);
// bt_conn_mgr offloads the callback to KernelBG
fake_system_task_callbacks_invoke_pending();
}
}
void launcher_task_add_callback(void (*callback)(void *data), void *data) {
callback(data);
}
// Tests
///////////////////////////////////////////////////////////
static int s_granted_count;
static GAPLEConnection s_hdl;
void test_bt_conn_mgr__initialize(void) {
s_granted_count = 0;
fake_rtc_init(0, 0);
regular_timer_init();
fake_gap_le_connect_params_init();
s_hdl.conn_mgr_info = bt_conn_mgr_info_init();
}
void test_bt_conn_mgr__cleanup(void) {
regular_timer_deinit();
}
void test_bt_conn_mgr__ble_latency_mgr(void) {
// 1 consumer at fastest rate should result in fastest rate getting scheduled
conn_mgr_set_ble_conn_response_time(
&s_hdl, BtConsumerLeServiceDiscovery, ResponseTimeMin, 100);
uint16_t secs_to_wait;
ResponseTimeState state;
state = conn_mgr_get_latency_for_le_connection(&s_hdl, &secs_to_wait);
cl_assert_equal_i(state, ResponseTimeMin);
cl_assert_equal_i(secs_to_wait, 100);
cl_assert_equal_i(fake_gap_le_connect_params_get_last_requested(), ResponseTimeMin);
// another consumer at lower rate should not have any effect
fake_gap_le_connect_params_reset_last_requested();
conn_mgr_set_ble_conn_response_time(
&s_hdl, BtConsumerUnitTests, ResponseTimeMiddle, 30);
state = conn_mgr_get_latency_for_le_connection(&s_hdl, &secs_to_wait);
cl_assert_equal_i(state, ResponseTimeMin);
cl_assert_equal_i(secs_to_wait, 100);
cl_assert_equal_i(fake_gap_le_connect_params_get_last_requested(), ResponseTimeInvalid);
// removing the fastest consumer should result in the next fastest being scheduled, but only
// after an "inactivity timeout":
conn_mgr_set_ble_conn_response_time(
&s_hdl, BtConsumerLeServiceDiscovery, ResponseTimeMax, 0);
state = conn_mgr_get_latency_for_le_connection(&s_hdl, &secs_to_wait);
cl_assert_equal_i(state, ResponseTimeMin);
cl_assert_equal_i(secs_to_wait, BT_CONN_MGR_INACTIVITY_TIMEOUT_SECS);
cl_assert_equal_i(fake_gap_le_connect_params_get_last_requested(), ResponseTimeInvalid);
prv_regular_timer_spend_seconds(BT_CONN_MGR_INACTIVITY_TIMEOUT_SECS);
state = conn_mgr_get_latency_for_le_connection(&s_hdl, &secs_to_wait);
cl_assert_equal_i(state, ResponseTimeMiddle);
cl_assert_equal_i(secs_to_wait, 30 - BT_CONN_MGR_INACTIVITY_TIMEOUT_SECS);
cl_assert_equal_i(fake_gap_le_connect_params_get_last_requested(), ResponseTimeMiddle);
// removing all consumers we should fall back to slowest interval, but only
// after an "inactivity timeout":
fake_gap_le_connect_params_reset_last_requested();
conn_mgr_set_ble_conn_response_time(
&s_hdl, BtConsumerUnitTests, ResponseTimeMax, 0);
prv_regular_timer_spend_seconds(BT_CONN_MGR_INACTIVITY_TIMEOUT_SECS);
state = conn_mgr_get_latency_for_le_connection(&s_hdl, &secs_to_wait);
cl_assert_equal_i(state, ResponseTimeMax);
cl_assert_equal_i(fake_gap_le_connect_params_get_last_requested(), ResponseTimeMax);
// if nothing else is scheduled, middle rate should get picked up right away
conn_mgr_set_ble_conn_response_time(
&s_hdl, BtConsumerUnitTests, ResponseTimeMiddle, 30);
state = conn_mgr_get_latency_for_le_connection(&s_hdl, &secs_to_wait);
cl_assert_equal_i(state, ResponseTimeMiddle);
cl_assert_equal_i(secs_to_wait, 30);
cl_assert_equal_i(fake_gap_le_connect_params_get_last_requested(), ResponseTimeMiddle);
// higher rate should take over lower rate already scheduled
conn_mgr_set_ble_conn_response_time(
&s_hdl, BtConsumerLeServiceDiscovery, ResponseTimeMin, 25);
state = conn_mgr_get_latency_for_le_connection(&s_hdl, &secs_to_wait);
cl_assert_equal_i(state, ResponseTimeMin);
cl_assert_equal_i(secs_to_wait, 25);
cl_assert_equal_i(fake_gap_le_connect_params_get_last_requested(), ResponseTimeMin);
// two requests at same high rate, longest time should be selected as timeout
fake_gap_le_connect_params_reset_last_requested();
conn_mgr_set_ble_conn_response_time(
&s_hdl, BtConsumerUnitTests, ResponseTimeMin, 250);
state = conn_mgr_get_latency_for_le_connection(&s_hdl, &secs_to_wait);
cl_assert_equal_i(state, ResponseTimeMin);
cl_assert_equal_i(secs_to_wait, 250);
cl_assert_equal_i(fake_gap_le_connect_params_get_last_requested(), ResponseTimeInvalid);
bt_conn_mgr_info_deinit(&s_hdl.conn_mgr_info);
}
static void prv_granted_handler(void) {
++s_granted_count;
}
void test_bt_conn_mgr__granted_handler_request_max_no_existing_node(void) {
fake_gap_le_connect_params_set_actual_state(ResponseTimeMax);
conn_mgr_set_ble_conn_response_time_ext(&s_hdl, BtConsumerLeServiceDiscovery, ResponseTimeMax, 1,
prv_granted_handler);
// Expect granted handler to be called immediately:
cl_assert_equal_i(s_granted_count, 1);
}
void test_bt_conn_mgr__granted_handler_request_existing(void) {
fake_gap_le_connect_params_set_actual_state(ResponseTimeMax);
conn_mgr_set_ble_conn_response_time_ext(&s_hdl, BtConsumerLeServiceDiscovery, ResponseTimeMin, 1,
prv_granted_handler);
cl_assert_equal_i(s_granted_count, 0);
// Simulate that the requested state takes effect:
fake_gap_le_connect_params_set_actual_state(ResponseTimeMin);
conn_mgr_handle_desired_state_granted(&s_hdl, ResponseTimeMin);
cl_assert_equal_i(s_granted_count, 1);
conn_mgr_set_ble_conn_response_time_ext(&s_hdl, BtConsumerLeServiceDiscovery, ResponseTimeMin, 1,
prv_granted_handler);
cl_assert_equal_i(s_granted_count, 2);
conn_mgr_set_ble_conn_response_time_ext(&s_hdl, BtConsumerLeServiceDiscovery, ResponseTimeMiddle,
1, prv_granted_handler);
cl_assert_equal_i(s_granted_count, 3);
conn_mgr_set_ble_conn_response_time_ext(&s_hdl, BtConsumerLeServiceDiscovery, ResponseTimeMax,
1, prv_granted_handler);
cl_assert_equal_i(s_granted_count, 4);
}
void test_bt_conn_mgr__request_max_time_while_no_requests_are_running(void) {
uint16_t secs_to_wait;
ResponseTimeState state;
// Always start off with ResponseTimeMax:
state = conn_mgr_get_latency_for_le_connection(&s_hdl, &secs_to_wait);
cl_assert_equal_i(state, ResponseTimeMax);
// Requesting ResponseTimeMax should have no effect:
conn_mgr_set_ble_conn_response_time(&s_hdl, BtConsumerLeServiceDiscovery, ResponseTimeMax, 1);
state = conn_mgr_get_latency_for_le_connection(&s_hdl, &secs_to_wait);
cl_assert_equal_i(state, ResponseTimeMax);
// Not even after waiting 10 seconds:
prv_regular_timer_spend_seconds(10);
state = conn_mgr_get_latency_for_le_connection(&s_hdl, &secs_to_wait);
cl_assert_equal_i(state, ResponseTimeMax);
bt_conn_mgr_info_deinit(&s_hdl.conn_mgr_info);
}
+955
View File
@@ -0,0 +1,955 @@
/*
* 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 "comm/ble/gap_le_advert.h"
#include "comm/ble/gap_le_connection.h"
#include "services/common/regular_timer.h"
#include "util/size.h"
#include "clar.h"
// Fakes
///////////////////////////////////////////////////////////
#include "fake_GAPAPI.h"
#include "fake_new_timer.h"
#include "fake_rtc.h"
#include "fake_system_task.h"
#include "fake_pbl_malloc.h"
// Stubs
///////////////////////////////////////////////////////////
#include "stubs_analytics.h"
#include "stubs_bluetopia_interface.h"
#include "stubs_bt_lock.h"
#include "stubs_gatt_client_discovery.h"
#include "stubs_gatt_client_subscriptions.h"
#include "stubs_logging.h"
#include "stubs_mutex.h"
#include "stubs_passert.h"
#include "stubs_prompt.h"
void gap_le_connect_bluetopia_connection_callback(uint32_t stack_id,
GAP_LE_Event_Data_t* event_data,
uint32_t CallbackParameter) {
}
bool static s_is_connected_as_slave = false;
bool gap_le_connect_is_connected_as_slave(void) {
return s_is_connected_as_slave;
}
void ble_legacy_discovery_enable(uint32_t timeout_secs) {
}
void gap_le_slave_reconnect_stop(void) {
}
void gap_le_slave_reconnect_start(void) {
}
void gatt_service_changed_server_cleanup_by_connection(GAPLEConnection *connection) {
}
int HCI_LE_Set_Advertise_Enable(unsigned int BluetoothStackID, ...) {
return (0);
}
void launcher_task_add_callback(void (*callback)(void *data), void *data) {
callback(data);
}
static uint32_t s_unscheduled_cb_count;
static void * s_unscheduled_cb_data = "Callback Data";
static GAPLEAdvertisingJobRef s_unscheduled_job;
static bool s_unscheduled_completed;
static void unscheduled_callback(GAPLEAdvertisingJobRef job,
bool completed,
void *cb_data) {
s_unscheduled_job = job;
++s_unscheduled_cb_count;
s_unscheduled_completed = completed;
cl_assert_equal_p(cb_data, s_unscheduled_cb_data);
}
void test_gap_le_advert__initialize(void) {
fake_GAPAPI_init();
s_unscheduled_cb_count = 0;
s_unscheduled_job = NULL;
s_unscheduled_completed = false;
// This bypasses the work-around for the CC2564 advertising bug, that pauses the round-robinning
// through scheduled advertisment jobs:
s_is_connected_as_slave = true;
regular_timer_init();
gap_le_advert_init();
}
void test_gap_le_advert__cleanup(void) {
gap_le_advert_deinit();
// Make sure deinit did disable advertising and clean up timer:
cl_assert(!gap_le_is_advertising_enabled());
cl_assert_equal_i(regular_timer_seconds_count(), 0);
regular_timer_deinit();
}
//! Helper to assert whether a piece of ad_data is set to the controller.
//! For convenience, C strings are used (while in reality it can be arbitrary
//! binary data).
#define assert_ad_data(ad_data_cstring) \
{ \
Advertising_Data_t ad_data_out; \
const size_t data_length = strlen(ad_data_cstring) + 1; \
cl_assert_equal_i(gap_le_get_advertising_data(&ad_data_out), data_length); \
cl_assert_equal_s((const char *) &ad_data_out, ad_data_cstring); \
}
//! Helper to create BLEAdData from C strings.
//! In reality, people will use ble_ad_create() and the ble_ad_set_* functions,
//! but that's part of another test.
static BLEAdData *create_ad(const char ad_data[],
const char scan_resp_data[]) {
const size_t ad_data_length = ad_data ? strlen(ad_data) + 1 : 0;
const size_t scan_resp_data_length =
scan_resp_data ? strlen(scan_resp_data) + 1 : 0;
BLEAdData *ad = (BLEAdData *) malloc(sizeof(BLEAdData) +
ad_data_length +
scan_resp_data_length);
ad->ad_data_length = ad_data_length;
ad->scan_resp_data_length = scan_resp_data_length;
if (ad_data_length) {
strncpy((char *) ad->data,
ad_data, ad_data_length);
}
if (scan_resp_data_length) {
strncpy((char *) ad->data + ad_data_length,
scan_resp_data, scan_resp_data_length);
}
return ad;
}
void test_gap_le_advert__single_job(void) {
const char ad_data[] = "ad data";
const char scan_resp_data[] = "scan resp data";
BLEAdData *ad = create_ad(ad_data, scan_resp_data);
GAPLEAdvertisingJobTerm advert_term = {
.min_interval_slots = 160,
.max_interval_slots = 241,
.duration_secs = 10,
};
GAPLEAdvertisingJobRef job;
job = gap_le_advert_schedule(ad,
&advert_term,
sizeof(advert_term)/sizeof(GAPLEAdvertisingJobTerm),
unscheduled_callback, s_unscheduled_cb_data, 0);
cl_assert(job);
// Since there was nothing scheduled, expect that the ad data is set to the
// controller immediately:
cl_assert(gap_le_is_advertising_enabled());
// Check that the ad data that is passed to the controller is the same that
// was given when calling the API:
assert_ad_data(ad_data);
// Check that the scan resp data that is passed to the controller is the same
// that was given when calling the API:
Scan_Response_Data_t scan_resp_data_out;
cl_assert_equal_i(gap_le_get_scan_response_data(&scan_resp_data_out),
sizeof(scan_resp_data));
cl_assert(memcmp(&scan_resp_data_out, scan_resp_data,
sizeof(scan_resp_data)) == 0);
// Expect one regular timer to be running for advertisements:
cl_assert_equal_i(regular_timer_seconds_count(), 1);
// Unschedule callback should not have been called:
cl_assert_equal_i(s_unscheduled_cb_count, 0);
// Unschedule and expect not to be advertising any more:
gap_le_advert_unschedule(job);
cl_assert(!gap_le_is_advertising_enabled());
// Unschedule callback should have been called once:
cl_assert_equal_i(s_unscheduled_cb_count, 1);
cl_assert_equal_p(s_unscheduled_job, job);
cl_assert_equal_b(s_unscheduled_completed, false);
// Expect no advertisement timer
cl_assert_equal_i(regular_timer_seconds_count(), 0);
free(ad);
}
void test_gap_le_advert__single_job_multiple_terms_silence_and_loop_around(void) {
BLEAdData *ad = create_ad("yo", NULL);
GAPLEAdvertisingJobTerm advert_terms[] =
{
{
.min_interval_slots = 160,
.max_interval_slots = 240,
.duration_secs = 1,
},
{
.min_interval_slots = 320,
.max_interval_slots = 480,
.duration_secs = 1,
},
{
.min_interval_slots = GAPLE_ADVERTISING_SILENCE_INTERVAL_SLOTS,
.max_interval_slots = GAPLE_ADVERTISING_SILENCE_INTERVAL_SLOTS,
.duration_secs = 1,
},
{
.duration_secs = GAPLE_ADVERTISING_DURATION_LOOP_AROUND,
.loop_around_index = 1,
},
};
GAPLEAdvertisingJobRef job;
job = gap_le_advert_schedule(ad,
advert_terms,
sizeof(advert_terms)/sizeof(GAPLEAdvertisingJobTerm),
unscheduled_callback, s_unscheduled_cb_data, 0);
cl_assert(job);
// First term:
cl_assert(gap_le_is_advertising_enabled());
assert_ad_data("yo");
gap_le_assert_advertising_interval(advert_terms[0].min_interval_slots,
advert_terms[0].max_interval_slots);
regular_timer_fire_seconds(1);
// Second term:
cl_assert(gap_le_is_advertising_enabled());
assert_ad_data("yo");
gap_le_assert_advertising_interval(advert_terms[1].min_interval_slots,
advert_terms[1].max_interval_slots);
regular_timer_fire_seconds(1);
// Silent term:
cl_assert(!gap_le_is_advertising_enabled());
regular_timer_fire_seconds(1);
// Looped around to second term (index==1):
cl_assert(gap_le_is_advertising_enabled());
assert_ad_data("yo");
gap_le_assert_advertising_interval(advert_terms[1].min_interval_slots,
advert_terms[1].max_interval_slots);
cl_assert_equal_i(s_unscheduled_cb_count, 0);
}
void test_gap_le_advert__single_job_multiple_terms(void) {
const char ad_data[] = "ad data";
const char scan_resp_data[] = "scan resp data";
BLEAdData *ad = create_ad(ad_data, scan_resp_data);
GAPLEAdvertisingJobTerm advert_terms[3] =
{
{
.min_interval_slots = 160,
.max_interval_slots = 240,
.duration_secs = 4,
},
{
.min_interval_slots = 320,
.max_interval_slots = 400,
.duration_secs = 4,
},
{
.min_interval_slots = 640,
.max_interval_slots = 800,
.duration_secs = 4,
},
};
GAPLEAdvertisingJobRef job;
job = gap_le_advert_schedule(ad,
advert_terms,
sizeof(advert_terms)/sizeof(GAPLEAdvertisingJobTerm),
unscheduled_callback, s_unscheduled_cb_data, 0);
cl_assert(job);
// Since there was nothing scheduled, expect that the ad data is set to the
// controller immediately:
cl_assert(gap_le_is_advertising_enabled());
// Check that the ad data that is passed to the controller is the same that
// was given when calling the API:
assert_ad_data(ad_data);
// Check that the scan resp data that is passed to the controller is the same
// that was given when calling the API:
Scan_Response_Data_t scan_resp_data_out;
cl_assert_equal_i(gap_le_get_scan_response_data(&scan_resp_data_out),
sizeof(scan_resp_data));
cl_assert(memcmp(&scan_resp_data_out, scan_resp_data,
sizeof(scan_resp_data)) == 0);
// Expect one regular timer to be running for adverts:
cl_assert_equal_i(regular_timer_seconds_count(), 1);
// Unschedule callback should not have been called:
cl_assert_equal_i(s_unscheduled_cb_count, 0);
// Make sure the all the terms in the job are run:
for (int term = 0; term < ARRAY_LENGTH(advert_terms); ++term) {
for (int second_tick = 0; second_tick < 4; ++second_tick) {
cl_assert_equal_i(s_unscheduled_cb_count, 0);
assert_ad_data("ad data");
gap_le_assert_advertising_interval(advert_terms[term].min_interval_slots,
advert_terms[term].max_interval_slots);
regular_timer_fire_seconds(1);
}
}
cl_assert(!gap_le_is_advertising_enabled());
cl_assert_equal_i(s_unscheduled_cb_count, 1);
cl_assert_equal_p(s_unscheduled_job, job);
// Expect no advertisement timer
cl_assert_equal_i(regular_timer_seconds_count(), 0);
free(ad);
}
void test_gap_le_advert__job_round_robin(void) {
GAPLEAdvertisingJobTerm advert_term = {
.min_interval_slots = 160,
.max_interval_slots = 320,
.duration_secs = GAPLE_ADVERTISING_DURATION_INFINITE,
};
// Schedule infinite job "A":
BLEAdData *ad_a = create_ad("A", NULL);
GAPLEAdvertisingJobRef infinite_job_a;
infinite_job_a = gap_le_advert_schedule(ad_a,
&advert_term, sizeof(advert_term)/sizeof(GAPLEAdvertisingJobTerm),
unscheduled_callback, s_unscheduled_cb_data, 0);
cl_assert(infinite_job_a);
assert_ad_data("A");
// Schedule infinite job "B":
BLEAdData *ad_b = create_ad("B", NULL);
GAPLEAdvertisingJobRef infinite_job_b;
infinite_job_b = gap_le_advert_schedule(ad_b,
&advert_term, sizeof(advert_term)/sizeof(GAPLEAdvertisingJobTerm),
unscheduled_callback, s_unscheduled_cb_data, 0);
cl_assert(infinite_job_b);
assert_ad_data("B");
// Round-robin 10 times:
for (int i = 0; i < 10; ++i) {
regular_timer_fire_seconds(1);
assert_ad_data("A");
regular_timer_fire_seconds(1);
assert_ad_data("B");
}
// Introduce non-infinite job "C" for 10 seconds:
advert_term.duration_secs = 10;
BLEAdData *ad_c = create_ad("C", NULL);
GAPLEAdvertisingJobRef infinite_job_c;
infinite_job_c = gap_le_advert_schedule(ad_c,
&advert_term, sizeof(advert_term)/sizeof(GAPLEAdvertisingJobTerm),
unscheduled_callback, s_unscheduled_cb_data, 0);
cl_assert(infinite_job_c);
assert_ad_data("C");
// Round-robin 5 times:
for (int i = 0; i < 5; ++i) {
regular_timer_fire_seconds(1);
assert_ad_data("A");
regular_timer_fire_seconds(1);
assert_ad_data("B");
regular_timer_fire_seconds(1);
assert_ad_data("C");
}
// Introduce a second non-infinite job "D" for 10 seconds:
BLEAdData *ad_d = create_ad("D", NULL);
GAPLEAdvertisingJobRef infinite_job_d;
infinite_job_d = gap_le_advert_schedule(ad_d,
&advert_term, sizeof(advert_term)/sizeof(GAPLEAdvertisingJobTerm),
unscheduled_callback, s_unscheduled_cb_data, 0);
// It should get immediate air-time for one cycle:
cl_assert(infinite_job_d);
assert_ad_data("D");
// Round-robin 4 times:
for (int i = 0; i < 4; ++i) {
regular_timer_fire_seconds(1);
assert_ad_data("A");
regular_timer_fire_seconds(1);
assert_ad_data("B");
regular_timer_fire_seconds(1);
assert_ad_data("C");
regular_timer_fire_seconds(1);
assert_ad_data("D");
}
// Schedule yet another infinite job "E".
BLEAdData *ad_e = create_ad("E", NULL);
advert_term.duration_secs = GAPLE_ADVERTISING_DURATION_INFINITE;
GAPLEAdvertisingJobRef infinite_job_e;
infinite_job_e = gap_le_advert_schedule(ad_e,
&advert_term, sizeof(advert_term)/sizeof(GAPLEAdvertisingJobTerm),
unscheduled_callback, s_unscheduled_cb_data, 0);
cl_assert(infinite_job_e);
// Infinite jobs are equal in priority to finite jobs, so it should get immediate air-time for one cycle:
assert_ad_data("E");
// No jobs should been have been unscheduled:
cl_assert_equal_i(s_unscheduled_cb_count, 0);
// This is the last round for "C":
regular_timer_fire_seconds(1);
assert_ad_data("A");
regular_timer_fire_seconds(1);
assert_ad_data("B");
regular_timer_fire_seconds(1);
assert_ad_data("C");
regular_timer_fire_seconds(1);
assert_ad_data("D");
// One job ("C") should been have been unscheduled:
cl_assert_equal_i(s_unscheduled_cb_count, 1);
// Round-robin 5 times:
for (int i = 0; i < 5; ++i) {
regular_timer_fire_seconds(1);
assert_ad_data("E");
regular_timer_fire_seconds(1);
assert_ad_data("A");
regular_timer_fire_seconds(1);
assert_ad_data("B");
regular_timer_fire_seconds(1);
assert_ad_data("D");
}
// "D" should be done now, so expect only infinite jobs to get air-time again:
for (int i = 0; i < 10; ++i) {
regular_timer_fire_seconds(1);
assert_ad_data("E");
regular_timer_fire_seconds(1);
assert_ad_data("A");
regular_timer_fire_seconds(1);
assert_ad_data("B");
// Jobs "C" and "D" should have been unscheduled, infinite jobs should never
// get unscheduled:
cl_assert_equal_i(s_unscheduled_cb_count, 2);
cl_assert_equal_b(s_unscheduled_completed, true);
}
free(ad_a);
free(ad_b);
free(ad_c);
free(ad_d);
}
// Tests that the adv data is set when a job goes from a silent term to a non-silent one, and
// another job's data was previously advertised.
void test_gap_le_advert__data_set_after_silent_term(void) {
GAPLEAdvertisingJobTerm advert_terms_a[] =
{
{
.min_interval_slots = 160,
.max_interval_slots = 320,
.duration_secs = 1,
},
};
GAPLEAdvertisingJobTerm advert_terms_b[] =
{
{
.min_interval_slots = GAPLE_ADVERTISING_SILENCE_INTERVAL_SLOTS,
.max_interval_slots = GAPLE_ADVERTISING_SILENCE_INTERVAL_SLOTS,
.duration_secs = 1,
},
{
.min_interval_slots = 32,
.max_interval_slots = 64,
.duration_secs = 1,
},
};
// Schedule job "A":
BLEAdData *ad_a = create_ad("A", NULL);
GAPLEAdvertisingJobRef job_a;
job_a = gap_le_advert_schedule(ad_a,
advert_terms_a,
sizeof(advert_terms_a)/sizeof(GAPLEAdvertisingJobTerm),
unscheduled_callback, s_unscheduled_cb_data, 0);
cl_assert(job_a);
assert_ad_data("A");
regular_timer_fire_seconds(1);
cl_assert_equal_b(gap_le_is_advertising_enabled(), false);
cl_assert_equal_p(s_unscheduled_job, job_a);
// Schedule job "B":
BLEAdData *ad_b = create_ad("B", NULL);
GAPLEAdvertisingJobRef job_b;
job_b = gap_le_advert_schedule(ad_b,
advert_terms_b,
sizeof(advert_terms_b)/sizeof(GAPLEAdvertisingJobTerm),
unscheduled_callback, s_unscheduled_cb_data, 0);
cl_assert(job_b);
cl_assert_equal_b(gap_le_is_advertising_enabled(), false);
regular_timer_fire_seconds(1);
cl_assert_equal_b(gap_le_is_advertising_enabled(), true);
assert_ad_data("B");
}
void test_gap_le_advert__round_robin_two_jobs_incl_silent_terms(void) {
GAPLEAdvertisingJobTerm advert_terms_a[] =
{
{
.min_interval_slots = 160,
.max_interval_slots = 320,
.duration_secs = 1,
},
{
.min_interval_slots = 480,
.max_interval_slots = 960,
.duration_secs = 1,
},
{
.min_interval_slots = GAPLE_ADVERTISING_SILENCE_INTERVAL_SLOTS,
.max_interval_slots = GAPLE_ADVERTISING_SILENCE_INTERVAL_SLOTS,
.duration_secs = 1,
},
{
.min_interval_slots = 256,
.max_interval_slots = 512,
.duration_secs = 1,
},
};
GAPLEAdvertisingJobTerm advert_terms_b[] =
{
{
.min_interval_slots = GAPLE_ADVERTISING_SILENCE_INTERVAL_SLOTS,
.max_interval_slots = GAPLE_ADVERTISING_SILENCE_INTERVAL_SLOTS,
.duration_secs = 1,
},
{
.min_interval_slots = 32,
.max_interval_slots = 64,
.duration_secs = 1,
},
{
.min_interval_slots = GAPLE_ADVERTISING_SILENCE_INTERVAL_SLOTS,
.max_interval_slots = GAPLE_ADVERTISING_SILENCE_INTERVAL_SLOTS,
.duration_secs = 2,
},
{
.min_interval_slots = 960,
.max_interval_slots = 1240,
.duration_secs = 1,
},
};
// Schedule job "A":
BLEAdData *ad_a = create_ad("A", NULL);
GAPLEAdvertisingJobRef job_a;
job_a = gap_le_advert_schedule(ad_a,
advert_terms_a, sizeof(advert_terms_a)/sizeof(GAPLEAdvertisingJobTerm),
unscheduled_callback, s_unscheduled_cb_data, 0);
cl_assert(job_a);
assert_ad_data("A");
// Schedule job "B":
BLEAdData *ad_b = create_ad("B", NULL);
GAPLEAdvertisingJobRef job_b;
job_b = gap_le_advert_schedule(ad_b,
advert_terms_b, sizeof(advert_terms_b)/sizeof(GAPLEAdvertisingJobTerm),
unscheduled_callback, s_unscheduled_cb_data, 0);
// Even though B is newer, expect "A" to be scheduled still, because B's first term is silent:
cl_assert(job_b);
cl_assert_equal_b(gap_le_is_advertising_enabled(), true);
assert_ad_data("A");
gap_le_assert_advertising_interval(advert_terms_a[0].min_interval_slots,
advert_terms_a[0].max_interval_slots);
// After A's first term, expect that B's second term will follow. B's first term was silent, so
// this "ran" at the same time as A's first term.
regular_timer_fire_seconds(1);
cl_assert_equal_b(gap_le_is_advertising_enabled(), true);
assert_ad_data("B");
gap_le_assert_advertising_interval(advert_terms_b[1].min_interval_slots,
advert_terms_b[1].max_interval_slots);
// Expect A's second term. B's third term (silent) will "run" now too.
regular_timer_fire_seconds(1);
cl_assert_equal_b(gap_le_is_advertising_enabled(), true);
assert_ad_data("A");
gap_le_assert_advertising_interval(advert_terms_a[1].min_interval_slots,
advert_terms_a[1].max_interval_slots);
// Expect silence. B's third (silent) is 2 secs, so one more to go and A's third term is silent.
regular_timer_fire_seconds(1);
cl_assert_equal_b(gap_le_is_advertising_enabled(), false);
// Expect B's fourth term now.
regular_timer_fire_seconds(1);
cl_assert_equal_b(gap_le_is_advertising_enabled(), true);
assert_ad_data("B");
gap_le_assert_advertising_interval(advert_terms_b[3].min_interval_slots,
advert_terms_b[3].max_interval_slots);
regular_timer_fire_seconds(1);
// Expect B to be done:
cl_assert_equal_i(s_unscheduled_cb_count, 1);
cl_assert_equal_p(s_unscheduled_job, job_b);
cl_assert_equal_b(s_unscheduled_completed, true);
// Expect A's fourth term:
assert_ad_data("A");
cl_assert_equal_b(gap_le_is_advertising_enabled(), true);
gap_le_assert_advertising_interval(advert_terms_a[3].min_interval_slots,
advert_terms_a[3].max_interval_slots);
regular_timer_fire_seconds(1);
// Expect A to be done as well:
cl_assert_equal_i(s_unscheduled_cb_count, 2);
cl_assert_equal_p(s_unscheduled_job, job_a);
cl_assert_equal_b(s_unscheduled_completed, true);
}
void test_gap_le_advert__job_round_robin_multiple_terms(void) {
GAPLEAdvertisingJobTerm advert_terms[2] =
{
{
.min_interval_slots = 160,
.max_interval_slots = 320,
.duration_secs = 5,
},
{
.min_interval_slots = 480,
.max_interval_slots = 620,
.duration_secs = 5,
},
};
// Schedule job "A":
BLEAdData *ad_a = create_ad("A", NULL);
GAPLEAdvertisingJobRef job_a;
job_a = gap_le_advert_schedule(ad_a,
advert_terms, sizeof(advert_terms)/sizeof(GAPLEAdvertisingJobTerm),
unscheduled_callback, s_unscheduled_cb_data, 0);
cl_assert(job_a);
assert_ad_data("A");
// Schedule job "B":
BLEAdData *ad_b = create_ad("B", NULL);
GAPLEAdvertisingJobRef job_b;
job_b = gap_le_advert_schedule(ad_b,
advert_terms, sizeof(advert_terms)/sizeof(GAPLEAdvertisingJobTerm),
unscheduled_callback, s_unscheduled_cb_data, 0);
cl_assert(job_b);
assert_ad_data("B");
// Round-robin. Each term is 5 seconds, each job is 10 seconds:
for (int i = 0; i < 9; ++i) {
regular_timer_fire_seconds(1);
assert_ad_data("A");
cl_assert_equal_i(s_unscheduled_cb_count, 0);
regular_timer_fire_seconds(1);
assert_ad_data("B");
cl_assert_equal_i(s_unscheduled_cb_count, 0);
}
// Unschedule job "B", air job "A"
regular_timer_fire_seconds(1);
assert_ad_data("A");
cl_assert_equal_i(s_unscheduled_cb_count, 1);
cl_assert_equal_p(s_unscheduled_job, job_b);
cl_assert_equal_b(s_unscheduled_completed, true);
// Unschedule job "A"
regular_timer_fire_seconds(1);
cl_assert_equal_i(s_unscheduled_cb_count, 2);
cl_assert_equal_p(s_unscheduled_job, job_a);
cl_assert_equal_b(s_unscheduled_completed, true);
cl_assert(!gap_le_is_advertising_enabled());
free(ad_a);
free(ad_b);
}
void test_gap_le_advert__expiring_job(void) {
// Test that a job is expired correctly after the set duration:
uint16_t seconds_left = 5;
GAPLEAdvertisingJobTerm advert_term = {
.min_interval_slots = 160,
.max_interval_slots = 320,
.duration_secs = seconds_left,
};
BLEAdData *ad = create_ad(NULL, NULL);
GAPLEAdvertisingJobRef job;
job = gap_le_advert_schedule(ad, &advert_term, sizeof(advert_term)/sizeof(GAPLEAdvertisingJobTerm),
unscheduled_callback, s_unscheduled_cb_data, 0);
cl_assert(job);
// No jobs should been have been unscheduled:
cl_assert_equal_i(s_unscheduled_cb_count, 0);
while (seconds_left) {
cl_assert(gap_le_is_advertising_enabled());
regular_timer_fire_seconds(1);
--seconds_left;
}
cl_assert(!gap_le_is_advertising_enabled());
cl_assert_equal_i(regular_timer_seconds_count(), 0);
// The job should been have been unscheduled:
cl_assert_equal_i(s_unscheduled_cb_count, 1);
cl_assert_equal_p(s_unscheduled_job, job);
cl_assert_equal_b(s_unscheduled_completed, true);
free(ad);
}
void test_gap_le_advert__invalid_params(void) {
GAPLEAdvertisingJobRef job;
BLEAdData *ad = create_ad(NULL, NULL);
GAPLEAdvertisingJobTerm advert_term = {
.min_interval_slots = 31,
.max_interval_slots = 31,
.duration_secs = 1,
};
// Minimum interval boundary (no scan resp):
job = gap_le_advert_schedule(ad, &advert_term, sizeof(advert_term)/sizeof(GAPLEAdvertisingJobTerm),
unscheduled_callback, s_unscheduled_cb_data, 0);
cl_assert_equal_p(job, NULL);
advert_term.min_interval_slots = 32;
advert_term.max_interval_slots = 32;
job = gap_le_advert_schedule(ad, &advert_term, sizeof(advert_term)/sizeof(GAPLEAdvertisingJobTerm),
unscheduled_callback, s_unscheduled_cb_data, 0);
cl_assert(job);
// Minimum interval boundary (with scan resp):
char scan_resp_data[] = "scan resp data";
BLEAdData *ad_scannable = create_ad(NULL, scan_resp_data);
advert_term.min_interval_slots = 159;
advert_term.max_interval_slots = 159;
job = gap_le_advert_schedule(ad_scannable,
&advert_term, sizeof(advert_term)/sizeof(GAPLEAdvertisingJobTerm),
unscheduled_callback, s_unscheduled_cb_data, 0);
cl_assert_equal_p(job, NULL);
advert_term.min_interval_slots = 160;
advert_term.max_interval_slots = 160;
job = gap_le_advert_schedule(ad_scannable,
&advert_term, sizeof(advert_term)/sizeof(GAPLEAdvertisingJobTerm),
unscheduled_callback, s_unscheduled_cb_data, 0);
cl_assert(job);
// Max < Min:
advert_term.min_interval_slots = 200;
advert_term.max_interval_slots = 32;
job = gap_le_advert_schedule(ad, &advert_term, sizeof(advert_term)/sizeof(GAPLEAdvertisingJobTerm),
unscheduled_callback, s_unscheduled_cb_data, 0);
cl_assert_equal_p(job, NULL);
// Loop-around in the first term:
advert_term.min_interval_slots = 200;
advert_term.max_interval_slots = 200;
advert_term.duration_secs = GAPLE_ADVERTISING_DURATION_LOOP_AROUND;
job = gap_le_advert_schedule(ad, &advert_term, sizeof(advert_term)/sizeof(GAPLEAdvertisingJobTerm),
unscheduled_callback, s_unscheduled_cb_data, 0);
cl_assert_equal_p(job, NULL);
// Forever silent term:
advert_term.min_interval_slots = GAPLE_ADVERTISING_SILENCE_INTERVAL_SLOTS;
advert_term.max_interval_slots = GAPLE_ADVERTISING_SILENCE_INTERVAL_SLOTS;
advert_term.duration_secs = GAPLE_ADVERTISING_DURATION_INFINITE;
job = gap_le_advert_schedule(ad, &advert_term, sizeof(advert_term)/sizeof(GAPLEAdvertisingJobTerm),
unscheduled_callback, s_unscheduled_cb_data, 0);
cl_assert_equal_p(job, NULL);
// No terms:
job = gap_le_advert_schedule(ad, NULL, 0,
unscheduled_callback, s_unscheduled_cb_data, 0);
cl_assert_equal_p(job, NULL);
// No ad data:
advert_term.min_interval_slots = 200;
advert_term.max_interval_slots = 200;
advert_term.duration_secs = 1;
job = gap_le_advert_schedule(NULL, &advert_term,
sizeof(advert_term)/sizeof(GAPLEAdvertisingJobTerm),
unscheduled_callback, s_unscheduled_cb_data, 0);
cl_assert_equal_p(job, NULL);
free(ad);
free(ad_scannable);
}
void test_gap_le_advert__unschedule_non_existent(void) {
// Unscheduling non-existent job should be fine, should not crash:
gap_le_advert_unschedule((GAPLEAdvertisingJobRef)(uintptr_t) 0x1234);
// Unschedule callback should not have been called:
cl_assert_equal_i(s_unscheduled_cb_count, 0);
}
void test_gap_le_advert__deinit_unschedules(void) {
BLEAdData *ad = create_ad(NULL, NULL);
GAPLEAdvertisingJobTerm advert_term = {
.min_interval_slots = 160,
.max_interval_slots = 320,
.duration_secs = 10,
};
GAPLEAdvertisingJobRef job;
job = gap_le_advert_schedule(ad, &advert_term, sizeof(advert_term)/sizeof(GAPLEAdvertisingJobTerm),
unscheduled_callback, s_unscheduled_cb_data, 0);
cl_assert(job);
gap_le_advert_deinit();
cl_assert_equal_i(s_unscheduled_cb_count, 1);
cl_assert_equal_p(s_unscheduled_job, job);
cl_assert_equal_b(s_unscheduled_completed, false);
cl_assert_equal_i(regular_timer_seconds_count(), 0);
free(ad);
}
void test_gap_le_advert__cant_schedule_after_deinit(void) {
gap_le_advert_deinit();
BLEAdData *ad = create_ad(NULL, NULL);
GAPLEAdvertisingJobTerm advert_term = {
.min_interval_slots = 160,
.max_interval_slots = 320,
.duration_secs = 10,
};
GAPLEAdvertisingJobRef job;
job = gap_le_advert_schedule(ad, &advert_term, sizeof(advert_term)/sizeof(GAPLEAdvertisingJobTerm),
unscheduled_callback, s_unscheduled_cb_data, 0);
cl_assert_equal_p(job, NULL);
cl_assert_equal_i(regular_timer_seconds_count(), 0);
}
void test_gap_le_advert__continue_after_slave_connection(void) {
BLEAdData *ad = create_ad(NULL, NULL);
GAPLEAdvertisingJobTerm advert_term = {
.min_interval_slots = 160,
.max_interval_slots = 320,
.duration_secs = 10,
};
GAPLEAdvertisingJobRef job;
job = gap_le_advert_schedule(ad, &advert_term, sizeof(advert_term)/sizeof(GAPLEAdvertisingJobTerm),
unscheduled_callback, s_unscheduled_cb_data, 0);
cl_assert_equal_b(gap_le_is_advertising_enabled(), true);
// Simulate stopping advertising because of inbound connection:
gap_le_set_advertising_disabled();
s_is_connected_as_slave = true;
// Call the connection handler:
gap_le_advert_handle_connect_as_slave();
// we should have stopped advertising for reconnection
cl_assert_equal_b(gap_le_is_advertising_enabled(), false);
// Trigger the advertising to start up again. Since we still have an advertisement job set,
// it should continue.
regular_timer_fire_seconds(1);
cl_assert_equal_b(gap_le_is_advertising_enabled(), true);
free(ad);
}
void test_gap_le_advert__unschedule_job_types(void) {
BLEAdData *ad = create_ad(NULL, NULL);
GAPLEAdvertisingJobTerm advert_term = {
.min_interval_slots = 160,
.max_interval_slots = 320,
.duration_secs = 10,
};
GAPLEAdvertisingJobRef job_a;
job_a = gap_le_advert_schedule(ad, &advert_term, sizeof(advert_term)/sizeof(GAPLEAdvertisingJobTerm),
unscheduled_callback, s_unscheduled_cb_data, GAPLEAdvertisingJobTagDiscovery);
GAPLEAdvertisingJobTag tag = GAPLEAdvertisingJobTagDiscovery;
gap_le_advert_unschedule_job_types(&tag, 1);
// make sure we can remove a tag when it is the only one scheduled
cl_assert(s_unscheduled_job == job_a);
cl_assert_equal_i(s_unscheduled_cb_count, 1);
// add back the job we just unscheduled
job_a = gap_le_advert_schedule(ad, &advert_term, sizeof(advert_term)/sizeof(GAPLEAdvertisingJobTerm),
unscheduled_callback, s_unscheduled_cb_data, GAPLEAdvertisingJobTagDiscovery);
GAPLEAdvertisingJobRef job_b;
job_b = gap_le_advert_schedule(ad, &advert_term, sizeof(advert_term)/sizeof(GAPLEAdvertisingJobTerm),
unscheduled_callback, s_unscheduled_cb_data, GAPLEAdvertisingJobTagReconnection);
GAPLEAdvertisingJobRef job_c;
job_c = gap_le_advert_schedule(ad, &advert_term, sizeof(advert_term)/sizeof(GAPLEAdvertisingJobTerm),
unscheduled_callback, s_unscheduled_cb_data, GAPLEAdvertisingJobTagReconnection);
// run some Ad cycling
for (int i = 0; i < 3; i++) {
regular_timer_fire_seconds(1);
}
cl_assert_equal_b(gap_le_is_advertising_enabled(), true);
// make sure we can remove a few jobs with the same advertisement type
tag = GAPLEAdvertisingJobTagReconnection;
gap_le_advert_unschedule_job_types(&tag, 1);
// should have 3 jobs unscheduled at this point and 1 still running
cl_assert_equal_i(s_unscheduled_cb_count, 3);
cl_assert_equal_b(gap_le_is_advertising_enabled(), true);
// make sure after we unschedule all the jobs no more are running
tag = GAPLEAdvertisingJobTagDiscovery;
gap_le_advert_unschedule_job_types(&tag, 1);
cl_assert_equal_i(s_unscheduled_cb_count, 4);
cl_assert_equal_b(gap_le_is_advertising_enabled(), false);
free(ad);
}
+871
View File
@@ -0,0 +1,871 @@
/*
* 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 "bluetooth/gap_le_connect.h"
#include "comm/ble/gap_le_connect.h"
#include "comm/ble/gap_le_connection.h"
#include "comm/ble/gap_le_task.h"
#include "kernel/events.h"
#include "services/common/analytics/analytics.h"
#include "clar.h"
#include <bluetooth/bonding_sync.h>
#include <bluetooth/sm_types.h>
#include <btutil/bt_device.h>
// Fakes
///////////////////////////////////////////////////////////
#include "fake_events.h"
#include "fake_GAPAPI.h"
#include "fake_bluetooth_persistent_storage.h"
#include "fake_HCIAPI.h"
#include "fake_new_timer.h"
#include "fake_pbl_malloc.h"
#include "fake_system_task.h"
// Stubs
///////////////////////////////////////////////////////////
#include "stubs_bluetopia_interface.h"
#include "stubs_bt_lock.h"
#include "stubs_gap_le_advert.h"
#include "stubs_bluetooth_analytics.h"
#include "stubs_gatt_client_discovery.h"
#include "stubs_gatt_client_subscriptions.h"
#include "stubs_hexdump.h"
#include "stubs_logging.h"
#include "stubs_mutex.h"
#include "stubs_passert.h"
#include "stubs_pebble_pairing_service.h"
#include "stubs_regular_timer.h"
#include "stubs_shared_prf_storage.h"
// Note:
// The unit tests for "Pebble as Master" are disabled because role switching is not implemented yet,
// and the FW is currently "hard-wired" to be slave as a precautionary measure to prevent it from
// trying to connect as master. See PBL-20368.
void bt_driver_cb_handle_create_bonding(const BleBonding *bonding,
const BTDeviceAddress *addr) {
}
void cc2564A_bad_le_connection_complete_handle(unsigned int stack_id,
const GAP_LE_Current_Connection_Parameters_t *params) {
}
const GAP_LE_Pairing_Capabilities_t* gap_le_pairing_capabilities(void) {
return NULL;
}
void gap_le_device_name_request(uintptr_t stack_id, GAPLEConnection *connection) {
}
void gatt_service_changed_server_cleanup_by_connection(GAPLEConnection *connection) {
}
void bt_driver_handle_le_conn_params_update_event(
const BleConnectionUpdateCompleteEvent *event) {
}
typedef struct PairingUserConfirmationCtx PairingUserConfirmationCtx;
void bt_driver_pebble_pairing_service_handle_status_change(const GAPLEConnection *connection) {
}
void bt_driver_cb_pairing_confirm_handle_request(const PairingUserConfirmationCtx *ctx,
const char *device_name,
const char *confirmation_token) {
}
void bt_driver_cb_pairing_confirm_handle_completed(const PairingUserConfirmationCtx *ctx,
bool success) {
}
void launcher_task_add_callback(void (*callback)(void *data), void *data) {
callback(data);
}
void analytics_event_bt_error(AnalyticsEvent type, uint32_t error) {
}
void bluetooth_analytics_handle_connection_disconnection_event(
AnalyticsEvent type, uint8_t reason, const BleRemoteVersionInfo *vers_info) {
}
void analytics_inc(AnalyticsMetric metric, AnalyticsClient client) {
}
// Helpers
///////////////////////////////////////////////////////////
static BTDeviceInternal prv_dummy_device(uint8_t octet) {
BTDeviceAddress address = {
.octets = {
[0] = octet,
[1] = octet,
[2] = octet,
[3] = octet,
[4] = octet,
[5] = octet,
},
};
BTDevice device = bt_device_init_with_address(address, true /* is_random */);
return *(BTDeviceInternal *)(&device);
}
static BTBondingID prv_add_bonding_for_fake_resolvable_device(void) {
BTDeviceInternal identity_device = {};
const SMIdentityResolvingKey *irk = (const SMIdentityResolvingKey *) fake_GAPAPI_get_fake_irk();
BleBonding bonding = {
.pairing_info = {
.identity = identity_device,
.irk = *irk,
.is_remote_identity_info_valid = true,
},
.is_gateway = true,
};
bt_driver_handle_host_added_bonding(&bonding);
return fake_bt_persistent_storage_add(irk, &identity_device, "Dummy", true /* is_gateway */);
}
static void prv_assert_no_event(void) {
PebbleEvent event = fake_event_get_last();
cl_assert_equal_i(event.type, PEBBLE_NULL_EVENT);
}
static void prv_fake_connect(const BTDeviceInternal *device, bool is_master) {
// Simulate getting a Connection Complete event for the device from Bluetopia:
fake_gap_put_connection_event(HCI_ERROR_CODE_SUCCESS,
is_master, device);
cl_assert_equal_b(gap_le_connection_is_connected(device), true);
}
static void prv_fake_disconnect(const BTDeviceInternal *device, bool is_master) {
fake_gap_put_disconnection_event(HCI_ERROR_CODE_SUCCESS,
HCI_ERROR_CODE_CONNECTION_TERMINATED_BY_LOCAL_HOST,
is_master,
device);
cl_assert_equal_b(gap_le_connection_is_connected(device), false);
}
static void prv_assert_client_event(const BTDeviceInternal *device,
bool connected,
PebbleTaskBitset client_tasks,
uint8_t hci_reason) {
// Verify the Pebble event:
PebbleEvent event = fake_event_get_last();
cl_assert_equal_i(event.type, PEBBLE_BLE_CONNECTION_EVENT);
// Event should only go to app:
cl_assert_equal_i(event.task_mask, (PebbleTaskBitset) ~(client_tasks));
const PebbleBLEConnectionEvent *conn_event = &event.bluetooth.le.connection;
const BTDeviceInternal event_device = PebbleEventToBTDeviceInternal(conn_event);
const bool is_same_device = bt_device_equal(&event_device.opaque,
&device->opaque);
cl_assert_equal_b(is_same_device, true);
cl_assert_equal_b(conn_event->connected, connected);
cl_assert_equal_i(conn_event->hci_reason, hci_reason);
}
// Tests
///////////////////////////////////////////////////////////
extern void gap_le_connect_bluetopia_connection_callback(
unsigned int stack_id, GAP_LE_Event_Data_t* event_data, unsigned long CallbackParameter);
void test_gap_le_connect__initialize(void) {
fake_GAPAPI_init();
// Register slave connection event callback for tests involving Pebble as slave:
// This normally happens in gap_le_advert.c. Taking a shortcut to avoid dragging in more code.
GAP_LE_Advertising_Enable(1, TRUE, NULL, NULL, gap_le_connect_bluetopia_connection_callback, 0);
fake_event_init();
fake_bt_persistent_storage_reset();
gap_le_connection_init();
gap_le_connect_init();
}
void test_gap_le_connect__cleanup(void) {
// Cancel all connection intents:
for (GAPLEClient c = 0; c < GAPLEClientNum; ++c) {
gap_le_connect_cancel_all(c);
}
gap_le_connect_deinit();
cl_assert_equal_b(gap_le_connect_has_pending_create_connection(), false);
cl_assert_equal_i(gap_le_connect_connection_intents_count(), 0);
gap_le_connection_deinit();
cl_assert_equal_b(fake_HCIAPI_whitelist_error_count(), 0);
fake_HCIAPI_deinit();
}
// -----------------------------------------------------------------------------
// Parameter / Bounds checking
void test_gap_le_connect__register_max_intents(void) {
for (int i = 0; i < GAP_LE_CONNECT_MASTER_MAX_CONNECTION_INTENTS + 1; ++i) {
BTDeviceInternal device = prv_dummy_device(i);
BTErrno e = gap_le_connect_connect(&device,
true /* auto_reconnect */,
false /* is_pairing_required */,
GAPLEClientApp);
if (i == GAP_LE_CONNECT_MASTER_MAX_CONNECTION_INTENTS) {
// When the limit is reached, expect "not enough resources" error:
cl_assert_equal_i(e, BTErrnoNotEnoughResources);
} else {
cl_assert_equal_i(e, BTErrnoOK);
const bool registered = gap_le_connect_has_connection_intent(&device,
GAPLEClientApp);
cl_assert_equal_b(registered, true);
}
}
}
void test_gap_le_connect__register_null_device(void) {
BTErrno e = gap_le_connect_connect(NULL,
true /* auto_reconnect */,
false /* is_pairing_required */,
GAPLEClientApp);
cl_assert_equal_i(e, BTErrnoInvalidParameter);
}
void test_gap_le_connect__unregister_null_device(void) {
BTErrno e = gap_le_connect_cancel(NULL, GAPLEClientApp);
cl_assert_equal_i(e, BTErrnoInvalidParameter);
}
void test_gap_le_connect__register_invalid_bonding(void) {
BTErrno e = gap_le_connect_connect_by_bonding(BT_BONDING_ID_INVALID,
true /* auto_reconnect */,
false /* is_pairing_required */,
GAPLEClientApp);
cl_assert_equal_i(e, BTErrnoInvalidParameter);
}
void test_gap_le_connect__register_non_existing_bonding(void) {
BTErrno e = gap_le_connect_connect_by_bonding(~0,
true /* auto_reconnect */,
false /* is_pairing_required */,
GAPLEClientApp);
cl_assert_equal_i(e, BTErrnoInvalidParameter);
}
void test_gap_le_connect__unregister_invalid_bonding(void) {
BTErrno e = gap_le_connect_cancel_by_bonding(BT_BONDING_ID_INVALID, GAPLEClientApp);
cl_assert_equal_i(e, BTErrnoInvalidParameter);
}
void test_gap_le_connect__unregister_non_existing_bonding(void) {
BTErrno e = gap_le_connect_cancel_by_bonding(~0, GAPLEClientApp);
cl_assert_equal_i(e, BTErrnoInvalidParameter);
}
void test_gap_le_connect__register_is_already_registered_for_same_client(void) {
BTErrno e;
bool registered;
BTDeviceInternal device = prv_dummy_device(1);
// Register connection intent:
e = gap_le_connect_connect(&device,
true /* auto_reconnect */,
false /* is_pairing_required */,
GAPLEClientApp);
cl_assert_equal_i(e, BTErrnoOK);
registered = gap_le_connect_has_connection_intent(&device, GAPLEClientApp);
cl_assert_equal_b(registered, true);
// Try registering the device again as same client:
e = gap_le_connect_connect(&device,
true /* auto_reconnect */,
false /* is_pairing_required */,
GAPLEClientApp);
cl_assert_equal_i(e, BTErrnoInvalidState);
// Should still be registered from the first call:
registered = gap_le_connect_has_connection_intent(&device, GAPLEClientApp);
cl_assert_equal_b(registered, true);
}
void test_gap_le_connect__register_same_device_and_bonding(void) {
// Test that it is possible to have 2 intents for the same device, when registering one intent
// using the resolvable address and one with a bonding. Pebble will not try to collate these,
// because there are many addresses that resolve to the same bonding. The current implementation
// uses one address or one bonding per intent.
BTErrno e;
bool registered;
BTDeviceInternal device = *fake_GAPAPI_get_device_resolving_to_fake_irk();
BTBondingID bonding_id = prv_add_bonding_for_fake_resolvable_device();
// Register connection intent:
e = gap_le_connect_connect(&device,
true /* auto_reconnect */,
false /* is_pairing_required */,
GAPLEClientApp);
cl_assert_equal_i(e, BTErrnoOK);
registered = gap_le_connect_has_connection_intent(&device, GAPLEClientApp);
cl_assert_equal_b(registered, true);
// Register another connection intent using the bonding:
e = gap_le_connect_connect_by_bonding(bonding_id,
true /* auto_reconnect */,
false /* is_pairing_required */,
GAPLEClientApp);
cl_assert_equal_i(e, BTErrnoOK);
registered = gap_le_connect_has_connection_intent_for_bonding(bonding_id, GAPLEClientApp);
cl_assert_equal_b(registered, true);
}
void test_gap_le_connect__register_two_clients_same_device(void) {
BTErrno e;
bool registered;
BTDeviceInternal device = prv_dummy_device(1);
// Register connection intent:
e = gap_le_connect_connect(&device,
true /* auto_reconnect */,
false /* is_pairing_required */,
GAPLEClientApp);
cl_assert_equal_i(e, BTErrnoOK);
registered = gap_le_connect_has_connection_intent(&device, GAPLEClientApp);
cl_assert_equal_b(registered, true);
// Try registering the device again for different client:
e = gap_le_connect_connect(&device,
true /* auto_reconnect */,
false /* is_pairing_required */,
GAPLEClientKernel);
cl_assert_equal_i(e, BTErrnoOK);
// Assert registrations:
registered = gap_le_connect_has_connection_intent(&device, GAPLEClientApp);
cl_assert_equal_b(registered, true);
registered = gap_le_connect_has_connection_intent(&device, GAPLEClientKernel);
cl_assert_equal_b(registered, true);
// Only one registration (co-owned by the 2 clients):
cl_assert_equal_i(gap_le_connect_connection_intents_count(), 1);
}
void test_gap_le_connect__unregister_unknown_device(void) {
BTDeviceInternal device = prv_dummy_device(1);
BTErrno e = gap_le_connect_cancel(&device, GAPLEClientApp);
cl_assert_equal_i(e, BTErrnoInvalidParameter);
}
void test_gap_le_connect__unregister_unowned_intent(void) {
BTDeviceInternal device = prv_dummy_device(1);
BTErrno e;
// Register connection intent owned by kernel:
e = gap_le_connect_connect(&device,
true /* auto_reconnect */,
false /* is_pairing_required */,
GAPLEClientKernel);
cl_assert_equal_i(e, BTErrnoOK);
// Unregister connection intent owned by app:
e = gap_le_connect_cancel(&device, GAPLEClientApp);
cl_assert_equal_i(e, BTErrnoInvalidParameter);
}
// -----------------------------------------------------------------------------
// Virtual (dis)connection events
void __disabled_test_gap_le_connect__connection_event_for_registered_client(void) {
BTDeviceInternal device = prv_dummy_device(1);
// Register connection intent:
BTErrno e = gap_le_connect_connect(&device,
true /* auto_reconnect */,
false /* is_pairing_required */,
GAPLEClientApp);
cl_assert_equal_i(e, BTErrnoOK);
// Device isn't connected. Verify no event was caused as a result of the
// registration.
prv_assert_no_event();
// Connect & verify the client task gets the event:
prv_fake_connect(&device, true /* is_master*/);
prv_assert_client_event(&device, true /* connected */, (1 << PebbleTask_App),
HCI_ERROR_CODE_SUCCESS);
// Disconnect & verify the client task gets the event:
prv_fake_disconnect(&device, true /* is_master */);
prv_assert_client_event(&device, false /* connected */, (1 << PebbleTask_App),
HCI_ERROR_CODE_CONNECTION_TERMINATED_BY_LOCAL_HOST);
}
void test_gap_le_connect__connection_event_for_registered_client_by_bonding(void) {
BTDeviceInternal device = *fake_GAPAPI_get_device_resolving_to_fake_irk();
BTBondingID bonding_id = prv_add_bonding_for_fake_resolvable_device();
// Register connection intent:
BTErrno e = gap_le_connect_connect_by_bonding(bonding_id,
true /* auto_reconnect */,
false /* is_pairing_required */,
GAPLEClientApp);
cl_assert_equal_i(e, BTErrnoOK);
// Device isn't connected. Verify no event was caused as a result of the
// registration.
prv_assert_no_event();
// Connect & verify the client task gets the event:
prv_fake_connect(&device, false /* is_master*/);
prv_assert_client_event(&device, true /* connected */, (1 << PebbleTask_App),
HCI_ERROR_CODE_SUCCESS);
// Disconnect & verify the client task gets the event:
prv_fake_disconnect(&device, false /* is_master */);
prv_assert_client_event(&device, false /* connected */, (1 << PebbleTask_App),
HCI_ERROR_CODE_CONNECTION_TERMINATED_BY_LOCAL_HOST);
}
void __disabled_test_gap_le_connect__register_for_already_connected_device(void) {
BTErrno e;
BTDeviceInternal device = prv_dummy_device(1);
// Register connection intent for kernel:
e = gap_le_connect_connect(&device,
true /* auto_reconnect */,
false /* is_pairing_required */,
GAPLEClientKernel);
cl_assert_equal_i(e, BTErrnoOK);
// Simulate getting a Connection Complete event for the device from Bluetopia:
prv_fake_connect(&device, true /* is_master*/);
// Verify the kernel task got a (virtual) connection event:
prv_assert_client_event(&device, true /* connected */, (1 << PebbleTask_KernelMain),
HCI_ERROR_CODE_SUCCESS);
// Register connection intent for app:
e = gap_le_connect_connect(&device,
true /* auto_reconnect */,
false /* is_pairing_required */,
GAPLEClientApp);
cl_assert_equal_i(e, BTErrnoOK);
// Verify (only) the app task got a (virtual) connection event:
prv_assert_client_event(&device, true /* connected */, (1 << PebbleTask_App),
HCI_ERROR_CODE_SUCCESS);
}
void test_gap_le_connect__register_for_already_connected_bonding(void) {
BTErrno e;
BTDeviceInternal device = *fake_GAPAPI_get_device_resolving_to_fake_irk();
BTBondingID bonding_id = prv_add_bonding_for_fake_resolvable_device();
// Register connection intent for kernel:
e = gap_le_connect_connect_by_bonding(bonding_id,
true /* auto_reconnect */,
false /* is_pairing_required */,
GAPLEClientKernel);
cl_assert_equal_i(e, BTErrnoOK);
// Simulate getting a Connection Complete event for the device from Bluetopia:
prv_fake_connect(&device, false /* is_master*/);
// Verify the kernel task got a (virtual) connection event:
prv_assert_client_event(&device, true /* connected */, (1 << PebbleTask_KernelMain),
HCI_ERROR_CODE_SUCCESS);
// Register connection intent for app:
e = gap_le_connect_connect_by_bonding(bonding_id,
true /* auto_reconnect */,
false /* is_pairing_required */,
GAPLEClientApp);
cl_assert_equal_i(e, BTErrnoOK);
// Verify (only) the app task got a (virtual) connection event:
prv_assert_client_event(&device, true /* connected */, (1 << PebbleTask_App),
HCI_ERROR_CODE_SUCCESS);
}
void __disabled_test_gap_le_connect__disconnection_event_upon_airplane_mode(void) {
BTDeviceInternal device = prv_dummy_device(1);
// Register connection intent:
BTErrno e = gap_le_connect_connect(&device,
true /* auto_reconnect */,
false /* is_pairing_required */,
GAPLEClientApp);
cl_assert_equal_i(e, BTErrnoOK);
prv_fake_connect(&device, true /* is_master*/);
// Air-plane mode:
gap_le_connect_deinit();
// Verify (only) the app task got a (virtual) connection event:
prv_assert_client_event(&device, false /* connected */, (1 << PebbleTask_App),
GAPLEConnectHCIReasonExtensionAirPlaneMode);
}
// -----------------------------------------------------------------------------
// Auto-reconnect Tests
void __disabled_test_gap_le_connect__single_client_no_autoreconnect(void) {
BTDeviceInternal device = prv_dummy_device(1);
// Register connection intent for app:
gap_le_connect_connect(&device,
false /* no auto_reconnect */,
false /* is_pairing_required */,
GAPLEClientApp);
prv_fake_connect(&device, true /* is_master*/);
// Verify the app task got a (virtual) connection event:
prv_assert_client_event(&device, true /* connected */, (1 << PebbleTask_App),
HCI_ERROR_CODE_SUCCESS);
prv_fake_disconnect(&device, true /* is_master */);
// Verify the app task got a (virtual) disconnection event:
prv_assert_client_event(&device, false /* connected */, (1 << PebbleTask_App),
HCI_ERROR_CODE_CONNECTION_TERMINATED_BY_LOCAL_HOST);
// Verify that the connection intent has been removed after disconnection:
cl_assert_equal_i(gap_le_connect_connection_intents_count(), 0);
}
void __disabled_test_gap_le_connect__two_clients_one_without_autoreconnect(void) {
BTDeviceInternal device = prv_dummy_device(1);
// Register auto-reconnecting connection intent for kernel:
gap_le_connect_connect(&device,
true /* auto_reconnect */,
false /* is_pairing_required */,
GAPLEClientKernel);
prv_fake_connect(&device, true /* is_master*/);
// Register one-shot connection intent for app:
gap_le_connect_connect(&device,
false /* no auto_reconnect */,
false /* is_pairing_required */,
GAPLEClientApp);
prv_fake_disconnect(&device, true /* is_master */);
// Verify both app task and kernel got the (virtual) disconnection event:
prv_assert_client_event(&device, false /* connected */,
(1 << PebbleTask_App) | (1 << PebbleTask_KernelMain),
HCI_ERROR_CODE_CONNECTION_TERMINATED_BY_LOCAL_HOST);
// Verify that the connection intent is still there for the Kernel:
cl_assert_equal_i(gap_le_connect_connection_intents_count(), 1);
cl_assert_equal_b(gap_le_connect_has_connection_intent(&device, GAPLEClientKernel), true);
cl_assert_equal_b(gap_le_connect_has_connection_intent(&device, GAPLEClientApp), false);
cl_assert_equal_b(gap_le_connect_has_pending_create_connection(), true);
}
// -----------------------------------------------------------------------------
// Cancel Connect (as Master)
void __disabled_test_gap_le_connect__cancel_connect(void) {
BTDeviceInternal device = prv_dummy_device(1);
// Register connection intent for app:
gap_le_connect_connect(&device,
false /* no auto_reconnect */,
false /* is_pairing_required */,
GAPLEClientApp);
BTErrno e = gap_le_connect_cancel(&device, GAPLEClientApp);
cl_assert_equal_i(e, BTErrnoOK);
// The LE Cancel Create Connection command is always followed by an event that
// is sent by the BT Controller. Simulate this event:
fake_gap_le_put_cancel_create_event(&device, true /* is_master */);
prv_assert_no_event();
// Verify there are no more intents:
cl_assert_equal_i(gap_le_connect_connection_intents_count(), 0);
cl_assert_equal_b(gap_le_connect_has_pending_create_connection(), false);
}
void __disabled_test_gap_le_connect__disconnection_event_upon_cancel_connect(void) {
BTDeviceInternal device = prv_dummy_device(1);
// Register connection intent for app:
gap_le_connect_connect(&device,
false /* no auto_reconnect */,
false /* is_pairing_required */,
GAPLEClientApp);
prv_fake_connect(&device, false /* is_master*/);
BTErrno e = gap_le_connect_cancel(&device, GAPLEClientApp);
cl_assert_equal_i(e, BTErrnoOK);
// The LE Cancel Create Connection command is always followed by an event that
// is sent by the BT Controller. Simulate this event:
fake_gap_le_put_cancel_create_event(&device, true /* is_master */);
// Verify the app task got a (virtual) disconnection event:
prv_assert_client_event(&device, false /* connected */, (1 << PebbleTask_App),
GAPLEConnectHCIReasonExtensionCancelConnect);
// Verify there are no more intents:
cl_assert_equal_i(gap_le_connect_connection_intents_count(), 0);
}
// -----------------------------------------------------------------------------
// Cancel Connect by Bonding (as Slave)
void test_gap_le_connect__slave_cancel_connect_by_bonding(void) {
BTBondingID bonding_id = prv_add_bonding_for_fake_resolvable_device();
// Register connection intent for app:
gap_le_connect_connect_by_bonding(bonding_id,
false /* no auto_reconnect */,
false /* is_pairing_required */,
GAPLEClientApp);
BTErrno e = gap_le_connect_cancel_by_bonding(bonding_id, GAPLEClientApp);
cl_assert_equal_i(e, BTErrnoOK);
prv_assert_no_event();
// Verify there are no more intents:
cl_assert_equal_i(gap_le_connect_connection_intents_count(), 0);
cl_assert_equal_b(gap_le_connect_has_pending_create_connection(), false);
}
void test_gap_le_connect__slave_disconnection_event_upon_cancel_connect_by_bonding(void) {
BTDeviceInternal device = *fake_GAPAPI_get_device_resolving_to_fake_irk();
BTBondingID bonding_id = prv_add_bonding_for_fake_resolvable_device();
prv_fake_connect(&device, false /* is_master*/);
// Register connection intent for app:
gap_le_connect_connect_by_bonding(bonding_id,
false /* no auto_reconnect */,
false /* is_pairing_required */,
GAPLEClientApp);
BTErrno e = gap_le_connect_cancel_by_bonding(bonding_id, GAPLEClientApp);
cl_assert_equal_i(e, BTErrnoOK);
// Verify the app task got a (virtual) disconnection event:
prv_assert_client_event(&device, false /* connected */, (1 << PebbleTask_App),
GAPLEConnectHCIReasonExtensionCancelConnect);
// Verify there are no more intents:
cl_assert_equal_i(gap_le_connect_connection_intents_count(), 0);
}
// -------------------------------------------------------------------------------------------------
// Pairing
void __disabled_test_gap_le_connect__one_shot_intent_removed_when_disconnected_before_encrpt(void) {
BTDeviceInternal device = prv_dummy_device(1);
// Register connection one-shot intent, with pairing required:
gap_le_connect_connect(&device,
false /* no auto_reconnect */,
true /* is_pairing_required */,
GAPLEClientKernel);
// Expect intent:
cl_assert_equal_b(gap_le_connect_has_connection_intent(&device, GAPLEClientKernel), true);
prv_fake_connect(&device, true /* is_master*/);
prv_fake_disconnect(&device, true /* is_master */);
// Expect intent to be removed:
cl_assert_equal_b(gap_le_connect_has_connection_intent(&device, GAPLEClientKernel), false);
}
void test_gap_le_connect__connection_event_only_after_encrypted_if_encryption_required(void) {
BTDeviceInternal device = *fake_GAPAPI_get_device_resolving_to_fake_irk();
BTBondingID bonding_id = prv_add_bonding_for_fake_resolvable_device();
// Register connection intent for app:
gap_le_connect_connect_by_bonding(bonding_id,
true /* no auto_reconnect */,
true /* is_pairing_required */,
GAPLEClientApp);
prv_fake_connect(&device, false /* is_master*/);
// Verify the app task got NO (virtual) connection event, the link is not encrypted yet:
// TODO: legacy PEBBLE_BT_CONNECTION_EVENT is still emitted, see gap_le_connect.c
// prv_put_legacy_connection_event.
//
// prv_assert_no_event();
fake_event_clear_last();
fake_GAPAPI_set_encrypted_for_device(&device);
fake_GAPAPI_put_encryption_change_event(true /* encrypted */, GAP_LE_PAIRING_STATUS_NO_ERROR,
false /* is_master */, &device);
// Verify the app task got a (virtual) connection event:
prv_assert_client_event(&device, true /* connected */, (1 << PebbleTask_App),
HCI_ERROR_CODE_SUCCESS);
}
void test_gap_le_connect__add_intent_requiring_pairing_after_connected_and_encrypted(void) {
BTDeviceInternal device = *fake_GAPAPI_get_device_resolving_to_fake_irk();
BTBondingID bonding_id = prv_add_bonding_for_fake_resolvable_device();
prv_fake_connect(&device, false /* is_master*/);
fake_GAPAPI_set_encrypted_for_device(&device);
fake_GAPAPI_put_encryption_change_event(true /* encrypted */, GAP_LE_PAIRING_STATUS_NO_ERROR,
false /* is_master */, &device);
fake_event_clear_last();
const BleAddressAndIRKChange e = {
.device = device,
.is_address_updated = true,
.new_device = device,
.is_resolved = true,
.irk = *(const SMIdentityResolvingKey *) fake_GAPAPI_get_fake_irk(),
};
bt_driver_handle_le_connection_handle_update_address_and_irk(&e);
gap_le_connection_by_device(&device);
// Register connection intent for app:
gap_le_connect_connect_by_bonding(bonding_id,
true /* no auto_reconnect */,
true /* is_pairing_required */,
GAPLEClientApp);
// Verify the app task got a (virtual) connection event:
prv_assert_client_event(&device, true /* connected */, (1 << PebbleTask_App),
HCI_ERROR_CODE_SUCCESS);
}
// -----------------------------------------------------------------------------
// Handling Bonding Changes
void test_gap_le_connect__removed_bonding_while_connected(void) {
BTDeviceInternal device = *fake_GAPAPI_get_device_resolving_to_fake_irk();
BTBondingID bonding_id = prv_add_bonding_for_fake_resolvable_device();
// Register connection intent for app:
gap_le_connect_connect_by_bonding(bonding_id,
false /* no auto_reconnect */,
false /* is_pairing_required */,
GAPLEClientApp);
prv_fake_connect(&device, false /* is_master*/);
// Verify the app task got a (virtual) connection event:
prv_assert_client_event(&device, true /* connected */, (1 << PebbleTask_App),
HCI_ERROR_CODE_SUCCESS);
// Simulate "bonding will delete" callback:
gap_le_connect_handle_bonding_change(bonding_id, BtPersistBondingOpWillDelete);
// Verify the app task got a (virtual) disconnection event:
prv_assert_client_event(&device, false /* connected */, (1 << PebbleTask_App),
GAPLEConnectHCIReasonExtensionUserRemovedBonding);
// Verify there are no more intents:
cl_assert_equal_i(gap_le_connect_connection_intents_count(), 0);
}
// -----------------------------------------------------------------------------
// BT Controller White-list management
void __disabled_test_gap_le_connect__whitelist_add_when_disconnected(void) {
BTDeviceInternal device = prv_dummy_device(1);
// Register connection intent for app:
gap_le_connect_connect(&device,
false /* no auto_reconnect */,
false /* is_pairing_required */,
GAPLEClientApp);
// Not connected yet, so expect to be added to white-list:
cl_assert_equal_b(fake_HCIAPI_whitelist_contains(&device), true);
prv_fake_connect(&device, true /* is_master*/);
// Connected, so expect to be removed from white-list:
cl_assert_equal_b(fake_HCIAPI_whitelist_contains(&device), false);
}
void __disabled_test_gap_le_connect__whitelist_add_when_connected(void) {
BTDeviceInternal device = prv_dummy_device(1);
// Register connection intent for app:
gap_le_connect_connect(&device,
false /* no auto_reconnect */,
false /* is_pairing_required */,
GAPLEClientKernel);
prv_fake_connect(&device, true /* is_master*/);
// Register connection intent for app:
gap_le_connect_connect(&device,
false /* no auto_reconnect */,
false /* is_pairing_required */,
GAPLEClientApp);
// Connected, so expect to be removed from white-list:
cl_assert_equal_b(fake_HCIAPI_whitelist_contains(&device), false);
}
void __disabled_test_gap_le_connect__whitelist_remove_when_connected(void) {
BTDeviceInternal device = prv_dummy_device(1);
// Register connection intent for app:
gap_le_connect_connect(&device,
false /* no auto_reconnect */,
false /* is_pairing_required */,
GAPLEClientApp);
prv_fake_connect(&device, true /* is_master*/);
gap_le_connect_cancel(&device, GAPLEClientApp);
// Connected, so expect to be removed from white-list:
cl_assert_equal_b(fake_HCIAPI_whitelist_contains(&device), false);
}
void __disabled_test_gap_le_connect__whitelist_repopulated_on_init(void) {
BTDeviceInternal device = prv_dummy_device(1);
gap_le_connect_connect(&device,
false /* no auto_reconnect */,
false /* is_pairing_required */,
GAPLEClientApp);
gap_le_connect_deinit();
// Connection intents survive air-plane mode:
cl_assert_equal_i(gap_le_connect_connection_intents_count(), 1);
// "Reset" BT Controller:
fake_HCIAPI_deinit();
cl_assert_equal_i(fake_HCIAPI_whitelist_count(), 0);
gap_le_connect_init();
// Not connected yet, so expect to be added to white-list:
cl_assert_equal_b(fake_HCIAPI_whitelist_contains(&device), true);
cl_assert_equal_i(fake_HCIAPI_whitelist_count(), 1);
}
+319
View File
@@ -0,0 +1,319 @@
/*
* 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 <bluetooth/gatt.h>
#include "comm/ble/gatt_client_accessors.h"
#include "comm/ble/gatt_client_discovery.h"
#include "comm/ble/gatt_service_changed.h"
#include "comm/ble/gap_le_connection.h"
#include "kernel/events.h"
#include "clar.h"
#include <btutil/bt_device.h>
#include <btutil/bt_uuid.h>
// Fakes
///////////////////////////////////////////////////////////
#include "fake_GAPAPI.h"
#include "fake_GATTAPI.h"
#include "fake_GATTAPI_test_vectors.h"
#include "fake_events.h"
#include "fake_new_timer.h"
#include "fake_system_task.h"
#include "fake_pbl_malloc.h"
// Stubs
///////////////////////////////////////////////////////////
#include "stubs_bluetopia_interface.h"
#include "stubs_bt_driver_gatt.h"
#include "stubs_bt_lock.h"
#include "stubs_gatt_client_subscriptions.h"
#include "stubs_logging.h"
#include "stubs_mutex.h"
#include "stubs_passert.h"
#include "stubs_prompt.h"
#include "stubs_rand_ptr.h"
#include "stubs_regular_timer.h"
void core_dump_reset(bool is_forced) {
}
void launcher_task_add_callback(void (*callback)(void *data), void *data) {
callback(data);
}
uint16_t gaps_get_starting_att_handle(void) {
return 4;
}
// Helpers
///////////////////////////////////////////////////////////
#define TEST_GATT_CONNECTION_ID (1234)
static BTDeviceInternal prv_dummy_device(uint8_t octet) {
BTDeviceAddress address = {
.octets = {
[0] = octet,
[1] = octet,
[2] = octet,
[3] = octet,
[4] = octet,
[5] = octet,
},
};
BTDevice device = bt_device_init_with_address(address, true /* is_random */);
return *(BTDeviceInternal *)(&device);
}
static BTDeviceInternal prv_connected_dummy_device(uint8_t octet) {
BTDeviceInternal device = prv_dummy_device(octet);
gap_le_connection_add(&device, NULL, true /* local_is_master */);
GAPLEConnection *connection = gap_le_connection_by_device(&device);
connection->gatt_connection_id = TEST_GATT_CONNECTION_ID;
return device;
}
static void prv_mock_put_service_discovery_events(void) {
// Simulate discovery of Blood Pressure service:
fake_gatt_put_discovery_indication_blood_pressure_service(TEST_GATT_CONNECTION_ID);
fake_gatt_put_discovery_indication_health_thermometer_service(TEST_GATT_CONNECTION_ID);
fake_gatt_put_discovery_indication_random_128bit_uuid_service(TEST_GATT_CONNECTION_ID);
fake_gatt_put_discovery_complete_event(GATT_SERVICE_DISCOVERY_STATUS_SUCCESS,
TEST_GATT_CONNECTION_ID);
}
// Tests
///////////////////////////////////////////////////////////
void test_gatt_client_accessors__initialize(void) {
fake_gatt_init();
fake_event_init();
gap_le_connection_init();
}
void test_gatt_client_accessors__cleanup(void) {
gap_le_connection_deinit();
}
void test_gatt_client_accessors__copy_service_refs(void) {
BTDeviceInternal device = prv_connected_dummy_device(1);
// Start discovery:
cl_assert_equal_i(gatt_client_discovery_discover_all(&device), BTErrnoOK);
prv_mock_put_service_discovery_events();
const Service *bp_service = fake_gatt_get_blood_pressure_service();
const Service *thermo_service = fake_gatt_get_health_thermometer_service();
const Service *random_128bit_service = fake_gatt_get_random_128bit_uuid_service();
const Service *services[] = {
bp_service,
thermo_service,
random_128bit_service,
};
const uint8_t num_services = 3;
// Test gatt_client_copy_service_refs():
BLEService service_refs[num_services];
uint8_t num_found_services = gatt_client_copy_service_refs(&device, service_refs, num_services);
cl_assert_equal_i(num_found_services, num_services);
for (uint8_t s = 0; s < num_found_services; ++s) {
BLEService service_ref = service_refs[s];
const Service *expected_service = services[s];
// Test gatt_client_service_get_uuid():
const Uuid uuid = gatt_client_service_get_uuid(service_ref);
cl_assert(uuid_equal(&uuid, &expected_service->uuid));
// Test gatt_client_service_get_device():
BTDeviceInternal returned_device = gatt_client_service_get_device(service_ref);
cl_assert(bt_device_equal(&returned_device.opaque, &device.opaque));
// Check Characteristics:
const uint8_t num_characteristics = expected_service->num_characteristics;
BLECharacteristic characteristic_refs[num_characteristics];
// Test gatt_client_service_get_characteristics():
const uint8_t num_found_characteristics =
gatt_client_service_get_characteristics(service_ref, characteristic_refs, num_characteristics);
cl_assert_equal_i(num_characteristics, num_found_characteristics);
for (uint8_t c = 0; c < num_found_characteristics; ++c) {
BLECharacteristic characteristic_ref = characteristic_refs[c];
const Characteristic *expected_characteristic = &expected_service->characteristics[c];
// Test gatt_client_characteristic_get_uuid():
const Uuid uuid = gatt_client_characteristic_get_uuid(characteristic_ref);
cl_assert(uuid_equal(&uuid, &expected_characteristic->uuid));
// Test gatt_client_characteristic_get_properties():
cl_assert_equal_i(gatt_client_characteristic_get_properties(characteristic_ref),
expected_characteristic->properties);
// Test gatt_client_characteristic_get_service():
cl_assert_equal_i(gatt_client_characteristic_get_service(characteristic_ref), service_ref);
// Test gatt_client_characteristic_get_device():
BTDeviceInternal returned_device = gatt_client_characteristic_get_device(characteristic_ref);
cl_assert(bt_device_equal(&returned_device.opaque, &device.opaque));
// Test gatt_client_characteristic_get_descriptors():
const uint8_t num_descriptors = expected_characteristic->num_descriptors;
BLEDescriptor descriptor_refs[num_descriptors];
const uint8_t num_found_descriptors =
gatt_client_characteristic_get_descriptors(characteristic_ref, descriptor_refs, num_descriptors);
cl_assert_equal_i(num_descriptors, num_found_descriptors);
for (uint8_t d = 0; d < num_descriptors; ++d) {
const Descriptor *expected_descriptor = &expected_characteristic->descriptors[d];
const BLEDescriptor descriptor_ref = descriptor_refs[d];
// Test gatt_client_descriptor_get_uuid():
const Uuid uuid = gatt_client_descriptor_get_uuid(descriptor_ref);
cl_assert(uuid_equal(&uuid, &expected_descriptor->uuid));
// Test gatt_client_descriptor_get_characteristic():
cl_assert_equal_i(gatt_client_descriptor_get_characteristic(descriptor_ref), characteristic_ref);
}
}
// Test gatt_client_service_get_included_services():
const uint8_t num_inc_services = expected_service->num_included_services;
BLEService inc_service_refs[num_inc_services];
const uint8_t num_found_included_services =
gatt_client_service_get_included_services(service_ref, inc_service_refs, num_inc_services);
cl_assert_equal_i(num_inc_services, num_found_included_services);
for (uint8_t i = 0; i < num_inc_services; ++i) {
const Service *expected_inc_service = expected_service->included_services[i];
BLEService inc_service = inc_service_refs[i];
// Only check the Service UUID:
const Uuid uuid = gatt_client_service_get_uuid(inc_service);
cl_assert(uuid_equal(&uuid, &expected_inc_service->uuid));
}
}
}
void test_gatt_client_accessors__copy_service_refs_matching(void) {
BTDeviceInternal device = prv_connected_dummy_device(1);
// Start discovery:
cl_assert_equal_i(gatt_client_discovery_discover_all(&device), BTErrnoOK);
prv_mock_put_service_discovery_events();
const uint8_t num_services = 1;
// Test gatt_client_copy_service_refs():
BLEService service_refs[num_services];
const Service *bp_service = fake_gatt_get_blood_pressure_service();
uint8_t num_found_services = gatt_client_copy_service_refs_matching_uuid(&device, service_refs,
num_services,
&bp_service->uuid);
cl_assert_equal_i(num_found_services, num_services);
// Test that the UUID matches the Blood Pressure UUID:
const Uuid uuid = gatt_client_service_get_uuid(service_refs[0]);
cl_assert(uuid_equal(&uuid, &bp_service->uuid));
}
void test_gatt_client_accessors__get_characteristics_matching_uuids(void) {
BTDeviceInternal device = prv_connected_dummy_device(1);
// Start discovery:
cl_assert_equal_i(gatt_client_discovery_discover_all(&device), BTErrnoOK);
prv_mock_put_service_discovery_events();
const Service *bp_service = fake_gatt_get_blood_pressure_service();
// Get the reference to the Blood Pressure service:
const uint8_t num_services = 1;
BLEService service_refs[num_services];
gatt_client_copy_service_refs_matching_uuid(&device, service_refs,
num_services, &bp_service->uuid);
Uuid matching_uuids[3];
matching_uuids[0] = bp_service->characteristics[1].uuid;
matching_uuids[1] = bt_uuid_expand_16bit(0xffff); // not expected to match
matching_uuids[2] = bp_service->characteristics[0].uuid;
BLECharacteristic characteristics[3];
const uint8_t found =
gatt_client_service_get_characteristics_matching_uuids(service_refs[0], characteristics,
matching_uuids, 3);
cl_assert_equal_i(found, 2);
// Expect the order of the matching_uuids array is preserved:
cl_assert(uuid_equal(&matching_uuids[0], &bp_service->characteristics[1].uuid));
cl_assert(uuid_equal(&matching_uuids[2], &bp_service->characteristics[0].uuid));
// Expect the 0xffff UUID to return "no match":
cl_assert_equal_i(characteristics[1], BLE_CHARACTERISTIC_INVALID);
}
extern uint8_t gatt_client_copy_service_refs_by_discovery_generation(
const BTDeviceInternal *device, BLEService services_out[],
uint8_t num_services, uint8_t discovery_gen);
extern void gatt_client_discovery_discover_range(GAPLEConnection *connection,
ATTHandleRange *hdl_range);
void test_gatt_client_accessors__get_service_refs_by_discovery_gen(void) {
BTDeviceInternal device = prv_connected_dummy_device(1);
cl_assert_equal_i(gatt_client_discovery_discover_all(&device), BTErrnoOK);
fake_gatt_put_discovery_indication_blood_pressure_service(TEST_GATT_CONNECTION_ID);
fake_gatt_put_discovery_complete_event(GATT_SERVICE_DISCOVERY_STATUS_SUCCESS, TEST_GATT_CONNECTION_ID);
ATTHandleRange range = {
.start = 0x1,
.end = 0xC000,
};
gatt_client_discovery_discover_range(gap_le_connection_by_device(&device), &range);
fake_gatt_put_discovery_indication_health_thermometer_service(TEST_GATT_CONNECTION_ID);
fake_gatt_put_discovery_indication_random_128bit_uuid_service(TEST_GATT_CONNECTION_ID);
fake_gatt_put_discovery_complete_event(GATT_SERVICE_DISCOVERY_STATUS_SUCCESS, TEST_GATT_CONNECTION_ID);
const Service *bp_service = fake_gatt_get_blood_pressure_service();
const Service *thermo_service = fake_gatt_get_health_thermometer_service();
const Service *random_128bit_service = fake_gatt_get_random_128bit_uuid_service();
BLEService service_refs_out[3];
// Only the BP service should be part of the first generation
uint8_t refs_out = gatt_client_copy_service_refs_by_discovery_generation(
&device, service_refs_out, 3, 0);
cl_assert_equal_i(1, refs_out);
const Uuid uuid = gatt_client_service_get_uuid(service_refs_out[0]);
cl_assert(uuid_equal(&uuid, &bp_service->uuid));
// Thermo & Random 128 bit service should be part of the second gen
refs_out = gatt_client_copy_service_refs_by_discovery_generation(
&device, service_refs_out, 3, 1);
cl_assert_equal_i(2, refs_out);
const Uuid uuid1 = gatt_client_service_get_uuid(service_refs_out[0]);
const Uuid uuid2 = gatt_client_service_get_uuid(service_refs_out[0]);
cl_assert(uuid_equal(&uuid1, &thermo_service->uuid) ||
uuid_equal(&uuid1, &random_128bit_service->uuid));
cl_assert(uuid_equal(&uuid2, &thermo_service->uuid) ||
uuid_equal(&uuid2, &random_128bit_service->uuid));
}
+520
View File
@@ -0,0 +1,520 @@
/*
* 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 "comm/ble/gatt_client_discovery.h"
#include "comm/ble/gatt_service_changed.h"
#include "comm/ble/gap_le_connection.h"
#include "comm/ble/gap_le_task.h"
#include "kernel/events.h"
#include "clar.h"
#include <btutil/bt_device.h>
// Fakes
///////////////////////////////////////////////////////////
#include "fake_GAPAPI.h"
#include "fake_GATTAPI.h"
#include "fake_GATTAPI_test_vectors.h"
#include "fake_events.h"
#include "fake_new_timer.h"
#include "fake_pbl_malloc.h"
#include "fake_system_task.h"
// Stubs
///////////////////////////////////////////////////////////
#include "stubs_bluetopia_interface.h"
#include "stubs_bt_driver_gatt.h"
#include "stubs_bt_lock.h"
#include "stubs_gatt_client_subscriptions.h"
#include "stubs_logging.h"
#include "stubs_mutex.h"
#include "stubs_passert.h"
#include "stubs_rand_ptr.h"
#include "stubs_regular_timer.h"
void core_dump_reset(bool is_forced) {
}
void launcher_task_add_callback(void (*callback)(void *data), void *data) {
callback(data);
}
uint16_t gaps_get_starting_att_handle(void) {
return 4;
}
// Helpers
///////////////////////////////////////////////////////////
extern TimerID bt_driver_gatt_get_watchdog_timer_id(void);
#define TEST_GATT_CONNECTION_ID (1234)
static BTDeviceInternal prv_dummy_device(uint8_t octet) {
BTDeviceAddress address = {
.octets = {
[0] = octet,
[1] = octet,
[2] = octet,
[3] = octet,
[4] = octet,
[5] = octet,
},
};
BTDevice device = bt_device_init_with_address(address, true /* is_random */);
return *(BTDeviceInternal *)(&device);
}
static BTDeviceInternal prv_connected_dummy_device(uint8_t octet) {
BTDeviceInternal device = prv_dummy_device(octet);
gap_le_connection_add(&device, NULL, true /* local_is_master */);
GAPLEConnection *connection = gap_le_connection_by_device(&device);
connection->gatt_connection_id = TEST_GATT_CONNECTION_ID;
return device;
}
static void prv_assert_no_event(void) {
PebbleEvent event = fake_event_get_last();
cl_assert_equal_i(event.type, PEBBLE_NULL_EVENT);
}
static void prv_assert_event(const BTDeviceInternal *device, BTErrno status) {
PebbleEvent event = fake_event_get_last();
cl_assert_equal_i(event.type, PEBBLE_BLE_GATT_CLIENT_EVENT);
cl_assert_equal_i(event.bluetooth.le.gatt_client_service.subtype,
PebbleBLEGATTClientEventTypeServiceChange);
cl_assert_equal_i(event.bluetooth.le.gatt_client_service.info->status,
status);
const BTDeviceInternal event_device = event.bluetooth.le.gatt_client_service.info->device;
const bool equal_devices = bt_device_equal(&device->opaque,
&event_device.opaque);
cl_assert_equal_b(equal_devices, true);
// clear the event
fake_event_clear_last();
fake_event_reset_count();
}
static void prv_simulate_and_assert_discovery_of_one_service(const BTDeviceInternal *device) {
// Simulate discovery of Blood Pressure service:
fake_gatt_put_discovery_indication_blood_pressure_service(TEST_GATT_CONNECTION_ID);
fake_gatt_put_discovery_complete_event(GATT_SERVICE_DISCOVERY_STATUS_SUCCESS,
TEST_GATT_CONNECTION_ID);
prv_assert_event(device, BTErrnoOK);
}
// Tests
///////////////////////////////////////////////////////////
void test_gatt_client_discovery__initialize(void) {
fake_gatt_init();
fake_event_init();
gap_le_connection_init();
}
void test_gatt_client_discovery__cleanup(void) {
gap_le_connection_deinit();
// make sure we haven't leaked any memory!
fake_pbl_malloc_check_net_allocs();
}
// -----------------------------------------------------------------------------
// Edge cases
void test_gatt_client_discovery__not_connected(void) {
BTDeviceInternal device = prv_dummy_device(1);
cl_assert_equal_i(gatt_client_discovery_discover_all(&device),
BTErrnoInvalidParameter);
cl_assert_equal_b(fake_gatt_is_service_discovery_running(), false);
}
void test_gatt_client_discovery__already_in_progress(void) {
BTDeviceInternal device = prv_connected_dummy_device(1);
// Start discovery for device:
cl_assert_equal_i(gatt_client_discovery_discover_all(&device), BTErrnoOK);
cl_assert_equal_b(fake_gatt_is_service_discovery_running(), true);
// Start again (expect to fail):
cl_assert_equal_i(gatt_client_discovery_discover_all(&device),
BTErrnoInvalidState);
// take down the connection and a disconnection event should be emitted
gap_le_connection_remove(&device);
prv_assert_event(&device, BTErrnoServiceDiscoveryDisconnected);
}
void test_gatt_client_discovery__event_is_sent_when_already_discovered(void) {
BTDeviceInternal device = prv_connected_dummy_device(1);
// Start discovery:
cl_assert_equal_i(gatt_client_discovery_discover_all(&device), BTErrnoOK);
cl_assert_equal_b(fake_gatt_is_service_discovery_running(), true);
// Simulate discovery of 1 service:
fake_gatt_put_discovery_indication_blood_pressure_service(TEST_GATT_CONNECTION_ID);
fake_gatt_put_discovery_complete_event(GATT_SERVICE_DISCOVERY_STATUS_SUCCESS,
TEST_GATT_CONNECTION_ID);
cl_assert_equal_b(fake_gatt_is_service_discovery_running(), false);
prv_assert_event(&device, BTErrnoOK);
fake_event_clear_last();
// Start discovery again, expect not to run (already discovered):
cl_assert_equal_i(gatt_client_discovery_discover_all(&device), BTErrnoOK);
cl_assert_equal_b(fake_gatt_is_service_discovery_running(), false);
// Expect event:
prv_assert_event(&device, BTErrnoOK);
}
void test_gatt_client_discovery__disconnected_during_discovery(void) {
BTDeviceInternal device = prv_connected_dummy_device(1);
// Start discovery:
cl_assert_equal_i(gatt_client_discovery_discover_all(&device), BTErrnoOK);
// Simulate disconnection:
gap_le_connection_remove(&device);
// Process racing discovery indication:
fake_gatt_put_discovery_indication_blood_pressure_service(TEST_GATT_CONNECTION_ID);
// Bluetopia's GATT module does *NOT* emit a service discovery completion event for disconnections
// Test that our API *does* emit an service discovery event with "disconnected" reason:
prv_assert_event(&device, BTErrnoServiceDiscoveryDisconnected);
}
void test_gatt_client_discovery__complete_error(void) {
BTDeviceInternal device = prv_connected_dummy_device(1);
// Start discovery:
cl_assert_equal_i(gatt_client_discovery_discover_all(&device), BTErrnoOK);
// Simulate getting one service indication...
fake_gatt_put_discovery_indication_blood_pressure_service(TEST_GATT_CONNECTION_ID);
// ... then a failure:
fake_gatt_put_discovery_complete_event(GATT_SERVICE_DISCOVERY_STATUS_RESPONSE_TIMEOUT,
TEST_GATT_CONNECTION_ID);
// Expect event with error status and 0 services:
prv_assert_event(&device,
BTErrnoWithBluetopiaError(GATT_SERVICE_DISCOVERY_STATUS_RESPONSE_TIMEOUT));
// Expect service discovery to be stopped:
cl_assert_equal_b(fake_gatt_is_service_discovery_running(), false);
}
// -------------------------------------------------------------------------------------------------
// Watchdog timeout tests
static void prv_fire_watchdog_timeouts(const BTDeviceInternal *device, int retries) {
for (int i = 0; i <= retries; ++i) {
const int start_count = fake_gatt_is_service_discovery_start_count();
const int stop_count = fake_gatt_is_service_discovery_stop_count();
// Fire the watchdog timer:
const TimerID watchdog_timer = bt_driver_gatt_get_watchdog_timer_id();
stub_new_timer_fire(watchdog_timer);
// Check whether GATT_Stop_Service_Discovery has been called:
cl_assert_equal_i(stop_count + 1, fake_gatt_is_service_discovery_stop_count());
if (i < GATT_CLIENT_DISCOVERY_MAX_RETRY) {
// Check whether GATT_Start_Service_Discovery has been called, except for the last iteration:
cl_assert_equal_i(start_count + 1, fake_gatt_is_service_discovery_start_count());
// No client event:
prv_assert_no_event();
} else {
// Last iteration: expect event with error status and 0 services:
prv_assert_event(device, BTErrnoServiceDiscoveryTimeout);
}
}
}
void test_gatt_client_discovery__watchdog_error_out_after_max_retries(void) {
BTDeviceInternal device = prv_connected_dummy_device(1);
// Start discovery:
cl_assert_equal_i(gatt_client_discovery_discover_all(&device), BTErrnoOK);
prv_fire_watchdog_timeouts(&device, GATT_CLIENT_DISCOVERY_MAX_RETRY);
}
void test_gatt_client_discovery__watchdog_retry_counter_not_affecting_successive_process(void) {
BTDeviceInternal device = prv_connected_dummy_device(1);
// Start discovery:
cl_assert_equal_i(gatt_client_discovery_discover_all(&device), BTErrnoOK);
prv_fire_watchdog_timeouts(&device, GATT_CLIENT_DISCOVERY_MAX_RETRY);
// Make sure the previous retry counter doesn't affect any new discovery process:
fake_event_clear_last();
cl_assert_equal_i(gatt_client_discovery_discover_all(&device), BTErrnoOK);
// Fire watchdog one less time than the maximum:
prv_fire_watchdog_timeouts(&device, GATT_CLIENT_DISCOVERY_MAX_RETRY - 1);
// Finally make the Bluetopia discovery events come in:
prv_simulate_and_assert_discovery_of_one_service(&device);
}
void test_gatt_client_discovery__watchdog_race_with_stopping(void) {
BTDeviceInternal device = prv_connected_dummy_device(1);
// Start discovery:
cl_assert_equal_i(gatt_client_discovery_discover_all(&device), BTErrnoOK);
// Make Bluetopia's GATT_Stop_Service_Discovery fail:
// (Service discovery has finished in the mean time, disconnected, ...)
fake_gatt_set_stop_return_value(BTGATT_ERROR_INVALID_PARAMETER);
// Fire the watchdog timer:
const TimerID watchdog_timer = bt_driver_gatt_get_watchdog_timer_id();
stub_new_timer_fire(watchdog_timer);
// No event should be generated, because the finishing / disconnecting / ... should cause
// Bluetopia to call back to prv_handle_bluetopia_discovery_event() and therefore the normal
// path will be taken.
prv_assert_no_event();
// take down the connection and a disconnection event should be emitted
gap_le_connection_remove(&device);
prv_assert_event(&device, BTErrnoServiceDiscoveryDisconnected);
}
void test_gatt_client_discovery__watchdog_race_with_restarting(void) {
BTDeviceInternal device = prv_connected_dummy_device(1);
// Start discovery:
cl_assert_equal_i(gatt_client_discovery_discover_all(&device), BTErrnoOK);
// Make Bluetopia's GATT_Start_Service_Discovery fail:
// (Disconnected in the mean time, ...)
fake_gatt_set_start_return_value(BTGATT_ERROR_INVALID_PARAMETER);
// Fire the watchdog timer:
const TimerID watchdog_timer = bt_driver_gatt_get_watchdog_timer_id();
stub_new_timer_fire(watchdog_timer);
// Stopping did not fail, but restarting did. In this case we need to generate an event that
// the discovery process failed. The error from GATT_Start_Service_Discovery is expected to be
// passed in the event.
prv_assert_event(&device, BTErrnoWithBluetopiaError(BTGATT_ERROR_INVALID_PARAMETER));
}
// -------------------------------------------------------------------------------------------------
// Re-discovery
extern BTErrno gatt_client_discovery_rediscover_all(const BTDeviceInternal *device);
void test_gatt_client_discovery__rediscover_not_already_running(void) {
BTDeviceInternal device = prv_connected_dummy_device(1);
// Start discovery:
cl_assert_equal_i(gatt_client_discovery_discover_all(&device), BTErrnoOK);
prv_simulate_and_assert_discovery_of_one_service(&device);
GAPLEConnection *connection = gap_le_connection_by_gatt_id(TEST_GATT_CONNECTION_ID);
// Expect one service, Blood Pressure:
cl_assert_equal_i(list_count(&connection->gatt_remote_services->node), 1);
fake_event_clear_last();
// Re-discovery:
cl_assert_equal_i(gatt_client_discovery_rediscover_all(&device), BTErrnoOK);
// Expect "Database Changed" event:
prv_assert_event(&device, BTErrnoServiceDiscoveryDatabaseChanged);
// Expect all services nodes to be cleaned up:
cl_assert_equal_i(list_count(&connection->gatt_remote_services->node), 0);
// Put one, expect one:
prv_simulate_and_assert_discovery_of_one_service(&device);
cl_assert_equal_i(list_count(&connection->gatt_remote_services->node), 1);
}
void test_gatt_client_discovery__rediscover_already_running(void) {
BTDeviceInternal device = prv_connected_dummy_device(1);
// Start discovery:
cl_assert_equal_i(gatt_client_discovery_discover_all(&device), BTErrnoOK);
// Put one service, but do not finish...
fake_gatt_put_discovery_indication_blood_pressure_service(TEST_GATT_CONNECTION_ID);
prv_assert_no_event();
cl_assert_equal_b(fake_gatt_is_service_discovery_running(), true);
const int stop_count_before_rediscovery = fake_gatt_is_service_discovery_stop_count();
// Re-discovery:
cl_assert_equal_i(gatt_client_discovery_rediscover_all(&device), BTErrnoOK);
// Assert the previous process has been stopped:
cl_assert_equal_i(stop_count_before_rediscovery + 1, fake_gatt_is_service_discovery_stop_count());
// Expect "Database Changed" event:
prv_assert_event(&device, BTErrnoServiceDiscoveryDatabaseChanged);
// Put one, expect one:
prv_simulate_and_assert_discovery_of_one_service(&device);
}
extern void gatt_client_discovery_discover_range(GAPLEConnection *connection,
ATTHandleRange *hdl_range);
void test_gatt_client_discovery__multiple_jobs_pending(void) {
BTDeviceInternal device = prv_connected_dummy_device(1);
GAPLEConnection *connection = gap_le_connection_by_device(&device);
ATTHandleRange range = {
.start = 0x1,
.end = 0x3000
};
ATTHandleRange range_alt = {
.start = 0x3001,
.end = 0x4000
};
cl_assert_equal_b(fake_gatt_is_service_discovery_running(), false);
// start a discovery job, pretend nothing is found
gatt_client_discovery_discover_range(connection, &range);
cl_assert_equal_b(fake_gatt_is_service_discovery_running(), true);
// pend up another service discovery job
gatt_client_discovery_discover_range(connection, &range);
cl_assert_equal_i(1, fake_gatt_is_service_discovery_start_count());
fake_gatt_put_discovery_complete_event(GATT_SERVICE_DISCOVERY_STATUS_SUCCESS,
TEST_GATT_CONNECTION_ID);
// Nothing was found so we should just have a completion event
cl_assert_equal_i(1, fake_event_get_count());
prv_assert_event(&device, BTErrnoOK);
cl_assert_equal_i(2, fake_gatt_is_service_discovery_start_count());
// next job should be in progress
cl_assert_equal_b(fake_gatt_is_service_discovery_running(), true);
// BP service was discovered
fake_gatt_put_discovery_indication_blood_pressure_service(TEST_GATT_CONNECTION_ID);
// start another job, should be pended
gatt_client_discovery_discover_range(connection, &range);
fake_gatt_put_discovery_complete_event(GATT_SERVICE_DISCOVERY_STATUS_SUCCESS,
TEST_GATT_CONNECTION_ID);
// Since we are restarting discovery over the same range and discovered one
// service two events should be generated, one about the discovery complete
// and one invalidating the just discovered service
cl_assert_equal_i(2, fake_event_get_count());
prv_assert_event(&device, BTErrnoOK);
cl_assert_equal_i(3, fake_gatt_is_service_discovery_start_count());
fake_gatt_put_discovery_indication_blood_pressure_service(TEST_GATT_CONNECTION_ID);
gatt_client_discovery_discover_range(connection, &range_alt);
// BP service should have been discovered
fake_gatt_put_discovery_complete_event(GATT_SERVICE_DISCOVERY_STATUS_SUCCESS,
TEST_GATT_CONNECTION_ID);
// Only one event should have been pended since we were not rediscovering the
// same handle range
cl_assert_equal_i(1, fake_event_get_count());
prv_assert_event(&device, BTErrnoOK);
// Nothing discovered for final query
fake_gatt_put_discovery_complete_event(GATT_SERVICE_DISCOVERY_STATUS_SUCCESS,
TEST_GATT_CONNECTION_ID);
prv_assert_event(&device, BTErrnoOK);
cl_assert_equal_i(4, fake_gatt_is_service_discovery_start_count());
cl_assert_equal_b(fake_gatt_is_service_discovery_running(), false);
}
void test_gatt_client_discovery__partial_and_full_discovery_jobs_intermixed(void) {
BTDeviceInternal device = prv_connected_dummy_device(1);
GAPLEConnection *connection = gap_le_connection_by_device(&device);
// queue up a few jobs - note only one should be running at any time
ATTHandleRange range = {
.start = 0x1,
.end = 0x3000
};
for (int i = 0; i < 10; i++) {
gatt_client_discovery_discover_range(connection, &range);
}
// kick off a full discovery
cl_assert_equal_i(gatt_client_discovery_rediscover_all(&device), BTErrnoOK);
// Assert the previous process has been stopped:
cl_assert_equal_i(1, fake_gatt_is_service_discovery_stop_count());
// Expect "Database Changed" event:
prv_assert_event(&device, BTErrnoServiceDiscoveryDatabaseChanged);
prv_simulate_and_assert_discovery_of_one_service(&device);
// None of the batched up jobs should have run
cl_assert_equal_i(2, fake_gatt_is_service_discovery_start_count());
// nothing should still be running
cl_assert_equal_b(fake_gatt_is_service_discovery_running(), false);
}
// -------------------------------------------------------------------------------------------------
// Test vectors
static void prv_assert_blood_pressure_service(const GATTService *service) {
const Service *bp_service = fake_gatt_get_blood_pressure_service();
const uint16_t service_handle = bp_service->handle;
cl_assert_equal_i(service->att_handle, service_handle);
cl_assert_equal_b(uuid_equal(&service->uuid, &bp_service->uuid), true);
cl_assert_equal_i(service->num_att_handles_included_services, bp_service->num_included_services);
cl_assert_equal_i(service->num_characteristics, bp_service->num_characteristics);
const GATTCharacteristic *characteristic_one = service->characteristics;
const Characteristic *expected_characteristic1 = &bp_service->characteristics[0];
cl_assert_equal_i(characteristic_one->att_handle_offset, expected_characteristic1->handle - service_handle);
cl_assert_equal_i(characteristic_one->num_descriptors, expected_characteristic1->num_descriptors);
cl_assert_equal_i(characteristic_one->descriptors[0].att_handle_offset,
expected_characteristic1->descriptors[0].handle - service_handle);
cl_assert_equal_i(characteristic_one->properties, expected_characteristic1->properties);
cl_assert_equal_b(uuid_equal(&characteristic_one->uuid, &expected_characteristic1->uuid), true);
// Second characteristic is tacked right after the first one:
const GATTCharacteristic *characteristic_two =
(const GATTCharacteristic *) &characteristic_one->descriptors[1];
const Characteristic *expected_characteristic2 = &bp_service->characteristics[1];
cl_assert_equal_i(characteristic_two->att_handle_offset, expected_characteristic2->handle - service_handle);
cl_assert_equal_i(characteristic_two->num_descriptors, expected_characteristic2->num_descriptors);
cl_assert_equal_i(characteristic_two->descriptors[0].att_handle_offset,
expected_characteristic2->descriptors[0].handle - service_handle);
cl_assert_equal_i(characteristic_two->properties, expected_characteristic2->properties);
cl_assert_equal_b(uuid_equal(&characteristic_two->uuid, &expected_characteristic2->uuid), true);
}
void test_gatt_client_discovery__single_blood_pressure_service(void) {
BTDeviceInternal device = prv_connected_dummy_device(1);
// Start discovery:
cl_assert_equal_i(gatt_client_discovery_discover_all(&device), BTErrnoOK);
prv_simulate_and_assert_discovery_of_one_service(&device);
GAPLEConnection *connection = gap_le_connection_by_gatt_id(TEST_GATT_CONNECTION_ID);
// Expect one service, Blood Pressure:
cl_assert_equal_i(list_count(&connection->gatt_remote_services->node), 1);
const GATTService *service = connection->gatt_remote_services->service;
prv_assert_blood_pressure_service(service);
}
@@ -0,0 +1,942 @@
/*
* 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 <bluetooth/gatt.h>
#include "comm/ble/gatt_client_subscriptions.h"
#include "comm/ble/gap_le_connection.h"
#include "comm/ble/gap_le_task.h"
#include "comm/ble/gatt_service_changed.h"
#include "clar.h"
#include <btutil/bt_device.h>
#include <btutil/bt_uuid.h>
#include "FreeRTOS.h"
#include "semphr.h"
// Fakes
///////////////////////////////////////////////////////////
#include "fake_events.h"
#include "fake_pbl_malloc.h"
#include "fake_GATTAPI.h"
#include "fake_GATTAPI_test_vectors.h"
#include "fake_new_timer.h"
#include "fake_queue.h"
#include "fake_system_task.h"
#include "stubs_regular_timer.h"
static BTErrno s_write_descriptor_cccd_result;
static BLEDescriptor s_last_cccd_ref;
static uint16_t s_last_cccd_value;
BTErrno gatt_client_op_write_descriptor_cccd(BLEDescriptor cccd, const uint16_t *value) {
s_last_cccd_ref = cccd;
s_last_cccd_value = *value;
return s_write_descriptor_cccd_result;
}
// FIXME: PBL-23945
void fake_kernel_malloc_mark(void) { }
void fake_kernel_malloc_mark_assert_equal(void) { }
// Stubs
///////////////////////////////////////////////////////////
#include "stubs_analytics.h"
#include "stubs_bluetopia_interface.h"
#include "stubs_bt_driver_gatt.h"
#include "stubs_bt_lock.h"
#include "stubs_logging.h"
#include "stubs_mutex.h"
#include "stubs_passert.h"
#include "stubs_rand_ptr.h"
#include "stubs_tick.h"
void core_dump_reset(bool is_forced) {
}
void launcher_task_add_callback(void (*callback)(void *data), void *data) {
callback(data);
}
uint16_t gaps_get_starting_att_handle(void) {
return 4;
}
// Helpers
///////////////////////////////////////////////////////////
extern void gatt_client_subscriptions_handle_server_notification(GAPLEConnection *connection,
uint16_t att_handle,
const uint8_t *att_value,
uint16_t att_length);
extern void gatt_client_subscriptions_handle_write_cccd_response(BLEDescriptor cccd,
BLEGATTError error);
extern uint16_t gatt_client_characteristic_get_handle_and_connection(
BLECharacteristic characteristic_ref,
GAPLEConnection **connection);
extern SemaphoreHandle_t gatt_client_subscription_get_semaphore(void);
extern void gatt_client_subscription_cleanup(void);
#define TEST_GATT_CONNECTION_ID (1234)
#define BOGUS_CHARACTERISTIC ((BLECharacteristic) 888)
#define BOGUS_ATT_HANDLE ((uint16_t) 0xffff)
static BTDeviceInternal s_device;
static GAPLEConnection *s_connection;
static uint16_t s_handle;
static BTDeviceInternal prv_dummy_device(uint8_t octet) {
BTDeviceAddress address = {
.octets = {
[0] = octet,
[1] = octet,
[2] = octet,
[3] = octet,
[4] = octet,
[5] = octet,
},
};
BTDevice device = bt_device_init_with_address(address, true /* is_random */);
return *(BTDeviceInternal *)(&device);
}
static BTDeviceInternal prv_connected_dummy_device(uint8_t octet) {
BTDeviceInternal device = prv_dummy_device(octet);
gap_le_connection_add(&device, NULL, true /* local_is_master */);
s_connection = gap_le_connection_by_device(&device);
s_connection->gatt_connection_id = TEST_GATT_CONNECTION_ID;
return device;
}
static void prv_assert_no_event(void) {
PebbleEvent event = fake_event_get_last();
cl_assert_equal_i(event.type, PEBBLE_NULL_EVENT);
}
static void prv_assert_subscription_event(BLECharacteristic characteristic,
BLESubscription subscription_type,
BLEGATTError error, bool kernel, bool app) {
PebbleEvent event = fake_event_get_last();
cl_assert_equal_i(event.type, PEBBLE_BLE_GATT_CLIENT_EVENT);
cl_assert_equal_i(event.bluetooth.le.gatt_client.subtype,
PebbleBLEGATTClientEventTypeCharacteristicSubscribe);
cl_assert_equal_i(event.bluetooth.le.gatt_client.subscription_type, subscription_type);
cl_assert_equal_i(event.bluetooth.le.gatt_client.object_ref, characteristic);
cl_assert_equal_i(event.bluetooth.le.gatt_client.gatt_error, error);
PebbleTaskBitset task_mask = ~0;
if (kernel) {
task_mask &= ~gap_le_pebble_task_bit_for_client(GAPLEClientKernel);
}
if (app) {
task_mask &= ~gap_le_pebble_task_bit_for_client(GAPLEClientApp);
}
cl_assert_equal_i(event.task_mask, task_mask);
}
static void prv_assert_notification_event_ext(BLECharacteristic characteristic,
const uint8_t *value, uint16_t assert_value_length,
bool kernel, bool app, bool should_consume) {
PebbleEvent event = fake_event_get_last();
cl_assert_equal_i(event.type, PEBBLE_BLE_GATT_CLIENT_EVENT);
cl_assert_equal_i(event.bluetooth.le.gatt_client.subtype,
PebbleBLEGATTClientEventTypeNotification);
PebbleTaskBitset task_mask = ~0;
if (kernel) {
task_mask &= ~gap_le_pebble_task_bit_for_client(GAPLEClientKernel);
}
if (app) {
task_mask &= ~gap_le_pebble_task_bit_for_client(GAPLEClientApp);
}
cl_assert_equal_i(event.task_mask, task_mask);
BLECharacteristic characteristic_out;
uint8_t *buffer = (uint8_t *) malloc(assert_value_length);
memset(buffer, 0, assert_value_length);
if (kernel) {
GATTBufferedNotificationHeader header = {};
gatt_client_subscriptions_get_notification_header(GAPLEClientKernel, &header);
cl_assert_equal_i(header.value_length, assert_value_length);
cl_assert_equal_i(header.characteristic, characteristic);
if (should_consume) {
uint16_t value_length = assert_value_length;
gatt_client_subscriptions_consume_notification(&characteristic_out, buffer, &value_length,
GAPLEClientKernel, NULL);
cl_assert_equal_i(memcmp(buffer, value, value_length), 0);
cl_assert_equal_i(characteristic_out, characteristic);
}
}
memset(buffer, 0, assert_value_length);
if (app) {
GATTBufferedNotificationHeader header = {};
gatt_client_subscriptions_get_notification_header(GAPLEClientApp, &header);
cl_assert_equal_i(header.value_length, assert_value_length);
cl_assert_equal_i(header.characteristic, characteristic);
if (should_consume) {
uint16_t value_length = assert_value_length;
gatt_client_subscriptions_consume_notification(&characteristic_out, buffer, &value_length,
GAPLEClientApp, NULL);
cl_assert_equal_i(memcmp(buffer, value, value_length), 0);
cl_assert_equal_i(characteristic_out, characteristic);
}
}
free(buffer);
}
static void prv_assert_notification_event(BLECharacteristic characteristic,
const uint8_t *value, uint16_t assert_value_length,
bool kernel, bool app) {
prv_assert_notification_event_ext(characteristic, value, assert_value_length, kernel, app,
true /* should_consume */);
}
static void prv_simulate_and_assert_discovery_of_one_service(const BTDeviceInternal *device) {
// Simulate discovery of Blood Pressure service:
fake_gatt_put_discovery_indication_blood_pressure_service(TEST_GATT_CONNECTION_ID);
// Simulate discovery of random 128-bit service:
fake_gatt_put_discovery_indication_random_128bit_uuid_service(TEST_GATT_CONNECTION_ID);
fake_gatt_put_discovery_complete_event(GATT_SERVICE_DISCOVERY_STATUS_SUCCESS,
TEST_GATT_CONNECTION_ID);
}
static BLECharacteristic prv_get_indicatable_characteristic(void) {
// The Blood Pressure Service UUID:
Uuid service_uuid = bt_uuid_expand_16bit(0x1810);
BLEService service;
uint8_t num_copied = gatt_client_copy_service_refs_matching_uuid(&s_device, &service, 1,
&service_uuid);
cl_assert_equal_i(num_copied, 1);
// UUID for indicatable Pressure Measurement characteristic:
Uuid characteristic_uuid = bt_uuid_expand_16bit(0x2a35);
BLECharacteristic characteristic;
num_copied = gatt_client_service_get_characteristics_matching_uuids(service, &characteristic,
&characteristic_uuid, 1);
cl_assert_equal_i(num_copied, 1);
GAPLEConnection *connection;
s_handle = gatt_client_characteristic_get_handle_and_connection(characteristic, &connection);
return characteristic;
}
static void prv_confirm_cccd_write(BLEGATTError error) {
gatt_client_subscriptions_handle_write_cccd_response(s_last_cccd_ref, error);
}
// Tests
///////////////////////////////////////////////////////////
void test_gatt_client_subscriptions__initialize(void) {
s_write_descriptor_cccd_result = BTErrnoOK;
fake_pbl_malloc_clear_tracking();
fake_event_init();
gap_le_connection_init();
gatt_client_subscription_boot();
// Prepare connected device with Blood Pressure GATT service discovered:
s_device = prv_connected_dummy_device(1);
cl_assert_equal_i(gatt_client_discovery_discover_all(&s_device), BTErrnoOK);
prv_simulate_and_assert_discovery_of_one_service(&s_device);
fake_event_clear_last();
}
void test_gatt_client_subscriptions__cleanup(void) {
for (GAPLEClient c = 0; c < GAPLEClientNum; ++c) {
gatt_client_subscriptions_cleanup_by_client(c);
}
gap_le_connection_deinit();
gatt_client_subscription_cleanup();
fake_pbl_malloc_check_net_allocs();
fake_pbl_malloc_clear_tracking();
}
// -------------------------------------------------------------------------------------------------
// gatt_client_subscriptions_subscribe
void test_gatt_client_subscriptions__subscribe_invalid_characteristic(void) {
fake_kernel_malloc_mark();
BTErrno e = gatt_client_subscriptions_subscribe(BOGUS_CHARACTERISTIC,
BLESubscriptionAny, GAPLEClientKernel);
cl_assert_equal_i(e, BTErrnoInvalidParameter);
fake_kernel_malloc_mark_assert_equal();
prv_assert_no_event();
}
void test_gatt_client_subscriptions__subscribe_no_cccd(void) {
// The random 128-bit Service UUID:
Uuid service_uuid = UuidMake(0xF7, 0x68, 0x09, 0x5B, 0x1B, 0xFA, 0x4F, 0x63,
0x97, 0xEE, 0xFD, 0xED, 0xAC, 0x66, 0xF9, 0xB0);
BLEService service;
uint8_t num_copied = gatt_client_copy_service_refs_matching_uuid(&s_device, &service, 1,
&service_uuid);
cl_assert_equal_i(num_copied, 1);
// UUID for Characteristic that has no CCCD:
Uuid characteristic_uuid = UuidMake(0xF7, 0x68, 0x09, 0x5B, 0x1B, 0xFA, 0x4F, 0x63,
0x97, 0xEE, 0xFD, 0xED, 0xAC, 0x66, 0xF9, 0xB1);
BLECharacteristic characteristic;
num_copied = gatt_client_service_get_characteristics_matching_uuids(service, &characteristic,
&characteristic_uuid, 1);
cl_assert_equal_i(num_copied, 1);
fake_kernel_malloc_mark();
// Try to subscribe to the non-subscribe-able characteristic:
BTErrno e = gatt_client_subscriptions_subscribe(characteristic,
BLESubscriptionAny, GAPLEClientKernel);
cl_assert_equal_i(e, BTErrnoInvalidParameter);
fake_kernel_malloc_mark_assert_equal();
prv_assert_no_event();
}
void test_gatt_client_subscriptions__subscribe_unsupported_subscription_type(void) {
BTErrno e;
BLECharacteristic characteristic = prv_get_indicatable_characteristic();
fake_kernel_malloc_mark();
// Try to subscribe for notifications to the indicatable (but not notify-able) characteristic:
e = gatt_client_subscriptions_subscribe(characteristic, BLESubscriptionNotifications,
GAPLEClientKernel);
cl_assert_equal_i(e, BTErrnoInvalidParameter);
fake_kernel_malloc_mark_assert_equal();
prv_assert_no_event();
// Try to subscribe for indications to the indicatable characteristic:
e = gatt_client_subscriptions_subscribe(characteristic, BLESubscriptionIndications,
GAPLEClientKernel);
cl_assert_equal_i(e, BTErrnoOK);
}
void test_gatt_client_subscriptions__subscribe_already_subscribed(void) {
BTErrno e;
BLECharacteristic characteristic = prv_get_indicatable_characteristic();
e = gatt_client_subscriptions_subscribe(characteristic, BLESubscriptionIndications,
GAPLEClientKernel);
cl_assert_equal_i(e, BTErrnoOK);
fake_event_clear_last();
fake_kernel_malloc_mark();
// Subscribe again:
e = gatt_client_subscriptions_subscribe(characteristic, BLESubscriptionIndications,
GAPLEClientKernel);
cl_assert_equal_i(e, BTErrnoInvalidState);
fake_kernel_malloc_mark_assert_equal();
prv_assert_no_event();
}
void test_gatt_client_subscriptions__unsubscribe_pending_subscription(void) {
BTErrno e;
BLECharacteristic characteristic = prv_get_indicatable_characteristic();
e = gatt_client_subscriptions_subscribe(characteristic, BLESubscriptionIndications,
GAPLEClientKernel);
cl_assert_equal_i(e, BTErrnoOK);
fake_event_clear_last();
fake_kernel_malloc_mark();
// Un-subscribe, while subscribing process is still pending:
e = gatt_client_subscriptions_subscribe(characteristic, BLESubscriptionNone, GAPLEClientKernel);
cl_assert_equal_i(e, BTErrnoInvalidState);
fake_kernel_malloc_mark_assert_equal();
prv_assert_no_event();
}
void test_gatt_client_subscriptions__subscribe_oom_for_subscription_allocation(void) {
fake_kernel_malloc_mark();
fake_malloc_set_largest_free_block(sizeof(GATTClientSubscriptionNode) - 1);
BTErrno e = gatt_client_subscriptions_subscribe(prv_get_indicatable_characteristic(),
BLESubscriptionIndications,
GAPLEClientKernel);
cl_assert_equal_i(e, BTErrnoNotEnoughResources);
fake_kernel_malloc_mark_assert_equal();
prv_assert_no_event();
}
void test_gatt_client_subscriptions__subscribe_oom_for_buffer_allocation(void) {
fake_kernel_malloc_mark();
fake_malloc_set_largest_free_block(sizeof(GATTClientSubscriptionNode));
BTErrno e = gatt_client_subscriptions_subscribe(prv_get_indicatable_characteristic(),
BLESubscriptionIndications,
GAPLEClientKernel);
cl_assert_equal_i(e, BTErrnoNotEnoughResources);
fake_kernel_malloc_mark_assert_equal();
prv_assert_no_event();
}
void test_gatt_client_subscriptions__subscribe_cccd_write_error(void) {
s_write_descriptor_cccd_result = -1;
fake_kernel_malloc_mark();
// Try to subscribe for indications to the indicatable characteristic:
BTErrno e = gatt_client_subscriptions_subscribe(prv_get_indicatable_characteristic(),
BLESubscriptionIndications,
GAPLEClientKernel);
cl_assert_equal_i(e, -1);
fake_kernel_malloc_mark_assert_equal();
prv_assert_no_event();
}
void test_gatt_client_subscriptions__unsubscribe_no_clients_subscribed(void) {
fake_kernel_malloc_mark();
BTErrno e;
BLECharacteristic characteristic = prv_get_indicatable_characteristic();
e = gatt_client_subscriptions_subscribe(characteristic, BLESubscriptionNone, GAPLEClientKernel);
cl_assert_equal_i(e, BTErrnoInvalidState);
fake_kernel_malloc_mark_assert_equal();
prv_assert_no_event();
}
void test_gatt_client_subscriptions__unsubscribe_not_subscribed_but_other_client_is(void) {
BTErrno e;
BLECharacteristic characteristic = prv_get_indicatable_characteristic();
e = gatt_client_subscriptions_subscribe(characteristic, BLESubscriptionIndications, GAPLEClientApp);
cl_assert_equal_i(e, BTErrnoOK);
fake_kernel_malloc_mark();
e = gatt_client_subscriptions_subscribe(characteristic, BLESubscriptionNone, GAPLEClientKernel);
cl_assert_equal_i(e, BTErrnoInvalidState);
fake_kernel_malloc_mark_assert_equal();
prv_assert_no_event();
}
void test_gatt_client_subscriptions__subscribe_first_subscriber(void) {
BLECharacteristic characteristic = prv_get_indicatable_characteristic();
BTErrno e = gatt_client_subscriptions_subscribe(characteristic, BLESubscriptionIndications,
GAPLEClientKernel);
cl_assert_equal_i(e, BTErrnoOK);
cl_assert_equal_i(s_last_cccd_value, BLESubscriptionIndications);
prv_confirm_cccd_write(BLEGATTErrorSuccess);
prv_assert_subscription_event(characteristic, BLESubscriptionIndications, BLEGATTErrorSuccess,
true /* kernel */, false /* app */);
}
void test_gatt_client_subscriptions__subscribe_not_first_subscriber(void) {
BLECharacteristic characteristic = prv_get_indicatable_characteristic();
// Subscribe kernel:
BTErrno e = gatt_client_subscriptions_subscribe(characteristic, BLESubscriptionIndications,
GAPLEClientKernel);
cl_assert_equal_i(e, BTErrnoOK);
cl_assert_equal_i(s_last_cccd_value, BLESubscriptionIndications);
// Simulate getting confirmation from remote:
prv_confirm_cccd_write(BLEGATTErrorSuccess);
fake_event_clear_last();
// Subscribe app:
e = gatt_client_subscriptions_subscribe(characteristic, BLESubscriptionIndications,
GAPLEClientApp);
// App should get event immediately:
prv_assert_subscription_event(characteristic, BLESubscriptionIndications, BLEGATTErrorSuccess,
false /* kernel */, true /* app */);
}
void test_gatt_client_subscriptions__two_subscribers_before_cccd_write_response(void) {
BLECharacteristic characteristic = prv_get_indicatable_characteristic();
// Subscribe kernel:
BTErrno e = gatt_client_subscriptions_subscribe(characteristic, BLESubscriptionIndications,
GAPLEClientKernel);
cl_assert_equal_i(e, BTErrnoOK);
cl_assert_equal_i(s_last_cccd_value, BLESubscriptionIndications);
s_last_cccd_value = ~0;
// Subscribe app:
e = gatt_client_subscriptions_subscribe(characteristic, BLESubscriptionIndications,
GAPLEClientApp);
cl_assert_equal_i(e, BTErrnoOK);
// Should not be written again, because BLESubscriptionIndications was already written:
cl_assert_equal_i(s_last_cccd_value, (uint16_t) ~0);
fake_event_clear_last();
// Simulate getting confirmation from remote:
prv_confirm_cccd_write(BLEGATTErrorSuccess);
// Expect one event for both Kernel + App:
prv_assert_subscription_event(characteristic, BLESubscriptionIndications, BLEGATTErrorSuccess,
true /* kernel */, true /* app */);
}
void test_gatt_client_subscriptions__unsubscribe_last_subscriber(void) {
BTErrno e;
BLECharacteristic characteristic = prv_get_indicatable_characteristic();
fake_kernel_malloc_mark();
// Subscribe kernel:
e = gatt_client_subscriptions_subscribe(characteristic, BLESubscriptionIndications,
GAPLEClientKernel);
cl_assert_equal_i(e, BTErrnoOK);
// Simulate getting confirmation from remote:
prv_confirm_cccd_write(BLEGATTErrorSuccess);
fake_event_clear_last();
// Unsubscribe kernel:
e = gatt_client_subscriptions_subscribe(characteristic, BLESubscriptionNone, GAPLEClientKernel);
cl_assert_equal_i(e, BTErrnoOK);
// Kernel should get event immediately:
prv_assert_subscription_event(characteristic, BLESubscriptionNone, BLEGATTErrorSuccess,
true /* kernel */, false /* app */);
// CCCD value should be 0 now:
cl_assert_equal_i(s_last_cccd_value, 0);
// Verify everything is cleaned up:
fake_kernel_malloc_mark_assert_equal();
}
void test_gatt_client_subscriptions__unsubscribe_not_last_subscriber(void) {
BLECharacteristic characteristic = prv_get_indicatable_characteristic();
// Subscribe kernel:
BTErrno e = gatt_client_subscriptions_subscribe(characteristic, BLESubscriptionIndications,
GAPLEClientKernel);
cl_assert_equal_i(e, BTErrnoOK);
cl_assert_equal_i(s_last_cccd_value, BLESubscriptionIndications);
// Simulate getting confirmation from remote:
prv_confirm_cccd_write(BLEGATTErrorSuccess);
fake_event_clear_last();
// Subscribe app:
e = gatt_client_subscriptions_subscribe(characteristic, BLESubscriptionIndications,
GAPLEClientApp);
s_last_cccd_value = ~0;
// Unsubscribe kernel:
e = gatt_client_subscriptions_subscribe(characteristic, BLESubscriptionNone, GAPLEClientKernel);
// Kernel should get event immediately:
prv_assert_subscription_event(characteristic, BLESubscriptionNone, BLEGATTErrorSuccess,
true /* kernel */, false /* app */);
// CCCD value should not be written, because app is still subscribed:
cl_assert_equal_i(s_last_cccd_value, (uint16_t) ~0);
}
void test_gatt_client_subscriptions__subscribe_failed_cccd_write(void) {
BLECharacteristic characteristic = prv_get_indicatable_characteristic();
fake_kernel_malloc_mark();
// Subscribe kernel:
BTErrno e = gatt_client_subscriptions_subscribe(characteristic, BLESubscriptionIndications,
GAPLEClientKernel);
cl_assert_equal_i(e, BTErrnoOK);
cl_assert_equal_i(s_last_cccd_value, BLESubscriptionIndications);
// Simulate getting confirmation from remote:
prv_confirm_cccd_write(BLEGATTErrorInvalidHandle);
// Expect kernel event with 'BLESubscriptionNone' type and error bubbled up:
prv_assert_subscription_event(characteristic, BLESubscriptionNone, BLEGATTErrorInvalidHandle,
true /* kernel */, false /* app */);
// Verify everything is cleaned up:
fake_kernel_malloc_mark_assert_equal();
}
// -------------------------------------------------------------------------------------------------
// gatt_client_subscriptions_handle_server_notification &
// gatt_client_subscriptions_consume_notification
void test_gatt_client_subscriptions__notification_but_no_subscribers(void) {
const uint8_t value = 0xAA;
gatt_client_subscriptions_handle_server_notification(s_connection, s_handle,
&value, sizeof(value));
prv_assert_no_event();
}
void test_gatt_client_subscriptions__cccd_write_confirmation_but_no_subscription(void) {
s_last_cccd_ref = 1;
prv_confirm_cccd_write(BLEGATTErrorSuccess);
// This used to cause a crash:
// https://pebbletechnology.atlassian.net/browse/PBL-23455
}
void test_gatt_client_subscriptions__notification_single_subscriber(void) {
// Subscribe app:
BLECharacteristic characteristic = prv_get_indicatable_characteristic();
BTErrno e = gatt_client_subscriptions_subscribe(characteristic, BLESubscriptionIndications,
GAPLEClientApp);
cl_assert_equal_i(e, BTErrnoOK);
// Simulate getting confirmation from remote:
prv_confirm_cccd_write(BLEGATTErrorSuccess);
fake_event_clear_last();
// Nothing to be read before getting the notification:
bool has_notification = gatt_client_subscriptions_get_notification_header(GAPLEClientApp,
NULL);
cl_assert_equal_b(has_notification, false);
const uint8_t value[] = {0xAA, 0xBB, 0xCC};
gatt_client_subscriptions_handle_server_notification(s_connection, s_handle,
value, sizeof(value));
prv_assert_notification_event(characteristic, value, sizeof(value),
false /* kernel */, true /* app */);
// Nothing to be read after "consuming" it:
has_notification = gatt_client_subscriptions_get_notification_header(GAPLEClientApp, NULL);
cl_assert_equal_b(has_notification, false);
}
void test_gatt_client_subscriptions__zero_length_notification(void) {
// Subscribe app:
BLECharacteristic characteristic = prv_get_indicatable_characteristic();
BTErrno e = gatt_client_subscriptions_subscribe(characteristic, BLESubscriptionIndications,
GAPLEClientApp);
cl_assert_equal_i(e, BTErrnoOK);
// Simulate getting confirmation from remote:
prv_confirm_cccd_write(BLEGATTErrorSuccess);
fake_event_clear_last();
gatt_client_subscriptions_handle_server_notification(s_connection, s_handle, NULL, 0);
prv_assert_notification_event(characteristic, NULL, 0, false /* kernel */, true /* app */);
// Nothing to be read after "consuming" it:
bool has_notification = gatt_client_subscriptions_get_notification_header(GAPLEClientApp, NULL);
cl_assert_equal_b(has_notification, false);
}
void test_gatt_client_subscriptions__notification_app_and_kernel_subscribers(void) {
// Subscribe app:
BLECharacteristic characteristic = prv_get_indicatable_characteristic();
BTErrno e = gatt_client_subscriptions_subscribe(characteristic, BLESubscriptionIndications,
GAPLEClientApp);
cl_assert_equal_i(e, BTErrnoOK);
// Subscribe kernel:
e = gatt_client_subscriptions_subscribe(characteristic, BLESubscriptionIndications,
GAPLEClientKernel);
cl_assert_equal_i(e, BTErrnoOK);
// Simulate getting confirmation from remote:
prv_confirm_cccd_write(BLEGATTErrorSuccess);
fake_event_clear_last();
fake_event_reset_count();
const uint8_t value[] = {0xAA, 0xBB, 0xCC};
gatt_client_subscriptions_handle_server_notification(s_connection, s_handle,
value, sizeof(value));
cl_assert_equal_i(fake_event_get_count(), 1);
// Send another notification, before reading out the previous one:
gatt_client_subscriptions_handle_server_notification(s_connection, s_handle,
value, sizeof(value));
// Only one event should have been put on the queue:
cl_assert_equal_i(fake_event_get_count(), 1);
// Assert 2 events can be read out:
prv_assert_notification_event(characteristic, value, sizeof(value),
true /* kernel */, true /* app */);
prv_assert_notification_event(characteristic, value, sizeof(value),
true /* kernel */, true /* app */);
// Send the 3rd notification:
gatt_client_subscriptions_handle_server_notification(s_connection, s_handle,
value, sizeof(value));
cl_assert_equal_i(fake_event_get_count(), 2);
prv_assert_notification_event(characteristic, value, sizeof(value),
true /* kernel */, true /* app */);
}
static TickType_t prv_taking_too_long_to_consume_yield_cb(QueueHandle_t queue) {
return milliseconds_to_ticks(1000);
}
static TickType_t prv_consume_in_time_yield_cb(QueueHandle_t queue) {
// Consume while BT task is waiting for buffer to be freed up:
BLECharacteristic characteristic_out;
uint8_t *value_out = (uint8_t *) malloc(GATT_CLIENT_SUBSCRIPTIONS_BUFFER_SIZE);
uint16_t value_length = GATT_CLIENT_SUBSCRIPTIONS_BUFFER_SIZE;
gatt_client_subscriptions_consume_notification(&characteristic_out, value_out, &value_length,
GAPLEClientApp, NULL);
free(value_out);
return milliseconds_to_ticks(5);
}
void test_gatt_client_subscriptions__notification_buffer_full(void) {
// Subscribe app:
BLECharacteristic characteristic = prv_get_indicatable_characteristic();
BTErrno e = gatt_client_subscriptions_subscribe(characteristic, BLESubscriptionIndications,
GAPLEClientApp);
cl_assert_equal_i(e, BTErrnoOK);
// Simulate getting confirmation from remote:
prv_confirm_cccd_write(BLEGATTErrorSuccess);
fake_event_clear_last();
// We had a bug at some point where the header size was not taken into account correctly, take
// a value that won't fit by 1 byte, so this bug does not happen again:
const size_t too_big = GATT_CLIENT_SUBSCRIPTIONS_BUFFER_SIZE -
sizeof(GATTBufferedNotificationHeader) + 1;
uint8_t *value = (uint8_t *) malloc(too_big);
memset(value, 0x55, too_big);
gatt_client_subscriptions_handle_server_notification(s_connection, s_handle,
value, too_big);
prv_assert_no_event();
// Receive a GATT notification that is supposed to fill up the buffer entirely:
const size_t fill_entirely_size = (GATT_CLIENT_SUBSCRIPTIONS_BUFFER_SIZE -
sizeof(GATTBufferedNotificationHeader));
gatt_client_subscriptions_handle_server_notification(s_connection, s_handle,
value, fill_entirely_size);
prv_assert_notification_event_ext(characteristic, value, fill_entirely_size,
false /* kernel */, true /* app */, false /* should_consume */);
// Receive another GATT notification. Won't fit until consumed. Consuming is taking to long:
fake_queue_set_yield_callback(gatt_client_subscription_get_semaphore(),
prv_taking_too_long_to_consume_yield_cb);
fake_event_clear_last();
gatt_client_subscriptions_handle_server_notification(s_connection, s_handle,
value, 1 /* one byte */);
// Data will be dropped, no event :(
prv_assert_no_event();
// Receive another GATT notification. Won't fit until consumed.
// Consuming is happening before the timeout hits (in the yield callback):
fake_queue_set_yield_callback(gatt_client_subscription_get_semaphore(),
prv_consume_in_time_yield_cb);
gatt_client_subscriptions_handle_server_notification(s_connection, s_handle,
value, 1 /* one byte */);
prv_assert_notification_event(characteristic, value, 1 /* one byte */,
false /* kernel */, true /* app */);
free(value);
}
void test_gatt_client_subscriptions__consume_but_too_small_buffer(void) {
// Subscribe app:
BLECharacteristic characteristic = prv_get_indicatable_characteristic();
BTErrno e = gatt_client_subscriptions_subscribe(characteristic, BLESubscriptionIndications,
GAPLEClientApp);
cl_assert_equal_i(e, BTErrnoOK);
// Simulate getting confirmation from remote:
prv_confirm_cccd_write(BLEGATTErrorSuccess);
fake_event_clear_last();
const uint8_t value[] = {0xAA, 0xBB, 0xCC};
gatt_client_subscriptions_handle_server_notification(s_connection, s_handle,
value, sizeof(value));
BLECharacteristic handle_out = ~0;
uint8_t out[1];
uint16_t value_length = sizeof(out);
bool has_more = true;
const uint16_t next_length =
gatt_client_subscriptions_consume_notification(&handle_out, out, &value_length,
GAPLEClientApp, &has_more);
cl_assert_equal_i(handle_out, BLE_CHARACTERISTIC_INVALID);
cl_assert_equal_i(value_length, 0);
// Notification will be eaten, regardless of whether it was copied:
cl_assert_equal_b(has_more, false);
cl_assert_equal_i(next_length, 0);
}
void test_gatt_client_subscriptions__consume_but_nothing_in_buffer(void) {
// Subscribe app:
BLECharacteristic characteristic = prv_get_indicatable_characteristic();
BTErrno e = gatt_client_subscriptions_subscribe(characteristic, BLESubscriptionIndications,
GAPLEClientApp);
cl_assert_equal_i(e, BTErrnoOK);
// Simulate getting confirmation from remote:
prv_confirm_cccd_write(BLEGATTErrorSuccess);
fake_event_clear_last();
BLECharacteristic handle_out = ~0;
uint8_t out[1];
uint16_t value_length = sizeof(out);
bool has_more = true;
const uint16_t next_length =
gatt_client_subscriptions_consume_notification(&handle_out, out, &value_length,
GAPLEClientApp, &has_more);
cl_assert_equal_i(handle_out, BLE_CHARACTERISTIC_INVALID);
cl_assert_equal_i(value_length, 0);
cl_assert_equal_b(has_more, false);
cl_assert_equal_i(next_length, 0);
}
void test_gatt_client_subscriptions__consume_but_buffer_client_buffer_null(void) {
bool has_more = true;
BLECharacteristic handle_out = ~0;
uint8_t out[1];
uint16_t value_length = sizeof(out);
const uint16_t next_length =
gatt_client_subscriptions_consume_notification(&handle_out, out, &value_length,
GAPLEClientApp, &has_more);
cl_assert_equal_i(next_length, 0);
cl_assert_equal_b(has_more, false);
}
void test_gatt_client_subscriptions__notification_consume_without_notification(void) {
BLECharacteristic characteristic = prv_get_indicatable_characteristic();
uint16_t value_length;
GATTBufferedNotificationHeader header = {
.characteristic = BLE_CHARACTERISTIC_INVALID,
.value_length = 0,
};
bool has_more = gatt_client_subscriptions_get_notification_header(GAPLEClientKernel,
&header);
cl_assert_equal_i(header.value_length, 0);
cl_assert_equal_i(header.characteristic, BLE_CHARACTERISTIC_INVALID);
cl_assert_equal_b(has_more, false);
BLECharacteristic characteristic_out = ~0;
uint8_t value = 0xff;
value_length = sizeof(value);
gatt_client_subscriptions_consume_notification(&characteristic_out, &value, &value_length,
GAPLEClientKernel, &has_more);
// Expect untouched:
cl_assert_equal_i(characteristic_out, ~0);
cl_assert_equal_i(value, 0xff);
cl_assert_equal_b(has_more, false);
}
// -------------------------------------------------------------------------------------------------
// gatt_client_subscriptions_cleanup_by_client
void test_gatt_client_subscriptions__cleanup_by_client(void) {
fake_kernel_malloc_mark();
// Subscribe app:
BLECharacteristic characteristic = prv_get_indicatable_characteristic();
BTErrno e = gatt_client_subscriptions_subscribe(characteristic, BLESubscriptionIndications,
GAPLEClientApp);
cl_assert_equal_i(e, BTErrnoOK);
gatt_client_subscriptions_cleanup_by_client(GAPLEClientApp);
prv_assert_no_event();
fake_kernel_malloc_mark_assert_equal();
}
// -------------------------------------------------------------------------------------------------
// gatt_client_subscriptions_cleanup_by_connection
bool gatt_client_get_event_pending_state(GAPLEClient);
static void prv_pend_events_to_kernel_and_app(void) {
// fake pend an event to the kernel
gatt_client_subscriptions_reschedule(GAPLEClientKernel);
cl_assert_equal_b(gatt_client_get_event_pending_state(GAPLEClientKernel), true);
fake_event_clear_last();
// fake pend an event to the app
gatt_client_subscriptions_reschedule(GAPLEClientApp);
cl_assert_equal_b(gatt_client_get_event_pending_state(GAPLEClientApp), true);
fake_event_clear_last();
}
static void prv_assert_no_pending_events_to_kernel_and_app(void) {
cl_assert_equal_b(gatt_client_get_event_pending_state(GAPLEClientKernel), false);
cl_assert_equal_b(gatt_client_get_event_pending_state(GAPLEClientApp), false);
}
void test_gatt_client_subscriptions__cleanup_by_connection(void) {
fake_kernel_malloc_mark();
// Subscribe app and kernel:
BLECharacteristic characteristic = prv_get_indicatable_characteristic();
BTErrno e = gatt_client_subscriptions_subscribe(characteristic, BLESubscriptionIndications,
GAPLEClientApp);
cl_assert_equal_i(e, BTErrnoOK);
e = gatt_client_subscriptions_subscribe(characteristic, BLESubscriptionIndications,
GAPLEClientKernel);
cl_assert_equal_i(e, BTErrnoOK);
prv_pend_events_to_kernel_and_app();
gatt_client_subscriptions_cleanup_by_connection(s_connection, false /* should_unsubscribe */);
prv_assert_no_event();
// there should be no more subscriptions
cl_assert(s_connection->gatt_subscriptions == NULL);
prv_assert_no_pending_events_to_kernel_and_app();
fake_kernel_malloc_mark_assert_equal();
}
extern void gatt_client_subscription_cleanup_by_att_handle_range(
struct GAPLEConnection *connection, ATTHandleRange *range);
void test_gatt_client_subscriptions__cleanup_by_att_handle_range(void) {
fake_kernel_malloc_mark();
BLECharacteristic characteristic = prv_get_indicatable_characteristic();
BTErrno e = gatt_client_subscriptions_subscribe(characteristic, BLESubscriptionIndications,
GAPLEClientApp);
cl_assert_equal_i(e, BTErrnoOK);
e = gatt_client_subscriptions_subscribe(characteristic, BLESubscriptionIndications,
GAPLEClientKernel);
cl_assert_equal_i(e, BTErrnoOK);
cl_assert(s_connection->gatt_subscriptions != NULL);
ATTHandleRange range;
fake_gatt_get_bp_att_handle_range(&range.start, &range.end);
ATTHandleRange bogus_range = {
.start = range.end + 1,
.end = range.end + 5
};
prv_pend_events_to_kernel_and_app();
// should have no effect since service is not in this range
gatt_client_subscription_cleanup_by_att_handle_range(s_connection, &bogus_range);
prv_assert_no_event();
cl_assert(s_connection->gatt_subscriptions != NULL);
// should actually remove everything
gatt_client_subscription_cleanup_by_att_handle_range(s_connection, &range);
prv_assert_no_event();
// there should be no more subscriptions
cl_assert(s_connection->gatt_subscriptions == NULL);
prv_assert_no_pending_events_to_kernel_and_app();
fake_kernel_malloc_mark_assert_equal();
}
// -------------------------------------------------------------------------------------------------
// TODO: Write tests that exercise applib/bluetooth/ble_client.c
@@ -0,0 +1,236 @@
/*
* 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 "comm/ble/gatt_service_changed.h"
#include "comm/ble/gap_le_connection.h"
#include "kernel/events.h"
#include "clar.h"
#include <btutil/bt_device.h>
// Fakes
///////////////////////////////////////////////////////////
#include "fake_GAPAPI.h"
#include "fake_GATTAPI.h"
#include "fake_GATTAPI_test_vectors.h"
#include "fake_pbl_malloc.h"
#include "fake_new_timer.h"
#include "fake_rtc.h"
#include "fake_system_task.h"
// Stubs
///////////////////////////////////////////////////////////
#include "stubs_bluetopia_interface.h"
#include "stubs_bt_driver_gatt.h"
#include "stubs_bt_lock.h"
#include "stubs_events.h"
#include "stubs_gatt_client_subscriptions.h"
#include "stubs_logging.h"
#include "stubs_mutex.h"
#include "stubs_passert.h"
#include "stubs_prompt.h"
#include "stubs_rand_ptr.h"
#include "stubs_regular_timer.h"
extern bool prv_contains_service_changed_characteristic(
GAPLEConnection *connection,
const GATT_Service_Discovery_Indication_Data_t *event);
void core_dump_reset(bool is_forced) {
}
static GAPLEConnection s_connection;
GAPLEConnection *gap_le_connection_by_device(const BTDeviceInternal *addr) {
return &s_connection;
}
GAPLEConnection *gap_le_connection_by_addr(const BTDeviceAddress *addr) {
return &s_connection;
}
GAPLEConnection *gap_le_connection_by_gatt_id(unsigned int connection_id) {
return &s_connection;
}
bool gap_le_connection_is_valid(const GAPLEConnection *conn) {
return true;
}
GAPLEConnection *gap_le_connection_any(void) {
return NULL;
}
uint16_t gaps_get_starting_att_handle(void) {
return 4;
}
GAPLEConnection *gatt_client_characteristic_get_connection(BLECharacteristic characteristic_ref) {
return NULL;
}
BLEService gatt_client_att_handle_get_service(
GAPLEConnection *connection, uint16_t att_handle, const GATTServiceNode **service_node_out) {
return 0;
}
uint8_t gatt_client_copy_service_refs_by_discovery_generation(
const BTDeviceInternal *device, BLEService services_out[],
uint8_t num_services, uint8_t discovery_gen) { return 0;}
void gatt_client_service_get_all_characteristics_and_descriptors(
GAPLEConnection *connection, GATTService *service,
BLECharacteristic *characteristic_hdls_out,
BLEDescriptor *descriptor_hdls_out) { }
void launcher_task_add_callback(void (*callback)(void *data), void *data) {
callback(data);
}
// FIXME: PBL-23945
void fake_kernel_malloc_mark(void) { }
void fake_kernel_malloc_mark_assert_equal(void) { }
// Helpers
///////////////////////////////////////////////////////////
#define TEST_GATT_CONNECTION_ID (1234)
#define TEST_BT_STACK_ID (1)
typedef enum {
Unknown,
Handled,
Unhandled,
} HandleResult;
static HandleResult s_last_handle_discovery_result;
static void prv_bluetopia_service_discovery_cb(unsigned int stack_id,
GATT_Service_Discovery_Event_Data_t *event,
unsigned long callback_param) {
cl_assert_equal_i(stack_id, TEST_BT_STACK_ID);
if (event->Event_Data_Type == etGATT_Service_Discovery_Indication) {
const GATT_Service_Discovery_Indication_Data_t *indication =
event->Event_Data.GATT_Service_Discovery_Indication_Data;
cl_assert_equal_i(s_connection.gatt_connection_id, TEST_GATT_CONNECTION_ID);
cl_assert_equal_i(indication->ConnectionID, TEST_GATT_CONNECTION_ID);
s_last_handle_discovery_result =
prv_contains_service_changed_characteristic(&s_connection, indication) ? Handled : Unhandled;
}
}
// Tests
///////////////////////////////////////////////////////////
void test_gatt_service_changed_client__initialize(void) {
s_last_handle_discovery_result = Unknown;
fake_gatt_init();
s_connection = (GAPLEConnection) {
.gatt_connection_id = TEST_GATT_CONNECTION_ID,
.gatt_service_changed_att_handle = 0,
};
GATT_Start_Service_Discovery_Handle_Range(TEST_BT_STACK_ID, TEST_GATT_CONNECTION_ID, NULL, 0, NULL,
prv_bluetopia_service_discovery_cb, 0);
}
void test_gatt_service_changed_client__cleanup(void) {
}
// Discovery
///////////////////////////////////////////////////////////
void test_gatt_service_changed_client__handle_non_gatt_profile_service(void) {
fake_gatt_put_discovery_indication_blood_pressure_service(TEST_GATT_CONNECTION_ID);
cl_assert_equal_i(s_last_handle_discovery_result, Unhandled);
}
void test_gatt_service_changed_client__handle_gatt_profile_service(void) {
fake_gatt_put_discovery_indication_gatt_profile_service(TEST_GATT_CONNECTION_ID,
true /* has_service_changed_characteristic */);
cl_assert_equal_i(s_last_handle_discovery_result, Handled);
// Verify the CCCD of the Service Changed characteristic has been written:
cl_assert_equal_i(fake_gatt_write_last_written_handle(),
fake_gatt_gatt_profile_service_service_changed_cccd_att_handle());
// Simulate getting a Write Response confirmation for the written CCCD:
fake_gatt_put_write_response_for_last_write();
// Today we don't really do anything upon getting the confirmation
}
void test_gatt_service_changed_client__handle_gatt_profile_service_missing_service_changed(void) {
fake_gatt_put_discovery_indication_gatt_profile_service(TEST_GATT_CONNECTION_ID,
false /* has_service_changed_characteristic */);
cl_assert_equal_i(s_last_handle_discovery_result, Handled);
}
// Characteristic Value Indications
///////////////////////////////////////////////////////////
void test_gatt_service_changed_client__handle_indication_non_service_changed(void) {
fake_gatt_put_discovery_indication_gatt_profile_service(TEST_GATT_CONNECTION_ID,
true /* has_service_changed_characteristic */);
const uint8_t value;
const bool handled = gatt_service_changed_client_handle_indication(&s_connection, 0xfffe,
&value, sizeof(value));
cl_assert_equal_b(handled, false);
}
void test_gatt_service_changed_client__handle_indication_service_changed(void) {
fake_gatt_put_discovery_indication_gatt_profile_service(TEST_GATT_CONNECTION_ID,
true /* has_service_changed_characteristic */);
const uint16_t att_handle = fake_gatt_gatt_profile_service_service_changed_att_handle();
fake_kernel_malloc_mark();
const int start_count_before_indication = fake_gatt_is_service_discovery_start_count();
const uint16_t handle_range[2] = {
[0] = 0x1,
[1] = 0xfffe,
};
const bool handled = gatt_service_changed_client_handle_indication(&s_connection, att_handle,
(const uint8_t *) handle_range, sizeof(uint16_t) * 2);
// Re-discovery is trigger on KernelBG:
fake_system_task_callbacks_invoke_pending();
// The KernelBG trip uses kernel_malloc, make sure it's cleaning up properly:
fake_kernel_malloc_mark_assert_equal();
cl_assert_equal_b(handled, true);
// Expect service discovery to be started once more:
cl_assert_equal_i(start_count_before_indication + 1,
fake_gatt_is_service_discovery_start_count());
}
void test_gatt_service_changed_client__handle_indication_service_changed_malformatted(void) {
fake_gatt_put_discovery_indication_gatt_profile_service(TEST_GATT_CONNECTION_ID,
true /* has_service_changed_characteristic */);
const uint16_t att_handle = fake_gatt_gatt_profile_service_service_changed_att_handle();
const uint16_t handle_range[1] = {
[0] = 0x1,
};
const bool handled = gatt_service_changed_client_handle_indication(&s_connection, att_handle,
(const uint8_t *) handle_range, sizeof(uint16_t) * 1);
cl_assert_equal_b(handled, true);
}
@@ -0,0 +1,213 @@
/*
* 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 "comm/ble/gatt_service_changed.h"
#include "comm/ble/gap_le_connection.h"
#include "bluetopia_interface.h"
#include "kernel/events.h"
#include "clar.h"
#include <btutil/bt_device.h>
extern void gatt_service_changed_server_init(void);
// Fakes
///////////////////////////////////////////////////////////
#include "fake_GAPAPI.h"
#include "fake_GATTAPI.h"
#include "fake_GATTAPI_test_vectors.h"
#include "fake_pbl_malloc.h"
#include "fake_new_timer.h"
#include "fake_rtc.h"
#include "fake_system_task.h"
// Stubs
///////////////////////////////////////////////////////////
#include "stubs_bluetopia_interface.h"
#include "stubs_bt_driver_gatt.h"
#include "stubs_bt_driver_gatt_client_discovery.h"
#include "stubs_bt_lock.h"
#include "stubs_events.h"
#include "stubs_gatt_client_subscriptions.h"
#include "stubs_logging.h"
#include "stubs_mutex.h"
#include "stubs_passert.h"
#include "stubs_prompt.h"
#include "stubs_rand_ptr.h"
#include "stubs_regular_timer.h"
uint16_t gaps_get_starting_att_handle(void) {
return 4;
}
BLEService gatt_client_att_handle_get_service(
GAPLEConnection *connection, uint16_t att_handle, const GATTServiceNode **service_node_out) {
return 0;
}
uint8_t gatt_client_copy_service_refs_by_discovery_generation(
const BTDeviceInternal *device, BLEService services_out[],
uint8_t num_services, uint8_t discovery_gen) {
return 0;
}
void gatt_client_service_get_all_characteristics_and_descriptors(
GAPLEConnection *connection, GATTService *service,
BLECharacteristic *characteristic_hdls_out,
BLEDescriptor *descriptor_hdls_out) {
}
void launcher_task_add_callback(void (*callback)(void *data), void *data) {
callback(data);
}
// Helpers
///////////////////////////////////////////////////////////
static const BTDeviceInternal s_device = {
.address = {
.octets = {
1, 2, 3, 4, 5, 6,
},
},
};
static uint32_t s_connection_id = 1;
static GAPLEConnection *s_connection;
static void prv_cccd_write(bool is_subscribing) {
GattServerSubscribeEvent event = {
.connection_id = s_connection_id,
.dev_address = s_device.address,
.is_subscribing = is_subscribing,
};
bt_driver_cb_gatt_service_changed_server_subscribe(&event);
}
static void prv_process_pending_callbacks(GAPLEConnection *connection) {
if (connection && connection->gatt_service_changed_indication_timer) {
stub_new_timer_fire(connection->gatt_service_changed_indication_timer);
}
fake_system_task_callbacks_invoke_pending();
}
#define prv_expect_service_changed_indication_api_call_count(expected_count) \
{ \
prv_process_pending_callbacks(s_connection); \
cl_assert_equal_i(fake_gatt_get_service_changed_indication_count(), expected_count); \
}
// Tests
///////////////////////////////////////////////////////////
void test_gatt_service_changed_server__initialize(void) {
gatt_service_changed_server_init();
fake_gatt_init();
gap_le_connection_init();
gap_le_connection_add(&s_device, NULL, false /* local_is_master */);
s_connection = gap_le_connection_by_device(&s_device);
cl_assert(s_connection);
s_connection->gatt_connection_id = s_connection_id;
}
void test_gatt_service_changed_server__cleanup(void) {
if (s_connection) {
gap_le_connection_remove(&s_device);
s_connection = NULL;
}
gap_le_connection_deinit();
stub_new_timer_cleanup();
}
void test_gatt_service_changed_server__unsubscribe(void) {
prv_cccd_write(false /* is_subscribing */);
prv_expect_service_changed_indication_api_call_count(0);
}
void test_gatt_service_changed_server__subscribe_event_but_no_connection(void) {
gap_le_connection_remove(&s_device);
s_connection = NULL;
prv_cccd_write(true /* is_subscribing */);
prv_expect_service_changed_indication_api_call_count(0);
}
void test_gatt_service_changed_server__subscribe_fw_not_updated(void) {
prv_cccd_write(true /* is_subscribing */);
prv_expect_service_changed_indication_api_call_count(0);
}
void test_gatt_service_changed_server__resubscribe_indication_already_pending(void) {
gatt_service_changed_server_handle_fw_update();
prv_cccd_write(true /* is_subscribing */);
prv_cccd_write(true /* is_subscribing */);
prv_expect_service_changed_indication_api_call_count(1);
}
void test_gatt_service_changed_server__reconnect_resubscribe_stop_sending_after_n_times(void) {
gatt_service_changed_server_handle_fw_update();
gap_le_connection_remove(&s_device);
s_connection = NULL;
static const int max_times = 5;
for (int i = 0; i < max_times + 1; ++i) {
gap_le_connection_add(&s_device, NULL, false /* local_is_master */);
s_connection = gap_le_connection_by_device(&s_device);
cl_assert(s_connection);
s_connection->gatt_connection_id = s_connection_id;
prv_cccd_write(true /* is_subscribing */);
if (i < max_times) {
prv_expect_service_changed_indication_api_call_count(i + 1);
} else {
prv_expect_service_changed_indication_api_call_count(max_times);
}
gap_le_connection_remove(&s_device);
s_connection = NULL;
}
}
void test_gatt_service_changed_server__disconnect_during_delay(void) {
gatt_service_changed_server_handle_fw_update();
prv_cccd_write(true /* is_subscribing */);
// Grab the timer ID:
TimerID t = s_connection->gatt_service_changed_indication_timer;
s_connection->gatt_service_changed_indication_timer = TIMER_INVALID_ID;
// Simulate disconnection:
gap_le_connection_remove(&s_device);
s_connection = NULL;
// Timer fires:
stub_new_timer_fire(t);
prv_expect_service_changed_indication_api_call_count(0);
}
+297
View File
@@ -0,0 +1,297 @@
/*
* 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 "comm/ble/gap_le_advert.h"
#include "comm/ble/gap_le_task.h"
#include "comm/ble/gatt_client_subscriptions.h"
#include "comm/ble/kernel_le_client/kernel_le_client.h"
#include "comm/ble/kernel_le_client/test/test_definition.h"
#include "kernel/events.h"
#include "util/size.h"
// Stubs
////////////////////////////////////////////////////////////////////////////////////////////////////
#include "fake_system_task.h"
#include "stubs_logging.h"
#include "stubs_passert.h"
#include "stubs_pbl_malloc.h"
#include "stubs_rand_ptr.h"
#include "stubs_rtc.h"
void ams_create(void) {
}
void ams_destroy(void) {
}
void ancs_create(void) {
}
void ancs_destroy(void) {
}
void app_launch_handle_disconnection(void) {
}
BTBondingID bt_persistent_storage_get_ble_ancs_bonding(void) {
return 1;
}
bool bt_persistent_storage_is_ble_ancs_bonding(BTBondingID bonding) {
return true;
}
void gap_le_advert_unschedule_job_types(GAPLEAdvertisingJobTag *tag_types, size_t num_types) {
}
void gap_le_connect_cancel_all(GAPLEClient client) {
}
BTErrno gap_le_connect_cancel_by_bonding(BTBondingID bonding_id, GAPLEClient client) {
return BTErrnoOK;
}
BTErrno gap_le_connect_connect_by_bonding(BTBondingID bonding_id, bool auto_reconnect,
bool is_pairing_required, GAPLEClient client) {
return BTErrnoOK;
}
void gap_le_slave_reconnect_start(void) {
}
void gap_le_slave_reconnect_stop(void) {
}
BTErrno gatt_client_discovery_discover_all(const BTDeviceInternal *device) {
return BTErrnoOK;
}
uint16_t gatt_client_subscriptions_consume_notification(BLECharacteristic *characteristic_ref_out,
uint8_t *value_out,
uint16_t *value_length_in_out,
GAPLEClient client, bool *has_more_out) {
return 0;
}
bool gatt_client_subscriptions_get_notification_header(GAPLEClient client,
GATTBufferedNotificationHeader *header_out) {
return false;
}
void gatt_client_subscriptions_reschedule(GAPLEClient c) {
}
void launcher_task_add_callback(CallbackEventCallback callback, void *data) {
// Use fake_system_task as mock:
system_task_add_callback(callback, data);
}
void ppogatt_create(void) {
}
void ppogatt_destroy(void) {
}
void ppogatt_handle_buffer_empty(void) {
}
void bt_driver_reconnect_try_now(bool ignore_paused) {
}
void gatt_client_op_cleanup(GAPLEClient client) {
}
void ppogatt_reset_disconnect_counter(void) {
}
// Fakes & Helpers
////////////////////////////////////////////////////////////////////////////////////////////////////
static const BTDeviceInternal s_test_device = {
.address = (const BTDeviceAddress) {
.octets = {0x11, 0x22, 0x33, 0x44, 0x55, 0x66},
},
};
typedef enum {
TestServiceInstanceComplete = 1,
TestServiceInstanceIncomplete = 2,
TestServiceInstanceUnsupported = 3,
} TestServiceInstance;
static BLEService s_service_handles[] = {
TestServiceInstanceComplete,
TestServiceInstanceIncomplete,
TestServiceInstanceUnsupported,
};
typedef enum {
TestCharacteristicInstanceCompleteOne = 11,
TestCharacteristicInstanceCompleteTwo = 12,
TestCharacteristicInstanceIncompleteOne = 21,
TestCharacteristicInstanceUnsupported = 33,
} TestCharacteristicInstance;
Uuid gatt_client_service_get_uuid(BLEService service_ref) {
switch (service_ref) {
case TestServiceInstanceComplete:
case TestServiceInstanceIncomplete:
return s_test_service_uuid;
case TestServiceInstanceUnsupported:
default:
return UUID_INVALID;
}
}
uint8_t gatt_client_service_get_characteristics_matching_uuids(BLEService service_ref,
BLECharacteristic characteristics_out[],
const Uuid matching_characteristic_uuids[],
uint8_t num_characteristics) {
cl_assert_equal_i(num_characteristics, TestCharacteristicCount);
switch (service_ref) {
case TestServiceInstanceComplete:
characteristics_out[0] = TestCharacteristicInstanceCompleteOne;
characteristics_out[1] = TestCharacteristicInstanceCompleteTwo;
return 2;
case TestServiceInstanceIncomplete:
characteristics_out[0] = TestCharacteristicInstanceIncompleteOne;
return 1;
case TestCharacteristicInstanceUnsupported:
characteristics_out[0] = TestCharacteristicInstanceUnsupported;
return 1;
default:
return 0;
}
}
static int s_read_responses_consumed_count;
void gatt_client_consume_read_response(uintptr_t object_ref,
uint8_t value_out[],
uint16_t value_length,
GAPLEClient client) {
++s_read_responses_consumed_count;
}
static int s_services_discovered_count;
void test_client_handle_service_discovered(BLECharacteristic *characteristics) {
++s_services_discovered_count;
}
void test_client_invalidate_all_references(void) {
}
void test_client_handle_service_removed(BLECharacteristic *characteristics,
uint8_t num_characteristics) {
}
static bool s_can_handle_characteristic;
bool test_client_can_handle_characteristic(BLECharacteristic characteristic) {
return s_can_handle_characteristic;
}
void test_client_handle_write_response(BLECharacteristic characteristic, BLEGATTError error) {
}
void test_client_handle_subscribe(BLECharacteristic characteristic,
BLESubscription subscription_type, BLEGATTError error) {
}
void test_client_handle_read_or_notification(BLECharacteristic characteristic, const uint8_t *value,
size_t value_length, BLEGATTError error) {
}
// Tests
////////////////////////////////////////////////////////////////////////////////////////////////////
void test_kernel_le_client__initialize(void) {
s_services_discovered_count = 0;
s_read_responses_consumed_count = 0;
s_can_handle_characteristic = false;
kernel_le_client_init();
}
void test_kernel_le_client__cleanup(void) {
kernel_le_client_deinit();
fake_system_task_callbacks_cleanup();
}
void test_kernel_le_client__read_response_consumed_even_if_client_is_gone(void) {
// Simulate the client goes away:
s_can_handle_characteristic = false;
PebbleEvent e = (PebbleEvent) {
.type = PEBBLE_BLE_GATT_CLIENT_EVENT,
.bluetooth.le.gatt_client = {
.object_ref = TestCharacteristicInstanceCompleteOne,
.value_length = 1,
.gatt_error = BLEGATTErrorSuccess,
.subtype = PebbleBLEGATTClientEventTypeCharacteristicRead,
},
};
kernel_le_client_handle_event(&e);
cl_assert_equal_i(s_read_responses_consumed_count, 1);
// When value_length is zero, the read response should NOT be consumed:
e.bluetooth.le.gatt_client.value_length = 0;
s_read_responses_consumed_count = 0;
kernel_le_client_handle_event(&e);
cl_assert_equal_i(s_read_responses_consumed_count, 0);
}
void test_kernel_le_client__service_added(void) {
uint8_t num_services_added = ARRAY_LENGTH(s_service_handles);
PebbleBLEGATTClientServiceEventInfo *info =
kernel_malloc(sizeof(PebbleBLEGATTClientServiceEventInfo) +
(num_services_added * sizeof(BLEService)));
*info = (PebbleBLEGATTClientServiceEventInfo) {
.status = BTErrnoOK,
.type = PebbleServicesAdded,
.device = s_test_device,
};
info->services_added_data.num_services_added = num_services_added;
memcpy(info->services_added_data.services, s_service_handles, sizeof(s_service_handles));
PebbleEvent e = (PebbleEvent) {
.type = PEBBLE_BLE_GATT_CLIENT_EVENT,
.bluetooth.le.gatt_client_service = {
.info = info,
.subtype = PebbleBLEGATTClientEventTypeServiceChange,
},
};
kernel_le_client_handle_event(&e);
// Found one complete service instance:
cl_assert_equal_i(s_services_discovered_count, 1);
kernel_free(info);
}
// FIXME: PBL-27751: Improve test coverage of kernel_le_client.c
File diff suppressed because it is too large Load Diff
+182
View File
@@ -0,0 +1,182 @@
from waftools.pebble_test import clar
import waftools.ragel
def build(bld):
clar(bld,
sources_ant_glob = "src/fw/comm/ble/kernel_le_client/ams/ams_util.c "
"src/fw/comm/ble/kernel_le_client/ams/ams.c "
"src/fw/services/normal/music.c "
"tests/fakes/fake_events.c "
"tests/fakes/fake_gatt_client_operations.c "
"tests/fakes/fake_gatt_client_subscriptions.c "
"tests/fakes/fake_rtc.c",
test_sources_ant_glob = "test_ams.c")
clar(bld,
sources_ant_glob = "src/fw/comm/ble/kernel_le_client/ams/ams_util.c",
test_sources_ant_glob = "test_ams_util.c")
# TODO: the bt_driver calls here should really be a fake and not rely on
# the cc2564 module
clar(bld,
sources_ant_glob = "src/fw/comm/ble/gap_le_advert.c "
"src/fw/comm/ble/gap_le_connection.c "
"src/fw/services/common/regular_timer.c "
"src/fw/comm/internals/bt_conn_mgr.c "
"tests/fakes/fake_GAPAPI.c "
"tests/fakes/fake_rtc.c "
"tests/fakes/fake_HCIAPI.c "
"tests/fakes/fake_gap_le_connect_params.c ",
test_sources_ant_glob = "test_gap_le_advert.c",
override_includes=['dummy_board'])
clar(bld,
sources_ant_glob = "src/fw/comm/internals/bt_conn_mgr.c "
"src/fw/services/common/regular_timer.c "
"tests/fakes/fake_gap_le_connect_params.c "
"tests/fakes/fake_rtc.c ",
test_sources_ant_glob = "test_bt_conn_mgr.c")
clar(bld,
sources_ant_glob = "src/fw/util/rand/rand.c "
"src/fw/vendor/tinymt32/tinymt32.c "
"src/fw/comm/ble/gap_le_connection.c "
"src/fw/comm/ble/gatt_client_accessors.c "
"src/fw/comm/ble/gatt_client_discovery.c "
"src/fw/comm/ble/gatt_service_changed.c "
"src/fw/comm/internals/bt_conn_mgr.c "
"tests/fakes/fake_events.c "
"tests/fakes/fake_rtc.c "
"tests/fakes/fake_GATTAPI.c "
"tests/fakes/fake_GATTAPI_test_vectors.c "
"tests/fakes/fake_gap_le_connect_params.c ",
test_sources_ant_glob = "test_gatt_client_accessors.c")
clar(bld,
sources_ant_glob = "src/fw/util/rand/rand.c "
"src/fw/vendor/tinymt32/tinymt32.c "
"src/fw/comm/ble/gap_le_connection.c "
"src/fw/comm/ble/gatt_client_accessors.c "
"src/fw/comm/ble/gatt_client_discovery.c "
"src/fw/comm/ble/gatt_service_changed.c "
"src/fw/comm/internals/bt_conn_mgr.c "
"tests/fakes/fake_gap_le_connect_params.c "
"tests/fakes/fake_events.c "
"tests/fakes/fake_rtc.c "
"tests/fakes/fake_GATTAPI.c "
"tests/fakes/fake_GATTAPI_test_vectors.c ",
test_sources_ant_glob = "test_gatt_client_discovery.c")
clar(bld,
sources_ant_glob = "src/fw/util/rand/rand.c "
"src/fw/vendor/tinymt32/tinymt32.c "
"src/fw/comm/ble/gap_le_connection.c "
"src/fw/comm/ble/gap_le_task.c "
"src/fw/comm/ble/gatt_client_accessors.c "
"src/fw/comm/ble/gatt_client_discovery.c "
"src/fw/comm/ble/gatt_client_subscriptions.c "
"src/fw/comm/ble/gatt_service_changed.c "
"src/fw/comm/internals/bt_conn_mgr.c "
"tests/fakes/fake_gap_le_connect_params.c "
"tests/fakes/fake_events.c "
"tests/fakes/fake_GATTAPI.c "
"tests/fakes/fake_GATTAPI_test_vectors.c "
"tests/fakes/fake_queue.c "
"tests/fakes/fake_rtc.c ",
test_sources_ant_glob = "test_gatt_client_subscriptions.c")
clar(bld,
sources_ant_glob = "src/fw/util/rand/rand.c "
"src/fw/vendor/tinymt32/tinymt32.c "
"src/fw/comm/ble/gatt_client_discovery.c "
"src/fw/comm/ble/gatt_service_changed.c "
"src/fw/comm/internals/bt_conn_mgr.c "
"tests/fakes/fake_gap_le_connect_params.c "
"tests/fakes/fake_GATTAPI.c "
"tests/fakes/fake_GATTAPI_test_vectors.c "
"tests/fakes/fake_rtc.c",
test_sources_ant_glob = "test_gatt_service_changed_client.c")
clar(bld,
sources_ant_glob = "src/fw/util/rand/rand.c "
"src/fw/vendor/tinymt32/tinymt32.c "
"src/fw/comm/ble/gap_le_connection.c "
"src/fw/comm/ble/gatt_client_discovery.c "
"src/fw/comm/ble/gatt_service_changed.c "
"src/fw/comm/internals/bt_conn_mgr.c "
"tests/fakes/fake_gap_le_connect_params.c "
"tests/fakes/fake_GATTAPI.c "
"tests/fakes/fake_GATTAPI_test_vectors.c "
"tests/fakes/fake_rtc.c",
test_sources_ant_glob="test_gatt_service_changed_server.c")
clar(bld,
sources_ant_glob = "src/fw/comm/ble/gap_le_connect.c "
"src/fw/comm/ble/gap_le_connection.c "
"src/fw/comm/ble/gap_le_task.c "
"src/fw/comm/internals/bt_conn_mgr.c "
"tests/fakes/fake_events.c "
"tests/fakes/fake_GAPAPI.c "
"tests/fakes/fake_bluetooth_persistent_storage.c "
"tests/fakes/fake_gap_le_connect_params.c "
"tests/fakes/fake_HCIAPI.c "
"tests/fakes/fake_rtc.c ",
test_sources_ant_glob = "test_gap_le_connect.c",
override_includes=['dummy_board'])
clar(bld,
sources_ant_glob = "src/fw/util/time/mktime.c " \
"src/fw/util/buffer.c " \
"src/fw/util/time/time.c " \
"src/fw/comm/ble/kernel_le_client/ancs/ancs_util.c " \
"tests/fakes/fake_rtc.c",
test_sources_ant_glob = "test_ancs_util.c")
clar(bld,
sources_ant_glob = "src/fw/util/buffer.c " \
"src/fw/util/pstring.c " \
"src/fw/util/time/mktime.c " \
"src/fw/util/time/time.c "\
"src/fw/comm/ble/kernel_le_client/ancs/ancs_util.c " \
"src/fw/comm/ble/kernel_le_client/ancs/ancs.c " \
"src/fw/comm/ble/kernel_le_client/ancs/ancs_app_name_storage.c " \
"src/fw/services/common/regular_timer.c " \
"src/fw/util/rand/rand.c " \
"src/fw/vendor/tinymt32/tinymt32.c " \
"tests/fakes/fake_events.c " \
"tests/fakes/fake_kernel_services_notifications.c " \
"tests/fakes/fake_gatt_client_subscriptions.c " \
"tests/fakes/fake_rtc.c " \
"src/fw/services/normal/notifications/ancs/ancs_notifications.c " \
" src/fw/services/normal/notifications/ancs/ancs_notifications_util.c " \
"src/fw/services/normal/notifications/ancs/ancs_item.c " \
"src/fw/services/normal/notifications/ancs/ancs_filtering.c " \
"src/fw/services/normal/notifications/ancs/ancs_phone_call.c " \
"src/fw/services/normal/timeline/item.c " \
"src/fw/services/normal/timeline/attributes_actions.c " \
"src/fw/services/normal/timeline/attribute_group.c " \
"src/fw/services/normal/timeline/attribute.c",
test_sources_ant_glob = "test_ancs.c")
clar(bld,
sources_ant_glob="src/fw/comm/ble/kernel_le_client/kernel_le_client.c "
"src/fw/util/rand/rand.c "
"src/fw/vendor/tinymt32/tinymt32.c",
test_sources_ant_glob="test_kernel_le_client.c")
for ppogatt_version in [ 0, 1]:
clar(bld,
sources_ant_glob = "src/fw/system/hexdump.c " \
"src/fw/comm/ble/kernel_le_client/ppogatt/ppogatt.c " \
"src/fw/services/common/regular_timer.c " \
"src/fw/util/rand/rand.c "
"src/fw/vendor/tinymt32/tinymt32.c "
"tests/fakes/fake_gatt_client_operations.c " \
"tests/fakes/fake_gatt_client_subscriptions.c " \
"tests/fakes/fake_rtc.c " \
"tests/fakes/fake_session.c",
defines=["USE_PPOGATT_VERSION=%d" % ppogatt_version],
test_name='test_ppogatt_v%d' % ppogatt_version,
test_sources_ant_glob = "test_ppogatt.c")
# vim:filetype=python
+130
View File
@@ -0,0 +1,130 @@
/*
* 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 <stdbool.h>
#include <stdint.h>
#include <stddef.h>
#include <string.h>
#include "clar.h"
#include "console/cobs.h"
static CobsDecodeContext ctx;
static unsigned char out[1024];
static void decode_start(size_t length) {
cobs_streaming_decode_start(&ctx, out, length);
}
static void assert_decode_char_succeeds(char c) {
cl_assert(cobs_streaming_decode(&ctx, c));
}
static void assert_decode_succeeds(const char * restrict buf, size_t length) {
while (length--) {
assert_decode_char_succeeds(*buf++);
}
}
static void assert_decode_fails(const char * restrict buf, size_t length) {
bool result = true;
while (result && length--) {
result = result && cobs_streaming_decode(&ctx, *buf++);
}
cl_assert(result == false);
}
static void assert_buffer_not_touched(size_t index) {
cl_assert_equal_i(out[index], 0xcc);
}
static void assert_decode_completed(size_t expected_length) {
size_t decoded_length = cobs_streaming_decode_finish(&ctx);
cl_assert_equal_i(decoded_length, expected_length);
if (decoded_length != SIZE_MAX) {
assert_buffer_not_touched(decoded_length);
}
}
static void assert_output_equal(const void * restrict expected, size_t length) {
cl_assert(memcmp(out, expected, length) == 0);
}
void test_cobs_decode__initialize(void) {
memset(&ctx, 0xdd, sizeof(ctx));
memset(out, 0xcc, sizeof(out));
}
void test_cobs_decode__simple(void) {
const char vector[] = { 0x06, 'H', 'e', 'l', 'l', 'o' };
decode_start(5);
assert_decode_succeeds(vector, sizeof(vector));
assert_decode_completed(5);
assert_output_equal("Hello", 5);
}
void test_cobs_decode__zeroes(void) {
const char vector[] = { 0x01, 0x02, 'A', 0x01, 0x02, 'B', 0x01 };
decode_start(6);
assert_decode_succeeds(vector, sizeof(vector));
assert_decode_completed(6);
assert_output_equal("\0A\0\0B\0", 6);
}
void test_cobs_decode__max_length_block(void) {
decode_start(254);
assert_decode_char_succeeds(0xff);
for (int c = 0x01; c <= 0xfe; ++c) {
assert_decode_char_succeeds(c);
}
assert_decode_completed(254);
const unsigned char *iter = out;
for (int c = 0x01; c <= 0xfe; ++c) {
cl_assert_equal_i(*iter++, c);
}
}
void test_cobs_decode__empty_data(void) {
decode_start(100);
assert_decode_char_succeeds(0x01);
assert_decode_completed(0);
assert_buffer_not_touched(0);
}
void test_cobs_decode__output_too_small_1(void) {
const char vector[] = { 0x06, 'L', 'a', 'r', 'g', 'e', '.' };
decode_start(5);
assert_decode_fails(vector, sizeof(vector));
assert_decode_completed(SIZE_MAX);
assert_buffer_not_touched(6);
}
void test_cobs_decode__output_too_small_2(void) {
const char vector[] = { 0x05, 'a', 'b', 'c', 'd', 0x01, 0x01 };
decode_start(5);
assert_decode_fails(vector, sizeof(vector));
assert_decode_completed(SIZE_MAX);
assert_buffer_not_touched(6);
}
void test_cobs_decode__input_truncated(void) {
const char vector[] = { 0x05, 'a', 'b', 'c' };
decode_start(100);
assert_decode_succeeds(vector, sizeof(vector));
assert_decode_completed(SIZE_MAX);
}
+122
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 <stdbool.h>
#include <stdint.h>
#include <stddef.h>
#include <string.h>
#include "clar.h"
#include "console/cobs.h"
unsigned char out[1024];
static void assert_out_not_touched(size_t index) {
cl_assert_equal_i(out[index], 0xcc);
}
static void assert_out_equal(const void * restrict expected, size_t length) {
cl_assert(memcmp(out, expected, length) == 0);
}
static void assert_encode(const void * restrict src, size_t in_length,
size_t expected_length) {
size_t out_length = cobs_encode(out, src, in_length);
cl_assert_equal_i(out_length, expected_length);
if (expected_length != SIZE_MAX) {
assert_out_not_touched(expected_length);
}
}
void test_cobs_encode__initialize(void) {
memset(out, 0xcc, sizeof(out));
}
void test_cobs_encode__empty(void) {
assert_encode("", 0, 1);
assert_out_equal("\x01", 1);
}
void test_cobs_encode__zero(void) {
assert_encode("\0", 1, 2);
assert_out_equal("\x01\x01", 2);
}
void test_cobs_encode__simple(void) {
assert_encode("Hello", 5, 6);
assert_out_equal("\x06Hello", 6);
}
void test_cobs_encode__multiple_blocks(void) {
assert_encode("Hello\0w\0rld", 11, 12);
assert_out_equal("\x06Hello\x02w\x04rld", 12);
}
void test_cobs_encode__max_block_1(void) {
// vector[] = { 0x01, 0x02, ..., 0xfe };
uint8_t vector[254];
for (size_t i = 0; i < sizeof(vector); ++i) {
vector[i] = i + 1;
}
// expected[] = { 0xff, 0x01, 0x02, ..., 0xfe };
uint8_t expected[255];
expected[0] = 0xff;
for (size_t i = 1; i < sizeof(expected); ++i) {
expected[i] = i;
}
assert_encode(vector, sizeof(vector), sizeof(expected));
assert_out_equal(expected, sizeof(expected));
}
void test_cobs_encode__max_block_2(void) {
// vector[] = { 0x01, 0x02, ..., 0xfe, 0xff };
uint8_t vector[255];
for (size_t i = 0; i < sizeof(vector); ++i) {
vector[i] = i + 1;
}
// expected[] = { 0xff, 0x01, 0x02, ..., 0xfe, 0x02, 0xff };
uint8_t expected[257];
expected[0] = 0xff;
uint8_t c = 1;
for (int i = 1; i <= 255; ++i) {
expected[i] = c++;
}
expected[255] = 0x02;
expected[256] = 0xff;
assert_encode(vector, sizeof(vector), sizeof(expected));
assert_out_equal(expected, sizeof(expected));
}
void test_cobs_encode__max_block_3(void) {
// vector[] = { 0x00, 0x01, 0x02, 0x03, ..., 0xfe, 0xff };
uint8_t vector[256];
for (size_t i = 0; i < sizeof(vector); ++i) {
vector[i] = i;
}
// expected[] = { 0x01, 0xff, 0x01, 0x02, ..., 0xfe, 0x02, 0xff };
uint8_t expected[258];
expected[0] = 0x01;
expected[1] = 0xff;
uint8_t c = 1;
for (int i = 2; i <= 256; ++i) {
expected[i] = c++;
}
expected[256] = 0x02;
expected[257] = 0xff;
assert_encode(vector, sizeof(vector), sizeof(expected));
assert_out_equal(expected, sizeof(expected));
}
+13
View File
@@ -0,0 +1,13 @@
from waftools.pebble_test import clar
def build(ctx):
clar(ctx,
sources_ant_glob = "src/fw/console/cobs.c",
test_sources_ant_glob = "test_cobs_decode.c")
clar(ctx,
sources_ant_glob = "src/fw/console/cobs.c",
test_sources_ant_glob = "test_cobs_encode.c")
# vim:filetype=python
+99
View File
@@ -0,0 +1,99 @@
/*
* 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 "drivers/battery.h"
static uint32_t prv_convert_millivolts_to_12bit_reading(int millivolts) {
return 4095 * millivolts / 1800;
}
#define VREF_VOLTAGE 1200
void test_battery__reading_conversion_boring(void) {
ADCVoltageMonitorReading reading = {
.vref_total = prv_convert_millivolts_to_12bit_reading(VREF_VOLTAGE),
.vmon_total = prv_convert_millivolts_to_12bit_reading(1800)
};
uint32_t result = battery_convert_reading_to_millivolts(reading, 1, 1);
cl_assert_equal_i(result, 1800);
reading.vmon_total = prv_convert_millivolts_to_12bit_reading(1200);
result = battery_convert_reading_to_millivolts(reading, 1, 1);
cl_assert_equal_i(result, 1200);
reading.vmon_total = prv_convert_millivolts_to_12bit_reading(0);
result = battery_convert_reading_to_millivolts(reading, 1, 1);
cl_assert_equal_i(result, 0);
}
void test_battery__reading_conversion_40_samples(void) {
ADCVoltageMonitorReading reading = {
.vref_total = prv_convert_millivolts_to_12bit_reading(VREF_VOLTAGE) * 40,
.vmon_total = prv_convert_millivolts_to_12bit_reading(1800) * 40
};
uint32_t result = battery_convert_reading_to_millivolts(reading, 1, 1);
cl_assert_equal_i(result, 1800);
reading.vmon_total = prv_convert_millivolts_to_12bit_reading(1200) * 40;
result = battery_convert_reading_to_millivolts(reading, 1, 1);
cl_assert_equal_i(result, 1200);
reading.vmon_total = prv_convert_millivolts_to_12bit_reading(0);
result = battery_convert_reading_to_millivolts(reading, 1, 1);
cl_assert_equal_i(result, 0);
}
// Make sure our new method for calculating battery usage matches the old one for stm32f2
///////////////////////////////////////////////////////////
static int prv_legacy_f2_calculation_millivolts(ADCVoltageMonitorReading reading) {
return (int) ((reading.vmon_total * (2730) / reading.vref_total) * 295) / 256;
}
void test_battery__reading_conversion_f2(void) {
ADCVoltageMonitorReading reading = {
.vref_total = prv_convert_millivolts_to_12bit_reading(VREF_VOLTAGE),
.vmon_total = prv_convert_millivolts_to_12bit_reading(1800)
};
uint32_t result = battery_convert_reading_to_millivolts(reading, 3599, 1373);
cl_assert_equal_i(result, prv_legacy_f2_calculation_millivolts(reading));
}
// Make sure our new method for calculating battery usage matches the old one for stm32f4
///////////////////////////////////////////////////////////
static int prv_legacy_f4_calculation_millivolts(ADCVoltageMonitorReading reading) {
return (int) (((reading.vmon_total * (2730) / reading.vref_total) * 120) / 91);
}
void test_battery__reading_conversion_f4(void) {
ADCVoltageMonitorReading reading = {
.vref_total = prv_convert_millivolts_to_12bit_reading(VREF_VOLTAGE),
.vmon_total = prv_convert_millivolts_to_12bit_reading(1800)
};
uint32_t result = battery_convert_reading_to_millivolts(reading, 3, 1);
cl_assert_equal_i(result, prv_legacy_f4_calculation_millivolts(reading));
}
+245
View File
@@ -0,0 +1,245 @@
/*
* 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 "fake_new_timer.h"
#include "stubs_analytics.h"
#include "stubs_freertos.h"
#include "stubs_logging.h"
#include "stubs_mutex.h"
#include "stubs_passert.h"
#include "stubs_pebble_tasks.h"
#include "stubs_prompt.h"
#include "stubs_queue.h"
#include "stubs_sleep.h"
#include "stubs_stop.h"
#include "stubs_task_watchdog.h"
#include "stubs_worker_manager.h"
#include "drivers/flash.h"
#include "drivers/flash/flash_impl.h"
void flash_api_reset_for_test(void);
TimerID flash_api_get_erase_poll_timer_for_test(void);
status_t return_success(void) {
return S_SUCCESS;
}
status_t return_error(void) {
return E_ERROR;
}
status_t flash_impl_init(bool coredump_mode) {
return S_SUCCESS;
}
void flash_impl_use(void) {}
void flash_impl_release_many(uint32_t num_locks) {}
int get_subsector_base_calls = 0;
FlashAddress flash_impl_get_subsector_base_address(FlashAddress addr) {
get_subsector_base_calls++;
return addr & 0xffffff00;
}
int get_sector_base_calls = 0;
FlashAddress flash_impl_get_sector_base_address(FlashAddress addr) {
get_sector_base_calls++;
return addr & 0xfffff000;
}
int erase_subsector_begin_calls = 0;
status_t erase_subsector_begin_return = S_SUCCESS;
status_t flash_impl_erase_subsector_begin(FlashAddress addr) {
erase_subsector_begin_calls++;
return erase_subsector_begin_return;
}
int erase_sector_begin_calls = 0;
status_t erase_sector_begin_return = S_SUCCESS;
status_t flash_impl_erase_sector_begin(FlashAddress addr) {
erase_sector_begin_calls++;
return erase_sector_begin_return;
}
int get_erase_status_calls = 0;
status_t (*get_erase_status_fn)(void) = return_success;
status_t flash_impl_get_erase_status(void) {
get_erase_status_calls++;
return get_erase_status_fn();
}
int blank_check_subsector_calls = 0;
status_t blank_check_subsector_return = S_FALSE;
status_t flash_impl_blank_check_subsector(FlashAddress addr) {
blank_check_subsector_calls++;
return blank_check_subsector_return;
}
int blank_check_sector_calls = 0;
status_t blank_check_sector_return = S_FALSE;
status_t flash_impl_blank_check_sector(FlashAddress addr) {
blank_check_sector_calls++;
return blank_check_sector_return;
}
status_t flash_impl_enter_low_power_mode(void) {
return S_SUCCESS;
}
status_t flash_impl_exit_low_power_mode(void) {
return S_SUCCESS;
}
status_t flash_impl_erase_suspend(FlashAddress addr) {
return S_SUCCESS;
}
status_t flash_impl_erase_resume(FlashAddress addr) {
return S_SUCCESS;
}
uint32_t flash_impl_get_typical_subsector_erase_duration_ms(void) {
return 100;
}
uint32_t flash_impl_get_typical_sector_erase_duration_ms(void) {
return 100;
}
status_t flash_impl_get_write_status(void) {
return E_UNKNOWN;
}
status_t flash_impl_read_sync(void *buffer, FlashAddress addr, size_t len) {
return E_UNKNOWN;
}
status_t flash_impl_set_burst_mode(bool enable) {
return E_UNKNOWN;
}
status_t flash_impl_unprotect(void) {
return S_SUCCESS;
}
int flash_impl_write_page_begin(const void *buffer, FlashAddress addr,
size_t len) {
return E_UNKNOWN;
}
void flash_impl_enable_write_protection(void) {
}
status_t flash_impl_write_protect(FlashAddress start_sector,
FlashAddress end_sector) {
return E_UNKNOWN;
}
status_t flash_impl_set_nvram_erase_status(bool is_subsector,
FlashAddress addr) {
return S_SUCCESS;
}
status_t flash_impl_clear_nvram_erase_status(void) {
return S_SUCCESS;
}
status_t flash_impl_get_nvram_erase_status(bool *is_subsector,
FlashAddress *addr) {
return S_FALSE;
}
void flash_erase_init(void) {
}
void *callback_context = NULL;
status_t callback_status = -12345;
static void callback(void *context, status_t status) {
callback_context = context;
callback_status = status;
}
void test_flash_api__initialize(void) {
callback_context = NULL;
callback_status = -12345;
get_sector_base_calls = 0;
get_subsector_base_calls = 0;
erase_subsector_begin_calls = 0;
erase_sector_begin_calls = 0;
get_erase_status_calls = 0;
get_erase_status_fn = return_success;
blank_check_subsector_calls = 0;
blank_check_sector_calls = 0;
flash_api_reset_for_test();
flash_init();
}
void test_flash_api__cleanup(void) {
stub_new_timer_cleanup();
}
void test_flash_api__erase_subsector_calls_right_impl_func(void) {
flash_erase_subsector(0, callback, NULL);
cl_assert_equal_i(erase_subsector_begin_calls, 1);
cl_assert_equal_i(erase_sector_begin_calls, 0);
}
void test_flash_api__erase_sector_calls_right_impl_func(void) {
flash_erase_sector(0, callback, NULL);
cl_assert_equal_i(erase_sector_begin_calls, 1);
cl_assert_equal_i(erase_subsector_begin_calls, 0);
}
///////////////////////////////////////////////////////////////////////
status_t erase_status_return_error_once(void) {
get_erase_status_fn = return_success;
return E_ERROR;
}
void test_flash_api__retry_erase_on_first_error(void) {
get_erase_status_fn = erase_status_return_error_once;
flash_erase_sector_blocking(0);
cl_assert_equal_i(erase_sector_begin_calls, 2);
}
///////////////////////////////////////////////////////////////////////
bool uncorrectable_erase_error_cb_called = false;
void uncorrectable_erase_error_cb(void *context, status_t result) {
cl_assert_equal_i(E_ERROR, result);
uncorrectable_erase_error_cb_called = true;
}
void test_flash_api__handle_uncorrectable_erase_error(void) {
get_erase_status_fn = return_error;
TimerID erase_timer = flash_api_get_erase_poll_timer_for_test();
flash_erase_sector(0, uncorrectable_erase_error_cb, NULL);
int i;
for (i = 0; i < 20 && !uncorrectable_erase_error_cb_called; ++i) {
cl_assert(stub_new_timer_is_scheduled(erase_timer));
stub_new_timer_fire(erase_timer);
}
cl_assert(i > 1 && i < 20);
cl_assert_equal_i(uncorrectable_erase_error_cb_called, true);
}
+320
View File
@@ -0,0 +1,320 @@
/*
* 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 "drivers/flash.h"
#include "services/common/new_timer/new_timer.h"
#include "clar.h"
#include <stdbool.h>
// Stubs
///////////////////////////////////////////////////////////
#include "stubs_logging.h"
#include "stubs_passert.h"
// Fakes
///////////////////////////////////////////////////////////
typedef enum EraseCommandType {
SectorEraseCommand = 1,
SubsectorEraseCommand
} EraseCommandType;
typedef struct EraseCommand {
uint32_t addr;
EraseCommandType type;
} EraseCommand;
static EraseCommand s_command_list[32];
static int s_command_list_index = 0;
static int s_callback_called_count = 0;
static status_t s_callback_status;
static int s_simulate_flash_driver_error_countdown;
static int s_simulate_work_queue_full_countdown;
static bool s_erase_mutex_locked;
void prv_init_erase_mutex(void) {
}
void prv_lock_erase_mutex(void) {
cl_assert_equal_i(s_erase_mutex_locked, false);
s_erase_mutex_locked = true;
}
void prv_unlock_erase_mutex(void) {
s_erase_mutex_locked = false;
}
void flash_erase_subsector_blocking(uint32_t subsector_addr) {
s_command_list[s_command_list_index++] = (EraseCommand) {
.addr = subsector_addr,
.type = SubsectorEraseCommand
};
}
void flash_erase_sector_blocking(uint32_t sector_addr) {
s_command_list[s_command_list_index++] = (EraseCommand) {
.addr = sector_addr,
.type = SectorEraseCommand
};
}
void flash_erase_subsector(uint32_t subsector_addr, FlashOperationCompleteCb cb,
void *context) {
s_command_list[s_command_list_index++] = (EraseCommand) {
.addr = subsector_addr,
.type = SubsectorEraseCommand
};
cl_assert_equal_i(s_erase_mutex_locked, true);
cb(context, (--s_simulate_flash_driver_error_countdown? S_SUCCESS : E_BUSY));
}
void flash_erase_sector(uint32_t sector_addr, FlashOperationCompleteCb cb,
void *context) {
s_command_list[s_command_list_index++] = (EraseCommand) {
.addr = sector_addr,
.type = SectorEraseCommand
};
cl_assert_equal_i(s_erase_mutex_locked, true);
cb(context, (--s_simulate_flash_driver_error_countdown? S_SUCCESS : E_BUSY));
}
bool new_timer_add_work_callback(NewTimerWorkCallback cb, void *data) {
if (--s_simulate_work_queue_full_countdown == 0) {
return false;
}
cb(data);
return true;
}
static void prv_assert_erase_commands(EraseCommand *commands) {
int expected_command_count = 0;
EraseCommand *cursor = commands;
while ((cursor++)->type != 0) {
++expected_command_count;
}
cl_assert_equal_i(s_command_list_index, expected_command_count);
for (int i = 0; i < s_command_list_index; ++i) {
cl_assert_equal_i(s_command_list[i].addr, commands[i].addr);
cl_assert_equal_i(s_command_list[i].type, commands[i].type);
}
}
static int s_dummy_value = 42;
static int *s_callback_ctx = &s_dummy_value;
static void prv_callback(void *context, status_t status) {
cl_assert_equal_p(context, s_callback_ctx);
cl_assert_equal_i(s_erase_mutex_locked, false);
s_callback_status = status;
s_callback_called_count++;
}
static void prv_assert_callback_called(status_t expected_status) {
cl_assert_equal_i(s_callback_called_count, 1);
cl_assert_equal_i(s_callback_status, expected_status);
}
static void prv_test_erase_optimal_range(
uint32_t min_start, uint32_t max_start, uint32_t min_end, uint32_t max_end,
EraseCommand *expected_commands) {
flash_erase_optimal_range(min_start, max_start, min_end, max_end,
prv_callback, s_callback_ctx);
prv_assert_erase_commands(expected_commands);
prv_assert_callback_called(
s_command_list_index? S_SUCCESS : S_NO_ACTION_REQUIRED);
}
// Tests
///////////////////////////////////////////////////////////
void test_flash_erase__initialize(void) {
s_command_list_index = 0;
s_callback_called_count = 0;
s_callback_status = 42;
s_simulate_work_queue_full_countdown = -1;
s_simulate_flash_driver_error_countdown = -1;
s_erase_mutex_locked = false;
}
void test_flash_erase__cleanup(void) {
}
void test_flash_erase__empty(void) {
prv_test_erase_optimal_range(0, 0, 0, 0, (EraseCommand[]){ { } });
}
void test_flash_erase__sectors_simple_1(void) {
// Erase one sector 0x10000 - 0x20000
prv_test_erase_optimal_range(
64 * 1024, 64 * 1024, 2 * 64 * 1024, 2 * 64 * 1024,
(EraseCommand[]) {
{ 64 * 1024, SectorEraseCommand },
{ },
});
}
void test_flash_erase__sectors_simple_2(void) {
// Erase one sectors 0x10000 - 0x20000 but allow us to erase more
prv_test_erase_optimal_range(
0, 64 * 1024, 2 * 64 * 1024, 3 * 64 * 1024,
(EraseCommand[]) {
{ 64 * 1024, SectorEraseCommand },
{ },
});
}
void test_flash_erase__two_sectors(void) {
// Erase two sectors 0x10000 - 0x30000 but allow us to erase more
prv_test_erase_optimal_range(
0, 64 * 1024, 3 * 64 * 1024, 4 * 64 * 1024,
(EraseCommand[]) {
{ 64 * 1024, SectorEraseCommand },
{ 2 * 64 * 1024, SectorEraseCommand },
{ },
});
}
void test_flash_erase__subsectors_1(void) {
// Offer a less than full sector range but erase the full range
prv_test_erase_optimal_range(
0, 4 * 1024, 64 * 1024, 64 * 1024,
(EraseCommand[]) {
{ 0, SectorEraseCommand },
{ },
});
}
void test_flash_erase__sector_and_subsector(void) {
// Offer a more than a full sector range, needs a sector and a subsector
prv_test_erase_optimal_range(
60 * 1024, 60 * 1024, 2 * 64 * 1024, 2 * 64 * 1024,
(EraseCommand[]) {
{ 60 * 1024, SubsectorEraseCommand },
{ 64 * 1024, SectorEraseCommand },
{ },
});
}
void test_flash_erase__subsectors_on_both_sides(void) {
// Offer a more than a full sector range, needs subsectors on both sides
prv_test_erase_optimal_range(
60 * 1024, 60 * 1024, ((2 * 64) + 4) * 1024, ((2 * 64) + 8) * 1024,
(EraseCommand[]) {
{ 60 * 1024, SubsectorEraseCommand },
{ 64 * 1024, SectorEraseCommand },
{ 2 * 64 * 1024, SubsectorEraseCommand },
{ },
});
}
// Various tests that look like erasing our 96k app resource banks
void test_flash_erase__96k_app_banks_1(void) {
// App that's in an aligned bank but smaller than 64k
prv_test_erase_optimal_range(
0, 0, 32 * 1024, 96 * 1024,
(EraseCommand[]) {
{ 0, SectorEraseCommand },
{ },
});
}
void test_flash_erase__96k_app_banks_2(void) {
// App that's in an aligned bank but larger than than 64k
prv_test_erase_optimal_range(
0, 0, 69 * 1024, 96 * 1024,
(EraseCommand[]) {
{ 0, SectorEraseCommand },
{ 64 * 1024, SubsectorEraseCommand },
{ 68 * 1024, SubsectorEraseCommand },
{ },
});
}
void test_flash_erase__96k_app_banks_3(void) {
// App that's in an unaligned bank but smaller than 64k
prv_test_erase_optimal_range(
32 * 1024, 32 * 1024, (32 + 18) * 1024, (32 + 96) * 1024,
(EraseCommand[]) {
{ 32 * 1024, SubsectorEraseCommand },
{ 36 * 1024, SubsectorEraseCommand },
{ 40 * 1024, SubsectorEraseCommand },
{ 44 * 1024, SubsectorEraseCommand },
{ 48 * 1024, SubsectorEraseCommand },
{ },
});
}
void test_flash_erase__96k_app_banks_4(void) {
// App that's in an unaligned bank but larger than than 64k
prv_test_erase_optimal_range(
32 * 1024, 32 * 1024, (32 + 71) * 1024, (32 + 96) * 1024,
(EraseCommand[]) {
{ 32 * 1024, SubsectorEraseCommand },
{ 36 * 1024, SubsectorEraseCommand },
{ 40 * 1024, SubsectorEraseCommand },
{ 44 * 1024, SubsectorEraseCommand },
{ 48 * 1024, SubsectorEraseCommand },
{ 52 * 1024, SubsectorEraseCommand },
{ 56 * 1024, SubsectorEraseCommand },
{ 60 * 1024, SubsectorEraseCommand },
{ 64 * 1024, SectorEraseCommand },
{ },
});
}
void test_flash_erase__watch_and_learn(void) {
// Test cases stolen from Alvin's watch and learn app that originally hit this bug
prv_test_erase_optimal_range(
0x320000, 0x320000, 0x33177c, 0x338000,
(EraseCommand[]) {
{ 0x320000, SectorEraseCommand },
{ 0x330000, SubsectorEraseCommand },
{ 0x331000, SubsectorEraseCommand },
{ },
});
}
void test_flash_erase__handle_work_queue_full(void) {
s_simulate_work_queue_full_countdown = 3;
flash_erase_optimal_range(
32 * 1024, 32 * 1024, (32 + 71) * 1024, (32 + 96) * 1024,
prv_callback, s_callback_ctx);
prv_assert_callback_called(E_INTERNAL);
prv_assert_erase_commands((EraseCommand[]) {
{ 32 * 1024, SubsectorEraseCommand },
{ 36 * 1024, SubsectorEraseCommand },
{ 40 * 1024, SubsectorEraseCommand },
{ },
});
}
void test_flash_erase__handle_flash_driver_error(void) {
s_simulate_flash_driver_error_countdown = 3;
flash_erase_optimal_range(
32 * 1024, 32 * 1024, (32 + 71) * 1024, (32 + 96) * 1024,
prv_callback, s_callback_ctx);
prv_assert_callback_called(E_BUSY);
prv_assert_erase_commands((EraseCommand[]) {
{ 32 * 1024, SubsectorEraseCommand },
{ 36 * 1024, SubsectorEraseCommand },
{ 40 * 1024, SubsectorEraseCommand },
{ },
});
}
+83
View File
@@ -0,0 +1,83 @@
/*
* 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 "board/board.h"
#include "drivers/fpc_pinstrap.h"
static bool s_pin_pull_up_enabled[2] = { false, false };
void gpio_input_init_pull_up_down(const InputConfig *input_cfg, GPIOPuPd_TypeDef pupd) {
s_pin_pull_up_enabled[input_cfg->gpio_pin] = (pupd == GPIO_PuPd_UP);
}
void gpio_analog_init(const InputConfig *input_cfg) {
}
static enum {
PinstrapResult_GND,
PinstrapResult_Vplus,
PinstrapResult_Float
} s_pinstrap_results[2];
bool gpio_input_read(const InputConfig *input_cfg) {
switch (s_pinstrap_results[input_cfg->gpio_pin]) {
case PinstrapResult_GND:
return false;
case PinstrapResult_Vplus:
return true;
case PinstrapResult_Float:
return s_pin_pull_up_enabled[input_cfg->gpio_pin];
}
}
void test_fpc_pinstrap__simple(void) {
s_pinstrap_results[0] = PinstrapResult_GND;
s_pinstrap_results[1] = PinstrapResult_GND;
cl_assert(fpc_pinstrap_get_value() == 0x0);
s_pinstrap_results[1] = PinstrapResult_Vplus;
cl_assert(fpc_pinstrap_get_value() == 0x1);
s_pinstrap_results[1] = PinstrapResult_Float;
cl_assert(fpc_pinstrap_get_value() == 0x2);
s_pinstrap_results[0] = PinstrapResult_Vplus;
s_pinstrap_results[1] = PinstrapResult_GND;
cl_assert(fpc_pinstrap_get_value() == 0x3);
s_pinstrap_results[1] = PinstrapResult_Vplus;
cl_assert(fpc_pinstrap_get_value() == 0x4);
s_pinstrap_results[1] = PinstrapResult_Float;
cl_assert(fpc_pinstrap_get_value() == 0x5);
s_pinstrap_results[0] = PinstrapResult_Float;
s_pinstrap_results[1] = PinstrapResult_GND;
cl_assert(fpc_pinstrap_get_value() == 0x6);
s_pinstrap_results[1] = PinstrapResult_Vplus;
cl_assert(fpc_pinstrap_get_value() == 0x7);
s_pinstrap_results[1] = PinstrapResult_Float;
cl_assert(fpc_pinstrap_get_value() == 0x8);
}
+130
View File
@@ -0,0 +1,130 @@
/*
* 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 "drivers/stm32f7/i2c_timingr.h"
#define MHZ_TO_HZ(val) ((val) * 1000000)
#define KHZ_TO_HZ(val) ((val) * 1000)
static void prv_check_result(uint32_t timingr, uint32_t expected_prescaler,
uint32_t expected_scl_low, uint32_t expected_scl_high,
uint32_t expected_scl_delay) {
cl_assert_(timingr != I2C_TIMINGR_INVALID_VALUE, "timingr == I2C_TIMINGR_INVALID_VALUE");
cl_assert_equal_i((timingr >> 28) + 1, expected_prescaler);
cl_assert_equal_i((timingr & 0xFF) + 1, expected_scl_low);
cl_assert_equal_i(((timingr >> 8) & 0xFF) + 1, expected_scl_high);
cl_assert_equal_i(((timingr >> 20) & 0xF) + 1, expected_scl_delay);
}
void test_i2c_timingr__valid_no_prescaler_no_rise_fall_time(void) {
// We'll use a base clock speed of 36Mhz and try to get to 400kHz I2C. We should be able to do
// this with a prescaler of 1.
//
// 36MHz / 400kHz = 90 cycles => 90 - 6 sync cycles = 84 cycles to play with
// minimum low = ceil(1300ns / (1 / 36MHz)) = 47 cycles
// minimum high = ceil(600ns / (1 / 36MHz)) = 22 cycles
// extra cycles = 15 => (7 low, 8 high) => SCLL of 54 and SCLH of 30
//
// SCLDEL = ceil((t_r 0 + t_SU 100ns) / (1 / 36MHz)) = 4 cycles
prv_check_result(i2c_timingr_calculate(MHZ_TO_HZ(36), I2CBusMode_FastMode,
KHZ_TO_HZ(400), 0, 0),
1, 54, 30, 4);
}
void test_i2c_timingr__valid_prescaler_no_rise_fall_time(void) {
// We'll use a base clock speed of 360Mhz and try to get to 100kHz I2C. This requires a prescaler
// of 8 which gets us down to a base clock speed of 45MHz.
//
// 45MHz / 100kHz = 450 cycles => 450 - ceil(6 / 8) sync cycles = 449 cycles to play with
// minimum low = ceil(4700ns / (1 / 45MHz)) = 212 cycles
// minimum high = ceil(4000ns / (1 / 45MHz)) = 181 cycles
// extra cycles = 56 => (28 low, 28 high) => SCLL of 240 and SCLH of 209
//
// SCLDEL = ceil((t_r 0 + t_SU 250ns) / (1 / 45MHz)) = 12
prv_check_result(i2c_timingr_calculate(MHZ_TO_HZ(360), I2CBusMode_Standard,
KHZ_TO_HZ(100), 0, 0),
8, 240, 209, 12);
}
void test_i2c_timingr__valid_no_prescaler_rise_fall_time(void) {
// We'll use a base clock speed of 20MHz and try to get to 100kHz I2C with fall and rise times of
// 500ns each.
//
// 20MHz / 100kHz = 200 cycles => 200 - 6 sync cycles - (2 * 500ns / (1 / 20MHz)) = 174 cycles to
// play with
// minimum low = ceil(4700ns / (1 / 20MHz)) = 94 cycles
// minimum high = ceil(4000ns / (1 / 20MHz)) = 80 cycles
// extra cycles = 0 => (0 low, 0 high) => SCLL of 94 and SCLH of 80
//
// SCLDEL = ceil((t_r 500ns + t_SU 250ns) / (1 / 20MHz)) = 15
prv_check_result(i2c_timingr_calculate(MHZ_TO_HZ(20), I2CBusMode_Standard,
KHZ_TO_HZ(100), 500, 500),
1, 94, 80, 15);
}
void test_i2c_timingr__data_delay_requires_prescaler(void) {
// We'll increase the rise time enough that the required SCLDEL will exceed the max value with
// no prescaler, forcing the use of the prescaler even though the SCLL and SCLH values wouldn't
// otherwise require it.
//
// With prescaler 1: SCLDEL = ceil((800ns + 250ns) / (1 / 20MHz)) = 21 > 16
// With prescaler 2: base clock is 10MHz.
// 10MHz / 100kHz = 100 cycles => 100 - 6 sync cycles - ((800ns + 200ns) / (1 / 10 MHz)) = 84
// cycles to play with
// minimum low = ceil(4700ns / (1 / 10MHz)) = 47 cycles
// minimum high = ceil(4000ns / (1 / 10MHz)) = 40 cycles
// extra cycles = 0 => (0 low, 0 high) => SCLL of 47 and SCLH of 80
//
// SCLDEL = ceil((t_r 800ns + t_SU 250ns) / (1 / 10MHz)) = 11
prv_check_result(i2c_timingr_calculate(MHZ_TO_HZ(20), I2CBusMode_Standard,
KHZ_TO_HZ(100), 800, 200),
2, 47, 40, 11);
}
void test_i2c_timingr__invalid_speed_too_high(void) {
// We'll use a base clock speed of 1Mhz and try to get to 400KHz I2C, which won't be possible
// because the sync cycles alone will make us way slower than 400kHz.
cl_assert_equal_i(i2c_timingr_calculate(MHZ_TO_HZ(1), I2CBusMode_FastMode,
KHZ_TO_HZ(400), 0, 0),
I2C_TIMINGR_INVALID_VALUE);
}
void test_i2c_timingr__invalid_speed_too_low(void) {
// We'll use a base clock speed of 1600Mhz and try to get to 100KHz I2C, which won't be possible
// because the max prescaler is 16, which still leaves us with 1000 clock periods which is too
// many to fit.
cl_assert_equal_i(i2c_timingr_calculate(MHZ_TO_HZ(1600), I2CBusMode_Standard,
KHZ_TO_HZ(100), 0, 0),
I2C_TIMINGR_INVALID_VALUE);
}
void test_i2c_timingr__invalid_speed_too_high_for_mode(void) {
// Try calculating timing for 400kHz in Standard mode, which is out of spec for that mode.
cl_assert_equal_i(i2c_timingr_calculate(MHZ_TO_HZ(36), I2CBusMode_Standard,
KHZ_TO_HZ(400), 0, 0),
I2C_TIMINGR_INVALID_VALUE);
}
void test_i2c_timingr__invalid_long_rise_fall(void) {
// We'll use a base clock speed of 100Mhz and try to get to 100KHz I2C with very long (out of
// spec) 5us rise and fall times which prevent us from hitting the target frequency.
cl_assert_equal_i(i2c_timingr_calculate(MHZ_TO_HZ(100), I2CBusMode_Standard,
KHZ_TO_HZ(100), 5000, 5000),
I2C_TIMINGR_INVALID_VALUE);
}
+160
View File
@@ -0,0 +1,160 @@
/*
* 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 <stdarg.h>
#include <stdint.h>
#include <string.h>
#include <time.h>
#include "drivers/qemu/qemu_serial.h"
#include "drivers/qemu/qemu_serial_private.h"
#include "util/net.h"
#include "clar.h"
extern bool qemu_test_add_byte_from_isr(QemuSerialGlobals *state, uint8_t byte);
// Stubs
////////////////////////////////////
#include "stubs_passert.h"
#include "stubs_logging.h"
#include "stubs_pbl_malloc.h"
#include "stubs_mutex.h"
// Globals
QemuSerialGlobals s_state;
// Setup
////////////////////////////////////
void test_qemu_serial__initialize(void) {
qemu_serial_private_init_state(&s_state);
}
void test_qemu_serial__cleanup(void) {
}
// ------------------------------------------------------------------------------------
static void prv_send_bytes(void *p, uint32_t size) {
uint8_t *src = (uint8_t *)p;
for (uint32_t i=0; i<size; i++) {
qemu_test_add_byte_from_isr(&s_state, *src++);
}
}
// ------------------------------------------------------------------------------------
static void prv_send_hdr(uint16_t protocol, uint16_t data_len) {
QemuCommChannelHdr hdr = (QemuCommChannelHdr) {
.signature = htons(QEMU_HEADER_SIGNATURE),
.protocol = htons(protocol),
.len = htons(data_len)
};
prv_send_bytes(&hdr, sizeof(hdr));
}
static void prv_send_footer(void) {
QemuCommChannelFooter footer = (QemuCommChannelFooter) {
.signature = htons(QEMU_FOOTER_SIGNATURE)
};
prv_send_bytes(&footer, sizeof(footer));
}
// ------------------------------------------------------------------------------------
// Tests
void test_qemu_serial__foo(void) {
uint8_t *rcv_msg;
uint32_t rcv_bytes;
uint16_t rcv_protocol;
// Our test message
uint8_t msg_data[] = {0x11, 0x22, 0x33};
// -----------------------------------------------------------------------------
// Send message all at once before checking
prv_send_hdr(QemuProtocol_SPP, sizeof(msg_data));
prv_send_bytes(msg_data, sizeof(msg_data));
prv_send_footer();
rcv_msg = qemu_serial_private_assemble_message(&s_state, &rcv_bytes, &rcv_protocol);
cl_assert(rcv_msg);
cl_assert_equal_i(rcv_protocol, QemuProtocol_SPP);
cl_assert_equal_i(rcv_bytes, sizeof(msg_data));
cl_assert_equal_m(msg_data, rcv_msg, sizeof(msg_data));
// -----------------------------------------------------------------------------
// Send 2 messages before checking
for (int i=0; i<2; i++) {
prv_send_hdr(QemuProtocol_SPP, sizeof(msg_data));
prv_send_bytes(msg_data, sizeof(msg_data));
prv_send_footer();
}
for (int i=0; i<2; i++) {
rcv_msg = qemu_serial_private_assemble_message(&s_state, &rcv_bytes, &rcv_protocol);
cl_assert(rcv_msg);
cl_assert_equal_i(rcv_protocol, QemuProtocol_SPP);
cl_assert_equal_i(rcv_bytes, sizeof(msg_data));
cl_assert_equal_m(msg_data, rcv_msg, sizeof(msg_data));
}
// -----------------------------------------------------------------------------
// Check after each part
prv_send_hdr(QemuProtocol_SPP, sizeof(msg_data));
cl_assert(!qemu_serial_private_assemble_message(&s_state, &rcv_bytes, &rcv_protocol));
prv_send_bytes(msg_data, sizeof(msg_data));
// Message is available now
cl_assert(qemu_serial_private_assemble_message(&s_state, &rcv_bytes, &rcv_protocol));
prv_send_footer();
cl_assert(!qemu_serial_private_assemble_message(&s_state, &rcv_bytes, &rcv_protocol));
// -----------------------------------------------------------------------------
// Send garbage before a good packet
prv_send_bytes(msg_data, sizeof(msg_data));
cl_assert(!qemu_serial_private_assemble_message(&s_state, &rcv_bytes, &rcv_protocol));
prv_send_hdr(QemuProtocol_SPP, sizeof(msg_data));
cl_assert(!qemu_serial_private_assemble_message(&s_state, &rcv_bytes, &rcv_protocol));
prv_send_bytes(msg_data, sizeof(msg_data));
cl_assert(qemu_serial_private_assemble_message(&s_state, &rcv_bytes, &rcv_protocol));
prv_send_footer();
cl_assert(!qemu_serial_private_assemble_message(&s_state, &rcv_bytes, &rcv_protocol));
// -----------------------------------------------------------------------------
// Check after just part of the data
prv_send_hdr(QemuProtocol_SPP, 2*sizeof(msg_data));
cl_assert(!qemu_serial_private_assemble_message(&s_state, &rcv_bytes, &rcv_protocol));
prv_send_bytes(msg_data, sizeof(msg_data));
cl_assert(!qemu_serial_private_assemble_message(&s_state, &rcv_bytes, &rcv_protocol));
prv_send_bytes(msg_data, sizeof(msg_data));
rcv_msg = qemu_serial_private_assemble_message(&s_state, &rcv_bytes, &rcv_protocol);
cl_assert(rcv_msg);
cl_assert_equal_i(rcv_bytes, 2*sizeof(msg_data));
cl_assert_equal_m(msg_data, rcv_msg, sizeof(msg_data));
cl_assert_equal_m(msg_data, rcv_msg+sizeof(msg_data), sizeof(msg_data));
prv_send_footer();
cl_assert(!qemu_serial_private_assemble_message(&s_state, &rcv_bytes, &rcv_protocol));
}
+92
View File
@@ -0,0 +1,92 @@
/*
* 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 "drivers/stm32f2/rtc_calibration.h"
#include "clar.h"
// Stubs and stuff
#include "stubs_logging.h"
#include "mcu.h"
#define TARGET_FREQUENCY_mHZ (32768 * 1000)
#define ALTERNATE_FREQUENCY_mHZ (1000000 * 1000)
// Tests
void test_rtc_calibration__no_calibration_required(void) {
RTCCalibConfig config = rtc_calibration_get_config(32768000, TARGET_FREQUENCY_mHZ);
cl_assert_equal_i(config.units, 0);
}
void test_rtc_calibration__slightly_slow_but_not_enough_to_calibrate(void) {
// Approximately -2.01ppm
RTCCalibConfig config = rtc_calibration_get_config(32767934, TARGET_FREQUENCY_mHZ);
cl_assert_equal_i(config.units, 0);
}
void test_rtc_calibration__just_slow_enough_to_calibrate(void) {
// Approximately -2.04ppm
RTCCalibConfig config = rtc_calibration_get_config(32767933, TARGET_FREQUENCY_mHZ);
cl_assert_equal_i(config.units, 1);
cl_assert_equal_i(config.sign, RTC_CalibSign_Positive);
}
void test_rtc_calibration__slightly_fast_but_not_enough_to_calibrate(void) {
// Approximately +1.01ppm
RTCCalibConfig config = rtc_calibration_get_config(32768033, TARGET_FREQUENCY_mHZ);
cl_assert_equal_i(config.units, 0);
}
void test_rtc_calibration__just_fast_enough_to_calibrate(void) {
// Approximately +1.04ppm
RTCCalibConfig config = rtc_calibration_get_config(32768034, TARGET_FREQUENCY_mHZ);
cl_assert_equal_i(config.units, 1);
cl_assert_equal_i(config.sign, RTC_CalibSign_Negative);
}
void test_rtc_calibration__out_of_bounds_slow(void) {
// Approximately -130ppm
RTCCalibConfig config = rtc_calibration_get_config(32763740, TARGET_FREQUENCY_mHZ);
cl_assert_equal_i(config.units, 31);
cl_assert_equal_i(config.sign, RTC_CalibSign_Positive);
}
void test_rtc_calibration__out_of_bounds_fast(void) {
// Approximately +70ppm
RTCCalibConfig config = rtc_calibration_get_config(32770294, TARGET_FREQUENCY_mHZ);
cl_assert_equal_i(config.units, 31);
cl_assert_equal_i(config.sign, RTC_CalibSign_Negative);
}
void test_rtc_calibration__different_target_frequency_not_fast_enough(void) {
// Approximately +1.017ppm
RTCCalibConfig config = rtc_calibration_get_config(1000001017, ALTERNATE_FREQUENCY_mHZ);
cl_assert_equal_i(config.units, 0);
}
void test_rtc_calibration__different_target_frequency_just_fast_enough(void) {
// Approximately +1.018ppm
RTCCalibConfig config = rtc_calibration_get_config(1000001018, ALTERNATE_FREQUENCY_mHZ);
cl_assert_equal_i(config.units, 1);
cl_assert_equal_i(config.sign, RTC_CalibSign_Negative);
}
void test_rtc_calibration__invalid_frequency(void) {
// Bigboards don't have a frequency stored in their mfg info registry.
RTCCalibConfig config = rtc_calibration_get_config(0, TARGET_FREQUENCY_mHZ);
cl_assert_equal_i(config.units, 0);
}
+41
View File
@@ -0,0 +1,41 @@
from waftools.pebble_test import clar
def build(ctx):
clar(ctx,
sources_ant_glob = "src/fw/drivers/qemu/qemu_serial_util.c" \
" src/fw/util/shared_circular_buffer.c" \
" tests/fakes/fake_rtc.c",
test_sources_ant_glob = "test_qemu_serial.c",
override_includes=['dummy_board'])
clar(ctx,
sources_ant_glob="src/fw/drivers/fpc_pinstrap/fpc_pinstrap_snowy.c ",
test_sources_ant_glob="test_fpc_pinstrap.c",
override_includes=['fpc_pinstrap_board'])
clar(ctx,
sources_ant_glob = "src/fw/drivers/battery/battery_adc_conversion.c",
test_sources_ant_glob = "test_battery.c")
clar(ctx,
sources_ant_glob = "src/fw/drivers/stm32f2/rtc_calibration.c",
test_sources_ant_glob = "test_rtc_calibration.c",
override_includes=['rtc_calibration'])
clar(ctx,
sources_ant_glob = ('src/fw/drivers/stm32f7/i2c_timingr.c'),
test_sources_ant_glob = 'test_i2c_timingr.c')
clar(ctx,
sources_ant_glob = ('src/fw/drivers/flash/flash_api.c'),
test_sources_ant_glob = 'test_flash_api.c',
override_includes=['dummy_board'])
clar(ctx,
sources_ant_glob=('src/fw/drivers/flash/flash_erase.c'),
test_sources_ant_glob='test_flash_erase.c',
override_includes=['dummy_board'],
platforms=['tintin'])
# vim:filetype=python
+195
View File
@@ -0,0 +1,195 @@
/*
* 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/framebuffer.h"
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include "../test_graphics.h"
typedef struct stat STAT_T;
#ifndef PATH_MAX
#define PATH_MAX 4096
#endif
#define IMAGE_DWORDS_PER_LINE 6
static void print_framebuffer_as_literal(const char* unit_name, const FrameBuffer* framebuffer, int col, int row, int cols, int rows) {
printf("\n-- %s image --\n", unit_name);
printf(" uint32_t image[] = {\n");
int num_words = 0;
for (int j = row; j < row+rows && j < FrameBuffer_MaxY; j++) {
bool j_more = j+1 < row+rows && j+1 < FrameBuffer_MaxY;
for (int i = col; i < col+cols && i < FRAMEBUFFER_WORDS_PER_ROW; i++) {
bool i_more = i+1 < col+cols && i+1 < FRAMEBUFFER_WORDS_PER_ROW;
num_words++;
if (num_words % IMAGE_DWORDS_PER_LINE == 1) { printf(" "); }
printf("0x%x", framebuffer->buffer[j*FRAMEBUFFER_WORDS_PER_ROW+i]);
if (i_more || j_more) {
printf(",");
if (num_words % IMAGE_DWORDS_PER_LINE == 0) { printf("\n"); }
}
}
}
printf("\n };\n");
}
static void fread_pbi_header(FILE* file, GBitmap* bitmap) {
fread(&bitmap->row_size_bytes, sizeof(bitmap->row_size_bytes), 1, file);
fread(&bitmap->info_flags, sizeof(bitmap->info_flags), 1, file);
fread(&bitmap->bounds, sizeof(bitmap->bounds), 1, file);
}
static uint32_t* fread_pbi(FILE* file, GBitmap* bitmap) {
fread_pbi_header(file, bitmap);
size_t image_size = bitmap->row_size_bytes/4 * bitmap->bounds.size.h;
uint32_t* buffer = bitmap->addr = (uint32_t*) malloc(image_size*4);
if (buffer) {
fread(buffer, 4, image_size, file);
return buffer;
}
return NULL;
}
static uint32_t* read_pbi(const char* filename, GBitmap* bitmap) {
char res_path[strlen(CLAR_FIXTURE_PATH) + 1 + strlen(GRAPHICS_FIXTURE_PATH) + 1 + strlen(filename) + 1];
sprintf(res_path, "%s/%s/%s", CLAR_FIXTURE_PATH, GRAPHICS_FIXTURE_PATH, filename);
FILE* file = fopen(res_path, "r");
if (!file) {
printf("\ncould not open %s for reading\n", res_path);
return NULL;
}
uint32_t* buffer = fread_pbi(file, bitmap);
fclose(file);
return buffer;
}
static void free_pbi(GBitmap* bitmap) {
if (bitmap->addr) {
free(bitmap->addr);
}
}
static void fwrite_screenshot_from_framebuffer(FILE* file, const FrameBuffer* framebuffer) {
uint16_t row_size_bytes = (FrameBuffer_MaxX + 31) / 32 * 4;
uint16_t info_flags = 1 << 1;
GRect bounds = GRect(0, 0, FrameBuffer_MaxX, FrameBuffer_MaxY);
fwrite(&row_size_bytes, sizeof(row_size_bytes), 1, file);
fwrite(&info_flags, sizeof(info_flags), 1, file);
fwrite(&bounds, sizeof(bounds), 1, file);
fwrite(framebuffer->buffer, sizeof(framebuffer->buffer[0]), row_size_bytes/4*FrameBuffer_MaxY, file);
}
static bool write_screenshot_from_framebuffer(const char* filename, const FrameBuffer* framebuffer) {
FILE* file = fopen(filename, "w");
if (!file) {
printf("\ncould not open %s for writing a screenshot\n", filename);
return false;
}
fwrite_screenshot_from_framebuffer(file, framebuffer);
fclose(file);
return true;
}
static bool framebuffer_eq_image_raw(const FrameBuffer* framebuffer, uint32_t* image, int col, int row, int cols, int rows) {
for (int j = row; j < row+rows && j < FrameBuffer_MaxY; j++) {
for (int i = col; i < col+cols && i < FRAMEBUFFER_WORDS_PER_ROW; i++) {
int fb_index = j*FRAMEBUFFER_WORDS_PER_ROW+i;
int img_index = (j-row)*cols+(i-col);
uint32_t number_of_bits = col < IMAGE_DWORDS_PER_LINE-1 ? 32 : DISP_COLS % 32;
uint32_t mask = number_of_bits == 32 ? -1 : ((1 << number_of_bits) - 1);
uint32_t fb_part = mask & framebuffer->buffer[fb_index];
uint32_t img_part = mask & image[img_index];
if (fb_part != img_part) {
printf("\nframebuffer[%d] != image[%d], (0x%x, 0x%x) col=%d row=%d\n",
fb_index, img_index, framebuffer->buffer[fb_index], image[img_index], i, j);
return false;
}
}
}
return true;
}
static bool framebuffer_eq_image(const char* unit_name, const FrameBuffer* framebuffer, uint32_t* image, int col, int row, int cols, int rows) {
#ifndef TEST_GRAPHICS_SILENT
print_framebuffer_as_literal(unit_name, framebuffer, col, row, cols, rows);
#endif
return framebuffer_eq_image_raw(framebuffer, image, col, row, cols, rows);
}
static bool framebuffer_eq(const char* unit_name, const FrameBuffer* framebuffer, FrameBuffer* other) {
return framebuffer_eq_image(unit_name, framebuffer, other->buffer, 0, 0, FRAMEBUFFER_WORDS_PER_ROW, FrameBuffer_MaxY);
}
static bool framebuffer_eq_screenshot_raw(const FrameBuffer* framebuffer, const char* filename) {
GBitmap bitmap;
FILE* file = fopen(filename, "r");
if (!file) {
printf("\nfailed to open %s\n", filename);
return false;
}
fread_pbi_header(file, &bitmap);
uint32_t buffer[bitmap.row_size_bytes/4 * bitmap.bounds.size.h];
fread(buffer, 1, sizeof(buffer), file);
fclose(file);
if (!framebuffer_eq_image_raw(framebuffer, buffer, 0, 0, bitmap.row_size_bytes/4, bitmap.bounds.size.h)) {
printf("\ndoes not match screenshot %s\n", filename);
return false;
}
return true;
}
static bool framebuffer_eq_screenshot(const FrameBuffer* framebuffer, const char* filename) {
char ref_path[strlen(CLAR_FIXTURE_PATH) + 1 + strlen(GRAPHICS_FIXTURE_PATH) + 1 + strlen(filename) + 1];
sprintf(ref_path, "%s/%s/%s", CLAR_FIXTURE_PATH, GRAPHICS_FIXTURE_PATH, filename);
STAT_T st;
if (stat(ref_path, &st) != 0 || !framebuffer_eq_screenshot_raw(framebuffer, ref_path)) {
char out_path[strlen(GRAPHICS_FIXTURE_OUT_PATH) + 1 + strlen(filename) + 1];
sprintf(out_path, "%s/%s", GRAPHICS_FIXTURE_OUT_PATH,filename);
write_screenshot_from_framebuffer(out_path, framebuffer);
char cwd[PATH_MAX];
if (!getcwd(cwd, PATH_MAX)) {
printf("\nfailed to get working directory\n");
return false;
}
printf("\ngenerated %s/%s\n", cwd, out_path);
return false;
}
return true;
}
static bool framebuffer_is_empty(const char* unit_name, FrameBuffer* framebuffer, GColor color) {
for (int j = 0; j < FrameBuffer_MaxY; j++) {
for (int i = 0; i < FRAMEBUFFER_WORDS_PER_ROW; i++) {
int fb_index = j*FRAMEBUFFER_WORDS_PER_ROW+i;
if (framebuffer->buffer[fb_index] != (gcolor_equal(color, GColorBlack) ? 0 : 0xffffffff)) {
printf("\nframebuffer[%d] is not empty(%d), has 0x%x, col=%d row=%d\n",
fb_index, color.argb, framebuffer->buffer[fb_index], i, j);
return false;
}
}
}
return true;
}
+216
View File
@@ -0,0 +1,216 @@
/*
* 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/framebuffer.h"
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include "../test_graphics.h"
#include "../util.h"
typedef struct stat STAT_T;
#ifndef PATH_MAX
#define PATH_MAX 4096
#endif
#define IMAGE_DWORDS_PER_LINE 6
static void print_framebuffer_as_literal(const char* unit_name, const FrameBuffer* framebuffer,
int col, int row, int cols, int rows) {
printf("\n-- %s image --\n", unit_name);
printf(" uint8_t image[] = {\n");
for (int y = row; y < row+rows && y < FrameBuffer_MaxY; ++y) {
for (int x = col; x < col + cols && x < FrameBuffer_MaxX; ++x) {
printf("0x%x", framebuffer->buffer[y*FRAMEBUFFER_BYTES_PER_ROW + x]);
}
printf("\n");
}
printf("\n };\n");
}
static void fread_pbi_header(FILE* file, GBitmap* bitmap) {
fread(&bitmap->row_size_bytes, sizeof(bitmap->row_size_bytes), 1, file);
fread(&bitmap->info_flags, sizeof(bitmap->info_flags), 1, file);
fread(&bitmap->bounds, sizeof(bitmap->bounds), 1, file);
bitmap->info.format = GBitmapFormat1Bit;
}
static uint32_t* fread_pbi(FILE* file, GBitmap* bitmap) {
fread_pbi_header(file, bitmap);
size_t image_size = bitmap->row_size_bytes/4 * bitmap->bounds.size.h;
uint32_t* buffer = bitmap->addr = (uint32_t*) malloc(image_size*4);
if (buffer) {
fread(buffer, 4, image_size, file);
return buffer;
}
return NULL;
}
static uint32_t* read_pbi(const char* filename, GBitmap* bitmap) {
char res_path[strlen(CLAR_FIXTURE_PATH) + 1 + strlen(GRAPHICS_FIXTURE_PATH) + 1 + strlen(filename) + 1];
sprintf(res_path, "%s/%s/%s", CLAR_FIXTURE_PATH, GRAPHICS_FIXTURE_PATH, filename);
FILE* file = fopen(res_path, "r");
if (!file) {
printf("\ncould not open %s for reading\n", res_path);
return NULL;
}
uint32_t* buffer = fread_pbi(file, bitmap);
fclose(file);
return buffer;
}
static void free_pbi(GBitmap* bitmap) {
if (bitmap->addr) {
free(bitmap->addr);
}
}
static void fwrite_screenshot_from_framebuffer(FILE* file, const FrameBuffer* framebuffer) {
uint16_t row_size_bytes = FRAMEBUFFER_BYTES_PER_ROW;
uint16_t info_flags = 1 << 1;
GRect bounds = GRect(0, 0, FrameBuffer_MaxX, FrameBuffer_MaxY);
fwrite(&row_size_bytes, sizeof(row_size_bytes), 1, file);
fwrite(&info_flags, sizeof(info_flags), 1, file);
fwrite(&bounds, sizeof(bounds), 1, file);
fwrite(framebuffer->buffer, sizeof(framebuffer->buffer[0]), FRAMEBUFFER_SIZE_BYTES, file);
}
static bool write_screenshot_from_framebuffer(const char* filename, const FrameBuffer* framebuffer) {
FILE* file = fopen(filename, "w");
if (!file) {
printf("\ncould not open %s for writing a screenshot\n", filename);
return false;
}
fwrite_screenshot_from_framebuffer(file, framebuffer);
fclose(file);
return true;
}
static bool framebuffer_eq_image_raw(const FrameBuffer* framebuffer, uint8_t* image, int col,
int row, int cols, int rows) {
const uint8_t *fb = framebuffer->buffer;
for (int y = row; y < row+rows && y < FrameBuffer_MaxY; ++y) {
for (int x = col; x < col+cols && x < FrameBuffer_MaxX; ++x) {
int fb_index = y * FRAMEBUFFER_BYTES_PER_ROW + x;
int img_index = (y-row)*FRAMEBUFFER_BYTES_PER_ROW + (x-col);
if (fb[fb_index] != image[img_index]) {
printf("\nframebuffer[%d] != image[%d], (0x%x, 0x%x) col=%d row=%d\n",
fb_index, img_index, fb[fb_index], image[img_index], x, y);
return false;
}
}
}
return true;
}
static bool framebuffer_eq_image(const char* unit_name, const FrameBuffer* framebuffer,
uint8_t* image, int col, int row, int cols, int rows) {
#ifndef TEST_GRAPHICS_SILENT
print_framebuffer_as_literal(unit_name, framebuffer, col, row, cols, rows);
#endif
return framebuffer_eq_image_raw(framebuffer, image, col, row, cols, rows);
}
static bool framebuffer_eq(const char* unit_name, const FrameBuffer* framebuffer,
FrameBuffer* other) {
return framebuffer_eq_image(unit_name, framebuffer, other->buffer, 0, 0,
FRAMEBUFFER_BYTES_PER_ROW, FrameBuffer_MaxY);
}
static void convert_to_8bit_image(const uint32_t *in, GBitmap *bmp, uint8_t *out) {
for (int y = 0; y < bmp->bounds.size.h; ++y) {
for (int x = 0; x < bmp->bounds.size.w; ++x) {
int in_idx = y * (bmp->row_size_bytes/4) + (x / 32);
int bit_idx = x % 32;
bool set = in[in_idx] & (1 << bit_idx);
int out_idx = y * bmp->bounds.size.w + x;
out[out_idx] = (set) ? GColorWhite.argb : GColorBlack.argb;
}
}
}
static bool framebuffer_eq_screenshot_raw(const FrameBuffer* framebuffer, const char* filename) {
GBitmap bitmap;
FILE* file = fopen(filename, "r");
if (!file) {
printf("\nfailed to open %s\n", filename);
return false;
}
fread_pbi_header(file, &bitmap);
uint32_t buffer[bitmap.row_size_bytes/4 * bitmap.bounds.size.h];
fread(buffer, 1, sizeof(buffer), file);
fclose(file);
uint8_t image[bitmap.bounds.size.w * bitmap.bounds.size.h];
convert_to_8bit_image(buffer, &bitmap, image);
if (!framebuffer_eq_image_raw(framebuffer, image, 0, 0, bitmap.row_size_bytes,
bitmap.bounds.size.h)) {
bitmap.info.format = GBitmapFormat8Bit;
bitmap.row_size_bytes = FRAMEBUFFER_BYTES_PER_ROW;
printf("\ndoes not match screenshot %s\n", filename);
printf("Good:\n");
bitmap.addr = &image;
print_bitmap(&bitmap);
printf("Bad:\n");
bitmap.addr = (void*)&framebuffer->buffer;
print_bitmap(&bitmap);
return false;
}
return true;
}
static bool framebuffer_eq_screenshot(const FrameBuffer* framebuffer, const char* filename) {
char ref_path[strlen(CLAR_FIXTURE_PATH) + 1 + strlen(GRAPHICS_FIXTURE_PATH) + 1 + strlen(filename) + 1];
sprintf(ref_path, "%s/%s/%s", CLAR_FIXTURE_PATH, GRAPHICS_FIXTURE_PATH, filename);
STAT_T st;
if (stat(ref_path, &st) != 0 || !framebuffer_eq_screenshot_raw(framebuffer, ref_path)) {
char out_path[strlen(GRAPHICS_FIXTURE_OUT_PATH) + 1 + strlen(filename) + 1];
sprintf(out_path, "%s/%s", GRAPHICS_FIXTURE_OUT_PATH,filename);
write_screenshot_from_framebuffer(out_path, framebuffer);
char cwd[PATH_MAX];
if (!getcwd(cwd, PATH_MAX)) {
printf("\nfailed to get working directory\n");
return false;
}
printf("\ngenerated %s/%s\n", cwd, out_path);
return false;
}
return true;
}
static bool framebuffer_is_empty(const char* unit_name, FrameBuffer* framebuffer, GColor color) {
for (int j = 0; j < FrameBuffer_MaxY; j++) {
for (int i = 0; i < FRAMEBUFFER_BYTES_PER_ROW; i++) {
int fb_index = j*FRAMEBUFFER_BYTES_PER_ROW + i;
if (framebuffer->buffer[fb_index] != color.argb) {
printf("\nframebuffer[%d] is not empty(%d), has 0x%x, col=%d row=%d\n",
fb_index, color.argb, framebuffer->buffer[fb_index], i, j);
return false;
}
}
}
return true;
}
+29
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.
*/
#include "stubs_app_state.h"
#include "stubs_heap.h"
#include "stubs_language_ui.h"
#include "stubs_logging.h"
#include "stubs_passert.h"
#include "stubs_pbl_malloc.h"
#include "stubs_pebble_tasks.h"
#include "stubs_print.h"
#include "stubs_resources.h"
#include "stubs_serial.h"
#include "stubs_syscalls.h"
#include "stubs_ui_window.h"
#include "stubs_unobstructed_area.h"
+920
View File
@@ -0,0 +1,920 @@
/*
* 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/graphics/graphics.h"
#include "applib/graphics/bitblt.h"
#include "applib/graphics/bitblt_private.h"
#include "applib/graphics/8_bit/framebuffer.h"
#include "clar.h"
#include "util.h"
#include <string.h>
#include <stdio.h>
// Stubs
////////////////////////////////////
#include "graphics_common_stubs.h"
#include "stubs_applib_resource.h"
#include "test_graphics.h"
static GContext ctx;
static FrameBuffer framebuffer;
// Helpers
////////////////////////////////////
static void prv_set_opacity(GBitmap *bmp, uint8_t opacity) {
GRect bounds = bmp->bounds;
for (uint32_t idx = 0; idx < bounds.size.w * bounds.size.h; idx++) {
((GColor *)(bmp->addr + idx))->a = opacity;
}
}
static GColor prv_next_color(GColor color) {
GColor8 result = (GColor8){.argb = (color.argb + 1) % 64};
result.a = color.a;
return result;
}
// Tests
////////////////////////////////////
// setup and teardown
void test_bitblt__initialize(void) {
framebuffer_init(&framebuffer, &(GSize) {DISP_COLS, DISP_ROWS});
test_graphics_context_init(&ctx, &framebuffer);
}
void test_bitblt__cleanup(void) {
}
// Test images reside in "tests/fw/graphics/test_images/".
// The wscript will convert them from PNGs in that directory to PBIs in the build directory.
// Naming conventions of these images tends to be '<test_name>.<bitdepth>.png'.
// For example:
// test_bitblt__8bit_assign would have:
// - test_bitblt__8bit_assign.8bit.png
// - test_bitblt__8bit_assign-expect.8bit.png
// Tests assign, from same size to same size.
// Setup:
// - Source is 10x10, white.
// - Dest is 10x10, green.
// Result:
// - All white.
void test_bitblt__8bit_compop(void) {
GBitmap *src_bitmap = get_gbitmap_from_pbi("test_bitblt__8bit_assign.8bit.pbi");
uint8_t dest_data[src_bitmap->bounds.size.w*src_bitmap->bounds.size.h];
GBitmap dest_bitmap = {
.addr = dest_data,
.row_size_bytes = src_bitmap->bounds.size.w,
.info.format = GBitmapFormat8Bit,
.info.version = GBITMAP_VERSION_CURRENT,
.bounds = src_bitmap->bounds
};
// All compositing modes except GCompOpSet should be the same as GCompAssign
memset(dest_data, GColorGreen.argb, sizeof(dest_data));
bitblt_bitmap_into_bitmap(&dest_bitmap, src_bitmap, GPointZero, GCompOpAssign, GColorWhite);
cl_assert(gbitmap_pbi_eq(&dest_bitmap, "test_bitblt__8bit_assign-expect.8bit.pbi"));
memset(dest_data, GColorGreen.argb, sizeof(dest_data));
bitblt_bitmap_into_bitmap(&dest_bitmap, src_bitmap, GPointZero, GCompOpAssignInverted, GColorWhite);
cl_assert(gbitmap_pbi_eq(&dest_bitmap, "test_bitblt__8bit_assign-expect.8bit.pbi"));
memset(dest_data, GColorGreen.argb, sizeof(dest_data));
bitblt_bitmap_into_bitmap(&dest_bitmap, src_bitmap, GPointZero, GCompOpOr, GColorWhite);
cl_assert(gbitmap_pbi_eq(&dest_bitmap, "test_bitblt__8bit_assign-expect.8bit.pbi"));
memset(dest_data, GColorGreen.argb, sizeof(dest_data));
bitblt_bitmap_into_bitmap(&dest_bitmap, src_bitmap, GPointZero, GCompOpAnd, GColorWhite);
cl_assert(gbitmap_pbi_eq(&dest_bitmap, "test_bitblt__8bit_assign-expect.8bit.pbi"));
memset(dest_data, GColorGreen.argb, sizeof(dest_data));
bitblt_bitmap_into_bitmap(&dest_bitmap, src_bitmap, GPointZero, GCompOpClear, GColorWhite);
cl_assert(gbitmap_pbi_eq(&dest_bitmap, "test_bitblt__8bit_assign-expect.8bit.pbi"));
memset(dest_data, GColorGreen.argb, sizeof(dest_data));
// Update source bitmap to be semi-transparent
uint8_t *src_bitmap_buff = src_bitmap->addr;
for (int index = 0; index < src_bitmap->bounds.size.h * src_bitmap->row_size_bytes; index++) {
src_bitmap_buff[index] = src_bitmap_buff[index] & 0xbf;
}
bitblt_bitmap_into_bitmap(&dest_bitmap, src_bitmap, GPointZero, GCompOpSet, GColorWhite);
cl_assert(gbitmap_pbi_eq(&dest_bitmap, "test_bitblt__8bit_set-expect.8bit.pbi"));
gbitmap_destroy(src_bitmap);
}
// Test GCompOpTint, from same size to size.
// Setup:
// - Source is a 10x10 square, white.
// - Destination is either a black or white square.
// Result:
// - When source is transparent or tint color is clear, dest is black.
// - When source is opaque, dest is blended blue
void test_bitblt__8bit_to_8bit_comptint(void) {
GBitmap *src_bitmap = get_gbitmap_from_pbi("test_bitblt__8bit_assign.8bit.pbi");
GRect bounds = gbitmap_get_bounds(src_bitmap);
uint8_t dest_data[bounds.size.w * bounds.size.h];
GBitmap dest_bitmap = {
.addr = dest_data,
.row_size_bytes = bounds.size.w,
.info.format = GBitmapFormat8Bit,
.info.version = GBITMAP_VERSION_CURRENT,
.bounds = src_bitmap->bounds
};
uint8_t expect_bmp_data[bounds.size.w * bounds.size.h];
GBitmap expect_bmp = {
.addr = expect_bmp_data,
.row_size_bytes = bounds.size.w,
.info.format = GBitmapFormat8Bit,
.info.version = GBITMAP_VERSION_CURRENT,
.bounds = src_bitmap->bounds
};
// Verify that the compositing mode is correctly applied when the source is opaque and
// the tint color is not clear.
memset(dest_data, GColorClear.argb, sizeof(dest_data));
memset(expect_bmp_data, GColorBlue.argb, sizeof(expect_bmp_data));
bitblt_bitmap_into_bitmap(&dest_bitmap, src_bitmap, GPointZero, GCompOpTint, GColorBlue);
cl_assert(gbitmap_eq(&dest_bitmap, &expect_bmp, "test_bitblt__8bit_comptint-expect.8bit.pbi"));
// Rewrite the destination bitmap to be all black.
memset(dest_data, GColorBlack.argb, sizeof(dest_data));
// Verify that if the tint color is clear, than the source bitmap should not affect the destination bitmap
memset(expect_bmp_data, GColorBlack.argb, sizeof(expect_bmp_data));
bitblt_bitmap_into_bitmap(&dest_bitmap, src_bitmap, GPointZero, GCompOpTint, GColorClear);
cl_assert(gbitmap_eq(&dest_bitmap, &expect_bmp, "test_bitblt__8bit_comptint_clear-expect.8bit.pbi"));
// Verify that if the source is transparent, it should not affect the destination bitmap
prv_set_opacity(src_bitmap, 0);
bitblt_bitmap_into_bitmap(&dest_bitmap, src_bitmap, GPointZero, GCompOpTint, GColorRed);
cl_assert(gbitmap_eq(&dest_bitmap, &expect_bmp, "test_bitblt__8bit_comptint_clear-expect.8bit.pbi"));
gbitmap_destroy(src_bitmap);
}
// Tests comptint, multiple tint colors and varying opacity of the source image.
// Setup:
// - Destination is two 128 by 64 blocks expressing the 64 colors; each row expresses
// one of the colors stack vertically.
// - Source is a set of 4 by 4 blocks each with a 1 pixel wide vertical strip of a color
// with an opacity in [0,3] inclusive.
// Result:
// - Destination should be blended properly
void test_bitblt__8bit_comptint_blend(void) {
const uint8_t NUM_COLORS = 64;
const uint8_t WIDTH = NUM_COLORS * 2;
const uint8_t LEGEND_WIDTH = 4;
const uint8_t LEGEND_HEIGHT = 4;
const uint8_t OFFSET = 1;
const uint8_t HEIGHT = WIDTH;
const uint8_t NUM_OPACITIES = 4;
const uint8_t TOTAL_WIDTH = LEGEND_WIDTH * 2 + WIDTH;
const uint8_t TOTAL_HEIGHT = HEIGHT + OFFSET + LEGEND_HEIGHT * 2;
uint8_t dest_data[TOTAL_WIDTH * TOTAL_HEIGHT];
memset(dest_data, GColorWhite.argb, sizeof(dest_data));
GBitmap dest_bitmap = {
.addr = dest_data,
.row_size_bytes = TOTAL_WIDTH,
.info.format = GBitmapFormat8Bit,
.info.version = GBITMAP_VERSION_CURRENT,
.bounds = (GRect){GPointZero, (GSize){TOTAL_WIDTH, TOTAL_HEIGHT}}
};
GColor color = (GColor){ .a = 3, .r = 0, .g = 0, .b = 0 };
uint8_t src_data[] = { color.argb };
GBitmap src_bmp = (GBitmap){
.addr = src_data,
.row_size_bytes = 1,
.info.format = GBitmapFormat8Bit,
.info.version = GBITMAP_VERSION_CURRENT,
.bounds = GRect(0, 0, 1, 1)
};
for (uint8_t offset_y = 0; offset_y < NUM_COLORS; offset_y++) {
const int16_t offset_x = OFFSET + LEGEND_WIDTH + WIDTH;
const int16_t x = LEGEND_WIDTH;
memset(src_data, color.argb, sizeof(src_data));
color = prv_next_color(color);
const int16_t y_upper = LEGEND_HEIGHT + offset_y;
const GRect upper_rect = GRect(x, y_upper, WIDTH, 1);
const GRect upper_left_legend_rect = GRect(0, y_upper, 3, 1);
const GRect upper_right_legend_rect = GRect(offset_x, y_upper, 3, 1);
bitblt_bitmap_into_bitmap_tiled(&dest_bitmap, &src_bmp, upper_rect,
GPointZero, GCompOpAssign, GColorWhite);
bitblt_bitmap_into_bitmap_tiled(&dest_bitmap, &src_bmp, upper_left_legend_rect,
GPointZero, GCompOpAssign, GColorWhite);
bitblt_bitmap_into_bitmap_tiled(&dest_bitmap, &src_bmp, upper_right_legend_rect,
GPointZero, GCompOpAssign, GColorWhite);
const int16_t y_lower = y_upper + LEGEND_HEIGHT + NUM_COLORS + OFFSET;
const GRect lower_rect = GRect(x, y_lower, WIDTH, 1);
const GRect lower_left_legend_rect = GRect(0, y_lower, 3, 1);
const GRect lower_right_legend_rect = GRect(offset_x, y_lower, 3, 1);
bitblt_bitmap_into_bitmap_tiled(&dest_bitmap, &src_bmp, lower_rect,
GPointZero, GCompOpAssign, GColorWhite);
bitblt_bitmap_into_bitmap_tiled(&dest_bitmap, &src_bmp, lower_left_legend_rect,
GPointZero, GCompOpAssign, GColorWhite);
bitblt_bitmap_into_bitmap_tiled(&dest_bitmap, &src_bmp, lower_right_legend_rect,
GPointZero, GCompOpAssign, GColorWhite);
}
// RGB value should be discarded later on adding them here might relveal bugs.
// .a is the important part
GColor8 test_blend_colors[] = {
(GColor8){.a = 0, .r = 3, .g = 2, .b = 1},
(GColor8){.a = 1, .r = 0, .g = 3, .b = 2},
(GColor8){.a = 2, .r = 1, .g = 0, .b = 3},
(GColor8){.a = 3, .r = 2, .g = 1, .b = 0}
};
// Test image with four pixels of all our suported alpha values
GBitmap test_bmp = (GBitmap){
.addr = test_blend_colors,
.row_size_bytes = 4,
.bounds = GRect(0, 0, 4, 1),
.info.format = GBitmapFormat8Bit,
.info.version = GBITMAP_VERSION_CURRENT
};
for (uint8_t rgb_half = 0; rgb_half < NUM_COLORS / 2; rgb_half++) {
const int16_t x = rgb_half * NUM_OPACITIES + LEGEND_WIDTH;
const int8_t legend_height = 3;
// Upper row with destination colors from 0..31
const GColor upper_tint_color = (GColor){.argb = 0b11000000 | rgb_half};
const int16_t y_upper = LEGEND_HEIGHT;
const GRect upper_rect = GRect(x, y_upper, test_bmp.bounds.size.w, NUM_COLORS);
const GRect upper_legend_rect = GRect(x, y_upper - LEGEND_HEIGHT, test_bmp.bounds.size.w, legend_height);
GColor8 upper_legend[4] = { [0 ... 3] = upper_tint_color };
test_bmp.addr = upper_legend;
bitblt_bitmap_into_bitmap_tiled(&dest_bitmap, &test_bmp, upper_legend_rect, GPointZero, GCompOpAssign, GColorWhite);
test_bmp.addr = test_blend_colors;
bitblt_bitmap_into_bitmap_tiled(&dest_bitmap, &test_bmp, upper_rect, GPointZero, GCompOpTint, upper_tint_color);
// Lower row with destination colors from 31..63
const int16_t y_lower = y_upper + NUM_COLORS + OFFSET + LEGEND_HEIGHT;
const GRect lower_rect = GRect(x, y_lower, test_bmp.bounds.size.w, NUM_COLORS);
const GColor lower_tint_color = (GColor){.argb = 0b11000000 | (rgb_half + (NUM_COLORS / 2))};
const GRect lower_legend_rect = GRect(x, y_lower - LEGEND_HEIGHT, test_bmp.bounds.size.w, legend_height);
GColor8 lower_legend[4] = { [0 ... 3] = lower_tint_color };
test_bmp.addr = lower_legend;
bitblt_bitmap_into_bitmap_tiled(&dest_bitmap, &test_bmp, lower_legend_rect, GPointZero, GCompOpAssign, GColorWhite);
test_bmp.addr = test_blend_colors;
bitblt_bitmap_into_bitmap_tiled(&dest_bitmap, &test_bmp, lower_rect, GPointZero, GCompOpTint, lower_tint_color);
}
cl_assert(gbitmap_pbi_eq(&dest_bitmap, "test_bitblt__8bit_comptint_blend-expect.8bit.pbi"));
}
// Tests assign, clipping, makes sure in bottom right corner.
// Setup:
// - Source is 10x15, black box ((0, 0), (5, 10)), rest is red.
// - Dest is White, 50x50.
// - Dest offset is set to 5x10 pixels in bottom right corner.
// Result:
// - White, with 5x10 black box in bottom right corner.
void test_bitblt__8bit_clipping(void) {
GBitmap *src_bitmap = get_gbitmap_from_pbi("test_bitblt__8bit_clipping.8bit.pbi");
uint8_t dest_data[50*50];
GBitmap dest_bitmap = {
.addr = dest_data,
.row_size_bytes = 50,
.info.format = GBitmapFormat8Bit,
.info.version = GBITMAP_VERSION_CURRENT,
.bounds = { { 0, 0 }, { 50, 50 } }
};
memset(dest_data, GColorWhite.argb, sizeof(dest_data));
GPoint dest_offset = { dest_bitmap.bounds.size.w-5, dest_bitmap.bounds.size.h-10 };
bitblt_bitmap_into_bitmap(&dest_bitmap, src_bitmap, dest_offset, GCompOpAssign, GColorWhite);
cl_assert(gbitmap_pbi_eq(&dest_bitmap, "test_bitblt__8bit_clipping-expect.8bit.pbi"));
gbitmap_destroy(src_bitmap);
}
// Test horizontal wrapping when dest_rect wider than src_bitmap.
// Setup:
// - Source 15 x 10, each row has the folling pattern:
// - 2px Red
// - 13px Black
// - Dest Green 50x50
// - Dest rect (17, 10) at (0, 0)
// Result:
// - 2px Red
// - 13px Black
// - 2px Red
// - Rest Blue
void test_bitblt__8bit_wrap_x(void) {
GBitmap *src_bitmap = get_gbitmap_from_pbi("test_bitblt__8bit_wrap_x.8bit.pbi");
uint8_t dest_data[50*50];
GBitmap dest_bitmap = {
.addr = dest_data,
.row_size_bytes = 50,
.info.format = GBitmapFormat8Bit,
.info.version = GBITMAP_VERSION_CURRENT,
.bounds = { { 0, 0 }, { 50, 50 } }
};
memset(dest_data, GColorBlue.argb, sizeof(dest_data));
// 2 wider than src_bitmap, so 2 columns of red will repeat again.
GRect dest_rect = GRect(0, 0, src_bitmap->bounds.size.w + 2, src_bitmap->bounds.size.h);
bitblt_bitmap_into_bitmap_tiled_8bit_to_8bit(
&dest_bitmap, src_bitmap, dest_rect, GPointZero, GCompOpAssign, GColorWhite);
cl_assert(gbitmap_pbi_eq(&dest_bitmap, "test_bitblt__8bit_wrap_x-expect.8bit.pbi"));
gbitmap_destroy(src_bitmap);
}
// Test vertical wrapping when dest_rect taller than src_bitmap.
// Setup:
// - Source is 25 x 10
// - 4 rows red, 2 rows blue, 4 rows black.
// - Dest is Green, 50 x 50
// - Dest Rect is 10 x 24 at (0, 0)
// Result:
// - Pattern repeated vertically x2, plus 4 rows of red.
void test_bitblt__8bit_wrap_y(void) {
GBitmap *src_bitmap = get_gbitmap_from_pbi("test_bitblt__8bit_wrap_y.8bit.pbi");
uint8_t dest_data[50*50];
GBitmap dest_bitmap = {
.addr = dest_data,
.row_size_bytes = 50,
.info.format = GBitmapFormat8Bit,
.info.version = GBITMAP_VERSION_CURRENT,
.bounds = { { 0, 0 }, { 50, 50 } }
};
memset(dest_data, GColorGreen.argb, sizeof(dest_data));
GRect dest_rect = GRect(0, 0, src_bitmap->bounds.size.w, src_bitmap->bounds.size.h*2 + 4);
bitblt_bitmap_into_bitmap_tiled_8bit_to_8bit(
&dest_bitmap, src_bitmap, dest_rect, GPointZero, GCompOpAssign, GColorWhite);
cl_assert(gbitmap_pbi_eq(&dest_bitmap, "test_bitblt__8bit_wrap_y-expect.8bit.pbi"));
gbitmap_destroy(src_bitmap);
}
// Test src_origin_offset, shouldn't see any red in dest_bitmap.
// This covers src_origin_offset, y-axis and x-axis wraparound.
// Setup:
// - Source 25x25, 2 columns, 2 rows red, rest is black.
// - Source offset starts at (2, 2)
// - Dest is blue, 100x100.
// - Dest rect is 50x50 at (0,0).
// Result:
// - No red in dest_bitmap.
// - 50x50 black square at (0,0), rest is blue.
void test_bitblt__8bit_src_origin_offset_wrap(void) {
GBitmap *src_bitmap = get_gbitmap_from_pbi("test_bitblt__8bit_src_origin_offset_wrap.8bit.pbi");
uint8_t dest_data[100*100];
GBitmap dest_bitmap = {
.addr = dest_data,
.row_size_bytes = 100,
.info.format = GBitmapFormat8Bit,
.info.version = GBITMAP_VERSION_CURRENT,
.bounds = { { 0, 0 }, { 100, 100 } }
};
memset(dest_data, GColorBlue.argb, sizeof(dest_data));
GRect dest_rect = GRect(0, 0, src_bitmap->bounds.size.w*2, src_bitmap->bounds.size.h*2);
GPoint src_origin_offset = { 2, 2 }; // Offset past the 2 red rows
bitblt_bitmap_into_bitmap_tiled_8bit_to_8bit(
&dest_bitmap, src_bitmap, dest_rect, src_origin_offset, GCompOpAssign, GColorWhite);
cl_assert(gbitmap_pbi_eq(&dest_bitmap,
"test_bitblt__8bit_src_origin_offset_wrap-expect.8bit.pbi"));
gbitmap_destroy(src_bitmap);
}
//
// Test 1-bit to 8-bit blitting
///////////////////////////////
// Setup:
// - Source is 25x25.
// - Source has alternating white / black lines.
// - Dest is Blue, 100x100.
// - Dest offset is (0,0) to blit to top left corner.
// Result:
// - 25x25 alternating black / white lines in top left corner.
void test_bitblt__1bit_to_8bit_compop(void) {
GBitmap *src_bitmap = get_gbitmap_from_pbi("test_bitblt__1bit_to_8bit_assign.1bit.pbi");
uint8_t dest_data[100*100];
GBitmap dest_bitmap = {
.addr = dest_data,
.row_size_bytes = 100,
.info.format = GBitmapFormat8Bit,
.info.version = GBITMAP_VERSION_CURRENT,
.bounds = { { 0, 0 }, { 100, 100 } }
};
memset(dest_data, GColorBlue.argb, sizeof(dest_data));
bitblt_bitmap_into_bitmap(&dest_bitmap, src_bitmap, GPointZero, GCompOpAssign, GColorWhite);
cl_assert(gbitmap_pbi_eq(&dest_bitmap, "test_bitblt__1bit_to_8bit_assign-expect.8bit.pbi"));
memset(dest_data, GColorBlue.argb, sizeof(dest_data));
bitblt_bitmap_into_bitmap(&dest_bitmap, src_bitmap, GPointZero, GCompOpAssignInverted, GColorWhite);
cl_check(gbitmap_pbi_eq(&dest_bitmap, "test_bitblt__1bit_to_8bit_assigninverted-expect.8bit.pbi"));
memset(dest_data, GColorBlue.argb, sizeof(dest_data));
bitblt_bitmap_into_bitmap(&dest_bitmap, src_bitmap, GPointZero, GCompOpOr, GColorWhite);
cl_check(gbitmap_pbi_eq(&dest_bitmap, "test_bitblt__1bit_to_8bit_or-expect.8bit.pbi"));
memset(dest_data, GColorBlue.argb, sizeof(dest_data));
bitblt_bitmap_into_bitmap(&dest_bitmap, src_bitmap, GPointZero, GCompOpAnd, GColorWhite);
cl_check(gbitmap_pbi_eq(&dest_bitmap, "test_bitblt__1bit_to_8bit_and-expect.8bit.pbi"));
memset(dest_data, GColorBlue.argb, sizeof(dest_data));
bitblt_bitmap_into_bitmap(&dest_bitmap, src_bitmap, GPointZero, GCompOpClear, GColorWhite);
cl_check(gbitmap_pbi_eq(&dest_bitmap, "test_bitblt__1bit_to_8bit_clear-expect.8bit.pbi"));
memset(dest_data, GColorBlue.argb, sizeof(dest_data));
bitblt_bitmap_into_bitmap(&dest_bitmap, src_bitmap, GPointZero, GCompOpSet, GColorWhite);
cl_check(gbitmap_pbi_eq(&dest_bitmap, "test_bitblt__1bit_to_8bit_set-expect.8bit.pbi"));
gbitmap_destroy(src_bitmap);
}
// Setup:
// - Source is an image of a white cross
// - Dest is blue, same size as source
// Result:
// - Destination should be written with a White cross
// Description:
// - This test verifies that when the bitmap is 1-bit, we treat white as a
// non-transparent color
void test_bitblt__1bit_to_8bit_compor(void) {
GBitmap *src_bitmap = get_gbitmap_from_pbi("test_bitblt__1bit_to_8bit_compor.1bit.pbi");
GRect bounds = gbitmap_get_bounds(src_bitmap);
cl_assert_equal_i(src_bitmap->info.format, GBitmapFormat1Bit);
uint8_t dest_data[bounds.size.w * bounds.size.h];
GBitmap dest_bitmap = {
.addr = dest_data,
.row_size_bytes = bounds.size.w,
.info.format = GBitmapFormat8Bit,
.info.version = GBITMAP_VERSION_CURRENT,
.bounds = src_bitmap->bounds
};
memset(dest_data, GColorBlue.argb, sizeof(dest_data));
bitblt_bitmap_into_bitmap(&dest_bitmap, src_bitmap, (GPoint){0}, GCompOpOr, GColorLightGray);
cl_check(gbitmap_pbi_eq(&dest_bitmap, "test_bitblt__1bit_to_8bit_compor-expect.8bit.pbi"));
gbitmap_destroy(src_bitmap);
}
// Setup:
// - Source is a 1bit image of a white cross with a black background
// Result:
// - The image names describe the expected result of each destination color / tint color
// combination
void test_bitblt__1bit_to_8bit_comptint(void) {
GBitmap *src_bitmap = get_gbitmap_from_pbi("test_bitblt__1bit_to_8bit_comptint.1bit.pbi");
const GRect bounds = gbitmap_get_bounds(src_bitmap);
cl_assert_equal_i(src_bitmap->info.format, GBitmapFormat1Bit);
uint8_t dest_data[bounds.size.w * bounds.size.h];
GBitmap dest_bitmap = {
.addr = dest_data,
.row_size_bytes = bounds.size.w,
.info.format = GBitmapFormat8Bit,
.info.version = GBITMAP_VERSION_CURRENT,
.bounds = src_bitmap->bounds
};
// Image name descriptions
// comptint_<cross_color>_on_<background_color>.8bit
// Destination White
memset(dest_data, GColorWhite.argb, sizeof(dest_data));
bitblt_bitmap_into_bitmap(&dest_bitmap, src_bitmap, (GPoint){0}, GCompOpTint, GColorWhite);
cl_check(gbitmap_pbi_eq(&dest_bitmap, "test_bitblt__1bit_to_8bit_comptint_white_cross_white_corners-expect.8bit.pbi"));
memset(dest_data, GColorWhite.argb, sizeof(dest_data));
bitblt_bitmap_into_bitmap(&dest_bitmap, src_bitmap, (GPoint){0}, GCompOpTint, GColorBlack);
cl_check(gbitmap_pbi_eq(&dest_bitmap, "test_bitblt__1bit_to_8bit_comptint_white_cross_black_corners-expect.8bit.pbi"));
memset(dest_data, GColorWhite.argb, sizeof(dest_data));
bitblt_bitmap_into_bitmap(&dest_bitmap, src_bitmap, (GPoint){0}, GCompOpTint, GColorLightGray);
cl_check(gbitmap_pbi_eq(&dest_bitmap, "test_bitblt__1bit_to_8bit_comptint_white_cross_lightgray_corners-expect.8bit.pbi"));
// Destination Black
memset(dest_data, GColorBlack.argb, sizeof(dest_data));
bitblt_bitmap_into_bitmap(&dest_bitmap, src_bitmap, (GPoint){0}, GCompOpTint, GColorWhite);
cl_check(gbitmap_pbi_eq(&dest_bitmap, "test_bitblt__1bit_to_8bit_comptint_black_cross_white_corners-expect.8bit.pbi"));
memset(dest_data, GColorBlack.argb, sizeof(dest_data));
bitblt_bitmap_into_bitmap(&dest_bitmap, src_bitmap, (GPoint){0}, GCompOpTint, GColorBlack);
cl_check(gbitmap_pbi_eq(&dest_bitmap, "test_bitblt__1bit_to_8bit_comptint_black_cross_black_corners-expect.8bit.pbi"));
memset(dest_data, GColorBlack.argb, sizeof(dest_data));
bitblt_bitmap_into_bitmap(&dest_bitmap, src_bitmap, (GPoint){0}, GCompOpTint, GColorLightGray);
cl_check(gbitmap_pbi_eq(&dest_bitmap, "test_bitblt__1bit_to_8bit_comptint_black_cross_lightgray_corners-expect.8bit.pbi"));
gbitmap_destroy(src_bitmap);
}
// Setup:
// - Source is 25x25.
// - Source has alternating white / black lines.
// - Dest is Blue, 100x100.
// - Dest offset is set to 8x10 clipped in the bottom right corner.
// Result:
// - There should be an 8x10 alternating black & white lines in the bottom right corner.
void test_bitblt__1bit_to_8bit_clipping(void) {
GBitmap *src_bitmap = get_gbitmap_from_pbi("test_bitblt__1bit_to_8bit_clipping.1bit.pbi");
uint8_t dest_data[100*100];
GBitmap dest_bitmap = {
.addr = dest_data,
.row_size_bytes = 100,
.info.format = GBitmapFormat8Bit,
.info.version = GBITMAP_VERSION_CURRENT,
.bounds = { { 0, 0 }, { 100, 100 } }
};
memset(dest_data, GColorBlue.argb, sizeof(dest_data));
GPoint dest_offset = { dest_bitmap.bounds.size.w-8, dest_bitmap.bounds.size.h-10 };
bitblt_bitmap_into_bitmap(&dest_bitmap, src_bitmap, dest_offset, GCompOpAssign, GColorWhite);
cl_assert(gbitmap_pbi_eq(&dest_bitmap, "test_bitblt__1bit_to_8bit_clipping-expect.8bit.pbi"));
gbitmap_destroy(src_bitmap);
}
// Setup:
// - Source is 25 x 25.
// - Source rows alternating: 1px black, 1px white.
// - Dest is Blue, 100x100
// - Dest rect is 50 x 25, at (0, 0)
// Result:
// - 50x20 of alternating stripes, rest is blue.
void test_bitblt__1bit_to_8bit_wrap_x(void) {
GBitmap *src_bitmap = get_gbitmap_from_pbi("test_bitblt__1bit_to_8bit_wrap_x.1bit.pbi");
uint8_t dest_data[100*100];
GBitmap dest_bitmap = {
.addr = dest_data,
.row_size_bytes = 100,
.info.format = GBitmapFormat8Bit,
.info.version = GBITMAP_VERSION_CURRENT,
.bounds = { { 0, 0 }, { 100, 100 } }
};
memset(dest_data, GColorBlue.argb, sizeof(dest_data));
GRect dest_rect = GRect(0, 0, src_bitmap->bounds.size.w*2, src_bitmap->bounds.size.h);
bitblt_bitmap_into_bitmap_tiled_1bit_to_8bit(
&dest_bitmap, src_bitmap, dest_rect, GPointZero, GCompOpAssign, GColorWhite);
cl_assert(gbitmap_pbi_eq(&dest_bitmap, "test_bitblt__1bit_to_8bit_wrap_x-expect.8bit.pbi"));
gbitmap_destroy(src_bitmap);
}
// Setup:
// - Source is 40 x 30.
// - Source has 2 columns, 4 rows of black, rest is white.
// - Dest is all blue.
// - Source offset (2, 4) past black.
// - Destination is 100 x 100
// - Destination rect is size of white portion of source.
// Result:
// - Blue bitmap with white square at 0,0 of dest_rect size.
void test_bitblt__1bit_to_8bit_src_origin_offset(void) {
GBitmap *src_bitmap =
get_gbitmap_from_pbi("test_bitblt__1bit_to_8bit_src_origin_offset.1bit.pbi");
uint8_t dest_data[100*100];
GBitmap dest_bitmap = {
.addr = dest_data,
.row_size_bytes = 100,
.info.format = GBitmapFormat8Bit,
.info.version = GBITMAP_VERSION_CURRENT,
.bounds = { { 0, 0 }, { 100, 100 } }
};
memset(dest_data, GColorBlue.argb, sizeof(dest_data));
GRect dest_rect = GRect(0, 0, src_bitmap->bounds.size.w - 2, src_bitmap->bounds.size.h - 4);
GPoint src_origin_offset = { 2, 4 }; // Offset past the black
bitblt_bitmap_into_bitmap_tiled_1bit_to_8bit(
&dest_bitmap, src_bitmap, dest_rect, src_origin_offset, GCompOpAssign, GColorWhite);
cl_assert(gbitmap_pbi_eq(&dest_bitmap,
"test_bitblt__1bit_to_8bit_src_origin_offset-expect.8bit.pbi"));
gbitmap_destroy(src_bitmap);
}
// Setup:
// - Source is a 10x32 white square.
// - Dest is all black.
// - Dest origin offset set to 15, 18.
// - Dest clipped to 10x10
// Result:
// - Black bitmap with 10x10 white square starting at (15, 10)
void test_bitblt__1bit_to_8bit_dest_origin_offset_clip(void) {
GBitmap *src_bitmap =
get_gbitmap_from_pbi("test_bitblt__1bit_to_8bit_dest_origin_offset_clip.1bit.pbi");
uint8_t dest_data[100*100];
GBitmap dest_bitmap = {
.addr = dest_data,
.row_size_bytes = 100,
.info.format = GBitmapFormat8Bit,
.info.version = GBITMAP_VERSION_CURRENT,
.bounds = { { 0, 0 }, { 100, 100 } }
};
memset(dest_data, GColorBlack.argb, sizeof(dest_data));
GRect dest_rect = GRect(15, 10, 10, 10);
GPoint src_origin_offset = { 0, 0 };
bitblt_bitmap_into_bitmap_tiled(&dest_bitmap, src_bitmap, dest_rect, src_origin_offset,
GCompOpAssign, GColorWhite);
cl_assert(gbitmap_pbi_eq(&dest_bitmap,
"test_bitblt__1bit_to_8bit_dest_origin_offset_clip-expect.8bit.pbi"));
gbitmap_destroy(src_bitmap);
}
// Setup:
// - Source width is 32 pixels (ie. a word in source)
// - Source starts with 2 rows and 4 columns of black pixels.
// - Dest is all blue.
// - Src origin is set to (4, 2)
// - dest origin is set to 10, 25
// - dest size is twice the height / width of the source.
void test_bitblt__1bit_to_8bit_src_origin_offset_wrap(void) {
GBitmap *src_bitmap =
get_gbitmap_from_pbi("test_bitblt__1bit_to_8bit_src_origin_offset_wrap.1bit.pbi");
uint8_t dest_data[100*100];
GBitmap dest_bitmap = {
.addr = dest_data,
.row_size_bytes = 100,
.info.format = GBitmapFormat8Bit,
.info.version = GBITMAP_VERSION_CURRENT,
.bounds = { { 0, 0 }, { 100, 100 } }
};
memset(dest_data, GColorBlue.argb, sizeof(dest_data));
GRect dest_rect = GRect(10, 25, src_bitmap->bounds.size.w*2, src_bitmap->bounds.size.h*2);
GPoint src_origin_offset = { .x = 4, .y = 2 }; // Offset past the black
bitblt_bitmap_into_bitmap_tiled(&dest_bitmap, src_bitmap, dest_rect, src_origin_offset,
GCompOpAssign, GColorWhite);
cl_assert(gbitmap_pbi_eq(&dest_bitmap,
"test_bitblt__1bit_to_8bit_src_origin_offset_wrap-expect.8bit.pbi"));
gbitmap_destroy(src_bitmap);
}
// Setup:
// - Source width is not a multiple of 8 (ie. not byte aligned in source)
// - Source starts with 2 rows and 4 columns of black pixels.
// - Dest is all blue.
// - Src origin is set to (4, 2)
// - dest origin is set to 22, 6
// - dest size is twice the height / width of the source.
void test_bitblt__1bit_to_8bit_src_origin_offset_wrap2(void) {
GBitmap *src_bitmap =
get_gbitmap_from_pbi("test_bitblt__1bit_to_8bit_src_origin_offset_wrap2.1bit.pbi");
uint8_t dest_data[100*100];
GBitmap dest_bitmap = {
.addr = dest_data,
.row_size_bytes = 100,
.info.format = GBitmapFormat8Bit,
.info.version = GBITMAP_VERSION_CURRENT,
.bounds = { { 0, 0 }, { 100, 100 } }
};
memset(dest_data, GColorBlue.argb, sizeof(dest_data));
GRect dest_rect = GRect(21, 6, src_bitmap->bounds.size.w*2, src_bitmap->bounds.size.h*2);
GPoint src_origin_offset = { .x = 4, .y = 2 }; // Offset past the black
bitblt_bitmap_into_bitmap_tiled(&dest_bitmap, src_bitmap, dest_rect, src_origin_offset,
GCompOpAssign, GColorWhite);
cl_assert(gbitmap_pbi_eq(&dest_bitmap,
"test_bitblt__1bit_to_8bit_src_origin_offset_wrap2-expect.8bit.pbi"));
gbitmap_destroy(src_bitmap);
}
// Setup:
// - Source has 2 lines of white and the rest black.
// - Destination all blue.
// - Use gbitmap_init_as_sub_bitmap to get a sub-bitmap that starts at y = 2
// Result:
// - A 48 x 50 black box starting at y=2 in dest, rest is blue.
void test_bitblt__bitmap_into_bitmap_sub_bitmap(void) {
GBitmap *src_bitmap = get_gbitmap_from_pbi("test_bitblt__bitmap_into_bitmap_sub_bitmap.8bit.pbi");
GBitmap cropped_src_bitmap;
gbitmap_init_as_sub_bitmap(&cropped_src_bitmap, src_bitmap,
(GRect) { { 0, 2 }, src_bitmap->bounds.size });
uint8_t dest_data[50*50];
GBitmap dest_bitmap = {
.addr = dest_data,
.row_size_bytes = 50,
.info.format = GBitmapFormat8Bit,
.info.version = GBITMAP_VERSION_CURRENT,
.bounds = { { 0, 0 }, { 50, 50 } }
};
memset(dest_data, GColorBlue.argb, sizeof(dest_data));
bitblt_bitmap_into_bitmap(&dest_bitmap, &cropped_src_bitmap, (GPoint) { 0, 2 },
GCompOpAssign, GColorWhite);
cl_assert(gbitmap_pbi_eq(&dest_bitmap,
"test_bitblt__bitmap_into_bitmap_sub_bitmap-expect.8bit.pbi"));
gbitmap_destroy(src_bitmap);
}
// Test:
// - source origin offset
// - source bounds origin and size
// - wrapping into larger destination
// - dest rect not at { 0, 0 }
// Setup:
// - Source has non-zero bounds origin, { 5, 5 }, outside of this is red.
// - Source has non-full bounds size, { 10, 10 }, outside of this is white.
// - Source has an origin offset, { 3, 6 }, outside of this is blue.
// - Rest is black
//
// - Destination has non-zero dest_rect origin, { 4, 4 }
// - Destination has non-full dest_rect size larger than source, { 10, 10 }
// - Destination has full size of { 20, 20 }
// Result:
// - Expect a 10 x 10 black box at { 4, 4 } that is of size { 10, 10 }
// - Remainder should be green.
void test_bitblt__8bit_bounds_and_origin_offset(void) {
GBitmap *src_bitmap = get_gbitmap_from_pbi("test_bitblt__bounds_and_origin_offset.8bit.pbi");
src_bitmap->bounds = (GRect) { { 5, 5}, { 10, 10 } };
uint8_t dest_data[50*50];
GBitmap dest_bitmap = {
.addr = dest_data,
.row_size_bytes = 50,
.info.format = GBitmapFormat8Bit,
.info.version = GBITMAP_VERSION_CURRENT,
.bounds = { { 0, 0 }, { 50, 50 } }
};
memset(dest_data, GColorGreen.argb, sizeof(dest_data));
const GRect dest_rect = GRect(4, 4, 10, 10);
const GPoint src_origin_offset = GPoint(3, 6);
bitblt_bitmap_into_bitmap_tiled_8bit_to_8bit(&dest_bitmap, src_bitmap, dest_rect,
src_origin_offset, GCompOpAssign, GColorWhite);
cl_assert(gbitmap_pbi_eq(&dest_bitmap, "test_bitblt__bounds_and_origin_offset-expect.8bit.pbi"));
gbitmap_destroy(src_bitmap);
}
// Setup:
// - Source is a 1bit image of a white cross with a black background
// Result:
// - The image names describe the expected result of each destination color / tint color
// combination
void test_bitblt__1bit_to_1bit_comptint(void) {
GBitmap *src_bitmap = get_gbitmap_from_pbi("test_bitblt__1bit_to_1bit_comptint.1bit.pbi");
const GRect bounds = gbitmap_get_bounds(src_bitmap);
cl_assert_equal_i(src_bitmap->info.format, GBitmapFormat1Bit);
uint8_t dest_data[bounds.size.w * bounds.size.h];
GBitmap dest_bitmap = {
.addr = dest_data,
.row_size_bytes = src_bitmap->row_size_bytes,
.info.format = GBitmapFormat1Bit,
.info.version = GBITMAP_VERSION_CURRENT,
.bounds = src_bitmap->bounds
};
// Image name descriptions
// comptint_<cross_color>_on_<background_color>.1bit
// Destination White
memset(dest_data, 0b11111111, sizeof(dest_data));
bitblt_bitmap_into_bitmap(&dest_bitmap, src_bitmap, (GPoint){0}, GCompOpTint, GColorClear);
cl_check(gbitmap_pbi_eq(&dest_bitmap, "test_bitblt__1bit_to_1bit_comptint_white_on_white-expect.1bit.pbi"));
memset(dest_data, 0b11111111, sizeof(dest_data));
bitblt_bitmap_into_bitmap(&dest_bitmap, src_bitmap, (GPoint){0}, GCompOpTint, GColorWhite);
cl_check(gbitmap_pbi_eq(&dest_bitmap, "test_bitblt__1bit_to_1bit_comptint_white_on_white-expect.1bit.pbi"));
memset(dest_data, 0b11111111, sizeof(dest_data));
bitblt_bitmap_into_bitmap(&dest_bitmap, src_bitmap, (GPoint){0}, GCompOpTint, GColorBlack);
cl_check(gbitmap_pbi_eq(&dest_bitmap, "test_bitblt__1bit_to_1bit_comptint_black_on_white-expect.1bit.pbi"));
// Destination Black
memset(dest_data, 0b00000000, sizeof(dest_data));
bitblt_bitmap_into_bitmap(&dest_bitmap, src_bitmap, (GPoint){0}, GCompOpTint, GColorClear);
cl_check(gbitmap_pbi_eq(&dest_bitmap, "test_bitblt__1bit_to_1bit_comptint_black_on_black-expect.1bit.pbi"));
memset(dest_data, 0b00000000, sizeof(dest_data));
bitblt_bitmap_into_bitmap(&dest_bitmap, src_bitmap, (GPoint){0}, GCompOpTint, GColorWhite);
cl_check(gbitmap_pbi_eq(&dest_bitmap, "test_bitblt__1bit_to_1bit_comptint_white_on_black-expect.1bit.pbi"));
memset(dest_data, 0b00000000, sizeof(dest_data));
bitblt_bitmap_into_bitmap(&dest_bitmap, src_bitmap, (GPoint){0}, GCompOpTint, GColorBlack);
cl_check(gbitmap_pbi_eq(&dest_bitmap, "test_bitblt__1bit_to_1bit_comptint_black_on_black-expect.1bit.pbi"));
gbitmap_destroy(src_bitmap);
}
// Test:
// - source origin offset
// - source bounds origin and size
// - Source bounds origin is beyond (32, y) to pass word boundary.
// - Source origin offset is beyond (32, y) to pass another word boundary.
// - wrapping into larger destination
// - dest rect not at { 0, 0 }
// Setup:
// - Dest rect at {4, 4}, repeat twice and a bit: {140, 55}
// - Source bounds origin at {37, 3), size {63, 23}
// - Source origin offset (39, 11)
void DISABLED_test_bitblt__1bit_to_8bit_bounds_and_origin_offset(void) {
GBitmap *src_bitmap =
get_gbitmap_from_pbi("test_bitblt__1bit_to_8bit_bounds_and_origin_offset.1bit.pbi");
uint8_t dest_data[144*168];
GBitmap dest_bitmap = {
.addr = dest_data,
.row_size_bytes = 144,
.info.format = GBitmapFormat8Bit,
.info.version = GBITMAP_VERSION_CURRENT,
.bounds = { { 0, 0 }, { 144, 168 } }
};
memset(dest_data, GColorGreen.argb, sizeof(dest_data));
const GRect dest_rect = GRect(4, 4, 140, 55);
const GPoint src_origin_offset = GPoint(39, 11);
src_bitmap->bounds = (GRect) { { 37, 3}, { 63, 23 } };
bitblt_bitmap_into_bitmap_tiled_1bit_to_8bit(&dest_bitmap, src_bitmap, dest_rect,
src_origin_offset, GCompOpAssign, GColorWhite);
cl_assert(gbitmap_pbi_eq(&dest_bitmap,
"test_bitblt__1bit_to_8bit_bounds_and_origin_offset-expect.8bit.pbi"));
gbitmap_destroy(src_bitmap);
}
// This test is the same as test_bitblt__1bit_to_1bit_bounds_and_origin_offset, except it is for
// 1-bit to 1-bit.
// FIXME: This is a known legacy-broken case in 1-bit bitblt. See PBL-14671 for more information
void DISABLED__test_bitblt__1bit_to_1bit_bounds_and_origin_offset(void) {
GBitmap *src_bitmap =
get_gbitmap_from_pbi("test_bitblt__1bit_to_1bit_bounds_and_origin_offset.1bit.pbi");
uint8_t dest_data[src_bitmap->row_size_bytes * src_bitmap->bounds.size.h];
GBitmap dest_bitmap = *src_bitmap;
dest_bitmap.addr = dest_data;
dest_bitmap.bounds = src_bitmap->bounds;
memset(dest_data, 0xff, sizeof(dest_data));
const GRect dest_rect = GRect(4, 4, 140, 55);
const GPoint src_origin_offset = GPoint(39, 11);
src_bitmap->bounds = (GRect) { { 37, 3}, { 63, 23 } };
bitblt_bitmap_into_bitmap_tiled_1bit_to_1bit(&dest_bitmap, src_bitmap, dest_rect,
src_origin_offset, GCompOpAssign, GColorWhite);
cl_assert(gbitmap_pbi_eq(&dest_bitmap,
"test_bitblt__1bit_to_1bit_bounds_and_origin_offset-expect.8bit.pbi"));
gbitmap_destroy(src_bitmap);
}
+200
View File
@@ -0,0 +1,200 @@
/*
* 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/graphics/gtypes.h"
#include "applib/graphics/bitblt.h"
#include "clar.h"
#include "util.h"
#include <string.h>
#include <stdio.h>
// Stubs
////////////////////////////////////
#include "graphics_common_stubs.h"
#include "stubs_applib_resource.h"
// Helper Functions
////////////////////////////////////
#include "test_graphics.h"
#if SCREEN_COLOR_DEPTH_BITS == 8
#include "8bit/test_framebuffer.h"
#else
Static_assert(false, "These tests are only for color displays");
#endif
static GContext *ctx;
static FrameBuffer *fb = NULL;
// Setup
void test_bitblt_circular__initialize(void) {
fb = malloc(sizeof(FrameBuffer));
framebuffer_init(fb, &(GSize) {DISP_COLS, DISP_ROWS});
ctx = malloc(sizeof(GContext));
test_graphics_context_init(ctx, fb);
framebuffer_clear(fb);
}
// Teardown
void test_bitblt_circular__cleanup(void) {
free(fb);
free(ctx);
}
// Tests
////////////////////////////////////
// Reference PNGs reside in "tests/test_images/"
// and are created at build time, with the test PBI file generated
// by bitmapgen.py from the reference PNG copied to TEST_IMAGES_PATH
// covers 1-bit b&w image
// covers 1,2,4,8 bit palettized
// Tests 1-bit black&white PBI loading into gbitmap
// Result:
// - gbitmap matches platform loaded PBI
void test_bitblt_circular__color_1_bit_bw(void) {
const char test_file[] = "test_bitblt_circular__color_1_bit_bw.1bit.pbi";
GBitmap *bitmap = setup_pbi_test(test_file);
GBitmap *native_framebuffer = graphics_context_get_bitmap(ctx);
native_framebuffer->info.format = GBitmapFormat8BitCircular;
cl_assert(DISPLAY_FRAMEBUFFER_BYTES == 25944);
bitblt_bitmap_into_bitmap(native_framebuffer, bitmap, GPointZero, GCompOpAssign, GColorWhite);
cl_assert(gbitmap_pbi_eq(native_framebuffer, test_file));
}
// Tests 1-bit red&white palettized PBI loading into gbitmap
// Result:
// - gbitmap matches platform loaded PBI
void test_bitblt_circular__color_1_bit(void) {
GBitmap *bitmap = setup_png_test(TEST_PNG_FILE);
GBitmap *native_framebuffer = graphics_context_get_bitmap(ctx);
cl_assert(native_framebuffer->info.format == GBitmapFormat8BitCircular);
cl_assert(DISPLAY_FRAMEBUFFER_BYTES == 25944);
bitblt_bitmap_into_bitmap(native_framebuffer, bitmap, GPointZero, GCompOpAssign, GColorWhite);
cl_assert(gbitmap_pbi_eq(native_framebuffer, TEST_PBI_FILE));
}
// Tests 2-bit palettized PBI loading into gbitmap
// Result:
// - gbitmap matches platform loaded PBI
void test_bitblt_circular__color_2_bit(void) {
GBitmap *bitmap = setup_png_test(TEST_PNG_FILE);
GBitmap *native_framebuffer = graphics_context_get_bitmap(ctx);
cl_assert(native_framebuffer->info.format == GBitmapFormat8BitCircular);
cl_assert(DISPLAY_FRAMEBUFFER_BYTES == 25944);
bitblt_bitmap_into_bitmap(native_framebuffer, bitmap, GPointZero, GCompOpAssign, GColorWhite);
cl_assert(gbitmap_pbi_eq(native_framebuffer, TEST_PBI_FILE));
}
// Tests 4-bit bitblt palettized to circular display buffer
// Result:
// - gbitmap matches platform loaded PBI
void test_bitblt_circular__color_4_bit(void) {
GBitmap *bitmap = setup_png_test(TEST_PNG_FILE);
GBitmap *native_framebuffer = graphics_context_get_bitmap(ctx);
cl_assert(native_framebuffer->info.format == GBitmapFormat8BitCircular);
cl_assert(DISPLAY_FRAMEBUFFER_BYTES == 25944);
bitblt_bitmap_into_bitmap(native_framebuffer, bitmap, GPointZero, GCompOpAssign, GColorWhite);
cl_assert(gbitmap_pbi_eq(native_framebuffer, TEST_PBI_FILE));
}
// Tests 8-bit bitblt to circular display buffer
// Result:
// - gbitmap matches platform loaded PBI
void test_bitblt_circular__color_8_bit(void) {
GBitmap *bitmap = setup_png_test(TEST_PNG_FILE);
GBitmap *native_framebuffer = graphics_context_get_bitmap(ctx);
cl_assert(native_framebuffer->info.format == GBitmapFormat8BitCircular);
cl_assert(DISPLAY_FRAMEBUFFER_BYTES == 25944);
bitblt_bitmap_into_bitmap(native_framebuffer, bitmap, GPointZero, GCompOpAssign, GColorWhite);
cl_assert(gbitmap_pbi_eq(native_framebuffer, TEST_PBI_FILE));
}
// Tests 8-bit bitblt tiling support to circular display buffer
// Result:
// - gbitmap matches platform loaded PBI
void test_bitblt_circular__color_8_bit_tiling(void) {
GBitmap *bitmap = setup_png_test(TEST_NAMED_PNG_FILE("test_bitblt_circular__tile"));
cl_assert(bitmap->info.format == GBitmapFormat8Bit);
GBitmap *native_framebuffer = graphics_context_get_bitmap(ctx);
cl_assert(native_framebuffer->info.format == GBitmapFormat8BitCircular);
cl_assert(DISPLAY_FRAMEBUFFER_BYTES == 25944);
bitblt_bitmap_into_bitmap_tiled(
native_framebuffer, bitmap, native_framebuffer->bounds,
GPointZero, GCompOpAssign, GColorWhite);
cl_assert(gbitmap_pbi_eq(native_framebuffer, TEST_PBI_FILE));
}
// Tests palettized bitblt non-power-of-two tiling support to circular display buffer
// Result:
// - gbitmap matches platform loaded PBI
void test_bitblt_circular__color_8_bit_tiling_palettized(void) {
GBitmap *bitmap = setup_png_test(TEST_NAMED_PNG_FILE("test_bitblt_circular__tile_palettized"));
cl_assert(bitmap->info.format == GBitmapFormat2BitPalette);
GBitmap *native_framebuffer = graphics_context_get_bitmap(ctx);
cl_assert(native_framebuffer->info.format == GBitmapFormat8BitCircular);
cl_assert(DISPLAY_FRAMEBUFFER_BYTES == 25944);
bitblt_bitmap_into_bitmap_tiled(
native_framebuffer, bitmap, native_framebuffer->bounds,
GPointZero, GCompOpAssign, GColorWhite);
cl_assert(gbitmap_pbi_eq(native_framebuffer, TEST_PBI_FILE));
}
void test_bitblt_circular__8_bit_converted_circular(void) {
GBitmap *bitmap = setup_png_test(TEST_NAMED_PNG_FILE("test_bitblt_circular__spiral"));
cl_assert(bitmap->info.format == GBitmapFormat8Bit);
// Convert input PNG from rectangular to circular
gbitmap_8bit_to_8bit_circular(bitmap);
cl_assert(bitmap->info.format == GBitmapFormat8BitCircular);
GBitmap *native_framebuffer = graphics_context_get_bitmap(ctx);
GRect framebuffer_bounds = gbitmap_get_bounds(native_framebuffer);
cl_assert(native_framebuffer->info.format == GBitmapFormat8BitCircular);
cl_assert(DISPLAY_FRAMEBUFFER_BYTES == 25944);
// Set screen to black to match empty region with test image
graphics_context_set_fill_color(ctx, GColorBlack);
graphics_fill_rect(ctx, &framebuffer_bounds);
bitblt_bitmap_into_bitmap(native_framebuffer, bitmap, GPointZero, GCompOpAssign, GColorWhite);
cl_assert(gbitmap_pbi_eq(native_framebuffer, TEST_PBI_FILE));
}
void test_bitblt_circular__8_bit_converted_circular_offset(void) {
GBitmap *bitmap = setup_png_test(TEST_NAMED_PNG_FILE("test_bitblt_circular__spiral"));
cl_assert(bitmap->info.format == GBitmapFormat8Bit);
// Convert input PNG from rectangular to circular
gbitmap_8bit_to_8bit_circular(bitmap);
cl_assert(bitmap->info.format == GBitmapFormat8BitCircular);
GBitmap *native_framebuffer = graphics_context_get_bitmap(ctx);
GRect framebuffer_bounds = gbitmap_get_bounds(native_framebuffer);
cl_assert(native_framebuffer->info.format == GBitmapFormat8BitCircular);
cl_assert(DISPLAY_FRAMEBUFFER_BYTES == 25944);
// Set screen to black to match empty region with test image
graphics_context_set_fill_color(ctx, GColorBlack);
graphics_fill_rect(ctx, &framebuffer_bounds);
// Shift the output by non-power-of-2 values for testing
bitblt_bitmap_into_bitmap(native_framebuffer, bitmap, GPoint(33, 10), GCompOpAssign, GColorWhite);
cl_assert(gbitmap_pbi_eq(native_framebuffer, TEST_PBI_FILE));
}
+361
View File
@@ -0,0 +1,361 @@
/*
* 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/graphics/graphics.h"
#include "applib/graphics/bitblt.h"
#include "applib/graphics/bitblt_private.h"
#include "applib/graphics/8_bit/framebuffer.h"
#include "clar.h"
#include <string.h>
// Stubs
////////////////////////////////////
#include "graphics_common_stubs.h"
#include "stubs_applib_resource.h"
#include "stubs_compiled_with_legacy2_sdk.h"
#include "test_graphics.h"
// Setup
////////////////////////////////////
static GContext ctx;
static FrameBuffer framebuffer;
static uint8_t dest_bitmap_data[FRAMEBUFFER_SIZE_BYTES];
static GBitmap dest_bitmap = {
.addr = dest_bitmap_data,
.row_size_bytes = FRAMEBUFFER_BYTES_PER_ROW,
.info.is_bitmap_heap_allocated = false,
.info.format = GBitmapFormat8Bit,
.info.version = GBITMAP_VERSION_CURRENT,
.bounds = {
.size = {
.w = DISP_COLS,
.h = DISP_ROWS
},
.origin = { 0, 0 },
},
};
// Utilities
////////////////////////////////////
#if SCREEN_COLOR_DEPTH_BITS == 8
extern GColor get_bitmap_color(GBitmap *bmp, int x, int y);
#endif
// @param color_index the color index (the value) to put into the bitmap at (x, y).
// @param bpp Bits per pixel.
// @para line_stride how many bytes per line in the bitmap data.
void packed_pixel_set(uint8_t *buf, uint8_t color_index, int16_t x, int16_t y,
uint8_t bpp, int16_t line_stride) {
const uint8_t ppb = 8 / bpp;
uint8_t idx = y*line_stride + (x/(ppb));
const uint8_t shift = (8 - bpp) - bpp * (x % ppb);
const uint8_t mask = ~(((1 << bpp) - 1) << shift);
buf[idx] = buf[idx] & mask;
buf[idx] |= ((color_index & ((1 << bpp) - 1)) << shift);
}
static bool prv_check_source_stripe_blit(const uint8_t *data,
const GBitmap *src_bmp, GColor surround_color) {
for (uint8_t y = 0; y < (FRAMEBUFFER_SIZE_BYTES / FRAMEBUFFER_BYTES_PER_ROW); ++y) {
for (uint8_t x = 0; x < FRAMEBUFFER_BYTES_PER_ROW; ++x) {
uint8_t color = data[y*FRAMEBUFFER_BYTES_PER_ROW + x];
if (y < src_bmp->bounds.size.h && x < src_bmp->bounds.size.w) {
if (color != src_bmp->palette[x].argb) {
return false;
}
} else {
if (color != surround_color.argb) {
return false;
}
}
}
}
return true;
}
// Tests
////////////////////////////////////
// setup and teardown
void test_bitblt_palette__initialize(void) {
framebuffer_init(&framebuffer, &(GSize) { DISP_COLS, DISP_ROWS });
test_graphics_context_init(&ctx, &framebuffer);
}
void test_bitblt_palette__cleanup(void) {
}
void test_bitblt_palette__1Bit_color(void) {
const int BITS_PER_PIXEL = 1;
const int PIXELS_PER_BYTE = (8 / BITS_PER_PIXEL);
const int WIDTH = 2;
const int HEIGHT = 2;
const int ROW_STRIDE = (WIDTH + (PIXELS_PER_BYTE - 1)) / PIXELS_PER_BYTE;
uint8_t s_data[ROW_STRIDE * HEIGHT];
GColor s_palette[1 << 1] = {
GColorMelon, GColorIcterine
};
cl_assert(sizeof(s_palette) == (1 << BITS_PER_PIXEL));
GBitmap s_bmp = (GBitmap) {
.addr = s_data,
.row_size_bytes = ROW_STRIDE,
.info.format = GBitmapFormat1BitPalette,
.info.version = GBITMAP_VERSION_CURRENT,
.bounds = { .size = { WIDTH, HEIGHT } },
.palette = s_palette,
};
memset(s_data, 0, sizeof(s_data));
for (int y = 0; y < HEIGHT; ++y) {
for (int x = 0; x < WIDTH; ++x) {
packed_pixel_set(s_data, x /* color_index */, x, y, BITS_PER_PIXEL, s_bmp.row_size_bytes);
}
}
memset(dest_bitmap_data, GColorWhite.argb, sizeof(dest_bitmap_data));
#if SCREEN_COLOR_DEPTH_BITS == 8
char print_buf[20];
for (int y = 0; y < HEIGHT; ++y) {
for (int x = 0; x < WIDTH; ++x) {
snprintf(print_buf, sizeof(print_buf), "Failed index = %d, %d", x, y);
cl_check_(gcolor_equal(get_bitmap_color(&s_bmp, x, y), s_palette[x]), print_buf);
cl_check_(gcolor_equal(get_bitmap_color(&s_bmp, x, y), s_palette[x]), print_buf);
}
}
#endif
}
void test_bitblt_palette__4Bit_assign(void) {
const int BITS_PER_PIXEL = 4;
const int PIXELS_PER_BYTE = (8 / BITS_PER_PIXEL);
const int WIDTH = 16;
const int HEIGHT = 16;
const int ROW_STRIDE = (WIDTH + (PIXELS_PER_BYTE - 1)) / PIXELS_PER_BYTE;
uint8_t s_data[ROW_STRIDE * HEIGHT];
GColor s_palette[1 << 4] = {
GColorMelon, GColorIcterine, GColorYellow, GColorSunsetOrange,
GColorScreaminGreen, GColorMagenta, GColorOrange, GColorFolly,
GColorLimerick, GColorPictonBlue, GColorPurple, GColorCadetBlue,
GColorMalachite, GColorGreen, GColorIndigo, GColorVividCerulean
};
cl_assert(sizeof(s_palette) == (1 << BITS_PER_PIXEL));
GBitmap s_bmp = (GBitmap) {
.addr = s_data,
.row_size_bytes = ROW_STRIDE,
.info.format = GBitmapFormat4BitPalette,
.info.version = GBITMAP_VERSION_CURRENT,
.bounds = { .size = { WIDTH, HEIGHT } },
.palette = s_palette,
};
memset(s_data, 0, sizeof(s_data));
for (int y = 0; y < HEIGHT; ++y) {
for (int x = 0; x < WIDTH; ++x) {
packed_pixel_set(s_data, x /* color_index */, x, y, BITS_PER_PIXEL, s_bmp.row_size_bytes);
}
}
memset(dest_bitmap_data, GColorWhite.argb, sizeof(dest_bitmap_data));
#if SCREEN_COLOR_DEPTH_BITS == 8
char print_buf[20];
for (int y = 0; y < HEIGHT; ++y) {
for (int x = 0; x < WIDTH; ++x) {
snprintf(print_buf, sizeof(print_buf), "Failed index = %d, %d", x, y);
cl_check_(gcolor_equal(get_bitmap_color(&s_bmp, x, y), s_palette[x]), print_buf);
cl_check_(gcolor_equal(get_bitmap_color(&s_bmp, x, y), s_palette[x]), print_buf);
}
}
#endif
bitblt_bitmap_into_bitmap(&dest_bitmap, &s_bmp, GPointZero, GCompOpAssign, GColorWhite);
cl_assert(prv_check_source_stripe_blit(dest_bitmap_data, &s_bmp, GColorWhite));
}
static void prv_opaque_2bit_simple(GCompOp compositing_mode) {
const int BITS_PER_PIXEL = 2;
const int PIXELS_PER_BYTE = (8 / BITS_PER_PIXEL);
const int WIDTH = 4;
const int HEIGHT = 4;
const int ROW_STRIDE = (WIDTH + (PIXELS_PER_BYTE - 1)) / PIXELS_PER_BYTE;
uint8_t s_data[ROW_STRIDE * HEIGHT];
GColor s_palette[] = {
GColorRed, GColorWhite, GColorBlack, GColorBlue
};
cl_assert(sizeof(s_palette) == (1 << BITS_PER_PIXEL));
GBitmap s_bmp = (GBitmap) {
.addr = s_data,
.row_size_bytes = ROW_STRIDE,
.info.format = GBitmapFormat2BitPalette,
.info.version = GBITMAP_VERSION_CURRENT,
.bounds = { .size = { WIDTH, HEIGHT } },
.palette = s_palette,
};
memset(s_data, 0, sizeof(s_data));
for (int y = 0; y < HEIGHT; ++y) {
for (int x = 0; x < WIDTH; ++x) {
packed_pixel_set(s_data, x /* color_index */, x, y, BITS_PER_PIXEL, s_bmp.row_size_bytes);
}
}
memset(dest_bitmap_data, GColorWhite.argb, sizeof(dest_bitmap_data));
#if SCREEN_COLOR_DEPTH_BITS == 8
char print_buf[20];
for (int y = 0; y < HEIGHT; ++y) {
for (int x = 0; x < WIDTH; ++x) {
snprintf(print_buf, sizeof(print_buf), "Failed index = %d, %d", x, y);
cl_check_(gcolor_equal(get_bitmap_color(&s_bmp, x, y), s_palette[x]), print_buf);
cl_check_(gcolor_equal(get_bitmap_color(&s_bmp, x, y), s_palette[x]), print_buf);
}
}
#endif
bitblt_bitmap_into_bitmap(&dest_bitmap, &s_bmp, GPointZero, compositing_mode, GColorWhite);
switch (compositing_mode) {
case GCompOpTint: {
// Since this is opaque, the tint_color will be used for the RGB value of the destination
// image, so we want to use that as our palette.
for (uint8_t idx = 0; idx < WIDTH; idx++) {
if (s_palette[idx].a == 3) {
s_palette[idx] = GColorWhite;
}
}
break;
}
default:
break;
}
cl_assert(prv_check_source_stripe_blit(dest_bitmap_data, &s_bmp, GColorWhite));
}
static void prv_4bit_simple(GCompOp compositing_mode, GColor color, bool transparent) {
const int BITS_PER_PIXEL = 4;
const int PIXELS_PER_BYTE = (8 / BITS_PER_PIXEL);
const int WIDTH = 16;
const int HEIGHT = 16;
const int ROW_STRIDE = (WIDTH + (PIXELS_PER_BYTE - 1)) / PIXELS_PER_BYTE;
uint8_t s_data[ROW_STRIDE * HEIGHT];
GColor s_palette[] = {
GColorMelon, GColorIcterine, GColorYellow, GColorSunsetOrange,
GColorScreaminGreen, GColorMagenta, GColorOrange, GColorFolly,
GColorLimerick, GColorPictonBlue, GColorPurple, GColorCadetBlue,
GColorMalachite, GColorGreen, GColorIndigo, GColorVividCerulean
};
cl_assert(sizeof(s_palette) == (1 << BITS_PER_PIXEL));
if (transparent) {
// Make all of those colors fully transparent
for (int i = 0; i < sizeof(s_palette); ++i) {
s_palette[i].a = 0;
}
}
GBitmap s_bmp = (GBitmap) {
.addr = s_data,
.row_size_bytes = ROW_STRIDE,
.info.format = GBitmapFormat4BitPalette,
.info.version = GBITMAP_VERSION_CURRENT,
.bounds = { .size = { WIDTH, HEIGHT } },
.palette = s_palette,
};
memset(s_data, 0, sizeof(s_data));
for (int y = 0; y < HEIGHT; ++y) {
for (int x = 0; x < WIDTH; ++x) {
packed_pixel_set(s_data, x /* color_index */, x, y, BITS_PER_PIXEL, s_bmp.row_size_bytes);
}
}
memset(dest_bitmap_data, GColorWhite.argb, sizeof(dest_bitmap_data));
bitblt_bitmap_into_bitmap(&dest_bitmap, &s_bmp, GPointZero, compositing_mode, color);
switch (compositing_mode) {
case GCompOpSet: {
if (transparent) {
memset(s_palette, color.argb, sizeof(s_palette));
}
break;
}
case GCompOpAssign:
break;
case GCompOpTint: {
for (uint8_t idx = 0; idx < WIDTH; idx++) {
if (s_palette[idx].a == 3 || transparent) {
s_palette[idx] = color;
}
}
break;
}
default:
break;
}
cl_assert(prv_check_source_stripe_blit(dest_bitmap_data, &s_bmp, GColorWhite));
}
void test_bitblt_palette__2Bit_assign_opaque(void) {
prv_opaque_2bit_simple(GCompOpAssign);
}
void test_bitblt_palette__2Bit_set_opaque(void) {
prv_opaque_2bit_simple(GCompOpSet);
}
void test_bitblt_palette__2Bit_comptint_opaque(void) {
prv_opaque_2bit_simple(GCompOpTint);
}
void test_bitblt_palette__4Bit_assign_opaque(void) {
prv_4bit_simple(GCompOpAssign, GColorWhite, false /* opaque */);
}
void test_bitblt_palette__4Bit_assign_transparent(void) {
prv_4bit_simple(GCompOpAssign, GColorWhite, true /* transparent */);
}
void test_bitblt_palette__4Bit_set_opaque(void) {
prv_4bit_simple(GCompOpSet, GColorWhite, false /* opaque */);
}
void test_bitblt_palette__4Bit_set_transparent(void) {
prv_4bit_simple(GCompOpSet, GColorWhite, true /* transparent */);
}
void test_bitblt_palette__4Bit_comptint_opaque(void) {
prv_4bit_simple(GCompOpTint, GColorBlack, false /* opaque */);
prv_4bit_simple(GCompOpTint, GColorBlue, false /* opaque */);
}
void test_bitblt_palette__4Bit_comptint_transparent(void) {
prv_4bit_simple(GCompOpTint, GColorWhite, true /* transparent */);
}
@@ -0,0 +1,212 @@
/*
* 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/graphics/graphics.h"
#include "applib/graphics/graphics_private.h"
#include "applib/graphics/bitblt.h"
#include "applib/graphics/bitblt_private.h"
#include "applib/graphics/1_bit/framebuffer.h"
#include "applib/graphics/gtypes.h"
#include "clar.h"
#include "util.h"
#include <string.h>
#include <stdio.h>
// Stubs
////////////////////////////////////
#include "graphics_common_stubs.h"
#include "stubs_applib_resource.h"
#include "test_graphics.h"
extern void prv_apply_tint_color(GColor *color, GColor tint_color);
static uint8_t s_dest_data[100 * 100];
static GBitmap s_dest_bitmap = {
.addr = s_dest_data,
.row_size_bytes = 16,
.info.format = GBitmapFormat1Bit,
.info.version = GBITMAP_VERSION_CURRENT,
.bounds = GRect(0, 0, 100, 100)
};
// Tests
////////////////////////////////////
// setup and teardown
void test_bitblt_palette_1bit__initialize(void) {
// Set dest bitmap to white
memset(s_dest_data, 0b11111111, sizeof(s_dest_data));
}
void test_bitblt_palette_1bit__cleanup(void) {
}
// Test images reside in "tests/fw/graphics/test_images/".
// The wscript will convert them from PNGs in that directory to PBIs in the build directory.
// Tests assign, from same size to same size.
// Setup:
// - Source is 50x50, the left half is semi transparent orange and the right half is orange.
// - Dest is 100x100, white.
// Result:
// - All dithered gray.
void test_bitblt_palette_1bit__1bit_palette_to_1bit_assign(void) {
GBitmap *src_bitmap =
get_gbitmap_from_pbi("test_bitblt_palette_1bit__1bit_palette_to_1bit.pbi");
bitblt_bitmap_into_bitmap(&s_dest_bitmap, src_bitmap, GPointZero, GCompOpAssign, GColorWhite);
cl_assert(gbitmap_pbi_eq(&s_dest_bitmap,
"test_bitblt_palette_1bit__1bit_palette_to_1bit_assign-expect.pbi"));
gbitmap_destroy(src_bitmap);
}
// Test images reside in "tests/fw/graphics/test_images/".
// The wscript will convert them from PNGs in that directory to PBIs in the build directory.
// Tests assign, from same size to same size.
// Setup:
// - Source is 50x50, the left half is semi transparent orange and the right half is orange.
// - Dest is 100x100, white.
// Result:
// - The left half will be white and the right half will be dithered gray.
void test_bitblt_palette_1bit__1bit_palette_to_1bit_set(void) {
GBitmap *src_bitmap =
get_gbitmap_from_pbi("test_bitblt_palette_1bit__1bit_palette_to_1bit.pbi");
bitblt_bitmap_into_bitmap(&s_dest_bitmap, src_bitmap, GPointZero, GCompOpSet, GColorWhite);
cl_assert(gbitmap_pbi_eq(&s_dest_bitmap,
"test_bitblt_palette_1bit__1bit_palette_to_1bit_set-expect.pbi"));
gbitmap_destroy(src_bitmap);
}
// Tests assign, from same size to same size.
// Setup:
// - Source is 50x50, alternating lines between orange and blue for the top half.
// The bottom half is a diagonal orange line over blue.
// The left half is semi-transparent and the right half is completely opaque
// - Dest is 100x100, white.
// Result:
// - The top half will be alternating between dithered gray and black lines
// The bottom half consists of a diagonal white line on a black background
void test_bitblt_palette_1bit__2bit_palette_to_1bit_assign(void) {
GBitmap *src_bitmap =
get_gbitmap_from_pbi("test_bitblt_palette_1bit__2bit_palette_to_1bit.pbi");
bitblt_bitmap_into_bitmap(&s_dest_bitmap, src_bitmap, GPointZero, GCompOpAssign, GColorWhite);
cl_assert(gbitmap_pbi_eq(&s_dest_bitmap,
"test_bitblt_palette_1bit__2bit_palette_to_1bit-expect.pbi"));
gbitmap_destroy(src_bitmap);
}
// Tests assign, from same size to same size.
// Setup:
// - Source is 50x50, alternating lines between orange and blue for the top half.
// The bottom half is a diagonal orange line over blue.
// The left half is semi-transparent and the right half is completely opaque
// - Dest is 100x100, white.
// Result:
// - The top right half will be alternating between dithered gray and black lines
// The bottom right half consists of a diagonal white line on a black background
// The left half will be completely white
void test_bitblt_palette_1bit__2bit_palette_to_1bit_set(void) {
GBitmap *src_bitmap =
get_gbitmap_from_pbi("test_bitblt_palette_1bit__2bit_palette_to_1bit.pbi");
bitblt_bitmap_into_bitmap(&s_dest_bitmap, src_bitmap, GPointZero, GCompOpSet, GColorWhite);
cl_assert(gbitmap_pbi_eq(&s_dest_bitmap,
"test_bitblt_palette_1bit__2bit_palette_to_1bit_set-expect.pbi"));
gbitmap_destroy(src_bitmap);
}
// Tests assign, from same size to same size.
// Setup:
// - Source is 50x50, alternating lines between orange and blue for the top half.
// The bottom half is a diagonal orange line over blue.
// The left half is semi-transparent and the right half is completely opaque
// - Dest is 50x50, white.
// Result:
// - The image desribed will be tiled in each of the four corners
// The top right half will be alternating between dithered gray and black lines
// The bottom right half consists of a diagonal white line on a black background
// The left half will be completely white
void test_bitblt_palette_1bit__2bit_palette_to_1bit_wrap(void) {
GBitmap *src_bitmap =
get_gbitmap_from_pbi("test_bitblt_palette_1bit__2bit_palette_to_1bit.pbi");
bitblt_bitmap_into_bitmap_tiled(&s_dest_bitmap, src_bitmap, s_dest_bitmap.bounds, GPointZero, GCompOpAssign, GColorWhite);
cl_assert(gbitmap_pbi_eq(&s_dest_bitmap,
"test_bitblt_palette_1bit__2bit_palette_to_1bit_wrap-expect.pbi"));
gbitmap_destroy(src_bitmap);
}
// Tests assign, from same size to same size.
// Setup:
// - Source is 50x50, alternating lines between orange and blue for the top half.
// The bottom half is a diagonal orange line over blue.
// The left half is semi-transparent and the right half is completely opaque
// - Dest is 100x100, white.
// Result:
// - The image described below will be at an offset of 20, 20
// The top right half will be alternating between dithered gray and black lines
// The bottom right half consists of a diagonal white line on a black background
// The left half will be completely white
void test_bitblt_palette_1bit__2bit_palette_to_1bit_offest(void) {
GBitmap *src_bitmap =
get_gbitmap_from_pbi("test_bitblt_palette_1bit__2bit_palette_to_1bit.pbi");
bitblt_bitmap_into_bitmap(&s_dest_bitmap, src_bitmap, GPoint(20,20), GCompOpAssign, GColorWhite);
cl_assert(gbitmap_pbi_eq(&s_dest_bitmap,
"test_bitblt_palette_1bit__2bit_palette_to_1bit_offest-expect.pbi"));
gbitmap_destroy(src_bitmap);
}
void test_bitblt_palette_1bit__get_1bit_graphics_grayscale_pattern(void) {
cl_assert_equal_i(graphics_private_get_1bit_grayscale_pattern(GColorWhite, 0), 0xFFFFFFFF);
cl_assert_equal_i(graphics_private_get_1bit_grayscale_pattern(GColorWhite, 1), 0xFFFFFFFF);
cl_assert_equal_i(graphics_private_get_1bit_grayscale_pattern(GColorLightGray, 0), 0x55555555);
cl_assert_equal_i(graphics_private_get_1bit_grayscale_pattern(GColorLightGray, 1), 0xAAAAAAAA);
cl_assert_equal_i(graphics_private_get_1bit_grayscale_pattern(GColorDarkGray, 0), 0x55555555);
cl_assert_equal_i(graphics_private_get_1bit_grayscale_pattern(GColorDarkGray, 1), 0xAAAAAAAA);
cl_assert_equal_i(graphics_private_get_1bit_grayscale_pattern(GColorBlack, 0), 0x00000000);
cl_assert_equal_i(graphics_private_get_1bit_grayscale_pattern(GColorBlack, 1), 0x00000000);
}
void test_bitblt_palette_1bit__prv_apply_tint_color(void) {
GColor color = GColorBlack;
GColor tinted_color = GColorBlack;
prv_apply_tint_color(&color, GColorClear);
cl_assert_equal_i(color.argb, tinted_color.argb);
tinted_color = GColorRed;
prv_apply_tint_color(&color, GColorRed);
cl_assert_equal_i(color.argb, tinted_color.argb);
tinted_color.a = 2;
color.a = 2;
prv_apply_tint_color(&color, GColorRed);
cl_assert_equal_i(color.argb, tinted_color.argb);
tinted_color.a = 1;
color.a = 1;
prv_apply_tint_color(&color, GColorRed);
cl_assert_equal_i(color.argb, tinted_color.argb);
}
+150
View File
@@ -0,0 +1,150 @@
/*
* 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/graphics/graphics.h"
#include "applib/graphics/framebuffer.h"
#include "applib/graphics/bitblt_private.h"
#include "applib/ui/window_private.h"
#include "applib/ui/layer.h"
#include "util/math.h"
#include "util.h"
#include "clar.h"
#include <stdio.h>
// Helper Functions
////////////////////////////////////
#include "test_graphics.h"
#include "${BIT_DEPTH_NAME}/test_framebuffer.h"
// Stubs
////////////////////////////////////
#include "graphics_common_stubs.h"
#include "stubs_applib_resource.h"
// Tests
////////////////////////////////////
#define ORIGIN_RECT_NO_CLIP GRect(0, 0, 144, 168)
#define OFFSET_X 9
#define OFFSET_Y 20
#define STRIPE_WIDTH 64
#define STRIPE_HEIGHT 1
void prv_test_plot_horizontal_line(GBitmap *framebuffer, GRect area, GColor color) {
int y = area.origin.y;
int x0 = area.origin.x;
int x1 = x0 + area.size.w;
if (y >= framebuffer->bounds.size.h || y < 0) {
return;
}
int16_t x_min = MAX(MIN(x0, x1), framebuffer->bounds.origin.x);
int16_t x_max = MIN(MAX(x0, x1), framebuffer->bounds.origin.x + framebuffer->bounds.size.w);
for (int i=x_min; i < x_max; i++) {
#if SCREEN_COLOR_DEPTH_BITS == 8
GColor *output = (GColor*)framebuffer->addr + y * framebuffer->row_size_bytes + i;
// src_col, dest_col
output->argb = gcolor_alpha_blend(color, (*output)).argb;
#endif // SCREEN_COLOR_DEPTH_BITS == 8
}
}
void test_blending_${BIT_DEPTH_NAME}__photoshop(void) {
GContext ctx;
FrameBuffer *fb = malloc(sizeof(FrameBuffer));
framebuffer_init(fb, &(GSize) { DISP_COLS, DISP_ROWS });
test_graphics_context_init(&ctx, fb);
GBitmap *background_0_100 = get_gbitmap_from_pbi("blendtest_0_100_backdrop.pbi");
GBitmap *background_33_66 = get_gbitmap_from_pbi("blendtest_33_66_backdrop.pbi");
setup_test_aa_sw(&ctx, fb, ORIGIN_RECT_NO_CLIP, ORIGIN_RECT_NO_CLIP, false, 1);
graphics_draw_bitmap_in_rect(&ctx, background_0_100, &ORIGIN_RECT_NO_CLIP);
#if SCREEN_COLOR_DEPTH_BITS == 8
// Sanity check
cl_check(gbitmap_pbi_eq(&ctx.dest_bitmap, "blendtest_0_100_backdrop.pbi"));
// 0% alpha channel blend test
uint8_t color = 0x0;
uint8_t alpha = 0x0;
setup_test_aa_sw(&ctx, fb, ORIGIN_RECT_NO_CLIP, ORIGIN_RECT_NO_CLIP, false, 1);
graphics_draw_bitmap_in_rect(&ctx, background_0_100, &ORIGIN_RECT_NO_CLIP);
for (int i=0; i < 64; i++) {
prv_test_plot_horizontal_line(&ctx.dest_bitmap,
GRect(OFFSET_X, OFFSET_Y + i, STRIPE_WIDTH, STRIPE_HEIGHT),
(GColor){.argb = color | alpha});
color++;
}
// 100% alpha channel blend test
color = 0x0;
alpha = 0xC0;
for (int i=64; i < 128; i++) {
prv_test_plot_horizontal_line(&ctx.dest_bitmap,
GRect(OFFSET_X, OFFSET_Y + i, STRIPE_WIDTH, STRIPE_HEIGHT),
(GColor){.argb = color | alpha});
color++;
}
// Compare the results
cl_check(gbitmap_pbi_eq(&ctx.dest_bitmap, "blendtest_0_100.pbi"));
// Reset canvas
setup_test_aa_sw(&ctx, fb, ORIGIN_RECT_NO_CLIP, ORIGIN_RECT_NO_CLIP, false, 1);
graphics_draw_bitmap_in_rect(&ctx, background_33_66, &ORIGIN_RECT_NO_CLIP);
// 33% alpha channel blend test
color = 0x0;
alpha = 0x40;
for (int i=0; i < 64; i++) {
prv_test_plot_horizontal_line(&ctx.dest_bitmap,
GRect(OFFSET_X, OFFSET_Y + i, STRIPE_WIDTH, STRIPE_HEIGHT),
(GColor){.argb = color | alpha});
color++;
}
// 66% alpha channel blend test
color = 0x0;
alpha = 0x80;
for (int i=64; i < 128; i++) {
prv_test_plot_horizontal_line(&ctx.dest_bitmap,
GRect(OFFSET_X, OFFSET_Y + i, STRIPE_WIDTH, STRIPE_HEIGHT),
(GColor){.argb = color | alpha});
color++;
}
// Compare the results
cl_check(gbitmap_pbi_eq(&ctx.dest_bitmap, "blendtest_33_66.pbi"));
#endif
gbitmap_destroy(background_0_100);
gbitmap_destroy(background_33_66);
free(fb);
}
@@ -0,0 +1,59 @@
/*
* 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/graphics/graphics.h"
#include "applib/graphics/framebuffer.h"
#include "util/bitset.h"
#include "util/size.h"
#include "${BIT_DEPTH_NAME}/test_framebuffer.h"
#include "clar.h"
// Stubs
////////////////////////////////////
#include "graphics_common_stubs.h"
#include "stubs_applib_resource.h"
// Setup
////////////////////////////////////
static FrameBuffer framebuffer;
// Tests
////////////////////////////////////
void test_framebuffer_${BIT_DEPTH_NAME}__framebuffer_clear(void) {
// clear the framebuffer
framebuffer_init(&framebuffer, &(GSize) { DISP_COLS, DISP_ROWS });
framebuffer_clear(&framebuffer);
// check the clearing
#if SCREEN_COLOR_DEPTH_BITS == 1
for (int i = 0; i < ARRAY_LENGTH(framebuffer.buffer); i++) {
cl_assert(framebuffer.buffer[i] == 0xffffffff);
}
#elif SCREEN_COLOR_DEPTH_BITS == 8
for (int i = 0; i < ARRAY_LENGTH(framebuffer.buffer); i++) {
cl_assert(framebuffer.buffer[i] == GColorWhite.argb);
}
GRect expected_dirty = GRect(0, 0, DISP_COLS, DISP_ROWS);
#else
cl_assert(!memcmp(&framebuffer.dirty_rect, &expected_dirty, sizeof(GRect)));
cl_assert(false);
#endif
cl_assert(framebuffer.is_dirty == true);
}
@@ -0,0 +1,103 @@
/*
* 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/graphics/graphics.h"
#include "applib/graphics/graphics_private.h"
#include "applib/graphics/graphics_private_raw.h"
#include "applib/graphics/framebuffer.h"
#include "util/trig.h"
#include "applib/ui/window_private.h"
#include "applib/ui/layer.h"
#include "clar.h"
#include "util.h"
#include <stdio.h>
// Helper Functions
////////////////////////////////////
#include "test_graphics.h"
#include "8bit/test_framebuffer.h"
// Stubs
////////////////////////////////////
#include "graphics_common_stubs.h"
#include "stubs_applib_resource.h"
static FrameBuffer *fb = NULL;
#define CLIP_RECT_DRAW_BOX GRect(0, 0, DISP_COLS, DISP_ROWS)
#define CLIP_RECT_CLIP_BOX GRect(0, 0, DISP_COLS, DISP_ROWS)
// Setup and Teardown
////////////////////////////////////
// Setup
void test_framebuffer_clipping__initialize(void) {
fb = malloc(sizeof(FrameBuffer));
framebuffer_init(fb, &(GSize) { DISP_COLS, DISP_ROWS });
}
// Teardown
void test_framebuffer_clipping__cleanup(void) {
free(fb);
}
// Tests
////////////////////////////////////
void test_framebuffer_clipping__off_screen_left_aa_clipping(void) {
cl_assert_equal_i(GBITMAP_NATIVE_FORMAT, GBitmapFormat8BitCircular);
GContext ctx;
test_graphics_context_init(&ctx, fb);
setup_test_aa_sw(&ctx, fb, CLIP_RECT_CLIP_BOX, CLIP_RECT_DRAW_BOX, true, 1);
cl_assert_equal_i(ctx.dest_bitmap.info.format, GBitmapFormat8BitCircular);
memset(ctx.dest_bitmap.addr, GColorRedARGB8, FRAMEBUFFER_SIZE_BYTES);
GRect radial_container_rect = ctx.dest_bitmap.bounds;
radial_container_rect = grect_inset(radial_container_rect, GEdgeInsets(-10));
const uint16_t inset_thickness = radial_container_rect.size.w / 4;
graphics_context_set_fill_color(&ctx, GColorGreen);
graphics_fill_radial(&ctx, radial_container_rect, GOvalScaleModeFillCircle, inset_thickness,
TRIG_MAX_ANGLE / 2, TRIG_MAX_ANGLE);
cl_check(gbitmap_pbi_eq(&ctx.dest_bitmap, TEST_NAMED_PBI_FILE("off_screen_left_aa_clipping")));
};
void test_framebuffer_clipping__off_screen_right_aa_clipping(void) {
cl_assert_equal_i(GBITMAP_NATIVE_FORMAT, GBitmapFormat8BitCircular);
GContext ctx;
test_graphics_context_init(&ctx, fb);
setup_test_aa_sw(&ctx, fb, CLIP_RECT_CLIP_BOX, CLIP_RECT_DRAW_BOX, true, 1);
cl_assert_equal_i(ctx.dest_bitmap.info.format, GBitmapFormat8BitCircular);
memset(ctx.dest_bitmap.addr, GColorRedARGB8, FRAMEBUFFER_SIZE_BYTES);
GRect radial_container_rect = ctx.dest_bitmap.bounds;
radial_container_rect = grect_inset(radial_container_rect, GEdgeInsets(-10));
const uint16_t inset_thickness = radial_container_rect.size.w / 4;
graphics_context_set_fill_color(&ctx, GColorGreen);
graphics_fill_radial(&ctx, radial_container_rect, GOvalScaleModeFillCircle, inset_thickness, 0,
TRIG_MAX_ANGLE / 2);
cl_check(gbitmap_pbi_eq(&ctx.dest_bitmap, TEST_NAMED_PBI_FILE("off_screen_right_aa_clipping")));
};
+101
View File
@@ -0,0 +1,101 @@
/*
* 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/graphics/graphics.h"
#include "applib/graphics/framebuffer.h"
#include "applib/ui/window_private.h"
#include "applib/ui/layer.h"
#include "util/graphics.h"
#include "clar.h"
#include "util.h"
#include <stdio.h>
// Helper Functions
////////////////////////////////////
#include "test_graphics.h"
#include "8bit/test_framebuffer.h"
// Stubs
////////////////////////////////////
#include "graphics_common_stubs.h"
#include "stubs_applib_resource.h"
static FrameBuffer *fb = NULL;
#define COLOR(x) ((GColor){.argb=GColor ## x ## ARGB8})
#define NUM_COLORS 4
GColor color_table[NUM_COLORS] = {
COLOR(Red),
COLOR(Yellow),
COLOR(Cyan),
COLOR(Black),
};
// Setup
void test_framebuffer_duma__initialize(void) {
fb = malloc(sizeof(FrameBuffer));
framebuffer_init(fb, &(GSize) { DISP_COLS, DISP_ROWS });
}
// Teardown
void test_framebuffer_duma__cleanup(void) {
free(fb);
}
// Intentionally unchecked framebuffer drawing function
static void draw_fb_raw(uint8_t *buffer, int offset, GColor8 color) {
buffer[offset] = color.argb;
}
// Tests
////////////////////////////////////
void test_framebuffer_duma__draw_within_framebuffer(void) {
GContext ctx;
test_graphics_context_init(&ctx, fb);
// This test should only be running on color displays
cl_assert(ctx.dest_bitmap.info.format == GBitmapFormat8Bit ||
ctx.dest_bitmap.info.format == GBitmapFormat8BitCircular);
// This should touch all valid bytes in the framebuffer
for (int y = DISP_ROWS - 1; y >= 0; y--) {
GColor color = color_table[y % NUM_COLORS];
int16_t row_offset = DISP_COLS * y;
GBitmapDataRowInfo row_info = gbitmap_get_data_row_info(&ctx.dest_bitmap, y);
for (int x = row_info.min_x; x < row_info.max_x; x++) {
// Use direct framebuffer access to prove framebuffer-correct positioning
row_info.data[x] = color.argb;
}
}
}
// This test validates that a duma assert is caught when drawing outside of the framebuffer
void test_framebuffer_duma__draw_beyond_framebuffer(void) {
GContext ctx;
test_graphics_context_init(&ctx, fb);
// This test should only be running on color displays
cl_assert(ctx.dest_bitmap.info.format == GBitmapFormat8Bit ||
ctx.dest_bitmap.info.format == GBitmapFormat8BitCircular);
uint8_t *buffer = (uint8_t*)ctx.dest_bitmap.addr;
// Expect this to assert using duma protection, we are writing past framebuffer
cl_assert_passert(draw_fb_raw(buffer, FRAMEBUFFER_SIZE_BYTES + 1, GColorWhite));
}
@@ -0,0 +1,75 @@
/*
* 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/graphics/gtypes.h"
#include <stdio.h>
// stubs
#include "stubs_applib_resource.h"
#include "stubs_app_state.h"
#include "stubs_graphics_context.h"
#include "stubs_heap.h"
#include "stubs_logging.h"
#include "stubs_passert.h"
#include "stubs_process_manager.h"
/////////////////////////////
void test_gbitmap_data_row_info__get_info_rectangular(void) {
uint8_t some_addr;
GBitmap bmp = {
.addr = &some_addr,
.row_size_bytes = 123,
.bounds = GRect(1, 2, 3, 4),
.info.format = GBitmapFormat8Bit,
};
cl_assert_equal_i(123, gbitmap_format_get_row_size_bytes(123, GBitmapFormat8Bit));
cl_assert_equal_i(123, gbitmap_get_bytes_per_row(&bmp));
cl_assert_equal_p(&some_addr + 0 * 123, gbitmap_get_data_row_info(&bmp, 0).data);
cl_assert_equal_p(&some_addr + 1 * 123, gbitmap_get_data_row_info(&bmp, 1).data);
cl_assert_equal_i(0, gbitmap_get_data_row_info(&bmp, 3).min_x);
cl_assert_equal_i(3, gbitmap_get_data_row_info(&bmp, 3).max_x);
}
void test_gbitmap_data_row_info__get_info_circular(void) {
uint8_t some_addr;
GBitmapDataRowInfoInternal infos[] = {
{.offset = 1, .min_x = 2, .max_x = 3}, // 0
{.offset = 4, .min_x = 5, .max_x = 6}, // 1
{.offset = 7, .min_x = 8, .max_x = 9}, // 2 // 0
{.offset = 10, .min_x = 11, .max_x = 12}, // 3 // 1
{.offset = 13, .min_x = 14, .max_x = 15}, // 4 // 2
{.offset = 16, .min_x = 17, .max_x = 18}, // 5 // 3
};
GBitmap bmp = {
.addr = &some_addr,
.row_size_bytes = 123,
.bounds = GRect(1, 2, 3, 4),
.info.format = GBitmapFormat8BitCircular,
.data_row_infos = infos,
};
cl_assert_equal_i(0, gbitmap_format_get_row_size_bytes(123, GBitmapFormat8BitCircular));
cl_assert_equal_i(123, gbitmap_get_bytes_per_row(&bmp));
cl_assert_equal_p(&some_addr + 1, gbitmap_get_data_row_info(&bmp, 0).data);
cl_assert_equal_p(&some_addr + 4, gbitmap_get_data_row_info(&bmp, 1).data);
cl_assert_equal_i(11, gbitmap_get_data_row_info(&bmp, 3).min_x);
cl_assert_equal_i(12, gbitmap_get_data_row_info(&bmp, 3).max_x);
}
+222
View File
@@ -0,0 +1,222 @@
/*
* 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/graphics/gtypes.h"
#include "clar.h"
#include <stdio.h>
// stubs
#include "stubs_applib_resource.h"
#include "stubs_app_state.h"
#include "stubs_graphics_context.h"
#include "stubs_heap.h"
#include "stubs_logging.h"
#include "stubs_passert.h"
#include "stubs_process_manager.h"
/////////////////////////////
const void * const g_gbitmap_spalding_data_row_infos = &g_gbitmap_spalding_data_row_infos;
void test_gbitmap_formats__create_blank(void) {
const GSize s10 = GSize(10, 10);
const GSize s180 = GSize(180, 180);
GBitmap *bmp = NULL;
cl_assert((void*)&bmp->palette == (void*)&bmp->data_row_infos); // union with .palette
cl_assert(NULL != g_gbitmap_spalding_data_row_infos); // make sure unit-test fixture is ok
#ifdef PLATFORM_TINTIN
cl_assert(!process_manager_compiled_with_legacy2_sdk());
bmp = gbitmap_create_blank(s10, GBitmapFormat1Bit);
cl_assert(NULL != bmp);
cl_assert(NULL == bmp->data_row_infos);
bmp = gbitmap_create_blank(s10, GBitmapFormat8Bit);
cl_assert(NULL == bmp);
bmp = gbitmap_create_blank(s10, GBitmapFormat1BitPalette);
cl_assert(NULL != bmp);
cl_assert(g_gbitmap_spalding_data_row_infos != bmp->data_row_infos); // union with .palette
bmp = gbitmap_create_blank(s10, GBitmapFormat2BitPalette);
cl_assert(NULL != bmp);
cl_assert(g_gbitmap_spalding_data_row_infos != bmp->data_row_infos); // union with .palette
bmp = gbitmap_create_blank(s10, GBitmapFormat4BitPalette);
cl_assert(NULL == bmp);
bmp = gbitmap_create_blank(s10, GBitmapFormat8BitCircular);
cl_assert(NULL == bmp);
bmp = gbitmap_create_blank(s180, GBitmapFormat8BitCircular);
cl_assert(NULL == bmp);
#endif
#ifdef PLATFORM_SNOWY
bmp = gbitmap_create_blank(s10, GBitmapFormat1Bit);
cl_assert(NULL != bmp);
cl_assert(NULL == bmp->data_row_infos);
bmp = gbitmap_create_blank(s10, GBitmapFormat8Bit);
cl_assert(NULL != bmp);
cl_assert(NULL == bmp->data_row_infos);
bmp = gbitmap_create_blank(s10, GBitmapFormat1BitPalette);
cl_assert(NULL != bmp);
cl_assert(g_gbitmap_spalding_data_row_infos != bmp->data_row_infos); // union with .palette
bmp = gbitmap_create_blank(s10, GBitmapFormat2BitPalette);
cl_assert(NULL != bmp);
cl_assert(g_gbitmap_spalding_data_row_infos != bmp->data_row_infos); // union with .palette
bmp = gbitmap_create_blank(s10, GBitmapFormat4BitPalette);
cl_assert(NULL != bmp);
cl_assert(g_gbitmap_spalding_data_row_infos != bmp->data_row_infos); // union with .palette
bmp = gbitmap_create_blank(s10, GBitmapFormat8BitCircular);
cl_assert(NULL == bmp);
bmp = gbitmap_create_blank(s180, GBitmapFormat8BitCircular);
cl_assert(NULL == bmp);
#endif
#ifdef PLATFORM_SPALDING
bmp = gbitmap_create_blank(s10, GBitmapFormat1Bit);
cl_assert(NULL != bmp);
cl_assert(NULL == bmp->data_row_infos);
bmp = gbitmap_create_blank(s10, GBitmapFormat8Bit);
cl_assert(NULL != bmp);
cl_assert(NULL == bmp->data_row_infos);
bmp = gbitmap_create_blank(s10, GBitmapFormat1BitPalette);
cl_assert(NULL != bmp);
cl_assert(g_gbitmap_spalding_data_row_infos != bmp->data_row_infos); // union with .palette
bmp = gbitmap_create_blank(s10, GBitmapFormat2BitPalette);
cl_assert(NULL != bmp);
cl_assert(g_gbitmap_spalding_data_row_infos != bmp->data_row_infos); // union with .palette
bmp = gbitmap_create_blank(s10, GBitmapFormat4BitPalette);
cl_assert(NULL != bmp);
cl_assert(g_gbitmap_spalding_data_row_infos != bmp->data_row_infos); // union with .palette
bmp = gbitmap_create_blank(s10, GBitmapFormat8BitCircular);
cl_assert(NULL == bmp);
bmp = gbitmap_create_blank(s180, GBitmapFormat8BitCircular);
cl_assert(NULL != bmp);
cl_assert(g_gbitmap_spalding_data_row_infos == bmp->data_row_infos);
#endif
}
void test_gbitmap_formats__create_blank_with_palette(void) {
const GSize s10 = GSize(10, 10);
const GSize s180 = GSize(180, 180);
GBitmap *bmp;
GColor8 *p = (GColor8 *)&p; // some value to test against
#ifdef PLATFORM_TINTIN
cl_assert(!process_manager_compiled_with_legacy2_sdk());
cl_assert(NULL == gbitmap_create_blank_with_palette(s10, GBitmapFormat1Bit, p, true));
cl_assert(NULL == gbitmap_create_blank_with_palette(s10, GBitmapFormat8Bit, p, true));
bmp = gbitmap_create_blank_with_palette(s10, GBitmapFormat1BitPalette, p, true);
cl_assert(NULL != bmp);
cl_assert(p == gbitmap_get_palette(bmp));
bmp = gbitmap_create_blank_with_palette(s10, GBitmapFormat2BitPalette, p, true);
cl_assert(NULL != bmp);
cl_assert(p == gbitmap_get_palette(bmp));
bmp = gbitmap_create_blank_with_palette(s10, GBitmapFormat4BitPalette, p, true);
cl_assert(NULL == bmp);
cl_assert(NULL == gbitmap_create_blank_with_palette(s10, GBitmapFormat8BitCircular, p, true));
cl_assert(NULL == gbitmap_create_blank_with_palette(s180, GBitmapFormat8BitCircular, p, true));
#endif
#ifdef PLATFORM_SNOWY
cl_assert(NULL == gbitmap_create_blank_with_palette(s10, GBitmapFormat1Bit, p, true));
cl_assert(NULL == gbitmap_create_blank_with_palette(s10, GBitmapFormat8Bit, p, true));
bmp = gbitmap_create_blank_with_palette(s10, GBitmapFormat1BitPalette, p, true);
cl_assert(NULL != bmp);
cl_assert(p == gbitmap_get_palette(bmp));
bmp = gbitmap_create_blank_with_palette(s10, GBitmapFormat2BitPalette, p, true);
cl_assert(NULL != bmp);
cl_assert(p == gbitmap_get_palette(bmp));
bmp = gbitmap_create_blank_with_palette(s10, GBitmapFormat4BitPalette, p, true);
cl_assert(NULL != bmp);
cl_assert(p == gbitmap_get_palette(bmp));
cl_assert(NULL == gbitmap_create_blank_with_palette(s10, GBitmapFormat8BitCircular, p, true));
cl_assert(NULL == gbitmap_create_blank_with_palette(s180, GBitmapFormat8BitCircular, p, true));
#endif
#ifdef PLATFORM_SPALDING
cl_assert(NULL == gbitmap_create_blank_with_palette(s10, GBitmapFormat1Bit, p, true));
cl_assert(NULL == gbitmap_create_blank_with_palette(s10, GBitmapFormat8Bit, p, true));
bmp = gbitmap_create_blank_with_palette(s10, GBitmapFormat1BitPalette, p, true);
cl_assert(NULL != bmp);
cl_assert(p == gbitmap_get_palette(bmp));
bmp = gbitmap_create_blank_with_palette(s10, GBitmapFormat2BitPalette, p, true);
cl_assert(NULL != bmp);
cl_assert(p == gbitmap_get_palette(bmp));
bmp = gbitmap_create_blank_with_palette(s10, GBitmapFormat4BitPalette, p, true);
cl_assert(NULL != bmp);
cl_assert(p == gbitmap_get_palette(bmp));
cl_assert(NULL == gbitmap_create_blank_with_palette(s10, GBitmapFormat8BitCircular, p, true));
cl_assert(NULL == gbitmap_create_blank_with_palette(s180, GBitmapFormat8BitCircular, p, true));
#endif
}
void test_gbitmap_formats__display_framebuffer_bytes(void) {
#ifdef PLATFORM_TINTIN
const size_t expected = 20 * 168; // 20 * 8 == 144px + 2 bytes padding per scanline
#endif
#ifdef PLATFORM_SNOWY
const size_t expected = 144 * 168;
#endif
#ifdef PLATFORM_SPALDING
// all pixels + 2*76
const size_t expected = 25944;
#endif
cl_assert_equal_i(expected, DISPLAY_FRAMEBUFFER_BYTES);
}
size_t prv_gbitmap_size_for_data(GSize size, GBitmapFormat format);
void test_gbitmap_formats__size_for_data(void) {
cl_assert_equal_i( 40, prv_gbitmap_size_for_data(GSize(13, 10), GBitmapFormat1Bit));
cl_assert_equal_i(130, prv_gbitmap_size_for_data(GSize(13, 10), GBitmapFormat8Bit));
cl_assert_equal_i( 20, prv_gbitmap_size_for_data(GSize(13, 10), GBitmapFormat1BitPalette));
cl_assert_equal_i( 40, prv_gbitmap_size_for_data(GSize(13, 10), GBitmapFormat2BitPalette));
cl_assert_equal_i( 70, prv_gbitmap_size_for_data(GSize(13, 10), GBitmapFormat4BitPalette));
cl_assert_equal_i( 0, prv_gbitmap_size_for_data(GSize(13, 10), GBitmapFormat8BitCircular));
const size_t expected = PBL_IF_RECT_ELSE(0, DISPLAY_FRAMEBUFFER_BYTES);
cl_assert_equal_i(expected,
prv_gbitmap_size_for_data(GSize(180, 180), GBitmapFormat8BitCircular));
cl_assert_equal_i(expected,
prv_gbitmap_size_for_data(GSize(DISP_COLS, DISP_ROWS), GBitmapFormat8BitCircular));
}
+302
View File
@@ -0,0 +1,302 @@
/*
* 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/graphics/framebuffer.h"
// Stubs
///////////////////////
#include "stubs_app_state.h"
#include "stubs_graphics_circle.h"
#include "stubs_graphics_line.h"
#include "stubs_graphics_private.h"
#include "stubs_logging.h"
#include "stubs_passert.h"
#include "stubs_process_manager.h"
const GDrawRawImplementation g_default_draw_implementation;
// Statics
///////////////////////
static GContext s_ctx;
static FrameBuffer s_fb;
typedef struct MockBitbltBitmapIntoBitmapTiledCallRecording {
GBitmap *dest_bitmap;
const GBitmap *src_bitmap;
GRect dest_rect;
GPoint src_origin_offset;
GCompOp compositing_mode;
GColor8 tint_color;
} MockBitbltBitmapIntoBitmapTiledCallRecording;
typedef struct MockBitbltBitmapIntoBitmapTiledCallRecordings {
unsigned int call_count;
MockBitbltBitmapIntoBitmapTiledCallRecording last_call;
} MockBitbltBitmapIntoBitmapTiledCallRecordings;
static MockBitbltBitmapIntoBitmapTiledCallRecordings s_bitblt_bitmap_into_bitmap_tiled_calls;
// Fakes
///////////////////////
GContext *graphics_context_get_current_context(void) {
return &s_ctx;
}
void bitblt_bitmap_into_bitmap_tiled(GBitmap* dest_bitmap, const GBitmap *src_bitmap,
GRect dest_rect, GPoint src_origin_offset,
GCompOp compositing_mode, GColor8 tint_color) {
s_bitblt_bitmap_into_bitmap_tiled_calls.call_count++;
s_bitblt_bitmap_into_bitmap_tiled_calls.last_call =
(MockBitbltBitmapIntoBitmapTiledCallRecording) {
.dest_bitmap = dest_bitmap,
.src_bitmap = src_bitmap,
.dest_rect = dest_rect,
.src_origin_offset = src_origin_offset,
.compositing_mode = compositing_mode,
.tint_color = tint_color,
};
}
// Helpers
///////////////////////
static void cl_assert_equal_rect(const GRect a, const GRect b) {
cl_assert_equal_i(a.origin.x, b.origin.x);
cl_assert_equal_i(a.origin.y, b.origin.y);
cl_assert_equal_i(a.size.w, b.size.w);
cl_assert_equal_i(a.size.h, b.size.h);
}
// Setup
///////////////////////
void test_gbitmap_processor__initialize(void) {
s_fb = (FrameBuffer) {};
framebuffer_init(&s_fb, &(GSize) {DISP_COLS, DISP_ROWS});
graphics_context_init(&s_ctx, &s_fb, GContextInitializationMode_App);
s_bitblt_bitmap_into_bitmap_tiled_calls = (MockBitbltBitmapIntoBitmapTiledCallRecordings) {};
}
void test_gbitmap_processor__cleanup(void) {
}
// Tests
///////////////////////
#define EXPECTED_RECT_IN_PRE_FUNCTION (GRect(4, 3, 2, 1))
void test_gbitmap_processor__null_arguments(void) {
GBitmap bitmap = {0};
const GRect rect = EXPECTED_RECT_IN_PRE_FUNCTION;
// Passing NULL for the processor shouldn't cause any problems
graphics_draw_bitmap_in_rect_processed(&s_ctx, &bitmap, &rect, NULL);
// And it should try to draw the bitmap
cl_assert_equal_i(s_bitblt_bitmap_into_bitmap_tiled_calls.call_count, 1);
// Passing a processor with NULL functions shouldn't cause any problems
GBitmapProcessor processor = {0};
graphics_draw_bitmap_in_rect_processed(&s_ctx, &bitmap, &rect, &processor);
// And it should once again try to draw the bitmap
cl_assert_equal_i(s_bitblt_bitmap_into_bitmap_tiled_calls.call_count, 2);
}
#define EXPECTED_COMPOSITING_MODE_BEFORE_AND_AFTER_PRE_FUNCTION (GCompOpSet)
#define EXPECTED_TINT_COLOR_BEFORE_AND_AFTER_PRE_FUNCTION (GColorShockingPink)
#define COMPOSITING_MODE_TO_SPECIFY_IN_PRE_FUNCTION (GCompOpTint)
#define TINT_COLOR_TO_SPECIFY_IN_PRE_FUNCTION (GColorTiffanyBlue)
#define RECT_TO_SPECIFY_IN_PRE_FUNCTION (GRect(-50, -50, 100, 100))
#define BITMAP_TO_SPECIFY_IN_PRE_FUNCTION ((GBitmap *)1234)
#define EXPECTED_CLIPPED_RECT_AFTER_DRAWING_BITMAP (GRect(0, 0, 50, 50))
typedef struct PreAndPostFunctionsTestProcessor {
GBitmapProcessor processor;
GCompOp previous_compositing_mode;
GColor previous_tint_color;
} PreAndPostFunctionsTestProcessor;
static void prv_pre_and_post_functions__pre(GBitmapProcessor *processor, GContext *ctx,
const GBitmap **bitmap_to_use,
GRect *global_grect_to_use) {
PreAndPostFunctionsTestProcessor *processor_with_data =
(PreAndPostFunctionsTestProcessor *)processor;
// Record the existing compositing mode and tint color and check that they are what we expect
cl_assert(ctx->draw_state.compositing_mode ==
EXPECTED_COMPOSITING_MODE_BEFORE_AND_AFTER_PRE_FUNCTION);
processor_with_data->previous_compositing_mode = ctx->draw_state.compositing_mode;
cl_assert(gcolor_equal(ctx->draw_state.tint_color,
EXPECTED_TINT_COLOR_BEFORE_AND_AFTER_PRE_FUNCTION));
processor_with_data->previous_tint_color = ctx->draw_state.tint_color;
// Set the compositing mode and tint color to different values
ctx->draw_state.compositing_mode = COMPOSITING_MODE_TO_SPECIFY_IN_PRE_FUNCTION;
ctx->draw_state.tint_color = TINT_COLOR_TO_SPECIFY_IN_PRE_FUNCTION;
// Check that the rect here is what we gave to graphics_draw_bitmap_in_rect_processed()
cl_assert_equal_rect(*global_grect_to_use, EXPECTED_RECT_IN_PRE_FUNCTION);
// Change the rect
*global_grect_to_use = RECT_TO_SPECIFY_IN_PRE_FUNCTION;
// Change the bitmap
*bitmap_to_use = BITMAP_TO_SPECIFY_IN_PRE_FUNCTION;
}
static void prv_pre_and_post_functions__post(GBitmapProcessor *processor, GContext *ctx,
const GBitmap *bitmap_used,
const GRect *global_clipped_grect_used) {
PreAndPostFunctionsTestProcessor *processor_with_data =
(PreAndPostFunctionsTestProcessor *)processor;
// Check that the changes made to the GContext in .pre are still present
cl_assert(ctx->draw_state.compositing_mode == COMPOSITING_MODE_TO_SPECIFY_IN_PRE_FUNCTION);
cl_assert(gcolor_equal(ctx->draw_state.tint_color, TINT_COLOR_TO_SPECIFY_IN_PRE_FUNCTION));
// Reverse the changes to the GContext that were made in .pre
ctx->draw_state.compositing_mode = processor_with_data->previous_compositing_mode;
ctx->draw_state.tint_color = processor_with_data->previous_tint_color;
// Check that the bitmap here is the bitmap we specified in the .pre function
cl_assert_equal_p(bitmap_used, BITMAP_TO_SPECIFY_IN_PRE_FUNCTION);
// Check that the rect here is the clipped version of the rect we specified in the .pre function
cl_assert_equal_rect(*global_clipped_grect_used, EXPECTED_CLIPPED_RECT_AFTER_DRAWING_BITMAP);
}
void test_gbitmap_processor__pre_and_post_functions(void) {
GBitmap bitmap = {0};
const GRect rect = EXPECTED_RECT_IN_PRE_FUNCTION;
// Set the compositing mode and tint color to known values
s_ctx.draw_state.compositing_mode = EXPECTED_COMPOSITING_MODE_BEFORE_AND_AFTER_PRE_FUNCTION;
s_ctx.draw_state.tint_color = EXPECTED_TINT_COLOR_BEFORE_AND_AFTER_PRE_FUNCTION;
PreAndPostFunctionsTestProcessor processor = (PreAndPostFunctionsTestProcessor) {
.processor.pre = prv_pre_and_post_functions__pre,
.processor.post = prv_pre_and_post_functions__post,
};
graphics_draw_bitmap_in_rect_processed(&s_ctx, &bitmap, &rect, &processor.processor);
// Check that the bitmap was drawn
cl_assert_equal_i(s_bitblt_bitmap_into_bitmap_tiled_calls.call_count, 1);
// Check that the modifications made in the .pre function propagated to the bitmap drawing
cl_assert(s_bitblt_bitmap_into_bitmap_tiled_calls.last_call.compositing_mode ==
COMPOSITING_MODE_TO_SPECIFY_IN_PRE_FUNCTION);
cl_assert(gcolor_equal(s_bitblt_bitmap_into_bitmap_tiled_calls.last_call.tint_color,
TINT_COLOR_TO_SPECIFY_IN_PRE_FUNCTION));
cl_assert_equal_rect(s_bitblt_bitmap_into_bitmap_tiled_calls.last_call.dest_rect,
EXPECTED_CLIPPED_RECT_AFTER_DRAWING_BITMAP);
cl_assert_equal_p(s_bitblt_bitmap_into_bitmap_tiled_calls.last_call.src_bitmap,
BITMAP_TO_SPECIFY_IN_PRE_FUNCTION);
// Check that the modifications made to the GContext in the .pre function were reversed in .post
cl_assert(s_ctx.draw_state.compositing_mode ==
EXPECTED_COMPOSITING_MODE_BEFORE_AND_AFTER_PRE_FUNCTION);
cl_assert(gcolor_equal(s_ctx.draw_state.tint_color,
EXPECTED_TINT_COLOR_BEFORE_AND_AFTER_PRE_FUNCTION));
// Note that additional checks are performed in the .pre and .post functions
}
typedef struct PostFunctionCalledEvenIfPreFunctionCausesNothingToBeDrawnTestProcessor {
GBitmapProcessor processor;
const GBitmap *expected_bitmap_in_post;
bool post_func_called;
} PostFunctionCalledEvenIfPreFunctionCausesNothingToBeDrawnTestProcessor;
static void post_function_called_even_if_pre_function_causes_nothing_to_be_drawn__post(
GBitmapProcessor *processor, GContext *ctx, const GBitmap *bitmap_used,
const GRect *global_clipped_grect_used) {
PostFunctionCalledEvenIfPreFunctionCausesNothingToBeDrawnTestProcessor *processor_with_data =
(PostFunctionCalledEvenIfPreFunctionCausesNothingToBeDrawnTestProcessor *)processor;
// Check that the rectangle here is empty to verify that nothing was drawn
cl_assert_equal_rect(*global_clipped_grect_used, GRectZero);
// Check that bitmap_used is what we expect it to be (the expected value is set in .pre)
cl_assert_equal_p(bitmap_used, processor_with_data->expected_bitmap_in_post);
// Record that the .post function was called
processor_with_data->post_func_called = true;
}
//! Helper function for testing that the .post function is called even if the .pre function
//! causes no bitmap to be drawn
static void prv_post_function_called_even_if_pre_function_causes_nothing_to_be_drawn_test(
GBitmapProcessorPreFunc pre_func) {
GBitmap bitmap = {0};
const GRect rect = EXPECTED_RECT_IN_PRE_FUNCTION;
PostFunctionCalledEvenIfPreFunctionCausesNothingToBeDrawnTestProcessor processor =
(PostFunctionCalledEvenIfPreFunctionCausesNothingToBeDrawnTestProcessor) {
.processor.pre = pre_func,
.processor.post = post_function_called_even_if_pre_function_causes_nothing_to_be_drawn__post,
};
graphics_draw_bitmap_in_rect_processed(&s_ctx, &bitmap, &rect, &processor.processor);
// Check that the bitmap was not drawn
cl_assert_equal_i(s_bitblt_bitmap_into_bitmap_tiled_calls.call_count, 0);
// Check that the .post function was called even though the .pre function makes some change
// that causes no bitmap to be drawn
cl_assert(processor.post_func_called);
}
static void post_function_called_even_if_pre_function_specifies_null_bitmap__pre(
GBitmapProcessor *processor, GContext *ctx, const GBitmap **bitmap_to_use,
GRect *global_grect_to_use) {
PostFunctionCalledEvenIfPreFunctionCausesNothingToBeDrawnTestProcessor *processor_with_data =
(PostFunctionCalledEvenIfPreFunctionCausesNothingToBeDrawnTestProcessor *)processor;
// Change the bitmap to use to NULL to cause nothing to be drawn
*bitmap_to_use = NULL;
// We'll expect the bitmap we set there to be the bitmap passed into .post
processor_with_data->expected_bitmap_in_post = *bitmap_to_use;
}
void test_gbitmap_processor__post_function_called_even_if_pre_function_specifies_null_bitmap(void) {
prv_post_function_called_even_if_pre_function_causes_nothing_to_be_drawn_test(
post_function_called_even_if_pre_function_specifies_null_bitmap__pre);
};
static void post_function_called_even_if_pre_function_specifies_empty_rect__pre(
GBitmapProcessor *processor, GContext *ctx, const GBitmap **bitmap_to_use,
GRect *global_grect_to_use) {
PostFunctionCalledEvenIfPreFunctionCausesNothingToBeDrawnTestProcessor *processor_with_data =
(PostFunctionCalledEvenIfPreFunctionCausesNothingToBeDrawnTestProcessor *)processor;
// Change the rectangle to be empty to cause nothing to be drawn
*global_grect_to_use = GRectZero;
// We'll expect the bitmap we passed into graphics_draw_bitmap_in_rect_processed() in .post
// even though nothing will be drawn
processor_with_data->expected_bitmap_in_post = *bitmap_to_use;
}
void test_gbitmap_processor__post_function_called_even_if_pre_function_specifies_empty_rect(void) {
prv_post_function_called_even_if_pre_function_causes_nothing_to_be_drawn_test(
post_function_called_even_if_pre_function_specifies_empty_rect__pre);
};
@@ -0,0 +1,170 @@
/*
* 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/graphics/gtypes.h"
#include "applib/graphics/gbitmap_pbi.h"
#include "stubs_app_state.h"
#include "stubs_graphics_context.h"
#include "stubs_logging.h"
#include "stubs_process_manager.h"
#include "stubs_passert.h"
// Stubs
///////////////////////
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;
}
ResAppNum sys_get_current_resource_num(void) {
return 0;
}
const uint8_t *sys_resource_read_only_bytes(ResAppNum app_num, uint32_t resource_id,
size_t *num_bytes_out) {
return NULL;
}
// Fakes
///////////////////////
size_t s_resource_size;
size_t sys_resource_size(ResAppNum app_num, uint32_t resource_id) {
return s_resource_size;
}
typedef struct {
uint16_t row_size_bytes;
union {
uint16_t info_flags;
BitmapInfo info;
};
uint16_t width;
uint16_t height;
} FakeBitmapData;
FakeBitmapData s_fake_bitmap_data;
size_t sys_resource_load_range(
ResAppNum app_num, uint32_t id, uint32_t start_offset, uint8_t *data, size_t num_bytes) {
BitmapData *bitmap = (BitmapData*) data;
*bitmap = (BitmapData) {
.row_size_bytes = s_fake_bitmap_data.row_size_bytes,
.info_flags = s_fake_bitmap_data.info_flags,
.width = s_fake_bitmap_data.width,
.height = s_fake_bitmap_data.height
};
return num_bytes;
}
// Tests
///////////////////////
void test_gbitmap_resource_validation__initialize(void) {
s_resource_size = 0;
s_fake_bitmap_data = (FakeBitmapData) { 0 };
}
static uint32_t prv_calculate_size(FakeBitmapData *bitmap) {
const uint32_t required_size_bytes =
offsetof(BitmapData, data) + // header size
(bitmap->row_size_bytes * bitmap->height) + // pixel data
gbitmap_get_palette_size(bitmap->info.format); // palette data
return required_size_bytes;
}
void test_gbitmap_resource_validation__total_size(void) {
s_fake_bitmap_data = (FakeBitmapData) {
.row_size_bytes = 8,
.info.format = GBitmapFormat8Bit,
.info.version = GBITMAP_VERSION_1,
.width = 8,
.height = 1
};
// Set the resource size to be valid.
s_resource_size = prv_calculate_size(&s_fake_bitmap_data);
// We should load it successfully
GBitmap bitmap;
cl_assert(gbitmap_init_with_resource_system(&bitmap, 0, 0));
// However, if we corrupt the row_size_bytes field we should fail.
s_fake_bitmap_data.row_size_bytes = 12;
cl_assert(!gbitmap_init_with_resource_system(&bitmap, 0, 0));
// Corrupt it the other way, so that there's not enough data
s_fake_bitmap_data.row_size_bytes = 4;
cl_assert(!gbitmap_init_with_resource_system(&bitmap, 0, 0));
// Fix it up again
s_fake_bitmap_data.row_size_bytes = 8;
cl_assert(gbitmap_init_with_resource_system(&bitmap, 0, 0));
// But now change the palette format to something that requires more space and watch it fail
s_fake_bitmap_data.info.format = GBitmapFormat4BitPalette;
cl_assert(!gbitmap_init_with_resource_system(&bitmap, 0, 0));
// But if we have space for the palette, it should pass
s_resource_size = prv_calculate_size(&s_fake_bitmap_data);
cl_assert(gbitmap_init_with_resource_system(&bitmap, 0, 0));
}
void test_gbitmap_resource_validation__row_size(void) {
s_fake_bitmap_data = (FakeBitmapData) {
.row_size_bytes = 8,
.info.format = GBitmapFormat8Bit,
.info.version = GBITMAP_VERSION_1,
.width = 8,
.height = 1
};
// Set the resource size to be valid.
s_resource_size = prv_calculate_size(&s_fake_bitmap_data);
// We should load it successfully
GBitmap bitmap;
cl_assert(gbitmap_init_with_resource_system(&bitmap, 0, 0));
// Vary the width without changing the height to be too large
s_fake_bitmap_data.width = 10,
cl_assert(!gbitmap_init_with_resource_system(&bitmap, 0, 0));
// Too small is fine though
s_fake_bitmap_data.width = 6,
cl_assert(gbitmap_init_with_resource_system(&bitmap, 0, 0));
// Test with an uneven number of bits and make sure we're rounding correctly
s_fake_bitmap_data.info.format = GBitmapFormat1Bit;
s_fake_bitmap_data.width = 64;
cl_assert(gbitmap_init_with_resource_system(&bitmap, 0, 0));
s_fake_bitmap_data.info.format = GBitmapFormat1Bit;
s_fake_bitmap_data.width = 65;
cl_assert(!gbitmap_init_with_resource_system(&bitmap, 0, 0));
s_fake_bitmap_data.info.format = GBitmapFormat1Bit;
s_fake_bitmap_data.width = 63;
cl_assert(gbitmap_init_with_resource_system(&bitmap, 0, 0));
}
+345
View File
@@ -0,0 +1,345 @@
/*
* 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/graphics/gbitmap_sequence.h"
#include "applib/graphics/graphics.h"
#include "clar.h"
#include "util.h"
#include <util/size.h>
#include <string.h>
#include <stdio.h>
#include <time.h>
// Test files are Creative Commons 0 (ie. Public Domain) from
// http://opengameart.org/content/game-character-blue-flappy-bird-sprite-sheets
// http://opengameart.org/content/pixel-puncher-sprites
// http://opengameart.org/content/spike-man-monster
// http://opengameart.org/content/platformer-baddies
// http://opengameart.org/content/greyscale-special-effects-%E2%80%94-various-dimensions-and-flavours
// http://opengameart.org/content/bouncing-ball-guy
// http://opengameart.org/content/medals-3
// http://opengameart.org/content/open-pixel-platformer-tiles-sprites
#if SCREEN_COLOR_DEPTH_BITS == 8
#include "applib/graphics/8_bit/framebuffer.c"
#elif SCREEN_COLOR_DEPTH_BITS == 1
#include "applib/graphics/1_bit/framebuffer.c"
#endif
// Fakes
////////////////////////////////////
#include "fake_resource_syscalls.h"
#include "fake_app_timer.h"
// Stubs
////////////////////////////////////
#include "stubs_applib_resource.h"
#include "stubs_app_state.h"
#include "stubs_logging.h"
#include "stubs_heap.h"
#include "stubs_passert.h"
#include "stubs_pbl_malloc.h"
#include "stubs_pebble_tasks.h"
#include "stubs_print.h"
#include "stubs_queue.h"
#include "stubs_resources.h"
#include "stubs_serial.h"
#include "stubs_ui_window.h"
#define GET_PBI_NAME(x) prv_get_image_name(__func__, x, "pbi")
#define GET_APNG_NAME prv_get_image_name(__func__, 0, "apng")
// Used to work around __func__ not being a string literal (necessary for macro concatenation)
static const char *prv_get_image_name(const char* func_name, int index, const char *extension) {
char *filename = malloc(PATH_STRING_LENGTH);
if (index) {
snprintf(filename, PATH_STRING_LENGTH, "%s_%u.%s", func_name, index, extension);
} else {
snprintf(filename, PATH_STRING_LENGTH, "%s.%s", func_name, extension);
}
return filename;
}
// Tests
////////////////////////////////////
// Reference PNGs reside in "tests/test_images/"
// and are created at build time, with the files copied to TEST_IMAGES_PATH
// and a separate PBI generated by bitmapgen.py from the PNG copied to TEST_IMAGES_PATH
// Tests APNG file with 2-bit color, 6 frames and loop = infinite
// Tests start, stop, reset and loop
// Result:
// - gbitmaps matches platform decoded APNG
void test_gbitmap_sequence__color_2bit_bouncing_ball(void) {
#if PLATFROM_SPALDING
uint32_t resource_id = sys_resource_load_file_as_resource(TEST_IMAGES_PATH, GET_APNG_NAME);
cl_assert(resource_id != UINT32_MAX);
GBitmapSequence *bitmap_sequence = gbitmap_sequence_create_with_resource(resource_id);
cl_assert(bitmap_sequence);
GBitmap *bitmap = gbitmap_create_blank(gbitmap_sequence_get_bitmap_size(bitmap_sequence),
GBitmapFormat8Bit);
cl_assert(bitmap);
gbitmap_sequence_update_bitmap_next_frame(bitmap_sequence, bitmap, NULL);
cl_check(gbitmap_pbi_eq(bitmap, GET_PBI_NAME(1)));
gbitmap_sequence_update_bitmap_next_frame(bitmap_sequence, bitmap, NULL);
cl_check(gbitmap_pbi_eq(bitmap, GET_PBI_NAME(2)));
gbitmap_sequence_update_bitmap_next_frame(bitmap_sequence, bitmap, NULL);
cl_check(gbitmap_pbi_eq(bitmap, GET_PBI_NAME(3)));
gbitmap_sequence_update_bitmap_next_frame(bitmap_sequence, bitmap, NULL);
cl_check(gbitmap_pbi_eq(bitmap, GET_PBI_NAME(4)));
gbitmap_sequence_update_bitmap_next_frame(bitmap_sequence, bitmap, NULL);
cl_check(gbitmap_pbi_eq(bitmap, GET_PBI_NAME(5)));
gbitmap_sequence_update_bitmap_next_frame(bitmap_sequence, bitmap, NULL);
cl_check(gbitmap_pbi_eq(bitmap, GET_PBI_NAME(6)));
// Test loop around
gbitmap_sequence_update_bitmap_next_frame(bitmap_sequence, bitmap, NULL);
cl_check(gbitmap_pbi_eq(bitmap, GET_PBI_NAME(1)));
#endif
}
// Tests APNG file with 8-bit color, 88 frames of delay 100ms each and loop = 2
// Tests gbitmap_sequence_update_bitmap_by_elapsed
// Result:
// - gbitmaps matches platform decoded APNG
void test_gbitmap_sequence__color_8bit_fight(void) {
#if PLATFORM_SPALDING
bool status = false;
uint32_t resource_id = sys_resource_load_file_as_resource(TEST_IMAGES_PATH, GET_APNG_NAME);
cl_assert(resource_id != UINT32_MAX);
GBitmapSequence *bitmap_sequence = gbitmap_sequence_create_with_resource(resource_id);
cl_assert(bitmap_sequence);
GBitmap *bitmap = gbitmap_create_blank(gbitmap_sequence_get_bitmap_size(bitmap_sequence),
GBitmapFormat8Bit);
cl_assert(bitmap);
// first frame always shows at the beginning (0 ms)
status = gbitmap_sequence_update_bitmap_by_elapsed(bitmap_sequence, bitmap, 0);
cl_assert_equal_b(status, true);
cl_check(gbitmap_pbi_eq(bitmap, GET_PBI_NAME(1)));
// each frame has delay 100ms, so (33 - 1) * 100 ms shows 33rd frame
status = gbitmap_sequence_update_bitmap_by_elapsed(bitmap_sequence, bitmap, (33 - 1) * 100);
cl_assert_equal_b(status, true);
cl_check(gbitmap_pbi_eq(bitmap, GET_PBI_NAME(33)));
status = gbitmap_sequence_update_bitmap_by_elapsed(bitmap_sequence, bitmap, (60 - 1) * 100);
cl_assert_equal_b(status, true);
cl_check(gbitmap_pbi_eq(bitmap, GET_PBI_NAME(60)));
// Since we have loop count 2 and 88 frames, see if we can go to elapsed at 2nd loop correctly
status = gbitmap_sequence_update_bitmap_by_elapsed(bitmap_sequence, bitmap,
88 * 100 + (49 - 1) * 100);
cl_assert_equal_b(status, true);
cl_check(gbitmap_pbi_eq(bitmap, GET_PBI_NAME(49)));
// Since loop only equals 2, make sure that huge elapsed time leaves us at final frame
status = gbitmap_sequence_update_bitmap_by_elapsed(bitmap_sequence, bitmap, 12345 * 100);
cl_assert_equal_b(status, true);
cl_check(gbitmap_pbi_eq(bitmap, GET_PBI_NAME(88)));
#endif
}
// Tests APNG file with 8-bit color, 5 frames and loop = 1
// Tests gbitmap_sequence_update_bitmap_by_elapsed with 0 delay frames for 1, 4 and 5
// and 2 & 3 have delay 1000ms
// Result:
// - gbitmaps matches platform decoded APNG
void test_gbitmap_sequence__color_8bit_coin(void) {
#if PLATFORM_SPALDING
uint32_t resource_id = sys_resource_load_file_as_resource(TEST_IMAGES_PATH, GET_APNG_NAME);
cl_assert(resource_id != UINT32_MAX);
GBitmapSequence *bitmap_sequence = gbitmap_sequence_create_with_resource(resource_id);
cl_assert(bitmap_sequence);
GBitmap *bitmap = gbitmap_create_blank(gbitmap_sequence_get_bitmap_size(bitmap_sequence),
GBitmapFormat8Bit);
cl_assert(bitmap);
// Since frame 1 has 0 delay, it renders then immediately we render frame 2
gbitmap_sequence_update_bitmap_by_elapsed(bitmap_sequence, bitmap, 0);
cl_check(gbitmap_pbi_eq(bitmap, GET_PBI_NAME(2)));
// Frame 2 has delay 1000 ms, so at time 1000 ms, we expect frame 3
gbitmap_sequence_update_bitmap_by_elapsed(bitmap_sequence, bitmap, 1 * 1000);
cl_check(gbitmap_pbi_eq(bitmap, GET_PBI_NAME(3)));
// Frame 3 has delay 1000 ms, frame 4 and 5 have zero delay, so at time 2000 ms == frame 5
gbitmap_sequence_update_bitmap_by_elapsed(bitmap_sequence, bitmap, 2 * 1000);
cl_check(gbitmap_pbi_eq(bitmap, GET_PBI_NAME(5)));
// Since loop only equals 1, make sure that huge elapsed time leaves us at final frame
gbitmap_sequence_update_bitmap_by_elapsed(bitmap_sequence, bitmap, 12345 * 1000);
cl_check(gbitmap_pbi_eq(bitmap, GET_PBI_NAME(5)));
#endif
}
// Tests APNG file with 8-bit color, 5 frames and loop = 1
// Tests gbitmap_sequence_update_bitmap_by_elapsed with 0 delay frames for 1, 4 and 5
// and 2 & 3 have delay 1000ms
// Result:
// - gbitmaps matches platform decoded APNG
void test_gbitmap_sequence__color_8bit_coin_round(void) {
#if PLATFORM_SPALDING
uint32_t resource_id = sys_resource_load_file_as_resource(
TEST_IMAGES_PATH, "test_gbitmap_sequence__color_8bit_coin.apng");
cl_assert(resource_id != UINT32_MAX);
GBitmapSequence *bitmap_sequence = gbitmap_sequence_create_with_resource(resource_id);
cl_assert(bitmap_sequence);
GBitmap *bitmap = gbitmap_create_blank(GSize(180, 180), GBitmapFormat8BitCircular);
cl_assert(bitmap);
// Since frame 1 has 0 delay, it renders then immediately we render frame 2
gbitmap_sequence_update_bitmap_by_elapsed(bitmap_sequence, bitmap, 0);
cl_check(gbitmap_pbi_eq(bitmap, GET_PBI_NAME(2)));
// Frame 2 has delay 1000 ms, so at time 1000 ms, we expect frame 3
gbitmap_sequence_update_bitmap_by_elapsed(bitmap_sequence, bitmap, 1 * 1000);
cl_check(gbitmap_pbi_eq(bitmap, GET_PBI_NAME(3)));
// Frame 3 has delay 1000 ms, frame 4 and 5 have zero delay, so at time 2000 ms == frame 5
gbitmap_sequence_update_bitmap_by_elapsed(bitmap_sequence, bitmap, 2 * 1000);
cl_check(gbitmap_pbi_eq(bitmap, GET_PBI_NAME(5)));
// Since loop only equals 1, make sure that huge elapsed time leaves us at final frame
gbitmap_sequence_update_bitmap_by_elapsed(bitmap_sequence, bitmap, 12345 * 1000);
cl_check(gbitmap_pbi_eq(bitmap, GET_PBI_NAME(5)));
#endif
}
// Tests APNG file with 8-bit color, 5 frames and loop infinite, size 64x64
// Tests gbitmap_sequence for bitmap bounds offset and DISPOSE_OP_BACKGROUND
// Result:
// - gbitmaps matches platform decoded APNG
void test_gbitmap_sequence__color_8bit_bounds(void) {
#if PLATFORM_SPALDING
uint32_t resource_id = sys_resource_load_file_as_resource(TEST_IMAGES_PATH, GET_APNG_NAME);
cl_assert(resource_id != UINT32_MAX);
GRect orig_bounds = GRect(0, 0, 180, 180);
int xshift = 71;
int yshift = 39;
GRect shift_bounds = GRect(xshift, yshift,
orig_bounds.size.w - xshift,
orig_bounds.size.h - yshift);
GBitmapSequence *bitmap_sequence = gbitmap_sequence_create_with_resource(resource_id);
cl_assert(bitmap_sequence);
GBitmap *bitmap = gbitmap_create_blank(orig_bounds.size, GBitmapFormat8BitCircular);
cl_assert(bitmap);
// Shift the bounds when updating
gbitmap_set_bounds(bitmap, shift_bounds);
gbitmap_sequence_update_bitmap_next_frame(bitmap_sequence, bitmap, NULL);
// set the original bounds to do whole-image comparison
gbitmap_set_bounds(bitmap, orig_bounds);
cl_check(gbitmap_pbi_eq(bitmap, GET_PBI_NAME(1)));
gbitmap_set_bounds(bitmap, shift_bounds);
gbitmap_sequence_update_bitmap_next_frame(bitmap_sequence, bitmap, NULL);
gbitmap_set_bounds(bitmap, orig_bounds);
cl_check(gbitmap_pbi_eq(bitmap, GET_PBI_NAME(2)));
gbitmap_set_bounds(bitmap, shift_bounds);
gbitmap_sequence_update_bitmap_next_frame(bitmap_sequence, bitmap, NULL);
gbitmap_set_bounds(bitmap, orig_bounds);
cl_check(gbitmap_pbi_eq(bitmap, GET_PBI_NAME(3)));
gbitmap_set_bounds(bitmap, shift_bounds);
gbitmap_sequence_update_bitmap_next_frame(bitmap_sequence, bitmap, NULL);
gbitmap_set_bounds(bitmap, orig_bounds);
cl_check(gbitmap_pbi_eq(bitmap, GET_PBI_NAME(4)));
gbitmap_set_bounds(bitmap, shift_bounds);
gbitmap_sequence_update_bitmap_next_frame(bitmap_sequence, bitmap, NULL);
gbitmap_set_bounds(bitmap, orig_bounds);
cl_check(gbitmap_pbi_eq(bitmap, GET_PBI_NAME(5)));
#endif
}
// Tests APNG file with 8-bit color, originally broke the updated upng+tinflate code
// Result:
// - gbitmaps matches platform decoded APNG
void test_gbitmap_sequence__color_8bit_yoshi(void) {
#if PLATFORM_SPALDING
bool status = false;
uint32_t resource_id = sys_resource_load_file_as_resource(TEST_IMAGES_PATH, GET_APNG_NAME);
cl_assert(resource_id != UINT32_MAX);
GBitmapSequence *bitmap_sequence = gbitmap_sequence_create_with_resource(resource_id);
GBitmap *bitmap = gbitmap_create_blank(gbitmap_sequence_get_bitmap_size(bitmap_sequence),
GBitmapFormat8Bit);
status = gbitmap_sequence_update_bitmap_next_frame(bitmap_sequence, bitmap, NULL);
cl_assert_equal_b(status, true);
cl_check(gbitmap_pbi_eq(bitmap, GET_PBI_NAME(1)));
status = gbitmap_sequence_update_bitmap_next_frame(bitmap_sequence, bitmap, NULL);
cl_assert_equal_b(status, true);
cl_check(gbitmap_pbi_eq(bitmap, GET_PBI_NAME(2)));
status = gbitmap_sequence_update_bitmap_next_frame(bitmap_sequence, bitmap, NULL);
cl_assert_equal_b(status, true);
cl_check(gbitmap_pbi_eq(bitmap, GET_PBI_NAME(3)));
#endif
}
void test_gbitmap_sequence__1bit_to_1bit_notification(void) {
uint32_t resource_id = sys_resource_load_file_as_resource(TEST_IMAGES_PATH, GET_APNG_NAME);
cl_assert(resource_id != UINT32_MAX);
GBitmapSequence *bitmap_sequence = gbitmap_sequence_create_with_resource(resource_id);
GBitmap *bitmap = gbitmap_create_blank(gbitmap_sequence_get_bitmap_size(bitmap_sequence),
GBitmapFormat1Bit);
// We're interested in checking the following frames
const int check_frames[] = { 1, 4, 23, 24, 25, 31, 75 };
int current_frame = 0;
for (int i = 0; i < ARRAY_LENGTH(check_frames); ++i) {
// Advance to the next frame we're interested in.
for (; current_frame < check_frames[i]; ++current_frame) {
bool status = gbitmap_sequence_update_bitmap_next_frame(bitmap_sequence, bitmap, NULL);
cl_assert_equal_b(status, true);
}
printf("Checking %u\n", current_frame);
// Don't use GET_PBI_NAME, it doesn't like not using anything other than an integer literal
char filename_buffer[128];
snprintf(filename_buffer, sizeof(filename_buffer), "%s_%u.pbi", __func__, current_frame);
cl_check(gbitmap_pbi_eq(bitmap, filename_buffer));
}
}
+815
View File
@@ -0,0 +1,815 @@
/*
* 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/graphics/gdraw_command.h"
#include "applib/graphics/gdraw_command_list.h"
#include "applib/graphics/gdraw_command_image.h"
#include "applib/graphics/gdraw_command_private.h"
#include "applib/graphics/gtypes.h"
#include "applib/graphics/graphics.h"
#include "applib/graphics/graphics_line.h"
#include "applib/graphics/gpath.h"
#include <string.h>
#include "stubs_applib_resource.h"
#include "stubs_app_state.h"
#include "stubs_compiled_with_legacy2_sdk.h"
#include "stubs_heap.h"
#include "stubs_memory_layout.h"
#include "stubs_passert.h"
#include "stubs_pbl_malloc.h"
#include "stubs_resources.h"
#include "stubs_syscalls.h"
extern size_t prv_get_list_max_command_size(GDrawCommandList *command_list);
static GColor s_fill_color;
static GColor s_stroke_color;
static uint8_t s_stroke_width;
static uint16_t s_path_num_points;
static GPoint *s_stroke_points = NULL;
static GPoint *s_fill_points = NULL;
static bool s_path_open;
static uint16_t s_radius;
struct PreciseLine {
GPointPrecise p0;
GPointPrecise p1;
} *s_precise_lines = NULL;
static int s_num_precise_lines;
static int s_path_stroke_count;
static int s_path_stroke_precise_count;
static int s_path_fill_count;
static int s_path_fill_precise_count;
static int s_circle_stroke_count;
static int s_circle_fill_count;
static GPoint s_offset;
static GPoint *prv_copy_points(GPoint *points, uint16_t num_points, GPoint offset) {
s_path_num_points = num_points;
GPoint *copied_points = malloc(num_points * sizeof(GPoint));
cl_assert(copied_points != NULL);
for (int i = 0; i < num_points; i++) {
copied_points[i] = gpoint_add(points[i], offset);
}
return copied_points;
}
static bool prv_compare_points(GPoint *a, GPoint *b, uint16_t num_points) {
return (memcmp(a, b, sizeof(GPoint) * num_points) == 0);
}
// Stubs
void graphics_context_set_stroke_color(GContext* ctx, GColor color) {
s_stroke_color = color;
}
void graphics_context_set_fill_color(GContext* ctx, GColor color) {
s_fill_color = color;
}
void graphics_context_set_antialiased(GContext *ctx, bool enable) {
}
void graphics_context_set_stroke_width(GContext* ctx, uint8_t stroke_width) {
s_stroke_width = stroke_width;
}
void gpath_draw_stroke(GContext* ctx, GPath* path, bool open) {
s_stroke_points = prv_copy_points(path->points, path->num_points, s_offset);
s_path_open = open;
s_path_stroke_count++;
}
void gpath_fill_precise_internal(GContext *ctx, GPointPrecise *points, size_t num_points) {
s_fill_points = prv_copy_points((GPoint*)points, num_points, s_offset);
s_path_fill_precise_count++;
}
void gpath_draw_filled(GContext* ctx, GPath *path) {
s_fill_points = prv_copy_points(path->points, path->num_points, s_offset);
s_path_fill_count++;
}
void gpath_draw_outline_precise_internal(GContext *ctx, GPointPrecise *points, size_t num_points,
bool open) {
s_stroke_points = prv_copy_points((GPoint*)points, num_points, s_offset);
s_path_open = open;
s_path_stroke_precise_count++;
}
void graphics_draw_circle(GContext* ctx, GPoint p, uint16_t radius) {
s_stroke_points = prv_copy_points(&p, 1, s_offset);
s_radius = radius;
s_circle_stroke_count++;
}
void graphics_fill_circle(GContext* ctx, GPoint p, uint16_t radius) {
s_fill_points = prv_copy_points(&p, 1, s_offset);
s_radius = radius;
s_circle_fill_count++;
}
void graphics_context_move_draw_box(GContext* ctx, GPoint offset) {
s_offset = offset;
}
void graphics_line_draw_precise_stroked(GContext* ctx, GPointPrecise p0, GPointPrecise p1) {
s_precise_lines = realloc(s_precise_lines,
(s_num_precise_lines + 1) * sizeof(*s_precise_lines));
s_precise_lines[s_num_precise_lines].p0 = p0;
s_precise_lines[s_num_precise_lines].p1 = p1;
s_num_precise_lines++;
}
void prv_reset(void) {
s_fill_color = GColorClear;
s_stroke_color = GColorClear;
s_stroke_width = 0;
s_path_num_points = 0;
if (s_stroke_points) {
free(s_stroke_points);
s_stroke_points = NULL;
}
if (s_fill_points) {
free(s_fill_points);
s_fill_points = NULL;
}
s_path_open = false;
s_radius = 0;
s_path_stroke_count = 0;
s_path_stroke_precise_count = 0;
s_path_fill_count = 0;
s_path_fill_precise_count = 0;
s_circle_stroke_count = 0;
s_circle_fill_count = 0;
s_offset = (GPoint){ 0 };
if (s_precise_lines) {
free(s_precise_lines);
s_precise_lines = NULL;
}
s_num_precise_lines = 0;
}
// setup and teardown
void test_gdraw_command__initialize(void) {
prv_reset();
}
void test_gdraw_command__cleanup(void) {
if (s_stroke_points) {
free(s_stroke_points);
s_stroke_points = NULL;
}
if (s_fill_points) {
free(s_fill_points);
s_fill_points = NULL;
}
if (s_precise_lines) {
free(s_precise_lines);
s_precise_lines = NULL;
}
}
// tests
void test_gdraw_command__draw_command_stroke(void) {
GDrawCommand *command = malloc(sizeof(GDrawCommand) + (sizeof(GPoint) * 2));
*command = (GDrawCommand){
.type = GDrawCommandTypePath,
.hidden = false,
.stroke_color = GColorRed,
.stroke_width = 1,
.fill_color = GColorBlue,
.path_open = false,
.num_points = 2,
};
GPoint points[] = {{ 3, 97 }, {5, 5} };
memcpy(command->points, points, sizeof(points));
gdraw_command_draw(NULL, command);
cl_assert_equal_i(s_stroke_color.argb, GColorRedARGB8);
cl_assert_equal_i(s_fill_color.argb, GColorBlueARGB8);
cl_assert_equal_i(s_stroke_width, 1);
cl_assert_equal_i(s_path_num_points, 2);
cl_assert_equal_b(s_path_open, false);
cl_assert_equal_i(s_path_fill_count, 1);
cl_assert_equal_i(s_path_stroke_count, 1);
cl_assert(prv_compare_points(points, s_stroke_points, s_path_num_points));
cl_assert(prv_compare_points(points, s_fill_points, s_path_num_points));
prv_reset();
// set stroke width to zero - fill should be drawn, but not outline
gdraw_command_set_stroke_width(command, 0);
gdraw_command_draw(NULL, command);
cl_assert_equal_i(s_stroke_color.argb, GColorClearARGB8);
cl_assert_equal_i(s_fill_color.argb, GColorBlueARGB8);
cl_assert_equal_i(s_stroke_width, 0);
cl_assert_equal_i(s_path_num_points, 2);
cl_assert_equal_b(s_path_open, false);
cl_assert_equal_i(s_path_fill_count, 1);
cl_assert_equal_i(s_path_stroke_count, 0);
cl_assert(prv_compare_points(points, s_fill_points, s_path_num_points));
cl_assert_equal_p(s_stroke_points, NULL);
prv_reset();
// make fill color transparent (nothing should be drawn because the stroke width is zero and the
// fill is transparent
GColor color = gdraw_command_get_fill_color(command);
color.a = 0;
gdraw_command_set_fill_color(command, color);
gdraw_command_draw(NULL, command);
cl_assert_equal_i(s_stroke_color.argb, GColorClearARGB8);
cl_assert_equal_i(s_fill_color.argb, GColorClearARGB8);
cl_assert_equal_i(s_stroke_width, 0);
cl_assert_equal_i(s_path_num_points, 0);
cl_assert_equal_b(s_path_open, false);
cl_assert_equal_i(s_path_fill_count, 0);
cl_assert_equal_i(s_path_stroke_count, 0);
cl_assert_equal_p(s_stroke_points, NULL);
cl_assert_equal_p(s_fill_points, NULL);
prv_reset();
// set stroke width to non-zero value. stroke should be drawn, but no fill
gdraw_command_set_stroke_width(command, 2);
gdraw_command_draw(NULL, command);
cl_assert_equal_i(s_stroke_color.argb, GColorRedARGB8);
cl_assert_equal_i(s_fill_color.argb, GColorClearARGB8);
cl_assert_equal_i(s_stroke_width, 2);
cl_assert_equal_i(s_path_num_points, 2);
cl_assert_equal_b(s_path_open, false);
cl_assert_equal_i(s_path_fill_count, 0);
cl_assert_equal_i(s_path_stroke_count, 1);
cl_assert(prv_compare_points(points, s_stroke_points, s_path_num_points));
cl_assert_equal_p(s_fill_points, NULL);
prv_reset();
// set stroke color to be transparent and restore fill - fill should be drawn, but no outline
// should be drawn
gdraw_command_set_fill_color(command, GColorGreen);
color = gdraw_command_get_stroke_color(command);
color.a = 0;
gdraw_command_set_stroke_color(command, color);
gdraw_command_draw(NULL, command);
cl_assert_equal_i(s_stroke_color.argb, GColorClearARGB8);
cl_assert_equal_i(s_fill_color.argb, GColorGreenARGB8);
cl_assert_equal_i(s_stroke_width, 0);
cl_assert_equal_i(s_path_num_points, 2);
cl_assert_equal_b(s_path_open, false);
cl_assert_equal_i(s_path_fill_count, 1);
cl_assert_equal_i(s_path_stroke_count, 0);
cl_assert_equal_p(s_stroke_points, NULL);
cl_assert(prv_compare_points(points, s_fill_points, s_path_num_points));
prv_reset();
// restore stroke color and change both points
gdraw_command_set_stroke_color(command, GColorPurple);
GPoint points2[] = { { 23, 45 }, { 67, 13} };
gdraw_command_set_point(command, 0, points2[0]);
gdraw_command_set_point(command, 1, points2[1]);
gdraw_command_draw(NULL, command);
cl_assert_equal_i(s_stroke_color.argb, GColorPurpleARGB8);
cl_assert_equal_i(s_fill_color.argb, GColorGreenARGB8);
cl_assert_equal_i(s_stroke_width, 2);
cl_assert_equal_i(s_path_num_points, 2);
cl_assert_equal_b(s_path_open, false);
cl_assert_equal_i(s_path_fill_count, 1);
cl_assert_equal_i(s_path_stroke_count, 1);
cl_assert(prv_compare_points(points2, s_stroke_points, s_path_num_points));
cl_assert(prv_compare_points(points2, s_fill_points, s_path_num_points));
prv_reset();
// set path to be open
gdraw_command_set_path_open(command, true);
gdraw_command_draw(NULL, command);
cl_assert_equal_i(s_stroke_color.argb, GColorPurpleARGB8);
cl_assert_equal_i(s_fill_color.argb, GColorGreenARGB8);
cl_assert_equal_i(s_stroke_width, 2);
cl_assert_equal_i(s_path_num_points, 2);
cl_assert_equal_b(s_path_open, true);
cl_assert_equal_i(s_path_fill_count, 1);
cl_assert_equal_i(s_path_stroke_count, 1);
cl_assert(prv_compare_points(points2, s_stroke_points, s_path_num_points));
cl_assert(prv_compare_points(points2, s_fill_points, s_path_num_points));
prv_reset();
// set command to be hidden - nothing should be drawn
gdraw_command_set_hidden(command, true);
cl_assert_equal_i(s_stroke_color.argb, GColorClearARGB8);
cl_assert_equal_i(s_fill_color.argb, GColorClearARGB8);
cl_assert_equal_i(s_stroke_width, 0);
cl_assert_equal_i(s_path_num_points, 0);
cl_assert_equal_b(s_path_open, false);
cl_assert_equal_i(s_path_fill_count, 0);
cl_assert_equal_i(s_path_stroke_count, 0);
cl_assert_equal_p(s_stroke_points, NULL);
cl_assert_equal_p(s_fill_points, NULL);
free(command);
}
void test_gdraw_command__draw_precise_path(void) {
GDrawCommand *command = malloc(sizeof(GDrawCommand) + (sizeof(GPoint) * 3 ));
*command = (GDrawCommand){
.type = GDrawCommandTypePrecisePath,
.hidden = false,
.stroke_color = GColorRed,
.stroke_width = 1,
.fill_color = GColorBlue,
.path_open = false,
.num_points = 3,
};
GPointPrecise points[] = {
{ .x.raw_value = 8, .y.raw_value = 17 },
{ .x.raw_value = 4, .y.raw_value = 16 },
{ .x.raw_value = 2,.y.raw_value = 7 }
};
memcpy(command->precise_points, points, sizeof(points));
gdraw_command_draw(NULL, command);
cl_assert_equal_i(1, s_path_fill_precise_count);
cl_assert(prv_compare_points((GPoint*)points, s_fill_points, s_path_num_points));
cl_assert_equal_i(1, s_path_stroke_precise_count);
cl_assert_equal_b(false, s_path_open);
cl_assert(prv_compare_points((GPoint*)points, s_stroke_points, s_path_num_points));
prv_reset();
// change to open path and ensure that only draws 2 lines
command->path_open = true;
gdraw_command_draw(NULL, command);
cl_assert_equal_i(1, s_path_stroke_precise_count);
cl_assert_equal_b(true, s_path_open);
cl_assert(prv_compare_points((GPoint*)points, s_stroke_points, s_path_num_points));
free(command);
}
void test_gdraw_command__draw_circle(void) {
GDrawCommand *command = malloc(sizeof(GDrawCommand) + sizeof(GPoint));
*command = (GDrawCommand){
.type = GDrawCommandTypeCircle,
.hidden = false,
.stroke_color = GColorGreen,
.stroke_width = 1,
.fill_color = GColorOrange,
.radius = 300,
.num_points = 1,
};
GPoint center = { 15, 17 };
command->points[0] = center;
gdraw_command_draw(NULL, command);
cl_assert_equal_i(s_stroke_color.argb, GColorGreenARGB8);
cl_assert_equal_i(s_fill_color.argb, GColorOrangeARGB8);
cl_assert_equal_i(s_stroke_width, 1);
cl_assert_equal_i(s_path_num_points, 1);
cl_assert_equal_b(s_radius, 300);
cl_assert_equal_i(s_circle_fill_count, 1);
cl_assert_equal_i(s_circle_stroke_count, 1);
cl_assert(prv_compare_points(&center, s_stroke_points, s_path_num_points));
cl_assert(prv_compare_points(&center, s_fill_points, s_path_num_points));
prv_reset();
// set stroke width to zero - fill should be drawn, but not outline
gdraw_command_set_stroke_width(command, 0);
gdraw_command_draw(NULL, command);
cl_assert_equal_i(s_stroke_color.argb, GColorClearARGB8);
cl_assert_equal_i(s_fill_color.argb, GColorOrangeARGB8);
cl_assert_equal_i(s_stroke_width, 0);
cl_assert_equal_i(s_path_num_points, 1);
cl_assert_equal_b(s_radius, 300);
cl_assert_equal_i(s_circle_fill_count, 1);
cl_assert_equal_i(s_circle_stroke_count, 0);
cl_assert_equal_p(s_stroke_points, NULL);
cl_assert(prv_compare_points(&center, s_fill_points, s_path_num_points));
prv_reset();
// make fill color transparent (nothing should be drawn because the stroke width is zero and the
// fill is transparent
GColor color = gdraw_command_get_fill_color(command);
color.a = 0;
gdraw_command_set_fill_color(command, color);
gdraw_command_draw(NULL, command);
cl_assert_equal_i(s_stroke_color.argb, GColorClearARGB8);
cl_assert_equal_i(s_fill_color.argb, GColorClearARGB8);
cl_assert_equal_i(s_stroke_width, 0);
cl_assert_equal_i(s_path_num_points, 0);
cl_assert_equal_b(s_radius, 0);
cl_assert_equal_i(s_circle_fill_count, 0);
cl_assert_equal_i(s_circle_stroke_count, 0);
cl_assert_equal_p(s_stroke_points, NULL);
cl_assert_equal_p(s_fill_points, NULL);
prv_reset();
// set stroke width to non-zero value. stroke should be drawn, but no fill
gdraw_command_set_stroke_width(command, 2);
gdraw_command_draw(NULL, command);
cl_assert_equal_i(s_stroke_color.argb, GColorGreenARGB8);
cl_assert_equal_i(s_fill_color.argb, GColorClearARGB8);
cl_assert_equal_i(s_stroke_width, 2);
cl_assert_equal_i(s_path_num_points, 1);
cl_assert_equal_b(s_radius, 300);
cl_assert_equal_i(s_circle_fill_count, 0);
cl_assert_equal_i(s_circle_stroke_count, 1);
cl_assert(prv_compare_points(&center, s_stroke_points, s_path_num_points));
cl_assert_equal_p(s_fill_points, NULL);
prv_reset();
// set stroke color to be transparent and restore fill - fill should be drawn, but no outline
// should be drawn
gdraw_command_set_fill_color(command, GColorRed);
color = gdraw_command_get_stroke_color(command);
color.a = 0;
gdraw_command_set_stroke_color(command, color);
gdraw_command_draw(NULL, command);
cl_assert_equal_i(s_stroke_color.argb, GColorClearARGB8);
cl_assert_equal_i(s_fill_color.argb, GColorRedARGB8);
cl_assert_equal_i(s_stroke_width, 0);
cl_assert_equal_i(s_path_num_points, 1);
cl_assert_equal_b(s_path_open, false);
cl_assert_equal_i(s_circle_fill_count, 1);
cl_assert_equal_i(s_circle_stroke_count, 0);
cl_assert_equal_p(s_stroke_points, NULL);
cl_assert(prv_compare_points(&center, s_fill_points, s_path_num_points));
prv_reset();
// restore stroke color and set radius to zero - only a stroke should be drawn
gdraw_command_set_stroke_color(command, GColorPurple);
gdraw_command_set_radius(command, 0);
gdraw_command_draw(NULL, command);
cl_assert_equal_i(s_stroke_color.argb, GColorPurpleARGB8);
cl_assert_equal_i(s_fill_color.argb, GColorClearARGB8);
cl_assert_equal_i(s_stroke_width, 2);
cl_assert_equal_i(s_path_num_points, 1);
cl_assert_equal_b(s_radius, 0);
cl_assert_equal_i(s_circle_fill_count, 0);
cl_assert_equal_i(s_circle_stroke_count, 1);
cl_assert(prv_compare_points(&center, s_stroke_points, s_path_num_points));
cl_assert_equal_p(s_fill_points, NULL);
prv_reset();
// restore radius and set hidden - nothing should be drawn
gdraw_command_set_radius(command, 300);
gdraw_command_set_hidden(command, true);
gdraw_command_draw(NULL, command);
cl_assert_equal_i(s_stroke_color.argb, GColorClearARGB8);
cl_assert_equal_i(s_fill_color.argb, GColorClearARGB8);
cl_assert_equal_i(s_stroke_width, 0);
cl_assert_equal_i(s_path_num_points, 0);
cl_assert_equal_b(s_radius, 0);
cl_assert_equal_i(s_circle_fill_count, 0);
cl_assert_equal_i(s_circle_stroke_count, 0);
cl_assert_equal_p(s_stroke_points, NULL);
cl_assert_equal_p(s_fill_points, NULL);
free(command);
}
static GDrawCommandList *prv_create_command_list_3(void) {
GDrawCommandList *command_list = malloc(sizeof(GDrawCommandList) + (3 * sizeof(GDrawCommand)) +
(sizeof(GPoint) * 6));
*command_list = (GDrawCommandList) {
.num_commands = 3
};
GDrawCommand *command;
command = gdraw_command_list_get_command(command_list, 0);
*command = (GDrawCommand) {
.type = GDrawCommandTypePath,
.hidden = false,
.stroke_color = GColorRed,
.stroke_width = 1,
.fill_color = GColorBlue,
.path_open = false,
.num_points = 2,
};
GPoint points1[] = { { 3, 97 }, {5, 5} };
memcpy(command_list->commands[0].points, points1, sizeof(points1));
command = gdraw_command_list_get_command(command_list, 1);
*command = (GDrawCommand) {
.type = GDrawCommandTypeCircle,
.hidden = false,
.stroke_color = GColorGreen,
.stroke_width = 1,
.fill_color = GColorOrange,
.radius = 300,
.num_points = 1,
};
command->points[0] = (GPoint) { 1, 2 };
command = gdraw_command_list_get_command(command_list, 2);
*command = (GDrawCommand) {
.type = GDrawCommandTypePath,
.hidden = false,
.stroke_color = GColorGreen,
.stroke_width = 1,
.fill_color = GColorPurple,
.path_open = false,
.num_points = 3,
};
GPoint points2[] = { { 6, 7 }, {5, 5}, { 0, 0 } };
memcpy(command->points, points2, sizeof(points2));
return command_list;
}
void test_gdraw_command__draw_command_list(void) {
GDrawCommandList *command_list = malloc(sizeof(GDrawCommandList) + sizeof(GDrawCommand) +
(sizeof(GPoint) * 2));
*command_list = (GDrawCommandList) {
.num_commands = 1
};
command_list->commands[0] = (GDrawCommand) {
.type = GDrawCommandTypePath,
.hidden = false,
.stroke_color = GColorRed,
.stroke_width = 1,
.fill_color = GColorBlue,
.path_open = false,
.num_points = 2,
};
GPoint points1[] = { { 3, 97 }, {5, 5} };
memcpy(command_list->commands[0].points, points1, sizeof(points1));
GContext *ctx = (GContext *)123; // just a fake internal guard != NULL
gdraw_command_list_draw(ctx, command_list);
cl_assert_equal_i(s_path_stroke_count, 1);
cl_assert_equal_i(s_path_fill_count, 1);
cl_assert_equal_i(s_circle_stroke_count, 0);
cl_assert_equal_i(s_circle_fill_count, 0);
prv_reset();
free(command_list);
command_list = prv_create_command_list_3();
gdraw_command_list_draw(ctx, command_list);
cl_assert_equal_i(s_path_stroke_count, 2);
cl_assert_equal_i(s_path_fill_count, 2);
cl_assert_equal_i(s_circle_stroke_count, 1);
cl_assert_equal_i(s_circle_fill_count, 1);
prv_reset();
gdraw_command_list_get_command(command_list, 2)->hidden = true;
gdraw_command_list_draw(ctx, command_list);
cl_assert_equal_i(s_path_stroke_count, 1);
cl_assert_equal_i(s_path_fill_count, 1);
cl_assert_equal_i(s_circle_stroke_count, 1);
cl_assert_equal_i(s_circle_fill_count, 1);
prv_reset();
gdraw_command_list_get_command(command_list, 0)->hidden = true;
gdraw_command_list_draw(ctx, command_list);
cl_assert_equal_i(s_path_stroke_count, 0);
cl_assert_equal_i(s_path_fill_count, 0);
cl_assert_equal_i(s_circle_stroke_count, 1);
cl_assert_equal_i(s_circle_fill_count, 1);
prv_reset();
gdraw_command_list_get_command(command_list, 1)->hidden = true;
gdraw_command_list_draw(ctx, command_list);
cl_assert_equal_i(s_path_stroke_count, 0);
cl_assert_equal_i(s_path_fill_count, 0);
cl_assert_equal_i(s_circle_stroke_count, 0);
cl_assert_equal_i(s_circle_fill_count, 0);
free(command_list);
}
void test_gdraw_command__validate_list(void) {
GDrawCommandList *command_list = prv_create_command_list_3();
size_t size = sizeof(GDrawCommandList) + (3 * sizeof(GDrawCommand)) +
(sizeof(GPoint) * 6);
cl_assert(gdraw_command_list_validate(command_list, size));
command_list->num_commands = 4;
cl_assert(!gdraw_command_list_validate(command_list, size));
command_list->num_commands = 3;
GDrawCommand *command = gdraw_command_list_get_command(command_list, 0);
command->num_points = 0;
cl_assert(!gdraw_command_list_validate(command_list, size));
command->num_points = 2;
command = gdraw_command_list_get_command(command_list, 2);
command->num_points = 4;
cl_assert(!gdraw_command_list_validate(command_list, size));
command->num_points = 3;
command->type = GDrawCommandTypeCircle;
cl_assert(!gdraw_command_list_validate(command_list, size));
command->type = GDrawCommandTypePrecisePath;
cl_assert(gdraw_command_list_validate(command_list, size));
free(command_list);
}
void test_gdraw_command__validate_image(void) {
size_t size = sizeof(GDrawCommandImage) + (3 * sizeof(GDrawCommand)) +
(sizeof(GPoint) * 6);
GDrawCommandImage *image = malloc(size);
GDrawCommandList *command_list = prv_create_command_list_3();
memcpy(&image->command_list, command_list, sizeof(GDrawCommandList) + (3 * sizeof(GDrawCommand)) +
(sizeof(GPoint) * 6));
free(command_list);
image->version = 1;
image->size = GSize(20, 20);
cl_assert(gdraw_command_image_validate(image, size));
cl_assert(!gdraw_command_image_validate(image, size - 1));
cl_assert(!gdraw_command_image_validate(image, size + 1));
cl_assert_equal_i(size, gdraw_command_image_get_data_size(image));
image->version = 2;
cl_assert(!gdraw_command_image_validate(image, size));
free(image);
}
void test_gdraw_command__clone_image(void) {
cl_assert_equal_p(gdraw_command_image_clone(NULL), NULL);
size_t size = sizeof(GDrawCommandImage) + (3 * sizeof(GDrawCommand)) +
(sizeof(GPoint) * 6);
GDrawCommandImage *image = malloc(size);
memset(image, 0, size);
GDrawCommandList *command_list = prv_create_command_list_3();
memcpy(&image->command_list, command_list, sizeof(GDrawCommandList) + (3 * sizeof(GDrawCommand)) +
(sizeof(GPoint) * 6));
free(command_list);
image->version = 1;
image->size = GSize(20, 20);
GDrawCommandImage *clone = gdraw_command_image_clone(image);
cl_assert(clone != image);
cl_assert_equal_i(gdraw_command_image_get_data_size(clone), size);
cl_assert_equal_i(0, memcmp(clone, image, size));
free(clone);
free(image);
}
void test_gdraw_command__draw_image(void) {
GDrawCommandImage *image = malloc(sizeof(GDrawCommandImage) + sizeof(GDrawCommand) +
(sizeof(GPoint) * 2));
GDrawCommandList *command_list = &image->command_list;
*command_list = (GDrawCommandList) {
.num_commands = 1
};
GDrawCommand *command;
command = gdraw_command_list_get_command(command_list, 0);
*command = (GDrawCommand) {
.type = GDrawCommandTypePath,
.hidden = false,
.stroke_color = GColorRed,
.stroke_width = 1,
.fill_color = GColorBlue,
.path_open = true,
.num_points = 2,
};
GPoint points[] = {{ 6, 1 }, {5, -5} };
memcpy(command_list->commands[0].points, points, sizeof(points));
GContext *ctx = (GContext *)123; // just a fake internal guard != NULL
gdraw_command_image_draw(ctx, image, GPoint(0, 0));
cl_assert_equal_i(s_path_num_points, 2);
cl_assert(prv_compare_points(points, s_stroke_points, s_path_num_points));
cl_assert(prv_compare_points(points, s_fill_points, s_path_num_points));
prv_reset();
gdraw_command_image_draw(ctx, image, GPoint(-1, 1));
cl_assert_equal_i(s_path_num_points, 2);
points[0] = GPoint(5, 2);
points[1] = GPoint(4, -4);
cl_assert(prv_compare_points(points, s_stroke_points, s_path_num_points));
cl_assert(prv_compare_points(points, s_fill_points, s_path_num_points));
free(image);
}
void test_gdraw_command__draw_frame(void) {
GDrawCommandFrame *frame = malloc(sizeof(GDrawCommandFrame) + sizeof(GDrawCommand) +
(sizeof(GPoint) * 2));
GDrawCommandList *command_list = &frame->command_list;
*command_list = (GDrawCommandList) {
.num_commands = 1
};
GDrawCommand *command = gdraw_command_list_get_command(command_list, 0);
*command = (GDrawCommand) {
.type = GDrawCommandTypePath,
.hidden = false,
.stroke_color = GColorRed,
.stroke_width = 1,
.fill_color = GColorBlue,
.path_open = true,
.num_points = 2,
};
GPoint points[] = {{ 1, 1 }, {2, -2} };
memcpy(command_list->commands[0].points, points, sizeof(points));
GContext *ctx = (GContext *)123; // just a fake internal guard != NULL
gdraw_command_frame_draw(ctx, NULL, frame, GPoint(0, 0));
cl_assert_equal_i(s_path_num_points, 2);
cl_assert(prv_compare_points(points, s_stroke_points, s_path_num_points));
cl_assert(prv_compare_points(points, s_fill_points, s_path_num_points));
prv_reset();
gdraw_command_frame_draw(ctx, NULL, frame, GPoint(-1, 1));
cl_assert_equal_i(s_path_num_points, 2);
points[0] = GPoint(0, 2);
points[1] = GPoint(1, -1);
cl_assert(prv_compare_points(points, s_stroke_points, s_path_num_points));
cl_assert(prv_compare_points(points, s_fill_points, s_path_num_points));
free(frame);
}
static int s_iterations = 0;
static bool prv_iterate(GDrawCommand *command, uint32_t index, void *context) {
s_iterations++;
return true;
}
void test_gdraw_command__iterate(void) {
GDrawCommandList *command_list = prv_create_command_list_3();
void *end = gdraw_command_list_iterate_private(command_list, prv_iterate, NULL);
cl_assert_equal_i(s_iterations, 3);
GDrawCommand *command = gdraw_command_list_get_command(command_list, 2);
void *expected_end = command->points + command->num_points;
cl_assert_equal_p(end, expected_end);
free(command_list);
}
typedef struct {
GDrawCommandProcessor processor;
GColor stroke_color;
} SetStrokeColorProcessor;
static void prv_set_stroke_color(GDrawCommandProcessor *processor,
GDrawCommand *processed_command,
const size_t processed_command_max_size,
const GDrawCommandList* list,
const GDrawCommand *command) {
SetStrokeColorProcessor *stroke_processor = (SetStrokeColorProcessor *)processor;
gdraw_command_set_stroke_color(processed_command, stroke_processor->stroke_color);
}
void test_gdraw_command__draw_command_list_processed(void) {
GDrawCommandList *command_list = prv_create_command_list_3();
GColor stroke_color = GColorTiffanyBlue;
SetStrokeColorProcessor stroke_processor = {
.processor = {
.command = prv_set_stroke_color,
},
.stroke_color = stroke_color,
};
// ctx can't be null pass in random pointer to satisfy function
GContext ctx;
gdraw_command_list_draw_processed(&ctx, command_list, (GDrawCommandProcessor *)&stroke_processor);
cl_assert_equal_i(s_stroke_color.argb, GColorTiffanyBlueARGB8);
free(command_list);
}
void test_gdraw_command__get_max_command_size_in_list(void) {
GDrawCommandList *command_list = prv_create_command_list_3();
// The third command in the list is the largest so it's size should be returned by
// prv_get_list_max_command_size
cl_assert_equal_i(gdraw_command_get_data_size(gdraw_command_list_get_command(command_list, 2)),
prv_get_list_max_command_size(command_list));
free(command_list);
}
@@ -0,0 +1,81 @@
/*
* 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/graphics/gdraw_command.h"
#include "applib/graphics/gdraw_command_list.h"
#include "applib/graphics/gdraw_command_image.h"
#include "applib/graphics/gdraw_command_private.h"
#include "applib/graphics/gtypes.h"
#include "applib/graphics/graphics.h"
#include "applib/graphics/graphics_line.h"
#include "applib/graphics/gpath.h"
#include <string.h>
#include "util.h" // graphics tests utils
#include "fake_pbl_malloc.h"
#include "fake_resource_syscalls.h"
#include "stubs_app_state.h"
#include "stubs_compiled_with_legacy2_sdk.h"
#include "stubs_graphics_context.h"
#include "stubs_gpath.h"
#include "stubs_heap.h"
#include "stubs_logging.h"
#include "stubs_memory_layout.h"
#include "stubs_passert.h"
// Stubs
void graphics_draw_circle(GContext* ctx, GPoint p, uint16_t radius) {}
void graphics_fill_circle(GContext* ctx, GPoint p, uint16_t radius) {}
void framebuffer_clear(FrameBuffer* f){}
void graphics_context_move_draw_box(GContext* ctx, GPoint offset) {}
void graphics_line_draw_precise_stroked(GContext* ctx, GPointPrecise p0, GPointPrecise p1) {}
const uint8_t *resource_get_builtin_bytes(ResAppNum app_num, uint32_t resource_id,
uint32_t *num_bytes_out) { return NULL; }
void test_gdraw_command_resources__load_pdci(void) {
uint32_t resource_id = sys_resource_load_file_as_resource(TEST_IMAGES_PATH, TEST_PDC_FILE);
cl_assert(resource_id != UINT32_MAX);
GDrawCommandImage *image = gdraw_command_image_create_with_resource(resource_id);
cl_assert(image != NULL);
}
void test_gdraw_command_resources__load_pdcs(void) {
uint32_t resource_id = sys_resource_load_file_as_resource(TEST_IMAGES_PATH, TEST_PDC_FILE);
cl_assert(resource_id != UINT32_MAX);
GDrawCommandSequence *sequence = gdraw_command_sequence_create_with_resource(resource_id);
cl_assert(sequence != NULL);
}
// Test that loading an invalid PDC file fails
void test_gdraw_command_resources__load_invalid(void) {
uint32_t resource_id = sys_resource_load_file_as_resource(TEST_IMAGES_PATH, TEST_PDC_FILE);
cl_assert(resource_id != UINT32_MAX);
// Test Command_Image
GDrawCommandImage *image = gdraw_command_image_create_with_resource(resource_id);
cl_assert(image == NULL);
// Test Command Sequence
GDrawCommandSequence *sequence = gdraw_command_sequence_create_with_resource(resource_id);
cl_assert(sequence == NULL);
}
@@ -0,0 +1,260 @@
/*
* 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/graphics/gdraw_command.h"
#include "applib/graphics/gdraw_command_list.h"
#include "applib/graphics/gdraw_command_frame.h"
#include "applib/graphics/gdraw_command_private.h"
#include "applib/graphics/gdraw_command_sequence.h"
#include "applib/graphics/gtypes.h"
#include "applib/graphics/graphics.h"
#include "applib/graphics/gpath.h"
#include "util/size.h"
#include "stubs_applib_resource.h"
#include "stubs_memory_layout.h"
#include "stubs_passert.h"
#include "stubs_pbl_malloc.h"
#include "stubs_resources.h"
#include "stubs_syscalls.h"
// Stubs
void graphics_context_set_stroke_color(GContext* ctx, GColor color) {}
void graphics_context_set_fill_color(GContext* ctx, GColor color) {}
void graphics_context_set_stroke_width(GContext* ctx, uint8_t stroke_width) {}
void gpath_draw_stroke(GContext* ctx, GPath* path, bool open) {}
void gpath_draw_filled(GContext* ctx, GPath *path) {}
void graphics_draw_circle(GContext* ctx, GPoint p, uint16_t radius) {}
void graphics_fill_circle(GContext* ctx, GPoint p, uint16_t radius) {}
void graphics_context_move_draw_box(GContext* ctx, GPoint offset) {}
void graphics_line_draw_precise_stroked(GContext* ctx, GPointPrecise p0, GPointPrecise p1) {}
void gpath_fill_precise_internal(GContext *ctx, GPointPrecise *points, size_t num_points) {}
void gpath_draw_outline_precise_internal(GContext *ctx, GPointPrecise *points, size_t num_points,
bool open) {}
typedef uint16_t ResourceId;
const uint8_t *resource_get_builtin_bytes(ResAppNum app_num, uint32_t resource_id,
uint32_t *num_bytes_out) { return NULL; }
// setup and teardown
void test_gdraw_command_sequence__initialize(void) {
}
void test_gdraw_command_sequence__cleanup(void) {
}
size_t prv_create_test_sequence(GDrawCommandSequence **sequence_ptr) {
size_t size = sizeof(GDrawCommandSequence) + (sizeof(GDrawCommandFrame) * 2) +
(sizeof(GDrawCommand) * 4) + (sizeof(GPoint) * 9);
GDrawCommandSequence *sequence = calloc(1, size);
*sequence_ptr = sequence;
*sequence = (GDrawCommandSequence){
.version = GDRAW_COMMAND_VERSION,
.num_frames = 2,
.play_count = 1,
};
GDrawCommandFrame *frame;
frame = &sequence->frames[0];
*frame = (GDrawCommandFrame) {
.duration = 15,
};
frame->command_list = (GDrawCommandList) {
.num_commands = 3
};
GDrawCommand *command;
command = gdraw_command_list_get_command(&frame->command_list, 0);
GPoint points1[] = { { 3, 97 }, {5, 5} };
*command = (GDrawCommand) {
.type = GDrawCommandTypePath,
.hidden = false,
.stroke_color = GColorRed,
.stroke_width = 1,
.fill_color = GColorBlue,
.path_open = false,
.num_points = ARRAY_LENGTH(points1),
};
memcpy(command->points, points1, sizeof(points1));
command = gdraw_command_list_get_command(&frame->command_list, 1);
*command = (GDrawCommand) {
.type = GDrawCommandTypeCircle,
.hidden = false,
.stroke_color = GColorGreen,
.stroke_width = 1,
.fill_color = GColorOrange,
.radius = 300,
.num_points = 1,
};
command->points[0] = (GPoint) { 1, 2 };
command = gdraw_command_list_get_command(&frame->command_list, 2);
GPoint points2[] = { { 6, 7 }, {5, 5}, { 0, 0 } };
*command = (GDrawCommand) {
.type = GDrawCommandTypePath,
.hidden = false,
.stroke_color = GColorGreen,
.stroke_width = 1,
.fill_color = GColorPurple,
.path_open = false,
.num_points = ARRAY_LENGTH(points2),
};
memcpy(command->points, points2, sizeof(points2));
frame = (GDrawCommandFrame *)(command->points + command->num_points);
*frame = (GDrawCommandFrame) {
.duration = 30,
};
frame->command_list = (GDrawCommandList) {
.num_commands = 1
};
command = gdraw_command_list_get_command(&frame->command_list, 0);
points2[0].x++; // increment x value to distinguish draw command from command in previous frame
*command = (GDrawCommand) {
.type = GDrawCommandTypePath,
.hidden = false,
.stroke_color = GColorRed,
.stroke_width = 5,
.fill_color = GColorBlack,
.path_open = false,
.num_points = ARRAY_LENGTH(points2),
};
memcpy(command->points, points2, sizeof(points2));
return size;
}
// tests
void test_gdraw_command_sequence__validate(void) {
GDrawCommandSequence *sequence;
size_t size = prv_create_test_sequence(&sequence);
cl_assert_equal_i(size, gdraw_command_sequence_get_data_size(sequence));
cl_assert(gdraw_command_sequence_validate(sequence, size));
cl_assert(!gdraw_command_sequence_validate(sequence, size - 1));
cl_assert(!gdraw_command_sequence_validate(sequence, size + 1));
cl_assert(!gdraw_command_sequence_validate(sequence, 0));
sequence->num_frames = 0;
cl_assert(!gdraw_command_sequence_validate(sequence, size));
sequence->num_frames = 1;
cl_assert(!gdraw_command_sequence_validate(sequence, size));
sequence->num_frames = 3;
cl_assert(!gdraw_command_sequence_validate(sequence, size));
sequence->num_frames = 2;
sequence->version = 0xFF;
cl_assert(!gdraw_command_sequence_validate(sequence, size));
free(sequence);
}
void test_gdraw_command_sequence__get_frame_by_elapsed(void) {
GDrawCommandSequence *sequence;
prv_create_test_sequence(&sequence);
GDrawCommandFrame *frame = gdraw_command_sequence_get_frame_by_elapsed(sequence, 0);
cl_assert_equal_p(frame, gdraw_command_sequence_get_frame_by_index(sequence, 0));
cl_assert_equal_p(frame, gdraw_command_sequence_get_frame_by_elapsed(sequence, 14));
frame = gdraw_command_sequence_get_frame_by_elapsed(sequence, 15);
cl_assert_equal_p(frame, gdraw_command_sequence_get_frame_by_index(sequence, 1));;
cl_assert_equal_p(frame, gdraw_command_sequence_get_frame_by_elapsed(sequence, 44));
cl_assert_equal_p(frame, gdraw_command_sequence_get_frame_by_elapsed(sequence, 45));
cl_assert_equal_p(frame, gdraw_command_sequence_get_frame_by_elapsed(sequence, 46));
// test that frame is skipped when the duration is zero (first frame shown will be the first one
// with non-zero duration
frame = gdraw_command_sequence_get_frame_by_elapsed(sequence, 0);
frame->duration = 0;
frame = gdraw_command_sequence_get_frame_by_elapsed(sequence, 0);
cl_assert_equal_p(frame, gdraw_command_sequence_get_frame_by_index(sequence, 1));
frame = gdraw_command_sequence_get_frame_by_index(sequence, 0);
frame->duration = 15;
// test that the sequence loops when the play count is greater than 1
sequence->play_count = 2;
frame = gdraw_command_sequence_get_frame_by_elapsed(sequence, 45);
cl_assert_equal_p(frame, gdraw_command_sequence_get_frame_by_index(sequence, 0));
frame = gdraw_command_sequence_get_frame_by_elapsed(sequence, 45 + 15);
cl_assert_equal_p(frame, gdraw_command_sequence_get_frame_by_index(sequence, 1));
// test that the sequence loops infinitely when the play count is infinite
sequence->play_count = (uint16_t)PLAY_COUNT_INFINITE;
frame = gdraw_command_sequence_get_frame_by_elapsed(sequence, 45 * 5);
cl_assert_equal_p(frame, gdraw_command_sequence_get_frame_by_index(sequence, 0));
frame = gdraw_command_sequence_get_frame_by_elapsed(sequence, (45 + 15) * 5);
cl_assert_equal_p(frame, gdraw_command_sequence_get_frame_by_index(sequence, 1));
// test that the sequence returns the last frame if the play count is zero
sequence->play_count = 0;
frame = gdraw_command_sequence_get_frame_by_elapsed(sequence, 1);
cl_assert_equal_p(frame, gdraw_command_sequence_get_frame_by_index(sequence, 1));
free(sequence);
}
void test_gdraw_command_sequence__get_frame_by_index(void) {
GDrawCommandSequence *sequence;
prv_create_test_sequence(&sequence);
GDrawCommandFrame *frame = gdraw_command_sequence_get_frame_by_index(sequence, 0);
cl_assert_equal_i(frame->duration, 15);
cl_assert_equal_i(frame->command_list.num_commands, 3);
GDrawCommand *command = gdraw_command_list_get_command(&frame->command_list, 2);
cl_assert_equal_i(command->type, GDrawCommandTypePath);
cl_assert_equal_i(command->num_points, 3);
cl_assert_equal_i(command->stroke_color.argb, GColorGreenARGB8);
cl_assert_equal_i(command->fill_color.argb, GColorPurpleARGB8);
cl_assert_equal_i(command->points[0].x, 6);
frame = gdraw_command_sequence_get_frame_by_index(sequence, 1);
cl_assert_equal_i(frame->duration, 30);
cl_assert_equal_i(frame->command_list.num_commands, 1);
command = gdraw_command_list_get_command(&frame->command_list, 0);
cl_assert_equal_i(command->type, GDrawCommandTypePath);
cl_assert_equal_i(command->num_points, 3);
cl_assert_equal_i(command->stroke_color.argb, GColorRedARGB8);
cl_assert_equal_i(command->fill_color.argb, GColorBlackARGB8);
cl_assert_equal_i(command->points[0].x, 7);
cl_assert_equal_p(gdraw_command_sequence_get_frame_by_index(sequence, 2), NULL);
free(sequence);
}
void test_gdraw_command_sequence__clone(void) {
cl_assert_equal_p(gdraw_command_sequence_clone(NULL), NULL);
GDrawCommandSequence *sequence;
prv_create_test_sequence(&sequence);
GDrawCommandSequence *clone = gdraw_command_sequence_clone(sequence);
cl_assert(clone != sequence);
size_t expected_size = gdraw_command_sequence_get_data_size(sequence);
cl_assert_equal_i(gdraw_command_sequence_get_data_size(clone), expected_size);
cl_assert_equal_i(0, memcmp(clone, sequence, expected_size));
free(sequence);
}
@@ -0,0 +1,244 @@
/*
* 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/graphics/gdraw_command_private.h"
#include "applib/graphics/graphics.h"
#include "applib/graphics/graphics_line.h"
#include "applib/graphics/gtypes.h"
#include "applib/graphics/framebuffer.h"
#include "applib/ui/animation.h"
#include "applib/ui/animation_timing.h"
#include "applib/ui/animation_interpolate.h"
#include "applib/graphics/gdraw_command_transforms.h"
#include "util.h"
#include "test_graphics.h"
#include "8bit/test_framebuffer.h"
#include "weather_app_resources.h"
// Stubs
////////////////////////////////////
#include "stubs_applib_resource.h"
#include "stubs_app_state.h"
#include "stubs_heap.h"
#include "stubs_logging.h"
#include "stubs_memory_layout.h"
#include "stubs_passert.h"
#include "stubs_pbl_malloc.h"
#include "stubs_resources.h"
#include "stubs_syscalls.h"
#include <stdio.h>
// stubs
InterpolateInt64Function animation_private_current_interpolate_override(void) {
return NULL;
}
static FrameBuffer *fb = NULL;
// Setup
void test_gdraw_command_transforms__initialize(void) {
fb = malloc(sizeof(FrameBuffer));
framebuffer_init(fb, &(GSize) {DISP_COLS, DISP_ROWS});
}
// Teardown
void test_gdraw_command_transforms__cleanup(void) {
free(fb);
}
void test_gdraw_command_transforms__to_square(void) {
GContext ctx;
test_graphics_context_init(&ctx, fb);
graphics_context_set_fill_color(&ctx, GColorFromHEX(0x55aaff));
graphics_context_set_antialiased(&ctx, true);
graphics_fill_rect(&ctx, &ctx.dest_bitmap.bounds);
GDrawCommandImage *l = weather_app_resource_create_sun();
gdraw_command_image_draw(&ctx, l, GPoint(0, 0));
int32_t dt = ANIMATION_NORMALIZED_MAX / 5;
int32_t t = 0;
l = weather_app_resource_create_sun();
gdraw_command_image_attract_to_square(l, t += dt);
gdraw_command_image_draw(&ctx, l, GPoint(48, 0));
l = weather_app_resource_create_sun();
gdraw_command_image_attract_to_square(l, t += dt);
gdraw_command_image_draw(&ctx, l, GPoint(0, 48));
l = weather_app_resource_create_sun();
gdraw_command_image_attract_to_square(l, t += dt);
gdraw_command_image_draw(&ctx, l, GPoint(48, 48));
l = weather_app_resource_create_sun();
gdraw_command_image_attract_to_square(l, t += dt);
gdraw_command_image_draw(&ctx, l, GPoint(0, 96));
l = weather_app_resource_create_sun();
gdraw_command_image_attract_to_square(l, t += dt);
gdraw_command_image_draw(&ctx, l, GPoint(48, 96));
cl_check(gbitmap_pbi_eq(&ctx.dest_bitmap, "test_gdraw_command_transforms__to_square.8bit.pbi"));
}
// re-enable this "test" to debug per-frame transitions
void _test_gdraw_command_transforms__to_square_sequence(void) {
GContext ctx;
test_graphics_context_init(&ctx, fb);
graphics_context_set_antialiased(&ctx, true);
int32_t dt = ANIMATION_NORMALIZED_MAX / 16;
int32_t t = 0;
while (t <= ANIMATION_NORMALIZED_MAX) {
graphics_context_set_fill_color(&ctx, GColorFromHEX(0x55aaff));
graphics_fill_rect(&ctx, &ctx.dest_bitmap.bounds);
GDrawCommandImage *img = weather_app_resource_create_sun();
gdraw_command_image_attract_to_square(img, t);
gdraw_command_image_draw(&ctx, img, GPoint(48, 96));
char fn[20];
snprintf(fn, sizeof(fn), "tos_%06d.png", t);
tests_write_gbitmap_to_pbi(&ctx.dest_bitmap, fn);
free(img);
t += dt;
}
}
int16_t prv_int_scale_and_translate_to(
int16_t value, int16_t size, int16_t from_range, int16_t to_range,
int16_t from_min, int16_t to_min, int32_t normalized, InterpolateInt64Function interpolate);
int64_t prv_default_interpolate(int32_t normalized, int64_t from, int64_t to);
void test_gdraw_command_transforms__int_scale_to_translate_overflow(void) {
InterpolateInt64Function interp = prv_default_interpolate;
int y = prv_int_scale_and_translate_to(255, 10, 10, 10, 0, 255, ANIMATION_NORMALIZED_MAX,
interp);
cl_assert_equal_i(y, 255 + 255);
}
void test_gdraw_command_transforms__int_scale_to_translate_overflow_neg(void) {
InterpolateInt64Function interp = prv_default_interpolate;
int y = prv_int_scale_and_translate_to(-255, 10, 10, 10, 0, -255, ANIMATION_NORMALIZED_MAX,
interp);
cl_assert_equal_i(y, -255 + -255);
}
void test_gdraw_command_transforms__int_scale_to_scale_overflow(void) {
InterpolateInt64Function interp = prv_default_interpolate;
int y = prv_int_scale_and_translate_to(181, 1, 1, 181, 0, 0, ANIMATION_NORMALIZED_MAX,
interp);
cl_assert_equal_i(y, 181 * 181);
}
void test_gdraw_command_transforms__int_scale_to_scale_overflow_neg(void) {
InterpolateInt64Function interp = prv_default_interpolate;
int y = prv_int_scale_and_translate_to(-181, 1, 1, 181, 0, 0, ANIMATION_NORMALIZED_MAX,
interp);
cl_assert_equal_i(y, -181 * 181);
}
void test_gdraw_command_transforms__segmented_scale(void) {
GContext ctx;
test_graphics_context_init(&ctx, fb);
graphics_context_set_fill_color(&ctx, GColorFromHEX(0x55aaff));
graphics_context_set_antialiased(&ctx, true);
graphics_fill_rect(&ctx, &ctx.dest_bitmap.bounds);
GDrawCommandImage *l = weather_app_resource_create_sun();
gdraw_command_image_draw(&ctx, l, GPointZero);
int32_t dt = ANIMATION_NORMALIZED_MAX / 5;
int32_t t = 0;
InterpolateInt64Function interp = NULL;
int16_t s = 48;
GRect from = GRect(0, 0, s, s);
GRect to = GRect(90, 0, s, s);
GPointIndexLookup *index_lookup = gdraw_command_list_create_index_lookup_by_distance(
gdraw_command_image_get_command_list(l), GPoint(s, s / 2));
Fixed_S32_16 f = Fixed_S32_16(FIXED_S32_16_ONE.raw_value / 2);
l = weather_app_resource_create_sun();
gdraw_command_image_scale_segmented_to(l, from, to, t += dt, interp, index_lookup, f, false);
gdraw_command_image_draw(&ctx, l, GPoint(0, 0));
l = weather_app_resource_create_sun();
gdraw_command_image_scale_segmented_to(l, from, to, t += dt, interp, index_lookup, f, false);
gdraw_command_image_draw(&ctx, l, GPoint(0, 48));
l = weather_app_resource_create_sun();
gdraw_command_image_scale_segmented_to(l, from, to, t += dt, interp, index_lookup, f, false);
gdraw_command_image_draw(&ctx, l, GPoint(0, 96));
l = weather_app_resource_create_sun();
gdraw_command_image_scale_segmented_to(l, from, to, t += dt, interp, index_lookup, f, false);
gdraw_command_image_draw(&ctx, l, GPoint(0, 144));
l = weather_app_resource_create_sun();
gdraw_command_image_scale_segmented_to(l, from, to, t += dt, interp, index_lookup, f, false);
gdraw_command_image_draw(&ctx, l, GPoint(0, 192));
free(index_lookup);
cl_check(gbitmap_pbi_eq(&ctx.dest_bitmap, "test_gdraw_command_transforms__segmented_scale.8bit.pbi"));
}
// re-enable this "test" to debug per-frame transitions
void _test_gdraw_command_transforms__scale_segmented_sequence(void) {
GContext ctx;
test_graphics_context_init(&ctx, fb);
graphics_context_set_antialiased(&ctx, true);
int32_t dt = ANIMATION_NORMALIZED_MAX / 16;
int32_t t = 0;
InterpolateInt64Function interp = NULL;
int16_t s = 48;
GRect from = GRect(144 - s * 3 / 4, s / 4, s / 2, s / 2);
GRect to = GRect(s / 2, s, 2 * s, 2 * s);
GDrawCommandImage *img = weather_app_resource_create_cloud();
GPointIndexLookup *index_lookup = gdraw_command_list_create_index_lookup_by_distance(
gdraw_command_image_get_command_list(img), GPoint(s / 2, s));
free(img);
Fixed_S32_16 f = Fixed_S32_16(FIXED_S32_16_ONE.raw_value / 8);
while (t <= ANIMATION_NORMALIZED_MAX) {
graphics_context_set_fill_color(&ctx, GColorFromHEX(0x55aaff));
graphics_fill_rect(&ctx, &ctx.dest_bitmap.bounds);
img = weather_app_resource_create_cloud();
gdraw_command_image_scale_segmented_to(img, from, to, t, interp, index_lookup, f, false);
gdraw_command_image_draw(&ctx, img, GPointZero);
char fn[20];
snprintf(fn, sizeof(fn), "elo_%06d.png", t);
tests_write_gbitmap_to_pbi(&ctx.dest_bitmap, fn);
t += dt;
}
}
+40
View File
@@ -0,0 +1,40 @@
/*
* 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/graphics.h"
#include "applib/graphics/framebuffer.h"
#include "stubs_compiled_with_legacy2_sdk.h"
#include "stubs_app_state.h"
#define GRAPHICS_FIXTURE_PATH "graphics"
#define GRAPHICS_FIXTURE_OUT_PATH ".."
extern GContext *s_app_state_get_graphics_context;
static void test_graphics_context_reset(GContext* ctx, FrameBuffer* framebuffer) {
graphics_context_init(ctx, framebuffer, GContextInitializationMode_App);
framebuffer_clear(framebuffer);
}
static void test_graphics_context_init(GContext* ctx, FrameBuffer* framebuffer) {
test_graphics_context_reset(ctx, framebuffer);
s_app_state_get_graphics_context = ctx; // needed by app_state_get_graphics_context
graphics_context_set_antialiased(ctx, false);
}
+114
View File
@@ -0,0 +1,114 @@
/*
* 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/graphics/gtypes.h"
#include "applib/graphics/graphics.h"
#include "stubs_applib_resource.h"
#include "stubs_app_state.h"
#include "stubs_compiled_with_legacy2_sdk.h"
#include "stubs_graphics_line.h"
#include "stubs_heap.h"
#include "stubs_passert.h"
#include "stubs_pbl_malloc.h"
#include "stubs_logging.h"
const int FrameBuffer_MaxX = 144;
const int FrameBuffer_MaxY = 85;
// stubs
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 cos_lookup(){}
void sin_lookup(){}
void framebuffer_dirty_all(){}
void framebuffer_mark_dirty_rect(){}
void graphics_circle_draw_quadrant(GContext* ctx, int x0, int y0, uint16_t radius,
GCornerMask quadrant){}
void graphics_circle_quadrant_draw_1px_non_aa(GContext* ctx, GPoint p,
uint16_t radius, GCornerMask quadrant) {}
void graphics_internal_circle_quadrant_fill_aa(GContext* ctx, GPoint p,
uint16_t radius, GCornerMask quadrant) {}
void graphics_circle_quadrant_draw(GContext* ctx, GPoint p, uint16_t radius,
GCornerMask quadrant) {}
void graphics_circle_quadrant_fill_non_aa(GContext* ctx, GPoint p,
uint16_t radius, GCornerMask quadrant) {}
void sys_get_current_resource_num(){}
void sys_resource_read_only_bytes(){}
void sys_resource_load_range(){}
void sys_resource_size(){}
int32_t integer_sqrt(int64_t x){ return 0;}
GBitmap framebuffer_get_as_bitmap(FrameBuffer *fb, const GSize *size) {
return (GBitmap) {
.addr = fb,
.row_size_bytes = FrameBuffer_MaxX,
.info = (BitmapInfo) {.format = GBitmapFormat8Bit, .version = GBITMAP_VERSION_CURRENT},
.bounds = (GRect) { { 0, 0 }, { FrameBuffer_MaxX, FrameBuffer_MaxY } },
};
}
// tests
void test_graphics_blending__closest_opaque(void) {
cl_assert_equal_i(GColorRedARGB8, gcolor_closest_opaque((GColor8){.a= 3, .r = 3}).argb);
cl_assert_equal_i(GColorRedARGB8, gcolor_closest_opaque((GColor8){.a= 2, .r = 3}).argb);
cl_assert_equal_i(GColorClearARGB8, gcolor_closest_opaque((GColor8){.a= 1, .r = 3}).argb);
cl_assert_equal_i(GColorClearARGB8, gcolor_closest_opaque((GColor8){.a= 0, .r = 3}).argb);
}
void test_graphics_blending__ctx_text_color_discards_alpha(void) {
GContext ctx = {};
graphics_context_set_text_color(&ctx, (GColor8){.a= 3, .r = 3});
cl_assert_equal_i(GColorRedARGB8, ctx.draw_state.text_color.argb);
graphics_context_set_text_color(&ctx, (GColor8){.a= 2, .r = 3});
cl_assert_equal_i(GColorRedARGB8, ctx.draw_state.text_color.argb);
graphics_context_set_text_color(&ctx, (GColor8){.a= 1, .r = 3});
cl_assert_equal_i(GColorClearARGB8, ctx.draw_state.text_color.argb);
graphics_context_set_text_color(&ctx, (GColor8){.a= 0, .r = 3});
cl_assert_equal_i(GColorClearARGB8, ctx.draw_state.text_color.argb);
}
void test_graphics_blending__ctx_stroke_color_discards_alpha(void) {
GContext ctx = {};
graphics_context_set_stroke_color(&ctx, (GColor8){.a= 3, .r = 3});
cl_assert_equal_i(GColorRedARGB8, ctx.draw_state.stroke_color.argb);
graphics_context_set_stroke_color(&ctx, (GColor8){.a= 2, .r = 3});
cl_assert_equal_i(GColorRedARGB8, ctx.draw_state.stroke_color.argb);
graphics_context_set_stroke_color(&ctx, (GColor8){.a= 1, .r = 3});
cl_assert_equal_i(GColorClearARGB8, ctx.draw_state.stroke_color.argb);
graphics_context_set_stroke_color(&ctx, (GColor8){.a= 0, .r = 3});
cl_assert_equal_i(GColorClearARGB8, ctx.draw_state.stroke_color.argb);
}
void test_graphics_blending__ctx_fill_color_discards_alpha(void) {
GContext ctx = {};
graphics_context_set_fill_color(&ctx, (GColor8){.a= 3, .r = 3});
cl_assert_equal_i(GColorRedARGB8, ctx.draw_state.fill_color.argb);
graphics_context_set_fill_color(&ctx, (GColor8){.a= 2, .r = 3});
cl_assert_equal_i(GColorRedARGB8, ctx.draw_state.fill_color.argb);
graphics_context_set_fill_color(&ctx, (GColor8){.a= 1, .r = 3});
cl_assert_equal_i(GColorClearARGB8, ctx.draw_state.fill_color.argb);
graphics_context_set_fill_color(&ctx, (GColor8){.a= 0, .r = 3});
cl_assert_equal_i(GColorClearARGB8, ctx.draw_state.fill_color.argb);
}
+417
View File
@@ -0,0 +1,417 @@
/*
* 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/graphics/graphics_circle.h"
#include "util/trig.h"
#include "clar.h"
#include "pebble_asserts.h"
#include <stdio.h>
// stubs
#include "stubs_process_manager.h"
#include "stubs_passert.h"
#include "stubs_logging.h"
#include "stubs_app_state.h"
#include "stubs_heap.h"
GBitmap* graphics_capture_frame_buffer(GContext* ctx) {
return NULL;
}
bool graphics_release_frame_buffer(GContext* ctx, GBitmap* buffer) {
return true;
}
void graphics_draw_pixel(){}
void graphics_fill_rect(GContext* ctx, const GRect *rect) {}
void graphics_private_draw_horizontal_line(){}
void graphics_private_draw_vertical_line(){}
void graphics_private_plot_pixel(){}
void graphics_private_set_pixel(){}
/////////////////////////////
static GPointPrecise s_center;
static Fixed_S16_3 s_radius;
static Fixed_S16_3 s_radius_inner;
static Fixed_S16_3 s_radius_outer;
static int32_t s_angle_start;
static int32_t s_angle_end;
////////////////////////////
void test_graphics_circle__initialize(void) {
s_center = (GPointPrecise){};
s_radius = (Fixed_S16_3){};
s_radius_inner = (Fixed_S16_3){};
s_radius_outer = (Fixed_S16_3){};
}
void test_graphics_circle__gpoint_from_polar_returns_zero_for_null(void) {
const uint16_t radius = 5;
const GPoint result = gpoint_from_polar_internal(NULL, radius, 0);
cl_assert_equal_gpoint(result, GPointZero);
}
void test_graphics_circle__gpoint_from_polar_returns_correct_points(void) {
const uint16_t radius = 5;
GPoint result;
const GPoint origin_center = GPointZero;
// 90 degrees should be (5, 0)
result = gpoint_from_polar_internal(&origin_center, radius, (TRIG_MAX_ANGLE / 4));
cl_assert_equal_gpoint(result, GPoint(5, 0));
// 270 (90 * 3) degrees should be (-5, 0)
result = gpoint_from_polar_internal(&origin_center, radius, (TRIG_MAX_ANGLE * 3 / 4));
cl_assert_equal_gpoint(result, GPoint(-5, 0));
const GPoint offset_center = GPoint(1, 1);
// 90 degrees should be (6, 1)
result = gpoint_from_polar_internal(&offset_center, radius, (TRIG_MAX_ANGLE / 4));
cl_assert_equal_gpoint(result, GPoint(6, 1));
// 270 (90 * 3) degrees should be (-4, 1)
result = gpoint_from_polar_internal(&offset_center, radius, (TRIG_MAX_ANGLE * 3 / 4));
cl_assert_equal_gpoint(result, GPoint(-4, 1));
}
void test_graphics_circle__gpoint_from_polar_normalizes_input_angles(void) {
const uint16_t radius = 5;
const GPoint center = GPointZero;
GPoint result;
// -180 degrees should be (0, 5)
result = gpoint_from_polar_internal(&center, radius, -(TRIG_MAX_ANGLE / 2));
cl_assert_equal_gpoint(result, GPoint(0, 5));
// -90 degrees should be (-5, 0)
result = gpoint_from_polar_internal(&center, radius, -(TRIG_MAX_ANGLE / 4));
cl_assert_equal_gpoint(result, GPoint(-5, 0));
// -450 degrees (-90 * 5 -> -90) should be (-5, 0)
result = gpoint_from_polar_internal(&center, radius, -(TRIG_MAX_ANGLE * 5 / 4));
cl_assert_equal_gpoint(result, GPoint(-5, 0));
// 450 degrees (90 * 5 -> 90) should be (0, 5)
result = gpoint_from_polar_internal(&center, radius, (TRIG_MAX_ANGLE * 5 / 4));
cl_assert_equal_gpoint(result, GPoint(5, 0));
// 360 degrees (-> 0 degrees) should be (0, -5)
result = gpoint_from_polar_internal(&center, radius, TRIG_MAX_ANGLE);
cl_assert_equal_gpoint(result, GPoint(0, -5));
// -360 degrees (-> 0 degrees) should be (0, -5)
result = gpoint_from_polar_internal(&center, radius, -TRIG_MAX_ANGLE);
cl_assert_equal_gpoint(result, GPoint(0, -5));
}
void test_graphics_circle__gpoint_from_polar_correct_scale(void) {
// edge cases are covered above, this test only verifies that
// the internal implementation correctly scales
GPoint result = gpoint_from_polar(GRect(0, 0, 10, 10), GOvalScaleModeFillCircle, 0);
cl_assert_equal_gpoint(result, GPoint(4, 0));
}
void test_graphics_circle__grect_centered_from_polar(void) {
GOvalScaleMode mode = GOvalScaleModeFillCircle;
// Basic container rect with GPointZero origin
const GRect container_rect1 = GRect(0, 0, 10, 10);
const GRect resulting_rect1 = grect_centered_from_polar(container_rect1, mode, 0, GSize(3, 5));
cl_assert_equal_grect(resulting_rect1, GRect(3, -2, 3, 5));
// Container rect is offset from GPointZero
const GRect container_rect2 = GRect(2, 2, 4, 4);
const GRect resulting_rect2 = grect_centered_from_polar(container_rect2, mode, 0, GSize(2, 4));
cl_assert_equal_grect(resulting_rect2, GRect(3, 0, 2, 4));
// Odd-length width and height for container rect, 180 degree angle
const GRect container_rect3 = GRect(2, 2, 5, 5);
const GRect resulting_rect3 = grect_centered_from_polar(container_rect3, mode,
DEG_TO_TRIGANGLE(180), GSize(2, 4));
cl_assert_equal_grect(resulting_rect3, GRect(3, 4, 2, 4));
}
void test_graphics_circle__grect_centered_internal(void) {
GPointPrecise p1 = GPointPrecise(0, 0);
// GRectZero + standardize
cl_assert_equal_grect(GRect(0, 0, 0, 0), grect_centered_internal(&p1, GSize(0, 0)));
cl_assert_equal_grect(GRect(0, -1, 1, 2), grect_centered_internal(&p1, GSize(-1, -2)));
// handles fixed point
cl_assert_equal_grect(GRect(-1, -1, 2, 2), grect_centered_internal(&p1, GSize(2, 2)));
p1.x.raw_value += FIXED_S16_3_HALF.raw_value;
cl_assert_equal_grect(GRect(0, -1, 2, 2), grect_centered_internal(&p1, GSize(2, 2)));
// Repeat for an offset center point
GPointPrecise p2 = GPointPreciseFromGPoint(GPoint(5, 5));
// GRectZero + standardize
cl_assert_equal_grect(GRect(5, 5, 0, 0), grect_centered_internal(&p2, GSize(0, 0)));
cl_assert_equal_grect(GRect(5, 4, 1, 2), grect_centered_internal(&p2, GSize(-1, -2)));
// handles fixed point
cl_assert_equal_grect(GRect(4, 4, 2, 2), grect_centered_internal(&p2, GSize(2, 2)));
p2.x.raw_value += FIXED_S16_3_HALF.raw_value;
cl_assert_equal_grect(GRect(5, 4, 2, 2), grect_centered_internal(&p2, GSize(2, 2)));
// Repeat for a positive offset center point with 0.5 fractions
GPointPrecise p3 = GPointPreciseFromGPoint(GPoint(5, 5));
p3.x.raw_value += FIXED_S16_3_HALF.raw_value;
p3.y.raw_value += FIXED_S16_3_HALF.raw_value;
// GRectZero + standardize
cl_assert_equal_grect(GRect(6, 6, 0, 0), grect_centered_internal(&p3, GSize(0, 0)));
cl_assert_equal_grect(GRect(5, 5, 1, 2), grect_centered_internal(&p3, GSize(-1, -2)));
// handles fixed point
cl_assert_equal_grect(GRect(5, 5, 2, 2), grect_centered_internal(&p3, GSize(2, 2)));
p3.x.raw_value += FIXED_S16_3_HALF.raw_value;
cl_assert_equal_grect(GRect(5, 5, 2, 2), grect_centered_internal(&p3, GSize(2, 2)));
// Repeat for a negative offset center point with 0.5 fractions
GPointPrecise p4 = GPointPreciseFromGPoint(GPoint(-5, -5));
p4.x.raw_value -= FIXED_S16_3_HALF.raw_value;
p4.y.raw_value -= FIXED_S16_3_HALF.raw_value;
// GRectZero + standardize
cl_assert_equal_grect(GRect(-5, -5, 0, 0), grect_centered_internal(&p4, GSize(0, 0)));
cl_assert_equal_grect(GRect(-6, -6, 1, 2), grect_centered_internal(&p4, GSize(-1, -2)));
// handles fixed point
cl_assert_equal_grect(GRect(-6, -6, 2, 2), grect_centered_internal(&p4, GSize(2, 2)));
p4.x.raw_value += FIXED_S16_3_HALF.raw_value;
cl_assert_equal_grect(GRect(-6, -6, 2, 2), grect_centered_internal(&p4, GSize(2, 2)));
}
#define cl_assert_fixedS16_3(v, f) \
cl_assert_equal_i((v).raw_value, (int)((f) * FIXED_S16_3_ONE.raw_value))
#define cl_assert_gpoint_precise(p, px, py) \
do { \
cl_assert_fixedS16_3(p.x, px); \
cl_assert_fixedS16_3(p.y, py); \
} while(0)
void test_graphics_circle__grect_polar_calc_values_handles_null(void) {
GPointPrecise center = {};
Fixed_S16_3 radius = {};
const GRect r = GRect(0, 0, 3, 5);
const GOvalScaleMode mode = GOvalScaleModeFitCircle;
grect_polar_calc_values(&r, mode, NULL, NULL);
grect_polar_calc_values(&r, mode, &center, NULL);
grect_polar_calc_values(&r, mode, NULL, &radius);
cl_assert_gpoint_precise(center, 1, 2);
cl_assert_fixedS16_3(radius, 1);
grect_polar_calc_values(NULL, mode, &center, &radius);
cl_assert_gpoint_precise(center, 1, 2);
cl_assert_fixedS16_3(radius, 1);
}
void test_graphics_circle__grect_polar_calc_values_edge_cases(void) {
GPointPrecise center = {};
Fixed_S16_3 radius = {};
const GOvalScaleMode mode = GOvalScaleModeFillCircle;
GRect r = GRect(0, 5, 0, 0);
grect_polar_calc_values(&r, mode, &center, &radius);
cl_assert_gpoint_precise(center, 0, 5);
cl_assert_fixedS16_3(radius, 0);
// 1 pixel width means radius of 0
r = GRect(-1, -5, 1, 1);
grect_polar_calc_values(&r, mode, &center, &radius);
cl_assert_gpoint_precise(center, -1, -5);
cl_assert_fixedS16_3(radius, 0);
// 2 pixel width means: center is 1px from side, 0.5 pixels to center of outer pixels
r = GRect(-1, -5, 2, 2);
grect_polar_calc_values(&r, mode, &center, &radius);
cl_assert_gpoint_precise(center, -0.5, -4.5);
cl_assert_fixedS16_3(radius, 0.5);
}
void test_graphics_circle__grect_polar_calc_values_standardizes(void) {
GPointPrecise center = {};
Fixed_S16_3 radius = {};
const GOvalScaleMode mode = GOvalScaleModeFitCircle;
GRect r = GRect(0, 0, 10, 20);
grect_polar_calc_values(&r, mode, &center, &radius);
cl_assert_gpoint_precise(center, 4.5, 9.5);
cl_assert_fixedS16_3(radius, 4.5);
r = GRect(0, 0, -10, -20);
grect_polar_calc_values(&r, mode, &center, &radius);
cl_assert_gpoint_precise(center, -5.5, -10.5);
cl_assert_fixedS16_3(radius, 4.5);
}
void test_graphics_circle__grect_polar_calc_values_square(void) {
GPointPrecise center = {};
Fixed_S16_3 radius = {};
GRect r = GRect(0, 0, 5, 5);
GOvalScaleMode mode = GOvalScaleModeFitCircle; // irrelevant as we deal with squares
grect_polar_calc_values(&r, mode, &center, &radius);
cl_assert_gpoint_precise(center, 2, 2);
cl_assert_fixedS16_3(radius, 2);
r = GRect(0, 0, 6, 6);
grect_polar_calc_values(&r, mode, &center, &radius);
cl_assert_gpoint_precise(center, 2.5, 2.5);
cl_assert_fixedS16_3(radius, 2.5);
r = GRect(0, 0, 10, 10);
grect_polar_calc_values(&r, mode, &center, &radius);
cl_assert_gpoint_precise(center, 4.5, 4.5);
cl_assert_fixedS16_3(radius, 4.5);
r = GRect(1, 1, 9, 9);
grect_polar_calc_values(&r, mode, &center, &radius);
cl_assert_gpoint_precise(center, 5, 5);
cl_assert_fixedS16_3(radius, 4);
r = GRect(2, 2, 8, 8);
grect_polar_calc_values(&r, mode, &center, &radius);
cl_assert_gpoint_precise(center, 5.5, 5.5);
cl_assert_fixedS16_3(radius, 3.5);
}
void test_graphics_circle__grect_polar_calc_values_mode(void) {
GPointPrecise center = {};
Fixed_S16_3 radius = {};
GRect r = GRect(0, 0, 144, 168);
grect_polar_calc_values(&r, GOvalScaleModeFitCircle, &center, &radius);
cl_assert_gpoint_precise(center, 144 / 2 - 0.5, 168 / 2 - 0.5);
cl_assert_fixedS16_3(radius, 144 / 2 - 0.5);
grect_polar_calc_values(&r, GOvalScaleModeFillCircle, &center, &radius);
cl_assert_gpoint_precise(center, 144 / 2 - 0.5, 168 / 2 - 0.5);
cl_assert_fixedS16_3(radius, 168 / 2 - 0.5);
}
void graphics_draw_arc_precise_internal(GContext *ctx, GPointPrecise center,
Fixed_S16_3 radius,
int32_t angle_start, int32_t angle_end) {
s_center = center;
s_radius = radius;
}
void test_graphics_circle__draw_arc(void) {
cl_assert_gpoint_precise(s_center, 0, 0);
cl_assert_fixedS16_3(s_radius, 0);
graphics_draw_arc(NULL, GRect(0, 0, 10, 12), GOvalScaleModeFitCircle, 0, 0);
cl_assert_gpoint_precise(s_center, 4.5, 5.5);
cl_assert_fixedS16_3(s_radius, 4.5);
}
void test_graphics_circle__fill_oval(void) {
cl_assert_gpoint_precise(s_center, 0, 0);
cl_assert_fixedS16_3(s_radius_inner, 0);
cl_assert_fixedS16_3(s_radius_outer, 0);
cl_assert_equal_i(s_angle_start, 0);
cl_assert_equal_i(s_angle_end, 0);
graphics_fill_oval(NULL, GRect(0, 0, 10, 12), GOvalScaleModeFitCircle);
cl_assert_gpoint_precise(s_center, 4.5, 5.5);
cl_assert_fixedS16_3(s_radius_outer, 4.5);
cl_assert(s_radius_inner.integer <= 0);
cl_assert_equal_i(s_angle_start, 0);
cl_assert_equal_i(s_angle_end, TRIG_MAX_ANGLE);
graphics_fill_oval(NULL, GRect(10, 12, -10, -12), GOvalScaleModeFitCircle);
cl_assert_gpoint_precise(s_center, 4.5, 5.5);
cl_assert_fixedS16_3(s_radius_outer, 4.5);
cl_assert(s_radius_inner.integer <= 0);
cl_assert_equal_i(s_angle_start, 0);
cl_assert_equal_i(s_angle_end, TRIG_MAX_ANGLE);
graphics_fill_oval(NULL, GRect(0, 0, 0, 0), GOvalScaleModeFillCircle);
cl_assert_gpoint_precise(s_center, 0.0, 0.0);
cl_assert_fixedS16_3(s_radius_outer, 0.0);
cl_assert(s_radius_inner.integer <= 0);
cl_assert_equal_i(s_angle_start, 0);
cl_assert_equal_i(s_angle_end, TRIG_MAX_ANGLE);
}
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) {
s_center = center;
s_radius_inner = radius_inner;
s_radius_outer = radius_outer;
s_angle_start = angle_start;
s_angle_end = angle_end;
}
void test_graphics_circle__fill_radial(void) {
GContext *ctx = (GContext *)&ctx;
cl_assert_gpoint_precise(s_center, 0, 0);
cl_assert_fixedS16_3(s_radius_inner, 0);
cl_assert_fixedS16_3(s_radius_outer, 0);
graphics_fill_radial(NULL, GRect(0, 0, 10, 12), GOvalScaleModeFitCircle, 3, 0, 0);
cl_assert_gpoint_precise(s_center, 4.5, 5.5);
cl_assert_fixedS16_3(s_radius_outer, 4.5);
cl_assert_fixedS16_3(s_radius_inner, 1.5);
}
void test_graphics_circle__DEG_TO_TRIGANGLE(void) {
cl_assert_equal_i(DEG_TO_TRIGANGLE(720), TRIG_MAX_ANGLE * 2);
cl_assert_equal_i(DEG_TO_TRIGANGLE(-720), -TRIG_MAX_ANGLE * 2);
cl_assert_equal_i(DEG_TO_TRIGANGLE(360), TRIG_MAX_ANGLE);
cl_assert_equal_i(DEG_TO_TRIGANGLE(-360), -TRIG_MAX_ANGLE);
cl_assert_equal_i(DEG_TO_TRIGANGLE(180), TRIG_PI);
cl_assert_equal_i(DEG_TO_TRIGANGLE(-180), -TRIG_PI);
cl_assert_equal_i(DEG_TO_TRIGANGLE(90), TRIG_PI / 2);
cl_assert_equal_i(DEG_TO_TRIGANGLE(-90), -TRIG_PI / 2);
cl_assert_equal_i(DEG_TO_TRIGANGLE(0), 0);
}
void test_graphics_circle__TRIGANGLE_TO_DEG(void) {
cl_assert_equal_i(TRIGANGLE_TO_DEG(TRIG_MAX_ANGLE * 2), 720);
cl_assert_equal_i(TRIGANGLE_TO_DEG(-TRIG_MAX_ANGLE * 2), -720);
cl_assert_equal_i(TRIGANGLE_TO_DEG(TRIG_MAX_ANGLE), 360);
cl_assert_equal_i(TRIGANGLE_TO_DEG(-TRIG_MAX_ANGLE), -360);
cl_assert_equal_i(TRIGANGLE_TO_DEG(TRIG_PI / 2), 90);
cl_assert_equal_i(TRIGANGLE_TO_DEG(-TRIG_PI / 2), -90);
cl_assert_equal_i(TRIGANGLE_TO_DEG(TRIG_MAX_ANGLE / 2), 180);
cl_assert_equal_i(TRIGANGLE_TO_DEG(-TRIG_MAX_ANGLE / 2), -180);
cl_assert_equal_i(TRIGANGLE_TO_DEG(TRIG_PI), 180);
cl_assert_equal_i(TRIGANGLE_TO_DEG(-TRIG_PI), -180);
cl_assert_equal_i(TRIGANGLE_TO_DEG(0), 0);
}
+263
View File
@@ -0,0 +1,263 @@
/*
* 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/graphics/gtypes.h"
#include "clar.h"
#include "stubs_app_state.h"
#include "stubs_heap.h"
#include "stubs_passert.h"
#include "stubs_process_manager.h"
void test_graphics_colors__black(void) {
cl_assert(GColorBlack.r == 0b00);
cl_assert(GColorBlack.g == 0b00);
cl_assert(GColorBlack.b == 0b00);
cl_assert(GColorBlack.a == 0b11);
}
void test_graphics_colors__white(void) {
cl_assert(GColorWhite.r == 0b11);
cl_assert(GColorWhite.g == 0b11);
cl_assert(GColorWhite.b == 0b11);
cl_assert(GColorWhite.a == 0b11);
}
void test_graphics_colors__red(void) {
cl_assert(GColorRed.r == 0b11);
cl_assert(GColorRed.g == 0b00);
cl_assert(GColorRed.b == 0b00);
cl_assert(GColorRed.a == 0b11);
}
void test_graphics_colors__green(void) {
cl_assert(GColorGreen.r == 0b00);
cl_assert(GColorGreen.g == 0b11);
cl_assert(GColorGreen.b == 0b00);
cl_assert(GColorGreen.a == 0b11);
}
void test_graphics_colors__blue(void) {
cl_assert(GColorBlue.r == 0b00);
cl_assert(GColorBlue.g == 0b00);
cl_assert(GColorBlue.b == 0b11);
cl_assert(GColorBlue.a == 0b11);
}
void test_graphics_colors__equal(void) {
cl_assert(gcolor_equal(GColorBlue, GColorBlue));
cl_assert(gcolor_equal(GColorRed, GColorRed));
cl_assert(!gcolor_equal(GColorRed, GColorBlue));
// Two colors with zero alpha values should be equal regardless of what their RGB channels are
GColor8 color1 = GColorBlue;
color1.a = 0;
GColor8 color2 = GColorRed;
color2.a = 0;
cl_assert(gcolor_equal(color1, color2));
// But two colors with semi-transparent alpha values should not be equal if their RGB channels
// don't match
color1.a = 1;
color2.a = 1;
cl_assert(!gcolor_equal(color1, color2));
}
void test_graphics_colors__equal__deprecated(void) {
cl_assert(gcolor_equal__deprecated(GColorBlue, GColorBlue));
cl_assert(gcolor_equal__deprecated(GColorRed, GColorRed));
cl_assert(!gcolor_equal__deprecated(GColorRed, GColorBlue));
// Check the behavior of the deprecated function (which is incorrect) that two colors with
// zero alpha values but different RGB values are not considered equal
GColor8 color1 = GColorBlue;
color1.a = 0;
GColor8 color2 = GColorRed;
color2.a = 0;
cl_assert(!gcolor_equal__deprecated(color1, color2));
}
void test_graphics_colors__inverted_readable_color(void) {
GColor8 (*fun)(GColor8) = gcolor_legible_over;
// transparent colors result in transparen - who has a better idea?
cl_assert_equal_i(GColorClearARGB8, fun(GColorClear).argb);
// // obvious cases
cl_assert_equal_i(GColorWhiteARGB8, fun(GColorBlack).argb);
cl_assert_equal_i(GColorBlackARGB8, fun(GColorWhite).argb);
// // expectation as derived from Appfaces_all.psd
cl_assert_equal_i(GColorWhiteARGB8, fun(GColorFromHEX(0xff0000)).argb);
cl_assert_equal_i(GColorBlackARGB8, fun(GColorFromHEX(0x00aaff)).argb);
cl_assert_equal_i(GColorWhiteARGB8, fun(GColorFromHEX(0xaa0055)).argb);
cl_assert_equal_i(GColorBlackARGB8, fun(GColorFromHEX(0x55aa55)).argb);
cl_assert_equal_i(GColorBlackARGB8, fun(GColorFromHEX(0xff5555)).argb);
cl_assert_equal_i(GColorWhiteARGB8, fun(GColorFromHEX(0x0055aa)).argb);
cl_assert_equal_i(GColorBlackARGB8, fun(GColorFromHEX(0xff5500)).argb);
cl_assert_equal_i(GColorBlackARGB8, fun(GColorFromHEX(0xaaaaaa)).argb);
// // expectation as derived from Appfaces_generic_colors.psd
cl_assert_equal_i(GColorWhiteARGB8, fun(GColorFromHEX(0x0000aa)).argb);
cl_assert_equal_i(GColorWhiteARGB8, fun(GColorFromHEX(0x005500)).argb);
cl_assert_equal_i(GColorWhiteARGB8, fun(GColorFromHEX(0x550055)).argb);
cl_assert_equal_i(GColorWhiteARGB8, fun(GColorFromHEX(0xaa0000)).argb);
// contradiction with previous case - oh dear...
// cl_assert_equal_i(GColorWhiteARGB8, fun(GColorFromHEX(0xff5500)).argb);
cl_assert_equal_i(GColorWhiteARGB8, fun(GColorFromHEX(0xaa5500)).argb);
}
void test_graphics_colors__grayscale_colors(void) {
cl_assert_equal_i(GColorClearARGB8, gcolor_get_grayscale(GColorClear).argb);
cl_assert_equal_i(GColorWhiteARGB8, gcolor_get_grayscale(GColorWhite).argb);
cl_assert_equal_i(GColorBlackARGB8, gcolor_get_grayscale(GColorBlack).argb);
cl_assert_equal_i(GColorLightGrayARGB8, gcolor_get_grayscale(GColorLightGray).argb);
cl_assert_equal_i(GColorDarkGrayARGB8, gcolor_get_grayscale(GColorDarkGray).argb);
cl_assert_equal_i(GColorWhiteARGB8, gcolor_get_grayscale(GColorYellow).argb);
cl_assert_equal_i(GColorBlackARGB8, gcolor_get_grayscale(GColorBlue).argb);
cl_assert_equal_i(GColorLightGrayARGB8, gcolor_get_grayscale(GColorTiffanyBlue).argb);
cl_assert_equal_i(GColorDarkGrayARGB8, gcolor_get_grayscale(GColorOrange).argb);
}
void test_graphics_colors__bw_colors(void) {
cl_assert_equal_i(GColorClearARGB8, gcolor_get_bw(GColorClear).argb);
cl_assert_equal_i(GColorWhiteARGB8, gcolor_get_bw(GColorWhite).argb);
cl_assert_equal_i(GColorBlackARGB8, gcolor_get_bw(GColorBlack).argb);
cl_assert_equal_i(GColorWhiteARGB8, gcolor_get_bw(GColorLightGray).argb);
cl_assert_equal_i(GColorBlackARGB8, gcolor_get_bw(GColorDarkGray).argb);
cl_assert_equal_i(GColorWhiteARGB8, gcolor_get_bw(GColorYellow).argb);
cl_assert_equal_i(GColorBlackARGB8, gcolor_get_bw(GColorBlue).argb);
cl_assert_equal_i(GColorWhiteARGB8, gcolor_get_bw(GColorTiffanyBlue).argb);
cl_assert_equal_i(GColorBlackARGB8, gcolor_get_bw(GColorOrange).argb);
}
static void prv_test_get_luminance(GColor8 color_to_test, GColor8Component expected_luminance) {
cl_assert_equal_i(gcolor_get_luminance(color_to_test), expected_luminance);
// Test that alpha doesn't affect luminance
GColor8 semitransparent_color = color_to_test;
semitransparent_color.a = 1;
cl_assert_equal_i(gcolor_get_luminance(semitransparent_color), expected_luminance);
GColor8 transparent_color = color_to_test;
transparent_color.a = 0;
cl_assert_equal_i(gcolor_get_luminance(transparent_color), expected_luminance);
}
void test_graphics_colors__get_luminance(void) {
prv_test_get_luminance(GColorBlack, 0);
prv_test_get_luminance(GColorWhite, 3);
prv_test_get_luminance(GColorYellow, 3);
prv_test_get_luminance(GColorRed, 2);
prv_test_get_luminance(GColorBlue, 1);
}
void test_graphics_colors__tint_luminance_lookup_table_init(void) {
GColor8 tint_color = GColorClear;
GColor8 lookup_table_out[GCOLOR8_COMPONENT_NUM_VALUES];
// Passing in NULL for lookup_table_out should assert
cl_assert_passert(gcolor_tint_luminance_lookup_table_init(tint_color, NULL));
// Setting the tint color to black should result in a gradient from black to white
tint_color = GColorBlack;
gcolor_tint_luminance_lookup_table_init(tint_color, lookup_table_out);
cl_assert(gcolor_equal(lookup_table_out[0], GColorBlack));
cl_assert(gcolor_equal(lookup_table_out[1], GColorDarkGray));
cl_assert(gcolor_equal(lookup_table_out[2], GColorLightGray));
cl_assert(gcolor_equal(lookup_table_out[3], GColorWhite));
// Setting the tint color to blue should result in a gradient from blue to yellow
tint_color = GColorBlue;
gcolor_tint_luminance_lookup_table_init(tint_color, lookup_table_out);
cl_assert(gcolor_equal(lookup_table_out[0], GColorBlue));
cl_assert(gcolor_equal(lookup_table_out[1], GColorLiberty));
cl_assert(gcolor_equal(lookup_table_out[2], GColorBrass));
cl_assert(gcolor_equal(lookup_table_out[3], GColorYellow));
// lookup_table_out's entries should all preserve the alpha of tint_color
tint_color = GColorBlack;
tint_color.a = 1;
gcolor_tint_luminance_lookup_table_init(tint_color, lookup_table_out);
for (GColor8Component i = 0; i < GCOLOR8_COMPONENT_NUM_VALUES; i++) {
cl_assert_equal_i(lookup_table_out[i].a, tint_color.a);
}
}
static void prv_test_tint_using_luminance_and_perform_lookup_using_color_luminance(
GColor8 src_color, GColor8 tint_color, GColor8 expected_result_color) {
GColor8 lookup_table[GCOLOR8_COMPONENT_NUM_VALUES];
gcolor_tint_luminance_lookup_table_init(tint_color, lookup_table);
cl_assert(gcolor_equal(
gcolor_perform_lookup_using_color_luminance_and_multiply_alpha(src_color, lookup_table),
expected_result_color));
cl_assert(gcolor_equal(gcolor_tint_using_luminance_and_multiply_alpha(src_color, tint_color),
expected_result_color));
}
void test_graphics_colors__tint_using_luminance_and_perform_lookup_using_color_luminance(void) {
// Passing in NULL for lookup_table should assert
cl_assert_passert(gcolor_perform_lookup_using_color_luminance_and_multiply_alpha(GColorRed,
NULL));
// A src_color of yellow should have a luminance that picks white from lookup_table
// initialized with a tint_color of black
prv_test_tint_using_luminance_and_perform_lookup_using_color_luminance(GColorYellow,
GColorBlack,
GColorWhite);
// A src_color of red should have a luminance that picks light gray from lookup_table initialized
// with a tint_color of black
prv_test_tint_using_luminance_and_perform_lookup_using_color_luminance(GColorRed,
GColorBlack,
GColorLightGray);
// A src_color of blue should have a luminance that picks dark gray from lookup_table
// initialized with a tint_color of black
prv_test_tint_using_luminance_and_perform_lookup_using_color_luminance(GColorBlue,
GColorBlack,
GColorDarkGray);
// A src_color of black should have a luminance that picks black from lookup_table initialized
// with a tint_color of black
prv_test_tint_using_luminance_and_perform_lookup_using_color_luminance(GColorBlack,
GColorBlack,
GColorBlack);
}
void test_graphics_colors__component_multiply(void) {
const GColor8Component max_component_value = GCOLOR8_COMPONENT_NUM_VALUES - 1;
// 0% multiplied by anything is 0%
for (GColor8Component i = 0; i <= max_component_value; i++) {
cl_assert_equal_i(gcolor_component_multiply(i, 0), 0);
cl_assert_equal_i(gcolor_component_multiply(0, i), 0);
}
// Associative property (excluding 0% since we already test it above)
for (GColor8Component i = 1; i <= max_component_value; i++) {
for (GColor8Component j = 1; j <= max_component_value; j++) {
cl_assert_equal_i(gcolor_component_multiply(i, j), gcolor_component_multiply(j, i));
}
}
// A few specific combinations
cl_assert_equal_i(gcolor_component_multiply(3, 3), 3);
cl_assert_equal_i(gcolor_component_multiply(3, 2), 2);
cl_assert_equal_i(gcolor_component_multiply(2, 2), 1);
cl_assert_equal_i(gcolor_component_multiply(1, 3), 1);
cl_assert_equal_i(gcolor_component_multiply(2, 1), 1);
}
@@ -0,0 +1,847 @@
/*
* 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/graphics/gtypes.h"
#include "applib/graphics/graphics.h"
#include "util/attributes.h"
#include "clar.h"
#include "util.h"
// Helper Functions
////////////////////////////////////
#include "test_graphics.h"
#include "${BIT_DEPTH_NAME}/test_framebuffer.h"
// Stubs
////////////////////////////////////
#include "graphics_common_stubs.h"
#include "stubs_applib_resource.h"
// Definitions
#define SW_EVEN 4
#define SW_ODD 5
// Includes all possible fields for all shapes
// The fields that are not needed for a particular shape will be zeroed out.
typedef struct PACKED {
char func[30];
GContext ctx;
GPoint p0;
GPoint p1;
GRect r0;
uint16_t radius;
uint8_t quadrant;
GCornerMask corner_mask;
int16_t major_axis_offset;
Fixed_S16_3 offset_start;
Fixed_S16_3 offset_stop;
bool anti_aliased;
uint16_t brightness;
GColor color;
GBitmap fb;
} ArgsForMock;
static ArgsForMock s_last_args_for_mock;
static GContext context;
static FrameBuffer *fb = NULL;
/////////////////////////////////////
/// FUNCTION OVERRIDES
/////////////////////////////////////
void graphics_line_draw_1px_non_aa(GContext* ctx, GPoint p0, GPoint p1) {
s_last_args_for_mock = (ArgsForMock){
.ctx = *ctx,
.p0 = p0,
.p1 = p1
};
strncpy(s_last_args_for_mock.func, __func__, sizeof(s_last_args_for_mock.func));
}
void graphics_line_draw_1px_aa(GContext* ctx, GPoint p0, GPoint p1) {
s_last_args_for_mock = (ArgsForMock){
.ctx = *ctx,
.p0 = p0,
.p1 = p1
};
strncpy(s_last_args_for_mock.func, __func__, sizeof(s_last_args_for_mock.func));
}
void graphics_line_draw_stroked_aa(GContext* ctx, GPoint p0, GPoint p1, uint8_t stroke_width) {
s_last_args_for_mock = (ArgsForMock){
.ctx = *ctx,
.p0 = p0,
.p1 = p1
};
strncpy(s_last_args_for_mock.func, __func__, sizeof(s_last_args_for_mock.func));
}
void graphics_line_draw_stroked_non_aa(GContext* ctx, GPoint p0, GPoint p1, uint8_t stroke_width) {
s_last_args_for_mock = (ArgsForMock){
.ctx = *ctx,
.p0 = p0,
.p1 = p1
};
strncpy(s_last_args_for_mock.func, __func__, sizeof(s_last_args_for_mock.func));
}
void prv_fill_rect_non_aa(GContext* ctx, const GRect *rect, uint16_t radius,
GCornerMask corner_mask, GColor fill_color) {
s_last_args_for_mock = (ArgsForMock){
.ctx = *ctx,
.r0 = *rect,
.radius = radius,
.corner_mask = corner_mask
};
strncpy(s_last_args_for_mock.func, __func__, sizeof(s_last_args_for_mock.func));
}
void prv_fill_rect_aa(GContext* ctx, const GRect *rect, uint16_t radius,
GCornerMask corner_mask, GColor fill_color) {
s_last_args_for_mock = (ArgsForMock){
.ctx = *ctx,
.r0 = *rect,
.radius = radius,
.corner_mask = corner_mask
};
strncpy(s_last_args_for_mock.func, __func__, sizeof(s_last_args_for_mock.func));
}
void prv_draw_rect(GContext *ctx, const GRect *rect) {
s_last_args_for_mock = (ArgsForMock){
.ctx = *ctx,
.r0 = *rect,
.radius = 0,
.corner_mask = GCornerNone
};
strncpy(s_last_args_for_mock.func, __func__, sizeof(s_last_args_for_mock.func));
}
void prv_draw_rect_aa_stroked(GContext *ctx, const GRect *rect, uint8_t stroke_width) {
s_last_args_for_mock = (ArgsForMock){
.ctx = *ctx,
.r0 = *rect,
.radius = 0,
.corner_mask = GCornerNone
};
strncpy(s_last_args_for_mock.func, __func__, sizeof(s_last_args_for_mock.func));
}
void prv_draw_rect_stroked(GContext *ctx, const GRect *rect, uint8_t stroke_width) {
s_last_args_for_mock = (ArgsForMock){
.ctx = *ctx,
.r0 = *rect,
.radius = 0,
.corner_mask = GCornerNone
};
strncpy(s_last_args_for_mock.func, __func__, sizeof(s_last_args_for_mock.func));
}
void graphics_circle_draw_1px_non_aa(GContext* ctx, GPoint p, uint16_t radius) {
s_last_args_for_mock = (ArgsForMock){
.ctx = *ctx,
.p0 = p,
.radius = radius
};
strncpy(s_last_args_for_mock.func, __func__, sizeof(s_last_args_for_mock.func));
}
void graphics_circle_draw_1px_aa(GContext* ctx, GPoint p, uint16_t radius) {
s_last_args_for_mock = (ArgsForMock){
.ctx = *ctx,
.p0 = p,
.radius = radius
};
strncpy(s_last_args_for_mock.func, __func__, sizeof(s_last_args_for_mock.func));
}
void graphics_circle_draw_stroked_aa(GContext* ctx, GPoint p, uint16_t radius, uint8_t stroke_width) {
s_last_args_for_mock = (ArgsForMock){
.ctx = *ctx,
.p0 = p,
.radius = radius
};
strncpy(s_last_args_for_mock.func, __func__, sizeof(s_last_args_for_mock.func));
}
void graphics_circle_draw_stroked_non_aa(GContext* ctx, GPoint p, uint16_t radius, uint8_t stroke_width) {
s_last_args_for_mock = (ArgsForMock){
.ctx = *ctx,
.p0 = p,
.radius = radius
};
strncpy(s_last_args_for_mock.func, __func__, sizeof(s_last_args_for_mock.func));
}
void graphics_internal_circle_quadrant_fill_aa(GContext* ctx, GPoint p,
uint16_t radius, GCornerMask quadrant) {
s_last_args_for_mock = (ArgsForMock){
.ctx = *ctx,
.p0 = p,
.radius = radius,
.corner_mask = quadrant
};
strncpy(s_last_args_for_mock.func, __func__, sizeof(s_last_args_for_mock.func));
}
void graphics_circle_fill_non_aa(GContext* ctx, GPoint p, uint16_t radius) {
s_last_args_for_mock = (ArgsForMock){
.ctx = *ctx,
.p0 = p,
.radius = radius
};
strncpy(s_last_args_for_mock.func, __func__, sizeof(s_last_args_for_mock.func));
}
void prv_draw_round_rect(GContext* ctx, const GRect *rect, uint16_t radius) {
s_last_args_for_mock = (ArgsForMock){
.ctx = *ctx,
.r0 = *rect,
.radius = radius,
.corner_mask = GCornersAll
};
strncpy(s_last_args_for_mock.func, __func__, sizeof(s_last_args_for_mock.func));
}
void prv_draw_round_rect_aa(GContext* ctx, const GRect *rect, uint16_t radius) {
s_last_args_for_mock = (ArgsForMock){
.ctx = *ctx,
.r0 = *rect,
.radius = radius,
.corner_mask = GCornersAll
};
strncpy(s_last_args_for_mock.func, __func__, sizeof(s_last_args_for_mock.func));
}
void prv_draw_round_rect_aa_stroked(GContext* ctx, const GRect *rect, uint16_t radius,
uint8_t stroke_width) {
s_last_args_for_mock = (ArgsForMock){
.ctx = *ctx,
.r0 = *rect,
.radius = radius,
.corner_mask = GCornersAll
};
strncpy(s_last_args_for_mock.func, __func__, sizeof(s_last_args_for_mock.func));
}
void prv_draw_round_rect_stroked(GContext* ctx, const GRect *rect, uint16_t radius, uint8_t stroke_width) {
s_last_args_for_mock = (ArgsForMock){
.ctx = *ctx,
.r0 = *rect,
.radius = radius,
.corner_mask = GCornersAll
};
strncpy(s_last_args_for_mock.func, __func__, sizeof(s_last_args_for_mock.func));
}
/////////////////////////////////////
/// HELPER FUNCTIONS
/////////////////////////////////////
// Copy over the last arguments - take the largest possible shape
static void copy_last_args_for_mock(ArgsForMock *actual_args, ArgsForMock *last_args) {
memcpy(actual_args, last_args, sizeof(ArgsForMock));
}
// Zero out the arguments from the last run
static void reset_last_args_for_mock() {
memset(&s_last_args_for_mock, 0x00, sizeof(s_last_args_for_mock));
}
// Validate the arguments based on the shape that is drawn
static bool validate_args(ArgsForMock *actual_args, ArgsForMock *valid_args) {
if (memcmp(actual_args, valid_args, sizeof(ArgsForMock)) == 0) {
return true;
}
return false;
}
// This macro will call the expected code_block and then validate the arguments set in code_block
// match what was expected from the last mock call (i.e. if graphics_draw_line was called, then
// internally it would call prv_draw_line... and the actual_args would be set to the arguments
// that are set in prv_draw_line... so when compared to directly calling prv_draw_line..., the two
// should match.
#define ASSERT_CALLED(code_block) \
do {\
ArgsForMock actual_args; \
copy_last_args_for_mock(&actual_args, &s_last_args_for_mock); \
reset_last_args_for_mock(); \
code_block; \
bool cmp_result = validate_args(&actual_args, &s_last_args_for_mock); \
printf("1 %d %d %d %d\n", actual_args.r0.origin.x, actual_args.r0.origin.y, \
actual_args.r0.size.w, actual_args.r0.size.h); \
actual_args = s_last_args_for_mock; \
printf("2 %d %d %d %d\n", actual_args.r0.origin.x, actual_args.r0.origin.y, \
actual_args.r0.size.w, actual_args.r0.size.h); \
cl_assert(cmp_result); \
} while(0)
// This is used to make sure nothing has changed when calling code_block
// i.e. no prv_... functions have been called
// If any prv_... functions are called then s_last_args_for_mock will be updated
#define ASSERT_NO_CHANGE(code_block) \
do {\
ArgsForMock actual_args; \
reset_last_args_for_mock(); \
copy_last_args_for_mock(&actual_args, &s_last_args_for_mock); \
code_block; \
bool cmp_result = validate_args(&actual_args, &s_last_args_for_mock); \
cl_assert(cmp_result); \
} while(0)
static void setup_test(GContext* ctx, bool antialiased, uint8_t stroke_width, GColor stroke_color,
GColor fill_color, bool lock) {
graphics_context_set_antialiased(ctx, antialiased);
graphics_context_set_stroke_width(ctx, stroke_width);
graphics_context_set_stroke_color(ctx, stroke_color);
graphics_context_set_fill_color(ctx, fill_color);
ctx->lock = lock;
reset_last_args_for_mock();
}
// Setup
void test_graphics_context_${BIT_DEPTH_NAME}__initialize(void) {
fb = malloc(sizeof(FrameBuffer));
framebuffer_init(fb, &(GSize) {DISP_COLS, DISP_ROWS});
test_graphics_context_init(&context, fb);
}
// Teardown
void test_graphics_context_${BIT_DEPTH_NAME}__cleanup(void) {
free(fb);
}
/////////////////////////////////////
/// TESTS
/////////////////////////////////////
void test_graphics_context_${BIT_DEPTH_NAME}__set(void) {
GDrawState draw_state;
GColor color;
// Stroke Color
graphics_context_set_stroke_color(&context, GColorClear);
cl_assert_equal_i(context.draw_state.stroke_color.argb, GColorClear.argb);
color = GColorBlue;
graphics_context_set_stroke_color(&context, color);
#if PBL_COLOR
cl_assert_equal_i(context.draw_state.stroke_color.argb, GColorBlue.argb);
#else
cl_assert_equal_i(context.draw_state.stroke_color.argb, GColorBlack.argb);
#endif
color.a = 2;
graphics_context_set_stroke_color(&context, color);
#if PBL_COLOR
cl_assert_equal_i(context.draw_state.stroke_color.argb, GColorBlue.argb);
#else
cl_assert_equal_i(context.draw_state.stroke_color.argb, GColorBlack.argb);
#endif
color.a = 1;
graphics_context_set_stroke_color(&context, color);
cl_assert_equal_i(context.draw_state.stroke_color.argb, GColorClear.argb);
color.a = 0;
graphics_context_set_stroke_color(&context, color);
cl_assert_equal_i(context.draw_state.stroke_color.argb, GColorClear.argb);
// Stroke Color - 2-bit
graphics_context_set_stroke_color_2bit(&context, GColor2Black);
cl_assert(gcolor_equal(context.draw_state.stroke_color, GColorBlack));
// Fill Color
graphics_context_set_fill_color(&context, GColorClear);
cl_assert_equal_i(context.draw_state.fill_color.argb, GColorClear.argb);
color = GColorOrange;
graphics_context_set_fill_color(&context, color);
#if PBL_COLOR
cl_assert_equal_i(context.draw_state.fill_color.argb, GColorOrange.argb);
#else
cl_assert_equal_i(context.draw_state.fill_color.argb, GColorDarkGray.argb);
#endif
color.a = 2;
graphics_context_set_fill_color(&context, color);
#if PBL_COLOR
cl_assert_equal_i(context.draw_state.fill_color.argb, GColorOrange.argb);
#else
cl_assert_equal_i(context.draw_state.fill_color.argb, GColorDarkGray.argb);
#endif
color.a = 1;
graphics_context_set_fill_color(&context, color);
cl_assert_equal_i(context.draw_state.fill_color.argb, GColorClear.argb);
color.a = 0;
graphics_context_set_fill_color(&context, color);
cl_assert_equal_i(context.draw_state.fill_color.argb, GColorClear.argb);
// Fill Color - 2-bit
graphics_context_set_fill_color_2bit(&context, GColor2White);
cl_assert(gcolor_equal(context.draw_state.fill_color, GColorWhite));
// Compositing Mode
graphics_context_set_compositing_mode(&context, GCompOpOr);
cl_assert(context.draw_state.compositing_mode == GCompOpOr);
// Text Color
graphics_context_set_text_color(&context, GColorClear);
cl_assert_equal_i(context.draw_state.text_color.argb, GColorClear.argb);
color = GColorYellow;
graphics_context_set_text_color(&context, color);
#if PBL_COLOR
cl_assert_equal_i(context.draw_state.text_color.argb, GColorYellow.argb);
#else
cl_assert_equal_i(context.draw_state.text_color.argb, GColorWhite.argb);
#endif
color.a = 2;
graphics_context_set_text_color(&context, color);
#if PBL_COLOR
cl_assert_equal_i(context.draw_state.text_color.argb, GColorYellow.argb);
#else
cl_assert_equal_i(context.draw_state.text_color.argb, GColorWhite.argb);
#endif
color.a = 1;
graphics_context_set_text_color(&context, color);
cl_assert_equal_i(context.draw_state.text_color.argb, GColorClear.argb);
color.a = 0;
graphics_context_set_text_color(&context, color);
cl_assert_equal_i(context.draw_state.text_color.argb, GColorClear.argb);
// Text Color - 2-bit
graphics_context_set_text_color_2bit(&context, GColor2White);
cl_assert(gcolor_equal(context.draw_state.text_color, GColorWhite));
#if PBL_COLOR
// Antialiased
graphics_context_set_antialiased(&context, true);
cl_assert(context.draw_state.antialiased == true);
#endif
// Stroke Width
graphics_context_set_stroke_width(&context, 11);
cl_assert(context.draw_state.stroke_width == 11);
// Make sure setting to zero is ignored
draw_state = graphics_context_get_drawing_state(&context);
graphics_context_set_stroke_width(&context, 0);
cl_assert(memcmp(&draw_state, &context.draw_state, sizeof(GDrawState)) == 0);
// Check draw state
memset(&draw_state, 0x5A, sizeof(GDrawState));
graphics_context_set_drawing_state(&context, draw_state);
cl_assert(memcmp(&draw_state, &context.draw_state, sizeof(GDrawState)) == 0);
}
void test_graphics_context_${BIT_DEPTH_NAME}__draw_antialiased(void) {
// Stroke width = 1, antialiased
setup_test(&context, true, 1, GColorBlack, GColorBlack, false);
graphics_draw_line(&context, GPoint(5, 5), GPoint(45, 10));
#if PBL_COLOR
ASSERT_CALLED(graphics_line_draw_1px_aa(&context, GPoint(5, 5), GPoint(45, 10)));
#else
ASSERT_CALLED(graphics_line_draw_1px_non_aa(&context, GPoint(5, 5), GPoint(45, 10)));
#endif
setup_test(&context, true, 1, GColorBlack, GColorBlack, false);
graphics_draw_rect(&context, &GRect(10, 20, 40, 10));
ASSERT_CALLED(prv_draw_rect(&context, &GRect(10, 20, 40, 10)));
setup_test(&context, true, 1, GColorBlack, GColorBlack, false);
graphics_draw_circle(&context, GPoint(50, 50), 10);
#if PBL_COLOR
ASSERT_CALLED(graphics_circle_draw_1px_aa(&context, GPoint(50, 50), 10));
#else
ASSERT_CALLED(graphics_circle_draw_1px_non_aa(&context, GPoint(50, 50), 10));
#endif
setup_test(&context, true, 1, GColorBlack, GColorBlack, false);
graphics_draw_round_rect(&context, &GRect(20, 80, 40, 10), 4);
#if PBL_COLOR
ASSERT_CALLED(prv_draw_round_rect_aa(&context, &GRect(20, 80, 40, 10), 4));
#else
ASSERT_CALLED(prv_draw_round_rect(&context, &GRect(20, 80, 40, 10), 4));
#endif
}
void test_graphics_context_${BIT_DEPTH_NAME}__draw_stroke_width_1(void) {
// Stroke width 1, non-antialiased
setup_test(&context, false, 1, GColorBlack, GColorBlack, false);
graphics_draw_line(&context, GPoint(5, 5), GPoint(45, 10));
ASSERT_CALLED(graphics_line_draw_1px_non_aa(&context, GPoint(5, 5), GPoint(45, 10)));
setup_test(&context, false, 1, GColorBlack, GColorBlack, false);
graphics_draw_rect(&context, &GRect(10, 20, 40, 10));
ASSERT_CALLED(prv_draw_rect(&context, &GRect(10, 20, 40, 10)));
setup_test(&context, false, 1, GColorBlack, GColorBlack, false);
graphics_draw_circle(&context, GPoint(50, 50), 10);
ASSERT_CALLED(graphics_circle_draw_1px_non_aa(&context, GPoint(50, 50), 10));
setup_test(&context, false, 1, GColorBlack, GColorBlack, false);
graphics_draw_round_rect(&context, &GRect(20, 80, 40, 10), 4);
ASSERT_CALLED(prv_draw_round_rect(&context, &GRect(20, 80, 40, 10), 4));
}
void test_graphics_context_${BIT_DEPTH_NAME}__draw_stroke_width_2(void) {
// Stroke width 2, non-antialiased
setup_test(&context, false, 2, GColorBlack, GColorBlack, false);
graphics_draw_line(&context, GPoint(5, 5), GPoint(45, 10));
ASSERT_CALLED(graphics_line_draw_stroked_non_aa(&context, GPoint(5, 5), GPoint(45, 10), 2));
setup_test(&context, false, 2, GColorBlack, GColorBlack, false);
graphics_draw_rect(&context, &GRect(10, 20, 40, 10));
ASSERT_CALLED(prv_draw_rect(&context, &GRect(10, 20, 40, 10)));
setup_test(&context, false, 2, GColorBlack, GColorBlack, false);
graphics_draw_circle(&context, GPoint(50, 50), 10);
ASSERT_CALLED(graphics_circle_draw_stroked_non_aa(&context, GPoint(50, 50), 10, 2));
setup_test(&context, false, 2, GColorBlack, GColorBlack, false);
graphics_draw_round_rect(&context, &GRect(20, 80, 40, 10), 4);
ASSERT_CALLED(prv_draw_round_rect_stroked(&context, &GRect(20, 80, 40, 10), 4, 2));
}
void test_graphics_context_${BIT_DEPTH_NAME}__draw_stroke_width_even(void) {
// Stroke width even > 2, non-antialiased
setup_test(&context, false, SW_EVEN, GColorBlack, GColorBlack, false);
graphics_draw_line(&context, GPoint(5, 5), GPoint(45, 10));
ASSERT_CALLED(graphics_line_draw_stroked_non_aa(&context, GPoint(5, 5), GPoint(45, 10), SW_EVEN));
setup_test(&context, false, SW_EVEN, GColorBlack, GColorBlack, false);
graphics_draw_rect(&context, &GRect(10, 20, 40, 10));
ASSERT_CALLED(prv_draw_rect_stroked(&context, &GRect(10, 20, 40, 10), SW_EVEN));
setup_test(&context, false, SW_EVEN, GColorBlack, GColorBlack, false);
graphics_draw_circle(&context, GPoint(50, 50), 10);
ASSERT_CALLED(graphics_circle_draw_stroked_non_aa(&context, GPoint(50, 50), 10, SW_EVEN));
setup_test(&context, false, SW_EVEN, GColorBlack, GColorBlack, false);
graphics_draw_round_rect(&context, &GRect(20, 80, 40, 10), 4);
ASSERT_CALLED(prv_draw_round_rect_stroked(&context, &GRect(20, 80, 40, 10), 4, SW_EVEN));
}
void test_graphics_context_${BIT_DEPTH_NAME}__draw_stroke_width_odd(void) {
// Stroke width odd > 1, non-antialiased
setup_test(&context, false, SW_ODD, GColorBlack, GColorBlack, false);
graphics_draw_line(&context, GPoint(5, 5), GPoint(45, 10));
ASSERT_CALLED(graphics_line_draw_stroked_non_aa(&context, GPoint(5, 5), GPoint(45, 10), SW_ODD));
setup_test(&context, false, SW_ODD, GColorBlack, GColorBlack, false);
graphics_draw_rect(&context, &GRect(10, 20, 40, 10));
ASSERT_CALLED(prv_draw_rect_stroked(&context, &GRect(10, 20, 40, 10), SW_ODD));
setup_test(&context, false, SW_ODD, GColorBlack, GColorBlack, false);
graphics_draw_circle(&context, GPoint(50, 50), 10);
ASSERT_CALLED(graphics_circle_draw_stroked_non_aa(&context, GPoint(50, 50), 10, SW_ODD));
setup_test(&context, false, SW_ODD, GColorBlack, GColorBlack, false);
graphics_draw_round_rect(&context, &GRect(20, 80, 40, 10), 4);
ASSERT_CALLED(prv_draw_round_rect_stroked(&context, &GRect(20, 80, 40, 10), 4, SW_ODD));
}
void test_graphics_context_${BIT_DEPTH_NAME}__draw_antialiased_stroke_width_2(void) {
// Stroke width = 2, antialiased
setup_test(&context, true, 2, GColorBlack, GColorBlack, false);
graphics_draw_line(&context, GPoint(5, 5), GPoint(45, 10));
#if PBL_COLOR
ASSERT_CALLED(graphics_line_draw_stroked_aa(&context, GPoint(5, 5), GPoint(45, 10), 2));
#else
ASSERT_CALLED(graphics_line_draw_stroked_non_aa(&context, GPoint(5, 5), GPoint(45, 10), 2));
#endif
setup_test(&context, true, 2, GColorBlack, GColorBlack, false);
graphics_draw_rect(&context, &GRect(10, 20, 40, 10));
ASSERT_CALLED(prv_draw_rect(&context, &GRect(10, 20, 40, 10)));
setup_test(&context, true, 2, GColorBlack, GColorBlack, false);
graphics_draw_circle(&context, GPoint(50, 50), 10);
#if PBL_COLOR
ASSERT_CALLED(graphics_circle_draw_stroked_aa(&context, GPoint(50, 50), 10, 2));
#else
ASSERT_CALLED(graphics_circle_draw_stroked_non_aa(&context, GPoint(50, 50), 10, 2));
#endif
setup_test(&context, true, 2, GColorBlack, GColorBlack, false);
graphics_draw_round_rect(&context, &GRect(20, 80, 40, 10), 4);
#if PBL_COLOR
ASSERT_CALLED(prv_draw_round_rect_aa_stroked(&context, &GRect(20, 80, 40, 10), 4, 2));
#else
ASSERT_CALLED(prv_draw_round_rect_stroked(&context, &GRect(20, 80, 40, 10), 4, 2));
#endif
}
void test_graphics_context_${BIT_DEPTH_NAME}__draw_antialiased_stroke_width_even(void) {
// Stroke width even > 2, antialiased
setup_test(&context, true, SW_EVEN, GColorBlack, GColorBlack, false);
graphics_draw_line(&context, GPoint(5, 5), GPoint(45, 10));
#if PBL_COLOR
ASSERT_CALLED(graphics_line_draw_stroked_aa(&context, GPoint(5, 5), GPoint(45, 10), SW_EVEN));
#else
ASSERT_CALLED(graphics_line_draw_stroked_non_aa(&context, GPoint(5, 5), GPoint(45, 10), SW_EVEN));
#endif
setup_test(&context, true, SW_EVEN, GColorBlack, GColorBlack, false);
graphics_draw_rect(&context, &GRect(10, 20, 40, 10));
#if PBL_COLOR
ASSERT_CALLED(prv_draw_rect_aa_stroked(&context, &GRect(10, 20, 40, 10), SW_EVEN));
#else
ASSERT_CALLED(prv_draw_rect_stroked(&context, &GRect(10, 20, 40, 10), SW_EVEN));
#endif
setup_test(&context, true, SW_EVEN, GColorBlack, GColorBlack, false);
graphics_draw_circle(&context, GPoint(50, 50), 10);
#if PBL_COLOR
ASSERT_CALLED(graphics_circle_draw_stroked_aa(&context, GPoint(50, 50), 10, SW_EVEN));
#else
ASSERT_CALLED(graphics_circle_draw_stroked_non_aa(&context, GPoint(50, 50), 10, SW_EVEN));
#endif
setup_test(&context, true, SW_EVEN, GColorBlack, GColorBlack, false);
graphics_draw_round_rect(&context, &GRect(20, 80, 40, 10), 4);
#if PBL_COLOR
ASSERT_CALLED(prv_draw_round_rect_aa_stroked(&context, &GRect(20, 80, 40, 10), 4, SW_EVEN));
#else
ASSERT_CALLED(prv_draw_round_rect_stroked(&context, &GRect(20, 80, 40, 10), 4, SW_EVEN));
#endif
}
void test_graphics_context_${BIT_DEPTH_NAME}__draw_antialiased_stroke_width_odd(void) {
// Stroke width odd > 1, antialiased
setup_test(&context, true, SW_ODD, GColorBlack, GColorBlack, false);
graphics_draw_line(&context, GPoint(5, 5), GPoint(45, 10));
#if PBL_COLOR
ASSERT_CALLED(graphics_line_draw_stroked_aa(&context, GPoint(5, 5), GPoint(45, 10), SW_ODD));
#else
ASSERT_CALLED(graphics_line_draw_stroked_non_aa(&context, GPoint(5, 5), GPoint(45, 10), SW_ODD));
#endif
setup_test(&context, true, SW_ODD, GColorBlack, GColorBlack, false);
graphics_draw_rect(&context, &GRect(10, 20, 40, 10));
#if PBL_COLOR
ASSERT_CALLED(prv_draw_rect_aa_stroked(&context, &GRect(10, 20, 40, 10), SW_ODD));
#else
ASSERT_CALLED(prv_draw_rect_stroked(&context, &GRect(10, 20, 40, 10), SW_ODD));
#endif
setup_test(&context, true, SW_ODD, GColorBlack, GColorBlack, false);
graphics_draw_circle(&context, GPoint(50, 50), 10);
#if PBL_COLOR
ASSERT_CALLED(graphics_circle_draw_stroked_aa(&context, GPoint(50, 50), 10, SW_ODD));
#else
ASSERT_CALLED(graphics_circle_draw_stroked_non_aa(&context, GPoint(50, 50), 10, SW_ODD));
#endif
setup_test(&context, true, SW_ODD, GColorBlack, GColorBlack, false);
graphics_draw_round_rect(&context, &GRect(20, 80, 40, 10), 4);
#if PBL_COLOR
ASSERT_CALLED(prv_draw_round_rect_aa_stroked(&context, &GRect(20, 80, 40, 10), 4, SW_ODD));
#else
ASSERT_CALLED(prv_draw_round_rect_stroked(&context, &GRect(20, 80, 40, 10), 4, SW_ODD));
#endif
}
void test_graphics_context_${BIT_DEPTH_NAME}__fill(void) {
// Fill shape, non-antialiased (Stroke width/color N/A)
setup_test(&context, false, 5, GColorBlack, GColorBlack, false);
graphics_fill_rect(&context, &GRect(10, 20, 40, 10));
ASSERT_CALLED(prv_fill_rect_non_aa(&context, &GRect(10, 20, 40, 10), 0, GCornerNone, GColorBlack));
setup_test(&context, false, 5, GColorBlack, GColorBlack, false);
graphics_fill_circle(&context, GPoint(50, 50), 10);
ASSERT_CALLED(graphics_circle_fill_non_aa(&context, GPoint(50, 50), 10));
setup_test(&context, false, 5, GColorBlack, GColorBlack, false);
graphics_fill_round_rect(&context, &GRect(20, 80, 40, 10), 4, GCornersAll);
ASSERT_CALLED(prv_fill_rect_non_aa(&context, &GRect(20, 80, 40, 10), 4, GCornersAll, GColorBlack));
}
void test_graphics_context_${BIT_DEPTH_NAME}__fill_antialiased(void) {
// Fill shape, antialiased (Stroke width/color N/A)
setup_test(&context, true, 5, GColorBlack, GColorBlack, false);
graphics_fill_rect(&context, &GRect(10, 20, 40, 10));
#if SCREEN_COLOR_DEPTH_BITS == 1
ASSERT_CALLED(prv_fill_rect_non_aa(&context, &GRect(10, 20, 40, 10), 0, GCornerNone, GColorBlack));
#else
ASSERT_CALLED(prv_fill_rect_aa(&context, &GRect(10, 20, 40, 10), 0, GCornerNone, GColorBlack));
#endif
setup_test(&context, true, 5, GColorBlack, GColorBlack, false);
graphics_fill_circle(&context, GPoint(50, 50), 10);
#if PBL_COLOR
ASSERT_CALLED(graphics_internal_circle_quadrant_fill_aa(&context, GPoint(50, 50), 10, GCornersAll));
#else
ASSERT_CALLED(graphics_circle_fill_non_aa(&context, GPoint(50, 50), 10));
#endif
setup_test(&context, true, 5, GColorBlack, GColorBlack, false);
graphics_fill_round_rect(&context, &GRect(20, 80, 40, 10), 4, GCornersAll);
#if SCREEN_COLOR_DEPTH_BITS == 1
ASSERT_CALLED(prv_fill_rect_non_aa(&context, &GRect(20, 80, 40, 10), 4, GCornersAll, GColorBlack));
#else
ASSERT_CALLED(prv_fill_rect_aa(&context, &GRect(20, 80, 40, 10), 4, GCornersAll, GColorBlack));
#endif
}
void test_graphics_context_${BIT_DEPTH_NAME}__lock(void) {
// Test all the setup test combinations as above
setup_test(&context, false, 1, GColorBlack, GColorBlack, true);
ASSERT_NO_CHANGE(graphics_draw_line(&context, GPoint(5, 5), GPoint(45, 10)));
ASSERT_NO_CHANGE(graphics_draw_rect(&context, &GRect(10, 20, 40, 10)));
ASSERT_NO_CHANGE(graphics_draw_circle(&context, GPoint(50, 50), 10));
ASSERT_NO_CHANGE(graphics_draw_round_rect(&context, &GRect(20, 80, 40, 10), 4));
setup_test(&context, false, 2, GColorBlack, GColorBlack, true);
ASSERT_NO_CHANGE(graphics_draw_line(&context, GPoint(5, 5), GPoint(45, 10)));
ASSERT_NO_CHANGE(graphics_draw_rect(&context, &GRect(10, 20, 40, 10)));
ASSERT_NO_CHANGE(graphics_draw_circle(&context, GPoint(50, 50), 10));
ASSERT_NO_CHANGE(graphics_draw_round_rect(&context, &GRect(20, 80, 40, 10), 4));
setup_test(&context, false, SW_EVEN, GColorBlack, GColorBlack, true);
ASSERT_NO_CHANGE(graphics_draw_line(&context, GPoint(5, 5), GPoint(45, 10)));
ASSERT_NO_CHANGE(graphics_draw_rect(&context, &GRect(10, 20, 40, 10)));
ASSERT_NO_CHANGE(graphics_draw_circle(&context, GPoint(50, 50), 10));
ASSERT_NO_CHANGE(graphics_draw_round_rect(&context, &GRect(20, 80, 40, 10), 4));
setup_test(&context, false, SW_ODD, GColorBlack, GColorBlack, true);
ASSERT_NO_CHANGE(graphics_draw_line(&context, GPoint(5, 5), GPoint(45, 10)));
ASSERT_NO_CHANGE(graphics_draw_rect(&context, &GRect(10, 20, 40, 10)));
ASSERT_NO_CHANGE(graphics_draw_circle(&context, GPoint(50, 50), 10));
ASSERT_NO_CHANGE(graphics_draw_round_rect(&context, &GRect(20, 80, 40, 10), 4));
ASSERT_NO_CHANGE(graphics_fill_rect(&context, &GRect(10, 20, 40, 10)));
ASSERT_NO_CHANGE(graphics_fill_circle(&context, GPoint(50, 50), 10));
ASSERT_NO_CHANGE(graphics_fill_round_rect(&context, &GRect(20, 80, 40, 10), 4, GCornersAll));
setup_test(&context, true, 1, GColorBlack, GColorBlack, true);
ASSERT_NO_CHANGE(graphics_draw_line(&context, GPoint(5, 5), GPoint(45, 10)));
ASSERT_NO_CHANGE(graphics_draw_rect(&context, &GRect(10, 20, 40, 10)));
ASSERT_NO_CHANGE(graphics_draw_circle(&context, GPoint(50, 50), 10));
ASSERT_NO_CHANGE(graphics_draw_round_rect(&context, &GRect(20, 80, 40, 10), 4));
setup_test(&context, true, 2, GColorBlack, GColorBlack, true);
ASSERT_NO_CHANGE(graphics_draw_line(&context, GPoint(5, 5), GPoint(45, 10)));
ASSERT_NO_CHANGE(graphics_draw_rect(&context, &GRect(10, 20, 40, 10)));
ASSERT_NO_CHANGE(graphics_draw_circle(&context, GPoint(50, 50), 10));
ASSERT_NO_CHANGE(graphics_draw_round_rect(&context, &GRect(20, 80, 40, 10), 4));
setup_test(&context, true, SW_EVEN, GColorBlack, GColorBlack, true);
ASSERT_NO_CHANGE(graphics_draw_line(&context, GPoint(5, 5), GPoint(45, 10)));
ASSERT_NO_CHANGE(graphics_draw_rect(&context, &GRect(10, 20, 40, 10)));
ASSERT_NO_CHANGE(graphics_draw_circle(&context, GPoint(50, 50), 10));
ASSERT_NO_CHANGE(graphics_draw_round_rect(&context, &GRect(20, 80, 40, 10), 4));
setup_test(&context, true, SW_ODD, GColorBlack, GColorBlack, true);
ASSERT_NO_CHANGE(graphics_draw_line(&context, GPoint(5, 5), GPoint(45, 10)));
ASSERT_NO_CHANGE(graphics_draw_rect(&context, &GRect(10, 20, 40, 10)));
ASSERT_NO_CHANGE(graphics_draw_circle(&context, GPoint(50, 50), 10));
ASSERT_NO_CHANGE(graphics_draw_round_rect(&context, &GRect(20, 80, 40, 10), 4));
ASSERT_NO_CHANGE(graphics_fill_rect(&context, &GRect(10, 20, 40, 10)));
ASSERT_NO_CHANGE(graphics_fill_circle(&context, GPoint(50, 50), 10));
ASSERT_NO_CHANGE(graphics_fill_round_rect(&context, &GRect(20, 80, 40, 10), 4, GCornersAll));
}
void test_graphics_context_${BIT_DEPTH_NAME}__lock_context(void) {
GDrawState draw_state;
context.lock = true;
// Stroke Color
draw_state = graphics_context_get_drawing_state(&context);
graphics_context_set_stroke_color(&context, GColorBlue);
cl_assert(memcmp(&draw_state, &context.draw_state, sizeof(GDrawState)) == 0);
// Fill Color
draw_state = graphics_context_get_drawing_state(&context);
graphics_context_set_fill_color(&context, GColorGreen);
cl_assert(memcmp(&draw_state, &context.draw_state, sizeof(GDrawState)) == 0);
// Compositing Mode
draw_state = graphics_context_get_drawing_state(&context);
graphics_context_set_compositing_mode(&context, GCompOpOr);
cl_assert(memcmp(&draw_state, &context.draw_state, sizeof(GDrawState)) == 0);
// Text Color
draw_state = graphics_context_get_drawing_state(&context);
graphics_context_set_text_color(&context, GColorRed);
cl_assert(memcmp(&draw_state, &context.draw_state, sizeof(GDrawState)) == 0);
// Antialiased
draw_state = graphics_context_get_drawing_state(&context);
graphics_context_set_antialiased(&context, true);
cl_assert(memcmp(&draw_state, &context.draw_state, sizeof(GDrawState)) == 0);
// Stroke Width
draw_state = graphics_context_get_drawing_state(&context);
graphics_context_set_stroke_width(&context, 11);
cl_assert(memcmp(&draw_state, &context.draw_state, sizeof(GDrawState)) == 0);
}
void test_graphics_context_${BIT_DEPTH_NAME}__lock_framebuffer(void) {
FrameBuffer fb = {};
framebuffer_init(&fb, &(GSize) {DISP_COLS, DISP_ROWS});
GContext ctx = {.dest_bitmap.info = {
.format = GBITMAP_NATIVE_FORMAT,
.version = GBITMAP_VERSION_CURRENT,
}, .parent_framebuffer=&fb};
GBitmap *framebuffer = graphics_capture_frame_buffer(&ctx);
cl_assert(ctx.lock == true);
cl_assert_equal_p(framebuffer, &ctx.dest_bitmap);
// Test releasing on any platform
cl_assert(fb.is_dirty == false);
graphics_release_frame_buffer(&ctx, framebuffer);
cl_assert(ctx.lock == false);
cl_assert(fb.is_dirty == true);
};
void test_graphics_context_${BIT_DEPTH_NAME}__lock_framebuffer_8BitCircular(void) {
// Test for locking of requested framebuffer format
GContext ctx = {.dest_bitmap.info.format = GBitmapFormat8BitCircular};
GBitmap *bmp = graphics_capture_frame_buffer_format(&ctx, GBitmapFormat8BitCircular);
cl_assert_equal_p(bmp, &ctx.dest_bitmap);
cl_assert(ctx.lock == true);
};
void test_graphics_context_${BIT_DEPTH_NAME}__lock_framebuffer_fails_from_8BitCircular(void) {
// Test for locking of 8Bit Circular framebuffer when framebuffer is regular 8Bit
GContext ctx = {.dest_bitmap.info.format = GBitmapFormat8BitCircular};
GBitmap *bmp = graphics_capture_frame_buffer_format(&ctx, GBitmapFormat8Bit);
cl_assert(ctx.lock == false);
cl_assert_equal_p(bmp, NULL);
};
void test_graphics_context_${BIT_DEPTH_NAME}__lock_framebuffer_1Bit_on_8BitCircular_must_fail(void) {
// Test for locking of 1Bit framebuffer when frambuffer is 8Bit Circular
GContext ctx = {.dest_bitmap.info.format = GBitmapFormat8BitCircular};
GBitmap *bmp = graphics_capture_frame_buffer_format(&ctx, GBitmapFormat1Bit);
cl_assert(ctx.lock == false);
cl_assert_equal_p(bmp, NULL);
};
void test_graphics_context_${BIT_DEPTH_NAME}__lock_framebuffer_2BitPalette_must_fail(void) {
// Ensure that the code path for unsupported capture formats leaves the GContext unlocked.
GContext ctx = {.dest_bitmap.info.format = GBitmapFormat8Bit};
GBitmap *bmp = graphics_capture_frame_buffer_format(&ctx, GBitmapFormat2BitPalette);
cl_assert(ctx.lock == false);
cl_assert_equal_p(bmp, NULL);
}
@@ -0,0 +1,699 @@
/*
* 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/graphics/graphics.h"
#include "applib/graphics/graphics_private.h"
#include "applib/graphics/graphics_private_raw.h"
#include "applib/graphics/graphics_private_raw_mask.h"
#include "applib/graphics/gtypes.h"
#include "applib/graphics/framebuffer.h"
#include "applib/ui/window_private.h"
#include "applib/ui/layer.h"
#include "util/graphics.h"
#include "util/trig.h"
#include "clar.h"
#include "util.h"
#include <stdio.h>
// Helper Includes
////////////////////////////////////
#include "test_graphics.h"
#include "test_graphics_mask.h"
#include "8bit/test_framebuffer.h"
// Stubs
////////////////////////////////////
#include "graphics_common_stubs.h"
////////////////////////////////////////////////////////////////////////////////////////////////////
// Setup and Teardown
////////////////////////////////////////////////////////////////////////////////////////////////////
static FrameBuffer *s_fb = NULL;
static GContext *s_ctx = NULL;
static GBitmap *s_dest_bitmap = NULL;
// Setup
void test_graphics_context_mask__initialize(void) {
s_fb = malloc(sizeof(*s_fb));
s_ctx = malloc(sizeof(*s_ctx));
framebuffer_init(s_fb, &(GSize) {DISP_COLS, DISP_ROWS});
test_graphics_context_init(s_ctx, s_fb);
framebuffer_clear(s_fb);
}
void test_graphics_context_mask__cleanup(void) {
free(s_ctx);
free(s_fb);
gbitmap_destroy(s_dest_bitmap);
s_dest_bitmap = NULL;
}
////////////////////////////////////////////////////////////////////////////////////////////////////
// Helpers
////////////////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////
// COMMON HELPERS //
///////////////////////////////////////
#define CHECK_EXPECTED_TEST_IMAGE(ctx) (cl_check(gbitmap_pbi_eq(&ctx->dest_bitmap, TEST_PBI_FILE)))
static const int16_t num_pixels_per_mask_value = 5;
static const int16_t num_mask_values = 4;
static const int16_t num_src_colors = 256; // 8-bit color
static const int16_t num_dest_colors = 64; // Framebuffer ignores alpha, so only 6-bit color
static void prv_prepare_canvas(GContext *ctx, GSize desired_size) {
s_dest_bitmap = gbitmap_create_blank(desired_size, GBitmapFormat8Bit);
ctx->dest_bitmap = *s_dest_bitmap;
ctx->draw_state.clip_box.size = desired_size;
ctx->draw_state.drawing_box.size = desired_size;
}
///////////////////////////////////////
// RECORDING HORIZONTAL LINE HELPERS //
///////////////////////////////////////
static void prv_prepare_canvas_for_hline_recording_test(GContext *ctx) {
const GSize bitmap_size = GSize(num_mask_values * num_pixels_per_mask_value, num_src_colors);
prv_prepare_canvas(ctx, bitmap_size);
}
static GDrawMask *prv_create_mask_for_hline_recording_test(GContext *ctx) {
// The initial transparency doesn't really matter since we're about to overwrite it
const bool transparent = false;
GDrawMask *mask = graphics_context_mask_create(ctx, transparent);
cl_assert(mask);
const GSize mask_size = ctx->dest_bitmap.bounds.size;
for (int16_t x = 0; x < mask_size.w; x++) {
const uint8_t mask_pixel_value = (uint8_t)(x / num_pixels_per_mask_value);
for (int16_t y = 0; y < mask_size.h; y++) {
test_graphics_context_mask_set_value_for_coordinate(ctx, mask, mask_pixel_value,
GPoint(x, y));
}
}
return mask;
}
typedef void (*HorizontalClippingMaskRecordFunc)(GContext *ctx, int16_t y, int16_t x1, int16_t x2,
GColor color);
static void prv_mask_record_hline_test_pattern(HorizontalClippingMaskRecordFunc record_func) {
GContext *ctx = s_ctx;
prv_prepare_canvas_for_hline_recording_test(ctx);
graphics_context_set_antialiased(ctx, true);
GDrawMask *mask = prv_create_mask_for_hline_recording_test(ctx);
cl_assert(graphics_context_mask_record(ctx, mask));
for (int16_t y = 0; y < num_src_colors; y++) {
GColor src_color = (GColor) { .argb = (uint8_t)y };
for (int mask_value_index = 0; mask_value_index < num_mask_values; mask_value_index++) {
int16_t x1 = (int16_t)(mask_value_index * num_pixels_per_mask_value);
int16_t x2 = (int16_t)(x1 + num_pixels_per_mask_value - 1);
record_func(ctx, y, x1, x2, src_color);
}
}
cl_assert(graphics_context_mask_record(ctx, NULL));
test_graphics_context_mask_debug(ctx, mask);
graphics_context_mask_destroy(ctx, mask);
}
///////////////////////////////////////
// RECORDING VERTICAL LINE HELPERS //
///////////////////////////////////////
static void prv_prepare_canvas_for_vline_recording_test(GContext *ctx) {
const GSize bitmap_size = GSize(num_src_colors, num_mask_values * num_pixels_per_mask_value);
prv_prepare_canvas(ctx, bitmap_size);
}
static GDrawMask *prv_create_mask_for_vline_recording_test(GContext *ctx) {
// The initial transparency doesn't really matter since we're about to overwrite it
const bool transparent = false;
GDrawMask *mask = graphics_context_mask_create(ctx, transparent);
cl_assert(mask);
const GSize mask_size = ctx->dest_bitmap.bounds.size;
for (int16_t y = 0; y < mask_size.h; y++) {
const uint8_t mask_pixel_value = (uint8_t)(y / num_pixels_per_mask_value);
for (int16_t x = 0; x < mask_size.w; x++) {
test_graphics_context_mask_set_value_for_coordinate(ctx, mask, mask_pixel_value,
GPoint(x, y));
}
}
return mask;
}
typedef void (*VerticalClippingMaskRecordFunc)(GContext *ctx, int16_t x, int16_t y1, int16_t y2,
GColor color);
static void prv_mask_record_vline_test_pattern(VerticalClippingMaskRecordFunc record_func) {
GContext *ctx = s_ctx;
prv_prepare_canvas_for_vline_recording_test(ctx);
graphics_context_set_antialiased(ctx, true);
GDrawMask *mask = prv_create_mask_for_vline_recording_test(ctx);
cl_assert(graphics_context_mask_record(ctx, mask));
for (int16_t x = 0; x < num_src_colors; x++) {
GColor src_color = (GColor) { .argb = (uint8_t)x };
for (int mask_value_index = 0; mask_value_index < num_mask_values; mask_value_index++) {
int16_t y1 = (int16_t)(mask_value_index * num_pixels_per_mask_value);
int16_t y2 = (int16_t)(y1 + num_pixels_per_mask_value - 1);
record_func(ctx, x, y1, y2, src_color);
}
}
cl_assert(graphics_context_mask_record(ctx, NULL));
test_graphics_context_mask_debug(ctx, mask);
graphics_context_mask_destroy(ctx, mask);
}
///////////////////////////////////////
// APPLYING HORIZONTAL LINE HELPERS //
///////////////////////////////////////
static const int16_t hline_applying_test_column_width = num_mask_values * num_pixels_per_mask_value;
static void prv_prepare_canvas_for_hline_applying_test(GContext *ctx) {
const GSize bitmap_size = GSize(num_dest_colors * hline_applying_test_column_width,
num_src_colors);
prv_prepare_canvas(ctx, bitmap_size);
// Fill the canvas so each column (of width hline_applying_test_column_width) is set to one of the
// different possible dest_colors
for (int16_t y = 0; y < bitmap_size.h; y++) {
for (int16_t column_index = 0; column_index < num_dest_colors; column_index++) {
const int16_t starting_x = column_index * hline_applying_test_column_width;
for (int16_t x = starting_x; x < starting_x + hline_applying_test_column_width; x++) {
ctx->draw_state.stroke_color = (GColor) { .argb = (uint8_t)(column_index | 0b11000000) };
graphics_draw_pixel(ctx, GPoint(x, y));
}
}
}
}
static GDrawMask *prv_create_mask_for_hline_applying_test(GContext *ctx) {
// The initial transparency doesn't really matter since we're about to overwrite it
const bool transparent = false;
GDrawMask *mask = graphics_context_mask_create(ctx, transparent);
cl_assert(mask);
const GSize mask_size = ctx->dest_bitmap.bounds.size;
for (int16_t x = 0; x < mask_size.w; x++) {
const uint8_t mask_pixel_value = (uint8_t)((x % hline_applying_test_column_width) /
num_pixels_per_mask_value);
for (int16_t y = 0; y < mask_size.h; y++) {
test_graphics_context_mask_set_value_for_coordinate(ctx, mask, mask_pixel_value,
GPoint(x, y));
}
}
return mask;
}
typedef void (*HorizontalClippingMaskApplyFunc)(GContext *ctx, int16_t y, int16_t x1, int16_t x2,
GColor color);
static void prv_mask_apply_hline_test_pattern(HorizontalClippingMaskApplyFunc apply_func) {
GContext *ctx = s_ctx;
prv_prepare_canvas_for_hline_applying_test(ctx);
graphics_context_set_antialiased(ctx, true);
GDrawMask *mask = prv_create_mask_for_hline_applying_test(ctx);
cl_assert(graphics_context_mask_use(ctx, mask));
for (int16_t y = 0; y < num_src_colors; y++) {
GColor src_color = (GColor) { .argb = (uint8_t)y };
for (int dest_color_index = 0; dest_color_index < num_dest_colors; dest_color_index++) {
for (int mask_value_index = 0; mask_value_index < num_mask_values; mask_value_index++) {
int16_t x1 = (int16_t)((dest_color_index * hline_applying_test_column_width) +
(mask_value_index * num_pixels_per_mask_value));
int16_t x2 = (int16_t)(x1 + num_pixels_per_mask_value - 1);
apply_func(ctx, y, x1, x2, src_color);
}
}
}
graphics_context_mask_destroy(ctx, mask);
}
//////////////////////////////////////
// APPLYING VERTICAL LINE HELPERS //
//////////////////////////////////////
static const int16_t vline_applying_test_row_height = hline_applying_test_column_width;
static void prv_prepare_canvas_for_vline_applying_test(GContext *ctx) {
const GSize bitmap_size = GSize(num_src_colors,
num_dest_colors * hline_applying_test_column_width);
prv_prepare_canvas(ctx, bitmap_size);
// Fill the canvas so each row (of width vline_applying_test_row_height) is set to one of the
// different possible dest_colors
for (int16_t x = 0; x < bitmap_size.w; x++) {
for (int16_t row_index = 0; row_index < num_dest_colors; row_index++) {
const int16_t starting_y = row_index * vline_applying_test_row_height;
for (int16_t y = starting_y; y < starting_y + vline_applying_test_row_height; y++) {
ctx->draw_state.stroke_color = (GColor) { .argb = (uint8_t)(row_index | 0b11000000) };
graphics_draw_pixel(ctx, GPoint(x, y));
}
}
}
}
static GDrawMask *prv_create_mask_for_vline_applying_test(GContext *ctx) {
// The initial transparency doesn't really matter since we're about to overwrite it
const bool transparent = false;
GDrawMask *mask = graphics_context_mask_create(ctx, transparent);
cl_assert(mask);
const GSize mask_size = ctx->dest_bitmap.bounds.size;
for (int16_t y = 0; y < mask_size.h; y++) {
const uint8_t mask_pixel_value = (uint8_t)((y % vline_applying_test_row_height) /
num_pixels_per_mask_value);
for (int16_t x = 0; x < mask_size.w; x++) {
test_graphics_context_mask_set_value_for_coordinate(ctx, mask, mask_pixel_value,
GPoint(x, y));
}
}
return mask;
}
typedef void (*VerticalClippingMaskApplyFunc)(GContext *ctx, int16_t x, int16_t y1, int16_t y2,
GColor color);
static void prv_mask_apply_vline_test_pattern(VerticalClippingMaskApplyFunc apply_func) {
GContext *ctx = s_ctx;
prv_prepare_canvas_for_vline_applying_test(ctx);
graphics_context_set_antialiased(ctx, true);
GDrawMask *mask = prv_create_mask_for_vline_applying_test(ctx);
cl_assert(graphics_context_mask_use(ctx, mask));
for (int16_t x = 0; x < num_src_colors; x++) {
GColor src_color = (GColor) { .argb = (uint8_t)x };
for (int dest_color_index = 0; dest_color_index < num_dest_colors; dest_color_index++) {
for (int mask_value_index = 0; mask_value_index < num_mask_values; mask_value_index++) {
int16_t y1 = (int16_t)((dest_color_index * vline_applying_test_row_height) +
(mask_value_index * num_pixels_per_mask_value));
int16_t y2 = (int16_t)(y1 + num_pixels_per_mask_value - 1);
apply_func(ctx, x, y1, y2, src_color);
}
}
}
graphics_context_mask_destroy(ctx, mask);
}
////////////////////////////////////////////////////////////////////////////////////////////////////
// Tests
////////////////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////
// RECORDING HORIZONTAL LINE TESTS //
/////////////////////////////////////
// These tests initialize a mask that has 4 columns, each of width `num_pixels_per_mask_value`.
// Each of these columns in the mask is initialized to have one of the 4 possible mask values. Then,
// the test iterates over the mask's 256 rows, each of which uses a different src color
// (256 possibilities for 8-bit color) to draw a horizontal line (using the test's specified raw
// drawing function) for each column into the mask (recording).
void prv_mask_recording_assign_horizontal_line(GContext *ctx, int16_t y, Fixed_S16_3 x1,
Fixed_S16_3 x2, GColor color);
static void prv_hline_pattern_record_assign_horizontal_line_raw(GContext *ctx, int16_t y,
int16_t x1, int16_t x2,
GColor color) {
// x1 and x2 here will be the integer start/end of the line, so we need to adjust them so we see
// the same blending on the first and last pixel
x2--;
const Fixed_S16_3 x1_fixed = (Fixed_S16_3) {
.integer = x1,
.fraction = 4,
};
const Fixed_S16_3 x2_fixed = (Fixed_S16_3) {
.integer = x2,
.fraction = 4,
};
prv_mask_recording_assign_horizontal_line(ctx, y, x1_fixed, x2_fixed, color);
}
void test_graphics_context_mask__record_assign_horizontal_line_raw(void) {
prv_mask_record_hline_test_pattern(prv_hline_pattern_record_assign_horizontal_line_raw);
CHECK_EXPECTED_TEST_IMAGE(s_ctx);
};
void prv_mask_recording_blend_horizontal_line_raw(GContext *ctx, int16_t y, int16_t x1,
int16_t x2, GColor color);
void test_graphics_context_mask__record_blend_horizontal_line_raw(void) {
prv_mask_record_hline_test_pattern(prv_mask_recording_blend_horizontal_line_raw);
CHECK_EXPECTED_TEST_IMAGE(s_ctx);
};
void prv_mask_recording_assign_horizontal_line_delta_raw(GContext *ctx, int16_t y,
Fixed_S16_3 x1, Fixed_S16_3 x2,
uint8_t left_aa_offset,
uint8_t right_aa_offset,
int16_t clip_box_min_x,
int16_t clip_box_max_x,
GColor color);
static void prv_hline_pattern_record_assign_horizontal_line_delta_raw(GContext *ctx, int16_t y,
int16_t x1, int16_t x2,
GColor color) {
const Fixed_S16_3 x1_fixed = (Fixed_S16_3) { .integer = x1 };
Fixed_S16_3 x2_fixed = (Fixed_S16_3) { .integer = x2 };
const uint8_t gradient_width = (uint8_t)((x2 - x1) / 2);
x2_fixed.integer -= gradient_width;
const int16_t clip_box_min_x = ctx->draw_state.clip_box.origin.x;
const int16_t clip_box_max_x = (int16_t)(grect_get_max_x(&ctx->draw_state.clip_box) - 1);
prv_mask_recording_assign_horizontal_line_delta_raw(ctx, y, x1_fixed, x2_fixed, gradient_width,
gradient_width, clip_box_min_x,
clip_box_max_x, color);
}
void test_graphics_context_mask__record_assign_horizontal_line_delta_raw(void) {
prv_mask_record_hline_test_pattern(prv_hline_pattern_record_assign_horizontal_line_delta_raw);
CHECK_EXPECTED_TEST_IMAGE(s_ctx);
};
/////////////////////////////////////
// RECORDING VERTICAL LINE TESTS //
/////////////////////////////////////
// These tests initialize a mask that has 4 rows, each of height `num_pixels_per_mask_value`.
// Each of these rows in the mask is initialized to have one of the 4 possible mask values. Then,
// the test iterates over the mask's 256 columns, each of which uses a different src color
// (256 possibilities for 8-bit color) to draw a vertical line (using the test's specified raw
// drawing function) for each row into the mask (recording).
void prv_mask_recording_assign_vertical_line(GContext *ctx, int16_t x, Fixed_S16_3 y1,
Fixed_S16_3 y2, GColor color);
static void prv_vline_pattern_record_assign_vertical_line(GContext *ctx, int16_t x, int16_t y1,
int16_t y2, GColor color) {
// y1 and y2 here will be the integer start/end of the line, so we need to adjust them so we see
// the same blending on the first and last pixel
y2--;
const Fixed_S16_3 y1_fixed = (Fixed_S16_3) {
.integer = y1,
.fraction = 4,
};
const Fixed_S16_3 y2_fixed = (Fixed_S16_3) {
.integer = y2,
.fraction = 4,
};
prv_mask_recording_assign_vertical_line(ctx, x, y1_fixed, y2_fixed, color);
}
void test_graphics_context_mask__record_assign_vertical_line_raw(void) {
prv_mask_record_vline_test_pattern(prv_vline_pattern_record_assign_vertical_line);
CHECK_EXPECTED_TEST_IMAGE(s_ctx);
};
void prv_mask_recording_blend_vertical_line_raw(GContext *ctx, int16_t x, int16_t y1, int16_t y2,
GColor color);
void test_graphics_context_mask__record_blend_vertical_line_raw(void) {
prv_mask_record_vline_test_pattern(prv_mask_recording_blend_vertical_line_raw);
CHECK_EXPECTED_TEST_IMAGE(s_ctx);
};
/////////////////////////////////////
// APPLYING HORIZONTAL LINE TESTS //
/////////////////////////////////////
// These tests initialize a mask that has 4 * 64 columns, each of width `num_pixels_per_mask_value`.
// Each quad grouping of these columns in the mask is initialized to have one of the 4 possible mask
// values. Additionally, the test initializes the framebuffer to have 64 columns, each of width
// `4 * num_pixels_per_mask_value`, where each column is one of the 64 possible dest_colors (the
// framebuffer ignores alpha so 6-bit color). Then, the test activates the mask and iterates over
// the framebuffer's 256 rows, each of which uses a different src color
// (256 possibilities for 8-bit color) to draw a horizontal line (using the test's specified raw
// drawing function) for each `num_pixels_per_mask_value`-wide column.
void prv_assign_horizontal_line_raw(GContext *ctx, int16_t y, Fixed_S16_3 x1, Fixed_S16_3 x2,
GColor color);
static void prv_hline_pattern_apply_assign_horizontal_line_raw(GContext *ctx, int16_t y,
int16_t x1, int16_t x2,
GColor color) {
// x1 and x2 here will be the integer start/end of the line, so we need to adjust them so we see
// the same blending on the first and last pixel
x2--;
const Fixed_S16_3 x1_fixed = (Fixed_S16_3) {
.integer = x1,
.fraction = 4,
};
const Fixed_S16_3 x2_fixed = (Fixed_S16_3) {
.integer = x2,
.fraction = 4,
};
prv_assign_horizontal_line_raw(ctx, y, x1_fixed, x2_fixed, color);
}
void test_graphics_context_mask__apply_assign_horizontal_line_raw(void) {
prv_mask_apply_hline_test_pattern(prv_hline_pattern_apply_assign_horizontal_line_raw);
CHECK_EXPECTED_TEST_IMAGE(s_ctx);
};
void prv_blend_horizontal_line_raw(GContext *ctx, int16_t y, int16_t x1, int16_t x2,
GColor color);
void test_graphics_context_mask__apply_blend_horizontal_line_raw(void) {
prv_mask_apply_hline_test_pattern(prv_blend_horizontal_line_raw);
CHECK_EXPECTED_TEST_IMAGE(s_ctx);
};
void prv_assign_horizontal_line_delta_raw(GContext *ctx, int16_t y,
Fixed_S16_3 x1, Fixed_S16_3 x2,
uint8_t left_aa_offset, uint8_t right_aa_offset,
int16_t clip_box_min_x, int16_t clip_box_max_x,
GColor color);
static void prv_hline_pattern_apply_assign_horizontal_line_delta_raw(GContext *ctx, int16_t y,
int16_t x1, int16_t x2,
GColor color) {
// FIXME PBL-34552: This test produces an incorrect image, see JIRA
const Fixed_S16_3 x1_fixed = (Fixed_S16_3) { .integer = x1 };
Fixed_S16_3 x2_fixed = (Fixed_S16_3) { .integer = x2 };
const uint8_t gradient_width = (uint8_t)((x2 - x1) / 2);
x2_fixed.integer -= gradient_width;
const int16_t clip_box_min_x = ctx->draw_state.clip_box.origin.x;
const int16_t clip_box_max_x = (int16_t)(grect_get_max_x(&ctx->draw_state.clip_box) - 1);
prv_assign_horizontal_line_delta_raw(ctx, y, x1_fixed, x2_fixed, gradient_width, gradient_width,
clip_box_min_x, clip_box_max_x, color);
}
void test_graphics_context_mask__apply_assign_horizontal_line_delta_raw(void) {
prv_mask_apply_hline_test_pattern(prv_hline_pattern_apply_assign_horizontal_line_delta_raw);
CHECK_EXPECTED_TEST_IMAGE(s_ctx);
};
//////////////////////////////////
// APPLYING VERTICAL LINE TESTS //
//////////////////////////////////
// These tests initialize a mask that has 4 * 64 rows, each of height
// `vline_applying_test_row_height`. Each quad grouping of these rows in the mask is initialized to
// have one of the 4 possible mask values. Additionally, the test initializes the framebuffer to
// have 64 rows, each of height `4 * num_pixels_per_mask_value`, where each row is one of the 64
// possible dest_colors (the framebuffer ignores alpha so 6-bit color). Then, the test activates the
// mask and iterates over the framebuffer's 256 columns, each of which uses a different src color
// (256 possibilities for 8-bit color) to draw a vertical line (using the test's specified raw
// drawing function) for each `num_pixels_per_mask_value`-tall row.
void prv_assign_vertical_line_raw(GContext *ctx, int16_t x, Fixed_S16_3 y1, Fixed_S16_3 y2,
GColor color);
static void prv_vline_pattern_apply_assign_vertical_line(GContext *ctx, int16_t x, int16_t y1,
int16_t y2, GColor color) {
// y1 and y2 here will be the integer start/end of the line, so we need to adjust them so we see
// the same blending on the first and last pixel
y2--;
const Fixed_S16_3 y1_fixed = (Fixed_S16_3) {
.integer = y1,
.fraction = 4,
};
const Fixed_S16_3 y2_fixed = (Fixed_S16_3) {
.integer = y2,
.fraction = 4,
};
prv_assign_vertical_line_raw(ctx, x, y1_fixed, y2_fixed, color);
}
void test_graphics_context_mask__apply_assign_vertical_line_raw(void) {
prv_mask_apply_vline_test_pattern(prv_vline_pattern_apply_assign_vertical_line);
CHECK_EXPECTED_TEST_IMAGE(s_ctx);
};
void prv_blend_vertical_line_raw(GContext *ctx, int16_t x, int16_t y1, int16_t y2, GColor color);
void test_graphics_context_mask__apply_blend_vertical_line_raw(void) {
// FIXME PBL-34141: This test produces an incorrect image, see JIRA
prv_mask_apply_vline_test_pattern(prv_blend_vertical_line_raw);
CHECK_EXPECTED_TEST_IMAGE(s_ctx);
};
/////////////////
// BASIC TESTS //
/////////////////
// These tests test the basic functionality of the clipping mask API.
static void prv_verify_mask_pixel_values(const GContext *ctx, const GDrawMask *mask,
uint8_t expected_value) {
// Assumes rectangular framebuffer
cl_assert(ctx->dest_bitmap.info.format == GBitmapFormat8Bit);
const GSize framebuffer_size = ctx->dest_bitmap.bounds.size;
for (int16_t x = 0; x < framebuffer_size.w; x++) {
for (int16_t y = 0; y < framebuffer_size.h; y++) {
cl_assert_equal_i(test_graphics_context_mask_get_value_for_coordinate(ctx, mask,
GPoint(x, y)),
expected_value);
}
}
}
void test_graphics_context_mask__basic_create(void) {
GContext *ctx = s_ctx;
// Should be safe to call create with NULL values
cl_assert_equal_p(graphics_context_mask_create(NULL, NULL), NULL);
GDrawMask *transparent_mask = graphics_context_mask_create(ctx, true /* transparent */);
cl_assert(transparent_mask);
// Verify all mask pixels are initialized to be transparent (0)
prv_verify_mask_pixel_values(ctx, transparent_mask, 0);
graphics_context_mask_destroy(ctx, transparent_mask);
GDrawMask *opaque_mask = graphics_context_mask_create(ctx, false /* transparent */);
cl_assert(opaque_mask);
// Verify all mask pixels are initialized to be opaque (3)
prv_verify_mask_pixel_values(ctx, opaque_mask, 3);
graphics_context_mask_destroy(ctx, opaque_mask);
}
//extern const GDrawRawImplementation g_mask_recording_draw_implementation;
void test_graphics_context_mask__basic_record(void) {
GContext *ctx = s_ctx;
// Should be safe to call record with NULL values, will return false
cl_assert(!graphics_context_mask_record(NULL, NULL));
// Should start with default draw implementation
cl_assert_equal_p(ctx->draw_state.draw_implementation, &g_default_draw_implementation);
GDrawMask *mask1 = graphics_context_mask_create(ctx, true /* transparent */);
cl_assert(mask1);
cl_assert(graphics_context_mask_record(ctx, mask1));
// Should have switched to mask recording draw implementation
cl_assert_equal_p(ctx->draw_state.draw_implementation, &g_mask_recording_draw_implementation);
// Should have attached mask1 to GContext
cl_assert_equal_p(ctx->draw_state.draw_mask, mask1);
GDrawMask *mask2 = graphics_context_mask_create(ctx, true /* transparent */);
cl_assert(mask2);
cl_assert(graphics_context_mask_record(ctx, mask2));
// Should still be on mask recording draw implementation
cl_assert_equal_p(ctx->draw_state.draw_implementation, &g_mask_recording_draw_implementation);
// Should now have mask2 attached to GContext
cl_assert_equal_p(ctx->draw_state.draw_mask, mask2);
// Calling record with NULL should reset draw impl to default and mask in GContext to NULL
cl_assert(graphics_context_mask_record(ctx, NULL));
cl_assert_equal_p(ctx->draw_state.draw_implementation, &g_default_draw_implementation);
cl_assert_equal_p(ctx->draw_state.draw_mask, NULL);
graphics_context_mask_destroy(ctx, mask1);
graphics_context_mask_destroy(ctx, mask2);
}
void test_graphics_context_mask__basic_use(void) {
GContext *ctx = s_ctx;
// Should be safe to call use with NULL values, will return false
cl_assert(!graphics_context_mask_use(NULL, NULL));
// Should start with default draw implementation
cl_assert_equal_p(ctx->draw_state.draw_implementation, &g_default_draw_implementation);
GDrawMask *mask1 = graphics_context_mask_create(ctx, true /* transparent */);
cl_assert(mask1);
cl_assert(graphics_context_mask_use(ctx, mask1));
// Should still be on default draw implementation
cl_assert_equal_p(ctx->draw_state.draw_implementation, &g_default_draw_implementation);
// Should have attached mask1 to GContext
cl_assert_equal_p(ctx->draw_state.draw_mask, mask1);
GDrawMask *mask2 = graphics_context_mask_create(ctx, true /* transparent */);
cl_assert(mask2);
cl_assert(graphics_context_mask_use(ctx, mask2));
// Should still be on default draw implementation
cl_assert_equal_p(ctx->draw_state.draw_implementation, &g_default_draw_implementation);
// Should now have mask2 attached to GContext
cl_assert_equal_p(ctx->draw_state.draw_mask, mask2);
// Calling use with NULL should reset draw impl to default and mask in GContext to NULL
cl_assert(graphics_context_mask_use(ctx, NULL));
cl_assert_equal_p(ctx->draw_state.draw_implementation, &g_default_draw_implementation);
cl_assert_equal_p(ctx->draw_state.draw_mask, NULL);
graphics_context_mask_destroy(ctx, mask1);
graphics_context_mask_destroy(ctx, mask2);
}
void test_graphics_context_mask__basic_destroy(void) {
// Should be safe to call destroy on NULL values
graphics_context_mask_destroy(NULL, NULL);
}
@@ -0,0 +1,450 @@
/*
* 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/graphics/graphics.h"
#include "applib/graphics/framebuffer.h"
#include "applib/ui/bitmap_layer.h"
#include "applib/ui/window_private.h"
#include "util/size.h"
#include <stdio.h>
// Helper Functions
////////////////////////////////////
#include "test_graphics.h"
#include "../graphics/util.h"
#if (SCREEN_COLOR_DEPTH_BITS == 1)
#include "1bit/test_framebuffer.h"
#elif (SCREEN_COLOR_DEPTH_BITS == 8)
#include "8bit/test_framebuffer.h"
#else
#error "Unrecognized SCREEN_COLOR_DEPTH_BITS"
#endif
// Stubs
////////////////////////////////////
#include "graphics_common_stubs.h"
#include "stubs_applib_resource.h"
// Setup
////////////////////////////////////
static GBitmap test_image;
static GContext s_ctx;
static FrameBuffer *s_fb = NULL;
void test_graphics_draw_bitmap__initialize(void) {
s_fb = malloc(sizeof(FrameBuffer));
framebuffer_init(s_fb, &(GSize) {DISP_COLS, DISP_ROWS});
test_graphics_context_init(&s_ctx, s_fb);
read_pbi("no_litter_crop.png.pbi", &test_image);
}
void test_graphics_draw_bitmap__cleanup(void) {
free(s_fb);
free(test_image.addr);
}
// Layout Test Support
////////////////////////////////////
#define TEST_LAYER_SIZE (GSize(64, 110))
#define TEST_LAYER_OFFSET_ORIGIN (GPoint(80, 55))
static GPoint s_layer_test_image_bounds_offset;
static void prv_layer_test_update_proc(Layer *layer, GContext *ctx) {
GRect destination = test_image.bounds;
destination.origin = s_layer_test_image_bounds_offset;
graphics_context_set_compositing_mode(ctx, GCompOpAssign);
graphics_draw_bitmap_in_rect(ctx, &test_image, &destination);
}
static void prv_layer_test(GPoint layer_origin, GPoint image_bounds_offset) {
Layer layer;
const GRect layer_frame = (GRect) {
.origin = layer_origin,
.size = TEST_LAYER_SIZE,
};
layer_init(&layer, &layer_frame);
layer_set_update_proc(&layer, prv_layer_test_update_proc);
s_layer_test_image_bounds_offset = image_bounds_offset;
layer_render_tree(&layer, &s_ctx);
}
static void prv_origin_layer_test(GPoint image_bounds_offset) {
prv_layer_test(GPointZero, image_bounds_offset);
}
static void prv_offset_layer_test(GPoint image_bounds_offset) {
prv_layer_test(TEST_LAYER_OFFSET_ORIGIN, image_bounds_offset);
}
static void prv_bitmap_layer_test(GPoint frame_origin, GPoint bounds_origin) {
BitmapLayer bitmap_layer;
const GRect bitmap_layer_frame = (GRect) {
.origin = frame_origin,
.size = TEST_LAYER_SIZE,
};
bitmap_layer_init(&bitmap_layer, &bitmap_layer_frame);
Layer *layer = bitmap_layer_get_layer(&bitmap_layer);
GRect bitmap_layer_bounds = bitmap_layer_frame;
bitmap_layer_bounds.origin = bounds_origin;
layer_set_bounds(layer, &bitmap_layer_bounds);
bitmap_layer_set_bitmap(&bitmap_layer, &test_image);
bitmap_layer_set_compositing_mode(&bitmap_layer, GCompOpAssign);
layer_render_tree(bitmap_layer_get_layer(&bitmap_layer), &s_ctx);
}
static void prv_origin_bitmap_layer_test(GPoint bounds_origin) {
prv_bitmap_layer_test(GPointZero, bounds_origin);
}
static void prv_offset_bitmap_layer_test(GPoint bounds_origin) {
prv_bitmap_layer_test(TEST_LAYER_OFFSET_ORIGIN, bounds_origin);
}
// Composite Test Support
////////////////////////////////////
static GBitmap *prv_create_bitmap_from_png_file(const char *png_filename_without_extension) {
GBitmap *result = NULL;
char png_file_path[strlen(CLAR_FIXTURE_PATH) + 1 + strlen(GRAPHICS_FIXTURE_PATH) + 1 +
strlen(png_filename_without_extension) + 1];
sprintf(png_file_path, "%s/%s/%s.png", CLAR_FIXTURE_PATH, GRAPHICS_FIXTURE_PATH,
png_filename_without_extension);
FILE *fp = fopen(png_file_path, "r");
if (fp) {
// Go to the end of the file to discover the size of the file
if (fseek(fp, 0, SEEK_END) != 0) {
goto cleanup;
}
const long end_of_file = ftell(fp);
const size_t file_size = (size_t)end_of_file;
if (end_of_file == -1) {
goto cleanup;
}
uint8_t *png_data = malloc((size_t)file_size);
if (!png_data) {
goto cleanup;
}
// Go back to the start of the file and read it into the buffer
if (fseek(fp, 0L, SEEK_SET) != 0) {
goto cleanup;
}
cl_assert_equal_i(fread(png_data, 1, file_size, fp), file_size);
// Create a GBitmap using the PNG data
result = gbitmap_create_from_png_data(png_data, file_size);
if (result) {
free(png_data);
}
}
cleanup:
fclose(fp);
return result;
}
static GBitmap *prv_create_bitmap_from_pbi_file(const char *pbi_filename_without_extension) {
// Create a static GBitmap to avoid modifying the read_pbi() function to return a GBitmap
static GBitmap result;
char pbi_filename[PATH_STRING_LENGTH] = {0};
snprintf(pbi_filename, PATH_STRING_LENGTH, "%s.pbi", pbi_filename_without_extension);
if (!read_pbi(pbi_filename, &result)) {
return NULL;
} else {
return &result;
}
}
typedef GBitmap *(*CompositeTestGBitmapCreateFunc)(const char *filename);
typedef struct CompositeTest {
const char *test_name;
GBitmapFormat expected_test_image_bitmap_format;
CompositeTestGBitmapCreateFunc bitmap_create_func;
bool need_to_destroy_bitmap;
} CompositeTest;
static const CompositeTest s_composite_tests[] = {
{
.test_name = "1bitBW",
.expected_test_image_bitmap_format = GBitmapFormat1Bit,
.bitmap_create_func = prv_create_bitmap_from_pbi_file,
.need_to_destroy_bitmap = false,
},
{
.test_name = "2bitTrns",
.expected_test_image_bitmap_format = GBitmapFormat2BitPalette,
.bitmap_create_func = prv_create_bitmap_from_png_file,
.need_to_destroy_bitmap = true,
},
#if PBL_COLOR
{
.test_name = "4bitTrns",
.expected_test_image_bitmap_format = GBitmapFormat4BitPalette,
.bitmap_create_func = prv_create_bitmap_from_png_file,
.need_to_destroy_bitmap = true,
},
{
.test_name = "8bitTrns",
.expected_test_image_bitmap_format = GBitmapFormat8Bit,
.bitmap_create_func = prv_create_bitmap_from_png_file,
.need_to_destroy_bitmap = true,
},
#endif
};
#define COMPOSITE_TEST_IMAGE_SIZE_WIDTH (100)
#define COMPOSITE_TEST_OFFSET_X (COMPOSITE_TEST_IMAGE_SIZE_WIDTH / 2)
#define COMPOSITE_TEST_OFFSET_Y (0)
static bool prv_gbitmap_format_and_compositing_mode_combo_is_valid(GBitmapFormat bitmap_format,
GCompOp compositing_mode) {
return !((bitmap_format != GBitmapFormat1Bit) &&
((compositing_mode == GCompOpAssignInverted) ||
(compositing_mode == GCompOpOr) ||
(compositing_mode == GCompOpAnd) ||
(compositing_mode == GCompOpClear)));
}
static void prv_composite_test_draw_bitmap(GContext *ctx, GBitmap *bitmap, GPoint offset,
GCompOp compositing_mode) {
GRect destination = bitmap->bounds;
destination.origin = gpoint_add(destination.origin, offset);
graphics_context_set_compositing_mode(ctx, GCompOpAssign);
graphics_draw_bitmap_in_rect(ctx, bitmap, &destination);
destination.origin = gpoint_add(destination.origin, GPoint(COMPOSITE_TEST_OFFSET_X,
COMPOSITE_TEST_OFFSET_Y));
graphics_context_set_compositing_mode(ctx, compositing_mode);
graphics_draw_bitmap_in_rect(ctx, bitmap, &destination);
}
static void prv_composite_test(const char *unit_test_name, GCompOp compositing_mode) {
for (size_t i = 0; i < ARRAY_LENGTH(s_composite_tests); i++) {
const CompositeTest *test_data = &s_composite_tests[i];
// Skip invalid GBitmapFormat and GCompOp combinations
if (!prv_gbitmap_format_and_compositing_mode_combo_is_valid(
test_data->expected_test_image_bitmap_format, compositing_mode)) {
break;
}
framebuffer_clear(s_fb);
char test_image_filename[PATH_STRING_LENGTH] = {0};
snprintf(test_image_filename, PATH_STRING_LENGTH, "test_graphics_draw_bitmap_%s_test_image",
test_data->test_name);
GBitmap *bitmap = test_data->bitmap_create_func(test_image_filename);
cl_assert(bitmap);
cl_assert(gbitmap_get_format(bitmap) == test_data->expected_test_image_bitmap_format);
// Draw the two variations of the test image at GPointZero
prv_composite_test_draw_bitmap(&s_ctx, bitmap, GPointZero, compositing_mode);
// Then redraw the two variations offset so the bottom right edge of the right variation
// is aligned with the bottom right edge of the framebuffer
const GPoint framebuffer_bottom_right_point =
GPoint(grect_get_max_x(&s_ctx.dest_bitmap.bounds),
grect_get_max_y(&s_ctx.dest_bitmap.bounds));
GPoint offset_point = gpoint_sub(framebuffer_bottom_right_point,
GPoint(bitmap->bounds.size.w, bitmap->bounds.size.h));
offset_point = gpoint_sub(offset_point, GPoint(COMPOSITE_TEST_OFFSET_X,
COMPOSITE_TEST_OFFSET_Y));
prv_composite_test_draw_bitmap(&s_ctx, bitmap, offset_point, compositing_mode);
// Check the result
char unit_test_result_image_file_base_name[PATH_STRING_LENGTH] = {0};
snprintf(unit_test_result_image_file_base_name, PATH_STRING_LENGTH,
"%s_%s", unit_test_name, test_data->test_name);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap,
namecat(unit_test_result_image_file_base_name, ".pbi")));
if (test_data->need_to_destroy_bitmap) {
gbitmap_destroy(bitmap);
}
}
}
// Tests
////////////////////////////////////
void test_graphics_draw_bitmap__origin_layer_inside(void) {
prv_origin_layer_test(GPointZero);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_graphics_draw_bitmap__origin_layer_across_x(void) {
prv_origin_layer_test(GPoint(25, 0));
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_graphics_draw_bitmap__origin_layer_across_nx(void) {
prv_origin_layer_test(GPoint(-25, 0));
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_graphics_draw_bitmap__origin_layer_across_y(void) {
prv_origin_layer_test(GPoint(0, 40));
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_graphics_draw_bitmap__origin_layer_across_ny(void) {
prv_origin_layer_test(GPoint(0, -40));
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_graphics_draw_bitmap__origin_bitmap_layer_inside(void) {
prv_origin_bitmap_layer_test(GPointZero);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_graphics_draw_bitmap__origin_bitmap_layer_across_x(void) {
prv_origin_bitmap_layer_test(GPoint(25, 0));
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_graphics_draw_bitmap__origin_bitmap_layer_across_nx(void) {
prv_origin_bitmap_layer_test(GPoint(-25, 0));
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_graphics_draw_bitmap__origin_bitmap_layer_across_y(void) {
prv_origin_bitmap_layer_test(GPoint(0, 75));
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_graphics_draw_bitmap__origin_bitmap_layer_across_ny(void) {
prv_origin_bitmap_layer_test(GPoint(0, -25));
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_graphics_draw_bitmap__offset_layer_inside(void) {
prv_offset_layer_test(GPointZero);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_graphics_draw_bitmap__offset_layer_across_x(void) {
prv_offset_layer_test(GPoint(25, 0));
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_graphics_draw_bitmap__offset_layer_across_nx(void) {
prv_offset_layer_test(GPoint(-25, 0));
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_graphics_draw_bitmap__offset_layer_across_y(void) {
prv_offset_layer_test(GPoint(0, 40));
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_graphics_draw_bitmap__offset_layer_across_ny(void) {
prv_offset_layer_test(GPoint(0, -40));
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_graphics_draw_bitmap__offset_bitmap_layer_inside(void) {
prv_offset_bitmap_layer_test(GPointZero);
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_graphics_draw_bitmap__offset_bitmap_layer_across_x(void) {
prv_offset_bitmap_layer_test(GPoint(25, 0));
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_graphics_draw_bitmap__offset_bitmap_layer_across_nx(void) {
prv_offset_bitmap_layer_test(GPoint(-25, 0));
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_graphics_draw_bitmap__offset_bitmap_layer_across_y(void) {
prv_offset_bitmap_layer_test(GPoint(0, 75));
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_graphics_draw_bitmap__offset_bitmap_layer_across_ny(void) {
prv_offset_bitmap_layer_test(GPoint(0, -25));
cl_check(gbitmap_pbi_eq(&s_ctx.dest_bitmap, TEST_PBI_FILE));
}
void test_graphics_draw_bitmap__composite_assign(void) {
prv_composite_test(__func__, GCompOpAssign);
}
void test_graphics_draw_bitmap__composite_assign_inverted(void) {
prv_composite_test(__func__, GCompOpAssignInverted);
}
void test_graphics_draw_bitmap__composite_or(void) {
prv_composite_test(__func__, GCompOpOr);
}
void test_graphics_draw_bitmap__composite_and(void) {
prv_composite_test(__func__, GCompOpAnd);
}
void test_graphics_draw_bitmap__composite_clear(void) {
prv_composite_test(__func__, GCompOpClear);
}
void test_graphics_draw_bitmap__composite_set(void) {
prv_composite_test(__func__, GCompOpSet);
}
void test_graphics_draw_bitmap__composite_tint(void) {
graphics_context_set_tint_color(&s_ctx, GColorOrange);
prv_composite_test(__func__, GCompOpTint);
}
void test_graphics_draw_bitmap__composite_tint_luminance_black_opaque(void) {
graphics_context_set_tint_color(&s_ctx, GColorBlack);
prv_composite_test(__func__, GCompOpTintLuminance);
}
void test_graphics_draw_bitmap__composite_tint_luminance_black_semitransparent(void) {
GColor tint_color = GColorBlack;
tint_color.a = 2;
// We have to set the tint color directly in the GContext because
// graphics_context_set_tint_color() calls gcolor_closest_opaque() on the color you give it
s_ctx.draw_state.tint_color = tint_color;
prv_composite_test(__func__, GCompOpTintLuminance);
}
void test_graphics_draw_bitmap__composite_tint_luminance_blue_opaque(void) {
graphics_context_set_tint_color(&s_ctx, GColorBlue);
prv_composite_test(__func__, GCompOpTintLuminance);
}
@@ -0,0 +1,926 @@
/*
* 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/graphics/graphics.h"
#include "applib/graphics/graphics_circle_private.h"
#include "applib/graphics/framebuffer.h"
#include "util/trig.h"
#include "applib/ui/window_private.h"
#include "applib/ui/layer.h"
#include "util/size.h"
#include "clar.h"
#include "util.h"
#include "pebble_asserts.h"
#include <stdio.h>
// Helper Functions
////////////////////////////////////
#include "test_graphics.h"
#include "${BIT_DEPTH_NAME}/test_framebuffer.h"
// Stubs
////////////////////////////////////
#include "graphics_common_stubs.h"
#include "stubs_applib_resource.h"
static FrameBuffer *fb = NULL;
// Setup
void test_graphics_draw_circle_${BIT_DEPTH_NAME}__initialize(void) {
fb = malloc(sizeof(FrameBuffer));
framebuffer_init(fb, &(GSize) {DISP_COLS, DISP_ROWS});
}
// Teardown
void test_graphics_draw_circle_${BIT_DEPTH_NAME}__cleanup(void) {
free(fb);
}
// Tests
////////////////////////////////////
void inside_layer_update_callback(Layer* me, GContext* ctx) {
graphics_context_set_stroke_color(ctx, GColorBlack);
graphics_context_set_antialiased(ctx, false);
graphics_draw_circle(ctx, GPoint(14, 14), 12);
}
void white_layer_update_callback(Layer* me, GContext* ctx) {
graphics_context_set_stroke_color(ctx, GColorWhite);
graphics_context_set_antialiased(ctx, false);
graphics_draw_circle(ctx, GPoint(14, 14), 12);
}
void clear_layer_update_callback(Layer* me, GContext* ctx) {
graphics_context_set_stroke_color(ctx, GColorClear);
graphics_context_set_antialiased(ctx, false);
graphics_draw_circle(ctx, GPoint(14, 14), 12);
}
void across_x_layer_update_callback(Layer* me, GContext* ctx) {
graphics_context_set_stroke_color(ctx, GColorBlack);
graphics_context_set_antialiased(ctx, false);
graphics_draw_circle(ctx, GPoint(28, 14), 12);
}
void across_nx_layer_update_callback(Layer* me, GContext* ctx) {
graphics_context_set_stroke_color(ctx, GColorBlack);
graphics_context_set_antialiased(ctx, false);
graphics_draw_circle(ctx, GPoint(-14, 14), 12);
}
void across_y_layer_update_callback(Layer* me, GContext* ctx) {
graphics_context_set_stroke_color(ctx, GColorBlack);
graphics_context_set_antialiased(ctx, false);
graphics_draw_circle(ctx, GPoint(14, 28), 12);
}
void across_ny_layer_update_callback(Layer* me, GContext* ctx) {
graphics_context_set_stroke_color(ctx, GColorBlack);
graphics_context_set_antialiased(ctx, false);
graphics_draw_circle(ctx, GPoint(14, -14), 12);
}
void test_graphics_draw_circle_${BIT_DEPTH_NAME}__origin_layer(void) {
GContext ctx;
Layer layer;
test_graphics_context_init(&ctx, fb);
layer_init(&layer, &GRect(0, 0, 28, 28));
layer_set_update_proc(&layer, &inside_layer_update_callback);
layer_render_tree(&layer, &ctx);
cl_check(gbitmap_pbi_eq(&ctx.dest_bitmap, "draw_circle_inside_origin_layer.${BIT_DEPTH_NAME}.pbi"));
test_graphics_context_reset(&ctx, fb);
layer_set_update_proc(&layer, &across_x_layer_update_callback);
layer_render_tree(&layer, &ctx);
cl_check(gbitmap_pbi_eq(&ctx.dest_bitmap, "draw_circle_across_x_origin_layer.${BIT_DEPTH_NAME}.pbi"));
test_graphics_context_reset(&ctx, fb);
layer_set_update_proc(&layer, &across_nx_layer_update_callback);
layer_render_tree(&layer, &ctx);
cl_check(gbitmap_pbi_eq(&ctx.dest_bitmap, "draw_circle_across_nx_origin_layer.${BIT_DEPTH_NAME}.pbi"));
test_graphics_context_reset(&ctx, fb);
layer_set_update_proc(&layer, &across_y_layer_update_callback);
layer_render_tree(&layer, &ctx);
cl_check(gbitmap_pbi_eq(&ctx.dest_bitmap, "draw_circle_across_y_origin_layer.${BIT_DEPTH_NAME}.pbi"));
test_graphics_context_reset(&ctx, fb);
layer_set_update_proc(&layer, &across_ny_layer_update_callback);
layer_render_tree(&layer, &ctx);
cl_check(gbitmap_pbi_eq(&ctx.dest_bitmap, "draw_circle_across_ny_origin_layer.${BIT_DEPTH_NAME}.pbi"));
}
#define RADIUS_BIG 15
#define RADIUS_MEDIUM 8
#define RADIUS_MIN_CALCULATED 3
#define RADIUS_MAX_PRECOMPUTED 2
#define RADIUS_SMALL 1
#define RADIUS_NONE 0
#define ORIGIN_RECT_NO_CLIP GRect(0, 0, 144, 168)
#define ORIGIN_RECT_CLIP_XY GRect(0, 0, 30, 40)
#define ORIGIN_RECT_CLIP_NXNY GRect(0, 0, 30, 40)
#define CENTER_OF_ORIGIN_RECT GPoint(20, 25)
#define CENTER_OF_ORIGIN_RECT_NXNY GPoint(10, 15)
void test_graphics_draw_circle_${BIT_DEPTH_NAME}__origin_layer_aa(void) {
GContext ctx;
test_graphics_context_init(&ctx, fb);
// Big circles
setup_test_aa_sw(&ctx, fb, ORIGIN_RECT_NO_CLIP, ORIGIN_RECT_NO_CLIP, true, 1);
graphics_draw_circle(&ctx, CENTER_OF_ORIGIN_RECT, RADIUS_BIG);
cl_check(gbitmap_pbi_eq(&ctx.dest_bitmap, "draw_circle_origin_aa_r16_no_clip.${BIT_DEPTH_NAME}.pbi"));
setup_test_aa_sw(&ctx, fb, ORIGIN_RECT_CLIP_XY, ORIGIN_RECT_CLIP_XY, true, 1);
graphics_draw_circle(&ctx, CENTER_OF_ORIGIN_RECT, RADIUS_BIG);
cl_check(gbitmap_pbi_eq(&ctx.dest_bitmap, "draw_circle_origin_aa_r16_clip_xy.${BIT_DEPTH_NAME}.pbi"));
setup_test_aa_sw(&ctx, fb, ORIGIN_RECT_CLIP_NXNY, ORIGIN_RECT_CLIP_NXNY, true, 1);
graphics_draw_circle(&ctx, CENTER_OF_ORIGIN_RECT_NXNY, RADIUS_BIG);
cl_check(gbitmap_pbi_eq(&ctx.dest_bitmap, "draw_circle_origin_aa_r16_clip_nxny.${BIT_DEPTH_NAME}.pbi"));
// Medium circles
setup_test_aa_sw(&ctx, fb, ORIGIN_RECT_NO_CLIP, ORIGIN_RECT_NO_CLIP, true, 1);
graphics_draw_circle(&ctx, CENTER_OF_ORIGIN_RECT, RADIUS_MEDIUM);
cl_check(gbitmap_pbi_eq(&ctx.dest_bitmap, "draw_circle_origin_aa_r8_no_clip.${BIT_DEPTH_NAME}.pbi"));
setup_test_aa_sw(&ctx, fb, ORIGIN_RECT_CLIP_XY, ORIGIN_RECT_CLIP_XY, true, 1);
graphics_draw_circle(&ctx, CENTER_OF_ORIGIN_RECT, RADIUS_MEDIUM);
cl_check(gbitmap_pbi_eq(&ctx.dest_bitmap, "draw_circle_origin_aa_r8_clip_xy.${BIT_DEPTH_NAME}.pbi"));
setup_test_aa_sw(&ctx, fb, ORIGIN_RECT_CLIP_NXNY, ORIGIN_RECT_CLIP_NXNY, true, 1);
graphics_draw_circle(&ctx, CENTER_OF_ORIGIN_RECT_NXNY, RADIUS_MEDIUM);
cl_check(gbitmap_pbi_eq(&ctx.dest_bitmap, "draw_circle_origin_aa_r8_clip_nxny.${BIT_DEPTH_NAME}.pbi"));
// Small circles
setup_test_aa_sw(&ctx, fb, ORIGIN_RECT_NO_CLIP, ORIGIN_RECT_NO_CLIP, true, 1);
graphics_draw_circle(&ctx, CENTER_OF_ORIGIN_RECT, RADIUS_SMALL);
cl_check(gbitmap_pbi_eq(&ctx.dest_bitmap, "draw_circle_origin_aa_r1_no_clip.${BIT_DEPTH_NAME}.pbi"));
setup_test_aa_sw(&ctx, fb, ORIGIN_RECT_CLIP_XY, ORIGIN_RECT_CLIP_XY, true, 1);
graphics_draw_circle(&ctx, CENTER_OF_ORIGIN_RECT, RADIUS_SMALL);
cl_check(gbitmap_pbi_eq(&ctx.dest_bitmap, "draw_circle_origin_aa_r1_clip_xy.${BIT_DEPTH_NAME}.pbi"));
setup_test_aa_sw(&ctx, fb, ORIGIN_RECT_CLIP_NXNY, ORIGIN_RECT_CLIP_NXNY, true, 1);
graphics_draw_circle(&ctx, CENTER_OF_ORIGIN_RECT_NXNY, RADIUS_SMALL);
cl_check(gbitmap_pbi_eq(&ctx.dest_bitmap, "draw_circle_origin_aa_r1_clip_nxny.${BIT_DEPTH_NAME}.pbi"));
// Testing of the special cases for radius:
// Radius of 3 - starting point for calculated edges
setup_test_aa_sw(&ctx, fb, ORIGIN_RECT_NO_CLIP, ORIGIN_RECT_NO_CLIP, true, 1);
graphics_draw_circle(&ctx, CENTER_OF_ORIGIN_RECT_NXNY, RADIUS_MIN_CALCULATED);
cl_check(gbitmap_pbi_eq(&ctx.dest_bitmap, "draw_circle_origin_aa_r3_no_clip.${BIT_DEPTH_NAME}.pbi"));
// Radius of 2 - ending point for precomputed edges
setup_test_aa_sw(&ctx, fb, ORIGIN_RECT_NO_CLIP, ORIGIN_RECT_NO_CLIP, true, 1);
graphics_draw_circle(&ctx, CENTER_OF_ORIGIN_RECT_NXNY, RADIUS_MAX_PRECOMPUTED);
cl_check(gbitmap_pbi_eq(&ctx.dest_bitmap, "draw_circle_origin_aa_r2_no_clip.${BIT_DEPTH_NAME}.pbi"));
// Radius of 0 - draw a single pixel
setup_test_aa_sw(&ctx, fb, ORIGIN_RECT_NO_CLIP, ORIGIN_RECT_NO_CLIP, true, 1);
graphics_draw_circle(&ctx, CENTER_OF_ORIGIN_RECT_NXNY, RADIUS_NONE);
cl_check(gbitmap_pbi_eq(&ctx.dest_bitmap, "draw_circle_origin_aa_r0_no_clip.${BIT_DEPTH_NAME}.pbi"));
}
void test_graphics_draw_circle_${BIT_DEPTH_NAME}__offset_layer(void) {
GContext ctx;
Layer layer;
test_graphics_context_init(&ctx, fb);
layer_init(&layer, &GRect(10, 15, 28, 28));
layer_set_update_proc(&layer, &inside_layer_update_callback);
layer_render_tree(&layer, &ctx);
cl_check(gbitmap_pbi_eq(&ctx.dest_bitmap, "draw_circle_inside_offset_layer.${BIT_DEPTH_NAME}.pbi"));
test_graphics_context_reset(&ctx, fb);
layer_set_update_proc(&layer, &across_x_layer_update_callback);
layer_render_tree(&layer, &ctx);
cl_check(gbitmap_pbi_eq(&ctx.dest_bitmap, "draw_circle_across_x_offset_layer.${BIT_DEPTH_NAME}.pbi"));
test_graphics_context_reset(&ctx, fb);
layer_set_update_proc(&layer, &across_nx_layer_update_callback);
layer_render_tree(&layer, &ctx);
cl_check(gbitmap_pbi_eq(&ctx.dest_bitmap, "draw_circle_across_nx_offset_layer.${BIT_DEPTH_NAME}.pbi"));
test_graphics_context_reset(&ctx, fb);
layer_set_update_proc(&layer, &across_y_layer_update_callback);
layer_render_tree(&layer, &ctx);
cl_check(gbitmap_pbi_eq(&ctx.dest_bitmap, "draw_circle_across_y_offset_layer.${BIT_DEPTH_NAME}.pbi"));
test_graphics_context_reset(&ctx, fb);
layer_set_update_proc(&layer, &across_ny_layer_update_callback);
layer_render_tree(&layer, &ctx);
cl_check(gbitmap_pbi_eq(&ctx.dest_bitmap, "draw_circle_across_ny_offset_layer.${BIT_DEPTH_NAME}.pbi"));
}
#define OFFSET_RECT_NO_CLIP GRect(10, 10, 40, 50)
#define OFFSET_RECT_CLIP_XY GRect(10, 10, 30, 40)
#define OFFSET_RECT_CLIP_NXNY GRect(0, 0, 30, 40)
#define CENTER_OF_OFFSET_RECT GPoint(10, 15)
#define CENTER_OF_OFFSET_RECT_NXNY GPoint(0, 5)
void test_graphics_draw_circle_${BIT_DEPTH_NAME}__offset_layer_aa(void) {
GContext ctx;
test_graphics_context_init(&ctx, fb);
// Big circles
setup_test_aa_sw(&ctx, fb, OFFSET_RECT_NO_CLIP, OFFSET_RECT_NO_CLIP, true, 1);
graphics_draw_circle(&ctx, CENTER_OF_OFFSET_RECT, RADIUS_BIG);
cl_check(gbitmap_pbi_eq(&ctx.dest_bitmap, "draw_circle_offset_aa_r16_no_clip.${BIT_DEPTH_NAME}.pbi"));
setup_test_aa_sw(&ctx, fb, OFFSET_RECT_CLIP_XY, OFFSET_RECT_CLIP_XY, true, 1);
graphics_draw_circle(&ctx, CENTER_OF_OFFSET_RECT, RADIUS_BIG);
cl_check(gbitmap_pbi_eq(&ctx.dest_bitmap, "draw_circle_offset_aa_r16_clip_xy.${BIT_DEPTH_NAME}.pbi"));
setup_test_aa_sw(&ctx, fb, OFFSET_RECT_CLIP_NXNY, OFFSET_RECT_CLIP_NXNY, true, 1);
graphics_draw_circle(&ctx, CENTER_OF_OFFSET_RECT_NXNY, RADIUS_BIG);
cl_check(gbitmap_pbi_eq(&ctx.dest_bitmap, "draw_circle_offset_aa_r16_clip_nxny.${BIT_DEPTH_NAME}.pbi"));
// Medium circles
setup_test_aa_sw(&ctx, fb, OFFSET_RECT_NO_CLIP, OFFSET_RECT_NO_CLIP, true, 1);
graphics_draw_circle(&ctx, CENTER_OF_OFFSET_RECT, RADIUS_MEDIUM);
cl_check(gbitmap_pbi_eq(&ctx.dest_bitmap, "draw_circle_offset_aa_r8_no_clip.${BIT_DEPTH_NAME}.pbi"));
setup_test_aa_sw(&ctx, fb, OFFSET_RECT_CLIP_XY, OFFSET_RECT_CLIP_XY, true, 1);
graphics_draw_circle(&ctx, CENTER_OF_OFFSET_RECT, RADIUS_MEDIUM);
cl_check(gbitmap_pbi_eq(&ctx.dest_bitmap, "draw_circle_offset_aa_r8_clip_xy.${BIT_DEPTH_NAME}.pbi"));
setup_test_aa_sw(&ctx, fb, OFFSET_RECT_CLIP_NXNY, OFFSET_RECT_CLIP_NXNY, true, 1);
graphics_draw_circle(&ctx, CENTER_OF_OFFSET_RECT_NXNY, RADIUS_MEDIUM);
cl_check(gbitmap_pbi_eq(&ctx.dest_bitmap, "draw_circle_offset_aa_r8_clip_nxny.${BIT_DEPTH_NAME}.pbi"));
// Small circles
setup_test_aa_sw(&ctx, fb, OFFSET_RECT_NO_CLIP, OFFSET_RECT_NO_CLIP, true, 1);
graphics_draw_circle(&ctx, CENTER_OF_OFFSET_RECT, RADIUS_SMALL);
cl_check(gbitmap_pbi_eq(&ctx.dest_bitmap, "draw_circle_offset_aa_r1_no_clip.${BIT_DEPTH_NAME}.pbi"));
setup_test_aa_sw(&ctx, fb, OFFSET_RECT_CLIP_XY, OFFSET_RECT_CLIP_XY, true, 1);
graphics_draw_circle(&ctx, CENTER_OF_OFFSET_RECT, RADIUS_SMALL);
cl_check(gbitmap_pbi_eq(&ctx.dest_bitmap, "draw_circle_offset_aa_r1_clip_xy.${BIT_DEPTH_NAME}.pbi"));
setup_test_aa_sw(&ctx, fb, OFFSET_RECT_CLIP_NXNY, OFFSET_RECT_CLIP_NXNY, true, 1);
graphics_draw_circle(&ctx, CENTER_OF_OFFSET_RECT_NXNY, RADIUS_SMALL);
cl_check(gbitmap_pbi_eq(&ctx.dest_bitmap, "draw_circle_offset_aa_r1_clip_nxny.${BIT_DEPTH_NAME}.pbi"));
}
extern void graphics_circle_quadrant_draw_1px_non_aa(GContext* ctx, GPoint p,
uint16_t radius,
GCornerMask quadrant);
void test_graphics_draw_circle_${BIT_DEPTH_NAME}__quadrants(void) {
GContext ctx;
test_graphics_context_init(&ctx, fb);
setup_test_aa_sw(&ctx, fb, ORIGIN_RECT_NO_CLIP, ORIGIN_RECT_NO_CLIP, false, 1);
graphics_circle_quadrant_draw_1px_non_aa(&ctx, CENTER_OF_ORIGIN_RECT, RADIUS_MEDIUM, GCornerTopLeft);
cl_check(gbitmap_pbi_eq(&ctx.dest_bitmap, "draw_circle_offset_r8_quad_top_left.${BIT_DEPTH_NAME}.pbi"));
setup_test_aa_sw(&ctx, fb, ORIGIN_RECT_NO_CLIP, ORIGIN_RECT_NO_CLIP, false, 1);
graphics_circle_quadrant_draw_1px_non_aa(&ctx, CENTER_OF_ORIGIN_RECT, RADIUS_MEDIUM, GCornerTopRight);
cl_check(gbitmap_pbi_eq(&ctx.dest_bitmap, "draw_circle_offset_r8_quad_top_right.${BIT_DEPTH_NAME}.pbi"));
setup_test_aa_sw(&ctx, fb, ORIGIN_RECT_NO_CLIP, ORIGIN_RECT_NO_CLIP, false, 1);
graphics_circle_quadrant_draw_1px_non_aa(&ctx, CENTER_OF_ORIGIN_RECT, RADIUS_MEDIUM, GCornerBottomLeft);
cl_check(gbitmap_pbi_eq(&ctx.dest_bitmap, "draw_circle_offset_r8_quad_bottom_left.${BIT_DEPTH_NAME}.pbi"));
setup_test_aa_sw(&ctx, fb, ORIGIN_RECT_NO_CLIP, ORIGIN_RECT_NO_CLIP, false, 1);
graphics_circle_quadrant_draw_1px_non_aa(&ctx, CENTER_OF_ORIGIN_RECT, RADIUS_MEDIUM, GCornerBottomRight);
cl_check(gbitmap_pbi_eq(&ctx.dest_bitmap, "draw_circle_offset_r8_quad_bottom_right.${BIT_DEPTH_NAME}.pbi"));
setup_test_aa_sw(&ctx, fb, ORIGIN_RECT_NO_CLIP, ORIGIN_RECT_NO_CLIP, false, 1);
graphics_circle_quadrant_draw_1px_non_aa(&ctx, CENTER_OF_ORIGIN_RECT, RADIUS_MEDIUM, GCornersTop);
cl_check(gbitmap_pbi_eq(&ctx.dest_bitmap, "draw_circle_offset_r8_quads_top.${BIT_DEPTH_NAME}.pbi"));
setup_test_aa_sw(&ctx, fb, ORIGIN_RECT_NO_CLIP, ORIGIN_RECT_NO_CLIP, false, 1);
graphics_circle_quadrant_draw_1px_non_aa(&ctx, CENTER_OF_ORIGIN_RECT, RADIUS_MEDIUM, GCornersBottom);
cl_check(gbitmap_pbi_eq(&ctx.dest_bitmap, "draw_circle_offset_r8_quads_bottom.${BIT_DEPTH_NAME}.pbi"));
setup_test_aa_sw(&ctx, fb, ORIGIN_RECT_NO_CLIP, ORIGIN_RECT_NO_CLIP, false, 1);
graphics_circle_quadrant_draw_1px_non_aa(&ctx, CENTER_OF_ORIGIN_RECT, RADIUS_MEDIUM, GCornersRight);
cl_check(gbitmap_pbi_eq(&ctx.dest_bitmap, "draw_circle_offset_r8_quads_right.${BIT_DEPTH_NAME}.pbi"));
setup_test_aa_sw(&ctx, fb, ORIGIN_RECT_NO_CLIP, ORIGIN_RECT_NO_CLIP, false, 1);
graphics_circle_quadrant_draw_1px_non_aa(&ctx, CENTER_OF_ORIGIN_RECT, RADIUS_MEDIUM, GCornersLeft);
cl_check(gbitmap_pbi_eq(&ctx.dest_bitmap, "draw_circle_offset_r8_quads_left.${BIT_DEPTH_NAME}.pbi"));
}
extern void graphics_circle_quadrant_draw_1px_aa(GContext* ctx, GPoint p,
uint16_t radius,
GCornerMask quadrant);
void test_graphics_draw_circle_${BIT_DEPTH_NAME}__quadrants_aa(void) {
#if PBL_COLOR
GContext ctx;
test_graphics_context_init(&ctx, fb);
typedef struct {
char *filename_part;
GCornerMask mask;
} TestConfig;
TestConfig test_config[] = {
{
.filename_part = "quad_top_left",
.mask = GCornerTopLeft,
},
{
.filename_part = "quad_top_right",
.mask = GCornerTopRight,
},
{
.filename_part = "quad_bottom_right",
.mask = GCornerBottomRight,
},
{
.filename_part = "quad_bottom_left",
.mask = GCornerBottomLeft,
},
{
.filename_part = "quads_top",
.mask = GCornersTop,
},
{
.filename_part = "quads_bottom",
.mask = GCornersBottom,
},
{
.filename_part = "quads_right",
.mask = GCornersRight,
},
{
.filename_part = "quads_left",
.mask = GCornersLeft,
},
};
// note: not the prettiest, fast a quick way to render all the scenarios Nitin was interested in
for (int i = 0; i < ARRAY_LENGTH(test_config); i++) {
TestConfig c = test_config[i];
setup_test_aa_sw(&ctx, fb, ORIGIN_RECT_NO_CLIP, ORIGIN_RECT_NO_CLIP, true, 1);
GPoint pt = CENTER_OF_ORIGIN_RECT;
// draw multiple quads with different radiuses
for (int r = 0; r <= 15; r++) {
graphics_circle_quadrant_draw_1px_aa(&ctx, pt, r, c.mask);
// center point follows a grid
pt.x += 30;
if (pt.x > 120) {
pt.x = CENTER_OF_ORIGIN_RECT.x;
pt.y += 30;
}
}
// construct file name and create meaningful assert description
char filename[100];
snprintf(filename, sizeof(filename),
"draw_circle_offset_aa_r8_%s.${BIT_DEPTH_NAME}.pbi", c.filename_part);
cl_check(gbitmap_pbi_eq(&ctx.dest_bitmap, filename));
}
#endif
}
void test_graphics_draw_circle_${BIT_DEPTH_NAME}__clear(void) {
GContext ctx;
Layer layer;
test_graphics_context_init(&ctx, fb);
layer_init(&layer, &GRect(0, 0, 60, 60));
layer_set_update_proc(&layer, &inside_layer_update_callback);
layer_render_tree(&layer, &ctx);
cl_check(gbitmap_pbi_eq(&ctx.dest_bitmap, "draw_circle_inside_origin_layer.${BIT_DEPTH_NAME}.pbi"));
layer_set_update_proc(&layer, &white_layer_update_callback);
layer_render_tree(&layer, &ctx);
cl_check(framebuffer_is_empty("white_over_black", ctx.parent_framebuffer, GColorWhite));
test_graphics_context_reset(&ctx, fb);
layer_set_update_proc(&layer, &inside_layer_update_callback);
layer_render_tree(&layer, &ctx);
cl_check(gbitmap_pbi_eq(&ctx.dest_bitmap, "draw_circle_inside_origin_layer.${BIT_DEPTH_NAME}.pbi"));
layer_set_update_proc(&layer, &clear_layer_update_callback);
layer_render_tree(&layer, &ctx);
#if PBL_COLOR
cl_check(gbitmap_pbi_eq(&ctx.dest_bitmap, "draw_circle_clear.8bit.pbi"));
#else
cl_check(framebuffer_is_empty("clear_over_black", ctx.parent_framebuffer, GColorWhite));
#endif
}
// Draws circles in a grid pattern while increasing the stroke width for each circle
static void prv_draw_circles(GContext *ctx) {
GPoint pt = CENTER_OF_ORIGIN_RECT;
for (int sw = 0; sw <= 10; sw++) {
ctx->draw_state.stroke_width = sw;
graphics_draw_circle(ctx, pt, 0);
// center point follows a grid
pt.x += 30;
if (pt.x > 120) {
pt.x = CENTER_OF_ORIGIN_RECT.x;
pt.y += 30;
}
}
}
#define TO_TRIG(deg) (((deg) * TRIG_MAX_ANGLE) / 360)
void test_graphics_draw_circle_${BIT_DEPTH_NAME}__zero_swX_black(void) {
GContext ctx;
test_graphics_context_init(&ctx, fb);
// Draw black circles with different alpha values in the stroke color with antialiasing disabled
setup_test_aa_sw(&ctx, fb, ORIGIN_RECT_NO_CLIP, ORIGIN_RECT_NO_CLIP, false, 0);
graphics_context_set_stroke_color(&ctx, GColorBlack);
prv_draw_circles(&ctx);
cl_check(gbitmap_pbi_eq(&ctx.dest_bitmap, "draw_circle_r0_swX_black.${BIT_DEPTH_NAME}.pbi"));
}
void test_graphics_draw_circle_${BIT_DEPTH_NAME}__zero_aa_swX_black(void) {
GContext ctx;
test_graphics_context_init(&ctx, fb);
// Draw black circles with different alpha values in the stroke color with antialiasing enabled
setup_test_aa_sw(&ctx, fb, ORIGIN_RECT_NO_CLIP, ORIGIN_RECT_NO_CLIP, true, 0);
graphics_context_set_stroke_color(&ctx, GColorBlack);
prv_draw_circles(&ctx);
cl_check(gbitmap_pbi_eq(&ctx.dest_bitmap, "draw_circle_r0_aa_swX_black.${BIT_DEPTH_NAME}.pbi"));
}
void test_graphics_draw_circle_8bit__zero_swX_color(void) {
GContext ctx;
test_graphics_context_init(&ctx, fb);
// Draw red circles with different alpha values in the stroke color with antialiasing disabled
setup_test_aa_sw(&ctx, fb, ORIGIN_RECT_NO_CLIP, ORIGIN_RECT_NO_CLIP, false, 0);
graphics_context_set_stroke_color(&ctx, GColorRed);
prv_draw_circles(&ctx);
cl_check(gbitmap_pbi_eq(&ctx.dest_bitmap, "draw_circle_r0_swX_color.8bit.pbi"));
}
void test_graphics_draw_circle_8bit__zero_aa_swX_color(void) {
GContext ctx;
test_graphics_context_init(&ctx, fb);
// Draw red circles with different alpha values in the stroke color with antialiasing enabled
setup_test_aa_sw(&ctx, fb, ORIGIN_RECT_NO_CLIP, ORIGIN_RECT_NO_CLIP, true, 0);
graphics_context_set_stroke_color(&ctx, GColorRed);
prv_draw_circles(&ctx);
cl_check(gbitmap_pbi_eq(&ctx.dest_bitmap, "draw_circle_r0_aa_swX_color.8bit.pbi"));
}
void test_graphics_draw_circle_${BIT_DEPTH_NAME}__zero_swX_clear(void) {
GContext ctx;
test_graphics_context_init(&ctx, fb);
// Draw clear circles with antialiasing disabled
setup_test_aa_sw(&ctx, fb, ORIGIN_RECT_NO_CLIP, ORIGIN_RECT_NO_CLIP, false, 0);
graphics_context_set_stroke_color(&ctx, GColorClear);
prv_draw_circles(&ctx);
cl_check(gbitmap_pbi_eq(&ctx.dest_bitmap, "draw_circle_r0_swX_clear.${BIT_DEPTH_NAME}.pbi"));
}
void test_graphics_draw_circle_${BIT_DEPTH_NAME}__zero_aa_swX_clear(void) {
GContext ctx;
test_graphics_context_init(&ctx, fb);
// Draw clear circles with antialiasing enabled
setup_test_aa_sw(&ctx, fb, ORIGIN_RECT_NO_CLIP, ORIGIN_RECT_NO_CLIP, true, 0);
graphics_context_set_stroke_color(&ctx, GColorClear);
prv_draw_circles(&ctx);
cl_check(gbitmap_pbi_eq(&ctx.dest_bitmap, "draw_circle_r0_aa_swX_clear.${BIT_DEPTH_NAME}.pbi"));
}
void test_graphics_draw_circle_${BIT_DEPTH_NAME}__arc_stroked(void){
GContext ctx;
test_graphics_context_init(&ctx, fb);
// Letter C
uint32_t angle_end = TRIG_MAX_ANGLE + (TRIG_MAX_ANGLE / 8);
uint32_t angle_start = (TRIG_MAX_ANGLE / 8) * 3;
setup_test_aa_sw(&ctx, fb, ORIGIN_RECT_NO_CLIP, ORIGIN_RECT_NO_CLIP, true, 4);
graphics_draw_arc_internal(&ctx, CENTER_OF_ORIGIN_RECT, RADIUS_BIG, angle_start, angle_end);
cl_check(gbitmap_pbi_eq(&ctx.dest_bitmap, "draw_arc_origin_aa_letter_c.${BIT_DEPTH_NAME}.pbi"));
// Stroke width bigger than radius
setup_test_aa_sw(&ctx, fb, ORIGIN_RECT_NO_CLIP, ORIGIN_RECT_NO_CLIP, true, RADIUS_BIG * 2);
graphics_draw_arc_internal(&ctx, CENTER_OF_ORIGIN_RECT, RADIUS_BIG, angle_start, angle_end);
cl_check(gbitmap_pbi_eq(&ctx.dest_bitmap, "draw_arc_origin_aa_stroke_bigger_than_radius.${BIT_DEPTH_NAME}.pbi"));
// Negative angle letter C
angle_end -= TRIG_MAX_ANGLE;
angle_start -= TRIG_MAX_ANGLE;
setup_test_aa_sw(&ctx, fb, ORIGIN_RECT_NO_CLIP, ORIGIN_RECT_NO_CLIP, true, 4);
graphics_draw_arc_internal(&ctx, CENTER_OF_ORIGIN_RECT, RADIUS_BIG, angle_start, angle_end);
cl_check(gbitmap_pbi_eq(&ctx.dest_bitmap, "draw_arc_origin_aa_letter_c.${BIT_DEPTH_NAME}.pbi"));
// More negative angle letter C (both angle are actually negative)
angle_end -= TRIG_MAX_ANGLE;
angle_start -= TRIG_MAX_ANGLE;
setup_test_aa_sw(&ctx, fb, ORIGIN_RECT_NO_CLIP, ORIGIN_RECT_NO_CLIP, true, 4);
graphics_draw_arc_internal(&ctx, CENTER_OF_ORIGIN_RECT, RADIUS_BIG, angle_start, angle_end);
cl_check(gbitmap_pbi_eq(&ctx.dest_bitmap, "draw_arc_origin_aa_letter_c.${BIT_DEPTH_NAME}.pbi"));
// Incorrect angles (angle_start > angle_end) - should result in empty image
setup_test_aa_sw(&ctx, fb, ORIGIN_RECT_NO_CLIP, ORIGIN_RECT_NO_CLIP, true, RADIUS_BIG * 2);
graphics_draw_arc_internal(&ctx, CENTER_OF_ORIGIN_RECT, RADIUS_BIG, angle_end, angle_start);
cl_check(framebuffer_is_empty(NULL, ctx.parent_framebuffer, GColorWhite));
// table with most popular angles to test
typedef struct {
char *filename_part;
uint32_t angle;
} TestAngles;
TestAngles test_angles[] = {
{
.filename_part = "__1_degrees",
.angle = TRIG_MAX_ANGLE / 360,
},
{
.filename_part = "__6_degrees",
.angle = TRIG_MAX_ANGLE / 60,
},
{
.filename_part = "_30_degrees",
.angle = TRIG_MAX_ANGLE / 12,
},
{
.filename_part = "_45_degrees",
.angle = TRIG_MAX_ANGLE / 8,
},
{
.filename_part = "_90_degrees",
.angle = TRIG_MAX_ANGLE / 4,
},
{
.filename_part = "181_degrees",
.angle = TRIG_MAX_ANGLE / 2 + TRIG_MAX_ANGLE / 360,
}
};
for (int i = 0; i < ARRAY_LENGTH(test_angles); i++) {
setup_test_aa_sw(&ctx, fb, ORIGIN_RECT_NO_CLIP, ORIGIN_RECT_NO_CLIP, true, 1);
GPoint pt = CENTER_OF_ORIGIN_RECT;
uint16_t radius = 10;
uint16_t stroke_width = 1;
for (int r = 1; r <= 7; r++) {
graphics_context_set_stroke_width(&ctx, stroke_width);
graphics_draw_arc_internal(&ctx, pt, radius, 0, test_angles[i].angle);
stroke_width += 1;
radius += 3;
pt.x += (radius + stroke_width) * 2;
if (pt.x > 120) {
pt.x = CENTER_OF_ORIGIN_RECT.x;
pt.y += (radius + stroke_width) * 2;
}
}
char filename[100];
snprintf(filename, sizeof(filename),
"draw_arc_offset_aa_end_angle_%s.${BIT_DEPTH_NAME}.pbi", test_angles[i].filename_part);
cl_check_(gbitmap_pbi_eq(&ctx.dest_bitmap, filename), filename);
}
for (int i = 0; i < ARRAY_LENGTH(test_angles); i++) {
setup_test_aa_sw(&ctx, fb, ORIGIN_RECT_NO_CLIP, ORIGIN_RECT_NO_CLIP, true, 1);
GPoint pt = CENTER_OF_ORIGIN_RECT;
uint16_t radius = 10;
uint16_t stroke_width = 1;
for (int r = 1; r <= 7; r++) {
graphics_context_set_stroke_width(&ctx, stroke_width);
graphics_draw_arc_internal(&ctx, pt, radius, test_angles[i].angle, TRIG_MAX_ANGLE);
stroke_width += 1;
radius += 3;
pt.x += (radius + stroke_width) * 2;
if (pt.x > 120) {
pt.x = CENTER_OF_ORIGIN_RECT.x;
pt.y += (radius + stroke_width) * 2;
}
}
char filename[100];
snprintf(filename, sizeof(filename),
"draw_arc_offset_aa_start_angle_%s.${BIT_DEPTH_NAME}.pbi", test_angles[i].filename_part);
cl_check_(gbitmap_pbi_eq(&ctx.dest_bitmap, filename), filename);
}
// Additional 90° end_angle unit tests:
angle_start = TO_TRIG(45);
angle_end = TO_TRIG(90);
setup_test_aa_sw(&ctx, fb, ORIGIN_RECT_NO_CLIP, ORIGIN_RECT_NO_CLIP, true, 5);
GPoint pt = CENTER_OF_ORIGIN_RECT;
// This test is sponsored by number 4
for (int r = 0; r < 4; r++) {
graphics_draw_arc_internal(&ctx, pt, RADIUS_BIG, angle_start, angle_end);
angle_start += TRIG_MAX_ANGLE/4;
angle_end += TRIG_MAX_ANGLE/4;
pt.x += RADIUS_BIG * 4;
if (pt.x > 120) {
pt.x = CENTER_OF_ORIGIN_RECT.x;
pt.y += RADIUS_BIG * 4;
}
}
cl_check(gbitmap_pbi_eq(&ctx.dest_bitmap, "draw_arc_aa_end_angle_on_divider.${BIT_DEPTH_NAME}.pbi"));
}
void test_graphics_draw_circle_8bit__arc_colors(void){
GContext ctx;
test_graphics_context_init(&ctx, fb);
// Letter C
uint32_t angle_end = TRIG_MAX_ANGLE + (TRIG_MAX_ANGLE / 8);
uint32_t angle_start = (TRIG_MAX_ANGLE / 8) * 3;
setup_test_aa_sw(&ctx, fb, ORIGIN_RECT_NO_CLIP, ORIGIN_RECT_NO_CLIP, true, 1);
graphics_context_set_fill_color(&ctx, GColorRed);
graphics_context_set_stroke_color(&ctx, GColorGreen);
graphics_draw_arc_internal(&ctx, CENTER_OF_ORIGIN_RECT, RADIUS_BIG, angle_start, angle_end);
cl_check(gbitmap_pbi_eq(&ctx.dest_bitmap, "draw_arc_origin_aa_letter_c_color_1px.8bit.pbi"));
// Letter C SW 4
setup_test_aa_sw(&ctx, fb, ORIGIN_RECT_NO_CLIP, ORIGIN_RECT_NO_CLIP, true, 4);
graphics_context_set_fill_color(&ctx, GColorRed);
graphics_context_set_stroke_color(&ctx, GColorGreen);
graphics_draw_arc_internal(&ctx, CENTER_OF_ORIGIN_RECT, RADIUS_BIG, angle_start, angle_end);
cl_check(gbitmap_pbi_eq(&ctx.dest_bitmap, "draw_arc_origin_aa_letter_c_color_4px.8bit.pbi"));
}
#define DRAWING_SPACING 40
char *mode_names[] = {
"_90_degrees",
"180_degrees",
"270_degrees",
};
char *precision_modes[] = {
"without_faction",
"with_fraction",
};
void test_graphics_draw_circle_${BIT_DEPTH_NAME}__radial_precise(void){
GContext ctx;
test_graphics_context_init(&ctx, fb);
uint32_t angle_start = TRIG_MAX_ANGLE / 8;
uint32_t angle_end = angle_start + (TRIG_MAX_ANGLE / 4);
GPointPrecise center = GPointPrecise(CENTER_OF_ORIGIN_RECT.x * 8, CENTER_OF_ORIGIN_RECT.y * 8);
Fixed_S16_3 radius = (Fixed_S16_3){.integer = RADIUS_BIG};
for (int precision_mode = 0; precision_mode < 2; precision_mode++) {
for (int mode = 0; mode < 3; mode++) {
setup_test_aa_sw(&ctx, fb, ORIGIN_RECT_NO_CLIP, ORIGIN_RECT_NO_CLIP, true, 4);
for (int angle_offset=0; angle_offset < 4; angle_offset++) {
graphics_draw_arc_precise_internal(&ctx, center, radius, angle_start, angle_end);
center.x.integer += (angle_offset % 2 == 0) ? DRAWING_SPACING : -DRAWING_SPACING;
center.y.integer += (angle_offset == 1) ? DRAWING_SPACING : 0;
angle_start += TRIG_MAX_ANGLE / 4;
angle_end += TRIG_MAX_ANGLE / 4;
}
char filename[100];
snprintf(filename, sizeof(filename),
"draw_arc_origin_aa_precise_%s_%s_.${BIT_DEPTH_NAME}.pbi", precision_modes[precision_mode], mode_names[mode]);
cl_check_(gbitmap_pbi_eq(&ctx.dest_bitmap, filename), filename);
center.y.integer -= DRAWING_SPACING;
angle_end += TRIG_MAX_ANGLE / 4;
}
center.x.fraction = 4;
center.y.fraction = 4;
radius.fraction = 4;
angle_start = TRIG_MAX_ANGLE / 8;
angle_end = angle_start + (TRIG_MAX_ANGLE / 4);
}
}
void test_graphics_draw_circle_${BIT_DEPTH_NAME}__calc_draw_config_caps(void) {
// ## Section with special cases
// Test for no angles
cl_assert_equal_edc(prv_calc_draw_config_ellipsis(TO_TRIG(0), TO_TRIG(0)), ((EllipsisDrawConfig){
.start_quadrant = (EllipsisPartDrawConfig){TO_TRIG(0),GCornerNone},
.full_quadrants = GCornerNone,
.end_quadrant = (EllipsisPartDrawConfig){TO_TRIG(0),GCornerNone}
}));
// Tests for 360° angle
cl_assert_equal_edc(prv_calc_draw_config_ellipsis(TO_TRIG(0), TO_TRIG(360)), ((EllipsisDrawConfig){
.start_quadrant = (EllipsisPartDrawConfig){TO_TRIG(0),GCornerNone},
.full_quadrants = GCornersAll,
.end_quadrant = (EllipsisPartDrawConfig){TO_TRIG(0),GCornerNone}
}));
cl_assert_equal_edc(prv_calc_draw_config_ellipsis(TO_TRIG(20), TO_TRIG(380)), ((EllipsisDrawConfig){
.start_quadrant = (EllipsisPartDrawConfig){TO_TRIG(0),GCornerNone},
.full_quadrants = GCornersAll,
.end_quadrant = (EllipsisPartDrawConfig){TO_TRIG(0),GCornerNone}
}));
// Tests for overflowing angles
cl_assert_equal_edc(prv_calc_draw_config_ellipsis(TO_TRIG(20), TO_TRIG(1000)), ((EllipsisDrawConfig){
.start_quadrant = (EllipsisPartDrawConfig){TO_TRIG(0),GCornerNone},
.full_quadrants = GCornersAll,
.end_quadrant = (EllipsisPartDrawConfig){TO_TRIG(0),GCornerNone}
}));
cl_assert_equal_edc(prv_calc_draw_config_ellipsis(TO_TRIG(360), TO_TRIG(370)), ((EllipsisDrawConfig){
.start_quadrant = (EllipsisPartDrawConfig){TO_TRIG(0),GCornerTopRight},
.full_quadrants = GCornerNone,
.end_quadrant = (EllipsisPartDrawConfig){TO_TRIG(10),GCornerTopRight}
}));
// # Section with quadrant filling
// Tests for full quadrants
cl_assert_equal_edc(prv_calc_draw_config_ellipsis(TO_TRIG(0), TO_TRIG(90)), ((EllipsisDrawConfig){
.start_quadrant = (EllipsisPartDrawConfig){TO_TRIG(0),GCornerTopRight},
.full_quadrants = GCornerTopRight,
.end_quadrant = (EllipsisPartDrawConfig){TO_TRIG(90),GCornerBottomRight}
}));
cl_assert_equal_edc(prv_calc_draw_config_ellipsis(TO_TRIG(90), TO_TRIG(180)), ((EllipsisDrawConfig){
.start_quadrant = (EllipsisPartDrawConfig){TO_TRIG(90),GCornerBottomRight},
.full_quadrants = GCornerBottomRight,
.end_quadrant = (EllipsisPartDrawConfig){TO_TRIG(180),GCornerBottomLeft}
}));
cl_assert_equal_edc(prv_calc_draw_config_ellipsis(TO_TRIG(180), TO_TRIG(270)), ((EllipsisDrawConfig){
.start_quadrant = (EllipsisPartDrawConfig){TO_TRIG(180),GCornerBottomLeft},
.full_quadrants = GCornerBottomLeft,
.end_quadrant = (EllipsisPartDrawConfig){TO_TRIG(270),GCornerTopLeft}
}));
cl_assert_equal_edc(prv_calc_draw_config_ellipsis(TO_TRIG(270), TO_TRIG(360)), ((EllipsisDrawConfig){
.start_quadrant = (EllipsisPartDrawConfig){TO_TRIG(270),GCornerTopLeft},
.full_quadrants = GCornerTopLeft,
.end_quadrant = (EllipsisPartDrawConfig){TO_TRIG(0),GCornerTopRight}
}));
// Tests for pairs of quadrants
cl_assert_equal_edc(prv_calc_draw_config_ellipsis(TO_TRIG(0), TO_TRIG(180)), ((EllipsisDrawConfig){
.start_quadrant = (EllipsisPartDrawConfig){TO_TRIG(0),GCornerTopRight},
.full_quadrants = GCornersRight,
.end_quadrant = (EllipsisPartDrawConfig){TO_TRIG(180),GCornerBottomLeft}
}));
cl_assert_equal_edc(prv_calc_draw_config_ellipsis(TO_TRIG(180), TO_TRIG(360)), ((EllipsisDrawConfig){
.start_quadrant = (EllipsisPartDrawConfig){TO_TRIG(180),GCornerBottomLeft},
.full_quadrants = GCornersLeft,
.end_quadrant = (EllipsisPartDrawConfig){TO_TRIG(0),GCornerTopRight}
}));
cl_assert_equal_edc(prv_calc_draw_config_ellipsis(TO_TRIG(270), TO_TRIG(360 + 90)), ((EllipsisDrawConfig){
.start_quadrant = (EllipsisPartDrawConfig){TO_TRIG(270),GCornerTopLeft},
.full_quadrants = GCornersTop,
.end_quadrant = (EllipsisPartDrawConfig){TO_TRIG(90),GCornerBottomRight}
}));
cl_assert_equal_edc(prv_calc_draw_config_ellipsis(TO_TRIG(90), TO_TRIG(270)), ((EllipsisDrawConfig){
.start_quadrant = (EllipsisPartDrawConfig){TO_TRIG(90),GCornerBottomRight},
.full_quadrants = GCornersBottom,
.end_quadrant = (EllipsisPartDrawConfig){TO_TRIG(270),GCornerTopLeft}
}));
// Tests for triples of quadrants
cl_assert_equal_edc(prv_calc_draw_config_ellipsis(TO_TRIG(0), TO_TRIG(270)), ((EllipsisDrawConfig){
.start_quadrant = (EllipsisPartDrawConfig){TO_TRIG(0),GCornerTopRight},
.full_quadrants = (GCornersBottom | GCornerTopRight),
.end_quadrant = (EllipsisPartDrawConfig){TO_TRIG(270),GCornerTopLeft}
}));
cl_assert_equal_edc(prv_calc_draw_config_ellipsis(TO_TRIG(90), TO_TRIG(360)), ((EllipsisDrawConfig){
.start_quadrant = (EllipsisPartDrawConfig){TO_TRIG(90),GCornerBottomRight},
.full_quadrants = (GCornersBottom | GCornerTopLeft),
.end_quadrant = (EllipsisPartDrawConfig){TO_TRIG(0),GCornerTopRight}
}));
cl_assert_equal_edc(prv_calc_draw_config_ellipsis(TO_TRIG(180), TO_TRIG(360 + 90)), ((EllipsisDrawConfig){
.start_quadrant = (EllipsisPartDrawConfig){TO_TRIG(180),GCornerBottomLeft},
.full_quadrants = (GCornersTop | GCornerBottomLeft),
.end_quadrant = (EllipsisPartDrawConfig){TO_TRIG(90),GCornerBottomRight}
}));
cl_assert_equal_edc(prv_calc_draw_config_ellipsis(TO_TRIG(270), TO_TRIG(360 + 180)), ((EllipsisDrawConfig){
.start_quadrant = (EllipsisPartDrawConfig){TO_TRIG(270),GCornerTopLeft},
.full_quadrants = (GCornersTop | GCornerBottomRight),
.end_quadrant = (EllipsisPartDrawConfig){TO_TRIG(180),GCornerBottomLeft}
}));
// ## Section with regular angles
// Within same quadrant
cl_assert_equal_edc(prv_calc_draw_config_ellipsis(TO_TRIG(30), TO_TRIG(60)), ((EllipsisDrawConfig){
.start_quadrant = (EllipsisPartDrawConfig){TO_TRIG(30),GCornerTopRight},
.full_quadrants = GCornerNone,
.end_quadrant = (EllipsisPartDrawConfig){TO_TRIG(60),GCornerTopRight}
}));
// Starting quadrant filling up and ending quadrant finishing
cl_assert_equal_edc(prv_calc_draw_config_ellipsis(TO_TRIG(45), TO_TRIG(270 + 45)), ((EllipsisDrawConfig){
.start_quadrant = (EllipsisPartDrawConfig){TO_TRIG(45),GCornerTopRight},
.full_quadrants = GCornersBottom,
.end_quadrant = (EllipsisPartDrawConfig){TO_TRIG(270 + 45),GCornerTopLeft}
}));
// Ending quadrant filling up and starting quadrant finishing
cl_assert_equal_edc(prv_calc_draw_config_ellipsis(TO_TRIG(90 + 45), TO_TRIG(360 + 45)), ((EllipsisDrawConfig){
.start_quadrant = (EllipsisPartDrawConfig){TO_TRIG(90 + 45),GCornerBottomRight},
.full_quadrants = GCornersLeft,
.end_quadrant = (EllipsisPartDrawConfig){TO_TRIG(45),GCornerTopRight}
}));
// ## Section with regular angles but over 360°
// Within same quadrant
cl_assert_equal_edc(prv_calc_draw_config_ellipsis(TO_TRIG(360 + 30), TO_TRIG(360 + 60)), ((EllipsisDrawConfig){
.start_quadrant = (EllipsisPartDrawConfig){TO_TRIG(30),GCornerTopRight},
.full_quadrants = GCornerNone,
.end_quadrant = (EllipsisPartDrawConfig){TO_TRIG(60),GCornerTopRight}
}));
// Starting quadrant filling up and ending quadrant finishing
cl_assert_equal_edc(prv_calc_draw_config_ellipsis(TO_TRIG(360 + 45), TO_TRIG(360 + 270 + 45)), ((EllipsisDrawConfig){
.start_quadrant = (EllipsisPartDrawConfig){TO_TRIG(45),GCornerTopRight},
.full_quadrants = GCornersBottom,
.end_quadrant = (EllipsisPartDrawConfig){TO_TRIG(270 + 45),GCornerTopLeft}
}));
// Ending quadrant filling up and starting quadrant finishing
cl_assert_equal_edc(prv_calc_draw_config_ellipsis(TO_TRIG(360 + 90 + 45), TO_TRIG(360 + 360 + 45)), ((EllipsisDrawConfig){
.start_quadrant = (EllipsisPartDrawConfig){TO_TRIG(90 + 45),GCornerBottomRight},
.full_quadrants = GCornersLeft,
.end_quadrant = (EllipsisPartDrawConfig){TO_TRIG(45),GCornerTopRight}
}));
// ## Section with negative angles
// Within same quadrant
cl_assert_equal_edc(prv_calc_draw_config_ellipsis(TO_TRIG(30 - 360), TO_TRIG(60 - 360)), ((EllipsisDrawConfig){
.start_quadrant = (EllipsisPartDrawConfig){TO_TRIG(30)+1,GCornerTopRight},
.full_quadrants = GCornerNone,
.end_quadrant = (EllipsisPartDrawConfig){TO_TRIG(60)+1,GCornerTopRight}
}));
// Starting quadrant filling up and ending quadrant finishing
cl_assert_equal_edc(prv_calc_draw_config_ellipsis(TO_TRIG(45 - 360), TO_TRIG(270 + 45 - 360)), ((EllipsisDrawConfig){
.start_quadrant = (EllipsisPartDrawConfig){TO_TRIG(45),GCornerTopRight},
.full_quadrants = GCornersBottom,
.end_quadrant = (EllipsisPartDrawConfig){TO_TRIG(270 + 45),GCornerTopLeft}
}));
// Ending quadrant filling up and starting quadrant finishing
cl_assert_equal_edc(prv_calc_draw_config_ellipsis(TO_TRIG(90 + 45 - 360), TO_TRIG(360 + 45 - 360)), ((EllipsisDrawConfig){
.start_quadrant = (EllipsisPartDrawConfig){TO_TRIG(90 + 45),GCornerBottomRight},
.full_quadrants = GCornersLeft,
.end_quadrant = (EllipsisPartDrawConfig){TO_TRIG(45),GCornerTopRight}
}));
// Pair of quadrants
cl_assert_equal_edc(prv_calc_draw_config_ellipsis(TO_TRIG(0 - 360), TO_TRIG(180 - 360)), ((EllipsisDrawConfig){
.start_quadrant = (EllipsisPartDrawConfig){TO_TRIG(360),GCornerTopRight},
.full_quadrants = GCornersRight,
.end_quadrant = (EllipsisPartDrawConfig){TO_TRIG(180),GCornerBottomLeft}
}));
// Negative to positive
cl_assert_equal_edc(prv_calc_draw_config_ellipsis(TO_TRIG(-45), TO_TRIG(45)), ((EllipsisDrawConfig){
.start_quadrant = (EllipsisPartDrawConfig){TO_TRIG(270 + 45),GCornerTopLeft},
.full_quadrants = GCornerNone,
.end_quadrant = (EllipsisPartDrawConfig){TO_TRIG(45),GCornerTopRight}
}));
// Flipping quadrant issue
cl_assert_equal_edc(prv_calc_draw_config_ellipsis(TO_TRIG(70), TO_TRIG(90)), ((EllipsisDrawConfig){
.start_quadrant = (EllipsisPartDrawConfig){TO_TRIG(70),GCornerTopRight},
.full_quadrants = GCornerNone,
.end_quadrant = (EllipsisPartDrawConfig){TO_TRIG(90),GCornerBottomRight}
}));
}

Some files were not shown because too many files have changed in this diff Show More