Fix insertions & test cases, Add debug information
This commit is contained in:
parent
30d454a9d2
commit
e1fc9cf4a7
@ -5,6 +5,7 @@ import java.util.List;
|
|||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
import me.mrletsplay.shareclientcore.connection.message.Message;
|
import me.mrletsplay.shareclientcore.connection.message.Message;
|
||||||
|
import me.mrletsplay.shareclientcore.debug.DebugValues;
|
||||||
|
|
||||||
public class DummyConnection implements RemoteConnection {
|
public class DummyConnection implements RemoteConnection {
|
||||||
|
|
||||||
@ -67,4 +68,9 @@ public class DummyConnection implements RemoteConnection {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DebugValues getDebugValues() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,19 @@
|
|||||||
package me.mrletsplay.shareclientcore.connection;
|
package me.mrletsplay.shareclientcore.connection;
|
||||||
|
|
||||||
import me.mrletsplay.shareclientcore.connection.message.Message;
|
import me.mrletsplay.shareclientcore.connection.message.Message;
|
||||||
|
import me.mrletsplay.shareclientcore.debug.DebugValues;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a connection to a remote user or server
|
* Represents a connection to a remote user or server
|
||||||
*/
|
*/
|
||||||
public interface RemoteConnection {
|
public interface RemoteConnection {
|
||||||
|
|
||||||
|
public static final String
|
||||||
|
DEBUG_MESSAGES_SENT = "messagesSent",
|
||||||
|
DEBUG_MESSAGES_RECEIVED = "messagesReceived",
|
||||||
|
DEBUG_CHANGES_SENT = "changesSent",
|
||||||
|
DEBUG_CHANGES_RECEIVED = "changesReceived";
|
||||||
|
|
||||||
public static final int PROTOCOL_VERSION = 1;
|
public static final int PROTOCOL_VERSION = 1;
|
||||||
|
|
||||||
public void connect(String sessionID) throws ConnectionException;
|
public void connect(String sessionID) throws ConnectionException;
|
||||||
@ -23,4 +30,6 @@ public interface RemoteConnection {
|
|||||||
|
|
||||||
public void setDisconnectListener(DisconnectListener listener);
|
public void setDisconnectListener(DisconnectListener listener);
|
||||||
|
|
||||||
|
public DebugValues getDebugValues();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -14,9 +14,11 @@ import org.java_websocket.client.WebSocketClient;
|
|||||||
import org.java_websocket.framing.CloseFrame;
|
import org.java_websocket.framing.CloseFrame;
|
||||||
import org.java_websocket.handshake.ServerHandshake;
|
import org.java_websocket.handshake.ServerHandshake;
|
||||||
|
|
||||||
|
import me.mrletsplay.shareclientcore.connection.message.ChangeMessage;
|
||||||
import me.mrletsplay.shareclientcore.connection.message.ClientHelloMessage;
|
import me.mrletsplay.shareclientcore.connection.message.ClientHelloMessage;
|
||||||
import me.mrletsplay.shareclientcore.connection.message.Message;
|
import me.mrletsplay.shareclientcore.connection.message.Message;
|
||||||
import me.mrletsplay.shareclientcore.connection.message.ServerHelloMessage;
|
import me.mrletsplay.shareclientcore.connection.message.ServerHelloMessage;
|
||||||
|
import me.mrletsplay.shareclientcore.debug.DebugValues;
|
||||||
|
|
||||||
public class WebSocketConnection implements RemoteConnection {
|
public class WebSocketConnection implements RemoteConnection {
|
||||||
|
|
||||||
@ -29,11 +31,18 @@ public class WebSocketConnection implements RemoteConnection {
|
|||||||
private Object wait = new Object();
|
private Object wait = new Object();
|
||||||
private boolean helloReceived;
|
private boolean helloReceived;
|
||||||
private ConnectionException connectException;
|
private ConnectionException connectException;
|
||||||
|
private DebugValues debugValues;
|
||||||
|
|
||||||
public WebSocketConnection(URI uri, String username, Map<String, String> httpHeaders) {
|
public WebSocketConnection(URI uri, String username, Map<String, String> httpHeaders) {
|
||||||
this.client = new WSClient(uri, httpHeaders);
|
this.client = new WSClient(uri, httpHeaders);
|
||||||
this.username = username;
|
this.username = username;
|
||||||
this.listeners = new HashSet<>();
|
this.listeners = new HashSet<>();
|
||||||
|
this.debugValues = new DebugValues(
|
||||||
|
DEBUG_MESSAGES_SENT,
|
||||||
|
DEBUG_MESSAGES_RECEIVED,
|
||||||
|
DEBUG_CHANGES_SENT,
|
||||||
|
DEBUG_CHANGES_RECEIVED
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public WebSocketConnection(URI uri, String username) {
|
public WebSocketConnection(URI uri, String username) {
|
||||||
@ -68,6 +77,9 @@ public class WebSocketConnection implements RemoteConnection {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void send(Message message) throws ConnectionException {
|
public void send(Message message) throws ConnectionException {
|
||||||
|
debugValues.increment(DEBUG_MESSAGES_SENT);
|
||||||
|
if(message instanceof ChangeMessage) debugValues.increment(DEBUG_CHANGES_SENT);
|
||||||
|
|
||||||
ByteArrayOutputStream bOut = new ByteArrayOutputStream();
|
ByteArrayOutputStream bOut = new ByteArrayOutputStream();
|
||||||
DataOutputStream dOut = new DataOutputStream(bOut);
|
DataOutputStream dOut = new DataOutputStream(bOut);
|
||||||
|
|
||||||
@ -96,6 +108,11 @@ public class WebSocketConnection implements RemoteConnection {
|
|||||||
this.disconnectListener = listener;
|
this.disconnectListener = listener;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DebugValues getDebugValues() {
|
||||||
|
return debugValues;
|
||||||
|
}
|
||||||
|
|
||||||
private class WSClient extends WebSocketClient {
|
private class WSClient extends WebSocketClient {
|
||||||
|
|
||||||
public WSClient(URI serverUri) {
|
public WSClient(URI serverUri) {
|
||||||
@ -126,6 +143,9 @@ public class WebSocketConnection implements RemoteConnection {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
debugValues.increment(DEBUG_MESSAGES_RECEIVED);
|
||||||
|
if(m instanceof ChangeMessage) debugValues.increment(DEBUG_CHANGES_RECEIVED);
|
||||||
|
|
||||||
if(m instanceof ServerHelloMessage hello) {
|
if(m instanceof ServerHelloMessage hello) {
|
||||||
helloReceived = true;
|
helloReceived = true;
|
||||||
siteID = hello.siteID();
|
siteID = hello.siteID();
|
||||||
|
@ -35,7 +35,6 @@ public record FullSyncMessage(int siteID, String documentPath, List<Char> conten
|
|||||||
List<Char> content = new ArrayList<>(contentSize);
|
List<Char> content = new ArrayList<>(contentSize);
|
||||||
for(int i = 0; i < contentSize; i++) {
|
for(int i = 0; i < contentSize; i++) {
|
||||||
content.add(Char.deserialize(in));
|
content.add(Char.deserialize(in));
|
||||||
System.out.println(content.get(content.size() - 1));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return new FullSyncMessage(siteID, documentPath, content);
|
return new FullSyncMessage(siteID, documentPath, content);
|
||||||
|
@ -0,0 +1,66 @@
|
|||||||
|
package me.mrletsplay.shareclientcore.debug;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
public class DebugValues {
|
||||||
|
|
||||||
|
private Set<String> keys;
|
||||||
|
private Map<String, Integer> values;
|
||||||
|
|
||||||
|
public DebugValues(Collection<String> keys) {
|
||||||
|
this.keys = Collections.unmodifiableSet(new LinkedHashSet<>(keys));
|
||||||
|
this.values = new HashMap<>(this.keys.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
public DebugValues(String... keys) {
|
||||||
|
this(Arrays.asList(keys));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Set<String> getKeys() {
|
||||||
|
return keys;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkKey(String key) throws IllegalArgumentException {
|
||||||
|
if(!keys.contains(key)) throw new IllegalArgumentException("Not a valid key");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void set(String key, int value) throws IllegalArgumentException {
|
||||||
|
checkKey(key);
|
||||||
|
values.put(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int get(String key) throws IllegalArgumentException {
|
||||||
|
checkKey(key);
|
||||||
|
return values.getOrDefault(key, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void reset(String key) throws IllegalArgumentException {
|
||||||
|
checkKey(key);
|
||||||
|
values.remove(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void increment(String key) throws IllegalArgumentException {
|
||||||
|
checkKey(key);
|
||||||
|
values.put(key, values.getOrDefault(key, 0) + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void decrement(String key) throws IllegalArgumentException {
|
||||||
|
checkKey(key);
|
||||||
|
values.put(key, values.getOrDefault(key, 0) + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return keys.stream()
|
||||||
|
.map(key -> key + ": " + get(key))
|
||||||
|
.collect(Collectors.joining("\n"));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -2,27 +2,42 @@ package me.mrletsplay.shareclientcore.document;
|
|||||||
|
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import me.mrletsplay.shareclientcore.debug.DebugValues;
|
||||||
|
|
||||||
public class ArrayCharBag implements CharBag {
|
public class ArrayCharBag implements CharBag {
|
||||||
|
|
||||||
private List<Char> chars;
|
private List<Char> chars;
|
||||||
|
|
||||||
public ArrayCharBag() {
|
private DebugValues debugValues;
|
||||||
this.chars = new ArrayList<>();
|
|
||||||
}
|
|
||||||
|
|
||||||
public ArrayCharBag(List<Char> chars) {
|
public ArrayCharBag(List<Char> chars) {
|
||||||
this.chars = new ArrayList<>(chars);
|
this.chars = new ArrayList<>(chars);
|
||||||
|
this.debugValues = new DebugValues(
|
||||||
|
DEBUG_INSERTIONS,
|
||||||
|
DEBUG_INSERTIONS_DROPPED,
|
||||||
|
DEBUG_DELETIONS,
|
||||||
|
DEBUG_DELETIONS_DROPPED
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ArrayCharBag() {
|
||||||
|
this(Collections.emptyList());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int add(Char character) {
|
public int add(Char character) {
|
||||||
int i = 0;
|
int i = 0;
|
||||||
// TODO: use binary search
|
// TODO: use binary search
|
||||||
while(i < chars.size() && Util.comparePositions(chars.get(i).position(), character.position()) < 0) i++;
|
while(i < chars.size() && Util.compareChars(chars.get(i), character) < 0) i++;
|
||||||
if(i < chars.size() && Util.comparePositions(chars.get(i).position(), character.position()) == 0) return -1;
|
if(i < chars.size() && Util.compareChars(chars.get(i), character) == 0) {
|
||||||
|
debugValues.increment(DEBUG_INSERTIONS_DROPPED);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
chars.add(i, character);
|
chars.add(i, character);
|
||||||
|
debugValues.increment(DEBUG_INSERTIONS);
|
||||||
return i;
|
return i;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -30,9 +45,13 @@ public class ArrayCharBag implements CharBag {
|
|||||||
public int remove(Char character) {
|
public int remove(Char character) {
|
||||||
int i = 0;
|
int i = 0;
|
||||||
// TODO: use binary search
|
// TODO: use binary search
|
||||||
while(i < chars.size() && Util.comparePositions(chars.get(i).position(), character.position()) < 0) i++;
|
while(i < chars.size() && Util.compareChars(chars.get(i), character) < 0) i++;
|
||||||
if(i == chars.size() || Util.comparePositions(chars.get(i).position(), character.position()) != 0) return -1;
|
if(i == chars.size() || Util.compareChars(chars.get(i), character) != 0) {
|
||||||
|
debugValues.increment(DEBUG_DELETIONS_DROPPED);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
chars.remove(i);
|
chars.remove(i);
|
||||||
|
debugValues.increment(DEBUG_DELETIONS);
|
||||||
return i;
|
return i;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -70,4 +89,9 @@ public class ArrayCharBag implements CharBag {
|
|||||||
return new String(getContents(), StandardCharsets.UTF_8);
|
return new String(getContents(), StandardCharsets.UTF_8);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DebugValues getDebugValues() {
|
||||||
|
return debugValues;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -3,8 +3,20 @@ package me.mrletsplay.shareclientcore.document;
|
|||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import me.mrletsplay.shareclientcore.debug.DebugValues;
|
||||||
|
|
||||||
public interface CharBag {
|
public interface CharBag {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Debug keys
|
||||||
|
* @see #getDebugValues()
|
||||||
|
*/
|
||||||
|
public static final String
|
||||||
|
DEBUG_INSERTIONS = "insertions",
|
||||||
|
DEBUG_INSERTIONS_DROPPED = "insertionsDropped",
|
||||||
|
DEBUG_DELETIONS = "deletions",
|
||||||
|
DEBUG_DELETIONS_DROPPED = "deletionsDropped";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds a character to the bag and returns the index it was inserted at, or -1 if it was not inserted because it already exists
|
* Adds a character to the bag and returns the index it was inserted at, or -1 if it was not inserted because it already exists
|
||||||
* @param character The character to add
|
* @param character The character to add
|
||||||
@ -39,7 +51,7 @@ public interface CharBag {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Collects the chars in this bag ordered by their position into a list
|
* Collects the chars in this bag ordered by their position into a list
|
||||||
* @return
|
* @return The chars in this bag
|
||||||
*/
|
*/
|
||||||
public List<Char> toList();
|
public List<Char> toList();
|
||||||
|
|
||||||
@ -56,4 +68,13 @@ public interface CharBag {
|
|||||||
*/
|
*/
|
||||||
public String getContentsAsString();
|
public String getContentsAsString();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Debug information about this bag
|
||||||
|
* @see #DEBUG_INSERTIONS
|
||||||
|
* @see #DEBUG_INSERTIONS_DROPPED
|
||||||
|
* @see #DEBUG_DELETIONS
|
||||||
|
* @see #DEBUG_DELETIONS_DROPPED
|
||||||
|
*/
|
||||||
|
public DebugValues getDebugValues();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -113,7 +113,6 @@ public class SharedDocument implements MessageListener {
|
|||||||
if(charBag.remove(toRemove) == -1) throw new IllegalStateException("Couldn't remove existing char");
|
if(charBag.remove(toRemove) == -1) throw new IllegalStateException("Couldn't remove existing char");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
for(Change c : changes) {
|
for(Change c : changes) {
|
||||||
try {
|
try {
|
||||||
connection.send(new ChangeMessage(c));
|
connection.send(new ChangeMessage(c));
|
||||||
|
@ -19,6 +19,12 @@ public class Util {
|
|||||||
return Integer.compare(a.length, b.length);
|
return Integer.compare(a.length, b.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static int compareChars(Char a, Char b) {
|
||||||
|
int pos = comparePositions(a.position(), b.position());
|
||||||
|
if(pos != 0) return pos;
|
||||||
|
return Integer.compare(a.lamport(), b.lamport());
|
||||||
|
}
|
||||||
|
|
||||||
public static Identifier[] generatePositionBetween(Identifier[] before, Identifier[] after, int site) {
|
public static Identifier[] generatePositionBetween(Identifier[] before, Identifier[] after, int site) {
|
||||||
if(comparePositions(before, after) != -1) throw new IllegalArgumentException("before must be strictly less than after");
|
if(comparePositions(before, after) != -1) throw new IllegalArgumentException("before must be strictly less than after");
|
||||||
|
|
||||||
@ -28,7 +34,8 @@ public class Util {
|
|||||||
Identifier c1 = i >= before.length ? new Identifier(0, site) : before[i];
|
Identifier c1 = i >= before.length ? new Identifier(0, site) : before[i];
|
||||||
Identifier c2 = i >= after.length ? new Identifier(BASE - 1, site) : after[i];
|
Identifier c2 = i >= after.length ? new Identifier(BASE - 1, site) : after[i];
|
||||||
|
|
||||||
if(c1.digit() != c2.digit()) {
|
// Modification to the original source because otherwise it can lead to cases where positions can't be generated and this seems to work
|
||||||
|
if(i >= before.length || c1.digit() != c2.digit()) {
|
||||||
int[] incremented = getIncremented(before, after, i);
|
int[] incremented = getIncremented(before, after, i);
|
||||||
return constructPosition(incremented, before, after, site);
|
return constructPosition(incremented, before, after, site);
|
||||||
}
|
}
|
||||||
|
@ -63,40 +63,49 @@ public class SharingTest {
|
|||||||
String chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567890";
|
String chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567890";
|
||||||
|
|
||||||
Random r = new Random(0);
|
Random r = new Random(0);
|
||||||
for(int i = 0; i < 1000; i++) {
|
for(int i = 0; i < 10_000; i++) {
|
||||||
switch(r.nextInt(sharedA.getContents().length == 0 ? 2 : 4)) {
|
switch(r.nextInt(sharedA.getContents().length == 0 ? 2 : 4)) {
|
||||||
case 0: {
|
case 0: {
|
||||||
// Insert A
|
// Insert A
|
||||||
char[] insert = new char[r.nextInt(16)];
|
char[] insert = new char[r.nextInt(16)];
|
||||||
for(int j = 0; j < insert.length; j++) insert[j] = chars.charAt(r.nextInt(chars.length()));
|
for(int j = 0; j < insert.length; j++) insert[j] = chars.charAt(r.nextInt(chars.length()));
|
||||||
sharedA.localInsert(r.nextInt(sharedA.getContents().length + 1), String.valueOf(insert));
|
sharedA.localInsert(r.nextInt(sharedA.getContents().length + 1), String.valueOf(insert));
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
case 1: {
|
case 1: {
|
||||||
// Insert B
|
// Insert B
|
||||||
char[] insert = new char[r.nextInt(16)];
|
char[] insert = new char[r.nextInt(16)];
|
||||||
for(int j = 0; j < insert.length; j++) insert[j] = chars.charAt(r.nextInt(chars.length()));
|
for(int j = 0; j < insert.length; j++) insert[j] = chars.charAt(r.nextInt(chars.length()));
|
||||||
sharedB.localInsert(r.nextInt(sharedB.getContents().length + 1), String.valueOf(insert));
|
sharedB.localInsert(r.nextInt(sharedB.getContents().length + 1), String.valueOf(insert));
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
case 2: {
|
case 2: {
|
||||||
// Delete A
|
// Delete A
|
||||||
int len = sharedA.getContents().length;
|
int len = sharedA.getContents().length;
|
||||||
int idx = r.nextInt(len);
|
int idx = r.nextInt(len);
|
||||||
int n = r.nextInt(len - idx);
|
int n = r.nextInt(Math.min(16, len - idx)) + 1;
|
||||||
sharedA.localDelete(idx, n);
|
sharedA.localDelete(idx, n);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
case 3: {
|
case 3: {
|
||||||
// Delete B
|
// Delete B
|
||||||
int len = sharedB.getContents().length;
|
int len = sharedB.getContents().length;
|
||||||
int idx = r.nextInt(len);
|
int idx = r.nextInt(len);
|
||||||
int n = r.nextInt(len - idx + 1);
|
int n = r.nextInt(Math.min(16, len - idx)) + 1;
|
||||||
sharedB.localDelete(idx, n);
|
sharedB.localDelete(idx, n);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
System.out.println("A: " + sharedA.getContentsAsString());
|
// System.out.println("A: " + sharedA.getContentsAsString());
|
||||||
System.out.println("B: " + sharedB.getContentsAsString());
|
// System.out.println("B: " + sharedB.getContentsAsString());
|
||||||
assertEquals(sharedA.getContentsAsString(), sharedB.getContentsAsString());
|
assertEquals(sharedA.getContentsAsString(), sharedB.getContentsAsString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSharedDocumentInterleaving() {
|
||||||
|
// TODO: test random interleaving of messages
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user