Extend the INVOKE_VIEW_METHOD of the "Generic View Operation" (VUOP) DDM handler to

1) Return the invoked method's return value
2) In addition to the primitive values, support String and byte[]

This DDM API is used by some development tools (such as go/web-hv), where the ability retrieve values from views and sending strings and byte blobs allows to trigger specific view behavior.

## String support
Allows passing in and returning String objects, using the (non-standard) R type specifier. This implementation is modelled after ViewHierarchyEncoder for consistency (whose result is passed back via the same DdmHandleViewDebug class).

## byte[] support
Allows passing in and returning byte[] objects, using the "[Z" type specifier.
For simplicity, only byte arrays are supported, though other types can be added in a backwards compatible way in the future, if needed.

## testing
Added unit tests for serialization/deserialization. Unable to test `invokeViewMethod`, since it uses the `Chunk` class which is not exposed outside of the framework

Test: Adding unit tests for serialization deserialization
Test: Manually invoked method via go/web-hv
Bug: 245727371

Change-Id: I29d2d6ec3b6eb59a5f2e7d3312dc36c00007672d
This commit is contained in:
Mike Schneider
2022-09-08 10:04:15 +02:00
parent 1d04b19658
commit 25546aa381
2 changed files with 632 additions and 63 deletions

View File

@@ -16,12 +16,16 @@
package android.ddm;
import static com.android.internal.util.Preconditions.checkArgument;
import android.util.Log;
import android.view.View;
import android.view.ViewDebug;
import android.view.ViewRootImpl;
import android.view.WindowManagerGlobal;
import com.android.internal.annotations.VisibleForTesting;
import org.apache.harmony.dalvik.ddmc.Chunk;
import org.apache.harmony.dalvik.ddmc.ChunkHandler;
import org.apache.harmony.dalvik.ddmc.DdmServer;
@@ -34,6 +38,7 @@ import java.io.OutputStreamWriter;
import java.lang.reflect.Method;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
/**
* Handle various requests related to profiling / debugging of the view system.
@@ -123,14 +128,15 @@ public class DdmHandleViewDebug extends DdmHandle {
}
if (type == CHUNK_VURT) {
if (op == VURT_DUMP_HIERARCHY)
if (op == VURT_DUMP_HIERARCHY) {
return dumpHierarchy(rootView, in);
else if (op == VURT_CAPTURE_LAYERS)
} else if (op == VURT_CAPTURE_LAYERS) {
return captureLayers(rootView);
else if (op == VURT_DUMP_THEME)
} else if (op == VURT_DUMP_THEME) {
return dumpTheme(rootView);
else
} else {
return createFailChunk(ERR_INVALID_OP, "Unknown view root operation: " + op);
}
}
final View targetView = getTargetView(rootView, in);
@@ -207,9 +213,9 @@ public class DdmHandleViewDebug extends DdmHandle {
/**
* Returns the view hierarchy and/or view properties starting at the provided view.
* Based on the input options, the return data may include:
* - just the view hierarchy
* - view hierarchy & the properties for each of the views
* - just the view properties for a specific view.
* - just the view hierarchy
* - view hierarchy & the properties for each of the views
* - just the view properties for a specific view.
* TODO: Currently this only returns views starting at the root, need to fix so that
* it can return properties of any view.
*/
@@ -220,7 +226,7 @@ public class DdmHandleViewDebug extends DdmHandle {
long start = System.currentTimeMillis();
ByteArrayOutputStream b = new ByteArrayOutputStream(2*1024*1024);
ByteArrayOutputStream b = new ByteArrayOutputStream(2 * 1024 * 1024);
try {
if (v2) {
ViewDebug.dumpv2(rootView, b);
@@ -304,17 +310,47 @@ public class DdmHandleViewDebug extends DdmHandle {
* Invokes provided method on the view.
* The method name and its arguments are passed in as inputs via the byte buffer.
* The buffer contains:<ol>
* <li> len(method name) </li>
* <li> method name </li>
* <li> # of args </li>
* <li> arguments: Each argument comprises of a type specifier followed by the actual argument.
* The type specifier is a single character as used in JNI:
* (Z - boolean, B - byte, C - char, S - short, I - int, J - long,
* F - float, D - double). <p>
* The type specifier is followed by the actual value of argument.
* Booleans are encoded via bytes with 0 indicating false.</li>
* <li> len(method name) </li>
* <li> method name (encoded as UTF-16 2-byte characters) </li>
* <li> # of args </li>
* <li> arguments: Each argument comprises of a type specifier followed by the actual argument.
* The type specifier is one character modelled after JNI signatures:
* <ul>
* <li>[ - array<br>
* This is followed by a second character according to this spec, indicating the
* array type, then the array length as an Int, followed by a repeated encoding
* of the actual data.
* WARNING: Only <b>byte[]</b> is supported currently.
* </li>
* <li>Z - boolean<br>
* Booleans are encoded via bytes with 0 indicating false</li>
* <li>B - byte</li>
* <li>C - char</li>
* <li>S - short</li>
* <li>I - int</li>
* <li>J - long</li>
* <li>F - float</li>
* <li>D - double</li>
* <li>V - void<br>
* NOT followed by a value. Only used for return types</li>
* <li>R - String (not a real JNI type, but added for convenience)<br>
* Strings are encoded as an unsigned short of the number of <b>bytes</b>,
* followed by the actual UTF-8 encoded bytes.
* WARNING: This is the same encoding as produced by
* ViewHierarchyEncoder#writeString. However, note that this encoding is
* different to what DdmHandle#getString() expects, which is used in other places
* in this class.
* WARNING: Since the length is the number of UTF-8 encoded bytes, Strings can
* contain up to 64k ASCII characters, yet depending on the actual data, the true
* maximum might be as little as 21844 unicode characters.
* <b>null</b> String objects are encoded as an empty string
* </li>
* </ul>
* </li>
* </ol>
* Methods that take no arguments need only specify the method name.
*
* The return value is encoded the same way as a single parameter (type + value)
*/
private Chunk invokeViewMethod(final View rootView, final View targetView, ByteBuffer in) {
int l = in.getInt();
@@ -327,54 +363,17 @@ public class DdmHandleViewDebug extends DdmHandle {
args = new Object[0];
} else {
int nArgs = in.getInt();
argTypes = new Class<?>[nArgs];
args = new Object[nArgs];
for (int i = 0; i < nArgs; i++) {
char c = in.getChar();
switch (c) {
case 'Z':
argTypes[i] = boolean.class;
args[i] = in.get() == 0 ? false : true;
break;
case 'B':
argTypes[i] = byte.class;
args[i] = in.get();
break;
case 'C':
argTypes[i] = char.class;
args[i] = in.getChar();
break;
case 'S':
argTypes[i] = short.class;
args[i] = in.getShort();
break;
case 'I':
argTypes[i] = int.class;
args[i] = in.getInt();
break;
case 'J':
argTypes[i] = long.class;
args[i] = in.getLong();
break;
case 'F':
argTypes[i] = float.class;
args[i] = in.getFloat();
break;
case 'D':
argTypes[i] = double.class;
args[i] = in.getDouble();
break;
default:
Log.e(TAG, "arg " + i + ", unrecognized type: " + c);
return createFailChunk(ERR_INVALID_PARAM,
"Unsupported parameter type (" + c + ") to invoke view method.");
}
try {
deserializeMethodParameters(args, argTypes, in);
} catch (ViewMethodInvocationSerializationException e) {
return createFailChunk(ERR_INVALID_PARAM, e.getMessage());
}
}
Method method = null;
Method method;
try {
method = targetView.getClass().getMethod(methodName, argTypes);
} catch (NoSuchMethodException e) {
@@ -384,7 +383,10 @@ public class DdmHandleViewDebug extends DdmHandle {
}
try {
ViewDebug.invokeViewMethod(targetView, method, args);
Object result = ViewDebug.invokeViewMethod(targetView, method, args);
Class<?> returnType = method.getReturnType();
byte[] returnValue = serializeReturnValue(returnType, returnType.cast(result));
return new Chunk(CHUNK_VUOP, returnValue, 0, returnValue.length);
} catch (Exception e) {
Log.e(TAG, "Exception while invoking method: " + e.getCause().getMessage());
String msg = e.getCause().getMessage();
@@ -393,8 +395,6 @@ public class DdmHandleViewDebug extends DdmHandle {
}
return createFailChunk(ERR_EXCEPTION, msg);
}
return null;
}
private Chunk setLayoutParameter(final View rootView, final View targetView, ByteBuffer in) {
@@ -406,7 +406,7 @@ public class DdmHandleViewDebug extends DdmHandle {
} catch (Exception e) {
Log.e(TAG, "Exception setting layout parameter: " + e);
return createFailChunk(ERR_EXCEPTION, "Error accessing field "
+ param + ":" + e.getMessage());
+ param + ":" + e.getMessage());
}
return null;
@@ -431,4 +431,175 @@ public class DdmHandleViewDebug extends DdmHandle {
byte[] data = b.toByteArray();
return new Chunk(CHUNK_VUOP, data, 0, data.length);
}
/**
* Deserializes parameters according to the VUOP_INVOKE_VIEW_METHOD protocol the {@code in}
* buffer.
*
* The length of {@code args} determines how many arguments are read. The {@code argTypes} must
* be the same length, and will be set to the argument types of the data read.
*
* @hide
*/
@VisibleForTesting
public static void deserializeMethodParameters(
Object[] args, Class<?>[] argTypes, ByteBuffer in) throws
ViewMethodInvocationSerializationException {
checkArgument(args.length == argTypes.length);
for (int i = 0; i < args.length; i++) {
char typeSignature = in.getChar();
boolean isArray = typeSignature == SIG_ARRAY;
if (isArray) {
char arrayType = in.getChar();
if (arrayType != SIG_BYTE) {
// This implementation only supports byte-arrays for now.
throw new ViewMethodInvocationSerializationException(
"Unsupported array parameter type (" + typeSignature
+ ") to invoke view method @argument " + i);
}
int arrayLength = in.getInt();
if (arrayLength > in.remaining()) {
// The sender did not actually sent the specified amount of bytes. This
// avoids a malformed packet to trigger an out-of-memory error.
throw new BufferUnderflowException();
}
byte[] byteArray = new byte[arrayLength];
in.get(byteArray);
argTypes[i] = byte[].class;
args[i] = byteArray;
} else {
switch (typeSignature) {
case SIG_BOOLEAN:
argTypes[i] = boolean.class;
args[i] = in.get() != 0;
break;
case SIG_BYTE:
argTypes[i] = byte.class;
args[i] = in.get();
break;
case SIG_CHAR:
argTypes[i] = char.class;
args[i] = in.getChar();
break;
case SIG_SHORT:
argTypes[i] = short.class;
args[i] = in.getShort();
break;
case SIG_INT:
argTypes[i] = int.class;
args[i] = in.getInt();
break;
case SIG_LONG:
argTypes[i] = long.class;
args[i] = in.getLong();
break;
case SIG_FLOAT:
argTypes[i] = float.class;
args[i] = in.getFloat();
break;
case SIG_DOUBLE:
argTypes[i] = double.class;
args[i] = in.getDouble();
break;
case SIG_STRING: {
argTypes[i] = String.class;
int stringUtf8ByteCount = Short.toUnsignedInt(in.getShort());
byte[] rawStringBuffer = new byte[stringUtf8ByteCount];
in.get(rawStringBuffer);
args[i] = new String(rawStringBuffer, StandardCharsets.UTF_8);
break;
}
default:
Log.e(TAG, "arg " + i + ", unrecognized type: " + typeSignature);
throw new ViewMethodInvocationSerializationException(
"Unsupported parameter type (" + typeSignature
+ ") to invoke view method.");
}
}
}
}
/**
* Serializes {@code value} to the wire protocol of VUOP_INVOKE_VIEW_METHOD.
* @hide
*/
@VisibleForTesting
public static byte[] serializeReturnValue(Class<?> type, Object value)
throws ViewMethodInvocationSerializationException, IOException {
ByteArrayOutputStream byteOutStream = new ByteArrayOutputStream(1024);
DataOutputStream dos = new DataOutputStream(byteOutStream);
if (type.isArray()) {
if (!type.equals(byte[].class)) {
// Only byte arrays are supported currently.
throw new ViewMethodInvocationSerializationException(
"Unsupported array return type (" + type + ")");
}
byte[] byteArray = (byte[]) value;
dos.writeChar(SIG_ARRAY);
dos.writeChar(SIG_BYTE);
dos.writeInt(byteArray.length);
dos.write(byteArray);
} else if (boolean.class.equals(type)) {
dos.writeChar(SIG_BOOLEAN);
dos.write((boolean) value ? 1 : 0);
} else if (byte.class.equals(type)) {
dos.writeChar(SIG_BYTE);
dos.writeByte((byte) value);
} else if (char.class.equals(type)) {
dos.writeChar(SIG_CHAR);
dos.writeChar((char) value);
} else if (short.class.equals(type)) {
dos.writeChar(SIG_SHORT);
dos.writeShort((short) value);
} else if (int.class.equals(type)) {
dos.writeChar(SIG_INT);
dos.writeInt((int) value);
} else if (long.class.equals(type)) {
dos.writeChar(SIG_LONG);
dos.writeLong((long) value);
} else if (double.class.equals(type)) {
dos.writeChar(SIG_DOUBLE);
dos.writeDouble((double) value);
} else if (float.class.equals(type)) {
dos.writeChar(SIG_FLOAT);
dos.writeFloat((float) value);
} else if (String.class.equals(type)) {
dos.writeChar(SIG_STRING);
dos.writeUTF(value != null ? (String) value : "");
} else {
dos.writeChar(SIG_VOID);
}
return byteOutStream.toByteArray();
}
// Prefixes for simple primitives. These match the JNI definitions.
private static final char SIG_ARRAY = '[';
private static final char SIG_BOOLEAN = 'Z';
private static final char SIG_BYTE = 'B';
private static final char SIG_SHORT = 'S';
private static final char SIG_CHAR = 'C';
private static final char SIG_INT = 'I';
private static final char SIG_LONG = 'J';
private static final char SIG_FLOAT = 'F';
private static final char SIG_DOUBLE = 'D';
private static final char SIG_VOID = 'V';
// Prefixes for some commonly used objects
private static final char SIG_STRING = 'R';
/**
* @hide
*/
@VisibleForTesting
public static class ViewMethodInvocationSerializationException extends Exception {
ViewMethodInvocationSerializationException(String message) {
super(message);
}
}
}

View File

@@ -0,0 +1,398 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.ddm;
import static android.ddm.DdmHandleViewDebug.deserializeMethodParameters;
import static android.ddm.DdmHandleViewDebug.serializeReturnValue;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;
import android.ddm.DdmHandleViewDebug.ViewMethodInvocationSerializationException;
import android.platform.test.annotations.Presubmit;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.util.Arrays;
@RunWith(AndroidJUnit4.class)
@SmallTest
@Presubmit
public final class DdmHandleViewDebugTest {
// true
private static final byte[] SERIALIZED_BOOLEAN_TRUE = {0x00, 0x5A, 1};
@Test
public void serializeReturnValue_booleanTrue() throws Exception {
assertArrayEquals(SERIALIZED_BOOLEAN_TRUE, serializeReturnValue(boolean.class, true));
}
@Test
public void deserializeMethodParameters_booleanTrue() throws Exception {
expectDeserializedArgument(boolean.class, true, SERIALIZED_BOOLEAN_TRUE);
}
// false
private static final byte[] SERIALIZED_BOOLEAN_FALSE = {0x00, 0x5A, 0};
@Test
public void serializeReturnValue_booleanFalse() throws Exception {
assertArrayEquals(SERIALIZED_BOOLEAN_FALSE, serializeReturnValue(boolean.class, false));
}
@Test
public void deserializeMethodParameters_booleanFalse() throws Exception {
expectDeserializedArgument(boolean.class, false, SERIALIZED_BOOLEAN_FALSE);
}
// (byte) 42
private static final byte[] SERIALIZED_BYTE = {0x00, 0x42, 42};
@Test
public void serializeReturnValue_byte() throws Exception {
assertArrayEquals(SERIALIZED_BYTE, serializeReturnValue(byte.class, (byte) 42));
}
@Test
public void deserializeMethodParameters_byte() throws Exception {
expectDeserializedArgument(byte.class, (byte) 42, SERIALIZED_BYTE);
}
// '\u1122'
private static final byte[] SERIALIZED_CHAR = {0x00, 0x43, 0x11, 0x22};
@Test
public void serializeReturnValue_char() throws Exception {
assertArrayEquals(SERIALIZED_CHAR, serializeReturnValue(char.class, '\u1122'));
}
@Test
public void deserializeMethodParameters_char() throws Exception {
expectDeserializedArgument(char.class, '\u1122', SERIALIZED_CHAR);
}
// (short) 0x1011
private static final byte[] SERIALIZED_SHORT = {0x00, 0x53, 0x10, 0x11};
@Test
public void serializeReturnValue_short() throws Exception {
assertArrayEquals(SERIALIZED_SHORT,
serializeReturnValue(short.class, (short) 0x1011));
}
@Test
public void deserializeMethodParameters_short() throws Exception {
expectDeserializedArgument(short.class, (short) 0x1011, SERIALIZED_SHORT);
}
// 0x11223344
private static final byte[] SERIALIZED_INT = {0x00, 0x49, 0x11, 0x22, 0x33, 0x44};
@Test
public void serializeReturnValue_int() throws Exception {
assertArrayEquals(SERIALIZED_INT,
serializeReturnValue(int.class, 0x11223344));
}
@Test
public void deserializeMethodParameters_int() throws Exception {
expectDeserializedArgument(int.class, 0x11223344, SERIALIZED_INT);
}
// 0x0011223344556677L
private static final byte[] SERIALIZED_LONG =
{0x00, 0x4a, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77};
@Test
public void serializeReturnValue_long() throws Exception {
assertArrayEquals(SERIALIZED_LONG,
serializeReturnValue(long.class, 0x0011223344556677L));
}
@Test
public void deserializeMethodParameters_long() throws Exception {
expectDeserializedArgument(long.class, 0x0011223344556677L, SERIALIZED_LONG);
}
// 3.141d
private static final byte[] SERIALIZED_DOUBLE =
{0x00, 0x44, (byte) 0x40, (byte) 0x09, (byte) 0x20, (byte) 0xc4, (byte) 0x9b,
(byte) 0xa5, (byte) 0xe3, (byte) 0x54};
@Test
public void serializeReturnValue_double() throws Exception {
assertArrayEquals(
SERIALIZED_DOUBLE,
serializeReturnValue(double.class, 3.141d));
}
@Test
public void deserializeMethodParameters_double() throws Exception {
expectDeserializedArgument(double.class, 3.141d, SERIALIZED_DOUBLE);
}
// 3.141f
private static final byte[] SERIALIZED_FLOAT =
{0x00, 0x46, (byte) 0x40, (byte) 0x49, (byte) 0x06, (byte) 0x25};
@Test
public void serializeReturnValue_float() throws Exception {
assertArrayEquals(SERIALIZED_FLOAT,
serializeReturnValue(float.class, 3.141f));
}
@Test
public void deserializeMethodParameters_float() throws Exception {
expectDeserializedArgument(float.class, 3.141f, SERIALIZED_FLOAT);
}
// "foo"
private static final byte[] SERIALIZED_ASCII_STRING = {0x00, 0x52, 0, 3, 0x66, 0x6f, 0x6f};
@Test
public void serializeReturnValue_asciiString() throws Exception {
assertArrayEquals(SERIALIZED_ASCII_STRING,
serializeReturnValue(String.class, "foo"));
}
@Test
public void deserializeMethodParameters_asciiString() throws Exception {
expectDeserializedArgument(String.class, "foo", SERIALIZED_ASCII_STRING);
}
// "\u1122"
private static final byte[] SERIALIZED_NON_ASCII_STRING =
{0x00, 0x52, 0, 3, (byte) 0xe1, (byte) 0x84, (byte) 0xa2};
@Test
public void serializeReturnValue_nonAsciiString_encodesAsUtf8() throws Exception {
assertArrayEquals(SERIALIZED_NON_ASCII_STRING,
serializeReturnValue(String.class, "\u1122"));
}
@Test
public void deserializeMethodParameters_decodesFromUtf8() throws Exception {
expectDeserializedArgument(String.class, "\u1122", SERIALIZED_NON_ASCII_STRING);
}
// ""
private static final byte[] SERIALIZED_EMPTY_STRING = {0x00, 0x52, 0, 0};
@Test
public void serializeReturnValue_emptyString() throws Exception {
assertArrayEquals(SERIALIZED_EMPTY_STRING, serializeReturnValue(String.class, ""));
}
@Test
public void deserializeMethodParameters_emptyString() throws Exception {
expectDeserializedArgument(String.class, "", SERIALIZED_EMPTY_STRING);
}
@Test
public void serializeReturnValue_nullString_encodesAsEmptyString() throws Exception {
assertArrayEquals(new byte[]{0x00, 0x52, 0, 0}, serializeReturnValue(String.class, null));
}
// Illegal - string length exceeding actual bytes
private static final byte[] SERIALIZED_INVALID_STRING =
{0x00, 0x52, 0, 3, 0x66};
@Test
public void deserializeMethodParameters_stringPayloadMissing_throws() throws Exception {
Object[] args = new Object[1];
Class<?>[] argTypes = new Class<?>[1];
assertThrows(BufferUnderflowException.class,
() -> deserializeMethodParameters(args, argTypes,
ByteBuffer.wrap(SERIALIZED_INVALID_STRING)));
}
@Test
public void serializeAndDeserialize_handlesStringsUpTo64k() throws Exception {
char[] chars = new char[65535];
Arrays.fill(chars, 'a');
String original = new String(chars);
byte[] serialized = serializeReturnValue(String.class, original);
// 2 bytes for the R signature char, 2 bytes char string byte count, 2^16-1 bytes ASCII
// payload
assertEquals(2 + 2 + 65535, serialized.length);
// length is unsigned short
assertArrayEquals(new byte[]{0x00, 0x52, (byte) 0xff, (byte) 0xff},
Arrays.copyOfRange(serialized, 0, 4));
// length of string must be interpreted as unsigned short, returning original content
expectDeserializedArgument(String.class, original, serialized);
}
private static final byte[] SERIALIZED_VOID = {0x00, 0x56};
@Test
public void serializeReturnValue_void() throws Exception {
assertArrayEquals(SERIALIZED_VOID, serializeReturnValue(void.class, null));
}
@Test
public void deserializeMethodParameters_void_throws() throws Exception {
Object[] args = new Object[1];
Class<?>[] argTypes = new Class<?>[1];
assertThrows(ViewMethodInvocationSerializationException.class,
() -> deserializeMethodParameters(args, argTypes,
ByteBuffer.wrap(SERIALIZED_VOID)));
}
// new byte[]{}
private static final byte[] SERIALIZED_EMPTY_BYTE_ARRAY = {0x00, 0x5b, 0x00, 0x42, 0, 0, 0, 0};
@Test
public void serializeReturnValue_emptyByteArray() throws Exception {
assertArrayEquals(SERIALIZED_EMPTY_BYTE_ARRAY,
serializeReturnValue(byte[].class, new byte[]{}));
}
@Test
public void deserializeMethodParameters_emptyByteArray() throws Exception {
expectDeserializedArgument(byte[].class, new byte[]{}, SERIALIZED_EMPTY_BYTE_ARRAY);
}
// new byte[]{0, 42}
private static final byte[] SERIALIZED_SIMPLE_BYTE_ARRAY =
{0x00, 0x5b, 0x00, 0x42, 0, 0, 0, 2, 0, 42};
@Test
public void serializeReturnValue_byteArray() throws Exception {
assertArrayEquals(SERIALIZED_SIMPLE_BYTE_ARRAY,
serializeReturnValue(byte[].class, new byte[]{0, 42}));
}
@Test
public void deserializeMethodParameters_byteArray() throws Exception {
expectDeserializedArgument(byte[].class, new byte[]{0, 42}, SERIALIZED_SIMPLE_BYTE_ARRAY);
}
@Test
public void serializeReturnValue_largeByteArray_encodesSizeCorrectly() throws Exception {
byte[] result = serializeReturnValue(byte[].class, new byte[0x012233]);
// 2 bytes for the each [Z signature char, 4 bytes int array length, 0x012233 bytes payload
assertEquals(2 + 2 + 4 + 74291, result.length);
assertArrayEquals(new byte[]{0x00, 0x5b, 0x00, 0x42, 0x00, 0x01, 0x22, 0x33},
Arrays.copyOfRange(result, 0, 8));
}
// Illegal - declared size exceeds remaining buffer length
private static final byte[] SERIALIZED_INVALID_BYTE_ARRAY =
{0x00, 0x5b, 0x00, 0x42, 0, 0, 0, 3, 0, 42};
@Test
public void deserializeMethodParameters_sizeExceedsBuffer_throws() throws Exception {
Object[] args = new Object[1];
Class<?>[] argTypes = new Class<?>[1];
assertThrows(BufferUnderflowException.class,
() -> deserializeMethodParameters(args, argTypes,
ByteBuffer.wrap(SERIALIZED_INVALID_BYTE_ARRAY)));
}
// new int[]{}
private static final byte[] SERIALIZED_EMPTY_INT_ARRAY = {0x00, 0x5b, 0x00, 0x49, 0, 0, 0, 0};
@Test
public void serializeReturnValue_nonByteArrayType_throws() throws Exception {
assertThrows(ViewMethodInvocationSerializationException.class,
() -> serializeReturnValue(int[].class, 42));
}
@Test
public void deserializeMethodParameters_nonByteArrayType_throws() throws Exception {
Object[] args = new Object[1];
Class<?>[] argTypes = new Class<?>[1];
assertThrows(ViewMethodInvocationSerializationException.class,
() -> deserializeMethodParameters(args, argTypes,
ByteBuffer.wrap(SERIALIZED_EMPTY_INT_ARRAY)));
}
// new byte[]{0, 42}
private static final byte[] SERIALIZED_MULTIPLE_PARAMETERS =
{0x00, 0x42, 42, 0x00, 0x5A, 1};
@Test
public void deserializeMethodParameters_multipleParameters() throws Exception {
expectDeserializedArguments(new Class[]{byte.class, boolean.class},
new Object[]{(byte) 42, true}, SERIALIZED_MULTIPLE_PARAMETERS);
}
// Illegal - type 'X'
private static final byte[] SERIALIZED_INVALID_UNKNOWN_TYPE = {0x00, 0x58};
@Test
public void deserializeMethodParameters_unknownType_throws() throws Exception {
Object[] args = new Object[1];
Class<?>[] argTypes = new Class<?>[1];
assertThrows(ViewMethodInvocationSerializationException.class,
() -> deserializeMethodParameters(args, argTypes,
ByteBuffer.wrap(SERIALIZED_INVALID_UNKNOWN_TYPE)));
}
@Test
public void deserializeMethodParameters_noArgumentsEmptyPacket_isNoop() throws Exception {
Object[] args = new Object[0];
Class<?>[] argTypes = new Class<?>[0];
deserializeMethodParameters(args, argTypes, ByteBuffer.wrap(new byte[0]));
}
@Test
public void deserializeMethodParameters_withArgumentsEmptyPacket_throws() throws Exception {
Object[] args = new Object[1];
Class<?>[] argTypes = new Class<?>[1];
assertThrows(BufferUnderflowException.class,
() -> deserializeMethodParameters(args, argTypes, ByteBuffer.wrap(new byte[0])));
}
private static void expectDeserializedArgument(Class<?> expectedType, Object expectedValue,
byte[] argumentBuffer) throws Exception {
expectDeserializedArguments(new Class[]{expectedType}, new Object[]{expectedValue},
argumentBuffer);
}
private static void expectDeserializedArguments(Class<?>[] expectedTypes,
Object[] expectedValues, byte[] argumentBuffer) throws Exception {
final int argCount = expectedTypes.length;
assertEquals("test helper not used correctly", argCount, expectedValues.length);
Object[] actualArgs = new Object[argCount];
Class<?>[] actualArgTypes = new Class<?>[argCount];
ByteBuffer buffer = ByteBuffer.wrap(argumentBuffer);
deserializeMethodParameters(actualArgs, actualArgTypes, buffer);
for (int i = 0; i < argCount; i++) {
String context = "argument " + i;
assertEquals(context, expectedTypes[i], actualArgTypes[i]);
if (byte[].class.equals(expectedTypes[i])) {
assertArrayEquals((byte[]) expectedValues[i], (byte[]) actualArgs[i]);
} else {
assertEquals(expectedValues[i], actualArgs[i]);
}
}
}
}