This is an automated email from the ASF dual-hosted git repository.

voonhous pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/hudi.git


The following commit(s) were added to refs/heads/master by this push:
     new 7b377a51ae1b test(spark): cover low-coverage small classes in spark 
datasource (#19164)
7b377a51ae1b is described below

commit 7b377a51ae1b3727795b73fafae76d7d67d32d90
Author: Y Ethan Guo <[email protected]>
AuthorDate: Fri Aug 28 02:45:33 2026 -0700

    test(spark): cover low-coverage small classes in spark datasource (#19164)
    
    One suite per class, each in the package of the class it covers:
    
    - TestCachingIterator (hudi-spark-common, org.apache.hudi.util): full
      iteration and hasNext idempotency against a counting source iterator.
      The trait has had no in-repo user since #17457 moved the datasource
      read paths onto the file-group reader; removing it is left to a
      separate cleanup.
    - TestHoodieCatalog: HoodieCatalog.convertTransforms for identity
      columns, a single bucket transform, a sorted bucket transform keeping
      its sort columns, and the two rejections (multiple bucket transforms,
      unsupported transform). HoodieSparkCatalogUtils.MatchBucketTransform
      is covered through it, including the sorted_bucket arm.
    - TestBasicStagedTable (added by #19251): commit leaves the catalog
      alone, abort drops the staged table through it.
    - TestHoodieSparkValidateDuplicateKeyRecordMerger: the strategy id
      pinned as the literal, since a table that loads the merger through
      hoodie.write.record.merge.custom.implementation.classes selects it by
      that id and persists it, plus the pre-combining fallback to
      DefaultSparkRecordMerger. The merger was never wired in-repo: from
      #7345 it appeared in production sources only as a TODO in
      ProvidesHoodieConfig, beside the ValidateDuplicateKeyPayload
      selection that #12588 deleted; the class doc now says so instead of
      pointing at that deleted payload.
    - TestProcedureParameterImpl (added by #19161): the factories'
      required/default semantics, toString field presence, a hashCode
      inequality case, and an equals case that isolates required from
      default.
    - TestCliSchemaProvider: the CLI bootstrap SchemaProvider in
      org.apache.hudi.cli (distinct from the Streamer one; named to avoid
      the TestSchemaProvider stubs in hudi-utilities and
      hudi-kafka-connect), shaped like flink's
      TestSchemaProviderCompatibility: legacy provider,
      modern provider whose target falls back to the overridden source
      HoodieSchema, and null handling. The getTargetHoodieSchema catch
      comment now names the two provider shapes that reach it.
    
    Production changes are limited to those two comments; no behavior
    change.
    
    ---------
    
    Co-authored-by: voon <[email protected]>
---
 ...odieSparkValidateDuplicateKeyRecordMerger.scala | 11 ++-
 .../org/apache/hudi/util/TestCachingIterator.scala | 84 ++++++++++++++++++++
 .../sql/hudi/catalog/TestBasicStagedTable.scala    | 14 +++-
 .../spark/sql/hudi/catalog/TestHoodieCatalog.scala | 74 ++++++++++++++++++
 ...odieSparkValidateDuplicateKeyRecordMerger.scala | 37 +++++++++
 .../java/org/apache/hudi/cli/SchemaProvider.java   | 10 ++-
 .../org/apache/hudi/cli/TestCliSchemaProvider.java | 90 ++++++++++++++++++++++
 .../procedure/TestProcedureParameterImpl.scala     | 41 ++++++++--
 8 files changed, 346 insertions(+), 15 deletions(-)

diff --git 
a/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/spark/sql/hudi/command/HoodieSparkValidateDuplicateKeyRecordMerger.scala
 
b/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/spark/sql/hudi/command/HoodieSparkValidateDuplicateKeyRecordMerger.scala
index ec99711704b9..a01010d2cdf6 100644
--- 
a/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/spark/sql/hudi/command/HoodieSparkValidateDuplicateKeyRecordMerger.scala
+++ 
b/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/spark/sql/hudi/command/HoodieSparkValidateDuplicateKeyRecordMerger.scala
@@ -28,9 +28,14 @@ import org.apache.hudi.exception.HoodieDuplicateKeyException
 import 
org.apache.spark.sql.hudi.command.HoodieSparkValidateDuplicateKeyRecordMerger.STRATEGY_ID
 
 /**
- * Validate the duplicate key for insert statement without enable the 
INSERT_DROP_DUPS_OPT
- * config.
- * @see org.apache.spark.sql.hudi.command.ValidateDuplicateKeyPayload
+ * Merger that rejects every merge with a [[HoodieDuplicateKeyException]], for 
INSERT statements that must fail on a
+ * duplicate key rather than drop or update it.
+ *
+ * Never wired in-repo: from its introduction in #7345 it appeared in 
production sources only as a TODO in
+ * `ProvidesHoodieConfig`, beside the `ValidateDuplicateKeyPayload` selection 
that #12588 deleted (the strict-insert
+ * duplicate check lives in `DataSourceUtils.shouldIncludeRecord`). It stays 
loadable through
+ * `HoodieWriteConfig.RECORD_MERGE_IMPL_CLASSES`, where it is selected by 
[[STRATEGY_ID]], which such tables then
+ * persist as their merge strategy id.
  */
 class HoodieSparkValidateDuplicateKeyRecordMerger extends 
HoodieSparkRecordMerger with OperationModeAwareness {
 
diff --git 
a/hudi-spark-datasource/hudi-spark-common/src/test/scala/org/apache/hudi/util/TestCachingIterator.scala
 
b/hudi-spark-datasource/hudi-spark-common/src/test/scala/org/apache/hudi/util/TestCachingIterator.scala
new file mode 100644
index 000000000000..cd9519784549
--- /dev/null
+++ 
b/hudi-spark-datasource/hudi-spark-common/src/test/scala/org/apache/hudi/util/TestCachingIterator.scala
@@ -0,0 +1,84 @@
+/*
+ * 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.
+ */
+
+package org.apache.hudi.util
+
+import org.junit.jupiter.api.Assertions.{assertEquals, assertFalse, assertTrue}
+import org.junit.jupiter.api.Test
+
+import scala.collection.mutable.ArrayBuffer
+
+/**
+ * Tests the [[CachingIterator]] contract: repeated 
[[CachingIterator.hasNext]] calls step the underlying
+ * iterator at most once until [[CachingIterator.next]] consumes the cached 
record.
+ */
+class TestCachingIterator {
+
+  @Test
+  def testYieldsAllElements(): Unit = {
+    val it = new SeqCachingIterator(new CountingIterator(Seq("a", "b", "c")))
+    val collected = ArrayBuffer[String]()
+    while (it.hasNext) {
+      collected += it.next
+    }
+    assertEquals(Seq("a", "b", "c"), collected.toSeq)
+    assertFalse(it.hasNext)
+  }
+
+  @Test
+  def testHasNextIsIdempotent(): Unit = {
+    val source = new CountingIterator(Seq("x", "y"))
+    val it = new SeqCachingIterator(source)
+    // Repeated hasNext without next must not advance the underlying iterator.
+    assertTrue(it.hasNext)
+    assertTrue(it.hasNext)
+    assertTrue(it.hasNext)
+    assertEquals(1, source.advances)
+    assertEquals("x", it.next)
+    assertEquals(1, source.advances)
+    assertTrue(it.hasNext)
+    assertEquals(2, source.advances)
+    assertEquals("y", it.next)
+    assertFalse(it.hasNext)
+    assertEquals(2, source.advances)
+  }
+
+  /** Concrete [[CachingIterator]] backed by a plain iterator, used to 
exercise the trait. */
+  private class SeqCachingIterator(source: Iterator[String]) extends 
CachingIterator[String] {
+    override protected def doHasNext: Boolean = {
+      if (source.hasNext) {
+        nextRecord = source.next()
+        true
+      } else {
+        false
+      }
+    }
+  }
+
+  /** Iterator that counts how many times it has been advanced. */
+  private class CountingIterator(elems: Seq[String]) extends Iterator[String] {
+    private val underlying = elems.iterator
+    var advances: Int = 0
+
+    override def hasNext: Boolean = underlying.hasNext
+
+    override def next(): String = {
+      advances += 1
+      underlying.next()
+    }
+  }
+}
diff --git 
a/hudi-spark-datasource/hudi-spark-common/src/test/scala/org/apache/spark/sql/hudi/catalog/TestBasicStagedTable.scala
 
b/hudi-spark-datasource/hudi-spark-common/src/test/scala/org/apache/spark/sql/hudi/catalog/TestBasicStagedTable.scala
index 7efc70a82abf..27545fab1eca 100644
--- 
a/hudi-spark-datasource/hudi-spark-common/src/test/scala/org/apache/spark/sql/hudi/catalog/TestBasicStagedTable.scala
+++ 
b/hudi-spark-datasource/hudi-spark-common/src/test/scala/org/apache/spark/sql/hudi/catalog/TestBasicStagedTable.scala
@@ -23,12 +23,24 @@ import org.apache.spark.sql.connector.catalog.{Identifier, 
SupportsWrite, Table,
 import org.apache.spark.sql.connector.write.{LogicalWriteInfo, WriteBuilder}
 import org.junit.jupiter.api.Assertions.{assertSame, assertThrows, assertTrue}
 import org.junit.jupiter.api.Test
-import org.mockito.Mockito.{mock, when}
+import org.mockito.Mockito.{mock, verify, verifyNoInteractions, when}
 
 class TestBasicStagedTable {
 
   private val ident = Identifier.of(Array("db"), "tbl")
 
+  @Test
+  def testCommitLeavesCatalogAloneAndAbortDropsTable(): Unit = {
+    val catalog = mock(classOf[TableCatalog])
+    val staged = BasicStagedTable(ident, mock(classOf[Table]), catalog)
+
+    staged.commitStagedChanges()
+    verifyNoInteractions(catalog)
+
+    staged.abortStagedChanges()
+    verify(catalog).dropTable(ident)
+  }
+
   @Test
   def testNewWriteBuilderDelegatesToWritableTable(): Unit = {
     val table = mock(classOf[SupportsWrite])
diff --git 
a/hudi-spark-datasource/hudi-spark-common/src/test/scala/org/apache/spark/sql/hudi/catalog/TestHoodieCatalog.scala
 
b/hudi-spark-datasource/hudi-spark-common/src/test/scala/org/apache/spark/sql/hudi/catalog/TestHoodieCatalog.scala
new file mode 100644
index 000000000000..845ee6f7d040
--- /dev/null
+++ 
b/hudi-spark-datasource/hudi-spark-common/src/test/scala/org/apache/spark/sql/hudi/catalog/TestHoodieCatalog.scala
@@ -0,0 +1,74 @@
+/*
+ * 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.
+ */
+
+package org.apache.spark.sql.hudi.catalog
+
+import org.apache.hudi.exception.HoodieException
+
+import org.apache.spark.sql.catalyst.catalog.BucketSpec
+import org.apache.spark.sql.connector.expressions.{Expressions, 
LogicalExpressions, Transform}
+import org.junit.jupiter.api.Assertions.{assertEquals, assertThrows, 
assertTrue}
+import org.junit.jupiter.api.Test
+
+/**
+ * Tests [[HoodieCatalog.convertTransforms]], which maps the V2 partition 
transforms of a CREATE TABLE into
+ * identity partition columns plus an optional bucket spec.
+ * [[org.apache.spark.sql.HoodieSparkCatalogUtils.MatchBucketTransform]] is 
exercised through it, including
+ * the sorted-bucket arm.
+ */
+class TestHoodieCatalog {
+
+  @Test
+  def testIdentityTransformsBecomePartitionColumns(): Unit = {
+    val (partitionCols, bucketSpec) = HoodieCatalog.convertTransforms(
+      Seq(Expressions.identity("dt"), Expressions.identity("region")))
+    assertEquals(Seq("dt", "region"), partitionCols)
+    assertTrue(bucketSpec.isEmpty)
+  }
+
+  @Test
+  def testBucketTransformBecomesBucketSpec(): Unit = {
+    val (partitionCols, bucketSpec) = HoodieCatalog.convertTransforms(
+      Seq(Expressions.identity("dt"), Expressions.bucket(8, "id")))
+    assertEquals(Seq("dt"), partitionCols)
+    assertEquals(Some(BucketSpec(8, Seq("id"), Nil)), bucketSpec)
+  }
+
+  @Test
+  def testSortedBucketTransformKeepsSortColumns(): Unit = {
+    val sortedBucket: Transform = LogicalExpressions.bucket(
+      4, Array(Expressions.column("id")), Array(Expressions.column("ts")))
+    val (partitionCols, bucketSpec) = 
HoodieCatalog.convertTransforms(Seq(sortedBucket))
+    assertTrue(partitionCols.isEmpty)
+    assertEquals(Some(BucketSpec(4, Seq("id"), Seq("ts"))), bucketSpec)
+  }
+
+  @Test
+  def testMultipleBucketTransformsAreRejected(): Unit = {
+    val ex = assertThrows(classOf[HoodieException], () => 
HoodieCatalog.convertTransforms(
+      Seq(Expressions.bucket(8, "id"), Expressions.bucket(4, "name"))))
+    assertTrue(ex.getMessage.contains("Multiple bucket transformations are not 
supported"))
+  }
+
+  @Test
+  def testUnsupportedTransformIsRejected(): Unit = {
+    val ex = assertThrows(classOf[HoodieException], () => 
HoodieCatalog.convertTransforms(
+      Seq(Expressions.years("ts"))))
+    assertTrue(ex.getMessage.startsWith("Partitioning by transformation"), 
ex.getMessage)
+    assertTrue(ex.getMessage.contains("years"), ex.getMessage)
+  }
+}
diff --git 
a/hudi-spark-datasource/hudi-spark-common/src/test/scala/org/apache/spark/sql/hudi/command/TestHoodieSparkValidateDuplicateKeyRecordMerger.scala
 
b/hudi-spark-datasource/hudi-spark-common/src/test/scala/org/apache/spark/sql/hudi/command/TestHoodieSparkValidateDuplicateKeyRecordMerger.scala
new file mode 100644
index 000000000000..ff17f45dda27
--- /dev/null
+++ 
b/hudi-spark-datasource/hudi-spark-common/src/test/scala/org/apache/spark/sql/hudi/command/TestHoodieSparkValidateDuplicateKeyRecordMerger.scala
@@ -0,0 +1,37 @@
+/*
+ * 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.
+ */
+
+package org.apache.spark.sql.hudi.command
+
+import org.apache.hudi.DefaultSparkRecordMerger
+
+import org.junit.jupiter.api.Assertions.{assertEquals, assertTrue}
+import org.junit.jupiter.api.Test
+
+class TestHoodieSparkValidateDuplicateKeyRecordMerger {
+
+  @Test
+  def testMergingStrategyAndPreCombiningMode(): Unit = {
+    val merger = new HoodieSparkValidateDuplicateKeyRecordMerger
+    // No in-repo code path has ever instantiated this merger (see the class 
doc), but a table may load it through
+    // HoodieWriteConfig.RECORD_MERGE_IMPL_CLASSES; the id then selects the 
merger and is persisted as the table's
+    // merge strategy id, so pin the literal: a change to the constant would 
break those tables.
+    assertEquals("fb092649-0fdc-4c14-9113-acde3034a6c4", 
merger.getMergingStrategy)
+    // Pre-combining falls back to the default Spark record merger.
+    
assertTrue(merger.asPreCombiningMode().isInstanceOf[DefaultSparkRecordMerger])
+  }
+}
diff --git 
a/hudi-spark-datasource/hudi-spark/src/main/java/org/apache/hudi/cli/SchemaProvider.java
 
b/hudi-spark-datasource/hudi-spark/src/main/java/org/apache/hudi/cli/SchemaProvider.java
index dd81229aeba6..335480fb04d3 100644
--- 
a/hudi-spark-datasource/hudi-spark/src/main/java/org/apache/hudi/cli/SchemaProvider.java
+++ 
b/hudi-spark-datasource/hudi-spark/src/main/java/org/apache/hudi/cli/SchemaProvider.java
@@ -30,8 +30,9 @@ import org.apache.spark.api.java.JavaSparkContext;
 import java.io.Serializable;
 
 /**
- * Class to provide schema for reading data and also writing into a Hoodie 
table,
- * used by Hudi Streamer (runs over Spark).
+ * Class to provide the schema for the CLI bootstrap path ({@link 
BootstrapExecutorUtils}, which loads the
+ * configured implementation by class name); distinct from the Hudi Streamer 
provider in
+ * {@code org.apache.hudi.utilities.schema}.
  */
 @PublicAPIClass(maturity = ApiMaturityLevel.STABLE)
 public abstract class SchemaProvider implements Serializable {
@@ -73,8 +74,9 @@ public abstract class SchemaProvider implements Serializable {
       Schema schema = getTargetSchema();
       return schema == null ? null : HoodieSchema.fromAvroSchema(schema);
     } catch (UnsupportedOperationException e) {
-      // If the legacy getTargetSchema() calls getSourceSchema() which is not 
implemented,
-      // fall back to using getSourceHoodieSchema as target schema
+      // Reached by a provider that overrides getSourceHoodieSchema() and 
leaves the deprecated
+      // getSourceSchema() (hence the default getTargetSchema()) throwing, or 
that overrides
+      // getTargetSchema() to throw; either way the source HoodieSchema is the 
target.
       return getSourceHoodieSchema();
     }
   }
diff --git 
a/hudi-spark-datasource/hudi-spark/src/test/java/org/apache/hudi/cli/TestCliSchemaProvider.java
 
b/hudi-spark-datasource/hudi-spark/src/test/java/org/apache/hudi/cli/TestCliSchemaProvider.java
new file mode 100644
index 000000000000..a9bfc3a2e879
--- /dev/null
+++ 
b/hudi-spark-datasource/hudi-spark/src/test/java/org/apache/hudi/cli/TestCliSchemaProvider.java
@@ -0,0 +1,90 @@
+/*
+ * 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.
+ */
+
+package org.apache.hudi.cli;
+
+import org.apache.hudi.common.config.TypedProperties;
+import org.apache.hudi.common.schema.HoodieSchema;
+
+import org.apache.avro.Schema;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+
+/**
+ * Tests the CLI bootstrap {@link SchemaProvider} ({@code 
org.apache.hudi.cli}), which is distinct from the Hudi
+ * Streamer one ({@code org.apache.hudi.utilities.schema.SchemaProvider}). Its 
only in-repo consumer is
+ * {@link BootstrapExecutorUtils}, which calls the legacy {@link 
SchemaProvider#getTargetSchema()}; the
+ * {@link HoodieSchema} accessors and the target fallback are public API for 
out-of-tree providers and are pinned
+ * here directly.
+ */
+public class TestCliSchemaProvider {
+
+  private static final Schema AVRO_SCHEMA = new Schema.Parser().parse(
+      
"{\"type\":\"record\",\"name\":\"r\",\"fields\":[{\"name\":\"id\",\"type\":\"int\"}]}");
+
+  @Test
+  @SuppressWarnings("deprecation")
+  void testLegacyProviderConvertsSourceAndTargetSchemas() {
+    SchemaProvider provider = new SchemaProvider(new TypedProperties()) {
+      @Override
+      public Schema getSourceSchema() {
+        return AVRO_SCHEMA;
+      }
+    };
+
+    // The target schema defaults to the source schema, and both are wrapped 
into HoodieSchema.
+    assertSame(AVRO_SCHEMA, provider.getTargetSchema());
+    assertEquals(HoodieSchema.fromAvroSchema(AVRO_SCHEMA), 
provider.getSourceHoodieSchema());
+    assertEquals(HoodieSchema.fromAvroSchema(AVRO_SCHEMA), 
provider.getTargetHoodieSchema());
+  }
+
+  @Test
+  void testModernProviderFallsBackToSourceHoodieSchemaForTarget() {
+    HoodieSchema schema = HoodieSchema.fromAvroSchema(AVRO_SCHEMA);
+    SchemaProvider provider = new SchemaProvider(new TypedProperties()) {
+      @Override
+      public Schema getSourceSchema() {
+        throw new UnsupportedOperationException("legacy accessor not 
implemented");
+      }
+
+      @Override
+      public HoodieSchema getSourceHoodieSchema() {
+        return schema;
+      }
+    };
+
+    // The default getTargetSchema() delegates to the throwing 
getSourceSchema(), so the target falls back to
+    // the overridden source HoodieSchema.
+    assertSame(schema, provider.getTargetHoodieSchema());
+  }
+
+  @Test
+  void testNullSourceSchemaYieldsNullHoodieSchemas() {
+    SchemaProvider provider = new SchemaProvider(new TypedProperties()) {
+      @Override
+      public Schema getSourceSchema() {
+        return null;
+      }
+    };
+
+    assertNull(provider.getSourceHoodieSchema());
+    assertNull(provider.getTargetHoodieSchema());
+  }
+}
diff --git 
a/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/spark/sql/hudi/procedure/TestProcedureParameterImpl.scala
 
b/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/spark/sql/hudi/procedure/TestProcedureParameterImpl.scala
index fac0db989444..79004efd392d 100644
--- 
a/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/spark/sql/hudi/procedure/TestProcedureParameterImpl.scala
+++ 
b/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/spark/sql/hudi/procedure/TestProcedureParameterImpl.scala
@@ -17,19 +17,33 @@
 
 package org.apache.spark.sql.hudi.procedure
 
-import org.apache.spark.sql.hudi.command.procedures.ProcedureParameter
+import org.apache.spark.sql.hudi.command.procedures.{ProcedureParameter, 
ProcedureParameterImpl}
 import org.apache.spark.sql.types.DataTypes
-import org.junit.jupiter.api.Assertions.{assertEquals, assertFalse, 
assertNotEquals, assertTrue}
+import org.junit.jupiter.api.Assertions.{assertEquals, assertFalse, 
assertNotEquals, assertNull, assertTrue}
 import org.scalatest.funsuite.AnyFunSuite
 
 /**
- * Unit tests for ProcedureParameterImpl.equals. The implementation used to 
call this == other for
- * the identity check, which dispatches straight back into equals and recurses 
until it overflows
- * the stack, and it cast the argument before the null/type guard, so a 
foreign argument threw
- * ClassCastException. Both are self-contained checks, so no Spark session is 
needed.
+ * Unit tests for ProcedureParameterImpl: the ProcedureParameter factories, 
equals, hashCode and
+ * toString. equals used to call this == other for the identity check, which 
dispatches straight
+ * back into equals and recurses until it overflows the stack, and it cast the 
argument before the
+ * null/type guard, so a foreign argument threw ClassCastException. All checks 
are self-contained,
+ * so no Spark session is needed.
  */
 class TestProcedureParameterImpl extends AnyFunSuite {
 
+  test("Test ProcedureParameter factories set required and default") {
+    val required = ProcedureParameter.required(0, "path", DataTypes.StringType)
+    assertTrue(required.required)
+    assertNull(required.default)
+
+    val optional = ProcedureParameter.optional(2, "limit", 
DataTypes.IntegerType, 10)
+    assertFalse(optional.required)
+    assertEquals(10, optional.default)
+
+    // The default value itself defaults to null.
+    assertNull(ProcedureParameter.optional(3, "filter", 
DataTypes.StringType).default)
+  }
+
   test("Test ProcedureParameterImpl equals identity, foreign types and null") {
     val param = ProcedureParameter.optional(0, "table", DataTypes.StringType, 
"default")
 
@@ -58,6 +72,19 @@ class TestProcedureParameterImpl extends AnyFunSuite {
     assertNotEquals(optional, ProcedureParameter.optional(1, "backup", 
DataTypes.BooleanType, true))
     assertNotEquals(optional, ProcedureParameter.optional(1, "dry_run", 
DataTypes.StringType, true))
     assertNotEquals(optional, ProcedureParameter.optional(1, "dry_run", 
DataTypes.BooleanType, false))
-    assertNotEquals(optional, ProcedureParameter.required(1, "dry_run", 
DataTypes.BooleanType))
+    // Built directly so that only `required` differs; the required(...) 
factory would also null the default.
+    assertNotEquals(optional, ProcedureParameterImpl(1, "dry_run", 
DataTypes.BooleanType, true, required = true))
+
+    // hashCode is derived from the same fields, so a differing index also 
changes the hash;
+    // without this a constant hashCode would satisfy the equal-hash 
assertions above.
+    assertNotEquals(optional.hashCode(),
+      ProcedureParameter.optional(2, "dry_run", DataTypes.BooleanType, 
true).hashCode())
+  }
+
+  test("Test ProcedureParameterImpl toString includes every field") {
+    val rendered = ProcedureParameter.optional(1, "col", DataTypes.StringType, 
"def").toString
+    Seq("index='1'", "name='col'", "type=StringType", "required=false", 
"default=def").foreach { field =>
+      assertTrue(rendered.contains(field), s"missing $field in $rendered")
+    }
   }
 }

Reply via email to