aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLibravatar Kristóf Marussy <kristof@marussy.com>2022-12-14 00:57:24 +0100
committerLibravatar Kristóf Marussy <kristof@marussy.com>2022-12-21 18:34:11 +0100
commitbfc9e33a54985797968708176690a9ecfc207f83 (patch)
tree600b4508eb1289c4e30a4f6e0aecfb9d5d56cefd
parentchore(deps): bump dependencies (diff)
downloadrefinery-bfc9e33a54985797968708176690a9ecfc207f83.tar.gz
refinery-bfc9e33a54985797968708176690a9ecfc207f83.tar.zst
refinery-bfc9e33a54985797968708176690a9ecfc207f83.zip
refactor(store): compare VersionedMap instances
The Java hashCode and equals API is inappropriate here, because an AnyVersionedMap is mutable. Added new methods to hash and compare AnyVersionedMap instances by their contents and marked the built-in Java methods as deprecated.
-rw-r--r--subprojects/store/src/main/java/tools/refinery/store/map/AnyVersionedMap.java35
-rw-r--r--subprojects/store/src/main/java/tools/refinery/store/map/ContentHashCode.java19
-rw-r--r--subprojects/store/src/main/java/tools/refinery/store/map/internal/MapCursor.java5
-rw-r--r--subprojects/store/src/main/java/tools/refinery/store/map/internal/VersionedMapImpl.java12
-rw-r--r--subprojects/store/src/test/java/tools/refinery/store/map/tests/fuzz/ContentEqualsFuzzTest.java56
-rw-r--r--subprojects/store/src/test/java/tools/refinery/store/map/tests/utils/MapTestEnvironment.java66
6 files changed, 135 insertions, 58 deletions
diff --git a/subprojects/store/src/main/java/tools/refinery/store/map/AnyVersionedMap.java b/subprojects/store/src/main/java/tools/refinery/store/map/AnyVersionedMap.java
index b94b9f38..f82a8bb1 100644
--- a/subprojects/store/src/main/java/tools/refinery/store/map/AnyVersionedMap.java
+++ b/subprojects/store/src/main/java/tools/refinery/store/map/AnyVersionedMap.java
@@ -2,4 +2,39 @@ package tools.refinery.store.map;
2 2
3public sealed interface AnyVersionedMap extends Versioned permits VersionedMap { 3public sealed interface AnyVersionedMap extends Versioned permits VersionedMap {
4 long getSize(); 4 long getSize();
5
6 int contentHashCode(ContentHashCode mode);
7
8 boolean contentEquals(AnyVersionedMap other);
9
10 /**
11 * Returns a hash code value for the object.
12 *
13 * @return A hash code value for this object.
14 * @deprecated {@link AnyVersionedMap} instances are mutable, and it is inappropriate to use them as keys in
15 * hash-based collections. Use {@link AnyVersionedMap#contentHashCode(ContentHashCode)} to compute a
16 * <code>hashCode</code> for a {@link AnyVersionedMap} instance according to its contents.
17 */
18 @Override
19 // This method is mark as @Deprecated to prevent inappropriate use of built-in Java API.
20 // Therefore, we don't follow normal procedures for removing deprecated code.
21 @SuppressWarnings("squid:S1133")
22 @Deprecated(since = "0.0.0")
23 int hashCode();
24
25 /**
26 * Compares two objects by reference.
27 *
28 * @param obj The reference object with which to compare.
29 * @return <code>true</code> if this object is the same as the <code>obj</code> argument.
30 * @deprecated {@link AnyVersionedMap} instances are mutable, and it is inappropriate to use them as keys in
31 * hash-based collections. Use {@link AnyVersionedMap#contentEquals(AnyVersionedMap)} to compare two
32 * {@link AnyVersionedMap} instances by their contents.
33 */
34 @Override
35 // This method is mark as @Deprecated to prevent inappropriate use of built-in Java API.
36 // Therefore, we don't follow normal procedures for removing deprecated code.
37 @SuppressWarnings("squid:S1133")
38 @Deprecated(since = "0.0.0")
39 boolean equals(Object obj);
5} 40}
diff --git a/subprojects/store/src/main/java/tools/refinery/store/map/ContentHashCode.java b/subprojects/store/src/main/java/tools/refinery/store/map/ContentHashCode.java
new file mode 100644
index 00000000..8deeab23
--- /dev/null
+++ b/subprojects/store/src/main/java/tools/refinery/store/map/ContentHashCode.java
@@ -0,0 +1,19 @@
1package tools.refinery.store.map;
2
3public enum ContentHashCode {
4 /**
5 * Calculate the <code>hashCode</code> of the contents of this {@link AnyVersionedMap} to reduce hash collisions as
6 * much as possible, even iterating over the full contents in necessary.
7 */
8 PRECISE_SLOW,
9
10 /**
11 * Compute an approximate <code>hashCode</code> of the contents of this {@link AnyVersionedMap} that may have a
12 * large number of hash collisions, but can be calculated without iterating over the full contents.
13 * <p>
14 * In the extreme case, {@link AnyVersionedMap#contentHashCode(ContentHashCode)} may return the same
15 * <code>hashCode</code> irrespectively of the actual contents of the {@link AnyVersionedMap}.
16 * </p>
17 */
18 APPROXIMATE_FAST
19}
diff --git a/subprojects/store/src/main/java/tools/refinery/store/map/internal/MapCursor.java b/subprojects/store/src/main/java/tools/refinery/store/map/internal/MapCursor.java
index 1698830f..91a71e3d 100644
--- a/subprojects/store/src/main/java/tools/refinery/store/map/internal/MapCursor.java
+++ b/subprojects/store/src/main/java/tools/refinery/store/map/internal/MapCursor.java
@@ -1,6 +1,7 @@
1package tools.refinery.store.map.internal; 1package tools.refinery.store.map.internal;
2 2
3import tools.refinery.store.map.AnyVersionedMap; 3import tools.refinery.store.map.AnyVersionedMap;
4import tools.refinery.store.map.ContentHashCode;
4import tools.refinery.store.map.Cursor; 5import tools.refinery.store.map.Cursor;
5import tools.refinery.store.map.VersionedMap; 6import tools.refinery.store.map.VersionedMap;
6 7
@@ -45,7 +46,7 @@ public class MapCursor<K, V> implements Cursor<K, V> {
45 46
46 // Initializing state 47 // Initializing state
47 this.map = map; 48 this.map = map;
48 this.creationHash = map.hashCode(); 49 this.creationHash = map.contentHashCode(ContentHashCode.APPROXIMATE_FAST);
49 } 50 }
50 51
51 public K getKey() { 52 public K getKey() {
@@ -87,7 +88,7 @@ public class MapCursor<K, V> implements Cursor<K, V> {
87 88
88 @Override 89 @Override
89 public boolean isDirty() { 90 public boolean isDirty() {
90 return this.map.hashCode() != this.creationHash; 91 return this.map.contentHashCode(ContentHashCode.APPROXIMATE_FAST) != this.creationHash;
91 } 92 }
92 93
93 @Override 94 @Override
diff --git a/subprojects/store/src/main/java/tools/refinery/store/map/internal/VersionedMapImpl.java b/subprojects/store/src/main/java/tools/refinery/store/map/internal/VersionedMapImpl.java
index 866e7b33..301bcb95 100644
--- a/subprojects/store/src/main/java/tools/refinery/store/map/internal/VersionedMapImpl.java
+++ b/subprojects/store/src/main/java/tools/refinery/store/map/internal/VersionedMapImpl.java
@@ -5,6 +5,7 @@ import tools.refinery.store.map.*;
5import java.util.Iterator; 5import java.util.Iterator;
6import java.util.LinkedList; 6import java.util.LinkedList;
7import java.util.List; 7import java.util.List;
8import java.util.Objects;
8 9
9/** 10/**
10 * Not threadSafe in itself 11 * Not threadSafe in itself
@@ -145,4 +146,15 @@ public class VersionedMapImpl<K, V> implements VersionedMap<K, V> {
145 this.root.checkIntegrity(hashProvider, defaultValue, 0); 146 this.root.checkIntegrity(hashProvider, defaultValue, 0);
146 } 147 }
147 } 148 }
149
150 @Override
151 public int contentHashCode(ContentHashCode mode) {
152 // Calculating the root hashCode is always fast, because {@link Node} caches its hashCode.
153 return Objects.hashCode(root);
154 }
155
156 @Override
157 public boolean contentEquals(AnyVersionedMap other) {
158 return other instanceof VersionedMapImpl<?, ?> otherImpl && Objects.equals(root, otherImpl.root);
159 }
148} 160}
diff --git a/subprojects/store/src/test/java/tools/refinery/store/map/tests/fuzz/ContentEqualsFuzzTest.java b/subprojects/store/src/test/java/tools/refinery/store/map/tests/fuzz/ContentEqualsFuzzTest.java
index d7f77d1a..93ecfec3 100644
--- a/subprojects/store/src/test/java/tools/refinery/store/map/tests/fuzz/ContentEqualsFuzzTest.java
+++ b/subprojects/store/src/test/java/tools/refinery/store/map/tests/fuzz/ContentEqualsFuzzTest.java
@@ -17,12 +17,11 @@ import java.util.List;
17import java.util.Random; 17import java.util.Random;
18import java.util.stream.Stream; 18import java.util.stream.Stream;
19 19
20import static org.junit.jupiter.api.Assertions.assertEquals; 20import static org.junit.jupiter.api.Assertions.*;
21import static org.junit.jupiter.api.Assertions.fail;
22 21
23class ContentEqualsFuzzTest { 22class ContentEqualsFuzzTest {
24 private void runFuzzTest(String scenario, int seed, int steps, int maxKey, int maxValue, int commitFrequency, 23 private void runFuzzTest(String scenario, int seed, int steps, int maxKey, int maxValue, int commitFrequency,
25 boolean evilHash) { 24 boolean evilHash) {
26 String[] values = MapTestEnvironment.prepareValues(maxValue); 25 String[] values = MapTestEnvironment.prepareValues(maxValue);
27 ContinousHashProvider<Integer> chp = MapTestEnvironment.prepareHashProvider(evilHash); 26 ContinousHashProvider<Integer> chp = MapTestEnvironment.prepareHashProvider(evilHash);
28 27
@@ -31,7 +30,9 @@ class ContentEqualsFuzzTest {
31 iterativeRandomPutsAndCommitsThenCompare(scenario, chp, steps, maxKey, values, r, commitFrequency); 30 iterativeRandomPutsAndCommitsThenCompare(scenario, chp, steps, maxKey, values, r, commitFrequency);
32 } 31 }
33 32
34 private void iterativeRandomPutsAndCommitsThenCompare(String scenario, ContinousHashProvider<Integer> chp, int steps, int maxKey, String[] values, Random r, int commitFrequency) { 33 private void iterativeRandomPutsAndCommitsThenCompare(String scenario, ContinousHashProvider<Integer> chp,
34 int steps, int maxKey, String[] values, Random r,
35 int commitFrequency) {
35 VersionedMapStore<Integer, String> store1 = new VersionedMapStoreImpl<Integer, String>(chp, values[0]); 36 VersionedMapStore<Integer, String> store1 = new VersionedMapStoreImpl<Integer, String>(chp, values[0]);
36 VersionedMap<Integer, String> sut1 = store1.createMap(); 37 VersionedMap<Integer, String> sut1 = store1.createMap();
37 38
@@ -67,58 +68,43 @@ class ContentEqualsFuzzTest {
67 int index2 = 1; 68 int index2 = 1;
68 for (SimpleEntry<Integer, String> entry : content) { 69 for (SimpleEntry<Integer, String> entry : content) {
69 sut2.put(entry.getKey(), entry.getValue()); 70 sut2.put(entry.getKey(), entry.getValue());
70 if(index2++%commitFrequency == 0) 71 if (index2++ % commitFrequency == 0)
71 sut2.commit(); 72 sut2.commit();
72 } 73 }
73 74
74 // Check the integrity of the maps 75 // Check the integrity of the maps
75 ((VersionedMapImpl<Integer,String>) sut1).checkIntegrity(); 76 ((VersionedMapImpl<Integer, String>) sut1).checkIntegrity();
76 ((VersionedMapImpl<Integer,String>) sut2).checkIntegrity(); 77 ((VersionedMapImpl<Integer, String>) sut2).checkIntegrity();
77 78
78// // Compare the two maps 79 // Compare the two maps
79 // By size 80 MapTestEnvironment.compareTwoMaps(scenario, sut1, sut2);
80 assertEquals(sut1.getSize(), content.size());
81 assertEquals(sut2.getSize(), content.size());
82
83 // By cursors
84 Cursor<Integer, String> cursor1 = sut1.getAll();
85 Cursor<Integer, String> cursor2 = sut2.getAll();
86 int index3 = 1;
87 boolean canMove = true;
88 do{
89 boolean canMove1 = cursor1.move();
90 boolean canMove2 = cursor2.move();
91 assertEquals(canMove1, canMove2, scenario + ":" + index3 +" Cursors stopped at different times!");
92 assertEquals(cursor1.getKey(), cursor2.getKey(), scenario + ":" + index3 +" Cursors have different keys!");
93 assertEquals(cursor1.getValue(), cursor2.getValue(), scenario + ":" + index3 +" Cursors have different values!");
94
95 canMove = canMove1;
96 MapTestEnvironment.printStatus(scenario, index3++, content.size(), "Compare");
97 } while (canMove);
98 } 81 }
99 82
100 @ParameterizedTest(name = "Compare {index}/{0} Steps={1} Keys={2} Values={3} commit frequency={4} seed={5} evil-hash={6}") 83 @ParameterizedTest(name = "Compare {index}/{0} Steps={1} Keys={2} Values={3} commit frequency={4} seed={5} " +
84 "evil-hash={6}")
101 @MethodSource 85 @MethodSource
102 @Timeout(value = 10) 86 @Timeout(value = 10)
103 @Tag("fuzz") 87 @Tag("fuzz")
104 void parametrizedFastFuzz(int tests, int steps, int noKeys, int noValues, int commitFrequency, int seed, 88 void parametrizedFastFuzz(int tests, int steps, int noKeys, int noValues, int commitFrequency, int seed,
105 boolean evilHash) { 89 boolean evilHash) {
106 runFuzzTest("CompareS" + steps + "K" + noKeys + "V" + noValues + "s" + seed, seed, steps, noKeys, noValues, 90 runFuzzTest("CompareS" + steps + "K" + noKeys + "V" + noValues + "s" + seed, seed, steps, noKeys, noValues,
107 commitFrequency, evilHash); 91 commitFrequency, evilHash);
108 } 92 }
109 93
110 static Stream<Arguments> parametrizedFastFuzz() { 94 static Stream<Arguments> parametrizedFastFuzz() {
111 return FuzzTestUtils.permutationWithSize(new Object[] { FuzzTestUtils.FAST_STEP_COUNT }, new Object[] { 3, 32, 32 * 32 }, 95 return FuzzTestUtils.permutationWithSize(new Object[]{FuzzTestUtils.FAST_STEP_COUNT}, new Object[]{3, 32,
112 new Object[] { 2, 3 }, new Object[] { 1, 10, 100 }, new Object[] { 1, 2, 3 }, 96 32 * 32},
113 new Object[] { false, true }); 97 new Object[]{2, 3}, new Object[]{1, 10, 100}, new Object[]{1, 2, 3},
98 new Object[]{false, true});
114 } 99 }
115 100
116 @ParameterizedTest(name = "Compare {index}/{0} Steps={1} Keys={2} Values={3} commit frequency={4} seed={5} evil-hash={6}") 101 @ParameterizedTest(name = "Compare {index}/{0} Steps={1} Keys={2} Values={3} commit frequency={4} seed={5} " +
102 "evil-hash={6}")
117 @MethodSource 103 @MethodSource
118 @Tag("fuzz") 104 @Tag("fuzz")
119 @Tag("slow") 105 @Tag("slow")
120 void parametrizedSlowFuzz(int tests, int steps, int noKeys, int noValues, int commitFrequency, int seed, 106 void parametrizedSlowFuzz(int tests, int steps, int noKeys, int noValues, int commitFrequency, int seed,
121 boolean evilHash) { 107 boolean evilHash) {
122 runFuzzTest("CompareS" + steps + "K" + noKeys + "V" + noValues + "s" + seed, seed, steps, noKeys, noValues, 108 runFuzzTest("CompareS" + steps + "K" + noKeys + "V" + noValues + "s" + seed, seed, steps, noKeys, noValues,
123 commitFrequency, evilHash); 109 commitFrequency, evilHash);
124 } 110 }
diff --git a/subprojects/store/src/test/java/tools/refinery/store/map/tests/utils/MapTestEnvironment.java b/subprojects/store/src/test/java/tools/refinery/store/map/tests/utils/MapTestEnvironment.java
index b73f8b32..2d03ebaf 100644
--- a/subprojects/store/src/test/java/tools/refinery/store/map/tests/utils/MapTestEnvironment.java
+++ b/subprojects/store/src/test/java/tools/refinery/store/map/tests/utils/MapTestEnvironment.java
@@ -1,8 +1,6 @@
1package tools.refinery.store.map.tests.utils; 1package tools.refinery.store.map.tests.utils;
2 2
3import tools.refinery.store.map.ContinousHashProvider; 3import tools.refinery.store.map.*;
4import tools.refinery.store.map.Cursor;
5import tools.refinery.store.map.VersionedMap;
6import tools.refinery.store.map.internal.VersionedMapImpl; 4import tools.refinery.store.map.internal.VersionedMapImpl;
7 5
8import java.util.*; 6import java.util.*;
@@ -50,40 +48,65 @@ public class MapTestEnvironment<K, V> {
50 48
51 } 49 }
52 50
53 public static <K, V> void compareTwoMaps(String title, VersionedMapImpl<K, V> map1, 51 public static <K, V> void compareTwoMaps(String title, VersionedMap<K, V> map1,
54 VersionedMapImpl<K, V> map2) { 52 VersionedMap<K, V> map2) {
55 compareTwoMaps(title, map1, map2, null); 53 compareTwoMaps(title, map1, map2, null);
56 } 54 }
57 public static <K, V> void compareTwoMaps(String title, VersionedMapImpl<K, V> map1, 55
58 VersionedMapImpl<K, V> map2, List<Throwable> errors) { 56 public static <K, V> void compareTwoMaps(String title, VersionedMap<K, V> map1,
57 VersionedMap<K, V> map2, List<Throwable> errors) {
58 assertEqualsList(map1.getSize(), map2.getSize(), title + ": Sizes not equal", errors);
59
59 Cursor<K, V> cursor1 = map1.getAll(); 60 Cursor<K, V> cursor1 = map1.getAll();
60 Cursor<K, V> cursor2 = map2.getAll(); 61 Cursor<K, V> cursor2 = map2.getAll();
61 while (!cursor1.isTerminated()) { 62 while (!cursor1.isTerminated()) {
62 if (cursor2.isTerminated()) { 63 if (cursor2.isTerminated()) {
63 fail("cursor 2 terminated before cursor1"); 64 fail("cursor 2 terminated before cursor1");
64 } 65 }
65 assertEqualsList(cursor1.getKey(), cursor2.getKey(),"Keys not equal", errors); 66 assertEqualsList(cursor1.getKey(), cursor2.getKey(), title + ": Keys not equal", errors);
66 assertEqualsList(cursor2.getValue(), cursor2.getValue(), "Values not equal", errors); 67 assertEqualsList(cursor2.getValue(), cursor2.getValue(), title + ": Values not equal", errors);
67 cursor1.move(); 68 cursor1.move();
68 cursor2.move(); 69 cursor2.move();
69 } 70 }
70 if (!cursor2.isTerminated()) 71 if (!cursor2.isTerminated()) {
71 fail("cursor 1 terminated before cursor 2"); 72 fail("cursor 1 terminated before cursor 2");
73 }
74
75 for (var mode : ContentHashCode.values()) {
76 assertEqualsList(map1.contentHashCode(mode), map2.contentHashCode(mode),
77 title + ": " + mode + " hashCode check", errors);
78 }
79 assertContentEqualsList(map1, map2, title + ": map1.contentEquals(map2)", errors);
80 assertContentEqualsList(map2, map1, title + ": map2.contentEquals(map1)", errors);
72 } 81 }
73 82
74 private static void assertEqualsList(Object o1, Object o2, String message, List<Throwable> errors) { 83 private static void assertEqualsList(Object o1, Object o2, String message, List<Throwable> errors) {
75 if(errors == null) { 84 if (errors == null) {
76 assertEquals(o1, o2, message); 85 assertEquals(o1, o2, message);
77 } else { 86 } else {
78 if(o1 != null) { 87 if (o1 != null) {
79 if(!(o1.equals(o2))) { 88 if (!(o1.equals(o2))) {
80 AssertionError error = new AssertionError((message != null ? message+" " : "") + "expected: " + o1 + " but was : " + o2); 89 AssertionError error =
90 new AssertionError((message != null ? message + " " : "") + "expected: " + o1 + " but was " +
91 ": " + o2);
81 errors.add(error); 92 errors.add(error);
82 } 93 }
83 } 94 }
84 } 95 }
85 } 96 }
86 97
98 private static void assertContentEqualsList(AnyVersionedMap o1, AnyVersionedMap o2, String message,
99 List<Throwable> errors) {
100 boolean result = o1.contentEquals(o2);
101 if (errors == null) {
102 assertTrue(result, message);
103 } else if (!result) {
104 AssertionError error =
105 new AssertionError((message != null ? message + " " : "") + "expected: true but was: false");
106 errors.add(error);
107 }
108 }
109
87 public VersionedMapImpl<K, V> sut; 110 public VersionedMapImpl<K, V> sut;
88 Map<K, V> oracle = new HashMap<K, V>(); 111 Map<K, V> oracle = new HashMap<K, V>();
89 112
@@ -99,10 +122,10 @@ public class MapTestEnvironment<K, V> {
99 } else { 122 } else {
100 oldOracleValue = oracle.remove(key); 123 oldOracleValue = oracle.remove(key);
101 } 124 }
102 if(oldSutValue == sut.getDefaultValue() && oldOracleValue != null) { 125 if (oldSutValue == sut.getDefaultValue() && oldOracleValue != null) {
103 fail("After put, SUT old nodeId was default, but oracle old value was " + oldOracleValue); 126 fail("After put, SUT old nodeId was default, but oracle old value was " + oldOracleValue);
104 } 127 }
105 if(oldSutValue != sut.getDefaultValue()) { 128 if (oldSutValue != sut.getDefaultValue()) {
106 assertEquals(oldOracleValue, oldSutValue); 129 assertEquals(oldOracleValue, oldSutValue);
107 } 130 }
108 } 131 }
@@ -158,14 +181,15 @@ public class MapTestEnvironment<K, V> {
158 } 181 }
159 } 182 }
160 183
161 public static <K,V> void checkOrder(String scenario, VersionedMap<K,V> versionedMap) { 184 public static <K, V> void checkOrder(String scenario, VersionedMap<K, V> versionedMap) {
162 K previous = null; 185 K previous = null;
163 Cursor<K, V> cursor = versionedMap.getAll(); 186 Cursor<K, V> cursor = versionedMap.getAll();
164 while(cursor.move()) { 187 while (cursor.move()) {
165 System.out.println(cursor.getKey() + " " + ((VersionedMapImpl<K, V>) versionedMap).getHashProvider().getHash(cursor.getKey(), 0)); 188 System.out.println(cursor.getKey() + " " + ((VersionedMapImpl<K, V>) versionedMap).getHashProvider().getHash(cursor.getKey(), 0));
166 if(previous != null) { 189 if (previous != null) {
167 int comparisonResult = ((VersionedMapImpl<K, V>) versionedMap).getHashProvider().compare(previous, cursor.getKey()); 190 int comparisonResult = ((VersionedMapImpl<K, V>) versionedMap).getHashProvider().compare(previous,
168 assertTrue(comparisonResult<0,scenario+" Cursor order is not incremental!"); 191 cursor.getKey());
192 assertTrue(comparisonResult < 0, scenario + " Cursor order is not incremental!");
169 } 193 }
170 previous = cursor.getKey(); 194 previous = cursor.getKey();
171 } 195 }