/** * @file test_max_results.c * @brief Explicit measured-max persistence and bounded migration regressions. */ #include #include #include #include #include #include #include #include "trainlog/database.h" /* ABI CONTRACT: these offsets and total size are the published LP64 layout; * performance read-model fixes must use existing fields or private storage. */ _Static_assert(offsetof(TrainlogExercisePerformancePoint, actual_set_count) == 184U, "performance point ABI: actual_set_count"); _Static_assert(offsetof(TrainlogExercisePerformancePoint, has_performance) == 192U, "performance point ABI: has_performance"); _Static_assert(offsetof(TrainlogExercisePerformancePoint, has_explicit_max) == 196U, "performance point ABI: has_explicit_max"); _Static_assert(offsetof(TrainlogExercisePerformancePoint, weight_kg) == 208U, "performance point ABI: weight_kg"); _Static_assert(offsetof(TrainlogExercisePerformancePoint, equipment_id) == 216U, "performance point ABI: equipment_id"); _Static_assert(sizeof(TrainlogExercisePerformancePoint) == 352U, "performance point ABI: size"); #define CHECK(condition) \ do { \ if (!(condition)) { \ (void)fprintf(stderr, "CHECK failed at %s:%d: %s\n", \ __FILE__, __LINE__, #condition); \ return false; \ } \ } while (0) static bool add_exercise(TrainlogDatabase *database, const char *id, const char *name) { return trainlog_database_insert_exercise(database, id, name, name, TRAINLOG_TRACKING_REPS) == TRAINLOG_STATUS_OK; } static void max_input(TrainlogSessionExerciseInput *input, const char *entry_id, const char *exercise_id, double weight) { (void)memset(input, 0, sizeof(*input)); (void)snprintf(input->entry_id, sizeof(input->entry_id), "%s", entry_id); (void)snprintf(input->exercise_id, sizeof(input->exercise_id), "%s", exercise_id); (void)snprintf(input->equipment_id, sizeof(input->equipment_id), "%s", "rear_delt_pec_fly"); input->recording_mode = TRAINLOG_RECORDING_SETS; input->tracking_mode = TRAINLOG_TRACKING_REPS; input->load_mode = TRAINLOG_LOAD_NONE; input->has_max_weight = true; input->max_weight_kg = weight; } static bool test_explicit_max_round_trip_and_identity(void) { TrainlogDatabase *database = NULL; TrainlogSessionExerciseInput entries[3]; TrainlogSessionInput session; TrainlogSessionSummary summary; TrainlogPersistedExerciseDetail details[3]; TrainlogEditableExerciseRecord editable[3]; TrainlogExercisePerformancePoint points[2]; size_t count = 0U; size_t set_count = 0U; CHECK(trainlog_database_open(":memory:", &database) == TRAINLOG_STATUS_OK); CHECK(add_exercise(database, "ex_pec", "Pec Fly")); CHECK(add_exercise(database, "ex_rear", "Rear Delt Fly")); CHECK(trainlog_database_insert_exercise_profiled(database, "ex_walk", "Marche", "marche", TRAINLOG_TRACKING_DURATION, TRAINLOG_RECORDING_CONTINUOUS, TRAINLOG_EXERCISE_DATA_SPEED_KMH) == TRAINLOG_STATUS_OK); max_input(&entries[0], "sxe_pec", "ex_pec", 100.0); max_input(&entries[1], "sxe_rear", "ex_rear", 86.0); (void)memset(&entries[2], 0, sizeof(entries[2])); (void)snprintf(entries[2].entry_id, sizeof(entries[2].entry_id), "%s", "sxe_walk"); (void)snprintf(entries[2].exercise_id, sizeof(entries[2].exercise_id), "%s", "ex_walk"); entries[2].recording_mode = TRAINLOG_RECORDING_CONTINUOUS; entries[2].tracking_mode = TRAINLOG_TRACKING_DURATION; entries[2].data_fields = TRAINLOG_EXERCISE_DATA_SPEED_KMH; entries[2].load_mode = TRAINLOG_LOAD_NONE; entries[2].continuous_duration_seconds = 600; entries[2].continuous_has_speed = true; entries[2].continuous_speed_kmh = 5.5; (void)memset(&session, 0, sizeof(session)); (void)snprintf(session.session_id, sizeof(session.session_id), "%s", "se_explicit_max"); (void)snprintf(session.started_at, sizeof(session.started_at), "%s", "2031-02-03T08:15:00+01:00"); session.session_type = TRAINLOG_SESSION_MAX_TEST; session.exercises = entries; session.exercise_count = 3U; CHECK(trainlog_database_insert_session(database, &session) == TRAINLOG_STATUS_OK); CHECK(trainlog_database_get_session_details(database, "se_explicit_max", &summary, details, 3U, &count) == TRAINLOG_STATUS_OK); CHECK(count == 3U); CHECK(details[0].has_max_weight != 0 && details[0].max_weight_kg == 100.0); CHECK(details[1].has_max_weight != 0 && details[1].max_weight_kg == 86.0); CHECK(details[0].actual_set_count == 0U && details[1].actual_set_count == 0U); CHECK(strcmp(details[0].entry_id, "sxe_pec") == 0); CHECK(strcmp(details[0].equipment_id, details[1].equipment_id) == 0); CHECK(details[2].continuous_duration_seconds == 600); CHECK(details[2].has_continuous_speed != 0 && details[2].continuous_speed_kmh == 5.5); trainlog_database_free_session_details(details, count); CHECK(trainlog_database_load_session_editable(database, "se_explicit_max", &summary, editable, 3U, &count, NULL, 0U, &set_count) == TRAINLOG_STATUS_OK); CHECK(count == 3U && set_count == 0U); CHECK(editable[2].continuous_duration_seconds == 600); CHECK(editable[2].has_continuous_speed != 0 && editable[2].continuous_speed_kmh == 5.5); CHECK(trainlog_database_list_exercise_performance(database, "ex_pec", points, 2U, &count) == TRAINLOG_STATUS_OK); CHECK(count == 1U && points[0].has_performance != 0); CHECK(points[0].has_explicit_max != 0); CHECK(points[0].actual_set_count == 0U && points[0].weight_kg == 100.0); CHECK(strcmp(points[0].equipment_id, "rear_delt_pec_fly") == 0); entries[0].max_weight_kg = 101.5; CHECK(trainlog_database_replace_session_exercises(database, "se_explicit_max", entries, 3U) == TRAINLOG_STATUS_OK); CHECK(trainlog_database_get_session_details(database, "se_explicit_max", &summary, details, 3U, &count) == TRAINLOG_STATUS_OK); CHECK(details[0].max_weight_kg == 101.5); CHECK(strcmp(details[0].entry_id, "sxe_pec") == 0); trainlog_database_free_session_details(details, count); session.session_type = TRAINLOG_SESSION_TRAINING; (void)snprintf(session.session_id, sizeof(session.session_id), "%s", "se_invalid_training_max"); CHECK(trainlog_database_insert_session(database, &session) == TRAINLOG_STATUS_INVALID_ARGUMENT); trainlog_database_close(database); return true; } static bool test_v8_migration_refuses_to_guess_multiple_attempts(void) { static const char *const SQL = /* A real v8 database always owns the exercise table even though this * MAX-only fixture has no exercise rows. */ "CREATE TABLE exercises(id INTEGER PRIMARY KEY,exercise_id TEXT NOT NULL UNIQUE," "name TEXT NOT NULL,normalized_name TEXT NOT NULL UNIQUE,tracking_mode TEXT NOT NULL," "recording_mode TEXT NOT NULL,data_fields INTEGER NOT NULL);" "CREATE TABLE sessions(id INTEGER PRIMARY KEY,session_type TEXT);" "CREATE TABLE session_exercises(id INTEGER PRIMARY KEY,entry_id TEXT NOT NULL UNIQUE," "session_row_id INTEGER NOT NULL,exercise_row_id INTEGER NOT NULL,recording_mode TEXT NOT NULL," "data_fields INTEGER NOT NULL,position INTEGER NOT NULL,load_mode TEXT NOT NULL,rest_seconds INTEGER NOT NULL," "target_sets INTEGER,target_reps INTEGER,target_duration_seconds INTEGER,target_weight_kg REAL," "equipment_id TEXT,notes TEXT,UNIQUE(session_row_id,position));" "CREATE TABLE performed_sets(id INTEGER PRIMARY KEY," "session_exercise_row_id INTEGER,position INTEGER,reps INTEGER," "duration_seconds INTEGER,weight_kg REAL);" "INSERT INTO exercises VALUES(1,'ex_max_a','Max A','max a','reps','sets',0);" "INSERT INTO exercises VALUES(2,'ex_max_b','Max B','max b','reps','sets',0);" "INSERT INTO exercises VALUES(3,'ex_max_c','Max C','max c','reps','sets',0);" "INSERT INTO sessions VALUES(1,'max_test');" "INSERT INTO session_exercises VALUES(10,'sxe_max_a',1,1,'sets',0,0,'none',0,NULL,NULL,NULL,NULL,NULL,NULL);" "INSERT INTO session_exercises VALUES(11,'sxe_max_b',1,2,'sets',0,1,'none',0,NULL,NULL,NULL,NULL,NULL,NULL);" "INSERT INTO session_exercises VALUES(12,'sxe_max_c',1,3,'sets',0,2,'none',0,NULL,NULL,NULL,NULL,NULL,NULL);" "INSERT INTO performed_sets VALUES(20,10,0,1,NULL,100.0);" "INSERT INTO performed_sets VALUES(21,11,0,1,NULL,80.0);" "INSERT INTO performed_sets VALUES(22,11,1,1,NULL,86.0);" "INSERT INTO performed_sets VALUES(23,12,0,1,NULL,0.0);" "PRAGMA user_version=8;"; char path[] = "/tmp/trainlog-max-v8-XXXXXX"; sqlite3 *raw = NULL; sqlite3_stmt *statement = NULL; TrainlogDatabase *database = NULL; int fd = mkstemp(path); CHECK(fd >= 0 && close(fd) == 0); CHECK(sqlite3_open(path, &raw) == SQLITE_OK); CHECK(sqlite3_exec(raw, SQL, NULL, NULL, NULL) == SQLITE_OK); CHECK(sqlite3_close(raw) == SQLITE_OK); CHECK(trainlog_database_open(path, &database) == TRAINLOG_STATUS_OK); trainlog_database_close(database); CHECK(sqlite3_open_v2(path, &raw, SQLITE_OPEN_READONLY, NULL) == SQLITE_OK); CHECK(sqlite3_prepare_v2(raw, "SELECT session_exercise_row_id,max_weight_kg FROM max_results;", -1, &statement, NULL) == SQLITE_OK); CHECK(sqlite3_step(statement) == SQLITE_ROW); CHECK(sqlite3_column_int(statement, 0) == 10); CHECK(sqlite3_column_double(statement, 1) == 100.0); CHECK(sqlite3_step(statement) == SQLITE_DONE); CHECK(sqlite3_finalize(statement) == SQLITE_OK); CHECK(sqlite3_prepare_v2(raw, "SELECT reps,weight_kg FROM performed_sets " "WHERE session_exercise_row_id=12;", -1, &statement, NULL) == SQLITE_OK); CHECK(sqlite3_step(statement) == SQLITE_ROW); CHECK(sqlite3_column_int(statement, 0) == 1); CHECK(sqlite3_column_double(statement, 1) == 0.0); CHECK(sqlite3_step(statement) == SQLITE_DONE); CHECK(sqlite3_finalize(statement) == SQLITE_OK); CHECK(sqlite3_prepare_v2(raw, "SELECT position,reps,weight_kg FROM performed_sets " "WHERE session_exercise_row_id=11 ORDER BY position;", -1, &statement, NULL) == SQLITE_OK); CHECK(sqlite3_step(statement) == SQLITE_ROW && sqlite3_column_double(statement, 2) == 80.0); CHECK(sqlite3_step(statement) == SQLITE_ROW && sqlite3_column_double(statement, 2) == 86.0); CHECK(sqlite3_step(statement) == SQLITE_DONE); CHECK(sqlite3_finalize(statement) == SQLITE_OK); CHECK(sqlite3_close(raw) == SQLITE_OK); CHECK(unlink(path) == 0); return true; } static bool test_public_performance_retains_session_representative_contract(void) { char path[] = "/tmp/trainlog-performance-max-XXXXXX"; int fd = mkstemp(path); sqlite3 *raw = NULL; TrainlogDatabase *database = NULL; TrainlogSessionExerciseInput entry; TrainlogSessionInput session; TrainlogExercisePerformancePoint points[2]; size_t count = 0U; CHECK(fd >= 0 && close(fd) == 0); CHECK(trainlog_database_open(path, &database) == TRAINLOG_STATUS_OK); CHECK(add_exercise(database, "ex_mixed_fact", "Mixed Fact")); max_input(&entry, "sxe_mixed_fact", "ex_mixed_fact", 100.0); (void)memset(&session, 0, sizeof(session)); (void)snprintf(session.session_id, sizeof(session.session_id), "%s", "se_mixed_fact"); (void)snprintf(session.started_at, sizeof(session.started_at), "%s", "2031-02-04T08:15:00+01:00"); session.session_type = TRAINLOG_SESSION_MAX_TEST; session.exercises = &entry; session.exercise_count = 1U; CHECK(trainlog_database_insert_session(database, &session) == TRAINLOG_STATUS_OK); trainlog_database_close(database); /* The writer deliberately rejects mixed input. This controlled persisted * fixture proves the read model independently preserves both compatible * facts if an imported/historical occurrence owns them. */ CHECK(sqlite3_open(path, &raw) == SQLITE_OK); CHECK(sqlite3_exec(raw, "INSERT INTO performed_sets(session_exercise_row_id,position,reps,weight_kg) " "SELECT id,0,5,80.0 FROM session_exercises WHERE entry_id='sxe_mixed_fact';", NULL, NULL, NULL) == SQLITE_OK); CHECK(sqlite3_close(raw) == SQLITE_OK); CHECK(trainlog_database_open(path, &database) == TRAINLOG_STATUS_OK); CHECK(trainlog_database_list_exercise_performance(database, "ex_mixed_fact", points, 2U, &count) == TRAINLOG_STATUS_OK); /* PUBLIC CONTRACT: mixed persisted facts still produce one session point; * STATS preserves both through its private fact projection. */ CHECK(count == 1U); CHECK(points[0].actual_set_count == 0U); CHECK(points[0].has_explicit_max != 0 && points[0].weight_kg == 100.0); trainlog_database_close(database); CHECK(unlink(path) == 0); return true; } int main(void) { if (!test_explicit_max_round_trip_and_identity() || !test_v8_migration_refuses_to_guess_multiple_attempts() || !test_public_performance_retains_session_representative_contract()) { return 1; } (void)printf("max results: PASS\n"); return 0; }