Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1069,7 +1069,8 @@ jobs:
--filter 'src/' \
--exclude 'src/OgreXML/' \
--exclude 'src/dependencies/' \
--exclude '.*/(LLMManager|LLMWorker|ModelDownloader|SDManager)\.cpp' \
--exclude '.*/(LLMManager|LLMWorker|ModelDownloader|SDManager|AIChatManager)\.cpp' \
--exclude '.*/(AIChatManager|SDManager|SDWorker)\.h' \
--exclude '.*_test\.cpp' \
--exclude '.*TestHelpers\.h' \
--exclude '.*QtAppEnvironment_test\.cpp' \
Expand Down
3 changes: 2 additions & 1 deletion sonar-project.properties
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ sonar.exclusions=**/OgreXML/**,**/dependencies/**,**/*_autogen/**,**/CMakeFiles/
# Coverage exclusions - exclude test infrastructure only
sonar.coverage.exclusions=**/*_test.cpp,**/test_*.cpp,tests/**/*.cpp,tests/**/*.qml,\
**/*_autogen/**,**/TestHelpers.h,**/test_main.cpp,\
**/LLMManager.cpp,**/LLMWorker.cpp,**/ModelDownloader.cpp,**/SDManager.cpp
**/LLMManager.cpp,**/LLMWorker.cpp,**/ModelDownloader.cpp,**/SDManager.cpp,\
**/AIChatManager.cpp,**/AIChatManager.h,**/SDManager.h,**/SDWorker.h

# Coverage settings for C++ projects
# Generic coverage report (SonarQube XML format from gcovr)
Expand Down
42 changes: 42 additions & 0 deletions src/CLIPipeline_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -875,6 +875,21 @@ TEST(CLIPipelineCmdFixError, NonexistentFileWithFlagsAndLongOutputFlag)
EXPECT_EQ(CLIPipeline::cmdFix(args.argc(), args.argv()), 1);
}

TEST(CLIPipelineCmdFixError, ExistingInvalidFileWithAllFlagReturnsError)
{
const QString file = QDir::tempPath() + "/cli_test_fix_existing_invalid_input.fbx";
QFile invalid(file);
ASSERT_TRUE(invalid.open(QIODevice::WriteOnly | QIODevice::Text));
invalid.write("invalid fbx payload");
invalid.close();

QByteArray fileBa = file.toUtf8();
TestArgv args({"qtmesh", "fix", fileBa.constData(), "--all"});
EXPECT_EQ(CLIPipeline::cmdFix(args.argc(), args.argv()), 1);

QFile::remove(file);
}

// -- cmdFix success paths --

TEST_F(CLIPipelineCmdTest, CmdFix_Basic)
Expand Down Expand Up @@ -1016,6 +1031,21 @@ TEST(CLIPipelineCmdAnimError, MergeModeWithMissingBaseFile)
EXPECT_EQ(CLIPipeline::cmdAnim(args.argc(), args.argv()), 1);
}

TEST(CLIPipelineCmdAnimError, RenameModeWithoutOutputUsesDefaultOutputPath)
{
TestArgv args({"qtmesh", "anim", "/tmp/nonexistent_cli_test_rename_default_33333.fbx",
"--rename", "OldAnimName", "NewAnimName"});
EXPECT_EQ(CLIPipeline::cmdAnim(args.argc(), args.argv()), 1);
}

TEST(CLIPipelineCmdAnimError, MergeModeWithoutOutputUsesDefaultOutputPath)
{
TestArgv args({"qtmesh", "anim", "/tmp/nonexistent_cli_test_merge_default_44444.fbx",
"--merge", "/tmp/nonexistent_anim_source_44444_a.fbx",
"/tmp/nonexistent_anim_source_44444_b.fbx"});
EXPECT_EQ(CLIPipeline::cmdAnim(args.argc(), args.argv()), 1);
}

// -- cmdAnim list --

TEST_F(CLIPipelineCmdTest, CmdAnimList_Text)
Expand Down Expand Up @@ -1621,6 +1651,18 @@ TEST(CLIPipelineCmdLodError, NonexistentFileWithRemoveMode)
EXPECT_EQ(CLIPipeline::cmdLod(args.argc(), args.argv()), 1);
}

TEST(CLIPipelineCmdLodError, NonexistentFileWithInfoAndJsonMode)
{
TestArgv args({"qtmesh", "lod", "/tmp/nonexistent_cli_lod_info_67890.fbx", "--info", "--json"});
EXPECT_EQ(CLIPipeline::cmdLod(args.argc(), args.argv()), 1);
}

TEST(CLIPipelineCmdLodError, InvalidCountValueReportsModeError)
{
TestArgv args({"qtmesh", "lod", "/tmp/nonexistent_cli_lod_count_abc.fbx", "--count", "abc"});
EXPECT_EQ(CLIPipeline::cmdLod(args.argc(), args.argv()), 2);
}

class CLIPipelineCmdLodTest : public ::testing::Test {
protected:
void SetUp() override {
Expand Down
121 changes: 121 additions & 0 deletions src/MaterialEditorQML_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
#include <QSignalSpy>
#include <QThread>
#include <QDir>
#include <QFile>
#include <QTemporaryDir>
#include "MaterialEditorQML.h"
#include "Manager.h"
#include <OgreException.h>
Expand Down Expand Up @@ -138,6 +140,66 @@ TEST_F(MaterialEditorQMLTest, FileSystem_ListDirectory) {
EXPECT_EQ(empty.size(), 0);
}

TEST_F(MaterialEditorQMLTest, FileSystem_ListDirectoryFiltersOnlyImagesAndDirectories) {
QTemporaryDir tempDir;
ASSERT_TRUE(tempDir.isValid());

const QString subdirPath = tempDir.filePath("textures");
ASSERT_TRUE(QDir().mkpath(subdirPath));

QFile imageFile(tempDir.filePath("albedo.png"));
ASSERT_TRUE(imageFile.open(QIODevice::WriteOnly));
imageFile.write("png");
imageFile.close();

QFile textFile(tempDir.filePath("notes.txt"));
ASSERT_TRUE(textFile.open(QIODevice::WriteOnly));
textFile.write("txt");
textFile.close();

QVariantList entries = editor->listDirectory(tempDir.path());
ASSERT_GE(entries.size(), 2);

bool foundDir = false;
bool foundImage = false;
bool foundText = false;
for (const QVariant& v : entries) {
const QVariantMap item = v.toMap();
const QString name = item.value("name").toString();
const QString type = item.value("type").toString();
if (name == "textures" && type == "dir") foundDir = true;
if (name == "albedo.png" && type == "file") foundImage = true;
if (name == "notes.txt") foundText = true;
}

EXPECT_TRUE(foundDir);
EXPECT_TRUE(foundImage);
EXPECT_FALSE(foundText);
}

TEST_F(MaterialEditorQMLTest, FileSystem_GetFileSizeStringCoversKbAndMbBranches) {
QTemporaryDir tempDir;
ASSERT_TRUE(tempDir.isValid());

const QString kbPath = tempDir.filePath("kb.bin");
QFile kbFile(kbPath);
ASSERT_TRUE(kbFile.open(QIODevice::WriteOnly));
kbFile.write(QByteArray(2048, 'k'));
kbFile.close();

const QString mbPath = tempDir.filePath("mb.bin");
QFile mbFile(mbPath);
ASSERT_TRUE(mbFile.open(QIODevice::WriteOnly));
mbFile.write(QByteArray(2 * 1024 * 1024, 'm'));
mbFile.close();

const QString kbSize = editor->getFileSizeString(kbPath);
const QString mbSize = editor->getFileSizeString(mbPath);

EXPECT_TRUE(kbSize.contains("KB"));
EXPECT_TRUE(mbSize.contains("MB"));
}

// ===========================================================================
// Basic fixture tests -- enum name getters
// ===========================================================================
Expand Down Expand Up @@ -1877,6 +1939,65 @@ TEST_F(MaterialEditorQMLTest, LLMProperties_InitialState) {
Q_UNUSED(model);
}

TEST_F(MaterialEditorQMLTest, GenerateMaterialFromPrompt_EmptyPromptEmitsError) {
QSignalSpy errorSpy(editor.get(), &MaterialEditorQML::aiGenerationError);

editor->generateMaterialFromPrompt("");

ASSERT_EQ(errorSpy.count(), 1);
const QList<QVariant> args = errorSpy.takeFirst();
ASSERT_EQ(args.size(), 1);
EXPECT_EQ(args.at(0).toString(), "Please enter a prompt");
}

TEST_F(MaterialEditorQMLTest, GenerateMaterialFromPrompt_NoModelLoadedEmitsError) {
if (editor->llmModelLoaded()) {
GTEST_SKIP() << "LLM model is already loaded in this environment";
}

QSignalSpy errorSpy(editor.get(), &MaterialEditorQML::aiGenerationError);

editor->generateMaterialFromPrompt("polished metal with scratches");

ASSERT_GE(errorSpy.count(), 1);
const QList<QVariant> args = errorSpy.takeFirst();
ASSERT_EQ(args.size(), 1);
EXPECT_TRUE(args.at(0).toString().contains("No AI model loaded"));
}

TEST_F(MaterialEditorQMLTest, GenerateTextureFromPrompt_EmptyPromptEmitsError) {
QSignalSpy errorSpy(editor.get(), &MaterialEditorQML::sdGenerationError);

editor->generateTextureFromPrompt("", 512, 512);

ASSERT_EQ(errorSpy.count(), 1);
const QList<QVariant> args = errorSpy.takeFirst();
ASSERT_EQ(args.size(), 1);
EXPECT_EQ(args.at(0).toString(), "Please enter a texture prompt");
}

TEST_F(MaterialEditorQMLTest, GenerateTextureFromPrompt_ReportsUnavailableBackendOrModel) {
QSignalSpy errorSpy(editor.get(), &MaterialEditorQML::sdGenerationError);

editor->generateTextureFromPrompt("brushed steel", 512, 512);

ASSERT_GE(errorSpy.count(), 1);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Guard SD error-path test when a model is already loaded

generateTextureFromPrompt("brushed steel", ...) only emits sdGenerationError synchronously when SD is disabled or no SD model is loaded; when ENABLE_STABLE_DIFFUSION is on and a model has already been auto-loaded, the call takes the generation path and this assertion fails immediately even though nothing is wrong. This makes the test environment-dependent/flaky (similar to the LLM test right above, which already skips when a model is loaded), so the check should be conditioned on sdModelLoaded() or otherwise handle the loaded-model path explicitly.

Useful? React with 👍 / 👎.

const QList<QVariant> args = errorSpy.takeFirst();
ASSERT_EQ(args.size(), 1);
const QString message = args.at(0).toString();

if (editor->stableDiffusionEnabled()) {
EXPECT_TRUE(message.contains("No SD model loaded") || message.contains("AI Settings"));
} else {
EXPECT_TRUE(message.contains("Stable Diffusion support is not enabled"));
}
}
Comment on lines +1979 to +1994

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Make the SD unavailable-path assertion deterministic across environments.

This test hard-requires an error signal, but in environments where SD is fully available/configured, generateTextureFromPrompt(...) may not emit sdGenerationError, causing a false failure.

Proposed test hardening
 TEST_F(MaterialEditorQMLTest, GenerateTextureFromPrompt_ReportsUnavailableBackendOrModel) {
     QSignalSpy errorSpy(editor.get(), &MaterialEditorQML::sdGenerationError);

     editor->generateTextureFromPrompt("brushed steel", 512, 512);

-    ASSERT_GE(errorSpy.count(), 1);
+    if (errorSpy.count() == 0 && !errorSpy.wait(300)) {
+        GTEST_SKIP() << "Stable Diffusion backend appears available/configured; unavailable-path assertion not applicable";
+    }
+    ASSERT_GE(errorSpy.count(), 1);
     const QList<QVariant> args = errorSpy.takeFirst();
     ASSERT_EQ(args.size(), 1);
     const QString message = args.at(0).toString();

     if (editor->stableDiffusionEnabled()) {
         EXPECT_TRUE(message.contains("No SD model loaded") || message.contains("AI Settings"));
     } else {
         EXPECT_TRUE(message.contains("Stable Diffusion support is not enabled"));
     }
 }

As per coding guidelines: "Features depending on optional components (e.g., local LLM / llama.cpp) may not be available in the test environment — guard with #ifdef ENABLE_LOCAL_LLM or skip gracefully".

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
TEST_F(MaterialEditorQMLTest, GenerateTextureFromPrompt_ReportsUnavailableBackendOrModel) {
QSignalSpy errorSpy(editor.get(), &MaterialEditorQML::sdGenerationError);
editor->generateTextureFromPrompt("brushed steel", 512, 512);
ASSERT_GE(errorSpy.count(), 1);
const QList<QVariant> args = errorSpy.takeFirst();
ASSERT_EQ(args.size(), 1);
const QString message = args.at(0).toString();
if (editor->stableDiffusionEnabled()) {
EXPECT_TRUE(message.contains("No SD model loaded") || message.contains("AI Settings"));
} else {
EXPECT_TRUE(message.contains("Stable Diffusion support is not enabled"));
}
}
TEST_F(MaterialEditorQMLTest, GenerateTextureFromPrompt_ReportsUnavailableBackendOrModel) {
QSignalSpy errorSpy(editor.get(), &MaterialEditorQML::sdGenerationError);
editor->generateTextureFromPrompt("brushed steel", 512, 512);
if (errorSpy.count() == 0 && !errorSpy.wait(300)) {
GTEST_SKIP() << "Stable Diffusion backend appears available/configured; unavailable-path assertion not applicable";
}
ASSERT_GE(errorSpy.count(), 1);
const QList<QVariant> args = errorSpy.takeFirst();
ASSERT_EQ(args.size(), 1);
const QString message = args.at(0).toString();
if (editor->stableDiffusionEnabled()) {
EXPECT_TRUE(message.contains("No SD model loaded") || message.contains("AI Settings"));
} else {
EXPECT_TRUE(message.contains("Stable Diffusion support is not enabled"));
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/MaterialEditorQML_test.cpp` around lines 1979 - 1994, The test
GenerateTextureFromPrompt_ReportsUnavailableBackendOrModel should be made
deterministic by skipping the "SD unavailable" assertions when Stable Diffusion
is present: check editor->stableDiffusionEnabled() at the start of the test and
call GTEST_SKIP() (or otherwise short-circuit the test) with an explanatory
message if true, otherwise keep the existing errorSpy assertions that expect
sdGenerationError after calling editor->generateTextureFromPrompt; this uses the
MaterialEditorQMLTest test, editor->generateTextureFromPrompt,
editor->stableDiffusionEnabled(), and the sdGenerationError signal to locate
where to add the guard.


TEST_F(MaterialEditorQMLTest, StopGenerationMethodsWithoutActiveJobsDoNotCrash) {
EXPECT_NO_THROW(editor->stopAIGeneration());
EXPECT_NO_THROW(editor->stopTextureGeneration());
}

// ===========================================================================
// Additional coverage tests (MaterialEditorQMLTest fixture)
// ===========================================================================
Expand Down
30 changes: 30 additions & 0 deletions src/mainwindow_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@
#include <QKeyEvent>
#include <QMenu>
#include <QMimeData>
#include <QMessageBox>
#include <QSettings>
#include <QSignalSpy>
#include <QTemporaryDir>
#include <QThread>
#include <QTimer>

// NOTE: These access-specifier redefinitions are a pragmatic test-only workaround
// to cover MainWindow internals. Prefer dedicated test APIs or friend tests when feasible.
Expand Down Expand Up @@ -143,6 +145,11 @@ TEST_F(MainWindowTest, KeyFFrameSelectionEmptyDoesNotCrash) {
EXPECT_NO_THROW(window->keyPressEvent(&event));
}

TEST_F(MainWindowTest, KeyReleaseEventDoesNotCrash) {
QKeyEvent event(QEvent::KeyRelease, Qt::Key_W, Qt::NoModifier);
EXPECT_NO_THROW(window->keyReleaseEvent(&event));
}

// keyReleaseEvent is protected — tested implicitly via keyPressEvent

// ---- setPlaying ----
Expand Down Expand Up @@ -318,6 +325,29 @@ TEST_F(MainWindowTest, OpenRecentFileWithoutActionSenderDoesNothing) {
EXPECT_TRUE(window->mUriList.isEmpty());
}

TEST_F(MainWindowTest, OpenRecentFileRemovesMissingPathFromSettings) {
const QString missingPath = tempDir.filePath("missing.mesh");
window->addToRecentFiles(missingPath);
QAction* action = recentFileAction(0);
ASSERT_NE(action, nullptr);
ASSERT_EQ(action->data().toString(), missingPath);

// Auto-close the warning QMessageBox shown by openRecentFile().
QTimer::singleShot(0, []() {
for (QWidget* w : QApplication::topLevelWidgets()) {
if (auto* box = qobject_cast<QMessageBox*>(w)) {
box->accept();
}
}
});

action->trigger();

const QStringList files = QSettings().value("RecentFiles/files").toStringList();
EXPECT_FALSE(files.contains(missingPath));
EXPECT_FALSE(window->mUriList.contains(missingPath));
}

TEST_F(MainWindowTest, ToolbarTogglesUpdateWidgetVisibility) {
window->on_actionObjects_Toolbar_toggled(false);
EXPECT_TRUE(window->ui->objectsToolbar->isHidden());
Expand Down
Loading