From a6e9cb132a4e00f963360f889baae89bb59977bc Mon Sep 17 00:00:00 2001 From: NthPortal Date: Wed, 15 Apr 2020 17:52:07 -0400 Subject: [PATCH 1/2] Backport scala.util.Using --- .../scala-2.11_2.12/scala/util/Using.scala | 403 ++++++++ .../src/test/scala/scala/util/UsingTest.scala | 873 ++++++++++++++++++ 2 files changed, 1276 insertions(+) create mode 100644 compat/src/main/scala-2.11_2.12/scala/util/Using.scala create mode 100644 compat/src/test/scala/scala/util/UsingTest.scala diff --git a/compat/src/main/scala-2.11_2.12/scala/util/Using.scala b/compat/src/main/scala-2.11_2.12/scala/util/Using.scala new file mode 100644 index 00000000..32798eb5 --- /dev/null +++ b/compat/src/main/scala-2.11_2.12/scala/util/Using.scala @@ -0,0 +1,403 @@ +/* + * Scala (https://www.scala-lang.org) + * + * Copyright EPFL and Lightbend, Inc. + * + * Licensed under Apache License 2.0 + * (http://www.apache.org/licenses/LICENSE-2.0). + * + * See the NOTICE file distributed with this work for + * additional information regarding copyright ownership. + */ + +package scala.util + +import scala.util.control.{ControlThrowable, NonFatal} + +/** A utility for performing automatic resource management. It can be used to perform an + * operation using resources, after which it releases the resources in reverse order + * of their creation. + * + * ==Usage== + * + * There are multiple ways to automatically manage resources with `Using`. If you only need + * to manage a single resource, the [[Using.apply `apply`]] method is easiest; it wraps the + * resource opening, operation, and resource releasing in a `Try`. + * + * Example: + * {{{ + * import java.io.{BufferedReader, FileReader} + * import scala.util.{Try, Using} + * + * val lines: Try[Seq[String]] = + * Using(new BufferedReader(new FileReader("file.txt"))) { reader => + * Iterator.continually(reader.readLine()).takeWhile(_ != null).toSeq + * } + * }}} + * + * If you need to manage multiple resources, [[Using.Manager$.apply `Using.Manager`]] should + * be used. It allows the managing of arbitrarily many resources, whose creation, use, and + * release are all wrapped in a `Try`. + * + * Example: + * {{{ + * import java.io.{BufferedReader, FileReader} + * import scala.util.{Try, Using} + * + * val lines: Try[Seq[String]] = Using.Manager { use => + * val r1 = use(new BufferedReader(new FileReader("file1.txt"))) + * val r2 = use(new BufferedReader(new FileReader("file2.txt"))) + * val r3 = use(new BufferedReader(new FileReader("file3.txt"))) + * val r4 = use(new BufferedReader(new FileReader("file4.txt"))) + * + * // use your resources here + * def lines(reader: BufferedReader): Iterator[String] = + * Iterator.continually(reader.readLine()).takeWhile(_ != null) + * + * (lines(r1) ++ lines(r2) ++ lines(r3) ++ lines(r4)).toList + * } + * }}} + * + * If you wish to avoid wrapping management and operations in a `Try`, you can use + * [[Using.resource `Using.resource`]], which throws any exceptions that occur. + * + * Example: + * {{{ + * import java.io.{BufferedReader, FileReader} + * import scala.util.Using + * + * val lines: Seq[String] = + * Using.resource(new BufferedReader(new FileReader("file.txt"))) { reader => + * Iterator.continually(reader.readLine()).takeWhile(_ != null).toSeq + * } + * }}} + * + * ==Suppression Behavior== + * + * If two exceptions are thrown (e.g., by an operation and closing a resource), + * one of them is re-thrown, and the other is + * [[java.lang.Throwable#addSuppressed added to it as a suppressed exception]]. + * If the two exceptions are of different 'severities' (see below), the one of a higher + * severity is re-thrown, and the one of a lower severity is added to it as a suppressed + * exception. If the two exceptions are of the same severity, the one thrown first is + * re-thrown, and the one thrown second is added to it as a suppressed exception. + * If an exception is a [[scala.util.control.ControlThrowable `ControlThrowable`]], or + * if it does not support suppression (see + * [[java.lang.Throwable `Throwable`'s constructor with an `enableSuppression` parameter]]), + * an exception that would have been suppressed is instead discarded. + * + * Exceptions are ranked from highest to lowest severity as follows: + * - `java.lang.VirtualMachineError` + * - `java.lang.LinkageError` + * - `java.lang.InterruptedException` and `java.lang.ThreadDeath` + * - [[scala.util.control.NonFatal fatal exceptions]], excluding `scala.util.control.ControlThrowable` + * - `scala.util.control.ControlThrowable` + * - all other exceptions + * + * When more than two exceptions are thrown, the first two are combined and + * re-thrown as described above, and each successive exception thrown is combined + * as it is thrown. + * + * @define suppressionBehavior See the main doc for [[Using `Using`]] for full details of + * suppression behavior. + */ +object Using { + + /** Performs an operation using a resource, and then releases the resource, + * even if the operation throws an exception. + * + * $suppressionBehavior + * + * @return a [[Try]] containing an exception if one or more were thrown, + * or the result of the operation if no exceptions were thrown + */ + def apply[R: Releasable, A](resource: => R)(f: R => A): Try[A] = Try { + Using.resource(resource)(f) + } + + /** A resource manager. + * + * Resources can be registered with the manager by calling [[acquire `acquire`]]; + * such resources will be released in reverse order of their acquisition + * when the manager is closed, regardless of any exceptions thrown + * during use. + * + * $suppressionBehavior + * + * @note It is recommended for API designers to require an implicit `Manager` + * for the creation of custom resources, and to call `acquire` during those + * resources' construction. Doing so guarantees that the resource ''must'' be + * automatically managed, and makes it impossible to forget to do so. + * + * + * Example: + * {{{ + * class SafeFileReader(file: File)(implicit manager: Using.Manager) + * extends BufferedReader(new FileReader(file)) { + * + * def this(fileName: String)(implicit manager: Using.Manager) = this(new File(fileName)) + * + * manager.acquire(this) + * } + * }}} + */ + final class Manager private { + import Manager._ + + private var closed = false + private[this] var resources: List[Resource[_]] = Nil + + /** Registers the specified resource with this manager, so that + * the resource is released when the manager is closed, and then + * returns the (unmodified) resource. + */ + def apply[R: Releasable](resource: R): R = { + acquire(resource) + resource + } + + /** Registers the specified resource with this manager, so that + * the resource is released when the manager is closed. + */ + def acquire[R: Releasable](resource: R): Unit = { + if (resource == null) throw new NullPointerException("null resource") + if (closed) throw new IllegalStateException("Manager has already been closed") + resources = new Resource(resource) :: resources + } + + private def manage[A](op: Manager => A): A = { + var toThrow: Throwable = null + try { + op(this) + } catch { + case t: Throwable => + toThrow = t + null.asInstanceOf[A] // compiler doesn't know `finally` will throw + } finally { + closed = true + var rs = resources + resources = null // allow GC, in case something is holding a reference to `this` + while (rs.nonEmpty) { + val resource = rs.head + rs = rs.tail + try resource.release() + catch { + case t: Throwable => + if (toThrow == null) toThrow = t + else toThrow = preferentiallySuppress(toThrow, t) + } + } + if (toThrow != null) throw toThrow + } + } + } + + object Manager { + + /** Performs an operation using a `Manager`, then closes the `Manager`, + * releasing its resources (in reverse order of acquisition). + * + * Example: + * {{{ + * val lines = Using.Manager { use => + * use(new BufferedReader(new FileReader("file.txt"))).lines() + * } + * }}} + * + * If using resources which require an implicit `Manager` as a parameter, + * this method should be invoked with an `implicit` modifier before the function + * parameter: + * + * Example: + * {{{ + * val lines = Using.Manager { implicit use => + * new SafeFileReader("file.txt").lines() + * } + * }}} + * + * See the main doc for [[Using `Using`]] for full details of suppression behavior. + * + * @param op the operation to perform using the manager + * @tparam A the return type of the operation + * @return a [[Try]] containing an exception if one or more were thrown, + * or the result of the operation if no exceptions were thrown + */ + def apply[A](op: Manager => A): Try[A] = Try { (new Manager).manage(op) } + + private final class Resource[R](resource: R)(implicit releasable: Releasable[R]) { + def release(): Unit = releasable.release(resource) + } + } + + private def preferentiallySuppress(primary: Throwable, secondary: Throwable): Throwable = { + def score(t: Throwable): Int = t match { + case _: VirtualMachineError => 4 + case _: LinkageError => 3 + case _: InterruptedException | _: ThreadDeath => 2 + case _: ControlThrowable => 0 + case e if !NonFatal(e) => 1 // in case this method gets out of sync with NonFatal + case _ => -1 + } + // special-case `ControlThrowable`, which incorrectly suppresses exceptions + // before 2.13 + @inline def suppress(t: Throwable, suppressed: Throwable): Throwable = { + if (!t.isInstanceOf[ControlThrowable]) t.addSuppressed(suppressed); t + } + + if (score(secondary) > score(primary)) suppress(secondary, primary) + else suppress(primary, secondary) + } + + /** Performs an operation using a resource, and then releases the resource, + * even if the operation throws an exception. This method behaves similarly + * to Java's try-with-resources. + * + * $suppressionBehavior + * + * @param resource the resource + * @param body the operation to perform with the resource + * @tparam R the type of the resource + * @tparam A the return type of the operation + * @return the result of the operation, if neither the operation nor + * releasing the resource throws + */ + def resource[R, A](resource: R)(body: R => A)(implicit releasable: Releasable[R]): A = { + if (resource == null) throw new NullPointerException("null resource") + + var toThrow: Throwable = null + try { + body(resource) + } catch { + case t: Throwable => + toThrow = t + null.asInstanceOf[A] // compiler doesn't know `finally` will throw + } finally { + if (toThrow eq null) releasable.release(resource) + else { + try releasable.release(resource) + catch { + case other: Throwable => toThrow = preferentiallySuppress(toThrow, other) + } finally throw toThrow + } + } + } + + /** Performs an operation using two resources, and then releases the resources + * in reverse order, even if the operation throws an exception. This method + * behaves similarly to Java's try-with-resources. + * + * $suppressionBehavior + * + * @param resource1 the first resource + * @param resource2 the second resource + * @param body the operation to perform using the resources + * @tparam R1 the type of the first resource + * @tparam R2 the type of the second resource + * @tparam A the return type of the operation + * @return the result of the operation, if neither the operation nor + * releasing the resources throws + */ + def resources[R1: Releasable, R2: Releasable, A]( + resource1: R1, + resource2: => R2 + )(body: (R1, R2) => A): A = + resource(resource1) { r1 => + resource(resource2) { r2 => + body(r1, r2) + } + } + + /** Performs an operation using three resources, and then releases the resources + * in reverse order, even if the operation throws an exception. This method + * behaves similarly to Java's try-with-resources. + * + * $suppressionBehavior + * + * @param resource1 the first resource + * @param resource2 the second resource + * @param resource3 the third resource + * @param body the operation to perform using the resources + * @tparam R1 the type of the first resource + * @tparam R2 the type of the second resource + * @tparam R3 the type of the third resource + * @tparam A the return type of the operation + * @return the result of the operation, if neither the operation nor + * releasing the resources throws + */ + def resources[R1: Releasable, R2: Releasable, R3: Releasable, A]( + resource1: R1, + resource2: => R2, + resource3: => R3 + )(body: (R1, R2, R3) => A): A = + resource(resource1) { r1 => + resource(resource2) { r2 => + resource(resource3) { r3 => + body(r1, r2, r3) + } + } + } + + /** Performs an operation using four resources, and then releases the resources + * in reverse order, even if the operation throws an exception. This method + * behaves similarly to Java's try-with-resources. + * + * $suppressionBehavior + * + * @param resource1 the first resource + * @param resource2 the second resource + * @param resource3 the third resource + * @param resource4 the fourth resource + * @param body the operation to perform using the resources + * @tparam R1 the type of the first resource + * @tparam R2 the type of the second resource + * @tparam R3 the type of the third resource + * @tparam R4 the type of the fourth resource + * @tparam A the return type of the operation + * @return the result of the operation, if neither the operation nor + * releasing the resources throws + */ + def resources[R1: Releasable, R2: Releasable, R3: Releasable, R4: Releasable, A]( + resource1: R1, + resource2: => R2, + resource3: => R3, + resource4: => R4 + )(body: (R1, R2, R3, R4) => A): A = + resource(resource1) { r1 => + resource(resource2) { r2 => + resource(resource3) { r3 => + resource(resource4) { r4 => + body(r1, r2, r3, r4) + } + } + } + } + + /** A typeclass describing how to release a particular type of resource. + * + * A resource is anything which needs to be released, closed, or otherwise cleaned up + * in some way after it is finished being used, and for which waiting for the object's + * garbage collection to be cleaned up would be unacceptable. For example, an instance of + * [[java.io.OutputStream]] would be considered a resource, because it is important to close + * the stream after it is finished being used. + * + * An instance of `Releasable` is needed in order to automatically manage a resource + * with [[Using `Using`]]. An implicit instance is provided for all types extending + * [[java.lang.AutoCloseable]]. + * + * @tparam R the type of the resource + */ + trait Releasable[-R] { + + /** Releases the specified resource. */ + def release(resource: R): Unit + } + + object Releasable { + + /** An implicit `Releasable` for [[java.lang.AutoCloseable `AutoCloseable`s]]. */ + implicit object AutoCloseableIsReleasable extends Releasable[AutoCloseable] { + def release(resource: AutoCloseable): Unit = resource.close() + } + } + +} diff --git a/compat/src/test/scala/scala/util/UsingTest.scala b/compat/src/test/scala/scala/util/UsingTest.scala new file mode 100644 index 00000000..a2f1173e --- /dev/null +++ b/compat/src/test/scala/scala/util/UsingTest.scala @@ -0,0 +1,873 @@ +package scala.util + +import org.junit.Test +import org.junit.Assert._ + +import scala.reflect.ClassTag +import scala.runtime.NonLocalReturnControl + +class UsingTest { + import UsingTest._ + + /* `Using.resource` exception preference */ + + private def genericResourceThrowing[CloseT <: Throwable: ClassTag]( + resource: => CustomResource[CloseT], + onLinkage: SuppressionBehavior, + onInterruption: SuppressionBehavior, + onControl: SuppressionBehavior, + onException: SuppressionBehavior + ): Unit = { + def check[UseT <: Throwable: ClassTag]( + t: String => UseT, + behavior: SuppressionBehavior, + allowsSuppression: Boolean + ): Unit = { + val ex = use(resource, t) + if (behavior == IsSuppressed) { + assertThrowableClass[UseT](ex) + if (allowsSuppression) assertSingleSuppressed[CloseT](ex) + else assertNoSuppressed(ex) + } else { + assertThrowableClass[CloseT](ex) + if (behavior == AcceptsSuppressed) assertSingleSuppressed[UseT](ex) + else assertNoSuppressed(ex) + } + } + + check(new UsingVMError(_), behavior = IsSuppressed, allowsSuppression = true) + check(new UsingLinkageError(_), onLinkage, allowsSuppression = true) + check(_ => new UsingInterruption, onInterruption, allowsSuppression = true) + check(new UsingControl(_), onControl, allowsSuppression = false) + check(new UsingError(_), onException, allowsSuppression = true) + check(new UsingException(_), onException, allowsSuppression = true) + } + + @Test + def resourceThrowingVMError(): Unit = { + genericResourceThrowing(new VMErrorResource, + onLinkage = AcceptsSuppressed, + onInterruption = AcceptsSuppressed, + onControl = AcceptsSuppressed, + onException = AcceptsSuppressed) + } + + @Test + def resourceThrowingLinkageError(): Unit = { + genericResourceThrowing(new LinkageResource, + onLinkage = IsSuppressed, + onInterruption = AcceptsSuppressed, + onControl = AcceptsSuppressed, + onException = AcceptsSuppressed) + } + + @Test + def resourceThrowingInterruption(): Unit = { + genericResourceThrowing(new InterruptionResource, + onLinkage = IsSuppressed, + onInterruption = IsSuppressed, + onControl = AcceptsSuppressed, + onException = AcceptsSuppressed) + } + + @Test + def resourceThrowingControl(): Unit = { + genericResourceThrowing(new ControlResource, + onLinkage = IsSuppressed, + onInterruption = IsSuppressed, + onControl = IsSuppressed, + onException = IgnoresSuppressed) + } + + @Test + def resourceThrowingError(): Unit = { + genericResourceThrowing(new ErrorResource, + onLinkage = IsSuppressed, + onInterruption = IsSuppressed, + onControl = IsSuppressed, + onException = IsSuppressed) + } + + @Test + def resourceThrowingException(): Unit = { + genericResourceThrowing(new ExceptionResource, + onLinkage = IsSuppressed, + onInterruption = IsSuppressed, + onControl = IsSuppressed, + onException = IsSuppressed) + } + + /* `Using.apply` exception preference */ + + private def genericUsingThrowing[CloseT <: Throwable: ClassTag]( + resource: => CustomResource[CloseT], + onLinkage: SuppressionBehavior, + onInterruption: SuppressionBehavior, + onControl: SuppressionBehavior, + onException: SuppressionBehavior + ): Unit = { + def check[UseT <: Throwable: ClassTag]( + t: String => UseT, + behavior: SuppressionBehavior, + allowsSuppression: Boolean, + yieldsTry: Boolean + ): Unit = { + val ex = if (yieldsTry) UseWrapped(resource, t) else UseWrapped.catching(resource, t) + if (behavior == IsSuppressed) { + assertThrowableClass[UseT](ex) + if (allowsSuppression) assertSingleSuppressed[CloseT](ex) + else assertNoSuppressed(ex) + } else { + assertThrowableClass[CloseT](ex) + if (behavior == AcceptsSuppressed) assertSingleSuppressed[UseT](ex) + else assertNoSuppressed(ex) + } + } + + check(new UsingVMError(_), behavior = IsSuppressed, allowsSuppression = true, yieldsTry = false) + check(new UsingLinkageError(_), onLinkage, allowsSuppression = true, yieldsTry = false) + check(_ => new UsingInterruption, onInterruption, allowsSuppression = true, yieldsTry = false) + check(new UsingControl(_), onControl, allowsSuppression = false, yieldsTry = false) + check(new UsingError(_), + onException, + allowsSuppression = true, + yieldsTry = onException == IsSuppressed) + check(new UsingException(_), + onException, + allowsSuppression = true, + yieldsTry = onException == IsSuppressed) + } + + @Test + def usingThrowingVMError(): Unit = { + genericUsingThrowing(new VMErrorResource, + onLinkage = AcceptsSuppressed, + onInterruption = AcceptsSuppressed, + onControl = AcceptsSuppressed, + onException = AcceptsSuppressed) + } + + @Test + def usingThrowingLinkageError(): Unit = { + genericUsingThrowing(new LinkageResource, + onLinkage = IsSuppressed, + onInterruption = AcceptsSuppressed, + onControl = AcceptsSuppressed, + onException = AcceptsSuppressed) + } + + @Test + def usingThrowingInterruption(): Unit = { + genericUsingThrowing(new InterruptionResource, + onLinkage = IsSuppressed, + onInterruption = IsSuppressed, + onControl = AcceptsSuppressed, + onException = AcceptsSuppressed) + } + + @Test + def usingThrowingControl(): Unit = { + genericUsingThrowing(new ControlResource, + onLinkage = IsSuppressed, + onInterruption = IsSuppressed, + onControl = IsSuppressed, + onException = IgnoresSuppressed) + } + + @Test + def usingThrowingError(): Unit = { + genericUsingThrowing(new ErrorResource, + onLinkage = IsSuppressed, + onInterruption = IsSuppressed, + onControl = IsSuppressed, + onException = IsSuppressed) + } + + @Test + def usingThrowingException(): Unit = { + genericUsingThrowing(new ExceptionResource, + onLinkage = IsSuppressed, + onInterruption = IsSuppressed, + onControl = IsSuppressed, + onException = IsSuppressed) + } + + /* `Using.Manager.apply` exception preference */ + + private def genericManagerThrowing[CloseT <: Throwable: ClassTag]( + resource: => CustomResource[CloseT], + onLinkage: SuppressionBehavior, + onInterruption: SuppressionBehavior, + onControl: SuppressionBehavior, + onException: SuppressionBehavior + ): Unit = { + def check[UseT <: Throwable: ClassTag]( + t: String => UseT, + behavior: SuppressionBehavior, + allowsSuppression: Boolean, + yieldsTry: Boolean + ): Unit = { + val ex = if (yieldsTry) UseManager(resource, t) else UseManager.catching(resource, t) + if (behavior == IsSuppressed) { + assertThrowableClass[UseT](ex) + if (allowsSuppression) assertSingleSuppressed[CloseT](ex) + else assertNoSuppressed(ex) + } else { + assertThrowableClass[CloseT](ex) + if (behavior == AcceptsSuppressed) assertSingleSuppressed[UseT](ex) + else assertNoSuppressed(ex) + } + } + + check(new UsingVMError(_), behavior = IsSuppressed, allowsSuppression = true, yieldsTry = false) + check(new UsingLinkageError(_), onLinkage, allowsSuppression = true, yieldsTry = false) + check(_ => new UsingInterruption, onInterruption, allowsSuppression = true, yieldsTry = false) + check(new UsingControl(_), onControl, allowsSuppression = false, yieldsTry = false) + check(new UsingError(_), + onException, + allowsSuppression = true, + yieldsTry = onException == IsSuppressed) + check(new UsingException(_), + onException, + allowsSuppression = true, + yieldsTry = onException == IsSuppressed) + } + + @Test + def managerThrowingVMError(): Unit = { + genericManagerThrowing(new VMErrorResource, + onLinkage = AcceptsSuppressed, + onInterruption = AcceptsSuppressed, + onControl = AcceptsSuppressed, + onException = AcceptsSuppressed) + } + + @Test + def managerThrowingLinkageError(): Unit = { + genericManagerThrowing(new LinkageResource, + onLinkage = IsSuppressed, + onInterruption = AcceptsSuppressed, + onControl = AcceptsSuppressed, + onException = AcceptsSuppressed) + } + + @Test + def managerThrowingInterruption(): Unit = { + genericManagerThrowing(new InterruptionResource, + onLinkage = IsSuppressed, + onInterruption = IsSuppressed, + onControl = AcceptsSuppressed, + onException = AcceptsSuppressed) + } + + @Test + def managerThrowingControl(): Unit = { + genericManagerThrowing(new ControlResource, + onLinkage = IsSuppressed, + onInterruption = IsSuppressed, + onControl = IsSuppressed, + onException = IgnoresSuppressed) + } + + @Test + def managerThrowingError(): Unit = { + genericManagerThrowing(new ErrorResource, + onLinkage = IsSuppressed, + onInterruption = IsSuppressed, + onControl = IsSuppressed, + onException = IsSuppressed) + } + + @Test + def managerThrowingException(): Unit = { + genericManagerThrowing(new ExceptionResource, + onLinkage = IsSuppressed, + onInterruption = IsSuppressed, + onControl = IsSuppressed, + onException = IsSuppressed) + } + + /* nested resource usage returns the correct exception */ + + private def checkMultiplePropagatesCorrectlySimple(usingException: Throwable): Unit = { + /* + UsingException + |- ClosingError + |- ClosingException + */ + assertThrowableClass[UsingException](usingException) + val suppressed = usingException.getSuppressed + assertEquals(suppressed.length, 2) + val closingError = suppressed(0) + val closingException = suppressed(1) + assertThrowableClass[ClosingError](closingError) + assertThrowableClass[ClosingException](closingException) + } + + private def checkMultiplePropagatesCorrectlyComplex(vmError: Throwable): Unit = { + /* + ClosingVMError + |- UsingException + | |- ClosingError + |- ClosingException + */ + assertThrowableClass[ClosingVMError](vmError) + val firstLevelSuppressed = vmError.getSuppressed + assertEquals(firstLevelSuppressed.length, 2) + val usingException = firstLevelSuppressed(0) + val closingException = firstLevelSuppressed(1) + assertThrowableClass[UsingException](usingException) + assertThrowableClass[ClosingException](closingException) + assertSingleSuppressed[ClosingError](usingException) + } + + private def checkMultiplePropagatesCorrectlyExtremelyComplex(vmError: Throwable): Unit = { + /* + ClosingVMError + |- ClosingLinkageError + | |- ClosingInterruption + | | |- UsingException + | | | |- ClosingError + | | |- ClosingException + | |- ClosingError + | |- ClosingControl + |- ClosingException + */ + assertThrowableClass[ClosingVMError](vmError) + + val firstLevelSuppressed = vmError.getSuppressed + assertEquals(firstLevelSuppressed.length, 2) + val closingLinkage = firstLevelSuppressed(0) + val closingException1 = firstLevelSuppressed(1) + assertThrowableClass[ClosingLinkageError](closingLinkage) + assertThrowableClass[ClosingException](closingException1) + assertNoSuppressed(closingException1) + + val secondLevelSuppressed = closingLinkage.getSuppressed + assertEquals(secondLevelSuppressed.length, 3) + val closingInterruption = secondLevelSuppressed(0) + val closingError2 = secondLevelSuppressed(1) + val closingControl = secondLevelSuppressed(2) + assertNoSuppressed(closingError2) + assertNoSuppressed(closingControl) + + val thirdLevelSuppressed = closingInterruption.getSuppressed + assertEquals(thirdLevelSuppressed.length, 2) + val usingException = thirdLevelSuppressed(0) + val closingException2 = thirdLevelSuppressed(1) + assertSingleSuppressed[ClosingError](usingException) + assertNoSuppressed(closingException2) + } + + /* `Using.resource` nesting */ + + @Test + def resourceMultiplePropagatesCorrectlySimple(): Unit = { + val usingException = catchThrowable { + Using.resource(new ExceptionResource) { _ => + Using.resource(new ErrorResource) { _ => + throw new UsingException("nested `Using.resource`") + } + } + } + + // uncomment to debug actual suppression nesting + //usingException.printStackTrace() + + checkMultiplePropagatesCorrectlySimple(usingException) + } + + @Test + def resourceMultiplePropagatesCorrectlyComplex(): Unit = { + val vmError = catchThrowable { + Using.resource(new ExceptionResource) { _ => + Using.resource(new VMErrorResource) { _ => + Using.resource(new ErrorResource) { _ => + throw new UsingException("nested `Using.resource`") + } + } + } + } + + // uncomment to debug actual suppression nesting + //vmError.printStackTrace() + + checkMultiplePropagatesCorrectlyComplex(vmError) + } + + @Test + def resourceMultiplePropagatesCorrectlyExtremelyComplex(): Unit = { + val vmError = catchThrowable { + Using.resource(new ExceptionResource) { _ => + Using.resource(new VMErrorResource) { _ => + Using.resource(new ControlResource) { _ => + Using.resource(new ErrorResource) { _ => + Using.resource(new LinkageResource) { _ => + Using.resource(new ExceptionResource) { _ => + Using.resource(new InterruptionResource) { _ => + Using.resource(new ErrorResource) { _ => + throw new UsingException("nested `Using.resource`") + } + } + } + } + } + } + } + } + } + + // uncomment to debug actual suppression nesting + //vmError.printStackTrace() + + checkMultiplePropagatesCorrectlyExtremelyComplex(vmError) + } + + /* `Using.apply` nesting */ + + @Test + def usingMultiplePropagatesCorrectlySimple(): Unit = { + val scala.util.Failure(usingException) = + Using(new ExceptionResource) { _ => + Using(new ErrorResource) { _ => + throw new UsingException("nested `Using`") + }.get + } + + // uncomment to debug actual suppression nesting + //usingException.printStackTrace() + + checkMultiplePropagatesCorrectlySimple(usingException) + } + + @Test + def usingMultiplePropagatesCorrectlyComplex(): Unit = { + val vmError = catchThrowable { + Using(new ExceptionResource) { _ => + Using(new VMErrorResource) { _ => + Using(new ErrorResource) { _ => + throw new UsingException("nested `Using`") + }.get + }.get + }.get + } + + // uncomment to debug actual suppression nesting + //vmError.printStackTrace() + + checkMultiplePropagatesCorrectlyComplex(vmError) + } + + @Test + def usingMultiplePropagatesCorrectlyExtremelyComplex(): Unit = { + val vmError = catchThrowable { + Using(new ExceptionResource) { _ => + Using(new VMErrorResource) { _ => + Using(new ControlResource) { _ => + Using(new ErrorResource) { _ => + Using(new LinkageResource) { _ => + Using(new ExceptionResource) { _ => + Using(new InterruptionResource) { _ => + Using(new ErrorResource) { _ => + throw new UsingException("nested `Using`") + }.get + }.get + }.get + }.get + }.get + }.get + }.get + }.get + } + + // uncomment to debug actual suppression nesting + //vmError.printStackTrace() + + checkMultiplePropagatesCorrectlyExtremelyComplex(vmError) + } + + /* `Using.Manager.apply` nesting */ + + @Test + def managerMultipleResourcesPropagatesCorrectlySimple(): Unit = { + val scala.util.Failure(usingException) = Using.Manager { m => + val _r1 = m(new ExceptionResource) + val _r2 = m(new ErrorResource) + throw new UsingException("`Using.Manager`") + } + + // uncomment to debug actual suppression nesting + //usingException.printStackTrace() + + checkMultiplePropagatesCorrectlySimple(usingException) + } + + @Test + def managerMultipleResourcesPropagatesCorrectlyComplex(): Unit = { + val vmError = catchThrowable { + Using.Manager { m => + val _r1 = m(new ExceptionResource) + val _r2 = m(new VMErrorResource) + val _r3 = m(new ErrorResource) + throw new UsingException("`Using.Manager`") + } + } + + // uncomment to debug actual suppression nesting + //vmError.printStackTrace() + + checkMultiplePropagatesCorrectlyComplex(vmError) + } + + @Test + def managerMultiplePropagatesCorrectlyExtremelyComplex(): Unit = { + val vmError = catchThrowable { + Using.Manager { m => + val _r1 = m(new ExceptionResource) + val _r2 = m(new VMErrorResource) + val _r3 = m(new ControlResource) + val _r4 = m(new ErrorResource) + val _r5 = m(new LinkageResource) + val _r6 = m(new ExceptionResource) + val _r7 = m(new InterruptionResource) + val _r8 = m(new ErrorResource) + throw new UsingException("`Using.Manager`") + } + } + + // uncomment to debug actual suppression nesting + //vmError.printStackTrace() + + checkMultiplePropagatesCorrectlyExtremelyComplex(vmError) + } + + /* works when throwing no exceptions */ + + @Test + def resourceWithNoThrow(): Unit = { + val res = Using.resource(new NoOpResource) { _.identity("test") } + assertEquals(res, "test") + } + + @Test + def usingWithNoThrow(): Unit = { + val res = Using(new NoOpResource) { _.identity("test") } + assertEquals(res, scala.util.Success("test")) + } + + @Test + def managerWithNoThrow(): Unit = { + val res = Using.Manager { m => + m(new NoOpResource).identity("test") + } + assertEquals(res, scala.util.Success("test")) + } + + /* works when only throwing one exception */ + + @Test + def resourceOpThrow(): Unit = { + val ex = use(new NoOpResource, new UsingException(_)) + assertThrowableClass[UsingException](ex) + } + + @Test + def usingOpThrow(): Unit = { + val ex = UseWrapped(new NoOpResource, new UsingException(_)) + assertThrowableClass[UsingException](ex) + } + + @Test + def managerOpThrow(): Unit = { + val ex = UseManager(new NoOpResource, new UsingException(_)) + assertThrowableClass[UsingException](ex) + } + + @Test + def resourceClosingThrow(): Unit = { + val ex = catchThrowable { + Using.resource(new ExceptionResource)(_.identity("test")) + } + assertThrowableClass[ClosingException](ex) + } + + @Test + def usingClosingThrow(): Unit = { + val ex = Using(new ExceptionResource)(_.identity("test")).failed.get + assertThrowableClass[ClosingException](ex) + } + + @Test + def managerClosingThrow(): Unit = { + val ex = Using + .Manager { m => + m(new ExceptionResource).identity("test") + } + .failed + .get + assertThrowableClass[ClosingException](ex) + } + + /* using multiple resources close in the correct order */ + + @Test + def resources2(): Unit = { + val group = new ResourceGroup + val res = Using.resources( + group.newResource(), + group.newResource() + ) { (r1, r2) => + r1.identity(1) + r2.identity(1) + } + assertEquals(res, 2) + group.assertAllClosed() + } + + @Test + def resources3(): Unit = { + val group = new ResourceGroup + val res = Using.resources( + group.newResource(), + group.newResource(), + group.newResource() + ) { (r1, r2, r3) => + r1.identity(1) + + r2.identity(1) + + r3.identity(1) + } + assertEquals(res, 3) + group.assertAllClosed() + } + + @Test + def resources4(): Unit = { + val group = new ResourceGroup + val res = Using.resources( + group.newResource(), + group.newResource(), + group.newResource(), + group.newResource() + ) { (r1, r2, r3, r4) => + r1.identity(1) + + r2.identity(1) + + r3.identity(1) + + r4.identity(1) + } + assertEquals(res, 4) + group.assertAllClosed() + } + + @Test + def manager2(): Unit = { + val group = new ResourceGroup + val res = Using.Manager { m => + val r1 = m(group.newResource()) + val r2 = m(group.newResource()) + r1.identity(1) + r2.identity(1) + } + assertEquals(res, scala.util.Success(2)) + group.assertAllClosed() + } + + @Test + def manager3(): Unit = { + val group = new ResourceGroup + val res = Using.Manager { m => + val r1 = m(group.newResource()) + val r2 = m(group.newResource()) + val r3 = m(group.newResource()) + + r1.identity(1) + + r2.identity(1) + + r3.identity(1) + } + assertEquals(res, scala.util.Success(3)) + group.assertAllClosed() + } + + @Test + def manager4(): Unit = { + val group = new ResourceGroup + val res = Using.Manager { m => + val r1 = m(group.newResource()) + val r2 = m(group.newResource()) + val r3 = m(group.newResource()) + val r4 = m(group.newResource()) + + r1.identity(1) + + r2.identity(1) + + r3.identity(1) + + r4.identity(1) + } + assertEquals(res, scala.util.Success(4)) + group.assertAllClosed() + } + + /* misc */ + + @Test + def resourceDisallowsNull(): Unit = { + val npe = catchThrowable(Using.resource(null: AutoCloseable)(_ => "test")) + assertThrowableClass[NullPointerException](npe) + } + + @Test + def usingDisallowsNull(): Unit = { + val npe = Using(null: AutoCloseable)(_ => "test").failed.get + assertThrowableClass[NullPointerException](npe) + } + + @Test + def managerDisallowsNull(): Unit = { + val npe = Using + .Manager { m => + m(null: AutoCloseable) + "test" + } + .failed + .get + assertThrowableClass[NullPointerException](npe) + } + + @Test + def usingCatchesOpeningException(): Unit = { + val ex = Using({ throw new RuntimeException }: AutoCloseable)(_ => "test").failed.get + assertThrowableClass[RuntimeException](ex) + } + + @Test + def managerCatchesOpeningException(): Unit = { + val ex = Using + .Manager { m => + m({ throw new RuntimeException }: AutoCloseable) + "test" + } + .failed + .get + assertThrowableClass[RuntimeException](ex) + } +} + +object UsingTest { + final class ClosingVMError(message: String) extends VirtualMachineError(message) + final class UsingVMError(message: String) extends VirtualMachineError(message) + final class ClosingLinkageError(message: String) extends LinkageError(message) + final class UsingLinkageError(message: String) extends LinkageError(message) + type ClosingInterruption = InterruptedException + type UsingInterruption = ThreadDeath + // `NonLocalReturnControl` incorrectly suppresses exceptions, so this tests that + // `Using` special-cases it. + final class ClosingControl(message: String) extends NonLocalReturnControl(message, message) + final class UsingControl(message: String) extends NonLocalReturnControl(message, message) + final class ClosingError(message: String) extends Error(message) + final class UsingError(message: String) extends Error(message) + final class ClosingException(message: String) extends Exception(message) + final class UsingException(message: String) extends Exception(message) + + abstract class BaseResource extends AutoCloseable { + final def identity[A](a: A): A = a + } + + final class NoOpResource extends BaseResource { + override def close(): Unit = () + } + + abstract class CustomResource[T <: Throwable](t: String => T) extends BaseResource { + override final def close(): Unit = throw t("closing " + getClass.getSimpleName) + } + + final class VMErrorResource extends CustomResource(new ClosingVMError(_)) + final class LinkageResource extends CustomResource(new ClosingLinkageError(_)) + final class InterruptionResource extends CustomResource(new ClosingInterruption(_)) + final class ControlResource extends CustomResource(new ClosingControl(_)) + final class ErrorResource extends CustomResource(new ClosingError(_)) + final class ExceptionResource extends CustomResource(new ClosingException(_)) + + sealed trait SuppressionBehavior + + /** is added as a suppressed exception to the other exception, and the other exception is thrown */ + case object IsSuppressed extends SuppressionBehavior + + /** is thrown, and the other exception is added to this as suppressed */ + case object AcceptsSuppressed extends SuppressionBehavior + + /** is thrown, and the other exception is ignored */ + case object IgnoresSuppressed extends SuppressionBehavior + + def assertThrowableClass[T <: Throwable: ClassTag](t: Throwable): Unit = { + assertEquals(s"Caught [${t.getMessage}]", implicitly[ClassTag[T]].runtimeClass, t.getClass) + } + + def assertSingleSuppressed[T <: Throwable: ClassTag](t: Throwable): Unit = { + val suppressed = t.getSuppressed + assertEquals(1, suppressed.length) + assertThrowableClass[T](suppressed(0)) + } + + def assertNoSuppressed(t: Throwable): Unit = { + assertEquals(0, t.getSuppressed.length) + } + + def catchThrowable(thunk: => Any): Throwable = { + try { + thunk + throw new AssertionError("unreachable") + } catch { + case t: Throwable => t + } + } + + object UseWrapped { + def apply(resource: => BaseResource, t: String => Throwable): Throwable = + Using(resource)(opThrowing(t)).failed.get + + def catching(resource: => BaseResource, t: String => Throwable): Throwable = + catchThrowable(Using(resource)(opThrowing(t))) + } + + object UseManager { + def apply(resource: => BaseResource, t: String => Throwable): Throwable = + Using + .Manager { m => + val r = m(resource) + opThrowing(t)(r) + } + .failed + .get + def catching(resource: => BaseResource, t: String => Throwable): Throwable = + catchThrowable { + Using.Manager { m => + val r = m(resource) + opThrowing(t)(r) + } + } + } + + def use(resource: BaseResource, t: String => Throwable): Throwable = + catchThrowable(Using.resource(resource)(opThrowing(t))) + + private def opThrowing(t: String => Throwable): BaseResource => Nothing = + r => { + r.identity("test") + throw t("exception using resource") + } + + final class ResourceGroup { + // tracks the number of open resources + private var openCount: Int = 0 + + def newResource(): BaseResource = { + openCount += 1 + new CountingResource(openCount) + } + + def assertAllClosed(): Unit = assertEquals(openCount, 0) + + private final class CountingResource(countWhenCreated: Int) extends BaseResource { + override def close(): Unit = { + assertEquals(countWhenCreated, openCount) + openCount -= 1 + } + } + } +} From 99d7f16128136a56e2e06653af0779d3b4e232d4 Mon Sep 17 00:00:00 2001 From: NthPortal Date: Wed, 15 Apr 2020 18:05:44 -0400 Subject: [PATCH 2/2] Backport new ControlThrowable Backport `scala.util.control.ControlThrowable` from 2.13 (which does not suppress exceptions or have a stack trace) as `scala.util.control.compat.ControlThrowable` (it still extends previous Scala versions' `scala.util.control.ControlThrowable`). --- .../control/compat/ControlThrowable.scala | 54 +++++++++++++++++++ .../scala/util/control/compat/package.scala | 5 ++ .../control/compat/ControlThrowableTest.scala | 17 ++++++ 3 files changed, 76 insertions(+) create mode 100644 compat/src/main/scala-2.11_2.12/scala/util/control/compat/ControlThrowable.scala create mode 100644 compat/src/main/scala-2.13/scala/util/control/compat/package.scala create mode 100644 compat/src/test/scala/scala/util/control/compat/ControlThrowableTest.scala diff --git a/compat/src/main/scala-2.11_2.12/scala/util/control/compat/ControlThrowable.scala b/compat/src/main/scala-2.11_2.12/scala/util/control/compat/ControlThrowable.scala new file mode 100644 index 00000000..08b4bb8f --- /dev/null +++ b/compat/src/main/scala-2.11_2.12/scala/util/control/compat/ControlThrowable.scala @@ -0,0 +1,54 @@ +/* + * Scala (https://www.scala-lang.org) + * + * Copyright EPFL and Lightbend, Inc. + * + * Licensed under Apache License 2.0 + * (http://www.apache.org/licenses/LICENSE-2.0). + * + * See the NOTICE file distributed with this work for + * additional information regarding copyright ownership. + */ +package scala.util.control.compat + +/** A parent class for throwable objects intended for flow control. + * + * Instances of `ControlThrowable` should not normally be caught. + * + * As a convenience, `NonFatal` does not match `ControlThrowable`. + * + * {{{ + * import scala.util.control.{Breaks, NonFatal}, Breaks.{break, breakable} + * + * breakable { + * for (v <- values) { + * try { + * if (p(v)) break + * else ??? + * } catch { + * case NonFatal(t) => log(t) // can't catch a break + * } + * } + * } + * }}} + * + * Suppression is disabled, because flow control should not suppress + * an exceptional condition. Stack traces are also disabled, allowing + * instances of `ControlThrowable` to be safely reused. + * + * Instances of `ControlThrowable` should not normally have a cause. + * Legacy subclasses may set a cause using `initCause`. + * + * @note this compat class exists so that instances of `ControlThrowable` + * can be created using the same API and with the same suppression + * and stack trace writability semantics across versions. + */ +abstract class ControlThrowable(message: String) + extends Throwable(message, /*cause=*/ null, /*enableSuppression=*/ false, + /*writableStackTrace=*/ false) + with scala.util.control.ControlThrowable { + + override def fillInStackTrace(): Throwable = super[Throwable].fillInStackTrace() + + def this() = this(message = null) +} diff --git a/compat/src/main/scala-2.13/scala/util/control/compat/package.scala b/compat/src/main/scala-2.13/scala/util/control/compat/package.scala new file mode 100644 index 00000000..bad246d6 --- /dev/null +++ b/compat/src/main/scala-2.13/scala/util/control/compat/package.scala @@ -0,0 +1,5 @@ +package scala.util.control + +package object compat { + type ControlThrowable = scala.util.control.ControlThrowable +} diff --git a/compat/src/test/scala/scala/util/control/compat/ControlThrowableTest.scala b/compat/src/test/scala/scala/util/control/compat/ControlThrowableTest.scala new file mode 100644 index 00000000..43caa63a --- /dev/null +++ b/compat/src/test/scala/scala/util/control/compat/ControlThrowableTest.scala @@ -0,0 +1,17 @@ +package scala.util.control.compat + +import org.junit.Test + +class ControlThrowableTest { + @Test + def doesNotSuppress(): Unit = { + val t = new ControlThrowable {} + t.addSuppressed(new Exception) + assert(t.getSuppressed.isEmpty) + } + + @Test + def doesNotHaveStackTrace(): Unit = { + assert(new ControlThrowable {}.getStackTrace.isEmpty) + } +}