-
Notifications
You must be signed in to change notification settings - Fork 59
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Unify tests into common app. Add timing tests for BigPipe.
- Loading branch information
Showing
12 changed files
with
398 additions
and
82 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
34 changes: 13 additions & 21 deletions
34
sample-app-java/test/IntegrationSpec.scala → ...a/com/ybrikman/ping/BaseBigPipeSpec.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,45 +1,37 @@ | ||
import org.specs2.mutable.Specification | ||
import play.api.test.WithBrowser | ||
import scala.collection.JavaConverters._ | ||
package com.ybrikman.ping | ||
|
||
class IntegrationSpec extends Specification { | ||
"Application" should { | ||
import play.api.test.WithBrowser | ||
|
||
"render the page without BigPipe" in new WithBrowser { | ||
/** | ||
* End-to-end tests of BigPipe functionality. The Scala and Java sample apps can extend this trait to run all the tests | ||
* in it. | ||
*/ | ||
trait BaseBigPipeSpec extends PingSpecification { | ||
"The sample app" should { | ||
"render the page without BigPipe" in new WithBrowser(app = createTestComponents().app) { | ||
browser.goTo(s"http://localhost:$port/withoutBigPipe") | ||
browser.$("#profile .id").getTexts.get(0) must equalTo("profile") | ||
} | ||
|
||
"render the page client-side with BigPipe" in new WithBrowser { | ||
"render the page client-side with BigPipe" in new WithBrowser(app = createTestComponents().app) { | ||
browser.goTo(s"http://localhost:$port/withBigPipe") | ||
browser.$("#profile .id").getTexts.get(0) must equalTo("profile") | ||
} | ||
|
||
"render the page server-side with BigPipe" in new WithBrowser { | ||
"render the page server-side with BigPipe" in new WithBrowser(app = createTestComponents().app) { | ||
browser.goTo(s"http://localhost:$port/serverSideRendering") | ||
browser.$("#profile .id").getTexts.get(0) must equalTo("profile") | ||
} | ||
|
||
"render the page client-side with BigPipe and Mustache.js JavaScript templates" in new WithBrowser { | ||
"render the page client-side with BigPipe and Mustache.js JavaScript templates" in new WithBrowser(app = createTestComponents().app) { | ||
browser.goTo(s"http://localhost:$port/clientSideTemplating") | ||
browser.$("#profile .id").getTexts.get(0) must equalTo("profile") | ||
} | ||
|
||
"handle errors while rendering with BigPipe" in new WithBrowser { | ||
"handle errors while rendering with BigPipe" in new WithBrowser(app = createTestComponents().app) { | ||
browser.goTo(s"http://localhost:$port/errorHandling") | ||
browser.$("#profile .id").getTexts.get(0) must equalTo("profile") | ||
browser.$("#feed .id").getTexts.get(0) must equalTo("error") | ||
} | ||
|
||
"dedupe remote calls" in new WithBrowser { | ||
browser.goTo(s"http://localhost:$port/dedupe") | ||
val values = browser.$(".id").getTexts.asScala | ||
|
||
// First 3 values should be the same since they were de-duped, fourth should be different | ||
values must have size 4 | ||
values(0) mustEqual values(1) | ||
values(1) mustEqual values(2) | ||
values(1) mustNotEqual values(3) | ||
} | ||
} | ||
} |
223 changes: 223 additions & 0 deletions
223
sample-app-common/src/test/scala/com/ybrikman/ping/BaseBigPipeTimingSpec.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,223 @@ | ||
package com.ybrikman.ping | ||
|
||
import com.ybrikman.ping.TimingHelper._ | ||
import com.ybrikman.ping.CustomRoutes._ | ||
import com.ybrikman.ping.javaapi.bigpipe.PageletRenderOptions | ||
import com.ybrikman.ping.scalaapi.bigpipe.HtmlStreamImplicits._ | ||
import com.ybrikman.ping.scalaapi.bigpipe._ | ||
import data.FutureUtil | ||
import play.api.libs.concurrent.Execution.Implicits._ | ||
import play.api.libs.iteratee.{Enumerator, Iteratee} | ||
import play.api.libs.ws.WSClient | ||
import play.api.mvc.{Action, Results} | ||
import play.api.routing.Router | ||
import play.api.routing.sird._ | ||
|
||
import scala.concurrent.{ExecutionContext, Future} | ||
|
||
/** | ||
* Tests that BigPipe is actually streaming data as soon as it's available and that chunks are not blocked anywhere. | ||
* The Scala and Java sample apps can extend this trait to run all the tests in it. | ||
*/ | ||
trait BaseBigPipeTimingSpec extends PingSpecification { | ||
"BigPipe streaming" should { | ||
"Send down the data in-order, only after all of it is available, without BigPipe" in new WithWarmedUpPingTestServer(createTestComponents(withRouterToTestTimings)) { | ||
val chunkTimings = getTimings(components.wsClient, s"http://localhost:$port/withoutBigPipe") | ||
chunkTimings must not be empty | ||
|
||
// Time-to-first-byte: make sure the first chunk was sent back after the maxDelay (within a tolerance of | ||
// ToleranceInMillis) | ||
val firstChunk = chunkTimings(0) | ||
firstChunk.content mustEqual FirstChunkContent | ||
firstChunk.timeElapsed must beGreaterThan(maxDelay - ToleranceInMillis) and beLessThan(maxDelay + ToleranceInMillis) | ||
|
||
// Make sure the contents for each pagelet were sent back exactly once | ||
val pageletContentTiming = PageletIndices.flatMap { index => | ||
chunkTimings.filter(_.content.contains(content(index))) | ||
} | ||
pageletContentTiming must have size PageletIndices.size | ||
|
||
// Check that contents for each pagelet were delayed by the slowest pagelet and no more (within a tolerance of | ||
// ToleranceInMillis) | ||
val expectedTimingMatchers = PageletIndices.map { index => | ||
beGreaterThan(maxDelay - ToleranceInMillis) and beLessThan(maxDelay + ToleranceInMillis) | ||
} | ||
pageletContentTiming.map(_.timeElapsed) must contain(eachOf(expectedTimingMatchers:_*)).inOrder | ||
} | ||
|
||
"Send down the data out-of-order, as soon as any of it is available, with client-side streaming" in new WithWarmedUpPingTestServer(createTestComponents(withRouterToTestTimings)) { | ||
val chunkTimings = getTimings(components.wsClient, s"http://localhost:$port/withBigPipeClientSide") | ||
chunkTimings must not be empty | ||
|
||
// Time-to-first-byte: make sure the first chunk was sent back almost immediately | ||
val firstChunk = chunkTimings(0) | ||
firstChunk.content mustEqual FirstChunkContent | ||
firstChunk.timeElapsed must beLessThan(ToleranceInMillis) | ||
|
||
// Placeholders: make sure all the placeholders were sent back almost immediately and exactly once | ||
val pageletPlaceholderTiming = PageletIndices.flatMap { index => | ||
chunkTimings.filter(_.content.contains(placeholder(id(index)))) | ||
} | ||
pageletPlaceholderTiming must have size PageletIndices.size | ||
pageletPlaceholderTiming.map(_.timeElapsed) must contain(beLessThan(ToleranceInMillis)).forall | ||
|
||
// Make sure the contents for each pagelet were sent back exactly once | ||
val pageletContentTiming = PageletIndices.flatMap { index => | ||
chunkTimings.filter(_.content.contains(content(index))) | ||
} | ||
pageletContentTiming must have size PageletIndices.size | ||
|
||
// Check that contents for each pagelet were delayed by no more and no less than | ||
// DELAY_MULTIPLIER_IN_MILLIS (within a tolerance of ToleranceInMillis) | ||
val expectedTimingMatchers = PageletIndices.map { index => | ||
val expecteDelay = delay(index) | ||
beGreaterThan(expecteDelay - ToleranceInMillis) and beLessThan(expecteDelay + ToleranceInMillis) | ||
} | ||
pageletContentTiming.map(_.timeElapsed) must contain(eachOf(expectedTimingMatchers:_*)).inOrder | ||
} | ||
|
||
"Send down the data in-order, as soon as it's available, with server-side streaming" in new WithWarmedUpPingTestServer(createTestComponents(withRouterToTestTimings)) { | ||
val chunkTimings = getTimings(components.wsClient, s"http://localhost:$port/withBigPipeServerSide") | ||
chunkTimings must not be empty | ||
|
||
// Time-to-first-byte: make sure the first chunk was sent back almost immediately | ||
val firstChunk = chunkTimings(0) | ||
firstChunk.content mustEqual FirstChunkContent | ||
firstChunk.timeElapsed must beLessThan(ToleranceInMillis) | ||
|
||
// Make sure the contents for each pagelet were sent back exactly once | ||
val pageletContentTiming = PageletIndices.flatMap { index => | ||
chunkTimings.filter(_.content.contains(content(index))) | ||
} | ||
pageletContentTiming must have size PageletIndices.size | ||
|
||
// Check that contents for each pagelet were delayed by the slowest pagelet and no more (within a tolerance of | ||
// ToleranceInMillis) | ||
val expectedTimingMatchers = PageletIndices.map { index => | ||
beGreaterThan(maxDelay - ToleranceInMillis) and beLessThan(maxDelay + ToleranceInMillis) | ||
} | ||
pageletContentTiming.map(_.timeElapsed) must contain(eachOf(expectedTimingMatchers:_*)).inOrder | ||
} | ||
} | ||
|
||
private def getTimings(wsClient: WSClient, url: String): Seq[Timing] = { | ||
val initialTimings = Timings() | ||
val (_, bodyEnumerator) = await(wsClient.url(url).getStream()) | ||
|
||
val checkTiming = Iteratee.fold[Array[Byte], Timings](initialTimings) { (timings, chunk) => | ||
timings.addChunk(chunk) | ||
} | ||
|
||
val timings = await(bodyEnumerator.run(checkTiming)) | ||
|
||
// Useful for debugging | ||
println(s"Timings and content for url $url:\n") | ||
timings.chunkTimings.foreach(timing => println(s"----- ${timing.timeElapsed} ms -----\n\n${timing.content}\n\n")) | ||
|
||
timings.chunkTimings | ||
} | ||
} | ||
|
||
object TimingHelper { | ||
val PageletIndices = 1 until 5 | ||
|
||
// Each pagelet id will have this prefix to make it easier to find in the stream of data | ||
val IdPrefix = "pagelet_id_" | ||
|
||
// The contents of each pagelet will have this prefix to make it easier to find in the stream of data | ||
val ContentPrefix = "pagelet_content_" | ||
|
||
// The placeholder for each pagelet will have this prefix to make it easier to find in the stream of data | ||
val PlaceHolderPrefix = "pagelet_placeholder_" | ||
|
||
// The contents of the very first chunk that should be sent back by each page | ||
val FirstChunkContent = "first_chunk_content" | ||
|
||
// Each pagelet will be delayed by this many milliseconds | ||
val DelayMultiplierInMillis = 3000L | ||
|
||
// With tests running in parallel, things may get a bit delayed, so check all timings within this tolerance | ||
val ToleranceInMillis = DelayMultiplierInMillis / 10 | ||
|
||
def id(index: Int): String = { | ||
s"$IdPrefix$index" | ||
} | ||
|
||
def content(index: Int): String = { | ||
s"$ContentPrefix$index" | ||
} | ||
|
||
def delay(index: Int): Long = { | ||
(PageletIndices.size - index) * DelayMultiplierInMillis | ||
} | ||
|
||
def maxDelay: Long = { | ||
delay(PageletIndices.head) | ||
} | ||
|
||
def placeholder(id: String): String = { | ||
s"$PlaceHolderPrefix$id" | ||
} | ||
} | ||
|
||
case class Timings(startTime: Long = System.currentTimeMillis(), chunkTimings: Seq[Timing] = Seq.empty) { | ||
def addChunk(contents: Array[Byte]): Timings = { | ||
val timeElapsed = System.currentTimeMillis() - startTime | ||
copy(chunkTimings = chunkTimings :+ Timing(new String(contents, "UTF-8"), timeElapsed)) | ||
} | ||
} | ||
|
||
case class Timing(content: String, timeElapsed: Long) | ||
|
||
class MockTextPagelet(id: String, content: Future[String]) extends TextPagelet(id, content) { | ||
override def renderPlaceholder(implicit ec: ExecutionContext): HtmlStream = { | ||
HtmlStream.fromHtml(com.ybrikman.bigpipe.html.pageletServerSide(placeholder(id), PageletConstants.EmptyContent)) | ||
} | ||
} | ||
|
||
object CustomRoutes { | ||
def withRouterToTestTimings: Option[RouterComponents => Router] = { | ||
def createRoutes(routerComponents: RouterComponents): Router = { | ||
val futureUtil = new FutureUtil(routerComponents.actorSystem) | ||
|
||
Router.from { | ||
case GET(p"/withoutBigPipe") => Action.async { | ||
val futures = mockRemoteServiceCalls(futureUtil).map(_._2) | ||
Future.sequence(futures).map { contents => | ||
Results.Ok.chunked(Enumerator(FirstChunkContent).andThen(Enumerator(contents:_*))) | ||
} | ||
} | ||
case GET(p"/withBigPipeClientSide") => Action { | ||
val pagelets = mockRemoteServiceCalls(futureUtil).map { case (id, data) => new MockTextPagelet(id, data) } | ||
Results.Ok.chunked(renderPagelets(PageletRenderOptions.ClientSide, pagelets)) | ||
} | ||
case GET(p"/withBigPipeServerSide") => Action { | ||
val pagelets = mockRemoteServiceCalls(futureUtil).map { case (id, data) => new MockTextPagelet(id, data) } | ||
Results.Ok.chunked(renderPagelets(PageletRenderOptions.ServerSide, pagelets)) | ||
} | ||
case GET(p"/warmup") => Action { | ||
Results.Ok("warmup") | ||
} | ||
} | ||
} | ||
|
||
Option(createRoutes _) | ||
} | ||
|
||
// Generate a series of Futures that represent remote calls. The Futures are returned in reverse order, from slowest | ||
// to fastest, as a way to demonstrate the advantages of out-of-order client-side rendering. | ||
private def mockRemoteServiceCalls(futureUtil: FutureUtil): Seq[(String, Future[String])] = { | ||
PageletIndices.map { index => | ||
id(index) -> futureUtil.timeout(content(index), delay(index)) | ||
} | ||
} | ||
|
||
private def renderPagelets(renderOptions: PageletRenderOptions, pagelets: Seq[Pagelet]): HtmlStream = { | ||
val bigPipe = new BigPipe(renderOptions, pagelets:_*) | ||
bigPipe.render { renderedPagelets => | ||
pagelets.foldLeft(HtmlStream.fromString(FirstChunkContent)) { (stream, pagelet) => | ||
stream.andThen(renderedPagelets(pagelet.id)) | ||
} | ||
} | ||
} | ||
} |
23 changes: 23 additions & 0 deletions
23
sample-app-common/src/test/scala/com/ybrikman/ping/BaseDedupeSpec.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
package com.ybrikman.ping | ||
|
||
import play.api.test.WithBrowser | ||
import scala.collection.JavaConverters._ | ||
|
||
/** | ||
* An end-to-end test of the de-duping cache. The Scala and Java sample apps can extend this trait to run all the tests | ||
* in it. | ||
*/ | ||
trait BaseDedupeSpec extends PingSpecification { | ||
"The Deduping controller" should { | ||
"dedupe remote calls" in new WithBrowser(app = createTestComponents().app) { | ||
browser.goTo(s"http://localhost:$port/dedupe") | ||
val values = browser.$(".id").getTexts.asScala | ||
|
||
// First 3 values should be the same since they were de-duped, fourth should be different | ||
values must have size 4 | ||
values(0) mustEqual values(1) | ||
values(1) mustEqual values(2) | ||
values(1) mustNotEqual values(3) | ||
} | ||
} | ||
} |
Oops, something went wrong.