Skip to content

Improve MeshImporterExporter test coverage to ~95%+ - #189

Merged
fernandotonon merged 3 commits into
masterfrom
test/mesh-importer-exporter-coverage
Mar 10, 2026
Merged

Improve MeshImporterExporter test coverage to ~95%+#189
fernandotonon merged 3 commits into
masterfrom
test/mesh-importer-exporter-coverage

Conversation

@fernandotonon

@fernandotonon fernandotonon commented Mar 10, 2026

Copy link
Copy Markdown
Owner

Summary

  • Add 25 new tests targeting every uncovered branch in MeshImporterExporter: versioned Ogre Mesh exports, XML import error paths, format URI edge cases, and exporter error handling
  • Add writeTestXMLFile() helper to TestHelpers.h for creating test XML files
  • Data-driven test validates all 18 export format URI extensions in a single test

Test plan

  • All 16 standalone tests pass without Ogre (verified locally)
  • All 64 fixture tests compile and gracefully GTEST_SKIP on macOS (no Ogre plugins)
  • CI Linux run exercises the full test suite with Ogre + Xvfb

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Tests

    • Expanded test coverage for mesh import/export: many importer/exporter edge cases, format fallbacks, versioned formats, in-memory exports, XML/OgreXML import/export edge cases, skeleton preservation, and multiple round-trip scenarios.
    • Added test utilities for creating/cleaning up test artifacts and writing test XML files to support the new suites.
  • New Features

    • Material editor: ability to stop ongoing AI material generation and clearer error reporting when no prompt or AI model is available.

Add 25 new tests covering previously untested code paths:
- All 18 export format URI extensions (data-driven)
- All 5 versioned Ogre Mesh exports (v1.0 through v1.10)
- Import edge cases: empty list, empty string, camera configuration
- Exporter errors: no entity on node, unknown format fallback to suffix
- XML import error paths: invalid XML, missing <mesh> root, empty
  submeshes, missing material, missing faces, empty geometry, shared
  geometry, missing skeleton file, positions-only (no normals/UVs)
- writeTestXMLFile() helper added to TestHelpers.h

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Mar 10, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Adds a large suite of MeshImporterExporter tests and a small TestHelpers file-write utility; also adds LCOV coverage exclusion markers and defensive checks in LLM-related files and a new MaterialEditorQML::stopAIGeneration() method. No public API surface removals or breaking changes.

Changes

Cohort / File(s) Summary
MeshImporterExporter tests
src/MeshImporterExporter_test.cpp
Adds ~491 lines of tests: export dialog/filter validation (18 formats), multiple export URI/extension paths (FBX, versioned Ogre meshes), importer/exporter edge cases, many round-trip and in-memory export tests (OBJ/STL/PLY/X/GLB2/COLLADA/OGRE/XML), XML/OgreXML edge-case checks, skeleton import/export verification, and artifact cleanup.
Test helpers & includes
src/TestHelpers.h, src/MeshImporterExporter_test.cpp
Adds writeTestXMLFile(const QString&, const QByteArray&) helper and #include <QFile>; adds #include <QDir> in the test file to support filesystem cleanup.
LLM coverage guards
src/LLMManager.cpp, src/LLMWorker.cpp
Wraps multiple runtime paths in LCOV_EXCL_START/STOP to exclude model-dependent code from coverage reports; adds a model-file existence check in tryAutoLoadModel and ensures proper returns on disabled paths. No signature changes.
Material editor AI control
src/MaterialEditorQML.cpp
Adds early error returns when AI model or prompt is missing, LCOV exclusions around model-dependent code, emits aiGenerationError/aiGenerationCompleted as appropriate, and introduces void stopAIGeneration() to stop ongoing generation. Declaration likely added in corresponding header.

Sequence Diagram(s)

(omitted — changes are test additions and coverage/guard annotations; no new multi-component runtime control flow warranting a diagram)

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰
I hopped through code and spun a test,
Eighteen formats tried their best.
XML bones and FBX dreams,
Round-trip rivers and cleanup streams —
Tiny paws applaud the rest.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 24.14% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically summarizes the main objective of the PR: improving test coverage for MeshImporterExporter to ~95%+.
Description check ✅ Passed The description includes summary, technical details, and test plan sections that align well with the template structure and provide clear information about the changes.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch test/mesh-importer-exporter-coverage

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/TestHelpers.h (1)

417-429: LGTM! Consider checking write result for robustness.

The helper function is clean and useful for the XML import tests. For slightly more robust error detection, you could check the return value of write():

Optional improvement
 static inline bool writeTestXMLFile(const QString& path, const QByteArray& content)
 {
     QFile f(path);
     if (!f.open(QIODevice::WriteOnly | QIODevice::Truncate))
         return false;
-    f.write(content);
+    if (f.write(content) != content.size())
+        return false;
     f.close();
     return true;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/TestHelpers.h` around lines 417 - 429, The writeTestXMLFile helper
currently opens QFile f and calls f.write(content) but ignores the return value;
update writeTestXMLFile to capture qint64 bytesWritten = f.write(content),
ensure bytesWritten equals content.size() (and that bytesWritten != -1), close
the file, and return false if the write failed or was partial so the function
only returns true on a complete successful write.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/MeshImporterExporter_test.cpp`:
- Around line 1197-1216: The test records camBefore (auto camBefore =
...getPosition()) but never uses it; either remove the unused variable or add an
assertion comparing pre- and post-import positions to ensure the camera moved.
Update the TEST_F MeshImporterExporterTest Importer_ConfiguresCameraAfterImport
to either delete the camBefore declaration or add a check like comparing
camBefore to camAfter (e.g., EXPECT_NE(camBefore, camAfter) or a directional
check) after calling MeshImporterExporter::importer(uri) to validate the camera
actually changed.

---

Nitpick comments:
In `@src/TestHelpers.h`:
- Around line 417-429: The writeTestXMLFile helper currently opens QFile f and
calls f.write(content) but ignores the return value; update writeTestXMLFile to
capture qint64 bytesWritten = f.write(content), ensure bytesWritten equals
content.size() (and that bytesWritten != -1), close the file, and return false
if the write failed or was partial so the function only returns true on a
complete successful write.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 57b5617c-778a-4ff7-8fa6-cf930e5e6a4c

📥 Commits

Reviewing files that changed from the base of the PR and between e328297 and 5f6d590.

📒 Files selected for processing (2)
  • src/MeshImporterExporter_test.cpp
  • src/TestHelpers.h

Comment thread src/MeshImporterExporter_test.cpp

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5f6d59050e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/MeshImporterExporter_test.cpp Outdated
{
auto mesh = createInMemoryTriangleMesh("VersionedMesh_" + suffix);
auto* node = Manager::getSingleton()->addSceneNode(("VersionedNode_" + suffix).c_str());
auto* entity = sceneMgr->createEntity("VersionedEntity_" + suffix, mesh);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Create versioned export entities with the scene node name

MeshImporterExporter::exporter resolves the entity by sceneNode->getName() and returns -1 when no entity with that name exists. In this helper, the node is named VersionedNode_* but the entity is created as VersionedEntity_*, so the new Exporter_OgreMeshV1_* tests fail before exercising any version-specific export path in environments where mesh-loading tests run.

Useful? React with 👍 / 👎.

fernandotonon and others added 2 commits March 10, 2026 16:00
- Entity names now match scene node names (use node->getName()) so
  the exporter's hasEntity() lookup succeeds on CI
- Remove unused camBefore variable in camera config test
- Check write() return value in writeTestXMLFile helper

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add LCOV_EXCL_START/STOP markers around code that requires a loaded
LLM model, active generation, or downloaded GGUF file — none of which
are available in CI. Preserves coverage for all currently tested paths
(settings, validation, cleanup, error handling, initial state).

Files affected:
- LLMWorker.cpp: model load success path, unload, generate loop,
  initializeContext, cleanupContext, tokenize, detokenize
- LLMManager.cpp: worker signal handlers, model load/generate
  success paths, auto-load
- MaterialEditorQML.cpp: AI generation success path, stop generation,
  network AI callback, all onLLM* signal handlers

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
src/LLMWorker.cpp (1)

365-378: ⚠️ Potential issue | 🟠 Major

Return on token-decode failure instead of falling through to completion.

Line 366 emits generationError, but the function then reaches Line 377 and emits generationCompleted with partial output. That gives callers two terminal states for the same generation.

🧩 Minimal fix
         llama_batch nextBatch = llama_batch_get_one(&newToken, 1);
         if (llama_decode(m_ctx, nextBatch) != 0) {
+            llama_sampler_free(sampler);
+            m_isGenerating.store(false);
             emit generationError("Failed to decode token");
-            break;
+            return;
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/LLMWorker.cpp` around lines 365 - 378, The token-decode failure path
currently emits generationError but then continues to the normal completion
path, causing both generationError and generationCompleted to be emitted; update
the loop that calls llama_decode so that on a non-zero return you clean up (free
the sampler via llama_sampler_free and set m_isGenerating.store(false)) and then
return/exit immediately instead of falling through; ensure generationCompleted
is only emitted on successful completion and keep references to llama_decode,
generationError, generationCompleted, llama_sampler_free, m_isGenerating,
generatedText and generatedTokens to locate and adjust the logic.
src/MaterialEditorQML.cpp (1)

2563-2566: ⚠️ Potential issue | 🟠 Major

Complete the stop flow by handling generationStopped.

stopAIGeneration() only forwards the stop request. This class never subscribes to LLMManager::generationStopped, so a successful stop leaves the progress state stale and QML gets no terminal event.

🔌 Suggested follow-up
 // constructor
 connect(llmManager, &LLMManager::generationStarted, this, &MaterialEditorQML::onLLMGenerationStarted);
 connect(llmManager, &LLMManager::generationProgress, this, &MaterialEditorQML::onLLMGenerationProgress);
 connect(llmManager, &LLMManager::generationCompleted, this, &MaterialEditorQML::onLLMGenerationCompleted);
 connect(llmManager, &LLMManager::generationError, this, &MaterialEditorQML::onLLMGenerationError);
+connect(llmManager, &LLMManager::generationStopped, this, &MaterialEditorQML::onLLMGenerationStopped);

...
+void MaterialEditorQML::onLLMGenerationStopped()
+{
+    m_llmGenerationProgress = 0.0f;
+    emit llmGenerationProgressChanged();
+    emit aiGenerationError("Generation stopped");
+}

Add the matching declaration in the header, or emit a dedicated stopped signal if the QML side already expects one.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/MaterialEditorQML.cpp` around lines 2563 - 2566, stopAIGeneration
currently only calls LLMManager::instance()->stopGeneration() but never handles
the completion event, so subscribe to LLMManager::generationStopped and
update/clear the progress state when it fires: add a slot or handler in
MaterialEditorQML (declare it in the header, e.g. onGenerationStopped or
handleGenerationStopped) and connect LLMManager::generationStopped to that slot
(or emit a dedicated QML-visible stopped signal from the slot) so QML receives
the terminal event and the internal progress state is cleared/reset after
stopAIGeneration() is called.
src/LLMManager.cpp (1)

307-310: ⚠️ Potential issue | 🔴 Critical

Avoid capturing this in queued worker lambdas.

These functors run later on m_workerThread. If LLMManager is destroyed before delivery, the captured this becomes dangling and the lambda dereferences freed state (m_settings, getOgre3DSystemPrompt(), m_worker).

🛠️ Safer pattern
+#include <QPointer>
...
-    QMetaObject::invokeMethod(m_worker, [this, modelPath]() {
-        m_worker->setSettings(m_settings);
-        m_worker->loadModel(modelPath);
+    QPointer<LLMWorker> worker(m_worker);
+    const LLMSettings settings = m_settings;
+    QMetaObject::invokeMethod(m_worker, [worker, settings, modelPath]() {
+        if (!worker) {
+            return;
+        }
+        worker->setSettings(settings);
+        worker->loadModel(modelPath);
     }, Qt::QueuedConnection);
...
-    QMetaObject::invokeMethod(m_worker, [this, userPrompt]() {
-        m_worker->generate(getOgre3DSystemPrompt(), userPrompt);
+    QPointer<LLMWorker> worker(m_worker);
+    const QString systemPrompt = getOgre3DSystemPrompt();
+    QMetaObject::invokeMethod(m_worker, [worker, systemPrompt, userPrompt]() {
+        if (!worker) {
+            return;
+        }
+        worker->generate(systemPrompt, userPrompt);
     }, Qt::QueuedConnection);

Also applies to: 430-432

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/LLMManager.cpp` around lines 307 - 310, The queued lambda currently
captures this (and accesses m_settings, getOgre3DSystemPrompt(), and m_worker)
which can dangle if LLMManager is destroyed; instead make local copies of all
needed data (e.g., copy m_settings, modelPath, and the result of
getOgre3DSystemPrompt()) and use a QPointer or raw pointer check to the target
worker (e.g., QPointer<LLMWorker> wp = m_worker) captured by value in the
lambda, then inside the lambda verify wp is not null before calling
wp->setSettings(...) and wp->loadModel(...); apply the same pattern to the other
queued lambda(s) that reference this (lines around the second instance invoking
m_worker).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@src/LLMManager.cpp`:
- Around line 307-310: The queued lambda currently captures this (and accesses
m_settings, getOgre3DSystemPrompt(), and m_worker) which can dangle if
LLMManager is destroyed; instead make local copies of all needed data (e.g.,
copy m_settings, modelPath, and the result of getOgre3DSystemPrompt()) and use a
QPointer or raw pointer check to the target worker (e.g., QPointer<LLMWorker> wp
= m_worker) captured by value in the lambda, then inside the lambda verify wp is
not null before calling wp->setSettings(...) and wp->loadModel(...); apply the
same pattern to the other queued lambda(s) that reference this (lines around the
second instance invoking m_worker).

In `@src/LLMWorker.cpp`:
- Around line 365-378: The token-decode failure path currently emits
generationError but then continues to the normal completion path, causing both
generationError and generationCompleted to be emitted; update the loop that
calls llama_decode so that on a non-zero return you clean up (free the sampler
via llama_sampler_free and set m_isGenerating.store(false)) and then return/exit
immediately instead of falling through; ensure generationCompleted is only
emitted on successful completion and keep references to llama_decode,
generationError, generationCompleted, llama_sampler_free, m_isGenerating,
generatedText and generatedTokens to locate and adjust the logic.

In `@src/MaterialEditorQML.cpp`:
- Around line 2563-2566: stopAIGeneration currently only calls
LLMManager::instance()->stopGeneration() but never handles the completion event,
so subscribe to LLMManager::generationStopped and update/clear the progress
state when it fires: add a slot or handler in MaterialEditorQML (declare it in
the header, e.g. onGenerationStopped or handleGenerationStopped) and connect
LLMManager::generationStopped to that slot (or emit a dedicated QML-visible
stopped signal from the slot) so QML receives the terminal event and the
internal progress state is cleared/reset after stopAIGeneration() is called.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 03697d6a-f4cd-4d80-ad6e-88c5109c9917

📥 Commits

Reviewing files that changed from the base of the PR and between e73facb and 68246f2.

📒 Files selected for processing (3)
  • src/LLMManager.cpp
  • src/LLMWorker.cpp
  • src/MaterialEditorQML.cpp

@sonarqubecloud

Copy link
Copy Markdown

@fernandotonon
fernandotonon merged commit 6c4ca69 into master Mar 10, 2026
16 checks passed
@fernandotonon
fernandotonon deleted the test/mesh-importer-exporter-coverage branch March 10, 2026 20:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant