diff --git a/pages/docs/dsl.md b/pages/docs/dsl.md index 19125a893a07aa3dda53a34ddd7f46537675bbb8..e8b6b2bb11c67168bafd33efa48b505248ea9b37 100644 --- a/pages/docs/dsl.md +++ b/pages/docs/dsl.md @@ -13,7 +13,7 @@ The kind of the element determines, whether an endpoint for it can be receiving, To declare a new endpoints, use the following syntax: ``` -("send"|"receive") ["indexed"] ["with add"] <Non-Terminal>[.<Target>["()"]] ["using" <Mapping-Name> (, <Mapping-Name>)*] ";" +("send"|"receive") ["indexed"] ["with add"] <Non-Terminal>[.<Target>["(<AttributeType>)"]] ["using" <Mapping-Name> (, <Mapping-Name>)*] ";" ``` A breakdown of the parts of that syntax: @@ -28,10 +28,12 @@ A breakdown of the parts of that syntax: - The second optional keyword `with add` can also be used only for receiving endpoints targeting a list children. As described above, it can be combined with `indexed`. If used on its own, the incoming data is interpreted as a complete list and its elements will be appended to the current list. -- The `<Non-Terminal>[.<Target>["()"]]` notation describes the actual affected node. +- The `<Non-Terminal>[.<Target>["(<AttributeType>)"]]` notation describes the actual affected node. - If the target is omitted, all nodes of that non-terminal type can be connected, irrespective of their context. This is a context-free endpoint definition. - The target can be any child on the right-hand side of a production rule, a role of a relation, or an attribute. - The brackets `()` after the target must be used in case of an attribute, and only then. + The brackets `(<AttributeType>)` after the target must be used in case of an attribute, and only then. + Here, the return type of the attribute has to be specified, as aspect files are not parsed by RagConnect. + Hence, RagConnect can not and will not verify the existence of the attribute, and the possible non-existence of an attribute will be found by the Java compiler. - Optionally, an endpoint can use one or more [mappings](#mappings). They will be applied before sending, or after receiving a message. Mappings will always be applied in the order they are listed after `using`. diff --git a/pages/docs/inner-workings.md b/pages/docs/inner-workings.md index bd9986a9c7c81c034e30e57e364d243393a8e39a..1cb22a597804d2a67a6f05c0181f4b7c359644ee 100644 --- a/pages/docs/inner-workings.md +++ b/pages/docs/inner-workings.md @@ -1,4 +1,4 @@ -# Inner workings of `RagConnect` +# Inner Workings of `RagConnect` Please see [API documentation](ragdoc/index.html) for more details. @@ -20,11 +20,11 @@ The other main aspect (which is currently not really used) is `IntermediateToYAM This is used to generate a YAML file containing the data used by mustache. It can be used by the default mustache implementation together with the templates. -# Implementation details +# Implementation Details In the following, details for special implementation topics are discussed. -## forwarding +## Forwarding When a nonterminal is used in a send endpoints, it needs an implicit forwarding attribute to work, because only _computed elements_ can be sent. Since the nonterminal itself should be sent, the generated attribute simply returns this nonterminal. @@ -33,3 +33,29 @@ However, changing any token within the whole subtree or changing the structure o This way, the dependency tracking registers a dependency between structure and tokens to the attribute. The attribute (as well as any other generated element) is prefixed with `_ragconnect_` to avoid potential name conflicts with user-specified elements. + +# Implementation Hints + +## Debugging Tests and Finding Bugs + +To help with finding errors/bugs when tests fail, there are several things to find the correct spot. + +- **Look closely**. Analyze the error message closely, and possible any previous error message(s) that could have caused the test to fail. +- **Focus on single error** + - To only inspect one test, mark them with `@Tag("New")` and use the gradle task "newTests". + - Use `Assumptions.assumeTrue(false);` to abort unneeded test cases early. + - When editing RagConnect itself and force recreating source for the affected test, e.g., `compileForwardingIncremental.outputs.upToDateWhen { false }` + - _Remember to undo all changes, once the bug is fixed._ +- **Activate logs**. Add the following to the `ragconnect` specification of the compile-task of the affected test: + ``` + logReads = true + logWrites = true + logIncremental = true + ``` + _Remember to remove those lines, once the bug is fixed._ +- **Trace incremental events**. Add the following right after create the root node (named `model` here): + ```java + model.trace().setReceiver(TestUtils::logEvent); + ``` + This will output every event fired by the incremental evaluation engine. _Remember to remove this line, once the bug is fixed._ +- **Add log statements**. As there will be quite some log output, add some identifying log statement (i.e., using `logger.fatal("---")`) right before the suspicious statement to inspect only the relevant log message after that. diff --git a/ragconnect.base/src/main/jastadd/Analysis.jrag b/ragconnect.base/src/main/jastadd/Analysis.jrag index 85ff38d40d88ddbc415d3bd3ea39867c00d1e2fd..d4e166a34a2b62150d76dfa0decf3c0a827bb649 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,13 +76,17 @@ aspect Analysis { } } - syn boolean EndpointTarget.entityIsNormalAttribute(); - eq TokenEndpointTarget.entityIsNormalAttribute() = !getToken().getNTA(); - eq TypeEndpointTarget.entityIsNormalAttribute() = !getType().getNTA(); - eq ContextFreeTypeEndpointTarget.entityIsNormalAttribute() = false; + syn boolean EndpointTarget.hasAttributeResetMethod(); + eq AttributeEndpointTarget.hasAttributeResetMethod() = false; + eq TokenEndpointTarget.hasAttributeResetMethod() = getToken().getNTA(); + eq TypeEndpointTarget.hasAttributeResetMethod() = getType().getNTA(); + eq ContextFreeTypeEndpointTarget.hasAttributeResetMethod() = false; // --- needProxyToken --- - syn boolean TokenComponent.needProxyToken() = !getDependencySourceDefinitionList().isEmpty() || getTokenEndpointTargetList().stream().map(EndpointTarget::containingEndpointDefinition).anyMatch(EndpointDefinition::shouldSendValue); + syn boolean TokenComponent.needProxyToken() = !getDependencySourceDefinitionList().isEmpty() || + getTokenEndpointTargetList().stream() + .map(EndpointTarget::containingEndpointDefinition) + .anyMatch(EndpointDefinition::shouldNotResetValue); // --- effectiveUsedAt --- coll Set<EndpointDefinition> MappingDefinition.effectiveUsedAt() diff --git a/ragconnect.base/src/main/jastadd/Intermediate.jadd b/ragconnect.base/src/main/jastadd/Intermediate.jadd index bcd4cc4da4242ccd317f3d23bb66bb70a62f3599..47f3aa473e0959ece3a24b684b98b59893c58413 100644 --- a/ragconnect.base/src/main/jastadd/Intermediate.jadd +++ b/ragconnect.base/src/main/jastadd/Intermediate.jadd @@ -168,6 +168,10 @@ aspect MustacheMappingApplicationAndDefinition { syn String MEndpointDefinition.preemptiveReturn(); syn String MEndpointDefinition.firstInputVarName(); + 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 +327,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 +352,7 @@ aspect MustacheReceiveAndSendAndHandleUri { } else { extra = ""; } - return "disconnect" + extra + entityName(); + return "disconnect" + extra + capitalize(entityName()); } syn String EndpointDefinition.entityName() = getEndpointTarget().entityName(); @@ -371,6 +375,10 @@ aspect MustacheReceiveAndSendAndHandleUri { syn String EndpointTarget.parentTypeName(); syn String EndpointTarget.entityName(); + eq AttributeEndpointTarget.getterMethodName() = getName(); + eq AttributeEndpointTarget.parentTypeName() = getParentTypeDecl().getName(); + eq AttributeEndpointTarget.entityName() = getName(); + eq TokenEndpointTarget.getterMethodName() = "get" + getToken().getName(); eq TokenEndpointTarget.parentTypeName() = getToken().containingTypeDecl().getName(); eq TokenEndpointTarget.entityName() = getToken().getName(); @@ -429,7 +437,7 @@ aspect MustacheSendDefinition { syn String EndpointDefinition.senderName() = getEndpointTarget().senderName(); - syn boolean EndpointDefinition.shouldSendValue() = getSend() && getEndpointTarget().entityIsNormalAttribute(); + syn boolean EndpointDefinition.shouldNotResetValue() = getSend() && !getEndpointTarget().hasAttributeResetMethod(); syn String EndpointDefinition.tokenResetMethodName() = getterMethodName() + "_reset"; @@ -456,14 +464,15 @@ aspect MustacheSendDefinition { getTypeDecl().getName() : ragconnect().configJastAddList() + "<" + getTypeDecl().getName() + ">"; - syn String EndpointTarget.senderName(); - eq TokenEndpointTarget.senderName() = ragconnect().internalRagConnectPrefix() + "_sender_" + getToken().getName(); - eq TypeEndpointTarget.senderName() = ragconnect().internalRagConnectPrefix() + "_sender_" + getType().getName(); + syn String EndpointTarget.senderName() = ragconnect().internalRagConnectPrefix() + "_sender_" + entityName(); eq ContextFreeTypeEndpointTarget.senderName() = null; syn String MEndpointDefinition.updateMethodName(); syn String MEndpointDefinition.writeMethodName(); + eq MAttributeSendDefinition.updateMethodName() = ragconnect().internalRagConnectPrefix() + "_update_attr_" + getEndpointDefinition().entityName(); + eq MAttributeSendDefinition.writeMethodName() = ragconnect().internalRagConnectPrefix() + "_writeLastValue_attr_" + getEndpointDefinition().entityName(); + eq MTokenReceiveDefinition.updateMethodName() = null; eq MTokenReceiveDefinition.writeMethodName() = null; @@ -497,7 +506,7 @@ aspect MustacheTokenComponent { syn EndpointDefinition TokenComponent.normalTokenSendDef() { for (EndpointTarget target : getTokenEndpointTargetList()) { - if (target.isTokenEndpointTarget() && target.containingEndpointDefinition().shouldSendValue()) { + if (target.isTokenEndpointTarget() && target.containingEndpointDefinition().shouldNotResetValue()) { return target.containingEndpointDefinition(); } } @@ -580,6 +589,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 79097503ef32b417be179ccb5f679886a6297582..486158a4a4094291a756cf8c7715ed6104f5e252 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/IntermediateToYAML.jrag b/ragconnect.base/src/main/jastadd/IntermediateToYAML.jrag index fb198ce9cfe34db3234cc5a7feff5ddfb210dc91..49c2693696a02de14852e252f3aadd3a722dfb95 100644 --- a/ragconnect.base/src/main/jastadd/IntermediateToYAML.jrag +++ b/ragconnect.base/src/main/jastadd/IntermediateToYAML.jrag @@ -103,7 +103,7 @@ aspect IntermediateToYAML { result.put("lastValueGetterCall" , lastValueGetterCall()); result.put("lastValueSetter" , lastValueSetter()); result.put("senderName" , senderName()); - result.put("shouldSendValue" , shouldSendValue()); + result.put("shouldNotResetValue" , shouldNotResetValue()); result.put("tokenResetMethodName" , tokenResetMethodName()); result.put("updateMethodName" , updateMethodName()); result.put("writeMethodName" , writeMethodName()); diff --git a/ragconnect.base/src/main/jastadd/Mappings.jrag b/ragconnect.base/src/main/jastadd/Mappings.jrag index 738028b3a264a0c7dcf976618aa0b63f35be62c5..aa070695a83e31f819d62308b094679b27125a8e 100644 --- a/ragconnect.base/src/main/jastadd/Mappings.jrag +++ b/ragconnect.base/src/main/jastadd/Mappings.jrag @@ -191,13 +191,6 @@ aspect Mappings { // --- suitableReceiveDefaultMapping --- syn DefaultMappingDefinition EndpointDefinition.suitableReceiveDefaultMapping() { - if (getEndpointTarget().isTypeEndpointTarget()) { - try { - TypeDecl typeDecl = program().resolveTypeDecl(targetTypeName()); - return typeIsList() && !getIndexBasedListAccess() ? ragconnect().defaultBytesToListTreeMapping(typeDecl.getName()) : ragconnect().defaultBytesToTreeMapping(typeDecl.getName()); - } catch (Exception ignore) { - } - } switch (targetTypeName()) { case "boolean": case "Boolean": @@ -225,8 +218,7 @@ aspect Mappings { default: try { TypeDecl typeDecl = program().resolveTypeDecl(targetTypeName()); - // TODO: also support list-types, if list is first type - return ragconnect().defaultBytesToTreeMapping(typeDecl.getName()); + return getEndpointTarget().isTypeEndpointTarget() && typeIsList() && !getIndexBasedListAccess() ? ragconnect().defaultBytesToListTreeMapping(typeDecl.getName()) : ragconnect().defaultBytesToTreeMapping(typeDecl.getName()); } catch (Exception ignore) { } System.err.println("Could not find suitable default mapping for " + targetTypeName() + " on " + this); @@ -236,9 +228,6 @@ aspect Mappings { // --- suitableSendDefaultMapping --- syn DefaultMappingDefinition EndpointDefinition.suitableSendDefaultMapping() { - if (getEndpointTarget().isTypeEndpointTarget() && typeIsList() && !getIndexBasedListAccess()) { - return ragconnect().defaultListTreeToBytesMapping(); - } switch (targetTypeName()) { case "boolean": case "Boolean": @@ -264,6 +253,9 @@ aspect Mappings { case "String": return ragconnect().defaultStringToBytesMapping(); default: + if (getEndpointTarget().isTypeEndpointTarget() && typeIsList() && !getIndexBasedListAccess()) { + return ragconnect().defaultListTreeToBytesMapping(); + } try { TypeDecl typeDecl = program().resolveTypeDecl(targetTypeName()); return ragconnect().defaultTreeToBytesMapping(typeDecl.getName()); @@ -288,6 +280,7 @@ aspect Mappings { } } syn String EndpointTarget.targetTypeName(); + eq AttributeEndpointTarget.targetTypeName() = getTypeName(); eq TokenEndpointTarget.targetTypeName() = getToken().effectiveJavaTypeUse().getName(); eq TypeEndpointTarget.targetTypeName() = getType().getTypeDecl().getName(); eq ContextFreeTypeEndpointTarget.targetTypeName() = getTypeDecl().getName(); diff --git a/ragconnect.base/src/main/jastadd/Navigation.jrag b/ragconnect.base/src/main/jastadd/Navigation.jrag index a65ab72fa946bcec3351bb496f6632c012bac678..dac193142b1382f01b61e696a0f9d5b02dd8b038 100644 --- a/ragconnect.base/src/main/jastadd/Navigation.jrag +++ b/ragconnect.base/src/main/jastadd/Navigation.jrag @@ -24,6 +24,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' */ @@ -51,6 +57,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/RagConnect.relast b/ragconnect.base/src/main/jastadd/RagConnect.relast index 01a0f2f884a2f0824bf2787eaf42d7d2d5797c41..f6cf0dd68708157aa291b6b0b50077b2fe72034c 100644 --- a/ragconnect.base/src/main/jastadd/RagConnect.relast +++ b/ragconnect.base/src/main/jastadd/RagConnect.relast @@ -13,9 +13,10 @@ TypeEndpointTarget : EndpointTarget; rel TypeEndpointTarget.Type <-> TypeComponent.TypeEndpointTarget*; ContextFreeTypeEndpointTarget : EndpointTarget; rel ContextFreeTypeEndpointTarget.TypeDecl <-> TypeDecl.ContextFreeTypeEndpointTarget*; -UntypedEndpointTarget : EndpointTarget ::= <TypeName> <ChildName>; // only used by parser +UntypedEndpointTarget : EndpointTarget ::= <TypeName> <ChildName> <IsAttribute:boolean>; // only used by parser // to be integrated: -//AttributeEndpointTarget : EndpointTarget ::= <Name> ; +AttributeEndpointTarget : EndpointTarget ::= <Name> <TypeName> ; +rel AttributeEndpointTarget.ParentTypeDecl <-> TypeDecl.AttributeEndpointTarget*; //RelationEndpointTarget : EndpointTarget ; //rel RelationEndpointTarget.Role <-> Role.RelationEndpointTarget* ; diff --git a/ragconnect.base/src/main/jastadd/Util.jadd b/ragconnect.base/src/main/jastadd/Util.jadd index 81b820c0b4610e17f82aa31f97c44c97405443e0..31b15ba693b320a4cb502ef3034ea036979bf05b 100644 --- a/ragconnect.base/src/main/jastadd/Util.jadd +++ b/ragconnect.base/src/main/jastadd/Util.jadd @@ -1,5 +1,7 @@ aspect Util { static String ASTNode.capitalize(String s) { + if (s == null) return null; + if (s.isEmpty()) return ""; return Character.toUpperCase(s.charAt(0)) + s.substring(1); } protected T JastAddList.firstChild() { return getChild(0); } diff --git a/ragconnect.base/src/main/jastadd/parser/ParserRewrites.jrag b/ragconnect.base/src/main/jastadd/parser/ParserRewrites.jrag index f4a8912fe7c2affa7034d3eb8651f9d210bbba54..04e8cadc459653830e5f29e2d189b1aa2c6c0ad5 100644 --- a/ragconnect.base/src/main/jastadd/parser/ParserRewrites.jrag +++ b/ragconnect.base/src/main/jastadd/parser/ParserRewrites.jrag @@ -23,6 +23,19 @@ aspect ParserRewrites { result.setTypeDecl(TypeDecl.createRef(getTypeName())); return result; } + + when (getIsAttribute()) + to AttributeEndpointTarget { + AttributeEndpointTarget result = new AttributeEndpointTarget(); + String[] tokens = this.getChildName().split(":"); + String attributeName = tokens[0]; + String attributeTypeName = tokens[1]; + result.copyOtherValuesFrom(this); + result.setName(attributeName); + result.setTypeName(attributeTypeName); + result.setParentTypeDecl(TypeDecl.createRef(getTypeName())); + return result; + } } syn String UntypedEndpointTarget.combinedName() = getTypeName() + "." + getChildName(); @@ -37,7 +50,7 @@ aspect ParserRewrites { eq UntypedEndpointTarget.parentTypeName() = "<untyped.parentTypeName>"; eq UntypedEndpointTarget.entityName() = "<untyped.entityName>"; eq UntypedEndpointTarget.isAlreadyDefined() = false; - eq UntypedEndpointTarget.entityIsNormalAttribute() = false; + eq UntypedEndpointTarget.hasAttributeResetMethod() = false; eq UntypedEndpointTarget.targetTypeName() = "<untyped.targetTypeName>"; eq UntypedEndpointTarget.isTypeEndpointTarget() = false; } diff --git a/ragconnect.base/src/main/jastadd/parser/RagConnect.parser b/ragconnect.base/src/main/jastadd/parser/RagConnect.parser index 54a388e51d35bc52296fd4f60e220d325be00674..f822bf01139341080c65f5026bd5661124cafd4e 100644 --- a/ragconnect.base/src/main/jastadd/parser/RagConnect.parser +++ b/ragconnect.base/src/main/jastadd/parser/RagConnect.parser @@ -62,8 +62,10 @@ EndpointDefinition endpoint_definition_type ; EndpointTarget endpoint_target - = ID.type_name DOT ID.child_name {: return new UntypedEndpointTarget(type_name, child_name); :} - | ID.type_name {: return new UntypedEndpointTarget(type_name, ""); :} + = 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 {: return new UntypedEndpointTarget(type_name, "", false); :} ; ArrayList string_list diff --git a/ragconnect.base/src/main/jastadd/scanner/Keywords.flex b/ragconnect.base/src/main/jastadd/scanner/Keywords.flex index 8a1eaec1e8d95c0d8786f9f44168d1f060b27cfa..cda59f5c346fa12f5b83723ed82a0c8849d639b8 100644 --- a/ragconnect.base/src/main/jastadd/scanner/Keywords.flex +++ b/ragconnect.base/src/main/jastadd/scanner/Keywords.flex @@ -8,3 +8,5 @@ "with" { return sym(Terminals.WITH); } "indexed" { return sym(Terminals.INDEXED); } "add" { return sym(Terminals.ADD); } +"(" { 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 a7ef5977683a236275a14eb37b9775d24f31526d..0d368731fd37454d5ea3982ebd2a3e2d03c8f047 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/ragconnect.mustache b/ragconnect.base/src/main/resources/ragconnect.mustache index fbe3dd572676b60fdb826ec01b098d8a6aa7252a..dc6b839a87050d3993cd1bf6f19ffe3694d16242 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,73 @@ 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) { {{#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.peekFirst(); + 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(); + startEntries.removeFirst(); + {{#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 293b37c65f2159687fbbe81c8eb112789dd952e9..18d48cafc978318d3e06b32af8e9f315e75c31a4 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; @@ -97,9 +97,9 @@ public boolean {{parentTypeName}}.{{disconnectMethodName}}(String {{connectParam } protected boolean {{parentTypeName}}.{{updateMethodName}}({{#IndexBasedListAccess}}int index{{/IndexBasedListAccess}}) { - {{^shouldSendValue}} + {{^shouldNotResetValue}} {{tokenResetMethodName}}(); - {{/shouldSendValue}} + {{/shouldNotResetValue}} {{> mappingApplication}} {{lastValueSetter}}({{#IndexBasedListAccess}}index, {{/IndexBasedListAccess}}{{lastResult}}); // normally we would return true here. unless no connect method was called so far to initialize {{senderName}} yet @@ -110,6 +110,10 @@ protected void {{parentTypeName}}.{{writeMethodName}}({{#IndexBasedListAccess}}i {{senderName}}.run({{#IndexBasedListAccess}}index{{/IndexBasedListAccess}}); } +protected void {{parentTypeName}}.{{writeMethodName}}({{#IndexBasedListAccess}}int index, {{/IndexBasedListAccess}}RagConnectToken token) { + {{senderName}}.run({{#IndexBasedListAccess}}index, {{/IndexBasedListAccess}}token); +} + {{#needForwardingNTA}} syn {{{forwardingNTA_Type}}} {{parentTypeName}}.{{forwardingNTA_Name}}({{#IndexBasedListAccess}}int index{{/IndexBasedListAccess}}) = {{realGetterMethodCall}}.{{touchedTerminalsMethodName}}(); {{/needForwardingNTA}} diff --git a/ragconnect.tests/build.gradle b/ragconnect.tests/build.gradle index 8d073b55ac6b14ebdf6b295bd993dda64dbbd701..8bc120a2d071c77105a2560ac5b8a8b204883d1d 100644 --- a/ragconnect.tests/build.gradle +++ b/ragconnect.tests/build.gradle @@ -370,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 @@ -413,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 @@ -604,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 { @@ -621,3 +616,25 @@ task compileIndexedSendIncremental(type: RagConnectTest, dependsOn: ':ragconnect extraOptions = JASTADD_INCREMENTAL_OPTIONS_TRACING_FULL } } + +// --- Test: attribute-incremental --- +task compileAttributeIncremental(type: RagConnectTest, dependsOn: ':ragconnect.base:jar') { + ragconnect { + outputDir = file('src/test/02-after-ragconnect/attributeInc') + inputFiles = [file('src/test/01-input/attribute/Test.relast'), + file('src/test/01-input/attribute/Test.connect')] + rootNode = 'Root' + extraOptions = ['--experimental-jastadd-329'] + } + relast { + useJastAddNames = true + grammarName = 'src/test/03-after-relast/attributeInc/attributeInc' + serializer = 'jackson' + } + jastadd { + jastAddList = 'JastAddList' + packageName = 'attributeInc.ast' + inputFiles = [file('src/test/01-input/attribute/Test.jadd')] + extraOptions = JASTADD_INCREMENTAL_OPTIONS_TRACING_FULL + } +} diff --git a/ragconnect.tests/src/test/01-input/attribute/README.md b/ragconnect.tests/src/test/01-input/attribute/README.md new file mode 100644 index 0000000000000000000000000000000000000000..08481b7a25582721dfa8435ac199b94acb205de6 --- /dev/null +++ b/ragconnect.tests/src/test/01-input/attribute/README.md @@ -0,0 +1,3 @@ +# Attribute + +Idea: Use send definitions for attributes. diff --git a/ragconnect.tests/src/test/01-input/attribute/Test.connect b/ragconnect.tests/src/test/01-input/attribute/Test.connect new file mode 100644 index 0000000000000000000000000000000000000000..03688a96bdb327076e974498d3ce62c9497f89be --- /dev/null +++ b/ragconnect.tests/src/test/01-input/attribute/Test.connect @@ -0,0 +1,31 @@ +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" + a.getInner().getInnerValue())); + return result; +:} + +AddStringSuffix maps String s to String {: + return s + "post"; +:} + +AddPlusOne maps int i to int {: + return i + 1; +:} + +receive ReceiverRoot.FromBasic; +receive ReceiverRoot.FromSimpleNoMapping; +receive ReceiverRoot.FromSimpleWithMapping using AddStringSuffix; +receive ReceiverRoot.FromTransformedNoMapping; +receive ReceiverRoot.FromTransformedWithMapping using AddPlusOne; +receive ReceiverRoot.FromReferenceTypeNoMapping; +receive ReceiverRoot.FromReferenceTypeWithMapping using AddSuffix; +receive ReceiverRoot.FromNTANoMapping; +receive ReceiverRoot.FromNTAWithMapping using AddSuffix; diff --git a/ragconnect.tests/src/test/01-input/attribute/Test.jadd b/ragconnect.tests/src/test/01-input/attribute/Test.jadd new file mode 100644 index 0000000000000000000000000000000000000000..34b1b45e7a181d0368df2da491c25c197cc009b8 --- /dev/null +++ b/ragconnect.tests/src/test/01-input/attribute/Test.jadd @@ -0,0 +1,39 @@ +aspect Computation { + syn String SenderRoot.basic() = getInput(); + syn String SenderRoot.simple() = getInput() + "Post"; + syn int SenderRoot.transformed() = Integer.parseInt(getInput()); + syn A SenderRoot.toReferenceType() { + A result = new A(); + result.setValue(getInput()); + Inner inner = new Inner(); + inner.setInnerValue("1"); + result.setInner(inner); + return result; + } + syn nta A SenderRoot.toNTA() { + A result = new A(); + result.setValue(getInput()); + Inner inner = new Inner(); + inner.setInnerValue("2"); + result.setInner(inner); + return result; + } +} +aspect MakeCodeCompile { + +} +aspect MakeCodeWork { + +} +aspect NameResolution { + // overriding customID guarantees to produce the same JSON representation for equal lists + // otherwise, the value for id is different each time + @Override + protected String A.customID() { + return getClass().getSimpleName() + getValue(); + } + @Override + protected String Inner.customID() { + return getClass().getSimpleName() + getInnerValue(); + } +} diff --git a/ragconnect.tests/src/test/01-input/attribute/Test.relast b/ragconnect.tests/src/test/01-input/attribute/Test.relast new file mode 100644 index 0000000000000000000000000000000000000000..7afc9b7745c0da1cab4af5140fdddb2e5981f3b1 --- /dev/null +++ b/ragconnect.tests/src/test/01-input/attribute/Test.relast @@ -0,0 +1,14 @@ +Root ::= SenderRoot* ReceiverRoot; +SenderRoot ::= <Input> ; +ReceiverRoot ::= + <FromBasic> + <FromSimpleNoMapping> + <FromSimpleWithMapping> + <FromTransformedNoMapping:int> + <FromTransformedWithMapping:int> + FromReferenceTypeNoMapping:A + FromReferenceTypeWithMapping:A + FromNTANoMapping:A + FromNTAWithMapping:A ; +A ::= <Value> Inner ; +Inner ::= <InnerValue> ; diff --git a/ragconnect.tests/src/test/01-input/errors/Standard.connect b/ragconnect.tests/src/test/01-input/errors/Standard.connect index 6224df4184a2f7cc21d913934785cd022b14f5b0..fb2c7cb2400d1b8ca44903a9aeb3bb0bfcf28537 100644 --- a/ragconnect.tests/src/test/01-input/errors/Standard.connect +++ b/ragconnect.tests/src/test/01-input/errors/Standard.connect @@ -60,6 +60,15 @@ D.SourceDoubledValue canDependOn D.TargetDoubledValue as DoubledValue ; D.SourceDoubledValue canDependOn D.TargetDoubledValue as DoubledValue ; send D.TargetDoubledValue; +// non-existence of attributes is not checked by RagConnect +send A.nonExistingAttribute(int); + +// Already defined endpoints for attributes will be reported, however +send A.nonExistingAttribute(int); + +// mappings are not checked, here string would not match +send A.anotherIntAttribute(int) using StringToString; + // --- mapping definitions --- ListToList maps java.util.List<String> list to java.util.List<String> {: return list; diff --git a/ragconnect.tests/src/test/01-input/errors/Standard.expected b/ragconnect.tests/src/test/01-input/errors/Standard.expected index 9b3d23ac98118e0857ffb9964e1a04d592b1dea1..1f2b02da133cc26bc9660b466112cb73421ab529 100644 --- a/ragconnect.tests/src/test/01-input/errors/Standard.expected +++ b/ragconnect.tests/src/test/01-input/errors/Standard.expected @@ -11,3 +11,5 @@ Standard.connect Line 43, column 1: Endpoint definition already defined for C.Do Standard.connect Line 44, column 1: Endpoint definition already defined for C.DoubledValue Standard.connect Line 55, column 1: The name of a dependency definition must not be equal to a list-node on the source Standard.connect Line 60, column 1: Dependency definition already defined for D with name DoubledValue +Standard.connect Line 64, column 1: Endpoint definition already defined for A.nonExistingAttribute +Standard.connect Line 67, column 1: Endpoint definition already defined for A.nonExistingAttribute 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 new file mode 100644 index 0000000000000000000000000000000000000000..2e7e0c7d79bd7d20e938eff6257008379306bc8d --- /dev/null +++ b/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/AttributeTest.java @@ -0,0 +1,251 @@ +package org.jastadd.ragconnect.tests; + +import attributeInc.ast.*; +import org.junit.jupiter.api.Tag; + +import java.io.IOException; +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.jastadd.ragconnect.tests.TestUtils.*; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Test case "attribute". + * + * @author rschoene - Initial contribution + */ +@Tag("Incremental") +public class AttributeTest extends AbstractMqttTest { + + 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 static final String INITIAL_STRING_FOR_INT = "1"; + + 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_FOR_INT); + 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); + waitForValue(senderInt.transformed(), receiverRoot::getFromTransformedNoMapping); + waitForNonNull(receiverRoot::getFromReferenceTypeNoMapping); + waitForNonNull(receiverRoot::getFromNTANoMapping); + } + + private <T> void waitForValue(T expectedValue, Callable<T> callable) { + if (isWriteCurrentValue()) { + awaitMqtt().until(callable, isEqual(expectedValue)); + } + } + + private <T> void waitForNonNull(Callable<T> callable) { + if (isWriteCurrentValue()) { + awaitMqtt().until(callable, Predicate.not(isEqual(null))); + } + } + + @Override + protected void communicateSendInitialValue() throws IOException, InterruptedException { + // 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_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 a, String ntaNoMapping) { + awaitEquals(numberOfValues, () -> data.numberOfValues, "numberOfValues"); + + 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"); + } + + 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"); + } + + 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"); + } + + if (ntaNoMapping != null) { + awaitA(ntaNoMapping, "2", + receiverRoot.getFromNTANoMapping(), "nta"); + } else { + awaitNull(receiverRoot::getFromNTANoMapping, "nta null"); + } + } + + private void awaitNull(Supplier<A> actual, String alias) { + awaitMqtt().alias(alias).until(() -> actual.get() == null); + } + + private <T> void awaitEquals(T expected, Callable<T> actual, String alias) { + awaitMqtt().alias(alias).until(actual, isEqual(expected)); + } + + private void awaitA(String expectedValue, String expectedInner, A actual, String message) { + awaitEquals(expectedValue, actual::getValue, message + " value"); + awaitEquals(expectedInner, actual.getInner()::getInnerValue, message + " inner"); + } + + @Override + protected void closeConnections() { + if (handler != null) { + handler.close(); + } + if (model != null) { + model.ragconnectCloseConnections(); + } + } + + private static class ReceiverData { + int numberOfValues = 0; + } +} diff --git a/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/ForwardingTest.java b/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/ForwardingTest.java index 64430eb40800d7bea28e38e63d0cb73728f5856f..570c4d04742d78501f2a206aa4149d1d79d3b9f5 100644 --- a/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/ForwardingTest.java +++ b/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/ForwardingTest.java @@ -1,8 +1,6 @@ package org.jastadd.ragconnect.tests; import forwardingInc.ast.*; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; import org.assertj.core.groups.Tuple; import org.junit.jupiter.api.Tag; @@ -14,9 +12,7 @@ import java.util.concurrent.atomic.AtomicBoolean; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.groups.Tuple.tuple; -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.jastadd.ragconnect.tests.TestUtils.*; import static org.junit.jupiter.api.Assertions.*; /** @@ -183,7 +179,7 @@ public class ForwardingTest extends AbstractMqttTest { private void waitForValue() { if (isWriteCurrentValue()) { - await().until(() -> data.valueSentSinceLastCheck.getAndSet(false)); + awaitMqtt().until(() -> data.valueSentSinceLastCheck.getAndSet(false)); } } @@ -524,9 +520,7 @@ public class ForwardingTest extends AbstractMqttTest { if (model != null) { model.ragconnectCloseConnections(); } - if (observer != null) { - observer.init(); - } + observer.init(); } static class Values<T> { 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 a4575feafc716fe6dbd95f43f2b2212436a843d0..75d7d16b18c64f7b5fd39c71acba81f92223a2af 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 @@ -11,9 +11,7 @@ import java.util.concurrent.TimeUnit; import java.util.function.Predicate; import static org.assertj.core.groups.Tuple.tuple; -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.jastadd.ragconnect.tests.TestUtils.*; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -99,7 +97,7 @@ public class IndexedSendTest extends AbstractMqttTest { private void waitForValue(Callable<Integer> callable, int expectedValue) { if (isWriteCurrentValue()) { - await().until(callable, Predicate.isEqual(expectedValue)); + awaitMqtt().until(callable, Predicate.isEqual(expectedValue)); } } 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 937c930d4b4a9e75003dbf74cdc42f12bc85ca0e..9a6c22bd962940b5d0dc0e942dcc91a3ca1fbf19 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 @@ -5,6 +5,8 @@ import com.fasterxml.jackson.core.JsonFactory; import com.fasterxml.jackson.core.JsonGenerator; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.awaitility.Awaitility; +import org.awaitility.core.ConditionFactory; import org.jastadd.ragconnect.compiler.Compiler; import org.junit.jupiter.api.Assertions; @@ -24,7 +26,6 @@ import java.util.stream.Collectors; import static java.util.Collections.addAll; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.util.Lists.newArrayList; -import static org.awaitility.Awaitility.await; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; @@ -145,6 +146,10 @@ public class TestUtils { TimeUnit.MILLISECONDS.sleep(1500); } + public static ConditionFactory awaitMqtt() { + return Awaitility.await().atMost(1500, TimeUnit.MILLISECONDS); + } + static <T_Event, T_ASTNode> void logEvent(T_Event event, T_ASTNode node, String attribute, Object params, Object value) { logger.info("event: {}, node: {}, attribute: {}, params: {}, value: {}", event, node, attribute, params, value); @@ -388,7 +393,7 @@ public class TestUtils { } void awaitChange() { - await().until(hasChanged); + awaitMqtt().until(hasChanged); updatePrevious(); } diff --git a/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/singleList/AbstractSingleListTest.java b/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/singleList/AbstractSingleListTest.java index 832ba76d7df935164b7ebd5ac90af1bbb3e854e9..cdcb41c72e1af5b9b53f9afba3fd78eefbe06bd6 100644 --- a/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/singleList/AbstractSingleListTest.java +++ b/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/singleList/AbstractSingleListTest.java @@ -14,10 +14,8 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Function; -import static org.awaitility.Awaitility.await; +import static org.jastadd.ragconnect.tests.TestUtils.*; import static org.jastadd.ragconnect.tests.TestUtils.IntList.list; -import static org.jastadd.ragconnect.tests.TestUtils.mqttUri; -import static org.jastadd.ragconnect.tests.TestUtils.testJaddContainReferenceToJackson; import static org.junit.jupiter.api.Assertions.*; /** @@ -167,7 +165,7 @@ public abstract class AbstractSingleListTest extends AbstractMqttTest { private void waitForValue() { if (isWriteCurrentValue()) { - await().until(() -> data.valueSentSinceLastCheck.getAndSet(false)); + awaitMqtt().until(() -> data.valueSentSinceLastCheck.getAndSet(false)); } }