From 808740eb46252ff294e6c2ec3e4aff06dbbc1abc Mon Sep 17 00:00:00 2001 From: susanw1 Date: Mon, 30 Sep 2024 14:35:52 +0100 Subject: [PATCH] [#167] Added notifications tests --- .../ZscriptClientException.java | 24 +++++-- .../ZscriptFieldOutOfRangeException.java | 8 +-- .../ZscriptMissingFieldException.java | 9 +-- .../notifications/NotificationHandle.java | 37 ++++++++++ .../commands/JavaCommandBuilder.mustache | 3 + .../templates/commands/notifications.mustache | 32 ++++++--- .../JavaCommandBuilderNotificationTest.java | 67 +++++++++++++++++++ .../mod/test-module.yaml | 46 ++++++++++++- .../javaclient/commandPaths/Response.java | 2 +- .../devices/NotificationSequenceCallback.java | 1 + .../devices/ResponseSequenceCallback.java | 5 +- .../net/zscript/demo/morse/MorseFullCli.java | 5 +- .../net/zscript/demo/morse/MorseReceiver.java | 4 +- .../main/templates/JavaZscriptStatus.mustache | 2 +- 14 files changed, 210 insertions(+), 35 deletions(-) create mode 100644 clients/java-client-lib/client-command-builders/src/test/java/net/zscript/model/modules/testing/test/JavaCommandBuilderNotificationTest.java diff --git a/clients/java-client-lib/client-command-api/src/main/java/net/zscript/javaclient/commandbuilder/ZscriptClientException.java b/clients/java-client-lib/client-command-api/src/main/java/net/zscript/javaclient/commandbuilder/ZscriptClientException.java index 04b8470c0..ddee65e10 100644 --- a/clients/java-client-lib/client-command-api/src/main/java/net/zscript/javaclient/commandbuilder/ZscriptClientException.java +++ b/clients/java-client-lib/client-command-api/src/main/java/net/zscript/javaclient/commandbuilder/ZscriptClientException.java @@ -3,15 +3,25 @@ import static java.lang.String.format; public class ZscriptClientException extends RuntimeException { + /** + * Creates a generic zscript client exception, allowing {@link String#format(String, Object...)}-style format strings with params. + * + * @param format a format string with optional standard '%s'-style format elements + * @param params the params, matching the format specifiers in the format string + */ public ZscriptClientException(String format, Object... params) { super(format(format, params)); } - - public ZscriptClientException(String msg, Exception e) { - super(msg, e); - } - - public ZscriptClientException(Exception e, String format, Object... params) { - super(format(format, params), e); + + /** + * Creates a generic zscript client exception, allowing a chained "cause" exception plus {@link String#format(String, Object...)}-style format strings with params. Annoyingly, + * as the varargs param list has to be the last argument, the 'cause' is a bit stuck in the middle. + * + * @param format a format string with optional standard '%s'-style format elements + * @param cause the upstream exception which caused this exception + * @param params the params, matching the format specifiers in the format string + */ + public ZscriptClientException(String format, Exception cause, Object... params) { + super(format(format, params), cause); } } diff --git a/clients/java-client-lib/client-command-api/src/main/java/net/zscript/javaclient/commandbuilder/ZscriptFieldOutOfRangeException.java b/clients/java-client-lib/client-command-api/src/main/java/net/zscript/javaclient/commandbuilder/ZscriptFieldOutOfRangeException.java index f5041705f..550214161 100644 --- a/clients/java-client-lib/client-command-api/src/main/java/net/zscript/javaclient/commandbuilder/ZscriptFieldOutOfRangeException.java +++ b/clients/java-client-lib/client-command-api/src/main/java/net/zscript/javaclient/commandbuilder/ZscriptFieldOutOfRangeException.java @@ -1,14 +1,12 @@ package net.zscript.javaclient.commandbuilder; -import static java.lang.String.format; - public class ZscriptFieldOutOfRangeException extends ZscriptClientException { public ZscriptFieldOutOfRangeException(String format, Object... params) { - super(format(format, params)); + super(format, params); } - public ZscriptFieldOutOfRangeException(String msg, Exception e) { - super(msg, e); + public ZscriptFieldOutOfRangeException(String format, Exception e, Object... params) { + super(format, e, params); } } diff --git a/clients/java-client-lib/client-command-api/src/main/java/net/zscript/javaclient/commandbuilder/ZscriptMissingFieldException.java b/clients/java-client-lib/client-command-api/src/main/java/net/zscript/javaclient/commandbuilder/ZscriptMissingFieldException.java index 13a39fb3e..cb2a7689e 100644 --- a/clients/java-client-lib/client-command-api/src/main/java/net/zscript/javaclient/commandbuilder/ZscriptMissingFieldException.java +++ b/clients/java-client-lib/client-command-api/src/main/java/net/zscript/javaclient/commandbuilder/ZscriptMissingFieldException.java @@ -1,15 +1,12 @@ package net.zscript.javaclient.commandbuilder; -import static java.lang.String.format; - public class ZscriptMissingFieldException extends ZscriptClientException { public ZscriptMissingFieldException(String format, Object... params) { - super(format(format, params)); + super(format, params); } - public ZscriptMissingFieldException(String msg, Exception e) { - super(msg, e); + public ZscriptMissingFieldException(String format, Exception cause, Object... params) { + super(format, cause, params); } - } diff --git a/clients/java-client-lib/client-command-api/src/main/java/net/zscript/javaclient/commandbuilder/notifications/NotificationHandle.java b/clients/java-client-lib/client-command-api/src/main/java/net/zscript/javaclient/commandbuilder/notifications/NotificationHandle.java index 3a4d01846..f96b36acc 100644 --- a/clients/java-client-lib/client-command-api/src/main/java/net/zscript/javaclient/commandbuilder/notifications/NotificationHandle.java +++ b/clients/java-client-lib/client-command-api/src/main/java/net/zscript/javaclient/commandbuilder/notifications/NotificationHandle.java @@ -1,8 +1,13 @@ package net.zscript.javaclient.commandbuilder.notifications; import javax.annotation.Nonnull; +import java.util.ArrayList; +import java.util.Iterator; import java.util.List; +import net.zscript.javaclient.commandPaths.Response; +import net.zscript.javaclient.commandPaths.ResponseExecutionPath; +import net.zscript.javaclient.commandbuilder.ZscriptClientException; import net.zscript.javaclient.commandbuilder.ZscriptResponse; public abstract class NotificationHandle { @@ -10,5 +15,37 @@ public abstract class NotificationHandle { public abstract NotificationSection getSection(NotificationSectionId response); @Nonnull + @Deprecated public abstract List> getSections(); + + /** + * Creates a list of the right types of notification section content + * + * @param responsePath + * @return + */ + public List buildNotificationContent(final ResponseExecutionPath responsePath) { + final List actualResponses = new ArrayList<>(); + + final Iterator> sectionIt = getSections().iterator(); + final Iterator respIt = responsePath.iterator(); + + for (int i = 0; sectionIt.hasNext() && respIt.hasNext(); i++) { + final NotificationSection section = sectionIt.next(); + final Response response = respIt.next(); + final ZscriptResponse content = section.parseResponse(response.getFields()); + if (!content.isValid()) { + throw new ZscriptClientException("Invalid notification section [ntf=%s, section#=%s, section=%s, text=%s]", this, i, section, response); + } + + actualResponses.add(content); + } + if (sectionIt.hasNext()) { + throw new IllegalStateException("Notification needs more response sections"); + } + if (respIt.hasNext()) { + throw new IllegalStateException("Too many response sections received for notification"); + } + return actualResponses; + } } diff --git a/clients/java-client-lib/client-command-builders/src/main/resources/templates/commands/JavaCommandBuilder.mustache b/clients/java-client-lib/client-command-builders/src/main/resources/templates/commands/JavaCommandBuilder.mustache index e5d044d25..c790ebda8 100644 --- a/clients/java-client-lib/client-command-builders/src/main/resources/templates/commands/JavaCommandBuilder.mustache +++ b/clients/java-client-lib/client-command-builders/src/main/resources/templates/commands/JavaCommandBuilder.mustache @@ -9,6 +9,8 @@ import javax.annotation.processing.Generated; import static net.zscript.javaclient.commandbuilder.Utils.*; +import net.zscript.javaclient.commandPaths.*; +import net.zscript.javaclient.commandbuilder.commandnodes.*; import net.zscript.javaclient.commandbuilder.commandnodes.*; import net.zscript.javaclient.commandbuilder.defaultCommands.*; import net.zscript.javaclient.commandbuilder.*; @@ -192,5 +194,6 @@ public final class {{#upperCamel}}{{moduleName}}{{/upperCamel}}Module { {{! ============ NOTIFICATION PROCESSING ============= }} // +++++++++++++ NOTIFICATIONS +++++++++++++ + {{>notifications.mustache}} } diff --git a/clients/java-client-lib/client-command-builders/src/main/resources/templates/commands/notifications.mustache b/clients/java-client-lib/client-command-builders/src/main/resources/templates/commands/notifications.mustache index fccad572b..b520b9bc2 100644 --- a/clients/java-client-lib/client-command-builders/src/main/resources/templates/commands/notifications.mustache +++ b/clients/java-client-lib/client-command-builders/src/main/resources/templates/commands/notifications.mustache @@ -1,7 +1,8 @@ -// Individual Notification Sections +// Individual Notification Sections, to be grouped into actual notifications. Processed separately from the actual notifications, because many Sections are +// replicated identically across multiple notifications, and would otherwise create pointless duplicates. {{#notificationSections}} - /** {{description}} */ + /** Singleton identifier for specific notification section: {{description}} */ public static final class {{#upperCamel}}{{name}}{{/upperCamel}}NotificationSectionId extends NotificationSectionId<{{#upperCamel}}{{name}}{{/upperCamel}}NotificationSectionContent> { private static final {{#upperCamel}}{{name}}{{/upperCamel}}NotificationSectionId ID = new {{#upperCamel}}{{name}}{{/upperCamel}}NotificationSectionId(); @@ -16,6 +17,7 @@ } private {{#upperCamel}}{{name}}{{/upperCamel}}NotificationSectionId() { + // prevent instantiation } } @@ -34,24 +36,33 @@ } } + /** Response wrapper for this section of the Notification, with accessors for the fields. */ public static final class {{#upperCamel}}{{name}}{{/upperCamel}}NotificationSectionContent extends ValidatingResponse { - public {{#upperCamel}}{{name}}NotificationSectionContent{{/upperCamel}}(ZscriptExpression response) { - super(response, new byte[] { {{#responseFields}}{{#required}}(byte) '{{key}}', {{/required}}{{/responseFields}} }); + /** Constructs the notification object, representing the supplied Zscript response expression. */ + public {{#upperCamel}}{{name}}NotificationSectionContent{{/upperCamel}}(@Nonnull ZscriptExpression response) { + super(response, new byte[] { {{#fields}}{{#required}}(byte) '{{key}}', {{/required}}{{/fields}} }); } + + // Notification section field accessors + {{#fields}} - {{>responseField.mustache}} + {{>responseField.mustache}} {{/fields}} } - {{/notificationSections}} +{{/notificationSections}} - {{#notifications}} - /** {{description}} */ +// Notification-level classes. + +{{#notifications}} + /** Singleton identifier for the {{notificationName}} notification: {{description}} */ public static final class {{#upperCamel}}{{notificationName}}{{/upperCamel}}NotificationId extends NotificationId<{{#upperCamel}}{{notificationName}}{{/upperCamel}}NotificationHandle> { public static final {{#upperCamel}}{{moduleName}}{{/upperCamel}}Notifications NTFN = {{#upperCamel}}{{moduleName}}{{/upperCamel}}Notifications.{{#upperCamel}}{{notificationName}}{{/upperCamel}}; private static final {{#upperCamel}}{{name}}{{/upperCamel}}NotificationId ID = new {{#upperCamel}}{{name}}{{/upperCamel}}NotificationId(); - private {{#upperCamel}}{{name}}{{/upperCamel}}NotificationId() {} + private {{#upperCamel}}{{name}}{{/upperCamel}}NotificationId() { + // prevent instantiation + } @Nonnull public static {{#upperCamel}}{{notificationName}}{{/upperCamel}}NotificationId {{#lowerCamel}}{{notificationName}}{{/lowerCamel}}NotificationId() { @@ -81,6 +92,7 @@ } } + /** Handle for {{#upperCamel}}{{notificationName}}{{/upperCamel}} Notifications, referencing the sections within. */ public static final class {{#upperCamel}}{{notificationName}}{{/upperCamel}}NotificationHandle extends NotificationHandle { private final LinkedHashMap, NotificationSection> sections = new LinkedHashMap<>(); @@ -105,4 +117,4 @@ } } - {{/notifications}} +{{/notifications}} diff --git a/clients/java-client-lib/client-command-builders/src/test/java/net/zscript/model/modules/testing/test/JavaCommandBuilderNotificationTest.java b/clients/java-client-lib/client-command-builders/src/test/java/net/zscript/model/modules/testing/test/JavaCommandBuilderNotificationTest.java new file mode 100644 index 000000000..8ec250059 --- /dev/null +++ b/clients/java-client-lib/client-command-builders/src/test/java/net/zscript/model/modules/testing/test/JavaCommandBuilderNotificationTest.java @@ -0,0 +1,67 @@ +package net.zscript.model.modules.testing.test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +import net.zscript.client.modules.test.testing.TestingModule; +import net.zscript.javaclient.addressing.CompleteAddressedResponse; +import net.zscript.javaclient.commandbuilder.ZscriptResponse; +import net.zscript.javaclient.commandbuilder.notifications.NotificationSection; +import net.zscript.javaclient.tokens.ExtendingTokenBuffer; +import net.zscript.tokenizer.TokenBuffer.TokenReader; +import net.zscript.tokenizer.Tokenizer; + +public class JavaCommandBuilderNotificationTest { + final ExtendingTokenBuffer buffer = new ExtendingTokenBuffer(); + final Tokenizer tokenizer = new Tokenizer(buffer.getTokenWriter(), 2); + final TokenReader tokenReader = buffer.getTokenReader(); + + @Test + void shouldDefineNotificationClasses() { + TestingModule.TestNtfANotificationId ntfIdA = TestingModule.TestNtfANotificationId.get(); + assertThat(ntfIdA.getHandleType()).isEqualTo(TestingModule.TestNtfANotificationHandle.class); + + TestingModule.TestNtfANotificationHandle handleA = ntfIdA.newHandle(); + List> sectionsA = handleA.getSections(); + assertThat(sectionsA).hasSize(1); + assertThat(sectionsA).hasExactlyElementsOfTypes(TestingModule.Expr1NotificationSection.class); + + TestingModule.TestNtfBNotificationHandle handleB = TestingModule.TestNtfBNotificationId.get().newHandle(); + List> sectionsB = handleB.getSections(); + assertThat(sectionsB).hasSize(2); + assertThat(sectionsB).hasExactlyElementsOfTypes(TestingModule.Expr1NotificationSection.class, TestingModule.Expr2NotificationSection.class); + + assertThat(sectionsB.get(0).getResponseType()).isEqualTo(TestingModule.Expr1NotificationSectionContent.class); + } + + @Test + void shouldCreateNotificationWithRequiredFields() { + "!234 Dab Lcd & Xef\n".chars().forEach(c -> tokenizer.accept((byte) c)); + + final TestingModule.TestNtfBNotificationHandle handle = TestingModule.TestNtfBNotificationId.get().newHandle(); + + final CompleteAddressedResponse car = CompleteAddressedResponse.parse(buffer.getTokenReader()); + assertThat(car.asResponse().hasAddress()).isFalse(); + assertThat(car.getContent().getResponseValue()).isEqualTo(0x234); + + final List sections = handle.buildNotificationContent(car.getContent().getExecutionPath()); + assertThat(sections) + .hasExactlyElementsOfTypes(TestingModule.Expr1NotificationSectionContent.class, TestingModule.Expr2NotificationSectionContent.class); + + final TestingModule.Expr1NotificationSectionContent sec0 = (TestingModule.Expr1NotificationSectionContent) sections.get(0); + final TestingModule.Expr2NotificationSectionContent sec1 = (TestingModule.Expr2NotificationSectionContent) sections.get(1); + + assertThat(sec0.getTestNtfARespDField1()).isEqualTo(0xab); + assertThat(sec0.getTestNtfARespLField2()).isEqualTo(0xcd); + + assertThat(sec1.getTestNtfBRespXField1()).isEqualTo(0xef); + assertThat(sec1.getTestNtfBRespYField2AsString()).isEqualTo(""); + + assertThat(sec0.getField((byte) 'D')).as("field 'D'").hasValue(0xab); + assertThat(sec0.getField((byte) 'L')).as("field 'L'").hasValue(0xcd); + assertThat(sec1.getField((byte) 'X')).as("field 'X'").hasValue(0xef); + } +} diff --git a/clients/java-client-lib/client-command-builders/src/test/resources/zscript-test-datamodel/mod/test-module.yaml b/clients/java-client-lib/client-command-builders/src/test/resources/zscript-test-datamodel/mod/test-module.yaml index 03cf34505..434022b48 100644 --- a/clients/java-client-lib/client-command-builders/src/test/resources/zscript-test-datamodel/mod/test-module.yaml +++ b/clients/java-client-lib/client-command-builders/src/test/resources/zscript-test-datamodel/mod/test-module.yaml @@ -200,4 +200,48 @@ commands: '@type': bytes required: no -notifications: [ ] +notifications: +- name: testNtfA + notification: 0 + description: looks like a real notification, for testing + condition: something happens + sections: + - section: §ion_first + name: expr1 + description: first expression + fields: + - key: D + name: testNtfARespDField1 + description: tomato + typeDefinition: + '@type': number + required: yes + - key: L + name: testNtfARespLField2 + description: radish + typeDefinition: + '@type': number + required: yes + +- name: testNtfB + notification: 1 + description: looks like another real notification, for testing + condition: something else happens + sections: + - section: *section_first + - section: + name: expr2 + description: second expression + fields: + - key: X + name: testNtfBRespXField1 + description: lettuce + typeDefinition: + '@type': number + required: yes + - key: Y + name: testNtfBRespYField2 + description: kale + typeDefinition: + '@type': text + required: no diff --git a/clients/java-client-lib/client-core/src/main/java/net/zscript/javaclient/commandPaths/Response.java b/clients/java-client-lib/client-core/src/main/java/net/zscript/javaclient/commandPaths/Response.java index 80a540069..099312d3f 100644 --- a/clients/java-client-lib/client-core/src/main/java/net/zscript/javaclient/commandPaths/Response.java +++ b/clients/java-client-lib/client-core/src/main/java/net/zscript/javaclient/commandPaths/Response.java @@ -6,7 +6,7 @@ import net.zscript.util.ByteString.ByteAppendable; import net.zscript.util.ByteString.ByteStringBuilder; -public class Response implements ByteAppendable { +public final class Response implements ByteAppendable { private final ZscriptFieldSet fieldSet; private final Response next; diff --git a/clients/java-client-lib/client-main/src/main/java/net/zscript/javaclient/devices/NotificationSequenceCallback.java b/clients/java-client-lib/client-main/src/main/java/net/zscript/javaclient/devices/NotificationSequenceCallback.java index 41a6c71f0..f1ffd99b6 100644 --- a/clients/java-client-lib/client-main/src/main/java/net/zscript/javaclient/devices/NotificationSequenceCallback.java +++ b/clients/java-client-lib/client-main/src/main/java/net/zscript/javaclient/devices/NotificationSequenceCallback.java @@ -95,6 +95,7 @@ public Optional getResponseFor(NotificationSection node) { @Nonnull public Optional getResponseFor(ResponseCaptor captor) { + // FIXME: ensure source can't be null! return getResponseFor((NotificationSection) captor.getSource()).map(r -> ((NotificationSection) captor.getSource()).getResponseType().cast(r)); } } diff --git a/clients/java-client-lib/client-main/src/main/java/net/zscript/javaclient/devices/ResponseSequenceCallback.java b/clients/java-client-lib/client-main/src/main/java/net/zscript/javaclient/devices/ResponseSequenceCallback.java index 4f3597911..e19ccb066 100644 --- a/clients/java-client-lib/client-main/src/main/java/net/zscript/javaclient/devices/ResponseSequenceCallback.java +++ b/clients/java-client-lib/client-main/src/main/java/net/zscript/javaclient/devices/ResponseSequenceCallback.java @@ -17,6 +17,7 @@ import net.zscript.javaclient.commandPaths.Command; import net.zscript.javaclient.commandPaths.MatchedCommandResponse; import net.zscript.javaclient.commandPaths.Response; +import net.zscript.javaclient.commandbuilder.Respondable; import net.zscript.javaclient.commandbuilder.ZscriptResponse; import net.zscript.javaclient.commandbuilder.commandnodes.ResponseCaptor; import net.zscript.javaclient.commandbuilder.commandnodes.ZscriptCommandNode; @@ -164,7 +165,9 @@ public Optional getResponseFor(ZscriptCommandNode node) { } public Optional getResponseFor(ResponseCaptor captor) { - return getResponseFor((ZscriptCommandNode) captor.getSource()).map(r -> ((ZscriptCommandNode) captor.getSource()).getResponseType().cast(r)); + // FIXME: ensure source can't be null! + final Respondable source = captor.getSource(); + return getResponseFor((ZscriptCommandNode) source).map(r -> ((ZscriptCommandNode) source).getResponseType().cast(r)); } public boolean wasExecuted() { diff --git a/demo/demo-morse/client/src/main/java/net/zscript/demo/morse/MorseFullCli.java b/demo/demo-morse/client/src/main/java/net/zscript/demo/morse/MorseFullCli.java index 8182ef16c..3605462da 100644 --- a/demo/demo-morse/client/src/main/java/net/zscript/demo/morse/MorseFullCli.java +++ b/demo/demo-morse/client/src/main/java/net/zscript/demo/morse/MorseFullCli.java @@ -281,8 +281,9 @@ private int parsePin(Device device, String name, String verb, Predicate captor = ResponseCaptor.create(); - int pinCount = device.sendAndWaitExpectSuccess( - PinsModule.capabilitiesBuilder().capture(captor).build()).getResponseFor(captor).orElseThrow().getPinCount(); + int pinCount = device.sendAndWaitExpectSuccess(PinsModule.capabilitiesBuilder().capture(captor).build()) + .getResponseFor(captor) + .orElseThrow().getPinCount(); System.out.println(name + " has " + pinCount + " pins"); Set pinsSupporting = new HashSet<>(); // assemble a big command sequence to check pins in batches... diff --git a/demo/demo-morse/client/src/main/java/net/zscript/demo/morse/MorseReceiver.java b/demo/demo-morse/client/src/main/java/net/zscript/demo/morse/MorseReceiver.java index 94cb0dd33..d147fd5a6 100644 --- a/demo/demo-morse/client/src/main/java/net/zscript/demo/morse/MorseReceiver.java +++ b/demo/demo-morse/client/src/main/java/net/zscript/demo/morse/MorseReceiver.java @@ -40,7 +40,9 @@ public void startReceiving() { try { lastTimeNano = System.nanoTime(); ResponseCaptor captor = ResponseCaptor.create(); - device.getNotificationHandle(PinsModule.DigitalNotificationId.get()).getSection(PinsModule.DigitalNotificationSectionId.get()).setCaptor(captor); + device.getNotificationHandle(PinsModule.DigitalNotificationId.get()) + .getSection(PinsModule.DigitalNotificationSectionId.get()) + .setCaptor(captor); device.setNotificationListener(PinsModule.DigitalNotificationId.get(), notificationSequenceCallback -> { PinsModule.DigitalNotificationSectionContent content = notificationSequenceCallback.getResponseFor(captor).get(); diff --git a/receivers/jvm/java-model-components/src/main/templates/JavaZscriptStatus.mustache b/receivers/jvm/java-model-components/src/main/templates/JavaZscriptStatus.mustache index 36edd832f..98e7c5e26 100644 --- a/receivers/jvm/java-model-components/src/main/templates/JavaZscriptStatus.mustache +++ b/receivers/jvm/java-model-components/src/main/templates/JavaZscriptStatus.mustache @@ -13,7 +13,7 @@ import javax.annotation.processing.Generated; *
*
00
Success
*
01-0f
Failure (allows ORELSE logic)
- *
0f-ff
Error (unexpected, or not immediately recoverable)
+ *
10-ff
Error (unexpected, or not immediately recoverable)
*
*/ @Generated(value = "JavaZscriptStatus.mustache",