package li.cil.oc2.vm; import com.google.gson.*; import it.unimi.dsi.fastutil.bytes.ByteArrayFIFOQueue; import li.cil.oc2.api.bus.DeviceBusElement; import li.cil.oc2.api.device.DeviceMethod; import li.cil.oc2.api.device.IdentifiableDevice; import li.cil.oc2.api.device.object.Callback; import li.cil.oc2.api.device.object.ObjectDevice; import li.cil.oc2.api.device.object.Parameter; import li.cil.oc2.common.bus.DeviceBusControllerImpl; import li.cil.oc2.common.bus.DeviceBusElementImpl; import li.cil.oc2.common.capabilities.Capabilities; import li.cil.oc2.common.device.IdentifiableDeviceImpl; import li.cil.sedna.api.device.serial.SerialDevice; import net.minecraft.tileentity.TileEntity; import net.minecraft.util.math.BlockPos; import net.minecraft.world.World; import net.minecraftforge.common.capabilities.Capability; import net.minecraftforge.common.util.LazyOptional; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import javax.annotation.Nullable; import java.io.ByteArrayOutputStream; import java.util.Collections; import java.util.List; import java.util.UUID; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; public class ObjectDeviceProtocolTests { private static final BlockPos CONTROLLER_POS = new BlockPos(0, 0, 0); @Mock private Capability busElementCapability; private World world; private TestSerialDevice serialDevice; private DeviceBusControllerImpl controller; private DeviceBusElement busElement; @BeforeEach public void setupEach() { MockitoAnnotations.initMocks(this); Capabilities.DEVICE_BUS_ELEMENT_CAPABILITY = busElementCapability; serialDevice = new TestSerialDevice(); controller = new DeviceBusControllerImpl(serialDevice); busElement = new DeviceBusElementImpl(); world = mock(World.class); when(world.chunkExists(anyInt(), anyInt())).thenReturn(true); final TileEntity tileEntity = mock(TileEntity.class); when(world.getTileEntity(eq(CONTROLLER_POS))).thenReturn(tileEntity); when(tileEntity.getCapability(eq(busElementCapability), any())).thenReturn(LazyOptional.of(() -> busElement)); } @Test public void resetAndReadDescriptor() { final VoidIntMethod method = new VoidIntMethod(); busElement.addDevice(new TestDevice(method)); controller.scan(world, CONTROLLER_POS); final JsonObject request = new JsonObject(); request.addProperty("type", "status"); serialDevice.putAsVM(request.toString()); controller.step(0); // process message final String message = serialDevice.readMessageAsVM(); Assertions.assertNotNull(message); final JsonObject json = new JsonParser().parse(message).getAsJsonObject(); final JsonArray devices = json.getAsJsonArray("data"); Assertions.assertEquals(1, devices.size()); final JsonObject device = devices.get(0).getAsJsonObject(); final JsonArray methods = device.getAsJsonArray("methods"); Assertions.assertEquals(1, methods.size()); } @Test public void simpleMethod() { final VoidIntMethod method = new VoidIntMethod(); final TestDevice device = new TestDevice(method); busElement.addDevice(device); controller.scan(world, CONTROLLER_POS); invokeMethod(device, method.getName(), 0xdeadbeef); Assertions.assertEquals(0xdeadbeef, method.passedValue); } @Test public void returningMethod() { final IntLongMethod method = new IntLongMethod(); final TestDevice device = new TestDevice(method); busElement.addDevice(device); controller.scan(world, CONTROLLER_POS); final JsonElement result = invokeMethod(device, method.getName(), 0xdeadbeefcafebabeL); Assertions.assertNotNull(result); Assertions.assertTrue(result.isJsonPrimitive()); Assertions.assertEquals(0xcafebabe, result.getAsInt()); } @Test public void annotatedObject() { final SimpleObject object = new SimpleObject(); final ObjectDevice device = new ObjectDevice(object); final IdentifiableDeviceImpl identifiableDevice = new IdentifiableDeviceImpl(LazyOptional.of(() -> device), UUID.randomUUID()); busElement.addDevice(identifiableDevice); controller.scan(world, CONTROLLER_POS); Assertions.assertEquals(42 + 23, invokeMethod(identifiableDevice, "add", 42, 23).getAsInt()); } private JsonElement invokeMethod(final IdentifiableDevice device, final String name, final Object... parameters) { final JsonObject request = new JsonObject(); request.addProperty("type", "invoke"); final JsonObject methodInvocation = new JsonObject(); methodInvocation.addProperty("deviceId", device.getUniqueId().toString()); methodInvocation.addProperty("name", name); final JsonArray parametersJson = new JsonArray(); methodInvocation.add("parameters", parametersJson); for (final Object parameter : parameters) { parametersJson.add(new Gson().toJson(parameter)); } request.add("data", methodInvocation); serialDevice.putAsVM(request.toString()); controller.step(0); final String result = serialDevice.readMessageAsVM(); Assertions.assertNotNull(result); final JsonObject resultJson = new JsonParser().parse(result).getAsJsonObject(); Assertions.assertEquals("result", resultJson.get("type").getAsString()); return resultJson.get("data"); } private static final class VoidIntMethod extends AbstractTestMethod { public int passedValue; VoidIntMethod() { super(void.class, int.class); } @Override public Object invoke(final Object... parameters) { passedValue = (int) parameters[0]; return 0; } } private static final class IntLongMethod extends AbstractTestMethod { public long passedValue; IntLongMethod() { super(int.class, long.class); } @Override public Object invoke(final Object... parameters) { passedValue = (long) parameters[0]; return (int) passedValue; } } public static final class SimpleObject { @Callback(synchronize = false) public int add(@Parameter("a") final int a, @Parameter("b") final int b) { return a + b; } @Callback(synchronize = false) public int div(@Parameter("a") final long a, @Parameter("b") final long b) { return (int) (a / b); } } private static final class TestSerialDevice implements SerialDevice { private final ByteArrayFIFOQueue transmit = new ByteArrayFIFOQueue(); private final ByteArrayFIFOQueue receive = new ByteArrayFIFOQueue(); public void putAsVM(final String data) { final byte[] bytes = data.getBytes(); for (int i = 0; i < bytes.length; i++) { transmit.enqueue(bytes[i]); } transmit.enqueue((byte) 0); } @Nullable public String readMessageAsVM() { final ByteArrayOutputStream bytes = new ByteArrayOutputStream(); while (!receive.isEmpty()) { final byte value = receive.dequeueByte(); if (value == 0) { if (bytes.size() == 0) { continue; } else { break; } } bytes.write(value); } if (bytes.size() > 0) { return new String(bytes.toByteArray()); } else { return null; } } @Override public int read() { return transmit.isEmpty() ? -1 : transmit.dequeueByte(); } @Override public boolean canPutByte() { return true; } @Override public void putByte(final byte value) { receive.enqueue(value); } } private static final class TestDevice implements IdentifiableDevice { private static final UUID UUID = java.util.UUID.randomUUID(); private final DeviceMethod method; public TestDevice(final DeviceMethod method) { this.method = method; } @Override public List getTypeNames() { return Collections.singletonList(getClass().getSimpleName()); } @Override public List getMethods() { return Collections.singletonList(method); } @Override public UUID getUniqueId() { return UUID; } } }