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); + } +}