A SubFlow (or SubSource) is not a Graph, it is an unfinished builder that accepts transformations. This allows us to capture the substreams’ transformations before materializing the flow, which will be very helpful in fully fusing all operators. Another change is that groupBy now requires a maxSubstreams parameter in order to bound its resource usage. In exchange the matching merge can be unbounded. This trades silent deadlock for explicit stream failure. This commit also changes all uses of Predef.identity to use `conforms` and removes the HTTP impl.util.identityFunc.
178 lines
13 KiB
Scala
178 lines
13 KiB
Scala
/*
|
|
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
|
*/
|
|
|
|
package akka.http.scaladsl.coding
|
|
|
|
import java.io.{ OutputStream, InputStream, ByteArrayInputStream, ByteArrayOutputStream }
|
|
import java.util
|
|
import java.util.zip.DataFormatException
|
|
import scala.annotation.tailrec
|
|
import scala.concurrent.duration._
|
|
import scala.concurrent.Await
|
|
import scala.concurrent.ExecutionContext.Implicits.global
|
|
import scala.concurrent.forkjoin.ThreadLocalRandom
|
|
import scala.util.Random
|
|
import scala.util.control.NoStackTrace
|
|
import org.scalatest.{ Inspectors, WordSpec }
|
|
import akka.util.ByteString
|
|
import akka.stream.scaladsl.{ Sink, Source }
|
|
import akka.http.scaladsl.model.{ HttpEntity, HttpRequest }
|
|
import akka.http.scaladsl.model.HttpMethods._
|
|
import akka.http.impl.util._
|
|
|
|
abstract class CoderSpec extends WordSpec with CodecSpecSupport with Inspectors {
|
|
protected def Coder: Coder with StreamDecoder
|
|
protected def newDecodedInputStream(underlying: InputStream): InputStream
|
|
protected def newEncodedOutputStream(underlying: OutputStream): OutputStream
|
|
|
|
case object AllDataAllowed extends Exception with NoStackTrace
|
|
protected def corruptInputCheck: Boolean = true
|
|
|
|
def extraTests(): Unit = {}
|
|
|
|
s"The ${Coder.encoding.value} codec" should {
|
|
"properly encode a small string" in {
|
|
streamDecode(ourEncode(smallTextBytes)) should readAs(smallText)
|
|
}
|
|
"properly decode a small string" in {
|
|
ourDecode(streamEncode(smallTextBytes)) should readAs(smallText)
|
|
}
|
|
"properly round-trip encode/decode a small string" in {
|
|
ourDecode(ourEncode(smallTextBytes)) should readAs(smallText)
|
|
}
|
|
"properly encode a large string" in {
|
|
streamDecode(ourEncode(largeTextBytes)) should readAs(largeText)
|
|
}
|
|
"properly decode a large string" in {
|
|
ourDecode(streamEncode(largeTextBytes)) should readAs(largeText)
|
|
}
|
|
"properly round-trip encode/decode a large string" in {
|
|
ourDecode(ourEncode(largeTextBytes)) should readAs(largeText)
|
|
}
|
|
"properly round-trip encode/decode an HttpRequest" in {
|
|
val request = HttpRequest(POST, entity = HttpEntity(largeText))
|
|
Coder.decode(Coder.encode(request)).toStrict(1.second).awaitResult(1.second) should equal(request)
|
|
}
|
|
|
|
if (corruptInputCheck) {
|
|
"throw an error on corrupt input" in {
|
|
(the[RuntimeException] thrownBy {
|
|
ourDecode(corruptContent)
|
|
}).getCause should be(a[DataFormatException])
|
|
}
|
|
}
|
|
|
|
"not throw an error if a subsequent block is corrupt" in {
|
|
pending // FIXME: should we read as long as possible and only then report an error, that seems somewhat arbitrary
|
|
ourDecode(Seq(encode("Hello,"), encode(" dear "), corruptContent).join) should readAs("Hello, dear ")
|
|
}
|
|
"decompress in very small chunks" in {
|
|
val compressed = encode("Hello")
|
|
|
|
decodeChunks(Source(Vector(compressed.take(10), compressed.drop(10)))) should readAs("Hello")
|
|
}
|
|
"support chunked round-trip encoding/decoding" in {
|
|
val chunks = largeTextBytes.grouped(512).toVector
|
|
val comp = Coder.newCompressor
|
|
val compressedChunks = chunks.map { chunk ⇒ comp.compressAndFlush(chunk) } :+ comp.finish()
|
|
val uncompressed = decodeFromIterator(() ⇒ compressedChunks.iterator)
|
|
|
|
uncompressed should readAs(largeText)
|
|
}
|
|
"works for any split in prefix + suffix" in {
|
|
val compressed = streamEncode(smallTextBytes)
|
|
def tryWithPrefixOfSize(prefixSize: Int): Unit = {
|
|
val prefix = compressed.take(prefixSize)
|
|
val suffix = compressed.drop(prefixSize)
|
|
|
|
decodeChunks(Source(prefix :: suffix :: Nil)) should readAs(smallText)
|
|
}
|
|
(0 to compressed.size).foreach(tryWithPrefixOfSize)
|
|
}
|
|
"works for chunked compressed data of sizes just above 1024" in {
|
|
val comp = Coder.newCompressor
|
|
val inputBytes = ByteString("""{"baseServiceURL":"http://www.acme.com","endpoints":{"assetSearchURL":"/search","showsURL":"/shows","mediaContainerDetailURL":"/container","featuredTapeURL":"/tape","assetDetailURL":"/asset","moviesURL":"/movies","recentlyAddedURL":"/recent","topicsURL":"/topics","scheduleURL":"/schedule"},"urls":{"aboutAweURL":"www.foobar.com"},"channelName":"Cool Stuff","networkId":"netId","slotProfile":"slot_1","brag":{"launchesUntilPrompt":10,"daysUntilPrompt":5,"launchesUntilReminder":5,"daysUntilReminder":2},"feedbackEmailAddress":"feedback@acme.com","feedbackEmailSubject":"Commends from User","splashSponsor":[],"adProvider":{"adProviderProfile":"","adProviderProfileAndroid":"","adProviderNetworkID":0,"adProviderSiteSectionNetworkID":0,"adProviderVideoAssetNetworkID":0,"adProviderSiteSectionCustomID":{},"adProviderServerURL":"","adProviderLiveVideoAssetID":""},"update":[{"forPlatform":"ios","store":{"iTunes":"www.something.com"},"minVer":"1.2.3","notificationVer":"1.2.5"},{"forPlatform":"android","store":{"amazon":"www.something.com","play":"www.something.com"},"minVer":"1.2.3","notificationVer":"1.2.5"}],"tvRatingPolicies":[{"type":"sometype","imageKey":"tv_rating_small","durationMS":15000,"precedence":1},{"type":"someothertype","imageKey":"tv_rating_big","durationMS":15000,"precedence":2}],"exts":{"adConfig":{"globals":{"#{adNetworkID}":"2620","#{ssid}":"usa_tveapp"},"iPad":{"showlist":{"adMobAdUnitID":"/2620/usa_tveapp_ipad/shows","adSize":[{"#{height}":90,"#{width}":728}]},"launch":{"doubleClickCallbackURL":"http://pubads.g.doubleclick.net/gampad/ad?iu=/2620/usa_tveapp_ipad&sz=1x1&t=&c=#{doubleclickrandom}"},"watchwithshowtile":{"adMobAdUnitID":"/2620/usa_tveapp_ipad/watchwithshowtile","adSize":[{"#{height}":120,"#{width}":240}]},"showpage":{"doubleClickCallbackURL":"http://pubads.g.doubleclick.net/gampad/ad?iu=/2620/usa_tveapp_ipad/shows/#{SHOW_NAME}&sz=1x1&t=&c=#{doubleclickrandom}"}},"iPadRetina":{"showlist":{"adMobAdUnitID":"/2620/usa_tveapp_ipad/shows","adSize":[{"#{height}":90,"#{width}":728}]},"launch":{"doubleClickCallbackURL":"http://pubads.g.doubleclick.net/gampad/ad?iu=/2620/usa_tveapp_ipad&sz=1x1&t=&c=#{doubleclickrandom}"},"watchwithshowtile":{"adMobAdUnitID":"/2620/usa_tveapp_ipad/watchwithshowtile","adSize":[{"#{height}":120,"#{width}":240}]},"showpage":{"doubleClickCallbackURL":"http://pubads.g.doubleclick.net/gampad/ad?iu=/2620/usa_tveapp_ipad/shows/#{SHOW_NAME}&sz=1x1&t=&c=#{doubleclickrandom}"}},"iPhone":{"home":{"adMobAdUnitID":"/2620/usa_tveapp_iphone/home","adSize":[{"#{height}":50,"#{width}":300},{"#{height}":50,"#{width}":320}]},"showlist":{"adMobAdUnitID":"/2620/usa_tveapp_iphone/shows","adSize":[{"#{height}":50,"#{width}":300},{"#{height}":50,"#{width}":320}]},"episodepage":{"adMobAdUnitID":"/2620/usa_tveapp_iphone/shows/#{SHOW_NAME}","adSize":[{"#{height}":50,"#{width}":300},{"#{height}":50,"#{width}":320}]},"launch":{"doubleClickCallbackURL":"http://pubads.g.doubleclick.net/gampad/ad?iu=/2620/usa_tveapp_iphone&sz=1x1&t=&c=#{doubleclickrandom}"},"showpage":{"doubleClickCallbackURL":"http://pubads.g.doubleclick.net/gampad/ad?iu=/2620/usa_tveapp_iphone/shows/#{SHOW_NAME}&sz=1x1&t=&c=#{doubleclickrandom}"}},"iPhoneRetina":{"home":{"adMobAdUnitID":"/2620/usa_tveapp_iphone/home","adSize":[{"#{height}":50,"#{width}":300},{"#{height}":50,"#{width}":320}]},"showlist":{"adMobAdUnitID":"/2620/usa_tveapp_iphone/shows","adSize":[{"#{height}":50,"#{width}":300},{"#{height}":50,"#{width}":320}]},"episodepage":{"adMobAdUnitID":"/2620/usa_tveapp_iphone/shows/#{SHOW_NAME}","adSize":[{"#{height}":50,"#{width}":300},{"#{height}":50,"#{width}":320}]},"launch":{"doubleClickCallbackURL":"http://pubads.g.doubleclick.net/gampad/ad?iu=/2620/usa_tveapp_iphone&sz=1x1&t=&c=#{doubleclickrandom}"},"showpage":{"doubleClickCallbackURL":"http://pubads.g.doubleclick.net/gampad/ad?iu=/2620/usa_tveapp_iphone/shows/#{SHOW_NAME}&sz=1x1&t=&c=#{doubleclickrandom}"}},"Tablet":{"home":{"adMobAdUnitID":"/2620/usa_tveapp_androidtab/home","adSize":[{"#{height}":90,"#{width}":728},{"#{height}":50,"#{width}":320},{"#{height}":50,"#{width}":300}]},"showlist":{"adMobAdUnitID":"/2620/usa_tveapp_androidtab/shows","adSize":[{"#{height}":90,"#{width}":728},{"#{height}":50,"#{width}":320},{"#{height}":50,"#{width}":300}]},"episodepage":{"adMobAdUnitID":"/2620/usa_tveapp_androidtab/shows/#{SHOW_NAME}","adSize":[{"#{height}":90,"#{width}":728},{"#{height}":50,"#{width}":320},{"#{height}":50,"#{width}":300}]},"launch":{"doubleClickCallbackURL":"http://pubads.g.doubleclick.net/gampad/ad?iu=/2620/usa_tveapp_androidtab&sz=1x1&t=&c=#{doubleclickrandom}"},"showpage":{"doubleClickCallbackURL":"http://pubads.g.doubleclick.net/gampad/ad?iu=/2620/usa_tveapp_androidtab/shows/#{SHOW_NAME}&sz=1x1&t=&c=#{doubleclickrandom}"}},"TabletHD":{"home":{"adMobAdUnitID":"/2620/usa_tveapp_androidtab/home","adSize":[{"#{height}":90,"#{width}":728},{"#{height}":50,"#{width}":320},{"#{height}":50,"#{width}":300}]},"showlist":{"adMobAdUnitID":"/2620/usa_tveapp_androidtab/shows","adSize":[{"#{height}":90,"#{width}":728},{"#{height}":50,"#{width}":320},{"#{height}":50,"#{width}":300}]},"episodepage":{"adMobAdUnitID":"/2620/usa_tveapp_androidtab/shows/#{SHOW_NAME}","adSize":[{"#{height}":90,"#{width}":728},{"#{height}":50,"#{width}":320},{"#{height}":50,"#{width}":300}]},"launch":{"doubleClickCallbackURL":"http://pubads.g.doubleclick.net/gampad/ad?iu=/2620/usa_tveapp_androidtab&sz=1x1&t=&c=#{doubleclickrandom}"},"showpage":{"doubleClickCallbackURL":"http://pubads.g.doubleclick.net/gampad/ad?iu=/2620/usa_tveapp_androidtab/shows/#{SHOW_NAME}&sz=1x1&t=&c=#{doubleclickrandom}"}},"Phone":{"home":{"adMobAdUnitID":"/2620/usa_tveapp_android/home","adSize":[{"#{height}":50,"#{width}":300},{"#{height}":50,"#{width}":320}]},"showlist":{"adMobAdUnitID":"/2620/usa_tveapp_android/shows","adSize":[{"#{height}":50,"#{width}":300},{"#{height}":50,"#{width}":320}]},"episodepage":{"adMobAdUnitID":"/2620/usa_tveapp_android/shows/#{SHOW_NAME}","adSize":[{"#{height}":50,"#{width}":300},{"#{height}":50,"#{width}":320}]},"launch":{"doubleClickCallbackURL":"http://pubads.g.doubleclick.net/gampad/ad?iu=/2620/usa_tveapp_android&sz=1x1&t=&c=#{doubleclickrandom}"},"showpage":{"doubleClickCallbackURL":"http://pubads.g.doubleclick.net/gampad/ad?iu=/2620/usa_tveapp_android/shows/#{SHOW_NAME}&sz=1x1&t=&c=#{doubleclickrandom}"}},"PhoneHD":{"home":{"adMobAdUnitID":"/2620/usa_tveapp_android/home","adSize":[{"#{height}":50,"#{width}":300},{"#{height}":50,"#{width}":320}]},"showlist":{"adMobAdUnitID":"/2620/usa_tveapp_android/shows","adSize":[{"#{height}":50,"#{width}":300},{"#{height}":50,"#{width}":320}]},"episodepage":{"adMobAdUnitID":"/2620/usa_tveapp_android/shows/#{SHOW_NAME}","adSize":[{"#{height}":50,"#{width}":300},{"#{height}":50,"#{width}":320}]},"launch":{"doubleClickCallbackURL":"http://pubads.g.doubleclick.net/gampad/ad?iu=/2620/usa_tveapp_android&sz=1x1&t=&c=#{doubleclickrandom}"},"showpage":{"doubleClickCallbackURL":"http://pubads.g.doubleclick.net/gampad/ad?iu=/2620/usa_tveapp_android/shows/#{SHOW_NAME}&sz=1x1&t=&c=#{doubleclickrandom}"}}}}}""", "utf8")
|
|
val compressed = comp.compressAndFinish(inputBytes)
|
|
|
|
ourDecode(compressed) should equal(inputBytes)
|
|
}
|
|
|
|
"shouldn't produce huge ByteStrings for some input" in {
|
|
val array = new Array[Byte](10) // FIXME
|
|
util.Arrays.fill(array, 1.toByte)
|
|
val compressed = streamEncode(ByteString(array))
|
|
val limit = 10000
|
|
val resultBs =
|
|
Source.single(compressed)
|
|
.via(Coder.withMaxBytesPerChunk(limit).decoderFlow)
|
|
.grouped(4200).runWith(Sink.head)
|
|
.awaitResult(1.second)
|
|
|
|
forAll(resultBs) { bs ⇒
|
|
bs.length should be < limit
|
|
bs.forall(_ == 1) should equal(true)
|
|
}
|
|
}
|
|
|
|
"be able to decode chunk-by-chunk (depending on input chunks)" in {
|
|
val minLength = 100
|
|
val maxLength = 1000
|
|
val numElements = 1000
|
|
|
|
val random = ThreadLocalRandom.current()
|
|
val sizes = Seq.fill(numElements)(random.nextInt(minLength, maxLength))
|
|
def createByteString(size: Int): ByteString =
|
|
ByteString(Array.fill(size)(1.toByte))
|
|
|
|
val sizesAfterRoundtrip =
|
|
Source(() ⇒ sizes.toIterator.map(createByteString))
|
|
.via(Coder.encoderFlow)
|
|
.via(Coder.decoderFlow)
|
|
.runFold(Seq.empty[Int])(_ :+ _.size)
|
|
|
|
sizes shouldEqual sizesAfterRoundtrip.awaitResult(1.second)
|
|
}
|
|
|
|
extraTests()
|
|
}
|
|
|
|
def encode(s: String) = ourEncode(ByteString(s, "UTF8"))
|
|
def ourEncode(bytes: ByteString): ByteString = Coder.encode(bytes)
|
|
def ourDecode(bytes: ByteString): ByteString = Coder.decode(bytes).awaitResult(1.second)
|
|
|
|
lazy val corruptContent = {
|
|
val content = encode(largeText).toArray
|
|
content(14) = 26.toByte
|
|
ByteString(content)
|
|
}
|
|
|
|
def streamEncode(bytes: ByteString): ByteString = {
|
|
val output = new ByteArrayOutputStream()
|
|
val gos = newEncodedOutputStream(output); gos.write(bytes.toArray); gos.close()
|
|
ByteString(output.toByteArray)
|
|
}
|
|
|
|
def streamDecode(bytes: ByteString): ByteString = {
|
|
val output = new ByteArrayOutputStream()
|
|
val input = newDecodedInputStream(new ByteArrayInputStream(bytes.toArray))
|
|
|
|
val buffer = new Array[Byte](500)
|
|
@tailrec def copy(from: InputStream, to: OutputStream): Unit = {
|
|
val read = from.read(buffer)
|
|
if (read >= 0) {
|
|
to.write(buffer, 0, read)
|
|
copy(from, to)
|
|
}
|
|
}
|
|
|
|
copy(input, output)
|
|
ByteString(output.toByteArray)
|
|
}
|
|
|
|
def decodeChunks(input: Source[ByteString, Unit]): ByteString =
|
|
input.via(Coder.decoderFlow).join.awaitResult(3.seconds)
|
|
|
|
def decodeFromIterator(iterator: () ⇒ Iterator[ByteString]): ByteString =
|
|
Await.result(Source(iterator).via(Coder.decoderFlow).join, 3.seconds)
|
|
}
|