Skip to content

test(paint): expand TexturePaintController coverage 3 → 65 tests#542

Merged
fernandotonon merged 1 commit into
fix/inspector-thumbnail-blinkfrom
test/texture-paint-controller-coverage
May 17, 2026
Merged

test(paint): expand TexturePaintController coverage 3 → 65 tests#542
fernandotonon merged 1 commit into
fix/inspector-thumbnail-blinkfrom
test/texture-paint-controller-coverage

Conversation

@fernandotonon
Copy link
Copy Markdown
Owner

Stacked on #541 (fix/inspector-thumbnail-blink). Rebase/merge order: #541 first, then this.

Summary

The controller had three thin tests against a 2796-line implementation. This brings broad coverage of the public API and the QML-facing properties — the surfaces every Inspector / Material-Mode interaction hits.

Two suites:

TexturePaintControllerStandalone (no scene needed)

  • Default state on instance(), brush-tool / paint-target / uv-overlay setters (sticky, no-emit on same value, emit on transition).
  • setSmartSelectTolerance clamps to [0..1].
  • Brush param mirror: setBrushRadius/Strength/Falloff/Color write through to EditModeController; reads (texturePaintRadius etc.) flow back.
  • No-session edge cases: fullResPreviewUrl empty, snapshotBufferImage null, mask actions / smart-select / bake / load all no-op cleanly, ensurePaintableTexture without selection returns false, bakeToOriginalFile empty, beginStrokeUV refused.
  • setPaintTarget side-effects: also fires sessionChanged + smartSelectChanged, not only paintTargetChanged.
  • Enable-without-selection is harmless (no crash, no session).

TexturePaintControllerSceneTest (fixture: 3-vertex UV triangle + material with a diffuse_map TUS)

  • enable → session created → disable tears it down.
  • ensurePaintableTexture builds buffer at requested resolution and reuses on second call.
  • closeSession resets every observable.
  • refreshSlots exposes slot model with expected keys.
  • fullResPreviewUrl shape (image://paintbuffer/...?v=N); version bumps after a buffer-mutating op.
  • snapshotBufferImage matches buffer dimensions.
  • Save / load PNG round-trip; loadPaintBuffer writes seeded pixel into buffer.
  • findMeshPointForUV hits each triangle corner, misses outside.
  • texturePaintRadiusUV maps through mesh half-extent.
  • Smart-select adds pixels; selectAllMask covers W×H; invertSelectionMask flips pixel count; clearSelectionMask emits smartSelectChanged.
  • fillMaskWithFG / fillMaskWithBG / deleteMaskPixels write every selected pixel; delete zeroes the alpha too.
  • applyPixelSnapshot round-trips (undo path); mismatched size rejected without crash.
  • Paint target tex → vertex tears down the session (the "switch crashes app" regression guard).
  • setHoveredUV / clearHoveredUV emit hoveredUVChanged with the expected payload.
  • beginStrokeUV / updateStrokeUV / endStrokeUV completes on a session.
  • smartSelectAtUV handles unrecognised combine mode (falls to replace); exercises add (mode=1) and subtract (mode=2).
  • bakeVertexColorsToTexture is callable on an entity without crashing.
  • Opening + closing the editor window flips the flag.

Test plan

  • CI (Linux + Xvfb): UnitTests --gtest_filter="TexturePaintController*" passes (3 → 65 tests).
  • Coverage report shows TexturePaintController significantly closer to 100% on public methods.

Tests will skip cleanly on macOS where test_main FATALs on tryInitOgre (this behavior is preexisting, unchanged by this PR).

🤖 Generated with Claude Code

The controller had three thin tests (singleton brush-tool defaults +
one in-memory UV hit) against a 2796-line implementation. This brings
broad coverage of the public API and the QML-facing properties — the
parts that get touched by every Inspector / Material-Mode interaction.

Two suites:

* `TexturePaintControllerStandalone` (no scene needed)
  - default state on instance(), brush-tool/target/uv-overlay setters
    (sticky, no-emit on same value, signal emit on transition)
  - smart-select tolerance clamping to [0..1]
  - brush param mirror: setBrushRadius/Strength/Falloff/Color update
    EditModeController; the read-side mirrors (texturePaintRadius
    etc.) flow back through
  - no-session edge cases — fullResPreviewUrl empty, snapshotBufferImage
    null, mask actions / smartSelect / bake / loadPaintBuffer all
    no-op cleanly, ensurePaintableTexture without selection returns
    false, bakeToOriginalFile empty, beginStrokeUV refused
  - paint-target change side-effects (sessionChanged + smartSelectChanged
    also emitted, not only paintTargetChanged)
  - enable-without-selection is harmless (no crash, no session)

* `TexturePaintControllerSceneTest` (fixture: scene + 3-vertex UV
  triangle + material with a `diffuse_map` TUS)
  - enable → session created → disable tears it down
  - ensurePaintableTexture builds buffer at requested resolution and
    reuses on second call
  - closeSession resets every observable
  - refreshSlots exposes slot model with the expected keys
  - fullResPreviewUrl shape (`image://paintbuffer/...?v=N`) when
    session is active; URL version bumps after a buffer-mutating op
  - snapshotBufferImage matches buffer dimensions
  - save / load PNG roundtrip; loadPaintBuffer accepts a known PNG
    and writes the seeded pixel into the buffer
  - findMeshPointForUV hits each triangle corner, misses outside
  - texturePaintRadiusUV maps through mesh half-extent
  - smart-select adds pixels; selectAll covers W×H; invert flips
    pixel count; clearSelectionMask emits smartSelectChanged
  - fillMaskWithFG / fillMaskWithBG / deleteMaskPixels all write
    every selected pixel and zero the buffer respectively
  - applyPixelSnapshot round-trips (undo path); mismatched size
    is rejected without crash
  - paint target tex→vertex tears down the session (the "switch
    crashes app" regression guard)
  - setHoveredUV / clearHoveredUV emit hoveredUVChanged with the
    expected payload
  - beginStrokeUV/updateStrokeUV/endStrokeUV completes on a session
  - smartSelect handles unrecognised combine mode (falls to replace),
    exercises add (mode=1) and subtract (mode=2)
  - bakeVertexColorsToTexture is callable on an entity without
    crashing (return value depends on whether mesh has vertex
    colors — we just assert it's ≥ -1)
  - opening + closing the editor window flips the flag

Tests will skip cleanly on macOS (test_main FATAL on tryInitOgre is
unchanged); CI runs on Linux under Xvfb where the scene fixture works.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 17, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1c77cebb-f2dd-4031-981a-6edf932f7076

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch test/texture-paint-controller-coverage

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@fernandotonon fernandotonon merged commit 37c5b0e into fix/inspector-thumbnail-blink May 17, 2026
1 check passed
@fernandotonon fernandotonon deleted the test/texture-paint-controller-coverage branch May 17, 2026 03:27
Copy link
Copy Markdown

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

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: 34482ee141

ℹ️ 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 on lines +813 to +814
const QString url2 = ctrl->fullResPreviewUrl();
EXPECT_NE(url1, url2) << "refresh must bump the version counter";
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 Wait for async flush before asserting preview URL changes

fillMaskWithFG() does not refresh the preview synchronously: it calls flushDirtyToOgre(), which schedules work on timers (QTimer::singleShot(16, ...) and then singleShot(60, ...) before refreshPreviewUri() in src/TexturePaintController.cpp). This assertion runs immediately after fillMaskWithFG(), so url2 can still equal url1, making the test fail/flap depending on event-loop timing rather than functional behavior.

Useful? React with 👍 / 👎.

Comment on lines +836 to +837
ctrl->fillMaskWithFG();
EXPECT_GE(spy.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.

P1 Badge Pump event loop before expecting fullResPreviewChanged

This expectation assumes the signal is emitted during fillMaskWithFG(), but the production path emits fullResPreviewChanged only after deferred timer callbacks in flushDirtyToOgre()/refreshPreviewUri() (src/TexturePaintController.cpp). Without waiting for those timers or processing events, spy.count() can remain 0, causing nondeterministic failures unrelated to the behavior under test.

Useful? React with 👍 / 👎.

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