├── .gitignore ├── LICENSE ├── README.md ├── build.gradle └── src ├── main └── java │ └── crdt │ ├── CRDT.java │ ├── counters │ ├── GCounter.java │ ├── GState.java │ └── PNCounter.java │ └── sets │ ├── GSet.java │ ├── ORSet.java │ └── TwoPSet.java └── test └── java └── crdt ├── counters ├── GCounterTest.java ├── GStateTest.java └── PNCounterTest.java └── sets ├── GSetTest.java ├── ORSetTest.java └── TwoPSetTest.java /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | *.jar 3 | .classpath 4 | .project 5 | .settings/ 6 | .gradle/ 7 | bin/ 8 | build/ 9 | target/ 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Andrejs Jermakovics 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | CRDTs 2 | ================ 3 | 4 | Simple [Conflict-free Replicated Data Types](http://en.wikipedia.org/wiki/Conflict-free_replicated_data_type) (CRDTs) for distributed systems. CRDTs on different replicas can diverge from one another but at the end they can be safely merged providing an eventually consistent value. In other words, CRDTs have a **merge** method that is *idempotent*, *commutative* and *associative*. 5 | 6 | The following CRDTs are currently implemented: 7 | - G-Counter - Grow only counter 8 | - PN-Counter - Increment/decrement counter 9 | - G-Set - Grow only set. Allows only adds 10 | - 2P-Set - Two Phase Set. Allows adds and removes but an item can be removed only once 11 | - OR-Set- Ovserved-Removed Set. Allows multiple adds and removes. Adds win in case of a conflict during merge. 12 | 13 | Examples 14 | =========== 15 | PN-Counter: 16 | ```java 17 | PNCounter replica1 = new PNCounter<>(); 18 | replica1.increment("hostname1"); 19 | 20 | PNCounter replica2 = replica1.copy(); 21 | 22 | replica1.increment("hostname2"); 23 | replica2.decrement("hostname2"); 24 | 25 | replica1.merge( replica2 ); 26 | replica1.get(); // counter is 1 27 | ``` 28 | 2-P Set: 29 | ```java 30 | TwoPSet replica1 = new TwoPSet<>(); 31 | 32 | replica1.add("a"); 33 | replica1.add("b"); 34 | 35 | TwoPSet replica2 = replica1.copy(); 36 | replica2.remove("b"); 37 | replica2.add("c"); 38 | 39 | replica1.merge(replica2); 40 | replica1.get(); // set is {"a", "c"} 41 | ``` 42 | 43 | Download 44 | ======== 45 | 46 | Maven/Gradle dependency: https://jitpack.io/#ajermakovics/crdts 47 | 48 | 49 | Serialization 50 | =========== 51 | 52 | All CRDT classes implement Java Serializable but you should be able to use any other library. 53 | 54 | Requirements 55 | ============ 56 | 57 | Java 1.6 58 | The only dependency is Google Guava 59 | 60 | More info 61 | ========= 62 | - http://book.mixu.net/distsys/eventual.html 63 | - [A comprehensive study of 64 | Convergent and Commutative Replicated Data Types](http://hal.upmc.fr/file/index/docid/555588/filename/techreport.pdf) 65 | 66 | License 67 | ======= 68 | 69 | MIT 70 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'java' 2 | apply plugin: 'eclipse' 3 | apply plugin: 'maven' 4 | 5 | sourceCompatibility = 1.6 6 | targetCompatibility = 1.6 7 | 8 | group = 'crdts' 9 | version = '1.0' 10 | 11 | jar { 12 | manifest { 13 | attributes 'Implementation-Title': 'CRDTs', 'Implementation-Version': version 14 | } 15 | } 16 | 17 | repositories { 18 | mavenCentral() 19 | mavenLocal() 20 | } 21 | 22 | dependencies { 23 | compile group: 'com.google.guava', name: 'guava', version: '18.0' 24 | testCompile group: 'junit', name: 'junit', version: '4.+' 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/crdt/CRDT.java: -------------------------------------------------------------------------------- 1 | package crdt; 2 | 3 | import java.io.Serializable; 4 | 5 | /** Interface for all implemented CRDTs **/ 6 | public interface CRDT> extends Serializable { 7 | 8 | /** Merge another CRDT into this one **/ 9 | void merge(T other); 10 | 11 | /** Create a copy of this CRDT **/ 12 | T copy(); 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/crdt/counters/GCounter.java: -------------------------------------------------------------------------------- 1 | package crdt.counters; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | import java.util.Map.Entry; 6 | 7 | import static java.lang.Math.max; 8 | import crdt.CRDT; 9 | 10 | /** 11 | * Grow only counter. Can only be incremented 12 | */ 13 | public class GCounter implements CRDT> { 14 | 15 | private static final long serialVersionUID = 1L; 16 | 17 | private Map counts = new HashMap(); 18 | 19 | /** 20 | * Increment a given key 21 | */ 22 | public void increment(T key) { 23 | Integer count = counts.get(key); 24 | if( count == null ) 25 | count = 0; 26 | 27 | counts.put(key, count + 1); 28 | } 29 | 30 | /** 31 | * Get the counter value 32 | */ 33 | public int get() { 34 | int sum = 0; 35 | for(int count: counts.values()) 36 | sum += count; 37 | return sum; 38 | } 39 | 40 | /** 41 | * Merge another counter into this one 42 | */ 43 | @Override 44 | public void merge(GCounter other) { 45 | for(Entry e: other.counts.entrySet()) { 46 | T key = e.getKey(); 47 | if( counts.containsKey(key) ) 48 | counts.put(key, max(e.getValue(), counts.get(key)) ); 49 | else 50 | counts.put(key, e.getValue()); 51 | } 52 | } 53 | 54 | @Override 55 | public String toString() { 56 | return "GCounter{" + counts + "}"; 57 | } 58 | 59 | @Override 60 | public GCounter copy() { 61 | GCounter copy = new GCounter(); 62 | copy.counts = new HashMap(counts); 63 | return copy; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/main/java/crdt/counters/GState.java: -------------------------------------------------------------------------------- 1 | package crdt.counters; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Collections; 5 | import java.util.Comparator; 6 | import java.util.EnumSet; 7 | import java.util.HashMap; 8 | import java.util.List; 9 | import java.util.Map; 10 | import java.util.Map.Entry; 11 | 12 | import static com.google.common.base.Preconditions.checkState; 13 | import static java.util.Arrays.asList; 14 | import static java.util.Collections.max; 15 | import crdt.CRDT; 16 | 17 | /** 18 | * Grow only state. State is defined with an enum and can advance to the next state with the next ordinal value. 19 | */ 20 | public class GState> implements CRDT> { 21 | 22 | private static final long serialVersionUID = 1L; 23 | private final EnumOrdinalComparator comparator = new EnumOrdinalComparator(); 24 | 25 | private Map state = new HashMap(); 26 | private List states = new ArrayList(); 27 | private Class clazz; 28 | 29 | public GState(Class clazz) { 30 | this.clazz = clazz; 31 | states.addAll(EnumSet.allOf(clazz)); 32 | Collections.sort(states, comparator); 33 | } 34 | 35 | /** 36 | * Advance the state for the given key 37 | */ 38 | public void increment(T key) { 39 | S count = state.get(key); 40 | int ord = count == null ? 0 : count.ordinal() + 1; 41 | checkState(ord < states.size(), ""); 42 | 43 | state.put(key, states.get(ord)); 44 | } 45 | 46 | /** 47 | * Get the current states 48 | */ 49 | public Map get() { 50 | return Collections.unmodifiableMap(state); 51 | } 52 | 53 | /** 54 | * Merge another counter into this one 55 | */ 56 | @Override 57 | public void merge(GState other) { 58 | for(Entry e: other.state.entrySet()) { 59 | T key = e.getKey(); 60 | S value = e.getValue(); 61 | if( state.containsKey(key) ) 62 | state.put(key, max(asList(value, state.get(key)), comparator) ); 63 | else 64 | state.put(key, value); 65 | } 66 | } 67 | 68 | @Override 69 | public String toString() { 70 | return "GCounter{" + state + "}"; 71 | } 72 | 73 | @Override 74 | public GState copy() { 75 | GState copy = new GState(clazz); 76 | copy.state = new HashMap(state); 77 | return copy; 78 | } 79 | 80 | static class EnumOrdinalComparator> implements Comparator> { 81 | 82 | @Override 83 | public int compare(Enum o1, Enum o2) { 84 | return o1.ordinal() - o2.ordinal(); 85 | } 86 | 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/main/java/crdt/counters/PNCounter.java: -------------------------------------------------------------------------------- 1 | package crdt.counters; 2 | 3 | import crdt.CRDT; 4 | 5 | /** 6 | * Increment and decrement counter 7 | */ 8 | public class PNCounter implements CRDT> { 9 | 10 | private static final long serialVersionUID = 1L; 11 | 12 | private GCounter inc = new GCounter(); 13 | private GCounter dec = new GCounter(); 14 | 15 | public void increment(T key) { 16 | inc.increment(key); 17 | } 18 | 19 | public int get() { 20 | return inc.get() - dec.get(); 21 | } 22 | 23 | public void decrement(T key) { 24 | dec.increment(key); 25 | } 26 | 27 | /** 28 | * Merge another counter into this one 29 | */ 30 | @Override 31 | public void merge(PNCounter other) { 32 | inc.merge( other.inc ); 33 | dec.merge( other.dec ); 34 | } 35 | 36 | @Override 37 | public PNCounter copy() { 38 | PNCounter copy = new PNCounter(); 39 | copy.inc = inc.copy(); 40 | copy.dec = dec.copy(); 41 | return copy; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/crdt/sets/GSet.java: -------------------------------------------------------------------------------- 1 | package crdt.sets; 2 | 3 | import java.util.Collection; 4 | import java.util.Collections; 5 | import java.util.LinkedHashSet; 6 | import java.util.Set; 7 | 8 | import crdt.CRDT; 9 | 10 | /** 11 | * Grow only set. Items can only be added 12 | */ 13 | public class GSet implements CRDT> { 14 | 15 | private static final long serialVersionUID = 1L; 16 | 17 | private Set items = new LinkedHashSet(); 18 | 19 | public void add(T item) { 20 | items.add(item); 21 | } 22 | 23 | public Set get() { 24 | return Collections.unmodifiableSet(items); 25 | } 26 | 27 | /** 28 | * Merge another set into this one 29 | */ 30 | @Override 31 | public void merge(GSet set) { 32 | items.addAll(set.items); 33 | } 34 | 35 | public boolean contains(T item) { 36 | return items.contains(item); 37 | } 38 | 39 | public void addAll(Collection itemToAdd) { 40 | items.addAll(itemToAdd); 41 | } 42 | 43 | @Override 44 | public GSet copy() { 45 | GSet copy = new GSet(); 46 | copy.items = new LinkedHashSet(items); 47 | return copy; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/crdt/sets/ORSet.java: -------------------------------------------------------------------------------- 1 | package crdt.sets; 2 | 3 | import java.util.Collection; 4 | import java.util.Set; 5 | import java.util.UUID; 6 | 7 | import com.google.common.collect.LinkedHashMultimap; 8 | import com.google.common.collect.Multimap; 9 | 10 | import crdt.CRDT; 11 | 12 | /** 13 | * Observed-Removed Set. Supports adding and removing items multiple times. 14 | * In each addition items are tagged and an add wins over a remove during merge. 15 | */ 16 | public class ORSet implements CRDT> { 17 | 18 | private static final long serialVersionUID = 1L; 19 | 20 | private Multimap observed = LinkedHashMultimap.create(); 21 | private Multimap removed = LinkedHashMultimap.create(); 22 | 23 | public void add(T item) { 24 | observed.put(item, UUID.randomUUID()); 25 | } 26 | 27 | public void remove(T item) { 28 | Collection removedTags = observed.removeAll(item); 29 | removed.putAll(item, removedTags); 30 | } 31 | 32 | public Set get() { 33 | return observed.keySet(); 34 | } 35 | 36 | @Override 37 | public void merge(ORSet other) { 38 | observed.putAll(other.observed); 39 | removed.putAll(other.removed); 40 | 41 | observed.removeAll(removed); // adds will stay 42 | } 43 | 44 | @Override 45 | public ORSet copy() { 46 | ORSet copy = new ORSet(); 47 | copy.observed = LinkedHashMultimap.create(observed); 48 | copy.removed = LinkedHashMultimap.create(removed); 49 | return copy; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/crdt/sets/TwoPSet.java: -------------------------------------------------------------------------------- 1 | package crdt.sets; 2 | 3 | import java.util.LinkedHashSet; 4 | import java.util.Set; 5 | 6 | import crdt.CRDT; 7 | 8 | /** 9 | * Two Phase Set. Items can be added and removed but only removed once. 10 | */ 11 | public class TwoPSet implements CRDT> { 12 | 13 | private static final long serialVersionUID = 1L; 14 | 15 | private GSet adds = new GSet(); 16 | private GSet removes = new GSet(); 17 | 18 | public void add(T item) { 19 | if(removes.contains(item)) 20 | throw new IllegalArgumentException("Item was already removed"); 21 | adds.add(item); 22 | } 23 | 24 | public void remove(T item) { 25 | if( adds.contains(item) ) 26 | removes.add(item); 27 | } 28 | 29 | public Set get() { 30 | Set added = new LinkedHashSet( adds.get() ); 31 | added.removeAll( removes.get() ); 32 | return added; 33 | } 34 | 35 | @Override 36 | public void merge(TwoPSet set) { 37 | adds.addAll( set.adds.get() ); 38 | removes.addAll( set.removes.get() ); 39 | } 40 | 41 | @Override 42 | public TwoPSet copy() { 43 | TwoPSet copy = new TwoPSet(); 44 | copy.adds = adds.copy(); 45 | copy.removes = removes.copy(); 46 | return copy; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/test/java/crdt/counters/GCounterTest.java: -------------------------------------------------------------------------------- 1 | package crdt.counters; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | 5 | import org.junit.Test; 6 | 7 | 8 | public class GCounterTest { 9 | 10 | @Test 11 | public void testCounterIncreases_whenIncremented() { 12 | GCounter counter = new GCounter(); 13 | 14 | counter.increment("a"); 15 | 16 | assertEquals(1, counter.get()); 17 | } 18 | 19 | @Test 20 | public void testCounterIncreases_whenIncrementedTwice() { 21 | GCounter counter = newCounter("a", "a"); 22 | 23 | assertEquals(2, counter.get()); 24 | assertEquals(2, counter.copy().get()); 25 | } 26 | 27 | @Test 28 | public void testCounterIncreases_whenIncrementedByDifferentMembers() { 29 | GCounter counter = newCounter("a", "b"); 30 | 31 | assertEquals(2, counter.get()); 32 | } 33 | 34 | private static GCounter newCounter(String... keys) { 35 | GCounter counter = new GCounter(); 36 | 37 | for(String key: keys) 38 | counter.increment(key); 39 | 40 | return counter; 41 | } 42 | 43 | @Test 44 | public void testCountersSummed_whenTwoCountersMerged() { 45 | GCounter replica1 = newCounter("b"); 46 | GCounter replica2 = replica1.copy(); 47 | 48 | replica1.increment("a"); 49 | replica2.increment("c"); 50 | 51 | replica1.merge( replica2 ); 52 | 53 | assertEquals(3, replica1.get()); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/test/java/crdt/counters/GStateTest.java: -------------------------------------------------------------------------------- 1 | package crdt.counters; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | 5 | import org.junit.Test; 6 | 7 | 8 | public class GStateTest { 9 | 10 | enum State { 11 | Joined, 12 | Left 13 | } 14 | 15 | // track state of a cluster. nodes can join and then leave, but cannot join back 16 | GState cluster = new GState(State.class); 17 | 18 | @Test 19 | public void testAdvancesState_whenIncremented() throws Exception { 20 | 21 | cluster.increment("host1"); 22 | assertEquals(State.Joined, cluster.get().get("host1")); 23 | 24 | cluster.increment("host2"); 25 | cluster.increment("host2"); 26 | 27 | assertEquals(State.Left, cluster.get().get("host2")); 28 | } 29 | 30 | @Test(expected = IllegalStateException.class) 31 | public void testThrowsException_whenIncrementedPastStates() throws Exception { 32 | cluster.increment("host2"); 33 | cluster.increment("host2"); 34 | cluster.increment("host2"); 35 | } 36 | 37 | @Test 38 | public void testHighestStatePicked_whenMerged() throws Exception { 39 | 40 | cluster.increment("host1"); 41 | 42 | GState copy = cluster.copy(); 43 | copy.increment("host1"); 44 | 45 | cluster.merge(copy); 46 | 47 | assertEquals(State.Left, cluster.get().get("host1")); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/test/java/crdt/counters/PNCounterTest.java: -------------------------------------------------------------------------------- 1 | package crdt.counters; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | 5 | import org.junit.Test; 6 | 7 | public class PNCounterTest { 8 | 9 | @Test 10 | public void testIncreases_whenIncremented() throws Exception { 11 | PNCounter counter = new PNCounter(); 12 | 13 | counter.increment("a"); 14 | 15 | assertEquals( 1, counter.get() ); 16 | assertEquals( 1, counter.copy().get() ); 17 | } 18 | 19 | @Test 20 | public void testDecreases_whenDecremented() throws Exception { 21 | PNCounter counter = new PNCounter(); 22 | 23 | counter.increment("a"); 24 | counter.decrement("b"); 25 | 26 | assertEquals( 0, counter.get() ); 27 | } 28 | 29 | @Test 30 | public void testCountCorrect_whenMerged() throws Exception { 31 | PNCounter replica1 = new PNCounter(); 32 | replica1.increment("hostname1"); 33 | PNCounter replica2 = replica1.copy(); 34 | 35 | replica1.increment("hostname2"); 36 | replica2.decrement("hostname2"); 37 | replica1.merge( replica2 ); 38 | 39 | assertEquals( 1, replica1.get() ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/test/java/crdt/sets/GSetTest.java: -------------------------------------------------------------------------------- 1 | package crdt.sets; 2 | 3 | import static com.google.common.collect.Sets.newHashSet; 4 | import static java.util.Arrays.asList; 5 | 6 | import static org.junit.Assert.assertEquals; 7 | 8 | import org.junit.Test; 9 | 10 | 11 | public class GSetTest { 12 | 13 | GSet set = new GSet(); 14 | 15 | @Test 16 | public void testSetGrows_whenItemsAdded() throws Exception { 17 | 18 | set.add("a"); 19 | set.add("b"); 20 | set.add("a"); 21 | 22 | assertEquals( newHashSet("a", "b"), set.get() ); 23 | } 24 | 25 | @Test 26 | public void testAllItemsPresent_whenGsetsMerged() throws Exception { 27 | GSet set2 = new GSet(); 28 | 29 | set.add("a"); 30 | set2.add("b"); 31 | set2.add("c"); 32 | 33 | set.merge(set2); 34 | 35 | assertEquals( newHashSet("a", "b", "c"), set.get() ); 36 | } 37 | 38 | @Test 39 | public void testSetGrows_whenCollectionAdded() throws Exception { 40 | 41 | set.addAll(asList("a", "b", "a")); 42 | 43 | assertEquals( newHashSet("a", "b"), set.get() ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/test/java/crdt/sets/ORSetTest.java: -------------------------------------------------------------------------------- 1 | package crdt.sets; 2 | 3 | import static com.google.common.collect.Sets.newHashSet; 4 | 5 | import static org.junit.Assert.assertEquals; 6 | 7 | import org.junit.Test; 8 | 9 | 10 | public class ORSetTest { 11 | 12 | ORSet set = new ORSet(); 13 | 14 | @Test 15 | public void testContainsItem_whenAdded() throws Exception { 16 | set.add("a"); 17 | set.add("a"); 18 | set.add("b"); 19 | 20 | assertEquals( newHashSet("a", "b"), set.get() ); 21 | } 22 | 23 | @Test 24 | public void testDoesntContainItem_whenRemoved() throws Exception { 25 | set.add("a"); 26 | set.add("b"); 27 | set.remove("a"); 28 | set.add("a"); 29 | set.remove("a"); 30 | 31 | assertEquals( newHashSet("b"), set.get() ); 32 | } 33 | 34 | @Test 35 | public void testContainsAllItems_whenMerged() throws Exception { 36 | 37 | ORSet set2 = new ORSet(); 38 | 39 | set.add("a"); 40 | set2.add("b"); 41 | 42 | set.merge(set2); 43 | 44 | assertEquals( newHashSet("a", "b"), set.get() ); 45 | } 46 | 47 | @Test 48 | public void testDoesntContainsItems_whenRemovedAndMerged() throws Exception { 49 | 50 | set.add("a"); 51 | set.add("b"); 52 | 53 | ORSet set2 = set.copy(); 54 | 55 | set2.remove("b"); 56 | set.merge(set2); 57 | 58 | assertEquals( newHashSet("a", "b"), set.get() ); 59 | } 60 | 61 | @Test 62 | public void testAddWins_whenRemovedAndMerged() throws Exception { 63 | 64 | ORSet set2 = new ORSet(); 65 | 66 | set.add("a"); 67 | set.add("b"); 68 | 69 | set2.add("a"); 70 | set2.remove("a"); 71 | 72 | set.merge(set2); 73 | 74 | assertEquals( newHashSet("a", "b"), set.get() ); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/test/java/crdt/sets/TwoPSetTest.java: -------------------------------------------------------------------------------- 1 | package crdt.sets; 2 | 3 | import static com.google.common.collect.Sets.newHashSet; 4 | 5 | import static org.junit.Assert.assertEquals; 6 | 7 | import org.junit.Test; 8 | 9 | 10 | public class TwoPSetTest { 11 | 12 | TwoPSet set = new TwoPSet(); 13 | 14 | @Test 15 | public void testElementExists_whenAdded() throws Exception { 16 | 17 | set.add("a"); 18 | 19 | assertEquals( newHashSet("a"), set.get() ); 20 | } 21 | 22 | @Test 23 | public void testElementDoesNotExists_whenRemoved() throws Exception { 24 | 25 | set.add("a"); 26 | set.add("b"); 27 | set.remove("b"); 28 | 29 | assertEquals( newHashSet("a"), set.get() ); 30 | } 31 | 32 | @Test(expected = IllegalArgumentException.class) 33 | public void testExceptionThrown_whenAddedAfterRemove() throws Exception { 34 | 35 | set.add("b"); 36 | set.remove("b"); 37 | set.add("b"); 38 | } 39 | 40 | @Test 41 | public void testAllAddedPresent_whenMerged() throws Exception { 42 | TwoPSet set2 = new TwoPSet(); 43 | 44 | set.add("a"); 45 | set2.add("b"); 46 | 47 | set.merge(set2); 48 | 49 | assertEquals(newHashSet("a", "b"), set.get()); 50 | assertEquals(newHashSet("a", "b"), set.copy().get()); 51 | 52 | } 53 | 54 | @Test 55 | public void testItemNotPresent_whenRemovedInOtherSetAndMerged() throws Exception { 56 | 57 | set.add("b"); 58 | TwoPSet replica2 = set.copy(); 59 | 60 | set.add("a"); 61 | replica2.remove("b"); 62 | replica2.add("c"); 63 | 64 | set.merge(replica2); 65 | 66 | assertEquals(newHashSet("a", "c"), set.get()); 67 | } 68 | } 69 | --------------------------------------------------------------------------------