run: make release
- name: Test
- env:
- QT_QPA_PLATFORM: offscreen
run: make test
run: make release
- name: Test
- env:
- QT_QPA_PLATFORM: offscreen
run: make test
- name: Upload Artifact
# 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)
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()
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()
#include <QDateTime>
#include <QString>
+#include <map>
#include <vector>
struct RecipeItem {
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
}
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) {
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;
}
--- /dev/null
+#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"