Skip to content

Commit 5beaa5b

Browse files
sarutakpan3793
authored andcommitted
[SPARK-56353][BUILD][TESTS] Generate Java-based test JARs dynamically instead of storing pre-built binaries
### What changes were proposed in this pull request? This PR is a part of SPARK-56352 for Java-based test JARs, replacing pre-built test JAR files containing Java classes with dynamic compilation at test time, removing 7 binary JAR files from the repository. Changes: - Add `TestUtils.createJarWithJavaSources()` in `SparkTestUtils.scala` that compiles Java source code with `javac` and packages the resulting classes into a JAR. - Externalize Java source files for Hive UDFs/UDAFs/UDTFs to `src/test/resources/` (9 files), loaded at test time via `getContextClassLoader.getResource()`. These source files were reverse-engineered from the class files in the pre-built JARs, as no original source code existed in the repository. - Update test suites to use dynamically generated JARs instead of pre-built ones. - Remove deleted JAR entries from `dev/test-jars.txt`. JARs removed: - `core/src/test/resources/TestUDTF.jar` - `sql/core/src/test/resources/SPARK-33084.jar` - `sql/hive-thriftserver/src/test/resources/TestUDTF.jar` - `sql/hive/src/test/noclasspath/hive-test-udfs.jar` - `sql/hive/src/test/resources/SPARK-21101-1.0.jar` - `sql/hive/src/test/resources/TestUDTF.jar` - `sql/hive/src/test/resources/data/files/TestSerDe.jar` ### Why are the changes needed? As noted in the PR discussion [here](#50378 (review)): > the ultimate goal is to refactor the tests to automatically build the jars instead of using pre-built ones This PR achieves that goal for all Java-based test JARs. By generating JARs dynamically at test time, no binary artifacts need to be stored in the source tree, and the release-time workaround becomes unnecessary for these files. ### Does this PR introduce _any_ user-facing change? No. ### How was this patch tested? All affected test suites pass: - SparkContextSuite, SparkSubmitSuite, TaskSetManagerSuite (core) - SQLQuerySuite (sql/core) - HiveUDFDynamicLoadSuite, HiveDDLSuite, HiveQuerySuite, HiveUDFSuite, SQLQuerySuite (sql/hive) - CliSuite, HiveThriftServer2Suites (sql/hive-thriftserver) No tests were added or removed. Existing tests now compile Java sources at runtime instead of loading pre-built JARs. ### Was this patch authored or co-authored using generative AI tooling? Kiro CLI / Opus 4.6 Closes #55192 from sarutak/remove-test-jars-ab. Authored-by: Kousuke Saruta <sarutak@amazon.co.jp> Signed-off-by: Cheng Pan <chengpan@apache.org>
1 parent 732f30b commit 5beaa5b

33 files changed

Lines changed: 1341 additions & 86 deletions

File tree

common/utils/src/main/scala/org/apache/spark/util/SparkTestUtils.scala

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,11 @@
1717

1818
package org.apache.spark.util
1919

20-
import java.io.File
20+
import java.io.{File, FileInputStream, FileOutputStream}
2121
import java.net.{URI, URL}
2222
import java.nio.file.Files
2323
import java.util.Arrays
24+
import java.util.jar.{JarEntry, JarOutputStream}
2425
import javax.tools.{JavaFileObject, SimpleJavaFileObject, ToolProvider}
2526

2627
import scala.jdk.CollectionConverters._
@@ -99,6 +100,69 @@ private[spark] trait SparkTestUtils {
99100
createCompiledClass(className, destDir, sourceFile, classpathUrls)
100101
}
101102

103+
/**
104+
* Compile Java source code and package the resulting class files into a JAR.
105+
* Supports classes with package declarations - the JAR will contain the proper
106+
* directory structure (e.g., org/apache/spark/Foo.class).
107+
*
108+
* @param sources map of fully-qualified class name to Java source code
109+
* @param jarFile the JAR file to create
110+
* @param classpathUrls additional classpath URLs needed for compilation
111+
* @return URL of the created JAR file
112+
*/
113+
def createJarWithJavaSources(
114+
sources: Map[String, String],
115+
jarFile: File,
116+
classpathUrls: Seq[URL] = Seq.empty): URL = {
117+
val compiler = ToolProvider.getSystemJavaCompiler
118+
val classDir = Files.createTempDirectory("spark-test-classes").toFile
119+
120+
val sourceFiles = sources.map { case (name, code) =>
121+
new JavaSourceFromString(name, code)
122+
}.toList
123+
124+
val options = Seq("-d", classDir.getAbsolutePath) ++ (
125+
if (classpathUrls.nonEmpty) {
126+
Seq("-classpath", classpathUrls.map(_.getFile).mkString(File.pathSeparator))
127+
} else Seq.empty
128+
)
129+
130+
val success = compiler.getTask(
131+
null, null, null, options.asJava, null,
132+
java.util.Arrays.asList(sourceFiles: _*)).call()
133+
assert(success, s"Compilation failed for: ${sources.keys.mkString(", ")}")
134+
135+
// Collect all .class files under classDir
136+
val classFiles = listFilesRecursively(classDir).filter(_.getName.endsWith(".class"))
137+
138+
val jarStream = new JarOutputStream(new FileOutputStream(jarFile))
139+
try {
140+
for (classFile <- classFiles) {
141+
val entryName = classDir.toPath.relativize(classFile.toPath).toString.replace('\\', '/')
142+
jarStream.putNextEntry(new JarEntry(entryName))
143+
val in = new FileInputStream(classFile)
144+
try { in.transferTo(jarStream) } finally { in.close() }
145+
}
146+
} finally {
147+
jarStream.close()
148+
}
149+
150+
SparkFileUtils.deleteRecursively(classDir)
151+
jarFile.toURI.toURL
152+
}
153+
154+
155+
private def listFilesRecursively(dir: File): Seq[File] = {
156+
val children = dir.listFiles()
157+
if (children == null) {
158+
Seq.empty
159+
} else {
160+
children.flatMap { f =>
161+
if (f.isDirectory) { listFilesRecursively(f) } else { Seq(f) }
162+
}.toSeq
163+
}
164+
}
165+
102166
}
103167

104168
private[spark] object SparkTestUtils extends SparkTestUtils
-1.3 KB
Binary file not shown.

core/src/test/scala/org/apache/spark/SparkContextSuite.scala

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -244,23 +244,22 @@ class SparkContextSuite extends SparkFunSuite with LocalSparkContext with Eventu
244244
}
245245

246246
test("add and list jar files") {
247-
val testJar = Thread.currentThread().getContextClassLoader.getResource("TestUDTF.jar")
248-
assume(testJar != null)
247+
val testJar = TestUtils.createJarWithClasses(Seq("SparkContextSuite_AddJar"))
249248
try {
250249
sc = new SparkContext(new SparkConf().setAppName("test").setMaster("local"))
251250
sc.addJar(testJar.toString)
252-
assert(sc.listJars().count(_.contains("TestUDTF.jar")) == 1)
251+
assert(sc.listJars().count(_.contains("testJar")) == 1)
253252
} finally {
254253
sc.stop()
255254
}
256255
}
257256

258257
test("add FS jar files not exists") {
259258
try {
260-
val jarPath = "hdfs:///no/path/to/TestUDTF.jar"
259+
val jarPath = "hdfs:///no/path/to/nonexistent.jar"
261260
sc = new SparkContext(new SparkConf().setAppName("test").setMaster("local"))
262261
sc.addJar(jarPath)
263-
assert(sc.listJars().forall(!_.contains("TestUDTF.jar")))
262+
assert(sc.listJars().forall(!_.contains("nonexistent.jar")))
264263
} finally {
265264
sc.stop()
266265
}
@@ -402,8 +401,7 @@ class SparkContextSuite extends SparkFunSuite with LocalSparkContext with Eventu
402401
case "non-local-mode" => "local-cluster[1,1,1024]"
403402
}
404403
test(s"$method can be called twice with same file in $schedulingMode (SPARK-16787)") {
405-
val testJar = Thread.currentThread().getContextClassLoader.getResource("TestUDTF.jar")
406-
assume(testJar != null)
404+
val testJar = TestUtils.createJarWithClasses(Seq("SparkContextSuite_SPARK16787"))
407405
sc = new SparkContext(master, "test")
408406
val jarPath = testJar.toString
409407
method match {

core/src/test/scala/org/apache/spark/deploy/SparkSubmitSuite.scala

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,10 @@ class SparkSubmitSuite
109109
private val emptyIvySettings = File.createTempFile("ivy", ".xml")
110110
Files.writeString(emptyIvySettings.toPath, "<ivysettings />")
111111

112+
private val testJarPath: String = {
113+
val url = TestUtils.createJarWithClasses(Seq("SparkSubmitSuite_Dummy"))
114+
new File(url.toURI).getAbsolutePath
115+
}
112116
private val submit = new SparkSubmit()
113117

114118
override def beforeEach(): Unit = {
@@ -502,8 +506,7 @@ class SparkSubmitSuite
502506

503507
test("SPARK-47495: Not to add primary resource to jars again" +
504508
" in k8s client mode & driver runs inside a POD") {
505-
val testJar = "src/test/resources/TestUDTF.jar"
506-
assume(new File(testJar).exists)
509+
val testJar = testJarPath
507510
val clArgs = Seq(
508511
"--deploy-mode", "client",
509512
"--proxy-user", "test.user",
@@ -518,12 +521,11 @@ class SparkSubmitSuite
518521
val appArgs = new SparkSubmitArguments(clArgs)
519522
val (_, _, sparkConf, _) = submit.prepareSubmitEnvironment(appArgs)
520523
sparkConf.get("spark.jars").contains("jarToIgnore") shouldBe false
521-
sparkConf.get("spark.jars").contains("TestUDTF") shouldBe true
524+
sparkConf.get("spark.jars").contains("testJar") shouldBe true
522525
}
523526

524527
test("SPARK-33782: handles k8s files download to current directory") {
525-
val testJar = "src/test/resources/TestUDTF.jar"
526-
assume(new File(testJar).exists)
528+
val testJar = testJarPath
527529
val clArgs = Seq(
528530
"--deploy-mode", "client",
529531
"--proxy-user", "test.user",
@@ -548,21 +550,22 @@ class SparkSubmitSuite
548550
conf.get("spark.kubernetes.namespace") should be ("spark")
549551
conf.get("spark.kubernetes.driver.container.image") should be ("bar")
550552

553+
val testJarName = new File(testJar).getName
551554
Files.exists(Paths.get("test_metrics_config.properties")) should be (true)
552555
Files.exists(Paths.get("test_metrics_system.properties")) should be (true)
553556
Files.exists(Paths.get("log4j2.properties")) should be (true)
554-
Files.exists(Paths.get("TestUDTF.jar")) should be (true)
557+
Files.exists(Paths.get(testJarName)) should be (true)
555558
Files.delete(Paths.get("test_metrics_config.properties"))
556559
Files.delete(Paths.get("test_metrics_system.properties"))
557560
Files.delete(Paths.get("log4j2.properties"))
558-
Files.delete(Paths.get("TestUDTF.jar"))
561+
Files.delete(Paths.get(testJarName))
559562
}
560563

561564
test("SPARK-47475: Avoid jars download if scheme matches " +
562565
"spark.kubernetes.jars.avoidDownloadSchemes " +
563566
"in k8s client mode & driver runs inside a POD") {
564-
val testJar = "src/test/resources/TestUDTF.jar"
565-
assume(new File(testJar).exists)
567+
val testJar = testJarPath
568+
val testJarName = new File(testJar).getName
566569
val hadoopConf = new Configuration()
567570
updateConfWithFakeS3Fs(hadoopConf)
568571
withTempDir { tmpDir =>
@@ -588,17 +591,17 @@ class SparkSubmitSuite
588591
val (_, _, conf, _) = submit.prepareSubmitEnvironment(appArgs, Some(hadoopConf))
589592
conf.get("spark.master") should be("k8s://https://host:port")
590593
conf.get("spark.jars").contains(remoteJarFile) shouldBe true
591-
conf.get("spark.jars").contains("TestUDTF") shouldBe true
594+
conf.get("spark.jars").contains(testJarName) shouldBe true
592595

593596
Files.exists(Paths.get("test_metrics_config.properties")) should be(true)
594597
Files.exists(Paths.get("test_metrics_system.properties")) should be(true)
595598
Files.exists(Paths.get("log4j2.properties")) should be(true)
596-
Files.exists(Paths.get("TestUDTF.jar")) should be(true)
599+
Files.exists(Paths.get(testJarName)) should be(true)
597600
Files.exists(Paths.get(notToDownload.getName)) should be(false)
598601
Files.delete(Paths.get("test_metrics_config.properties"))
599602
Files.delete(Paths.get("test_metrics_system.properties"))
600603
Files.delete(Paths.get("log4j2.properties"))
601-
Files.delete(Paths.get("TestUDTF.jar"))
604+
Files.delete(Paths.get(testJarName))
602605
}
603606
}
604607

@@ -1932,7 +1935,7 @@ class SparkSubmitSuite
19321935
TestUtils.createJar(Seq(text1), zipFile1)
19331936
val testFile = "test_metrics_config.properties"
19341937
val testPyFile = "test_metrics_system.properties"
1935-
val testJar = "TestUDTF.jar"
1938+
val testJar = new File(testJarPath).getName
19361939
val clArgs = Seq(
19371940
"--deploy-mode", "client",
19381941
"--proxy-user", "test.user",
@@ -1945,7 +1948,7 @@ class SparkSubmitSuite
19451948
"--conf", "spark.kubernetes.submitInDriver=true",
19461949
"--files", s"src/test/resources/$testFile",
19471950
"--py-files", s"src/test/resources/$testPyFile",
1948-
"--jars", s"src/test/resources/$testJar",
1951+
"--jars", testJarPath,
19491952
"--archives", s"${zipFile1.getAbsolutePath}#test_archives",
19501953
"/home/thejar.jar",
19511954
"arg1")

core/src/test/scala/org/apache/spark/executor/ClassLoaderIsolationSuite.scala

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ class ClassLoaderIsolationSuite extends SparkFunSuite with LocalSparkContext {
3333
.take(2)
3434
.mkString(".")
3535

36-
private val jarURL1 = Thread.currentThread().getContextClassLoader.getResource("TestUDTF.jar")
36+
private val jarURL1 = TestUtils.createJarWithClasses(Seq("ClassLoaderIsolation_Dummy"))
3737
private lazy val jar1 = jarURL1.toString
3838

3939
// package com.example
@@ -51,7 +51,6 @@ class ClassLoaderIsolationSuite extends SparkFunSuite with LocalSparkContext {
5151
private lazy val jar3 = jarURL3.toString
5252

5353
test("Executor classloader isolation with JobArtifactSet") {
54-
assume(jarURL1 != null)
5554
assume(jarURL2 != null)
5655
assume(jarURL3 != null)
5756

core/src/test/scala/org/apache/spark/scheduler/TaskSetManagerSuite.scala

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1573,8 +1573,7 @@ class TaskSetManagerSuite
15731573
}
15741574

15751575
test("SPARK-21563 context's added jars shouldn't change mid-TaskSet") {
1576-
val jarPath = Thread.currentThread().getContextClassLoader.getResource("TestUDTF.jar")
1577-
assume(jarPath != null)
1576+
val jarPath = TestUtils.createJarWithClasses(Seq("TaskSetManagerSuite_SPARK21563"))
15781577
sc = new SparkContext("local", "test")
15791578
val addedJarsPreTaskSet = Map[String, Long](sc.allAddedJars.toSeq: _*)
15801579
assert(addedJarsPreTaskSet.size === 0)

dev/test-jars.txt

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,10 @@
11
core/src/test/resources/TestHelloV2_2.13.jar
22
core/src/test/resources/TestHelloV3_2.13.jar
3-
core/src/test/resources/TestUDTF.jar
43
data/artifact-tests/junitLargeJar.jar
54
data/artifact-tests/smallJar.jar
65
sql/connect/client/jvm/src/test/resources/TestHelloV2_2.13.jar
76
sql/connect/client/jvm/src/test/resources/udf2.13.jar
87
sql/connect/common/src/test/resources/artifact-tests/junitLargeJar.jar
98
sql/connect/common/src/test/resources/artifact-tests/smallJar.jar
10-
sql/core/src/test/resources/SPARK-33084.jar
119
sql/core/src/test/resources/artifact-tests/udf_noA.jar
12-
sql/hive-thriftserver/src/test/resources/TestUDTF.jar
13-
sql/hive/src/test/noclasspath/hive-test-udfs.jar
14-
sql/hive/src/test/resources/SPARK-21101-1.0.jar
15-
sql/hive/src/test/resources/TestUDTF.jar
16-
sql/hive/src/test/resources/data/files/TestSerDe.jar
1710
sql/hive/src/test/resources/regression-test-SPARK-8489/test-2.13.jar
-5.98 KB
Binary file not shown.
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package org.apache.spark.examples.sql;
19+
20+
import org.apache.spark.sql.Row;
21+
import org.apache.spark.sql.expressions.MutableAggregationBuffer;
22+
import org.apache.spark.sql.expressions.UserDefinedAggregateFunction;
23+
import org.apache.spark.sql.types.*;
24+
25+
public class Spark33084 extends UserDefinedAggregateFunction {
26+
public StructType inputSchema() {
27+
return new StructType().add("inputColumn", DataTypes.LongType);
28+
}
29+
30+
public StructType bufferSchema() {
31+
return new StructType()
32+
.add("sum", DataTypes.LongType)
33+
.add("count", DataTypes.LongType);
34+
}
35+
36+
public DataType dataType() {
37+
return DataTypes.DoubleType;
38+
}
39+
40+
public boolean deterministic() {
41+
return true;
42+
}
43+
44+
public void initialize(MutableAggregationBuffer buffer) {
45+
buffer.update(0, 0L);
46+
buffer.update(1, 0L);
47+
}
48+
49+
public void update(MutableAggregationBuffer buffer, Row input) {
50+
if (!input.isNullAt(0)) {
51+
buffer.update(0, buffer.getLong(0) + input.getLong(0));
52+
buffer.update(1, buffer.getLong(1) + 1L);
53+
}
54+
}
55+
56+
public void merge(MutableAggregationBuffer buffer1, Row buffer2) {
57+
buffer1.update(0, buffer1.getLong(0) + buffer2.getLong(0));
58+
buffer1.update(1, buffer1.getLong(1) + buffer2.getLong(1));
59+
}
60+
61+
public Object evaluate(Row buffer) {
62+
return (double) buffer.getLong(0) / (double) buffer.getLong(1);
63+
}
64+
}

sql/core/src/test/scala/org/apache/spark/sql/SQLQuerySuite.scala

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@
1818
package org.apache.spark.sql
1919

2020
import java.io.File
21+
import java.lang.management.ManagementFactory
2122
import java.net.{MalformedURLException, URI}
23+
import java.nio.charset.StandardCharsets
24+
import java.nio.file.{Files, Paths}
2225
import java.sql.{Date, Timestamp}
2326
import java.time.{Duration, Period}
2427
import java.util.Locale
@@ -56,7 +59,7 @@ import org.apache.spark.sql.test.SQLTestData._
5659
import org.apache.spark.sql.types._
5760
import org.apache.spark.tags.ExtendedSQLTest
5861
import org.apache.spark.unsafe.types.{CalendarInterval, UTF8String}
59-
import org.apache.spark.util.{ResetSystemProperties, Utils}
62+
import org.apache.spark.util.{ResetSystemProperties, SparkTestUtils, Utils}
6063

6164
@ExtendedSQLTest
6265
class SQLQuerySuite extends QueryTest with SharedSparkSession with AdaptiveSparkPlanHelper
@@ -3865,19 +3868,25 @@ class SQLQuerySuite extends QueryTest with SharedSparkSession with AdaptiveSpark
38653868
}
38663869

38673870
test("SPARK-33084: Add jar support Ivy URI in SQL -- jar contains udf class") {
3868-
val jarPath = Thread.currentThread().getContextClassLoader
3869-
.getResource("SPARK-33084.jar")
3870-
assume(jarPath != null)
38713871
val sumFuncClass = "org.apache.spark.examples.sql.Spark33084"
3872+
val resourceName = "SPARK-33084/Spark33084.java"
3873+
val sourceUrl = Thread.currentThread().getContextClassLoader
3874+
.getResource(resourceName)
3875+
assert(sourceUrl != null, s"Resource not found: $resourceName")
3876+
val source = Map(sumFuncClass ->
3877+
new String(Files.readAllBytes(Paths.get(sourceUrl.toURI)), StandardCharsets.UTF_8))
3878+
val classpath = ManagementFactory.getRuntimeMXBean.getClassPath
3879+
.split(File.pathSeparator).map(p => new File(p).toURI.toURL).toSeq
3880+
val jarFile = new File(Utils.createTempDir(), "SPARK-33084.jar")
3881+
SparkTestUtils.createJarWithJavaSources(source, jarFile, classpath)
38723882
val functionName = "test_udf"
38733883
withTempDir { dir =>
38743884
System.setProperty("ivy.home", dir.getAbsolutePath)
3875-
val sourceJar = new File(jarPath.getFile)
38763885
val targetCacheJarDir = new File(dir.getAbsolutePath +
38773886
"/local/org.apache.spark/SPARK-33084/1.0/jars/")
38783887
targetCacheJarDir.mkdir()
38793888
// copy jar to local cache
3880-
Utils.copyFileToDirectory(sourceJar, targetCacheJarDir)
3889+
Utils.copyFileToDirectory(jarFile, targetCacheJarDir)
38813890
withTempView("v1") {
38823891
withUserDefinedFunction(
38833892
s"default.$functionName" -> false,

0 commit comments

Comments
 (0)