package com.ruoyi.ft546.protocol; import org.junit.jupiter.api.Test; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import static com.ruoyi.ft546.protocol.Ft546RegisterWrite.DataType.INT16; import static com.ruoyi.ft546.protocol.Ft546RegisterWrite.DataType.INT32; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; class Ft546SrgvCommandEncoderTest { private final Ft546SrgvCommandEncoder encoder = new Ft546SrgvCommandEncoder(); @Test void ordinaryIntentEncodesTheExactLegacySequence() { Ft546EncodedCommand encoded = encoder.encode( intent((short) 1, points(70501, 70502), null, null), fullProfile(), capabilities("POINT_TO_POINT_COMMAND")); assertWrites(encoded, write(1, "4004", INT32, false, 70501, 70502, 0, 0, 0, 0, 0, 0), write(2, "4000", INT32, false, 30001), write(3, "4002", INT16, false, 1), write(4, "4021", INT16, true, 1)); assertEquals("1", encoded.getDeviceId()); assertEquals("PARENT-1", encoded.getParentTaskId()); assertEquals("LOADED_TO_PLACE", encoded.getPhase()); assertEquals("KEY-1", encoded.getCommandKey()); assertEquals(3L, encoded.getRouteVersion()); assertEquals(30001, encoded.getTaskNo()); assertEquals((short) 1, encoded.getTaskType()); assertEquals(points(70501, 70502), encoded.getPathPoints()); assertEquals(1, encoded.getTriggerValue()); } @Test void crossFloorIntentEncodesTheExactLegacyPostTriggerSequence() { Ft546EncodedCommand encoded = encoder.encode( intent((short) 8, points(70501, 70502), Short.valueOf((short) 8), null), fullProfile(), capabilities("POINT_TO_POINT_COMMAND")); assertWrites(encoded, write(1, "4004", INT32, false, 70501, 70502, 0, 0, 0, 0, 0, 0), write(2, "4000", INT32, false, 30001), write(3, "4002", INT16, false, 8), write(4, "4021", INT16, true, 1), write(5, "4020", INT16, false, 1), write(6, "4022", INT16, false, 8)); } @Test void twoStageOneEncodesTheExactLegacySequence() { Ft546EncodedCommand encoded = encoder.encode( intent((short) 1, points(70501, 70503), null, 1), fullProfile(), capabilities("TWO_STAGE_ATOMIC_TASK")); assertWrites(encoded, write(1, "4404", INT32, false, 70501, 70503, 0, 0, 0, 0, 0, 0), write(2, "4422", INT16, true, 1), write(3, "4402", INT16, false, 1), write(4, "4400", INT32, false, 30001)); assertEquals(1, encoded.getTriggerValue()); } @Test void twoStageTwoEncodesTheExactLegacySequence() { Ft546EncodedCommand encoded = encoder.encode( intent((short) 9, points(70503, 70504), null, 2), fullProfile(), capabilities("TWO_STAGE_ATOMIC_TASK")); assertWrites(encoded, write(1, "4404", INT32, false, 70503, 70504, 0, 0, 0, 0, 0, 0), write(2, "4422", INT16, true, 2), write(3, "4423", INT16, false, 99), write(4, "4402", INT16, false, 9), write(5, "4400", INT32, false, 30001)); assertEquals(2, encoded.getTriggerValue()); } @Test void missingRegisterFailsClosedBeforeEncoding() { Ft546DeviceProfile profile = fullProfile(); Map registers = new LinkedHashMap( profile.getRegisters()); registers.remove("trigger"); profile.setRegisters(registers); IllegalArgumentException failure = assertThrows( IllegalArgumentException.class, () -> encoder.encode(intent((short) 1, points(70501, 70502), null, null), profile, capabilities("POINT_TO_POINT_COMMAND"))); assertEquals("profile register is missing: trigger", failure.getMessage()); } @Test void routeLengthTwoAndEightAreAcceptedAndPaddedExactly() { Ft546EncodedCommand two = encodeRoute(points(1, 2)); Ft546EncodedCommand eight = encodeRoute( points(1, 2, 3, 4, 5, 6, 7, 8)); assertEquals(points(1, 2, 0, 0, 0, 0, 0, 0), two.getWrites().get(0).getValues()); assertEquals(points(1, 2, 3, 4, 5, 6, 7, 8), eight.getWrites().get(0).getValues()); } @Test void routeLengthOneAndNineAreRejected() { assertRouteRejected(points(1)); assertRouteRejected(points(1, 2, 3, 4, 5, 6, 7, 8, 9)); } @Test void routePointBeyondSixDigitsIsRejected() { IllegalArgumentException failure = assertThrows( IllegalArgumentException.class, () -> encodeRoute(points(1, 1000000))); assertEquals("route node exceeds six digits", failure.getMessage()); } @Test void capabilityGatesMainAndTwoStageIndependently() { IllegalStateException main = assertThrows(IllegalStateException.class, () -> encoder.encode(intent((short) 1, points(1, 2), null, null), fullProfile(), Collections.emptySet())); IllegalStateException stage = assertThrows(IllegalStateException.class, () -> encoder.encode(intent((short) 1, points(1, 2), null, 1), fullProfile(), capabilities( "POINT_TO_POINT_COMMAND"))); assertEquals("POINT_TO_POINT_COMMAND is not enabled", main.getMessage()); assertEquals("TWO_STAGE_ATOMIC_TASK is not enabled", stage.getMessage()); } @Test void profileIntentAndEncodedCollectionsAreImmutableSnapshots() { Ft546DeviceProfile profile = fullProfile(); List mutablePoints = new ArrayList( points(70501, 70502)); Ft546SrgvCommandIntent intent = intent((short) 1, mutablePoints, null, null); mutablePoints.set(0, 99999); Ft546EncodedCommand encoded = encoder.encode(intent, profile, capabilities("POINT_TO_POINT_COMMAND")); assertThrows(UnsupportedOperationException.class, () -> profile.getRegisters().put("path", "9999")); assertThrows(UnsupportedOperationException.class, () -> profile.getCapabilities().add("UNSAFE")); assertThrows(UnsupportedOperationException.class, () -> intent.getPathPoints().add(3)); assertThrows(UnsupportedOperationException.class, () -> encoded.getWrites().clear()); assertThrows(UnsupportedOperationException.class, () -> encoded.getWrites().get(0).getValues().set(0, 99999)); assertEquals(Integer.valueOf(70501), encoded.getPathPoints().get(0)); } private Ft546EncodedCommand encodeRoute(List route) { return encoder.encode(intent((short) 1, route, null, null), fullProfile(), capabilities("POINT_TO_POINT_COMMAND")); } private void assertRouteRejected(List route) { IllegalArgumentException failure = assertThrows( IllegalArgumentException.class, () -> encodeRoute(route)); assertEquals( "legacy route must contain between 2 and 8 command points", failure.getMessage()); } private static Ft546SrgvCommandIntent intent(short taskType, List route, Short crossFloorTarget, Integer stage) { return new Ft546SrgvCommandIntent("1", "PARENT-1", "LOADED_TO_PLACE", "KEY-1", 3L, 30001, taskType, route, crossFloorTarget, stage); } private static Ft546DeviceProfile fullProfile() { Ft546DeviceProfile profile = new Ft546DeviceProfile(); profile.setDeviceId("1"); profile.setDeviceType("SHUTTLE"); profile.setAdapterType("FT546_LEGACY_SRGV"); profile.setProfileVersion(1L); profile.setCapabilities(capabilities("POINT_TO_POINT_COMMAND", "TWO_STAGE_ATOMIC_TASK")); Map registers = new LinkedHashMap(); registers.put("taskNo", "4000"); registers.put("taskType", "4002"); registers.put("path", "4004"); registers.put("trigger", "4021"); registers.put("crossFloorFlag", "4020"); registers.put("crossFloorTarget", "4022"); registers.put("secondTaskNo", "4400"); registers.put("secondTaskType", "4402"); registers.put("secondPath", "4404"); registers.put("secondStageFlag", "4422"); registers.put("secondComplete", "4423"); profile.setRegisters(registers); return profile; } private static Set capabilities(String... values) { return new LinkedHashSet(Arrays.asList(values)); } private static List points(Integer... values) { return Arrays.asList(values); } private static ExpectedWrite write(int sequence, String address, Ft546RegisterWrite.DataType dataType, boolean trigger, Integer... values) { return new ExpectedWrite(sequence, address, dataType, trigger, Arrays.asList(values)); } private static void assertWrites(Ft546EncodedCommand command, ExpectedWrite... expected) { assertEquals(expected.length, command.getWrites().size()); for (int index = 0; index < expected.length; index++) { ExpectedWrite wanted = expected[index]; Ft546RegisterWrite actual = command.getWrites().get(index); assertEquals(wanted.sequence, actual.getWriteSequence()); assertEquals(wanted.address, actual.getAddress()); assertEquals(wanted.dataType, actual.getDataType()); assertEquals(wanted.values, actual.getValues()); assertEquals(wanted.trigger, actual.isTrigger()); } } private static final class ExpectedWrite { private final int sequence; private final String address; private final Ft546RegisterWrite.DataType dataType; private final boolean trigger; private final List values; private ExpectedWrite(int sequence, String address, Ft546RegisterWrite.DataType dataType, boolean trigger, List values) { this.sequence = sequence; this.address = address; this.dataType = dataType; this.trigger = trigger; this.values = values; } } }