This is an automated email from the ASF dual-hosted git repository.
terrymanu pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/shardingsphere.git
The following commit(s) were added to refs/heads/master by this push:
new abc21607383 Add Excel reports for external SQL parser integration
tests (#40105)
abc21607383 is described below
commit abc216073835ac4af42bb713675d1403494bb9e5
Author: Liang Zhang <[email protected]>
AuthorDate: Thu Oct 8 21:58:33 2026 +0800
Add Excel reports for external SQL parser integration tests (#40105)
* Fix CI defaults for fork pull requests
* Add Excel reports for external SQL parser integration tests
- Reuse the commercial report generator with English workbook labels
- Add an opt-in Failsafe profile preserving complete SQL and stack traces
- Generate and upload reports after failed external tests
- Clear stale target XML reports before execution
---
.github/scripts/generate-sql-parser-report.py | 145 ++++++++++++++++++++++++++
.github/workflows/nightly-sql-parser.yml | 27 ++++-
test/it/parser/pom.xml | 23 ++++
3 files changed, 192 insertions(+), 3 deletions(-)
diff --git a/.github/scripts/generate-sql-parser-report.py
b/.github/scripts/generate-sql-parser-report.py
new file mode 100644
index 00000000000..0f0175f37b0
--- /dev/null
+++ b/.github/scripts/generate-sql-parser-report.py
@@ -0,0 +1,145 @@
+#!/usr/bin/env python3
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import argparse
+from collections import Counter
+from pathlib import Path
+import re
+import xml.etree.ElementTree as ET
+
+from openpyxl import Workbook
+from openpyxl.cell import Cell
+from openpyxl.styles import Alignment, Font, PatternFill
+
+
+def split_cell_text(value):
+ if len(value.encode("utf-16-le")) // 2 <= 32767 and value.count("\n") <=
253:
+ return [value]
+ chunks = []
+ start = units = line_feeds = 0
+ for offset, character in enumerate(value):
+ size = 2 if ord(character) > 0xFFFF else 1
+ if units + size > 32767 or line_feeds + (character == "\n") > 253:
+ chunks.append(value[start:offset])
+ start, units, line_feeds = offset, 0, 0
+ units += size
+ line_feeds += character == "\n"
+ chunks.append(value[start:])
+ return chunks
+
+
+def append_row(sheet, values):
+ cells = []
+ for value in values:
+ cell = Cell(sheet, value=value)
+ if isinstance(value, str):
+ cell.data_type = "s"
+ cells.append(cell)
+ sheet.append(cells)
+
+
+def append_error(details, long_text, values):
+ for index, value in enumerate(values):
+ if not isinstance(value, str):
+ continue
+ chunks = split_cell_text(value)
+ if len(chunks) == 1:
+ continue
+ field = details.cell(1, index + 1).value
+ for part, chunk in enumerate(chunks, 1):
+ append_row(long_text, [values[0], field, part, chunk])
+ values[index] = f"See Long Text: error {values[0]}, field '{field}'
({len(chunks)} parts)."
+ append_row(details, values)
+
+
+def format_sheet(sheet, widths):
+ sheet.freeze_panes = "A2"
+ sheet.auto_filter.ref = sheet.dimensions
+ for cell in sheet[1]:
+ cell.font = Font(bold=True, color="FFFFFF")
+ cell.fill = PatternFill("solid", fgColor="17365D")
+ for column, width in widths.items():
+ sheet.column_dimensions[column].width = width
+ for row in sheet.iter_rows(min_row=2):
+ for cell in row:
+ cell.alignment = Alignment(vertical="top", wrap_text=True)
+ sheet.row_dimensions[row[0].row].height = 60
+
+
+def generate_report(reports_dir, test_class, database, output):
+ reports = sorted(reports_dir.glob(f"TEST-*{test_class}.xml"))
+ if not reports:
+ raise FileNotFoundError(f"No Failsafe XML reports for {test_class} in
{reports_dir}")
+ workbook = Workbook()
+ summary = workbook.active
+ summary.title = "Summary"
+ details = workbook.create_sheet("Errors")
+ long_text = workbook.create_sheet("Long Text")
+ append_row(details, ["Error ID", "Database", "Case ID", "SQL", "Result",
"Exception Type", "Error Message", "Stack Trace", "Test Name", "Report File"])
+ append_row(long_text, ["Error ID", "Field", "Part", "Text"])
+ totals = Counter()
+ exceptions = Counter()
+ for report in reports:
+ for _, testcase in ET.iterparse(report, events=("end",)):
+ if testcase.tag != "testcase":
+ continue
+ totals["tests"] += 1
+ totals["skipped"] += testcase.find("skipped") is not None
+ totals["errors"] += testcase.find("error") is not None
+ totals["failures"] += testcase.find("failure") is not None
+ name = testcase.attrib["name"]
+ parameters = re.fullmatch(r"assertParseSQL\([^)]*\) (.*?)
\(([^()]*)\) -> (.*)", name, re.DOTALL)
+ for failure in list(testcase.findall("error")) +
list(testcase.findall("failure")):
+ totals["error_entries"] += 1
+ exceptions[failure.get("type", "")] += 1
+ append_error(details, long_text, [
+ totals["error_entries"], parameters[2] if parameters else
database, parameters[1] if parameters else "", parameters[3] if parameters else
"",
+ failure.tag, failure.get("type", ""),
failure.get("message", ""), failure.text or "", name, report.name,
+ ])
+ testcase.clear()
+ append_row(summary, ["Metric", "Count or Value"])
+ for label, value in [
+ ("Database", database), ("Test Class", test_class), ("Total Tests",
totals["tests"]),
+ ("Errors", totals["errors"]), ("Failures", totals["failures"]),
("Skipped", totals["skipped"]),
+ ("Passed", totals["tests"] - totals["errors"] - totals["failures"] -
totals["skipped"]),
+ ("Notes", "Only error/failure entries are listed. Initialization
failures may have no SQL. Text exceeding Excel cell limits is stored in Long
Text by error ID and field."),
+ ]:
+ append_row(summary, [label, value])
+ append_row(summary, ["Exception Type", "Error Entries"])
+ for exception, count in exceptions.most_common():
+ append_row(summary, [exception, count])
+ format_sheet(summary, {"A": 65, "B": 100})
+ format_sheet(details, {"A": 12, "B": 16, "C": 45, "D": 100, "E": 12, "F":
65, "G": 100, "H": 100, "I": 65, "J": 65})
+ format_sheet(long_text, {"A": 12, "B": 20, "C": 12, "D": 120})
+ output.parent.mkdir(parents=True, exist_ok=True)
+ workbook.save(output)
+ print(f"Excel report: {output}; tests={totals['tests']};
errors={totals['errors']}; failures={totals['failures']}")
+
+
+def main():
+ parser = argparse.ArgumentParser(description="Export SQL parser Failsafe
errors to Excel.")
+ parser.add_argument("--reports-dir", type=Path, required=True)
+ parser.add_argument("--test-class", required=True)
+ parser.add_argument("--database", required=True)
+ parser.add_argument("--output", type=Path, required=True)
+ args = parser.parse_args()
+ generate_report(args.reports_dir, args.test_class, args.database,
args.output)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/.github/workflows/nightly-sql-parser.yml
b/.github/workflows/nightly-sql-parser.yml
index 7b02ad62719..8d756687f25 100644
--- a/.github/workflows/nightly-sql-parser.yml
+++ b/.github/workflows/nightly-sql-parser.yml
@@ -60,10 +60,31 @@ jobs:
cache-prefix: ${{
needs.global-environment.outputs.GLOBAL_CACHE_PREFIX }}
- name: Build Project with Maven
run: ./mvnw -T1C -B -ntp clean install
+ - name: Clear Previous SQL Parser XML
+ run: rm -f test/it/parser/target/failsafe-reports/TEST-*${{
matrix.test }}.xml
- name: Run SQL Parser
- run: ./mvnw -nsu -B install -f test/it/parser/pom.xml
-Dsql.parser.external.it.enabled=true -Dit.test=${{ matrix.test }}
+ id: sql-parser
+ run: |
+ mkdir -p "$RUNNER_TEMP/sql-parser-report-${{ matrix.database }}"
+ ./mvnw -nsu -B install -f test/it/parser/pom.xml
-Pexternal-sql-parser-report \
+ -Dsql.parser.external.it.enabled=true -Dit.test=${{ matrix.test }}
\
+
-Dsql.parser.external.it.report.path="$RUNNER_TEMP/sql-parser-report-${{
matrix.database }}/"
+ - name: Set up Python for SQL Report
+ if: ${{ !cancelled() && steps.sql-parser.outcome != 'skipped' }}
+ uses: actions/setup-python@v6
+ with:
+ python-version: '3.12'
+ - name: Generate SQL Parser Excel Report
+ if: ${{ !cancelled() && steps.sql-parser.outcome != 'skipped' }}
+ run: |
+ python -m pip install --disable-pip-version-check --target
"$RUNNER_TEMP/sql-parser-report-dependencies" openpyxl==3.1.5
+ PYTHONPATH="$RUNNER_TEMP/sql-parser-report-dependencies" python
.github/scripts/generate-sql-parser-report.py \
+ --reports-dir test/it/parser/target/failsafe-reports --test-class
${{ matrix.test }} \
+ --database ${{ matrix.database }} \
+ --output "$RUNNER_TEMP/sql-parser-report-${{ matrix.database
}}/${{ matrix.database }}-errors.xlsx"
- uses: actions/upload-artifact@v6
- if: ${{ !cancelled() }}
+ if: ${{ !cancelled() && steps.sql-parser.outcome != 'skipped' }}
with:
name: sql-report-${{ matrix.database }}
- path: /tmp/*.csv
+ path: ${{ runner.temp }}/sql-parser-report-${{ matrix.database }}/
+ if-no-files-found: error
diff --git a/test/it/parser/pom.xml b/test/it/parser/pom.xml
index aefd0e1e87b..2152787d784 100644
--- a/test/it/parser/pom.xml
+++ b/test/it/parser/pom.xml
@@ -41,6 +41,7 @@
<artifactId>shardingsphere-parser-sql-engine-core</artifactId>
<version>${project.version}</version>
</dependency>
+
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>shardingsphere-data-pipeline-cdc-distsql-parser</artifactId>
@@ -204,4 +205,26 @@
<scope>compile</scope>
</dependency>
</dependencies>
+ <profiles>
+ <profile>
+ <id>external-sql-parser-report</id>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-failsafe-plugin</artifactId>
+ <configuration>
+ <trimStackTrace>false</trimStackTrace>
+ <statelessTestsetReporter
implementation="org.apache.maven.plugin.surefire.extensions.junit5.JUnit5Xml30StatelessReporter">
+
<usePhrasedTestCaseMethodName>true</usePhrasedTestCaseMethodName>
+ </statelessTestsetReporter>
+ <properties>
+
<configurationParameters>junit.jupiter.params.displayname.argument.maxlength=2147483647</configurationParameters>
+ </properties>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+ </profile>
+ </profiles>
</project>