mirror of
https://github.com/cagnulein/qdomyos-zwift.git
synced 2026-02-18 00:17:41 +01:00
Compare commits
16 Commits
Mobi-Rower
...
build-1266
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
874f8fd956 | ||
|
|
a7db9fb890 | ||
|
|
f6af04d297 | ||
|
|
af8530fcca | ||
|
|
e258980c36 | ||
|
|
7631eb7d6f | ||
|
|
d7b12c6495 | ||
|
|
fbc94bbee1 | ||
|
|
1c47337f1b | ||
|
|
514644bfa5 | ||
|
|
b4538f3bae | ||
|
|
a75685af4c | ||
|
|
af1262b208 | ||
|
|
32d56f29bf | ||
|
|
513414102b | ||
|
|
ff1ea64e5e |
11
.github/workflows/main.yml
vendored
11
.github/workflows/main.yml
vendored
@@ -428,7 +428,16 @@ jobs:
|
||||
if: failure()
|
||||
with:
|
||||
name: test_results_xml
|
||||
path: tst/test-results/**/*.xml
|
||||
path: tst/test-results/**/*.xml
|
||||
|
||||
- name: Upload test FIT files and database
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: test_fit_files_and_db
|
||||
path: |
|
||||
tst/test-artifacts/*.fit
|
||||
tst/test-artifacts/*.sqlite
|
||||
|
||||
# - name: Test Peloton API
|
||||
# if: github.event_name == 'push' || github.event_name == 'schedule'
|
||||
|
||||
@@ -4573,7 +4573,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_ENTITLEMENTS = "../src/ios/qdomyos-zwift.entitlements";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 1264;
|
||||
CURRENT_PROJECT_VERSION = 1266;
|
||||
DERIVE_MACCATALYST_PRODUCT_BUNDLE_IDENTIFIER = YES;
|
||||
DEVELOPMENT_TEAM = 6335M7T29D;
|
||||
ENABLE_BITCODE = NO;
|
||||
@@ -4774,7 +4774,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_ENTITLEMENTS = "../src/ios/qdomyos-zwift.entitlements";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 1264;
|
||||
CURRENT_PROJECT_VERSION = 1266;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DERIVE_MACCATALYST_PRODUCT_BUNDLE_IDENTIFIER = YES;
|
||||
DEVELOPMENT_TEAM = 6335M7T29D;
|
||||
@@ -5011,7 +5011,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1264;
|
||||
CURRENT_PROJECT_VERSION = 1266;
|
||||
DEVELOPMENT_TEAM = 6335M7T29D;
|
||||
ENABLE_BITCODE = YES;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
@@ -5107,7 +5107,7 @@
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1264;
|
||||
CURRENT_PROJECT_VERSION = 1266;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = 6335M7T29D;
|
||||
ENABLE_BITCODE = YES;
|
||||
@@ -5199,7 +5199,7 @@
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1264;
|
||||
CURRENT_PROJECT_VERSION = 1266;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"watchkit Extension/Preview Content\"";
|
||||
ENABLE_BITCODE = YES;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
@@ -5315,7 +5315,7 @@
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1264;
|
||||
CURRENT_PROJECT_VERSION = 1266;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_ASSET_PATHS = "\"watchkit Extension/Preview Content\"";
|
||||
ENABLE_BITCODE = YES;
|
||||
@@ -5425,7 +5425,7 @@
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = QZWidgetExtension.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1264;
|
||||
CURRENT_PROJECT_VERSION = 1266;
|
||||
DEVELOPMENT_TEAM = 6335M7T29D;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
@@ -5516,7 +5516,7 @@
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = QZWidgetExtension.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1264;
|
||||
CURRENT_PROJECT_VERSION = 1266;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = 6335M7T29D;
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
|
||||
@@ -128,8 +128,10 @@ void FitDatabaseProcessor::processDirectory(const QString& dirPath) {
|
||||
|
||||
void FitDatabaseProcessor::processFile(const QString& filePath) {
|
||||
if (!db.isOpen()) {
|
||||
emit error("Failed to initialize database for single file processing");
|
||||
return;
|
||||
if (!initializeDatabase()) {
|
||||
emit error("Failed to initialize database for single file processing");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!processFitFile(filePath)) {
|
||||
|
||||
86
src/qfit.cpp
86
src/qfit.cpp
@@ -298,7 +298,7 @@ void qfit::save(const QString &filename, QList<SessionLine> session, BLUETOOTH_T
|
||||
activityTitle.SetFitBaseTypeId(FIT_BASE_TYPE_STRING);
|
||||
activityTitle.SetFieldName(0, L"Activity Title");
|
||||
activityTitle.SetUnits(0, L"Title");
|
||||
activityTitle.SetNativeMesgNum(FIT_MESG_NUM_SESSION);
|
||||
activityTitle.SetNativeMesgNum(FIT_MESG_NUM_WORKOUT); // Workout message for developer metadata
|
||||
|
||||
fit::FieldDescriptionMesg targetCadenceMesg;
|
||||
targetCadenceMesg.SetDeveloperDataIndex(0);
|
||||
@@ -330,7 +330,7 @@ void qfit::save(const QString &filename, QList<SessionLine> session, BLUETOOTH_T
|
||||
ftpSessionMesg.SetFitBaseTypeId(FIT_BASE_TYPE_FLOAT64);
|
||||
ftpSessionMesg.SetFieldName(0, L"FTP");
|
||||
ftpSessionMesg.SetUnits(0, L"FTP");
|
||||
ftpSessionMesg.SetNativeMesgNum(FIT_MESG_NUM_SESSION);
|
||||
ftpSessionMesg.SetNativeMesgNum(FIT_MESG_NUM_WORKOUT); // Workout message for developer metadata
|
||||
|
||||
// Peloton and workout source fields
|
||||
fit::FieldDescriptionMesg workoutSourceMesg;
|
||||
@@ -339,7 +339,7 @@ void qfit::save(const QString &filename, QList<SessionLine> session, BLUETOOTH_T
|
||||
workoutSourceMesg.SetFitBaseTypeId(FIT_BASE_TYPE_STRING);
|
||||
workoutSourceMesg.SetFieldName(0, L"Workout Source");
|
||||
workoutSourceMesg.SetUnits(0, L"source");
|
||||
workoutSourceMesg.SetNativeMesgNum(FIT_MESG_NUM_SESSION);
|
||||
workoutSourceMesg.SetNativeMesgNum(FIT_MESG_NUM_WORKOUT); // Workout message for developer metadata
|
||||
|
||||
fit::FieldDescriptionMesg pelotonWorkoutIdMesg;
|
||||
pelotonWorkoutIdMesg.SetDeveloperDataIndex(0);
|
||||
@@ -347,7 +347,7 @@ void qfit::save(const QString &filename, QList<SessionLine> session, BLUETOOTH_T
|
||||
pelotonWorkoutIdMesg.SetFitBaseTypeId(FIT_BASE_TYPE_STRING);
|
||||
pelotonWorkoutIdMesg.SetFieldName(0, L"Peloton Workout ID");
|
||||
pelotonWorkoutIdMesg.SetUnits(0, L"id");
|
||||
pelotonWorkoutIdMesg.SetNativeMesgNum(FIT_MESG_NUM_SESSION);
|
||||
pelotonWorkoutIdMesg.SetNativeMesgNum(FIT_MESG_NUM_WORKOUT); // Workout message for developer metadata
|
||||
|
||||
fit::FieldDescriptionMesg pelotonUrlMesg;
|
||||
pelotonUrlMesg.SetDeveloperDataIndex(0);
|
||||
@@ -355,7 +355,7 @@ void qfit::save(const QString &filename, QList<SessionLine> session, BLUETOOTH_T
|
||||
pelotonUrlMesg.SetFitBaseTypeId(FIT_BASE_TYPE_STRING);
|
||||
pelotonUrlMesg.SetFieldName(0, L"Peloton URL");
|
||||
pelotonUrlMesg.SetUnits(0, L"url");
|
||||
pelotonUrlMesg.SetNativeMesgNum(FIT_MESG_NUM_SESSION);
|
||||
pelotonUrlMesg.SetNativeMesgNum(FIT_MESG_NUM_WORKOUT); // Workout message for developer metadata
|
||||
|
||||
fit::FieldDescriptionMesg trainingProgramFileMesg;
|
||||
trainingProgramFileMesg.SetDeveloperDataIndex(0);
|
||||
@@ -363,7 +363,7 @@ void qfit::save(const QString &filename, QList<SessionLine> session, BLUETOOTH_T
|
||||
trainingProgramFileMesg.SetFitBaseTypeId(FIT_BASE_TYPE_STRING);
|
||||
trainingProgramFileMesg.SetFieldName(0, L"Training Program File");
|
||||
trainingProgramFileMesg.SetUnits(0, L"filename");
|
||||
trainingProgramFileMesg.SetNativeMesgNum(FIT_MESG_NUM_SESSION);
|
||||
trainingProgramFileMesg.SetNativeMesgNum(FIT_MESG_NUM_WORKOUT); // Workout message for developer metadata
|
||||
|
||||
fit::SessionMesg sessionMesg;
|
||||
sessionMesg.SetTimestamp(session.at(firstRealIndex).time.toSecsSinceEpoch() - 631065600L);
|
||||
@@ -385,15 +385,18 @@ void qfit::save(const QString &filename, QList<SessionLine> session, BLUETOOTH_T
|
||||
|
||||
// Set training load in FIT file
|
||||
// Always set training_load_peak (Garmin uses this for acute training load)
|
||||
// COMMENTED OUT: Garmin Connect doesn't properly reflect these values
|
||||
// Moving to developer data message instead
|
||||
if (training_load > 0) {
|
||||
sessionMesg.SetTrainingLoadPeak(training_load);
|
||||
qDebug() << "Setting training_load_peak in FIT file:" << training_load;
|
||||
//sessionMesg.SetTrainingLoadPeak(training_load);
|
||||
qDebug() << "Training load will be stored in developer data:" << training_load;
|
||||
}
|
||||
|
||||
|
||||
// For cycling with power, also set training_stress_score (TSS)
|
||||
// COMMENTED OUT: Moving to developer data message
|
||||
if (has_tss) {
|
||||
sessionMesg.SetTrainingStressScore(tss);
|
||||
qDebug() << "Setting training_stress_score (TSS) in FIT file:" << tss;
|
||||
//sessionMesg.SetTrainingStressScore(tss);
|
||||
qDebug() << "TSS will be stored in developer data:" << tss;
|
||||
}
|
||||
|
||||
// First, set sport and subsport based on device type
|
||||
@@ -540,18 +543,8 @@ void qfit::save(const QString &filename, QList<SessionLine> session, BLUETOOTH_T
|
||||
trainingProgramFileField.SetSTRINGValue(trainingProgramFile.toStdWString());
|
||||
}
|
||||
|
||||
sessionMesg.AddDeveloperField(activityTitleField);
|
||||
sessionMesg.AddDeveloperField(ftpSessionField);
|
||||
sessionMesg.AddDeveloperField(workoutSourceField);
|
||||
if (!pelotonWorkoutId.isEmpty()) {
|
||||
sessionMesg.AddDeveloperField(pelotonWorkoutIdField);
|
||||
}
|
||||
if (!pelotonUrl.isEmpty()) {
|
||||
sessionMesg.AddDeveloperField(pelotonUrlField);
|
||||
}
|
||||
if (!trainingProgramFile.isEmpty()) {
|
||||
sessionMesg.AddDeveloperField(trainingProgramFileField);
|
||||
}
|
||||
// Developer fields are now added to custom message instead of session
|
||||
// This improves Garmin Connect compatibility
|
||||
|
||||
fit::ActivityMesg activityMesg;
|
||||
activityMesg.SetTimestamp(session.at(firstRealIndex).time.toSecsSinceEpoch() - 631065600L);
|
||||
@@ -608,6 +601,8 @@ void qfit::save(const QString &filename, QList<SessionLine> session, BLUETOOTH_T
|
||||
|
||||
encode.Write(timestampCorrelationMesg);
|
||||
|
||||
// Write workout message with developer metadata fields when workout name exists
|
||||
// This keeps workout-related metadata separate from session/activity for better compatibility
|
||||
if (workoutName.length() > 0) {
|
||||
fit::TrainingFileMesg trainingFile;
|
||||
trainingFile.SetTimestamp(sessionMesg.GetTimestamp());
|
||||
@@ -622,6 +617,21 @@ void qfit::save(const QString &filename, QList<SessionLine> session, BLUETOOTH_T
|
||||
workout.SetWktName(workoutName.toStdWString());
|
||||
#endif
|
||||
workout.SetNumValidSteps(1);
|
||||
|
||||
// Add developer fields to workout message
|
||||
workout.AddDeveloperField(activityTitleField);
|
||||
workout.AddDeveloperField(ftpSessionField);
|
||||
workout.AddDeveloperField(workoutSourceField);
|
||||
if (!pelotonWorkoutId.isEmpty()) {
|
||||
workout.AddDeveloperField(pelotonWorkoutIdField);
|
||||
}
|
||||
if (!pelotonUrl.isEmpty()) {
|
||||
workout.AddDeveloperField(pelotonUrlField);
|
||||
}
|
||||
if (!trainingProgramFile.isEmpty()) {
|
||||
workout.AddDeveloperField(trainingProgramFileField);
|
||||
}
|
||||
|
||||
encode.Write(workout);
|
||||
|
||||
fit::WorkoutStepMesg workoutStep;
|
||||
@@ -870,6 +880,36 @@ class Listener : public fit::FileIdMesgListener,
|
||||
// std::wcout << L" New Mesg: " << mesg.GetName().c_str() << L". It has " << mesg.GetNumFields() << L"
|
||||
// field(s) and " << mesg.GetNumDevFields() << " developer field(s).\n";
|
||||
|
||||
// Check if this is a Workout message with developer fields (new format)
|
||||
if (mesg.GetNum() == FIT_MESG_NUM_WORKOUT) {
|
||||
printf("Found Workout message with developer fields\n");
|
||||
// Read developer fields from workout message (new format)
|
||||
for (auto devField : mesg.GetDeveloperFields()) {
|
||||
std::string fieldName = devField.GetName();
|
||||
if (fieldName == "Activity Title" && workoutName != nullptr) {
|
||||
std::wstring wWorkoutName = devField.GetSTRINGValue(0);
|
||||
*workoutName = QString::fromStdWString(wWorkoutName);
|
||||
printf(" Found Activity Title in workout: %s\n", workoutName->toStdString().c_str());
|
||||
} else if (fieldName == "Workout Source" && workoutSource != nullptr) {
|
||||
std::wstring wWorkoutSource = devField.GetSTRINGValue(0);
|
||||
*workoutSource = QString::fromStdWString(wWorkoutSource);
|
||||
printf(" Found Workout Source in workout: %s\n", workoutSource->toStdString().c_str());
|
||||
} else if (fieldName == "Peloton Workout ID" && pelotonWorkoutId != nullptr) {
|
||||
std::wstring wPelotonWorkoutId = devField.GetSTRINGValue(0);
|
||||
*pelotonWorkoutId = QString::fromStdWString(wPelotonWorkoutId);
|
||||
printf(" Found Peloton Workout ID in workout: %s\n", pelotonWorkoutId->toStdString().c_str());
|
||||
} else if (fieldName == "Peloton URL" && pelotonUrl != nullptr) {
|
||||
std::wstring wPelotonUrl = devField.GetSTRINGValue(0);
|
||||
*pelotonUrl = QString::fromStdWString(wPelotonUrl);
|
||||
printf(" Found Peloton URL in workout: %s\n", pelotonUrl->toStdString().c_str());
|
||||
} else if (fieldName == "Training Program File" && trainingProgramFile != nullptr) {
|
||||
std::wstring wTrainingProgramFile = devField.GetSTRINGValue(0);
|
||||
*trainingProgramFile = QString::fromStdWString(wTrainingProgramFile);
|
||||
printf(" Found Training Program File in workout: %s\n", trainingProgramFile->toStdString().c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (FIT_UINT16 i = 0; i < (FIT_UINT16)mesg.GetNumFields(); i++) {
|
||||
fit::Field *field = mesg.GetFieldByIndex(i);
|
||||
// std::wcout << L" Field" << i << " (" << field->GetName().c_str() << ") has " << field->GetNumValues()
|
||||
|
||||
210
tst/ToolTests/qfittestsuite.cpp
Normal file
210
tst/ToolTests/qfittestsuite.cpp
Normal file
@@ -0,0 +1,210 @@
|
||||
#include "qfittestsuite.h"
|
||||
#include "../../src/qfit.h"
|
||||
#include "../../src/fitdatabaseprocessor.h"
|
||||
#include <QDateTime>
|
||||
#include <QFile>
|
||||
#include <QDir>
|
||||
#include <QDebug>
|
||||
#include <QEventLoop>
|
||||
#include <QTimer>
|
||||
|
||||
QFitTestSuite::QFitTestSuite() : tempDir(nullptr) {
|
||||
}
|
||||
|
||||
QFitTestSuite::~QFitTestSuite() {
|
||||
if (tempDir) {
|
||||
delete tempDir;
|
||||
tempDir = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void QFitTestSuite::SetUp() {
|
||||
tempDir = new QTemporaryDir();
|
||||
ASSERT_TRUE(tempDir->isValid()) << "Failed to create temporary directory";
|
||||
}
|
||||
|
||||
void QFitTestSuite::TearDown() {
|
||||
if (tempDir) {
|
||||
delete tempDir;
|
||||
tempDir = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
QList<SessionLine> QFitTestSuite::createTestSession() {
|
||||
QList<SessionLine> session;
|
||||
QDateTime startTime = QDateTime::currentDateTime();
|
||||
|
||||
// Create a simple 10-minute workout session
|
||||
for (int i = 0; i < 600; i += 5) { // 5 second intervals for 10 minutes
|
||||
SessionLine line;
|
||||
line.time = startTime.addSecs(i);
|
||||
line.elapsedTime = i;
|
||||
line.distance = i * 0.05; // 3 km/h = 0.05 km per 5 seconds
|
||||
line.speed = 3.0; // 3 km/h
|
||||
line.cadence = 60;
|
||||
line.heart = 120 + (i % 30); // Varying HR between 120-150
|
||||
line.calories = i / 10;
|
||||
line.watt = 100;
|
||||
|
||||
session.append(line);
|
||||
}
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
QString QFitTestSuite::createNewFormatFitFile() {
|
||||
QString filename = tempDir->filePath("test_new_format.fit");
|
||||
QList<SessionLine> session = createTestSession();
|
||||
|
||||
// Create a FIT file with developer fields
|
||||
qfit::save(filename, session, BIKE, QFIT_PROCESS_NONE, FIT_SPORT_CYCLING,
|
||||
"Test Workout Title",
|
||||
"Test Device",
|
||||
"PELOTON",
|
||||
"test_workout_id_123",
|
||||
"https://peloton.com/workout/123",
|
||||
"/path/to/training.zwo");
|
||||
|
||||
return filename;
|
||||
}
|
||||
|
||||
bool QFitTestSuite::verifyDeveloperFields(const QString& workoutName, const QString& workoutSource,
|
||||
const QString& pelotonWorkoutId, const QString& pelotonUrl,
|
||||
const QString& trainingProgramFile) {
|
||||
bool allCorrect = true;
|
||||
|
||||
if (workoutName != "Test Workout Title") {
|
||||
qDebug() << "Workout name mismatch. Expected: 'Test Workout Title', Got:" << workoutName;
|
||||
allCorrect = false;
|
||||
}
|
||||
|
||||
if (workoutSource != "PELOTON") {
|
||||
qDebug() << "Workout source mismatch. Expected: 'PELOTON', Got:" << workoutSource;
|
||||
allCorrect = false;
|
||||
}
|
||||
|
||||
if (pelotonWorkoutId != "test_workout_id_123") {
|
||||
qDebug() << "Peloton workout ID mismatch. Expected: 'test_workout_id_123', Got:" << pelotonWorkoutId;
|
||||
allCorrect = false;
|
||||
}
|
||||
|
||||
if (pelotonUrl != "https://peloton.com/workout/123") {
|
||||
qDebug() << "Peloton URL mismatch. Expected: 'https://peloton.com/workout/123', Got:" << pelotonUrl;
|
||||
allCorrect = false;
|
||||
}
|
||||
|
||||
if (trainingProgramFile != "/path/to/training.zwo") {
|
||||
qDebug() << "Training program file mismatch. Expected: '/path/to/training.zwo', Got:" << trainingProgramFile;
|
||||
allCorrect = false;
|
||||
}
|
||||
|
||||
return allCorrect;
|
||||
}
|
||||
|
||||
void QFitTestSuite::test_newFormatDeveloperFields() {
|
||||
// Create a FIT file with new format
|
||||
QString filename = createNewFormatFitFile();
|
||||
ASSERT_TRUE(QFile::exists(filename)) << "Failed to create FIT file";
|
||||
|
||||
// Copy to test-artifacts directory for download
|
||||
QDir artifactsDir("test-artifacts");
|
||||
if (!artifactsDir.exists()) {
|
||||
artifactsDir.mkpath(".");
|
||||
}
|
||||
QString artifactPath = "test-artifacts/test_new_format.fit";
|
||||
QFile::remove(artifactPath);
|
||||
QFile::copy(filename, artifactPath);
|
||||
qDebug() << "FIT file saved to:" << artifactPath;
|
||||
|
||||
// Read the file back
|
||||
QList<SessionLine> session;
|
||||
FIT_SPORT sport = FIT_SPORT_INVALID;
|
||||
QString workoutName;
|
||||
QString workoutSource;
|
||||
QString pelotonWorkoutId;
|
||||
QString pelotonUrl;
|
||||
QString trainingProgramFile;
|
||||
|
||||
qfit::open(filename, &session, &sport, &workoutName, &workoutSource,
|
||||
&pelotonWorkoutId, &pelotonUrl, &trainingProgramFile);
|
||||
|
||||
// Verify basic data was read
|
||||
EXPECT_FALSE(session.isEmpty()) << "Session should not be empty";
|
||||
EXPECT_EQ(sport, FIT_SPORT_CYCLING) << "Sport should be cycling";
|
||||
|
||||
// Verify all developer fields were read correctly from WorkoutMesg
|
||||
EXPECT_TRUE(verifyDeveloperFields(workoutName, workoutSource, pelotonWorkoutId,
|
||||
pelotonUrl, trainingProgramFile))
|
||||
<< "Developer fields should be read correctly from WorkoutMesg";
|
||||
|
||||
qDebug() << "✓ New format developer fields test passed";
|
||||
}
|
||||
|
||||
void QFitTestSuite::test_databaseReadability() {
|
||||
// Create a FIT file with new format
|
||||
QString filename = createNewFormatFitFile();
|
||||
ASSERT_TRUE(QFile::exists(filename)) << "Failed to create FIT file";
|
||||
|
||||
// Copy to test-artifacts directory for download
|
||||
QDir artifactsDir("test-artifacts");
|
||||
if (!artifactsDir.exists()) {
|
||||
artifactsDir.mkpath(".");
|
||||
}
|
||||
QString artifactPath = "test-artifacts/test_database_readability.fit";
|
||||
QFile::remove(artifactPath);
|
||||
QFile::copy(filename, artifactPath);
|
||||
qDebug() << "FIT file saved to:" << artifactPath;
|
||||
|
||||
// Create a temporary database path
|
||||
QString dbPath = tempDir->filePath("test_db.sqlite");
|
||||
|
||||
// Create a FIT database processor with the database path
|
||||
FitDatabaseProcessor processor(dbPath);
|
||||
|
||||
// Setup event loop to wait for async processing
|
||||
QEventLoop loop;
|
||||
QTimer timeout;
|
||||
timeout.setSingleShot(true);
|
||||
timeout.setInterval(5000); // 5 second timeout
|
||||
|
||||
// Process the FIT file
|
||||
bool processed = false;
|
||||
QObject::connect(&processor, &FitDatabaseProcessor::fileProcessed,
|
||||
[&processed, &loop](const QString&) {
|
||||
processed = true;
|
||||
loop.quit();
|
||||
});
|
||||
|
||||
bool error = false;
|
||||
QString errorMsg;
|
||||
QObject::connect(&processor, &FitDatabaseProcessor::error,
|
||||
[&error, &errorMsg, &loop](const QString& msg) {
|
||||
qDebug() << "Database processor error:" << msg;
|
||||
error = true;
|
||||
errorMsg = msg;
|
||||
loop.quit();
|
||||
});
|
||||
|
||||
QObject::connect(&timeout, &QTimer::timeout, &loop, &QEventLoop::quit);
|
||||
|
||||
processor.processFile(filename);
|
||||
timeout.start();
|
||||
|
||||
// Wait for processing to complete or timeout
|
||||
loop.exec();
|
||||
timeout.stop();
|
||||
|
||||
EXPECT_TRUE(processed) << "FIT file should be processed successfully by database";
|
||||
EXPECT_FALSE(error) << "No errors should occur during database processing. Error: "
|
||||
<< errorMsg.toStdString();
|
||||
|
||||
// Copy database file to test-artifacts directory for download
|
||||
if (QFile::exists(dbPath)) {
|
||||
QString dbArtifactPath = "test-artifacts/test_database.sqlite";
|
||||
QFile::remove(dbArtifactPath);
|
||||
QFile::copy(dbPath, dbArtifactPath);
|
||||
qDebug() << "Database file saved to:" << dbArtifactPath;
|
||||
}
|
||||
|
||||
qDebug() << "✓ Database readability test passed";
|
||||
}
|
||||
66
tst/ToolTests/qfittestsuite.h
Normal file
66
tst/ToolTests/qfittestsuite.h
Normal file
@@ -0,0 +1,66 @@
|
||||
#ifndef QFITTESTSUITE_H
|
||||
#define QFITTESTSUITE_H
|
||||
|
||||
#include "gtest/gtest.h"
|
||||
#include <QString>
|
||||
#include <QTemporaryDir>
|
||||
#include <QList>
|
||||
#include "../../src/sessionline.h"
|
||||
#include "../../src/devices/bluetoothdevice.h"
|
||||
|
||||
/**
|
||||
* @brief Test suite for qfit FIT file reading/writing
|
||||
*
|
||||
* Tests developer fields moved from Session to WorkoutMesg for better
|
||||
* Garmin Connect compatibility
|
||||
*/
|
||||
class QFitTestSuite: public testing::Test {
|
||||
|
||||
public:
|
||||
QFitTestSuite();
|
||||
~QFitTestSuite() override;
|
||||
|
||||
/**
|
||||
* @brief Test that FIT files with new format (developer fields in WorkoutMesg) can be read correctly
|
||||
*/
|
||||
void test_newFormatDeveloperFields();
|
||||
|
||||
/**
|
||||
* @brief Test that FIT files can be processed by the database
|
||||
*/
|
||||
void test_databaseReadability();
|
||||
|
||||
protected:
|
||||
void SetUp() override;
|
||||
void TearDown() override;
|
||||
|
||||
private:
|
||||
QTemporaryDir* tempDir;
|
||||
|
||||
/**
|
||||
* @brief Create a sample session for testing
|
||||
*/
|
||||
QList<SessionLine> createTestSession();
|
||||
|
||||
/**
|
||||
* @brief Create a FIT file with developer fields in new format (WorkoutMesg)
|
||||
*/
|
||||
QString createNewFormatFitFile();
|
||||
|
||||
/**
|
||||
* @brief Verify developer fields were read correctly
|
||||
*/
|
||||
bool verifyDeveloperFields(const QString& workoutName, const QString& workoutSource,
|
||||
const QString& pelotonWorkoutId, const QString& pelotonUrl,
|
||||
const QString& trainingProgramFile);
|
||||
};
|
||||
|
||||
TEST_F(QFitTestSuite, TestNewFormatDeveloperFields) {
|
||||
this->test_newFormatDeveloperFields();
|
||||
}
|
||||
|
||||
TEST_F(QFitTestSuite, TestDatabaseReadability) {
|
||||
this->test_databaseReadability();
|
||||
}
|
||||
|
||||
#endif // QFITTESTSUITE_H
|
||||
@@ -21,6 +21,7 @@ SOURCES += \
|
||||
Devices/devicetestdataindex.cpp \
|
||||
Erg/ergtabletestsuite.cpp \
|
||||
GarminConnect/garminconnecttestsuite.cpp \
|
||||
ToolTests/qfittestsuite.cpp \
|
||||
ToolTests/testsettingstestsuite.cpp \
|
||||
ToolTests/testtrainingloadtestsuite.cpp \
|
||||
Tools/testsettings.cpp \
|
||||
@@ -55,6 +56,7 @@ HEADERS += \
|
||||
Devices/devicetestdataindex.h \
|
||||
Erg/ergtabletestsuite.h \
|
||||
GarminConnect/garminconnecttestsuite.h \
|
||||
ToolTests/qfittestsuite.h \
|
||||
ToolTests/testsettingstestsuite.h \
|
||||
ToolTests/testtrainingloadtestsuite.h \
|
||||
Tools/devicetypeid.h \
|
||||
|
||||
Reference in New Issue
Block a user