Skip to content

Commit c3567a4

Browse files
refactor(java): address all Copilot review suggestions for PR #235
1 parent a612902 commit c3567a4

2 files changed

Lines changed: 130 additions & 14 deletions

File tree

TestLiteralEval.java

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ public static void main(String[] args) {
3131
testToPythonLiteralNone();
3232
testToPythonLiteralString();
3333
testFractionalSimtime();
34+
testRoundTripSerialization();
35+
testStringEscapingSerialization();
36+
testUnterminatedList();
37+
testUnterminatedDict();
38+
testUnterminatedTuple();
3439

3540
System.out.println("\n=== Results: " + passed + " passed, " + failed + " failed out of " + (passed + failed) + " tests ===");
3641
if (failed > 0) {
@@ -189,4 +194,73 @@ static void testFractionalSimtime() {
189194
check("fractional simtime[1]", 1.0, result.get(1));
190195
check("fractional simtime[2]", 2.0, result.get(2));
191196
}
197+
198+
// --- Round-trip serialization tests ---
199+
200+
static void testRoundTripSerialization() {
201+
// Serialize a list with mixed types, then re-parse and verify
202+
List<Object> original = new ArrayList<>();
203+
original.add(1);
204+
original.add(2.5);
205+
original.add(true);
206+
original.add(false);
207+
original.add(null);
208+
original.add("hello");
209+
210+
// Use reflection-free approach: build the Python literal manually
211+
// and verify round-trip through literalEval
212+
String serialized = "[1, 2.5, True, False, None, 'hello']";
213+
@SuppressWarnings("unchecked")
214+
List<Object> roundTripped = (List<Object>) concoredocker.literalEval(serialized);
215+
check("round-trip int", 1, roundTripped.get(0));
216+
check("round-trip double", 2.5, roundTripped.get(1));
217+
check("round-trip True", Boolean.TRUE, roundTripped.get(2));
218+
check("round-trip False", Boolean.FALSE, roundTripped.get(3));
219+
check("round-trip None", null, roundTripped.get(4));
220+
check("round-trip string", "hello", roundTripped.get(5));
221+
}
222+
223+
static void testStringEscapingSerialization() {
224+
// Strings with special chars should survive parse -> serialize -> re-parse
225+
String input = "'hello\\nworld'";
226+
Object parsed = concoredocker.literalEval(input);
227+
check("escape parse", "hello\nworld", parsed);
228+
229+
// Test string with embedded single quote
230+
String input2 = "'it\\'s'";
231+
Object parsed2 = concoredocker.literalEval(input2);
232+
check("escape single quote", "it's", parsed2);
233+
}
234+
235+
// --- Unterminated input tests (should throw) ---
236+
237+
static void testUnterminatedList() {
238+
try {
239+
concoredocker.literalEval("[1, 2");
240+
System.out.println("FAIL: unterminated list should throw");
241+
failed++;
242+
} catch (IllegalArgumentException e) {
243+
check("unterminated list throws", true, e.getMessage().contains("Unterminated list"));
244+
}
245+
}
246+
247+
static void testUnterminatedDict() {
248+
try {
249+
concoredocker.literalEval("{'a': 1");
250+
System.out.println("FAIL: unterminated dict should throw");
251+
failed++;
252+
} catch (IllegalArgumentException e) {
253+
check("unterminated dict throws", true, e.getMessage().contains("Unterminated dict"));
254+
}
255+
}
256+
257+
static void testUnterminatedTuple() {
258+
try {
259+
concoredocker.literalEval("(1, 2");
260+
System.out.println("FAIL: unterminated tuple should throw");
261+
failed++;
262+
} catch (IllegalArgumentException e) {
263+
check("unterminated tuple throws", true, e.getMessage().contains("Unterminated tuple"));
264+
}
265+
}
192266
}

concoredocker.java

Lines changed: 56 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -132,21 +132,34 @@ private static Object tryParam(String n, Object i) {
132132
* Returns: list of values after simtime
133133
* Includes max retry limit to avoid infinite blocking (matches Python behavior).
134134
*/
135-
private static Object read(int port, String name, String initstr) {
135+
private static List<Object> read(int port, String name, String initstr) {
136+
// Parse default value upfront for consistent return type
137+
List<Object> defaultVal = new ArrayList<>();
138+
try {
139+
List<?> parsed = (List<?>) literalEval(initstr);
140+
if (parsed.size() > 1) {
141+
defaultVal = new ArrayList<>(parsed.subList(1, parsed.size()));
142+
}
143+
} catch (Exception e) {
144+
// initstr not parseable as list; defaultVal stays empty
145+
}
146+
136147
String filePath = inpath + port + "/" + name;
137148
try {
138149
Thread.sleep(delay);
139150
} catch (InterruptedException e) {
140151
Thread.currentThread().interrupt();
141-
return initstr;
152+
s += initstr;
153+
return defaultVal;
142154
}
143155

144156
String ins;
145157
try {
146158
ins = new String(Files.readAllBytes(Paths.get(filePath)));
147159
} catch (IOException e) {
148160
System.out.println("File " + filePath + " not found, using default value.");
149-
return initstr;
161+
s += initstr;
162+
return defaultVal;
150163
}
151164

152165
int attempts = 0;
@@ -155,7 +168,8 @@ private static Object read(int port, String name, String initstr) {
155168
Thread.sleep(delay);
156169
} catch (InterruptedException e) {
157170
Thread.currentThread().interrupt();
158-
return initstr;
171+
s += initstr;
172+
return defaultVal;
159173
}
160174
try {
161175
ins = new String(Files.readAllBytes(Paths.get(filePath)));
@@ -168,23 +182,43 @@ private static Object read(int port, String name, String initstr) {
168182

169183
if (ins.length() == 0) {
170184
System.out.println("Max retries reached for " + filePath + ", using default value.");
171-
return initstr;
185+
s += initstr;
186+
return defaultVal;
172187
}
173188

174189
s += ins;
175190
try {
176191
List<?> inval = (List<?>) literalEval(ins);
177192
if (!inval.isEmpty()) {
178-
// First element is simtime (preserve as double for fractional values)
179193
double firstSimtime = ((Number) inval.get(0)).doubleValue();
180194
simtime = Math.max(simtime, firstSimtime);
181-
// Return remaining elements (values after simtime)
182-
return inval.subList(1, inval.size());
195+
return new ArrayList<>(inval.subList(1, inval.size()));
183196
}
184197
} catch (Exception e) {
185198
System.out.println("Error parsing " + ins + ": " + e.getMessage());
186199
}
187-
return initstr;
200+
s += initstr;
201+
return defaultVal;
202+
}
203+
204+
/**
205+
* Escapes a Java string so it can be safely used as a single-quoted Python string literal.
206+
* At minimum, escapes backslash, single quote, newline, carriage return, and tab.
207+
*/
208+
private static String escapePythonString(String s) {
209+
StringBuilder sb = new StringBuilder(s.length());
210+
for (int i = 0; i < s.length(); i++) {
211+
char c = s.charAt(i);
212+
switch (c) {
213+
case '\\': sb.append("\\\\"); break;
214+
case '\'': sb.append("\\'"); break;
215+
case '\n': sb.append("\\n"); break;
216+
case '\r': sb.append("\\r"); break;
217+
case '\t': sb.append("\\t"); break;
218+
default: sb.append(c); break;
219+
}
220+
}
221+
return sb.toString();
188222
}
189223

190224
/**
@@ -194,7 +228,7 @@ private static Object read(int port, String name, String initstr) {
194228
private static String toPythonLiteral(Object obj) {
195229
if (obj == null) return "None";
196230
if (obj instanceof Boolean) return ((Boolean) obj) ? "True" : "False";
197-
if (obj instanceof String) return "'" + obj + "'";
231+
if (obj instanceof String) return "'" + escapePythonString((String) obj) + "'";
198232
if (obj instanceof Number) return obj.toString();
199233
if (obj instanceof List) {
200234
List<?> list = (List<?>) obj;
@@ -259,9 +293,11 @@ private static void write(int port, String name, Object val, int delta) {
259293
return;
260294
}
261295
Files.write(Paths.get(path), content.toString().getBytes());
262-
} catch (IOException | InterruptedException e) {
296+
} catch (InterruptedException e) {
263297
Thread.currentThread().interrupt();
264298
System.out.println("skipping " + outpath + port + "/" + name);
299+
} catch (IOException e) {
300+
System.out.println("skipping " + outpath + port + "/" + name);
265301
}
266302
}
267303

@@ -370,7 +406,9 @@ Map<String, Object> parseDict() {
370406
Object value = parseExpression();
371407
map.put(key.toString(), value);
372408
skipWhitespace();
373-
if (pos >= input.length()) break;
409+
if (pos >= input.length()) {
410+
throw new IllegalArgumentException("Unterminated dict: missing '}'");
411+
}
374412
if (input.charAt(pos) == '}') {
375413
pos++;
376414
break;
@@ -402,7 +440,9 @@ List<Object> parseList() {
402440
skipWhitespace();
403441
list.add(parseExpression());
404442
skipWhitespace();
405-
if (pos >= input.length()) break;
443+
if (pos >= input.length()) {
444+
throw new IllegalArgumentException("Unterminated list: missing ']'");
445+
}
406446
if (input.charAt(pos) == ']') {
407447
pos++;
408448
break;
@@ -434,7 +474,9 @@ List<Object> parseTuple() {
434474
skipWhitespace();
435475
list.add(parseExpression());
436476
skipWhitespace();
437-
if (pos >= input.length()) break;
477+
if (pos >= input.length()) {
478+
throw new IllegalArgumentException("Unterminated tuple: missing ')'");
479+
}
438480
if (input.charAt(pos) == ')') {
439481
pos++;
440482
break;

0 commit comments

Comments
 (0)