/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.harmony.tests.java.lang; import java.util.Vector; public class ThreadGroupTest extends junit.framework.TestCase { private TestThreadDefaultUncaughtExceptionHandler testThreadDefaultUncaughtExceptionHandler; private ThreadGroup rootThreadGroup; private ThreadGroup initialThreadGroup; private Thread.UncaughtExceptionHandler originalThreadDefaultUncaughtExceptionHandler; @Override protected void setUp() { initialThreadGroup = Thread.currentThread().getThreadGroup(); rootThreadGroup = initialThreadGroup; while (rootThreadGroup.getParent() != null) { rootThreadGroup = rootThreadGroup.getParent(); } // When running as a CTS test Android will by default treat an uncaught exception as a // fatal application error and kill the test. To avoid this the default // UncaughtExceptionHandler is replaced for the duration of the test (if one exists). It // also allows us to test that ultimately the default handler is called if a ThreadGroup's // UncaughtExceptionHandler doesn't handle an exception. originalThreadDefaultUncaughtExceptionHandler = Thread.getDefaultUncaughtExceptionHandler(); testThreadDefaultUncaughtExceptionHandler = new TestThreadDefaultUncaughtExceptionHandler(); Thread.setDefaultUncaughtExceptionHandler(testThreadDefaultUncaughtExceptionHandler); } @Override protected void tearDown() { // Reset the uncaughtExceptionHandler to what it was when the test began. Thread.setDefaultUncaughtExceptionHandler(originalThreadDefaultUncaughtExceptionHandler); } // Test for method java.lang.ThreadGroup(java.lang.String) public void test_ConstructorLjava_lang_String() { // Unfortunately we have to use other APIs as well as we test the constructor ThreadGroup initial = initialThreadGroup; final String name = "Test name"; ThreadGroup newGroup = new ThreadGroup(name); assertTrue( "Has to be possible to create a subgroup of current group using simple constructor", newGroup.getParent() == initial); assertTrue("Name has to be correct", newGroup.getName().equals(name)); // cleanup newGroup.destroy(); } // Test for method java.lang.ThreadGroup(java.lang.ThreadGroup, java.lang.String) public void test_ConstructorLjava_lang_ThreadGroupLjava_lang_String() { // Unfortunately we have to use other APIs as well as we test the constructor ThreadGroup newGroup = null; try { newGroup = new ThreadGroup(null, null); } catch (NullPointerException e) { } assertNull("Can't create a ThreadGroup with a null parent", newGroup); newGroup = new ThreadGroup(initialThreadGroup, null); assertTrue("Has to be possible to create a subgroup of current group", newGroup.getParent() == Thread.currentThread().getThreadGroup()); // Lets start all over newGroup.destroy(); newGroup = new ThreadGroup(rootThreadGroup, "a name here"); assertTrue("Has to be possible to create a subgroup of root group", newGroup.getParent() == rootThreadGroup); // Lets start all over newGroup.destroy(); try { newGroup = new ThreadGroup(newGroup, "a name here"); } catch (IllegalThreadStateException e) { newGroup = null; } assertNull("Can't create a subgroup of a destroyed group", newGroup); } // Test for method int java.lang.ThreadGroup.activeCount() public void test_activeCount() { ThreadGroup tg = new ThreadGroup("activeCount"); Thread t1 = new Thread(tg, new Runnable() { public void run() { try { Thread.sleep(5000); } catch (InterruptedException e) { } } }); int count = tg.activeCount(); assertTrue("wrong active count: " + count, count == 0); t1.start(); count = tg.activeCount(); assertTrue("wrong active count: " + count, count == 1); t1.interrupt(); try { t1.join(); } catch (InterruptedException e) { } // cleanup tg.destroy(); } // Test for method void java.lang.ThreadGroup.destroy() public void test_destroy() { final ThreadGroup originalCurrent = initialThreadGroup; ThreadGroup testRoot = new ThreadGroup(originalCurrent, "Test group"); final int DEPTH = 4; final Vector<ThreadGroup> subgroups = buildRandomTreeUnder(testRoot, DEPTH); // destroy them all testRoot.destroy(); for (int i = 0; i < subgroups.size(); i++) { ThreadGroup child = subgroups.elementAt(i); assertEquals("Destroyed child can't have children", 0, child.activeCount()); boolean passed = false; try { child.destroy(); } catch (IllegalThreadStateException e) { passed = true; } assertTrue("Destroyed child can't be destroyed again", passed); } testRoot = new ThreadGroup(originalCurrent, "Test group (daemon)"); testRoot.setDaemon(true); ThreadGroup child = new ThreadGroup(testRoot, "daemon child"); // If we destroy the last daemon's child, the daemon should get destroyed // as well child.destroy(); boolean passed = false; try { child.destroy(); } catch (IllegalThreadStateException e) { passed = true; } assertTrue("Daemon should have been destroyed already", passed); passed = false; try { testRoot.destroy(); } catch (IllegalThreadStateException e) { passed = true; } assertTrue("Daemon parent should have been destroyed automatically", passed); assertTrue( "Destroyed daemon's child should not be in daemon's list anymore", !arrayIncludes(groups(testRoot), child)); assertTrue("Destroyed daemon should not be in parent's list anymore", !arrayIncludes(groups(originalCurrent), testRoot)); testRoot = new ThreadGroup(originalCurrent, "Test group (daemon)"); testRoot.setDaemon(true); Thread noOp = new Thread(testRoot, null, "no-op thread") { @Override public void run() { } }; noOp.start(); // Wait for the no-op thread to run inside daemon ThreadGroup waitForThreadToDieUninterrupted(noOp); passed = false; try { child.destroy(); } catch (IllegalThreadStateException e) { passed = true; } assertTrue("Daemon group should have been destroyed already when last thread died", passed); testRoot = new ThreadGroup(originalCurrent, "Test group (daemon)"); noOp = new Thread(testRoot, null, "no-op thread") { @Override public void run() { try { Thread.sleep(500); } catch (InterruptedException ie) { fail("Should not be interrupted"); } } }; // Has to execute the next lines in an interval < the sleep interval of the no-op thread noOp.start(); passed = false; try { testRoot.destroy(); } catch (IllegalThreadStateException its) { passed = true; } assertTrue("Can't destroy a ThreadGroup that has threads", passed); // But after the thread dies, we have to be able to destroy the thread group waitForThreadToDieUninterrupted(noOp); passed = true; try { testRoot.destroy(); } catch (IllegalThreadStateException its) { passed = false; } assertTrue("Should be able to destroy a ThreadGroup that has no threads", passed); } // Test for method java.lang.ThreadGroup.destroy() public void test_destroy_subtest0() { ThreadGroup group1 = new ThreadGroup("test_destroy_subtest0"); group1.destroy(); try { new Thread(group1, "test_destroy_subtest0"); fail("should throw IllegalThreadStateException"); } catch (IllegalThreadStateException e) { } } // Test for method int java.lang.ThreadGroup.getMaxPriority() public void test_getMaxPriority() { final ThreadGroup originalCurrent = initialThreadGroup; ThreadGroup testRoot = new ThreadGroup(originalCurrent, "Test group"); boolean passed = true; try { testRoot.setMaxPriority(Thread.MIN_PRIORITY); } catch (IllegalArgumentException iae) { passed = false; } assertTrue("Should be able to set priority", passed); assertTrue("New value should be the same as we set", testRoot.getMaxPriority() == Thread.MIN_PRIORITY); testRoot.destroy(); } // Test for method java.lang.String java.lang.ThreadGroup.getName() public void test_getName() { final ThreadGroup originalCurrent = initialThreadGroup; final String name = "Test group"; final ThreadGroup testRoot = new ThreadGroup(originalCurrent, name); assertTrue("Setting a name&getting does not work", testRoot.getName().equals(name)); testRoot.destroy(); } // Test for method java.lang.ThreadGroup java.lang.ThreadGroup.getParent() public void test_getParent() { final ThreadGroup originalCurrent = initialThreadGroup; ThreadGroup testRoot = new ThreadGroup(originalCurrent, "Test group"); assertTrue("Parent is wrong", testRoot.getParent() == originalCurrent); // Create some groups, nested some levels. final int TOTAL_DEPTH = 5; ThreadGroup current = testRoot; Vector<ThreadGroup> groups = new Vector<ThreadGroup>(); // To maintain the invariant that a thread in the Vector is parent // of the next one in the collection (and child of the previous one) groups.addElement(testRoot); for (int i = 0; i < TOTAL_DEPTH; i++) { current = new ThreadGroup(current, "level " + i); groups.addElement(current); } // Now we walk the levels down, checking if parent is ok for (int i = 1; i < groups.size(); i++) { current = groups.elementAt(i); ThreadGroup previous = groups.elementAt(i - 1); assertTrue("Parent is wrong", current.getParent() == previous); } testRoot.destroy(); } // Test for method void java.lang.ThreadGroup.list() public void test_list() { final ThreadGroup originalCurrent = initialThreadGroup; final ThreadGroup testRoot = new ThreadGroup(originalCurrent, "Test group"); // First save the original System.out java.io.PrintStream originalOut = System.out; try { java.io.ByteArrayOutputStream contentsStream = new java.io.ByteArrayOutputStream(100); java.io.PrintStream newOut = new java.io.PrintStream(contentsStream); // We have to "redirect" System.out to test the method 'list' System.setOut(newOut); originalCurrent.list(); /* * The output has to look like this: * * java.lang.ThreadGroup[name=main,maxpri=10] Thread[main,5,main] * java.lang.ThreadGroup[name=Test group,maxpri=10] */ String contents = new String(contentsStream.toByteArray()); boolean passed = (contents.indexOf("ThreadGroup[name=main") != -1) && (contents.indexOf("Thread[") != -1) && (contents.indexOf("ThreadGroup[name=Test group") != -1); assertTrue("'list()' does not print expected contents. " + "Result from list: " + contents, passed); // Do proper cleanup testRoot.destroy(); } finally { // No matter what, we need to restore the original System.out System.setOut(originalOut); } } // Test for method boolean java.lang.ThreadGroup.parentOf(java.lang.ThreadGroup) public void test_parentOfLjava_lang_ThreadGroup() { final ThreadGroup originalCurrent = initialThreadGroup; final ThreadGroup testRoot = new ThreadGroup(originalCurrent, "Test group"); final int DEPTH = 4; buildRandomTreeUnder(testRoot, DEPTH); final ThreadGroup[] allChildren = allGroups(testRoot); for (ThreadGroup element : allChildren) { assertTrue("Have to be parentOf all children", testRoot.parentOf(element)); } assertTrue("Have to be parentOf itself", testRoot.parentOf(testRoot)); testRoot.destroy(); assertTrue("Parent can't have test group as subgroup anymore", !arrayIncludes(groups(testRoot.getParent()), testRoot)); } // Test for method boolean java.lang.ThreadGroup.isDaemon() and // void java.lang.ThreadGroup.setDaemon(boolean) public void test_setDaemon_isDaemon() { final ThreadGroup originalCurrent = initialThreadGroup; final ThreadGroup testRoot = new ThreadGroup(originalCurrent, "Test group"); testRoot.setDaemon(true); assertTrue("Setting daemon&getting does not work", testRoot.isDaemon()); testRoot.setDaemon(false); assertTrue("Setting daemon&getting does not work", !testRoot.isDaemon()); testRoot.destroy(); } /* * java.lang.ThreadGroupt#setDaemon(boolean) */ public void test_setDaemon_Parent_Child() { ThreadGroup ptg = new ThreadGroup("Parent"); ThreadGroup ctg = new ThreadGroup(ptg, "Child"); ctg.setDaemon(true); assertTrue(ctg.isDaemon()); ctg.setDaemon(false); assertFalse(ctg.isDaemon()); ptg.setDaemon(true); assertFalse(ctg.isDaemon()); ptg.setDaemon(false); assertFalse(ctg.isDaemon()); } // Test for method void java.lang.ThreadGroup.setMaxPriority(int) public void test_setMaxPriorityI() { final ThreadGroup originalCurrent = initialThreadGroup; ThreadGroup testRoot = new ThreadGroup(originalCurrent, "Test group"); boolean passed; // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - int currentMax = testRoot.getMaxPriority(); testRoot.setMaxPriority(Thread.MAX_PRIORITY + 1); passed = testRoot.getMaxPriority() == currentMax; assertTrue( "setMaxPriority: Any value higher than the current one is ignored. Before: " + currentMax + " , after: " + testRoot.getMaxPriority(), passed); // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - currentMax = testRoot.getMaxPriority(); testRoot.setMaxPriority(Thread.MIN_PRIORITY - 1); passed = testRoot.getMaxPriority() == Thread.MIN_PRIORITY; assertTrue( "setMaxPriority: Any value smaller than MIN_PRIORITY is adjusted to MIN_PRIORITY. Before: " + currentMax + " , after: " + testRoot.getMaxPriority(), passed); // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - testRoot.destroy(); testRoot = new ThreadGroup(originalCurrent, "Test group"); // Create some groups, nested some levels. Each level will have maxPrio // 1 unit smaller than the parent's. However, there can't be a group // with priority < Thread.MIN_PRIORITY final int TOTAL_DEPTH = testRoot.getMaxPriority() - Thread.MIN_PRIORITY - 2; ThreadGroup current = testRoot; for (int i = 0; i < TOTAL_DEPTH; i++) { current = new ThreadGroup(current, "level " + i); } // Now we walk the levels down, changing the maxPrio and later verifying // that the value is indeed 1 unit smaller than the parent's maxPrio. int maxPrio, parentMaxPrio; current = testRoot; // To maintain the invariant that when we are to modify a child, // its maxPriority is always 1 unit smaller than its parent's. // We have to set it for the root manually, and the loop does the rest // for all the other sub-levels current.setMaxPriority(current.getParent().getMaxPriority() - 1); for (int i = 0; i < TOTAL_DEPTH; i++) { maxPrio = current.getMaxPriority(); parentMaxPrio = current.getParent().getMaxPriority(); ThreadGroup[] children = groups(current); assertEquals("Can only have 1 subgroup", 1, children.length); current = children[0]; assertTrue( "Had to be 1 unit smaller than parent's priority in iteration=" + i + " checking->" + current, maxPrio == parentMaxPrio - 1); current.setMaxPriority(maxPrio - 1); // The next test is sort of redundant, since in next iteration it // will be the parent tGroup, so the test will be done. assertTrue("Had to be possible to change max priority", current .getMaxPriority() == maxPrio - 1); } assertTrue( "Priority of leaf child group has to be much smaller than original root group", current.getMaxPriority() == testRoot.getMaxPriority() - TOTAL_DEPTH); testRoot.destroy(); // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - passed = true; testRoot = new ThreadGroup(originalCurrent, "Test group"); try { testRoot.setMaxPriority(Thread.MAX_PRIORITY); } catch (IllegalArgumentException iae) { passed = false; } assertTrue( "Max Priority = Thread.MAX_PRIORITY should be possible if the test is run with default system ThreadGroup as root", passed); testRoot.destroy(); } /* * Test for method void java.lang.ThreadGroup.uncaughtException(java.lang.Thread, * java.lang.Throwable) * Tests if a Thread tells its ThreadGroup about ThreadDeath. */ public void test_uncaughtException_threadDeath() { final boolean[] passed = new boolean[1]; ThreadGroup testRoot = new ThreadGroup(rootThreadGroup, "Test Forcing a throw of ThreadDeath") { @Override public void uncaughtException(Thread t, Throwable e) { if (e instanceof ThreadDeath) { passed[0] = true; } // always forward, any exception super.uncaughtException(t, e); } }; final ThreadDeath threadDeath = new ThreadDeath(); Thread thread = new Thread(testRoot, null, "suicidal thread") { @Override public void run() { throw threadDeath; } }; thread.start(); waitForThreadToDieUninterrupted(thread); testThreadDefaultUncaughtExceptionHandler.assertWasCalled(thread, threadDeath); testRoot.destroy(); assertTrue( "Any thread should notify its ThreadGroup about its own death, even if suicide:" + testRoot, passed[0]); } /* * Test for method void java.lang.ThreadGroup.uncaughtException(java.lang.Thread, * java.lang.Throwable) * Test if a Thread tells its ThreadGroup about a natural (non-exception) death. */ public void test_uncaughtException_naturalDeath() { final boolean[] failed = new boolean[1]; ThreadGroup testRoot = new ThreadGroup(initialThreadGroup, "Test ThreadDeath") { @Override public void uncaughtException(Thread t, Throwable e) { failed[0] = true; // always forward any exception super.uncaughtException(t, e); } }; Thread thread = new Thread(testRoot, null, "no-op thread"); thread.start(); waitForThreadToDieUninterrupted(thread); testThreadDefaultUncaughtExceptionHandler.assertWasNotCalled(); testRoot.destroy(); assertFalse("A thread should not call uncaughtException when it dies:" + testRoot, failed[0]); } /* * Test for method void java.lang.ThreadGroup.uncaughtException(java.lang.Thread, * java.lang.Throwable) * Test if a Thread tells its ThreadGroup about an Exception */ public void test_uncaughtException_runtimeException() { // Our own exception class class TestException extends RuntimeException { private static final long serialVersionUID = 1L; } final boolean[] passed = new boolean[1]; ThreadGroup testRoot = new ThreadGroup(initialThreadGroup, "Test other Exception") { @Override public void uncaughtException(Thread t, Throwable e) { if (e instanceof TestException) { passed[0] = true; } // always forward any exception super.uncaughtException(t, e); } }; final TestException testException = new TestException(); Thread thread = new Thread(testRoot, null, "RuntimeException thread") { @Override public void run() { throw testException; } }; thread.start(); waitForThreadToDieUninterrupted(thread); testThreadDefaultUncaughtExceptionHandler.assertWasCalled(thread, testException); testRoot.destroy(); assertTrue( "Any thread should notify its ThreadGroup about an uncaught exception:" + testRoot, passed[0]); } /* * Test for method void java.lang.ThreadGroup.uncaughtException(java.lang.Thread, * java.lang.Throwable) * Test if a handler doesn't pass on the exception to super.uncaughtException that's ok. */ public void test_uncaughtException_exceptionHandledByHandler() { // Our own exception class class TestException extends RuntimeException { private static final long serialVersionUID = 1L; } ThreadGroup testRoot = new ThreadGroup(initialThreadGroup, "Test other Exception") { @Override public void uncaughtException(Thread t, Throwable e) { // Swallow TestException and always forward any other exception if (!(e instanceof TestException)) { super.uncaughtException(t, e); } } }; final TestException testException = new TestException(); Thread thread = new Thread(testRoot, null, "RuntimeException thread") { @Override public void run() { throw testException; } }; thread.start(); waitForThreadToDieUninterrupted(thread); testThreadDefaultUncaughtExceptionHandler.assertWasNotCalled(); testRoot.destroy(); } /* * Test for method void java.lang.ThreadGroup.uncaughtException(java.lang.Thread, * java.lang.Throwable) * Tests an exception thrown by the handler itself. */ public void test_uncaughtException_exceptionInUncaughtException() { // Our own uncaught exception classes class UncaughtException extends RuntimeException { private static final long serialVersionUID = 1L; } ThreadGroup testRoot = new ThreadGroup(initialThreadGroup, "Test Exception in uncaught exception") { @Override public void uncaughtException(Thread t, Throwable e) { // This should be no-op according to the spec throw new UncaughtException(); } }; Thread thread = new Thread(testRoot, null, "no-op thread") { @Override public void run() { throw new RuntimeException(); } }; thread.start(); waitForThreadToDieUninterrupted(thread); testThreadDefaultUncaughtExceptionHandler.assertWasNotCalled(); testRoot.destroy(); } private static ThreadGroup[] allGroups(ThreadGroup parent) { int count = parent.activeGroupCount(); ThreadGroup[] all = new ThreadGroup[count]; parent.enumerate(all, true); return all; } private static void asyncBuildRandomTreeUnder(final ThreadGroup aGroup, final int depth, final Vector<ThreadGroup> allCreated) { if (depth <= 0) { return; } final int maxImmediateSubgroups = random(3); for (int i = 0; i < maxImmediateSubgroups; i++) { final int iClone = i; final String name = " Depth = " + depth + ",N = " + iClone + ",Vector size at creation: " + allCreated.size(); // Use concurrency to maximize chance of exposing concurrency bugs // in ThreadGroups Thread t = new Thread(aGroup, name) { @Override public void run() { ThreadGroup newGroup = new ThreadGroup(aGroup, name); allCreated.addElement(newGroup); asyncBuildRandomTreeUnder(newGroup, depth - 1, allCreated); } }; t.start(); } } private static Vector<ThreadGroup> asyncBuildRandomTreeUnder(final ThreadGroup aGroup, final int depth) { Vector<ThreadGroup> result = new Vector<ThreadGroup>(); asyncBuildRandomTreeUnder(aGroup, depth, result); return result; } private static ThreadGroup[] groups(ThreadGroup parent) { // No API to get the count of immediate children only ? int count = parent.activeGroupCount(); ThreadGroup[] all = new ThreadGroup[count]; parent.enumerate(all, false); // Now we may have nulls in the array, we must find the actual size int actualSize = 0; for (; actualSize < all.length; actualSize++) { if (all[actualSize] == null) { break; } } ThreadGroup[] result; if (actualSize == all.length) { result = all; } else { result = new ThreadGroup[actualSize]; System.arraycopy(all, 0, result, 0, actualSize); } return result; } private static int random(int max) { return 1 + ((new Object()).hashCode() % max); } private static Vector<ThreadGroup> buildRandomTreeUnder(ThreadGroup aGroup, int depth) { Vector<ThreadGroup> result = asyncBuildRandomTreeUnder(aGroup, depth); while (true) { int sizeBefore = result.size(); try { Thread.sleep(1000); int sizeAfter = result.size(); // If no activity for a while, we assume async building may be // done. if (sizeBefore == sizeAfter) { // It can only be done if no more threads. Unfortunately we // are relying on this API to work as well. // If it does not, we may loop forever. if (aGroup.activeCount() == 0) { break; } } } catch (InterruptedException e) { } } return result; } private static boolean arrayIncludes(Object[] array, Object toTest) { for (Object element : array) { if (element == toTest) { return true; } } return false; } private static void waitForThreadToDieUninterrupted(Thread thread) { try { thread.join(); } catch (InterruptedException ie) { fail("Should not have been interrupted"); } } private static class TestThreadDefaultUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler { private boolean called; private Throwable ex; private Thread thread; @Override public void uncaughtException(Thread thread, Throwable ex) { this.called = true; this.thread = thread; this.ex = ex; } public void assertWasCalled(Thread thread, Throwable ex) { assertTrue(called); assertSame(this.thread, thread); assertSame(this.ex, ex); } public void assertWasNotCalled() { assertFalse(called); } } }