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

danny0405 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 92f9640509b0 test(flink): improve coverage for utility classes (#19546)
92f9640509b0 is described below

commit 92f9640509b0c4043a6daca3c1127170cced672a
Author: Shuo Cheng <[email protected]>
AuthorDate: Tue Aug 11 10:43:20 2026 +0800

    test(flink): improve coverage for utility classes (#19546)
    
    * test(flink): improve coverage for utility classes
---
 .../schema/TestSchemaProviderCompatibility.java    |  79 +++++++++++
 .../aggregate/TestBootstrapAggregation.java        |  58 ++++++++
 .../sink/buffer/TestBufferAccountingUtilities.java | 103 ++++++++++++++
 .../sink/clustering/TestClusteringEventModels.java |  63 +++++++++
 .../hudi/sink/common/TestWriteOperatorFactory.java |  43 ++++++
 .../sink/compact/TestCompactionEventModels.java    |  64 +++++++++
 .../sink/event/TestCorrespondentEventModels.java   |  59 ++++++++
 .../TestMemoryPagesExhaustedException.java         |  40 ++++++
 .../muttley/TestFlinkHudiMuttleyExceptions.java    |  58 ++++++++
 .../sink/overwrite/TestPartitionOverwriteMode.java |  35 +++++
 .../sink/partitioner/index/TestIndexBackends.java  |  56 ++++++++
 .../sink/partitioner/index/TestIndexRowUtils.java  | 115 +++++++++++++++
 .../sink/transform/TestTransformUtilities.java     | 106 ++++++++++++++
 .../apache/hudi/sink/utils/TestSinkUtilities.java  | 132 +++++++++++++++++
 .../enumerator/TestHoodieEnumeratorPosition.java   |  54 +++++++
 .../hudi/source/prune/TestPrimaryKeyPruners.java   |  75 ++++++++++
 .../source/rebalance/TestStreamReadRebalance.java  |  96 +++++++++++++
 .../source/split/TestSplitRequestEventModel.java   |  47 +++++++
 .../hudi/source/stats/TestColumnStatsModels.java   |  59 ++++++++
 .../format/TestFormatIteratorsAndIOFactory.java    | 105 ++++++++++++++
 .../format/mor/TestMergeOnReadTableState.java      |  68 +++++++++
 .../hudi/table/lookup/TestLookupUtilities.java     |  67 +++++++++
 .../org/apache/hudi/util/TestFlinkUtilities.java   | 156 +++++++++++++++++++++
 23 files changed, 1738 insertions(+)

diff --git 
a/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/schema/TestSchemaProviderCompatibility.java
 
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/schema/TestSchemaProviderCompatibility.java
new file mode 100644
index 000000000000..ac010b2440ee
--- /dev/null
+++ 
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/schema/TestSchemaProviderCompatibility.java
@@ -0,0 +1,79 @@
+/*
+ * 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.schema;
+
+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;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+class TestSchemaProviderCompatibility {
+
+  private static final Schema AVRO_SCHEMA = new Schema.Parser().parse(
+      
"{\"type\":\"record\",\"name\":\"record\",\"fields\":[{\"name\":\"id\",\"type\":\"string\"}]}");
+
+  @Test
+  @SuppressWarnings("deprecation")
+  void testLegacyProviderConvertsSourceAndTargetSchemas() {
+    SchemaProvider provider = new SchemaProvider() {
+      @Override
+      public Schema getSourceSchema() {
+        return AVRO_SCHEMA;
+      }
+    };
+
+    assertEquals(HoodieSchema.fromAvroSchema(AVRO_SCHEMA), 
provider.getSourceHoodieSchema());
+    assertEquals(HoodieSchema.fromAvroSchema(AVRO_SCHEMA), 
provider.getTargetHoodieSchema());
+    assertSame(AVRO_SCHEMA, provider.getTargetSchema());
+  }
+
+  @Test
+  void testModernProviderFallsBackToSourceHoodieSchemaForTarget() {
+    HoodieSchema schema = HoodieSchema.fromAvroSchema(AVRO_SCHEMA);
+    SchemaProvider provider = new SchemaProvider() {
+      @Override
+      public HoodieSchema getSourceHoodieSchema() {
+        return schema;
+      }
+    };
+
+    assertSame(schema, provider.getTargetHoodieSchema());
+  }
+
+  @Test
+  @SuppressWarnings("deprecation")
+  void testNullLegacySchemaAndUnsupportedDefault() {
+    SchemaProvider nullProvider = new SchemaProvider() {
+      @Override
+      public Schema getSourceSchema() {
+        return null;
+      }
+    };
+    SchemaProvider defaultProvider = new SchemaProvider() { };
+
+    assertNull(nullProvider.getSourceHoodieSchema());
+    assertNull(nullProvider.getTargetHoodieSchema());
+    assertThrows(UnsupportedOperationException.class, 
defaultProvider::getSourceSchema);
+  }
+}
diff --git 
a/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/sink/bootstrap/aggregate/TestBootstrapAggregation.java
 
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/sink/bootstrap/aggregate/TestBootstrapAggregation.java
new file mode 100644
index 000000000000..8a4caa473111
--- /dev/null
+++ 
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/sink/bootstrap/aggregate/TestBootstrapAggregation.java
@@ -0,0 +1,58 @@
+/*
+ * 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.sink.bootstrap.aggregate;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertSame;
+
+class TestBootstrapAggregation {
+
+  @Test
+  void testAccumulatorCountsDistinctTasksAndMerges() {
+    BootstrapAccumulator first = new BootstrapAccumulator();
+    first.update(0);
+    first.update(0);
+    first.update(1);
+    assertEquals(2, first.readyTaskNum());
+    assertSame(first, first.merge(null));
+
+    BootstrapAccumulator second = new BootstrapAccumulator();
+    second.update(1);
+    second.update(2);
+    assertSame(first, first.merge(second));
+    assertEquals(3, first.readyTaskNum());
+  }
+
+  @Test
+  void testAggregateFunctionLifecycle() {
+    BootstrapAggFunction function = new BootstrapAggFunction();
+    BootstrapAccumulator first = function.createAccumulator();
+    BootstrapAccumulator second = function.createAccumulator();
+
+    assertSame(first, function.add(3, first));
+    function.add(3, first);
+    function.add(4, second);
+    assertEquals(1, function.getResult(first));
+    assertSame(first, function.merge(first, second));
+    assertEquals(2, function.getResult(first));
+    assertEquals("BootstrapAggFunction", BootstrapAggFunction.NAME);
+  }
+}
diff --git 
a/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/sink/buffer/TestBufferAccountingUtilities.java
 
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/sink/buffer/TestBufferAccountingUtilities.java
new file mode 100644
index 000000000000..cb3b36d5df92
--- /dev/null
+++ 
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/sink/buffer/TestBufferAccountingUtilities.java
@@ -0,0 +1,103 @@
+/*
+ * 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.sink.buffer;
+
+import org.apache.hudi.configuration.FlinkOptions;
+
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.core.memory.MemorySegmentFactory;
+import org.apache.flink.table.data.binary.BinaryRowData;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class TestBufferAccountingUtilities {
+
+  @Test
+  void testTotalSizeTracerTracksCountdownAndReset() {
+    Configuration conf = new Configuration();
+    conf.set(FlinkOptions.WRITE_TASK_MAX_SIZE, 103D);
+    conf.set(FlinkOptions.WRITE_MERGE_MAX_MEMORY, 1);
+    TotalSizeTracer tracer = new TotalSizeTracer(conf);
+    long oneMb = 1024L * 1024L;
+
+    assertEquals(2 * oneMb, tracer.maxBufferSize);
+    assertFalse(tracer.trace(oneMb));
+    assertFalse(tracer.trace(oneMb));
+    assertTrue(tracer.trace(1));
+    tracer.countDown(oneMb + 1);
+    assertEquals(oneMb, tracer.bufferSize);
+    tracer.reset();
+    assertEquals(0, tracer.bufferSize);
+  }
+
+  @Test
+  void testTotalSizeTracerRejectsInsufficientTaskMemory() {
+    Configuration conf = new Configuration();
+    conf.set(FlinkOptions.WRITE_TASK_MAX_SIZE, 101D);
+    conf.set(FlinkOptions.WRITE_MERGE_MAX_MEMORY, 1);
+
+    IllegalStateException exception = 
assertThrows(IllegalStateException.class, () -> new TotalSizeTracer(conf));
+    
assertTrue(exception.getMessage().contains(FlinkOptions.WRITE_TASK_MAX_SIZE.key()));
+  }
+
+  @Test
+  void testBufferSizeDetectorUsesBinaryRowSizeAndResets() {
+    BufferSizeDetector detector = new BufferSizeDetector(0.00001);
+    BinaryRowData smallRow = new BinaryRowData(0);
+    smallRow.pointTo(MemorySegmentFactory.wrap(new byte[8]), 0, 8);
+
+    assertFalse(detector.detect(smallRow));
+    assertEquals(8, detector.getLastRecordSize());
+    assertTrue(detector.detect(smallRow));
+    assertTrue(detector.isFull());
+
+    detector.reset();
+    assertEquals(-1, detector.getLastRecordSize());
+    assertEquals(0, detector.totalSize);
+    assertFalse(detector.isFull());
+  }
+
+  @Test
+  void testBufferSizeDetectorReusesLastSampleForRegularObjects() {
+    BufferSizeDetector detector = new BufferSizeDetector(10);
+    assertFalse(detector.detect("first value"));
+    long sampledSize = detector.getLastRecordSize();
+
+    assertTrue(sampledSize > 0);
+    assertFalse(detector.detect("second value"));
+    assertEquals(sampledSize * 2, detector.totalSize);
+
+    // The seeded random makes sampling deterministic and covers both outcomes.
+    boolean sampled = false;
+    boolean skipped = false;
+    for (int i = 0; i < 500 && !(sampled && skipped); i++) {
+      if (detector.sampling()) {
+        sampled = true;
+      } else {
+        skipped = true;
+      }
+    }
+    assertTrue(sampled);
+    assertTrue(skipped);
+  }
+}
diff --git 
a/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/sink/clustering/TestClusteringEventModels.java
 
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/sink/clustering/TestClusteringEventModels.java
new file mode 100644
index 000000000000..62d0209da75f
--- /dev/null
+++ 
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/sink/clustering/TestClusteringEventModels.java
@@ -0,0 +1,63 @@
+/*
+ * 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.sink.clustering;
+
+import org.apache.hudi.client.WriteStatus;
+import org.apache.hudi.common.model.ClusteringGroupInfo;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.Collections;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mock;
+
+class TestClusteringEventModels {
+
+  @Test
+  void testPlanEventAccessors() {
+    ClusteringGroupInfo group = mock(ClusteringGroupInfo.class);
+    Map<String, String> params = Collections.singletonMap("strategy", "sort");
+    ClusteringPlanEvent event = new ClusteringPlanEvent("001", group, params);
+    event.setIndex(7);
+
+    assertEquals("001", event.getClusteringInstantTime());
+    assertSame(group, event.getClusteringGroupInfo());
+    assertSame(params, event.getStrategyParams());
+    assertEquals(7, event.getIndex());
+  }
+
+  @Test
+  void testCommitEventDistinguishesSuccessAndFailure() {
+    assertTrue(new ClusteringCommitEvent("001", "file-1", 1).isFailed());
+
+    WriteStatus status = mock(WriteStatus.class);
+    ClusteringCommitEvent success = new ClusteringCommitEvent(
+        "002", "file-2", Collections.singletonList(status), 2);
+    assertFalse(success.isFailed());
+    assertEquals("002", success.getInstant());
+    assertEquals("file-2", success.getFileIds());
+    assertSame(status, success.getWriteStatuses().get(0));
+    assertEquals(2, success.getTaskID());
+  }
+}
diff --git 
a/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/sink/common/TestWriteOperatorFactory.java
 
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/sink/common/TestWriteOperatorFactory.java
new file mode 100644
index 000000000000..635393c00143
--- /dev/null
+++ 
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/sink/common/TestWriteOperatorFactory.java
@@ -0,0 +1,43 @@
+/*
+ * 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.sink.common;
+
+import org.apache.hudi.sink.StreamWriteOperatorCoordinator;
+
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.runtime.jobgraph.OperatorID;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.mockito.Mockito.mock;
+
+class TestWriteOperatorFactory {
+
+  @Test
+  @SuppressWarnings("unchecked")
+  void testFactoryAndCoordinatorProvider() {
+    WriteOperatorFactory<String> factory = WriteOperatorFactory.instance(
+        new Configuration(), mock(AbstractWriteOperator.class));
+
+    assertNotNull(factory);
+    assertInstanceOf(StreamWriteOperatorCoordinator.Provider.class,
+        factory.getCoordinatorProvider("operator", new OperatorID()));
+  }
+}
diff --git 
a/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/sink/compact/TestCompactionEventModels.java
 
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/sink/compact/TestCompactionEventModels.java
new file mode 100644
index 000000000000..87149b39d90b
--- /dev/null
+++ 
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/sink/compact/TestCompactionEventModels.java
@@ -0,0 +1,64 @@
+/*
+ * 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.sink.compact;
+
+import org.apache.hudi.client.WriteStatus;
+import org.apache.hudi.common.model.CompactionOperation;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.Collections;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mock;
+
+class TestCompactionEventModels {
+
+  @Test
+  void testPlanEventConstructorsAndAccessors() {
+    CompactionOperation operation = mock(CompactionOperation.class);
+    CompactionPlanEvent event = new CompactionPlanEvent("001", operation, 3, 
true, true);
+
+    assertEquals("001", event.getCompactionInstantTime());
+    assertSame(operation, event.getOperation());
+    assertEquals(3, event.getIndex());
+    assertTrue(event.isMetadataTable());
+    assertTrue(event.isLogCompaction());
+  }
+
+  @Test
+  void testCommitEventDistinguishesSuccessAndFailure() {
+    CompactionCommitEvent failed = new CompactionCommitEvent("001", "file-1", 
2, false, true);
+    assertTrue(failed.isFailed());
+    assertTrue(failed.isLogCompaction());
+
+    WriteStatus status = mock(WriteStatus.class);
+    CompactionCommitEvent success = new CompactionCommitEvent(
+        "002", "file-2", Collections.singletonList(status), 4, true, false);
+    assertFalse(success.isFailed());
+    assertEquals("002", success.getInstant());
+    assertEquals("file-2", success.getFileId());
+    assertSame(status, success.getWriteStatuses().get(0));
+    assertEquals(4, success.getTaskID());
+    assertTrue(success.isMetadataTable());
+  }
+}
diff --git 
a/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/sink/event/TestCorrespondentEventModels.java
 
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/sink/event/TestCorrespondentEventModels.java
new file mode 100644
index 000000000000..8dc7102aad25
--- /dev/null
+++ 
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/sink/event/TestCorrespondentEventModels.java
@@ -0,0 +1,59 @@
+/*
+ * 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.sink.event;
+
+import org.apache.flink.runtime.jobgraph.OperatorID;
+import org.apache.flink.runtime.jobgraph.tasks.TaskOperatorEventGateway;
+import org.junit.jupiter.api.Test;
+
+import java.util.HashMap;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.mockito.Mockito.mock;
+
+class TestCorrespondentEventModels {
+
+  @Test
+  void testCorrespondentFactoryAndRequestResponseModels() {
+    OperatorID operatorId = new OperatorID();
+    TaskOperatorEventGateway gateway = mock(TaskOperatorEventGateway.class);
+    Correspondent correspondent = Correspondent.getInstance(operatorId, 
gateway);
+    assertSame(operatorId, correspondent.getOperatorID());
+    assertSame(gateway, correspondent.getGateway());
+
+    assertEquals(9L, 
Correspondent.InstantTimeRequest.getInstance(9L).getCheckpointId());
+    assertEquals("001", 
Correspondent.InstantTimeResponse.getInstance("001").getInstant());
+    assertNotNull(Correspondent.InflightInstantsRequest.getInstance());
+
+    HashMap<Long, String> instants = new HashMap<>();
+    instants.put(9L, "001");
+    assertSame(instants,
+        
Correspondent.InflightInstantsResponse.getInstance(instants).getInflightInstants());
+  }
+
+  @Test
+  void testCommitAckFactoryAndAccessors() {
+    CommitAckEvent event = CommitAckEvent.getInstance(11L);
+    assertEquals(11L, event.getCheckpointId());
+    event.setCheckpointId(12L);
+    assertEquals(12L, event.getCheckpointId());
+  }
+}
diff --git 
a/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/sink/exception/TestMemoryPagesExhaustedException.java
 
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/sink/exception/TestMemoryPagesExhaustedException.java
new file mode 100644
index 000000000000..a0776d8396e7
--- /dev/null
+++ 
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/sink/exception/TestMemoryPagesExhaustedException.java
@@ -0,0 +1,40 @@
+/*
+ * 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.sink.exception;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertSame;
+
+class TestMemoryPagesExhaustedException {
+
+  @Test
+  void testConstructorsPreserveMessageAndCause() {
+    RuntimeException cause = new RuntimeException("root cause");
+    MemoryPagesExhaustedException withCause =
+        new MemoryPagesExhaustedException("no pages", cause);
+    MemoryPagesExhaustedException withoutCause =
+        new MemoryPagesExhaustedException("still no pages");
+
+    assertEquals("no pages", withCause.getMessage());
+    assertSame(cause, withCause.getCause());
+    assertEquals("still no pages", withoutCause.getMessage());
+  }
+}
diff --git 
a/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/sink/muttley/TestFlinkHudiMuttleyExceptions.java
 
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/sink/muttley/TestFlinkHudiMuttleyExceptions.java
new file mode 100644
index 000000000000..0fbfbdcc6b69
--- /dev/null
+++ 
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/sink/muttley/TestFlinkHudiMuttleyExceptions.java
@@ -0,0 +1,58 @@
+/*
+ * 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.sink.muttley;
+
+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.assertThrows;
+
+class TestFlinkHudiMuttleyExceptions {
+
+  @Test
+  void testBaseExceptionConstructorsAndValidation() {
+    FlinkHudiMuttleyException withoutMessage = new 
FlinkHudiMuttleyException(418);
+    FlinkHudiMuttleyException withMessage = new 
FlinkHudiMuttleyException("teapot", 418);
+
+    assertNull(withoutMessage.getMessage());
+    assertEquals(418, withoutMessage.getStatusCode());
+    assertEquals("teapot", withMessage.getMessage());
+    assertEquals(418, FlinkHudiMuttleyException.validate(418, code -> code >= 
400));
+    assertThrows(IllegalArgumentException.class,
+        () -> FlinkHudiMuttleyException.validate(399, code -> code >= 400));
+  }
+
+  @Test
+  void testClientAndServerExceptionConstructors() {
+    FlinkHudiMuttleyClientException client = new 
FlinkHudiMuttleyClientException(404);
+    FlinkHudiMuttleyClientException customClient =
+        new FlinkHudiMuttleyClientException("missing", 404);
+    FlinkHudiMuttleyServerException server = new 
FlinkHudiMuttleyServerException(503);
+    FlinkHudiMuttleyServerException customServer =
+        new FlinkHudiMuttleyServerException("unavailable", 503);
+
+    assertEquals("Muttley client error with status code: 404", 
client.getMessage());
+    assertEquals("missing", customClient.getMessage());
+    assertEquals(404, customClient.getStatusCode());
+    assertEquals("Muttley server error with status code: 503", 
server.getMessage());
+    assertEquals("unavailable", customServer.getMessage());
+    assertEquals(503, customServer.getStatusCode());
+  }
+}
diff --git 
a/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/sink/overwrite/TestPartitionOverwriteMode.java
 
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/sink/overwrite/TestPartitionOverwriteMode.java
new file mode 100644
index 000000000000..c09a623ea144
--- /dev/null
+++ 
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/sink/overwrite/TestPartitionOverwriteMode.java
@@ -0,0 +1,35 @@
+/*
+ * 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.sink.overwrite;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class TestPartitionOverwriteMode {
+
+  @Test
+  void testEnumValues() {
+    assertArrayEquals(
+        new PartitionOverwriteMode[] {PartitionOverwriteMode.STATIC, 
PartitionOverwriteMode.DYNAMIC},
+        PartitionOverwriteMode.values());
+    assertEquals(PartitionOverwriteMode.DYNAMIC, 
PartitionOverwriteMode.valueOf("DYNAMIC"));
+  }
+}
diff --git 
a/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/sink/partitioner/index/TestIndexBackends.java
 
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/sink/partitioner/index/TestIndexBackends.java
new file mode 100644
index 000000000000..85d6feed642c
--- /dev/null
+++ 
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/sink/partitioner/index/TestIndexBackends.java
@@ -0,0 +1,56 @@
+/*
+ * 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.sink.partitioner.index;
+
+import org.apache.hudi.common.model.HoodieRecordGlobalLocation;
+
+import org.apache.flink.api.common.state.ValueState;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+class TestIndexBackends {
+
+  @Test
+  @SuppressWarnings("unchecked")
+  void testFlinkStateBackendDelegatesGetAndUpdate() throws Exception {
+    ValueState<HoodieRecordGlobalLocation> state = mock(ValueState.class);
+    HoodieRecordGlobalLocation location = 
mock(HoodieRecordGlobalLocation.class);
+    when(state.value()).thenReturn(location);
+    FlinkStateIndexBackend backend = new FlinkStateIndexBackend(state);
+
+    assertSame(location, backend.get("ignored-key"));
+    backend.update("ignored-key", location);
+    verify(state).update(location);
+    backend.close();
+  }
+
+  @Test
+  void testDummyPartitionedBackendIsNoOp() throws Exception {
+    DummyPartitionedIndexBackend backend = new DummyPartitionedIndexBackend();
+
+    assertNull(backend.get("partition", "key"));
+    backend.update("partition", "key", "file-id");
+    backend.close();
+  }
+}
diff --git 
a/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/sink/partitioner/index/TestIndexRowUtils.java
 
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/sink/partitioner/index/TestIndexRowUtils.java
new file mode 100644
index 000000000000..a3452e0560e7
--- /dev/null
+++ 
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/sink/partitioner/index/TestIndexRowUtils.java
@@ -0,0 +1,115 @@
+/*
+ * 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.sink.partitioner.index;
+
+import org.apache.hudi.client.model.HoodieFlinkInternalRow;
+import org.apache.hudi.common.model.HoodieKey;
+import org.apache.hudi.common.model.HoodieRecord;
+import org.apache.hudi.config.HoodieWriteConfig;
+import org.apache.hudi.exception.HoodieException;
+
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.StringData;
+import org.apache.flink.types.RowKind;
+import org.junit.jupiter.api.Test;
+
+import java.util.UUID;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+class TestIndexRowUtils {
+
+  @Test
+  void testCreateRecordIndexRowsForInsertAndDelete() {
+    String fileId = UUID.randomUUID().toString();
+    HoodieFlinkInternalRow insert = internalRow("I", fileId);
+    HoodieFlinkInternalRow delete = internalRow("D", fileId);
+
+    RowData insertIndexRow = IndexRowUtils.createRecordIndexRow(insert);
+    RowData deleteIndexRow = IndexRowUtils.createRecordIndexRow(delete);
+
+    assertEquals(RowKind.INSERT, insertIndexRow.getRowKind());
+    assertEquals(RowKind.DELETE, deleteIndexRow.getRowKind());
+    assertEquals(IndexRowUtils.RLI_TYPE, insertIndexRow.getByte(0));
+    assertEquals("key-1", IndexRowUtils.getRecordKey(insertIndexRow));
+    assertEquals("partition-1", IndexRowUtils.getPartition(insertIndexRow));
+    assertEquals(new HoodieKey("key-1", "partition-1"), 
IndexRowUtils.getHoodieKey(insertIndexRow));
+  }
+
+  @Test
+  void testCreateRecordIndexRowRejectsUnexpectedOperation() {
+    HoodieException exception = assertThrows(HoodieException.class,
+        () -> IndexRowUtils.createRecordIndexRow(internalRow("U", 
UUID.randomUUID().toString())));
+    assertEquals("Unexpected operation type: U", exception.getMessage());
+  }
+
+  @Test
+  void testConvertInsertAndDeleteIndexRowsToHoodieRecords() {
+    String fileId = UUID.randomUUID().toString();
+    RowData insertIndexRow = 
IndexRowUtils.createRecordIndexRow(internalRow("I", fileId));
+    RowData deleteIndexRow = 
IndexRowUtils.createRecordIndexRow(internalRow("D", fileId));
+    HoodieWriteConfig writeConfig = mock(HoodieWriteConfig.class);
+    when(writeConfig.getWritesFileIdEncoding()).thenReturn(0);
+    when(writeConfig.isRecordLevelIndexEnabled()).thenReturn(true);
+
+    HoodieRecord insertRecord = IndexRowUtils.convertToHoodieRecord(1L, 
insertIndexRow, writeConfig);
+    HoodieRecord deleteRecord = IndexRowUtils.convertToHoodieRecord(1L, 
deleteIndexRow, writeConfig);
+
+    assertNotNull(insertRecord);
+    assertNotNull(deleteRecord);
+    assertEquals("key-1", insertRecord.getRecordKey());
+    assertEquals("key-1", deleteRecord.getRecordKey());
+  }
+
+  @Test
+  void testConvertRejectsUnsupportedRowKindAndIndexType() {
+    HoodieWriteConfig writeConfig = mock(HoodieWriteConfig.class);
+    GenericRowData unsupportedKind = indexRow(IndexRowUtils.RLI_TYPE, 
RowKind.UPDATE_AFTER);
+    GenericRowData unsupportedType = indexRow((byte) 1, RowKind.INSERT);
+
+    HoodieException kindException = assertThrows(HoodieException.class,
+        () -> IndexRowUtils.convertToHoodieRecord(1L, unsupportedKind, 
writeConfig));
+    assertEquals("Unsupported operation type for index row: UPDATE_AFTER", 
kindException.getMessage());
+    HoodieException typeException = assertThrows(HoodieException.class,
+        () -> IndexRowUtils.convertToHoodieRecord(1L, unsupportedType, 
writeConfig));
+    assertEquals("Unsupported type for index row: 1", 
typeException.getMessage());
+  }
+
+  private static HoodieFlinkInternalRow internalRow(String operation, String 
fileId) {
+    HoodieFlinkInternalRow row =
+        new HoodieFlinkInternalRow("key-1", "partition-1", operation, new 
GenericRowData(0));
+    row.setFileId(fileId);
+    return row;
+  }
+
+  private static GenericRowData indexRow(byte indexType, RowKind rowKind) {
+    GenericRowData row = new 
GenericRowData(IndexRowUtils.INDEX_ROW_TYPE.getFieldCount());
+    row.setField(0, indexType);
+    row.setField(1, StringData.fromString("key-1"));
+    row.setField(2, StringData.fromString("partition-1"));
+    row.setField(3, StringData.fromString(UUID.randomUUID().toString()));
+    row.setRowKind(rowKind);
+    return row;
+  }
+}
diff --git 
a/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/sink/transform/TestTransformUtilities.java
 
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/sink/transform/TestTransformUtilities.java
new file mode 100644
index 000000000000..177800590b09
--- /dev/null
+++ 
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/sink/transform/TestTransformUtilities.java
@@ -0,0 +1,106 @@
+/*
+ * 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.sink.transform;
+
+import org.apache.hudi.client.model.HoodieFlinkInternalRow;
+import org.apache.hudi.common.model.HoodieRecord;
+import org.apache.hudi.configuration.FlinkOptions;
+import org.apache.hudi.sink.bulk.RowDataKeyGen;
+import org.apache.hudi.table.action.commit.BucketInfo;
+import org.apache.hudi.table.action.commit.BucketType;
+
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.StringData;
+import org.apache.flink.table.types.logical.RowType;
+import org.junit.jupiter.api.Test;
+
+import java.util.Arrays;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+class TestTransformUtilities {
+
+  @Test
+  void testRecordConverterBuildsFlinkRecordFromKeyAndBucket() {
+    RowDataKeyGen keyGen = mock(RowDataKeyGen.class);
+    RowData row = GenericRowData.of(1);
+    when(keyGen.getRecordKey(row)).thenReturn("record-key");
+    BucketInfo bucket = new BucketInfo(BucketType.INSERT, "file-id", 
"partition");
+
+    HoodieRecord record = RecordConverter.getInstance(keyGen).convert(row, 
bucket);
+
+    assertEquals("record-key", record.getRecordKey());
+    assertEquals("partition", record.getPartitionPath());
+    assertSame(row, record.getData());
+  }
+
+  @Test
+  void testRowDataToHoodieFunctionFactoryAndMapping() throws Exception {
+    RowType rowType = (RowType) DataTypes.ROW(
+        DataTypes.FIELD("id", DataTypes.STRING()),
+        DataTypes.FIELD("partition", DataTypes.STRING())).getLogicalType();
+    Configuration conf = new Configuration();
+    conf.set(FlinkOptions.RECORD_KEY_FIELD, "id");
+    conf.set(FlinkOptions.PARTITION_PATH_FIELD, "partition");
+
+    RowDataToHoodieFunction<RowData, HoodieFlinkInternalRow> regular =
+        RowDataToHoodieFunctions.create(rowType, conf);
+    assertEquals(RowDataToHoodieFunction.class, regular.getClass());
+
+    GenericRowData row = GenericRowData.of(
+        StringData.fromString("id-1"), StringData.fromString("p1"));
+    HoodieFlinkInternalRow result = regular.map(row);
+    assertEquals("id-1", result.getRecordKey());
+    assertEquals("p1", result.getPartitionPath());
+    assertSame(row, result.getRowData());
+
+    conf.set(FlinkOptions.WRITE_RATE_LIMIT, 10L);
+    assertInstanceOf(RowDataToHoodieFunctionWithRateLimit.class,
+        RowDataToHoodieFunctions.create(rowType, conf));
+  }
+
+  @Test
+  @SuppressWarnings("unchecked")
+  void testChainedTransformerAppliesInOrderAndReportsNames() {
+    Transformer first = mock(Transformer.class);
+    Transformer second = mock(Transformer.class);
+    DataStream<RowData> source = mock(DataStream.class);
+    DataStream<RowData> intermediate = mock(DataStream.class);
+    DataStream<RowData> result = mock(DataStream.class);
+    when(first.apply(source)).thenReturn(intermediate);
+    when(second.apply(intermediate)).thenReturn(result);
+    ChainedTransformer chained = new ChainedTransformer(Arrays.asList(first, 
second));
+
+    assertSame(result, chained.apply(source));
+    org.mockito.InOrder order = inOrder(first, second);
+    order.verify(first).apply(source);
+    order.verify(second).apply(intermediate);
+    assertEquals(Arrays.asList(first.getClass().getName(), 
second.getClass().getName()),
+        chained.getTransformersNames());
+  }
+}
diff --git 
a/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/sink/utils/TestSinkUtilities.java
 
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/sink/utils/TestSinkUtilities.java
new file mode 100644
index 000000000000..2eaa59d0f0e2
--- /dev/null
+++ 
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/sink/utils/TestSinkUtilities.java
@@ -0,0 +1,132 @@
+/*
+ * 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.sink.utils;
+
+import org.apache.hudi.exception.HoodieException;
+
+import org.apache.flink.core.memory.MemorySegment;
+import org.apache.flink.core.memory.MemorySegmentFactory;
+import org.apache.flink.runtime.jobgraph.OperatorID;
+import org.apache.flink.table.data.GenericRowData;
+import org.junit.jupiter.api.Test;
+
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class TestSinkUtilities {
+
+  @Test
+  void testNaturalOrderImplementationsUseConstantKeys() {
+    GenericRowData first = GenericRowData.of(1);
+    GenericRowData second = GenericRowData.of(2);
+    assertEquals(0, new NaturalOrderRecordComparator().compare(first, second));
+
+    NaturalOrderKeyComputer keyComputer = new NaturalOrderKeyComputer();
+    MemorySegment firstSegment = MemorySegmentFactory.wrap(new byte[] {42});
+    MemorySegment secondSegment = MemorySegmentFactory.wrap(new byte[] {24});
+    keyComputer.putKey(first, firstSegment, 0);
+
+    assertEquals(0, firstSegment.get(0));
+    assertEquals(0, keyComputer.compareKey(firstSegment, 0, secondSegment, 0));
+    keyComputer.swapKey(firstSegment, 0, secondSegment, 0);
+    assertEquals(0, firstSegment.get(0));
+    assertEquals(24, secondSegment.get(0));
+    assertEquals(1, keyComputer.getNumKeyBytes());
+    assertTrue(keyComputer.isKeyFullyDetermines());
+    assertFalse(keyComputer.invertKey());
+  }
+
+  @Test
+  void testOperatorIdGenerationIsDeterministicAndUidSpecific() {
+    OperatorID first = OperatorIDGenerator.fromUid("writer");
+
+    assertEquals(first, OperatorIDGenerator.fromUid("writer"));
+    assertNotEquals(first, OperatorIDGenerator.fromUid("committer"));
+  }
+
+  @Test
+  void testSamplingActionRunsAtEachConfiguredBoundary() {
+    SamplingActionExecutor executor = new SamplingActionExecutor(3);
+    AtomicInteger invocations = new AtomicInteger();
+
+    for (int i = 0; i < 7; i++) {
+      executor.runIfNecessary(invocations::incrementAndGet);
+    }
+
+    assertEquals(2, invocations.get());
+  }
+
+  @Test
+  void testExplicitClassloaderThreadFactoryConfiguresSingleThread() throws 
Exception {
+    ClassLoader classLoader = new ClassLoader() { };
+    AtomicReference<Throwable> failure = new AtomicReference<>();
+    Thread.UncaughtExceptionHandler handler = (thread, throwable) -> 
failure.set(throwable);
+    ExplicitClassloaderThreadFactory factory =
+        new ExplicitClassloaderThreadFactory("coordinator", classLoader, 
handler);
+    RuntimeException expected = new RuntimeException("expected");
+
+    Thread thread = factory.newThread(() -> {
+      throw expected;
+    });
+    assertEquals("coordinator", thread.getName());
+    assertSame(classLoader, thread.getContextClassLoader());
+    assertSame(handler, thread.getUncaughtExceptionHandler());
+    thread.start();
+    thread.join();
+
+    assertSame(expected, failure.get());
+    assertThrows(Error.class, () -> factory.newThread(() -> { }));
+  }
+
+  @Test
+  void testTimeWaitBuilderAndTimeout() {
+    assertThrows(NullPointerException.class, () -> TimeWait.builder().build());
+
+    TimeWait wait = TimeWait.builder()
+        .action("checkpoint acknowledgement")
+        .timeout(1)
+        .interval(1)
+        .build();
+    wait.waitFor();
+    wait.waitFor();
+
+    HoodieException timeout = assertThrows(HoodieException.class, 
wait::waitFor);
+    assertTrue(timeout.getMessage().contains("checkpoint acknowledgement"));
+  }
+
+  @Test
+  void testTimeWaitWrapsInterruptionAndPreservesCause() {
+    TimeWait wait = TimeWait.builder().action("interruptible 
action").interval(1).build();
+    Thread.currentThread().interrupt();
+    try {
+      HoodieException exception = assertThrows(HoodieException.class, 
wait::waitFor);
+      assertTrue(exception.getCause() instanceof InterruptedException);
+      assertTrue(exception.getMessage().contains("interruptible action"));
+    } finally {
+      assertFalse(Thread.currentThread().isInterrupted());
+    }
+  }
+}
diff --git 
a/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/source/enumerator/TestHoodieEnumeratorPosition.java
 
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/source/enumerator/TestHoodieEnumeratorPosition.java
new file mode 100644
index 000000000000..5d84a49b7740
--- /dev/null
+++ 
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/source/enumerator/TestHoodieEnumeratorPosition.java
@@ -0,0 +1,54 @@
+/*
+ * 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.source.enumerator;
+
+import org.apache.hudi.common.util.Option;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class TestHoodieEnumeratorPosition {
+
+  @Test
+  void testEmptyAndNullStringsProduceEmptyPosition() {
+    HoodieEnumeratorPosition empty = HoodieEnumeratorPosition.empty();
+
+    assertEquals(empty, HoodieEnumeratorPosition.of(null, ""));
+    assertFalse(empty.getIssuedInstant().isPresent());
+    assertFalse(empty.getIssuedOffset().isPresent());
+  }
+
+  @Test
+  void testStringAndOptionFactoriesPreservePosition() {
+    HoodieEnumeratorPosition fromStrings = HoodieEnumeratorPosition.of("001", 
"002");
+    HoodieEnumeratorPosition fromOptions =
+        HoodieEnumeratorPosition.of(Option.of("001"), Option.of("002"));
+
+    assertEquals(fromStrings, fromOptions);
+    assertEquals(fromStrings.hashCode(), fromOptions.hashCode());
+    assertEquals("001", fromStrings.getIssuedInstant().get());
+    assertEquals("002", fromStrings.getIssuedOffset().get());
+    assertTrue(fromStrings.toString().contains("001"));
+    assertNotEquals(fromStrings, HoodieEnumeratorPosition.empty());
+  }
+}
diff --git 
a/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/source/prune/TestPrimaryKeyPruners.java
 
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/source/prune/TestPrimaryKeyPruners.java
new file mode 100644
index 000000000000..f96574aec723
--- /dev/null
+++ 
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/source/prune/TestPrimaryKeyPruners.java
@@ -0,0 +1,75 @@
+/*
+ * 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.source.prune;
+
+import org.apache.hudi.common.table.HoodieTableMetaClient;
+import org.apache.hudi.common.util.Option;
+import org.apache.hudi.configuration.FlinkOptions;
+import org.apache.hudi.index.bucket.BucketIdentifier;
+
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.expressions.CallExpression;
+import org.apache.flink.table.expressions.FieldReferenceExpression;
+import org.apache.flink.table.expressions.ResolvedExpression;
+import org.apache.flink.table.expressions.ValueLiteralExpression;
+import org.apache.flink.table.functions.BuiltInFunctionDefinitions;
+import org.junit.jupiter.api.Test;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.Function;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.mockito.Mockito.mock;
+
+class TestPrimaryKeyPruners {
+
+  @Test
+  void testBucketIdFollowsRecordKeyFieldOrderRegardlessOfFilterOrder() {
+    Configuration conf = new Configuration();
+    conf.set(FlinkOptions.RECORD_KEY_FIELD, "id,tenant");
+    ResolvedExpression tenantFilter = equality("tenant", "tenant-a", true);
+    ResolvedExpression idFilter = equality("id", "id-1", false);
+
+    Function<Integer, Integer> bucketId =
+        PrimaryKeyPruners.getBucketIdFunc(Arrays.asList(tenantFilter, 
idFilter), conf);
+
+    List<String> orderedValues = Arrays.asList("id-1", "tenant-a");
+    assertEquals(BucketIdentifier.getBucketId(orderedValues, 8), 
bucketId.apply(8));
+    assertEquals(BucketIdentifier.getBucketId(orderedValues, 16), 
bucketId.apply(16));
+  }
+
+  @Test
+  void testPartitionBucketIdFunctionIsDisabledWithoutBucketFunction() {
+    assertNull(PartitionBucketIdFunc.create(Option.empty(), 
mock(HoodieTableMetaClient.class), 8));
+  }
+
+  private static ResolvedExpression equality(String field, String value, 
boolean literalFirst) {
+    FieldReferenceExpression fieldReference =
+        new FieldReferenceExpression(field, DataTypes.STRING(), 0, 0);
+    ValueLiteralExpression literal =
+        new ValueLiteralExpression(value, DataTypes.STRING().notNull());
+    return CallExpression.permanent(
+        BuiltInFunctionDefinitions.EQUALS,
+        literalFirst ? Arrays.asList(literal, fieldReference) : 
Arrays.asList(fieldReference, literal),
+        DataTypes.BOOLEAN());
+  }
+}
diff --git 
a/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/source/rebalance/TestStreamReadRebalance.java
 
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/source/rebalance/TestStreamReadRebalance.java
new file mode 100644
index 000000000000..9c3b1325db86
--- /dev/null
+++ 
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/source/rebalance/TestStreamReadRebalance.java
@@ -0,0 +1,96 @@
+/*
+ * 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.source.rebalance;
+
+import org.apache.hudi.common.util.Option;
+import org.apache.hudi.common.util.collection.Pair;
+import org.apache.hudi.common.util.hash.BucketIndexUtil;
+import org.apache.hudi.configuration.FlinkOptions;
+import org.apache.hudi.index.bucket.BucketIdentifier;
+import 
org.apache.hudi.source.rebalance.partitioner.StreamReadAppendPartitioner;
+import 
org.apache.hudi.source.rebalance.partitioner.StreamReadBucketIndexPartitioner;
+import org.apache.hudi.source.rebalance.selector.StreamReadAppendKeySelector;
+import 
org.apache.hudi.source.rebalance.selector.StreamReadBucketIndexKeySelector;
+import org.apache.hudi.table.format.mor.MergeOnReadInputSplit;
+
+import org.apache.flink.configuration.Configuration;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+
+class TestStreamReadRebalance {
+
+  @Test
+  void testAppendSelectorAndPartitionerUseSplitNumber() throws Exception {
+    MergeOnReadInputSplit split = newSplit(7, "partition", "00000003-file");
+
+    Integer key = new StreamReadAppendKeySelector().getKey(split);
+    assertEquals(7, key);
+    assertEquals(3, new StreamReadAppendPartitioner(4).partition(key, 128));
+  }
+
+  @Test
+  void testBucketSelectorAndPartitionerUsePartitionAndFileId() throws 
Exception {
+    String partition = "partition=par1";
+    String fileId = BucketIdentifier.newBucketFileIdPrefix(1);
+    MergeOnReadInputSplit split = newSplit(1, partition, fileId);
+    Pair<String, String> key = new 
StreamReadBucketIndexKeySelector().getKey(split);
+
+    assertEquals(Pair.of(partition, fileId), key);
+
+    Configuration conf = new Configuration();
+    conf.set(FlinkOptions.READ_TASKS, 4);
+    conf.set(FlinkOptions.BUCKET_INDEX_NUM_BUCKETS, 3);
+    StreamReadBucketIndexPartitioner partitioner = new 
StreamReadBucketIndexPartitioner(conf);
+    int actual = partitioner.partition(key, 128);
+    int expected = BucketIndexUtil.getPartitionIndexFunc(4).apply(3, 
partition, 1);
+    assertEquals(expected, actual);
+
+    String otherPartition = "partition=par2";
+    Pair<String, String> otherPartitionKey = Pair.of(otherPartition, fileId);
+    int otherPartitionResult = partitioner.partition(otherPartitionKey, 128);
+    assertEquals(
+        BucketIndexUtil.getPartitionIndexFunc(4).apply(3, otherPartition, 1),
+        otherPartitionResult);
+    assertNotEquals(actual, otherPartitionResult);
+
+    Pair<String, String> otherBucketKey =
+        Pair.of(partition, BucketIdentifier.newBucketFileIdPrefix(2));
+    int otherBucketResult = partitioner.partition(otherBucketKey, 128);
+    assertEquals(
+        BucketIndexUtil.getPartitionIndexFunc(4).apply(3, partition, 2),
+        otherBucketResult);
+    assertNotEquals(actual, otherBucketResult);
+  }
+
+  private static MergeOnReadInputSplit newSplit(int splitNumber, String 
partition, String fileId) {
+    return new MergeOnReadInputSplit(
+        splitNumber,
+        null,
+        Option.empty(),
+        "001",
+        "/tmp/table",
+        1024,
+        "payload_combine",
+        null,
+        fileId,
+        partition);
+  }
+}
diff --git 
a/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/source/split/TestSplitRequestEventModel.java
 
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/source/split/TestSplitRequestEventModel.java
new file mode 100644
index 000000000000..5d1f15f22689
--- /dev/null
+++ 
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/source/split/TestSplitRequestEventModel.java
@@ -0,0 +1,47 @@
+/*
+ * 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.source.split;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.Arrays;
+import java.util.Collections;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class TestSplitRequestEventModel {
+
+  @Test
+  void testAllConstructorsAndAccessors() {
+    SplitRequestEvent empty = new SplitRequestEvent();
+    assertTrue(empty.finishedSplitIds().isEmpty());
+    assertNull(empty.requesterHostname());
+
+    SplitRequestEvent withoutHost = new 
SplitRequestEvent(Collections.singletonList("split-1"));
+    assertEquals(Collections.singletonList("split-1"), 
withoutHost.finishedSplitIds());
+    assertNull(withoutHost.requesterHostname());
+
+    SplitRequestEvent withHost = new SplitRequestEvent(
+        Arrays.asList("split-1", "split-2"), "worker.example");
+    assertEquals(Arrays.asList("split-1", "split-2"), 
withHost.finishedSplitIds());
+    assertEquals("worker.example", withHost.requesterHostname());
+  }
+}
diff --git 
a/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/source/stats/TestColumnStatsModels.java
 
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/source/stats/TestColumnStatsModels.java
new file mode 100644
index 000000000000..c4767db0d968
--- /dev/null
+++ 
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/source/stats/TestColumnStatsModels.java
@@ -0,0 +1,59 @@
+/*
+ * 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.source.stats;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+class TestColumnStatsModels {
+
+  @Test
+  void testColumnStatsValueSemantics() {
+    ColumnStats stats = new ColumnStats(1, 9, 2);
+
+    assertEquals(1, stats.getMinVal());
+    assertEquals(9, stats.getMaxVal());
+    assertEquals(2, stats.getNullCnt());
+    assertEquals(stats, new ColumnStats(1, 9, 2));
+    assertEquals(stats.hashCode(), new ColumnStats(1, 9, 2).hashCode());
+    assertNotEquals(stats, new ColumnStats(null, 9, 2));
+    assertNull(new ColumnStats(null, null, 0).getMinVal());
+  }
+
+  @Test
+  void testColumnStatsSchemaConstantsResolveExpectedFields() {
+    assertNotNull(ColumnStatsSchemas.METADATA_SCHEMA);
+    assertNotNull(ColumnStatsSchemas.METADATA_DATA_TYPE);
+    assertNotNull(ColumnStatsSchemas.COL_STATS_DATA_TYPE);
+    assertEquals(6, ColumnStatsSchemas.COL_STATS_TARGET_POS.length);
+    int[] expectedSourcePositions = new int[6];
+    expectedSourcePositions[ColumnStatsSchemas.ORD_FILE_NAME] = 0;
+    expectedSourcePositions[ColumnStatsSchemas.ORD_MIN_VAL] = 2;
+    expectedSourcePositions[ColumnStatsSchemas.ORD_MAX_VAL] = 3;
+    expectedSourcePositions[ColumnStatsSchemas.ORD_NULL_CNT] = 5;
+    expectedSourcePositions[ColumnStatsSchemas.ORD_VAL_CNT] = 4;
+    expectedSourcePositions[ColumnStatsSchemas.ORD_COL_NAME] = 1;
+    assertArrayEquals(expectedSourcePositions, 
ColumnStatsSchemas.COL_STATS_TARGET_POS);
+  }
+}
diff --git 
a/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/table/format/TestFormatIteratorsAndIOFactory.java
 
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/table/format/TestFormatIteratorsAndIOFactory.java
new file mode 100644
index 000000000000..911daab8b7fc
--- /dev/null
+++ 
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/table/format/TestFormatIteratorsAndIOFactory.java
@@ -0,0 +1,105 @@
+/*
+ * 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.table.format;
+
+import org.apache.hudi.common.model.HoodieRecord;
+import org.apache.hudi.common.util.collection.ClosableIterator;
+import org.apache.hudi.exception.HoodieIOException;
+import org.apache.hudi.io.storage.row.HoodieRowDataFileWriterFactory;
+import org.apache.hudi.storage.HoodieStorage;
+import 
org.apache.hudi.table.format.cow.vector.reader.ParquetColumnarRowSplitReader;
+import org.apache.hudi.util.RowDataProjection;
+
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+class TestFormatIteratorsAndIOFactory {
+
+  @Test
+  void testParquetSplitIteratorDelegatesLifecycle() throws Exception {
+    ParquetColumnarRowSplitReader reader = 
mock(ParquetColumnarRowSplitReader.class);
+    RowData row = GenericRowData.of(1);
+    when(reader.reachedEnd()).thenReturn(false, true);
+    when(reader.nextRecord()).thenReturn(row);
+    ParquetSplitRecordIterator iterator = new 
ParquetSplitRecordIterator(reader);
+
+    assertTrue(iterator.hasNext());
+    assertSame(row, iterator.next());
+    assertFalse(iterator.hasNext());
+    iterator.close();
+    verify(reader).close();
+  }
+
+  @Test
+  void testParquetSplitIteratorWrapsReaderIoFailures() throws Exception {
+    ParquetColumnarRowSplitReader reader = 
mock(ParquetColumnarRowSplitReader.class);
+    when(reader.reachedEnd()).thenThrow(new IOException("read failure"));
+    ParquetSplitRecordIterator iterator = new 
ParquetSplitRecordIterator(reader);
+
+    HoodieIOException readFailure = assertThrows(HoodieIOException.class, 
iterator::hasNext);
+    assertInstanceOf(IOException.class, readFailure.getCause());
+
+    org.mockito.Mockito.doThrow(new IOException("close 
failure")).when(reader).close();
+    HoodieIOException closeFailure = assertThrows(HoodieIOException.class, 
iterator::close);
+    assertInstanceOf(IOException.class, closeFailure.getCause());
+  }
+
+  @Test
+  @SuppressWarnings("unchecked")
+  void testSchemaEvolvedIteratorProjectsAndClosesNestedIterator() {
+    ClosableIterator<RowData> nested = mock(ClosableIterator.class);
+    GenericRowData input = GenericRowData.of(1, 2);
+    when(nested.hasNext()).thenReturn(true);
+    when(nested.next()).thenReturn(input);
+    LogicalType[] projectedTypes = {DataTypes.INT().getLogicalType()};
+    RowDataProjection projection = RowDataProjection.instance(projectedTypes, 
new int[] {1});
+    SchemaEvolvedRecordIterator iterator = new 
SchemaEvolvedRecordIterator(nested, projection);
+
+    assertTrue(iterator.hasNext());
+    assertEquals(2, iterator.next().getInt(0));
+    iterator.close();
+    verify(nested).close();
+  }
+
+  @Test
+  void testFlinkIoFactoryCreatesRowDataFactories() {
+    HoodieStorage storage = mock(HoodieStorage.class);
+    HoodieFlinkIOFactory factory = new HoodieFlinkIOFactory(storage);
+
+    assertInstanceOf(HoodieRowDataFileWriterFactory.class,
+        factory.getWriterFactory(HoodieRecord.HoodieRecordType.FLINK));
+    assertInstanceOf(HoodieRowDataFileReaderFactory.class,
+        factory.getReaderFactory(HoodieRecord.HoodieRecordType.FLINK));
+  }
+}
diff --git 
a/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/table/format/mor/TestMergeOnReadTableState.java
 
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/table/format/mor/TestMergeOnReadTableState.java
new file mode 100644
index 000000000000..e7f64f1237ca
--- /dev/null
+++ 
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/table/format/mor/TestMergeOnReadTableState.java
@@ -0,0 +1,68 @@
+/*
+ * 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.table.format.mor;
+
+import org.apache.hudi.common.model.HoodieRecord;
+
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.types.logical.RowType;
+import org.junit.jupiter.api.Test;
+
+import java.util.Arrays;
+import java.util.Collections;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertSame;
+
+class TestMergeOnReadTableState {
+
+  @Test
+  void testStateExposesSchemasSplitsAndRequiredPositions() {
+    RowType rowType = (RowType) DataTypes.ROW(
+        DataTypes.FIELD("id", DataTypes.STRING()),
+        DataTypes.FIELD(HoodieRecord.OPERATION_METADATA_FIELD, 
DataTypes.STRING()),
+        DataTypes.FIELD("name", DataTypes.STRING())).getLogicalType();
+    RowType requiredRowType = (RowType) DataTypes.ROW(
+        DataTypes.FIELD("name", DataTypes.STRING()),
+        DataTypes.FIELD("id", DataTypes.STRING())).getLogicalType();
+    MergeOnReadTableState<String> state = new MergeOnReadTableState<>(
+        rowType, requiredRowType, "table-schema", "required-schema", 
Collections.singletonList("split"));
+
+    assertSame(rowType, state.getRowType());
+    assertSame(requiredRowType, state.getRequiredRowType());
+    assertEquals("table-schema", state.getTableSchema());
+    assertEquals("required-schema", state.getRequiredSchema());
+    assertEquals(Collections.singletonList("split"), state.getInputSplits());
+    assertEquals(1, state.getOperationPos());
+    assertArrayEquals(new int[] {2, 0}, state.getRequiredPositions());
+  }
+
+  @Test
+  void testMissingRequiredFieldIsReportedAsNegativePosition() {
+    RowType rowType = RowType.of(DataTypes.INT().getLogicalType(), 
DataTypes.STRING().getLogicalType());
+    RowType required = new RowType(Collections.singletonList(
+        new RowType.RowField("missing", DataTypes.STRING().getLogicalType())));
+    MergeOnReadTableState<Integer> state =
+        new MergeOnReadTableState<>(rowType, required, "schema", "required", 
Arrays.asList(1, 2));
+
+    assertEquals(-1, state.getOperationPos());
+    assertArrayEquals(new int[] {-1}, state.getRequiredPositions());
+  }
+}
diff --git 
a/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/table/lookup/TestLookupUtilities.java
 
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/table/lookup/TestLookupUtilities.java
new file mode 100644
index 000000000000..1d714bfe4496
--- /dev/null
+++ 
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/table/lookup/TestLookupUtilities.java
@@ -0,0 +1,67 @@
+/*
+ * 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.table.lookup;
+
+import 
org.apache.flink.table.connector.source.lookup.AsyncLookupFunctionProvider;
+import org.apache.flink.table.connector.source.lookup.LookupFunctionProvider;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.mockito.Mockito.mock;
+
+class TestLookupUtilities {
+
+  @Test
+  void testHeapLookupCacheStoresDuplicateKeysAndClears() throws Exception {
+    HeapLookupCache cache = new HeapLookupCache();
+    RowData key = GenericRowData.of(1);
+    RowData first = GenericRowData.of("first");
+    RowData second = GenericRowData.of("second");
+
+    assertNull(cache.getRows(key));
+    cache.addRow(key, first);
+    cache.addRow(key, second);
+    List<RowData> rows = cache.getRows(key);
+    assertEquals(2, rows.size());
+    assertEquals(first, rows.get(0));
+    assertEquals(second, rows.get(1));
+
+    cache.clear();
+    assertNull(cache.getRows(key));
+    cache.addRow(key, first);
+    cache.close();
+    assertNull(cache.getRows(key));
+  }
+
+  @Test
+  void testLookupRuntimeProviderFactorySelectsSyncAndAsyncProviders() {
+    HoodieLookupFunction function = mock(HoodieLookupFunction.class);
+
+    assertInstanceOf(LookupFunctionProvider.class,
+        LookupRuntimeProviderFactory.create(function, false, 1));
+    assertInstanceOf(AsyncLookupFunctionProvider.class,
+        LookupRuntimeProviderFactory.create(function, true, 3));
+  }
+}
diff --git 
a/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/util/TestFlinkUtilities.java
 
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/util/TestFlinkUtilities.java
new file mode 100644
index 000000000000..c883076ed04d
--- /dev/null
+++ 
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/util/TestFlinkUtilities.java
@@ -0,0 +1,156 @@
+/*
+ * 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.apache.hudi.exception.HoodieException;
+import org.apache.hudi.table.format.CastMap;
+
+import org.apache.flink.api.common.io.InputFormat;
+import org.apache.flink.contrib.streaming.state.EmbeddedRocksDBStateBackend;
+import org.apache.flink.core.io.GenericInputSplit;
+import org.apache.flink.formats.json.JsonRowDataDeserializationSchema;
+import org.apache.flink.runtime.state.hashmap.HashMapStateBackend;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.connector.sink.abilities.SupportsRowLevelDelete;
+import org.apache.flink.table.connector.sink.abilities.SupportsRowLevelUpdate;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.StringData;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.junit.jupiter.api.Test;
+
+import java.util.NoSuchElementException;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+class TestFlinkUtilities {
+
+  @Test
+  void testStateBackendConverterHandlesSupportedAndUnknownValues() {
+    FlinkStateBackendConverter converter = new FlinkStateBackendConverter();
+
+    assertInstanceOf(HashMapStateBackend.class, converter.convert("hashmap"));
+    assertInstanceOf(EmbeddedRocksDBStateBackend.class, 
converter.convert("rocksdb"));
+    HoodieException exception = assertThrows(HoodieException.class, () -> 
converter.convert("memory"));
+    assertTrue(exception.getMessage().contains("memory"));
+  }
+
+  @Test
+  @SuppressWarnings("unchecked")
+  void testEmptyInputFormatContainsNoRecords() throws Exception {
+    InputFormat<RowData, GenericInputSplit> inputFormat =
+        (InputFormat<RowData, GenericInputSplit>) 
InputFormats.EMPTY_INPUT_FORMAT;
+    GenericInputSplit[] splits = inputFormat.createInputSplits(2);
+
+    assertEquals(1, splits.length);
+    inputFormat.open(splits[0]);
+    assertTrue(inputFormat.reachedEnd());
+    assertThrows(NoSuchElementException.class, () -> 
inputFormat.nextRecord(null));
+    inputFormat.close();
+  }
+
+  @Test
+  void testRowDataProjectionPreservesKindNullsAndSelectedOrder() {
+    RowType rowType = RowType.of(
+        DataTypes.INT().getLogicalType(),
+        DataTypes.STRING().getLogicalType());
+    GenericRowData input = GenericRowData.of(7, null);
+    input.setRowKind(RowKind.DELETE);
+
+    RowDataProjection projection = RowDataProjection.instanceV2(rowType, new 
int[] {1, 0});
+    RowData projected = projection.project(input);
+
+    assertEquals(RowKind.DELETE, projected.getRowKind());
+    assertTrue(projected.isNullAt(0));
+    assertEquals(7, projected.getInt(1));
+    assertArrayEquals(new Object[] {null, 7}, 
projection.projectAsValues(input));
+  }
+
+  @Test
+  void testRowDataProjectionFactoriesAndValidation() {
+    LogicalType[] types = {
+        DataTypes.INT().getLogicalType(),
+        DataTypes.STRING().getLogicalType()
+    };
+    RowType rowType = RowType.of(types);
+    GenericRowData input = GenericRowData.of(3, 
StringData.fromString("value"));
+
+    RowData projected = RowDataProjection.instance(rowType, new int[] {0, 
1}).project(input);
+    assertEquals(3, projected.getInt(0));
+    assertEquals("value", projected.getString(1).toString());
+    assertThrows(IllegalArgumentException.class,
+        () -> RowDataProjection.instance(types, new int[] {0}));
+  }
+
+  @Test
+  void testCastProjectionHandlesValuesAndNulls() {
+    LogicalType[] types = {
+        DataTypes.INT().getLogicalType(),
+        DataTypes.STRING().getLogicalType()
+    };
+    RowDataCastProjection projection = new RowDataCastProjection(types, new 
CastMap());
+    GenericRowData input = GenericRowData.of(11, null);
+
+    RowData projected = projection.project(input);
+    assertEquals(11, projected.getInt(0));
+    assertTrue(projected.isNullAt(1));
+  }
+
+  @Test
+  void testSharedModificationAndChangelogConstants() {
+    assertTrue(ChangelogModes.FULL.contains(RowKind.UPDATE_BEFORE));
+    assertFalse(ChangelogModes.UPSERT.contains(RowKind.UPDATE_BEFORE));
+    assertTrue(ChangelogModes.UPSERT.contains(RowKind.DELETE));
+    assertEquals(
+        SupportsRowLevelDelete.RowLevelDeleteMode.DELETED_ROWS,
+        DataModificationInfos.DEFAULT_DELETE_INFO.getRowLevelDeleteMode());
+    
assertTrue(DataModificationInfos.DEFAULT_DELETE_INFO.requiredColumns().isEmpty());
+    assertEquals(
+        SupportsRowLevelUpdate.RowLevelUpdateMode.UPDATED_ROWS,
+        DataModificationInfos.DEFAULT_UPDATE_INFO.getRowLevelUpdateMode());
+    
assertTrue(DataModificationInfos.DEFAULT_UPDATE_INFO.requiredColumns().isEmpty());
+  }
+
+  @Test
+  void testJsonDeserializationFunctionFactoriesAndLifecycle() throws Exception 
{
+    RowType rowType = RowType.of(DataTypes.STRING().getLogicalType());
+    assertInstanceOf(JsonDeserializationFunction.class, 
JsonDeserializationFunction.getInstance(rowType));
+
+    JsonRowDataDeserializationSchema schema = 
mock(JsonRowDataDeserializationSchema.class);
+    RowData expected = GenericRowData.of(StringData.fromString("value"));
+    when(schema.deserialize(any(byte[].class))).thenReturn(expected);
+    JsonDeserializationFunction function = new 
JsonDeserializationFunction(schema);
+
+    function.open(new org.apache.flink.configuration.Configuration());
+    assertSame(expected, function.map("{\"f0\":\"value\"}"));
+    verify(schema).open(null);
+  }
+}

Reply via email to