]> Nutra Git (v1) - nutratech/gui.git/commitdiff
wip keep going dev 2/head
authorShane Jaroch <chown_tee@proton.me>
Mon, 26 Jan 2026 11:37:54 +0000 (06:37 -0500)
committerShane Jaroch <chown_tee@proton.me>
Mon, 26 Jan 2026 11:37:54 +0000 (06:37 -0500)
.github/workflows/ci-full.yml
.github/workflows/ubuntu-24.04.yml
CMakeLists.txt
include/db/reciperepository.h
src/db/databasemanager.cpp
src/db/reciperepository.cpp
tests/test_reciperepository.cpp [new file with mode: 0644]

index 2ae5b294e6b8a53ae2540ac585386ae51b43a2fd..943a8cf69ba2cb18d2b048e7daa44a3553c8e7bf 100644 (file)
@@ -37,6 +37,4 @@ jobs:
         run: make release
 
       - name: Test
-        env:
-          QT_QPA_PLATFORM: offscreen
         run: make test
index 81ff1269314766f219fda6153ffad19175e825d0..c20728dba98eedcfc0a5ee70c1ebc7a3098358b8 100644 (file)
@@ -30,8 +30,6 @@ jobs:
         run: make release
 
       - name: Test
-        env:
-          QT_QPA_PLATFORM: offscreen
         run: make test
 
       - name: Upload Artifact
index f523a119b45ba1a0fa21c77e78c7a0dd98d3efc8..69baf5e23ec95c90f884bf0eea992aeab5da3943 100644 (file)
@@ -15,11 +15,13 @@ find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Widgets Sql)
 
 
 # Sources
-file(GLOB_RECURSE SOURCES CONFIGURE_DEPENDS "src/*.cpp")
-file(GLOB_RECURSE HEADERS CONFIGURE_DEPENDS "include/*.h")
+file(GLOB_RECURSE CORE_SOURCES_CPP "src/db/*.cpp" "src/utils/*.cpp")
+file(GLOB_RECURSE CORE_HEADERS "include/db/*.h" "include/utils/*.h")
+set(CORE_SOURCES ${CORE_SOURCES_CPP} ${CORE_HEADERS})
 
-# Filter out main.cpp for the library
-list(FILTER SOURCES EXCLUDE REGEX ".*src/main\\.cpp$")
+file(GLOB_RECURSE UI_SOURCES_CPP "src/widgets/*.cpp" "src/mainwindow.cpp")
+file(GLOB_RECURSE UI_HEADERS "include/widgets/*.h" "include/mainwindow.h")
+set(UI_SOURCES ${UI_SOURCES_CPP} ${UI_HEADERS})
 
 # Versioning
 if(NOT NUTRA_VERSION)
@@ -38,14 +40,10 @@ if(NOT NUTRA_VERSION)
 endif()
 add_compile_definitions(NUTRA_VERSION_STRING="${NUTRA_VERSION}")
 
-# Core Library
-add_library(nutra_lib STATIC ${SOURCES} ${HEADERS} "resources.qrc")
-target_include_directories(nutra_lib PUBLIC ${CMAKE_SOURCE_DIR}/include)
-target_link_libraries(nutra_lib PUBLIC Qt${QT_VERSION_MAJOR}::Widgets Qt${QT_VERSION_MAJOR}::Sql)
-
 # Main Executable
-add_executable(nutra src/main.cpp)
-target_link_libraries(nutra PRIVATE nutra_lib)
+add_executable(nutra src/main.cpp ${CORE_SOURCES} ${UI_SOURCES} "resources.qrc")
+target_include_directories(nutra PUBLIC ${CMAKE_SOURCE_DIR}/include)
+target_link_libraries(nutra PRIVATE Qt${QT_VERSION_MAJOR}::Widgets Qt${QT_VERSION_MAJOR}::Sql)
 
 # Testing
 enable_testing()
@@ -58,10 +56,12 @@ add_custom_target(build_tests)
 
 foreach(TEST_SOURCE ${TEST_SOURCES})
     get_filename_component(TEST_NAME ${TEST_SOURCE} NAME_WE)
-    
-    add_executable(${TEST_NAME} EXCLUDE_FROM_ALL ${TEST_SOURCE})
-    target_link_libraries(${TEST_NAME} PRIVATE nutra_lib Qt${QT_VERSION_MAJOR}::Test)
-    
+
+    # Create independent test executable with only CORE sources
+    add_executable(${TEST_NAME} EXCLUDE_FROM_ALL ${TEST_SOURCE} ${CORE_SOURCES})
+    target_include_directories(${TEST_NAME} PRIVATE ${CMAKE_SOURCE_DIR}/include)
+    target_link_libraries(${TEST_NAME} PRIVATE Qt${QT_VERSION_MAJOR}::Test Qt${QT_VERSION_MAJOR}::Sql)
+
     add_test(NAME ${TEST_NAME} COMMAND ${TEST_NAME})
     add_dependencies(build_tests ${TEST_NAME})
 endforeach()
index df4312d2eda43075c278b0dda90b72470952a44e..1beb30adb36b755686bd05c460dfb63eedb3cc29 100644 (file)
@@ -3,6 +3,7 @@
 
 #include <QDateTime>
 #include <QString>
+#include <map>
 #include <vector>
 
 struct RecipeItem {
@@ -40,6 +41,11 @@ public:
     bool removeIngredient(int recipeId, int foodId);
     bool updateIngredient(int recipeId, int foodId, double amount);
     std::vector<RecipeIngredient> getIngredients(int recipeId);
+
+private:
+    void processCsvFile(const QString& filePath, std::map<QString, int>& recipeMap);
+    int getOrCreateRecipe(const QString& name, const QString& instructions,
+                          std::map<QString, int>& recipeMap);
 };
 
 #endif  // RECIPEREPOSITORY_H
index a957a1678219f1d61f3b3d612cc48d2904f5e630..1a87d490cda8e742a981b44456ae66ad1608f45c 100644 (file)
@@ -156,6 +156,28 @@ void DatabaseManager::initUserDatabase() {
         }
         applySchema(query, schemaPath);
     }
+
+    // Ensure recipe tables exist
+    {
+        query.exec(
+            "CREATE TABLE IF NOT EXISTS recipe ("
+            "id integer PRIMARY KEY AUTOINCREMENT,"
+            "uuid text NOT NULL UNIQUE DEFAULT (hex(randomblob(24))),"
+            "name text NOT NULL,"
+            "instructions text,"
+            "created int DEFAULT (strftime ('%s', 'now'))"
+            ");");
+
+        query.exec(
+            "CREATE TABLE IF NOT EXISTS recipe_ingredient ("
+            "recipe_id int NOT NULL,"
+            "food_id int NOT NULL,"
+            "amount real NOT NULL,"
+            "msre_id int,"
+            "FOREIGN KEY (recipe_id) REFERENCES recipe (id) ON DELETE CASCADE,"
+            "FOREIGN KEY (msre_id) REFERENCES measure (id) ON UPDATE CASCADE ON DELETE SET NULL"
+            ");");
+    }
 }
 
 void DatabaseManager::applySchema(QSqlQuery& query, const QString& schemaPath) {
index 31fc8598a2c3e9ac38af0af136d6087a7c43a84f..2ba6e32903142b34c60e59cfd7bc7436bb8bfce0 100644 (file)
@@ -186,52 +186,57 @@ void RecipeRepository::loadCsvRecipes(const QString& directory) {
     QDir dir(directory);
     if (!dir.exists()) return;
 
+    // Pre-load all recipes into a map for O(1) lookup
+    std::vector<RecipeItem> existingRecipes = getAllRecipes();
+    std::map<QString, int> recipeMap;
+    for (const auto& r : existingRecipes) {
+        recipeMap[r.name] = r.id;
+    }
+
     QStringList filters;
     filters << "*.csv";
     QFileInfoList fileList = dir.entryInfoList(filters, QDir::Files);
 
     for (const auto& fileInfo : fileList) {
-        QFile file(fileInfo.absoluteFilePath());
-        if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) continue;
+        processCsvFile(fileInfo.absoluteFilePath(), recipeMap);
+    }
+}
 
-        while (!file.atEnd()) {
-            QString line = file.readLine().trimmed();
-            if (line.isEmpty() || line.startsWith("#")) continue;
+void RecipeRepository::processCsvFile(const QString& filePath, std::map<QString, int>& recipeMap) {
+    QFile file(filePath);
+    if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) return;
 
-            QStringList parts = line.split(',');
-            if (parts.size() < 4) continue;
+    while (!file.atEnd()) {
+        QString line = file.readLine().trimmed();
+        if (line.isEmpty() || line.startsWith("#")) continue;
 
-            QString recipeName = parts[0].trimmed();
-            QString instructions = parts[1].trimmed();
-            int foodId = parts[2].toInt();
-            double amount = parts[3].toDouble();
+        QStringList parts = line.split(',');
+        if (parts.size() < 4) continue;
 
-            if (foodId <= 0 || amount <= 0) continue;
+        QString recipeName = parts[0].trimmed();
+        QString instructions = parts[1].trimmed();
+        int foodId = parts[2].toInt();
+        double amount = parts[3].toDouble();
 
-            // Check if recipe exists or create it
-            int recipeId = -1;
+        if (foodId <= 0 || amount <= 0) continue;
 
-            // Inefficient check, but works for now.
-            // Better: Cache existing recipe names -> IDs or add getRecipeByName
-            auto existingRecipes = getAllRecipes();
-            for (const auto& r : existingRecipes) {
-                if (r.name == recipeName) {
-                    recipeId = r.id;
-                    break;
-                }
-            }
+        int recipeId = getOrCreateRecipe(recipeName, instructions, recipeMap);
+        if (recipeId != -1) {
+            addIngredient(recipeId, foodId, amount);
+        }
+    }
+    file.close();
+}
 
-            if (recipeId == -1) {
-                recipeId = createRecipe(recipeName, instructions);
-            }
+int RecipeRepository::getOrCreateRecipe(const QString& name, const QString& instructions,
+                                        std::map<QString, int>& recipeMap) {
+    if (recipeMap.count(name) != 0U) {
+        return recipeMap[name];
+    }
 
-            if (recipeId != -1) {
-                // Check if ingredient exists?
-                // Or just try insert? database might error on duplicate PK if defined.
-                // Assuming (recipe_id, food_id) unique constraint?
-                addIngredient(recipeId, foodId, amount);
-            }
-        }
-        file.close();
+    int newId = createRecipe(name, instructions);
+    if (newId != -1) {
+        recipeMap[name] = newId;
     }
+    return newId;
 }
diff --git a/tests/test_reciperepository.cpp b/tests/test_reciperepository.cpp
new file mode 100644 (file)
index 0000000..93d2afd
--- /dev/null
@@ -0,0 +1,53 @@
+#include <QDir>
+#include <QFile>
+#include <QtTest>
+
+#include "db/databasemanager.h"
+#include "db/reciperepository.h"
+
+class TestRecipeRepository : public QObject {
+    Q_OBJECT
+
+private slots:
+    void initTestCase() {
+        // Setup temporary DB and directory
+        QStandardPaths::setTestModeEnabled(true);
+        // Ensure user DB is open (in memory or temp file)
+        // DatabaseManager singleton might need configuration
+    }
+
+    void testLoadCsv() {
+        RecipeRepository repo;
+
+        // Create dummy CSV
+        QString recipeDir = QDir::tempPath() + "/nutra_test_recipes";
+        QDir().mkpath(recipeDir);
+
+        QFile file(recipeDir + "/test_recipe.csv");
+        if (file.open(QIODevice::WriteOnly | QIODevice::Text)) {
+            QTextStream out(&file);
+            out << "Unit Test Recipe,Mix it up,1234,200\n";
+            file.close();
+        }
+
+        repo.loadCsvRecipes(recipeDir);
+
+        // Verify
+        auto recipes = repo.getAllRecipes();
+        bool found = false;
+        for (const auto& r : recipes) {
+            if (r.name == "Unit Test Recipe") {
+                found = true;
+                break;
+            }
+        }
+        QVERIFY(found);
+    }
+
+    void cleanupTestCase() {
+        QDir(QDir::tempPath() + "/nutra_test_recipes").removeRecursively();
+    }
+};
+
+QTEST_MAIN(TestRecipeRepository)
+#include "test_reciperepository.moc"