trainlog/tui/tests/test_web_heart_rate.c

269 lines
13 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#define _POSIX_C_SOURCE 200809L
#include <sqlite3.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <yyjson.h>
#include "trainlog/database.h"
#include "trainlog/web_heart_rate.h"
#include "database_internal.h"
#define CHECK(value) \
do { \
if (!(value)) { \
fprintf(stderr, "CHECK failed line %d: %s\n", __LINE__, #value); \
goto cleanup; \
} \
} while (0)
static const char *const SESSION_ID = "se_11111111-1111-4111-8111-111111111111";
static const char *const ENTRY_ID = "sxe_22222222-2222-4222-8222-222222222222";
static const char *const RUN_ID = "cgr_55555555-5555-4555-8555-555555555555";
static const char *const CALIBRATION_ID = "cal_77777777-7777-4777-8777-777777777777";
static const char *const SLEEP_ID = "sl_88888888-8888-4888-8888-888888888888";
static bool execute_sql(sqlite3 *database, const char *sql) {
int rc = sqlite3_exec(database, sql, NULL, NULL, NULL);
if (rc != SQLITE_OK) {
fprintf(stderr, "seed SQLite rc=%d: %s\n", rc, sqlite3_errmsg(database));
return false;
}
return true;
}
static bool seed(sqlite3 *database) {
const char *const statements[] = {
"INSERT INTO "
"exercises(exercise_id,name,normalized_name,recording_mode,tracking_mode,data_fields)"
" VALUES('ex_33333333-3333-4333-8333-333333333333','Vélo guidé','velo guide',"
"'continuous','duration',0);"
"INSERT INTO sessions(session_id,started_at,ended_at,session_type,session_kind)"
" VALUES('se_11111111-1111-4111-8111-111111111111','2026-09-23T10:00:00+02:00',"
"'2026-09-23T10:10:00+02:00','training','cardio');",
"INSERT INTO session_exercises(entry_id,session_row_id,exercise_row_id,recording_mode,"
"tracking_mode,data_fields,position,load_mode,rest_seconds)"
" SELECT 'sxe_22222222-2222-4222-8222-222222222222',s.id,e.id,'continuous','duration',"
"0,0,'none',0 FROM sessions s,exercises e WHERE "
"s.session_id='se_11111111-1111-4111-8111-111111111111' AND "
"e.exercise_id='ex_33333333-3333-4333-8333-333333333333';"
"INSERT INTO "
"session_exercise_timeline(session_id,entry_id,exercise_id,started_at,ended_at,imported_at)"
" VALUES('se_11111111-1111-4111-8111-111111111111',"
"'sxe_22222222-2222-4222-8222-222222222222',"
"'ex_33333333-3333-4333-8333-333333333333','2026-09-23T10:00:10+02:00',"
"'2026-09-23T10:09:50+02:00','2026-09-23T10:11:00+02:00');",
"INSERT INTO "
"heart_rate_captures(capture_id,context_kind,context_id,started_at,ended_at,sensor_name,"
"imported_at)"
" VALUES('hrc_44444444-4444-4444-8444-444444444444','cardio',"
"'se_11111111-1111-4111-8111-111111111111','2026-09-23T10:00:00+02:00',"
"'2026-09-23T10:10:00+02:00','CYCPLUS H2','2026-09-23T10:11:00+02:00');"
"INSERT INTO heart_rate_samples(capture_id,sequence,observed_at,bpm,exercise_entry_id,"
"sensor_contact_detected,energy_expended) VALUES"
"('hrc_44444444-4444-4444-8444-444444444444',0,'2026-09-23T10:00:20+02:00',110,"
"'sxe_22222222-2222-4222-8222-222222222222',NULL,NULL),"
"('hrc_44444444-4444-4444-8444-444444444444',1,'2026-09-23T10:00:40+02:00',130,"
"'sxe_22222222-2222-4222-8222-222222222222',NULL,NULL);"
"INSERT INTO heart_rate_rr_intervals(capture_id,sample_sequence,rr_index,value_1024)"
" VALUES('hrc_44444444-4444-4444-8444-444444444444',0,0,1024);",
"INSERT INTO cardio_calibrations(calibration_id,protocol_version,session_id,entry_id,"
"started_at,effort_end_at,ended_at,heart_rate_capture_id,observed_peak_bpm,imported_at)"
" VALUES('cal_77777777-7777-4777-8777-777777777777',1,"
"'se_11111111-1111-4111-8111-111111111111','sxe_22222222-2222-4222-8222-222222222222',"
"'2026-09-23T10:00:00+02:00','2026-09-23T10:05:00+02:00','2026-09-23T10:10:00+02:00',"
"'hrc_44444444-4444-4444-8444-444444444444',160,'2026-09-23T10:11:00+02:00');"
"INSERT INTO "
"cardio_calibration_recovery(calibration_id,target_offset_seconds,observed_at,bpm)"
" VALUES('cal_77777777-7777-4777-8777-777777777777',60,'2026-09-23T10:06:00+02:00',140);",
"INSERT INTO cardio_guidance_runs(run_id,session_id,started_at,ended_at,imported_at)"
" VALUES('cgr_55555555-5555-4555-8555-555555555555',"
"'se_11111111-1111-4111-8111-111111111111','2026-09-23T10:00:20+02:00',"
"'2026-09-23T10:03:20+02:00','2026-09-23T10:11:00+02:00');"
"INSERT INTO cardio_guidance_phases(run_id,phase_id,entry_id,position,kind,target_min_bpm,"
"target_max_bpm,calibration_id,calibration_observed_peak_bpm,minimum_percent,maximum_"
"percent,"
"exit_kind,exit_seconds,exit_bpm,started_at,ended_at,final_instruction)"
" VALUES('cgr_55555555-5555-4555-8555-555555555555',"
"'cgp_66666666-6666-4666-8666-666666666666','sxe_22222222-2222-4222-8222-222222222222',"
"0,'work',120,140,'cal_77777777-7777-4777-8777-777777777777',160,75,88,"
"'fixed_duration',180,NULL,'2026-09-23T10:00:20+02:00','2026-09-23T10:03:20+02:00','"
"maintain');",
"INSERT INTO cardio_guidance_events(run_id,phase_id,sequence,observed_at,instruction,bpm,"
"target_min_bpm,target_max_bpm) VALUES"
"('cgr_55555555-5555-4555-8555-555555555555','cgp_66666666-6666-4666-8666-666666666666',"
"0,'2026-09-23T10:00:20+02:00','accelerate',110,120,140),"
"('cgr_55555555-5555-4555-8555-555555555555','cgp_66666666-6666-4666-8666-666666666666',"
"1,'2026-09-23T10:00:40+02:00','maintain',130,120,140);",
"INSERT INTO "
"sleep_diary_entries(entry_id,night_start_date,night_end_date,created_at,updated_at,"
"current_revision_id,deleted) "
"VALUES('sl_88888888-8888-4888-8888-888888888888','2026-09-22',"
"'2026-09-23','2026-09-22T22:00:00+02:00','2026-09-23T06:30:00+02:00',"
"'slr_99999999-9999-4999-8999-999999999999',0);"
"INSERT INTO sleep_diary_revisions(revision_id,entry_id,parent_revision_id,created_at,"
"sleep_quality,wake_quality,day_form,treatment_and_notes) VALUES("
"'slr_99999999-9999-4999-8999-999999999999','sl_88888888-8888-4888-8888-888888888888',"
"NULL,'2026-09-23T06:30:00+02:00',NULL,NULL,NULL,'');",
"INSERT INTO sleep_diary_events(revision_id,event_id,event_type,start_at,end_at) VALUES"
"('slr_99999999-9999-4999-8999-999999999999','sle_1','bed_time',"
"'2026-09-22T22:30:00+02:00',NULL),"
"('slr_99999999-9999-4999-8999-999999999999','sle_2','sleep',"
"'2026-09-23T02:10:00+02:00','2026-09-23T03:20:00+02:00'),"
"('slr_99999999-9999-4999-8999-999999999999','sle_3','final_get_up',"
"'2026-09-23T06:20:00+02:00',NULL);"
"INSERT INTO sleep_medication_intakes(revision_id,intake_id,medication_id,medication_name,"
"taken_at,dose_value,dose_unit,quantity,note,created_at) VALUES("
"'slr_99999999-9999-4999-8999-999999999999','mdi_1','med_1','Médicament test',"
"'2026-09-22T22:40:00+02:00',NULL,NULL,2,NULL,'2026-09-22T22:40:00+02:00');",
"INSERT INTO "
"heart_rate_captures(capture_id,context_kind,context_id,started_at,ended_at,sensor_name,"
"imported_at)"
" VALUES('hrc_aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa','sleep',"
"'sl_88888888-8888-4888-8888-888888888888','2026-09-22T22:30:00+02:00',"
"'2026-09-23T06:20:00+02:00','CYCPLUS H2','2026-09-23T07:00:00+02:00');"
"INSERT INTO heart_rate_samples(capture_id,sequence,observed_at,bpm,exercise_entry_id,"
"sensor_contact_detected,energy_expended) VALUES("
"'hrc_aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa',0,'2026-09-23T02:10:00+02:00',75,NULL,NULL,"
"NULL);",
};
size_t index;
for (index = 0U; index < sizeof(statements) / sizeof(statements[0]); ++index) {
if (!execute_sql(database, statements[index])) {
return false;
}
}
return true;
}
static yyjson_doc *timeline(TrainlogDatabase *database, const char *context_id) {
char *json = malloc(TRAINLOG_WEB_HEART_RATE_JSON_CAPACITY);
size_t size = 0U;
yyjson_doc *document = NULL;
if (json == NULL) {
return NULL;
}
if (trainlog_web_heart_rate_timeline_json(
database, context_id, json, TRAINLOG_WEB_HEART_RATE_JSON_CAPACITY, &size) ==
TRAINLOG_STATUS_OK &&
size > 0U) {
document = yyjson_read(json, size, 0);
}
free(json);
return document;
}
int main(void) {
char path[] = "/tmp/trainlog-web-heart-rate-XXXXXX";
int descriptor = mkstemp(path);
TrainlogDatabase *database = NULL;
yyjson_doc *document = NULL;
yyjson_val *root;
yyjson_val *capture;
yyjson_val *samples;
yyjson_val *events;
yyjson_val *guidance;
yyjson_val *phase;
yyjson_val *calibration;
int result = EXIT_FAILURE;
CHECK(descriptor >= 0 && close(descriptor) == 0);
CHECK(trainlog_database_open(path, &database) == TRAINLOG_STATUS_OK);
CHECK(seed(database->connection));
document = timeline(database, SESSION_ID);
CHECK(document != NULL);
root = yyjson_doc_get_root(document);
CHECK(yyjson_get_bool(yyjson_obj_get(root, "available")));
capture = yyjson_obj_get(root, "capture");
CHECK(strcmp(yyjson_get_str(yyjson_obj_get(capture, "context_kind")), "cardio") == 0);
CHECK(yyjson_get_int(yyjson_obj_get(capture, "sample_count")) == 2);
samples = yyjson_obj_get(capture, "samples");
CHECK(yyjson_arr_size(samples) == 2U);
CHECK(yyjson_arr_size(yyjson_obj_get(yyjson_arr_get(samples, 0U), "rr_1024")) == 1U);
events = yyjson_obj_get(root, "events");
CHECK(yyjson_arr_size(events) == 2U);
guidance = yyjson_obj_get(root, "guidance");
CHECK(yyjson_is_obj(guidance));
CHECK(strcmp(yyjson_get_str(yyjson_obj_get(guidance, "run_id")), RUN_ID) == 0);
phase = yyjson_arr_get(yyjson_obj_get(guidance, "phases"), 0U);
CHECK(strcmp(yyjson_get_str(yyjson_obj_get(phase, "entry_id")), ENTRY_ID) == 0);
CHECK(yyjson_get_int(yyjson_obj_get(yyjson_obj_get(phase, "target"), "minimum_bpm")) == 120);
CHECK(yyjson_arr_size(yyjson_obj_get(phase, "events")) == 2U);
calibration = yyjson_obj_get(root, "calibration");
CHECK(yyjson_is_obj(calibration));
CHECK(strcmp(yyjson_get_str(yyjson_obj_get(calibration, "calibration_id")), CALIBRATION_ID) ==
0);
CHECK(yyjson_get_int(yyjson_obj_get(calibration, "observed_peak_bpm")) == 160);
CHECK(yyjson_arr_size(yyjson_obj_get(calibration, "recovery")) == 1U);
yyjson_doc_free(document);
document = NULL;
document = timeline(database, SLEEP_ID);
CHECK(document != NULL);
root = yyjson_doc_get_root(document);
CHECK(yyjson_get_bool(yyjson_obj_get(root, "available")));
capture = yyjson_obj_get(root, "capture");
CHECK(strcmp(yyjson_get_str(yyjson_obj_get(capture, "context_kind")), "sleep") == 0);
CHECK(yyjson_get_int(yyjson_obj_get(capture, "sample_count")) == 1);
events = yyjson_obj_get(root, "events");
CHECK(yyjson_arr_size(events) == 4U);
CHECK(strcmp(yyjson_get_str(yyjson_obj_get(yyjson_arr_get(events, 0U), "at")),
"2026-09-22T22:30:00+02:00") == 0);
CHECK(yyjson_is_null(yyjson_obj_get(yyjson_arr_get(events, 0U), "end_at")));
CHECK(strcmp(yyjson_get_str(yyjson_obj_get(yyjson_arr_get(events, 1U), "at")),
"2026-09-23T02:10:00+02:00") == 0);
CHECK(strcmp(yyjson_get_str(yyjson_obj_get(yyjson_arr_get(events, 1U), "end_at")),
"2026-09-23T03:20:00+02:00") == 0);
CHECK(strcmp(yyjson_get_str(yyjson_obj_get(yyjson_arr_get(events, 3U), "at")),
"2026-09-22T22:40:00+02:00") == 0);
CHECK(strcmp(yyjson_get_str(yyjson_obj_get(yyjson_arr_get(events, 3U), "label")),
"Médicament test ×2") == 0);
CHECK(yyjson_is_null(yyjson_obj_get(root, "guidance")));
CHECK(yyjson_is_null(yyjson_obj_get(root, "calibration")));
yyjson_doc_free(document);
document = NULL;
document = timeline(database, "se_bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb");
CHECK(document != NULL);
root = yyjson_doc_get_root(document);
CHECK(!yyjson_get_bool(yyjson_obj_get(root, "available")));
CHECK(yyjson_is_null(yyjson_obj_get(root, "capture")));
yyjson_doc_free(document);
document = NULL;
{
char buffer[64];
size_t size = 123U;
CHECK(
trainlog_web_heart_rate_timeline_json(database, "bad", buffer, sizeof(buffer), &size) ==
TRAINLOG_STATUS_INVALID_ARGUMENT);
}
result = EXIT_SUCCESS;
cleanup:
if (document != NULL) {
yyjson_doc_free(document);
}
if (database != NULL) {
trainlog_database_close(database);
}
(void)unlink(path);
if (result == EXIT_SUCCESS) {
puts("PASS factual Web heart-rate timeline");
}
return result;
}