/*
* 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);
}
}
}