/
DemienMedich
/
BodyweightBase
Обзор
Документация
Войти
/
DemienMedich
/
BodyweightBase
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
develop
tests/workout_session_runner_tests.cpp
618 строк
23 KB
DemienMedich
feat: continue quick workouts into full sessions
23 июл 2026, 16:34
23 июл 2026, 16:34
a2ccab1
Код
Авторство
О чём код?
#include "bodyweight/workout_session_runner.h" #include <QtTest> using namespace bodyweight; namespace { QVector<ExerciseDefinition> exercises() { QVector<ExerciseDefinition> result{ {"squat", "Squat", ExerciseMetric::repetitions, 10, 5}, {"lunge", "Lunge", ExerciseMetric::repetitions, 8, 5}, {"hold", "Hold", ExerciseMetric::seconds, 4, 0} }; result[0].category = QStringLiteral("Ноги"); result[1].category = QStringLiteral("Ноги и баланс"); result[1].alternatingSides = true; result[2].category = QStringLiteral("Кор"); return result; } WorkoutPlan plan() { WorkoutPlan result; result.id = "test"; result.name = "Test"; result.steps = { {"squat", 12, 5, {}}, {"hold", 4, 0, {}} }; return result; } } // namespace class WorkoutSessionRunnerTests : public QObject { Q_OBJECT private slots: void completesMixedPlan(); void pauseFreezesCountdown(); void skipRecordsIncompleteStep(); void finishesEarlyAndKeepsPartialStep(); void replacesCurrentExerciseWithMatchingMetric(); void autoCompletesRepetitionExercise(); void completesConfiguredSets(); void expandsWarmupAndCooldown(); void repeatsWorkBlockForRounds(); void usesThreeSecondSideSwitch(); void resetsSetsForAlternatingExerciseAfterRest(); void completesHeterogeneousSetsAcrossRounds(); void recalculatesSlotsWhenReplacingAlternatingExercise(); void carriesLargeAdvanceAcrossRepetitionPhases(); void picksWarmupAndCooldownForPlanCategories(); void picksWingChunWarmupAndCooldown(); void picksQigongWarmupAndCooldown(); void rejectsUnknownExercise(); void restoresPausedDraft(); void rejectsInvalidDraft(); void recordsAndResetsStructuredSetLog(); void pausesAtQuickCheckpointAndKeepsOneSession(); }; void WorkoutSessionRunnerTests::completesMixedPlan() { WorkoutSessionRunner runner(exercises()); QVERIFY(runner.start(plan())); QCOMPARE(runner.snapshot().phase, SessionPhase::preparation); QCOMPARE(runner.snapshot().currentValue, 12); runner.advance(8); QCOMPARE(runner.snapshot().phase, SessionPhase::repetitionExercise); runner.advance(3); runner.setRepValue(13); runner.completeRepExercise(); QCOMPARE(runner.snapshot().phase, SessionPhase::rest); runner.advance(5); QCOMPARE(runner.snapshot().phase, SessionPhase::preparation); runner.advance(12); QCOMPARE(runner.snapshot().phase, SessionPhase::completed); const std::optional<FinishedSession> finished = runner.takeFinishedSession(); QVERIFY(finished.has_value()); QCOMPARE(finished->steps.size(), 2); QCOMPARE(finished->totalWorkSeconds, 7); QCOMPARE(finished->totalRestSeconds, 5); QVERIFY(finished->completedAll); } void WorkoutSessionRunnerTests::recordsAndResetsStructuredSetLog() { WorkoutPlan single; single.id = "load-log"; single.name = "Load log"; single.steps = {{"squat", 10, 0, {}, 2}}; WorkoutSessionRunner runner(exercises()); QVERIFY(runner.start(single)); runner.advance(8); runner.setCurrentSetLog(8.0, 7, 2, "slow tempo"); runner.setRepValue(10); runner.completeRepExercise(); QCOMPARE(runner.snapshot().currentSet, 2); QVERIFY(runner.lastCompletedExercise().has_value()); QCOMPARE(runner.lastCompletedExercise()->externalLoadKg, 8.0); QVERIFY(runner.updateLastCompletedSetLog(10.0, 8, 3, "after set")); const std::optional<SessionDraft> draft = runner.createDraft(); QVERIFY(draft.has_value()); QCOMPARE(draft->completedSteps.size(), 1); QCOMPARE(draft->completedSteps.first().externalLoadKg, 10.0); QCOMPARE(draft->completedSteps.first().effortRating, 8); QCOMPARE(draft->completedSteps.first().discomfortRating, 3); QCOMPARE(draft->completedSteps.first().loadNote, QString("after set")); runner.advance(8); runner.setRepValue(10); runner.completeRepExercise(); const std::optional<FinishedSession> finished = runner.takeFinishedSession(); QVERIFY(finished.has_value()); QCOMPARE(finished->steps.size(), 2); QCOMPARE(finished->steps.at(1).externalLoadKg, 0.0); QCOMPARE(finished->steps.at(1).effortRating, 0); QCOMPARE(finished->steps.at(1).discomfortRating, 0); QVERIFY(finished->steps.at(1).loadNote.isEmpty()); } void WorkoutSessionRunnerTests::pausesAtQuickCheckpointAndKeepsOneSession() { QVector<ExerciseDefinition> catalog = exercises(); catalog.append({"warmup_march_place", "Warmup", ExerciseMetric::seconds, 2, 0}); catalog.append({"switch_90_90", "Cooldown", ExerciseMetric::seconds, 2, 0}); WorkoutPlan quick; quick.id = "quick-flow"; quick.name = "Quick flow"; quick.variantId = "quick-start"; quick.warmupIncluded = true; quick.warmupStepCount = 1; quick.cooldownIncluded = true; quick.cooldownStepCount = 1; quick.steps = { {"squat", 2, 0, {}, 1}, {"hold", 2, 0, {}, 1} }; WorkoutSessionRunner runner(catalog); QVERIFY(runner.startWithQuickCheckpoint(quick, 1)); runner.advance(8 + 2 + 8 + 2 * WorkoutSessionRunner::defaultSecondsPerRepetition); QCOMPARE(runner.snapshot().phase, SessionPhase::quickDecision); QCOMPARE(runner.snapshot().stepIndex, 2); const std::optional<SessionDraft> checkpointDraft = runner.createDraft(); QVERIFY(checkpointDraft.has_value()); QCOMPARE(checkpointDraft->quickCheckpointStepIndex, 2); QVERIFY(runner.continueAfterQuickCheckpoint()); QCOMPARE(runner.snapshot().phase, SessionPhase::preparation); runner.advance(8 + 2 + 8 + 2); const std::optional<FinishedSession> full = runner.takeFinishedSession(); QVERIFY(full.has_value()); QCOMPARE(full->sessionMode, QString("full")); QCOMPARE(full->steps.size(), 4); QVERIFY(full->completedAll); QVERIFY(runner.startWithQuickCheckpoint(quick, 1)); runner.advance(8 + 2 + 8 + 2 * WorkoutSessionRunner::defaultSecondsPerRepetition); QVERIFY(runner.finishQuickAtCheckpoint()); QCOMPARE(runner.activePlan()->steps.size(), 3); QVERIFY(runner.activePlan()->steps.last().isCooldown); runner.advance(8 + 2); const std::optional<FinishedSession> stopped = runner.takeFinishedSession(); QVERIFY(stopped.has_value()); QCOMPARE(stopped->sessionMode, QString("quick")); QCOMPARE(stopped->steps.size(), 3); QVERIFY(stopped->completedAll); } void WorkoutSessionRunnerTests::pauseFreezesCountdown() { WorkoutSessionRunner runner(exercises()); QVERIFY(runner.start(plan())); runner.advance(3); runner.pause(); QCOMPARE(runner.snapshot().phase, SessionPhase::paused); QCOMPARE(runner.snapshot().phaseElapsedSeconds, 3); runner.advance(100); QCOMPARE(runner.snapshot().phaseElapsedSeconds, 3); runner.resume(); runner.advance(5); QCOMPARE(runner.snapshot().phase, SessionPhase::repetitionExercise); } void WorkoutSessionRunnerTests::skipRecordsIncompleteStep() { WorkoutSessionRunner runner(exercises()); QVERIFY(runner.start(plan())); runner.skipCurrentExercise(); runner.skipCountdown(); runner.advance(8); runner.skipCurrentExercise(); const std::optional<FinishedSession> finished = runner.takeFinishedSession(); QVERIFY(finished.has_value()); QCOMPARE(finished->steps.size(), 2); QVERIFY(!finished->steps.first().completed); QVERIFY(!finished->steps.last().completed); } void WorkoutSessionRunnerTests::finishesEarlyAndKeepsPartialStep() { WorkoutSessionRunner runner(exercises()); QVERIFY(runner.start(plan())); runner.skipCountdown(); runner.advance(6); QCOMPARE(runner.snapshot().currentValue, 2); runner.finishEarly(); QCOMPARE(runner.snapshot().phase, SessionPhase::completed); const std::optional<FinishedSession> finished = runner.takeFinishedSession(); QVERIFY(finished.has_value()); QCOMPARE(finished->steps.size(), 1); QCOMPARE(finished->steps.first().actualValue, 2); QCOMPARE(finished->steps.first().workSeconds, 6); QVERIFY(!finished->steps.first().completed); QVERIFY(!finished->completedAll); } void WorkoutSessionRunnerTests::replacesCurrentExerciseWithMatchingMetric() { WorkoutSessionRunner runner(exercises()); QVERIFY(runner.start(plan())); runner.skipCountdown(); runner.advance(6); QString error; QVERIFY2(runner.replaceCurrentExercise("lunge", &error), qPrintable(error)); QCOMPARE(runner.snapshot().phase, SessionPhase::preparation); QCOMPARE(runner.snapshot().currentValue, 12); QCOMPARE(runner.activePlan()->steps.first().exerciseId, QString("lunge")); QVERIFY(!runner.replaceCurrentExercise("hold", &error)); QVERIFY(error.contains("metric")); runner.skipCountdown(); runner.advance(6); runner.finishEarly(); const std::optional<FinishedSession> finished = runner.takeFinishedSession(); QVERIFY(finished.has_value()); QCOMPARE(finished->steps.first().exerciseId, QString("lunge")); QCOMPARE(finished->steps.first().actualValue, 2); } void WorkoutSessionRunnerTests::autoCompletesRepetitionExercise() { WorkoutSessionRunner runner(exercises()); QVERIFY(runner.start(plan())); runner.advance(8); QCOMPARE(runner.snapshot().phase, SessionPhase::repetitionExercise); QCOMPARE(runner.snapshot().currentValue, 0); runner.advance(WorkoutSessionRunner::defaultSecondsPerRepetition); QCOMPARE(runner.snapshot().currentValue, 1); runner.advance(11 * WorkoutSessionRunner::defaultSecondsPerRepetition); QCOMPARE(runner.snapshot().phase, SessionPhase::rest); runner.skipCountdown(); runner.advance(12); const std::optional<FinishedSession> finished = runner.takeFinishedSession(); QVERIFY(finished.has_value()); QCOMPARE(finished->steps.first().actualValue, 12); QVERIFY(finished->steps.first().completed); QVERIFY(finished->completedAll); } void WorkoutSessionRunnerTests::completesConfiguredSets() { WorkoutPlan multiSet = plan(); multiSet.steps = {{"squat", 3, 2, {}, 3}}; WorkoutSessionRunner runner(exercises()); QVERIFY(runner.start(multiSet)); for (int set = 1; set <= 3; ++set) { QCOMPARE(runner.snapshot().currentSet, set); QCOMPARE(runner.snapshot().totalSets, 3); runner.advance(8); runner.advance(3 * WorkoutSessionRunner::defaultSecondsPerRepetition); if (set < 3) { QCOMPARE(runner.snapshot().phase, SessionPhase::rest); runner.advance(2); } } const std::optional<FinishedSession> finished = runner.takeFinishedSession(); QVERIFY(finished.has_value()); QCOMPARE(finished->steps.size(), 3); QVERIFY(finished->completedAll); } void WorkoutSessionRunnerTests::expandsWarmupAndCooldown() { QVector<ExerciseDefinition> catalog = exercises(); catalog.append({"warmup_march_place", "Warmup", ExerciseMetric::seconds, 5, 0}); catalog.append({"switch_90_90", "Cooldown", ExerciseMetric::seconds, 6, 0}); WorkoutPlan withFlow = plan(); withFlow.warmupIncluded = true; withFlow.warmupStepCount = 1; withFlow.cooldownIncluded = true; withFlow.cooldownStepCount = 1; WorkoutSessionRunner runner(catalog); QVERIFY(runner.start(withFlow)); QVERIFY(runner.activePlan()); QCOMPARE(runner.activePlan()->steps.size(), 4); QVERIFY(runner.activePlan()->steps.first().isWarmup); QVERIFY(runner.activePlan()->steps.last().isCooldown); } void WorkoutSessionRunnerTests::repeatsWorkBlockForRounds() { QVector<ExerciseDefinition> catalog = exercises(); catalog.append({"warmup_march_place", "Warmup", ExerciseMetric::seconds, 5, 0}); catalog.append({"switch_90_90", "Cooldown", ExerciseMetric::seconds, 6, 0}); WorkoutPlan rounds = plan(); rounds.roundCount = 3; rounds.warmupIncluded = true; rounds.warmupStepCount = 1; rounds.cooldownIncluded = true; rounds.cooldownStepCount = 1; WorkoutSessionRunner runner(catalog); QVERIFY(runner.start(rounds)); QVERIFY(runner.activePlan()); QCOMPARE(runner.activePlan()->steps.size(), 8); QVERIFY(runner.activePlan()->steps.first().isWarmup); QVERIFY(runner.activePlan()->steps.last().isCooldown); QCOMPARE(runner.activePlan()->steps.at(1).exerciseId, QString("squat")); QCOMPARE(runner.activePlan()->steps.at(2).exerciseId, QString("hold")); QCOMPARE(runner.activePlan()->steps.at(3).exerciseId, QString("squat")); QCOMPARE(runner.activePlan()->steps.at(4).exerciseId, QString("hold")); QCOMPARE(runner.activePlan()->steps.at(5).exerciseId, QString("squat")); QCOMPARE(runner.activePlan()->steps.at(6).exerciseId, QString("hold")); } void WorkoutSessionRunnerTests::usesThreeSecondSideSwitch() { WorkoutPlan sidePlan; sidePlan.id = "side"; sidePlan.name = "Side"; sidePlan.steps = {{"lunge", 2, 9, {}, 1}}; WorkoutSessionRunner runner(exercises()); QVERIFY(runner.start(sidePlan)); QCOMPARE(runner.snapshot().totalSets, 2); runner.advance(8); runner.advance(2 * WorkoutSessionRunner::defaultSecondsPerRepetition); QCOMPARE(runner.snapshot().phase, SessionPhase::rest); QCOMPARE(runner.snapshot().phaseDurationSeconds, 3); runner.advance(3); QCOMPARE(runner.snapshot().currentSet, 2); runner.advance(8); runner.advance(2 * WorkoutSessionRunner::defaultSecondsPerRepetition); const std::optional<FinishedSession> finished = runner.takeFinishedSession(); QVERIFY(finished.has_value()); QCOMPARE(finished->steps.size(), 2); QCOMPARE(finished->expectedStepCount, 2); QCOMPARE(finished->totalRestSeconds, 3); QVERIFY(finished->completedAll); } void WorkoutSessionRunnerTests::resetsSetsForAlternatingExerciseAfterRest() { WorkoutPlan mixed; mixed.id = "ordinary-to-side"; mixed.name = "Ordinary to side"; mixed.steps = { {"squat", 1, 5, {}, 1}, {"lunge", 1, 9, {}, 1} }; WorkoutSessionRunner runner(exercises()); QVERIFY(runner.start(mixed)); runner.advance(8 + WorkoutSessionRunner::defaultSecondsPerRepetition); QCOMPARE(runner.snapshot().phase, SessionPhase::rest); QCOMPARE(runner.snapshot().stepIndex, 1); QCOMPARE(runner.snapshot().currentSet, 1); QCOMPARE(runner.snapshot().totalSets, 2); runner.advance(5 + 8 + WorkoutSessionRunner::defaultSecondsPerRepetition); QCOMPARE(runner.snapshot().phase, SessionPhase::rest); QCOMPARE(runner.snapshot().phaseDurationSeconds, 3); QCOMPARE(runner.snapshot().currentSet, 2); runner.advance(3 + 8 + WorkoutSessionRunner::defaultSecondsPerRepetition); const std::optional<FinishedSession> finished = runner.takeFinishedSession(); QVERIFY(finished.has_value()); QCOMPARE(finished->steps.size(), 3); QCOMPARE(finished->expectedStepCount, 3); QVERIFY(finished->completedAll); } void WorkoutSessionRunnerTests::completesHeterogeneousSetsAcrossRounds() { WorkoutPlan rounds; rounds.id = "heterogeneous-rounds"; rounds.name = "Heterogeneous rounds"; rounds.roundCount = 2; rounds.steps = { {"squat", 1, 2, {}, 2}, {"hold", 1, 1, {}, 1} }; WorkoutSessionRunner runner(exercises()); QVERIFY(runner.start(rounds)); runner.advance(1000); const std::optional<FinishedSession> finished = runner.takeFinishedSession(); QVERIFY(finished.has_value()); QCOMPARE(finished->steps.size(), 6); QCOMPARE(finished->expectedStepCount, 6); QCOMPARE(finished->totalWorkSeconds, 14); QCOMPARE(finished->totalRestSeconds, 9); QVERIFY(finished->completedAll); } void WorkoutSessionRunnerTests::recalculatesSlotsWhenReplacingAlternatingExercise() { WorkoutPlan replacementPlan; replacementPlan.id = "replacement-slots"; replacementPlan.name = "Replacement slots"; replacementPlan.steps = {{"squat", 1, 1, {}, 2}}; WorkoutSessionRunner runner(exercises()); QVERIFY(runner.start(replacementPlan)); runner.advance(8 + WorkoutSessionRunner::defaultSecondsPerRepetition + 1); QCOMPARE(runner.snapshot().phase, SessionPhase::preparation); QCOMPARE(runner.snapshot().currentSet, 2); QCOMPARE(runner.snapshot().totalSets, 2); QString error; QVERIFY2(runner.replaceCurrentExercise("lunge", &error), qPrintable(error)); QCOMPARE(runner.snapshot().currentSet, 3); QCOMPARE(runner.snapshot().totalSets, 4); QCOMPARE(runner.snapshot().phase, SessionPhase::preparation); QVERIFY2(runner.replaceCurrentExercise("squat", &error), qPrintable(error)); QCOMPARE(runner.snapshot().currentSet, 2); QCOMPARE(runner.snapshot().totalSets, 2); } void WorkoutSessionRunnerTests::carriesLargeAdvanceAcrossRepetitionPhases() { WorkoutPlan continuous; continuous.id = "large-advance"; continuous.name = "Large advance"; continuous.steps = { {"squat", 2, 4, {}, 1}, {"hold", 2, 0, {}, 1} }; WorkoutSessionRunner runner(exercises()); QVERIFY(runner.start(continuous)); runner.advance(8 + 2 * WorkoutSessionRunner::defaultSecondsPerRepetition + 4 + 8 + 2); QCOMPARE(runner.snapshot().phase, SessionPhase::completed); const std::optional<FinishedSession> finished = runner.takeFinishedSession(); QVERIFY(finished.has_value()); QCOMPARE(finished->steps.size(), 2); QCOMPARE(finished->expectedStepCount, 2); QCOMPARE(finished->totalWorkSeconds, 8); QCOMPARE(finished->totalRestSeconds, 4); QVERIFY(finished->completedAll); } void WorkoutSessionRunnerTests::picksWarmupAndCooldownForPlanCategories() { QVector<ExerciseDefinition> catalog = exercises(); catalog.append({"warmup_march_place", "March", ExerciseMetric::seconds, 5, 0}); catalog.append({"warmup_squat_to_stand", "Squat warmup", ExerciseMetric::seconds, 5, 0}); catalog.append({"warmup_hip_hinge_reach", "Hinge warmup", ExerciseMetric::seconds, 5, 0}); catalog.append({"warmup_arm_swing", "Arm warmup", ExerciseMetric::seconds, 5, 0}); catalog.append({"switch_90_90", "90/90", ExerciseMetric::seconds, 6, 0}); catalog.append({"hip_flexor_stretch", "Hip stretch", ExerciseMetric::seconds, 6, 0}); catalog.append({"world_greatest_stretch", "Stretch", ExerciseMetric::seconds, 6, 0}); WorkoutPlan legPlan; legPlan.id = "legs"; legPlan.name = "Legs"; legPlan.warmupIncluded = true; legPlan.warmupStepCount = 2; legPlan.cooldownIncluded = true; legPlan.cooldownStepCount = 2; legPlan.steps = {{"squat", 5, 0, {}}}; WorkoutSessionRunner runner(catalog); QVERIFY(runner.start(legPlan)); QVERIFY(runner.activePlan()); QCOMPARE(runner.activePlan()->steps.first().exerciseId, QString("warmup_march_place")); QCOMPARE(runner.activePlan()->steps.at(1).exerciseId, QString("warmup_squat_to_stand")); QCOMPARE(runner.activePlan()->steps.at(runner.activePlan()->steps.size() - 2).exerciseId, QString("switch_90_90")); QCOMPARE(runner.activePlan()->steps.last().exerciseId, QString("hip_flexor_stretch")); } void WorkoutSessionRunnerTests::picksWingChunWarmupAndCooldown() { QVector<ExerciseDefinition> catalog = exercises(); ExerciseDefinition technique{"tan_sao_drill", "Tan Sau", ExerciseMetric::seconds, 30, 15}; technique.category = QStringLiteral("Вин-Чун"); catalog.append(technique); catalog.append({"warmup_wrist_roll", "Wrist", ExerciseMetric::seconds, 5, 0}); catalog.append({"warmup_arm_swing", "Arms", ExerciseMetric::seconds, 5, 0}); catalog.append({"warmup_march_place", "March", ExerciseMetric::seconds, 5, 0}); catalog.append({"thoracic_rotation", "Rotation", ExerciseMetric::seconds, 5, 0}); catalog.append({"warmup_breath_reset", "Breathing", ExerciseMetric::seconds, 5, 0}); WorkoutPlan wingPlan; wingPlan.id = "wing"; wingPlan.name = "Wing Chun"; wingPlan.warmupIncluded = true; wingPlan.warmupStepCount = 3; wingPlan.cooldownIncluded = true; wingPlan.cooldownStepCount = 2; wingPlan.steps = {{"tan_sao_drill", 30, 15, {}}}; WorkoutSessionRunner runner(catalog); QVERIFY(runner.start(wingPlan)); QVERIFY(runner.activePlan()); QCOMPARE(runner.activePlan()->steps.first().exerciseId, QString("warmup_wrist_roll")); QCOMPARE(runner.activePlan()->steps.at(1).exerciseId, QString("warmup_arm_swing")); QCOMPARE(runner.activePlan()->steps.at(2).exerciseId, QString("warmup_march_place")); QCOMPARE(runner.activePlan()->steps.at(runner.activePlan()->steps.size() - 2).exerciseId, QString("thoracic_rotation")); QCOMPARE(runner.activePlan()->steps.last().exerciseId, QString("warmup_breath_reset")); } void WorkoutSessionRunnerTests::picksQigongWarmupAndCooldown() { QVector<ExerciseDefinition> catalog = exercises(); ExerciseDefinition qigong{"baduanjin_1", "Baduanjin", ExerciseMetric::seconds, 45, 15}; qigong.category = QStringLiteral("Цигун"); catalog.append(qigong); catalog.append({"warmup_breath_reset", "Breathing", ExerciseMetric::seconds, 5, 0}); catalog.append({"warmup_shoulder_circle", "Shoulders", ExerciseMetric::seconds, 5, 0}); catalog.append({"warmup_march_place", "March", ExerciseMetric::seconds, 5, 0}); catalog.append({"thoracic_rotation", "Rotation", ExerciseMetric::seconds, 5, 0}); WorkoutPlan qigongPlan; qigongPlan.id = "qigong"; qigongPlan.name = "Qigong"; qigongPlan.warmupIncluded = true; qigongPlan.warmupStepCount = 3; qigongPlan.cooldownIncluded = true; qigongPlan.cooldownStepCount = 2; qigongPlan.steps = {{"baduanjin_1", 45, 15, {}}}; WorkoutSessionRunner runner(catalog); QVERIFY(runner.start(qigongPlan)); QVERIFY(runner.activePlan()); QCOMPARE(runner.activePlan()->steps.first().exerciseId, QString("warmup_breath_reset")); QCOMPARE(runner.activePlan()->steps.at(1).exerciseId, QString("warmup_shoulder_circle")); QCOMPARE(runner.activePlan()->steps.at(2).exerciseId, QString("warmup_march_place")); QCOMPARE(runner.activePlan()->steps.at(runner.activePlan()->steps.size() - 2).exerciseId, QString("warmup_breath_reset")); QCOMPARE(runner.activePlan()->steps.last().exerciseId, QString("thoracic_rotation")); } void WorkoutSessionRunnerTests::rejectsUnknownExercise() { WorkoutSessionRunner runner(exercises()); WorkoutPlan invalid = plan(); invalid.steps.first().exerciseId = "missing"; QString error; QVERIFY(!runner.start(invalid, &error)); QVERIFY(error.contains("missing")); QCOMPARE(runner.snapshot().phase, SessionPhase::idle); } void WorkoutSessionRunnerTests::restoresPausedDraft() { WorkoutSessionRunner source(exercises()); QVERIFY(source.start(plan())); source.advance(8); source.advance(3); source.setRepValue(11); source.pause(); const std::optional<SessionDraft> draft = source.createDraft(); QVERIFY(draft.has_value()); WorkoutSessionRunner restored(exercises()); QString error; QVERIFY2(restored.restore(*draft, &error), qPrintable(error)); QCOMPARE(restored.snapshot().phase, SessionPhase::paused); QCOMPARE(restored.snapshot().currentValue, 11); QCOMPARE(restored.snapshot().phaseElapsedSeconds, 3); restored.resume(); QCOMPARE(restored.snapshot().phase, SessionPhase::repetitionExercise); } void WorkoutSessionRunnerTests::rejectsInvalidDraft() { WorkoutSessionRunner runner(exercises()); SessionDraft draft; draft.plan = plan(); draft.stepIndex = 99; draft.phase = SessionPhase::preparation; draft.phaseDurationSeconds = 8; QString error; QVERIFY(!runner.restore(draft, &error)); QVERIFY(error.contains("index")); } QTEST_MAIN(WorkoutSessionRunnerTests) #include "workout_session_runner_tests.moc"