mirror of
https://github.com/google/pebble.git
synced 2025-03-21 03:11:21 +00:00
1865 lines
72 KiB
C
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);
|
|
}
|
|
|