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
51 changes: 43 additions & 8 deletions TestConcoredockerApi.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ public static void main(String[] args) {
testInitValExtractsSimtime();
testInitValReturnsRemainingValues();
testOutputFileMatchesPythonWireFormat();
testReadFileNotFound();
testReadRetriesExceeded();
testReadParseError();

System.out.println("\n=== Results: " + passed + " passed, " + failed + " failed out of " + (passed + failed) + " tests ===");
if (failed > 0) {
Expand Down Expand Up @@ -102,10 +105,11 @@ static void testReadParsesFileAndStripsSimtime() {
concoredocker.setInPath(tmp.toString());
writeFile(tmp, 1, "sensor", "[0.0, 42.0, 99.0]");

List<Object> result = concoredocker.read(1, "sensor", "[0.0, 0.0, 0.0]");
check("read: strips simtime, size=2", 2, result.size());
check("read: val1 correct", 42.0, result.get(0));
check("read: val2 correct", 99.0, result.get(1));
concoredocker.ReadResult result = concoredocker.read(1, "sensor", "[0.0, 0.0, 0.0]");
check("read: status SUCCESS", concoredocker.ReadStatus.SUCCESS, result.status);
check("read: strips simtime, size=2", 2, result.data.size());
check("read: val1 correct", 42.0, result.data.get(0));
check("read: val2 correct", 99.0, result.data.get(1));
}

static void testReadWriteRoundtrip() {
Expand All @@ -119,10 +123,11 @@ static void testReadWriteRoundtrip() {
outVals.add(8.0);
concoredocker.write(1, "data", outVals, 1);

List<Object> inVals = concoredocker.read(1, "data", "[0.0, 0.0, 0.0]");
check("roundtrip: size", 2, inVals.size());
check("roundtrip: val1", 7.0, inVals.get(0));
check("roundtrip: val2", 8.0, inVals.get(1));
concoredocker.ReadResult inVals = concoredocker.read(1, "data", "[0.0, 0.0, 0.0]");
check("roundtrip: status", concoredocker.ReadStatus.SUCCESS, inVals.status);
check("roundtrip: size", 2, inVals.data.size());
check("roundtrip: val1", 7.0, inVals.data.get(0));
check("roundtrip: val2", 8.0, inVals.data.get(1));
}

static void testSimtimeAdvancesWithDelta() {
Expand Down Expand Up @@ -195,4 +200,34 @@ static void testOutputFileMatchesPythonWireFormat() {
Object reparsed = concoredocker.literalEval(raw);
check("wire format: re-parseable as list", true, reparsed instanceof List);
}

static void testReadFileNotFound() {
Path tmp = makeTempDir();
concoredocker.resetState();
concoredocker.setInPath(tmp.toString());
// no file written, port 1/missing does not exist
concoredocker.ReadResult result = concoredocker.read(1, "missing", "[0.0, 0.0]");
check("read file not found: status", concoredocker.ReadStatus.FILE_NOT_FOUND, result.status);
check("read file not found: data is default", 1, result.data.size());
}

static void testReadRetriesExceeded() {
Path tmp = makeTempDir();
concoredocker.resetState();
concoredocker.setInPath(tmp.toString());
writeFile(tmp, 1, "empty", ""); // always empty, exhausts retries
concoredocker.ReadResult result = concoredocker.read(1, "empty", "[0.0, 0.0]");
check("read retries exceeded: status", concoredocker.ReadStatus.RETRIES_EXCEEDED, result.status);
check("read retries exceeded: data is default", 1, result.data.size());
}

static void testReadParseError() {
Path tmp = makeTempDir();
concoredocker.resetState();
concoredocker.setInPath(tmp.toString());
writeFile(tmp, 1, "bad", "not_a_valid_list");
concoredocker.ReadResult result = concoredocker.read(1, "bad", "[0.0, 0.0]");
check("read parse error: status", concoredocker.ReadStatus.PARSE_ERROR, result.status);
check("read parse error: data is default", 1, result.data.size());
}
}
37 changes: 25 additions & 12 deletions concoredocker.java
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ public static Object tryParam(String n, Object i) {
* Returns: list of values after simtime
* Includes max retry limit to avoid infinite blocking (matches Python behavior).
*/
public static List<Object> read(int port, String name, String initstr) {
public static ReadResult read(int port, String name, String initstr) {
// Parse default value upfront for consistent return type
List<Object> defaultVal = new ArrayList<>();
try {
Expand All @@ -178,7 +178,7 @@ public static List<Object> read(int port, String name, String initstr) {
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
s += initstr;
return defaultVal;
return new ReadResult(ReadStatus.TIMEOUT, defaultVal);
}

String ins;
Expand All @@ -187,7 +187,7 @@ public static List<Object> read(int port, String name, String initstr) {
} catch (IOException e) {
System.out.println("File " + filePath + " not found, using default value.");
s += initstr;
return defaultVal;
return new ReadResult(ReadStatus.FILE_NOT_FOUND, defaultVal);
}

int attempts = 0;
Expand All @@ -197,7 +197,7 @@ public static List<Object> read(int port, String name, String initstr) {
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
s += initstr;
return defaultVal;
return new ReadResult(ReadStatus.TIMEOUT, defaultVal);
}
try {
ins = new String(Files.readAllBytes(Paths.get(filePath)));
Expand All @@ -210,7 +210,7 @@ public static List<Object> read(int port, String name, String initstr) {

if (ins.length() == 0) {
System.out.println("Max retries reached for " + filePath + ", using default value.");
return defaultVal;
return new ReadResult(ReadStatus.RETRIES_EXCEEDED, defaultVal);
}

s += ins;
Expand All @@ -219,12 +219,12 @@ public static List<Object> read(int port, String name, String initstr) {
if (!inval.isEmpty()) {
double firstSimtime = ((Number) inval.get(0)).doubleValue();
simtime = Math.max(simtime, firstSimtime);
return new ArrayList<>(inval.subList(1, inval.size()));
return new ReadResult(ReadStatus.SUCCESS, new ArrayList<>(inval.subList(1, inval.size())));
}
} catch (Exception e) {
System.out.println("Error parsing " + ins + ": " + e.getMessage());
}
return defaultVal;
return new ReadResult(ReadStatus.PARSE_ERROR, defaultVal);
}

/**
Expand Down Expand Up @@ -392,7 +392,7 @@ private static int zmqSocketTypeFromString(String s) {
* Reads data from a ZMQ port. Same wire format as file-based read:
* expects [simtime, val1, val2, ...], strips simtime, returns the rest.
*/
public static List<Object> read(String portName, String name, String initstr) {
public static ReadResult read(String portName, String name, String initstr) {
List<Object> defaultVal = new ArrayList<>();
try {
List<?> parsed = (List<?>) literalEval(initstr);
Expand All @@ -404,24 +404,24 @@ public static List<Object> read(String portName, String name, String initstr) {
ZeroMQPort port = zmqPorts.get(portName);
if (port == null) {
System.err.println("read: ZMQ port '" + portName + "' not initialized");
return defaultVal;
return new ReadResult(ReadStatus.FILE_NOT_FOUND, defaultVal);
}
String msg = port.recvWithRetry();
if (msg == null) {
System.err.println("read: ZMQ recv timeout on port '" + portName + "'");
return defaultVal;
return new ReadResult(ReadStatus.TIMEOUT, defaultVal);
}
s += msg;
try {
List<?> inval = (List<?>) literalEval(msg);
if (!inval.isEmpty()) {
simtime = Math.max(simtime, ((Number) inval.get(0)).doubleValue());
return new ArrayList<>(inval.subList(1, inval.size()));
return new ReadResult(ReadStatus.SUCCESS, new ArrayList<>(inval.subList(1, inval.size())));
}
} catch (Exception e) {
System.out.println("Error parsing ZMQ message '" + msg + "': " + e.getMessage());
}
return defaultVal;
return new ReadResult(ReadStatus.PARSE_ERROR, defaultVal);
}

/**
Expand Down Expand Up @@ -472,6 +472,19 @@ static Object literalEval(String s) {
return result;
}

public enum ReadStatus {
SUCCESS, FILE_NOT_FOUND, TIMEOUT, PARSE_ERROR, RETRIES_EXCEEDED
}

public static class ReadResult {
public final ReadStatus status;
public final List<Object> data;
ReadResult(ReadStatus status, List<Object> data) {
this.status = status;
this.data = data;
}
}

/**
* ZMQ socket wrapper with bind/connect, timeouts, and retry.
*/
Expand Down