From d63d359cf04b14a4a3e78e1bd28ca40a1899a25f Mon Sep 17 00:00:00 2001 From: Peter Vlugter Date: Fri, 18 Dec 2020 11:22:41 +1300 Subject: [PATCH 1/7] Reorganise TCK into separate traits and files --- .../javasupport/tck/JavaSupportTck.java | 16 +- .../ValueEntityConfiguredEntity.java} | 18 +- .../valuebased/ValueEntityTwoEntity.java | 3 +- .../tck/model/entitypassivation.proto | 62 - .../cloudstate/tck/model/valueentity.proto | 15 +- .../proxy/UserFunctionTypeSupport.scala | 2 +- tck/src/it/scala/io/cloudstate/tck/TCK.scala | 14 +- tck/src/main/resources/tck.conf | 1 - .../scala/io/cloudstate/tck/ActionTCK.scala | 425 +++ .../io/cloudstate/tck/CloudStateTCK.scala | 3353 ----------------- .../io/cloudstate/tck/CloudstateTCK.scala | 59 + .../io/cloudstate/tck/CrdtEntityTCK.scala | 1825 +++++++++ .../scala/io/cloudstate/tck/EntityTCK.scala | 324 ++ .../tck/EventSourcedEntityTCK.scala | 323 ++ .../tck/EventSourcedShoppingCartTCK.scala | 240 ++ .../EventSourcedShoppingCartVerifier.scala | 107 - .../scala/io/cloudstate/tck/EventingTCK.scala | 139 + .../scala/io/cloudstate/tck/ProxyTCK.scala | 68 + .../io/cloudstate/tck/ShoppingCartTCK.scala | 299 ++ .../scala/io/cloudstate/tck/TCKSpec.scala | 116 + .../tck/ValueEntityShoppingCartVerifier.scala | 112 - .../cloudstate/testkit/InterceptService.scala | 27 +- .../io/cloudstate/testkit/TestClient.scala | 2 + 23 files changed, 3869 insertions(+), 3681 deletions(-) rename java-support/tck/src/main/java/io/cloudstate/javasupport/tck/model/{passivation/PassivationTckModelEntity.java => valuebased/ValueEntityConfiguredEntity.java} (58%) delete mode 100644 protocols/tck/cloudstate/tck/model/entitypassivation.proto delete mode 100644 tck/src/main/resources/tck.conf create mode 100644 tck/src/main/scala/io/cloudstate/tck/ActionTCK.scala delete mode 100644 tck/src/main/scala/io/cloudstate/tck/CloudStateTCK.scala create mode 100644 tck/src/main/scala/io/cloudstate/tck/CloudstateTCK.scala create mode 100644 tck/src/main/scala/io/cloudstate/tck/CrdtEntityTCK.scala create mode 100644 tck/src/main/scala/io/cloudstate/tck/EntityTCK.scala create mode 100644 tck/src/main/scala/io/cloudstate/tck/EventSourcedEntityTCK.scala create mode 100644 tck/src/main/scala/io/cloudstate/tck/EventSourcedShoppingCartTCK.scala delete mode 100644 tck/src/main/scala/io/cloudstate/tck/EventSourcedShoppingCartVerifier.scala create mode 100644 tck/src/main/scala/io/cloudstate/tck/EventingTCK.scala create mode 100644 tck/src/main/scala/io/cloudstate/tck/ProxyTCK.scala create mode 100644 tck/src/main/scala/io/cloudstate/tck/ShoppingCartTCK.scala create mode 100644 tck/src/main/scala/io/cloudstate/tck/TCKSpec.scala delete mode 100644 tck/src/main/scala/io/cloudstate/tck/ValueEntityShoppingCartVerifier.scala diff --git a/java-support/tck/src/main/java/io/cloudstate/javasupport/tck/JavaSupportTck.java b/java-support/tck/src/main/java/io/cloudstate/javasupport/tck/JavaSupportTck.java index 7f92e7667..bf07f380e 100644 --- a/java-support/tck/src/main/java/io/cloudstate/javasupport/tck/JavaSupportTck.java +++ b/java-support/tck/src/main/java/io/cloudstate/javasupport/tck/JavaSupportTck.java @@ -20,8 +20,8 @@ import io.cloudstate.javasupport.CloudState; import io.cloudstate.javasupport.PassivationStrategy; import io.cloudstate.javasupport.entity.EntityOptions; -import io.cloudstate.javasupport.tck.model.passivation.PassivationTckModelEntity; import io.cloudstate.javasupport.tck.model.eventlogeventing.EventLogSubscriber; +import io.cloudstate.javasupport.tck.model.valuebased.ValueEntityConfiguredEntity; import io.cloudstate.javasupport.tck.model.valuebased.ValueEntityTckModelEntity; import io.cloudstate.javasupport.tck.model.valuebased.ValueEntityTwoEntity; import io.cloudstate.javasupport.tck.model.action.ActionTckModelBehavior; @@ -35,7 +35,6 @@ import io.cloudstate.tck.model.Crdt; import io.cloudstate.tck.model.Eventlogeventing; import io.cloudstate.tck.model.Eventsourced; -import io.cloudstate.tck.model.entitypassivation.Entitypassivation; import io.cloudstate.tck.model.valueentity.Valueentity; import java.time.Duration; @@ -58,6 +57,11 @@ public static final void main(String[] args) throws Exception { .registerEntity( ValueEntityTwoEntity.class, Valueentity.getDescriptor().findServiceByName("ValueEntityTwo")) + .registerEntity( + ValueEntityConfiguredEntity.class, + Valueentity.getDescriptor().findServiceByName("ValueEntityConfigured"), + EntityOptions.defaults() // required timeout of 100 millis for TCK tests + .withPassivationStrategy(PassivationStrategy.timeout(Duration.ofMillis(100)))) .registerEntity( ShoppingCartEntity.class, Shoppingcart.getDescriptor().findServiceByName("ShoppingCart"), @@ -89,14 +93,6 @@ public static final void main(String[] args) throws Exception { io.cloudstate.samples.eventsourced.shoppingcart.ShoppingCartEntity.class, com.example.shoppingcart.Shoppingcart.getDescriptor().findServiceByName("ShoppingCart"), com.example.shoppingcart.persistence.Domain.getDescriptor()) - .registerEntity( - PassivationTckModelEntity.class, - Entitypassivation.getDescriptor().findServiceByName("PassivationTckModel"), - EntityOptions.defaults() - .withPassivationStrategy( - PassivationStrategy.timeout( - Duration.ofSeconds(2))), // 2 seconds for timeout passivation testing! - Entitypassivation.getDescriptor()) .start() .toCompletableFuture() .get(); diff --git a/java-support/tck/src/main/java/io/cloudstate/javasupport/tck/model/passivation/PassivationTckModelEntity.java b/java-support/tck/src/main/java/io/cloudstate/javasupport/tck/model/valuebased/ValueEntityConfiguredEntity.java similarity index 58% rename from java-support/tck/src/main/java/io/cloudstate/javasupport/tck/model/passivation/PassivationTckModelEntity.java rename to java-support/tck/src/main/java/io/cloudstate/javasupport/tck/model/valuebased/ValueEntityConfiguredEntity.java index 0c829d068..a07a616e5 100644 --- a/java-support/tck/src/main/java/io/cloudstate/javasupport/tck/model/passivation/PassivationTckModelEntity.java +++ b/java-support/tck/src/main/java/io/cloudstate/javasupport/tck/model/valuebased/ValueEntityConfiguredEntity.java @@ -14,22 +14,18 @@ * limitations under the License. */ -package io.cloudstate.javasupport.tck.model.passivation; +package io.cloudstate.javasupport.tck.model.valuebased; -import io.cloudstate.javasupport.entity.CommandContext; import io.cloudstate.javasupport.entity.CommandHandler; import io.cloudstate.javasupport.entity.Entity; -import io.cloudstate.tck.model.entitypassivation.Entitypassivation.*; +import io.cloudstate.tck.model.valueentity.Valueentity.Request; +import io.cloudstate.tck.model.valueentity.Valueentity.Response; -import java.util.Optional; - -@Entity(persistenceId = "entity-passivation-tck-model") -public class PassivationTckModelEntity { - - private final String state = "state"; +@Entity(persistenceId = "value-entity-configured") +public class ValueEntityConfiguredEntity { @CommandHandler - public Optional activate(Request request, CommandContext context) { - return Optional.of(Response.newBuilder().setMessage(state).build()); + public Response call(Request request) { + return Response.getDefaultInstance(); } } diff --git a/java-support/tck/src/main/java/io/cloudstate/javasupport/tck/model/valuebased/ValueEntityTwoEntity.java b/java-support/tck/src/main/java/io/cloudstate/javasupport/tck/model/valuebased/ValueEntityTwoEntity.java index ef5ecd4d7..731c747bb 100644 --- a/java-support/tck/src/main/java/io/cloudstate/javasupport/tck/model/valuebased/ValueEntityTwoEntity.java +++ b/java-support/tck/src/main/java/io/cloudstate/javasupport/tck/model/valuebased/ValueEntityTwoEntity.java @@ -23,10 +23,9 @@ @Entity(persistenceId = "value-entity-tck-model-two") public class ValueEntityTwoEntity { - public ValueEntityTwoEntity() {} @CommandHandler public Response call(Request request) { - return Response.newBuilder().build(); + return Response.getDefaultInstance(); } } diff --git a/protocols/tck/cloudstate/tck/model/entitypassivation.proto b/protocols/tck/cloudstate/tck/model/entitypassivation.proto deleted file mode 100644 index 4ddbf6197..000000000 --- a/protocols/tck/cloudstate/tck/model/entitypassivation.proto +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright 2019 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. - -// -// == Cloudstate TCK model test for passivation == -// - -syntax = "proto3"; - -package cloudstate.tck.model.entitypassivation; - -import "cloudstate/entity_key.proto"; - -option java_package = "io.cloudstate.tck.model.entitypassivation"; -option go_package = "github.com/cloudstateio/go-support/tck/entitypassivation;entitypassivation"; - -// -// The `PassivationTckModel` service should be implemented in the following ways: -// -// - The entity persistence-id must be `entity-passivation-tck-model`. -// - The passivation timeout must be set to a duration of 2 seconds or 2000 millis. -// - The state of the entity is a string and has the value "state". -// - The command handler does nothing and must return a `Response` with the state. -// - The `Persisted` message is just used for the command context. -// - The `Activate` method receives a `Request` message for activating the entity. -// - The `Activate` method must reply with the state in a `Response`. -// -service PassivationTckModel { - rpc Activate(Request) returns (Response); -} - -// -// A `Request` message for activating the entity. -// -message Request { - string id = 1 [(.cloudstate.entity_key) = true]; -} - -// -// The `Response` message for the `Activate` must contain the current state. -// -message Response { - string message = 1; -} - -// -// The `Persisted` message wraps the state value. It is just used for the command context. -// -message Persisted { - string value = 1; -} diff --git a/protocols/tck/cloudstate/tck/model/valueentity.proto b/protocols/tck/cloudstate/tck/model/valueentity.proto index cee66f15a..7a615078b 100644 --- a/protocols/tck/cloudstate/tck/model/valueentity.proto +++ b/protocols/tck/cloudstate/tck/model/valueentity.proto @@ -43,12 +43,25 @@ service ValueEntityTckModel { // // The `ValueBasedTwo` service is only for verifying forward actions and side effects. -// The `Call` method is not required to do anything, and may simply return an empty `Response` message. +// +// - The entity persistence-id must be `value-entity-tck-model-two`. +// - The `Call` method is not required to do anything, and may simply return an empty `Response` message. // service ValueEntityTwo { rpc Call(Request) returns (Response); } +// +// The `ValueEntityConfigured` service is for testing entity configuration from the language support: +// +// - The entity persistence-id must be `value-entity-configured`. +// - The passivation strategy must be set with a timeout of 100 millis. +// - The `Call` method is not required to do anything, and must return an empty `Response` message. +// +service ValueEntityConfigured { + rpc Call(Request) returns (Response); +} + // // A `Request` message contains any actions that the entity should process. // Actions must be processed in order. Any actions after a `Fail` may be ignored. diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/UserFunctionTypeSupport.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/UserFunctionTypeSupport.scala index ddc5042ea..a48c95fc6 100644 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/UserFunctionTypeSupport.scala +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/UserFunctionTypeSupport.scala @@ -83,8 +83,8 @@ object EntityTypeSupportFactory { strategy.strategy match { case Strategy.Timeout(TimeoutPassivationStrategy(timeout, _)) => Timeout(timeout, TimeUnit.MILLISECONDS) + case _ => default } - case _ => default } } diff --git a/tck/src/it/scala/io/cloudstate/tck/TCK.scala b/tck/src/it/scala/io/cloudstate/tck/TCK.scala index 8c1bdaa3d..689ac3d7e 100644 --- a/tck/src/it/scala/io/cloudstate/tck/TCK.scala +++ b/tck/src/it/scala/io/cloudstate/tck/TCK.scala @@ -37,13 +37,13 @@ class TCK extends Suites({ iterator. asScala. filter(section => verify(section.getString("name"))). - map(c => new ManagedCloudStateTCK(TckConfiguration.fromConfig(c))). + map(c => new ManagedCloudstateTCK(TckConfiguration.fromConfig(c))). toVector }: _*) with SequentialNestedSuiteExecution -object ManagedCloudStateTCK { - def settings(config: TckConfiguration): CloudStateTCK.Settings = { - CloudStateTCK.Settings( +object ManagedCloudstateTCK { + def settings(config: TckConfiguration): TCKSpec.Settings = { + TCKSpec.Settings( ServiceAddress(config.tckHostname, config.tckPort), ServiceAddress(config.proxy.hostname, config.proxy.port), ServiceAddress(config.service.hostname, config.service.port) @@ -51,14 +51,14 @@ object ManagedCloudStateTCK { } } -class ManagedCloudStateTCK(config: TckConfiguration) extends CloudStateTCK("for " + config.name, ManagedCloudStateTCK.settings(config)) { +class ManagedCloudstateTCK(config: TckConfiguration) extends CloudstateTCK("for " + config.name, ManagedCloudstateTCK.settings(config)) { config.validate() val processes: TckProcesses = TckProcesses.create(config) - override def beforeAll(): Unit = try { + override def start(): Unit = try { processes.service.start() - super.beforeAll() + super.start() processes.proxy.start() } catch { case error: Throwable => diff --git a/tck/src/main/resources/tck.conf b/tck/src/main/resources/tck.conf deleted file mode 100644 index b2798c07e..000000000 --- a/tck/src/main/resources/tck.conf +++ /dev/null @@ -1 +0,0 @@ -akka.http.server.preview.enable-http2 = on diff --git a/tck/src/main/scala/io/cloudstate/tck/ActionTCK.scala b/tck/src/main/scala/io/cloudstate/tck/ActionTCK.scala new file mode 100644 index 000000000..c79c85ca6 --- /dev/null +++ b/tck/src/main/scala/io/cloudstate/tck/ActionTCK.scala @@ -0,0 +1,425 @@ +/* + * Copyright 2019 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 io.cloudstate.tck + +import io.cloudstate.protocol.action.{ActionCommand, ActionProtocol, ActionResponse} +import io.cloudstate.tck.model.action.{ActionTckModel, ActionTwo} +import io.cloudstate.tck.model.action._ +import io.cloudstate.testkit.action.ActionMessages._ + +trait ActionTCK extends TCKSpec { + + object ActionTCKModel { + val Protocol: String = ActionProtocol.name + val Service: String = ActionTckModel.name + val ServiceTwo: String = ActionTwo.name + + def actionTest(test: => Any): Unit = + testFor(ActionTckModel, ActionTwo)(test) + + def processUnary(request: Request): ActionCommand = + command(Service, "ProcessUnary", request) + + val processStreamedIn: ActionCommand = + command(Service, "ProcessStreamedIn") + + def processStreamedOut(request: Request): ActionCommand = + command(Service, "ProcessStreamedOut", request) + + val processStreamed: ActionCommand = + command(Service, "ProcessStreamed") + + def single(steps: ProcessStep*): Request = + request(group(steps: _*)) + + def request(groups: ProcessGroup*): Request = + Request(groups) + + def group(steps: ProcessStep*): ProcessGroup = + ProcessGroup(steps) + + def replyWith(message: String): ProcessStep = + ProcessStep(ProcessStep.Step.Reply(Reply(message))) + + def sideEffectTo(id: String, synchronous: Boolean = false): ProcessStep = + ProcessStep(ProcessStep.Step.Effect(SideEffect(id, synchronous))) + + def forwardTo(id: String): ProcessStep = + ProcessStep(ProcessStep.Step.Forward(Forward(id))) + + def failWith(message: String): ProcessStep = + ProcessStep(ProcessStep.Step.Fail(Fail(message))) + + def sideEffects(ids: String*): SideEffects = + createSideEffects(synchronous = false, ids) + + def synchronousSideEffects(ids: String*): SideEffects = + createSideEffects(synchronous = true, ids) + + def createSideEffects(synchronous: Boolean, ids: Seq[String]): SideEffects = + ids.map(id => sideEffect(ServiceTwo, "Call", OtherRequest(id), synchronous)) + + def forwarded(id: String, sideEffects: SideEffects = Seq.empty): ActionResponse = + forward(ServiceTwo, "Call", OtherRequest(id), sideEffects) + } + + def verifyActionModel(): Unit = { + import ActionTCKModel._ + + "verify action entity discovery" in actionTest { + discoveredServices must (contain("ActionTckModel") and contain("ActionTwo")) + entity(ActionTCKModel.Service).value.entityType mustBe ActionTCKModel.Protocol + entity(ActionTCKModel.ServiceTwo).value.entityType mustBe ActionTCKModel.Protocol + } + + "verify unary command reply" in actionTest { + protocol.action.unary + .send(processUnary(single(replyWith("one")))) + .expect(reply(Response("one"))) + } + + "verify unary command reply with side effect" in actionTest { + protocol.action.unary + .send(processUnary(single(replyWith("two"), sideEffectTo("other")))) + .expect(reply(Response("two"), sideEffects("other"))) + } + + "verify unary command reply with synchronous side effect" in actionTest { + protocol.action.unary + .send(processUnary(single(replyWith("three"), sideEffectTo("another", synchronous = true)))) + .expect(reply(Response("three"), synchronousSideEffects("another"))) + } + + "verify unary command reply with multiple side effects" in actionTest { + protocol.action.unary + .send(processUnary(single(replyWith("four"), sideEffectTo("a"), sideEffectTo("b"), sideEffectTo("c")))) + .expect(reply(Response("four"), sideEffects("a", "b", "c"))) + } + + "verify unary command no reply" in actionTest { + protocol.action.unary + .send(processUnary(single())) + .expect(noReply()) + } + + "verify unary command no reply with side effect" in actionTest { + protocol.action.unary + .send(processUnary(single(sideEffectTo("other")))) + .expect(noReply(sideEffects("other"))) + } + + "verify unary command no reply with synchronous side effect" in actionTest { + protocol.action.unary + .send(processUnary(single(sideEffectTo("another", synchronous = true)))) + .expect(noReply(synchronousSideEffects("another"))) + } + + "verify unary command no reply with multiple side effects" in actionTest { + protocol.action.unary + .send(processUnary(single(sideEffectTo("a"), sideEffectTo("b"), sideEffectTo("c")))) + .expect(noReply(sideEffects("a", "b", "c"))) + } + + "verify unary command forward" in actionTest { + protocol.action.unary + .send(processUnary(single(forwardTo("one")))) + .expect(forwarded("one")) + } + + "verify unary command forward with side effect" in actionTest { + protocol.action.unary + .send(processUnary(single(forwardTo("two"), sideEffectTo("other")))) + .expect(forwarded("two", sideEffects("other"))) + } + + "verify unary command forward with synchronous side effect" in actionTest { + protocol.action.unary + .send(processUnary(single(forwardTo("three"), sideEffectTo("another", synchronous = true)))) + .expect(forwarded("three", synchronousSideEffects("another"))) + } + + "verify unary command forward with multiple side effects" in actionTest { + protocol.action.unary + .send(processUnary(single(forwardTo("four"), sideEffectTo("a"), sideEffectTo("b"), sideEffectTo("c")))) + .expect(forwarded("four", sideEffects("a", "b", "c"))) + } + + "verify unary command failure" in actionTest { + protocol.action.unary + .send(processUnary(single(failWith("one")))) + .expect(failure("one")) + } + + "verify unary command failure with side effect" in actionTest { + protocol.action.unary + .send(processUnary(single(failWith("two"), sideEffectTo("other")))) + .expect(failure("two", sideEffects("other"))) + } + + "verify unary command failure with synchronous side effect" in actionTest { + protocol.action.unary + .send(processUnary(single(failWith("three"), sideEffectTo("another", synchronous = true)))) + .expect(failure("three", synchronousSideEffects("another"))) + } + + "verify unary command failure with multiple side effects" in actionTest { + protocol.action.unary + .send(processUnary(single(failWith("four"), sideEffectTo("a"), sideEffectTo("b"), sideEffectTo("c")))) + .expect(failure("four", sideEffects("a", "b", "c"))) + } + + "verify streamed-in command reply" in actionTest { + protocol.action + .connectIn() + .send(processStreamedIn) + .send(command(single(replyWith("one")))) + .complete() + .expect(reply(Response("one"))) + } + + "verify streamed-in command reply with side effects" in actionTest { + protocol.action + .connectIn() + .send(processStreamedIn) + .send(command(single(replyWith("two"), sideEffectTo("a")))) + .send(command(single(sideEffectTo("b", synchronous = true)))) + .send(command(single(sideEffectTo("c"), sideEffectTo("d")))) + .complete() + .expect(reply(Response("two"), sideEffects("a") ++ synchronousSideEffects("b") ++ sideEffects("c", "d"))) + } + + "verify streamed-in command no reply (no requests)" in actionTest { + protocol.action + .connectIn() + .send(processStreamedIn) + .complete() + .expect(noReply()) + } + + "verify streamed-in command no reply" in actionTest { + protocol.action + .connectIn() + .send(processStreamedIn) + .send(command(single())) + .complete() + .expect(noReply()) + } + + "verify streamed-in command no reply with side effects" in actionTest { + protocol.action + .connectIn() + .send(processStreamedIn) + .send(command(single(sideEffectTo("a")))) + .send(command(single(sideEffectTo("b", synchronous = true)))) + .send(command(single(sideEffectTo("c"), sideEffectTo("d")))) + .complete() + .expect(noReply(sideEffects("a") ++ synchronousSideEffects("b") ++ sideEffects("c", "d"))) + } + + "verify streamed-in command forward" in actionTest { + protocol.action + .connectIn() + .send(processStreamedIn) + .send(command(single(forwardTo("one")))) + .complete() + .expect(forwarded("one")) + } + + "verify streamed-in command forward with side effects" in actionTest { + protocol.action + .connectIn() + .send(processStreamedIn) + .send(command(single(forwardTo("two"), sideEffectTo("a")))) + .send(command(single(sideEffectTo("b", synchronous = true)))) + .send(command(single(sideEffectTo("c"), sideEffectTo("d")))) + .complete() + .expect(forwarded("two", sideEffects("a") ++ synchronousSideEffects("b") ++ sideEffects("c", "d"))) + } + + "verify streamed-in command failure" in actionTest { + protocol.action + .connectIn() + .send(processStreamedIn) + .send(command(single(failWith("one")))) + .complete() + .expect(failure("one")) + } + + "verify streamed-in command failure with side effects" in actionTest { + protocol.action + .connectIn() + .send(processStreamedIn) + .send(command(single(failWith("two"), sideEffectTo("a")))) + .send(command(single(sideEffectTo("b", synchronous = true)))) + .send(command(single(sideEffectTo("c"), sideEffectTo("d")))) + .complete() + .expect(failure("two", sideEffects("a") ++ synchronousSideEffects("b") ++ sideEffects("c", "d"))) + } + + "verify streamed-out command replies and side effects" in actionTest { + protocol.action + .connectOut() + .send( + processStreamedOut( + request( + group(replyWith("one")), + group(replyWith("two"), sideEffectTo("other")), + group(replyWith("three"), sideEffectTo("another", synchronous = true)), + group(replyWith("four"), sideEffectTo("a"), sideEffectTo("b"), sideEffectTo("c")) + ) + ) + ) + .expect(reply(Response("one"))) + .expect(reply(Response("two"), sideEffects("other"))) + .expect(reply(Response("three"), synchronousSideEffects("another"))) + .expect(reply(Response("four"), sideEffects("a", "b", "c"))) + .expectClosed() + } + + "verify streamed-out command no replies and side effects" in actionTest { + protocol.action + .connectOut() + .send( + processStreamedOut( + request( + group(), + group(sideEffectTo("other")), + group(sideEffectTo("another", synchronous = true)), + group(sideEffectTo("a"), sideEffectTo("b"), sideEffectTo("c")) + ) + ) + ) + .expect(noReply()) + .expect(noReply(sideEffects("other"))) + .expect(noReply(synchronousSideEffects("another"))) + .expect(noReply(sideEffects("a", "b", "c"))) + .expectClosed() + } + + "verify streamed-out command forwards and side effects" in actionTest { + protocol.action + .connectOut() + .send( + processStreamedOut( + request( + group(forwardTo("one")), + group(forwardTo("two"), sideEffectTo("other")), + group(forwardTo("three"), sideEffectTo("another", synchronous = true)), + group(forwardTo("four"), sideEffectTo("a"), sideEffectTo("b"), sideEffectTo("c")) + ) + ) + ) + .expect(forwarded("one")) + .expect(forwarded("two", sideEffects("other"))) + .expect(forwarded("three", synchronousSideEffects("another"))) + .expect(forwarded("four", sideEffects("a", "b", "c"))) + .expectClosed() + } + + "verify streamed-out command failures and side effects" in actionTest { + protocol.action + .connectOut() + .send( + processStreamedOut( + request( + group(failWith("one")), + group(failWith("two"), sideEffectTo("other")), + group(failWith("three"), sideEffectTo("another", synchronous = true)), + group(failWith("four"), sideEffectTo("a"), sideEffectTo("b"), sideEffectTo("c")) + ) + ) + ) + .expect(failure("one")) + .expect(failure("two", sideEffects("other"))) + .expect(failure("three", synchronousSideEffects("another"))) + .expect(failure("four", sideEffects("a", "b", "c"))) + .expectClosed() + } + + "verify streamed command replies and side effects" in actionTest { + protocol.action + .connect() + .send(processStreamed) + .send(command(single(replyWith("one")))) + .expect(reply(Response("one"))) + .send(command(single(replyWith("two"), sideEffectTo("other")))) + .expect(reply(Response("two"), sideEffects("other"))) + .send(command(single(replyWith("three"), sideEffectTo("another", synchronous = true)))) + .expect(reply(Response("three"), synchronousSideEffects("another"))) + .send(command(single(replyWith("four"), sideEffectTo("a"), sideEffectTo("b"), sideEffectTo("c")))) + .expect(reply(Response("four"), sideEffects("a", "b", "c"))) + .send(command(request(group(replyWith("five")), group(replyWith("six"), sideEffectTo("other"))))) + .expect(reply(Response("five"))) + .expect(reply(Response("six"), sideEffects("other"))) + .complete() + } + + "verify streamed command no replies and side effects" in actionTest { + protocol.action + .connect() + .send(processStreamed) + .send(command(single())) + .expect(noReply()) + .send(command(single(sideEffectTo("other")))) + .expect(noReply(sideEffects("other"))) + .send(command(single(sideEffectTo("another", synchronous = true)))) + .expect(noReply(synchronousSideEffects("another"))) + .send(command(single(sideEffectTo("a"), sideEffectTo("b"), sideEffectTo("c")))) + .expect(noReply(sideEffects("a", "b", "c"))) + .send(command(request(group(), group(sideEffectTo("other"))))) + .expect(noReply()) + .expect(noReply(sideEffects("other"))) + .complete() + } + + "verify streamed command forwards and side effects" in actionTest { + protocol.action + .connect() + .send(processStreamed) + .send(command(single(forwardTo("one")))) + .expect(forwarded("one")) + .send(command(single(forwardTo("two"), sideEffectTo("other")))) + .expect(forwarded("two", sideEffects("other"))) + .send(command(single(forwardTo("three"), sideEffectTo("another", synchronous = true)))) + .expect(forwarded("three", synchronousSideEffects("another"))) + .send(command(single(forwardTo("four"), sideEffectTo("a"), sideEffectTo("b"), sideEffectTo("c")))) + .expect(forwarded("four", sideEffects("a", "b", "c"))) + .send(command(request(group(forwardTo("five")), group(forwardTo("six"), sideEffectTo("other"))))) + .expect(forwarded("five")) + .expect(forwarded("six", sideEffects("other"))) + .complete() + } + + "verify streamed command failures and side effects" in actionTest { + protocol.action + .connect() + .send(processStreamed) + .send(command(single(failWith("one")))) + .expect(failure("one")) + .send(command(single(failWith("two"), sideEffectTo("other")))) + .expect(failure("two", sideEffects("other"))) + .send(command(single(failWith("three"), sideEffectTo("another", synchronous = true)))) + .expect(failure("three", synchronousSideEffects("another"))) + .send(command(single(failWith("four"), sideEffectTo("a"), sideEffectTo("b"), sideEffectTo("c")))) + .expect(failure("four", sideEffects("a", "b", "c"))) + .send(command(request(group(failWith("five")), group(failWith("six"), sideEffectTo("other"))))) + .expect(failure("five")) + .expect(failure("six", sideEffects("other"))) + .complete() + } + } +} diff --git a/tck/src/main/scala/io/cloudstate/tck/CloudStateTCK.scala b/tck/src/main/scala/io/cloudstate/tck/CloudStateTCK.scala deleted file mode 100644 index 8645c5eb8..000000000 --- a/tck/src/main/scala/io/cloudstate/tck/CloudStateTCK.scala +++ /dev/null @@ -1,3353 +0,0 @@ -/* - * Copyright 2019 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 io.cloudstate.tck - -import java.util.concurrent.TimeUnit -import akka.actor.ActorSystem -import akka.grpc.ServiceDescription -import akka.stream.testkit.scaladsl.TestSink -import akka.testkit.TestKit -import com.example.shoppingcart.shoppingcart.{ - ShoppingCart => EventSourcedShoppingCart, - ShoppingCartClient => EventSourcedShoppingCartClient -} -import com.example.valueentity.shoppingcart.shoppingcart.{ - ShoppingCart => ValueEntityShoppingCart, - ShoppingCartClient => ValueEntityShoppingCartClient -} -import com.google.protobuf.{ByteString, DescriptorProtos} -import com.google.protobuf.any.{Any => ScalaPbAny} -import com.typesafe.config.{Config, ConfigFactory} -import io.cloudstate.protocol.action._ -import io.cloudstate.protocol.crdt._ -import io.cloudstate.protocol.value_entity._ -import io.cloudstate.protocol.event_sourced._ -import io.cloudstate.protocol.entity._ -import io.cloudstate.tck.model.valueentity.valueentity.{ValueEntityTckModel, ValueEntityTwo} -import io.cloudstate.tck.model.action.{ActionTckModel, ActionTwo} -import io.cloudstate.tck.model.entitypassivation.entitypassivation.{PassivationTckModel, PassivationTckModelClient} -import io.cloudstate.tck.model.crdt.{CrdtTckModel, CrdtTckModelClient, CrdtTwo} -import io.cloudstate.tck.model.eventlogeventing.{EmitEventRequest, EventLogSubscriberModel} -import io.cloudstate.tck.model.eventlogeventing -import io.cloudstate.testkit.InterceptService.InterceptorSettings -import io.cloudstate.testkit.eventsourced.EventSourcedMessages -import io.cloudstate.testkit.{InterceptService, ServiceAddress, TestClient, TestProtocol} -import io.grpc.StatusRuntimeException -import io.cloudstate.tck.model.eventsourced.{EventSourcedTckModel, EventSourcedTwo} -import io.cloudstate.testkit.valueentity.ValueEntityMessages -import org.scalatest.concurrent.ScalaFutures -import org.scalatest.{BeforeAndAfter, BeforeAndAfterAll, MustMatchers, WordSpec} - -import scala.concurrent.duration._ - -object CloudStateTCK { - final case class Settings(tck: ServiceAddress, proxy: ServiceAddress, service: ServiceAddress) - - object Settings { - def fromConfig(config: Config): Settings = { - val tckConfig = config.getConfig("cloudstate.tck") - Settings( - ServiceAddress(tckConfig.getString("hostname"), tckConfig.getInt("port")), - ServiceAddress(tckConfig.getString("proxy.hostname"), tckConfig.getInt("proxy.port")), - ServiceAddress(tckConfig.getString("service.hostname"), tckConfig.getInt("service.port")) - ) - } - } -} - -class ConfiguredCloudStateTCK extends CloudStateTCK(CloudStateTCK.Settings.fromConfig(ConfigFactory.load())) - -class CloudStateTCK(description: String, settings: CloudStateTCK.Settings) - extends WordSpec - with MustMatchers - with BeforeAndAfterAll - with BeforeAndAfter - with ScalaFutures { - - def this(settings: CloudStateTCK.Settings) = this("", settings) - - private[this] final val system = ActorSystem("CloudStateTCK", ConfigFactory.load("tck")) - - private[this] final val client = TestClient(settings.proxy.host, settings.proxy.port) - private[this] final val eventSourcedShoppingCartClient = EventSourcedShoppingCartClient(client.settings)(system) - private[this] final val valueEntityShoppingCartClient = ValueEntityShoppingCartClient(client.settings)(system) - private[this] final val valueEntityPassivationEntityClient = PassivationTckModelClient(client.settings)(system) - private[this] final val crdtTckModelClient = CrdtTckModelClient(client.settings)(client.context.system) - private[this] final val eventLogEventingEventSourcedEntityOne = - eventlogeventing.EventSourcedEntityOneClient(client.settings)(system) - private[this] final val eventLogEventingEventSourcedEntityTwo = - eventlogeventing.EventSourcedEntityTwoClient(client.settings)(system) - - private[this] final val protocol = TestProtocol(settings.service.host, settings.service.port) - - @volatile private[this] final var interceptor: InterceptService = _ - @volatile private[this] final var enabledServices = Seq.empty[String] - @volatile private[this] final var passivationEntities = Seq.empty[Entity] - - override implicit val patienceConfig: PatienceConfig = PatienceConfig(timeout = 3.seconds, interval = 100.millis) - - override def beforeAll(): Unit = - interceptor = new InterceptService(InterceptorSettings(bind = settings.tck, intercept = settings.service)) - - override def afterAll(): Unit = - try eventSourcedShoppingCartClient.close().futureValue - finally try valueEntityShoppingCartClient.close().futureValue - finally try crdtTckModelClient.close().futureValue - finally try valueEntityPassivationEntityClient.close().futureValue - finally try client.terminate() - finally try protocol.terminate() - finally interceptor.terminate() - - after { - interceptor.verifyNoMoreInteractions() - } - - def expectProxyOnline(): Unit = - TestKit.awaitCond(client.http.probe(), max = 10.seconds) - - def testFor(services: ServiceDescription*)(test: => Any): Unit = { - val enabled = services.map(_.name).forall(enabledServices.contains) - if (enabled) test else pending - } - - ("Cloudstate TCK " + description) when { - - "verifying discovery protocol" must { - "verify proxy info and entity discovery" in { - import scala.jdk.CollectionConverters._ - - expectProxyOnline() - - val discovery = interceptor.expectEntityDiscovery() - - val info = discovery.expectProxyInfo() - - info.protocolMajorVersion mustBe 0 - info.protocolMinorVersion mustBe 2 - - info.supportedEntityTypes must contain theSameElementsAs Seq( - EventSourced.name, - Crdt.name, - ValueEntity.name, - ActionProtocol.name - ) - - val spec = discovery.expectEntitySpec() - - val descriptorSet = DescriptorProtos.FileDescriptorSet.parseFrom(spec.proto) - val serviceNames = descriptorSet.getFileList.asScala.flatMap(_.getServiceList.asScala.map(_.getName)) - - serviceNames.size mustBe spec.entities.size - - spec.entities.find(_.serviceName == ActionTckModel.name).foreach { entity => - serviceNames must contain("ActionTckModel") - entity.entityType mustBe ActionProtocol.name - } - - spec.entities.find(_.serviceName == ActionTwo.name).foreach { entity => - serviceNames must contain("ActionTwo") - entity.entityType mustBe ActionProtocol.name - } - - spec.entities.find(_.serviceName == CrdtTckModel.name).foreach { entity => - serviceNames must contain("CrdtTckModel") - entity.entityType mustBe Crdt.name - } - - spec.entities.find(_.serviceName == CrdtTwo.name).foreach { entity => - serviceNames must contain("CrdtTwo") - entity.entityType mustBe Crdt.name - } - - spec.entities.find(_.serviceName == EventSourcedTckModel.name).foreach { entity => - serviceNames must contain("EventSourcedTckModel") - entity.entityType mustBe EventSourced.name - entity.persistenceId mustBe "event-sourced-tck-model" - } - - spec.entities.find(_.serviceName == EventSourcedTwo.name).foreach { entity => - serviceNames must contain("EventSourcedTwo") - entity.entityType mustBe EventSourced.name - } - - spec.entities.find(_.serviceName == EventSourcedShoppingCart.name).foreach { entity => - serviceNames must contain("ShoppingCart") - entity.entityType mustBe EventSourced.name - entity.persistenceId must not be empty - } - - spec.entities.find(_.serviceName == ValueEntityTckModel.name).foreach { entity => - serviceNames must contain("ValueEntityTckModel") - entity.entityType mustBe ValueEntity.name - entity.persistenceId mustBe "value-entity-tck-model" - } - - spec.entities.find(_.serviceName == ValueEntityTwo.name).foreach { entity => - serviceNames must contain("ValueEntityTwo") - entity.entityType mustBe ValueEntity.name - entity.persistenceId mustBe "value-entity-tck-model-two" - } - - spec.entities.find(_.serviceName == ValueEntityShoppingCart.name).foreach { entity => - serviceNames must contain("ShoppingCart") - entity.entityType mustBe ValueEntity.name - entity.persistenceId must not be empty - } - - enabledServices = spec.entities.map(_.serviceName) - passivationEntities = spec.entities.filter(_.serviceName == PassivationTckModel.name) - } - } - - "verifying model test: entity passivation" must { - import ValueEntityMessages._ - import io.cloudstate.tck.model.entitypassivation.entitypassivation._ - import io.cloudstate.protocol.entity.EntityPassivationStrategy._ - - var entityId: Int = 0 - def nextEntityId(): String = { entityId += 1; s"entity:$entityId" } - - def passivationEntityTest(test: String => Any): Unit = - testFor(PassivationTckModel)(test(nextEntityId())) - - def passivationTimeout(entity: Entity): FiniteDuration = { - val additionalTimeout = 1000 // additional time in millis for allowing passivation - val timeoutStrategy = entity.passivationStrategy.get.strategy.timeout - Duration(timeoutStrategy.get.timeout + additionalTimeout, TimeUnit.MILLISECONDS) - } - - "verify timeout passivation is enabled for value-based entity" in { - passivationEntities.find(_.serviceName == PassivationTckModel.name).foreach { entity => - entity.entityType mustBe ValueEntity.name - entity.persistenceId mustBe "entity-passivation-tck-model" - entity.passivationStrategy mustBe Some( - EntityPassivationStrategy(Strategy.Timeout(TimeoutPassivationStrategy(2000))) - ) - } - } - - "verify value-based entity is passivated after timeout" in passivationEntityTest { id => - passivationEntities.find(_.serviceName == PassivationTckModel.name).foreach { entity => - val timeout = passivationTimeout(entity) - valueEntityPassivationEntityClient.activate(Request(id)) - - val connection = interceptor.expectValueBasedConnection() - connection.expectClient(init(PassivationTckModel.name, id)) - connection.expectClient(command(1, id, "Activate", Request(id))) - connection.expectService(reply(1, Response("state"))) - connection.expectOutClosed(timeout) // check passivation - connection.expectInClosed(timeout) // check passivation - } - } - } - - "verifying model test: actions" must { - import io.cloudstate.tck.model.action._ - import io.cloudstate.testkit.action.ActionMessages._ - - val Service = ActionTckModel.name - val ServiceTwo = ActionTwo.name - - def actionTest(test: => Any): Unit = - testFor(ActionTckModel, ActionTwo)(test) - - def processUnary(request: Request): ActionCommand = - command(Service, "ProcessUnary", request) - - val processStreamedIn: ActionCommand = - command(Service, "ProcessStreamedIn") - - def processStreamedOut(request: Request): ActionCommand = - command(Service, "ProcessStreamedOut", request) - - val processStreamed: ActionCommand = - command(Service, "ProcessStreamed") - - def single(steps: ProcessStep*): Request = - request(group(steps: _*)) - - def request(groups: ProcessGroup*): Request = - Request(groups) - - def group(steps: ProcessStep*): ProcessGroup = - ProcessGroup(steps) - - def replyWith(message: String): ProcessStep = - ProcessStep(ProcessStep.Step.Reply(Reply(message))) - - def sideEffectTo(id: String, synchronous: Boolean = false): ProcessStep = - ProcessStep(ProcessStep.Step.Effect(SideEffect(id, synchronous))) - - def forwardTo(id: String): ProcessStep = - ProcessStep(ProcessStep.Step.Forward(Forward(id))) - - def failWith(message: String): ProcessStep = - ProcessStep(ProcessStep.Step.Fail(Fail(message))) - - def sideEffects(ids: String*): SideEffects = - createSideEffects(synchronous = false, ids) - - def synchronousSideEffects(ids: String*): SideEffects = - createSideEffects(synchronous = true, ids) - - def createSideEffects(synchronous: Boolean, ids: Seq[String]): SideEffects = - ids.map(id => sideEffect(ServiceTwo, "Call", OtherRequest(id), synchronous)) - - def forwarded(id: String, sideEffects: SideEffects = Seq.empty): ActionResponse = - forward(ServiceTwo, "Call", OtherRequest(id), sideEffects) - - "verify unary command reply" in actionTest { - protocol.action.unary - .send(processUnary(single(replyWith("one")))) - .expect(reply(Response("one"))) - } - - "verify unary command reply with side effect" in actionTest { - protocol.action.unary - .send(processUnary(single(replyWith("two"), sideEffectTo("other")))) - .expect(reply(Response("two"), sideEffects("other"))) - } - - "verify unary command reply with synchronous side effect" in actionTest { - protocol.action.unary - .send(processUnary(single(replyWith("three"), sideEffectTo("another", synchronous = true)))) - .expect(reply(Response("three"), synchronousSideEffects("another"))) - } - - "verify unary command reply with multiple side effects" in actionTest { - protocol.action.unary - .send(processUnary(single(replyWith("four"), sideEffectTo("a"), sideEffectTo("b"), sideEffectTo("c")))) - .expect(reply(Response("four"), sideEffects("a", "b", "c"))) - } - - "verify unary command no reply" in actionTest { - protocol.action.unary - .send(processUnary(single())) - .expect(noReply()) - } - - "verify unary command no reply with side effect" in actionTest { - protocol.action.unary - .send(processUnary(single(sideEffectTo("other")))) - .expect(noReply(sideEffects("other"))) - } - - "verify unary command no reply with synchronous side effect" in actionTest { - protocol.action.unary - .send(processUnary(single(sideEffectTo("another", synchronous = true)))) - .expect(noReply(synchronousSideEffects("another"))) - } - - "verify unary command no reply with multiple side effects" in actionTest { - protocol.action.unary - .send(processUnary(single(sideEffectTo("a"), sideEffectTo("b"), sideEffectTo("c")))) - .expect(noReply(sideEffects("a", "b", "c"))) - } - - "verify unary command forward" in actionTest { - protocol.action.unary - .send(processUnary(single(forwardTo("one")))) - .expect(forwarded("one")) - } - - "verify unary command forward with side effect" in actionTest { - protocol.action.unary - .send(processUnary(single(forwardTo("two"), sideEffectTo("other")))) - .expect(forwarded("two", sideEffects("other"))) - } - - "verify unary command forward with synchronous side effect" in actionTest { - protocol.action.unary - .send(processUnary(single(forwardTo("three"), sideEffectTo("another", synchronous = true)))) - .expect(forwarded("three", synchronousSideEffects("another"))) - } - - "verify unary command forward with multiple side effects" in actionTest { - protocol.action.unary - .send(processUnary(single(forwardTo("four"), sideEffectTo("a"), sideEffectTo("b"), sideEffectTo("c")))) - .expect(forwarded("four", sideEffects("a", "b", "c"))) - } - - "verify unary command failure" in actionTest { - protocol.action.unary - .send(processUnary(single(failWith("one")))) - .expect(failure("one")) - } - - "verify unary command failure with side effect" in actionTest { - protocol.action.unary - .send(processUnary(single(failWith("two"), sideEffectTo("other")))) - .expect(failure("two", sideEffects("other"))) - } - - "verify unary command failure with synchronous side effect" in actionTest { - protocol.action.unary - .send(processUnary(single(failWith("three"), sideEffectTo("another", synchronous = true)))) - .expect(failure("three", synchronousSideEffects("another"))) - } - - "verify unary command failure with multiple side effects" in actionTest { - protocol.action.unary - .send(processUnary(single(failWith("four"), sideEffectTo("a"), sideEffectTo("b"), sideEffectTo("c")))) - .expect(failure("four", sideEffects("a", "b", "c"))) - } - - "verify streamed-in command reply" in actionTest { - protocol.action - .connectIn() - .send(processStreamedIn) - .send(command(single(replyWith("one")))) - .complete() - .expect(reply(Response("one"))) - } - - "verify streamed-in command reply with side effects" in actionTest { - protocol.action - .connectIn() - .send(processStreamedIn) - .send(command(single(replyWith("two"), sideEffectTo("a")))) - .send(command(single(sideEffectTo("b", synchronous = true)))) - .send(command(single(sideEffectTo("c"), sideEffectTo("d")))) - .complete() - .expect(reply(Response("two"), sideEffects("a") ++ synchronousSideEffects("b") ++ sideEffects("c", "d"))) - } - - "verify streamed-in command no reply (no requests)" in actionTest { - protocol.action - .connectIn() - .send(processStreamedIn) - .complete() - .expect(noReply()) - } - - "verify streamed-in command no reply" in actionTest { - protocol.action - .connectIn() - .send(processStreamedIn) - .send(command(single())) - .complete() - .expect(noReply()) - } - - "verify streamed-in command no reply with side effects" in actionTest { - protocol.action - .connectIn() - .send(processStreamedIn) - .send(command(single(sideEffectTo("a")))) - .send(command(single(sideEffectTo("b", synchronous = true)))) - .send(command(single(sideEffectTo("c"), sideEffectTo("d")))) - .complete() - .expect(noReply(sideEffects("a") ++ synchronousSideEffects("b") ++ sideEffects("c", "d"))) - } - - "verify streamed-in command forward" in actionTest { - protocol.action - .connectIn() - .send(processStreamedIn) - .send(command(single(forwardTo("one")))) - .complete() - .expect(forwarded("one")) - } - - "verify streamed-in command forward with side effects" in actionTest { - protocol.action - .connectIn() - .send(processStreamedIn) - .send(command(single(forwardTo("two"), sideEffectTo("a")))) - .send(command(single(sideEffectTo("b", synchronous = true)))) - .send(command(single(sideEffectTo("c"), sideEffectTo("d")))) - .complete() - .expect(forwarded("two", sideEffects("a") ++ synchronousSideEffects("b") ++ sideEffects("c", "d"))) - } - - "verify streamed-in command failure" in actionTest { - protocol.action - .connectIn() - .send(processStreamedIn) - .send(command(single(failWith("one")))) - .complete() - .expect(failure("one")) - } - - "verify streamed-in command failure with side effects" in actionTest { - protocol.action - .connectIn() - .send(processStreamedIn) - .send(command(single(failWith("two"), sideEffectTo("a")))) - .send(command(single(sideEffectTo("b", synchronous = true)))) - .send(command(single(sideEffectTo("c"), sideEffectTo("d")))) - .complete() - .expect(failure("two", sideEffects("a") ++ synchronousSideEffects("b") ++ sideEffects("c", "d"))) - } - - "verify streamed-out command replies and side effects" in actionTest { - protocol.action - .connectOut() - .send( - processStreamedOut( - request( - group(replyWith("one")), - group(replyWith("two"), sideEffectTo("other")), - group(replyWith("three"), sideEffectTo("another", synchronous = true)), - group(replyWith("four"), sideEffectTo("a"), sideEffectTo("b"), sideEffectTo("c")) - ) - ) - ) - .expect(reply(Response("one"))) - .expect(reply(Response("two"), sideEffects("other"))) - .expect(reply(Response("three"), synchronousSideEffects("another"))) - .expect(reply(Response("four"), sideEffects("a", "b", "c"))) - .expectClosed() - } - - "verify streamed-out command no replies and side effects" in actionTest { - protocol.action - .connectOut() - .send( - processStreamedOut( - request( - group(), - group(sideEffectTo("other")), - group(sideEffectTo("another", synchronous = true)), - group(sideEffectTo("a"), sideEffectTo("b"), sideEffectTo("c")) - ) - ) - ) - .expect(noReply()) - .expect(noReply(sideEffects("other"))) - .expect(noReply(synchronousSideEffects("another"))) - .expect(noReply(sideEffects("a", "b", "c"))) - .expectClosed() - } - - "verify streamed-out command forwards and side effects" in actionTest { - protocol.action - .connectOut() - .send( - processStreamedOut( - request( - group(forwardTo("one")), - group(forwardTo("two"), sideEffectTo("other")), - group(forwardTo("three"), sideEffectTo("another", synchronous = true)), - group(forwardTo("four"), sideEffectTo("a"), sideEffectTo("b"), sideEffectTo("c")) - ) - ) - ) - .expect(forwarded("one")) - .expect(forwarded("two", sideEffects("other"))) - .expect(forwarded("three", synchronousSideEffects("another"))) - .expect(forwarded("four", sideEffects("a", "b", "c"))) - .expectClosed() - } - - "verify streamed-out command failures and side effects" in actionTest { - protocol.action - .connectOut() - .send( - processStreamedOut( - request( - group(failWith("one")), - group(failWith("two"), sideEffectTo("other")), - group(failWith("three"), sideEffectTo("another", synchronous = true)), - group(failWith("four"), sideEffectTo("a"), sideEffectTo("b"), sideEffectTo("c")) - ) - ) - ) - .expect(failure("one")) - .expect(failure("two", sideEffects("other"))) - .expect(failure("three", synchronousSideEffects("another"))) - .expect(failure("four", sideEffects("a", "b", "c"))) - .expectClosed() - } - - "verify streamed command replies and side effects" in actionTest { - protocol.action - .connect() - .send(processStreamed) - .send(command(single(replyWith("one")))) - .expect(reply(Response("one"))) - .send(command(single(replyWith("two"), sideEffectTo("other")))) - .expect(reply(Response("two"), sideEffects("other"))) - .send(command(single(replyWith("three"), sideEffectTo("another", synchronous = true)))) - .expect(reply(Response("three"), synchronousSideEffects("another"))) - .send(command(single(replyWith("four"), sideEffectTo("a"), sideEffectTo("b"), sideEffectTo("c")))) - .expect(reply(Response("four"), sideEffects("a", "b", "c"))) - .send(command(request(group(replyWith("five")), group(replyWith("six"), sideEffectTo("other"))))) - .expect(reply(Response("five"))) - .expect(reply(Response("six"), sideEffects("other"))) - .complete() - } - - "verify streamed command no replies and side effects" in actionTest { - protocol.action - .connect() - .send(processStreamed) - .send(command(single())) - .expect(noReply()) - .send(command(single(sideEffectTo("other")))) - .expect(noReply(sideEffects("other"))) - .send(command(single(sideEffectTo("another", synchronous = true)))) - .expect(noReply(synchronousSideEffects("another"))) - .send(command(single(sideEffectTo("a"), sideEffectTo("b"), sideEffectTo("c")))) - .expect(noReply(sideEffects("a", "b", "c"))) - .send(command(request(group(), group(sideEffectTo("other"))))) - .expect(noReply()) - .expect(noReply(sideEffects("other"))) - .complete() - } - - "verify streamed command forwards and side effects" in actionTest { - protocol.action - .connect() - .send(processStreamed) - .send(command(single(forwardTo("one")))) - .expect(forwarded("one")) - .send(command(single(forwardTo("two"), sideEffectTo("other")))) - .expect(forwarded("two", sideEffects("other"))) - .send(command(single(forwardTo("three"), sideEffectTo("another", synchronous = true)))) - .expect(forwarded("three", synchronousSideEffects("another"))) - .send(command(single(forwardTo("four"), sideEffectTo("a"), sideEffectTo("b"), sideEffectTo("c")))) - .expect(forwarded("four", sideEffects("a", "b", "c"))) - .send(command(request(group(forwardTo("five")), group(forwardTo("six"), sideEffectTo("other"))))) - .expect(forwarded("five")) - .expect(forwarded("six", sideEffects("other"))) - .complete() - } - - "verify streamed command failures and side effects" in actionTest { - protocol.action - .connect() - .send(processStreamed) - .send(command(single(failWith("one")))) - .expect(failure("one")) - .send(command(single(failWith("two"), sideEffectTo("other")))) - .expect(failure("two", sideEffects("other"))) - .send(command(single(failWith("three"), sideEffectTo("another", synchronous = true)))) - .expect(failure("three", synchronousSideEffects("another"))) - .send(command(single(failWith("four"), sideEffectTo("a"), sideEffectTo("b"), sideEffectTo("c")))) - .expect(failure("four", sideEffects("a", "b", "c"))) - .send(command(request(group(failWith("five")), group(failWith("six"), sideEffectTo("other"))))) - .expect(failure("five")) - .expect(failure("six", sideEffects("other"))) - .complete() - } - } - - "verifying model test: CRDT entities" must { - import io.cloudstate.tck.model.crdt._ - import io.cloudstate.testkit.crdt.CrdtMessages._ - - val ServiceTwo = CrdtTwo.name - - var entityId: Int = 0 - - def nextEntityId(crdtType: String): String = { - entityId += 1; s"$crdtType-$entityId" - } - - def crdtTest(crdtType: String)(test: String => Any): Unit = - testFor(CrdtTckModel, CrdtTwo)(test(nextEntityId(crdtType))) - - def requestUpdate(update: Update): RequestAction = - RequestAction(RequestAction.Action.Update(update)) - - val requestDelete: RequestAction = - RequestAction(RequestAction.Action.Delete(Delete())) - - val deleteCrdt: Effects = - Effects(stateAction = crdtDelete) - - def forwardTo(id: String): RequestAction = - RequestAction(RequestAction.Action.Forward(Forward(id))) - - def sideEffectTo(id: String, synchronous: Boolean = false): RequestAction = - RequestAction(RequestAction.Action.Effect(Effect(id, synchronous))) - - def sideEffectsTo(ids: String*): Seq[RequestAction] = - ids.map(id => sideEffectTo(id, synchronous = false)) - - def sideEffects(ids: String*): Effects = - createSideEffects(synchronous = false, ids) - - def synchronousSideEffects(ids: String*): Effects = - createSideEffects(synchronous = true, ids) - - def createSideEffects(synchronous: Boolean, ids: Seq[String]): Effects = - ids.foldLeft(Effects.empty) { case (e, id) => e.withSideEffect(ServiceTwo, "Call", Request(id), synchronous) } - - def forwarded(id: Long, entityId: String, effects: Effects = Effects.empty) = - forward(id, ServiceTwo, "Call", Request(entityId), effects) - - def failWith(message: String): RequestAction = - RequestAction(RequestAction.Action.Fail(Fail(message))) - - // Sort sequences in received Deltas (using ScalaPB Lens support) for comparing easily - object Delta { - def sorted(out: CrdtStreamOut): CrdtStreamOut = - out.update(_.reply.stateAction.update.modify(Delta.sort)) - - def sort(delta: CrdtDelta): CrdtDelta = - if (delta.delta.isGset) - delta.update( - _.gset.added.modify(_.sortBy(readPrimitiveString)) - ) - else if (delta.delta.isOrset) - delta.update( - _.orset.update( - _.removed.modify(_.sortBy(readPrimitiveString)), - _.added.modify(_.sortBy(readPrimitiveString)) - ) - ) - else if (delta.delta.isOrmap) - delta.update( - _.ormap.update( - _.removed.modify(_.sortBy(readPrimitiveString)), - _.updated.modify(_.map(_.update(_.delta.modify(Delta.sort))).sortBy(_.key.map(readPrimitiveString))), - _.added.modify(_.map(_.update(_.delta.modify(Delta.sort))).sortBy(_.key.map(readPrimitiveString))) - ) - ) - else delta - } - - // GCounter tests - - object GCounter { - def state(value: Long): Response = - Response(Some(State(State.Value.Gcounter(GCounterValue(value))))) - - def incrementBy(increments: Long*): Seq[RequestAction] = - increments.map(increment => requestUpdate(updateWith(increment))) - - def updateWith(increment: Long): Update = - Update(Update.Update.Gcounter(GCounterUpdate(increment))) - - def update(value: Long): Effects = - Effects(stateAction = crdtUpdate(delta(value))) - - def delta(value: Long): CrdtDelta.Delta.Gcounter = deltaGCounter(value) - - def test(run: String => Any): Unit = crdtTest("GCounter")(run) - } - - "verify GCounter initial empty state" in GCounter.test { id => - protocol.crdt - .connect() - .send(init(CrdtTckModel.name, id)) - .send(command(1, id, "Process", Request(id))) - .expect(reply(1, GCounter.state(0))) - .passivate() - } - - "verify GCounter state changes" in GCounter.test { id => - protocol.crdt - .connect() - .send(init(CrdtTckModel.name, id)) - .send(command(1, id, "Process", Request(id, GCounter.incrementBy(42)))) - .expect(reply(1, GCounter.state(42), GCounter.update(42))) - .send(command(2, id, "Process", Request(id, GCounter.incrementBy(1, 2, 3)))) - .expect(reply(2, GCounter.state(48), GCounter.update(6))) - .passivate() - } - - "verify GCounter initial delta in init" in GCounter.test { id => - protocol.crdt - .connect() - .send(init(CrdtTckModel.name, id, GCounter.delta(42))) - .send(command(1, id, "Process", Request(id, GCounter.incrementBy(123)))) - .expect(reply(1, GCounter.state(165), GCounter.update(123))) - .passivate() - } - - "verify GCounter initial empty state with replicated initial delta" in GCounter.test { id => - protocol.crdt - .connect() - .send(init(CrdtTckModel.name, id)) - .send(delta(GCounter.delta(42))) - .send(command(1, id, "Process", Request(id, GCounter.incrementBy(123)))) - .expect(reply(1, GCounter.state(165), GCounter.update(123))) - .passivate() - } - - "verify GCounter mix of local and replicated state changes" in GCounter.test { id => - protocol.crdt - .connect() - .send(init(CrdtTckModel.name, id)) - .send(command(1, id, "Process", Request(id, GCounter.incrementBy(1)))) - .expect(reply(1, GCounter.state(1), GCounter.update(1))) - .send(delta(GCounter.delta(2))) - .send(delta(GCounter.delta(3))) - .send(command(2, id, "Process", Request(id))) - .expect(reply(2, GCounter.state(6))) - .send(command(3, id, "Process", Request(id, GCounter.incrementBy(4)))) - .expect(reply(3, GCounter.state(10), GCounter.update(4))) - .send(delta(GCounter.delta(5))) - .send(command(4, id, "Process", Request(id, GCounter.incrementBy(6)))) - .expect(reply(4, GCounter.state(21), GCounter.update(6))) - .passivate() - } - - "verify GCounter rehydration after passivation" in GCounter.test { id => - protocol.crdt - .connect() - .send(init(CrdtTckModel.name, id)) - .send(command(1, id, "Process", Request(id, GCounter.incrementBy(1)))) - .expect(reply(1, GCounter.state(1), GCounter.update(1))) - .send(delta(GCounter.delta(2))) - .send(command(2, id, "Process", Request(id))) - .expect(reply(2, GCounter.state(3))) - .send(command(3, id, "Process", Request(id, GCounter.incrementBy(3)))) - .expect(reply(3, GCounter.state(6), GCounter.update(3))) - .passivate() - protocol.crdt - .connect() - .send(init(CrdtTckModel.name, id, GCounter.delta(6))) - .send(command(1, id, "Process", Request(id))) - .expect(reply(1, GCounter.state(6))) - .send(delta(GCounter.delta(4))) - .send(command(2, id, "Process", Request(id, GCounter.incrementBy(5)))) - .expect(reply(2, GCounter.state(15), GCounter.update(5))) - .passivate() - } - - "verify GCounter delete action received from entity" in GCounter.test { id => - protocol.crdt - .connect() - .send(init(CrdtTckModel.name, id)) - .send(command(1, id, "Process", Request(id, GCounter.incrementBy(42)))) - .expect(reply(1, GCounter.state(42), GCounter.update(42))) - .send(command(2, id, "Process", Request(id, Seq(requestDelete)))) - .expect(reply(2, GCounter.state(42), deleteCrdt)) - .passivate() - } - - "verify GCounter delete action sent to entity" in GCounter.test { id => - protocol.crdt - .connect() - .send(init(CrdtTckModel.name, id)) - .send(delete) - .passivate() - } - - // PNCounter tests - - object PNCounter { - def state(value: Long): Response = - Response(Some(State(State.Value.Pncounter(PNCounterValue(value))))) - - def changeBy(changes: Long*): Seq[RequestAction] = - changes.map(change => requestUpdate(updateWith(change))) - - def updateWith(change: Long): Update = - Update(Update.Update.Pncounter(PNCounterUpdate(change))) - - def update(value: Long): Effects = - Effects(stateAction = crdtUpdate(delta(value))) - - def delta(value: Long): CrdtDelta.Delta.Pncounter = deltaPNCounter(value) - - def test(run: String => Any): Unit = crdtTest("PNCounter")(run) - } - - "verify PNCounter initial empty state" in PNCounter.test { id => - protocol.crdt - .connect() - .send(init(CrdtTckModel.name, id)) - .send(command(1, id, "Process", Request(id))) - .expect(reply(1, PNCounter.state(0))) - .passivate() - } - - "verify PNCounter state changes" in PNCounter.test { id => - protocol.crdt - .connect() - .send(init(CrdtTckModel.name, id)) - .send(command(1, id, "Process", Request(id, PNCounter.changeBy(+1, -2, +3)))) - .expect(reply(1, PNCounter.state(+2), PNCounter.update(+2))) - .send(command(2, id, "Process", Request(id, PNCounter.changeBy(-4, +5, -6)))) - .expect(reply(2, PNCounter.state(-3), PNCounter.update(-5))) - .passivate() - } - - "verify PNCounter initial delta in init" in PNCounter.test { id => - protocol.crdt - .connect() - .send(init(CrdtTckModel.name, id, PNCounter.delta(42))) - .send(command(1, id, "Process", Request(id, PNCounter.changeBy(-123)))) - .expect(reply(1, PNCounter.state(-81), PNCounter.update(-123))) - .passivate() - } - - "verify PNCounter initial empty state with replicated initial delta" in PNCounter.test { id => - protocol.crdt - .connect() - .send(init(CrdtTckModel.name, id)) - .send(delta(PNCounter.delta(-42))) - .send(command(1, id, "Process", Request(id, PNCounter.changeBy(+123)))) - .expect(reply(1, PNCounter.state(81), PNCounter.update(+123))) - .passivate() - } - - "verify PNCounter mix of local and replicated state changes" in PNCounter.test { id => - protocol.crdt - .connect() - .send(init(CrdtTckModel.name, id)) - .send(command(1, id, "Process", Request(id, PNCounter.changeBy(+1)))) - .expect(reply(1, PNCounter.state(+1), PNCounter.update(+1))) - .send(delta(PNCounter.delta(-2))) - .send(delta(PNCounter.delta(+3))) - .send(command(2, id, "Process", Request(id))) - .expect(reply(2, PNCounter.state(+2))) - .send(command(3, id, "Process", Request(id, PNCounter.changeBy(-4)))) - .expect(reply(3, PNCounter.state(-2), PNCounter.update(-4))) - .send(delta(PNCounter.delta(+5))) - .send(command(4, id, "Process", Request(id, PNCounter.changeBy(-6)))) - .expect(reply(4, PNCounter.state(-3), PNCounter.update(-6))) - .passivate() - } - - "verify PNCounter rehydration after passivation" in PNCounter.test { id => - protocol.crdt - .connect() - .send(init(CrdtTckModel.name, id)) - .send(command(1, id, "Process", Request(id, PNCounter.changeBy(+1)))) - .expect(reply(1, PNCounter.state(+1), PNCounter.update(+1))) - .send(delta(PNCounter.delta(-2))) - .send(command(2, id, "Process", Request(id))) - .expect(reply(2, PNCounter.state(-1))) - .send(command(3, id, "Process", Request(id, PNCounter.changeBy(+3)))) - .expect(reply(3, PNCounter.state(+2), PNCounter.update(+3))) - .passivate() - protocol.crdt - .connect() - .send(init(CrdtTckModel.name, id, PNCounter.delta(+2))) - .send(command(1, id, "Process", Request(id))) - .expect(reply(1, PNCounter.state(+2))) - .send(delta(PNCounter.delta(-4))) - .send(command(2, id, "Process", Request(id, PNCounter.changeBy(+5)))) - .expect(reply(2, PNCounter.state(+3), PNCounter.update(+5))) - .passivate() - } - - "verify PNCounter delete action received from entity" in PNCounter.test { id => - protocol.crdt - .connect() - .send(init(CrdtTckModel.name, id)) - .send(command(1, id, "Process", Request(id, PNCounter.changeBy(+42)))) - .expect(reply(1, PNCounter.state(+42), PNCounter.update(+42))) - .send(command(2, id, "Process", Request(id, Seq(requestDelete)))) - .expect(reply(2, PNCounter.state(+42), deleteCrdt)) - .passivate() - } - - "verify PNCounter delete action sent to entity" in PNCounter.test { id => - protocol.crdt - .connect() - .send(init(CrdtTckModel.name, id)) - .send(delete) - .passivate() - } - - // GSet tests - - object GSet { - def state(elements: String*): Response = - Response(Some(State(State.Value.Gset(GSetValue(elements))))) - - def add(elements: String*): Seq[RequestAction] = - elements.map(element => requestUpdate(updateWith(element))) - - def updateWith(element: String): Update = - Update(Update.Update.Gset(GSetUpdate(element))) - - def update(added: String*): Effects = - Effects(stateAction = crdtUpdate(delta(added: _*))) - - def delta(added: String*): CrdtDelta.Delta.Gset = deltaGSet(added.map(primitiveString)) - - def test(run: String => Any): Unit = crdtTest("GSet")(run) - } - - "verify GSet initial empty state" in GSet.test { id => - protocol.crdt - .connect() - .send(init(CrdtTckModel.name, id)) - .send(command(1, id, "Process", Request(id))) - .expect(reply(1, GSet.state())) - .passivate() - } - - "verify GSet state changes" in GSet.test { id => - protocol.crdt - .connect() - .send(init(CrdtTckModel.name, id)) - .send(command(1, id, "Process", Request(id, GSet.add("a", "b")))) - .expect(reply(1, GSet.state("a", "b"), GSet.update("a", "b")), Delta.sorted) - .send(command(2, id, "Process", Request(id, GSet.add("b", "c")))) - .expect(reply(2, GSet.state("a", "b", "c"), GSet.update("c"))) - .send(command(3, id, "Process", Request(id, GSet.add("c", "a")))) - .expect(reply(3, GSet.state("a", "b", "c"))) - .passivate() - } - - "verify GSet initial delta in init" in GSet.test { id => - protocol.crdt - .connect() - .send(init(CrdtTckModel.name, id, GSet.delta("a", "b", "c"))) - .send(command(1, id, "Process", Request(id, GSet.add("c", "d", "e")))) - .expect(reply(1, GSet.state("a", "b", "c", "d", "e"), GSet.update("d", "e")), Delta.sorted) - .passivate() - } - - "verify GSet initial empty state with replicated initial delta" in GSet.test { id => - protocol.crdt - .connect() - .send(init(CrdtTckModel.name, id)) - .send(delta(GSet.delta("x", "y"))) - .send(command(1, id, "Process", Request(id, GSet.add("z")))) - .expect(reply(1, GSet.state("x", "y", "z"), GSet.update("z"))) - .passivate() - } - - "verify GSet mix of local and replicated state changes" in GSet.test { id => - protocol.crdt - .connect() - .send(init(CrdtTckModel.name, id)) - .send(command(1, id, "Process", Request(id, GSet.add("a")))) - .expect(reply(1, GSet.state("a"), GSet.update("a"))) - .send(delta(GSet.delta("a"))) - .send(delta(GSet.delta("b"))) - .send(delta(GSet.delta("c"))) - .send(command(2, id, "Process", Request(id))) - .expect(reply(2, GSet.state("a", "b", "c"))) - .send(command(3, id, "Process", Request(id, GSet.add("c", "d", "e")))) - .expect(reply(3, GSet.state("a", "b", "c", "d", "e"), GSet.update("d", "e")), Delta.sorted) - .send(delta(GSet.delta("f"))) - .send(command(4, id, "Process", Request(id, GSet.add("g", "d", "b")))) - .expect(reply(4, GSet.state("a", "b", "c", "d", "e", "f", "g"), GSet.update("g"))) - .passivate() - } - - "verify GSet rehydration after passivation" in GSet.test { id => - protocol.crdt - .connect() - .send(init(CrdtTckModel.name, id)) - .send(command(1, id, "Process", Request(id, GSet.add("a")))) - .expect(reply(1, GSet.state("a"), GSet.update("a"))) - .send(delta(GSet.delta("b"))) - .send(command(2, id, "Process", Request(id))) - .expect(reply(2, GSet.state("a", "b"))) - .send(command(3, id, "Process", Request(id, GSet.add("c")))) - .expect(reply(3, GSet.state("a", "b", "c"), GSet.update("c"))) - .passivate() - protocol.crdt - .connect() - .send(init(CrdtTckModel.name, id, GSet.delta("a", "b", "c"))) - .send(command(1, id, "Process", Request(id))) - .expect(reply(1, GSet.state("a", "b", "c"))) - .send(delta(GSet.delta("d"))) - .send(command(2, id, "Process", Request(id, GSet.add("e")))) - .expect(reply(2, GSet.state("a", "b", "c", "d", "e"), GSet.update("e"))) - .passivate() - } - - "verify GSet delete action received from entity" in GSet.test { id => - protocol.crdt - .connect() - .send(init(CrdtTckModel.name, id)) - .send(command(1, id, "Process", Request(id, GSet.add("x")))) - .expect(reply(1, GSet.state("x"), GSet.update("x"))) - .send(command(2, id, "Process", Request(id, Seq(requestDelete)))) - .expect(reply(2, GSet.state("x"), deleteCrdt)) - .passivate() - } - - "verify GSet delete action sent to entity" in GSet.test { id => - protocol.crdt - .connect() - .send(init(CrdtTckModel.name, id)) - .send(delete) - .passivate() - } - - // ORSet tests - - object ORSet { - def state(elements: String*): Response = - Response(Some(State(State.Value.Orset(ORSetValue(elements))))) - - def add(elements: String*): Seq[RequestAction] = - elements.map(element => action(ORSetUpdate.Action.Add(element))) - - def remove(elements: String*): Seq[RequestAction] = - elements.map(element => action(ORSetUpdate.Action.Remove(element))) - - def clear(value: Boolean = true): Seq[RequestAction] = - Seq(action(ORSetUpdate.Action.Clear(value))) - - def action(updateAction: ORSetUpdate.Action): RequestAction = - requestUpdate(Update(Update.Update.Orset(ORSetUpdate(updateAction)))) - - def update(cleared: Boolean = false, - removed: Seq[String] = Seq.empty, - added: Seq[String] = Seq.empty): Effects = - Effects(stateAction = crdtUpdate(delta(cleared, removed, added))) - - def delta(cleared: Boolean = false, - removed: Seq[String] = Seq.empty, - added: Seq[String] = Seq.empty): CrdtDelta.Delta.Orset = - CrdtDelta.Delta.Orset(ORSetDelta(cleared, removed.map(primitiveString), added.map(primitiveString))) - - def test(run: String => Any): Unit = crdtTest("ORSet")(run) - } - - "verify ORSet initial empty state" in ORSet.test { id => - protocol.crdt - .connect() - .send(init(CrdtTckModel.name, id)) - .send(command(1, id, "Process", Request(id))) - .expect(reply(1, ORSet.state())) - .passivate() - } - - "verify ORSet state changes" in ORSet.test { id => - protocol.crdt - .connect() - .send(init(CrdtTckModel.name, id)) - .send(command(1, id, "Process", Request(id, ORSet.add("a", "b")))) - .expect(reply(1, ORSet.state("a", "b"), ORSet.update(added = Seq("a", "b"))), Delta.sorted) - .send(command(2, id, "Process", Request(id, ORSet.add("b", "c")))) - .expect(reply(2, ORSet.state("a", "b", "c"), ORSet.update(added = Seq("c")))) - .send(command(3, id, "Process", Request(id, ORSet.add("c", "a")))) - .expect(reply(3, ORSet.state("a", "b", "c"))) - .send(command(4, id, "Process", Request(id, ORSet.remove("b", "d")))) - .expect(reply(4, ORSet.state("a", "c"), ORSet.update(removed = Seq("b")))) - .send(command(5, id, "Process", Request(id, ORSet.remove("c", "d") ++ ORSet.add("b", "c")))) - .expect(reply(5, ORSet.state("a", "b", "c"), ORSet.update(added = Seq("b")))) - .send(command(6, id, "Process", Request(id, ORSet.clear()))) - .expect(reply(6, ORSet.state(), ORSet.update(cleared = true))) - .send(command(7, id, "Process", Request(id, ORSet.add("a") ++ ORSet.clear() ++ ORSet.add("x")))) - .expect(reply(7, ORSet.state("x"), ORSet.update(cleared = true, added = Seq("x")))) - .passivate() - } - - "verify ORSet initial delta in init" in ORSet.test { id => - protocol.crdt - .connect() - .send(init(CrdtTckModel.name, id, ORSet.delta(added = Seq("a", "b", "c")))) - .send(command(1, id, "Process", Request(id, ORSet.remove("c") ++ ORSet.add("d")))) - .expect(reply(1, ORSet.state("a", "b", "d"), ORSet.update(removed = Seq("c"), added = Seq("d")))) - .passivate() - } - - "verify ORSet initial empty state with replicated initial delta" in ORSet.test { id => - protocol.crdt - .connect() - .send(init(CrdtTckModel.name, id)) - .send(delta(ORSet.delta(added = Seq("x", "y")))) - .send(command(1, id, "Process", Request(id, ORSet.remove("x") ++ ORSet.add("z")))) - .expect(reply(1, ORSet.state("y", "z"), ORSet.update(removed = Seq("x"), added = Seq("z")))) - .passivate() - } - - "verify ORSet mix of local and replicated state changes" in ORSet.test { id => - protocol.crdt - .connect() - .send(init(CrdtTckModel.name, id)) - .send(command(1, id, "Process", Request(id, ORSet.add("a")))) - .expect(reply(1, ORSet.state("a"), ORSet.update(added = Seq("a")))) - .send(delta(ORSet.delta(added = Seq("a", "b")))) - .send(delta(ORSet.delta(added = Seq("c", "d")))) - .send(delta(ORSet.delta(removed = Seq("b", "d")))) - .send(command(2, id, "Process", Request(id))) - .expect(reply(2, ORSet.state("a", "c"))) - .send(command(3, id, "Process", Request(id, ORSet.add("c", "d", "e")))) - .expect(reply(3, ORSet.state("a", "c", "d", "e"), ORSet.update(added = Seq("d", "e"))), Delta.sorted) - .send(delta(ORSet.delta(removed = Seq("a", "c"), added = Seq("f")))) - .send(command(4, id, "Process", Request(id, ORSet.add("g") ++ ORSet.remove("a", "d")))) - .expect(reply(4, ORSet.state("e", "f", "g"), ORSet.update(removed = Seq("d"), added = Seq("g")))) - .send(delta(ORSet.delta(cleared = true, added = Seq("x", "y", "z")))) - .send(command(5, id, "Process", Request(id, ORSet.add("q", "x") ++ ORSet.remove("z")))) - .expect(reply(5, ORSet.state("q", "x", "y"), ORSet.update(removed = Seq("z"), added = Seq("q")))) - .send(delta(ORSet.delta(removed = Seq("x", "y"), added = Seq("r", "p")))) - .send(command(6, id, "Process", Request(id, ORSet.add("s")))) - .expect(reply(6, ORSet.state("p", "q", "r", "s"), ORSet.update(added = Seq("s")))) - .send(delta(ORSet.delta(added = Seq("a", "b", "c")))) - .send(command(7, id, "Process", Request(id, ORSet.clear() ++ ORSet.add("abc")))) - .expect(reply(7, ORSet.state("abc"), ORSet.update(cleared = true, added = Seq("abc")))) - .passivate() - } - - "verify ORSet rehydration after passivation" in ORSet.test { id => - protocol.crdt - .connect() - .send(init(CrdtTckModel.name, id)) - .send(command(1, id, "Process", Request(id, ORSet.add("a")))) - .expect(reply(1, ORSet.state("a"), ORSet.update(added = Seq("a")))) - .send(delta(ORSet.delta(removed = Seq("a"), added = Seq("b")))) - .send(command(2, id, "Process", Request(id))) - .expect(reply(2, ORSet.state("b"))) - .send(command(3, id, "Process", Request(id, ORSet.add("c")))) - .expect(reply(3, ORSet.state("b", "c"), ORSet.update(added = Seq("c")))) - .passivate() - protocol.crdt - .connect() - .send(init(CrdtTckModel.name, id, ORSet.delta(added = Seq("b", "c")))) - .send(command(1, id, "Process", Request(id))) - .expect(reply(1, ORSet.state("b", "c"))) - .send(delta(ORSet.delta(cleared = true, added = Seq("p", "q")))) - .send(command(2, id, "Process", Request(id, ORSet.add("x", "y") ++ ORSet.remove("x")))) - .expect(reply(2, ORSet.state("p", "q", "y"), ORSet.update(added = Seq("y")))) - .passivate() - } - - "verify ORSet delete action received from entity" in ORSet.test { id => - protocol.crdt - .connect() - .send(init(CrdtTckModel.name, id)) - .send(command(1, id, "Process", Request(id, ORSet.add("x")))) - .expect(reply(1, ORSet.state("x"), ORSet.update(added = Seq("x")))) - .send(command(2, id, "Process", Request(id, Seq(requestDelete)))) - .expect(reply(2, ORSet.state("x"), deleteCrdt)) - .passivate() - } - - "verify ORSet delete action sent to entity" in ORSet.test { id => - protocol.crdt - .connect() - .send(init(CrdtTckModel.name, id)) - .send(delete) - .passivate() - } - - // LWWRegister tests - - object LWWRegister { - val DefaultClock: LWWRegisterClockType = LWWRegisterClockType.DEFAULT - val ReverseClock: LWWRegisterClockType = LWWRegisterClockType.REVERSE - val CustomClock: LWWRegisterClockType = LWWRegisterClockType.CUSTOM - val CustomAutoIncClock: LWWRegisterClockType = LWWRegisterClockType.CUSTOM_AUTO_INCREMENT - - def state(value: String): Response = - Response(Some(State(State.Value.Lwwregister(LWWRegisterValue(value))))) - - def setTo(value: String, - clockType: LWWRegisterClockType = DefaultClock, - customClockValue: Long = 0L): Seq[RequestAction] = - updateWith(LWWRegisterUpdate(value, Some(LWWRegisterClock(clockType, customClockValue)))) - - def updateWith(update: LWWRegisterUpdate): Seq[RequestAction] = - Seq(requestUpdate(Update(Update.Update.Lwwregister(update)))) - - def update(value: String, clock: CrdtClock = CrdtClock.DEFAULT, customClockValue: Long = 0L): Effects = - Effects(stateAction = crdtUpdate(delta(value, clock, customClockValue))) - - def delta(value: String, - clock: CrdtClock = CrdtClock.DEFAULT, - customClockValue: Long = 0L): CrdtDelta.Delta.Lwwregister = - deltaLWWRegister(Some(primitiveString(value)), clock, customClockValue) - - def test(run: String => Any): Unit = crdtTest("LWWRegister")(run) - } - - "verify LWWRegister initial empty state" in LWWRegister.test { id => - protocol.crdt - .connect() - .send(init(CrdtTckModel.name, id)) - .send(command(1, id, "Process", Request(id))) - .expect(reply(1, LWWRegister.state(""), LWWRegister.update(""))) - .passivate() - } - - "verify LWWRegister state changes" in LWWRegister.test { id => - protocol.crdt - .connect() - .send(init(CrdtTckModel.name, id)) - .send(command(1, id, "Process", Request(id, LWWRegister.setTo("one")))) - .expect(reply(1, LWWRegister.state("one"), LWWRegister.update("one"))) - .send(command(2, id, "Process", Request(id, LWWRegister.setTo("two")))) - .expect(reply(2, LWWRegister.state("two"), LWWRegister.update("two"))) - .passivate() - } - - "verify LWWRegister initial delta in init" in LWWRegister.test { id => - protocol.crdt - .connect() - .send(init(CrdtTckModel.name, id, LWWRegister.delta("one"))) - .send(command(1, id, "Process", Request(id))) - .expect(reply(1, LWWRegister.state("one"))) - .passivate() - } - - "verify LWWRegister initial empty state with replicated initial delta" in LWWRegister.test { id => - protocol.crdt - .connect() - .send(init(CrdtTckModel.name, id)) - .send(delta(LWWRegister.delta("one"))) - .send(command(1, id, "Process", Request(id))) - .expect(reply(1, LWWRegister.state("one"))) - .passivate() - } - - "verify LWWRegister mix of local and replicated state changes" in LWWRegister.test { id => - protocol.crdt - .connect() - .send(init(CrdtTckModel.name, id)) - .send(command(1, id, "Process", Request(id, LWWRegister.setTo("A")))) - .expect(reply(1, LWWRegister.state("A"), LWWRegister.update("A"))) - .send(delta(LWWRegister.delta("B"))) - .send(delta(LWWRegister.delta("C"))) - .send(command(2, id, "Process", Request(id))) - .expect(reply(2, LWWRegister.state("C"))) - .send(command(3, id, "Process", Request(id, LWWRegister.setTo("D")))) - .expect(reply(3, LWWRegister.state("D"), LWWRegister.update("D"))) - .send(delta(LWWRegister.delta("D"))) - .send(command(4, id, "Process", Request(id, LWWRegister.setTo("E")))) - .expect(reply(4, LWWRegister.state("E"), LWWRegister.update("E"))) - .passivate() - } - - "verify LWWRegister state changes with clock settings" in LWWRegister.test { id => - protocol.crdt - .connect() - .send(init(CrdtTckModel.name, id)) - .send(command(1, id, "Process", Request(id, LWWRegister.setTo("A", LWWRegister.ReverseClock)))) - .expect(reply(1, LWWRegister.state("A"), LWWRegister.update("A", CrdtClock.REVERSE))) - .send(command(2, id, "Process", Request(id, LWWRegister.setTo("A", LWWRegister.CustomClock, 123L)))) - .expect(reply(2, LWWRegister.state("A"), LWWRegister.update("A", CrdtClock.CUSTOM, 123L))) - .send(command(2, id, "Process", Request(id, LWWRegister.setTo("A", LWWRegister.CustomAutoIncClock, 456L)))) - .expect(reply(2, LWWRegister.state("A"), LWWRegister.update("A", CrdtClock.CUSTOM_AUTO_INCREMENT, 456L))) - .passivate() - } - - "verify LWWRegister rehydration after passivation" in LWWRegister.test { id => - protocol.crdt - .connect() - .send(init(CrdtTckModel.name, id)) - .send(command(1, id, "Process", Request(id, LWWRegister.setTo("A")))) - .expect(reply(1, LWWRegister.state("A"), LWWRegister.update("A"))) - .send(delta(LWWRegister.delta("B"))) - .send(command(2, id, "Process", Request(id))) - .expect(reply(2, LWWRegister.state("B"))) - .send(command(3, id, "Process", Request(id, LWWRegister.setTo("C")))) - .expect(reply(3, LWWRegister.state("C"), LWWRegister.update("C"))) - .passivate() - protocol.crdt - .connect() - .send(init(CrdtTckModel.name, id, LWWRegister.delta("C"))) - .send(command(1, id, "Process", Request(id))) - .expect(reply(1, LWWRegister.state("C"))) - .send(delta(LWWRegister.delta("D"))) - .send(command(2, id, "Process", Request(id, LWWRegister.setTo("E")))) - .expect(reply(2, LWWRegister.state("E"), LWWRegister.update("E"))) - .passivate() - } - - "verify LWWRegister delete action received from entity" in LWWRegister.test { id => - protocol.crdt - .connect() - .send(init(CrdtTckModel.name, id)) - .send(command(1, id, "Process", Request(id, LWWRegister.setTo("one")))) - .expect(reply(1, LWWRegister.state("one"), LWWRegister.update("one"))) - .send(command(2, id, "Process", Request(id, Seq(requestDelete)))) - .expect(reply(2, LWWRegister.state("one"), deleteCrdt)) - .passivate() - } - - "verify LWWRegister delete action sent to entity" in LWWRegister.test { id => - protocol.crdt - .connect() - .send(init(CrdtTckModel.name, id)) - .send(delete) - .passivate() - } - - // Flag tests - - object Flag { - def state(value: Boolean): Response = - Response(Some(State(State.Value.Flag(FlagValue(value))))) - - def enable(): Seq[RequestAction] = - Seq(requestUpdate(Update(Update.Update.Flag(FlagUpdate())))) - - def update(value: Boolean): Effects = - Effects(stateAction = crdtUpdate(delta(value))) - - def delta(value: Boolean): CrdtDelta.Delta.Flag = deltaFlag(value) - - def test(run: String => Any): Unit = crdtTest("Flag")(run) - } - - "verify Flag initial empty state" in Flag.test { id => - protocol.crdt - .connect() - .send(init(CrdtTckModel.name, id)) - .send(command(1, id, "Process", Request(id))) - .expect(reply(1, Flag.state(false))) - .passivate() - } - - "verify Flag state changes" in Flag.test { id => - protocol.crdt - .connect() - .send(init(CrdtTckModel.name, id)) - .send(command(1, id, "Process", Request(id, Flag.enable()))) - .expect(reply(1, Flag.state(true), Flag.update(true))) - .passivate() - } - - "verify Flag initial delta in init" in Flag.test { id => - protocol.crdt - .connect() - .send(init(CrdtTckModel.name, id, Flag.delta(false))) - .send(command(1, id, "Process", Request(id))) - .expect(reply(1, Flag.state(false))) - .passivate() - protocol.crdt - .connect() - .send(init(CrdtTckModel.name, id, Flag.delta(true))) - .send(command(1, id, "Process", Request(id))) - .expect(reply(1, Flag.state(true))) - .passivate() - } - - "verify Flag initial empty state with replicated initial delta" in Flag.test { id => - protocol.crdt - .connect() - .send(init(CrdtTckModel.name, id)) - .send(delta(Flag.delta(false))) - .send(command(1, id, "Process", Request(id))) - .expect(reply(1, Flag.state(false))) - .passivate() - protocol.crdt - .connect() - .send(init(CrdtTckModel.name, id)) - .send(delta(Flag.delta(true))) - .send(command(1, id, "Process", Request(id))) - .expect(reply(1, Flag.state(true))) - .passivate() - } - - "verify Flag mix of local and replicated state changes" in Flag.test { id => - protocol.crdt - .connect() - .send(init(CrdtTckModel.name, id)) - .send(delta(Flag.delta(false))) - .send(command(1, id, "Process", Request(id))) - .expect(reply(1, Flag.state(false))) - .send(command(2, id, "Process", Request(id, Flag.enable()))) - .expect(reply(2, Flag.state(true), Flag.update(true))) - .send(delta(Flag.delta(true))) - .send(delta(Flag.delta(false))) - .send(command(3, id, "Process", Request(id))) - .expect(reply(3, Flag.state(true))) - .passivate() - protocol.crdt - .connect() - .send(init(CrdtTckModel.name, id)) - .send(delta(Flag.delta(true))) - .send(command(1, id, "Process", Request(id))) - .expect(reply(1, Flag.state(true))) - .send(command(2, id, "Process", Request(id, Flag.enable()))) - .expect(reply(2, Flag.state(true))) - .send(delta(Flag.delta(true))) - .send(delta(Flag.delta(false))) - .send(command(3, id, "Process", Request(id))) - .expect(reply(3, Flag.state(true))) - .passivate() - } - - "verify Flag rehydration after passivation" in Flag.test { id => - protocol.crdt - .connect() - .send(init(CrdtTckModel.name, id)) - .send(command(1, id, "Process", Request(id))) - .expect(reply(1, Flag.state(false))) - .send(command(2, id, "Process", Request(id, Flag.enable()))) - .expect(reply(2, Flag.state(true), Flag.update(true))) - .passivate() - protocol.crdt - .connect() - .send(init(CrdtTckModel.name, id, Flag.delta(true))) - .send(command(1, id, "Process", Request(id))) - .expect(reply(1, Flag.state(true))) - .passivate() - } - - "verify Flag delete action received from entity" in Flag.test { id => - protocol.crdt - .connect() - .send(init(CrdtTckModel.name, id)) - .send(command(1, id, "Process", Request(id, Flag.enable()))) - .expect(reply(1, Flag.state(true), Flag.update(true))) - .send(command(2, id, "Process", Request(id, Seq(requestDelete)))) - .expect(reply(2, Flag.state(true), deleteCrdt)) - .passivate() - } - - "verify Flag delete action sent to entity" in Flag.test { id => - protocol.crdt - .connect() - .send(init(CrdtTckModel.name, id)) - .send(delete) - .passivate() - } - - // ORMap tests - - object ORMap { - def state(entries: (String, Response)*): Response = - Response( - Some(State(State.Value.Ormap(ORMapValue(entries.map { - case (key, value) => ORMapEntryValue(key, value.state) - })))) - ) - - def add(keys: String*): Seq[RequestAction] = - keys.map(key => action(ORMapUpdate.Action.Add(key))) - - def updateWith(entries: (String, Seq[RequestAction])*): Seq[RequestAction] = - entries flatMap { - case (key, actions) => - actions map { a => - action(ORMapUpdate.Action.Update(ORMapEntryUpdate(key, Option(a.getUpdate)))) - } - } - - def remove(keys: String*): Seq[RequestAction] = - keys.map(key => action(ORMapUpdate.Action.Remove(key))) - - def clear(value: Boolean = true): Seq[RequestAction] = - Seq(action(ORMapUpdate.Action.Clear(value))) - - def action(updateAction: ORMapUpdate.Action): RequestAction = - requestUpdate(Update(Update.Update.Ormap(ORMapUpdate(updateAction)))) - - def update(cleared: Boolean = false, - removed: Seq[String] = Seq.empty, - updated: Seq[(String, Effects)] = Seq.empty, - added: Seq[(String, Effects)] = Seq.empty): Effects = - Effects( - stateAction = crdtUpdate( - delta( - cleared, - removed, - updated map { case (key, effects) => key -> effects.stateAction.get.getUpdate.delta }, - added map { case (key, effects) => key -> effects.stateAction.get.getUpdate.delta } - ) - ) - ) - - def delta(cleared: Boolean = false, - removed: Seq[String] = Seq.empty, - updated: Seq[(String, CrdtDelta.Delta)] = Seq.empty, - added: Seq[(String, CrdtDelta.Delta)] = Seq.empty): CrdtDelta.Delta.Ormap = - CrdtDelta.Delta.Ormap( - ORMapDelta( - cleared, - removed.map(primitiveString), - updated map { case (key, delta) => ORMapEntryDelta(Some(primitiveString(key)), Some(CrdtDelta(delta))) }, - added map { case (key, delta) => ORMapEntryDelta(Some(primitiveString(key)), Some(CrdtDelta(delta))) } - ) - ) - - def test(run: String => Any): Unit = crdtTest("ORMap")(run) - } - - "verify ORMap initial empty state" in ORMap.test { id => - protocol.crdt - .connect() - .send(init(CrdtTckModel.name, id)) - .send(command(1, id, "Process", Request(id))) - .expect(reply(1, ORMap.state())) - .passivate() - } - - "verify ORMap state changes" in ORMap.test { id => - protocol.crdt - .connect() - .send(init(CrdtTckModel.name, id)) - .send(command(1, id, "Process", Request(id, ORMap.add("PNCounter-1", "ORSet-1")))) - .expect( - reply( - 1, - ORMap.state( - "ORSet-1" -> ORSet.state(), - "PNCounter-1" -> PNCounter.state(0) - ), - ORMap.update( - added = Seq( - "ORSet-1" -> ORSet.update(), - "PNCounter-1" -> PNCounter.update(0) - ) - ) - ), - Delta.sorted - ) - .send( - command(2, - id, - "Process", - Request(id, - ORMap.add("ORSet-1") ++ ORMap.updateWith( - "LWWRegister-1" -> LWWRegister.setTo("zero") - ))) - ) - .expect( - reply( - 2, - ORMap.state( - "LWWRegister-1" -> LWWRegister.state("zero"), - "ORSet-1" -> ORSet.state(), - "PNCounter-1" -> PNCounter.state(0) - ), - ORMap.update( - added = Seq( - "LWWRegister-1" -> LWWRegister.update("zero") - ) - ) - ), - Delta.sorted - ) - .send( - command(3, - id, - "Process", - Request(id, - ORMap.updateWith( - "PNCounter-1" -> PNCounter.changeBy(+42), - "LWWRegister-1" -> LWWRegister.setTo("one") - ))) - ) - .expect( - reply( - 3, - ORMap.state( - "LWWRegister-1" -> LWWRegister.state("one"), - "ORSet-1" -> ORSet.state(), - "PNCounter-1" -> PNCounter.state(+42) - ), - ORMap.update( - updated = Seq( - "LWWRegister-1" -> LWWRegister.update("one"), - "PNCounter-1" -> PNCounter.update(+42) - ) - ) - ), - Delta.sorted - ) - .send(command(4, id, "Process", Request(id, ORMap.updateWith("ORSet-1" -> ORSet.add("a", "b", "c"))))) - .expect( - reply( - 4, - ORMap.state( - "LWWRegister-1" -> LWWRegister.state("one"), - "ORSet-1" -> ORSet.state("a", "b", "c"), - "PNCounter-1" -> PNCounter.state(+42) - ), - ORMap.update( - updated = Seq( - "ORSet-1" -> ORSet.update(added = Seq("a", "b", "c")) - ) - ) - ), - Delta.sorted - ) - .send( - command( - 5, - id, - "Process", - Request(id, - ORMap.updateWith( - "ORSet-1" -> (ORSet.remove("b") ++ ORSet.add("d")), - "PNCounter-1" -> PNCounter.changeBy(-123), - "LWWRegister-1" -> LWWRegister.setTo("two") - )) - ) - ) - .expect( - reply( - 5, - ORMap.state( - "LWWRegister-1" -> LWWRegister.state("two"), - "ORSet-1" -> ORSet.state("a", "c", "d"), - "PNCounter-1" -> PNCounter.state(-81) - ), - ORMap.update( - updated = Seq( - "LWWRegister-1" -> LWWRegister.update("two"), - "ORSet-1" -> ORSet.update(removed = Seq("b"), added = Seq("d")), - "PNCounter-1" -> PNCounter.update(-123) - ) - ) - ), - Delta.sorted - ) - .send(command(6, id, "Process", Request(id, ORMap.remove("ORSet-1")))) - .expect( - reply( - 6, - ORMap.state( - "LWWRegister-1" -> LWWRegister.state("two"), - "PNCounter-1" -> PNCounter.state(-81) - ), - ORMap.update(removed = Seq("ORSet-1")) - ), - Delta.sorted - ) - .send( - command(7, - id, - "Process", - Request(id, ORMap.remove("LWWRegister-1") ++ ORMap.updateWith("Flag-1" -> Flag.enable()))) - ) - .expect( - reply( - 7, - ORMap.state( - "Flag-1" -> Flag.state(true), - "PNCounter-1" -> PNCounter.state(-81) - ), - ORMap.update(removed = Seq("LWWRegister-1"), added = Seq("Flag-1" -> Flag.update(true))) - ), - Delta.sorted - ) - .send(command(8, id, "Process", Request(id, ORMap.clear()))) - .expect(reply(8, ORMap.state(), ORMap.update(cleared = true))) - .send( - command( - 9, - id, - "Process", - Request(id, - ORMap.add("PNCounter-2") ++ ORMap.clear() ++ ORMap.updateWith( - "GCounter-1" -> GCounter.incrementBy(123) - )) - ) - ) - .expect( - reply(9, - ORMap.state("GCounter-1" -> GCounter.state(123)), - ORMap.update(cleared = true, added = Seq("GCounter-1" -> GCounter.update(123)))), - Delta.sorted - ) - .passivate() - } - - "verify ORMap initial delta in init" in ORMap.test { id => - protocol.crdt - .connect() - .send( - init( - CrdtTckModel.name, - id, - ORMap.delta( - added = Seq("PNCounter-1" -> PNCounter.delta(+123), - "LWWRegister-1" -> LWWRegister.delta("one"), - "GSet-1" -> GSet.delta("a", "b", "c")) - ) - ) - ) - .send(command(1, id, "Process", Request(id))) - .expect( - reply(1, - ORMap.state("GSet-1" -> GSet.state("a", "b", "c"), - "LWWRegister-1" -> LWWRegister.state("one"), - "PNCounter-1" -> PNCounter.state(+123))) - ) - .send( - command( - 2, - id, - "Process", - Request(id, ORMap.remove("LWWRegister-1") ++ ORMap.updateWith("PNCounter-1" -> PNCounter.changeBy(-42))) - ) - ) - .expect( - reply( - 2, - ORMap.state("GSet-1" -> GSet.state("a", "b", "c"), "PNCounter-1" -> PNCounter.state(+81)), - ORMap.update(removed = Seq("LWWRegister-1"), updated = Seq("PNCounter-1" -> PNCounter.update(-42))) - ), - Delta.sorted - ) - .passivate() - } - - "verify ORMap initial empty state with replicated initial delta" in ORMap.test { id => - protocol.crdt - .connect() - .send(init(CrdtTckModel.name, id)) - .send( - delta( - ORMap.delta( - added = Seq("GCounter-1" -> GCounter.delta(42), - "ORSet-1" -> ORSet.delta(added = Seq("a", "b", "c")), - "Flag-1" -> Flag.delta(false)) - ) - ) - ) - .send(command(1, id, "Process", Request(id))) - .expect( - reply(1, - ORMap.state("Flag-1" -> Flag.state(false), - "GCounter-1" -> GCounter.state(42), - "ORSet-1" -> ORSet.state("a", "b", "c"))) - ) - .passivate() - } - - "verify ORMap mix of local and replicated state changes" in ORMap.test { id => - protocol.crdt - .connect() - .send(init(CrdtTckModel.name, id)) - .send(command(1, id, "Process", Request(id, ORMap.add("PNCounter-1")))) - .expect( - reply( - 1, - ORMap.state( - "PNCounter-1" -> PNCounter.state(0) - ), - ORMap.update( - added = Seq( - "PNCounter-1" -> PNCounter.update(0) - ) - ) - ), - Delta.sorted - ) - .send(delta(ORMap.delta(added = Seq("ORSet-1" -> ORSet.delta(added = Seq("x")))))) - .send( - delta( - ORMap.delta(added = Seq("LWWRegister-1" -> LWWRegister.delta("one")), - updated = Seq("ORSet-1" -> ORSet.delta(added = Seq("y", "z")))) - ) - ) - .send( - command(2, - id, - "Process", - Request(id, - ORMap.add("ORSet-2") ++ ORMap.updateWith( - "LWWRegister-2" -> LWWRegister.setTo("two") - ))) - ) - .expect( - reply( - 2, - ORMap.state( - "LWWRegister-1" -> LWWRegister.state("one"), - "LWWRegister-2" -> LWWRegister.state("two"), - "ORSet-1" -> ORSet.state("x", "y", "z"), - "ORSet-2" -> ORSet.state(), - "PNCounter-1" -> PNCounter.state(0) - ), - ORMap.update( - added = Seq( - "LWWRegister-2" -> LWWRegister.update("two"), - "ORSet-2" -> ORSet.update() - ) - ) - ), - Delta.sorted - ) - .send( - command(3, - id, - "Process", - Request(id, - ORMap.updateWith( - "PNCounter-1" -> PNCounter.changeBy(+42), - "LWWRegister-1" -> LWWRegister.setTo("ONE") - ))) - ) - .expect( - reply( - 3, - ORMap.state( - "LWWRegister-1" -> LWWRegister.state("ONE"), - "LWWRegister-2" -> LWWRegister.state("two"), - "ORSet-1" -> ORSet.state("x", "y", "z"), - "ORSet-2" -> ORSet.state(), - "PNCounter-1" -> PNCounter.state(+42) - ), - ORMap.update( - updated = Seq( - "LWWRegister-1" -> LWWRegister.update("ONE"), - "PNCounter-1" -> PNCounter.update(+42) - ) - ) - ), - Delta.sorted - ) - .send( - delta( - ORMap.delta(removed = Seq("LWWRegister-2"), - added = Seq("ORMap-1" -> ORMap.delta(added = Seq("PNCounter-1" -> PNCounter.delta(+123))))) - ) - ) - .send( - delta( - ORMap.delta( - removed = Seq("ORSet-1"), - updated = Seq("ORSet-2" -> ORSet.delta(added = Seq("a", "b", "c")), - "ORMap-1" -> ORMap.delta(added = Seq("LWWRegister-1" -> LWWRegister.delta("ABC")))) - ) - ) - ) - .send( - command(4, - id, - "Process", - Request(id, - ORMap.updateWith("ORMap-1" -> ORMap.updateWith("PNCounter-1" -> PNCounter.changeBy(-456))))) - ) - .expect( - reply( - 4, - ORMap.state( - "LWWRegister-1" -> LWWRegister.state("ONE"), - "ORMap-1" -> ORMap.state("LWWRegister-1" -> LWWRegister.state("ABC"), - "PNCounter-1" -> PNCounter.state(-333)), - "ORSet-2" -> ORSet.state("a", "b", "c"), - "PNCounter-1" -> PNCounter.state(+42) - ), - ORMap.update( - updated = Seq( - "ORMap-1" -> ORMap.update(updated = Seq("PNCounter-1" -> PNCounter.update(-456))) - ) - ) - ), - Delta.sorted - ) - .send(delta(ORMap.delta(cleared = true, added = Seq("GSet-1" -> GSet.delta("1", "2", "3"))))) - .send( - command( - 5, - id, - "Process", - Request(id, - ORMap.remove("PNCounter-1", "LWWRegister-1") ++ - ORMap.updateWith("GSet-1" -> GSet.add("2", "4", "6"))) - ) - ) - .expect( - reply( - 5, - ORMap.state( - "GSet-1" -> GSet.state("1", "2", "3", "4", "6") - ), - ORMap.update( - updated = Seq( - "GSet-1" -> GSet.update("4", "6") - ) - ) - ), - Delta.sorted - ) - .send(command(6, id, "Process", Request(id, ORMap.clear() ++ ORMap.add("GCounter-1")))) - .expect( - reply(6, - ORMap.state("GCounter-1" -> GCounter.state(0)), - ORMap.update(cleared = true, added = Seq("GCounter-1" -> GCounter.update(0)))) - ) - .send(delta(ORMap.delta(removed = Seq("GSet-1"), added = Seq("GCounter-1" -> GCounter.delta(42))))) - .send(command(7, id, "Process", Request(id, ORMap.updateWith("GCounter-1" -> GCounter.incrementBy(7))))) - .expect( - reply(7, - ORMap.state("GCounter-1" -> GCounter.state(49)), - ORMap.update(updated = Seq("GCounter-1" -> GCounter.update(7)))) - ) - .passivate() - } - "verify ORMap rehydration after passivation" in ORMap.test { id => - protocol.crdt - .connect() - .send(init(CrdtTckModel.name, id)) - .send(delta(ORMap.delta(added = Seq("PNCounter-1" -> PNCounter.delta(+123))))) - .send(command(1, id, "Process", Request(id, ORMap.updateWith("PNCounter-1" -> PNCounter.changeBy(+321))))) - .expect( - reply(1, - ORMap.state("PNCounter-1" -> PNCounter.state(+444)), - ORMap.update(updated = Seq("PNCounter-1" -> PNCounter.update(+321)))) - ) - .passivate() - protocol.crdt - .connect() - .send(init(CrdtTckModel.name, id, ORMap.delta(added = Seq("PNCounter-1" -> PNCounter.delta(+444))))) - .send(command(1, id, "Process", Request(id))) - .expect(reply(1, ORMap.state("PNCounter-1" -> PNCounter.state(+444)))) - .send(delta(ORMap.delta(updated = Seq("PNCounter-1" -> PNCounter.delta(-42))))) - .send(command(2, id, "Process", Request(id, ORMap.updateWith("PNCounter-1" -> PNCounter.changeBy(-360))))) - .expect( - reply(2, - ORMap.state("PNCounter-1" -> PNCounter.state(+42)), - ORMap.update(updated = Seq("PNCounter-1" -> PNCounter.update(-360)))) - ) - .passivate() - } - - "verify ORMap delete action received from entity" in ORMap.test { id => - protocol.crdt - .connect() - .send(init(CrdtTckModel.name, id)) - .send(command(1, id, "Process", Request(id, ORMap.add("Flag-1")))) - .expect( - reply(1, - ORMap.state("Flag-1" -> Flag.state(false)), - ORMap.update(added = Seq("Flag-1" -> Flag.update(false)))) - ) - .send(command(2, id, "Process", Request(id, ORMap.updateWith("Flag-1" -> Flag.enable()) :+ requestDelete))) - .expect(reply(2, ORMap.state("Flag-1" -> Flag.state(true)), deleteCrdt)) - .passivate() - } - - "verify ORMap delete action sent to entity" in ORMap.test { id => - protocol.crdt - .connect() - .send(init(CrdtTckModel.name, id)) - .send(delete) - .passivate() - } - - // Vote tests - - object Vote { - def state(selfVote: Boolean, votesFor: Int, totalVoters: Int): Response = - Response(Some(State(State.Value.Vote(VoteValue(selfVote, votesFor, totalVoters))))) - - def self(vote: Boolean): Seq[RequestAction] = - Seq(requestUpdate(updateWith(vote))) - - def updateWith(vote: Boolean): Update = - Update(Update.Update.Vote(VoteUpdate(vote))) - - def update(selfVote: Boolean): Effects = - Effects(stateAction = crdtUpdate(delta(selfVote))) - - def delta(selfVote: Boolean, votesFor: Int = 0, totalVoters: Int = 0): CrdtDelta.Delta.Vote = - deltaVote(selfVote, votesFor, totalVoters) - - def test(run: String => Any): Unit = crdtTest("Vote")(run) - } - - "verify Vote initial empty state" in Vote.test { id => - protocol.crdt - .connect() - .send(init(CrdtTckModel.name, id)) - .send(command(1, id, "Process", Request(id))) - .expect(reply(1, Vote.state(selfVote = false, votesFor = 0, totalVoters = 1))) - .passivate() - } - - "verify Vote state changes" in Vote.test { id => - protocol.crdt - .connect() - .send(init(CrdtTckModel.name, id)) - .send(command(1, id, "Process", Request(id, Vote.self(true)))) - .expect(reply(1, Vote.state(selfVote = true, votesFor = 1, totalVoters = 1), Vote.update(true))) - .send(command(2, id, "Process", Request(id, Vote.self(true)))) - .expect(reply(2, Vote.state(selfVote = true, votesFor = 1, totalVoters = 1))) - .send(command(3, id, "Process", Request(id, Vote.self(false)))) - .expect(reply(3, Vote.state(selfVote = false, votesFor = 0, totalVoters = 1), Vote.update(false))) - .send(command(4, id, "Process", Request(id, Vote.self(false)))) - .expect(reply(4, Vote.state(selfVote = false, votesFor = 0, totalVoters = 1))) - .passivate() - } - - "verify Vote initial delta in init" in Vote.test { id => - protocol.crdt - .connect() - .send(init(CrdtTckModel.name, id, Vote.delta(selfVote = true, votesFor = 2, totalVoters = 3))) - .send(command(1, id, "Process", Request(id))) - .expect(reply(1, Vote.state(selfVote = true, votesFor = 2, totalVoters = 3))) - .send(command(2, id, "Process", Request(id, Vote.self(false)))) - .expect(reply(2, Vote.state(selfVote = false, votesFor = 1, totalVoters = 3), Vote.update(false))) - .passivate() - } - - "verify Vote initial empty state with replicated initial delta" in Vote.test { id => - protocol.crdt - .connect() - .send(init(CrdtTckModel.name, id)) - .send(delta(Vote.delta(selfVote = false, votesFor = 3, totalVoters = 5))) - .send(command(1, id, "Process", Request(id))) - .expect(reply(1, Vote.state(selfVote = false, votesFor = 3, totalVoters = 5))) - .send(command(2, id, "Process", Request(id, Vote.self(true)))) - .expect(reply(2, Vote.state(selfVote = true, votesFor = 4, totalVoters = 5), Vote.update(true))) - .passivate() - } - - "verify Vote mix of local and replicated state changes" in Vote.test { id => - protocol.crdt - .connect() - .send(init(CrdtTckModel.name, id)) - .send(delta(Vote.delta(selfVote = false, votesFor = 2, totalVoters = 4))) - .send(command(1, id, "Process", Request(id))) - .expect(reply(1, Vote.state(selfVote = false, votesFor = 2, totalVoters = 4))) - .send(command(2, id, "Process", Request(id, Vote.self(true)))) - .expect(reply(2, Vote.state(selfVote = true, votesFor = 3, totalVoters = 4), Vote.update(true))) - .send(delta(Vote.delta(selfVote = true, votesFor = 5, totalVoters = 7))) - .send(delta(Vote.delta(selfVote = true, votesFor = 4, totalVoters = 6))) - .send(command(3, id, "Process", Request(id, Vote.self(false)))) - .expect(reply(3, Vote.state(selfVote = false, votesFor = 3, totalVoters = 6), Vote.update(false))) - .passivate() - } - - "verify Vote rehydration after passivation" in Vote.test { id => - protocol.crdt - .connect() - .send(init(CrdtTckModel.name, id)) - .send(command(1, id, "Process", Request(id))) - .expect(reply(1, Vote.state(selfVote = false, votesFor = 0, totalVoters = 1))) - .send(delta(Vote.delta(selfVote = false, votesFor = 2, totalVoters = 5))) - .send(command(2, id, "Process", Request(id, Vote.self(true)))) - .expect(reply(2, Vote.state(selfVote = true, votesFor = 3, totalVoters = 5), Vote.update(true))) - .passivate() - protocol.crdt - .connect() - .send(init(CrdtTckModel.name, id, Vote.delta(selfVote = true, votesFor = 3, totalVoters = 5))) - .send(command(1, id, "Process", Request(id))) - .expect(reply(1, Vote.state(selfVote = true, votesFor = 3, totalVoters = 5))) - .send(command(2, id, "Process", Request(id, Vote.self(false)))) - .expect(reply(2, Vote.state(selfVote = false, votesFor = 2, totalVoters = 5), Vote.update(false))) - .passivate() - } - - "verify Vote delete action received from entity" in Vote.test { id => - protocol.crdt - .connect() - .send(init(CrdtTckModel.name, id)) - .send(command(1, id, "Process", Request(id, Vote.self(true)))) - .expect(reply(1, Vote.state(selfVote = true, votesFor = 1, totalVoters = 1), Vote.update(true))) - .send(command(2, id, "Process", Request(id, Seq(requestDelete)))) - .expect(reply(2, Vote.state(selfVote = true, votesFor = 1, totalVoters = 1), deleteCrdt)) - .passivate() - } - - "verify Vote delete action sent to entity" in Vote.test { id => - protocol.crdt - .connect() - .send(init(CrdtTckModel.name, id)) - .send(delete) - .passivate() - } - - // forward and side effect tests - - "verify forward to second service" in GCounter.test { id => - protocol.crdt - .connect() - .send(init(CrdtTckModel.name, id)) - .send(command(1, id, "Process", Request(id, Seq(forwardTo(s"X-$id"))))) - .expect(forwarded(1, s"X-$id")) - .passivate() - } - - "verify forward with state changes" in PNCounter.test { id => - protocol.crdt - .connect() - .send(init(CrdtTckModel.name, id)) - .send(command(1, id, "Process", Request(id, PNCounter.changeBy(-42) :+ forwardTo(s"X-$id")))) - .expect(forwarded(1, s"X-$id", PNCounter.update(-42))) - .passivate() - } - - "verify reply with side effect to second service" in GSet.test { id => - protocol.crdt - .connect() - .send(init(CrdtTckModel.name, id)) - .send(command(1, id, "Process", Request(id, Seq(sideEffectTo(s"X-$id"))))) - .expect(reply(1, GSet.state(), sideEffects(s"X-$id"))) - .passivate() - } - - "verify synchronous side effect to second service" in ORSet.test { id => - protocol.crdt - .connect() - .send(init(CrdtTckModel.name, id)) - .send(command(1, id, "Process", Request(id, Seq(sideEffectTo(s"X-$id", synchronous = true))))) - .expect(reply(1, ORSet.state(), synchronousSideEffects(s"X-$id"))) - .passivate() - } - - "verify forward and side effect to second service" in LWWRegister.test { id => - protocol.crdt - .connect() - .send(init(CrdtTckModel.name, id)) - .send(command(1, id, "Process", Request(id, Seq(sideEffectTo(s"B-$id"), forwardTo(s"A-$id"))))) - .expect(forwarded(1, s"A-$id", LWWRegister.update("") ++ sideEffects(s"B-$id"))) - .passivate() - } - - "verify reply with multiple side effects" in Flag.test { id => - protocol.crdt - .connect() - .send(init(CrdtTckModel.name, id)) - .send(command(1, id, "Process", Request(id, sideEffectsTo("1", "2", "3")))) - .expect(reply(1, Flag.state(false), sideEffects("1", "2", "3"))) - .passivate() - } - - "verify reply with multiple side effects and state changes" in ORMap.test { id => - val crdtActions = ORMap.add("PNCounter-1") ++ ORMap.updateWith("GSet-1" -> GSet.add("a", "b", "c")) - val actions = crdtActions ++ sideEffectsTo("1", "2", "3") - val expectedState = ORMap.state("GSet-1" -> GSet.state("a", "b", "c"), "PNCounter-1" -> PNCounter.state(0)) - val crdtUpdates = ORMap.update( - added = Seq( - "GSet-1" -> GSet.update("a", "b", "c"), - "PNCounter-1" -> PNCounter.update(0) - ) - ) - val effects = crdtUpdates ++ sideEffects("1", "2", "3") - protocol.crdt - .connect() - .send(init(CrdtTckModel.name, id)) - .send(command(1, id, "Process", Request(id, actions))) - .expect(reply(1, expectedState, effects), Delta.sorted) - .passivate() - } - - // failure tests - - "verify failure action" in GCounter.test { id => - protocol.crdt - .connect() - .send(init(CrdtTckModel.name, id)) - .send(command(1, id, "Process", Request(id, Seq(failWith("expected failure"))))) - .expect(failure(1, "expected failure")) - .passivate() - } - - "verify connection after failure action" in PNCounter.test { id => - protocol.crdt - .connect() - .send(init(CrdtTckModel.name, id)) - .send(command(1, id, "Process", Request(id, PNCounter.changeBy(+42)))) - .expect(reply(1, PNCounter.state(+42), PNCounter.update(+42))) - .send(command(2, id, "Process", Request(id, Seq(failWith("expected failure"))))) - .expect(failure(2, "expected failure")) - .send(command(3, id, "Process", Request(id))) - .expect(reply(3, PNCounter.state(+42))) - .passivate() - } - - // streamed response tests - - "verify streamed responses" in GCounter.test { id => - protocol.crdt - .connect() - .send(init(CrdtTckModel.name, id)) - .send(command(1, id, "ProcessStreamed", StreamedRequest(id), streamed = true)) - .expect(reply(1, GCounter.state(0), Effects(streamed = true))) - .send(command(2, id, "Process", Request(id, GCounter.incrementBy(1)))) - .expect(reply(2, GCounter.state(1), GCounter.update(1))) - .expect(streamed(1, GCounter.state(1))) - .send(command(3, id, "Process", Request(id, GCounter.incrementBy(2)))) - .expect(reply(3, GCounter.state(3), GCounter.update(2))) - .expect(streamed(1, GCounter.state(3))) - .send(command(4, id, "Process", Request(id, GCounter.incrementBy(3)))) - .expect(reply(4, GCounter.state(6), GCounter.update(3))) - .expect(streamed(1, GCounter.state(6))) - .passivate() - } - - "verify streamed responses with stream ending action" in GCounter.test { id => - protocol.crdt - .connect() - .send(init(CrdtTckModel.name, id)) - .send( - command(1, id, "ProcessStreamed", StreamedRequest(id, endState = GCounter.state(3).state), streamed = true) - ) - .expect(reply(1, GCounter.state(0), Effects(streamed = true))) - .send(command(2, id, "Process", Request(id, GCounter.incrementBy(1)))) - .expect(reply(2, GCounter.state(1), GCounter.update(1))) - .expect(streamed(1, GCounter.state(1))) - .send(command(3, id, "Process", Request(id, GCounter.incrementBy(2)))) - .expect(reply(3, GCounter.state(3), GCounter.update(2))) - .expect(streamed(1, GCounter.state(3), Effects(endStream = true))) - .send(command(4, id, "Process", Request(id, GCounter.incrementBy(3)))) - .expect(reply(4, GCounter.state(6), GCounter.update(3))) - .passivate() - } - - "verify streamed responses with cancellation" in PNCounter.test { id => - protocol.crdt - .connect() - .send(init(CrdtTckModel.name, id)) - .send( - command( - 1, - id, - "ProcessStreamed", - StreamedRequest(id, cancelUpdate = Some(PNCounter.updateWith(-42))), - streamed = true - ) - ) - .expect(reply(1, PNCounter.state(0), Effects(streamed = true))) - .send(command(2, id, "Process", Request(id, PNCounter.changeBy(+10)))) - .expect(reply(2, PNCounter.state(+10), PNCounter.update(+10))) - .expect(streamed(1, PNCounter.state(+10))) - .send(command(3, id, "Process", Request(id, PNCounter.changeBy(+20)))) - .expect(reply(3, PNCounter.state(+30), PNCounter.update(+20))) - .expect(streamed(1, PNCounter.state(+30))) - .send(crdtStreamCancelled(1, id)) - .expect(streamCancelledResponse(1, PNCounter.update(-42))) - .send(command(4, id, "Process", Request(id))) - .expect(reply(4, PNCounter.state(-12))) - .send(command(5, id, "Process", Request(id, PNCounter.changeBy(+30)))) - .expect(reply(5, PNCounter.state(+18), PNCounter.update(+30))) - .passivate() - } - - "verify empty streamed responses with cancellation" in PNCounter.test { id => - protocol.crdt - .connect() - .send(init(CrdtTckModel.name, id)) - .send(command(1, id, "Process", Request(id, PNCounter.changeBy(+10)))) - .expect(reply(1, PNCounter.state(+10), PNCounter.update(+10))) - .send( - command( - 2, - id, - "ProcessStreamed", - StreamedRequest(id, cancelUpdate = Some(PNCounter.updateWith(-42)), empty = true), - streamed = true - ) - ) - .expect(crdtReply(2, None, Effects(streamed = true))) - .send(crdtStreamCancelled(2, id)) - .expect(streamCancelledResponse(2, PNCounter.update(-42))) - .send(command(3, id, "Process", Request(id))) - .expect(reply(3, PNCounter.state(-32))) - .passivate() - } - - "verify streamed responses with side effects" in GSet.test { id => - protocol.crdt - .connect() - .send(init(CrdtTckModel.name, id)) - .send( - command( - 1, - id, - "ProcessStreamed", - StreamedRequest(id, effects = Seq(Effect("one"), Effect("two", synchronous = true))), - streamed = true - ) - ) - .expect(reply(1, GSet.state(), Effects(streamed = true))) - .send(command(2, id, "Process", Request(id, GSet.add("a")))) - .expect(reply(2, GSet.state("a"), GSet.update("a"))) - .expect(streamed(1, GSet.state("a"), sideEffects("one") ++ synchronousSideEffects("two"))) - .send(command(3, id, "Process", Request(id, GSet.add("b")))) - .expect(reply(3, GSet.state("a", "b"), GSet.update("b"))) - .expect(streamed(1, GSet.state("a", "b"), sideEffects("one") ++ synchronousSideEffects("two"))) - .passivate() - } - - "verify streamed responses for connection tracking (proxy test)" in Vote.test { id => - implicit val actorSystem: ActorSystem = system - - val state0 = Vote.state(selfVote = false, votesFor = 0, totalVoters = 1) - val state1 = Vote.state(selfVote = true, votesFor = 1, totalVoters = 1) - val voteTrue = Some(Vote.updateWith(true)) - val voteFalse = Some(Vote.updateWith(false)) - - val monitor = crdtTckModelClient.processStreamed(StreamedRequest(id)).runWith(TestSink.probe[Response]) - monitor.request(1).expectNext(state0) - val crdtProtocol = interceptor.expectCrdtConnection() - crdtProtocol.expectClient(init(CrdtTckModel.name, id)) - crdtProtocol.expectClient(command(1, id, "ProcessStreamed", StreamedRequest(id), streamed = true)) - crdtProtocol.expectService(reply(1, state0, Effects(streamed = true))) - - val connectRequest = StreamedRequest(id, initialUpdate = voteTrue, cancelUpdate = voteFalse, empty = true) - val connect = crdtTckModelClient.processStreamed(connectRequest).runWith(TestSink.probe[Response]) - connect.request(1).expectNoMessage(100.millis) - monitor.request(1).expectNext(state1) - crdtProtocol.expectClient(command(2, id, "ProcessStreamed", connectRequest, streamed = true)) - crdtProtocol.expectService(crdtReply(2, None, Effects(streamed = true) ++ Vote.update(true))) - crdtProtocol.expectService(streamed(1, state1)) - - connect.cancel() - monitor.request(1).expectNext(state0) - crdtProtocol.expectClient(crdtStreamCancelled(2, id)) - crdtProtocol.expectService(streamCancelledResponse(2, Vote.update(false))) - crdtProtocol.expectService(streamed(1, state0)) - - monitor.cancel() - crdtProtocol.expectClient(crdtStreamCancelled(1, id)) - crdtProtocol.expectService(streamCancelledResponse(1)) - - val deleteRequest = Request(id, Seq(requestDelete)) - crdtTckModelClient.process(deleteRequest).futureValue mustBe state0 - crdtProtocol.expectClient(command(3, id, "Process", deleteRequest)) - crdtProtocol.expectService(reply(3, state0, deleteCrdt)) - crdtProtocol.expectClosed() - } - - // write consistency tests - - def incrementWith(increment: Long, writeConsistency: UpdateWriteConsistency): Seq[RequestAction] = - Seq(requestUpdate(GCounter.updateWith(increment).withWriteConsistency(writeConsistency))) - - def updateWith(value: Long, writeConsistency: CrdtWriteConsistency): Effects = - Effects(stateAction = crdtUpdate(GCounter.delta(value), writeConsistency)) - - "verify write consistency can be configured" in GCounter.test { id => - protocol.crdt - .connect() - .send(init(CrdtTckModel.name, id)) - .send(command(1, id, "Process", Request(id, incrementWith(1, UpdateWriteConsistency.LOCAL)))) - .expect(reply(1, GCounter.state(1), updateWith(1, CrdtWriteConsistency.LOCAL))) - .send(command(2, id, "Process", Request(id, incrementWith(2, UpdateWriteConsistency.MAJORITY)))) - .expect(reply(2, GCounter.state(3), updateWith(2, CrdtWriteConsistency.MAJORITY))) - .send(command(3, id, "Process", Request(id, incrementWith(3, UpdateWriteConsistency.ALL)))) - .expect(reply(3, GCounter.state(6), updateWith(3, CrdtWriteConsistency.ALL))) - .passivate() - } - } - - "verifying model test: event-sourced entities" must { - import EventSourcedMessages._ - import io.cloudstate.tck.model.eventsourced._ - - val ServiceTwo = EventSourcedTwo.name - - var entityId: Int = 0 - def nextEntityId(): String = { entityId += 1; s"entity:$entityId" } - - def eventSourcedTest(test: String => Any): Unit = - testFor(EventSourcedTckModel, EventSourcedTwo)(test(nextEntityId())) - - def emitEvent(value: String): RequestAction = - RequestAction(RequestAction.Action.Emit(Emit(value))) - - def emitEvents(values: String*): Seq[RequestAction] = - values.map(emitEvent) - - def forwardTo(id: String): RequestAction = - RequestAction(RequestAction.Action.Forward(Forward(id))) - - def sideEffectTo(id: String, synchronous: Boolean = false): RequestAction = - RequestAction(RequestAction.Action.Effect(Effect(id, synchronous))) - - def sideEffectsTo(ids: String*): Seq[RequestAction] = - ids.map(id => sideEffectTo(id, synchronous = false)) - - def failWith(message: String): RequestAction = - RequestAction(RequestAction.Action.Fail(Fail(message))) - - def persisted(value: String): ScalaPbAny = - protobufAny(Persisted(value)) - - def events(values: String*): Effects = - Effects(events = values.map(persisted)) - - def snapshotAndEvents(snapshotValue: String, eventValues: String*): Effects = - events(eventValues: _*).withSnapshot(persisted(snapshotValue)) - - def sideEffects(ids: String*): Effects = - createSideEffects(synchronous = false, ids) - - def synchronousSideEffects(ids: String*): Effects = - createSideEffects(synchronous = true, ids) - - def createSideEffects(synchronous: Boolean, ids: Seq[String]): Effects = - ids.foldLeft(Effects.empty) { case (e, id) => e.withSideEffect(ServiceTwo, "Call", Request(id), synchronous) } - - "verify initial empty state" in eventSourcedTest { id => - protocol.eventSourced - .connect() - .send(init(EventSourcedTckModel.name, id)) - .send(command(1, id, "Process", Request(id))) - .expect(reply(1, Response())) - .passivate() - } - - "verify single emitted event" in eventSourcedTest { id => - protocol.eventSourced - .connect() - .send(init(EventSourcedTckModel.name, id)) - .send(command(1, id, "Process", Request(id, emitEvents("A")))) - .expect(reply(1, Response("A"), events("A"))) - .send(command(2, id, "Process", Request(id))) - .expect(reply(2, Response("A"))) - .passivate() - } - - "verify multiple emitted events" in eventSourcedTest { id => - protocol.eventSourced - .connect() - .send(init(EventSourcedTckModel.name, id)) - .send(command(1, id, "Process", Request(id, emitEvents("A", "B", "C")))) - .expect(reply(1, Response("ABC"), events("A", "B", "C"))) - .send(command(2, id, "Process", Request(id))) - .expect(reply(2, Response("ABC"))) - .passivate() - } - - "verify multiple emitted events and snapshots" in eventSourcedTest { id => - protocol.eventSourced - .connect() - .send(init(EventSourcedTckModel.name, id)) - .send(command(1, id, "Process", Request(id, emitEvents("A")))) - .expect(reply(1, Response("A"), events("A"))) - .send(command(2, id, "Process", Request(id, emitEvents("B")))) - .expect(reply(2, Response("AB"), events("B"))) - .send(command(3, id, "Process", Request(id, emitEvents("C")))) - .expect(reply(3, Response("ABC"), events("C"))) - .send(command(4, id, "Process", Request(id, emitEvents("D")))) - .expect(reply(4, Response("ABCD"), events("D"))) - .send(command(5, id, "Process", Request(id, emitEvents("E")))) - .expect(reply(5, Response("ABCDE"), snapshotAndEvents("ABCDE", "E"))) - .send(command(6, id, "Process", Request(id, emitEvents("F", "G", "H")))) - .expect(reply(6, Response("ABCDEFGH"), events("F", "G", "H"))) - .send(command(7, id, "Process", Request(id, emitEvents("I", "J")))) - .expect(reply(7, Response("ABCDEFGHIJ"), snapshotAndEvents("ABCDEFGHIJ", "I", "J"))) - .send(command(8, id, "Process", Request(id, emitEvents("K")))) - .expect(reply(8, Response("ABCDEFGHIJK"), events("K"))) - .send(command(9, id, "Process", Request(id))) - .expect(reply(9, Response("ABCDEFGHIJK"))) - .passivate() - } - - "verify initial snapshot" in eventSourcedTest { id => - protocol.eventSourced - .connect() - .send(init(EventSourcedTckModel.name, id, snapshot(5, persisted("ABCDE")))) - .send(command(1, id, "Process", Request(id))) - .expect(reply(1, Response("ABCDE"))) - .passivate() - } - - "verify initial snapshot and events" in eventSourcedTest { id => - protocol.eventSourced - .connect() - .send(init(EventSourcedTckModel.name, id, snapshot(5, persisted("ABCDE")))) - .send(event(6, persisted("F"))) - .send(event(7, persisted("G"))) - .send(command(1, id, "Process", Request(id))) - .expect(reply(1, Response("ABCDEFG"))) - .passivate() - } - - "verify rehydration after passivation" in eventSourcedTest { id => - protocol.eventSourced - .connect() - .send(init(EventSourcedTckModel.name, id)) - .send(command(1, id, "Process", Request(id, emitEvents("A", "B", "C")))) - .expect(reply(1, Response("ABC"), events("A", "B", "C"))) - .send(command(2, id, "Process", Request(id, emitEvents("D", "E")))) - .expect(reply(2, Response("ABCDE"), snapshotAndEvents("ABCDE", "D", "E"))) - .send(command(3, id, "Process", Request(id, emitEvents("F")))) - .expect(reply(3, Response("ABCDEF"), events("F"))) - .send(command(4, id, "Process", Request(id, emitEvents("G")))) - .expect(reply(4, Response("ABCDEFG"), events("G"))) - .passivate() - protocol.eventSourced - .connect() - .send(init(EventSourcedTckModel.name, id, snapshot(5, persisted("ABCDE")))) - .send(event(6, persisted("F"))) - .send(event(7, persisted("G"))) - .send(command(1, id, "Process", Request(id))) - .expect(reply(1, Response("ABCDEFG"))) - .passivate() - } - - "verify forward to second service" in eventSourcedTest { id => - protocol.eventSourced - .connect() - .send(init(EventSourcedTckModel.name, id)) - .send(command(1, id, "Process", Request(id, Seq(forwardTo(id))))) - .expect(forward(1, EventSourcedTwo.name, "Call", Request(id))) - .passivate() - } - - "verify forward with emitted events" in eventSourcedTest { id => - protocol.eventSourced - .connect() - .send(init(EventSourcedTckModel.name, id)) - .send(command(1, id, "Process", Request(id, Seq(emitEvent("A"), forwardTo(id))))) - .expect(forward(1, EventSourcedTwo.name, "Call", Request(id), events("A"))) - .passivate() - } - - "verify forward with emitted events and snapshot" in eventSourcedTest { id => - protocol.eventSourced - .connect() - .send(init(EventSourcedTckModel.name, id)) - .send(command(1, id, "Process", Request(id, emitEvents("A", "B", "C")))) - .expect(reply(1, Response("ABC"), events("A", "B", "C"))) - .send(command(2, id, "Process", Request(id, Seq(emitEvent("D"), emitEvent("E"), forwardTo(id))))) - .expect(forward(2, EventSourcedTwo.name, "Call", Request(id), snapshotAndEvents("ABCDE", "D", "E"))) - .passivate() - } - - "verify reply with side effect to second service" in eventSourcedTest { id => - protocol.eventSourced - .connect() - .send(init(EventSourcedTckModel.name, id)) - .send(command(1, id, "Process", Request(id, Seq(sideEffectTo(id))))) - .expect(reply(1, Response(), sideEffects(id))) - .passivate() - } - - "verify synchronous side effect to second service" in eventSourcedTest { id => - protocol.eventSourced - .connect() - .send(init(EventSourcedTckModel.name, id)) - .send(command(1, id, "Process", Request(id, Seq(sideEffectTo(id, synchronous = true))))) - .expect(reply(1, Response(), synchronousSideEffects(id))) - .passivate() - } - - "verify forward and side effect to second service" in eventSourcedTest { id => - protocol.eventSourced - .connect() - .send(init(EventSourcedTckModel.name, id)) - .send(command(1, id, "Process", Request(id, Seq(sideEffectTo(id), forwardTo(id))))) - .expect(forward(1, ServiceTwo, "Call", Request(id), sideEffects(id))) - .passivate() - } - - "verify reply with multiple side effects" in eventSourcedTest { id => - protocol.eventSourced - .connect() - .send(init(EventSourcedTckModel.name, id)) - .send(command(1, id, "Process", Request(id, sideEffectsTo("1", "2", "3")))) - .expect(reply(1, Response(), sideEffects("1", "2", "3"))) - .passivate() - } - - "verify reply with multiple side effects, events, and snapshot" in eventSourcedTest { id => - val actions = emitEvents("A", "B", "C", "D", "E") ++ sideEffectsTo("1", "2", "3") - val effects = snapshotAndEvents("ABCDE", "A", "B", "C", "D", "E") ++ sideEffects("1", "2", "3") - protocol.eventSourced - .connect() - .send(init(EventSourcedTckModel.name, id)) - .send(command(1, id, "Process", Request(id, actions))) - .expect(reply(1, Response("ABCDE"), effects)) - .passivate() - } - - "verify failure action" in eventSourcedTest { id => - protocol.eventSourced - .connect() - .send(init(EventSourcedTckModel.name, id)) - .send(command(1, id, "Process", Request(id, Seq(failWith("expected failure"))))) - .expect(actionFailure(1, "expected failure")) - .passivate() - } - - "verify connection after failure action" in eventSourcedTest { id => - protocol.eventSourced - .connect() - .send(init(EventSourcedTckModel.name, id)) - .send(command(1, id, "Process", Request(id, Seq(emitEvent("A"))))) - .expect(reply(1, Response("A"), events("A"))) - .send(command(2, id, "Process", Request(id, Seq(failWith("expected failure"))))) - .expect(actionFailure(2, "expected failure")) - .send(command(3, id, "Process", Request(id))) - .expect(reply(3, Response("A"))) - .passivate() - } - - "verify failure actions do not retain emitted events, by requesting entity restart" in eventSourcedTest { id => - protocol.eventSourced - .connect() - .send(init(EventSourcedTckModel.name, id)) - .send(command(1, id, "Process", Request(id, emitEvents("A", "B", "C")))) - .expect(reply(1, Response("ABC"), events("A", "B", "C"))) - .send(command(2, id, "Process", Request(id, Seq(emitEvent("4"), emitEvent("5"), failWith("failure 1"))))) - .expect(actionFailure(2, "failure 1", restart = true)) - .passivate() - protocol.eventSourced - .connect() - .send(init(EventSourcedTckModel.name, id)) - .send(event(1, persisted("A"))) - .send(event(2, persisted("B"))) - .send(event(3, persisted("C"))) - .send(command(1, id, "Process", Request(id, emitEvents("D")))) - .expect(reply(1, Response("ABCD"), events("D"))) - .send(command(2, id, "Process", Request(id, Seq(emitEvent("6"), failWith("failure 2"), emitEvent("7"))))) - .expect(actionFailure(2, "failure 2", restart = true)) - .passivate() - protocol.eventSourced - .connect() - .send(init(EventSourcedTckModel.name, id)) - .send(event(1, persisted("A"))) - .send(event(2, persisted("B"))) - .send(event(3, persisted("C"))) - .send(event(4, persisted("D"))) - .send(command(1, id, "Process", Request(id, emitEvents("E")))) - .expect(reply(1, Response("ABCDE"), snapshotAndEvents("ABCDE", "E"))) - .passivate() - } - - "verify failure actions do not allow side effects" in eventSourcedTest { id => - protocol.eventSourced - .connect() - .send(init(EventSourcedTckModel.name, id)) - .send(command(1, id, "Process", Request(id, Seq(sideEffectTo(id), failWith("expected failure"))))) - .expect(actionFailure(1, "expected failure")) - .passivate() - } - } - - // TODO convert this into a ScalaCheck generated test case - "verifying app test: event-sourced shopping cart" must { - import com.example.shoppingcart.shoppingcart._ - import EventSourcedMessages._ - import EventSourcedShoppingCartVerifier._ - - def verifyGetInitialEmptyCart(session: EventSourcedShoppingCartVerifier, cartId: String): Unit = { - eventSourcedShoppingCartClient.getCart(GetShoppingCart(cartId)).futureValue mustBe Cart() - session.verifyConnection() - session.verifyGetInitialEmptyCart(cartId) - } - - def verifyGetCart(session: EventSourcedShoppingCartVerifier, cartId: String, expected: Item*): Unit = { - val expectedCart = shoppingCart(expected: _*) - eventSourcedShoppingCartClient.getCart(GetShoppingCart(cartId)).futureValue mustBe expectedCart - session.verifyGetCart(cartId, expectedCart) - } - - def verifyAddItem(session: EventSourcedShoppingCartVerifier, cartId: String, item: Item): Unit = { - val addLineItem = AddLineItem(cartId, item.id, item.name, item.quantity) - eventSourcedShoppingCartClient.addItem(addLineItem).futureValue mustBe EmptyScalaMessage - session.verifyAddItem(cartId, item) - } - - def verifyRemoveItem(session: EventSourcedShoppingCartVerifier, cartId: String, itemId: String): Unit = { - val removeLineItem = RemoveLineItem(cartId, itemId) - eventSourcedShoppingCartClient.removeItem(removeLineItem).futureValue mustBe EmptyScalaMessage - session.verifyRemoveItem(cartId, itemId) - } - - def verifyAddItemFailure(session: EventSourcedShoppingCartVerifier, cartId: String, item: Item): Unit = { - val addLineItem = AddLineItem(cartId, item.id, item.name, item.quantity) - val error = eventSourcedShoppingCartClient.addItem(addLineItem).failed.futureValue - error mustBe a[StatusRuntimeException] - val description = error.asInstanceOf[StatusRuntimeException].getStatus.getDescription - session.verifyAddItemFailure(cartId, item, description) - } - - def verifyRemoveItemFailure(session: EventSourcedShoppingCartVerifier, cartId: String, itemId: String): Unit = { - val removeLineItem = RemoveLineItem(cartId, itemId) - val error = eventSourcedShoppingCartClient.removeItem(removeLineItem).failed.futureValue - error mustBe a[StatusRuntimeException] - val description = error.asInstanceOf[StatusRuntimeException].getStatus.getDescription - session.verifyRemoveItemFailure(cartId, itemId, description) - } - - "verify get cart, add item, remove item, and failures" in testFor(ShoppingCart) { - val session = shoppingCartSession(interceptor) - verifyGetInitialEmptyCart(session, "cart:1") // initial empty state - verifyAddItem(session, "cart:1", Item("product:1", "Product1", 1)) // add the first product - verifyAddItem(session, "cart:1", Item("product:2", "Product2", 2)) // add the second product - verifyAddItem(session, "cart:1", Item("product:1", "Product1", 11)) // increase first product - verifyAddItem(session, "cart:1", Item("product:2", "Product2", 31)) // increase second product - verifyGetCart(session, "cart:1", Item("product:1", "Product1", 12), Item("product:2", "Product2", 33)) // check state - verifyRemoveItem(session, "cart:1", "product:1") // remove first product - verifyAddItemFailure(session, "cart:1", Item("product:2", "Product2", -7)) // add negative quantity - verifyAddItemFailure(session, "cart:1", Item("product:1", "Product1", 0)) // add zero quantity - verifyRemoveItemFailure(session, "cart:1", "product:1") // remove non-existing product - verifyGetCart(session, "cart:1", Item("product:2", "Product2", 33)) // check final state - } - } - - "verifying proxy test: gRPC ServerReflection API" must { - "verify that the proxy supports server reflection" in { - import grpc.reflection.v1alpha.reflection._ - import ServerReflectionRequest.{MessageRequest => In} - import ServerReflectionResponse.{MessageResponse => Out} - - val expectedServices = Seq(ServerReflection.name) ++ enabledServices.sorted - - val connection = client.serverReflection.connect() - - connection.sendAndExpect( - In.ListServices(""), - Out.ListServicesResponse(ListServiceResponse(expectedServices.map(s => ServiceResponse(s)))) - ) - - connection.sendAndExpect( - In.ListServices("nonsense.blabla."), - Out.ListServicesResponse(ListServiceResponse(expectedServices.map(s => ServiceResponse(s)))) - ) - - connection.sendAndExpect( - In.FileContainingSymbol("nonsense.blabla.Void"), - Out.FileDescriptorResponse(FileDescriptorResponse(Nil)) - ) - - connection.close() - } - } - - "verifying proxy test: HTTP API" must { - "verify the HTTP API for event-sourced ShoppingCart service" in testFor(EventSourcedShoppingCart) { - import EventSourcedShoppingCartVerifier._ - - def checkHttpRequest(path: String, body: String = null)(expected: => String): Unit = { - val response = client.http.request(path, body) - val expectedResponse = expected - response.futureValue mustBe expectedResponse - } - - val session = shoppingCartSession(interceptor) - - checkHttpRequest("carts/foo") { - session.verifyConnection() - session.verifyGetInitialEmptyCart("foo") - """{"items":[]}""" - } - - checkHttpRequest("cart/foo/items/add", """{"productId": "A14362347", "name": "Deluxe", "quantity": 5}""") { - session.verifyAddItem("foo", Item("A14362347", "Deluxe", 5)) - "{}" - } - - checkHttpRequest("cart/foo/items/add", """{"productId": "B14623482", "name": "Basic", "quantity": 1}""") { - session.verifyAddItem("foo", Item("B14623482", "Basic", 1)) - "{}" - } - - checkHttpRequest("cart/foo/items/add", """{"productId": "A14362347", "name": "Deluxe", "quantity": 2}""") { - session.verifyAddItem("foo", Item("A14362347", "Deluxe", 2)) - "{}" - } - - checkHttpRequest("carts/foo") { - session.verifyGetCart("foo", shoppingCart(Item("A14362347", "Deluxe", 7), Item("B14623482", "Basic", 1))) - """{"items":[{"productId":"A14362347","name":"Deluxe","quantity":7},{"productId":"B14623482","name":"Basic","quantity":1}]}""" - } - - checkHttpRequest("carts/foo/items") { - session.verifyGetCart("foo", shoppingCart(Item("A14362347", "Deluxe", 7), Item("B14623482", "Basic", 1))) - """[{"productId":"A14362347","name":"Deluxe","quantity":7.0},{"productId":"B14623482","name":"Basic","quantity":1.0}]""" - } - - checkHttpRequest("cart/foo/items/A14362347/remove", "") { - session.verifyRemoveItem("foo", "A14362347") - "{}" - } - - checkHttpRequest("carts/foo") { - session.verifyGetCart("foo", shoppingCart(Item("B14623482", "Basic", 1))) - """{"items":[{"productId":"B14623482","name":"Basic","quantity":1}]}""" - } - - checkHttpRequest("carts/foo/items") { - session.verifyGetCart("foo", shoppingCart(Item("B14623482", "Basic", 1))) - """[{"productId":"B14623482","name":"Basic","quantity":1.0}]""" - } - } - - "verify the HTTP API for value-based ShoppingCart service" in testFor(ValueEntityShoppingCart) { - import ValueEntityShoppingCartVerifier._ - - def checkHttpRequest(path: String, body: String = null)(expected: => String): Unit = { - val response = client.http.request(path, body) - val expectedResponse = expected - response.futureValue mustBe expectedResponse - } - - val session = shoppingCartSession(interceptor) - - checkHttpRequest("ve/carts/foo") { - session.verifyConnection() - session.verifyGetInitialEmptyCart("foo") - """{"items":[]}""" - } - - checkHttpRequest("ve/cart/foo/items/add", """{"productId": "A14362347", "name": "Deluxe", "quantity": 5}""") { - session.verifyAddItem("foo", Item("A14362347", "Deluxe", 5), Cart(Item("A14362347", "Deluxe", 5))) - "{}" - } - - checkHttpRequest("ve/cart/foo/items/add", """{"productId": "B14623482", "name": "Basic", "quantity": 1}""") { - session.verifyAddItem("foo", - Item("B14623482", "Basic", 1), - Cart(Item("A14362347", "Deluxe", 5), Item("B14623482", "Basic", 1))) - "{}" - } - - checkHttpRequest("ve/cart/foo/items/add", """{"productId": "A14362347", "name": "Deluxe", "quantity": 2}""") { - session.verifyAddItem("foo", - Item("A14362347", "Deluxe", 2), - Cart(Item("A14362347", "Deluxe", 7), Item("B14623482", "Basic", 1))) - "{}" - } - - checkHttpRequest("ve/carts/foo") { - session.verifyGetCart("foo", shoppingCart(Item("A14362347", "Deluxe", 7), Item("B14623482", "Basic", 1))) - """{"items":[{"productId":"A14362347","name":"Deluxe","quantity":7},{"productId":"B14623482","name":"Basic","quantity":1}]}""" - } - - checkHttpRequest("ve/carts/foo/items") { - session.verifyGetCart("foo", shoppingCart(Item("A14362347", "Deluxe", 7), Item("B14623482", "Basic", 1))) - """[{"productId":"A14362347","name":"Deluxe","quantity":7.0},{"productId":"B14623482","name":"Basic","quantity":1.0}]""" - } - - checkHttpRequest("ve/cart/foo/items/A14362347/remove", "") { - session.verifyRemoveItem("foo", "A14362347", Cart(Item("B14623482", "Basic", 1))) - "{}" - } - - checkHttpRequest("ve/carts/foo") { - session.verifyGetCart("foo", shoppingCart(Item("B14623482", "Basic", 1))) - """{"items":[{"productId":"B14623482","name":"Basic","quantity":1}]}""" - } - - checkHttpRequest("ve/carts/foo/items") { - session.verifyGetCart("foo", shoppingCart(Item("B14623482", "Basic", 1))) - """[{"productId":"B14623482","name":"Basic","quantity":1.0}]""" - } - - checkHttpRequest("ve/carts/foo/remove", """{"userId": "foo"}""") { - session.verifyRemoveCart("foo") - "{}" - } - - checkHttpRequest("ve/carts/foo") { - session.verifyGetCart("foo", shoppingCart()) - """{"items":[]}""" - } - } - } - - "verifying model test: value-based entities" must { - import ValueEntityMessages._ - import io.cloudstate.tck.model.valueentity.valueentity._ - - val ServiceTwo = ValueEntityTwo.name - - var entityId: Int = 0 - def nextEntityId(): String = { entityId += 1; s"entity:$entityId" } - - def valueEntityTest(test: String => Any): Unit = - testFor(ValueEntityTckModel, ValueEntityTwo)(test(nextEntityId())) - - def updateState(value: String): RequestAction = - RequestAction(RequestAction.Action.Update(Update(value))) - - def updateStates(values: String*): Seq[RequestAction] = - values.map(updateState) - - def deleteState(): RequestAction = - RequestAction(RequestAction.Action.Delete(Delete())) - - def updateAndDeleteActions(values: String*): Seq[RequestAction] = - values.map(updateState) :+ deleteState() - - def deleteBetweenUpdateActions(first: String, second: String): Seq[RequestAction] = - Seq(updateState(first), deleteState(), updateState(second)) - - def forwardTo(id: String): RequestAction = - RequestAction(RequestAction.Action.Forward(Forward(id))) - - def sideEffectTo(id: String, synchronous: Boolean = false): RequestAction = - RequestAction(RequestAction.Action.Effect(Effect(id, synchronous))) - - def sideEffectsTo(ids: String*): Seq[RequestAction] = - ids.map(id => sideEffectTo(id)) - - def failWith(message: String): RequestAction = - RequestAction(RequestAction.Action.Fail(Fail(message))) - - def persisted(value: String): ScalaPbAny = - protobufAny(Persisted(value)) - - def update(value: String): Effects = - Effects.empty.withUpdateAction(persisted(value)) - - def delete(): Effects = - Effects.empty.withDeleteAction() - - def sideEffects(ids: String*): Effects = - createSideEffects(synchronous = false, ids) - - def synchronousSideEffects(ids: String*): Effects = - createSideEffects(synchronous = true, ids) - - def createSideEffects(synchronous: Boolean, ids: Seq[String]): Effects = - ids.foldLeft(Effects.empty) { case (e, id) => e.withSideEffect(ServiceTwo, "Call", Request(id), synchronous) } - - "verify initial empty state" in valueEntityTest { id => - protocol.valueEntity - .connect() - .send(init(ValueEntityTckModel.name, id)) - .send(command(1, id, "Process", Request(id))) - .expect(reply(1, Response())) - .passivate() - } - - "verify update state" in valueEntityTest { id => - protocol.valueEntity - .connect() - .send(init(ValueEntityTckModel.name, id)) - .send(command(1, id, "Process", Request(id, updateStates("A")))) - .expect(reply(1, Response("A"), update("A"))) - .send(command(2, id, "Process", Request(id))) - .expect(reply(2, Response("A"))) - .passivate() - } - - "verify delete state" in valueEntityTest { id => - protocol.valueEntity - .connect() - .send(init(ValueEntityTckModel.name, id)) - .send(command(1, id, "Process", Request(id, updateStates("A")))) - .expect(reply(1, Response("A"), update("A"))) - .send(command(2, id, "Process", Request(id, Seq(deleteState())))) - .expect(reply(2, Response(), delete())) - .send(command(3, id, "Process", Request(id))) - .expect(reply(3, Response())) - .passivate() - } - - "verify sub invocations with multiple update states" in valueEntityTest { id => - protocol.valueEntity - .connect() - .send(init(ValueEntityTckModel.name, id)) - .send(command(1, id, "Process", Request(id, updateStates("A", "B", "C")))) - .expect(reply(1, Response("C"), update("C"))) - .send(command(2, id, "Process", Request(id))) - .expect(reply(2, Response("C"))) - .passivate() - } - - "verify sub invocations with multiple update states and delete states" in valueEntityTest { id => - protocol.valueEntity - .connect() - .send(init(ValueEntityTckModel.name, id)) - .send(command(1, id, "Process", Request(id, updateAndDeleteActions("A", "B")))) - .expect(reply(1, Response(), delete())) - .send(command(2, id, "Process", Request(id))) - .expect(reply(2, Response())) - .passivate() - } - - "verify sub invocations with update, delete and update states" in valueEntityTest { id => - protocol.valueEntity - .connect() - .send(init(ValueEntityTckModel.name, id)) - .send(command(1, id, "Process", Request(id, deleteBetweenUpdateActions("A", "B")))) - .expect(reply(1, Response("B"), update("B"))) - .send(command(2, id, "Process", Request(id))) - .expect(reply(2, Response("B"))) - .passivate() - } - - "verify rehydration after passivation" in valueEntityTest { id => - protocol.valueEntity - .connect() - .send(init(ValueEntityTckModel.name, id)) - .send(command(1, id, "Process", Request(id, updateStates("A")))) - .expect(reply(1, Response("A"), update("A"))) - .send(command(2, id, "Process", Request(id, updateStates("B")))) - .expect(reply(2, Response("B"), update("B"))) - .send(command(3, id, "Process", Request(id, updateStates("C")))) - .expect(reply(3, Response("C"), update("C"))) - .send(command(4, id, "Process", Request(id, updateStates("D")))) - .expect(reply(4, Response("D"), update("D"))) - .passivate() - protocol.valueEntity - .connect() - .send(init(ValueEntityTckModel.name, id, state(persisted("D")))) - .send(command(1, id, "Process", Request(id, updateStates("E")))) - .expect(reply(1, Response("E"), update("E"))) - .send(command(2, id, "Process", Request(id))) - .expect(reply(2, Response("E"))) - .passivate() - } - - "verify reply with multiple side effects" in valueEntityTest { id => - protocol.valueEntity - .connect() - .send(init(ValueEntityTckModel.name, id)) - .send(command(1, id, "Process", Request(id, sideEffectsTo("1", "2", "3")))) - .expect(reply(1, Response(), sideEffects("1", "2", "3"))) - .passivate() - } - - "verify reply with side effect to second service" in valueEntityTest { id => - protocol.valueEntity - .connect() - .send(init(ValueEntityTckModel.name, id)) - .send(command(1, id, "Process", Request(id, Seq(sideEffectTo(id))))) - .expect(reply(1, Response(), sideEffects(id))) - .passivate() - } - - "verify reply with multiple side effects and state" in valueEntityTest { id => - val actions = updateStates("A", "B", "C", "D", "E") ++ sideEffectsTo("1", "2", "3") - val effects = sideEffects("1", "2", "3").withUpdateAction(persisted("E")) - protocol.valueEntity - .connect() - .send(init(ValueEntityTckModel.name, id)) - .send(command(1, id, "Process", Request(id, actions))) - .expect(reply(1, Response("E"), effects)) - .passivate() - } - - "verify synchronous side effect to second service" in valueEntityTest { id => - protocol.valueEntity - .connect() - .send(init(ValueEntityTckModel.name, id)) - .send(command(1, id, "Process", Request(id, Seq(sideEffectTo(id, synchronous = true))))) - .expect(reply(1, Response(), synchronousSideEffects(id))) - .passivate() - } - - "verify forward to second service" in valueEntityTest { id => - protocol.valueEntity - .connect() - .send(init(ValueEntityTckModel.name, id)) - .send(command(1, id, "Process", Request(id, Seq(forwardTo(id))))) - .expect(forward(1, ServiceTwo, "Call", Request(id))) - .passivate() - } - - "verify forward with updated state to second service" in valueEntityTest { id => - protocol.valueEntity - .connect() - .send(init(ValueEntityTckModel.name, id)) - .send(command(1, id, "Process", Request(id, Seq(updateState("A"), forwardTo(id))))) - .expect(forward(1, ServiceTwo, "Call", Request(id), update("A"))) - .passivate() - } - - "verify forward and side effect to second service" in valueEntityTest { id => - protocol.valueEntity - .connect() - .send(init(ValueEntityTckModel.name, id)) - .send(command(1, id, "Process", Request(id, Seq(sideEffectTo(id), forwardTo(id))))) - .expect(forward(1, ServiceTwo, "Call", Request(id), sideEffects(id))) - .passivate() - } - - "verify failure action" in valueEntityTest { id => - protocol.valueEntity - .connect() - .send(init(ValueEntityTckModel.name, id)) - .send(command(1, id, "Process", Request(id, Seq(failWith("expected failure"))))) - .expect(actionFailure(1, "expected failure")) - .passivate() - } - - "verify connection after failure action" in valueEntityTest { id => - protocol.valueEntity - .connect() - .send(init(ValueEntityTckModel.name, id)) - .send(command(1, id, "Process", Request(id, Seq(updateState("A"))))) - .expect(reply(1, Response("A"), update("A"))) - .send(command(2, id, "Process", Request(id, Seq(failWith("expected failure"))))) - .expect(actionFailure(2, "expected failure")) - .send(command(3, id, "Process", Request(id))) - .expect(reply(3, Response("A"))) - .passivate() - } - - "verify failure action do not allow side effects" in valueEntityTest { id => - protocol.valueEntity - .connect() - .send(init(ValueEntityTckModel.name, id)) - .send(command(1, id, "Process", Request(id, Seq(sideEffectTo(id), failWith("expected failure"))))) - .expect(actionFailure(1, "expected failure")) - .passivate() - } - } - - "verifying app test: value-based entity shopping cart" must { - import com.example.valueentity.shoppingcart.shoppingcart.{ - AddLineItem => ValueEntityAddLineItem, - Cart => ValueEntityCart, - GetShoppingCart => ValueEntityGetShoppingCart, - RemoveLineItem => ValueEntityRemoveLineItem - } - import ValueEntityMessages._ - import ValueEntityShoppingCartVerifier._ - - def verifyGetInitialEmptyCart(session: ValueEntityShoppingCartVerifier, cartId: String): Unit = { - valueEntityShoppingCartClient.getCart(ValueEntityGetShoppingCart(cartId)).futureValue mustBe ValueEntityCart() - session.verifyConnection() - session.verifyGetInitialEmptyCart(cartId) - } - - def verifyGetCart(session: ValueEntityShoppingCartVerifier, cartId: String, expected: Item*): Unit = { - val expectedCart = shoppingCart(expected: _*) - valueEntityShoppingCartClient.getCart(ValueEntityGetShoppingCart(cartId)).futureValue mustBe expectedCart - session.verifyGetCart(cartId, expectedCart) - } - - def verifyAddItem(session: ValueEntityShoppingCartVerifier, cartId: String, item: Item, expected: Cart): Unit = { - val addLineItem = ValueEntityAddLineItem(cartId, item.id, item.name, item.quantity) - valueEntityShoppingCartClient.addItem(addLineItem).futureValue mustBe EmptyScalaMessage - session.verifyAddItem(cartId, item, expected) - } - - def verifyRemoveItem(session: ValueEntityShoppingCartVerifier, - cartId: String, - itemId: String, - expected: Cart): Unit = { - val removeLineItem = ValueEntityRemoveLineItem(cartId, itemId) - valueEntityShoppingCartClient.removeItem(removeLineItem).futureValue mustBe EmptyScalaMessage - session.verifyRemoveItem(cartId, itemId, expected) - } - - def verifyAddItemFailure(session: ValueEntityShoppingCartVerifier, cartId: String, item: Item): Unit = { - val addLineItem = ValueEntityAddLineItem(cartId, item.id, item.name, item.quantity) - val error = valueEntityShoppingCartClient.addItem(addLineItem).failed.futureValue - error mustBe a[StatusRuntimeException] - val description = error.asInstanceOf[StatusRuntimeException].getStatus.getDescription - session.verifyAddItemFailure(cartId, item, description) - } - - def verifyRemoveItemFailure(session: ValueEntityShoppingCartVerifier, cartId: String, itemId: String): Unit = { - val removeLineItem = ValueEntityRemoveLineItem(cartId, itemId) - val error = valueEntityShoppingCartClient.removeItem(removeLineItem).failed.futureValue - error mustBe a[StatusRuntimeException] - val description = error.asInstanceOf[StatusRuntimeException].getStatus.getDescription - session.verifyRemoveItemFailure(cartId, itemId, description) - } - - "verify get cart, add item, remove item, and failures" in testFor(ValueEntityShoppingCart) { - val session = shoppingCartSession(interceptor) - verifyGetInitialEmptyCart(session, "cart:1") // initial empty state - - // add the first product and pass the expected state - verifyAddItem(session, "cart:1", Item("product:1", "Product1", 1), Cart(Item("product:1", "Product1", 1))) - - // add the second product and pass the expected state - verifyAddItem( - session, - "cart:1", - Item("product:2", "Product2", 2), - Cart(Item("product:1", "Product1", 1), Item("product:2", "Product2", 2)) - ) - - // increase first product and pass the expected state - verifyAddItem( - session, - "cart:1", - Item("product:1", "Product1", 11), - Cart(Item("product:1", "Product1", 12), Item("product:2", "Product2", 2)) - ) - - // increase second product and pass the expected state - verifyAddItem( - session, - "cart:1", - Item("product:2", "Product2", 31), - Cart(Item("product:1", "Product1", 12), Item("product:2", "Product2", 33)) - ) - - verifyGetCart(session, "cart:1", Item("product:1", "Product1", 12), Item("product:2", "Product2", 33)) // check state - - // remove first product and pass the expected state - verifyRemoveItem(session, "cart:1", "product:1", Cart(Item("product:2", "Product2", 33))) - verifyAddItemFailure(session, "cart:1", Item("product:2", "Product2", -7)) // add negative quantity - verifyAddItemFailure(session, "cart:1", Item("product:1", "Product1", 0)) // add zero quantity - verifyRemoveItemFailure(session, "cart:1", "product:1") // remove non-existing product - verifyGetCart(session, "cart:1", Item("product:2", "Product2", 33)) // check final state - } - } - - "verify proxy test: event log subscriptions" must { - def eventLogSubscriptionTest(test: => Any): Unit = - testFor(EventLogSubscriberModel)(test) - - def emitEventOne(id: String, step: eventlogeventing.ProcessStep.Step) = - eventLogEventingEventSourcedEntityOne.emitEvent( - EmitEventRequest(id, - EmitEventRequest.Event - .EventOne(eventlogeventing.EventOne(Some(eventlogeventing.ProcessStep(step))))) - ) - def emitReplyEventOne(id: String, message: String) = - emitEventOne(id, eventlogeventing.ProcessStep.Step.Reply(eventlogeventing.Reply(message))) - def emitForwardEventOne(id: String, message: String) = - emitEventOne(id, eventlogeventing.ProcessStep.Step.Forward(eventlogeventing.Forward(message))) - def verifyEventSourcedInitCommandReply(id: String) = { - val connection = interceptor.expectEventSourcedConnection() - val init = connection.expectClientMessage[EventSourcedStreamIn.Message.Init] - init.value.serviceName must ===(eventlogeventing.EventSourcedEntityOne.name) - init.value.entityId must ===(id) - connection.expectClientMessage[EventSourcedStreamIn.Message.Command] - connection.expectServiceMessage[EventSourcedStreamOut.Message.Reply] - } - def verifySubscriberCommandResponse(step: eventlogeventing.ProcessStep.Step) = { - val subscriberConnection = interceptor.expectActionUnaryConnection() - val eventOneIn = eventlogeventing.EventOne.parseFrom( - subscriberConnection.command.payload.fold(ByteString.EMPTY)(_.value).newCodedInput() - ) - eventOneIn.step must ===(Some(eventlogeventing.ProcessStep(step))) - subscriberConnection.expectResponse() - } - def verifySubscriberReplyCommand(id: String, message: String) = { - val response = - verifySubscriberCommandResponse(eventlogeventing.ProcessStep.Step.Reply(eventlogeventing.Reply(message))) - response.response.isReply must ===(true) - val reply = eventlogeventing.Response.parseFrom(response.response.reply.get.payload.get.value.newCodedInput()) - reply.id must ===(id) - reply.message must ===(message) - } - def verifySubscriberForwardCommand(id: String, message: String) = { - val response = - verifySubscriberCommandResponse(eventlogeventing.ProcessStep.Step.Forward(eventlogeventing.Forward(message))) - response.response.isForward must ===(true) - val subscriberConnection = interceptor.expectActionUnaryConnection() - subscriberConnection.command.name must ===("Effect") - } - "consume an event" in eventLogSubscriptionTest { - emitReplyEventOne("eventlogeventing:1", "some message") - verifyEventSourcedInitCommandReply("eventlogeventing:1") - verifySubscriberReplyCommand("eventlogeventing:1", "some message") - } - - "forward a consumed event" in eventLogSubscriptionTest { - emitForwardEventOne("eventlogeventing:2", "some message") - verifyEventSourcedInitCommandReply("eventlogeventing:2") - verifySubscriberForwardCommand("eventlogeventing:2", "some message") - } - - "process json events" in eventLogSubscriptionTest { - eventLogEventingEventSourcedEntityTwo.emitJsonEvent( - eventlogeventing.JsonEvent("eventlogeventing:3", "some json message") - ) - - val connection = interceptor.expectEventSourcedConnection() - val init = connection.expectClientMessage[EventSourcedStreamIn.Message.Init] - init.value.serviceName must ===(eventlogeventing.EventSourcedEntityTwo.name) - init.value.entityId must ===("eventlogeventing:3") - connection.expectClientMessage[EventSourcedStreamIn.Message.Command] - val reply = connection.expectServiceMessage[EventSourcedStreamOut.Message.Reply] - reply.value.events must have size (1) - reply.value.events.head.typeUrl must startWith("json.cloudstate.io/") - - val subscriberConnection = interceptor.expectActionUnaryConnection() - val response = subscriberConnection.expectResponse() - val parsed = eventlogeventing.Response.parseFrom(response.response.reply.get.payload.get.value.newCodedInput()) - parsed.id must ===("eventlogeventing:3") - parsed.message must ===("some json message") - } - } - } -} diff --git a/tck/src/main/scala/io/cloudstate/tck/CloudstateTCK.scala b/tck/src/main/scala/io/cloudstate/tck/CloudstateTCK.scala new file mode 100644 index 000000000..c1244dbad --- /dev/null +++ b/tck/src/main/scala/io/cloudstate/tck/CloudstateTCK.scala @@ -0,0 +1,59 @@ +/* + * Copyright 2019 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 io.cloudstate.tck + +class ConfiguredCloudstateTCK extends CloudstateTCK(TCKSpec.Settings.loadFromConfig()) + +class CloudstateTCK(description: String, val settings: TCKSpec.Settings) + extends TCKSpec + with ProxyTCK + with ActionTCK + with EntityTCK + with ShoppingCartTCK + with EventSourcedEntityTCK + with EventSourcedShoppingCartTCK + with CrdtEntityTCK + with EventingTCK { + + def this(settings: TCKSpec.Settings) = this("", settings) + + ("Cloudstate TCK " + description) when { + + "verifying discovery protocol" must verifyDiscovery() + + "verifying model test: action" must verifyActionModel() + + "verifying model test: entity" must verifyEntityModel() + + "verifying app test: shopping cart" must verifyShoppingCart() + + "verifying model test: event sourced entity" must verifyEventSourcedEntityModel() + + "verifying app test: event sourced shopping cart" must verifyEventSourcedShoppingCart() + + "verifying model test: CRDT entity" must verifyCrdtEntityModel() + + "verifying proxy test: entity" must verifyEntityProxy() + + "verifying proxy test: CRDT entity" must verifyCrdtEntityProxy() + + "verifying proxy test: gRPC server reflection" must verifyServerReflection() + + "verifying proxy test: event log subscriptions" must verifyEventingProxy() + + } +} diff --git a/tck/src/main/scala/io/cloudstate/tck/CrdtEntityTCK.scala b/tck/src/main/scala/io/cloudstate/tck/CrdtEntityTCK.scala new file mode 100644 index 000000000..20028970f --- /dev/null +++ b/tck/src/main/scala/io/cloudstate/tck/CrdtEntityTCK.scala @@ -0,0 +1,1825 @@ +/* + * Copyright 2019 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 io.cloudstate.tck + +import akka.actor.ActorSystem +import akka.stream.testkit.scaladsl.TestSink +import io.cloudstate.protocol.crdt._ +import io.cloudstate.tck.model.crdt._ +import io.cloudstate.testkit.crdt.CrdtMessages._ +import scala.concurrent.duration._ + +trait CrdtEntityTCK extends TCKSpec { + + object CrdtEntityTCKModel { + val Protocol: String = Crdt.name + val Service: String = CrdtTckModel.name + val ServiceTwo: String = CrdtTwo.name + + var entityId: Int = 0 + + def nextEntityId(crdtType: String): String = { + entityId += 1; s"$crdtType-$entityId" + } + + def crdtTest(crdtType: String)(test: String => Any): Unit = + testFor(CrdtTckModel, CrdtTwo)(test(nextEntityId(crdtType))) + + def requestUpdate(update: Update): RequestAction = + RequestAction(RequestAction.Action.Update(update)) + + val requestDelete: RequestAction = + RequestAction(RequestAction.Action.Delete(Delete())) + + val deleteCrdt: Effects = + Effects(stateAction = crdtDelete) + + def forwardTo(id: String): RequestAction = + RequestAction(RequestAction.Action.Forward(Forward(id))) + + def sideEffectTo(id: String, synchronous: Boolean = false): RequestAction = + RequestAction(RequestAction.Action.Effect(Effect(id, synchronous))) + + def sideEffectsTo(ids: String*): Seq[RequestAction] = + ids.map(id => sideEffectTo(id)) + + def sideEffects(ids: String*): Effects = + createSideEffects(synchronous = false, ids) + + def synchronousSideEffects(ids: String*): Effects = + createSideEffects(synchronous = true, ids) + + def createSideEffects(synchronous: Boolean, ids: Seq[String]): Effects = + ids.foldLeft(Effects.empty) { case (e, id) => e.withSideEffect(ServiceTwo, "Call", Request(id), synchronous) } + + def forwarded(id: Long, entityId: String, effects: Effects = Effects.empty): CrdtStreamOut.Message = + forward(id, ServiceTwo, "Call", Request(entityId), effects) + + def failWith(message: String): RequestAction = + RequestAction(RequestAction.Action.Fail(Fail(message))) + + // Sort sequences in received Deltas (using ScalaPB Lens support) for comparing easily + object Delta { + def sorted(out: CrdtStreamOut): CrdtStreamOut = + out.update(_.reply.stateAction.update.modify(Delta.sort)) + + def sort(delta: CrdtDelta): CrdtDelta = + if (delta.delta.isGset) + delta.update( + _.gset.added.modify(_.sortBy(readPrimitiveString)) + ) + else if (delta.delta.isOrset) + delta.update( + _.orset.update( + _.removed.modify(_.sortBy(readPrimitiveString)), + _.added.modify(_.sortBy(readPrimitiveString)) + ) + ) + else if (delta.delta.isOrmap) + delta.update( + _.ormap.update( + _.removed.modify(_.sortBy(readPrimitiveString)), + _.updated.modify(_.map(_.update(_.delta.modify(Delta.sort))).sortBy(_.key.map(readPrimitiveString))), + _.added.modify(_.map(_.update(_.delta.modify(Delta.sort))).sortBy(_.key.map(readPrimitiveString))) + ) + ) + else delta + } + + object GCounter { + def state(value: Long): Response = + Response(Some(State(State.Value.Gcounter(GCounterValue(value))))) + + def incrementBy(increments: Long*): Seq[RequestAction] = + increments.map(increment => requestUpdate(updateWith(increment))) + + def updateWith(increment: Long): Update = + Update(Update.Update.Gcounter(GCounterUpdate(increment))) + + def update(value: Long): Effects = + Effects(stateAction = crdtUpdate(delta(value))) + + def delta(value: Long): CrdtDelta.Delta.Gcounter = deltaGCounter(value) + + def test(run: String => Any): Unit = crdtTest("GCounter")(run) + } + + object PNCounter { + def state(value: Long): Response = + Response(Some(State(State.Value.Pncounter(PNCounterValue(value))))) + + def changeBy(changes: Long*): Seq[RequestAction] = + changes.map(change => requestUpdate(updateWith(change))) + + def updateWith(change: Long): Update = + Update(Update.Update.Pncounter(PNCounterUpdate(change))) + + def update(value: Long): Effects = + Effects(stateAction = crdtUpdate(delta(value))) + + def delta(value: Long): CrdtDelta.Delta.Pncounter = deltaPNCounter(value) + + def test(run: String => Any): Unit = crdtTest("PNCounter")(run) + } + + object GSet { + def state(elements: String*): Response = + Response(Some(State(State.Value.Gset(GSetValue(elements))))) + + def add(elements: String*): Seq[RequestAction] = + elements.map(element => requestUpdate(updateWith(element))) + + def updateWith(element: String): Update = + Update(Update.Update.Gset(GSetUpdate(element))) + + def update(added: String*): Effects = + Effects(stateAction = crdtUpdate(delta(added: _*))) + + def delta(added: String*): CrdtDelta.Delta.Gset = deltaGSet(added.map(primitiveString)) + + def test(run: String => Any): Unit = crdtTest("GSet")(run) + } + + object ORSet { + def state(elements: String*): Response = + Response(Some(State(State.Value.Orset(ORSetValue(elements))))) + + def add(elements: String*): Seq[RequestAction] = + elements.map(element => action(ORSetUpdate.Action.Add(element))) + + def remove(elements: String*): Seq[RequestAction] = + elements.map(element => action(ORSetUpdate.Action.Remove(element))) + + def clear(value: Boolean = true): Seq[RequestAction] = + Seq(action(ORSetUpdate.Action.Clear(value))) + + def action(updateAction: ORSetUpdate.Action): RequestAction = + requestUpdate(Update(Update.Update.Orset(ORSetUpdate(updateAction)))) + + def update(cleared: Boolean = false, removed: Seq[String] = Seq.empty, added: Seq[String] = Seq.empty): Effects = + Effects(stateAction = crdtUpdate(delta(cleared, removed, added))) + + def delta(cleared: Boolean = false, + removed: Seq[String] = Seq.empty, + added: Seq[String] = Seq.empty): CrdtDelta.Delta.Orset = + CrdtDelta.Delta.Orset(ORSetDelta(cleared, removed.map(primitiveString), added.map(primitiveString))) + + def test(run: String => Any): Unit = crdtTest("ORSet")(run) + } + + object LWWRegister { + val DefaultClock: LWWRegisterClockType = LWWRegisterClockType.DEFAULT + val ReverseClock: LWWRegisterClockType = LWWRegisterClockType.REVERSE + val CustomClock: LWWRegisterClockType = LWWRegisterClockType.CUSTOM + val CustomAutoIncClock: LWWRegisterClockType = LWWRegisterClockType.CUSTOM_AUTO_INCREMENT + + def state(value: String): Response = + Response(Some(State(State.Value.Lwwregister(LWWRegisterValue(value))))) + + def setTo(value: String, + clockType: LWWRegisterClockType = DefaultClock, + customClockValue: Long = 0L): Seq[RequestAction] = + updateWith(LWWRegisterUpdate(value, Some(LWWRegisterClock(clockType, customClockValue)))) + + def updateWith(update: LWWRegisterUpdate): Seq[RequestAction] = + Seq(requestUpdate(Update(Update.Update.Lwwregister(update)))) + + def update(value: String, clock: CrdtClock = CrdtClock.DEFAULT, customClockValue: Long = 0L): Effects = + Effects(stateAction = crdtUpdate(delta(value, clock, customClockValue))) + + def delta(value: String, + clock: CrdtClock = CrdtClock.DEFAULT, + customClockValue: Long = 0L): CrdtDelta.Delta.Lwwregister = + deltaLWWRegister(Some(primitiveString(value)), clock, customClockValue) + + def test(run: String => Any): Unit = crdtTest("LWWRegister")(run) + } + + object Flag { + def state(value: Boolean): Response = + Response(Some(State(State.Value.Flag(FlagValue(value))))) + + def enable(): Seq[RequestAction] = + Seq(requestUpdate(Update(Update.Update.Flag(FlagUpdate())))) + + def update(value: Boolean): Effects = + Effects(stateAction = crdtUpdate(delta(value))) + + def delta(value: Boolean): CrdtDelta.Delta.Flag = deltaFlag(value) + + def test(run: String => Any): Unit = crdtTest("Flag")(run) + } + + object ORMap { + def state(entries: (String, Response)*): Response = + Response( + Some(State(State.Value.Ormap(ORMapValue(entries.map { + case (key, value) => ORMapEntryValue(key, value.state) + })))) + ) + + def add(keys: String*): Seq[RequestAction] = + keys.map(key => action(ORMapUpdate.Action.Add(key))) + + def updateWith(entries: (String, Seq[RequestAction])*): Seq[RequestAction] = + entries flatMap { + case (key, actions) => + actions map { a => + action(ORMapUpdate.Action.Update(ORMapEntryUpdate(key, Option(a.getUpdate)))) + } + } + + def remove(keys: String*): Seq[RequestAction] = + keys.map(key => action(ORMapUpdate.Action.Remove(key))) + + def clear(value: Boolean = true): Seq[RequestAction] = + Seq(action(ORMapUpdate.Action.Clear(value))) + + def action(updateAction: ORMapUpdate.Action): RequestAction = + requestUpdate(Update(Update.Update.Ormap(ORMapUpdate(updateAction)))) + + def update(cleared: Boolean = false, + removed: Seq[String] = Seq.empty, + updated: Seq[(String, Effects)] = Seq.empty, + added: Seq[(String, Effects)] = Seq.empty): Effects = + Effects( + stateAction = crdtUpdate( + delta( + cleared, + removed, + updated map { case (key, effects) => key -> effects.stateAction.get.getUpdate.delta }, + added map { case (key, effects) => key -> effects.stateAction.get.getUpdate.delta } + ) + ) + ) + + def delta(cleared: Boolean = false, + removed: Seq[String] = Seq.empty, + updated: Seq[(String, CrdtDelta.Delta)] = Seq.empty, + added: Seq[(String, CrdtDelta.Delta)] = Seq.empty): CrdtDelta.Delta.Ormap = + CrdtDelta.Delta.Ormap( + ORMapDelta( + cleared, + removed.map(primitiveString), + updated map { case (key, delta) => ORMapEntryDelta(Some(primitiveString(key)), Some(CrdtDelta(delta))) }, + added map { case (key, delta) => ORMapEntryDelta(Some(primitiveString(key)), Some(CrdtDelta(delta))) } + ) + ) + + def test(run: String => Any): Unit = crdtTest("ORMap")(run) + } + + object Vote { + def state(selfVote: Boolean, votesFor: Int, totalVoters: Int): Response = + Response(Some(State(State.Value.Vote(VoteValue(selfVote, votesFor, totalVoters))))) + + def self(vote: Boolean): Seq[RequestAction] = + Seq(requestUpdate(updateWith(vote))) + + def updateWith(vote: Boolean): Update = + Update(Update.Update.Vote(VoteUpdate(vote))) + + def update(selfVote: Boolean): Effects = + Effects(stateAction = crdtUpdate(delta(selfVote))) + + def delta(selfVote: Boolean, votesFor: Int = 0, totalVoters: Int = 0): CrdtDelta.Delta.Vote = + deltaVote(selfVote, votesFor, totalVoters) + + def test(run: String => Any): Unit = crdtTest("Vote")(run) + } + } + + def verifyCrdtEntityModel(): Unit = { + import CrdtEntityTCKModel._ + + "verify CRDT entity discovery" in testFor(CrdtTckModel, CrdtTwo) { + discoveredServices must (contain("CrdtTckModel") and contain("CrdtTwo")) + entity(CrdtEntityTCKModel.Service).value.entityType mustBe CrdtEntityTCKModel.Protocol + entity(CrdtEntityTCKModel.ServiceTwo).value.entityType mustBe CrdtEntityTCKModel.Protocol + } + + // GCounter tests + + "verify GCounter initial empty state" in GCounter.test { id => + protocol.crdt + .connect() + .send(init(CrdtTckModel.name, id)) + .send(command(1, id, "Process", Request(id))) + .expect(reply(1, GCounter.state(0))) + .passivate() + } + + "verify GCounter state changes" in GCounter.test { id => + protocol.crdt + .connect() + .send(init(CrdtTckModel.name, id)) + .send(command(1, id, "Process", Request(id, GCounter.incrementBy(42)))) + .expect(reply(1, GCounter.state(42), GCounter.update(42))) + .send(command(2, id, "Process", Request(id, GCounter.incrementBy(1, 2, 3)))) + .expect(reply(2, GCounter.state(48), GCounter.update(6))) + .passivate() + } + + "verify GCounter initial delta in init" in GCounter.test { id => + protocol.crdt + .connect() + .send(init(CrdtTckModel.name, id, GCounter.delta(42))) + .send(command(1, id, "Process", Request(id, GCounter.incrementBy(123)))) + .expect(reply(1, GCounter.state(165), GCounter.update(123))) + .passivate() + } + + "verify GCounter initial empty state with replicated initial delta" in GCounter.test { id => + protocol.crdt + .connect() + .send(init(CrdtTckModel.name, id)) + .send(delta(GCounter.delta(42))) + .send(command(1, id, "Process", Request(id, GCounter.incrementBy(123)))) + .expect(reply(1, GCounter.state(165), GCounter.update(123))) + .passivate() + } + + "verify GCounter mix of local and replicated state changes" in GCounter.test { id => + protocol.crdt + .connect() + .send(init(CrdtTckModel.name, id)) + .send(command(1, id, "Process", Request(id, GCounter.incrementBy(1)))) + .expect(reply(1, GCounter.state(1), GCounter.update(1))) + .send(delta(GCounter.delta(2))) + .send(delta(GCounter.delta(3))) + .send(command(2, id, "Process", Request(id))) + .expect(reply(2, GCounter.state(6))) + .send(command(3, id, "Process", Request(id, GCounter.incrementBy(4)))) + .expect(reply(3, GCounter.state(10), GCounter.update(4))) + .send(delta(GCounter.delta(5))) + .send(command(4, id, "Process", Request(id, GCounter.incrementBy(6)))) + .expect(reply(4, GCounter.state(21), GCounter.update(6))) + .passivate() + } + + "verify GCounter rehydration after passivation" in GCounter.test { id => + protocol.crdt + .connect() + .send(init(CrdtTckModel.name, id)) + .send(command(1, id, "Process", Request(id, GCounter.incrementBy(1)))) + .expect(reply(1, GCounter.state(1), GCounter.update(1))) + .send(delta(GCounter.delta(2))) + .send(command(2, id, "Process", Request(id))) + .expect(reply(2, GCounter.state(3))) + .send(command(3, id, "Process", Request(id, GCounter.incrementBy(3)))) + .expect(reply(3, GCounter.state(6), GCounter.update(3))) + .passivate() + protocol.crdt + .connect() + .send(init(CrdtTckModel.name, id, GCounter.delta(6))) + .send(command(1, id, "Process", Request(id))) + .expect(reply(1, GCounter.state(6))) + .send(delta(GCounter.delta(4))) + .send(command(2, id, "Process", Request(id, GCounter.incrementBy(5)))) + .expect(reply(2, GCounter.state(15), GCounter.update(5))) + .passivate() + } + + "verify GCounter delete action received from entity" in GCounter.test { id => + protocol.crdt + .connect() + .send(init(CrdtTckModel.name, id)) + .send(command(1, id, "Process", Request(id, GCounter.incrementBy(42)))) + .expect(reply(1, GCounter.state(42), GCounter.update(42))) + .send(command(2, id, "Process", Request(id, Seq(requestDelete)))) + .expect(reply(2, GCounter.state(42), deleteCrdt)) + .passivate() + } + + "verify GCounter delete action sent to entity" in GCounter.test { id => + protocol.crdt + .connect() + .send(init(CrdtTckModel.name, id)) + .send(delete) + .passivate() + } + + // PNCounter tests + + "verify PNCounter initial empty state" in PNCounter.test { id => + protocol.crdt + .connect() + .send(init(CrdtTckModel.name, id)) + .send(command(1, id, "Process", Request(id))) + .expect(reply(1, PNCounter.state(0))) + .passivate() + } + + "verify PNCounter state changes" in PNCounter.test { id => + protocol.crdt + .connect() + .send(init(CrdtTckModel.name, id)) + .send(command(1, id, "Process", Request(id, PNCounter.changeBy(+1, -2, +3)))) + .expect(reply(1, PNCounter.state(+2), PNCounter.update(+2))) + .send(command(2, id, "Process", Request(id, PNCounter.changeBy(-4, +5, -6)))) + .expect(reply(2, PNCounter.state(-3), PNCounter.update(-5))) + .passivate() + } + + "verify PNCounter initial delta in init" in PNCounter.test { id => + protocol.crdt + .connect() + .send(init(CrdtTckModel.name, id, PNCounter.delta(42))) + .send(command(1, id, "Process", Request(id, PNCounter.changeBy(-123)))) + .expect(reply(1, PNCounter.state(-81), PNCounter.update(-123))) + .passivate() + } + + "verify PNCounter initial empty state with replicated initial delta" in PNCounter.test { id => + protocol.crdt + .connect() + .send(init(CrdtTckModel.name, id)) + .send(delta(PNCounter.delta(-42))) + .send(command(1, id, "Process", Request(id, PNCounter.changeBy(+123)))) + .expect(reply(1, PNCounter.state(81), PNCounter.update(+123))) + .passivate() + } + + "verify PNCounter mix of local and replicated state changes" in PNCounter.test { id => + protocol.crdt + .connect() + .send(init(CrdtTckModel.name, id)) + .send(command(1, id, "Process", Request(id, PNCounter.changeBy(+1)))) + .expect(reply(1, PNCounter.state(+1), PNCounter.update(+1))) + .send(delta(PNCounter.delta(-2))) + .send(delta(PNCounter.delta(+3))) + .send(command(2, id, "Process", Request(id))) + .expect(reply(2, PNCounter.state(+2))) + .send(command(3, id, "Process", Request(id, PNCounter.changeBy(-4)))) + .expect(reply(3, PNCounter.state(-2), PNCounter.update(-4))) + .send(delta(PNCounter.delta(+5))) + .send(command(4, id, "Process", Request(id, PNCounter.changeBy(-6)))) + .expect(reply(4, PNCounter.state(-3), PNCounter.update(-6))) + .passivate() + } + + "verify PNCounter rehydration after passivation" in PNCounter.test { id => + protocol.crdt + .connect() + .send(init(CrdtTckModel.name, id)) + .send(command(1, id, "Process", Request(id, PNCounter.changeBy(+1)))) + .expect(reply(1, PNCounter.state(+1), PNCounter.update(+1))) + .send(delta(PNCounter.delta(-2))) + .send(command(2, id, "Process", Request(id))) + .expect(reply(2, PNCounter.state(-1))) + .send(command(3, id, "Process", Request(id, PNCounter.changeBy(+3)))) + .expect(reply(3, PNCounter.state(+2), PNCounter.update(+3))) + .passivate() + protocol.crdt + .connect() + .send(init(CrdtTckModel.name, id, PNCounter.delta(+2))) + .send(command(1, id, "Process", Request(id))) + .expect(reply(1, PNCounter.state(+2))) + .send(delta(PNCounter.delta(-4))) + .send(command(2, id, "Process", Request(id, PNCounter.changeBy(+5)))) + .expect(reply(2, PNCounter.state(+3), PNCounter.update(+5))) + .passivate() + } + + "verify PNCounter delete action received from entity" in PNCounter.test { id => + protocol.crdt + .connect() + .send(init(CrdtTckModel.name, id)) + .send(command(1, id, "Process", Request(id, PNCounter.changeBy(+42)))) + .expect(reply(1, PNCounter.state(+42), PNCounter.update(+42))) + .send(command(2, id, "Process", Request(id, Seq(requestDelete)))) + .expect(reply(2, PNCounter.state(+42), deleteCrdt)) + .passivate() + } + + "verify PNCounter delete action sent to entity" in PNCounter.test { id => + protocol.crdt + .connect() + .send(init(CrdtTckModel.name, id)) + .send(delete) + .passivate() + } + + // GSet tests + + "verify GSet initial empty state" in GSet.test { id => + protocol.crdt + .connect() + .send(init(CrdtTckModel.name, id)) + .send(command(1, id, "Process", Request(id))) + .expect(reply(1, GSet.state())) + .passivate() + } + + "verify GSet state changes" in GSet.test { id => + protocol.crdt + .connect() + .send(init(CrdtTckModel.name, id)) + .send(command(1, id, "Process", Request(id, GSet.add("a", "b")))) + .expect(reply(1, GSet.state("a", "b"), GSet.update("a", "b")), Delta.sorted) + .send(command(2, id, "Process", Request(id, GSet.add("b", "c")))) + .expect(reply(2, GSet.state("a", "b", "c"), GSet.update("c"))) + .send(command(3, id, "Process", Request(id, GSet.add("c", "a")))) + .expect(reply(3, GSet.state("a", "b", "c"))) + .passivate() + } + + "verify GSet initial delta in init" in GSet.test { id => + protocol.crdt + .connect() + .send(init(CrdtTckModel.name, id, GSet.delta("a", "b", "c"))) + .send(command(1, id, "Process", Request(id, GSet.add("c", "d", "e")))) + .expect(reply(1, GSet.state("a", "b", "c", "d", "e"), GSet.update("d", "e")), Delta.sorted) + .passivate() + } + + "verify GSet initial empty state with replicated initial delta" in GSet.test { id => + protocol.crdt + .connect() + .send(init(CrdtTckModel.name, id)) + .send(delta(GSet.delta("x", "y"))) + .send(command(1, id, "Process", Request(id, GSet.add("z")))) + .expect(reply(1, GSet.state("x", "y", "z"), GSet.update("z"))) + .passivate() + } + + "verify GSet mix of local and replicated state changes" in GSet.test { id => + protocol.crdt + .connect() + .send(init(CrdtTckModel.name, id)) + .send(command(1, id, "Process", Request(id, GSet.add("a")))) + .expect(reply(1, GSet.state("a"), GSet.update("a"))) + .send(delta(GSet.delta("a"))) + .send(delta(GSet.delta("b"))) + .send(delta(GSet.delta("c"))) + .send(command(2, id, "Process", Request(id))) + .expect(reply(2, GSet.state("a", "b", "c"))) + .send(command(3, id, "Process", Request(id, GSet.add("c", "d", "e")))) + .expect(reply(3, GSet.state("a", "b", "c", "d", "e"), GSet.update("d", "e")), Delta.sorted) + .send(delta(GSet.delta("f"))) + .send(command(4, id, "Process", Request(id, GSet.add("g", "d", "b")))) + .expect(reply(4, GSet.state("a", "b", "c", "d", "e", "f", "g"), GSet.update("g"))) + .passivate() + } + + "verify GSet rehydration after passivation" in GSet.test { id => + protocol.crdt + .connect() + .send(init(CrdtTckModel.name, id)) + .send(command(1, id, "Process", Request(id, GSet.add("a")))) + .expect(reply(1, GSet.state("a"), GSet.update("a"))) + .send(delta(GSet.delta("b"))) + .send(command(2, id, "Process", Request(id))) + .expect(reply(2, GSet.state("a", "b"))) + .send(command(3, id, "Process", Request(id, GSet.add("c")))) + .expect(reply(3, GSet.state("a", "b", "c"), GSet.update("c"))) + .passivate() + protocol.crdt + .connect() + .send(init(CrdtTckModel.name, id, GSet.delta("a", "b", "c"))) + .send(command(1, id, "Process", Request(id))) + .expect(reply(1, GSet.state("a", "b", "c"))) + .send(delta(GSet.delta("d"))) + .send(command(2, id, "Process", Request(id, GSet.add("e")))) + .expect(reply(2, GSet.state("a", "b", "c", "d", "e"), GSet.update("e"))) + .passivate() + } + + "verify GSet delete action received from entity" in GSet.test { id => + protocol.crdt + .connect() + .send(init(CrdtTckModel.name, id)) + .send(command(1, id, "Process", Request(id, GSet.add("x")))) + .expect(reply(1, GSet.state("x"), GSet.update("x"))) + .send(command(2, id, "Process", Request(id, Seq(requestDelete)))) + .expect(reply(2, GSet.state("x"), deleteCrdt)) + .passivate() + } + + "verify GSet delete action sent to entity" in GSet.test { id => + protocol.crdt + .connect() + .send(init(CrdtTckModel.name, id)) + .send(delete) + .passivate() + } + + // ORSet tests + + "verify ORSet initial empty state" in ORSet.test { id => + protocol.crdt + .connect() + .send(init(CrdtTckModel.name, id)) + .send(command(1, id, "Process", Request(id))) + .expect(reply(1, ORSet.state())) + .passivate() + } + + "verify ORSet state changes" in ORSet.test { id => + protocol.crdt + .connect() + .send(init(CrdtTckModel.name, id)) + .send(command(1, id, "Process", Request(id, ORSet.add("a", "b")))) + .expect(reply(1, ORSet.state("a", "b"), ORSet.update(added = Seq("a", "b"))), Delta.sorted) + .send(command(2, id, "Process", Request(id, ORSet.add("b", "c")))) + .expect(reply(2, ORSet.state("a", "b", "c"), ORSet.update(added = Seq("c")))) + .send(command(3, id, "Process", Request(id, ORSet.add("c", "a")))) + .expect(reply(3, ORSet.state("a", "b", "c"))) + .send(command(4, id, "Process", Request(id, ORSet.remove("b", "d")))) + .expect(reply(4, ORSet.state("a", "c"), ORSet.update(removed = Seq("b")))) + .send(command(5, id, "Process", Request(id, ORSet.remove("c", "d") ++ ORSet.add("b", "c")))) + .expect(reply(5, ORSet.state("a", "b", "c"), ORSet.update(added = Seq("b")))) + .send(command(6, id, "Process", Request(id, ORSet.clear()))) + .expect(reply(6, ORSet.state(), ORSet.update(cleared = true))) + .send(command(7, id, "Process", Request(id, ORSet.add("a") ++ ORSet.clear() ++ ORSet.add("x")))) + .expect(reply(7, ORSet.state("x"), ORSet.update(cleared = true, added = Seq("x")))) + .passivate() + } + + "verify ORSet initial delta in init" in ORSet.test { id => + protocol.crdt + .connect() + .send(init(CrdtTckModel.name, id, ORSet.delta(added = Seq("a", "b", "c")))) + .send(command(1, id, "Process", Request(id, ORSet.remove("c") ++ ORSet.add("d")))) + .expect(reply(1, ORSet.state("a", "b", "d"), ORSet.update(removed = Seq("c"), added = Seq("d")))) + .passivate() + } + + "verify ORSet initial empty state with replicated initial delta" in ORSet.test { id => + protocol.crdt + .connect() + .send(init(CrdtTckModel.name, id)) + .send(delta(ORSet.delta(added = Seq("x", "y")))) + .send(command(1, id, "Process", Request(id, ORSet.remove("x") ++ ORSet.add("z")))) + .expect(reply(1, ORSet.state("y", "z"), ORSet.update(removed = Seq("x"), added = Seq("z")))) + .passivate() + } + + "verify ORSet mix of local and replicated state changes" in ORSet.test { id => + protocol.crdt + .connect() + .send(init(CrdtTckModel.name, id)) + .send(command(1, id, "Process", Request(id, ORSet.add("a")))) + .expect(reply(1, ORSet.state("a"), ORSet.update(added = Seq("a")))) + .send(delta(ORSet.delta(added = Seq("a", "b")))) + .send(delta(ORSet.delta(added = Seq("c", "d")))) + .send(delta(ORSet.delta(removed = Seq("b", "d")))) + .send(command(2, id, "Process", Request(id))) + .expect(reply(2, ORSet.state("a", "c"))) + .send(command(3, id, "Process", Request(id, ORSet.add("c", "d", "e")))) + .expect(reply(3, ORSet.state("a", "c", "d", "e"), ORSet.update(added = Seq("d", "e"))), Delta.sorted) + .send(delta(ORSet.delta(removed = Seq("a", "c"), added = Seq("f")))) + .send(command(4, id, "Process", Request(id, ORSet.add("g") ++ ORSet.remove("a", "d")))) + .expect(reply(4, ORSet.state("e", "f", "g"), ORSet.update(removed = Seq("d"), added = Seq("g")))) + .send(delta(ORSet.delta(cleared = true, added = Seq("x", "y", "z")))) + .send(command(5, id, "Process", Request(id, ORSet.add("q", "x") ++ ORSet.remove("z")))) + .expect(reply(5, ORSet.state("q", "x", "y"), ORSet.update(removed = Seq("z"), added = Seq("q")))) + .send(delta(ORSet.delta(removed = Seq("x", "y"), added = Seq("r", "p")))) + .send(command(6, id, "Process", Request(id, ORSet.add("s")))) + .expect(reply(6, ORSet.state("p", "q", "r", "s"), ORSet.update(added = Seq("s")))) + .send(delta(ORSet.delta(added = Seq("a", "b", "c")))) + .send(command(7, id, "Process", Request(id, ORSet.clear() ++ ORSet.add("abc")))) + .expect(reply(7, ORSet.state("abc"), ORSet.update(cleared = true, added = Seq("abc")))) + .passivate() + } + + "verify ORSet rehydration after passivation" in ORSet.test { id => + protocol.crdt + .connect() + .send(init(CrdtTckModel.name, id)) + .send(command(1, id, "Process", Request(id, ORSet.add("a")))) + .expect(reply(1, ORSet.state("a"), ORSet.update(added = Seq("a")))) + .send(delta(ORSet.delta(removed = Seq("a"), added = Seq("b")))) + .send(command(2, id, "Process", Request(id))) + .expect(reply(2, ORSet.state("b"))) + .send(command(3, id, "Process", Request(id, ORSet.add("c")))) + .expect(reply(3, ORSet.state("b", "c"), ORSet.update(added = Seq("c")))) + .passivate() + protocol.crdt + .connect() + .send(init(CrdtTckModel.name, id, ORSet.delta(added = Seq("b", "c")))) + .send(command(1, id, "Process", Request(id))) + .expect(reply(1, ORSet.state("b", "c"))) + .send(delta(ORSet.delta(cleared = true, added = Seq("p", "q")))) + .send(command(2, id, "Process", Request(id, ORSet.add("x", "y") ++ ORSet.remove("x")))) + .expect(reply(2, ORSet.state("p", "q", "y"), ORSet.update(added = Seq("y")))) + .passivate() + } + + "verify ORSet delete action received from entity" in ORSet.test { id => + protocol.crdt + .connect() + .send(init(CrdtTckModel.name, id)) + .send(command(1, id, "Process", Request(id, ORSet.add("x")))) + .expect(reply(1, ORSet.state("x"), ORSet.update(added = Seq("x")))) + .send(command(2, id, "Process", Request(id, Seq(requestDelete)))) + .expect(reply(2, ORSet.state("x"), deleteCrdt)) + .passivate() + } + + "verify ORSet delete action sent to entity" in ORSet.test { id => + protocol.crdt + .connect() + .send(init(CrdtTckModel.name, id)) + .send(delete) + .passivate() + } + + // LWWRegister tests + + "verify LWWRegister initial empty state" in LWWRegister.test { id => + protocol.crdt + .connect() + .send(init(CrdtTckModel.name, id)) + .send(command(1, id, "Process", Request(id))) + .expect(reply(1, LWWRegister.state(""), LWWRegister.update(""))) + .passivate() + } + + "verify LWWRegister state changes" in LWWRegister.test { id => + protocol.crdt + .connect() + .send(init(CrdtTckModel.name, id)) + .send(command(1, id, "Process", Request(id, LWWRegister.setTo("one")))) + .expect(reply(1, LWWRegister.state("one"), LWWRegister.update("one"))) + .send(command(2, id, "Process", Request(id, LWWRegister.setTo("two")))) + .expect(reply(2, LWWRegister.state("two"), LWWRegister.update("two"))) + .passivate() + } + + "verify LWWRegister initial delta in init" in LWWRegister.test { id => + protocol.crdt + .connect() + .send(init(CrdtTckModel.name, id, LWWRegister.delta("one"))) + .send(command(1, id, "Process", Request(id))) + .expect(reply(1, LWWRegister.state("one"))) + .passivate() + } + + "verify LWWRegister initial empty state with replicated initial delta" in LWWRegister.test { id => + protocol.crdt + .connect() + .send(init(CrdtTckModel.name, id)) + .send(delta(LWWRegister.delta("one"))) + .send(command(1, id, "Process", Request(id))) + .expect(reply(1, LWWRegister.state("one"))) + .passivate() + } + + "verify LWWRegister mix of local and replicated state changes" in LWWRegister.test { id => + protocol.crdt + .connect() + .send(init(CrdtTckModel.name, id)) + .send(command(1, id, "Process", Request(id, LWWRegister.setTo("A")))) + .expect(reply(1, LWWRegister.state("A"), LWWRegister.update("A"))) + .send(delta(LWWRegister.delta("B"))) + .send(delta(LWWRegister.delta("C"))) + .send(command(2, id, "Process", Request(id))) + .expect(reply(2, LWWRegister.state("C"))) + .send(command(3, id, "Process", Request(id, LWWRegister.setTo("D")))) + .expect(reply(3, LWWRegister.state("D"), LWWRegister.update("D"))) + .send(delta(LWWRegister.delta("D"))) + .send(command(4, id, "Process", Request(id, LWWRegister.setTo("E")))) + .expect(reply(4, LWWRegister.state("E"), LWWRegister.update("E"))) + .passivate() + } + + "verify LWWRegister state changes with clock settings" in LWWRegister.test { id => + protocol.crdt + .connect() + .send(init(CrdtTckModel.name, id)) + .send(command(1, id, "Process", Request(id, LWWRegister.setTo("A", LWWRegister.ReverseClock)))) + .expect(reply(1, LWWRegister.state("A"), LWWRegister.update("A", CrdtClock.REVERSE))) + .send(command(2, id, "Process", Request(id, LWWRegister.setTo("A", LWWRegister.CustomClock, 123L)))) + .expect(reply(2, LWWRegister.state("A"), LWWRegister.update("A", CrdtClock.CUSTOM, 123L))) + .send(command(2, id, "Process", Request(id, LWWRegister.setTo("A", LWWRegister.CustomAutoIncClock, 456L)))) + .expect(reply(2, LWWRegister.state("A"), LWWRegister.update("A", CrdtClock.CUSTOM_AUTO_INCREMENT, 456L))) + .passivate() + } + + "verify LWWRegister rehydration after passivation" in LWWRegister.test { id => + protocol.crdt + .connect() + .send(init(CrdtTckModel.name, id)) + .send(command(1, id, "Process", Request(id, LWWRegister.setTo("A")))) + .expect(reply(1, LWWRegister.state("A"), LWWRegister.update("A"))) + .send(delta(LWWRegister.delta("B"))) + .send(command(2, id, "Process", Request(id))) + .expect(reply(2, LWWRegister.state("B"))) + .send(command(3, id, "Process", Request(id, LWWRegister.setTo("C")))) + .expect(reply(3, LWWRegister.state("C"), LWWRegister.update("C"))) + .passivate() + protocol.crdt + .connect() + .send(init(CrdtTckModel.name, id, LWWRegister.delta("C"))) + .send(command(1, id, "Process", Request(id))) + .expect(reply(1, LWWRegister.state("C"))) + .send(delta(LWWRegister.delta("D"))) + .send(command(2, id, "Process", Request(id, LWWRegister.setTo("E")))) + .expect(reply(2, LWWRegister.state("E"), LWWRegister.update("E"))) + .passivate() + } + + "verify LWWRegister delete action received from entity" in LWWRegister.test { id => + protocol.crdt + .connect() + .send(init(CrdtTckModel.name, id)) + .send(command(1, id, "Process", Request(id, LWWRegister.setTo("one")))) + .expect(reply(1, LWWRegister.state("one"), LWWRegister.update("one"))) + .send(command(2, id, "Process", Request(id, Seq(requestDelete)))) + .expect(reply(2, LWWRegister.state("one"), deleteCrdt)) + .passivate() + } + + "verify LWWRegister delete action sent to entity" in LWWRegister.test { id => + protocol.crdt + .connect() + .send(init(CrdtTckModel.name, id)) + .send(delete) + .passivate() + } + + // Flag tests + + "verify Flag initial empty state" in Flag.test { id => + protocol.crdt + .connect() + .send(init(CrdtTckModel.name, id)) + .send(command(1, id, "Process", Request(id))) + .expect(reply(1, Flag.state(false))) + .passivate() + } + + "verify Flag state changes" in Flag.test { id => + protocol.crdt + .connect() + .send(init(CrdtTckModel.name, id)) + .send(command(1, id, "Process", Request(id, Flag.enable()))) + .expect(reply(1, Flag.state(true), Flag.update(true))) + .passivate() + } + + "verify Flag initial delta in init" in Flag.test { id => + protocol.crdt + .connect() + .send(init(CrdtTckModel.name, id, Flag.delta(false))) + .send(command(1, id, "Process", Request(id))) + .expect(reply(1, Flag.state(false))) + .passivate() + protocol.crdt + .connect() + .send(init(CrdtTckModel.name, id, Flag.delta(true))) + .send(command(1, id, "Process", Request(id))) + .expect(reply(1, Flag.state(true))) + .passivate() + } + + "verify Flag initial empty state with replicated initial delta" in Flag.test { id => + protocol.crdt + .connect() + .send(init(CrdtTckModel.name, id)) + .send(delta(Flag.delta(false))) + .send(command(1, id, "Process", Request(id))) + .expect(reply(1, Flag.state(false))) + .passivate() + protocol.crdt + .connect() + .send(init(CrdtTckModel.name, id)) + .send(delta(Flag.delta(true))) + .send(command(1, id, "Process", Request(id))) + .expect(reply(1, Flag.state(true))) + .passivate() + } + + "verify Flag mix of local and replicated state changes" in Flag.test { id => + protocol.crdt + .connect() + .send(init(CrdtTckModel.name, id)) + .send(delta(Flag.delta(false))) + .send(command(1, id, "Process", Request(id))) + .expect(reply(1, Flag.state(false))) + .send(command(2, id, "Process", Request(id, Flag.enable()))) + .expect(reply(2, Flag.state(true), Flag.update(true))) + .send(delta(Flag.delta(true))) + .send(delta(Flag.delta(false))) + .send(command(3, id, "Process", Request(id))) + .expect(reply(3, Flag.state(true))) + .passivate() + protocol.crdt + .connect() + .send(init(CrdtTckModel.name, id)) + .send(delta(Flag.delta(true))) + .send(command(1, id, "Process", Request(id))) + .expect(reply(1, Flag.state(true))) + .send(command(2, id, "Process", Request(id, Flag.enable()))) + .expect(reply(2, Flag.state(true))) + .send(delta(Flag.delta(true))) + .send(delta(Flag.delta(false))) + .send(command(3, id, "Process", Request(id))) + .expect(reply(3, Flag.state(true))) + .passivate() + } + + "verify Flag rehydration after passivation" in Flag.test { id => + protocol.crdt + .connect() + .send(init(CrdtTckModel.name, id)) + .send(command(1, id, "Process", Request(id))) + .expect(reply(1, Flag.state(false))) + .send(command(2, id, "Process", Request(id, Flag.enable()))) + .expect(reply(2, Flag.state(true), Flag.update(true))) + .passivate() + protocol.crdt + .connect() + .send(init(CrdtTckModel.name, id, Flag.delta(true))) + .send(command(1, id, "Process", Request(id))) + .expect(reply(1, Flag.state(true))) + .passivate() + } + + "verify Flag delete action received from entity" in Flag.test { id => + protocol.crdt + .connect() + .send(init(CrdtTckModel.name, id)) + .send(command(1, id, "Process", Request(id, Flag.enable()))) + .expect(reply(1, Flag.state(true), Flag.update(true))) + .send(command(2, id, "Process", Request(id, Seq(requestDelete)))) + .expect(reply(2, Flag.state(true), deleteCrdt)) + .passivate() + } + + "verify Flag delete action sent to entity" in Flag.test { id => + protocol.crdt + .connect() + .send(init(CrdtTckModel.name, id)) + .send(delete) + .passivate() + } + + // ORMap tests + + "verify ORMap initial empty state" in ORMap.test { id => + protocol.crdt + .connect() + .send(init(CrdtTckModel.name, id)) + .send(command(1, id, "Process", Request(id))) + .expect(reply(1, ORMap.state())) + .passivate() + } + + "verify ORMap state changes" in ORMap.test { id => + protocol.crdt + .connect() + .send(init(CrdtTckModel.name, id)) + .send(command(1, id, "Process", Request(id, ORMap.add("PNCounter-1", "ORSet-1")))) + .expect( + reply( + 1, + ORMap.state( + "ORSet-1" -> ORSet.state(), + "PNCounter-1" -> PNCounter.state(0) + ), + ORMap.update( + added = Seq( + "ORSet-1" -> ORSet.update(), + "PNCounter-1" -> PNCounter.update(0) + ) + ) + ), + Delta.sorted + ) + .send( + command(2, + id, + "Process", + Request(id, + ORMap.add("ORSet-1") ++ ORMap.updateWith( + "LWWRegister-1" -> LWWRegister.setTo("zero") + ))) + ) + .expect( + reply( + 2, + ORMap.state( + "LWWRegister-1" -> LWWRegister.state("zero"), + "ORSet-1" -> ORSet.state(), + "PNCounter-1" -> PNCounter.state(0) + ), + ORMap.update( + added = Seq( + "LWWRegister-1" -> LWWRegister.update("zero") + ) + ) + ), + Delta.sorted + ) + .send( + command(3, + id, + "Process", + Request(id, + ORMap.updateWith( + "PNCounter-1" -> PNCounter.changeBy(+42), + "LWWRegister-1" -> LWWRegister.setTo("one") + ))) + ) + .expect( + reply( + 3, + ORMap.state( + "LWWRegister-1" -> LWWRegister.state("one"), + "ORSet-1" -> ORSet.state(), + "PNCounter-1" -> PNCounter.state(+42) + ), + ORMap.update( + updated = Seq( + "LWWRegister-1" -> LWWRegister.update("one"), + "PNCounter-1" -> PNCounter.update(+42) + ) + ) + ), + Delta.sorted + ) + .send(command(4, id, "Process", Request(id, ORMap.updateWith("ORSet-1" -> ORSet.add("a", "b", "c"))))) + .expect( + reply( + 4, + ORMap.state( + "LWWRegister-1" -> LWWRegister.state("one"), + "ORSet-1" -> ORSet.state("a", "b", "c"), + "PNCounter-1" -> PNCounter.state(+42) + ), + ORMap.update( + updated = Seq( + "ORSet-1" -> ORSet.update(added = Seq("a", "b", "c")) + ) + ) + ), + Delta.sorted + ) + .send( + command( + 5, + id, + "Process", + Request(id, + ORMap.updateWith( + "ORSet-1" -> (ORSet.remove("b") ++ ORSet.add("d")), + "PNCounter-1" -> PNCounter.changeBy(-123), + "LWWRegister-1" -> LWWRegister.setTo("two") + )) + ) + ) + .expect( + reply( + 5, + ORMap.state( + "LWWRegister-1" -> LWWRegister.state("two"), + "ORSet-1" -> ORSet.state("a", "c", "d"), + "PNCounter-1" -> PNCounter.state(-81) + ), + ORMap.update( + updated = Seq( + "LWWRegister-1" -> LWWRegister.update("two"), + "ORSet-1" -> ORSet.update(removed = Seq("b"), added = Seq("d")), + "PNCounter-1" -> PNCounter.update(-123) + ) + ) + ), + Delta.sorted + ) + .send(command(6, id, "Process", Request(id, ORMap.remove("ORSet-1")))) + .expect( + reply( + 6, + ORMap.state( + "LWWRegister-1" -> LWWRegister.state("two"), + "PNCounter-1" -> PNCounter.state(-81) + ), + ORMap.update(removed = Seq("ORSet-1")) + ), + Delta.sorted + ) + .send( + command(7, + id, + "Process", + Request(id, ORMap.remove("LWWRegister-1") ++ ORMap.updateWith("Flag-1" -> Flag.enable()))) + ) + .expect( + reply( + 7, + ORMap.state( + "Flag-1" -> Flag.state(true), + "PNCounter-1" -> PNCounter.state(-81) + ), + ORMap.update(removed = Seq("LWWRegister-1"), added = Seq("Flag-1" -> Flag.update(true))) + ), + Delta.sorted + ) + .send(command(8, id, "Process", Request(id, ORMap.clear()))) + .expect(reply(8, ORMap.state(), ORMap.update(cleared = true))) + .send( + command( + 9, + id, + "Process", + Request(id, + ORMap.add("PNCounter-2") ++ ORMap.clear() ++ ORMap.updateWith( + "GCounter-1" -> GCounter.incrementBy(123) + )) + ) + ) + .expect( + reply(9, + ORMap.state("GCounter-1" -> GCounter.state(123)), + ORMap.update(cleared = true, added = Seq("GCounter-1" -> GCounter.update(123)))), + Delta.sorted + ) + .passivate() + } + + "verify ORMap initial delta in init" in ORMap.test { id => + protocol.crdt + .connect() + .send( + init( + CrdtTckModel.name, + id, + ORMap.delta( + added = Seq("PNCounter-1" -> PNCounter.delta(+123), + "LWWRegister-1" -> LWWRegister.delta("one"), + "GSet-1" -> GSet.delta("a", "b", "c")) + ) + ) + ) + .send(command(1, id, "Process", Request(id))) + .expect( + reply(1, + ORMap.state("GSet-1" -> GSet.state("a", "b", "c"), + "LWWRegister-1" -> LWWRegister.state("one"), + "PNCounter-1" -> PNCounter.state(+123))) + ) + .send( + command( + 2, + id, + "Process", + Request(id, ORMap.remove("LWWRegister-1") ++ ORMap.updateWith("PNCounter-1" -> PNCounter.changeBy(-42))) + ) + ) + .expect( + reply( + 2, + ORMap.state("GSet-1" -> GSet.state("a", "b", "c"), "PNCounter-1" -> PNCounter.state(+81)), + ORMap.update(removed = Seq("LWWRegister-1"), updated = Seq("PNCounter-1" -> PNCounter.update(-42))) + ), + Delta.sorted + ) + .passivate() + } + + "verify ORMap initial empty state with replicated initial delta" in ORMap.test { id => + protocol.crdt + .connect() + .send(init(CrdtTckModel.name, id)) + .send( + delta( + ORMap.delta( + added = Seq("GCounter-1" -> GCounter.delta(42), + "ORSet-1" -> ORSet.delta(added = Seq("a", "b", "c")), + "Flag-1" -> Flag.delta(false)) + ) + ) + ) + .send(command(1, id, "Process", Request(id))) + .expect( + reply(1, + ORMap.state("Flag-1" -> Flag.state(false), + "GCounter-1" -> GCounter.state(42), + "ORSet-1" -> ORSet.state("a", "b", "c"))) + ) + .passivate() + } + + "verify ORMap mix of local and replicated state changes" in ORMap.test { id => + protocol.crdt + .connect() + .send(init(CrdtTckModel.name, id)) + .send(command(1, id, "Process", Request(id, ORMap.add("PNCounter-1")))) + .expect( + reply( + 1, + ORMap.state( + "PNCounter-1" -> PNCounter.state(0) + ), + ORMap.update( + added = Seq( + "PNCounter-1" -> PNCounter.update(0) + ) + ) + ), + Delta.sorted + ) + .send(delta(ORMap.delta(added = Seq("ORSet-1" -> ORSet.delta(added = Seq("x")))))) + .send( + delta( + ORMap.delta(added = Seq("LWWRegister-1" -> LWWRegister.delta("one")), + updated = Seq("ORSet-1" -> ORSet.delta(added = Seq("y", "z")))) + ) + ) + .send( + command(2, + id, + "Process", + Request(id, + ORMap.add("ORSet-2") ++ ORMap.updateWith( + "LWWRegister-2" -> LWWRegister.setTo("two") + ))) + ) + .expect( + reply( + 2, + ORMap.state( + "LWWRegister-1" -> LWWRegister.state("one"), + "LWWRegister-2" -> LWWRegister.state("two"), + "ORSet-1" -> ORSet.state("x", "y", "z"), + "ORSet-2" -> ORSet.state(), + "PNCounter-1" -> PNCounter.state(0) + ), + ORMap.update( + added = Seq( + "LWWRegister-2" -> LWWRegister.update("two"), + "ORSet-2" -> ORSet.update() + ) + ) + ), + Delta.sorted + ) + .send( + command(3, + id, + "Process", + Request(id, + ORMap.updateWith( + "PNCounter-1" -> PNCounter.changeBy(+42), + "LWWRegister-1" -> LWWRegister.setTo("ONE") + ))) + ) + .expect( + reply( + 3, + ORMap.state( + "LWWRegister-1" -> LWWRegister.state("ONE"), + "LWWRegister-2" -> LWWRegister.state("two"), + "ORSet-1" -> ORSet.state("x", "y", "z"), + "ORSet-2" -> ORSet.state(), + "PNCounter-1" -> PNCounter.state(+42) + ), + ORMap.update( + updated = Seq( + "LWWRegister-1" -> LWWRegister.update("ONE"), + "PNCounter-1" -> PNCounter.update(+42) + ) + ) + ), + Delta.sorted + ) + .send( + delta( + ORMap.delta(removed = Seq("LWWRegister-2"), + added = Seq("ORMap-1" -> ORMap.delta(added = Seq("PNCounter-1" -> PNCounter.delta(+123))))) + ) + ) + .send( + delta( + ORMap.delta( + removed = Seq("ORSet-1"), + updated = Seq("ORSet-2" -> ORSet.delta(added = Seq("a", "b", "c")), + "ORMap-1" -> ORMap.delta(added = Seq("LWWRegister-1" -> LWWRegister.delta("ABC")))) + ) + ) + ) + .send( + command(4, + id, + "Process", + Request(id, + ORMap.updateWith("ORMap-1" -> ORMap.updateWith("PNCounter-1" -> PNCounter.changeBy(-456))))) + ) + .expect( + reply( + 4, + ORMap.state( + "LWWRegister-1" -> LWWRegister.state("ONE"), + "ORMap-1" -> ORMap.state("LWWRegister-1" -> LWWRegister.state("ABC"), + "PNCounter-1" -> PNCounter.state(-333)), + "ORSet-2" -> ORSet.state("a", "b", "c"), + "PNCounter-1" -> PNCounter.state(+42) + ), + ORMap.update( + updated = Seq( + "ORMap-1" -> ORMap.update(updated = Seq("PNCounter-1" -> PNCounter.update(-456))) + ) + ) + ), + Delta.sorted + ) + .send(delta(ORMap.delta(cleared = true, added = Seq("GSet-1" -> GSet.delta("1", "2", "3"))))) + .send( + command( + 5, + id, + "Process", + Request(id, + ORMap.remove("PNCounter-1", "LWWRegister-1") ++ + ORMap.updateWith("GSet-1" -> GSet.add("2", "4", "6"))) + ) + ) + .expect( + reply( + 5, + ORMap.state( + "GSet-1" -> GSet.state("1", "2", "3", "4", "6") + ), + ORMap.update( + updated = Seq( + "GSet-1" -> GSet.update("4", "6") + ) + ) + ), + Delta.sorted + ) + .send(command(6, id, "Process", Request(id, ORMap.clear() ++ ORMap.add("GCounter-1")))) + .expect( + reply(6, + ORMap.state("GCounter-1" -> GCounter.state(0)), + ORMap.update(cleared = true, added = Seq("GCounter-1" -> GCounter.update(0)))) + ) + .send(delta(ORMap.delta(removed = Seq("GSet-1"), added = Seq("GCounter-1" -> GCounter.delta(42))))) + .send(command(7, id, "Process", Request(id, ORMap.updateWith("GCounter-1" -> GCounter.incrementBy(7))))) + .expect( + reply(7, + ORMap.state("GCounter-1" -> GCounter.state(49)), + ORMap.update(updated = Seq("GCounter-1" -> GCounter.update(7)))) + ) + .passivate() + } + "verify ORMap rehydration after passivation" in ORMap.test { id => + protocol.crdt + .connect() + .send(init(CrdtTckModel.name, id)) + .send(delta(ORMap.delta(added = Seq("PNCounter-1" -> PNCounter.delta(+123))))) + .send(command(1, id, "Process", Request(id, ORMap.updateWith("PNCounter-1" -> PNCounter.changeBy(+321))))) + .expect( + reply(1, + ORMap.state("PNCounter-1" -> PNCounter.state(+444)), + ORMap.update(updated = Seq("PNCounter-1" -> PNCounter.update(+321)))) + ) + .passivate() + protocol.crdt + .connect() + .send(init(CrdtTckModel.name, id, ORMap.delta(added = Seq("PNCounter-1" -> PNCounter.delta(+444))))) + .send(command(1, id, "Process", Request(id))) + .expect(reply(1, ORMap.state("PNCounter-1" -> PNCounter.state(+444)))) + .send(delta(ORMap.delta(updated = Seq("PNCounter-1" -> PNCounter.delta(-42))))) + .send(command(2, id, "Process", Request(id, ORMap.updateWith("PNCounter-1" -> PNCounter.changeBy(-360))))) + .expect( + reply(2, + ORMap.state("PNCounter-1" -> PNCounter.state(+42)), + ORMap.update(updated = Seq("PNCounter-1" -> PNCounter.update(-360)))) + ) + .passivate() + } + + "verify ORMap delete action received from entity" in ORMap.test { id => + protocol.crdt + .connect() + .send(init(CrdtTckModel.name, id)) + .send(command(1, id, "Process", Request(id, ORMap.add("Flag-1")))) + .expect( + reply(1, + ORMap.state("Flag-1" -> Flag.state(false)), + ORMap.update(added = Seq("Flag-1" -> Flag.update(false)))) + ) + .send(command(2, id, "Process", Request(id, ORMap.updateWith("Flag-1" -> Flag.enable()) :+ requestDelete))) + .expect(reply(2, ORMap.state("Flag-1" -> Flag.state(true)), deleteCrdt)) + .passivate() + } + + "verify ORMap delete action sent to entity" in ORMap.test { id => + protocol.crdt + .connect() + .send(init(CrdtTckModel.name, id)) + .send(delete) + .passivate() + } + + // Vote tests + + "verify Vote initial empty state" in Vote.test { id => + protocol.crdt + .connect() + .send(init(CrdtTckModel.name, id)) + .send(command(1, id, "Process", Request(id))) + .expect(reply(1, Vote.state(selfVote = false, votesFor = 0, totalVoters = 1))) + .passivate() + } + + "verify Vote state changes" in Vote.test { id => + protocol.crdt + .connect() + .send(init(CrdtTckModel.name, id)) + .send(command(1, id, "Process", Request(id, Vote.self(true)))) + .expect(reply(1, Vote.state(selfVote = true, votesFor = 1, totalVoters = 1), Vote.update(true))) + .send(command(2, id, "Process", Request(id, Vote.self(true)))) + .expect(reply(2, Vote.state(selfVote = true, votesFor = 1, totalVoters = 1))) + .send(command(3, id, "Process", Request(id, Vote.self(false)))) + .expect(reply(3, Vote.state(selfVote = false, votesFor = 0, totalVoters = 1), Vote.update(false))) + .send(command(4, id, "Process", Request(id, Vote.self(false)))) + .expect(reply(4, Vote.state(selfVote = false, votesFor = 0, totalVoters = 1))) + .passivate() + } + + "verify Vote initial delta in init" in Vote.test { id => + protocol.crdt + .connect() + .send(init(CrdtTckModel.name, id, Vote.delta(selfVote = true, votesFor = 2, totalVoters = 3))) + .send(command(1, id, "Process", Request(id))) + .expect(reply(1, Vote.state(selfVote = true, votesFor = 2, totalVoters = 3))) + .send(command(2, id, "Process", Request(id, Vote.self(false)))) + .expect(reply(2, Vote.state(selfVote = false, votesFor = 1, totalVoters = 3), Vote.update(false))) + .passivate() + } + + "verify Vote initial empty state with replicated initial delta" in Vote.test { id => + protocol.crdt + .connect() + .send(init(CrdtTckModel.name, id)) + .send(delta(Vote.delta(selfVote = false, votesFor = 3, totalVoters = 5))) + .send(command(1, id, "Process", Request(id))) + .expect(reply(1, Vote.state(selfVote = false, votesFor = 3, totalVoters = 5))) + .send(command(2, id, "Process", Request(id, Vote.self(true)))) + .expect(reply(2, Vote.state(selfVote = true, votesFor = 4, totalVoters = 5), Vote.update(true))) + .passivate() + } + + "verify Vote mix of local and replicated state changes" in Vote.test { id => + protocol.crdt + .connect() + .send(init(CrdtTckModel.name, id)) + .send(delta(Vote.delta(selfVote = false, votesFor = 2, totalVoters = 4))) + .send(command(1, id, "Process", Request(id))) + .expect(reply(1, Vote.state(selfVote = false, votesFor = 2, totalVoters = 4))) + .send(command(2, id, "Process", Request(id, Vote.self(true)))) + .expect(reply(2, Vote.state(selfVote = true, votesFor = 3, totalVoters = 4), Vote.update(true))) + .send(delta(Vote.delta(selfVote = true, votesFor = 5, totalVoters = 7))) + .send(delta(Vote.delta(selfVote = true, votesFor = 4, totalVoters = 6))) + .send(command(3, id, "Process", Request(id, Vote.self(false)))) + .expect(reply(3, Vote.state(selfVote = false, votesFor = 3, totalVoters = 6), Vote.update(false))) + .passivate() + } + + "verify Vote rehydration after passivation" in Vote.test { id => + protocol.crdt + .connect() + .send(init(CrdtTckModel.name, id)) + .send(command(1, id, "Process", Request(id))) + .expect(reply(1, Vote.state(selfVote = false, votesFor = 0, totalVoters = 1))) + .send(delta(Vote.delta(selfVote = false, votesFor = 2, totalVoters = 5))) + .send(command(2, id, "Process", Request(id, Vote.self(true)))) + .expect(reply(2, Vote.state(selfVote = true, votesFor = 3, totalVoters = 5), Vote.update(true))) + .passivate() + protocol.crdt + .connect() + .send(init(CrdtTckModel.name, id, Vote.delta(selfVote = true, votesFor = 3, totalVoters = 5))) + .send(command(1, id, "Process", Request(id))) + .expect(reply(1, Vote.state(selfVote = true, votesFor = 3, totalVoters = 5))) + .send(command(2, id, "Process", Request(id, Vote.self(false)))) + .expect(reply(2, Vote.state(selfVote = false, votesFor = 2, totalVoters = 5), Vote.update(false))) + .passivate() + } + + "verify Vote delete action received from entity" in Vote.test { id => + protocol.crdt + .connect() + .send(init(CrdtTckModel.name, id)) + .send(command(1, id, "Process", Request(id, Vote.self(true)))) + .expect(reply(1, Vote.state(selfVote = true, votesFor = 1, totalVoters = 1), Vote.update(true))) + .send(command(2, id, "Process", Request(id, Seq(requestDelete)))) + .expect(reply(2, Vote.state(selfVote = true, votesFor = 1, totalVoters = 1), deleteCrdt)) + .passivate() + } + + "verify Vote delete action sent to entity" in Vote.test { id => + protocol.crdt + .connect() + .send(init(CrdtTckModel.name, id)) + .send(delete) + .passivate() + } + + // forward and side effect tests + + "verify forward to second service" in GCounter.test { id => + protocol.crdt + .connect() + .send(init(CrdtTckModel.name, id)) + .send(command(1, id, "Process", Request(id, Seq(forwardTo(s"X-$id"))))) + .expect(forwarded(1, s"X-$id")) + .passivate() + } + + "verify forward with state changes" in PNCounter.test { id => + protocol.crdt + .connect() + .send(init(CrdtTckModel.name, id)) + .send(command(1, id, "Process", Request(id, PNCounter.changeBy(-42) :+ forwardTo(s"X-$id")))) + .expect(forwarded(1, s"X-$id", PNCounter.update(-42))) + .passivate() + } + + "verify reply with side effect to second service" in GSet.test { id => + protocol.crdt + .connect() + .send(init(CrdtTckModel.name, id)) + .send(command(1, id, "Process", Request(id, Seq(sideEffectTo(s"X-$id"))))) + .expect(reply(1, GSet.state(), sideEffects(s"X-$id"))) + .passivate() + } + + "verify synchronous side effect to second service" in ORSet.test { id => + protocol.crdt + .connect() + .send(init(CrdtTckModel.name, id)) + .send(command(1, id, "Process", Request(id, Seq(sideEffectTo(s"X-$id", synchronous = true))))) + .expect(reply(1, ORSet.state(), synchronousSideEffects(s"X-$id"))) + .passivate() + } + + "verify forward and side effect to second service" in LWWRegister.test { id => + protocol.crdt + .connect() + .send(init(CrdtTckModel.name, id)) + .send(command(1, id, "Process", Request(id, Seq(sideEffectTo(s"B-$id"), forwardTo(s"A-$id"))))) + .expect(forwarded(1, s"A-$id", LWWRegister.update("") ++ sideEffects(s"B-$id"))) + .passivate() + } + + "verify reply with multiple side effects" in Flag.test { id => + protocol.crdt + .connect() + .send(init(CrdtTckModel.name, id)) + .send(command(1, id, "Process", Request(id, sideEffectsTo("1", "2", "3")))) + .expect(reply(1, Flag.state(false), sideEffects("1", "2", "3"))) + .passivate() + } + + "verify reply with multiple side effects and state changes" in ORMap.test { id => + val crdtActions = ORMap.add("PNCounter-1") ++ ORMap.updateWith("GSet-1" -> GSet.add("a", "b", "c")) + val actions = crdtActions ++ sideEffectsTo("1", "2", "3") + val expectedState = ORMap.state("GSet-1" -> GSet.state("a", "b", "c"), "PNCounter-1" -> PNCounter.state(0)) + val crdtUpdates = ORMap.update( + added = Seq( + "GSet-1" -> GSet.update("a", "b", "c"), + "PNCounter-1" -> PNCounter.update(0) + ) + ) + val effects = crdtUpdates ++ sideEffects("1", "2", "3") + protocol.crdt + .connect() + .send(init(CrdtTckModel.name, id)) + .send(command(1, id, "Process", Request(id, actions))) + .expect(reply(1, expectedState, effects), Delta.sorted) + .passivate() + } + + // failure tests + + "verify failure action" in GCounter.test { id => + protocol.crdt + .connect() + .send(init(CrdtTckModel.name, id)) + .send(command(1, id, "Process", Request(id, Seq(failWith("expected failure"))))) + .expect(failure(1, "expected failure")) + .passivate() + } + + "verify connection after failure action" in PNCounter.test { id => + protocol.crdt + .connect() + .send(init(CrdtTckModel.name, id)) + .send(command(1, id, "Process", Request(id, PNCounter.changeBy(+42)))) + .expect(reply(1, PNCounter.state(+42), PNCounter.update(+42))) + .send(command(2, id, "Process", Request(id, Seq(failWith("expected failure"))))) + .expect(failure(2, "expected failure")) + .send(command(3, id, "Process", Request(id))) + .expect(reply(3, PNCounter.state(+42))) + .passivate() + } + + // streamed response tests + + "verify streamed responses" in GCounter.test { id => + protocol.crdt + .connect() + .send(init(CrdtTckModel.name, id)) + .send(command(1, id, "ProcessStreamed", StreamedRequest(id), streamed = true)) + .expect(reply(1, GCounter.state(0), Effects(streamed = true))) + .send(command(2, id, "Process", Request(id, GCounter.incrementBy(1)))) + .expect(reply(2, GCounter.state(1), GCounter.update(1))) + .expect(streamed(1, GCounter.state(1))) + .send(command(3, id, "Process", Request(id, GCounter.incrementBy(2)))) + .expect(reply(3, GCounter.state(3), GCounter.update(2))) + .expect(streamed(1, GCounter.state(3))) + .send(command(4, id, "Process", Request(id, GCounter.incrementBy(3)))) + .expect(reply(4, GCounter.state(6), GCounter.update(3))) + .expect(streamed(1, GCounter.state(6))) + .passivate() + } + + "verify streamed responses with stream ending action" in GCounter.test { id => + protocol.crdt + .connect() + .send(init(CrdtTckModel.name, id)) + .send( + command(1, id, "ProcessStreamed", StreamedRequest(id, endState = GCounter.state(3).state), streamed = true) + ) + .expect(reply(1, GCounter.state(0), Effects(streamed = true))) + .send(command(2, id, "Process", Request(id, GCounter.incrementBy(1)))) + .expect(reply(2, GCounter.state(1), GCounter.update(1))) + .expect(streamed(1, GCounter.state(1))) + .send(command(3, id, "Process", Request(id, GCounter.incrementBy(2)))) + .expect(reply(3, GCounter.state(3), GCounter.update(2))) + .expect(streamed(1, GCounter.state(3), Effects(endStream = true))) + .send(command(4, id, "Process", Request(id, GCounter.incrementBy(3)))) + .expect(reply(4, GCounter.state(6), GCounter.update(3))) + .passivate() + } + + "verify streamed responses with cancellation" in PNCounter.test { id => + protocol.crdt + .connect() + .send(init(CrdtTckModel.name, id)) + .send( + command( + 1, + id, + "ProcessStreamed", + StreamedRequest(id, cancelUpdate = Some(PNCounter.updateWith(-42))), + streamed = true + ) + ) + .expect(reply(1, PNCounter.state(0), Effects(streamed = true))) + .send(command(2, id, "Process", Request(id, PNCounter.changeBy(+10)))) + .expect(reply(2, PNCounter.state(+10), PNCounter.update(+10))) + .expect(streamed(1, PNCounter.state(+10))) + .send(command(3, id, "Process", Request(id, PNCounter.changeBy(+20)))) + .expect(reply(3, PNCounter.state(+30), PNCounter.update(+20))) + .expect(streamed(1, PNCounter.state(+30))) + .send(crdtStreamCancelled(1, id)) + .expect(streamCancelledResponse(1, PNCounter.update(-42))) + .send(command(4, id, "Process", Request(id))) + .expect(reply(4, PNCounter.state(-12))) + .send(command(5, id, "Process", Request(id, PNCounter.changeBy(+30)))) + .expect(reply(5, PNCounter.state(+18), PNCounter.update(+30))) + .passivate() + } + + "verify empty streamed responses with cancellation" in PNCounter.test { id => + protocol.crdt + .connect() + .send(init(CrdtTckModel.name, id)) + .send(command(1, id, "Process", Request(id, PNCounter.changeBy(+10)))) + .expect(reply(1, PNCounter.state(+10), PNCounter.update(+10))) + .send( + command( + 2, + id, + "ProcessStreamed", + StreamedRequest(id, cancelUpdate = Some(PNCounter.updateWith(-42)), empty = true), + streamed = true + ) + ) + .expect(crdtReply(2, None, Effects(streamed = true))) + .send(crdtStreamCancelled(2, id)) + .expect(streamCancelledResponse(2, PNCounter.update(-42))) + .send(command(3, id, "Process", Request(id))) + .expect(reply(3, PNCounter.state(-32))) + .passivate() + } + + "verify streamed responses with side effects" in GSet.test { id => + protocol.crdt + .connect() + .send(init(CrdtTckModel.name, id)) + .send( + command( + 1, + id, + "ProcessStreamed", + StreamedRequest(id, effects = Seq(Effect("one"), Effect("two", synchronous = true))), + streamed = true + ) + ) + .expect(reply(1, GSet.state(), Effects(streamed = true))) + .send(command(2, id, "Process", Request(id, GSet.add("a")))) + .expect(reply(2, GSet.state("a"), GSet.update("a"))) + .expect(streamed(1, GSet.state("a"), sideEffects("one") ++ synchronousSideEffects("two"))) + .send(command(3, id, "Process", Request(id, GSet.add("b")))) + .expect(reply(3, GSet.state("a", "b"), GSet.update("b"))) + .expect(streamed(1, GSet.state("a", "b"), sideEffects("one") ++ synchronousSideEffects("two"))) + .passivate() + } + + // write consistency tests + + def incrementWith(increment: Long, writeConsistency: UpdateWriteConsistency): Seq[RequestAction] = + Seq(requestUpdate(GCounter.updateWith(increment).withWriteConsistency(writeConsistency))) + + def updateWith(value: Long, writeConsistency: CrdtWriteConsistency): Effects = + Effects(stateAction = crdtUpdate(GCounter.delta(value), writeConsistency)) + + "verify write consistency can be configured" in GCounter.test { id => + protocol.crdt + .connect() + .send(init(CrdtTckModel.name, id)) + .send(command(1, id, "Process", Request(id, incrementWith(1, UpdateWriteConsistency.LOCAL)))) + .expect(reply(1, GCounter.state(1), updateWith(1, CrdtWriteConsistency.LOCAL))) + .send(command(2, id, "Process", Request(id, incrementWith(2, UpdateWriteConsistency.MAJORITY)))) + .expect(reply(2, GCounter.state(3), updateWith(2, CrdtWriteConsistency.MAJORITY))) + .send(command(3, id, "Process", Request(id, incrementWith(3, UpdateWriteConsistency.ALL)))) + .expect(reply(3, GCounter.state(6), updateWith(3, CrdtWriteConsistency.ALL))) + .passivate() + } + } + + object CrdtEntityTCKProxy { + val crdtTckModelClient: CrdtTckModelClient = CrdtTckModelClient(client.settings)(client.system) + + def terminate(): Unit = crdtTckModelClient.close() + } + + override def afterAll(): Unit = + try CrdtEntityTCKProxy.terminate() + finally super.afterAll() + + def verifyCrdtEntityProxy(): Unit = { + import CrdtEntityTCKModel._ + import CrdtEntityTCKProxy._ + + "verify streamed responses for connection tracking" in Vote.test { id => + implicit val actorSystem: ActorSystem = client.system + + val state0 = Vote.state(selfVote = false, votesFor = 0, totalVoters = 1) + val state1 = Vote.state(selfVote = true, votesFor = 1, totalVoters = 1) + val voteTrue = Some(Vote.updateWith(true)) + val voteFalse = Some(Vote.updateWith(false)) + + val monitor = crdtTckModelClient.processStreamed(StreamedRequest(id)).runWith(TestSink.probe[Response]) + monitor.request(1).expectNext(state0) + val crdtProtocol = interceptor.expectCrdtConnection() + crdtProtocol.expectClient(init(CrdtTckModel.name, id)) + crdtProtocol.expectClient(command(1, id, "ProcessStreamed", StreamedRequest(id), streamed = true)) + crdtProtocol.expectService(reply(1, state0, Effects(streamed = true))) + + val connectRequest = StreamedRequest(id, initialUpdate = voteTrue, cancelUpdate = voteFalse, empty = true) + val connect = crdtTckModelClient.processStreamed(connectRequest).runWith(TestSink.probe[Response]) + connect.request(1).expectNoMessage(100.millis) + monitor.request(1).expectNext(state1) + crdtProtocol.expectClient(command(2, id, "ProcessStreamed", connectRequest, streamed = true)) + crdtProtocol.expectService(crdtReply(2, None, Effects(streamed = true) ++ Vote.update(true))) + crdtProtocol.expectService(streamed(1, state1)) + + connect.cancel() + monitor.request(1).expectNext(state0) + crdtProtocol.expectClient(crdtStreamCancelled(2, id)) + crdtProtocol.expectService(streamCancelledResponse(2, Vote.update(false))) + crdtProtocol.expectService(streamed(1, state0)) + + monitor.cancel() + crdtProtocol.expectClient(crdtStreamCancelled(1, id)) + crdtProtocol.expectService(streamCancelledResponse(1)) + + val deleteRequest = Request(id, Seq(requestDelete)) + crdtTckModelClient.process(deleteRequest).futureValue mustBe state0 + crdtProtocol.expectClient(command(3, id, "Process", deleteRequest)) + crdtProtocol.expectService(reply(3, state0, deleteCrdt)) + crdtProtocol.expectClosed() + } + } +} diff --git a/tck/src/main/scala/io/cloudstate/tck/EntityTCK.scala b/tck/src/main/scala/io/cloudstate/tck/EntityTCK.scala new file mode 100644 index 000000000..522cd275c --- /dev/null +++ b/tck/src/main/scala/io/cloudstate/tck/EntityTCK.scala @@ -0,0 +1,324 @@ +/* + * Copyright 2019 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 io.cloudstate.tck + +import com.google.protobuf.any.{Any => ScalaPbAny} +import io.cloudstate.protocol.entity.{EntityPassivationStrategy, TimeoutPassivationStrategy} +import io.cloudstate.protocol.entity.EntityPassivationStrategy.Strategy +import io.cloudstate.protocol.value_entity.ValueEntity +import io.cloudstate.tck.model.valueentity.valueentity._ +import io.cloudstate.testkit.valueentity.ValueEntityMessages._ +import scala.concurrent.duration._ + +trait EntityTCK extends TCKSpec { + + object EntityTCKModel { + val Protocol: String = ValueEntity.name + val Service: String = ValueEntityTckModel.name + val ServiceTwo: String = ValueEntityTwo.name + val ServiceConfigured: String = ValueEntityConfigured.name + + var entityId: Int = 0 + + def nextEntityId(): String = { entityId += 1; s"entity:$entityId" } + + def valueEntityTest(test: String => Any): Unit = + testFor(ValueEntityTckModel, ValueEntityTwo)(test(nextEntityId())) + + def valueEntityConfiguredTest(test: String => Any): Unit = + testFor(ValueEntityConfigured)(test(nextEntityId())) + + def updateState(value: String): RequestAction = + RequestAction(RequestAction.Action.Update(Update(value))) + + def updateStates(values: String*): Seq[RequestAction] = + values.map(updateState) + + def deleteState(): RequestAction = + RequestAction(RequestAction.Action.Delete(Delete())) + + def updateAndDeleteActions(values: String*): Seq[RequestAction] = + values.map(updateState) :+ deleteState() + + def deleteBetweenUpdateActions(first: String, second: String): Seq[RequestAction] = + Seq(updateState(first), deleteState(), updateState(second)) + + def forwardTo(id: String): RequestAction = + RequestAction(RequestAction.Action.Forward(Forward(id))) + + def sideEffectTo(id: String, synchronous: Boolean = false): RequestAction = + RequestAction(RequestAction.Action.Effect(Effect(id, synchronous))) + + def sideEffectsTo(ids: String*): Seq[RequestAction] = + ids.map(id => sideEffectTo(id)) + + def failWith(message: String): RequestAction = + RequestAction(RequestAction.Action.Fail(Fail(message))) + + def persisted(value: String): ScalaPbAny = + protobufAny(Persisted(value)) + + def update(value: String): Effects = + Effects.empty.withUpdateAction(persisted(value)) + + def delete(): Effects = + Effects.empty.withDeleteAction() + + def sideEffects(ids: String*): Effects = + createSideEffects(synchronous = false, ids) + + def synchronousSideEffects(ids: String*): Effects = + createSideEffects(synchronous = true, ids) + + def createSideEffects(synchronous: Boolean, ids: Seq[String]): Effects = + ids.foldLeft(Effects.empty) { case (e, id) => e.withSideEffect(ServiceTwo, "Call", Request(id), synchronous) } + } + + def verifyEntityModel(): Unit = { + import EntityTCKModel._ + + "verify entity discovery" in testFor(ValueEntityTckModel, ValueEntityTwo) { + discoveredServices must (contain("ValueEntityTckModel") and contain("ValueEntityTwo")) + entity(EntityTCKModel.Service).value.entityType mustBe EntityTCKModel.Protocol + entity(EntityTCKModel.ServiceTwo).value.entityType mustBe EntityTCKModel.Protocol + entity(EntityTCKModel.Service).value.persistenceId mustBe "value-entity-tck-model" + entity(EntityTCKModel.ServiceTwo).value.persistenceId mustBe "value-entity-tck-model-two" + } + + "verify configured entity" in testFor(ValueEntityConfigured) { + entity(EntityTCKModel.ServiceConfigured).value.entityType mustBe EntityTCKModel.Protocol + entity(EntityTCKModel.ServiceConfigured).value.persistenceId mustBe "value-entity-configured" + entity(EntityTCKModel.ServiceConfigured).value.passivationStrategy mustBe Some( + EntityPassivationStrategy(Strategy.Timeout(TimeoutPassivationStrategy(100))) + ) + } + + "verify initial empty state" in valueEntityTest { id => + protocol.valueEntity + .connect() + .send(init(ValueEntityTckModel.name, id)) + .send(command(1, id, "Process", Request(id))) + .expect(reply(1, Response())) + .passivate() + } + + "verify update state" in valueEntityTest { id => + protocol.valueEntity + .connect() + .send(init(ValueEntityTckModel.name, id)) + .send(command(1, id, "Process", Request(id, updateStates("A")))) + .expect(reply(1, Response("A"), update("A"))) + .send(command(2, id, "Process", Request(id))) + .expect(reply(2, Response("A"))) + .passivate() + } + + "verify delete state" in valueEntityTest { id => + protocol.valueEntity + .connect() + .send(init(ValueEntityTckModel.name, id)) + .send(command(1, id, "Process", Request(id, updateStates("A")))) + .expect(reply(1, Response("A"), update("A"))) + .send(command(2, id, "Process", Request(id, Seq(deleteState())))) + .expect(reply(2, Response(), delete())) + .send(command(3, id, "Process", Request(id))) + .expect(reply(3, Response())) + .passivate() + } + + "verify sub invocations with multiple update states" in valueEntityTest { id => + protocol.valueEntity + .connect() + .send(init(ValueEntityTckModel.name, id)) + .send(command(1, id, "Process", Request(id, updateStates("A", "B", "C")))) + .expect(reply(1, Response("C"), update("C"))) + .send(command(2, id, "Process", Request(id))) + .expect(reply(2, Response("C"))) + .passivate() + } + + "verify sub invocations with multiple update states and delete states" in valueEntityTest { id => + protocol.valueEntity + .connect() + .send(init(ValueEntityTckModel.name, id)) + .send(command(1, id, "Process", Request(id, updateAndDeleteActions("A", "B")))) + .expect(reply(1, Response(), delete())) + .send(command(2, id, "Process", Request(id))) + .expect(reply(2, Response())) + .passivate() + } + + "verify sub invocations with update, delete and update states" in valueEntityTest { id => + protocol.valueEntity + .connect() + .send(init(ValueEntityTckModel.name, id)) + .send(command(1, id, "Process", Request(id, deleteBetweenUpdateActions("A", "B")))) + .expect(reply(1, Response("B"), update("B"))) + .send(command(2, id, "Process", Request(id))) + .expect(reply(2, Response("B"))) + .passivate() + } + + "verify rehydration after passivation" in valueEntityTest { id => + protocol.valueEntity + .connect() + .send(init(ValueEntityTckModel.name, id)) + .send(command(1, id, "Process", Request(id, updateStates("A")))) + .expect(reply(1, Response("A"), update("A"))) + .send(command(2, id, "Process", Request(id, updateStates("B")))) + .expect(reply(2, Response("B"), update("B"))) + .send(command(3, id, "Process", Request(id, updateStates("C")))) + .expect(reply(3, Response("C"), update("C"))) + .send(command(4, id, "Process", Request(id, updateStates("D")))) + .expect(reply(4, Response("D"), update("D"))) + .passivate() + protocol.valueEntity + .connect() + .send(init(ValueEntityTckModel.name, id, state(persisted("D")))) + .send(command(1, id, "Process", Request(id, updateStates("E")))) + .expect(reply(1, Response("E"), update("E"))) + .send(command(2, id, "Process", Request(id))) + .expect(reply(2, Response("E"))) + .passivate() + } + + "verify reply with multiple side effects" in valueEntityTest { id => + protocol.valueEntity + .connect() + .send(init(ValueEntityTckModel.name, id)) + .send(command(1, id, "Process", Request(id, sideEffectsTo("1", "2", "3")))) + .expect(reply(1, Response(), sideEffects("1", "2", "3"))) + .passivate() + } + + "verify reply with side effect to second service" in valueEntityTest { id => + protocol.valueEntity + .connect() + .send(init(ValueEntityTckModel.name, id)) + .send(command(1, id, "Process", Request(id, Seq(sideEffectTo(id))))) + .expect(reply(1, Response(), sideEffects(id))) + .passivate() + } + + "verify reply with multiple side effects and state" in valueEntityTest { id => + val actions = updateStates("A", "B", "C", "D", "E") ++ sideEffectsTo("1", "2", "3") + val effects = sideEffects("1", "2", "3").withUpdateAction(persisted("E")) + protocol.valueEntity + .connect() + .send(init(ValueEntityTckModel.name, id)) + .send(command(1, id, "Process", Request(id, actions))) + .expect(reply(1, Response("E"), effects)) + .passivate() + } + + "verify synchronous side effect to second service" in valueEntityTest { id => + protocol.valueEntity + .connect() + .send(init(ValueEntityTckModel.name, id)) + .send(command(1, id, "Process", Request(id, Seq(sideEffectTo(id, synchronous = true))))) + .expect(reply(1, Response(), synchronousSideEffects(id))) + .passivate() + } + + "verify forward to second service" in valueEntityTest { id => + protocol.valueEntity + .connect() + .send(init(ValueEntityTckModel.name, id)) + .send(command(1, id, "Process", Request(id, Seq(forwardTo(id))))) + .expect(forward(1, ServiceTwo, "Call", Request(id))) + .passivate() + } + + "verify forward with updated state to second service" in valueEntityTest { id => + protocol.valueEntity + .connect() + .send(init(ValueEntityTckModel.name, id)) + .send(command(1, id, "Process", Request(id, Seq(updateState("A"), forwardTo(id))))) + .expect(forward(1, ServiceTwo, "Call", Request(id), update("A"))) + .passivate() + } + + "verify forward and side effect to second service" in valueEntityTest { id => + protocol.valueEntity + .connect() + .send(init(ValueEntityTckModel.name, id)) + .send(command(1, id, "Process", Request(id, Seq(sideEffectTo(id), forwardTo(id))))) + .expect(forward(1, ServiceTwo, "Call", Request(id), sideEffects(id))) + .passivate() + } + + "verify failure action" in valueEntityTest { id => + protocol.valueEntity + .connect() + .send(init(ValueEntityTckModel.name, id)) + .send(command(1, id, "Process", Request(id, Seq(failWith("expected failure"))))) + .expect(actionFailure(1, "expected failure")) + .passivate() + } + + "verify connection after failure action" in valueEntityTest { id => + protocol.valueEntity + .connect() + .send(init(ValueEntityTckModel.name, id)) + .send(command(1, id, "Process", Request(id, Seq(updateState("A"))))) + .expect(reply(1, Response("A"), update("A"))) + .send(command(2, id, "Process", Request(id, Seq(failWith("expected failure"))))) + .expect(actionFailure(2, "expected failure")) + .send(command(3, id, "Process", Request(id))) + .expect(reply(3, Response("A"))) + .passivate() + } + + "verify failure action do not allow side effects" in valueEntityTest { id => + protocol.valueEntity + .connect() + .send(init(ValueEntityTckModel.name, id)) + .send(command(1, id, "Process", Request(id, Seq(sideEffectTo(id), failWith("expected failure"))))) + .expect(actionFailure(1, "expected failure")) + .passivate() + } + } + + object EntityTCKProxy { + val tckModelClient: ValueEntityTckModelClient = ValueEntityTckModelClient(client.settings)(client.system) + val configuredClient: ValueEntityConfiguredClient = ValueEntityConfiguredClient(client.settings)(client.system) + + def terminate(): Unit = + try tckModelClient.close() + finally configuredClient.close() + } + + override def afterAll(): Unit = + try EntityTCKProxy.terminate() + finally super.afterAll() + + def verifyEntityProxy(): Unit = { + import EntityTCKModel._ + import EntityTCKProxy._ + + "verify passivation timeout" in valueEntityConfiguredTest { id => + configuredClient.call(Request(id)) + interceptor + .expectValueBasedConnection() + .expectClient(init(ValueEntityConfigured.name, id)) + .expectClient(command(1, id, "Call", Request(id))) + .expectService(reply(1, Response())) + .expectInClosed(2.seconds) // check passivation (with expected timeout of 100 millis) + .expectOutClosed(2.seconds) // check passivation (with expected timeout of 100 millis) + } + } +} diff --git a/tck/src/main/scala/io/cloudstate/tck/EventSourcedEntityTCK.scala b/tck/src/main/scala/io/cloudstate/tck/EventSourcedEntityTCK.scala new file mode 100644 index 000000000..606ebe381 --- /dev/null +++ b/tck/src/main/scala/io/cloudstate/tck/EventSourcedEntityTCK.scala @@ -0,0 +1,323 @@ +/* + * Copyright 2019 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 io.cloudstate.tck + +import com.google.protobuf.any.{Any => ScalaPbAny} +import io.cloudstate.protocol.event_sourced.EventSourced +import io.cloudstate.tck.model.eventsourced._ +import io.cloudstate.testkit.eventsourced.EventSourcedMessages._ + +trait EventSourcedEntityTCK extends TCKSpec { + + object EventSourcedEntityTCKModel { + val Protocol: String = EventSourced.name + val Service: String = EventSourcedTckModel.name + val ServiceTwo: String = EventSourcedTwo.name + + var entityId: Int = 0 + + def nextEntityId(): String = { entityId += 1; s"entity:$entityId" } + + def eventSourcedTest(test: String => Any): Unit = + testFor(EventSourcedTckModel, EventSourcedTwo)(test(nextEntityId())) + + def emitEvent(value: String): RequestAction = + RequestAction(RequestAction.Action.Emit(Emit(value))) + + def emitEvents(values: String*): Seq[RequestAction] = + values.map(emitEvent) + + def forwardTo(id: String): RequestAction = + RequestAction(RequestAction.Action.Forward(Forward(id))) + + def sideEffectTo(id: String, synchronous: Boolean = false): RequestAction = + RequestAction(RequestAction.Action.Effect(Effect(id, synchronous))) + + def sideEffectsTo(ids: String*): Seq[RequestAction] = + ids.map(id => sideEffectTo(id, synchronous = false)) + + def failWith(message: String): RequestAction = + RequestAction(RequestAction.Action.Fail(Fail(message))) + + def persisted(value: String): ScalaPbAny = + protobufAny(Persisted(value)) + + def events(values: String*): Effects = + Effects(events = values.map(persisted)) + + def snapshotAndEvents(snapshotValue: String, eventValues: String*): Effects = + events(eventValues: _*).withSnapshot(persisted(snapshotValue)) + + def sideEffects(ids: String*): Effects = + createSideEffects(synchronous = false, ids) + + def synchronousSideEffects(ids: String*): Effects = + createSideEffects(synchronous = true, ids) + + def createSideEffects(synchronous: Boolean, ids: Seq[String]): Effects = + ids.foldLeft(Effects.empty) { case (e, id) => e.withSideEffect(ServiceTwo, "Call", Request(id), synchronous) } + } + + def verifyEventSourcedEntityModel(): Unit = { + import EventSourcedEntityTCKModel._ + + "verify event sourced entity discovery" in testFor(EventSourcedTckModel, EventSourcedTwo) { + discoveredServices must (contain("EventSourcedTckModel") and contain("EventSourcedTwo")) + entity(EventSourcedEntityTCKModel.Service).value.entityType mustBe EventSourcedEntityTCKModel.Protocol + entity(EventSourcedEntityTCKModel.ServiceTwo).value.entityType mustBe EventSourcedEntityTCKModel.Protocol + entity(EventSourcedEntityTCKModel.Service).value.persistenceId mustBe "event-sourced-tck-model" + } + + "verify initial empty state" in eventSourcedTest { id => + protocol.eventSourced + .connect() + .send(init(EventSourcedTckModel.name, id)) + .send(command(1, id, "Process", Request(id))) + .expect(reply(1, Response())) + .passivate() + } + + "verify single emitted event" in eventSourcedTest { id => + protocol.eventSourced + .connect() + .send(init(EventSourcedTckModel.name, id)) + .send(command(1, id, "Process", Request(id, emitEvents("A")))) + .expect(reply(1, Response("A"), events("A"))) + .send(command(2, id, "Process", Request(id))) + .expect(reply(2, Response("A"))) + .passivate() + } + + "verify multiple emitted events" in eventSourcedTest { id => + protocol.eventSourced + .connect() + .send(init(EventSourcedTckModel.name, id)) + .send(command(1, id, "Process", Request(id, emitEvents("A", "B", "C")))) + .expect(reply(1, Response("ABC"), events("A", "B", "C"))) + .send(command(2, id, "Process", Request(id))) + .expect(reply(2, Response("ABC"))) + .passivate() + } + + "verify multiple emitted events and snapshots" in eventSourcedTest { id => + protocol.eventSourced + .connect() + .send(init(EventSourcedTckModel.name, id)) + .send(command(1, id, "Process", Request(id, emitEvents("A")))) + .expect(reply(1, Response("A"), events("A"))) + .send(command(2, id, "Process", Request(id, emitEvents("B")))) + .expect(reply(2, Response("AB"), events("B"))) + .send(command(3, id, "Process", Request(id, emitEvents("C")))) + .expect(reply(3, Response("ABC"), events("C"))) + .send(command(4, id, "Process", Request(id, emitEvents("D")))) + .expect(reply(4, Response("ABCD"), events("D"))) + .send(command(5, id, "Process", Request(id, emitEvents("E")))) + .expect(reply(5, Response("ABCDE"), snapshotAndEvents("ABCDE", "E"))) + .send(command(6, id, "Process", Request(id, emitEvents("F", "G", "H")))) + .expect(reply(6, Response("ABCDEFGH"), events("F", "G", "H"))) + .send(command(7, id, "Process", Request(id, emitEvents("I", "J")))) + .expect(reply(7, Response("ABCDEFGHIJ"), snapshotAndEvents("ABCDEFGHIJ", "I", "J"))) + .send(command(8, id, "Process", Request(id, emitEvents("K")))) + .expect(reply(8, Response("ABCDEFGHIJK"), events("K"))) + .send(command(9, id, "Process", Request(id))) + .expect(reply(9, Response("ABCDEFGHIJK"))) + .passivate() + } + + "verify initial snapshot" in eventSourcedTest { id => + protocol.eventSourced + .connect() + .send(init(EventSourcedTckModel.name, id, snapshot(5, persisted("ABCDE")))) + .send(command(1, id, "Process", Request(id))) + .expect(reply(1, Response("ABCDE"))) + .passivate() + } + + "verify initial snapshot and events" in eventSourcedTest { id => + protocol.eventSourced + .connect() + .send(init(EventSourcedTckModel.name, id, snapshot(5, persisted("ABCDE")))) + .send(event(6, persisted("F"))) + .send(event(7, persisted("G"))) + .send(command(1, id, "Process", Request(id))) + .expect(reply(1, Response("ABCDEFG"))) + .passivate() + } + + "verify rehydration after passivation" in eventSourcedTest { id => + protocol.eventSourced + .connect() + .send(init(EventSourcedTckModel.name, id)) + .send(command(1, id, "Process", Request(id, emitEvents("A", "B", "C")))) + .expect(reply(1, Response("ABC"), events("A", "B", "C"))) + .send(command(2, id, "Process", Request(id, emitEvents("D", "E")))) + .expect(reply(2, Response("ABCDE"), snapshotAndEvents("ABCDE", "D", "E"))) + .send(command(3, id, "Process", Request(id, emitEvents("F")))) + .expect(reply(3, Response("ABCDEF"), events("F"))) + .send(command(4, id, "Process", Request(id, emitEvents("G")))) + .expect(reply(4, Response("ABCDEFG"), events("G"))) + .passivate() + protocol.eventSourced + .connect() + .send(init(EventSourcedTckModel.name, id, snapshot(5, persisted("ABCDE")))) + .send(event(6, persisted("F"))) + .send(event(7, persisted("G"))) + .send(command(1, id, "Process", Request(id))) + .expect(reply(1, Response("ABCDEFG"))) + .passivate() + } + + "verify forward to second service" in eventSourcedTest { id => + protocol.eventSourced + .connect() + .send(init(EventSourcedTckModel.name, id)) + .send(command(1, id, "Process", Request(id, Seq(forwardTo(id))))) + .expect(forward(1, EventSourcedTwo.name, "Call", Request(id))) + .passivate() + } + + "verify forward with emitted events" in eventSourcedTest { id => + protocol.eventSourced + .connect() + .send(init(EventSourcedTckModel.name, id)) + .send(command(1, id, "Process", Request(id, Seq(emitEvent("A"), forwardTo(id))))) + .expect(forward(1, EventSourcedTwo.name, "Call", Request(id), events("A"))) + .passivate() + } + + "verify forward with emitted events and snapshot" in eventSourcedTest { id => + protocol.eventSourced + .connect() + .send(init(EventSourcedTckModel.name, id)) + .send(command(1, id, "Process", Request(id, emitEvents("A", "B", "C")))) + .expect(reply(1, Response("ABC"), events("A", "B", "C"))) + .send(command(2, id, "Process", Request(id, Seq(emitEvent("D"), emitEvent("E"), forwardTo(id))))) + .expect(forward(2, EventSourcedTwo.name, "Call", Request(id), snapshotAndEvents("ABCDE", "D", "E"))) + .passivate() + } + + "verify reply with side effect to second service" in eventSourcedTest { id => + protocol.eventSourced + .connect() + .send(init(EventSourcedTckModel.name, id)) + .send(command(1, id, "Process", Request(id, Seq(sideEffectTo(id))))) + .expect(reply(1, Response(), sideEffects(id))) + .passivate() + } + + "verify synchronous side effect to second service" in eventSourcedTest { id => + protocol.eventSourced + .connect() + .send(init(EventSourcedTckModel.name, id)) + .send(command(1, id, "Process", Request(id, Seq(sideEffectTo(id, synchronous = true))))) + .expect(reply(1, Response(), synchronousSideEffects(id))) + .passivate() + } + + "verify forward and side effect to second service" in eventSourcedTest { id => + protocol.eventSourced + .connect() + .send(init(EventSourcedTckModel.name, id)) + .send(command(1, id, "Process", Request(id, Seq(sideEffectTo(id), forwardTo(id))))) + .expect(forward(1, ServiceTwo, "Call", Request(id), sideEffects(id))) + .passivate() + } + + "verify reply with multiple side effects" in eventSourcedTest { id => + protocol.eventSourced + .connect() + .send(init(EventSourcedTckModel.name, id)) + .send(command(1, id, "Process", Request(id, sideEffectsTo("1", "2", "3")))) + .expect(reply(1, Response(), sideEffects("1", "2", "3"))) + .passivate() + } + + "verify reply with multiple side effects, events, and snapshot" in eventSourcedTest { id => + val actions = emitEvents("A", "B", "C", "D", "E") ++ sideEffectsTo("1", "2", "3") + val effects = snapshotAndEvents("ABCDE", "A", "B", "C", "D", "E") ++ sideEffects("1", "2", "3") + protocol.eventSourced + .connect() + .send(init(EventSourcedTckModel.name, id)) + .send(command(1, id, "Process", Request(id, actions))) + .expect(reply(1, Response("ABCDE"), effects)) + .passivate() + } + + "verify failure action" in eventSourcedTest { id => + protocol.eventSourced + .connect() + .send(init(EventSourcedTckModel.name, id)) + .send(command(1, id, "Process", Request(id, Seq(failWith("expected failure"))))) + .expect(actionFailure(1, "expected failure")) + .passivate() + } + + "verify connection after failure action" in eventSourcedTest { id => + protocol.eventSourced + .connect() + .send(init(EventSourcedTckModel.name, id)) + .send(command(1, id, "Process", Request(id, Seq(emitEvent("A"))))) + .expect(reply(1, Response("A"), events("A"))) + .send(command(2, id, "Process", Request(id, Seq(failWith("expected failure"))))) + .expect(actionFailure(2, "expected failure")) + .send(command(3, id, "Process", Request(id))) + .expect(reply(3, Response("A"))) + .passivate() + } + + "verify failure actions do not retain emitted events, by requesting entity restart" in eventSourcedTest { id => + protocol.eventSourced + .connect() + .send(init(EventSourcedTckModel.name, id)) + .send(command(1, id, "Process", Request(id, emitEvents("A", "B", "C")))) + .expect(reply(1, Response("ABC"), events("A", "B", "C"))) + .send(command(2, id, "Process", Request(id, Seq(emitEvent("4"), emitEvent("5"), failWith("failure 1"))))) + .expect(actionFailure(2, "failure 1", restart = true)) + .passivate() + protocol.eventSourced + .connect() + .send(init(EventSourcedTckModel.name, id)) + .send(event(1, persisted("A"))) + .send(event(2, persisted("B"))) + .send(event(3, persisted("C"))) + .send(command(1, id, "Process", Request(id, emitEvents("D")))) + .expect(reply(1, Response("ABCD"), events("D"))) + .send(command(2, id, "Process", Request(id, Seq(emitEvent("6"), failWith("failure 2"), emitEvent("7"))))) + .expect(actionFailure(2, "failure 2", restart = true)) + .passivate() + protocol.eventSourced + .connect() + .send(init(EventSourcedTckModel.name, id)) + .send(event(1, persisted("A"))) + .send(event(2, persisted("B"))) + .send(event(3, persisted("C"))) + .send(event(4, persisted("D"))) + .send(command(1, id, "Process", Request(id, emitEvents("E")))) + .expect(reply(1, Response("ABCDE"), snapshotAndEvents("ABCDE", "E"))) + .passivate() + } + + "verify failure actions do not allow side effects" in eventSourcedTest { id => + protocol.eventSourced + .connect() + .send(init(EventSourcedTckModel.name, id)) + .send(command(1, id, "Process", Request(id, Seq(sideEffectTo(id), failWith("expected failure"))))) + .expect(actionFailure(1, "expected failure")) + .passivate() + } + } +} diff --git a/tck/src/main/scala/io/cloudstate/tck/EventSourcedShoppingCartTCK.scala b/tck/src/main/scala/io/cloudstate/tck/EventSourcedShoppingCartTCK.scala new file mode 100644 index 000000000..b8ebee85b --- /dev/null +++ b/tck/src/main/scala/io/cloudstate/tck/EventSourcedShoppingCartTCK.scala @@ -0,0 +1,240 @@ +/* + * Copyright 2019 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 io.cloudstate.tck + +import com.example.shoppingcart.persistence.domain +import com.example.shoppingcart.shoppingcart._ +import io.cloudstate.protocol.event_sourced.{EventSourced, EventSourcedStreamOut} +import io.cloudstate.testkit.InterceptService +import io.cloudstate.testkit.eventsourced.{EventSourcedMessages, InterceptEventSourcedService} +import io.grpc.StatusRuntimeException +import org.scalatest.MustMatchers +import scala.collection.mutable + +trait EventSourcedShoppingCartTCK extends TCKSpec { + + object EventSourcedShoppingCart { + import EventSourcedMessages._ + import EventSourcedShoppingCartVerifier._ + + val Service: String = ShoppingCart.name + + private val shoppingCartClient = ShoppingCartClient(client.settings)(client.system) + + def terminate(): Unit = shoppingCartClient.close() + + def verifyGetInitialEmptyCart(session: EventSourcedShoppingCartVerifier, cartId: String): Unit = { + shoppingCartClient.getCart(GetShoppingCart(cartId)).futureValue mustBe Cart() + session.verifyConnection() + session.verifyGetInitialEmptyCart(cartId) + } + + def verifyGetCart(session: EventSourcedShoppingCartVerifier, cartId: String, expected: Item*): Unit = { + val expectedCart = shoppingCart(expected: _*) + shoppingCartClient.getCart(GetShoppingCart(cartId)).futureValue mustBe expectedCart + session.verifyGetCart(cartId, expectedCart) + } + + def verifyAddItem(session: EventSourcedShoppingCartVerifier, cartId: String, item: Item): Unit = { + val addLineItem = AddLineItem(cartId, item.id, item.name, item.quantity) + shoppingCartClient.addItem(addLineItem).futureValue mustBe EmptyScalaMessage + session.verifyAddItem(cartId, item) + } + + def verifyRemoveItem(session: EventSourcedShoppingCartVerifier, cartId: String, itemId: String): Unit = { + val removeLineItem = RemoveLineItem(cartId, itemId) + shoppingCartClient.removeItem(removeLineItem).futureValue mustBe EmptyScalaMessage + session.verifyRemoveItem(cartId, itemId) + } + + def verifyAddItemFailure(session: EventSourcedShoppingCartVerifier, cartId: String, item: Item): Unit = { + val addLineItem = AddLineItem(cartId, item.id, item.name, item.quantity) + val error = shoppingCartClient.addItem(addLineItem).failed.futureValue + error mustBe a[StatusRuntimeException] + val description = error.asInstanceOf[StatusRuntimeException].getStatus.getDescription + session.verifyAddItemFailure(cartId, item, description) + } + + def verifyRemoveItemFailure(session: EventSourcedShoppingCartVerifier, cartId: String, itemId: String): Unit = { + val removeLineItem = RemoveLineItem(cartId, itemId) + val error = shoppingCartClient.removeItem(removeLineItem).failed.futureValue + error mustBe a[StatusRuntimeException] + val description = error.asInstanceOf[StatusRuntimeException].getStatus.getDescription + session.verifyRemoveItemFailure(cartId, itemId, description) + } + } + + override def afterAll(): Unit = + try EventSourcedShoppingCart.terminate() + finally super.afterAll() + + def verifyEventSourcedShoppingCart(): Unit = { + import EventSourcedShoppingCart._ + import EventSourcedShoppingCartVerifier._ + + "verify event sourced shopping cart discovery" in testFor(ShoppingCart) { + discoveredServices must contain("ShoppingCart") + entity(EventSourcedShoppingCart.Service).value.entityType mustBe EventSourced.name + entity(EventSourcedShoppingCart.Service).value.persistenceId must not be empty + } + + "verify get cart, add item, remove item, and failures" in testFor(ShoppingCart) { + val session = shoppingCartSession(interceptor) + verifyGetInitialEmptyCart(session, "cart:1") // initial empty state + verifyAddItem(session, "cart:1", Item("product:1", "Product1", 1)) // add the first product + verifyAddItem(session, "cart:1", Item("product:2", "Product2", 2)) // add the second product + verifyAddItem(session, "cart:1", Item("product:1", "Product1", 11)) // increase first product + verifyAddItem(session, "cart:1", Item("product:2", "Product2", 31)) // increase second product + verifyGetCart(session, "cart:1", Item("product:1", "Product1", 12), Item("product:2", "Product2", 33)) // check state + verifyRemoveItem(session, "cart:1", "product:1") // remove first product + verifyAddItemFailure(session, "cart:1", Item("product:2", "Product2", -7)) // add negative quantity + verifyAddItemFailure(session, "cart:1", Item("product:1", "Product1", 0)) // add zero quantity + verifyRemoveItemFailure(session, "cart:1", "product:1") // remove non-existing product + verifyGetCart(session, "cart:1", Item("product:2", "Product2", 33)) // check final state + } + + "verify the HTTP API for event sourced shopping cart" in testFor(ShoppingCart) { + def checkHttpRequest(path: String, body: String = null)(expected: => String): Unit = { + val response = client.http.request(path, body) + val expectedResponse = expected + response.futureValue mustBe expectedResponse + } + + val session = shoppingCartSession(interceptor) + + checkHttpRequest("carts/foo") { + session.verifyConnection() + session.verifyGetInitialEmptyCart("foo") + """{"items":[]}""" + } + + checkHttpRequest("cart/foo/items/add", """{"productId": "A14362347", "name": "Deluxe", "quantity": 5}""") { + session.verifyAddItem("foo", Item("A14362347", "Deluxe", 5)) + "{}" + } + + checkHttpRequest("cart/foo/items/add", """{"productId": "B14623482", "name": "Basic", "quantity": 1}""") { + session.verifyAddItem("foo", Item("B14623482", "Basic", 1)) + "{}" + } + + checkHttpRequest("cart/foo/items/add", """{"productId": "A14362347", "name": "Deluxe", "quantity": 2}""") { + session.verifyAddItem("foo", Item("A14362347", "Deluxe", 2)) + "{}" + } + + checkHttpRequest("carts/foo") { + session.verifyGetCart("foo", shoppingCart(Item("A14362347", "Deluxe", 7), Item("B14623482", "Basic", 1))) + """{"items":[{"productId":"A14362347","name":"Deluxe","quantity":7},{"productId":"B14623482","name":"Basic","quantity":1}]}""" + } + + checkHttpRequest("carts/foo/items") { + session.verifyGetCart("foo", shoppingCart(Item("A14362347", "Deluxe", 7), Item("B14623482", "Basic", 1))) + """[{"productId":"A14362347","name":"Deluxe","quantity":7.0},{"productId":"B14623482","name":"Basic","quantity":1.0}]""" + } + + checkHttpRequest("cart/foo/items/A14362347/remove", "") { + session.verifyRemoveItem("foo", "A14362347") + "{}" + } + + checkHttpRequest("carts/foo") { + session.verifyGetCart("foo", shoppingCart(Item("B14623482", "Basic", 1))) + """{"items":[{"productId":"B14623482","name":"Basic","quantity":1}]}""" + } + + checkHttpRequest("carts/foo/items") { + session.verifyGetCart("foo", shoppingCart(Item("B14623482", "Basic", 1))) + """[{"productId":"B14623482","name":"Basic","quantity":1.0}]""" + } + } + } +} + +object EventSourcedShoppingCartVerifier { + case class Item(id: String, name: String, quantity: Int) + + def shoppingCartSession(interceptor: InterceptService): EventSourcedShoppingCartVerifier = + new EventSourcedShoppingCartVerifier(interceptor) + + def shoppingCart(items: Item*): Cart = Cart(items.map(i => LineItem(i.id, i.name, i.quantity))) +} + +class EventSourcedShoppingCartVerifier(interceptor: InterceptService) extends MustMatchers { + import EventSourcedMessages._ + import EventSourcedShoppingCartVerifier.Item + + private val commandIds = mutable.Map.empty[String, Long] + private var connection: InterceptEventSourcedService.Connection = _ + + private def nextCommandId(cartId: String): Long = commandIds.updateWith(cartId)(_.map(_ + 1).orElse(Some(1L))).get + + def verifyConnection(): Unit = connection = interceptor.expectEventSourcedConnection() + + def verifyGetInitialEmptyCart(cartId: String): Unit = { + val commandId = nextCommandId(cartId) + connection.expectClient(init(ShoppingCart.name, cartId)) + connection.expectClient(command(commandId, cartId, "GetCart", GetShoppingCart(cartId))) + connection.expectService(reply(commandId, Cart())) + connection.expectNoInteraction() + } + + def verifyGetCart(cartId: String, expected: Cart): Unit = { + val commandId = nextCommandId(cartId) + connection.expectClient(command(commandId, cartId, "GetCart", GetShoppingCart(cartId))) + connection.expectService(reply(commandId, expected)) + connection.expectNoInteraction() + } + + def verifyAddItem(cartId: String, item: Item): Unit = { + val commandId = nextCommandId(cartId) + val addLineItem = AddLineItem(cartId, item.id, item.name, item.quantity) + val itemAdded = domain.ItemAdded(Some(domain.LineItem(item.id, item.name, item.quantity))) + connection.expectClient(command(commandId, cartId, "AddItem", addLineItem)) + // shopping cart implementations may or may not have snapshots configured, so match without snapshot + val replied = connection.expectServiceMessage[EventSourcedStreamOut.Message.Reply] + replied.copy(value = replied.value.clearSnapshot) mustBe reply(commandId, EmptyScalaMessage, persist(itemAdded)) + connection.expectNoInteraction() + } + + def verifyRemoveItem(cartId: String, itemId: String): Unit = { + val commandId = nextCommandId(cartId) + val removeLineItem = RemoveLineItem(cartId, itemId) + val itemRemoved = domain.ItemRemoved(itemId) + connection.expectClient(command(commandId, cartId, "RemoveItem", removeLineItem)) + // shopping cart implementations may or may not have snapshots configured, so match without snapshot + val replied = connection.expectServiceMessage[EventSourcedStreamOut.Message.Reply] + replied.copy(value = replied.value.clearSnapshot) mustBe reply(commandId, EmptyScalaMessage, persist(itemRemoved)) + connection.expectNoInteraction() + } + + def verifyAddItemFailure(cartId: String, item: Item, failure: String): Unit = { + val commandId = nextCommandId(cartId) + val addLineItem = AddLineItem(cartId, item.id, item.name, item.quantity) + connection.expectClient(command(commandId, cartId, "AddItem", addLineItem)) + connection.expectService(actionFailure(commandId, failure)) + connection.expectNoInteraction() + } + + def verifyRemoveItemFailure(cartId: String, itemId: String, failure: String): Unit = { + val commandId = nextCommandId(cartId) + val removeLineItem = RemoveLineItem(cartId, itemId) + connection.expectClient(command(commandId, cartId, "RemoveItem", removeLineItem)) + connection.expectService(actionFailure(commandId, failure)) + connection.expectNoInteraction() + } +} diff --git a/tck/src/main/scala/io/cloudstate/tck/EventSourcedShoppingCartVerifier.scala b/tck/src/main/scala/io/cloudstate/tck/EventSourcedShoppingCartVerifier.scala deleted file mode 100644 index 1187115cf..000000000 --- a/tck/src/main/scala/io/cloudstate/tck/EventSourcedShoppingCartVerifier.scala +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Copyright 2019 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 io.cloudstate.tck - -import com.example.shoppingcart.persistence.domain -import com.example.shoppingcart.shoppingcart.{ - AddLineItem, - Cart, - GetShoppingCart, - LineItem, - RemoveLineItem, - ShoppingCart -} -import io.cloudstate.protocol.event_sourced.EventSourcedStreamOut -import io.cloudstate.testkit.InterceptService -import io.cloudstate.testkit.eventsourced.{EventSourcedMessages, InterceptEventSourcedService} -import org.scalatest.MustMatchers - -import scala.collection.mutable - -object EventSourcedShoppingCartVerifier { - case class Item(id: String, name: String, quantity: Int) - - def shoppingCartSession(interceptor: InterceptService): EventSourcedShoppingCartVerifier = - new EventSourcedShoppingCartVerifier(interceptor) - - def shoppingCart(items: Item*): Cart = Cart(items.map(i => LineItem(i.id, i.name, i.quantity))) -} - -class EventSourcedShoppingCartVerifier(interceptor: InterceptService) extends MustMatchers { - import EventSourcedMessages._ - import EventSourcedShoppingCartVerifier.Item - - private val commandIds = mutable.Map.empty[String, Long] - private var connection: InterceptEventSourcedService.Connection = _ - - private def nextCommandId(cartId: String): Long = commandIds.updateWith(cartId)(_.map(_ + 1).orElse(Some(1L))).get - - def verifyConnection(): Unit = connection = interceptor.expectEventSourcedConnection() - - def verifyGetInitialEmptyCart(cartId: String): Unit = { - val commandId = nextCommandId(cartId) - connection.expectClient(init(ShoppingCart.name, cartId)) - connection.expectClient(command(commandId, cartId, "GetCart", GetShoppingCart(cartId))) - connection.expectService(reply(commandId, Cart())) - connection.expectNoInteraction() - } - - def verifyGetCart(cartId: String, expected: Cart): Unit = { - val commandId = nextCommandId(cartId) - connection.expectClient(command(commandId, cartId, "GetCart", GetShoppingCart(cartId))) - connection.expectService(reply(commandId, expected)) - connection.expectNoInteraction() - } - - def verifyAddItem(cartId: String, item: Item): Unit = { - val commandId = nextCommandId(cartId) - val addLineItem = AddLineItem(cartId, item.id, item.name, item.quantity) - val itemAdded = domain.ItemAdded(Some(domain.LineItem(item.id, item.name, item.quantity))) - connection.expectClient(command(commandId, cartId, "AddItem", addLineItem)) - // shopping cart implementations may or may not have snapshots configured, so match without snapshot - val replied = connection.expectServiceMessage[EventSourcedStreamOut.Message.Reply] - replied.copy(value = replied.value.clearSnapshot) mustBe reply(commandId, EmptyScalaMessage, persist(itemAdded)) - connection.expectNoInteraction() - } - - def verifyRemoveItem(cartId: String, itemId: String): Unit = { - val commandId = nextCommandId(cartId) - val removeLineItem = RemoveLineItem(cartId, itemId) - val itemRemoved = domain.ItemRemoved(itemId) - connection.expectClient(command(commandId, cartId, "RemoveItem", removeLineItem)) - // shopping cart implementations may or may not have snapshots configured, so match without snapshot - val replied = connection.expectServiceMessage[EventSourcedStreamOut.Message.Reply] - replied.copy(value = replied.value.clearSnapshot) mustBe reply(commandId, EmptyScalaMessage, persist(itemRemoved)) - connection.expectNoInteraction() - } - - def verifyAddItemFailure(cartId: String, item: Item, failure: String): Unit = { - val commandId = nextCommandId(cartId) - val addLineItem = AddLineItem(cartId, item.id, item.name, item.quantity) - connection.expectClient(command(commandId, cartId, "AddItem", addLineItem)) - connection.expectService(actionFailure(commandId, failure)) - connection.expectNoInteraction() - } - - def verifyRemoveItemFailure(cartId: String, itemId: String, failure: String): Unit = { - val commandId = nextCommandId(cartId) - val removeLineItem = RemoveLineItem(cartId, itemId) - connection.expectClient(command(commandId, cartId, "RemoveItem", removeLineItem)) - connection.expectService(actionFailure(commandId, failure)) - connection.expectNoInteraction() - } -} diff --git a/tck/src/main/scala/io/cloudstate/tck/EventingTCK.scala b/tck/src/main/scala/io/cloudstate/tck/EventingTCK.scala new file mode 100644 index 000000000..bec0fca09 --- /dev/null +++ b/tck/src/main/scala/io/cloudstate/tck/EventingTCK.scala @@ -0,0 +1,139 @@ +/* + * Copyright 2019 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 io.cloudstate.tck + +import com.google.protobuf.ByteString +import io.cloudstate.protocol.action.ActionResponse +import io.cloudstate.protocol.event_sourced.{EventSourcedStreamIn, EventSourcedStreamOut} +import io.cloudstate.tck.model.eventlogeventing.{ + EmitEventRequest, + EventLogSubscriberModel, + EventSourcedEntityOneClient, + EventSourcedEntityTwoClient +} +import io.cloudstate.tck.model.eventlogeventing + +trait EventingTCK extends TCKSpec { + + object EventingTCKModel { + def eventLogSubscriptionTest(test: => Any): Unit = + testFor(EventLogSubscriberModel)(test) + } + + object EventingTCKProxy { + val eventLogEventingEventSourcedEntityOne: EventSourcedEntityOneClient = + eventlogeventing.EventSourcedEntityOneClient(client.settings)(client.system) + + val eventLogEventingEventSourcedEntityTwo: EventSourcedEntityTwoClient = + eventlogeventing.EventSourcedEntityTwoClient(client.settings)(client.system) + + def emitEventOne(id: String, step: eventlogeventing.ProcessStep.Step): Unit = + eventLogEventingEventSourcedEntityOne.emitEvent( + EmitEventRequest(id, + EmitEventRequest.Event + .EventOne(eventlogeventing.EventOne(Some(eventlogeventing.ProcessStep(step))))) + ) + + def emitReplyEventOne(id: String, message: String): Unit = + emitEventOne(id, eventlogeventing.ProcessStep.Step.Reply(eventlogeventing.Reply(message))) + + def emitForwardEventOne(id: String, message: String): Unit = + emitEventOne(id, eventlogeventing.ProcessStep.Step.Forward(eventlogeventing.Forward(message))) + + def verifyEventSourcedInitCommandReply(id: String): Unit = { + val connection = interceptor.expectEventSourcedConnection() + val init = connection.expectClientMessage[EventSourcedStreamIn.Message.Init] + init.value.serviceName must ===(eventlogeventing.EventSourcedEntityOne.name) + init.value.entityId must ===(id) + connection.expectClientMessage[EventSourcedStreamIn.Message.Command] + connection.expectServiceMessage[EventSourcedStreamOut.Message.Reply] + } + + def verifySubscriberCommandResponse(step: eventlogeventing.ProcessStep.Step): ActionResponse = { + val subscriberConnection = interceptor.expectActionUnaryConnection() + val eventOneIn = eventlogeventing.EventOne.parseFrom( + subscriberConnection.command.payload.fold(ByteString.EMPTY)(_.value).newCodedInput() + ) + eventOneIn.step must ===(Some(eventlogeventing.ProcessStep(step))) + subscriberConnection.expectResponse() + } + + def verifySubscriberReplyCommand(id: String, message: String): Unit = { + val response = + verifySubscriberCommandResponse(eventlogeventing.ProcessStep.Step.Reply(eventlogeventing.Reply(message))) + response.response.isReply must ===(true) + val reply = eventlogeventing.Response.parseFrom(response.response.reply.get.payload.get.value.newCodedInput()) + reply.id must ===(id) + reply.message must ===(message) + } + + def verifySubscriberForwardCommand(id: String, message: String): Unit = { + val response = + verifySubscriberCommandResponse(eventlogeventing.ProcessStep.Step.Forward(eventlogeventing.Forward(message))) + response.response.isForward must ===(true) + val subscriberConnection = interceptor.expectActionUnaryConnection() + subscriberConnection.command.name must ===("Effect") + } + + def terminate(): Unit = { + eventLogEventingEventSourcedEntityOne.close() + eventLogEventingEventSourcedEntityTwo.close() + } + } + + override def afterAll(): Unit = + try EventingTCKProxy.terminate() + finally super.afterAll() + + def verifyEventingProxy(): Unit = { + import EventingTCKModel._ + import EventingTCKProxy._ + + "consume an event" in eventLogSubscriptionTest { + emitReplyEventOne("eventlogeventing:1", "some message") + verifyEventSourcedInitCommandReply("eventlogeventing:1") + verifySubscriberReplyCommand("eventlogeventing:1", "some message") + } + + "forward a consumed event" in eventLogSubscriptionTest { + emitForwardEventOne("eventlogeventing:2", "some message") + verifyEventSourcedInitCommandReply("eventlogeventing:2") + verifySubscriberForwardCommand("eventlogeventing:2", "some message") + } + + "process json events" in eventLogSubscriptionTest { + eventLogEventingEventSourcedEntityTwo.emitJsonEvent( + eventlogeventing.JsonEvent("eventlogeventing:3", "some json message") + ) + + val connection = interceptor.expectEventSourcedConnection() + val init = connection.expectClientMessage[EventSourcedStreamIn.Message.Init] + init.value.serviceName must ===(eventlogeventing.EventSourcedEntityTwo.name) + init.value.entityId must ===("eventlogeventing:3") + connection.expectClientMessage[EventSourcedStreamIn.Message.Command] + val reply = connection.expectServiceMessage[EventSourcedStreamOut.Message.Reply] + reply.value.events must have size (1) + reply.value.events.head.typeUrl must startWith("json.cloudstate.io/") + + val subscriberConnection = interceptor.expectActionUnaryConnection() + val response = subscriberConnection.expectResponse() + val parsed = eventlogeventing.Response.parseFrom(response.response.reply.get.payload.get.value.newCodedInput()) + parsed.id must ===("eventlogeventing:3") + parsed.message must ===("some json message") + } + } +} diff --git a/tck/src/main/scala/io/cloudstate/tck/ProxyTCK.scala b/tck/src/main/scala/io/cloudstate/tck/ProxyTCK.scala new file mode 100644 index 000000000..f18233b47 --- /dev/null +++ b/tck/src/main/scala/io/cloudstate/tck/ProxyTCK.scala @@ -0,0 +1,68 @@ +/* + * Copyright 2019 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 io.cloudstate.tck + +import io.cloudstate.protocol.action.ActionProtocol +import io.cloudstate.protocol.crdt.Crdt +import io.cloudstate.protocol.event_sourced.EventSourced +import io.cloudstate.protocol.value_entity.ValueEntity + +trait ProxyTCK extends TCKSpec { + + def verifyDiscovery(): Unit = + "verify proxy info and entity discovery" in { + proxyInfo.protocolMajorVersion mustBe 0 + proxyInfo.protocolMinorVersion mustBe 2 + + proxyInfo.supportedEntityTypes must contain theSameElementsAs Seq( + ActionProtocol.name, + ValueEntity.name, + EventSourced.name, + Crdt.name + ) + + discoveredServices.size mustBe serviceNames.size + } + + def verifyServerReflection(): Unit = + "verify that the proxy supports server reflection" in { + import grpc.reflection.v1alpha.reflection._ + import ServerReflectionRequest.{MessageRequest => In} + import ServerReflectionResponse.{MessageResponse => Out} + + val expectedServices = Seq(ServerReflection.name) ++ serviceNames.sorted + + val connection = client.serverReflection.connect() + + connection.sendAndExpect( + In.ListServices(""), + Out.ListServicesResponse(ListServiceResponse(expectedServices.map(s => ServiceResponse(s)))) + ) + + connection.sendAndExpect( + In.ListServices("nonsense.blabla."), + Out.ListServicesResponse(ListServiceResponse(expectedServices.map(s => ServiceResponse(s)))) + ) + + connection.sendAndExpect( + In.FileContainingSymbol("nonsense.blabla.Void"), + Out.FileDescriptorResponse(FileDescriptorResponse(Nil)) + ) + + connection.close() + } +} diff --git a/tck/src/main/scala/io/cloudstate/tck/ShoppingCartTCK.scala b/tck/src/main/scala/io/cloudstate/tck/ShoppingCartTCK.scala new file mode 100644 index 000000000..298072265 --- /dev/null +++ b/tck/src/main/scala/io/cloudstate/tck/ShoppingCartTCK.scala @@ -0,0 +1,299 @@ +/* + * Copyright 2019 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 io.cloudstate.tck + +import io.cloudstate.testkit.InterceptService +import com.example.valueentity.shoppingcart.shoppingcart._ +import com.example.valueentity.shoppingcart.persistence.domain +import io.cloudstate.protocol.value_entity.{ValueEntity, ValueEntityStreamOut} +import io.cloudstate.testkit.valueentity.{InterceptValueEntityService, ValueEntityMessages} +import io.grpc.StatusRuntimeException +import org.scalatest.MustMatchers +import scala.collection.mutable + +trait ShoppingCartTCK extends TCKSpec { + + object ValueEntityShoppingCart { + import ValueEntityMessages._ + import ValueEntityShoppingCartVerifier._ + + val Service: String = ShoppingCart.name + + private val shoppingCartClient = ShoppingCartClient(client.settings)(client.system) + + def terminate(): Unit = shoppingCartClient.close() + + def verifyGetInitialEmptyCart(session: ValueEntityShoppingCartVerifier, cartId: String): Unit = { + shoppingCartClient.getCart(GetShoppingCart(cartId)).futureValue mustBe Cart() + session.verifyConnection() + session.verifyGetInitialEmptyCart(cartId) + } + + def verifyGetCart(session: ValueEntityShoppingCartVerifier, cartId: String, expected: Item*): Unit = { + val expectedCart = shoppingCart(expected: _*) + shoppingCartClient.getCart(GetShoppingCart(cartId)).futureValue mustBe expectedCart + session.verifyGetCart(cartId, expectedCart) + } + + def verifyAddItem(session: ValueEntityShoppingCartVerifier, + cartId: String, + item: Item, + expected: CartValue): Unit = { + val addLineItem = AddLineItem(cartId, item.id, item.name, item.quantity) + shoppingCartClient.addItem(addLineItem).futureValue mustBe EmptyScalaMessage + session.verifyAddItem(cartId, item, expected) + } + + def verifyRemoveItem(session: ValueEntityShoppingCartVerifier, + cartId: String, + itemId: String, + expected: CartValue): Unit = { + val removeLineItem = RemoveLineItem(cartId, itemId) + shoppingCartClient.removeItem(removeLineItem).futureValue mustBe EmptyScalaMessage + session.verifyRemoveItem(cartId, itemId, expected) + } + + def verifyAddItemFailure(session: ValueEntityShoppingCartVerifier, cartId: String, item: Item): Unit = { + val addLineItem = AddLineItem(cartId, item.id, item.name, item.quantity) + val error = shoppingCartClient.addItem(addLineItem).failed.futureValue + error mustBe a[StatusRuntimeException] + val description = error.asInstanceOf[StatusRuntimeException].getStatus.getDescription + session.verifyAddItemFailure(cartId, item, description) + } + + def verifyRemoveItemFailure(session: ValueEntityShoppingCartVerifier, cartId: String, itemId: String): Unit = { + val removeLineItem = RemoveLineItem(cartId, itemId) + val error = shoppingCartClient.removeItem(removeLineItem).failed.futureValue + error mustBe a[StatusRuntimeException] + val description = error.asInstanceOf[StatusRuntimeException].getStatus.getDescription + session.verifyRemoveItemFailure(cartId, itemId, description) + } + } + + override def afterAll(): Unit = + try ValueEntityShoppingCart.terminate() + finally super.afterAll() + + def verifyShoppingCart(): Unit = { + import ValueEntityShoppingCart._ + import ValueEntityShoppingCartVerifier._ + + "verify shopping cart discovery" in testFor(ShoppingCart) { + discoveredServices must contain("ShoppingCart") + entity(ValueEntityShoppingCart.Service).value.entityType mustBe ValueEntity.name + entity(ValueEntityShoppingCart.Service).value.persistenceId must not be empty + } + + "verify get cart, add item, remove item, and failures" in testFor(ShoppingCart) { + val session = shoppingCartSession(interceptor) + verifyGetInitialEmptyCart(session, "cart:1") // initial empty state + + // add the first product and pass the expected state + verifyAddItem(session, "cart:1", Item("product:1", "Product1", 1), CartValue(Item("product:1", "Product1", 1))) + + // add the second product and pass the expected state + verifyAddItem( + session, + "cart:1", + Item("product:2", "Product2", 2), + CartValue(Item("product:1", "Product1", 1), Item("product:2", "Product2", 2)) + ) + + // increase first product and pass the expected state + verifyAddItem( + session, + "cart:1", + Item("product:1", "Product1", 11), + CartValue(Item("product:1", "Product1", 12), Item("product:2", "Product2", 2)) + ) + + // increase second product and pass the expected state + verifyAddItem( + session, + "cart:1", + Item("product:2", "Product2", 31), + CartValue(Item("product:1", "Product1", 12), Item("product:2", "Product2", 33)) + ) + + verifyGetCart(session, "cart:1", Item("product:1", "Product1", 12), Item("product:2", "Product2", 33)) // check state + + // remove first product and pass the expected state + verifyRemoveItem(session, "cart:1", "product:1", CartValue(Item("product:2", "Product2", 33))) + verifyAddItemFailure(session, "cart:1", Item("product:2", "Product2", -7)) // add negative quantity + verifyAddItemFailure(session, "cart:1", Item("product:1", "Product1", 0)) // add zero quantity + verifyRemoveItemFailure(session, "cart:1", "product:1") // remove non-existing product + verifyGetCart(session, "cart:1", Item("product:2", "Product2", 33)) // check final state + } + + "verify the HTTP API for shopping cart" in testFor(ShoppingCart) { + import ValueEntityShoppingCartVerifier._ + + def checkHttpRequest(path: String, body: String = null)(expected: => String): Unit = { + val response = client.http.request(path, body) + val expectedResponse = expected + response.futureValue mustBe expectedResponse + } + + val session = shoppingCartSession(interceptor) + + checkHttpRequest("ve/carts/foo") { + session.verifyConnection() + session.verifyGetInitialEmptyCart("foo") + """{"items":[]}""" + } + + checkHttpRequest("ve/cart/foo/items/add", """{"productId": "A14362347", "name": "Deluxe", "quantity": 5}""") { + session.verifyAddItem("foo", Item("A14362347", "Deluxe", 5), CartValue(Item("A14362347", "Deluxe", 5))) + "{}" + } + + checkHttpRequest("ve/cart/foo/items/add", """{"productId": "B14623482", "name": "Basic", "quantity": 1}""") { + session.verifyAddItem("foo", + Item("B14623482", "Basic", 1), + CartValue(Item("A14362347", "Deluxe", 5), Item("B14623482", "Basic", 1))) + "{}" + } + + checkHttpRequest("ve/cart/foo/items/add", """{"productId": "A14362347", "name": "Deluxe", "quantity": 2}""") { + session.verifyAddItem("foo", + Item("A14362347", "Deluxe", 2), + CartValue(Item("A14362347", "Deluxe", 7), Item("B14623482", "Basic", 1))) + "{}" + } + + checkHttpRequest("ve/carts/foo") { + session.verifyGetCart("foo", shoppingCart(Item("A14362347", "Deluxe", 7), Item("B14623482", "Basic", 1))) + """{"items":[{"productId":"A14362347","name":"Deluxe","quantity":7},{"productId":"B14623482","name":"Basic","quantity":1}]}""" + } + + checkHttpRequest("ve/carts/foo/items") { + session.verifyGetCart("foo", shoppingCart(Item("A14362347", "Deluxe", 7), Item("B14623482", "Basic", 1))) + """[{"productId":"A14362347","name":"Deluxe","quantity":7.0},{"productId":"B14623482","name":"Basic","quantity":1.0}]""" + } + + checkHttpRequest("ve/cart/foo/items/A14362347/remove", "") { + session.verifyRemoveItem("foo", "A14362347", CartValue(Item("B14623482", "Basic", 1))) + "{}" + } + + checkHttpRequest("ve/carts/foo") { + session.verifyGetCart("foo", shoppingCart(Item("B14623482", "Basic", 1))) + """{"items":[{"productId":"B14623482","name":"Basic","quantity":1}]}""" + } + + checkHttpRequest("ve/carts/foo/items") { + session.verifyGetCart("foo", shoppingCart(Item("B14623482", "Basic", 1))) + """[{"productId":"B14623482","name":"Basic","quantity":1.0}]""" + } + + checkHttpRequest("ve/carts/foo/remove", """{"userId": "foo"}""") { + session.verifyRemoveCart("foo") + "{}" + } + + checkHttpRequest("ve/carts/foo") { + session.verifyGetCart("foo", shoppingCart()) + """{"items":[]}""" + } + } + } +} + +object ValueEntityShoppingCartVerifier { + case class Item(id: String, name: String, quantity: Int) + case class CartValue(items: Item*) + + def shoppingCartSession(interceptor: InterceptService): ValueEntityShoppingCartVerifier = + new ValueEntityShoppingCartVerifier(interceptor) + + def shoppingCart(items: Item*): Cart = Cart(items.map(i => LineItem(i.id, i.name, i.quantity))) + + def domainShoppingCart(cart: CartValue): domain.Cart = + domain.Cart(cart.items.map(i => domain.LineItem(i.id, i.name, i.quantity))) +} + +class ValueEntityShoppingCartVerifier(interceptor: InterceptService) extends MustMatchers { + import ValueEntityMessages._ + import ValueEntityShoppingCartVerifier._ + + private val commandIds = mutable.Map.empty[String, Long] + private var connection: InterceptValueEntityService.Connection = _ + + private def nextCommandId(cartId: String): Long = commandIds.updateWith(cartId)(_.map(_ + 1).orElse(Some(1L))).get + + def verifyConnection(): Unit = connection = interceptor.expectValueBasedConnection() + + def verifyGetInitialEmptyCart(cartId: String): Unit = { + val commandId = nextCommandId(cartId) + connection.expectClient(init(ShoppingCart.name, cartId)) + connection.expectClient(command(commandId, cartId, "GetCart", GetShoppingCart(cartId))) + connection.expectService(reply(commandId, Cart())) + connection.expectNoInteraction() + } + + def verifyGetCart(cartId: String, expected: Cart): Unit = { + val commandId = nextCommandId(cartId) + connection.expectClient(command(commandId, cartId, "GetCart", GetShoppingCart(cartId))) + connection.expectService(reply(commandId, expected)) + connection.expectNoInteraction() + } + + def verifyAddItem(cartId: String, item: Item, expected: CartValue): Unit = { + val commandId = nextCommandId(cartId) + val addLineItem = AddLineItem(cartId, item.id, item.name, item.quantity) + val cartUpdated = domainShoppingCart(expected) + connection.expectClient(command(commandId, cartId, "AddItem", addLineItem)) + val replied = connection.expectServiceMessage[ValueEntityStreamOut.Message.Reply] + replied mustBe reply(commandId, EmptyScalaMessage, update(cartUpdated)) + connection.expectNoInteraction() + } + + def verifyRemoveItem(cartId: String, itemId: String, expected: CartValue): Unit = { + val commandId = nextCommandId(cartId) + val removeLineItem = RemoveLineItem(cartId, itemId) + val cartUpdated = domainShoppingCart(expected) + connection.expectClient(command(commandId, cartId, "RemoveItem", removeLineItem)) + val replied = connection.expectServiceMessage[ValueEntityStreamOut.Message.Reply] + replied mustBe reply(commandId, EmptyScalaMessage, update(cartUpdated)) + connection.expectNoInteraction() + } + + def verifyRemoveCart(cartId: String): Unit = { + val commandId = nextCommandId(cartId) + val removeCart = RemoveShoppingCart(cartId) + connection.expectClient(command(commandId, cartId, "RemoveCart", removeCart)) + val replied = connection.expectServiceMessage[ValueEntityStreamOut.Message.Reply] + replied mustBe reply(commandId, EmptyScalaMessage, delete()) + connection.expectNoInteraction() + } + + def verifyAddItemFailure(cartId: String, item: Item, failure: String): Unit = { + val commandId = nextCommandId(cartId) + val addLineItem = AddLineItem(cartId, item.id, item.name, item.quantity) + connection.expectClient(command(commandId, cartId, "AddItem", addLineItem)) + connection.expectService(actionFailure(commandId, failure)) + connection.expectNoInteraction() + } + + def verifyRemoveItemFailure(cartId: String, itemId: String, failure: String): Unit = { + val commandId = nextCommandId(cartId) + val removeLineItem = RemoveLineItem(cartId, itemId) + connection.expectClient(command(commandId, cartId, "RemoveItem", removeLineItem)) + connection.expectService(actionFailure(commandId, failure)) + connection.expectNoInteraction() + } +} diff --git a/tck/src/main/scala/io/cloudstate/tck/TCKSpec.scala b/tck/src/main/scala/io/cloudstate/tck/TCKSpec.scala new file mode 100644 index 000000000..d0e094cc3 --- /dev/null +++ b/tck/src/main/scala/io/cloudstate/tck/TCKSpec.scala @@ -0,0 +1,116 @@ +/* + * Copyright 2019 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 io.cloudstate.tck + +import akka.grpc.ServiceDescription +import akka.testkit.TestKit +import com.google.protobuf.DescriptorProtos +import com.typesafe.config.{Config, ConfigFactory} +import io.cloudstate.protocol.entity.{Entity, EntitySpec, ProxyInfo} +import io.cloudstate.testkit.InterceptService.InterceptorSettings +import io.cloudstate.testkit.{InterceptService, ServiceAddress, TestClient, TestProtocol} +import org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach, MustMatchers, OptionValues, WordSpec} +import org.scalatest.concurrent.ScalaFutures + +import scala.concurrent.duration._ +import scala.jdk.CollectionConverters._ + +object TCKSpec { + final case class Settings(tck: ServiceAddress, proxy: ServiceAddress, service: ServiceAddress) + + object Settings { + def loadFromConfig(): Settings = fromConfig(ConfigFactory.load()) + + def fromConfig(config: Config): Settings = { + val tckConfig = config.getConfig("cloudstate.tck") + Settings( + ServiceAddress(tckConfig.getString("hostname"), tckConfig.getInt("port")), + ServiceAddress(tckConfig.getString("proxy.hostname"), tckConfig.getInt("proxy.port")), + ServiceAddress(tckConfig.getString("service.hostname"), tckConfig.getInt("service.port")) + ) + } + } +} + +trait TCKSpec + extends WordSpec + with MustMatchers + with BeforeAndAfterAll + with BeforeAndAfterEach + with ScalaFutures + with OptionValues { + + def settings: TCKSpec.Settings + + override implicit val patienceConfig: PatienceConfig = PatienceConfig(timeout = 3.seconds, interval = 100.millis) + + val client: TestClient = TestClient(settings.proxy.host, settings.proxy.port) + + val protocol: TestProtocol = TestProtocol(settings.service.host, settings.service.port) + + val interceptor: InterceptService = new InterceptService(InterceptorSettings(settings.tck, settings.service)) + + @volatile private[this] var _entitySpec: EntitySpec = EntitySpec() + @volatile private[this] var _proxyInfo: ProxyInfo = ProxyInfo() + + override def beforeAll(): Unit = { + start() + discover() + super.beforeAll() + } + + override def afterEach(): Unit = { + super.afterEach() + interceptor.verifyNoMoreInteractions() + } + + override def afterAll(): Unit = + try super.afterAll() + finally stop() + + def start(): Unit = interceptor.start() + + def entitySpec: EntitySpec = _entitySpec + + def proxyInfo: ProxyInfo = _proxyInfo + + def discover(): Unit = { + TestKit.awaitCond(client.http.probe(), max = 10.seconds) + val discovery = interceptor.expectEntityDiscovery() + _entitySpec = discovery.expectEntitySpec() + _proxyInfo = discovery.expectProxyInfo() + } + + def stop(): Unit = + try client.terminate() + finally try protocol.terminate() + finally interceptor.terminate() + + def entity(name: String): Option[Entity] = entitySpec.entities.find(_.serviceName == name) + + lazy val serviceNames: Seq[String] = entitySpec.entities.map(_.serviceName) + + lazy val discoveredServices: Seq[String] = { + val descriptorSet = DescriptorProtos.FileDescriptorSet.parseFrom(entitySpec.proto) + descriptorSet.getFileList.asScala.flatMap(_.getServiceList.asScala.map(_.getName)).toSeq + } + + def testFor(services: ServiceDescription*)(test: => Any): Unit = { + val enabled = services.map(_.name).forall(serviceNames.contains) + if (enabled) test else pending + } +} diff --git a/tck/src/main/scala/io/cloudstate/tck/ValueEntityShoppingCartVerifier.scala b/tck/src/main/scala/io/cloudstate/tck/ValueEntityShoppingCartVerifier.scala deleted file mode 100644 index 8ac9cafef..000000000 --- a/tck/src/main/scala/io/cloudstate/tck/ValueEntityShoppingCartVerifier.scala +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Copyright 2019 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 io.cloudstate.tck - -import io.cloudstate.testkit.InterceptService -import com.example.valueentity.shoppingcart.shoppingcart.{Cart => ValueEntityCart} -import com.example.valueentity.shoppingcart.shoppingcart._ -import com.example.valueentity.shoppingcart.persistence.domain -import io.cloudstate.protocol.value_entity.ValueEntityStreamOut -import io.cloudstate.testkit.valueentity.{InterceptValueEntityService, ValueEntityMessages} -import org.scalatest.MustMatchers - -import scala.collection.mutable - -object ValueEntityShoppingCartVerifier { - case class Item(id: String, name: String, quantity: Int) - case class Cart(items: Item*) - - def shoppingCartSession(interceptor: InterceptService): ValueEntityShoppingCartVerifier = - new ValueEntityShoppingCartVerifier(interceptor) - - def shoppingCart(items: Item*): ValueEntityCart = ValueEntityCart(items.map(i => LineItem(i.id, i.name, i.quantity))) - - def domainShoppingCart(cart: Cart): domain.Cart = - domain.Cart(cart.items.map(i => domain.LineItem(i.id, i.name, i.quantity))) -} - -class ValueEntityShoppingCartVerifier(interceptor: InterceptService) extends MustMatchers { - import ValueEntityMessages._ - import ValueEntityShoppingCartVerifier._ - - private val commandIds = mutable.Map.empty[String, Long] - private var connection: InterceptValueEntityService.Connection = _ - - private def nextCommandId(cartId: String): Long = commandIds.updateWith(cartId)(_.map(_ + 1).orElse(Some(1L))).get - - def verifyConnection(): Unit = connection = interceptor.expectValueBasedConnection() - - def verifyGetInitialEmptyCart(cartId: String): Unit = { - val commandId = nextCommandId(cartId) - connection.expectClient(init(ShoppingCart.name, cartId)) - connection.expectClient(command(commandId, cartId, "GetCart", GetShoppingCart(cartId))) - connection.expectService(reply(commandId, ValueEntityCart())) - connection.expectNoInteraction() - } - - def verifyGetCart(cartId: String, expected: ValueEntityCart): Unit = { - val commandId = nextCommandId(cartId) - connection.expectClient(command(commandId, cartId, "GetCart", GetShoppingCart(cartId))) - connection.expectService(reply(commandId, expected)) - connection.expectNoInteraction() - } - - def verifyAddItem(cartId: String, item: Item, expected: Cart): Unit = { - val commandId = nextCommandId(cartId) - val addLineItem = AddLineItem(cartId, item.id, item.name, item.quantity) - val cartUpdated = domainShoppingCart(expected) - connection.expectClient(command(commandId, cartId, "AddItem", addLineItem)) - val replied = connection.expectServiceMessage[ValueEntityStreamOut.Message.Reply] - replied mustBe reply(commandId, EmptyScalaMessage, update(cartUpdated)) - connection.expectNoInteraction() - } - - def verifyRemoveItem(cartId: String, itemId: String, expected: Cart): Unit = { - val commandId = nextCommandId(cartId) - val removeLineItem = RemoveLineItem(cartId, itemId) - val cartUpdated = domainShoppingCart(expected) - connection.expectClient(command(commandId, cartId, "RemoveItem", removeLineItem)) - val replied = connection.expectServiceMessage[ValueEntityStreamOut.Message.Reply] - replied mustBe reply(commandId, EmptyScalaMessage, update(cartUpdated)) - connection.expectNoInteraction() - } - - def verifyRemoveCart(cartId: String): Unit = { - val commandId = nextCommandId(cartId) - val removeCart = RemoveShoppingCart(cartId) - connection.expectClient(command(commandId, cartId, "RemoveCart", removeCart)) - val replied = connection.expectServiceMessage[ValueEntityStreamOut.Message.Reply] - replied mustBe reply(commandId, EmptyScalaMessage, delete()) - connection.expectNoInteraction() - } - - def verifyAddItemFailure(cartId: String, item: Item, failure: String): Unit = { - val commandId = nextCommandId(cartId) - val addLineItem = AddLineItem(cartId, item.id, item.name, item.quantity) - connection.expectClient(command(commandId, cartId, "AddItem", addLineItem)) - connection.expectService(actionFailure(commandId, failure)) - connection.expectNoInteraction() - } - - def verifyRemoveItemFailure(cartId: String, itemId: String, failure: String): Unit = { - val commandId = nextCommandId(cartId) - val removeLineItem = RemoveLineItem(cartId, itemId) - connection.expectClient(command(commandId, cartId, "RemoveItem", removeLineItem)) - connection.expectService(actionFailure(commandId, failure)) - connection.expectNoInteraction() - } -} diff --git a/testkit/src/main/scala/io/cloudstate/testkit/InterceptService.scala b/testkit/src/main/scala/io/cloudstate/testkit/InterceptService.scala index dc5cb17d7..6079dec0e 100644 --- a/testkit/src/main/scala/io/cloudstate/testkit/InterceptService.scala +++ b/testkit/src/main/scala/io/cloudstate/testkit/InterceptService.scala @@ -43,19 +43,18 @@ final class InterceptService(settings: InterceptorSettings) { private val valueBased = new InterceptValueEntityService(context) private val action = new InterceptActionService(context) - import context.system - - entityDiscovery.expectOnline(60.seconds) - - Await.result( - Http().bindAndHandleAsync( - handler = entityDiscovery.handler orElse crdt.handler orElse eventSourced.handler orElse valueBased.handler - orElse action.handler, - interface = settings.bind.host, - port = settings.bind.port - ), - 10.seconds - ) + def start(): Unit = { + import context.system + entityDiscovery.expectOnline(60.seconds) + Await.result( + Http().bindAndHandleAsync( + handler = entityDiscovery.handler orElse crdt.handler orElse eventSourced.handler orElse valueBased.handler orElse action.handler, + interface = settings.bind.host, + port = settings.bind.port + ), + 10.seconds + ) + } def expectEntityDiscovery(): InterceptEntityDiscovery.Discovery = entityDiscovery.expectDiscovery() @@ -83,8 +82,8 @@ final class InterceptService(settings: InterceptorSettings) { crdt.terminate() eventSourced.terminate() valueBased.terminate() - context.terminate() action.terminate() + context.terminate() } } diff --git a/testkit/src/main/scala/io/cloudstate/testkit/TestClient.scala b/testkit/src/main/scala/io/cloudstate/testkit/TestClient.scala index 09e2deb9d..61aeeccaf 100644 --- a/testkit/src/main/scala/io/cloudstate/testkit/TestClient.scala +++ b/testkit/src/main/scala/io/cloudstate/testkit/TestClient.scala @@ -34,6 +34,8 @@ final class TestClient(host: String, port: Int) { def settings: GrpcClientSettings = context.clientSettings + def system: ActorSystem = context.system + def terminate(): Unit = { http.terminate() serverReflection.terminate() From 8810a458b4dd897a3c35de5bc9332f7d5a85fde3 Mon Sep 17 00:00:00 2001 From: Peter Vlugter Date: Fri, 18 Dec 2020 11:22:43 +1300 Subject: [PATCH 2/7] Add proxy intercept tests using TCK models --- .../javasupport/tck/JavaSupportTck.java | 14 ++ .../tck/model/action/ActionTwoBehavior.java | 2 +- .../tck/model/crdt/CrdtConfiguredEntity.java | 29 +++ .../tck/model/crdt/CrdtTwoEntity.java | 10 +- .../EventSourcedConfiguredEntity.java | 29 +++ .../eventsourced/EventSourcedTwoEntity.java | 3 +- .../tck/cloudstate/tck/model/action.proto | 11 +- protocols/tck/cloudstate/tck/model/crdt.proto | 21 +- .../cloudstate/tck/model/eventsourced.proto | 21 +- .../cloudstate/tck/model/valueentity.proto | 10 +- .../scala/io/cloudstate/tck/ActionTCK.scala | 200 +++++++++++++++++ .../io/cloudstate/tck/CloudstateTCK.scala | 4 + .../io/cloudstate/tck/CrdtEntityTCK.scala | 204 ++++++++++++++++-- .../scala/io/cloudstate/tck/EntityTCK.scala | 148 ++++++++++++- .../tck/EventSourcedEntityTCK.scala | 200 ++++++++++++++++- .../tck/EventSourcedShoppingCartTCK.scala | 2 +- .../scala/io/cloudstate/tck/EventingTCK.scala | 4 +- .../io/cloudstate/tck/ShoppingCartTCK.scala | 2 +- .../cloudstate/testkit/InterceptService.scala | 42 ++-- .../action/InterceptActionService.scala | 67 +++++- .../testkit/crdt/InterceptCrdtService.scala | 5 + .../InterceptEventSourcedService.scala | 10 + .../testkit/http/TestHttpClient.scala | 12 +- .../InterceptValueEntityService.scala | 11 +- .../TestValueEntityServiceClient.scala | 76 ------- 25 files changed, 982 insertions(+), 155 deletions(-) create mode 100644 java-support/tck/src/main/java/io/cloudstate/javasupport/tck/model/crdt/CrdtConfiguredEntity.java create mode 100644 java-support/tck/src/main/java/io/cloudstate/javasupport/tck/model/eventsourced/EventSourcedConfiguredEntity.java delete mode 100644 testkit/src/main/scala/io/cloudstate/testkit/valueentity/TestValueEntityServiceClient.scala diff --git a/java-support/tck/src/main/java/io/cloudstate/javasupport/tck/JavaSupportTck.java b/java-support/tck/src/main/java/io/cloudstate/javasupport/tck/JavaSupportTck.java index bf07f380e..277fe88f6 100644 --- a/java-support/tck/src/main/java/io/cloudstate/javasupport/tck/JavaSupportTck.java +++ b/java-support/tck/src/main/java/io/cloudstate/javasupport/tck/JavaSupportTck.java @@ -19,8 +19,12 @@ import com.example.valueentity.shoppingcart.Shoppingcart; import io.cloudstate.javasupport.CloudState; import io.cloudstate.javasupport.PassivationStrategy; +import io.cloudstate.javasupport.crdt.CrdtEntityOptions; import io.cloudstate.javasupport.entity.EntityOptions; +import io.cloudstate.javasupport.eventsourced.EventSourcedEntityOptions; +import io.cloudstate.javasupport.tck.model.crdt.CrdtConfiguredEntity; import io.cloudstate.javasupport.tck.model.eventlogeventing.EventLogSubscriber; +import io.cloudstate.javasupport.tck.model.eventsourced.EventSourcedConfiguredEntity; import io.cloudstate.javasupport.tck.model.valuebased.ValueEntityConfiguredEntity; import io.cloudstate.javasupport.tck.model.valuebased.ValueEntityTckModelEntity; import io.cloudstate.javasupport.tck.model.valuebased.ValueEntityTwoEntity; @@ -71,6 +75,11 @@ public static final void main(String[] args) throws Exception { Crdt.getDescriptor().findServiceByName("CrdtTckModel"), Crdt.getDescriptor()) .registerCrdtEntity(CrdtTwoEntity.class, Crdt.getDescriptor().findServiceByName("CrdtTwo")) + .registerCrdtEntity( + CrdtConfiguredEntity.class, + Crdt.getDescriptor().findServiceByName("CrdtConfigured"), + CrdtEntityOptions.defaults() // required timeout of 100 millis for TCK tests + .withPassivationStrategy(PassivationStrategy.timeout(Duration.ofMillis(100)))) .registerEventSourcedEntity( EventSourcedTckModelEntity.class, Eventsourced.getDescriptor().findServiceByName("EventSourcedTckModel"), @@ -78,6 +87,11 @@ public static final void main(String[] args) throws Exception { .registerEventSourcedEntity( EventSourcedTwoEntity.class, Eventsourced.getDescriptor().findServiceByName("EventSourcedTwo")) + .registerEventSourcedEntity( + EventSourcedConfiguredEntity.class, + Eventsourced.getDescriptor().findServiceByName("EventSourcedConfigured"), + EventSourcedEntityOptions.defaults() // required timeout of 100 millis for TCK tests + .withPassivationStrategy(PassivationStrategy.timeout(Duration.ofMillis(100)))) .registerAction( new EventLogSubscriber(), Eventlogeventing.getDescriptor().findServiceByName("EventLogSubscriberModel")) diff --git a/java-support/tck/src/main/java/io/cloudstate/javasupport/tck/model/action/ActionTwoBehavior.java b/java-support/tck/src/main/java/io/cloudstate/javasupport/tck/model/action/ActionTwoBehavior.java index a34b140cf..6dc162ca1 100644 --- a/java-support/tck/src/main/java/io/cloudstate/javasupport/tck/model/action/ActionTwoBehavior.java +++ b/java-support/tck/src/main/java/io/cloudstate/javasupport/tck/model/action/ActionTwoBehavior.java @@ -25,6 +25,6 @@ public ActionTwoBehavior() {} @CallHandler public Response call(OtherRequest request) { - return Response.newBuilder().build(); + return Response.getDefaultInstance(); } } diff --git a/java-support/tck/src/main/java/io/cloudstate/javasupport/tck/model/crdt/CrdtConfiguredEntity.java b/java-support/tck/src/main/java/io/cloudstate/javasupport/tck/model/crdt/CrdtConfiguredEntity.java new file mode 100644 index 000000000..17bde3611 --- /dev/null +++ b/java-support/tck/src/main/java/io/cloudstate/javasupport/tck/model/crdt/CrdtConfiguredEntity.java @@ -0,0 +1,29 @@ +/* + * Copyright 2019 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 io.cloudstate.javasupport.tck.model.crdt; + +import io.cloudstate.javasupport.crdt.*; +import io.cloudstate.tck.model.Crdt.*; + +@CrdtEntity +public class CrdtConfiguredEntity { + + @CommandHandler + public Response call(Request request) { + return Response.getDefaultInstance(); + } +} diff --git a/java-support/tck/src/main/java/io/cloudstate/javasupport/tck/model/crdt/CrdtTwoEntity.java b/java-support/tck/src/main/java/io/cloudstate/javasupport/tck/model/crdt/CrdtTwoEntity.java index 49589f886..ecd515471 100644 --- a/java-support/tck/src/main/java/io/cloudstate/javasupport/tck/model/crdt/CrdtTwoEntity.java +++ b/java-support/tck/src/main/java/io/cloudstate/javasupport/tck/model/crdt/CrdtTwoEntity.java @@ -21,10 +21,14 @@ @CrdtEntity public class CrdtTwoEntity { - public CrdtTwoEntity() {} + // create a CRDT to be able to call delete + public CrdtTwoEntity(GCounter counter) {} @CommandHandler - public Response call(Request request) { - return Response.newBuilder().build(); + public Response call(Request request, CommandContext context) { + for (RequestAction action : request.getActionsList()) { + if (action.hasDelete()) context.delete(); + } + return Response.getDefaultInstance(); } } diff --git a/java-support/tck/src/main/java/io/cloudstate/javasupport/tck/model/eventsourced/EventSourcedConfiguredEntity.java b/java-support/tck/src/main/java/io/cloudstate/javasupport/tck/model/eventsourced/EventSourcedConfiguredEntity.java new file mode 100644 index 000000000..9f3de76a3 --- /dev/null +++ b/java-support/tck/src/main/java/io/cloudstate/javasupport/tck/model/eventsourced/EventSourcedConfiguredEntity.java @@ -0,0 +1,29 @@ +/* + * Copyright 2019 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 io.cloudstate.javasupport.tck.model.eventsourced; + +import io.cloudstate.javasupport.eventsourced.*; +import io.cloudstate.tck.model.Eventsourced.*; + +@EventSourcedEntity(persistenceId = "event-sourced-configured") +public class EventSourcedConfiguredEntity { + + @CommandHandler + public Response call(Request request) { + return Response.getDefaultInstance(); + } +} diff --git a/java-support/tck/src/main/java/io/cloudstate/javasupport/tck/model/eventsourced/EventSourcedTwoEntity.java b/java-support/tck/src/main/java/io/cloudstate/javasupport/tck/model/eventsourced/EventSourcedTwoEntity.java index 9b556a32b..74c3e34c0 100644 --- a/java-support/tck/src/main/java/io/cloudstate/javasupport/tck/model/eventsourced/EventSourcedTwoEntity.java +++ b/java-support/tck/src/main/java/io/cloudstate/javasupport/tck/model/eventsourced/EventSourcedTwoEntity.java @@ -21,10 +21,9 @@ @EventSourcedEntity public class EventSourcedTwoEntity { - public EventSourcedTwoEntity() {} @CommandHandler public Response call(Request request) { - return Response.newBuilder().build(); + return Response.getDefaultInstance(); } } diff --git a/protocols/tck/cloudstate/tck/model/action.proto b/protocols/tck/cloudstate/tck/model/action.proto index 81102d8eb..ad6de2708 100644 --- a/protocols/tck/cloudstate/tck/model/action.proto +++ b/protocols/tck/cloudstate/tck/model/action.proto @@ -20,6 +20,8 @@ syntax = "proto3"; package cloudstate.tck.model.action; +import "google/api/annotations.proto"; + option java_package = "io.cloudstate.tck.model"; option go_package = "github.com/cloudstateio/go-support/tck/action;action"; @@ -41,7 +43,12 @@ option go_package = "github.com/cloudstateio/go-support/tck/action;action"; // - Forwarding and side effects must always be made to the second service `ActionTwo`. // service ActionTckModel { - rpc ProcessUnary(Request) returns (Response); + rpc ProcessUnary(Request) returns (Response) { + option (google.api.http) = { + post: "/tck/model/action/unary", + body: "*" + }; + } rpc ProcessStreamedIn(stream Request) returns (Response); rpc ProcessStreamedOut(Request) returns (stream Response); rpc ProcessStreamed(stream Request) returns (stream Response); @@ -49,7 +56,7 @@ service ActionTckModel { // // The `ActionTwo` service is only for verifying forwards and side effects. -// The `Call` method is not required to do anything, and may simply return an empty `Response` message. +// The `Call` method is not required to do anything, and must return an empty `Response` message. // service ActionTwo { rpc Call(OtherRequest) returns (Response); diff --git a/protocols/tck/cloudstate/tck/model/crdt.proto b/protocols/tck/cloudstate/tck/model/crdt.proto index f5ea22928..d1f4889bb 100644 --- a/protocols/tck/cloudstate/tck/model/crdt.proto +++ b/protocols/tck/cloudstate/tck/model/crdt.proto @@ -21,6 +21,7 @@ syntax = "proto3"; package cloudstate.tck.model.crdt; import "cloudstate/entity_key.proto"; +import "google/api/annotations.proto"; option java_package = "io.cloudstate.tck.model"; option go_package = "github.com/cloudstateio/go-support/tck/crdt;crdt"; @@ -42,18 +43,34 @@ option go_package = "github.com/cloudstateio/go-support/tck/crdt;crdt"; // - The `ProcessStreamed` method must stream the current state in a `Response`, on any changes. // - A `StreamedRequest` message may have an end state, an update to apply on stream cancellation, or side effects. service CrdtTckModel { - rpc Process(Request) returns (Response); + rpc Process(Request) returns (Response) { + option (google.api.http) = { + post: "/tck/model/crdt/{id}", + body: "*" + }; + } rpc ProcessStreamed(StreamedRequest) returns (stream Response); } // // The `CrdtTwo` service is only for verifying forwards and side effects. -// The `Call` method is not required to do anything, and may simply return an empty `Response` message. +// The only action the `Call` method is expected to handle is a delete action, and otherwise +// the `Call` method is not required to do anything, and must return an empty `Response` message. // service CrdtTwo { rpc Call(Request) returns (Response); } +// +// The `CrdtConfigured` service is for testing entity configuration from the language support: +// +// - The passivation strategy must be set with a timeout of 100 millis. +// - The `Call` method is not required to do anything, and must return an empty `Response` message. +// +service CrdtConfigured { + rpc Call(Request) returns (Response); +} + // // A `Request` message contains any actions that the entity should process. // Actions must be processed in order. Any actions after a `Fail` may be ignored. diff --git a/protocols/tck/cloudstate/tck/model/eventsourced.proto b/protocols/tck/cloudstate/tck/model/eventsourced.proto index 0eb2c7e9a..f346222f5 100644 --- a/protocols/tck/cloudstate/tck/model/eventsourced.proto +++ b/protocols/tck/cloudstate/tck/model/eventsourced.proto @@ -21,6 +21,7 @@ syntax = "proto3"; package cloudstate.tck.model; import "cloudstate/entity_key.proto"; +import "google/api/annotations.proto"; option java_package = "io.cloudstate.tck.model"; option go_package = "github.com/cloudstateio/go-support/tck/eventsourced;eventsourced"; @@ -40,17 +41,33 @@ option go_package = "github.com/cloudstateio/go-support/tck/eventsourced;eventso // - Forwarding and side effects must always be made to the second service `EventSourcedTwo`. // service EventSourcedTckModel { - rpc Process(Request) returns (Response); + rpc Process(Request) returns (Response) { + option (google.api.http) = { + post: "/tck/model/eventsourced/{id}", + body: "*" + }; + } } // // The `EventSourcedTwo` service is only for verifying forward actions and side effects. -// The `Call` method is not required to do anything, and may simply return an empty `Response` message. +// The `Call` method is not required to do anything, and must return an empty `Response` message. // service EventSourcedTwo { rpc Call(Request) returns (Response); } +// +// The `EventSourcedConfigured` service is for testing entity configuration from the language support: +// +// - The entity persistence-id must be `event-sourced-configured`. +// - The passivation strategy must be set with a timeout of 100 millis. +// - The `Call` method is not required to do anything, and must return an empty `Response` message. +// +service EventSourcedConfigured { + rpc Call(Request) returns (Response); +} + // // A `Request` message contains any actions that the entity should process. // Actions must be processed in order. Any actions after a `Fail` may be ignored. diff --git a/protocols/tck/cloudstate/tck/model/valueentity.proto b/protocols/tck/cloudstate/tck/model/valueentity.proto index 7a615078b..12530dd7e 100644 --- a/protocols/tck/cloudstate/tck/model/valueentity.proto +++ b/protocols/tck/cloudstate/tck/model/valueentity.proto @@ -21,6 +21,7 @@ syntax = "proto3"; package cloudstate.tck.model.valueentity; import "cloudstate/entity_key.proto"; +import "google/api/annotations.proto"; option java_package = "io.cloudstate.tck.model.valueentity"; option go_package = "github.com/cloudstateio/go-support/tck/valueentity;valueentity"; @@ -38,14 +39,19 @@ option go_package = "github.com/cloudstateio/go-support/tck/valueentity;valueent // - Forwarding and side effects must always be made to the second service `ValueEntityTwo`. // service ValueEntityTckModel { - rpc Process(Request) returns (Response); + rpc Process(Request) returns (Response) { + option (google.api.http) = { + post: "/tck/model/entity/{id}", + body: "*" + }; + } } // // The `ValueBasedTwo` service is only for verifying forward actions and side effects. // // - The entity persistence-id must be `value-entity-tck-model-two`. -// - The `Call` method is not required to do anything, and may simply return an empty `Response` message. +// - The `Call` method is not required to do anything, and must return an empty `Response` message. // service ValueEntityTwo { rpc Call(Request) returns (Response); diff --git a/tck/src/main/scala/io/cloudstate/tck/ActionTCK.scala b/tck/src/main/scala/io/cloudstate/tck/ActionTCK.scala index c79c85ca6..d55d8f616 100644 --- a/tck/src/main/scala/io/cloudstate/tck/ActionTCK.scala +++ b/tck/src/main/scala/io/cloudstate/tck/ActionTCK.scala @@ -16,10 +16,15 @@ package io.cloudstate.tck +import akka.actor.ActorSystem +import akka.stream.scaladsl.Source +import akka.stream.testkit.TestPublisher +import akka.stream.testkit.scaladsl.TestSink import io.cloudstate.protocol.action.{ActionCommand, ActionProtocol, ActionResponse} import io.cloudstate.tck.model.action.{ActionTckModel, ActionTwo} import io.cloudstate.tck.model.action._ import io.cloudstate.testkit.action.ActionMessages._ +import io.grpc.StatusRuntimeException trait ActionTCK extends TCKSpec { @@ -55,6 +60,9 @@ trait ActionTCK extends TCKSpec { def replyWith(message: String): ProcessStep = ProcessStep(ProcessStep.Step.Reply(Reply(message))) + def serviceTwoCall(id: String): ActionCommand = + command(ServiceTwo, "Call", OtherRequest(id)) + def sideEffectTo(id: String, synchronous: Boolean = false): ProcessStep = ProcessStep(ProcessStep.Step.Effect(SideEffect(id, synchronous))) @@ -422,4 +430,196 @@ trait ActionTCK extends TCKSpec { .complete() } } + + object ActionTCKProxy { + val tckModelClient: ActionTckModelClient = ActionTckModelClient(client.settings)(client.system) + + def terminate(): Unit = tckModelClient.close() + } + + override def afterAll(): Unit = + try ActionTCKProxy.terminate() + finally super.afterAll() + + def verifyActionProxy(): Unit = { + import ActionTCKModel._ + import ActionTCKProxy._ + + "verify unary command processing" in actionTest { + tckModelClient.processUnary(single(replyWith("one"))).futureValue mustBe Response("one") + interceptor + .expectActionUnaryConnection() + .expectClient(processUnary(single(replyWith("one")))) + .expectService(reply(Response("one"))) + } + + "verify streamed-in command processing" in actionTest { + implicit val actorSystem: ActorSystem = client.system + val requests = TestPublisher.probe[Request]() + val response = tckModelClient.processStreamedIn(Source.fromPublisher(requests)) + requests.sendNext(single(replyWith("two"))).sendComplete() + response.futureValue mustBe Response("two") + interceptor + .expectActionStreamedInConnection() + .expectClient(processStreamedIn) + .expectClient(command(single(replyWith("two")))) + .expectInComplete() + .expectService(reply(Response("two"))) + } + + "verify streamed-out command processing" in actionTest { + implicit val actorSystem: ActorSystem = client.system + val streamedOutRequest = request(group(replyWith("A")), group(replyWith("B")), group(replyWith("C"))) + tckModelClient + .processStreamedOut(streamedOutRequest) + .runWith(TestSink.probe[Response]) + .ensureSubscription() + .request(3) + .expectNext(Response("A")) + .expectNext(Response("B")) + .expectNext(Response("C")) + .expectComplete() + interceptor + .expectActionStreamedOutConnection() + .expectClient(processStreamedOut(streamedOutRequest)) + .expectService(reply(Response("A"))) + .expectService(reply(Response("B"))) + .expectService(reply(Response("C"))) + .expectOutComplete() + } + + "verify streamed command processing" in actionTest { + implicit val actorSystem: ActorSystem = client.system + val requests = TestPublisher.probe[Request]() + val responses = tckModelClient + .processStreamed(Source.fromPublisher(requests)) + .runWith(TestSink.probe[Response]) + .ensureSubscription() + requests + .sendNext(single(replyWith("X"))) + .sendNext(single(replyWith("Y"))) + .sendNext(single(replyWith("Z"))) + .sendComplete() + responses + .request(3) + .expectNext(Response("X")) + .expectNext(Response("Y")) + .expectNext(Response("Z")) + .expectComplete() + interceptor + .expectActionStreamedConnection() + .expectClient(processStreamed) + .expectClient(command(single(replyWith("X")))) + .expectService(reply(Response("X"))) + .expectClient(command(single(replyWith("Y")))) + .expectService(reply(Response("Y"))) + .expectClient(command(single(replyWith("Z")))) + .expectService(reply(Response("Z"))) + .expectComplete() + } + + "verify unary forwards and side effects" in actionTest { + tckModelClient.processUnary(single(forwardTo("other"))).futureValue mustBe Response() + interceptor + .expectActionUnaryConnection() + .expectClient(processUnary(single(forwardTo("other")))) + .expectService(forwarded("other")) + interceptor + .expectActionUnaryConnection() + .expectClient(serviceTwoCall("other")) + .expectService(reply(Response())) + + tckModelClient.processUnary(single(replyWith(""), sideEffectTo("another"))).futureValue mustBe Response() + interceptor + .expectActionUnaryConnection() + .expectClient(processUnary(single(replyWith(""), sideEffectTo("another")))) + .expectService(reply(Response(), sideEffects("another"))) + interceptor + .expectActionUnaryConnection() + .expectClient(serviceTwoCall("another")) + .expectService(reply(Response())) + } + + "verify streamed forwards and side effects" in actionTest { + implicit val actorSystem: ActorSystem = client.system + val requests = TestPublisher.probe[Request]() + val responses = tckModelClient + .processStreamed(Source.fromPublisher(requests)) + .runWith(TestSink.probe[Response]) + .ensureSubscription() + + requests.sendNext(single(forwardTo("one"))) + responses.request(1).expectNext(Response()) + val connection = interceptor + .expectActionStreamedConnection() + .expectClient(processStreamed) + .expectClient(command(single(forwardTo("one")))) + .expectService(forwarded("one")) + interceptor + .expectActionUnaryConnection() + .expectClient(serviceTwoCall("one")) + .expectService(reply(Response())) + + requests.sendNext(single(sideEffectTo("two"))) + connection + .expectClient(command(single(sideEffectTo("two")))) + .expectService(noReply(sideEffects("two"))) + interceptor + .expectActionUnaryConnection() + .expectClient(serviceTwoCall("two")) + .expectService(reply(Response())) + + requests.sendComplete() + responses.expectComplete() + connection.expectComplete() + } + + "verify unary failures" in actionTest { + val failed = tckModelClient.processUnary(single(failWith("expected failure"))).failed.futureValue + failed mustBe a[StatusRuntimeException] + failed.asInstanceOf[StatusRuntimeException].getStatus.getDescription mustBe "expected failure" + interceptor + .expectActionUnaryConnection() + .expectClient(processUnary(single(failWith("expected failure")))) + .expectService(failure("expected failure")) + } + + "verify streamed failures" in actionTest { + implicit val actorSystem: ActorSystem = client.system + val requests = TestPublisher.probe[Request]() + val responses = tckModelClient + .processStreamed(Source.fromPublisher(requests)) + .runWith(TestSink.probe[Response]) + .ensureSubscription() + val connection = interceptor + .expectActionStreamedConnection() + .expectClient(processStreamed) + requests.sendNext(single(failWith("expected failure"))) + val failed = responses.request(1).expectError() + failed mustBe a[StatusRuntimeException] + failed.asInstanceOf[StatusRuntimeException].getStatus.getDescription mustBe "expected failure" + requests.expectCancellation() + connection + .expectClient(command(single(failWith("expected failure")))) + .expectService(failure("expected failure")) + } + + "verify unary HTTP API" in actionTest { + client.http + .request("tck/model/action/unary", """{"groups": [{"steps": [{"reply": {"message": "foo"}}]}]}""") + .futureValue mustBe """{"message":"foo"}""" + interceptor + .expectActionUnaryConnection() + .expectClient(processUnary(single(replyWith("foo")))) + .expectService(reply(Response("foo"))) + + client.http + .requestToError("tck/model/action/unary", """{"groups": [{"steps": [{"fail": {"message": "boom"}}]}]}""") + .futureValue mustBe "boom" + interceptor + .expectActionUnaryConnection() + .expectClient(processUnary(single(failWith("boom")))) + .expectService(failure("boom")) + } + } } diff --git a/tck/src/main/scala/io/cloudstate/tck/CloudstateTCK.scala b/tck/src/main/scala/io/cloudstate/tck/CloudstateTCK.scala index c1244dbad..24741f1a2 100644 --- a/tck/src/main/scala/io/cloudstate/tck/CloudstateTCK.scala +++ b/tck/src/main/scala/io/cloudstate/tck/CloudstateTCK.scala @@ -47,8 +47,12 @@ class CloudstateTCK(description: String, val settings: TCKSpec.Settings) "verifying model test: CRDT entity" must verifyCrdtEntityModel() + "verifying proxy test: action" must verifyActionProxy() + "verifying proxy test: entity" must verifyEntityProxy() + "verifying proxy test: event sourced entity" must verifyEventSourcedEntityProxy() + "verifying proxy test: CRDT entity" must verifyCrdtEntityProxy() "verifying proxy test: gRPC server reflection" must verifyServerReflection() diff --git a/tck/src/main/scala/io/cloudstate/tck/CrdtEntityTCK.scala b/tck/src/main/scala/io/cloudstate/tck/CrdtEntityTCK.scala index 20028970f..1f08703ab 100644 --- a/tck/src/main/scala/io/cloudstate/tck/CrdtEntityTCK.scala +++ b/tck/src/main/scala/io/cloudstate/tck/CrdtEntityTCK.scala @@ -19,8 +19,11 @@ package io.cloudstate.tck import akka.actor.ActorSystem import akka.stream.testkit.scaladsl.TestSink import io.cloudstate.protocol.crdt._ +import io.cloudstate.protocol.entity.{EntityPassivationStrategy, TimeoutPassivationStrategy} import io.cloudstate.tck.model.crdt._ import io.cloudstate.testkit.crdt.CrdtMessages._ +import io.grpc.StatusRuntimeException + import scala.concurrent.duration._ trait CrdtEntityTCK extends TCKSpec { @@ -29,6 +32,7 @@ trait CrdtEntityTCK extends TCKSpec { val Protocol: String = Crdt.name val Service: String = CrdtTckModel.name val ServiceTwo: String = CrdtTwo.name + val ServiceConfigured: String = CrdtConfigured.name var entityId: Int = 0 @@ -39,6 +43,9 @@ trait CrdtEntityTCK extends TCKSpec { def crdtTest(crdtType: String)(test: String => Any): Unit = testFor(CrdtTckModel, CrdtTwo)(test(nextEntityId(crdtType))) + def crdtConfiguredTest(test: String => Any): Unit = + testFor(CrdtConfigured)(test(nextEntityId("Configured"))) + def requestUpdate(update: Update): RequestAction = RequestAction(RequestAction.Action.Update(update)) @@ -312,6 +319,14 @@ trait CrdtEntityTCK extends TCKSpec { entity(CrdtEntityTCKModel.ServiceTwo).value.entityType mustBe CrdtEntityTCKModel.Protocol } + "verify CRDT configured entity" in testFor(CrdtConfigured) { + discoveredServices must contain("CrdtConfigured") + entity(CrdtEntityTCKModel.ServiceConfigured).value.entityType mustBe CrdtEntityTCKModel.Protocol + entity(CrdtEntityTCKModel.ServiceConfigured).value.passivationStrategy mustBe Some( + EntityPassivationStrategy(EntityPassivationStrategy.Strategy.Timeout(TimeoutPassivationStrategy(100))) + ) + } + // GCounter tests "verify GCounter initial empty state" in GCounter.test { id => @@ -1769,9 +1784,15 @@ trait CrdtEntityTCK extends TCKSpec { } object CrdtEntityTCKProxy { - val crdtTckModelClient: CrdtTckModelClient = CrdtTckModelClient(client.settings)(client.system) + val tckModelClient: CrdtTckModelClient = CrdtTckModelClient(client.settings)(client.system) + val modelTwoClient: CrdtTwoClient = CrdtTwoClient(client.settings)(client.system) + val configuredClient: CrdtConfiguredClient = CrdtConfiguredClient(client.settings)(client.system) - def terminate(): Unit = crdtTckModelClient.close() + def terminate(): Unit = { + tckModelClient.close() + modelTwoClient.close() + configuredClient.close() + } } override def afterAll(): Unit = @@ -1782,6 +1803,92 @@ trait CrdtEntityTCK extends TCKSpec { import CrdtEntityTCKModel._ import CrdtEntityTCKProxy._ + "verify state changes" in PNCounter.test { id => + tckModelClient.process(Request(id)).futureValue mustBe PNCounter.state(0) + val connection = interceptor + .expectCrdtEntityConnection() + .expectClient(init(Service, id)) + .expectClient(command(1, id, "Process", Request(id))) + .expectService(reply(1, PNCounter.state(0))) + + tckModelClient.process(Request(id, PNCounter.changeBy(+1, -2, +3))).futureValue mustBe PNCounter.state(+2) + connection + .expectClient(command(2, id, "Process", Request(id, PNCounter.changeBy(+1, -2, +3)))) + .expectService(reply(2, PNCounter.state(+2), PNCounter.update(+2))) + + tckModelClient.process(Request(id, PNCounter.changeBy(+4, -5, +6))).futureValue mustBe PNCounter.state(+7) + connection + .expectClient(command(3, id, "Process", Request(id, PNCounter.changeBy(+4, -5, +6)))) + .expectService(reply(3, PNCounter.state(+7), PNCounter.update(+5))) + + tckModelClient.process(Request(id, Seq(requestDelete))).futureValue mustBe PNCounter.state(+7) + connection + .expectClient(command(4, id, "Process", Request(id, Seq(requestDelete)))) + .expectService(reply(4, PNCounter.state(+7), deleteCrdt)) + .expectClosed() + } + + "verify forwards and side effects" in GSet.test { id => + tckModelClient.process(Request(id, GSet.add("one"))).futureValue mustBe GSet.state("one") + val connection = interceptor + .expectCrdtEntityConnection() + .expectClient(init(Service, id)) + .expectClient(command(1, id, "Process", Request(id, GSet.add("one")))) + .expectService(reply(1, GSet.state("one"), GSet.update("one"))) + + tckModelClient.process(Request(id, Seq(forwardTo(id)))).futureValue mustBe Response() + connection + .expectClient(command(2, id, "Process", Request(id, Seq(forwardTo(id))))) + .expectService(forward(2, ServiceTwo, "Call", Request(id))) + val connection2 = interceptor + .expectCrdtEntityConnection() + .expectClient(init(ServiceTwo, id)) + .expectClient(command(1, id, "Call", Request(id))) + .expectService(reply(1, Response())) + + tckModelClient.process(Request(id, Seq(sideEffectTo(id)))).futureValue mustBe GSet.state("one") + connection + .expectClient(command(3, id, "Process", Request(id, Seq(sideEffectTo(id))))) + .expectService(reply(3, GSet.state("one"), sideEffects(id))) + connection2 + .expectClient(command(2, id, "Call", Request(id))) + .expectService(reply(2, Response())) + + tckModelClient.process(Request(id, Seq(requestDelete))).futureValue mustBe GSet.state("one") + connection + .expectClient(command(4, id, "Process", Request(id, Seq(requestDelete)))) + .expectService(reply(4, GSet.state("one"), deleteCrdt)) + .expectClosed() + + modelTwoClient.call(Request(id, Seq(requestDelete))).futureValue mustBe Response() + connection2 + .expectClient(command(3, id, "Call", Request(id, Seq(requestDelete)))) + .expectService(reply(3, Response(), deleteCrdt)) + .expectClosed() + } + + "verify failures" in GCounter.test { id => + tckModelClient.process(Request(id, GCounter.incrementBy(42))).futureValue mustBe GCounter.state(42) + val connection = interceptor + .expectCrdtEntityConnection() + .expectClient(init(Service, id)) + .expectClient(command(1, id, "Process", Request(id, GCounter.incrementBy(42)))) + .expectService(reply(1, GCounter.state(42), GCounter.update(42))) + + val failed = tckModelClient.process(Request(id, Seq(failWith("expected failure")))).failed.futureValue + failed mustBe a[StatusRuntimeException] + failed.asInstanceOf[StatusRuntimeException].getStatus.getDescription mustBe "expected failure" + connection + .expectClient(command(2, id, "Process", Request(id, Seq(failWith("expected failure"))))) + .expectService(failure(2, "expected failure")) + + tckModelClient.process(Request(id, Seq(requestDelete))).futureValue mustBe GCounter.state(42) + connection + .expectClient(command(3, id, "Process", Request(id, Seq(requestDelete)))) + .expectService(reply(3, GCounter.state(42), deleteCrdt)) + .expectClosed() + } + "verify streamed responses for connection tracking" in Vote.test { id => implicit val actorSystem: ActorSystem = client.system @@ -1790,36 +1897,93 @@ trait CrdtEntityTCK extends TCKSpec { val voteTrue = Some(Vote.updateWith(true)) val voteFalse = Some(Vote.updateWith(false)) - val monitor = crdtTckModelClient.processStreamed(StreamedRequest(id)).runWith(TestSink.probe[Response]) + val monitor = tckModelClient.processStreamed(StreamedRequest(id)).runWith(TestSink.probe[Response]) monitor.request(1).expectNext(state0) - val crdtProtocol = interceptor.expectCrdtConnection() - crdtProtocol.expectClient(init(CrdtTckModel.name, id)) - crdtProtocol.expectClient(command(1, id, "ProcessStreamed", StreamedRequest(id), streamed = true)) - crdtProtocol.expectService(reply(1, state0, Effects(streamed = true))) + val connection = interceptor + .expectCrdtEntityConnection() + .expectClient(init(CrdtTckModel.name, id)) + .expectClient(command(1, id, "ProcessStreamed", StreamedRequest(id), streamed = true)) + .expectService(reply(1, state0, Effects(streamed = true))) val connectRequest = StreamedRequest(id, initialUpdate = voteTrue, cancelUpdate = voteFalse, empty = true) - val connect = crdtTckModelClient.processStreamed(connectRequest).runWith(TestSink.probe[Response]) + val connect = tckModelClient.processStreamed(connectRequest).runWith(TestSink.probe[Response]) connect.request(1).expectNoMessage(100.millis) monitor.request(1).expectNext(state1) - crdtProtocol.expectClient(command(2, id, "ProcessStreamed", connectRequest, streamed = true)) - crdtProtocol.expectService(crdtReply(2, None, Effects(streamed = true) ++ Vote.update(true))) - crdtProtocol.expectService(streamed(1, state1)) + connection + .expectClient(command(2, id, "ProcessStreamed", connectRequest, streamed = true)) + .expectService(crdtReply(2, None, Effects(streamed = true) ++ Vote.update(true))) + .expectService(streamed(1, state1)) connect.cancel() monitor.request(1).expectNext(state0) - crdtProtocol.expectClient(crdtStreamCancelled(2, id)) - crdtProtocol.expectService(streamCancelledResponse(2, Vote.update(false))) - crdtProtocol.expectService(streamed(1, state0)) + connection + .expectClient(crdtStreamCancelled(2, id)) + .expectService(streamCancelledResponse(2, Vote.update(false))) + .expectService(streamed(1, state0)) monitor.cancel() - crdtProtocol.expectClient(crdtStreamCancelled(1, id)) - crdtProtocol.expectService(streamCancelledResponse(1)) + connection + .expectClient(crdtStreamCancelled(1, id)) + .expectService(streamCancelledResponse(1)) val deleteRequest = Request(id, Seq(requestDelete)) - crdtTckModelClient.process(deleteRequest).futureValue mustBe state0 - crdtProtocol.expectClient(command(3, id, "Process", deleteRequest)) - crdtProtocol.expectService(reply(3, state0, deleteCrdt)) - crdtProtocol.expectClosed() + tckModelClient.process(deleteRequest).futureValue mustBe state0 + connection + .expectClient(command(3, id, "Process", deleteRequest)) + .expectService(reply(3, state0, deleteCrdt)) + .expectClosed() + } + + "verify HTTP API" in PNCounter.test { id => + client.http + .request(s"tck/model/crdt/$id", "{}") + .futureValue mustBe """{"state":{"pncounter":{"value":"0"}}}""" + val connection = interceptor + .expectCrdtEntityConnection() + .expectClient(init(Service, id)) + .expectClient(command(1, id, "Process", Request(id))) + .expectService(reply(1, PNCounter.state(0))) + + client.http + .request(s"tck/model/crdt/$id", """{"actions": [{"update": {"pncounter": {"change": 42} }}]}""") + .futureValue mustBe """{"state":{"pncounter":{"value":"42"}}}""" + connection + .expectClient(command(2, id, "Process", Request(id, PNCounter.changeBy(+42)))) + .expectService(reply(2, PNCounter.state(+42), PNCounter.update(+42))) + + client.http + .requestToError(s"tck/model/crdt/$id", """{"actions": [{"fail": {"message": "expected failure"}}]}""") + .futureValue mustBe "expected failure" + connection + .expectClient(command(3, id, "Process", Request(id, Seq(failWith("expected failure"))))) + .expectService(failure(3, "expected failure")) + + client.http + .request(s"tck/model/crdt/$id", """{"actions": [{"update": {"pncounter": {"change": -123} }}]}""") + .futureValue mustBe """{"state":{"pncounter":{"value":"-81"}}}""" + connection + .expectClient(command(4, id, "Process", Request(id, PNCounter.changeBy(-123)))) + .expectService(reply(4, PNCounter.state(-81), PNCounter.update(-123))) + + client.http + .request(s"tck/model/crdt/$id", """{"actions": [{"delete": {}}]}""") + .futureValue mustBe """{"state":{"pncounter":{"value":"-81"}}}""" + connection + .expectClient(command(5, id, "Process", Request(id, Seq(requestDelete)))) + .expectService(reply(5, PNCounter.state(-81), deleteCrdt)) + .expectClosed() + } + + "verify passivation timeout" in crdtConfiguredTest { id => + pendingUntilFixed { // FIXME: we don't get stream completion, but a failed stream with PeerClosedStreamException + configuredClient.call(Request(id)) + interceptor + .expectCrdtEntityConnection() + .expectClient(init(ServiceConfigured, id)) + .expectClient(command(1, id, "Call", Request(id))) + .expectService(reply(1, Response())) + .expectClosed(2.seconds) // check passivation (with expected timeout of 100 millis) + } } } } diff --git a/tck/src/main/scala/io/cloudstate/tck/EntityTCK.scala b/tck/src/main/scala/io/cloudstate/tck/EntityTCK.scala index 522cd275c..64ee7edc7 100644 --- a/tck/src/main/scala/io/cloudstate/tck/EntityTCK.scala +++ b/tck/src/main/scala/io/cloudstate/tck/EntityTCK.scala @@ -18,10 +18,10 @@ package io.cloudstate.tck import com.google.protobuf.any.{Any => ScalaPbAny} import io.cloudstate.protocol.entity.{EntityPassivationStrategy, TimeoutPassivationStrategy} -import io.cloudstate.protocol.entity.EntityPassivationStrategy.Strategy import io.cloudstate.protocol.value_entity.ValueEntity import io.cloudstate.tck.model.valueentity.valueentity._ import io.cloudstate.testkit.valueentity.ValueEntityMessages._ +import io.grpc.StatusRuntimeException import scala.concurrent.duration._ trait EntityTCK extends TCKSpec { @@ -34,7 +34,7 @@ trait EntityTCK extends TCKSpec { var entityId: Int = 0 - def nextEntityId(): String = { entityId += 1; s"entity:$entityId" } + def nextEntityId(): String = { entityId += 1; s"entity-$entityId" } def valueEntityTest(test: String => Any): Unit = testFor(ValueEntityTckModel, ValueEntityTwo)(test(nextEntityId())) @@ -100,10 +100,11 @@ trait EntityTCK extends TCKSpec { } "verify configured entity" in testFor(ValueEntityConfigured) { + discoveredServices must contain("ValueEntityConfigured") entity(EntityTCKModel.ServiceConfigured).value.entityType mustBe EntityTCKModel.Protocol entity(EntityTCKModel.ServiceConfigured).value.persistenceId mustBe "value-entity-configured" entity(EntityTCKModel.ServiceConfigured).value.passivationStrategy mustBe Some( - EntityPassivationStrategy(Strategy.Timeout(TimeoutPassivationStrategy(100))) + EntityPassivationStrategy(EntityPassivationStrategy.Strategy.Timeout(TimeoutPassivationStrategy(100))) ) } @@ -297,9 +298,10 @@ trait EntityTCK extends TCKSpec { val tckModelClient: ValueEntityTckModelClient = ValueEntityTckModelClient(client.settings)(client.system) val configuredClient: ValueEntityConfiguredClient = ValueEntityConfiguredClient(client.settings)(client.system) - def terminate(): Unit = - try tckModelClient.close() - finally configuredClient.close() + def terminate(): Unit = { + tckModelClient.close() + configuredClient.close() + } } override def afterAll(): Unit = @@ -310,15 +312,141 @@ trait EntityTCK extends TCKSpec { import EntityTCKModel._ import EntityTCKProxy._ + "verify state changes" in valueEntityTest { id => + tckModelClient.process(Request(id)).futureValue mustBe Response() + val connection = interceptor + .expectEntityConnection() + .expectClient(init(Service, id)) + .expectClient(command(1, id, "Process", Request(id))) + .expectService(reply(1, Response())) + + tckModelClient.process(Request(id, updateStates("one"))).futureValue mustBe Response("one") + connection + .expectClient(command(2, id, "Process", Request(id, updateStates("one")))) + .expectService(reply(2, Response("one"), update("one"))) + + tckModelClient.process(Request(id, updateStates("two"))).futureValue mustBe Response("two") + connection + .expectClient(command(3, id, "Process", Request(id, updateStates("two")))) + .expectService(reply(3, Response("two"), update("two"))) + + tckModelClient.process(Request(id)).futureValue mustBe Response("two") + connection + .expectClient(command(4, id, "Process", Request(id))) + .expectService(reply(4, Response("two"))) + + tckModelClient.process(Request(id, Seq(deleteState()))).futureValue mustBe Response() + connection + .expectClient(command(5, id, "Process", Request(id, Seq(deleteState())))) + .expectService(reply(5, Response(), delete())) + + tckModelClient.process(Request(id)).futureValue mustBe Response() + connection + .expectClient(command(6, id, "Process", Request(id))) + .expectService(reply(6, Response())) + + tckModelClient.process(Request(id, updateStates("foo"))).futureValue mustBe Response("foo") + connection + .expectClient(command(7, id, "Process", Request(id, updateStates("foo")))) + .expectService(reply(7, Response("foo"), update("foo"))) + + tckModelClient.process(Request(id, Seq(deleteState()))).futureValue mustBe Response() + connection + .expectClient(command(8, id, "Process", Request(id, Seq(deleteState())))) + .expectService(reply(8, Response(), delete())) + } + + "verify forwards and side effects" in valueEntityTest { id => + tckModelClient.process(Request(id, updateStates("one"))).futureValue mustBe Response("one") + val connection = interceptor + .expectEntityConnection() + .expectClient(init(Service, id)) + .expectClient(command(1, id, "Process", Request(id, updateStates("one")))) + .expectService(reply(1, Response("one"), update("one"))) + + tckModelClient.process(Request(id, Seq(forwardTo(id)))).futureValue mustBe Response() + connection + .expectClient(command(2, id, "Process", Request(id, Seq(forwardTo(id))))) + .expectService(forward(2, ServiceTwo, "Call", Request(id))) + val connection2 = interceptor + .expectEntityConnection() + .expectClient(init(ServiceTwo, id)) + .expectClient(command(1, id, "Call", Request(id))) + .expectService(reply(1, Response())) + + tckModelClient.process(Request(id, Seq(sideEffectTo(id)))).futureValue mustBe Response("one") + connection + .expectClient(command(3, id, "Process", Request(id, Seq(sideEffectTo(id))))) + .expectService(reply(3, Response("one"), sideEffects(id))) + connection2 + .expectClient(command(2, id, "Call", Request(id))) + .expectService(reply(2, Response())) + } + + "verify failures" in valueEntityTest { id => + tckModelClient.process(Request(id, updateStates("one"))).futureValue mustBe Response("one") + val connection = interceptor + .expectEntityConnection() + .expectClient(init(Service, id)) + .expectClient(command(1, id, "Process", Request(id, updateStates("one")))) + .expectService(reply(1, Response("one"), update("one"))) + + val failed = tckModelClient.process(Request(id, Seq(failWith("expected failure")))).failed.futureValue + failed mustBe a[StatusRuntimeException] + failed.asInstanceOf[StatusRuntimeException].getStatus.getDescription mustBe "expected failure" + connection + .expectClient(command(2, id, "Process", Request(id, Seq(failWith("expected failure"))))) + .expectService(actionFailure(2, "expected failure")) + + tckModelClient.process(Request(id)).futureValue mustBe Response("one") + connection + .expectClient(command(3, id, "Process", Request(id))) + .expectService(reply(3, Response("one"))) + } + + "verify HTTP API" in valueEntityTest { id => + client.http.request(s"tck/model/entity/$id", "{}").futureValue mustBe """{"message":""}""" + val connection = interceptor + .expectEntityConnection() + .expectClient(init(Service, id)) + .expectClient(command(1, id, "Process", Request(id))) + .expectService(reply(1, Response())) + + client.http + .request(s"tck/model/entity/$id", """{"actions": [{"update": {"value": "one"}}]}""") + .futureValue mustBe """{"message":"one"}""" + connection + .expectClient(command(2, id, "Process", Request(id, updateStates("one")))) + .expectService(reply(2, Response("one"), update("one"))) + + client.http + .requestToError(s"tck/model/entity/$id", """{"actions": [{"fail": {"message": "expected failure"}}]}""") + .futureValue mustBe "expected failure" + connection + .expectClient(command(3, id, "Process", Request(id, Seq(failWith("expected failure"))))) + .expectService(actionFailure(3, "expected failure")) + + client.http + .request(s"tck/model/entity/$id", """{"actions": [{"update": {"value": "two"}}]}""") + .futureValue mustBe """{"message":"two"}""" + connection + .expectClient(command(4, id, "Process", Request(id, updateStates("two")))) + .expectService(reply(4, Response("two"), update("two"))) + + client.http.request(s"tck/model/entity/$id", "{}").futureValue mustBe """{"message":"two"}""" + connection + .expectClient(command(5, id, "Process", Request(id))) + .expectService(reply(5, Response("two"))) + } + "verify passivation timeout" in valueEntityConfiguredTest { id => configuredClient.call(Request(id)) interceptor - .expectValueBasedConnection() - .expectClient(init(ValueEntityConfigured.name, id)) + .expectEntityConnection() + .expectClient(init(ServiceConfigured, id)) .expectClient(command(1, id, "Call", Request(id))) .expectService(reply(1, Response())) - .expectInClosed(2.seconds) // check passivation (with expected timeout of 100 millis) - .expectOutClosed(2.seconds) // check passivation (with expected timeout of 100 millis) + .expectClosed(2.seconds) // check passivation (with expected timeout of 100 millis) } } } diff --git a/tck/src/main/scala/io/cloudstate/tck/EventSourcedEntityTCK.scala b/tck/src/main/scala/io/cloudstate/tck/EventSourcedEntityTCK.scala index 606ebe381..1b7bfb3ce 100644 --- a/tck/src/main/scala/io/cloudstate/tck/EventSourcedEntityTCK.scala +++ b/tck/src/main/scala/io/cloudstate/tck/EventSourcedEntityTCK.scala @@ -17,9 +17,12 @@ package io.cloudstate.tck import com.google.protobuf.any.{Any => ScalaPbAny} +import io.cloudstate.protocol.entity.{EntityPassivationStrategy, TimeoutPassivationStrategy} import io.cloudstate.protocol.event_sourced.EventSourced import io.cloudstate.tck.model.eventsourced._ import io.cloudstate.testkit.eventsourced.EventSourcedMessages._ +import io.grpc.StatusRuntimeException +import scala.concurrent.duration._ trait EventSourcedEntityTCK extends TCKSpec { @@ -27,14 +30,18 @@ trait EventSourcedEntityTCK extends TCKSpec { val Protocol: String = EventSourced.name val Service: String = EventSourcedTckModel.name val ServiceTwo: String = EventSourcedTwo.name + val ServiceConfigured: String = EventSourcedConfigured.name var entityId: Int = 0 - def nextEntityId(): String = { entityId += 1; s"entity:$entityId" } + def nextEntityId(): String = { entityId += 1; s"entity-$entityId" } def eventSourcedTest(test: String => Any): Unit = testFor(EventSourcedTckModel, EventSourcedTwo)(test(nextEntityId())) + def eventSourcedConfiguredTest(test: String => Any): Unit = + testFor(EventSourcedConfigured)(test(nextEntityId())) + def emitEvent(value: String): RequestAction = RequestAction(RequestAction.Action.Emit(Emit(value))) @@ -82,6 +89,15 @@ trait EventSourcedEntityTCK extends TCKSpec { entity(EventSourcedEntityTCKModel.Service).value.persistenceId mustBe "event-sourced-tck-model" } + "verify event sourced configured entity" in testFor(EventSourcedConfigured) { + discoveredServices must contain("EventSourcedConfigured") + entity(EventSourcedEntityTCKModel.ServiceConfigured).value.entityType mustBe EventSourcedEntityTCKModel.Protocol + entity(EventSourcedEntityTCKModel.ServiceConfigured).value.persistenceId mustBe "event-sourced-configured" + entity(EventSourcedEntityTCKModel.ServiceConfigured).value.passivationStrategy mustBe Some( + EntityPassivationStrategy(EntityPassivationStrategy.Strategy.Timeout(TimeoutPassivationStrategy(100))) + ) + } + "verify initial empty state" in eventSourcedTest { id => protocol.eventSourced .connect() @@ -320,4 +336,186 @@ trait EventSourcedEntityTCK extends TCKSpec { .passivate() } } + + object EventSourcedEntityTCKProxy { + val tckModelClient: EventSourcedTckModelClient = EventSourcedTckModelClient(client.settings)(client.system) + val configuredClient: EventSourcedConfiguredClient = EventSourcedConfiguredClient(client.settings)(client.system) + + def terminate(): Unit = { + tckModelClient.close() + configuredClient.close() + } + } + + override def afterAll(): Unit = + try EventSourcedEntityTCKProxy.terminate() + finally super.afterAll() + + def verifyEventSourcedEntityProxy(): Unit = { + import EventSourcedEntityTCKModel._ + import EventSourcedEntityTCKProxy._ + + "verify state changes" in eventSourcedTest { id => + tckModelClient.process(Request(id)).futureValue mustBe Response() + val connection = interceptor + .expectEventSourcedEntityConnection() + .expectClient(init(Service, id)) + .expectClient(command(1, id, "Process", Request(id))) + .expectService(reply(1, Response())) + + tckModelClient.process(Request(id, emitEvents("A"))).futureValue mustBe Response("A") + connection + .expectClient(command(2, id, "Process", Request(id, emitEvents("A")))) + .expectService(reply(2, Response("A"), events("A"))) + + tckModelClient.process(Request(id, emitEvents("B", "C", "D"))).futureValue mustBe Response("ABCD") + connection + .expectClient(command(3, id, "Process", Request(id, emitEvents("B", "C", "D")))) + .expectService(reply(3, Response("ABCD"), events("B", "C", "D"))) + + tckModelClient.process(Request(id, emitEvents("E"))).futureValue mustBe Response("ABCDE") + connection + .expectClient(command(4, id, "Process", Request(id, emitEvents("E")))) + .expectService(reply(4, Response("ABCDE"), snapshotAndEvents("ABCDE", "E"))) + + tckModelClient.process(Request(id, emitEvents("F", "G", "H", "I"))).futureValue mustBe Response("ABCDEFGHI") + connection + .expectClient(command(5, id, "Process", Request(id, emitEvents("F", "G", "H", "I")))) + .expectService(reply(5, Response("ABCDEFGHI"), events("F", "G", "H", "I"))) + + tckModelClient.process(Request(id, emitEvents("J", "K"))).futureValue mustBe Response("ABCDEFGHIJK") + connection + .expectClient(command(6, id, "Process", Request(id, emitEvents("J", "K")))) + .expectService(reply(6, Response("ABCDEFGHIJK"), snapshotAndEvents("ABCDEFGHIJK", "J", "K"))) + } + + "verify forwards and side effects" in eventSourcedTest { id => + tckModelClient.process(Request(id, emitEvents("one"))).futureValue mustBe Response("one") + val connection = interceptor + .expectEventSourcedEntityConnection() + .expectClient(init(Service, id)) + .expectClient(command(1, id, "Process", Request(id, emitEvents("one")))) + .expectService(reply(1, Response("one"), events("one"))) + + tckModelClient.process(Request(id, Seq(forwardTo(id)))).futureValue mustBe Response() + connection + .expectClient(command(2, id, "Process", Request(id, Seq(forwardTo(id))))) + .expectService(forward(2, ServiceTwo, "Call", Request(id))) + val connection2 = interceptor + .expectEventSourcedEntityConnection() + .expectClient(init(ServiceTwo, id)) + .expectClient(command(1, id, "Call", Request(id))) + .expectService(reply(1, Response())) + + tckModelClient.process(Request(id, Seq(sideEffectTo(id)))).futureValue mustBe Response("one") + connection + .expectClient(command(3, id, "Process", Request(id, Seq(sideEffectTo(id))))) + .expectService(reply(3, Response("one"), sideEffects(id))) + connection2 + .expectClient(command(2, id, "Call", Request(id))) + .expectService(reply(2, Response())) + } + + "verify failures" in eventSourcedTest { id => + tckModelClient.process(Request(id, emitEvents("1", "2", "3"))).futureValue mustBe Response("123") + val connection = interceptor + .expectEventSourcedEntityConnection() + .expectClient(init(Service, id)) + .expectClient(command(1, id, "Process", Request(id, emitEvents("1", "2", "3")))) + .expectService(reply(1, Response("123"), events("1", "2", "3"))) + + val failed = tckModelClient.process(Request(id, Seq(failWith("expected failure")))).failed.futureValue + failed mustBe a[StatusRuntimeException] + failed.asInstanceOf[StatusRuntimeException].getStatus.getDescription mustBe "expected failure" + connection + .expectClient(command(2, id, "Process", Request(id, Seq(failWith("expected failure"))))) + .expectService(actionFailure(2, "expected failure")) + + tckModelClient.process(Request(id)).futureValue mustBe Response("123") + connection + .expectClient(command(3, id, "Process", Request(id))) + .expectService(reply(3, Response("123"))) + + val emitAndFail = Seq(emitEvent("4"), failWith("another failure"), emitEvent("5")) + val failed2 = tckModelClient.process(Request(id, emitAndFail)).failed.futureValue + failed2 mustBe a[StatusRuntimeException] + failed2.asInstanceOf[StatusRuntimeException].getStatus.getDescription mustBe "another failure" + connection + .expectClient(command(4, id, "Process", Request(id, emitAndFail))) + .expectService(actionFailure(4, "another failure", restart = true)) + .expectClosed() + val connection2 = interceptor + .expectEventSourcedEntityConnection() + .expectClient(init(Service, id)) + .expectClient(event(1, persisted("1"))) + .expectClient(event(2, persisted("2"))) + .expectClient(event(3, persisted("3"))) + + tckModelClient.process(Request(id, emitEvents("4", "5"))).futureValue mustBe Response("12345") + connection2 + .expectClient(command(1, id, "Process", Request(id, emitEvents("4", "5")))) + .expectService(reply(1, Response("12345"), snapshotAndEvents("12345", "4", "5"))) + } + + "verify HTTP API" in eventSourcedTest { id => + client.http.request(s"tck/model/eventsourced/$id", "{}").futureValue mustBe """{"message":""}""" + val connection = interceptor + .expectEventSourcedEntityConnection() + .expectClient(init(Service, id)) + .expectClient(command(1, id, "Process", Request(id))) + .expectService(reply(1, Response())) + + client.http + .request(s"tck/model/eventsourced/$id", """{"actions": [{"emit": {"value": "x"}}]}""") + .futureValue mustBe """{"message":"x"}""" + connection + .expectClient(command(2, id, "Process", Request(id, emitEvents("x")))) + .expectService(reply(2, Response("x"), events("x"))) + + client.http + .requestToError(s"tck/model/eventsourced/$id", """{"actions": [{"fail": {"message": "expected failure"}}]}""") + .futureValue mustBe "expected failure" + connection + .expectClient(command(3, id, "Process", Request(id, Seq(failWith("expected failure"))))) + .expectService(actionFailure(3, "expected failure")) + + client.http + .request(s"tck/model/eventsourced/$id", """{"actions": [{"emit": {"value": "y"}}]}""") + .futureValue mustBe """{"message":"xy"}""" + connection + .expectClient(command(4, id, "Process", Request(id, emitEvents("y")))) + .expectService(reply(4, Response("xy"), events("y"))) + + client.http + .requestToError(s"tck/model/eventsourced/$id", + """{"actions": [{"emit": {"value": "z"}}, {"fail": {"message": "emit then fail"}}]}""") + .futureValue mustBe "emit then fail" + connection + .expectClient(command(5, id, "Process", Request(id, Seq(emitEvent("z"), failWith("emit then fail"))))) + .expectService(actionFailure(5, "emit then fail", restart = true)) + .expectClosed() + val connection2 = interceptor + .expectEventSourcedEntityConnection() + .expectClient(init(Service, id)) + .expectClient(event(1, persisted("x"))) + .expectClient(event(2, persisted("y"))) + + client.http + .request(s"tck/model/eventsourced/$id", """{"actions": [{"emit": {"value": "z"}}]}""") + .futureValue mustBe """{"message":"xyz"}""" + connection2 + .expectClient(command(1, id, "Process", Request(id, emitEvents("z")))) + .expectService(reply(1, Response("xyz"), events("z"))) + } + + "verify passivation timeout" in eventSourcedConfiguredTest { id => + configuredClient.call(Request(id)) + interceptor + .expectEventSourcedEntityConnection() + .expectClient(init(ServiceConfigured, id)) + .expectClient(command(1, id, "Call", Request(id))) + .expectService(reply(1, Response())) + .expectClosed(2.seconds) // check passivation (with expected timeout of 100 millis) + } + } } diff --git a/tck/src/main/scala/io/cloudstate/tck/EventSourcedShoppingCartTCK.scala b/tck/src/main/scala/io/cloudstate/tck/EventSourcedShoppingCartTCK.scala index b8ebee85b..fb7fda91b 100644 --- a/tck/src/main/scala/io/cloudstate/tck/EventSourcedShoppingCartTCK.scala +++ b/tck/src/main/scala/io/cloudstate/tck/EventSourcedShoppingCartTCK.scala @@ -183,7 +183,7 @@ class EventSourcedShoppingCartVerifier(interceptor: InterceptService) extends Mu private def nextCommandId(cartId: String): Long = commandIds.updateWith(cartId)(_.map(_ + 1).orElse(Some(1L))).get - def verifyConnection(): Unit = connection = interceptor.expectEventSourcedConnection() + def verifyConnection(): Unit = connection = interceptor.expectEventSourcedEntityConnection() def verifyGetInitialEmptyCart(cartId: String): Unit = { val commandId = nextCommandId(cartId) diff --git a/tck/src/main/scala/io/cloudstate/tck/EventingTCK.scala b/tck/src/main/scala/io/cloudstate/tck/EventingTCK.scala index bec0fca09..aa7b30813 100644 --- a/tck/src/main/scala/io/cloudstate/tck/EventingTCK.scala +++ b/tck/src/main/scala/io/cloudstate/tck/EventingTCK.scala @@ -55,7 +55,7 @@ trait EventingTCK extends TCKSpec { emitEventOne(id, eventlogeventing.ProcessStep.Step.Forward(eventlogeventing.Forward(message))) def verifyEventSourcedInitCommandReply(id: String): Unit = { - val connection = interceptor.expectEventSourcedConnection() + val connection = interceptor.expectEventSourcedEntityConnection() val init = connection.expectClientMessage[EventSourcedStreamIn.Message.Init] init.value.serviceName must ===(eventlogeventing.EventSourcedEntityOne.name) init.value.entityId must ===(id) @@ -120,7 +120,7 @@ trait EventingTCK extends TCKSpec { eventlogeventing.JsonEvent("eventlogeventing:3", "some json message") ) - val connection = interceptor.expectEventSourcedConnection() + val connection = interceptor.expectEventSourcedEntityConnection() val init = connection.expectClientMessage[EventSourcedStreamIn.Message.Init] init.value.serviceName must ===(eventlogeventing.EventSourcedEntityTwo.name) init.value.entityId must ===("eventlogeventing:3") diff --git a/tck/src/main/scala/io/cloudstate/tck/ShoppingCartTCK.scala b/tck/src/main/scala/io/cloudstate/tck/ShoppingCartTCK.scala index 298072265..66a64167e 100644 --- a/tck/src/main/scala/io/cloudstate/tck/ShoppingCartTCK.scala +++ b/tck/src/main/scala/io/cloudstate/tck/ShoppingCartTCK.scala @@ -235,7 +235,7 @@ class ValueEntityShoppingCartVerifier(interceptor: InterceptService) extends Mus private def nextCommandId(cartId: String): Long = commandIds.updateWith(cartId)(_.map(_ + 1).orElse(Some(1L))).get - def verifyConnection(): Unit = connection = interceptor.expectValueBasedConnection() + def verifyConnection(): Unit = connection = interceptor.expectEntityConnection() def verifyGetInitialEmptyCart(cartId: String): Unit = { val commandId = nextCommandId(cartId) diff --git a/testkit/src/main/scala/io/cloudstate/testkit/InterceptService.scala b/testkit/src/main/scala/io/cloudstate/testkit/InterceptService.scala index 6079dec0e..93e3bcebb 100644 --- a/testkit/src/main/scala/io/cloudstate/testkit/InterceptService.scala +++ b/testkit/src/main/scala/io/cloudstate/testkit/InterceptService.scala @@ -38,17 +38,21 @@ final class InterceptService(settings: InterceptorSettings) { private val context = new InterceptorContext(settings.intercept.host, settings.intercept.port) private val entityDiscovery = new InterceptEntityDiscovery(context) - private val crdt = new InterceptCrdtService(context) - private val eventSourced = new InterceptEventSourcedService(context) - private val valueBased = new InterceptValueEntityService(context) private val action = new InterceptActionService(context) + private val valueEntity = new InterceptValueEntityService(context) + private val eventSourcedEntity = new InterceptEventSourcedService(context) + private val crdtEntity = new InterceptCrdtService(context) def start(): Unit = { import context.system entityDiscovery.expectOnline(60.seconds) Await.result( Http().bindAndHandleAsync( - handler = entityDiscovery.handler orElse crdt.handler orElse eventSourced.handler orElse valueBased.handler orElse action.handler, + handler = entityDiscovery.handler + orElse action.handler + orElse valueEntity.handler + orElse eventSourcedEntity.handler + orElse crdtEntity.handler, interface = settings.bind.host, port = settings.bind.port ), @@ -56,15 +60,11 @@ final class InterceptService(settings: InterceptorSettings) { ) } - def expectEntityDiscovery(): InterceptEntityDiscovery.Discovery = entityDiscovery.expectDiscovery() + def expectEntityDiscovery(): InterceptEntityDiscovery.Discovery = + entityDiscovery.expectDiscovery() - def expectCrdtConnection(): InterceptCrdtService.Connection = crdt.expectConnection() - - def expectEventSourcedConnection(): InterceptEventSourcedService.Connection = eventSourced.expectConnection() - - def expectValueBasedConnection(): InterceptValueEntityService.Connection = valueBased.expectConnection() - - def expectActionUnaryConnection(): InterceptActionService.UnaryConnection = action.expectUnaryConnection() + def expectActionUnaryConnection(): InterceptActionService.UnaryConnection = + action.expectUnaryConnection() def expectActionStreamedInConnection(): InterceptActionService.StreamedInConnection = action.expectStreamedInConnection() @@ -72,17 +72,27 @@ final class InterceptService(settings: InterceptorSettings) { def expectActionStreamedOutConnection(): InterceptActionService.StreamedOutConnection = action.expectStreamedOutConnection() - def expectActionStreamedConnection(): InterceptActionService.StreamedConnection = action.expectStreamedConnection() + def expectActionStreamedConnection(): InterceptActionService.StreamedConnection = + action.expectStreamedConnection() + + def expectEntityConnection(): InterceptValueEntityService.Connection = + valueEntity.expectConnection() + + def expectEventSourcedEntityConnection(): InterceptEventSourcedService.Connection = + eventSourcedEntity.expectConnection() + + def expectCrdtEntityConnection(): InterceptCrdtService.Connection = + crdtEntity.expectConnection() def verifyNoMoreInteractions(): Unit = context.probe.expectNoMessage(10.millis) def terminate(): Unit = { entityDiscovery.terminate() - crdt.terminate() - eventSourced.terminate() - valueBased.terminate() action.terminate() + valueEntity.terminate() + eventSourcedEntity.terminate() + crdtEntity.terminate() context.terminate() } } diff --git a/testkit/src/main/scala/io/cloudstate/testkit/action/InterceptActionService.scala b/testkit/src/main/scala/io/cloudstate/testkit/action/InterceptActionService.scala index 7aa46f834..dd08d8c56 100644 --- a/testkit/src/main/scala/io/cloudstate/testkit/action/InterceptActionService.scala +++ b/testkit/src/main/scala/io/cloudstate/testkit/action/InterceptActionService.scala @@ -17,6 +17,8 @@ package io.cloudstate.testkit.action import akka.NotUsed +import akka.actor.ActorSystem +import akka.grpc.Trailers import akka.http.scaladsl.model.{HttpRequest, HttpResponse} import akka.stream.scaladsl.{Sink, Source} import akka.testkit.TestProbe @@ -28,6 +30,7 @@ import io.cloudstate.protocol.action.{ ActionResponse } import io.cloudstate.testkit.InterceptService.InterceptorContext +import io.grpc.Status import scala.concurrent.Future import scala.util.{Failure, Success} @@ -42,8 +45,12 @@ final class InterceptActionService(context: InterceptorContext) { def expectStreamedOutConnection(): StreamedOutConnection = context.probe.expectMsgType[StreamedOutConnection] def expectStreamedConnection(): StreamedConnection = context.probe.expectMsgType[StreamedConnection] + private val errorHandler: ActorSystem => PartialFunction[Throwable, Trailers] = _ => { + case e: Exception => Trailers(Status.INTERNAL.augmentDescription(e.getMessage)) + } + def handler: PartialFunction[HttpRequest, Future[HttpResponse]] = - ActionProtocolHandler.partial(interceptor)(context.system) + ActionProtocolHandler.partial(interceptor, eHandler = errorHandler)(context.system) def terminate(): Unit = interceptor.terminate() } @@ -98,8 +105,20 @@ object InterceptActionService { final class UnaryConnection(context: InterceptorContext, val command: ActionCommand) { private[testkit] val out = TestProbe("UnaryConnectionOutProbe")(context.system) + def expectResponse(): ActionResponse = out.expectMsgType[ActionResponse] + + def expectClient(expected: ActionCommand): UnaryConnection = { + val received = command.copy(metadata = None) // ignore attached metadata + assert(received == expected, s"Unexpected unary action command: expected $expected, found $received") + this + } + + def expectService(response: ActionResponse): UnaryConnection = { + out.expectMsg(response) + this + } } final class StreamedInConnection(context: InterceptorContext) { @@ -114,6 +133,17 @@ object InterceptActionService { def expectCommand(): ActionCommand = in.expectMsgType[ActionCommand] + def expectClient(expected: ActionCommand): StreamedInConnection = { + val command = expectCommand().copy(metadata = None) // ignore attached metadata + assert(command == expected, s"Unexpected streamed-in action command: expected $expected, found $command") + this + } + + def expectService(response: ActionResponse): StreamedInConnection = { + out.expectMsg(response) + this + } + def expectInComplete(): StreamedInConnection = { in.expectMsg(Complete) this @@ -128,6 +158,17 @@ object InterceptActionService { def expectResponse(): ActionResponse = out.expectMsgType[ActionResponse] + def expectClient(expected: ActionCommand): StreamedOutConnection = { + val received = command.copy(metadata = None) // ignore attached metadata + assert(received == expected, s"Unexpected streamed-out action command: expected $expected, found $received") + this + } + + def expectService(response: ActionResponse): StreamedOutConnection = { + out.expectMsg(response) + this + } + def expectOutComplete(): StreamedOutConnection = { out.expectMsg(Complete) this @@ -141,20 +182,36 @@ object InterceptActionService { private[testkit] def inSink: Sink[ActionCommand, NotUsed] = Sink.actorRef(in.ref, Complete, Error.apply) private[testkit] def outSink: Sink[ActionResponse, NotUsed] = Sink.actorRef(out.ref, Complete, Error.apply) + def expectCommand(): ActionCommand = + in.expectMsgType[ActionCommand] + def expectResponse(): ActionResponse = out.expectMsgType[ActionResponse] - def expectOutComplete(): StreamedConnection = { - out.expectMsg(Complete) + def expectClient(expected: ActionCommand): StreamedConnection = { + val command = expectCommand().copy(metadata = None) // ignore attached metadata + assert(command == expected, s"Unexpected streamed action command: expected $expected, found $command") this } - def expectCommand(): ActionCommand = - in.expectMsgType[ActionCommand] + def expectService(response: ActionResponse): StreamedConnection = { + out.expectMsg(response) + this + } def expectInComplete(): StreamedConnection = { in.expectMsg(Complete) this } + + def expectOutComplete(): StreamedConnection = { + out.expectMsg(Complete) + this + } + + def expectComplete(): StreamedConnection = { + expectInComplete() + expectOutComplete() + } } } diff --git a/testkit/src/main/scala/io/cloudstate/testkit/crdt/InterceptCrdtService.scala b/testkit/src/main/scala/io/cloudstate/testkit/crdt/InterceptCrdtService.scala index 9dd39f030..22be1f658 100644 --- a/testkit/src/main/scala/io/cloudstate/testkit/crdt/InterceptCrdtService.scala +++ b/testkit/src/main/scala/io/cloudstate/testkit/crdt/InterceptCrdtService.scala @@ -95,5 +95,10 @@ object InterceptCrdtService { in.expectMsg(Complete) out.expectMsg(Complete) } + + def expectClosed(max: FiniteDuration): Unit = { + in.expectMsg(max, Complete) + out.expectMsg(max, Complete) + } } } diff --git a/testkit/src/main/scala/io/cloudstate/testkit/eventsourced/InterceptEventSourcedService.scala b/testkit/src/main/scala/io/cloudstate/testkit/eventsourced/InterceptEventSourcedService.scala index 55e3facc8..cc8f1ed8b 100644 --- a/testkit/src/main/scala/io/cloudstate/testkit/eventsourced/InterceptEventSourcedService.scala +++ b/testkit/src/main/scala/io/cloudstate/testkit/eventsourced/InterceptEventSourcedService.scala @@ -97,5 +97,15 @@ object InterceptEventSourcedService { out.expectNoMessage(timeout) this } + + def expectClosed(): Unit = { + in.expectMsg(Complete) + out.expectMsg(Complete) + } + + def expectClosed(max: FiniteDuration): Unit = { + in.expectMsg(max, Complete) + out.expectMsg(max, Complete) + } } } diff --git a/testkit/src/main/scala/io/cloudstate/testkit/http/TestHttpClient.scala b/testkit/src/main/scala/io/cloudstate/testkit/http/TestHttpClient.scala index 285d6182f..c8cc6d84c 100644 --- a/testkit/src/main/scala/io/cloudstate/testkit/http/TestHttpClient.scala +++ b/testkit/src/main/scala/io/cloudstate/testkit/http/TestHttpClient.scala @@ -28,7 +28,7 @@ final class TestHttpClient(context: TestClientContext) { import context.system import context.system.dispatcher - def request(path: String, body: String = null): Future[String] = + def singleRequest(path: String, body: String = null): Future[HttpResponse] = Http() .singleRequest( HttpRequest( @@ -38,12 +38,22 @@ final class TestHttpClient(context: TestClientContext) { protocol = HttpProtocols.`HTTP/1.1` ) ) + + def request(path: String, body: String = null): Future[String] = + singleRequest(path, body) .flatMap { response => assert(response.status == StatusCodes.OK, "response status was not OK") assert(response.entity.contentType == ContentTypes.`application/json`, "response content type was not json") Unmarshal(response).to[String] } + def requestToError(path: String, body: String = null): Future[String] = + singleRequest(path, body) + .flatMap { response => + assert(response.status == StatusCodes.InternalServerError, "response status was not InternalServerError") + Unmarshal(response).to[String] + } + private val noRetries = ConnectionPoolSettings(context.system).withMaxRetries(0) private val probeRequest = HttpRequest(uri = s"http://${context.host}:${context.port}") diff --git a/testkit/src/main/scala/io/cloudstate/testkit/valueentity/InterceptValueEntityService.scala b/testkit/src/main/scala/io/cloudstate/testkit/valueentity/InterceptValueEntityService.scala index 81099efc8..00471daaf 100644 --- a/testkit/src/main/scala/io/cloudstate/testkit/valueentity/InterceptValueEntityService.scala +++ b/testkit/src/main/scala/io/cloudstate/testkit/valueentity/InterceptValueEntityService.scala @@ -93,14 +93,9 @@ object InterceptValueEntityService { this } - def expectInClosed(duration: FiniteDuration): Connection = { - in.expectMsg(duration, Complete) - this - } - - def expectOutClosed(duration: FiniteDuration): Connection = { - out.expectMsg(duration, Complete) - this + def expectClosed(max: FiniteDuration): Unit = { + in.expectMsg(max, Complete) + out.expectMsg(max, Complete) } } } diff --git a/testkit/src/main/scala/io/cloudstate/testkit/valueentity/TestValueEntityServiceClient.scala b/testkit/src/main/scala/io/cloudstate/testkit/valueentity/TestValueEntityServiceClient.scala deleted file mode 100644 index e93f7c1dd..000000000 --- a/testkit/src/main/scala/io/cloudstate/testkit/valueentity/TestValueEntityServiceClient.scala +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright 2019 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 io.cloudstate.testkit.valueentity - -import akka.actor.ActorSystem -import akka.grpc.GrpcClientSettings -import akka.stream.scaladsl.Source -import akka.stream.testkit.TestPublisher -import akka.stream.testkit.scaladsl.TestSink -import akka.testkit.TestKit -import com.typesafe.config.{Config, ConfigFactory} -import io.cloudstate.protocol.value_entity.{ValueEntityClient, ValueEntityStreamIn, ValueEntityStreamOut} - -class TestValueEntityServiceClient(port: Int) { - private val config: Config = ConfigFactory.load(ConfigFactory.parseString(""" - akka.http.server { - preview.enable-http2 = on - } - """)) - - private implicit val system: ActorSystem = ActorSystem("TestValueEntityServiceClient", config) - private val client = ValueEntityClient( - GrpcClientSettings.connectToServiceAt("localhost", port).withTls(false) - ) - - def connect: TestValueEntityServiceClient.Connection = new TestValueEntityServiceClient.Connection(client, system) - - def terminate(): Unit = { - client.close() - TestKit.shutdownActorSystem(system) - } -} - -object TestValueEntityServiceClient { - def apply(port: Int) = new TestValueEntityServiceClient(port) - - final class Connection(client: ValueEntityClient, system: ActorSystem) { - private implicit val actorSystem: ActorSystem = system - private val in = TestPublisher.probe[ValueEntityStreamIn]() - private val out = client.handle(Source.fromPublisher(in)).runWith(TestSink.probe[ValueEntityStreamOut]) - - out.ensureSubscription() - - def send(message: ValueEntityStreamIn.Message): Unit = - in.sendNext(ValueEntityStreamIn(message)) - - def expect(message: ValueEntityStreamOut.Message): Unit = - out.request(1).expectNext(ValueEntityStreamOut(message)) - - def expectClosed(): Unit = { - out.expectComplete() - in.expectCancellation() - } - - def passivate(): Unit = close() - - def close(): Unit = { - in.sendComplete() - out.expectComplete() - } - } -} From e31b3ef4fc9f8464e699dbd041982185868c3680 Mon Sep 17 00:00:00 2001 From: Peter Vlugter Date: Fri, 18 Dec 2020 11:22:45 +1300 Subject: [PATCH 3/7] Remove shopping cart from TCK --- build.sbt | 2 +- .../javasupport/tck/JavaSupportTck.java | 10 - tck/src/it/resources/application.conf | 3 +- tck/src/it/resources/native-image.conf | 3 +- .../io/cloudstate/tck/CloudstateTCK.scala | 6 - .../tck/EventSourcedShoppingCartTCK.scala | 240 -------------- .../io/cloudstate/tck/ShoppingCartTCK.scala | 299 ------------------ 7 files changed, 5 insertions(+), 558 deletions(-) delete mode 100644 tck/src/main/scala/io/cloudstate/tck/EventSourcedShoppingCartTCK.scala delete mode 100644 tck/src/main/scala/io/cloudstate/tck/ShoppingCartTCK.scala diff --git a/build.sbt b/build.sbt index 553916953..c75090051 100644 --- a/build.sbt +++ b/build.sbt @@ -653,7 +653,7 @@ lazy val `java-support-docs` = (project in file("java-support/docs")) ) lazy val `java-support-tck` = (project in file("java-support/tck")) - .dependsOn(`java-support`, `java-shopping-cart`, `java-eventsourced-shopping-cart`) + .dependsOn(`java-support`) .enablePlugins(AkkaGrpcPlugin, AssemblyPlugin, JavaAppPackaging, DockerPlugin, AutomateHeaderPlugin, NoPublish) .settings( name := "cloudstate-java-tck", diff --git a/java-support/tck/src/main/java/io/cloudstate/javasupport/tck/JavaSupportTck.java b/java-support/tck/src/main/java/io/cloudstate/javasupport/tck/JavaSupportTck.java index 277fe88f6..8d966362a 100644 --- a/java-support/tck/src/main/java/io/cloudstate/javasupport/tck/JavaSupportTck.java +++ b/java-support/tck/src/main/java/io/cloudstate/javasupport/tck/JavaSupportTck.java @@ -16,7 +16,6 @@ package io.cloudstate.javasupport.tck; -import com.example.valueentity.shoppingcart.Shoppingcart; import io.cloudstate.javasupport.CloudState; import io.cloudstate.javasupport.PassivationStrategy; import io.cloudstate.javasupport.crdt.CrdtEntityOptions; @@ -34,7 +33,6 @@ import io.cloudstate.javasupport.tck.model.crdt.CrdtTwoEntity; import io.cloudstate.javasupport.tck.model.eventsourced.EventSourcedTckModelEntity; import io.cloudstate.javasupport.tck.model.eventsourced.EventSourcedTwoEntity; -import io.cloudstate.samples.shoppingcart.ShoppingCartEntity; import io.cloudstate.tck.model.Action; import io.cloudstate.tck.model.Crdt; import io.cloudstate.tck.model.Eventlogeventing; @@ -66,10 +64,6 @@ public static final void main(String[] args) throws Exception { Valueentity.getDescriptor().findServiceByName("ValueEntityConfigured"), EntityOptions.defaults() // required timeout of 100 millis for TCK tests .withPassivationStrategy(PassivationStrategy.timeout(Duration.ofMillis(100)))) - .registerEntity( - ShoppingCartEntity.class, - Shoppingcart.getDescriptor().findServiceByName("ShoppingCart"), - com.example.valueentity.shoppingcart.persistence.Domain.getDescriptor()) .registerCrdtEntity( CrdtTckModelEntity.class, Crdt.getDescriptor().findServiceByName("CrdtTckModel"), @@ -103,10 +97,6 @@ public static final void main(String[] args) throws Exception { io.cloudstate.javasupport.tck.model.eventlogeventing.EventSourcedEntityTwo.class, Eventlogeventing.getDescriptor().findServiceByName("EventSourcedEntityTwo"), Eventlogeventing.getDescriptor()) - .registerEventSourcedEntity( - io.cloudstate.samples.eventsourced.shoppingcart.ShoppingCartEntity.class, - com.example.shoppingcart.Shoppingcart.getDescriptor().findServiceByName("ShoppingCart"), - com.example.shoppingcart.persistence.Domain.getDescriptor()) .start() .toCompletableFuture() .get(); diff --git a/tck/src/it/resources/application.conf b/tck/src/it/resources/application.conf index 23ba5aa03..dff535e1b 100644 --- a/tck/src/it/resources/application.conf +++ b/tck/src/it/resources/application.conf @@ -1,4 +1,5 @@ -cloudstate-tck.verify = ["Akka + Node.js", "Akka + Java", "Akka + Go", "Akka + Kotlin"] +cloudstate-tck.verify = ["Akka + Java"] +#cloudstate-tck.verify = ["Akka + Node.js", "Akka + Java", "Akka + Go", "Akka + Kotlin"] cloudstate-tck.combinations = [{ name = "Akka + Node.js" tck { diff --git a/tck/src/it/resources/native-image.conf b/tck/src/it/resources/native-image.conf index 0359ee089..3c95d70ff 100644 --- a/tck/src/it/resources/native-image.conf +++ b/tck/src/it/resources/native-image.conf @@ -1,4 +1,5 @@ -cloudstate-tck.verify = ["Akka + Node.js", "Akka + Java", "Akka + Go", "Akka + Kotlin"] +cloudstate-tck.verify = ["Akka + Java"] +#cloudstate-tck.verify = ["Akka + Node.js", "Akka + Java", "Akka + Go", "Akka + Kotlin"] cloudstate-tck.combinations = [{ name = "Akka + Node.js" tck { diff --git a/tck/src/main/scala/io/cloudstate/tck/CloudstateTCK.scala b/tck/src/main/scala/io/cloudstate/tck/CloudstateTCK.scala index 24741f1a2..32db1fd16 100644 --- a/tck/src/main/scala/io/cloudstate/tck/CloudstateTCK.scala +++ b/tck/src/main/scala/io/cloudstate/tck/CloudstateTCK.scala @@ -23,9 +23,7 @@ class CloudstateTCK(description: String, val settings: TCKSpec.Settings) with ProxyTCK with ActionTCK with EntityTCK - with ShoppingCartTCK with EventSourcedEntityTCK - with EventSourcedShoppingCartTCK with CrdtEntityTCK with EventingTCK { @@ -39,12 +37,8 @@ class CloudstateTCK(description: String, val settings: TCKSpec.Settings) "verifying model test: entity" must verifyEntityModel() - "verifying app test: shopping cart" must verifyShoppingCart() - "verifying model test: event sourced entity" must verifyEventSourcedEntityModel() - "verifying app test: event sourced shopping cart" must verifyEventSourcedShoppingCart() - "verifying model test: CRDT entity" must verifyCrdtEntityModel() "verifying proxy test: action" must verifyActionProxy() diff --git a/tck/src/main/scala/io/cloudstate/tck/EventSourcedShoppingCartTCK.scala b/tck/src/main/scala/io/cloudstate/tck/EventSourcedShoppingCartTCK.scala deleted file mode 100644 index fb7fda91b..000000000 --- a/tck/src/main/scala/io/cloudstate/tck/EventSourcedShoppingCartTCK.scala +++ /dev/null @@ -1,240 +0,0 @@ -/* - * Copyright 2019 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 io.cloudstate.tck - -import com.example.shoppingcart.persistence.domain -import com.example.shoppingcart.shoppingcart._ -import io.cloudstate.protocol.event_sourced.{EventSourced, EventSourcedStreamOut} -import io.cloudstate.testkit.InterceptService -import io.cloudstate.testkit.eventsourced.{EventSourcedMessages, InterceptEventSourcedService} -import io.grpc.StatusRuntimeException -import org.scalatest.MustMatchers -import scala.collection.mutable - -trait EventSourcedShoppingCartTCK extends TCKSpec { - - object EventSourcedShoppingCart { - import EventSourcedMessages._ - import EventSourcedShoppingCartVerifier._ - - val Service: String = ShoppingCart.name - - private val shoppingCartClient = ShoppingCartClient(client.settings)(client.system) - - def terminate(): Unit = shoppingCartClient.close() - - def verifyGetInitialEmptyCart(session: EventSourcedShoppingCartVerifier, cartId: String): Unit = { - shoppingCartClient.getCart(GetShoppingCart(cartId)).futureValue mustBe Cart() - session.verifyConnection() - session.verifyGetInitialEmptyCart(cartId) - } - - def verifyGetCart(session: EventSourcedShoppingCartVerifier, cartId: String, expected: Item*): Unit = { - val expectedCart = shoppingCart(expected: _*) - shoppingCartClient.getCart(GetShoppingCart(cartId)).futureValue mustBe expectedCart - session.verifyGetCart(cartId, expectedCart) - } - - def verifyAddItem(session: EventSourcedShoppingCartVerifier, cartId: String, item: Item): Unit = { - val addLineItem = AddLineItem(cartId, item.id, item.name, item.quantity) - shoppingCartClient.addItem(addLineItem).futureValue mustBe EmptyScalaMessage - session.verifyAddItem(cartId, item) - } - - def verifyRemoveItem(session: EventSourcedShoppingCartVerifier, cartId: String, itemId: String): Unit = { - val removeLineItem = RemoveLineItem(cartId, itemId) - shoppingCartClient.removeItem(removeLineItem).futureValue mustBe EmptyScalaMessage - session.verifyRemoveItem(cartId, itemId) - } - - def verifyAddItemFailure(session: EventSourcedShoppingCartVerifier, cartId: String, item: Item): Unit = { - val addLineItem = AddLineItem(cartId, item.id, item.name, item.quantity) - val error = shoppingCartClient.addItem(addLineItem).failed.futureValue - error mustBe a[StatusRuntimeException] - val description = error.asInstanceOf[StatusRuntimeException].getStatus.getDescription - session.verifyAddItemFailure(cartId, item, description) - } - - def verifyRemoveItemFailure(session: EventSourcedShoppingCartVerifier, cartId: String, itemId: String): Unit = { - val removeLineItem = RemoveLineItem(cartId, itemId) - val error = shoppingCartClient.removeItem(removeLineItem).failed.futureValue - error mustBe a[StatusRuntimeException] - val description = error.asInstanceOf[StatusRuntimeException].getStatus.getDescription - session.verifyRemoveItemFailure(cartId, itemId, description) - } - } - - override def afterAll(): Unit = - try EventSourcedShoppingCart.terminate() - finally super.afterAll() - - def verifyEventSourcedShoppingCart(): Unit = { - import EventSourcedShoppingCart._ - import EventSourcedShoppingCartVerifier._ - - "verify event sourced shopping cart discovery" in testFor(ShoppingCart) { - discoveredServices must contain("ShoppingCart") - entity(EventSourcedShoppingCart.Service).value.entityType mustBe EventSourced.name - entity(EventSourcedShoppingCart.Service).value.persistenceId must not be empty - } - - "verify get cart, add item, remove item, and failures" in testFor(ShoppingCart) { - val session = shoppingCartSession(interceptor) - verifyGetInitialEmptyCart(session, "cart:1") // initial empty state - verifyAddItem(session, "cart:1", Item("product:1", "Product1", 1)) // add the first product - verifyAddItem(session, "cart:1", Item("product:2", "Product2", 2)) // add the second product - verifyAddItem(session, "cart:1", Item("product:1", "Product1", 11)) // increase first product - verifyAddItem(session, "cart:1", Item("product:2", "Product2", 31)) // increase second product - verifyGetCart(session, "cart:1", Item("product:1", "Product1", 12), Item("product:2", "Product2", 33)) // check state - verifyRemoveItem(session, "cart:1", "product:1") // remove first product - verifyAddItemFailure(session, "cart:1", Item("product:2", "Product2", -7)) // add negative quantity - verifyAddItemFailure(session, "cart:1", Item("product:1", "Product1", 0)) // add zero quantity - verifyRemoveItemFailure(session, "cart:1", "product:1") // remove non-existing product - verifyGetCart(session, "cart:1", Item("product:2", "Product2", 33)) // check final state - } - - "verify the HTTP API for event sourced shopping cart" in testFor(ShoppingCart) { - def checkHttpRequest(path: String, body: String = null)(expected: => String): Unit = { - val response = client.http.request(path, body) - val expectedResponse = expected - response.futureValue mustBe expectedResponse - } - - val session = shoppingCartSession(interceptor) - - checkHttpRequest("carts/foo") { - session.verifyConnection() - session.verifyGetInitialEmptyCart("foo") - """{"items":[]}""" - } - - checkHttpRequest("cart/foo/items/add", """{"productId": "A14362347", "name": "Deluxe", "quantity": 5}""") { - session.verifyAddItem("foo", Item("A14362347", "Deluxe", 5)) - "{}" - } - - checkHttpRequest("cart/foo/items/add", """{"productId": "B14623482", "name": "Basic", "quantity": 1}""") { - session.verifyAddItem("foo", Item("B14623482", "Basic", 1)) - "{}" - } - - checkHttpRequest("cart/foo/items/add", """{"productId": "A14362347", "name": "Deluxe", "quantity": 2}""") { - session.verifyAddItem("foo", Item("A14362347", "Deluxe", 2)) - "{}" - } - - checkHttpRequest("carts/foo") { - session.verifyGetCart("foo", shoppingCart(Item("A14362347", "Deluxe", 7), Item("B14623482", "Basic", 1))) - """{"items":[{"productId":"A14362347","name":"Deluxe","quantity":7},{"productId":"B14623482","name":"Basic","quantity":1}]}""" - } - - checkHttpRequest("carts/foo/items") { - session.verifyGetCart("foo", shoppingCart(Item("A14362347", "Deluxe", 7), Item("B14623482", "Basic", 1))) - """[{"productId":"A14362347","name":"Deluxe","quantity":7.0},{"productId":"B14623482","name":"Basic","quantity":1.0}]""" - } - - checkHttpRequest("cart/foo/items/A14362347/remove", "") { - session.verifyRemoveItem("foo", "A14362347") - "{}" - } - - checkHttpRequest("carts/foo") { - session.verifyGetCart("foo", shoppingCart(Item("B14623482", "Basic", 1))) - """{"items":[{"productId":"B14623482","name":"Basic","quantity":1}]}""" - } - - checkHttpRequest("carts/foo/items") { - session.verifyGetCart("foo", shoppingCart(Item("B14623482", "Basic", 1))) - """[{"productId":"B14623482","name":"Basic","quantity":1.0}]""" - } - } - } -} - -object EventSourcedShoppingCartVerifier { - case class Item(id: String, name: String, quantity: Int) - - def shoppingCartSession(interceptor: InterceptService): EventSourcedShoppingCartVerifier = - new EventSourcedShoppingCartVerifier(interceptor) - - def shoppingCart(items: Item*): Cart = Cart(items.map(i => LineItem(i.id, i.name, i.quantity))) -} - -class EventSourcedShoppingCartVerifier(interceptor: InterceptService) extends MustMatchers { - import EventSourcedMessages._ - import EventSourcedShoppingCartVerifier.Item - - private val commandIds = mutable.Map.empty[String, Long] - private var connection: InterceptEventSourcedService.Connection = _ - - private def nextCommandId(cartId: String): Long = commandIds.updateWith(cartId)(_.map(_ + 1).orElse(Some(1L))).get - - def verifyConnection(): Unit = connection = interceptor.expectEventSourcedEntityConnection() - - def verifyGetInitialEmptyCart(cartId: String): Unit = { - val commandId = nextCommandId(cartId) - connection.expectClient(init(ShoppingCart.name, cartId)) - connection.expectClient(command(commandId, cartId, "GetCart", GetShoppingCart(cartId))) - connection.expectService(reply(commandId, Cart())) - connection.expectNoInteraction() - } - - def verifyGetCart(cartId: String, expected: Cart): Unit = { - val commandId = nextCommandId(cartId) - connection.expectClient(command(commandId, cartId, "GetCart", GetShoppingCart(cartId))) - connection.expectService(reply(commandId, expected)) - connection.expectNoInteraction() - } - - def verifyAddItem(cartId: String, item: Item): Unit = { - val commandId = nextCommandId(cartId) - val addLineItem = AddLineItem(cartId, item.id, item.name, item.quantity) - val itemAdded = domain.ItemAdded(Some(domain.LineItem(item.id, item.name, item.quantity))) - connection.expectClient(command(commandId, cartId, "AddItem", addLineItem)) - // shopping cart implementations may or may not have snapshots configured, so match without snapshot - val replied = connection.expectServiceMessage[EventSourcedStreamOut.Message.Reply] - replied.copy(value = replied.value.clearSnapshot) mustBe reply(commandId, EmptyScalaMessage, persist(itemAdded)) - connection.expectNoInteraction() - } - - def verifyRemoveItem(cartId: String, itemId: String): Unit = { - val commandId = nextCommandId(cartId) - val removeLineItem = RemoveLineItem(cartId, itemId) - val itemRemoved = domain.ItemRemoved(itemId) - connection.expectClient(command(commandId, cartId, "RemoveItem", removeLineItem)) - // shopping cart implementations may or may not have snapshots configured, so match without snapshot - val replied = connection.expectServiceMessage[EventSourcedStreamOut.Message.Reply] - replied.copy(value = replied.value.clearSnapshot) mustBe reply(commandId, EmptyScalaMessage, persist(itemRemoved)) - connection.expectNoInteraction() - } - - def verifyAddItemFailure(cartId: String, item: Item, failure: String): Unit = { - val commandId = nextCommandId(cartId) - val addLineItem = AddLineItem(cartId, item.id, item.name, item.quantity) - connection.expectClient(command(commandId, cartId, "AddItem", addLineItem)) - connection.expectService(actionFailure(commandId, failure)) - connection.expectNoInteraction() - } - - def verifyRemoveItemFailure(cartId: String, itemId: String, failure: String): Unit = { - val commandId = nextCommandId(cartId) - val removeLineItem = RemoveLineItem(cartId, itemId) - connection.expectClient(command(commandId, cartId, "RemoveItem", removeLineItem)) - connection.expectService(actionFailure(commandId, failure)) - connection.expectNoInteraction() - } -} diff --git a/tck/src/main/scala/io/cloudstate/tck/ShoppingCartTCK.scala b/tck/src/main/scala/io/cloudstate/tck/ShoppingCartTCK.scala deleted file mode 100644 index 66a64167e..000000000 --- a/tck/src/main/scala/io/cloudstate/tck/ShoppingCartTCK.scala +++ /dev/null @@ -1,299 +0,0 @@ -/* - * Copyright 2019 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 io.cloudstate.tck - -import io.cloudstate.testkit.InterceptService -import com.example.valueentity.shoppingcart.shoppingcart._ -import com.example.valueentity.shoppingcart.persistence.domain -import io.cloudstate.protocol.value_entity.{ValueEntity, ValueEntityStreamOut} -import io.cloudstate.testkit.valueentity.{InterceptValueEntityService, ValueEntityMessages} -import io.grpc.StatusRuntimeException -import org.scalatest.MustMatchers -import scala.collection.mutable - -trait ShoppingCartTCK extends TCKSpec { - - object ValueEntityShoppingCart { - import ValueEntityMessages._ - import ValueEntityShoppingCartVerifier._ - - val Service: String = ShoppingCart.name - - private val shoppingCartClient = ShoppingCartClient(client.settings)(client.system) - - def terminate(): Unit = shoppingCartClient.close() - - def verifyGetInitialEmptyCart(session: ValueEntityShoppingCartVerifier, cartId: String): Unit = { - shoppingCartClient.getCart(GetShoppingCart(cartId)).futureValue mustBe Cart() - session.verifyConnection() - session.verifyGetInitialEmptyCart(cartId) - } - - def verifyGetCart(session: ValueEntityShoppingCartVerifier, cartId: String, expected: Item*): Unit = { - val expectedCart = shoppingCart(expected: _*) - shoppingCartClient.getCart(GetShoppingCart(cartId)).futureValue mustBe expectedCart - session.verifyGetCart(cartId, expectedCart) - } - - def verifyAddItem(session: ValueEntityShoppingCartVerifier, - cartId: String, - item: Item, - expected: CartValue): Unit = { - val addLineItem = AddLineItem(cartId, item.id, item.name, item.quantity) - shoppingCartClient.addItem(addLineItem).futureValue mustBe EmptyScalaMessage - session.verifyAddItem(cartId, item, expected) - } - - def verifyRemoveItem(session: ValueEntityShoppingCartVerifier, - cartId: String, - itemId: String, - expected: CartValue): Unit = { - val removeLineItem = RemoveLineItem(cartId, itemId) - shoppingCartClient.removeItem(removeLineItem).futureValue mustBe EmptyScalaMessage - session.verifyRemoveItem(cartId, itemId, expected) - } - - def verifyAddItemFailure(session: ValueEntityShoppingCartVerifier, cartId: String, item: Item): Unit = { - val addLineItem = AddLineItem(cartId, item.id, item.name, item.quantity) - val error = shoppingCartClient.addItem(addLineItem).failed.futureValue - error mustBe a[StatusRuntimeException] - val description = error.asInstanceOf[StatusRuntimeException].getStatus.getDescription - session.verifyAddItemFailure(cartId, item, description) - } - - def verifyRemoveItemFailure(session: ValueEntityShoppingCartVerifier, cartId: String, itemId: String): Unit = { - val removeLineItem = RemoveLineItem(cartId, itemId) - val error = shoppingCartClient.removeItem(removeLineItem).failed.futureValue - error mustBe a[StatusRuntimeException] - val description = error.asInstanceOf[StatusRuntimeException].getStatus.getDescription - session.verifyRemoveItemFailure(cartId, itemId, description) - } - } - - override def afterAll(): Unit = - try ValueEntityShoppingCart.terminate() - finally super.afterAll() - - def verifyShoppingCart(): Unit = { - import ValueEntityShoppingCart._ - import ValueEntityShoppingCartVerifier._ - - "verify shopping cart discovery" in testFor(ShoppingCart) { - discoveredServices must contain("ShoppingCart") - entity(ValueEntityShoppingCart.Service).value.entityType mustBe ValueEntity.name - entity(ValueEntityShoppingCart.Service).value.persistenceId must not be empty - } - - "verify get cart, add item, remove item, and failures" in testFor(ShoppingCart) { - val session = shoppingCartSession(interceptor) - verifyGetInitialEmptyCart(session, "cart:1") // initial empty state - - // add the first product and pass the expected state - verifyAddItem(session, "cart:1", Item("product:1", "Product1", 1), CartValue(Item("product:1", "Product1", 1))) - - // add the second product and pass the expected state - verifyAddItem( - session, - "cart:1", - Item("product:2", "Product2", 2), - CartValue(Item("product:1", "Product1", 1), Item("product:2", "Product2", 2)) - ) - - // increase first product and pass the expected state - verifyAddItem( - session, - "cart:1", - Item("product:1", "Product1", 11), - CartValue(Item("product:1", "Product1", 12), Item("product:2", "Product2", 2)) - ) - - // increase second product and pass the expected state - verifyAddItem( - session, - "cart:1", - Item("product:2", "Product2", 31), - CartValue(Item("product:1", "Product1", 12), Item("product:2", "Product2", 33)) - ) - - verifyGetCart(session, "cart:1", Item("product:1", "Product1", 12), Item("product:2", "Product2", 33)) // check state - - // remove first product and pass the expected state - verifyRemoveItem(session, "cart:1", "product:1", CartValue(Item("product:2", "Product2", 33))) - verifyAddItemFailure(session, "cart:1", Item("product:2", "Product2", -7)) // add negative quantity - verifyAddItemFailure(session, "cart:1", Item("product:1", "Product1", 0)) // add zero quantity - verifyRemoveItemFailure(session, "cart:1", "product:1") // remove non-existing product - verifyGetCart(session, "cart:1", Item("product:2", "Product2", 33)) // check final state - } - - "verify the HTTP API for shopping cart" in testFor(ShoppingCart) { - import ValueEntityShoppingCartVerifier._ - - def checkHttpRequest(path: String, body: String = null)(expected: => String): Unit = { - val response = client.http.request(path, body) - val expectedResponse = expected - response.futureValue mustBe expectedResponse - } - - val session = shoppingCartSession(interceptor) - - checkHttpRequest("ve/carts/foo") { - session.verifyConnection() - session.verifyGetInitialEmptyCart("foo") - """{"items":[]}""" - } - - checkHttpRequest("ve/cart/foo/items/add", """{"productId": "A14362347", "name": "Deluxe", "quantity": 5}""") { - session.verifyAddItem("foo", Item("A14362347", "Deluxe", 5), CartValue(Item("A14362347", "Deluxe", 5))) - "{}" - } - - checkHttpRequest("ve/cart/foo/items/add", """{"productId": "B14623482", "name": "Basic", "quantity": 1}""") { - session.verifyAddItem("foo", - Item("B14623482", "Basic", 1), - CartValue(Item("A14362347", "Deluxe", 5), Item("B14623482", "Basic", 1))) - "{}" - } - - checkHttpRequest("ve/cart/foo/items/add", """{"productId": "A14362347", "name": "Deluxe", "quantity": 2}""") { - session.verifyAddItem("foo", - Item("A14362347", "Deluxe", 2), - CartValue(Item("A14362347", "Deluxe", 7), Item("B14623482", "Basic", 1))) - "{}" - } - - checkHttpRequest("ve/carts/foo") { - session.verifyGetCart("foo", shoppingCart(Item("A14362347", "Deluxe", 7), Item("B14623482", "Basic", 1))) - """{"items":[{"productId":"A14362347","name":"Deluxe","quantity":7},{"productId":"B14623482","name":"Basic","quantity":1}]}""" - } - - checkHttpRequest("ve/carts/foo/items") { - session.verifyGetCart("foo", shoppingCart(Item("A14362347", "Deluxe", 7), Item("B14623482", "Basic", 1))) - """[{"productId":"A14362347","name":"Deluxe","quantity":7.0},{"productId":"B14623482","name":"Basic","quantity":1.0}]""" - } - - checkHttpRequest("ve/cart/foo/items/A14362347/remove", "") { - session.verifyRemoveItem("foo", "A14362347", CartValue(Item("B14623482", "Basic", 1))) - "{}" - } - - checkHttpRequest("ve/carts/foo") { - session.verifyGetCart("foo", shoppingCart(Item("B14623482", "Basic", 1))) - """{"items":[{"productId":"B14623482","name":"Basic","quantity":1}]}""" - } - - checkHttpRequest("ve/carts/foo/items") { - session.verifyGetCart("foo", shoppingCart(Item("B14623482", "Basic", 1))) - """[{"productId":"B14623482","name":"Basic","quantity":1.0}]""" - } - - checkHttpRequest("ve/carts/foo/remove", """{"userId": "foo"}""") { - session.verifyRemoveCart("foo") - "{}" - } - - checkHttpRequest("ve/carts/foo") { - session.verifyGetCart("foo", shoppingCart()) - """{"items":[]}""" - } - } - } -} - -object ValueEntityShoppingCartVerifier { - case class Item(id: String, name: String, quantity: Int) - case class CartValue(items: Item*) - - def shoppingCartSession(interceptor: InterceptService): ValueEntityShoppingCartVerifier = - new ValueEntityShoppingCartVerifier(interceptor) - - def shoppingCart(items: Item*): Cart = Cart(items.map(i => LineItem(i.id, i.name, i.quantity))) - - def domainShoppingCart(cart: CartValue): domain.Cart = - domain.Cart(cart.items.map(i => domain.LineItem(i.id, i.name, i.quantity))) -} - -class ValueEntityShoppingCartVerifier(interceptor: InterceptService) extends MustMatchers { - import ValueEntityMessages._ - import ValueEntityShoppingCartVerifier._ - - private val commandIds = mutable.Map.empty[String, Long] - private var connection: InterceptValueEntityService.Connection = _ - - private def nextCommandId(cartId: String): Long = commandIds.updateWith(cartId)(_.map(_ + 1).orElse(Some(1L))).get - - def verifyConnection(): Unit = connection = interceptor.expectEntityConnection() - - def verifyGetInitialEmptyCart(cartId: String): Unit = { - val commandId = nextCommandId(cartId) - connection.expectClient(init(ShoppingCart.name, cartId)) - connection.expectClient(command(commandId, cartId, "GetCart", GetShoppingCart(cartId))) - connection.expectService(reply(commandId, Cart())) - connection.expectNoInteraction() - } - - def verifyGetCart(cartId: String, expected: Cart): Unit = { - val commandId = nextCommandId(cartId) - connection.expectClient(command(commandId, cartId, "GetCart", GetShoppingCart(cartId))) - connection.expectService(reply(commandId, expected)) - connection.expectNoInteraction() - } - - def verifyAddItem(cartId: String, item: Item, expected: CartValue): Unit = { - val commandId = nextCommandId(cartId) - val addLineItem = AddLineItem(cartId, item.id, item.name, item.quantity) - val cartUpdated = domainShoppingCart(expected) - connection.expectClient(command(commandId, cartId, "AddItem", addLineItem)) - val replied = connection.expectServiceMessage[ValueEntityStreamOut.Message.Reply] - replied mustBe reply(commandId, EmptyScalaMessage, update(cartUpdated)) - connection.expectNoInteraction() - } - - def verifyRemoveItem(cartId: String, itemId: String, expected: CartValue): Unit = { - val commandId = nextCommandId(cartId) - val removeLineItem = RemoveLineItem(cartId, itemId) - val cartUpdated = domainShoppingCart(expected) - connection.expectClient(command(commandId, cartId, "RemoveItem", removeLineItem)) - val replied = connection.expectServiceMessage[ValueEntityStreamOut.Message.Reply] - replied mustBe reply(commandId, EmptyScalaMessage, update(cartUpdated)) - connection.expectNoInteraction() - } - - def verifyRemoveCart(cartId: String): Unit = { - val commandId = nextCommandId(cartId) - val removeCart = RemoveShoppingCart(cartId) - connection.expectClient(command(commandId, cartId, "RemoveCart", removeCart)) - val replied = connection.expectServiceMessage[ValueEntityStreamOut.Message.Reply] - replied mustBe reply(commandId, EmptyScalaMessage, delete()) - connection.expectNoInteraction() - } - - def verifyAddItemFailure(cartId: String, item: Item, failure: String): Unit = { - val commandId = nextCommandId(cartId) - val addLineItem = AddLineItem(cartId, item.id, item.name, item.quantity) - connection.expectClient(command(commandId, cartId, "AddItem", addLineItem)) - connection.expectService(actionFailure(commandId, failure)) - connection.expectNoInteraction() - } - - def verifyRemoveItemFailure(cartId: String, itemId: String, failure: String): Unit = { - val commandId = nextCommandId(cartId) - val removeLineItem = RemoveLineItem(cartId, itemId) - connection.expectClient(command(commandId, cartId, "RemoveItem", removeLineItem)) - connection.expectService(actionFailure(commandId, failure)) - connection.expectNoInteraction() - } -} From b62bd85cae0214e91a3dec2a87fa2fe32e6536dd Mon Sep 17 00:00:00 2001 From: Peter Vlugter Date: Fri, 18 Dec 2020 18:06:09 +1300 Subject: [PATCH 4/7] Include latest Go support in TCK integration tests --- build.sbt | 6 +-- tck/src/it/resources/application.conf | 4 +- tck/src/it/resources/native-image.conf | 4 +- tck/src/main/resources/reference.conf | 2 +- .../io/cloudstate/tck/CrdtEntityTCK.scala | 16 +++++-- .../scala/io/cloudstate/tck/EntityTCK.scala | 21 ++++++++-- .../tck/EventSourcedEntityTCK.scala | 21 ++++++++-- .../action/InterceptActionService.scala | 42 ++++++++++++------- 8 files changed, 82 insertions(+), 34 deletions(-) diff --git a/build.sbt b/build.sbt index c75090051..5a1fbf2fd 100644 --- a/build.sbt +++ b/build.sbt @@ -782,7 +782,7 @@ lazy val `testkit` = (project in file("testkit")) lazy val `tck` = (project in file("tck")) .enablePlugins(AkkaGrpcPlugin, JavaAppPackaging, DockerPlugin, NoPublish) .configs(IntegrationTest) - .dependsOn(`akka-client`, testkit) + .dependsOn(testkit) .settings( Defaults.itSettings, common, @@ -798,11 +798,11 @@ lazy val `tck` = (project in file("tck")) ), PB.protoSources in Compile ++= { val baseDir = (baseDirectory in ThisBuild).value / "protocols" - Seq(baseDir / "protocol", baseDir / "tck") + Seq(baseDir / "protocol", baseDir / "frontend", baseDir / "tck") }, dockerSettings, Compile / bashScriptDefines / mainClass := Some("org.scalatest.run"), - bashScriptExtraDefines += "addApp io.cloudstate.tck.ConfiguredCloudStateTCK", + bashScriptExtraDefines += "addApp io.cloudstate.tck.ConfiguredCloudstateTCK", headerSettings(IntegrationTest), automateHeaderSettings(IntegrationTest), fork in IntegrationTest := true, diff --git a/tck/src/it/resources/application.conf b/tck/src/it/resources/application.conf index dff535e1b..29f765213 100644 --- a/tck/src/it/resources/application.conf +++ b/tck/src/it/resources/application.conf @@ -1,4 +1,4 @@ -cloudstate-tck.verify = ["Akka + Java"] +cloudstate-tck.verify = ["Akka + Java", "Akka + Go"] #cloudstate-tck.verify = ["Akka + Node.js", "Akka + Java", "Akka + Go", "Akka + Kotlin"] cloudstate-tck.combinations = [{ name = "Akka + Node.js" @@ -83,7 +83,7 @@ cloudstate-tck.combinations = [{ hostname = "127.0.0.1" port = 8080 directory = ${user.dir} - command = ["docker", "run", "--rm", "-p", "127.0.0.1:8080:8080", "cloudstateio/cloudstate-go-tck:0.1.1"] + command = ["docker", "run", "--rm", "-p", "127.0.0.1:8080:8080", "cloudstateio/cloudstate-go-tck:latest"] env-vars { HOST = "127.0.0.1" PORT = "8080" diff --git a/tck/src/it/resources/native-image.conf b/tck/src/it/resources/native-image.conf index 3c95d70ff..27d40d7b2 100644 --- a/tck/src/it/resources/native-image.conf +++ b/tck/src/it/resources/native-image.conf @@ -1,4 +1,4 @@ -cloudstate-tck.verify = ["Akka + Java"] +cloudstate-tck.verify = ["Akka + Java", "Akka + Go"] #cloudstate-tck.verify = ["Akka + Node.js", "Akka + Java", "Akka + Go", "Akka + Kotlin"] cloudstate-tck.combinations = [{ name = "Akka + Node.js" @@ -57,7 +57,7 @@ cloudstate-tck.combinations = [{ hostname = "127.0.0.1" port = 8080 directory = ${user.dir} - command = ["docker", "run", "--rm", "-p", "127.0.0.1:8080:8080", "gcr.io/mrcllnz/cloudstate-go-tck:latest"] + command = ["docker", "run", "--rm", "-p", "127.0.0.1:8080:8080", "cloudstateio/cloudstate-go-tck:latest"] env-vars { HOST = "127.0.0.1" PORT = "8080" diff --git a/tck/src/main/resources/reference.conf b/tck/src/main/resources/reference.conf index 2e3cddd89..9dcb3cb56 100644 --- a/tck/src/main/resources/reference.conf +++ b/tck/src/main/resources/reference.conf @@ -1,5 +1,5 @@ cloudstate.tck { - hostname = "127.0.0.1" + hostname = "0.0.0.0" hostname = ${?TCK_HOST} port = 8090 port = ${?TCK_PORT} diff --git a/tck/src/main/scala/io/cloudstate/tck/CrdtEntityTCK.scala b/tck/src/main/scala/io/cloudstate/tck/CrdtEntityTCK.scala index 1f08703ab..f7d6ea32a 100644 --- a/tck/src/main/scala/io/cloudstate/tck/CrdtEntityTCK.scala +++ b/tck/src/main/scala/io/cloudstate/tck/CrdtEntityTCK.scala @@ -1554,8 +1554,12 @@ trait CrdtEntityTCK extends TCKSpec { protocol.crdt .connect() .send(init(CrdtTckModel.name, id)) - .send(command(1, id, "Process", Request(id, Seq(forwardTo(s"X-$id"))))) - .expect(forwarded(1, s"X-$id")) + .send(command(1, id, "Process", Request(id, GCounter.incrementBy(42)))) + .expect(reply(1, GCounter.state(42), GCounter.update(42))) + .send(command(2, id, "Process", Request(id, Seq(forwardTo(s"X-$id"))))) + .expect(forwarded(2, s"X-$id")) + .send(command(3, id, "Process", Request(id))) + .expect(reply(3, GCounter.state(42))) .passivate() } @@ -1572,8 +1576,12 @@ trait CrdtEntityTCK extends TCKSpec { protocol.crdt .connect() .send(init(CrdtTckModel.name, id)) - .send(command(1, id, "Process", Request(id, Seq(sideEffectTo(s"X-$id"))))) - .expect(reply(1, GSet.state(), sideEffects(s"X-$id"))) + .send(command(1, id, "Process", Request(id, GSet.add("one")))) + .expect(reply(1, GSet.state("one"), GSet.update("one"))) + .send(command(2, id, "Process", Request(id, Seq(sideEffectTo(s"X-$id"))))) + .expect(reply(2, GSet.state("one"), sideEffects(s"X-$id"))) + .send(command(3, id, "Process", Request(id))) + .expect(reply(3, GSet.state("one"))) .passivate() } diff --git a/tck/src/main/scala/io/cloudstate/tck/EntityTCK.scala b/tck/src/main/scala/io/cloudstate/tck/EntityTCK.scala index 64ee7edc7..496696054 100644 --- a/tck/src/main/scala/io/cloudstate/tck/EntityTCK.scala +++ b/tck/src/main/scala/io/cloudstate/tck/EntityTCK.scala @@ -210,8 +210,12 @@ trait EntityTCK extends TCKSpec { protocol.valueEntity .connect() .send(init(ValueEntityTckModel.name, id)) - .send(command(1, id, "Process", Request(id, Seq(sideEffectTo(id))))) - .expect(reply(1, Response(), sideEffects(id))) + .send(command(1, id, "Process", Request(id, updateStates("X")))) + .expect(reply(1, Response("X"), update("X"))) + .send(command(2, id, "Process", Request(id, Seq(sideEffectTo(id))))) + .expect(reply(2, Response("X"), sideEffects(id))) + .send(command(3, id, "Process", Request(id))) + .expect(reply(3, Response("X"))) .passivate() } @@ -239,8 +243,12 @@ trait EntityTCK extends TCKSpec { protocol.valueEntity .connect() .send(init(ValueEntityTckModel.name, id)) - .send(command(1, id, "Process", Request(id, Seq(forwardTo(id))))) - .expect(forward(1, ServiceTwo, "Call", Request(id))) + .send(command(1, id, "Process", Request(id, updateStates("X")))) + .expect(reply(1, Response("X"), update("X"))) + .send(command(2, id, "Process", Request(id, Seq(forwardTo(id))))) + .expect(forward(2, ServiceTwo, "Call", Request(id))) + .send(command(3, id, "Process", Request(id))) + .expect(reply(3, Response("X"))) .passivate() } @@ -381,6 +389,11 @@ trait EntityTCK extends TCKSpec { connection2 .expectClient(command(2, id, "Call", Request(id))) .expectService(reply(2, Response())) + + tckModelClient.process(Request(id)).futureValue mustBe Response("one") + connection + .expectClient(command(4, id, "Process", Request(id))) + .expectService(reply(4, Response("one"))) } "verify failures" in valueEntityTest { id => diff --git a/tck/src/main/scala/io/cloudstate/tck/EventSourcedEntityTCK.scala b/tck/src/main/scala/io/cloudstate/tck/EventSourcedEntityTCK.scala index 1b7bfb3ce..1448a9782 100644 --- a/tck/src/main/scala/io/cloudstate/tck/EventSourcedEntityTCK.scala +++ b/tck/src/main/scala/io/cloudstate/tck/EventSourcedEntityTCK.scala @@ -201,8 +201,12 @@ trait EventSourcedEntityTCK extends TCKSpec { protocol.eventSourced .connect() .send(init(EventSourcedTckModel.name, id)) - .send(command(1, id, "Process", Request(id, Seq(forwardTo(id))))) - .expect(forward(1, EventSourcedTwo.name, "Call", Request(id))) + .send(command(1, id, "Process", Request(id, emitEvents("A", "B", "C")))) + .expect(reply(1, Response("ABC"), events("A", "B", "C"))) + .send(command(2, id, "Process", Request(id, Seq(forwardTo(id))))) + .expect(forward(2, EventSourcedTwo.name, "Call", Request(id))) + .send(command(3, id, "Process", Request(id))) + .expect(reply(3, Response("ABC"))) .passivate() } @@ -230,8 +234,12 @@ trait EventSourcedEntityTCK extends TCKSpec { protocol.eventSourced .connect() .send(init(EventSourcedTckModel.name, id)) - .send(command(1, id, "Process", Request(id, Seq(sideEffectTo(id))))) - .expect(reply(1, Response(), sideEffects(id))) + .send(command(1, id, "Process", Request(id, emitEvents("A", "B", "C")))) + .expect(reply(1, Response("ABC"), events("A", "B", "C"))) + .send(command(2, id, "Process", Request(id, Seq(sideEffectTo(id))))) + .expect(reply(2, Response("ABC"), sideEffects(id))) + .send(command(3, id, "Process", Request(id))) + .expect(reply(3, Response("ABC"))) .passivate() } @@ -414,6 +422,11 @@ trait EventSourcedEntityTCK extends TCKSpec { connection2 .expectClient(command(2, id, "Call", Request(id))) .expectService(reply(2, Response())) + + tckModelClient.process(Request(id)).futureValue mustBe Response("one") + connection + .expectClient(command(4, id, "Process", Request(id))) + .expectService(reply(4, Response("one"))) } "verify failures" in eventSourcedTest { id => diff --git a/testkit/src/main/scala/io/cloudstate/testkit/action/InterceptActionService.scala b/testkit/src/main/scala/io/cloudstate/testkit/action/InterceptActionService.scala index dd08d8c56..a5d2d3d4d 100644 --- a/testkit/src/main/scala/io/cloudstate/testkit/action/InterceptActionService.scala +++ b/testkit/src/main/scala/io/cloudstate/testkit/action/InterceptActionService.scala @@ -103,6 +103,16 @@ object InterceptActionService { def terminate(): Unit = client.close() } + def ignoreMetadata(command: ActionCommand): ActionCommand = + command.copy(metadata = None) + + def ignoreMetadata(response: ActionResponse): ActionResponse = + response.update( + if (response.response.isReply) _.reply.optionalMetadata := None else _ => identity, + if (response.response.isForward) _.forward.optionalMetadata := None else _ => identity, + _.sideEffects.modify(_.map(_.update(_.optionalMetadata := None))) + ) + final class UnaryConnection(context: InterceptorContext, val command: ActionCommand) { private[testkit] val out = TestProbe("UnaryConnectionOutProbe")(context.system) @@ -110,13 +120,14 @@ object InterceptActionService { out.expectMsgType[ActionResponse] def expectClient(expected: ActionCommand): UnaryConnection = { - val received = command.copy(metadata = None) // ignore attached metadata + val received = ignoreMetadata(command) assert(received == expected, s"Unexpected unary action command: expected $expected, found $received") this } - def expectService(response: ActionResponse): UnaryConnection = { - out.expectMsg(response) + def expectService(expected: ActionResponse): UnaryConnection = { + val received = ignoreMetadata(expectResponse()) + assert(received == expected, s"Unexpected unary action response: expected $expected, found $received") this } } @@ -134,13 +145,14 @@ object InterceptActionService { in.expectMsgType[ActionCommand] def expectClient(expected: ActionCommand): StreamedInConnection = { - val command = expectCommand().copy(metadata = None) // ignore attached metadata - assert(command == expected, s"Unexpected streamed-in action command: expected $expected, found $command") + val received = ignoreMetadata(expectCommand()) + assert(received == expected, s"Unexpected streamed-in action command: expected $expected, found $received") this } - def expectService(response: ActionResponse): StreamedInConnection = { - out.expectMsg(response) + def expectService(expected: ActionResponse): StreamedInConnection = { + val received = ignoreMetadata(expectResponse()) + assert(received == expected, s"Unexpected unary action response: expected $expected, found $received") this } @@ -159,13 +171,14 @@ object InterceptActionService { out.expectMsgType[ActionResponse] def expectClient(expected: ActionCommand): StreamedOutConnection = { - val received = command.copy(metadata = None) // ignore attached metadata + val received = ignoreMetadata(command) assert(received == expected, s"Unexpected streamed-out action command: expected $expected, found $received") this } - def expectService(response: ActionResponse): StreamedOutConnection = { - out.expectMsg(response) + def expectService(expected: ActionResponse): StreamedOutConnection = { + val received = ignoreMetadata(expectResponse()) + assert(received == expected, s"Unexpected unary action response: expected $expected, found $received") this } @@ -189,13 +202,14 @@ object InterceptActionService { out.expectMsgType[ActionResponse] def expectClient(expected: ActionCommand): StreamedConnection = { - val command = expectCommand().copy(metadata = None) // ignore attached metadata - assert(command == expected, s"Unexpected streamed action command: expected $expected, found $command") + val received = ignoreMetadata(expectCommand()) + assert(received == expected, s"Unexpected streamed action command: expected $expected, found $received") this } - def expectService(response: ActionResponse): StreamedConnection = { - out.expectMsg(response) + def expectService(expected: ActionResponse): StreamedConnection = { + val received = ignoreMetadata(expectResponse()) + assert(received == expected, s"Unexpected unary action response: expected $expected, found $received") this } From d52e74e7dd6d9f7e96a7d3ac260aa339e9a91a88 Mon Sep 17 00:00:00 2001 From: Peter Vlugter Date: Mon, 21 Dec 2020 12:23:45 +1300 Subject: [PATCH 5/7] Add graceful stop and stream completion for CRDT entities --- .../io/cloudstate/proxy/crdt/CrdtEntity.scala | 27 ++++++++++++++----- .../io/cloudstate/tck/CrdtEntityTCK.scala | 16 +++++------ 2 files changed, 27 insertions(+), 16 deletions(-) diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/crdt/CrdtEntity.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/crdt/CrdtEntity.scala index c2d9174de..956bacce9 100644 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/crdt/CrdtEntity.scala +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/crdt/CrdtEntity.scala @@ -46,11 +46,17 @@ object CrdtEntity { private final case class Relay(actorRef: ActorRef) /** - * This is sent by Akka streams when the gRPC stream to the user function has closed - which typically shouldn't - * happen unless it crashes for some reason. + * This is sent by Akka streams when the gRPC stream to the user function has closed - which is expected when the + * entity is stopping (such as for passivation) or when deleted. */ final case object EntityStreamClosed + /** + * This is sent by Akka streams when the gRPC stream to the user function has failed - which typically shouldn't + * happen unless it crashes for some reason. + */ + final case class EntityStreamFailed(cause: Throwable) + final case object Stop private final case class AnyKey(_id: String) extends Key[ReplicatedData](_id) @@ -149,6 +155,7 @@ final class CrdtEntity(client: Crdt, configuration: CrdtEntity.Configuration, en private[this] final var outstanding = Map.empty[Long, Initiator] private[this] final var streamedCalls = Map.empty[Long, ActorRef] private[this] final var closingStreams = Set.empty[Long] + private[this] final var closing = false private[this] final var stopping = false implicit val ec = context.dispatcher @@ -167,7 +174,7 @@ final class CrdtEntity(client: Crdt, configuration: CrdtEntity.Configuration, en NotUsed } ) - .runWith(Sink.actorRef(self, EntityStreamClosed)) + .runWith(Sink.actorRef(self, EntityStreamClosed, EntityStreamFailed.apply)) // We initially do a read to get the initial state. Try a majority read first in case this is a new node. replicator ! Get(key, ReadMajority(configuration.initialReadTimeout)) @@ -400,9 +407,10 @@ final class CrdtEntity(client: Crdt, configuration: CrdtEntity.Configuration, en } case EntityStreamClosed => - crash("Unexpected entity termination due to stream closure") + if (closing) context.stop(self) + else crash("Unexpected entity termination due to stream closure") - case Status.Failure(cause) => + case EntityStreamFailed(cause) => // Means the stream stopped unexpectedly crash("Entity crashed", Some(cause)) @@ -414,7 +422,8 @@ final class CrdtEntity(client: Crdt, configuration: CrdtEntity.Configuration, en actorRef ! Status.Success(Done) streamedCalls -= commandId } - context.stop(self) + relay ! Status.Success(()) + closing = true // wait for stream to be closed before stopping actor } else { stopping = true } @@ -522,7 +531,8 @@ final class CrdtEntity(client: Crdt, configuration: CrdtEntity.Configuration, en private def operationFinished(): Unit = if (stopping) { if (outstanding.isEmpty) { - context.stop(self) + relay ! Status.Success(()) + closing = true // wait for stream to be closed before stopping actor } } else { if (outstandingMutatingOperations > 1) { @@ -659,6 +669,9 @@ final class CrdtEntity(client: Crdt, configuration: CrdtEntity.Configuration, en case EntityStreamClosed => // Ignore + case EntityStreamFailed(_) => + // Ignore + case ReceiveTimeout => context.parent ! CrdtEntityManager.Passivate diff --git a/tck/src/main/scala/io/cloudstate/tck/CrdtEntityTCK.scala b/tck/src/main/scala/io/cloudstate/tck/CrdtEntityTCK.scala index f7d6ea32a..307f1a847 100644 --- a/tck/src/main/scala/io/cloudstate/tck/CrdtEntityTCK.scala +++ b/tck/src/main/scala/io/cloudstate/tck/CrdtEntityTCK.scala @@ -1983,15 +1983,13 @@ trait CrdtEntityTCK extends TCKSpec { } "verify passivation timeout" in crdtConfiguredTest { id => - pendingUntilFixed { // FIXME: we don't get stream completion, but a failed stream with PeerClosedStreamException - configuredClient.call(Request(id)) - interceptor - .expectCrdtEntityConnection() - .expectClient(init(ServiceConfigured, id)) - .expectClient(command(1, id, "Call", Request(id))) - .expectService(reply(1, Response())) - .expectClosed(2.seconds) // check passivation (with expected timeout of 100 millis) - } + configuredClient.call(Request(id)) + interceptor + .expectCrdtEntityConnection() + .expectClient(init(ServiceConfigured, id)) + .expectClient(command(1, id, "Call", Request(id))) + .expectService(reply(1, Response())) + .expectClosed(2.seconds) // check passivation (with expected timeout of 100 millis) } } } From f3602efd0953b410c89bdca3eef3698be20158a0 Mon Sep 17 00:00:00 2001 From: Peter Vlugter Date: Mon, 21 Dec 2020 12:36:09 +1300 Subject: [PATCH 6/7] Rename interceptor expect client/service to expect incoming/outgoing --- .../scala/io/cloudstate/tck/ActionTCK.scala | 84 ++++++------ .../io/cloudstate/tck/CrdtEntityTCK.scala | 118 ++++++++--------- .../scala/io/cloudstate/tck/EntityTCK.scala | 104 +++++++-------- .../tck/EventSourcedEntityTCK.scala | 122 +++++++++--------- .../scala/io/cloudstate/tck/EventingTCK.scala | 12 +- .../action/InterceptActionService.scala | 16 +-- .../testkit/crdt/InterceptCrdtService.scala | 10 +- .../InterceptEventSourcedService.scala | 12 +- .../InterceptValueEntityService.scala | 10 +- 9 files changed, 244 insertions(+), 244 deletions(-) diff --git a/tck/src/main/scala/io/cloudstate/tck/ActionTCK.scala b/tck/src/main/scala/io/cloudstate/tck/ActionTCK.scala index d55d8f616..2ad585b3c 100644 --- a/tck/src/main/scala/io/cloudstate/tck/ActionTCK.scala +++ b/tck/src/main/scala/io/cloudstate/tck/ActionTCK.scala @@ -449,8 +449,8 @@ trait ActionTCK extends TCKSpec { tckModelClient.processUnary(single(replyWith("one"))).futureValue mustBe Response("one") interceptor .expectActionUnaryConnection() - .expectClient(processUnary(single(replyWith("one")))) - .expectService(reply(Response("one"))) + .expectIncoming(processUnary(single(replyWith("one")))) + .expectOutgoing(reply(Response("one"))) } "verify streamed-in command processing" in actionTest { @@ -461,10 +461,10 @@ trait ActionTCK extends TCKSpec { response.futureValue mustBe Response("two") interceptor .expectActionStreamedInConnection() - .expectClient(processStreamedIn) - .expectClient(command(single(replyWith("two")))) + .expectIncoming(processStreamedIn) + .expectIncoming(command(single(replyWith("two")))) .expectInComplete() - .expectService(reply(Response("two"))) + .expectOutgoing(reply(Response("two"))) } "verify streamed-out command processing" in actionTest { @@ -481,10 +481,10 @@ trait ActionTCK extends TCKSpec { .expectComplete() interceptor .expectActionStreamedOutConnection() - .expectClient(processStreamedOut(streamedOutRequest)) - .expectService(reply(Response("A"))) - .expectService(reply(Response("B"))) - .expectService(reply(Response("C"))) + .expectIncoming(processStreamedOut(streamedOutRequest)) + .expectOutgoing(reply(Response("A"))) + .expectOutgoing(reply(Response("B"))) + .expectOutgoing(reply(Response("C"))) .expectOutComplete() } @@ -508,13 +508,13 @@ trait ActionTCK extends TCKSpec { .expectComplete() interceptor .expectActionStreamedConnection() - .expectClient(processStreamed) - .expectClient(command(single(replyWith("X")))) - .expectService(reply(Response("X"))) - .expectClient(command(single(replyWith("Y")))) - .expectService(reply(Response("Y"))) - .expectClient(command(single(replyWith("Z")))) - .expectService(reply(Response("Z"))) + .expectIncoming(processStreamed) + .expectIncoming(command(single(replyWith("X")))) + .expectOutgoing(reply(Response("X"))) + .expectIncoming(command(single(replyWith("Y")))) + .expectOutgoing(reply(Response("Y"))) + .expectIncoming(command(single(replyWith("Z")))) + .expectOutgoing(reply(Response("Z"))) .expectComplete() } @@ -522,22 +522,22 @@ trait ActionTCK extends TCKSpec { tckModelClient.processUnary(single(forwardTo("other"))).futureValue mustBe Response() interceptor .expectActionUnaryConnection() - .expectClient(processUnary(single(forwardTo("other")))) - .expectService(forwarded("other")) + .expectIncoming(processUnary(single(forwardTo("other")))) + .expectOutgoing(forwarded("other")) interceptor .expectActionUnaryConnection() - .expectClient(serviceTwoCall("other")) - .expectService(reply(Response())) + .expectIncoming(serviceTwoCall("other")) + .expectOutgoing(reply(Response())) tckModelClient.processUnary(single(replyWith(""), sideEffectTo("another"))).futureValue mustBe Response() interceptor .expectActionUnaryConnection() - .expectClient(processUnary(single(replyWith(""), sideEffectTo("another")))) - .expectService(reply(Response(), sideEffects("another"))) + .expectIncoming(processUnary(single(replyWith(""), sideEffectTo("another")))) + .expectOutgoing(reply(Response(), sideEffects("another"))) interceptor .expectActionUnaryConnection() - .expectClient(serviceTwoCall("another")) - .expectService(reply(Response())) + .expectIncoming(serviceTwoCall("another")) + .expectOutgoing(reply(Response())) } "verify streamed forwards and side effects" in actionTest { @@ -552,22 +552,22 @@ trait ActionTCK extends TCKSpec { responses.request(1).expectNext(Response()) val connection = interceptor .expectActionStreamedConnection() - .expectClient(processStreamed) - .expectClient(command(single(forwardTo("one")))) - .expectService(forwarded("one")) + .expectIncoming(processStreamed) + .expectIncoming(command(single(forwardTo("one")))) + .expectOutgoing(forwarded("one")) interceptor .expectActionUnaryConnection() - .expectClient(serviceTwoCall("one")) - .expectService(reply(Response())) + .expectIncoming(serviceTwoCall("one")) + .expectOutgoing(reply(Response())) requests.sendNext(single(sideEffectTo("two"))) connection - .expectClient(command(single(sideEffectTo("two")))) - .expectService(noReply(sideEffects("two"))) + .expectIncoming(command(single(sideEffectTo("two")))) + .expectOutgoing(noReply(sideEffects("two"))) interceptor .expectActionUnaryConnection() - .expectClient(serviceTwoCall("two")) - .expectService(reply(Response())) + .expectIncoming(serviceTwoCall("two")) + .expectOutgoing(reply(Response())) requests.sendComplete() responses.expectComplete() @@ -580,8 +580,8 @@ trait ActionTCK extends TCKSpec { failed.asInstanceOf[StatusRuntimeException].getStatus.getDescription mustBe "expected failure" interceptor .expectActionUnaryConnection() - .expectClient(processUnary(single(failWith("expected failure")))) - .expectService(failure("expected failure")) + .expectIncoming(processUnary(single(failWith("expected failure")))) + .expectOutgoing(failure("expected failure")) } "verify streamed failures" in actionTest { @@ -593,15 +593,15 @@ trait ActionTCK extends TCKSpec { .ensureSubscription() val connection = interceptor .expectActionStreamedConnection() - .expectClient(processStreamed) + .expectIncoming(processStreamed) requests.sendNext(single(failWith("expected failure"))) val failed = responses.request(1).expectError() failed mustBe a[StatusRuntimeException] failed.asInstanceOf[StatusRuntimeException].getStatus.getDescription mustBe "expected failure" requests.expectCancellation() connection - .expectClient(command(single(failWith("expected failure")))) - .expectService(failure("expected failure")) + .expectIncoming(command(single(failWith("expected failure")))) + .expectOutgoing(failure("expected failure")) } "verify unary HTTP API" in actionTest { @@ -610,16 +610,16 @@ trait ActionTCK extends TCKSpec { .futureValue mustBe """{"message":"foo"}""" interceptor .expectActionUnaryConnection() - .expectClient(processUnary(single(replyWith("foo")))) - .expectService(reply(Response("foo"))) + .expectIncoming(processUnary(single(replyWith("foo")))) + .expectOutgoing(reply(Response("foo"))) client.http .requestToError("tck/model/action/unary", """{"groups": [{"steps": [{"fail": {"message": "boom"}}]}]}""") .futureValue mustBe "boom" interceptor .expectActionUnaryConnection() - .expectClient(processUnary(single(failWith("boom")))) - .expectService(failure("boom")) + .expectIncoming(processUnary(single(failWith("boom")))) + .expectOutgoing(failure("boom")) } } } diff --git a/tck/src/main/scala/io/cloudstate/tck/CrdtEntityTCK.scala b/tck/src/main/scala/io/cloudstate/tck/CrdtEntityTCK.scala index 307f1a847..2e2e4aa3b 100644 --- a/tck/src/main/scala/io/cloudstate/tck/CrdtEntityTCK.scala +++ b/tck/src/main/scala/io/cloudstate/tck/CrdtEntityTCK.scala @@ -1815,24 +1815,24 @@ trait CrdtEntityTCK extends TCKSpec { tckModelClient.process(Request(id)).futureValue mustBe PNCounter.state(0) val connection = interceptor .expectCrdtEntityConnection() - .expectClient(init(Service, id)) - .expectClient(command(1, id, "Process", Request(id))) - .expectService(reply(1, PNCounter.state(0))) + .expectIncoming(init(Service, id)) + .expectIncoming(command(1, id, "Process", Request(id))) + .expectOutgoing(reply(1, PNCounter.state(0))) tckModelClient.process(Request(id, PNCounter.changeBy(+1, -2, +3))).futureValue mustBe PNCounter.state(+2) connection - .expectClient(command(2, id, "Process", Request(id, PNCounter.changeBy(+1, -2, +3)))) - .expectService(reply(2, PNCounter.state(+2), PNCounter.update(+2))) + .expectIncoming(command(2, id, "Process", Request(id, PNCounter.changeBy(+1, -2, +3)))) + .expectOutgoing(reply(2, PNCounter.state(+2), PNCounter.update(+2))) tckModelClient.process(Request(id, PNCounter.changeBy(+4, -5, +6))).futureValue mustBe PNCounter.state(+7) connection - .expectClient(command(3, id, "Process", Request(id, PNCounter.changeBy(+4, -5, +6)))) - .expectService(reply(3, PNCounter.state(+7), PNCounter.update(+5))) + .expectIncoming(command(3, id, "Process", Request(id, PNCounter.changeBy(+4, -5, +6)))) + .expectOutgoing(reply(3, PNCounter.state(+7), PNCounter.update(+5))) tckModelClient.process(Request(id, Seq(requestDelete))).futureValue mustBe PNCounter.state(+7) connection - .expectClient(command(4, id, "Process", Request(id, Seq(requestDelete)))) - .expectService(reply(4, PNCounter.state(+7), deleteCrdt)) + .expectIncoming(command(4, id, "Process", Request(id, Seq(requestDelete)))) + .expectOutgoing(reply(4, PNCounter.state(+7), deleteCrdt)) .expectClosed() } @@ -1840,38 +1840,38 @@ trait CrdtEntityTCK extends TCKSpec { tckModelClient.process(Request(id, GSet.add("one"))).futureValue mustBe GSet.state("one") val connection = interceptor .expectCrdtEntityConnection() - .expectClient(init(Service, id)) - .expectClient(command(1, id, "Process", Request(id, GSet.add("one")))) - .expectService(reply(1, GSet.state("one"), GSet.update("one"))) + .expectIncoming(init(Service, id)) + .expectIncoming(command(1, id, "Process", Request(id, GSet.add("one")))) + .expectOutgoing(reply(1, GSet.state("one"), GSet.update("one"))) tckModelClient.process(Request(id, Seq(forwardTo(id)))).futureValue mustBe Response() connection - .expectClient(command(2, id, "Process", Request(id, Seq(forwardTo(id))))) - .expectService(forward(2, ServiceTwo, "Call", Request(id))) + .expectIncoming(command(2, id, "Process", Request(id, Seq(forwardTo(id))))) + .expectOutgoing(forward(2, ServiceTwo, "Call", Request(id))) val connection2 = interceptor .expectCrdtEntityConnection() - .expectClient(init(ServiceTwo, id)) - .expectClient(command(1, id, "Call", Request(id))) - .expectService(reply(1, Response())) + .expectIncoming(init(ServiceTwo, id)) + .expectIncoming(command(1, id, "Call", Request(id))) + .expectOutgoing(reply(1, Response())) tckModelClient.process(Request(id, Seq(sideEffectTo(id)))).futureValue mustBe GSet.state("one") connection - .expectClient(command(3, id, "Process", Request(id, Seq(sideEffectTo(id))))) - .expectService(reply(3, GSet.state("one"), sideEffects(id))) + .expectIncoming(command(3, id, "Process", Request(id, Seq(sideEffectTo(id))))) + .expectOutgoing(reply(3, GSet.state("one"), sideEffects(id))) connection2 - .expectClient(command(2, id, "Call", Request(id))) - .expectService(reply(2, Response())) + .expectIncoming(command(2, id, "Call", Request(id))) + .expectOutgoing(reply(2, Response())) tckModelClient.process(Request(id, Seq(requestDelete))).futureValue mustBe GSet.state("one") connection - .expectClient(command(4, id, "Process", Request(id, Seq(requestDelete)))) - .expectService(reply(4, GSet.state("one"), deleteCrdt)) + .expectIncoming(command(4, id, "Process", Request(id, Seq(requestDelete)))) + .expectOutgoing(reply(4, GSet.state("one"), deleteCrdt)) .expectClosed() modelTwoClient.call(Request(id, Seq(requestDelete))).futureValue mustBe Response() connection2 - .expectClient(command(3, id, "Call", Request(id, Seq(requestDelete)))) - .expectService(reply(3, Response(), deleteCrdt)) + .expectIncoming(command(3, id, "Call", Request(id, Seq(requestDelete)))) + .expectOutgoing(reply(3, Response(), deleteCrdt)) .expectClosed() } @@ -1879,21 +1879,21 @@ trait CrdtEntityTCK extends TCKSpec { tckModelClient.process(Request(id, GCounter.incrementBy(42))).futureValue mustBe GCounter.state(42) val connection = interceptor .expectCrdtEntityConnection() - .expectClient(init(Service, id)) - .expectClient(command(1, id, "Process", Request(id, GCounter.incrementBy(42)))) - .expectService(reply(1, GCounter.state(42), GCounter.update(42))) + .expectIncoming(init(Service, id)) + .expectIncoming(command(1, id, "Process", Request(id, GCounter.incrementBy(42)))) + .expectOutgoing(reply(1, GCounter.state(42), GCounter.update(42))) val failed = tckModelClient.process(Request(id, Seq(failWith("expected failure")))).failed.futureValue failed mustBe a[StatusRuntimeException] failed.asInstanceOf[StatusRuntimeException].getStatus.getDescription mustBe "expected failure" connection - .expectClient(command(2, id, "Process", Request(id, Seq(failWith("expected failure"))))) - .expectService(failure(2, "expected failure")) + .expectIncoming(command(2, id, "Process", Request(id, Seq(failWith("expected failure"))))) + .expectOutgoing(failure(2, "expected failure")) tckModelClient.process(Request(id, Seq(requestDelete))).futureValue mustBe GCounter.state(42) connection - .expectClient(command(3, id, "Process", Request(id, Seq(requestDelete)))) - .expectService(reply(3, GCounter.state(42), deleteCrdt)) + .expectIncoming(command(3, id, "Process", Request(id, Seq(requestDelete)))) + .expectOutgoing(reply(3, GCounter.state(42), deleteCrdt)) .expectClosed() } @@ -1909,36 +1909,36 @@ trait CrdtEntityTCK extends TCKSpec { monitor.request(1).expectNext(state0) val connection = interceptor .expectCrdtEntityConnection() - .expectClient(init(CrdtTckModel.name, id)) - .expectClient(command(1, id, "ProcessStreamed", StreamedRequest(id), streamed = true)) - .expectService(reply(1, state0, Effects(streamed = true))) + .expectIncoming(init(CrdtTckModel.name, id)) + .expectIncoming(command(1, id, "ProcessStreamed", StreamedRequest(id), streamed = true)) + .expectOutgoing(reply(1, state0, Effects(streamed = true))) val connectRequest = StreamedRequest(id, initialUpdate = voteTrue, cancelUpdate = voteFalse, empty = true) val connect = tckModelClient.processStreamed(connectRequest).runWith(TestSink.probe[Response]) connect.request(1).expectNoMessage(100.millis) monitor.request(1).expectNext(state1) connection - .expectClient(command(2, id, "ProcessStreamed", connectRequest, streamed = true)) - .expectService(crdtReply(2, None, Effects(streamed = true) ++ Vote.update(true))) - .expectService(streamed(1, state1)) + .expectIncoming(command(2, id, "ProcessStreamed", connectRequest, streamed = true)) + .expectOutgoing(crdtReply(2, None, Effects(streamed = true) ++ Vote.update(true))) + .expectOutgoing(streamed(1, state1)) connect.cancel() monitor.request(1).expectNext(state0) connection - .expectClient(crdtStreamCancelled(2, id)) - .expectService(streamCancelledResponse(2, Vote.update(false))) - .expectService(streamed(1, state0)) + .expectIncoming(crdtStreamCancelled(2, id)) + .expectOutgoing(streamCancelledResponse(2, Vote.update(false))) + .expectOutgoing(streamed(1, state0)) monitor.cancel() connection - .expectClient(crdtStreamCancelled(1, id)) - .expectService(streamCancelledResponse(1)) + .expectIncoming(crdtStreamCancelled(1, id)) + .expectOutgoing(streamCancelledResponse(1)) val deleteRequest = Request(id, Seq(requestDelete)) tckModelClient.process(deleteRequest).futureValue mustBe state0 connection - .expectClient(command(3, id, "Process", deleteRequest)) - .expectService(reply(3, state0, deleteCrdt)) + .expectIncoming(command(3, id, "Process", deleteRequest)) + .expectOutgoing(reply(3, state0, deleteCrdt)) .expectClosed() } @@ -1948,37 +1948,37 @@ trait CrdtEntityTCK extends TCKSpec { .futureValue mustBe """{"state":{"pncounter":{"value":"0"}}}""" val connection = interceptor .expectCrdtEntityConnection() - .expectClient(init(Service, id)) - .expectClient(command(1, id, "Process", Request(id))) - .expectService(reply(1, PNCounter.state(0))) + .expectIncoming(init(Service, id)) + .expectIncoming(command(1, id, "Process", Request(id))) + .expectOutgoing(reply(1, PNCounter.state(0))) client.http .request(s"tck/model/crdt/$id", """{"actions": [{"update": {"pncounter": {"change": 42} }}]}""") .futureValue mustBe """{"state":{"pncounter":{"value":"42"}}}""" connection - .expectClient(command(2, id, "Process", Request(id, PNCounter.changeBy(+42)))) - .expectService(reply(2, PNCounter.state(+42), PNCounter.update(+42))) + .expectIncoming(command(2, id, "Process", Request(id, PNCounter.changeBy(+42)))) + .expectOutgoing(reply(2, PNCounter.state(+42), PNCounter.update(+42))) client.http .requestToError(s"tck/model/crdt/$id", """{"actions": [{"fail": {"message": "expected failure"}}]}""") .futureValue mustBe "expected failure" connection - .expectClient(command(3, id, "Process", Request(id, Seq(failWith("expected failure"))))) - .expectService(failure(3, "expected failure")) + .expectIncoming(command(3, id, "Process", Request(id, Seq(failWith("expected failure"))))) + .expectOutgoing(failure(3, "expected failure")) client.http .request(s"tck/model/crdt/$id", """{"actions": [{"update": {"pncounter": {"change": -123} }}]}""") .futureValue mustBe """{"state":{"pncounter":{"value":"-81"}}}""" connection - .expectClient(command(4, id, "Process", Request(id, PNCounter.changeBy(-123)))) - .expectService(reply(4, PNCounter.state(-81), PNCounter.update(-123))) + .expectIncoming(command(4, id, "Process", Request(id, PNCounter.changeBy(-123)))) + .expectOutgoing(reply(4, PNCounter.state(-81), PNCounter.update(-123))) client.http .request(s"tck/model/crdt/$id", """{"actions": [{"delete": {}}]}""") .futureValue mustBe """{"state":{"pncounter":{"value":"-81"}}}""" connection - .expectClient(command(5, id, "Process", Request(id, Seq(requestDelete)))) - .expectService(reply(5, PNCounter.state(-81), deleteCrdt)) + .expectIncoming(command(5, id, "Process", Request(id, Seq(requestDelete)))) + .expectOutgoing(reply(5, PNCounter.state(-81), deleteCrdt)) .expectClosed() } @@ -1986,9 +1986,9 @@ trait CrdtEntityTCK extends TCKSpec { configuredClient.call(Request(id)) interceptor .expectCrdtEntityConnection() - .expectClient(init(ServiceConfigured, id)) - .expectClient(command(1, id, "Call", Request(id))) - .expectService(reply(1, Response())) + .expectIncoming(init(ServiceConfigured, id)) + .expectIncoming(command(1, id, "Call", Request(id))) + .expectOutgoing(reply(1, Response())) .expectClosed(2.seconds) // check passivation (with expected timeout of 100 millis) } } diff --git a/tck/src/main/scala/io/cloudstate/tck/EntityTCK.scala b/tck/src/main/scala/io/cloudstate/tck/EntityTCK.scala index 496696054..3e5f6269e 100644 --- a/tck/src/main/scala/io/cloudstate/tck/EntityTCK.scala +++ b/tck/src/main/scala/io/cloudstate/tck/EntityTCK.scala @@ -324,141 +324,141 @@ trait EntityTCK extends TCKSpec { tckModelClient.process(Request(id)).futureValue mustBe Response() val connection = interceptor .expectEntityConnection() - .expectClient(init(Service, id)) - .expectClient(command(1, id, "Process", Request(id))) - .expectService(reply(1, Response())) + .expectIncoming(init(Service, id)) + .expectIncoming(command(1, id, "Process", Request(id))) + .expectOutgoing(reply(1, Response())) tckModelClient.process(Request(id, updateStates("one"))).futureValue mustBe Response("one") connection - .expectClient(command(2, id, "Process", Request(id, updateStates("one")))) - .expectService(reply(2, Response("one"), update("one"))) + .expectIncoming(command(2, id, "Process", Request(id, updateStates("one")))) + .expectOutgoing(reply(2, Response("one"), update("one"))) tckModelClient.process(Request(id, updateStates("two"))).futureValue mustBe Response("two") connection - .expectClient(command(3, id, "Process", Request(id, updateStates("two")))) - .expectService(reply(3, Response("two"), update("two"))) + .expectIncoming(command(3, id, "Process", Request(id, updateStates("two")))) + .expectOutgoing(reply(3, Response("two"), update("two"))) tckModelClient.process(Request(id)).futureValue mustBe Response("two") connection - .expectClient(command(4, id, "Process", Request(id))) - .expectService(reply(4, Response("two"))) + .expectIncoming(command(4, id, "Process", Request(id))) + .expectOutgoing(reply(4, Response("two"))) tckModelClient.process(Request(id, Seq(deleteState()))).futureValue mustBe Response() connection - .expectClient(command(5, id, "Process", Request(id, Seq(deleteState())))) - .expectService(reply(5, Response(), delete())) + .expectIncoming(command(5, id, "Process", Request(id, Seq(deleteState())))) + .expectOutgoing(reply(5, Response(), delete())) tckModelClient.process(Request(id)).futureValue mustBe Response() connection - .expectClient(command(6, id, "Process", Request(id))) - .expectService(reply(6, Response())) + .expectIncoming(command(6, id, "Process", Request(id))) + .expectOutgoing(reply(6, Response())) tckModelClient.process(Request(id, updateStates("foo"))).futureValue mustBe Response("foo") connection - .expectClient(command(7, id, "Process", Request(id, updateStates("foo")))) - .expectService(reply(7, Response("foo"), update("foo"))) + .expectIncoming(command(7, id, "Process", Request(id, updateStates("foo")))) + .expectOutgoing(reply(7, Response("foo"), update("foo"))) tckModelClient.process(Request(id, Seq(deleteState()))).futureValue mustBe Response() connection - .expectClient(command(8, id, "Process", Request(id, Seq(deleteState())))) - .expectService(reply(8, Response(), delete())) + .expectIncoming(command(8, id, "Process", Request(id, Seq(deleteState())))) + .expectOutgoing(reply(8, Response(), delete())) } "verify forwards and side effects" in valueEntityTest { id => tckModelClient.process(Request(id, updateStates("one"))).futureValue mustBe Response("one") val connection = interceptor .expectEntityConnection() - .expectClient(init(Service, id)) - .expectClient(command(1, id, "Process", Request(id, updateStates("one")))) - .expectService(reply(1, Response("one"), update("one"))) + .expectIncoming(init(Service, id)) + .expectIncoming(command(1, id, "Process", Request(id, updateStates("one")))) + .expectOutgoing(reply(1, Response("one"), update("one"))) tckModelClient.process(Request(id, Seq(forwardTo(id)))).futureValue mustBe Response() connection - .expectClient(command(2, id, "Process", Request(id, Seq(forwardTo(id))))) - .expectService(forward(2, ServiceTwo, "Call", Request(id))) + .expectIncoming(command(2, id, "Process", Request(id, Seq(forwardTo(id))))) + .expectOutgoing(forward(2, ServiceTwo, "Call", Request(id))) val connection2 = interceptor .expectEntityConnection() - .expectClient(init(ServiceTwo, id)) - .expectClient(command(1, id, "Call", Request(id))) - .expectService(reply(1, Response())) + .expectIncoming(init(ServiceTwo, id)) + .expectIncoming(command(1, id, "Call", Request(id))) + .expectOutgoing(reply(1, Response())) tckModelClient.process(Request(id, Seq(sideEffectTo(id)))).futureValue mustBe Response("one") connection - .expectClient(command(3, id, "Process", Request(id, Seq(sideEffectTo(id))))) - .expectService(reply(3, Response("one"), sideEffects(id))) + .expectIncoming(command(3, id, "Process", Request(id, Seq(sideEffectTo(id))))) + .expectOutgoing(reply(3, Response("one"), sideEffects(id))) connection2 - .expectClient(command(2, id, "Call", Request(id))) - .expectService(reply(2, Response())) + .expectIncoming(command(2, id, "Call", Request(id))) + .expectOutgoing(reply(2, Response())) tckModelClient.process(Request(id)).futureValue mustBe Response("one") connection - .expectClient(command(4, id, "Process", Request(id))) - .expectService(reply(4, Response("one"))) + .expectIncoming(command(4, id, "Process", Request(id))) + .expectOutgoing(reply(4, Response("one"))) } "verify failures" in valueEntityTest { id => tckModelClient.process(Request(id, updateStates("one"))).futureValue mustBe Response("one") val connection = interceptor .expectEntityConnection() - .expectClient(init(Service, id)) - .expectClient(command(1, id, "Process", Request(id, updateStates("one")))) - .expectService(reply(1, Response("one"), update("one"))) + .expectIncoming(init(Service, id)) + .expectIncoming(command(1, id, "Process", Request(id, updateStates("one")))) + .expectOutgoing(reply(1, Response("one"), update("one"))) val failed = tckModelClient.process(Request(id, Seq(failWith("expected failure")))).failed.futureValue failed mustBe a[StatusRuntimeException] failed.asInstanceOf[StatusRuntimeException].getStatus.getDescription mustBe "expected failure" connection - .expectClient(command(2, id, "Process", Request(id, Seq(failWith("expected failure"))))) - .expectService(actionFailure(2, "expected failure")) + .expectIncoming(command(2, id, "Process", Request(id, Seq(failWith("expected failure"))))) + .expectOutgoing(actionFailure(2, "expected failure")) tckModelClient.process(Request(id)).futureValue mustBe Response("one") connection - .expectClient(command(3, id, "Process", Request(id))) - .expectService(reply(3, Response("one"))) + .expectIncoming(command(3, id, "Process", Request(id))) + .expectOutgoing(reply(3, Response("one"))) } "verify HTTP API" in valueEntityTest { id => client.http.request(s"tck/model/entity/$id", "{}").futureValue mustBe """{"message":""}""" val connection = interceptor .expectEntityConnection() - .expectClient(init(Service, id)) - .expectClient(command(1, id, "Process", Request(id))) - .expectService(reply(1, Response())) + .expectIncoming(init(Service, id)) + .expectIncoming(command(1, id, "Process", Request(id))) + .expectOutgoing(reply(1, Response())) client.http .request(s"tck/model/entity/$id", """{"actions": [{"update": {"value": "one"}}]}""") .futureValue mustBe """{"message":"one"}""" connection - .expectClient(command(2, id, "Process", Request(id, updateStates("one")))) - .expectService(reply(2, Response("one"), update("one"))) + .expectIncoming(command(2, id, "Process", Request(id, updateStates("one")))) + .expectOutgoing(reply(2, Response("one"), update("one"))) client.http .requestToError(s"tck/model/entity/$id", """{"actions": [{"fail": {"message": "expected failure"}}]}""") .futureValue mustBe "expected failure" connection - .expectClient(command(3, id, "Process", Request(id, Seq(failWith("expected failure"))))) - .expectService(actionFailure(3, "expected failure")) + .expectIncoming(command(3, id, "Process", Request(id, Seq(failWith("expected failure"))))) + .expectOutgoing(actionFailure(3, "expected failure")) client.http .request(s"tck/model/entity/$id", """{"actions": [{"update": {"value": "two"}}]}""") .futureValue mustBe """{"message":"two"}""" connection - .expectClient(command(4, id, "Process", Request(id, updateStates("two")))) - .expectService(reply(4, Response("two"), update("two"))) + .expectIncoming(command(4, id, "Process", Request(id, updateStates("two")))) + .expectOutgoing(reply(4, Response("two"), update("two"))) client.http.request(s"tck/model/entity/$id", "{}").futureValue mustBe """{"message":"two"}""" connection - .expectClient(command(5, id, "Process", Request(id))) - .expectService(reply(5, Response("two"))) + .expectIncoming(command(5, id, "Process", Request(id))) + .expectOutgoing(reply(5, Response("two"))) } "verify passivation timeout" in valueEntityConfiguredTest { id => configuredClient.call(Request(id)) interceptor .expectEntityConnection() - .expectClient(init(ServiceConfigured, id)) - .expectClient(command(1, id, "Call", Request(id))) - .expectService(reply(1, Response())) + .expectIncoming(init(ServiceConfigured, id)) + .expectIncoming(command(1, id, "Call", Request(id))) + .expectOutgoing(reply(1, Response())) .expectClosed(2.seconds) // check passivation (with expected timeout of 100 millis) } } diff --git a/tck/src/main/scala/io/cloudstate/tck/EventSourcedEntityTCK.scala b/tck/src/main/scala/io/cloudstate/tck/EventSourcedEntityTCK.scala index 1448a9782..e8ee3afba 100644 --- a/tck/src/main/scala/io/cloudstate/tck/EventSourcedEntityTCK.scala +++ b/tck/src/main/scala/io/cloudstate/tck/EventSourcedEntityTCK.scala @@ -367,167 +367,167 @@ trait EventSourcedEntityTCK extends TCKSpec { tckModelClient.process(Request(id)).futureValue mustBe Response() val connection = interceptor .expectEventSourcedEntityConnection() - .expectClient(init(Service, id)) - .expectClient(command(1, id, "Process", Request(id))) - .expectService(reply(1, Response())) + .expectIncoming(init(Service, id)) + .expectIncoming(command(1, id, "Process", Request(id))) + .expectOutgoing(reply(1, Response())) tckModelClient.process(Request(id, emitEvents("A"))).futureValue mustBe Response("A") connection - .expectClient(command(2, id, "Process", Request(id, emitEvents("A")))) - .expectService(reply(2, Response("A"), events("A"))) + .expectIncoming(command(2, id, "Process", Request(id, emitEvents("A")))) + .expectOutgoing(reply(2, Response("A"), events("A"))) tckModelClient.process(Request(id, emitEvents("B", "C", "D"))).futureValue mustBe Response("ABCD") connection - .expectClient(command(3, id, "Process", Request(id, emitEvents("B", "C", "D")))) - .expectService(reply(3, Response("ABCD"), events("B", "C", "D"))) + .expectIncoming(command(3, id, "Process", Request(id, emitEvents("B", "C", "D")))) + .expectOutgoing(reply(3, Response("ABCD"), events("B", "C", "D"))) tckModelClient.process(Request(id, emitEvents("E"))).futureValue mustBe Response("ABCDE") connection - .expectClient(command(4, id, "Process", Request(id, emitEvents("E")))) - .expectService(reply(4, Response("ABCDE"), snapshotAndEvents("ABCDE", "E"))) + .expectIncoming(command(4, id, "Process", Request(id, emitEvents("E")))) + .expectOutgoing(reply(4, Response("ABCDE"), snapshotAndEvents("ABCDE", "E"))) tckModelClient.process(Request(id, emitEvents("F", "G", "H", "I"))).futureValue mustBe Response("ABCDEFGHI") connection - .expectClient(command(5, id, "Process", Request(id, emitEvents("F", "G", "H", "I")))) - .expectService(reply(5, Response("ABCDEFGHI"), events("F", "G", "H", "I"))) + .expectIncoming(command(5, id, "Process", Request(id, emitEvents("F", "G", "H", "I")))) + .expectOutgoing(reply(5, Response("ABCDEFGHI"), events("F", "G", "H", "I"))) tckModelClient.process(Request(id, emitEvents("J", "K"))).futureValue mustBe Response("ABCDEFGHIJK") connection - .expectClient(command(6, id, "Process", Request(id, emitEvents("J", "K")))) - .expectService(reply(6, Response("ABCDEFGHIJK"), snapshotAndEvents("ABCDEFGHIJK", "J", "K"))) + .expectIncoming(command(6, id, "Process", Request(id, emitEvents("J", "K")))) + .expectOutgoing(reply(6, Response("ABCDEFGHIJK"), snapshotAndEvents("ABCDEFGHIJK", "J", "K"))) } "verify forwards and side effects" in eventSourcedTest { id => tckModelClient.process(Request(id, emitEvents("one"))).futureValue mustBe Response("one") val connection = interceptor .expectEventSourcedEntityConnection() - .expectClient(init(Service, id)) - .expectClient(command(1, id, "Process", Request(id, emitEvents("one")))) - .expectService(reply(1, Response("one"), events("one"))) + .expectIncoming(init(Service, id)) + .expectIncoming(command(1, id, "Process", Request(id, emitEvents("one")))) + .expectOutgoing(reply(1, Response("one"), events("one"))) tckModelClient.process(Request(id, Seq(forwardTo(id)))).futureValue mustBe Response() connection - .expectClient(command(2, id, "Process", Request(id, Seq(forwardTo(id))))) - .expectService(forward(2, ServiceTwo, "Call", Request(id))) + .expectIncoming(command(2, id, "Process", Request(id, Seq(forwardTo(id))))) + .expectOutgoing(forward(2, ServiceTwo, "Call", Request(id))) val connection2 = interceptor .expectEventSourcedEntityConnection() - .expectClient(init(ServiceTwo, id)) - .expectClient(command(1, id, "Call", Request(id))) - .expectService(reply(1, Response())) + .expectIncoming(init(ServiceTwo, id)) + .expectIncoming(command(1, id, "Call", Request(id))) + .expectOutgoing(reply(1, Response())) tckModelClient.process(Request(id, Seq(sideEffectTo(id)))).futureValue mustBe Response("one") connection - .expectClient(command(3, id, "Process", Request(id, Seq(sideEffectTo(id))))) - .expectService(reply(3, Response("one"), sideEffects(id))) + .expectIncoming(command(3, id, "Process", Request(id, Seq(sideEffectTo(id))))) + .expectOutgoing(reply(3, Response("one"), sideEffects(id))) connection2 - .expectClient(command(2, id, "Call", Request(id))) - .expectService(reply(2, Response())) + .expectIncoming(command(2, id, "Call", Request(id))) + .expectOutgoing(reply(2, Response())) tckModelClient.process(Request(id)).futureValue mustBe Response("one") connection - .expectClient(command(4, id, "Process", Request(id))) - .expectService(reply(4, Response("one"))) + .expectIncoming(command(4, id, "Process", Request(id))) + .expectOutgoing(reply(4, Response("one"))) } "verify failures" in eventSourcedTest { id => tckModelClient.process(Request(id, emitEvents("1", "2", "3"))).futureValue mustBe Response("123") val connection = interceptor .expectEventSourcedEntityConnection() - .expectClient(init(Service, id)) - .expectClient(command(1, id, "Process", Request(id, emitEvents("1", "2", "3")))) - .expectService(reply(1, Response("123"), events("1", "2", "3"))) + .expectIncoming(init(Service, id)) + .expectIncoming(command(1, id, "Process", Request(id, emitEvents("1", "2", "3")))) + .expectOutgoing(reply(1, Response("123"), events("1", "2", "3"))) val failed = tckModelClient.process(Request(id, Seq(failWith("expected failure")))).failed.futureValue failed mustBe a[StatusRuntimeException] failed.asInstanceOf[StatusRuntimeException].getStatus.getDescription mustBe "expected failure" connection - .expectClient(command(2, id, "Process", Request(id, Seq(failWith("expected failure"))))) - .expectService(actionFailure(2, "expected failure")) + .expectIncoming(command(2, id, "Process", Request(id, Seq(failWith("expected failure"))))) + .expectOutgoing(actionFailure(2, "expected failure")) tckModelClient.process(Request(id)).futureValue mustBe Response("123") connection - .expectClient(command(3, id, "Process", Request(id))) - .expectService(reply(3, Response("123"))) + .expectIncoming(command(3, id, "Process", Request(id))) + .expectOutgoing(reply(3, Response("123"))) val emitAndFail = Seq(emitEvent("4"), failWith("another failure"), emitEvent("5")) val failed2 = tckModelClient.process(Request(id, emitAndFail)).failed.futureValue failed2 mustBe a[StatusRuntimeException] failed2.asInstanceOf[StatusRuntimeException].getStatus.getDescription mustBe "another failure" connection - .expectClient(command(4, id, "Process", Request(id, emitAndFail))) - .expectService(actionFailure(4, "another failure", restart = true)) + .expectIncoming(command(4, id, "Process", Request(id, emitAndFail))) + .expectOutgoing(actionFailure(4, "another failure", restart = true)) .expectClosed() val connection2 = interceptor .expectEventSourcedEntityConnection() - .expectClient(init(Service, id)) - .expectClient(event(1, persisted("1"))) - .expectClient(event(2, persisted("2"))) - .expectClient(event(3, persisted("3"))) + .expectIncoming(init(Service, id)) + .expectIncoming(event(1, persisted("1"))) + .expectIncoming(event(2, persisted("2"))) + .expectIncoming(event(3, persisted("3"))) tckModelClient.process(Request(id, emitEvents("4", "5"))).futureValue mustBe Response("12345") connection2 - .expectClient(command(1, id, "Process", Request(id, emitEvents("4", "5")))) - .expectService(reply(1, Response("12345"), snapshotAndEvents("12345", "4", "5"))) + .expectIncoming(command(1, id, "Process", Request(id, emitEvents("4", "5")))) + .expectOutgoing(reply(1, Response("12345"), snapshotAndEvents("12345", "4", "5"))) } "verify HTTP API" in eventSourcedTest { id => client.http.request(s"tck/model/eventsourced/$id", "{}").futureValue mustBe """{"message":""}""" val connection = interceptor .expectEventSourcedEntityConnection() - .expectClient(init(Service, id)) - .expectClient(command(1, id, "Process", Request(id))) - .expectService(reply(1, Response())) + .expectIncoming(init(Service, id)) + .expectIncoming(command(1, id, "Process", Request(id))) + .expectOutgoing(reply(1, Response())) client.http .request(s"tck/model/eventsourced/$id", """{"actions": [{"emit": {"value": "x"}}]}""") .futureValue mustBe """{"message":"x"}""" connection - .expectClient(command(2, id, "Process", Request(id, emitEvents("x")))) - .expectService(reply(2, Response("x"), events("x"))) + .expectIncoming(command(2, id, "Process", Request(id, emitEvents("x")))) + .expectOutgoing(reply(2, Response("x"), events("x"))) client.http .requestToError(s"tck/model/eventsourced/$id", """{"actions": [{"fail": {"message": "expected failure"}}]}""") .futureValue mustBe "expected failure" connection - .expectClient(command(3, id, "Process", Request(id, Seq(failWith("expected failure"))))) - .expectService(actionFailure(3, "expected failure")) + .expectIncoming(command(3, id, "Process", Request(id, Seq(failWith("expected failure"))))) + .expectOutgoing(actionFailure(3, "expected failure")) client.http .request(s"tck/model/eventsourced/$id", """{"actions": [{"emit": {"value": "y"}}]}""") .futureValue mustBe """{"message":"xy"}""" connection - .expectClient(command(4, id, "Process", Request(id, emitEvents("y")))) - .expectService(reply(4, Response("xy"), events("y"))) + .expectIncoming(command(4, id, "Process", Request(id, emitEvents("y")))) + .expectOutgoing(reply(4, Response("xy"), events("y"))) client.http .requestToError(s"tck/model/eventsourced/$id", """{"actions": [{"emit": {"value": "z"}}, {"fail": {"message": "emit then fail"}}]}""") .futureValue mustBe "emit then fail" connection - .expectClient(command(5, id, "Process", Request(id, Seq(emitEvent("z"), failWith("emit then fail"))))) - .expectService(actionFailure(5, "emit then fail", restart = true)) + .expectIncoming(command(5, id, "Process", Request(id, Seq(emitEvent("z"), failWith("emit then fail"))))) + .expectOutgoing(actionFailure(5, "emit then fail", restart = true)) .expectClosed() val connection2 = interceptor .expectEventSourcedEntityConnection() - .expectClient(init(Service, id)) - .expectClient(event(1, persisted("x"))) - .expectClient(event(2, persisted("y"))) + .expectIncoming(init(Service, id)) + .expectIncoming(event(1, persisted("x"))) + .expectIncoming(event(2, persisted("y"))) client.http .request(s"tck/model/eventsourced/$id", """{"actions": [{"emit": {"value": "z"}}]}""") .futureValue mustBe """{"message":"xyz"}""" connection2 - .expectClient(command(1, id, "Process", Request(id, emitEvents("z")))) - .expectService(reply(1, Response("xyz"), events("z"))) + .expectIncoming(command(1, id, "Process", Request(id, emitEvents("z")))) + .expectOutgoing(reply(1, Response("xyz"), events("z"))) } "verify passivation timeout" in eventSourcedConfiguredTest { id => configuredClient.call(Request(id)) interceptor .expectEventSourcedEntityConnection() - .expectClient(init(ServiceConfigured, id)) - .expectClient(command(1, id, "Call", Request(id))) - .expectService(reply(1, Response())) + .expectIncoming(init(ServiceConfigured, id)) + .expectIncoming(command(1, id, "Call", Request(id))) + .expectOutgoing(reply(1, Response())) .expectClosed(2.seconds) // check passivation (with expected timeout of 100 millis) } } diff --git a/tck/src/main/scala/io/cloudstate/tck/EventingTCK.scala b/tck/src/main/scala/io/cloudstate/tck/EventingTCK.scala index aa7b30813..5dd6ebfa4 100644 --- a/tck/src/main/scala/io/cloudstate/tck/EventingTCK.scala +++ b/tck/src/main/scala/io/cloudstate/tck/EventingTCK.scala @@ -56,11 +56,11 @@ trait EventingTCK extends TCKSpec { def verifyEventSourcedInitCommandReply(id: String): Unit = { val connection = interceptor.expectEventSourcedEntityConnection() - val init = connection.expectClientMessage[EventSourcedStreamIn.Message.Init] + val init = connection.expectIncomingMessage[EventSourcedStreamIn.Message.Init] init.value.serviceName must ===(eventlogeventing.EventSourcedEntityOne.name) init.value.entityId must ===(id) - connection.expectClientMessage[EventSourcedStreamIn.Message.Command] - connection.expectServiceMessage[EventSourcedStreamOut.Message.Reply] + connection.expectIncomingMessage[EventSourcedStreamIn.Message.Command] + connection.expectOutgoingMessage[EventSourcedStreamOut.Message.Reply] } def verifySubscriberCommandResponse(step: eventlogeventing.ProcessStep.Step): ActionResponse = { @@ -121,11 +121,11 @@ trait EventingTCK extends TCKSpec { ) val connection = interceptor.expectEventSourcedEntityConnection() - val init = connection.expectClientMessage[EventSourcedStreamIn.Message.Init] + val init = connection.expectIncomingMessage[EventSourcedStreamIn.Message.Init] init.value.serviceName must ===(eventlogeventing.EventSourcedEntityTwo.name) init.value.entityId must ===("eventlogeventing:3") - connection.expectClientMessage[EventSourcedStreamIn.Message.Command] - val reply = connection.expectServiceMessage[EventSourcedStreamOut.Message.Reply] + connection.expectIncomingMessage[EventSourcedStreamIn.Message.Command] + val reply = connection.expectOutgoingMessage[EventSourcedStreamOut.Message.Reply] reply.value.events must have size (1) reply.value.events.head.typeUrl must startWith("json.cloudstate.io/") diff --git a/testkit/src/main/scala/io/cloudstate/testkit/action/InterceptActionService.scala b/testkit/src/main/scala/io/cloudstate/testkit/action/InterceptActionService.scala index a5d2d3d4d..f25ff8c0b 100644 --- a/testkit/src/main/scala/io/cloudstate/testkit/action/InterceptActionService.scala +++ b/testkit/src/main/scala/io/cloudstate/testkit/action/InterceptActionService.scala @@ -119,13 +119,13 @@ object InterceptActionService { def expectResponse(): ActionResponse = out.expectMsgType[ActionResponse] - def expectClient(expected: ActionCommand): UnaryConnection = { + def expectIncoming(expected: ActionCommand): UnaryConnection = { val received = ignoreMetadata(command) assert(received == expected, s"Unexpected unary action command: expected $expected, found $received") this } - def expectService(expected: ActionResponse): UnaryConnection = { + def expectOutgoing(expected: ActionResponse): UnaryConnection = { val received = ignoreMetadata(expectResponse()) assert(received == expected, s"Unexpected unary action response: expected $expected, found $received") this @@ -144,13 +144,13 @@ object InterceptActionService { def expectCommand(): ActionCommand = in.expectMsgType[ActionCommand] - def expectClient(expected: ActionCommand): StreamedInConnection = { + def expectIncoming(expected: ActionCommand): StreamedInConnection = { val received = ignoreMetadata(expectCommand()) assert(received == expected, s"Unexpected streamed-in action command: expected $expected, found $received") this } - def expectService(expected: ActionResponse): StreamedInConnection = { + def expectOutgoing(expected: ActionResponse): StreamedInConnection = { val received = ignoreMetadata(expectResponse()) assert(received == expected, s"Unexpected unary action response: expected $expected, found $received") this @@ -170,13 +170,13 @@ object InterceptActionService { def expectResponse(): ActionResponse = out.expectMsgType[ActionResponse] - def expectClient(expected: ActionCommand): StreamedOutConnection = { + def expectIncoming(expected: ActionCommand): StreamedOutConnection = { val received = ignoreMetadata(command) assert(received == expected, s"Unexpected streamed-out action command: expected $expected, found $received") this } - def expectService(expected: ActionResponse): StreamedOutConnection = { + def expectOutgoing(expected: ActionResponse): StreamedOutConnection = { val received = ignoreMetadata(expectResponse()) assert(received == expected, s"Unexpected unary action response: expected $expected, found $received") this @@ -201,13 +201,13 @@ object InterceptActionService { def expectResponse(): ActionResponse = out.expectMsgType[ActionResponse] - def expectClient(expected: ActionCommand): StreamedConnection = { + def expectIncoming(expected: ActionCommand): StreamedConnection = { val received = ignoreMetadata(expectCommand()) assert(received == expected, s"Unexpected streamed action command: expected $expected, found $received") this } - def expectService(expected: ActionResponse): StreamedConnection = { + def expectOutgoing(expected: ActionResponse): StreamedConnection = { val received = ignoreMetadata(expectResponse()) assert(received == expected, s"Unexpected unary action response: expected $expected, found $received") this diff --git a/testkit/src/main/scala/io/cloudstate/testkit/crdt/InterceptCrdtService.scala b/testkit/src/main/scala/io/cloudstate/testkit/crdt/InterceptCrdtService.scala index 22be1f658..93b773af9 100644 --- a/testkit/src/main/scala/io/cloudstate/testkit/crdt/InterceptCrdtService.scala +++ b/testkit/src/main/scala/io/cloudstate/testkit/crdt/InterceptCrdtService.scala @@ -66,20 +66,20 @@ object InterceptCrdtService { private[testkit] def inSink: Sink[CrdtStreamIn, NotUsed] = Sink.actorRef(in.ref, Complete, Error.apply) private[testkit] def outSink: Sink[CrdtStreamOut, NotUsed] = Sink.actorRef(out.ref, Complete, Error.apply) - def expectClient(message: CrdtStreamIn.Message): Connection = { + def expectIncoming(message: CrdtStreamIn.Message): Connection = { in.expectMsg(CrdtStreamIn(message)) this } - def expectService(message: CrdtStreamOut.Message): Connection = { + def expectOutgoing(message: CrdtStreamOut.Message): Connection = { out.expectMsg(CrdtStreamOut(message)) this } - def expectServiceMessage[T](implicit classTag: ClassTag[T]): T = - expectServiceMessageClass(classTag.runtimeClass.asInstanceOf[Class[T]]) + def expectOutgoingMessage[T](implicit classTag: ClassTag[T]): T = + expectOutgoingMessageClass(classTag.runtimeClass.asInstanceOf[Class[T]]) - def expectServiceMessageClass[T](messageClass: Class[T]): T = { + def expectOutgoingMessageClass[T](messageClass: Class[T]): T = { val message = out.expectMsgType[CrdtStreamOut].message assert(messageClass.isInstance(message), s"expected message $messageClass, found ${message.getClass} ($message)") message.asInstanceOf[T] diff --git a/testkit/src/main/scala/io/cloudstate/testkit/eventsourced/InterceptEventSourcedService.scala b/testkit/src/main/scala/io/cloudstate/testkit/eventsourced/InterceptEventSourcedService.scala index cc8f1ed8b..818472bb6 100644 --- a/testkit/src/main/scala/io/cloudstate/testkit/eventsourced/InterceptEventSourcedService.scala +++ b/testkit/src/main/scala/io/cloudstate/testkit/eventsourced/InterceptEventSourcedService.scala @@ -66,27 +66,27 @@ object InterceptEventSourcedService { private[testkit] def inSink: Sink[EventSourcedStreamIn, NotUsed] = Sink.actorRef(in.ref, Complete, Error.apply) private[testkit] def outSink: Sink[EventSourcedStreamOut, NotUsed] = Sink.actorRef(out.ref, Complete, Error.apply) - def expectClient(message: EventSourcedStreamIn.Message): Connection = { + def expectIncoming(message: EventSourcedStreamIn.Message): Connection = { in.expectMsg(EventSourcedStreamIn(message)) this } - def expectClientMessage[T](implicit classTag: ClassTag[T]): T = { + def expectIncomingMessage[T](implicit classTag: ClassTag[T]): T = { val message = in.expectMsgType[EventSourcedStreamIn].message assert(classTag.runtimeClass.isInstance(message), s"expected message ${classTag.runtimeClass}, found ${message.getClass} ($message)") message.asInstanceOf[T] } - def expectService(message: EventSourcedStreamOut.Message): Connection = { + def expectOutgoing(message: EventSourcedStreamOut.Message): Connection = { out.expectMsg(EventSourcedStreamOut(message)) this } - def expectServiceMessage[T](implicit classTag: ClassTag[T]): T = - expectServiceMessageClass(classTag.runtimeClass.asInstanceOf[Class[T]]) + def expectOutgoingMessage[T](implicit classTag: ClassTag[T]): T = + expectOutgoingMessageClass(classTag.runtimeClass.asInstanceOf[Class[T]]) - def expectServiceMessageClass[T](messageClass: Class[T]): T = { + def expectOutgoingMessageClass[T](messageClass: Class[T]): T = { val message = out.expectMsgType[EventSourcedStreamOut].message assert(messageClass.isInstance(message), s"expected message $messageClass, found ${message.getClass} ($message)") message.asInstanceOf[T] diff --git a/testkit/src/main/scala/io/cloudstate/testkit/valueentity/InterceptValueEntityService.scala b/testkit/src/main/scala/io/cloudstate/testkit/valueentity/InterceptValueEntityService.scala index 00471daaf..c159c8b0e 100644 --- a/testkit/src/main/scala/io/cloudstate/testkit/valueentity/InterceptValueEntityService.scala +++ b/testkit/src/main/scala/io/cloudstate/testkit/valueentity/InterceptValueEntityService.scala @@ -68,20 +68,20 @@ object InterceptValueEntityService { private[testkit] def inSink: Sink[ValueEntityStreamIn, NotUsed] = Sink.actorRef(in.ref, Complete, Error.apply) private[testkit] def outSink: Sink[ValueEntityStreamOut, NotUsed] = Sink.actorRef(out.ref, Complete, Error.apply) - def expectClient(message: ValueEntityStreamIn.Message): Connection = { + def expectIncoming(message: ValueEntityStreamIn.Message): Connection = { in.expectMsg(ValueEntityStreamIn(message)) this } - def expectService(message: ValueEntityStreamOut.Message): Connection = { + def expectOutgoing(message: ValueEntityStreamOut.Message): Connection = { out.expectMsg(ValueEntityStreamOut(message)) this } - def expectServiceMessage[T](implicit classTag: ClassTag[T]): T = - expectServiceMessageClass(classTag.runtimeClass.asInstanceOf[Class[T]]) + def expectOutgoingMessage[T](implicit classTag: ClassTag[T]): T = + expectOutgoingMessageClass(classTag.runtimeClass.asInstanceOf[Class[T]]) - def expectServiceMessageClass[T](messageClass: Class[T]): T = { + def expectOutgoingMessageClass[T](messageClass: Class[T]): T = { val message = out.expectMsgType[ValueEntityStreamOut].message assert(messageClass.isInstance(message), s"expected message $messageClass, found ${message.getClass} ($message)") message.asInstanceOf[T] From 4000d76e533a8ccceae8b25911d7df6cce8fd822 Mon Sep 17 00:00:00 2001 From: Peter Vlugter Date: Mon, 21 Dec 2020 12:39:57 +1300 Subject: [PATCH 7/7] Require compatible protocol version from language support --- proxy/core/src/main/resources/reference.conf | 1 + .../proxy/CloudStateProxyMain.scala | 21 ++++++++++++------- .../proxy/EntityDiscoveryManager.scala | 11 ++++++---- tck/src/it/scala/io/cloudstate/tck/TCK.scala | 15 +++++++------ .../scala/io/cloudstate/tck/TCKSpec.scala | 15 ++++++++----- .../TestEntityDiscoveryService.scala | 7 ++++++- 6 files changed, 46 insertions(+), 24 deletions(-) diff --git a/proxy/core/src/main/resources/reference.conf b/proxy/core/src/main/resources/reference.conf index 4c672416e..e98ec0213 100644 --- a/proxy/core/src/main/resources/reference.conf +++ b/proxy/core/src/main/resources/reference.conf @@ -8,6 +8,7 @@ cloudstate.proxy { user-function-host = ${?USER_FUNCTION_HOST} user-function-port = 8080 user-function-port = ${?USER_FUNCTION_PORT} + protocol-compatibility-check = true relay-timeout = 1m max-inbound-message-size = 12M relay-buffer-size = 100 diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/CloudStateProxyMain.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/CloudStateProxyMain.scala index 3e66af1db..20386cfa3 100644 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/CloudStateProxyMain.scala +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/CloudStateProxyMain.scala @@ -20,7 +20,7 @@ import java.util.concurrent.ThreadLocalRandom import java.util.concurrent.atomic.AtomicLong import com.typesafe.config.Config -import akka.actor.{ActorSelection, ActorSystem} +import akka.actor.{ActorSelection, ActorSystem, OneForOneStrategy, SupervisorStrategy} import akka.pattern.ask import akka.util.Timeout import akka.cluster.Cluster @@ -177,13 +177,18 @@ object CloudStateProxyMain { system.actorOf( BackoffSupervisor.props( - BackoffOpts.onFailure( - EntityDiscoveryManager.props(serverConfig), - childName = "server-manager", - minBackoff = appConfig.backoffMin, - maxBackoff = appConfig.backoffMax, - randomFactor = appConfig.backoffRandomFactor - ) + BackoffOpts + .onFailure( + EntityDiscoveryManager.props(serverConfig), + childName = "server-manager", + minBackoff = appConfig.backoffMin, + maxBackoff = appConfig.backoffMax, + randomFactor = appConfig.backoffRandomFactor + ) + .withSupervisorStrategy(OneForOneStrategy() { + // don't keep restarting and retrying if an entity discovery exception is thrown + case _: EntityDiscoveryException => SupervisorStrategy.Stop + }) ), "server-manager-supervisor" ) diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/EntityDiscoveryManager.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/EntityDiscoveryManager.scala index fc5b351db..4b7e60e8f 100644 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/EntityDiscoveryManager.scala +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/EntityDiscoveryManager.scala @@ -50,6 +50,7 @@ object EntityDiscoveryManager { httpPort: Int, userFunctionHost: String, userFunctionPort: Int, + protocolCompatibilityCheck: Boolean, relayTimeout: Timeout, relayOutputBufferSize: Int, maxInboundMessageSize: Long, @@ -68,6 +69,7 @@ object EntityDiscoveryManager { httpPort = config.getInt("http-port"), userFunctionHost = config.getString("user-function-host"), userFunctionPort = config.getInt("user-function-port"), + protocolCompatibilityCheck = config.getBoolean("protocol-compatibility-check"), relayTimeout = Timeout(config.getDuration("relay-timeout").toMillis.millis), maxInboundMessageSize = config.getBytes("max-inbound-message-size"), relayOutputBufferSize = config.getInt("relay-buffer-size"), @@ -203,9 +205,7 @@ class EntityDiscoveryManager(config: EntityDiscoveryManager.Configuration)( val supportedProtocolVersionString: String = s"${supportedProtocolMajorVersion}.${supportedProtocolMinorVersion}" def compatibleProtocol(majorVersion: Int, minorVersion: Int): Boolean = - // allow empty protocol version to be compatible, until all library supports report their protocol version - ((majorVersion == 0) && (minorVersion == 0)) || - // otherwise it's currently strict matching of protocol versions + // currently strict matching of protocol versions ((majorVersion == supportedProtocolMajorVersion) && (minorVersion == supportedProtocolMinorVersion)) override def receive: Receive = { @@ -213,7 +213,10 @@ class EntityDiscoveryManager(config: EntityDiscoveryManager.Configuration)( log.info("Received EntitySpec from user function with info: {}", spec.getServiceInfo) try { - if (!compatibleProtocol(spec.getServiceInfo.protocolMajorVersion, spec.getServiceInfo.protocolMinorVersion)) + if (!config.protocolCompatibilityCheck) + log.warning("Protocol version compatibility is configured to be ignored") + else if (!compatibleProtocol(spec.getServiceInfo.protocolMajorVersion, + spec.getServiceInfo.protocolMinorVersion)) throw EntityDiscoveryException( s"Incompatible protocol version ${spec.getServiceInfo.protocolMajorVersion}.${spec.getServiceInfo.protocolMinorVersion}, only $supportedProtocolVersionString is supported" ) diff --git a/tck/src/it/scala/io/cloudstate/tck/TCK.scala b/tck/src/it/scala/io/cloudstate/tck/TCK.scala index 689ac3d7e..339f56f96 100644 --- a/tck/src/it/scala/io/cloudstate/tck/TCK.scala +++ b/tck/src/it/scala/io/cloudstate/tck/TCK.scala @@ -56,15 +56,18 @@ class ManagedCloudstateTCK(config: TckConfiguration) extends CloudstateTCK("for val processes: TckProcesses = TckProcesses.create(config) - override def start(): Unit = try { + override def start(): Unit = { processes.service.start() super.start() processes.proxy.start() - } catch { - case error: Throwable => - processes.service.logs("service") - processes.proxy.logs("proxy") - throw error + } + + override def onStartError(error: Throwable): Unit = { + processes.service.logs("service") + processes.proxy.logs("proxy") + try processes.proxy.stop() + finally try processes.service.stop() + finally throw error } override def afterAll(): Unit = { diff --git a/tck/src/main/scala/io/cloudstate/tck/TCKSpec.scala b/tck/src/main/scala/io/cloudstate/tck/TCKSpec.scala index d0e094cc3..aab13028c 100644 --- a/tck/src/main/scala/io/cloudstate/tck/TCKSpec.scala +++ b/tck/src/main/scala/io/cloudstate/tck/TCKSpec.scala @@ -67,11 +67,16 @@ trait TCKSpec @volatile private[this] var _entitySpec: EntitySpec = EntitySpec() @volatile private[this] var _proxyInfo: ProxyInfo = ProxyInfo() - override def beforeAll(): Unit = { - start() - discover() - super.beforeAll() - } + override def beforeAll(): Unit = + try { + start() + discover() + super.beforeAll() + } catch { + case error: Throwable => onStartError(error) + } + + def onStartError(error: Throwable): Unit = throw error override def afterEach(): Unit = { super.afterEach() diff --git a/testkit/src/main/scala/io/cloudstate/testkit/discovery/TestEntityDiscoveryService.scala b/testkit/src/main/scala/io/cloudstate/testkit/discovery/TestEntityDiscoveryService.scala index b859acf7a..aa0f0f02d 100644 --- a/testkit/src/main/scala/io/cloudstate/testkit/discovery/TestEntityDiscoveryService.scala +++ b/testkit/src/main/scala/io/cloudstate/testkit/discovery/TestEntityDiscoveryService.scala @@ -23,6 +23,7 @@ import com.google.protobuf.DescriptorProtos import com.google.protobuf.Descriptors.{FileDescriptor, ServiceDescriptor} import com.google.protobuf.empty.{Empty => ScalaPbEmpty} import io.cloudstate.protocol.entity._ +import io.cloudstate.testkit.BuildInfo import io.cloudstate.testkit.TestService.TestServiceContext import scala.concurrent.{Future, Promise} @@ -40,7 +41,11 @@ final class TestEntityDiscoveryService(context: TestServiceContext) { } object TestEntityDiscoveryService { - val info: ServiceInfo = ServiceInfo(supportLibraryName = "Cloudstate TestKit") + val info: ServiceInfo = ServiceInfo( + supportLibraryName = "Cloudstate TestKit", + protocolMajorVersion = BuildInfo.protocolMajorVersion, + protocolMinorVersion = BuildInfo.protocolMinorVersion + ) def entitySpec(entityType: String, service: ServiceDescription): EntitySpec = { import scala.jdk.CollectionConverters._