diff --git a/fluss-common/src/main/java/org/apache/fluss/utils/OperatingSystem.java b/fluss-common/src/main/java/org/apache/fluss/utils/OperatingSystem.java
index 166c23561a6..e761dd80f3b 100644
--- a/fluss-common/src/main/java/org/apache/fluss/utils/OperatingSystem.java
+++ b/fluss-common/src/main/java/org/apache/fluss/utils/OperatingSystem.java
@@ -57,9 +57,9 @@ public static boolean isWindows() {
}
/**
- * Checks whether the operating system this JVM runs on is Windows.
+ * Checks whether the operating system this JVM runs on is Mac OS.
*
- * @return true if the operating system this JVM runs on is Windows, false
+ * @return true if the operating system this JVM runs on is Mac OS, false
* otherwise
*/
public static boolean isMac() {
diff --git a/fluss-common/src/test/java/org/apache/fluss/utils/AbstractIteratorTest.java b/fluss-common/src/test/java/org/apache/fluss/utils/AbstractIteratorTest.java
new file mode 100644
index 00000000000..3a8ac039f4c
--- /dev/null
+++ b/fluss-common/src/test/java/org/apache/fluss/utils/AbstractIteratorTest.java
@@ -0,0 +1,119 @@
+/*
+ * 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.fluss.utils;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.NoSuchElementException;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/** Tests for {@link AbstractIterator}. */
+class AbstractIteratorTest {
+
+ /** Simple iterator over a list of integers for testing. */
+ private static class IntIterator extends AbstractIterator {
+ private final List data;
+ private int index = 0;
+
+ IntIterator(List data) {
+ this.data = data;
+ }
+
+ @Override
+ protected Integer makeNext() {
+ if (index >= data.size()) {
+ return allDone();
+ }
+ return data.get(index++);
+ }
+ }
+
+ /** Iterator that throws on first makeNext() call to test FAILED state. */
+ private static class FailingIterator extends AbstractIterator {
+ @Override
+ protected Integer makeNext() {
+ throw new RuntimeException("Intentional failure");
+ }
+ }
+
+ @Test
+ void testNormalIteration() {
+ IntIterator iter = new IntIterator(Arrays.asList(1, 2, 3));
+ List result = new ArrayList<>();
+ while (iter.hasNext()) {
+ result.add(iter.next());
+ }
+ assertThat(result).containsExactly(1, 2, 3);
+ }
+
+ @Test
+ void testEmptyIterator() {
+ IntIterator iter = new IntIterator(new ArrayList());
+ assertThat(iter.hasNext()).isFalse();
+ assertThatThrownBy(iter::next).isInstanceOf(NoSuchElementException.class);
+ }
+
+ @Test
+ void testPeek() {
+ IntIterator iter = new IntIterator(Arrays.asList(10, 20));
+ assertThat(iter.peek()).isEqualTo(10);
+ // peek does not advance
+ assertThat(iter.peek()).isEqualTo(10);
+ assertThat(iter.next()).isEqualTo(10);
+ assertThat(iter.peek()).isEqualTo(20);
+ assertThat(iter.next()).isEqualTo(20);
+ assertThatThrownBy(iter::peek).isInstanceOf(NoSuchElementException.class);
+ }
+
+ @Test
+ void testNextAfterExhaustion() {
+ IntIterator iter = new IntIterator(Arrays.asList(1));
+ iter.next();
+ assertThatThrownBy(iter::next).isInstanceOf(NoSuchElementException.class);
+ }
+
+ @Test
+ void testRemoveThrowsUnsupportedOperationException() {
+ IntIterator iter = new IntIterator(Arrays.asList(1));
+ assertThatThrownBy(iter::remove).isInstanceOf(UnsupportedOperationException.class);
+ }
+
+ @Test
+ void testMultipleHasNextCallsAreIdempotent() {
+ IntIterator iter = new IntIterator(Arrays.asList(5));
+ assertThat(iter.hasNext()).isTrue();
+ assertThat(iter.hasNext()).isTrue();
+ assertThat(iter.next()).isEqualTo(5);
+ assertThat(iter.hasNext()).isFalse();
+ assertThat(iter.hasNext()).isFalse();
+ }
+
+ @Test
+ void testFailedStateThrowsIllegalStateException() {
+ FailingIterator iter = new FailingIterator();
+ // First call triggers FAILED state via RuntimeException in makeNext()
+ assertThatThrownBy(iter::hasNext).isInstanceOf(RuntimeException.class);
+ // Subsequent calls should throw IllegalStateException (FAILED state)
+ assertThatThrownBy(iter::hasNext).isInstanceOf(IllegalStateException.class);
+ }
+}
diff --git a/fluss-common/src/test/java/org/apache/fluss/utils/CollectionUtilsTest.java b/fluss-common/src/test/java/org/apache/fluss/utils/CollectionUtilsTest.java
new file mode 100644
index 00000000000..e05c036d130
--- /dev/null
+++ b/fluss-common/src/test/java/org/apache/fluss/utils/CollectionUtilsTest.java
@@ -0,0 +1,80 @@
+/*
+ * 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.fluss.utils;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.HashMap;
+
+import static org.apache.fluss.utils.CollectionUtils.HASH_MAP_DEFAULT_LOAD_FACTOR;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/** Tests for {@link CollectionUtils}. */
+class CollectionUtilsTest {
+
+ @Test
+ void testComputeRequiredCapacity() {
+ // expectedSize <= 2 returns expectedSize + 1
+ assertThat(CollectionUtils.computeRequiredCapacity(0, 0.75f)).isEqualTo(1);
+ assertThat(CollectionUtils.computeRequiredCapacity(1, 0.75f)).isEqualTo(2);
+ assertThat(CollectionUtils.computeRequiredCapacity(2, 0.75f)).isEqualTo(3);
+
+ // expectedSize > 2 uses ceil(expectedSize / loadFactor)
+ assertThat(CollectionUtils.computeRequiredCapacity(3, 0.75f)).isEqualTo(4);
+ assertThat(CollectionUtils.computeRequiredCapacity(10, 0.75f)).isEqualTo(14);
+ assertThat(CollectionUtils.computeRequiredCapacity(100, 0.75f)).isEqualTo(134);
+
+ // Large expectedSize threshold
+ int maxThreshold = Integer.MAX_VALUE / 2 + 1;
+ assertThat(CollectionUtils.computeRequiredCapacity(maxThreshold, 0.75f))
+ .isEqualTo(Integer.MAX_VALUE);
+ assertThat(CollectionUtils.computeRequiredCapacity(Integer.MAX_VALUE, 0.75f))
+ .isEqualTo(Integer.MAX_VALUE);
+
+ // Invalid arguments
+ assertThatThrownBy(() -> CollectionUtils.computeRequiredCapacity(-1, 0.75f))
+ .isInstanceOf(IllegalArgumentException.class);
+ assertThatThrownBy(() -> CollectionUtils.computeRequiredCapacity(5, 0f))
+ .isInstanceOf(IllegalArgumentException.class);
+ assertThatThrownBy(() -> CollectionUtils.computeRequiredCapacity(5, -0.5f))
+ .isInstanceOf(IllegalArgumentException.class);
+ }
+
+ @Test
+ void testNewHashMapWithExpectedSize() {
+ HashMap map = CollectionUtils.newHashMapWithExpectedSize(5);
+ assertThat(map).isNotNull();
+ assertThat(map).isEmpty();
+
+ for (int i = 0; i < 5; i++) {
+ map.put("key" + i, i);
+ }
+ assertThat(map).hasSize(5);
+ for (int i = 0; i < 5; i++) {
+ assertThat(map.get("key" + i)).isEqualTo(i);
+ }
+
+ // Test with 0 expected size
+ HashMap emptyMap = CollectionUtils.newHashMapWithExpectedSize(0);
+ assertThat(emptyMap).isNotNull().isEmpty();
+
+ // Test default load factor constant
+ assertThat(HASH_MAP_DEFAULT_LOAD_FACTOR).isEqualTo(0.75f);
+ }
+}
diff --git a/fluss-common/src/test/java/org/apache/fluss/utils/CopyOnWriteMapTest.java b/fluss-common/src/test/java/org/apache/fluss/utils/CopyOnWriteMapTest.java
new file mode 100644
index 00000000000..db91453d0f6
--- /dev/null
+++ b/fluss-common/src/test/java/org/apache/fluss/utils/CopyOnWriteMapTest.java
@@ -0,0 +1,169 @@
+/*
+ * 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.fluss.utils;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/** Tests for {@link CopyOnWriteMap}. */
+class CopyOnWriteMapTest {
+
+ private CopyOnWriteMap map;
+
+ @BeforeEach
+ void setUp() {
+ map = new CopyOnWriteMap<>();
+ }
+
+ @Test
+ void testPutAndGet() {
+ assertThat(map.isEmpty()).isTrue();
+ assertThat(map.size()).isEqualTo(0);
+
+ map.put("a", 1);
+ assertThat(map.get("a")).isEqualTo(1);
+ assertThat(map.size()).isEqualTo(1);
+ assertThat(map.isEmpty()).isFalse();
+ }
+
+ @Test
+ void testContainsKeyAndValue() {
+ map.put("x", 42);
+ assertThat(map.containsKey("x")).isTrue();
+ assertThat(map.containsKey("y")).isFalse();
+ assertThat(map.containsValue(42)).isTrue();
+ assertThat(map.containsValue(99)).isFalse();
+ }
+
+ @Test
+ void testRemove() {
+ map.put("k", 10);
+ assertThat(map.remove("k")).isEqualTo(10);
+ assertThat(map.containsKey("k")).isFalse();
+ assertThat(map.remove("nonexistent")).isNull();
+ }
+
+ @Test
+ void testClear() {
+ map.put("a", 1);
+ map.put("b", 2);
+ map.clear();
+ assertThat(map).isEmpty();
+ assertThat(map.size()).isEqualTo(0);
+ }
+
+ @Test
+ void testPutAll() {
+ Map source = new HashMap<>();
+ source.put("p", 100);
+ source.put("q", 200);
+ map.putAll(source);
+ assertThat(map.size()).isEqualTo(2);
+ assertThat(map.get("p")).isEqualTo(100);
+ assertThat(map.get("q")).isEqualTo(200);
+ }
+
+ @Test
+ void testKeySetEntrySetValues() {
+ map.put("a", 1);
+ map.put("b", 2);
+
+ Set keys = map.keySet();
+ assertThat(keys).containsExactlyInAnyOrder("a", "b");
+
+ Set> entries = map.entrySet();
+ assertThat(entries).hasSize(2);
+
+ Collection values = map.values();
+ assertThat(values).containsExactlyInAnyOrder(1, 2);
+ }
+
+ @Test
+ void testPutIfAbsent() {
+ // Key absent: inserts and returns null
+ Integer prev = map.putIfAbsent("k", 5);
+ assertThat(prev).isNull();
+ assertThat(map.get("k")).isEqualTo(5);
+
+ // Key present: does NOT overwrite, returns existing value
+ Integer existing = map.putIfAbsent("k", 99);
+ assertThat(existing).isEqualTo(5);
+ assertThat(map.get("k")).isEqualTo(5);
+ }
+
+ @Test
+ void testRemoveKeyValue() {
+ map.put("k", 10);
+
+ // Wrong value: does not remove
+ assertThat(map.remove("k", 999)).isFalse();
+ assertThat(map.containsKey("k")).isTrue();
+
+ // Correct value: removes and returns true
+ assertThat(map.remove("k", 10)).isTrue();
+ assertThat(map.containsKey("k")).isFalse();
+ }
+
+ @Test
+ void testReplaceOldNewValue() {
+ map.put("k", 1);
+
+ // Wrong old value: no replacement
+ assertThat(map.replace("k", 99, 2)).isFalse();
+ assertThat(map.get("k")).isEqualTo(1);
+
+ // Correct old value: replaces
+ assertThat(map.replace("k", 1, 2)).isTrue();
+ assertThat(map.get("k")).isEqualTo(2);
+
+ // Non-existent key: false
+ assertThat(map.replace("missing", 1, 2)).isFalse();
+ }
+
+ @Test
+ void testReplaceExistingKey() {
+ map.put("k", 1);
+ assertThat(map.replace("k", 42)).isEqualTo(1);
+ assertThat(map.get("k")).isEqualTo(42);
+
+ // Non-existent key returns null
+ assertThat(map.replace("missing", 42)).isNull();
+ }
+
+ @Test
+ void testCopyOnWriteIsolation() {
+ map.put("a", 1);
+ // Capture a snapshot of the key set before modification
+ Set snapshotKeys = map.keySet();
+ assertThat(snapshotKeys).contains("a");
+
+ // Modify the map
+ map.put("b", 2);
+ // The snapshot should still only see what it had when captured
+ // (CopyOnWriteMap returns the underlying map's keySet, which is now the new map)
+ // The new keySet should reflect both entries
+ assertThat(map.keySet()).containsExactlyInAnyOrder("a", "b");
+ }
+}
diff --git a/fluss-common/src/test/java/org/apache/fluss/utils/EncodingUtilsTest.java b/fluss-common/src/test/java/org/apache/fluss/utils/EncodingUtilsTest.java
new file mode 100644
index 00000000000..4571687f6d5
--- /dev/null
+++ b/fluss-common/src/test/java/org/apache/fluss/utils/EncodingUtilsTest.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.fluss.utils;
+
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/** Tests for {@link EncodingUtils}. */
+class EncodingUtilsTest {
+
+ @Test
+ void testEscapeIdentifier() {
+ // Plain identifier is wrapped in backticks
+ assertThat(EncodingUtils.escapeIdentifier("myTable")).isEqualTo("`myTable`");
+ // Identifier with existing backtick gets it doubled inside the wrapper
+ assertThat(EncodingUtils.escapeIdentifier("my`Table")).isEqualTo("`my``Table`");
+ // Empty string
+ assertThat(EncodingUtils.escapeIdentifier("")).isEqualTo("``");
+ }
+
+ @Test
+ void testEscapeBackticks() {
+ // No backticks – unchanged
+ assertThat(EncodingUtils.escapeBackticks("hello")).isEqualTo("hello");
+ // Single backtick becomes double
+ assertThat(EncodingUtils.escapeBackticks("a`b")).isEqualTo("a``b");
+ // Multiple backticks
+ assertThat(EncodingUtils.escapeBackticks("a`b`c")).isEqualTo("a``b``c");
+ // Only backtick
+ assertThat(EncodingUtils.escapeBackticks("`")).isEqualTo("``");
+ // Empty string
+ assertThat(EncodingUtils.escapeBackticks("")).isEqualTo("");
+ }
+
+ @Test
+ void testEscapeSingleQuotes() {
+ // No single quotes – unchanged
+ assertThat(EncodingUtils.escapeSingleQuotes("hello")).isEqualTo("hello");
+ // Single quote becomes two single quotes
+ assertThat(EncodingUtils.escapeSingleQuotes("it's")).isEqualTo("it''s");
+ // Multiple single quotes
+ assertThat(EncodingUtils.escapeSingleQuotes("a'b'c")).isEqualTo("a''b''c");
+ // Only a single quote
+ assertThat(EncodingUtils.escapeSingleQuotes("'")).isEqualTo("''");
+ // Empty string
+ assertThat(EncodingUtils.escapeSingleQuotes("")).isEqualTo("");
+ }
+}
diff --git a/fluss-common/src/test/java/org/apache/fluss/utils/OperatingSystemTest.java b/fluss-common/src/test/java/org/apache/fluss/utils/OperatingSystemTest.java
new file mode 100644
index 00000000000..a33a45b2677
--- /dev/null
+++ b/fluss-common/src/test/java/org/apache/fluss/utils/OperatingSystemTest.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.fluss.utils;
+
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/** Tests for {@link OperatingSystem}. */
+class OperatingSystemTest {
+
+ @Test
+ void testGetCurrentOperatingSystemIsNotNull() {
+ OperatingSystem os = OperatingSystem.getCurrentOperatingSystem();
+ assertThat(os).isNotNull();
+ }
+
+ @Test
+ void testIsWindowsAndIsMacAreConsistentWithCurrentOs() {
+ OperatingSystem current = OperatingSystem.getCurrentOperatingSystem();
+
+ if (current == OperatingSystem.WINDOWS) {
+ assertThat(OperatingSystem.isWindows()).isTrue();
+ assertThat(OperatingSystem.isMac()).isFalse();
+ } else if (current == OperatingSystem.MAC_OS) {
+ assertThat(OperatingSystem.isMac()).isTrue();
+ assertThat(OperatingSystem.isWindows()).isFalse();
+ } else {
+ assertThat(OperatingSystem.isWindows()).isFalse();
+ assertThat(OperatingSystem.isMac()).isFalse();
+ }
+ }
+
+ @Test
+ void testEnumValues() {
+ OperatingSystem[] values = OperatingSystem.values();
+ assertThat(values)
+ .contains(
+ OperatingSystem.LINUX,
+ OperatingSystem.WINDOWS,
+ OperatingSystem.MAC_OS,
+ OperatingSystem.FREE_BSD,
+ OperatingSystem.SOLARIS,
+ OperatingSystem.UNKNOWN);
+ }
+
+ @Test
+ void testCurrentOsIsDetectedOnThisMachine() {
+ // On the CI runner / developer Mac we at least expect a known OS
+ OperatingSystem os = OperatingSystem.getCurrentOperatingSystem();
+ assertThat(os).isNotEqualTo(OperatingSystem.UNKNOWN);
+ }
+}
diff --git a/fluss-common/src/test/java/org/apache/fluss/utils/PropertiesUtilsTest.java b/fluss-common/src/test/java/org/apache/fluss/utils/PropertiesUtilsTest.java
new file mode 100644
index 00000000000..30b4df72836
--- /dev/null
+++ b/fluss-common/src/test/java/org/apache/fluss/utils/PropertiesUtilsTest.java
@@ -0,0 +1,98 @@
+/*
+ * 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.fluss.utils;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/** Tests for {@link PropertiesUtils}. */
+class PropertiesUtilsTest {
+
+ @Test
+ void testAsPrefixedMap() {
+ Map properties = new HashMap<>();
+ properties.put("host", "localhost");
+ properties.put("port", "8080");
+
+ Map prefixed = PropertiesUtils.asPrefixedMap(properties, "server.");
+ assertThat(prefixed).hasSize(2);
+ assertThat(prefixed.get("server.host")).isEqualTo("localhost");
+ assertThat(prefixed.get("server.port")).isEqualTo("8080");
+
+ // Empty map
+ assertThat(PropertiesUtils.asPrefixedMap(Collections.emptyMap(), "prefix."))
+ .isEmpty();
+ }
+
+ @Test
+ void testExtractAndRemovePrefix() {
+ Map original = new HashMap<>();
+ original.put("server.host", "localhost");
+ original.put("server.port", "8080");
+ original.put("client.timeout", "5000");
+
+ Map extracted = PropertiesUtils.extractAndRemovePrefix(original, "server.");
+ assertThat(extracted).hasSize(2);
+ assertThat(extracted.get("host")).isEqualTo("localhost");
+ assertThat(extracted.get("port")).isEqualTo("8080");
+ assertThat(extracted.containsKey("client.timeout")).isFalse();
+
+ // When prefix does not match any key
+ Map nonMatching =
+ PropertiesUtils.extractAndRemovePrefix(original, "database.");
+ assertThat(nonMatching).isEmpty();
+ }
+
+ @Test
+ void testExtractPrefix() {
+ Map original = new HashMap<>();
+ original.put("server.host", "localhost");
+ original.put("server.port", "8080");
+ original.put("client.timeout", "5000");
+
+ Map extracted = PropertiesUtils.extractPrefix(original, "server.");
+ assertThat(extracted).hasSize(2);
+ assertThat(extracted.get("server.host")).isEqualTo("localhost");
+ assertThat(extracted.get("server.port")).isEqualTo("8080");
+ assertThat(extracted.containsKey("client.timeout")).isFalse();
+
+ // When prefix does not match
+ assertThat(PropertiesUtils.extractPrefix(original, "database.")).isEmpty();
+ }
+
+ @Test
+ void testExcludeByPrefix() {
+ Map original = new HashMap<>();
+ original.put("server.host", "localhost");
+ original.put("server.port", "8080");
+ original.put("client.timeout", "5000");
+
+ Map remaining = PropertiesUtils.excludeByPrefix(original, "server.");
+ assertThat(remaining).hasSize(1);
+ assertThat(remaining.get("client.timeout")).isEqualTo("5000");
+
+ // When prefix matches nothing
+ Map allRemaining = PropertiesUtils.excludeByPrefix(original, "database.");
+ assertThat(allRemaining).hasSize(3);
+ }
+}