trainlog/tui/tests/test_session_generation.c

228 lines
10 KiB
C

#include "trainlog/session_generation.h"
#include "trainlog/database.h"
#include <assert.h>
#include <math.h>
#include <stdio.h>
#include <string.h>
void trainlog_run_generated_session_generation_fixtures(void);
static TrainlogStatus accept_row(void *context, const TrainlogGenerationHistoryRow *row)
{
return trainlog_session_generation_analyzer_accept(context, row);
}
static TrainlogGenerationCandidate leg_press_candidate(void)
{
static const char *const patterns[] = {"knee_dominant"};
static const char *const refs[] = {"acsm_2009"};
TrainlogGenerationCandidate result = {
.exercise_id = "ex_b432623f-bfe9-4daf-a653-60ec7fdffbde",
.equipment_id = "leg_press",
.primary_zone_id = "thighs",
.pattern_ids = patterns,
.pattern_count = 1U,
.source_ref_ids = refs,
.source_ref_count = 1U,
.confidence = "moderate",
.equipment_load_semantics = "external",
};
return result;
}
static TrainlogGenerationHistoryRow base_row(void)
{
TrainlogGenerationHistoryRow row = {
.session_id = "se_10000000-0000-4000-8000-000000000000",
.occurrence_id = "sxe_10000000-0000-4000-8000-000000000000",
.exercise_id = "ex_b432623f-bfe9-4daf-a653-60ec7fdffbde",
.started_at = "2026-09-08T12:00:00.500+00:00",
.equipment_id = "leg_press",
.recording_mode = TRAINLOG_RECORDING_SETS,
.tracking_mode = TRAINLOG_TRACKING_REPS,
.load_mode = TRAINLOG_LOAD_NONE,
.rest_seconds = 0,
.has_actual_set = true,
.repetitions = 10,
.has_weight = true,
.weight_kg = 80.0,
};
return row;
}
static void test_anchor_exposure_and_boundaries(void)
{
TrainlogGenerationCandidate candidate = leg_press_candidate();
TrainlogGenerationRequest request = {
.zone_id = "thighs", .goal_id = "hypertrophy", .duration_minutes = 30,
.reference_time = "2026-09-09T12:00:00.500Z",
.candidates = &candidate, .candidate_count = 1U,
};
TrainlogSessionGenerationAnalyzer *analyzer = NULL;
TrainlogGeneratedSession result;
TrainlogGenerationHistoryRow row = base_row();
size_t index;
assert(trainlog_session_generation_analyzer_create(&request, &analyzer) == TRAINLOG_STATUS_OK);
for (index = 0U; index < 3U; ++index) {
row.set_position = index;
row.weight_kg = index == 1U ? 85.0 : 80.0;
assert(accept_row(analyzer, &row) == TRAINLOG_STATUS_OK);
}
assert(trainlog_session_generation_analyzer_finish(analyzer, &result) == TRAINLOG_STATUS_OK);
assert(result.exercise_count == 1U);
assert(result.exercises[0].has_target_weight && result.exercises[0].target_weight_kg == 80.0);
/* Exactly 24h is excluded; it remains the latest full-history exposure. */
assert(result.exposure.within_24h.primary_set_count == 0U);
assert(result.exposure.within_72h.primary_set_count == 3U);
assert(result.exposure.warning_level == TRAINLOG_GENERATION_WARNING_NONE);
assert(result.exposure.has_latest && strcmp(result.exposure.latest_occurrence_id, row.occurrence_id) == 0);
trainlog_session_generation_analyzer_destroy(analyzer);
}
static void test_fraction_inside_boundary_and_invalid_time(void)
{
TrainlogGenerationCandidate candidate = leg_press_candidate();
TrainlogGenerationRequest request = {
.zone_id = "thighs", .goal_id = "general", .duration_minutes = 30,
.reference_time = "2026-09-09T12:00:00.500Z",
.candidates = &candidate, .candidate_count = 1U,
};
TrainlogSessionGenerationAnalyzer *analyzer = NULL;
TrainlogGenerationHistoryRow row = base_row();
TrainlogGeneratedSession result;
row.started_at = "2026-09-08T12:00:00.501Z";
row.has_weight = false;
assert(trainlog_session_generation_analyzer_create(&request, &analyzer) == TRAINLOG_STATUS_OK);
assert(trainlog_session_generation_analyzer_accept(analyzer, &row) == TRAINLOG_STATUS_OK);
assert(trainlog_session_generation_analyzer_finish(analyzer, &result) == TRAINLOG_STATUS_OK);
assert(result.exposure.within_24h.primary_set_count == 1U);
assert(result.exposure.warning_level == TRAINLOG_GENERATION_WARNING_WARNING);
trainlog_session_generation_analyzer_destroy(analyzer);
assert(trainlog_session_generation_analyzer_create(&request, &analyzer) == TRAINLOG_STATUS_OK);
row.started_at = "not-a-time";
assert(trainlog_session_generation_analyzer_accept(analyzer, &row) == TRAINLOG_STATUS_DATABASE_ERROR);
assert(trainlog_session_generation_analyzer_finish(analyzer, &result) == TRAINLOG_STATUS_DATABASE_ERROR);
trainlog_session_generation_analyzer_destroy(analyzer);
}
static void test_sparse_and_max_only_shape(void)
{
TrainlogGenerationRequest request = {
.zone_id = "chest", .goal_id = "strength", .duration_minutes = 30,
.reference_time = "2026-09-09T12:00Z",
};
TrainlogSessionGenerationAnalyzer *analyzer = NULL;
TrainlogGeneratedSession result;
assert(trainlog_session_generation_analyzer_create(&request, &analyzer) == TRAINLOG_STATUS_OK);
assert(trainlog_session_generation_analyzer_finish(analyzer, &result) == TRAINLOG_STATUS_OK);
assert(result.exercise_count == 0U && result.insufficient_resolved_candidates);
trainlog_session_generation_analyzer_destroy(analyzer);
}
static void test_database_convenience_and_explicit_empty_equipment(void)
{
TrainlogDatabase *database = NULL;
const TrainlogSetInput sets[] = {
{.reps = 10, .has_weight = true, .weight_kg = 70.0},
{.reps = 10, .has_weight = true, .weight_kg = 75.0},
};
const TrainlogSessionExerciseInput exercise = {
.entry_id = "sxe_30000000-0000-4000-8000-000000000000",
.exercise_id = "ex_b432623f-bfe9-4daf-a653-60ec7fdffbde",
.equipment_id = "leg_press",
.recording_mode = TRAINLOG_RECORDING_SETS,
.load_mode = TRAINLOG_LOAD_NONE,
.sets = sets,
.set_count = 2U,
};
const TrainlogSessionInput session = {
.session_id = "se_30000000-0000-4000-8000-000000000000",
.started_at = "2026-09-08T12:00:00Z",
.session_type = TRAINLOG_SESSION_TRAINING,
.exercises = &exercise,
.exercise_count = 1U,
};
TrainlogGenerationDatabaseRequest request = {
.zone_id = "thighs", .goal_id = "general", .duration_minutes = 30,
.reference_time = "2026-09-09T12:00:00Z",
};
TrainlogGeneratedSession result;
const char *const no_equipment_storage[] = {NULL};
assert(trainlog_database_open(":memory:", &database) == TRAINLOG_STATUS_OK);
assert(trainlog_database_insert_exercise_profiled(database,
"ex_b432623f-bfe9-4daf-a653-60ec7fdffbde", "Leg press", "leg press",
TRAINLOG_TRACKING_REPS, TRAINLOG_RECORDING_SETS, 0U) == TRAINLOG_STATUS_OK);
assert(trainlog_database_insert_session(database, &session) == TRAINLOG_STATUS_OK);
assert(trainlog_session_generate_from_database(database, &request, &result) == TRAINLOG_STATUS_OK);
assert(result.exercise_count == 1U && result.exercises[0].has_target_weight);
request.available_equipment_ids = no_equipment_storage;
request.available_equipment_count = 0U;
assert(trainlog_session_generate_from_database(database, &request, &result) == TRAINLOG_STATUS_OK);
assert(result.exercise_count == 0U && result.insufficient_resolved_candidates);
trainlog_database_close(database);
}
static void test_analyzer_owns_request_bytes_after_create(void)
{
char zone[] = "thighs";
char goal[] = "general";
char reference[] = "2026-09-09T12:00:00.500Z";
char exercise[] = "ex_b432623f-bfe9-4daf-a653-60ec7fdffbde";
char equipment[] = "leg_press";
char primary[] = "thighs";
char secondary[] = "glutes";
char pattern[] = "knee_dominant";
char source[] = "acsm_2009";
char confidence[] = "moderate";
char semantics[] = "external";
const char *secondary_values[] = {secondary};
const char *pattern_values[] = {pattern};
const char *source_values[] = {source};
TrainlogGenerationCandidate candidate = {
.exercise_id=exercise,.equipment_id=equipment,.primary_zone_id=primary,
.secondary_zone_ids=secondary_values,.secondary_zone_count=1U,
.pattern_ids=pattern_values,.pattern_count=1U,
.source_ref_ids=source_values,.source_ref_count=1U,
.confidence=confidence,.equipment_load_semantics=semantics,
};
TrainlogGenerationRequest request = {
.zone_id=zone,.goal_id=goal,.duration_minutes=30,.reference_time=reference,
.candidates=&candidate,.candidate_count=1U,
};
TrainlogSessionGenerationAnalyzer *analyzer = NULL;
TrainlogGeneratedSession output;
assert(trainlog_session_generation_analyzer_create(&request, &analyzer) == TRAINLOG_STATUS_OK);
(void)memset(zone, 'x', sizeof(zone)-1U);
(void)memset(goal, 'x', sizeof(goal)-1U);
(void)memset(reference, 'x', sizeof(reference)-1U);
(void)memset(exercise, 'x', sizeof(exercise)-1U);
(void)memset(equipment, 'x', sizeof(equipment)-1U);
(void)memset(primary, 'x', sizeof(primary)-1U);
(void)memset(secondary, 'x', sizeof(secondary)-1U);
(void)memset(pattern, 'x', sizeof(pattern)-1U);
(void)memset(source, 'x', sizeof(source)-1U);
(void)memset(confidence, 'x', sizeof(confidence)-1U);
(void)memset(semantics, 'x', sizeof(semantics)-1U);
assert(trainlog_session_generation_analyzer_finish(analyzer, &output) == TRAINLOG_STATUS_OK);
assert(output.exercise_count == 1U);
assert(strcmp(output.exercises[0].exercise_id,
"ex_b432623f-bfe9-4daf-a653-60ec7fdffbde") == 0);
assert(strcmp(output.exercises[0].secondary_zone_ids[0], "glutes") == 0);
assert(strcmp(output.exercises[0].pattern_ids[0], "knee_dominant") == 0);
assert(strcmp(output.exercises[0].source_ref_ids[0], "acsm_2009") == 0);
trainlog_session_generation_analyzer_destroy(analyzer);
}
int main(void)
{
trainlog_run_generated_session_generation_fixtures();
test_anchor_exposure_and_boundaries();
test_fraction_inside_boundary_and_invalid_time();
test_sparse_and_max_only_shape();
test_database_convenience_and_explicit_empty_equipment();
test_analyzer_owns_request_bytes_after_create();
(void)puts("session generation tests passed");
return 0;
}