Skip to content

Commit 6d69440

Browse files
authored
Introduce YAML formatter for better testing/debugging (#4274)
* Implement YamlFormatter Signed-off-by: Tomoyuki Morita <[email protected]> * Enable YAML based plan comparison in tests Signed-off-by: Tomoyuki Morita <[email protected]> * Fix line break issue in Windows Signed-off-by: Tomoyuki Morita <[email protected]> * Minor fix in test case Signed-off-by: Tomoyuki Morita <[email protected]> * Fix line break issue Signed-off-by: Tomoyuki Morita <[email protected]> * Fix comment Signed-off-by: Tomoyuki Morita <[email protected]> --------- Signed-off-by: Tomoyuki Morita <[email protected]>
1 parent 0e2dc7c commit 6d69440

File tree

11 files changed

+297
-14
lines changed

11 files changed

+297
-14
lines changed

core/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ dependencies {
5454
api "com.fasterxml.jackson.core:jackson-core:${versions.jackson}"
5555
api "com.fasterxml.jackson.core:jackson-databind:${versions.jackson_databind}"
5656
api "com.fasterxml.jackson.core:jackson-annotations:${versions.jackson}"
57+
api "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:${versions.jackson}"
5758
api group: 'com.google.code.gson', name: 'gson', version: '2.8.9'
5859
api group: 'com.tdunning', name: 't-digest', version: '3.3'
5960
api "net.minidev:json-smart:${versions.json_smart}"
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/*
2+
* Copyright OpenSearch Contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package org.opensearch.sql.utils;
7+
8+
import com.fasterxml.jackson.core.JsonProcessingException;
9+
import com.fasterxml.jackson.databind.ObjectMapper;
10+
import com.fasterxml.jackson.databind.SerializationFeature;
11+
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
12+
import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator;
13+
14+
/**
15+
* YAML formatter utility class. Attributes are sorted alphabetically for consistent output. Check
16+
* {@link YamlFormatterTest} for the actual formatting behavior.
17+
*/
18+
public class YamlFormatter {
19+
20+
private static final ObjectMapper YAML_MAPPER;
21+
22+
static {
23+
YAMLFactory yamlFactory = new YAMLFactory();
24+
yamlFactory.disable(YAMLGenerator.Feature.WRITE_DOC_START_MARKER);
25+
yamlFactory.enable(YAMLGenerator.Feature.MINIMIZE_QUOTES); // Enable smart quoting
26+
yamlFactory.enable(
27+
YAMLGenerator.Feature.ALWAYS_QUOTE_NUMBERS_AS_STRINGS); // Quote numeric strings
28+
yamlFactory.enable(YAMLGenerator.Feature.INDENT_ARRAYS_WITH_INDICATOR);
29+
YAML_MAPPER = new ObjectMapper(yamlFactory);
30+
YAML_MAPPER.enable(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS);
31+
}
32+
33+
/**
34+
* Formats any object into YAML format.
35+
*
36+
* @param object the object to format
37+
* @return YAML-formatted string representation
38+
*/
39+
public static String formatToYaml(Object object) {
40+
try {
41+
return YAML_MAPPER.writeValueAsString(object);
42+
} catch (JsonProcessingException e) {
43+
throw new RuntimeException("Failed to format object to YAML", e);
44+
}
45+
}
46+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* Copyright OpenSearch Contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package org.opensearch.sql.utils;
7+
8+
import com.fasterxml.jackson.core.JsonProcessingException;
9+
import com.fasterxml.jackson.databind.ObjectMapper;
10+
import com.fasterxml.jackson.databind.SerializationFeature;
11+
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
12+
import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator;
13+
14+
/**
15+
* YAML formatter utility class. Attributes are sorted alphabetically for consistent output. Check
16+
* {@link YamlFormatterTest} for the actual formatting behavior.
17+
*/
18+
public class YamlFormatter {
19+
20+
private static final ObjectMapper YAML_MAPPER = initObjectMapper();
21+
22+
private static ObjectMapper initObjectMapper() {
23+
YAMLFactory yamlFactory = new YAMLFactory();
24+
yamlFactory.disable(YAMLGenerator.Feature.WRITE_DOC_START_MARKER);
25+
yamlFactory.enable(YAMLGenerator.Feature.MINIMIZE_QUOTES); // Enable smart quoting
26+
yamlFactory.enable(
27+
YAMLGenerator.Feature.ALWAYS_QUOTE_NUMBERS_AS_STRINGS); // Quote numeric strings
28+
yamlFactory.enable(YAMLGenerator.Feature.INDENT_ARRAYS_WITH_INDICATOR);
29+
30+
ObjectMapper mapper = new ObjectMapper(yamlFactory);
31+
mapper.enable(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS);
32+
return mapper;
33+
}
34+
35+
/** Formats any object into YAML. It will always use LF as line break regardless of OS. */
36+
public static String formatToYaml(Object object) {
37+
try {
38+
return YAML_MAPPER.writer().withDefaultPrettyPrinter().writeValueAsString(object);
39+
} catch (JsonProcessingException e) {
40+
throw new RuntimeException("Failed to format object to YAML", e);
41+
}
42+
}
43+
}
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
/*
2+
* Copyright OpenSearch Contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package org.opensearch.sql.utils;
7+
8+
import static org.junit.jupiter.api.Assertions.assertEquals;
9+
import static org.junit.jupiter.api.Assertions.assertNotNull;
10+
import static org.junit.jupiter.api.Assertions.assertTrue;
11+
12+
import net.minidev.json.JSONObject;
13+
import org.junit.jupiter.api.Test;
14+
15+
class YamlFormatterTest {
16+
@Test
17+
void testAttributes() {
18+
JSONObject json1 = new JSONObject();
19+
json1.put("attr1", null);
20+
json1.put("attr2", "null");
21+
json1.put("attr3", 123);
22+
json1.put("attr4", "123");
23+
24+
String actualYaml = YamlFormatter.formatToYaml(json1);
25+
26+
String expectedYaml = "attr1: null\nattr2: \"null\"\nattr3: 123\nattr4: \"123\"\n";
27+
assertEquals(expectedYaml, actualYaml);
28+
}
29+
30+
@Test
31+
void testJSONObjectConsistentOutput() {
32+
JSONObject json1 = new JSONObject();
33+
json1.put("query", "SELECT * FROM users");
34+
json1.put("database", "test");
35+
json1.put("filters", new String[] {"active = true", "role = 'user'"});
36+
37+
JSONObject metadata1 = new JSONObject();
38+
metadata1.put("version", "1.0");
39+
metadata1.put("author", "system");
40+
json1.put("metadata", metadata1);
41+
42+
// Create second JSONObject with same data but different insertion order
43+
JSONObject json2 = new JSONObject();
44+
JSONObject metadata2 = new JSONObject();
45+
metadata2.put("author", "system");
46+
metadata2.put("version", "1.0");
47+
json2.put("metadata", metadata2);
48+
49+
json2.put("filters", new String[] {"active = true", "role = 'user'"});
50+
json2.put("database", "test");
51+
json2.put("query", "SELECT * FROM users");
52+
53+
String yaml1 = YamlFormatter.formatToYaml(json1);
54+
String yaml2 = YamlFormatter.formatToYaml(json2);
55+
56+
String expectedYaml =
57+
"database: test\n"
58+
+ "filters:\n"
59+
+ " - active = true\n"
60+
+ " - role = 'user'\n"
61+
+ "metadata:\n"
62+
+ " author: system\n"
63+
+ " version: \"1.0\"\n"
64+
+ "query: SELECT * FROM users\n";
65+
66+
assertEquals(expectedYaml, yaml1, "YAML output should match expected sorted format");
67+
assertEquals(yaml1, yaml2, "YAML output should be identical for same JSONObject data");
68+
assertTrue(yaml1.indexOf("database:") < yaml1.indexOf("filters:"));
69+
assertTrue(yaml1.indexOf("filters:") < yaml1.indexOf("metadata:"));
70+
assertTrue(yaml1.indexOf("metadata:") < yaml1.indexOf("query:"));
71+
}
72+
73+
@Test
74+
void testMultiLineStrings() {
75+
JSONObject json = new JSONObject();
76+
json.put(
77+
"query",
78+
"SELECT name, age, department\n"
79+
+ " FROM users u\n"
80+
+ " JOIN departments d ON u.dept_id = d.id\n"
81+
+ "WHERE u.active = true\n"
82+
+ "ORDER BY u.created_date DESC\n");
83+
json.put("singleLine", "Simple single line text");
84+
json.put("number", 42);
85+
86+
// Create nested metadata object with multi-line description
87+
JSONObject metadata = new JSONObject();
88+
metadata.put(
89+
"description", "Multi-line description\nof the query purpose\nand expected results");
90+
metadata.put("author", "system");
91+
metadata.put("version", "2.0");
92+
json.put("metadata", metadata);
93+
94+
String yaml = YamlFormatter.formatToYaml(json);
95+
96+
// Expected complete YAML output with nested multi-line string
97+
String expectedYaml =
98+
"metadata:\n"
99+
+ " author: system\n"
100+
+ " description: |-\n"
101+
+ " Multi-line description\n"
102+
+ " of the query purpose\n"
103+
+ " and expected results\n"
104+
+ " version: \"2.0\"\n"
105+
+ "number: 42\n"
106+
+ "query: |\n"
107+
+ " SELECT name, age, department\n"
108+
+ " FROM users u\n"
109+
+ " JOIN departments d ON u.dept_id = d.id\n"
110+
+ " WHERE u.active = true\n"
111+
+ " ORDER BY u.created_date DESC\n"
112+
+ "singleLine: Simple single line text\n";
113+
114+
assertEquals(
115+
expectedYaml,
116+
yaml,
117+
"YAML output should match expected format with nested multi-line strings");
118+
}
119+
120+
@Test
121+
void testFormatArbitraryObject() {
122+
TestObject testObj = new TestObject("test", 42);
123+
124+
String yaml = YamlFormatter.formatToYaml(testObj);
125+
126+
assertNotNull(yaml);
127+
assertTrue(yaml.contains("name:"));
128+
assertTrue(yaml.contains("value: 42"));
129+
}
130+
131+
private static class TestObject {
132+
public String name;
133+
public int value;
134+
135+
public TestObject(String name, int value) {
136+
this.name = name;
137+
this.value = value;
138+
}
139+
}
140+
}

integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalciteExplainIT.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import static org.opensearch.sql.legacy.TestsConstants.TEST_INDEX_NESTED_SIMPLE;
1111
import static org.opensearch.sql.legacy.TestsConstants.TEST_INDEX_STRINGS;
1212
import static org.opensearch.sql.util.MatcherUtils.assertJsonEqualsIgnoreId;
13+
import static org.opensearch.sql.util.MatcherUtils.assertYamlEqualsJsonIgnoreId;
1314

1415
import java.io.IOException;
1516
import java.util.Locale;
@@ -39,8 +40,8 @@ public void supportSearchSargPushDown_singleRange() throws IOException {
3940
String query =
4041
"source=opensearch-sql_test_index_account | where age >= 1.0 and age < 10 | fields age";
4142
var result = explainQueryToString(query);
42-
String expected = loadExpectedPlan("explain_sarg_filter_push_single_range.json");
43-
assertJsonEqualsIgnoreId(expected, result);
43+
String expected = loadExpectedPlan("explain_sarg_filter_push_single_range.yaml");
44+
assertYamlEqualsJsonIgnoreId(expected, result);
4445
}
4546

4647
// Only for Calcite

integ-test/src/test/java/org/opensearch/sql/ppl/PPLIntegTestCase.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import org.opensearch.sql.common.setting.Settings.Key;
3232
import org.opensearch.sql.legacy.SQLIntegTestCase;
3333
import org.opensearch.sql.util.RetryProcessor;
34+
import org.opensearch.sql.utils.YamlFormatter;
3435

3536
/** OpenSearch Rest integration test base for PPL testing. */
3637
public abstract class PPLIntegTestCase extends SQLIntegTestCase {
@@ -59,6 +60,12 @@ protected String explainQueryToString(String query) throws IOException {
5960
return explainQueryToString(query, false);
6061
}
6162

63+
protected String explainQueryToYaml(String query) throws IOException {
64+
String jsonResponse = explainQueryToString(query);
65+
JSONObject jsonObject = jsonify(jsonResponse);
66+
return YamlFormatter.formatToYaml(jsonObject);
67+
}
68+
6269
protected String explainQueryToString(String query, boolean extended) throws IOException {
6370
Response response =
6471
client()

integ-test/src/test/java/org/opensearch/sql/util/MatcherUtils.java

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import static org.hamcrest.Matchers.hasItems;
1919
import static org.junit.Assert.assertEquals;
2020

21+
import com.fasterxml.jackson.databind.ObjectMapper;
2122
import com.google.common.base.Strings;
2223
import com.google.gson.JsonParser;
2324
import java.math.BigDecimal;
@@ -37,10 +38,12 @@
3738
import org.json.JSONObject;
3839
import org.opensearch.search.SearchHit;
3940
import org.opensearch.search.SearchHits;
41+
import org.opensearch.sql.utils.YamlFormatter;
4042

4143
public class MatcherUtils {
4244

4345
private static final Logger LOG = LogManager.getLogger();
46+
private static final ObjectMapper JSON_MAPPER = new ObjectMapper();
4447

4548
/**
4649
* Assert field value in object by a custom matcher and getter to access the field.
@@ -422,4 +425,40 @@ private static String eliminateRelId(String s) {
422425
private static String eliminatePid(String s) {
423426
return s.replaceAll("pitId=[^,]+,", "pitId=*,");
424427
}
428+
429+
public static void assertYamlEqualsJsonIgnoreId(String expectedYaml, String actualJson) {
430+
String cleanedYaml = cleanUpYaml(jsonToYaml(actualJson));
431+
assertYamlEquals(expectedYaml, cleanedYaml);
432+
}
433+
434+
public static void assertYamlEquals(String expected, String actual) {
435+
String normalizedExpected = normalizeLineBreaks(expected).trim();
436+
String normalizedActual = normalizeLineBreaks(actual).trim();
437+
assertEquals(
438+
formatMessage(normalizedExpected, normalizedActual), normalizedExpected, normalizedActual);
439+
}
440+
441+
private static String normalizeLineBreaks(String s) {
442+
return s.replace("\r\n", "\n").replace("\r", "\n");
443+
}
444+
445+
private static String cleanUpYaml(String s) {
446+
return s.replaceAll("\"utcTimestamp\":\\d+", "\"utcTimestamp\": 0")
447+
.replaceAll("rel#\\d+", "rel#")
448+
.replaceAll("RelSubset#\\d+", "RelSubset#")
449+
.replaceAll("pitId=[^,]+,", "pitId=*,");
450+
}
451+
452+
private static String jsonToYaml(String json) {
453+
try {
454+
Object jsonObject = JSON_MAPPER.readValue(json, Object.class);
455+
return YamlFormatter.formatToYaml(jsonObject);
456+
} catch (Exception e) {
457+
throw new RuntimeException("Failed to convert JSON to YAML", e);
458+
}
459+
}
460+
461+
private static String formatMessage(String expected, String actual) {
462+
return String.format("### Expected ###\n%s\n### Actual###\n%s\n", expected, actual);
463+
}
425464
}

integ-test/src/test/resources/expectedOutput/calcite/explain_sarg_filter_push_single_range.json

Lines changed: 0 additions & 6 deletions
This file was deleted.
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
calcite:
2+
logical: |
3+
LogicalSystemLimit(fetch=[10000], type=[QUERY_SIZE_LIMIT])
4+
LogicalProject(age=[$8])
5+
LogicalFilter(condition=[SEARCH($8, Sarg[[1.0:DECIMAL(11, 1)..10:DECIMAL(11, 1))]:DECIMAL(11, 1))])
6+
CalciteLogicalIndexScan(table=[[OpenSearch, opensearch-sql_test_index_account]])
7+
physical: |
8+
CalciteEnumerableIndexScan(table=[[OpenSearch, opensearch-sql_test_index_account]], PushDownContext=[[PROJECT->[age], FILTER->SEARCH($0, Sarg[[1.0:DECIMAL(11, 1)..10:DECIMAL(11, 1))]:DECIMAL(11, 1)), LIMIT->10000], OpenSearchRequestBuilder(sourceBuilder={"from":0,"size":10000,"timeout":"1m","query":{"range":{"age":{"from":1.0,"to":10.0,"include_lower":true,"include_upper":false,"boost":1.0}}},"_source":{"includes":["age"],"excludes":[]},"sort":[{"_doc":{"order":"asc"}}]}, requestedTotalSize=10000, pageSize=null, startFrom=0)])

integ-test/src/test/resources/expectedOutput/calcite_no_pushdown/explain_sarg_filter_push_single_range.json

Lines changed: 0 additions & 6 deletions
This file was deleted.

0 commit comments

Comments
 (0)