All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.scalatest.concurrent.SerialExecutionContext.scala Maven / Gradle / Ivy

/*
 * Copyright 2001-2015 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 scala.annotation.tailrec
import scala.concurrent.{ExecutionContext, Future}
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
import org.scalatest.Outcome

/*
This is private[scalatest] so that no one can actually use it except
when we install it as the default. It can't be used generally because
we special case it in the AsyncEngine. Whatever is referenced from
executionContext is what is passed into AsyncEngine, and then we
do a runtime type check to see if it is this one, and if so, we 
call runNow.
*/
private[scalatest] class SerialExecutionContext extends ExecutionContext {

  /*
  A LinkedBlockingQueue is thread-safe, so don't need to synchronize
  its access.
  */
  private final val queue = new org.scalatest.LinkedBlockingQueue[Runnable]

  def execute(runnable: Runnable): Unit = {
    queue.put(runnable)
    // If this is a thread different than the main test thread, we'll
    // notify the main test thread in case it is sitting in the wait
    // in runNow.
    // synchronized { notifyAll() }
  }

  def reportFailure(t: Throwable): Unit =
    t.printStackTrace()

  /*
     runNow will keep executing jobs passed to execute until the
     Future passed to runNow completes. If it runs out of jobs to
     do before the Future completes, it enters the wait set.
     Adding another job to the queue will notify it. We don't
     notify when the future completes, because the last thing an
     AsyncTestSuite currently does is that it transforms a completed
     Future[Assertion] returned from the test into a Future[Outcome].
     That transformation is done using the executionContext, which
     in this case is a SerialExecutionContext. This is always the
     final transformation needed to complete a test, after which
     the Future[Outcome] will be complete. It therefore is enqueued
     onto the LinkedBlockingQueue by calling execute. If from a
     thread other than the main test thread, this will do a notifyAll
     to wake up the main test thread if it is sitting in the wait
     loop in runNow:

     // After being notified, wait() will return
     synchronized { wait() }

     If so, it will loop back and find that as yet the Future[Outcome]
     is not completed, so it will execute the while body again:
  
      while (!future.isCompleted) {
        // It will execute here again
      }
 
     It will find out that the queue does actually contain a job to do:

     // queue.peek will return a job to do, not null
     while (queue.peek != null)

     It will execute this job, which is the transformation from
     Future[Assertion] to Future[Outcome]:

     // This will transform Future[Assertion] => Future[Outcome]
     queue.poll().run()

     That will complete the Future[Outcome], so when it tests to
     see if the Future[Outcome] is completed, it will find that it
     has:

     // This if test fails. !future.isCompleted is now false
     if (!future.isCompleted)

     Thus it will skip the wait() call and loop back to the top
     of the outer while. That will finally return false:
    
      while (!future.isCompleted) {
        // Doesn't get here
      } // Just hops down here because the future is now complete
 
      And runNow will return, as this test has finished running.
   */
  /*def runNow(future: Future[Outcome]): Unit = {
    while (!future.isCompleted || queue.size > 0)
      queue.take().run() // What to do about exceptions here?
  }*/


  def runNow(future: Future[Outcome]): Unit = recRunNow(future)

  @tailrec
  private def recRunNow(future: Future[Outcome]): Unit = // Could take a scala.concurrent.duration.Deadline to be used for poll(timeout, timeUnit)
    if (future.isCompleted && queue.size == 0) ()
    else {
      //SCALATESTJS,NATIVE-ONLY if (!future.isCompleted && queue.size == 0)
      //SCALATESTJS,NATIVE-ONLY throw new IllegalStateException("Queue is empty while future is not completed, this means you're probably using a wrong ExecutionContext for your task, please double check your Future.")
      val task = queue.take() // Note that this will block if queue is empty, alternatively we can use poll(timeout, timeUnit) to deal with deadlines
      task.run()  // TODO: this should abort the suite, let's write a test for that
      recRunNow(future)
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy