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

org.scalatest.Status.scala Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2001-2013 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

import scala.collection.GenSet
import java.io.Serializable
import scala.concurrent.{ExecutionException, Future, Promise}
import scala.util.{Try, Success, Failure}

/**
 * The result status of running a test or a suite, which is used to support parallel and asynchronous execution of tests.
 *
 * 

* This trait is the result type of the "run" lifecycle methods of trait Suite: * run, runNestedSuites, runTests, and runTest. It can be used to determine whether * a test or suite has completed, and if so, whether it succeeded, and if not, whether an exception was thrown that was * not yet reported via a ScalaTest event. A Status is like a domain-specific Future[Boolean], where: *

* *
    *
  • an activity in which no test failed and no suite aborted is represented by Success(true)
  • *
  • an activity during which at least one test failed or one suite aborted, but all exceptions that occured * were reported by a ScalaTest events (such as TestFailedException) * is represented by Success(false)
  • *
  • an activity during which at least one test failed or one suite aborted and at least one exception occurred that was * not reported via a ScalaTest event is represented by Failure(unreportedException)
  • *
* *

* Note that pending and canceled tests will not cause a Status to fail. Only failed tests * and aborted suites will cause a Status to fail. *

* *

* One use case of Status is to ensure that "after" code (such as an afterEach or afterAll method) * does not execute until after the relevant entity (one test, one suite, or all of a suite's tests or nested suites) has completed. * Another use case is to implement the default behavior of asynchronous styles, in which subsequent each test does not begin * execution until after the previous test has completed. *

*/ sealed trait Status { thisStatus => // SKIP-SCALATESTJS,NATIVE-START /** * Blocking call that waits until the entity represented by this Status (one test, one suite, or all of * a suite's tests or nested suites) has completed, then returns true if no tests failed and no * suites aborted, else returns false, or if an unreported exception has been installed, completes * abruptly with that exception. * *

* This method only reports false if there was a failed test or aborted suite in the context of the "run" lifecycle method * from which it was returned. * For example, if you call succeeds on a Status returned by runTest, succeeds * will (after that test has completed) return false if the test whose name was passed to runTest fails, * else it will return true. * In other words, so long as the test doesn't fail —whether the test succeeds, is canceled, or is pending—succeeds * will return true. * If you call succeeds on a Status returned by runTests, by contrast, succeeds * will (after the suite's * tests have completed) return true only if none of the tests in the suite fail. If any test in the suite fails, * succeeds will return false. * If you call succeeds on a Status returned by runNestedSuites, succeeds will * return true only if no tests fail and no suites abort when running all nested suites (and their nested suites, recursively). * Similarly, if you call succeeds on a Status returned by run, succeeds will * return true only if no tests fail and no suites abort when running all tests nested suites (and their nested suites, recursively). *

* *

* If this Status fails with an "unreported exception," an exception that occurred during the * activity represented by this Status that was not reported to the Reporter via a * ScalaTest event, the succeeds method will complete abruptly with that exception. If the * original exception was a run-aborting exception, such as StackOverflowError, the * unreportedException method will return a java.util.ExecutionException that contains * the original run-aborting exception as its cause. The succeeds method will in that case * complete abruptly with the ExecutionException that wraps the original run-aborting exception. *

* *

* Note: because blocking is not possible on Scala.js, this method is not available on Scala.js. *

* * @return after waiting until completion, true if no tests failed and no suites aborted, false otherwise * @throws unreportedException if an exception occurred during the activity represented by this Status that was not reported * via a ScalaTest event and therefore was installed as an unreported exception on this Status. */ def succeeds(): Boolean // SKIP-SCALATESTJS,NATIVE-END /** * Non-blocking call that indicates whether the entity represented by this * Status (one test, one suite, or all of a suite's tests or nested suites) has completed. Because this is non-blocking, * you can use this to poll the completion status. * *

* Note: this method will not indicate whether a test has failed, suite has aborted, or an unreported exception has been installed. * It just indicates whether the Status has completed or not by returning true or false. *

* * @return true if the test or suite run is already completed, false otherwise. */ def isCompleted(): Boolean // SKIP-SCALATESTJS,NATIVE-START /** * Blocking call that waits until the entity represented by this Status (one test, one suite, or all of * a suite's tests or nested suites) has completed, then either returns normally, or if an unreported exception has * been installed, completes abruptly with that unreported exception. * *

* If this Status fails with an "unreported exception," an exception that occurred during the * activity represented by this Status that was not reported to the Reporter via a * ScalaTest event, the waitUntilCompleted method will complete abruptly with that exception. If the * original exception was a run-aborting exception, such as StackOverflowError, the * unreportedException method will return a java.util.ExecutionException that contains * the original run-aborting exception as its cause. The waitUntilCompleted method will in that case * complete abruptly with the ExecutionException that wraps the original run-aborting exception. *

* *

* Note: because blocking is not possible on Scala.js, this method is not available on Scala.js. *

* * @throws unreportedException if an exception occurred during the activity represented by this Status that was not reported * via a ScalaTest event and therefore was installed as an unreported exception on this Status. */ def waitUntilCompleted(): Unit // SKIP-SCALATESTJS,NATIVE-END /** * Registers the passed callback function to be executed when this status completes. * *

* If an unreported exception has been installed on this Status, the * Try passed into the callback function will be a Failure containing that exception. Otherwise * the Try will be a Success containing true if no tests failed * or suites aborted during the activity represented by this Status, else false. *

* *

* The callback functions registered with whenCompleted will be executed after the Status * has completed, in an undefined order. If the Status has already completed, functions passed to this method will be * executed immediately by the calling thread before returning. *

* *

* Any exception thrown by a callback function will be propagated back on the thread used to invoke the callback. *

* *

* Internally ScalaTest uses this method to register callbacks that * fire completion events (TestSucceeded, TestFailed, * SuiteCompleted, etc.) to the Reporter. *

* * @param callback the callback function to execute once this Status has completed */ def whenCompleted(callback: Try[Boolean] => Unit): Unit // TODO: We are not yet propagating installed unreported exceptions in thenRun. Write the tests and implement the code. /** * Registers a Status-producing by-name function to execute after this * Status completes, returning a Status that mirrors the Status * returned by the by-name. * *

* The Status returned by this method will complete when the status produced by the * Status produced by the passed-by name completes. The returned Status * will complete with the same succeeds and unreportedException values. * But unlike the Status produced by the by-name, the returned Status will * be available immediately. *

* *

* If the by-name function passed to this method completes abruptly with a non-run-aborting exception, * that exception will be caught and installed as the unreportedException on the * Status returned by this method. The Status returned by this method * will then complete. The thread that attempted to evaluate the by-name function will be allowed * to continue (i.e., the non-run-aborting exception will not be rethrown * on that thread). *

* *

* If the by-name function passed to this method completes abruptly with a run-aborting exception, * such as StackOverflowError, that exception will be caught and a new * java.util.concurrent.ExecutionException that contains the run-aborting exception as its * cause will be installed as the unreportedException on the * Status returned by this method. The Status returned by this method * will then complete. The original run-aborting exception will then be rethrown on the * thread that attempted to evaluate the by-name function. *

* *

* If an unreported exception is installed on this Status, the passed by-name function will * still be executed. *

* *

* Internally, ScalaTest uses this method in async styles to ensure that by default, each subsequent test in an async-style * suite begins execution only after the previous test has completed. This method is not used if * ParallelTestExection is mixed into an async style. Instead, tests are allowed to begin * execution concurrently. *

* * @param status A Status-producing by-name function to invoke after this Status has completed. * @return a Status that represents the status of executing the by-name function passed to this method. */ final def thenRun(f: => Status): Status = { val returnedStatus = new ScalaTestStatefulStatus // Ignore the completion status of this Status, because regardless of whether it // represents a failure, we want to for sure run the next Status. For example, if // some test fails, we still want to run the next test. whenCompleted { _ => try { val innerStatus = f innerStatus.whenCompleted { tri => tri match { case Success(false) => returnedStatus.setFailed() case Failure(ex) => returnedStatus.setFailed() returnedStatus.setFailedWith(ex) case _ => } returnedStatus.setCompleted() } } catch { case ex: Throwable => if (Suite.anExceptionThatShouldCauseAnAbort(ex)) { returnedStatus.setFailedWith(new ExecutionException(ex)) returnedStatus.setCompleted() throw ex } else { returnedStatus.setFailedWith(ex) returnedStatus.setCompleted() } } } returnedStatus } /** * Converts this Status to a Future[Boolean] where Success(true) means * no tests failed and suites aborted, Success(false), means at least one test failed or one * suite aborted and any thrown exception was was reported to the Reporter via a ScalaTest * event, Failure(unreportedException) means * an exception, unreportedException, was thrown that was not reported to the Reporter * via a ScalaTest event. * * @return a Future[Boolean] representing this Status. */ final def toFuture: Future[Boolean] = { val promise = Promise[Boolean] whenCompleted { t => promise.complete(t) } promise.future } // TODO: Make sure to test what happens when before and after code throw exceptions. /** * An exception that was thrown during the activity represented by this Status that * was not reported via a ScalaTest event fired to the Reporter. * *

* When a test executes, "non-run-aborting" thrown exceptions are reported by events * fired to the reporter. A TestPendingException is reported via a * TestPending event. A TestCanceledException is reported via a * TestCanceled event. Any other non-run-aborting exceptions, including * TestFailedException will be reported via a * TestFailed event. *

* *

* Run-aborting exceptions indicate critical * problems, such as OutOfMemoryError, that instead of being reported via a test completion event * should instead cause the entire suite to abort. In synchronous testing styles, this exception will be allowed * to just propagate up the call stack. But in async styles, the thread or threads executing the test will often * be taken from the async suite's execution context. Instead of propagating these run-aborting exceptions up * the call stack, they will be installed as an "unreported exception" in the test's Status. * They are "unreported" because no test completion event will be fired to report them. For more explanation and * a list of run-aborting exception types, see Treatment of java.lang.Errors. *

* *

* Another way for an unreported exception to occur is if an exception of any type is thrown outside of the * body of an actual test. For example, traits BeforeAndAfter, BeforeAndAfterEach, * and BeforeAndAfterEachTestData execute code before and after tests. Traits * BeforeAndAfterAll and

BeforeAndAfterAllConfigMap execute code before * and after all tests and nested suites of a suite. If any "before" or "after" * code completes abruptly with an exception (of any type, not just run-aborting types) on a thread taken * from an async suite's execution context, this exception will * installed as an unreportedException of the relevant Status. *

* *

* In addition, ScalaTest Suite exposes four "run" lifecycle methods--run, * runNestedSuites, runTests, and runTest--that users can override to customize * the framework. If a "run" lifecycle methods completes abruptly with an exception, that exception occurs outside * the context of a test body. As a result, such exceptions will be * installed as an unreportedException of the relevant Status. *

* *

* The toFuture method on Status returns a Future[Boolean]. If the Future * succeeds with the Boolean value of true, that indicates no tests failed and no suites aborted * during the activity represented * by this Status. If a test failed or suite aborted, and that event was reported by a fired ScalaTest * Event, the * Future will succeed with the value false. If an unreported exception has been installed * on the Status, however, the Future will fail with that exception. *

* * @return a optional unreported Throwable */ def unreportedException: Option[Throwable] = None /** * Registers a by-name function (producing an optional exception) to execute * after this Status completes. * *

* If the by-name function passed to this method completes abruptly with a non-run-aborting exception, * that exception will be caught and installed as the unreportedException on the * Status returned by this method. The Status returned by this method * will then complete. The thread that attempted to evaluate the by-name function will be allowed * to continue (i.e., the non-run-aborting exception will not be rethrown * on that thread). *

* *

* If the by-name function passed to this method completes abruptly with a run-aborting exception, * such as StackOverflowError, that exception will be caught and a new * java.util.concurrent.ExecutionException that contains the run-aborting exception as its * cause will be installed as the unreportedException on the * Status returned by this method. The Status returned by this method * will then complete. The original run-aborting exception will then be rethrown on the * thread that attempted to evaluate the by-name function. *

* *

* If an unreported exception is installed on this Status, the passed by-name function will * not be executed. Instead, the same unreported exception will be installed on the Status * returned by this method. *

* *

* Internally, ScalaTest uses this method in traits BeforeAndAfter, * BeforeAndAfterEach, and BeforeAndAfterEachTestData to ensure "after" code is * executed after the relevant test has completed, and in traits BeforeAndAfterAll and * BeforeAndAfterAllConfigMap to ensure "after" code is executed after the * relevant tests and nested suites have completed. *

* * @param f A by-name function to invoke after this Status has completed. * @return a Status that represents this Status, * modified by any exception thrown by the passed by-name function. */ final def withAfterEffect(f: => Unit): Status = { val returnedStatus = new ScalaTestStatefulStatus whenCompleted { tri => tri match { case Success(result) => try { f if (!result) returnedStatus.setFailed() } catch { case ex: Throwable if Suite.anExceptionThatShouldCauseAnAbort(ex) => val execEx = new ExecutionException(ex) returnedStatus.setFailedWith(execEx) throw ex case ex: Throwable => returnedStatus.setFailedWith(ex) } finally { returnedStatus.setCompleted() } case Failure(originalEx) => try { f returnedStatus.setFailedWith(originalEx) } catch { case ex: Throwable => returnedStatus.setFailedWith(originalEx) println("ScalaTest can't report this exception because another preceded it, so printing its stack trace:") ex.printStackTrace() } finally { returnedStatus.setCompleted() } } } returnedStatus } } private[scalatest] object Status { def executeQueue(queue: ConcurrentLinkedQueue[Try[Boolean] => Unit], result: Try[Boolean]): Unit = { while (!queue.isEmpty) { val f = queue.poll if (f != null) f(result) } } } /** * Singleton status that represents an already completed run with no tests failed and no suites aborted. * *

* Note: the difference between this SucceededStatus object and the similarly named Succeeded * object is that the Succeeded object indicates one test succeeded, whereas this SucceededStatus object indicates the absence * of any failed tests or aborted suites during a run. Both are used as the result type of Suite lifecycle methods, but Succeeded * is a possible result of withFixture, whereas SucceededStatus is a possible result of run, runNestedSuites, * runTests, or runTest. In short, Succeeded is always just about one test, whereas SucceededStatus could be * about something larger: multiple tests or an entire suite. *

*/ object SucceededStatus extends Status with Serializable { // SKIP-SCALATESTJS,NATIVE-START /** * Always returns true. * * @return true */ def succeeds() = true // SKIP-SCALATESTJS,NATIVE-END /** * Always returns true. * * @return true */ def isCompleted() = true // SKIP-SCALATESTJS,NATIVE-START /** * Always returns immediately. */ def waitUntilCompleted(): Unit = {} // SKIP-SCALATESTJS,NATIVE-END /** * Executes the passed function immediately on the calling thread. */ def whenCompleted(f: Try[Boolean] => Unit): Unit = { f(Success(true)) } } /** * Singleton status that represents an already completed run with at least one failed test or aborted suite. * *

* Note: the difference between this FailedStatus object and the similarly named Failed * class is that a Failed instance indicates one test failed, whereas this FailedStatus object indicates either one or more tests failed * and/or one or more suites aborted during a run. Both are used as the result type of Suite lifecycle methods, but Failed * is a possible result of withFixture, whereas FailedStatus is a possible result of run, runNestedSuites, * runTests, or runTest. In short, Failed is always just about one test, whereas FailedStatus could be * about something larger: multiple tests or an entire suite. *

*/ object FailedStatus extends Status with Serializable { // SKIP-SCALATESTJS,NATIVE-START /** * Always returns false. * * @return true */ def succeeds() = false // SKIP-SCALATESTJS,NATIVE-END /** * Always returns true. * * @return true */ def isCompleted() = true // SKIP-SCALATESTJS,NATIVE-START /** * Always returns immediately. */ def waitUntilCompleted(): Unit = {} // SKIP-SCALATESTJS,NATIVE-END /** * Executes the passed function immediately on the calling thread. */ def whenCompleted(f: Try[Boolean] => Unit): Unit = { f(Success(false)) } } // Used internally in ScalaTest. We don't use the StatefulStatus, because // then user code could pattern match on it and then access the setCompleted // and setFailed methods. We wouldn't want that. private[scalatest] final class ScalaTestStatefulStatus extends Status with Serializable { // Safely published @transient private final val latch = new CountDownLatch(1) // protected by synchronized blocks private var succeeded = true // Safely published private final val queue = new ConcurrentLinkedQueue[Try[Boolean] => Unit] // protected by synchronized blocks private var asyncException: Option[Throwable] = None // Safely published private final val setCompletedMutex: AnyRef = new AnyRef with Serializable override def unreportedException: Option[Throwable] = { synchronized { asyncException } } // SKIP-SCALATESTJS,NATIVE-START def succeeds() = { waitUntilCompleted() synchronized { succeeded } } // SKIP-SCALATESTJS,NATIVE-END def isCompleted() = latch.getCount == 0L // SKIP-SCALATESTJS,NATIVE-START def waitUntilCompleted(): Unit = { latch.await() unreportedException match { case Some(ue) => throw ue case None => // Do nothing } } // SKIP-SCALATESTJS,NATIVE-END def setFailed(): Unit = { synchronized { if (isCompleted()) throw new IllegalStateException("status is already completed") succeeded = false } } /** * Sets the status to failed with an unreported exception, without changing the completion status. * *

* This method may be invoked repeatedly, even though invoking it once is sufficient to set the state of the Status to failed, but only * up until setCompleted has been called. Once setCompleted has been called, invoking this method will result in a * thrown IllegalStateException. Also, only the first exception passed will be reported as the unreported exception. Any exceptions * passed via subsequent invocations of setFailedWith after the first will have their stack traces printed to standard output. *

* * @throws IllegalStateException if this method is invoked on this instance after setCompleted has been invoked on this instance. * @param ex an unreported exception */ def setFailedWith(ex: Throwable): Unit = { synchronized { if (isCompleted()) throw new IllegalStateException("status is already completed") succeeded = false if (asyncException.isEmpty) asyncException = Some(ex) else { println("ScalaTest can't report this exception because another preceded it, so printing its stack trace:") ex.printStackTrace() } } } def setCompleted(): Unit = { val tri: Try[Boolean] = unreportedException match { case Some(ex) => Failure(ex) case None => Success(succeeded) } // If setCompleted is called twice by two different threads concurrently, ensure // one will execute all the callbacks in order before letting the second one proceed // rather than allowing both to race through the queue concurrently. setCompletedMutex.synchronized { Status.executeQueue(queue, tri) // Synchronize this latch count down so that it is serialized with the code // in whenCompleted that checks the status of the latch by calling isCompleted, // then either adds to the queue or sets executeLocally to true: // // val executeLocally = // synchronized { // if (!isCompleted()) { // queue.add(f) // false // } // else true // } // // This way either one or the other will go first. If this method goes first, then isCompleted will return false, // and the callback will be executed locally. If the whenCompleted method goes first, then isCompleted will // return false and the callback will be added to the queue. Once whenCompleted executes past the end of its // synchronized block, this method will count down the latch, and the next line will execute the callback // that was added. This way no callbacks should ever be lost. synchronized { latch.countDown() } // Execute any callbacks that were registered by whenCompleted after executeQueue above finishes, but // before the latch was counted down. Status.executeQueue(queue, tri) } } def whenCompleted(f: Try[Boolean] => Unit): Unit = { // This synchronized block serializes this method's check of the latch via // the isCompleted method, followed by a potential queue insertion, with // the count down of the latch in the setCompleted method. This ensures no // callback is lost. It will either be executed later by the setCompleted method // or now by this method. val executeLocally = synchronized { if (!isCompleted()) { queue.add(f) false } else true } if (executeLocally) { val tri: Try[Boolean] = unreportedException match { case Some(ex) => Failure(ex) case None => Success(succeeded) } f(tri) } } } /** * Status implementation that can change its state over time. * *

* A StatefulStatus begins its life in a successful state, and will remain successful unless setFailed is called. * Once setFailed is called, the status will remain at failed. The setFailed method can be called multiple times (even * though invoking it once is sufficient to permanently set the status to failed), but only up until setCompleted has been called. * After setCompleted has been called, any invocation of setFailed will be greeted with an IllegalStateException. *

* *

* Instances of this class are thread safe. *

*/ final class StatefulStatus extends Status with Serializable { // Safely published @transient private final val latch = new CountDownLatch(1) // protected by synchronized blocks private var succeeded = true // Safely published private final val queue = new ConcurrentLinkedQueue[Try[Boolean] => Unit] // protected by synchronized blocks private var asyncException: Option[Throwable] = None // Safely published private final val setCompletedMutex: AnyRef = new AnyRef with Serializable override def unreportedException: Option[Throwable] = { synchronized { asyncException } } // SKIP-SCALATESTJS,NATIVE-START /** * Blocking call that waits until completion, as indicated by an invocation of setCompleted on this instance, then returns false * if setFailed was called on this instance, else returns true. * * @return true if no tests failed and no suites aborted, false otherwise */ def succeeds() = { waitUntilCompleted() synchronized { succeeded } } // SKIP-SCALATESTJS,NATIVE-END /** * Non-blocking call that returns true if setCompleted has been invoked on this instance, false otherwise. * * @return true if the test or suite run is already completed, false otherwise. */ def isCompleted() = latch.getCount == 0L // SKIP-SCALATESTJS,NATIVE-START /** * Blocking call that returns only after setCompleted has been invoked on this StatefulStatus instance. */ def waitUntilCompleted(): Unit = { latch.await() unreportedException match { case Some(ue) => throw ue case None => // Do nothing } } // SKIP-SCALATESTJS,NATIVE-END /** * Sets the status to failed without changing the completion status. * *

* This method may be invoked repeatedly, even though invoking it once is sufficient to set the state of the Status to failed, but only * up until setCompleted has been called. Once setCompleted has been called, invoking this method will result in a * thrown IllegalStateException. *

* * @throws IllegalStateException if this method is invoked on this instance after setCompleted has been invoked on this instance. */ def setFailed(): Unit = { synchronized { if (isCompleted()) throw new IllegalStateException("status is already completed") succeeded = false } } /** * Sets the status to failed with an unreported exception, without changing the completion status. * *

* This method may be invoked repeatedly, even though invoking it once is sufficient to set the state of the Status to failed, but only * up until setCompleted has been called. Once setCompleted has been called, invoking this method will result in a * thrown IllegalStateException. Also, only the first exception passed will be reported as the unreported exception. Any exceptions * passed via subsequent invocations of setFailedWith after the first will have their stack traces printed to standard output. *

* * @throws IllegalStateException if this method is invoked on this instance after setCompleted has been invoked on this instance. * @param ex an unreported exception */ def setFailedWith(ex: Throwable): Unit = { synchronized { if (isCompleted()) throw new IllegalStateException("status is already completed") succeeded = false if (asyncException.isEmpty) asyncException = Some(ex) else { println("ScalaTest can't report this exception because another preceded it, so printing its stack trace:") ex.printStackTrace() } } } /** * Sets the status to completed. * *

* This method may be invoked repeatedly, even though invoking it once is sufficient to set the state of the Status to completed. *

* *

* This method invokes any callbacks that have been registered with whenCompleted using the thread that invoked * this method prior to declaring this status as completed. This method then executes any callbacks that were registered between * thie time this method decided it was done executing previously registered callbacks and the time it declared this status as * completed. This second pass ensures no callbacks are lost. Any subsequent callbacks registered with whenCompleted * will be executed using the thread that invoked whenCompleted. *

*/ def setCompleted(): Unit = { val tri: Try[Boolean] = unreportedException match { case Some(ex) => Failure(ex) case None => Success(succeeded) } // If setCompleted is called twice by two different threads concurrently, ensure // one will execute all the callbacks in order before letting the second one proceed // rather than allowing both to race through the queue concurrently. setCompletedMutex.synchronized { Status.executeQueue(queue, tri) // Synchronize this latch count down so that it is serialized with the code // in whenCompleted that checks the status of the latch by calling isCompleted, // then either adds to the queue or sets executeLocally to true: // // val executeLocally = // synchronized { // if (!isCompleted()) { // queue.add(f) // false // } // else true // } // // This way either one or the other will go first. If this method goes first, then isCompleted will return false, // and the callback will be executed locally. If the whenCompleted method goes first, then isCompleted will // return false and the callback will be added to the queue. Once whenCompleted executes past the end of its // synchronized block, this method will count down the latch, and the next line will execute the callback // that was added. This way no callbacks should ever be lost. synchronized { latch.countDown() } // Execute any callbacks that were registered by whenCompleted after executeQueue above finishes, but // before the latch was counted down. Status.executeQueue(queue, tri) } } /** * Registers the passed function to be executed when this status completes. * *

* You may register multiple functions, which on completion will be executed in an undefined * order. *

*/ def whenCompleted(f: Try[Boolean] => Unit): Unit = { // This synchronized block serializes this method's check of the latch via // the isCompleted method, followed by a potential queue insertion, with // the count down of the latch in the setCompleted method. This ensures no // callback is lost. It will either be executed later by the setCompleted method // or now by this method. val executeLocally = synchronized { if (!isCompleted()) { queue.add(f) false } else true } if (executeLocally) { val tri: Try[Boolean] = unreportedException match { case Some(ex) => Failure(ex) case None => Success(succeeded) } f(tri) } } } import java.util.concurrent.atomic.AtomicReference /** * Composite Status that aggregates its completion and failed states of set of other Statuses passed to its constructor. * * @param statuses the Statuses out of which this status is composed. */ final class CompositeStatus(statuses: Set[Status]) extends Status with Serializable { import CompositeStatus._ // Before you can use the AtomicReference or ConcurrentLinkedQueue, you must acquire // the lock on this mutex. You need not acquire it to use the CountDownLatch or the // statuses passed to the constructor. private final val mutex: AnyRef = new AnyRef // You must acquire the mutex to use this queue. private final val queue: ConcurrentLinkedQueue[Try[Boolean] => Unit] = new ConcurrentLinkedQueue[Try[Boolean] => Unit] // When this latch reaches zero, that means this Status has completed. You need // not acquire the mutex to look at the latch. private final val latch: CountDownLatch = new CountDownLatch(statuses.size) // You must acquire the mutex to use this atomic reference. // The asyncException contained in this bundle is set possibly by the whenCompleted function // registered on all the inner statuses. If any of them are Failures, then the first one to // fail goes in as this Composite's unreported exception. Any subsequent exceptions are just printed // to the standard output. Then if it is the last inner status to complete, that unreported // exception is passed to the callback functions registered with this composite status. private final val bundleRef: AtomicReference[Bundle] = new AtomicReference( Bundle( succeeded = true, asyncException = None ) ) // The this reference cannot escape during construction, because the this reference is not passed // to the initializeBundle method. intializeBundle(statuses, mutex, queue, latch, bundleRef) // SKIP-SCALATESTJS,NATIVE-START /** * Blocking call that waits until all composite Statuses have completed, then returns * true only if all of the composite Statuses succeeded. If any Status * passed in the statuses set fails, this method will return false. * * @return true if all composite Statuses succeed, false otherwise. */ def succeeds(): Boolean = { latch.await() statuses.forall(_.succeeds()) } // SKIP-SCALATESTJS,NATIVE-END /** * Non-blocking call to check if the test or suite run is completed, returns true if all composite Statuses have completed, * false otherwise. You can use this to poll the run status. * * @return true if all composite Statuses have completed, false otherwise. */ def isCompleted(): Boolean = latch.getCount == 0 // Note, the previous implementation did this, grabbing statuses within a synchronized block, which makes no sense: // statuses.forall(_.isCompleted()) // I think that was a fuzzy design if not a buggy one. In this implementation, the latch is the ultimate arbiter // of whether the status has completed. // SKIP-SCALATESTJS,NATIVE-START /** * Blocking call that returns only after all composite Statuss have completed. */ def waitUntilCompleted(): Unit = latch.await() // SKIP-SCALATESTJS,NATIVE-END /** * Registers the passed function to be executed when this status completes. * *

* You may register multiple functions, which on completion will be executed in an undefined * order. *

*/ def whenCompleted(f: Try[Boolean] => Unit): Unit = { val executeLocally = mutex.synchronized { // Check the latch (by calling isCompleted) inside a mutex.synchronized to prevent // a race condition between this decision to either 1) add the callback, f, to the queue // or 2) execute it locally, with the execution of the final callback registered with the // nested statuses that will execute the callbacks in the queue. if (!isCompleted()) { queue.add(f) false } else true } if (executeLocally) { // Once this status is completed, the asyncException in the Bundle, if any, is the exception // to be passed to the callback f. Note: StatefulStatus (and ScalaTestStatefulStatus) can // fail without an exception via the setFailed method that takes no parameters. This supports // tests that do not use exceptions to determine success or failure. val bundle = bundleRef.get val tri: Try[Boolean] = bundle.asyncException match { case Some(ex) => Failure(ex) case None => Success(bundle.succeeded) } f(tri) } } /** * An optional exception that has not been reported to the reporter for this run. * *

* This will be defined if any of the composite Statuss (passed to this Status's * constructor) has a defined unreportedException. If more than one composite Status * has a defined unreportedException, one of them (not specified) will be reported by this method * and the others will have their stack traces printed to standard output. *

*/ override def unreportedException: Option[Throwable] = { mutex.synchronized { val bundle = bundleRef.get if (bundle.asyncException.isDefined) bundle.asyncException else { val optStatusWithUnrepEx = statuses.find(_.unreportedException.isDefined) for { status <- optStatusWithUnrepEx unrepEx <- status.unreportedException } yield unrepEx } } } } private[scalatest] object CompositeStatus { case class Bundle( succeeded: Boolean, asyncException: Option[Throwable] ) // The asyncException is set possibly by the whenCompleted function registered on all the // inner statuses. If any of them are Failures, then that first one goes in // as this Composite's unreported exception. Any subsequent ones are just printed. // Then if it is the last inner status to complete, that unreported exception is passed // to the callback functions registered with this composite status. def intializeBundle( statuses: Set[Status], mutex: AnyRef, queue: ConcurrentLinkedQueue[Try[Boolean] => Unit], latch: CountDownLatch, bundleRef: AtomicReference[Bundle] ): Unit = { for (status <- statuses) { status.whenCompleted { tri => mutex.synchronized { val bundle = bundleRef.get val (newSucceeded, newAsyncException) = tri match { case Success(res) => // Set succeeded to false if it either is already false or res is false. // Given this tri is a Success, there's no new exception to deal with, so // just keep the existing Option[Throwable] in place as the asyncException. (bundle.succeeded && res, bundle.asyncException) case Failure(ex) => // Given it is a failure, set succeeded to false (it may already be false, but // it doesn't matter. if (bundle.asyncException.isEmpty) // Given there's no asyncException currently in the bundle, add it. (false, Some(ex)) else { // Given there's already an asyncException in the bundle, just print this one // and keep the "winner" exception nit he bundle. println("ScalaTest can't report this exception because another preceded it, so printing its stack trace:") ex.printStackTrace() (false, bundle.asyncException) } } bundleRef.set(Bundle(newSucceeded, newAsyncException)) if (latch.getCount == 1) { // Because the latch is at 1, once we count it down the status will be completed. // First execute the queue. We're doing this in the mutex.synchronized to mostly // serialize async activities. This status will not be observed to complete until // any callbacks in the queue are drained. val completionTri: Try[Boolean] = newAsyncException match { case Some(ex) => Failure(ex) case None => Success(newSucceeded) } import Status.executeQueue // This will execute all callbacks currently in the queue. Because this is done in // the mutex.synchronized, no others can be added until after we countdown the // latch to zero. If a thread is currently blocked waiting to acquire the mutex // in the whenCompleted method, it will observe the status as completed and // therefore execute that callback locally. executeQueue(queue, completionTri) } // Once this latch counts down to zero, other threads will see it because we don't synchronize // on the mutex to look at the latch in CompositeStatus's methods. latch.countDown() } } } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy