Skip to content

Commit 38ea5a1

Browse files
committed
feat(integ-test): add analytics-engine IT compatibility testing scripts
Scripts to run SQL V2 + Legacy IT suite against analytics-engine (DataFusion) path on a local sandbox cluster with parquet-backed indices, and generate a bucketed compatibility report — without modifying production code. Includes: - start-cluster.sh: start sandbox cluster with all 9 required plugins - run-sql-compat-test.sh: run filtered ITs + generate report - publish-sql-plugin.sh: build and install SQL plugin to maven local - generate-report.py: parse JUnit XML into bucketed markdown report - README.md: full SOP with prerequisites, quick start, and PR testing
1 parent b10d541 commit 38ea5a1

6 files changed

Lines changed: 473 additions & 0 deletions

File tree

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# Analytics Engine IT — Agent Prompt
2+
3+
## What This Is
4+
Scripts to run SQL V2 + Legacy IT suite against analytics-engine (DataFusion)
5+
and generate a compatibility report — without modifying SQL plugin production code.
6+
7+
## Before Starting — Ask the User For:
8+
9+
1. **OS_REPO** — path to their OpenSearch core checkout (must be on `main` branch)
10+
2. **JAVA25** — path to JDK 25 (default: `/usr/lib/jvm/java-25-amazon-corretto`)
11+
3. **Whether the native lib is built** — check if `$OS_REPO/sandbox/libs/dataformat-native/rust/target/release/libopensearch_native.so` exists. If not, build it:
12+
```bash
13+
cd "$OS_REPO/sandbox/libs/dataformat-native/rust" && cargo build --release
14+
```
15+
(~20 min first time)
16+
4. **Any PR to cherry-pick** — if testing a specific PR's impact, get the PR number
17+
18+
## Execution Order
19+
20+
```bash
21+
export OS_REPO=<user-provided path>
22+
export JAVA25=<user-provided path>
23+
24+
# 1. Publish SQL plugin to maven local
25+
./analytics-engine-test/publish-sql-plugin.sh
26+
27+
# 2. Start cluster (runs in foreground — use a background process or separate terminal)
28+
./analytics-engine-test/start-cluster.sh
29+
30+
# 3. Wait for cluster: curl -s localhost:9200/_cluster/health
31+
32+
# 4. Run tests + generate report
33+
./analytics-engine-test/run-sql-compat-test.sh
34+
35+
# Report at: integ-test/build/reports/analytics-engine-compatibility/REPORT.md
36+
```
37+
38+
## If Testing a PR
39+
40+
```bash
41+
git fetch upstream pull/<N>/head:pr-<N>
42+
MERGE_BASE=$(git merge-base pr-<N> upstream/main)
43+
# Cherry-pick only the PR's own commits (on top of main)
44+
git log --oneline $MERGE_BASE..pr-<N> # review
45+
git cherry-pick $MERGE_BASE..pr-<N>
46+
47+
# Rebuild + republish + restart cluster + rerun tests (steps 1-4 above)
48+
49+
# Revert after:
50+
git reset --hard HEAD~<number-of-cherry-picked-commits>
51+
```
52+
53+
## Troubleshooting
54+
55+
| Symptom | Fix |
56+
|---------|-----|
57+
| `release version 25 not supported` | Set `JAVA_HOME=$JAVA25` for the cluster build |
58+
| `multiple committer factories found: []` | Missing `analytics-backend-lucene` plugin |
59+
| `StreamTransportService` null / Guice errors | `run.gradle` missing `transport.stream.enabled=true` (script auto-patches) |
60+
| `NoSuchElementException` in `NativeAllocatorConfig` | Rebuild native lib: `cargo build --release` |
61+
| Index creation 500 errors | Cluster needs all 9 plugins — check `curl localhost:9200/_cat/plugins` |
62+
63+
## Full Details
64+
See `README.md` in this directory.

analytics-engine-test/README.md

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
# Analytics Engine IT Compatibility Testing
2+
3+
Run the full SQL V2 + Legacy IT suite against the analytics-engine (DataFusion) path on a local sandbox cluster and generate a bucketed compatibility report — **without modifying SQL plugin production code**.
4+
5+
## Prerequisites
6+
7+
- **JDK 21** — for building the SQL plugin
8+
- **JDK 25** — for running the OpenSearch sandbox cluster (set `JAVA25` env var)
9+
- **Rust toolchain** — for building the native parquet library (one-time)
10+
- **OpenSearch core checkout** on `main` (set `OS_REPO` env var)
11+
- **This SQL plugin repo** checked out
12+
13+
## How It Works
14+
15+
1. Every test index is created with parquet-backed settings (`tests.analytics.parquet_indices=true`)
16+
2. The cluster setting `plugins.calcite.analytics.force_routing=true` routes ALL queries through analytics-engine
17+
3. The analytics-engine (DataFusion) executes queries instead of the standard Calcite/DSL path
18+
4. Tests that pass = compatible with analytics-engine; tests that fail = gaps to fill
19+
20+
## Quick Start
21+
22+
```bash
23+
# Set environment
24+
export OS_REPO=~/path/to/OpenSearch # OpenSearch core checkout (main branch)
25+
export JAVA25=/usr/lib/jvm/java-25-amazon-corretto # JDK 25 path
26+
27+
# 1. Build native library (one-time, ~20 min)
28+
cd "$OS_REPO/sandbox/libs/dataformat-native/rust"
29+
cargo build --release
30+
31+
# 2. Publish SQL plugin to maven local
32+
./analytics-engine-test/publish-sql-plugin.sh
33+
34+
# 3. Start cluster (in a separate terminal)
35+
./analytics-engine-test/start-cluster.sh
36+
37+
# 4. Run tests + generate report (in another terminal)
38+
./analytics-engine-test/run-sql-compat-test.sh
39+
```
40+
41+
## Scripts
42+
43+
| Script | Purpose |
44+
|--------|---------|
45+
| `publish-sql-plugin.sh` | Build SQL plugin zip → install to `~/.m2` |
46+
| `start-cluster.sh` | Start sandbox cluster with all 9 plugins (foreground) |
47+
| `run-sql-compat-test.sh` | Run SQL V2 + Legacy ITs → generate report |
48+
| `generate-report.py` | Parse JUnit XML → bucketed markdown report |
49+
50+
## Cluster Plugins (9 total)
51+
52+
| Plugin | Source | Purpose |
53+
|--------|--------|---------|
54+
| `opensearch-job-scheduler` | Maven snapshots | Required by SQL plugin |
55+
| `arrow-base` | Core `:plugins:` | Arrow memory/classloader |
56+
| `arrow-flight-rpc` | Core `:plugins:` | Arrow Flight transport |
57+
| `composite-engine` | Sandbox | Composite index engine |
58+
| `parquet-data-format` | Sandbox | Parquet data format |
59+
| `analytics-engine` | Sandbox | Query routing hub |
60+
| `analytics-backend-datafusion` | Sandbox | DataFusion execution |
61+
| `analytics-backend-lucene` | Sandbox | Lucene committer factory |
62+
| `opensearch-sql-plugin` | Maven local (`~/.m2`) | SQL/PPL plugin under test |
63+
64+
## Key Configuration
65+
66+
| Setting | Where | Purpose |
67+
|---------|-------|---------|
68+
| `tests.analytics.parquet_indices=true` | Gradle sys prop | Inject parquet settings into all test indices |
69+
| `plugins.calcite.analytics.force_routing=true` | Cluster setting | Route ALL queries through analytics-engine |
70+
| `opensearch.experimental.feature.transport.stream.enabled=true` | JVM flag (run.gradle patch) | Enable StreamTransportService for analytics-engine |
71+
| `opensearch.experimental.feature.pluggable.dataformat.enabled=true` | JVM flag (auto-set) | Enable parquet data format |
72+
73+
## Testing a PR
74+
75+
To test a PR's impact on compatibility:
76+
77+
```bash
78+
# Cherry-pick PR commits
79+
git fetch upstream pull/<PR_NUMBER>/head:pr-<PR_NUMBER>
80+
git cherry-pick <commits>
81+
82+
# Rebuild + republish
83+
./analytics-engine-test/publish-sql-plugin.sh
84+
85+
# Restart cluster (Ctrl+C the old one first)
86+
./analytics-engine-test/start-cluster.sh
87+
88+
# Rerun tests
89+
./analytics-engine-test/run-sql-compat-test.sh
90+
```
91+
92+
## Report Location
93+
94+
After running: `integ-test/build/reports/analytics-engine-compatibility/REPORT.md`
95+
96+
## Cleanup
97+
98+
```bash
99+
# Revert run.gradle patch in OpenSearch repo
100+
cd "$OS_REPO" && git checkout gradle/run.gradle
101+
102+
# Remove cherry-picked commits (if any)
103+
git reset --hard HEAD~N
104+
105+
# Delete PR branch
106+
git branch -D pr-<NUMBER>
107+
```
108+
109+
## Known Failure Patterns
110+
111+
| Pattern | Meaning | Count (baseline) |
112+
|---------|---------|-----------------|
113+
| "Other Error" (500) | Analytics-engine doesn't support the query pattern | ~421 |
114+
| "Result Mismatch" | Query runs but returns different results | ~5 |
115+
| "Bad Request" (400) | SQL syntax/feature not supported in unified path | ~3 |
116+
| Skipped (legacy) | Tests skipped due to legacy engine deprecation | ~470 |
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
#!/usr/bin/env python3
2+
"""Parse JUnit XML results and generate a bucketed analytics-engine compatibility report."""
3+
import xml.etree.ElementTree as ET
4+
import os, sys, glob
5+
from collections import defaultdict
6+
7+
def categorize(msg):
8+
if "DataFormatAwareEngine" in msg:
9+
return "Direct Shard Op"
10+
elif "400 Bad Request" in msg:
11+
return "Bad Request"
12+
elif "AssertionError" in msg or "expected" in msg.lower():
13+
return "Result Mismatch"
14+
elif "timeout" in msg.lower() or "timed out" in msg.lower():
15+
return "Timeout"
16+
elif "NullPointerException" in msg:
17+
return "NPE"
18+
elif "index_not_found" in msg:
19+
return "Index Setup"
20+
else:
21+
return "Other Error"
22+
23+
def main():
24+
if len(sys.argv) < 3:
25+
print(f"Usage: {sys.argv[0]} <results-dir> <output.md>")
26+
sys.exit(1)
27+
28+
results_dir, output_path = sys.argv[1], sys.argv[2]
29+
classes = defaultdict(lambda: {"passed": 0, "failed": 0, "skipped": 0, "failures": []})
30+
31+
for xml_file in sorted(glob.glob(os.path.join(results_dir, "*.xml"))):
32+
try:
33+
tree = ET.parse(xml_file)
34+
root = tree.getroot()
35+
classname = root.get("name", "")
36+
if not (classname.startswith("org.opensearch.sql.sql.") or
37+
classname.startswith("org.opensearch.sql.legacy.")):
38+
continue
39+
for tc in root.findall(".//testcase"):
40+
tc_class = tc.get("classname", classname)
41+
if not (tc_class.startswith("org.opensearch.sql.sql.") or
42+
tc_class.startswith("org.opensearch.sql.legacy.")):
43+
continue
44+
failure = tc.find("failure")
45+
error = tc.find("error")
46+
skipped_el = tc.find("skipped")
47+
short_class = tc_class.replace("org.opensearch.sql.", "")
48+
if skipped_el is not None:
49+
classes[short_class]["skipped"] += 1
50+
elif failure is not None or error is not None:
51+
classes[short_class]["failed"] += 1
52+
msg = (failure if failure is not None else error).get("message", "")[:200]
53+
classes[short_class]["failures"].append((tc.get("name"), msg))
54+
else:
55+
classes[short_class]["passed"] += 1
56+
except Exception:
57+
pass
58+
59+
total_p = sum(c["passed"] for c in classes.values())
60+
total_f = sum(c["failed"] for c in classes.values())
61+
total_s = sum(c["skipped"] for c in classes.values())
62+
total = total_p + total_f + total_s
63+
64+
if total == 0:
65+
print("ERROR: No test results found for sql.sql.* or sql.legacy.*")
66+
sys.exit(1)
67+
68+
fail_cats = defaultdict(int)
69+
for c in classes.values():
70+
for _, msg in c["failures"]:
71+
fail_cats[categorize(msg)] += 1
72+
73+
sql_v2 = {k: v for k, v in classes.items() if k.startswith("sql.")}
74+
legacy = {k: v for k, v in classes.items() if k.startswith("legacy.")}
75+
76+
def area_totals(d):
77+
p = sum(c["passed"] for c in d.values())
78+
f = sum(c["failed"] for c in d.values())
79+
s = sum(c["skipped"] for c in d.values())
80+
return p, f, s
81+
82+
sql_p, sql_f, sql_s = area_totals(sql_v2)
83+
leg_p, leg_f, leg_s = area_totals(legacy)
84+
85+
lines = []
86+
w = lines.append
87+
88+
w("# SQL V2 + Legacy IT — Analytics Engine Compatibility Report\n")
89+
w(f"**Total Tests:** {total} | ✅ Passed: {total_p} ({total_p/total*100:.1f}%) | "
90+
f"❌ Failed: {total_f} ({total_f/total*100:.1f}%) | ⏭️ Skipped: {total_s} ({total_s/total*100:.1f}%)\n")
91+
w("")
92+
w("| Area | Passed | Failed | Skipped | Pass Rate |")
93+
w("|------|-------:|-------:|--------:|----------:|")
94+
if sql_p + sql_f > 0:
95+
w(f"| SQL V2 (`sql.sql.*`) | {sql_p} | {sql_f} | {sql_s} | {sql_p/(sql_p+sql_f)*100:.1f}% |")
96+
if leg_p + leg_f > 0:
97+
w(f"| Legacy (`sql.legacy.*`) | {leg_p} | {leg_f} | {leg_s} | {leg_p/(leg_p+leg_f)*100:.1f}% |")
98+
w("")
99+
100+
w("## Failure Categories\n")
101+
w("| Category | Count | % |")
102+
w("|----------|------:|--:|")
103+
for cat, cnt in sorted(fail_cats.items(), key=lambda x: -x[1]):
104+
w(f"| {cat} | {cnt} | {cnt/total_f*100:.1f}% |")
105+
w("")
106+
107+
w("## ✅ Fully Passing Test Classes\n")
108+
fully_passing = sorted([(k, v) for k, v in classes.items() if v["failed"] == 0 and v["passed"] > 0],
109+
key=lambda x: -x[1]["passed"])
110+
for name, v in fully_passing:
111+
w(f"- **{name}** ({v['passed']} tests)")
112+
w("")
113+
114+
w("## 🟡 Partially Passing (>50%)\n")
115+
w("| Class | Passed | Failed | Rate |")
116+
w("|-------|-------:|-------:|-----:|")
117+
partial = sorted([(k, v) for k, v in classes.items()
118+
if v["failed"] > 0 and v["passed"] > 0 and v["passed"]/(v["passed"]+v["failed"]) > 0.5],
119+
key=lambda x: -x[1]["passed"]/(x[1]["passed"]+x[1]["failed"]))
120+
for name, v in partial:
121+
rate = v["passed"]/(v["passed"]+v["failed"])*100
122+
w(f"| {name} | {v['passed']} | {v['failed']} | {rate:.0f}% |")
123+
w("")
124+
125+
w("## ❌ Failing Test Classes\n")
126+
w("| Class | Passed | Failed | Skipped | Top Failure |")
127+
w("|-------|-------:|-------:|--------:|-------------|")
128+
failing = sorted([(k, v) for k, v in classes.items()
129+
if v["failed"] > 0 and (v["passed"] == 0 or v["passed"]/(v["passed"]+v["failed"]) <= 0.5)],
130+
key=lambda x: -x[1]["failed"])
131+
for name, v in failing:
132+
top_fail = categorize(v["failures"][0][1]) if v["failures"] else "?"
133+
w(f"| {name} | {v['passed']} | {v['failed']} | {v['skipped']} | {top_fail} |")
134+
w("")
135+
136+
w("## 🔍 Result Mismatch Details\n")
137+
for name, v in sorted(classes.items()):
138+
mismatches = [(t, m) for t, m in v["failures"] if categorize(m) == "Result Mismatch"]
139+
if mismatches:
140+
w(f"### {name}")
141+
for test, msg in mismatches[:5]:
142+
w(f"- `{test}`: {msg[:150]}")
143+
w("")
144+
145+
os.makedirs(os.path.dirname(output_path), exist_ok=True)
146+
with open(output_path, "w") as f:
147+
f.write("\n".join(lines))
148+
print(f"Report written to {output_path}")
149+
print(f" Total: {total} | Pass: {total_p} ({total_p/total*100:.1f}%) | Fail: {total_f} | Skip: {total_s}")
150+
151+
if __name__ == "__main__":
152+
main()
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
#!/bin/bash
2+
# Builds the SQL plugin zip and installs it to ~/.m2 for OpenSearch run.gradle resolution.
3+
set -euo pipefail
4+
5+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
6+
SQL_REPO="${SQL_REPO:-$(cd "$SCRIPT_DIR/.." && pwd)}"
7+
VERSION="${SQL_VERSION:-3.7.0.0-SNAPSHOT}"
8+
ARTIFACT="opensearch-sql-plugin"
9+
GROUP_PATH="org/opensearch/plugin"
10+
M2_DIR="$HOME/.m2/repository/$GROUP_PATH/$ARTIFACT/$VERSION"
11+
12+
echo "=== Building SQL plugin ==="
13+
cd "$SQL_REPO"
14+
./gradlew :plugin:bundlePlugin
15+
16+
# The zip is named opensearch-sql-<version>.zip (not opensearch-sql-plugin-)
17+
ZIP="plugin/build/distributions/opensearch-sql-$VERSION.zip"
18+
if [[ ! -f "$ZIP" ]]; then
19+
echo "ERROR: Expected zip not found at $ZIP" >&2
20+
echo "Available:" >&2
21+
ls plugin/build/distributions/ >&2
22+
exit 1
23+
fi
24+
25+
echo "=== Installing to Maven local ==="
26+
mkdir -p "$M2_DIR"
27+
cp "$ZIP" "$M2_DIR/$ARTIFACT-$VERSION.zip"
28+
29+
cat > "$M2_DIR/$ARTIFACT-$VERSION.pom" << EOF
30+
<?xml version="1.0" encoding="UTF-8"?>
31+
<project>
32+
<modelVersion>4.0.0</modelVersion>
33+
<groupId>org.opensearch.plugin</groupId>
34+
<artifactId>$ARTIFACT</artifactId>
35+
<version>$VERSION</version>
36+
<packaging>zip</packaging>
37+
</project>
38+
EOF
39+
40+
rm -f "$M2_DIR/$ARTIFACT-$VERSION.module"
41+
rm -f "$M2_DIR/_remote.repositories"
42+
43+
echo "✅ Published $ARTIFACT-$VERSION.zip to $M2_DIR"

0 commit comments

Comments
 (0)