/** * @file test_database.c * @brief Black-box tests for Trainlog persistence. */ #include #include #include #include "trainlog/database.h" #include "trainlog/id.h" #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 test_database_open_and_schema(void) { TrainlogDatabase *database = NULL; int schema_version = 0; int foreign_keys = 0; CHECK( trainlog_database_open(":memory:", &database) == TRAINLOG_STATUS_OK ); CHECK( trainlog_database_schema_version(database, &schema_version) == TRAINLOG_STATUS_OK ); CHECK(schema_version == TRAINLOG_DATABASE_SCHEMA_VERSION); CHECK( trainlog_database_foreign_keys_enabled(database, &foreign_keys) == TRAINLOG_STATUS_OK ); CHECK(foreign_keys == 1); trainlog_database_close(database); return true; } static bool test_database_open_diagnostic(void) { TrainlogDatabase *database = NULL; char diagnostic[256]; /* CONTRACT: a launcher must retain the stable status code while showing * the SQLite operation that blocked access to the user's real database. */ CHECK( trainlog_database_open_with_diagnostic( "/", &database, diagnostic, sizeof(diagnostic) ) == TRAINLOG_STATUS_DATABASE_ERROR ); CHECK(database == NULL); CHECK(strstr(diagnostic, "open database:") != NULL); return true; } static bool test_generated_ids(void) { char first[TRAINLOG_GENERATED_ID_CAPACITY]; char second[TRAINLOG_GENERATED_ID_CAPACITY]; char sync_id[TRAINLOG_GENERATED_ID_CAPACITY]; CHECK( trainlog_id_generate("ex", first, sizeof(first)) == TRAINLOG_STATUS_OK ); CHECK( trainlog_id_generate("ex", second, sizeof(second)) == TRAINLOG_STATUS_OK ); CHECK(strncmp(first, "ex_", 3U) == 0); CHECK(strlen(first) == TRAINLOG_GENERATED_ID_CAPACITY - 1U); CHECK(strcmp(first, second) != 0); CHECK(first[3U + 14U] == '4'); /* TRAINLOG_SYNC_ID_PREFIX_TEST */ CHECK( trainlog_id_generate( "sy", sync_id, sizeof(sync_id) ) == TRAINLOG_STATUS_OK ); CHECK( strncmp( sync_id, "sy_", 3U ) == 0 ); CHECK( sync_id[3U + 14U] == '4' ); return true; } static bool seed_exercise(TrainlogDatabase *database) { return trainlog_database_insert_exercise( database, "ex_test", "Presse à cuisses", "presse à cuisses", TRAINLOG_TRACKING_REPS ) == TRAINLOG_STATUS_OK; } static bool test_session_insert(void) { TrainlogDatabase *database = NULL; TrainlogSetInput sets[2]; TrainlogSessionExerciseInput exercise; TrainlogSessionInput session; size_t count = 0U; CHECK( trainlog_database_open(":memory:", &database) == TRAINLOG_STATUS_OK ); CHECK(seed_exercise(database)); (void)memset(sets, 0, sizeof(sets)); sets[0].reps = 5; sets[0].has_weight = true; sets[0].weight_kg = 80.0; sets[1] = sets[0]; (void)memset(&exercise, 0, sizeof(exercise)); (void)snprintf( exercise.exercise_id, sizeof(exercise.exercise_id), "%s", "ex_test" ); exercise.load_mode = TRAINLOG_LOAD_EXTERNAL; exercise.rest_seconds = 60; exercise.target_sets = 2; exercise.target_reps = 5; exercise.target_has_weight = true; exercise.target_weight_kg = 80.0; exercise.sets = sets; exercise.set_count = 2U; (void)memset(&session, 0, sizeof(session)); (void)snprintf( session.session_id, sizeof(session.session_id), "%s", "se_test" ); (void)snprintf( session.started_at, sizeof(session.started_at), "%s", "2026-09-05T18:00:00+02:00" ); (void)snprintf( session.ended_at, sizeof(session.ended_at), "%s", "2026-09-05T19:00:00+02:00" ); session.exercises = &exercise; session.exercise_count = 1U; CHECK( trainlog_database_insert_session(database, &session) == TRAINLOG_STATUS_OK ); CHECK( trainlog_database_insert_session(database, &session) == TRAINLOG_STATUS_CONFLICT ); CHECK( trainlog_database_session_count(database, &count) == TRAINLOG_STATUS_OK ); CHECK(count == 1U); trainlog_database_close(database); return true; } static bool test_body_weight_history(void) { TrainlogDatabase *database = NULL; TrainlogBodyObservationInput observation; TrainlogWeightPoint points[4]; size_t count = 0U; CHECK( trainlog_database_open(":memory:", &database) == TRAINLOG_STATUS_OK ); (void)memset(&observation, 0, sizeof(observation)); (void)snprintf( observation.observation_id, sizeof(observation.observation_id), "%s", "bo_test" ); (void)snprintf( observation.observed_at, sizeof(observation.observed_at), "%s", "2026-09-05T07:00:00+02:00" ); observation.has_body_weight = true; observation.body_weight_kg = 82.4; CHECK( trainlog_database_insert_body_observation( database, &observation ) == TRAINLOG_STATUS_OK ); CHECK( trainlog_database_list_weight_points( database, points, 4U, &count ) == TRAINLOG_STATUS_OK ); CHECK(count == 1U); CHECK(points[0].body_weight_kg > 82.39); CHECK(points[0].body_weight_kg < 82.41); trainlog_database_close(database); return true; } static bool test_transaction_rollback(void) { TrainlogDatabase *database = NULL; size_t count = 0U; CHECK( trainlog_database_open(":memory:", &database) == TRAINLOG_STATUS_OK ); CHECK(trainlog_database_begin(database) == TRAINLOG_STATUS_OK); CHECK(seed_exercise(database)); CHECK(trainlog_database_rollback(database) == TRAINLOG_STATUS_OK); CHECK( trainlog_database_exercise_count(database, &count) == TRAINLOG_STATUS_OK ); CHECK(count == 0U); trainlog_database_close(database); return true; } struct TestCase { const char *name; bool (*function)(void); }; int main(void) { static const struct TestCase tests[] = { {"database_open_and_schema", test_database_open_and_schema}, {"database_open_diagnostic", test_database_open_diagnostic}, {"generated_ids", test_generated_ids}, {"session_insert", test_session_insert}, {"body_weight_history", test_body_weight_history}, {"transaction_rollback", test_transaction_rollback}, }; size_t index; for (index = 0U; index < sizeof(tests) / sizeof(tests[0]); ++index) { if (!tests[index].function()) { (void)fprintf(stderr, "FAIL %s\n", tests[index].name); return 1; } (void)printf("PASS %s\n", tests[index].name); } return 0; }