From 2a281c3ea3e3734f6c079f43d5048c58315824f2 Mon Sep 17 00:00:00 2001
From: rschoene <rene.schoene@tu-dresden.de>
Date: Thu, 3 Feb 2022 10:36:37 +0100
Subject: [PATCH] working on attributes as endpoint target

- change syntax in connect file, avoid colon
- begin with AttributeTest
- discovered error when endpoint is connected twice
---
 .../src/main/jastadd/Analysis.jrag            |  16 +-
 .../src/main/jastadd/Intermediate.jadd        |  22 ++-
 .../src/main/jastadd/Intermediate.relast      |   1 +
 .../src/main/jastadd/Navigation.jrag          |  13 ++
 .../src/main/jastadd/parser/RagConnect.parser |   2 +-
 .../src/main/jastadd/scanner/Keywords.flex    |   4 +-
 .../src/main/resources/handler.mustache       |   8 +
 .../main/resources/sendDefinition.mustache    |   8 +-
 ragconnect.tests/build.gradle                 |   1 +
 .../src/test/01-input/attribute/Test.connect  |  12 +-
 .../src/test/01-input/attribute/Test.relast   |   2 +-
 .../ragconnect/tests/AttributeTest.java       | 148 +++++++++++++++++-
 12 files changed, 215 insertions(+), 22 deletions(-)

diff --git a/ragconnect.base/src/main/jastadd/Analysis.jrag b/ragconnect.base/src/main/jastadd/Analysis.jrag
index b48146f..b5fb2ce 100644
--- a/ragconnect.base/src/main/jastadd/Analysis.jrag
+++ b/ragconnect.base/src/main/jastadd/Analysis.jrag
@@ -2,6 +2,19 @@ aspect Analysis {
   // --- isAlreadyDefined ---
   syn boolean EndpointDefinition.isAlreadyDefined() = getEndpointTarget().isAlreadyDefined();
   syn boolean EndpointTarget.isAlreadyDefined();
+  eq AttributeEndpointTarget.isAlreadyDefined() {
+    // define lookup here, as not used elsewhere
+    int numberOfSameDefs = 0;
+    for (EndpointTarget target : ragconnect().givenEndpointTargetList()) {
+      if (target.isAttributeEndpointTarget()) {
+        AttributeEndpointTarget other = target.asAttributeEndpointTarget();
+        if (other.getParentTypeDecl().equals(this.getParentTypeDecl()) && other.getName().equals(this.getName())) {
+          numberOfSameDefs += 1;
+        }
+      }
+    }
+    return numberOfSameDefs > 1;
+  }
   eq TokenEndpointTarget.isAlreadyDefined() {
     return lookupTokenEndpointDefinitions(getToken()).stream()
         .filter(containingEndpointDefinition()::matchesType)
@@ -63,8 +76,9 @@ aspect Analysis {
     }
   }
 
+  // TODO rename entityIsNormalAttribute to actual meaning
   syn boolean EndpointTarget.entityIsNormalAttribute();
-  // TODO AttributeEndpointTarget.entityIsNormalAttribute
+  eq AttributeEndpointTarget.entityIsNormalAttribute() = true;
   eq TokenEndpointTarget.entityIsNormalAttribute() = !getToken().getNTA();
   eq TypeEndpointTarget.entityIsNormalAttribute() = !getType().getNTA();
   eq ContextFreeTypeEndpointTarget.entityIsNormalAttribute() = false;
diff --git a/ragconnect.base/src/main/jastadd/Intermediate.jadd b/ragconnect.base/src/main/jastadd/Intermediate.jadd
index b215f18..e26b797 100644
--- a/ragconnect.base/src/main/jastadd/Intermediate.jadd
+++ b/ragconnect.base/src/main/jastadd/Intermediate.jadd
@@ -168,6 +168,11 @@ aspect MustacheMappingApplicationAndDefinition {
   syn String MEndpointDefinition.preemptiveReturn();
   syn String MEndpointDefinition.firstInputVarName();
 
+  // TODO check MAttributeSendDefinition
+  eq MAttributeSendDefinition.firstInputVarName() = getterMethodCall();
+  eq MAttributeSendDefinition.preemptiveExpectedValue() = lastValueGetterCall();
+  eq MAttributeSendDefinition.preemptiveReturn() = "return false;";
+
   eq MTokenReceiveDefinition.firstInputVarName() = "message";
   eq MTokenReceiveDefinition.preemptiveExpectedValue() = getterMethodCall();
   eq MTokenReceiveDefinition.preemptiveReturn() = "return;";
@@ -323,7 +328,7 @@ aspect MustacheRagConnect {
 
 aspect MustacheReceiveAndSendAndHandleUri {
   // === EndpointDefinition ===
-  syn String EndpointDefinition.connectMethodName() = "connect" + entityName();
+  syn String EndpointDefinition.connectMethodName() = "connect" + capitalize(entityName());
 
   syn String EndpointDefinition.connectParameterName() = "uriString";
 
@@ -348,7 +353,7 @@ aspect MustacheReceiveAndSendAndHandleUri {
     } else {
       extra = "";
     }
-    return "disconnect" + extra + entityName();
+    return "disconnect" + extra + capitalize(entityName());
   }
 
   syn String EndpointDefinition.entityName() = getEndpointTarget().entityName();
@@ -371,7 +376,7 @@ aspect MustacheReceiveAndSendAndHandleUri {
   syn String EndpointTarget.parentTypeName();
   syn String EndpointTarget.entityName();
 
-  // TODO AttributeEndpointTarget.getterName
+  eq AttributeEndpointTarget.getterMethodName() = getName();
   eq AttributeEndpointTarget.parentTypeName() = getParentTypeDecl().getName();
   eq AttributeEndpointTarget.entityName() = getName();
 
@@ -463,9 +468,14 @@ aspect MustacheSendDefinition {
   syn String EndpointTarget.senderName() = ragconnect().internalRagConnectPrefix() + "_sender_" + entityName();
   eq ContextFreeTypeEndpointTarget.senderName() = null;
 
+  // TOOO both updateMethodName and writeMethodName could be defined for MEndpointDefinition using getEndpointDefinition().enitityName()
   syn String MEndpointDefinition.updateMethodName();
   syn String MEndpointDefinition.writeMethodName();
 
+  // TODO check MAttributeSendDefinition. maybe name-clash possible if there is an attribute with same name as a child
+  eq MAttributeSendDefinition.updateMethodName() = ragconnect().internalRagConnectPrefix() + "_update_" + getEndpointDefinition().entityName();
+  eq MAttributeSendDefinition.writeMethodName() = ragconnect().internalRagConnectPrefix() + "_writeLastValue_" + getEndpointDefinition().entityName();
+
   eq MTokenReceiveDefinition.updateMethodName() = null;
   eq MTokenReceiveDefinition.writeMethodName() = null;
 
@@ -582,6 +592,12 @@ aspect AttributesForMustache {
     return result;
   }
   abstract MEndpointDefinition EndpointTarget.createMEndpointDefinition(boolean isSend);
+  MEndpointDefinition AttributeEndpointTarget.createMEndpointDefinition(boolean isSend) {
+    if (!isSend) {
+      throw new IllegalArgumentException("AttributeEndpointTarget can only be sent!");
+    }
+    return new MAttributeSendDefinition();
+  }
   MEndpointDefinition TokenEndpointTarget.createMEndpointDefinition(boolean isSend) {
     return isSend ? new MTokenSendDefinition() : new MTokenReceiveDefinition();
   }
diff --git a/ragconnect.base/src/main/jastadd/Intermediate.relast b/ragconnect.base/src/main/jastadd/Intermediate.relast
index 7909750..486158a 100644
--- a/ragconnect.base/src/main/jastadd/Intermediate.relast
+++ b/ragconnect.base/src/main/jastadd/Intermediate.relast
@@ -1,6 +1,7 @@
 abstract MEndpointDefinition ::= InnerMappingDefinition:MInnerMappingDefinition*;
 rel MEndpointDefinition.EndpointDefinition -> EndpointDefinition;
 
+MAttributeSendDefinition : MEndpointDefinition;
 abstract MTokenEndpointDefinition : MEndpointDefinition;
 MTokenReceiveDefinition : MTokenEndpointDefinition;
 MTokenSendDefinition : MTokenEndpointDefinition;
diff --git a/ragconnect.base/src/main/jastadd/Navigation.jrag b/ragconnect.base/src/main/jastadd/Navigation.jrag
index 260ed55..cb89e31 100644
--- a/ragconnect.base/src/main/jastadd/Navigation.jrag
+++ b/ragconnect.base/src/main/jastadd/Navigation.jrag
@@ -25,6 +25,12 @@ aspect NewStuff {
   syn boolean EndpointTarget.isUntypedEndpointTarget() = false;
   eq UntypedEndpointTarget.isUntypedEndpointTarget() = true;
 
+  /** Tests if EndpointTarget is a AttributeEndpointTarget.
+  *  @return 'true' if this is a AttributeEndpointTarget, otherwise 'false'
+  */
+  syn boolean EndpointTarget.isAttributeEndpointTarget() = false;
+  eq AttributeEndpointTarget.isAttributeEndpointTarget() = true;
+
   /** casts a EndpointTarget into a TokenEndpointTarget if possible.
    *  @return 'this' cast to a TokenEndpointTarget or 'null'
    */
@@ -52,6 +58,13 @@ aspect NewStuff {
   syn UntypedEndpointTarget EndpointTarget.asUntypedEndpointTarget();
   eq EndpointTarget.asUntypedEndpointTarget() = null;
   eq UntypedEndpointTarget.asUntypedEndpointTarget() = this;
+
+  /** casts a EndpointTarget into a AttributeEndpointTarget if possible.
+   *  @return 'this' cast to a AttributeEndpointTarget or 'null'
+   */
+  syn AttributeEndpointTarget EndpointTarget.asAttributeEndpointTarget();
+  eq EndpointTarget.asAttributeEndpointTarget() = null;
+  eq AttributeEndpointTarget.asAttributeEndpointTarget() = this;
 }
 aspect RagConnectNavigation {
 
diff --git a/ragconnect.base/src/main/jastadd/parser/RagConnect.parser b/ragconnect.base/src/main/jastadd/parser/RagConnect.parser
index dcd55d2..f822bf0 100644
--- a/ragconnect.base/src/main/jastadd/parser/RagConnect.parser
+++ b/ragconnect.base/src/main/jastadd/parser/RagConnect.parser
@@ -63,7 +63,7 @@ EndpointDefinition endpoint_definition_type
 
 EndpointTarget endpoint_target
   = ID.type_name DOT ID.child_name    {: return new UntypedEndpointTarget(type_name, child_name, false); :}
-  | ID.type_name DOT ID.child_name BRACKETS COLON ID.attribute_type_name
+  | ID.type_name DOT ID.child_name BRACKET_LEFT ID.attribute_type_name BRACKET_RIGHT
      {: return new UntypedEndpointTarget(type_name, child_name + ":" + attribute_type_name, true); :}
   | ID.type_name                      {: return new UntypedEndpointTarget(type_name, "", false); :}
 ;
diff --git a/ragconnect.base/src/main/jastadd/scanner/Keywords.flex b/ragconnect.base/src/main/jastadd/scanner/Keywords.flex
index ffa057b..cda59f5 100644
--- a/ragconnect.base/src/main/jastadd/scanner/Keywords.flex
+++ b/ragconnect.base/src/main/jastadd/scanner/Keywords.flex
@@ -8,5 +8,5 @@
 "with"       { return sym(Terminals.WITH); }
 "indexed"    { return sym(Terminals.INDEXED); }
 "add"        { return sym(Terminals.ADD); }
-"()"         { return sym(Terminals.BRACKETS); }
-":"          { return sym(Terminals.COLON); }
+"("          { return sym(Terminals.BRACKET_LEFT); }
+")"          { return sym(Terminals.BRACKET_RIGHT); }
diff --git a/ragconnect.base/src/main/resources/handler.mustache b/ragconnect.base/src/main/resources/handler.mustache
index a7ef597..0d36873 100644
--- a/ragconnect.base/src/main/resources/handler.mustache
+++ b/ragconnect.base/src/main/resources/handler.mustache
@@ -110,6 +110,10 @@ aspect RagConnectHandler {
       senders.forEach(Runnable::run);
     }
 
+    void run(RagConnectToken token) {
+      tokenToSender.get(token).run();
+    }
+
     byte[] getLastValue() {
       return lastValue;
     }
@@ -150,6 +154,10 @@ aspect RagConnectHandler {
       java.util.Optional.ofNullable(publishers.get(index)).ifPresent(RagConnectPublisher::run);
     }
 
+    void run(int index, RagConnectToken token) {
+      java.util.Optional.ofNullable(publishers.get(index)).ifPresent(publisher -> publisher.run(token));
+    }
+
     byte[] getLastValue(int index) {
       RagConnectPublisher publisher = publishers.get(index);
       if (publisher == null) {
diff --git a/ragconnect.base/src/main/resources/sendDefinition.mustache b/ragconnect.base/src/main/resources/sendDefinition.mustache
index 293b37c..499116e 100644
--- a/ragconnect.base/src/main/resources/sendDefinition.mustache
+++ b/ragconnect.base/src/main/resources/sendDefinition.mustache
@@ -18,7 +18,7 @@ public boolean {{parentTypeName}}.{{connectMethodName}}(String {{connectParamete
         }{{#IndexBasedListAccess}}, index{{/IndexBasedListAccess}}, connectToken);
       {{updateMethodName}}({{#IndexBasedListAccess}}index{{/IndexBasedListAccess}});
       if (writeCurrentValue) {
-        {{writeMethodName}}({{#IndexBasedListAccess}}index{{/IndexBasedListAccess}});
+        {{writeMethodName}}({{#IndexBasedListAccess}}index, {{/IndexBasedListAccess}}connectToken);
       }
       success = true;
       break;
@@ -49,7 +49,7 @@ public boolean {{parentTypeName}}.{{connectMethodName}}(String {{connectParamete
       {{#IndexBasedListAccess}}index,{{/IndexBasedListAccess}}
       () -> {
         if (this.{{updateMethodName}}({{#IndexBasedListAccess}}index{{/IndexBasedListAccess}})) {
-          this.{{writeMethodName}}({{#IndexBasedListAccess}}index{{/IndexBasedListAccess}});
+          this.{{writeMethodName}}({{#IndexBasedListAccess}}index, {{/IndexBasedListAccess}}connectToken);
         }
       }
     );
@@ -106,8 +106,8 @@ protected boolean {{parentTypeName}}.{{updateMethodName}}({{#IndexBasedListAcces
   return {{senderName}} != null;
 }
 
-protected void {{parentTypeName}}.{{writeMethodName}}({{#IndexBasedListAccess}}int index{{/IndexBasedListAccess}}) {
-  {{senderName}}.run({{#IndexBasedListAccess}}index{{/IndexBasedListAccess}});
+protected void {{parentTypeName}}.{{writeMethodName}}({{#IndexBasedListAccess}}int index, {{/IndexBasedListAccess}}RagConnectToken token) {
+  {{senderName}}.run({{#IndexBasedListAccess}}index, {{/IndexBasedListAccess}}token);
 }
 
 {{#needForwardingNTA}}
diff --git a/ragconnect.tests/build.gradle b/ragconnect.tests/build.gradle
index b545b19..f57f477 100644
--- a/ragconnect.tests/build.gradle
+++ b/ragconnect.tests/build.gradle
@@ -50,6 +50,7 @@ dependencies {
     testRuntimeOnly group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: '5.4.0'
     testImplementation group: 'org.assertj', name: 'assertj-core', version: '3.12.1'
     testImplementation group: 'org.awaitility', name: 'awaitility', version: '4.1.1'
+    testImplementation group: 'de.tudresden.inf.st', name: 'dumpAst', version: '0.3.5'
 
     // jackson (for serialization of types)
     implementation group: 'com.fasterxml.jackson.core', name: 'jackson-core', version: '2.12.1'
diff --git a/ragconnect.tests/src/test/01-input/attribute/Test.connect b/ragconnect.tests/src/test/01-input/attribute/Test.connect
index 3e6a66e..03688a9 100644
--- a/ragconnect.tests/src/test/01-input/attribute/Test.connect
+++ b/ragconnect.tests/src/test/01-input/attribute/Test.connect
@@ -1,14 +1,14 @@
-send SenderRoot.basic():String ;
-send SenderRoot.simple():String ;
-send SenderRoot.transformed():int ;
-send SenderRoot.toReferenceType():A ;
-send SenderRoot.toNTA():A ;
+send SenderRoot.basic(String) ;
+send SenderRoot.simple(String) ;
+send SenderRoot.transformed(int) ;
+send SenderRoot.toReferenceType(A) ;
+send SenderRoot.toNTA(A) ;
 
 AddSuffix maps A a to A {:
   A result = new A();
   String changedValue = a.getValue() + "post";
   result.setValue(changedValue);
-  result.setInner(new Inner("inner" + changedValue));
+  result.setInner(new Inner("inner" + a.getInner().getInnerValue()));
   return result;
 :}
 
diff --git a/ragconnect.tests/src/test/01-input/attribute/Test.relast b/ragconnect.tests/src/test/01-input/attribute/Test.relast
index d7edb5c..7afc9b7 100644
--- a/ragconnect.tests/src/test/01-input/attribute/Test.relast
+++ b/ragconnect.tests/src/test/01-input/attribute/Test.relast
@@ -1,4 +1,4 @@
-Root ::= SenderRoot ReceiverRoot;
+Root ::= SenderRoot* ReceiverRoot;
 SenderRoot ::= <Input> ;
 ReceiverRoot ::=
     <FromBasic>
diff --git a/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/AttributeTest.java b/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/AttributeTest.java
index 6b87744..ed6ef0c 100644
--- a/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/AttributeTest.java
+++ b/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/AttributeTest.java
@@ -1,9 +1,22 @@
 package org.jastadd.ragconnect.tests;
 
-import attribute.ast.*;
+import attributeInc.ast.*;
+import de.tudresden.inf.st.jastadd.dumpAst.ast.Dumper;
 import org.junit.jupiter.api.Tag;
 
 import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.concurrent.Callable;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Predicate;
+
+import static java.util.function.Predicate.isEqual;
+import static org.awaitility.Awaitility.await;
+import static org.jastadd.ragconnect.tests.TestUtils.mqttUri;
+import static org.jastadd.ragconnect.tests.TestUtils.waitForMqtt;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 
 /**
  * Test case "attribute".
@@ -13,27 +26,150 @@ import java.io.IOException;
 @Tag("Incremental")
 @Tag("New")
 public class AttributeTest extends AbstractMqttTest {
-  Root model;
-  MqttHandler handler;
+
+  private static final String TOPIC_WILDCARD = "attr/#";
+  private static final String TOPIC_BASIC = "attr/string/basic";
+  private static final String TOPIC_SIMPLE_NO_MAPPING = "attr/string/simple/plain";
+  private static final String TOPIC_SIMPLE_WITH_MAPPING = "attr/string/simple/mapped";
+  private static final String TOPIC_TRANSFORMED_NO_MAPPING = "attr/int/transformed/plain";
+  private static final String TOPIC_TRANSFORMED_WITH_MAPPING = "attr/int/transformed/mapped";
+  private static final String TOPIC_REFERENCE_TYPE_NO_MAPPING = "attr/a/ref/plain";
+  private static final String TOPIC_REFERENCE_TYPE_WITH_MAPPING = "attr/a/ref/mapped";
+  private static final String TOPIC_NTA_NO_MAPPING = "attr/a/nta/plain";
+  private static final String TOPIC_NTA_WITH_MAPPING = "attr/a/nta/mapped";
+
+  private static final String INITIAL_STRING = "initial";
+
+  private MqttHandler handler;
+  private ReceiverData data;
+
+  private Root model;
+  private SenderRoot senderString;
+  private SenderRoot senderInt;
+  private SenderRoot senderA;
+  private ReceiverRoot receiverRoot;
 
   @Override
   protected void createModel() {
-
+    model = new Root();
+    // model.trace().setReceiver(TestUtils::logEvent);
+    senderString = new SenderRoot().setInput(INITIAL_STRING);
+    senderInt = new SenderRoot().setInput(INITIAL_STRING);
+    senderA = new SenderRoot().setInput(INITIAL_STRING);
+    receiverRoot = new ReceiverRoot();
+    model.addSenderRoot(senderString);
+    model.addSenderRoot(senderInt);
+    model.addSenderRoot(senderA);
+    model.setReceiverRoot(receiverRoot);
   }
 
   @Override
   protected void setupReceiverAndConnect() throws IOException, InterruptedException {
+    model.ragconnectSetupMqttWaitUntilReady(2, TimeUnit.SECONDS);
+    handler = new MqttHandler().setHost(TestUtils.getMqttHost()).dontSendWelcomeMessage();
+    assertTrue(handler.waitUntilReady(2, TimeUnit.SECONDS));
+
+    data = new ReceiverData();
+    assertTrue(handler.newConnection(TOPIC_WILDCARD, bytes -> data.numberOfValues += 1));
+
+    // connect receive
+    assertTrue(receiverRoot.connectFromBasic(mqttUri(TOPIC_BASIC)));
+    assertTrue(receiverRoot.connectFromSimpleNoMapping(mqttUri(TOPIC_SIMPLE_NO_MAPPING)));
+    assertTrue(receiverRoot.connectFromSimpleWithMapping(mqttUri(TOPIC_SIMPLE_WITH_MAPPING)));
+    assertTrue(receiverRoot.connectFromTransformedNoMapping(mqttUri(TOPIC_TRANSFORMED_NO_MAPPING)));
+    assertTrue(receiverRoot.connectFromTransformedWithMapping(mqttUri(TOPIC_TRANSFORMED_WITH_MAPPING)));
+    assertTrue(receiverRoot.connectFromReferenceTypeNoMapping(mqttUri(TOPIC_REFERENCE_TYPE_NO_MAPPING)));
+    assertTrue(receiverRoot.connectFromReferenceTypeWithMapping(mqttUri(TOPIC_REFERENCE_TYPE_WITH_MAPPING)));
+    assertTrue(receiverRoot.connectFromNTANoMapping(mqttUri(TOPIC_NTA_NO_MAPPING)));
+    assertTrue(receiverRoot.connectFromNTAWithMapping(mqttUri(TOPIC_NTA_WITH_MAPPING)));
 
+    // connect send, and wait to receive (if writeCurrentValue is set)
+    assertTrue(senderString.connectBasic(mqttUri(TOPIC_BASIC), isWriteCurrentValue()));
+    assertTrue(senderString.connectSimple(mqttUri(TOPIC_SIMPLE_NO_MAPPING), isWriteCurrentValue()));
+    assertTrue(senderString.connectSimple(mqttUri(TOPIC_SIMPLE_WITH_MAPPING), isWriteCurrentValue()));
+
+    assertTrue(senderInt.connectTransformed(mqttUri(TOPIC_TRANSFORMED_NO_MAPPING), isWriteCurrentValue()));
+    assertTrue(senderInt.connectTransformed(mqttUri(TOPIC_TRANSFORMED_WITH_MAPPING), isWriteCurrentValue()));
+
+    assertTrue(senderA.connectToReferenceType(mqttUri(TOPIC_REFERENCE_TYPE_NO_MAPPING), isWriteCurrentValue()));
+    assertTrue(senderA.connectToReferenceType(mqttUri(TOPIC_REFERENCE_TYPE_WITH_MAPPING), isWriteCurrentValue()));
+    assertTrue(senderA.connectToNTA(mqttUri(TOPIC_NTA_NO_MAPPING), isWriteCurrentValue()));
+    assertTrue(senderA.connectToNTA(mqttUri(TOPIC_NTA_WITH_MAPPING), isWriteCurrentValue()));
+
+    waitForValue(senderString.basic(), receiverRoot::getFromBasic);
+    waitForValue(senderString.simple(), receiverRoot::getFromSimpleNoMapping);
+    waitForNonNull(receiverRoot::getFromReferenceTypeNoMapping);
+    waitForNonNull(receiverRoot::getFromNTANoMapping);
+  }
+
+  private <T> void waitForValue(T expectedValue, Callable<T> callable) {
+    if (isWriteCurrentValue()) {
+      await().until(callable, isEqual(expectedValue));
+    }
+  }
+
+  private <T> void waitForNonNull(Callable<T> callable) {
+    if (isWriteCurrentValue()) {
+      await().until(callable, Predicate.not(isEqual(null)));
+    }
   }
 
   @Override
   protected void communicateSendInitialValue() throws IOException, InterruptedException {
+    // basic, simple   <-- senderString
+    // transformed     <-- senderInt
+    // ref-type, nta   <-- senderA
+    check(9, INITIAL_STRING, INITIAL_STRING + "Post", INITIAL_STRING, INITIAL_STRING, INITIAL_STRING);
 
+    senderString.setInput("test-01");
+    check(12, "test-01", "test-01Post", INITIAL_STRING, INITIAL_STRING, INITIAL_STRING);
   }
 
   @Override
   protected void communicateOnlyUpdatedValue() throws IOException, InterruptedException {
+    waitForMqtt();
+  }
+
+  private void check(int numberOfValues, String basic, String simple, String transformed,
+                     String refType, String nta) {
+    awaitEquals(numberOfValues, () -> data.numberOfValues, "numberOfValues");
+
+    try {
+      Path tempFile = Files.createTempFile("receiverRoot", "yml");
+      Dumper.read(receiverRoot).dumpAsYaml(tempFile, false);
+      String content = Files.readString(tempFile);
+      logger.debug("receiverRoot\n" + content);
+    } catch (IOException e) {
+      e.printStackTrace();
+    }
+
+    awaitEquals(basic, receiverRoot::getFromBasic, "basic");
+
+    awaitEquals(simple, receiverRoot::getFromSimpleNoMapping, "simple");
+    awaitEquals(simple + "post", receiverRoot::getFromSimpleWithMapping, "simple mapped");
+
+    int transformedLength = transformed.length();
+    awaitEquals(transformedLength, receiverRoot::getFromTransformedNoMapping, "transformed");
+    awaitEquals(transformedLength + 1, receiverRoot::getFromTransformedWithMapping, "transformed mapped");
 
+    checkA(refType, "1",
+            receiverRoot.getFromReferenceTypeNoMapping(), "ref-type");
+    checkA(refType + "post", "inner1",
+            receiverRoot.getFromReferenceTypeWithMapping(), "ref-type mapped");
+
+    checkA(nta, "2",
+            receiverRoot.getFromNTANoMapping(), "nta");
+    checkA(nta + "post", "inner2",
+            receiverRoot.getFromNTAWithMapping(), "nta mapped");
+  }
+
+  private void checkA(String expectedValue, String expectedInner, A actual, String message) {
+    awaitEquals(expectedValue, actual::getValue, message + " value");
+    awaitEquals(expectedInner, actual.getInner()::getInnerValue, message + " inner");
+  }
+
+  private <T> void awaitEquals(T expected, Callable<T> actual, String alias) {
+    await(alias).atMost(1500, TimeUnit.MILLISECONDS).until(actual, isEqual(expected));
   }
 
   @Override
@@ -45,4 +181,8 @@ public class AttributeTest extends AbstractMqttTest {
       model.ragconnectCloseConnections();
     }
   }
+
+  private static class ReceiverData {
+    int numberOfValues = 0;
+  }
 }
-- 
GitLab