diff --git a/.travis.yml b/.travis.yml index 93c6f69da..47aaf5ac4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -86,14 +86,14 @@ jobs: if: tag =~ ^v script: - echo "$DOCKER_PASSWORD" | docker login -u cloudstatebot --password-stdin - - sbt "set concurrentRestrictions in Global += Tags.limitAll(1)" "dockerBuildAllNonNative publish" operator/docker:publish + - sbt "set concurrentRestrictions in Global += Tags.limitAll(1)" "dockerBuildAllNonNative publish" operator/docker:publish tck/docker:publish - stage: Deploy name: Publish latest builds if: branch = master AND type = push script: - echo "$DOCKER_PASSWORD" | docker login -u cloudstatebot --password-stdin - - sbt -Duse.native.builds=false -Ddocker.tag=latest "set concurrentRestrictions in Global += Tags.limitAll(1)" "dockerBuildAllNonNative publish" operator/docker:publish + - sbt -Duse.native.builds=false -Ddocker.tag=latest "set concurrentRestrictions in Global += Tags.limitAll(1)" "dockerBuildAllNonNative publish" operator/docker:publish tck/docker:publish cache: directories: diff --git a/build.sbt b/build.sbt index 36577f90e..905ed2499 100644 --- a/build.sbt +++ b/build.sbt @@ -154,8 +154,7 @@ lazy val protocols = (project in file("protocols")) IO.zip( archiveStructure(cloudstateProtocolsName, (base / "frontend" ** "*.proto" +++ - base / "protocol" ** "*.proto" +++ - base / "proxy" ** "*.proto")), + base / "protocol" ** "*.proto")), cloudstateProtos ) @@ -456,7 +455,7 @@ lazy val `proxy-core` = (project in file("proxy/core")) }, PB.protoSources in Compile ++= { val baseDir = (baseDirectory in ThisBuild).value / "protocols" - Seq(baseDir / "proxy", baseDir / "frontend", baseDir / "protocol") + Seq(baseDir / "frontend", baseDir / "protocol") }, PB.protoSources in Test ++= { val baseDir = (baseDirectory in ThisBuild).value / "protocols" @@ -796,13 +795,13 @@ lazy val `load-generator` = (project in file("samples/js-shopping-cart-load-gene ) lazy val `tck` = (project in file("tck")) - .enablePlugins(AkkaGrpcPlugin) + .enablePlugins(AkkaGrpcPlugin, JavaAppPackaging, DockerPlugin) .configs(IntegrationTest) .dependsOn(`akka-client`) .settings( Defaults.itSettings, common, - name := "tck", + name := "cloudstate-tck", libraryDependencies ++= Seq( akkaDependency("akka-stream"), akkaDependency("akka-discovery"), @@ -814,8 +813,11 @@ lazy val `tck` = (project in file("tck")) ), PB.protoSources in Compile ++= { val baseDir = (baseDirectory in ThisBuild).value / "protocols" - Seq(baseDir / "proxy", baseDir / "protocol") + Seq(baseDir / "protocol") }, + dockerSettings, + Compile / bashScriptDefines / mainClass := Some("org.scalatest.run"), + bashScriptExtraDefines += "addApp io.cloudstate.tck.ConfiguredCloudStateTCK", javaOptions in IntegrationTest := sys.props.get("config.resource").map(r => s"-Dconfig.resource=$r").toSeq, parallelExecution in IntegrationTest := false, executeTests in IntegrationTest := (executeTests in IntegrationTest) diff --git a/tck/src/it/resources/reference.conf b/tck/src/it/resources/reference.conf new file mode 100644 index 000000000..6d7e2ad6e --- /dev/null +++ b/tck/src/it/resources/reference.conf @@ -0,0 +1,39 @@ +cloudstate-tck { + #verify is a list of names of combinations to run for the TCK + verify = [] + #combinations is a list of config objects with a name, a proxy, and a frontend + combinations = [] + + tck { + hostname = "127.0.0.1" + hostname = ${?HOST} + port = 8090 + } + + proxy { + hostname = "127.0.0.1" + hostname = ${?HOST} + port = 9000 + directory = ${user.dir} + command = [] + stop-command = [] + env-vars { + } + # If specified, will start a docker container, with the environment variable USER_FUNCTION_HOST set to the host + # to connect to, USER_FUNCTION_PORT set to the port to connect to, and HTTP_PORT set to the port above. + docker-image = "" + } + + frontend { + hostname = "127.0.0.1" + hostname = ${?HOST} + port = 8080 + directory = ${user.dir} + command = [] + stop-command = [] + env-vars { + } + # If specified, will start a docker container, with the environment variable PORT set to the port above. + docker-image = "" + } +} \ No newline at end of file diff --git a/tck/src/it/scala/io/cloudstate/tck/TCK.scala b/tck/src/it/scala/io/cloudstate/tck/TCK.scala index 8928ecda6..f8b074848 100644 --- a/tck/src/it/scala/io/cloudstate/tck/TCK.scala +++ b/tck/src/it/scala/io/cloudstate/tck/TCK.scala @@ -1,7 +1,7 @@ package io.cloudstate.tck import org.scalatest._ -import com.typesafe.config.{Config, ConfigFactory} +import com.typesafe.config.ConfigFactory import scala.collection.JavaConverters._ @@ -21,6 +21,34 @@ class TCK extends Suites({ iterator. asScala. filter(section => verify(section.getString("name"))). - map(c => new CloudStateTCK(TckConfiguration.fromConfig(c))). + map(c => new ManagedCloudStateTCK(TckConfiguration.fromConfig(c))). toVector }: _*) with SequentialNestedSuiteExecution + +object ManagedCloudStateTCK { + def settings(config: TckConfiguration): CloudStateTCK.Settings = { + CloudStateTCK.Settings( + CloudStateTCK.Address(config.tckHostname, config.tckPort), + CloudStateTCK.Address(config.proxy.hostname, config.proxy.port), + CloudStateTCK.Address(config.frontend.hostname, config.frontend.port) + ) + } +} + +class ManagedCloudStateTCK(config: TckConfiguration) extends CloudStateTCK("for " + config.name, ManagedCloudStateTCK.settings(config)) { + config.validate() + + val processes: TckProcesses = TckProcesses.create(config) + + override def beforeAll(): Unit = { + processes.frontend.start() + super.beforeAll() + processes.proxy.start() + } + + override def afterAll(): Unit = { + try Option(processes).foreach(_.proxy.stop()) + finally try Option(processes).foreach(_.frontend.stop()) + finally super.afterAll() + } +} diff --git a/tck/src/main/scala/io/cloudstate/tck/TckConfiguration.scala b/tck/src/it/scala/io/cloudstate/tck/TckConfiguration.scala similarity index 100% rename from tck/src/main/scala/io/cloudstate/tck/TckConfiguration.scala rename to tck/src/it/scala/io/cloudstate/tck/TckConfiguration.scala diff --git a/tck/src/main/scala/io/cloudstate/tck/TckProcesses.scala b/tck/src/it/scala/io/cloudstate/tck/TckProcesses.scala similarity index 100% rename from tck/src/main/scala/io/cloudstate/tck/TckProcesses.scala rename to tck/src/it/scala/io/cloudstate/tck/TckProcesses.scala diff --git a/tck/src/main/resources/reference.conf b/tck/src/main/resources/reference.conf index 6d7e2ad6e..dcedb254d 100644 --- a/tck/src/main/resources/reference.conf +++ b/tck/src/main/resources/reference.conf @@ -1,39 +1,20 @@ -cloudstate-tck { - #verify is a list of names of combinations to run for the TCK - verify = [] - #combinations is a list of config objects with a name, a proxy, and a frontend - combinations = [] - - tck { - hostname = "127.0.0.1" - hostname = ${?HOST} - port = 8090 - } +cloudstate.tck { + hostname = "127.0.0.1" + hostname = ${?TCK_HOST} + port = 8090 + port = ${?TCK_PORT} proxy { hostname = "127.0.0.1" - hostname = ${?HOST} + hostname = ${?TCK_PROXY_HOST} port = 9000 - directory = ${user.dir} - command = [] - stop-command = [] - env-vars { - } - # If specified, will start a docker container, with the environment variable USER_FUNCTION_HOST set to the host - # to connect to, USER_FUNCTION_PORT set to the port to connect to, and HTTP_PORT set to the port above. - docker-image = "" + port = ${?TCK_PROXY_PORT} } frontend { hostname = "127.0.0.1" - hostname = ${?HOST} + hostname = ${?TCK_FRONTEND_HOST} port = 8080 - directory = ${user.dir} - command = [] - stop-command = [] - env-vars { - } - # If specified, will start a docker container, with the environment variable PORT set to the port above. - docker-image = "" + port = ${?TCK_FRONTEND_PORT} } -} \ No newline at end of file +} diff --git a/tck/src/main/scala/io/cloudstate/tck/CloudStateTCK.scala b/tck/src/main/scala/io/cloudstate/tck/CloudStateTCK.scala index 38c27a984..523c837fe 100644 --- a/tck/src/main/scala/io/cloudstate/tck/CloudStateTCK.scala +++ b/tck/src/main/scala/io/cloudstate/tck/CloudStateTCK.scala @@ -18,50 +18,42 @@ package io.cloudstate.tck import akka.NotUsed import akka.actor.{ActorSystem, Scheduler} +import akka.grpc.GrpcClientSettings +import akka.http.scaladsl.Http +import akka.http.scaladsl.Http.ServerBinding +import akka.http.scaladsl.model._ +import akka.http.scaladsl.unmarshalling._ +import akka.pattern.after import akka.stream.ActorMaterializer import akka.stream.scaladsl.{Sink, Source} -import akka.pattern.after -import akka.grpc.GrpcClientSettings +import akka.testkit.TestProbe +import com.example.shoppingcart.shoppingcart._ +import com.google.protobuf.empty.Empty import com.google.protobuf.{ByteString => ProtobufByteString} -import org.scalatest._ import com.typesafe.config.{Config, ConfigFactory} +import io.cloudstate.protocol.entity._ +import io.cloudstate.protocol.event_sourced._ +import org.scalatest._ -import scala.concurrent.{Await, ExecutionContext, Future} import scala.concurrent.duration._ +import scala.concurrent.{Await, ExecutionContext, Future} import scala.util.Try -import java.util.{Map => JMap} -import java.util.concurrent.TimeUnit -import java.io.File -import java.net.InetAddress - -import akka.http.scaladsl.{Http, HttpConnectionContext, UseHttp2} -import akka.http.scaladsl.Http.ServerBinding -import akka.http.scaladsl.model.{ - ContentTypes, - HttpEntity, - HttpMethods, - HttpProtocols, - HttpRequest, - HttpResponse, - StatusCodes -} -import akka.http.scaladsl.unmarshalling._ -import io.cloudstate.protocol.entity._ -import com.example.shoppingcart.shoppingcart._ -import akka.testkit.TestProbe -import com.google.protobuf.empty.Empty -import io.cloudstate.protocol.event_sourced.{ - EventSourced, - EventSourcedClient, - EventSourcedHandler, - EventSourcedInit, - EventSourcedReply, - EventSourcedStreamIn, - EventSourcedStreamOut -} -import io.grpc.netty.shaded.io.grpc.netty.NegotiationType object CloudStateTCK { + final case class Address(hostname: String, port: Int) + final case class Settings(tck: Address, proxy: Address, frontend: Address) + + object Settings { + def fromConfig(config: Config): Settings = { + val tckConfig = config.getConfig("cloudstate.tck") + Settings( + Address(tckConfig.getString("hostname"), tckConfig.getInt("port")), + Address(tckConfig.getString("proxy.hostname"), tckConfig.getInt("proxy.port")), + Address(tckConfig.getString("frontend.hostname"), tckConfig.getInt("frontend.port")) + ) + } + } + final val noWait = 0.seconds // FIXME add interception to enable asserting exchanges @@ -116,12 +108,16 @@ object CloudStateTCK { ) } -class CloudStateTCK(private[this] final val config: TckConfiguration) +class ConfiguredCloudStateTCK extends CloudStateTCK(CloudStateTCK.Settings.fromConfig(ConfigFactory.load())) + +class CloudStateTCK(description: String, settings: CloudStateTCK.Settings) extends AsyncWordSpec with MustMatchers with BeforeAndAfterAll { import CloudStateTCK._ + def this(settings: CloudStateTCK.Settings) = this("", settings) + private[this] final val system = ActorSystem("CloudStateTCK", ConfigFactory.load("tck")) private[this] final val mat = ActorMaterializer()(system) private[this] final val discoveryFromBackend = TestProbe("discoveryFromBackend")(system) @@ -131,7 +127,6 @@ class CloudStateTCK(private[this] final val config: TckConfiguration) @volatile private[this] final var shoppingClient: ShoppingCartClient = _ @volatile private[this] final var entityDiscoveryClient: EntityDiscoveryClient = _ @volatile private[this] final var eventSourcedClient: EventSourcedClient = _ - @volatile private[this] final var processes: TckProcesses = _ @volatile private[this] final var tckProxy: ServerBinding = _ def buildTCKProxy(entityDiscovery: EntityDiscovery, eventSourced: EventSourced): Future[ServerBinding] = { @@ -139,20 +134,14 @@ class CloudStateTCK(private[this] final val config: TckConfiguration) implicit val m = mat Http().bindAndHandleAsync( handler = EntityDiscoveryHandler.partial(entityDiscovery) orElse EventSourcedHandler.partial(eventSourced), - interface = config.tckHostname, - port = config.tckPort + interface = settings.tck.hostname, + port = settings.tck.port ) } override def beforeAll(): Unit = { - - config.validate() - - processes = TckProcesses.create(config) - processes.frontend.start() - val clientSettings = - GrpcClientSettings.connectToServiceAt(config.frontend.hostname, config.frontend.port)(system).withTls(false) + GrpcClientSettings.connectToServiceAt(settings.frontend.hostname, settings.frontend.port)(system).withTls(false) val edc = EntityDiscoveryClient(clientSettings)(mat, mat.executionContext) @@ -172,30 +161,22 @@ class CloudStateTCK(private[this] final val config: TckConfiguration) tckProxy = tp - // Wait for the backend to come up before starting the frontend, otherwise the discovery call from the backend, + // Wait for the frontend to come up before starting the backend, otherwise the discovery call from the backend, // if it happens before the frontend starts, will cause the proxy probes to have failures in them Await.ready(attempt(entityDiscoveryClient.discover(proxyInfo), 4.seconds, 10)(system.dispatcher, system.scheduler), 1.minute) - processes.proxy.start() - val sc = ShoppingCartClient( - GrpcClientSettings.connectToServiceAt(config.proxy.hostname, config.proxy.port)(system).withTls(false) + GrpcClientSettings.connectToServiceAt(settings.proxy.hostname, settings.proxy.port)(system).withTls(false) )(mat, mat.executionContext) shoppingClient = sc } - override final def afterAll(): Unit = { + override def afterAll(): Unit = try Option(shoppingClient).foreach(c => Await.result(c.close(), 10.seconds)) - finally try { - Option(processes).foreach(_.proxy.stop()) - } finally { - Seq(entityDiscoveryClient, eventSourcedClient).foreach(c => Await.result(c.close(), 10.seconds)) - } - try Option(processes).foreach(_.frontend.stop()) + finally try Seq(entityDiscoveryClient, eventSourcedClient).foreach(c => Await.result(c.close(), 10.seconds)) finally Await.ready(tckProxy.unbind().transformWith(_ => system.terminate())(system.dispatcher), 30.seconds) - } final def fromFrontend_expectEntitySpec(within: FiniteDuration): EntitySpec = withClue("EntitySpec was not received, or not well-formed: ") { @@ -264,7 +245,7 @@ class CloudStateTCK(private[this] final val config: TckConfiguration) cmd.id must not be commandId } - ("The TCK for " + config.name) must { + ("Cloudstate TCK " + description) must { implicit val scheduler = system.scheduler "verify that the user function process responds" in { @@ -374,10 +355,10 @@ class CloudStateTCK(private[this] final val config: TckConfiguration) import ServerReflectionResponse.{MessageResponse => Out} val reflectionClient = ServerReflectionClient( - GrpcClientSettings.connectToServiceAt(config.proxy.hostname, config.proxy.port)(system).withTls(false) + GrpcClientSettings.connectToServiceAt(settings.proxy.hostname, settings.proxy.port)(system).withTls(false) )(mat, mat.executionContext) - val Host = config.proxy.hostname + val Host = settings.proxy.hostname val ShoppingCart = "com.example.shoppingcart.ShoppingCart" val testData = List[(In, Out)]( @@ -428,7 +409,7 @@ class CloudStateTCK(private[this] final val config: TckConfiguration) HttpRequest( method = HttpMethods.GET, headers = Nil, - uri = s"http://${config.proxy.hostname}:${config.proxy.port}/carts/${userId}", + uri = s"http://${settings.proxy.hostname}:${settings.proxy.port}/carts/${userId}", entity = HttpEntity.Empty, protocol = HttpProtocols.`HTTP/1.1` ) @@ -441,7 +422,7 @@ class CloudStateTCK(private[this] final val config: TckConfiguration) HttpRequest( method = HttpMethods.GET, headers = Nil, - uri = s"http://${config.proxy.hostname}:${config.proxy.port}/carts/${userId}/items", + uri = s"http://${settings.proxy.hostname}:${settings.proxy.port}/carts/${userId}/items", entity = HttpEntity.Empty, protocol = HttpProtocols.`HTTP/1.1` ) @@ -454,7 +435,7 @@ class CloudStateTCK(private[this] final val config: TckConfiguration) HttpRequest( method = HttpMethods.POST, headers = Nil, - uri = s"http://${config.proxy.hostname}:${config.proxy.port}/cart/${userId}/items/add", + uri = s"http://${settings.proxy.hostname}:${settings.proxy.port}/cart/${userId}/items/add", entity = HttpEntity( ContentTypes.`application/json`, s""" @@ -476,7 +457,7 @@ class CloudStateTCK(private[this] final val config: TckConfiguration) HttpRequest( method = HttpMethods.POST, headers = Nil, - uri = s"http://${config.proxy.hostname}:${config.proxy.port}/cart/${userId}/items/${productId}/remove", + uri = s"http://${settings.proxy.hostname}:${settings.proxy.port}/cart/${userId}/items/${productId}/remove", entity = HttpEntity.Empty, protocol = HttpProtocols.`HTTP/1.1` )