Add exercise performance history and framed TUI views
This commit is contained in:
parent
55efbf84bb
commit
d555ddeffd
9 changed files with 1608 additions and 13 deletions
|
|
@ -76,3 +76,11 @@ Next:
|
||||||
- empty months remain visible and empty;
|
- empty months remain visible and empty;
|
||||||
- no zero fill or interpolation across missing months;
|
- no zero fill or interpolation across missing months;
|
||||||
- multiple readings in one month use the last monthly value.
|
- multiple readings in one month use the last monthly value.
|
||||||
|
|
||||||
|
### Exercise performance history
|
||||||
|
|
||||||
|
- added Enter-to-open exercise performance detail;
|
||||||
|
- added mode-aware representative best-set history;
|
||||||
|
- added terminal performance graph;
|
||||||
|
- assistance explicitly treats lower assistance as better;
|
||||||
|
- best recorded set remains distinct from measured max.
|
||||||
|
|
|
||||||
27
docs/reviews/exercise_performance.md
Normal file
27
docs/reviews/exercise_performance.md
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
# Exercise performance history
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
```text
|
||||||
|
TUI_EXERCISE_PERFORMANCE=IMPLEMENTED
|
||||||
|
MEASURED_MAX_TRACKING=DEFERRED
|
||||||
|
```
|
||||||
|
|
||||||
|
`F3 Exercices` now opens a read-only performance screen with Enter.
|
||||||
|
|
||||||
|
Representative session performance is deliberately mode-aware:
|
||||||
|
|
||||||
|
- no load: greatest successful reps or duration;
|
||||||
|
- external load: greatest actual load, then reps/duration;
|
||||||
|
- assistance: lowest actual assistance, then reps/duration.
|
||||||
|
|
||||||
|
A failed 0-repetition attempt is not promoted as representative performance.
|
||||||
|
|
||||||
|
The graph follows the most recent load mode and excludes incompatible load
|
||||||
|
modes from that graph. Assistance remains displayed in actual assistance kg and
|
||||||
|
is explicitly labelled `moins = mieux`.
|
||||||
|
|
||||||
|
The screen distinguishes `meilleur set enregistré` from a future
|
||||||
|
`max mesuré`. No estimated 1RM or pseudo-max is created in this slice.
|
||||||
|
|
||||||
|
No database schema or Trainlog JSON v1 change is required.
|
||||||
|
|
@ -118,3 +118,9 @@ No Trainlog JSON v1 change is expected.
|
||||||
```text
|
```text
|
||||||
DASHBOARD_12_MONTHS=IMPLEMENTED
|
DASHBOARD_12_MONTHS=IMPLEMENTED
|
||||||
```
|
```
|
||||||
|
|
||||||
|
```text
|
||||||
|
TUI_EXERCISE_PERFORMANCE=IMPLEMENTED
|
||||||
|
MEASURED_MAX_TRACKING=NEXT
|
||||||
|
PREVIOUS_SESSION_DEFAULTS=AFTER
|
||||||
|
```
|
||||||
|
|
|
||||||
30
docs/tui.md
30
docs/tui.md
|
|
@ -494,3 +494,33 @@ Rules:
|
||||||
|
|
||||||
The dashboard legend keeps the last visible raw value and the percentage
|
The dashboard legend keeps the last visible raw value and the percentage
|
||||||
change over the visible 12-month window.
|
change over the visible 12-month window.
|
||||||
|
|
||||||
|
## Exercise performance history
|
||||||
|
|
||||||
|
`F3 Exercices` opens an exercise performance screen with `Enter`.
|
||||||
|
|
||||||
|
The current slice tracks representative actual performance per workout without
|
||||||
|
inventing a maximum.
|
||||||
|
|
||||||
|
Semantics:
|
||||||
|
|
||||||
|
```text
|
||||||
|
load none
|
||||||
|
greatest successful reps/duration
|
||||||
|
|
||||||
|
external load
|
||||||
|
greatest actual load
|
||||||
|
tie -> greatest reps/duration
|
||||||
|
|
||||||
|
assistance
|
||||||
|
lowest actual assistance
|
||||||
|
tie -> greatest reps/duration
|
||||||
|
```
|
||||||
|
|
||||||
|
Assistance graphs keep kilograms as actual assistance and explicitly state that
|
||||||
|
lower assistance is better.
|
||||||
|
|
||||||
|
A recorded best set is not a measured maximum.
|
||||||
|
|
||||||
|
Measured maxima, max-session scheduling and working-load percentages remain a
|
||||||
|
separate later contract.
|
||||||
|
|
|
||||||
|
|
@ -172,4 +172,39 @@ TrainlogStatus trainlog_database_latest_body_pair(
|
||||||
TrainlogBodyPairPoint *output
|
TrainlogBodyPairPoint *output
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
|
/* TRAINLOG_EXERCISE_PERFORMANCE_API */
|
||||||
|
|
||||||
|
typedef struct TrainlogExercisePerformancePoint {
|
||||||
|
char session_id[TRAINLOG_ID_MAX + 1U];
|
||||||
|
char started_at[TRAINLOG_TIMESTAMP_MAX + 1U];
|
||||||
|
TrainlogTrackingMode tracking_mode;
|
||||||
|
TrainlogLoadMode load_mode;
|
||||||
|
size_t actual_set_count;
|
||||||
|
int has_performance;
|
||||||
|
int metric_value;
|
||||||
|
int has_weight;
|
||||||
|
double weight_kg;
|
||||||
|
} TrainlogExercisePerformancePoint;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Read newest-first per-session representative performance.
|
||||||
|
*
|
||||||
|
* Representative-set semantics:
|
||||||
|
*
|
||||||
|
* - no load: greatest successful reps/duration;
|
||||||
|
* - external load: greatest load, then greatest reps/duration;
|
||||||
|
* - assistance: lowest assistance, then greatest reps/duration.
|
||||||
|
*
|
||||||
|
* A zero-repetition failed attempt is never promoted to representative
|
||||||
|
* performance. This API does not create or infer a measured maximum.
|
||||||
|
*/
|
||||||
|
TrainlogStatus trainlog_database_list_exercise_performance(
|
||||||
|
TrainlogDatabase *database,
|
||||||
|
const char *exercise_id,
|
||||||
|
TrainlogExercisePerformancePoint *output,
|
||||||
|
size_t capacity,
|
||||||
|
size_t *output_count
|
||||||
|
);
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
||||||
|
|
@ -135,3 +135,15 @@ test_bodyviz = executable(
|
||||||
)
|
)
|
||||||
|
|
||||||
test('bodyviz', test_bodyviz)
|
test('bodyviz', test_bodyviz)
|
||||||
|
|
||||||
|
test_exercise_performance = executable(
|
||||||
|
'test_exercise_performance',
|
||||||
|
'tests/test_exercise_performance.c',
|
||||||
|
dependencies: trainlog_core_dep,
|
||||||
|
c_args: strict_c_args,
|
||||||
|
)
|
||||||
|
|
||||||
|
test(
|
||||||
|
'exercise_performance',
|
||||||
|
test_exercise_performance,
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -1910,3 +1910,283 @@ TrainlogStatus trainlog_database_latest_body_pair(
|
||||||
? TRAINLOG_STATUS_OK
|
? TRAINLOG_STATUS_OK
|
||||||
: TRAINLOG_STATUS_DATABASE_ERROR;
|
: TRAINLOG_STATUS_DATABASE_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* TRAINLOG_EXERCISE_PERFORMANCE_IMPLEMENTATION */
|
||||||
|
|
||||||
|
static TrainlogLoadMode performance_load_mode_from_sql(
|
||||||
|
const char *text
|
||||||
|
)
|
||||||
|
{
|
||||||
|
if (text != NULL &&
|
||||||
|
strcmp(text, "external") == 0) {
|
||||||
|
return TRAINLOG_LOAD_EXTERNAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (text != NULL &&
|
||||||
|
strcmp(text, "assistance") == 0) {
|
||||||
|
return TRAINLOG_LOAD_ASSISTANCE;
|
||||||
|
}
|
||||||
|
|
||||||
|
return TRAINLOG_LOAD_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool performance_candidate_better(
|
||||||
|
const TrainlogExercisePerformancePoint *current,
|
||||||
|
TrainlogLoadMode load_mode,
|
||||||
|
int metric_value,
|
||||||
|
int has_weight,
|
||||||
|
double weight_kg
|
||||||
|
)
|
||||||
|
{
|
||||||
|
if (current == NULL ||
|
||||||
|
metric_value <= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current->has_performance == 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (load_mode) {
|
||||||
|
case TRAINLOG_LOAD_EXTERNAL:
|
||||||
|
if (has_weight == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (weight_kg > current->weight_kg) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
weight_kg == current->weight_kg &&
|
||||||
|
metric_value > current->metric_value;
|
||||||
|
|
||||||
|
case TRAINLOG_LOAD_ASSISTANCE:
|
||||||
|
if (has_weight == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (weight_kg < current->weight_kg) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
weight_kg == current->weight_kg &&
|
||||||
|
metric_value > current->metric_value;
|
||||||
|
|
||||||
|
case TRAINLOG_LOAD_NONE:
|
||||||
|
default:
|
||||||
|
return metric_value > current->metric_value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TrainlogStatus trainlog_database_list_exercise_performance(
|
||||||
|
TrainlogDatabase *database,
|
||||||
|
const char *exercise_id,
|
||||||
|
TrainlogExercisePerformancePoint *output,
|
||||||
|
size_t capacity,
|
||||||
|
size_t *output_count
|
||||||
|
)
|
||||||
|
{
|
||||||
|
static const char *const SQL =
|
||||||
|
"SELECT "
|
||||||
|
"s.session_id, "
|
||||||
|
"s.started_at, "
|
||||||
|
"e.tracking_mode, "
|
||||||
|
"se.load_mode, "
|
||||||
|
"ps.id, "
|
||||||
|
"ps.reps, "
|
||||||
|
"ps.duration_seconds, "
|
||||||
|
"ps.weight_kg "
|
||||||
|
"FROM session_exercises AS se "
|
||||||
|
"JOIN sessions AS s "
|
||||||
|
" ON s.id = se.session_row_id "
|
||||||
|
"JOIN exercises AS e "
|
||||||
|
" ON e.id = se.exercise_row_id "
|
||||||
|
"LEFT JOIN performed_sets AS ps "
|
||||||
|
" ON ps.session_exercise_row_id = se.id "
|
||||||
|
"WHERE e.exercise_id = ?1 "
|
||||||
|
"ORDER BY "
|
||||||
|
"s.started_at DESC, "
|
||||||
|
"s.id DESC, "
|
||||||
|
"ps.position ASC;";
|
||||||
|
|
||||||
|
sqlite3_stmt *statement = NULL;
|
||||||
|
TrainlogExercisePerformancePoint *current = NULL;
|
||||||
|
char current_session_id[TRAINLOG_ID_MAX + 1U];
|
||||||
|
size_t copied = 0U;
|
||||||
|
int rc;
|
||||||
|
|
||||||
|
if (database == NULL ||
|
||||||
|
database->connection == NULL ||
|
||||||
|
exercise_id == NULL ||
|
||||||
|
exercise_id[0] == '\0' ||
|
||||||
|
output_count == NULL ||
|
||||||
|
(capacity > 0U && output == NULL)) {
|
||||||
|
return TRAINLOG_STATUS_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
*output_count = 0U;
|
||||||
|
current_session_id[0] = '\0';
|
||||||
|
|
||||||
|
rc = sqlite3_prepare_v2(
|
||||||
|
database->connection,
|
||||||
|
SQL,
|
||||||
|
-1,
|
||||||
|
&statement,
|
||||||
|
NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
if (rc != SQLITE_OK) {
|
||||||
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
rc = sqlite3_bind_text(
|
||||||
|
statement,
|
||||||
|
1,
|
||||||
|
exercise_id,
|
||||||
|
-1,
|
||||||
|
SQLITE_TRANSIENT
|
||||||
|
);
|
||||||
|
|
||||||
|
if (rc != SQLITE_OK) {
|
||||||
|
(void)sqlite3_finalize(statement);
|
||||||
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
while ((rc = sqlite3_step(statement)) == SQLITE_ROW) {
|
||||||
|
const unsigned char *session_id =
|
||||||
|
sqlite3_column_text(statement, 0);
|
||||||
|
|
||||||
|
const unsigned char *started_at =
|
||||||
|
sqlite3_column_text(statement, 1);
|
||||||
|
|
||||||
|
const unsigned char *tracking_mode =
|
||||||
|
sqlite3_column_text(statement, 2);
|
||||||
|
|
||||||
|
const unsigned char *load_mode =
|
||||||
|
sqlite3_column_text(statement, 3);
|
||||||
|
|
||||||
|
bool new_session;
|
||||||
|
|
||||||
|
if (session_id == NULL ||
|
||||||
|
started_at == NULL ||
|
||||||
|
tracking_mode == NULL ||
|
||||||
|
load_mode == NULL) {
|
||||||
|
(void)sqlite3_finalize(statement);
|
||||||
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
new_session =
|
||||||
|
current_session_id[0] == '\0' ||
|
||||||
|
strcmp(
|
||||||
|
current_session_id,
|
||||||
|
(const char *)session_id
|
||||||
|
) != 0;
|
||||||
|
|
||||||
|
if (new_session) {
|
||||||
|
(void)snprintf(
|
||||||
|
current_session_id,
|
||||||
|
sizeof(current_session_id),
|
||||||
|
"%s",
|
||||||
|
(const char *)session_id
|
||||||
|
);
|
||||||
|
|
||||||
|
current = NULL;
|
||||||
|
|
||||||
|
if (copied < capacity) {
|
||||||
|
current = &output[copied];
|
||||||
|
|
||||||
|
(void)memset(
|
||||||
|
current,
|
||||||
|
0,
|
||||||
|
sizeof(*current)
|
||||||
|
);
|
||||||
|
|
||||||
|
(void)snprintf(
|
||||||
|
current->session_id,
|
||||||
|
sizeof(current->session_id),
|
||||||
|
"%s",
|
||||||
|
(const char *)session_id
|
||||||
|
);
|
||||||
|
|
||||||
|
(void)snprintf(
|
||||||
|
current->started_at,
|
||||||
|
sizeof(current->started_at),
|
||||||
|
"%s",
|
||||||
|
(const char *)started_at
|
||||||
|
);
|
||||||
|
|
||||||
|
current->tracking_mode =
|
||||||
|
tracking_mode_from_sql(
|
||||||
|
(const char *)tracking_mode
|
||||||
|
);
|
||||||
|
|
||||||
|
current->load_mode =
|
||||||
|
performance_load_mode_from_sql(
|
||||||
|
(const char *)load_mode
|
||||||
|
);
|
||||||
|
|
||||||
|
++copied;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current != NULL &&
|
||||||
|
sqlite3_column_type(statement, 4) != SQLITE_NULL) {
|
||||||
|
int metric_value;
|
||||||
|
int has_weight;
|
||||||
|
double weight_kg;
|
||||||
|
|
||||||
|
++current->actual_set_count;
|
||||||
|
|
||||||
|
if (current->tracking_mode ==
|
||||||
|
TRAINLOG_TRACKING_REPS) {
|
||||||
|
metric_value =
|
||||||
|
sqlite3_column_type(statement, 5) !=
|
||||||
|
SQLITE_NULL
|
||||||
|
? sqlite3_column_int(statement, 5)
|
||||||
|
: 0;
|
||||||
|
} else {
|
||||||
|
metric_value =
|
||||||
|
sqlite3_column_type(statement, 6) !=
|
||||||
|
SQLITE_NULL
|
||||||
|
? sqlite3_column_int(statement, 6)
|
||||||
|
: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
has_weight =
|
||||||
|
sqlite3_column_type(statement, 7) !=
|
||||||
|
SQLITE_NULL;
|
||||||
|
|
||||||
|
weight_kg =
|
||||||
|
has_weight != 0
|
||||||
|
? sqlite3_column_double(statement, 7)
|
||||||
|
: 0.0;
|
||||||
|
|
||||||
|
if (performance_candidate_better(
|
||||||
|
current,
|
||||||
|
current->load_mode,
|
||||||
|
metric_value,
|
||||||
|
has_weight,
|
||||||
|
weight_kg
|
||||||
|
)) {
|
||||||
|
current->has_performance = 1;
|
||||||
|
current->metric_value = metric_value;
|
||||||
|
current->has_weight = has_weight;
|
||||||
|
current->weight_kg = weight_kg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rc != SQLITE_DONE) {
|
||||||
|
(void)sqlite3_finalize(statement);
|
||||||
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sqlite3_finalize(statement) != SQLITE_OK) {
|
||||||
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
*output_count = copied;
|
||||||
|
return TRAINLOG_STATUS_OK;
|
||||||
|
}
|
||||||
|
|
|
||||||
960
tui/src/tui.c
960
tui/src/tui.c
File diff suppressed because it is too large
Load diff
263
tui/tests/test_exercise_performance.c
Normal file
263
tui/tests/test_exercise_performance.c
Normal file
|
|
@ -0,0 +1,263 @@
|
||||||
|
/**
|
||||||
|
* @file test_exercise_performance.c
|
||||||
|
* @brief Representative exercise-performance history tests.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include "trainlog/database.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 void bind_exercise(
|
||||||
|
TrainlogSessionExerciseInput *exercise,
|
||||||
|
const char *exercise_id,
|
||||||
|
TrainlogLoadMode load_mode,
|
||||||
|
double target_weight,
|
||||||
|
TrainlogSetInput *sets,
|
||||||
|
size_t set_count
|
||||||
|
)
|
||||||
|
{
|
||||||
|
(void)memset(exercise, 0, sizeof(*exercise));
|
||||||
|
|
||||||
|
(void)snprintf(
|
||||||
|
exercise->exercise_id,
|
||||||
|
sizeof(exercise->exercise_id),
|
||||||
|
"%s",
|
||||||
|
exercise_id
|
||||||
|
);
|
||||||
|
|
||||||
|
exercise->load_mode = load_mode;
|
||||||
|
exercise->rest_seconds = 60;
|
||||||
|
exercise->target_sets = 2;
|
||||||
|
exercise->target_reps = 10;
|
||||||
|
exercise->target_has_weight =
|
||||||
|
load_mode != TRAINLOG_LOAD_NONE;
|
||||||
|
exercise->target_weight_kg = target_weight;
|
||||||
|
exercise->sets = sets;
|
||||||
|
exercise->set_count = set_count;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool test_performance_semantics(void)
|
||||||
|
{
|
||||||
|
TrainlogDatabase *database = NULL;
|
||||||
|
|
||||||
|
TrainlogSetInput external_sets[2];
|
||||||
|
TrainlogSetInput assistance_sets[2];
|
||||||
|
TrainlogSetInput bodyweight_sets[2];
|
||||||
|
|
||||||
|
TrainlogSessionExerciseInput exercises[3];
|
||||||
|
TrainlogSessionInput session;
|
||||||
|
|
||||||
|
TrainlogExercisePerformancePoint points[8];
|
||||||
|
size_t count = 0U;
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
trainlog_database_open(
|
||||||
|
":memory:",
|
||||||
|
&database
|
||||||
|
) == TRAINLOG_STATUS_OK
|
||||||
|
);
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
trainlog_database_insert_exercise(
|
||||||
|
database,
|
||||||
|
"ex_external",
|
||||||
|
"External",
|
||||||
|
"external",
|
||||||
|
TRAINLOG_TRACKING_REPS
|
||||||
|
) == TRAINLOG_STATUS_OK
|
||||||
|
);
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
trainlog_database_insert_exercise(
|
||||||
|
database,
|
||||||
|
"ex_assistance",
|
||||||
|
"Assistance",
|
||||||
|
"assistance",
|
||||||
|
TRAINLOG_TRACKING_REPS
|
||||||
|
) == TRAINLOG_STATUS_OK
|
||||||
|
);
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
trainlog_database_insert_exercise(
|
||||||
|
database,
|
||||||
|
"ex_none",
|
||||||
|
"Bodyweight",
|
||||||
|
"bodyweight",
|
||||||
|
TRAINLOG_TRACKING_REPS
|
||||||
|
) == TRAINLOG_STATUS_OK
|
||||||
|
);
|
||||||
|
|
||||||
|
(void)memset(
|
||||||
|
external_sets,
|
||||||
|
0,
|
||||||
|
sizeof(external_sets)
|
||||||
|
);
|
||||||
|
|
||||||
|
external_sets[0].reps = 10;
|
||||||
|
external_sets[0].has_weight = true;
|
||||||
|
external_sets[0].weight_kg = 80.0;
|
||||||
|
|
||||||
|
external_sets[1].reps = 5;
|
||||||
|
external_sets[1].has_weight = true;
|
||||||
|
external_sets[1].weight_kg = 90.0;
|
||||||
|
|
||||||
|
(void)memset(
|
||||||
|
assistance_sets,
|
||||||
|
0,
|
||||||
|
sizeof(assistance_sets)
|
||||||
|
);
|
||||||
|
|
||||||
|
assistance_sets[0].reps = 10;
|
||||||
|
assistance_sets[0].has_weight = true;
|
||||||
|
assistance_sets[0].weight_kg = 40.0;
|
||||||
|
|
||||||
|
assistance_sets[1].reps = 5;
|
||||||
|
assistance_sets[1].has_weight = true;
|
||||||
|
assistance_sets[1].weight_kg = 30.0;
|
||||||
|
|
||||||
|
(void)memset(
|
||||||
|
bodyweight_sets,
|
||||||
|
0,
|
||||||
|
sizeof(bodyweight_sets)
|
||||||
|
);
|
||||||
|
|
||||||
|
bodyweight_sets[0].reps = 8;
|
||||||
|
bodyweight_sets[1].reps = 12;
|
||||||
|
|
||||||
|
bind_exercise(
|
||||||
|
&exercises[0],
|
||||||
|
"ex_external",
|
||||||
|
TRAINLOG_LOAD_EXTERNAL,
|
||||||
|
80.0,
|
||||||
|
external_sets,
|
||||||
|
2U
|
||||||
|
);
|
||||||
|
|
||||||
|
bind_exercise(
|
||||||
|
&exercises[1],
|
||||||
|
"ex_assistance",
|
||||||
|
TRAINLOG_LOAD_ASSISTANCE,
|
||||||
|
40.0,
|
||||||
|
assistance_sets,
|
||||||
|
2U
|
||||||
|
);
|
||||||
|
|
||||||
|
bind_exercise(
|
||||||
|
&exercises[2],
|
||||||
|
"ex_none",
|
||||||
|
TRAINLOG_LOAD_NONE,
|
||||||
|
0.0,
|
||||||
|
bodyweight_sets,
|
||||||
|
2U
|
||||||
|
);
|
||||||
|
|
||||||
|
(void)memset(&session, 0, sizeof(session));
|
||||||
|
|
||||||
|
(void)snprintf(
|
||||||
|
session.session_id,
|
||||||
|
sizeof(session.session_id),
|
||||||
|
"%s",
|
||||||
|
"se_performance"
|
||||||
|
);
|
||||||
|
|
||||||
|
(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 = exercises;
|
||||||
|
session.exercise_count = 3U;
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
trainlog_database_insert_session(
|
||||||
|
database,
|
||||||
|
&session
|
||||||
|
) == TRAINLOG_STATUS_OK
|
||||||
|
);
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
trainlog_database_list_exercise_performance(
|
||||||
|
database,
|
||||||
|
"ex_external",
|
||||||
|
points,
|
||||||
|
8U,
|
||||||
|
&count
|
||||||
|
) == TRAINLOG_STATUS_OK
|
||||||
|
);
|
||||||
|
|
||||||
|
CHECK(count == 1U);
|
||||||
|
CHECK(points[0].has_performance != 0);
|
||||||
|
CHECK(points[0].weight_kg > 89.99);
|
||||||
|
CHECK(points[0].weight_kg < 90.01);
|
||||||
|
CHECK(points[0].metric_value == 5);
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
trainlog_database_list_exercise_performance(
|
||||||
|
database,
|
||||||
|
"ex_assistance",
|
||||||
|
points,
|
||||||
|
8U,
|
||||||
|
&count
|
||||||
|
) == TRAINLOG_STATUS_OK
|
||||||
|
);
|
||||||
|
|
||||||
|
CHECK(count == 1U);
|
||||||
|
CHECK(points[0].has_performance != 0);
|
||||||
|
CHECK(points[0].weight_kg > 29.99);
|
||||||
|
CHECK(points[0].weight_kg < 30.01);
|
||||||
|
CHECK(points[0].metric_value == 5);
|
||||||
|
|
||||||
|
CHECK(
|
||||||
|
trainlog_database_list_exercise_performance(
|
||||||
|
database,
|
||||||
|
"ex_none",
|
||||||
|
points,
|
||||||
|
8U,
|
||||||
|
&count
|
||||||
|
) == TRAINLOG_STATUS_OK
|
||||||
|
);
|
||||||
|
|
||||||
|
CHECK(count == 1U);
|
||||||
|
CHECK(points[0].has_performance != 0);
|
||||||
|
CHECK(points[0].metric_value == 12);
|
||||||
|
CHECK(points[0].has_weight == 0);
|
||||||
|
|
||||||
|
trainlog_database_close(database);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
CHECK(test_performance_semantics());
|
||||||
|
|
||||||
|
(void)printf(
|
||||||
|
"PASS exercise_performance\n"
|
||||||
|
);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue