test(paint): expand TexturePaintController coverage 3 → 65 tests#542
Conversation
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>
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
💡 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".
| const QString url2 = ctrl->fullResPreviewUrl(); | ||
| EXPECT_NE(url1, url2) << "refresh must bump the version counter"; |
There was a problem hiding this comment.
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 👍 / 👎.
| ctrl->fillMaskWithFG(); | ||
| EXPECT_GE(spy.count(), 1); |
There was a problem hiding this comment.
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 👍 / 👎.
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)instance(), brush-tool / paint-target / uv-overlay setters (sticky, no-emit on same value, emit on transition).setSmartSelectToleranceclamps to[0..1].setBrushRadius/Strength/Falloff/Colorwrite through toEditModeController; reads (texturePaintRadiusetc.) flow back.fullResPreviewUrlempty,snapshotBufferImagenull, mask actions / smart-select / bake / load all no-op cleanly,ensurePaintableTexturewithout selection returns false,bakeToOriginalFileempty,beginStrokeUVrefused.setPaintTargetside-effects: also firessessionChanged+smartSelectChanged, not onlypaintTargetChanged.TexturePaintControllerSceneTest(fixture: 3-vertex UV triangle + material with adiffuse_mapTUS)ensurePaintableTexturebuilds buffer at requested resolution and reuses on second call.closeSessionresets every observable.refreshSlotsexposes slot model with expected keys.fullResPreviewUrlshape (image://paintbuffer/...?v=N); version bumps after a buffer-mutating op.snapshotBufferImagematches buffer dimensions.loadPaintBufferwrites seeded pixel into buffer.findMeshPointForUVhits each triangle corner, misses outside.texturePaintRadiusUVmaps through mesh half-extent.selectAllMaskcovers W×H;invertSelectionMaskflips pixel count;clearSelectionMaskemitssmartSelectChanged.fillMaskWithFG / fillMaskWithBG / deleteMaskPixelswrite every selected pixel; delete zeroes the alpha too.applyPixelSnapshotround-trips (undo path); mismatched size rejected without crash.setHoveredUV / clearHoveredUVemithoveredUVChangedwith the expected payload.beginStrokeUV / updateStrokeUV / endStrokeUVcompletes on a session.smartSelectAtUVhandles unrecognised combine mode (falls to replace); exercises add (mode=1) and subtract (mode=2).bakeVertexColorsToTextureis callable on an entity without crashing.Test plan
UnitTests --gtest_filter="TexturePaintController*"passes (3 → 65 tests).🤖 Generated with Claude Code