/* * Copyright 2024 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "applib/accel_service.h" #include "applib/data_logging.h" #include "applib/health_service.h" #include "applib/health_service_private.h" #include "drivers/rtc.h" #include "drivers/vibe.h" #include "kernel/events.h" #include "services/common/hrm/hrm_manager_private.h" #include "services/normal/activity/activity.h" #include "services/normal/activity/activity_algorithm.h" #include "services/normal/activity/activity_private.h" #include "services/normal/activity/kraepelin/activity_algorithm_kraepelin.h" #include "services/normal/data_logging/data_logging_service.h" #include "services/normal/filesystem/pfs.h" #include "services/normal/protobuf_log/protobuf_log.h" #include "shell/prefs.h" #include "system/logging.h" #include "system/passert.h" #include "util/math.h" #include "util/size.h" #include #include "clar.h" #ifndef PATH_MAX #define PATH_MAX 4096 #endif // Stubs #include "stubs_activity_insights.h" #include "stubs_alarm.h" #include "stubs_app_manager.h" #include "stubs_analytics.h" #include "stubs_app_install_manager.h" #include "stubs_battery.h" #include "stubs_freertos.h" #include "stubs_health_db.h" #include "stubs_hexdump.h" #include "stubs_i18n.h" #include "stubs_logging.h" #include "stubs_mutex.h" #include "stubs_passert.h" #include "stubs_pebble_process_info.h" #include "stubs_prompt.h" #include "stubs_sleep.h" #include "stubs_system_theme.h" #include "stubs_task_watchdog.h" #include "stubs_worker_manager.h" #include "stubs_workout_service.h" // Fakes #include "fake_accel_service.h" #include "fake_cron.h" #include "fake_events.h" #include "fake_new_timer.h" #include "fake_pbl_std.h" #include "fake_rtc.h" #include "fake_spi_flash.h" #include "fake_system_task.h" // We start time out at 5pm on Jan 1, 2015 for all of these tests static const struct tm s_init_time_tm = { // Thursday, Jan 1, 2015, 5:pm .tm_hour = 17, .tm_mday = 1, .tm_mon = 0, .tm_year = 115 }; #define ACTIVITY_FIXTURE_PATH "activity" // The expected resting kcalories is determined empirically from a known good commmit and // is based on the current time of day and the user's weight, age etc. const int s_exp_5pm_resting_kcalories = 1031; const int s_exp_full_day_resting_kcalories = 1455; // Stub for health tracking disabled UI void health_tracking_ui_feature_show_disabled(void) { } void health_tracking_ui_app_show_disabled(void) { } // These are declared as T_STATIC in activity.c void prv_hrm_subscription_cb(PebbleHRMEvent *hrm_event, void *context); void prv_minute_system_task_cb(void *data); bool mfg_info_is_hrm_present(void) { return true; } void hrm_manager_handle_prefs_changed(void) { } #define ASSERT_EQUAL_I(i1,i2,file,line) \ clar__assert_equal_i((i1),(i2),file,line,#i1 " != " #i2, 1) // ====================================================================================== // Misc stubs static HealthServiceState s_health_service; HealthServiceState *app_state_get_health_service_state(void) { return &s_health_service; } HealthServiceState *worker_state_get_health_service_state(void) { cl_fail("should never be called"); return NULL; } void event_service_client_subscribe(EventServiceInfo * service_info) {} void event_service_client_unsubscribe(EventServiceInfo * service_info) {} void sys_send_pebble_event_to_kernel(PebbleEvent* event) {} static UnitsDistance s_units_distance_result; UnitsDistance sys_shell_prefs_get_units_distance(void) { return s_units_distance_result; } int32_t vibes_get_vibe_strength(void) { return VIBE_STRENGTH_OFF; } HRMSessionRef s_hrm_next_session_ref = 1; static uint32_t s_hrm_manager_update_interval; static int s_hrm_manager_num_update_interval_changes; static uint16_t s_hrm_manager_expire_s; HRMSessionRef hrm_manager_subscribe_with_callback(AppInstallId app_id, uint32_t update_interval_s, uint16_t expire_s, HRMFeature features, HRMSubscriberCallback callback, void *context) { s_hrm_manager_update_interval = update_interval_s; s_hrm_manager_expire_s = expire_s; return s_hrm_next_session_ref++; } bool sys_hrm_manager_unsubscribe(HRMSessionRef session) { cl_assert(session < s_hrm_next_session_ref); return true; } bool sys_hrm_manager_set_update_interval(HRMSessionRef session, uint32_t update_interval_s, uint16_t expire_s) { cl_assert(session < s_hrm_next_session_ref); s_hrm_manager_update_interval = update_interval_s; s_hrm_manager_expire_s = expire_s; s_hrm_manager_num_update_interval_changes++; return true; } HRMSessionRef sys_hrm_manager_app_subscribe(AppInstallId app_id, uint32_t update_interval_s, uint16_t expire_s, HRMFeature features) { return HRM_INVALID_SESSION_REF; } HRMSessionRef sys_hrm_manager_get_app_subscription(AppInstallId app_id) { return HRM_INVALID_SESSION_REF; } 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; } AppInstallId app_get_app_id(void) { return 1; } // ====================================================================================== // Queue stubs, to support the semaphore that activity.c uses to block on a kernel BG callback #define QUEUE_HANDLE ((QueueHandle_t)0x11) int s_queue_value = 0; signed portBASE_TYPE xQueueGenericReceive(QueueHandle_t xQueue, void * const pvBuffer, TickType_t xTicksToWait, portBASE_TYPE xJustPeeking) { while (s_queue_value <= 0) { fake_system_task_callbacks_invoke_pending(); } s_queue_value--; return true; } signed portBASE_TYPE xQueueGenericSend(QueueHandle_t xQueue, const void * const pvItemToQueue, TickType_t xTicksToWait, portBASE_TYPE xCopyPosition) { PBL_ASSERTN(xQueue == QUEUE_HANDLE); s_queue_value++; return true; } QueueHandle_t xQueueGenericCreate(unsigned portBASE_TYPE uxQueueLength, unsigned portBASE_TYPE uxItemSize, unsigned char ucQueueType) { return QUEUE_HANDLE; } // ============================================================================================= // Data logging stubs typedef enum { DataLoggingSession_AccelSamples = 1, DataLoggingSession_ActivitySessions = 2, } DataLoggingSession_ID; // Logged items static bool s_dls_accel_samples_created; static int s_num_dls_accel_records; static ActivityRawSamplesRecord s_dls_accel_records[100]; static bool s_dls_activity_sessions_created; static int s_num_dls_activity_records; static ActivitySessionDataLoggingRecord s_dls_activity_records[100]; static void prv_reset_captured_dls_data(void) { s_num_dls_accel_records = 0; s_num_dls_activity_records = 0; } DataLoggingResult dls_log(DataLoggingSession *logging_session, const void *data, uint32_t num_items) { if (logging_session == (DataLoggingSession *)DataLoggingSession_AccelSamples) { cl_assert(s_dls_accel_samples_created); ActivityRawSamplesRecord *records = (ActivityRawSamplesRecord *)data; for (int i = 0; i < num_items; i++) { cl_assert(s_num_dls_accel_records < ARRAY_LENGTH(s_dls_accel_records)); s_dls_accel_records[s_num_dls_accel_records++] = records[i]; } } else if (logging_session == (DataLoggingSession *) DataLoggingSession_ActivitySessions) { cl_assert(s_dls_activity_sessions_created); ActivitySessionDataLoggingRecord *records = (ActivitySessionDataLoggingRecord *)data; for (int i = 0; i < num_items; i++) { if (s_num_dls_activity_records >= 100) { cl_assert(false); } cl_assert(s_num_dls_activity_records < ARRAY_LENGTH(s_dls_activity_records)); s_dls_activity_records[s_num_dls_activity_records++] = records[i]; } } else { return DATA_LOGGING_INVALID_PARAMS; } return DATA_LOGGING_SUCCESS; } DataLoggingSession *dls_create(uint32_t tag, DataLoggingItemType item_type, uint16_t item_size, bool buffered, bool resume, const Uuid *uuid) { if (tag == DlsSystemTagActivityAccelSamples) { s_dls_accel_samples_created = true; cl_assert_equal_i(item_size, sizeof(ActivityRawSamplesRecord)); return (DataLoggingSession *)DataLoggingSession_AccelSamples; } else if (tag == DlsSystemTagActivitySession) { s_dls_activity_sessions_created = true; cl_assert_equal_i(item_size, sizeof(ActivitySessionDataLoggingRecord)); return (DataLoggingSession *) DataLoggingSession_ActivitySessions; } else { return NULL; } } void dls_finish(DataLoggingSession *logging_session) { if (logging_session == (DataLoggingSession *)DataLoggingSession_AccelSamples) { s_dls_accel_samples_created = false; } else if (logging_session == (DataLoggingSession *) DataLoggingSession_ActivitySessions) { s_dls_activity_sessions_created = false; } else { cl_assert(false); } } // ================================================================================= // Measurement logging stubs ProtobufLogRef protobuf_log_hr_create(void) { return (ProtobufLogRef)1; } bool protobuf_log_session_delete(ProtobufLogRef session) { return true; } bool protobuf_log_hr_add_sample(ProtobufLogRef ref, time_t now_utc, uint8_t bpm, HRMQuality quality) { return true; } // ============================================================================================= // Assertion utilities // -------------------------------------------------------------------------------------- static void prv_assert_equal_metric_history(ActivityMetric metric, const uint32_t expected[ACTIVITY_HISTORY_DAYS], char* file, int line) { int32_t actual[ACTIVITY_HISTORY_DAYS]; activity_get_metric(metric, ACTIVITY_HISTORY_DAYS, actual); for (int i = 0; i < ACTIVITY_HISTORY_DAYS; i++) { ASSERT_EQUAL_I(actual[i], expected[i], file, line); } } #define ASSERT_EQUAL_METRIC_HISTORY(metric, expected) \ prv_assert_equal_metric_history((metric), (expected), __FILE__, __LINE__) static void prv_assert_dls_activity_record_present(ActivitySessionDataLoggingRecord *record, char *file, int line) { for (int i = 0; i < s_num_dls_activity_records; i++) { if (!memcmp(record, &s_dls_activity_records[i], sizeof(*record))) { return; } } printf("\nFound records:"); for (int i = 0; i < s_num_dls_activity_records; i++) { printf("\ntype: %d, start_utc: %"PRIu32", elapsed: %"PRIu32", utc_to_local: %"PRIu32" ", (int)s_dls_activity_records[i].activity, (uint32_t)s_dls_activity_records[i].start_utc, s_dls_activity_records[i].elapsed_sec, s_dls_activity_records[i].utc_to_local); } printf("\nLooking for: type: %d, start_utc: %"PRIu32", elapsed: %"PRIu32", " "utc_to_local: %"PRIu32" ", (int)record->activity, (uint32_t)record->start_utc, record->elapsed_sec, record->utc_to_local); clar__assert(false, file, line, "Missing activity record", "", true); } #define ASSERT_ACTIVITY_DLS_RECORD_PRESENT(record) \ prv_assert_dls_activity_record_present((record), __FILE__, __LINE__) // Assert that given number of activity sessions are present static void prv_assert_num_activities(uint32_t num_expected, char *file, int line) { ActivitySession sessions[ACTIVITY_MAX_ACTIVITY_SESSIONS_COUNT]; uint32_t num_sessions = ACTIVITY_MAX_ACTIVITY_SESSIONS_COUNT; activity_get_sessions(&num_sessions, sessions); if (num_sessions != num_expected) { printf("Expected %"PRIu32" activities, but found %"PRIu32".\n", num_expected, num_sessions); } clar__assert(num_sessions == num_expected, file, line, "wrong number of activities", "", true); } // Assert that a particular step activity session is present in the sessions list static void prv_assert_step_activity_present(ActivitySession *exp_session, char *file, int line) { ActivitySession sessions[ACTIVITY_MAX_ACTIVITY_SESSIONS_COUNT]; uint32_t num_sessions = ACTIVITY_MAX_ACTIVITY_SESSIONS_COUNT; activity_get_sessions(&num_sessions, sessions); for (int i = 0; i < num_sessions; i++) { if (sessions[i].type == exp_session->type && sessions[i].start_utc == exp_session->start_utc && sessions[i].length_min == exp_session->length_min && sessions[i].step_data.active_kcalories == exp_session->step_data.active_kcalories && sessions[i].step_data.resting_kcalories == exp_session->step_data.resting_kcalories && sessions[i].step_data.distance_meters == exp_session->step_data.distance_meters && sessions[i].step_data.steps == exp_session->step_data.steps) { return; } } printf("\nFound activities:"); for (int i = 0; i < num_sessions; i++) { printf("\nFound: type: %d, start_utc: %d, len: %"PRIu16", steps: %"PRIu16", " "rest_cal: %"PRIu32", active_cal: %"PRIu32", dist: %"PRIu32" ", (int)sessions[i].type, (int)sessions[i].start_utc, sessions[i].length_min, sessions[i].step_data.steps, sessions[i].step_data.resting_kcalories, sessions[i].step_data.active_kcalories, sessions[i].step_data.distance_meters); } printf("\nLooking for: type: %d, start_utc: %d, len: %"PRIu16", steps: %"PRIu16", " "rest_cal: %"PRIu32", active_cal: %"PRIu32", dist: %"PRIu32" ", (int)exp_session->type, (int)exp_session->start_utc, exp_session->length_min, exp_session->step_data.steps, exp_session->step_data.resting_kcalories, exp_session->step_data.active_kcalories, exp_session->step_data.distance_meters); clar__assert(false, file, line, "Missing activity record", "", true); } // Assert that a particular sleep activity session is present in the sessions list static void prv_assert_sleep_activity_present(ActivitySession *exp_session, char *file, int line) { ActivitySession sessions[ACTIVITY_MAX_ACTIVITY_SESSIONS_COUNT]; uint32_t num_sessions = ACTIVITY_MAX_ACTIVITY_SESSIONS_COUNT; activity_get_sessions(&num_sessions, sessions); for (int i = 0; i < num_sessions; i++) { if (sessions[i].type == exp_session->type && sessions[i].start_utc == exp_session->start_utc && sessions[i].length_min == exp_session->length_min) { return; } } printf("\nFound activities:"); for (int i = 0; i < num_sessions; i++) { printf("\nFound: type: %d, start_utc: %d, len: %"PRIu16" ", (int)sessions[i].type, (int)sessions[i].start_utc, sessions[i].length_min); } printf("\nLooking for: type: %d, start_utc: %d, len: %"PRIu16" ", (int)exp_session->type, (int)exp_session->start_utc, exp_session->length_min); clar__assert(false, file, line, "Missing sleep activity record", "", true); } #define ASSERT_STEP_ACTIVITY_SESSION_PRESENT(session) \ prv_assert_step_activity_present((session), __FILE__, __LINE__) #define ASSERT_SLEEP_ACTIVITY_SESSION_PRESENT(session) \ prv_assert_sleep_activity_present((session), __FILE__, __LINE__) #define ASSERT_NUM_ACTIVITY_SESSIONS(num_sessions) \ prv_assert_num_activities((num_sessions), __FILE__, __LINE__) // ============================================================================================= // Activity algorithm stub // For each accel sample that is fed in, it updates the metrics as follows: // x: increment step count by this much // y: sleep state // #define ALGORITHM_SAMPLING_RATE ACCEL_SAMPLING_25HZ #define TEST_ACTIVITY_MAX_SESSIONS 24 typedef struct { // Captured sessions ActivitySession sessions[TEST_ACTIVITY_MAX_SESSIONS]; int num_sessions_created; time_t last_captured_utc; int sleep_current_container_idx; // >=0 if we have a container in progress ActivitySleepState sleep_state; // Our current sleep state } AlgorithmStateMinuteData; typedef struct { uint16_t steps; // Captured sessions AlgorithmStateMinuteData minute_data; // Rate info uint16_t rate_last_steps; time_t rate_last_update_time; uint16_t rate_steps; uint32_t rate_elapsed_ms; time_t last_sleep_utc; uint8_t orientation; } AlgorithmState; static AlgorithmState s_test_alg_state; bool activity_algorithm_init(AccelSamplingRate *sampling_rate) { *sampling_rate = ALGORITHM_SAMPLING_RATE; AlgorithmStateMinuteData minute_data = s_test_alg_state.minute_data; // Preserve the minute data from the last boot s_test_alg_state = (AlgorithmState) { .minute_data = minute_data, .rate_last_update_time = rtc_get_time(), }; return true; } // Call from unit tests to clear out the "minute data" that might have been left over // from last time static void prv_activity_algorithm_erase_minute_data(void) { s_test_alg_state.minute_data = (AlgorithmStateMinuteData){ }; s_test_alg_state.minute_data.sleep_current_container_idx = -1; s_test_alg_state.minute_data.sleep_state = ActivitySleepStateAwake; } void activity_algorithm_early_deinit(void) { } bool activity_algorithm_deinit(void) { return true; } void activity_algorithm_handle_accel(AccelRawData *data, uint32_t num_samples, uint64_t timestamp) { // For testing purposes, we'll use the x movment as the steps and y as the sleep state ActivitySleepState prior_state = s_test_alg_state.minute_data.sleep_state; time_t now_secs = rtc_get_time(); s_test_alg_state.minute_data.last_captured_utc = now_secs; for (int i = 0; i < num_samples; i++) { s_test_alg_state.steps += data[i].x; s_test_alg_state.minute_data.sleep_state = data[i].y; // Update the length of the current sleep container if we have one if (s_test_alg_state.minute_data.sleep_current_container_idx >= 0) { cl_assert(prior_state != ActivitySleepStateAwake); ActivitySession *session = &s_test_alg_state.minute_data.sessions [s_test_alg_state.minute_data.sleep_current_container_idx]; session->length_min = ROUND(now_secs - session->start_utc, SECONDS_PER_MINUTE); // Inform the activity service of the new state activity_sessions_prv_add_activity_session(session); } // If we were in restful sleep, update that session as well if (prior_state == ActivitySleepStateRestfulSleep) { cl_assert(s_test_alg_state.minute_data.num_sessions_created > 0); ActivitySession *session = &s_test_alg_state.minute_data.sessions [s_test_alg_state.minute_data.num_sessions_created - 1]; session->length_min = ROUND(now_secs - session->start_utc, SECONDS_PER_MINUTE); // Inform the activity service of the new state activity_sessions_prv_add_activity_session(session); } if (s_test_alg_state.minute_data.sleep_state == prior_state) { // No change in state, continue continue; } switch (s_test_alg_state.minute_data.sleep_state) { // We are waking -------------------- case ActivitySleepStateAwake: // End the container s_test_alg_state.minute_data.sleep_current_container_idx = -1; // Send all stored sleep sessions to the activity service now that sleep is over for (int i = 0; i < s_test_alg_state.minute_data.num_sessions_created; i++) { s_test_alg_state.minute_data.sessions[i].ongoing = false; activity_sessions_prv_add_activity_session(&s_test_alg_state.minute_data.sessions[i]); } s_test_alg_state.minute_data.num_sessions_created = 0; break; // We are entering light sleep ------------------ case ActivitySleepStateLightSleep: // Start a light sleep session if we were awake before. If we were in restful sleep, // we should already have one if (prior_state == ActivitySleepStateAwake) { cl_assert(s_test_alg_state.minute_data.num_sessions_created < TEST_ACTIVITY_MAX_SESSIONS); cl_assert(s_test_alg_state.minute_data.sleep_current_container_idx < 0); s_test_alg_state.minute_data.sleep_current_container_idx = s_test_alg_state.minute_data.num_sessions_created; s_test_alg_state.minute_data.sessions[s_test_alg_state.minute_data.num_sessions_created++] = (ActivitySession) { .type = ActivitySessionType_Sleep, .start_utc = now_secs, .length_min = 0, .ongoing = true, }; } else { // We were in restful sleep before, we should already have a container cl_assert(s_test_alg_state.minute_data.sleep_current_container_idx >= 0); } break; // We are entering restful sleep ------------------ case ActivitySleepStateRestfulSleep: // Start a container session if we don't have already if (s_test_alg_state.minute_data.sleep_current_container_idx < 0) { cl_assert(s_test_alg_state.minute_data.num_sessions_created < TEST_ACTIVITY_MAX_SESSIONS); s_test_alg_state.minute_data.sleep_current_container_idx = s_test_alg_state.minute_data.num_sessions_created; s_test_alg_state.minute_data.sessions[s_test_alg_state.minute_data.num_sessions_created++] = (ActivitySession) { .type = ActivitySessionType_Sleep, .start_utc = now_secs, .length_min = 0, .ongoing = true, }; } // Start a restful sleep session cl_assert(s_test_alg_state.minute_data.num_sessions_created < TEST_ACTIVITY_MAX_SESSIONS); s_test_alg_state.minute_data.sessions[s_test_alg_state.minute_data.num_sessions_created++] = (ActivitySession) { .type = ActivitySessionType_RestfulSleep, .start_utc = now_secs, .length_min = 0, .ongoing = true, }; break; case ActivitySleepStateUnknown: break; } prior_state = s_test_alg_state.minute_data.sleep_state; } // Update the rate info // The actual implementation only sends a rate update once every epoch (5 seconds), so // emulate that if ((now_secs - s_test_alg_state.rate_last_update_time) >= 5) { s_test_alg_state.rate_steps = s_test_alg_state.steps - s_test_alg_state.rate_last_steps; s_test_alg_state.rate_elapsed_ms = (now_secs - s_test_alg_state.rate_last_update_time) * MS_PER_SECOND; s_test_alg_state.rate_last_update_time = now_secs; s_test_alg_state.rate_last_steps = s_test_alg_state.steps; } } bool activity_algorithm_set_user(uint32_t height_mm, uint32_t weight_g, ActivityGender gender, uint32_t age_years) { return true; } bool activity_algorithm_get_steps(uint16_t *steps) { *steps = s_test_alg_state.steps; return true; } bool activity_algorithm_get_step_rate(uint16_t *steps, uint32_t *elapsed_ms, time_t *end_sec) { *steps = s_test_alg_state.rate_steps; *elapsed_ms = s_test_alg_state.rate_elapsed_ms; *end_sec = s_test_alg_state.rate_last_update_time; return true; } bool activity_algorithm_metrics_changed_notification(void) { s_test_alg_state.steps = 0; s_test_alg_state.rate_last_steps = 0; s_test_alg_state.rate_last_update_time = rtc_get_time(); return true; } bool activity_algorithm_get_sleep_sessions(time_t sleep_earliest_end_utc, time_t *last_processed_utc) { *last_processed_utc = s_test_alg_state.minute_data.last_captured_utc; for (uint32_t i = 0; i < s_test_alg_state.minute_data.num_sessions_created; i++) { ActivitySession *session = &s_test_alg_state.minute_data.sessions[i]; int start_minute = time_util_get_minute_of_day(session->start_utc); ACTIVITY_LOG_DEBUG("Found session %d: start_min: %d, len_min: %"PRIu16" ", session->type, start_minute, session->length_min); if (!activity_sessions_prv_is_sleep_activity(session->type)) { continue; } if (s_test_alg_state.minute_data.sessions[i].start_utc + (s_test_alg_state.minute_data.sessions[i].length_min * SECONDS_PER_MINUTE) < sleep_earliest_end_utc) { continue; } ACTIVITY_LOG_DEBUG("Returning session %d: start_min: %d, len_min: %"PRIu16" ", session->type, start_minute, session->length_min); activity_sessions_prv_add_activity_session(&s_test_alg_state.minute_data.sessions[i]); } return true; } void activity_algorithm_post_process_sleep_sessions(uint16_t num_input_sessions, ActivitySession *sessions) { } void activity_algorithm_minute_handler(time_t utc_sec, AlgMinuteRecord *record_out) { s_test_alg_state.last_sleep_utc = utc_sec; record_out->data.base.orientation = s_test_alg_state.orientation; } bool activity_algorithm_dump_minute_data_to_log(void) { return false; } bool activity_algorithm_minute_file_info(bool compact_first, uint32_t *num_records, uint32_t *data_bytes, uint32_t *minutes) { *num_records = 0; *data_bytes = 0; *minutes = 0; return true; } bool activity_algorithm_test_fill_minute_file(void) { return true; } // We simulate the activity_algorithm_get_minute_history() call to return data that reflects // that we record chunks of ALG_MINUTES_PER_RECORD minutes at a time. If we don't ask on a // ALG_MINUTES_PER_RECORD minute boundary, we will have up to ALG_MINUTES_PER_RECORD minutes // of data still unavailable before the current time. The data that we do return, we will set // the number of steps equal to (% 255) of the timestamp of that minute. bool activity_algorithm_get_minute_history(HealthMinuteData *minute_data, uint32_t *num_records, time_t *utc_start) { // Get the current time time_t now = rtc_get_time(); // Get the minute index uint32_t minute_idx = now / SECONDS_PER_MINUTE; // Compute the timestamp of the end of the last record we would have available uint32_t last_minute_avail = minute_idx - (minute_idx % ALG_MINUTES_PER_FILE_RECORD); time_t last_second_available = last_minute_avail * SECONDS_PER_MINUTE; // Return the data now uint32_t num_records_requested = *num_records; // Start on next minute boundary *utc_start = ((*utc_start + SECONDS_PER_MINUTE - 1) / SECONDS_PER_MINUTE) * SECONDS_PER_MINUTE; int num_records_returned; time_t record_start_time = *utc_start; for (num_records_returned = 0; num_records_returned < num_records_requested; num_records_returned++, record_start_time += SECONDS_PER_MINUTE) { if (record_start_time + SECONDS_PER_MINUTE > last_second_available) { // This record not available yet. break; } minute_data[num_records_returned] = (HealthMinuteData) { .steps = record_start_time % 255, }; } *num_records = num_records_returned; return true; } time_t activity_algorithm_get_last_sleep_utc(void) { return s_test_alg_state.last_sleep_utc; } bool activity_algorithm_test_send_fake_minute_data_dls_record(void) { return true; } // ========================================================================================= // Tests // --------------------------------------------------------------------------------------- // Feed in X seconds of data with the given statistics. // The fake algorithm we plug in assumes that each accel sample contains the following: // .x : the number of steps to increment by (either 0 or 1) // .y : the current sleep state // .z : 0 static void prv_feed_cannned_accel_data(uint32_t num_sec, uint32_t steps_per_minute, ActivitySleepState sleep_state) { uint32_t num_steps = (steps_per_minute * num_sec + 30) / 60; uint32_t num_samples = num_sec * ALGORITHM_SAMPLING_RATE; uint32_t samples_per_step = 0; if (num_steps > 0) { samples_per_step = num_samples / num_steps; } int need_step_ctr = samples_per_step; time_t utc_secs; uint16_t ms; rtc_get_time_ms(&utc_secs, &ms); uint64_t start_ms = utc_secs * 1000 + ms; uint64_t ms_per_sample = 1000 / ALGORITHM_SAMPLING_RATE; for (int i = 0; i < num_samples; ) { AccelData accel_data[ALGORITHM_SAMPLING_RATE]; for (int j = 0; j < ALGORITHM_SAMPLING_RATE; j++, i++) { need_step_ctr -= 1; accel_data[j] = (AccelData) { .x = (num_steps > 0) && (need_step_ctr <= 0), .y = sleep_state, .z = 0, .timestamp = start_ms, }; start_ms += ms_per_sample; if (need_step_ctr <= 0) { need_step_ctr = samples_per_step; if (num_steps > 0) { num_steps--; } } } fake_accel_service_invoke_callbacks(accel_data, ALGORITHM_SAMPLING_RATE); // Advance time fake_rtc_increment_time(1); fake_rtc_increment_ticks(configTICK_RATE_HZ); // Is it time to call the minute callback? utc_secs += 1; if ((utc_secs % 60) == 0) { fake_cron_job_fire(); fake_system_task_callbacks_invoke_pending(); } } PBL_ASSERTN(num_steps == 0); } // --------------------------------------------------------------------------------------- // Feed in X seconds of raw data static void prv_feed_raw_accel_data(AccelRawData *samples, uint32_t num_samples) { time_t utc_secs; uint16_t ms; rtc_get_time_ms(&utc_secs, &ms); uint64_t start_ms = utc_secs * 1000 + ms; for (int i = 0; i < num_samples; ) { AccelData accel_data[ALGORITHM_SAMPLING_RATE]; int j; for (j = 0; j < ALGORITHM_SAMPLING_RATE && i < num_samples; j++, i++) { accel_data[j] = (AccelData) { .x = samples[i].x, .y = samples[i].y, .z = samples[i].z, .did_vibrate = false, .timestamp = start_ms }; } fake_accel_service_invoke_callbacks(accel_data, j); // Advance time fake_rtc_increment_time(1); fake_rtc_increment_ticks(configTICK_RATE_HZ); // Is it time to call the minute callback? utc_secs += 1; if ((utc_secs % 60) == 0) { fake_cron_job_fire(); fake_system_task_callbacks_invoke_pending(); } } } // -------------------------------------------------------------------------------- // Fast forward time, one minute at a time, calling all minute callbacks along the way. // This does not feed in any accel data static void prv_advance_by_days(uint32_t num_days) { for (int i = 0; i < num_days; i++) { // Advance time fake_rtc_increment_time(SECONDS_PER_DAY); fake_rtc_increment_ticks(configTICK_RATE_HZ * SECONDS_PER_DAY); fake_cron_job_fire(); fake_system_task_callbacks_invoke_pending(); } } // --------------------------------------------------------------------------------------- // Uncompress data stored in the raw accel DLS records static void prv_uncompress_captured_data(AccelRawData *data, uint32_t num_samples) { for (int i = 0; i < s_num_dls_accel_records; i++) { ActivityRawSamplesRecord *record = &s_dls_accel_records[i]; // Verify the header info cl_assert_equal_i(record->version, ACTIVITY_RAW_SAMPLES_VERSION); cl_assert_equal_i(record->len, sizeof(ActivityRawSamplesRecord)); if (i == 0) { cl_assert(record->flags & ACTIVITY_RAW_SAMPLE_FLAG_FIRST_RECORD); } else { cl_assert(!(record->flags & ACTIVITY_RAW_SAMPLE_FLAG_FIRST_RECORD)); } if (i == s_num_dls_accel_records - 1) { cl_assert(record->flags & ACTIVITY_RAW_SAMPLE_FLAG_LAST_RECORD); } else { cl_assert(!(record->flags & ACTIVITY_RAW_SAMPLE_FLAG_LAST_RECORD)); } // Uncompress the entries into samples uint32_t num_samples_seen = 0; for (int j = 0; j < record->num_entries; j++) { uint32_t encoded = record->entries[j]; uint32_t run_size = ACTIVITY_RAW_SAMPLE_GET_RUN_SIZE(encoded); AccelRawData sample = (AccelRawData) { .x = ACTIVITY_RAW_SAMPLE_GET_X(encoded), .y = ACTIVITY_RAW_SAMPLE_GET_Y(encoded), .z = ACTIVITY_RAW_SAMPLE_GET_Z(encoded), }; while (run_size--) { cl_assert(num_samples > 0); *data++ = sample; num_samples--; num_samples_seen++; } } cl_assert_equal_i(num_samples_seen, record->num_samples); } cl_assert_equal_i(num_samples, 0); } // --------------------------------------------------------------------------------------- // Init and enable the activity service static void prv_activity_init_and_set_enabled(bool enable) { activity_init(); activity_set_enabled(enable); fake_system_task_callbacks_invoke_pending(); } // ----------------------------------------------------------------------------------------- // Fetch sleep sessions using the health_service API static uint32_t s_health_sessions_count; static uint32_t s_health_sessions_max; static ActivitySession *s_health_sessions; static time_t s_health_sessions_sleep_time; static time_t s_health_sessions_awake_time; static bool prv_activity_iterate_cb(HealthActivity activity, time_t time_start, time_t time_end, void *context) { if (s_health_sessions_count >= s_health_sessions_max) { return false; } // Update bed and awake time if appropriate if (activity == HealthActivitySleep) { if (s_health_sessions_sleep_time == 0) { s_health_sessions_sleep_time = time_start; } if ((s_health_sessions_awake_time == 0) || (time_end > s_health_sessions_awake_time)) { s_health_sessions_awake_time = time_end; } } char time_start_text[64]; struct tm *local_tm = localtime(&time_start); strftime(time_start_text, sizeof(time_start_text), "%F %r", local_tm); char time_end_text[64]; local_tm = localtime(&time_end); strftime(time_end_text, sizeof(time_end_text), "%F %r", local_tm); PBL_LOG(LOG_LEVEL_DEBUG, "Got activity: %d %s to %s (%d min)", (int)activity, time_start_text, time_end_text, (int)((time_end - time_start) / SECONDS_PER_MINUTE)); // Save the session info ActivitySessionType session_type = ActivitySessionType_Sleep; if (activity == HealthActivitySleep) { session_type = ActivitySessionType_Sleep; } else if (activity == HealthActivityRestfulSleep) { session_type = ActivitySessionType_RestfulSleep; } else { cl_assert(false); } s_health_sessions[s_health_sessions_count] = (ActivitySession) { .type = session_type, .start_utc = time_start, .length_min = ROUND(time_end - time_start, SECONDS_PER_MINUTE), }; s_health_sessions_count += 1; return true; } static void prv_sleep_sessions_using_health_service(uint32_t *session_entries, ActivitySession *sessions, HealthIterationDirection direction) { time_t now = rtc_get_time(); s_health_sessions_count = 0; s_health_sessions_max = *session_entries; s_health_sessions = sessions; s_health_sessions_awake_time = 0; s_health_sessions_sleep_time = 0; health_service_activities_iterate(HealthActivityMaskAll, now - (2 * SECONDS_PER_DAY), now, direction, prv_activity_iterate_cb, NULL); PBL_LOG(LOG_LEVEL_DEBUG, "Found %"PRIu32" activities", s_health_sessions_count); *session_entries = s_health_sessions_count; } static void prv_assert_equal_activity_and_health_sleep_sessions(int exp_num_sessions) { // Get the sleep sessions and make sure we get the expected ones stub_pebble_tasks_set_current(PebbleTask_App); uint32_t session_entries = 24; ActivitySession sessions[session_entries]; activity_get_sessions(&session_entries, sessions); cl_assert_equal_i(session_entries, exp_num_sessions); // Get the sleep sessions using the health API uint32_t health_session_entries = 24; ActivitySession health_sessions[session_entries]; prv_sleep_sessions_using_health_service(&health_session_entries, health_sessions, HealthIterationDirectionFuture); cl_assert_equal_i(health_session_entries, exp_num_sessions); for (int i = 0; i < exp_num_sessions; i++) { cl_assert_equal_i(sessions[i].type, health_sessions[i].type); cl_assert_equal_i(sessions[i].start_utc, health_sessions[i].start_utc); cl_assert_equal_i(sessions[i].length_min, health_sessions[i].length_min); } } // ============================================================================================= // Start of unit tests void test_activity__initialize(void) { TimezoneInfo tz_info = { .tm_zone = "UTC", .tm_gmtoff = 0, }; time_util_update_timezone(&tz_info); struct tm time_tm = s_init_time_tm; time_t utc_sec = mktime(&time_tm); fake_rtc_init(100 /*initial_ticks*/, utc_sec); fake_spi_flash_init(0, 0x1000000); pfs_init(false); pfs_format(false); prv_activity_algorithm_erase_minute_data(); prv_activity_init_and_set_enabled(true); // Set default user settings activity_prefs_set_height_mm(ACTIVITY_DEFAULT_HEIGHT_MM); activity_prefs_set_weight_dag(ACTIVITY_DEFAULT_WEIGHT_DAG); activity_prefs_set_gender(ACTIVITY_DEFAULT_GENDER); activity_prefs_set_age_years(ACTIVITY_DEFAULT_AGE_YEARS); } // --------------------------------------------------------------------------------------- void test_activity__cleanup(void) { activity_stop_tracking(); fake_system_task_callbacks_invoke_pending(); } // --------------------------------------------------------------------------------------- // Test that we correctly initialize the history upon startup based on stored settings void test_activity__init_history(void) { uint32_t exp_resting_kcalories[ACTIVITY_HISTORY_DAYS]; for (int i = 0; i < ACTIVITY_HISTORY_DAYS; i++) { if (i == 0) { exp_resting_kcalories[i] = s_exp_5pm_resting_kcalories; } else { exp_resting_kcalories[i] = s_exp_full_day_resting_kcalories; } } // Should start out with 0 in the history ASSERT_EQUAL_METRIC_HISTORY(ActivityMetricStepCount, ((const uint32_t [ACTIVITY_HISTORY_DAYS]){0, 0, 0, 0, 0, 0, 0})); ASSERT_EQUAL_METRIC_HISTORY(ActivityMetricDistanceMeters, ((const uint32_t [ACTIVITY_HISTORY_DAYS]){0, 0, 0, 0, 0, 0, 0})); ASSERT_EQUAL_METRIC_HISTORY(ActivityMetricSleepTotalSeconds, ((const uint32_t [ACTIVITY_HISTORY_DAYS]){0, 0, 0, 0, 0, 0, 0})); ASSERT_EQUAL_METRIC_HISTORY(ActivityMetricSleepRestfulSeconds, ((const uint32_t [ACTIVITY_HISTORY_DAYS]){0, 0, 0, 0, 0, 0, 0})); ASSERT_EQUAL_METRIC_HISTORY(ActivityMetricActiveKCalories, ((const uint32_t [ACTIVITY_HISTORY_DAYS]){0, 0, 0, 0, 0, 0, 0})); ASSERT_EQUAL_METRIC_HISTORY(ActivityMetricRestingKCalories, exp_resting_kcalories); // Start activity tracking. This method assumes it can be called from any task, so we must // invoke system callbacks to handle its KernelBG callback. activity_start_tracking(false /*test_mode*/); fake_system_task_callbacks_invoke_pending(); // Feed in 100 steps/min over 1 min, 1 minute of deep and 1 minute of light sleep prv_feed_cannned_accel_data(60, 100, ActivitySleepStateAwake); prv_feed_cannned_accel_data(60, 0, ActivitySleepStateLightSleep); prv_feed_cannned_accel_data(60, 0, ActivitySleepStateRestfulSleep); // Put in a stepping activity time_t day_start = time_util_get_midnight_of(rtc_get_time()); ActivitySession walk_activity = { .start_utc = day_start + 12 * SECONDS_PER_HOUR, .length_min = 120, .type = ActivitySessionType_Walk, .step_data = { .steps = 100, .active_kcalories = 200, .resting_kcalories = 300, .distance_meters = 400, }, }; activity_sessions_prv_add_activity_session(&walk_activity); // Capture the resting kcalories now, It is time dependent and we're not sure exactly which time // of day it will be saved to storage int32_t min_resting_kcalories; activity_get_metric(ActivityMetricRestingKCalories, 1, &min_resting_kcalories); // Wait long enough for our recompute sleep and periodic update logic to run. uint32_t wait_min = MAX(ACTIVITY_SESSION_UPDATE_MIN, ACTIVITY_SETTINGS_UPDATE_MIN); prv_feed_cannned_accel_data(SECONDS_PER_MINUTE * wait_min, 0, ActivitySleepStateAwake); ASSERT_EQUAL_METRIC_HISTORY(ActivityMetricStepCount, ((const uint32_t [ACTIVITY_HISTORY_DAYS]){100, 0, 0, 0, 0, 0, 0})); ASSERT_EQUAL_METRIC_HISTORY(ActivityMetricSleepTotalSeconds, ((const uint32_t [ACTIVITY_HISTORY_DAYS]){2 * SECONDS_PER_MINUTE, 0, 0, 0, 0, 0, 0})); ASSERT_EQUAL_METRIC_HISTORY(ActivityMetricSleepRestfulSeconds, ((const uint32_t [ACTIVITY_HISTORY_DAYS]){1 * SECONDS_PER_MINUTE, 0, 0, 0, 0, 0, 0})); // Check that we have the expected # of activities ASSERT_NUM_ACTIVITY_SESSIONS(3); // 2 sleep sessions + 1 activity sessions ASSERT_STEP_ACTIVITY_SESSION_PRESENT(&walk_activity); // The expected resting calories int minutes_today = 17 * MINUTES_PER_HOUR + 3 + wait_min; const int exp_resting_kcalories_now = ROUND(s_exp_full_day_resting_kcalories * minutes_today, MINUTES_PER_DAY); exp_resting_kcalories[0] = exp_resting_kcalories_now; ASSERT_EQUAL_METRIC_HISTORY(ActivityMetricRestingKCalories, exp_resting_kcalories); // See what distance we walked int32_t exp_distance; activity_get_metric(ActivityMetricDistanceMeters, 1, &exp_distance); cl_assert(exp_distance > 0); // Read the active calories int32_t exp_active_kcalories; activity_get_metric(ActivityMetricActiveKCalories, 1, &exp_active_kcalories); cl_assert(exp_active_kcalories > 0); // If we init again, we should start out with the same metrics because we // would have retrieved them from settings prv_activity_init_and_set_enabled(true); ASSERT_EQUAL_METRIC_HISTORY(ActivityMetricStepCount, ((const uint32_t [ACTIVITY_HISTORY_DAYS]){100, 0, 0, 0, 0, 0, 0})); ASSERT_EQUAL_METRIC_HISTORY(ActivityMetricDistanceMeters, ((const uint32_t [ACTIVITY_HISTORY_DAYS]){exp_distance, 0, 0, 0, 0, 0, 0})); ASSERT_EQUAL_METRIC_HISTORY(ActivityMetricActiveKCalories, ((const uint32_t [ACTIVITY_HISTORY_DAYS]){exp_active_kcalories, 0, 0, 0, 0, 0, 0})); ASSERT_EQUAL_METRIC_HISTORY(ActivityMetricSleepTotalSeconds, ((const uint32_t [ACTIVITY_HISTORY_DAYS]){2 * SECONDS_PER_MINUTE, 0, 0, 0, 0, 0, 0})); ASSERT_EQUAL_METRIC_HISTORY(ActivityMetricSleepRestfulSeconds, ((const uint32_t [ACTIVITY_HISTORY_DAYS]){1 * SECONDS_PER_MINUTE, 0, 0, 0, 0, 0, 0})); // The actual resting calories must be in the range from min_resting_kcalories to // exp_resting_kcalories_now because we don't know at exactly which time settings were saved to // storage int32_t actual_resting_kcalories[ACTIVITY_HISTORY_DAYS]; activity_get_metric(ActivityMetricRestingKCalories, ACTIVITY_HISTORY_DAYS, actual_resting_kcalories); for (int i = 0; i < ACTIVITY_HISTORY_DAYS; i++) { if (i == 0) { cl_assert(actual_resting_kcalories[i] >= min_resting_kcalories && actual_resting_kcalories[i] <= exp_resting_kcalories_now); } else { cl_assert_equal_i(actual_resting_kcalories[i], exp_resting_kcalories[i]); } } // Make sure all of our activities persisted ASSERT_NUM_ACTIVITY_SESSIONS(3); // 2 sleep sessions + 1 activity sessions ASSERT_STEP_ACTIVITY_SESSION_PRESENT(&walk_activity); // Pretend that 24 hours has elapsed since we saved prefs. This should put both the step and // sleep history 1 day behind struct tm time_tm = s_init_time_tm; time_tm.tm_mday += 1; time_t utc_sec = mktime(&time_tm); rtc_set_time(utc_sec); prv_activity_init_and_set_enabled(true); ASSERT_EQUAL_METRIC_HISTORY(ActivityMetricStepCount, ((const uint32_t [ACTIVITY_HISTORY_DAYS]){0, 100, 0, 0, 0, 0, 0})); ASSERT_EQUAL_METRIC_HISTORY(ActivityMetricDistanceMeters, ((const uint32_t [ACTIVITY_HISTORY_DAYS]){0, exp_distance, 0, 0, 0, 0, 0})); ASSERT_EQUAL_METRIC_HISTORY(ActivityMetricActiveKCalories, ((const uint32_t [ACTIVITY_HISTORY_DAYS]){0, exp_active_kcalories, 0, 0, 0, 0, 0})); ASSERT_EQUAL_METRIC_HISTORY(ActivityMetricSleepTotalSeconds, ((const uint32_t [ACTIVITY_HISTORY_DAYS]){0, 2 * SECONDS_PER_MINUTE, 0, 0, 0, 0, 0})); ASSERT_EQUAL_METRIC_HISTORY(ActivityMetricSleepRestfulSeconds, ((const uint32_t [ACTIVITY_HISTORY_DAYS]){0, 1 * SECONDS_PER_MINUTE, 0, 0, 0, 0, 0})); activity_get_metric(ActivityMetricRestingKCalories, ACTIVITY_HISTORY_DAYS, actual_resting_kcalories); for (int i = 0; i < ACTIVITY_HISTORY_DAYS; i++) { if (i == 0) { cl_assert_equal_i(actual_resting_kcalories[i], s_exp_5pm_resting_kcalories); } else if (i == 1) { cl_assert(actual_resting_kcalories[i] >= min_resting_kcalories && actual_resting_kcalories[i] <= exp_resting_kcalories_now); } else { cl_assert_equal_i(actual_resting_kcalories[i], exp_resting_kcalories[i]); } } } // --------------------------------------------------------------------------------------- // Test that we correctly initialize the setting upon startup based on the stored settings file void test_activity__settings(void) { // Should start out with defaults uint16_t height_mm; uint16_t weight_dag; ActivityGender gender; uint8_t age_years; height_mm = activity_prefs_get_height_mm(); cl_assert_equal_i(height_mm, ACTIVITY_DEFAULT_HEIGHT_MM); weight_dag = activity_prefs_get_weight_dag(); cl_assert_equal_i(weight_dag, ACTIVITY_DEFAULT_WEIGHT_DAG); gender = activity_prefs_get_gender(); cl_assert_equal_i(gender, ACTIVITY_DEFAULT_GENDER); age_years = activity_prefs_get_age_years(); cl_assert_equal_i(age_years, ACTIVITY_DEFAULT_AGE_YEARS); // Set the settings, re-init, and make sure they stick height_mm += 10; weight_dag += 11; gender = ActivityGenderOther; age_years += 10; activity_prefs_set_height_mm(height_mm); activity_prefs_set_weight_dag(weight_dag); activity_prefs_set_gender(gender); activity_prefs_set_age_years(age_years); // Re-init prv_activity_init_and_set_enabled(true); // Check settings uint32_t value; value = activity_prefs_get_height_mm(); cl_assert_equal_i(height_mm, value); value = activity_prefs_get_weight_dag(); cl_assert_equal_i(weight_dag, value); value = activity_prefs_get_gender(); cl_assert_equal_i(gender, value); value = activity_prefs_get_age_years(); cl_assert_equal_i(age_years, value); // Reset settings activity_prefs_set_height_mm(ACTIVITY_DEFAULT_HEIGHT_MM); activity_prefs_set_weight_dag(ACTIVITY_DEFAULT_WEIGHT_DAG); activity_prefs_set_gender(ACTIVITY_DEFAULT_GENDER); activity_prefs_set_age_years(ACTIVITY_DEFAULT_AGE_YEARS); } // --------------------------------------------------------------------------------------- // Test that our periodic minute callback correctly detects the midnight rollover void test_activity__day_rollover(void) { // Start activity tracking. This method assumes it can be called from any task, so we must // invoke system callbacks to handle its KernelBG callback. activity_start_tracking(false /*test_mode*/); fake_system_task_callbacks_invoke_pending(); // Feed in 100 steps/min over 1 min, 1 minute of deep and 1 minute of light sleep prv_feed_cannned_accel_data(60, 100, ActivitySleepStateAwake); prv_feed_cannned_accel_data(60, 0, ActivitySleepStateLightSleep); prv_feed_cannned_accel_data(60, 0, ActivitySleepStateRestfulSleep); // Wait long enough for our recompute sleep logic to run. prv_feed_cannned_accel_data(SECONDS_PER_MINUTE * ACTIVITY_SESSION_UPDATE_MIN, 0, ActivitySleepStateAwake); ASSERT_EQUAL_METRIC_HISTORY(ActivityMetricStepCount, ((const uint32_t [ACTIVITY_HISTORY_DAYS]){100, 0, 0, 0, 0, 0, 0})); ASSERT_EQUAL_METRIC_HISTORY(ActivityMetricSleepTotalSeconds, ((const uint32_t [ACTIVITY_HISTORY_DAYS]){2 * SECONDS_PER_MINUTE, 0, 0, 0, 0, 0, 0})); ASSERT_EQUAL_METRIC_HISTORY(ActivityMetricSleepRestfulSeconds, ((const uint32_t [ACTIVITY_HISTORY_DAYS]){1 * SECONDS_PER_MINUTE, 0, 0, 0, 0, 0, 0})); // Expected resting calories uint32_t exp_resting_kcalories[ACTIVITY_HISTORY_DAYS]; for (int i = 0; i < ACTIVITY_HISTORY_DAYS; i++) { if (i == 0) { // All tests start at 5pm, we we just entered 3 minutes of data. uint32_t minutes_today = 17 * MINUTES_PER_HOUR + 3 + ACTIVITY_SESSION_UPDATE_MIN; exp_resting_kcalories[i] = ROUND(s_exp_full_day_resting_kcalories * minutes_today, MINUTES_PER_DAY); } else { exp_resting_kcalories[i] = s_exp_full_day_resting_kcalories; } } ASSERT_EQUAL_METRIC_HISTORY(ActivityMetricRestingKCalories, exp_resting_kcalories); // Put in 2 activities, one of which should drop off on a new day because it's old and the // other which drop off because it is in the future (invalid) time_t day_start = time_util_get_midnight_of(rtc_get_time()); ActivitySession old_activity = { .start_utc = day_start + 12 * SECONDS_PER_HOUR, .length_min = 120, .type = ActivitySessionType_Walk, .step_data = { .steps = 100, .active_kcalories = 200, .resting_kcalories = 300, .distance_meters = 400, }, }; ActivitySession new_activity = { .start_utc = day_start + 23 * SECONDS_PER_HOUR, .length_min = 120, .type = ActivitySessionType_Run, .step_data = { .steps = 1000, .active_kcalories = 300, .resting_kcalories = 400, .distance_meters = 500, }, }; activity_sessions_prv_add_activity_session(&old_activity); activity_sessions_prv_add_activity_session(&new_activity); ASSERT_NUM_ACTIVITY_SESSIONS(4); // 2 sleep sessions + 2 activity sessions ASSERT_STEP_ACTIVITY_SESSION_PRESENT(&old_activity); ASSERT_STEP_ACTIVITY_SESSION_PRESENT(&new_activity); // Wait long enough for our midnight rollover to occur. We init time at 5pm, so we need to wait // for at least 7 hours. const int minutes_till_midnight = (7 * MINUTES_PER_HOUR) - ACTIVITY_SESSION_UPDATE_MIN - 3; prv_feed_cannned_accel_data(SECONDS_PER_MINUTE * (minutes_till_midnight + 1), 0, ActivitySleepStateAwake); ASSERT_EQUAL_METRIC_HISTORY(ActivityMetricStepCount, ((const uint32_t [ACTIVITY_HISTORY_DAYS]){0, 100, 0, 0, 0, 0, 0})); ASSERT_EQUAL_METRIC_HISTORY(ActivityMetricSleepTotalSeconds, ((const uint32_t [ACTIVITY_HISTORY_DAYS]){0, 2 * SECONDS_PER_MINUTE, 0, 0, 0, 0, 0})); ASSERT_EQUAL_METRIC_HISTORY(ActivityMetricSleepRestfulSeconds, ((const uint32_t [ACTIVITY_HISTORY_DAYS]){0, 1 * SECONDS_PER_MINUTE, 0, 0, 0, 0, 0})); for (int i = 0; i < ACTIVITY_HISTORY_DAYS; i++) { if (i == 0) { exp_resting_kcalories[i] = 1; } else if (i == 1) { exp_resting_kcalories[i] = ROUND(s_exp_full_day_resting_kcalories * (MINUTES_PER_DAY - 1), MINUTES_PER_DAY); } else { exp_resting_kcalories[i] = s_exp_full_day_resting_kcalories; } } ASSERT_EQUAL_METRIC_HISTORY(ActivityMetricRestingKCalories, exp_resting_kcalories); // Verify that the expired and invalid activity session have been removed ASSERT_NUM_ACTIVITY_SESSIONS(0); // Verify that we have the right history capacity uint32_t exp_history[ACTIVITY_HISTORY_DAYS]; for (int i = 1; i < ACTIVITY_HISTORY_DAYS; i++) { memset(exp_history, 0, sizeof(exp_history)); exp_history[i] = 100; ASSERT_EQUAL_METRIC_HISTORY(ActivityMetricStepCount, exp_history); prv_advance_by_days(1); } } // --------------------------------------------------------------------------------------- // Derived metrics like distance, calories, and walking minutes that are based on steps void test_activity__step_derived_metrics(void) { int32_t value; // All tests start at 5pm, which is 1020 minutes into the day const int k_minute_start = 1020; // Set the user's dimensions const int k_height_mm = 1630; activity_prefs_set_height_mm(k_height_mm); activity_prefs_set_weight_dag(6800); activity_prefs_set_gender(ActivityGenderFemale); activity_prefs_set_age_years(30); // The health_service calls expect to be in the app or worker task stub_pebble_tasks_set_current(PebbleTask_App); // Advance to a new day to give a chance for the new resting metabolism to be incorporated struct tm time_tm = s_init_time_tm; time_tm.tm_mday += 1; time_t utc_sec = mktime(&time_tm); rtc_set_time(utc_sec); prv_activity_init_and_set_enabled(true); // Start activity tracking. This method assumes it can be called from any task, so we must // invoke system callbacks to handle its KernelBG callback. activity_start_tracking(false /*test_mode*/); fake_system_task_callbacks_invoke_pending(); // All tests start at 5pm, which is 1020 minutes into a 1440 minute day. The BMR for // the above user is 1388 kcalories per day, so we expect to get: // 1388 * 1020/1440 = 1023 kcalories activity_get_metric(ActivityMetricRestingKCalories, 1, &value); const int k_bmr_cal = 1388 * ACTIVITY_CALORIES_PER_KCAL; cl_assert_equal_i(value, ROUND(k_bmr_cal * k_minute_start / MINUTES_PER_DAY, ACTIVITY_CALORIES_PER_KCAL)); cl_assert_equal_i(health_service_sum_today(HealthMetricRestingKCalories), value); // Feed in 100 steps/minute over 1 hour (walking rate) prv_feed_cannned_accel_data(SECONDS_PER_HOUR, 100, ActivitySleepStateAwake); const int k_exp_steps = 100 * MINUTES_PER_HOUR; // Test the derived metrics activity_get_metric(ActivityMetricStepCount, 1, &value); cl_assert_equal_i(value, k_exp_steps); cl_assert_equal_i(health_service_sum_today(HealthMetricStepCount), k_exp_steps); activity_get_metric(ActivityMetricActiveSeconds, 1, &value); cl_assert_equal_i(value, SECONDS_PER_HOUR); cl_assert_equal_i(health_service_sum_today(HealthMetricActiveSeconds), SECONDS_PER_HOUR); activity_get_metric(ActivityMetricActiveKCalories, 1, &value); // The following determined from a known good commit int32_t exp_active_kcalories = 152; cl_assert_equal_i(value, exp_active_kcalories); cl_assert_equal_i(health_service_sum_today(HealthMetricActiveKCalories), exp_active_kcalories); // We now expect to get the following resting calories since we are now 1025 minutes into the day: const int exp_resting_calories = k_bmr_cal * (k_minute_start + MINUTES_PER_HOUR) / MINUTES_PER_DAY; activity_get_metric(ActivityMetricRestingKCalories, 1, &value); cl_assert_equal_i(value, ROUND(exp_resting_calories, ACTIVITY_CALORIES_PER_KCAL)); // Test that ActivityMetricStepMinutes responds correctly prv_feed_cannned_accel_data(1 * SECONDS_PER_MINUTE, 100, ActivitySleepStateAwake); prv_feed_cannned_accel_data(1 * SECONDS_PER_MINUTE, 10, ActivitySleepStateAwake); prv_feed_cannned_accel_data(1 * SECONDS_PER_MINUTE, 100, ActivitySleepStateAwake); prv_feed_cannned_accel_data(1 * SECONDS_PER_MINUTE, 10, ActivitySleepStateAwake); activity_get_metric(ActivityMetricActiveSeconds, 1, &value); cl_assert_equal_i(value, SECONDS_PER_HOUR + (2 * SECONDS_PER_MINUTE)); cl_assert_equal_i(health_service_sum_today(HealthMetricActiveSeconds), SECONDS_PER_HOUR + (2 * SECONDS_PER_MINUTE)); // ---------------------------------------------------------------------------------- // Reset and try another case. Faster pace and taller person activity_stop_tracking(); fake_system_task_callbacks_invoke_pending(); const int k_height_mm_2 = 1830; activity_prefs_set_height_mm(k_height_mm_2); activity_prefs_set_weight_dag(9100); activity_prefs_set_gender(ActivityGenderMale); activity_prefs_set_age_years(40); // Another day utc_sec += SECONDS_PER_DAY; rtc_set_time(utc_sec); prv_activity_init_and_set_enabled(true); // Start activity tracking. This method assumes it can be called from any task, so we must // invoke system callbacks to handle its KernelBG callback. activity_start_tracking(false /*test_mode*/); fake_system_task_callbacks_invoke_pending(); // All tests start at 5pm, which is 1020 minutes into a 1440 minute day. The BMR for // the above user is 1859 kcalories per day, so we expect to get: // 1859 * 1020/1440 = 1328 kcalories activity_get_metric(ActivityMetricRestingKCalories, 1, &value); const int k_bmr_cal_2 = 1859 * ACTIVITY_CALORIES_PER_KCAL; cl_assert_equal_i(value, ROUND(k_bmr_cal_2 * k_minute_start / MINUTES_PER_DAY, ACTIVITY_CALORIES_PER_KCAL)); // Feed in 125 steps/minute over 60 minutes prv_feed_cannned_accel_data(60 * SECONDS_PER_MINUTE, 125, ActivitySleepStateAwake); const int k_exp_steps_2 = 125 * MINUTES_PER_HOUR; // Test the derived metrics activity_get_metric(ActivityMetricActiveSeconds, 1, &value); cl_assert_equal_i(value, SECONDS_PER_HOUR); activity_get_metric(ActivityMetricStepCount, 1, &value); cl_assert_equal_i(value, k_exp_steps_2); activity_get_metric(ActivityMetricActiveKCalories, 1, &value); // The following determined from a known good commit int32_t exp_active_kcalories_2 = 486; cl_assert_equal_i(value, exp_active_kcalories_2); // We now expect to get the following resting calories const int exp_resting_calories_2 = k_bmr_cal_2 * (k_minute_start + MINUTES_PER_HOUR) / MINUTES_PER_DAY; activity_get_metric(ActivityMetricRestingKCalories, 1, &value); cl_assert_equal_i(value, ROUND(exp_resting_calories_2, ACTIVITY_CALORIES_PER_KCAL)); } // --------------------------------------------------------------------------------------- // Test derived metrics based on sleep data void test_activity__sleep_derived_metrics(void) { int32_t value; // Start activity tracking. This method assumes it can be called from any task, so we must // invoke system callbacks to handle its KernelBG callback. activity_start_tracking(false /*test_mode*/); fake_system_task_callbacks_invoke_pending(); // All of our tests start at 5pm. Let's enter a sleep cycle where the user gets into bed // at 10pm, takes 30 minutes to fall asleep, and wakes up at 6am. // Light walking, 50 steps/minute, until 10pm prv_feed_cannned_accel_data(5 * SECONDS_PER_HOUR, 50, ActivitySleepStateAwake); // Falling asleep for 30 minutes prv_feed_cannned_accel_data(30 * SECONDS_PER_MINUTE, 5, ActivitySleepStateAwake); // Starting at 10:30pm: 2 Cycles of light (60 min), deep (50 min), awake (10 min) for (int i = 0; i < 2; i++) { prv_feed_cannned_accel_data(60 * SECONDS_PER_MINUTE, 0, ActivitySleepStateLightSleep); activity_get_metric(ActivityMetricSleepState, 1, &value); cl_assert_equal_i(value, ActivitySleepStateLightSleep); prv_feed_cannned_accel_data(50 * SECONDS_PER_MINUTE, 0, ActivitySleepStateRestfulSleep); activity_get_metric(ActivityMetricSleepState, 1, &value); cl_assert_equal_i(value, ActivitySleepStateRestfulSleep); prv_feed_cannned_accel_data(10 * SECONDS_PER_MINUTE, 20, ActivitySleepStateAwake); } // 30 minute "morning walk" 4 hours later at 2:30am prv_feed_cannned_accel_data(30 * SECONDS_PER_MINUTE, 50, ActivitySleepStateAwake); activity_get_metric(ActivityMetricSleepState, 1, &value); cl_assert_equal_i(value, ActivitySleepStateAwake); cl_assert_equal_i(health_service_peek_current_activities(), HealthActivityNone); int exp_value = 22 * SECONDS_PER_HOUR + 30 * SECONDS_PER_MINUTE; // 10:30pm in minutes activity_get_metric(ActivityMetricSleepEnterAtSeconds, 1, &value); cl_assert_equal_i(value, exp_value); activity_get_metric(ActivityMetricSleepStateSeconds, 1, &value); // Ideally it would show 40 minutes, but we only sample once every ACTIVITY_SESSION_UPDATE_MIN minutes cl_assert(value <= 40 * SECONDS_PER_MINUTE && value >= (40 - ACTIVITY_SESSION_UPDATE_MIN) * SECONDS_PER_MINUTE); // Verify the root metrics. Since we also verify these using the health_service api, set // the task to the app task now stub_pebble_tasks_set_current(PebbleTask_App); activity_get_metric(ActivityMetricSleepTotalSeconds, 1, &value); cl_assert_equal_i(value, 220 * SECONDS_PER_MINUTE); cl_assert_equal_i(health_service_sum_today(HealthMetricSleepSeconds), 220 * SECONDS_PER_MINUTE); activity_get_metric(ActivityMetricSleepRestfulSeconds, 1, &value); cl_assert_equal_i(value, 100 * SECONDS_PER_MINUTE); cl_assert_equal_i(health_service_sum_today(HealthMetricSleepRestfulSeconds), 100 * SECONDS_PER_MINUTE); activity_get_metric(ActivityMetricSleepExitAtSeconds, 1, &value); cl_assert_equal_i(value, 2 * SECONDS_PER_HOUR + 20 * SECONDS_PER_MINUTE /* 2:20am in minutes */); } // --------------------------------------------------------------------------------------- // Test that sleep sessions get registered in the correct day void test_activity__sleep_history(void) { // Start activity tracking. This method assumes it can be called from any task, so we must // invoke system callbacks to handle its KernelBG callback. activity_start_tracking(false /*test_mode*/); fake_system_task_callbacks_invoke_pending(); // All of our tests start at 5pm. Let's enter a sleep cycle where the user has a sleep session // before the cut-off for the new day // Light walking, 50 steps/minute, until 6pm prv_feed_cannned_accel_data(1 * SECONDS_PER_HOUR, 50, ActivitySleepStateAwake); // 2.5 hours of sleep, put's us at 8:30pm. The cut-off for the next day is // ACTIVITY_LAST_SLEEP_MINUTE_OF_DAY, currently set for 9pm so this session should be // registered for today prv_feed_cannned_accel_data(150 * SECONDS_PER_MINUTE, 0, ActivitySleepStateLightSleep); // Awake for 30 minutes which puts us at 9pm. prv_feed_cannned_accel_data(30 * SECONDS_PER_MINUTE, 20, ActivitySleepStateAwake); // Another 2 hour sleep session starting at 9pm. This will leave us at 11pm. Since this // session ends after the the cutoff, it should be registered for the next day prv_feed_cannned_accel_data(120 * SECONDS_PER_MINUTE, 0, ActivitySleepStateLightSleep); // Awake for 2 hours which puts us at 1am prv_feed_cannned_accel_data(120 * SECONDS_PER_MINUTE, 20, ActivitySleepStateAwake); // Now if we get sleep history, we should have 2.5 hours yesterday, and 2 hours today ASSERT_EQUAL_METRIC_HISTORY(ActivityMetricSleepTotalSeconds, ((const uint32_t [ACTIVITY_HISTORY_DAYS]){120 * SECONDS_PER_MINUTE, 150 * SECONDS_PER_MINUTE})); // Another 2 hour sleep session starting at 1am. This will leave us at 3am. prv_feed_cannned_accel_data(120 * SECONDS_PER_MINUTE, 0, ActivitySleepStateLightSleep); // Awake for 1 hour which puts us at 4am prv_feed_cannned_accel_data(60 * SECONDS_PER_MINUTE, 20, ActivitySleepStateAwake); ASSERT_EQUAL_METRIC_HISTORY(ActivityMetricSleepTotalSeconds, ((const uint32_t [ACTIVITY_HISTORY_DAYS]){240 * SECONDS_PER_MINUTE, 150 * SECONDS_PER_MINUTE})); } // --------------------------------------------------------------------------------------- // Test raw sample capturing void test_activity__raw_sample_collection(void) { bool enabled; uint32_t session_id; uint32_t num_samples; uint32_t seconds; // Start activity tracking. This method assumes it can be called from any task, so we must // invoke system callbacks to handle its KernelBG callback. activity_start_tracking(false /*test_mode*/); fake_system_task_callbacks_invoke_pending(); // --------------------------------------------------------------------------------------- // Feed in some raw samples where every sample is unique { prv_reset_captured_dls_data(); activity_raw_sample_collection(true, false, &enabled, &session_id, &num_samples, &seconds); cl_assert(enabled); cl_assert_equal_i(num_samples, 0); // Feed in 510 values to test entire dynamic range const int k_raw_samples = 510; AccelRawData raw_data[k_raw_samples]; for (int i = 0; i < k_raw_samples; i++) { // We store multiples of 8 because the compression algorithm divides by 8. raw_data[i].x = i * 8; raw_data[i].y = -i * 8; raw_data[i].z = (i + 1) * 8; } prv_feed_raw_accel_data(raw_data, k_raw_samples); // Stop collection activity_raw_sample_collection(false, true, &enabled, &session_id, &num_samples, &seconds); cl_assert(!enabled); cl_assert_equal_i(num_samples, k_raw_samples); cl_assert_equal_i(seconds, (k_raw_samples + ALGORITHM_SAMPLING_RATE - 1) / ALGORITHM_SAMPLING_RATE); // Verify the collected data AccelRawData captured_data[k_raw_samples]; prv_uncompress_captured_data(captured_data, k_raw_samples); cl_assert_equal_m(raw_data, captured_data, k_raw_samples * sizeof(AccelRawData)); } // --------------------------------------------------------------------------------------- // Feed in some raw samples with some runs { prv_reset_captured_dls_data(); activity_raw_sample_collection(true, false, &enabled, &session_id, &num_samples, &seconds); cl_assert(enabled); cl_assert_equal_i(num_samples, 0); // Feed in 510 values to test entire dynamic range const int k_raw_samples = 510; AccelRawData raw_data[k_raw_samples]; int value = 0; for (int i = 0; i < k_raw_samples; i++) { // We store multiples of 8 because the compression algorithm divides by 8. raw_data[i].x = value * 8; raw_data[i].y = -value * 8; raw_data[i].z = (value + 1) * 8; if ((i % 7) == 0) { value += 1; } } prv_feed_raw_accel_data(raw_data, k_raw_samples); // Stop collection activity_raw_sample_collection(false, true, &enabled, &session_id, &num_samples, &seconds); cl_assert(!enabled); cl_assert_equal_i(num_samples, k_raw_samples); cl_assert_equal_i(seconds, (k_raw_samples + ALGORITHM_SAMPLING_RATE - 1) / ALGORITHM_SAMPLING_RATE); // Verify the collected data AccelRawData captured_data[k_raw_samples]; prv_uncompress_captured_data(captured_data, k_raw_samples); cl_assert_equal_m(raw_data, captured_data, k_raw_samples * sizeof(AccelRawData)); } } // --------------------------------------------------------------------------------------- // Test getting the sleep sessions void test_activity__get_sleep_sessions(void) { int32_t value; // Start activity tracking. This method assumes it can be called from any task, so we must // invoke system callbacks to handle its KernelBG callback. activity_start_tracking(false /*test_mode*/); fake_system_task_callbacks_invoke_pending(); // Light walking, 50 steps/minute, until 10pm prv_feed_cannned_accel_data(5 * SECONDS_PER_HOUR, 50, ActivitySleepStateAwake); // Falling asleep for 30 minutes prv_feed_cannned_accel_data(30 * SECONDS_PER_MINUTE, 5, ActivitySleepStateAwake); // Starting at 10:30pm: 2 Cycles of light (60 min), deep (50 min), awake (10 min) for (int i = 0; i < 2; i++) { prv_feed_cannned_accel_data(60 * SECONDS_PER_MINUTE, 0, ActivitySleepStateLightSleep); prv_feed_cannned_accel_data(50 * SECONDS_PER_MINUTE, 0, ActivitySleepStateRestfulSleep); prv_feed_cannned_accel_data(10 * SECONDS_PER_MINUTE, 20, ActivitySleepStateAwake); } // 30 minute "morning walk" 4 hours later at 2:30am prv_feed_cannned_accel_data(30 * SECONDS_PER_MINUTE, 50, ActivitySleepStateAwake); activity_get_metric(ActivityMetricSleepState, 1, &value); cl_assert_equal_i(value, ActivitySleepStateAwake); // Assert that we got the same sleep sessions using the activity service as we do using // the health API prv_assert_equal_activity_and_health_sleep_sessions(4); } // --------------------------------------------------------------------------------------- // Test getting the minute history void test_activity__get_minute_history(void) { // Start activity tracking. This method assumes it can be called from any task, so we must // invoke system callbacks to handle its KernelBG callback. activity_start_tracking(false /*test_mode*/); fake_system_task_callbacks_invoke_pending(); const uint32_t exp_num_records = 10; HealthMinuteData minutes[exp_num_records]; // The last ALG_MINUTES_PER_RECORD of minutes may not be available yet, so start // well enough before that time_t utc_start = rtc_get_time() - ((ALG_MINUTES_PER_FILE_RECORD * 2) * SECONDS_PER_MINUTE); const time_t exp_utc_start = utc_start; stub_pebble_tasks_set_current(PebbleTask_App); uint32_t num_records = exp_num_records; activity_get_minute_history(minutes, &num_records, &utc_start); cl_assert_equal_i(num_records, exp_num_records); cl_assert_equal_i(utc_start, exp_utc_start); cl_assert_equal_i(minutes[0].steps, exp_utc_start % 255); // --------------------------------------------------------------------------------------- // Once a minute, retrieve the last ALG_MINUTES_PER_RECORD minutes of data. We should // get 1 fewer record each time because we know that the activity algorithm code only // writes a new minute data record once every ALG_MINUTES_PER_RECORD minutes. // Start on a ALG_MINUTES_PER_RECORD minute boundary so that we know we have // ALG_MINUTES_PER_RECORD records available up to the current time struct tm start_tm = { // Jan 1, 2015, 5am .tm_hour = 5, .tm_mday = 1, .tm_mon = 0, .tm_year = 115 }; time_t utc_sec = mktime(&start_tm); rtc_set_time(utc_sec); time_t oldest_to_fetch = rtc_get_time() - (ALG_MINUTES_PER_FILE_RECORD * SECONDS_PER_MINUTE); for (int i = 0; i < ALG_MINUTES_PER_FILE_RECORD; i++) { // Ask for the last ALG_MINUTES_PER_RECORD minutes of data num_records = ALG_MINUTES_PER_FILE_RECORD; time_t start_time = oldest_to_fetch + (i * SECONDS_PER_MINUTE); time_t end_time = utc_sec; HealthMinuteData received_records[ALG_MINUTES_PER_FILE_RECORD]; num_records = health_service_get_minute_history(received_records, num_records, &start_time, &end_time); cl_assert_equal_i(num_records, ALG_MINUTES_PER_FILE_RECORD - i); cl_assert_equal_i(start_time, oldest_to_fetch + (i * SECONDS_PER_MINUTE)); printf("\nReceived %d minute records", (int)num_records); for (int j = 0; j < num_records; j++) { printf("\nRecord:%d, steps: %d", j, (int)received_records[j].steps); } // Verify the contents of the records for (int j = 0; j < num_records; j++) { cl_assert_equal_i(received_records[j].steps, (start_time + (j * SECONDS_PER_MINUTE)) % 255); } // Advance another minute. rtc_set_time(utc_sec + (i * SECONDS_PER_MINUTE)); } } // --------------------------------------------------------------------------------------- // Return the index of the step averages slot that contains the given minute static uint16_t prv_step_avg_slot(int hour, int min) { int minutes = hour * MINUTES_PER_HOUR + min; return minutes / (MINUTES_PER_DAY / ACTIVITY_NUM_METRIC_AVERAGES); } // Used by the test_activity__step_averages() method to figure out what steps/min we should // feed in for the given 15-minute time slot int prv_expected_steps_per_min(int slot, int multiplier) { if (multiplier == 1) { // The slot % 50 was chosen so that the total # of steps per day does not exceeed 2^16 return ((slot % 50) + 1); } else if (multiplier == 2) { // The slot % 30 was chosen so that the total # of steps per day does not exceeed 2^16 return 2 * ((slot % 30) + 1); } else { cl_assert(false); return 0; } } // ------------------------------------------------------------------------------------ // Verify that the settings are what we expected from prv_save_known_settings() void prv_assert_known_settings(void) { struct tm time_tm = s_init_time_tm; time_t utc_sec = mktime(&time_tm); rtc_set_time(utc_sec); prv_activity_init_and_set_enabled(true); ASSERT_EQUAL_METRIC_HISTORY(ActivityMetricStepCount, ((const uint32_t [ACTIVITY_HISTORY_DAYS]){300, 200, 100, 0, 0, 0, 0})); ASSERT_EQUAL_METRIC_HISTORY(ActivityMetricSleepTotalSeconds, ((const uint32_t [ACTIVITY_HISTORY_DAYS]) {6 * SECONDS_PER_MINUTE, 4 * SECONDS_PER_MINUTE, 2 * SECONDS_PER_MINUTE, 0, 0, 0, 0})); ASSERT_EQUAL_METRIC_HISTORY(ActivityMetricSleepRestfulSeconds, ((const uint32_t [ACTIVITY_HISTORY_DAYS]) {3 * SECONDS_PER_MINUTE, 2 * SECONDS_PER_MINUTE, 1 * SECONDS_PER_MINUTE, 0, 0, 0, 0})); } // -------------------------------------------------------------------------------------- // Save the current settings file format with known data to the local file system so that it can // be checked in and used for migration tests. static void prv_save_known_settings_file(const char *filename) { // Let's include 3 days of history by start at s_init_time_tm - 3 days struct tm time_tm = s_init_time_tm; time_t utc_sec = mktime(&time_tm); utc_sec -= 2 * SECONDS_PER_DAY; rtc_set_time(utc_sec); prv_activity_init_and_set_enabled(true); activity_start_tracking(false /*test_mode*/); fake_system_task_callbacks_invoke_pending(); // Feed in 100 steps/min over 1 min, 1 minute of deep and 1 minute of light sleep prv_feed_cannned_accel_data(60, 100, ActivitySleepStateAwake); prv_feed_cannned_accel_data(60, 0, ActivitySleepStateRestfulSleep); prv_feed_cannned_accel_data(60, 0, ActivitySleepStateLightSleep); // Wait long enough for our recompute sleep logic to run. prv_feed_cannned_accel_data(SECONDS_PER_MINUTE * ACTIVITY_SESSION_UPDATE_MIN, 0, ActivitySleepStateAwake); // Advance to next day prv_feed_cannned_accel_data(SECONDS_PER_HOUR * 24, 0, ActivitySleepStateAwake); // Feed in 100 steps/min over 2 min, 2 minute of deep and 2 minute of light sleep prv_feed_cannned_accel_data(120, 100, ActivitySleepStateAwake); prv_feed_cannned_accel_data(120, 0, ActivitySleepStateRestfulSleep); prv_feed_cannned_accel_data(120, 0, ActivitySleepStateLightSleep); // Wait long enough for our recompute sleep logic to run. prv_feed_cannned_accel_data(SECONDS_PER_MINUTE * ACTIVITY_SESSION_UPDATE_MIN, 0, ActivitySleepStateAwake); // Advance to next day prv_feed_cannned_accel_data(SECONDS_PER_HOUR * 24, 0, ActivitySleepStateAwake); // Feed in 100 steps/min over 3 min, 3 minute of deep and 3 minute of light sleep prv_feed_cannned_accel_data(180, 100, ActivitySleepStateAwake); prv_feed_cannned_accel_data(180, 0, ActivitySleepStateRestfulSleep); prv_feed_cannned_accel_data(180, 0, ActivitySleepStateLightSleep); // Wait long enough for our recompute sleep logic to run. prv_feed_cannned_accel_data(SECONDS_PER_MINUTE * ACTIVITY_SESSION_UPDATE_MIN, 0, ActivitySleepStateAwake); // Make sure they are what we expected prv_assert_known_settings(); // Extract activity settings file from PFS and save to the local file system char out_path[strlen(CLAR_FIXTURE_PATH) + strlen(ACTIVITY_FIXTURE_PATH) + strlen(filename) + 3]; sprintf(out_path, "%s/%s/%s", CLAR_FIXTURE_PATH, ACTIVITY_FIXTURE_PATH, filename); // Open and read the settings file from PFS int fd = pfs_open(ACTIVITY_SETTINGS_FILE_NAME, OP_FLAG_READ, FILE_TYPE_STATIC, ACTIVITY_SETTINGS_FILE_LEN); cl_assert(fd >= S_SUCCESS); size_t size = pfs_get_file_size(fd); uint8_t *buf = malloc(size); cl_assert(buf != NULL); cl_assert(pfs_read(fd, buf, size) == size); pfs_close(fd); // Save it to the local file system FILE *file = fopen(out_path, "wb"); cl_assert(file != NULL); cl_assert_equal_i(fwrite(buf, size, 1, file), 1); fclose(file); free(buf); printf("\nSaved current settings file to %s", out_path); } // --------------------------------------------------------------------------------------- // Create the settings file in PFS from a file saved in the local file system static void prv_load_settings_file_onto_pfs(const char *filename, const char *pfs_name) { char in_path[strlen(CLAR_FIXTURE_PATH) + strlen(ACTIVITY_FIXTURE_PATH) + strlen(filename) + 3]; sprintf(in_path, "%s/%s/%s", CLAR_FIXTURE_PATH, ACTIVITY_FIXTURE_PATH, filename); // check that file exists and fits in buffer struct stat st; cl_assert(stat(in_path, &st) == 0); FILE *file = fopen(in_path, "r"); cl_assert(file); uint8_t buf[st.st_size]; // copy file to fake flash storage cl_assert(fread(buf, 1, st.st_size, file) > 0); pfs_remove(pfs_name); int fd = pfs_open(pfs_name, OP_FLAG_WRITE, FILE_TYPE_STATIC, st.st_size); cl_assert(fd >= 0); int bytes_written = pfs_write(fd, buf, st.st_size); cl_assert(st.st_size == bytes_written); pfs_close(fd); } // --------------------------------------------------------------------------------------- // Test that we correctly migrate older versions of activity settings files void test_activity__migrate_settings(void) { // Uncomment this call to prv_save_known_settings_file() in order to save the current version // of settings to the fixture directory. After doing this, you will need to git add it and modify // this migration test to read it in and verify its contents after migration. // prv_save_known_settings_file("activity_settings.v1"); // Load the v1 settings format. prv_load_settings_file_onto_pfs("activity_settings.v1", ACTIVITY_SETTINGS_FILE_NAME); // Make sure it got migrated correctly. prv_activity_init_and_set_enabled(true); prv_assert_known_settings(); } // ---------------------------------------------------------------------------- // fake_event callback used to look for sleep events generated by the health_events test static PebbleEvent s_captured_sleep_event = { }; static int s_num_captured_sleep_events = 0; static void prv_fake_sleep_event_cb(PebbleEvent *event) { if ((event->type == PEBBLE_HEALTH_SERVICE_EVENT) && (event->health_event.type == HealthEventSleepUpdate)) { s_captured_sleep_event = *event; s_num_captured_sleep_events++; } } // ---------------------------------------------------------------------------- // fake_event callback used to look for history update events generated by the health_events test static PebbleEvent s_captured_history_event = { }; static int s_num_captured_history_events = 0; static void prv_fake_history_event_cb(PebbleEvent *event) { if ((event->type == PEBBLE_HEALTH_SERVICE_EVENT) && (event->health_event.type == HealthEventSignificantUpdate)) { s_captured_history_event = *event; s_num_captured_history_events++; } } // --------------------------------------------------------------------------------------- // Test that we generate health events at the appropriate time void test_activity__health_events(void) { // Start activity tracking. This method assumes it can be called from any task, so we must // invoke system callbacks to handle its KernelBG callback. activity_start_tracking(false /*test_mode*/); fake_system_task_callbacks_invoke_pending(); // ----------------------------------- // Test that we receive step update events fake_event_reset_count(); // Feed in 100 steps/minute over 1 minute. We should get some step update events prv_feed_cannned_accel_data(1 * SECONDS_PER_MINUTE, 100, ActivitySleepStateAwake); uint32_t event_count = fake_event_get_count(); // Our fake algorithm generates a step update once a second cl_assert_equal_i(event_count, 1 * SECONDS_PER_MINUTE); PebbleEvent event = fake_event_get_last(); cl_assert_equal_i(event.type, PEBBLE_HEALTH_SERVICE_EVENT); cl_assert_equal_i(event.health_event.type, HealthEventMovementUpdate); // ----------------------------------- // Test that we receive sleep update events prv_reset_captured_dls_data(); // Falling asleep for 30 minutes prv_feed_cannned_accel_data(30 * SECONDS_PER_MINUTE, 5, ActivitySleepStateAwake); // Starting at 10:31pm: 1 Cycle of light (60 min), deep (50 min) fake_event_reset_count(); fake_event_set_callback(prv_fake_sleep_event_cb); s_captured_sleep_event = (PebbleEvent) { }; s_num_captured_sleep_events = 0; prv_feed_cannned_accel_data(60 * SECONDS_PER_MINUTE, 0, ActivitySleepStateLightSleep); prv_feed_cannned_accel_data(50 * SECONDS_PER_MINUTE, 0, ActivitySleepStateRestfulSleep); prv_feed_cannned_accel_data(15 * SECONDS_PER_MINUTE, 0, ActivitySleepStateAwake); prv_feed_cannned_accel_data(60 * SECONDS_PER_MINUTE, 0, ActivitySleepStateLightSleep); prv_feed_cannned_accel_data(50 * SECONDS_PER_MINUTE, 0, ActivitySleepStateRestfulSleep); // Wait long enough for our recompute sleep logic to run. prv_feed_cannned_accel_data(SECONDS_PER_MINUTE * ACTIVITY_SESSION_UPDATE_MIN, 60, ActivitySleepStateAwake); // See if we got the expected sleep events cl_assert(s_num_captured_sleep_events > 0); event = s_captured_sleep_event; cl_assert_equal_i(event.type, PEBBLE_HEALTH_SERVICE_EVENT); cl_assert_equal_i(event.health_event.type, HealthEventSleepUpdate); // ----------------------------------- // Test that we receive history update events fake_event_reset_count(); fake_event_set_callback(prv_fake_history_event_cb); s_captured_history_event = (PebbleEvent) { }; s_num_captured_history_events = 0; // Get the current day_id int32_t actual; activity_get_metric(ActivityMetricStepCount, 1, &actual); // Wait long enough for a midnight rollover. All tests start at 5pm, so if we wait // 7 hours, we should get a midnight rollover prv_feed_cannned_accel_data(7 * SECONDS_PER_HOUR, 0, ActivitySleepStateAwake); // See if we got the expected history events cl_assert_equal_i(s_num_captured_history_events, 1); event = s_captured_history_event; cl_assert_equal_i(event.type, PEBBLE_HEALTH_SERVICE_EVENT); cl_assert_equal_i(event.health_event.type, HealthEventSignificantUpdate); } // --------------------------------------------------------------------------------------- // Test derived sleep metrics after the watch goes through a timezone change. void test_activity__sleep_after_timezone_change(void) { int32_t value; // ---------------------------------------------------------------------------- // Let's start out in EST time when tracking starts. All of our tests start at 5pm UTC, which is // 12pm EST. Let's start out in this time zone then switch back to PST right before we fall // asleep. This replicates the conditions that resulted in PBL-24823 TimezoneInfo tz_info = { .tm_zone = "EST", .tm_gmtoff = -5 * SECONDS_PER_HOUR, }; time_util_update_timezone(&tz_info); // Start activity tracking. This method assumes it can be called from any task, so we must // invoke system callbacks to handle its KernelBG callback. activity_start_tracking(false /*test_mode*/); fake_system_task_callbacks_invoke_pending(); // Advance to 6pm EST prv_feed_cannned_accel_data(6 * SECONDS_PER_HOUR, 50, ActivitySleepStateAwake); // switch into PST (which would be 3pm) tz_info = (TimezoneInfo) { .tm_zone = "PST", .tm_gmtoff = -8 * SECONDS_PER_HOUR, }; time_util_update_timezone(&tz_info); // Walk some more until 11pm PST prv_feed_cannned_accel_data(8 * SECONDS_PER_HOUR, 50, ActivitySleepStateAwake); // Starting at 11pm: 2 Cycles of 3 hrs each light (165 min), awake (15 min) for (int i = 0; i < 2; i++) { prv_feed_cannned_accel_data(165 * SECONDS_PER_MINUTE, 0, ActivitySleepStateLightSleep); prv_feed_cannned_accel_data(15 * SECONDS_PER_MINUTE, 20, ActivitySleepStateAwake); } activity_get_metric(ActivityMetricSleepEnterAtSeconds, 1, &value); cl_assert_equal_i(value, 23 * SECONDS_PER_HOUR /* 11pm */); activity_get_metric(ActivityMetricSleepTotalSeconds, 1, &value); cl_assert_equal_i(value, 330 * SECONDS_PER_MINUTE); activity_get_metric(ActivityMetricSleepExitAtSeconds, 1, &value); cl_assert_equal_i(value, 4 * SECONDS_PER_HOUR + 45 * SECONDS_PER_MINUTE /* 4:45am */); // Assert that we got the same sleep sessions using the activity service as we do using // the health API prv_assert_equal_activity_and_health_sleep_sessions(2); // ---------------------------------------------------------------------------- // The previous test left us at 5am PST. Let's try going the other way and switch from PST to // EST right before we fall asleep // Advance to 11pm PST prv_feed_cannned_accel_data(18 * SECONDS_PER_HOUR, 50, ActivitySleepStateAwake); // It is now 11pm PST. Switch to EST, which would be 2am tz_info = (TimezoneInfo) { .tm_zone = "EST", .tm_gmtoff = -5 * SECONDS_PER_HOUR, }; time_util_update_timezone(&tz_info); // Starting at 2am EST: 2 Cycles of 3 hrs each light (165 min), awake (15 min) for (int i = 0; i < 2; i++) { prv_feed_cannned_accel_data(165 * SECONDS_PER_MINUTE, 0, ActivitySleepStateLightSleep); prv_feed_cannned_accel_data(15 * SECONDS_PER_MINUTE, 20, ActivitySleepStateAwake); } activity_get_metric(ActivityMetricSleepEnterAtSeconds, 1, &value); cl_assert_equal_i(value, 2 * SECONDS_PER_HOUR /* 2am */); activity_get_metric(ActivityMetricSleepTotalSeconds, 1, &value); cl_assert_equal_i(value, 330 * SECONDS_PER_MINUTE); activity_get_metric(ActivityMetricSleepExitAtSeconds, 1, &value); cl_assert_equal_i(value, 7 * SECONDS_PER_HOUR + 45 * SECONDS_PER_MINUTE /* 7:45am */); // Assert that we got the same sleep sessions using the activity service as we do using // the health API prv_assert_equal_activity_and_health_sleep_sessions(2); } // --------------------------------------------------------------------------------------- // Test that the health service correctly interpolates when asked for a metric over partial days void test_activity__health_service_interpolation(void) { // Let's start out in PST time when tracking starts. All of our tests start at 5pm UTC, which is // 9am PST. TimezoneInfo tz_info = { .tm_zone = "PST", .tm_gmtoff = -8 * SECONDS_PER_HOUR, }; time_util_update_timezone(&tz_info); // Start activity tracking. This method assumes it can be called from any task, so we must // invoke system callbacks to handle its KernelBG callback. activity_start_tracking(false /*test_mode*/); fake_system_task_callbacks_invoke_pending(); // Feed in 100 steps/min over 10 minutes, for a total of 1000 steps for today prv_feed_cannned_accel_data(10 * SECONDS_PER_MINUTE, 100, ActivitySleepStateAwake); // Wait long enough until we start the next day (15 hours) prv_feed_cannned_accel_data(SECONDS_PER_HOUR * 15, 0, ActivitySleepStateAwake); ASSERT_EQUAL_METRIC_HISTORY(ActivityMetricStepCount, ((const uint32_t [ACTIVITY_HISTORY_DAYS]){0, 1000, 0, 0, 0, 0, 0})); // Feed in 100 steps/min over 20 minutes, for a total of 2000 steps for today prv_feed_cannned_accel_data(20 * SECONDS_PER_MINUTE, 100, ActivitySleepStateAwake); ASSERT_EQUAL_METRIC_HISTORY(ActivityMetricStepCount, ((const uint32_t [ACTIVITY_HISTORY_DAYS]){2000, 1000, 0, 0, 0, 0, 0})); // If we ask for the sum of the latter half of yesterday, we should get 500 HealthValue steps = health_service_sum( HealthMetricStepCount, time_start_of_today() - (12 * SECONDS_PER_HOUR), time_start_of_today()); cl_assert_equal_i(steps, 500); // If we ask for the sum from latter half of yesterday till now, we should get 2500 steps = health_service_sum( HealthMetricStepCount, time_start_of_today() - (12 * SECONDS_PER_HOUR), rtc_get_time()); cl_assert_equal_i(steps, 2500); // If we ask for the sum from latter half of yesterday till half of today, we should get 1500 time_t elapsed_today = rtc_get_time() - time_start_of_today(); steps = health_service_sum( HealthMetricStepCount, time_start_of_today() - (12 * SECONDS_PER_HOUR), time_start_of_today() + (elapsed_today / 2)); cl_assert_equal_i(steps, 1500); } // --------------------------------------------------------------------------------------- // Test distance using various speeds and user dimensions typedef struct { int height_in; int gender; int steps; float seconds; int exp_distance_m; // expected distance } DistanceTestParams; void test_activity__distance(void) { int32_t value; // The health_service calls expect to be in the app or worker task stub_pebble_tasks_set_current(PebbleTask_App); DistanceTestParams tests[] = { {69, ActivityGenderMale, 19177, 6360, 23352}, {69, ActivityGenderMale, 10351, 3600, 11764}, {69, ActivityGenderMale, 3003, 1560, 2398}, {69, ActivityGenderMale, 3423, 2100, 2881}, {65, ActivityGenderFemale, 6940, 3120, 8047}, {65, ActivityGenderFemale, 4577, 2460, 3508}, {63, ActivityGenderFemale, 4738, 1860, 4989}, {63, ActivityGenderFemale, 4799, 1860, 5134}, {63, ActivityGenderFemale, 2896, 1500, 2334}, {71, ActivityGenderMale, 7529, 4020, 5568}, {67, ActivityGenderMale, 6592, 3960, 6067}, {73, ActivityGenderMale, 4467, 1740, 5118}, {73, ActivityGenderMale, 4080, 1800, 5102}, {73, ActivityGenderMale, 2890, 1680, 2382}, {73, ActivityGenderMale, 4143, 2400, 3251}, {64, ActivityGenderMale, 4373, 1823, 4168}, {64, ActivityGenderMale, 642, 384, 483}, {64, ActivityGenderMale, 4455, 1819, 4072}, {64, ActivityGenderMale, 2008, 1229, 1448}, {64, ActivityGenderMale, 2217, 1302, 1674}, {64, ActivityGenderMale, 4568, 1820, 4152}, }; // Init the time struct tm time_tm = s_init_time_tm; time_tm.tm_mday += 1; time_t utc_sec = mktime(&time_tm); rtc_set_time(utc_sec); fake_system_task_callbacks_invoke_pending(); int act_distance[ARRAY_LENGTH(tests)]; const int k_elapsed_sec = 2 * SECONDS_PER_MINUTE; // Evaluate each test case for (int i = 0; i < ARRAY_LENGTH(tests); i++) { DistanceTestParams *params = &tests[i]; // Advance to new day to reset the distance utc_sec += SECONDS_PER_DAY; rtc_set_time(utc_sec); prv_activity_init_and_set_enabled(true); // Set the user's dimensions activity_prefs_set_height_mm((int)(params->height_in * 25.4)); activity_prefs_set_gender(params->gender); // Start activity tracking. This method assumes it can be called from any task, so we must // invoke system callbacks to handle its KernelBG callback. activity_start_tracking(false /*test_mode*/); fake_system_task_callbacks_invoke_pending(); // Feed in the test cadence for 2 minutes. Compute the expected distance in 2 minutes // as well int steps_per_minute = (int)((float)params->steps / params->seconds * SECONDS_PER_MINUTE); int exp_distance_m = ROUND(params->exp_distance_m * k_elapsed_sec, params->seconds); // Feed in the test cadence for the given amount of time prv_feed_cannned_accel_data(k_elapsed_sec, steps_per_minute, ActivitySleepStateAwake); activity_get_metric(ActivityMetricStepCount, 1, &value); cl_assert_near(value, ROUND(steps_per_minute * k_elapsed_sec, SECONDS_PER_MINUTE), 5); activity_get_metric(ActivityMetricDistanceMeters, 1, &value); act_distance[i] = value; float err = abs(exp_distance_m - value); float pct_err = err * 100.0 / exp_distance_m; printf("\nTest %d: height:%d, steps:%d, seconds:%.1f, exp_distance:%d, exp_distance_2min:%d, " "act_distance_2min:%"PRIu32", pct_err: %.2f%% \n", i, params->height_in, params->steps, params->seconds, params->exp_distance_m, exp_distance_m, value, pct_err); // Check the percent error cl_assert(pct_err < 25); cl_assert_equal_i(value, health_service_sum_today(HealthMetricWalkedDistanceMeters)); activity_stop_tracking(); fake_system_task_callbacks_invoke_pending(); } // Print summary of results printf("\ntest height steps seconds cadence exp_dist exp_dist_2min act_dist_2min %%err"); printf("\n------------------------------------------------------------------------------------"); float pct_err_sum = 0; for (int i = 0; i < ARRAY_LENGTH(tests); i++) { DistanceTestParams *params = &tests[i]; int steps_per_minute = (int)((float)params->steps / params->seconds * SECONDS_PER_MINUTE); int exp_distance_m = ROUND(params->exp_distance_m * k_elapsed_sec, params->seconds); float err = act_distance[i] - exp_distance_m; float pct_err = err * 100.0 / exp_distance_m; printf("\n%4d %5d %4d %7.2f %7d %7d %13d %13d %+.2f", i, params->height_in, params->steps, params->seconds, steps_per_minute, params->exp_distance_m, exp_distance_m, act_distance[i], pct_err); pct_err_sum += pct_err >= 0 ? pct_err : -pct_err; } printf("\n--------------------------"); float avg_pct_err = pct_err_sum / ARRAY_LENGTH(tests); printf("\nAVERAGE PCT ERROR: %.2f", avg_pct_err); // Check the overall percent error cl_assert(avg_pct_err < 10); } // -------------------------------------------------------------------------------------------- // Advance through time simulating the heart rate manager calls static int s_num_hrm_callbacks; static void prv_advance_time_hr(uint32_t num_sec, uint8_t bpm, HRMQuality quality, bool force_continuous) { // Call the minute handler, which computes the minute stats and saves them to data logging // as well as the sleep PFS file. for (int i = 0; i < num_sec; i++) { fake_rtc_set_ticks(rtc_get_ticks() + configTICK_RATE_HZ); rtc_set_time(rtc_get_time() + 1); if ((s_hrm_manager_update_interval == 1) || force_continuous) { PebbleHRMEvent hrm_event = { .event_type = HRMEvent_BPM, .bpm.bpm = bpm, .bpm.quality = quality, }; prv_hrm_subscription_cb(&hrm_event, NULL); s_num_hrm_callbacks++; } if ((rtc_get_time() % SECONDS_PER_MINUTE) == 0) { prv_minute_system_task_cb(NULL); } } } // --------------------------------------------------------------------------------------- // Test that we subscribe to the HR events at the expected times void test_activity__hrm_sampling_period(void) { // Start activity tracking. This method assumes it can be called from any task, so we must // invoke system callbacks to handle its KernelBG callback. activity_start_tracking(false /*test_mode*/); fake_system_task_callbacks_invoke_pending(); s_test_alg_state.orientation = 0x11; // Not flat prv_advance_time_hr(ACTIVITY_DEFAULT_HR_PERIOD_SEC, 100 /*bpm*/, HRMQuality_Good, false /*force_continuous*/); // Should be 1 second sampling when we start up cl_assert_equal_i(s_hrm_manager_update_interval, 1); // The last update time should be 0 int32_t last_update_utc; activity_get_metric(ActivityMetricHeartRateRawUpdatedTimeUTC, 1, &last_update_utc); cl_assert_equal_i(last_update_utc, 0); // Simulate callbacks one second away from turning down the sampling rate // Use Acceptable because of the short circuiting in `prv_heart_rate_subscription_update` prv_advance_time_hr(ACTIVITY_DEFAULT_HR_ON_TIME_SEC - 1, 100 /*bpm*/, HRMQuality_Acceptable, false /*force_continuous*/); // The last update time should be within a second activity_get_metric(ActivityMetricHeartRateRawUpdatedTimeUTC, 1, &last_update_utc); cl_assert(last_update_utc >= rtc_get_time() - 1); cl_assert(last_update_utc <= rtc_get_time()); // Should still be sampling every 1 second cl_assert_equal_i(s_hrm_manager_update_interval, 1); // Tick one more second, should trigger slow sampling prv_advance_time_hr(1, 100 /*bpm*/, HRMQuality_Good, false /*force_continuous*/); // Should be back to no sampling by now (very large sampling period) cl_assert(s_hrm_manager_update_interval > SECONDS_PER_HOUR); // Advance to our next sampling period, but the watch is flat so we shouldn't start sampling s_test_alg_state.orientation = 0x00; // Flat prv_advance_time_hr(ACTIVITY_DEFAULT_HR_PERIOD_SEC, 100 /*bpm*/, HRMQuality_Good, false /*force_continuous*/); cl_assert(s_hrm_manager_update_interval > SECONDS_PER_HOUR); // Advance to our next sampling period, the watch is no longer flat so we should be sampling s_test_alg_state.orientation = 0x22; // Not flat prv_advance_time_hr(ACTIVITY_DEFAULT_HR_PERIOD_SEC, 100 /*bpm*/, HRMQuality_Good, false /*force_continuous*/); cl_assert_equal_i(s_hrm_manager_update_interval, 1); } // --------------------------------------------------------------------------------------- // Test that average heart rate is reported correctly void test_activity__hrm_median(void) { int32_t median, total_weight; // Start activity tracking. This method assumes it can be called from any task, so we must // invoke system callbacks to handle its KernelBG callback. activity_start_tracking(false /*test_mode*/); fake_system_task_callbacks_invoke_pending(); // Reset the median activity_metrics_prv_reset_hr_stats(); // Our previous median should be since we have no data int32_t last_median; int32_t last_update_utc; activity_get_metric(ActivityMetricHeartRateFilteredBPM, 1, &last_median); activity_get_metric(ActivityMetricHeartRateFilteredUpdatedTimeUTC, 1, &last_update_utc); cl_assert_equal_i(last_median, 0); cl_assert_equal_i(last_update_utc, 0); // Simulate some HRM callbacks with no heart rate, should get 0 median prv_advance_time_hr(10 /*sec*/, 0 /*hr*/, HRMQuality_Good, true /*force_continuous*/); activity_metrics_prv_get_median_hr_bpm(&median, &total_weight); cl_assert_equal_i(median, 0); cl_assert_equal_i(total_weight, 0); // Our previous median should be since we have no data (valid data) activity_get_metric(ActivityMetricHeartRateFilteredBPM, 1, &last_median); activity_get_metric(ActivityMetricHeartRateFilteredUpdatedTimeUTC, 1, &last_update_utc); cl_assert_equal_i(last_median, 0); cl_assert_equal_i(last_update_utc, 0); // Simulate some HRM callbacks with non-zero heart rate prv_advance_time_hr(3 /*sec*/, 50 /*hr*/, HRMQuality_Good, true /*force_continuous*/); prv_advance_time_hr(3 /*sec*/, 100 /*hr*/, HRMQuality_Good, true /*force_continuous*/); prv_advance_time_hr(1 /*sec*/, 51 /*hr*/, HRMQuality_Good, true /*force_continuous*/); prv_advance_time_hr(8 /*sec*/, 120 /*hr*/, HRMQuality_Worst, true /*force_continuous*/); prv_minute_system_task_cb(NULL); activity_metrics_prv_get_median_hr_bpm(&median, &total_weight); cl_assert_equal_i(median, 51); // The last median should be stored and accessable via the LastStableBPM metric activity_get_metric(ActivityMetricHeartRateFilteredBPM, 1, &last_median); activity_get_metric(ActivityMetricHeartRateFilteredUpdatedTimeUTC, 1, &last_update_utc); cl_assert_equal_i(last_median, 51); cl_assert(last_update_utc >= rtc_get_time() - 1); cl_assert(last_update_utc <= rtc_get_time()); // Reset the stats, the median should be 0 activity_metrics_prv_reset_hr_stats(); activity_metrics_prv_get_median_hr_bpm(&median, &total_weight); cl_assert_equal_i(median, 0); // But the last stable BPM shouldn't get wiped activity_get_metric(ActivityMetricHeartRateFilteredBPM, 1, &last_median); activity_get_metric(ActivityMetricHeartRateFilteredUpdatedTimeUTC, 1, &last_update_utc); cl_assert_equal_i(last_median, 51); cl_assert(last_update_utc >= rtc_get_time() - 1); cl_assert(last_update_utc <= rtc_get_time()); } static uint32_t s_num_hr_events; static PebbleHealthEvent s_last_hr_event; static void prv_fake_hr_event_handler(PebbleEvent *e) { s_num_hr_events++; s_last_hr_event = e->health_event; } // --------------------------------------------------------------------------------------- // Test that some HRM events aren't passed on from activity service void test_activity__hrm_ignore(void) { int32_t median, total_weight; s_num_hr_events = 0; // Start activity tracking. This method assumes it can be called from any task, so we must // invoke system callbacks to handle its KernelBG callback. activity_start_tracking(false /*test_mode*/); fake_system_task_callbacks_invoke_pending(); fake_event_reset_count(); fake_event_set_callback(prv_fake_hr_event_handler); // Should not fire off an event. Bad HR reading prv_advance_time_hr(1 /*sec*/, 0 /*hr*/, HRMQuality_Good, true /*force_continuous*/); cl_assert_equal_i(s_num_hr_events, 0); // Should fire off an event. Good HR and Good quality prv_advance_time_hr(1 /*sec*/, 120 /*hr*/, HRMQuality_Good, true /*force_continuous*/); cl_assert_equal_i(s_num_hr_events, 1); // Should fire off an event. OffWrist, tell clients prv_advance_time_hr(1 /*sec*/, 120 /*hr*/, HRMQuality_OffWrist, true /*force_continuous*/); cl_assert_equal_i(s_num_hr_events, 2); cl_assert_equal_i(s_last_hr_event.data.heart_rate_update.current_bpm, 0); cl_assert_equal_i(s_last_hr_event.data.heart_rate_update.quality, HRMQuality_OffWrist); // Should fire off an event. OffWrist, tell clients prv_advance_time_hr(1 /*sec*/, 0 /*hr*/, HRMQuality_OffWrist, true /*force_continuous*/); cl_assert_equal_i(s_num_hr_events, 3); cl_assert_equal_i(s_last_hr_event.data.heart_rate_update.current_bpm, 0); cl_assert_equal_i(s_last_hr_event.data.heart_rate_update.quality, HRMQuality_OffWrist); // Should fire off an event. Good HR and Good Quality prv_advance_time_hr(1 /*sec*/, 120 /*hr*/, HRMQuality_Excellent, true /*force_continuous*/); cl_assert_equal_i(s_num_hr_events, 4); cl_assert_equal_i(s_last_hr_event.data.heart_rate_update.current_bpm, 120); cl_assert_equal_i(s_last_hr_event.data.heart_rate_update.quality, HRMQuality_Excellent); // Should not fire off an event. Bad HR reading prv_advance_time_hr(1 /*sec*/, 20 /*hr*/, HRMQuality_Excellent, true /*force_continuous*/); cl_assert_equal_i(s_num_hr_events, 4); } // --------------------------------------------------------------------------------------- // Today is Thursday void test_activity__prv_set_metric(void) { activity_start_tracking(false /*test_mode*/); fake_system_task_callbacks_invoke_pending(); int32_t metric_values[ACTIVITY_HISTORY_DAYS] = {0}; // Set today's value activity_metrics_prv_set_metric(ActivityMetricStepCount, Thursday, 1111); // Set yesterday's value activity_metrics_prv_set_metric(ActivityMetricStepCount, Wednesday, 2222); // Set last friday value activity_metrics_prv_set_metric(ActivityMetricStepCount, Friday, 3333); activity_get_metric(ActivityMetricStepCount, 7, metric_values); cl_assert_equal_i(metric_values[0], 1111); cl_assert_equal_i(metric_values[1], 2222); cl_assert_equal_i(metric_values[2], 0000); cl_assert_equal_i(metric_values[3], 0000); cl_assert_equal_i(metric_values[4], 0000); cl_assert_equal_i(metric_values[5], 0000); cl_assert_equal_i(metric_values[6], 3333); // Set the current value to something larger activity_metrics_prv_set_metric(ActivityMetricStepCount, Thursday, 4444); activity_get_metric(ActivityMetricStepCount, 1, metric_values); cl_assert_equal_i(metric_values[0], 4444); // Set the current value to something smaller (will be ignored) activity_metrics_prv_set_metric(ActivityMetricStepCount, Thursday, 1); activity_get_metric(ActivityMetricStepCount, 1, metric_values); cl_assert_equal_i(metric_values[0], 4444); // Verify some other metrics work activity_metrics_prv_set_metric(ActivityMetricActiveSeconds, Thursday, 60); activity_get_metric(ActivityMetricActiveSeconds, 1, metric_values); cl_assert_equal_i(metric_values[0], 60); activity_metrics_prv_set_metric(ActivityMetricDistanceMeters, Thursday, 66); activity_metrics_prv_set_metric(ActivityMetricDistanceMeters, Wednesday, 22); activity_get_metric(ActivityMetricDistanceMeters, 2, metric_values); cl_assert_equal_i(metric_values[0], 66); cl_assert_equal_i(metric_values[1], 22); cl_assert_equal_i(activity_metrics_prv_get_distance_mm(), 66 * MM_PER_METER); activity_metrics_prv_set_metric(ActivityMetricActiveKCalories, Thursday, 22); activity_metrics_prv_set_metric(ActivityMetricActiveKCalories, Wednesday, 33); activity_get_metric(ActivityMetricActiveKCalories, 2, metric_values); cl_assert_equal_i(metric_values[0], 22); cl_assert_equal_i(metric_values[1], 33); cl_assert_equal_i(activity_metrics_prv_get_active_calories(), 22 * ACTIVITY_CALORIES_PER_KCAL); activity_metrics_prv_set_metric(ActivityMetricRestingKCalories, Thursday, 2000); activity_metrics_prv_set_metric(ActivityMetricRestingKCalories, Wednesday, 44); activity_get_metric(ActivityMetricRestingKCalories, 2, metric_values); cl_assert_equal_i(metric_values[0], 2000); cl_assert_equal_i(metric_values[1], 44); cl_assert_equal_i(activity_metrics_prv_get_resting_calories(), 2000 * ACTIVITY_CALORIES_PER_KCAL); activity_metrics_prv_set_metric(ActivityMetricSleepTotalSeconds, Thursday, 60); activity_get_metric(ActivityMetricSleepTotalSeconds, 1, metric_values); cl_assert_equal_i(metric_values[0], 60); activity_metrics_prv_set_metric(ActivityMetricSleepRestfulSeconds, Wednesday, 60); activity_get_metric(ActivityMetricSleepRestfulSeconds, 2, metric_values); cl_assert_equal_i(metric_values[1], 60); activity_metrics_prv_set_metric(ActivityMetricSleepEnterAtSeconds, Thursday, 60); activity_get_metric(ActivityMetricSleepEnterAtSeconds, 1, metric_values); cl_assert_equal_i(metric_values[0], 60); activity_metrics_prv_set_metric(ActivityMetricSleepExitAtSeconds, Wednesday, 60); activity_get_metric(ActivityMetricSleepExitAtSeconds, 2, metric_values); cl_assert_equal_i(metric_values[1], 60); activity_stop_tracking(); fake_system_task_callbacks_invoke_pending(); activity_metrics_prv_set_metric(ActivityMetricStepCount, Thursday, 5555); activity_get_metric(ActivityMetricStepCount, 1, metric_values); cl_assert_equal_i(metric_values[0], 4444); } // Test that we report the that a run session is ongoing. void test_activity__activity_sessions_run_ongoing_then_end(void) { // Start activity tracking. This method assumes it can be called from any task, so we must // invoke system callbacks to handle its KernelBG callback. activity_start_tracking(false /*test_mode*/); fake_system_task_callbacks_invoke_pending(); // No sessions active, ensure that asking if a run is ongoing returns false cl_assert_equal_b(false, activity_sessions_is_session_type_ongoing(ActivitySessionType_Run)); cl_assert_equal_i(0, health_service_peek_current_activities()); // Start on known boundary struct tm start_tm = { // Jan 1, 2015, 5am .tm_hour = 5, .tm_mday = 1, .tm_mon = 0, .tm_year = 115 }; time_t utc_sec = mktime(&start_tm); rtc_set_time(utc_sec); // Add a run session const time_t time_elapsed = (20 * SECONDS_PER_MINUTE); ActivitySession run_activity = { .start_utc = utc_sec - time_elapsed, .length_min = time_elapsed, .type = ActivitySessionType_Run, .ongoing = true, }; activity_sessions_prv_add_activity_session(&run_activity); // Run session active, ensure that asking if a run is ongoing returns true cl_assert_equal_b(true, activity_sessions_is_session_type_ongoing(ActivitySessionType_Run)); cl_assert_equal_i(HealthActivityRun, health_service_peek_current_activities()); // Finish the run session utc_sec += (10 * SECONDS_PER_MINUTE); rtc_set_time(utc_sec); run_activity.ongoing = false; // Update session activity_sessions_prv_add_activity_session(&run_activity); // Run session ended, ensure that asking if a run is ongoing returns false cl_assert_equal_b(false, activity_sessions_is_session_type_ongoing(ActivitySessionType_Run)); cl_assert_equal_i(0, health_service_peek_current_activities()); } // --------------------------------------------------------------------------------------- // Test that we report the that a Sleep session is ongoing. void test_activity__activity_sessions_sleep_ongoing_then_delete(void) { // Start activity tracking. This method assumes it can be called from any task, so we must // invoke system callbacks to handle its KernelBG callback. activity_start_tracking(false /*test_mode*/); fake_system_task_callbacks_invoke_pending(); // No sessions active, ensure that asking if a sleep session is ongoing returns false cl_assert_equal_b(false, activity_sessions_is_session_type_ongoing(ActivitySessionType_Sleep)); cl_assert_equal_i(0, health_service_peek_current_activities()); // Start on known boundary struct tm start_tm = { // Jan 1, 2015, 5am .tm_hour = 5, .tm_mday = 1, .tm_mon = 0, .tm_year = 115 }; time_t utc_sec = mktime(&start_tm); rtc_set_time(utc_sec); // Add a Sleep session const time_t time_elapsed = (120 * SECONDS_PER_MINUTE); ActivitySession sleep_session = { .start_utc = utc_sec - time_elapsed, .length_min = time_elapsed, .type = ActivitySessionType_Sleep, .ongoing = true, }; activity_sessions_prv_add_activity_session(&sleep_session); // Flip the switch to say we are in light sleep. activity_private_state()->sleep_data.cur_state = ActivitySleepStateLightSleep; // Sleep session active, ensure that asking if a Sleep is ongoing returns true cl_assert_equal_b(true, activity_sessions_is_session_type_ongoing(ActivitySessionType_Sleep)); cl_assert_equal_i(HealthActivitySleep, health_service_peek_current_activities()); // Delete session activity_sessions_prv_delete_activity_session(&sleep_session); // Flip the switch to say we are in an awake state. activity_private_state()->sleep_data.cur_state = ActivitySleepStateAwake; // Sleep session ended, ensure that asking if a Sleep is ongoing returns false cl_assert_equal_b(false, activity_sessions_is_session_type_ongoing(ActivitySessionType_Sleep)); cl_assert_equal_i(0, health_service_peek_current_activities()); } // --------------------------------------------------------------------------------------- // Test that we report the that multiple sessions are ongoing. void test_activity__activity_sessions_ongoing_multiple(void) { // Start activity tracking. This method assumes it can be called from any task, so we must // invoke system callbacks to handle its KernelBG callback. activity_start_tracking(false /*test_mode*/); fake_system_task_callbacks_invoke_pending(); // No sessions active, ensure that asking for run,walk,sleep returns false cl_assert_equal_b(false, activity_sessions_is_session_type_ongoing(ActivitySessionType_Run)); cl_assert_equal_b(false, activity_sessions_is_session_type_ongoing(ActivitySessionType_Walk)); cl_assert_equal_b(false, activity_sessions_is_session_type_ongoing(ActivitySessionType_Sleep)); cl_assert_equal_i(0, health_service_peek_current_activities()); // Start on known boundary struct tm start_tm = { // Jan 1, 2015, 5am .tm_hour = 5, .tm_mday = 1, .tm_mon = 0, .tm_year = 115 }; time_t utc_sec = mktime(&start_tm); rtc_set_time(utc_sec); const time_t time_elapsed = (20 * SECONDS_PER_MINUTE); // Add a run session ActivitySession run_activity = { .start_utc = utc_sec - time_elapsed, .length_min = time_elapsed, .type = ActivitySessionType_Run, .ongoing = true, }; activity_sessions_prv_add_activity_session(&run_activity); // Add a walk session ActivitySession walk_activity = { .start_utc = utc_sec - time_elapsed, .length_min = time_elapsed, .type = ActivitySessionType_Walk, .ongoing = true, }; activity_sessions_prv_add_activity_session(&walk_activity); // Add a sleep session ActivitySession sleep_activity = { .start_utc = utc_sec - time_elapsed, .length_min = time_elapsed, .type = ActivitySessionType_Sleep, .ongoing = true, }; activity_sessions_prv_add_activity_session(&sleep_activity); // Flip the switch to say we are in light sleep. activity_private_state()->sleep_data.cur_state = ActivitySleepStateLightSleep; // Run,Walk,Sleep sessions active, ensure that asking if they are ongoing, it returns true cl_assert_equal_b(true, activity_sessions_is_session_type_ongoing(ActivitySessionType_Run)); cl_assert_equal_b(true, activity_sessions_is_session_type_ongoing(ActivitySessionType_Walk)); cl_assert_equal_b(true, activity_sessions_is_session_type_ongoing(ActivitySessionType_Sleep)); cl_assert_equal_i(HealthActivityRun | HealthActivityWalk | HealthActivitySleep , health_service_peek_current_activities()); } static void prv_set_median_hr_for_minutes(int bpm, int num_minutes) { const int num_samples = 15; activity_private_state()->hr.num_samples = num_samples; memset(activity_private_state()->hr.samples, bpm, num_samples); memset(activity_private_state()->hr.weights, 100, num_samples); for (int i = 0; i < num_minutes; i++) { prv_minute_system_task_cb(NULL); } } static bool prv_is_hr_elevated(void) { return activity_private_state()->hr.metrics.is_hr_elevated; } void test_activity__update_time_in_hr_zones(void) { int32_t zone1_minutes, zone2_minutes, zone3_minutes; activity_start_tracking(false /*test_mode*/); fake_system_task_callbacks_invoke_pending(); activity_metrics_prv_reset_hr_stats(); cl_assert_equal_b(prv_is_hr_elevated(), false); activity_get_metric(ActivityMetricHeartRateZone1Minutes, 1, &zone1_minutes); cl_assert_equal_i(zone1_minutes, 0); activity_get_metric(ActivityMetricHeartRateZone2Minutes, 1, &zone2_minutes); cl_assert_equal_i(zone2_minutes, 0); activity_get_metric(ActivityMetricHeartRateZone3Minutes, 1, &zone3_minutes); cl_assert_equal_i(zone3_minutes, 0); // Add some "regular" heart rates. This shouldn't affect our zone counts prv_set_median_hr_for_minutes(70 /* BPM */, 3 /* minutes */); cl_assert_equal_b(prv_is_hr_elevated(), false); activity_get_metric(ActivityMetricHeartRateZone1Minutes, 1, &zone1_minutes); cl_assert_equal_i(zone1_minutes, 0); activity_get_metric(ActivityMetricHeartRateZone2Minutes, 1, &zone2_minutes); cl_assert_equal_i(zone2_minutes, 0); activity_get_metric(ActivityMetricHeartRateZone3Minutes, 1, &zone3_minutes); cl_assert_equal_i(zone3_minutes, 0); // Add some "very elevated" heart rates. // The zone should wait 1 minute, move up 1 zone per minute, stop at the top prv_set_median_hr_for_minutes(185 /* BPM */, 5 /* minutes */); cl_assert_equal_b(prv_is_hr_elevated(), true); activity_get_metric(ActivityMetricHeartRateZone1Minutes, 1, &zone1_minutes); cl_assert_equal_i(zone1_minutes, 1); activity_get_metric(ActivityMetricHeartRateZone2Minutes, 1, &zone2_minutes); cl_assert_equal_i(zone2_minutes, 1); activity_get_metric(ActivityMetricHeartRateZone3Minutes, 1, &zone3_minutes); cl_assert_equal_i(zone3_minutes, 2); // Add some "regular" heart rates. // The zone should move down 1 zone per minute prv_set_median_hr_for_minutes(70 /* BPM */, 4 /* minutes */); cl_assert_equal_b(prv_is_hr_elevated(), false); activity_get_metric(ActivityMetricHeartRateZone1Minutes, 1, &zone1_minutes); cl_assert_equal_i(zone1_minutes, 2); activity_get_metric(ActivityMetricHeartRateZone2Minutes, 1, &zone2_minutes); cl_assert_equal_i(zone2_minutes, 2); activity_get_metric(ActivityMetricHeartRateZone3Minutes, 1, &zone3_minutes); cl_assert_equal_i(zone3_minutes, 2); // Add some more "regular" heart rates. // This shouldn't affect our zone counts prv_set_median_hr_for_minutes(70 /* BPM */, 3 /* minutes */); cl_assert_equal_b(prv_is_hr_elevated(), false); activity_get_metric(ActivityMetricHeartRateZone1Minutes, 1, &zone1_minutes); cl_assert_equal_i(zone1_minutes, 2); activity_get_metric(ActivityMetricHeartRateZone2Minutes, 1, &zone2_minutes); cl_assert_equal_i(zone2_minutes, 2); activity_get_metric(ActivityMetricHeartRateZone3Minutes, 1, &zone3_minutes); cl_assert_equal_i(zone3_minutes, 2); // Add a "blip" which shouldn't affect our zone counts. // This shouldn't affect our zone counts prv_set_median_hr_for_minutes(180 /* BPM */, 1 /* minutes */); cl_assert_equal_b(prv_is_hr_elevated(), true); prv_set_median_hr_for_minutes(70 /* BPM */, 1 /* minutes */); cl_assert_equal_b(prv_is_hr_elevated(), false); activity_get_metric(ActivityMetricHeartRateZone1Minutes, 1, &zone1_minutes); cl_assert_equal_i(zone1_minutes, 2); activity_get_metric(ActivityMetricHeartRateZone2Minutes, 1, &zone2_minutes); cl_assert_equal_i(zone2_minutes, 2); activity_get_metric(ActivityMetricHeartRateZone3Minutes, 1, &zone3_minutes); cl_assert_equal_i(zone3_minutes, 2); // Ad some "Semi-active" heart rates prv_set_median_hr_for_minutes(130 /* BPM */, 3 /* minutes */); cl_assert_equal_b(prv_is_hr_elevated(), true); activity_get_metric(ActivityMetricHeartRateZone1Minutes, 1, &zone1_minutes); cl_assert_equal_i(zone1_minutes, 4); activity_get_metric(ActivityMetricHeartRateZone2Minutes, 1, &zone2_minutes); cl_assert_equal_i(zone2_minutes, 2); activity_get_metric(ActivityMetricHeartRateZone3Minutes, 1, &zone3_minutes); cl_assert_equal_i(zone3_minutes, 2); // Advance to a new day. The HR zone stats should get reset time_t utc_sec = rtc_get_time(); utc_sec += SECONDS_PER_DAY; rtc_set_time(utc_sec); prv_minute_system_task_cb(NULL); cl_assert_equal_b(prv_is_hr_elevated(), true); // stays elevated activity_get_metric(ActivityMetricHeartRateZone1Minutes, 1, &zone1_minutes); cl_assert_equal_i(zone1_minutes, 0); activity_get_metric(ActivityMetricHeartRateZone2Minutes, 1, &zone2_minutes); cl_assert_equal_i(zone2_minutes, 0); activity_get_metric(ActivityMetricHeartRateZone3Minutes, 1, &zone3_minutes); cl_assert_equal_i(zone3_minutes, 0); } // --------------------------------------------------------------------------------------- // Test that we can add / delete an activity session void test_activity__activity_sessions_add_delete_sessions(void) { ActivitySession empty_session = {}; ActivitySession walk_activity = { .start_utc = 1, .length_min = 5, .type = ActivitySessionType_Walk, .ongoing = true, }; // Add then delete activity_sessions_prv_add_activity_session(&walk_activity); cl_assert_equal_i(activity_private_state()->activity_sessions_count, 1); cl_assert_equal_m(&activity_private_state()->activity_sessions[0], &walk_activity, sizeof(ActivitySession)); activity_sessions_prv_delete_activity_session(&walk_activity); cl_assert_equal_i(activity_private_state()->activity_sessions_count, 0); cl_assert_equal_m(&activity_private_state()->activity_sessions[0], &empty_session, sizeof(ActivitySession)); // Add lots of sessions then delete from the front for (int i = 0; i < ACTIVITY_MAX_ACTIVITY_SESSIONS_COUNT; i++) { ActivitySession activity = walk_activity; activity.start_utc = i; activity_sessions_prv_add_activity_session(&activity); cl_assert_equal_i(activity_private_state()->activity_sessions_count, i + 1); cl_assert_equal_m(&activity_private_state()->activity_sessions[i], &activity, sizeof(ActivitySession)); } for (int i = 0; i < ACTIVITY_MAX_ACTIVITY_SESSIONS_COUNT; i++) { ActivitySession activity = walk_activity; activity.start_utc = i; ActivitySession next_activity = activity; next_activity.start_utc = i + 1; activity_sessions_prv_delete_activity_session(&activity); cl_assert_equal_i(activity_private_state()->activity_sessions_count, ACTIVITY_MAX_ACTIVITY_SESSIONS_COUNT - 1 - i); if (activity_private_state()->activity_sessions_count) { cl_assert_equal_m(&activity_private_state()->activity_sessions[0], &next_activity, sizeof(ActivitySession)); } } cl_assert_equal_m(&activity_private_state()->activity_sessions[0], &empty_session, sizeof(ActivitySession)); // Add lots of sessions then delete from the back for (int i = 0; i < ACTIVITY_MAX_ACTIVITY_SESSIONS_COUNT; i++) { ActivitySession activity = walk_activity; activity.start_utc = i; activity_sessions_prv_add_activity_session(&activity); cl_assert_equal_i(activity_private_state()->activity_sessions_count, i + 1); cl_assert_equal_m(&activity_private_state()->activity_sessions[i], &activity, sizeof(ActivitySession)); } for (int i = ACTIVITY_MAX_ACTIVITY_SESSIONS_COUNT - 1; i >= 0; i--) { ActivitySession activity = walk_activity; activity.start_utc = i; ActivitySession next_activity = activity; next_activity.start_utc = i - 1; activity_sessions_prv_delete_activity_session(&activity); cl_assert_equal_i(activity_private_state()->activity_sessions_count, i); if (activity_private_state()->activity_sessions_count) { cl_assert_equal_m(&activity_private_state()->activity_sessions[i], &empty_session, sizeof(ActivitySession)); } } cl_assert_equal_m(&activity_private_state()->activity_sessions[0], &empty_session, sizeof(ActivitySession)); // Add 3 sessions and delete from the middle ActivitySession a1 = walk_activity; a1.start_utc = 1; ActivitySession a2 = walk_activity; a2.start_utc = 2; ActivitySession a3 = walk_activity; a3.start_utc = 3; activity_sessions_prv_add_activity_session(&a1); activity_sessions_prv_add_activity_session(&a2); activity_sessions_prv_add_activity_session(&a3); cl_assert_equal_i(activity_private_state()->activity_sessions_count, 3); activity_sessions_prv_delete_activity_session(&a2); cl_assert_equal_i(activity_private_state()->activity_sessions_count, 2); cl_assert_equal_m(&activity_private_state()->activity_sessions[0], &a1, sizeof(ActivitySession)); cl_assert_equal_m(&activity_private_state()->activity_sessions[1], &a3, sizeof(ActivitySession)); }