Skip to content

Commit 28d2826

Browse files
committed
Add for comprehension support to UsingUtils with resource management and guards, and extend test coverage.
1 parent 767cd14 commit 28d2826

File tree

2 files changed

+304
-1
lines changed

2 files changed

+304
-1
lines changed

cobol-parser/src/main/scala/za/co/absa/cobrix/cobol/utils/UsingUtils.scala

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,4 +64,63 @@ object UsingUtils {
6464
}
6565
}
6666
}
67+
68+
/**
69+
* Implicits implementing resource management via for comprehension. Example usage:
70+
* {{{
71+
* import za.co.absa.cobrix.cobol.utils.UsingUtils.Implicits._
72+
*
73+
* for {
74+
* res1 <- new AutoCloseableResource()
75+
* res2 <- new AutoCloseableResource()
76+
* } {
77+
* // Perform operations with the resource
78+
* }
79+
* }}}
80+
*
81+
* or
82+
*
83+
* {{{
84+
* import za.co.absa.cobrix.cobol.utils.UsingUtils.Implicits._
85+
*
86+
* val result = for {
87+
* res1 <- new AutoCloseableResource()
88+
* res2 <- new AutoCloseableResource()
89+
* } yield {
90+
* // Perform operations with the resource, and return a value
91+
* }
92+
* }}}
93+
*/
94+
object Implicits {
95+
implicit class ResourceWrapper[T <: AutoCloseable](private val resource: T) {
96+
def foreach(f: T => Unit): Unit = using(resource)(f)
97+
98+
def map[U](body: T => U): U = using(resource)(body)
99+
100+
def flatMap[U](body: T => U): U = using(resource)(body)
101+
102+
def withFilter(p: T => Boolean): FilteredResourceWrapper[T] = new FilteredResourceWrapper[T](resource, p)
103+
}
104+
}
105+
106+
final class FilteredResourceWrapper[T <: AutoCloseable](private val resource: T,
107+
private val p: T => Boolean) {
108+
def foreach(f: T => Unit): Unit =
109+
using(resource) { r =>
110+
if (p(r)) f(r) else ()
111+
}
112+
113+
def map[U](body: T => U): U =
114+
using(resource) { r =>
115+
if (p(r)) body(r) else throw new NoSuchElementException("withFilter predicate is false")
116+
}
117+
118+
def flatMap[U](body: T => U): U =
119+
using(resource) { r =>
120+
if (p(r)) body(r) else throw new NoSuchElementException("withFilter predicate is false")
121+
}
122+
123+
def withFilter(p2: T => Boolean): FilteredResourceWrapper[T] =
124+
new FilteredResourceWrapper[T](resource, r => p(r) && p2(r))
125+
}
67126
}

cobol-parser/src/test/scala/za/co/absa/cobrix/cobol/utils/UsingUtilsSuite.scala

Lines changed: 245 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,13 @@ package za.co.absa.cobrix.cobol.utils
1919
import org.scalatest.wordspec.AnyWordSpec
2020

2121
class UsingUtilsSuite extends AnyWordSpec {
22+
import UsingUtils.Implicits._
23+
2224
"using with a single resource" should {
2325
"properly close the resource" in {
2426
var resource: AutoCloseableSpy = null
2527

26-
UsingUtils.using(new AutoCloseableSpy()) { res =>
28+
for (res <- new AutoCloseableSpy()) {
2729
resource = res
2830
res.dummyAction()
2931
}
@@ -179,6 +181,28 @@ class UsingUtilsSuite extends AnyWordSpec {
179181
assert(resource2.closeCallCount == 1)
180182
}
181183

184+
"work with for comprehension" in {
185+
var resource1: AutoCloseableSpy = null
186+
var resource2: AutoCloseableSpy = null
187+
188+
val result = for {
189+
res1 <- new AutoCloseableSpy()
190+
res2 <- new AutoCloseableSpy()
191+
} yield {
192+
resource1 = res1
193+
resource2 = res2
194+
res1.dummyAction()
195+
res2.dummyAction()
196+
100
197+
}
198+
199+
assert(result == 100)
200+
assert(resource1.actionCallCount == 1)
201+
assert(resource1.closeCallCount == 1)
202+
assert(resource2.actionCallCount == 1)
203+
assert(resource2.closeCallCount == 1)
204+
}
205+
182206
"properly close both resources when an inner one throws an exception during action and close" in {
183207
var resource1: AutoCloseableSpy = null
184208
var resource2: AutoCloseableSpy = null
@@ -209,6 +233,37 @@ class UsingUtilsSuite extends AnyWordSpec {
209233
assert(resource2.closeCallCount == 1)
210234
}
211235

236+
"properly close both resources when an inner one throws an exception during action and close (for comprehension)" in {
237+
var resource1: AutoCloseableSpy = null
238+
var resource2: AutoCloseableSpy = null
239+
var exceptionThrown = false
240+
241+
try {
242+
for {
243+
res1 <- new AutoCloseableSpy()
244+
res2 <- new AutoCloseableSpy(failAction = true, failClose = true)
245+
} {
246+
resource1 = res1
247+
resource2 = res2
248+
res1.dummyAction()
249+
res2.dummyAction()
250+
}
251+
} catch {
252+
case ex: Throwable =>
253+
exceptionThrown = true
254+
assert(ex.getMessage.contains("Failed during action"))
255+
val suppressed = ex.getSuppressed
256+
assert(suppressed.length == 1)
257+
assert(suppressed(0).getMessage.contains("Failed to close resource"))
258+
}
259+
260+
assert(exceptionThrown)
261+
assert(resource1.actionCallCount == 1)
262+
assert(resource1.closeCallCount == 1)
263+
assert(resource2.actionCallCount == 1)
264+
assert(resource2.closeCallCount == 1)
265+
}
266+
212267
"properly close both resources when an outer one throws an exception during action and close" in {
213268
var resource1: AutoCloseableSpy = null
214269
var resource2: AutoCloseableSpy = null
@@ -239,6 +294,37 @@ class UsingUtilsSuite extends AnyWordSpec {
239294
assert(resource2.closeCallCount == 1)
240295
}
241296

297+
"properly close both resources when an outer one throws an exception during action and close (for comprehension)" in {
298+
var resource1: AutoCloseableSpy = null
299+
var resource2: AutoCloseableSpy = null
300+
var exceptionThrown = false
301+
302+
try {
303+
for {
304+
res1 <- new AutoCloseableSpy(failAction = true, failClose = true)
305+
res2 <- new AutoCloseableSpy()
306+
} {
307+
resource1 = res1
308+
resource2 = res2
309+
res1.dummyAction()
310+
res2.dummyAction()
311+
}
312+
} catch {
313+
case ex: Throwable =>
314+
exceptionThrown = true
315+
assert(ex.getMessage.contains("Failed during action"))
316+
val suppressed = ex.getSuppressed
317+
assert(suppressed.length == 1)
318+
assert(suppressed(0).getMessage.contains("Failed to close resource"))
319+
}
320+
321+
assert(exceptionThrown)
322+
assert(resource1.actionCallCount == 1)
323+
assert(resource1.closeCallCount == 1)
324+
assert(resource2.actionCallCount == 0)
325+
assert(resource2.closeCallCount == 1)
326+
}
327+
242328
"properly close the outer resource when the inner one fails on create" in {
243329
var resource1: AutoCloseableSpy = null
244330
var resource2: AutoCloseableSpy = null
@@ -264,5 +350,163 @@ class UsingUtilsSuite extends AnyWordSpec {
264350
assert(resource1.closeCallCount == 1)
265351
assert(resource2 == null)
266352
}
353+
354+
"properly close the outer resource when the inner one fails on create (for comprehension)" in {
355+
val resource1: AutoCloseableSpy = new AutoCloseableSpy()
356+
var resource2: AutoCloseableSpy = null
357+
var exceptionThrown = false
358+
359+
try {
360+
resource1
361+
.flatMap(res1 =>
362+
new AutoCloseableSpy(failCreate = true)
363+
.map(res2 => {
364+
resource2 = res2
365+
res1.dummyAction()
366+
res2.dummyAction()
367+
})
368+
)
369+
} catch {
370+
case ex: Throwable =>
371+
exceptionThrown = true
372+
assert(ex.getMessage.contains("Failed to create resource"))
373+
}
374+
375+
assert(exceptionThrown)
376+
assert(resource1.actionCallCount == 0)
377+
assert(resource1.closeCallCount == 1)
378+
assert(resource2 == null)
379+
}
380+
}
381+
382+
"withFilter (for-comprehension guards)" should {
383+
"skip the body when the guard is false (foreach form) and still close the resource" in {
384+
val res = new AutoCloseableSpy()
385+
386+
var bodyRan = false
387+
for {
388+
r <- res if false
389+
} {
390+
bodyRan = true
391+
r.dummyAction()
392+
}
393+
394+
assert(!bodyRan)
395+
assert(res.actionCallCount == 0)
396+
assert(res.closeCallCount == 1)
397+
}
398+
399+
"run the body when the guard is true (foreach form) and close the resource" in {
400+
val res = new AutoCloseableSpy()
401+
402+
var bodyRan = false
403+
for {
404+
r <- res if true
405+
} {
406+
bodyRan = true
407+
r.dummyAction()
408+
}
409+
410+
assert(bodyRan)
411+
assert(res.actionCallCount == 1)
412+
assert(res.closeCallCount == 1)
413+
}
414+
415+
"close both resources when an inner guard is false (nested generators)" in {
416+
val r1 = new AutoCloseableSpy()
417+
val r2 = new AutoCloseableSpy()
418+
419+
var bodyRan = false
420+
for {
421+
a <- r1
422+
b <- r2 if false
423+
} {
424+
bodyRan = true
425+
a.dummyAction()
426+
b.dummyAction()
427+
}
428+
429+
assert(!bodyRan)
430+
assert(r1.actionCallCount == 0)
431+
assert(r2.actionCallCount == 0)
432+
assert(r2.closeCallCount == 1)
433+
assert(r1.closeCallCount == 1)
434+
}
435+
436+
"compose multiple guards correctly (both must be true)" in {
437+
val res = new AutoCloseableSpy()
438+
439+
var bodyRan = false
440+
for {
441+
r <- res if true if false
442+
} {
443+
bodyRan = true
444+
r.dummyAction()
445+
}
446+
447+
assert(!bodyRan)
448+
assert(res.actionCallCount == 0)
449+
assert(res.closeCallCount == 1)
450+
}
451+
452+
"not evaluate the body when the first guard is false (side-effect check)" in {
453+
val res = new AutoCloseableSpy()
454+
455+
var sideEffect = 0
456+
for {
457+
r <- res if false if { sideEffect += 1; true }
458+
} {
459+
r.dummyAction()
460+
}
461+
462+
assert(sideEffect == 0)
463+
assert(res.actionCallCount == 0)
464+
assert(res.closeCallCount == 1)
465+
}
466+
467+
"throw on yield when the guard is false (map path) and still close the resource" in {
468+
val res = new AutoCloseableSpy()
469+
470+
var thrown = false
471+
try {
472+
val _ = for {
473+
r <- res if false
474+
} yield {
475+
r.dummyAction()
476+
1
477+
}
478+
} catch {
479+
case _: NoSuchElementException => thrown = true
480+
}
481+
482+
assert(thrown)
483+
assert(res.actionCallCount == 0)
484+
assert(res.closeCallCount == 1)
485+
}
486+
487+
"throw on a guarded middle generator in a yield (flatMap->map path) and close all opened resources" in {
488+
val r1 = new AutoCloseableSpy()
489+
val r2 = new AutoCloseableSpy()
490+
491+
var thrown = false
492+
try {
493+
val _ = for {
494+
a <- r1
495+
b <- r2 if false
496+
} yield {
497+
a.dummyAction()
498+
b.dummyAction()
499+
1
500+
}
501+
} catch {
502+
case _: NoSuchElementException => thrown = true
503+
}
504+
505+
assert(thrown)
506+
assert(r1.actionCallCount == 0)
507+
assert(r2.actionCallCount == 0)
508+
assert(r2.closeCallCount == 1)
509+
assert(r1.closeCallCount == 1)
510+
}
267511
}
268512
}

0 commit comments

Comments
 (0)