diff --git a/compat/src/main/scala-2.11_2.12/scala/util/compat/ChainingOps.scala b/compat/src/main/scala-2.11_2.12/scala/util/compat/ChainingOps.scala new file mode 100644 index 00000000..0f615791 --- /dev/null +++ b/compat/src/main/scala-2.11_2.12/scala/util/compat/ChainingOps.scala @@ -0,0 +1,41 @@ +package scala.util.compat + +trait ChainingSyntax { + implicit final def scalaUtilChainingOps[A](a: A): ChainingOps[A] = new ChainingOps(a) +} + +/** Adds chaining methods `tap` and `pipe` to every type. + */ +final class ChainingOps[A](val self: A) extends AnyVal { + /** Applies `f` to the value for its side effects, and returns the original value. + * + * {{{ + * val xs = List(1, 2, 3) + * .tap(ys => println("debug " + ys.toString)) + * // xs == List(1, 2, 3) + * }}} + * + * @param f the function to apply to the value. + * @tparam U the result type of the function `f`. + * @return the original value `self`. + */ + def tap[U](f: A => U): A = { + f(self) + self + } + + /** Converts the value by applying the function `f`. + * + * {{{ + * val times6 = (_: Int) * 6 + * val i = (1 - 2 - 3).pipe(times6).pipe(scala.math.abs) + * // i == 24 + * }}} + * + * @param f the function to apply to the value. + * @tparam B the result type of the function `f`. + * @return a new value resulting from applying the given function + * `f` to this value. + */ + def pipe[B](f: A => B): B = f(self) +} diff --git a/compat/src/main/scala-2.11_2.12/scala/util/compat/Using.scala b/compat/src/main/scala-2.11_2.12/scala/util/compat/Using.scala new file mode 100644 index 00000000..6e64db23 --- /dev/null +++ b/compat/src/main/scala-2.11_2.12/scala/util/compat/Using.scala @@ -0,0 +1,292 @@ +package scala.util.compat + +import java.util.concurrent.atomic.AtomicBoolean + +import scala.util.control.{ControlThrowable, NonFatal} +import scala.util._ + +/** A utility for performing automatic resource management. It can be used to perform an + * operation using resources, after which it will release the resources, in reverse order + * of their creation. The resource opening, operation, and resource releasing are wrapped + * in a `Try`. + * + * If more than one exception is thrown by the operation and releasing resources, + * the exception thrown ''first'' is returned within the `Try`, with the other exceptions + * [[java.lang.Throwable.addSuppressed(Throwable) added as suppressed exceptions]] + * to the one thrown first. This is the case ''unless'' a later exception is + * [[scala.util.control.NonFatal fatal]], and the one preceding it is not. In that case, + * the first exception is added as a suppressed exception to the fatal one, and the fatal + * one is thrown. If an exception is a + * [[scala.util.control.ControlThrowable ControlThrowable]], no exception will be added to + * it as a suppressed exception. + * + * @example + * {{{ + * val lines: Try[List[String]] = Using(resource1) { r1 => + * r1.lines.toList + * } + * }}} + * + * @example + * {{{ + * val lines: Try[Seq[String]] = for { + * r1 <- Using(resource1) + * r2 <- Using(resource2) + * r3 <- Using(resource3) + * r4 <- Using(resource4) + * } yield { + * // use your resources here + * r1.lines ++ r2.lines ++ r3.lines ++ r4.lines + * } + * }}} + */ +final class Using[R] private(resource: => R) { + private[this] val used = new AtomicBoolean(false) + + /** Performs an operation using a resource, and then releases the resource, + * even if the operation throws an exception. + * + * @param f the operation to perform + * @param r an implicit [[Using.Resource]] + * @tparam A the return type of the operation + * @throws java.lang.IllegalStateException if the resource has already been used + * @return a [[scala.util.Try `Try`]] containing the result of the operation, or + * an exception if one was thrown by the operation or by releasing the resource + */ + @throws[IllegalStateException]("if the resource has already been used") + @inline def apply[A](f: R => A)(implicit r: Using.Resource[R]): Try[A] = map(f) + + /** Performs an operation using a resource, and then releases the resource, + * even if the operation throws an exception. + * + * @param f the operation to perform + * @param r an implicit [[Using.Resource]] + * @tparam A the return type of the operation + * @throws java.lang.IllegalStateException if the resource has already been used + * @return a [[scala.util.Try `Try`]] containing the result of the operation, or + * an exception if one was thrown by the operation or by releasing the resource + */ + @throws[IllegalStateException]("if the resource has already been used") + def map[A](f: R => A)(implicit r: Using.Resource[R]): Try[A] = Try { useWith(f) } + + /** Performs an operation which returns a [[scala.util.Try `Try`]] using a resource, + * and then releases the resource, even if the operation throws an exception. + * + * @param f the `Try`-returning operation to perform + * @param r an implicit [[Using.Resource]] + * @tparam A the return type of the operation + * @throws java.lang.IllegalStateException if the resource has already been used + * @return the result of the inner operation, or a [[scala.util.Try `Try`]] + * containing an exception if one was thrown by the operation or by + * releasing the resource + */ + @throws[IllegalStateException]("if the resource has already been used") + def flatMap[A](f: R => Try[A])(implicit r: Using.Resource[R]): Try[A] = + map { + r => f(r).get // otherwise inner Failure will be lost on exceptional release + } + + @inline private[this] def useWith[A](f: R => A)(implicit r: Using.Resource[R]): A = + if (used.getAndSet(true)) throw new IllegalStateException("resource has already been used") + else Using.resource(resource)(f) +} + +/** @define recommendUsing It is highly recommended to use the `Using` construct, + * which safely wraps resource usage and management in a `Try`. + * @define multiResourceSuppressionBehavior If more than one exception is thrown by the operation and releasing resources, + * the exception thrown ''first'' is thrown, with the other exceptions + * [[java.lang.Throwable.addSuppressed(Throwable) added as suppressed exceptions]] + * to the one thrown first. This is the case ''unless'' a later exception is + * [[scala.util.control.NonFatal fatal]], and the one preceding it is not. In that case, + * the first exception is added as a suppressed exception to the fatal one, and the fatal + * one is thrown. If an exception is a + * [[scala.util.control.ControlThrowable ControlThrowable]], no exception will be added to + * it as a suppressed exception. + */ +object Using { + /** Creates a `Using` from the given resource. + * + * @note If the resource does not have an implicit [[Resource]] in + * scope, the returned `Using` will be useless. + */ + def apply[R](resource: => R): Using[R] = new Using(resource) + + /** 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. + * + * $recommendUsing + * + * If both the operation and releasing the resource throw exceptions, the one thrown + * when releasing the resource is + * [[java.lang.Throwable.addSuppressed(Throwable) added as a suppressed exception]] + * to the one thrown by the operation, ''unless'' the exception thrown when releasing + * the resource is [[scala.util.control.NonFatal fatal]], and the one thrown by the + * operation is not. In that case, the exception thrown by the operation is added + * as a suppressed exception to the one thrown when releasing the resource. If an + * exception is a [[scala.util.control.ControlThrowable ControlThrowable]], no + * exception will be added to it as a suppressed exception. + * + * @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: Resource, A](resource: R)(body: R => A): A = { + if (resource == null) throw new NullPointerException("null resource") + + @inline def safeAddSuppressed(t: Throwable, suppressed: Throwable): Unit = { + // don't `addSuppressed` to something which is a `ControlThrowable` + // nor to scalaJS + if (!t.isInstanceOf[ControlThrowable] && (1.0.toString == "1.0")) t.addSuppressed(suppressed) + } + + var primary: Throwable = null + try { + body(resource) + } catch { + case t: Throwable => + primary = t + null.asInstanceOf[A] // compiler doesn't know `finally` will throw + } finally { + if (primary eq null) implicitly[Resource[R]].release(resource) + else { + var toThrow = primary + try { + implicitly[Resource[R]].release(resource) + } catch { + case other: Throwable => + if (NonFatal(primary) && !NonFatal(other)) { + // `other` is fatal, `primary` is not + toThrow = other + safeAddSuppressed(other, primary) + } else { + // `toThrow` is already `primary` + safeAddSuppressed(primary, 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. + * + * $recommendUsing + * + * $multiResourceSuppressionBehavior + * + * @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: Resource, R2: Resource, 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. + * + * $recommendUsing + * + * $multiResourceSuppressionBehavior + * + * @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: Resource, R2: Resource, R3: Resource, 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. + * + * $recommendUsing + * + * $multiResourceSuppressionBehavior + * + * @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: Resource, R2: Resource, R3: Resource, R4: Resource, 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 a resource which can be released. + * + * @tparam R the type of the resource + */ + trait Resource[-R] { + /** Releases the specified resource. */ + def release(resource: R): Unit + } + + object Resource { + import java.io.Closeable + /** An implicit `Resource` for [[java.lang.Closeable `Closeable`s]]. */ + implicit val closeableResource: Resource[Closeable] = new Resource[Closeable] { + def release(resource: Closeable) = resource.close() + } + } + +} diff --git a/compat/src/main/scala-2.11_2.12/scala/util/compat/package.scala b/compat/src/main/scala-2.11_2.12/scala/util/compat/package.scala new file mode 100644 index 00000000..10324f99 --- /dev/null +++ b/compat/src/main/scala-2.11_2.12/scala/util/compat/package.scala @@ -0,0 +1,8 @@ +package scala.util + +package object compat { + /** + * Adds chaining methods `tap` and `pipe` to every type. See [[ChainingOps]]. + */ + object chainingOps extends ChainingSyntax +} diff --git a/compat/src/main/scala-2.13/scala/util/compat/package.scala b/compat/src/main/scala-2.13/scala/util/compat/package.scala new file mode 100644 index 00000000..9b3be24e --- /dev/null +++ b/compat/src/main/scala-2.13/scala/util/compat/package.scala @@ -0,0 +1,11 @@ +package scala.util + +package object compat { + /** + * Adds chaining methods `tap` and `pipe` to every type. See [[ChainingOps]]. + */ + object chainingOps extends ChainingSyntax + + type Using[R] = scala.util.Using[R] + val Using = scala.util.Using +} diff --git a/compat/src/test/scala/test/scala/util/ChainingOpsTest.scala b/compat/src/test/scala/test/scala/util/ChainingOpsTest.scala new file mode 100644 index 00000000..aeb26331 --- /dev/null +++ b/compat/src/test/scala/test/scala/util/ChainingOpsTest.scala @@ -0,0 +1,28 @@ +package scala.util.compat + +import org.junit.Assert._ +import org.junit.Test + +class ChainingOpsTest { + import scala.util.compat.chainingOps._ + + @Test + def testAnyTap: Unit = { + var x: Int = 0 + val result = List(1, 2, 3) + .tap(xs => x = xs.head) + + assertEquals(1, x) + assertEquals(List(1, 2, 3), result) + } + + @Test + def testAnyPipe: Unit = { + val times6 = (_: Int) * 6 + val result = (1 - 2 - 3) + .pipe(times6) + .pipe(scala.math.abs) + + assertEquals(24, result) + } +} diff --git a/compat/src/test/scala/test/scala/util/UsingTest.scala b/compat/src/test/scala/test/scala/util/UsingTest.scala new file mode 100644 index 00000000..8744357f --- /dev/null +++ b/compat/src/test/scala/test/scala/util/UsingTest.scala @@ -0,0 +1,484 @@ +package scala.util.compat + +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.junit.Test +import org.junit.Assert._ + +import scala.reflect.ClassTag +import scala.util.control.ControlThrowable +import java.io.Closeable + +@RunWith(classOf[JUnit4]) +class UsingTest { + import UsingTest._ + + /* raw `Using.resource` that doesn't use `Try` */ + + @Test + def usingResourceThatThrowsException(): Unit = { + val exception = use(new ExceptionResource, new UsingException(_)) + assertThrowableClass[UsingException](exception) + assertSingleSuppressed[ClosingException](exception) + + val error = use(new ExceptionResource, new Error(_)) + assertThrowableClass[Error](error) + assertSingleSuppressed[ClosingException](error) + + val fatal = use(new ExceptionResource, new OutOfMemoryError(_)) + assertThrowableClass[OutOfMemoryError](fatal) + assertSingleSuppressed[ClosingException](fatal) + + val control = use(new ExceptionResource, new UsingMarker(_)) + assertThrowableClass[UsingMarker](control) + assertNoSuppressed(control) + } + + @Test + def usingResourceThatThrowsError(): Unit = { + val exception = use(new ErrorResource, new UsingException(_)) + assertThrowableClass[UsingException](exception) + assertSingleSuppressed[ClosingError](exception) + + val error = use(new ErrorResource, new Error(_)) + assertThrowableClass[Error](error) + assertSingleSuppressed[ClosingError](error) + + val fatal = use(new ErrorResource, new OutOfMemoryError(_)) + assertThrowableClass[OutOfMemoryError](fatal) + assertSingleSuppressed[ClosingError](fatal) + + val control = use(new ErrorResource, new UsingMarker(_)) + assertThrowableClass[UsingMarker](control) + assertNoSuppressed(control) + } + + @Test + def usingResourceThatThrowsFatal(): Unit = { + val exception = use(new FatalResource, new UsingException(_)) + assertThrowableClass[StackOverflowError](exception) + assertSingleSuppressed[UsingException](exception) + + val error = use(new FatalResource, new Error(_)) + assertThrowableClass[StackOverflowError](error) + assertSingleSuppressed[Error](error) + + val fatal = use(new FatalResource, new OutOfMemoryError(_)) + assertThrowableClass[OutOfMemoryError](fatal) + assertSingleSuppressed[StackOverflowError](fatal) + + val control = use(new FatalResource, new UsingMarker(_)) + assertThrowableClass[UsingMarker](control) + assertNoSuppressed(control) + } + + @Test + def usingResourceThatThrowsControlThrowable(): Unit = { + val exception = use(new MarkerResource, new UsingException(_)) + assertThrowableClass[ClosingMarker](exception) + assertNoSuppressed(exception) + + val error = use(new MarkerResource, new Error(_)) + assertThrowableClass[ClosingMarker](error) + assertNoSuppressed(error) + + val fatal = use(new MarkerResource, new OutOfMemoryError(_)) + assertThrowableClass[OutOfMemoryError](fatal) + assertSingleSuppressed[ClosingMarker](fatal) + + val control = use(new MarkerResource, new UsingMarker(_)) + assertThrowableClass[UsingMarker](control) + assertNoSuppressed(control) + } + + /* safe `Using` that returns `Try` */ + + @Test + def safeUsingResourceThatThrowsException(): Unit = { + val exception = UseWrapped(new ExceptionResource, new UsingException(_)) + assertThrowableClass[UsingException](exception) + assertSingleSuppressed[ClosingException](exception) + + val error = UseWrapped(new ExceptionResource, new Error(_)) + assertThrowableClass[Error](error) + assertSingleSuppressed[ClosingException](error) + + val fatal = UseWrapped.catching(new ExceptionResource, new OutOfMemoryError(_)) + assertThrowableClass[OutOfMemoryError](fatal) + assertSingleSuppressed[ClosingException](fatal) + + val control = UseWrapped.catching(new ExceptionResource, new UsingMarker(_)) + assertThrowableClass[UsingMarker](control) + assertNoSuppressed(control) + } + + @Test + def safeUsingResourceThatThrowsError(): Unit = { + val exception = UseWrapped(new ErrorResource, new UsingException(_)) + assertThrowableClass[UsingException](exception) + assertSingleSuppressed[ClosingError](exception) + + val error = UseWrapped(new ErrorResource, new Error(_)) + assertThrowableClass[Error](error) + assertSingleSuppressed[ClosingError](error) + + val fatal = UseWrapped.catching(new ErrorResource, new OutOfMemoryError(_)) + assertThrowableClass[OutOfMemoryError](fatal) + assertSingleSuppressed[ClosingError](fatal) + + val control = UseWrapped.catching(new ErrorResource, new UsingMarker(_)) + assertThrowableClass[UsingMarker](control) + assertNoSuppressed(control) + } + + @Test + def safeUsingResourceThatThrowsFatal(): Unit = { + val exception = UseWrapped.catching(new FatalResource, new UsingException(_)) + assertThrowableClass[StackOverflowError](exception) + assertSingleSuppressed[UsingException](exception) + + val error = UseWrapped.catching(new FatalResource, new Error(_)) + assertThrowableClass[StackOverflowError](error) + assertSingleSuppressed[Error](error) + + val fatal = UseWrapped.catching(new FatalResource, new OutOfMemoryError(_)) + assertThrowableClass[OutOfMemoryError](fatal) + assertSingleSuppressed[StackOverflowError](fatal) + + val control = UseWrapped.catching(new FatalResource, new UsingMarker(_)) + assertThrowableClass[UsingMarker](control) + assertNoSuppressed(control) + } + + @Test + def safeUsingResourceThatThrowsControlThrowable(): Unit = { + val exception = UseWrapped.catching(new MarkerResource, new UsingException(_)) + assertThrowableClass[ClosingMarker](exception) + assertNoSuppressed(exception) + + val error = UseWrapped.catching(new MarkerResource, new Error(_)) + assertThrowableClass[ClosingMarker](error) + assertNoSuppressed(error) + + val fatal = UseWrapped.catching(new MarkerResource, new OutOfMemoryError(_)) + assertThrowableClass[OutOfMemoryError](fatal) + assertSingleSuppressed[ClosingMarker](fatal) + + val control = UseWrapped.catching(new MarkerResource, new UsingMarker(_)) + assertThrowableClass[UsingMarker](control) + assertNoSuppressed(control) + } + + /* nested resource usage returns the correct exception */ + + @Test + def usingMultipleResourcesPropagatesCorrectlySimple(): 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() + + /* + 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) + } + + @Test + def usingMultipleResourcesPropagatesCorrectlyComplex(): Unit = { + val fatal = catchThrowable { + Using.resource(new ExceptionResource) { _ => + Using.resource(new FatalResource) { _ => + Using.resource(new ErrorResource) { _ => + throw new UsingException("nested `Using.resource`") + } + } + } + } + + // uncomment to debug actual suppression nesting + //fatal.printStackTrace() + + /* + StackOverflowError + |- UsingException + | |- ClosingError + |- ClosingException + */ + assertThrowableClass[StackOverflowError](fatal) + val firstLevelSuppressed = fatal.getSuppressed + assertEquals(firstLevelSuppressed.length, 2) + val usingException = firstLevelSuppressed(0) + val closingException = firstLevelSuppressed(1) + assertThrowableClass[UsingException](usingException) + assertThrowableClass[ClosingException](closingException) + assertSingleSuppressed[ClosingError](usingException) + } + + @Test + def safeUsingMultipleResourcesPropagatesCorrectlySimple(): Unit = { + val scala.util.Failure(usingException) = for { + _ <- Using(new ExceptionResource) + _ <- Using(new ErrorResource) + } yield { + throw new UsingException("nested `Using`") + } + + // uncomment to debug actual suppression nesting + //usingException.printStackTrace() + + /* + 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) + } + + @Test + def safeUsingMultipleResourcesPropagatesCorrectlyComplex(): Unit = { + val fatal = catchThrowable { + for { + _ <- Using(new ExceptionResource) + _ <- Using(new FatalResource) + _ <- Using(new ErrorResource) + } yield { + throw new UsingException("nested `Using`") + } + } + + // uncomment to debug actual suppression nesting + //fatal.printStackTrace() + + /* + StackOverflowError + |- UsingException + | |- ClosingError + |- ClosingException + */ + assertThrowableClass[StackOverflowError](fatal) + val firstLevelSuppressed = fatal.getSuppressed + assertEquals(firstLevelSuppressed.length, 2) + val usingException = firstLevelSuppressed(0) + val closingException = firstLevelSuppressed(1) + assertThrowableClass[UsingException](usingException) + assertThrowableClass[ClosingException](closingException) + assertSingleSuppressed[ClosingError](usingException) + } + + /* works when throwing no exceptions */ + + @Test + def usingResourceWithNoThrow(): Unit = { + val res = Using.resource(new NoOpResource) { r => + r.identity("test") + } + assertEquals(res, "test") + } + + @Test + def safeUsingResourceWithNoThrow(): Unit = { + val res = Using(new NoOpResource) { r => + r.identity("test") + } + assertEquals(res, scala.util.Success("test")) + } + + /* using multiple resources close in the correct order */ + + @Test + def using2Resources(): Unit = { + val group = new ResourceGroup + val res = Using.resources( + group.newResource(), + group.newResource() + ) { (r1, r2) => + r1.identity(1) + r2.identity(1) + } + assertEquals(res, 2) + } + + @Test + def using3Resources(): 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) + } + + @Test + def using4Resources(): 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) + } + + @Test + def safeUsing2Resources(): Unit = { + val group = new ResourceGroup + val res = for { + r1 <- Using(group.newResource()) + r2 <- Using(group.newResource()) + } yield { + r1.identity(1) + r2.identity(1) + } + assertEquals(res, scala.util.Success(2)) + } + + @Test + def safeUsing3Resources(): Unit = { + val group = new ResourceGroup + val res = for { + r1 <- Using(group.newResource()) + r2 <- Using(group.newResource()) + r3 <- Using(group.newResource()) + } yield { + r1.identity(1) + + r2.identity(1) + + r3.identity(1) + } + assertEquals(res, scala.util.Success(3)) + } + + /* misc */ + + @Test + def usingDisallowsNull(): Unit = { + val npe = catchThrowable(Using.resource(null: Closeable)(_ => "test")) + assertThrowableClass[NullPointerException](npe) + } + + @Test + def safeUsingDisallowsNull(): Unit = { + val npe = Using(null: Closeable)(_ => "test").failed.get + assertThrowableClass[NullPointerException](npe) + } + + @Test + def safeUsingCatchesOpeningException(): Unit = { + val ex = Using({ throw new RuntimeException }: Closeable)(_ => "test").failed.get + assertThrowableClass[RuntimeException](ex) + } +} + +object UsingTest { + final class ClosingException(message: String) extends Exception(message) + final class UsingException(message: String) extends Exception(message) + final class ClosingError(message: String) extends Error(message) + final class UsingError(message: String) extends Error(message) + final class ClosingMarker(message: String) extends Throwable(message) with ControlThrowable + final class UsingMarker(message: String) extends Throwable(message) with ControlThrowable + + abstract class BaseResource extends Closeable { + final def identity[A](a: A): A = a + } + + final class NoOpResource extends BaseResource { + override def close(): Unit = () + } + + abstract class CustomResource(t: String => Throwable) extends BaseResource { + override final def close(): Unit = throw t("closing " + getClass.getSimpleName) + } + + final class ExceptionResource extends CustomResource(new ClosingException(_)) + final class ErrorResource extends CustomResource(new ClosingError(_)) + final class FatalResource extends CustomResource(new StackOverflowError(_)) + final class MarkerResource extends CustomResource(new ClosingMarker(_)) + + def assertThrowableClass[T <: Throwable: ClassTag](t: Throwable): Unit = { + assertEquals(t.getClass, implicitly[ClassTag[T]].runtimeClass) + } + + def assertSingleSuppressed[T <: Throwable: ClassTag](t: Throwable): Unit = { + val suppressed = t.getSuppressed + assertEquals(suppressed.length, 1) + assertThrowableClass[T](suppressed(0)) + } + + def assertNoSuppressed(t: Throwable): Unit = { + assertEquals(t.getSuppressed.length, 0) + } + + 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))) + } + + 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) + } + + private final class CountingResource(countWhenCreated: Int) extends BaseResource { + override def close(): Unit = { + assertEquals(countWhenCreated, openCount) + openCount -= 1 + } + } + + } + +}