Add profile-aware continuous exercise tracking
This commit is contained in:
parent
a3eeb91869
commit
db349c609d
17 changed files with 2677 additions and 280 deletions
|
|
@ -260,3 +260,27 @@ Transport/export is added only after the local recorder workflow is comfortable.
|
|||
The Android client must preserve stable exercise IDs so a catalog snapshot from
|
||||
the PC can normalize exercise selection on both sides.
|
||||
<!-- TRAINLOG_ANDROID_NEXT_SLICE _END -->
|
||||
|
||||
<!-- TRAINLOG_ANDROID_PROFILE_AWARE_ENTRY -->
|
||||
## Profile-aware Android forms
|
||||
|
||||
Android uses the same exercise metadata as the TUI:
|
||||
|
||||
```text
|
||||
recording_mode
|
||||
tracking_mode
|
||||
data_fields
|
||||
```
|
||||
|
||||
A continuous `Marche` configured with speed displays only:
|
||||
|
||||
```text
|
||||
Durée
|
||||
Vitesse
|
||||
```
|
||||
|
||||
and no set count.
|
||||
|
||||
Creating an exercise directly inside session entry must configure this metadata
|
||||
before adding it to the catalog/session.
|
||||
<!-- TRAINLOG_ANDROID_PROFILE_AWARE_ENTRY _END -->
|
||||
|
|
|
|||
|
|
@ -282,3 +282,25 @@ python tools/validate_json.py
|
|||
python tools/validate_import_contract.py
|
||||
git diff --check
|
||||
```
|
||||
|
||||
<!-- TRAINLOG_EXERCISE_DATA_MODEL_V1 -->
|
||||
## Exercise recording metadata — schema v3 direction
|
||||
|
||||
The next SQLite migration adds:
|
||||
|
||||
```text
|
||||
recording_mode = sets | continuous
|
||||
data_fields = bounded bit mask
|
||||
```
|
||||
|
||||
Existing `tracking_mode = reps | duration` remains stable.
|
||||
|
||||
Every `session_exercises` row snapshots this metadata so later catalog changes
|
||||
do not reinterpret historical sessions.
|
||||
|
||||
Migration v2 -> v3 defaults all existing rows to `sets` with `data_fields = 0`.
|
||||
No name-based migration is allowed.
|
||||
|
||||
Continuous actual activity data is stored separately from `performed_sets`;
|
||||
Trainlog will not manufacture a fake one-set workout.
|
||||
<!-- TRAINLOG_EXERCISE_DATA_MODEL_V1 _END -->
|
||||
|
|
|
|||
195
docs/exercise_data_model.md
Normal file
195
docs/exercise_data_model.md
Normal file
|
|
@ -0,0 +1,195 @@
|
|||
# Exercise data model
|
||||
|
||||
## Status
|
||||
|
||||
```text
|
||||
EXERCISE_DATA_MODEL_V1=FROZEN_FOR_IMPLEMENTATION
|
||||
DATABASE_SCHEMA_V3=NEXT
|
||||
TUI_PROFILE_AWARE_ENTRY=AFTER_SCHEMA_V3
|
||||
ANDROID_PROFILE_AWARE_ENTRY=AFTER_TUI
|
||||
TRAINLOG_FORMAT_V1=FROZEN
|
||||
SESSION_EXCHANGE_V2=DESIGN_REQUIRED_LATER
|
||||
```
|
||||
|
||||
Trainlog must not use one universal exercise form.
|
||||
|
||||
An exercise is defined by three independent pieces of metadata:
|
||||
|
||||
```text
|
||||
recording_mode = SETS | CONTINUOUS
|
||||
tracking_mode = REPS | DURATION
|
||||
data_fields = supplemental field bit mask
|
||||
```
|
||||
|
||||
Initial valid combinations:
|
||||
|
||||
```text
|
||||
SETS + REPS
|
||||
SETS + DURATION
|
||||
CONTINUOUS + DURATION
|
||||
```
|
||||
|
||||
`CONTINUOUS + REPS` is invalid in model v1.
|
||||
|
||||
Initial supplemental fields:
|
||||
|
||||
```text
|
||||
SPEED_KMH
|
||||
DISTANCE_KM
|
||||
```
|
||||
|
||||
Unknown field bits are invalid.
|
||||
|
||||
Examples:
|
||||
|
||||
```text
|
||||
Presse à cuisses
|
||||
SETS + REPS
|
||||
|
||||
Gainage
|
||||
SETS + DURATION
|
||||
|
||||
Marche
|
||||
CONTINUOUS + DURATION
|
||||
SPEED_KMH
|
||||
|
||||
Course
|
||||
CONTINUOUS + DURATION
|
||||
SPEED_KMH
|
||||
|
||||
Vélo
|
||||
CONTINUOUS + DURATION
|
||||
SPEED_KMH | DISTANCE_KM
|
||||
|
||||
Rameur
|
||||
CONTINUOUS + DURATION
|
||||
DISTANCE_KM
|
||||
```
|
||||
|
||||
Load semantics remain separate and session-specific:
|
||||
|
||||
```text
|
||||
none
|
||||
external
|
||||
assistance
|
||||
```
|
||||
|
||||
A continuous exercise does not ask for:
|
||||
|
||||
```text
|
||||
number of sets
|
||||
repetitions
|
||||
per-set rest
|
||||
```
|
||||
|
||||
For example:
|
||||
|
||||
```text
|
||||
Marche
|
||||
|
||||
Durée 45 min
|
||||
Vitesse 5.8 km/h
|
||||
```
|
||||
|
||||
Creating/editing an exercise asks for:
|
||||
|
||||
```text
|
||||
Name
|
||||
Organization: Sets | Continuous
|
||||
Primary metric: Repetitions | Duration
|
||||
Supplemental fields: Speed | Distance
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- continuous forces duration in model v1;
|
||||
- sets accepts reps or duration;
|
||||
- UI fields are driven by metadata, never exercise-name heuristics;
|
||||
- changing catalog metadata affects future entry only.
|
||||
|
||||
Historical stability:
|
||||
|
||||
Every session exercise stores a snapshot of:
|
||||
|
||||
```text
|
||||
recording_mode
|
||||
tracking_mode
|
||||
data_fields
|
||||
```
|
||||
|
||||
So changing `Marche` from an old set-based duration exercise to continuous
|
||||
duration + speed does not reinterpret old sessions.
|
||||
|
||||
## SQLite schema v3 direction
|
||||
|
||||
Schema v3 adds to `exercises`:
|
||||
|
||||
```text
|
||||
recording_mode
|
||||
data_fields
|
||||
```
|
||||
|
||||
and snapshots the same values in `session_exercises`.
|
||||
|
||||
Migration v2 -> v3 is conservative:
|
||||
|
||||
```text
|
||||
all existing exercises -> SETS
|
||||
all existing session exercises -> SETS
|
||||
data_fields -> 0
|
||||
```
|
||||
|
||||
No migration guesses by exercise name.
|
||||
|
||||
Continuous actual activity data gets its own one-to-one record:
|
||||
|
||||
```text
|
||||
duration_seconds
|
||||
speed_kmh nullable
|
||||
distance_km nullable
|
||||
```
|
||||
|
||||
Continuous activities have no `performed_sets` rows and no fake one-set
|
||||
representation.
|
||||
|
||||
## Frozen JSON v1
|
||||
|
||||
Trainlog JSON session v1 remains frozen.
|
||||
|
||||
It represents the existing set-based exchange model.
|
||||
|
||||
Continuous data that cannot be represented in v1 must not be:
|
||||
|
||||
- hidden in notes;
|
||||
- converted into a fake set;
|
||||
- silently discarded.
|
||||
|
||||
A future explicit session exchange v2 will carry profile-aware exercise data
|
||||
while v1 import remains supported.
|
||||
|
||||
## Android/TUI parity
|
||||
|
||||
Both interfaces consume identical exercise metadata.
|
||||
|
||||
The future PC -> Android catalog snapshot must contain:
|
||||
|
||||
```text
|
||||
exercise_id
|
||||
name
|
||||
recording_mode
|
||||
tracking_mode
|
||||
data_fields
|
||||
```
|
||||
|
||||
## Implementation order
|
||||
|
||||
```text
|
||||
1. SQLite schema v3 + migration tests
|
||||
2. C model/API additions
|
||||
3. catalog create/edit support
|
||||
4. TUI profile-aware session entry
|
||||
5. manually convert Marche to CONTINUOUS + SPEED_KMH
|
||||
6. profile-aware detail/history
|
||||
7. Android uses the same model
|
||||
8. design session exchange v2
|
||||
```
|
||||
|
|
@ -285,3 +285,27 @@ Fictitious sessions, exercises, and body observations are used during Android
|
|||
development and synchronization testing. The development database will be
|
||||
purged before normal production use begins.
|
||||
<!-- TRAINLOG_SYNC_FINAL_ROADMAP _END -->
|
||||
|
||||
<!-- TRAINLOG_EXERCISE_DATA_MODEL_ROADMAP -->
|
||||
## Exercise data-model checkpoint
|
||||
|
||||
```text
|
||||
EXERCISE_DATA_MODEL_V1=FROZEN_FOR_IMPLEMENTATION
|
||||
DATABASE_SCHEMA_V3=NEXT
|
||||
PROFILE_AWARE_C_MODEL=AFTER_SCHEMA
|
||||
PROFILE_AWARE_TUI=AFTER
|
||||
ANDROID_PROFILE_AWARE_UI=AFTER
|
||||
SESSION_EXCHANGE_V2=DESIGN_LATER
|
||||
TRAINLOG_FORMAT_V1=FROZEN
|
||||
```
|
||||
|
||||
The model separates:
|
||||
|
||||
```text
|
||||
recording organization: SETS | CONTINUOUS
|
||||
primary metric: REPS | DURATION
|
||||
supplemental fields: SPEED_KMH | DISTANCE_KM
|
||||
```
|
||||
|
||||
Existing schema-v2 data migrates conservatively to `SETS`.
|
||||
<!-- TRAINLOG_EXERCISE_DATA_MODEL_ROADMAP _END -->
|
||||
|
|
|
|||
22
docs/tui.md
22
docs/tui.md
|
|
@ -674,3 +674,25 @@ PC -> Android
|
|||
The session exchange remains frozen Trainlog JSON v1.
|
||||
Catalog synchronization is a separate versioned contract.
|
||||
<!-- TRAINLOG_SYNC_FINAL_CHECKPOINT _END -->
|
||||
|
||||
<!-- TRAINLOG_PROFILE_AWARE_ENTRY -->
|
||||
## Profile-aware exercise entry
|
||||
|
||||
The TUI form is driven by exercise metadata.
|
||||
|
||||
```text
|
||||
SETS + REPS
|
||||
series, reps, optional load, rest
|
||||
|
||||
SETS + DURATION
|
||||
series, duration, optional load, rest
|
||||
|
||||
CONTINUOUS + DURATION + SPEED_KMH
|
||||
duration, speed
|
||||
```
|
||||
|
||||
Continuous exercises do not display a set count.
|
||||
|
||||
`Marche` will use the continuous form only after its catalog metadata is
|
||||
explicitly changed; behavior is never inferred from its name.
|
||||
<!-- TRAINLOG_PROFILE_AWARE_ENTRY _END -->
|
||||
|
|
|
|||
|
|
@ -40,4 +40,13 @@ TrainlogStatus trainlog_catalog_create_exercise(
|
|||
TrainlogExercise *output_exercise
|
||||
);
|
||||
|
||||
TrainlogStatus trainlog_catalog_create_exercise_profiled(
|
||||
TrainlogDatabase *database,
|
||||
const char *name,
|
||||
TrainlogTrackingMode tracking_mode,
|
||||
TrainlogRecordingMode recording_mode,
|
||||
TrainlogExerciseDataFields data_fields,
|
||||
TrainlogExercise *output_exercise
|
||||
);
|
||||
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
#include "trainlog/model.h"
|
||||
#include "trainlog/status.h"
|
||||
|
||||
#define TRAINLOG_DATABASE_SCHEMA_VERSION 2
|
||||
#define TRAINLOG_DATABASE_SCHEMA_VERSION 4
|
||||
|
||||
typedef struct TrainlogDatabase TrainlogDatabase;
|
||||
|
||||
|
|
@ -50,6 +50,16 @@ TrainlogStatus trainlog_database_insert_exercise(
|
|||
TrainlogTrackingMode tracking_mode
|
||||
);
|
||||
|
||||
TrainlogStatus trainlog_database_insert_exercise_profiled(
|
||||
TrainlogDatabase *database,
|
||||
const char *exercise_id,
|
||||
const char *name,
|
||||
const char *normalized_name,
|
||||
TrainlogTrackingMode tracking_mode,
|
||||
TrainlogRecordingMode recording_mode,
|
||||
TrainlogExerciseDataFields data_fields
|
||||
);
|
||||
|
||||
TrainlogStatus trainlog_database_exercise_count(
|
||||
TrainlogDatabase *database,
|
||||
size_t *output_count
|
||||
|
|
@ -99,6 +109,8 @@ TrainlogStatus trainlog_database_list_weight_points(
|
|||
typedef struct TrainlogPersistedExerciseDetail {
|
||||
char name[TRAINLOG_NAME_MAX + 1U];
|
||||
TrainlogTrackingMode tracking_mode;
|
||||
TrainlogRecordingMode recording_mode;
|
||||
TrainlogExerciseDataFields data_fields;
|
||||
TrainlogLoadMode load_mode;
|
||||
int rest_seconds;
|
||||
int target_sets;
|
||||
|
|
@ -106,6 +118,13 @@ typedef struct TrainlogPersistedExerciseDetail {
|
|||
int target_duration_seconds;
|
||||
int has_target_weight;
|
||||
double target_weight_kg;
|
||||
|
||||
int continuous_duration_seconds;
|
||||
int has_continuous_speed;
|
||||
double continuous_speed_kmh;
|
||||
int has_continuous_distance;
|
||||
double continuous_distance_km;
|
||||
|
||||
size_t actual_set_count;
|
||||
char actual_summary[TRAINLOG_SET_SUMMARY_MAX + 1U];
|
||||
} TrainlogPersistedExerciseDetail;
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#define TRAINLOG_ID_MAX 128U
|
||||
#define TRAINLOG_NAME_MAX 200U
|
||||
|
|
@ -19,6 +20,19 @@ typedef enum TrainlogTrackingMode {
|
|||
TRAINLOG_TRACKING_DURATION
|
||||
} TrainlogTrackingMode;
|
||||
|
||||
typedef enum TrainlogRecordingMode {
|
||||
TRAINLOG_RECORDING_SETS = 0,
|
||||
TRAINLOG_RECORDING_CONTINUOUS
|
||||
} TrainlogRecordingMode;
|
||||
|
||||
typedef uint32_t TrainlogExerciseDataFields;
|
||||
|
||||
#define TRAINLOG_EXERCISE_DATA_SPEED_KMH UINT32_C(1)
|
||||
#define TRAINLOG_EXERCISE_DATA_DISTANCE_KM UINT32_C(2)
|
||||
#define TRAINLOG_EXERCISE_DATA_KNOWN_MASK \
|
||||
(TRAINLOG_EXERCISE_DATA_SPEED_KMH | \
|
||||
TRAINLOG_EXERCISE_DATA_DISTANCE_KM)
|
||||
|
||||
typedef enum TrainlogLoadMode {
|
||||
TRAINLOG_LOAD_NONE = 0,
|
||||
TRAINLOG_LOAD_EXTERNAL,
|
||||
|
|
@ -34,6 +48,8 @@ typedef struct TrainlogExercise {
|
|||
char exercise_id[TRAINLOG_ID_MAX + 1U];
|
||||
char name[TRAINLOG_NAME_MAX + 1U];
|
||||
TrainlogTrackingMode tracking_mode;
|
||||
TrainlogRecordingMode recording_mode;
|
||||
TrainlogExerciseDataFields data_fields;
|
||||
} TrainlogExercise;
|
||||
|
||||
typedef struct TrainlogSetInput {
|
||||
|
|
@ -45,6 +61,8 @@ typedef struct TrainlogSetInput {
|
|||
|
||||
typedef struct TrainlogSessionExerciseInput {
|
||||
char exercise_id[TRAINLOG_ID_MAX + 1U];
|
||||
TrainlogRecordingMode recording_mode;
|
||||
TrainlogExerciseDataFields data_fields;
|
||||
TrainlogLoadMode load_mode;
|
||||
int rest_seconds;
|
||||
int target_sets;
|
||||
|
|
@ -52,6 +70,13 @@ typedef struct TrainlogSessionExerciseInput {
|
|||
int target_duration_seconds;
|
||||
bool target_has_weight;
|
||||
double target_weight_kg;
|
||||
|
||||
int continuous_duration_seconds;
|
||||
bool continuous_has_speed;
|
||||
double continuous_speed_kmh;
|
||||
bool continuous_has_distance;
|
||||
double continuous_distance_km;
|
||||
|
||||
const char *notes;
|
||||
const TrainlogSetInput *sets;
|
||||
size_t set_count;
|
||||
|
|
|
|||
|
|
@ -243,3 +243,39 @@ trainlog_mtp_roundtrip_probe = executable(
|
|||
dependencies: trainlog_core_dep,
|
||||
c_args: strict_c_args,
|
||||
)
|
||||
|
||||
test_exercise_profile_schema = executable(
|
||||
'test_exercise_profile_schema',
|
||||
'tests/test_exercise_profile_schema.c',
|
||||
dependencies: trainlog_core_dep,
|
||||
c_args: strict_c_args,
|
||||
)
|
||||
|
||||
test(
|
||||
'exercise_profile_schema',
|
||||
test_exercise_profile_schema,
|
||||
)
|
||||
|
||||
test_continuous_session = executable(
|
||||
'test_continuous_session',
|
||||
'tests/test_continuous_session.c',
|
||||
dependencies: trainlog_core_dep,
|
||||
c_args: strict_c_args,
|
||||
)
|
||||
|
||||
test(
|
||||
'continuous_session',
|
||||
test_continuous_session,
|
||||
)
|
||||
|
||||
test_continuous_detail = executable(
|
||||
'test_continuous_detail',
|
||||
'tests/test_continuous_detail.c',
|
||||
dependencies: trainlog_core_dep,
|
||||
c_args: strict_c_args,
|
||||
)
|
||||
|
||||
test(
|
||||
'continuous_detail',
|
||||
test_continuous_detail,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -131,10 +131,12 @@ TrainlogStatus trainlog_catalog_normalize_name(
|
|||
return TRAINLOG_STATUS_OK;
|
||||
}
|
||||
|
||||
TrainlogStatus trainlog_catalog_create_exercise(
|
||||
TrainlogStatus trainlog_catalog_create_exercise_profiled(
|
||||
TrainlogDatabase *database,
|
||||
const char *name,
|
||||
TrainlogTrackingMode tracking_mode,
|
||||
TrainlogRecordingMode recording_mode,
|
||||
TrainlogExerciseDataFields data_fields,
|
||||
TrainlogExercise *output_exercise
|
||||
)
|
||||
{
|
||||
|
|
@ -154,6 +156,7 @@ TrainlogStatus trainlog_catalog_create_exercise(
|
|||
normalized,
|
||||
sizeof(normalized)
|
||||
);
|
||||
|
||||
if (status != TRAINLOG_STATUS_OK) {
|
||||
return status;
|
||||
}
|
||||
|
|
@ -163,35 +166,65 @@ TrainlogStatus trainlog_catalog_create_exercise(
|
|||
exercise_id,
|
||||
sizeof(exercise_id)
|
||||
);
|
||||
|
||||
if (status != TRAINLOG_STATUS_OK) {
|
||||
return status;
|
||||
}
|
||||
|
||||
status = trainlog_database_insert_exercise(
|
||||
status = trainlog_database_insert_exercise_profiled(
|
||||
database,
|
||||
exercise_id,
|
||||
name,
|
||||
normalized,
|
||||
tracking_mode
|
||||
tracking_mode,
|
||||
recording_mode,
|
||||
data_fields
|
||||
);
|
||||
|
||||
if (status != TRAINLOG_STATUS_OK) {
|
||||
return status;
|
||||
}
|
||||
|
||||
(void)memset(output_exercise, 0, sizeof(*output_exercise));
|
||||
(void)memset(
|
||||
output_exercise,
|
||||
0,
|
||||
sizeof(*output_exercise)
|
||||
);
|
||||
|
||||
(void)snprintf(
|
||||
output_exercise->exercise_id,
|
||||
sizeof(output_exercise->exercise_id),
|
||||
"%s",
|
||||
exercise_id
|
||||
);
|
||||
|
||||
(void)snprintf(
|
||||
output_exercise->name,
|
||||
sizeof(output_exercise->name),
|
||||
"%s",
|
||||
name
|
||||
);
|
||||
|
||||
output_exercise->tracking_mode = tracking_mode;
|
||||
output_exercise->recording_mode = recording_mode;
|
||||
output_exercise->data_fields = data_fields;
|
||||
|
||||
return TRAINLOG_STATUS_OK;
|
||||
}
|
||||
|
||||
TrainlogStatus trainlog_catalog_create_exercise(
|
||||
TrainlogDatabase *database,
|
||||
const char *name,
|
||||
TrainlogTrackingMode tracking_mode,
|
||||
TrainlogExercise *output_exercise
|
||||
)
|
||||
{
|
||||
return trainlog_catalog_create_exercise_profiled(
|
||||
database,
|
||||
name,
|
||||
tracking_mode,
|
||||
TRAINLOG_RECORDING_SETS,
|
||||
0U,
|
||||
output_exercise
|
||||
);
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
711
tui/src/tui.c
711
tui/src/tui.c
|
|
@ -38,6 +38,7 @@
|
|||
|
||||
|
||||
/* TRAINLOG_TUI_V02_POLISH */
|
||||
/* TRAINLOG_TUI_PROFILED_EXERCISE_CREATION */
|
||||
|
||||
typedef enum DashboardAction {
|
||||
DASHBOARD_NEW_SESSION = 0,
|
||||
|
|
@ -1756,7 +1757,11 @@ if (primary_top_nav_activate(
|
|||
if (key == 'a' ||
|
||||
key == 'A') {
|
||||
char name[TRAINLOG_NAME_MAX + 1U];
|
||||
int mode = 1;
|
||||
int mode = 1;
|
||||
int organization = 1;
|
||||
int wants_speed = 0;
|
||||
int wants_distance = 0;
|
||||
TrainlogExerciseDataFields data_fields = 0U;
|
||||
TrainlogExercise created;
|
||||
TrainlogStatus status;
|
||||
|
||||
|
|
@ -1786,14 +1791,64 @@ if (primary_top_nav_activate(
|
|||
continue;
|
||||
}
|
||||
|
||||
status =
|
||||
trainlog_catalog_create_exercise(
|
||||
if (!prompt_int_value(
|
||||
8,
|
||||
"Organisation (1 séries, 2 continu)",
|
||||
1,
|
||||
2,
|
||||
1,
|
||||
&organization
|
||||
)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (organization == 2) {
|
||||
mode = 2;
|
||||
|
||||
if (!prompt_int_value(
|
||||
10,
|
||||
"Mesurer la vitesse km/h ? (0 non, 1 oui)",
|
||||
0,
|
||||
1,
|
||||
1,
|
||||
&wants_speed
|
||||
)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!prompt_int_value(
|
||||
12,
|
||||
"Mesurer la distance km ? (0 non, 1 oui)",
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
&wants_distance
|
||||
)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (wants_speed != 0) {
|
||||
data_fields |=
|
||||
TRAINLOG_EXERCISE_DATA_SPEED_KMH;
|
||||
}
|
||||
|
||||
if (wants_distance != 0) {
|
||||
data_fields |=
|
||||
TRAINLOG_EXERCISE_DATA_DISTANCE_KM;
|
||||
}
|
||||
}
|
||||
status =
|
||||
trainlog_catalog_create_exercise_profiled(
|
||||
database,
|
||||
name,
|
||||
mode == 1
|
||||
? TRAINLOG_TRACKING_REPS
|
||||
: TRAINLOG_TRACKING_DURATION,
|
||||
&created
|
||||
organization == 2
|
||||
? TRAINLOG_RECORDING_CONTINUOUS
|
||||
: TRAINLOG_RECORDING_SETS,
|
||||
data_fields,
|
||||
&created
|
||||
);
|
||||
|
||||
if (status ==
|
||||
|
|
@ -1827,7 +1882,11 @@ static bool create_exercise_inline(
|
|||
)
|
||||
{
|
||||
char name[TRAINLOG_NAME_MAX + 1U];
|
||||
int mode = 1;
|
||||
int mode = 1;
|
||||
int organization = 1;
|
||||
int wants_speed = 0;
|
||||
int wants_distance = 0;
|
||||
TrainlogExerciseDataFields data_fields = 0U;
|
||||
TrainlogExercise created;
|
||||
TrainlogStatus status;
|
||||
|
||||
|
|
@ -1857,14 +1916,64 @@ static bool create_exercise_inline(
|
|||
return false;
|
||||
}
|
||||
|
||||
status =
|
||||
trainlog_catalog_create_exercise(
|
||||
if (!prompt_int_value(
|
||||
8,
|
||||
"Organisation (1 séries, 2 continu)",
|
||||
1,
|
||||
2,
|
||||
1,
|
||||
&organization
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (organization == 2) {
|
||||
mode = 2;
|
||||
|
||||
if (!prompt_int_value(
|
||||
10,
|
||||
"Mesurer la vitesse km/h ? (0 non, 1 oui)",
|
||||
0,
|
||||
1,
|
||||
1,
|
||||
&wants_speed
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!prompt_int_value(
|
||||
12,
|
||||
"Mesurer la distance km ? (0 non, 1 oui)",
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
&wants_distance
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (wants_speed != 0) {
|
||||
data_fields |=
|
||||
TRAINLOG_EXERCISE_DATA_SPEED_KMH;
|
||||
}
|
||||
|
||||
if (wants_distance != 0) {
|
||||
data_fields |=
|
||||
TRAINLOG_EXERCISE_DATA_DISTANCE_KM;
|
||||
}
|
||||
}
|
||||
status =
|
||||
trainlog_catalog_create_exercise_profiled(
|
||||
database,
|
||||
name,
|
||||
mode == 1
|
||||
? TRAINLOG_TRACKING_REPS
|
||||
: TRAINLOG_TRACKING_DURATION,
|
||||
&created
|
||||
organization == 2
|
||||
? TRAINLOG_RECORDING_CONTINUOUS
|
||||
: TRAINLOG_RECORDING_SETS,
|
||||
data_fields,
|
||||
&created
|
||||
);
|
||||
|
||||
if (status == TRAINLOG_STATUS_OK) {
|
||||
|
|
@ -4378,12 +4487,119 @@ static bool build_session_exercise(
|
|||
double target_weight = 0.0;
|
||||
size_t set_index;
|
||||
|
||||
if (!choose_exercise(database, &exercise)) {
|
||||
if (!choose_exercise(
|
||||
database,
|
||||
&exercise
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
(void)memset(
|
||||
output,
|
||||
0,
|
||||
sizeof(*output)
|
||||
);
|
||||
|
||||
(void)snprintf(
|
||||
output->exercise_id,
|
||||
sizeof(output->exercise_id),
|
||||
"%s",
|
||||
exercise.exercise_id
|
||||
);
|
||||
|
||||
output->recording_mode =
|
||||
exercise.recording_mode;
|
||||
|
||||
output->data_fields =
|
||||
exercise.data_fields;
|
||||
|
||||
if (exercise.recording_mode ==
|
||||
TRAINLOG_RECORDING_CONTINUOUS) {
|
||||
int duration_minutes = 30;
|
||||
int duration_seconds;
|
||||
bool has_value = false;
|
||||
double value = 0.0;
|
||||
|
||||
draw_shell(
|
||||
exercise.name,
|
||||
"Échap annuler · activité continue"
|
||||
);
|
||||
|
||||
if (!prompt_int_value(
|
||||
4,
|
||||
"Durée (minutes)",
|
||||
1,
|
||||
1440,
|
||||
duration_minutes,
|
||||
&duration_minutes
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
duration_seconds =
|
||||
duration_minutes * 60;
|
||||
|
||||
output->continuous_duration_seconds =
|
||||
duration_seconds;
|
||||
|
||||
output->load_mode =
|
||||
TRAINLOG_LOAD_NONE;
|
||||
|
||||
output->rest_seconds = 0;
|
||||
output->target_sets = 0;
|
||||
output->target_reps = 0;
|
||||
output->target_duration_seconds = 0;
|
||||
output->target_has_weight = false;
|
||||
output->sets = NULL;
|
||||
output->set_count = 0U;
|
||||
|
||||
if ((exercise.data_fields &
|
||||
TRAINLOG_EXERCISE_DATA_SPEED_KMH) != 0U) {
|
||||
if (!prompt_optional_double(
|
||||
5,
|
||||
"Vitesse km/h : ",
|
||||
&has_value,
|
||||
&value
|
||||
) ||
|
||||
!has_value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
output->continuous_has_speed =
|
||||
true;
|
||||
|
||||
output->continuous_speed_kmh =
|
||||
value;
|
||||
}
|
||||
|
||||
if ((exercise.data_fields &
|
||||
TRAINLOG_EXERCISE_DATA_DISTANCE_KM) != 0U) {
|
||||
has_value = false;
|
||||
value = 0.0;
|
||||
|
||||
if (!prompt_optional_double(
|
||||
6,
|
||||
"Distance km : ",
|
||||
&has_value,
|
||||
&value
|
||||
) ||
|
||||
!has_value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
output->continuous_has_distance =
|
||||
true;
|
||||
|
||||
output->continuous_distance_km =
|
||||
value;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
target_metric =
|
||||
exercise.tracking_mode == TRAINLOG_TRACKING_REPS
|
||||
exercise.tracking_mode ==
|
||||
TRAINLOG_TRACKING_REPS
|
||||
? 10
|
||||
: 45;
|
||||
|
||||
|
|
@ -4409,7 +4625,8 @@ static bool build_session_exercise(
|
|||
"Charge cible kg : ",
|
||||
&target_has_weight,
|
||||
&target_weight
|
||||
) || !target_has_weight) {
|
||||
) ||
|
||||
!target_has_weight) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -4425,7 +4642,8 @@ static bool build_session_exercise(
|
|||
return false;
|
||||
}
|
||||
|
||||
if (exercise.tracking_mode == TRAINLOG_TRACKING_REPS) {
|
||||
if (exercise.tracking_mode ==
|
||||
TRAINLOG_TRACKING_REPS) {
|
||||
if (!prompt_int_value(
|
||||
7,
|
||||
"Répétitions cibles",
|
||||
|
|
@ -4471,42 +4689,45 @@ static bool build_session_exercise(
|
|||
return false;
|
||||
}
|
||||
|
||||
(void)memset(output, 0, sizeof(*output));
|
||||
|
||||
(void)snprintf(
|
||||
output->exercise_id,
|
||||
sizeof(output->exercise_id),
|
||||
"%s",
|
||||
exercise.exercise_id
|
||||
);
|
||||
|
||||
output->load_mode =
|
||||
load_mode == 1
|
||||
? TRAINLOG_LOAD_NONE
|
||||
: (load_mode == 2
|
||||
? TRAINLOG_LOAD_EXTERNAL
|
||||
: TRAINLOG_LOAD_ASSISTANCE);
|
||||
: (
|
||||
load_mode == 2
|
||||
? TRAINLOG_LOAD_EXTERNAL
|
||||
: TRAINLOG_LOAD_ASSISTANCE
|
||||
);
|
||||
|
||||
output->rest_seconds = rest_seconds;
|
||||
output->target_sets = target_sets;
|
||||
|
||||
output->target_reps =
|
||||
exercise.tracking_mode == TRAINLOG_TRACKING_REPS
|
||||
exercise.tracking_mode ==
|
||||
TRAINLOG_TRACKING_REPS
|
||||
? target_metric
|
||||
: 0;
|
||||
|
||||
output->target_duration_seconds =
|
||||
exercise.tracking_mode == TRAINLOG_TRACKING_DURATION
|
||||
exercise.tracking_mode ==
|
||||
TRAINLOG_TRACKING_DURATION
|
||||
? target_metric
|
||||
: 0;
|
||||
|
||||
output->target_has_weight = target_has_weight;
|
||||
output->target_weight_kg = target_weight;
|
||||
output->sets = set_storage;
|
||||
output->set_count = (size_t)actual_sets;
|
||||
output->target_has_weight =
|
||||
target_has_weight;
|
||||
|
||||
for (set_index = 0U; set_index < output->set_count; ++set_index) {
|
||||
int actual_metric = target_metric;
|
||||
output->target_weight_kg =
|
||||
target_weight;
|
||||
|
||||
output->sets = set_storage;
|
||||
output->set_count =
|
||||
(size_t)actual_sets;
|
||||
|
||||
for (set_index = 0U;
|
||||
set_index < output->set_count;
|
||||
++set_index) {
|
||||
int actual_metric =
|
||||
target_metric;
|
||||
|
||||
(void)memset(
|
||||
&set_storage[set_index],
|
||||
|
|
@ -4527,7 +4748,8 @@ static bool build_session_exercise(
|
|||
output->set_count
|
||||
);
|
||||
|
||||
if (exercise.tracking_mode == TRAINLOG_TRACKING_REPS) {
|
||||
if (exercise.tracking_mode ==
|
||||
TRAINLOG_TRACKING_REPS) {
|
||||
if (!prompt_int_value(
|
||||
5,
|
||||
"Répétitions réalisées",
|
||||
|
|
@ -4539,7 +4761,8 @@ static bool build_session_exercise(
|
|||
return false;
|
||||
}
|
||||
|
||||
set_storage[set_index].reps = actual_metric;
|
||||
set_storage[set_index].reps =
|
||||
actual_metric;
|
||||
} else {
|
||||
if (!prompt_duration_value(
|
||||
5,
|
||||
|
|
@ -4552,13 +4775,17 @@ static bool build_session_exercise(
|
|||
return false;
|
||||
}
|
||||
|
||||
set_storage[set_index].duration_seconds = actual_metric;
|
||||
set_storage[
|
||||
set_index
|
||||
].duration_seconds =
|
||||
actual_metric;
|
||||
}
|
||||
|
||||
if (target_has_weight) {
|
||||
char buffer[64];
|
||||
char prompt[128];
|
||||
double actual_weight = target_weight;
|
||||
double actual_weight =
|
||||
target_weight;
|
||||
|
||||
(void)snprintf(
|
||||
prompt,
|
||||
|
|
@ -4586,12 +4813,20 @@ static bool build_session_exercise(
|
|||
"Charge invalide.",
|
||||
TRAINLOG_COLOR_ERROR
|
||||
);
|
||||
|
||||
wait_key();
|
||||
return false;
|
||||
}
|
||||
|
||||
set_storage[set_index].has_weight = true;
|
||||
set_storage[set_index].weight_kg = actual_weight;
|
||||
set_storage[
|
||||
set_index
|
||||
].has_weight =
|
||||
true;
|
||||
|
||||
set_storage[
|
||||
set_index
|
||||
].weight_kg =
|
||||
actual_weight;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -6178,173 +6413,277 @@ static void screen_session_detail(
|
|||
TrainlogPersistedExerciseDetail *exercise =
|
||||
&exercises[selected];
|
||||
|
||||
char rest_text[64];
|
||||
char target_duration_text[64];
|
||||
if (exercise->recording_mode ==
|
||||
TRAINLOG_RECORDING_CONTINUOUS) {
|
||||
char duration_text[64];
|
||||
|
||||
int title_row =
|
||||
decorated ? 16 : 6;
|
||||
int title_row =
|
||||
decorated ? 16 : 6;
|
||||
|
||||
int mode_row =
|
||||
decorated ? 18 : 8;
|
||||
int mode_row =
|
||||
decorated ? 18 : 8;
|
||||
|
||||
int target_row =
|
||||
decorated ? 20 : 10;
|
||||
int first_data_row =
|
||||
decorated ? 20 : 10;
|
||||
|
||||
int weight_row =
|
||||
decorated ? 21 : 11;
|
||||
int row =
|
||||
first_data_row;
|
||||
|
||||
int actual_row =
|
||||
decorated ? 23 : 13;
|
||||
if (trainlog_duration_format(
|
||||
exercise->continuous_duration_seconds,
|
||||
duration_text,
|
||||
sizeof(duration_text)
|
||||
) != TRAINLOG_STATUS_OK) {
|
||||
(void)snprintf(
|
||||
duration_text,
|
||||
sizeof(duration_text),
|
||||
"%d s",
|
||||
exercise->continuous_duration_seconds
|
||||
);
|
||||
}
|
||||
|
||||
int summary_row =
|
||||
decorated ? 25 : 15;
|
||||
|
||||
int warning_row =
|
||||
decorated ? 27 : 17;
|
||||
|
||||
if (trainlog_duration_format(
|
||||
exercise->rest_seconds,
|
||||
rest_text,
|
||||
sizeof(rest_text)
|
||||
) != TRAINLOG_STATUS_OK) {
|
||||
(void)snprintf(
|
||||
rest_text,
|
||||
sizeof(rest_text),
|
||||
"%ds",
|
||||
exercise->rest_seconds
|
||||
);
|
||||
}
|
||||
|
||||
target_duration_text[0] = '\0';
|
||||
|
||||
if (exercise->tracking_mode ==
|
||||
TRAINLOG_TRACKING_DURATION) {
|
||||
(void)trainlog_duration_format(
|
||||
exercise->target_duration_seconds,
|
||||
target_duration_text,
|
||||
sizeof(target_duration_text)
|
||||
);
|
||||
}
|
||||
|
||||
attron(
|
||||
A_BOLD |
|
||||
trainlog_theme_attribute(
|
||||
TRAINLOG_COLOR_ACCENT
|
||||
)
|
||||
);
|
||||
|
||||
mvprintw(
|
||||
title_row,
|
||||
decorated ? 5 : 4,
|
||||
"Exercice %zu/%zu — %s",
|
||||
selected + 1U,
|
||||
count,
|
||||
exercise->name
|
||||
);
|
||||
|
||||
attroff(
|
||||
A_BOLD |
|
||||
trainlog_theme_attribute(
|
||||
TRAINLOG_COLOR_ACCENT
|
||||
)
|
||||
);
|
||||
|
||||
mvprintw(
|
||||
mode_row,
|
||||
decorated ? 5 : 4,
|
||||
"Mode : %-12s Charge : %-10s Repos : %s",
|
||||
exercise->tracking_mode ==
|
||||
TRAINLOG_TRACKING_REPS
|
||||
? "répétitions"
|
||||
: "durée",
|
||||
session_detail_load_label(
|
||||
exercise->load_mode
|
||||
),
|
||||
rest_text
|
||||
);
|
||||
|
||||
if (exercise->tracking_mode ==
|
||||
TRAINLOG_TRACKING_REPS) {
|
||||
mvprintw(
|
||||
target_row,
|
||||
decorated ? 5 : 4,
|
||||
"Cible : %d série(s) × %d reps",
|
||||
exercise->target_sets,
|
||||
exercise->target_reps
|
||||
);
|
||||
} else {
|
||||
mvprintw(
|
||||
target_row,
|
||||
decorated ? 5 : 4,
|
||||
"Cible : %d série(s) × %s",
|
||||
exercise->target_sets,
|
||||
target_duration_text
|
||||
);
|
||||
}
|
||||
|
||||
if (exercise->has_target_weight != 0) {
|
||||
mvprintw(
|
||||
weight_row,
|
||||
decorated ? 5 : 4,
|
||||
"Charge cible : %.1f kg",
|
||||
exercise->target_weight_kg
|
||||
);
|
||||
} else {
|
||||
mvprintw(
|
||||
weight_row,
|
||||
decorated ? 5 : 4,
|
||||
"Charge cible : —"
|
||||
);
|
||||
}
|
||||
|
||||
attron(
|
||||
A_BOLD |
|
||||
trainlog_theme_attribute(
|
||||
TRAINLOG_COLOR_SUCCESS
|
||||
)
|
||||
);
|
||||
|
||||
mvprintw(
|
||||
actual_row,
|
||||
decorated ? 5 : 4,
|
||||
"Réalisé : %zu série(s)",
|
||||
exercise->actual_set_count
|
||||
);
|
||||
|
||||
attroff(
|
||||
A_BOLD |
|
||||
trainlog_theme_attribute(
|
||||
TRAINLOG_COLOR_SUCCESS
|
||||
)
|
||||
);
|
||||
|
||||
mvprintw(
|
||||
summary_row,
|
||||
decorated ? 5 : 4,
|
||||
"%.*s",
|
||||
COLS - 10,
|
||||
exercise->actual_summary
|
||||
);
|
||||
|
||||
if (exercise->load_mode ==
|
||||
TRAINLOG_LOAD_ASSISTANCE) {
|
||||
attron(
|
||||
A_BOLD |
|
||||
trainlog_theme_attribute(
|
||||
TRAINLOG_COLOR_WARNING
|
||||
TRAINLOG_COLOR_ACCENT
|
||||
)
|
||||
);
|
||||
|
||||
mvprintw(
|
||||
warning_row,
|
||||
title_row,
|
||||
decorated ? 5 : 4,
|
||||
"Assistance : plus de kg = davantage d'aide."
|
||||
"Exercice %zu/%zu — %s",
|
||||
selected + 1U,
|
||||
count,
|
||||
exercise->name
|
||||
);
|
||||
|
||||
attroff(
|
||||
A_BOLD |
|
||||
trainlog_theme_attribute(
|
||||
TRAINLOG_COLOR_WARNING
|
||||
TRAINLOG_COLOR_ACCENT
|
||||
)
|
||||
);
|
||||
|
||||
mvprintw(
|
||||
mode_row,
|
||||
decorated ? 5 : 4,
|
||||
"Mode : continu"
|
||||
);
|
||||
|
||||
mvprintw(
|
||||
row++,
|
||||
decorated ? 5 : 4,
|
||||
"Durée : %s",
|
||||
duration_text
|
||||
);
|
||||
|
||||
if (exercise->has_continuous_speed != 0) {
|
||||
mvprintw(
|
||||
row++,
|
||||
decorated ? 5 : 4,
|
||||
"Vitesse : %.1f km/h",
|
||||
exercise->continuous_speed_kmh
|
||||
);
|
||||
}
|
||||
|
||||
if (exercise->has_continuous_distance != 0) {
|
||||
mvprintw(
|
||||
row++,
|
||||
decorated ? 5 : 4,
|
||||
"Distance : %.2f km",
|
||||
exercise->continuous_distance_km
|
||||
);
|
||||
}
|
||||
|
||||
attron(
|
||||
A_BOLD |
|
||||
trainlog_theme_attribute(
|
||||
TRAINLOG_COLOR_SUCCESS
|
||||
)
|
||||
);
|
||||
|
||||
mvprintw(
|
||||
row + 1,
|
||||
decorated ? 5 : 4,
|
||||
"Réalisé : activité continue"
|
||||
);
|
||||
|
||||
attroff(
|
||||
A_BOLD |
|
||||
trainlog_theme_attribute(
|
||||
TRAINLOG_COLOR_SUCCESS
|
||||
)
|
||||
);
|
||||
} else {
|
||||
char rest_text[64];
|
||||
char target_duration_text[64];
|
||||
|
||||
int title_row =
|
||||
decorated ? 16 : 6;
|
||||
|
||||
int mode_row =
|
||||
decorated ? 18 : 8;
|
||||
|
||||
int target_row =
|
||||
decorated ? 20 : 10;
|
||||
|
||||
int weight_row =
|
||||
decorated ? 21 : 11;
|
||||
|
||||
int actual_row =
|
||||
decorated ? 23 : 13;
|
||||
|
||||
int summary_row =
|
||||
decorated ? 25 : 15;
|
||||
|
||||
int warning_row =
|
||||
decorated ? 27 : 17;
|
||||
|
||||
if (trainlog_duration_format(
|
||||
exercise->rest_seconds,
|
||||
rest_text,
|
||||
sizeof(rest_text)
|
||||
) != TRAINLOG_STATUS_OK) {
|
||||
(void)snprintf(
|
||||
rest_text,
|
||||
sizeof(rest_text),
|
||||
"%ds",
|
||||
exercise->rest_seconds
|
||||
);
|
||||
}
|
||||
|
||||
target_duration_text[0] = '\0';
|
||||
|
||||
if (exercise->tracking_mode ==
|
||||
TRAINLOG_TRACKING_DURATION) {
|
||||
(void)trainlog_duration_format(
|
||||
exercise->target_duration_seconds,
|
||||
target_duration_text,
|
||||
sizeof(target_duration_text)
|
||||
);
|
||||
}
|
||||
|
||||
attron(
|
||||
A_BOLD |
|
||||
trainlog_theme_attribute(
|
||||
TRAINLOG_COLOR_ACCENT
|
||||
)
|
||||
);
|
||||
|
||||
mvprintw(
|
||||
title_row,
|
||||
decorated ? 5 : 4,
|
||||
"Exercice %zu/%zu — %s",
|
||||
selected + 1U,
|
||||
count,
|
||||
exercise->name
|
||||
);
|
||||
|
||||
attroff(
|
||||
A_BOLD |
|
||||
trainlog_theme_attribute(
|
||||
TRAINLOG_COLOR_ACCENT
|
||||
)
|
||||
);
|
||||
|
||||
mvprintw(
|
||||
mode_row,
|
||||
decorated ? 5 : 4,
|
||||
"Mode : %-12s Charge : %-10s Repos : %s",
|
||||
exercise->tracking_mode ==
|
||||
TRAINLOG_TRACKING_REPS
|
||||
? "répétitions"
|
||||
: "durée",
|
||||
session_detail_load_label(
|
||||
exercise->load_mode
|
||||
),
|
||||
rest_text
|
||||
);
|
||||
|
||||
if (exercise->tracking_mode ==
|
||||
TRAINLOG_TRACKING_REPS) {
|
||||
mvprintw(
|
||||
target_row,
|
||||
decorated ? 5 : 4,
|
||||
"Cible : %d série(s) × %d reps",
|
||||
exercise->target_sets,
|
||||
exercise->target_reps
|
||||
);
|
||||
} else {
|
||||
mvprintw(
|
||||
target_row,
|
||||
decorated ? 5 : 4,
|
||||
"Cible : %d série(s) × %s",
|
||||
exercise->target_sets,
|
||||
target_duration_text
|
||||
);
|
||||
}
|
||||
|
||||
if (exercise->has_target_weight != 0) {
|
||||
mvprintw(
|
||||
weight_row,
|
||||
decorated ? 5 : 4,
|
||||
"Charge cible : %.1f kg",
|
||||
exercise->target_weight_kg
|
||||
);
|
||||
} else {
|
||||
mvprintw(
|
||||
weight_row,
|
||||
decorated ? 5 : 4,
|
||||
"Charge cible : —"
|
||||
);
|
||||
}
|
||||
|
||||
attron(
|
||||
A_BOLD |
|
||||
trainlog_theme_attribute(
|
||||
TRAINLOG_COLOR_SUCCESS
|
||||
)
|
||||
);
|
||||
|
||||
mvprintw(
|
||||
actual_row,
|
||||
decorated ? 5 : 4,
|
||||
"Réalisé : %zu série(s)",
|
||||
exercise->actual_set_count
|
||||
);
|
||||
|
||||
attroff(
|
||||
A_BOLD |
|
||||
trainlog_theme_attribute(
|
||||
TRAINLOG_COLOR_SUCCESS
|
||||
)
|
||||
);
|
||||
|
||||
mvprintw(
|
||||
summary_row,
|
||||
decorated ? 5 : 4,
|
||||
"%.*s",
|
||||
COLS - 10,
|
||||
exercise->actual_summary
|
||||
);
|
||||
|
||||
if (exercise->load_mode ==
|
||||
TRAINLOG_LOAD_ASSISTANCE) {
|
||||
attron(
|
||||
trainlog_theme_attribute(
|
||||
TRAINLOG_COLOR_WARNING
|
||||
)
|
||||
);
|
||||
|
||||
mvprintw(
|
||||
warning_row,
|
||||
decorated ? 5 : 4,
|
||||
"Assistance : plus de kg = davantage d'aide."
|
||||
);
|
||||
|
||||
attroff(
|
||||
trainlog_theme_attribute(
|
||||
TRAINLOG_COLOR_WARNING
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -72,6 +72,100 @@ static bool test_blank_rejected(void)
|
|||
return true;
|
||||
}
|
||||
|
||||
static bool test_profiled_creation(void)
|
||||
{
|
||||
TrainlogDatabase *database = NULL;
|
||||
TrainlogExercise walk;
|
||||
TrainlogExercise exercises[4];
|
||||
size_t count = 0U;
|
||||
size_t index;
|
||||
bool found = false;
|
||||
|
||||
CHECK(
|
||||
trainlog_database_open(
|
||||
":memory:",
|
||||
&database
|
||||
) == TRAINLOG_STATUS_OK
|
||||
);
|
||||
|
||||
CHECK(
|
||||
trainlog_catalog_create_exercise_profiled(
|
||||
database,
|
||||
"Marche",
|
||||
TRAINLOG_TRACKING_DURATION,
|
||||
TRAINLOG_RECORDING_CONTINUOUS,
|
||||
TRAINLOG_EXERCISE_DATA_SPEED_KMH,
|
||||
&walk
|
||||
) == TRAINLOG_STATUS_OK
|
||||
);
|
||||
|
||||
CHECK(
|
||||
walk.recording_mode ==
|
||||
TRAINLOG_RECORDING_CONTINUOUS
|
||||
);
|
||||
|
||||
CHECK(
|
||||
walk.tracking_mode ==
|
||||
TRAINLOG_TRACKING_DURATION
|
||||
);
|
||||
|
||||
CHECK(
|
||||
walk.data_fields ==
|
||||
TRAINLOG_EXERCISE_DATA_SPEED_KMH
|
||||
);
|
||||
|
||||
CHECK(
|
||||
trainlog_catalog_create_exercise_profiled(
|
||||
database,
|
||||
"Profil invalide",
|
||||
TRAINLOG_TRACKING_REPS,
|
||||
TRAINLOG_RECORDING_CONTINUOUS,
|
||||
0U,
|
||||
&walk
|
||||
) == TRAINLOG_STATUS_INVALID_ARGUMENT
|
||||
);
|
||||
|
||||
CHECK(
|
||||
trainlog_database_list_exercises(
|
||||
database,
|
||||
exercises,
|
||||
4U,
|
||||
&count
|
||||
) == TRAINLOG_STATUS_OK
|
||||
);
|
||||
|
||||
CHECK(count == 1U);
|
||||
|
||||
for (index = 0U; index < count; ++index) {
|
||||
if (strcmp(
|
||||
exercises[index].name,
|
||||
"Marche"
|
||||
) == 0) {
|
||||
CHECK(
|
||||
exercises[index].recording_mode ==
|
||||
TRAINLOG_RECORDING_CONTINUOUS
|
||||
);
|
||||
|
||||
CHECK(
|
||||
exercises[index].tracking_mode ==
|
||||
TRAINLOG_TRACKING_DURATION
|
||||
);
|
||||
|
||||
CHECK(
|
||||
exercises[index].data_fields ==
|
||||
TRAINLOG_EXERCISE_DATA_SPEED_KMH
|
||||
);
|
||||
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
|
||||
CHECK(found);
|
||||
|
||||
trainlog_database_close(database);
|
||||
return true;
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
CHECK(test_normalization());
|
||||
|
|
@ -80,5 +174,8 @@ int main(void)
|
|||
CHECK(test_blank_rejected());
|
||||
(void)printf("PASS blank_rejected\n");
|
||||
|
||||
CHECK(test_profiled_creation());
|
||||
(void)printf("PASS profiled_creation\n");
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
|
|
|||
168
tui/tests/test_continuous_detail.c
Normal file
168
tui/tests/test_continuous_detail.c
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
#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 1; \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
int main(void)
|
||||
{
|
||||
TrainlogDatabase *database = NULL;
|
||||
TrainlogSessionExerciseInput exercise;
|
||||
TrainlogSessionInput session;
|
||||
TrainlogSessionSummary summary;
|
||||
TrainlogPersistedExerciseDetail details[2];
|
||||
size_t count = 0U;
|
||||
|
||||
CHECK(
|
||||
trainlog_database_open(
|
||||
":memory:",
|
||||
&database
|
||||
) == TRAINLOG_STATUS_OK
|
||||
);
|
||||
|
||||
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
|
||||
);
|
||||
|
||||
(void)memset(
|
||||
&exercise,
|
||||
0,
|
||||
sizeof(exercise)
|
||||
);
|
||||
|
||||
(void)snprintf(
|
||||
exercise.exercise_id,
|
||||
sizeof(exercise.exercise_id),
|
||||
"%s",
|
||||
"ex_walk"
|
||||
);
|
||||
|
||||
exercise.recording_mode =
|
||||
TRAINLOG_RECORDING_CONTINUOUS;
|
||||
|
||||
exercise.data_fields =
|
||||
TRAINLOG_EXERCISE_DATA_SPEED_KMH;
|
||||
|
||||
exercise.load_mode =
|
||||
TRAINLOG_LOAD_NONE;
|
||||
|
||||
exercise.continuous_duration_seconds =
|
||||
1800;
|
||||
|
||||
exercise.continuous_has_speed =
|
||||
true;
|
||||
|
||||
exercise.continuous_speed_kmh =
|
||||
5.8;
|
||||
|
||||
(void)memset(
|
||||
&session,
|
||||
0,
|
||||
sizeof(session)
|
||||
);
|
||||
|
||||
(void)snprintf(
|
||||
session.session_id,
|
||||
sizeof(session.session_id),
|
||||
"%s",
|
||||
"se_walk_detail"
|
||||
);
|
||||
|
||||
(void)snprintf(
|
||||
session.started_at,
|
||||
sizeof(session.started_at),
|
||||
"%s",
|
||||
"2026-09-06T13:00:00+02:00"
|
||||
);
|
||||
|
||||
session.exercises = &exercise;
|
||||
session.exercise_count = 1U;
|
||||
|
||||
CHECK(
|
||||
trainlog_database_insert_session(
|
||||
database,
|
||||
&session
|
||||
) == TRAINLOG_STATUS_OK
|
||||
);
|
||||
|
||||
CHECK(
|
||||
trainlog_database_get_session_details(
|
||||
database,
|
||||
session.session_id,
|
||||
&summary,
|
||||
details,
|
||||
2U,
|
||||
&count
|
||||
) == TRAINLOG_STATUS_OK
|
||||
);
|
||||
|
||||
CHECK(count == 1U);
|
||||
|
||||
CHECK(
|
||||
details[0].recording_mode ==
|
||||
TRAINLOG_RECORDING_CONTINUOUS
|
||||
);
|
||||
|
||||
CHECK(
|
||||
details[0].continuous_duration_seconds ==
|
||||
1800
|
||||
);
|
||||
|
||||
CHECK(
|
||||
details[0].has_continuous_speed != 0
|
||||
);
|
||||
|
||||
CHECK(
|
||||
details[0].continuous_speed_kmh >
|
||||
5.79
|
||||
);
|
||||
|
||||
CHECK(
|
||||
details[0].continuous_speed_kmh <
|
||||
5.81
|
||||
);
|
||||
|
||||
CHECK(
|
||||
details[0].actual_set_count ==
|
||||
0U
|
||||
);
|
||||
|
||||
CHECK(
|
||||
strcmp(
|
||||
details[0].actual_summary,
|
||||
"activité continue"
|
||||
) == 0
|
||||
);
|
||||
|
||||
trainlog_database_close(
|
||||
database
|
||||
);
|
||||
|
||||
(void)printf(
|
||||
"PASS continuous_detail\n"
|
||||
);
|
||||
|
||||
return 0;
|
||||
}
|
||||
275
tui/tests/test_continuous_session.c
Normal file
275
tui/tests/test_continuous_session.c
Normal file
|
|
@ -0,0 +1,275 @@
|
|||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <sqlite3.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 1; \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
int main(void)
|
||||
{
|
||||
char path[] =
|
||||
"/tmp/trainlog-continuous-XXXXXX";
|
||||
|
||||
TrainlogDatabase *database = NULL;
|
||||
TrainlogSessionExerciseInput exercise;
|
||||
TrainlogSessionInput session;
|
||||
sqlite3 *raw = NULL;
|
||||
sqlite3_stmt *statement = NULL;
|
||||
int fd;
|
||||
|
||||
fd = mkstemp(path);
|
||||
CHECK(fd >= 0);
|
||||
CHECK(close(fd) == 0);
|
||||
|
||||
CHECK(
|
||||
trainlog_database_open(
|
||||
path,
|
||||
&database
|
||||
) == TRAINLOG_STATUS_OK
|
||||
);
|
||||
|
||||
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
|
||||
);
|
||||
|
||||
(void)memset(
|
||||
&exercise,
|
||||
0,
|
||||
sizeof(exercise)
|
||||
);
|
||||
|
||||
(void)snprintf(
|
||||
exercise.exercise_id,
|
||||
sizeof(exercise.exercise_id),
|
||||
"%s",
|
||||
"ex_walk"
|
||||
);
|
||||
|
||||
exercise.recording_mode =
|
||||
TRAINLOG_RECORDING_CONTINUOUS;
|
||||
|
||||
exercise.data_fields =
|
||||
TRAINLOG_EXERCISE_DATA_SPEED_KMH;
|
||||
|
||||
exercise.load_mode =
|
||||
TRAINLOG_LOAD_NONE;
|
||||
|
||||
exercise.continuous_duration_seconds =
|
||||
2700;
|
||||
|
||||
exercise.continuous_has_speed =
|
||||
true;
|
||||
|
||||
exercise.continuous_speed_kmh =
|
||||
5.8;
|
||||
|
||||
(void)memset(
|
||||
&session,
|
||||
0,
|
||||
sizeof(session)
|
||||
);
|
||||
|
||||
(void)snprintf(
|
||||
session.session_id,
|
||||
sizeof(session.session_id),
|
||||
"%s",
|
||||
"se_walk"
|
||||
);
|
||||
|
||||
(void)snprintf(
|
||||
session.started_at,
|
||||
sizeof(session.started_at),
|
||||
"%s",
|
||||
"2026-09-06T12:00:00+02:00"
|
||||
);
|
||||
|
||||
session.exercises = &exercise;
|
||||
session.exercise_count = 1U;
|
||||
|
||||
CHECK(
|
||||
trainlog_database_insert_session(
|
||||
database,
|
||||
&session
|
||||
) == TRAINLOG_STATUS_OK
|
||||
);
|
||||
|
||||
trainlog_database_close(database);
|
||||
database = NULL;
|
||||
|
||||
CHECK(sqlite3_open(path, &raw) == SQLITE_OK);
|
||||
|
||||
CHECK(
|
||||
sqlite3_prepare_v2(
|
||||
raw,
|
||||
"SELECT recording_mode, data_fields, "
|
||||
"target_sets, target_reps, "
|
||||
"target_duration_seconds "
|
||||
"FROM session_exercises "
|
||||
"WHERE id = 1;",
|
||||
-1,
|
||||
&statement,
|
||||
NULL
|
||||
) == SQLITE_OK
|
||||
);
|
||||
|
||||
CHECK(sqlite3_step(statement) == SQLITE_ROW);
|
||||
|
||||
CHECK(
|
||||
strcmp(
|
||||
(const char *)
|
||||
sqlite3_column_text(
|
||||
statement,
|
||||
0
|
||||
),
|
||||
"continuous"
|
||||
) == 0
|
||||
);
|
||||
|
||||
CHECK(
|
||||
sqlite3_column_int(
|
||||
statement,
|
||||
1
|
||||
) ==
|
||||
(int)TRAINLOG_EXERCISE_DATA_SPEED_KMH
|
||||
);
|
||||
|
||||
CHECK(
|
||||
sqlite3_column_type(
|
||||
statement,
|
||||
2
|
||||
) == SQLITE_NULL
|
||||
);
|
||||
|
||||
CHECK(
|
||||
sqlite3_column_type(
|
||||
statement,
|
||||
3
|
||||
) == SQLITE_NULL
|
||||
);
|
||||
|
||||
CHECK(
|
||||
sqlite3_column_type(
|
||||
statement,
|
||||
4
|
||||
) == SQLITE_NULL
|
||||
);
|
||||
|
||||
CHECK(
|
||||
sqlite3_finalize(
|
||||
statement
|
||||
) == SQLITE_OK
|
||||
);
|
||||
|
||||
statement = NULL;
|
||||
|
||||
CHECK(
|
||||
sqlite3_prepare_v2(
|
||||
raw,
|
||||
"SELECT duration_seconds, "
|
||||
"speed_kmh, distance_km "
|
||||
"FROM continuous_activity "
|
||||
"WHERE session_exercise_row_id = 1;",
|
||||
-1,
|
||||
&statement,
|
||||
NULL
|
||||
) == SQLITE_OK
|
||||
);
|
||||
|
||||
CHECK(sqlite3_step(statement) == SQLITE_ROW);
|
||||
|
||||
CHECK(
|
||||
sqlite3_column_int(
|
||||
statement,
|
||||
0
|
||||
) == 2700
|
||||
);
|
||||
|
||||
CHECK(
|
||||
sqlite3_column_double(
|
||||
statement,
|
||||
1
|
||||
) > 5.79
|
||||
);
|
||||
|
||||
CHECK(
|
||||
sqlite3_column_double(
|
||||
statement,
|
||||
1
|
||||
) < 5.81
|
||||
);
|
||||
|
||||
CHECK(
|
||||
sqlite3_column_type(
|
||||
statement,
|
||||
2
|
||||
) == SQLITE_NULL
|
||||
);
|
||||
|
||||
CHECK(
|
||||
sqlite3_finalize(
|
||||
statement
|
||||
) == SQLITE_OK
|
||||
);
|
||||
|
||||
statement = NULL;
|
||||
|
||||
CHECK(
|
||||
sqlite3_prepare_v2(
|
||||
raw,
|
||||
"SELECT COUNT(*) "
|
||||
"FROM performed_sets;",
|
||||
-1,
|
||||
&statement,
|
||||
NULL
|
||||
) == SQLITE_OK
|
||||
);
|
||||
|
||||
CHECK(sqlite3_step(statement) == SQLITE_ROW);
|
||||
|
||||
CHECK(
|
||||
sqlite3_column_int(
|
||||
statement,
|
||||
0
|
||||
) == 0
|
||||
);
|
||||
|
||||
CHECK(
|
||||
sqlite3_finalize(
|
||||
statement
|
||||
) == SQLITE_OK
|
||||
);
|
||||
|
||||
CHECK(sqlite3_close(raw) == SQLITE_OK);
|
||||
CHECK(unlink(path) == 0);
|
||||
|
||||
(void)printf(
|
||||
"PASS continuous_session\n"
|
||||
);
|
||||
|
||||
return 0;
|
||||
}
|
||||
288
tui/tests/test_exercise_profile_schema.c
Normal file
288
tui/tests/test_exercise_profile_schema.c
Normal file
|
|
@ -0,0 +1,288 @@
|
|||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <sqlite3.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 bool test_profile_roundtrip(void)
|
||||
{
|
||||
TrainlogDatabase *database = NULL;
|
||||
TrainlogExercise exercises[4];
|
||||
size_t count = 0U;
|
||||
size_t index;
|
||||
bool found_walk = false;
|
||||
bool found_legacy = false;
|
||||
|
||||
CHECK(
|
||||
trainlog_database_open(
|
||||
":memory:",
|
||||
&database
|
||||
) == TRAINLOG_STATUS_OK
|
||||
);
|
||||
|
||||
CHECK(
|
||||
trainlog_database_insert_exercise(
|
||||
database,
|
||||
"ex_legacy",
|
||||
"Presse",
|
||||
"presse",
|
||||
TRAINLOG_TRACKING_REPS
|
||||
) == TRAINLOG_STATUS_OK
|
||||
);
|
||||
|
||||
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
|
||||
);
|
||||
|
||||
CHECK(
|
||||
trainlog_database_insert_exercise_profiled(
|
||||
database,
|
||||
"ex_invalid",
|
||||
"Invalid",
|
||||
"invalid",
|
||||
TRAINLOG_TRACKING_REPS,
|
||||
TRAINLOG_RECORDING_CONTINUOUS,
|
||||
0U
|
||||
) == TRAINLOG_STATUS_INVALID_ARGUMENT
|
||||
);
|
||||
|
||||
CHECK(
|
||||
trainlog_database_insert_exercise_profiled(
|
||||
database,
|
||||
"ex_unknown",
|
||||
"Unknown",
|
||||
"unknown",
|
||||
TRAINLOG_TRACKING_DURATION,
|
||||
TRAINLOG_RECORDING_CONTINUOUS,
|
||||
UINT32_C(8)
|
||||
) == TRAINLOG_STATUS_INVALID_ARGUMENT
|
||||
);
|
||||
|
||||
CHECK(
|
||||
trainlog_database_list_exercises(
|
||||
database,
|
||||
exercises,
|
||||
4U,
|
||||
&count
|
||||
) == TRAINLOG_STATUS_OK
|
||||
);
|
||||
|
||||
CHECK(count == 2U);
|
||||
|
||||
for (index = 0U; index < count; ++index) {
|
||||
if (strcmp(exercises[index].exercise_id, "ex_walk") == 0) {
|
||||
CHECK(
|
||||
exercises[index].recording_mode ==
|
||||
TRAINLOG_RECORDING_CONTINUOUS
|
||||
);
|
||||
CHECK(
|
||||
exercises[index].tracking_mode ==
|
||||
TRAINLOG_TRACKING_DURATION
|
||||
);
|
||||
CHECK(
|
||||
exercises[index].data_fields ==
|
||||
TRAINLOG_EXERCISE_DATA_SPEED_KMH
|
||||
);
|
||||
found_walk = true;
|
||||
}
|
||||
|
||||
if (strcmp(exercises[index].exercise_id, "ex_legacy") == 0) {
|
||||
CHECK(
|
||||
exercises[index].recording_mode ==
|
||||
TRAINLOG_RECORDING_SETS
|
||||
);
|
||||
CHECK(exercises[index].data_fields == 0U);
|
||||
found_legacy = true;
|
||||
}
|
||||
}
|
||||
|
||||
CHECK(found_walk);
|
||||
CHECK(found_legacy);
|
||||
|
||||
trainlog_database_close(database);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool test_v2_to_v3_migration(void)
|
||||
{
|
||||
char path[] =
|
||||
"/tmp/trainlog-schema-v2-profile-XXXXXX";
|
||||
|
||||
static const char *const V2_SQL =
|
||||
"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"
|
||||
");"
|
||||
"CREATE TABLE sessions ("
|
||||
"id INTEGER PRIMARY KEY,"
|
||||
"session_id TEXT NOT NULL UNIQUE,"
|
||||
"started_at TEXT NOT NULL,"
|
||||
"ended_at TEXT,"
|
||||
"session_type TEXT NOT NULL DEFAULT 'training',"
|
||||
"notes TEXT"
|
||||
");"
|
||||
"CREATE TABLE session_exercises ("
|
||||
"id INTEGER PRIMARY KEY,"
|
||||
"session_row_id INTEGER NOT NULL REFERENCES sessions(id),"
|
||||
"exercise_row_id INTEGER NOT NULL REFERENCES exercises(id),"
|
||||
"position INTEGER NOT NULL,"
|
||||
"load_mode TEXT NOT NULL,"
|
||||
"rest_seconds INTEGER NOT NULL,"
|
||||
"target_sets INTEGER NOT NULL,"
|
||||
"target_reps INTEGER,"
|
||||
"target_duration_seconds INTEGER,"
|
||||
"target_weight_kg REAL,"
|
||||
"notes TEXT"
|
||||
");"
|
||||
"INSERT INTO exercises("
|
||||
"exercise_id, name, normalized_name, tracking_mode"
|
||||
") VALUES('ex_old', 'Ancien', 'ancien', 'duration');"
|
||||
"INSERT INTO sessions("
|
||||
"session_id, started_at, session_type"
|
||||
") VALUES('se_old', '2026-09-01T10:00:00+02:00', 'training');"
|
||||
"INSERT INTO session_exercises("
|
||||
"session_row_id, exercise_row_id, position, load_mode,"
|
||||
"rest_seconds, target_sets, target_duration_seconds"
|
||||
") VALUES(1, 1, 0, 'none', 0, 1, 60);"
|
||||
"PRAGMA user_version = 2;";
|
||||
|
||||
sqlite3 *raw = NULL;
|
||||
sqlite3_stmt *statement = NULL;
|
||||
TrainlogDatabase *database = NULL;
|
||||
int fd;
|
||||
int version = 0;
|
||||
|
||||
fd = mkstemp(path);
|
||||
CHECK(fd >= 0);
|
||||
CHECK(close(fd) == 0);
|
||||
|
||||
CHECK(sqlite3_open(path, &raw) == SQLITE_OK);
|
||||
CHECK(
|
||||
sqlite3_exec(raw, V2_SQL, NULL, NULL, NULL) ==
|
||||
SQLITE_OK
|
||||
);
|
||||
CHECK(sqlite3_close(raw) == SQLITE_OK);
|
||||
raw = NULL;
|
||||
|
||||
CHECK(
|
||||
trainlog_database_open(path, &database) ==
|
||||
TRAINLOG_STATUS_OK
|
||||
);
|
||||
|
||||
CHECK(
|
||||
trainlog_database_schema_version(
|
||||
database,
|
||||
&version
|
||||
) == TRAINLOG_STATUS_OK
|
||||
);
|
||||
|
||||
CHECK(version == 3);
|
||||
|
||||
trainlog_database_close(database);
|
||||
database = NULL;
|
||||
|
||||
CHECK(sqlite3_open(path, &raw) == SQLITE_OK);
|
||||
|
||||
CHECK(
|
||||
sqlite3_prepare_v2(
|
||||
raw,
|
||||
"SELECT recording_mode, data_fields "
|
||||
"FROM exercises WHERE exercise_id='ex_old';",
|
||||
-1,
|
||||
&statement,
|
||||
NULL
|
||||
) == SQLITE_OK
|
||||
);
|
||||
|
||||
CHECK(sqlite3_step(statement) == SQLITE_ROW);
|
||||
CHECK(
|
||||
strcmp(
|
||||
(const char *)sqlite3_column_text(statement, 0),
|
||||
"sets"
|
||||
) == 0
|
||||
);
|
||||
CHECK(sqlite3_column_int64(statement, 1) == 0);
|
||||
CHECK(sqlite3_finalize(statement) == SQLITE_OK);
|
||||
statement = NULL;
|
||||
|
||||
CHECK(
|
||||
sqlite3_prepare_v2(
|
||||
raw,
|
||||
"SELECT recording_mode, data_fields "
|
||||
"FROM session_exercises WHERE id=1;",
|
||||
-1,
|
||||
&statement,
|
||||
NULL
|
||||
) == SQLITE_OK
|
||||
);
|
||||
|
||||
CHECK(sqlite3_step(statement) == SQLITE_ROW);
|
||||
CHECK(
|
||||
strcmp(
|
||||
(const char *)sqlite3_column_text(statement, 0),
|
||||
"sets"
|
||||
) == 0
|
||||
);
|
||||
CHECK(sqlite3_column_int64(statement, 1) == 0);
|
||||
CHECK(sqlite3_finalize(statement) == SQLITE_OK);
|
||||
statement = NULL;
|
||||
|
||||
CHECK(
|
||||
sqlite3_prepare_v2(
|
||||
raw,
|
||||
"SELECT name FROM sqlite_master "
|
||||
"WHERE type='table' AND name='continuous_activity';",
|
||||
-1,
|
||||
&statement,
|
||||
NULL
|
||||
) == SQLITE_OK
|
||||
);
|
||||
|
||||
CHECK(sqlite3_step(statement) == SQLITE_ROW);
|
||||
CHECK(sqlite3_finalize(statement) == SQLITE_OK);
|
||||
|
||||
CHECK(sqlite3_close(raw) == SQLITE_OK);
|
||||
raw = NULL;
|
||||
|
||||
CHECK(unlink(path) == 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
CHECK(test_profile_roundtrip());
|
||||
CHECK(test_v2_to_v3_migration());
|
||||
|
||||
(void)printf("PASS exercise_profile_schema\n");
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
/**
|
||||
* @file test_session_type_schema.c
|
||||
* @brief Schema v2 and session type persistence tests.
|
||||
* @brief Historical schema migration and session type persistence tests.
|
||||
*/
|
||||
|
||||
#include <stdbool.h>
|
||||
|
|
@ -141,6 +141,14 @@ static bool test_v1_to_v2_migration(void)
|
|||
"/tmp/trainlog-schema-v1-XXXXXX";
|
||||
|
||||
static const char *const V1_SQL =
|
||||
"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"
|
||||
" CHECK (tracking_mode IN ('reps', 'duration'))"
|
||||
");"
|
||||
"CREATE TABLE sessions ("
|
||||
"id INTEGER PRIMARY KEY,"
|
||||
"session_id TEXT NOT NULL UNIQUE,"
|
||||
|
|
@ -148,6 +156,48 @@ static bool test_v1_to_v2_migration(void)
|
|||
"ended_at TEXT,"
|
||||
"notes TEXT"
|
||||
");"
|
||||
"CREATE TABLE session_exercises ("
|
||||
"id INTEGER PRIMARY KEY,"
|
||||
"session_row_id INTEGER NOT NULL REFERENCES sessions(id),"
|
||||
"exercise_row_id INTEGER NOT NULL REFERENCES exercises(id),"
|
||||
"position INTEGER NOT NULL,"
|
||||
"load_mode TEXT NOT NULL,"
|
||||
"rest_seconds INTEGER NOT NULL,"
|
||||
"target_sets INTEGER NOT NULL,"
|
||||
"target_reps INTEGER,"
|
||||
"target_duration_seconds INTEGER,"
|
||||
"target_weight_kg REAL,"
|
||||
"notes TEXT"
|
||||
");"
|
||||
"CREATE TABLE performed_sets ("
|
||||
"id INTEGER PRIMARY KEY,"
|
||||
"session_exercise_row_id INTEGER NOT NULL REFERENCES session_exercises(id),"
|
||||
"position INTEGER NOT NULL,"
|
||||
"reps INTEGER,"
|
||||
"duration_seconds INTEGER,"
|
||||
"weight_kg REAL"
|
||||
");"
|
||||
"CREATE TABLE body_observations ("
|
||||
"id INTEGER PRIMARY KEY,"
|
||||
"observation_id TEXT NOT NULL UNIQUE,"
|
||||
"observed_at TEXT NOT NULL,"
|
||||
"session_row_id INTEGER UNIQUE REFERENCES sessions(id),"
|
||||
"body_weight_kg REAL,"
|
||||
"neck_cm REAL,"
|
||||
"shoulders_cm REAL,"
|
||||
"chest_cm REAL,"
|
||||
"waist_cm REAL,"
|
||||
"hips_cm REAL,"
|
||||
"left_arm_cm REAL,"
|
||||
"right_arm_cm REAL,"
|
||||
"left_forearm_cm REAL,"
|
||||
"right_forearm_cm REAL,"
|
||||
"left_thigh_cm REAL,"
|
||||
"right_thigh_cm REAL,"
|
||||
"left_calf_cm REAL,"
|
||||
"right_calf_cm REAL,"
|
||||
"notes TEXT"
|
||||
");"
|
||||
"INSERT INTO sessions("
|
||||
"session_id, started_at, ended_at, notes"
|
||||
") VALUES("
|
||||
|
|
|
|||
Loading…
Reference in a new issue