2626public class SceneGraphThreadWardenTest {
2727
2828 private static ExecutorService executorService ;
29+ private static ExecutorService executorService2 ;
2930
3031 @ SuppressWarnings ({"ReassignedVariable" , "AssertWithSideEffects" })
3132 @ BeforeClass
@@ -42,11 +43,13 @@ public static void setupClass() {
4243 @ Before
4344 public void setup () {
4445 executorService = newSingleThreadDaemonExecutor ();
46+ executorService2 = newSingleThreadDaemonExecutor ();
4547 }
4648
4749 @ After
4850 public void tearDown () {
4951 executorService .shutdown ();
52+ executorService2 .shutdown ();
5053 SceneGraphThreadWarden .reset ();
5154 }
5255
@@ -71,6 +74,31 @@ public void testNormalNodeMutationOnMainThread() {
7174 rootNode .detachChild (child );
7275 }
7376
77+ @ Test
78+ public void restrictingTheSameRootNodeToTwoThreadsIsIllegal () throws ExecutionException , InterruptedException {
79+ Node rootNode = new Node ("root" );
80+
81+ executorService .submit (() -> SceneGraphThreadWarden .setup (rootNode )).get ();
82+
83+ try {
84+ executorService2 .submit (() -> SceneGraphThreadWarden .setup (rootNode )).get ();
85+ fail ("Expected an IllegalThreadSceneGraphMutation exception" );
86+ } catch (ExecutionException e ) {
87+ // This is expected - verify it's the right exception type
88+ assertTrue ("Expected IllegalStateException, got: " + e .getCause ().getClass ().getName (),
89+ e .getCause () instanceof IllegalStateException );
90+ }
91+ }
92+
93+ @ Test
94+ public void restrictingTwoRootNodesIsFine (){
95+ Node rootNode = new Node ("root" );
96+ Node rootNode2 = new Node ("root2" );
97+
98+ SceneGraphThreadWarden .setup (rootNode );
99+ SceneGraphThreadWarden .setup (rootNode2 );
100+ }
101+
74102 /**
75103 * Test that node mutation on nodes not connected to the root node is fine even on a non main thread.
76104 * <p>
@@ -107,7 +135,7 @@ public void testNodeMutationOnNonConnectedNodesOnNonMainThread() throws Executio
107135 * exception.
108136 */
109137 @ Test
110- public void testAddingNodeToSceneGraphOnNonMainThread () throws InterruptedException {
138+ public void testAddingNodeToSceneGraphOnNonMainThread () {
111139 Node rootNode = new Node ("root" );
112140 SceneGraphThreadWarden .setup (rootNode );
113141
@@ -122,14 +150,7 @@ public void testAddingNodeToSceneGraphOnNonMainThread() throws InterruptedExcept
122150 return null ;
123151 });
124152
125- try {
126- illegalMutationFuture .get ();
127- fail ("Expected an IllegalThreadSceneGraphMutation exception" );
128- } catch (ExecutionException e ) {
129- // This is expected - verify it's the right exception type
130- assertTrue ("Expected IllegalThreadSceneGraphMutation, got: " + e .getCause ().getClass ().getName (),
131- e .getCause () instanceof IllegalThreadSceneGraphMutation );
132- }
153+ expectSceneGraphException (illegalMutationFuture );
133154 }
134155
135156 /**
@@ -141,7 +162,7 @@ public void testAddingNodeToSceneGraphOnNonMainThread() throws InterruptedExcept
141162 * </p>
142163 */
143164 @ Test
144- public void testMovingNodeAttachedToRootOnNonMainThread () throws InterruptedException {
165+ public void testMovingNodeAttachedToRootOnNonMainThread () {
145166 Node rootNode = new Node ("root" );
146167 SceneGraphThreadWarden .setup (rootNode );
147168
@@ -157,14 +178,7 @@ public void testMovingNodeAttachedToRootOnNonMainThread() throws InterruptedExce
157178 return null ;
158179 });
159180
160- try {
161- illegalMutationFuture .get ();
162- fail ("Expected an IllegalThreadSceneGraphMutation exception" );
163- } catch (ExecutionException e ) {
164- // This is expected - verify it's the right exception type
165- assertTrue ("Expected IllegalThreadSceneGraphMutation, got: " + e .getCause ().getClass ().getName (),
166- e .getCause () instanceof IllegalThreadSceneGraphMutation );
167- }
181+ expectSceneGraphException (illegalMutationFuture );
168182 }
169183
170184 /**
@@ -199,7 +213,7 @@ public void testDetachmentReleasesProtection() throws ExecutionException, Interr
199213 * then try (and fail) to make an illegal on-thread change to the grandchild.
200214 */
201215 @ Test
202- public void testAddingAChildToTheRootNodeAlsoRestrictsTheGrandChild () throws InterruptedException {
216+ public void testAddingAChildToTheRootNodeAlsoRestrictsTheGrandChild () {
203217 Node rootNode = new Node ("root" );
204218 SceneGraphThreadWarden .setup (rootNode );
205219
@@ -221,14 +235,7 @@ public void testAddingAChildToTheRootNodeAlsoRestrictsTheGrandChild() throws Int
221235 return null ;
222236 });
223237
224- try {
225- illegalMutationFuture .get ();
226- fail ("Expected an IllegalThreadSceneGraphMutation exception" );
227- } catch (ExecutionException e ) {
228- // This is expected - verify it's the right exception type
229- assertTrue ("Expected IllegalThreadSceneGraphMutation, got: " + e .getCause ().getClass ().getName (),
230- e .getCause () instanceof IllegalThreadSceneGraphMutation );
231- }
238+ expectSceneGraphException (illegalMutationFuture );
232239 }
233240
234241 /**
@@ -294,7 +301,96 @@ public void testDisableChecksAllowsIllegalMutation() throws ExecutionException,
294301 mutationFuture .get ();
295302 }
296303
304+ /**
305+ * This tests that scenarios where multiple JME applications are running simultaneously within the same
306+ * JMV. This is a weird case but does happen sometimes (e.g. in TestChooser).
307+ * <p>
308+ * This test tests that using two seperate executor services with their own seperate threads
309+ * </p>
310+ */
311+ @ Test
312+ public void testCanCopeWithMultipleSimultaneousRootThreads () throws ExecutionException , InterruptedException {
313+ Node rootNodeThread1 = new Node ("root1" );
314+ Node rootNodeThread2 = new Node ("root2" );
297315
316+ // Create a child node and attach it to the root node
317+ Node childThread1 = new Node ("child1" );
318+ Node childThread2 = new Node ("child2" );
319+
320+ // on two threads set up two root nodes (for two JME applications on the same VM)
321+ Future <Void > mutationFuture1 = executorService .submit (() -> {
322+ SceneGraphThreadWarden .setup (rootNodeThread1 );
323+ rootNodeThread1 .attachChild (childThread1 );
324+ return null ;
325+ });
326+
327+ Future <Void > mutationFuture2 = executorService2 .submit (() -> {
328+ SceneGraphThreadWarden .setup (rootNodeThread2 );
329+ rootNodeThread2 .attachChild (childThread2 );
330+ return null ;
331+ });
332+
333+ // These should complete without exceptions, these are independent scene graphs
334+ mutationFuture1 .get ();
335+ mutationFuture2 .get ();
336+
337+ // try to use a child from one thread on the other, this should exception as the child is reserved
338+ expectSceneGraphException (executorService .submit (() -> {
339+ SceneGraphThreadWarden .setup (rootNodeThread1 );
340+ rootNodeThread1 .attachChild (childThread2 );
341+ return null ;
342+ }));
343+ }
344+
345+ /**
346+ * Where there are multiple JME roots this tests that is *is* allowed to remove a child from one root and attach it
347+ * to another as long as the detachments and reattachments are done in the right threads.
348+ *
349+ * <p>
350+ * This is a weird thing to do and unlikely to come up in practice but is allowed
351+ * </p>
352+ */
353+ @ Test
354+ public void testCanTransferNodeBetweenMultipleSimultaneousRootThreads () throws ExecutionException , InterruptedException {
355+ Node rootNodeThread1 = new Node ("root1" );
356+ Node rootNodeThread2 = new Node ("root2" );
357+
358+ // Create a child node and attach it to the root node
359+ Node childThread1 = new Node ("child1" );
360+ Node childThread2 = new Node ("child2" );
361+ Node childTransferred = new Node ("childTransferred" );
362+
363+ // on two threads set up two root nodes (for two JME applications on the same VM)
364+ Future <Void > mutationFuture1 = executorService .submit (() -> {
365+ SceneGraphThreadWarden .setup (rootNodeThread1 );
366+ rootNodeThread1 .attachChild (childThread1 );
367+ rootNodeThread1 .attachChild (childTransferred );
368+ return null ;
369+ });
370+
371+ Future <Void > mutationFuture2 = executorService2 .submit (() -> {
372+ SceneGraphThreadWarden .setup (rootNodeThread2 );
373+ rootNodeThread2 .attachChild (childThread2 );
374+ return null ;
375+ });
376+
377+ // These should complete without exceptions, these are independent scene graphs
378+ mutationFuture1 .get ();
379+ mutationFuture2 .get ();
380+
381+ Future <Void > legalRemovalFuture = executorService .submit (() -> {
382+ childTransferred .removeFromParent ();
383+ return null ;
384+ });
385+
386+ legalRemovalFuture .get ();
387+
388+ Future <Void > legalAttachmentFuture = executorService2 .submit (() -> {
389+ rootNodeThread2 .attachChild (childTransferred );
390+ return null ;
391+ });
392+ legalAttachmentFuture .get ();
393+ }
298394
299395 /**
300396 * Creates a single-threaded executor service with daemon threads.
@@ -313,4 +409,17 @@ private static ThreadFactory daemonThreadFactory() {
313409 return t ;
314410 };
315411 }
412+
413+ private void expectSceneGraphException (Future <Void > future ){
414+ try {
415+ future .get ();
416+ fail ("Expected an IllegalThreadSceneGraphMutation exception" );
417+ } catch (ExecutionException e ) {
418+ // This is expected - verify it's the right exception type
419+ assertTrue ("Expected IllegalThreadSceneGraphMutation, got: " + e .getCause ().getClass ().getName (),
420+ e .getCause () instanceof IllegalThreadSceneGraphMutation );
421+ } catch (InterruptedException e ){
422+ fail ("Unexpected InterruptedException" );
423+ }
424+ }
316425}
0 commit comments