@@ -7,6 +7,7 @@ import app.rive.runtime.kotlin.SharedSurface
77import app.rive.runtime.kotlin.controllers.RiveFileController
88import app.rive.runtime.kotlin.renderers.RiveArtboardRenderer
99import org.junit.Assert.assertFalse
10+ import org.junit.Assert.assertTrue
1011import org.junit.Test
1112import org.junit.runner.RunWith
1213import java.util.concurrent.CountDownLatch
@@ -60,6 +61,9 @@ class RiveArtboardRendererTest {
6061 */
6162 @Test
6263 fun deleteRendererDuringFrame () {
64+ // Ensure native libraries are loaded for JNI calls.
65+ testUtils.context
66+
6367 // Keep this low, as the test times out during non-critical activeArtboard access.
6468 val timeout = 100L
6569 // Latch to block signal we are passed the `hasCppObject` check in the draw() method
@@ -124,6 +128,9 @@ class RiveArtboardRendererTest {
124128 */
125129 @Test
126130 fun disposeArtboardDuringFrameAfterEnteringSyncBlock () {
131+ // Ensure native libraries are loaded for JNI calls.
132+ testUtils.context
133+
127134 // Keep this low, as we expect it to timeout.
128135 val timeout = 1000L
129136 // Signals that the artboard has entered draw() and is about to dereference the cppPointer.
@@ -193,6 +200,9 @@ class RiveArtboardRendererTest {
193200 */
194201 @Test
195202 fun disposeArtboardDuringFrameBeforeEnteringSyncBlock () {
203+ // Ensure native libraries are loaded for JNI calls.
204+ testUtils.context
205+
196206 val timeout = 1000L
197207 // Signals that the artboard has entered draw() and is about to dereference the cppPointer.
198208 val readyForRelease = CountDownLatch (1 )
@@ -258,12 +268,16 @@ class RiveArtboardRendererTest {
258268 }
259269
260270 /* *
261- * Tests that the renderer can be safely deleted while resizeArtboard() is executing.
262- * The fix adds a hasCppObject check at the start of resizeArtboard() to prevent
263- * accessing disposed C++ objects when accessing width/height properties.
271+ * Tests that the renderer can be safely deleted while resizeArtboard() is executing. The fix
272+ * reads renderer liveness and surface dimensions together under frameLock (in the Fit.LAYOUT
273+ * case where this is required), then applies artboard mutations under the file lock to avoid
274+ * disposal races.
264275 */
265276 @Test
266277 fun deleteRendererDuringResizeArtboard () {
278+ // Ensure native libraries are loaded for JNI calls.
279+ testUtils.context
280+
267281 val timeout = 1000L
268282 // Latch to signal we've entered resizeArtboard()
269283 val duringResizeLatch = CountDownLatch (1 )
@@ -324,4 +338,63 @@ class RiveArtboardRendererTest {
324338 " Got: ${exception?.javaClass?.simpleName} : ${exception?.message} "
325339 }
326340 }
341+
342+ /* *
343+ * Regression test for a race where resizeArtboard() reads hasCppObject, then the renderer is
344+ * deleted before width/height are read.
345+ *
346+ * The fit getter is used as a deterministic pause point between those operations.
347+ */
348+ @Test
349+ fun deleteRendererBetweenCppObjectCheckAndDimensionRead_doesNotCrash () {
350+ // Ensure native libraries are loaded for JNI calls.
351+ testUtils.context
352+
353+ val timeoutMs = 1000L
354+ val readyForDelete = CountDownLatch (1 )
355+ val resumeAfterDelete = CountDownLatch (1 )
356+ val exceptionRef = AtomicReference <Throwable ?>(null )
357+
358+ val controller = object : RiveFileController () {
359+ override var fit: Fit
360+ get() {
361+ readyForDelete.countDown()
362+ resumeAfterDelete.await(timeoutMs, TimeUnit .MILLISECONDS )
363+ return super .fit
364+ }
365+ set(value) {
366+ super .fit = value
367+ }
368+ }
369+ controller.fit = Fit .LAYOUT // Sets requireArtboardResize = true
370+
371+ val renderer = RiveArtboardRenderer (controller = controller)
372+ renderer.make()
373+
374+ val drawThread = Thread {
375+ try {
376+ renderer.draw()
377+ } catch (e: Throwable ) {
378+ exceptionRef.set(e)
379+ }
380+ }
381+ drawThread.start()
382+
383+ assertTrue(
384+ " draw() did not reach the fit getter in time; race was not induced." ,
385+ readyForDelete.await(timeoutMs, TimeUnit .MILLISECONDS )
386+ )
387+
388+ renderer.delete()
389+ resumeAfterDelete.countDown()
390+
391+ drawThread.join(timeoutMs)
392+
393+ val exception = exceptionRef.get()
394+ assertTrue(
395+ " Expected no exception when deleting renderer during resize race, " +
396+ " but got: ${exception?.javaClass?.simpleName} : ${exception?.message} " ,
397+ exception == null
398+ )
399+ }
327400}
0 commit comments