/
DemienMedich
/
BodyweightBase
Обзор
Документация
Войти
/
DemienMedich
/
BodyweightBase
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
develop
tests/state_store_tests.cpp
267 строк
10 KB
DemienMedich
feat: continue quick workouts into full sessions
23 июл 2026, 16:34
23 июл 2026, 16:34
a2ccab1
Код
Авторство
О чём код?
#include "bodyweight/state_importer.h" #include "bodyweight/state_store.h" #include "bodyweight/workout_session_runner.h" #include <QFile> #include <QJsonArray> #include <QJsonDocument> #include <QTemporaryDir> #include <QtTest> using namespace bodyweight; namespace { WorkoutPlan plan() { WorkoutPlan result; result.id = "vertical"; result.name = "Vertical"; result.steps = { {"squat", 10, 2, {}}, {"hold", 3, 0, {}} }; return result; } QVector<ExerciseDefinition> exercises() { return { {"squat", "Squat", ExerciseMetric::repetitions, 10, 2}, {"hold", "Hold", ExerciseMetric::seconds, 3, 0} }; } QJsonObject initialRoot() { return { {"schemaVersion", 1}, {"libraryVersion", "test"}, {"selectedPlanId", "vertical"}, {"plans", QJsonArray{}}, {"sessions", QJsonArray{}}, {"pendingDraft", QJsonValue::Null}, {"pendingResult", QJsonValue::Null} }; } } // namespace class StateStoreTests : public QObject { Q_OBJECT private slots: void completesSavesAndReloadsVerticalSlice(); void createsBackupAndRecoversCorruptPrimary(); void rejectsInvalidSessionHistory(); void capsSessionHistory(); void roundTripsCompatibleSessionDraft(); void roundTripsPendingFinishedSession(); void roundTripsQuickDecisionDraft(); }; void StateStoreTests::completesSavesAndReloadsVerticalSlice() { QTemporaryDir directory; QVERIFY(directory.isValid()); StateStore store(directory.path()); WorkoutSessionRunner runner(exercises()); QVERIFY(runner.start(plan())); runner.advance(8); runner.advance(2); runner.completeRepExercise(); runner.advance(2); runner.advance(11); const std::optional<FinishedSession> finished = runner.takeFinishedSession(); QVERIFY(finished.has_value()); QCOMPARE(finished->sessionMode, QString("full")); QJsonObject root = initialRoot(); QString error; const QDateTime started = QDateTime::fromString("2026-06-15T10:00:00", Qt::ISODate); QVERIFY2(StateStore::appendFinishedSession(root, *finished, started, started.addSecs(23), "ok", &error), qPrintable(error)); QVERIFY2(store.save(root, &error), qPrintable(error)); const StoreLoadResult loaded = store.load(); QVERIFY2(loaded.ok, qPrintable(loaded.error)); QCOMPARE(loaded.root.value("schemaVersion").toInt(), StateStore::schemaVersion); QCOMPARE(loaded.root.value("sessions").toArray().size(), 1); QCOMPARE(loaded.root.value("sessions").toArray().first().toObject() .value("sessionMode").toString(), QString("full")); const ImportResult compatibility = importNativeState(QJsonDocument(loaded.root).toJson()); QVERIFY2(compatibility.ok, qPrintable(compatibility.error)); QCOMPARE(compatibility.state.sessionCount, 1); } void StateStoreTests::createsBackupAndRecoversCorruptPrimary() { QTemporaryDir directory; QVERIFY(directory.isValid()); StateStore store(directory.path()); QString error; QVERIFY2(store.save(initialRoot(), &error), qPrintable(error)); QJsonObject second = initialRoot(); second.insert("Marker", 2); QVERIFY2(store.save(second, &error), qPrintable(error)); QVERIFY(QFile::exists(store.backupFilePath())); QFile corrupt(store.stateFilePath()); QVERIFY(corrupt.open(QIODevice::WriteOnly | QIODevice::Truncate)); QCOMPARE(corrupt.write("{ invalid"), 9); corrupt.close(); const StoreLoadResult recovered = store.load(); QVERIFY2(recovered.ok, qPrintable(recovered.error)); QVERIFY(recovered.recoveredFromBackup); QVERIFY(!recovered.preservedCorruptPath.isEmpty()); QVERIFY(QFile::exists(recovered.preservedCorruptPath)); QVERIFY(!recovered.root.contains("Marker")); } void StateStoreTests::rejectsInvalidSessionHistory() { QJsonObject root{{"sessions", "invalid"}}; FinishedSession session; session.planId = "plan"; session.steps.append(CompletedExercise{}); QString error; QVERIFY(!StateStore::appendFinishedSession( root, session, QDateTime::currentDateTime(), QDateTime::currentDateTime(), {}, &error)); QVERIFY(error.contains("array")); } void StateStoreTests::capsSessionHistory() { QJsonObject root = initialRoot(); QJsonArray history; for (int index = 0; index < StateStore::maxSessionHistoryItems; ++index) { history.append(QJsonObject{{"id", QString::number(index)}}); } root.insert("sessions", history); FinishedSession session; session.planId = "plan"; session.planName = "Plan"; session.sessionMode = "quick"; session.totalWorkSeconds = 1; session.steps.append(CompletedExercise{"squat", "Squat", ExerciseMetric::repetitions, 1, 1, 1, 0, true, {}}); QString error; const QDateTime started = QDateTime::fromString("2026-06-15T10:00:00", Qt::ISODate); QVERIFY2(StateStore::appendFinishedSession(root, session, started, started.addSecs(1), {}, &error), qPrintable(error)); const QJsonArray capped = root.value("sessions").toArray(); QCOMPARE(capped.size(), StateStore::maxSessionHistoryItems); QCOMPARE(capped.first().toObject().value("id").toString(), QString("1")); QCOMPARE(capped.last().toObject().value("planId").toString(), QString("plan")); } void StateStoreTests::roundTripsCompatibleSessionDraft() { QJsonObject root = initialRoot(); WorkoutSessionRunner runner(exercises()); QVERIFY(runner.start(plan())); runner.advance(8); runner.advance(2); runner.setCurrentSetLog(8.0, 7, 2, "slow tempo"); runner.pause(); const std::optional<SessionDraft> draft = runner.createDraft(); QVERIFY(draft.has_value()); QString error; const QDateTime started = QDateTime::fromString("2026-06-15T10:00:00", Qt::ISODate); QVERIFY2(StateStore::setPendingDraft(root, *draft, started, &error), qPrintable(error)); const QJsonObject jsonDraft = root.value("pendingDraft").toObject(); QCOMPARE(jsonDraft.value("phase").toInt(), 6); QCOMPARE(jsonDraft.value("phaseBeforePause").toInt(), 1); const std::optional<StoredSessionDraft> restored = StateStore::pendingDraft(root, &error); QVERIFY2(restored.has_value(), qPrintable(error)); QCOMPARE(restored->draft.phase, SessionPhase::paused); QCOMPARE(restored->draft.phaseBeforePause, SessionPhase::repetitionExercise); QCOMPARE(restored->draft.currentValue, draft->currentValue); QCOMPARE(restored->draft.currentExternalLoadKg, 8.0); QCOMPARE(restored->draft.currentEffortRating, 7); QCOMPARE(restored->draft.currentDiscomfortRating, 2); QCOMPARE(restored->draft.currentLoadNote, QString("slow tempo")); QCOMPARE(restored->sessionStartedAtLocal, started); } void StateStoreTests::roundTripsPendingFinishedSession() { QJsonObject root = initialRoot(); FinishedSession session; session.planId = "plan"; session.planName = "Plan"; session.sessionMode = "quick"; session.totalWorkSeconds = 42; session.totalRestSeconds = 7; session.expectedStepCount = 2; session.completedAll = false; session.steps.append(CompletedExercise{ "squat", "Squat", ExerciseMetric::repetitions, 10, 10, 30, 7, true, "slow tempo", 8.0, 7, 2}); const QDateTime started = QDateTime::fromString("2026-06-15T10:00:00", Qt::ISODate); const QDateTime ended = started.addSecs(49); QString error; QVERIFY2(StateStore::setPendingFinishedSession( root, session, started, ended, "default", &error), qPrintable(error)); QVERIFY(root.value("pendingDraft").isNull()); const std::optional<StoredFinishedSession> restored = StateStore::pendingFinishedSession(root, &error); QVERIFY2(restored.has_value(), qPrintable(error)); QCOMPARE(restored->profileId, QString("default")); QCOMPARE(restored->session.planId, QString("plan")); QCOMPARE(restored->session.sessionMode, QString("quick")); QCOMPARE(restored->session.expectedStepCount, 2); QCOMPARE(restored->session.steps.size(), 1); QCOMPARE(restored->session.steps.first().externalLoadKg, 8.0); QCOMPARE(restored->session.steps.first().effortRating, 7); QCOMPARE(restored->session.steps.first().discomfortRating, 2); QCOMPARE(restored->session.steps.first().loadNote, QString("slow tempo")); QCOMPARE(restored->sessionStartedAtLocal, started); QCOMPARE(restored->sessionEndedAtLocal, ended); StateStore::clearPendingFinishedSession(root); QVERIFY(!StateStore::pendingFinishedSession(root).has_value()); } void StateStoreTests::roundTripsQuickDecisionDraft() { QJsonObject root = initialRoot(); WorkoutSessionRunner runner(exercises()); WorkoutPlan checkpointPlan = plan(); QVERIFY(runner.startWithQuickCheckpoint(checkpointPlan, 1)); runner.advance(8 + 10 * WorkoutSessionRunner::defaultSecondsPerRepetition); QCOMPARE(runner.snapshot().phase, SessionPhase::quickDecision); const std::optional<SessionDraft> draft = runner.createDraft(); QVERIFY(draft.has_value()); QString error; const QDateTime started = QDateTime::fromString("2026-06-15T10:00:00", Qt::ISODate); QVERIFY2(StateStore::setPendingDraft(root, *draft, started, &error), qPrintable(error)); const QJsonObject jsonDraft = root.value("pendingDraft").toObject(); QCOMPARE(jsonDraft.value("phase").toInt(), 7); QCOMPARE(jsonDraft.value("quickCheckpointStepIndex").toInt(), 1); const std::optional<StoredSessionDraft> restored = StateStore::pendingDraft(root, &error); QVERIFY2(restored.has_value(), qPrintable(error)); QCOMPARE(restored->draft.phase, SessionPhase::quickDecision); QCOMPARE(restored->draft.quickCheckpointStepIndex, 1); WorkoutSessionRunner resumed(exercises()); QVERIFY2(resumed.restore(restored->draft, &error), qPrintable(error)); QVERIFY(resumed.continueAfterQuickCheckpoint()); resumed.advance(8 + 3); const std::optional<FinishedSession> finished = resumed.takeFinishedSession(); QVERIFY(finished.has_value()); QCOMPARE(finished->sessionMode, QString("full")); } QTEST_MAIN(StateStoreTests) #include "state_store_tests.moc"