org.scalatest.concurrent.Conductor.scala Maven / Gradle / Ivy
Show all versions of scalatest_2.11.0-M3 Show documentation
/* * Copyright 2001-2012 Artima, Inc. * * Licensed 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.scalatest.concurrent import org.scalatest._ import PimpedThreadGroup._ import _root_.java.util.concurrent._ import _root_.java.util.concurrent.atomic.AtomicReference import org.scalatest.exceptions.StackDepthExceptionHelper.getStackDepthFun import org.scalatest.exceptions.NotAllowedException /** *
may decide to advance the beat too early. * * * @author Josh Cough * @author Bill Venners */ @deprecated("org.scalatest.concurrent.Conductor has been deprecated and will be removed in a future version of ScalaTest. Please mix in trait Conductors, which now defines Conductor, instead of using Conductor directly.") final class Conductor { /** * The metronome used to coordinate between threads. * This clock is advanced by the clock thread. * The clock will not advance if it is frozen. */ private final val clock = new Clock /////////////////////// thread management start ////////////////////////////// // place all threads in a new thread group private final val threadGroup = new ThreadGroup("Orchestra") // all the threads in this test // This need not be volatile, because it is initialized with one object and // that stays forever. Because it is final, it private final val threads = new CopyOnWriteArrayList[Thread]() // Used to keep track of what names have been created so far, so that // it can be enforced that the names are unique. private final val threadNames = new CopyOnWriteArrayList[String]() // the main test thread private final val mainThread = Thread.currentThread /** * Creates a new thread that will execute the specified function. * *org.scalatest.concurrent.Conductor
has been deprecated and will * be removed in a future version of ScalaTest. Please mix in or import the members * of traitConductors
, into whichConductor
has been moved, instead * of using this class directly. * ** The reason
* *Conductor
was moved into traitConductors
* was so that it can extend trait *PatienceConfiguration
, which was * introduced in ScalaTest 1.8. This will makeConductor
configurable in a * way consistent with traitsEventually
andAsyncAssertions
* (both of which were also introduced in ScalaTest 1.8), and scalable with the *scaled
method of trait *ScaledTimeSpans
. ** Class that facilitates the testing of classes, traits, and libraries designed * to be used by multiple threads concurrently. *
* ** A
* *Conductor
conducts a multi-threaded scenario by maintaining * a clock of "beats." Beats are numbered starting with 0. You can ask a *Conductor
to run threads that interact with the class, trait, * or library (the subject) * you want to test. A thread can call theConductor
's *waitForBeat
method, which will cause the thread to block * until that beat has been reached. TheConductor
will advance * the beat only when all threads participating in the test are blocked. By * tying the timing of thread activities to specific beats, you can write * tests for concurrent systems that have deterministic interleavings of * threads. ** A
* *Conductor
object has a three-phase lifecycle. It begins its life * in the setup phase. During this phase, you can start threads by * invoking thethread
method on theConductor
. * Whenconduct
is invoked on aConductor
, it enters * the conducting phase. During this phase it conducts the one multi-threaded * scenario it was designed to conduct. After all participating threads have exited, either by * returning normally or throwing an exception, theconduct
method * will complete, either by returning normally or throwing an exception. As soon as * theconduct
method completes, theConductor
* enters its defunct phase. Once theConductor
has conducted * a multi-threaded scenario, it is defunct and can't be reused. To run the same test again, * you'll need to create a new instance ofConductor
. ** Here's an example of the use of
* *Conductor
to test theArrayBlockingQueue
* class fromjava.util.concurrent
: ** import org.scalatest.fixture.FunSuite * import org.scalatest.matchers.ShouldMatchers * import java.util.concurrent.ArrayBlockingQueue * * class ArrayBlockingQueueSuite extends FunSuite with ShouldMatchers { * * test("calling put on a full queue blocks the producer thread") { * * val conductor = new Conductor * import conductor._ * * val buf = new ArrayBlockingQueue[Int](1) * * thread("producer") { * buf put 42 * buf put 17 * beat should be (1) * } * * thread("consumer") { * waitForBeat(1) * buf.take should be (42) * buf.take should be (17) * } * * whenFinished { * buf should be ('empty) * } * } * } ** ** When the test shown is run, it will create one thread named producer and another named * consumer. The producer thread will eventually execute the code passed as a by-name * parameter to
* *thread("producer")
: ** buf put 42 * buf put 17 * beat should be (1) ** * Similarly, the consumer thread will eventually execute the code passed as a by-name parameter * tothread("consumer")
: * * ** waitForBeat(1) * buf.take should be (42) * buf.take should be (17) ** ** The
* *thread
calls create the threads and starts them, but they will not immediately * execute the by-name parameter passed to them. They will first block, waiting for theConductor
* to give them a green light to proceed. ** The next call in the test is
* *whenFinished
. This method will first callconduct
on * theConductor
, which will wait until all threads that were created (in this case, producer and consumer) are * at the "starting line", i.e., they have all started and are blocked, waiting on the green light. * Theconduct
method will then give these threads the green light and they will * all start executing their blocks concurrently. ** When the threads are given the green light, the beat is 0. The first thing the producer thread does is put 42 in * into the queue. As the queue is empty at this point, this succeeds. The producer thread next attempts to put a 17 * into the queue, but because the queue has size 1, this can't succeed until the consumer thread has read the 42 * from the queue. This hasn't happened yet, so producer blocks. Meanwhile, the consumer thread's first act is to * call
* *waitForBeat(1)
. Because the beat starts out at 0, this call will block the consumer thread. * As a result, once the producer thread has executedbuf put 17
and the consumer thread has executed *waitForBeat(1)
, both threads will be blocked. ** The
* *Conductor
maintains a clock that wakes up periodically and checks to see if all threads * participating in the multi-threaded scenario (in this case, producer and consumer) are blocked. If so, it * increments the beat. Thus sometime later the beat will be incremented, from 0 to 1. Because consumer was * waiting for beat 1, it will wake up (i.e., thewaitForBeat(1)
call will return) and * execute the next line of code in its block,buf.take should be (42)
. This will succeed, because * the producer thread had previously (during beat 0) put 42 into the queue. This act will also make * producer runnable again, because it was blocked on the secondput
, which was waiting for another * thread to read that 42. ** Now both threads are unblocked and able to execute their next statement. The order is * non-deterministic, and can even be simultaneous if running on multiple cores. If the
* *consumer
thread * happens to executebuf.take should be (17)
first, it will block (buf.take
will not return), because the queue is * at that point empty. At some point later, the producer thread will executebuf put 17
, which will * unblock the consumer thread. Again both threads will be runnable and the order non-deterministic and * possibly simulataneous. The producer thread may charge ahead and run its next statement,beat should be (1)
. * This will succeed because the beat is indeed 1 at this point. As this is the last statement in the producer's block, * the producer thread will exit normally (it won't throw an exception). At some point later the consumer thread will * be allowed to complete its last statement, thebuf.take
call will return 17. The consumer thread will * execute17 should be (17)
. This will succeed and as this was the last statement in its block, the consumer will return * normally. ** If either the producer or consumer thread had completed abruptbly with an exception, the
* *conduct
method * (which was called bywhenFinished
) would have completed abruptly with an exception to indicate the test * failed. However, since both threads returned normally,conduct
will return. Becauseconduct
doesn't * throw an exception,whenFinished
will execute the block of code passed as a by-name parameter to it:buf should be ('empty)
. * This will succeed, because the queue is indeed empty at this point. ThewhenFinished
method will then return, and * because thewhenFinished
call was the last statement in the test and it didn't throw an exception, the test completes successfully. ** This test tests
* *ArrayBlockingQueue
, to make sure it works as expected. If there were a bug inArrayBlockingQueue
* such as aput
called on a full queue didn't block, but instead overwrote the previous value, this test would detect * it. However, if there were a bug inArrayBlockingQueue
such that a call totake
called on an empty queue * never blocked and always returned 0, this test might not detect it. The reason is that whether the consumer thread will ever call *take
on an empty queue during this test is non-deterministic. It depends on how the threads get scheduled during beat 1. * What is deterministic in this test, because the consumer thread blocks during beat 0, is that the producer thread will definitely * attempt to write to a full queue. To make sure the other scenario is tested, you'd need a different test: ** test("calling take on an empty queue blocks the consumer thread") { * * val conductor = new Conductor * import conductor._ * * val buf = new ArrayBlockingQueue[Int](1) * * thread("producer") { * waitForBeat(1) * buf put 42 * buf put 17 * } * * thread("consumer") { * buf.take should be (42) * buf.take should be (17) * beat should be (1) * } * * whenFinished { * buf should be ('empty) * } * } ** ** In this test, the producer thread will block, waiting for beat 1. The consumer thread will invoke
* *buf.take
* as its first act. This will block, because the queue is empty. Because both threads are blocked, theConductor
* will at some point later increment the beat to 1. This will awaken the producer thread. It will return from its *waitForBeat(1)
call and executebuf put 42
. This will unblock the consumer thread, which will * take the 42, and so on. ** The problem that
* *Conductor
is designed to address is the difficulty, caused by the non-deterministic nature * of thread scheduling, of testing classes, traits, and libraries that are intended to be used by multiple threads. * If you just create a test in which one thread reads from anArrayBlockingQueue
and * another writes to it, you can't be sure that you have tested all possible interleavings of threads, no matter * how many times you run the test. The purpose ofConductor
* is to enable you to write tests with deterministic interleavings of threads. If you write one test for each possible * interleaving of threads, then you can be sure you have all the scenarios tested. The two tests shown here, for example, * ensure that both the scenario in which a producer thread tries to write to a full queue and the scenario in which a * consumer thread tries to take from an empty queue are tested. ** Class
* *Conductor
was inspired by the * MultithreadedTC project, * created by Bill Pugh and Nat Ayewah of the University of Maryland. ** Although useful, bear in mind that a
Conductor
's results are not guaranteed to be * accurate 100% of the time. The reason is that it usesjava.lang.Thread
'sgetState
method to * decide when to advance the beat. This kind of use is advised against in the Javadoc documentation for *getState
, which says, "This method is designed for use in monitoring of the system state, not for * synchronization." In short, sometimes the return value ofgetState
Conductor* The name of the thread will be of the form Conductor-Thread-N, where N is some integer. *
* ** This method may be safely called by any thread. *
* * @param fun the function to be executed by the newly created thread * @return the newly created thread */ def thread(fun: => Unit): Thread = thread("Conductor-Thread-" + threads.size) { fun } /** * Creates a new thread with the specified name that will execute the specified function. * ** This method may be safely called by any thread. *
* * @param name the name of the newly created thread * @param fun the function to be executed by the newly created thread * @return the newly created thread */ def thread(name: String)(fun: => Unit): Thread = { currentState.get match { case TestFinished => throw new NotAllowedException(Resources("threadCalledAfterConductingHasCompleted"), getStackDepthFun("Conductor.scala", "thread")) case _ => if (threadNames contains name) throw new NotAllowedException(Resources("cantRegisterThreadsWithSameName", name), getStackDepthFun("Conductor.scala", "thread")) val t = TestThread(name, fun _) threads add t threadNames add name t.start() t } } // The reason that the thread is started immediately, is so that nested threads // will start immediately, without requiring the user to explicitly start() them. // Also, so that the thread method can return a Thread object. /* * A test thread runs the given function. * It only does so after it is given permission to do so by the main thread. * The main thread grants permission after it receives notication that * all test threads are ready to go. */ private case class TestThread(name: String, f: () => Unit) extends Thread(threadGroup, name) { // Indicate a TestThread has been created that has not yet started running testThreadsStartingCounter.increment() override def run() { try { // Indicate to the TestThreadsStartingCounter that one more thread is ready to go testThreadsStartingCounter.decrement() // wait for the main thread to say its ok to go. greenLightForTestThreads.await // go f() } catch { case t: Throwable => if (firstExceptionThrown.isEmpty) { // The mainThread is likely joined to some test thread, so it needs to be awakened. If it // is joined to this thread, it will wake up shortly because this thread is about to die // by returning. If it is joined to a different thread, then it needs to be interrupted, // but this thread can't interrupt it, because then there's a race condition if it is // actually joined to this thread, between join returning because this thread returns // or join throwing an InterruptedException. So here just offer the throwable to // the firstExceptionThrown queue and return. Only the first will be accepted by the queue. // ThreadDeath exceptions that arise from being stopped will not go in because the queue // is already full. The clock thread checks the firestExceptionThrown queue each cycle, and // if it finds it is non-empty, it stops any live thread. firstExceptionThrown offer t } } } } /** * A BlockingQueue containing the first exception that occured * in test threads, or that was thrown by the clock thread. */ private val firstExceptionThrown = new ArrayBlockingQueue[Throwable](1) // Won't write one that takes clockPeriod and timeout for 1.0. For now people // can just call conduct(a, b) directly followed by the code they want to run // afterwords. See if anyone asks for a whenFinished(a, b) {} /** * Invokesconduct
and afterconduct
method returns, * ifconduct
returns normally (i.e., without throwing * an exception), invokes the passed function. * ** If
* *conduct
completes abruptly with an exception, this method * will complete abruptly with the same exception and not execute the passed * function. ** This method must be called by the thread that instantiated this
* *Conductor
, * and that same thread will invokeconduct
and, if it returns noramlly, execute * the passed function. ** Because
* * @param fun the function to execute afterwhenFinished
invokesconduct
, it can only be invoked * once on aConductor
instance. As a result, if you need to pass a block of * code towhenFinished
it should be the last statement of your test. If you * don't have a block of code that needs to be run once all the threads have finished * successfully, then you can simply invokeconduct
and never invoke *whenFinished
. *conduct
call returns * @throws NotAllowedException if the calling thread is not the thread that * instantiated thisConductor
, or ifconduct
has already * been invoked on this conductor. */ def whenFinished(fun: => Unit) { if (Thread.currentThread != mainThread) throw new NotAllowedException(Resources("whenFinishedCanOnlyBeCalledByMainThread"), getStackDepthFun("Conductor.scala", "whenFinished")) if (conductingHasBegun) throw new NotAllowedException(Resources("cannotInvokeWhenFinishedAfterConduct"), getStackDepthFun("Conductor.scala", "whenFinished")) conduct() fun } /** * Blocks the current thread until the thread beat reaches the * specified value, at which point the current thread will be unblocked. * * @param beat the tick value to wait for * @throws NotAllowedException if the abeat
less than or equal to zero is passed */ def waitForBeat(beat: Int) { if (beat == 0) throw new NotAllowedException(Resources("cannotWaitForBeatZero"), getStackDepthFun("Conductor.scala", "waitForBeat")) if (beat < 0) throw new NotAllowedException(Resources("cannotWaitForNegativeBeat"), getStackDepthFun("Conductor.scala", "waitForBeat")) clock waitForBeat beat } /** * The current value of the thread clock. * * @return the current beat value */ def beat: Int = clock.currentBeat /** * Executes the passed function with theConductor
frozen so that it * won't advance the clock. * ** While the
* * @param fun the function to execute while theConductor
is frozen, the beat will not advance. Once the * passed function has completed executing, theConductor
will be unfrozen * so that the beat will advance when all threads are blocked, as normal. *Conductor
is frozen. */ def withConductorFrozen[T](fun: => T) { clock.withClockFrozen(fun) } /** * Indicates whether the conductor has been frozen. * ** Note: The only way a thread * can freeze the conductor is by calling
*/ def isConductorFrozen: Boolean = clock.isFrozen private val testThreadsStartingCounter = new TestThreadsStartingCounter /** * Keeps the test threads from executing their bodies until the main thread * allows them to. */ private val greenLightForTestThreads = new CountDownLatch(1) /** * Conducts a multithreaded test with a default clock period of 10 milliseconds * and default run limit of 5 seconds. */ def conduct() { val DefaultClockPeriod = 10 // milliseconds val DefaultRunLimit = 5 // seconds conduct(DefaultClockPeriod, DefaultRunLimit) } private val currentState: AtomicReference[ConductorState] = new AtomicReference(Setup) /** * Indicates whether either of the two overloadedwithConductorFrozen
. *conduct
methods * have been invoked. * ** This method returns true if either
*/ def conductingHasBegun: Boolean = currentState.get.testWasStarted /** * Conducts a multithreaded test with the specified clock period (in milliseconds) * and timeout (in seconds). * *conduct
method has been invoked. The *conduct
method may have returned or not. (In other words, atrue
* result from this method does not mean theconduct
method has returned, * just that it was already been invoked and,therefore, the multi-threaded scenario it * conducts has definitely begun.) ** A
* * @param clockPeriod The period (in ms) the clock will sleep each time it sleeps * @param timeout The maximum allowed time between successive advances of the beat. If this time * is exceeded, the Conductor will abort the test. * @throws Throwable The first error or exception that is thrown by one of the test threads, or * aConductor
instance maintains an internal clock, which will wake up * periodically and check to see if it should advance the beat, abort the test, or go back to sleep. * It sleepsclockPeriod
milliseconds each time. It will abort the test * if either deadlock is suspected or the beat has not advanced for the number of * seconds specified astimeout
. Suspected deadlock will be declared if * for some number of consecutive clock cycles, all test threads are in theBLOCKED
or *WAITING
states and none of them are waiting for a beat. *TestFailedException
if the test was aborted due to a timeout or suspected deadlock. */ def conduct(clockPeriod: Int, timeout: Int) { if (clockPeriod <= 0) throw new NotAllowedException(Resources("cannotPassNonPositiveClockPeriod", clockPeriod.toString), getStackDepthFun("Conductor.scala", "conduct")) if (timeout <= 0) throw new NotAllowedException(Resources("cannotPassNonPositiveTimeout", timeout.toString), getStackDepthFun("Conductor.scala", "conduct")) // if the test was started already, explode // otherwise, change state to TestStarted if (conductingHasBegun) throw new NotAllowedException(Resources("cannotCallConductTwice"), getStackDepthFun("Conductor.scala", "conduct")) else currentState set TestStarted // wait until all threads are definitely ready to go if (threads.size > 0) testThreadsStartingCounter.waitUntilAllTestThreadsHaveStarted() // release the latch, allowing all threads to start // wait for all the test threads to start before starting the clock greenLightForTestThreads.countDown() // start the clock thread val clockThread = ClockThread(clockPeriod, timeout) clockThread.start() // wait until all threads have ended waitForThreads // change state to test finished currentState set TestFinished if (!firstExceptionThrown.isEmpty) throw firstExceptionThrown.peek } /** * Wait for all of the test case threads to complete, or for one * of the threads to throw an exception, or for the clock thread to * interrupt this (main) thread of execution. When the clock thread * or other threads fail, the error is placed in the shared error array * and thrown by this method. * * @param threads List of all the test case threads and the clock thread */ // Explain how we understand it works: if the thread that's been joined already dies with an exception // that will go into errors, and this thread that called join will return. If the thread that's been joined returns and doesn't // die, that means all went well, and join will return and it can loop to the next one. // There should be no race condition between the last thread being waited on by join, it dies, join // returns, and after that the error gets into the errors. Because if you look in run() in the // thread inside createTestThread, the signaling error happens in a catch Throwable block before the thread // returns. private def waitForThreads { var interrupted = false while(!interrupted && threadGroup.areAnyThreadsAlive) { threadGroup.getThreads.foreach { t => if (!interrupted && t.isAlive && firstExceptionThrown.isEmpty) try { t.join() } catch { // main thread will be interrupted if a timeout occurs, deadlock is suspected, // or a test thread completes abruptly with an exception. Just loop here, because // firstExceptionThrown should be non-empty after InterruptedException is caught, and // if not, then I don't know how it got interrupted, but just keep looping. case e: InterruptedException => interrupted = true } } } } /** * A Clock manages the current beat in a Conductor. * Several duties stem from that responsibility. * * The clock will: * **
*/ private class Clock { import java.util.concurrent.locks.ReentrantReadWriteLock import PimpedReadWriteLock._ // clock starts at time 0 private var currentTime = 0 // methods in Clock that access or modify the private instance vars of this // Clock are synchronized on the object referenced from lock private val lock = new AnyRef /** * Read locks are acquired when clock is frozen and must be * released before the clock can advance in a advance(). (In a * ReentrantReadWriteLock, multiple threads can hold the read lock (and these * threads might read the value of currentTime (the currentBeat method), or just execute a * function with the clock frozen (the withClockFrozen method). The write lock * of a ReentrantReadWriteLock is exclusive, so only one can hold it, and it * can't be held if there are a thread or threads holding the read lock. This * is why the clock can't advance during a withClockFrozen, because the read * lock is grabbed before the function is executed in withClockFrozen, thus * advance will not be able to acquire the write lock to update currentTime * until after withClockFrozen has released the read lock (and there are no other * threads holding a read lock or the write lock). */ private val rwLock = new ReentrantReadWriteLock private var highestBeatBeingWaitedOn = 0 /** * Advance the current beat. In order to do so, the clock will wait * until it has become unfrozen. * * All threads waiting for the clock to advance (they would have been put in the lock * object's wait set by invoking the waitForBeat method) will be notified after the advance. * * Only the clock thread should be calling this. * * If the clock has been frozen by a thread, then that thread will own the readLock. Write * lock can only be acquired when there are no readers, so ticks won't progress while someone * has the clock frozen. Other methods also grab the read lock, like time (which gets * the current beat.) */ def advance() { lock.synchronized { rwLock.write { currentTime += 1 } lock.notifyAll() } } /** * The current beat. */ def currentBeat: Int = lock.synchronized { rwLock read currentTime } /** * When wait for beat is called, the current thread will block until * the given beat is reached by the clock. */ def waitForBeat(beat: Int) { lock.synchronized { if (beat > highestBeatBeingWaitedOn) highestBeatBeingWaitedOn = beat while (currentBeat < beat) { try { lock.wait() } catch { // TODO: this is probably fine, but check JCIP about InterEx again case e: InterruptedException => throw new AssertionError(e) } // Actually I"m not sure. Maybe should reset the interupted status } } } // The reason there's no race condition between calling currentBeat in the while and calling // lock.wait() later (between that) and some other thread incrementing the beat and doing // a notify that this thread would miss (which it would want to know about if that's the // new time that it's waiting for) is because both this and the currentBeat method are synchronized // on the lock. /** * Returns true if any thread is waiting for a beat in the future (greater than the current beat) */ def isAnyThreadWaitingForABeat = { lock.synchronized { highestBeatBeingWaitedOn > currentTime } } /** * When the clock is frozen, it will not advance even when all threads * are blocked. Use this to block the current thread with a time limit, * but prevent the clock from advancing due to a waitForBeat(Int) in * another thread. */ def withClockFrozen[T](fun: => T): T = rwLock read fun /** * Check if the clock has been frozen by any threads. */ def isFrozen: Boolean = rwLock.getReadLockCount > 0 } /** * The clock thread is the manager of the multi-threaded scenario. * Periodically checks all the test threads and regulates them. * If all the threads are blocked and at least one is waiting for a beat, * the clock advances to the next beat and all waiting threads are notified. * If none of the threads are waiting for a tick or in timed waiting, * a deadlock is detected. The clock thread times out if a thread is in runnable * or all are blocked and one is in timed waiting for longer than the runLimit. * * Algorithm in detail: * * While there are threads alive * * If there are threads RUNNING * * If they have been running too long * * stop the test with a timeout error * * else if there are threads waiting for a beat * * advance the clock * * else if there are threads in TIMED_WAITING * * increment the deadlock counter * * if the deadlock counter has reached a threshold * * stop the test due to potential deadlock * * sleep clockPeriod ms * * * @param mainThread The main test thread. This thread will be waiting * for all the test threads to finish. It will be interrupted if the * ClockThread detects a deadlock or timeout. * * @param clockPeriod The period (in ms) between checks for the clock * * @param maxRunTime The limit to run the test in seconds */ private case class ClockThread(clockPeriod: Int, maxRunTime: Int) extends Thread("Conductor-Clock") { // When a test thread throws an exception, the main thread will stop all the other threads, // but won't stop the clock thread. This is because the clock thread will simply return after // all the other threads have died. Thus the clock thread could last beyond the end of the // application, if the clock period was set high. Thus by making the clock thread a daemon // thread, it won't keep the application up just because it is still asleep and hasn't noticed // yet that all the test threads are gone. this setDaemon true // used in detecting timeouts private var lastProgress = System.currentTimeMillis // used in detecting deadlocks private var deadlockCount = 0 private val MaxDeadlockDetectionsBeforeDeadlock = 50 /** * Runs the steps described above. */ override def run { // While there are threads that are not NEW or TERMINATED. (A thread is // NEW after it has been instantiated, but run() hasn't been called yet.) // So this means there are threads that are RUNNABLE, BLOCKED, WAITING, or // TIMED_WAITING. (BLOCKED is waiting for a lock. WAITING is in the wait set.) while (threadGroup.areAnyThreadsAlive) { if (!firstExceptionThrown.isEmpty) { // If any exception has been thrown, stop any live test thread. threadGroup.getThreads.foreach { t => if (t.isAlive) t.stop() } } // If any threads are in the RUNNABLE state, just check to see if there's been // no progress for more than the timeout amount of time. If RUNNABLE threads // exist, but the timeout limit has not been reached, then just go // back to sleep. else if (threadGroup.areAnyThreadsRunning) { if (runningTooLong) timeout() } // No RUNNABLE threads, so if any threads are waiting for a beat, advance // the beat. else if (clock.isAnyThreadWaitingForABeat) { clock.advance() deadlockCount = 0 lastProgress = System.currentTimeMillis } else if (!threadGroup.areAnyThreadsInTimedWaiting) { // At this point, no threads are RUNNABLE, None // are waiting for a beat, and none are in TimedWaiting. // If this persists for MaxDeadlockDetectionsBeforeDeadlock, // go ahead and abort. detectDeadlock() } Thread sleep clockPeriod } } /** * Threads have been running too long (timeout) if * The number of seconds since the last progress are more * than the allowed maximum run time. */ private def runningTooLong = System.currentTimeMillis - lastProgress > 1000L * maxRunTime /** * Stop the test due to a timeout. */ private def timeout() { val errorMessage = Resources("testTimedOutDEPRECATED", maxRunTime.toString) // The mainThread is likely joined to some test thread, so wake it up. It will look and // notice that the firstExceptionThrown is no longer empty, and will stop all live test threads, // then rethrow the rirst exception thrown. firstExceptionThrown offer new RuntimeException(errorMessage) mainThread.interrupt() } /** * Determine if there is a deadlock and if so, stop the test. */ private def detectDeadlock() { // Should never get to >= before ==, but just playing it safe if (deadlockCount >= MaxDeadlockDetectionsBeforeDeadlock) { // val errorMessage = "Apparent Deadlock! Threads waiting 50 clock periods (" + (clockPeriod * 50) + "ms)" val errorMessage = Resources("suspectedDeadlockDEPRECATED", MaxDeadlockDetectionsBeforeDeadlock.toString, (clockPeriod * MaxDeadlockDetectionsBeforeDeadlock).toString) firstExceptionThrown offer new RuntimeException(errorMessage) // The mainThread is likely joined to some test thread, so wake it up. It will look and // notice that the firstExceptionThrown is no longer empty, and will stop all live test threads, // then rethrow the rirst exception thrown. mainThread.interrupt() } else deadlockCount += 1 } } /** * Base class for the possible states of the Conductor. */ private sealed class ConductorState(val testWasStarted: Boolean, val testIsFinished: Boolean) /** * The initial state of the Conductor. * Any calls the thread{ ... } will result in started Threads that quickly block waiting for the * main thread to give it the green light. * Any call to conduct will start the test. */ private case object Setup extends ConductorState(false, false) /** * The state of the Conductor while its running. * Any calls the thread{ ... } will result in running Threads. * Any further call to conduct will result in an exception. */ private case object TestStarted extends ConductorState(true, false) /** * The state of the Conductor after all threads have finished, * and the whenFinished method has completed. * Any calls the thread{ ... } will result in an exception * Any call to conduct will result in an exception. */ private case object TestFinished extends ConductorState(true, true) }- Block a thread until the tick has reached a particular time.
*- Report the current time
*- Run operations with the clock frozen.
*