diff --git a/ragconnect.base/src/main/resources/ragconnect.mustache b/ragconnect.base/src/main/resources/ragconnect.mustache index fbe3dd572676b60fdb826ec01b098d8a6aa7252a..088d2d3504d90f73e46fe976727d68d2bbae33e0 100644 --- a/ragconnect.base/src/main/resources/ragconnect.mustache +++ b/ragconnect.base/src/main/resources/ragconnect.mustache @@ -87,22 +87,33 @@ aspect RagConnectObserver { class RagConnectObserver implements ASTState.Trace.Receiver { class RagConnectObserverEntry { - final RagConnectToken connectToken; final ASTNode node; final String attributeString; final boolean compareParams; final Object params; final Runnable attributeCall; + final java.util.List<RagConnectToken> connectList = new java.util.ArrayList<>(); - RagConnectObserverEntry(RagConnectToken connectToken, ASTNode node, String attributeString, + RagConnectObserverEntry(ASTNode node, String attributeString, boolean compareParams, Object params, Runnable attributeCall) { - this.connectToken = connectToken; this.node = node; this.attributeString = attributeString; this.compareParams = compareParams; this.params = params; this.attributeCall = attributeCall; } + + boolean baseMembersEqualTo(RagConnectObserverEntry other) { + return baseMembersEqualTo(other.node, other.attributeString, other.compareParams, other.params); + } + + boolean baseMembersEqualTo(ASTNode otherNode, String otherAttributeString, + boolean otherCompareParams, Object otherParams) { + return this.node.equals(otherNode) && + this.attributeString.equals(otherAttributeString) && + this.compareParams == otherCompareParams && + (!this.compareParams || java.util.Objects.equals(this.params, otherParams)); + } } {{#configExperimentalJastAdd329}} @@ -124,7 +135,7 @@ aspect RagConnectObserver { {{#configExperimentalJastAdd329}} java.util.Set<RagConnectObserverEntry> entryQueue = new java.util.HashSet<>(); - RagConnectObserverStartEntry startEntry = null; + java.util.Deque<RagConnectObserverStartEntry> startEntries = new java.util.LinkedList<>(); {{/configExperimentalJastAdd329}} RagConnectObserver(ASTNode node) { @@ -145,42 +156,72 @@ aspect RagConnectObserver { {{#configLoggingEnabledForIncremental}} System.out.println("** observer add: " + node + " on " + attributeString + (compareParams ? " (parameterized)" : "")); {{/configLoggingEnabledForIncremental}} - observedNodes.add(new RagConnectObserverEntry(connectToken, node, attributeString, - compareParams, params, attributeCall)); + // either add to an existing entry (with same node, attribute) or create new entry + boolean needNewEntry = true; + for (RagConnectObserverEntry entry : observedNodes) { + if (entry.baseMembersEqualTo(node, attributeString, compareParams, params)) { + entry.connectList.add(connectToken); + needNewEntry = false; + break; + } + } + if (needNewEntry) { + RagConnectObserverEntry newEntry = new RagConnectObserverEntry(node, attributeString, + compareParams, params, attributeCall); + newEntry.connectList.add(connectToken); + observedNodes.add(newEntry); + } } void remove(RagConnectToken connectToken) { - observedNodes.removeIf(entry -> entry.connectToken.equals(connectToken)); + RagConnectObserverEntry entryToDelete = null; + for (RagConnectObserverEntry entry : observedNodes) { + entry.connectList.remove(connectToken); + if (entry.connectList.isEmpty()) { + entryToDelete = entry; + } + } + if (entryToDelete != null) { + observedNodes.remove(entryToDelete); + } } + @Override public void accept(ASTState.Trace.Event event, ASTNode node, String attribute, Object params, Object value) { oldReceiver.accept(event, node, attribute, params, value); {{#configExperimentalJastAdd329}} // react to INC_FLUSH_START and remember entry - if (event == ASTState.Trace.Event.INC_FLUSH_START && startEntry == null) { + if (event == ASTState.Trace.Event.INC_FLUSH_START && startEntries.isEmpty()) { {{#configLoggingEnabledForIncremental}} System.out.println("** observer start: " + node + " on " + attribute); {{/configLoggingEnabledForIncremental}} - startEntry = new RagConnectObserverStartEntry(node, attribute, value); + startEntries.addFirst(new RagConnectObserverStartEntry(node, attribute, value)); return; } // react to INC_FLUSH_END and process queued entries, if it matches start entry - if (event == ASTState.Trace.Event.INC_FLUSH_END && - node == startEntry.node && + if (event == ASTState.Trace.Event.INC_FLUSH_END) { + if (startEntries.isEmpty()) { + {{#configLoggingEnabledForIncremental}} + System.out.println("** observer end without start! for " + node + " on " + attribute); + {{/configLoggingEnabledForIncremental}} + return; + } + RagConnectObserverStartEntry startEntry = startEntries.removeFirst(); + if (node == startEntry.node && attribute == startEntry.attributeString && value == startEntry.flushIncToken) { - // create a copy of the queue to avoid entering this again causing an endless recursion - RagConnectObserverEntry[] entriesToProcess = entryQueue.toArray(new RagConnectObserverEntry[entryQueue.size()]); - entryQueue.clear(); - startEntry = null; - {{#configLoggingEnabledForIncremental}} - System.out.println("** observer process (entries: " + entriesToProcess.length + "): " + node + " on " + attribute); - {{/configLoggingEnabledForIncremental}} - for (RagConnectObserverEntry entry : entriesToProcess) { - entry.attributeCall.run(); + // create a copy of the queue to avoid entering this again causing an endless recursion + RagConnectObserverEntry[] entriesToProcess = entryQueue.toArray(new RagConnectObserverEntry[entryQueue.size()]); + entryQueue.clear(); + {{#configLoggingEnabledForIncremental}} + System.out.println("** observer process (entries: " + entriesToProcess.length + "): " + node + " on " + attribute); + {{/configLoggingEnabledForIncremental}} + for (RagConnectObserverEntry entry : entriesToProcess) { + entry.attributeCall.run(); + } + return; } - return; } {{/configExperimentalJastAdd329}} diff --git a/ragconnect.base/src/main/resources/sendDefinition.mustache b/ragconnect.base/src/main/resources/sendDefinition.mustache index 499116ea5e5686960099a55f8eb3b6fecdced52d..28050973478b56cfa74b39430b16cf347c74d612 100644 --- a/ragconnect.base/src/main/resources/sendDefinition.mustache +++ b/ragconnect.base/src/main/resources/sendDefinition.mustache @@ -49,7 +49,7 @@ public boolean {{parentTypeName}}.{{connectMethodName}}(String {{connectParamete {{#IndexBasedListAccess}}index,{{/IndexBasedListAccess}} () -> { if (this.{{updateMethodName}}({{#IndexBasedListAccess}}index{{/IndexBasedListAccess}})) { - this.{{writeMethodName}}({{#IndexBasedListAccess}}index, {{/IndexBasedListAccess}}connectToken); + this.{{writeMethodName}}({{#IndexBasedListAccess}}index{{/IndexBasedListAccess}}); } } ); @@ -106,6 +106,10 @@ 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); } diff --git a/ragconnect.tests/build.gradle b/ragconnect.tests/build.gradle index f57f477889c46775b0f8290c5b5bf0b484c0bc7b..8bc120a2d071c77105a2560ac5b8a8b204883d1d 100644 --- a/ragconnect.tests/build.gradle +++ b/ragconnect.tests/build.gradle @@ -50,7 +50,6 @@ 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' @@ -371,7 +370,6 @@ task compileTreeIncremental(type: RagConnectTest) { inputFiles = [file('src/test/01-input/tree/Test.relast'), file('src/test/01-input/tree/Test.connect')] rootNode = 'Root' - logWrites = true } relast { useJastAddNames = true @@ -414,7 +412,6 @@ task compileTreeAllowedTokensIncremental(type: RagConnectTest) { inputFiles = [file('src/test/01-input/treeAllowedTokens/Test.relast'), file('src/test/01-input/treeAllowedTokens/Test.connect')] rootNode = 'Root' - logWrites = true } relast { useJastAddNames = true @@ -605,9 +602,6 @@ task compileIndexedSendIncremental(type: RagConnectTest, dependsOn: ':ragconnect inputFiles = [file('src/test/01-input/indexedSend/Test.relast'), file('src/test/01-input/indexedSend/Test.connect')] rootNode = 'Root' - logWrites = true - logReads = true - logIncremental = true extraOptions = ['--experimental-jastadd-329'] } relast { @@ -630,9 +624,6 @@ task compileAttributeIncremental(type: RagConnectTest, dependsOn: ':ragconnect.b inputFiles = [file('src/test/01-input/attribute/Test.relast'), file('src/test/01-input/attribute/Test.connect')] rootNode = 'Root' - logWrites = true - logReads = true - logIncremental = true extraOptions = ['--experimental-jastadd-329'] } relast { @@ -647,4 +638,3 @@ task compileAttributeIncremental(type: RagConnectTest, dependsOn: ':ragconnect.b extraOptions = JASTADD_INCREMENTAL_OPTIONS_TRACING_FULL } } -compileAttributeIncremental.outputs.upToDateWhen { false } diff --git a/ragconnect.tests/src/test/01-input/attribute/Test.jadd b/ragconnect.tests/src/test/01-input/attribute/Test.jadd index c5274c6f73b0a49d77443522d9596cd8e8b5f858..34b1b45e7a181d0368df2da491c25c197cc009b8 100644 --- a/ragconnect.tests/src/test/01-input/attribute/Test.jadd +++ b/ragconnect.tests/src/test/01-input/attribute/Test.jadd @@ -1,7 +1,7 @@ aspect Computation { syn String SenderRoot.basic() = getInput(); syn String SenderRoot.simple() = getInput() + "Post"; - syn int SenderRoot.transformed() = getInput().length(); + syn int SenderRoot.transformed() = Integer.parseInt(getInput()); syn A SenderRoot.toReferenceType() { A result = new A(); result.setValue(getInput()); 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 ed6ef0ccd6800e4454c2a998ee783479dbf5ddf7..54f0bcafbbce3bff91d9939d7dd58f23460188f0 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,21 +1,20 @@ package org.jastadd.ragconnect.tests; import attributeInc.ast.*; -import de.tudresden.inf.st.jastadd.dumpAst.ast.Dumper; +import org.awaitility.core.ConditionFactory; import org.junit.jupiter.api.Tag; import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; +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.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; /** @@ -39,6 +38,7 @@ public class AttributeTest extends AbstractMqttTest { private static final String TOPIC_NTA_WITH_MAPPING = "attr/a/nta/mapped"; private static final String INITIAL_STRING = "initial"; + private static final String INITIAL_STRING_FOR_INT = "1"; private MqttHandler handler; private ReceiverData data; @@ -52,9 +52,9 @@ public class AttributeTest extends AbstractMqttTest { @Override protected void createModel() { model = new Root(); - // model.trace().setReceiver(TestUtils::logEvent); +// model.trace().setReceiver(TestUtils::logEvent); senderString = new SenderRoot().setInput(INITIAL_STRING); - senderInt = new SenderRoot().setInput(INITIAL_STRING); + senderInt = new SenderRoot().setInput(INITIAL_STRING_FOR_INT); senderA = new SenderRoot().setInput(INITIAL_STRING); receiverRoot = new ReceiverRoot(); model.addSenderRoot(senderString); @@ -98,6 +98,7 @@ public class AttributeTest extends AbstractMqttTest { waitForValue(senderString.basic(), receiverRoot::getFromBasic); waitForValue(senderString.simple(), receiverRoot::getFromSimpleNoMapping); + waitForValue(senderInt.transformed(), receiverRoot::getFromTransformedNoMapping); waitForNonNull(receiverRoot::getFromReferenceTypeNoMapping); waitForNonNull(receiverRoot::getFromNTANoMapping); } @@ -116,60 +117,130 @@ public class AttributeTest extends AbstractMqttTest { @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); + // basic, simple(2) <-- senderString + // transformed(2) <-- senderInt + // ref-type(2), nta(2) <-- senderA + check(9, INITIAL_STRING, INITIAL_STRING + "Post", INITIAL_STRING_FOR_INT, INITIAL_STRING, INITIAL_STRING); senderString.setInput("test-01"); - check(12, "test-01", "test-01Post", INITIAL_STRING, INITIAL_STRING, INITIAL_STRING); + check(12, "test-01", "test-01Post", INITIAL_STRING_FOR_INT, INITIAL_STRING, INITIAL_STRING); + + senderString.setInput("test-01"); + check(12, "test-01", "test-01Post", INITIAL_STRING_FOR_INT, INITIAL_STRING, INITIAL_STRING); + + senderInt.setInput("20"); + check(14, "test-01", "test-01Post", "20", INITIAL_STRING, INITIAL_STRING); + + senderA.setInput("test-03"); + check(18, "test-01", "test-01Post", "20", "test-03", "test-03"); + + assertTrue(senderString.disconnectSimple(mqttUri(TOPIC_SIMPLE_NO_MAPPING))); + assertTrue(senderString.disconnectSimple(mqttUri(TOPIC_SIMPLE_WITH_MAPPING))); + senderString.setInput("test-04"); + check(19, "test-04", "test-01Post", "20", "test-03", "test-03"); + + assertTrue(senderA.disconnectToNTA(mqttUri(TOPIC_NTA_NO_MAPPING))); + senderA.setInput("test-05"); + check(22, "test-04", "test-01Post", "20", "test-05", "test-03"); } @Override protected void communicateOnlyUpdatedValue() throws IOException, InterruptedException { waitForMqtt(); + // basic, simple(2) <-- senderString + // transformed(2) <-- senderInt + // ref-type(2), nta(2) <-- senderA + check(0, null, null, null, null, null); + + senderString.setInput("test-01"); + check(3, "test-01", "test-01Post", null, null, null); + + senderString.setInput("test-01"); + check(3, "test-01", "test-01Post", null, null, null); + + senderInt.setInput("20"); + check(5, "test-01", "test-01Post", "20", null, null); + + senderA.setInput("test-03"); + check(9, "test-01", "test-01Post", "20", "test-03", "test-03"); + + assertTrue(senderString.disconnectSimple(mqttUri(TOPIC_SIMPLE_NO_MAPPING))); + assertTrue(senderString.disconnectSimple(mqttUri(TOPIC_SIMPLE_WITH_MAPPING))); + senderString.setInput("test-04"); + check(10, "test-04", "test-01Post", "20", "test-03", "test-03"); + + assertTrue(senderA.disconnectToNTA(mqttUri(TOPIC_NTA_NO_MAPPING))); + senderA.setInput("test-05"); + check(13, "test-04", "test-01Post", "20", "test-05", "test-03"); } private void check(int numberOfValues, String basic, String simple, String transformed, - String refType, String nta) { + String a, String ntaNoMapping) { 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(Objects.requireNonNullElse(basic, ""), + receiverRoot::getFromBasic, "basic"); + + if (simple != null) { + awaitEquals(simple, + receiverRoot::getFromSimpleNoMapping, "simple"); + awaitEquals(simple + "post", + receiverRoot::getFromSimpleWithMapping, "simple mapped"); + } else { + awaitEquals("", + receiverRoot::getFromSimpleNoMapping, "simple null"); + awaitEquals("", + receiverRoot::getFromSimpleWithMapping, "simple mapped null"); } - awaitEquals(basic, receiverRoot::getFromBasic, "basic"); + if (transformed != null) { + awaitEquals(Integer.parseInt(transformed), + receiverRoot::getFromTransformedNoMapping, "transformed"); + awaitEquals(Integer.parseInt(transformed) + 1, + receiverRoot::getFromTransformedWithMapping, "transformed mapped"); + } else { + awaitEquals(0, + receiverRoot::getFromTransformedNoMapping, "transformed null"); + awaitEquals(0, + receiverRoot::getFromTransformedWithMapping, "transformed mapped null"); + } - awaitEquals(simple, receiverRoot::getFromSimpleNoMapping, "simple"); - awaitEquals(simple + "post", receiverRoot::getFromSimpleWithMapping, "simple mapped"); + if (a != null) { + awaitA(a, "1", + receiverRoot.getFromReferenceTypeNoMapping(), "ref-type"); + awaitA(a + "post", "inner1", + receiverRoot.getFromReferenceTypeWithMapping(), "ref-type mapped"); + awaitA(a + "post", "inner2", + receiverRoot.getFromNTAWithMapping(), "nta mapped"); + } else { + awaitNull(receiverRoot::getFromReferenceTypeNoMapping, "manual ref-type null"); + awaitNull(receiverRoot::getFromReferenceTypeWithMapping, "ref-type mapped null"); + awaitNull(receiverRoot::getFromNTAWithMapping, "nta mapped null"); + } - int transformedLength = transformed.length(); - awaitEquals(transformedLength, receiverRoot::getFromTransformedNoMapping, "transformed"); - awaitEquals(transformedLength + 1, receiverRoot::getFromTransformedWithMapping, "transformed mapped"); + if (ntaNoMapping != null) { + awaitA(ntaNoMapping, "2", + receiverRoot.getFromNTANoMapping(), "nta"); + } else { + awaitNull(receiverRoot::getFromNTANoMapping, "nta null"); + } + } - checkA(refType, "1", - receiverRoot.getFromReferenceTypeNoMapping(), "ref-type"); - checkA(refType + "post", "inner1", - receiverRoot.getFromReferenceTypeWithMapping(), "ref-type mapped"); + private void awaitNull(Supplier<A> actual, String alias) { + internalAwait(alias).until(() -> actual.get() == null); + } - checkA(nta, "2", - receiverRoot.getFromNTANoMapping(), "nta"); - checkA(nta + "post", "inner2", - receiverRoot.getFromNTAWithMapping(), "nta mapped"); + private <T> void awaitEquals(T expected, Callable<T> actual, String alias) { + internalAwait(alias).until(actual, isEqual(expected)); } - private void checkA(String expectedValue, String expectedInner, A actual, String message) { + private void awaitA(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)); + private ConditionFactory internalAwait(String alias) { + return await(alias).atMost(1500, TimeUnit.MILLISECONDS); } @Override