From 130bc9d1750a0dab358b537b79f7c1508197b086 Mon Sep 17 00:00:00 2001
From: rschoene <rene.schoene@tu-dresden.de>
Date: Thu, 2 Jun 2022 16:36:48 +0200
Subject: [PATCH] WIP: Working on bugfixes for attributes

- parse complete JavaUse as type for attribute targets
- add new observer entries (inc. eval) for NTA and attribute targets
- tests currently not passing
---
 ragconnect.base/build.gradle                  |   2 +-
 .../src/main/jastadd/Intermediate.jadd        |  35 ++++-
 .../src/main/jastadd/Intermediate.relast      |   2 +
 .../src/main/jastadd/parser/RagConnect.parser |   4 +-
 .../src/main/resources/ragconnect.mustache    |  39 ++++--
 .../main/resources/sendDefinition.mustache    |  12 +-
 ragconnect.tests/build.gradle                 |   2 +-
 .../src/test/01-input/attribute/Test.connect  |  10 ++
 .../src/test/01-input/attribute/Test.jadd     |   8 ++
 .../src/test/01-input/attribute/Test.relast   |   8 +-
 .../test/01-input/indexedSend/Test.connect    |   5 +-
 .../src/test/01-input/indexedSend/Test.jadd   |  18 +++
 .../src/test/01-input/indexedSend/Test.relast |   4 +-
 .../ragconnect/tests/AbstractMqttTest.java    |   6 +-
 .../ragconnect/tests/AttributeTest.java       | 111 +++++++++++----
 .../ragconnect/tests/IndexedSendTest.java     | 127 ++++++++++++------
 .../jastadd/ragconnect/tests/TestUtils.java   |  17 ++-
 17 files changed, 310 insertions(+), 100 deletions(-)

diff --git a/ragconnect.base/build.gradle b/ragconnect.base/build.gradle
index 7dfe1e7..7af3fc6 100644
--- a/ragconnect.base/build.gradle
+++ b/ragconnect.base/build.gradle
@@ -34,7 +34,7 @@ dependencies {
     relast group: 'org.jastadd', name: 'relast', version: "0.3.0-137"
     implementation group: 'org.jastadd', name: 'relast-preprocessor', version: "${preprocessor_version}"
     implementation group: 'com.github.spullara.mustache.java', name: 'compiler', version: "${mustache_java_version}"
-    runtimeOnly group: 'org.jastadd', name: 'jastadd2', version: '2.3.5-dresden'
+    jastadd2 group: 'org.jastadd', name: 'jastadd2', version: '2.3.5-dresden'
     api group: 'net.sf.beaver', name: 'beaver-rt', version: '0.9.11'
 }
 
diff --git a/ragconnect.base/src/main/jastadd/Intermediate.jadd b/ragconnect.base/src/main/jastadd/Intermediate.jadd
index 95f9b86..dfd74a9 100644
--- a/ragconnect.base/src/main/jastadd/Intermediate.jadd
+++ b/ragconnect.base/src/main/jastadd/Intermediate.jadd
@@ -506,13 +506,36 @@ aspect MustacheSendDefinition {
   syn String EndpointDefinition.forwardingNTA_Name() = getEndpointTarget().forwardingNTA_Name();
   syn String EndpointDefinition.forwardingNTA_Type() = getEndpointTarget().forwardingNTA_Type();
 
+  syn boolean EndpointDefinition.targetIsAttribute() = getEndpointTarget().isAttributeEndpointTarget();
+
+  syn boolean EndpointDefinition.indexBasedAccessAndTargetIsNTA() {
+    return typeIsList() && getIndexBasedListAccess() && !needForwardingNTA();
+  }
+
   syn boolean EndpointDefinition.relationEndpointWithListRole() = getEndpointTarget().relationEndpointWithListRole();
 
   syn String EndpointDefinition.senderName() = getEndpointTarget().senderName();
 
+  syn java.util.List<SendIncrementalObserverEntry> EndpointDefinition.sendIncrementalObserverEntries() {
+    // todo maybe getterMethodName needs to be change for indexed send
+    java.util.List<SendIncrementalObserverEntry> result = new java.util.ArrayList<>();
+    // "{{getterMethodName}}{{#IndexBasedListAccess}}_int{{/IndexBasedListAccess}}"
+    result.add(SendIncrementalObserverEntry.of(getterMethodName() + (getIndexBasedListAccess() ? "_int" : ""),
+            getIndexBasedListAccess(), "index"));
+    if (indexBasedAccessAndTargetIsNTA()) {
+      // "{{getterMethodName}}List"
+      result.add(SendIncrementalObserverEntry.of(getterMethodName() + "List", false, "index"));
+    }
+    if (targetIsAttribute()) {
+      // "{{parentTypeName}}_{{getterMethodName}}{{#IndexBasedListAccess}}_int{{/IndexBasedListAccess}}"
+      result.add(SendIncrementalObserverEntry.of(parentTypeName() + "_" + getterMethodName() + (getIndexBasedListAccess() ? "_int" : ""), getIndexBasedListAccess(), "index"));
+    }
+    return result;
+  }
+
   syn boolean EndpointDefinition.shouldNotResetValue() = getSend() && !getEndpointTarget().hasAttributeResetMethod();
 
-  syn String EndpointDefinition.tokenResetMethodName() = getterMethodName() + "_reset";
+  syn String EndpointDefinition.tokenResetMethodName() = getEndpointTarget().tokenResetMethodName();
 
   syn String EndpointDefinition.updateMethodName() = toMustache().updateMethodName();
 
@@ -552,6 +575,9 @@ containingEndpointDefinition().getIndexBasedListAccess());
   syn String EndpointTarget.senderName() = ragconnect().internalRagConnectPrefix() + "_sender_" + entityName();
   eq ContextFreeTypeEndpointTarget.senderName() = null;
 
+  syn String EndpointTarget.tokenResetMethodName() = getterMethodName() + (
+          typeIsList() && containingEndpointDefinition().getIndexBasedListAccess() ? "List" : "") + "_reset";
+
   syn String MEndpointDefinition.updateMethodName();
   syn String MEndpointDefinition.writeMethodName();
 
@@ -584,6 +610,13 @@ containingEndpointDefinition().getIndexBasedListAccess());
 
   syn String EndpointDefinition.typeName() = type().getName();
   syn String MEndpointDefinition.typeName() = getEndpointDefinition().typeName();
+
+  static SendIncrementalObserverEntry SendIncrementalObserverEntry.of(String attributeString, boolean useParams, Object params) {
+    return new SendIncrementalObserverEntry()
+            .setParams(useParams ? params : null)
+            .setCompareParams(useParams)
+            .setAttributeString(attributeString);
+  }
 }
 
 aspect MustacheTokenComponent {
diff --git a/ragconnect.base/src/main/jastadd/Intermediate.relast b/ragconnect.base/src/main/jastadd/Intermediate.relast
index 8fa32f9..0307d8d 100644
--- a/ragconnect.base/src/main/jastadd/Intermediate.relast
+++ b/ragconnect.base/src/main/jastadd/Intermediate.relast
@@ -15,3 +15,5 @@ MContextFreeTypeSendDefinition : MContextFreeTypeEndpointDefinition;
 
 MInnerMappingDefinition;
 rel MInnerMappingDefinition.MappingDefinition -> MappingDefinition;
+
+SendIncrementalObserverEntry ::= <Params:Object> <CompareParams:boolean> <AttributeString>;
diff --git a/ragconnect.base/src/main/jastadd/parser/RagConnect.parser b/ragconnect.base/src/main/jastadd/parser/RagConnect.parser
index f822bf0..55eac24 100644
--- a/ragconnect.base/src/main/jastadd/parser/RagConnect.parser
+++ b/ragconnect.base/src/main/jastadd/parser/RagConnect.parser
@@ -63,8 +63,8 @@ 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 BRACKET_LEFT ID.attribute_type_name BRACKET_RIGHT
-     {: return new UntypedEndpointTarget(type_name, child_name + ":" + attribute_type_name, true); :}
+  | ID.type_name DOT ID.child_name BRACKET_LEFT java_type_use.attribute_type_name BRACKET_RIGHT
+     {: return new UntypedEndpointTarget(type_name, child_name + ":" + attribute_type_name.prettyPrint(), true); :}
   | ID.type_name                      {: return new UntypedEndpointTarget(type_name, "", false); :}
 ;
 
diff --git a/ragconnect.base/src/main/resources/ragconnect.mustache b/ragconnect.base/src/main/resources/ragconnect.mustache
index 7e054a4..f2195dd 100644
--- a/ragconnect.base/src/main/resources/ragconnect.mustache
+++ b/ragconnect.base/src/main/resources/ragconnect.mustache
@@ -110,6 +110,7 @@ aspect RagConnectObserver {
       final boolean compareParams;
       final Object params;
       final Runnable attributeCall;
+      //final RagConnectToken connectToken;
       final java.util.List<RagConnectToken> connectList = new java.util.ArrayList<>();
 
       RagConnectObserverEntry(ASTNode node, String attributeString,
@@ -126,11 +127,11 @@ aspect RagConnectObserver {
       }
 
       boolean baseMembersEqualTo(ASTNode otherNode, String otherAttributeString,
-          boolean otherCompareParams, Object otherParams) {
+          boolean forceCompareParams, Object otherParams) {
         return this.node.equals(otherNode) &&
             this.attributeString.equals(otherAttributeString) &&
-            this.compareParams == otherCompareParams &&
-            (!this.compareParams || java.util.Objects.equals(this.params, otherParams));
+            //this.compareParams == otherCompareParams &&
+            (!(this.compareParams || forceCompareParams) || java.util.Objects.equals(this.params, otherParams));
       }
     }
 
@@ -162,11 +163,21 @@ aspect RagConnectObserver {
       node.trace().setReceiver(this);
     }
 
-    void add(RagConnectToken connectToken, ASTNode node, String attributeString, Runnable attributeCall) {
-      internal_add(connectToken, node, attributeString, false, null, attributeCall);
+    void add(RagConnectToken connectToken, ASTNode node, Runnable attributeCall, String... attributeStrings) {
+      internal_add(connectToken, node, attributeStrings, false, null, attributeCall);
     }
-    void add(RagConnectToken connectToken, ASTNode node, String attributeString, Object params, Runnable attributeCall) {
-      internal_add(connectToken, node, attributeString, true, params, attributeCall);
+    void add(RagConnectToken connectToken, ASTNode node, Object params, Runnable attributeCall, String... attributeStrings) {
+      internal_add(connectToken, node, attributeStrings, true, params, attributeCall);
+    }
+
+    private void internal_add(RagConnectToken connectToken, ASTNode node, String[] attributeStrings,
+        boolean compareParams, Object params, Runnable attributeCall) {
+      if (attributeStrings.length == 0) {
+        {{logWarn}}("No attribute string given to observe for {{log_}}!", connectToken.uri);
+      }
+      for (String attributeString : attributeStrings) {
+        internal_add(connectToken, node, attributeString, compareParams, params, attributeCall);
+      }
     }
 
     private void internal_add(RagConnectToken connectToken, ASTNode node, String attributeString,
@@ -176,10 +187,12 @@ aspect RagConnectObserver {
         node, attributeString, (compareParams ? " (parameterized)" : ""));
       {{/configLoggingEnabledForIncremental}}
       // either add to an existing entry (with same node, attribute) or create new entry
+      //TODO check case, where runnable differs (and baseMembersEqual returns true). need list of runnables, and map<connectToken, runnable> to remove them!
       boolean needNewEntry = true;
       for (RagConnectObserverEntry entry : observedNodes) {
-        if (entry.baseMembersEqualTo(node, attributeString, compareParams, params)) {
+        if (entry.baseMembersEqualTo(node, attributeString, true, params)) {
           entry.connectList.add(connectToken);
+    {{logError}}("baseMembersEqualTo node: {{log_}} atrStr: {{log_}} compare: {{log_}} params: {{log_}}", node, attributeString, compareParams, params);
           needNewEntry = false;
           break;
         }
@@ -193,16 +206,14 @@ aspect RagConnectObserver {
     }
 
     void remove(RagConnectToken connectToken) {
-      RagConnectObserverEntry entryToDelete = null;
+      java.util.List<RagConnectObserverEntry> entriesToDelete = new java.util.ArrayList<>();
       for (RagConnectObserverEntry entry : observedNodes) {
         entry.connectList.remove(connectToken);
         if (entry.connectList.isEmpty()) {
-          entryToDelete = entry;
+          entriesToDelete.add(entry);
         }
       }
-      if (entryToDelete != null) {
-        observedNodes.remove(entryToDelete);
-      }
+      observedNodes.removeAll(entriesToDelete);
     }
 
     @Override
@@ -256,7 +267,7 @@ aspect RagConnectObserver {
       {{/configLoggingEnabledForIncremental}}
       // iterate through list, if matching pair. could maybe be more efficient.
       for (RagConnectObserverEntry entry : observedNodes) {
-        if (entry.node.equals(node) && entry.attributeString.equals(attribute) && (!entry.compareParams || java.util.Objects.equals(entry.params, params))) {
+        if (entry.baseMembersEqualTo(node, attribute, false, params)) {
           // hit. call the attribute/nta-token
           {{#configLoggingEnabledForIncremental}}
           {{logDebug}}("** observer hit: {{log_}} on {{log_}}", entry.node, entry.attributeString);
diff --git a/ragconnect.base/src/main/resources/sendDefinition.mustache b/ragconnect.base/src/main/resources/sendDefinition.mustache
index abcbdff..6bfeee4 100644
--- a/ragconnect.base/src/main/resources/sendDefinition.mustache
+++ b/ragconnect.base/src/main/resources/sendDefinition.mustache
@@ -28,6 +28,7 @@ public boolean {{parentTypeName}}.{{connectMethodName}}(String {{connectParamete
       final String topic = {{attributeName}}().extractTopic(uri);
       {{senderName}}.add(() -> {
         {{#configLoggingEnabledForWrites}}
+        {{!FIXME circular computation for collection attributes!!}}
         {{logDebug}}("[Send] {{entityName}} = {{log_}} -> {{log_}}", {{getterMethodCall}}, {{connectParameterName}});
         {{/configLoggingEnabledForWrites}}
         if ({{lastValueGetterCall}} != null) {
@@ -63,18 +64,19 @@ public boolean {{parentTypeName}}.{{connectMethodName}}(String {{connectParamete
   if (success) {
     connectTokenMap.add(this, false, connectToken);
     {{#configIncrementalOptionActive}}
-        {{!todo maybe getterMethodName needs to be change for indexed send}}
-      {{observerInstanceSingletonMethodName}}().add(
+    {{#sendIncrementalObserverEntries}}
+    {{observerInstanceSingletonMethodName}}().add(
       connectToken,
       this,
-      "{{getterMethodName}}{{#IndexBasedListAccess}}_int{{/IndexBasedListAccess}}",
-      {{#IndexBasedListAccess}}index,{{/IndexBasedListAccess}}
+      {{#CompareParams}}{{Params}},{{/CompareParams}}
       () -> {
         if (this.{{updateMethodName}}({{#IndexBasedListAccess}}index{{/IndexBasedListAccess}})) {
           this.{{writeMethodName}}({{#IndexBasedListAccess}}index{{/IndexBasedListAccess}});
         }
-      }
+      },
+      "{{AttributeString}}"
     );
+    {{/sendIncrementalObserverEntries}}
     {{/configIncrementalOptionActive}}
   }
   return success;
diff --git a/ragconnect.tests/build.gradle b/ragconnect.tests/build.gradle
index ad9cff1..5068a66 100644
--- a/ragconnect.tests/build.gradle
+++ b/ragconnect.tests/build.gradle
@@ -46,7 +46,7 @@ dependencies {
     ragconnect project(':ragconnect.base')
     testImplementation project(':ragconnect.base')
 
-    implementation group: 'org.jastadd', name: 'jastadd2', version: '2.3.5-dresden-5'
+    implementation group: 'org.jastadd', name: 'jastadd2', version: '2.3.5-dresden-7'
     relast group: 'org.jastadd', name: 'relast', version: "0.3.0-137"
 
     testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: '5.4.0'
diff --git a/ragconnect.tests/src/test/01-input/attribute/Test.connect b/ragconnect.tests/src/test/01-input/attribute/Test.connect
index 03688a9..a463af6 100644
--- a/ragconnect.tests/src/test/01-input/attribute/Test.connect
+++ b/ragconnect.tests/src/test/01-input/attribute/Test.connect
@@ -3,6 +3,8 @@ send SenderRoot.simple(String) ;
 send SenderRoot.transformed(int) ;
 send SenderRoot.toReferenceType(A) ;
 send SenderRoot.toNTA(A) ;
+send SenderRoot.circularAttribute(int);
+send SenderRoot.collectionAttribute(Set<String>) using SetToString;
 
 AddSuffix maps A a to A {:
   A result = new A();
@@ -20,6 +22,10 @@ AddPlusOne maps int i to int {:
   return i + 1;
 :}
 
+SetToString maps Set<String> set to String {:
+  return set.toString();
+:}
+
 receive ReceiverRoot.FromBasic;
 receive ReceiverRoot.FromSimpleNoMapping;
 receive ReceiverRoot.FromSimpleWithMapping using AddStringSuffix;
@@ -29,3 +35,7 @@ receive ReceiverRoot.FromReferenceTypeNoMapping;
 receive ReceiverRoot.FromReferenceTypeWithMapping using AddSuffix;
 receive ReceiverRoot.FromNTANoMapping;
 receive ReceiverRoot.FromNTAWithMapping using AddSuffix;
+receive ReceiverRoot.FromCircularNoMapping;
+receive ReceiverRoot.FromCircularWithMapping using AddPlusOne;
+receive ReceiverRoot.FromCollectionNoMapping;
+receive ReceiverRoot.FromCollectionWithMapping using AddStringSuffix;
diff --git a/ragconnect.tests/src/test/01-input/attribute/Test.jadd b/ragconnect.tests/src/test/01-input/attribute/Test.jadd
index 34b1b45..4ad7668 100644
--- a/ragconnect.tests/src/test/01-input/attribute/Test.jadd
+++ b/ragconnect.tests/src/test/01-input/attribute/Test.jadd
@@ -1,3 +1,5 @@
+import java.util.Set;
+import java.util.HashSet;
 aspect Computation {
   syn String SenderRoot.basic() = getInput();
   syn String SenderRoot.simple() = getInput() + "Post";
@@ -18,6 +20,12 @@ aspect Computation {
     result.setInner(inner);
     return result;
   }
+  syn int SenderRoot.circularAttribute() circular [0] {
+    return Integer.parseInt(getInput()) + 2;
+  }
+  coll Set<String> SenderRoot.collectionAttribute() [new HashSet<>()] root SenderRoot ;
+  A contributes getValue() to SenderRoot.collectionAttribute();
+  SenderRoot contributes nta toNTA() to SenderRoot.collectionAttribute();
 }
 aspect MakeCodeCompile {
 
diff --git a/ragconnect.tests/src/test/01-input/attribute/Test.relast b/ragconnect.tests/src/test/01-input/attribute/Test.relast
index 7afc9b7..e3a3a67 100644
--- a/ragconnect.tests/src/test/01-input/attribute/Test.relast
+++ b/ragconnect.tests/src/test/01-input/attribute/Test.relast
@@ -1,5 +1,5 @@
 Root ::= SenderRoot* ReceiverRoot;
-SenderRoot ::= <Input> ;
+SenderRoot ::= <Input> A* ;
 ReceiverRoot ::=
     <FromBasic>
     <FromSimpleNoMapping>
@@ -9,6 +9,10 @@ ReceiverRoot ::=
     FromReferenceTypeNoMapping:A
     FromReferenceTypeWithMapping:A
     FromNTANoMapping:A
-    FromNTAWithMapping:A ;
+    FromNTAWithMapping:A
+    <FromCircularNoMapping:int>
+    <FromCircularWithMapping:int>
+    <FromCollectionNoMapping>
+    <FromCollectionWithMapping> ;
 A ::= <Value> Inner ;
 Inner ::= <InnerValue> ;
diff --git a/ragconnect.tests/src/test/01-input/indexedSend/Test.connect b/ragconnect.tests/src/test/01-input/indexedSend/Test.connect
index 3ff49e1..9e390cd 100644
--- a/ragconnect.tests/src/test/01-input/indexedSend/Test.connect
+++ b/ragconnect.tests/src/test/01-input/indexedSend/Test.connect
@@ -1,6 +1,7 @@
 send indexed SenderRoot.MultipleA;
-
 send indexed SenderRoot.MultipleAWithSuffix using AddSuffix;
+send indexed SenderRoot.A;
+send indexed SenderRoot.ComputedA using AddSuffix;
 
 AddSuffix maps A a to A {:
   A result = new A();
@@ -12,3 +13,5 @@ AddSuffix maps A a to A {:
 
 receive indexed ReceiverRoot.ManyA;
 receive indexed ReceiverRoot.ManyAWithSuffix;
+receive indexed ReceiverRoot.FromNTA;
+receive indexed ReceiverRoot.FromNTAWithSuffix;
diff --git a/ragconnect.tests/src/test/01-input/indexedSend/Test.jadd b/ragconnect.tests/src/test/01-input/indexedSend/Test.jadd
index d37dd04..fc8893e 100644
--- a/ragconnect.tests/src/test/01-input/indexedSend/Test.jadd
+++ b/ragconnect.tests/src/test/01-input/indexedSend/Test.jadd
@@ -1,3 +1,21 @@
+aspect Computation {
+  syn JastAddList<A> SenderRoot.getAList() {
+    JastAddList<A> result = new JastAddList<>();
+    getMultipleAList().forEach(a -> result.add(a.touchedTerminals()));
+    return result;
+  }
+  syn JastAddList<A> SenderRoot.getComputedAList() {
+    JastAddList<A> result = new JastAddList<>();
+    getMultipleAWithSuffixList().forEach(a -> result.add(a.touchedTerminals()));
+    return result;
+  }
+  A A.touchedTerminals() {
+    getValue();
+    getInner().getInnerValue();
+    return this;
+  }
+}
+
 aspect NameResolution {
   // overriding customID guarantees to produce the same JSON representation for equal lists
   // otherwise, the value for id is different each time
diff --git a/ragconnect.tests/src/test/01-input/indexedSend/Test.relast b/ragconnect.tests/src/test/01-input/indexedSend/Test.relast
index 9480f18..2e9924b 100644
--- a/ragconnect.tests/src/test/01-input/indexedSend/Test.relast
+++ b/ragconnect.tests/src/test/01-input/indexedSend/Test.relast
@@ -1,5 +1,5 @@
 Root ::= SenderRoot ReceiverRoot;
-SenderRoot ::= MultipleA:A* MultipleAWithSuffix:A* ;
-ReceiverRoot ::= ManyA:A* ManyAWithSuffix:A* ;
+SenderRoot ::= MultipleA:A* MultipleAWithSuffix:A* /A*/ /ComputedA:A*/ ;
+ReceiverRoot ::= ManyA:A* ManyAWithSuffix:A* FromNTA:A* FromNTAWithSuffix:A* ;
 A ::= <Value> Inner ;
 Inner ::= <InnerValue> ;
diff --git a/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/AbstractMqttTest.java b/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/AbstractMqttTest.java
index 31335e5..b8f31b8 100644
--- a/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/AbstractMqttTest.java
+++ b/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/AbstractMqttTest.java
@@ -25,7 +25,7 @@ public abstract class AbstractMqttTest extends RagConnectTest {
   /**
    * if the initial/current value shall be sent upon connecting
    */
-  private boolean writeCurrentValue;
+  protected boolean writeCurrentValue;
 
   public boolean isWriteCurrentValue() {
     return writeCurrentValue;
@@ -58,7 +58,7 @@ public abstract class AbstractMqttTest extends RagConnectTest {
 
   @Tag("mqtt")
   @RepeatedIfExceptionsTest(repeats = TEST_REPETITIONS)
-  public final void testCommunicateSendInitialValue() throws IOException, InterruptedException {
+  public void testCommunicateSendInitialValue() throws IOException, InterruptedException {
     this.writeCurrentValue = true;
 
     createModel();
@@ -76,7 +76,7 @@ public abstract class AbstractMqttTest extends RagConnectTest {
 
   @Tag("mqtt")
   @RepeatedIfExceptionsTest(repeats = TEST_REPETITIONS)
-  public final void testCommunicateOnlyUpdatedValue() throws IOException, InterruptedException {
+  public void testCommunicateOnlyUpdatedValue() throws IOException, InterruptedException {
     this.writeCurrentValue = false;
 
     createModel();
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 1c10bd4..a0336d4 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
@@ -9,8 +9,10 @@ import java.util.Objects;
 import java.util.concurrent.Callable;
 import java.util.concurrent.TimeUnit;
 import java.util.function.Predicate;
+import java.util.function.Supplier;
 
 import static java.util.function.Predicate.isEqual;
+import static org.assertj.core.api.Assertions.assertThat;
 import static org.jastadd.ragconnect.tests.TestUtils.*;
 import static org.junit.jupiter.api.Assertions.*;
 
@@ -20,6 +22,7 @@ import static org.junit.jupiter.api.Assertions.*;
  * @author rschoene - Initial contribution
  */
 @Tag("Incremental")
+@Tag("New")
 public class AttributeTest extends AbstractMqttTest {
 
   private static final String TOPIC_WILDCARD = "attr/#";
@@ -32,15 +35,22 @@ public class AttributeTest extends AbstractMqttTest {
   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 TOPIC_CIRCULAR_NO_MAPPING = "attr/a/circular/plain";
+  private static final String TOPIC_CIRCULAR_WITH_MAPPING = "attr/a/circular/mapped";
+  private static final String TOPIC_COLLECTION_NO_MAPPING = "attr/a/collection/plain";
+  private static final String TOPIC_COLLECTION_WITH_MAPPING = "attr/a/collection/mapped";
 
   private static final String INITIAL_STRING = "initial";
   private static final String INITIAL_STRING_FOR_INT = "1";
+  private static final String INITIAL_STRING_FOR_INT_PLUS_2 = Integer.toString(Integer.parseInt(INITIAL_STRING_FOR_INT) + 2);
 
   private static final String CHECK_BASIC = "basic";
   private static final String CHECK_SIMPLE = "simple";
   private static final String CHECK_TRANSFORMED = "transformed";
   private static final String CHECK_A = "a";
   private static final String CHECK_NTA = "nta";
+  private static final String CHECK_CIRCULAR = "circular";
+  private static final String CHECK_COLLECTION = "collection";
 
   private MqttHandler handler;
   private ReceiverData data;
@@ -82,6 +92,8 @@ public class AttributeTest extends AbstractMqttTest {
             .setCheckForString(CHECK_TRANSFORMED, this::checkTransformed)
             .setCheckForString(CHECK_A, this::checkA)
             .setCheckForString(CHECK_NTA, this::checkNta)
+            .setCheckForString(CHECK_CIRCULAR, this::checkCircular)
+            .setCheckForString(CHECK_COLLECTION, this::checkCollection)
     ;
 
     // connect receive
@@ -94,14 +106,22 @@ public class AttributeTest extends AbstractMqttTest {
     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)));
+    assertTrue(receiverRoot.connectFromCircularNoMapping(mqttUri(TOPIC_CIRCULAR_NO_MAPPING)));
+    assertTrue(receiverRoot.connectFromCircularWithMapping(mqttUri(TOPIC_CIRCULAR_WITH_MAPPING)));
+    assertTrue(receiverRoot.connectFromCollectionNoMapping(mqttUri(TOPIC_COLLECTION_NO_MAPPING)));
+    assertTrue(receiverRoot.connectFromCollectionWithMapping(mqttUri(TOPIC_COLLECTION_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(senderString.connectCollectionAttribute(mqttUri(TOPIC_COLLECTION_NO_MAPPING), isWriteCurrentValue()));
+    assertTrue(senderString.connectCollectionAttribute(mqttUri(TOPIC_COLLECTION_WITH_MAPPING), isWriteCurrentValue()));
 
     assertTrue(senderInt.connectTransformed(mqttUri(TOPIC_TRANSFORMED_NO_MAPPING), isWriteCurrentValue()));
     assertTrue(senderInt.connectTransformed(mqttUri(TOPIC_TRANSFORMED_WITH_MAPPING), isWriteCurrentValue()));
+    assertTrue(senderInt.connectCircularAttribute(mqttUri(TOPIC_CIRCULAR_NO_MAPPING), isWriteCurrentValue()));
+    assertTrue(senderInt.connectCircularAttribute(mqttUri(TOPIC_CIRCULAR_WITH_MAPPING), isWriteCurrentValue()));
 
     assertTrue(senderA.connectToReferenceType(mqttUri(TOPIC_REFERENCE_TYPE_NO_MAPPING), isWriteCurrentValue()));
     assertTrue(senderA.connectToReferenceType(mqttUri(TOPIC_REFERENCE_TYPE_WITH_MAPPING), isWriteCurrentValue()));
@@ -111,58 +131,69 @@ public class AttributeTest extends AbstractMqttTest {
     waitForValue(senderString.basic(), receiverRoot::getFromBasic);
     waitForValue(senderString.simple(), receiverRoot::getFromSimpleNoMapping);
     waitForValue(senderInt.transformed(), receiverRoot::getFromTransformedNoMapping);
+    waitForNonNull(receiverRoot::getFromCollectionNoMapping);
     waitForNonNull(receiverRoot::getFromReferenceTypeNoMapping);
     waitForNonNull(receiverRoot::getFromNTANoMapping);
   }
 
   @Override
   protected void communicateSendInitialValue() throws IOException, InterruptedException {
-    // basic, simple(2)    <-- senderString
-    // transformed(2)      <-- senderInt
-    // ref-type(2), nta(2) <-- senderA
-    checker.addToNumberOfValues(9)
+    // 13 = basic, simple(2), collection(2), transformed(2), circular(2), ref-type(2), nta(2)
+    checker.addToNumberOfValues(13)
             .put(CHECK_BASIC, INITIAL_STRING)
             .put(CHECK_SIMPLE, INITIAL_STRING + "Post")
             .put(CHECK_TRANSFORMED, INITIAL_STRING_FOR_INT)
             .put(CHECK_A, INITIAL_STRING)
-            .put(CHECK_NTA, INITIAL_STRING);
+            .put(CHECK_NTA, INITIAL_STRING)
+            .put(CHECK_CIRCULAR, INITIAL_STRING_FOR_INT_PLUS_2)
+            .put(CHECK_COLLECTION, "[" + INITIAL_STRING + "]");
 
     communicateBoth();
   }
 
   @Override
   protected void communicateOnlyUpdatedValue() throws IOException, InterruptedException {
-    // basic, simple(2)    <-- senderString
-    // transformed(2)      <-- senderInt
-    // ref-type(2), nta(2) <-- senderA
     checker.put(CHECK_BASIC, (String) null)
             .put(CHECK_SIMPLE, (String) null)
             .put(CHECK_TRANSFORMED, (String) null)
             .put(CHECK_A, (String) null)
-            .put(CHECK_NTA, (String) null);
+            .put(CHECK_NTA, (String) null)
+            .put(CHECK_COLLECTION, (String) null);
 
     communicateBoth();
   }
 
   private void communicateBoth() throws IOException {
+    // basic, simple(2), collection(2) <-- senderString
+    // transformed(2), circular(2)     <-- senderInt
+    // ref-type(2), nta(2)             <-- senderA
     checker.check();
 
     senderString.setInput("test-01");
-    checker.addToNumberOfValues(3)
+    checker.addToNumberOfValues(5) // basic, simple(2), collection(2)
             .put(CHECK_BASIC, "test-01")
             .put(CHECK_SIMPLE, "test-01Post")
+            .put(CHECK_COLLECTION, "[test-01]")
             .check();
 
+    // no change for same value
     senderString.setInput("test-01");
     checker.check();
 
+    // TODO check checks for (circular and) collection
+    senderString.addA(new A().setValue("test-02").setInner(new Inner().setInnerValue("inner")));
+    checker.addToNumberOfValues(2) // collection(2)
+            .put(CHECK_COLLECTION, "[test-01, test-02]")
+            .check();
+
     senderInt.setInput("20");
-    checker.addToNumberOfValues(2)
+    checker.addToNumberOfValues(4) // transformed(2), circular(2)
             .put(CHECK_TRANSFORMED, "20")
+            .put(CHECK_CIRCULAR, "22")
             .check();
 
     senderA.setInput("test-03");
-    checker.addToNumberOfValues(4)
+    checker.addToNumberOfValues(4) // ref-type(2), nta(2)
             .put(CHECK_A, "test-03")
             .put(CHECK_NTA, "test-03")
             .check();
@@ -170,14 +201,22 @@ public class AttributeTest extends AbstractMqttTest {
     assertTrue(senderString.disconnectSimple(mqttUri(TOPIC_SIMPLE_NO_MAPPING)));
     assertTrue(senderString.disconnectSimple(mqttUri(TOPIC_SIMPLE_WITH_MAPPING)));
     senderString.setInput("test-04");
-    checker.incNumberOfValues()
+    checker.addToNumberOfValues(3) // basic, collection(2)
             .put(CHECK_BASIC, "test-04")
+            .put(CHECK_COLLECTION, "[test-02, test-04]")
+            .check();
+
+    assertTrue(senderString.disconnectCollectionAttribute(mqttUri(TOPIC_COLLECTION_NO_MAPPING)));
+    assertTrue(senderString.disconnectCollectionAttribute(mqttUri(TOPIC_COLLECTION_WITH_MAPPING)));
+    senderString.setInput("test-05");
+    checker.incNumberOfValues() // basic
+            .put(CHECK_BASIC, "test-05")
             .check();
 
     assertTrue(senderA.disconnectToNTA(mqttUri(TOPIC_NTA_NO_MAPPING)));
-    senderA.setInput("test-05");
+    senderA.setInput("test-06");
     checker.addToNumberOfValues(3)
-            .put(CHECK_A, "test-05")
+            .put(CHECK_A, "test-06")
             .check();
   }
 
@@ -208,18 +247,17 @@ public class AttributeTest extends AbstractMqttTest {
   }
 
   private void checkTransformed(String name, String expected) {
+    _checkInt(name, expected, receiverRoot::getFromTransformedNoMapping, receiverRoot::getFromTransformedWithMapping);
+  }
+
+  private void _checkInt(String name, String expected, Supplier<Integer> noMapping, Supplier<Integer> withMapping) {
     if (expected != null) {
-      assertEquals(Integer.parseInt(expected),
-              receiverRoot.getFromTransformedNoMapping(), "transformed");
-      assertEquals(Integer.parseInt(expected) + 1,
-              receiverRoot.getFromTransformedWithMapping(), "transformed mapped");
+      assertEquals(Integer.parseInt(expected), noMapping.get(), name);
+      assertEquals(Integer.parseInt(expected) + 1, withMapping.get(), name + " mapped");
     } else {
-      assertEquals(0,
-              receiverRoot.getFromTransformedNoMapping(), "transformed null");
-      assertEquals(0,
-              receiverRoot.getFromTransformedWithMapping(), "transformed mapped null");
+      assertEquals(0, noMapping.get(), name + " null");
+      assertEquals(0, withMapping.get(), name + " mapped null");
     }
-
   }
 
   private void checkA(String name, String expected) {
@@ -245,6 +283,31 @@ public class AttributeTest extends AbstractMqttTest {
     }
   }
 
+  private void checkCircular(String name, String expected) {
+    _checkInt(name, expected, receiverRoot::getFromCircularNoMapping, receiverRoot::getFromCircularWithMapping);
+  }
+
+  private void checkCollection(String name, String expected) {
+    if (expected != null) {
+      // TODO probably need to tokenize actual and expected, and compare their elements without order
+      assertThat(receiverRoot.getFromCollectionWithMapping()).hasSizeGreaterThan(4).endsWith("post");
+      checkCollectionContent(name, expected, receiverRoot.getFromCollectionNoMapping());
+      checkCollectionContent(name + " mapped", expected, receiverRoot.getFromCollectionWithMapping().substring(0, receiverRoot.getFromCollectionWithMapping().length() - 4));
+    } else {
+      assertEquals("", receiverRoot.getFromCollectionNoMapping(), "collection null");
+      assertEquals("", receiverRoot.getFromCollectionWithMapping(), "collection mapped null");
+    }
+  }
+
+  private void checkCollectionContent(String name, String expected, String actual) {
+    assertThat(actual).as(name)
+            .startsWith("[")
+            .endsWith("]");
+    String[] actualValues = actual.substring(1, actual.length() - 1).split(", ");
+    String[] expectedValues = expected.substring(1, expected.length() - 1).split(", ");
+    assertThat(actualValues).containsExactlyInAnyOrder(expectedValues);
+  }
+
   private void assertA(String expectedValue, String expectedInner, A actual, String message) {
     assertEquals(expectedValue, actual.getValue(), message + " value");
     assertEquals(expectedInner, actual.getInner().getInnerValue(), message + " inner");
diff --git a/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/IndexedSendTest.java b/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/IndexedSendTest.java
index a15d999..d84f713 100644
--- a/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/IndexedSendTest.java
+++ b/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/IndexedSendTest.java
@@ -2,12 +2,17 @@ package org.jastadd.ragconnect.tests;
 
 import indexedSendInc.ast.*;
 import org.assertj.core.api.Assertions;
+import org.assertj.core.groups.Tuple;
+import org.junit.jupiter.api.Assumptions;
 import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
 
 import java.io.IOException;
+import java.util.StringJoiner;
 import java.util.concurrent.Callable;
 import java.util.concurrent.TimeUnit;
 import java.util.function.Predicate;
+import java.util.function.Supplier;
 
 import static org.assertj.core.groups.Tuple.tuple;
 import static org.jastadd.ragconnect.tests.TestUtils.*;
@@ -20,6 +25,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
  * @author rschoene - Initial contribution
  */
 @Tag("Incremental")
+@Tag("New")
 public class IndexedSendTest extends AbstractMqttTest {
 
   private static final String TOPIC_A_MANY_NORMAL_WILDCARD = "a-many/#";
@@ -33,6 +39,8 @@ public class IndexedSendTest extends AbstractMqttTest {
   private static final String CHECK_MANY_A = "many-a";
   private static final String CHECK_WITH_SUFFIX = "many-a-with-suffix";
 
+  private boolean connectNTAsInstead;
+
   private MqttHandler handler;
   private ReceiverData data;
   private TestUtils.TestChecker checker;
@@ -83,70 +91,91 @@ public class IndexedSendTest extends AbstractMqttTest {
     checker = new TestChecker();
     checker.setActualNumberOfValues(() -> data.numberOfValues)
             .setCheckForTuple(CHECK_MANY_A, (name, expected) ->
-                    Assertions.assertThat(receiverRoot.getManyAList()).extracting("Value")
-                            .as(name)
-                            .containsExactlyElementsOf(expected.toList()))
+                    checkList(name, expected, receiverRoot::getManyAList))
             .setCheckForTuple(CHECK_WITH_SUFFIX, (name, expected) ->
-                    Assertions.assertThat(receiverRoot.getManyAWithSuffixList()).extracting("Value")
-                            .as(name)
-                            .containsExactlyElementsOf(expected.toList()));
+                    checkList(name, expected, receiverRoot::getManyAWithSuffixList));
 
     // connect receive
     assertTrue(receiverRoot.connectManyA(mqttUri(TOPIC_A_MANY_NORMAL_WILDCARD)));
     assertTrue(receiverRoot.connectManyAWithSuffix(mqttUri(TOPIC_A_MANY_SUFFIX_WILDCARD)));
 
     // connect send, and wait to receive (if writeCurrentValue is set)
-    assertTrue(senderRoot.connectMultipleA(mqttUri(TOPIC_A_MANY_NORMAL_0), 0, isWriteCurrentValue()));
+    assertTrue(connectNTAsInstead ?
+            senderRoot.connectA(mqttUri(TOPIC_A_MANY_NORMAL_0), 0, isWriteCurrentValue()) :
+            senderRoot.connectMultipleA(mqttUri(TOPIC_A_MANY_NORMAL_0), 0, isWriteCurrentValue()));
     waitForValue(receiverRoot::getNumManyA, 1);
 
-    assertTrue(senderRoot.connectMultipleA(mqttUri(TOPIC_A_MANY_NORMAL_1), 1, isWriteCurrentValue()));
+    assertTrue(connectNTAsInstead ?
+            senderRoot.connectA(mqttUri(TOPIC_A_MANY_NORMAL_1), 1, isWriteCurrentValue()) :
+            senderRoot.connectMultipleA(mqttUri(TOPIC_A_MANY_NORMAL_1), 1, isWriteCurrentValue()));
     waitForValue(receiverRoot::getNumManyA, 2);
 
-    assertTrue(senderRoot.connectMultipleAWithSuffix(mqttUri(TOPIC_A_MANY_SUFFIX_0), 0, isWriteCurrentValue()));
+    assertTrue(connectNTAsInstead ?
+            senderRoot.connectComputedA(mqttUri(TOPIC_A_MANY_SUFFIX_0), 0, isWriteCurrentValue()) :
+            senderRoot.connectMultipleAWithSuffix(mqttUri(TOPIC_A_MANY_SUFFIX_0), 0, isWriteCurrentValue()));
     waitForValue(receiverRoot::getNumManyAWithSuffix, 1);
 
-    assertTrue(senderRoot.connectMultipleAWithSuffix(mqttUri(TOPIC_A_MANY_SUFFIX_1), 1, isWriteCurrentValue()));
+    assertTrue(connectNTAsInstead ?
+            senderRoot.connectComputedA(mqttUri(TOPIC_A_MANY_SUFFIX_1), 1, isWriteCurrentValue()) :
+            senderRoot.connectMultipleAWithSuffix(mqttUri(TOPIC_A_MANY_SUFFIX_1), 1, isWriteCurrentValue()));
     waitForValue(receiverRoot::getNumManyAWithSuffix, 2);
   }
 
+  private void checkList(String name, Tuple expected, Supplier<JastAddList<A>> actual) {
+    Assertions.assertThat(actual.get()).extracting("Value")
+            .as(name)
+            .containsExactlyElementsOf(expected.toList());
+  }
+
   private void waitForValue(Callable<Integer> callable, int expectedValue) {
     if (isWriteCurrentValue()) {
       awaitMqtt().until(callable, Predicate.isEqual(expectedValue));
     }
   }
 
+  @Tag("mqtt")
+//  @RepeatedIfExceptionsTest(repeats = TEST_REPETITIONS)
+  @Test
+  public void testCommunicateSendInitialValueWithNTAs() throws IOException, InterruptedException {
+    this.writeCurrentValue = true;
+    this.connectNTAsInstead = true;
+
+    try {
+      createModel();
+      setupReceiverAndConnect();
+
+      logger.info("Calling communicateSendInitialValue");
+      communicateSendInitialValue();
+    } finally {
+      this.connectNTAsInstead = false;
+    }
+  }
+
+  @Tag("mqtt")
+//  @RepeatedIfExceptionsTest(repeats = TEST_REPETITIONS)
+  @Test
+  public void testCommunicateOnlyUpdatedValueWithNTAs() throws IOException, InterruptedException {
+    this.writeCurrentValue = false;
+    this.connectNTAsInstead = true;
+
+    try {
+      createModel();
+      setupReceiverAndConnect();
+
+      logger.info("Calling communicateOnlyUpdatedValue");
+      communicateOnlyUpdatedValue();
+    } finally {
+      this.connectNTAsInstead = false;
+    }
+  }
+
   @Override
   protected void communicateSendInitialValue() throws IOException, InterruptedException {
     checker.addToNumberOfValues(4)
             .put(CHECK_MANY_A, tuple("am0", "am1"))
             .put(CHECK_WITH_SUFFIX, tuple("am0post", "am1post"));
 
-    listA0.setValue("changedValue");
-    checker.incNumberOfValues().put(CHECK_MANY_A, tuple("changedValue", "am1")).check();
-
-    // setting same value must not change data, and must not trigger a new sent message
-    listA0.setValue("changedValue");
-    checker.check();
-
-    listA1.setValue("");
-    checker.incNumberOfValues().put(CHECK_MANY_A, tuple("changedValue", "")).check();
-
-    listA1InSuffix.setValue("re");
-    checker.incNumberOfValues().put(CHECK_WITH_SUFFIX, tuple("am0post", "repost")).check();
-
-    // adding a new element does not automatically send it
-    A listA3InSuffix = createA("out");
-    senderRoot.addMultipleAWithSuffix(listA3InSuffix);
-    checker.check();
-
-    // only after connecting it, the element gets sent
-    assertTrue(senderRoot.connectMultipleAWithSuffix(mqttUri(TOPIC_A_MANY_SUFFIX_2), 2, true));
-    checker.incNumberOfValues().put(CHECK_WITH_SUFFIX, tuple("am0post", "repost", "outpost")).check();
-
-    // after successful disconnect, no messages will be sent
-    assertTrue(senderRoot.disconnectMultipleAWithSuffix(mqttUri(TOPIC_A_MANY_SUFFIX_0)));
-    listA0InSuffix.setValue("willBeIgnored");
-    checker.check();
+    communicateBoth("am1", "am0post");
   }
 
   @Override
@@ -154,10 +183,10 @@ public class IndexedSendTest extends AbstractMqttTest {
     checker.put(CHECK_MANY_A, tuple())
             .put(CHECK_WITH_SUFFIX, tuple());
 
-    communicateBoth();
+    communicateBoth(null, null);
   }
 
-  private void communicateBoth() throws IOException {
+  private void communicateBoth(String manyAtIndex1, String suffixAtIndex0) throws IOException {
     // Sink.ManyA           <-- Root.MultipleA
     // Sink.ManyAWithSuffix <-- Root.MultipleAWithSuffix
     checker.check();
@@ -165,18 +194,26 @@ public class IndexedSendTest extends AbstractMqttTest {
     assertEquals(listA0.getValue(), senderRoot._ragconnect_MultipleA(0).getValue());
     listA0.setValue("changedValue");
     assertEquals(listA0.getValue(), senderRoot._ragconnect_MultipleA(0).getValue());
-    checker.incNumberOfValues().put(CHECK_MANY_A, tuple("changedValue")).check();
+
+    checker.incNumberOfValues()
+            .put(CHECK_MANY_A, manyAtIndex1 != null ? tuple("changedValue", manyAtIndex1) : tuple("changedValue"))
+            .check();
+
+    if (!connectNTAsInstead) { return; } // TODO remove after testing NTAs is complete
 
     // setting same value must not change data, and must not trigger a new sent message
     listA0.setValue("changedValue");
     checker.check();
 
+    logger.error(prettyPrint(senderRoot.getAList()));
     listA1.setValue("");
     checker.incNumberOfValues().put(CHECK_MANY_A, tuple("changedValue", "")).check();
 
     // first element in suffix-list
     listA1InSuffix.setValue("re");
-    checker.incNumberOfValues().put(CHECK_WITH_SUFFIX, tuple("repost")).check();
+    checker.incNumberOfValues()
+            .put(CHECK_WITH_SUFFIX, suffixAtIndex0 != null ? tuple(suffixAtIndex0, "repost") : tuple("repost"))
+            .check();
 
     // adding a new element does not automatically send it
     A listA3InSuffix = createA("out");
@@ -185,7 +222,9 @@ public class IndexedSendTest extends AbstractMqttTest {
 
     // only after connecting it, the element gets sent
     assertTrue(senderRoot.connectMultipleAWithSuffix(mqttUri(TOPIC_A_MANY_SUFFIX_2), 2, true));
-    checker.incNumberOfValues().put(CHECK_WITH_SUFFIX, tuple("repost", "outpost")).check();
+    checker.incNumberOfValues()
+            .put(CHECK_WITH_SUFFIX, suffixAtIndex0 != null ? tuple(suffixAtIndex0, "repost", "outpost") : tuple("repost", "outpost"))
+            .check();
 
     // after successful disconnect, no messages will be sent
     assertTrue(senderRoot.disconnectMultipleAWithSuffix(mqttUri(TOPIC_A_MANY_SUFFIX_0)));
@@ -193,6 +232,12 @@ public class IndexedSendTest extends AbstractMqttTest {
     checker.check();
   }
 
+  private String prettyPrint(JastAddList<A> aList) {
+    StringJoiner sj = new StringJoiner(", ", "[", "]");
+    aList.forEach(a -> sj.add(a.getValue() + "(" + a.getInner().getInnerValue() + ")"));
+    return sj.toString();
+  }
+
   @Override
   protected void closeConnections() {
     if (handler != null) {
diff --git a/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/TestUtils.java b/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/TestUtils.java
index 5e6c94f..7b2403f 100644
--- a/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/TestUtils.java
+++ b/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/TestUtils.java
@@ -22,6 +22,7 @@ import java.util.*;
 import java.util.concurrent.Callable;
 import java.util.concurrent.TimeUnit;
 import java.util.function.BiConsumer;
+import java.util.function.Function;
 import java.util.function.Predicate;
 import java.util.function.Supplier;
 import java.util.stream.Collectors;
@@ -226,20 +227,30 @@ public class TestUtils {
       }
 
       public TestChecker setActual(String name, Callable<T> actual) {
-        values.computeIfAbsent(name, ActualAndExpected::new).actual = actual;
+        _computeIfAbsent(name).actual = actual;
         return parent;
       }
 
       public TestChecker setCheck(String name, BiConsumer<String, T> check) {
-        values.computeIfAbsent(name, ActualAndExpected::new).customCheck = check;
+        _computeIfAbsent(name).customCheck = check;
         return parent;
       }
 
       public TestChecker put(String name, T expected) {
-        values.computeIfAbsent(name, ActualAndExpected::new).expected = expected;
+        _computeIfAbsent(name).expected = expected;
         return parent;
       }
 
+      public TestChecker updateExpected(String name, Function<T, T> updater) {
+        ActualAndExpected<T> aae = _computeIfAbsent(name);
+        aae.expected = updater.apply(aae.expected);
+        return parent;
+      }
+
+      private ActualAndExpected<T> _computeIfAbsent(String name) {
+        return values.computeIfAbsent(name, ActualAndExpected::new);
+      }
+
       ActualAndExpected<T> get(String name) {
         return values.get(name);
       }
-- 
GitLab