pebble/tests/fw/services/health/test_health.c
2025-01-27 11:38:16 -08:00

1865 lines
72 KiB
C

/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "clar.h"
#include "applib/health_service_private.h"
#include "services/normal/activity/activity.h"
#include "shell/prefs_syscalls.h"
#include "util/size.h"
// Stubs
#include "stubs_app_manager.h"
#include "stubs_logging.h"
#include "stubs_passert.h"
#include "stubs_pbl_malloc.h"
#include "stubs_worker_manager.h"
// Fakes
#include "fake_rtc.h"
#include "fake_pbl_std.h"
static HealthServiceState s_health_service;
// -----------------------------------
// T_STATIC functions from health_service.c
bool prv_calculate_time_range(time_t time_start, time_t time_end,
HealthServiceTimeRange *range);
void prv_adjust_value_boundaries(HealthValue *values, size_t num_values,
const HealthServiceTimeRange *range);
bool prv_activity_session_matches(const ActivitySession *session, HealthActivityMask mask,
time_t time_start, time_t time_end);
int64_t prv_session_compare(const ActivitySession *a, const ActivitySession *b,
HealthIterationDirection direction);
void prv_health_event_handler(PebbleEvent *e, void *context);
// -----------------------------------
// Stubs
AppInstallId app_get_app_id(void) {
return 1;
}
HealthServiceState *app_state_get_health_service_state(void) {
return &s_health_service;
}
PebbleTask pebble_task_get_current(void) {
return PebbleTask_App;
}
HealthServiceState *worker_state_get_health_service_state(void) {
cl_fail("should never be called");
return NULL;
}
void sys_send_pebble_event_to_kernel(PebbleEvent* event) {}
HRMSessionRef sys_hrm_manager_get_app_subscription(AppInstallId app_id) {
return HRM_INVALID_SESSION_REF;
}
static bool s_activity_prefs_heart_rate_enabled;
bool sys_activity_prefs_heart_rate_is_enabled(void) {
return s_activity_prefs_heart_rate_enabled;
}
bool sys_hrm_manager_get_subscription_info(HRMSessionRef session, AppInstallId *app_id,
uint32_t *update_interval_s, uint16_t *expire_s,
HRMFeature *features) {
return false;
}
typedef struct {
struct {
ActivityMetric metric;
uint32_t history_len;
} in;
struct {
HealthValue history[ACTIVITY_HISTORY_DAYS];
bool result;
} out;
} sys_activity_get_metric_values;
// Activity Metric Overrides
// Allows one to specify a return value for a specific metric
static struct {
bool overridden;
int32_t value;
} s_metric_overrides[ActivityMetricNumMetrics];
static bool prv_handle_override(ActivityMetric metric, uint32_t history_len, int32_t *history) {
if (!s_metric_overrides[metric].overridden) {
return false;
}
cl_assert_equal_i(1, history_len);
*history = s_metric_overrides[metric].value;
return true;
}
static void prv_override_metric(ActivityMetric metric, int32_t value) {
s_metric_overrides[metric].value = value;
s_metric_overrides[metric].overridden = true;
}
// End override code
static sys_activity_get_metric_values s_sys_activity_get_metric_values;
// mock that simply copies values from static vars and stores args for later inspection
bool sys_activity_get_metric(ActivityMetric metric, uint32_t history_len, int32_t *history) {
cl_assert(history_len <= ARRAY_LENGTH(s_sys_activity_get_metric_values.out.history));
s_sys_activity_get_metric_values.in.metric = metric;
s_sys_activity_get_metric_values.in.history_len = history_len;
// Check if this value is in our overrides. If it is return it and quit.
if (prv_handle_override(metric, history_len, history)) {
return true;
}
// yes, actual implementation can handle this
if (history) {
for (uint32_t i = 0; i < history_len; i++) {
history[i] = s_sys_activity_get_metric_values.out.history[i];
}
}
return s_sys_activity_get_metric_values.out.result;
}
void event_service_client_subscribe(EventServiceInfo * service_info) {}
void event_service_client_unsubscribe(EventServiceInfo * service_info) {}
static UnitsDistance s_units_distance_result;
UnitsDistance sys_shell_prefs_get_units_distance(void) {
return s_units_distance_result;
}
typedef struct {
struct {
ActivitySession sessions[30];
uint32_t num_sessions;
bool result;
} out;
} sys_activity_get_sessions_values;
static sys_activity_get_sessions_values s_sys_activity_get_sessions_values;
bool sys_activity_get_sessions(uint32_t *num_sessions, ActivitySession *sessions) {
cl_assert(num_sessions);
cl_assert(sessions);
cl_assert(s_sys_activity_get_sessions_values.out.num_sessions <=
ARRAY_LENGTH(s_sys_activity_get_sessions_values.out.sessions));
for (uint32_t i = 0;
i < MIN(*num_sessions,
s_sys_activity_get_sessions_values.out.num_sessions);
i++) {
sessions[i] = s_sys_activity_get_sessions_values.out.sessions[i];
}
*num_sessions = s_sys_activity_get_sessions_values.out.num_sessions;
return s_sys_activity_get_sessions_values.out.result;
}
typedef struct {
struct {
uint16_t day_of_week;
} in;
struct {
ActivityMetricAverages averages;
bool result;
} out;
} sys_activity_get_step_averages_values;
static sys_activity_get_step_averages_values s_sys_activity_get_step_averages_values_weekday;
static sys_activity_get_step_averages_values s_sys_activity_get_step_averages_values_weekend;
bool sys_activity_get_step_averages(uint16_t day_of_week, ActivityMetricAverages *averages) {
cl_assert(averages);
if (day_of_week == Sunday || day_of_week == Saturday) {
s_sys_activity_get_step_averages_values_weekend.in.day_of_week = day_of_week;
memcpy(averages, &s_sys_activity_get_step_averages_values_weekend.out.averages,
sizeof(*averages));
return s_sys_activity_get_step_averages_values_weekend.out.result;
} else {
s_sys_activity_get_step_averages_values_weekday.in.day_of_week = day_of_week;
memcpy(averages, &s_sys_activity_get_step_averages_values_weekday.out.averages,
sizeof(*averages));
return s_sys_activity_get_step_averages_values_weekday.out.result;
}
}
typedef struct {
HealthActivity activity;
time_t time_start;
time_t time_end;
void *context;
} HealthActivityCBData;
static HealthActivityCBData s_prv_activity_cb__args[100];
static uint32_t s_prv_activity_cb__call_count;
static uint32_t s_prv_activity_cb__false_at_call_no = UINT32_MAX;
bool prv_activity_cb(HealthActivity activity,
time_t time_start, time_t time_end,
void *context) {
s_prv_activity_cb__args[s_prv_activity_cb__call_count] = (HealthActivityCBData) {
.activity = activity, .time_start = time_start, .time_end = time_end, .context = context,
};
s_prv_activity_cb__call_count++;
cl_assert(s_prv_activity_cb__call_count <= s_prv_activity_cb__false_at_call_no);
return s_prv_activity_cb__call_count < s_prv_activity_cb__false_at_call_no;
}
typedef struct {
HealthMinuteData records[MINUTES_PER_DAY];
uint32_t num_records;
time_t utc_start;
bool result;
bool asserts;
} sys_activity_get_minute_history_out_values;
typedef struct {
uint32_t num_records;
time_t utc_start;
} sys_activity_get_minute_history_in_values;
typedef struct {
// Each time sys_activity_get_minute_history is called, we return the next stage of data
int stage;
sys_activity_get_minute_history_in_values in[4];
sys_activity_get_minute_history_out_values out[4];
} sys_activity_get_minute_history_values;
static sys_activity_get_minute_history_values s_sys_activity_get_minute_history_values;
bool sys_activity_get_minute_history(HealthMinuteData *minute_data, uint32_t *num_records,
time_t *utc_start) {
int stage = s_sys_activity_get_minute_history_values.stage++;
cl_assert(stage < ARRAY_LENGTH(s_sys_activity_get_minute_history_values.out));
sys_activity_get_minute_history_out_values *out =
&s_sys_activity_get_minute_history_values.out[stage];
cl_assert_equal_b(out->asserts, false);
cl_assert(minute_data);
cl_assert(num_records);
cl_assert(utc_start);
s_sys_activity_get_minute_history_values.in[stage].num_records = *num_records;
s_sys_activity_get_minute_history_values.in[stage].utc_start = *utc_start;
if (!out->result) {
return false;
}
*num_records = MIN(out->num_records, *num_records);
*utc_start = out->utc_start;
for (uint32_t i = 0; i < *num_records; i++) {
minute_data[i] = out->records[i];
}
return true;
}
static bool s_activity_sessions_ongoing[ActivitySessionTypeCount];
bool sys_activity_sessions_is_session_type_ongoing(ActivitySessionType type) {
return s_activity_sessions_ongoing[type];
}
/////////////////////////////////////////
void test_health__initialize(void) {
TimezoneInfo tz_info = {
.tm_zone = "UTC",
.tm_gmtoff = 0,
};
time_util_update_timezone(&tz_info);
s_health_service = (HealthServiceState){};
time_t utc_sec = 1451293942; // some constant time for this test
// Mon, 28 Dec 2015 09:12:22 GMT
// => 22+12*60+9*60*60 = 33142 seconds into this day
// => 24*60*60-33142 = 53258 seconds remaining this day
fake_rtc_init(100 /*initial_ticks*/, utc_sec);
s_sys_activity_get_metric_values = (sys_activity_get_metric_values) {
.out.result = true,
.in.metric = (ActivityMetric)-1,
};
memset(s_prv_activity_cb__args, 0, sizeof(s_prv_activity_cb__args));
memset(s_metric_overrides, 0, sizeof(s_metric_overrides));
s_prv_activity_cb__call_count = 0;
s_prv_activity_cb__false_at_call_no = UINT32_MAX;
s_sys_activity_get_minute_history_values = (sys_activity_get_minute_history_values) {
// as all these values need to be configured in the test, we assert per default
.out[0].asserts = true,
};
s_activity_prefs_heart_rate_enabled = true;
}
void test_health__sum_today_returns_0_on_failure(void) {
s_sys_activity_get_metric_values.out.result = false;
s_sys_activity_get_metric_values.out.history[0] = 456;
HealthValue result = health_service_sum_today(HealthMetricStepCount);
cl_assert_equal_i(0, result);
}
void test_health__sum_today(void) {
s_sys_activity_get_metric_values.out.history[0] = 123;
s_sys_activity_get_metric_values.out.history[1] = 456;
HealthValue result = health_service_sum_today(HealthMetricStepCount);
cl_assert_equal_i(123, result);
cl_assert_equal_i(s_sys_activity_get_metric_values.in.metric, ActivityMetricStepCount);
cl_assert_equal_i(s_sys_activity_get_metric_values.in.history_len, ACTIVITY_HISTORY_DAYS);
}
#define cl_assert_equal_range(a, b) \
do { \
HealthServiceTimeRange r_a = (a); \
HealthServiceTimeRange r_b = (b); \
bool success = memcmp(&r_a, &r_b, sizeof(r_a)) == 0; \
if (!success) { \
char error_msg[256] = {0}; \
snprintf(error_msg, sizeof(error_msg), \
"HealthServiceInternalTimeRange equal\n" \
" a: {last_day_idx:%d, num_days:%d, seconds_first_day:%d, seconds_last_day:%d, " \
"seconds_total_last_day: %d}\n" \
" b: {last_day_idx:%d, num_days:%d, seconds_first_day:%d, seconds_last_day:%d," \
"seconds_total_last_day: %d}\n", \
(int)r_a.last_day_idx, (int)r_a.num_days, \
(int)r_a.seconds_first_day, (int)r_a.seconds_last_day, \
(int)r_a.seconds_total_last_day, \
(int)r_b.last_day_idx, (int)r_b.num_days, \
(int)r_b.seconds_first_day, (int)r_b.seconds_last_day, \
(int)r_b.seconds_total_last_day); \
clar__assert(0, __FILE__, __LINE__, "Expression is not true: ", error_msg, 1); \
} \
} while(0);
void test_health__range_to_day_id(void) {
const time_t now = rtc_get_time();
bool result;
HealthServiceTimeRange range;
// today
result = prv_calculate_time_range(time_util_get_midnight_of(now), now, &range);
cl_assert(result);
cl_assert_equal_range(range, ((HealthServiceTimeRange){
.last_day_idx = 0,
.num_days = 1,
.seconds_first_day = 33142,
.seconds_last_day = 33142,
.seconds_total_last_day = 33142,
}));
// yesterday
result = prv_calculate_time_range(
time_util_get_midnight_of(now - SECONDS_PER_DAY), time_util_get_midnight_of(now), &range);
cl_assert(result);
cl_assert_equal_range(range, ((HealthServiceTimeRange){
.last_day_idx = 1,
.num_days = 1,
.seconds_first_day = 86400,
.seconds_last_day = 86400,
.seconds_total_last_day = 86400,
}));
// some time yesterday + today
result = prv_calculate_time_range(now - SECONDS_PER_DAY, now, &range);
cl_assert(result);
cl_assert_equal_range(range, ((HealthServiceTimeRange){
.last_day_idx = 0,
.num_days = 2,
.seconds_first_day = 53258,
.seconds_last_day = 33142,
.seconds_total_last_day = 33142,
}));
}
void test_health__range_to_day_id_respects_local_time(void) {
const time_t now = rtc_get_time();
bool result;
HealthServiceTimeRange range;
// some time yesterday + today - as if UTC == localtime
result = prv_calculate_time_range(now - SECONDS_PER_DAY, now, &range);
cl_assert(result);
cl_assert_equal_range(range, ((HealthServiceTimeRange){
.last_day_idx = 0,
.num_days = 2,
.seconds_first_day = 53258,
.seconds_last_day = 33142,
.seconds_total_last_day = 33142,
}));
// shifted one hour
time_t utc_to_local_delta = SECONDS_PER_HOUR;
TimezoneInfo tz_info = {
.tm_zone = "FOO",
.tm_gmtoff = utc_to_local_delta,
};
time_util_update_timezone(&tz_info);
result = prv_calculate_time_range(now - SECONDS_PER_DAY, now, &range);
cl_assert(result);
cl_assert_equal_range(range, ((HealthServiceTimeRange){
.last_day_idx = 0,
.num_days = 2,
.seconds_first_day = 53258 - utc_to_local_delta,
.seconds_last_day = 33142 + utc_to_local_delta,
.seconds_total_last_day = 33142 + utc_to_local_delta,
}));
}
void test_health__range_to_day_id_rejects_invalid_values(void) {
const time_t now = rtc_get_time();
bool result;
// check that we *can* return success
result = prv_calculate_time_range(now - 10, now, NULL);
cl_assert_equal_b(result, true);
// in the future
result = prv_calculate_time_range(now + 10, now + 20, NULL);
cl_assert_equal_b(result, false);
// too far in the past
result = prv_calculate_time_range(
now - (ACTIVITY_HISTORY_DAYS + 10) * SECONDS_PER_DAY,
now - (ACTIVITY_HISTORY_DAYS + 2) * SECONDS_PER_DAY, NULL);
cl_assert_equal_b(result, false);
// start after end
result = prv_calculate_time_range(now - 100, now - 200, NULL);
cl_assert_equal_b(result, false);
}
void test_health__range_to_day_id_clamps_values(void) {
const time_t now = rtc_get_time();
bool result;
HealthServiceTimeRange range;
// clamps value that goes into the future
result = prv_calculate_time_range(now - 10, now + 11, &range);
cl_assert_equal_b(result, true);
cl_assert_equal_range(range, ((HealthServiceTimeRange){
.last_day_idx = 0,
.num_days = 1,
.seconds_first_day = 10,
.seconds_last_day = 10,
.seconds_total_last_day = 33142,
}));
// clamps value that goes into the future
const time_t first_valid_time =
time_util_get_midnight_of(now - (ACTIVITY_HISTORY_DAYS - 1) * SECONDS_PER_DAY);
result = prv_calculate_time_range(first_valid_time - 12, first_valid_time + 13,
&range);
cl_assert_equal_b(result, true);
cl_assert_equal_range(range, ((HealthServiceTimeRange){
.last_day_idx = ACTIVITY_HISTORY_DAYS - 1,
.num_days = 1,
.seconds_first_day = 13,
.seconds_last_day = 13,
.seconds_total_last_day = 86400,
}));
}
void test_health__sum_full_days(void) {
// use values structured as binary mask so we can detect if we sum up currect days
s_sys_activity_get_metric_values.out.history[0] = 1000;
s_sys_activity_get_metric_values.out.history[1] = 2000;
s_sys_activity_get_metric_values.out.history[2] = 4000;
s_sys_activity_get_metric_values.out.history[3] = 8000;
s_sys_activity_get_metric_values.out.history[4] = 16000;
const time_t now = rtc_get_time();
HealthValue result;
// today until now
result = health_service_sum(HealthMetricStepCount,
time_util_get_midnight_of(now),
now);
cl_assert_equal_i(result, 1000);
cl_assert_equal_i(s_sys_activity_get_metric_values.in.history_len, ACTIVITY_HISTORY_DAYS);
// today into future
result = health_service_sum(HealthMetricStepCount,
time_util_get_midnight_of(now),
now + 12345);
cl_assert_equal_i(result, 1000);
// yesterday
result = health_service_sum(HealthMetricStepCount,
time_util_get_midnight_of(now) - SECONDS_PER_DAY,
time_util_get_midnight_of(now));
cl_assert_equal_i(result, 2000);
// yesterday and today
result = health_service_sum(HealthMetricStepCount,
time_util_get_midnight_of(now) - SECONDS_PER_DAY,
now);
cl_assert_equal_i(result, 1000 + 2000);
}
void test_health__process_range(void) {
HealthValue values[4] = {1000, 1000, 1000, 1000};
HealthServiceTimeRange range = {
.num_days = 3,
.seconds_first_day = SECONDS_PER_DAY / 10,
.seconds_last_day = SECONDS_PER_DAY / 5,
.seconds_total_last_day = SECONDS_PER_DAY,
};
// make sure we treat first and last day correctly (last == idx 0)
prv_adjust_value_boundaries(values, ARRAY_LENGTH(values), &range);
cl_assert_equal_i(values[0], 1000 / 5);
cl_assert_equal_i(values[1], 1000);
cl_assert_equal_i(values[2], 1000 / 10);
// ensure we look at seconds_total_last_day
values[0] = 1000;
values[2] = 1000;
range.seconds_total_last_day = SECONDS_PER_DAY / 4;
prv_adjust_value_boundaries(values, ARRAY_LENGTH(values), &range);
cl_assert_equal_i(values[0], 4 * 1000 / 5);
cl_assert_equal_i(values[1], 1000);
cl_assert_equal_i(values[2], 1000 / 10);
// ensure we don't calculate a single day multiple times
values[0] = 1000;
values[2] = 1000;
range.num_days = 1;
prv_adjust_value_boundaries(values, ARRAY_LENGTH(values), &range);
cl_assert_equal_i(values[0], 4 * 1000 / 5);
cl_assert_equal_i(values[1], 1000);
cl_assert_equal_i(values[2], 1000);
// ensure we can handle smaller array than range - nothing will be processed
values[0] = 1000;
range.num_days = 2;
prv_adjust_value_boundaries(values, 1, &range);
cl_assert_equal_i(values[0], 1000);
cl_assert_equal_i(values[1], 1000);
cl_assert_equal_i(values[2], 1000);
// ensure we can handle empty sets
values[0] = 1000;
prv_adjust_value_boundaries(values, 0, &range);
cl_assert_equal_i(values[0], 1000);
cl_assert_equal_i(values[1], 1000);
cl_assert_equal_i(values[2], 1000);
// ensure we can handle empty ranges
range.num_days = 0;
prv_adjust_value_boundaries(values, ARRAY_LENGTH(values), &range);
cl_assert_equal_i(values[0], 1000);
cl_assert_equal_i(values[1], 1000);
cl_assert_equal_i(values[2], 1000);
// ensure we correctly handle the day index
range = (HealthServiceTimeRange){
.num_days = 3,
.last_day_idx = 1,
.seconds_first_day = SECONDS_PER_DAY / 10,
.seconds_last_day = SECONDS_PER_DAY / 5,
.seconds_total_last_day = SECONDS_PER_DAY,
};
prv_adjust_value_boundaries(values, ARRAY_LENGTH(values), &range);
cl_assert_equal_i(values[0], 1000);
cl_assert_equal_i(values[1], 1000 / 5);
cl_assert_equal_i(values[2], 1000);
cl_assert_equal_i(values[3], 1000 / 10);
}
void test_health__sum_fraction_days(void) {
// use values structured as binary mask so we can detect if we sum up currect days
s_sys_activity_get_metric_values.out.history[0] = 1000;
s_sys_activity_get_metric_values.out.history[1] = 2000;
s_sys_activity_get_metric_values.out.history[2] = 4000;
s_sys_activity_get_metric_values.out.history[3] = 8000;
s_sys_activity_get_metric_values.out.history[4] = 16000;
const time_t now = rtc_get_time();
HealthValue result;
// 3/4 of yesterday
result = health_service_sum(HealthMetricStepCount,
time_util_get_midnight_of(now) - SECONDS_PER_DAY,
time_util_get_midnight_of(now) - SECONDS_PER_DAY / 4);
cl_assert_equal_i(result, 1500);
cl_assert_equal_i(s_sys_activity_get_metric_values.in.history_len, ACTIVITY_HISTORY_DAYS);
// 1/2 of today's captured seconds so far
result = health_service_sum(HealthMetricStepCount,
time_util_get_midnight_of(now),
(time_util_get_midnight_of(now) + now) / 2);
cl_assert_equal_i(result, 500);
}
void test_health__cache(void) {
cl_assert_equal_p(s_health_service.cache, NULL);
health_service_events_subscribe(NULL, NULL);
HealthServiceCache *const cache = s_health_service.cache;
cl_assert(cache != NULL);
// cache is preserved
health_service_events_subscribe(NULL, NULL);
cl_assert_equal_p(s_health_service.cache, cache);
health_service_events_unsubscribe();
cl_assert_equal_p(s_health_service.cache, NULL);
// multiple unsubscribe/empty cache doesn't cause a problem
health_service_events_unsubscribe();
cl_assert_equal_p(s_health_service.cache, NULL);
}
void test_health__metric_accessible(void) {
const time_t now = rtc_get_time();
HealthServiceAccessibilityMask accessible;
// all value in the future
accessible = health_service_metric_accessible(HealthMetricStepCount, now + 10, now + 20);
cl_assert_equal_i(accessible, HealthServiceAccessibilityMaskNotAvailable);
// normal value is available
accessible = health_service_metric_accessible(HealthMetricStepCount, now - 10, now);
cl_assert_equal_i(accessible, HealthServiceAccessibilityMaskAvailable);
// values that partly are in unsupported range are available
accessible = health_service_metric_accessible(HealthMetricStepCount, now - 10, now + 20);
cl_assert_equal_i(accessible, HealthServiceAccessibilityMaskAvailable);
// if all values are -1, data is not available
s_sys_activity_get_metric_values.out.history[0] = -1;
s_sys_activity_get_metric_values.out.history[1] = -1;
accessible = health_service_metric_accessible(HealthMetricStepCount,
now - SECONDS_PER_DAY, now);
cl_assert_equal_i(accessible, HealthServiceAccessibilityMaskNotAvailable);
// if some values are >= 0, data is not available (day at idx 2)
accessible = health_service_metric_accessible(HealthMetricStepCount,
now - 2 * SECONDS_PER_DAY, now);
cl_assert_equal_i(accessible, HealthServiceAccessibilityMaskAvailable);
}
void test_health__metric_hr_accessible(void) {
const time_t now = rtc_get_time();
HealthServiceAccessibilityMask accessible;
// all value in the future
accessible = health_service_metric_accessible(HealthMetricHeartRateBPM, now + 10, now + 20);
cl_assert_equal_i(accessible, HealthServiceAccessibilityMaskNotAvailable);
// normal value is available
accessible = health_service_metric_accessible(HealthMetricHeartRateBPM, now - 10, now);
cl_assert_equal_i(accessible, HealthServiceAccessibilityMaskAvailable);
// values that partly are in unsupported range are available
accessible = health_service_metric_accessible(HealthMetricHeartRateBPM, now - 10, now + 20);
cl_assert_equal_i(accessible, HealthServiceAccessibilityMaskAvailable);
// HR has a limit of two hours. Make sure if we are within that range it's available
accessible = health_service_metric_accessible(HealthMetricHeartRateBPM,
now - 2 * SECONDS_PER_HOUR, now);
cl_assert_equal_i(accessible, HealthServiceAccessibilityMaskAvailable);
}
void test_health__metric_hr_averaged_accessible(void) {
const time_t now = rtc_get_time();
typedef struct {
const char *desc;
struct {
HealthMetric metric;
time_t time_start;
time_t time_end;
HealthServiceTimeScope scope;
bool hr_disabled;
} in;
struct {
HealthServiceAccessibilityMask accessible;
} out;
} TestInputOutput;
const TestInputOutput tests[] = {
{
.desc = "Valid time range with ScopeOnce",
.in = { HealthMetricHeartRateBPM, now - 10, now, HealthServiceTimeScopeOnce },
.out = { HealthServiceAccessibilityMaskAvailable }
},
{
.desc = "Valid time range with ScopeWeekly",
.in = { HealthMetricHeartRateBPM, now - 10, now, HealthServiceTimeScopeWeekly },
.out = { HealthServiceAccessibilityMaskNotSupported }
},
{
.desc = "Valid time range with ScopeDailyWeekdayOrWeekend",
.in = { HealthMetricHeartRateBPM, now - 10, now, HealthServiceTimeScopeDailyWeekdayOrWeekend },
.out = { HealthServiceAccessibilityMaskNotSupported }
},
{
.desc = "Valid time range with ScopeDaily",
.in = { HealthMetricHeartRateBPM, now - 10, now, HealthServiceTimeScopeDaily },
.out = { HealthServiceAccessibilityMaskNotSupported }
},
{
.desc = "Invalid future time range with ScopeOnce",
.in = { HealthMetricHeartRateBPM, now + 10, now + 20, HealthServiceTimeScopeOnce },
.out = { HealthServiceAccessibilityMaskNotAvailable }
},
{
.desc = "Time range that goes further back into history than BPM supports",
.in = { HealthMetricHeartRateBPM, now - 3 * SECONDS_PER_HOUR, now, HealthServiceTimeScopeOnce },
.out = { HealthServiceAccessibilityMaskNotSupported }
},
{
.desc = "Time range that goes further back into history than BPM supports",
.in = { HealthMetricHeartRateBPM, now - 3 * SECONDS_PER_HOUR, now - 1 * SECONDS_PER_HOUR, HealthServiceTimeScopeOnce },
.out = { HealthServiceAccessibilityMaskNotSupported }
},
{
.desc = "HR Disabled. Return NoPermission",
.in = { HealthMetricHeartRateBPM, now - 10, now, HealthServiceTimeScopeOnce, true},
.out = { HealthServiceAccessibilityMaskNoPermission }
},
};
// Run all the tests
HealthServiceAccessibilityMask accessible;
for (int i = 0; i < ARRAY_LENGTH(tests); i++) {
const TestInputOutput *test = &tests[i];
s_activity_prefs_heart_rate_enabled = !test->in.hr_disabled;
accessible = health_service_metric_averaged_accessible(test->in.metric,
test->in.time_start,
test->in.time_end,
test->in.scope);
PBL_LOG(LOG_LEVEL_DEBUG, "%s\nMetric: %d, start: %d, end: %d, Scope: %d",
test->desc, (int)test->in.metric, (int)test->in.time_start,
(int)test->in.time_end, (int)test->in.scope);
cl_assert_equal_i(accessible, tests[i].out.accessible);
}
}
void test_health__metric_hr_aggregate_averaged_accessible(void) {
const time_t now = rtc_get_time();
typedef struct {
const char *desc;
struct {
HealthMetric metric;
time_t time_start;
time_t time_end;
HealthAggregation aggregation;
HealthServiceTimeScope scope;
bool hr_disabled;
} in;
struct {
HealthServiceAccessibilityMask accessible;
} out;
} TestInputOutput;
const TestInputOutput tests[] = {
{
.desc = "Valid time range with ScopeDaily and Sum. Should be NotSupported",
.in = { HealthMetricHeartRateBPM, now - 10, now, HealthAggregationSum,
HealthServiceTimeScopeDaily },
.out = { HealthServiceAccessibilityMaskNotSupported }
},
{
.desc = "Valid time range with ScopeDaily and Avg. Not available because Daily",
.in = { HealthMetricHeartRateBPM, now - 10, now, HealthAggregationAvg,
HealthServiceTimeScopeDaily },
.out = { HealthServiceAccessibilityMaskNotSupported }
},
{
.desc = "Valid time range with ScopeOnce and Min. Available",
.in = { HealthMetricHeartRateBPM, now - 10, now, HealthAggregationMin,
HealthServiceTimeScopeOnce },
.out = { HealthServiceAccessibilityMaskAvailable }
},
{
.desc = "Valid time range with ScopeOnce and Max. Available",
.in = { HealthMetricHeartRateBPM, now - 10, now, HealthAggregationMax,
HealthServiceTimeScopeOnce },
.out = { HealthServiceAccessibilityMaskAvailable }
},
{
.desc = "Valid time range with ScopeDaily and Max. NotSupported",
.in = { HealthMetricHeartRateBPM, now - 10, now, HealthAggregationMax,
HealthServiceTimeScopeDaily },
.out = { HealthServiceAccessibilityMaskNotSupported }
},
{
.desc = "Invalid time range with ScopeOnce and Max. NotSupported",
.in = { HealthMetricHeartRateBPM, now - 3 * SECONDS_PER_HOUR, now - 2 * SECONDS_PER_HOUR,
HealthAggregationMax, HealthServiceTimeScopeOnce },
.out = { HealthServiceAccessibilityMaskNotSupported }
},
{
.desc = "HR Disabled. Return NoPermission",
.in = { HealthMetricHeartRateBPM, now - 10, now, HealthAggregationMax,
HealthServiceTimeScopeOnce, true },
.out = { HealthServiceAccessibilityMaskNoPermission }
},
{
.desc = "Time range that goes further back into history than BPM supports",
.in = { HealthMetricHeartRateBPM, now - 3 * SECONDS_PER_HOUR, now - 1 * SECONDS_PER_HOUR,
HealthAggregationAvg, HealthServiceTimeScopeOnce },
.out = { HealthServiceAccessibilityMaskNotSupported }
},
};
// Run all the tests
HealthServiceAccessibilityMask accessible;
for (int i = 0; i < ARRAY_LENGTH(tests); i++) {
const TestInputOutput *test = &tests[i];
s_activity_prefs_heart_rate_enabled = !test->in.hr_disabled;
accessible = health_service_metric_aggregate_averaged_accessible(test->in.metric,
test->in.time_start,
test->in.time_end,
test->in.aggregation,
test->in.scope);
PBL_LOG(LOG_LEVEL_DEBUG, "%s\nMetric: %d, start: %d, end: %d, Aggregation: %d, Scope: %d",
test->desc, (int)test->in.metric, (int)test->in.time_start,
(int)test->in.time_end, (int)test->in.aggregation, (int)test->in.scope);
cl_assert_equal_i(accessible, tests[i].out.accessible);
}
}
void test_health__sleep_session_matches(void) {
const time_t now = rtc_get_time();
ActivitySession session = {
.type = ActivitySessionType_Sleep,
.start_utc = now - (10 * SECONDS_PER_MINUTE),
.length_min = 10,
};
bool (*fun)(const ActivitySession *, HealthActivityMask, time_t, time_t) =
prv_activity_session_matches;
// mask none matches nothing
cl_assert_equal_b(false, fun(&session, HealthActivityNone, now - (10 * SECONDS_PER_MINUTE),
now));
// mask restful doesn't match
cl_assert_equal_b(false, fun(&session, HealthActivityRestfulSleep,
now - (10 * SECONDS_PER_MINUTE), now));
// exact time range matches
cl_assert_equal_b(true, fun(&session, HealthActivityMaskAll, now - (10 * SECONDS_PER_MINUTE),
now));
// too large time range matches
cl_assert_equal_b(true, fun(&session, HealthActivityMaskAll, now - (20 * SECONDS_PER_MINUTE),
now + (10 * SECONDS_PER_MINUTE)));
// range before doesn't match, even if it touches
cl_assert_equal_b(false, fun(&session, HealthActivityMaskAll, now - (20 * SECONDS_PER_MINUTE),
now - (10 * SECONDS_PER_MINUTE)));
// range after doesn't match, even if it touches
cl_assert_equal_b(false, fun(&session, HealthActivityMaskAll, now,
now + (10 * SECONDS_PER_MINUTE)));
// range that starts before matches
cl_assert_equal_b(true, fun(&session, HealthActivityMaskAll, now - (20 * SECONDS_PER_MINUTE),
now - (9 * SECONDS_PER_MINUTE)));
// range that ends after matches
cl_assert_equal_b(true, fun(&session, HealthActivityMaskAll, now - (1 * SECONDS_PER_MINUTE),
now + (10 * SECONDS_PER_MINUTE)));
}
void test_health__any_activity_accessible(void) {
const time_t now = rtc_get_time();
HealthServiceAccessibilityMask accessible;
// empty mask => not available
accessible = health_service_any_activity_accessible(HealthActivityNone,
now - (10 * SECONDS_PER_MINUTE), now);
cl_assert_equal_i(accessible, HealthServiceAccessibilityMaskNotAvailable);
accessible = health_service_any_activity_accessible(HealthActivityMaskAll,
now - (10 * SECONDS_PER_MINUTE), now);
cl_assert_equal_i(accessible, HealthServiceAccessibilityMaskAvailable);
// too far in the past
accessible = health_service_any_activity_accessible(HealthActivityMaskAll,
now - 10 * SECONDS_PER_DAY,
now - 9 * SECONDS_PER_DAY);
cl_assert_equal_i(accessible, HealthServiceAccessibilityMaskNotAvailable);
// range far into to past and future
accessible = health_service_any_activity_accessible(HealthActivityMaskAll,
now - 10 * SECONDS_PER_DAY,
now + 10 * SECONDS_PER_DAY);
cl_assert_equal_i(accessible, HealthServiceAccessibilityMaskAvailable);
}
void test_health__activities_iterate(void) {
const time_t now = rtc_get_time();
// start from oldest to most-recent - this is more or less an arbitrary order
s_sys_activity_get_sessions_values.out.sessions[6] = (ActivitySession){
.type = ActivitySessionType_Open,
.start_utc = now - (95 * SECONDS_PER_MINUTE),
.length_min = 15, // end = -80
};
s_sys_activity_get_sessions_values.out.sessions[5] = (ActivitySession){
.type = ActivitySessionType_Run,
.start_utc = now - (80 * SECONDS_PER_MINUTE),
.length_min = 15, // end = -65
};
s_sys_activity_get_sessions_values.out.sessions[4] = (ActivitySession){
.type = ActivitySessionType_Walk,
.start_utc = now - (65 * SECONDS_PER_MINUTE),
.length_min = 15, // end = -50
};
s_sys_activity_get_sessions_values.out.sessions[3] = (ActivitySession){
.type = ActivitySessionType_Sleep,
.start_utc = now - (50 * SECONDS_PER_MINUTE),
.length_min = 20, // end = -30
};
s_sys_activity_get_sessions_values.out.sessions[2] = (ActivitySession){
.type = ActivitySessionType_RestfulSleep,
.start_utc = now - (45 * SECONDS_PER_MINUTE),
.length_min = 10, // end = -35
};
s_sys_activity_get_sessions_values.out.sessions[1] = (ActivitySession){
.type = ActivitySessionType_Sleep,
.start_utc = now - (20 * SECONDS_PER_MINUTE),
.length_min = 10, // end = -10
};
s_sys_activity_get_sessions_values.out.sessions[0] = (ActivitySession){
.type = ActivitySessionType_RestfulSleep,
.start_utc = now - (18 * SECONDS_PER_MINUTE),
.length_min = 5, // end = -13
};
// oldest to most-recent (looking at each session's start): 3, 2, 1, 0
// most-recent to oldest (looking at each session's end): 1, 0, 3, 2
const int num_sleep_sessions = 4;
const int num_restfulsleep_sessions = 2;
const int num_run_sessions = 1;
const int num_walk_sessions = 1;
const int num_open_sessions = 1;
const int num_sessions = num_sleep_sessions + num_restfulsleep_sessions +
num_run_sessions + num_walk_sessions + num_open_sessions;
// result from mocked sys_activity_get_sessions_values is still false
health_service_activities_iterate(HealthActivityMaskAll, now - (100 * SECONDS_PER_MINUTE), now,
HealthIterationDirectionPast, prv_activity_cb, NULL);
cl_assert_equal_i(0, s_prv_activity_cb__call_count);
s_sys_activity_get_sessions_values.out.result = true;
// result from mocked sys_activity_get_sessions_values is still 0 sessions
health_service_activities_iterate(HealthActivityMaskAll, now - (100 * SECONDS_PER_MINUTE), now,
HealthIterationDirectionPast, prv_activity_cb, NULL);
cl_assert_equal_i(0, s_prv_activity_cb__call_count);
// respect mask for RestfulSleep
s_prv_activity_cb__call_count = 0;
s_sys_activity_get_sessions_values.out.num_sessions = 7;
health_service_activities_iterate(HealthActivityRestfulSleep, now - (100 * SECONDS_PER_MINUTE),
now, HealthIterationDirectionPast, prv_activity_cb, NULL);
cl_assert_equal_i(num_restfulsleep_sessions, s_prv_activity_cb__call_count);
cl_assert_equal_b(s_prv_activity_cb__args[0].activity, HealthActivityRestfulSleep);
// respect mask for Run/Walk
s_prv_activity_cb__call_count = 0;
s_sys_activity_get_sessions_values.out.num_sessions = 7;
health_service_activities_iterate(HealthActivityRun | HealthActivityWalk |
HealthActivityOpenWorkout,
now - (100 * SECONDS_PER_MINUTE),
now, HealthIterationDirectionPast, prv_activity_cb, NULL);
cl_assert_equal_i(num_run_sessions + num_walk_sessions + num_open_sessions,
s_prv_activity_cb__call_count);
cl_assert_equal_b(s_prv_activity_cb__args[0].activity, HealthActivityRun);
// respect range
s_prv_activity_cb__call_count = 0;
s_sys_activity_get_sessions_values.out.num_sessions = 7;
health_service_activities_iterate(HealthActivitySleep, now - (15 * SECONDS_PER_MINUTE), now,
HealthIterationDirectionPast, prv_activity_cb, NULL);
cl_assert_equal_i(1, s_prv_activity_cb__call_count);
cl_assert_equal_b(s_prv_activity_cb__args[0].activity, HealthActivitySleep);
// order direction past
s_prv_activity_cb__call_count = 0;
health_service_activities_iterate(HealthActivityMaskAll, now - (200 * SECONDS_PER_MINUTE), now,
HealthIterationDirectionPast, prv_activity_cb, NULL);
cl_assert_equal_i(7, s_prv_activity_cb__call_count);
cl_assert_equal_i(s_prv_activity_cb__args[0].time_start,
s_sys_activity_get_sessions_values.out.sessions[1].start_utc);
cl_assert_equal_i(s_prv_activity_cb__args[3].time_start,
s_sys_activity_get_sessions_values.out.sessions[2].start_utc);
// order direction future
s_prv_activity_cb__call_count = 0;
health_service_activities_iterate(HealthActivityMaskAll, now - (200 * SECONDS_PER_MINUTE), now,
HealthIterationDirectionFuture, prv_activity_cb, NULL);
cl_assert_equal_i(7, s_prv_activity_cb__call_count);
cl_assert_equal_i(s_prv_activity_cb__args[0].time_start,
s_sys_activity_get_sessions_values.out.sessions[6].start_utc);
cl_assert_equal_i(s_prv_activity_cb__args[6].time_start,
s_sys_activity_get_sessions_values.out.sessions[0].start_utc);
}
void test_health__peek_current_activities(void) {
HealthActivityMask activities;
activities = health_service_peek_current_activities();
cl_assert_equal_i(activities, HealthActivityNone);
cl_assert_equal_i(s_sys_activity_get_metric_values.in.history_len, 1);
cl_assert_equal_i(s_sys_activity_get_metric_values.in.metric, ActivityMetricSleepState);
s_sys_activity_get_metric_values.out.history[0] = ActivitySleepStateLightSleep;
activities = health_service_peek_current_activities();
cl_assert_equal_i(activities, HealthActivitySleep);
s_sys_activity_get_metric_values.out.history[0] = ActivitySleepStateRestfulSleep;
activities = health_service_peek_current_activities();
cl_assert_equal_i(activities, HealthActivitySleep | HealthActivityRestfulSleep);
s_sys_activity_get_metric_values.out.history[0] = ActivitySleepStateAwake;
s_activity_sessions_ongoing[ActivitySessionType_Run] = true;
s_activity_sessions_ongoing[ActivitySessionType_Walk] = true;
s_activity_sessions_ongoing[ActivitySessionType_Open] = true;
activities = health_service_peek_current_activities();
cl_assert_equal_i(activities, HealthActivityRun | HealthActivityWalk | HealthActivityOpenWorkout);
}
void test_health__session_compare(void) {
const time_t now = rtc_get_time();
// both start at the same time
cl_assert(0 == prv_session_compare(
&(ActivitySession) {.start_utc = now, .length_min = 10},
&(ActivitySession) {.start_utc = now, .length_min = 5},
HealthIterationDirectionFuture));
// a starts earlier
cl_assert(0 > prv_session_compare(
&(ActivitySession) {.start_utc = now, .length_min = 10},
&(ActivitySession) {.start_utc = now + (2 * SECONDS_PER_MINUTE), .length_min = 5},
HealthIterationDirectionFuture));
// b starts earlier
cl_assert(0 < prv_session_compare(
&(ActivitySession) {.start_utc = now, .length_min = 10},
&(ActivitySession) {.start_utc = now - (2 * SECONDS_PER_MINUTE), .length_min = 5},
HealthIterationDirectionFuture));
// both end at the same time
cl_assert(0 == prv_session_compare(
&(ActivitySession) {.start_utc = now, .length_min = 10},
&(ActivitySession) {.start_utc = now + (5 * SECONDS_PER_MINUTE), .length_min = 5},
HealthIterationDirectionPast));
// a ends later
cl_assert(0 > prv_session_compare(
&(ActivitySession) {.start_utc = now, .length_min = 10},
&(ActivitySession) {.start_utc = now + (2 * SECONDS_PER_MINUTE), .length_min = 5},
HealthIterationDirectionPast));
// b ends later
cl_assert(0 < prv_session_compare(
&(ActivitySession) {.start_utc = now, .length_min = 5},
&(ActivitySession) {.start_utc = now + (2 * SECONDS_PER_MINUTE), .length_min = 5},
HealthIterationDirectionPast));
}
void test_health__get_minute_history_edge_case_args(void) {
const time_t now = rtc_get_time();
HealthMinuteData data[5] = {};
uint32_t written;
// null pointer
time_t time_start = now - 10 * 60 - 30;
time_t time_end = now - 20;
written = health_service_get_minute_history(NULL, ARRAY_LENGTH(data), &time_start, &time_end);
cl_assert_equal_i(0, written);
// empty boundary
written = health_service_get_minute_history(data, 0, &time_start, &time_end);
cl_assert_equal_i(0, written);
// empty start
written = health_service_get_minute_history(data, ARRAY_LENGTH(data), NULL, &time_end);
cl_assert_equal_i(0, written);
// empty end before start
time_t early_end = time_start - 20 * SECONDS_PER_MINUTE;
written = health_service_get_minute_history(data, ARRAY_LENGTH(data), &time_start, &early_end);
cl_assert_equal_i(0, written);
// empty end works just fine
s_sys_activity_get_minute_history_values = (sys_activity_get_minute_history_values) {
.out[0] = {
.num_records = 2,
.result = true,
},
};
written = health_service_get_minute_history(data, ARRAY_LENGTH(data), &time_start, NULL);
cl_assert_equal_i(2, written);
}
void test_health__get_minute_history(void) {
const time_t now = rtc_get_time();
HealthMinuteData data[5] = {};
uint32_t written;
s_sys_activity_get_minute_history_values = (sys_activity_get_minute_history_values) {
.out[0] = {
.num_records = 3,
.result = true,
.utc_start = now - 10 * SECONDS_PER_MINUTE,
.records = {
{.is_invalid = false, .steps = 1},
{.is_invalid = true, .steps = 2},
{.is_invalid = false, .steps = 3},
},
},
};
// pass time that's not exactly on a boundary
time_t time_start = now - 10 * SECONDS_PER_MINUTE - 30;
time_t time_end = now - 20;
written = health_service_get_minute_history(data, ARRAY_LENGTH(data), &time_start, &time_end);
cl_assert_equal_i(3, written);
cl_assert_equal_i(now - 10 * SECONDS_PER_MINUTE, time_start);
cl_assert_equal_i(time_start + written * SECONDS_PER_MINUTE, time_end);
cl_assert_equal_i(1, data[0].steps);
cl_assert_equal_i(2, data[1].steps);
cl_assert_equal_i(3, data[2].steps);
// if internal sys_activity returns false, no records were written
s_sys_activity_get_minute_history_values.stage = 0;
s_sys_activity_get_minute_history_values.out[0].result = false;
written = health_service_get_minute_history(data, ARRAY_LENGTH(data), &time_start, &time_end);
cl_assert_equal_i(0, written);
}
void test_health__get_minute_history_respects_time_end(void) {
s_sys_activity_get_minute_history_values.stage = 0;
s_sys_activity_get_minute_history_values.out[0].asserts = false;
HealthMinuteData data[5] = {};
// pass time that's not exactly on a boundary
time_t time_start;
time_t time_end;
////////////
// start time on boundary
const time_t time_on_boundary = (rtc_get_time() / 60 * 60) - 10 * 60;
// respects time_end, 2.5 minutes => 3 records
time_start = time_on_boundary;
time_end = time_start + (5 * SECONDS_PER_MINUTE / 2);
health_service_get_minute_history(data, ARRAY_LENGTH(data), &time_start, &time_end);
cl_assert_equal_i(3, s_sys_activity_get_minute_history_values.in[0].num_records);
// respects time_end, 1 minute => 1 records
s_sys_activity_get_minute_history_values.stage = 0;
time_start = time_on_boundary;
time_end = time_start + SECONDS_PER_MINUTE;
health_service_get_minute_history(data, ARRAY_LENGTH(data), &time_start, &time_end);
cl_assert_equal_i(1, s_sys_activity_get_minute_history_values.in[0].num_records);
// respects time_end == time_start => 0 records
s_sys_activity_get_minute_history_values.stage = 0;
time_start = time_on_boundary;
time_end = time_start;
health_service_get_minute_history(data, ARRAY_LENGTH(data), &time_start, &time_end);
cl_assert_equal_i(0, s_sys_activity_get_minute_history_values.in[0].num_records);
////////////
// start time almost on the next minute
s_sys_activity_get_minute_history_values.stage = 0;
const time_t time_almost_next_minute = time_on_boundary + 59;
// respects time_end, 2.5 minutes => 3 records
time_start = time_almost_next_minute;
time_end = time_start + (5 * SECONDS_PER_MINUTE / 2);
health_service_get_minute_history(data, ARRAY_LENGTH(data), &time_start, &time_end);
cl_assert_equal_i(4, s_sys_activity_get_minute_history_values.in[0].num_records);
// respects time_end, 1 minute => 1 records
s_sys_activity_get_minute_history_values.stage = 0;
time_start = time_almost_next_minute;
time_end = time_start + SECONDS_PER_MINUTE;
health_service_get_minute_history(data, ARRAY_LENGTH(data), &time_start, &time_end);
cl_assert_equal_i(2, s_sys_activity_get_minute_history_values.in[0].num_records);
// respects time_end == time_start => 0 records
s_sys_activity_get_minute_history_values.stage = 0;
time_start = time_almost_next_minute;
time_end = time_start;
health_service_get_minute_history(data, ARRAY_LENGTH(data), &time_start, &time_end);
cl_assert_equal_i(1, s_sys_activity_get_minute_history_values.in[0].num_records);
}
void test_health__get_yesterdays_sleep_activity(void) {
HealthValue start_sec;
HealthValue end_sec;
// our mock setup doesn't provide configurations for multiple calls
// as health_service_private_get_yesterdays_sleep_activity() calls the function twice, both
// values will have the same value
s_sys_activity_get_metric_values.out.history[0] = 123;
bool success = health_service_private_get_yesterdays_sleep_activity(&start_sec, &end_sec);
cl_assert(success);
cl_assert_equal_i(123, start_sec);
cl_assert_equal_i(123, end_sec);
cl_assert_equal_i(1, s_sys_activity_get_metric_values.in.history_len);
cl_assert_equal_i(ActivityMetricSleepExitAtSeconds, s_sys_activity_get_metric_values.in.metric);
}
// Test that health_service_sum_averaged() returns the correct result when asked to get the
// average daily value of a metric
void test_health__avg_full_days(void) {
// Get the current time and day
const time_t now = rtc_get_time();
struct tm local_tm;
localtime_r(&now, &local_tm);
DayInWeek day_in_week = local_tm.tm_wday;
// ----------------------------------------
// Let's fill in some known data for the daily totals and accumulate the totals and counts
// for each day of the week.
int day_totals[DAYS_PER_WEEK] = {};
int day_counts[DAYS_PER_WEEK] = {};
for (int i = 0; i < ACTIVITY_HISTORY_DAYS; i++, day_in_week--) {
day_in_week = positive_modulo(day_in_week, DAYS_PER_WEEK);
s_sys_activity_get_metric_values.out.history[i] = 1000 + (i * 50);
// Day 0 is not included in the stats
if (i == 0) {
continue;
}
// Increment totals for each day of the week
day_totals[day_in_week] += s_sys_activity_get_metric_values.out.history[i];
day_counts[day_in_week] += 1;
}
// ----------------------------------------
// Compute expected values
int exp_weekly = day_totals[local_tm.tm_wday] / day_counts[local_tm.tm_wday];
int exp_daily = 0;
int count = 0;
for (int i = 0; i < DAYS_PER_WEEK; i++) {
exp_daily += day_totals[i];
count += day_counts[i];
}
exp_daily /= count;
int exp_weekend = (day_totals[Sunday] + day_totals[Saturday])
/ (day_counts[Sunday] + day_counts[Saturday]);
int exp_weekday = 0;
count = 0;
for (int i = Monday; i <= Friday; i++) {
exp_weekday += day_totals[i];
count += day_counts[i];
}
exp_weekday /= count;
// ----------------------------------------
// Compute each type of daily average using the API and compare to expected
HealthValue result;
result = health_service_sum_averaged(HealthMetricStepCount,
time_util_get_midnight_of(now) - SECONDS_PER_DAY,
time_util_get_midnight_of(now), HealthServiceTimeScopeDaily);
cl_assert_equal_i(result, exp_daily);
// All of our tests set "now" to Mon, 28 Dec 2015 09:12:22 GMT, so yesteday was a Sunday
result = health_service_sum_averaged(HealthMetricStepCount,
time_util_get_midnight_of(now) - SECONDS_PER_DAY,
time_util_get_midnight_of(now),
HealthServiceTimeScopeDailyWeekdayOrWeekend);
cl_assert_equal_i(result, exp_weekend);
// All of our tests set "now" to Mon, 28 Dec 2015 09:12:22 GMT, so today is a weekday
result = health_service_sum_averaged(HealthMetricStepCount,
time_util_get_midnight_of(now),
time_util_get_midnight_of(now) + SECONDS_PER_DAY,
HealthServiceTimeScopeDailyWeekdayOrWeekend);
cl_assert_equal_i(result, exp_weekday);
// Average weekly value
result = health_service_sum_averaged(HealthMetricStepCount,
time_util_get_midnight_of(now),
time_util_get_midnight_of(now) + SECONDS_PER_DAY,
HealthServiceTimeScopeWeekly);
cl_assert_equal_i(result, exp_weekly);
// Average weekly 48hr avg
result = health_service_sum_averaged(HealthMetricStepCount,
time_util_get_midnight_of(now),
time_util_get_midnight_of(now) + 2 * SECONDS_PER_DAY,
HealthServiceTimeScopeWeekly);
cl_assert_equal_i(result, 2 *exp_weekly);
}
// Return the sum of a bunch of step average chunks that cover the given time range. The time
// range is given in the minute offsets from midnight. This logic is written to produce the
// same results as implemented in health_data_steps_get_current_average() of health_data.c
// Once the Health app is updated to also use the Health API, we can change this logic freely.
static uint32_t prv_averages_sum(uint32_t minute_start_idx, uint32_t minute_end_idx,
const ActivityMetricAverages *avgs) {
cl_assert(minute_start_idx < MINUTES_PER_DAY);
cl_assert(minute_end_idx < MINUTES_PER_DAY);
const int k_minutes_per_step_avg = MINUTES_PER_DAY / ACTIVITY_NUM_METRIC_AVERAGES;
uint32_t chunk_start_idx = minute_start_idx / k_minutes_per_step_avg;
uint32_t chunk_end_idx = minute_end_idx / k_minutes_per_step_avg;
uint32_t sum = 0;
for (int i = chunk_start_idx; i < chunk_end_idx; i++) {
sum += avgs->average[i];
}
return sum;
}
// Test that health_service_sum_averaged() returns the correct result when asked to get the
// intraday average of a metric.
void test_health__avg_partial_days(void) {
// Get the current time and day
const time_t now = rtc_get_time();
struct tm local_tm;
localtime_r(&now, &local_tm);
DayInWeek day_in_week = local_tm.tm_wday;
// Our _initialize should set us to Monday, 9am UTC
cl_assert_equal_i(day_in_week, Monday);
// ----------------------------------
// Let's fill in known data for the 15-minute step averages
s_sys_activity_get_step_averages_values_weekday = (sys_activity_get_step_averages_values){
.out.result = true,
};
s_sys_activity_get_step_averages_values_weekend = (sys_activity_get_step_averages_values){
.out.result = true,
};
for (int i = 0;
i < ARRAY_LENGTH(s_sys_activity_get_step_averages_values_weekday.out.averages.average);
i++) {
s_sys_activity_get_step_averages_values_weekday.out.averages.average[i] = i * 10;
s_sys_activity_get_step_averages_values_weekend.out.averages.average[i] = i * 5;
}
// Let's fill in daily totals that will be used when 15-minute averages are not available
// (i.e. for metrics other than step averages)
const int k_daily_total = 960;
for (int i = 0; i < ACTIVITY_HISTORY_DAYS; i++, day_in_week--) {
s_sys_activity_get_metric_values.out.history[i] = k_daily_total;
}
// ---
// Compute weekday step average from midnight to 9am. This should use the 15-minute
// step averages that we stuffed in.
uint32_t exp_value = prv_averages_sum(0, 9 * MINUTES_PER_HOUR,
&s_sys_activity_get_step_averages_values_weekday.out.averages);
time_t start_of_today = time_start_of_today();
HealthValue value = health_service_sum_averaged(HealthMetricStepCount, start_of_today,
start_of_today + (9 * SECONDS_PER_HOUR),
HealthServiceTimeScopeDailyWeekdayOrWeekend);
cl_assert_equal_i(value, exp_value);
// ---
// Compute weekday HealthMetricActiveSeconds from midnight to 9am. This should use the daily
// totals since we don't have 15-minute averages maintained for this metric
exp_value = (k_daily_total * 9 * MINUTES_PER_HOUR) / MINUTES_PER_DAY;
start_of_today = time_start_of_today();
value = health_service_sum_averaged(HealthMetricActiveSeconds, start_of_today,
start_of_today + (9 * SECONDS_PER_HOUR),
HealthServiceTimeScopeDailyWeekdayOrWeekend);
cl_assert_equal_i(value, exp_value);
// ---
// Compute weekend step average from 4am to 9am. This should use the 15-minute
// step averages that we stuffed in.
exp_value = prv_averages_sum(4 * MINUTES_PER_HOUR, 9 * MINUTES_PER_HOUR,
&s_sys_activity_get_step_averages_values_weekend.out.averages);
// Since "today" is Monday, going back 24 hours puts us on a weekend
time_t start_time = time_start_of_today() - SECONDS_PER_DAY
+ (4 * SECONDS_PER_HOUR);
value = health_service_sum_averaged(HealthMetricStepCount, start_time,
start_time + (5 * SECONDS_PER_HOUR),
HealthServiceTimeScopeDailyWeekdayOrWeekend);
cl_assert_equal_i(value, exp_value);
// ---
// Compute weekend HealthMetricActiveSeconds average from 4am to 9am. This should use the
// daily totals since we don't havce 15-minute averages maintained for this metric
exp_value = (k_daily_total * 5 * MINUTES_PER_HOUR) / MINUTES_PER_DAY;
// Since "today" is Monday, going back 24 hours puts us on a weekend
value = health_service_sum_averaged(HealthMetricActiveSeconds, start_time,
start_time + (5 * SECONDS_PER_HOUR),
HealthServiceTimeScopeDailyWeekdayOrWeekend);
cl_assert_equal_i(value, exp_value);
// ---
// Compute daily step average from midnight to 9am. This should use the 15-minute
// step averages that we stuffed in.
exp_value = 5 * prv_averages_sum(0, 9 * MINUTES_PER_HOUR,
&s_sys_activity_get_step_averages_values_weekday.out.averages);
exp_value += 2 * prv_averages_sum(0, 9 * MINUTES_PER_HOUR,
&s_sys_activity_get_step_averages_values_weekend.out.averages);
exp_value /= 7;
start_of_today = time_start_of_today();
value = health_service_sum_averaged(HealthMetricStepCount, start_of_today,
start_of_today + (9 * SECONDS_PER_HOUR),
HealthServiceTimeScopeDaily);
cl_assert_equal_i(value, exp_value);
}
void test_health__get_measurement_system_for_display(void) {
MeasurementSystem actual =
health_service_get_measurement_system_for_display(HealthMetricSleepSeconds);
cl_assert_equal_i(actual, MeasurementSystemUnknown);
s_units_distance_result = UnitsDistance_Miles;
actual = health_service_get_measurement_system_for_display(HealthMetricWalkedDistanceMeters);
cl_assert_equal_i(actual, MeasurementSystemImperial);
s_units_distance_result = UnitsDistance_KM;
actual = health_service_get_measurement_system_for_display(HealthMetricWalkedDistanceMeters);
cl_assert_equal_i(actual, MeasurementSystemMetric);
}
void test_health__peek_current_value(void) {
const time_t now_utc = rtc_get_time();
// Set the return value to a valid time (Less than HS_MAX_AGE_HR_SAMPLE from the current time)
prv_override_metric(ActivityMetricHeartRateFilteredUpdatedTimeUTC, now_utc);
s_sys_activity_get_metric_values.out.history[0] = 123;
s_sys_activity_get_metric_values.out.history[1] = 456;
HealthValue result = health_service_peek_current_value(HealthMetricHeartRateBPM);
cl_assert_equal_i(123, result);
const ActivityMetric IN_METRIC = ActivityMetricHeartRateFilteredBPM;
cl_assert_equal_i(s_sys_activity_get_metric_values.in.metric, IN_METRIC);
cl_assert_equal_i(s_sys_activity_get_metric_values.in.history_len, 1);
// This is the equivalent to `peek_value` with HeartRateBPM. Make sure it is equal.
result = health_service_aggregate_averaged(HealthMetricHeartRateBPM,
now_utc, now_utc,
HealthAggregationAvg, HealthServiceTimeScopeOnce);
cl_assert_equal_i(123, result);
// This is the equivalent to `peek_value` with HeartRateBPM. Make sure it is equal.
result = health_service_aggregate_averaged(HealthMetricHeartRateBPM,
now_utc - 60, now_utc - 60,
HealthAggregationAvg, HealthServiceTimeScopeOnce);
cl_assert_equal_i(123, result);
// The function call is the equivalent to `peek_value` with HeartRateBPM except for the
// time stamps, make sure it returns 0 because we haven't filled in that data)
result = health_service_aggregate_averaged(HealthMetricHeartRateBPM,
now_utc - 61, now_utc - 61,
HealthAggregationAvg, HealthServiceTimeScopeOnce);
cl_assert_equal_i(0, result);
// Set the return value to an invalid time (More than HS_MAX_AGE_HR_SAMPLE from the current time)
prv_override_metric(ActivityMetricHeartRateFilteredUpdatedTimeUTC,
rtc_get_time() - 20 * SECONDS_PER_MINUTE);
result = health_service_peek_current_value(HealthMetricHeartRateBPM);
cl_assert_equal_i(0, result);
// Asking for a cumulative metric should return error (0)
result = health_service_peek_current_value(HealthMetricStepCount);
cl_assert_equal_i(0, result);
}
static void prv_update_stats(HealthServiceStats *stats, HealthValue value) {
stats->sum += value;
stats->min = MIN(value, stats->min);
stats->max = MAX(value, stats->max);
stats->count++;
stats->avg = stats->sum / stats->count;
}
// Test that health_service_aggregate_averaged() returns the correct result when asked to get the
// aggregates of rate metrics (like heart rate). For these metrics, only min, max, and avg are
// valid aggregation functions. The sum function is only applicable to cumulative metrics and is
// tested above in test_health__sum_full_days().
// DISBLAED because the firmware doesn't actually store daily history of HRM values.
void DISABLED_test_health__min_max_avg_full_days(void) {
// Get the current time and day
const time_t now = rtc_get_time();
const time_t yeserday_utc = now - SECONDS_PER_DAY;
struct tm local_tm;
localtime_r(&now, &local_tm);
DayInWeek todays_day_in_week = local_tm.tm_wday;
localtime_r(&yeserday_utc, &local_tm);
DayInWeek yesterday_day_in_week = local_tm.tm_wday;
bool yesterday_was_weekend = (yesterday_day_in_week == Sunday)
|| (yesterday_day_in_week == Saturday);
PBL_LOG(LOG_LEVEL_DEBUG, "yesterday day in week: %d", yesterday_day_in_week);
// ----------------------------------------
// Let's fill in some known data for the daily totals and accumulate the stats
HealthServiceStats weekly_stats = {
.min = INT32_MAX,
.max = INT32_MIN,
};
HealthServiceStats daily_stats = {
.min = INT32_MAX,
.max = INT32_MIN,
};
HealthServiceStats weekday_stats = {
.min = INT32_MAX,
.max = INT32_MIN,
};
HealthServiceStats weekend_stats = {
.min = INT32_MAX,
.max = INT32_MIN,
};
HealthServiceStats yesterday_stats = {
.min = INT32_MAX,
.max = INT32_MIN,
};
DayInWeek day_in_week = todays_day_in_week;
for (int i = 0; i < ACTIVITY_HISTORY_DAYS; i++, day_in_week--) {
day_in_week = positive_modulo(day_in_week, DAYS_PER_WEEK);
HealthValue value = 1000 + (i * 50);
s_sys_activity_get_metric_values.out.history[i] = value;
PBL_LOG(LOG_LEVEL_DEBUG, "Day #%d, day_of_week: %d, value: %"PRIi32" ", i, day_in_week,
value);
// Day 0 is not included in the stats
if (i == 0) {
continue;
}
// Store if this is yesterday
if (i == 1) {
yesterday_stats = (HealthServiceStats) {
.max = value,
.min = value,
.avg = value,
.sum = value,
.count = 1,
};
}
// Update stats
if (day_in_week == yesterday_day_in_week) {
prv_update_stats(&weekly_stats, value);
PBL_LOG(LOG_LEVEL_DEBUG, "Updating weekly stats with %"PRIi32": sum: %"PRIi32", "
"avg: %"PRIi32" ", value, weekly_stats.sum, weekly_stats.avg);
}
if (day_in_week == Sunday || day_in_week == Saturday) {
prv_update_stats(&weekend_stats, value);
} else {
prv_update_stats(&weekday_stats, value);
}
prv_update_stats(&daily_stats, value);
}
// ----------------------------------------
// Compute each combination of agg/stat and compare to expected
for (HealthAggregation agg = HealthAggregationSum; agg <= HealthAggregationMax; agg++) {
for (HealthServiceTimeScope scope = HealthServiceTimeScopeOnce;
scope <= HealthServiceTimeScopeDaily; scope++) {
// Figure out the expected value
HealthServiceStats *stats = NULL;
char *scope_str;
switch (scope) {
case HealthServiceTimeScopeOnce:
stats = &yesterday_stats;
scope_str = "once";
break;
case HealthServiceTimeScopeWeekly:
stats = &weekly_stats;
scope_str = "weekly";
break;
case HealthServiceTimeScopeDailyWeekdayOrWeekend:
stats = yesterday_was_weekend ? &weekend_stats : &weekday_stats;
scope_str = "weekday/weekend";
break;
case HealthServiceTimeScopeDaily:
stats = &daily_stats;
scope_str = "daily";
break;
}
HealthValue exp_value = 0;
char *agg_str;
switch (agg) {
case HealthAggregationSum:
exp_value = 0; // error case
agg_str = "sum";
break;
case HealthAggregationAvg:
exp_value = stats->avg;
agg_str = "avg";
break;
case HealthAggregationMin:
exp_value = stats->min;
agg_str = "min";
break;
case HealthAggregationMax:
exp_value = stats->max;
agg_str = "max";
break;
}
// Get the value. Since we are computing min, max, avg and we only store 1 value per day
// in our history, passing in a time range less than a day should produce the same result
// as passing in a full day
time_t time_start = time_util_get_midnight_of(now) - SECONDS_PER_DAY;
time_t time_end = time_start + 12 * SECONDS_PER_HOUR; // partial day
// Heart rate should return error if asked for min/max with scope since we can't
// compute that.
if (scope != HealthServiceTimeScopeOnce
&& (agg == HealthAggregationMax || agg == HealthAggregationMin)) {
exp_value = 0;
}
HealthValue result;
result = health_service_aggregate_averaged(HealthMetricHeartRateBPM,
time_start, time_end, agg, scope);
PBL_LOG(LOG_LEVEL_INFO, "Testing %-16s %-16s exp_value: %5"PRIi32", act_value: "
"%5"PRIi32" " , scope_str, agg_str, exp_value, result);
if (scope != HealthServiceTimeScopeOnce) {
// Only test scoped results. Non-scoped results for heart rate are computed using
// minute data and verified in the test_health__heart_rate_scope_once()
cl_assert_equal_i(result, exp_value);
}
}
}
}
// --------------------------------------------------------------------------------------
// Test calls to compute heart rate stats with scope once. This ends up being implemented using
// the minute history
void test_health__heart_rate_scope_once(void) {
const time_t now = rtc_get_time();
const time_t time_start = now - 2 * SECONDS_PER_HOUR;
const time_t time_end = now;
// ----------------------------------------------------------------
// Put in our minute history
HealthServiceCache *cache = NULL;
unsigned num_minutes_per_call = ARRAY_LENGTH(cache->minute_data);
s_sys_activity_get_minute_history_values = (sys_activity_get_minute_history_values) {
.out[0] = {
.num_records = num_minutes_per_call,
.result = true,
.utc_start = time_start,
},
.out[1] = {
.num_records = num_minutes_per_call,
.result = true,
.utc_start = time_start,
},
};
int32_t min_value = INT32_MAX;
int32_t max_value = INT32_MIN;
int32_t sum = 0;
int32_t count = 0;
uint8_t value = 50;
for (unsigned i = 0; i < num_minutes_per_call; i++) {
min_value = MIN(min_value, value);
max_value = MAX(max_value, value);
sum += value;
count++;
s_sys_activity_get_minute_history_values.out[0].records[i].heart_rate_bpm = value;
value++;
if (value > 200) {
value = 50;
}
}
for (unsigned i = 0; i < num_minutes_per_call; i++) {
min_value = MIN(min_value, value);
max_value = MAX(max_value, value);
sum += value;
count++;
s_sys_activity_get_minute_history_values.out[1].records[i].heart_rate_bpm = value;
value++;
if (value > 200) {
value = 50;
}
}
int32_t avg_value = ROUND(sum, count);
// Test each aggregation
for (HealthAggregation agg = HealthAggregationAvg; agg <= HealthAggregationMax; agg++) {
s_sys_activity_get_minute_history_values.stage = 0;
if (agg == HealthAggregationMin) {
PBL_LOG(LOG_LEVEL_DEBUG, "foo");
}
HealthValue result = health_service_aggregate_averaged(HealthMetricHeartRateBPM,
time_start, time_end,
agg, HealthServiceTimeScopeOnce);
cl_assert_equal_i(s_sys_activity_get_minute_history_values.in[0].utc_start, time_start);
cl_assert_equal_i(s_sys_activity_get_minute_history_values.in[0].num_records,
num_minutes_per_call);
cl_assert_equal_i(s_sys_activity_get_minute_history_values.in[1].utc_start,
time_start + SECONDS_PER_HOUR);
cl_assert_equal_i(s_sys_activity_get_minute_history_values.in[1].num_records,
num_minutes_per_call);
if (agg == HealthAggregationAvg) {
cl_assert_equal_i(result, avg_value);
} else if (agg == HealthAggregationMin) {
cl_assert_equal_i(result, min_value);
} else if (agg == HealthAggregationMax) {
cl_assert_equal_i(result, max_value);
} else {
cl_assert(false);
}
}
}
// --------------------------------------------------------------------------------------
int s_metric_alert_count;
static void prv_test_event_handler(HealthEventType event, void *context) {
if (event == HealthEventMetricAlert) {
s_metric_alert_count++;
}
}
// --------------------------------------------------------------------------------------
// Test the health metric alert generation
void test_health__metric_alert_generation(void) {
health_service_events_subscribe(prv_test_event_handler, NULL);
s_sys_activity_get_metric_values.out.result = true;
PebbleEvent event = {
.type = PEBBLE_HEALTH_SERVICE_EVENT,
.health_event.type = HealthEventHeartRateUpdate,
};
// Process some events with various heart rates, should not get an alert
s_metric_alert_count = 0;
for (int i = 50; i < 60; i++) {
s_sys_activity_get_metric_values.out.history[0] = i;
prv_health_event_handler(&event, NULL);
}
// Should not get any metric alerts because none registered
cl_assert_equal_i(s_metric_alert_count, 0);
// Create an alert for heart rate 65
HealthMetricAlert *alert = health_service_register_metric_alert(HealthMetricHeartRateBPM, 65);
for (int i = 60; i < 70; i++) {
s_sys_activity_get_metric_values.out.history[0] = i;
prv_health_event_handler(&event, NULL);
}
// One alert on the way up
cl_assert_equal_i(s_metric_alert_count, 1);
for (int i = 70; i >= 60; i--) {
s_sys_activity_get_metric_values.out.history[0] = i;
PebbleEvent event = {
.type = PEBBLE_HEALTH_SERVICE_EVENT,
.health_event.type = HealthEventHeartRateUpdate,
};
prv_health_event_handler(&event, NULL);
}
// One alert on the way down
cl_assert_equal_i(s_metric_alert_count, 2);
// Remove the alert
s_metric_alert_count = 0;
health_service_cancel_metric_alert(alert);
for (int i = 60; i < 70; i++) {
s_sys_activity_get_metric_values.out.history[0] = i;
prv_health_event_handler(&event, NULL);
}
// Should not get an alert
cl_assert_equal_i(s_metric_alert_count, 0);
}
// --------------------------------------------------------------------------------------
// Test the health metric alert registration
void test_health__metric_alert_registration(void) {
health_service_events_subscribe(prv_test_event_handler, NULL);
time_t now = rtc_get_time();
HealthServiceAccessibilityMask accessible = health_service_metric_aggregate_averaged_accessible(
HealthMetricHeartRateBPM, now, now, HealthAggregationAvg, HealthServiceTimeScopeOnce);
cl_assert(accessible & HealthServiceAccessibilityMaskAvailable);
// Create an alert for heart rate
HealthMetricAlert *alert = health_service_register_metric_alert(HealthMetricHeartRateBPM, 65);
cl_assert(alert != NULL);
// If we try to register another for heart rate, it should fail
HealthMetricAlert *fail_alert = health_service_register_metric_alert(HealthMetricHeartRateBPM,
65);
cl_assert(fail_alert == NULL);
// Cancel the original
health_service_cancel_metric_alert(alert);
// Should be able to register another now
alert = health_service_register_metric_alert(HealthMetricHeartRateBPM, 65);
cl_assert(alert != NULL);
}