|
| 1 | +import java.util.*; |
| 2 | + |
| 3 | +/** |
| 4 | + * Test suite for concoredocker.literalEval() recursive descent parser. |
| 5 | + * Covers: dicts, lists, tuples, numbers, strings, booleans, None, |
| 6 | + * nested structures, escape sequences, scientific notation, |
| 7 | + * toPythonLiteral serialization, and fractional simtime. |
| 8 | + */ |
| 9 | +public class TestLiteralEval { |
| 10 | + static int passed = 0; |
| 11 | + static int failed = 0; |
| 12 | + |
| 13 | + public static void main(String[] args) { |
| 14 | + testEmptyDict(); |
| 15 | + testSimpleDict(); |
| 16 | + testDictWithIntValues(); |
| 17 | + testEmptyList(); |
| 18 | + testSimpleList(); |
| 19 | + testListOfDoubles(); |
| 20 | + testNestedDictWithList(); |
| 21 | + testNestedListsDeep(); |
| 22 | + testBooleansAndNone(); |
| 23 | + testStringsWithCommas(); |
| 24 | + testStringsWithColons(); |
| 25 | + testStringEscapeSequences(); |
| 26 | + testScientificNotation(); |
| 27 | + testNegativeNumbers(); |
| 28 | + testTuple(); |
| 29 | + testTrailingComma(); |
| 30 | + testToPythonLiteralBooleans(); |
| 31 | + testToPythonLiteralNone(); |
| 32 | + testToPythonLiteralString(); |
| 33 | + testFractionalSimtime(); |
| 34 | + testRoundTripSerialization(); |
| 35 | + testStringEscapingSerialization(); |
| 36 | + testUnterminatedList(); |
| 37 | + testUnterminatedDict(); |
| 38 | + testUnterminatedTuple(); |
| 39 | + |
| 40 | + System.out.println("\n=== Results: " + passed + " passed, " + failed + " failed out of " + (passed + failed) + " tests ==="); |
| 41 | + if (failed > 0) { |
| 42 | + System.exit(1); |
| 43 | + } |
| 44 | + } |
| 45 | + |
| 46 | + static void check(String testName, Object expected, Object actual) { |
| 47 | + if (Objects.equals(expected, actual)) { |
| 48 | + System.out.println("PASS: " + testName); |
| 49 | + passed++; |
| 50 | + } else { |
| 51 | + System.out.println("FAIL: " + testName + " | expected: " + expected + " | actual: " + actual); |
| 52 | + failed++; |
| 53 | + } |
| 54 | + } |
| 55 | + |
| 56 | + static void testEmptyDict() { |
| 57 | + Object result = concoredocker.literalEval("{}"); |
| 58 | + check("empty dict", new HashMap<>(), result); |
| 59 | + } |
| 60 | + |
| 61 | + static void testSimpleDict() { |
| 62 | + @SuppressWarnings("unchecked") |
| 63 | + Map<String, Object> result = (Map<String, Object>) concoredocker.literalEval("{'PYM': 1}"); |
| 64 | + check("simple dict key", true, result.containsKey("PYM")); |
| 65 | + check("simple dict value", 1, result.get("PYM")); |
| 66 | + } |
| 67 | + |
| 68 | + static void testDictWithIntValues() { |
| 69 | + @SuppressWarnings("unchecked") |
| 70 | + Map<String, Object> result = (Map<String, Object>) concoredocker.literalEval("{'a': 10, 'b': 20}"); |
| 71 | + check("dict int value a", 10, result.get("a")); |
| 72 | + check("dict int value b", 20, result.get("b")); |
| 73 | + } |
| 74 | + |
| 75 | + static void testEmptyList() { |
| 76 | + Object result = concoredocker.literalEval("[]"); |
| 77 | + check("empty list", new ArrayList<>(), result); |
| 78 | + } |
| 79 | + |
| 80 | + static void testSimpleList() { |
| 81 | + @SuppressWarnings("unchecked") |
| 82 | + List<Object> result = (List<Object>) concoredocker.literalEval("[1, 2, 3]"); |
| 83 | + check("simple list size", 3, result.size()); |
| 84 | + check("simple list[0]", 1, result.get(0)); |
| 85 | + check("simple list[2]", 3, result.get(2)); |
| 86 | + } |
| 87 | + |
| 88 | + static void testListOfDoubles() { |
| 89 | + @SuppressWarnings("unchecked") |
| 90 | + List<Object> result = (List<Object>) concoredocker.literalEval("[0.0, 1.5, 2.7]"); |
| 91 | + check("list doubles[0]", 0.0, result.get(0)); |
| 92 | + check("list doubles[1]", 1.5, result.get(1)); |
| 93 | + } |
| 94 | + |
| 95 | + static void testNestedDictWithList() { |
| 96 | + @SuppressWarnings("unchecked") |
| 97 | + Map<String, Object> result = (Map<String, Object>) concoredocker.literalEval("{'key': [1, 2, 3]}"); |
| 98 | + check("nested dict has key", true, result.containsKey("key")); |
| 99 | + @SuppressWarnings("unchecked") |
| 100 | + List<Object> inner = (List<Object>) result.get("key"); |
| 101 | + check("nested list size", 3, inner.size()); |
| 102 | + check("nested list[0]", 1, inner.get(0)); |
| 103 | + } |
| 104 | + |
| 105 | + static void testNestedListsDeep() { |
| 106 | + @SuppressWarnings("unchecked") |
| 107 | + List<Object> result = (List<Object>) concoredocker.literalEval("[[1, 2], [3, 4]]"); |
| 108 | + check("nested lists size", 2, result.size()); |
| 109 | + @SuppressWarnings("unchecked") |
| 110 | + List<Object> inner = (List<Object>) result.get(0); |
| 111 | + check("inner list[0]", 1, inner.get(0)); |
| 112 | + check("inner list[1]", 2, inner.get(1)); |
| 113 | + } |
| 114 | + |
| 115 | + static void testBooleansAndNone() { |
| 116 | + @SuppressWarnings("unchecked") |
| 117 | + List<Object> result = (List<Object>) concoredocker.literalEval("[True, False, None]"); |
| 118 | + check("boolean True", Boolean.TRUE, result.get(0)); |
| 119 | + check("boolean False", Boolean.FALSE, result.get(1)); |
| 120 | + check("None", null, result.get(2)); |
| 121 | + } |
| 122 | + |
| 123 | + static void testStringsWithCommas() { |
| 124 | + @SuppressWarnings("unchecked") |
| 125 | + Map<String, Object> result = (Map<String, Object>) concoredocker.literalEval("{'key': 'hello, world'}"); |
| 126 | + check("string with comma", "hello, world", result.get("key")); |
| 127 | + } |
| 128 | + |
| 129 | + static void testStringsWithColons() { |
| 130 | + @SuppressWarnings("unchecked") |
| 131 | + Map<String, Object> result = (Map<String, Object>) concoredocker.literalEval("{'url': 'http://example.com'}"); |
| 132 | + check("string with colon", "http://example.com", result.get("url")); |
| 133 | + } |
| 134 | + |
| 135 | + static void testStringEscapeSequences() { |
| 136 | + Object result = concoredocker.literalEval("'hello\\nworld'"); |
| 137 | + check("escaped newline", "hello\nworld", result); |
| 138 | + } |
| 139 | + |
| 140 | + static void testScientificNotation() { |
| 141 | + Object result = concoredocker.literalEval("1.5e3"); |
| 142 | + check("scientific notation", 1500.0, result); |
| 143 | + } |
| 144 | + |
| 145 | + static void testNegativeNumbers() { |
| 146 | + @SuppressWarnings("unchecked") |
| 147 | + List<Object> result = (List<Object>) concoredocker.literalEval("[-1, -2.5, 3]"); |
| 148 | + check("negative int", -1, result.get(0)); |
| 149 | + check("negative double", -2.5, result.get(1)); |
| 150 | + check("positive int", 3, result.get(2)); |
| 151 | + } |
| 152 | + |
| 153 | + static void testTuple() { |
| 154 | + @SuppressWarnings("unchecked") |
| 155 | + List<Object> result = (List<Object>) concoredocker.literalEval("(1, 2, 3)"); |
| 156 | + check("tuple size", 3, result.size()); |
| 157 | + check("tuple[0]", 1, result.get(0)); |
| 158 | + } |
| 159 | + |
| 160 | + static void testTrailingComma() { |
| 161 | + @SuppressWarnings("unchecked") |
| 162 | + List<Object> result = (List<Object>) concoredocker.literalEval("[1, 2, 3,]"); |
| 163 | + check("trailing comma size", 3, result.size()); |
| 164 | + } |
| 165 | + |
| 166 | + // --- Serialization tests (toPythonLiteral via write format) --- |
| 167 | + |
| 168 | + static void testToPythonLiteralBooleans() { |
| 169 | + // Test that booleans serialize to Python format (True/False, not true/false) |
| 170 | + @SuppressWarnings("unchecked") |
| 171 | + List<Object> input = (List<Object>) concoredocker.literalEval("[True, False]"); |
| 172 | + // Re-parse and check the values are correct Java booleans |
| 173 | + check("parsed True is Boolean.TRUE", Boolean.TRUE, input.get(0)); |
| 174 | + check("parsed False is Boolean.FALSE", Boolean.FALSE, input.get(1)); |
| 175 | + } |
| 176 | + |
| 177 | + static void testToPythonLiteralNone() { |
| 178 | + @SuppressWarnings("unchecked") |
| 179 | + List<Object> input = (List<Object>) concoredocker.literalEval("[None, 1]"); |
| 180 | + check("parsed None is null", null, input.get(0)); |
| 181 | + check("parsed 1 is Integer 1", 1, input.get(1)); |
| 182 | + } |
| 183 | + |
| 184 | + static void testToPythonLiteralString() { |
| 185 | + Object result = concoredocker.literalEval("'hello'"); |
| 186 | + check("parsed string", "hello", result); |
| 187 | + } |
| 188 | + |
| 189 | + static void testFractionalSimtime() { |
| 190 | + // Simtime values like [0.5, 1.0, 2.0] should preserve fractional part |
| 191 | + @SuppressWarnings("unchecked") |
| 192 | + List<Object> result = (List<Object>) concoredocker.literalEval("[0.5, 1.0, 2.0]"); |
| 193 | + check("fractional simtime[0]", 0.5, result.get(0)); |
| 194 | + check("fractional simtime[1]", 1.0, result.get(1)); |
| 195 | + check("fractional simtime[2]", 2.0, result.get(2)); |
| 196 | + } |
| 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 | + } |
| 266 | +} |
0 commit comments