mirror of
https://github.com/google/pebble
synced 2026-10-17 00:34:11 +02:00
Import of the watch repository from Pebble
This commit is contained in:
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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
|
||||
@@ -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" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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(¬ification->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);
|
||||
//}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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 },
|
||||
{ },
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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));
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 = ℑ
|
||||
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;
|
||||
}
|
||||
@@ -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"
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
|
||||
@@ -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")));
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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(¢er, s_stroke_points, s_path_num_points));
|
||||
cl_assert(prv_compare_points(¢er, 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(¢er, 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(¢er, 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(¢er, 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(¢er, 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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(¢er, radius, -(TRIG_MAX_ANGLE / 2));
|
||||
cl_assert_equal_gpoint(result, GPoint(0, 5));
|
||||
|
||||
// -90 degrees should be (-5, 0)
|
||||
result = gpoint_from_polar_internal(¢er, 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(¢er, 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(¢er, 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(¢er, 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(¢er, 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, ¢er, 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, ¢er, &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, ¢er, &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, ¢er, &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, ¢er, &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, ¢er, &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, ¢er, &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, ¢er, &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, ¢er, &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, ¢er, &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, ¢er, &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, ¢er, &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, ¢er, &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, ¢er, &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);
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user