PERFORMANCE: Remove lut Cache in Accessors

Fixes #7839
This commit is contained in:
Armin 2017-07-28 16:49:51 +02:00 committed by Armin Braun
parent f639df97cd
commit 23877c0b7c
3 changed files with 157 additions and 306 deletions

View file

@ -1,201 +1,157 @@
package org.logstash; package org.logstash;
import java.util.HashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
class Accessors { public final class Accessors {
private final ConvertedMap data; private Accessors() {
protected Map<String, Object> lut; //Utility Class
public Accessors(final ConvertedMap data) {
this.data = data;
this.lut = new HashMap<>(); // reference -> target LUT
} }
public Object get(String reference) { public static Object get(final ConvertedMap data, final String reference) {
FieldReference field = PathCache.cache(reference);
Object target = findTarget(field);
return (target == null) ? null : fetch(target, field.getKey());
}
public Object set(String reference, Object value) {
final FieldReference field = PathCache.cache(reference); final FieldReference field = PathCache.cache(reference);
final Object target = findCreateTarget(field); final Object target = findParent(data, field);
final String key = field.getKey(); return target == null ? null : fetch(target, field.getKey());
if (target instanceof ConvertedMap) {
((ConvertedMap) target).put(key, value);
} else if (target instanceof ConvertedList) {
int i;
try {
i = Integer.parseInt(key);
} catch (NumberFormatException e) {
return null;
}
int size = ((ConvertedList) target).size();
if (i >= size) {
// grow array by adding trailing null items
// this strategy reflects legacy Ruby impl behaviour and is backed by specs
// TODO: (colin) this is potentially dangerous, and could produce OOM using arbitrary big numbers
// TODO: (colin) should be guard against this?
for (int j = size; j < i; j++) {
((List<Object>) target).add(null);
}
((List<Object>) target).add(value);
} else {
int offset = listIndex(i, ((List) target).size());
((ConvertedList) target).set(offset, value);
}
} else {
throw newCollectionException(target);
}
return value;
} }
public Object del(String reference) { public static Object set(final ConvertedMap data, final String reference,
FieldReference field = PathCache.cache(reference); final Object value) {
Object target = findTarget(field); final FieldReference field = PathCache.cache(reference);
if (target != null) { return setChild(findCreateTarget(data, field), field.getKey(), value);
}
public static Object del(final ConvertedMap data, final String reference) {
final FieldReference field = PathCache.cache(reference);
final Object target = findParent(data, field);
if (target instanceof ConvertedMap) { if (target instanceof ConvertedMap) {
return ((ConvertedMap) target).remove(field.getKey()); return ((ConvertedMap) target).remove(field.getKey());
} else if (target instanceof ConvertedList) {
try {
int i = Integer.parseInt(field.getKey());
int offset = listIndex(i, ((List) target).size());
return ((List)target).remove(offset);
} catch (IndexOutOfBoundsException|NumberFormatException e) {
return null;
}
} else { } else {
throw newCollectionException(target); return target == null ? null : delFromList((ConvertedList) target, field.getKey());
} }
} }
return null;
}
public boolean includes(String reference) { public static boolean includes(final ConvertedMap data, final String reference) {
final FieldReference field = PathCache.cache(reference); final FieldReference field = PathCache.cache(reference);
final Object target = findTarget(field); final Object target = findParent(data, field);
final String key = field.getKey(); final String key = field.getKey();
return target instanceof ConvertedMap && ((ConvertedMap) target).containsKey(key) || return target instanceof ConvertedMap && ((ConvertedMap) target).containsKey(key) ||
target instanceof ConvertedList && foundInList(key, (ConvertedList) target); target instanceof ConvertedList && foundInList(key, (ConvertedList) target);
} }
private static boolean foundInList(final String key, final ConvertedList target) { private static Object delFromList(final ConvertedList list, final String key) {
try { try {
return foundInList(target, Integer.parseInt(key)); return list.remove(listIndex(key, list.size()));
} catch (NumberFormatException e) { } catch (IndexOutOfBoundsException | NumberFormatException e) {
return false; return null;
} }
} }
private Object findTarget(FieldReference field) { private static Object setOnList(final String key, final Object value, final ConvertedList list) {
final Object target = this.lut.get(field.getReference()); final int index;
return target != null ? target : cacheTarget(field); try {
} index = Integer.parseInt(key);
} catch (NumberFormatException e) {
private Object cacheTarget(final FieldReference field) { return null;
Object target = this.data; }
for (final String key : field.getPath()) { final int size = list.size();
target = fetch(target, key); if (index >= size) {
if (!isCollection(target)) { appendAtIndex(list, value, index, size);
} else {
list.set(listIndex(index, size), value);
}
return value;
}
private static void appendAtIndex(final ConvertedList list, final Object value, final int index,
final int size) {
// grow array by adding trailing null items
// this strategy reflects legacy Ruby impl behaviour and is backed by specs
// TODO: (colin) this is potentially dangerous, and could produce OOM using arbitrary big numbers
// TODO: (colin) should be guard against this?
for (int i = size; i < index; i++) {
list.add(null);
}
list.add(value);
}
private static Object findParent(final ConvertedMap data, final FieldReference field) {
Object target = data;
for (final String key : field.getPath()) {
target = fetch(target, key);
if (!(target instanceof ConvertedMap || target instanceof ConvertedList)) {
return null; return null;
} }
} }
this.lut.put(field.getReference(), target);
return target; return target;
} }
private Object findCreateTarget(FieldReference field) { private static Object findCreateTarget(final ConvertedMap data, final FieldReference field) {
Object target; Object target = data;
boolean create = false;
// flush the @lut to prevent stale cached fieldref which may point to an old target for (final String key : field.getPath()) {
// which was overwritten with a new value. for example, if "[a][b]" is cached and we Object result;
// set a new value for "[a]" then reading again "[a][b]" would point in a stale target. if (create) {
// flushing the complete @lut is suboptimal, but a hierarchical lut would be required result = createChild((ConvertedMap) target, key);
// to be able to invalidate fieldrefs from a common root. } else {
// see https://github.com/elastic/logstash/pull/5132 result = fetch(target, key);
this.lut.clear(); create = result == null;
if (create) {
target = this.data;
for (String key : field.getPath()) {
Object result = fetch(target, key);
if (result == null) {
result = new ConvertedMap(1); result = new ConvertedMap(1);
if (target instanceof ConvertedMap) { setChild(target, key, result);
((ConvertedMap) target).put(key, result);
} else if (target instanceof ConvertedList) {
try {
int i = Integer.parseInt(key);
// TODO: what about index out of bound?
((ConvertedList) target).set(i, result);
} catch (NumberFormatException e) {
continue;
}
} else if (target != null) {
throw newCollectionException(target);
} }
} }
target = result; target = result;
} }
this.lut.put(field.getReference(), target);
return target; return target;
} }
private static boolean foundInList(ConvertedList target, int index) { private static Object setChild(final Object target, final String key, final Object value) {
try { if (target instanceof Map) {
int offset = listIndex(index, target.size()); ((ConvertedMap) target).put(key, value);
return target.get(offset) != null; return value;
} catch (IndexOutOfBoundsException e) { } else {
return false; return setOnList(key, value, (ConvertedList) target);
}
} }
private static Object createChild(final ConvertedMap target, final String key) {
final Object result = new ConvertedMap(1);
target.put(key, result);
return result;
} }
private static Object fetch(Object target, String key) { private static Object fetch(Object target, String key) {
if (target instanceof ConvertedMap) { return target instanceof ConvertedMap
return ((ConvertedMap) target).get(key); ? ((ConvertedMap) target).get(key) : fetchFromList((ConvertedList) target, key);
} else if (target instanceof ConvertedList) { }
private static Object fetchFromList(final ConvertedList list, final String key) {
try { try {
int offset = listIndex(Integer.parseInt(key), ((ConvertedList) target).size()); return list.get(listIndex(key, list.size()));
return ((ConvertedList) target).get(offset); } catch (IndexOutOfBoundsException | NumberFormatException e) {
} catch (IndexOutOfBoundsException|NumberFormatException e) {
return null; return null;
} }
} else if (target == null) {
return null;
} else {
throw newCollectionException(target);
}
} }
private static boolean isCollection(Object target) { private static boolean foundInList(final String key, final ConvertedList target) {
return target instanceof ConvertedList || target instanceof ConvertedMap; return fetchFromList(target, key) != null;
} }
private static ClassCastException newCollectionException(Object target) { /**
return new ClassCastException("expecting ConvertedList or ConvertedMap, found " + target.getClass());
}
/*
* Returns a positive integer offset for a list of known size. * Returns a positive integer offset for a list of known size.
*
* @param i if positive, and offset from the start of the list. If negative, the offset from the end of the list, where -1 means the last element.
* @param size the size of the list. * @param size the size of the list.
* @return the positive integer offset for the list given by index i. * @return the positive integer offset for the list given by index i.
*/ */
public static int listIndex(int i, int size) { public static int listIndex(int i, int size) {
if (i >= size || i < -size) { return i < 0 ? size + i : i;
throw new IndexOutOfBoundsException("Index " + i + " is out of bounds for a list with size " + size);
} }
if (i < 0) { // Offset from the end of the array. /**
return size + i; * Returns a positive integer offset for a list of known size.
} else { * @param size the size of the list.
return i; * @return the positive integer offset for the list given by index i.
} */
private static int listIndex(final String key, final int size) {
return listIndex(Integer.parseInt(key), size);
} }
} }

View file

@ -26,8 +26,6 @@ public final class Event implements Cloneable, Queueable {
private ConvertedMap data; private ConvertedMap data;
private ConvertedMap metadata; private ConvertedMap metadata;
private Timestamp timestamp; private Timestamp timestamp;
private Accessors accessors;
private Accessors metadata_accessors;
public static final String METADATA = "@metadata"; public static final String METADATA = "@metadata";
public static final String METADATA_BRACKETS = "[" + METADATA + "]"; public static final String METADATA_BRACKETS = "[" + METADATA + "]";
@ -49,8 +47,6 @@ public final class Event implements Cloneable, Queueable {
this.cancelled = false; this.cancelled = false;
this.timestamp = new Timestamp(); this.timestamp = new Timestamp();
this.data.put(TIMESTAMP, this.timestamp); this.data.put(TIMESTAMP, this.timestamp);
this.accessors = new Accessors(this.data);
this.metadata_accessors = new Accessors(this.metadata);
} }
/** /**
@ -79,8 +75,6 @@ public final class Event implements Cloneable, Queueable {
} else { } else {
this.metadata = new ConvertedMap(10); this.metadata = new ConvertedMap(10);
} }
this.metadata_accessors = new Accessors(this.metadata);
this.cancelled = false; this.cancelled = false;
Object providedTimestamp = data.get(TIMESTAMP); Object providedTimestamp = data.get(TIMESTAMP);
@ -89,7 +83,6 @@ public final class Event implements Cloneable, Queueable {
this.timestamp = (parsedTimestamp == null) ? Timestamp.now() : parsedTimestamp; this.timestamp = (parsedTimestamp == null) ? Timestamp.now() : parsedTimestamp;
this.data.put(TIMESTAMP, this.timestamp); this.data.put(TIMESTAMP, this.timestamp);
this.accessors = new Accessors(this.data);
// the tag() method has to be called after the Accessors initialization // the tag() method has to be called after the Accessors initialization
if (parsedTimestamp == null) { if (parsedTimestamp == null) {
@ -106,10 +99,6 @@ public final class Event implements Cloneable, Queueable {
return this.metadata; return this.metadata;
} }
private Accessors getAccessors() {
return this.accessors;
}
public void cancel() { public void cancel() {
this.cancelled = true; this.cancelled = true;
} }
@ -144,23 +133,22 @@ public final class Event implements Cloneable, Queueable {
if (reference.equals(METADATA)) { if (reference.equals(METADATA)) {
return this.metadata; return this.metadata;
} else if (reference.startsWith(METADATA_BRACKETS)) { } else if (reference.startsWith(METADATA_BRACKETS)) {
return this.metadata_accessors.get(reference.substring(METADATA_BRACKETS.length())); return Accessors.get(metadata, reference.substring(METADATA_BRACKETS.length()));
} else { } else {
return this.accessors.get(reference); return Accessors.get(data, reference);
} }
} }
public void setField(String reference, Object value) { public void setField(String reference, Object value) {
if (reference.equals(TIMESTAMP)) { if (reference.equals(TIMESTAMP)) {
// TODO(talevy): check type of timestamp // TODO(talevy): check type of timestamp
this.accessors.set(reference, value); Accessors.set(data, reference, value);
} else if (reference.equals(METADATA_BRACKETS) || reference.equals(METADATA)) { } else if (reference.equals(METADATA_BRACKETS) || reference.equals(METADATA)) {
this.metadata = ConvertedMap.newFromMap((Map) value); this.metadata = ConvertedMap.newFromMap((Map) value);
this.metadata_accessors = new Accessors(this.metadata);
} else if (reference.startsWith(METADATA_BRACKETS)) { } else if (reference.startsWith(METADATA_BRACKETS)) {
this.metadata_accessors.set(reference.substring(METADATA_BRACKETS.length()), value); Accessors.set(metadata, reference.substring(METADATA_BRACKETS.length()), value);
} else { } else {
this.accessors.set(reference, Valuefier.convert(value)); Accessors.set(data, reference, Valuefier.convert(value));
} }
} }
@ -168,9 +156,9 @@ public final class Event implements Cloneable, Queueable {
if (reference.equals(METADATA_BRACKETS) || reference.equals(METADATA)) { if (reference.equals(METADATA_BRACKETS) || reference.equals(METADATA)) {
return true; return true;
} else if (reference.startsWith(METADATA_BRACKETS)) { } else if (reference.startsWith(METADATA_BRACKETS)) {
return this.metadata_accessors.includes(reference.substring(METADATA_BRACKETS.length())); return Accessors.includes(metadata, reference.substring(METADATA_BRACKETS.length()));
} else { } else {
return this.accessors.includes(reference); return Accessors.includes(data, reference);
} }
} }
@ -243,7 +231,6 @@ public final class Event implements Cloneable, Queueable {
public Event overwrite(Event e) { public Event overwrite(Event e) {
this.data = e.getData(); this.data = e.getData();
this.accessors = e.getAccessors();
this.cancelled = e.isCancelled(); this.cancelled = e.isCancelled();
try { try {
this.timestamp = e.getTimestamp(); this.timestamp = e.getTimestamp();
@ -261,7 +248,7 @@ public final class Event implements Cloneable, Queueable {
} }
public Object remove(String path) { public Object remove(String path) {
return this.accessors.del(path); return Accessors.del(data, path);
} }
public String sprintf(String s) throws IOException { public String sprintf(String s) throws IOException {
@ -331,7 +318,7 @@ public final class Event implements Cloneable, Queueable {
} }
public void tag(final String tag) { public void tag(final String tag) {
final Object tags = accessors.get("tags"); final Object tags = Accessors.get(data,"tags");
// short circuit the null case where we know we won't need deduplication step below at the end // short circuit the null case where we know we won't need deduplication step below at the end
if (tags == null) { if (tags == null) {
initTag(tag); initTag(tag);
@ -347,7 +334,7 @@ public final class Event implements Cloneable, Queueable {
private void initTag(final String tag) { private void initTag(final String tag) {
final ConvertedList list = new ConvertedList(1); final ConvertedList list = new ConvertedList(1);
list.add(new StringBiValue(tag)); list.add(new StringBiValue(tag));
accessors.set("tags", list); Accessors.set(data, "tags", list);
} }
/** /**
@ -373,7 +360,7 @@ public final class Event implements Cloneable, Queueable {
// TODO: we should eventually look into using alternate data structures to do more efficient dedup but that will require properly defining the tagging API too // TODO: we should eventually look into using alternate data structures to do more efficient dedup but that will require properly defining the tagging API too
if (!tags.contains(tag)) { if (!tags.contains(tag)) {
tags.add(tag); tags.add(tag);
accessors.set("tags", ConvertedList.newFromList(tags)); Accessors.set(data,"tags", ConvertedList.newFromList(tags));
} }
} }

View file

@ -5,13 +5,7 @@ import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.experimental.theories.DataPoint;
import org.junit.experimental.theories.Theories;
import org.junit.experimental.theories.Theory;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
import org.logstash.bivalues.StringBiValue; import org.logstash.bivalues.StringBiValue;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
@ -21,113 +15,68 @@ import static org.junit.Assert.assertTrue;
public class AccessorsTest { public class AccessorsTest {
private static final class TestableAccessors extends Accessors {
public TestableAccessors(ConvertedMap data) {
super(data);
}
public Object lutGet(String reference) {
return this.lut.get(reference);
}
}
@Test @Test
public void testBareGet() throws Exception { public void testBareGet() throws Exception {
final ConvertedMap data = new ConvertedMap(1); Map<Serializable, Object> data = new HashMap<>();
data.put("foo", "bar"); data.put("foo", "bar");
String reference = "foo"; String reference = "foo";
assertEquals(new StringBiValue("bar"), Accessors.get(ConvertedMap.newFromMap(data), reference));
TestableAccessors accessors = new TestableAccessors(data);
assertNull(accessors.lutGet(reference));
assertEquals("bar", accessors.get(reference));
assertEquals(data, accessors.lutGet(reference));
} }
@Test @Test
public void testAbsentBareGet() throws Exception { public void testAbsentBareGet() throws Exception {
final Map<Serializable, Object> java = new HashMap<>(1); Map<Serializable, Object> data = new HashMap<>();
java.put("foo", "bar"); data.put("foo", "bar");
final ConvertedMap data = ConvertedMap.newFromMap(java);
String reference = "baz"; String reference = "baz";
assertNull(Accessors.get(ConvertedMap.newFromMap(data), reference));
TestableAccessors accessors = new TestableAccessors(data);
assertNull(accessors.lutGet(reference));
assertNull(accessors.get(reference));
assertEquals(data, accessors.lutGet(reference));
} }
@Test @Test
public void testBareBracketsGet() throws Exception { public void testBareBracketsGet() throws Exception {
final Map<Serializable, Object> java = new HashMap<>(1); Map<Serializable, Object> data = new HashMap<>();
java.put("foo", "bar"); data.put("foo", "bar");
final ConvertedMap data = ConvertedMap.newFromMap(java);
String reference = "[foo]"; String reference = "[foo]";
TestableAccessors accessors = new TestableAccessors(data); assertEquals(new StringBiValue("bar"), Accessors.get(ConvertedMap.newFromMap(data), reference));
assertNull(accessors.lutGet(reference));
assertEquals(new StringBiValue("bar"), accessors.get(reference));
assertEquals(data, accessors.lutGet(reference));
} }
@Test @Test
public void testDeepMapGet() throws Exception { public void testDeepMapGet() throws Exception {
final ConvertedMap data = new ConvertedMap(1); Map<Serializable, Object> data = new HashMap<>();
Map<String, Object> inner = new ConvertedMap(1); Map<Serializable, Object> inner = new HashMap<>();
data.put("foo", inner); data.put("foo", inner);
inner.put("bar", "baz"); inner.put("bar", "baz");
String reference = "[foo][bar]"; String reference = "[foo][bar]";
assertEquals(new StringBiValue("baz"), Accessors.get(ConvertedMap.newFromMap(data), reference));
TestableAccessors accessors = new TestableAccessors(data);
assertNull(accessors.lutGet(reference));
assertEquals("baz", accessors.get(reference));
assertEquals(inner, accessors.lutGet(reference));
} }
@Test @Test
public void testAbsentDeepMapGet() throws Exception { public void testAbsentDeepMapGet() throws Exception {
final ConvertedMap data = new ConvertedMap(1); Map<Serializable, Object> data = new HashMap<>();
Map<String, Object> inner = new ConvertedMap(1); Map<Serializable, Object> inner = new HashMap<>();
data.put("foo", inner); data.put("foo", inner);
inner.put("bar", "baz"); inner.put("bar", "baz");
String reference = "[foo][foo]"; String reference = "[foo][foo]";
assertNull(Accessors.get(ConvertedMap.newFromMap(data), reference));
TestableAccessors accessors = new TestableAccessors(data);
assertNull(accessors.lutGet(reference));
assertNull(accessors.get(reference));
assertEquals(inner, accessors.lutGet(reference));
} }
@Test @Test
public void testDeepListGet() throws Exception { public void testDeepListGet() throws Exception {
final ConvertedMap data = new ConvertedMap(1); Map<Serializable, Object> data = new HashMap<>();
List inner = new ConvertedList(1); List inner = new ArrayList();
data.put("foo", inner); data.put("foo", inner);
inner.add("bar"); inner.add("bar");
String reference = "[foo][0]"; String reference = "[foo][0]";
assertEquals(new StringBiValue("bar"), Accessors.get(ConvertedMap.newFromMap(data), reference));
TestableAccessors accessors = new TestableAccessors(data);
assertNull(accessors.lutGet(reference));
assertEquals("bar", accessors.get(reference));
assertEquals(inner, accessors.lutGet(reference));
} }
@Test @Test
public void testAbsentDeepListGet() throws Exception { public void testAbsentDeepListGet() throws Exception {
final ConvertedMap data = new ConvertedMap(1); Map<Serializable, Object> data = new HashMap<>();
List inner = new ConvertedList(1); List inner = new ArrayList();
data.put("foo", inner); data.put("foo", inner);
inner.add("bar"); inner.add("bar");
String reference = "[foo][1]"; String reference = "[foo][1]";
assertNull(Accessors.get(ConvertedMap.newFromMap(data), reference));
TestableAccessors accessors = new TestableAccessors(data);
assertNull(accessors.lutGet(reference));
assertNull(accessors.get(reference));
assertEquals(inner, accessors.lutGet(reference));
} }
/* /*
* Check if accessors are able to recovery from * Check if accessors are able to recovery from
@ -138,32 +87,25 @@ public class AccessorsTest {
@Test @Test
public void testInvalidIdList() throws Exception { public void testInvalidIdList() throws Exception {
final ConvertedMap data = new ConvertedMap(1); final ConvertedMap data = new ConvertedMap(1);
List inner = new ConvertedList(1); List inner = new ConvertedList(2);
data.put("map1", inner); data.put("map1", inner);
inner.add("obj1"); inner.add("obj1");
inner.add("obj2"); inner.add("obj2");
String reference = "[map1][IdNonNumeric]"; String reference = "[map1][IdNonNumeric]";
TestableAccessors accessors = new TestableAccessors(data); assertNull(Accessors.get(data, reference));
assertNull(accessors.lutGet(reference)); assertNull(Accessors.set(data, reference, "obj3"));
assertNull(accessors.get(reference)); assertFalse(Accessors.includes(data, reference));
assertNull(accessors.set(reference, "obj3")); assertNull(Accessors.del(data, reference));
assertEquals(inner, accessors.lutGet(reference));
assertFalse(accessors.includes(reference));
assertNull(accessors.del(reference));
} }
@Test @Test
public void testBarePut() throws Exception { public void testBarePut() throws Exception {
final ConvertedMap data = new ConvertedMap(1); final ConvertedMap data = new ConvertedMap(1);
String reference = "foo"; String reference = "foo";
assertEquals("bar", Accessors.set(data, reference, "bar"));
TestableAccessors accessors = new TestableAccessors(data); assertEquals("bar", Accessors.get(data, reference));
assertNull(accessors.lutGet(reference));
assertEquals("bar", accessors.set(reference, "bar"));
assertEquals(data, accessors.lutGet(reference));
assertEquals("bar", accessors.get(reference));
} }
@Test @Test
@ -171,11 +113,8 @@ public class AccessorsTest {
final ConvertedMap data = new ConvertedMap(1); final ConvertedMap data = new ConvertedMap(1);
String reference = "[foo]"; String reference = "[foo]";
TestableAccessors accessors = new TestableAccessors(data); assertEquals("bar", Accessors.set(data, reference, "bar"));
assertNull(accessors.lutGet(reference)); assertEquals("bar", Accessors.get(data, reference));
assertEquals("bar", accessors.set(reference, "bar"));
assertEquals(data, accessors.lutGet(reference));
assertEquals("bar", accessors.get(reference));
} }
@Test @Test
@ -184,11 +123,8 @@ public class AccessorsTest {
String reference = "[foo][bar]"; String reference = "[foo][bar]";
TestableAccessors accessors = new TestableAccessors(data); assertEquals("baz", Accessors.set(data, reference, "baz"));
assertNull(accessors.lutGet(reference)); assertEquals("baz", Accessors.get(data, reference));
assertEquals("baz", accessors.set(reference, "baz"));
assertEquals(accessors.lutGet(reference), data.get("foo"));
assertEquals("baz", accessors.get(reference));
} }
@Test @Test
@ -198,44 +134,40 @@ public class AccessorsTest {
data.put("foo", inner); data.put("foo", inner);
inner.add("bar"); inner.add("bar");
data.put("bar", "baz"); data.put("bar", "baz");
TestableAccessors accessors = new TestableAccessors(data);
assertEquals("bar", accessors.del("[foo][0]")); assertEquals("bar", Accessors.del(data, "[foo][0]"));
assertNull(accessors.del("[foo][0]")); assertNull(Accessors.del(data, "[foo][0]"));
assertEquals(new ArrayList<>(), accessors.get("[foo]")); assertEquals(new ConvertedList(0), Accessors.get(data,"[foo]"));
assertEquals("baz", accessors.del("[bar]")); assertEquals("baz", Accessors.del(data, "[bar]"));
assertNull(accessors.get("[bar]")); assertNull(Accessors.get(data, "[bar]"));
} }
@Test @Test
public void testNilInclude() throws Exception { public void testNilInclude() throws Exception {
ConvertedMap data = new ConvertedMap(1); final ConvertedMap data = new ConvertedMap(1);
data.put("nilfield", null); data.put("nilfield", null);
TestableAccessors accessors = new TestableAccessors(data); assertTrue(Accessors.includes(data, "nilfield"));
assertTrue(accessors.includes("nilfield"));
} }
@Test @Test
public void testInvalidPath() throws Exception { public void testInvalidPath() throws Exception {
ConvertedMap data = new ConvertedMap(1); final ConvertedMap data = new ConvertedMap(1);
Accessors accessors = new Accessors(data);
assertEquals(1, accessors.set("[foo]", 1)); assertEquals(1, Accessors.set(data, "[foo]", 1));
assertNull(accessors.get("[foo][bar]")); assertNull(Accessors.get(data, "[foo][bar]"));
} }
@Test @Test
public void testStaleTargetCache() throws Exception { public void testStaleTargetCache() throws Exception {
ConvertedMap data = new ConvertedMap(1); final ConvertedMap data = new ConvertedMap(1);
Accessors accessors = new Accessors(data); assertNull(Accessors.get(data,"[foo][bar]"));
assertNull(accessors.get("[foo][bar]")); assertEquals("baz", Accessors.set(data,"[foo][bar]", "baz"));
assertEquals("baz", accessors.set("[foo][bar]", "baz")); assertEquals("baz", Accessors.get(data, "[foo][bar]"));
assertEquals("baz", accessors.get("[foo][bar]"));
assertEquals("boom", accessors.set("[foo]", "boom")); assertEquals("boom", Accessors.set(data, "[foo]", "boom"));
assertNull(accessors.get("[foo][bar]")); assertNull(Accessors.get(data, "[foo][bar]"));
assertEquals("boom", accessors.get("[foo]")); assertEquals("boom", Accessors.get(data,"[foo]"));
} }
@Test @Test
@ -247,28 +179,4 @@ public class AccessorsTest {
assertEquals(1, Accessors.listIndex(-9, 10)); assertEquals(1, Accessors.listIndex(-9, 10));
assertEquals(0, Accessors.listIndex(-10, 10)); assertEquals(0, Accessors.listIndex(-10, 10));
} }
@RunWith(Theories.class)
public static class TestListIndexFailureCases {
private static final int size = 10;
@DataPoint
public static final int tooLarge = size;
@DataPoint
public static final int tooLarge1 = size+1;
@DataPoint
public static final int tooLargeNegative = -size - 1;
@Rule
public ExpectedException exception = ExpectedException.none();
@Theory
public void testListIndexOutOfBounds(int i) {
exception.expect(IndexOutOfBoundsException.class);
Accessors.listIndex(i, size);
}
}
} }