Skip to content

Commit aba3841

Browse files
committed
Add unit tests for $forEach, $exists, $resultValue, and rich fixtures
1 parent 5c02980 commit aba3841

56 files changed

Lines changed: 4649 additions & 0 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 317 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,317 @@
1+
package blue.bex;
2+
3+
import blue.bex.api.BexEngine;
4+
import blue.bex.api.BexExecutionContext;
5+
import blue.bex.api.BexProgramSource;
6+
import blue.bex.api.BexStepResults;
7+
import blue.bex.api.FrozenBexDocumentView;
8+
import blue.bex.compile.BexCompiledProgram;
9+
import blue.bex.result.BexExecutionResult;
10+
import blue.bex.value.BexFrozenWriter;
11+
import blue.bex.value.BexNodeWriter;
12+
import blue.bex.value.BexValue;
13+
import blue.bex.value.BexValues;
14+
import blue.language.Blue;
15+
import blue.language.model.Node;
16+
import blue.language.snapshot.FrozenNode;
17+
import org.junit.jupiter.api.DynamicTest;
18+
import org.junit.jupiter.api.TestFactory;
19+
import org.yaml.snakeyaml.Yaml;
20+
21+
import java.io.IOException;
22+
import java.io.InputStream;
23+
import java.math.BigDecimal;
24+
import java.math.BigInteger;
25+
import java.net.URISyntaxException;
26+
import java.net.URL;
27+
import java.nio.file.Files;
28+
import java.nio.file.Path;
29+
import java.nio.file.Paths;
30+
import java.util.ArrayList;
31+
import java.util.Collection;
32+
import java.util.Collections;
33+
import java.util.LinkedHashMap;
34+
import java.util.List;
35+
import java.util.Map;
36+
import java.util.stream.Stream;
37+
38+
import static org.junit.jupiter.api.Assertions.assertEquals;
39+
import static org.junit.jupiter.api.Assertions.assertNotNull;
40+
import static org.junit.jupiter.api.Assertions.assertThrows;
41+
import static org.junit.jupiter.api.Assertions.assertTrue;
42+
import static org.junit.jupiter.api.Assertions.fail;
43+
44+
class BexRichFixtureTest {
45+
private static final String FIXTURE_ROOT = "rich-fixtures";
46+
private static final Blue YAML_BLUE = new Blue();
47+
private static final String TINY_EVENT_PROGRAM = String.join("\n",
48+
"type: Blue/BEX Program",
49+
"do:",
50+
" - $appendEvent:",
51+
" eventKind: Tiny",
52+
" payload: x");
53+
54+
private final Blue blue = new Blue(blueId -> {
55+
if ("HotelOrderType".equals(blueId)) {
56+
return Collections.singletonList(YAML_BLUE.yamlToNode(String.join("\n",
57+
"status:",
58+
" type: Text")));
59+
}
60+
if ("RestaurantOrderType".equals(blueId)) {
61+
return Collections.singletonList(YAML_BLUE.yamlToNode(String.join("\n",
62+
"restaurantStatus:",
63+
" type: Text")));
64+
}
65+
return Collections.emptyList();
66+
});
67+
private final BexEngine engine = BexEngine.builder().blue(blue).build();
68+
private final Yaml yaml = new Yaml();
69+
70+
@TestFactory
71+
Collection<DynamicTest> richFixtures() throws Exception {
72+
List<Path> paths = fixturePaths();
73+
List<DynamicTest> tests = new ArrayList<>();
74+
for (Path path : paths) {
75+
tests.add(DynamicTest.dynamicTest(displayName(path), () -> runFixture(path)));
76+
}
77+
return tests;
78+
}
79+
80+
private void runFixture(Path path) throws Exception {
81+
Map<String, Object> fixture = readFixture(path);
82+
Map<String, Object> expectation = map(fixture.get("expectation"));
83+
String outcome = string(expectation.get("outcome"));
84+
assertNotNull(outcome, "Fixture outcome is required: " + path);
85+
86+
if ("parse-error".equals(outcome)) {
87+
RuntimeException ex = assertThrows(RuntimeException.class, () -> parseProgram(fixture));
88+
assertErrorContains(ex, expectation);
89+
return;
90+
}
91+
if ("parse-error-or-output-conversion-error".equals(outcome)) {
92+
assertParseOrOutputConversionError(fixture, expectation);
93+
return;
94+
}
95+
96+
Node program = parseProgram(fixture);
97+
BexProgramSource source = BexProgramSource.inline(FrozenNode.fromResolvedNode(program));
98+
BexExecutionContext context = context(fixture);
99+
100+
if ("compile-error".equals(outcome)) {
101+
BexException ex = assertThrows(BexException.class, () -> engine.compile(source));
102+
assertErrorContains(ex, expectation);
103+
return;
104+
}
105+
106+
BexCompiledProgram compiled = engine.compile(source);
107+
if ("runtime-error".equals(outcome)) {
108+
BexException ex = assertThrows(BexException.class, () -> engine.execute(compiled, context));
109+
assertErrorContains(ex, expectation);
110+
return;
111+
}
112+
if ("output-conversion-error".equals(outcome)) {
113+
BexExecutionResult result = engine.execute(compiled, context);
114+
assertOutputConversionError(result.value(), expectation);
115+
return;
116+
}
117+
if ("gas-property".equals(outcome)) {
118+
assertGasProperty(compiled, context, expectation);
119+
return;
120+
}
121+
if (!"success".equals(outcome)) {
122+
fail("Unsupported fixture outcome: " + outcome);
123+
}
124+
125+
BexExecutionResult result = engine.execute(compiled, context);
126+
assertSuccessExpectations(result, expectation);
127+
}
128+
129+
private void assertParseOrOutputConversionError(Map<String, Object> fixture, Map<String, Object> expectation) {
130+
Node program;
131+
try {
132+
program = parseProgram(fixture);
133+
} catch (RuntimeException ex) {
134+
assertErrorContains(ex, expectation);
135+
return;
136+
}
137+
BexProgramSource source = BexProgramSource.inline(FrozenNode.fromResolvedNode(program));
138+
try {
139+
BexExecutionResult result = engine.compileAndExecute(source, context(fixture));
140+
assertOutputConversionError(result.value(), expectation);
141+
} catch (BexException ex) {
142+
assertErrorContains(ex, expectation);
143+
}
144+
}
145+
146+
private void assertOutputConversionError(BexValue value, Map<String, Object> expectation) {
147+
BexException thrown = null;
148+
try {
149+
BexNodeWriter.toNode(value);
150+
} catch (BexException ex) {
151+
thrown = ex;
152+
}
153+
if (thrown == null) {
154+
thrown = assertThrows(BexException.class, () -> BexFrozenWriter.toFrozen(value));
155+
}
156+
assertErrorContains(thrown, expectation);
157+
}
158+
159+
private void assertGasProperty(BexCompiledProgram compiled, BexExecutionContext context, Map<String, Object> expectation) {
160+
String property = string(expectation.get("property"));
161+
if (!"gasUsedGreaterThanEquivalentTinyEvent".equals(property)) {
162+
fail("Unsupported gas property: " + property);
163+
}
164+
long large = engine.execute(compiled, context).gasUsed();
165+
long tiny = engine.compileAndExecute(source(TINY_EVENT_PROGRAM), context).gasUsed();
166+
assertTrue(large > tiny, "Expected large output gas " + large + " to be greater than tiny output gas " + tiny);
167+
}
168+
169+
private void assertSuccessExpectations(BexExecutionResult result, Map<String, Object> expectation) {
170+
if (expectation.containsKey("resultSimple")) {
171+
assertEquals(normalize(expectation.get("resultSimple")), normalize(result.value().toSimple()));
172+
}
173+
if (expectation.containsKey("changeset")) {
174+
assertEquals(normalize(expectation.get("changeset")), normalize(result.changeset().asValue().toSimple()));
175+
}
176+
if (expectation.containsKey("events")) {
177+
assertEquals(normalize(expectation.get("events")), normalize(result.events().asValue().toSimple()));
178+
}
179+
}
180+
181+
private BexExecutionContext context(Map<String, Object> fixture) {
182+
Map<String, Object> context = map(fixture.get("context"));
183+
String scope = string(context.get("documentScope"));
184+
if (scope == null) {
185+
scope = "/";
186+
}
187+
Node root = parseNodeSource(string(context.get("rootDocumentSource")));
188+
Node event = parseNodeSource(string(context.get("eventSource")));
189+
Node currentContract = parseNodeSource(string(context.get("currentContractSource")));
190+
191+
return BexExecutionContext.builder()
192+
.document(new FrozenBexDocumentView(FrozenNode.fromResolvedNode(root), FrozenNode.fromResolvedNode(root), scope))
193+
.event(BexValues.nodeSnapshot(event))
194+
.currentContract(BexValues.nodeSnapshot(currentContract))
195+
.steps(steps(context.get("stepsBinding")))
196+
.gasLimit(1_000_000)
197+
.build();
198+
}
199+
200+
private BexStepResults steps(Object stepsObject) {
201+
Map<String, Object> stepsMap = map(stepsObject);
202+
BexStepResults.Builder builder = BexStepResults.builder();
203+
for (Map.Entry<String, Object> entry : stepsMap.entrySet()) {
204+
builder.put(entry.getKey(), BexValues.fromSimple(normalize(entry.getValue())));
205+
}
206+
return builder.build();
207+
}
208+
209+
private Node parseProgram(Map<String, Object> fixture) {
210+
return blue.yamlToNode(requiredString(fixture.get("programSource"), "programSource"));
211+
}
212+
213+
private Node parseNodeSource(String source) {
214+
if (source == null || source.trim().isEmpty()) {
215+
return blue.yamlToNode("{}");
216+
}
217+
return blue.yamlToNode(source);
218+
}
219+
220+
private BexProgramSource source(String source) {
221+
return BexProgramSource.inline(FrozenNode.fromResolvedNode(blue.yamlToNode(source)));
222+
}
223+
224+
@SuppressWarnings("unchecked")
225+
private Map<String, Object> readFixture(Path path) throws IOException {
226+
try (InputStream in = Files.newInputStream(path)) {
227+
Object loaded = yaml.load(in);
228+
if (!(loaded instanceof Map)) {
229+
throw new IllegalArgumentException("Fixture must be a map: " + path);
230+
}
231+
return (Map<String, Object>) loaded;
232+
}
233+
}
234+
235+
private List<Path> fixturePaths() throws URISyntaxException, IOException {
236+
URL url = Thread.currentThread().getContextClassLoader().getResource(FIXTURE_ROOT);
237+
assertNotNull(url, "Missing fixture resource root: " + FIXTURE_ROOT);
238+
final Path root = Paths.get(url.toURI());
239+
List<Path> paths = new ArrayList<>();
240+
try (Stream<Path> stream = Files.walk(root)) {
241+
stream.filter(path -> Files.isRegularFile(path) && path.getFileName().toString().endsWith(".yaml"))
242+
.forEach(paths::add);
243+
}
244+
Collections.sort(paths);
245+
return paths;
246+
}
247+
248+
private String displayName(Path path) {
249+
Path fileName = path.getFileName();
250+
return fileName != null ? fileName.toString() : path.toString();
251+
}
252+
253+
private void assertErrorContains(Throwable ex, Map<String, Object> expectation) {
254+
String expected = string(expectation.get("errorContains"));
255+
if (expected == null || expected.isEmpty()) {
256+
return;
257+
}
258+
String message = ex.getMessage();
259+
assertTrue(message != null && message.contains(expected),
260+
"Expected error to contain <" + expected + "> but was <" + message + ">");
261+
}
262+
263+
@SuppressWarnings("unchecked")
264+
private Map<String, Object> map(Object value) {
265+
if (value == null) {
266+
return Collections.emptyMap();
267+
}
268+
if (!(value instanceof Map)) {
269+
throw new IllegalArgumentException("Expected map but found: " + value);
270+
}
271+
Map<String, Object> out = new LinkedHashMap<>();
272+
for (Map.Entry<Object, Object> entry : ((Map<Object, Object>) value).entrySet()) {
273+
out.put(String.valueOf(entry.getKey()), entry.getValue());
274+
}
275+
return out;
276+
}
277+
278+
private String requiredString(Object value, String label) {
279+
String text = string(value);
280+
if (text == null) {
281+
throw new IllegalArgumentException("Missing fixture field: " + label);
282+
}
283+
return text;
284+
}
285+
286+
private String string(Object value) {
287+
return value != null ? String.valueOf(value) : null;
288+
}
289+
290+
@SuppressWarnings("unchecked")
291+
private Object normalize(Object value) {
292+
if (value instanceof Map) {
293+
Map<String, Object> out = new LinkedHashMap<>();
294+
for (Map.Entry<Object, Object> entry : ((Map<Object, Object>) value).entrySet()) {
295+
out.put(String.valueOf(entry.getKey()), normalize(entry.getValue()));
296+
}
297+
return out;
298+
}
299+
if (value instanceof List) {
300+
List<Object> out = new ArrayList<>();
301+
for (Object item : (List<Object>) value) {
302+
out.add(normalize(item));
303+
}
304+
return out;
305+
}
306+
if (value instanceof Integer || value instanceof Long || value instanceof Short || value instanceof Byte) {
307+
return BigInteger.valueOf(((Number) value).longValue());
308+
}
309+
if (value instanceof BigDecimal || value instanceof BigInteger || value instanceof String || value instanceof Boolean || value == null) {
310+
return value;
311+
}
312+
if (value instanceof Float || value instanceof Double) {
313+
return BigDecimal.valueOf(((Number) value).doubleValue());
314+
}
315+
return value;
316+
}
317+
}

0 commit comments

Comments
 (0)