pekko/akka-stream-tests/src/test/scala/akka/stream/scaladsl/FlowFlatMapPrefixSpec.scala
Arnout Engelen c41c0420ad
Update scala to 2.13.3 and silencer to 1.7.0 (#28991)
* Update scala to 2.13.3 and silencer to 1.7.0
* Also travis
* Fix various warnings
2020-08-10 12:54:38 +02:00

589 lines
21 KiB
Scala

/*
* Copyright (C) 2019-2020 Lightbend Inc. <https://www.lightbend.com>
*/
package akka.stream.scaladsl
import akka.{ Done, NotUsed }
import akka.stream.{
AbruptStageTerminationException,
AbruptTerminationException,
Attributes,
Materializer,
NeverMaterializedException,
SubscriptionWithCancelException
}
import akka.stream.testkit.{ StreamSpec, TestPublisher, TestSubscriber }
import akka.stream.testkit.Utils.TE
import akka.stream.testkit.scaladsl.StreamTestKit.assertAllStagesStopped
class FlowFlatMapPrefixSpec extends StreamSpec {
def src10(i: Int = 0) = Source(i until (i + 10))
for {
att <- List(
Attributes.NestedMaterializationCancellationPolicy.EagerCancellation,
Attributes.NestedMaterializationCancellationPolicy.PropagateToNested)
delayDownstreanCancellation = att.propagateToNestedMaterialization
attributes = Attributes(att)
} {
s"A PrefixAndDownstream with $att" must {
"work in the simple identity case" in assertAllStagesStopped {
src10()
.flatMapPrefixMat(2) { _ =>
Flow[Int]
}(Keep.left)
.withAttributes(attributes)
.runWith(Sink.seq[Int])
.futureValue should ===(2 until 10)
}
"expose mat value in the simple identity case" in assertAllStagesStopped {
val (prefixF, suffixF) = src10()
.flatMapPrefixMat(2) { prefix =>
Flow[Int].mapMaterializedValue(_ => prefix)
}(Keep.right)
.toMat(Sink.seq)(Keep.both)
.withAttributes(attributes)
.run()
prefixF.futureValue should ===(0 until 2)
suffixF.futureValue should ===(2 until 10)
}
"work when source is exactly the required prefix" in assertAllStagesStopped {
val (prefixF, suffixF) = src10()
.flatMapPrefixMat(10) { prefix =>
Flow[Int].mapMaterializedValue(_ => prefix)
}(Keep.right)
.toMat(Sink.seq)(Keep.both)
.withAttributes(attributes)
.run()
prefixF.futureValue should ===(0 until 10)
suffixF.futureValue should be(empty)
}
"work when source has less than the required prefix" in assertAllStagesStopped {
val (prefixF, suffixF) = src10()
.flatMapPrefixMat(20) { prefix =>
Flow[Int].mapMaterializedValue(_ => prefix)
}(Keep.right)
.toMat(Sink.seq)(Keep.both)
.withAttributes(attributes)
.run()
prefixF.futureValue should ===(0 until 10)
suffixF.futureValue should be(empty)
}
"simple identity case when downstream completes before consuming the entire stream" in assertAllStagesStopped {
val (prefixF, suffixF) = Source(0 until 100)
.flatMapPrefixMat(10) { prefix =>
Flow[Int].mapMaterializedValue(_ => prefix)
}(Keep.right)
.take(10)
.toMat(Sink.seq)(Keep.both)
.withAttributes(attributes)
.run()
prefixF.futureValue should ===(0 until 10)
suffixF.futureValue should ===(10 until 20)
}
"propagate failure to create the downstream flow" in assertAllStagesStopped {
val suffixF = Source(0 until 100)
.flatMapPrefixMat(10) { prefix =>
throw TE(s"I hate mondays! (${prefix.size})")
}(Keep.right)
.to(Sink.ignore)
.withAttributes(attributes)
.run()
val ex = suffixF.failed.futureValue
ex.getCause should not be null
ex.getCause should ===(TE("I hate mondays! (10)"))
}
"propagate flow failures" in assertAllStagesStopped {
val (prefixF, suffixF) = Source(0 until 100)
.flatMapPrefixMat(10) { prefix =>
Flow[Int].mapMaterializedValue(_ => prefix).map {
case 15 => throw TE("don't like 15 either!")
case n => n
}
}(Keep.right)
.toMat(Sink.ignore)(Keep.both)
.withAttributes(attributes)
.run()
prefixF.futureValue should ===(0 until 10)
val ex = suffixF.failed.futureValue
ex should ===(TE("don't like 15 either!"))
}
"produce multiple elements per input" in assertAllStagesStopped {
val (prefixF, suffixF) = src10()
.flatMapPrefixMat(7) { prefix =>
Flow[Int].mapMaterializedValue(_ => prefix).mapConcat(n => List.fill(n - 6)(n))
}(Keep.right)
.toMat(Sink.seq[Int])(Keep.both)
.withAttributes(attributes)
.run()
prefixF.futureValue should ===(0 until 7)
suffixF.futureValue should ===(7 :: 8 :: 8 :: 9 :: 9 :: 9 :: Nil)
}
"succeed when upstream produces no elements" in assertAllStagesStopped {
val (prefixF, suffixF) = Source
.empty[Int]
.flatMapPrefixMat(7) { prefix =>
Flow[Int].mapMaterializedValue(_ => prefix).mapConcat(n => List.fill(n - 6)(n))
}(Keep.right)
.toMat(Sink.seq[Int])(Keep.both)
.withAttributes(attributes)
.run()
prefixF.futureValue should be(empty)
suffixF.futureValue should be(empty)
}
"apply materialized flow's semantics when upstream produces no elements" in assertAllStagesStopped {
val (prefixF, suffixF) = Source
.empty[Int]
.flatMapPrefixMat(7) { prefix =>
Flow[Int].mapMaterializedValue(_ => prefix).mapConcat(n => List.fill(n - 6)(n)).prepend(Source(100 to 101))
}(Keep.right)
.toMat(Sink.seq[Int])(Keep.both)
.withAttributes(attributes)
.run()
prefixF.futureValue should be(empty)
suffixF.futureValue should ===(100 :: 101 :: Nil)
}
"handles upstream completion" in assertAllStagesStopped {
val publisher = TestPublisher.manualProbe[Int]()
val subscriber = TestSubscriber.manualProbe[Int]()
val matValue = Source
.fromPublisher(publisher)
.flatMapPrefixMat(2) { prefix =>
Flow[Int].mapMaterializedValue(_ => prefix).prepend(Source(100 to 101))
}(Keep.right)
.to(Sink.fromSubscriber(subscriber))
.withAttributes(attributes)
.run()
matValue.value should be(empty)
val upstream = publisher.expectSubscription()
val downstream = subscriber.expectSubscription()
downstream.request(1000)
upstream.expectRequest()
//completing publisher
upstream.sendComplete()
matValue.futureValue should ===(Nil)
subscriber.expectNext(100)
subscriber.expectNext(101).expectComplete()
}
"work when materialized flow produces no downstream elements" in assertAllStagesStopped {
val (prefixF, suffixF) = Source(0 until 100)
.flatMapPrefixMat(4) { prefix =>
Flow[Int].mapMaterializedValue(_ => prefix).filter(_ => false)
}(Keep.right)
.toMat(Sink.seq)(Keep.both)
.withAttributes(attributes)
.run()
prefixF.futureValue should ===(0 until 4)
suffixF.futureValue should be(empty)
}
"work when materialized flow does not consume upstream" in assertAllStagesStopped {
val (prefixF, suffixF) = Source(0 until 100)
.map { i =>
i should be <= 4
i
}
.flatMapPrefixMat(4) { prefix =>
Flow[Int].mapMaterializedValue(_ => prefix).take(0)
}(Keep.right)
.toMat(Sink.seq)(Keep.both)
.withAttributes(attributes)
.withAttributes(attributes)
.run()
prefixF.futureValue should ===(0 until 4)
suffixF.futureValue should be(empty)
}
"work when materialized flow cancels upstream but keep producing" in assertAllStagesStopped {
val (prefixF, suffixF) = src10()
.flatMapPrefixMat(4) { prefix =>
Flow[Int].mapMaterializedValue(_ => prefix).take(0).concat(Source(11 to 12))
}(Keep.right)
.toMat(Sink.seq)(Keep.both)
.withAttributes(attributes)
.run()
prefixF.futureValue should ===(0 until 4)
suffixF.futureValue should ===(11 :: 12 :: Nil)
}
"propagate materialization failure (when application of 'f' succeeds)" in assertAllStagesStopped {
val (prefixF, suffixF) = src10()
.flatMapPrefixMat(4) { prefix =>
Flow[Int].mapMaterializedValue(_ => throw TE(s"boom-bada-bang (${prefix.size})"))
}(Keep.right)
.toMat(Sink.seq)(Keep.both)
.withAttributes(attributes)
.run()
prefixF.failed.futureValue should be(a[NeverMaterializedException])
prefixF.failed.futureValue.getCause should ===(TE("boom-bada-bang (4)"))
suffixF.failed.futureValue should ===(TE("boom-bada-bang (4)"))
}
"succeed when materialized flow completes downstream but keep consuming elements" in assertAllStagesStopped {
val (prefixAndTailF, suffixF) = src10()
.flatMapPrefixMat(4) { prefix =>
Flow[Int]
.mapMaterializedValue(_ => prefix)
.viaMat {
Flow.fromSinkAndSourceMat(Sink.seq[Int], Source.empty[Int])(Keep.left)
}(Keep.both)
}(Keep.right)
.toMat(Sink.seq)(Keep.both)
.withAttributes(attributes)
.run()
suffixF.futureValue should be(empty)
val (prefix, suffix) = prefixAndTailF.futureValue
prefix should ===(0 until 4)
suffix.futureValue should ===(4 until 10)
}
"propagate downstream cancellation via the materialized flow" in assertAllStagesStopped {
val publisher = TestPublisher.manualProbe[Int]()
val subscriber = TestSubscriber.manualProbe[Int]()
val ((srcWatchTermF, innerMatVal), sinkMatVal) = src10()
.watchTermination()(Keep.right)
.flatMapPrefixMat(2) { prefix =>
prefix should ===(0 until 2)
Flow.fromSinkAndSource(Sink.fromSubscriber(subscriber), Source.fromPublisher(publisher))
}(Keep.both)
.take(1)
.toMat(Sink.seq)(Keep.both)
.withAttributes(attributes)
.run()
val subUpstream = publisher.expectSubscription()
val subDownstream = subscriber.expectSubscription()
// inner stream was materialized
innerMatVal.futureValue should ===(NotUsed)
subUpstream.expectRequest() should be >= (1L)
subDownstream.request(1)
subscriber.expectNext(2)
subUpstream.sendNext(22)
subUpstream.expectCancellation() // because take(1)
// this should not automatically pass the cancellation upstream of nested flow
srcWatchTermF.isCompleted should ===(false)
sinkMatVal.futureValue should ===(Seq(22))
// the nested flow then decides to cancel, which moves upstream
subDownstream.cancel()
srcWatchTermF.futureValue should ===(Done)
}
"early downstream cancellation is later handed out to materialized flow" in assertAllStagesStopped {
val publisher = TestPublisher.manualProbe[Int]()
val subscriber = TestSubscriber.manualProbe[Int]()
val (srcWatchTermF, matFlowWatchTermFF) = Source
.fromPublisher(publisher)
.watchTermination()(Keep.right)
.flatMapPrefixMat(3) { prefix =>
prefix should ===(0 until 3)
Flow[Int].watchTermination()(Keep.right)
}(Keep.both)
.to(Sink.fromSubscriber(subscriber))
.withAttributes(attributes)
.run()
val matFlowWatchTerm = matFlowWatchTermFF.flatten
matFlowWatchTerm.value should be(empty)
srcWatchTermF.value should be(empty)
val subDownstream = subscriber.expectSubscription()
val subUpstream = publisher.expectSubscription()
subDownstream.request(1)
subUpstream.expectRequest() should be >= (1L)
subUpstream.sendNext(0)
subUpstream.sendNext(1)
subDownstream.cancel()
//subflow not materialized yet, hence mat value (future) isn't ready yet
matFlowWatchTerm.value should be(empty)
if (delayDownstreanCancellation) {
srcWatchTermF.value should be(empty)
//this one is sent AFTER downstream cancellation
subUpstream.sendNext(2)
subUpstream.expectCancellation()
matFlowWatchTerm.futureValue should ===(Done)
srcWatchTermF.futureValue should ===(Done)
} else {
srcWatchTermF.futureValue should ===(Done)
matFlowWatchTerm.failed.futureValue should be(a[NeverMaterializedException])
}
}
"early downstream failure is deferred until prefix completion" in assertAllStagesStopped {
val publisher = TestPublisher.manualProbe[Int]()
val subscriber = TestSubscriber.manualProbe[Int]()
val (srcWatchTermF, matFlowWatchTermFF) = Source
.fromPublisher(publisher)
.watchTermination()(Keep.right)
.flatMapPrefixMat(3) { prefix =>
prefix should ===(0 until 3)
Flow[Int].watchTermination()(Keep.right)
}(Keep.both)
.to(Sink.fromSubscriber(subscriber))
.withAttributes(attributes)
.run()
val matFlowWatchTerm = matFlowWatchTermFF.flatten
matFlowWatchTerm.value should be(empty)
srcWatchTermF.value should be(empty)
val subDownstream = subscriber.expectSubscription()
val subUpstream = publisher.expectSubscription()
subDownstream.request(1)
subUpstream.expectRequest() should be >= (1L)
subUpstream.sendNext(0)
subUpstream.sendNext(1)
subDownstream.asInstanceOf[SubscriptionWithCancelException].cancel(TE("that again?!"))
if (delayDownstreanCancellation) {
matFlowWatchTerm.value should be(empty)
srcWatchTermF.value should be(empty)
subUpstream.sendNext(2)
matFlowWatchTerm.failed.futureValue should ===(TE("that again?!"))
srcWatchTermF.failed.futureValue should ===(TE("that again?!"))
subUpstream.expectCancellation()
} else {
subUpstream.expectCancellation()
srcWatchTermF.failed.futureValue should ===(TE("that again?!"))
matFlowWatchTerm.failed.futureValue should be(a[NeverMaterializedException])
matFlowWatchTerm.failed.futureValue.getCause should ===(TE("that again?!"))
}
}
"downstream failure is propagated via the materialized flow" in assertAllStagesStopped {
val publisher = TestPublisher.manualProbe[Int]()
val subscriber = TestSubscriber.manualProbe[Int]()
val ((srcWatchTermF, notUsedF), suffixF) = src10()
.watchTermination()(Keep.right)
.flatMapPrefixMat(2) { prefix =>
prefix should ===(0 until 2)
Flow.fromSinkAndSourceCoupled(Sink.fromSubscriber(subscriber), Source.fromPublisher(publisher))
}(Keep.both)
.map {
case 2 => 2
case 3 => throw TE("3!?!?!?")
case i => fail(s"unexpected value $i")
}
.toMat(Sink.seq)(Keep.both)
.withAttributes(attributes)
.run()
notUsedF.value should be(empty)
suffixF.value should be(empty)
srcWatchTermF.value should be(empty)
val subUpstream = publisher.expectSubscription()
val subDownstream = subscriber.expectSubscription()
notUsedF.futureValue should ===(NotUsed)
subUpstream.expectRequest() should be >= (1L)
subDownstream.request(1)
subscriber.expectNext(2)
subUpstream.sendNext(2)
subDownstream.request(1)
subscriber.expectNext(3)
subUpstream.sendNext(3)
subUpstream.expectCancellation() should ===(TE("3!?!?!?"))
subscriber.expectError(TE("3!?!?!?"))
suffixF.failed.futureValue should ===(TE("3!?!?!?"))
srcWatchTermF.failed.futureValue should ===(TE("3!?!?!?"))
}
"complete mat value with failures on abrupt termination before materializing the flow" in assertAllStagesStopped {
val mat = Materializer(system)
val publisher = TestPublisher.manualProbe[Int]()
val flow = Source
.fromPublisher(publisher)
.flatMapPrefixMat(2) { prefix =>
fail(s"unexpected prefix (length = ${prefix.size})")
}(Keep.right)
.toMat(Sink.ignore)(Keep.both)
.withAttributes(attributes)
val (prefixF, doneF) = flow.run()(mat)
publisher.expectSubscription()
prefixF.value should be(empty)
doneF.value should be(empty)
mat.shutdown()
prefixF.failed.futureValue match {
case _: AbruptTerminationException =>
case ex: NeverMaterializedException =>
ex.getCause should not be null
ex.getCause should be(a[AbruptTerminationException])
}
doneF.failed.futureValue should be(a[AbruptTerminationException])
}
"respond to abrupt termination after flow materialization" in assertAllStagesStopped {
val mat = Materializer(system)
val countFF = src10()
.flatMapPrefixMat(2) { prefix =>
prefix should ===(0 until 2)
Flow[Int]
.concat(Source.repeat(3))
.fold(0L) {
case (acc, _) => acc + 1
}
.alsoToMat(Sink.head)(Keep.right)
}(Keep.right)
.to(Sink.ignore)
.withAttributes(attributes)
.run()(mat)
val countF = countFF.futureValue
//at this point we know the flow was materialized, now we can stop the materializer
mat.shutdown()
//expect the nested flow to be terminated abruptly.
countF.failed.futureValue should be(a[AbruptStageTerminationException])
}
"behave like via when n = 0" in assertAllStagesStopped {
val (prefixF, suffixF) = src10()
.flatMapPrefixMat(0) { prefix =>
prefix should be(empty)
Flow[Int].mapMaterializedValue(_ => prefix)
}(Keep.right)
.toMat(Sink.seq)(Keep.both)
.withAttributes(attributes)
.run()
prefixF.futureValue should be(empty)
suffixF.futureValue should ===(0 until 10)
}
"behave like via when n = 0 and upstream produces no elements" in assertAllStagesStopped {
val (prefixF, suffixF) = Source
.empty[Int]
.flatMapPrefixMat(0) { prefix =>
prefix should be(empty)
Flow[Int].mapMaterializedValue(_ => prefix)
}(Keep.right)
.toMat(Sink.seq)(Keep.both)
.withAttributes(attributes)
.run()
prefixF.futureValue should be(empty)
suffixF.futureValue should be(empty)
}
"propagate errors during flow's creation when n = 0" in assertAllStagesStopped {
val (prefixF, suffixF) = src10()
.flatMapPrefixMat(0) { prefix =>
prefix should be(empty)
throw TE("not this time my friend!")
}(Keep.right)
.toMat(Sink.seq)(Keep.both)
.withAttributes(attributes)
.run()
prefixF.failed.futureValue should be(a[NeverMaterializedException])
prefixF.failed.futureValue.getCause === (TE("not this time my friend!"))
suffixF.failed.futureValue should ===(TE("not this time my friend!"))
}
"propagate materialization failures when n = 0" in assertAllStagesStopped {
val (prefixF, suffixF) = src10()
.flatMapPrefixMat(0) { prefix =>
prefix should be(empty)
Flow[Int].mapMaterializedValue(_ => throw TE("Bang! no materialization this time"))
}(Keep.right)
.toMat(Sink.seq)(Keep.both)
.withAttributes(attributes)
.run()
prefixF.failed.futureValue should be(a[NeverMaterializedException])
prefixF.failed.futureValue.getCause === (TE("Bang! no materialization this time"))
suffixF.failed.futureValue should ===(TE("Bang! no materialization this time"))
}
"run a detached flow" in assertAllStagesStopped {
val publisher = TestPublisher.manualProbe[Int]()
val subscriber = TestSubscriber.manualProbe[String]()
val detachedFlow = Flow.fromSinkAndSource(Sink.cancelled[Int], Source(List("a", "b", "c"))).via {
Flow.fromSinkAndSource(Sink.fromSubscriber(subscriber), Source.empty[Int])
}
val fHeadOpt = Source
.fromPublisher(publisher)
.flatMapPrefix(2) { prefix =>
prefix should ===(0 until 2)
detachedFlow
}
.withAttributes(attributes)
.runWith(Sink.headOption)
subscriber.expectNoMessage()
val subsc = publisher.expectSubscription()
subsc.expectRequest() should be >= 2L
subsc.sendNext(0)
subscriber.expectNoMessage()
subsc.sendNext(1)
val sinkSubscription = subscriber.expectSubscription()
//this indicates
fHeadOpt.futureValue should be(empty)
//materialize flow immediately cancels upstream
subsc.expectCancellation()
//at this point both ends of the 'external' fow are closed
sinkSubscription.request(10)
subscriber.expectNext("a", "b", "c")
subscriber.expectComplete()
}
}
}
}