Skip to content

Commit

Permalink
[#167] Added notifications tests
Browse files Browse the repository at this point in the history
  • Loading branch information
susanw1 committed Sep 30, 2024
1 parent 7af7c2f commit 808740e
Show file tree
Hide file tree
Showing 14 changed files with 210 additions and 35 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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);
}

}
Original file line number Diff line number Diff line change
@@ -1,14 +1,51 @@
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 {
@Nonnull
public abstract <T extends ZscriptResponse> NotificationSection<T> getSection(NotificationSectionId<T> response);

@Nonnull
@Deprecated
public abstract List<NotificationSection<?>> getSections();

/**
* Creates a list of the right types of notification section content
*
* @param responsePath
* @return
*/
public List<ZscriptResponse> buildNotificationContent(final ResponseExecutionPath responsePath) {
final List<ZscriptResponse> actualResponses = new ArrayList<>();

final Iterator<NotificationSection<?>> sectionIt = getSections().iterator();
final Iterator<Response> 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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.*;
Expand Down Expand Up @@ -192,5 +194,6 @@ public final class {{#upperCamel}}{{moduleName}}{{/upperCamel}}Module {
{{! ============ NOTIFICATION PROCESSING ============= }}

// +++++++++++++ NOTIFICATIONS +++++++++++++

{{>notifications.mustache}}
}
Original file line number Diff line number Diff line change
@@ -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();

Expand All @@ -16,6 +17,7 @@
}

private {{#upperCamel}}{{name}}{{/upperCamel}}NotificationSectionId() {
// prevent instantiation
}
}

Expand All @@ -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() {
Expand Down Expand Up @@ -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<NotificationSectionId<?>, NotificationSection<?>> sections = new LinkedHashMap<>();
Expand All @@ -105,4 +117,4 @@
}
}

{{/notifications}}
{{/notifications}}
Original file line number Diff line number Diff line change
@@ -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<NotificationSection<?>> sectionsA = handleA.getSections();
assertThat(sectionsA).hasSize(1);
assertThat(sectionsA).hasExactlyElementsOfTypes(TestingModule.Expr1NotificationSection.class);

TestingModule.TestNtfBNotificationHandle handleB = TestingModule.TestNtfBNotificationId.get().newHandle();
List<NotificationSection<?>> 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<ZscriptResponse> 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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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: &section_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
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ public Optional<ZscriptResponse> getResponseFor(NotificationSection<?> node) {

@Nonnull
public <T extends ZscriptResponse> Optional<T> getResponseFor(ResponseCaptor<T> captor) {
// FIXME: ensure source can't be null!
return getResponseFor((NotificationSection<T>) captor.getSource()).map(r -> ((NotificationSection<T>) captor.getSource()).getResponseType().cast(r));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -164,7 +165,9 @@ public Optional<ZscriptResponse> getResponseFor(ZscriptCommandNode<?> node) {
}

public <T extends ZscriptResponse> Optional<T> getResponseFor(ResponseCaptor<T> captor) {
return getResponseFor((ZscriptCommandNode<T>) captor.getSource()).map(r -> ((ZscriptCommandNode<T>) captor.getSource()).getResponseType().cast(r));
// FIXME: ensure source can't be null!
final Respondable<T> source = captor.getSource();
return getResponseFor((ZscriptCommandNode<T>) source).map(r -> ((ZscriptCommandNode<T>) source).getResponseType().cast(r));
}

public boolean wasExecuted() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -281,8 +281,9 @@ private int parsePin(Device device, String name, String verb, Predicate<DigitalS
throws InterruptedException {
ResponseCaptor<PinsModule.CapabilitiesCommand.CapabilitiesResponse> 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<Integer> pinsSupporting = new HashSet<>();
// assemble a big command sequence to check pins in batches...
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@ public void startReceiving() {
try {
lastTimeNano = System.nanoTime();
ResponseCaptor<PinsModule.DigitalNotificationSectionContent> 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();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import javax.annotation.processing.Generated;
* <dl>
* <dt>00</dt><dd>Success</dd>
* <dt>01-0f</dt><dd>Failure (allows ORELSE logic)</dd>
* <dt>0f-ff</dt><dd>Error (unexpected, or not immediately recoverable)</dd>
* <dt>10-ff</dt><dd>Error (unexpected, or not immediately recoverable)</dd>
* </dl>
*/
@Generated(value = "JavaZscriptStatus.mustache",
Expand Down

0 comments on commit 808740e

Please sign in to comment.