Jelajahi Sumber

#2562 Add parameterized tests for SceneGraphThreadWarden

Introduce tests to verify thread safety of scene graph mutations under various conditions. Includes checks for both main-thread and non-main-thread scenarios with attached and detached objects. Ensures compliance with scene graph thread safety guarantees.
Richard Tingle 3 bulan lalu
induk
melakukan
82466eda58

+ 203 - 0
jme3-core/src/test/java/com/jme3/scene/threadwarden/SceneGraphThreadWardenExtendedTest.java

@@ -0,0 +1,203 @@
+package com.jme3.scene.threadwarden;
+
+import com.jme3.material.Material;
+import com.jme3.material.MatParamOverride;
+import com.jme3.scene.Node;
+import com.jme3.scene.Spatial;
+import com.jme3.shader.VarType;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.mockito.Mockito;
+
+import java.util.Arrays;
+import java.util.Collection;
+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 java.util.function.Consumer;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+/**
+ * Parameterized tests for SceneGraphThreadWarden class.
+ * These tests verify that various scene graph mutations are properly checked for thread safety.
+ */
+@RunWith(Parameterized.class)
+public class SceneGraphThreadWardenExtendedTest {
+
+    private static ExecutorService executorService;
+
+    private final String testName;
+    private final Consumer<Spatial> action;
+
+    @SuppressWarnings({"ReassignedVariable", "AssertWithSideEffects"})
+    @BeforeClass
+    public static void setupClass() {
+        // Make sure assertions are enabled
+        boolean assertsEnabled = false;
+        assert assertsEnabled = true;
+        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();
+    }
+
+    /**
+     * Constructor for the parameterized test.
+     * 
+     * @param testName A descriptive name for the test
+     * @param action The action to perform on the spatial
+     */
+    public SceneGraphThreadWardenExtendedTest(String testName, Consumer<Spatial> action) {
+        this.testName = testName;
+        this.action = action;
+    }
+
+    /**
+     * Define the parameters for the test.
+     * Each parameter is a pair of (test name, action to perform on spatial).
+     */
+    @Parameterized.Parameters(name = "{0}")
+    public static Collection<Object[]> data() {
+        Material mockMaterial = Mockito.mock(Material.class);
+        MatParamOverride override = new MatParamOverride(VarType.Float, "TestParam", 1.0f);
+
+        return Arrays.asList(new Object[][] {
+            { 
+                "setMaterial", 
+                (Consumer<Spatial>) spatial -> spatial.setMaterial(mockMaterial) 
+            },
+            { 
+                "setLodLevel", 
+                (Consumer<Spatial>) spatial -> spatial.setLodLevel(1) 
+            },
+            { 
+                "addMatParamOverride", 
+                (Consumer<Spatial>) spatial -> spatial.addMatParamOverride(override) 
+            },
+            { 
+                "removeMatParamOverride", 
+                (Consumer<Spatial>) spatial -> spatial.removeMatParamOverride(override) 
+            },
+            { 
+                "clearMatParamOverrides", 
+                (Consumer<Spatial>) Spatial::clearMatParamOverrides
+            }
+        });
+    }
+
+    /**
+     * Test that scene graph mutation is fine on the main thread when the object is attached to the root.
+     */
+    @Test
+    public void testMutationOnMainThreadOnAttachedObject() {
+        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);
+
+        // This should work fine since we're on the main thread
+        action.accept(child);
+    }
+
+    /**
+     * Test that scene graph mutation is fine on the main thread when the object is not attached to the root.
+     */
+    @Test
+    public void testMutationOnMainThreadOnDetachedObject() {
+        Node rootNode = new Node("root");
+        SceneGraphThreadWarden.setup(rootNode);
+
+        // Create a child node but don't attach it to the root node
+        Node child = new Node("child");
+
+        // This should work fine since we're on the main thread
+        action.accept(child);
+    }
+
+    /**
+     * Test that scene graph mutation is fine on a non-main thread when the object is not attached to the root.
+     */
+    @Test
+    public void testMutationOnNonMainThreadOnDetachedObject() throws ExecutionException, InterruptedException {
+        Node rootNode = new Node("root");
+        SceneGraphThreadWarden.setup(rootNode);
+
+        // Create a child node but don't attach it to the root node
+        Node child = new Node("child");
+
+        Future<Void> future = executorService.submit(() -> {
+            // This should work fine since the node is not connected to the root node
+            action.accept(child);
+            return null;
+        });
+
+        // This should complete without exceptions
+        future.get();
+    }
+
+    /**
+     * Test that scene graph mutation is not allowed on a non-main thread when the object is attached to the root.
+     */
+    @Test
+    public void testMutationOnNonMainThreadOnAttachedObject() 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> future = executorService.submit(() -> {
+            // This should fail because we're trying to modify a node that's connected to the scene graph
+            action.accept(child);
+            return null;
+        });
+
+        try {
+            future.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);
+        }
+    }
+
+    /**
+     * 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;
+        };
+    }
+}