|
|
@@ -0,0 +1,316 @@
|
|
|
+package com.jme3.scene.threadwarden;
|
|
|
+
|
|
|
+import com.jme3.scene.Node;
|
|
|
+import org.junit.After;
|
|
|
+import org.junit.Before;
|
|
|
+import org.junit.BeforeClass;
|
|
|
+import org.junit.Test;
|
|
|
+
|
|
|
+import java.util.concurrent.ExecutionException;
|
|
|
+import java.util.concurrent.ExecutorService;
|
|
|
+import java.util.concurrent.Executors;
|
|
|
+import java.util.concurrent.Future;
|
|
|
+import java.util.concurrent.ThreadFactory;
|
|
|
+
|
|
|
+import static org.junit.Assert.assertTrue;
|
|
|
+import static org.junit.Assert.fail;
|
|
|
+
|
|
|
+/**
|
|
|
+ * Tests for SceneGraphThreadWarden class.
|
|
|
+ * These tests verify that:
|
|
|
+ * - Normal node mutation is fine on the main thread
|
|
|
+ * - Node mutation on nodes not connected to the root node is fine even on a non main thread
|
|
|
+ * - Adding a node to the scene graph (indirectly) connected to the root node isn't fine on a non main thread
|
|
|
+ * - Adding a node currently attached to a root node to a different node isn't fine on a non main thread
|
|
|
+ */
|
|
|
+public class SceneGraphThreadWardenTest {
|
|
|
+
|
|
|
+ private static ExecutorService executorService;
|
|
|
+
|
|
|
+ @SuppressWarnings({"ReassignedVariable", "AssertWithSideEffects"})
|
|
|
+ @BeforeClass
|
|
|
+ public static void setupClass() {
|
|
|
+ // Make sure assertions are enabled
|
|
|
+ boolean assertsEnabled = false;
|
|
|
+ assert assertsEnabled = true;
|
|
|
+ //noinspection ConstantValue
|
|
|
+ if (!assertsEnabled) {
|
|
|
+ throw new RuntimeException("WARNING: Assertions are not enabled! Tests may not work correctly.");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @Before
|
|
|
+ public void setup() {
|
|
|
+ executorService = newSingleThreadDaemonExecutor();
|
|
|
+ }
|
|
|
+
|
|
|
+ @After
|
|
|
+ public void tearDown() {
|
|
|
+ executorService.shutdown();
|
|
|
+ SceneGraphThreadWarden.reset();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Test that normal node mutation is fine on the main thread.
|
|
|
+ */
|
|
|
+ @Test
|
|
|
+ public void testNormalNodeMutationOnMainThread() {
|
|
|
+ Node rootNode = new Node("root");
|
|
|
+ SceneGraphThreadWarden.setup(rootNode);
|
|
|
+
|
|
|
+ // This should work fine since we're on the main thread
|
|
|
+ Node child = new Node("child");
|
|
|
+ rootNode.attachChild(child);
|
|
|
+
|
|
|
+ // Add another level of children
|
|
|
+ Node grandchild = new Node("grandchild");
|
|
|
+ child.attachChild(grandchild);
|
|
|
+
|
|
|
+ // Detach should also work fine
|
|
|
+ child.detachChild(grandchild);
|
|
|
+ rootNode.detachChild(child);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Test that node mutation on nodes not connected to the root node is fine even on a non main thread.
|
|
|
+ * <p>
|
|
|
+ * This is a use case where a thread is preparing things for later attachment to the scene graph.
|
|
|
+ * </p>
|
|
|
+ */
|
|
|
+ @Test
|
|
|
+ public void testNodeMutationOnNonConnectedNodesOnNonMainThread() throws ExecutionException, InterruptedException {
|
|
|
+ Node rootNode = new Node("root");
|
|
|
+ SceneGraphThreadWarden.setup(rootNode);
|
|
|
+
|
|
|
+ Future<Node> nonConnectedNodeFuture = executorService.submit(() -> {
|
|
|
+ // This should work fine since these nodes are not connected to the root node
|
|
|
+ Node parent = new Node("parent");
|
|
|
+ Node child = new Node("child");
|
|
|
+ parent.attachChild(child);
|
|
|
+
|
|
|
+ // Add another level of children
|
|
|
+ Node grandchild = new Node("grandchild");
|
|
|
+ child.attachChild(grandchild);
|
|
|
+
|
|
|
+ return parent;
|
|
|
+ });
|
|
|
+
|
|
|
+ // Get the result to ensure the task completed without exceptions
|
|
|
+ Node nonConnectedNode = nonConnectedNodeFuture.get();
|
|
|
+
|
|
|
+ // Now we can attach it to the root node on the main thread
|
|
|
+ rootNode.attachChild(nonConnectedNode);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Test that adding a node to the scene graph connected to the root node in a non main thread leads to an
|
|
|
+ * exception.
|
|
|
+ */
|
|
|
+ @Test
|
|
|
+ public void testAddingNodeToSceneGraphOnNonMainThread() throws InterruptedException {
|
|
|
+ Node rootNode = new Node("root");
|
|
|
+ SceneGraphThreadWarden.setup(rootNode);
|
|
|
+
|
|
|
+ // Create a child node and attach it to the root node
|
|
|
+ Node child = new Node("child");
|
|
|
+ rootNode.attachChild(child);
|
|
|
+
|
|
|
+ Future<Void> illegalMutationFuture = executorService.submit(() -> {
|
|
|
+ // This should fail because we're trying to add a node to a node that's connected to the scene graph
|
|
|
+ Node grandchild = new Node("grandchild");
|
|
|
+ child.attachChild(grandchild);
|
|
|
+ return null;
|
|
|
+ });
|
|
|
+
|
|
|
+ try {
|
|
|
+ illegalMutationFuture.get();
|
|
|
+ fail("Expected an IllegalThreadSceneGraphMutation exception");
|
|
|
+ } catch (ExecutionException e) {
|
|
|
+ // This is expected - verify it's the right exception type
|
|
|
+ assertTrue("Expected IllegalThreadSceneGraphMutation, got: " + e.getCause().getClass().getName(),
|
|
|
+ e.getCause() instanceof IllegalThreadSceneGraphMutation);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Test that adding a node currently attached to a root node to a different node leads to an exception.
|
|
|
+ * <p>
|
|
|
+ * This is testing an edge case where you think you'd working with non-connected nodes, but in reality
|
|
|
+ * one of your nodes is already attached to the scene graph (and you're attaching it to a different node which will
|
|
|
+ * detach it from the scene graph).
|
|
|
+ * </p>
|
|
|
+ */
|
|
|
+ @Test
|
|
|
+ public void testMovingNodeAttachedToRootOnNonMainThread() throws InterruptedException {
|
|
|
+ Node rootNode = new Node("root");
|
|
|
+ SceneGraphThreadWarden.setup(rootNode);
|
|
|
+
|
|
|
+ // Create two child nodes and attach them to the root node
|
|
|
+ Node child1 = new Node("child1");
|
|
|
+ Node child2 = new Node("child2");
|
|
|
+
|
|
|
+ rootNode.attachChild(child2);
|
|
|
+
|
|
|
+ Future<Void> illegalMutationFuture = executorService.submit(() -> {
|
|
|
+ // This should fail because we're trying to move a node that's connected to the root node
|
|
|
+ child1.attachChild(child2); // This implicitly detaches child2 from rootNode
|
|
|
+ return null;
|
|
|
+ });
|
|
|
+
|
|
|
+ try {
|
|
|
+ illegalMutationFuture.get();
|
|
|
+ fail("Expected an IllegalThreadSceneGraphMutation exception");
|
|
|
+ } catch (ExecutionException e) {
|
|
|
+ // This is expected - verify it's the right exception type
|
|
|
+ assertTrue("Expected IllegalThreadSceneGraphMutation, got: " + e.getCause().getClass().getName(),
|
|
|
+ e.getCause() instanceof IllegalThreadSceneGraphMutation);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Test that detaching a node releases it from thread protection.
|
|
|
+ */
|
|
|
+ @Test
|
|
|
+ public void testDetachmentReleasesProtection() throws ExecutionException, InterruptedException {
|
|
|
+ Node rootNode = new Node("root");
|
|
|
+ SceneGraphThreadWarden.setup(rootNode);
|
|
|
+
|
|
|
+ // Create a child node and attach it to the root node
|
|
|
+ Node child = new Node("child");
|
|
|
+ rootNode.attachChild(child);
|
|
|
+
|
|
|
+ // Now detach it from the root node
|
|
|
+ child.removeFromParent();
|
|
|
+
|
|
|
+ // Now we should be able to modify it on another thread
|
|
|
+ Future<Void> legalMutationFuture = executorService.submit(() -> {
|
|
|
+ Node grandchild = new Node("grandchild");
|
|
|
+ child.attachChild(grandchild);
|
|
|
+ return null;
|
|
|
+ });
|
|
|
+
|
|
|
+ // This should complete without exceptions
|
|
|
+ legalMutationFuture.get();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Test that adding a child to the root node also restricts the grandchild.
|
|
|
+ * This test will add a grandchild to a child BEFORE adding the child to the root,
|
|
|
+ * then try (and fail) to make an illegal on-thread change to the grandchild.
|
|
|
+ */
|
|
|
+ @Test
|
|
|
+ public void testAddingAChildToTheRootNodeAlsoRestrictsTheGrandChild() throws InterruptedException {
|
|
|
+ Node rootNode = new Node("root");
|
|
|
+ SceneGraphThreadWarden.setup(rootNode);
|
|
|
+
|
|
|
+ // Create a child node and a grandchild node
|
|
|
+ Node child = new Node("child");
|
|
|
+ Node grandchild = new Node("grandchild");
|
|
|
+
|
|
|
+ // Attach the grandchild to the child BEFORE adding the child to the root
|
|
|
+ child.attachChild(grandchild);
|
|
|
+
|
|
|
+ // Now attach the child to the root node
|
|
|
+ rootNode.attachChild(child);
|
|
|
+
|
|
|
+ // Try to make an illegal on-thread change to the grandchild
|
|
|
+ Future<Void> illegalMutationFuture = executorService.submit(() -> {
|
|
|
+ // This should fail because the grandchild is now restricted
|
|
|
+ Node greatGrandchild = new Node("greatGrandchild");
|
|
|
+ grandchild.attachChild(greatGrandchild);
|
|
|
+ return null;
|
|
|
+ });
|
|
|
+
|
|
|
+ try {
|
|
|
+ illegalMutationFuture.get();
|
|
|
+ fail("Expected an IllegalThreadSceneGraphMutation exception");
|
|
|
+ } catch (ExecutionException e) {
|
|
|
+ // This is expected - verify it's the right exception type
|
|
|
+ assertTrue("Expected IllegalThreadSceneGraphMutation, got: " + e.getCause().getClass().getName(),
|
|
|
+ e.getCause() instanceof IllegalThreadSceneGraphMutation);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Test that removing a child from the root node also unrestricts the grandchild.
|
|
|
+ * This test will add a child with a grandchild to the root node, then remove the child
|
|
|
+ * and verify that the grandchild can be modified on a non-main thread.
|
|
|
+ */
|
|
|
+ @Test
|
|
|
+ public void testRemovingAChildFromTheRootNodeAlsoUnrestrictsTheGrandChild() throws ExecutionException, InterruptedException {
|
|
|
+ Node rootNode = new Node("root");
|
|
|
+ SceneGraphThreadWarden.setup(rootNode);
|
|
|
+
|
|
|
+ // Create a child node and a grandchild node
|
|
|
+ Node child = new Node("child");
|
|
|
+ Node grandchild = new Node("grandchild");
|
|
|
+
|
|
|
+ // Attach the grandchild to the child
|
|
|
+ child.attachChild(grandchild);
|
|
|
+
|
|
|
+ // Attach the child to the root node
|
|
|
+ rootNode.attachChild(child);
|
|
|
+
|
|
|
+ // Now remove the child from the root node
|
|
|
+ child.removeFromParent();
|
|
|
+
|
|
|
+ // Try to make a change to the grandchild on a non-main thread
|
|
|
+ Future<Void> legalMutationFuture = executorService.submit(() -> {
|
|
|
+ // This should succeed because the grandchild is no longer restricted
|
|
|
+ Node greatGrandchild = new Node("greatGrandchild");
|
|
|
+ grandchild.attachChild(greatGrandchild);
|
|
|
+ return null;
|
|
|
+ });
|
|
|
+
|
|
|
+ // This should complete without exceptions
|
|
|
+ legalMutationFuture.get();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Test that an otherwise illegal scene graph mutation won't throw an exception
|
|
|
+ * if the checks have been disabled by calling disableChecks().
|
|
|
+ */
|
|
|
+ @Test
|
|
|
+ public void testDisableChecksAllowsIllegalMutation() throws ExecutionException, InterruptedException {
|
|
|
+ Node rootNode = new Node("root");
|
|
|
+ SceneGraphThreadWarden.setup(rootNode);
|
|
|
+
|
|
|
+ // Create a child node and attach it to the root node
|
|
|
+ Node child = new Node("child");
|
|
|
+ rootNode.attachChild(child);
|
|
|
+
|
|
|
+ // Disable the thread warden checks
|
|
|
+ SceneGraphThreadWarden.disableChecks();
|
|
|
+
|
|
|
+ // Try to make a change to the child on a non-main thread
|
|
|
+ // This would normally be illegal, but should succeed because checks are disabled
|
|
|
+ Future<Void> mutationFuture = executorService.submit(() -> {
|
|
|
+ Node grandchild = new Node("grandchild");
|
|
|
+ child.attachChild(grandchild);
|
|
|
+ return null;
|
|
|
+ });
|
|
|
+
|
|
|
+ // This should complete without exceptions
|
|
|
+ mutationFuture.get();
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Creates a single-threaded executor service with daemon threads.
|
|
|
+ */
|
|
|
+ private static ExecutorService newSingleThreadDaemonExecutor() {
|
|
|
+ return Executors.newSingleThreadExecutor(daemonThreadFactory());
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Creates a thread factory that produces daemon threads.
|
|
|
+ */
|
|
|
+ private static ThreadFactory daemonThreadFactory() {
|
|
|
+ return r -> {
|
|
|
+ Thread t = Executors.defaultThreadFactory().newThread(r);
|
|
|
+ t.setDaemon(true);
|
|
|
+ return t;
|
|
|
+ };
|
|
|
+ }
|
|
|
+}
|