blob: 471fdac498fd8a2e528f3b036e6271b68bf852c8 [file]
# 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.
# The test matrix for .github/workflows/spark_sql_test_reusable.yml.
#
# The rows used to be a literal `strategy.matrix.module` list in the workflow.
# They live here so that a caller can ask for a subset: the umbrella lets a
# pull request opt into only the Spark 4.1 `sql_hive` shards (see POLICY in
# dev/ci/compute-changes.py and issue #5870) and a job-level `if:` cannot see
# `matrix`, so the selection has to happen before the matrix is expanded. The
# `build` job runs this script and publishes the result as a job output that
# `spark-sql-test` reads with `fromJSON`, the same way the Iceberg reusable
# workflow sizes its shards.
#
# Usage:
# spark-sql-modules.py --modules all|core|hive --github-output $GITHUB_OUTPUT
# spark-sql-modules.py --modules core (prints the matrix JSON)
import argparse
import json
import sys
from pathlib import Path
# `group` is what --modules selects on. `heap` and `metaspace` are read by the
# "Run Spark tests" step as per-row forked-test-JVM caps for SparkBuild.scala;
# the sql_core rows set them because those shards were the ones hitting the
# 7 GB runner budget.
#
# The tag filters split each module three ways, the same as Spark's own CI.
# That left two rows far longer than the rest, and the longest row is what a
# merge-queue run waits for, so the suites below are moved into rows of their
# own (issue #6388). Each is excluded from the row it came from with sbt's
# `-<glob>` testOnly syntax, and its row keeps that row's tag filters, so every
# test still runs exactly once. The globs are package prefixes, which also
# catch suites added to those packages later.
#
# HivePartitionFilteringSuites runs HivePartitionFilteringSuite against every
# Hive client version, 21 to 35 minutes of sql_hive-1 in September 2026. It is
# one class, so no name filter splits it further.
HIVE_MOVED = ["org.apache.spark.sql.hive.client.HivePartitionFilteringSuites"]
# The data source and connector suites were 13 to 19 minutes of sql_core-1 and,
# with their Extended-tagged schema-pruning suites, 11 minutes of sql_core-2.
CORE_MOVED = [
"org.apache.spark.sql.execution.datasources.*",
"org.apache.spark.sql.connector.*",
]
SQL_EXTENDED = "org.apache.spark.tags.ExtendedSQLTest"
SQL_SLOW = "org.apache.spark.tags.SlowSQLTest"
HIVE_EXTENDED = "org.apache.spark.tags.ExtendedHiveTest"
HIVE_SLOW = "org.apache.spark.tags.SlowHiveTest"
def excluding(globs):
return " ".join(f"-{glob}" for glob in globs)
MODULES = [
{"name": "catalyst", "group": "core", "args1": "catalyst/test", "args2": ""},
{
"name": "sql_core-1",
"group": "core",
"args1": "",
"args2": f"sql/testOnly * {excluding(CORE_MOVED)} -- -l {SQL_EXTENDED} -l {SQL_SLOW}",
"heap": "3g",
"metaspace": "1g",
},
{
"name": "sql_core-2",
"group": "core",
"args1": "",
"args2": f"sql/testOnly * {excluding(CORE_MOVED)} -- -n {SQL_EXTENDED}",
"heap": "3g",
"metaspace": "1g",
},
{
"name": "sql_core-3",
"group": "core",
"args1": "",
"args2": f"sql/testOnly * -- -n {SQL_SLOW}",
"heap": "3g",
"metaspace": "1g",
},
{
# The moved suites' untagged and Extended tests; their Slow tests stay
# in sql_core-3 with every other Slow test.
"name": "sql_core-4",
"group": "core",
"args1": "",
"args2": f"sql/testOnly {' '.join(CORE_MOVED)} -- -l {SQL_SLOW}",
"heap": "3g",
"metaspace": "1g",
},
{
"name": "sql_hive-1",
"group": "hive",
"args1": "",
"args2": f"hive/testOnly * {excluding(HIVE_MOVED)} -- -l {HIVE_EXTENDED} -l {HIVE_SLOW}",
},
{
"name": "sql_hive-2",
"group": "hive",
"args1": "",
"args2": f"hive/testOnly * -- -n {HIVE_EXTENDED}",
},
{
"name": "sql_hive-3",
"group": "hive",
"args1": "",
"args2": f"hive/testOnly * -- -n {HIVE_SLOW}",
},
{
"name": "sql_hive-4",
"group": "hive",
"args1": "",
"args2": f"hive/testOnly {' '.join(HIVE_MOVED)} -- -l {HIVE_EXTENDED} -l {HIVE_SLOW}",
},
]
GROUPS = ("all", "core", "hive")
def select(modules):
"""Return the matrix rows for a --modules value, or raise ValueError."""
if modules not in GROUPS:
raise ValueError(f"--modules must be one of {', '.join(GROUPS)}, got {modules!r}")
rows = [row for row in MODULES if modules == "all" or row["group"] == modules]
# Every row carries every key, so a `${{ matrix.module.heap }}` lookup on a
# row without a cap is an empty string rather than a template error.
keys = sorted({key for row in MODULES for key in row})
return [{key: row.get(key, "") for key in keys} for row in rows]
def main(argv):
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--modules", default="all", help="all, core (catalyst + sql_core) or hive (sql_hive)")
parser.add_argument("--github-output", type=Path, help="append matrix=<json> to this $GITHUB_OUTPUT file")
args = parser.parse_args(argv)
try:
rows = select(args.modules)
except ValueError as e:
print(f"error: {e}", file=sys.stderr)
return 2
matrix = json.dumps({"module": rows})
if args.github_output:
with args.github_output.open("a", encoding="utf-8") as out:
out.write(f"matrix={matrix}\n")
else:
print(matrix)
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))