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>

Reply via email to