|
| 1 | +# DSL Golden File Tests |
| 2 | + |
| 3 | +## Overview |
| 4 | + |
| 5 | +Golden-file-based test framework for the `dsl-query-executor` plugin. The framework validates two conversion paths: |
| 6 | + |
| 7 | +1. **RelNode Generation** (forward path): DSL (`SearchSourceBuilder`) → Calcite `RelNode` logical plan via `SearchSourceConverter.convert()` |
| 8 | +2. **SearchResponse Generation** (reverse path): Mock `ExecutionResult` rows → `SearchResponse` via `SearchResponseBuilder.build()` |
| 9 | + |
| 10 | +Each golden file is a self-contained JSON document encoding the input DSL, expected RelNode plan, mock result rows, and expected output DSL. Tests auto-discover all `.json` files in `src/test/resources/golden/`, so adding a new test case requires only adding a new JSON file — no Java code changes needed. |
| 11 | + |
| 12 | +The framework runs as pure unit tests with zero cluster dependency. It constructs Calcite infrastructure (RelOptCluster, type factory, catalog reader) directly from the golden file's `indexMapping`, mirroring the pattern in `TestUtils`. |
| 13 | + |
| 14 | +## Architecture |
| 15 | + |
| 16 | +```mermaid |
| 17 | +graph TD |
| 18 | + subgraph Golden File on Disk |
| 19 | + GF["golden/*.json"] |
| 20 | + end |
| 21 | +
|
| 22 | + subgraph Test Classes |
| 23 | + SSC_TEST["SearchSourceConverterTests<br/>testGoldenFileRelNodeGeneration()"] |
| 24 | + SRB_TEST["SearchResponseBuilderTests<br/>testGoldenFileSearchResponseGeneration()"] |
| 25 | + end |
| 26 | +
|
| 27 | + subgraph Shared Utilities |
| 28 | + LOAD["GoldenFileLoader<br/>parse JSON → GoldenTestCase"] |
| 29 | + INFRA["CalciteTestInfra<br/>indexMapping → Calcite schema"] |
| 30 | + end |
| 31 | +
|
| 32 | + subgraph Production Code Under Test |
| 33 | + SSC["SearchSourceConverter.convert()"] |
| 34 | + SRB["SearchResponseBuilder.build()"] |
| 35 | + end |
| 36 | +
|
| 37 | + GF -->|auto-discovered| SSC_TEST |
| 38 | + GF -->|auto-discovered| SRB_TEST |
| 39 | + SSC_TEST --> LOAD |
| 40 | + SRB_TEST --> LOAD |
| 41 | + SSC_TEST --> INFRA |
| 42 | + SRB_TEST --> INFRA |
| 43 | + SSC_TEST -->|invokes| SSC |
| 44 | + SRB_TEST -->|invokes| SSC |
| 45 | + SRB_TEST -->|invokes| SRB |
| 46 | +``` |
| 47 | + |
| 48 | +The architecture has three layers: |
| 49 | + |
| 50 | +1. **Data Layer** — `GoldenTestCase` POJO and `GoldenFileLoader` handle JSON parsing. `CalciteTestInfra` builds Calcite schemas from golden file mappings. |
| 51 | +2. **Forward Path** — `SearchSourceConverterTests.testGoldenFileRelNodeGeneration()` auto-discovers all golden files, converts each `inputDsl` via `SearchSourceConverter`, and asserts the `RelNode.explain()` output matches `expectedRelNodePlan`. Also validates that RelNode field names match `mockResultFieldNames`. |
| 52 | +3. **Reverse Path** — `SearchResponseBuilderTests.testGoldenFileSearchResponseGeneration()` auto-discovers all golden files, builds an `ExecutionResult` from mock rows, invokes `SearchResponseBuilder.build()`, and asserts the serialized response matches `expectedOutputDsl` (ignoring non-deterministic fields like `took`, `_shards`, `_score`). |
| 53 | + |
| 54 | +### Key Design Decisions |
| 55 | + |
| 56 | +- **Auto-discovery over per-file test methods**: Tests loop over all `golden/*.json` files. Failures are collected with file names for traceability, then reported together. |
| 57 | +- **Integration into existing test classes**: Forward path tests live in `SearchSourceConverterTests`, reverse path in `SearchResponseBuilderTests` — no separate test class needed, reducing duplication. |
| 58 | +- **JSON golden files**: JSON is natively supported by OpenSearch's `XContentParser` and `SearchSourceBuilder.fromXContent()`, avoiding extra dependencies. |
| 59 | +- **Deterministic RelNode serialization**: Uses `RelNode.explain()` to produce a stable, human-readable plan string. |
| 60 | +- **Schema from golden file, not from cluster**: Each golden file carries an `indexMapping` field used to construct a Calcite `RelDataType` directly, eliminating any need for a live cluster. |
| 61 | +- **Plan as array of strings**: `expectedRelNodePlan` is a JSON array (one string per line) rather than a `\n`-delimited string, improving readability in golden files. |
| 62 | + |
| 63 | +## Components |
| 64 | + |
| 65 | +### GoldenTestCase |
| 66 | + |
| 67 | +POJO representing a single parsed golden file: |
| 68 | + |
| 69 | +```java |
| 70 | +public class GoldenTestCase { |
| 71 | + private String testName; |
| 72 | + private String indexName; |
| 73 | + private Map<String, String> indexMapping; // field name → SQL type name |
| 74 | + private Map<String, Object> inputDsl; // raw JSON map for SearchSourceBuilder |
| 75 | + private List<String> expectedRelNodePlan; // expected RelNode.explain() lines |
| 76 | + private List<String> mockResultFieldNames; // column names for mock result rows |
| 77 | + private List<List<Object>> mockResultRows; // simulated result rows |
| 78 | + private Map<String, Object> expectedOutputDsl; // expected SearchResponse JSON |
| 79 | + private String planType; // "HITS" or "AGGREGATION" |
| 80 | +} |
| 81 | +``` |
| 82 | + |
| 83 | +### GoldenFileLoader |
| 84 | + |
| 85 | +Parses and validates golden files: |
| 86 | + |
| 87 | +```java |
| 88 | +public class GoldenFileLoader { |
| 89 | + /** Parses a single golden file by name from src/test/resources/golden/ */ |
| 90 | + public static GoldenTestCase load(String goldenFileName); |
| 91 | + |
| 92 | + /** Parses a single golden file from a file-system path */ |
| 93 | + public static GoldenTestCase load(Path goldenFilePath); |
| 94 | +} |
| 95 | +``` |
| 96 | + |
| 97 | +### CalciteTestInfra |
| 98 | + |
| 99 | +Builds Calcite planning infrastructure from a golden file's index mapping: |
| 100 | + |
| 101 | +```java |
| 102 | +public class CalciteTestInfra { |
| 103 | + /** Builds a RelOptCluster, schema, and catalog reader from indexMapping */ |
| 104 | + public static InfraResult buildFromMapping(String indexName, Map<String, String> indexMapping); |
| 105 | + |
| 106 | + public record InfraResult(RelOptCluster cluster, RelOptTable table, SchemaPlus schema) {} |
| 107 | +} |
| 108 | +``` |
| 109 | + |
| 110 | +### Interaction Flow |
| 111 | + |
| 112 | +```mermaid |
| 113 | +sequenceDiagram |
| 114 | + participant Test as SearchSourceConverterTests / SearchResponseBuilderTests |
| 115 | + participant Loader as GoldenFileLoader |
| 116 | + participant Infra as CalciteTestInfra |
| 117 | + participant SSC as SearchSourceConverter |
| 118 | + participant SRB as SearchResponseBuilder |
| 119 | +
|
| 120 | + Note over Test: Auto-discover golden/*.json files |
| 121 | + Test->>Loader: load(fileName) |
| 122 | + Loader-->>Test: GoldenTestCase |
| 123 | +
|
| 124 | + Test->>Infra: buildFromMapping(indexName, indexMapping) |
| 125 | + Infra-->>Test: InfraResult(cluster, table, schema) |
| 126 | +
|
| 127 | + Note over Test: RelNode Generation (forward path) |
| 128 | + Test->>SSC: convert(searchSource, indexName) |
| 129 | + SSC-->>Test: QueryPlans |
| 130 | + Test->>Test: relNode.explain() → assert matches expectedRelNodePlan |
| 131 | + Test->>Test: assert relNode fieldNames == mockResultFieldNames |
| 132 | +
|
| 133 | + Note over Test: SearchResponse Generation (reverse path) |
| 134 | + Test->>Test: construct ExecutionResult from mockResultRows |
| 135 | + Test->>SRB: build(results, tookInMillis) |
| 136 | + SRB-->>Test: SearchResponse |
| 137 | + Test->>Test: serialize → assert matches expectedOutputDsl |
| 138 | +``` |
| 139 | + |
| 140 | +## Golden File JSON Schema |
| 141 | + |
| 142 | +```json |
| 143 | +{ |
| 144 | + "testName": "term_query_hits", |
| 145 | + "indexName": "test-index", |
| 146 | + "indexMapping": { |
| 147 | + "name": "VARCHAR", |
| 148 | + "price": "INTEGER", |
| 149 | + "brand": "VARCHAR", |
| 150 | + "rating": "DOUBLE" |
| 151 | + }, |
| 152 | + "planType": "HITS", |
| 153 | + "inputDsl": { |
| 154 | + "query": { |
| 155 | + "term": { "name": { "value": "laptop" } } |
| 156 | + }, |
| 157 | + "size": 10 |
| 158 | + }, |
| 159 | + "expectedRelNodePlan": [ |
| 160 | + "LogicalSort(fetch=[10])", |
| 161 | + " LogicalProject(name=[$0], price=[$1], brand=[$2], rating=[$3])", |
| 162 | + " LogicalFilter(condition=[=($0, 'laptop')])", |
| 163 | + " LogicalTableScan(table=[[test-index]])" |
| 164 | + ], |
| 165 | + "mockResultFieldNames": ["name", "price", "brand", "rating"], |
| 166 | + "mockResultRows": [ |
| 167 | + ["laptop", 999, "BrandA", 4.5], |
| 168 | + ["laptop", 1299, "BrandB", 4.8] |
| 169 | + ], |
| 170 | + "expectedOutputDsl": { |
| 171 | + "hits": { |
| 172 | + "total": { "value": 2, "relation": "eq" }, |
| 173 | + "hits": [ |
| 174 | + { "_source": { "name": "laptop", "price": 999, "brand": "BrandA", "rating": 4.5 } }, |
| 175 | + { "_source": { "name": "laptop", "price": 1299, "brand": "BrandB", "rating": 4.8 } } |
| 176 | + ] |
| 177 | + } |
| 178 | + } |
| 179 | +} |
| 180 | +``` |
| 181 | + |
| 182 | +### SQL Type Mapping |
| 183 | + |
| 184 | +The `indexMapping` field uses Calcite `SqlTypeName` strings: |
| 185 | + |
| 186 | +| Golden File Type | SqlTypeName | Java Type | |
| 187 | +|---|---|---| |
| 188 | +| `VARCHAR` | `SqlTypeName.VARCHAR` | `String` | |
| 189 | +| `INTEGER` | `SqlTypeName.INTEGER` | `Integer` | |
| 190 | +| `BIGINT` | `SqlTypeName.BIGINT` | `Long` | |
| 191 | +| `DOUBLE` | `SqlTypeName.DOUBLE` | `Double` | |
| 192 | +| `FLOAT` | `SqlTypeName.FLOAT` | `Float` | |
| 193 | +| `BOOLEAN` | `SqlTypeName.BOOLEAN` | `Boolean` | |
| 194 | +| `DATE` | `SqlTypeName.DATE` | `Date` | |
| 195 | +| `TIMESTAMP` | `SqlTypeName.TIMESTAMP` | `Timestamp` | |
| 196 | + |
| 197 | +All fields are created as nullable (matching `OpenSearchSchemaBuilder` behavior). |
| 198 | + |
| 199 | +## File Organization |
| 200 | + |
| 201 | +``` |
| 202 | +sandbox/plugins/dsl-query-executor/ |
| 203 | +├── src/test/ |
| 204 | +│ ├── README.md |
| 205 | +│ ├── java/org/opensearch/dsl/ |
| 206 | +│ │ ├── converter/ |
| 207 | +│ │ │ └── SearchSourceConverterTests.java ← forward path golden file tests |
| 208 | +│ │ ├── result/ |
| 209 | +│ │ │ └── SearchResponseBuilderTests.java ← reverse path golden file tests |
| 210 | +│ │ └── golden/ |
| 211 | +│ │ ├── GoldenTestCase.java ← POJO |
| 212 | +│ │ ├── GoldenFileLoader.java ← parser + validator |
| 213 | +│ │ └── CalciteTestInfra.java ← Calcite schema builder |
| 214 | +│ └── resources/golden/ |
| 215 | +│ ├── match_all_hits.json |
| 216 | +│ └── terms_with_avg_aggregation.json |
| 217 | +``` |
| 218 | + |
| 219 | +## Error Handling |
| 220 | + |
| 221 | +### Golden File Loading Errors |
| 222 | + |
| 223 | +| Error Condition | Behavior | |
| 224 | +|---|---| |
| 225 | +| Golden file contains invalid JSON | `GoldenFileLoader` throws `IllegalArgumentException` with file path and parse error details | |
| 226 | +| Required field missing from golden file | `GoldenFileLoader.validate()` throws `IllegalArgumentException` naming the missing field and file path | |
| 227 | +| `indexMapping` contains unsupported SQL type | `CalciteTestInfra.buildFromMapping()` throws `IllegalArgumentException` naming the unsupported type | |
| 228 | +| `planType` is invalid | `GoldenFileLoader.validate()` throws `IllegalArgumentException` with the invalid value | |
| 229 | + |
| 230 | +### Test Failure Reporting |
| 231 | + |
| 232 | +| Error Condition | Behavior | |
| 233 | +|---|---| |
| 234 | +| RelNode plan mismatch | Failure collected with file name, expected and actual plan strings | |
| 235 | +| Field names mismatch | Failure collected with file name, expected and actual field lists | |
| 236 | +| SearchResponse output mismatch | Failure collected with file name, expected and actual JSON | |
| 237 | +| Any exception during a golden file | Failure collected with file name, exception class and message | |
| 238 | +| One or more failures collected | Single `fail()` at end with all failures listed | |
| 239 | + |
| 240 | +## Build Integration |
| 241 | + |
| 242 | +Tests run as part of the standard test source set: |
| 243 | +- `gradle test` runs all golden file tests alongside existing unit tests |
| 244 | +- No cluster required — all tests are pure unit tests |
| 245 | +- Non-deterministic fields (`took`, `timed_out`, `_shards`, `_score`) are stripped before comparison |
| 246 | +- Aggregation bucket order is normalized (sorted by key) before comparison |
0 commit comments