blob: d839ef4222875f8897dd6d8f60b731dbda9a0688 [file]
#!/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.
#
"""End-to-end exercise of the drop-in ``pyspark`` API against a live Connect server.
Unlike the official parity suite (which uses the reference pyspark classes and only routes
bytes through our Rust transport), this drives OUR drop-in wrapper: every call goes
pyspark-rs (PyDataFrame/PySession/...) -> spark-connect (DataFrame/Session/...) -> plan
building -> execution. It is the coverage vehicle for the high-level API that the
transport-injection gate does not exercise.
Each operation is checked independently (via ``ck``) so one gap neither stops the rest nor
hides which methods are missing/divergent. Run against a live server:
RUST_PYSPARK_SO=/repo/python/pyspark/_pyspark.so \
SPARK_REMOTE=sc://localhost:15002 PYTHONPATH=/repo/python \
python scripts/e2e_wrapper.py
"""
import os
import sys
import tempfile
REMOTE = os.environ.get("SPARK_REMOTE") or os.environ.get(
"SPARK_CONNECT_TESTING_REMOTE", "sc://localhost:15002"
)
_ok = []
_fail = []
def ck(label, fn):
"""Run one operation independently; record ok/fail, never abort the run.
This only proves the call does not raise. For correctness (the bug class this
client has hit is *silently wrong values*, e.g. a nullary ``F.struct()``
returning an empty struct and throwing nothing), use ``ckv`` below, which
asserts the actual result.
"""
try:
fn()
_ok.append(label)
except Exception as e: # noqa: BLE001 - best-effort surface exercise
_fail.append(f"{label}: {type(e).__name__}: {str(e)[:110]}")
def ckv(label, fn, expected):
"""Like ``ck`` but assert ``fn() == expected`` - catches silently-wrong values."""
try:
got = fn()
if got != expected:
_fail.append(f"{label}: expected {expected!r}, got {got!r}")
else:
_ok.append(label)
except Exception as e: # noqa: BLE001
_fail.append(f"{label}: {type(e).__name__}: {str(e)[:110]}")
def main():
from pyspark.sql import Row, SparkSession
from pyspark.sql import functions as F
from pyspark.sql.window import Window
spark = SparkSession.builder.remote(REMOTE).getOrCreate()
df = spark.createDataFrame(
[(1, "a", 10.0), (2, "b", 20.0), (2, "c", 30.0), (3, "d", 40.0)],
["id", "name", "val"],
)
df.createOrReplaceTempView("t_e2e") if hasattr(df, "createOrReplaceTempView") else None
other = df.select("id", "name", "val")
# ---- SparkSession -----------------------------------------------------------
ck("session.version", lambda: spark.version)
ck("session.range", lambda: spark.range(3).collect())
ck("session.range3", lambda: spark.range(0, 10, 2).count())
ck("session.sql", lambda: spark.sql("SELECT 1 AS a, 'x' AS b").collect())
ck("session.conf.set", lambda: spark.conf.set("spark.sql.shuffle.partitions", "7"))
ck("session.conf.get", lambda: spark.conf.get("spark.sql.shuffle.partitions"))
ck("session.read", lambda: spark.read)
ck("session.catalog.prop", lambda: spark.catalog.currentDatabase())
ck("session.udf", lambda: spark.udf)
ck(
"session.createDataFrame.names",
lambda: spark.createDataFrame([(1, "a")], ["i", "s"]).count(),
)
ck(
"session.createDataFrame.ddl",
lambda: spark.createDataFrame([(1, "a")], "i int, s string").count(),
)
ck("session.createDataFrame.rows", lambda: spark.createDataFrame([Row(x=1), Row(x=2)]).count())
# ---- DataFrame projection / columns ----------------------------------------
ck("df.select.str", lambda: df.select("id", "name").collect())
ck("df.attr.col", lambda: df.select(df.id).collect())
ck("df.getitem.col", lambda: df.select(df["id"]).collect())
ck("df.select.expr", lambda: df.select(F.col("name").alias("n")).collect())
ck("df.selectExpr", lambda: df.selectExpr("id + 1 as id2", "upper(name) as up").collect())
ck("df.withColumn", lambda: df.withColumn("v2", F.col("val") * 2).collect())
ck("df.withColumns", lambda: df.withColumns({"a": F.lit(1), "b": F.lit(2)}).collect())
ck("df.withColumnRenamed", lambda: df.withColumnRenamed("val", "value").collect())
ck("df.withColumnsRenamed", lambda: df.withColumnsRenamed({"id": "identifier"}).collect())
ck("df.drop", lambda: df.drop("val").collect())
ck("df.toDF", lambda: df.toDF("c1", "c2", "c3").collect())
ck("df.colRegex", lambda: df.colRegex("`.*`").collect())
# ---- filter / sort / limit --------------------------------------------------
ck("df.filter.col", lambda: df.filter(F.col("id") > 1).collect())
ck("df.where.col", lambda: df.where(F.col("id") > 1).collect())
ck("df.where.str", lambda: df.where("id >= 2").collect())
ck("df.filter.str", lambda: df.filter("id >= 2").collect())
ck("df.sort", lambda: df.sort(F.col("id").desc()).collect())
ck("df.orderBy", lambda: df.orderBy("name").collect())
ck("df.sortWithinPartitions", lambda: df.sortWithinPartitions("id").collect())
ck("df.limit", lambda: df.limit(2).collect())
ck("df.offset", lambda: df.offset(1).collect())
ck("df.distinct", lambda: df.distinct().count())
ck("df.dropDuplicates", lambda: df.dropDuplicates(["id"]).count())
ck("df.sample", lambda: df.sample(0.5, seed=1).count())
# ---- set ops ----------------------------------------------------------------
ck("df.union", lambda: df.union(other).count())
ck("df.unionByName", lambda: df.unionByName(other).count())
ck("df.unionAll", lambda: df.unionAll(other).count())
ck("df.intersect", lambda: df.intersect(other).count())
ck("df.intersectAll", lambda: df.intersectAll(other).count())
ck("df.exceptAll", lambda: df.exceptAll(other).count())
ck("df.subtract", lambda: df.subtract(other).count())
# ---- join -------------------------------------------------------------------
a = df.select(F.col("id").alias("id"), "name")
b = df.select(F.col("id").alias("id"), "val")
b2 = df.select(F.col("id").alias("id2"), "val")
ck("df.join.on", lambda: a.join(b, "id").collect())
ck("df.join.how", lambda: a.join(b, on="id", how="left").collect())
ck("df.join.cond", lambda: a.join(b2, F.col("id") == F.col("id2"), "inner").collect())
ck("df.crossJoin", lambda: a.crossJoin(b.limit(1)).collect())
ck("df.hint", lambda: a.hint("broadcast").join(b, "id").collect())
# ---- aggregation ------------------------------------------------------------
ck("gb.count", lambda: df.groupBy("id").count().collect())
ck("gb.agg", lambda: df.groupBy("id").agg(F.sum("val").alias("s"), F.avg("val")).collect())
ck("gb.min", lambda: df.groupBy("id").min("val").collect())
ck("gb.max", lambda: df.groupBy("id").max("val").collect())
ck("gb.mean", lambda: df.groupBy("id").mean("val").collect())
ck("gb.sum", lambda: df.groupBy("id").sum("val").collect())
ck("gb.pivot", lambda: df.groupBy("id").pivot("name").sum("val").collect())
ck("df.rollup", lambda: df.rollup("id", "name").count().collect())
ck("df.cube", lambda: df.cube("id").count().collect())
ck("df.agg", lambda: df.agg(F.countDistinct("name").alias("cd")).collect())
# ---- na / stat --------------------------------------------------------------
ck("na.fill", lambda: df.na.fill(0).collect())
ck("na.drop", lambda: df.na.drop().collect())
ck("na.replace", lambda: df.na.replace(["a"], ["A"], "name").collect())
ck("df.fillna", lambda: df.fillna({"val": 0.0}).collect())
ck("df.dropna", lambda: df.dropna().collect())
ck("stat.corr", lambda: df.stat.corr("id", "val"))
ck("stat.cov", lambda: df.stat.cov("id", "val"))
ck("stat.crosstab", lambda: df.stat.crosstab("id", "name").collect())
ck("stat.freqItems", lambda: df.stat.freqItems(["name"]).collect())
ck("stat.approxQuantile", lambda: df.stat.approxQuantile("val", [0.5], 0.1))
ck("df.summary", lambda: df.summary().collect())
ck("df.describe", lambda: df.describe("val").collect())
# ---- reshape / partition ----------------------------------------------------
ck("df.repartition.n", lambda: df.repartition(2).count())
ck("df.repartition.cols", lambda: df.repartition(2, "id").count())
ck("df.coalesce", lambda: df.coalesce(1).count())
ck("df.unpivot", lambda: df.unpivot(["id"], ["val"], "var", "value").collect())
ck("df.melt", lambda: df.melt(["id"], ["val"], "var", "value").collect())
# ---- actions / metadata -----------------------------------------------------
ck("df.count", lambda: df.count())
ck("df.head", lambda: df.head())
ck("df.head.n", lambda: df.head(2))
ck("df.take", lambda: df.take(2))
ck("df.first", lambda: df.first())
ck("df.tail", lambda: df.tail(2))
ck("df.toPandas", lambda: df.limit(1).toPandas())
ck("df.schema", lambda: df.schema)
ck("df.dtypes", lambda: df.dtypes)
ck("df.columns", lambda: df.columns)
ck("df.isEmpty", lambda: df.isEmpty())
ck("df.printSchema", lambda: df.printSchema())
ck("df.explain", lambda: df.explain())
ck("df.show", lambda: df.show(2))
ck("df.cache", lambda: df.cache())
ck("df.persist", lambda: df.persist())
ck("df.unpersist", lambda: df.unpersist())
# ---- Column expression surface ---------------------------------------------
c = F.col("val")
ck("col.arith", lambda: df.select((c + 1), (c - 1), (c * 2), (c / 2), (c % 3), (-c)).collect())
ck("col.cmp", lambda: df.select(c > 1, c >= 1, c < 100, c <= 100, c == 10, c != 10).collect())
ck("col.isNull", lambda: df.select(c.isNull(), c.isNotNull()).collect())
ck("col.between", lambda: df.select(c.between(0, 100)).collect())
ck("col.isin", lambda: df.select(F.col("name").isin("a", "b")).collect())
ck("col.like", lambda: df.select(F.col("name").like("a%"), F.col("name").rlike("^a")).collect())
ck(
"col.strfns",
lambda: df.select(
F.col("name").contains("a"),
F.col("name").startswith("a"),
F.col("name").endswith("z"),
F.col("name").substr(1, 2),
).collect(),
)
ck("col.cast", lambda: df.select(c.cast("int")).collect())
ck("col.when", lambda: df.select(F.when(c > 15, "hi").otherwise("lo")).collect())
ck(
"col.bitwise",
lambda: df.select(
F.col("id").bitwiseAND(F.lit(1)), F.col("id").bitwiseOR(F.lit(1))
).collect(),
)
ck("col.getField", lambda: hasattr(F.col("x"), "getField"))
ck("col.sortmods", lambda: df.sort(F.col("id").asc()).sort(F.col("id").desc()).collect())
# ---- functions --------------------------------------------------------------
ck(
"fn.math",
lambda: df.select(F.abs(c), F.sqrt(c), F.round(c, 1), F.ceil(c), F.floor(c)).collect(),
)
ck(
"fn.string",
lambda: df.select(
F.upper(F.col("name")),
F.lower(F.col("name")),
F.length(F.col("name")),
F.concat(F.col("name"), F.lit("!")),
F.concat_ws("-", F.col("name"), F.col("name")),
).collect(),
)
ck(
"fn.cond",
lambda: df.select(
F.coalesce(c, F.lit(0.0)),
F.greatest(F.col("id"), F.lit(5)),
F.least(F.col("id"), F.lit(5)),
).collect(),
)
ck(
"fn.gen",
lambda: df.select(
F.current_date(), F.current_timestamp(), F.monotonically_increasing_id()
).collect(),
)
w = Window.partitionBy("id").orderBy("val")
ck(
"fn.window",
lambda: df.select(
F.row_number().over(w),
F.rank().over(w),
F.dense_rank().over(w),
F.lag("val").over(w),
F.lead("val").over(w),
).collect(),
)
# ---- read / write -----------------------------------------------------------
d = tempfile.mkdtemp()
ck("write.parquet", lambda: df.write.mode("overwrite").parquet(os.path.join(d, "p")))
ck("read.parquet", lambda: spark.read.parquet(os.path.join(d, "p")).count())
ck("write.json", lambda: df.write.mode("overwrite").json(os.path.join(d, "j")))
ck("read.json", lambda: spark.read.json(os.path.join(d, "j")).count())
ck(
"write.csv",
lambda: df.write.mode("overwrite").option("header", True).csv(os.path.join(d, "c")),
)
ck("read.csv", lambda: spark.read.option("header", True).csv(os.path.join(d, "c")).count())
ck(
"write.save",
lambda: df.write.format("parquet").mode("overwrite").save(os.path.join(d, "s")),
)
# ---- catalog ----------------------------------------------------------------
ck("catalog.currentDatabase", lambda: spark.catalog.currentDatabase())
ck("catalog.listDatabases", lambda: spark.catalog.listDatabases())
ck("catalog.listTables", lambda: spark.catalog.listTables())
ck("catalog.tempView", lambda: df.createOrReplaceTempView("v_e2e2"))
ck("catalog.tableExists", lambda: spark.catalog.tableExists("v_e2e2"))
ck("catalog.table", lambda: spark.table("v_e2e2").count())
ck("catalog.dropTempView", lambda: spark.catalog.dropTempView("v_e2e2"))
# ---- functions: build every function once (covers functions.rs) -------------
def _build_all_functions():
ci, cs, cd = F.col("id"), F.col("name"), F.col("val")
lit1 = F.lit(1)
variants = [
(ci,),
(ci, cd),
(ci, cd, ci),
(ci, cs),
(ci, "x"),
(ci, 1),
(cs,),
("x",),
(lit1,),
(),
(ci, ci, ci, ci),
(cs, cs),
]
built = 0
for name in dir(F):
if name.startswith("_"):
continue
fn = getattr(F, name)
if not callable(fn):
continue
for a in variants:
try:
fn(*a)
built += 1
break
except Exception: # noqa: BLE001 - wrong arity/type; try the next shape
continue
assert built > 400, f"only {built} functions built"
ck("functions.build_all", _build_all_functions)
# ---- window frame specs -----------------------------------------------------
ck(
"window.rowsBetween",
lambda: df.select(
F.sum("val").over(
Window.partitionBy("id")
.orderBy("val")
.rowsBetween(Window.unboundedPreceding, Window.currentRow)
)
).collect(),
)
ck(
"window.rangeBetween",
lambda: df.select(
F.sum("val").over(
Window.partitionBy("id")
.orderBy("val")
.rangeBetween(Window.unboundedPreceding, Window.currentRow)
)
).collect(),
)
# ---- session extras ---------------------------------------------------------
ck("session.sessionId", lambda: spark.sessionId)
ck("session.newSession", lambda: spark.newSession())
ck("session.cloneSession", lambda: spark.cloneSession())
ck("session.emptyDataFrame", lambda: spark.emptyDataFrame().count())
ck("session.range_full", lambda: spark.range(0, 10, 2, 2).count())
ck("session.addTag", lambda: spark.addTag("e2e-tag"))
ck("session.getTags", lambda: spark.getTags())
ck("session.removeTag", lambda: spark.removeTag("e2e-tag"))
ck("session.clearTags", lambda: spark.clearTags())
ck("session.interruptAll", lambda: spark.interruptAll())
ck("session.interruptTag", lambda: spark.interruptTag("e2e-tag"))
ck("session.getActiveSession", lambda: SparkSession.getActiveSession())
ck("session.profile", lambda: spark.profile)
ck("session.dataSource", lambda: spark.dataSource)
# ---- artifact upload (client-side chunking/CRC path) ------------------------
def _artifacts():
art_dir = tempfile.mkdtemp()
f1 = os.path.join(art_dir, "e2e_artifact_a.txt")
f2 = os.path.join(art_dir, "e2e_artifact_b.txt")
with open(f1, "w") as fh:
fh.write("drop-in artifact one")
with open(f2, "wb") as fh:
fh.write(b"y" * (256 * 1024)) # exercise the multi-chunk path
spark.addArtifact(f1)
spark.addArtifacts(f1, f2)
ck("session.addArtifact", _artifacts)
# ---- resource profile -------------------------------------------------------
def _resource():
from pyspark.resource import (
ExecutorResourceRequests,
ResourceProfileBuilder,
TaskResourceRequests,
)
rpb = ResourceProfileBuilder()
rpb.require(ExecutorResourceRequests().cores(2))
rpb.require(TaskResourceRequests().cpus(1))
_ = rpb.build
ck("resource.profile", _resource)
# ---- Column: remaining surface ----------------------------------------------
cid = F.col("id")
ck("col.asc_nulls", lambda: df.sort(cid.asc_nulls_first(), cid.asc_nulls_last()).collect())
ck(
"col.desc_nulls",
lambda: df.sort(cid.desc(), cid.desc_nulls_first(), cid.desc_nulls_last()).collect(),
)
ck("col.bitwiseXOR", lambda: df.select(cid.bitwiseXOR(F.lit(1))).collect())
ck("col.eqNullSafe", lambda: df.select(cid.eqNullSafe(F.lit(1))).collect())
ck("col.ilike", lambda: df.select(F.col("name").ilike("A%")).collect())
ck("col.isNaN", lambda: df.select(F.col("val").isNaN()).collect())
ck("col.alias", lambda: df.select(cid.alias("renamed")).collect())
def _struct_fields():
s = df.select(F.struct(F.col("id"), F.col("name"), F.col("val")).alias("s"))
# withField adds/replaces a field; dropFields removes one (leaving others).
s.select(F.col("s").withField("z", F.lit(1))).collect()
s.select(F.col("s").dropFields("id")).collect()
ck("col.withField/dropFields", _struct_fields)
# ---- Catalog: full surface --------------------------------------------------
df.createOrReplaceTempView("cat_view")
ck("catalog.currentCatalog", lambda: spark.catalog.currentCatalog())
ck("catalog.listCatalogs", lambda: spark.catalog.listCatalogs())
ck("catalog.databaseExists", lambda: spark.catalog.databaseExists("default"))
ck("catalog.functionExists", lambda: spark.catalog.functionExists("abs"))
ck("catalog.getDatabase", lambda: spark.catalog.getDatabase("default"))
ck("catalog.listColumns", lambda: spark.catalog.listColumns("cat_view"))
ck("catalog.listFunctions", lambda: spark.catalog.listFunctions())
ck("catalog.getTable", lambda: spark.catalog.getTable("cat_view"))
ck("catalog.cacheTable", lambda: spark.catalog.cacheTable("cat_view"))
ck("catalog.uncacheTable", lambda: spark.catalog.uncacheTable("cat_view"))
ck("catalog.setCurrentDatabase", lambda: spark.catalog.setCurrentDatabase("default"))
# ---- DataFrame: remaining surface -------------------------------------------
ck("df.alias", lambda: df.alias("d2").select("id").collect())
ck("df.toArrow", lambda: df.limit(2).toArrow())
ck("df.toLocalIterator", lambda: list(df.limit(2).toLocalIterator()))
ck("df.executionInfo", lambda: (df.limit(1).collect(), df.executionInfo))
# ---- DataFrameWriter: remaining formats -------------------------------------
ck("write.orc", lambda: df.write.mode("overwrite").orc(os.path.join(d, "o")))
ck("read.orc", lambda: spark.read.orc(os.path.join(d, "o")).count())
ck(
"write.text",
lambda: df.select(F.col("name")).write.mode("overwrite").text(os.path.join(d, "txt")),
)
ck("read.text", lambda: spark.read.text(os.path.join(d, "txt")).count())
ck("read.load", lambda: spark.read.format("parquet").load(os.path.join(d, "p")).count())
ck(
"write.saveAsTable",
lambda: df.write.mode("overwrite").saveAsTable("e2e_saved_table"),
)
ck("read.table.saved", lambda: spark.table("e2e_saved_table").count())
# ---- Window standalone builders ---------------------------------------------
ck("window.orderBy", lambda: F.row_number().over(Window.orderBy("id")))
ck(
"window.rangeBetween.unbounded",
lambda: df.select(
F.sum("val").over(
Window.orderBy("id").rangeBetween(
Window.unboundedPreceding, Window.unboundedFollowing
)
)
).collect(),
)
# ---- DataStreamReader: option/options/schema variants -----------------------
ck(
"readStream.options",
lambda: spark.readStream.format("rate").options(rowsPerSecond="1").load().isStreaming,
)
ck(
"readStream.schema",
lambda: spark.readStream.format("rate")
.schema("timestamp timestamp, value long")
.option("rowsPerSecond", 1)
.load()
.isStreaming,
)
# ---- streaming query: exercise the full StreamingQuery/manager surface -------
def _streaming():
import time
sdf = spark.readStream.format("rate").option("rowsPerSecond", 1).load()
assert sdf.isStreaming
q = (
sdf.writeStream.format("memory")
.queryName("e2e_stream")
.outputMode("append")
.trigger(processingTime="1 second")
.start()
)
time.sleep(2)
# Read-only StreamingQuery surface (all reachable without a Python worker).
# NB: not processAllAvailable() - it blocks forever on an unbounded rate source.
_ = (q.id, q.runId, q.name, q.isActive, q.status, q.recentProgress, q.lastProgress)
_ = q.explain()
_ = q.exception()
_ = q.awaitTermination(0.2)
# StreamingQueryManager surface.
mgr = spark.streams
_ = mgr.active
_ = mgr.get(q.id)
_ = mgr.awaitAnyTermination(0.2)
q.stop()
mgr.resetTerminated()
ck("streaming.query_surface", _streaming)
# ---- Trigger variants + writer options (no query started) -------------------
def _trigger_variants():
base = spark.readStream.format("rate").option("rowsPerSecond", 1).load()
# Each builds a distinct Trigger/DataStreamWriter without starting.
_ = base.writeStream.trigger(once=True)
_ = base.writeStream.trigger(availableNow=True)
_ = base.writeStream.trigger(continuous="1 second")
_ = base.writeStream.format("memory").options(truncate="false")
_ = base.writeStream.partitionBy("value")
ck("streaming.trigger_variants", _trigger_variants)
# ---- correctness assertions (catch silently-wrong values, not just exceptions) ---
# These pin actual results for the operations most prone to returning wrong-but-
# non-throwing data - especially the variadic functions whose bug was an empty
# result with no exception.
ckv("val.range.count", lambda: spark.range(5).count(), 5)
ckv("val.range.rows", lambda: [r.id for r in spark.range(3).collect()], [0, 1, 2])
ckv("val.df.count", lambda: df.count(), 4)
ckv("val.filter.count", lambda: df.filter(F.col("id") > 1).count(), 3)
ckv("val.distinct.count", lambda: df.select("id").distinct().count(), 3)
ckv(
"val.groupby.sum",
lambda: {r.id: r.s for r in df.groupBy("id").agg(F.sum("val").alias("s")).collect()},
{1: 10.0, 2: 50.0, 3: 40.0},
)
ckv("val.union.count", lambda: df.union(other).count(), 8)
ckv("val.join.count", lambda: df.select("id").join(other.select("id"), "id").count(), 6)
# The variadic-function bug class: these must return populated results.
ckv(
"val.struct",
# dict(...) normalizes whether the nested struct comes back as a Row or a dict;
# the point is it is *populated* (the nullary-F.struct bug returned {}).
lambda: dict(
spark.range(1)
.select(F.struct(F.lit(1).alias("a"), F.lit("x").alias("b")).alias("s"))
.collect()[0]["s"]
),
{"a": 1, "b": "x"},
)
ckv(
"val.array",
lambda: spark.range(1)
.select(F.array(F.lit(1), F.lit(2), F.lit(3)).alias("a"))
.collect()[0]["a"],
[1, 2, 3],
)
ckv(
"val.coalesce",
lambda: spark.range(1)
.select(F.coalesce(F.lit(None), F.lit(7)).alias("c"))
.collect()[0]["c"],
7,
)
ckv(
"val.concat",
lambda: spark.range(1)
.select(F.concat(F.lit("a"), F.lit("b")).alias("c"))
.collect()[0]["c"],
"ab",
)
ckv(
"val.create_map",
lambda: spark.range(1)
.select(F.create_map(F.lit("k"), F.lit(9)).alias("m"))
.collect()[0]["m"],
{"k": 9},
)
ckv("val.sql.lit", lambda: spark.sql("SELECT 1 AS a").collect()[0]["a"], 1)
ck("session.stop", lambda: spark.stop())
total = len(_ok) + len(_fail)
print(f"\n=== e2e_wrapper: {len(_ok)}/{total} operations OK, {len(_fail)} failed ===")
for s in _fail:
print(f" FAIL {s}")
# Non-zero only if almost nothing ran (harness broken), not on per-op gaps.
return 0 if len(_ok) >= total * 0.5 else 1
if __name__ == "__main__":
sys.exit(main())