Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ lazy val sdkScala = project
lazy val sdkScalaTestKit = project
.in(file("sdk/scala-sdk-testkit"))
.dependsOn(sdkScala)
.dependsOn(sdkJavaTestKit)
.enablePlugins(BuildInfoPlugin, PublishSonatype)
.settings(
name := "akkaserverless-scala-sdk-testkit",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,22 @@ object MainSourceGenerator {
// FIXME remove filtering
Seq(akkaServerlessFactorySource(excludedUntilImplemented(model)))

private[codegen] def mainSource(model: ModelBuilder.Model): File = {
def mainClassName(model: ModelBuilder.Model): FullyQualifiedName = {
val packageName = mainPackageName(model.services.keys ++ model.entities.keys).mkString(".")
val className = "Main"
FullyQualifiedName(
className,
new PackageNaming(
protoFileName = "",
name = "",
protoPackage = packageName,
javaPackageOption = None,
javaOuterClassnameOption = None,
javaMultipleFiles = false))
}

private[codegen] def mainSource(model: ModelBuilder.Model): File = {
val mainClass = mainClassName(model)

val entityImports = model.entities.values.collect {
case entity: ModelBuilder.EventSourcedEntity => entity.fqn.fullQualifiedName
Expand All @@ -79,7 +92,7 @@ object MainSourceGenerator {
List("com.akkaserverless.scalasdk.AkkaServerless", "org.slf4j.LoggerFactory")

val imports =
generateImports(Iterable.empty, packageName, allImports, semi = false)
generateImports(Iterable.empty, mainClass.parent.scalaPackage, allImports, semi = false)

val entityRegistrationParameters = model.entities.values.toList
.sortBy(_.fqn.name)
Expand All @@ -99,17 +112,17 @@ object MainSourceGenerator {
val registrationParameters = entityRegistrationParameters ::: serviceRegistrationParameters

File(
packageName,
className,
s"""|package $packageName
mainClass.parent.scalaPackage,
mainClass.name,
s"""|package ${mainClass.parent.scalaPackage}
|
|$imports
|
|$unmanagedComment
|
|object $className {
|object ${mainClass.name} {
|
| private val log = LoggerFactory.getLogger("$packageName.$className")
| private val log = LoggerFactory.getLogger("${mainClass.parent.scalaPackage}.${mainClass.name}")
|
| def createAkkaServerless(): AkkaServerless = {
| // The AkkaServerlessFactory automatically registers any generated Actions, Views or Entities,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,14 +93,16 @@ object SourceGenerator {
/**
* Generate the 'unmanaged' code for this model: code that is generated once on demand and then maintained by the user
*/
// FIXME we need to call this from the sbt plugin
def generateUnmanagedTest(model: ModelBuilder.Model): Seq[File] = {
def generateUnmanagedTest(model: ModelBuilder.Model): Seq[File] =
model.services.values
.flatMap {
case service: ModelBuilder.EntityService =>
model.lookupEntity(service) match {
case entity: ModelBuilder.ValueEntity =>
ValueEntityTestKitGenerator.generateUnmanagedTest(entity, service)
ValueEntityTestKitGenerator.generateUnmanagedTest(
MainSourceGenerator.mainClassName(model),
entity,
service)
case _: ModelBuilder.EventSourcedEntity =>
Nil // FIXME
case _: ModelBuilder.ReplicatedEntity =>
Expand All @@ -113,6 +115,4 @@ object SourceGenerator {
}
.map(_.prepend(unmanagedComment))
.toList
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,16 @@
package com.akkaserverless.codegen.scalasdk.impl

import com.akkaserverless.codegen.scalasdk.File
import com.lightbend.akkasls.codegen.Format
import com.lightbend.akkasls.codegen.ModelBuilder
import com.lightbend.akkasls.codegen.{ Format, FullyQualifiedName, ModelBuilder }

object ValueEntityTestKitGenerator {
import com.lightbend.akkasls.codegen.SourceGeneratorUtils._

def generateUnmanagedTest(valueEntity: ModelBuilder.ValueEntity, service: ModelBuilder.EntityService): Seq[File] =
Seq(test(valueEntity, service))
def generateUnmanagedTest(
main: FullyQualifiedName,
valueEntity: ModelBuilder.ValueEntity,
service: ModelBuilder.EntityService): Seq[File] =
Seq(test(valueEntity, service), integrationTest(main, valueEntity, service))

def generateManagedTest(valueEntity: ModelBuilder.ValueEntity, service: ModelBuilder.EntityService): Seq[File] =
Seq(testkit(valueEntity, service))
Expand Down Expand Up @@ -164,4 +166,70 @@ object ValueEntityTestKitGenerator {
|""".stripMargin)
}

def integrationTest(
main: FullyQualifiedName,
valueEntity: ModelBuilder.ValueEntity,
service: ModelBuilder.EntityService): File = {

val client = FullyQualifiedName(service.fqn.name + "Client", service.fqn.parent)

implicit val imports: Imports =
generateImports(
Seq(main, valueEntity.state.fqn, client) ++
service.commands.map(_.inputType) ++
service.commands.map(_.outputType),
service.fqn.parent.scalaPackage,
otherImports = Seq(
"com.akkaserverless.scalasdk.valueentity.ValueEntity",
"com.akkaserverless.scalasdk.testkit.ValueEntityResult",
"com.akkaserverless.scalasdk.testkit.AkkaServerlessTestkit",
"org.scalatest.matchers.should.Matchers",
"org.scalatest.wordspec.AnyWordSpec",
"org.scalatest.BeforeAndAfterAll",
"org.scalatest.concurrent.ScalaFutures",
"org.scalatest.time.Span",
"org.scalatest.time.Seconds",
"org.scalatest.time.Millis"),
packageImports = Seq(valueEntity.fqn.parent.scalaPackage),
semi = false)

val entityClassName = service.fqn.name

File(
service.fqn.fileBasename + "IntegrationSpec.scala",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if we have these in test scope I wonder if there is a way to run all unit tests without these?

Thinking about testOnly **Spec but that would include IntegrationSpec too. Shall we name the others UnitSpec so that testOnly **UnitSpec would work?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

*UnitSpec could work, though it's a bit unfortunate when ideally you'd want to have many unit tests and few integration tests. The only alternative I can think of is using ScalaTest tagging, though that's a bit obscure.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Created issue #571 for this thought

s"""|package ${service.fqn.parent.scalaPackage}
|
|$imports
|
Comment thread
raboof marked this conversation as resolved.
|class ${entityClassName}IntegrationSpec
| extends AnyWordSpec
| with Matchers
| with BeforeAndAfterAll
| with ScalaFutures {
|
| implicit val patience: PatienceConfig =
| PatienceConfig(Span(5, Seconds), Span(500, Millis))
|
| val testkit = AkkaServerlessTestkit(Main.createAkkaServerless())
| testkit.start()
| implicit val system = testkit.system
|
| "${entityClassName}" must {
| val client: ${typeName(client)} =
| ${typeName(client)}(testkit.grpcClientSettings)
|
| "have example test that can be removed" in {
| // use the gRPC client to send requests to the
| // proxy and verify the results
| }
|
| }
|
| override def afterAll() = {
| testkit.stop()
| super.afterAll()
| }
|}
|""".stripMargin)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,61 @@ class ValueEntityTestKitGeneratorSuite extends munit.FunSuite {
assertNoDiff(sourceCode, expected)
}

test("it can generate an specific integrationtest stub for the entity") {
Comment thread
raboof marked this conversation as resolved.
Outdated
val entity = createShoppingCartEntity()
val service = createShoppingCartService(entity)
val main = FullyQualifiedName("Main", packageNaming)

assertEquals(
ValueEntityTestKitGenerator.integrationTest(main, entity, service).content,
"""package com.example.shoppingcart.api

import com.akkaserverless.scalasdk.testkit.AkkaServerlessTestkit
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we moving out of using the |? I have see that at least one other suite were not using it.

I don't mind. Maybe be even easier to read. Soon I will be write codegen for the replicated entity. I will use that new style then.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, this was not particularly intentional, I'm fine with either

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe be easier if we remove the |.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since we have mostly used | I think we should continue with that, and at least to me it reads a bit weird to suddenly have code to the very left

import com.akkaserverless.scalasdk.testkit.ValueEntityResult
import com.akkaserverless.scalasdk.valueentity.ValueEntity
import com.example.shoppingcart.domain
import com.google.protobuf.Empty
import org.scalatest.BeforeAndAfterAll
import org.scalatest.concurrent.ScalaFutures
import org.scalatest.matchers.should.Matchers
import org.scalatest.time.Millis
import org.scalatest.time.Seconds
import org.scalatest.time.Span
import org.scalatest.wordspec.AnyWordSpec
import undefined.Main

class ShoppingCartServiceIntegrationSpec
extends AnyWordSpec
with Matchers
with BeforeAndAfterAll
with ScalaFutures {

implicit val patience: PatienceConfig =
PatienceConfig(Span(5, Seconds), Span(500, Millis))

val testkit = AkkaServerlessTestkit(Main.createAkkaServerless())
testkit.start()
implicit val system = testkit.system
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

system: ActorSystem to avoid warnings in Intellij

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is that needed for running streaming calls, or something else? wonder if we should not include it since it exposes a bit too much low level Akka? btw, if it's for streams we should have it as Materializer since that is what we have in the contexts

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's used for creating the Akka gRPC client

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah, we could have a grpcClient utility method similar to what we provide in the context. That would become more unified. Can be a separate ticket.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I created issue #568 for this idea


"ShoppingCartService" must {
val client: ShoppingCartServiceClient =
ShoppingCartServiceClient(testkit.grpcClientSettings)

"have example test that can be removed" in {
// use the gRPC client to send requests to the
// proxy and verify the results
}

}

override def afterAll() = {
testkit.stop()
super.afterAll()
}
}
""".stripMargin)
}

def createShoppingCartEntity(): ModelBuilder.ValueEntity = {

val domainProto = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import sbt.{ Compile, _ }
import sbt.Keys._
import sbtprotoc.ProtocPlugin
import sbtprotoc.ProtocPlugin.autoImport.PB
import sbtprotoc.ProtocPlugin.protobufConfigSettings
import scalapb.GeneratorOption

object AkkaserverlessPlugin extends AutoPlugin {
Expand All @@ -47,7 +48,6 @@ object AkkaserverlessPlugin extends AutoPlugin {
"These are the source files that are placed in the source tree, and after initial generation should typically be maintained by the user.\n" +
"Files that already exist they are not re-generated.")
val temporaryUnmanagedDirectory = settingKey[File]("Directory to generate 'unmanaged' sources into")
val temporaryUnmanagedTestDirectory = settingKey[File]("Directory to generate 'unmanaged' test sources into")
}

object autoImport extends Keys
Expand All @@ -64,15 +64,15 @@ object AkkaserverlessPlugin extends AutoPlugin {
gen(
akkaGrpcCodeGeneratorSettings.value :+ AkkaserverlessGenerator.enableDebug) -> (Compile / sourceManaged).value / "akkaserverless",
Compile / temporaryUnmanagedDirectory := (Compile / baseDirectory).value / "target" / "akkaserverless-unmanaged",
Test / temporaryUnmanagedTestDirectory := (Test / baseDirectory).value / "target" / "akkaserverless-unmanaged-test",
Test / temporaryUnmanagedDirectory := (Test / baseDirectory).value / "target" / "akkaserverless-unmanaged-test",
// FIXME there is a name clash between the Akka gRPC server-side service 'handler'
// and the Akka Serverless 'handler'. For now working around it by only generating
// the client, but we should probably resolve this before the first public release.
Compile / akkaGrpcGeneratedSources := Seq(AkkaGrpc.Client),
Compile / PB.targets ++= Seq(genUnmanaged(
akkaGrpcCodeGeneratorSettings.value :+ AkkaserverlessGenerator.enableDebug) -> (Compile / temporaryUnmanagedDirectory).value),
Test / PB.targets ++= Seq(genUnmanagedTest(
akkaGrpcCodeGeneratorSettings.value :+ AkkaserverlessGenerator.enableDebug) -> (Test / temporaryUnmanagedTestDirectory).value),
akkaGrpcCodeGeneratorSettings.value :+ AkkaserverlessGenerator.enableDebug) -> (Test / temporaryUnmanagedDirectory).value),
Test / PB.protoSources ++= (Compile / PB.protoSources).value,
Test / PB.targets +=
genTests(
Expand All @@ -87,20 +87,20 @@ object AkkaserverlessPlugin extends AutoPlugin {
Paths.get((Compile / sourceDirectory).value.toURI).resolve("scala"))
},
Test / generateUnmanaged := {
Files.createDirectories(Paths.get((Test / temporaryUnmanagedTestDirectory).value.toURI))
Files.createDirectories(Paths.get((Test / temporaryUnmanagedDirectory).value.toURI))
// Make sure generation has happened
val _ = (Test / PB.generate).value
// Then copy over any new generated unmanaged sources
copyIfNotExist(
Paths.get((Test / temporaryUnmanagedTestDirectory).value.toURI),
Paths.get((Test / temporaryUnmanagedDirectory).value.toURI),
Paths.get((Test / sourceDirectory).value.toURI).resolve("scala"))
},
Compile / managedSources :=
(Compile / managedSources).value.filter(s => !isIn(s, (Compile / temporaryUnmanagedDirectory).value)),
Compile / unmanagedSources :=
(Compile / generateUnmanaged).value ++ (Compile / unmanagedSources).value,
Test / managedSources :=
(Test / managedSources).value.filter(s => !isIn(s, (Test / temporaryUnmanagedTestDirectory).value)),
(Test / managedSources).value.filter(s => !isIn(s, (Test / temporaryUnmanagedDirectory).value)),
Test / unmanagedSources :=
(Test / generateUnmanaged).value ++ (Test / unmanagedSources).value)

Expand Down
2 changes: 1 addition & 1 deletion sbt-plugin/src/sbt-test/sbt-akkaserverless/basic/test
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,8 @@ $ exists src/main/scala/com/example/valueentity/domain/User.scala
$ exists target/scala-2.13/src_managed/main/akkaserverless/com/example/valueentity/domain/AbstractUser.scala

> Test/compile

# com/example/valueentity/some_value_entity_api.proto
# com/example/valueentity/domain/some_value_entity_domain.proto
$ exists src/test/scala/com/example/valueentity/domain/SomeValueEntitySpec.scala
$ exists src/test/scala/com/example/valueentity/domain/SomeValueEntityIntegrationSpec.scala
$ exists target/scala-2.13/src_managed/test/akkaserverless/com/example/valueentity/domain/SomeValueEntityTestKit.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* Copyright 2021 Lightbend Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.akkaserverless.scalasdk.testkit

import akka.grpc.GrpcClientSettings
import com.akkaserverless.scalasdk.AkkaServerless
import com.akkaserverless.javasdk.testkit.{ AkkaServerlessTestkit => JTestKit }

/**
* Testkit for running Akka Serverless services locally.
*
* <p>Requires Docker for starting a local instance of the Akka Serverless proxy.
*
* <p>Create an AkkaServerlessTestkit with an {@link AkkaServerless} service descriptor, and then {@link #start} the
* testkit before testing the service with gRPC or HTTP clients. Call {@link #stop} after tests are complete.
*/
class AkkaServerlessTestkit(impl: JTestKit) {
def start() = impl.start()
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpicking:

We have been using impl, but I think we should call it delegate. Both are implementations, but one is an adapter that delegates all calls to the other impl.


/**
* Get {@link GrpcClientSettings} for creating Akka gRPC clients.
*
* @return
* test gRPC client settings
*/
def grpcClientSettings: GrpcClientSettings = impl.getGrpcClientSettings()

def system = impl.getActorSystem()

def stop() = impl.stop()
}
object AkkaServerlessTestkit {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we have this at the top, since that is the starting point for using it?

def apply(main: AkkaServerless): AkkaServerlessTestkit =
new AkkaServerlessTestkit(new JTestKit(main.impl))
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ object AkkaServerless {
* The AkkaServerless class is the main interface to configuring entities to deploy, and subsequently starting a local
* server which will expose these entities to the AkkaServerless Proxy Sidecar.
*/
class AkkaServerless private (impl: javasdk.AkkaServerless) {
class AkkaServerless private (private[akkaserverless] val impl: javasdk.AkkaServerless) {

/**
* Sets the ClassLoader to be used for reflective access, the default value is the ClassLoader of the AkkaServerless
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
/**
* Testkit for running Akka Serverless services locally.
*
* <p>Requires Docker for starting a local instance of Akka Serverless.
* <p>Requires Docker for starting a local instance of the Akka Serverless proxy.
*
* <p>Create an AkkaServerlessTestkit with an {@link AkkaServerless} service descriptor, and then
* {@link #start} the testkit before testing the service with gRPC or HTTP clients. Call {@link
Expand Down