From 210a388fc9b50bcec0d406c4f77458a14814f585 Mon Sep 17 00:00:00 2001 From: Ganesh Patil <7030871503ganeshpatil@gmail.com> Date: Mon, 9 Feb 2026 16:31:10 +0530 Subject: [PATCH 1/2] fix(java): implement literalEval and restore Java execution path The Java implementation of concoredocker was fundamentally broken. literalEval() was implemented as a stub returning an empty HashMap, causing read(), write(), defaultMaxTime(), and initVal() to silently malfunction. Changes: - Implement proper Python-style literal parsing in literalEval() - Supports dicts: {'key': value} - Supports lists: [val1, val2] - Supports numbers, strings, booleans, None - Fix unchanged() to return boolean (was void) - Fix defaultMaxTime() to read actual integer value - Fix read() and write() to properly handle List types - Add simtime tracking to match Python semantics - Add TestLiteralEval.java with 15 test cases Fixes #228 --- TestLiteralEval.java | 218 +++++++++++++++++++++++++ concoredocker.java | 376 ++++++++++++++++++++++++++++++++++++++----- 2 files changed, 557 insertions(+), 37 deletions(-) create mode 100644 TestLiteralEval.java diff --git a/TestLiteralEval.java b/TestLiteralEval.java new file mode 100644 index 0000000..d14a6db --- /dev/null +++ b/TestLiteralEval.java @@ -0,0 +1,218 @@ +import java.util.List; +import java.util.Map; +import java.lang.reflect.Method; + +/** + * Test cases for the literalEval implementation in concoredocker. + * This verifies the fix for GitHub Issue #228. + */ +public class TestLiteralEval { + private static int passed = 0; + private static int failed = 0; + + public static void main(String[] args) throws Exception { + System.out.println("Testing literalEval implementation...\n"); + + // Get the private literalEval method via reflection + Method literalEval = concoredocker.class.getDeclaredMethod("literalEval", String.class); + literalEval.setAccessible(true); + + // Test 1: Simple dictionary (port file format) + testDict(literalEval, "{'PYM': 1}", "PYM", 1); + + // Test 2: Dictionary with multiple keys + testDict(literalEval, "{'CU': 1, 'PYM': 2}", "CU", 1); + + // Test 3: Simple list (data format) + testList(literalEval, "[0.0, 0.0]", 2); + + // Test 4: List with mixed types + testList(literalEval, "[0, 1.5, 2.3]", 3); + + // Test 5: Simple integer (maxtime format) + testNumber(literalEval, "100", 100); + + // Test 6: Float number + testNumber(literalEval, "3.14", 3.14); + + // Test 7: Negative number + testNumber(literalEval, "-42", -42); + + // Test 8: String value + testString(literalEval, "'hello'", "hello"); + + // Test 9: Double-quoted string + testString(literalEval, "\"world\"", "world"); + + // Test 10: Empty dictionary + testEmptyDict(literalEval, "{}"); + + // Test 11: Empty list + testEmptyList(literalEval, "[]"); + + // Test 12: Boolean True + testBoolean(literalEval, "True", true); + + // Test 13: Boolean False + testBoolean(literalEval, "False", false); + + // Test 14: Nested structure (dict with list value) + testNestedDictList(literalEval, "{'values': [1, 2, 3]}"); + + // Test 15: Scientific notation + testNumber(literalEval, "1.5e-3", 0.0015); + + System.out.println("\n========================================"); + System.out.println("Results: " + passed + " passed, " + failed + " failed"); + System.out.println("========================================"); + + if (failed > 0) { + System.exit(1); + } + } + + @SuppressWarnings("unchecked") + private static void testDict(Method literalEval, String input, String key, Object expectedValue) { + try { + Object result = literalEval.invoke(null, input); + if (result instanceof Map) { + Map map = (Map) result; + if (map.containsKey(key)) { + Object actual = map.get(key); + if (actual instanceof Number && expectedValue instanceof Number) { + if (((Number) actual).doubleValue() == ((Number) expectedValue).doubleValue()) { + pass("Dict parse: " + input); + return; + } + } else if (actual.equals(expectedValue)) { + pass("Dict parse: " + input); + return; + } + } + } + fail("Dict parse: " + input, "Expected key '" + key + "' = " + expectedValue + ", got: " + result); + } catch (Exception e) { + fail("Dict parse: " + input, e.getMessage()); + } + } + + private static void testList(Method literalEval, String input, int expectedSize) { + try { + Object result = literalEval.invoke(null, input); + if (result instanceof List) { + List list = (List) result; + if (list.size() == expectedSize) { + pass("List parse: " + input + " (size=" + expectedSize + ")"); + return; + } + fail("List parse: " + input, "Expected size " + expectedSize + ", got " + list.size()); + } else { + fail("List parse: " + input, "Expected List, got " + result.getClass().getSimpleName()); + } + } catch (Exception e) { + fail("List parse: " + input, e.getMessage()); + } + } + + private static void testNumber(Method literalEval, String input, double expectedValue) { + try { + Object result = literalEval.invoke(null, input); + if (result instanceof Number) { + double actual = ((Number) result).doubleValue(); + if (Math.abs(actual - expectedValue) < 0.0001) { + pass("Number parse: " + input + " = " + actual); + return; + } + fail("Number parse: " + input, "Expected " + expectedValue + ", got " + actual); + } else { + fail("Number parse: " + input, "Expected Number, got " + result.getClass().getSimpleName()); + } + } catch (Exception e) { + fail("Number parse: " + input, e.getMessage()); + } + } + + private static void testString(Method literalEval, String input, String expectedValue) { + try { + Object result = literalEval.invoke(null, input); + if (result instanceof String) { + if (result.equals(expectedValue)) { + pass("String parse: " + input); + return; + } + fail("String parse: " + input, "Expected '" + expectedValue + "', got '" + result + "'"); + } else { + fail("String parse: " + input, "Expected String, got " + result.getClass().getSimpleName()); + } + } catch (Exception e) { + fail("String parse: " + input, e.getMessage()); + } + } + + private static void testEmptyDict(Method literalEval, String input) { + try { + Object result = literalEval.invoke(null, input); + if (result instanceof Map && ((Map) result).isEmpty()) { + pass("Empty dict parse: " + input); + } else { + fail("Empty dict parse: " + input, "Expected empty Map, got " + result); + } + } catch (Exception e) { + fail("Empty dict parse: " + input, e.getMessage()); + } + } + + private static void testEmptyList(Method literalEval, String input) { + try { + Object result = literalEval.invoke(null, input); + if (result instanceof List && ((List) result).isEmpty()) { + pass("Empty list parse: " + input); + } else { + fail("Empty list parse: " + input, "Expected empty List, got " + result); + } + } catch (Exception e) { + fail("Empty list parse: " + input, e.getMessage()); + } + } + + private static void testBoolean(Method literalEval, String input, boolean expectedValue) { + try { + Object result = literalEval.invoke(null, input); + if (result instanceof Boolean && result.equals(expectedValue)) { + pass("Boolean parse: " + input); + } else { + fail("Boolean parse: " + input, "Expected " + expectedValue + ", got " + result); + } + } catch (Exception e) { + fail("Boolean parse: " + input, e.getMessage()); + } + } + + @SuppressWarnings("unchecked") + private static void testNestedDictList(Method literalEval, String input) { + try { + Object result = literalEval.invoke(null, input); + if (result instanceof Map) { + Map map = (Map) result; + Object values = map.get("values"); + if (values instanceof List && ((List) values).size() == 3) { + pass("Nested dict/list parse: " + input); + return; + } + } + fail("Nested dict/list parse: " + input, "Got: " + result); + } catch (Exception e) { + fail("Nested dict/list parse: " + input, e.getMessage()); + } + } + + private static void pass(String test) { + System.out.println("[PASS] " + test); + passed++; + } + + private static void fail(String test, String reason) { + System.out.println("[FAIL] " + test + " - " + reason); + failed++; + } +} diff --git a/concoredocker.java b/concoredocker.java index dde521c..ba1abff 100644 --- a/concoredocker.java +++ b/concoredocker.java @@ -1,9 +1,17 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Paths; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; +/** + * Java implementation of concore Docker communication. + * + * This class provides file-based inter-process communication for control systems, + * mirroring the functionality of concoredocker.py. + */ public class concoredocker { private static Map iport = new HashMap<>(); private static Map oport = new HashMap<>(); @@ -15,22 +23,23 @@ public class concoredocker { private static String outpath = "/out"; private static Map params = new HashMap<>(); private static int maxtime; + private static int simtime = 0; public static void main(String[] args) { try { - iport = parseFile("concore.iport"); + iport = parseFileAsMap("concore.iport"); } catch (IOException e) { e.printStackTrace(); } try { - oport = parseFile("concore.oport"); + oport = parseFileAsMap("concore.oport"); } catch (IOException e) { e.printStackTrace(); } try { String sparams = new String(Files.readAllBytes(Paths.get(inpath + "1/concore.params"))); - if (sparams.charAt(0) == '"') { // windows keeps "" need to remove + if (sparams.length() > 0 && sparams.charAt(0) == '"') { // windows keeps "" need to remove sparams = sparams.substring(1); sparams = sparams.substring(0, sparams.indexOf('"')); } @@ -40,7 +49,12 @@ public static void main(String[] args) { System.out.println("converted sparams: " + sparams); } try { - params = literalEval(sparams); + Object parsed = literalEval(sparams); + if (parsed instanceof Map) { + @SuppressWarnings("unchecked") + Map parsedMap = (Map) parsed; + params = parsedMap; + } } catch (Exception e) { System.out.println("bad params: " + sparams); } @@ -51,26 +65,50 @@ public static void main(String[] args) { defaultMaxTime(100); } - private static Map parseFile(String filename) throws IOException { + /** + * Parses a file containing a Python-style dictionary literal. + */ + private static Map parseFileAsMap(String filename) throws IOException { String content = new String(Files.readAllBytes(Paths.get(filename))); - return literalEval(content); + Object result = literalEval(content); + if (result instanceof Map) { + @SuppressWarnings("unchecked") + Map map = (Map) result; + return map; + } + return new HashMap<>(); } + /** + * Sets maxtime from concore.maxtime file, or uses defaultValue if file not found. + * The file contains a simple integer value. + */ private static void defaultMaxTime(int defaultValue) { try { String content = new String(Files.readAllBytes(Paths.get(inpath + "1/concore.maxtime"))); - maxtime = literalEval(content).size(); + Object parsed = literalEval(content.trim()); + if (parsed instanceof Number) { + maxtime = ((Number) parsed).intValue(); + } else { + maxtime = defaultValue; + } } catch (IOException e) { maxtime = defaultValue; } } - private static void unchanged() { + /** + * Checks if the accumulated input string has changed since last call. + * Returns true if unchanged (and clears s), false if changed (and updates olds). + * This matches the Python implementation semantics. + */ + private static boolean unchanged() { if (olds.equals(s)) { s = ""; - } else { - olds = s; + return true; } + olds = s; + return false; } private static Object tryParam(String n, Object i) { @@ -81,6 +119,11 @@ private static Object tryParam(String n, Object i) { } } + /** + * Reads data from a port file. Returns the values after extracting simtime. + * Input format: [simtime, val1, val2, ...] + * Returns: [val1, val2, ...] as List + */ private static Object read(int port, String name, String initstr) { try { String ins = new String(Files.readAllBytes(Paths.get(inpath + port + "/" + name))); @@ -90,60 +133,319 @@ private static Object read(int port, String name, String initstr) { retrycount++; } s += ins; - Object[] inval = new Map[] { literalEval(ins) }; - int simtime = Math.max((int) inval[0], 0); // assuming simtime is an integer - return inval[1]; + Object parsed = literalEval(ins); + if (parsed instanceof List) { + @SuppressWarnings("unchecked") + List inval = (List) parsed; + if (!inval.isEmpty()) { + // First element is simtime + Object first = inval.get(0); + if (first instanceof Number) { + simtime = Math.max(simtime, ((Number) first).intValue()); + } + // Return remaining elements (values after simtime) + return inval.subList(1, inval.size()); + } + } + return initstr; } catch (IOException | InterruptedException e) { return initstr; } } + /** + * Writes data to a port file. + * Output format: [simtime + delta, val1, val2, ...] + */ private static void write(int port, String name, Object val, int delta) { try { String path = outpath + port + "/" + name; StringBuilder content = new StringBuilder(); if (val instanceof String) { Thread.sleep(2 * delay); - } else if (!(val instanceof Object[])) { - System.out.println("mywrite must have list or str"); - System.exit(1); - } - if (val instanceof Object[]) { + content.append(val); + } else if (val instanceof List) { + @SuppressWarnings("unchecked") + List listVal = (List) val; + content.append("[").append(simtime + delta); + for (Object item : listVal) { + content.append(",").append(item); + } + content.append("]"); + simtime += delta; + } else if (val instanceof Object[]) { Object[] arrayVal = (Object[]) val; - content.append("[") - .append(maxtime + delta) - .append(",") - .append(arrayVal[0]); - for (int i = 1; i < arrayVal.length; i++) { - content.append(",") - .append(arrayVal[i]); + content.append("[").append(simtime + delta); + for (Object item : arrayVal) { + content.append(",").append(item); } content.append("]"); + simtime += delta; } else { - content.append(val); + System.out.println("write must have list or str"); + return; } Files.write(Paths.get(path), content.toString().getBytes()); } catch (IOException | InterruptedException e) { - System.out.println("skipping" + outpath + port + "/" + name); + System.out.println("skipping " + outpath + port + "/" + name); } } - private static Object[] initVal(String simtimeVal) { - int simtime = 0; - Object[] val = new Object[] {}; + /** + * Parses an initial value string and extracts values after simtime. + * Input format: "[simtime, val1, val2, ...]" + * Returns: [val1, val2, ...] as List + */ + private static List initVal(String simtimeVal) { try { - Object[] arrayVal = new Map[] { literalEval(simtimeVal) }; - simtime = (int) arrayVal[0]; // assuming simtime is an integer - val = new Object[arrayVal.length - 1]; - System.arraycopy(arrayVal, 1, val, 0, val.length); + Object parsed = literalEval(simtimeVal); + if (parsed instanceof List) { + @SuppressWarnings("unchecked") + List val = (List) parsed; + if (!val.isEmpty()) { + Object first = val.get(0); + if (first instanceof Number) { + simtime = ((Number) first).intValue(); + } + return val.subList(1, val.size()); + } + } } catch (Exception e) { e.printStackTrace(); } - return val; + return new ArrayList<>(); + } + + /** + * Parses a Python-style literal string and returns the corresponding Java object. + * + * Supports: + * - Dictionaries: {'key': value, ...} -> Map + * - Lists: [val1, val2, ...] -> List + * - Numbers: 42, 3.14 -> Integer or Double + * - Strings: 'hello' or "hello" -> String + * - Booleans: True, False -> Boolean + * - None -> null + * + * This implementation replaces the previous stub that always returned an empty HashMap, + * which caused read(), defaultMaxTime(), write(), and initVal() to silently fail. + * + * @param input The Python literal string to parse + * @return The parsed Java object (Map, List, Number, String, Boolean, or null) + * @throws IllegalArgumentException if the input cannot be parsed + */ + private static Object literalEval(String input) { + if (input == null) { + throw new IllegalArgumentException("Cannot parse null input"); + } + Parser parser = new Parser(input.trim()); + Object result = parser.parseValue(); + parser.skipWhitespace(); + if (parser.hasMore()) { + throw new IllegalArgumentException("Unexpected characters after parsed value: " + parser.remaining()); + } + return result; } - private static Map literalEval(String s) { + /** + * Simple recursive descent parser for Python-style literals. + */ + private static class Parser { + private final String input; + private int pos = 0; - return new HashMap<>(); + Parser(String input) { + this.input = input; + } + + boolean hasMore() { + return pos < input.length(); + } + + String remaining() { + return input.substring(pos); + } + + char peek() { + if (!hasMore()) { + throw new IllegalArgumentException("Unexpected end of input"); + } + return input.charAt(pos); + } + + char consume() { + return input.charAt(pos++); + } + + void skipWhitespace() { + while (hasMore() && Character.isWhitespace(input.charAt(pos))) { + pos++; + } + } + + void expect(char c) { + skipWhitespace(); + if (!hasMore() || consume() != c) { + throw new IllegalArgumentException("Expected '" + c + "' at position " + (pos - 1)); + } + } + + Object parseValue() { + skipWhitespace(); + if (!hasMore()) { + throw new IllegalArgumentException("Unexpected end of input"); + } + char c = peek(); + if (c == '{') { + return parseDict(); + } else if (c == '[') { + return parseList(); + } else if (c == '\'' || c == '"') { + return parseString(); + } else if (c == '-' || Character.isDigit(c)) { + return parseNumber(); + } else if (input.substring(pos).startsWith("True")) { + pos += 4; + return Boolean.TRUE; + } else if (input.substring(pos).startsWith("False")) { + pos += 5; + return Boolean.FALSE; + } else if (input.substring(pos).startsWith("None")) { + pos += 4; + return null; + } else { + throw new IllegalArgumentException("Unexpected character '" + c + "' at position " + pos); + } + } + + Map parseDict() { + Map map = new HashMap<>(); + expect('{'); + skipWhitespace(); + if (hasMore() && peek() == '}') { + consume(); + return map; + } + while (true) { + skipWhitespace(); + // Parse key (must be a string) + String key; + char c = peek(); + if (c == '\'' || c == '"') { + key = parseString(); + } else { + throw new IllegalArgumentException("Dictionary key must be a string at position " + pos); + } + skipWhitespace(); + expect(':'); + Object value = parseValue(); + map.put(key, value); + skipWhitespace(); + if (!hasMore()) { + throw new IllegalArgumentException("Unexpected end of input in dictionary"); + } + c = peek(); + if (c == '}') { + consume(); + break; + } else if (c == ',') { + consume(); + } else { + throw new IllegalArgumentException("Expected ',' or '}' at position " + pos); + } + } + return map; + } + + List parseList() { + List list = new ArrayList<>(); + expect('['); + skipWhitespace(); + if (hasMore() && peek() == ']') { + consume(); + return list; + } + while (true) { + list.add(parseValue()); + skipWhitespace(); + if (!hasMore()) { + throw new IllegalArgumentException("Unexpected end of input in list"); + } + char c = peek(); + if (c == ']') { + consume(); + break; + } else if (c == ',') { + consume(); + } else { + throw new IllegalArgumentException("Expected ',' or ']' at position " + pos); + } + } + return list; + } + + String parseString() { + char quote = consume(); // ' or " + StringBuilder sb = new StringBuilder(); + while (hasMore()) { + char c = consume(); + if (c == quote) { + return sb.toString(); + } else if (c == '\\' && hasMore()) { + char escaped = consume(); + switch (escaped) { + case 'n': sb.append('\n'); break; + case 't': sb.append('\t'); break; + case 'r': sb.append('\r'); break; + case '\\': sb.append('\\'); break; + case '\'': sb.append('\''); break; + case '"': sb.append('"'); break; + default: sb.append(escaped); break; + } + } else { + sb.append(c); + } + } + throw new IllegalArgumentException("Unterminated string"); + } + + Number parseNumber() { + int start = pos; + boolean isFloat = false; + if (peek() == '-') { + consume(); + } + while (hasMore() && Character.isDigit(peek())) { + consume(); + } + if (hasMore() && peek() == '.') { + isFloat = true; + consume(); + while (hasMore() && Character.isDigit(peek())) { + consume(); + } + } + // Handle scientific notation + if (hasMore() && (peek() == 'e' || peek() == 'E')) { + isFloat = true; + consume(); + if (hasMore() && (peek() == '+' || peek() == '-')) { + consume(); + } + while (hasMore() && Character.isDigit(peek())) { + consume(); + } + } + String numStr = input.substring(start, pos); + if (isFloat) { + return Double.parseDouble(numStr); + } else { + try { + return Integer.parseInt(numStr); + } catch (NumberFormatException e) { + return Long.parseLong(numStr); + } + } + } } } From a4f6a6bd54fb907647be9d8fd7c15218860bc925 Mon Sep 17 00:00:00 2001 From: Ganesh Patil <7030871503ganeshpatil@gmail.com> Date: Mon, 9 Feb 2026 16:48:15 +0530 Subject: [PATCH 2/2] fix(java): address all Copilot review suggestions Changes based on PR review: - Use double for simtime to preserve fractional values - Change delay from 1ms to 1000ms (matches Python's time.sleep(1)) - Add maxRetries limit in read() to avoid infinite blocking - Restore thread interrupt status in InterruptedException handlers - Catch RuntimeException in defaultMaxTime() for parse errors - Add try-catch in parseFileAsMap() with logging for malformed files - Wrap parseDouble/parseLong in try-catch with clear exceptions - Add toPythonLiteral() for proper serialization (True/False/None/strings) - Improve error messages throughout All changes align Java behavior with Python reference implementation. --- TestLiteralEval.java | 53 +++++++++++ concoredocker.java | 218 +++++++++++++++++++++++++++++++++---------- 2 files changed, 223 insertions(+), 48 deletions(-) diff --git a/TestLiteralEval.java b/TestLiteralEval.java index d14a6db..793630a 100644 --- a/TestLiteralEval.java +++ b/TestLiteralEval.java @@ -1,6 +1,7 @@ import java.util.List; import java.util.Map; import java.lang.reflect.Method; +import java.lang.reflect.Field; /** * Test cases for the literalEval implementation in concoredocker. @@ -17,6 +18,10 @@ public static void main(String[] args) throws Exception { Method literalEval = concoredocker.class.getDeclaredMethod("literalEval", String.class); literalEval.setAccessible(true); + // Get toPythonLiteral method for serialization tests + Method toPythonLiteral = concoredocker.class.getDeclaredMethod("toPythonLiteral", Object.class); + toPythonLiteral.setAccessible(true); + // Test 1: Simple dictionary (port file format) testDict(literalEval, "{'PYM': 1}", "PYM", 1); @@ -62,6 +67,21 @@ public static void main(String[] args) throws Exception { // Test 15: Scientific notation testNumber(literalEval, "1.5e-3", 0.0015); + // Test 16: Python literal serialization - Boolean + testSerialization(toPythonLiteral, Boolean.TRUE, "True"); + + // Test 17: Python literal serialization - Boolean False + testSerialization(toPythonLiteral, Boolean.FALSE, "False"); + + // Test 18: Python literal serialization - null + testSerialization(toPythonLiteral, null, "None"); + + // Test 19: Python literal serialization - String with quotes + testSerialization(toPythonLiteral, "hello", "'hello'"); + + // Test 20: Fractional simtime preserved (double) + testFractionalSimtime(literalEval); + System.out.println("\n========================================"); System.out.println("Results: " + passed + " passed, " + failed + " failed"); System.out.println("========================================"); @@ -206,6 +226,39 @@ private static void testNestedDictList(Method literalEval, String input) { } } + private static void testSerialization(Method toPythonLiteral, Object input, String expected) { + try { + Object result = toPythonLiteral.invoke(null, input); + if (expected.equals(result)) { + pass("Serialization: " + input + " -> " + result); + } else { + fail("Serialization: " + input, "Expected '" + expected + "', got '" + result + "'"); + } + } catch (Exception e) { + fail("Serialization: " + input, e.getMessage()); + } + } + + private static void testFractionalSimtime(Method literalEval) { + try { + // Test that parsing a list with fractional simtime works + Object result = literalEval.invoke(null, "[0.5, 1.2, 3.4]"); + if (result instanceof List) { + List list = (List) result; + if (list.size() == 3) { + Object first = list.get(0); + if (first instanceof Number && ((Number) first).doubleValue() == 0.5) { + pass("Fractional simtime: [0.5, 1.2, 3.4] preserves 0.5"); + return; + } + } + } + fail("Fractional simtime", "Could not verify fractional value preservation"); + } catch (Exception e) { + fail("Fractional simtime", e.getMessage()); + } + } + private static void pass(String test) { System.out.println("[PASS] " + test); passed++; diff --git a/concoredocker.java b/concoredocker.java index ba1abff..5a1d04c 100644 --- a/concoredocker.java +++ b/concoredocker.java @@ -17,13 +17,16 @@ public class concoredocker { private static Map oport = new HashMap<>(); private static String s = ""; private static String olds = ""; - private static int delay = 1; + // delay in milliseconds (Python uses time.sleep(1) = 1 second) + private static int delay = 1000; private static int retrycount = 0; + private static int maxRetries = 5; private static String inpath = "/in"; private static String outpath = "/out"; private static Map params = new HashMap<>(); private static int maxtime; - private static int simtime = 0; + // simtime as double to preserve fractional values (matches Python behavior) + private static double simtime = 0; public static void main(String[] args) { try { @@ -67,14 +70,25 @@ public static void main(String[] args) { /** * Parses a file containing a Python-style dictionary literal. + * Returns empty map if file is empty or malformed (matches Python safe_literal_eval behavior). */ private static Map parseFileAsMap(String filename) throws IOException { String content = new String(Files.readAllBytes(Paths.get(filename))); - Object result = literalEval(content); - if (result instanceof Map) { - @SuppressWarnings("unchecked") - Map map = (Map) result; - return map; + content = content.trim(); + if (content.isEmpty()) { + // Empty file: treat as empty map + return new HashMap<>(); + } + try { + Object result = literalEval(content); + if (result instanceof Map) { + @SuppressWarnings("unchecked") + Map map = (Map) result; + return map; + } + } catch (IllegalArgumentException e) { + // Malformed content: log and fall back to empty map + System.err.println("Failed to parse file as map: " + filename + " (" + e.getMessage() + ")"); } return new HashMap<>(); } @@ -82,6 +96,8 @@ private static Map parseFileAsMap(String filename) throws IOExce /** * Sets maxtime from concore.maxtime file, or uses defaultValue if file not found. * The file contains a simple integer value. + * Catches both IOException (file not found) and RuntimeException (parse errors) + * to match Python safe_literal_eval behavior. */ private static void defaultMaxTime(int defaultValue) { try { @@ -92,7 +108,7 @@ private static void defaultMaxTime(int defaultValue) { } else { maxtime = defaultValue; } - } catch (IOException e) { + } catch (IOException | RuntimeException e) { maxtime = defaultValue; } } @@ -123,71 +139,168 @@ private static Object tryParam(String n, Object i) { * Reads data from a port file. Returns the values after extracting simtime. * Input format: [simtime, val1, val2, ...] * Returns: [val1, val2, ...] as List + * Includes max retry limit to avoid infinite blocking (matches Python behavior). */ private static Object read(int port, String name, String initstr) { + String filePath = inpath + port + "/" + name; + try { + Thread.sleep(delay); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return initstr; + } + + String ins; try { - String ins = new String(Files.readAllBytes(Paths.get(inpath + port + "/" + name))); - while (ins.length() == 0) { + ins = new String(Files.readAllBytes(Paths.get(filePath))); + } catch (IOException e) { + System.out.println("File " + filePath + " not found, using default value."); + return initstr; + } + + int attempts = 0; + while (ins.length() == 0 && attempts < maxRetries) { + try { Thread.sleep(delay); - ins = new String(Files.readAllBytes(Paths.get(inpath + port + "/" + name))); - retrycount++; + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return initstr; + } + try { + ins = new String(Files.readAllBytes(Paths.get(filePath))); + } catch (IOException e) { + System.out.println("Retry " + (attempts + 1) + ": Error reading " + filePath); } - s += ins; + attempts++; + retrycount++; + } + + if (ins.length() == 0) { + System.out.println("Max retries reached for " + filePath + ", using default value."); + return initstr; + } + + s += ins; + try { Object parsed = literalEval(ins); if (parsed instanceof List) { @SuppressWarnings("unchecked") List inval = (List) parsed; if (!inval.isEmpty()) { - // First element is simtime + // First element is simtime (preserve as double for fractional values) Object first = inval.get(0); if (first instanceof Number) { - simtime = Math.max(simtime, ((Number) first).intValue()); + double firstSimtime = ((Number) first).doubleValue(); + simtime = Math.max(simtime, firstSimtime); } // Return remaining elements (values after simtime) return inval.subList(1, inval.size()); } } - return initstr; - } catch (IOException | InterruptedException e) { - return initstr; + } catch (IllegalArgumentException e) { + System.out.println("Error parsing " + ins + ": " + e.getMessage()); } + return initstr; } /** * Writes data to a port file. * Output format: [simtime + delta, val1, val2, ...] + * Uses Python-literal-compatible serialization for proper interoperability. */ private static void write(int port, String name, Object val, int delta) { - try { - String path = outpath + port + "/" + name; - StringBuilder content = new StringBuilder(); - if (val instanceof String) { + String path = outpath + port + "/" + name; + StringBuilder content = new StringBuilder(); + + if (val instanceof String) { + try { Thread.sleep(2 * delay); - content.append(val); - } else if (val instanceof List) { - @SuppressWarnings("unchecked") - List listVal = (List) val; - content.append("[").append(simtime + delta); - for (Object item : listVal) { - content.append(",").append(item); - } - content.append("]"); - simtime += delta; - } else if (val instanceof Object[]) { - Object[] arrayVal = (Object[]) val; - content.append("[").append(simtime + delta); - for (Object item : arrayVal) { - content.append(",").append(item); - } - content.append("]"); - simtime += delta; - } else { - System.out.println("write must have list or str"); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); return; } + content.append(val); + } else if (val instanceof List) { + @SuppressWarnings("unchecked") + List listVal = (List) val; + content.append("[").append(simtime + delta); + for (Object item : listVal) { + content.append(",").append(toPythonLiteral(item)); + } + content.append("]"); + simtime += delta; + } else if (val instanceof Object[]) { + Object[] arrayVal = (Object[]) val; + content.append("[").append(simtime + delta); + for (Object item : arrayVal) { + content.append(",").append(toPythonLiteral(item)); + } + content.append("]"); + simtime += delta; + } else { + System.out.println("write must have list or str"); + return; + } + + try { Files.write(Paths.get(path), content.toString().getBytes()); - } catch (IOException | InterruptedException e) { - System.out.println("skipping " + outpath + port + "/" + name); + } catch (IOException e) { + System.out.println("Error writing to " + path + ": " + e.getMessage()); + } + } + + /** + * Converts a Java object to its Python-literal-compatible string representation. + * This ensures proper interoperability when the receiving side parses the output. + */ + private static String toPythonLiteral(Object obj) { + if (obj == null) { + return "None"; + } else if (obj instanceof Boolean) { + return ((Boolean) obj) ? "True" : "False"; + } else if (obj instanceof String) { + // Quote strings and escape special characters + String s = (String) obj; + StringBuilder sb = new StringBuilder("'"); + for (char c : s.toCharArray()) { + 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; + } + } + sb.append("'"); + return sb.toString(); + } else if (obj instanceof Number) { + return obj.toString(); + } else if (obj instanceof List) { + @SuppressWarnings("unchecked") + List list = (List) obj; + StringBuilder sb = new StringBuilder("["); + for (int i = 0; i < list.size(); i++) { + if (i > 0) sb.append(", "); + sb.append(toPythonLiteral(list.get(i))); + } + sb.append("]"); + return sb.toString(); + } else if (obj instanceof Map) { + @SuppressWarnings("unchecked") + Map map = (Map) obj; + StringBuilder sb = new StringBuilder("{"); + boolean first = true; + for (Map.Entry entry : map.entrySet()) { + if (!first) sb.append(", "); + first = false; + sb.append(toPythonLiteral(entry.getKey())).append(": ").append(toPythonLiteral(entry.getValue())); + } + sb.append("}"); + return sb.toString(); + } else { + // Fallback: use toString() + return obj.toString(); } } @@ -205,13 +318,14 @@ private static List initVal(String simtimeVal) { if (!val.isEmpty()) { Object first = val.get(0); if (first instanceof Number) { - simtime = ((Number) first).intValue(); + // Preserve fractional simtime values + simtime = ((Number) first).doubleValue(); } return val.subList(1, val.size()); } } } catch (Exception e) { - e.printStackTrace(); + System.out.println("Error parsing simtime_val: " + e.getMessage()); } return new ArrayList<>(); } @@ -438,12 +552,20 @@ Number parseNumber() { } String numStr = input.substring(start, pos); if (isFloat) { - return Double.parseDouble(numStr); + try { + return Double.parseDouble(numStr); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Invalid numeric value: " + numStr, e); + } } else { try { return Integer.parseInt(numStr); } catch (NumberFormatException e) { - return Long.parseLong(numStr); + try { + return Long.parseLong(numStr); + } catch (NumberFormatException e2) { + throw new IllegalArgumentException("Invalid numeric value: " + numStr, e2); + } } } }