/*
* Copyright 2006 Amazon Technologies, Inc. or its affiliates.
* Amazon, Amazon.com and Carbonado are trademarks or registered trademarks
* of Amazon Technologies, Inc. or its affiliates. All rights reserved.
*
* 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 com.amazon.carbonado.raw;
import java.lang.reflect.*;
import java.util.*;
import junit.framework.TestCase;
import junit.framework.TestSuite;
import org.cojen.classfile.*;
import org.cojen.util.*;
import com.amazon.carbonado.*;
import com.amazon.carbonado.info.*;
import com.amazon.carbonado.spi.*;
/**
* Test case for {@link GenericEncodingStrategy}.
*
* It generates random selections of properties, encodes with random values,
* and checks that the decoding produces the original results. In addition, the
* proper ordering of encoded keys is checked.
*
* @author Brian S O'Neill
*/
public class TestEncodingStrategy extends TestCase {
private static final int SHORT_TEST = 100;
private static final int MEDIUM_TEST = 500;
private static final int LONG_TEST = 1000;
private static final int ENCODE_OBJECT_ARRAY = 0;
private static final int DECODE_OBJECT_ARRAY = 1;
private static final int ENCODE_OBJECT_ARRAY_PARTIAL = 2;
private static final int BOGUS_GENERATION = 99;
// Make sure BOGUS_GENERATION is not included.
private static final int[] GENERATIONS = {-1, 0, 1, 2, 127, 128, 129, Integer.MAX_VALUE};
public static void main(String[] args) {
junit.textui.TestRunner.run(suite());
}
public static TestSuite suite() {
return new TestSuite(TestEncodingStrategy.class);
}
private final long mSeed;
private final StorableProperty[] mProperties;
private Random mRandom;
public TestEncodingStrategy(String name) {
super(name);
mSeed = 986184829029842L;
Collection extends StorableProperty> properties =
StorableIntrospector.examine(TestStorable.class).getAllProperties().values();
mProperties = properties.toArray(new StorableProperty[0]);
}
protected void setUp() {
mRandom = new Random(mSeed);
}
protected void tearDown() {
}
public void test_dataEncoding_noProperties() throws Exception {
for (int generation : GENERATIONS) {
test_dataEncoding_noProperties(0, 0, generation);
}
}
public void test_dataEncoding_noProperties_prefix() throws Exception {
for (int generation : GENERATIONS) {
test_dataEncoding_noProperties(5, 0, generation);
}
}
public void test_dataEncoding_noProperties_suffix() throws Exception {
for (int generation : GENERATIONS) {
test_dataEncoding_noProperties(0, 7, generation);
}
}
public void test_dataEncoding_noProperties_prefixAndSuffix() throws Exception {
for (int generation : GENERATIONS) {
test_dataEncoding_noProperties(5, 7, generation);
}
}
private void test_dataEncoding_noProperties(int prefix, int suffix, int generation)
throws Exception
{
GenericEncodingStrategy strategy = new GenericEncodingStrategy
(TestStorable.class, null, 0, 0, prefix, suffix);
Method[] methods = generateCodecMethods
(strategy, new StorableProperty[0], null, generation);
byte[] encoded = (byte[]) methods[ENCODE_OBJECT_ARRAY]
.invoke(null, new Object[] {new Object[0]});
int generationPrefix;
if (generation < 0) {
generationPrefix = 0;
} else if (generation < 128) {
generationPrefix = 1;
} else {
generationPrefix = 4;
}
assertEquals(encoded.length, prefix + generationPrefix + suffix);
if (generation >= 0) {
if (generationPrefix == 1) {
assertEquals(generation, encoded[prefix]);
} else {
int actualGeneration = DataDecoder.decodeInt(encoded, prefix);
assertEquals(generation, actualGeneration);
}
}
// Decode should not throw an exception.
methods[DECODE_OBJECT_ARRAY].invoke(null, new Object[0], encoded);
// Generation mismatch should throw an exception.
if (generation >= 0) {
encoded[prefix] = BOGUS_GENERATION;
try {
methods[DECODE_OBJECT_ARRAY].invoke(null, new Object[0], encoded);
fail();
} catch (InvocationTargetException e) {
Throwable cause = e.getCause();
if (cause instanceof CorruptEncodingException) {
CorruptEncodingException cee = (CorruptEncodingException) cause;
// Make sure error message includes actual generation.
assertTrue(cee.getMessage().indexOf(String.valueOf(BOGUS_GENERATION)) >= 0);
} else {
throw e;
}
}
}
}
public void test_dataEncoding_multipleProperties() throws Exception {
for (int generation : GENERATIONS) {
test_dataEncoding_multipleProperties(0, 0, generation);
}
}
public void test_dataEncoding_multipleProperties_prefix() throws Exception {
for (int generation : GENERATIONS) {
test_dataEncoding_multipleProperties(5, 0, generation);
}
}
public void test_dataEncoding_multipleProperties_suffix() throws Exception {
for (int generation : GENERATIONS) {
test_dataEncoding_multipleProperties(0, 7, generation);
}
}
public void test_dataEncoding_multipleProperties_prefixAndSuffix() throws Exception {
for (int generation : GENERATIONS) {
test_dataEncoding_multipleProperties(5, 7, generation);
}
}
/**
* @param generation when non-negative, encode a storable layout generation
* value in one or four bytes.
*/
private void test_dataEncoding_multipleProperties(int prefix, int suffix, int generation)
throws Exception
{
GenericEncodingStrategy strategy = new GenericEncodingStrategy
(TestStorable.class, null, 0, 0, prefix, suffix);
for (int i=0; i[] properties = selectProperties(1, 50);
Method[] methods = generateCodecMethods(strategy, properties, null, generation);
Object[] values = selectPropertyValues(properties);
byte[] encoded = (byte[]) methods[ENCODE_OBJECT_ARRAY]
.invoke(null, new Object[] {values});
int generationPrefix;
if (generation < 0) {
generationPrefix = 0;
} else if (generation < 128) {
generationPrefix = 1;
} else {
generationPrefix = 4;
}
assertTrue(encoded.length > (prefix + generationPrefix + suffix));
if (generation >= 0) {
if (generationPrefix == 1) {
assertEquals(generation, encoded[prefix]);
} else {
int actualGeneration = DataDecoder.decodeInt(encoded, prefix);
assertEquals(generation, actualGeneration);
}
}
if (prefix > 0) {
// Fill in with data which should be ignored by decoder.
for (int p=0; p 0) {
// Fill in with data which should be ignored by decoder.
for (int p=0; p= 0) {
encoded[prefix] = BOGUS_GENERATION;
try {
methods[DECODE_OBJECT_ARRAY].invoke(null, new Object[0], encoded);
fail();
} catch (InvocationTargetException e) {
Throwable cause = e.getCause();
if (cause instanceof CorruptEncodingException) {
CorruptEncodingException cee = (CorruptEncodingException) cause;
// Make sure error message includes actual generation.
assertTrue
(cee.getMessage().indexOf(String.valueOf(BOGUS_GENERATION)) >= 0);
} else {
throw e;
}
}
}
}
}
public void test_keyEncoding_noProperties() throws Exception {
test_keyEncoding_noProperties(0, 0);
}
public void test_keyEncoding_noProperties_prefix() throws Exception {
test_keyEncoding_noProperties(5, 0);
}
public void test_keyEncoding_noProperties_suffix() throws Exception {
test_keyEncoding_noProperties(0, 7);
}
public void test_keyEncoding_noProperties_prefixAndSuffix() throws Exception {
test_keyEncoding_noProperties(5, 7);
}
private void test_keyEncoding_noProperties(int prefix, int suffix) throws Exception {
GenericEncodingStrategy strategy = new GenericEncodingStrategy
(TestStorable.class, null, prefix, suffix, 0, 0);
Method[] methods = generateCodecMethods
(strategy, new StorableProperty[0], new Direction[0], -1);
// Encode should return an empty array.
byte[] encoded = (byte[]) methods[ENCODE_OBJECT_ARRAY]
.invoke(null, new Object[] {new Object[0]});
assertEquals(encoded.length, prefix + suffix);
// Decode should not throw an exception.
methods[DECODE_OBJECT_ARRAY].invoke(null, new Object[0], encoded);
}
public void test_keyEncoding_multipleProperties() throws Exception {
test_keyEncoding_multipleProperties(0, 0);
}
public void test_keyEncoding_multipleProperties_prefix() throws Exception {
test_keyEncoding_multipleProperties(5, 0);
}
public void test_keyEncoding_multipleProperties_suffix() throws Exception {
test_keyEncoding_multipleProperties(0, 7);
}
public void test_keyEncoding_multipleProperties_prefixAndSuffix() throws Exception {
test_keyEncoding_multipleProperties(5, 7);
}
private void test_keyEncoding_multipleProperties(int prefix, int suffix) throws Exception {
GenericEncodingStrategy strategy = new GenericEncodingStrategy
(TestStorable.class, null, prefix, suffix, 0, 0);
for (int i=0; i[] properties = selectProperties(1, 50);
Direction[] directions = selectDirections(properties.length);
Method[] methods = generateCodecMethods(strategy, properties, directions, -1);
Object[] values = selectPropertyValues(properties);
byte[] encoded = (byte[]) methods[ENCODE_OBJECT_ARRAY]
.invoke(null, new Object[] {values});
assertTrue(encoded.length > (prefix + suffix));
// Encode using partial encoding method, but do all
// properties. Ensure that the encoding is exactly the same.
byte[] encoded2 = (byte[]) methods[ENCODE_OBJECT_ARRAY_PARTIAL]
.invoke(null, new Object[] {values, 0, properties.length});
assertTrue(Arrays.equals(encoded, encoded2));
if (prefix > 0) {
// Fill in with data which should be ignored by decoder.
for (int p=0; p 0) {
// Fill in with data which should be ignored by decoder.
for (int p=0; p 0) {
for (int p=0; p 0) {
for (int p=0; p 0);
}
if (properties.length == 1) {
break;
}
}
}
}
public void test_keyEncoding_ordering() throws Exception {
GenericEncodingStrategy strategy =
new GenericEncodingStrategy(TestStorable.class, null, 0, 0, 0, 0);
for (int i=0; i[] properties = selectProperties(1, 50);
Direction[] directions = selectDirections(properties.length);
Method[] methods = generateCodecMethods(strategy, properties, directions, -1);
Object[] values_1 = selectPropertyValues(properties);
byte[] encoded_1 = (byte[]) methods[ENCODE_OBJECT_ARRAY]
.invoke(null, new Object[] {values_1});
Object[] values_2 = selectPropertyValues(properties);
byte[] encoded_2 = (byte[]) methods[ENCODE_OBJECT_ARRAY]
.invoke(null, new Object[] {values_2});
int byteOrder = TestDataEncoding.byteArrayCompare(encoded_1, encoded_2);
int valueOrder = compareValues(properties, directions, values_1, values_2);
assertEquals(valueOrder, byteOrder);
}
}
private int compareValues(StorableProperty>[] properties, Direction[] directions,
Object[] values_1, Object[] values_2) {
int length = directions.length;
for (int i=0; i property = properties[i];
Direction direction = directions[i];
Object value_1 = values_1[i];
Object value_2 = values_2[i];
int result;
if (property.getType() == byte[].class) {
result = TestDataEncoding.byteArrayOrNullCompare
((byte[]) value_1, (byte[]) value_2);
} else {
if (value_1 == null) {
if (value_2 == null) {
result = 0;
} else {
result = 1;
}
} else if (value_2 == null) {
result = -1;
} else {
result = Integer.signum(((Comparable) value_1).compareTo(value_2));
}
}
if (result != 0) {
if (direction == Direction.DESCENDING) {
result = -result;
}
return result;
}
}
return 0;
}
// Method taken from String class and modified a bit.
private static int indexOf(byte[] source, int sourceOffset, int sourceCount,
byte[] target, int targetOffset, int targetCount,
int fromIndex) {
if (fromIndex >= sourceCount) {
return (targetCount == 0 ? sourceCount : -1);
}
if (fromIndex < 0) {
fromIndex = 0;
}
if (targetCount == 0) {
return fromIndex;
}
byte first = target[targetOffset];
int max = sourceOffset + (sourceCount - targetCount);
for (int i = sourceOffset + fromIndex; i <= max; i++) {
// Look for first byte.
if (source[i] != first) {
while (++i <= max && source[i] != first);
}
// Found first byte, now look at the rest of v2
if (i <= max) {
int j = i + 1;
int end = j + targetCount - 1;
for (int k = targetOffset + 1; j < end && source[j] == target[k]; j++, k++);
if (j == end) {
// Found whole string.
return i - sourceOffset;
}
}
}
return -1;
}
/**
* First method is the encoder, second is the decoder. Both methods are
* static. Encoder accepts an object array of property values, and it
* returns a byte array. Decoder accepts an object array to receive
* property values, an encoded byte array, and it returns void.
*
* If generating key encoding and the property count is more than zero,
* then third element in array is a key encoder that supports partial
* encoding. In addition to the object array argument, it also accepts an
* int start and an int end argument. The start of the range is inclusive,
* and the end is exclusive.
*
* @param directions when supplied, build key encoding/decoding. Otherwise,
* build data encoding/decoding.
* @param generation when non-negative, encode a storable layout generation
* value in one or four bytes.
*/
private Method[] generateCodecMethods(GenericEncodingStrategy strategy,
StorableProperty[] properties,
Direction[] directions,
int generation)
throws Exception
{
ClassInjector ci = ClassInjector.create(TestStorable.class.getName(), null);
ClassFile cf = new ClassFile(ci.getClassName());
cf.markSynthetic();
cf.setTarget("1.5");
cf.addDefaultConstructor();
TypeDesc byteArrayType = TypeDesc.forClass(byte[].class);
TypeDesc objectArrayType = TypeDesc.forClass(Object[].class);
// Build encode method.
MethodInfo mi = cf.addMethod(Modifiers.PUBLIC_STATIC, "encode", byteArrayType,
new TypeDesc[] {objectArrayType});
CodeBuilder b = new CodeBuilder(mi);
LocalVariable encodedVar;
if (directions != null) {
OrderedProperty[] ordered =
makeOrderedProperties(properties, directions);
encodedVar =
strategy.buildKeyEncoding(b, ordered, b.getParameter(0), null, false, null, null);
} else {
encodedVar = strategy.buildDataEncoding
(b, properties, b.getParameter(0), null, false, generation);
}
b.loadLocal(encodedVar);
b.returnValue(byteArrayType);
// Build decode method.
mi = cf.addMethod(Modifiers.PUBLIC_STATIC, "decode", null,
new TypeDesc[] {objectArrayType, byteArrayType});
b = new CodeBuilder(mi);
if (directions != null) {
OrderedProperty[] ordered =
makeOrderedProperties(properties, directions);
strategy.buildKeyDecoding
(b, ordered, b.getParameter(0), null, false, b.getParameter(1));
} else {
strategy.buildDataDecoding
(b, properties, b.getParameter(0), null, false,
generation, null, b.getParameter(1));
}
b.returnVoid();
if (directions != null && properties.length > 0) {
// Build encode partial key method.
mi = cf.addMethod
(Modifiers.PUBLIC_STATIC, "encodePartial", byteArrayType,
new TypeDesc[] {objectArrayType, TypeDesc.INT, TypeDesc.INT});
b = new CodeBuilder(mi);
OrderedProperty[] ordered =
makeOrderedProperties(properties, directions);
encodedVar =
strategy.buildKeyEncoding(b, ordered, b.getParameter(0), null, false,
b.getParameter(1), b.getParameter(2));
b.loadLocal(encodedVar);
b.returnValue(byteArrayType);
}
Class> clazz = ci.defineClass(cf);
Method encode = clazz.getMethod("encode", Object[].class);
Method decode = clazz.getMethod("decode", Object[].class, byte[].class);
Method encodePartial = null;
if (directions != null && properties.length > 0) {
encodePartial = clazz.getMethod("encodePartial", Object[].class, int.class, int.class);
}
Method[] methods = new Method[3];
methods[ENCODE_OBJECT_ARRAY] = encode;
methods[DECODE_OBJECT_ARRAY] = decode;
methods[ENCODE_OBJECT_ARRAY_PARTIAL] = encodePartial;
return methods;
}
private StorableProperty[] selectProperties(int minCount, int maxCount) {
int length = (minCount == maxCount) ? minCount
: (mRandom.nextInt(maxCount - minCount + 1) + minCount);
StorableProperty[] selection = new StorableProperty[length];
for (int i=length; --i>=0; ) {
selection[i] = mProperties[mRandom.nextInt(mProperties.length)];
}
return selection;
}
private Direction[] selectDirections(int count) {
Direction[] directions = new Direction[count];
for (int i=count; --i>=0; ) {
Direction dir;
switch (mRandom.nextInt(3)) {
default:
dir = Direction.UNSPECIFIED;
break;
case 1:
dir = Direction.ASCENDING;
break;
case 2:
dir = Direction.DESCENDING;
break;
}
directions[i] = dir;
}
return directions;
}
private OrderedProperty[] makeOrderedProperties
(StorableProperty[] properties, Direction[] directions) {
int length = properties.length;
OrderedProperty[] ordered = new OrderedProperty[length];
for (int i=length; --i>=0; ) {
ordered[i] = OrderedProperty.get(properties[i], directions[i]);
}
return ordered;
}
/**
* Returns an array of the same size with randomly selected values filled
* in that match the property type.
*/
private Object[] selectPropertyValues(StorableProperty>[] properties) {
int length = properties.length;
Object[] values = new Object[length];
for (int i=length; --i>=0; ) {
StorableProperty> property = properties[i];
TypeDesc type = TypeDesc.forClass(property.getType());
Object value;
if (property.isNullable() && mRandom.nextInt(100) == 0) {
value = null;
} else {
TypeDesc prim = type.toPrimitiveType();
if (prim != null) {
switch (prim.getTypeCode()) {
case TypeDesc.BOOLEAN_CODE: default:
value = mRandom.nextBoolean();
break;
case TypeDesc.CHAR_CODE:
value = (char) mRandom.nextInt();
break;
case TypeDesc.FLOAT_CODE:
value = Float.intBitsToFloat(mRandom.nextInt());
break;
case TypeDesc.DOUBLE_CODE:
value = Double.longBitsToDouble(mRandom.nextLong());
break;
case TypeDesc.BYTE_CODE:
value = (byte) mRandom.nextInt();
break;
case TypeDesc.SHORT_CODE:
value = (short) mRandom.nextInt();
break;
case TypeDesc.INT_CODE:
value = mRandom.nextInt();
break;
case TypeDesc.LONG_CODE:
value = mRandom.nextLong();
break;
}
} else if (type == TypeDesc.STRING) {
int len = mRandom.nextInt(100);
StringBuilder sb = new StringBuilder(len);
for (int j=0; j