Fix insertions & test cases, Add debug information

This commit is contained in:
MrLetsplay 2024-06-15 19:20:45 +02:00
parent 30d454a9d2
commit e1fc9cf4a7
Signed by: mr
SSH Key Fingerprint: SHA256:92jBH80vpXyaZHjaIl47pjRq+Yt7XGTArqQg1V7hSqg
10 changed files with 177 additions and 17 deletions

View File

@ -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;
}
} }

View File

@ -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();
} }

View File

@ -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();

View File

@ -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);

View File

@ -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"));
}
}

View File

@ -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;
}
} }

View File

@ -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();
} }

View File

@ -49,7 +49,7 @@ public class SharedDocument implements MessageListener {
if(index < 0 || index >= charBag.size() - 1) throw new IllegalArgumentException("Index out of bounds"); if(index < 0 || index >= charBag.size() - 1) throw new IllegalArgumentException("Index out of bounds");
Char charBefore = charBag.get(index); Char charBefore = charBag.get(index);
Char charAfter = charBag.get(index +1); Char charAfter = charBag.get(index + 1);
Change[] changes = new Change[bytes.length]; Change[] changes = new Change[bytes.length];
for(int i = 0; i < bytes.length; i++) { for(int i = 0; i < bytes.length; i++) {
@ -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));

View File

@ -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);
} }

View File

@ -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
}
} }