Skip to content

Commit 9c5362a

Browse files
authored
Merge pull request #252 from GaneshPatil7517/fix/java-literal-eval-only
fix(java): rewrite literalEval() with recursive descent parser
2 parents 2666d9e + 64e321a commit 9c5362a

2 files changed

Lines changed: 753 additions & 75 deletions

File tree

TestLiteralEval.java

Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
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+
* fractional simtime, and related round-trip parsing behavior.
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+
testNonStringDictKey();
40+
41+
System.out.println("\n=== Results: " + passed + " passed, " + failed + " failed out of " + (passed + failed) + " tests ===");
42+
if (failed > 0) {
43+
System.exit(1);
44+
}
45+
}
46+
47+
static void check(String testName, Object expected, Object actual) {
48+
if (Objects.equals(expected, actual)) {
49+
System.out.println("PASS: " + testName);
50+
passed++;
51+
} else {
52+
System.out.println("FAIL: " + testName + " | expected: " + expected + " | actual: " + actual);
53+
failed++;
54+
}
55+
}
56+
57+
static void testEmptyDict() {
58+
Object result = concoredocker.literalEval("{}");
59+
check("empty dict", new HashMap<>(), result);
60+
}
61+
62+
static void testSimpleDict() {
63+
@SuppressWarnings("unchecked")
64+
Map<String, Object> result = (Map<String, Object>) concoredocker.literalEval("{'PYM': 1}");
65+
check("simple dict key", true, result.containsKey("PYM"));
66+
check("simple dict value", 1, result.get("PYM"));
67+
}
68+
69+
static void testDictWithIntValues() {
70+
@SuppressWarnings("unchecked")
71+
Map<String, Object> result = (Map<String, Object>) concoredocker.literalEval("{'a': 10, 'b': 20}");
72+
check("dict int value a", 10, result.get("a"));
73+
check("dict int value b", 20, result.get("b"));
74+
}
75+
76+
static void testEmptyList() {
77+
Object result = concoredocker.literalEval("[]");
78+
check("empty list", new ArrayList<>(), result);
79+
}
80+
81+
static void testSimpleList() {
82+
@SuppressWarnings("unchecked")
83+
List<Object> result = (List<Object>) concoredocker.literalEval("[1, 2, 3]");
84+
check("simple list size", 3, result.size());
85+
check("simple list[0]", 1, result.get(0));
86+
check("simple list[2]", 3, result.get(2));
87+
}
88+
89+
static void testListOfDoubles() {
90+
@SuppressWarnings("unchecked")
91+
List<Object> result = (List<Object>) concoredocker.literalEval("[0.0, 1.5, 2.7]");
92+
check("list doubles[0]", 0.0, result.get(0));
93+
check("list doubles[1]", 1.5, result.get(1));
94+
}
95+
96+
static void testNestedDictWithList() {
97+
@SuppressWarnings("unchecked")
98+
Map<String, Object> result = (Map<String, Object>) concoredocker.literalEval("{'key': [1, 2, 3]}");
99+
check("nested dict has key", true, result.containsKey("key"));
100+
@SuppressWarnings("unchecked")
101+
List<Object> inner = (List<Object>) result.get("key");
102+
check("nested list size", 3, inner.size());
103+
check("nested list[0]", 1, inner.get(0));
104+
}
105+
106+
static void testNestedListsDeep() {
107+
@SuppressWarnings("unchecked")
108+
List<Object> result = (List<Object>) concoredocker.literalEval("[[1, 2], [3, 4]]");
109+
check("nested lists size", 2, result.size());
110+
@SuppressWarnings("unchecked")
111+
List<Object> inner = (List<Object>) result.get(0);
112+
check("inner list[0]", 1, inner.get(0));
113+
check("inner list[1]", 2, inner.get(1));
114+
}
115+
116+
static void testBooleansAndNone() {
117+
@SuppressWarnings("unchecked")
118+
List<Object> result = (List<Object>) concoredocker.literalEval("[True, False, None]");
119+
check("boolean True", Boolean.TRUE, result.get(0));
120+
check("boolean False", Boolean.FALSE, result.get(1));
121+
check("None", null, result.get(2));
122+
}
123+
124+
static void testStringsWithCommas() {
125+
@SuppressWarnings("unchecked")
126+
Map<String, Object> result = (Map<String, Object>) concoredocker.literalEval("{'key': 'hello, world'}");
127+
check("string with comma", "hello, world", result.get("key"));
128+
}
129+
130+
static void testStringsWithColons() {
131+
@SuppressWarnings("unchecked")
132+
Map<String, Object> result = (Map<String, Object>) concoredocker.literalEval("{'url': 'http://example.com'}");
133+
check("string with colon", "http://example.com", result.get("url"));
134+
}
135+
136+
static void testStringEscapeSequences() {
137+
Object result = concoredocker.literalEval("'hello\\nworld'");
138+
check("escaped newline", "hello\nworld", result);
139+
}
140+
141+
static void testScientificNotation() {
142+
Object result = concoredocker.literalEval("1.5e3");
143+
check("scientific notation", 1500.0, result);
144+
}
145+
146+
static void testNegativeNumbers() {
147+
@SuppressWarnings("unchecked")
148+
List<Object> result = (List<Object>) concoredocker.literalEval("[-1, -2.5, 3]");
149+
check("negative int", -1, result.get(0));
150+
check("negative double", -2.5, result.get(1));
151+
check("positive int", 3, result.get(2));
152+
}
153+
154+
static void testTuple() {
155+
@SuppressWarnings("unchecked")
156+
List<Object> result = (List<Object>) concoredocker.literalEval("(1, 2, 3)");
157+
check("tuple size", 3, result.size());
158+
check("tuple[0]", 1, result.get(0));
159+
}
160+
161+
static void testTrailingComma() {
162+
@SuppressWarnings("unchecked")
163+
List<Object> result = (List<Object>) concoredocker.literalEval("[1, 2, 3,]");
164+
check("trailing comma size", 3, result.size());
165+
}
166+
167+
// --- Serialization tests (toPythonLiteral via write format) ---
168+
169+
static void testToPythonLiteralBooleans() {
170+
// Test that booleans serialize to Python format (True/False, not true/false)
171+
@SuppressWarnings("unchecked")
172+
List<Object> input = (List<Object>) concoredocker.literalEval("[True, False]");
173+
// Re-parse and check the values are correct Java booleans
174+
check("parsed True is Boolean.TRUE", Boolean.TRUE, input.get(0));
175+
check("parsed False is Boolean.FALSE", Boolean.FALSE, input.get(1));
176+
}
177+
178+
static void testToPythonLiteralNone() {
179+
@SuppressWarnings("unchecked")
180+
List<Object> input = (List<Object>) concoredocker.literalEval("[None, 1]");
181+
check("parsed None is null", null, input.get(0));
182+
check("parsed 1 is Integer 1", 1, input.get(1));
183+
}
184+
185+
static void testToPythonLiteralString() {
186+
Object result = concoredocker.literalEval("'hello'");
187+
check("parsed string", "hello", result);
188+
}
189+
190+
static void testFractionalSimtime() {
191+
// Simtime values like [0.5, 1.0, 2.0] should preserve fractional part
192+
@SuppressWarnings("unchecked")
193+
List<Object> result = (List<Object>) concoredocker.literalEval("[0.5, 1.0, 2.0]");
194+
check("fractional simtime[0]", 0.5, result.get(0));
195+
check("fractional simtime[1]", 1.0, result.get(1));
196+
check("fractional simtime[2]", 2.0, result.get(2));
197+
}
198+
199+
// --- Round-trip serialization tests ---
200+
201+
static void testRoundTripSerialization() {
202+
// Conceptually: a list with mixed types [1, 2.5, true, false, null, "hello"]
203+
// Use reflection-free approach: build the Python literal manually
204+
// and verify round-trip through literalEval
205+
String serialized = "[1, 2.5, True, False, None, 'hello']";
206+
@SuppressWarnings("unchecked")
207+
List<Object> roundTripped = (List<Object>) concoredocker.literalEval(serialized);
208+
check("round-trip int", 1, roundTripped.get(0));
209+
check("round-trip double", 2.5, roundTripped.get(1));
210+
check("round-trip True", Boolean.TRUE, roundTripped.get(2));
211+
check("round-trip False", Boolean.FALSE, roundTripped.get(3));
212+
check("round-trip None", null, roundTripped.get(4));
213+
check("round-trip string", "hello", roundTripped.get(5));
214+
}
215+
216+
static void testStringEscapingSerialization() {
217+
// Strings with special chars should survive parse -> serialize -> re-parse
218+
String input = "'hello\\nworld'";
219+
Object parsed = concoredocker.literalEval(input);
220+
check("escape parse", "hello\nworld", parsed);
221+
222+
// Test string with embedded single quote
223+
String input2 = "'it\\'s'";
224+
Object parsed2 = concoredocker.literalEval(input2);
225+
check("escape single quote", "it's", parsed2);
226+
}
227+
228+
// --- Unterminated input tests (should throw) ---
229+
230+
static void testUnterminatedList() {
231+
try {
232+
concoredocker.literalEval("[1, 2");
233+
System.out.println("FAIL: unterminated list should throw");
234+
failed++;
235+
} catch (IllegalArgumentException e) {
236+
check("unterminated list throws", true, e.getMessage().contains("Unterminated list"));
237+
}
238+
}
239+
240+
static void testUnterminatedDict() {
241+
try {
242+
concoredocker.literalEval("{'a': 1");
243+
System.out.println("FAIL: unterminated dict should throw");
244+
failed++;
245+
} catch (IllegalArgumentException e) {
246+
check("unterminated dict throws", true, e.getMessage().contains("Unterminated dict"));
247+
}
248+
}
249+
250+
static void testUnterminatedTuple() {
251+
try {
252+
concoredocker.literalEval("(1, 2");
253+
System.out.println("FAIL: unterminated tuple should throw");
254+
failed++;
255+
} catch (IllegalArgumentException e) {
256+
check("unterminated tuple throws", true, e.getMessage().contains("Unterminated tuple"));
257+
}
258+
}
259+
260+
static void testNonStringDictKey() {
261+
// Dict keys must be strings - numeric keys should throw
262+
try {
263+
concoredocker.literalEval("{123: 'value'}");
264+
System.out.println("FAIL: numeric dict key should throw");
265+
failed++;
266+
} catch (IllegalArgumentException e) {
267+
check("numeric dict key throws", true, e.getMessage().contains("Dict keys must be non-null strings"));
268+
}
269+
}
270+
}

0 commit comments

Comments
 (0)