Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions TestLiteralEval.java
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ public static void main(String[] args) {
testUnterminatedDict();
testUnterminatedTuple();
testNonStringDictKey();
testJsonKeywordTrue();
testJsonKeywordFalse();
testJsonKeywordNull();
testJsonMixedList();
testJsonRoundTrip();

System.out.println("\n=== Results: " + passed + " passed, " + failed + " failed out of " + (passed + failed) + " tests ===");
if (failed > 0) {
Expand Down Expand Up @@ -267,4 +272,42 @@ static void testNonStringDictKey() {
check("numeric dict key throws", true, e.getMessage().contains("Dict keys must be non-null strings"));
}
}

// --- JSON keyword interop tests ---

static void testJsonKeywordTrue() {
Object result = concoredocker.literalEval("true");
check("json true -> Boolean.TRUE", Boolean.TRUE, result);
}

static void testJsonKeywordFalse() {
Object result = concoredocker.literalEval("false");
check("json false -> Boolean.FALSE", Boolean.FALSE, result);
}

static void testJsonKeywordNull() {
Object result = concoredocker.literalEval("null");
check("json null -> null", null, result);
}

static void testJsonMixedList() {
// Python sends [0.0, true, null] via json.dumps — Java must parse it
@SuppressWarnings("unchecked")
List<Object> result = (List<Object>) concoredocker.literalEval("[0.0, true, null]");
check("json list[0] simtime", 0.0, result.get(0));
check("json list[1] true", Boolean.TRUE, result.get(1));
check("json list[2] null", null, result.get(2));
}

static void testJsonRoundTrip() {
// JSON-style payload: true/false/null and double-quoted strings
String jsonPayload = "[0.0, 1.5, true, false, null, \"hello\"]";
@SuppressWarnings("unchecked")
List<Object> result = (List<Object>) concoredocker.literalEval(jsonPayload);
check("json round-trip double", 1.5, result.get(1));
check("json round-trip true", Boolean.TRUE, result.get(2));
check("json round-trip false", Boolean.FALSE, result.get(3));
check("json round-trip null", null, result.get(4));
check("json round-trip string", "hello", result.get(5));
}
}
64 changes: 59 additions & 5 deletions concoredocker.java
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,60 @@ private static String toPythonLiteral(Object obj) {
return obj.toString();
}

/**
* Escapes a Java string so it can be safely embedded in a JSON double-quoted string.
* Escapes backslash, double quote, newline, carriage return, and tab.
*/
private static String escapeJsonString(String s) {
StringBuilder sb = new StringBuilder(s.length());
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
switch (c) {
case '\\': sb.append("\\\\"); break;
case '"': sb.append("\\\""); break;
case '\n': sb.append("\\n"); break;
case '\r': sb.append("\\r"); break;
case '\t': sb.append("\\t"); break;
default: sb.append(c); break;
}
}
return sb.toString();
}

/**
* Converts a Java object to its JSON string representation.
* true/false/null instead of True/False/None; strings double-quoted.
*/
private static String toJsonLiteral(Object obj) {
if (obj == null) return "null";
if (obj instanceof Boolean) return ((Boolean) obj) ? "true" : "false";
if (obj instanceof String) return "\"" + escapeJsonString((String) obj) + "\"";
if (obj instanceof Number) return obj.toString();
if (obj instanceof List) {
List<?> list = (List<?>) obj;
StringBuilder sb = new StringBuilder("[");
for (int i = 0; i < list.size(); i++) {
if (i > 0) sb.append(", ");
sb.append(toJsonLiteral(list.get(i)));
}
sb.append("]");
return sb.toString();
}
if (obj instanceof Map) {
Map<?, ?> map = (Map<?, ?>) obj;
StringBuilder sb = new StringBuilder("{");
boolean first = true;
for (Map.Entry<?, ?> entry : map.entrySet()) {
if (!first) sb.append(", ");
sb.append(toJsonLiteral(entry.getKey())).append(": ").append(toJsonLiteral(entry.getValue()));
first = false;
}
sb.append("}");
return sb.toString();
}
return obj.toString();
}

/**
* Writes data to a port file.
* Prepends simtime+delta to the value list, then serializes to Python-literal format.
Expand Down Expand Up @@ -437,10 +491,10 @@ public static void write(String portName, String name, Object val, int delta) {
if (val instanceof List) {
List<?> listVal = (List<?>) val;
StringBuilder sb = new StringBuilder("[");
sb.append(toPythonLiteral(simtime + delta));
sb.append(toJsonLiteral(simtime + delta));
for (Object o : listVal) {
sb.append(", ");
sb.append(toPythonLiteral(o));
sb.append(toJsonLiteral(o));
}
sb.append("]");
payload = sb.toString();
Expand Down Expand Up @@ -750,9 +804,9 @@ Object parseKeyword() {
}
String word = input.substring(start, pos);
switch (word) {
case "True": return Boolean.TRUE;
case "False": return Boolean.FALSE;
case "None": return null;
case "True": case "true": return Boolean.TRUE;
case "False": case "false": return Boolean.FALSE;
case "None": case "null": return null;
default: throw new IllegalArgumentException("Unknown keyword: '" + word + "' at position " + start);
}
}
Expand Down