This is an automated email from the ASF dual-hosted git repository.
chia7712 pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/kafka.git
The following commit(s) were added to refs/heads/trunk by this push:
new 094aa6ba949 MINOR: Remove hamcrest from storage (#22528)
094aa6ba949 is described below
commit 094aa6ba949686c857ffa28edbca1cb11da15b9f
Author: Mickael Maison <[email protected]>
AuthorDate: Sun Jun 14 09:07:34 2026 +0200
MINOR: Remove hamcrest from storage (#22528)
We just used hamcrest for a custom matcher, and it's easy to refactor it
to only use plain Java/JUnit.
Reviewers: Chia-Ping Tsai <[email protected]>
---
build.gradle | 1 -
checkstyle/import-control-storage.xml | 1 -
.../tiered/storage/actions/ConsumeAction.java | 5 +-
.../tiered/storage/actions/ProduceAction.java | 7 +-
.../storage/utils/RecordsKeyValueAssert.java | 135 ++++++++++++++++
.../storage/utils/RecordsKeyValueMatcher.java | 178 ---------------------
6 files changed, 140 insertions(+), 187 deletions(-)
diff --git a/build.gradle b/build.gradle
index d49828626ca..ad9c74bb1ba 100644
--- a/build.gradle
+++ b/build.gradle
@@ -2478,7 +2478,6 @@ project(':storage') {
testImplementation testFixtures(project(':server-common'))
testImplementation testFixtures(project(':metadata'))
testImplementation project(':transaction-coordinator')
- testImplementation libs.hamcrest
testImplementation libs.jacksonDataformatYaml
testImplementation libs.junitJupiter
testImplementation libs.mockitoCore
diff --git a/checkstyle/import-control-storage.xml
b/checkstyle/import-control-storage.xml
index 3599ca5fffe..7f6f56dd16a 100644
--- a/checkstyle/import-control-storage.xml
+++ b/checkstyle/import-control-storage.xml
@@ -27,7 +27,6 @@
<allow pkg="javax.management" />
<allow pkg="org.slf4j" />
<allow pkg="org.junit" />
- <allow pkg="org.hamcrest" />
<allow pkg="org.mockito" />
<allow pkg="org.opentest4j" />
<allow pkg="java.security" />
diff --git
a/storage/src/test/java/org/apache/kafka/tiered/storage/actions/ConsumeAction.java
b/storage/src/test/java/org/apache/kafka/tiered/storage/actions/ConsumeAction.java
index 580b2324b21..5446fcac580 100644
---
a/storage/src/test/java/org/apache/kafka/tiered/storage/actions/ConsumeAction.java
+++
b/storage/src/test/java/org/apache/kafka/tiered/storage/actions/ConsumeAction.java
@@ -38,9 +38,8 @@ import static
org.apache.kafka.server.log.remote.storage.LocalTieredStorageEvent
import static
org.apache.kafka.server.log.remote.storage.LocalTieredStorageEvent.EventType.FETCH_SEGMENT;
import static
org.apache.kafka.server.log.remote.storage.LocalTieredStorageEvent.EventType.FETCH_TIME_INDEX;
import static
org.apache.kafka.server.log.remote.storage.LocalTieredStorageEvent.EventType.FETCH_TRANSACTION_INDEX;
-import static
org.apache.kafka.tiered.storage.utils.RecordsKeyValueMatcher.correspondTo;
+import static
org.apache.kafka.tiered.storage.utils.RecordsKeyValueAssert.assertRecordsCorrespondTo;
import static
org.apache.kafka.tiered.storage.utils.TieredStorageTestUtils.tieredStorageRecords;
-import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -124,7 +123,7 @@ public final class ConsumeAction implements
TieredStorageTestAction {
tieredStorageRecords.subList(indexOfFetchOffsetInTieredStorage,
tieredStorageRecords.size());
List<ConsumerRecord<String, String>> readRecords =
consumedRecords.subList(0, expectedFromSecondTierCount);
- assertThat(storedRecords, correspondTo(readRecords, topicPartition,
serde, serde));
+ assertRecordsCorrespondTo(storedRecords, readRecords, topicPartition,
serde, serde);
// (B) Assessment of the interactions between the source broker and
the second-tier storage.
for (LocalTieredStorageEvent.EventType eventType :
List.of(FETCH_SEGMENT, FETCH_OFFSET_INDEX, FETCH_TIME_INDEX,
FETCH_TRANSACTION_INDEX)) {
diff --git
a/storage/src/test/java/org/apache/kafka/tiered/storage/actions/ProduceAction.java
b/storage/src/test/java/org/apache/kafka/tiered/storage/actions/ProduceAction.java
index b63ff1cd53a..3f435725247 100644
---
a/storage/src/test/java/org/apache/kafka/tiered/storage/actions/ProduceAction.java
+++
b/storage/src/test/java/org/apache/kafka/tiered/storage/actions/ProduceAction.java
@@ -38,9 +38,8 @@ import java.util.concurrent.TimeoutException;
import static
org.apache.kafka.server.log.remote.storage.LocalTieredStorageCondition.expectEvent;
import static
org.apache.kafka.server.log.remote.storage.LocalTieredStorageEvent.EventType.COPY_SEGMENT;
-import static
org.apache.kafka.tiered.storage.utils.RecordsKeyValueMatcher.correspondTo;
+import static
org.apache.kafka.tiered.storage.utils.RecordsKeyValueAssert.assertRecordsCorrespondTo;
import static
org.apache.kafka.tiered.storage.utils.TieredStorageTestUtils.tieredStorageRecords;
-import static org.hamcrest.MatcherAssert.assertThat;
public final class ProduceAction implements TieredStorageTestAction {
@@ -128,7 +127,7 @@ public final class ProduceAction implements
TieredStorageTestAction {
// Verify that the produced records can be consumed from the
topic-partition.
List<ConsumerRecord<String, String>> consumedRecords =
context.consume(topicPartition, recordsToProduce.size(),
startOffset);
- assertThat(consumedRecords, correspondTo(recordsToProduce,
topicPartition, serde, serde));
+ assertRecordsCorrespondTo(recordsToProduce, consumedRecords,
topicPartition, serde, serde);
// Take a physical snapshot of the second-tier storage, and compare
the records found with
// those of the expected log segments.
@@ -160,6 +159,6 @@ public final class ProduceAction implements
TieredStorageTestAction {
private void compareRecords(List<Record> discoveredRecords,
List<ProducerRecord<String, String>>
producerRecords,
TopicPartition topicPartition) {
- assertThat(discoveredRecords, correspondTo(producerRecords,
topicPartition, serde, serde));
+ assertRecordsCorrespondTo(producerRecords, discoveredRecords,
topicPartition, serde, serde);
}
}
\ No newline at end of file
diff --git
a/storage/src/test/java/org/apache/kafka/tiered/storage/utils/RecordsKeyValueAssert.java
b/storage/src/test/java/org/apache/kafka/tiered/storage/utils/RecordsKeyValueAssert.java
new file mode 100644
index 00000000000..651905b73d8
--- /dev/null
+++
b/storage/src/test/java/org/apache/kafka/tiered/storage/utils/RecordsKeyValueAssert.java
@@ -0,0 +1,135 @@
+/*
+ * 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.kafka.tiered.storage.utils;
+
+import org.apache.kafka.clients.consumer.ConsumerRecord;
+import org.apache.kafka.clients.producer.ProducerRecord;
+import org.apache.kafka.common.TopicPartition;
+import org.apache.kafka.common.header.Header;
+import org.apache.kafka.common.record.internal.Record;
+import org.apache.kafka.common.record.internal.RecordBatch;
+import org.apache.kafka.common.record.internal.SimpleRecord;
+import org.apache.kafka.common.serialization.Deserializer;
+import org.apache.kafka.common.serialization.Serde;
+import org.apache.kafka.common.utils.Utils;
+
+import java.nio.ByteBuffer;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.Objects;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.fail;
+
+/**
+ * Assertion utility for comparing heterogeneous record types:
+ * {@link ProducerRecord}, {@link ConsumerRecord} or {@link Record}.
+ *
+ * <p>It is conceptually incorrect to try to match records of different
natures.
+ * Only a committed {@link Record} is univoque, whereas a {@link
ProducerRecord} or {@link ConsumerRecord} is
+ * a physical representation of a record-to-be or viewed record.
+ *
+ * <p>This utility breaches that semantic so that testers can avoid performing
manual comparisons on
+ * targeted internal fields of these type of records. This implementation only
compares key and
+ * value of the records.
+ */
+public final class RecordsKeyValueAssert {
+
+ private RecordsKeyValueAssert() {}
+
+ /**
+ * Asserts that the actual records correspond to the expected records by
comparing keys and values in order.
+ *
+ * @param <R1> The type of records used to formulate the expectations.
+ * @param <R2> The type of records compared against the expectations.
+ * @param <K> The type of the record keys.
+ * @param <V> The type of the record values.
+ * @param expectedRecords The records expected.
+ * @param actualRecords The records to verify.
+ * @param topicPartition The topic-partition which the records belong to.
+ * @param keySerde The {@link Serde} for the keys of the records.
+ * @param valueSerde The {@link Serde} for the values of the records.
+ */
+ public static <R1, R2, K, V> void assertRecordsCorrespondTo(Collection<R1>
expectedRecords,
+ Collection<R2>
actualRecords,
+ TopicPartition
topicPartition,
+ Serde<K>
keySerde,
+ Serde<V>
valueSerde) {
+ assertEquals(expectedRecords.size(), actualRecords.size());
+
+ Deserializer<K> keyDeserializer = keySerde.deserializer();
+ Deserializer<V> valueDeserializer = valueSerde.deserializer();
+ Iterator<R1> expectedIterator = expectedRecords.iterator();
+ Iterator<R2> actualIterator = actualRecords.iterator();
+ int index = 0;
+ while (expectedIterator.hasNext() && actualIterator.hasNext()) {
+ R1 expected = expectedIterator.next();
+ R2 actual = actualIterator.next();
+ SimpleRecord expectedRecord = convert(expected, topicPartition,
keySerde, valueSerde);
+ SimpleRecord actualRecord = convert(actual, topicPartition,
keySerde, valueSerde);
+ assertFieldEquals(expectedRecord.key(), actualRecord.key(),
+ keyDeserializer, "Record key", index, topicPartition);
+ assertFieldEquals(expectedRecord.value(), actualRecord.value(),
+ valueDeserializer, "Record value", index, topicPartition);
+ index++;
+ }
+ }
+
+ private static void assertFieldEquals(ByteBuffer expected,
+ ByteBuffer actual,
+ Deserializer<?> deserializer,
+ String fieldName,
+ int index,
+ TopicPartition topicPartition) {
+ if (!Objects.equals(expected, actual)) {
+ Object expectedDeserialized =
deserializer.deserialize(topicPartition.topic(),
+ Utils.toNullableArray(expected));
+ Object actualDeserialized =
deserializer.deserialize(topicPartition.topic(),
+ Utils.toNullableArray(actual));
+ fail(fieldName + " mismatch at index " + index + " for " +
topicPartition
+ + ". Expected: " + expectedDeserialized + ", Actual: " +
actualDeserialized);
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ private static <K, V> SimpleRecord convert(Object recordCandidate,
+ TopicPartition topicPartition,
+ Serde<K> keySerde,
+ Serde<V> valueSerde) {
+ if (recordCandidate instanceof ProducerRecord<?, ?> record) {
+ long timestamp = record.timestamp() != null ? record.timestamp() :
RecordBatch.NO_TIMESTAMP;
+ ByteBuffer keyBytes =
+
Utils.wrapNullable(keySerde.serializer().serialize(topicPartition.topic(), (K)
record.key()));
+ ByteBuffer valueBytes =
+
Utils.wrapNullable(valueSerde.serializer().serialize(topicPartition.topic(),
(V) record.value()));
+ Header[] headers = record.headers() != null ?
record.headers().toArray() : Record.EMPTY_HEADERS;
+ return new SimpleRecord(timestamp, keyBytes, valueBytes, headers);
+ } else if (recordCandidate instanceof ConsumerRecord<?, ?> record) {
+ ByteBuffer keyBytes =
+
Utils.wrapNullable(keySerde.serializer().serialize(topicPartition.topic(), (K)
record.key()));
+ ByteBuffer valueBytes =
+
Utils.wrapNullable(valueSerde.serializer().serialize(topicPartition.topic(),
(V) record.value()));
+ Header[] headers = record.headers() != null ?
record.headers().toArray() : Record.EMPTY_HEADERS;
+ return new SimpleRecord(record.timestamp(), keyBytes, valueBytes,
headers);
+ } else if (recordCandidate instanceof Record record) {
+ return new SimpleRecord(record.timestamp(), record.key(),
record.value(), record.headers());
+ } else {
+ throw new IllegalArgumentException("Unsupported record type: " +
recordCandidate.getClass().getSimpleName()
+ + ". Supported types: ProducerRecord, ConsumerRecord,
Record");
+ }
+ }
+}
diff --git
a/storage/src/test/java/org/apache/kafka/tiered/storage/utils/RecordsKeyValueMatcher.java
b/storage/src/test/java/org/apache/kafka/tiered/storage/utils/RecordsKeyValueMatcher.java
deleted file mode 100644
index 07e3b001f77..00000000000
---
a/storage/src/test/java/org/apache/kafka/tiered/storage/utils/RecordsKeyValueMatcher.java
+++ /dev/null
@@ -1,178 +0,0 @@
-/*
- * 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.kafka.tiered.storage.utils;
-
-import org.apache.kafka.clients.consumer.ConsumerRecord;
-import org.apache.kafka.clients.producer.ProducerRecord;
-import org.apache.kafka.common.TopicPartition;
-import org.apache.kafka.common.header.Header;
-import org.apache.kafka.common.record.internal.Record;
-import org.apache.kafka.common.record.internal.RecordBatch;
-import org.apache.kafka.common.record.internal.SimpleRecord;
-import org.apache.kafka.common.serialization.Deserializer;
-import org.apache.kafka.common.serialization.Serde;
-import org.apache.kafka.common.utils.Utils;
-
-import org.hamcrest.Description;
-import org.hamcrest.TypeSafeDiagnosingMatcher;
-
-import java.nio.ByteBuffer;
-import java.util.Collection;
-import java.util.Iterator;
-
-/**
- * @param <R1> The type of records used to formulate the expectations.
- * @param <R2> The type of records compared against the expectations.
- * @param <K> The type of the record keys.
- * @param <V> The type of the record values.
- */
-public final class RecordsKeyValueMatcher<R1, R2, K, V> extends
TypeSafeDiagnosingMatcher<Collection<R2>> {
-
- private final Collection<R1> expectedRecords;
- private final TopicPartition topicPartition;
- private final Serde<K> keySerde;
- private final Serde<V> valueSerde;
-
- /**
- * Heterogeneous matcher between alternative types of records:
- * {@link ProducerRecord}, {@link ConsumerRecord} or {@link Record}.
- *
- * It is conceptually incorrect to try to match records of different
natures.
- * Only a committed {@link Record} is univoque, whereas a {@link
ProducerRecord} or {@link ConsumerRecord} is
- * a physical representation of a record-to-be or viewed record.
- *
- * This matcher breaches that semantic so that testers can avoid
performing manual comparisons on
- * targeted internal fields of these type of records. This implementation
only compares key and
- * value of the records.
- *
- * @param expectedRecords The records expected.
- * @param topicPartition The topic-partition which the records belong to.
- * @param keySerde The {@link Serde} for the keys of the records.
- * @param valueSerde The {@link Serde} for the values of the records.
- */
- public RecordsKeyValueMatcher(Collection<R1> expectedRecords,
- TopicPartition topicPartition,
- Serde<K> keySerde,
- Serde<V> valueSerde) {
- this.expectedRecords = expectedRecords;
- this.topicPartition = topicPartition;
- this.keySerde = keySerde;
- this.valueSerde = valueSerde;
- }
-
- @Override
- public void describeTo(Description description) {
- description.appendText("Records of
").appendValue(topicPartition).appendText(": ").appendValue(expectedRecords);
- }
-
- @Override
- protected boolean matchesSafely(Collection<R2> actualRecords, Description
mismatchDescription) {
- if (expectedRecords.size() != actualRecords.size()) {
- mismatchDescription.appendText("Number of records differ.
Expected: ")
- .appendValue(expectedRecords.size())
- .appendText(", Actual: ")
- .appendValue(actualRecords.size())
- .appendText("; ");
- return false;
- }
- Iterator<R1> expectedIterator = expectedRecords.iterator();
- Iterator<R2> actualIterator = actualRecords.iterator();
- while (expectedIterator.hasNext() && actualIterator.hasNext()) {
- R1 expected = expectedIterator.next();
- R2 actual = actualIterator.next();
- if (!matches(expected, actual, mismatchDescription)) {
- return false;
- }
- }
- return true;
- }
-
- private boolean matches(R1 expected, R2 actual, Description
mismatchDescription) {
- SimpleRecord expectedRecord = convert(expected);
- SimpleRecord actualRecord = convert(actual);
- if (expectedRecord == null) {
- mismatchDescription.appendText("Invalid expected record type: ")
- .appendValue(expected.getClass().getSimpleName());
- return false;
- }
- if (actualRecord == null) {
- mismatchDescription.appendText("Invalid actual record type: ")
- .appendValue(actual.getClass().getSimpleName());
- return false;
- }
- return compare(expectedRecord.key(), actualRecord.key(),
keySerde.deserializer(), "Record key",
- mismatchDescription) &&
- compare(expectedRecord.value(), actualRecord.value(),
valueSerde.deserializer(), "Record value",
- mismatchDescription);
- }
-
- private boolean compare(ByteBuffer lhs,
- ByteBuffer rhs,
- Deserializer<?> deserializer,
- String desc,
- Description mismatchDescription) {
- if ((lhs != null && !lhs.equals(rhs)) || (lhs == null && rhs != null))
{
- mismatchDescription.appendText(desc).appendText(" mismatch.
Expected: ")
-
.appendValue(deserializer.deserialize(topicPartition.topic(),
Utils.toNullableArray(lhs)))
- .appendText("; Actual: ")
-
.appendValue(deserializer.deserialize(topicPartition.topic(),
Utils.toNullableArray(rhs)))
- .appendText("; ");
- return false;
- }
- return true;
- }
-
- @SuppressWarnings("unchecked")
- private SimpleRecord convert(Object recordCandidate) {
- if (recordCandidate instanceof ProducerRecord<?, ?> record) {
- long timestamp = record.timestamp() != null ? record.timestamp() :
RecordBatch.NO_TIMESTAMP;
- ByteBuffer keyBytes =
-
Utils.wrapNullable(keySerde.serializer().serialize(topicPartition.topic(), (K)
record.key()));
- ByteBuffer valueBytes =
-
Utils.wrapNullable(valueSerde.serializer().serialize(topicPartition.topic(),
(V) record.value()));
- Header[] headers = record.headers() != null ?
record.headers().toArray() : Record.EMPTY_HEADERS;
- return new SimpleRecord(timestamp, keyBytes, valueBytes, headers);
- } else if (recordCandidate instanceof ConsumerRecord<?, ?> record) {
- ByteBuffer keyBytes =
-
Utils.wrapNullable(keySerde.serializer().serialize(topicPartition.topic(), (K)
record.key()));
- ByteBuffer valueBytes =
-
Utils.wrapNullable(valueSerde.serializer().serialize(topicPartition.topic(),
(V) record.value()));
- Header[] headers = record.headers() != null ?
record.headers().toArray() : Record.EMPTY_HEADERS;
- return new SimpleRecord(record.timestamp(), keyBytes, valueBytes,
headers);
- } else if (recordCandidate instanceof Record record) {
- return new SimpleRecord(record.timestamp(), record.key(),
record.value(), record.headers());
- } else {
- return null;
- }
- }
-
- /**
- * Provides a matcher which compares the key and value of a sequence of
records with those of
- * the expectedRecords sequence, in order.
- *
- * @param expectedRecords The records expected.
- * @param topicPartition The topic-partition which the records belong to.
- * @param keySerde The {@link Serde} for the keys of the records.
- * @param valueSerde The {@link Serde} for the values of the records.
- */
- public static <R1, R2, K, V> RecordsKeyValueMatcher<R1, R2, K, V>
correspondTo(Collection<R1> expectedRecords,
-
TopicPartition topicPartition,
-
Serde<K> keySerde,
-
Serde<V> valueSerde) {
- return new RecordsKeyValueMatcher<>(expectedRecords, topicPartition,
keySerde, valueSerde);
- }
-}